@cosyte/astm 0.0.8 → 0.0.9

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
@@ -57,10 +57,59 @@ declare const WARNING_CODES: {
57
57
  * not recognize as one does not open a new message.
58
58
  */
59
59
  readonly ASTM_RECORD_UNKNOWN_TYPE: "ASTM_RECORD_UNKNOWN_TYPE";
60
+ /**
61
+ * The delimiters in force found **no field separator at all** in a record that carries content
62
+ * beyond its type letter, so the whole record read back as a single field and none of its modeled
63
+ * fields could be recovered. The raw line is surfaced intact and nothing is dropped, but on a
64
+ * result record it means the value, the units and the status are all absent from the parsed model.
65
+ *
66
+ * This is one signature of a record being read with a delimiter set that does not belong to it,
67
+ * which is how a delimiter-scoping mistake turns into lost values. It is reported rather than
68
+ * repaired, because recovering the fields would mean guessing which set the sender meant.
69
+ *
70
+ * **Its absence is not evidence that a record was read in its own set.** This tests one of the
71
+ * four delimiter roles, the field separator, and only in its total form, where that separator
72
+ * occurs in the line (unescaped). Two whole classes of the same loss are outside it: a foreign set
73
+ * whose field separator happens to occur somewhere in the line still splits (on the wrong
74
+ * boundaries, silently, and this can happen to one record inside a run of these warnings); and a
75
+ * set differing in the repeat, component or escape role usually splits into fields normally,
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
78
+ * and every field after it shifts. The escape role's worst case has narrowed and not
79
+ * disappeared: an escape character heading no sequence is now read as a literal and reported
80
+ * 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
83
+ * report that one record definitely lost its fields, never as a sweep that would have fired if
84
+ * any had.
85
+ */
86
+ readonly ASTM_RECORD_FIELDS_UNSEPARATED: "ASTM_RECORD_FIELDS_UNSEPARATED";
60
87
  /** The header declared delimiters other than the canonical `H|\^&`: tolerated, noted. */
61
88
  readonly ASTM_NONSTANDARD_DELIMITERS: "ASTM_NONSTANDARD_DELIMITERS";
62
89
  /** An escape sequence body was not one of `&F&`/`&S&`/`&R&`/`&E&`: preserved verbatim. */
63
90
  readonly ASTM_UNKNOWN_ESCAPE_SEQUENCE: "ASTM_UNKNOWN_ESCAPE_SEQUENCE";
91
+ /**
92
+ * An escape character appeared where no escape sequence starts (an escape sequence is the escape
93
+ * character, one body character, and the escape character again). It is read as the **literal
94
+ * character it is** and kept byte-for-byte in the decoded value. Nothing is dropped and no byte is
95
+ * invented: this flags that the sender did not write the character the spec-clean way, which
96
+ * is `&E&`.
97
+ *
98
+ * **What it replaced is the reason it exists.** The codec used to read such a character as the
99
+ * opening of a sequence that never closed and merge the whole remainder of the record into the
100
+ * field it sat in, reporting nothing at all. One `&` in a result value cost the units, the
101
+ * abnormal flag and the status together and left the status reading `unspecified` rather than
102
+ * `final`; one in a surname cost the patient's birth date and sex. Emit then re-escaped the merged
103
+ * text into a spec-clean-looking line that read back as the same wrong value, so the mis-read
104
+ * survived a round trip without ever surfacing.
105
+ *
106
+ * **This code is not a statement about the rest of the record.** It reports one character. A
107
+ * *different* escape character in the same record may still head a real three-character sequence,
108
+ * and if that sequence's body happens to be a delimiter (`&|&` under the canonical set) the atom
109
+ * 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.
111
+ */
112
+ readonly ASTM_UNPAIRED_ESCAPE_CHARACTER: "ASTM_UNPAIRED_ESCAPE_CHARACTER";
64
113
  /**
65
114
  * A result value field carried an *unescaped* component delimiter, so it split into more than one
66
115
  * component. Both the full raw value and the split are surfaced and this warning fires: the
@@ -201,6 +250,28 @@ interface AstmRecordWarning {
201
250
  * ```
202
251
  */
203
252
  declare function unknownRecordType(position: AstmPosition): AstmRecordWarning;
253
+ /**
254
+ * Build an `ASTM_RECORD_FIELDS_UNSEPARATED` warning. The raw line is surfaced
255
+ * intact; what is lost is every **modeled** field of the record, because the
256
+ * delimiters in force never split it.
257
+ *
258
+ * **This one is not cosmetic either.** A record with content but no field
259
+ * separator is a record being read with a delimiter set that does not belong to
260
+ * it, and on a result record that costs the value, the units and the status in
261
+ * one go. The fields are not reconstructed, because doing so would mean guessing
262
+ * which set the sender meant, so a profile is not permitted to tolerate this code.
263
+ *
264
+ * **It is a report, not a sweep.** See {@link WARNING_CODES} for the two classes
265
+ * of the same loss it does not see: its absence never certifies a record split
266
+ * correctly.
267
+ *
268
+ * @example
269
+ * ```ts
270
+ * import { fieldsUnseparated } from "@cosyte/astm";
271
+ * fieldsUnseparated({ recordIndex: 4, recordType: "R" });
272
+ * ```
273
+ */
274
+ declare function fieldsUnseparated(position: AstmPosition): AstmRecordWarning;
204
275
  /**
205
276
  * Build an `ASTM_NONSTANDARD_DELIMITERS` warning. The declared delimiters are
206
277
  * used as-is; this only flags that they differ from the canonical set.
@@ -249,6 +320,28 @@ declare function unreadableRedeclaration(position: AstmPosition): AstmRecordWarn
249
320
  * ```
250
321
  */
251
322
  declare function unknownEscapeSequence(position: AstmPosition): AstmRecordWarning;
323
+ /**
324
+ * Build an `ASTM_UNPAIRED_ESCAPE_CHARACTER` warning. The character is preserved
325
+ * verbatim as a literal in the decoded value and opens no atom, so it does not
326
+ * merge the rest of the record; the warning body carries neither the character's
327
+ * surroundings nor any field value.
328
+ *
329
+ * **It is a statement about that one character, not about the record.** A
330
+ * 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}.
333
+ *
334
+ * A profile **may** tolerate this code: the value it reports is byte-identical
335
+ * with the warning and without it, because reading the character as a literal is
336
+ * the parse, not a consequence of the warning.
337
+ *
338
+ * @example
339
+ * ```ts
340
+ * import { unpairedEscapeCharacter } from "@cosyte/astm";
341
+ * unpairedEscapeCharacter({ recordIndex: 4, recordType: "R", fieldIndex: 4 });
342
+ * ```
343
+ */
344
+ declare function unpairedEscapeCharacter(position: AstmPosition): AstmRecordWarning;
252
345
  /**
253
346
  * Build an `ASTM_RECORD_AMBIGUOUS_VALUE_SPLIT` warning. Emitted when a result
254
347
  * value field split on an unescaped component delimiter: the full raw value and
@@ -1700,6 +1793,16 @@ interface TerminatorRecord extends RecordBase {
1700
1793
  * starts by reading the type letter, so a header the reader could not recognize
1701
1794
  * arrives here instead, opens no message, and the messages either side of it are
1702
1795
  * grouped as one. Check `rawType` before trusting a split.
1796
+ *
1797
+ * **And one of these may be a `Q`.** Message classification counts type letters
1798
+ * too, so a message carrying an unsupported record is classified `indeterminate`
1799
+ * unless a `Q` was read outright: see
1800
+ * {@link AstmMessageClassification.hasUnrecognized}.
1801
+ *
1802
+ * **A header that arrives here also failed to re-scope the delimiters.** The
1803
+ * records after it are read with the set already in force, and where that set is
1804
+ * not theirs they do not split at all, which is reported per record as
1805
+ * `ASTM_RECORD_FIELDS_UNSEPARATED`.
1703
1806
  */
1704
1807
  interface UnsupportedRecord extends RecordBase {
1705
1808
  readonly type: "unsupported";
@@ -1738,7 +1841,14 @@ type AstmMessageKind = "host-query" | "results" | "orders" | "indeterminate";
1738
1841
  * ```
1739
1842
  */
1740
1843
  interface AstmMessageClassification {
1741
- /** The message kind. */
1844
+ /**
1845
+ * The message kind.
1846
+ *
1847
+ * `indeterminate` also covers a message the reader declines to classify: when
1848
+ * {@link AstmMessageClassification.hasUnrecognized} is `true` and no `Q` was read, the kind is
1849
+ * withheld rather than guessed, because the unreadable letter may have been that `Q`. The `has*`
1850
+ * flags below stay truthful in that case, so a caller that wants the raw counts still has them.
1851
+ */
1742
1852
  readonly kind: AstmMessageKind;
1743
1853
  /** At least one `Q` (request-information) record is present. */
1744
1854
  readonly hasQuery: boolean;
@@ -1747,8 +1857,20 @@ interface AstmMessageClassification {
1747
1857
  /** At least one `O` (order) record is present. */
1748
1858
  readonly hasOrders: boolean;
1749
1859
  /**
1750
- * `true` **iff** `kind === "host-query"` (a `Q` record is present), the safety surface: gate on this
1860
+ * At least one record's type letter was **not recognized** (an
1861
+ * {@link UnsupportedRecord}), so the letter counts above are known to be incomplete.
1862
+ *
1863
+ * Any of them may have been a `Q`, which is why a message carrying one is classified
1864
+ * `indeterminate` unless a `Q` was read outright. `ASTM_RECORD_UNKNOWN_TYPE` reports the same
1865
+ * condition on {@link AstmMessage.warnings}.
1866
+ */
1867
+ readonly hasUnrecognized: boolean;
1868
+ /**
1869
+ * `true` **iff** `kind === "host-query"` (a `Q` record was read), the safety surface: gate on this
1751
1870
  * before treating records as results, so a query is never misread as a result upload.
1871
+ *
1872
+ * `false` is **not** a warrant that the message is a result set. Read `kind` for that: it is
1873
+ * `indeterminate` whenever the reader could not account for every type letter.
1752
1874
  */
1753
1875
  readonly isHostQueryRequest: boolean;
1754
1876
  }
@@ -2639,6 +2761,17 @@ declare function messages(msg: AstmMessage): readonly AstmStreamMessage[];
2639
2761
  * result set, even if a result record is also present (an anomaly, which the
2640
2762
  * parser flags separately with `ASTM_RECORD_AMBIGUOUS_MESSAGE_KIND`). This is the
2641
2763
  * whole point: a `Q`-bearing message can never silently read as a result upload.
2764
+ *
2765
+ * **A letter the reader could not recognize blocks a positive answer.** The rule
2766
+ * above is stated on a count of type letters, so it only holds while every letter
2767
+ * was legible. A record whose type is unsupported may be the very `Q` the rule
2768
+ * exists to honor, and reading such a message as a result set is exactly the
2769
+ * outcome the rule forbids. So an unsupported record, with no `Q` to settle the
2770
+ * question, yields `indeterminate` rather than `results` or `orders`: the honest
2771
+ * answer is that the kind is not knowable, and the parser reports the unreadable
2772
+ * letter separately with `ASTM_RECORD_UNKNOWN_TYPE`. A `Q` that *was* read still
2773
+ * dominates, because an unreadable letter can only ever add a kind, never remove
2774
+ * the query that is already there.
2642
2775
  */
2643
2776
 
2644
2777
  /**
@@ -2649,6 +2782,12 @@ declare function messages(msg: AstmMessage): readonly AstmStreamMessage[];
2649
2782
  * a result (`R`) record is also present, so a query is never misread as a result
2650
2783
  * upload. (The `Q`+`R` anomaly is separately warned at parse time.)
2651
2784
  *
2785
+ * An **unsupported** record letter with no `Q` alongside it yields `indeterminate`,
2786
+ * never `results` or `orders`: an unreadable letter may have been the `Q`, and
2787
+ * claiming a positive kind over it is how a query comes to read as a result set.
2788
+ * {@link AstmMessageClassification.hasUnrecognized} reports why the answer is
2789
+ * withheld, and the `has*` counts stay truthful throughout.
2790
+ *
2652
2791
  * @param records - The parsed records, in wire order.
2653
2792
  * @returns The message classification.
2654
2793
  * @example
@@ -2677,8 +2816,10 @@ declare function classifyMessage(records: readonly AstmRecord[]): AstmMessageCla
2677
2816
  *
2678
2817
  * @param record - The raw record text.
2679
2818
  * @param d - The delimiters resolved from the header.
2680
- * @param onUnknownEscape - Called (with the 1-based field index) for each
2819
+ * @param onUnknownEscape - Called (with the 0-based field index) for each
2681
2820
  * unrecognized escape sequence encountered, so the caller can warn.
2821
+ * @param onUnpairedEscape - Called (with the 0-based field index) for each escape
2822
+ * character that heads no escape sequence and was therefore read as a literal.
2682
2823
  * @returns The record's fields.
2683
2824
  * @example
2684
2825
  * ```ts
@@ -2687,18 +2828,19 @@ declare function classifyMessage(records: readonly AstmRecord[]): AstmMessageCla
2687
2828
  * fields[3].components[0]; // "28.6"
2688
2829
  * ```
2689
2830
  */
2690
- declare function tokenizeRecord(record: string, d: Delimiters, onUnknownEscape?: (fieldIndex: number) => void): AstmField[];
2831
+ declare function tokenizeRecord(record: string, d: Delimiters, onUnknownEscape?: (fieldIndex: number) => void, onUnpairedEscape?: (fieldIndex: number) => void): AstmField[];
2691
2832
  /**
2692
2833
  * Tokenize an `H` (header) record into its fields.
2693
2834
  *
2694
2835
  * A header cannot go through {@link tokenizeRecord}: its second field is the
2695
- * **delimiter declaration**, which carries the escape character *literally*
2696
- * rather than as an escape sequence. The generic escape-aware split treats that
2697
- * lone escape character as the opening of an unterminated escape and swallows
2698
- * the whole remainder of the record into one field, so every data field after
2699
- * the declaration is lost. This tokenizer instead takes the declaration verbatim
2700
- * as one opaque field and applies the ordinary escape-aware tokenizer to the
2701
- * data portion that follows it.
2836
+ * **delimiter declaration**, which carries all three non-field delimiters
2837
+ * *literally* rather than as escape sequences. Run through the generic tokenizer
2838
+ * the declaration would be split on its own repeat and component characters and
2839
+ * its escape character would be decoded and reported as unpaired, so what the
2840
+ * header declares would come back as fragments plus a spurious warning. This
2841
+ * tokenizer instead takes the declaration verbatim as one opaque field, never
2842
+ * decoded, and applies the ordinary escape-aware tokenizer to the data portion
2843
+ * that follows it.
2702
2844
  *
2703
2845
  * `fields[0]` is the type-letter field and `fields[1]` is the delimiter
2704
2846
  * declaration (verbatim, never escape-decoded); the header's ASTM data fields
@@ -2708,6 +2850,9 @@ declare function tokenizeRecord(record: string, d: Delimiters, onUnknownEscape?:
2708
2850
  * @param d - The delimiters declared by this header.
2709
2851
  * @param onUnknownEscape - Called with the 0-based whole-record field index for
2710
2852
  * each unrecognized escape sequence in the data portion.
2853
+ * @param onUnpairedEscape - Called with the 0-based whole-record field index for
2854
+ * each unpaired escape character in the data portion. The declaration itself is
2855
+ * opaque, so the escape character it names never reports here.
2711
2856
  * @returns The header's fields.
2712
2857
  * @example
2713
2858
  * ```ts
@@ -2717,7 +2862,7 @@ declare function tokenizeRecord(record: string, d: Delimiters, onUnknownEscape?:
2717
2862
  * fields[4].raw; // "sender"
2718
2863
  * ```
2719
2864
  */
2720
- declare function tokenizeHeader(record: string, d: Delimiters, onUnknownEscape?: (fieldIndex: number) => void): AstmField[];
2865
+ declare function tokenizeHeader(record: string, d: Delimiters, onUnknownEscape?: (fieldIndex: number) => void, onUnpairedEscape?: (fieldIndex: number) => void): AstmField[];
2721
2866
  /**
2722
2867
  * The primary scalar of a field: its first repeat's first component, decoded.
2723
2868
  * Returns `undefined` for a truly empty field so callers can distinguish
@@ -2833,6 +2978,26 @@ declare class AstmParseError extends Error {
2833
2978
  * leaf**: the escape sequence survives the split intact and only then decodes to
2834
2979
  * a single literal, so `1&S&40` reads as exactly one component.
2835
2980
  *
2981
+ * **An escape sequence is exactly three characters** (the escape character, one
2982
+ * body character, and the escape character again), and both functions here agree
2983
+ * on that, because they have to: a split that thinks `&X…&` is one atom while the
2984
+ * decoder thinks otherwise is the same class of mis-read the ordering above
2985
+ * exists to prevent. An escape character that does not sit at the head of such a
2986
+ * triple is not an escape at all: it is read as the **literal character it is**,
2987
+ * it opens no atom, and the parse layer reports it
2988
+ * (`ASTM_UNPAIRED_ESCAPE_CHARACTER`). Both functions used to scan forward for the
2989
+ * next escape character with no bound, which meant a single unescaped `&` in a
2990
+ * value merged every field after it into that value, in silence.
2991
+ *
2992
+ * **The atom rule is unchanged and still costs a boundary in one case.** A triple
2993
+ * is opaque by design, so where its body is itself a delimiter (`&|&` under the
2994
+ * canonical set) that delimiter does not split. That is deliberate: it is what
2995
+ * keeps `&F&` one token under a declared set that names `F` as a delimiter. Where
2996
+ * the body is not a recognized mnemonic the parse layer reports it as
2997
+ * `ASTM_UNKNOWN_ESCAPE_SEQUENCE`, so it is never silent, but that code is
2998
+ * tolerable and a field boundary has still gone. Do not "fix" that by narrowing
2999
+ * the atom: the narrowing would break the guarantee the atom exists for.
3000
+ *
2836
3001
  * Re-escaping (the inverse, for spec-clean emit) lives in the serializer and is
2837
3002
  * deliberately not implemented here.
2838
3003
  */
@@ -2844,11 +3009,19 @@ declare class AstmParseError extends Error {
2844
3009
  * warning. Optional so the codec can be used purely.
2845
3010
  */
2846
3011
  type UnknownEscapeSink = () => void;
3012
+ /**
3013
+ * A callback the codec calls when it encounters an escape character that does not
3014
+ * head a three-character escape sequence. The character is preserved verbatim as a
3015
+ * literal; the callback lets the parser surface a value-free
3016
+ * `ASTM_UNPAIRED_ESCAPE_CHARACTER` warning. Optional so the codec can be used purely.
3017
+ */
3018
+ type UnpairedEscapeSink = () => void;
2847
3019
  /**
2848
3020
  * Decode the four recognized ASTM escape mnemonics in a single already-split
2849
3021
  * leaf (a component string), substituting the *active* delimiters read from the
2850
- * header. Unrecognized `&…&` bodies are preserved verbatim and reported through
2851
- * `onUnknown` (never dropped, never guessed).
3022
+ * header. Unrecognized `&X&` bodies are preserved verbatim and reported through
3023
+ * `onUnknown` (never dropped, never guessed), and an escape character that heads
3024
+ * no sequence at all is preserved as a literal and reported through `onUnpaired`.
2852
3025
  *
2853
3026
  * This runs **after** splitting, so a decoded delimiter becomes ordinary literal
2854
3027
  * text and can never introduce a new split boundary.
@@ -2856,23 +3029,34 @@ type UnknownEscapeSink = () => void;
2856
3029
  * @param leaf - One component string, escape-aware split already applied.
2857
3030
  * @param d - The delimiters resolved from the header.
2858
3031
  * @param onUnknown - Called once per unrecognized escape body encountered.
3032
+ * @param onUnpaired - Called once per escape character that heads no sequence.
2859
3033
  * @returns The decoded string.
2860
3034
  * @example
2861
3035
  * ```ts
2862
3036
  * import { decodeEscapes, CANONICAL_DELIMITERS } from "@cosyte/astm";
2863
3037
  * decodeEscapes("1&S&40", CANONICAL_DELIMITERS); // "1^40"
3038
+ * decodeEscapes("O&Brien", CANONICAL_DELIMITERS); // "O&Brien" (literal, reported)
2864
3039
  * ```
2865
3040
  */
2866
- declare function decodeEscapes(leaf: string, d: Delimiters, onUnknown?: UnknownEscapeSink): string;
3041
+ declare function decodeEscapes(leaf: string, d: Delimiters, onUnknown?: UnknownEscapeSink, onUnpaired?: UnpairedEscapeSink): string;
2867
3042
  /**
2868
- * Split `text` on `delimiter`, treating any escape sequence (`escape …
2869
- * escape`) as an opaque atom so a delimiter that appears *inside* an escape body
2870
- * never causes a split. Returns the raw (still-encoded) segments: decoding is
2871
- * the caller's next step, per the escape-aware-split-then-decode contract.
3043
+ * Split `text` on `delimiter`, treating an escape sequence (escape, one body
3044
+ * character, escape) as an opaque atom so a delimiter that appears *inside* an
3045
+ * escape body never causes a split. Returns the raw (still-encoded) segments:
3046
+ * decoding is the caller's next step, per the escape-aware-split-then-decode
3047
+ * contract.
2872
3048
  *
2873
- * For the four canonical mnemonics this is belt-and-suspenders (their bodies are
2874
- * letters, not delimiters), but it makes the "an escaped delimiter is one token"
2875
- * guarantee hold for any declared delimiter set, including adversarial input.
3049
+ * For the four canonical mnemonics the opacity is belt-and-suspenders (their
3050
+ * bodies are letters, not delimiters), but it makes the "an escaped delimiter is
3051
+ * one token" guarantee hold for any declared delimiter set, including adversarial
3052
+ * input. A single body character is all that guarantee needs.
3053
+ *
3054
+ * An escape character that heads no sequence is **not** an escape: it is ordinary
3055
+ * text, and it opens no atom. Reading it as the opening of a sequence that never
3056
+ * closes is what used to merge the whole remainder of a record into one field.
3057
+ * Decoding the resulting leaf is what reports it, so this function stays a pure
3058
+ * split. Note the two rules together: a delimiter after such a character does
3059
+ * split, and a delimiter sitting inside a real three-character atom does not.
2876
3060
  *
2877
3061
  * @param text - The field or repeat string to split.
2878
3062
  * @param delimiter - The delimiter to split on.
@@ -2883,6 +3067,7 @@ declare function decodeEscapes(leaf: string, d: Delimiters, onUnknown?: UnknownE
2883
3067
  * import { splitEscapeAware } from "@cosyte/astm";
2884
3068
  * splitEscapeAware("a^b^c", "^", "&"); // ["a", "b", "c"]
2885
3069
  * splitEscapeAware("1&S&40", "^", "&"); // ["1&S&40"] (escape body is opaque)
3070
+ * splitEscapeAware("O&Brien^John", "^", "&"); // ["O&Brien", "John"] (unpaired: literal)
2886
3071
  * ```
2887
3072
  */
2888
3073
  declare function splitEscapeAware(text: string, delimiter: string, escape: string): string[];
@@ -3190,40 +3375,96 @@ declare function resolveProfileTransport(profile: AstmProfile | undefined): Astm
3190
3375
  * where one message ends and the next begins, so an unrecognized header opens no
3191
3376
  * message and two messages are read as one, pairing a patient with results that
3192
3377
  * arrived under a different header. Message classification counts `Q` / `R` / `O`
3193
- * records by the same letter, so an unrecognized `Q` costs a host-query request its
3194
- * `Q`-dominates guarantee: it reads as a result set, and the
3195
- * `ASTM_RECORD_AMBIGUOUS_MESSAGE_KIND` warning that flags that contradiction goes
3378
+ * records by the same letter, so an unrecognized `Q` cost a host-query request its
3379
+ * `Q`-dominates guarantee: it read as a result set, and the
3380
+ * `ASTM_RECORD_AMBIGUOUS_MESSAGE_KIND` warning that flags that contradiction went
3196
3381
  * with it. In both cases `ASTM_RECORD_UNKNOWN_TYPE` is the only warning left on the
3197
3382
  * stream, while the allow-list still described it as harmless. Tolerating it
3198
3383
  * re-badged the single report that the reader had lost its place, and a strict parse
3199
3384
  * then accepted the stream. It is off the list for that reason, and its removal is
3200
3385
  * what part 2 above is written down to prevent a repeat of.
3201
3386
  *
3202
- * **So the three that remain are recorded with the reading each one survives**, not
3387
+ * The classifier no longer answers `results` over a letter it could not read (it
3388
+ * withholds the kind instead), so that particular misreading is closed at the
3389
+ * source as well as reported. The allow-list conclusion is unchanged either way:
3390
+ * part 2 asks whether anything **reads** the reported condition, and the classifier
3391
+ * still does, now to decide whether it may answer at all.
3392
+ *
3393
+ * **A third reader of record structure landed with
3394
+ * `ASTM_RECORD_FIELDS_UNSEPARATED`, and the allow-list was re-derived against it.**
3395
+ * That code reports a record the delimiters in force could not split at all, which
3396
+ * is one signature of a record being read in a set that is not its own. It is
3397
+ * safety-critical (it reports lost values, so it fails part 1 outright) and it is
3398
+ * on the forbidden side by construction, because that side is computed rather than
3399
+ * listed. What it required was re-reading the survivors below against it:
3400
+ * none of them is a statement about whether a record split, so none of them is now
3401
+ * hiding it. Note the code is deliberately **partial** (see its own docs): it does
3402
+ * not fire wherever a field went missing to a foreign delimiter set, only where the
3403
+ * field separator was absent outright, so it is a reader of record structure that
3404
+ * cannot be relied on as a sweep.
3405
+ *
3406
+ * **So the ones that remain are recorded with the reading each one survives**, not
3203
3407
  * merely with the value it preserves. Each was re-derived against both readers of
3204
3408
  * record structure above, plus the single-message / single-patient guards:
3205
3409
  *
3206
3410
  * - `ASTM_NONSTANDARD_DELIMITERS`: the header's own declared delimiters are read
3207
3411
  * and honored either way, and this notes only that they differ from the canonical
3208
- * set. Neither reader sees them: the stream is cut into records on a line break,
3412
+ * set. No reader sees them: the stream is cut into records on a line break,
3209
3413
  * and a record's type letter is the first character of its line, both taken before
3210
- * any delimiter-driven tokenization runs.
3414
+ * any delimiter-driven tokenization runs. It survives the split reader too, and
3415
+ * the distinction is worth stating precisely: this code says the declared set is
3416
+ * unusual, `ASTM_RECORD_FIELDS_UNSEPARATED` says a record did not match the set in
3417
+ * force. A stream may be wholly non-canonical and split perfectly, and a wholly
3418
+ * canonical stream can still carry a record that does not split, so tolerating the
3419
+ * first can never quiet the second.
3211
3420
  * - `ASTM_UNKNOWN_ESCAPE_SEQUENCE`: an unrecognized escape body is **preserved
3212
3421
  * byte-for-byte** in the decoded value (the escape codec does not guess at one),
3213
- * so the value is identical with or without the profile. Neither reader sees it
3214
- * either: a decoded field value is never cut back into records, and the type
3215
- * letter is read before decoding.
3422
+ * so the value is identical with or without the profile. No reader sees it
3423
+ * either: a decoded field value is never cut back into records, the type
3424
+ * letter is read before decoding, and the split reader counts fields, which the
3425
+ * escape-aware tokenizer has already finished dividing before any body is decoded.
3426
+ *
3427
+ * **This entry is on the list and is recorded as questionable, deliberately.**
3428
+ * The argument above is about the *decoded value*, and it holds. What it does not
3429
+ * cover is that the escape-aware split itself treats an `&X&` triple as opaque, so
3430
+ * where `X` is a delimiter that delimiter never became a boundary: the split the
3431
+ * argument says has "already finished dividing" divided one time too few, and this
3432
+ * code is the only report of it. Measured on the canonical set:
3433
+ * `R|1|^^^687|28.6&|&U/L||||F` yields a value of `28.6&|&U/L`, no units, and status
3434
+ * `unspecified` rather than `final`, with this as the sole warning, so a profile
3435
+ * tolerating it (the shipped `referenceCorpus` does) lets `{ strict: true }` accept
3436
+ * it. That fails part 1 of the two-clause test on the reading, not on the value.
3437
+ * It is **left on the list on purpose**: removing it would change behavior for
3438
+ * every profile naming it, and what should report a swallowed boundary instead, and
3439
+ * at what severity, is a question this file cannot settle on its own. Do not read
3440
+ * this bullet as an endorsement; read it as the open question it is, and do not
3441
+ * close it by narrowing the atom, which is what keeps `&F&` one token under a
3442
+ * delimiter set that names `F` as a delimiter.
3443
+ * - `ASTM_UNPAIRED_ESCAPE_CHARACTER`: an escape character heading no escape
3444
+ * sequence, read as the **literal character it is** and kept byte-for-byte in the
3445
+ * decoded value, so the value is identical with or without the profile. Note what
3446
+ * makes this admissible is a property of the *parse*, not of the warning: reading
3447
+ * the character as a literal is unconditional, and tolerating the code changes
3448
+ * nothing about how the record splits. No reader sees it, on the same grounds as
3449
+ * the entry above: the type letter is read before any decoding, the split reader
3450
+ * counts fields the escape-aware tokenizer has already finished dividing, and a
3451
+ * decoded value is never cut back into records. Contrast the condition it
3452
+ * replaced, which **would** have failed part 1: reading the same character as an
3453
+ * unterminated sequence merged every field after it into one, and a code reporting
3454
+ * that would have been a report of lost values.
3216
3455
  * - `ASTM_RECORD_UNINTERPRETED_QUERY_STATUS`: a request-information status carried
3217
3456
  * verbatim as a leaf field on a `Q` record; the code set is paywalled and is not
3218
3457
  * interpreted, profile or not. Classification reads whether a `Q` record is
3219
- * *present*, never what this field says, and grouping does not read `Q` at all.
3458
+ * *present*, never what this field says; grouping does not read `Q` at all; and the
3459
+ * split reader is a count of fields, not a reading of any one of them, so a record
3460
+ * carrying this warning has by definition already split.
3220
3461
  *
3221
3462
  * Everything a wrong value could hide from: a result value split ambiguity, an
3222
3463
  * undefined abnormal flag or result status, an unparseable reference range, absent
3223
3464
  * units, a mis-attached comment, a partial timestamp, a query-vs-result ambiguity,
3224
- * an unrecognized record type, a bad frame checksum / sequence gap / unterminated /
3225
- * oversize frame, an ambiguous transport, an unexpected protocol event, or a
3226
- * rejected frame: is forbidden.
3465
+ * an unrecognized record type, a record the delimiters in force could not split, a
3466
+ * bad frame checksum / sequence gap / unterminated / oversize frame, an ambiguous
3467
+ * transport, an unexpected protocol event, or a rejected frame: is forbidden.
3227
3468
  *
3228
3469
  * **What this file cannot do for you.** Part 2 is a review obligation, not a
3229
3470
  * mechanical one. There is no honest automatic check for "nothing load-bearing
@@ -3247,8 +3488,12 @@ declare function resolveProfileTransport(profile: AstmProfile | undefined): Astm
3247
3488
  * `ASTM_RECORD_UNKNOWN_TYPE` is **not** on this list, and used to be. An
3248
3489
  * unrecognized record type may be a header the reader did not recognize as one, in
3249
3490
  * which case two messages have been read as one, or a `Q` it did not recognize, in
3250
- * which case a host-query request reads as a result set. Tolerating it can quiet
3251
- * the only report either happened.
3491
+ * which case the message kind is no longer knowable. Tolerating it can quiet the
3492
+ * only report either happened.
3493
+ *
3494
+ * `ASTM_RECORD_FIELDS_UNSEPARATED` is not on this list either, and never was. It
3495
+ * reports a record the delimiters in force could not split, so every modeled field
3496
+ * of that record is missing: it fails the first half of the test outright.
3252
3497
  *
3253
3498
  * @example
3254
3499
  * ```ts
@@ -4414,4 +4659,4 @@ declare class AstmFrameStrictError extends Error {
4414
4659
  */
4415
4660
  declare const VERSION: string;
4416
4661
 
4417
- 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 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 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, 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, unparseableReferenceRange, unreadableRedeclaration };
4662
+ 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 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 };