@cosyte/astm 0.0.15 → 0.0.18

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,237 @@ 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";
241
+ /**
242
+ * Two escape alignments of the same bytes disagreed about a **repeat** boundary, the reading
243
+ * taken kept it, and the escape character that reading resumes on **heads no sequence at all**.
244
+ * Same contested position and same tail test as
245
+ * {@link WARNING_CODES.ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS}, on a different delimiter role, and
246
+ * what it costs is a different thing, which is why it is a different code.
247
+ *
248
+ * **Nothing shifts, and that is not the same as nothing being lost.** No field-indexed slot
249
+ * moves: the units and the result status are read out of the same field numbers under either
250
+ * alignment. What this reports is that **the field is read as more repeats than the competing
251
+ * alignment gives it**, which is true wherever it fires. What that costs depends on **which**
252
+ * boundary was gained, and the two cases are stated separately rather than folded into one claim.
253
+ *
254
+ * **Where it is the FIRST boundary in the field, a modeled slot is lost**, because a field's
255
+ * modeled value and components are taken from its **first repeat alone**. Everything past the
256
+ * boundary stays on the wire and stays in `repeats`, and leaves every modeled slot:
257
+ *
258
+ * - **A value truncates.** Under `H|F^&`, `R|1|^^^687|28.6&S&F&U/L||||F` reads the value field as
259
+ * the two repeats `28.6^` and `&U/L`, so a consumer reads `28.6^` and the rest of the value is
260
+ * gone. The competing alignment reads one repeat carrying all of it.
261
+ * - **A modeled component list is deleted rather than shifted.** The same boundary inside a
262
+ * Universal Test ID leaves the components of the first repeat only: under `H|F^&`,
263
+ * `R|1|&F&F&687|28.6|U/L||||F` reads a UTID of one component holding a decoded field separator,
264
+ * with no coding scheme and no local code, where the bytes carry `687`. A patient name loses
265
+ * its given and middle names the same way. That is the half of this condition the shift report
266
+ * could not cover, because components are modeled **inside** a field.
267
+ *
268
+ * Before this code the only warnings on those streams were **tolerable** ones
269
+ * ({@link WARNING_CODES.ASTM_NONSTANDARD_DELIMITERS} and, where the tail applies,
270
+ * {@link WARNING_CODES.ASTM_UNPAIRED_ESCAPE_CHARACTER}), so the widest gate-legal profile plus
271
+ * `{ strict: true }` accepted a truncated value and an emptied Universal Test ID.
272
+ *
273
+ * **Where it is a LATER boundary, no modeled slot moves and this still fires.** The first repeat
274
+ * is identical under both alignments, so the value, the components and every slot taken from them
275
+ * read the same either way (`R|1|^^^687|5.0F28.6&S&F&U/L||||F` under `H|F^&` reads a value of
276
+ * `5.0` under both), and what differs is the repeat structure after the first. It fires there
277
+ * deliberately: the boundary is still one the bytes do not force and a consumer reading `repeats`
278
+ * is still reading an alignment guess. Relative to the modeled slots that is **over-reporting,
279
+ * never under-reporting**. Narrowing it to the first boundary would change which streams a
280
+ * published package refuses and wants its own measurement, so the bound is written down instead.
281
+ *
282
+ * **It is a report, not a repair.** The split is unchanged, every decoded byte is identical,
283
+ * `repeats` still carries every one of them, and the value read is the value that was always
284
+ * read. Picking the other alignment would be a different guess with no more evidence behind it,
285
+ * on a published package.
286
+ *
287
+ * **Two further deliberate bounds, both stated rather than left to be found.**
288
+ * - **Only the repeat role.** The **component** role reaches a modeled slot too, and differently:
289
+ * there the components stay in the record and move one slot along, so a local code reads as a
290
+ * coding scheme and a given name as a middle name. That is measured, reported by nothing where
291
+ * the earlier body is a recognized mnemonic, and open. It is not covered here, because wiring a
292
+ * sink to another split is another criterion wanting its own population measurement.
293
+ * - **The tail is weighed one construct deep**, exactly as for the shift report. Where the escape
294
+ * character the reading resumes on heads a sequence this codec *recognizes*, the bytes prefer
295
+ * the reading taken and the repeats are the ones the sender wrote: under a set naming the
296
+ * repeat separator `F`, `28.6&F&F&F&U/L` is that separator escaped, written, and escaped again,
297
+ * and refusing it would be an over-refusal of a well-formed stream. Where it heads a sequence
298
+ * whose body is *unrecognized*, the competing alignment would leave **two** escape characters
299
+ * bare, so the preference is stronger again, and this stays silent there even though the gained
300
+ * boundary is real. That last case is the named residue: measured, not overlooked.
301
+ *
302
+ * **Catch it on the first read.** Emit rewrites the preserved sequences into recognized
303
+ * mnemonics, and those bytes carry the reading that was taken unambiguously, so a
304
+ * second-generation read is silent and is correct about its own bytes. A clean re-read is not
305
+ * evidence.
306
+ */
307
+ readonly ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD: "ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD";
89
308
  /** An escape sequence body was not one of `&F&`/`&S&`/`&R&`/`&E&`: preserved verbatim. */
90
309
  readonly ASTM_UNKNOWN_ESCAPE_SEQUENCE: "ASTM_UNKNOWN_ESCAPE_SEQUENCE";
91
310
  /**
@@ -107,7 +326,8 @@ declare const WARNING_CODES: {
107
326
  * *different* escape character in the same record may still head a real three-character sequence,
108
327
  * and if that sequence's body happens to be a delimiter (`&|&` under the canonical set) the atom
109
328
  * 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.
329
+ * {@link WARNING_CODES.ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE}, and it still costs a field
330
+ * boundary.
111
331
  */
112
332
  readonly ASTM_UNPAIRED_ESCAPE_CHARACTER: "ASTM_UNPAIRED_ESCAPE_CHARACTER";
113
333
  /**
@@ -173,7 +393,8 @@ declare const WARNING_CODES: {
173
393
  */
174
394
  readonly ASTM_RECORD_DELIMITERS_REDECLARED: "ASTM_RECORD_DELIMITERS_REDECLARED";
175
395
  /**
176
- * A later `H` (header) record could not declare a usable delimiter set: it was too short, or the
396
+ * A later `H` (header) record could not declare a usable delimiter set: it was not a header, it
397
+ * was too short, its delimiter definition held fewer than three characters, or the
177
398
  * field separator it named also appeared among the other three, leaving the four roles
178
399
  * indistinguishable. The delimiters **already in force are kept** and every record is still surfaced;
179
400
  * a set is never guessed and no record is dropped.
@@ -283,6 +504,106 @@ declare function fieldsUnseparated(position: AstmPosition): AstmRecordWarning;
283
504
  * ```
284
505
  */
285
506
  declare function nonStandardDelimiters(position: AstmPosition): AstmRecordWarning;
