@cosyte/astm 0.0.1

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.
@@ -0,0 +1,3861 @@
1
+ /**
2
+ * Positional context shared by warnings and fatal errors in the `@cosyte/astm`
3
+ * record layer.
4
+ *
5
+ * **PHI discipline (the whole point).** A position identifies *where* a
6
+ * deviation occurred — the record's ordinal index, its type letter, and the
7
+ * 1-based field / component indices — and **never** carries a field value. A
8
+ * warning or error may be logged verbatim without leaking a patient name, an
9
+ * identifier, or a result value.
10
+ */
11
+ /**
12
+ * Where in a record stream a warning or fatal originated. Every field is
13
+ * positional; none is a value.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * import type { AstmPosition } from "@cosyte/astm";
18
+ * const at: AstmPosition = { recordIndex: 3, recordType: "R", fieldIndex: 4 };
19
+ * ```
20
+ */
21
+ interface AstmPosition {
22
+ /** 0-based ordinal of the record within the message. */
23
+ readonly recordIndex: number;
24
+ /** The record's type letter (`H`/`P`/`O`/`R`/`L`/…), when known. */
25
+ readonly recordType?: string;
26
+ /** 1-based field index within the record (ASTM fields are 1-indexed). */
27
+ readonly fieldIndex?: number;
28
+ /** 1-based component index within the field, when the deviation is component-scoped. */
29
+ readonly componentIndex?: number;
30
+ }
31
+
32
+ /**
33
+ * Tier-2 warning registry and factories for the `@cosyte/astm` record parser.
34
+ *
35
+ * A warning is the lenient parser's record of a tolerated deviation: it never
36
+ * throws, never drops data, and never fabricates a value. Consumers compare
37
+ * `warning.code === WARNING_CODES.<CODE>` to react; renaming a code is a
38
+ * **breaking change**. Every warning carries a stable code plus a
39
+ * {@link AstmPosition} and **never** a field value (PHI discipline).
40
+ */
41
+
42
+ /**
43
+ * Stable string codes for every Tier-2 warning the record parser may emit.
44
+ * `key === value` so `Object.values(...)` yields a stable snapshot set.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * import { parseAstmRecords, WARNING_CODES } from "@cosyte/astm";
49
+ * const msg = parseAstmRecords("H|\\^&\rL|1\r");
50
+ * msg.warnings.some((w) => w.code === WARNING_CODES.ASTM_RECORD_UNKNOWN_TYPE);
51
+ * ```
52
+ */
53
+ declare const WARNING_CODES: {
54
+ /** A record's type letter is not one of the modeled types — surfaced as an unsupported record. */
55
+ readonly ASTM_RECORD_UNKNOWN_TYPE: "ASTM_RECORD_UNKNOWN_TYPE";
56
+ /** The header declared delimiters other than the canonical `H|\^&` — tolerated, noted. */
57
+ readonly ASTM_NONSTANDARD_DELIMITERS: "ASTM_NONSTANDARD_DELIMITERS";
58
+ /** An escape sequence body was not one of `&F&`/`&S&`/`&R&`/`&E&` — preserved verbatim. */
59
+ readonly ASTM_UNKNOWN_ESCAPE_SEQUENCE: "ASTM_UNKNOWN_ESCAPE_SEQUENCE";
60
+ /**
61
+ * A result value field carried an *unescaped* component delimiter, so it split into more than one
62
+ * component. Both the full raw value and the split are surfaced and this warning fires — the
63
+ * ambiguity is never resolved silently into a truncated value.
64
+ */
65
+ readonly ASTM_RECORD_AMBIGUOUS_VALUE_SPLIT: "ASTM_RECORD_AMBIGUOUS_VALUE_SPLIT";
66
+ /**
67
+ * A result's abnormal-flag field (`R` field 7) carried a letter outside HL7 Table 0078. The flag is
68
+ * surfaced as `undefined` — never dropped, and **never coerced to `normal`** (a clinical error).
69
+ */
70
+ readonly ASTM_RECORD_UNDEFINED_ABNORMAL_FLAG: "ASTM_RECORD_UNDEFINED_ABNORMAL_FLAG";
71
+ /**
72
+ * A result's status field (`R` field 9) carried a letter that is not a recognized status. It is
73
+ * surfaced as `undefined` and, like every non-`F` status, never reads as active-final.
74
+ */
75
+ readonly ASTM_RECORD_UNDEFINED_RESULT_STATUS: "ASTM_RECORD_UNDEFINED_RESULT_STATUS";
76
+ /**
77
+ * A result's reference-range field (`R` field 6) did not match a recognized form (`low-high`,
78
+ * `<high`, `>low`). The text is surfaced verbatim as `unparsed` — a bound is **never fabricated**.
79
+ */
80
+ readonly ASTM_RECORD_UNPARSEABLE_REFERENCE_RANGE: "ASTM_RECORD_UNPARSEABLE_REFERENCE_RANGE";
81
+ /**
82
+ * A result carried a numeric value but no units (`R` field 5 empty). Units are vendor free text
83
+ * (not UCUM); a missing unit is flagged here and **never defaulted, guessed, or converted**.
84
+ */
85
+ readonly ASTM_RECORD_UNITS_ABSENT: "ASTM_RECORD_UNITS_ABSENT";
86
+ /**
87
+ * A `C` (comment) record had no valid preceding `H`/`P`/`O`/`R` parent — an **orphan**. The comment
88
+ * is attached to the message root (`attachedToRoot: true`) and surfaced, **never dropped**.
89
+ */
90
+ readonly ASTM_RECORD_ORPHAN_COMMENT: "ASTM_RECORD_ORPHAN_COMMENT";
91
+ /**
92
+ * A `YYYYMMDDHHMMSS` timestamp had an odd digit run that truncates a two-digit component in half
93
+ * (e.g. a partial day/hour). The raw run is preserved and the structured value stops at the last
94
+ * **complete** component — the dangling digit is **never zero-filled into a fabricated time**.
95
+ */
96
+ readonly ASTM_RECORD_PARTIAL_TIMESTAMP: "ASTM_RECORD_PARTIAL_TIMESTAMP";
97
+ /**
98
+ * A `Q` (request-information) record carried a request-information status code (field 13). The code
99
+ * *set* is `[OSS-derived / paywalled]` with no publicly-groundable enumeration, so the parser
100
+ * interprets **none** of them: the status is surfaced verbatim and this value-free warning flags that
101
+ * it was passed through **uninterpreted** — never mapped to a guessed meaning.
102
+ */
103
+ readonly ASTM_RECORD_UNINTERPRETED_QUERY_STATUS: "ASTM_RECORD_UNINTERPRETED_QUERY_STATUS";
104
+ /**
105
+ * A message carried **both** a `Q` (request) and an `R` (result) record — a contradictory shape. The
106
+ * message is classified `host-query` (the `Q` **dominates**, so it is never read as a result set) and
107
+ * this warning flags the anomaly. Positional context only; no field value.
108
+ */
109
+ readonly ASTM_RECORD_AMBIGUOUS_MESSAGE_KIND: "ASTM_RECORD_AMBIGUOUS_MESSAGE_KIND";
110
+ /**
111
+ * The downgraded form an active vendor {@link AstmProfile} produces from a deviation it *expects*
112
+ * (see `src/profiles/`). The original warning is **never dropped**: its code moves to
113
+ * {@link AstmRecordWarning.toleratedCode}, the warning is re-badged `PROFILE_QUIRK_APPLIED` with
114
+ * `expected: true` and the tolerating profile named, so a consumer can filter known, grounded noise
115
+ * while the fact of the deviation — and where it was — survives. A profile can only ever reach this
116
+ * path for a **non-safety-critical** code (enforced at profile-definition time); a safety-critical
117
+ * deviation (a result value, flag, status, patient identifier, code system, or a frame-integrity
118
+ * warning) can **never** be tolerated, so it can never be re-badged here.
119
+ */
120
+ readonly PROFILE_QUIRK_APPLIED: "PROFILE_QUIRK_APPLIED";
121
+ };
122
+ /**
123
+ * Discriminant type for {@link AstmRecordWarning.code}. Narrowing by this code
124
+ * lets consumers write exhaustive `switch` blocks and guarantees a typo-free
125
+ * comparison against {@link WARNING_CODES}.
126
+ */
127
+ type WarningCode = (typeof WARNING_CODES)[keyof typeof WARNING_CODES];
128
+ /**
129
+ * A single Tier-2 warning: a stable code, a value-free human-readable message,
130
+ * and positional context. Plain data, accumulated onto `AstmMessage.warnings`.
131
+ *
132
+ * @example
133
+ * ```ts
134
+ * import type { AstmRecordWarning } from "@cosyte/astm";
135
+ * const w: AstmRecordWarning = {
136
+ * code: "ASTM_RECORD_UNKNOWN_TYPE",
137
+ * message: "Unknown record type.",
138
+ * position: { recordIndex: 2, recordType: "Z" },
139
+ * };
140
+ * ```
141
+ */
142
+ interface AstmRecordWarning {
143
+ readonly code: WarningCode;
144
+ /** Human-readable detail for logs. Never contains a field value. */
145
+ readonly message: string;
146
+ readonly position: AstmPosition;
147
+ /**
148
+ * `true` when an active vendor {@link AstmProfile} *expected* this deviation and re-badged it as a
149
+ * {@link WARNING_CODES.PROFILE_QUIRK_APPLIED}. An `expected` warning does **not** escalate to a
150
+ * thrown `AstmStrictError` in strict mode (the whole point of the profile is that this deviation is
151
+ * known and benign) — it is still recorded, so nothing is hidden. Absent on an untolerated warning.
152
+ */
153
+ readonly expected?: boolean;
154
+ /** The name of the {@link AstmProfile} that tolerated this warning, when `expected`. */
155
+ readonly profile?: string;
156
+ /**
157
+ * When `code` is {@link WARNING_CODES.PROFILE_QUIRK_APPLIED}, the original warning code the profile
158
+ * tolerated — so a consumer can still see *which* deviation was re-badged as expected.
159
+ */
160
+ readonly toleratedCode?: WarningCode;
161
+ }
162
+ /**
163
+ * Build an `ASTM_RECORD_UNKNOWN_TYPE` warning. The record is still surfaced (as
164
+ * an unsupported record), never dropped.
165
+ *
166
+ * @example
167
+ * ```ts
168
+ * import { unknownRecordType } from "@cosyte/astm";
169
+ * unknownRecordType({ recordIndex: 3, recordType: "Z" });
170
+ * ```
171
+ */
172
+ declare function unknownRecordType(position: AstmPosition): AstmRecordWarning;
173
+ /**
174
+ * Build an `ASTM_NONSTANDARD_DELIMITERS` warning. The declared delimiters are
175
+ * used as-is; this only flags that they differ from the canonical set.
176
+ *
177
+ * @example
178
+ * ```ts
179
+ * import { nonStandardDelimiters } from "@cosyte/astm";
180
+ * nonStandardDelimiters({ recordIndex: 0, recordType: "H" });
181
+ * ```
182
+ */
183
+ declare function nonStandardDelimiters(position: AstmPosition): AstmRecordWarning;
184
+ /**
185
+ * Build an `ASTM_UNKNOWN_ESCAPE_SEQUENCE` warning. The sequence is preserved
186
+ * verbatim in the decoded value; the warning body carries neither the sequence
187
+ * nor its surrounding text.
188
+ *
189
+ * @example
190
+ * ```ts
191
+ * import { unknownEscapeSequence } from "@cosyte/astm";
192
+ * unknownEscapeSequence({ recordIndex: 4, recordType: "R", fieldIndex: 4 });
193
+ * ```
194
+ */
195
+ declare function unknownEscapeSequence(position: AstmPosition): AstmRecordWarning;
196
+ /**
197
+ * Build an `ASTM_RECORD_AMBIGUOUS_VALUE_SPLIT` warning. Emitted when a result
198
+ * value field split on an unescaped component delimiter — the full raw value and
199
+ * the split are both surfaced, never a silent truncation.
200
+ *
201
+ * @example
202
+ * ```ts
203
+ * import { ambiguousValueSplit } from "@cosyte/astm";
204
+ * ambiguousValueSplit({ recordIndex: 3, recordType: "R", fieldIndex: 4 });
205
+ * ```
206
+ */
207
+ declare function ambiguousValueSplit(position: AstmPosition): AstmRecordWarning;
208
+ /**
209
+ * Build an `ASTM_RECORD_UNDEFINED_ABNORMAL_FLAG` warning. The flag is surfaced as
210
+ * `undefined` (never coerced to `normal`); the warning carries only the position.
211
+ *
212
+ * @example
213
+ * ```ts
214
+ * import { undefinedAbnormalFlag } from "@cosyte/astm";
215
+ * undefinedAbnormalFlag({ recordIndex: 4, recordType: "R", fieldIndex: 7 });
216
+ * ```
217
+ */
218
+ declare function undefinedAbnormalFlag(position: AstmPosition): AstmRecordWarning;
219
+ /**
220
+ * Build an `ASTM_RECORD_UNDEFINED_RESULT_STATUS` warning. The status is surfaced
221
+ * as `undefined` and, like every non-final status, never reads as active-final.
222
+ *
223
+ * @example
224
+ * ```ts
225
+ * import { undefinedResultStatus } from "@cosyte/astm";
226
+ * undefinedResultStatus({ recordIndex: 4, recordType: "R", fieldIndex: 9 });
227
+ * ```
228
+ */
229
+ declare function undefinedResultStatus(position: AstmPosition): AstmRecordWarning;
230
+ /**
231
+ * Build an `ASTM_RECORD_UNPARSEABLE_REFERENCE_RANGE` warning. The range text is
232
+ * surfaced verbatim as `unparsed`; no bound is fabricated.
233
+ *
234
+ * @example
235
+ * ```ts
236
+ * import { unparseableReferenceRange } from "@cosyte/astm";
237
+ * unparseableReferenceRange({ recordIndex: 4, recordType: "R", fieldIndex: 6 });
238
+ * ```
239
+ */
240
+ declare function unparseableReferenceRange(position: AstmPosition): AstmRecordWarning;
241
+ /**
242
+ * Build an `ASTM_RECORD_UNITS_ABSENT` warning. Emitted when a result carries a
243
+ * numeric value but no units; units are never defaulted, guessed, or converted.
244
+ *
245
+ * @example
246
+ * ```ts
247
+ * import { unitsAbsent } from "@cosyte/astm";
248
+ * unitsAbsent({ recordIndex: 4, recordType: "R", fieldIndex: 5 });
249
+ * ```
250
+ */
251
+ declare function unitsAbsent(position: AstmPosition): AstmRecordWarning;
252
+ /**
253
+ * Build an `ASTM_RECORD_ORPHAN_COMMENT` warning. Emitted when a `C` record had no
254
+ * valid preceding `H`/`P`/`O`/`R` parent; the comment is attached to the message
255
+ * root and surfaced, never dropped.
256
+ *
257
+ * @example
258
+ * ```ts
259
+ * import { orphanComment } from "@cosyte/astm";
260
+ * orphanComment({ recordIndex: 5, recordType: "C" });
261
+ * ```
262
+ */
263
+ declare function orphanComment(position: AstmPosition): AstmRecordWarning;
264
+ /**
265
+ * Build an `ASTM_RECORD_PARTIAL_TIMESTAMP` warning. Emitted when a
266
+ * `YYYYMMDDHHMMSS` value had an odd digit run that truncates a component; the raw
267
+ * run is preserved and no time is fabricated.
268
+ *
269
+ * @example
270
+ * ```ts
271
+ * import { partialTimestamp } from "@cosyte/astm";
272
+ * partialTimestamp({ recordIndex: 2, recordType: "P", fieldIndex: 8 });
273
+ * ```
274
+ */
275
+ declare function partialTimestamp(position: AstmPosition): AstmRecordWarning;
276
+ /**
277
+ * Build an `ASTM_RECORD_UNINTERPRETED_QUERY_STATUS` warning. Emitted when a `Q`
278
+ * record carries a request-information status code; the code set is paywalled, so
279
+ * the status is surfaced verbatim and never mapped to a guessed meaning.
280
+ *
281
+ * @example
282
+ * ```ts
283
+ * import { uninterpretedQueryStatus } from "@cosyte/astm";
284
+ * uninterpretedQueryStatus({ recordIndex: 2, recordType: "Q", fieldIndex: 13 });
285
+ * ```
286
+ */
287
+ declare function uninterpretedQueryStatus(position: AstmPosition): AstmRecordWarning;
288
+ /**
289
+ * Build an `ASTM_RECORD_AMBIGUOUS_MESSAGE_KIND` warning. Emitted when a message
290
+ * carries both a `Q` (request) and an `R` (result) record; the message is
291
+ * classified as a host-query request (the `Q` dominates) and the anomaly is
292
+ * flagged.
293
+ *
294
+ * @example
295
+ * ```ts
296
+ * import { ambiguousMessageKind } from "@cosyte/astm";
297
+ * ambiguousMessageKind({ recordIndex: 0, recordType: "H" });
298
+ * ```
299
+ */
300
+ declare function ambiguousMessageKind(position: AstmPosition): AstmRecordWarning;
301
+ /**
302
+ * Build a `PROFILE_QUIRK_APPLIED` warning — the downgraded form an active vendor profile produces from
303
+ * a deviation it *expects*. The original warning is **not dropped**: its `code` moves to
304
+ * `toleratedCode`, the warning is re-badged `PROFILE_QUIRK_APPLIED`, `expected` is set, and the
305
+ * tolerating profile is named. The original `position` and `message` are preserved (both PHI-free by
306
+ * the same construction as every other factory), so a consumer can filter known, grounded noise while
307
+ * the fact of the deviation, and where it was, survive. A profile can only ever reach this path for a
308
+ * **non-safety-critical** code (enforced at profile-definition time by the safety gate).
309
+ *
310
+ * @param original - The warning the profile tolerated.
311
+ * @param profileName - The name of the tolerating profile.
312
+ * @returns The re-badged, still-informative warning.
313
+ * @example
314
+ * ```ts
315
+ * import { profileQuirkApplied, unknownEscapeSequence } from "@cosyte/astm";
316
+ * const original = unknownEscapeSequence({ recordIndex: 4, recordType: "R", fieldIndex: 5 });
317
+ * const w = profileQuirkApplied(original, "referenceCorpus");
318
+ * w.code; // "PROFILE_QUIRK_APPLIED"
319
+ * w.toleratedCode; // "ASTM_UNKNOWN_ESCAPE_SEQUENCE"
320
+ * ```
321
+ */
322
+ declare function profileQuirkApplied(original: AstmRecordWarning, profileName: string): AstmRecordWarning;
323
+
324
+ /**
325
+ * The ASTM delimiter model and its self-declaration reader.
326
+ *
327
+ * ASTM/CLSI-LIS02 messages are **self-describing**: every `H` (header) record
328
+ * declares the four delimiters it uses, in the bytes immediately after the `H`.
329
+ * A parser MUST read them from the record and MUST NOT hardcode them — a vendor
330
+ * is free to declare non-standard delimiters and a conformant reader follows.
331
+ *
332
+ * The canonical declaration is `H|\^&`:
333
+ *
334
+ * | position | char | role |
335
+ * |--------------------------|------|-------------------------|
336
+ * | immediately after `H` | `\|` | **field** delimiter |
337
+ * | delimiter-definition [0] | `\` | **repeat** delimiter |
338
+ * | delimiter-definition [1] | `^` | **component** delimiter |
339
+ * | delimiter-definition [2] | `&` | **escape** delimiter |
340
+ *
341
+ * Note ASTM differs from HL7 v2 here: ASTM's `\` is the **repeat** delimiter and
342
+ * `&` is the **escape** delimiter, where HL7 uses `\` for escape and `~` for
343
+ * repetition. Getting this mapping wrong silently mis-reads values, so the roles
344
+ * are read positionally from the record, never assumed.
345
+ */
346
+ /**
347
+ * The four ASTM delimiters resolved from an `H` record. Immutable; carried on
348
+ * the parsed message as delimiter provenance.
349
+ *
350
+ * @example
351
+ * ```ts
352
+ * import { readDelimiters } from "@cosyte/astm";
353
+ * const d = readDelimiters("H|\\^&");
354
+ * d.field; // "|"
355
+ * d.repeat; // "\\"
356
+ * d.component; // "^"
357
+ * d.escape; // "&"
358
+ * ```
359
+ */
360
+ interface Delimiters {
361
+ /** Field separator — the char immediately after `H` in the header. */
362
+ readonly field: string;
363
+ /** Repeat (repetition) separator — ASTM `\` by default. */
364
+ readonly repeat: string;
365
+ /** Component separator — ASTM `^` by default. */
366
+ readonly component: string;
367
+ /** Escape character — ASTM `&` by default (introduces `&F&`/`&S&`/`&R&`/`&E&`). */
368
+ readonly escape: string;
369
+ }
370
+ /**
371
+ * The canonical ASTM delimiter set (`H|\^&`). Used only as documentation and as
372
+ * a comparison baseline for the non-standard-delimiter warning — never as a
373
+ * parse-time default (delimiters are always read from the header).
374
+ *
375
+ * @example
376
+ * ```ts
377
+ * import { CANONICAL_DELIMITERS } from "@cosyte/astm";
378
+ * CANONICAL_DELIMITERS.repeat; // "\\"
379
+ * ```
380
+ */
381
+ declare const CANONICAL_DELIMITERS: Delimiters;
382
+ /**
383
+ * Read the four delimiters from a header record's raw text (a single `H`
384
+ * record, its terminator already stripped).
385
+ *
386
+ * Returns `{ ok: false }` when the record cannot declare all four delimiters —
387
+ * it is shorter than `H` + a field separator + a 3-char delimiter definition —
388
+ * which the caller escalates to the `ASTM_RECORD_UNDECLARED_DELIMITERS` fatal.
389
+ * This function does not throw; delimiter resolution and the fatal decision are
390
+ * kept separate so the reader stays pure and testable.
391
+ *
392
+ * @param headerRecord - The raw `H` record text (no trailing CR/LF).
393
+ * @returns The resolved delimiters, or a declared failure.
394
+ * @example
395
+ * ```ts
396
+ * import { readDelimiters } from "@cosyte/astm";
397
+ * const d = readDelimiters("H|\\^&|||sender");
398
+ * d.field; // "|"
399
+ * ```
400
+ */
401
+ declare function readDelimiters(headerRecord: string): Delimiters | undefined;
402
+ /**
403
+ * Whether a resolved delimiter set differs from the canonical `H|\^&`. A `true`
404
+ * result is a tolerated vendor quirk, surfaced as a value-free warning so a
405
+ * consumer can notice a non-standard stream without the parser refusing it.
406
+ *
407
+ * @example
408
+ * ```ts
409
+ * import { isNonStandard, readDelimiters } from "@cosyte/astm";
410
+ * isNonStandard(readDelimiters("H|\\^&")!); // false
411
+ * ```
412
+ */
413
+ declare function isNonStandard(d: Delimiters): boolean;
414
+
415
+ /**
416
+ * The ASTM date/time value — `YYYYMMDDHHMMSS`, precision-preserving, no timezone.
417
+ *
418
+ * ASTM timestamps are a run of digits, most-significant first, truncated at
419
+ * whatever precision the instrument reports. There is **no timezone field**:
420
+ * the value is local to the instrument and this parser never assumes UTC. A
421
+ * date-only value (`YYYYMMDD`) is a normal, complete value at day precision —
422
+ * **not** an error and never zero-filled into a fake time.
423
+ */
424
+ /** The precision to which an {@link AstmDate} is populated. */
425
+ type AstmDatePrecision = "year" | "month" | "day" | "hour" | "minute" | "second";
426
+ /**
427
+ * A parsed ASTM date/time. Immutable plain data; the populated fields extend
428
+ * exactly as far as {@link AstmDate.precision}. Absent components are left
429
+ * `undefined` rather than defaulted, so a consumer can tell "midnight" from
430
+ * "no time given". No timezone is modeled — the value is instrument-local.
431
+ *
432
+ * @example
433
+ * ```ts
434
+ * import { parseAstmDate } from "@cosyte/astm";
435
+ * const d = parseAstmDate("20240315");
436
+ * d?.precision; // "day"
437
+ * d?.hour; // undefined (not 0)
438
+ * ```
439
+ */
440
+ interface AstmDate {
441
+ /** The raw digit string as it appeared on the wire. */
442
+ readonly raw: string;
443
+ readonly year: number;
444
+ readonly month?: number;
445
+ readonly day?: number;
446
+ readonly hour?: number;
447
+ readonly minute?: number;
448
+ readonly second?: number;
449
+ /** How far the components are populated. */
450
+ readonly precision: AstmDatePrecision;
451
+ /**
452
+ * `true` when the digit run does **not** align to a whole-component boundary — an odd number of
453
+ * digits that cuts a two-digit component (month/day/hour/minute/second) in half (lengths 5, 7, 9,
454
+ * 11, 13). The full run is preserved in {@link AstmDate.raw} and the structured value is truncated
455
+ * to the last **complete** component — the dangling digit is **never zero-filled into a fabricated
456
+ * time**. Absent (never `false`) for a clean value. A caller surfaces this as a value-free
457
+ * `ASTM_RECORD_PARTIAL_TIMESTAMP` warning.
458
+ */
459
+ readonly truncated?: true;
460
+ }
461
+ /**
462
+ * Parse an ASTM `YYYYMMDDHHMMSS` value, preserving whatever precision is
463
+ * present. Returns `undefined` for a value that is not a usable timestamp (fewer
464
+ * than four leading digits for the year, or non-digit content) — a caller keeps
465
+ * the raw field text either way, so nothing is lost. A partial value is parsed,
466
+ * never rejected.
467
+ *
468
+ * Extra trailing digits beyond seconds (fractional seconds some vendors append)
469
+ * are ignored for the structured value; the full input remains in {@link AstmDate.raw}.
470
+ *
471
+ * @param raw - The raw field text.
472
+ * @returns The parsed date, or `undefined` when it is not a timestamp.
473
+ * @example
474
+ * ```ts
475
+ * import { parseAstmDate } from "@cosyte/astm";
476
+ * parseAstmDate("20240315093000")?.precision; // "second"
477
+ * parseAstmDate("202403")?.precision; // "month"
478
+ * ```
479
+ */
480
+ declare function parseAstmDate(raw: string): AstmDate | undefined;
481
+ /**
482
+ * Render an {@link AstmDate} as an ISO-8601-*like* string truncated to its
483
+ * precision, with **no** `Z` and **no** offset — because ASTM carries no
484
+ * timezone and appending one would fabricate information. A consumer that knows
485
+ * the instrument's zone can attach it; this function never assumes UTC.
486
+ *
487
+ * @param d - The date to render.
488
+ * @returns e.g. `"2024-03-15T09:30"` (minute precision) or `"2024-03"` (month).
489
+ * @example
490
+ * ```ts
491
+ * import { astmDateToLocalISO, parseAstmDate } from "@cosyte/astm";
492
+ * astmDateToLocalISO(parseAstmDate("20240315")!); // "2024-03-15"
493
+ * ```
494
+ */
495
+ declare function astmDateToLocalISO(d: AstmDate): string;
496
+
497
+ /**
498
+ * Code-system **provenance recognition** for the ASTM Universal Test ID.
499
+ *
500
+ * ASTM carries a test identifier as a caret-component field (`^^^687`): the
501
+ * spec's component order is LOINC-slot / test-name / coding-scheme / local-code.
502
+ * In the field the way real analyzers emit it:
503
+ *
504
+ * - **component 1** is a LOINC slot — *almost always empty*; a few vendors place
505
+ * an inline LOINC when one exists;
506
+ * - **component 4** is the vendor/local code — the identifier that is actually
507
+ * present, and therefore the **primary** identifier here.
508
+ *
509
+ * Following the sibling parsers' conservative posture, this module is a
510
+ * **structural recognizer, not a dictionary**: it tags where a code came from
511
+ * (its provenance) and surfaces every code verbatim. It does not look codes up,
512
+ * does not validate them, and bundles no LOINC/SNOMED tables.
513
+ */
514
+ /** Where a Universal Test ID's usable identifier came from. */
515
+ type UniversalTestIdProvenance =
516
+ /** Component 1 (the LOINC slot) is populated — a candidate LOINC, recognized not validated. */
517
+ "inline-loinc-candidate"
518
+ /** Component 4 (the vendor/local code) carries the identifier. */
519
+ | "local-code"
520
+ /** Only the test name (component 2) is present — no code. */
521
+ | "name-only"
522
+ /** Nothing usable in the field. */
523
+ | "empty";
524
+ /**
525
+ * A recognized ASTM Universal Test ID. All components are surfaced verbatim
526
+ * (already escape-decoded by the tokenizer); nothing is looked up.
527
+ *
528
+ * @example
529
+ * ```ts
530
+ * import { recognizeUniversalTestId } from "@cosyte/astm";
531
+ * const u = recognizeUniversalTestId(["", "", "", "687"]);
532
+ * u.localCode; // "687"
533
+ * u.provenance; // "local-code"
534
+ * ```
535
+ */
536
+ interface UniversalTestId {
537
+ /** The field's components, verbatim and in order. */
538
+ readonly components: readonly string[];
539
+ /** Component 1 when populated — a *candidate* LOINC (provenance only, never validated). */
540
+ readonly loincCandidate?: string;
541
+ /** Component 2 — the test / battery name, when present. */
542
+ readonly testName?: string;
543
+ /** Component 3 — the coding-scheme selector, when present. */
544
+ readonly codingScheme?: string;
545
+ /** Component 4 — the vendor/local code: the primary identifier when no inline LOINC is given. */
546
+ readonly localCode?: string;
547
+ /** Where the primary identifier came from. */
548
+ readonly provenance: UniversalTestIdProvenance;
549
+ }
550
+ /**
551
+ * Recognize a Universal Test ID from a field's already-decoded components.
552
+ *
553
+ * @param components - The component strings (escape-decoded), in order.
554
+ * @returns The recognized, provenance-tagged Universal Test ID.
555
+ * @example
556
+ * ```ts
557
+ * import { recognizeUniversalTestId, primaryCode } from "@cosyte/astm";
558
+ * primaryCode(recognizeUniversalTestId(["", "Glucose", "L", "687"])); // "687"
559
+ * ```
560
+ */
561
+ declare function recognizeUniversalTestId(components: readonly string[]): UniversalTestId;
562
+ /**
563
+ * The primary code to key a result on: the inline LOINC candidate when a vendor
564
+ * supplied one, otherwise the local (vendor) code. Returns `undefined` when the
565
+ * field carries no code at all (name-only or empty) — never a guess.
566
+ *
567
+ * @param u - A recognized Universal Test ID.
568
+ * @returns The primary code, or `undefined`.
569
+ * @example
570
+ * ```ts
571
+ * import { primaryCode, recognizeUniversalTestId } from "@cosyte/astm";
572
+ * primaryCode(recognizeUniversalTestId(["2345-7", "Glucose", "LN", "687"])); // "2345-7"
573
+ * ```
574
+ */
575
+ declare function primaryCode(u: UniversalTestId): string | undefined;
576
+
577
+ /**
578
+ * Positional context for frame-codec warnings and errors.
579
+ *
580
+ * **PHI discipline (the whole point).** A frame deviation is located by **frame
581
+ * number + byte offset** and nothing else — never the record text a frame
582
+ * carries. A frame warning or error may be logged verbatim without ever leaking a
583
+ * result value, a patient identifier, or any other field the reassembled record
584
+ * bytes contain.
585
+ */
586
+ /**
587
+ * Where in a framed byte stream a warning or error originated. Both members are
588
+ * positional; neither is a value.
589
+ *
590
+ * @example
591
+ * ```ts
592
+ * import type { AstmFramePosition } from "@cosyte/astm";
593
+ * const at: AstmFramePosition = { frameNumber: 2, byteOffset: 251 };
594
+ * ```
595
+ */
596
+ interface AstmFramePosition {
597
+ /**
598
+ * The frame's sequence number (`0`–`7`) when it could be read, or `undefined`
599
+ * when the frame was so truncated the number was not present.
600
+ */
601
+ readonly frameNumber?: number;
602
+ /** Byte offset of the `STX` that opened the frame, within the decoded stream. */
603
+ readonly byteOffset: number;
604
+ }
605
+
606
+ /**
607
+ * The **frame** warning registry (`ASTM_FRAME_*`) for the ASTM/CLSI-LIS01 codec —
608
+ * the second of the package's two registries, alongside the record layer's
609
+ * `ASTM_RECORD_*` (both share only the `EMPTY_INPUT` fatal).
610
+ *
611
+ * A frame warning is the lenient codec's record of a tolerated byte-level
612
+ * deviation: it never crashes, never invents a value, and — critically — never
613
+ * lets an untrusted frame be silently merged into a record. Every warning carries
614
+ * a stable code plus an {@link AstmFramePosition} (frame number + byte offset) and
615
+ * **never** the record bytes a frame carries (PHI discipline). Consumers compare
616
+ * `warning.code === FRAME_WARNING_CODES.<CODE>`; renaming a code is a **breaking
617
+ * change**.
618
+ */
619
+
620
+ /**
621
+ * Stable string codes for every frame-codec warning. `key === value` so
622
+ * `Object.values(...)` yields a stable snapshot set.
623
+ *
624
+ * @example
625
+ * ```ts
626
+ * import { decodeAstmFrames, FRAME_WARNING_CODES } from "@cosyte/astm";
627
+ * const bytes = new Uint8Array([]); // ...a framed stream...
628
+ * void bytes;
629
+ * FRAME_WARNING_CODES.ASTM_FRAME_BAD_CHECKSUM; // "ASTM_FRAME_BAD_CHECKSUM"
630
+ * ```
631
+ */
632
+ declare const FRAME_WARNING_CODES: {
633
+ /**
634
+ * A frame's two-hex-char checksum did not match the modulo-256 sum recomputed over its bytes. The
635
+ * frame is surfaced with `trusted: false` and its text is **never merged** into a reassembled
636
+ * record (default **warn** in lenient mode, escalated to a thrown error in `strict`). Corruption is
637
+ * never silently trusted — the "checksums are routinely not validated" claim was refuted; we
638
+ * validate.
639
+ */
640
+ readonly ASTM_FRAME_BAD_CHECKSUM: "ASTM_FRAME_BAD_CHECKSUM";
641
+ /**
642
+ * A frame's sequence number was not the expected next value (`1 → … → 7 → 0 → …`) — a frame was
643
+ * possibly dropped. The stream is **never silently concatenated across the gap** as if contiguous;
644
+ * the in-progress record is tainted and not emitted as a clean reassembly.
645
+ */
646
+ readonly ASTM_FRAME_SEQUENCE_GAP: "ASTM_FRAME_SEQUENCE_GAP";
647
+ /**
648
+ * A frame opened with `STX` but no valid terminator + checksum was found before the stream ended
649
+ * (or before the next `STX`), or a record's frames ended on an intermediate `ETB` with no final
650
+ * `ETX`. The partial bytes are surfaced flagged untrusted and **no partial record is invented**
651
+ * (warn in lenient, thrown in `strict`).
652
+ */
653
+ readonly ASTM_FRAME_UNTERMINATED: "ASTM_FRAME_UNTERMINATED";
654
+ /**
655
+ * A frame's record text exceeded the 240-byte limit without a split. The frame is still surfaced
656
+ * (and, if its checksum validates, reassembled) — the deviation is flagged, not silently dropped
657
+ * (warn in lenient, thrown in `strict`).
658
+ */
659
+ readonly ASTM_FRAME_OVERSIZE: "ASTM_FRAME_OVERSIZE";
660
+ };
661
+ /**
662
+ * Discriminant type for {@link AstmFrameWarning.code}. Narrowing by this code lets
663
+ * consumers write exhaustive `switch` blocks against {@link FRAME_WARNING_CODES}.
664
+ */
665
+ type FrameWarningCode = (typeof FRAME_WARNING_CODES)[keyof typeof FRAME_WARNING_CODES];
666
+ /**
667
+ * A single frame-codec warning: a stable code, a value-free human-readable
668
+ * message, and positional context (frame number + byte offset).
669
+ *
670
+ * @example
671
+ * ```ts
672
+ * import type { AstmFrameWarning } from "@cosyte/astm";
673
+ * const w: AstmFrameWarning = {
674
+ * code: "ASTM_FRAME_BAD_CHECKSUM",
675
+ * message: "Frame checksum mismatch.",
676
+ * position: { frameNumber: 2, byteOffset: 251 },
677
+ * };
678
+ * ```
679
+ */
680
+ interface AstmFrameWarning {
681
+ readonly code: FrameWarningCode;
682
+ /** Human-readable detail for logs. Never contains the frame's record bytes. */
683
+ readonly message: string;
684
+ readonly position: AstmFramePosition;
685
+ }
686
+ /**
687
+ * Build an `ASTM_FRAME_BAD_CHECKSUM` warning. The frame is surfaced flagged
688
+ * untrusted and never merged into a record; the warning carries position only.
689
+ *
690
+ * @example
691
+ * ```ts
692
+ * import { frameBadChecksum } from "@cosyte/astm";
693
+ * frameBadChecksum({ frameNumber: 2, byteOffset: 251 });
694
+ * ```
695
+ */
696
+ declare function frameBadChecksum(position: AstmFramePosition): AstmFrameWarning;
697
+ /**
698
+ * Build an `ASTM_FRAME_SEQUENCE_GAP` warning. A frame was possibly dropped; the
699
+ * record is not concatenated across the gap as if contiguous.
700
+ *
701
+ * @example
702
+ * ```ts
703
+ * import { frameSequenceGap } from "@cosyte/astm";
704
+ * frameSequenceGap({ frameNumber: 3, byteOffset: 502 });
705
+ * ```
706
+ */
707
+ declare function frameSequenceGap(position: AstmFramePosition): AstmFrameWarning;
708
+ /**
709
+ * Build an `ASTM_FRAME_UNTERMINATED` warning. The partial bytes are surfaced
710
+ * flagged untrusted; no partial record is invented.
711
+ *
712
+ * @example
713
+ * ```ts
714
+ * import { frameUnterminated } from "@cosyte/astm";
715
+ * frameUnterminated({ byteOffset: 730 });
716
+ * ```
717
+ */
718
+ declare function frameUnterminated(position: AstmFramePosition): AstmFrameWarning;
719
+ /**
720
+ * Build an `ASTM_FRAME_OVERSIZE` warning. The 240-byte text limit was exceeded
721
+ * without a split; the frame is flagged, not silently dropped.
722
+ *
723
+ * @example
724
+ * ```ts
725
+ * import { frameOversize } from "@cosyte/astm";
726
+ * frameOversize({ frameNumber: 1, byteOffset: 0 });
727
+ * ```
728
+ */
729
+ declare function frameOversize(position: AstmFramePosition): AstmFrameWarning;
730
+
731
+ /**
732
+ * The **LTP** warning registry (`ASTM_LTP_*`) for the ASTM/CLSI-LIS01 protocol
733
+ * layer — the third of the package's registries, alongside the record layer's
734
+ * `ASTM_RECORD_*` and the frame codec's `ASTM_FRAME_*`.
735
+ *
736
+ * An LTP warning is the protocol reducer's (or the transport detector's) record
737
+ * of a tolerated session-level deviation: an ambiguous framed/raw stream, a
738
+ * control event that arrived in a state that did not expect it, or a frame the
739
+ * receiver had to reject (`NAK`) rather than accept. Every warning carries a
740
+ * stable code plus, at most, a **frame number** — never the record bytes a frame
741
+ * carries (PHI discipline). Consumers compare `warning.code ===
742
+ * LTP_WARNING_CODES.<CODE>`; renaming a code is a **breaking change**.
743
+ */
744
+ /**
745
+ * Stable string codes for every LTP protocol / transport warning. `key === value`
746
+ * so `Object.values(...)` yields a stable snapshot set.
747
+ *
748
+ * @example
749
+ * ```ts
750
+ * import { LTP_WARNING_CODES } from "@cosyte/astm";
751
+ * LTP_WARNING_CODES.ASTM_LTP_FRAME_REJECTED; // "ASTM_LTP_FRAME_REJECTED"
752
+ * ```
753
+ */
754
+ declare const LTP_WARNING_CODES: {
755
+ /**
756
+ * The transport detector could not tell a framed stream from a raw (unframed) one from its leading
757
+ * byte — it was neither `STX`/`ENQ` nor a bare record letter. It **defaults to framed** and warns,
758
+ * never guessing silently into data loss; a profile override forces the mode. (cobas b121 drops
759
+ * framing over TCP; cobas 4800 / Iguana retain it — both realities exist, so an unrecognizable lead
760
+ * is defaulted, not assumed.)
761
+ */
762
+ readonly ASTM_LTP_AMBIGUOUS_TRANSPORT: "ASTM_LTP_AMBIGUOUS_TRANSPORT";
763
+ /**
764
+ * A control event arrived in a protocol state that did not expect it — e.g. an inbound `ACK`/`NAK`
765
+ * at a receiver (which sends, never receives, those), or an `ENQ` mid-transfer. The event is
766
+ * surfaced and handled defensively (an unexpected `ACK`/`NAK` is **never** read as acceptance of
767
+ * data); it never advances the transfer as if valid.
768
+ */
769
+ readonly ASTM_LTP_UNEXPECTED_EVENT: "ASTM_LTP_UNEXPECTED_EVENT";
770
+ /**
771
+ * The receiver rejected a received frame with a `NAK` instead of accepting it — because its
772
+ * checksum failed, it was unterminated, or its frame number was out of sequence. The frame's text
773
+ * is **never** appended to the record and the transfer does **not** advance; the receiver awaits the
774
+ * sender's retransmit. This is the protocol-level face of the frame codec's fail-safe: a bad frame
775
+ * drives retransmit, not acceptance.
776
+ */
777
+ readonly ASTM_LTP_FRAME_REJECTED: "ASTM_LTP_FRAME_REJECTED";
778
+ };
779
+ /**
780
+ * Discriminant type for {@link AstmLtpWarning.code}. Narrowing by this code lets
781
+ * consumers write exhaustive `switch` blocks against {@link LTP_WARNING_CODES}.
782
+ */
783
+ type LtpWarningCode = (typeof LTP_WARNING_CODES)[keyof typeof LTP_WARNING_CODES];
784
+ /**
785
+ * A single LTP protocol / transport warning: a stable code, a value-free
786
+ * human-readable message, and — for a frame-scoped deviation — the frame number
787
+ * only. Never carries a frame's record bytes.
788
+ *
789
+ * @example
790
+ * ```ts
791
+ * import type { AstmLtpWarning } from "@cosyte/astm";
792
+ * const w: AstmLtpWarning = {
793
+ * code: "ASTM_LTP_FRAME_REJECTED",
794
+ * message: "Frame rejected — NAK sent, retransmit expected.",
795
+ * frameNumber: 2,
796
+ * };
797
+ * ```
798
+ */
799
+ interface AstmLtpWarning {
800
+ readonly code: LtpWarningCode;
801
+ /** Human-readable detail for logs. Never contains the frame's record bytes. */
802
+ readonly message: string;
803
+ /** The frame's sequence number when the warning is frame-scoped; absent otherwise. */
804
+ readonly frameNumber?: number;
805
+ }
806
+ /**
807
+ * Build an `ASTM_LTP_AMBIGUOUS_TRANSPORT` warning. The detector defaulted to
808
+ * framed; a profile override can force raw.
809
+ *
810
+ * @example
811
+ * ```ts
812
+ * import { ltpAmbiguousTransport } from "@cosyte/astm";
813
+ * ltpAmbiguousTransport();
814
+ * ```
815
+ */
816
+ declare function ltpAmbiguousTransport(): AstmLtpWarning;
817
+ /**
818
+ * Build an `ASTM_LTP_UNEXPECTED_EVENT` warning. The event was surfaced and handled
819
+ * defensively; it never advanced the transfer as if valid.
820
+ *
821
+ * @param frameNumber - The frame number, when the unexpected event was a frame; omitted otherwise.
822
+ * @example
823
+ * ```ts
824
+ * import { ltpUnexpectedEvent } from "@cosyte/astm";
825
+ * ltpUnexpectedEvent();
826
+ * ```
827
+ */
828
+ declare function ltpUnexpectedEvent(frameNumber?: number): AstmLtpWarning;
829
+ /**
830
+ * Build an `ASTM_LTP_FRAME_REJECTED` warning. The receiver sent a `NAK`; the
831
+ * frame's text was never appended and the transfer did not advance.
832
+ *
833
+ * @param frameNumber - The rejected frame's sequence number, when it could be read.
834
+ * @example
835
+ * ```ts
836
+ * import { ltpFrameRejected } from "@cosyte/astm";
837
+ * ltpFrameRejected(2);
838
+ * ```
839
+ */
840
+ declare function ltpFrameRejected(frameNumber?: number): AstmLtpWarning;
841
+
842
+ /**
843
+ * Framed-vs-raw transport auto-detection for ASTM/CLSI-LIS01.
844
+ *
845
+ * ASTM transport is not uniform, and that is what breaks naive parsers. **Serial**
846
+ * (RS-232) always uses full E1381 framing. Over **TCP it varies within a single
847
+ * vendor**: the Roche cobas 4800 and iNTERFACEWARE Iguana retain the full
848
+ * `ENQ`/`ACK` + `STX`/checksum framing over TCP (**framed-TCP**), while the cobas
849
+ * b121 drops all low-level framing — "TCP itself ensures correctness" — and streams
850
+ * de-framed record bytes directly (**raw-TCP**). A reader must handle both and must
851
+ * not assume the handshake is present.
852
+ *
853
+ * {@link detectFraming} decides which it is from the stream's leading byte: `STX`
854
+ * or `ENQ` ⇒ framed (feed it to the frame codec + {@link ltpReduce}); a bare record
855
+ * letter ⇒ raw (feed it straight to `parseAstmRecords`). Anything else is
856
+ * **ambiguous**, and the fail-safe is to **default to framed** and warn — never to
857
+ * guess silently into data loss — with an explicit override for a profile that
858
+ * knows better.
859
+ */
860
+
861
+ /**
862
+ * The transport framing of an ASTM byte stream: `"framed"` (E1381 `STX`/checksum
863
+ * frames, the serial and framed-TCP realities) or `"raw"` (framing dropped,
864
+ * de-framed record bytes streamed directly — the raw-TCP reality).
865
+ */
866
+ type AstmFraming = "framed" | "raw";
867
+ /**
868
+ * Options for {@link detectFraming}.
869
+ */
870
+ interface DetectFramingOptions {
871
+ /**
872
+ * A profile-supplied override. When set, detection is bypassed entirely and this
873
+ * value is returned with no warning — the way a Phase-8 vendor profile forces
874
+ * raw for a cobas b121 or framed for a cobas 4800 regardless of the leading byte.
875
+ */
876
+ readonly override?: AstmFraming;
877
+ }
878
+ /**
879
+ * The result of {@link detectFraming}: the decided framing plus any warning
880
+ * (exactly one `ASTM_LTP_AMBIGUOUS_TRANSPORT` when the lead was unrecognizable and
881
+ * the mode was defaulted; empty otherwise).
882
+ */
883
+ interface DetectFramingResult {
884
+ /** The decided transport framing. */
885
+ readonly framing: AstmFraming;
886
+ /** `true` when the lead byte was unrecognizable and `framing` was defaulted (not inferred). */
887
+ readonly defaulted: boolean;
888
+ /** The detection warnings (a single ambiguity warning, or none). */
889
+ readonly warnings: readonly AstmLtpWarning[];
890
+ }
891
+ /**
892
+ * Detect whether an ASTM byte stream is framed or raw from its leading byte.
893
+ *
894
+ * - Leading `STX` (`0x02`) or `ENQ` (`0x05`) ⇒ `"framed"`.
895
+ * - A leading bare record letter (`H`/`P`/`O`/`R`/`C`/`Q`/`M`/`S`/`L`) ⇒ `"raw"`.
896
+ * - Anything else (including an empty stream) is **ambiguous** ⇒ defaults to
897
+ * `"framed"` and emits one `ASTM_LTP_AMBIGUOUS_TRANSPORT` warning.
898
+ *
899
+ * An `override` short-circuits all of the above, returning the forced mode with no
900
+ * warning.
901
+ *
902
+ * @param bytes - The stream to inspect (only the leading byte is read).
903
+ * @param options - Detection options; an `override` forces the result.
904
+ * @returns The decided framing, whether it was defaulted, and any warning.
905
+ * @example
906
+ * ```ts
907
+ * import { detectFraming } from "@cosyte/astm";
908
+ * detectFraming(new Uint8Array([0x02])).framing; // "framed" (STX)
909
+ * detectFraming(new Uint8Array([0x48])).framing; // "raw" (leading "H")
910
+ * detectFraming(new Uint8Array([0x2a])).framing; // "framed" (ambiguous → defaulted, + warning)
911
+ * detectFraming(new Uint8Array([0x48]), { override: "framed" }).framing; // "framed" (forced)
912
+ * ```
913
+ */
914
+ declare function detectFraming(bytes: Uint8Array, options?: DetectFramingOptions): DetectFramingResult;
915
+
916
+ /**
917
+ * Public type surface for the `@cosyte/astm` vendor-profile subsystem. An
918
+ * {@link AstmProfile} bundles a **named, provenance-backed set of expected
919
+ * deviations** ("quirks") a class of real-world ASTM streams is known to carry,
920
+ * plus an optional **transport override** (the raw-vs-framed-TCP knob). It mirrors
921
+ * the sibling `@cosyte/hl7` `defineProfile` and `@cosyte/ccda` `defineCcdaProfile`
922
+ * shape (`name` / `lineage` / `describe()` / `extends`-merge) but models ASTM's
923
+ * quirks — a warning code the stream is expected to trip, and its framing reality —
924
+ * rather than HL7 v2 Z-segments or C-CDA template deviations.
925
+ *
926
+ * A profile **never changes an extracted value**. It operates purely at the
927
+ * warning layer (downgrading an *already-emitted*, non-safety-critical warning to
928
+ * a {@link WARNING_CODES.PROFILE_QUIRK_APPLIED}) and at the transport-detection
929
+ * layer (forcing framed/raw before a single byte is parsed). The two load-bearing
930
+ * safety rules, enforced by `defineAstmProfile`:
931
+ *
932
+ * 1. **A profile can never tolerate a safety-critical warning code** — a result
933
+ * value, abnormal flag, result status, reference range, units, patient/comment
934
+ * context, message-kind ambiguity, code system, or any frame/LTP integrity
935
+ * warning (see `src/profiles/safety.ts`). Naming one is a *definition-time
936
+ * throw*, not a silent relaxation — so a profile can never make a bad checksum
937
+ * "ok" or a cancelled result read "final."
938
+ * 2. **A tolerated deviation is downgraded, never dropped.** The parser still
939
+ * records it (re-coded {@link WARNING_CODES.PROFILE_QUIRK_APPLIED}, flagged
940
+ * `expected: true`, carrying the original `toleratedCode`) so nothing is
941
+ * silently hidden — Postel's Law with a receipt.
942
+ */
943
+
944
+ /**
945
+ * Any warning code from any of the three registries — the record layer
946
+ * (`ASTM_RECORD_*` / `ASTM_NONSTANDARD_DELIMITERS` / `ASTM_UNKNOWN_ESCAPE_SEQUENCE`),
947
+ * the frame codec (`ASTM_FRAME_*`), or the LTP protocol layer (`ASTM_LTP_*`). A
948
+ * {@link QuirkTolerance} may *name* any of them, but the safety gate refuses all
949
+ * but the small, benign, record-layer subset — so the union exists mainly to let
950
+ * the gate reject a frame/LTP code with a precise, typed message.
951
+ */
952
+ type AnyAstmWarningCode = WarningCode | FrameWarningCode | LtpWarningCode;
953
+ /**
954
+ * Provenance for an {@link AstmProfile} — the **real, cited public artifact** a
955
+ * profile's quirks are grounded in. A quirk is encoded only when a real
956
+ * (de-identified) document or a redistributable reference corpus grounds it; this
957
+ * record is where that grounding is stated, so a reviewer can trace every
958
+ * tolerated deviation back to evidence rather than invention.
959
+ *
960
+ * @example
961
+ * ```ts
962
+ * import type { AstmProfileProvenance } from "@cosyte/astm";
963
+ * const prov: AstmProfileProvenance = {
964
+ * source: "kxepal/python-astm codec.py",
965
+ * reference: "https://github.com/kxepal/python-astm/blob/master/astm/codec.py",
966
+ * retrieved: "2026-07-21",
967
+ * };
968
+ * ```
969
+ */
970
+ interface AstmProfileProvenance {
971
+ /** Short human-readable name of the grounding source (corpus, transcript, or spec). */
972
+ readonly source: string;
973
+ /** A citation the grounding can be traced to — a URL, DOI, or repo+path. */
974
+ readonly reference: string;
975
+ /** When the grounding was last verified (ISO date) or the pinned commit SHA. */
976
+ readonly retrieved?: string;
977
+ /** Optional clarifying note about what in the source grounds the quirks. */
978
+ readonly note?: string;
979
+ }
980
+ /**
981
+ * Optional structural narrowing for a {@link QuirkTolerance}. When present, the
982
+ * tolerance applies only to warnings whose PHI-free {@link AstmPosition} matches
983
+ * every provided field — so a profile can expect a deviation on one record type
984
+ * (e.g. an unknown escape only inside `R` result values) without blanket-tolerating
985
+ * it everywhere. Matching is on **structural identifiers only** (record-type
986
+ * letter, 1-based field index); there is no matching on any field *value*, by
987
+ * construction.
988
+ *
989
+ * @example
990
+ * ```ts
991
+ * import type { AstmQuirkMatch } from "@cosyte/astm";
992
+ * const onlyResultUnits: AstmQuirkMatch = { recordType: "R", fieldIndex: 5 };
993
+ * ```
994
+ */
995
+ interface AstmQuirkMatch {
996
+ /** Match only warnings carrying this record-type letter in their position. */
997
+ readonly recordType?: string;
998
+ /** Match only warnings carrying this 1-based field index in their position. */
999
+ readonly fieldIndex?: number;
1000
+ }
1001
+ /**
1002
+ * One expected deviation declared by a profile. `code` names an **existing,
1003
+ * non-safety-critical** warning code the profile expects; `rationale` documents
1004
+ * why (grounded in the profile's {@link AstmProfileProvenance}); optional `match`
1005
+ * narrows it to a structural location. `defineAstmProfile` throws if `code` is
1006
+ * safety-critical or is not a real warning code, or if `rationale` is empty.
1007
+ *
1008
+ * @example
1009
+ * ```ts
1010
+ * import type { AstmQuirkTolerance } from "@cosyte/astm";
1011
+ * const t: AstmQuirkTolerance = {
1012
+ * code: "ASTM_UNKNOWN_ESCAPE_SEQUENCE",
1013
+ * rationale: "Corpus stacks that treat '&' as literal data emit non-standard escape bodies.",
1014
+ * };
1015
+ * ```
1016
+ */
1017
+ interface AstmQuirkTolerance {
1018
+ /** The existing, non-safety-critical warning code this profile expects. */
1019
+ readonly code: AnyAstmWarningCode;
1020
+ /** Why the profile expects this deviation — grounded in its provenance. */
1021
+ readonly rationale: string;
1022
+ /** Optional structural narrowing (record type / field index). */
1023
+ readonly match?: AstmQuirkMatch;
1024
+ }
1025
+ /**
1026
+ * A frozen, immutable vendor/conformance profile. Produced by
1027
+ * {@link defineAstmProfile}; consumers pass it to `parseAstmRecords(raw, { profile })`
1028
+ * (or register it as the process default), and feed its {@link AstmProfile.transport}
1029
+ * to `detectFraming(bytes, { override })`. Hand-authoring the object literal is
1030
+ * supported but discouraged — the factory validates the safety rules and attaches
1031
+ * `describe()`.
1032
+ *
1033
+ * @example
1034
+ * ```ts
1035
+ * import { parseAstmRecords, astmProfiles } from "@cosyte/astm";
1036
+ * const msg = parseAstmRecords(raw, { profile: astmProfiles.referenceCorpus });
1037
+ * msg.profile?.name; // "referenceCorpus"
1038
+ * ```
1039
+ */
1040
+ interface AstmProfile {
1041
+ /** The profile's unique name (registry key / attribution label). */
1042
+ readonly name: string;
1043
+ /** Optional human-readable description. */
1044
+ readonly description?: string;
1045
+ /** Resolved lineage — `[...parents, name]`, first-occurrence deduped. */
1046
+ readonly lineage: readonly string[];
1047
+ /** The expected, non-safety-critical deviations this profile tolerates. */
1048
+ readonly tolerate: readonly AstmQuirkTolerance[];
1049
+ /**
1050
+ * The raw-vs-framed-TCP override. When set, a consumer forces this framing via
1051
+ * `detectFraming(bytes, { override: profile.transport })` — the way a profile
1052
+ * that knows a vendor's transport reality (e.g. framing dropped over raw TCP)
1053
+ * bypasses leading-byte auto-detection. Absent means "let detection decide."
1054
+ */
1055
+ readonly transport?: AstmFraming;
1056
+ /** The cited public grounding for this profile's quirks (absent for `default`). */
1057
+ readonly provenance?: AstmProfileProvenance;
1058
+ /** Multi-line human-readable summary; always present on factory-built profiles. */
1059
+ readonly describe?: () => string;
1060
+ }
1061
+ /**
1062
+ * Options accepted by {@link defineAstmProfile}. Mirrors the {@link AstmProfile}
1063
+ * shape minus the derived `lineage`/`describe`, plus the `extends` input key. Every
1064
+ * field except `name` is optional.
1065
+ *
1066
+ * @example
1067
+ * ```ts
1068
+ * import { defineAstmProfile, type DefineAstmProfileOptions } from "@cosyte/astm";
1069
+ * const opts: DefineAstmProfileOptions = {
1070
+ * name: "my-analyzer",
1071
+ * transport: "raw",
1072
+ * tolerate: [{ code: "ASTM_RECORD_UNKNOWN_TYPE", rationale: "partial vendor grammar" }],
1073
+ * };
1074
+ * const p = defineAstmProfile(opts);
1075
+ * ```
1076
+ */
1077
+ interface DefineAstmProfileOptions {
1078
+ readonly name: string;
1079
+ readonly description?: string;
1080
+ readonly tolerate?: readonly AstmQuirkTolerance[];
1081
+ readonly transport?: AstmFraming;
1082
+ readonly provenance?: AstmProfileProvenance;
1083
+ readonly extends?: AstmProfile | readonly AstmProfile[];
1084
+ }
1085
+
1086
+ /**
1087
+ * Safety-critical result semantics for the ASTM `R` (result) record — Phase 2.
1088
+ *
1089
+ * Phase 1 surfaced the `R` record's flag, status, units, and reference-range
1090
+ * fields **raw**. This module turns those raw letters into modeled, **fail-safe**
1091
+ * semantics, under one rule: **never a confident wrong value.**
1092
+ *
1093
+ * - An **abnormal flag** (field 7) is recognized against HL7 Table 0078; an
1094
+ * unrecognized flag is surfaced as `undefined` (with a warning) and is **never
1095
+ * coerced to `normal`**.
1096
+ * - A **result status** (field 9) is modeled so a **correction (`C`)** or a
1097
+ * **cancellation (`X`)** can never read as an active-final result, and an
1098
+ * **absent** status is a typed `unspecified` — **never assumed `F` (final)**.
1099
+ * - A **reference range** (field 6) is parsed into a low/high (or open-ended)
1100
+ * pair; an unparseable range is surfaced **verbatim** — a bound is **never
1101
+ * fabricated**.
1102
+ *
1103
+ * Every bound and code is surfaced verbatim; nothing is collapsed, reconciled,
1104
+ * or converted. Units are handled at the record layer (a numeric value without
1105
+ * units warns; units are never defaulted, guessed, or converted — see
1106
+ * `./parse.ts`).
1107
+ */
1108
+ /**
1109
+ * The recognized abnormal-flag letters, from **HL7 v2.2 Table 0078** (the flag
1110
+ * *value* set — a published fact set, not CLSI prose). Read in the context of an
1111
+ * `R` record's field 7: here `S`/`R`/`I` are the microbiology susceptibility
1112
+ * codes and `R` is **not** the repeat delimiter.
1113
+ */
1114
+ type AbnormalFlagCode = "L" | "H" | "LL" | "HH" | "<" | ">" | "N" | "A" | "AA" | "U" | "D" | "B" | "W" | "S" | "R" | "I";
1115
+ /**
1116
+ * The modeled meaning of a recognized {@link AbnormalFlagCode}, plus the two
1117
+ * fail-safe sentinels: `undefined` (a flag was present but is not in Table 0078)
1118
+ * — which is **never** collapsed to `normal`.
1119
+ */
1120
+ type AbnormalFlagMeaning = "below-normal" | "above-normal" | "critically-below-normal" | "critically-above-normal" | "below-scale" | "above-scale" | "normal" | "abnormal" | "very-abnormal" | "significant-change-up" | "significant-change-down" | "better" | "worse" | "susceptible" | "resistant" | "intermediate" | "undefined";
1121
+ /**
1122
+ * A recognized (or explicitly *un*recognized) abnormal flag. The raw field text
1123
+ * is always preserved; `recognized` is `false` and `meaning` is `"undefined"`
1124
+ * for any letter outside Table 0078 — the flag is surfaced, never dropped, and
1125
+ * **never coerced to `normal`**.
1126
+ *
1127
+ * @example
1128
+ * ```ts
1129
+ * import { interpretAbnormalFlag } from "@cosyte/astm";
1130
+ * const f = interpretAbnormalFlag("HH");
1131
+ * f.meaning; // "critically-above-normal"
1132
+ * f.recognized; // true
1133
+ * ```
1134
+ */
1135
+ interface AbnormalFlag {
1136
+ /** The verbatim field text, exactly as received. */
1137
+ readonly raw: string;
1138
+ /** The Table 0078 code, present only when the raw text is a recognized flag. */
1139
+ readonly code?: AbnormalFlagCode;
1140
+ /** The modeled meaning; `"undefined"` (never `"normal"`) for an unrecognized flag. */
1141
+ readonly meaning: AbnormalFlagMeaning;
1142
+ /** Whether the raw text matched a Table 0078 flag. */
1143
+ readonly recognized: boolean;
1144
+ }
1145
+ /**
1146
+ * Interpret an `R`-record abnormal-flag field (field 7) against HL7 Table 0078.
1147
+ *
1148
+ * Leading/trailing whitespace is ignored for the lookup but the `raw` text is
1149
+ * preserved verbatim. An unrecognized flag yields `{ recognized: false, meaning:
1150
+ * "undefined" }` — surfaced, never dropped, and **never `"normal"`**.
1151
+ *
1152
+ * @param raw - The verbatim field-7 text.
1153
+ * @returns The interpreted flag.
1154
+ * @example
1155
+ * ```ts
1156
+ * import { interpretAbnormalFlag } from "@cosyte/astm";
1157
+ * interpretAbnormalFlag("U").meaning; // "significant-change-up"
1158
+ * interpretAbnormalFlag("ZZ").meaning; // "undefined" (never "normal")
1159
+ * ```
1160
+ */
1161
+ declare function interpretAbnormalFlag(raw: string): AbnormalFlag;
1162
+ /**
1163
+ * The recognized result-status letters (`R`-record field 9). The clinically
1164
+ * load-bearing members are **`C` (correction — supersedes a previously
1165
+ * transmitted value)** and **`X` (cannot be done / cancelled)**.
1166
+ */
1167
+ type ResultStatusCode = "F" | "C" | "P" | "R" | "S" | "I" | "X";
1168
+ /**
1169
+ * The modeled meaning of a result status, plus two fail-safe sentinels:
1170
+ * `unspecified` (the field was absent — **never assumed `final`**) and
1171
+ * `undefined` (a letter was present but is not a recognized status).
1172
+ */
1173
+ type ResultStatusMeaning = "final" | "correction" | "preliminary" | "previously-transmitted" | "partial" | "pending" | "cancelled" | "unspecified" | "undefined";
1174
+ /**
1175
+ * A modeled result status. The three booleans are the safety surface a consumer
1176
+ * reads instead of string-matching the code:
1177
+ *
1178
+ * - **`isActiveFinal`** is `true` **only** for a plain `F` (final). It is `false`
1179
+ * for a correction (`C`), a cancellation (`X`), a preliminary/partial/pending
1180
+ * result, an absent status, and an unrecognized one — so a superseded or
1181
+ * cancelled result can **never** read as current/final.
1182
+ * - **`supersedes`** is `true` for `C` — this value replaces a previously
1183
+ * transmitted one.
1184
+ * - **`cancelled`** is `true` for `X` — the result cannot be done / was cancelled.
1185
+ *
1186
+ * @example
1187
+ * ```ts
1188
+ * import { interpretResultStatus } from "@cosyte/astm";
1189
+ * interpretResultStatus("C").isActiveFinal; // false (a correction is not active-final)
1190
+ * interpretResultStatus("X").cancelled; // true
1191
+ * interpretResultStatus(undefined).meaning; // "unspecified" (never "final")
1192
+ * ```
1193
+ */
1194
+ interface ResultStatus {
1195
+ /** The verbatim field text, present only when field 9 carried a value. */
1196
+ readonly raw?: string;
1197
+ /** The recognized status code, present only when the raw text is a known status. */
1198
+ readonly code?: ResultStatusCode;
1199
+ /** The modeled meaning; `"unspecified"` when absent, `"undefined"` when unrecognized. */
1200
+ readonly meaning: ResultStatusMeaning;
1201
+ /** Whether the raw text matched a recognized status letter. */
1202
+ readonly recognized: boolean;
1203
+ /** `true` **only** for a plain `F` (final) — never for `C`, `X`, absent, or unrecognized. */
1204
+ readonly isActiveFinal: boolean;
1205
+ /** `true` for `C` — this value supersedes a previously transmitted result. */
1206
+ readonly supersedes: boolean;
1207
+ /** `true` for `X` — the result cannot be done / was cancelled. */
1208
+ readonly cancelled: boolean;
1209
+ }
1210
+ /**
1211
+ * Interpret an `R`-record result-status field (field 9).
1212
+ *
1213
+ * An **absent** status (`undefined` or empty) is typed `unspecified` — **never
1214
+ * assumed `final`**. A **present but unrecognized** letter is `undefined`
1215
+ * (recognized `false`). In every non-`F` case `isActiveFinal` is `false`, so a
1216
+ * correction, a cancellation, or an unknown status can never read as current.
1217
+ *
1218
+ * @param raw - The verbatim field-9 text, or `undefined`/empty when absent.
1219
+ * @returns The modeled status.
1220
+ * @example
1221
+ * ```ts
1222
+ * import { interpretResultStatus } from "@cosyte/astm";
1223
+ * const s = interpretResultStatus("F");
1224
+ * s.isActiveFinal; // true
1225
+ * s.supersedes; // false
1226
+ * ```
1227
+ */
1228
+ declare function interpretResultStatus(raw: string | undefined): ResultStatus;
1229
+ /**
1230
+ * The shape of a parsed reference range.
1231
+ *
1232
+ * - `closed` — both a low and a high bound (`3.5-5.0`).
1233
+ * - `open-low` — an upper bound only (`<5`): everything at or below `high`.
1234
+ * - `open-high` — a lower bound only (`>10`): everything at or above `low`.
1235
+ * - `unparsed` — the text did not match a recognized form; both bounds are
1236
+ * absent and the raw text is surfaced. **A bound is never fabricated.**
1237
+ */
1238
+ type ReferenceRangeKind = "closed" | "open-low" | "open-high" | "unparsed";
1239
+ /**
1240
+ * A parsed reference range. Bounds are surfaced as the **verbatim numeric text**
1241
+ * (never coerced to a float, never rounded, never converted) so nothing is lost
1242
+ * or fabricated.
1243
+ *
1244
+ * **`[OSS-derived]` — the exact lower/upper delimiter (`-`) and the open-ended
1245
+ * `<x`/`>x` forms are taken from the permissively-licensed OSS reference parsers
1246
+ * and cross-verified vendor transcripts; they are not confirmed against the
1247
+ * purchased CLSI LIS02-A2 (roadmap §10 Q1). Anything that does not match these
1248
+ * forms is surfaced verbatim as `unparsed`, never guessed into a bound.**
1249
+ *
1250
+ * @example
1251
+ * ```ts
1252
+ * import { parseReferenceRange } from "@cosyte/astm";
1253
+ * const r = parseReferenceRange("3.5-5.0");
1254
+ * r.kind; // "closed"
1255
+ * r.low; // "3.5"
1256
+ * r.high; // "5.0"
1257
+ * ```
1258
+ */
1259
+ interface ReferenceRange {
1260
+ /** The verbatim field text, exactly as received. */
1261
+ readonly raw: string;
1262
+ /** The lower bound, verbatim numeric text (for `closed` and `open-high`). */
1263
+ readonly low?: string;
1264
+ /** The upper bound, verbatim numeric text (for `closed` and `open-low`). */
1265
+ readonly high?: string;
1266
+ /** The recognized shape, or `"unparsed"` when the text matched no known form. */
1267
+ readonly kind: ReferenceRangeKind;
1268
+ }
1269
+ /**
1270
+ * Parse an `R`-record reference-range field (field 6) into a low/high (or
1271
+ * open-ended) pair.
1272
+ *
1273
+ * Recognized forms are `low-high` (closed), `<high` (open-low), and `>low`
1274
+ * (open-high). Anything else — including an ambiguous multi-dash string or a
1275
+ * bare non-numeric token — is returned as `kind: "unparsed"` with the raw text
1276
+ * preserved and **no bound invented**. Bounds are surfaced as verbatim text, not
1277
+ * coerced to numbers.
1278
+ *
1279
+ * @param raw - The verbatim field-6 text.
1280
+ * @returns The parsed reference range.
1281
+ * @example
1282
+ * ```ts
1283
+ * import { parseReferenceRange } from "@cosyte/astm";
1284
+ * parseReferenceRange("<5").kind; // "open-low"
1285
+ * parseReferenceRange(">10").low; // "10"
1286
+ * parseReferenceRange("weird").kind; // "unparsed" (never a fabricated bound)
1287
+ * ```
1288
+ */
1289
+ declare function parseReferenceRange(raw: string): ReferenceRange;
1290
+
1291
+ /**
1292
+ * The immutable ASTM record model produced by {@link parseAstmRecords}.
1293
+ *
1294
+ * A message is a header (which carries the resolved delimiters as provenance), an
1295
+ * ordered list of typed records, and the Tier-2 warnings the lenient parser
1296
+ * accumulated. Every record keeps its raw {@link AstmField}s **and** the typed
1297
+ * accessors relevant to its type — the raw tree is always present so nothing the
1298
+ * parser did not model is lost.
1299
+ *
1300
+ * Phase-1 scope: `H`/`P`/`O`/`R`/`L` are modeled; every other type letter is an
1301
+ * {@link UnsupportedRecord} (surfaced, never dropped). Result-flag/status
1302
+ * *semantics*, comments/query/`M`/`S`, framing, and serialization are later
1303
+ * phases — this layer surfaces the safety-critical values **raw**.
1304
+ */
1305
+
1306
+ /**
1307
+ * One ASTM field, as split from a record. The tree holds **decoded** component
1308
+ * strings (escape sequences already resolved), while {@link AstmField.raw}
1309
+ * preserves the exact wire text of the field for round-trip and audit.
1310
+ *
1311
+ * `components` is the first repeat's components (the common single-repeat case);
1312
+ * `repeats` holds every repeat when a field uses the repeat delimiter.
1313
+ */
1314
+ interface AstmField {
1315
+ /** The exact field text as it appeared on the wire (escapes NOT decoded). */
1316
+ readonly raw: string;
1317
+ /** Components of the first repeat, each escape-decoded. Empty field → `[""]`. */
1318
+ readonly components: readonly string[];
1319
+ /** Every repeat, each an array of decoded components. `repeats[0] === components`. */
1320
+ readonly repeats: readonly (readonly string[])[];
1321
+ }
1322
+ /** Common shape of every parsed record. */
1323
+ interface RecordBase {
1324
+ /** The record's raw type letter. */
1325
+ readonly type: string;
1326
+ /** 0-based ordinal of the record within the message. */
1327
+ readonly recordIndex: number;
1328
+ /** The record's fields. `fields[0]` is the type-letter field; data fields are 1-indexed after it. */
1329
+ readonly fields: readonly AstmField[];
1330
+ }
1331
+ /**
1332
+ * The `H` (header) record. Carries the delimiters it declared as provenance.
1333
+ */
1334
+ interface HeaderRecord extends RecordBase {
1335
+ readonly type: "H";
1336
+ /** The four delimiters resolved from this header. */
1337
+ readonly delimiters: Delimiters;
1338
+ /**
1339
+ * The header's exact wire text (terminator excluded), preserved because the
1340
+ * escape character appears **literally** inside the delimiter-definition field
1341
+ * (`\^&`), which the generic escape-aware tokenizer cannot split cleanly. The
1342
+ * delimiters are read from this raw text (never from {@link RecordBase.fields}),
1343
+ * and the serializer reconstructs the header's data fields from it.
1344
+ */
1345
+ readonly rawLine: string;
1346
+ }
1347
+ /**
1348
+ * A patient name (`Last^First^Middle`), each component surfaced verbatim.
1349
+ */
1350
+ interface PatientName {
1351
+ readonly raw: string;
1352
+ readonly last?: string;
1353
+ readonly first?: string;
1354
+ readonly middle?: string;
1355
+ }
1356
+ /**
1357
+ * The `P` (patient) record.
1358
+ *
1359
+ * **Safety:** the practice-assigned ID (field 3) and the laboratory-assigned ID
1360
+ * (field 4) are modeled as **distinct** fields and never collapsed — conflating
1361
+ * them is the primary result-misfiling path.
1362
+ */
1363
+ interface PatientRecord extends RecordBase {
1364
+ readonly type: "P";
1365
+ /** Field 2 — sequence number. */
1366
+ readonly seq?: string;
1367
+ /** Field 3 — practice-assigned patient ID. Distinct from {@link PatientRecord.laboratoryAssignedId}. */
1368
+ readonly practiceAssignedId?: string;
1369
+ /** Field 4 — laboratory-assigned patient ID. Distinct from {@link PatientRecord.practiceAssignedId}. */
1370
+ readonly laboratoryAssignedId?: string;
1371
+ /**
1372
+ * Field 5 — a third patient identifier (e.g. a national/alternate ID), surfaced verbatim. Kept
1373
+ * separate from the practice- and laboratory-assigned IDs — the three never collapse into one.
1374
+ */
1375
+ readonly patientIdThree?: string;
1376
+ /** Field 6 — patient name (`Last^First^Middle`). */
1377
+ readonly name?: PatientName;
1378
+ /** Field 7 — mother's maiden name, surfaced verbatim (a surname component; PHI). */
1379
+ readonly mothersMaidenName?: string;
1380
+ /** Field 8 — birthdate (`YYYYMMDDHHMMSS`, precision-preserving; a truncated run sets `truncated`). */
1381
+ readonly birthDate?: AstmDate;
1382
+ /** Field 9 — sex, surfaced raw (`M`/`F`/`U`/vendor value). */
1383
+ readonly sex?: string;
1384
+ }
1385
+ /**
1386
+ * The `O` (order) record — binds a result to a specimen.
1387
+ */
1388
+ interface OrderRecord extends RecordBase {
1389
+ readonly type: "O";
1390
+ /** Field 2 — sequence number. */
1391
+ readonly seq?: string;
1392
+ /** Field 3 — specimen / accession ID. */
1393
+ readonly specimenId?: string;
1394
+ /** Field 4 — instrument specimen ID. */
1395
+ readonly instrumentSpecimenId?: string;
1396
+ /** Field 5 — Universal Test ID (same caret structure as a result's). */
1397
+ readonly universalTestId?: UniversalTestId;
1398
+ /**
1399
+ * Field 6 — priority, surfaced raw (e.g. `S` STAT, `R` routine, `A` ASAP). Vendor letters vary;
1400
+ * the code set is `[OSS-derived]` (the exact enumeration is in the paywalled CLSI LIS02-A2), so the
1401
+ * value is surfaced verbatim and **never mapped to a guessed meaning**.
1402
+ */
1403
+ readonly priority?: string;
1404
+ /**
1405
+ * Field 12 — action code, surfaced raw (e.g. `C` cancel, `A` add, `N` new). The exact field index
1406
+ * (`~12`) and the code set are `[OSS-derived]` (paywalled) — surfaced verbatim, never interpreted.
1407
+ */
1408
+ readonly actionCode?: string;
1409
+ /**
1410
+ * Field 26 — report type, surfaced raw (e.g. `F` final, `P` preliminary, `X` cancel). The exact
1411
+ * field index (`~26`) and the code set are `[OSS-derived]` (paywalled) — surfaced verbatim.
1412
+ */
1413
+ readonly reportType?: string;
1414
+ }
1415
+ /**
1416
+ * The `R` (result) record — the value itself.
1417
+ *
1418
+ * The raw safety-critical fields (value, `units`, `referenceRange`,
1419
+ * `abnormalFlags`, `resultStatus`) are always surfaced exactly as received.
1420
+ * Phase 2 adds the **modeled, fail-safe** semantics alongside them: `flag`
1421
+ * (Table 0078, `undefined` never coerced to normal), `status` (a `C`/`X` never
1422
+ * reads as active-final; an absent status is `unspecified`, never `final`), and
1423
+ * `range` (open/closed bounds surfaced verbatim, never fabricated). The raw
1424
+ * strings and the modeled views coexist — nothing is collapsed or reconciled.
1425
+ */
1426
+ interface ResultRecord extends RecordBase {
1427
+ readonly type: "R";
1428
+ /** Field 2 — sequence number. */
1429
+ readonly seq?: string;
1430
+ /** Field 3 — Universal Test ID (local code in component 4 is the primary identifier). */
1431
+ readonly universalTestId?: UniversalTestId;
1432
+ /**
1433
+ * Field 4 — the measured value. In the ordinary single-component case this is the decoded scalar.
1434
+ * When the value field carried an *unescaped* component delimiter (an ambiguity), this is the
1435
+ * **full raw field text** — never a truncated component — and {@link ResultRecord.valueComponents}
1436
+ * plus an `ASTM_RECORD_AMBIGUOUS_VALUE_SPLIT` warning are also present.
1437
+ */
1438
+ readonly value?: string;
1439
+ /**
1440
+ * Field 4 split into components, present only when the value field carried an *unescaped* component
1441
+ * delimiter — i.e. it read as more than one component. Surfaced alongside the full raw value (and a
1442
+ * warning) so an ambiguous split is visible, never resolved silently.
1443
+ */
1444
+ readonly valueComponents?: readonly string[];
1445
+ /** Field 5 — units (vendor free text; a missing unit is not defaulted). */
1446
+ readonly units?: string;
1447
+ /** Field 6 — reference range, surfaced raw. */
1448
+ readonly referenceRange?: string;
1449
+ /**
1450
+ * Field 6 — the reference range parsed into low/high (or open-ended) bounds, present only when the
1451
+ * field carried a value. An unparseable range is `kind: "unparsed"` with the raw text preserved and
1452
+ * **no bound fabricated**. Bounds are verbatim numeric text, never coerced to floats.
1453
+ */
1454
+ readonly range?: ReferenceRange;
1455
+ /** Field 7 — abnormal flags, surfaced raw (HL7 Table 0078 values). */
1456
+ readonly abnormalFlags?: string;
1457
+ /**
1458
+ * Field 7 — the abnormal flag interpreted against HL7 Table 0078, present only when the field
1459
+ * carried a value. An unrecognized flag is `{ recognized: false, meaning: "undefined" }` — surfaced,
1460
+ * never dropped, and **never coerced to `normal`**.
1461
+ */
1462
+ readonly flag?: AbnormalFlag;
1463
+ /** Field 9 — result status, surfaced raw (`F`/`C`/`X`/…). */
1464
+ readonly resultStatus?: string;
1465
+ /**
1466
+ * Field 9 — the modeled result status. **Always present** (an absent field yields a typed
1467
+ * `unspecified`, never assumed `final`), so `status.isActiveFinal` is always a reliable boolean: it
1468
+ * is `true` only for a plain `F`, and `false` for a correction (`C`), a cancellation (`X`), a
1469
+ * partial/preliminary/pending, an absent, or an unrecognized status.
1470
+ */
1471
+ readonly status: ResultStatus;
1472
+ /** Field 11 — operator. */
1473
+ readonly operator?: string;
1474
+ /** Field 12 — test started timestamp. */
1475
+ readonly startedAt?: AstmDate;
1476
+ /** Field 13 — test completed timestamp. */
1477
+ readonly completedAt?: AstmDate;
1478
+ /** Field 14 — instrument identifier. */
1479
+ readonly instrument?: string;
1480
+ }
1481
+ /**
1482
+ * The `C` (comment) record — free-text context attached to a parent record.
1483
+ *
1484
+ * A comment is **attached by position** to the immediately-preceding `H`/`P`/`O`/`R` record
1485
+ * ({@link CommentRecord.parentIndex}); consecutive comments share that parent. **Fail-safe:** a
1486
+ * comment with no valid preceding parent is an **orphan** — it is attached to the message root
1487
+ * ({@link CommentRecord.attachedToRoot} `true`) and a value-free `ASTM_RECORD_ORPHAN_COMMENT` warning
1488
+ * fires — **never silently dropped**, so a comment carrying (e.g.) "QC / non-compliant" context can
1489
+ * never float away unnoticed.
1490
+ */
1491
+ interface CommentRecord extends RecordBase {
1492
+ readonly type: "C";
1493
+ /** Field 2 — sequence number. */
1494
+ readonly seq?: string;
1495
+ /** Field 3 — comment source (who/what produced it), surfaced verbatim. */
1496
+ readonly source?: string;
1497
+ /**
1498
+ * Field 4 — the comment text, surfaced as the **full** field text (all components), never
1499
+ * truncated to the first component. The component structure is in {@link CommentRecord.textComponents}.
1500
+ */
1501
+ readonly text?: string;
1502
+ /**
1503
+ * Field 4 — the comment text split into its decoded components (comment text is component-capable;
1504
+ * multiple components are a normal structured comment, **not** an ambiguity). Present only when the
1505
+ * field carried more than one component.
1506
+ */
1507
+ readonly textComponents?: readonly string[];
1508
+ /**
1509
+ * Field 5 — comment type code, surfaced verbatim. **`[OSS-derived]`:** `I` (instrument) is the only
1510
+ * value seen in the permissively-licensed real transcripts; other values (e.g. `G`/`T`/`P`) are
1511
+ * defined only in the paywalled CLSI LIS02-A2 and are **not** interpreted here — the raw code is
1512
+ * surfaced, never mapped to a guessed meaning.
1513
+ */
1514
+ readonly commentType?: string;
1515
+ /**
1516
+ * The `recordIndex` of the `H`/`P`/`O`/`R` record this comment is attached to, or `undefined` when
1517
+ * the comment is an orphan attached to the message root (see {@link CommentRecord.attachedToRoot}).
1518
+ */
1519
+ readonly parentIndex?: number;
1520
+ /** `true` when no valid parent preceded — the comment is attached to the message root (and warned). */
1521
+ readonly attachedToRoot: boolean;
1522
+ }
1523
+ /**
1524
+ * The `Q` (Request Information) record — the host-query request.
1525
+ *
1526
+ * A `Q` record asks the LIS for information (e.g. outstanding orders for a
1527
+ * specimen); its **presence classifies the whole message as a request, never a
1528
+ * result set** (see {@link AstmMessage.classification}). Its safety-relevant
1529
+ * fields — the starting/ending range ID and the request-information status — are
1530
+ * surfaced **verbatim**; the field *positions* are the public ASTM E1394 layout,
1531
+ * but their internal structure and code meanings are **`[OSS-derived / paywalled]`**
1532
+ * (roadmap §10 Q3) and are therefore **never interpreted or guessed**.
1533
+ */
1534
+ interface QueryRecord extends RecordBase {
1535
+ /** Field 2 — sequence number. */
1536
+ readonly seq?: string;
1537
+ readonly type: "Q";
1538
+ /**
1539
+ * Field 3 — starting range ID number, surfaced as the **full** verbatim field text (never truncated
1540
+ * to a component). Its caret component structure (e.g. patient ID ^ specimen ID) is
1541
+ * **`[OSS-derived / paywalled]`** — the raw split is available via {@link RecordBase.fields}, but the
1542
+ * *meaning* of each component is **never** assigned here.
1543
+ */
1544
+ readonly startingRangeId?: string;
1545
+ /** Field 4 — ending range ID number, surfaced verbatim (same `[OSS-derived]` caveat as field 3). */
1546
+ readonly endingRangeId?: string;
1547
+ /**
1548
+ * Field 5 — Universal Test ID (the same caret structure as an `O`/`R` record's), recognized by
1549
+ * provenance only. When the field is the literal universal-query keyword, {@link QueryRecord.queriesAllTests}
1550
+ * is set instead — see its caveat.
1551
+ */
1552
+ readonly universalTestId?: UniversalTestId;
1553
+ /**
1554
+ * `true` when field 5 is the literal `ALL` universal-query keyword (case-insensitive). **`[OSS-derived
1555
+ * / paywalled]`:** the token is surfaced because it appears in the OSS references, but its exact
1556
+ * host-query *behavior* (which tests a bare `ALL` selects, and whether the vendor answers with a full
1557
+ * `H/P/O/L` or a `P/O`-only response) is paywalled and vendor-specific — **not decided here**.
1558
+ */
1559
+ readonly queriesAllTests: boolean;
1560
+ /**
1561
+ * Field 13 — request-information status code(s), surfaced **verbatim**. The status code *set* is
1562
+ * **`[OSS-derived / paywalled]`** (roadmap §10 Q3): with no publicly-groundable enumeration, this
1563
+ * parser recognizes **none** of them and interprets nothing — every present status is surfaced raw
1564
+ * and flagged with a value-free `ASTM_RECORD_UNINTERPRETED_QUERY_STATUS` warning. Never mapped to a
1565
+ * guessed meaning.
1566
+ */
1567
+ readonly requestInformationStatus?: string;
1568
+ }
1569
+ /**
1570
+ * The `M` (manufacturer) record — vendor-defined free-form data
1571
+ * (QC / calibration / maintenance), surfaced **VERBATIM** and **never interpreted
1572
+ * into typed clinical fields**.
1573
+ *
1574
+ * Interpreting a vendor `M` record as clinical data would be a fabrication, so
1575
+ * this record carries **no typed accessors at all**: the exact wire text is in
1576
+ * {@link ManufacturerRecord.rawLine} (byte-preserving) and the tokenized tree in
1577
+ * {@link RecordBase.fields}. Nothing is parsed into a value, a code, or a unit.
1578
+ */
1579
+ interface ManufacturerRecord extends RecordBase {
1580
+ readonly type: "M";
1581
+ /** The record's exact wire text (terminator excluded), preserved byte-for-byte for round-trip. */
1582
+ readonly rawLine: string;
1583
+ }
1584
+ /**
1585
+ * The `S` (scientific) record — vendor-defined free-form data, surfaced
1586
+ * **VERBATIM** and **never interpreted into typed clinical fields** (same posture
1587
+ * as {@link ManufacturerRecord}).
1588
+ */
1589
+ interface ScientificRecord extends RecordBase {
1590
+ readonly type: "S";
1591
+ /** The record's exact wire text (terminator excluded), preserved byte-for-byte for round-trip. */
1592
+ readonly rawLine: string;
1593
+ }
1594
+ /**
1595
+ * The `L` (terminator) record — closes a message.
1596
+ */
1597
+ interface TerminatorRecord extends RecordBase {
1598
+ readonly type: "L";
1599
+ }
1600
+ /**
1601
+ * Any record whose type letter is not modeled (a genuinely unknown letter).
1602
+ * `H`/`P`/`O`/`R`/`C`/`L` (Phases 1–3) and `Q`/`M`/`S` (Phase 4) are all modeled;
1603
+ * anything else is surfaced with its raw fields intact and flagged with an
1604
+ * `ASTM_RECORD_UNKNOWN_TYPE` warning — never dropped.
1605
+ */
1606
+ interface UnsupportedRecord extends RecordBase {
1607
+ readonly type: "unsupported";
1608
+ /** The raw type letter as it appeared on the wire. */
1609
+ readonly rawType: string;
1610
+ }
1611
+ /** The discriminated union of every parsed record. */
1612
+ type AstmRecord = HeaderRecord | PatientRecord | OrderRecord | ResultRecord | CommentRecord | QueryRecord | ManufacturerRecord | ScientificRecord | TerminatorRecord | UnsupportedRecord;
1613
+ /**
1614
+ * How a message is classified by the host-query flow.
1615
+ *
1616
+ * - `host-query` — the message carries at least one `Q` record: it is a **request
1617
+ * for information**, and **must never be read as a result set** (the load-bearing
1618
+ * safety distinction of this layer). `Q` **dominates**: a `Q` present classifies
1619
+ * the message as a request even if a result record is also present (an anomaly,
1620
+ * separately warned) — so a `Q`-bearing message is never silently treated as a
1621
+ * result upload.
1622
+ * - `results` — no `Q`, at least one `R` (result) record: a result upload / response.
1623
+ * - `orders` — no `Q`, no `R`, at least one `O` (order) record: an order download,
1624
+ * or a query response before results are attached.
1625
+ * - `indeterminate` — none of the above (e.g. header + terminator only). Not
1626
+ * guessed into one of the other kinds.
1627
+ */
1628
+ type AstmMessageKind = "host-query" | "results" | "orders" | "indeterminate";
1629
+ /**
1630
+ * The message-level classification from the host-query flow. `isHostQueryRequest`
1631
+ * is the single boolean a consumer should gate on before treating records as
1632
+ * results — it is `true` **iff** a `Q` record is present.
1633
+ *
1634
+ * @example
1635
+ * ```ts
1636
+ * import { parseAstmRecords } from "@cosyte/astm";
1637
+ * const req = parseAstmRecords("H|\\^&\rP|1\rQ|1|^SPEC-7||ALL\rL|1\r");
1638
+ * req.classification.kind; // "host-query"
1639
+ * req.classification.isHostQueryRequest; // true — never read its records as results
1640
+ * ```
1641
+ */
1642
+ interface AstmMessageClassification {
1643
+ /** The message kind. */
1644
+ readonly kind: AstmMessageKind;
1645
+ /** At least one `Q` (request-information) record is present. */
1646
+ readonly hasQuery: boolean;
1647
+ /** At least one `R` (result) record is present. */
1648
+ readonly hasResults: boolean;
1649
+ /** At least one `O` (order) record is present. */
1650
+ readonly hasOrders: boolean;
1651
+ /**
1652
+ * `true` **iff** `kind === "host-query"` (a `Q` record is present) — the safety surface: gate on this
1653
+ * before treating records as results, so a query is never misread as a result upload.
1654
+ */
1655
+ readonly isHostQueryRequest: boolean;
1656
+ }
1657
+ /**
1658
+ * A parsed ASTM message: the header, the ordered records (the header is also
1659
+ * `records[0]`), the resolved delimiters, and the accumulated warnings.
1660
+ *
1661
+ * @example
1662
+ * ```ts
1663
+ * import { parseAstmRecords } from "@cosyte/astm";
1664
+ * const msg = parseAstmRecords("H|\\^&\rL|1\r");
1665
+ * msg.header.delimiters.field; // "|"
1666
+ * msg.records.length; // 2
1667
+ * ```
1668
+ */
1669
+ interface AstmMessage {
1670
+ readonly header: HeaderRecord;
1671
+ readonly records: readonly AstmRecord[];
1672
+ readonly delimiters: Delimiters;
1673
+ /**
1674
+ * The host-query classification of this message — whether it is a request (`Q`
1675
+ * present), a result upload, an order download, or indeterminate. Gate on
1676
+ * {@link AstmMessageClassification.isHostQueryRequest} before reading records as
1677
+ * results.
1678
+ */
1679
+ readonly classification: AstmMessageClassification;
1680
+ readonly warnings: readonly AstmRecordWarning[];
1681
+ /**
1682
+ * The vendor profile that was active for this parse, when one applied — its
1683
+ * `name` and resolved `lineage` (attribution only; the profile object itself is
1684
+ * not embedded). Absent when no profile applied. A profile that tolerated a
1685
+ * deviation shows up as a `PROFILE_QUIRK_APPLIED` entry in {@link warnings}, not
1686
+ * here.
1687
+ */
1688
+ readonly profile?: {
1689
+ readonly name: string;
1690
+ readonly lineage: readonly string[];
1691
+ };
1692
+ }
1693
+ /**
1694
+ * Options for {@link parseAstmRecords}. Lenient by default (Postel's Law).
1695
+ */
1696
+ interface AstmParseOptions {
1697
+ /**
1698
+ * When `true`, escalate any Tier-2 deviation to a thrown {@link AstmStrictError}
1699
+ * instead of accumulating a warning. Off by default. A deviation a profile
1700
+ * *expected* (a `PROFILE_QUIRK_APPLIED`) does **not** escalate — it is known,
1701
+ * benign, and still recorded.
1702
+ */
1703
+ readonly strict?: boolean;
1704
+ /**
1705
+ * An active vendor {@link AstmProfile}. Its quirk tolerances downgrade the
1706
+ * deviations it *expects* to `PROFILE_QUIRK_APPLIED` warnings (values are never
1707
+ * altered). An explicit profile wins over the process-scoped default
1708
+ * ({@link setDefaultAstmProfile}); pass `null` to opt out of the default for a
1709
+ * single call. Omit to use the default, or no profile when none is registered.
1710
+ */
1711
+ readonly profile?: AstmProfile | null;
1712
+ }
1713
+
1714
+ /**
1715
+ * The record-layer entry point: {@link parseAstmRecords}.
1716
+ *
1717
+ * Phase 1 assumes **already-de-framed** record bytes (the E1381/LIS01 framing
1718
+ * layer is a later phase). It reads the four delimiters from the header, tokenizes
1719
+ * every record escape-aware, and builds the immutable {@link AstmMessage} — lenient
1720
+ * by default: vendor quirks become typed warnings, and only three unrecoverable
1721
+ * structural conditions are fatal.
1722
+ */
1723
+
1724
+ /**
1725
+ * Thrown by {@link parseAstmRecords} in `strict` mode when the lenient parser
1726
+ * would otherwise have accumulated one or more Tier-2 warnings. Carries the
1727
+ * warnings (code + position, never a value) so a caller can see every deviation.
1728
+ *
1729
+ * @example
1730
+ * ```ts
1731
+ * import { parseAstmRecords, AstmStrictError } from "@cosyte/astm";
1732
+ * try {
1733
+ * parseAstmRecords("H|\\^&\rZ|1\r", { strict: true });
1734
+ * } catch (err) {
1735
+ * if (err instanceof AstmStrictError) err.warnings.length; // >= 1
1736
+ * }
1737
+ * ```
1738
+ */
1739
+ declare class AstmStrictError extends Error {
1740
+ readonly warnings: readonly AstmRecordWarning[];
1741
+ /** @internal */
1742
+ constructor(warnings: readonly AstmRecordWarning[]);
1743
+ }
1744
+ /**
1745
+ * Parse an ASTM/CLSI-LIS02 record stream into an immutable {@link AstmMessage}.
1746
+ *
1747
+ * The stream is a sequence of records separated by `CR` (with `LF`/`CRLF`
1748
+ * tolerated); the first record must be an `H` header, which declares the
1749
+ * delimiters used by the rest. Lenient by default — set `strict` to reject any
1750
+ * tolerated deviation.
1751
+ *
1752
+ * @param raw - The de-framed record bytes, as a string or `Uint8Array` (decoded
1753
+ * latin1 so byte values survive 1:1).
1754
+ * @param options - Parse options; lenient unless `strict` is set.
1755
+ * @returns The parsed, deeply-frozen message.
1756
+ * @throws {@link AstmParseError} on a Tier-3 fatal (`EMPTY_INPUT`,
1757
+ * `ASTM_RECORD_NO_HEADER`, `ASTM_RECORD_UNDECLARED_DELIMITERS`).
1758
+ * @throws {@link AstmStrictError} when `strict` is set and a deviation occurs.
1759
+ * @example
1760
+ * ```ts
1761
+ * import { parseAstmRecords, results } from "@cosyte/astm";
1762
+ * const msg = parseAstmRecords("H|\\^&\rP|1\rO|1|ACC\rR|1|^^^687|28.6|U/L||N||F\rL|1|N\r");
1763
+ * results(msg)[0]?.value; // "28.6"
1764
+ * ```
1765
+ */
1766
+ declare function parseAstmRecords(raw: string | Uint8Array, options?: AstmParseOptions): AstmMessage;
1767
+ /**
1768
+ * Attach every `C` (comment) record to its parent by position — the immediately-preceding
1769
+ * `H`/`P`/`O`/`R` record; consecutive comments share that parent. **Fail-safe:** a comment with no
1770
+ * valid preceding parent is an **orphan** — it is attached to the message root (`attachedToRoot:
1771
+ * true`) with an `ASTM_RECORD_ORPHAN_COMMENT` warning, **never dropped**. Returns a new array; the
1772
+ * non-comment records pass through unchanged.
1773
+ *
1774
+ * @param records - The built records, in wire order.
1775
+ * @param warnings - The accumulator an orphan comment warns onto.
1776
+ * @returns The records with each comment's `parentIndex` / `attachedToRoot` resolved.
1777
+ * @example
1778
+ * ```ts
1779
+ * import { attachComments } from "@cosyte/astm";
1780
+ * // A comment with no preceding H/P/O/R is an orphan attached to the root.
1781
+ * const warnings: import("@cosyte/astm").AstmRecordWarning[] = [];
1782
+ * const out = attachComments(
1783
+ * [{ type: "C", recordIndex: 0, fields: [], attachedToRoot: false }],
1784
+ * warnings,
1785
+ * );
1786
+ * (out[0] as { attachedToRoot: boolean }).attachedToRoot; // true
1787
+ * ```
1788
+ */
1789
+ declare function attachComments(records: readonly AstmRecord[], warnings: AstmRecordWarning[]): AstmRecord[];
1790
+
1791
+ /**
1792
+ * The record-layer **emit** side: {@link serializeAstmRecords} — Phase 7.
1793
+ *
1794
+ * The inverse of `parseAstmRecords`. Where the parser is liberal (vendor quirks
1795
+ * become warnings), the serializer is **conservative**: it always emits a
1796
+ * spec-clean stream with the **canonical** delimiter set (`H|\^&`), re-escapes
1797
+ * every embedded delimiter so an embedded `|`/`^`/`\`/`&` in a value can never
1798
+ * break framing (the exact inverse of the Phase-1 escape codec), and terminates
1799
+ * each record with a `CR`.
1800
+ *
1801
+ * **Round-trip by construction.** Serialization emits from a record's **decoded**
1802
+ * component tree (`AstmField.repeats`) and re-escapes each leaf, so
1803
+ * `parseAstmRecords(serializeAstmRecords(msg))` reproduces the same modelled
1804
+ * fields — the same components, the same typed accessors, and the canonical
1805
+ * delimiter set (a non-canonical source is normalized to `H|\^&`, a documented
1806
+ * Phase-7 behavior; round-tripping a vendor's own delimiters is a Phase-8
1807
+ * profile concern).
1808
+ *
1809
+ * **Never break framing.** A component leaf that contains a record terminator
1810
+ * (`CR`/`LF`) cannot be escaped by the ASTM escape codec (only the four declared
1811
+ * delimiters have mnemonics), so emitting it would silently corrupt the wire.
1812
+ * The serializer refuses: an embedded `CR`/`LF` is a typed {@link AstmSerializeError},
1813
+ * never emitted raw.
1814
+ */
1815
+
1816
+ /**
1817
+ * Thrown by the record/frame emit side when a value cannot be serialized into a
1818
+ * spec-clean stream — specifically when a component contains a record terminator
1819
+ * (`CR`/`LF`), which the ASTM escape codec cannot encode and which would break
1820
+ * framing if emitted raw. Carries a stable code + positional context, never the
1821
+ * offending value (PHI discipline).
1822
+ *
1823
+ * @example
1824
+ * ```ts
1825
+ * import { serializeAstmRecord, AstmSerializeError, parseAstmRecords } from "@cosyte/astm";
1826
+ * const rec = parseAstmRecords("H|\\^&\rL|1\r").records[1]!;
1827
+ * try {
1828
+ * serializeAstmRecord(rec);
1829
+ * } catch (err) {
1830
+ * if (err instanceof AstmSerializeError) err.code; // "ASTM_EMIT_UNENCODABLE_VALUE"
1831
+ * }
1832
+ * ```
1833
+ */
1834
+ declare class AstmSerializeError extends Error {
1835
+ /** Stable discriminant; `ASTM_EMIT_UNENCODABLE_VALUE` for a `CR`/`LF` in a value. */
1836
+ readonly code: "ASTM_EMIT_UNENCODABLE_VALUE";
1837
+ /** 0-based ordinal of the record within the message, when known. */
1838
+ readonly recordIndex?: number;
1839
+ /** @internal */
1840
+ constructor(message: string, recordIndex?: number);
1841
+ }
1842
+ /**
1843
+ * Escape-encode one component leaf for spec-clean emit — the inverse of
1844
+ * `decodeEscapes`. The **escape character itself is encoded first** (`&` → `&E&`)
1845
+ * so a later delimiter substitution can never double-encode the `&` it just
1846
+ * introduced; then the field / component / repeat delimiters map to their
1847
+ * mnemonics.
1848
+ *
1849
+ * A `CR`/`LF` in the leaf has no escape mnemonic and would break framing, so it
1850
+ * is rejected with an {@link AstmSerializeError} rather than emitted raw.
1851
+ *
1852
+ * @param leaf - One already-decoded component string.
1853
+ * @param d - The delimiters to emit against (canonical for spec-clean output).
1854
+ * @param recordIndex - The enclosing record's index, for error context.
1855
+ * @returns The escaped component text.
1856
+ * @example
1857
+ * ```ts
1858
+ * import { encodeComponent, CANONICAL_DELIMITERS } from "@cosyte/astm";
1859
+ * encodeComponent("1^40", CANONICAL_DELIMITERS); // "1&S&40"
1860
+ * ```
1861
+ */
1862
+ declare function encodeComponent(leaf: string, d: Delimiters, recordIndex?: number): string;
1863
+ /**
1864
+ * Serialize a single ASTM record to its spec-clean wire text (no trailing
1865
+ * terminator). Emits with the given delimiters, defaulting to the canonical set.
1866
+ *
1867
+ * The header (`H`) is special-cased: its delimiter-definition field is emitted as
1868
+ * the **literal** canonical declaration (`\^&`), never escaped — escaping it would
1869
+ * corrupt the very declaration a reader depends on. Manufacturer (`M`) and
1870
+ * scientific (`S`) records are surfaced **byte-identically** from their preserved
1871
+ * `rawLine` — verbatim in, verbatim out.
1872
+ *
1873
+ * @param record - The record to serialize.
1874
+ * @param d - The delimiters to emit against; defaults to `H|\^&`.
1875
+ * @returns The record's wire text, terminator excluded.
1876
+ * @throws {@link AstmSerializeError} when a component contains an unencodable `CR`/`LF`.
1877
+ * @example
1878
+ * ```ts
1879
+ * import { serializeAstmRecord, parseAstmRecords } from "@cosyte/astm";
1880
+ * const msg = parseAstmRecords("H|\\^&\rR|1|^^^687|28.6|U/L||N||F\rL|1\r");
1881
+ * serializeAstmRecord(msg.records[1]!); // "R|1|^^^687|28.6|U/L||N||F"
1882
+ * ```
1883
+ */
1884
+ declare function serializeAstmRecord(record: AstmRecord, d?: Delimiters): string;
1885
+ /**
1886
+ * Serialize a whole ASTM message (or a bare record list) to a spec-clean,
1887
+ * `CR`-terminated record stream — the inverse of `parseAstmRecords`.
1888
+ *
1889
+ * Emit is **conservative**: the canonical `H|\^&` delimiters, every embedded
1890
+ * delimiter re-escaped, each record closed with a `CR`. A message parsed with
1891
+ * non-canonical delimiters is **normalized** to the canonical set on emit (a
1892
+ * documented Phase-7 behavior — vendor-delimiter round-tripping is Phase 8).
1893
+ *
1894
+ * @param input - A parsed {@link AstmMessage} or a list of {@link AstmRecord}s.
1895
+ * @returns The serialized record stream (`CR` after every record).
1896
+ * @throws {@link AstmSerializeError} when a component contains an unencodable `CR`/`LF`.
1897
+ * @example
1898
+ * ```ts
1899
+ * import { parseAstmRecords, serializeAstmRecords } from "@cosyte/astm";
1900
+ * const raw = "H|\\^&\rP|1\rR|1|^^^687|28.6|U/L||N||F\rL|1\r";
1901
+ * serializeAstmRecords(parseAstmRecords(raw)); // === raw
1902
+ * ```
1903
+ */
1904
+ declare function serializeAstmRecords(input: AstmMessage | readonly AstmRecord[], d?: Delimiters): string;
1905
+ /**
1906
+ * Serialize a single {@link AstmField} to its spec-clean wire text, re-escaping
1907
+ * each component. A low-level helper for callers assembling a field outside a
1908
+ * whole record.
1909
+ *
1910
+ * @param field - The field to serialize.
1911
+ * @param d - The delimiters to emit against; defaults to `H|\^&`.
1912
+ * @returns The escaped field text.
1913
+ * @example
1914
+ * ```ts
1915
+ * import { serializeField, tokenizeRecord, CANONICAL_DELIMITERS } from "@cosyte/astm";
1916
+ * const fields = tokenizeRecord("R|1|^^^687|1&S&40", CANONICAL_DELIMITERS);
1917
+ * serializeField(fields[3]!); // "1&S&40"
1918
+ * ```
1919
+ */
1920
+ declare function serializeField(field: AstmField, d?: Delimiters): string;
1921
+
1922
+ /**
1923
+ * The record-layer **builder**: {@link buildAstmMessage} — Phase 7.
1924
+ *
1925
+ * A builder produces spec-clean E1394 records **by construction** from typed
1926
+ * caller input, under two disciplines:
1927
+ *
1928
+ * - **Never fabricate.** A builder emits **only** the values the caller supplied.
1929
+ * An unsupplied field is left **empty**, never filled with a default clinical
1930
+ * value — no defaulted result status, abnormal flag, units, value, or patient
1931
+ * identifier. (Leaving a field empty is not a guess; inventing an `F` status or
1932
+ * an `N` flag would be.) A consumer that parses a built result whose status the
1933
+ * caller omitted reads `status.meaning === "unspecified"`, never `"final"`.
1934
+ * - **Compute, don't guess, the structure.** The record type letters, the
1935
+ * delimiter declaration (canonical `H|\^&`), the message terminator (`L`), and
1936
+ * the per-record sequence counters are **structural** — computed by the builder,
1937
+ * not clinical values. The caller may override any sequence number.
1938
+ *
1939
+ * Every value is escape-encoded on emit (via the serializer), so an embedded
1940
+ * delimiter can never break framing, and a value carrying an unencodable `CR`/`LF`
1941
+ * is a typed {@link AstmSerializeError} rather than a corrupted wire.
1942
+ */
1943
+ /** A patient name split into its components; only the supplied parts are emitted. */
1944
+ interface PatientNameInput {
1945
+ readonly last?: string;
1946
+ readonly first?: string;
1947
+ readonly middle?: string;
1948
+ }
1949
+ /** Input for a `P` (patient) record. The three IDs stay distinct — none defaults from another. */
1950
+ interface PatientInput {
1951
+ readonly type: "P";
1952
+ /** Structural sequence number; auto-computed when omitted. */
1953
+ readonly seq?: string;
1954
+ /** Field 3 — practice-assigned patient ID. */
1955
+ readonly practiceAssignedId?: string;
1956
+ /** Field 4 — laboratory-assigned patient ID. */
1957
+ readonly laboratoryAssignedId?: string;
1958
+ /** Field 5 — a third patient identifier. */
1959
+ readonly patientIdThree?: string;
1960
+ /** Field 6 — patient name (`Last^First^Middle`). */
1961
+ readonly name?: PatientNameInput;
1962
+ /** Field 7 — mother's maiden name. */
1963
+ readonly mothersMaidenName?: string;
1964
+ /** Field 8 — birthdate (`YYYYMMDDHHMMSS`), emitted verbatim, never reformatted. */
1965
+ readonly birthDate?: string;
1966
+ /** Field 9 — sex, emitted verbatim (never defaulted). */
1967
+ readonly sex?: string;
1968
+ }
1969
+ /** Input for an `O` (order) record. */
1970
+ interface OrderInput {
1971
+ readonly type: "O";
1972
+ readonly seq?: string;
1973
+ /** Field 3 — specimen / accession ID. */
1974
+ readonly specimenId?: string;
1975
+ /** Field 4 — instrument specimen ID. */
1976
+ readonly instrumentSpecimenId?: string;
1977
+ /** Field 5 — Universal Test ID components (verbatim, in order; e.g. `["", "", "", "687"]`). */
1978
+ readonly universalTestId?: readonly string[];
1979
+ /** Field 6 — priority, emitted verbatim. */
1980
+ readonly priority?: string;
1981
+ /** Field 12 — action code, emitted verbatim. */
1982
+ readonly actionCode?: string;
1983
+ /** Field 26 — report type, emitted verbatim. */
1984
+ readonly reportType?: string;
1985
+ }
1986
+ /** Input for an `R` (result) record. No clinical field is defaulted; unsupplied ⇒ empty. */
1987
+ interface ResultInput {
1988
+ readonly type: "R";
1989
+ readonly seq?: string;
1990
+ /** Field 3 — Universal Test ID components (verbatim). */
1991
+ readonly universalTestId?: readonly string[];
1992
+ /** Field 4 — the measured value, emitted verbatim (never defaulted). */
1993
+ readonly value?: string;
1994
+ /** Field 5 — units (vendor free text; never defaulted, guessed, or converted). */
1995
+ readonly units?: string;
1996
+ /** Field 6 — reference range, emitted verbatim. */
1997
+ readonly referenceRange?: string;
1998
+ /** Field 7 — abnormal flags, emitted verbatim (never defaulted to `N`). */
1999
+ readonly abnormalFlags?: string;
2000
+ /** Field 9 — result status, emitted verbatim (never defaulted to `F`). */
2001
+ readonly resultStatus?: string;
2002
+ /** Field 11 — operator. */
2003
+ readonly operator?: string;
2004
+ /** Field 12 — test-started timestamp (`YYYYMMDDHHMMSS`), verbatim. */
2005
+ readonly startedAt?: string;
2006
+ /** Field 13 — test-completed timestamp, verbatim. */
2007
+ readonly completedAt?: string;
2008
+ /** Field 14 — instrument identifier. */
2009
+ readonly instrument?: string;
2010
+ }
2011
+ /** Input for a `C` (comment) record. */
2012
+ interface CommentInput {
2013
+ readonly type: "C";
2014
+ readonly seq?: string;
2015
+ /** Field 3 — comment source. */
2016
+ readonly source?: string;
2017
+ /** Field 4 — comment text (a single component). Use {@link CommentInput.textComponents} for a structured comment. */
2018
+ readonly text?: string;
2019
+ /** Field 4 — comment text as explicit components (takes precedence over `text` when set). */
2020
+ readonly textComponents?: readonly string[];
2021
+ /** Field 5 — comment type code, emitted verbatim. */
2022
+ readonly commentType?: string;
2023
+ }
2024
+ /** Input for a `Q` (request-information) record. */
2025
+ interface QueryInput {
2026
+ readonly type: "Q";
2027
+ readonly seq?: string;
2028
+ /** Field 3 — starting range ID, emitted verbatim. */
2029
+ readonly startingRangeId?: string;
2030
+ /** Field 4 — ending range ID, emitted verbatim. */
2031
+ readonly endingRangeId?: string;
2032
+ /** Field 5 — Universal Test ID components; ignored when {@link QueryInput.queriesAllTests} is set. */
2033
+ readonly universalTestId?: readonly string[];
2034
+ /** Field 5 — emit the literal `ALL` universal-query keyword instead of a Universal Test ID. */
2035
+ readonly queriesAllTests?: boolean;
2036
+ /** Field 13 — request-information status, emitted verbatim. */
2037
+ readonly requestInformationStatus?: string;
2038
+ }
2039
+ /**
2040
+ * Input for an `M` (manufacturer) or `S` (scientific) record — vendor-defined
2041
+ * free-form data. Emitted verbatim from the caller's fields; never interpreted.
2042
+ */
2043
+ interface VerbatimInput {
2044
+ readonly type: "M" | "S";
2045
+ /** Data fields (after the type letter), emitted verbatim in order. */
2046
+ readonly fields: readonly string[];
2047
+ }
2048
+ /** Any record the message builder can emit (the terminator `L` is appended automatically). */
2049
+ type AstmRecordInput = PatientInput | OrderInput | ResultInput | CommentInput | QueryInput | VerbatimInput;
2050
+ /** Optional header fields (after the delimiter declaration). */
2051
+ interface HeaderInput {
2052
+ /** Extra `H`-record fields (field 3 onward), emitted verbatim in order. Field 2 is always `\^&`. */
2053
+ readonly fields?: readonly string[];
2054
+ }
2055
+ /** The message to build: an optional header, the body records, and an optional terminator code. */
2056
+ interface MessageInput {
2057
+ /** Header fields; the canonical `H|\^&` declaration is always emitted. */
2058
+ readonly header?: HeaderInput;
2059
+ /** The body records, in order. `H` and `L` are supplied by the builder. */
2060
+ readonly records: readonly AstmRecordInput[];
2061
+ /**
2062
+ * Field 3 of the auto-appended `L` (terminator) record — the termination code
2063
+ * (e.g. `N` normal). Omitted by default (not defaulted to a value the caller
2064
+ * did not choose); the `L` seq is always emitted.
2065
+ */
2066
+ readonly terminationCode?: string;
2067
+ }
2068
+ /**
2069
+ * Build a spec-clean ASTM/CLSI-LIS02 record stream from typed input.
2070
+ *
2071
+ * The builder emits the canonical `H|\^&` header (plus any header fields), then
2072
+ * every body record with each supplied field at its correct 1-based position and
2073
+ * every value escape-encoded, then an `L` terminator. **Nothing clinical is
2074
+ * defaulted** — an unsupplied field is empty, never a guessed value — and the
2075
+ * structural pieces (delimiters, record types, sequence counters, terminator) are
2076
+ * computed. The result parses back to an equal message: build → parse fidelity by
2077
+ * construction.
2078
+ *
2079
+ * @param input - The message to build.
2080
+ * @returns The serialized, `CR`-terminated record stream.
2081
+ * @throws {@link AstmSerializeError} when a value contains an unencodable `CR`/`LF`.
2082
+ * @example
2083
+ * ```ts
2084
+ * import { buildAstmMessage, parseAstmRecords, results } from "@cosyte/astm";
2085
+ * const raw = buildAstmMessage({
2086
+ * records: [{ type: "R", universalTestId: ["", "", "", "687"], value: "28.6", units: "U/L" }],
2087
+ * });
2088
+ * results(parseAstmRecords(raw))[0]?.value; // "28.6"
2089
+ * ```
2090
+ */
2091
+ declare function buildAstmMessage(input: MessageInput): string;
2092
+
2093
+ /**
2094
+ * Typed extractors over an {@link AstmMessage} — the one-line "pull the value out"
2095
+ * surface that is the package's north star.
2096
+ *
2097
+ * They are thin, total, and immutable: they read the already-parsed model and
2098
+ * never re-parse, never mutate, and never fabricate. `results()` returns every
2099
+ * `R` record in order; `patient()` returns the first `P` record (the identity a
2100
+ * result files against), or `undefined` when the message carries none.
2101
+ */
2102
+
2103
+ /**
2104
+ * Every result (`R`) record in the message, in wire order.
2105
+ *
2106
+ * @param msg - A parsed message.
2107
+ * @returns The result records (possibly empty).
2108
+ * @example
2109
+ * ```ts
2110
+ * import { parseAstmRecords, results } from "@cosyte/astm";
2111
+ * const msg = parseAstmRecords("H|\\^&\rR|1|^^^687|28.6|U/L||N||F\rL|1\r");
2112
+ * results(msg)[0]?.units; // "U/L"
2113
+ * ```
2114
+ */
2115
+ declare function results(msg: AstmMessage): readonly ResultRecord[];
2116
+ /**
2117
+ * The first patient (`P`) record, or `undefined` when the message has none.
2118
+ *
2119
+ * A message can carry several `P` records (multiple patients in a batch); this
2120
+ * returns the first. Consumers needing all of them can filter `msg.records`.
2121
+ *
2122
+ * @param msg - A parsed message.
2123
+ * @returns The first patient record, or `undefined`.
2124
+ * @example
2125
+ * ```ts
2126
+ * import { parseAstmRecords, patient } from "@cosyte/astm";
2127
+ * const msg = parseAstmRecords("H|\\^&\rP|1|PRAC|LAB\rL|1\r");
2128
+ * patient(msg)?.practiceAssignedId; // "PRAC"
2129
+ * ```
2130
+ */
2131
+ declare function patient(msg: AstmMessage): PatientRecord | undefined;
2132
+ /**
2133
+ * Every order (`O`) record in the message, in wire order.
2134
+ *
2135
+ * @param msg - A parsed message.
2136
+ * @returns The order records (possibly empty).
2137
+ * @example
2138
+ * ```ts
2139
+ * import { parseAstmRecords, orders } from "@cosyte/astm";
2140
+ * const msg = parseAstmRecords("H|\\^&\rO|1|ACC-42||^^^687|R\rL|1\r");
2141
+ * orders(msg)[0]?.specimenId; // "ACC-42"
2142
+ * ```
2143
+ */
2144
+ declare function orders(msg: AstmMessage): readonly OrderRecord[];
2145
+ /**
2146
+ * Every comment (`C`) record in the message, in wire order. Each carries the
2147
+ * `parentIndex` of the `H`/`P`/`O`/`R` it attaches to (or `attachedToRoot` when
2148
+ * it is an orphan) — use {@link commentsFor} to get the comments of one record.
2149
+ *
2150
+ * @param msg - A parsed message.
2151
+ * @returns The comment records (possibly empty).
2152
+ * @example
2153
+ * ```ts
2154
+ * import { parseAstmRecords, comments } from "@cosyte/astm";
2155
+ * const msg = parseAstmRecords("H|\\^&\rR|1|^^^687|5|U/L||||F\rC|1|I|checked|G\rL|1\r");
2156
+ * comments(msg)[0]?.text; // "checked"
2157
+ * ```
2158
+ */
2159
+ declare function comments(msg: AstmMessage): readonly CommentRecord[];
2160
+ /**
2161
+ * The comment (`C`) records attached to a given parent record, in wire order.
2162
+ * Returns the comments whose `parentIndex` is that record's `recordIndex`, so a
2163
+ * comment carrying (e.g.) QC context is read against the record it modifies —
2164
+ * never floated to the wrong one.
2165
+ *
2166
+ * @param msg - A parsed message.
2167
+ * @param parent - The `H`/`P`/`O`/`R` record whose comments to collect.
2168
+ * @returns The attached comment records (possibly empty).
2169
+ * @example
2170
+ * ```ts
2171
+ * import { parseAstmRecords, results, commentsFor } from "@cosyte/astm";
2172
+ * const msg = parseAstmRecords("H|\\^&\rR|1|^^^687|5|U/L||||F\rC|1|I|checked|G\rL|1\r");
2173
+ * commentsFor(msg, results(msg)[0]!)[0]?.text; // "checked"
2174
+ * ```
2175
+ */
2176
+ declare function commentsFor(msg: AstmMessage, parent: AstmRecord): readonly CommentRecord[];
2177
+ /**
2178
+ * Every request-information (`Q`) record in the message, in wire order. A non-empty
2179
+ * result means the message is a **host-query request**, not a result set — see
2180
+ * {@link AstmMessage.classification} (`isHostQueryRequest`).
2181
+ *
2182
+ * @param msg - A parsed message.
2183
+ * @returns The query records (possibly empty).
2184
+ * @example
2185
+ * ```ts
2186
+ * import { parseAstmRecords, query } from "@cosyte/astm";
2187
+ * const msg = parseAstmRecords("H|\\^&\rP|1\rQ|1|^SPEC-7||ALL\rL|1\r");
2188
+ * query(msg)[0]?.queriesAllTests; // true
2189
+ * ```
2190
+ */
2191
+ declare function query(msg: AstmMessage): readonly QueryRecord[];
2192
+
2193
+ /**
2194
+ * The **host-query flow**: classify a message as a request, a result upload, an
2195
+ * order download, or indeterminate — Phase 4.
2196
+ *
2197
+ * On real analyzers the host-query mode is first-class and sometimes mandatory
2198
+ * (on the Roche cobas 4800 there is *no* results-sending-only option): the
2199
+ * analyzer sends an `H/P/Q/L` **request** and the LIS answers with an `H/P/O/L`
2200
+ * **response**. Misreading a query as a result upload (or the reverse) breaks the
2201
+ * order flow, so this module makes the distinction **explicit**:
2202
+ *
2203
+ * **The `Q` record dominates.** Any message carrying a `Q` (request-information)
2204
+ * record is classified `host-query` — a request — and is **never** treated as a
2205
+ * result set, even if a result record is also present (an anomaly, which the
2206
+ * parser flags separately with `ASTM_RECORD_AMBIGUOUS_MESSAGE_KIND`). This is the
2207
+ * whole point: a `Q`-bearing message can never silently read as a result upload.
2208
+ */
2209
+
2210
+ /**
2211
+ * Classify a record stream by the host-query flow. Pure and total — it only reads
2212
+ * the record type letters, never a field value.
2213
+ *
2214
+ * `Q` dominates: a message with any `Q` record is a `host-query` request even when
2215
+ * a result (`R`) record is also present, so a query is never misread as a result
2216
+ * upload. (The `Q`+`R` anomaly is separately warned at parse time.)
2217
+ *
2218
+ * @param records - The parsed records, in wire order.
2219
+ * @returns The message classification.
2220
+ * @example
2221
+ * ```ts
2222
+ * import { classifyMessage, parseAstmRecords } from "@cosyte/astm";
2223
+ * const msg = parseAstmRecords("H|\\^&\rP|1\rQ|1|^SPEC-7||ALL\rL|1\r");
2224
+ * classifyMessage(msg.records).kind; // "host-query"
2225
+ * ```
2226
+ */
2227
+ declare function classifyMessage(records: readonly AstmRecord[]): AstmMessageClassification;
2228
+
2229
+ /**
2230
+ * Record tokenizer: split one raw ASTM record into typed {@link AstmField}s.
2231
+ *
2232
+ * The split is **escape-aware first, decode second** (see `../common/escapes.ts`):
2233
+ * a field / repeat / component boundary is only recognized on an *unescaped*
2234
+ * delimiter, and each resulting component leaf is decoded afterwards. That
2235
+ * ordering is the whole reason an embedded escaped component delimiter (`&S&`)
2236
+ * reads as a single component instead of being mis-split.
2237
+ */
2238
+
2239
+ /**
2240
+ * Tokenize a single record string (its terminator already stripped) into its
2241
+ * fields. `fields[0]` is the type-letter field; ASTM data fields follow at
2242
+ * 1-based indices.
2243
+ *
2244
+ * @param record - The raw record text.
2245
+ * @param d - The delimiters resolved from the header.
2246
+ * @param onUnknownEscape - Called (with the 1-based field index) for each
2247
+ * unrecognized escape sequence encountered, so the caller can warn.
2248
+ * @returns The record's fields.
2249
+ * @example
2250
+ * ```ts
2251
+ * import { tokenizeRecord, CANONICAL_DELIMITERS } from "@cosyte/astm";
2252
+ * const fields = tokenizeRecord("R|1|^^^687|28.6|U/L", CANONICAL_DELIMITERS);
2253
+ * fields[3].components[0]; // "28.6"
2254
+ * ```
2255
+ */
2256
+ declare function tokenizeRecord(record: string, d: Delimiters, onUnknownEscape?: (fieldIndex: number) => void): AstmField[];
2257
+ /**
2258
+ * The primary scalar of a field: its first repeat's first component, decoded.
2259
+ * Returns `undefined` for a truly empty field so callers can distinguish
2260
+ * "absent" from a value — never defaulting a missing value.
2261
+ *
2262
+ * @example
2263
+ * ```ts
2264
+ * import { fieldScalar, tokenizeRecord, CANONICAL_DELIMITERS } from "@cosyte/astm";
2265
+ * const f = tokenizeRecord("R|1|^^^687|28.6", CANONICAL_DELIMITERS);
2266
+ * fieldScalar(f[3]); // "28.6"
2267
+ * ```
2268
+ */
2269
+ declare function fieldScalar(field: AstmField | undefined): string | undefined;
2270
+
2271
+ /**
2272
+ * Fatal error taxonomy for the `@cosyte/astm` record parser.
2273
+ *
2274
+ * Tier-3 **fatal** codes mark input the parser cannot recover into a structured
2275
+ * `AstmMessage`; anything less severe is a Tier-2 warning (see `./warnings.ts`).
2276
+ * `AstmParseError` is thrown directly and consumers narrow via the `code`
2277
+ * discriminant. The set is additions-only across phases — the record layer
2278
+ * introduces three, one of which (`EMPTY_INPUT`) is shared with every future
2279
+ * layer (the frame codec adds its own `ASTM_FRAME_*` fatals later).
2280
+ */
2281
+
2282
+ /**
2283
+ * Stable string codes for every Tier-3 fatal the record parser may throw.
2284
+ * Consumers narrow on `err.code` to react to specific structural failures.
2285
+ * Renaming a code is a **breaking change**.
2286
+ *
2287
+ * @example
2288
+ * ```ts
2289
+ * import { parseAstmRecords, FATAL_CODES, AstmParseError } from "@cosyte/astm";
2290
+ * try {
2291
+ * parseAstmRecords("");
2292
+ * } catch (err) {
2293
+ * if (err instanceof AstmParseError && err.code === FATAL_CODES.EMPTY_INPUT) {
2294
+ * // handle empty input
2295
+ * }
2296
+ * }
2297
+ * ```
2298
+ */
2299
+ declare const FATAL_CODES: {
2300
+ /** Input was empty or whitespace-only — there is nothing to parse. Shared across layers. */
2301
+ readonly EMPTY_INPUT: "EMPTY_INPUT";
2302
+ /** The first record is not an `H` (header) record — an ASTM message must lead with `H`. */
2303
+ readonly ASTM_RECORD_NO_HEADER: "ASTM_RECORD_NO_HEADER";
2304
+ /** The `H` record is too short to declare the four delimiters (field/repeat/component/escape). */
2305
+ readonly ASTM_RECORD_UNDECLARED_DELIMITERS: "ASTM_RECORD_UNDECLARED_DELIMITERS";
2306
+ };
2307
+ /**
2308
+ * A value from {@link FATAL_CODES} — the type carried by a thrown {@link AstmParseError}.
2309
+ */
2310
+ type FatalCode = (typeof FATAL_CODES)[keyof typeof FATAL_CODES];
2311
+ /**
2312
+ * Thrown by {@link parseAstmRecords} when the input violates one of the Tier-3
2313
+ * unrecoverable structural rules (empty input, no leading `H`, or an `H` that
2314
+ * cannot declare its delimiters). Carries positional context so consumers can
2315
+ * log an actionable error.
2316
+ *
2317
+ * **PHI:** the error carries a stable code + position only, never a field value.
2318
+ *
2319
+ * @example
2320
+ * ```ts
2321
+ * import { parseAstmRecords, AstmParseError } from "@cosyte/astm";
2322
+ * try {
2323
+ * parseAstmRecords("P|1");
2324
+ * } catch (err) {
2325
+ * if (err instanceof AstmParseError && err.code === "ASTM_RECORD_NO_HEADER") {
2326
+ * // err.position is available; err carries no field value
2327
+ * }
2328
+ * }
2329
+ * ```
2330
+ */
2331
+ declare class AstmParseError extends Error {
2332
+ readonly code: FatalCode;
2333
+ readonly position: AstmPosition;
2334
+ /**
2335
+ * Construct a new `AstmParseError`. Both fields are required so every thrower
2336
+ * populates positional context and no PHI-bearing snippet is ever attached.
2337
+ *
2338
+ * @internal
2339
+ */
2340
+ constructor(code: FatalCode, message: string, position: AstmPosition);
2341
+ }
2342
+
2343
+ /**
2344
+ * The ASTM escape codec — the Phase-1 fix for a documented silent-misread class.
2345
+ *
2346
+ * ASTM escapes an embedded delimiter with the escape character on both sides of
2347
+ * a single mnemonic letter (the InterSystems mnemonics, cross-verified in the
2348
+ * roadmap's OSS references):
2349
+ *
2350
+ * ```
2351
+ * &F& → field delimiter (default "|")
2352
+ * &S& → component delimiter (default "^")
2353
+ * &R& → repeat delimiter (default "\")
2354
+ * &E& → escape character (default "&")
2355
+ * ```
2356
+ *
2357
+ * **Why this is a requirement, not a nicety.** A result value can legitimately
2358
+ * contain a component delimiter — e.g. a titre written `1^40`. On the wire that
2359
+ * is escaped as `1&S&40`. A parser that splits a field into components on the
2360
+ * raw component delimiter and *never decodes the escape* is safe here (the
2361
+ * escape body `&S&` contains no literal `^`), but a parser that **decodes first
2362
+ * and splits second** turns `1&S&40` into `1^40` and then mis-splits it into two
2363
+ * components — the silent misread `python-astm` / `Chistousov` exhibit. The
2364
+ * correct order, implemented here, is **escape-aware split, then decode each
2365
+ * leaf**: the escape sequence survives the split intact and only then decodes to
2366
+ * a single literal, so `1&S&40` reads as exactly one component.
2367
+ *
2368
+ * Re-escaping (the inverse, for spec-clean emit) belongs to the serialize phase
2369
+ * (P7) and is deliberately not implemented here.
2370
+ */
2371
+
2372
+ /**
2373
+ * A callback the codec calls when it encounters an escape sequence whose body is
2374
+ * not one of the four recognized mnemonics. The sequence is preserved verbatim;
2375
+ * the callback lets the parser surface a value-free `ASTM_UNKNOWN_ESCAPE_SEQUENCE`
2376
+ * warning. Optional so the codec can be used purely.
2377
+ */
2378
+ type UnknownEscapeSink = () => void;
2379
+ /**
2380
+ * Decode the four recognized ASTM escape mnemonics in a single already-split
2381
+ * leaf (a component string), substituting the *active* delimiters read from the
2382
+ * header. Unrecognized `&…&` bodies are preserved verbatim and reported through
2383
+ * `onUnknown` (never dropped, never guessed).
2384
+ *
2385
+ * This runs **after** splitting, so a decoded delimiter becomes ordinary literal
2386
+ * text and can never introduce a new split boundary.
2387
+ *
2388
+ * @param leaf - One component string, escape-aware split already applied.
2389
+ * @param d - The delimiters resolved from the header.
2390
+ * @param onUnknown - Called once per unrecognized escape body encountered.
2391
+ * @returns The decoded string.
2392
+ * @example
2393
+ * ```ts
2394
+ * import { decodeEscapes, CANONICAL_DELIMITERS } from "@cosyte/astm";
2395
+ * decodeEscapes("1&S&40", CANONICAL_DELIMITERS); // "1^40"
2396
+ * ```
2397
+ */
2398
+ declare function decodeEscapes(leaf: string, d: Delimiters, onUnknown?: UnknownEscapeSink): string;
2399
+ /**
2400
+ * Split `text` on `delimiter`, treating any escape sequence (`escape …
2401
+ * escape`) as an opaque atom so a delimiter that appears *inside* an escape body
2402
+ * never causes a split. Returns the raw (still-encoded) segments — decoding is
2403
+ * the caller's next step, per the escape-aware-split-then-decode contract.
2404
+ *
2405
+ * For the four canonical mnemonics this is belt-and-suspenders (their bodies are
2406
+ * letters, not delimiters), but it makes the "an escaped delimiter is one token"
2407
+ * guarantee hold for any declared delimiter set, including adversarial input.
2408
+ *
2409
+ * @param text - The field or repeat string to split.
2410
+ * @param delimiter - The delimiter to split on.
2411
+ * @param escape - The active escape character.
2412
+ * @returns The raw segments, in order.
2413
+ * @example
2414
+ * ```ts
2415
+ * import { splitEscapeAware } from "@cosyte/astm";
2416
+ * splitEscapeAware("a^b^c", "^", "&"); // ["a", "b", "c"]
2417
+ * splitEscapeAware("1&S&40", "^", "&"); // ["1&S&40"] (escape body is opaque)
2418
+ * ```
2419
+ */
2420
+ declare function splitEscapeAware(text: string, delimiter: string, escape: string): string[];
2421
+
2422
+ /**
2423
+ * Deep-freeze helper for the immutable ASTM model.
2424
+ *
2425
+ * The parser returns plain readonly data; `deepFreeze` makes that immutability
2426
+ * enforced at runtime (a stray `msg.records[0].fields[0] = …` throws in strict
2427
+ * mode and is a no-op otherwise), which the shared `immutabilityProperty`
2428
+ * invariant asserts. Zero-dep, Node stdlib only.
2429
+ */
2430
+ /**
2431
+ * Recursively freeze an object graph (objects and arrays), returning the same
2432
+ * reference typed as deeply readonly. Cyclic graphs are not produced by the
2433
+ * parser, so a simple recursive walk suffices.
2434
+ *
2435
+ * @param value - The value to freeze in place.
2436
+ * @returns The same value, now deeply frozen.
2437
+ * @example
2438
+ * ```ts
2439
+ * import { deepFreeze } from "@cosyte/astm";
2440
+ * const frozen = deepFreeze({ a: [1, 2] });
2441
+ * Object.isFrozen(frozen.a); // true
2442
+ * ```
2443
+ */
2444
+ declare function deepFreeze<T>(value: T): T;
2445
+
2446
+ /**
2447
+ * `defineAstmProfile()` — the public factory for building immutable
2448
+ * {@link AstmProfile} objects with the safety rules enforced and `describe()`
2449
+ * attached. Mirrors the sibling `@cosyte/hl7` `defineProfile()` / `@cosyte/ccda`
2450
+ * `defineCcdaProfile()` shape (name / lineage / `extends`-merge / `describe`) while
2451
+ * modelling ASTM quirks — a tolerated warning code with provenance, plus the
2452
+ * optional raw-vs-framed transport override — rather than HL7 v2 Z-segments or
2453
+ * C-CDA template deviations.
2454
+ *
2455
+ * Zero runtime deps. No `any`; immutability at the return boundary via
2456
+ * `Object.freeze` (top-level, matching the sibling's boundary-freeze doctrine).
2457
+ */
2458
+
2459
+ /**
2460
+ * Build a frozen {@link AstmProfile} from a validated options object. Throws
2461
+ * {@link AstmProfileDefinitionError} on a bad name, an unknown option key, an
2462
+ * invalid `transport`, or an invalid `tolerate` entry — including the **safety
2463
+ * rule**: a profile may never tolerate a safety-critical warning code (default-deny
2464
+ * across all three registries).
2465
+ *
2466
+ * `extends` composes profiles: lineage, `tolerate`, `transport`, `provenance`, and
2467
+ * `description` merge (parents left-to-right, then self; scalars are child-wins).
2468
+ * The merged `tolerate` set is re-validated so a safety-critical code cannot sneak
2469
+ * in via a hand-crafted parent.
2470
+ *
2471
+ * @param opts - The profile definition; see {@link DefineAstmProfileOptions}.
2472
+ * @returns A frozen, immutable profile with `describe()` attached.
2473
+ * @throws {@link AstmProfileDefinitionError} on any invalid definition.
2474
+ * @example
2475
+ * ```ts
2476
+ * import { defineAstmProfile } from "@cosyte/astm";
2477
+ * const site = defineAstmProfile({
2478
+ * name: "acme-analyzer",
2479
+ * description: "Acme's inbound raw-TCP analyzer",
2480
+ * transport: "raw",
2481
+ * tolerate: [
2482
+ * { code: "ASTM_RECORD_UNKNOWN_TYPE", rationale: "emits a vendor QC record letter" },
2483
+ * ],
2484
+ * provenance: { source: "Acme host-interface manual", reference: "internal-2026" },
2485
+ * });
2486
+ * site.lineage; // ["acme-analyzer"]
2487
+ * console.log(site.describe?.());
2488
+ * ```
2489
+ */
2490
+ declare function defineAstmProfile(opts: DefineAstmProfileOptions): AstmProfile;
2491
+
2492
+ /**
2493
+ * The definition-time error for the `@cosyte/astm` vendor-profile subsystem.
2494
+ *
2495
+ * A profile is validated when it is *defined*, not when it is applied — so a
2496
+ * misconfigured profile (a bad name, an unknown option key, a tolerated code that
2497
+ * is unknown or **safety-critical**) fails loudly at construction, long before it
2498
+ * could quiet a real deviation. This is the class that failure throws. It is a
2499
+ * programming error (a bad profile literal), distinct from `AstmParseError` (bad
2500
+ * input) — so consumers narrow on it separately.
2501
+ */
2502
+ /**
2503
+ * Thrown by `defineAstmProfile` when a profile definition is invalid: a missing or
2504
+ * empty `name`, an unknown option key, an invalid `transport` value, or a
2505
+ * `tolerate` entry whose code is unknown, whose `rationale` is empty, or — the
2506
+ * load-bearing safety rule — whose code is **safety-critical** (a result value,
2507
+ * abnormal flag, result status, reference range, units, patient/comment context,
2508
+ * message-kind, code system, or any frame/LTP integrity warning). Carries the
2509
+ * offending profile's `name` when it is known, so a multi-profile build names the
2510
+ * culprit.
2511
+ *
2512
+ * @example
2513
+ * ```ts
2514
+ * import { defineAstmProfile, AstmProfileDefinitionError } from "@cosyte/astm";
2515
+ * try {
2516
+ * defineAstmProfile({
2517
+ * name: "unsafe",
2518
+ * tolerate: [{ code: "ASTM_FRAME_BAD_CHECKSUM", rationale: "no" }],
2519
+ * });
2520
+ * } catch (err) {
2521
+ * if (err instanceof AstmProfileDefinitionError) err.profileName; // "unsafe"
2522
+ * }
2523
+ * ```
2524
+ */
2525
+ declare class AstmProfileDefinitionError extends Error {
2526
+ /** The offending profile's `name`, when it could be read. */
2527
+ readonly profileName?: string;
2528
+ /**
2529
+ * Construct a new `AstmProfileDefinitionError`.
2530
+ *
2531
+ * @param message - A human-readable explanation of what was invalid.
2532
+ * @param profileName - The offending profile's name, when known.
2533
+ * @internal
2534
+ */
2535
+ constructor(message: string, profileName?: string);
2536
+ }
2537
+
2538
+ /**
2539
+ * The profile **registry**: the named built-in set (with provenance) plus a
2540
+ * process-scoped default. Mirrors the sibling `@cosyte/hl7` / `@cosyte/ccda`
2541
+ * `set/getDefault*Profile` convention. `parseAstmRecords(raw)` with no explicit
2542
+ * `profile` option consults {@link getDefaultAstmProfile}; an explicit `profile`
2543
+ * option always wins, and `profile: null` opts out of the default for a single call.
2544
+ *
2545
+ * The single mutable module-scoped `let` is the only such state in the profile
2546
+ * subsystem — an intentional, documented trade-off (identical to the siblings).
2547
+ * Tests that set a default MUST clear it in teardown (`setDefaultAstmProfile(null)`)
2548
+ * to avoid cross-test bleed.
2549
+ */
2550
+
2551
+ /**
2552
+ * Look up a built-in profile by name. Returns `undefined` when no built-in has that
2553
+ * name (a user-defined profile is not in this registry — pass it directly).
2554
+ *
2555
+ * @param name - The built-in profile name.
2556
+ * @returns The profile, or `undefined`.
2557
+ * @example
2558
+ * ```ts
2559
+ * import { getAstmProfile } from "@cosyte/astm";
2560
+ * getAstmProfile("referenceCorpus")?.provenance?.source;
2561
+ * ```
2562
+ */
2563
+ declare function getAstmProfile(name: string): AstmProfile | undefined;
2564
+ /**
2565
+ * The names of every built-in profile, in registration order.
2566
+ *
2567
+ * @returns The built-in names.
2568
+ * @example
2569
+ * ```ts
2570
+ * import { listAstmProfiles } from "@cosyte/astm";
2571
+ * listAstmProfiles(); // ["default", "referenceCorpus"]
2572
+ * ```
2573
+ */
2574
+ declare function listAstmProfiles(): readonly string[];
2575
+ /**
2576
+ * Register a process-scoped default profile that `parseAstmRecords(raw)` applies
2577
+ * when no explicit `profile` option is passed. Pass `null` (or `undefined`) to
2578
+ * clear. An explicit `parseAstmRecords(raw, { profile })` always wins;
2579
+ * `{ profile: null }` opts out of the default for a single call.
2580
+ *
2581
+ * **Test hygiene:** the only mutable module-scoped state here — tests that call this
2582
+ * MUST clear it in teardown or default-profile bleed infects later tests.
2583
+ *
2584
+ * @param profile - The profile to register as default, or `null` to clear.
2585
+ * @example
2586
+ * ```ts
2587
+ * import { setDefaultAstmProfile, astmProfiles, parseAstmRecords } from "@cosyte/astm";
2588
+ * setDefaultAstmProfile(astmProfiles.referenceCorpus);
2589
+ * const msg = parseAstmRecords(raw); // uses referenceCorpus
2590
+ * setDefaultAstmProfile(null); // clear
2591
+ * ```
2592
+ */
2593
+ declare function setDefaultAstmProfile(profile: AstmProfile | null): void;
2594
+ /**
2595
+ * Return the current process-scoped default profile, or `undefined` if none is
2596
+ * registered.
2597
+ *
2598
+ * @returns The default profile, or `undefined`.
2599
+ * @example
2600
+ * ```ts
2601
+ * import { getDefaultAstmProfile } from "@cosyte/astm";
2602
+ * const p = getDefaultAstmProfile();
2603
+ * if (p !== undefined) console.log("default profile:", p.name);
2604
+ * ```
2605
+ */
2606
+ declare function getDefaultAstmProfile(): AstmProfile | undefined;
2607
+
2608
+ /**
2609
+ * The runtime side of the profile subsystem: turning an active {@link AstmProfile}
2610
+ * into a warning transform the record parser runs over its accumulated warnings,
2611
+ * plus the transport-override accessor the framing detector consumes.
2612
+ *
2613
+ * A profile **never touches an extracted value**. The warning transform operates
2614
+ * purely at the warning layer — downgrading a deviation it *expects* into a
2615
+ * `PROFILE_QUIRK_APPLIED` warning (flagged `expected`, carrying the original
2616
+ * `toleratedCode`) while leaving every un-expected warning untouched. Because the
2617
+ * transform only ever rewrites a warning object (never a record, field, or value),
2618
+ * a profile cannot change how any byte parses: a spec-clean message parses
2619
+ * identically with or without a profile.
2620
+ */
2621
+
2622
+ /**
2623
+ * Apply a profile to a single warning. Returns a downgraded `PROFILE_QUIRK_APPLIED`
2624
+ * warning when the profile expects this deviation; otherwise returns the original
2625
+ * warning unchanged (referential identity preserved, so an un-tolerated warning is
2626
+ * never reallocated). A warning that is already `expected` (e.g. re-processed) is
2627
+ * passed through untouched.
2628
+ *
2629
+ * @param profile - The active profile.
2630
+ * @param warning - One accumulated warning.
2631
+ * @returns The re-badged warning when tolerated, else the original.
2632
+ * @example
2633
+ * ```ts
2634
+ * import { applyAstmProfile, astmProfiles, unknownEscapeSequence } from "@cosyte/astm";
2635
+ * const w = unknownEscapeSequence({ recordIndex: 4, recordType: "R", fieldIndex: 5 });
2636
+ * const out = applyAstmProfile(astmProfiles.referenceCorpus, w);
2637
+ * out.code; // "PROFILE_QUIRK_APPLIED"
2638
+ * out.toleratedCode; // "ASTM_UNKNOWN_ESCAPE_SEQUENCE"
2639
+ * ```
2640
+ */
2641
+ declare function applyAstmProfile(profile: AstmProfile, warning: AstmRecordWarning): AstmRecordWarning;
2642
+ /**
2643
+ * Apply a profile across a whole warning list, returning a NEW array (the input is
2644
+ * never mutated). Returned unchanged (same reference contents, a shallow copy) when
2645
+ * `profile` is `undefined`, so the no-profile path pays only a copy. Every tolerated
2646
+ * warning is re-badged; every other warning passes through by identity — nothing is
2647
+ * dropped, reordered, or reallocated beyond the re-badge.
2648
+ *
2649
+ * @param warnings - The accumulated record warnings.
2650
+ * @param profile - The active profile, or `undefined` for no transform.
2651
+ * @returns A new array with tolerated warnings downgraded.
2652
+ * @example
2653
+ * ```ts
2654
+ * import { applyAstmProfileToWarnings, astmProfiles } from "@cosyte/astm";
2655
+ * const out = applyAstmProfileToWarnings(msgWarnings, astmProfiles.referenceCorpus);
2656
+ * ```
2657
+ */
2658
+ declare function applyAstmProfileToWarnings(warnings: readonly AstmRecordWarning[], profile: AstmProfile | undefined): AstmRecordWarning[];
2659
+ /**
2660
+ * The profile's transport override, if any — the value a consumer feeds to
2661
+ * `detectFraming(bytes, { override })` to force framed/raw and bypass leading-byte
2662
+ * auto-detection. `undefined` means "let detection decide."
2663
+ *
2664
+ * @param profile - The active profile, or `undefined`.
2665
+ * @returns `"framed"` / `"raw"` when the profile forces one, else `undefined`.
2666
+ * @example
2667
+ * ```ts
2668
+ * import { resolveProfileTransport, detectFraming, astmProfiles } from "@cosyte/astm";
2669
+ * const override = resolveProfileTransport(myRawTcpProfile);
2670
+ * const { framing } = detectFraming(bytes, override !== undefined ? { override } : {});
2671
+ * ```
2672
+ */
2673
+ declare function resolveProfileTransport(profile: AstmProfile | undefined): AstmFraming | undefined;
2674
+
2675
+ /**
2676
+ * The safety spine of the profile subsystem: the set of warning codes a profile is
2677
+ * **forbidden** to tolerate. A vendor profile exists to quiet known, benign
2678
+ * structural noise — never to hide a deviation that could change a clinical
2679
+ * reading or corrupt the wire.
2680
+ *
2681
+ * **Design: default-deny.** Rather than enumerate the *forbidden* codes (a list you
2682
+ * can forget to extend when a new warning ships — the exact incompleteness a
2683
+ * safety gate must not have), this module enumerates the tiny set of **explicitly
2684
+ * tolerable** codes and treats *every other* code, across all three registries
2685
+ * (record `ASTM_RECORD_*`, frame `ASTM_FRAME_*`, protocol `ASTM_LTP_*`), as
2686
+ * safety-critical. A warning code added in a future phase is therefore
2687
+ * safety-critical **by default** until someone deliberately, reviewably adds it to
2688
+ * the allow-list below. Widening the allow-list can only ever be an explicit,
2689
+ * argued act; forgetting to touch this file can never *weaken* the gate.
2690
+ *
2691
+ * The four tolerable codes are the only deviations that (a) are structural/
2692
+ * syntactic vendor noise and (b) **cannot alter, drop, or fabricate an extracted
2693
+ * value** — because a profile only ever re-badges a *warning*, never re-parses. In
2694
+ * every one, the underlying datum is already surfaced verbatim regardless of the
2695
+ * profile:
2696
+ *
2697
+ * - `ASTM_RECORD_UNKNOWN_TYPE` — an unmodeled record type, surfaced verbatim as an
2698
+ * `unsupported` record carrying no interpreted clinical field.
2699
+ * - `ASTM_NONSTANDARD_DELIMITERS` — the header's own declared delimiters are read
2700
+ * and honored either way; this only notes they differ from the canonical set.
2701
+ * - `ASTM_UNKNOWN_ESCAPE_SEQUENCE` — an unrecognized `&…&` body is **preserved
2702
+ * byte-for-byte** in the decoded value (the escape codec never guesses); the
2703
+ * warning is a purely syntactic advisory, the value is identical with or without
2704
+ * the profile.
2705
+ * - `ASTM_RECORD_UNINTERPRETED_QUERY_STATUS` — a `Q`-record request-information
2706
+ * status surfaced verbatim on a *request* record; the code set is paywalled and
2707
+ * is never interpreted, profile or not.
2708
+ *
2709
+ * Everything a wrong value could hide from — a result value split ambiguity, an
2710
+ * undefined abnormal flag or result status, an unparseable reference range, absent
2711
+ * units, a mis-attached comment, a partial timestamp, a query-vs-result ambiguity,
2712
+ * a bad frame checksum / sequence gap / unterminated / oversize frame, an
2713
+ * ambiguous transport, an unexpected protocol event, or a rejected frame — is
2714
+ * forbidden.
2715
+ */
2716
+
2717
+ /**
2718
+ * The **only** warning codes a profile may list in its `tolerate` set — benign,
2719
+ * structural/syntactic vendor noise that cannot alter, drop, or fabricate an
2720
+ * extracted value. Frozen so it cannot be mutated at runtime to smuggle a code in.
2721
+ * Adding to this set is a deliberate, reviewable act (and each addition must be an
2722
+ * `expected: false` non-safety code by the same reasoning as the four here).
2723
+ *
2724
+ * @example
2725
+ * ```ts
2726
+ * import { TOLERABLE_CODES } from "@cosyte/astm";
2727
+ * TOLERABLE_CODES.has("ASTM_UNKNOWN_ESCAPE_SEQUENCE"); // true
2728
+ * TOLERABLE_CODES.has("ASTM_FRAME_BAD_CHECKSUM"); // false
2729
+ * ```
2730
+ */
2731
+ declare const TOLERABLE_CODES: ReadonlySet<AnyAstmWarningCode>;
2732
+ /**
2733
+ * The set of every real warning code, for O(1) membership checks (used by the
2734
+ * validator to distinguish "unknown code" from "known but forbidden"). Frozen.
2735
+ *
2736
+ * @example
2737
+ * ```ts
2738
+ * import { ALL_ASTM_WARNING_CODES } from "@cosyte/astm";
2739
+ * ALL_ASTM_WARNING_CODES.has("ASTM_LTP_FRAME_REJECTED"); // true
2740
+ * ```
2741
+ */
2742
+ declare const ALL_ASTM_WARNING_CODES: ReadonlySet<AnyAstmWarningCode>;
2743
+ /**
2744
+ * The forbidden set — computed as **every known code minus the tolerable
2745
+ * allow-list**, so it is complete by construction. Frozen. A code appears here iff
2746
+ * it is a real warning code that is not in {@link TOLERABLE_CODES}.
2747
+ *
2748
+ * @example
2749
+ * ```ts
2750
+ * import { SAFETY_CRITICAL_CODES } from "@cosyte/astm";
2751
+ * SAFETY_CRITICAL_CODES.has("ASTM_RECORD_UNDEFINED_RESULT_STATUS"); // true
2752
+ * SAFETY_CRITICAL_CODES.has("ASTM_FRAME_BAD_CHECKSUM"); // true
2753
+ * ```
2754
+ */
2755
+ declare const SAFETY_CRITICAL_CODES: ReadonlySet<AnyAstmWarningCode>;
2756
+ /**
2757
+ * True when `code` is a known warning code that is **not** tolerable — i.e. a
2758
+ * profile may never list it. A code that is not a real warning code at all returns
2759
+ * `false` here (the validator reports "unknown code" separately, a distinct
2760
+ * failure with a distinct message).
2761
+ *
2762
+ * @param code - The warning code to test.
2763
+ * @returns `true` iff the code is a real code outside the tolerable allow-list.
2764
+ * @example
2765
+ * ```ts
2766
+ * import { isSafetyCriticalCode } from "@cosyte/astm";
2767
+ * isSafetyCriticalCode("ASTM_UNKNOWN_ESCAPE_SEQUENCE"); // false (tolerable)
2768
+ * isSafetyCriticalCode("ASTM_RECORD_AMBIGUOUS_VALUE_SPLIT"); // true
2769
+ * ```
2770
+ */
2771
+ declare function isSafetyCriticalCode(code: string): boolean;
2772
+
2773
+ /**
2774
+ * The `default` conservative baseline profile. Tolerates **nothing** and forces no
2775
+ * transport — every deviation surfaces as its own warning, unmodified, and framing
2776
+ * is auto-detected. It exists so "no profile" and "the default profile" are the
2777
+ * same, explicit behaviour, and so a consumer can name the baseline when composing
2778
+ * (`extends: astmProfiles.default`) without importing a special sentinel. Absence of
2779
+ * a profile means this conservative default applied — not that a stream was fully
2780
+ * understood.
2781
+ *
2782
+ * Authored through the public `defineAstmProfile()` API — zero privileged coupling;
2783
+ * it is exactly what a user would write.
2784
+ */
2785
+
2786
+ /**
2787
+ * The conservative baseline profile: no tolerated quirks, no transport override.
2788
+ *
2789
+ * @example
2790
+ * ```ts
2791
+ * import { parseAstmRecords, astmProfiles } from "@cosyte/astm";
2792
+ * const msg = parseAstmRecords(raw, { profile: astmProfiles.default });
2793
+ * // identical to parseAstmRecords(raw) with no profile.
2794
+ * ```
2795
+ */
2796
+ declare const defaultProfile: AstmProfile;
2797
+
2798
+ /**
2799
+ * The `referenceCorpus` profile — a **non-vendor**, evidence-backed tolerance
2800
+ * grounded firsthand in the **redistributable OSS reference corpus**, not an
2801
+ * invented per-vendor deviation matrix.
2802
+ *
2803
+ * Grounding (firsthand-verified 2026-07-21; the corpus is permissively licensed and
2804
+ * in scope for grounding, per the roadmap):
2805
+ *
2806
+ * - **`kxepal/python-astm` `codec.py`** (BSD) — its `decode_record` /
2807
+ * `decode_component` / `decode_repeated_component` split fields, components, and
2808
+ * repeats on the raw delimiters and **never un-escape** ASTM escape sequences: the
2809
+ * source contains zero handling of `&F&`/`&S&`/`&R&`/`&E&`, and the escape
2810
+ * character is treated as ordinary data. `senaite.astm` / `senaite.lis2a` inherit
2811
+ * the same escape-agnostic decode.
2812
+ * - Consequence, and what this profile expects: a stream produced or relayed by a
2813
+ * toolchain built on that escape-agnostic reference stack routinely carries a bare
2814
+ * or non-standard `&…&` body inside a free-text field (units, comments, test
2815
+ * names) that our **escape-aware** tokenizer flags as
2816
+ * `ASTM_UNKNOWN_ESCAPE_SEQUENCE`. We are deliberately stricter than the OSS
2817
+ * references here (roadmap §6 differential note), so the warning is *expected*
2818
+ * noise when talking to that ecosystem — recognizable and non-clinical.
2819
+ *
2820
+ * **Why this is safe.** The escape codec **preserves an unrecognized `&…&` body
2821
+ * byte-for-byte** in the decoded value (`decodeEscapes` never guesses); the value a
2822
+ * consumer reads is identical whether or not this profile is active. The profile
2823
+ * only re-badges the *warning* — no value, flag, status, or identifier is altered
2824
+ * or dropped. And by the safety gate in `defineAstmProfile`, it **cannot** tolerate
2825
+ * a result value / flag / status / range / units, a patient or comment context, a
2826
+ * message-kind ambiguity, or any frame / LTP integrity warning — those always
2827
+ * surface, profile or not.
2828
+ *
2829
+ * **Honesty.** This is a *reference-corpus* profile, not a claim about any named
2830
+ * analyzer vendor. No per-vendor ("cobas does X") behaviour is asserted; a named
2831
+ * vendor profile awaits a firsthand vendor-attributed quirk document and stays
2832
+ * deferred (`REAL-CORPUS`).
2833
+ */
2834
+
2835
+ /**
2836
+ * Tolerates the non-standard-escape noise that streams from the escape-agnostic OSS
2837
+ * reference stack (`python-astm` / `senaite`) carry — the value is preserved
2838
+ * verbatim; only the syntactic warning is expected.
2839
+ *
2840
+ * @example
2841
+ * ```ts
2842
+ * import { parseAstmRecords, astmProfiles, WARNING_CODES } from "@cosyte/astm";
2843
+ * const msg = parseAstmRecords(raw, { profile: astmProfiles.referenceCorpus });
2844
+ * // an unknown-escape deviation arrives as PROFILE_QUIRK_APPLIED (expected), not a bare warning:
2845
+ * const quirks = msg.warnings.filter((w) => w.code === WARNING_CODES.PROFILE_QUIRK_APPLIED);
2846
+ * ```
2847
+ */
2848
+ declare const referenceCorpus: AstmProfile;
2849
+
2850
+ /**
2851
+ * Public barrel for the `@cosyte/astm` vendor-profile subsystem. Assembles the
2852
+ * `astmProfiles` namespace object (the built-ins) and re-exports the public profile
2853
+ * API: `defineAstmProfile`, the registry accessors, the apply helpers, the safety
2854
+ * set, the definition-time error, and the profile types.
2855
+ *
2856
+ * The individual built-ins are reached via `astmProfiles.referenceCorpus` etc., not
2857
+ * as top-level named exports — mirrors the sibling `profiles` namespace convention
2858
+ * ("default" is too generic for a top-level export).
2859
+ */
2860
+
2861
+ /**
2862
+ * Namespace object exposing the built-in profiles: the conservative `default`
2863
+ * baseline plus the evidence-backed `referenceCorpus` (grounded firsthand in the
2864
+ * redistributable OSS reference corpus), each authored via the public
2865
+ * `defineAstmProfile()` API and carrying its cited provenance. Named per-vendor
2866
+ * profiles (cobas / Sysmex / …) are **deferred** pending a firsthand
2867
+ * vendor-attributed quirk document — the engine fully supports them; we do not ship
2868
+ * ungrounded ones.
2869
+ *
2870
+ * @example
2871
+ * ```ts
2872
+ * import { parseAstmRecords, astmProfiles } from "@cosyte/astm";
2873
+ * const msg = parseAstmRecords(raw, { profile: astmProfiles.referenceCorpus });
2874
+ * msg.profile?.name; // "referenceCorpus"
2875
+ * ```
2876
+ */
2877
+ declare const astmProfiles: {
2878
+ readonly default: typeof defaultProfile;
2879
+ readonly referenceCorpus: typeof referenceCorpus;
2880
+ };
2881
+
2882
+ /**
2883
+ * The **consumer-supplied** LIVD catalog: a vendor-test-code → LOINC index built
2884
+ * from an IICC LIVD ("LOINC to Vendor IVD") mapping the consumer provides.
2885
+ *
2886
+ * **Bring-your-own, by design (roadmap §5, Phase 9).** `@cosyte/astm` bundles
2887
+ * **no** LOINC, SNOMED, or LIVD data — LOINC is © Regenstrief (redistributable
2888
+ * only with its attribution notice) and the public CDC LIVD publication is a
2889
+ * SARS-CoV-2-specific file that also carries SNOMED CT (separately licensed), not
2890
+ * a general-analyte, public-domain catalog. So the package stays a **structural
2891
+ * recognizer, not a dictionary**: it recognizes the Universal Test ID's LOINC slot
2892
+ * and surfaces vendor codes verbatim (Phase 1), and this module lets a consumer
2893
+ * *supply* their own LIVD file to map those codes — the terminology data, and its
2894
+ * license obligations, are the consumer's.
2895
+ *
2896
+ * **Grounded firsthand on the IICC LIVD digital format / HL7 LIVD IG.** The
2897
+ * mapping key is the **Vendor Analyte Code** — the vendor *transmission code* the
2898
+ * instrument sends for an automated test, which is exactly the local code an ASTM
2899
+ * analyzer puts in the Universal Test ID (`R`/`O` field, component 4). The mapping
2900
+ * target is the **LOINC Code** (with the LOINC Long Common Name as an optional
2901
+ * human-readable label). Manufacturer/Model are optional provenance a consumer can
2902
+ * carry for their own auditing; this catalog keys on the vendor code alone and,
2903
+ * crucially, **refuses to choose** when one code maps to more than one LOINC.
2904
+ *
2905
+ * **Scope the catalog to the source device fleet.** The ASTM Universal Test ID
2906
+ * carries no manufacturer to disambiguate against, so two different instruments that
2907
+ * reuse the same transmission code for different analytes would both match a single
2908
+ * catalog entry. Supply a catalog built for the analyzers you actually receive from;
2909
+ * conflicting entries *within* one catalog are caught and surfaced as `ambiguous`
2910
+ * (never resolved to a guess), but cross-device code reuse the catalog cannot see is
2911
+ * a catalog-hygiene concern the consumer owns.
2912
+ */
2913
+ /**
2914
+ * One LIVD mapping row a consumer supplies — a vendor test code and the LOINC it
2915
+ * maps to, plus optional human-readable / provenance fields. Modeled on the IICC
2916
+ * LIVD digital format's data elements.
2917
+ *
2918
+ * @example
2919
+ * ```ts
2920
+ * import type { LivdEntry } from "@cosyte/astm";
2921
+ * const e: LivdEntry = { vendorCode: "687", loinc: "1920-8", loincLongName: "AST" };
2922
+ * ```
2923
+ */
2924
+ interface LivdEntry {
2925
+ /**
2926
+ * The **Vendor Analyte Code** — the vendor transmission code the instrument sends (the local code
2927
+ * in an ASTM Universal Test ID, component 4). The mapping key; compared **verbatim** (exact,
2928
+ * case-sensitive) against the reported code — never normalized or fuzzy-matched.
2929
+ */
2930
+ readonly vendorCode: string;
2931
+ /**
2932
+ * The **LOINC Code** this vendor code maps to (e.g. `"1920-8"`). Taken from the consumer's catalog
2933
+ * as-is and **never validated, altered, or invented** — the parser does not ship a LOINC table and
2934
+ * cannot check it; it only carries what the catalog says.
2935
+ */
2936
+ readonly loinc: string;
2937
+ /** The **LOINC Long Common Name**, when the catalog supplies it — an optional human-readable label. */
2938
+ readonly loincLongName?: string;
2939
+ /** The **Vendor Analyte Name** — the vendor's human-readable analyte label, when supplied. */
2940
+ readonly vendorAnalyteName?: string;
2941
+ /** The device **Manufacturer**, when the catalog scopes the mapping to a device (optional provenance). */
2942
+ readonly manufacturer?: string;
2943
+ /** The device **Model**, when the catalog scopes the mapping to a device (optional provenance). */
2944
+ readonly model?: string;
2945
+ }
2946
+ /**
2947
+ * The outcome of looking a vendor code up in a {@link LivdCatalog}. A distinct
2948
+ * value per safe disposition — a hit is `mapped`; a miss is `unmapped`; a code that
2949
+ * matched more than one **distinct** LOINC is `ambiguous` with the candidates
2950
+ * surfaced but **none chosen**. There is deliberately no "guessed" case.
2951
+ */
2952
+ type LivdLookup =
2953
+ /** Exactly one LOINC (one entry, or several entries that all agree on the same LOINC). */
2954
+ {
2955
+ readonly status: "mapped";
2956
+ readonly loinc: string;
2957
+ readonly loincLongName?: string;
2958
+ }
2959
+ /** No entry for this code — a miss. The code stays verbatim; no LOINC is invented. */
2960
+ | {
2961
+ readonly status: "unmapped";
2962
+ }
2963
+ /** More than one distinct LOINC — surfaced for inspection, never resolved to one. */
2964
+ | {
2965
+ readonly status: "ambiguous";
2966
+ readonly candidates: readonly string[];
2967
+ };
2968
+ /**
2969
+ * An immutable, consumer-supplied LIVD catalog. Look a vendor code up with
2970
+ * {@link LivdCatalog.lookup}; the catalog **never** picks between conflicting
2971
+ * LOINCs and **never** mutates. Build one with {@link defineLivdCatalog}.
2972
+ */
2973
+ interface LivdCatalog {
2974
+ /** The number of distinct vendor codes indexed (not the number of input rows). */
2975
+ readonly size: number;
2976
+ /**
2977
+ * Look a vendor code up, verbatim (exact, case-sensitive). Returns `mapped` on a
2978
+ * single-LOINC hit, `unmapped` on a miss, and `ambiguous` when the code carries
2979
+ * more than one distinct LOINC — never a guess.
2980
+ *
2981
+ * @param vendorCode - The reported vendor/local test code.
2982
+ * @returns The lookup outcome.
2983
+ */
2984
+ lookup(vendorCode: string): LivdLookup;
2985
+ }
2986
+ /**
2987
+ * Build a {@link LivdCatalog} from LIVD entries a consumer supplies. Entries are
2988
+ * indexed by their `vendorCode` (verbatim). When several entries share a vendor
2989
+ * code:
2990
+ *
2991
+ * - all agreeing on the same `loinc` → a single `mapped` result (the first entry's
2992
+ * optional `loincLongName` is kept);
2993
+ * - disagreeing (two distinct LOINCs) → an `ambiguous` result carrying every
2994
+ * distinct candidate, and **no** choice between them.
2995
+ *
2996
+ * The returned catalog is deeply frozen; nothing is mutated after construction.
2997
+ *
2998
+ * @param entries - The consumer's LIVD mapping rows.
2999
+ * @returns An immutable catalog.
3000
+ * @example
3001
+ * ```ts
3002
+ * import { defineLivdCatalog } from "@cosyte/astm";
3003
+ * const catalog = defineLivdCatalog([
3004
+ * { vendorCode: "687", loinc: "1920-8", loincLongName: "AST" },
3005
+ * { vendorCode: "690", loinc: "1742-6", loincLongName: "ALT" },
3006
+ * ]);
3007
+ * catalog.lookup("687"); // { status: "mapped", loinc: "1920-8", loincLongName: "AST" }
3008
+ * catalog.lookup("999"); // { status: "unmapped" }
3009
+ * ```
3010
+ */
3011
+ declare function defineLivdCatalog(entries: readonly LivdEntry[]): LivdCatalog;
3012
+
3013
+ /**
3014
+ * The **terminology** warning registry (`ASTM_LIVD_*`) for the Phase-9
3015
+ * LIVD-aware LOINC recognition layer — the fourth of the package's registries,
3016
+ * alongside the record layer's `ASTM_RECORD_*`, the frame codec's `ASTM_FRAME_*`,
3017
+ * and the protocol reducer's `ASTM_LTP_*`.
3018
+ *
3019
+ * A terminology warning is the record of a **safe non-mapping**: a reported
3020
+ * vendor test code that a consumer-supplied LIVD catalog did not map to a single
3021
+ * LOINC. It never carries a LOINC the catalog did not vouch for — the whole point
3022
+ * of the layer is that an unmapped or ambiguous code is surfaced as *unmapped*,
3023
+ * **never a guessed LOINC** (mis-identifying a test is safety-critical). Every
3024
+ * warning carries a stable code plus positional context ({@link AstmPosition}) and
3025
+ * **never** a field value. Consumers compare `warning.code ===
3026
+ * LIVD_WARNING_CODES.<CODE>`; renaming a code is a **breaking change**.
3027
+ *
3028
+ * These codes live in their own registry and are deliberately **not** part of the
3029
+ * profile safety gate's universe (`ALL_ASTM_WARNING_CODES`): a LIVD non-mapping is
3030
+ * a post-parse advisory produced by the opt-in {@link applyLivd} helper, not a
3031
+ * parse-time deviation a vendor profile could ever "tolerate". A profile referencing
3032
+ * one is rejected as an unknown code — correctly, since there is nothing here to
3033
+ * quiet.
3034
+ */
3035
+
3036
+ /**
3037
+ * Stable string codes for every terminology (LIVD) warning. `key === value` so
3038
+ * `Object.values(...)` yields a stable snapshot set.
3039
+ *
3040
+ * @example
3041
+ * ```ts
3042
+ * import { LIVD_WARNING_CODES } from "@cosyte/astm";
3043
+ * LIVD_WARNING_CODES.ASTM_LIVD_UNMAPPED_CODE; // "ASTM_LIVD_UNMAPPED_CODE"
3044
+ * ```
3045
+ */
3046
+ declare const LIVD_WARNING_CODES: {
3047
+ /**
3048
+ * A record carried a vendor/local test code, but the consumer-supplied LIVD catalog held **no**
3049
+ * entry for it. The code stays surfaced verbatim and the mapping is `unmapped` — a LOINC is
3050
+ * **never** guessed. Purely advisory: the raw code and value are untouched.
3051
+ */
3052
+ readonly ASTM_LIVD_UNMAPPED_CODE: "ASTM_LIVD_UNMAPPED_CODE";
3053
+ /**
3054
+ * A vendor/local test code matched **more than one distinct LOINC** in the catalog (e.g. the same
3055
+ * transmission code used by two devices for different analytes). The mapping is `ambiguous` and the
3056
+ * candidate LOINCs are surfaced for inspection, but **none is chosen** — refusing to pick is the
3057
+ * fail-safe, since guessing wrong mis-identifies the test.
3058
+ */
3059
+ readonly ASTM_LIVD_AMBIGUOUS_MAPPING: "ASTM_LIVD_AMBIGUOUS_MAPPING";
3060
+ };
3061
+ /**
3062
+ * Discriminant type for {@link AstmLivdWarning.code}. Narrowing by this code lets
3063
+ * consumers write exhaustive `switch` blocks against {@link LIVD_WARNING_CODES}.
3064
+ */
3065
+ type LivdWarningCode = (typeof LIVD_WARNING_CODES)[keyof typeof LIVD_WARNING_CODES];
3066
+ /**
3067
+ * A single terminology (LIVD) warning: a stable code, a value-free human-readable
3068
+ * message, and positional context. Never carries a test code, a value, or a LOINC.
3069
+ *
3070
+ * @example
3071
+ * ```ts
3072
+ * import type { AstmLivdWarning } from "@cosyte/astm";
3073
+ * const w: AstmLivdWarning = {
3074
+ * code: "ASTM_LIVD_UNMAPPED_CODE",
3075
+ * message: "Reported test code had no LIVD mapping — surfaced unmapped, never a guessed LOINC.",
3076
+ * position: { recordIndex: 3, recordType: "R" },
3077
+ * };
3078
+ * ```
3079
+ */
3080
+ interface AstmLivdWarning {
3081
+ readonly code: LivdWarningCode;
3082
+ /** Human-readable detail for logs. Never contains a test code, a value, or a LOINC. */
3083
+ readonly message: string;
3084
+ readonly position: AstmPosition;
3085
+ }
3086
+ /**
3087
+ * Build an `ASTM_LIVD_UNMAPPED_CODE` warning. The reported code stays verbatim and
3088
+ * the mapping is `unmapped`; no LOINC is fabricated.
3089
+ *
3090
+ * @param position - Where the unmapped code was seen (record ordinal + type; never the code).
3091
+ * @returns The warning.
3092
+ * @example
3093
+ * ```ts
3094
+ * import { livdUnmappedCode } from "@cosyte/astm";
3095
+ * livdUnmappedCode({ recordIndex: 3, recordType: "R" });
3096
+ * ```
3097
+ */
3098
+ declare function livdUnmappedCode(position: AstmPosition): AstmLivdWarning;
3099
+ /**
3100
+ * Build an `ASTM_LIVD_AMBIGUOUS_MAPPING` warning. The code matched multiple distinct
3101
+ * LOINCs; none is chosen (refusing to pick is the fail-safe).
3102
+ *
3103
+ * @param position - Where the ambiguous code was seen (record ordinal + type; never the code).
3104
+ * @returns The warning.
3105
+ * @example
3106
+ * ```ts
3107
+ * import { livdAmbiguousMapping } from "@cosyte/astm";
3108
+ * livdAmbiguousMapping({ recordIndex: 4, recordType: "O" });
3109
+ * ```
3110
+ */
3111
+ declare function livdAmbiguousMapping(position: AstmPosition): AstmLivdWarning;
3112
+
3113
+ /**
3114
+ * The **additive, advisory** LIVD annotation layer (Phase 9). {@link applyLivd}
3115
+ * reads a parsed {@link AstmMessage} and a consumer-supplied {@link LivdCatalog}
3116
+ * and returns a **separate** layer of per-record LOINC annotations — it **never
3117
+ * mutates, alters, or drops** the raw reported code or value. Recognition is an
3118
+ * annotation, not a rewrite: the wire stays exactly as parsed.
3119
+ *
3120
+ * **The never-fabricate rule (the safety-critical point).** A vendor code with no
3121
+ * single LIVD mapping surfaces as a typed `unmapped`/`ambiguous`/`no-code`
3122
+ * annotation — **never** a guessed LOINC. A wrong LOINC mis-identifies a test, so
3123
+ * the layer only ever reports a LOINC the wire itself carried (an inline LOINC
3124
+ * candidate) or one the consumer's catalog vouched for (labeled `derived`), and
3125
+ * otherwise reports nothing but the miss.
3126
+ */
3127
+
3128
+ /**
3129
+ * The outcome of annotating one record's Universal Test ID against a LIVD catalog.
3130
+ * A distinct case per disposition; there is no case in which a LOINC is guessed.
3131
+ *
3132
+ * - `mapped` — the vendor/local code resolved to a single LOINC **via the catalog**
3133
+ * (labeled `derived: true`, `source: "livd"`).
3134
+ * - `inline-loinc` — the wire itself carried a LOINC in the Universal Test ID's slot
3135
+ * (component 1); surfaced `source: "wire"`, **not** derived and **not** validated.
3136
+ * - `unmapped` — a vendor/local code with no catalog entry.
3137
+ * - `ambiguous` — a vendor/local code matching more than one distinct LOINC; the
3138
+ * candidates are surfaced but **none is chosen**.
3139
+ * - `no-code` — the record carried no usable test code at all (name-only/empty), so
3140
+ * there was nothing to map.
3141
+ */
3142
+ type LivdMapping = {
3143
+ readonly status: "mapped";
3144
+ readonly loinc: string;
3145
+ readonly loincLongName?: string;
3146
+ readonly source: "livd";
3147
+ readonly derived: true;
3148
+ } | {
3149
+ readonly status: "inline-loinc";
3150
+ readonly loinc: string;
3151
+ readonly source: "wire";
3152
+ } | {
3153
+ readonly status: "unmapped";
3154
+ } | {
3155
+ readonly status: "ambiguous";
3156
+ readonly candidates: readonly string[];
3157
+ } | {
3158
+ readonly status: "no-code";
3159
+ };
3160
+ /**
3161
+ * One record's LIVD annotation — which record, the code that was looked up
3162
+ * (verbatim), how it was recognized, and the mapping outcome. Additive: it points
3163
+ * at the record by index and never replaces it.
3164
+ *
3165
+ * @example
3166
+ * ```ts
3167
+ * import type { LivdAnnotation } from "@cosyte/astm";
3168
+ * const a: LivdAnnotation = {
3169
+ * recordIndex: 3,
3170
+ * recordType: "R",
3171
+ * reportedCode: "687",
3172
+ * provenance: "local-code",
3173
+ * mapping: { status: "mapped", loinc: "1920-8", source: "livd", derived: true },
3174
+ * };
3175
+ * ```
3176
+ */
3177
+ interface LivdAnnotation {
3178
+ /** The `recordIndex` of the annotated `R`/`O` record. */
3179
+ readonly recordIndex: number;
3180
+ /** The annotated record's type. */
3181
+ readonly recordType: "R" | "O";
3182
+ /** The reported primary code that was looked up, verbatim; absent when the record carried no code. */
3183
+ readonly reportedCode?: string;
3184
+ /** How the reported code was recognized in the Universal Test ID (provenance only, never a lookup). */
3185
+ readonly provenance: UniversalTestIdProvenance;
3186
+ /** The mapping outcome — never a guessed LOINC. */
3187
+ readonly mapping: LivdMapping;
3188
+ }
3189
+ /**
3190
+ * The result of {@link applyLivd}: the per-record annotations (one per `R`/`O`
3191
+ * record) and the value-free warnings for every unmapped or ambiguous code. Both
3192
+ * arrays are deeply frozen; the source message is untouched.
3193
+ */
3194
+ interface LivdResult {
3195
+ /** One annotation per `R`/`O` record, in wire order. */
3196
+ readonly annotations: readonly LivdAnnotation[];
3197
+ /** A value-free warning per `unmapped`/`ambiguous` code — never per `mapped`/`inline-loinc`/`no-code`. */
3198
+ readonly warnings: readonly AstmLivdWarning[];
3199
+ }
3200
+ /**
3201
+ * Annotate one `R` or `O` record against a LIVD catalog. The building block of
3202
+ * {@link applyLivd}; useful when a consumer holds a single record. Never mutates
3203
+ * the record and never fabricates a LOINC.
3204
+ *
3205
+ * @param record - The result/order record to annotate.
3206
+ * @param catalog - The consumer-supplied LIVD catalog.
3207
+ * @returns The record's annotation.
3208
+ * @example
3209
+ * ```ts
3210
+ * import { parseAstmRecords, results, defineLivdCatalog, lookupLivdForRecord } from "@cosyte/astm";
3211
+ * const msg = parseAstmRecords("H|\\^&\rR|1|^^^687|28.6|U/L||N||F\rL|1\r");
3212
+ * const catalog = defineLivdCatalog([{ vendorCode: "687", loinc: "1920-8" }]);
3213
+ * lookupLivdForRecord(results(msg)[0]!, catalog).mapping.status; // "mapped"
3214
+ * ```
3215
+ */
3216
+ declare function lookupLivdForRecord(record: ResultRecord | OrderRecord, catalog: LivdCatalog): LivdAnnotation;
3217
+ /**
3218
+ * Apply a consumer-supplied LIVD catalog to a parsed message, producing an
3219
+ * **additive, advisory** layer of LOINC annotations over its `R` (result) and `O`
3220
+ * (order) records. The source message is never mutated; the raw reported codes and
3221
+ * values stay exactly as parsed.
3222
+ *
3223
+ * Fail-safe: with no matching entry a code is `unmapped` (+ an
3224
+ * `ASTM_LIVD_UNMAPPED_CODE` warning); with a conflicting mapping it is `ambiguous`
3225
+ * (+ an `ASTM_LIVD_AMBIGUOUS_MAPPING` warning); a LOINC is **never** guessed. When
3226
+ * the wire already carried an inline LOINC candidate, it is surfaced from the wire
3227
+ * (never overwritten by the catalog).
3228
+ *
3229
+ * @param msg - A parsed ASTM message.
3230
+ * @param catalog - The consumer-supplied LIVD catalog (build with {@link defineLivdCatalog}).
3231
+ * @returns The annotations and value-free warnings (deeply frozen).
3232
+ * @example
3233
+ * ```ts
3234
+ * import { parseAstmRecords, defineLivdCatalog, applyLivd } from "@cosyte/astm";
3235
+ * const msg = parseAstmRecords("H|\\^&\rR|1|^^^687|28.6|U/L||N||F\rL|1\r");
3236
+ * const catalog = defineLivdCatalog([{ vendorCode: "687", loinc: "1920-8", loincLongName: "AST" }]);
3237
+ * const { annotations } = applyLivd(msg, catalog);
3238
+ * annotations[0]?.mapping; // { status: "mapped", loinc: "1920-8", loincLongName: "AST", source: "livd", derived: true }
3239
+ * ```
3240
+ */
3241
+ declare function applyLivd(msg: AstmMessage, catalog: LivdCatalog): LivdResult;
3242
+
3243
+ /**
3244
+ * The frame-codec value model produced by {@link decodeAstmFrames}.
3245
+ *
3246
+ * The codec decodes a framed byte stream into a list of {@link AstmFrame}s (every
3247
+ * frame surfaced, trusted or not) plus the **reassembled record bytes** of the
3248
+ * frames that were trusted and contiguous. Nothing is invented: a frame whose
3249
+ * checksum fails, or that arrives out of sequence, or that never terminates, is
3250
+ * surfaced and flagged but is **never merged** into the clean `records` output.
3251
+ */
3252
+
3253
+ /** Which terminator closed a frame: `ETB` (record continues) or `ETX` (record complete). */
3254
+ type FrameTerminator = "ETB" | "ETX";
3255
+ /**
3256
+ * A frame's checksum verdict: the modulo-256 value recomputed from the frame's
3257
+ * bytes, the value declared on the wire (or `undefined` when it was unreadable),
3258
+ * and whether they matched.
3259
+ */
3260
+ interface FrameChecksum {
3261
+ /** The modulo-256 checksum recomputed over the frame (frame number through terminator). */
3262
+ readonly computed: number;
3263
+ /**
3264
+ * The checksum declared on the wire (the two hex chars after the terminator), read
3265
+ * case-insensitively; `undefined` when those bytes were missing or not hex.
3266
+ */
3267
+ readonly declared?: number;
3268
+ /** `true` only when a declared checksum was present **and** equal to `computed`. */
3269
+ readonly valid: boolean;
3270
+ }
3271
+ /**
3272
+ * One decoded ASTM frame. `text` is the frame's record-byte payload (the bytes
3273
+ * between the frame number and the terminator, escapes untouched — this is the
3274
+ * framing layer, not the record layer). `trusted` is the single flag a consumer
3275
+ * gates on: it is `true` only for a fully-terminated frame whose checksum
3276
+ * validated, and such frames are the only ones reassembled into `records`.
3277
+ */
3278
+ interface AstmFrame {
3279
+ /**
3280
+ * The frame's sequence number as read from the `FN` byte. Normally `0`–`7`; a
3281
+ * value outside that range means the `FN` byte was not a `0`–`7` digit (a
3282
+ * corruption that also trips a sequence-gap warning). `undefined` when the frame
3283
+ * was too truncated to carry a frame number at all.
3284
+ */
3285
+ readonly frameNumber?: number;
3286
+ /** `ETB` (intermediate) or `ETX` (final); `undefined` for an unterminated frame. */
3287
+ readonly terminator?: FrameTerminator;
3288
+ /** The frame's record-byte payload (may be empty). A **copy**, safe for the caller to retain. */
3289
+ readonly text: Uint8Array;
3290
+ /** Byte offset of the `STX` that opened this frame, within the decoded stream. */
3291
+ readonly byteOffset: number;
3292
+ /** The checksum verdict. */
3293
+ readonly checksum: FrameChecksum;
3294
+ /**
3295
+ * `true` only when the frame was fully terminated **and** its checksum validated.
3296
+ * Only trusted frames are reassembled into {@link DecodeAstmFramesResult.records};
3297
+ * an untrusted frame is surfaced here but never merged.
3298
+ */
3299
+ readonly trusted: boolean;
3300
+ /** `true` when no valid terminator + checksum was found for this frame (a truncated/partial frame). */
3301
+ readonly unterminated: boolean;
3302
+ /** `true` when the frame's record text exceeded the 240-byte limit. */
3303
+ readonly oversize: boolean;
3304
+ }
3305
+ /**
3306
+ * The result of {@link decodeAstmFrames}: the reassembled record bytes, every
3307
+ * decoded frame, and the accumulated frame warnings.
3308
+ */
3309
+ interface DecodeAstmFramesResult {
3310
+ /**
3311
+ * The reassembled record byte-strings — one entry per complete record (a run of
3312
+ * frames closed by an `ETX`), in wire order. **Only** clean reassemblies appear:
3313
+ * a record whose frames included a bad checksum, a sequence gap, or an
3314
+ * unterminated frame is **omitted** (its frames are still in {@link DecodeAstmFramesResult.frames},
3315
+ * flagged). Each entry is ready to hand to `parseAstmRecords`.
3316
+ */
3317
+ readonly records: readonly Uint8Array[];
3318
+ /** Every decoded frame, trusted or not, in wire order. */
3319
+ readonly frames: readonly AstmFrame[];
3320
+ /** The frame warnings accumulated during a lenient decode (empty in a clean decode). */
3321
+ readonly warnings: readonly AstmFrameWarning[];
3322
+ }
3323
+ /**
3324
+ * Options for {@link decodeAstmFrames}. Lenient by default (Postel's Law).
3325
+ */
3326
+ interface FrameOptions {
3327
+ /**
3328
+ * When `true`, escalate any tolerated frame deviation (bad checksum, sequence
3329
+ * gap, unterminated, oversize) to a thrown {@link AstmFrameStrictError} instead
3330
+ * of accumulating a warning. Off by default.
3331
+ */
3332
+ readonly strict?: boolean;
3333
+ }
3334
+
3335
+ /**
3336
+ * The value model for the pure LTP protocol reducer ({@link ltpReduce}).
3337
+ *
3338
+ * The reducer models the **receiver** side of an ASTM/CLSI-LIS01 (LIS01-A2)
3339
+ * session — the host/LIS receiving an upload — because that is where the one
3340
+ * safety decision lives: whether to `ACK` (accept) or `NAK` (reject) a frame that
3341
+ * just arrived. It is a **pure function** of the current {@link LtpState} and one
3342
+ * inbound {@link LtpEvent}, returning an {@link LtpTransition} (the next state plus
3343
+ * the actions the consumer should take and any warnings). No sockets, no timers,
3344
+ * no I/O — the consumer owns the wire and the clock; the reducer owns the logic.
3345
+ */
3346
+
3347
+ /**
3348
+ * The protocol phase. The line is either **neutral** (idle — awaiting an `ENQ` to
3349
+ * establish, or reset there after an `EOT`) or in **transfer** (establishment
3350
+ * accepted; frames and their per-frame `ACK`/`NAK` flow until the sender's `EOT`).
3351
+ * The classic three-phase LIS01 model (establishment → transfer → termination)
3352
+ * collapses to these two, since establishment is the `neutral → transfer` edge and
3353
+ * termination is the `transfer → neutral` edge.
3354
+ */
3355
+ type LtpPhase = "neutral" | "transfer";
3356
+ /**
3357
+ * The reducer's immutable session state. Every {@link ltpReduce} call returns a new,
3358
+ * frozen `LtpState`; the previous one is never mutated.
3359
+ */
3360
+ interface LtpState {
3361
+ /** The current protocol phase. */
3362
+ readonly phase: LtpPhase;
3363
+ /**
3364
+ * The next frame number the receiver expects (`1 → … → 7 → 0 → …`), meaningful in
3365
+ * `transfer`. A trusted frame carrying this number is accepted and appended; any
3366
+ * other number is a duplicate retransmit (idempotent re-`ACK`) or an out-of-sequence
3367
+ * frame (rejected with `NAK`), never silently bridged.
3368
+ */
3369
+ readonly expectedFrame: number;
3370
+ /**
3371
+ * The frame number of the last frame accepted into the current record, used to
3372
+ * recognise a duplicate retransmit; `undefined` before the first frame of a
3373
+ * transfer is accepted.
3374
+ */
3375
+ readonly lastAcceptedFrame?: number;
3376
+ /**
3377
+ * The reassembled bytes of every **complete** record delivered so far in the
3378
+ * session (each closed by an `ETX` frame with a clean run). Concatenate these and
3379
+ * hand them to `parseAstmRecords` to get the message. Only trusted, in-sequence
3380
+ * frames contribute; a rejected or partial record never appears.
3381
+ */
3382
+ readonly records: readonly Uint8Array[];
3383
+ /** `true` when a record is mid-reassembly (an `ETB` frame was accepted, awaiting its `ETX`). */
3384
+ readonly recordOpen: boolean;
3385
+ /**
3386
+ * The bytes accumulated so far for the in-progress record — the concatenation of
3387
+ * the `ETB` frames accepted since the last `ETX`. Empty when no record is open.
3388
+ * These bytes are **never** delivered on their own: only an `ETX` completes a
3389
+ * record, at which point they (plus the final frame's text) become one entry in
3390
+ * {@link LtpState.records}. An `EOT` or an `ENQ` restart discards them unread.
3391
+ */
3392
+ readonly openRecord: Uint8Array;
3393
+ }
3394
+ /**
3395
+ * An inbound protocol event the consumer feeds the reducer: one of the four LTP
3396
+ * control signals (`ENQ`/`ACK`/`NAK`/`EOT`) read off the wire, or a `frame` the
3397
+ * consumer already decoded with `decodeAstmFrames` (the reducer reuses the codec's
3398
+ * `trusted`/`checksum` verdict — it never re-derives it).
3399
+ */
3400
+ type LtpEvent = {
3401
+ readonly type: "enq";
3402
+ } | {
3403
+ readonly type: "ack";
3404
+ } | {
3405
+ readonly type: "nak";
3406
+ } | {
3407
+ readonly type: "eot";
3408
+ } | {
3409
+ readonly type: "frame";
3410
+ readonly frame: AstmFrame;
3411
+ };
3412
+ /**
3413
+ * An action the reducer tells the consumer to take. The consumer performs the I/O
3414
+ * (write the byte to the socket, hand the record to the parser); the reducer only
3415
+ * decides. `sendAck`/`sendNak`/`sendEot` are single control bytes; `deliverRecord`
3416
+ * carries the freshly-completed record's reassembled bytes.
3417
+ */
3418
+ type LtpAction = {
3419
+ readonly type: "sendAck";
3420
+ } | {
3421
+ readonly type: "sendNak";
3422
+ } | {
3423
+ readonly type: "sendEot";
3424
+ } | {
3425
+ readonly type: "deliverRecord";
3426
+ readonly record: Uint8Array;
3427
+ };
3428
+ /**
3429
+ * The result of one {@link ltpReduce} step: the next {@link LtpState}, the
3430
+ * {@link LtpAction}s to perform (in order), and any {@link AstmLtpWarning}s raised.
3431
+ */
3432
+ interface LtpTransition {
3433
+ /** The next session state (frozen). */
3434
+ readonly state: LtpState;
3435
+ /** The actions the consumer should take, in order. */
3436
+ readonly actions: readonly LtpAction[];
3437
+ /** Warnings raised by this step (empty on a clean, expected event). */
3438
+ readonly warnings: readonly AstmLtpWarning[];
3439
+ }
3440
+
3441
+ /**
3442
+ * The pure LTP protocol reducer: {@link ltpReduce} and {@link ltpInitialState}.
3443
+ *
3444
+ * A deterministic, socket-free receiver-side state machine for ASTM/CLSI-LIS01
3445
+ * (LIS01-A2). The consumer owns the wire — it reads bytes, decodes frames with
3446
+ * `decodeAstmFrames`, and drives this reducer one {@link LtpEvent} at a time — and
3447
+ * the reducer decides, purely, what happens next: whether to `ACK` or `NAK`, when a
3448
+ * record is complete, and when the session returns to neutral.
3449
+ *
3450
+ * **The one inviolable rule (borrowed from `mllp`'s ACK-failsafe discipline):** a
3451
+ * frame the codec did not vouch for — a bad checksum, an unterminated frame, or one
3452
+ * out of sequence — is answered with `NAK`, **never** a fabricated positive `ACK`,
3453
+ * and its bytes are **never** appended to a record or delivered. A `NAK` drives
3454
+ * retransmit, not acceptance. This is the protocol-level twin of the frame codec's
3455
+ * "a bad-checksum frame is never merged into a record."
3456
+ */
3457
+
3458
+ /**
3459
+ * The neutral starting state for a fresh session: line idle, no record in flight,
3460
+ * nothing delivered yet. A consumer creates one of these when a connection opens
3461
+ * and folds inbound events through {@link ltpReduce} from here.
3462
+ *
3463
+ * @returns A frozen neutral {@link LtpState}.
3464
+ * @example
3465
+ * ```ts
3466
+ * import { ltpInitialState, ltpReduce } from "@cosyte/astm";
3467
+ * let state = ltpInitialState();
3468
+ * const step = ltpReduce(state, { type: "enq" });
3469
+ * step.actions; // [{ type: "sendAck" }]
3470
+ * state = step.state;
3471
+ * ```
3472
+ */
3473
+ declare function ltpInitialState(): LtpState;
3474
+ /**
3475
+ * Advance the LTP session by one inbound event — a pure function, no I/O.
3476
+ *
3477
+ * Behaviour by phase:
3478
+ * - **neutral** — `enq` accepts establishment (`sendAck`, enter transfer); `eot` is
3479
+ * a benign line reset (no-op); `ack`/`nak` are unexpected at a receiver (surfaced,
3480
+ * never read as acceptance); a `frame` before establishment is tolerated
3481
+ * (Postel's Law) — the session auto-establishes and processes it, with a warning.
3482
+ * - **transfer** — a `frame` is accepted (`sendAck`, appended, sequence advanced) only
3483
+ * when it is trusted *and* carries the expected number; a duplicate of the last
3484
+ * accepted frame is idempotently re-`ACK`ed without re-appending; a bad or
3485
+ * out-of-sequence frame is `NAK`ed and dropped. `eot` terminates the transfer and
3486
+ * returns to neutral (a record left open on an `ETB` is **not** delivered). `enq`
3487
+ * restarts establishment. `ack`/`nak` are unexpected.
3488
+ *
3489
+ * @param state - The current session state.
3490
+ * @param event - The inbound event.
3491
+ * @returns The next state, the actions to take, and any warnings.
3492
+ * @example
3493
+ * ```ts
3494
+ * import { ltpInitialState, ltpReduce } from "@cosyte/astm";
3495
+ * // A bad-checksum frame is NAKed, never ACKed, and never appended.
3496
+ * const est = ltpReduce(ltpInitialState(), { type: "enq" }).state;
3497
+ * const bad = { trusted: false } as never;
3498
+ * ltpReduce(est, { type: "frame", frame: bad }).actions; // [{ type: "sendNak" }]
3499
+ * ```
3500
+ */
3501
+ declare function ltpReduce(state: LtpState, event: LtpEvent): LtpTransition;
3502
+
3503
+ /**
3504
+ * The four LTP (low-level transfer protocol) control bytes for ASTM/CLSI-LIS01
3505
+ * (was E1381), plus the bare-record-letter set the transport detector keys on.
3506
+ *
3507
+ * The frame codec (`../frames`) owns the *inside* of a frame (`STX FN text
3508
+ * ETB/ETX CS CRLF`); this module owns the *handshake around* frames — the
3509
+ * establishment/transfer/termination signalling that the {@link ltpReduce}
3510
+ * reducer consumes. These are the raw byte values; the reducer and the transport
3511
+ * detector are the only consumers.
3512
+ */
3513
+ /** Enquiry (`0x05`) — the sender's request to establish a transfer. */
3514
+ declare const ENQ = 5;
3515
+ /** Acknowledge (`0x06`) — the receiver accepted the last establishment or frame. */
3516
+ declare const ACK = 6;
3517
+ /** Negative acknowledge (`0x15`) — the receiver rejected the last frame; retransmit, do not accept. */
3518
+ declare const NAK = 21;
3519
+ /** End of transmission (`0x04`) — the sender terminated the transfer; the line returns to neutral. */
3520
+ declare const EOT = 4;
3521
+
3522
+ /**
3523
+ * The frame-layer entry point: {@link decodeAstmFrames}.
3524
+ *
3525
+ * Decode a framed ASTM/CLSI-LIS01 byte stream (`<STX> FN text <ETB|ETX> CS <CR>
3526
+ * <LF>` frames) into a list of frames plus the reassembled record bytes of the
3527
+ * frames that were trusted and contiguous. Lenient by default: byte-level
3528
+ * deviations become {@link AstmFrameWarning}s, and the only fatal is the shared
3529
+ * `EMPTY_INPUT` (an empty stream). The two safety-critical rules are absolute in
3530
+ * **both** modes: a bad-checksum frame is surfaced flagged untrusted and **never**
3531
+ * merged into a record, and a sequence gap is **never** silently bridged.
3532
+ *
3533
+ * This is the framing layer only — it does not parse record grammar. Hand a
3534
+ * reassembled record to `parseAstmRecords`, or use `parseFramedAstm` to compose
3535
+ * both layers at the edge.
3536
+ */
3537
+
3538
+ /**
3539
+ * Decode a framed ASTM byte stream into frames + reassembled record bytes.
3540
+ *
3541
+ * Bytes outside a frame (before the first `STX`, or between a frame's trailing
3542
+ * `LF` and the next `STX`) are skipped: in ASTM they are low-level transfer
3543
+ * control (`ENQ`/`ACK`/`NAK`/`EOT` — the Phase 6 protocol layer), never record
3544
+ * content, so skipping them is not data loss.
3545
+ *
3546
+ * @param bytes - The framed byte stream.
3547
+ * @param options - Decode options; lenient unless `strict` is set.
3548
+ * @returns The decoded frames, the reassembled trusted record bytes, and any warnings.
3549
+ * @throws {@link AstmParseError} with `EMPTY_INPUT` when `bytes` is empty (both modes).
3550
+ * @throws {@link AstmFrameStrictError} when `strict` is set and any deviation occurred.
3551
+ * @example
3552
+ * ```ts
3553
+ * import { decodeAstmFrames } from "@cosyte/astm";
3554
+ * // One final frame carrying the record text "R|1|" — checksum "AF" over FN..ETX (mod 256).
3555
+ * const bytes = new Uint8Array([0x02, 0x31, 0x52, 0x7c, 0x31, 0x7c, 0x03, 0x41, 0x46, 0x0d, 0x0a]);
3556
+ * const { records, frames } = decodeAstmFrames(bytes);
3557
+ * frames[0]?.frameNumber; // 1
3558
+ * records.length; // 1
3559
+ * ```
3560
+ */
3561
+ declare function decodeAstmFrames(bytes: Uint8Array, options?: FrameOptions): DecodeAstmFramesResult;
3562
+
3563
+ /**
3564
+ * The frame-layer **emit** side: {@link composeAstmFrames} — Phase 7.
3565
+ *
3566
+ * The exact inverse of {@link decodeAstmFrames}. Given the reassembled record
3567
+ * byte-strings (one entry per complete record — the same shape `decodeAstmFrames`
3568
+ * *produces*), it wraps each into one or more `<STX> FN text <ETB|ETX> CS <CR><LF>`
3569
+ * frames, so `decodeAstmFrames(composeAstmFrames(records)).records` reproduces the
3570
+ * input records exactly.
3571
+ *
3572
+ * Everything structural is **computed, never accepted-as-given**:
3573
+ * - the **modulo-256 checksum** over `FN … terminator` (inclusive), emitted as two
3574
+ * **uppercase** hex chars (the conservative-emit form the decoder accepts either
3575
+ * case of);
3576
+ * - the **frame number** `0`–`7`, starting at `1` and rolling over `7 → 0 → 1`,
3577
+ * continuous across every frame in the stream (as the decoder's sequence check
3578
+ * expects);
3579
+ * - the **240-byte text split** — a record longer than 240 bytes is split across
3580
+ * frames closed by `ETB` (intermediate) with a final `ETX`; the seven framing
3581
+ * control bytes are never counted toward the 240.
3582
+ *
3583
+ * A record must carry bytes: an empty record (or an empty record list) is a typed
3584
+ * {@link AstmFrameEncodeError}, never an empty frame.
3585
+ */
3586
+ /**
3587
+ * Thrown by {@link composeAstmFrames} when the input cannot be framed into a
3588
+ * spec-clean stream — an empty record list, or a record with no bytes (a frame
3589
+ * must carry a record; an empty one is a structural error, never an empty frame).
3590
+ * Carries a stable code + the offending record index, never record bytes.
3591
+ *
3592
+ * @example
3593
+ * ```ts
3594
+ * import { composeAstmFrames, AstmFrameEncodeError } from "@cosyte/astm";
3595
+ * try {
3596
+ * composeAstmFrames([]);
3597
+ * } catch (err) {
3598
+ * if (err instanceof AstmFrameEncodeError) err.code; // "ASTM_FRAME_EMPTY_RECORD"
3599
+ * }
3600
+ * ```
3601
+ */
3602
+ declare class AstmFrameEncodeError extends Error {
3603
+ /** Stable discriminant. */
3604
+ readonly code: "ASTM_FRAME_EMPTY_RECORD";
3605
+ /** Index of the offending record within the input, when applicable. */
3606
+ readonly recordIndex?: number;
3607
+ /** @internal */
3608
+ constructor(message: string, recordIndex?: number);
3609
+ }
3610
+ /** Options for {@link composeAstmFrames}. */
3611
+ interface ComposeFramesOptions {
3612
+ /**
3613
+ * The frame number to start the sequence at (`0`–`7`). Defaults to
3614
+ * {@link FIRST_FRAME_NUMBER} (`1`) — the ASTM convention.
3615
+ */
3616
+ readonly startFrameNumber?: number;
3617
+ }
3618
+ /**
3619
+ * Frame reassembled record bytes into a spec-clean ASTM/CLSI-LIS01 byte stream —
3620
+ * the inverse of {@link decodeAstmFrames}.
3621
+ *
3622
+ * Each record is split at 240 text bytes into `ETB`-closed intermediate frames
3623
+ * and a final `ETX` frame; frame numbers run `1, 2, … 7, 0, 1 …` continuously
3624
+ * across the whole stream; every frame's modulo-256 checksum is computed and
3625
+ * emitted uppercase.
3626
+ *
3627
+ * @param records - The reassembled record byte-strings (`Uint8Array` or latin1
3628
+ * `string`), one entry per complete record.
3629
+ * @param options - Encode options.
3630
+ * @returns The framed byte stream.
3631
+ * @throws {@link AstmFrameEncodeError} when the list is empty or a record has no bytes.
3632
+ * @example
3633
+ * ```ts
3634
+ * import { composeAstmFrames, decodeAstmFrames } from "@cosyte/astm";
3635
+ * const records = [new TextEncoder().encode("H|\\^&\r"), new TextEncoder().encode("L|1\r")];
3636
+ * const bytes = composeAstmFrames(records);
3637
+ * decodeAstmFrames(bytes).records.length; // 2
3638
+ * ```
3639
+ */
3640
+ declare function composeAstmFrames(records: readonly (Uint8Array | string)[], options?: ComposeFramesOptions): Uint8Array;
3641
+
3642
+ /**
3643
+ * Composing the two layers at the edge: {@link parseFramedAstm}.
3644
+ *
3645
+ * The framing layer ({@link decodeAstmFrames}) and the record layer
3646
+ * (`parseAstmRecords`) share nothing but the payload boundary — a frame carries
3647
+ * record bytes; a record knows nothing about frames. This helper joins them at the
3648
+ * one point they meet: decode a framed stream into trusted, reassembled record
3649
+ * bytes, then parse those bytes into an {@link AstmMessage}. It is deliberately
3650
+ * thin — a consumer that already holds de-framed record bytes calls
3651
+ * `parseAstmRecords` directly, and a consumer that only wants frames calls
3652
+ * `decodeAstmFrames` directly.
3653
+ */
3654
+
3655
+ /**
3656
+ * The result of {@link parseFramedAstm}: the parsed message, plus the frame-layer
3657
+ * detail (every decoded frame and the frame warnings) so a consumer keeps full
3658
+ * visibility into the transport below the records.
3659
+ */
3660
+ interface FramedAstmResult {
3661
+ /** The message parsed from the trusted, reassembled record bytes. */
3662
+ readonly message: AstmMessage;
3663
+ /** Every decoded frame, trusted or not, in wire order. */
3664
+ readonly frames: readonly AstmFrame[];
3665
+ /** The frame-layer warnings (bad checksum, sequence gap, unterminated, oversize). */
3666
+ readonly frameWarnings: readonly AstmFrameWarning[];
3667
+ }
3668
+ /**
3669
+ * Decode a framed ASTM byte stream and parse its reassembled records in one call.
3670
+ *
3671
+ * Only **trusted, contiguous** frames are reassembled (a bad-checksum frame, a
3672
+ * sequence gap, or an unterminated frame is surfaced in `frames`/`frameWarnings`
3673
+ * but never fed to the record parser), so the parsed `message` reflects only bytes
3674
+ * the framing layer vouched for.
3675
+ *
3676
+ * @param bytes - The framed byte stream.
3677
+ * @param options - Frame decode options **and** record parse options (both layers
3678
+ * honor a shared `strict`).
3679
+ * @returns The parsed message plus the frame-layer detail.
3680
+ * @throws {@link AstmParseError} `EMPTY_INPUT` when the stream is empty, or when no
3681
+ * trusted records could be reassembled (nothing to parse).
3682
+ * @throws {@link AstmFrameStrictError} / {@link AstmStrictError} in `strict` mode on
3683
+ * a frame or record deviation, respectively.
3684
+ * @example
3685
+ * ```ts
3686
+ * import { parseFramedAstm } from "@cosyte/astm";
3687
+ * // A single final frame carrying "H|\^&\r" — checksum "E5" over FN..ETX (mod 256).
3688
+ * const bytes = new Uint8Array([
3689
+ * 0x02, 0x31, 0x48, 0x7c, 0x5c, 0x5e, 0x26, 0x0d, 0x03, 0x45, 0x35, 0x0d, 0x0a,
3690
+ * ]);
3691
+ * const { message, frames } = parseFramedAstm(bytes);
3692
+ * message.header.delimiters.field; // "|"
3693
+ * frames.length; // 1
3694
+ * ```
3695
+ */
3696
+ declare function parseFramedAstm(bytes: Uint8Array, options?: FrameOptions & AstmParseOptions): FramedAstmResult;
3697
+ /**
3698
+ * Serialize an ASTM message (or a bare record list) and frame it into a spec-clean
3699
+ * byte stream in one call — the inverse of {@link parseFramedAstm}, composing the
3700
+ * two emit layers at the edge.
3701
+ *
3702
+ * Each record is serialized to spec-clean, `CR`-terminated wire text (canonical
3703
+ * delimiters, embedded delimiters re-escaped) and then framed **independently**
3704
+ * (one record per `ETX`-closed frame run), so the framing exactly mirrors what
3705
+ * {@link decodeAstmFrames} reassembles: `parseFramedAstm(serializeFramedAstm(msg))`
3706
+ * yields an equal message.
3707
+ *
3708
+ * @param input - A parsed {@link AstmMessage} or a list of {@link AstmRecord}s.
3709
+ * @param options - Frame-encode options.
3710
+ * @returns The framed byte stream.
3711
+ * @throws {@link AstmSerializeError} when a value contains an unencodable `CR`/`LF`.
3712
+ * @throws {@link AstmFrameEncodeError} when there are no records to frame.
3713
+ * @example
3714
+ * ```ts
3715
+ * import { parseAstmRecords, serializeFramedAstm, parseFramedAstm } from "@cosyte/astm";
3716
+ * const msg = parseAstmRecords("H|\\^&\rR|1|^^^687|28.6|U/L||N||F\rL|1\r");
3717
+ * const bytes = serializeFramedAstm(msg);
3718
+ * parseFramedAstm(bytes).message.records.length; // 3
3719
+ * ```
3720
+ */
3721
+ declare function serializeFramedAstm(input: AstmMessage | readonly AstmRecord[], options?: ComposeFramesOptions): Uint8Array;
3722
+
3723
+ /**
3724
+ * The ASTM/CLSI-LIS01 modulo-256 frame checksum.
3725
+ *
3726
+ * The checksum is the sum, modulo 256, of every byte **after** the `STX` up to
3727
+ * and **including** the `ETB`/`ETX` terminator — i.e. the frame number, the record
3728
+ * text, and the terminator byte. It is carried on the wire as **two hexadecimal
3729
+ * characters**. This module computes it (`computeChecksum`), formats it for emit
3730
+ * (`toChecksumHex` — always **uppercase**), and reads it back leniently
3731
+ * (`parseChecksumHex` — **accepts lowercase**), matching the real-vendor quirk that
3732
+ * some analyzers emit a lowercase checksum.
3733
+ */
3734
+ /**
3735
+ * Sum bytes `[start, endInclusive]` of `bytes` modulo 256 — the ASTM frame
3736
+ * checksum span (frame number through the terminator, inclusive).
3737
+ *
3738
+ * @param bytes - The full decoded byte stream.
3739
+ * @param start - First index to include (the frame-number byte, one past `STX`).
3740
+ * @param endInclusive - Last index to include (the `ETB`/`ETX` terminator byte).
3741
+ * @returns The checksum in `0`–`255`.
3742
+ * @example
3743
+ * ```ts
3744
+ * import { computeChecksum } from "@cosyte/astm";
3745
+ * // bytes: STX '1' 'A' ETX → sum '1' + 'A' + ETX, mod 256.
3746
+ * computeChecksum(new Uint8Array([0x02, 0x31, 0x41, 0x03]), 1, 3); // 0x75
3747
+ * ```
3748
+ */
3749
+ declare function computeChecksum(bytes: Uint8Array, start: number, endInclusive: number): number;
3750
+ /**
3751
+ * Format a checksum byte as the two **uppercase** hex characters ASTM puts on the
3752
+ * wire (the conservative-emit form). Decode accepts lowercase; emit is uppercase.
3753
+ *
3754
+ * @param checksum - A value in `0`–`255`.
3755
+ * @returns Two uppercase hex characters, zero-padded (e.g. `"0A"`, `"75"`).
3756
+ * @example
3757
+ * ```ts
3758
+ * import { toChecksumHex } from "@cosyte/astm";
3759
+ * toChecksumHex(0x0a); // "0A"
3760
+ * ```
3761
+ */
3762
+ declare function toChecksumHex(checksum: number): string;
3763
+ /**
3764
+ * Read the two-hex-char checksum at `bytes[i0]`/`bytes[i1]`, **case-insensitively**
3765
+ * (a lowercase checksum is a tolerated real-vendor quirk). Returns `undefined` when
3766
+ * either position is out of range or not a hex digit — the caller then treats the
3767
+ * frame as having no readable declared checksum, never as a match.
3768
+ *
3769
+ * @param bytes - The full decoded byte stream.
3770
+ * @param i0 - Index of the high hex nibble (immediately after the terminator).
3771
+ * @param i1 - Index of the low hex nibble.
3772
+ * @returns The declared checksum in `0`–`255`, or `undefined` if unreadable.
3773
+ * @example
3774
+ * ```ts
3775
+ * import { parseChecksumHex } from "@cosyte/astm";
3776
+ * parseChecksumHex(new Uint8Array([0x37, 0x35]), 0, 1); // 0x75 ("75")
3777
+ * parseChecksumHex(new Uint8Array([0x37, 0x35]), 5, 6); // undefined (out of range)
3778
+ * ```
3779
+ */
3780
+ declare function parseChecksumHex(bytes: Uint8Array, i0: number, i1: number): number | undefined;
3781
+
3782
+ /**
3783
+ * The frame codec's thrown error — the `strict`-mode counterpart to its lenient
3784
+ * warnings.
3785
+ *
3786
+ * The frame layer is lenient by default (byte-level deviations become
3787
+ * {@link AstmFrameWarning}s), and shares the single {@link FATAL_CODES.EMPTY_INPUT}
3788
+ * fatal with the record layer (thrown as an {@link AstmParseError} on an empty
3789
+ * stream, in both modes). In `strict` mode every tolerated deviation is rejected
3790
+ * instead: {@link decodeAstmFrames} throws {@link AstmFrameStrictError} carrying the
3791
+ * warnings it would otherwise have accumulated (code + position, never a value).
3792
+ */
3793
+
3794
+ /**
3795
+ * Thrown by {@link decodeAstmFrames} in `strict` mode when the lenient codec would
3796
+ * otherwise have accumulated one or more frame warnings (a bad checksum, a
3797
+ * sequence gap, an unterminated frame, or an oversize frame). Carries every
3798
+ * warning (code + position, never the frame's record bytes) so a caller can see
3799
+ * each deviation.
3800
+ *
3801
+ * @example
3802
+ * ```ts
3803
+ * import { decodeAstmFrames, AstmFrameStrictError } from "@cosyte/astm";
3804
+ * try {
3805
+ * decodeAstmFrames(someFramedBytes, { strict: true });
3806
+ * } catch (err) {
3807
+ * if (err instanceof AstmFrameStrictError) err.warnings.length; // >= 1
3808
+ * }
3809
+ * ```
3810
+ */
3811
+ declare class AstmFrameStrictError extends Error {
3812
+ readonly warnings: readonly AstmFrameWarning[];
3813
+ /** @internal */
3814
+ constructor(warnings: readonly AstmFrameWarning[]);
3815
+ }
3816
+
3817
+ /**
3818
+ * Public entry point for the `@cosyte/astm` package — the ASTM/CLSI-LIS02
3819
+ * **record** layer.
3820
+ *
3821
+ * The north star: hand a de-framed ASTM record stream to {@link parseAstmRecords}
3822
+ * and pull a result's value, units, and flag out in one line — lenient on parse
3823
+ * (vendor quirks become typed warnings, never silent loss), and — the whole point
3824
+ * — **never a confident wrong value**. Delimiters are read from each header,
3825
+ * embedded escapes are decoded before a value is split, the practice- and
3826
+ * laboratory-assigned patient IDs stay distinct, and every deviation is a stable,
3827
+ * value-free warning. Result flag/status semantics (P2), patient/order identity
3828
+ * depth + the `C` comment record + partial-timestamp hardening (P3), and the
3829
+ * request-information (`Q`) record + host-query classification + verbatim `M`/`S`
3830
+ * records (P4) are all modeled — the **record-content layer is now feature-complete**.
3831
+ *
3832
+ * The E1381/CLSI-LIS01 **framing** layer (P5) lives alongside the record layer and
3833
+ * shares nothing but the payload boundary: {@link decodeAstmFrames} decodes a framed
3834
+ * byte stream (`<STX> FN text <ETB|ETX> CS <CR><LF>`) into frames + reassembled
3835
+ * record bytes — verifying the modulo-256 checksum (a bad frame is surfaced
3836
+ * untrusted, never merged), tracking frame-number sequencing (a gap is never
3837
+ * silently bridged), and reassembling the 240-byte-limited multi-frame records.
3838
+ * {@link parseFramedAstm} composes the two layers at the edge.
3839
+ *
3840
+ * The LTP **protocol** layer (P6) sits above the frame codec: {@link detectFraming}
3841
+ * auto-detects the transport reality — framed (serial / cobas 4800 / Iguana) vs raw
3842
+ * (cobas b121, framing dropped) — from the stream's leading byte, and
3843
+ * {@link ltpReduce} is a **pure, socket-free** receiver-side state machine over
3844
+ * `ENQ`/`ACK`/`NAK`/`EOT` + frame-received events. The consumer owns the wire; the
3845
+ * reducer decides. Its inviolable rule mirrors `mllp`'s ACK-failsafe: a frame the
3846
+ * codec did not vouch for is answered with `NAK`, **never** a fabricated positive
3847
+ * `ACK`, and never merged into a record — a `NAK` drives retransmit, not acceptance.
3848
+ * Serialize/build (P7) is deferred.
3849
+ */
3850
+ /**
3851
+ * Library version string, synced with `package.json#version` at build time.
3852
+ *
3853
+ * @example
3854
+ * ```ts
3855
+ * import { VERSION } from "@cosyte/astm";
3856
+ * console.log(VERSION);
3857
+ * ```
3858
+ */
3859
+ declare const VERSION = "0.0.0";
3860
+
3861
+ export { ALL_ASTM_WARNING_CODES, ACK as ASTM_ACK, ENQ as ASTM_ENQ, EOT as ASTM_EOT, NAK as ASTM_NAK, type AbnormalFlag, type AbnormalFlagCode, type AbnormalFlagMeaning, type AnyAstmWarningCode, 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, 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, detectFraming, encodeComponent, fieldScalar, frameBadChecksum, frameOversize, frameSequenceGap, frameUnterminated, getAstmProfile, getDefaultAstmProfile, interpretAbnormalFlag, interpretResultStatus, isNonStandard, isSafetyCriticalCode, listAstmProfiles, livdAmbiguousMapping, livdUnmappedCode, lookupLivdForRecord, ltpAmbiguousTransport, ltpFrameRejected, ltpInitialState, ltpReduce, ltpUnexpectedEvent, nonStandardDelimiters, orders, orphanComment, parseAstmDate, parseAstmRecords, parseChecksumHex, parseFramedAstm, parseReferenceRange, partialTimestamp, patient, primaryCode, profileQuirkApplied, query, readDelimiters, recognizeUniversalTestId, resolveProfileTransport, results, serializeAstmRecord, serializeAstmRecords, serializeField, serializeFramedAstm, setDefaultAstmProfile, splitEscapeAware, toChecksumHex, tokenizeRecord, undefinedAbnormalFlag, undefinedResultStatus, uninterpretedQueryStatus, unitsAbsent, unknownEscapeSequence, unknownRecordType, unparseableReferenceRange };