@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.
- package/CHANGELOG.md +358 -0
- package/LICENSE +21 -0
- package/README.md +185 -0
- package/dist/index.cjs +2071 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +3861 -0
- package/dist/index.d.ts +3861 -0
- package/dist/index.mjs +1977 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +100 -0
package/dist/index.d.ts
ADDED
|
@@ -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 };
|