507
+ /**
508
+ * Build an `ASTM_RECORD_DELIMITER_ROLE_COLLISION` warning. Emitted when a header
509
+ * declares one character in two of the repeat / component / escape roles. The
510
+ * declaration is honored and no record is dropped; the message names no character,
511
+ * because a delimiter is a byte off the wire.
512
+ *
513
+ * A profile may **not** tolerate this code: it reports a distinction the bytes no
514
+ * longer carry, and the only other warning such a set raises
515
+ * ({@link WARNING_CODES.ASTM_NONSTANDARD_DELIMITERS}) is tolerable.
516
+ *
517
+ * @example
518
+ * ```ts
519
+ * import { delimiterRoleCollision } from "@cosyte/astm";
520
+ * delimiterRoleCollision({ recordIndex: 0, recordType: "H" });
521
+ * ```
522
+ */
523
+ declare function delimiterRoleCollision(position: AstmPosition): AstmRecordWarning;
524
+ /**
525
+ * Build an `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE` warning. Emitted when an
526
+ * unrecognized escape body is itself a splitting delimiter in force, so the atom
527
+ * rule kept it out of the split. The value is preserved verbatim and is identical
528
+ * with the warning and without it; what the warning reports is the boundary that
529
+ * did not happen.
530
+ *
531
+ * A profile may **not** tolerate this code. It fires alongside
532
+ * {@link WARNING_CODES.ASTM_UNKNOWN_ESCAPE_SEQUENCE}, which remains tolerable and
533
+ * reports the strictly weaker fact that a body was not recognized.
534
+ *
535
+ * @example
536
+ * ```ts
537
+ * import { delimiterSwallowedByEscape } from "@cosyte/astm";
538
+ * delimiterSwallowedByEscape({ recordIndex: 4, recordType: "R", fieldIndex: 4 });
539
+ * ```
540
+ */
541
+ declare function delimiterSwallowedByEscape(position: AstmPosition): AstmRecordWarning;
542
+ /**
543
+ * Build an `ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT` warning. Emitted when the escape
544
+ * character that closed an unrecognized escape sequence could instead have opened one
545
+ * holding the delimiter that was split on, so the bytes carry two alignments that
546
+ * disagree about that boundary. The leftmost alignment is kept and every byte is
547
+ * preserved; what the warning reports is that the boundary is a choice.
548
+ *
549
+ * A profile may **not** tolerate this code. It fires alongside
550
+ * {@link WARNING_CODES.ASTM_UNKNOWN_ESCAPE_SEQUENCE}, which remains tolerable and
551
+ * reports the strictly weaker fact that a body was not recognized.
552
+ *
553
+ * @example
554
+ * ```ts
555
+ * import { ambiguousEscapeAlignment } from "@cosyte/astm";
556
+ * ambiguousEscapeAlignment({ recordIndex: 4, recordType: "R", fieldIndex: 4 });
557
+ * ```
558
+ */
559
+ declare function ambiguousEscapeAlignment(position: AstmPosition): AstmRecordWarning;
560
+ /**
561
+ * Build an `ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS` warning. Emitted when a competing escape
562
+ * alignment decided a **field** boundary and the escape character the reading taken resumes on
563
+ * heads no sequence of its own, so every field after that point sits one place further right than
564
+ * the competing alignment puts it. On a result record that reaches the units and the **result
565
+ * status**: a trailing status letter lands in field 9 under the reading taken and in no field at
566
+ * all under the competing one.
567
+ *
568
+ * A profile may **not** tolerate this code. It fires alongside
569
+ * {@link WARNING_CODES.ASTM_UNPAIRED_ESCAPE_CHARACTER}, which remains tolerable and reports the
570
+ * strictly weaker fact that one escape character was read as a literal, and alongside
571
+ * {@link WARNING_CODES.ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT} where that also applies. The reading
572
+ * is unchanged: this reports the shift, it does not repair it.
573
+ *
574
+ * @example
575
+ * ```ts
576
+ * import { alignmentShiftedFields } from "@cosyte/astm";
577
+ * alignmentShiftedFields({ recordIndex: 2, recordType: "R", fieldIndex: 4 });
578
+ * ```
579
+ */
580
+ declare function alignmentShiftedFields(position: AstmPosition): AstmRecordWarning;
581
+ /**
582
+ * Build an `ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD` warning. Emitted when a competing escape
583
+ * alignment decided a **repeat** boundary and the escape character the reading taken resumes on
584
+ * heads no sequence of its own, so the field is read as more repeats than the competing alignment
585
+ * gives it. No field-indexed slot moves, and that is the difference from
586
+ * {@link alignmentShiftedFields}. Where the gained boundary is the **first** in the field it still
587
+ * reaches a modeled slot, because a field's modeled value and components are taken from its first
588
+ * repeat alone: everything past the boundary stays in `repeats` and leaves every modeled slot, so a
589
+ * result value truncates and a Universal Test ID or a patient name loses the components that sat
590
+ * after it. At a **later** boundary nothing modeled moves and this still fires, which is
591
+ * over-reporting relative to those slots and never under-reporting; see
592
+ * {@link WARNING_CODES.ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD}.
593
+ *
594
+ * A profile may **not** tolerate this code. It fires alongside
595
+ * {@link WARNING_CODES.ASTM_UNPAIRED_ESCAPE_CHARACTER}, which remains tolerable and reports the
596
+ * strictly weaker fact that one escape character was read as a literal, and alongside
597
+ * {@link WARNING_CODES.ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT} where that also applies. The reading
598
+ * is unchanged: this reports the gained boundary, it does not repair it.
599
+ *
600
+ * @example
601
+ * ```ts
602
+ * import { alignmentTruncatedField } from "@cosyte/astm";
603
+ * alignmentTruncatedField({ recordIndex: 2, recordType: "R", fieldIndex: 4 });
604
+ * ```
605
+ */
606
+ declare function alignmentTruncatedField(position: AstmPosition): AstmRecordWarning;
286
607
  /**
287
608
  * Build an `ASTM_RECORD_DELIMITERS_REDECLARED` warning. Emitted when a later `H`
288
609
  * record declares a delimiter set different from the one in force; the new set is
@@ -328,8 +649,10 @@ declare function unknownEscapeSequence(position: AstmPosition): AstmRecordWarnin
328
649
  *
329
650
  * **It is a statement about that one character, not about the record.** A
330
651
  * 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}.
652
+ * sequence, and if that sequence's body is an unrecognized character that is itself a delimiter in
653
+ * force, that delimiter does not
654
+ * split, which is reported separately by
655
+ * {@link WARNING_CODES.ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE}.
333
656
  *
334
657
  * A profile **may** tolerate this code: the value it reports is byte-identical
335
658
  * with the warning and without it, because reading the character as a literal is
@@ -528,13 +851,39 @@ interface Delimiters {
528
851
  * ```
529
852
  */
530
853
  declare const CANONICAL_DELIMITERS: Delimiters;
854
+ /**
855
+ * Why a header record could not declare a usable delimiter set. Four distinct
856
+ * conditions, kept distinct because a consumer is told which one it was and two of
857
+ * them describe records that are **not** short.
858
+ *
859
+ * - `not-a-header`: the record does not begin with an `H` type letter.
860
+ * - `record-too-short`: fewer than five characters, so `H` plus a field separator
861
+ * plus a three-character definition cannot fit.
862
+ * - `definition-truncated`: the delimiter-definition field runs to the next field
863
+ * separator (or to the end of the record) and holds fewer than three characters.
864
+ * **This is the one a full-length header reaches**, and it is what a declaration
865
+ * naming its own field separator among the other three produces, because that
866
+ * separator ends the definition where it appears.
867
+ * - `field-separator-reused`: the field separator is also the repeat, component or
868
+ * escape character.
869
+ */
870
+ type DelimiterDeclarationFault = "not-a-header" | "record-too-short" | "definition-truncated" | "field-separator-reused";
871
+ /** The result of reading delimiters from a header record: either resolved or a named failure. */
872
+ type DelimiterReadResult = {
873
+ readonly ok: true;
874
+ readonly delimiters: Delimiters;
875
+ } | {
876
+ readonly ok: false;
877
+ readonly fault: DelimiterDeclarationFault;
878
+ };
531
879
  /**
532
880
  * Read the four delimiters from a header record's raw text (a single `H`
533
881
  * record, its terminator already stripped).
534
882
  *
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.
883
+ * Returns `undefined` when the record cannot declare all four delimiters. The four
884
+ * ways that happens are named by {@link readDelimiterDeclaration}, which is this
885
+ * function with the reason kept; use it wherever the reason is shown to a consumer,
886
+ * because "too short" is false of some of them.
538
887
  *
539
888
  * What the caller does with that depends on **which** header it is. On the first
540
889
  * header it is the `ASTM_RECORD_UNDECLARED_DELIMITERS` fatal, because there is no
@@ -543,6 +892,12 @@ declare const CANONICAL_DELIMITERS: Delimiters;
543
892
  * and an `ASTM_RECORD_UNREADABLE_REDECLARATION` warning is raised instead; a set is
544
893
  * never guessed and no record is dropped.
545
894
  *
895
+ * **A set two of whose other three roles share a character is resolved, not
896
+ * refused**, because the stream is still readable and refusing it would drop
897
+ * records the sender did send. What it costs is the boundary between those two
898
+ * roles, which the bytes no longer carry: see {@link hasCollidingRoles}, which
899
+ * the parse path calls to report it (`ASTM_RECORD_DELIMITER_ROLE_COLLISION`).
900
+ *
546
901
  * This function does not throw: delimiter resolution and the escalation decision are
547
902
  * kept separate so the reader stays pure and testable.
548
903
  *
@@ -556,6 +911,68 @@ declare const CANONICAL_DELIMITERS: Delimiters;
556
911
  * ```
557
912
  */
558
913
  declare function readDelimiters(headerRecord: string): Delimiters | undefined;
