@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.cjs
ADDED
|
@@ -0,0 +1,2071 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/common/errors.ts
|
|
4
|
+
var FATAL_CODES = {
|
|
5
|
+
/** Input was empty or whitespace-only — there is nothing to parse. Shared across layers. */
|
|
6
|
+
EMPTY_INPUT: "EMPTY_INPUT",
|
|
7
|
+
/** The first record is not an `H` (header) record — an ASTM message must lead with `H`. */
|
|
8
|
+
ASTM_RECORD_NO_HEADER: "ASTM_RECORD_NO_HEADER",
|
|
9
|
+
/** The `H` record is too short to declare the four delimiters (field/repeat/component/escape). */
|
|
10
|
+
ASTM_RECORD_UNDECLARED_DELIMITERS: "ASTM_RECORD_UNDECLARED_DELIMITERS"
|
|
11
|
+
};
|
|
12
|
+
var AstmParseError = class extends Error {
|
|
13
|
+
code;
|
|
14
|
+
position;
|
|
15
|
+
/**
|
|
16
|
+
* Construct a new `AstmParseError`. Both fields are required so every thrower
|
|
17
|
+
* populates positional context and no PHI-bearing snippet is ever attached.
|
|
18
|
+
*
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
21
|
+
constructor(code, message, position) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "AstmParseError";
|
|
24
|
+
this.code = code;
|
|
25
|
+
this.position = position;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// src/common/freeze.ts
|
|
30
|
+
function deepFreeze(value) {
|
|
31
|
+
if (value === null || typeof value !== "object") return value;
|
|
32
|
+
if (Object.isFrozen(value)) return value;
|
|
33
|
+
Object.freeze(value);
|
|
34
|
+
for (const key of Object.keys(value)) {
|
|
35
|
+
deepFreeze(value[key]);
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/common/delimiters.ts
|
|
41
|
+
var CANONICAL_DELIMITERS = {
|
|
42
|
+
field: "|",
|
|
43
|
+
repeat: "\\",
|
|
44
|
+
component: "^",
|
|
45
|
+
escape: "&"
|
|
46
|
+
};
|
|
47
|
+
function readDelimiters(headerRecord) {
|
|
48
|
+
if (headerRecord.length < 5) return void 0;
|
|
49
|
+
if (headerRecord.charAt(0) !== "H") return void 0;
|
|
50
|
+
const field = headerRecord.charAt(1);
|
|
51
|
+
const defEnd = headerRecord.indexOf(field, 2);
|
|
52
|
+
const definition = headerRecord.slice(2, defEnd === -1 ? headerRecord.length : defEnd);
|
|
53
|
+
if (definition.length < 3) return void 0;
|
|
54
|
+
const repeat = definition.charAt(0);
|
|
55
|
+
const component = definition.charAt(1);
|
|
56
|
+
const escape = definition.charAt(2);
|
|
57
|
+
if (field === repeat || field === component || field === escape) return void 0;
|
|
58
|
+
return { field, repeat, component, escape };
|
|
59
|
+
}
|
|
60
|
+
function isNonStandard(d) {
|
|
61
|
+
return d.field !== CANONICAL_DELIMITERS.field || d.repeat !== CANONICAL_DELIMITERS.repeat || d.component !== CANONICAL_DELIMITERS.component || d.escape !== CANONICAL_DELIMITERS.escape;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/common/dates.ts
|
|
65
|
+
function parseAstmDate(raw) {
|
|
66
|
+
const s = raw.trim();
|
|
67
|
+
if (!/^\d{4,}$/u.test(s)) return void 0;
|
|
68
|
+
const year = Number(s.slice(0, 4));
|
|
69
|
+
const truncated = s.length < 14 && s.length % 2 === 1;
|
|
70
|
+
const base = { raw: s, year, ...truncated ? { truncated: true } : {} };
|
|
71
|
+
if (s.length < 6) return { ...base, precision: "year" };
|
|
72
|
+
const month = Number(s.slice(4, 6));
|
|
73
|
+
if (s.length < 8) return { ...base, month, precision: "month" };
|
|
74
|
+
const day = Number(s.slice(6, 8));
|
|
75
|
+
if (s.length < 10) return { ...base, month, day, precision: "day" };
|
|
76
|
+
const hour = Number(s.slice(8, 10));
|
|
77
|
+
if (s.length < 12) return { ...base, month, day, hour, precision: "hour" };
|
|
78
|
+
const minute = Number(s.slice(10, 12));
|
|
79
|
+
if (s.length < 14) return { ...base, month, day, hour, minute, precision: "minute" };
|
|
80
|
+
const second = Number(s.slice(12, 14));
|
|
81
|
+
return { ...base, month, day, hour, minute, second, precision: "second" };
|
|
82
|
+
}
|
|
83
|
+
function astmDateToLocalISO(d) {
|
|
84
|
+
const p2 = (n) => String(n).padStart(2, "0");
|
|
85
|
+
let out = String(d.year).padStart(4, "0");
|
|
86
|
+
if (d.month !== void 0) out += `-${p2(d.month)}`;
|
|
87
|
+
if (d.day !== void 0) out += `-${p2(d.day)}`;
|
|
88
|
+
if (d.hour !== void 0) out += `T${p2(d.hour)}`;
|
|
89
|
+
if (d.minute !== void 0) out += `:${p2(d.minute)}`;
|
|
90
|
+
if (d.second !== void 0) out += `:${p2(d.second)}`;
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/common/coding-system.ts
|
|
95
|
+
function recognizeUniversalTestId(components) {
|
|
96
|
+
const nonEmpty = (i) => {
|
|
97
|
+
const c = components[i];
|
|
98
|
+
return c !== void 0 && c.length > 0 ? c : void 0;
|
|
99
|
+
};
|
|
100
|
+
const loincCandidate = nonEmpty(0);
|
|
101
|
+
const testName = nonEmpty(1);
|
|
102
|
+
const codingScheme = nonEmpty(2);
|
|
103
|
+
const localCode = nonEmpty(3);
|
|
104
|
+
let provenance;
|
|
105
|
+
if (loincCandidate !== void 0) provenance = "inline-loinc-candidate";
|
|
106
|
+
else if (localCode !== void 0) provenance = "local-code";
|
|
107
|
+
else if (testName !== void 0) provenance = "name-only";
|
|
108
|
+
else provenance = "empty";
|
|
109
|
+
const base = { components, provenance };
|
|
110
|
+
return {
|
|
111
|
+
...base,
|
|
112
|
+
...loincCandidate !== void 0 ? { loincCandidate } : {},
|
|
113
|
+
...testName !== void 0 ? { testName } : {},
|
|
114
|
+
...codingScheme !== void 0 ? { codingScheme } : {},
|
|
115
|
+
...localCode !== void 0 ? { localCode } : {}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function primaryCode(u) {
|
|
119
|
+
return u.loincCandidate ?? u.localCode;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/common/warnings.ts
|
|
123
|
+
var WARNING_CODES = {
|
|
124
|
+
/** A record's type letter is not one of the modeled types — surfaced as an unsupported record. */
|
|
125
|
+
ASTM_RECORD_UNKNOWN_TYPE: "ASTM_RECORD_UNKNOWN_TYPE",
|
|
126
|
+
/** The header declared delimiters other than the canonical `H|\^&` — tolerated, noted. */
|
|
127
|
+
ASTM_NONSTANDARD_DELIMITERS: "ASTM_NONSTANDARD_DELIMITERS",
|
|
128
|
+
/** An escape sequence body was not one of `&F&`/`&S&`/`&R&`/`&E&` — preserved verbatim. */
|
|
129
|
+
ASTM_UNKNOWN_ESCAPE_SEQUENCE: "ASTM_UNKNOWN_ESCAPE_SEQUENCE",
|
|
130
|
+
/**
|
|
131
|
+
* A result value field carried an *unescaped* component delimiter, so it split into more than one
|
|
132
|
+
* component. Both the full raw value and the split are surfaced and this warning fires — the
|
|
133
|
+
* ambiguity is never resolved silently into a truncated value.
|
|
134
|
+
*/
|
|
135
|
+
ASTM_RECORD_AMBIGUOUS_VALUE_SPLIT: "ASTM_RECORD_AMBIGUOUS_VALUE_SPLIT",
|
|
136
|
+
/**
|
|
137
|
+
* A result's abnormal-flag field (`R` field 7) carried a letter outside HL7 Table 0078. The flag is
|
|
138
|
+
* surfaced as `undefined` — never dropped, and **never coerced to `normal`** (a clinical error).
|
|
139
|
+
*/
|
|
140
|
+
ASTM_RECORD_UNDEFINED_ABNORMAL_FLAG: "ASTM_RECORD_UNDEFINED_ABNORMAL_FLAG",
|
|
141
|
+
/**
|
|
142
|
+
* A result's status field (`R` field 9) carried a letter that is not a recognized status. It is
|
|
143
|
+
* surfaced as `undefined` and, like every non-`F` status, never reads as active-final.
|
|
144
|
+
*/
|
|
145
|
+
ASTM_RECORD_UNDEFINED_RESULT_STATUS: "ASTM_RECORD_UNDEFINED_RESULT_STATUS",
|
|
146
|
+
/**
|
|
147
|
+
* A result's reference-range field (`R` field 6) did not match a recognized form (`low-high`,
|
|
148
|
+
* `<high`, `>low`). The text is surfaced verbatim as `unparsed` — a bound is **never fabricated**.
|
|
149
|
+
*/
|
|
150
|
+
ASTM_RECORD_UNPARSEABLE_REFERENCE_RANGE: "ASTM_RECORD_UNPARSEABLE_REFERENCE_RANGE",
|
|
151
|
+
/**
|
|
152
|
+
* A result carried a numeric value but no units (`R` field 5 empty). Units are vendor free text
|
|
153
|
+
* (not UCUM); a missing unit is flagged here and **never defaulted, guessed, or converted**.
|
|
154
|
+
*/
|
|
155
|
+
ASTM_RECORD_UNITS_ABSENT: "ASTM_RECORD_UNITS_ABSENT",
|
|
156
|
+
/**
|
|
157
|
+
* A `C` (comment) record had no valid preceding `H`/`P`/`O`/`R` parent — an **orphan**. The comment
|
|
158
|
+
* is attached to the message root (`attachedToRoot: true`) and surfaced, **never dropped**.
|
|
159
|
+
*/
|
|
160
|
+
ASTM_RECORD_ORPHAN_COMMENT: "ASTM_RECORD_ORPHAN_COMMENT",
|
|
161
|
+
/**
|
|
162
|
+
* A `YYYYMMDDHHMMSS` timestamp had an odd digit run that truncates a two-digit component in half
|
|
163
|
+
* (e.g. a partial day/hour). The raw run is preserved and the structured value stops at the last
|
|
164
|
+
* **complete** component — the dangling digit is **never zero-filled into a fabricated time**.
|
|
165
|
+
*/
|
|
166
|
+
ASTM_RECORD_PARTIAL_TIMESTAMP: "ASTM_RECORD_PARTIAL_TIMESTAMP",
|
|
167
|
+
/**
|
|
168
|
+
* A `Q` (request-information) record carried a request-information status code (field 13). The code
|
|
169
|
+
* *set* is `[OSS-derived / paywalled]` with no publicly-groundable enumeration, so the parser
|
|
170
|
+
* interprets **none** of them: the status is surfaced verbatim and this value-free warning flags that
|
|
171
|
+
* it was passed through **uninterpreted** — never mapped to a guessed meaning.
|
|
172
|
+
*/
|
|
173
|
+
ASTM_RECORD_UNINTERPRETED_QUERY_STATUS: "ASTM_RECORD_UNINTERPRETED_QUERY_STATUS",
|
|
174
|
+
/**
|
|
175
|
+
* A message carried **both** a `Q` (request) and an `R` (result) record — a contradictory shape. The
|
|
176
|
+
* message is classified `host-query` (the `Q` **dominates**, so it is never read as a result set) and
|
|
177
|
+
* this warning flags the anomaly. Positional context only; no field value.
|
|
178
|
+
*/
|
|
179
|
+
ASTM_RECORD_AMBIGUOUS_MESSAGE_KIND: "ASTM_RECORD_AMBIGUOUS_MESSAGE_KIND",
|
|
180
|
+
/**
|
|
181
|
+
* The downgraded form an active vendor {@link AstmProfile} produces from a deviation it *expects*
|
|
182
|
+
* (see `src/profiles/`). The original warning is **never dropped**: its code moves to
|
|
183
|
+
* {@link AstmRecordWarning.toleratedCode}, the warning is re-badged `PROFILE_QUIRK_APPLIED` with
|
|
184
|
+
* `expected: true` and the tolerating profile named, so a consumer can filter known, grounded noise
|
|
185
|
+
* while the fact of the deviation — and where it was — survives. A profile can only ever reach this
|
|
186
|
+
* path for a **non-safety-critical** code (enforced at profile-definition time); a safety-critical
|
|
187
|
+
* deviation (a result value, flag, status, patient identifier, code system, or a frame-integrity
|
|
188
|
+
* warning) can **never** be tolerated, so it can never be re-badged here.
|
|
189
|
+
*/
|
|
190
|
+
PROFILE_QUIRK_APPLIED: "PROFILE_QUIRK_APPLIED"
|
|
191
|
+
};
|
|
192
|
+
function unknownRecordType(position) {
|
|
193
|
+
return {
|
|
194
|
+
code: WARNING_CODES.ASTM_RECORD_UNKNOWN_TYPE,
|
|
195
|
+
message: "Unrecognized record type \u2014 surfaced verbatim as an unsupported record.",
|
|
196
|
+
position
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function nonStandardDelimiters(position) {
|
|
200
|
+
return {
|
|
201
|
+
code: WARNING_CODES.ASTM_NONSTANDARD_DELIMITERS,
|
|
202
|
+
message: "Header declared non-canonical delimiters \u2014 read from the header and honored.",
|
|
203
|
+
position
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function unknownEscapeSequence(position) {
|
|
207
|
+
return {
|
|
208
|
+
code: WARNING_CODES.ASTM_UNKNOWN_ESCAPE_SEQUENCE,
|
|
209
|
+
message: "Unrecognized escape sequence preserved verbatim.",
|
|
210
|
+
position
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function ambiguousValueSplit(position) {
|
|
214
|
+
return {
|
|
215
|
+
code: WARNING_CODES.ASTM_RECORD_AMBIGUOUS_VALUE_SPLIT,
|
|
216
|
+
message: "Result value contained an unescaped component delimiter \u2014 full raw value and split both surfaced.",
|
|
217
|
+
position
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function undefinedAbnormalFlag(position) {
|
|
221
|
+
return {
|
|
222
|
+
code: WARNING_CODES.ASTM_RECORD_UNDEFINED_ABNORMAL_FLAG,
|
|
223
|
+
message: "Abnormal flag is not in HL7 Table 0078 \u2014 surfaced as undefined, never as normal.",
|
|
224
|
+
position
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function undefinedResultStatus(position) {
|
|
228
|
+
return {
|
|
229
|
+
code: WARNING_CODES.ASTM_RECORD_UNDEFINED_RESULT_STATUS,
|
|
230
|
+
message: "Result status is not a recognized status letter \u2014 surfaced as undefined.",
|
|
231
|
+
position
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function unparseableReferenceRange(position) {
|
|
235
|
+
return {
|
|
236
|
+
code: WARNING_CODES.ASTM_RECORD_UNPARSEABLE_REFERENCE_RANGE,
|
|
237
|
+
message: "Reference range did not match a recognized form \u2014 surfaced verbatim, no bound invented.",
|
|
238
|
+
position
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function unitsAbsent(position) {
|
|
242
|
+
return {
|
|
243
|
+
code: WARNING_CODES.ASTM_RECORD_UNITS_ABSENT,
|
|
244
|
+
message: "Numeric result value carried no units \u2014 never defaulted, guessed, or converted.",
|
|
245
|
+
position
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function orphanComment(position) {
|
|
249
|
+
return {
|
|
250
|
+
code: WARNING_CODES.ASTM_RECORD_ORPHAN_COMMENT,
|
|
251
|
+
message: "Comment had no valid preceding parent \u2014 attached to the message root, never dropped.",
|
|
252
|
+
position
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function partialTimestamp(position) {
|
|
256
|
+
return {
|
|
257
|
+
code: WARNING_CODES.ASTM_RECORD_PARTIAL_TIMESTAMP,
|
|
258
|
+
message: "Timestamp digit run truncates a component \u2014 preserved verbatim, never zero-filled.",
|
|
259
|
+
position
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function uninterpretedQueryStatus(position) {
|
|
263
|
+
return {
|
|
264
|
+
code: WARNING_CODES.ASTM_RECORD_UNINTERPRETED_QUERY_STATUS,
|
|
265
|
+
message: "Query request-information status surfaced verbatim \u2014 code set paywalled, never interpreted.",
|
|
266
|
+
position
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function ambiguousMessageKind(position) {
|
|
270
|
+
return {
|
|
271
|
+
code: WARNING_CODES.ASTM_RECORD_AMBIGUOUS_MESSAGE_KIND,
|
|
272
|
+
message: "Message carried both a Q (request) and an R (result) record \u2014 classified host-query; Q dominates.",
|
|
273
|
+
position
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function profileQuirkApplied(original, profileName) {
|
|
277
|
+
return {
|
|
278
|
+
code: WARNING_CODES.PROFILE_QUIRK_APPLIED,
|
|
279
|
+
message: `Profile "${profileName}" expected ${original.code}: ${original.message}`,
|
|
280
|
+
position: original.position,
|
|
281
|
+
expected: true,
|
|
282
|
+
profile: profileName,
|
|
283
|
+
toleratedCode: original.code
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/records/result-semantics.ts
|
|
288
|
+
var ABNORMAL_FLAG_MEANINGS = {
|
|
289
|
+
L: "below-normal",
|
|
290
|
+
H: "above-normal",
|
|
291
|
+
LL: "critically-below-normal",
|
|
292
|
+
HH: "critically-above-normal",
|
|
293
|
+
"<": "below-scale",
|
|
294
|
+
">": "above-scale",
|
|
295
|
+
N: "normal",
|
|
296
|
+
A: "abnormal",
|
|
297
|
+
AA: "very-abnormal",
|
|
298
|
+
U: "significant-change-up",
|
|
299
|
+
D: "significant-change-down",
|
|
300
|
+
B: "better",
|
|
301
|
+
W: "worse",
|
|
302
|
+
S: "susceptible",
|
|
303
|
+
R: "resistant",
|
|
304
|
+
I: "intermediate"
|
|
305
|
+
};
|
|
306
|
+
function interpretAbnormalFlag(raw) {
|
|
307
|
+
const key = raw.trim();
|
|
308
|
+
if (isAbnormalFlagCode(key)) {
|
|
309
|
+
return { raw, code: key, meaning: ABNORMAL_FLAG_MEANINGS[key], recognized: true };
|
|
310
|
+
}
|
|
311
|
+
return { raw, meaning: "undefined", recognized: false };
|
|
312
|
+
}
|
|
313
|
+
function isAbnormalFlagCode(value) {
|
|
314
|
+
return Object.prototype.hasOwnProperty.call(ABNORMAL_FLAG_MEANINGS, value);
|
|
315
|
+
}
|
|
316
|
+
var RESULT_STATUS_MEANINGS = {
|
|
317
|
+
F: "final",
|
|
318
|
+
C: "correction",
|
|
319
|
+
P: "preliminary",
|
|
320
|
+
R: "previously-transmitted",
|
|
321
|
+
S: "partial",
|
|
322
|
+
I: "pending",
|
|
323
|
+
X: "cancelled"
|
|
324
|
+
};
|
|
325
|
+
function interpretResultStatus(raw) {
|
|
326
|
+
if (raw === void 0 || raw.trim().length === 0) {
|
|
327
|
+
return {
|
|
328
|
+
meaning: "unspecified",
|
|
329
|
+
recognized: false,
|
|
330
|
+
isActiveFinal: false,
|
|
331
|
+
supersedes: false,
|
|
332
|
+
cancelled: false
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
const key = raw.trim();
|
|
336
|
+
if (isResultStatusCode(key)) {
|
|
337
|
+
return {
|
|
338
|
+
raw,
|
|
339
|
+
code: key,
|
|
340
|
+
meaning: RESULT_STATUS_MEANINGS[key],
|
|
341
|
+
recognized: true,
|
|
342
|
+
isActiveFinal: key === "F",
|
|
343
|
+
supersedes: key === "C",
|
|
344
|
+
cancelled: key === "X"
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
raw,
|
|
349
|
+
meaning: "undefined",
|
|
350
|
+
recognized: false,
|
|
351
|
+
isActiveFinal: false,
|
|
352
|
+
supersedes: false,
|
|
353
|
+
cancelled: false
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function isResultStatusCode(value) {
|
|
357
|
+
return Object.prototype.hasOwnProperty.call(RESULT_STATUS_MEANINGS, value);
|
|
358
|
+
}
|
|
359
|
+
var NUMBER = "-?\\d+(?:\\.\\d+)?";
|
|
360
|
+
var OPEN_LOW = new RegExp(`^<\\s*(${NUMBER})$`, "u");
|
|
361
|
+
var OPEN_HIGH = new RegExp(`^>\\s*(${NUMBER})$`, "u");
|
|
362
|
+
var CLOSED = new RegExp(`^(${NUMBER})\\s*-\\s*(${NUMBER})$`, "u");
|
|
363
|
+
function parseReferenceRange(raw) {
|
|
364
|
+
const text = raw.trim();
|
|
365
|
+
const low = OPEN_LOW.exec(text);
|
|
366
|
+
if (low?.[1] !== void 0) return { raw, high: low[1], kind: "open-low" };
|
|
367
|
+
const high = OPEN_HIGH.exec(text);
|
|
368
|
+
if (high?.[1] !== void 0) return { raw, low: high[1], kind: "open-high" };
|
|
369
|
+
const closed = CLOSED.exec(text);
|
|
370
|
+
if (closed?.[1] !== void 0 && closed[2] !== void 0) {
|
|
371
|
+
return { raw, low: closed[1], high: closed[2], kind: "closed" };
|
|
372
|
+
}
|
|
373
|
+
return { raw, kind: "unparsed" };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// src/records/host-query.ts
|
|
377
|
+
function classifyMessage(records) {
|
|
378
|
+
let hasQuery = false;
|
|
379
|
+
let hasResults = false;
|
|
380
|
+
let hasOrders = false;
|
|
381
|
+
for (const r of records) {
|
|
382
|
+
if (r.type === "Q") hasQuery = true;
|
|
383
|
+
else if (r.type === "R") hasResults = true;
|
|
384
|
+
else if (r.type === "O") hasOrders = true;
|
|
385
|
+
}
|
|
386
|
+
let kind;
|
|
387
|
+
if (hasQuery) kind = "host-query";
|
|
388
|
+
else if (hasResults) kind = "results";
|
|
389
|
+
else if (hasOrders) kind = "orders";
|
|
390
|
+
else kind = "indeterminate";
|
|
391
|
+
return { kind, hasQuery, hasResults, hasOrders, isHostQueryRequest: kind === "host-query" };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// src/profiles/apply.ts
|
|
395
|
+
function toleranceApplies(tolerance, warning) {
|
|
396
|
+
if (tolerance.code !== warning.code) return false;
|
|
397
|
+
const match = tolerance.match;
|
|
398
|
+
if (match === void 0) return true;
|
|
399
|
+
if (match.recordType !== void 0 && warning.position.recordType !== match.recordType) {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
if (match.fieldIndex !== void 0 && warning.position.fieldIndex !== match.fieldIndex) {
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
function applyAstmProfile(profile, warning) {
|
|
408
|
+
if (warning.expected === true) return warning;
|
|
409
|
+
for (const tolerance of profile.tolerate) {
|
|
410
|
+
if (toleranceApplies(tolerance, warning)) {
|
|
411
|
+
return profileQuirkApplied(warning, profile.name);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return warning;
|
|
415
|
+
}
|
|
416
|
+
function applyAstmProfileToWarnings(warnings, profile) {
|
|
417
|
+
if (profile === void 0) return [...warnings];
|
|
418
|
+
return warnings.map((w) => applyAstmProfile(profile, w));
|
|
419
|
+
}
|
|
420
|
+
function resolveProfileTransport(profile) {
|
|
421
|
+
return profile?.transport;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// src/profiles/describe.ts
|
|
425
|
+
function buildDescribe(p) {
|
|
426
|
+
const lines = [`Profile '${p.name}'`];
|
|
427
|
+
if (p.description !== void 0) {
|
|
428
|
+
lines.push(` description: ${p.description}`);
|
|
429
|
+
}
|
|
430
|
+
lines.push(` lineage: ${p.lineage.join(" \u2192 ")}`);
|
|
431
|
+
if (p.transport !== void 0) {
|
|
432
|
+
lines.push(` transport: forces ${p.transport} framing (detection override)`);
|
|
433
|
+
}
|
|
434
|
+
if (p.provenance !== void 0) {
|
|
435
|
+
lines.push(` grounded in: ${p.provenance.source} (${p.provenance.reference})`);
|
|
436
|
+
}
|
|
437
|
+
if (p.tolerate.length === 0) {
|
|
438
|
+
lines.push(" tolerates: nothing (conservative baseline)");
|
|
439
|
+
} else {
|
|
440
|
+
lines.push(` tolerates ${String(p.tolerate.length)} quirk(s):`);
|
|
441
|
+
for (const t of p.tolerate) {
|
|
442
|
+
const scope = t.match?.recordType !== void 0 ? ` @record ${t.match.recordType}${t.match.fieldIndex !== void 0 ? `.${String(t.match.fieldIndex)}` : ""}` : t.match?.fieldIndex !== void 0 ? ` @field ${String(t.match.fieldIndex)}` : "";
|
|
443
|
+
lines.push(` - ${t.code}${scope}: ${t.rationale}`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return lines.join("\n");
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// src/profiles/merge.ts
|
|
450
|
+
function normaliseParents(ext) {
|
|
451
|
+
if (ext === void 0) return [];
|
|
452
|
+
if (Array.isArray(ext)) return ext;
|
|
453
|
+
return [ext];
|
|
454
|
+
}
|
|
455
|
+
function mergeLineage(parents, selfName) {
|
|
456
|
+
const seen = /* @__PURE__ */ new Set();
|
|
457
|
+
const out = [];
|
|
458
|
+
for (const p of parents) {
|
|
459
|
+
const parentLineage = p.lineage.length > 0 ? p.lineage : [p.name];
|
|
460
|
+
for (const n of parentLineage) {
|
|
461
|
+
if (!seen.has(n)) {
|
|
462
|
+
seen.add(n);
|
|
463
|
+
out.push(n);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (!seen.has(selfName)) out.push(selfName);
|
|
468
|
+
return Object.freeze(out);
|
|
469
|
+
}
|
|
470
|
+
function toleranceKey(t) {
|
|
471
|
+
return `${t.code} ${t.match?.recordType ?? ""} ${String(t.match?.fieldIndex ?? "")}`;
|
|
472
|
+
}
|
|
473
|
+
function mergeTolerations(parents, self) {
|
|
474
|
+
const order = [];
|
|
475
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
476
|
+
const layer = (list) => {
|
|
477
|
+
for (const t of list) {
|
|
478
|
+
const key = toleranceKey(t);
|
|
479
|
+
if (!byKey.has(key)) order.push(key);
|
|
480
|
+
byKey.set(key, t);
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
for (const p of parents) layer(p.tolerate);
|
|
484
|
+
layer(self);
|
|
485
|
+
return Object.freeze(order.map((k) => byKey.get(k)));
|
|
486
|
+
}
|
|
487
|
+
function mergeProvenance(parents, self) {
|
|
488
|
+
if (self !== void 0) return self;
|
|
489
|
+
for (let i = parents.length - 1; i >= 0; i--) {
|
|
490
|
+
const p = parents[i];
|
|
491
|
+
if (p?.provenance !== void 0) return p.provenance;
|
|
492
|
+
}
|
|
493
|
+
return void 0;
|
|
494
|
+
}
|
|
495
|
+
function mergeDescription(parents, self) {
|
|
496
|
+
if (self !== void 0) return self;
|
|
497
|
+
for (let i = parents.length - 1; i >= 0; i--) {
|
|
498
|
+
const p = parents[i];
|
|
499
|
+
if (p?.description !== void 0) return p.description;
|
|
500
|
+
}
|
|
501
|
+
return void 0;
|
|
502
|
+
}
|
|
503
|
+
function mergeTransport(parents, self) {
|
|
504
|
+
if (self !== void 0) return self;
|
|
505
|
+
for (let i = parents.length - 1; i >= 0; i--) {
|
|
506
|
+
const p = parents[i];
|
|
507
|
+
if (p?.transport !== void 0) return p.transport;
|
|
508
|
+
}
|
|
509
|
+
return void 0;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// src/profiles/errors.ts
|
|
513
|
+
var AstmProfileDefinitionError = class extends Error {
|
|
514
|
+
/** The offending profile's `name`, when it could be read. */
|
|
515
|
+
profileName;
|
|
516
|
+
/**
|
|
517
|
+
* Construct a new `AstmProfileDefinitionError`.
|
|
518
|
+
*
|
|
519
|
+
* @param message - A human-readable explanation of what was invalid.
|
|
520
|
+
* @param profileName - The offending profile's name, when known.
|
|
521
|
+
* @internal
|
|
522
|
+
*/
|
|
523
|
+
constructor(message, profileName) {
|
|
524
|
+
super(message);
|
|
525
|
+
this.name = "AstmProfileDefinitionError";
|
|
526
|
+
if (profileName !== void 0) this.profileName = profileName;
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
// src/frames/warnings.ts
|
|
531
|
+
var FRAME_WARNING_CODES = {
|
|
532
|
+
/**
|
|
533
|
+
* A frame's two-hex-char checksum did not match the modulo-256 sum recomputed over its bytes. The
|
|
534
|
+
* frame is surfaced with `trusted: false` and its text is **never merged** into a reassembled
|
|
535
|
+
* record (default **warn** in lenient mode, escalated to a thrown error in `strict`). Corruption is
|
|
536
|
+
* never silently trusted — the "checksums are routinely not validated" claim was refuted; we
|
|
537
|
+
* validate.
|
|
538
|
+
*/
|
|
539
|
+
ASTM_FRAME_BAD_CHECKSUM: "ASTM_FRAME_BAD_CHECKSUM",
|
|
540
|
+
/**
|
|
541
|
+
* A frame's sequence number was not the expected next value (`1 → … → 7 → 0 → …`) — a frame was
|
|
542
|
+
* possibly dropped. The stream is **never silently concatenated across the gap** as if contiguous;
|
|
543
|
+
* the in-progress record is tainted and not emitted as a clean reassembly.
|
|
544
|
+
*/
|
|
545
|
+
ASTM_FRAME_SEQUENCE_GAP: "ASTM_FRAME_SEQUENCE_GAP",
|
|
546
|
+
/**
|
|
547
|
+
* A frame opened with `STX` but no valid terminator + checksum was found before the stream ended
|
|
548
|
+
* (or before the next `STX`), or a record's frames ended on an intermediate `ETB` with no final
|
|
549
|
+
* `ETX`. The partial bytes are surfaced flagged untrusted and **no partial record is invented**
|
|
550
|
+
* (warn in lenient, thrown in `strict`).
|
|
551
|
+
*/
|
|
552
|
+
ASTM_FRAME_UNTERMINATED: "ASTM_FRAME_UNTERMINATED",
|
|
553
|
+
/**
|
|
554
|
+
* A frame's record text exceeded the 240-byte limit without a split. The frame is still surfaced
|
|
555
|
+
* (and, if its checksum validates, reassembled) — the deviation is flagged, not silently dropped
|
|
556
|
+
* (warn in lenient, thrown in `strict`).
|
|
557
|
+
*/
|
|
558
|
+
ASTM_FRAME_OVERSIZE: "ASTM_FRAME_OVERSIZE"
|
|
559
|
+
};
|
|
560
|
+
function frameBadChecksum(position) {
|
|
561
|
+
return {
|
|
562
|
+
code: FRAME_WARNING_CODES.ASTM_FRAME_BAD_CHECKSUM,
|
|
563
|
+
message: "Frame checksum mismatch \u2014 surfaced untrusted, never merged into a record.",
|
|
564
|
+
position
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
function frameSequenceGap(position) {
|
|
568
|
+
return {
|
|
569
|
+
code: FRAME_WARNING_CODES.ASTM_FRAME_SEQUENCE_GAP,
|
|
570
|
+
message: "Frame number out of sequence \u2014 possible dropped frame, never silently bridged.",
|
|
571
|
+
position
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
function frameUnterminated(position) {
|
|
575
|
+
return {
|
|
576
|
+
code: FRAME_WARNING_CODES.ASTM_FRAME_UNTERMINATED,
|
|
577
|
+
message: "Unterminated frame \u2014 partial bytes surfaced untrusted, no partial record invented.",
|
|
578
|
+
position
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
function frameOversize(position) {
|
|
582
|
+
return {
|
|
583
|
+
code: FRAME_WARNING_CODES.ASTM_FRAME_OVERSIZE,
|
|
584
|
+
message: "Frame text exceeded the 240-byte limit without a split \u2014 flagged, never dropped.",
|
|
585
|
+
position
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// src/ltp/warnings.ts
|
|
590
|
+
var LTP_WARNING_CODES = {
|
|
591
|
+
/**
|
|
592
|
+
* The transport detector could not tell a framed stream from a raw (unframed) one from its leading
|
|
593
|
+
* byte — it was neither `STX`/`ENQ` nor a bare record letter. It **defaults to framed** and warns,
|
|
594
|
+
* never guessing silently into data loss; a profile override forces the mode. (cobas b121 drops
|
|
595
|
+
* framing over TCP; cobas 4800 / Iguana retain it — both realities exist, so an unrecognizable lead
|
|
596
|
+
* is defaulted, not assumed.)
|
|
597
|
+
*/
|
|
598
|
+
ASTM_LTP_AMBIGUOUS_TRANSPORT: "ASTM_LTP_AMBIGUOUS_TRANSPORT",
|
|
599
|
+
/**
|
|
600
|
+
* A control event arrived in a protocol state that did not expect it — e.g. an inbound `ACK`/`NAK`
|
|
601
|
+
* at a receiver (which sends, never receives, those), or an `ENQ` mid-transfer. The event is
|
|
602
|
+
* surfaced and handled defensively (an unexpected `ACK`/`NAK` is **never** read as acceptance of
|
|
603
|
+
* data); it never advances the transfer as if valid.
|
|
604
|
+
*/
|
|
605
|
+
ASTM_LTP_UNEXPECTED_EVENT: "ASTM_LTP_UNEXPECTED_EVENT",
|
|
606
|
+
/**
|
|
607
|
+
* The receiver rejected a received frame with a `NAK` instead of accepting it — because its
|
|
608
|
+
* checksum failed, it was unterminated, or its frame number was out of sequence. The frame's text
|
|
609
|
+
* is **never** appended to the record and the transfer does **not** advance; the receiver awaits the
|
|
610
|
+
* sender's retransmit. This is the protocol-level face of the frame codec's fail-safe: a bad frame
|
|
611
|
+
* drives retransmit, not acceptance.
|
|
612
|
+
*/
|
|
613
|
+
ASTM_LTP_FRAME_REJECTED: "ASTM_LTP_FRAME_REJECTED"
|
|
614
|
+
};
|
|
615
|
+
function ltpAmbiguousTransport() {
|
|
616
|
+
return {
|
|
617
|
+
code: LTP_WARNING_CODES.ASTM_LTP_AMBIGUOUS_TRANSPORT,
|
|
618
|
+
message: "Leading byte was neither a frame start (STX/ENQ) nor a record letter \u2014 defaulted to framed; override with a profile to force raw."
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
function ltpUnexpectedEvent(frameNumber) {
|
|
622
|
+
return {
|
|
623
|
+
code: LTP_WARNING_CODES.ASTM_LTP_UNEXPECTED_EVENT,
|
|
624
|
+
message: "Control event arrived in a state that did not expect it \u2014 handled defensively, never read as acceptance.",
|
|
625
|
+
...frameNumber !== void 0 ? { frameNumber } : {}
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
function ltpFrameRejected(frameNumber) {
|
|
629
|
+
return {
|
|
630
|
+
code: LTP_WARNING_CODES.ASTM_LTP_FRAME_REJECTED,
|
|
631
|
+
message: "Frame rejected (bad checksum, unterminated, or out of sequence) \u2014 NAK sent, retransmit expected, never accepted.",
|
|
632
|
+
...frameNumber !== void 0 ? { frameNumber } : {}
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// src/profiles/safety.ts
|
|
637
|
+
var TOLERABLE_CODES = Object.freeze(
|
|
638
|
+
/* @__PURE__ */ new Set([
|
|
639
|
+
WARNING_CODES.ASTM_RECORD_UNKNOWN_TYPE,
|
|
640
|
+
WARNING_CODES.ASTM_NONSTANDARD_DELIMITERS,
|
|
641
|
+
WARNING_CODES.ASTM_UNKNOWN_ESCAPE_SEQUENCE,
|
|
642
|
+
WARNING_CODES.ASTM_RECORD_UNINTERPRETED_QUERY_STATUS
|
|
643
|
+
])
|
|
644
|
+
);
|
|
645
|
+
var ALL_WARNING_CODES = [
|
|
646
|
+
...Object.values(WARNING_CODES),
|
|
647
|
+
...Object.values(FRAME_WARNING_CODES),
|
|
648
|
+
...Object.values(LTP_WARNING_CODES)
|
|
649
|
+
];
|
|
650
|
+
var ALL_ASTM_WARNING_CODES = Object.freeze(
|
|
651
|
+
new Set(ALL_WARNING_CODES)
|
|
652
|
+
);
|
|
653
|
+
var SAFETY_CRITICAL_CODES = Object.freeze(
|
|
654
|
+
new Set(ALL_WARNING_CODES.filter((c) => !TOLERABLE_CODES.has(c)))
|
|
655
|
+
);
|
|
656
|
+
function isSafetyCriticalCode(code) {
|
|
657
|
+
return SAFETY_CRITICAL_CODES.has(code);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// src/profiles/validate.ts
|
|
661
|
+
var KNOWN_OPTION_KEYS = [
|
|
662
|
+
"name",
|
|
663
|
+
"description",
|
|
664
|
+
"tolerate",
|
|
665
|
+
"transport",
|
|
666
|
+
"provenance",
|
|
667
|
+
"extends"
|
|
668
|
+
];
|
|
669
|
+
var KNOWN_TRANSPORTS = ["framed", "raw"];
|
|
670
|
+
function levenshtein(a, b) {
|
|
671
|
+
if (a === b) return 0;
|
|
672
|
+
if (a.length === 0) return b.length;
|
|
673
|
+
if (b.length === 0) return a.length;
|
|
674
|
+
let prev = [];
|
|
675
|
+
for (let j = 0; j <= b.length; j++) prev.push(j);
|
|
676
|
+
for (let i = 1; i <= a.length; i++) {
|
|
677
|
+
const curr = [i];
|
|
678
|
+
for (let j = 1; j <= b.length; j++) {
|
|
679
|
+
const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
|
|
680
|
+
curr.push(Math.min((prev[j] ?? 0) + 1, (curr[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost));
|
|
681
|
+
}
|
|
682
|
+
prev = curr;
|
|
683
|
+
}
|
|
684
|
+
return prev[b.length] ?? 0;
|
|
685
|
+
}
|
|
686
|
+
function validateProfileName(opts) {
|
|
687
|
+
if (opts === null || opts === void 0) {
|
|
688
|
+
throw new AstmProfileDefinitionError(
|
|
689
|
+
`defineAstmProfile: options is required and must be an object. Received: ${String(opts)}.`
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
if (typeof opts.name !== "string") {
|
|
693
|
+
throw new AstmProfileDefinitionError(
|
|
694
|
+
`defineAstmProfile: 'name' is required and must be a non-empty string. Received: ${JSON.stringify(opts.name)}.`
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
if (opts.name.trim().length === 0) {
|
|
698
|
+
throw new AstmProfileDefinitionError(
|
|
699
|
+
`defineAstmProfile: 'name' is required and must be a non-empty string. Received: ${JSON.stringify(opts.name)}.`,
|
|
700
|
+
opts.name
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
function validateOptionKeys(opts) {
|
|
705
|
+
for (const key of Object.keys(opts)) {
|
|
706
|
+
if (KNOWN_OPTION_KEYS.includes(key)) continue;
|
|
707
|
+
let hint;
|
|
708
|
+
for (const known of KNOWN_OPTION_KEYS) {
|
|
709
|
+
if (levenshtein(key, known) <= 2) {
|
|
710
|
+
hint = known;
|
|
711
|
+
break;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
throw new AstmProfileDefinitionError(
|
|
715
|
+
`Profile '${opts.name}' has unknown option key '${key}'. ` + (hint !== void 0 ? `Did you mean '${hint}'? ` : "") + `Known keys: ${KNOWN_OPTION_KEYS.join(", ")}.`,
|
|
716
|
+
opts.name
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
function validateTransport(opts) {
|
|
721
|
+
if (opts.transport === void 0) return;
|
|
722
|
+
if (typeof opts.transport !== "string" || !KNOWN_TRANSPORTS.includes(opts.transport)) {
|
|
723
|
+
throw new AstmProfileDefinitionError(
|
|
724
|
+
`Profile '${opts.name}' has an invalid 'transport' ${JSON.stringify(opts.transport)}. It must be one of: ${KNOWN_TRANSPORTS.join(", ")}.`,
|
|
725
|
+
opts.name
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
function validateTolerations(tolerate, profileName) {
|
|
730
|
+
for (const t of tolerate) {
|
|
731
|
+
if (typeof t.code !== "string" || !ALL_ASTM_WARNING_CODES.has(t.code)) {
|
|
732
|
+
throw new AstmProfileDefinitionError(
|
|
733
|
+
`Profile '${profileName}' tolerate entry has unknown warning code ${JSON.stringify(t.code)}. Only real codes from the record / frame / LTP registries may be named.`,
|
|
734
|
+
profileName
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
if (isSafetyCriticalCode(t.code)) {
|
|
738
|
+
throw new AstmProfileDefinitionError(
|
|
739
|
+
`Profile '${profileName}' may not tolerate '${t.code}' \u2014 it is a safety-critical warning code (a result value / flag / status / range / units, patient or comment context, message-kind ambiguity, code system, or a frame / LTP integrity warning). A profile quiets benign structural noise, never a deviation that could change a clinical reading or corrupt the wire \u2014 it can never make a bad checksum "ok" or a cancelled result read "final."`,
|
|
740
|
+
profileName
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
if (typeof t.rationale !== "string" || t.rationale.trim().length === 0) {
|
|
744
|
+
throw new AstmProfileDefinitionError(
|
|
745
|
+
`Profile '${profileName}' tolerate entry for '${t.code}' needs a non-empty 'rationale' documenting the real, grounded deviation it expects.`,
|
|
746
|
+
profileName
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// src/profiles/define.ts
|
|
753
|
+
function defineAstmProfile(opts) {
|
|
754
|
+
validateProfileName(opts);
|
|
755
|
+
validateOptionKeys(opts);
|
|
756
|
+
validateTransport(opts);
|
|
757
|
+
const selfTolerate = opts.tolerate ?? [];
|
|
758
|
+
validateTolerations(selfTolerate, opts.name);
|
|
759
|
+
const parents = normaliseParents(opts.extends);
|
|
760
|
+
const lineage = mergeLineage(parents, opts.name);
|
|
761
|
+
const tolerate = mergeTolerations(parents, selfTolerate);
|
|
762
|
+
const transport = mergeTransport(parents, opts.transport);
|
|
763
|
+
const provenance = mergeProvenance(parents, opts.provenance);
|
|
764
|
+
const description = mergeDescription(parents, opts.description);
|
|
765
|
+
validateTolerations(tolerate, opts.name);
|
|
766
|
+
const profile = {
|
|
767
|
+
name: opts.name,
|
|
768
|
+
lineage,
|
|
769
|
+
tolerate
|
|
770
|
+
};
|
|
771
|
+
if (description !== void 0) profile.description = description;
|
|
772
|
+
if (transport !== void 0) profile.transport = transport;
|
|
773
|
+
if (provenance !== void 0) profile.provenance = provenance;
|
|
774
|
+
const finalised = profile;
|
|
775
|
+
profile.describe = () => buildDescribe(finalised);
|
|
776
|
+
return Object.freeze(profile);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// src/profiles/default.ts
|
|
780
|
+
var defaultProfile = defineAstmProfile({
|
|
781
|
+
name: "default",
|
|
782
|
+
description: "Conservative baseline \u2014 tolerates nothing; every deviation surfaces as a warning.",
|
|
783
|
+
tolerate: []
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
// src/profiles/reference-corpus.ts
|
|
787
|
+
var referenceCorpus = defineAstmProfile({
|
|
788
|
+
name: "referenceCorpus",
|
|
789
|
+
description: "Non-standard-escape tolerance grounded firsthand in the escape-agnostic OSS reference corpus (python-astm / senaite). Syntactic encoding noise only \u2014 the value is preserved verbatim, never a safety-critical value.",
|
|
790
|
+
provenance: {
|
|
791
|
+
source: "kxepal/python-astm codec.py (BSD) + senaite.astm / senaite.lis2a",
|
|
792
|
+
reference: "https://github.com/kxepal/python-astm/blob/master/astm/codec.py ; https://github.com/senaite/senaite.astm",
|
|
793
|
+
retrieved: "2026-07-21",
|
|
794
|
+
note: "Firsthand-read: decode_record/decode_component/decode_repeated_component split on raw delimiters and never un-escape &F&/&S&/&R&/&E&."
|
|
795
|
+
},
|
|
796
|
+
tolerate: [
|
|
797
|
+
{
|
|
798
|
+
code: "ASTM_UNKNOWN_ESCAPE_SEQUENCE",
|
|
799
|
+
rationale: "The OSS reference stack (python-astm codec.py, firsthand-verified) treats '&' as literal data and never decodes escape sequences, so streams from that ecosystem carry non-standard '&\u2026&' bodies our escape-aware tokenizer flags. The body is preserved byte-for-byte in the value; the deviation is recognizable, syntactic, and non-clinical."
|
|
800
|
+
}
|
|
801
|
+
]
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
// src/profiles/registry.ts
|
|
805
|
+
var BUILT_INS = /* @__PURE__ */ new Map([
|
|
806
|
+
[defaultProfile.name, defaultProfile],
|
|
807
|
+
[referenceCorpus.name, referenceCorpus]
|
|
808
|
+
]);
|
|
809
|
+
function getAstmProfile(name) {
|
|
810
|
+
return BUILT_INS.get(name);
|
|
811
|
+
}
|
|
812
|
+
function listAstmProfiles() {
|
|
813
|
+
return Object.freeze([...BUILT_INS.keys()]);
|
|
814
|
+
}
|
|
815
|
+
var _defaultProfile = void 0;
|
|
816
|
+
function setDefaultAstmProfile(profile) {
|
|
817
|
+
_defaultProfile = profile ?? void 0;
|
|
818
|
+
}
|
|
819
|
+
function getDefaultAstmProfile() {
|
|
820
|
+
return _defaultProfile;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// src/common/escapes.ts
|
|
824
|
+
function decodeEscapes(leaf, d, onUnknown) {
|
|
825
|
+
const esc = d.escape;
|
|
826
|
+
if (!leaf.includes(esc)) return leaf;
|
|
827
|
+
let out = "";
|
|
828
|
+
let i = 0;
|
|
829
|
+
while (i < leaf.length) {
|
|
830
|
+
const ch = leaf.charAt(i);
|
|
831
|
+
if (ch !== esc) {
|
|
832
|
+
out += ch;
|
|
833
|
+
i += 1;
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
const close = leaf.indexOf(esc, i + 1);
|
|
837
|
+
if (close === -1) {
|
|
838
|
+
out += leaf.slice(i);
|
|
839
|
+
break;
|
|
840
|
+
}
|
|
841
|
+
const body = leaf.slice(i + 1, close);
|
|
842
|
+
const replacement = escapeBody(body, d);
|
|
843
|
+
if (replacement === void 0) {
|
|
844
|
+
out += esc + body + esc;
|
|
845
|
+
onUnknown?.();
|
|
846
|
+
} else {
|
|
847
|
+
out += replacement;
|
|
848
|
+
}
|
|
849
|
+
i = close + 1;
|
|
850
|
+
}
|
|
851
|
+
return out;
|
|
852
|
+
}
|
|
853
|
+
function escapeBody(body, d) {
|
|
854
|
+
switch (body) {
|
|
855
|
+
case "F":
|
|
856
|
+
return d.field;
|
|
857
|
+
case "S":
|
|
858
|
+
return d.component;
|
|
859
|
+
case "R":
|
|
860
|
+
return d.repeat;
|
|
861
|
+
case "E":
|
|
862
|
+
return d.escape;
|
|
863
|
+
default:
|
|
864
|
+
return void 0;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
function splitEscapeAware(text, delimiter, escape) {
|
|
868
|
+
if (text.length === 0) return [""];
|
|
869
|
+
const out = [];
|
|
870
|
+
let current = "";
|
|
871
|
+
let i = 0;
|
|
872
|
+
while (i < text.length) {
|
|
873
|
+
const ch = text.charAt(i);
|
|
874
|
+
if (ch === escape) {
|
|
875
|
+
const close = text.indexOf(escape, i + 1);
|
|
876
|
+
if (close === -1) {
|
|
877
|
+
current += text.slice(i);
|
|
878
|
+
break;
|
|
879
|
+
}
|
|
880
|
+
current += text.slice(i, close + 1);
|
|
881
|
+
i = close + 1;
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
if (ch === delimiter) {
|
|
885
|
+
out.push(current);
|
|
886
|
+
current = "";
|
|
887
|
+
i += 1;
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
current += ch;
|
|
891
|
+
i += 1;
|
|
892
|
+
}
|
|
893
|
+
out.push(current);
|
|
894
|
+
return out;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// src/records/tokenize.ts
|
|
898
|
+
function tokenizeRecord(record, d, onUnknownEscape) {
|
|
899
|
+
const rawFields = splitEscapeAware(record, d.field, d.escape);
|
|
900
|
+
return rawFields.map((raw, fieldIndex) => toField(raw, d, () => onUnknownEscape?.(fieldIndex)));
|
|
901
|
+
}
|
|
902
|
+
function toField(raw, d, onUnknownEscape) {
|
|
903
|
+
const rawRepeats = splitEscapeAware(raw, d.repeat, d.escape);
|
|
904
|
+
const repeats = rawRepeats.map(
|
|
905
|
+
(rep) => splitEscapeAware(rep, d.component, d.escape).map(
|
|
906
|
+
(comp) => decodeEscapes(comp, d, onUnknownEscape)
|
|
907
|
+
)
|
|
908
|
+
);
|
|
909
|
+
const components = repeats[0] ?? [""];
|
|
910
|
+
return { raw, components, repeats };
|
|
911
|
+
}
|
|
912
|
+
function fieldScalar(field) {
|
|
913
|
+
if (field === void 0) return void 0;
|
|
914
|
+
const first = field.components[0];
|
|
915
|
+
return first !== void 0 && first.length > 0 ? first : void 0;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// src/records/parse.ts
|
|
919
|
+
var AstmStrictError = class extends Error {
|
|
920
|
+
warnings;
|
|
921
|
+
/** @internal */
|
|
922
|
+
constructor(warnings) {
|
|
923
|
+
super(`Strict mode: ${String(warnings.length)} tolerated deviation(s) rejected.`);
|
|
924
|
+
this.name = "AstmStrictError";
|
|
925
|
+
this.warnings = warnings;
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
function parseAstmRecords(raw, options = {}) {
|
|
929
|
+
const text = typeof raw === "string" ? raw : decodeBytes(raw);
|
|
930
|
+
if (text.trim().length === 0) {
|
|
931
|
+
throw new AstmParseError(FATAL_CODES.EMPTY_INPUT, "Input is empty.", { recordIndex: 0 });
|
|
932
|
+
}
|
|
933
|
+
const lines = text.split(/\r\n|\r|\n/u).filter((l) => l.length > 0);
|
|
934
|
+
const first = lines[0];
|
|
935
|
+
if (first === void 0 || first.charAt(0) !== "H") {
|
|
936
|
+
const leadType = first?.charAt(0);
|
|
937
|
+
throw new AstmParseError(
|
|
938
|
+
FATAL_CODES.ASTM_RECORD_NO_HEADER,
|
|
939
|
+
"First record is not an H (header) record.",
|
|
940
|
+
leadType !== void 0 && leadType.length > 0 ? { recordIndex: 0, recordType: leadType } : { recordIndex: 0 }
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
const delimiters = readDelimiters(first);
|
|
944
|
+
if (delimiters === void 0) {
|
|
945
|
+
throw new AstmParseError(
|
|
946
|
+
FATAL_CODES.ASTM_RECORD_UNDECLARED_DELIMITERS,
|
|
947
|
+
"Header record is too short to declare the four delimiters.",
|
|
948
|
+
{ recordIndex: 0, recordType: "H" }
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
const warnings = [];
|
|
952
|
+
if (isNonStandard(delimiters)) {
|
|
953
|
+
warnings.push(nonStandardDelimiters({ recordIndex: 0, recordType: "H" }));
|
|
954
|
+
}
|
|
955
|
+
const built = lines.map(
|
|
956
|
+
(line2, recordIndex) => buildRecord(line2, recordIndex, delimiters, warnings)
|
|
957
|
+
);
|
|
958
|
+
const records = attachComments(built, warnings);
|
|
959
|
+
const classification = classifyMessage(records);
|
|
960
|
+
if (classification.hasQuery && classification.hasResults) {
|
|
961
|
+
warnings.push(ambiguousMessageKind({ recordIndex: 0, recordType: "H" }));
|
|
962
|
+
}
|
|
963
|
+
const profile = resolveProfile(options);
|
|
964
|
+
const finalWarnings = applyAstmProfileToWarnings(warnings, profile);
|
|
965
|
+
if (options.strict === true) {
|
|
966
|
+
const escalating = finalWarnings.filter((w) => w.expected !== true);
|
|
967
|
+
if (escalating.length > 0) {
|
|
968
|
+
throw new AstmStrictError(escalating);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
const header = records[0];
|
|
972
|
+
const message = profile !== void 0 ? {
|
|
973
|
+
header,
|
|
974
|
+
records,
|
|
975
|
+
delimiters,
|
|
976
|
+
classification,
|
|
977
|
+
warnings: finalWarnings,
|
|
978
|
+
profile: { name: profile.name, lineage: profile.lineage }
|
|
979
|
+
} : { header, records, delimiters, classification, warnings: finalWarnings };
|
|
980
|
+
return deepFreeze(message);
|
|
981
|
+
}
|
|
982
|
+
function resolveProfile(options) {
|
|
983
|
+
if (options.profile === null) return void 0;
|
|
984
|
+
if (options.profile !== void 0) return options.profile;
|
|
985
|
+
return getDefaultAstmProfile();
|
|
986
|
+
}
|
|
987
|
+
function decodeBytes(bytes) {
|
|
988
|
+
let out = "";
|
|
989
|
+
for (const b of bytes) out += String.fromCharCode(b);
|
|
990
|
+
return out;
|
|
991
|
+
}
|
|
992
|
+
function astmField(fields, n) {
|
|
993
|
+
return fields[n - 1];
|
|
994
|
+
}
|
|
995
|
+
function buildRecord(line2, recordIndex, d, warnings) {
|
|
996
|
+
const rawType = line2.charAt(0);
|
|
997
|
+
const fields = tokenizeRecord(line2, d, (fieldIndex) => {
|
|
998
|
+
warnings.push(
|
|
999
|
+
unknownEscapeSequence({ recordIndex, recordType: rawType, fieldIndex: fieldIndex + 1 })
|
|
1000
|
+
);
|
|
1001
|
+
});
|
|
1002
|
+
switch (rawType) {
|
|
1003
|
+
case "H":
|
|
1004
|
+
return {
|
|
1005
|
+
type: "H",
|
|
1006
|
+
recordIndex,
|
|
1007
|
+
fields,
|
|
1008
|
+
delimiters: d,
|
|
1009
|
+
rawLine: line2
|
|
1010
|
+
};
|
|
1011
|
+
case "P":
|
|
1012
|
+
return buildPatient(recordIndex, fields, warnings);
|
|
1013
|
+
case "O":
|
|
1014
|
+
return buildOrder(recordIndex, fields);
|
|
1015
|
+
case "R":
|
|
1016
|
+
return buildResult(recordIndex, fields, warnings);
|
|
1017
|
+
case "C":
|
|
1018
|
+
return buildComment(recordIndex, fields);
|
|
1019
|
+
case "Q":
|
|
1020
|
+
return buildQuery(recordIndex, fields, warnings);
|
|
1021
|
+
// `M` (manufacturer) and `S` (scientific) are vendor-defined free-form data surfaced VERBATIM and
|
|
1022
|
+
// NEVER interpreted into typed clinical fields — the exact wire line is preserved for round-trip.
|
|
1023
|
+
case "M":
|
|
1024
|
+
return { type: "M", recordIndex, fields, rawLine: line2 };
|
|
1025
|
+
case "S":
|
|
1026
|
+
return { type: "S", recordIndex, fields, rawLine: line2 };
|
|
1027
|
+
case "L":
|
|
1028
|
+
return { type: "L", recordIndex, fields };
|
|
1029
|
+
default: {
|
|
1030
|
+
warnings.push(unknownRecordType({ recordIndex, recordType: rawType }));
|
|
1031
|
+
return { type: "unsupported", rawType, recordIndex, fields };
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
function buildPatient(recordIndex, fields, warnings) {
|
|
1036
|
+
const nameField = astmField(fields, 6);
|
|
1037
|
+
const name = nameField !== void 0 && fieldScalar(nameField) !== void 0 ? {
|
|
1038
|
+
raw: nameField.raw,
|
|
1039
|
+
...definedString("last", nameField.components[0]),
|
|
1040
|
+
...definedString("first", nameField.components[1]),
|
|
1041
|
+
...definedString("middle", nameField.components[2])
|
|
1042
|
+
} : void 0;
|
|
1043
|
+
const birthDate = parseDateField(
|
|
1044
|
+
fieldScalar(astmField(fields, 8)),
|
|
1045
|
+
recordIndex,
|
|
1046
|
+
"P",
|
|
1047
|
+
8,
|
|
1048
|
+
warnings
|
|
1049
|
+
);
|
|
1050
|
+
return {
|
|
1051
|
+
type: "P",
|
|
1052
|
+
recordIndex,
|
|
1053
|
+
fields,
|
|
1054
|
+
...definedString("seq", fieldScalar(astmField(fields, 2))),
|
|
1055
|
+
// The three patient identifiers stay DISTINCT — practice-assigned (3), laboratory-assigned (4),
|
|
1056
|
+
// and a third ID (5) never collapse into one; conflating them is the primary misfiling path.
|
|
1057
|
+
...definedString("practiceAssignedId", fieldScalar(astmField(fields, 3))),
|
|
1058
|
+
...definedString("laboratoryAssignedId", fieldScalar(astmField(fields, 4))),
|
|
1059
|
+
...definedString("patientIdThree", fieldScalar(astmField(fields, 5))),
|
|
1060
|
+
...name !== void 0 ? { name } : {},
|
|
1061
|
+
...definedString("mothersMaidenName", fieldScalar(astmField(fields, 7))),
|
|
1062
|
+
...birthDate !== void 0 ? { birthDate } : {},
|
|
1063
|
+
...definedString("sex", fieldScalar(astmField(fields, 9)))
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
function buildOrder(recordIndex, fields) {
|
|
1067
|
+
const utidField = astmField(fields, 5);
|
|
1068
|
+
return {
|
|
1069
|
+
type: "O",
|
|
1070
|
+
recordIndex,
|
|
1071
|
+
fields,
|
|
1072
|
+
...definedString("seq", fieldScalar(astmField(fields, 2))),
|
|
1073
|
+
...definedString("specimenId", fieldScalar(astmField(fields, 3))),
|
|
1074
|
+
...definedString("instrumentSpecimenId", fieldScalar(astmField(fields, 4))),
|
|
1075
|
+
...hasContent(utidField) ? { universalTestId: recognizeUniversalTestId(utidField.components) } : {},
|
|
1076
|
+
// Priority (6), action code (~12), and report type (~26) are surfaced verbatim. The `~` field
|
|
1077
|
+
// indices and the code sets are `[OSS-derived]` (paywalled), so they are never mapped to a
|
|
1078
|
+
// guessed meaning — see the JSDoc on `OrderRecord`.
|
|
1079
|
+
...definedString("priority", fieldScalar(astmField(fields, 6))),
|
|
1080
|
+
...definedString("actionCode", fieldScalar(astmField(fields, 12))),
|
|
1081
|
+
...definedString("reportType", fieldScalar(astmField(fields, 26)))
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
function hasContent(field) {
|
|
1085
|
+
return field !== void 0 && field.raw.length > 0;
|
|
1086
|
+
}
|
|
1087
|
+
function buildResult(recordIndex, fields, warnings) {
|
|
1088
|
+
const utidField = astmField(fields, 3);
|
|
1089
|
+
const valueField = astmField(fields, 4);
|
|
1090
|
+
const startedAt = parseDateField(
|
|
1091
|
+
fieldScalar(astmField(fields, 12)),
|
|
1092
|
+
recordIndex,
|
|
1093
|
+
"R",
|
|
1094
|
+
12,
|
|
1095
|
+
warnings
|
|
1096
|
+
);
|
|
1097
|
+
const completedAt = parseDateField(
|
|
1098
|
+
fieldScalar(astmField(fields, 13)),
|
|
1099
|
+
recordIndex,
|
|
1100
|
+
"R",
|
|
1101
|
+
13,
|
|
1102
|
+
warnings
|
|
1103
|
+
);
|
|
1104
|
+
const ambiguous = valueField !== void 0 && valueField.components.length > 1;
|
|
1105
|
+
const valueComponents = ambiguous ? valueField.components : void 0;
|
|
1106
|
+
const value = ambiguous ? valueField.raw : fieldScalar(valueField);
|
|
1107
|
+
if (ambiguous) {
|
|
1108
|
+
warnings.push(ambiguousValueSplit({ recordIndex, recordType: "R", fieldIndex: 4 }));
|
|
1109
|
+
}
|
|
1110
|
+
const units = fieldScalar(astmField(fields, 5));
|
|
1111
|
+
const rangeRaw = fieldRaw(astmField(fields, 6));
|
|
1112
|
+
const flagRaw = fieldScalar(astmField(fields, 7));
|
|
1113
|
+
const statusRaw = fieldScalar(astmField(fields, 9));
|
|
1114
|
+
if (units === void 0 && value !== void 0 && isNumericValue(value)) {
|
|
1115
|
+
warnings.push(unitsAbsent({ recordIndex, recordType: "R", fieldIndex: 5 }));
|
|
1116
|
+
}
|
|
1117
|
+
const range = rangeRaw !== void 0 ? parseReferenceRange(rangeRaw) : void 0;
|
|
1118
|
+
if (range?.kind === "unparsed") {
|
|
1119
|
+
warnings.push(unparseableReferenceRange({ recordIndex, recordType: "R", fieldIndex: 6 }));
|
|
1120
|
+
}
|
|
1121
|
+
const flag = flagRaw !== void 0 ? interpretAbnormalFlag(flagRaw) : void 0;
|
|
1122
|
+
if (flag !== void 0 && !flag.recognized) {
|
|
1123
|
+
warnings.push(undefinedAbnormalFlag({ recordIndex, recordType: "R", fieldIndex: 7 }));
|
|
1124
|
+
}
|
|
1125
|
+
const status = interpretResultStatus(statusRaw);
|
|
1126
|
+
if (statusRaw !== void 0 && !status.recognized) {
|
|
1127
|
+
warnings.push(undefinedResultStatus({ recordIndex, recordType: "R", fieldIndex: 9 }));
|
|
1128
|
+
}
|
|
1129
|
+
return {
|
|
1130
|
+
type: "R",
|
|
1131
|
+
recordIndex,
|
|
1132
|
+
fields,
|
|
1133
|
+
...definedString("seq", fieldScalar(astmField(fields, 2))),
|
|
1134
|
+
...hasContent(utidField) ? { universalTestId: recognizeUniversalTestId(utidField.components) } : {},
|
|
1135
|
+
...definedString("value", value),
|
|
1136
|
+
...valueComponents !== void 0 ? { valueComponents } : {},
|
|
1137
|
+
...definedString("units", units),
|
|
1138
|
+
...definedString("referenceRange", rangeRaw),
|
|
1139
|
+
...range !== void 0 ? { range } : {},
|
|
1140
|
+
...definedString("abnormalFlags", flagRaw),
|
|
1141
|
+
...flag !== void 0 ? { flag } : {},
|
|
1142
|
+
...definedString("resultStatus", statusRaw),
|
|
1143
|
+
status,
|
|
1144
|
+
...definedString("operator", fieldScalar(astmField(fields, 11))),
|
|
1145
|
+
...startedAt !== void 0 ? { startedAt } : {},
|
|
1146
|
+
...completedAt !== void 0 ? { completedAt } : {},
|
|
1147
|
+
...definedString("instrument", fieldScalar(astmField(fields, 14)))
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
function buildComment(recordIndex, fields) {
|
|
1151
|
+
const textField = astmField(fields, 4);
|
|
1152
|
+
const textComponents = textField !== void 0 && textField.components.length > 1 ? textField.components : void 0;
|
|
1153
|
+
const text = textField !== void 0 && textField.raw.length > 0 ? textField.raw : void 0;
|
|
1154
|
+
return {
|
|
1155
|
+
type: "C",
|
|
1156
|
+
recordIndex,
|
|
1157
|
+
fields,
|
|
1158
|
+
attachedToRoot: false,
|
|
1159
|
+
...definedString("seq", fieldScalar(astmField(fields, 2))),
|
|
1160
|
+
...definedString("source", fieldScalar(astmField(fields, 3))),
|
|
1161
|
+
...definedString("text", text),
|
|
1162
|
+
...textComponents !== void 0 ? { textComponents } : {},
|
|
1163
|
+
...definedString("commentType", fieldScalar(astmField(fields, 5)))
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
function buildQuery(recordIndex, fields, warnings) {
|
|
1167
|
+
const utidField = astmField(fields, 5);
|
|
1168
|
+
const utidRaw = fieldRaw(utidField);
|
|
1169
|
+
const queriesAllTests = utidRaw !== void 0 && utidRaw.trim().toUpperCase() === "ALL";
|
|
1170
|
+
const requestInformationStatus = fieldRaw(astmField(fields, 13));
|
|
1171
|
+
if (requestInformationStatus !== void 0) {
|
|
1172
|
+
warnings.push(uninterpretedQueryStatus({ recordIndex, recordType: "Q", fieldIndex: 13 }));
|
|
1173
|
+
}
|
|
1174
|
+
return {
|
|
1175
|
+
type: "Q",
|
|
1176
|
+
recordIndex,
|
|
1177
|
+
fields,
|
|
1178
|
+
...definedString("seq", fieldScalar(astmField(fields, 2))),
|
|
1179
|
+
// Range IDs are surfaced from the FULL field text (never truncated to a component); their internal
|
|
1180
|
+
// caret structure is [OSS-derived]/paywalled and is not interpreted here.
|
|
1181
|
+
...definedString("startingRangeId", fieldRaw(astmField(fields, 3))),
|
|
1182
|
+
...definedString("endingRangeId", fieldRaw(astmField(fields, 4))),
|
|
1183
|
+
...hasContent(utidField) && !queriesAllTests ? { universalTestId: recognizeUniversalTestId(utidField.components) } : {},
|
|
1184
|
+
queriesAllTests,
|
|
1185
|
+
...definedString("requestInformationStatus", requestInformationStatus)
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
function attachComments(records, warnings) {
|
|
1189
|
+
let lastParentIndex;
|
|
1190
|
+
return records.map((rec) => {
|
|
1191
|
+
if (rec.type === "H" || rec.type === "P" || rec.type === "O" || rec.type === "R") {
|
|
1192
|
+
lastParentIndex = rec.recordIndex;
|
|
1193
|
+
return rec;
|
|
1194
|
+
}
|
|
1195
|
+
if (rec.type !== "C") return rec;
|
|
1196
|
+
if (lastParentIndex === void 0) {
|
|
1197
|
+
warnings.push(orphanComment({ recordIndex: rec.recordIndex, recordType: "C" }));
|
|
1198
|
+
return { ...rec, attachedToRoot: true };
|
|
1199
|
+
}
|
|
1200
|
+
return { ...rec, parentIndex: lastParentIndex, attachedToRoot: false };
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
function parseDateField(raw, recordIndex, recordType, fieldIndex, warnings) {
|
|
1204
|
+
if (raw === void 0) return void 0;
|
|
1205
|
+
const date = parseAstmDate(raw);
|
|
1206
|
+
if (date?.truncated === true) {
|
|
1207
|
+
warnings.push(partialTimestamp({ recordIndex, recordType, fieldIndex }));
|
|
1208
|
+
}
|
|
1209
|
+
return date;
|
|
1210
|
+
}
|
|
1211
|
+
function isNumericValue(value) {
|
|
1212
|
+
return /^\s*[<>]?\s*-?\d+(?:\.\d+)?\s*$/u.test(value);
|
|
1213
|
+
}
|
|
1214
|
+
function fieldRaw(field) {
|
|
1215
|
+
return field !== void 0 && field.raw.length > 0 ? field.raw : void 0;
|
|
1216
|
+
}
|
|
1217
|
+
function definedString(key, value) {
|
|
1218
|
+
return value === void 0 ? {} : { [key]: value };
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// src/records/serialize.ts
|
|
1222
|
+
var AstmSerializeError = class extends Error {
|
|
1223
|
+
/** Stable discriminant; `ASTM_EMIT_UNENCODABLE_VALUE` for a `CR`/`LF` in a value. */
|
|
1224
|
+
code;
|
|
1225
|
+
/** 0-based ordinal of the record within the message, when known. */
|
|
1226
|
+
recordIndex;
|
|
1227
|
+
/** @internal */
|
|
1228
|
+
constructor(message, recordIndex) {
|
|
1229
|
+
super(message);
|
|
1230
|
+
this.name = "AstmSerializeError";
|
|
1231
|
+
this.code = "ASTM_EMIT_UNENCODABLE_VALUE";
|
|
1232
|
+
if (recordIndex !== void 0) this.recordIndex = recordIndex;
|
|
1233
|
+
}
|
|
1234
|
+
};
|
|
1235
|
+
function encodeComponent(leaf, d, recordIndex) {
|
|
1236
|
+
if (leaf.includes("\r") || leaf.includes("\n")) {
|
|
1237
|
+
throw new AstmSerializeError(
|
|
1238
|
+
"A value contains a record terminator (CR/LF), which cannot be escaped without breaking framing.",
|
|
1239
|
+
recordIndex
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
return leaf.split(d.escape).join(d.escape + "E" + d.escape).split(d.field).join(d.escape + "F" + d.escape).split(d.component).join(d.escape + "S" + d.escape).split(d.repeat).join(d.escape + "R" + d.escape);
|
|
1243
|
+
}
|
|
1244
|
+
function encodeField(repeats, d, recordIndex) {
|
|
1245
|
+
return repeats.map((rep) => rep.map((c) => encodeComponent(c, d, recordIndex)).join(d.component)).join(d.repeat);
|
|
1246
|
+
}
|
|
1247
|
+
function serializeAstmRecord(record, d = CANONICAL_DELIMITERS) {
|
|
1248
|
+
if (record.type === "M" || record.type === "S") return record.rawLine;
|
|
1249
|
+
if (record.type === "H") return serializeHeader(record, d);
|
|
1250
|
+
return record.fields.map((f) => encodeField(f.repeats, d, record.recordIndex)).join(d.field);
|
|
1251
|
+
}
|
|
1252
|
+
function serializeHeader(header, d) {
|
|
1253
|
+
const head = "H" + d.field + d.repeat + d.component + d.escape;
|
|
1254
|
+
const src = header.delimiters;
|
|
1255
|
+
const defEnd = header.rawLine.indexOf(src.field, 2);
|
|
1256
|
+
if (defEnd === -1) return head;
|
|
1257
|
+
const dataPortion = header.rawLine.slice(defEnd + 1);
|
|
1258
|
+
const dataFields = tokenizeRecord(dataPortion, src);
|
|
1259
|
+
const rest = dataFields.map((f) => d.field + encodeField(f.repeats, d, header.recordIndex)).join("");
|
|
1260
|
+
return head + rest;
|
|
1261
|
+
}
|
|
1262
|
+
function serializeAstmRecords(input, d = CANONICAL_DELIMITERS) {
|
|
1263
|
+
const records = Array.isArray(input) ? input : input.records;
|
|
1264
|
+
return records.map((r) => serializeAstmRecord(r, d) + "\r").join("");
|
|
1265
|
+
}
|
|
1266
|
+
function serializeField(field, d = CANONICAL_DELIMITERS) {
|
|
1267
|
+
return encodeField(field.repeats, d);
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// src/records/build.ts
|
|
1271
|
+
function line(fields) {
|
|
1272
|
+
const d = CANONICAL_DELIMITERS;
|
|
1273
|
+
const encoded = fields.map((comps) => comps.map((c) => encodeComponent(c, d)).join(d.component));
|
|
1274
|
+
let end = encoded.length;
|
|
1275
|
+
while (end > 0 && encoded[end - 1] === "") end -= 1;
|
|
1276
|
+
return encoded.slice(0, end).join(d.field);
|
|
1277
|
+
}
|
|
1278
|
+
function utid(components) {
|
|
1279
|
+
return components === void 0 ? [""] : [...components];
|
|
1280
|
+
}
|
|
1281
|
+
function at(fields, position1, value) {
|
|
1282
|
+
if (value === void 0) return;
|
|
1283
|
+
fields[position1 - 1] = [value];
|
|
1284
|
+
}
|
|
1285
|
+
function buildPatientLine(input, seq) {
|
|
1286
|
+
const f = [["P"], [seq]];
|
|
1287
|
+
at(f, 3, input.practiceAssignedId);
|
|
1288
|
+
at(f, 4, input.laboratoryAssignedId);
|
|
1289
|
+
at(f, 5, input.patientIdThree);
|
|
1290
|
+
if (input.name !== void 0) {
|
|
1291
|
+
f[5] = [input.name.last ?? "", input.name.first ?? "", input.name.middle ?? ""];
|
|
1292
|
+
}
|
|
1293
|
+
at(f, 7, input.mothersMaidenName);
|
|
1294
|
+
at(f, 8, input.birthDate);
|
|
1295
|
+
at(f, 9, input.sex);
|
|
1296
|
+
return line(padGaps(f));
|
|
1297
|
+
}
|
|
1298
|
+
function buildOrderLine(input, seq) {
|
|
1299
|
+
const f = [["O"], [seq]];
|
|
1300
|
+
at(f, 3, input.specimenId);
|
|
1301
|
+
at(f, 4, input.instrumentSpecimenId);
|
|
1302
|
+
if (input.universalTestId !== void 0) f[4] = [...utid(input.universalTestId)];
|
|
1303
|
+
at(f, 6, input.priority);
|
|
1304
|
+
at(f, 12, input.actionCode);
|
|
1305
|
+
at(f, 26, input.reportType);
|
|
1306
|
+
return line(padGaps(f));
|
|
1307
|
+
}
|
|
1308
|
+
function buildResultLine(input, seq) {
|
|
1309
|
+
const f = [["R"], [seq]];
|
|
1310
|
+
if (input.universalTestId !== void 0) f[2] = [...utid(input.universalTestId)];
|
|
1311
|
+
at(f, 4, input.value);
|
|
1312
|
+
at(f, 5, input.units);
|
|
1313
|
+
at(f, 6, input.referenceRange);
|
|
1314
|
+
at(f, 7, input.abnormalFlags);
|
|
1315
|
+
at(f, 9, input.resultStatus);
|
|
1316
|
+
at(f, 11, input.operator);
|
|
1317
|
+
at(f, 12, input.startedAt);
|
|
1318
|
+
at(f, 13, input.completedAt);
|
|
1319
|
+
at(f, 14, input.instrument);
|
|
1320
|
+
return line(padGaps(f));
|
|
1321
|
+
}
|
|
1322
|
+
function buildCommentLine(input, seq) {
|
|
1323
|
+
const f = [["C"], [seq]];
|
|
1324
|
+
at(f, 3, input.source);
|
|
1325
|
+
if (input.textComponents !== void 0) f[3] = [...input.textComponents];
|
|
1326
|
+
else at(f, 4, input.text);
|
|
1327
|
+
at(f, 5, input.commentType);
|
|
1328
|
+
return line(padGaps(f));
|
|
1329
|
+
}
|
|
1330
|
+
function buildQueryLine(input, seq) {
|
|
1331
|
+
const f = [["Q"], [seq]];
|
|
1332
|
+
at(f, 3, input.startingRangeId);
|
|
1333
|
+
at(f, 4, input.endingRangeId);
|
|
1334
|
+
if (input.queriesAllTests === true) f[4] = ["ALL"];
|
|
1335
|
+
else if (input.universalTestId !== void 0) f[4] = [...utid(input.universalTestId)];
|
|
1336
|
+
at(f, 13, input.requestInformationStatus);
|
|
1337
|
+
return line(padGaps(f));
|
|
1338
|
+
}
|
|
1339
|
+
function buildVerbatimLine(input, seq) {
|
|
1340
|
+
return [input.type, seq, ...input.fields.map(guardWireSafe)].join(CANONICAL_DELIMITERS.field);
|
|
1341
|
+
}
|
|
1342
|
+
function guardWireSafe(value) {
|
|
1343
|
+
if (value.includes("\r") || value.includes("\n")) {
|
|
1344
|
+
throw new AstmSerializeError(
|
|
1345
|
+
"A verbatim field contains a record terminator (CR/LF), which would break framing."
|
|
1346
|
+
);
|
|
1347
|
+
}
|
|
1348
|
+
return value;
|
|
1349
|
+
}
|
|
1350
|
+
function padGaps(fields) {
|
|
1351
|
+
const out = [];
|
|
1352
|
+
for (let i = 0; i < fields.length; i += 1) out[i] = fields[i] ?? [""];
|
|
1353
|
+
return out;
|
|
1354
|
+
}
|
|
1355
|
+
function buildAstmMessage(input) {
|
|
1356
|
+
const lines = [];
|
|
1357
|
+
const d = CANONICAL_DELIMITERS;
|
|
1358
|
+
const headerFields = input.header?.fields ?? [];
|
|
1359
|
+
const headerRest = headerFields.map((v) => d.field + guardWireSafe(v)).join("");
|
|
1360
|
+
lines.push("H" + d.field + d.repeat + d.component + d.escape + headerRest);
|
|
1361
|
+
const counters = /* @__PURE__ */ new Map();
|
|
1362
|
+
const nextSeq = (type, override) => {
|
|
1363
|
+
if (override !== void 0) return override;
|
|
1364
|
+
const n = (counters.get(type) ?? 0) + 1;
|
|
1365
|
+
counters.set(type, n);
|
|
1366
|
+
return String(n);
|
|
1367
|
+
};
|
|
1368
|
+
for (const rec of input.records) {
|
|
1369
|
+
switch (rec.type) {
|
|
1370
|
+
case "P":
|
|
1371
|
+
lines.push(buildPatientLine(rec, nextSeq("P", rec.seq)));
|
|
1372
|
+
break;
|
|
1373
|
+
case "O":
|
|
1374
|
+
lines.push(buildOrderLine(rec, nextSeq("O", rec.seq)));
|
|
1375
|
+
break;
|
|
1376
|
+
case "R":
|
|
1377
|
+
lines.push(buildResultLine(rec, nextSeq("R", rec.seq)));
|
|
1378
|
+
break;
|
|
1379
|
+
case "C":
|
|
1380
|
+
lines.push(buildCommentLine(rec, nextSeq("C", rec.seq)));
|
|
1381
|
+
break;
|
|
1382
|
+
case "Q":
|
|
1383
|
+
lines.push(buildQueryLine(rec, nextSeq("Q", rec.seq)));
|
|
1384
|
+
break;
|
|
1385
|
+
case "M":
|
|
1386
|
+
case "S":
|
|
1387
|
+
lines.push(buildVerbatimLine(rec, nextSeq(rec.type)));
|
|
1388
|
+
break;
|
|
1389
|
+
default: {
|
|
1390
|
+
const bad = rec;
|
|
1391
|
+
throw new TypeError(`Unknown ASTM record type to build: ${String(bad.type)}`);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
const term = [["L"], ["1"]];
|
|
1396
|
+
if (input.terminationCode !== void 0) term[2] = [input.terminationCode];
|
|
1397
|
+
lines.push(line(padGaps(term)));
|
|
1398
|
+
return lines.map((l) => l + "\r").join("");
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// src/records/extractors.ts
|
|
1402
|
+
function results(msg) {
|
|
1403
|
+
return msg.records.filter((r) => r.type === "R");
|
|
1404
|
+
}
|
|
1405
|
+
function patient(msg) {
|
|
1406
|
+
return msg.records.find((r) => r.type === "P");
|
|
1407
|
+
}
|
|
1408
|
+
function orders(msg) {
|
|
1409
|
+
return msg.records.filter((r) => r.type === "O");
|
|
1410
|
+
}
|
|
1411
|
+
function comments(msg) {
|
|
1412
|
+
return msg.records.filter((r) => r.type === "C");
|
|
1413
|
+
}
|
|
1414
|
+
function commentsFor(msg, parent) {
|
|
1415
|
+
return msg.records.filter(
|
|
1416
|
+
(r) => r.type === "C" && r.parentIndex === parent.recordIndex
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1419
|
+
function query(msg) {
|
|
1420
|
+
return msg.records.filter((r) => r.type === "Q");
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// src/profiles/index.ts
|
|
1424
|
+
var astmProfiles = Object.freeze({
|
|
1425
|
+
default: defaultProfile,
|
|
1426
|
+
referenceCorpus
|
|
1427
|
+
});
|
|
1428
|
+
|
|
1429
|
+
// src/terminology/catalog.ts
|
|
1430
|
+
function defineLivdCatalog(entries) {
|
|
1431
|
+
const index = /* @__PURE__ */ new Map();
|
|
1432
|
+
for (const entry of entries) {
|
|
1433
|
+
const bucket = index.get(entry.vendorCode);
|
|
1434
|
+
if (bucket) bucket.push(entry);
|
|
1435
|
+
else index.set(entry.vendorCode, [entry]);
|
|
1436
|
+
}
|
|
1437
|
+
const catalog = {
|
|
1438
|
+
size: index.size,
|
|
1439
|
+
lookup(vendorCode) {
|
|
1440
|
+
const bucket = index.get(vendorCode);
|
|
1441
|
+
if (!bucket || bucket.length === 0) return { status: "unmapped" };
|
|
1442
|
+
const distinct = [...new Set(bucket.map((e) => e.loinc))];
|
|
1443
|
+
if (distinct.length > 1) {
|
|
1444
|
+
return deepFreeze({ status: "ambiguous", candidates: distinct });
|
|
1445
|
+
}
|
|
1446
|
+
const [first] = bucket;
|
|
1447
|
+
if (first === void 0) return { status: "unmapped" };
|
|
1448
|
+
return deepFreeze({
|
|
1449
|
+
status: "mapped",
|
|
1450
|
+
loinc: first.loinc,
|
|
1451
|
+
...first.loincLongName !== void 0 ? { loincLongName: first.loincLongName } : {}
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
};
|
|
1455
|
+
return Object.freeze(catalog);
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// src/terminology/warnings.ts
|
|
1459
|
+
var LIVD_WARNING_CODES = {
|
|
1460
|
+
/**
|
|
1461
|
+
* A record carried a vendor/local test code, but the consumer-supplied LIVD catalog held **no**
|
|
1462
|
+
* entry for it. The code stays surfaced verbatim and the mapping is `unmapped` — a LOINC is
|
|
1463
|
+
* **never** guessed. Purely advisory: the raw code and value are untouched.
|
|
1464
|
+
*/
|
|
1465
|
+
ASTM_LIVD_UNMAPPED_CODE: "ASTM_LIVD_UNMAPPED_CODE",
|
|
1466
|
+
/**
|
|
1467
|
+
* A vendor/local test code matched **more than one distinct LOINC** in the catalog (e.g. the same
|
|
1468
|
+
* transmission code used by two devices for different analytes). The mapping is `ambiguous` and the
|
|
1469
|
+
* candidate LOINCs are surfaced for inspection, but **none is chosen** — refusing to pick is the
|
|
1470
|
+
* fail-safe, since guessing wrong mis-identifies the test.
|
|
1471
|
+
*/
|
|
1472
|
+
ASTM_LIVD_AMBIGUOUS_MAPPING: "ASTM_LIVD_AMBIGUOUS_MAPPING"
|
|
1473
|
+
};
|
|
1474
|
+
function livdUnmappedCode(position) {
|
|
1475
|
+
return {
|
|
1476
|
+
code: LIVD_WARNING_CODES.ASTM_LIVD_UNMAPPED_CODE,
|
|
1477
|
+
message: "Reported test code had no LIVD mapping \u2014 surfaced unmapped, never a guessed LOINC.",
|
|
1478
|
+
position
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
function livdAmbiguousMapping(position) {
|
|
1482
|
+
return {
|
|
1483
|
+
code: LIVD_WARNING_CODES.ASTM_LIVD_AMBIGUOUS_MAPPING,
|
|
1484
|
+
message: "Reported test code mapped to multiple distinct LOINCs \u2014 surfaced ambiguous, never one guessed.",
|
|
1485
|
+
position
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
// src/terminology/apply.ts
|
|
1490
|
+
function mapTestId(uid, catalog) {
|
|
1491
|
+
if (uid.loincCandidate !== void 0) {
|
|
1492
|
+
return { status: "inline-loinc", loinc: uid.loincCandidate, source: "wire" };
|
|
1493
|
+
}
|
|
1494
|
+
const code = primaryCode(uid);
|
|
1495
|
+
if (code === void 0) return { status: "no-code" };
|
|
1496
|
+
const hit = catalog.lookup(code);
|
|
1497
|
+
switch (hit.status) {
|
|
1498
|
+
case "mapped":
|
|
1499
|
+
return {
|
|
1500
|
+
status: "mapped",
|
|
1501
|
+
loinc: hit.loinc,
|
|
1502
|
+
...hit.loincLongName !== void 0 ? { loincLongName: hit.loincLongName } : {},
|
|
1503
|
+
source: "livd",
|
|
1504
|
+
derived: true
|
|
1505
|
+
};
|
|
1506
|
+
case "ambiguous":
|
|
1507
|
+
return { status: "ambiguous", candidates: hit.candidates };
|
|
1508
|
+
case "unmapped":
|
|
1509
|
+
return { status: "unmapped" };
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
function testIdOf(record) {
|
|
1513
|
+
return record.universalTestId;
|
|
1514
|
+
}
|
|
1515
|
+
function lookupLivdForRecord(record, catalog) {
|
|
1516
|
+
const uid = testIdOf(record);
|
|
1517
|
+
const recognized = uid ?? recognizeUniversalTestId([]);
|
|
1518
|
+
const mapping = mapTestId(recognized, catalog);
|
|
1519
|
+
const reportedCode = primaryCode(recognized);
|
|
1520
|
+
return {
|
|
1521
|
+
recordIndex: record.recordIndex,
|
|
1522
|
+
recordType: record.type,
|
|
1523
|
+
...reportedCode !== void 0 ? { reportedCode } : {},
|
|
1524
|
+
provenance: recognized.provenance,
|
|
1525
|
+
mapping
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
function applyLivd(msg, catalog) {
|
|
1529
|
+
const annotations = [];
|
|
1530
|
+
const warnings = [];
|
|
1531
|
+
for (const record of msg.records) {
|
|
1532
|
+
if (record.type !== "R" && record.type !== "O") continue;
|
|
1533
|
+
const annotation = lookupLivdForRecord(record, catalog);
|
|
1534
|
+
annotations.push(annotation);
|
|
1535
|
+
const position = { recordIndex: record.recordIndex, recordType: record.type };
|
|
1536
|
+
if (annotation.mapping.status === "unmapped") {
|
|
1537
|
+
warnings.push(livdUnmappedCode(position));
|
|
1538
|
+
} else if (annotation.mapping.status === "ambiguous") {
|
|
1539
|
+
warnings.push(livdAmbiguousMapping(position));
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
return deepFreeze({ annotations, warnings });
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
// src/ltp/constants.ts
|
|
1546
|
+
var ENQ = 5;
|
|
1547
|
+
var ACK = 6;
|
|
1548
|
+
var NAK = 21;
|
|
1549
|
+
var EOT = 4;
|
|
1550
|
+
var RECORD_TYPE_LETTERS = new Set(
|
|
1551
|
+
[..."HPORCQMSL"].map((c) => c.charCodeAt(0))
|
|
1552
|
+
);
|
|
1553
|
+
|
|
1554
|
+
// src/frames/constants.ts
|
|
1555
|
+
var STX = 2;
|
|
1556
|
+
var ETX = 3;
|
|
1557
|
+
var ETB = 23;
|
|
1558
|
+
var CR = 13;
|
|
1559
|
+
var LF = 10;
|
|
1560
|
+
var FN_ZERO = 48;
|
|
1561
|
+
var FN_SEVEN = 55;
|
|
1562
|
+
var MAX_FRAME_TEXT = 240;
|
|
1563
|
+
var FRAME_NUMBER_MODULUS = 8;
|
|
1564
|
+
var FIRST_FRAME_NUMBER = 1;
|
|
1565
|
+
|
|
1566
|
+
// src/ltp/transport.ts
|
|
1567
|
+
function detectFraming(bytes, options = {}) {
|
|
1568
|
+
if (options.override !== void 0) {
|
|
1569
|
+
return { framing: options.override, defaulted: false, warnings: [] };
|
|
1570
|
+
}
|
|
1571
|
+
const lead = bytes[0];
|
|
1572
|
+
if (lead === STX || lead === ENQ) {
|
|
1573
|
+
return { framing: "framed", defaulted: false, warnings: [] };
|
|
1574
|
+
}
|
|
1575
|
+
if (lead !== void 0 && RECORD_TYPE_LETTERS.has(lead)) {
|
|
1576
|
+
return { framing: "raw", defaulted: false, warnings: [] };
|
|
1577
|
+
}
|
|
1578
|
+
return { framing: "framed", defaulted: true, warnings: [ltpAmbiguousTransport()] };
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
// src/ltp/reducer.ts
|
|
1582
|
+
var EMPTY = new Uint8Array(0);
|
|
1583
|
+
function ltpInitialState() {
|
|
1584
|
+
return freezeState({
|
|
1585
|
+
phase: "neutral",
|
|
1586
|
+
expectedFrame: FIRST_FRAME_NUMBER,
|
|
1587
|
+
records: [],
|
|
1588
|
+
recordOpen: false,
|
|
1589
|
+
openRecord: EMPTY
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
function ltpReduce(state, event) {
|
|
1593
|
+
switch (event.type) {
|
|
1594
|
+
case "enq":
|
|
1595
|
+
return establish(state);
|
|
1596
|
+
case "eot":
|
|
1597
|
+
return terminate(state);
|
|
1598
|
+
case "ack":
|
|
1599
|
+
case "nak":
|
|
1600
|
+
return { state, actions: [], warnings: [ltpUnexpectedEvent()] };
|
|
1601
|
+
case "frame":
|
|
1602
|
+
return onFrame(state, event.frame);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
function establish(state) {
|
|
1606
|
+
const warnings = state.phase === "transfer" ? [ltpUnexpectedEvent()] : [];
|
|
1607
|
+
const next = freezeState({
|
|
1608
|
+
phase: "transfer",
|
|
1609
|
+
expectedFrame: FIRST_FRAME_NUMBER,
|
|
1610
|
+
records: state.records,
|
|
1611
|
+
recordOpen: false,
|
|
1612
|
+
openRecord: EMPTY
|
|
1613
|
+
});
|
|
1614
|
+
return { state: next, actions: [{ type: "sendAck" }], warnings };
|
|
1615
|
+
}
|
|
1616
|
+
function terminate(state) {
|
|
1617
|
+
const next = freezeState({
|
|
1618
|
+
phase: "neutral",
|
|
1619
|
+
expectedFrame: FIRST_FRAME_NUMBER,
|
|
1620
|
+
records: state.records,
|
|
1621
|
+
recordOpen: false,
|
|
1622
|
+
openRecord: EMPTY
|
|
1623
|
+
});
|
|
1624
|
+
return { state: next, actions: [], warnings: [] };
|
|
1625
|
+
}
|
|
1626
|
+
function onFrame(state, frame) {
|
|
1627
|
+
if (state.phase === "neutral") {
|
|
1628
|
+
const established = freezeState({
|
|
1629
|
+
phase: "transfer",
|
|
1630
|
+
expectedFrame: FIRST_FRAME_NUMBER,
|
|
1631
|
+
records: state.records,
|
|
1632
|
+
recordOpen: false,
|
|
1633
|
+
openRecord: EMPTY
|
|
1634
|
+
});
|
|
1635
|
+
const step = processTransferFrame(established, frame);
|
|
1636
|
+
return {
|
|
1637
|
+
state: step.state,
|
|
1638
|
+
actions: step.actions,
|
|
1639
|
+
warnings: [ltpUnexpectedEvent(frame.frameNumber), ...step.warnings]
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
return processTransferFrame(state, frame);
|
|
1643
|
+
}
|
|
1644
|
+
function processTransferFrame(state, frame) {
|
|
1645
|
+
if (!frame.trusted) {
|
|
1646
|
+
return reject(state, frame.frameNumber);
|
|
1647
|
+
}
|
|
1648
|
+
const fn = frame.frameNumber;
|
|
1649
|
+
if (fn === void 0) {
|
|
1650
|
+
return reject(state, void 0);
|
|
1651
|
+
}
|
|
1652
|
+
if (state.lastAcceptedFrame !== void 0 && fn === state.lastAcceptedFrame) {
|
|
1653
|
+
return { state, actions: [{ type: "sendAck" }], warnings: [] };
|
|
1654
|
+
}
|
|
1655
|
+
if (fn !== state.expectedFrame) {
|
|
1656
|
+
return reject(state, fn);
|
|
1657
|
+
}
|
|
1658
|
+
return acceptFrame(state, frame, fn);
|
|
1659
|
+
}
|
|
1660
|
+
function acceptFrame(state, frame, fn) {
|
|
1661
|
+
const nextExpected = (fn + 1) % FRAME_NUMBER_MODULUS;
|
|
1662
|
+
const accumulated = concatBytes([state.openRecord, frame.text]);
|
|
1663
|
+
const actions = [{ type: "sendAck" }];
|
|
1664
|
+
if (frame.terminator === "ETX") {
|
|
1665
|
+
actions.push({ type: "deliverRecord", record: accumulated.slice() });
|
|
1666
|
+
const next2 = freezeState({
|
|
1667
|
+
phase: "transfer",
|
|
1668
|
+
expectedFrame: nextExpected,
|
|
1669
|
+
lastAcceptedFrame: fn,
|
|
1670
|
+
records: [...state.records, accumulated],
|
|
1671
|
+
recordOpen: false,
|
|
1672
|
+
openRecord: EMPTY
|
|
1673
|
+
});
|
|
1674
|
+
return { state: next2, actions, warnings: [] };
|
|
1675
|
+
}
|
|
1676
|
+
const next = freezeState({
|
|
1677
|
+
phase: "transfer",
|
|
1678
|
+
expectedFrame: nextExpected,
|
|
1679
|
+
lastAcceptedFrame: fn,
|
|
1680
|
+
records: state.records,
|
|
1681
|
+
recordOpen: true,
|
|
1682
|
+
openRecord: accumulated
|
|
1683
|
+
});
|
|
1684
|
+
return { state: next, actions, warnings: [] };
|
|
1685
|
+
}
|
|
1686
|
+
function reject(state, frameNumber) {
|
|
1687
|
+
return {
|
|
1688
|
+
state,
|
|
1689
|
+
actions: [{ type: "sendNak" }],
|
|
1690
|
+
warnings: [ltpFrameRejected(frameNumber)]
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
function freezeState(fields) {
|
|
1694
|
+
const state = {
|
|
1695
|
+
phase: fields.phase,
|
|
1696
|
+
expectedFrame: fields.expectedFrame,
|
|
1697
|
+
...fields.lastAcceptedFrame !== void 0 ? { lastAcceptedFrame: fields.lastAcceptedFrame } : {},
|
|
1698
|
+
records: Object.freeze([...fields.records]),
|
|
1699
|
+
recordOpen: fields.recordOpen,
|
|
1700
|
+
openRecord: fields.openRecord
|
|
1701
|
+
};
|
|
1702
|
+
return Object.freeze(state);
|
|
1703
|
+
}
|
|
1704
|
+
function concatBytes(chunks) {
|
|
1705
|
+
let total = 0;
|
|
1706
|
+
for (const c of chunks) total += c.length;
|
|
1707
|
+
const out = new Uint8Array(total);
|
|
1708
|
+
let at2 = 0;
|
|
1709
|
+
for (const c of chunks) {
|
|
1710
|
+
out.set(c, at2);
|
|
1711
|
+
at2 += c.length;
|
|
1712
|
+
}
|
|
1713
|
+
return out;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
// src/frames/checksum.ts
|
|
1717
|
+
function computeChecksum(bytes, start, endInclusive) {
|
|
1718
|
+
let sum = 0;
|
|
1719
|
+
for (let i = start; i <= endInclusive; i++) {
|
|
1720
|
+
sum = sum + (bytes[i] ?? 0) & 255;
|
|
1721
|
+
}
|
|
1722
|
+
return sum;
|
|
1723
|
+
}
|
|
1724
|
+
function toChecksumHex(checksum) {
|
|
1725
|
+
return (checksum & 255).toString(16).toUpperCase().padStart(2, "0");
|
|
1726
|
+
}
|
|
1727
|
+
function hexNibble(code) {
|
|
1728
|
+
if (code === void 0) return void 0;
|
|
1729
|
+
if (code >= 48 && code <= 57) return code - 48;
|
|
1730
|
+
if (code >= 65 && code <= 70) return code - 65 + 10;
|
|
1731
|
+
if (code >= 97 && code <= 102) return code - 97 + 10;
|
|
1732
|
+
return void 0;
|
|
1733
|
+
}
|
|
1734
|
+
function parseChecksumHex(bytes, i0, i1) {
|
|
1735
|
+
const hi = hexNibble(bytes[i0]);
|
|
1736
|
+
const lo = hexNibble(bytes[i1]);
|
|
1737
|
+
if (hi === void 0 || lo === void 0) return void 0;
|
|
1738
|
+
return hi << 4 | lo;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
// src/frames/errors.ts
|
|
1742
|
+
var AstmFrameStrictError = class extends Error {
|
|
1743
|
+
warnings;
|
|
1744
|
+
/** @internal */
|
|
1745
|
+
constructor(warnings) {
|
|
1746
|
+
super(`Strict mode: ${String(warnings.length)} frame deviation(s) rejected.`);
|
|
1747
|
+
this.name = "AstmFrameStrictError";
|
|
1748
|
+
this.warnings = warnings;
|
|
1749
|
+
}
|
|
1750
|
+
};
|
|
1751
|
+
|
|
1752
|
+
// src/frames/decode.ts
|
|
1753
|
+
function decodeAstmFrames(bytes, options = {}) {
|
|
1754
|
+
if (bytes.length === 0) {
|
|
1755
|
+
throw new AstmParseError(FATAL_CODES.EMPTY_INPUT, "Framed input is empty.", { recordIndex: 0 });
|
|
1756
|
+
}
|
|
1757
|
+
const frames = [];
|
|
1758
|
+
const records = [];
|
|
1759
|
+
const warnings = [];
|
|
1760
|
+
let recordChunks = [];
|
|
1761
|
+
let recordTainted = false;
|
|
1762
|
+
let recordOpen = false;
|
|
1763
|
+
let expected = FIRST_FRAME_NUMBER;
|
|
1764
|
+
const n = bytes.length;
|
|
1765
|
+
let i = 0;
|
|
1766
|
+
while (i < n) {
|
|
1767
|
+
if (bytes[i] !== STX) {
|
|
1768
|
+
i++;
|
|
1769
|
+
continue;
|
|
1770
|
+
}
|
|
1771
|
+
const stxOffset = i;
|
|
1772
|
+
const fnIndex = i + 1;
|
|
1773
|
+
const frameNumber = fnIndex < n ? readFrameNumber(bytes[fnIndex]) : void 0;
|
|
1774
|
+
let termIndex = -1;
|
|
1775
|
+
let oversize = false;
|
|
1776
|
+
for (let k = i + 2; k < n; k++) {
|
|
1777
|
+
const b = bytes[k];
|
|
1778
|
+
if (b === ETB || b === ETX) {
|
|
1779
|
+
termIndex = k;
|
|
1780
|
+
break;
|
|
1781
|
+
}
|
|
1782
|
+
if (b === STX) break;
|
|
1783
|
+
if (k - (i + 2) + 1 > MAX_FRAME_TEXT) oversize = true;
|
|
1784
|
+
}
|
|
1785
|
+
const checksumTruncated = termIndex !== -1 && termIndex + 2 >= n;
|
|
1786
|
+
if (termIndex === -1 || checksumTruncated) {
|
|
1787
|
+
const textEnd = termIndex !== -1 ? termIndex : nextStxOrEnd(bytes, i + 2, n);
|
|
1788
|
+
const text2 = bytes.slice(Math.min(i + 2, textEnd), textEnd);
|
|
1789
|
+
frames.push({
|
|
1790
|
+
...frameNumber !== void 0 ? { frameNumber } : {},
|
|
1791
|
+
text: text2,
|
|
1792
|
+
byteOffset: stxOffset,
|
|
1793
|
+
checksum: { computed: 0, valid: false },
|
|
1794
|
+
trusted: false,
|
|
1795
|
+
unterminated: true,
|
|
1796
|
+
oversize
|
|
1797
|
+
});
|
|
1798
|
+
warnings.push(frameUnterminated(framePosition(frameNumber, stxOffset)));
|
|
1799
|
+
recordTainted = true;
|
|
1800
|
+
recordOpen = false;
|
|
1801
|
+
i = termIndex === -1 ? textEnd : n;
|
|
1802
|
+
continue;
|
|
1803
|
+
}
|
|
1804
|
+
const termKind = bytes[termIndex] === ETB ? "ETB" : "ETX";
|
|
1805
|
+
const computed = computeChecksum(bytes, fnIndex, termIndex);
|
|
1806
|
+
const declared = parseChecksumHex(bytes, termIndex + 1, termIndex + 2);
|
|
1807
|
+
const valid = declared !== void 0 && declared === computed;
|
|
1808
|
+
const checksum = declared !== void 0 ? { computed, declared, valid } : { computed, valid };
|
|
1809
|
+
const text = bytes.slice(i + 2, termIndex);
|
|
1810
|
+
const frame = {
|
|
1811
|
+
...frameNumber !== void 0 ? { frameNumber } : {},
|
|
1812
|
+
terminator: termKind,
|
|
1813
|
+
text,
|
|
1814
|
+
byteOffset: stxOffset,
|
|
1815
|
+
checksum,
|
|
1816
|
+
trusted: valid,
|
|
1817
|
+
unterminated: false,
|
|
1818
|
+
oversize
|
|
1819
|
+
};
|
|
1820
|
+
frames.push(frame);
|
|
1821
|
+
if (frameNumber !== void 0 && isFrameDigit(frameNumber) && frameNumber === expected) {
|
|
1822
|
+
expected = (frameNumber + 1) % FRAME_NUMBER_MODULUS;
|
|
1823
|
+
} else {
|
|
1824
|
+
warnings.push(frameSequenceGap(framePosition(frameNumber, stxOffset)));
|
|
1825
|
+
recordTainted = true;
|
|
1826
|
+
if (frameNumber !== void 0 && isFrameDigit(frameNumber)) {
|
|
1827
|
+
expected = (frameNumber + 1) % FRAME_NUMBER_MODULUS;
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
if (oversize) warnings.push(frameOversize(framePosition(frameNumber, stxOffset)));
|
|
1831
|
+
if (!valid) {
|
|
1832
|
+
warnings.push(frameBadChecksum(framePosition(frameNumber, stxOffset)));
|
|
1833
|
+
recordTainted = true;
|
|
1834
|
+
} else {
|
|
1835
|
+
recordChunks.push(text);
|
|
1836
|
+
}
|
|
1837
|
+
if (termKind === "ETX") {
|
|
1838
|
+
if (!recordTainted) records.push(concatBytes2(recordChunks));
|
|
1839
|
+
recordChunks = [];
|
|
1840
|
+
recordTainted = false;
|
|
1841
|
+
recordOpen = false;
|
|
1842
|
+
} else {
|
|
1843
|
+
recordOpen = true;
|
|
1844
|
+
}
|
|
1845
|
+
i = termIndex + 3;
|
|
1846
|
+
if (bytes[i] === CR) i++;
|
|
1847
|
+
if (bytes[i] === LF) i++;
|
|
1848
|
+
}
|
|
1849
|
+
if (recordOpen) {
|
|
1850
|
+
warnings.push(frameUnterminated({ byteOffset: n }));
|
|
1851
|
+
}
|
|
1852
|
+
if (options.strict === true && warnings.length > 0) {
|
|
1853
|
+
throw new AstmFrameStrictError(warnings);
|
|
1854
|
+
}
|
|
1855
|
+
return freezeResult({ records, frames, warnings });
|
|
1856
|
+
}
|
|
1857
|
+
function freezeResult(result) {
|
|
1858
|
+
for (const f of result.frames) {
|
|
1859
|
+
Object.freeze(f.checksum);
|
|
1860
|
+
Object.freeze(f);
|
|
1861
|
+
}
|
|
1862
|
+
for (const w of result.warnings) {
|
|
1863
|
+
Object.freeze(w.position);
|
|
1864
|
+
Object.freeze(w);
|
|
1865
|
+
}
|
|
1866
|
+
Object.freeze(result.records);
|
|
1867
|
+
Object.freeze(result.frames);
|
|
1868
|
+
Object.freeze(result.warnings);
|
|
1869
|
+
return Object.freeze(result);
|
|
1870
|
+
}
|
|
1871
|
+
function readFrameNumber(fnByte) {
|
|
1872
|
+
if (fnByte === void 0) return void 0;
|
|
1873
|
+
return fnByte - FN_ZERO;
|
|
1874
|
+
}
|
|
1875
|
+
function isFrameDigit(frameNumber) {
|
|
1876
|
+
return frameNumber >= 0 && frameNumber <= FN_SEVEN - FN_ZERO;
|
|
1877
|
+
}
|
|
1878
|
+
function framePosition(frameNumber, byteOffset) {
|
|
1879
|
+
return frameNumber !== void 0 ? { frameNumber, byteOffset } : { byteOffset };
|
|
1880
|
+
}
|
|
1881
|
+
function nextStxOrEnd(bytes, from, end) {
|
|
1882
|
+
for (let k = from; k < end; k++) {
|
|
1883
|
+
if (bytes[k] === STX) return k;
|
|
1884
|
+
}
|
|
1885
|
+
return end;
|
|
1886
|
+
}
|
|
1887
|
+
function concatBytes2(chunks) {
|
|
1888
|
+
let total = 0;
|
|
1889
|
+
for (const c of chunks) total += c.length;
|
|
1890
|
+
const out = new Uint8Array(total);
|
|
1891
|
+
let at2 = 0;
|
|
1892
|
+
for (const c of chunks) {
|
|
1893
|
+
out.set(c, at2);
|
|
1894
|
+
at2 += c.length;
|
|
1895
|
+
}
|
|
1896
|
+
return out;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
// src/frames/encode.ts
|
|
1900
|
+
var AstmFrameEncodeError = class extends Error {
|
|
1901
|
+
/** Stable discriminant. */
|
|
1902
|
+
code;
|
|
1903
|
+
/** Index of the offending record within the input, when applicable. */
|
|
1904
|
+
recordIndex;
|
|
1905
|
+
/** @internal */
|
|
1906
|
+
constructor(message, recordIndex) {
|
|
1907
|
+
super(message);
|
|
1908
|
+
this.name = "AstmFrameEncodeError";
|
|
1909
|
+
this.code = "ASTM_FRAME_EMPTY_RECORD";
|
|
1910
|
+
if (recordIndex !== void 0) this.recordIndex = recordIndex;
|
|
1911
|
+
}
|
|
1912
|
+
};
|
|
1913
|
+
function toBytes(input) {
|
|
1914
|
+
if (typeof input !== "string") return input;
|
|
1915
|
+
const out = new Uint8Array(input.length);
|
|
1916
|
+
for (let i = 0; i < input.length; i += 1) out[i] = input.charCodeAt(i) & 255;
|
|
1917
|
+
return out;
|
|
1918
|
+
}
|
|
1919
|
+
function encodeFrame(text, frameNumber, isFinal) {
|
|
1920
|
+
const term = isFinal ? ETX : ETB;
|
|
1921
|
+
const bytes = [STX, FN_ZERO + frameNumber, ...text, term];
|
|
1922
|
+
const cs = computeChecksum(Uint8Array.from(bytes), 1, bytes.length - 1);
|
|
1923
|
+
const hex = toChecksumHex(cs);
|
|
1924
|
+
return [...bytes, hex.charCodeAt(0), hex.charCodeAt(1), CR, LF];
|
|
1925
|
+
}
|
|
1926
|
+
function composeAstmFrames(records, options = {}) {
|
|
1927
|
+
if (records.length === 0) {
|
|
1928
|
+
throw new AstmFrameEncodeError("Cannot frame an empty record list \u2014 nothing to transmit.");
|
|
1929
|
+
}
|
|
1930
|
+
const out = [];
|
|
1931
|
+
let frameNumber = options.startFrameNumber ?? FIRST_FRAME_NUMBER;
|
|
1932
|
+
records.forEach((record, recordIndex) => {
|
|
1933
|
+
const bytes = toBytes(record);
|
|
1934
|
+
if (bytes.length === 0) {
|
|
1935
|
+
throw new AstmFrameEncodeError(
|
|
1936
|
+
"Cannot frame an empty record \u2014 a frame must carry record bytes.",
|
|
1937
|
+
recordIndex
|
|
1938
|
+
);
|
|
1939
|
+
}
|
|
1940
|
+
for (let offset = 0; offset < bytes.length; offset += MAX_FRAME_TEXT) {
|
|
1941
|
+
const chunk = bytes.subarray(offset, offset + MAX_FRAME_TEXT);
|
|
1942
|
+
const isFinal = offset + MAX_FRAME_TEXT >= bytes.length;
|
|
1943
|
+
out.push(...encodeFrame(chunk, frameNumber, isFinal));
|
|
1944
|
+
frameNumber = (frameNumber + 1) % FRAME_NUMBER_MODULUS;
|
|
1945
|
+
}
|
|
1946
|
+
});
|
|
1947
|
+
return Uint8Array.from(out);
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
// src/frames/compose.ts
|
|
1951
|
+
function parseFramedAstm(bytes, options = {}) {
|
|
1952
|
+
const { records, frames, warnings } = decodeAstmFrames(bytes, options);
|
|
1953
|
+
const joined = concatBytes3(records);
|
|
1954
|
+
const message = parseAstmRecords(joined, options);
|
|
1955
|
+
return { message, frames, frameWarnings: warnings };
|
|
1956
|
+
}
|
|
1957
|
+
function serializeFramedAstm(input, options = {}) {
|
|
1958
|
+
const records = Array.isArray(input) ? input : input.records;
|
|
1959
|
+
const recordBytes = records.map((r) => serializeAstmRecord(r) + "\r");
|
|
1960
|
+
return composeAstmFrames(recordBytes, options);
|
|
1961
|
+
}
|
|
1962
|
+
function concatBytes3(chunks) {
|
|
1963
|
+
let total = 0;
|
|
1964
|
+
for (const c of chunks) total += c.length;
|
|
1965
|
+
const out = new Uint8Array(total);
|
|
1966
|
+
let at2 = 0;
|
|
1967
|
+
for (const c of chunks) {
|
|
1968
|
+
out.set(c, at2);
|
|
1969
|
+
at2 += c.length;
|
|
1970
|
+
}
|
|
1971
|
+
return out;
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
// src/index.ts
|
|
1975
|
+
var VERSION = "0.0.0";
|
|
1976
|
+
|
|
1977
|
+
exports.ALL_ASTM_WARNING_CODES = ALL_ASTM_WARNING_CODES;
|
|
1978
|
+
exports.ASTM_ACK = ACK;
|
|
1979
|
+
exports.ASTM_ENQ = ENQ;
|
|
1980
|
+
exports.ASTM_EOT = EOT;
|
|
1981
|
+
exports.ASTM_NAK = NAK;
|
|
1982
|
+
exports.AstmFrameEncodeError = AstmFrameEncodeError;
|
|
1983
|
+
exports.AstmFrameStrictError = AstmFrameStrictError;
|
|
1984
|
+
exports.AstmParseError = AstmParseError;
|
|
1985
|
+
exports.AstmProfileDefinitionError = AstmProfileDefinitionError;
|
|
1986
|
+
exports.AstmSerializeError = AstmSerializeError;
|
|
1987
|
+
exports.AstmStrictError = AstmStrictError;
|
|
1988
|
+
exports.CANONICAL_DELIMITERS = CANONICAL_DELIMITERS;
|
|
1989
|
+
exports.FATAL_CODES = FATAL_CODES;
|
|
1990
|
+
exports.FRAME_WARNING_CODES = FRAME_WARNING_CODES;
|
|
1991
|
+
exports.LIVD_WARNING_CODES = LIVD_WARNING_CODES;
|
|
1992
|
+
exports.LTP_WARNING_CODES = LTP_WARNING_CODES;
|
|
1993
|
+
exports.SAFETY_CRITICAL_CODES = SAFETY_CRITICAL_CODES;
|
|
1994
|
+
exports.TOLERABLE_CODES = TOLERABLE_CODES;
|
|
1995
|
+
exports.VERSION = VERSION;
|
|
1996
|
+
exports.WARNING_CODES = WARNING_CODES;
|
|
1997
|
+
exports.ambiguousMessageKind = ambiguousMessageKind;
|
|
1998
|
+
exports.ambiguousValueSplit = ambiguousValueSplit;
|
|
1999
|
+
exports.applyAstmProfile = applyAstmProfile;
|
|
2000
|
+
exports.applyAstmProfileToWarnings = applyAstmProfileToWarnings;
|
|
2001
|
+
exports.applyLivd = applyLivd;
|
|
2002
|
+
exports.astmDateToLocalISO = astmDateToLocalISO;
|
|
2003
|
+
exports.astmProfiles = astmProfiles;
|
|
2004
|
+
exports.attachComments = attachComments;
|
|
2005
|
+
exports.buildAstmMessage = buildAstmMessage;
|
|
2006
|
+
exports.classifyMessage = classifyMessage;
|
|
2007
|
+
exports.comments = comments;
|
|
2008
|
+
exports.commentsFor = commentsFor;
|
|
2009
|
+
exports.composeAstmFrames = composeAstmFrames;
|
|
2010
|
+
exports.computeChecksum = computeChecksum;
|
|
2011
|
+
exports.decodeAstmFrames = decodeAstmFrames;
|
|
2012
|
+
exports.decodeEscapes = decodeEscapes;
|
|
2013
|
+
exports.deepFreeze = deepFreeze;
|
|
2014
|
+
exports.defineAstmProfile = defineAstmProfile;
|
|
2015
|
+
exports.defineLivdCatalog = defineLivdCatalog;
|
|
2016
|
+
exports.detectFraming = detectFraming;
|
|
2017
|
+
exports.encodeComponent = encodeComponent;
|
|
2018
|
+
exports.fieldScalar = fieldScalar;
|
|
2019
|
+
exports.frameBadChecksum = frameBadChecksum;
|
|
2020
|
+
exports.frameOversize = frameOversize;
|
|
2021
|
+
exports.frameSequenceGap = frameSequenceGap;
|
|
2022
|
+
exports.frameUnterminated = frameUnterminated;
|
|
2023
|
+
exports.getAstmProfile = getAstmProfile;
|
|
2024
|
+
exports.getDefaultAstmProfile = getDefaultAstmProfile;
|
|
2025
|
+
exports.interpretAbnormalFlag = interpretAbnormalFlag;
|
|
2026
|
+
exports.interpretResultStatus = interpretResultStatus;
|
|
2027
|
+
exports.isNonStandard = isNonStandard;
|
|
2028
|
+
exports.isSafetyCriticalCode = isSafetyCriticalCode;
|
|
2029
|
+
exports.listAstmProfiles = listAstmProfiles;
|
|
2030
|
+
exports.livdAmbiguousMapping = livdAmbiguousMapping;
|
|
2031
|
+
exports.livdUnmappedCode = livdUnmappedCode;
|
|
2032
|
+
exports.lookupLivdForRecord = lookupLivdForRecord;
|
|
2033
|
+
exports.ltpAmbiguousTransport = ltpAmbiguousTransport;
|
|
2034
|
+
exports.ltpFrameRejected = ltpFrameRejected;
|
|
2035
|
+
exports.ltpInitialState = ltpInitialState;
|
|
2036
|
+
exports.ltpReduce = ltpReduce;
|
|
2037
|
+
exports.ltpUnexpectedEvent = ltpUnexpectedEvent;
|
|
2038
|
+
exports.nonStandardDelimiters = nonStandardDelimiters;
|
|
2039
|
+
exports.orders = orders;
|
|
2040
|
+
exports.orphanComment = orphanComment;
|
|
2041
|
+
exports.parseAstmDate = parseAstmDate;
|
|
2042
|
+
exports.parseAstmRecords = parseAstmRecords;
|
|
2043
|
+
exports.parseChecksumHex = parseChecksumHex;
|
|
2044
|
+
exports.parseFramedAstm = parseFramedAstm;
|
|
2045
|
+
exports.parseReferenceRange = parseReferenceRange;
|
|
2046
|
+
exports.partialTimestamp = partialTimestamp;
|
|
2047
|
+
exports.patient = patient;
|
|
2048
|
+
exports.primaryCode = primaryCode;
|
|
2049
|
+
exports.profileQuirkApplied = profileQuirkApplied;
|
|
2050
|
+
exports.query = query;
|
|
2051
|
+
exports.readDelimiters = readDelimiters;
|
|
2052
|
+
exports.recognizeUniversalTestId = recognizeUniversalTestId;
|
|
2053
|
+
exports.resolveProfileTransport = resolveProfileTransport;
|
|
2054
|
+
exports.results = results;
|
|
2055
|
+
exports.serializeAstmRecord = serializeAstmRecord;
|
|
2056
|
+
exports.serializeAstmRecords = serializeAstmRecords;
|
|
2057
|
+
exports.serializeField = serializeField;
|
|
2058
|
+
exports.serializeFramedAstm = serializeFramedAstm;
|
|
2059
|
+
exports.setDefaultAstmProfile = setDefaultAstmProfile;
|
|
2060
|
+
exports.splitEscapeAware = splitEscapeAware;
|
|
2061
|
+
exports.toChecksumHex = toChecksumHex;
|
|
2062
|
+
exports.tokenizeRecord = tokenizeRecord;
|
|
2063
|
+
exports.undefinedAbnormalFlag = undefinedAbnormalFlag;
|
|
2064
|
+
exports.undefinedResultStatus = undefinedResultStatus;
|
|
2065
|
+
exports.uninterpretedQueryStatus = uninterpretedQueryStatus;
|
|
2066
|
+
exports.unitsAbsent = unitsAbsent;
|
|
2067
|
+
exports.unknownEscapeSequence = unknownEscapeSequence;
|
|
2068
|
+
exports.unknownRecordType = unknownRecordType;
|
|
2069
|
+
exports.unparseableReferenceRange = unparseableReferenceRange;
|
|
2070
|
+
//# sourceMappingURL=index.cjs.map
|
|
2071
|
+
//# sourceMappingURL=index.cjs.map
|