914
+ /**
915
+ * The same read as {@link readDelimiters}, keeping **why** it failed.
916
+ *
917
+ * The reason is not cosmetic. A caller that reports every failure as "too short"
918
+ * says something false about `H||^&`, a full-length header whose definition field is
919
+ * empty because its own field separator ends it, and a consumer reading that
920
+ * diagnostic looks for the wrong thing. The two conditions are separate rules that
921
+ * happen to coincide today (see the fault list on
922
+ * {@link DelimiterDeclarationFault}), so they are named separately.
923
+ *
924
+ * **`field-separator-reused` is unreachable through this function today, and the
925
+ * check stays.** A field separator occurring in the definition positions sits at
926
+ * index 2, 3 or 4, so it ends the definition where it appears and the definition is
927
+ * under three characters: `definition-truncated` answers first, every time. The
928
+ * outcome is right either way (such a declaration does not resolve at all). The
929
+ * check is the statement of an invariant the length rule currently enforces on its
930
+ * behalf, and the two are not the same rule: a change to how the definition field is
931
+ * bounded would separate them again. Deleting it would leave the invariant
932
+ * unstated and the change silent.
933
+ *
934
+ * @param headerRecord - The raw `H` record text (no trailing CR/LF).
935
+ * @returns The resolved delimiters, or the named reason they could not be read.
936
+ * @example
937
+ * ```ts
938
+ * import { readDelimiterDeclaration } from "@cosyte/astm";
939
+ * readDelimiterDeclaration("H|\\^&").ok; // true
940
+ * readDelimiterDeclaration("H||^&"); // { ok: false, fault: "definition-truncated" }
941
+ * ```
942
+ */
943
+ declare function readDelimiterDeclaration(headerRecord: string): DelimiterReadResult;
944
+ /**
945
+ * Whether a resolved set names one character in two roles, so the boundary
946
+ * between those two roles cannot be recovered from the bytes.
947
+ *
948
+ * {@link readDelimiters} already refuses a declaration whose **field** separator
949
+ * is one of the other three (it returns `undefined`, which is the
950
+ * `ASTM_RECORD_UNDECLARED_DELIMITERS` fatal on the first header and the
951
+ * `ASTM_RECORD_UNREADABLE_REDECLARATION` warning on a later one), so what is left
952
+ * to test here is the three unordered pairs among the remaining roles:
953
+ * **repeat/component, repeat/escape, and component/escape**. Three pairs, named
954
+ * rather than counted, because the count is only meaningful with the list.
955
+ *
956
+ * Such a declaration is read and honored (nothing is guessed, no record is
957
+ * dropped) and the loss it causes is real and was previously silent. Measured on
958
+ * the canonical-looking `H|^^&`, where the repeat and component roles are both
959
+ * `^`: the field `A^B^C^D` reads back as **four repeats of one component each**,
960
+ * so a two-repeats-of-two-components reading is unrecoverable and `components`
961
+ * holds only `A`. On `H|\&&`, where the component and escape roles are both `&`,
962
+ * the same character splits (`A&B` reads as two components) or opens an atom
963
+ * (`A&F&B` reads as the single component `A|B`) depending only on what follows
964
+ * it. Emit refuses such a set outright (`ASTM_EMIT_INVALID_DELIMITERS`).
965
+ *
966
+ * @param d - A resolved delimiter set.
967
+ * @returns `true` iff two of the repeat / component / escape roles share a character.
968
+ * @example
969
+ * ```ts
970
+ * import { hasCollidingRoles, readDelimiters } from "@cosyte/astm";
971
+ * hasCollidingRoles(readDelimiters("H|\\^&")!); // false
972
+ * hasCollidingRoles(readDelimiters("H|^^&")!); // true (repeat === component)
973
+ * ```
974
+ */
975
+ declare function hasCollidingRoles(d: Delimiters): boolean;
559
976
  /**
560
977
  * Whether a resolved delimiter set differs from the canonical `H|\^&`. A `true`
561
978
  * result is a tolerated vendor quirk, surfaced as a value-free warning so a
@@ -2116,10 +2533,18 @@ declare function attachComments(records: readonly AstmRecord[], warnings: AstmRe
2116
2533
  *
2117
2534
  * **What is still not guaranteed.** Together the two checks guarantee that every
2118
2535
  * record re-reads as its own *type*. They do not guarantee that every *field* of it
2119
- * lands where it did: an escape sequence whose body is itself a delimiter is read as
2536
+ * lands where it did: an escape sequence whose body is an unrecognized character that is itself a
2537
+ * delimiter in force is read as
2120
2538
  * one opaque atom, so that delimiter never becomes a boundary and the fields after it
2121
- * shift. That is reported on the parse side (`ASTM_UNKNOWN_ESCAPE_SEQUENCE`) and is
2122
- * not what these refusals cover.
2539
+ * shift. That is reported on the parse side, by `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE`
2540
+ * (which no profile may tolerate) alongside the tolerable `ASTM_UNKNOWN_ESCAPE_SEQUENCE`, and is
2541
+ * not what these refusals cover. Nor is the mirror of it: sequences are matched leftmost, so one
2542
+ * can end where another could have begun and the delimiter between them splits where the competing
2543
+ * alignment would have held it, reported by `ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT`. **Neither
2544
+ * report survives this serializer**, by construction rather than by oversight: it rewrites the
2545
+ * preserved characters into recognized mnemonics, so the emitted stream carries the reading that
2546
+ * was taken with nothing ambiguous left in it, and a second-generation parse is silent and correct
2547
+ * about its own bytes. Catch both on the first read of the wire bytes.
2123
2548
  */
2124
2549
 
2125
2550
  /**
@@ -2273,9 +2698,14 @@ declare function serializeAstmRecord(record: AstmRecord, d?: Delimiters): string
2273
2698
  * therefore checked to start with the letter the record models, and one that does
2274
2699
  * not is `ASTM_EMIT_TYPE_LETTER_COLLISION` rather than a stream that reads back as
2275
2700
  * different records. What neither check promises is that every *field* lands where
2276
- * it did: an escape sequence whose body is a delimiter is an opaque atom, so that
2701
+ * it did: an escape sequence whose body is an unrecognized character that is itself a delimiter in
2702
+ * force is an opaque atom, so that
2277
2703
  * delimiter never becomes a boundary, and the parse side reports that
2278
- * (`ASTM_UNKNOWN_ESCAPE_SEQUENCE`) rather than emit refusing it.
2704
+ * (`ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE`, plus the tolerable
2705
+ * `ASTM_UNKNOWN_ESCAPE_SEQUENCE`) rather than emit refusing it. The mirror case, where a sequence
2706
+ * ends where another could have begun so a delimiter splits that the competing alignment would
2707
+ * have held, is reported the same way (`ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT`). Emitting
2708
+ * normalizes both away rather than preserving them, so neither reaches a second generation.
2279
2709
  *
2280
2710
  * @param input - A parsed {@link AstmMessage} or a list of {@link AstmRecord}s.
2281
2711
  * @param d - The delimiters to emit against; defaults to the canonical `H|\^&` set.
@@ -2896,6 +3326,27 @@ declare function classifyMessage(records: readonly AstmRecord[]): AstmMessageCla
2896
3326
  * unrecognized escape sequence encountered, so the caller can warn.
2897
3327
  * @param onUnpairedEscape - Called (with the 0-based field index) for each escape
2898
3328
  * character that heads no escape sequence and was therefore read as a literal.
3329
+ * @param onSwallowedDelimiter - Called (with the 0-based field index) for each
3330
+ * unrecognized escape sequence whose body is a splitting delimiter in force, so
3331
+ * that delimiter never became a boundary.
3332
+ * @param onAmbiguousAlignment - Called (with the 0-based field index) for each
3333
+ * unrecognized escape sequence whose closing escape character could instead have
3334
+ * opened one holding the delimiter that split, so the boundary is one of two
3335
+ * readings the bytes carry.
3336
+ * @param onAlignmentShiftedFields - Called (with the 0-based field index) for each
3337
+ * contested **field** boundary the reading took while resuming on an escape
3338
+ * character that heads no sequence, so every later field is one place further
3339
+ * right than the competing alignment puts it. Wired only to the field split: a
3340
+ * repeat or component boundary divides one field and so moves no field-indexed
3341
+ * slot. That is a choice and not a consequence, because components are modeled
3342
+ * inside a field; see {@link ShiftedFieldsSink}.
3343
+ * @param onAlignmentTruncatedField - Called (with the 0-based field index) for that
3344
+ * same condition on the **repeat** split, where no field-indexed slot moves and the
3345
+ * field is instead read as more repeats than the competing alignment gives it.
3346
+ * Where that boundary is the **first** in the field the field's own modeled reading
3347
+ * stops at it, because a field is modeled out of its first repeat alone; at a later
3348
+ * one nothing modeled moves and this still fires. See {@link TruncatedFieldSink}.
3349
+ * The component split is wired to neither, deliberately.
2899
3350
  * @returns The record's fields.
2900
3351
  * @example
2901
3352
  * ```ts
@@ -2904,7 +3355,7 @@ declare function classifyMessage(records: readonly AstmRecord[]): AstmMessageCla
2904
3355
  * fields[3].components[0]; // "28.6"
2905
3356
  * ```
2906
3357
  */
2907
- declare function tokenizeRecord(record: string, d: Delimiters, onUnknownEscape?: (fieldIndex: number) => void, onUnpairedEscape?: (fieldIndex: number) => void): AstmField[];
3358
+ 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, onAlignmentTruncatedField?: (fieldIndex: number) => void): AstmField[];
2908
3359
  /**
2909
3360
  * Tokenize an `H` (header) record into its fields.
2910
3361
  *
@@ -2929,6 +3380,20 @@ declare function tokenizeRecord(record: string, d: Delimiters, onUnknownEscape?:
2929
3380
  * @param onUnpairedEscape - Called with the 0-based whole-record field index for
2930
3381
  * each unpaired escape character in the data portion. The declaration itself is
2931
3382
  * opaque, so the escape character it names never reports here.
3383
+ * @param onSwallowedDelimiter - Called with the 0-based whole-record field index
3384
+ * for each unrecognized escape sequence in the data portion whose body is a
3385
+ * splitting delimiter in force. The declaration is opaque, so the delimiters it
3386
+ * names literally never report here.
3387
+ * @param onAmbiguousAlignment - Called with the 0-based whole-record field index for
3388
+ * each competing escape alignment in the data portion. The declaration is opaque,
3389
+ * so the characters it names literally never report here either.
3390
+ * @param onAlignmentShiftedFields - Called with the 0-based whole-record field index
3391
+ * for each contested field boundary in the data portion whose reading resumes on
3392
+ * an escape character heading no sequence. The declaration is opaque, so it never
3393
+ * reports here either.
3394
+ * @param onAlignmentTruncatedField - Called with the 0-based whole-record field index
3395
+ * for each contested **repeat** boundary in the data portion on that same tail
3396
+ * test. The declaration is opaque, so it never reports here either.
2932
3397
  * @returns The header's fields.
2933
3398
  * @example
2934
3399
  * ```ts
@@ -2938,7 +3403,7 @@ declare function tokenizeRecord(record: string, d: Delimiters, onUnknownEscape?:
2938
3403
  * fields[4].raw; // "sender"
2939
3404
  * ```
2940
3405
  */
2941
- declare function tokenizeHeader(record: string, d: Delimiters, onUnknownEscape?: (fieldIndex: number) => void, onUnpairedEscape?: (fieldIndex: number) => void): AstmField[];
3406
+ 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, onAlignmentTruncatedField?: (fieldIndex: number) => void): AstmField[];
2942
3407
  /**
2943
3408
  * The primary scalar of a field: its first repeat's first component, decoded.
2944
3409
  * Returns `undefined` for a truly empty field so callers can distinguish
@@ -2990,7 +3455,19 @@ declare const FATAL_CODES: {
2990
3455
  readonly EMPTY_INPUT: "EMPTY_INPUT";
2991
3456
  /** The first record is not an `H` (header) record: an ASTM message must lead with `H`. */
2992
3457
  readonly ASTM_RECORD_NO_HEADER: "ASTM_RECORD_NO_HEADER";
2993
- /** The `H` record is too short to declare the four delimiters (field/repeat/component/escape). */
3458
+ /**
3459
+ * The first `H` record could not declare all four delimiters (field/repeat/component/escape).
3460
+ * One code, four reasons, and the message says which: the record is not a header, it is shorter
3461
+ * than a header plus a three-character definition, its definition field holds fewer than three
3462
+ * characters before the next field separator, or its field separator is also one of the other
3463
+ * three. Only the second of those is "too short", so read the message rather than assuming it.
3464
+ *
3465
+ * **Two of the four cannot be reached on this fatal, and are not dead code.** A first record that
3466
+ * is not an `H` raises `ASTM_RECORD_NO_HEADER` before the declaration is ever read, and a field
3467
+ * separator reused among the other three ends the delimiter definition where it appears, so the
3468
+ * truncation reason answers first. The reader names all four because it is also called directly
3469
+ * and on later headers, where the same conditions are a warning rather than this fatal.
3470
+ */
2994
3471
  readonly ASTM_RECORD_UNDECLARED_DELIMITERS: "ASTM_RECORD_UNDECLARED_DELIMITERS";
2995
3472
  };
2996
3473
  /**
@@ -3065,14 +3542,63 @@ declare class AstmParseError extends Error {
3065
3542
  * next escape character with no bound, which meant a single unescaped `&` in a
3066
3543
  * value merged every field after it into that value, in silence.
3067
3544
  *
3068
- * **The atom rule is unchanged and still costs a boundary in one case.** A triple
3069
- * is opaque by design, so where its body is itself a delimiter (`&|&` under the
3070
- * canonical set) that delimiter does not split. That is deliberate: it is what
3071
- * keeps `&F&` one token under a declared set that names `F` as a delimiter. Where
3072
- * the body is not a recognized mnemonic the parse layer reports it as
3073
- * `ASTM_UNKNOWN_ESCAPE_SEQUENCE`, so it is never silent, but that code is
3074
- * tolerable and a field boundary has still gone. Do not "fix" that by narrowing
3075
- * the atom: the narrowing would break the guarantee the atom exists for.
3545
+ * **The atom rule is unchanged and still costs a boundary in one case, which now
3546
+ * has a report of its own.** A triple is opaque by design, so where its body is
3547
+ * itself a delimiter (`&|&` under the canonical set) that delimiter does not
3548
+ * split. That is deliberate: it is what keeps `&F&` one token under a declared
3549
+ * set that names `F` as a delimiter. Where the body is not a recognized mnemonic
3550
+ * the parse layer reports it as `ASTM_UNKNOWN_ESCAPE_SEQUENCE`, and where that
3551
+ * unrecognized body is *also* one of the three splitting roles in force it
3552
+ * reports `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE` **as well**, which is the
3553
+ * code that says a boundary went missing rather than merely that a body was not
3554
+ * recognized. Do not "fix" this by narrowing the atom: the narrowing would break
3555
+ * the guarantee the atom exists for. The split is unchanged; what changed is that
3556
+ * the loss is now reported by a code no profile may tolerate.
3557
+ *
3558
+ * **The match is greedy and leftmost, and that costs a boundary in the opposite
3559
+ * direction, which also has a report of its own.** Triples are taken left to
3560
+ * right, so the escape character that *closes* one triple cannot also *open* the
3561
+ * next. Where it could have (the character after a triple is a splitting
3562
+ * delimiter and the one after that is the escape character), the bytes carry two
3563
+ * alignments that disagree about that delimiter: under the reading taken it ends
3564
+ * a field, repeat or component, and under the other it sits inside an opaque
3565
+ * atom and ends nothing. Where the earlier triple's body was **not** a
3566
+ * recognized mnemonic, the reading taken rests on a triple this codec cannot
3567
+ * interpret at all, so nothing in its own vocabulary prefers that reading to the
3568
+ * competing one, and the parse layer reports
3569
+ * `ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT`. The split is unchanged
3570
+ * here too: the leftmost reading is kept, and what is new is that a boundary
3571
+ * which may not be the sender's is no longer reported only by codes a profile
3572
+ * may tolerate. Where that earlier body **was** a recognized mnemonic nothing is
3573
+ * reported, and the reason is narrower than it looks, so read
3574
+ * {@link AmbiguousAlignmentSink} before widening it.
3575
+ *
3576
+ * **A second question is asked at the same position, and it is a different one,
3577
+ * which is why it is a second code and not a widening of the first.** That report
3578
+ * asks whether this codec's vocabulary prefers the reading taken *at* the contested
3579
+ * position. It says nothing about what the reading taken makes of the bytes *after*
3580
+ * the boundary, and that is where the two alignments really diverge: they resume
3581
+ * one character apart, so they disagree about the whole tail. Where the escape
3582
+ * character the reading taken resumes on heads no sequence at all, the boundary was
3583
+ * bought with a byte this reading cannot read, while the competing alignment is
3584
+ * precisely the reading that can. On the **field** separator that gained boundary
3585
+ * shifts every later field one place, so a result record's units and **result
3586
+ * status** are read out of slots the competing alignment does not put them in, and
3587
+ * a status of `final` can be a consequence of the alignment rather than something
3588
+ * the sender wrote there. That is `ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS`; see
3589
+ * {@link ShiftedFieldsSink}, including the tail it deliberately stays silent on, and
3590
+ * why that silence is not a claim that nothing was lost there.
3591
+ *
3592
+ * **The same question asked on the repeat separator has a different answer, so it
3593
+ * has a code of its own.** A gained repeat boundary moves no field-indexed slot, and
3594
+ * where it is the **first** boundary in the field it still reaches a modeled slot,
3595
+ * because a field's modeled reading is taken from its first repeat alone: everything
3596
+ * past that boundary leaves the record, so a result value truncates and a Universal
3597
+ * Test ID or a patient name loses the components that sat after it. That is
3598
+ * `ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD`; see {@link TruncatedFieldSink}, including
3599
+ * what it does at a later boundary, where it fires and no modeled slot moves. The
3600
+ * **component** separator is a third question again and is deliberately not answered
3601
+ * here, because there the components do not disappear, they shift.
3076
3602
  *
3077
3603
  * Re-escaping (the inverse, for spec-clean emit) lives in the serializer and is
3078
3604
  * deliberately not implemented here.
@@ -3092,6 +3618,195 @@ type UnknownEscapeSink = () => void;
3092
3618
  * `ASTM_UNPAIRED_ESCAPE_CHARACTER` warning. Optional so the codec can be used purely.
3093
3619
  */
3094
3620
  type UnpairedEscapeSink = () => void;
3621
+ /**
3622
+ * A callback the codec calls when an **unrecognized** escape body is itself one of
3623
+ * the three splitting delimiters in force (field, repeat, component), so the atom
3624
+ * rule kept that character out of the split and a boundary the sender's bytes
3625
+ * carried never became one. The value is preserved verbatim either way; the
3626
+ * callback lets the parser surface a value-free
3627
+ * `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE` warning. Optional so the codec can
3628
+ * be used purely.
3629
+ *
3630
+ * The **escape** character is deliberately not in that test: it is not a splitting
3631
+ * role, so `&&&` under the canonical set loses no boundary. A body that is a
3632
+ * *recognized* mnemonic is not in it either, whatever character it is: `&F&` under
3633
+ * a set naming `F` as the repeat delimiter is the escaped field delimiter the
3634
+ * sender wrote, and reading it as a swallowed repeat boundary would report the
3635
+ * escape mechanism working as a defect.
3636
+ */
3637
+ type SwallowedDelimiterSink = () => void;
3638
+ /**
3639
+ * A callback the split calls when the escape character that **closed** an
3640
+ * unrecognized escape sequence could instead have **opened** one whose body is the
3641
+ * delimiter being split on, so the two alignments of the same bytes disagree about
3642
+ * whether that delimiter ends a field, repeat or component. The leftmost reading is
3643
+ * kept and nothing is re-split; the callback lets the parser surface a value-free
3644
+ * `ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT` warning. Optional so the split can be
3645
+ * used purely.
3646
+ *
3647
+ * Two exclusions, both deliberate. The first is what keeps this off conformant
3648
+ * streams, and it is **wider than the case it is justified on**, so the residue is
3649
+ * named here rather than left to be found:
3650
+ *
3651
+ * - **The earlier sequence's body must not be a recognized mnemonic.** The test is
3652
+ * which alignment this codec's own vocabulary supports, not which one is tidier.
3653
+ * Where the earlier body is unrecognized the reading taken rests on a triple the
3654
+ * codec cannot interpret, while the competitor's body is the delimiter character,
3655
+ * which it usually cannot interpret either: nothing prefers one, and that is what
3656
+ * this reports. Where the earlier body is a recognized mnemonic the reading taken
3657
+ * interprets a construct (`&F&` is the sender escaping a field separator, which is
3658
+ * what the mechanism is for) and the competitor **usually** interprets none, so the
3659
+ * vocabulary usually prefers one, and reporting it would report the escape
3660
+ * mechanism working. Usually, not always: see the second residue below.
3661
+ * **What that argument does not cover, measured.** It does not follow that the
3662
+ * reading taken is *conformant*: under `28.6&F&|&U/L` it is `&F&`, a real
3663
+ * separator, and then a bare escape character, which this package reports as a
3664
+ * deviation of its own. And where the declared set names a mnemonic letter as a
3665
+ * splitting delimiter, both alignments interpret exactly one construct and
3666
+ * neither is preferred, yet the exclusion still silences it. Both residues are
3667
+ * recorded with their measurements rather than closed by widening this test: the
3668
+ * criterion that would cover them is a different one (counting what each
3669
+ * alignment interprets), and swapping criteria moves which streams a published
3670
+ * package refuses.
3671
+ * - **The following character must be the delimiter this split is taken on.** The
3672
+ * split runs once per role, so a character that is a delimiter in a *later* role
3673
+ * is reported by that role's pass, on the segment it survives into, and never
3674
+ * twice. A delimiter with no escape character two positions past it is excluded by
3675
+ * the rule that defines the condition rather than by a judgement: the sequence the
3676
+ * competing alignment would need never closes, so there is no competitor.
3677
+ *
3678
+ * @param segmentIndex - The 0-based index of the segment being accumulated when the
3679
+ * ambiguity was seen, so a caller splitting a record into fields can report which
3680
+ * field it sat in. A caller splitting one field into repeats or components
3681
+ * already knows the field index and ignores this.
3682
+ */
3683
+ type AmbiguousAlignmentSink = (segmentIndex: number) => void;
3684
+ /**
3685
+ * A callback the split calls when a competing escape alignment decided a boundary
3686
+ * **and the reading taken cannot read the byte immediately past it**: the escape
3687
+ * character the leftmost reading resumes on heads no escape sequence at all, so it
3688
+ * is kept as a bare literal (reported separately as
3689
+ * `ASTM_UNPAIRED_ESCAPE_CHARACTER`), while the competing alignment is exactly the
3690
+ * reading that gives that character a job, as the close of its own triple.
3691
+ *
3692
+ * Wired **only to the split taken on the field separator**, because that is the
3693
+ * whole of its claim: a gained *field* boundary shifts every later field one place,
3694
+ * so a record's modeled slots after it are decided by the alignment rather than by
3695
+ * the sender's own positions. On a result record that is the units slot and the
3696
+ * **result status** slot: the sender's trailing letter lands in field 9 under the
3697
+ * reading taken and in no field at all under the competing one, so a status of
3698
+ * `final` can be a consequence of the alignment rather than something the sender
3699
+ * put there. A gained *repeat* or *component* boundary divides one field and
3700
+ * reaches nothing outside it, so it moves no **field-indexed** slot and is
3701
+ * deliberately outside this. **That bound is a choice, not a consequence, and the
3702
+ * difference matters**: components are modeled *inside* a field (a Universal Test
3703
+ * ID's coding scheme and local code, a patient name's parts), so a gained repeat or
3704
+ * component boundary does reach a modeled slot. The **repeat** half of that is
3705
+ * reported by {@link TruncatedFieldSink}, on the same tail test and under its own
3706
+ * code, because what it costs is a different thing. The **component** half is
3707
+ * measured, reported by nothing, and open: not something either sink covers. The
3708
+ * callback lets the parser surface a value-free
3709
+ * `ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS` warning. Optional so the split can be used
3710
+ * purely.
3711
+ *
3712
+ * **It is a report, not a repair.** The split is unchanged, every decoded byte is
3713
+ * identical, and the status read is the status that was always read. What is new is
3714
+ * that the shift is reported by a code no profile may tolerate, where before it was
3715
+ * covered only by tolerable ones.
3716
+ *
3717
+ * **This is independent of {@link AmbiguousAlignmentSink}, and fires alongside it
3718
+ * rather than instead of it.** That one asks whether the codec's vocabulary prefers
3719
+ * the reading taken *at* the contested position and is silent where the earlier
3720
+ * body is a recognized mnemonic; this one asks what the reading taken makes of the
3721
+ * bytes *after* the boundary and does not consult the earlier body at all. The two
3722
+ * questions are different, so neither test is widened to answer the other.
3723
+ *
3724
+ * **The tail is weighed one construct deep, and that bound is stated rather than
3725
+ * left to be found.** Two tails are excluded, with the same reason: the bytes past
3726
+ * the boundary prefer the reading taken.
3727
+ *
3728
+ * - **The escape character heads a sequence this codec recognizes.** Then the
3729
+ * reading taken interprets it and leaves nothing bare, while the competing
3730
+ * alignment would leave it bare. Under a set naming the field separator `F`,
3731
+ * `28.6&F&F&F&U/L` is the sender escaping that separator, writing it, and
3732
+ * escaping it again: entirely well formed, and refusing it is the over-refusal
3733
+ * that sank the preceding candidate criterion for this family.
3734
+ * - **The escape character heads a sequence whose body this codec does not
3735
+ * recognize.** Then the reading taken still consumes it as a sequence and carries
3736
+ * one unreadable body, while the competing alignment would leave *two* escape
3737
+ * characters bare. The preference is stronger there, not weaker.
3738
+ *
3739
+ * What that bound leaves open is the second of those tails, where the field shift
3740
+ * is real and this stays silent. It is measured and named rather than closed by
3741
+ * widening the test, because widening it would report a boundary the bytes prefer.
3742
+ *
3743
+ * @param segmentIndex - The 0-based index of the field being accumulated when the
3744
+ * shifting boundary was taken. Every field after it is one place further right
3745
+ * than the competing alignment puts it.
3746
+ */
3747
+ type ShiftedFieldsSink = (segmentIndex: number) => void;
3748
+ /**
3749
+ * A callback the split calls on **exactly the condition {@link ShiftedFieldsSink}
3750
+ * reports**, wired instead to the split taken on the **repeat** separator. The two
3751
+ * sinks test the same predicate, because the predicate is about the bytes and the
3752
+ * split does not know which role it is being taken on; they are separate so the
3753
+ * caller that does know can name what the gained boundary costs, which is not the
3754
+ * same thing in the two roles. Do not "simplify" them into one: the codes would then
3755
+ * have to make one claim covering both, and the claim is different.
3756
+ *
3757
+ * **What a gained repeat boundary costs, and it is not a shift.** No field-indexed
3758
+ * slot moves: the units slot and the result-status slot are read out of the same
3759
+ * field numbers under either alignment. What the report says is that **the field is
3760
+ * read as more repeats than the competing alignment gives it**, and that is true
3761
+ * wherever it fires. What that costs depends on **which** boundary was gained, and
3762
+ * the two cases are stated separately rather than folded into one claim:
3763
+ *
3764
+ * - **The gained boundary is the FIRST in the field, and then a modeled slot is
3765
+ * lost.** A field's modeled value and its components are taken from its **first
3766
+ * repeat alone**, so everything past the boundary is still on the wire, still in
3767
+ * `repeats`, and gone from every modeled slot. Under a set naming the repeat
3768
+ * separator `F`, the value `28.6&S&F&U/L` reads as the two repeats `28.6^` and
3769
+ * `&U/L`, and every value extractor answers `28.6^`, while the competing alignment
3770
+ * reads one repeat carrying all of it. The same boundary inside a Universal Test ID
3771
+ * field leaves the components of the first repeat only, so a UTID whose bytes carry
3772
+ * a test code can come back as a single component holding a decoded delimiter, with
3773
+ * the coding scheme and local code read from nothing at all. A patient name loses
3774
+ * its given and middle names the same way.
3775
+ * - **The gained boundary is a LATER one, and then no modeled slot moves.** The first
3776
+ * repeat is identical under both alignments, so the value, the components and every
3777
+ * slot taken from them read the same either way; what differs is the repeat
3778
+ * structure after the first. **This still fires there, deliberately and measurably**
3779
+ * (`5.0F28.6&S&F&U/L` under that same set), because the boundary is still one the
3780
+ * bytes do not force and a consumer reading `repeats` is still reading an alignment
3781
+ * guess. Relative to the modeled slots that is **over-reporting, never
3782
+ * under-reporting**, which is the direction this package errs in. Narrowing the sink
3783
+ * to the first boundary would change which streams a published package refuses and
3784
+ * wants its own measurement, so the bound is written down instead of guessed at.
3785
+ *
3786
+ * **It is a report, not a repair.** The split is unchanged, every decoded byte is
3787
+ * identical, `repeats` still carries every one of them, and the value read is the
3788
+ * value that was always read. What is new is that the gained boundary is reported by
3789
+ * a code no profile may tolerate, where before it was covered only by tolerable ones.
3790
+ *
3791
+ * **The tail is weighed one construct deep, exactly as in {@link ShiftedFieldsSink},
3792
+ * and for the same reason.** Where the escape character the reading taken resumes on
3793
+ * heads a sequence this codec recognizes, the bytes prefer the reading taken and the
3794
+ * two repeats are the ones the sender wrote: under a set naming the repeat separator
3795
+ * `F`, `28.6&F&F&F&U/L` is that separator escaped, written, and escaped again.
3796
+ * Refusing that is the over-refusal that sank an earlier candidate criterion for this
3797
+ * family. Where it heads a sequence whose body is *unrecognized*, the competing
3798
+ * alignment would leave two escape characters bare, so the preference is stronger
3799
+ * again. **The truncation in that second case is real and this stays silent about
3800
+ * it**: a named residue, measured, not overlooked, and not closed by widening the
3801
+ * test, because widening it would report a boundary the bytes prefer.
3802
+ *
3803
+ * @param segmentIndex - The 0-based index of the repeat being accumulated when the
3804
+ * gained boundary was taken, so `0` is the boundary that ends the first repeat and
3805
+ * is the one that reaches a modeled slot. It is **not** a field index: the caller
3806
+ * splitting one field into repeats already knows which field it is in, and reports
3807
+ * that.
3808
+ */
3809
+ type TruncatedFieldSink = (segmentIndex: number) => void;
3095
3810
  /**
3096
3811
  * Decode the four recognized ASTM escape mnemonics in a single already-split
3097
3812
  * leaf (a component string), substituting the *active* delimiters read from the
@@ -3106,6 +3821,8 @@ type UnpairedEscapeSink = () => void;
3106
3821
  * @param d - The delimiters resolved from the header.
3107
3822
  * @param onUnknown - Called once per unrecognized escape body encountered.
3108
3823
  * @param onUnpaired - Called once per escape character that heads no sequence.
3824
+ * @param onSwallowedDelimiter - Called once per unrecognized escape body that is
3825
+ * itself a splitting delimiter in force, so that delimiter never split.
3109
3826
  * @returns The decoded string.
3110
3827
  * @example
3111
3828
  * ```ts
@@ -3114,7 +3831,7 @@ type UnpairedEscapeSink = () => void;
3114
3831
  * decodeEscapes("O&Brien", CANONICAL_DELIMITERS); // "O&Brien" (literal, reported)
3115
3832
  * ```
3116
3833
  */
3117
- declare function decodeEscapes(leaf: string, d: Delimiters, onUnknown?: UnknownEscapeSink, onUnpaired?: UnpairedEscapeSink): string;
3834
+ declare function decodeEscapes(leaf: string, d: Delimiters, onUnknown?: UnknownEscapeSink, onUnpaired?: UnpairedEscapeSink, onSwallowedDelimiter?: SwallowedDelimiterSink): string;
3118
3835
  /**
3119
3836
  * Split `text` on `delimiter`, treating an escape sequence (escape, one body
3120
3837
  * character, escape) as an opaque atom so a delimiter that appears *inside* an
@@ -3132,11 +3849,38 @@ declare function decodeEscapes(leaf: string, d: Delimiters, onUnknown?: UnknownE
3132
3849
  * closes is what used to merge the whole remainder of a record into one field.
3133
3850
  * Decoding the resulting leaf is what reports it, so this function stays a pure
3134
3851
  * split. Note the two rules together: a delimiter after such a character does
3135
- * split, and a delimiter sitting inside a real three-character atom does not.
3852
+ * split, and a delimiter sitting inside a real three-character atom does not. The
3853
+ * second of those is reported, from the decode step rather than from here,
3854
+ * whenever the atom's body was not a recognized mnemonic.
3855
+ *
3856
+ * **Atoms are matched greedily, leftmost first, and where that choice decides a
3857
+ * boundary it is reported from here.** The escape character closing one triple
3858
+ * cannot also open the next, so `28.6&Z&|&U/L` is read as the atom `&Z&`, then a
3859
+ * field separator that splits, and not as `&Z`, then the atom `&|&` whose field
3860
+ * separator would not have split. Both alignments are in the bytes and they
3861
+ * disagree by one boundary. The reading is **not** changed (that would only pick
3862
+ * the other alignment, with no more evidence for it); it is reported through
3863
+ * `onAmbiguousAlignment`, and only where the earlier body was unrecognized, since
3864
+ * a recognized mnemonic is a construct this codec can interpret and the
3865
+ * competitor's body usually is not. That exclusion is wider than that argument:
3866
+ * see {@link AmbiguousAlignmentSink}.
3136
3867
  *
3137
3868
  * @param text - The field or repeat string to split.
3138
3869
  * @param delimiter - The delimiter to split on.
3139
3870
  * @param escape - The active escape character.
3871
+ * @param onAmbiguousAlignment - Called once per unrecognized escape sequence whose
3872
+ * closing escape character could instead have opened a sequence holding this
3873
+ * delimiter, so the boundary taken here is not the only reading of the bytes.
3874
+ * @param onShiftedFields - Called once per contested boundary where the escape
3875
+ * character this reading resumes on heads no sequence of its own, so the boundary
3876
+ * was bought with a byte the reading cannot read. Wired only by the split taken on
3877
+ * the field separator: see {@link ShiftedFieldsSink} for why, and for the tail it
3878
+ * deliberately does not report.
3879
+ * @param onTruncatedField - Called on that same condition, wired only by the split
3880
+ * taken on the repeat separator, where the cost is not a shift but a field read as
3881
+ * more repeats than the competing alignment gives it. Where that boundary is the
3882
+ * **first** in the field the field's modeled reading stops at it; at a later one
3883
+ * nothing modeled moves and this still fires. See {@link TruncatedFieldSink}.
3140
3884
  * @returns The raw segments, in order.
3141
3885
  * @example
3142
3886
  * ```ts
@@ -3146,7 +3890,7 @@ declare function decodeEscapes(leaf: string, d: Delimiters, onUnknown?: UnknownE
3146
3890
  * splitEscapeAware("O&Brien^John", "^", "&"); // ["O&Brien", "John"] (unpaired: literal)
3147
3891
  * ```
3148
3892
  */
3149
- declare function splitEscapeAware(text: string, delimiter: string, escape: string): string[];
3893
+ declare function splitEscapeAware(text: string, delimiter: string, escape: string, onAmbiguousAlignment?: AmbiguousAlignmentSink, onShiftedFields?: ShiftedFieldsSink, onTruncatedField?: TruncatedFieldSink): string[];
3150
3894
 
3151
3895
  /**
3152
3896
  * Deep-freeze helper for the immutable ASTM model.
@@ -3479,9 +4223,81 @@ declare function resolveProfileTransport(profile: AstmProfile | undefined): Astm
3479
4223
  * field separator was absent outright, so it is a reader of record structure that
3480
4224
  * cannot be relied on as a sweep.
3481
4225
  *
4226
+ * **A fourth and a fifth reader of record structure landed together, and the
4227
+ * allow-list was re-derived against them.** `ASTM_RECORD_DELIMITER_ROLE_COLLISION`
4228
+ * reports a header declaring one character in two of the repeat / component /
4229
+ * escape roles, so the boundary between those two roles is not in the bytes;
4230
+ * `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE` reports an unrecognized escape body
4231
+ * that is itself a splitting delimiter, so the atom rule kept it out of the split.
4232
+ * Both are safety-critical by construction, because that side is computed rather
4233
+ * than listed. What they required was re-reading the survivors below against them,
4234
+ * and two of the four needed their arguments rewritten rather than merely
4235
+ * re-checked: each was, on its own, the *only* warning a stream exhibiting one of
4236
+ * these conditions raised. That is the state this file exists to prevent, and it is
4237
+ * why the remedy in both cases was a **second, narrower, non-tolerable code**
4238
+ * rather than striking the tolerable one off. Striking it off would have changed
4239
+ * behavior for every profile naming it while still leaving the two conditions
4240
+ * reported by codes that also fire on cases costing nothing.
4241
+ *
4242
+ * **A sixth reader landed with `ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT`, and it is
4243
+ * the mirror of the fifth.** That code reports that the escape character closing an
4244
+ * unrecognized sequence could instead have opened one holding the delimiter that
4245
+ * split, so the bytes carry two alignments disagreeing by one boundary and the
4246
+ * leftmost was taken. It is safety-critical by construction, and the direction is
4247
+ * why it could not be left to the codes below: the fifth reports a boundary the
4248
+ * reading **lost**, this one a boundary the reading may have **gained**, and a
4249
+ * gained boundary hands back a value the sender's bytes do not unambiguously carry.
4250
+ * Both of the codes that condition previously raised are on the list below, so a
4251
+ * profile naming them left `{ strict: true }` accepting it. Re-reading the survivors
4252
+ * against it changed no admission and rewrote no argument: none of the four is a
4253
+ * statement about where a boundary was taken, and the new code is not tolerable, so
4254
+ * the same "a profile re-badges the code it names and no other" reason already
4255
+ * written under the two escape entries covers it.
4256
+ *
4257
+ * **A further reader landed with `ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS`, and it is
4258
+ * the first one that reaches a MODELED SLOT rather than a boundary.** It reports
4259
+ * that a contested alignment decided a **field** boundary while the reading taken
4260
+ * resumes on an escape character heading no sequence, so every later field sits one
4261
+ * place further right than the competing alignment puts it. On a result record that
4262
+ * moves the units and the **result status**: the sender's trailing letter is read out
4263
+ * of field 9 under the reading taken and out of no field at all under the other, so a
4264
+ * status of `final` can be a consequence of the alignment rather than something the
4265
+ * sender wrote there. Until it existed the only warning on that stream was the
4266
+ * **tolerable** `ASTM_UNPAIRED_ESCAPE_CHARACTER`, so the widest gate-legal profile
4267
+ * plus `{ strict: true }` accepted a fabricated `final` on a lab result. It is
4268
+ * safety-critical by construction and must never be admitted. Re-reading the
4269
+ * survivors against it moved no admission, and the reason is worth stating because it
4270
+ * is not last time's: `ASTM_UNPAIRED_ESCAPE_CHARACTER` stays on the list because it
4271
+ * remains true and benign of every case that costs nothing (a bare escape character
4272
+ * in a value alters no boundary at all), and the case where it accompanies a shifted
4273
+ * field is now reported by a code no profile may re-badge.
4274
+ *
4275
+ * **`ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD` landed next, on the same contested
4276
+ * position and the same tail test, and it reaches a modeled slot by the other
4277
+ * route.** It reports a contested alignment deciding a **repeat** boundary. Nothing
4278
+ * shifts: no field-indexed slot moves, which is exactly why the shift report could
4279
+ * not cover it. What it reports is that the field is read as more repeats than the
4280
+ * competing alignment gives it, and **where that gained boundary is the first one in
4281
+ * the field** it reaches a modeled slot, because a field's modeled value and
4282
+ * components are taken from its **first repeat alone**: everything past it stays in
4283
+ * `repeats` and leaves every modeled slot, so a result value truncates and a
4284
+ * Universal Test ID or a patient name comes back holding only the components that sat
4285
+ * before it. Until it existed the warnings on those streams were
4286
+ * `ASTM_NONSTANDARD_DELIMITERS` and `ASTM_UNPAIRED_ESCAPE_CHARACTER`, both on the list
4287
+ * below, so the widest gate-legal profile plus `{ strict: true }` accepted a truncated
4288
+ * value and an emptied test identity. **At a later boundary it fires and no modeled
4289
+ * slot moves**, which is over-reporting relative to those slots and never
4290
+ * under-reporting: measured, and stated on the code itself rather than left to be
4291
+ * found. It is safety-critical by construction and must never be admitted.
4292
+ * Re-reading the survivors against it moved no admission, and the reason is the one
4293
+ * the entry above already turns on: each of them re-badges the code it names and no
4294
+ * other, and what this condition now raises is a code no profile may name.
4295
+ *
3482
4296
  * **So the ones that remain are recorded with the reading each one survives**, not
3483
- * merely with the value it preserves. Each was re-derived against both readers of
3484
- * record structure above, plus the single-message / single-patient guards:
4297
+ * merely with the value it preserves. Each was re-derived against **every** reader of
4298
+ * record structure named above (this sentence deliberately names no count: it has
4299
+ * been corrected upward at every slice that landed one), plus the single-message /
4300
+ * single-patient guards:
3485
4301
  *
3486
4302
  * - `ASTM_NONSTANDARD_DELIMITERS`: the header's own declared delimiters are read
3487
4303
  * and honored either way, and this notes only that they differ from the canonical
@@ -3493,41 +4309,102 @@ declare function resolveProfileTransport(profile: AstmProfile | undefined): Astm
3493
4309
  * force. A stream may be wholly non-canonical and split perfectly, and a wholly
3494
4310
  * canonical stream can still carry a record that does not split, so tolerating the
3495
4311
  * first can never quiet the second.
4312
+ *
4313
+ * **Its admission was resting on a set of declarations it does not distinguish,
4314
+ * and no longer is.** A declaration naming one character in two roles is
4315
+ * necessarily non-canonical, so this code fired on it, and until
4316
+ * `ASTM_RECORD_DELIMITER_ROLE_COLLISION` existed it was the *only* warning such a
4317
+ * stream raised: tolerating "the declared set is unusual" therefore quieted
4318
+ * "the declared set cannot express the boundary it was read with". That second
4319
+ * condition now has its own code, which is not tolerable, so what is left here is
4320
+ * the honest statement it always meant to be: the set differs from the canonical
4321
+ * one and was honored. The reason tolerating this one cannot quiet that one is the
4322
+ * profile mechanism and nothing subtler: a profile re-badges the code it names and
4323
+ * no other. It is **not** that the two conditions are independent. They are not:
4324
+ * every colliding set is non-canonical, which is exactly why this bullet needed
4325
+ * rewriting.
3496
4326
  * - `ASTM_UNKNOWN_ESCAPE_SEQUENCE`: an unrecognized escape body is **preserved
3497
4327
  * byte-for-byte** in the decoded value (the escape codec does not guess at one),
3498
- * so the value is identical with or without the profile. No reader sees it
3499
- * either: a decoded field value is never cut back into records, the type
3500
- * letter is read before decoding, and the split reader counts fields, which the
3501
- * escape-aware tokenizer has already finished dividing before any body is decoded.
3502
- *
3503
- * **This entry is on the list and is recorded as questionable, deliberately.**
3504
- * The argument above is about the *decoded value*, and it holds. What it does not
3505
- * cover is that the escape-aware split itself treats an `&X&` triple as opaque, so
3506
- * where `X` is a delimiter that delimiter never became a boundary: the split the
3507
- * argument says has "already finished dividing" divided one time too few, and this
3508
- * code is the only report of it. Measured on the canonical set:
4328
+ * so the value is identical with or without the profile. A decoded field value is
4329
+ * never cut back into records, the type letter is read before decoding, and the
4330
+ * split reader counts fields the escape-aware tokenizer has already finished
4331
+ * dividing, so none of those three sees it.
4332
+ *
4333
+ * **Part 2 acquired a reader here, and it is named rather than left to be found.**
4334
+ * `isSplittingDelimiter` in `../common/escapes.ts` runs **only** on the
4335
+ * unrecognized-body condition this code reports, and decides out of it whether to
4336
+ * raise `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE`. That is a reader of the
4337
+ * reported condition, which is exactly what part 2 asks about, so this entry no
4338
+ * longer survives on "nothing reads it". It survives on a narrower and checkable
4339
+ * claim: a profile tolerating a code re-badges **that code and no other**, so
4340
+ * tolerating this one cannot quiet what the reader raises, and what it raises is
4341
+ * not tolerable in any case. If a future reader of this same condition ever
4342
+ * produces something a profile **can** tolerate, this entry stops being admissible
4343
+ * and comes off the list.
4344
+ *
4345
+ * **A second reader of the same condition landed, and it is named here for the
4346
+ * same reason.** The mnemonic test in `../common/escapes.ts` is now asked twice:
4347
+ * once by the decoder, to decide whether a body is recognized, and once by the
4348
+ * escape-aware split, which raises `ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT` only
4349
+ * where an **unrecognized** body sits in a sequence a competing alignment would
4350
+ * have aligned differently. That is the condition this code reports, read a second
4351
+ * time, and this entry survives it on the same narrower claim: what that reader
4352
+ * raises is a different code, which a profile cannot name and cannot tolerate.
4353
+ *
4354
+ * **This entry used to be recorded as questionable, and what made it questionable
4355
+ * is now a separate code that is not on this list.** The argument above is about
4356
+ * the *decoded value*, and it holds. What it did not cover is that the
4357
+ * escape-aware split itself treats an `&X&` triple as opaque, so where `X` is a
4358
+ * delimiter that delimiter never became a boundary: the split the argument says
4359
+ * has "already finished dividing" divided one time too few, and this code was the
4360
+ * only report of it. Measured on the canonical set:
3509
4361
  * `R|1|^^^687|28.6&|&U/L||||F` yields a value of `28.6&|&U/L`, no units, and status
3510
- * `unspecified` rather than `final`, with this as the sole warning, so a profile
3511
- * tolerating it (the shipped `referenceCorpus` does) lets `{ strict: true }` accept
3512
- * it. That fails part 1 of the two-clause test on the reading, not on the value.
3513
- * It is **left on the list on purpose**: removing it would change behavior for
3514
- * every profile naming it, and what should report a swallowed boundary instead, and
3515
- * at what severity, is a question this file cannot settle on its own. Do not read
3516
- * this bullet as an endorsement; read it as the open question it is, and do not
3517
- * close it by narrowing the atom, which is what keeps `&F&` one token under a
3518
- * delimiter set that names `F` as a delimiter.
4362
+ * `unspecified` rather than `final`, and this was the sole warning, so a profile
4363
+ * tolerating it (the shipped `referenceCorpus` does) let `{ strict: true }` accept
4364
+ * it. That failed part 1 of the two-clause test on the reading, not on the value.
4365
+ * `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE` now fires **alongside** this code on
4366
+ * exactly that subset and is safety-critical, so the lost boundary is reported by
4367
+ * something no profile may tolerate and that same stream is refused under
4368
+ * `{ strict: true }`. This code was **not** removed, for two reasons: removing it
4369
+ * changes behavior for every profile naming it, and it remains true and benign of
4370
+ * every unrecognized body that is not a delimiter in force, where the sequence is
4371
+ * preserved byte-for-byte and no boundary is lost. What is scoped away here is a
4372
+ * claim, not a guard. Do not close the underlying condition by narrowing the atom,
4373
+ * which is what keeps `&F&` one token under a delimiter set that names `F` as a
4374
+ * delimiter, and do not read the new code as a repair: the value is byte-identical
4375
+ * to what it was, and only the reporting changed.
3519
4376
  * - `ASTM_UNPAIRED_ESCAPE_CHARACTER`: an escape character heading no escape
3520
4377
  * sequence, read as the **literal character it is** and kept byte-for-byte in the
3521
4378
  * decoded value, so the value is identical with or without the profile. Note what
3522
4379
  * makes this admissible is a property of the *parse*, not of the warning: reading
3523
4380
  * the character as a literal is unconditional, and tolerating the code changes
3524
- * nothing about how the record splits. No reader sees it, on the same grounds as
3525
- * the entry above: the type letter is read before any decoding, the split reader
4381
+ * nothing about how the record splits. None of the three readers named at the top of
4382
+ * this file sees it: the type letter is read before any decoding, the split reader
3526
4383
  * counts fields the escape-aware tokenizer has already finished dividing, and a
3527
- * decoded value is never cut back into records. Contrast the condition it
4384
+ * decoded value is never cut back into records. **Two later readers DO see it**, and
4385
+ * they are named below rather than left out of this bullet. Contrast the condition it
3528
4386
  * replaced, which **would** have failed part 1: reading the same character as an
3529
4387
  * unterminated sequence merged every field after it into one, and a code reporting
3530
4388
  * that would have been a report of lost values.
4389
+ *
4390
+ * **The alignment reader is not a reader of this condition**, and that much still
4391
+ * holds: it asks whether the character two positions past a sequence *is* the escape
4392
+ * character, never whether that character heads a sequence of its own. The two are
4393
+ * independent in both directions, measured: `28.6&Z&|&U/L` raises this code and the
4394
+ * alignment code together, and `28.6&Z&|&U&L` raises the alignment code with no
4395
+ * unpaired character anywhere in it.
4396
+ *
4397
+ * **The two TAIL readers are a different matter, and "no reader sees it" is
4398
+ * therefore FALSE of this entry and must not be restored.** Both
4399
+ * `ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS` and
4400
+ * `ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD` fire on `!isEscapeSequenceAt(text, i + 4,
4401
+ * escape)`, which is exactly the question this code answers, asked about the
4402
+ * character just past a contested boundary. That is a reader of the reported
4403
+ * condition, so this entry survives on the same narrower claim the unknown-sequence
4404
+ * entry above already uses, not on nothing reading it: a profile re-badges the code
4405
+ * it names and no other, and what those two readers raise is not tolerable in any
4406
+ * case. If a future reader of this condition ever produces something a profile
4407
+ * **can** tolerate, this entry stops being admissible and comes off the list.
3531
4408
  * - `ASTM_RECORD_UNINTERPRETED_QUERY_STATUS`: a request-information status carried
3532
4409
  * verbatim as a leaf field on a `Q` record; the code set is paywalled and is not
3533
4410
  * interpreted, profile or not. Classification reads whether a `Q` record is
@@ -3539,8 +4416,12 @@ declare function resolveProfileTransport(profile: AstmProfile | undefined): Astm
3539
4416
  * undefined abnormal flag or result status, an unparseable reference range, absent
3540
4417
  * units, a mis-attached comment, a partial timestamp, a query-vs-result ambiguity,
3541
4418
  * an unrecognized record type, a record the delimiters in force could not split, a
3542
- * bad frame checksum / sequence gap / unterminated / oversize frame, an ambiguous
3543
- * transport, an unexpected protocol event, or a rejected frame: is forbidden.
4419
+ * declaration naming one character in two roles, a delimiter an unrecognized escape
4420
+ * sequence kept out of the split, a boundary a competing escape alignment disagrees
4421
+ * about, a field whose modeled reading stops at a boundary only one alignment has,
4422
+ * a bad frame checksum / sequence gap / unterminated
4423
+ * / oversize frame, an ambiguous transport, an unexpected protocol event, or a
4424
+ * rejected frame: is forbidden.
3544
4425
  *
3545
4426
  * **What this file cannot do for you.** Part 2 is a review obligation, not a
3546
4427
  * mechanical one. There is no honest automatic check for "nothing load-bearing
@@ -3571,6 +4452,29 @@ declare function resolveProfileTransport(profile: AstmProfile | undefined): Astm
3571
4452
  * reports a record the delimiters in force could not split, so every modeled field
3572
4453
  * of that record is missing: it fails the first half of the test outright.
3573
4454
  *
4455
+ * `ASTM_RECORD_DELIMITER_ROLE_COLLISION`,
4456
+ * `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE` and
4457
+ * `ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT` are not on this list and must not be
4458
+ * added to it. Each reports a boundary the reading cannot defend from the bytes (the
4459
+ * first two a boundary that is not in the reading, the third one that may not be in
4460
+ * the bytes), and each exists precisely because the only warnings its condition
4461
+ * previously raised were among the four below.
4462
+ *
4463
+ * `ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS` is not on this list and must not be added
4464
+ * to it either, and it is the strongest case of the four: it reports not merely a
4465
+ * boundary but a modeled slot changing hands, up to and including a result status
4466
+ * reading `final` that the competing alignment of the same bytes puts in no field at
4467
+ * all.
4468
+ *
4469
+ * `ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD` is not on this list and must not be added
4470
+ * to it either. It reports the same contested alignment deciding a **repeat**
4471
+ * boundary, where no slot changes hands and a modeled reading is cut off instead: the
4472
+ * field is read out of its first repeat alone, so where that boundary is the first
4473
+ * one a value truncates and a Universal Test ID's coding scheme and local code are
4474
+ * read from bytes that are no longer in any modeled slot. Reporting a value that is
4475
+ * not the whole of what the sender wrote fails the first half of the two-clause test
4476
+ * outright.
4477
+ *
3574
4478
  * @example
3575
4479
  * ```ts
3576
4480
  * import { TOLERABLE_CODES } from "@cosyte/astm";
@@ -3675,7 +4579,10 @@ declare const defaultProfile: AstmProfile;
3675
4579
  * only re-badges the *warning*: no value, flag, status, or identifier is altered
3676
4580
  * or dropped. And by the safety gate in `defineAstmProfile`, it **cannot** tolerate
3677
4581
  * a result value / flag / status / range / units, a patient or comment context, a
3678
- * message-kind ambiguity, an unrecognized record type, or any frame / LTP integrity
4582
+ * message-kind ambiguity, an unrecognized record type, a declaration naming one
4583
+ * character in two delimiter roles, a delimiter an unrecognized
4584
+ * escape sequence kept out of the split, a field boundary a competing escape
4585
+ * alignment disagrees about, or any frame / LTP integrity
3679
4586
  * warning: those always surface, profile or not. The unrecognized-record-type case
3680
4587
  * matters because message grouping reads a record's type letter, so a letter the
3681
4588
  * reader does not recognize may be a header, and a header read as something else
@@ -4912,4 +5819,4 @@ declare class AstmFrameStrictError extends Error {
4912
5819
  */
4913
5820
  declare const VERSION: string;
4914
5821
 
4915
- 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 };
5822
+ 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 TruncatedFieldSink, type UniversalTestId, type UniversalTestIdProvenance, type UnknownEscapeSink, type UnpairedEscapeSink, type UnsupportedRecord, VERSION, type VerbatimInput, WARNING_CODES, type WarningCode, alignmentShiftedFields, alignmentTruncatedField, ambiguousEscapeAlignment, ambiguousMessageKind, ambiguousValueSplit, applyAstmProfile, applyAstmProfileToWarnings, applyLivd, astmDateToLocalISO, astmProfiles, attachComments, buildAstmMessage, classifyMessage, comments, commentsFor, composeAstmFrames, computeChecksum, decodeAstmFrames, decodeEscapes, deepFreeze, defineAstmProfile, defineLivdCatalog, delimiterRoleCollision, delimiterSwallowedByEscape, delimitersRedeclared, detectFraming, encodeComponent, fieldScalar, fieldsUnseparated, frameBadChecksum, frameOversize, frameSequenceGap, frameUnterminated, getAstmProfile, getDefaultAstmProfile, hasCollidingRoles, interpretAbnormalFlag, interpretResultStatus, isNonStandard, isSafetyCriticalCode, listAstmProfiles, livdAmbiguousMapping, livdUnmappedCode, lookupLivdForRecord, ltpAmbiguousTransport, ltpFrameRejected, ltpInitialState, ltpReduce, ltpUnexpectedEvent, messages, nonStandardDelimiters, orders, orphanComment, parseAstmDate, parseAstmRecords, parseChecksumHex, parseFramedAstm, parseReferenceRange, partialTimestamp, patient, primaryCode, profileQuirkApplied, query, readDelimiterDeclaration, readDelimiters, recognizeUniversalTestId, resolveProfileTransport, results, serializeAstmRecord, serializeAstmRecords, serializeField, serializeFramedAstm, setDefaultAstmProfile, splitEscapeAware, toChecksumHex, tokenizeHeader, tokenizeRecord, undefinedAbnormalFlag, undefinedResultStatus, uninterpretedQueryStatus, unitsAbsent, unknownEscapeSequence, unknownRecordType, unpairedEscapeCharacter, unparseableReferenceRange, unreadableRedeclaration };