@cosyte/astm 0.0.3 → 0.0.5
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 +503 -67
- package/README.md +53 -21
- package/dist/index.cjs +301 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +883 -434
- package/dist/index.d.ts +883 -434
- package/dist/index.mjs +296 -69
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -2
package/dist/index.cjs
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
// src/common/errors.ts
|
|
4
4
|
var FATAL_CODES = {
|
|
5
|
-
/** Input was empty or whitespace-only
|
|
5
|
+
/** Input was empty or whitespace-only: there is nothing to parse. Shared across layers. */
|
|
6
6
|
EMPTY_INPUT: "EMPTY_INPUT",
|
|
7
|
-
/** The first record is not an `H` (header) record
|
|
7
|
+
/** The first record is not an `H` (header) record: an ASTM message must lead with `H`. */
|
|
8
8
|
ASTM_RECORD_NO_HEADER: "ASTM_RECORD_NO_HEADER",
|
|
9
9
|
/** The `H` record is too short to declare the four delimiters (field/repeat/component/escape). */
|
|
10
10
|
ASTM_RECORD_UNDECLARED_DELIMITERS: "ASTM_RECORD_UNDECLARED_DELIMITERS"
|
|
@@ -121,21 +121,21 @@ function primaryCode(u) {
|
|
|
121
121
|
|
|
122
122
|
// src/common/warnings.ts
|
|
123
123
|
var WARNING_CODES = {
|
|
124
|
-
/** A record's type letter is not one of the modeled types
|
|
124
|
+
/** A record's type letter is not one of the modeled types: surfaced as an unsupported record. */
|
|
125
125
|
ASTM_RECORD_UNKNOWN_TYPE: "ASTM_RECORD_UNKNOWN_TYPE",
|
|
126
|
-
/** The header declared delimiters other than the canonical `H
|
|
126
|
+
/** The header declared delimiters other than the canonical `H|\^&`: tolerated, noted. */
|
|
127
127
|
ASTM_NONSTANDARD_DELIMITERS: "ASTM_NONSTANDARD_DELIMITERS",
|
|
128
|
-
/** An escape sequence body was not one of `&F&`/`&S&`/`&R&`/`&E
|
|
128
|
+
/** An escape sequence body was not one of `&F&`/`&S&`/`&R&`/`&E&`: preserved verbatim. */
|
|
129
129
|
ASTM_UNKNOWN_ESCAPE_SEQUENCE: "ASTM_UNKNOWN_ESCAPE_SEQUENCE",
|
|
130
130
|
/**
|
|
131
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
|
|
132
|
+
* component. Both the full raw value and the split are surfaced and this warning fires: the
|
|
133
133
|
* ambiguity is never resolved silently into a truncated value.
|
|
134
134
|
*/
|
|
135
135
|
ASTM_RECORD_AMBIGUOUS_VALUE_SPLIT: "ASTM_RECORD_AMBIGUOUS_VALUE_SPLIT",
|
|
136
136
|
/**
|
|
137
137
|
* A result's abnormal-flag field (`R` field 7) carried a letter outside HL7 Table 0078. The flag is
|
|
138
|
-
* surfaced as `undefined
|
|
138
|
+
* surfaced as `undefined`: never dropped, and **never coerced to `normal`** (a clinical error).
|
|
139
139
|
*/
|
|
140
140
|
ASTM_RECORD_UNDEFINED_ABNORMAL_FLAG: "ASTM_RECORD_UNDEFINED_ABNORMAL_FLAG",
|
|
141
141
|
/**
|
|
@@ -145,7 +145,7 @@ var WARNING_CODES = {
|
|
|
145
145
|
ASTM_RECORD_UNDEFINED_RESULT_STATUS: "ASTM_RECORD_UNDEFINED_RESULT_STATUS",
|
|
146
146
|
/**
|
|
147
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
|
|
148
|
+
* `<high`, `>low`). The text is surfaced verbatim as `unparsed`: a bound is **never fabricated**.
|
|
149
149
|
*/
|
|
150
150
|
ASTM_RECORD_UNPARSEABLE_REFERENCE_RANGE: "ASTM_RECORD_UNPARSEABLE_REFERENCE_RANGE",
|
|
151
151
|
/**
|
|
@@ -154,35 +154,57 @@ var WARNING_CODES = {
|
|
|
154
154
|
*/
|
|
155
155
|
ASTM_RECORD_UNITS_ABSENT: "ASTM_RECORD_UNITS_ABSENT",
|
|
156
156
|
/**
|
|
157
|
-
* A `C` (comment) record had no valid preceding `H`/`P`/`O`/`R` parent
|
|
157
|
+
* A `C` (comment) record had no valid preceding `H`/`P`/`O`/`R` parent: an **orphan**. The comment
|
|
158
158
|
* is attached to the message root (`attachedToRoot: true`) and surfaced, **never dropped**.
|
|
159
159
|
*/
|
|
160
160
|
ASTM_RECORD_ORPHAN_COMMENT: "ASTM_RECORD_ORPHAN_COMMENT",
|
|
161
161
|
/**
|
|
162
162
|
* A `YYYYMMDDHHMMSS` timestamp had an odd digit run that truncates a two-digit component in half
|
|
163
163
|
* (e.g. a partial day/hour). The raw run is preserved and the structured value stops at the last
|
|
164
|
-
* **complete** component
|
|
164
|
+
* **complete** component: the dangling digit is **never zero-filled into a fabricated time**.
|
|
165
165
|
*/
|
|
166
166
|
ASTM_RECORD_PARTIAL_TIMESTAMP: "ASTM_RECORD_PARTIAL_TIMESTAMP",
|
|
167
167
|
/**
|
|
168
168
|
* A `Q` (request-information) record carried a request-information status code (field 13). The code
|
|
169
169
|
* *set* is `[OSS-derived / paywalled]` with no publicly-groundable enumeration, so the parser
|
|
170
170
|
* interprets **none** of them: the status is surfaced verbatim and this value-free warning flags that
|
|
171
|
-
* it was passed through **uninterpreted
|
|
171
|
+
* it was passed through **uninterpreted**, never mapped to a guessed meaning.
|
|
172
172
|
*/
|
|
173
173
|
ASTM_RECORD_UNINTERPRETED_QUERY_STATUS: "ASTM_RECORD_UNINTERPRETED_QUERY_STATUS",
|
|
174
174
|
/**
|
|
175
|
-
* A message carried **both** a `Q` (request) and an `R` (result) record
|
|
175
|
+
* A message carried **both** a `Q` (request) and an `R` (result) record: a contradictory shape. The
|
|
176
176
|
* message is classified `host-query` (the `Q` **dominates**, so it is never read as a result set) and
|
|
177
177
|
* this warning flags the anomaly. Positional context only; no field value.
|
|
178
178
|
*/
|
|
179
179
|
ASTM_RECORD_AMBIGUOUS_MESSAGE_KIND: "ASTM_RECORD_AMBIGUOUS_MESSAGE_KIND",
|
|
180
|
+
/**
|
|
181
|
+
* A later `H` (header) record declared a **different** delimiter set from the one in force, and the
|
|
182
|
+
* parser **followed it**: that header and every record after it are read with the newly-declared
|
|
183
|
+
* delimiters, until the next `H`. Records already read keep the set that was in force when they were
|
|
184
|
+
* read: a redeclaration never reinterprets bytes that have already been consumed.
|
|
185
|
+
*
|
|
186
|
+
* A stream carrying several messages back to back is ordinary, and a header that simply **repeats**
|
|
187
|
+
* the delimiters already in force is a no-op that warns nothing. This code fires only when the set
|
|
188
|
+
* actually changes, because that is the point at which a reader still using the old set would begin
|
|
189
|
+
* merging fields together.
|
|
190
|
+
*/
|
|
191
|
+
ASTM_RECORD_DELIMITERS_REDECLARED: "ASTM_RECORD_DELIMITERS_REDECLARED",
|
|
192
|
+
/**
|
|
193
|
+
* A later `H` (header) record could not declare a usable delimiter set: it was too short, or the
|
|
194
|
+
* field separator it named also appeared among the other three, leaving the four roles
|
|
195
|
+
* indistinguishable. The delimiters **already in force are kept** and every record is still surfaced;
|
|
196
|
+
* a set is never guessed and no record is dropped.
|
|
197
|
+
*
|
|
198
|
+
* The same condition on the *first* header is unrecoverable and remains the
|
|
199
|
+
* `ASTM_RECORD_UNDECLARED_DELIMITERS` fatal: there is no earlier set to fall back to.
|
|
200
|
+
*/
|
|
201
|
+
ASTM_RECORD_UNREADABLE_REDECLARATION: "ASTM_RECORD_UNREADABLE_REDECLARATION",
|
|
180
202
|
/**
|
|
181
203
|
* The downgraded form an active vendor {@link AstmProfile} produces from a deviation it *expects*
|
|
182
204
|
* (see `src/profiles/`). The original warning is **never dropped**: its code moves to
|
|
183
205
|
* {@link AstmRecordWarning.toleratedCode}, the warning is re-badged `PROFILE_QUIRK_APPLIED` with
|
|
184
206
|
* `expected: true` and the tolerating profile named, so a consumer can filter known, grounded noise
|
|
185
|
-
* while the fact of the deviation
|
|
207
|
+
* while the fact of the deviation, and where it was, survives. A profile can only ever reach this
|
|
186
208
|
* path for a **non-safety-critical** code (enforced at profile-definition time); a safety-critical
|
|
187
209
|
* deviation (a result value, flag, status, patient identifier, code system, or a frame-integrity
|
|
188
210
|
* warning) can **never** be tolerated, so it can never be re-badged here.
|
|
@@ -192,14 +214,28 @@ var WARNING_CODES = {
|
|
|
192
214
|
function unknownRecordType(position) {
|
|
193
215
|
return {
|
|
194
216
|
code: WARNING_CODES.ASTM_RECORD_UNKNOWN_TYPE,
|
|
195
|
-
message: "Unrecognized record type
|
|
217
|
+
message: "Unrecognized record type, surfaced verbatim as an unsupported record.",
|
|
196
218
|
position
|
|
197
219
|
};
|
|
198
220
|
}
|
|
199
221
|
function nonStandardDelimiters(position) {
|
|
200
222
|
return {
|
|
201
223
|
code: WARNING_CODES.ASTM_NONSTANDARD_DELIMITERS,
|
|
202
|
-
message: "Header declared non-canonical delimiters
|
|
224
|
+
message: "Header declared non-canonical delimiters, read from the header and honored.",
|
|
225
|
+
position
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function delimitersRedeclared(position) {
|
|
229
|
+
return {
|
|
230
|
+
code: WARNING_CODES.ASTM_RECORD_DELIMITERS_REDECLARED,
|
|
231
|
+
message: "A later header declared different delimiters, honored from that header onward; earlier records keep the set they were read with.",
|
|
232
|
+
position
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function unreadableRedeclaration(position) {
|
|
236
|
+
return {
|
|
237
|
+
code: WARNING_CODES.ASTM_RECORD_UNREADABLE_REDECLARATION,
|
|
238
|
+
message: "A later header could not declare a usable delimiter set, the delimiters already in force were kept.",
|
|
203
239
|
position
|
|
204
240
|
};
|
|
205
241
|
}
|
|
@@ -213,63 +249,63 @@ function unknownEscapeSequence(position) {
|
|
|
213
249
|
function ambiguousValueSplit(position) {
|
|
214
250
|
return {
|
|
215
251
|
code: WARNING_CODES.ASTM_RECORD_AMBIGUOUS_VALUE_SPLIT,
|
|
216
|
-
message: "Result value contained an unescaped component delimiter
|
|
252
|
+
message: "Result value contained an unescaped component delimiter, full raw value and split both surfaced.",
|
|
217
253
|
position
|
|
218
254
|
};
|
|
219
255
|
}
|
|
220
256
|
function undefinedAbnormalFlag(position) {
|
|
221
257
|
return {
|
|
222
258
|
code: WARNING_CODES.ASTM_RECORD_UNDEFINED_ABNORMAL_FLAG,
|
|
223
|
-
message: "Abnormal flag is not in HL7 Table 0078
|
|
259
|
+
message: "Abnormal flag is not in HL7 Table 0078, surfaced as undefined, never as normal.",
|
|
224
260
|
position
|
|
225
261
|
};
|
|
226
262
|
}
|
|
227
263
|
function undefinedResultStatus(position) {
|
|
228
264
|
return {
|
|
229
265
|
code: WARNING_CODES.ASTM_RECORD_UNDEFINED_RESULT_STATUS,
|
|
230
|
-
message: "Result status is not a recognized status letter
|
|
266
|
+
message: "Result status is not a recognized status letter, surfaced as undefined.",
|
|
231
267
|
position
|
|
232
268
|
};
|
|
233
269
|
}
|
|
234
270
|
function unparseableReferenceRange(position) {
|
|
235
271
|
return {
|
|
236
272
|
code: WARNING_CODES.ASTM_RECORD_UNPARSEABLE_REFERENCE_RANGE,
|
|
237
|
-
message: "Reference range did not match a recognized form
|
|
273
|
+
message: "Reference range did not match a recognized form, surfaced verbatim, no bound invented.",
|
|
238
274
|
position
|
|
239
275
|
};
|
|
240
276
|
}
|
|
241
277
|
function unitsAbsent(position) {
|
|
242
278
|
return {
|
|
243
279
|
code: WARNING_CODES.ASTM_RECORD_UNITS_ABSENT,
|
|
244
|
-
message: "Numeric result value carried no units
|
|
280
|
+
message: "Numeric result value carried no units, never defaulted, guessed, or converted.",
|
|
245
281
|
position
|
|
246
282
|
};
|
|
247
283
|
}
|
|
248
284
|
function orphanComment(position) {
|
|
249
285
|
return {
|
|
250
286
|
code: WARNING_CODES.ASTM_RECORD_ORPHAN_COMMENT,
|
|
251
|
-
message: "Comment had no valid preceding parent
|
|
287
|
+
message: "Comment had no valid preceding parent, attached to the message root, never dropped.",
|
|
252
288
|
position
|
|
253
289
|
};
|
|
254
290
|
}
|
|
255
291
|
function partialTimestamp(position) {
|
|
256
292
|
return {
|
|
257
293
|
code: WARNING_CODES.ASTM_RECORD_PARTIAL_TIMESTAMP,
|
|
258
|
-
message: "Timestamp digit run truncates a component
|
|
294
|
+
message: "Timestamp digit run truncates a component, preserved verbatim, never zero-filled.",
|
|
259
295
|
position
|
|
260
296
|
};
|
|
261
297
|
}
|
|
262
298
|
function uninterpretedQueryStatus(position) {
|
|
263
299
|
return {
|
|
264
300
|
code: WARNING_CODES.ASTM_RECORD_UNINTERPRETED_QUERY_STATUS,
|
|
265
|
-
message: "Query request-information status surfaced verbatim
|
|
301
|
+
message: "Query request-information status surfaced verbatim, code set paywalled, never interpreted.",
|
|
266
302
|
position
|
|
267
303
|
};
|
|
268
304
|
}
|
|
269
305
|
function ambiguousMessageKind(position) {
|
|
270
306
|
return {
|
|
271
307
|
code: WARNING_CODES.ASTM_RECORD_AMBIGUOUS_MESSAGE_KIND,
|
|
272
|
-
message: "Message carried both a Q (request) and an R (result) record
|
|
308
|
+
message: "Message carried both a Q (request) and an R (result) record, classified host-query; Q dominates.",
|
|
273
309
|
position
|
|
274
310
|
};
|
|
275
311
|
}
|
|
@@ -533,12 +569,12 @@ var FRAME_WARNING_CODES = {
|
|
|
533
569
|
* A frame's two-hex-char checksum did not match the modulo-256 sum recomputed over its bytes. The
|
|
534
570
|
* frame is surfaced with `trusted: false` and its text is **never merged** into a reassembled
|
|
535
571
|
* record (default **warn** in lenient mode, escalated to a thrown error in `strict`). Corruption is
|
|
536
|
-
* never silently trusted
|
|
572
|
+
* never silently trusted: the "checksums are routinely not validated" claim was refuted; we
|
|
537
573
|
* validate.
|
|
538
574
|
*/
|
|
539
575
|
ASTM_FRAME_BAD_CHECKSUM: "ASTM_FRAME_BAD_CHECKSUM",
|
|
540
576
|
/**
|
|
541
|
-
* A frame's sequence number was not the expected next value (`1 → … → 7 → 0 → …`)
|
|
577
|
+
* A frame's sequence number was not the expected next value (`1 → … → 7 → 0 → …`): a frame was
|
|
542
578
|
* possibly dropped. The stream is **never silently concatenated across the gap** as if contiguous;
|
|
543
579
|
* the in-progress record is tainted and not emitted as a clean reassembly.
|
|
544
580
|
*/
|
|
@@ -552,7 +588,7 @@ var FRAME_WARNING_CODES = {
|
|
|
552
588
|
ASTM_FRAME_UNTERMINATED: "ASTM_FRAME_UNTERMINATED",
|
|
553
589
|
/**
|
|
554
590
|
* 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)
|
|
591
|
+
* (and, if its checksum validates, reassembled): the deviation is flagged, not silently dropped
|
|
556
592
|
* (warn in lenient, thrown in `strict`).
|
|
557
593
|
*/
|
|
558
594
|
ASTM_FRAME_OVERSIZE: "ASTM_FRAME_OVERSIZE"
|
|
@@ -560,28 +596,28 @@ var FRAME_WARNING_CODES = {
|
|
|
560
596
|
function frameBadChecksum(position) {
|
|
561
597
|
return {
|
|
562
598
|
code: FRAME_WARNING_CODES.ASTM_FRAME_BAD_CHECKSUM,
|
|
563
|
-
message: "Frame checksum mismatch
|
|
599
|
+
message: "Frame checksum mismatch, surfaced untrusted, never merged into a record.",
|
|
564
600
|
position
|
|
565
601
|
};
|
|
566
602
|
}
|
|
567
603
|
function frameSequenceGap(position) {
|
|
568
604
|
return {
|
|
569
605
|
code: FRAME_WARNING_CODES.ASTM_FRAME_SEQUENCE_GAP,
|
|
570
|
-
message: "Frame number out of sequence
|
|
606
|
+
message: "Frame number out of sequence, possible dropped frame, never silently bridged.",
|
|
571
607
|
position
|
|
572
608
|
};
|
|
573
609
|
}
|
|
574
610
|
function frameUnterminated(position) {
|
|
575
611
|
return {
|
|
576
612
|
code: FRAME_WARNING_CODES.ASTM_FRAME_UNTERMINATED,
|
|
577
|
-
message: "Unterminated frame
|
|
613
|
+
message: "Unterminated frame, partial bytes surfaced untrusted, no partial record invented.",
|
|
578
614
|
position
|
|
579
615
|
};
|
|
580
616
|
}
|
|
581
617
|
function frameOversize(position) {
|
|
582
618
|
return {
|
|
583
619
|
code: FRAME_WARNING_CODES.ASTM_FRAME_OVERSIZE,
|
|
584
|
-
message: "Frame text exceeded the 240-byte limit without a split
|
|
620
|
+
message: "Frame text exceeded the 240-byte limit without a split, flagged, never dropped.",
|
|
585
621
|
position
|
|
586
622
|
};
|
|
587
623
|
}
|
|
@@ -590,21 +626,21 @@ function frameOversize(position) {
|
|
|
590
626
|
var LTP_WARNING_CODES = {
|
|
591
627
|
/**
|
|
592
628
|
* The transport detector could not tell a framed stream from a raw (unframed) one from its leading
|
|
593
|
-
* byte
|
|
629
|
+
* byte: it was neither `STX`/`ENQ` nor a bare record letter. It **defaults to framed** and warns,
|
|
594
630
|
* never guessing silently into data loss; a profile override forces the mode. (cobas b121 drops
|
|
595
|
-
* framing over TCP; cobas 4800 / Iguana retain it
|
|
631
|
+
* framing over TCP; cobas 4800 / Iguana retain it, both realities exist, so an unrecognizable lead
|
|
596
632
|
* is defaulted, not assumed.)
|
|
597
633
|
*/
|
|
598
634
|
ASTM_LTP_AMBIGUOUS_TRANSPORT: "ASTM_LTP_AMBIGUOUS_TRANSPORT",
|
|
599
635
|
/**
|
|
600
|
-
* A control event arrived in a protocol state that did not expect it
|
|
636
|
+
* A control event arrived in a protocol state that did not expect it: e.g. an inbound `ACK`/`NAK`
|
|
601
637
|
* at a receiver (which sends, never receives, those), or an `ENQ` mid-transfer. The event is
|
|
602
638
|
* surfaced and handled defensively (an unexpected `ACK`/`NAK` is **never** read as acceptance of
|
|
603
639
|
* data); it never advances the transfer as if valid.
|
|
604
640
|
*/
|
|
605
641
|
ASTM_LTP_UNEXPECTED_EVENT: "ASTM_LTP_UNEXPECTED_EVENT",
|
|
606
642
|
/**
|
|
607
|
-
* The receiver rejected a received frame with a `NAK` instead of accepting it
|
|
643
|
+
* The receiver rejected a received frame with a `NAK` instead of accepting it: because its
|
|
608
644
|
* checksum failed, it was unterminated, or its frame number was out of sequence. The frame's text
|
|
609
645
|
* is **never** appended to the record and the transfer does **not** advance; the receiver awaits the
|
|
610
646
|
* sender's retransmit. This is the protocol-level face of the frame codec's fail-safe: a bad frame
|
|
@@ -615,20 +651,20 @@ var LTP_WARNING_CODES = {
|
|
|
615
651
|
function ltpAmbiguousTransport() {
|
|
616
652
|
return {
|
|
617
653
|
code: LTP_WARNING_CODES.ASTM_LTP_AMBIGUOUS_TRANSPORT,
|
|
618
|
-
message: "Leading byte was neither a frame start (STX/ENQ) nor a record letter
|
|
654
|
+
message: "Leading byte was neither a frame start (STX/ENQ) nor a record letter, defaulted to framed; override with a profile to force raw."
|
|
619
655
|
};
|
|
620
656
|
}
|
|
621
657
|
function ltpUnexpectedEvent(frameNumber) {
|
|
622
658
|
return {
|
|
623
659
|
code: LTP_WARNING_CODES.ASTM_LTP_UNEXPECTED_EVENT,
|
|
624
|
-
message: "Control event arrived in a state that did not expect it
|
|
660
|
+
message: "Control event arrived in a state that did not expect it, handled defensively, never read as acceptance.",
|
|
625
661
|
...frameNumber !== void 0 ? { frameNumber } : {}
|
|
626
662
|
};
|
|
627
663
|
}
|
|
628
664
|
function ltpFrameRejected(frameNumber) {
|
|
629
665
|
return {
|
|
630
666
|
code: LTP_WARNING_CODES.ASTM_LTP_FRAME_REJECTED,
|
|
631
|
-
message: "Frame rejected (bad checksum, unterminated, or out of sequence)
|
|
667
|
+
message: "Frame rejected (bad checksum, unterminated, or out of sequence), NAK sent, retransmit expected, never accepted.",
|
|
632
668
|
...frameNumber !== void 0 ? { frameNumber } : {}
|
|
633
669
|
};
|
|
634
670
|
}
|
|
@@ -736,7 +772,7 @@ function validateTolerations(tolerate, profileName) {
|
|
|
736
772
|
}
|
|
737
773
|
if (isSafetyCriticalCode(t.code)) {
|
|
738
774
|
throw new AstmProfileDefinitionError(
|
|
739
|
-
`Profile '${profileName}' may not tolerate '${t.code}'
|
|
775
|
+
`Profile '${profileName}' may not tolerate '${t.code}': 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: it can never make a bad checksum "ok" or a cancelled result read "final."`,
|
|
740
776
|
profileName
|
|
741
777
|
);
|
|
742
778
|
}
|
|
@@ -779,14 +815,14 @@ function defineAstmProfile(opts) {
|
|
|
779
815
|
// src/profiles/default.ts
|
|
780
816
|
var defaultProfile = defineAstmProfile({
|
|
781
817
|
name: "default",
|
|
782
|
-
description: "Conservative baseline
|
|
818
|
+
description: "Conservative baseline: tolerates nothing; every deviation surfaces as a warning.",
|
|
783
819
|
tolerate: []
|
|
784
820
|
});
|
|
785
821
|
|
|
786
822
|
// src/profiles/reference-corpus.ts
|
|
787
823
|
var referenceCorpus = defineAstmProfile({
|
|
788
824
|
name: "referenceCorpus",
|
|
789
|
-
description: "Non-standard-escape tolerance grounded firsthand in the escape-agnostic OSS reference corpus (python-astm / senaite). Syntactic encoding noise only
|
|
825
|
+
description: "Non-standard-escape tolerance grounded firsthand in the escape-agnostic OSS reference corpus (python-astm / senaite). Syntactic encoding noise only: the value is preserved verbatim, never a safety-critical value.",
|
|
790
826
|
provenance: {
|
|
791
827
|
source: "kxepal/python-astm codec.py (BSD) + senaite.astm / senaite.lis2a",
|
|
792
828
|
reference: "https://github.com/kxepal/python-astm/blob/master/astm/codec.py ; https://github.com/senaite/senaite.astm",
|
|
@@ -899,6 +935,17 @@ function tokenizeRecord(record, d, onUnknownEscape) {
|
|
|
899
935
|
const rawFields = splitEscapeAware(record, d.field, d.escape);
|
|
900
936
|
return rawFields.map((raw, fieldIndex) => toField(raw, d, () => onUnknownEscape?.(fieldIndex)));
|
|
901
937
|
}
|
|
938
|
+
function tokenizeHeader(record, d, onUnknownEscape) {
|
|
939
|
+
const defEnd = record.indexOf(d.field, 2);
|
|
940
|
+
const definition = defEnd === -1 ? record.slice(2) : record.slice(2, defEnd);
|
|
941
|
+
const head = [opaqueField(record.slice(0, Math.min(1, record.length))), opaqueField(definition)];
|
|
942
|
+
if (defEnd === -1) return head;
|
|
943
|
+
const data = tokenizeRecord(record.slice(defEnd + 1), d, (i) => onUnknownEscape?.(i + 2));
|
|
944
|
+
return [...head, ...data];
|
|
945
|
+
}
|
|
946
|
+
function opaqueField(raw) {
|
|
947
|
+
return { raw, components: [raw], repeats: [[raw]] };
|
|
948
|
+
}
|
|
902
949
|
function toField(raw, d, onUnknownEscape) {
|
|
903
950
|
const rawRepeats = splitEscapeAware(raw, d.repeat, d.escape);
|
|
904
951
|
const repeats = rawRepeats.map(
|
|
@@ -952,9 +999,13 @@ function parseAstmRecords(raw, options = {}) {
|
|
|
952
999
|
if (isNonStandard(delimiters)) {
|
|
953
1000
|
warnings.push(nonStandardDelimiters({ recordIndex: 0, recordType: "H" }));
|
|
954
1001
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1002
|
+
let active = delimiters;
|
|
1003
|
+
const built = lines.map((line2, recordIndex) => {
|
|
1004
|
+
if (recordIndex > 0 && line2.charAt(0) === "H") {
|
|
1005
|
+
active = adoptRedeclaredDelimiters(line2, active, recordIndex, warnings);
|
|
1006
|
+
}
|
|
1007
|
+
return buildRecord(line2, recordIndex, active, warnings);
|
|
1008
|
+
});
|
|
958
1009
|
const records = attachComments(built, warnings);
|
|
959
1010
|
const classification = classifyMessage(records);
|
|
960
1011
|
if (classification.hasQuery && classification.hasResults) {
|
|
@@ -984,6 +1035,21 @@ function resolveProfile(options) {
|
|
|
984
1035
|
if (options.profile !== void 0) return options.profile;
|
|
985
1036
|
return getDefaultAstmProfile();
|
|
986
1037
|
}
|
|
1038
|
+
function adoptRedeclaredDelimiters(line2, active, recordIndex, warnings) {
|
|
1039
|
+
const position = { recordIndex, recordType: "H" };
|
|
1040
|
+
const declared = readDelimiters(line2);
|
|
1041
|
+
if (declared === void 0) {
|
|
1042
|
+
warnings.push(unreadableRedeclaration(position));
|
|
1043
|
+
return active;
|
|
1044
|
+
}
|
|
1045
|
+
if (sameDelimiters(declared, active)) return active;
|
|
1046
|
+
warnings.push(delimitersRedeclared(position));
|
|
1047
|
+
if (isNonStandard(declared)) warnings.push(nonStandardDelimiters(position));
|
|
1048
|
+
return declared;
|
|
1049
|
+
}
|
|
1050
|
+
function sameDelimiters(a, b) {
|
|
1051
|
+
return a.field === b.field && a.repeat === b.repeat && a.component === b.component && a.escape === b.escape;
|
|
1052
|
+
}
|
|
987
1053
|
function decodeBytes(bytes) {
|
|
988
1054
|
let out = "";
|
|
989
1055
|
for (const b of bytes) out += String.fromCharCode(b);
|
|
@@ -994,11 +1060,12 @@ function astmField(fields, n) {
|
|
|
994
1060
|
}
|
|
995
1061
|
function buildRecord(line2, recordIndex, d, warnings) {
|
|
996
1062
|
const rawType = line2.charAt(0);
|
|
997
|
-
const
|
|
1063
|
+
const onUnknownEscape = (fieldIndex) => {
|
|
998
1064
|
warnings.push(
|
|
999
1065
|
unknownEscapeSequence({ recordIndex, recordType: rawType, fieldIndex: fieldIndex + 1 })
|
|
1000
1066
|
);
|
|
1001
|
-
}
|
|
1067
|
+
};
|
|
1068
|
+
const fields = rawType === "H" ? tokenizeHeader(line2, d, onUnknownEscape) : tokenizeRecord(line2, d, onUnknownEscape);
|
|
1002
1069
|
switch (rawType) {
|
|
1003
1070
|
case "H":
|
|
1004
1071
|
return {
|
|
@@ -1019,7 +1086,7 @@ function buildRecord(line2, recordIndex, d, warnings) {
|
|
|
1019
1086
|
case "Q":
|
|
1020
1087
|
return buildQuery(recordIndex, fields, warnings);
|
|
1021
1088
|
// `M` (manufacturer) and `S` (scientific) are vendor-defined free-form data surfaced VERBATIM and
|
|
1022
|
-
// NEVER interpreted into typed clinical fields
|
|
1089
|
+
// NEVER interpreted into typed clinical fields: the exact wire line is preserved for round-trip.
|
|
1023
1090
|
case "M":
|
|
1024
1091
|
return { type: "M", recordIndex, fields, rawLine: line2 };
|
|
1025
1092
|
case "S":
|
|
@@ -1052,7 +1119,7 @@ function buildPatient(recordIndex, fields, warnings) {
|
|
|
1052
1119
|
recordIndex,
|
|
1053
1120
|
fields,
|
|
1054
1121
|
...definedString("seq", fieldScalar(astmField(fields, 2))),
|
|
1055
|
-
// The three patient identifiers stay DISTINCT
|
|
1122
|
+
// The three patient identifiers stay DISTINCT: practice-assigned (3), laboratory-assigned (4),
|
|
1056
1123
|
// and a third ID (5) never collapse into one; conflating them is the primary misfiling path.
|
|
1057
1124
|
...definedString("practiceAssignedId", fieldScalar(astmField(fields, 3))),
|
|
1058
1125
|
...definedString("laboratoryAssignedId", fieldScalar(astmField(fields, 4))),
|
|
@@ -1075,7 +1142,7 @@ function buildOrder(recordIndex, fields) {
|
|
|
1075
1142
|
...hasContent(utidField) ? { universalTestId: recognizeUniversalTestId(utidField.components) } : {},
|
|
1076
1143
|
// Priority (6), action code (~12), and report type (~26) are surfaced verbatim. The `~` field
|
|
1077
1144
|
// indices and the code sets are `[OSS-derived]` (paywalled), so they are never mapped to a
|
|
1078
|
-
// guessed meaning
|
|
1145
|
+
// guessed meaning: see the JSDoc on `OrderRecord`.
|
|
1079
1146
|
...definedString("priority", fieldScalar(astmField(fields, 6))),
|
|
1080
1147
|
...definedString("actionCode", fieldScalar(astmField(fields, 12))),
|
|
1081
1148
|
...definedString("reportType", fieldScalar(astmField(fields, 26)))
|
|
@@ -1220,19 +1287,57 @@ function definedString(key, value) {
|
|
|
1220
1287
|
|
|
1221
1288
|
// src/records/serialize.ts
|
|
1222
1289
|
var AstmSerializeError = class extends Error {
|
|
1223
|
-
/**
|
|
1290
|
+
/**
|
|
1291
|
+
* Stable discriminant. `ASTM_EMIT_UNENCODABLE_VALUE` for a `CR`/`LF` in a value;
|
|
1292
|
+
* `ASTM_EMIT_INVALID_DELIMITERS` for a delimiter set that cannot be emitted
|
|
1293
|
+
* reversibly.
|
|
1294
|
+
*/
|
|
1224
1295
|
code;
|
|
1225
1296
|
/** 0-based ordinal of the record within the message, when known. */
|
|
1226
1297
|
recordIndex;
|
|
1227
1298
|
/** @internal */
|
|
1228
|
-
constructor(message, recordIndex) {
|
|
1299
|
+
constructor(message, recordIndex, code = "ASTM_EMIT_UNENCODABLE_VALUE") {
|
|
1229
1300
|
super(message);
|
|
1230
1301
|
this.name = "AstmSerializeError";
|
|
1231
|
-
this.code =
|
|
1302
|
+
this.code = code;
|
|
1232
1303
|
if (recordIndex !== void 0) this.recordIndex = recordIndex;
|
|
1233
1304
|
}
|
|
1234
1305
|
};
|
|
1306
|
+
var DELIMITER_ROLES = ["field", "repeat", "component", "escape"];
|
|
1307
|
+
var CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/;
|
|
1308
|
+
function assertEmittableDelimiters(d, recordIndex) {
|
|
1309
|
+
const invalid = (reason) => {
|
|
1310
|
+
throw new AstmSerializeError(
|
|
1311
|
+
`Cannot emit against these delimiters: ${reason}.`,
|
|
1312
|
+
recordIndex,
|
|
1313
|
+
"ASTM_EMIT_INVALID_DELIMITERS"
|
|
1314
|
+
);
|
|
1315
|
+
};
|
|
1316
|
+
if (typeof d !== "object" || d === null) {
|
|
1317
|
+
invalid("a delimiter set is required");
|
|
1318
|
+
}
|
|
1319
|
+
for (const role of DELIMITER_ROLES) {
|
|
1320
|
+
const char = d[role];
|
|
1321
|
+
if (typeof char !== "string" || char.length !== 1) {
|
|
1322
|
+
invalid(`the ${role} delimiter must be exactly one character`);
|
|
1323
|
+
}
|
|
1324
|
+
if (char === "\r" || char === "\n") {
|
|
1325
|
+
invalid(`the ${role} delimiter must not be a record terminator (CR/LF)`);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
DELIMITER_ROLES.forEach((a, i) => {
|
|
1329
|
+
for (const b of DELIMITER_ROLES.slice(i + 1)) {
|
|
1330
|
+
if (d[a] === d[b]) {
|
|
1331
|
+
invalid(`the ${a} and ${b} delimiters must not be the same character`);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1235
1336
|
function encodeComponent(leaf, d, recordIndex) {
|
|
1337
|
+
assertEmittableDelimiters(d, recordIndex);
|
|
1338
|
+
return encodeLeaf(leaf, d, recordIndex);
|
|
1339
|
+
}
|
|
1340
|
+
function encodeLeaf(leaf, d, recordIndex) {
|
|
1236
1341
|
if (leaf.includes("\r") || leaf.includes("\n")) {
|
|
1237
1342
|
throw new AstmSerializeError(
|
|
1238
1343
|
"A value contains a record terminator (CR/LF), which cannot be escaped without breaking framing.",
|
|
@@ -1242,28 +1347,58 @@ function encodeComponent(leaf, d, recordIndex) {
|
|
|
1242
1347
|
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
1348
|
}
|
|
1244
1349
|
function encodeField(repeats, d, recordIndex) {
|
|
1245
|
-
return repeats.map((rep) => rep.map((c) =>
|
|
1350
|
+
return repeats.map((rep) => rep.map((c) => encodeLeaf(c, d, recordIndex)).join(d.component)).join(d.repeat);
|
|
1246
1351
|
}
|
|
1247
1352
|
function serializeAstmRecord(record, d = CANONICAL_DELIMITERS) {
|
|
1248
|
-
|
|
1353
|
+
assertEmittableDelimiters(d, record.recordIndex);
|
|
1354
|
+
return serializeRecordChecked(record, d);
|
|
1355
|
+
}
|
|
1356
|
+
function serializeRecordChecked(record, d) {
|
|
1357
|
+
if (record.type === "M" || record.type === "S") return serializeVerbatimRecord(record, d);
|
|
1249
1358
|
if (record.type === "H") return serializeHeader(record, d);
|
|
1250
1359
|
return record.fields.map((f) => encodeField(f.repeats, d, record.recordIndex)).join(d.field);
|
|
1251
1360
|
}
|
|
1361
|
+
function serializeVerbatimRecord(record, d) {
|
|
1362
|
+
if (recoversVerbatim(record, d)) return record.rawLine;
|
|
1363
|
+
return record.fields.map((f) => encodeField(f.repeats, d, record.recordIndex)).join(d.field);
|
|
1364
|
+
}
|
|
1365
|
+
function recoversVerbatim(record, d) {
|
|
1366
|
+
return sameFieldTree(tokenizeRecord(record.rawLine, d), record.fields);
|
|
1367
|
+
}
|
|
1368
|
+
function sameFieldTree(a, b) {
|
|
1369
|
+
if (a.length !== b.length) return false;
|
|
1370
|
+
return a.every((fieldA, i) => {
|
|
1371
|
+
const fieldB = b[i];
|
|
1372
|
+
if (fieldB === void 0) return false;
|
|
1373
|
+
if (fieldA.repeats.length !== fieldB.repeats.length) return false;
|
|
1374
|
+
return fieldA.repeats.every((repA, r) => {
|
|
1375
|
+
const repB = fieldB.repeats[r];
|
|
1376
|
+
if (repB === void 0 || repA.length !== repB.length) return false;
|
|
1377
|
+
return repA.every((comp, c) => comp === repB[c]);
|
|
1378
|
+
});
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1252
1381
|
function serializeHeader(header, d) {
|
|
1253
|
-
const head = "H" + d.field + d.repeat + d.component + d.escape;
|
|
1254
|
-
const
|
|
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);
|
|
1382
|
+
const head = "H" + d.field + d.repeat + d.component + d.escape + declarationResidual(header, d);
|
|
1383
|
+
const dataFields = header.fields.slice(2);
|
|
1259
1384
|
const rest = dataFields.map((f) => d.field + encodeField(f.repeats, d, header.recordIndex)).join("");
|
|
1260
1385
|
return head + rest;
|
|
1261
1386
|
}
|
|
1387
|
+
function declarationResidual(header, d) {
|
|
1388
|
+
const declaration = header.fields[1]?.raw ?? "";
|
|
1389
|
+
const roles = d.repeat + d.component + d.escape;
|
|
1390
|
+
if (!declaration.startsWith(roles)) return "";
|
|
1391
|
+
const residual = declaration.slice(roles.length);
|
|
1392
|
+
if (residual.includes(d.field) || CONTROL_CHARACTER.test(residual)) return "";
|
|
1393
|
+
return residual;
|
|
1394
|
+
}
|
|
1262
1395
|
function serializeAstmRecords(input, d = CANONICAL_DELIMITERS) {
|
|
1396
|
+
assertEmittableDelimiters(d);
|
|
1263
1397
|
const records = Array.isArray(input) ? input : input.records;
|
|
1264
|
-
return records.map((r) =>
|
|
1398
|
+
return records.map((r) => serializeRecordChecked(r, d) + "\r").join("");
|
|
1265
1399
|
}
|
|
1266
1400
|
function serializeField(field, d = CANONICAL_DELIMITERS) {
|
|
1401
|
+
assertEmittableDelimiters(d);
|
|
1267
1402
|
return encodeField(field.repeats, d);
|
|
1268
1403
|
}
|
|
1269
1404
|
|
|
@@ -1398,17 +1533,108 @@ function buildAstmMessage(input) {
|
|
|
1398
1533
|
return lines.map((l) => l + "\r").join("");
|
|
1399
1534
|
}
|
|
1400
1535
|
|
|
1536
|
+
// src/records/messages.ts
|
|
1537
|
+
var AMBIGUOUS_CODES = {
|
|
1538
|
+
/** The stream carries more than one `H` … `L` message, so a stream-wide answer spans patients. */
|
|
1539
|
+
ASTM_AMBIGUOUS_MULTI_MESSAGE: "ASTM_AMBIGUOUS_MULTI_MESSAGE",
|
|
1540
|
+
/** The one message in the stream carries more than one `P`, so "the patient" is not determined. */
|
|
1541
|
+
ASTM_AMBIGUOUS_MULTI_PATIENT: "ASTM_AMBIGUOUS_MULTI_PATIENT"
|
|
1542
|
+
};
|
|
1543
|
+
var AstmAmbiguousStreamError = class extends Error {
|
|
1544
|
+
/** The stable discriminant: see {@link AMBIGUOUS_CODES}. */
|
|
1545
|
+
code;
|
|
1546
|
+
/** Where the ambiguity became visible: the second header, or the second `P`. Value-free. */
|
|
1547
|
+
position;
|
|
1548
|
+
/** How many `H` … `L` messages the stream carries. */
|
|
1549
|
+
messageCount;
|
|
1550
|
+
/** How many `P` records the message in question carries. */
|
|
1551
|
+
patientCount;
|
|
1552
|
+
/** @internal */
|
|
1553
|
+
constructor(code, message, position, counts) {
|
|
1554
|
+
super(message);
|
|
1555
|
+
this.name = "AstmAmbiguousStreamError";
|
|
1556
|
+
this.code = code;
|
|
1557
|
+
this.position = position;
|
|
1558
|
+
this.messageCount = counts.messageCount;
|
|
1559
|
+
this.patientCount = counts.patientCount;
|
|
1560
|
+
}
|
|
1561
|
+
};
|
|
1562
|
+
function messages(msg) {
|
|
1563
|
+
const groups = [];
|
|
1564
|
+
let current;
|
|
1565
|
+
for (const record of msg.records) {
|
|
1566
|
+
if (record.type === "H" || current === void 0) {
|
|
1567
|
+
current = [];
|
|
1568
|
+
groups.push(current);
|
|
1569
|
+
}
|
|
1570
|
+
current.push(record);
|
|
1571
|
+
}
|
|
1572
|
+
return Object.freeze(
|
|
1573
|
+
groups.map((records, index) => {
|
|
1574
|
+
const header = records.find((r) => r.type === "H") ?? msg.header;
|
|
1575
|
+
const patients = records.filter((r) => r.type === "P");
|
|
1576
|
+
return Object.freeze({
|
|
1577
|
+
index,
|
|
1578
|
+
header,
|
|
1579
|
+
delimiters: header.delimiters,
|
|
1580
|
+
records: Object.freeze(records),
|
|
1581
|
+
patient: patients.length === 1 ? patients[0] : void 0,
|
|
1582
|
+
patients: Object.freeze(patients),
|
|
1583
|
+
results: Object.freeze(records.filter((r) => r.type === "R")),
|
|
1584
|
+
orders: Object.freeze(records.filter((r) => r.type === "O")),
|
|
1585
|
+
comments: Object.freeze(records.filter((r) => r.type === "C")),
|
|
1586
|
+
queries: Object.freeze(records.filter((r) => r.type === "Q"))
|
|
1587
|
+
});
|
|
1588
|
+
})
|
|
1589
|
+
);
|
|
1590
|
+
}
|
|
1591
|
+
function surveyMessages(msg) {
|
|
1592
|
+
let count = 0;
|
|
1593
|
+
let secondHeader;
|
|
1594
|
+
for (const record of msg.records) {
|
|
1595
|
+
if (record.type !== "H") continue;
|
|
1596
|
+
count += 1;
|
|
1597
|
+
if (count === 2) secondHeader = record;
|
|
1598
|
+
}
|
|
1599
|
+
return { count, secondHeader };
|
|
1600
|
+
}
|
|
1601
|
+
function assertSingleMessage(msg, accessor) {
|
|
1602
|
+
const { count, secondHeader } = surveyMessages(msg);
|
|
1603
|
+
if (count <= 1) return;
|
|
1604
|
+
throw new AstmAmbiguousStreamError(
|
|
1605
|
+
AMBIGUOUS_CODES.ASTM_AMBIGUOUS_MULTI_MESSAGE,
|
|
1606
|
+
`${accessor} is scoped to the whole stream, and this stream carries ${String(count)} messages, so its answer would span patients. Use messages() and read each message's own records.`,
|
|
1607
|
+
{ recordIndex: secondHeader?.recordIndex ?? 0, recordType: "H" },
|
|
1608
|
+
{ messageCount: count, patientCount: 0 }
|
|
1609
|
+
);
|
|
1610
|
+
}
|
|
1611
|
+
function assertSinglePatient(msg, accessor) {
|
|
1612
|
+
const patients = msg.records.filter((r) => r.type === "P");
|
|
1613
|
+
if (patients.length <= 1) return;
|
|
1614
|
+
throw new AstmAmbiguousStreamError(
|
|
1615
|
+
AMBIGUOUS_CODES.ASTM_AMBIGUOUS_MULTI_PATIENT,
|
|
1616
|
+
`${accessor} cannot name one patient: this message carries ${String(patients.length)} P records. Read them from messages()[n].patients and pair each with the records it owns.`,
|
|
1617
|
+
{ recordIndex: patients[1]?.recordIndex ?? 0, recordType: "P" },
|
|
1618
|
+
{ messageCount: 1, patientCount: patients.length }
|
|
1619
|
+
);
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1401
1622
|
// src/records/extractors.ts
|
|
1402
1623
|
function results(msg) {
|
|
1624
|
+
assertSingleMessage(msg, "results()");
|
|
1403
1625
|
return msg.records.filter((r) => r.type === "R");
|
|
1404
1626
|
}
|
|
1405
1627
|
function patient(msg) {
|
|
1628
|
+
assertSingleMessage(msg, "patient()");
|
|
1629
|
+
assertSinglePatient(msg, "patient()");
|
|
1406
1630
|
return msg.records.find((r) => r.type === "P");
|
|
1407
1631
|
}
|
|
1408
1632
|
function orders(msg) {
|
|
1633
|
+
assertSingleMessage(msg, "orders()");
|
|
1409
1634
|
return msg.records.filter((r) => r.type === "O");
|
|
1410
1635
|
}
|
|
1411
1636
|
function comments(msg) {
|
|
1637
|
+
assertSingleMessage(msg, "comments()");
|
|
1412
1638
|
return msg.records.filter((r) => r.type === "C");
|
|
1413
1639
|
}
|
|
1414
1640
|
function commentsFor(msg, parent) {
|
|
@@ -1417,6 +1643,7 @@ function commentsFor(msg, parent) {
|
|
|
1417
1643
|
);
|
|
1418
1644
|
}
|
|
1419
1645
|
function query(msg) {
|
|
1646
|
+
assertSingleMessage(msg, "query()");
|
|
1420
1647
|
return msg.records.filter((r) => r.type === "Q");
|
|
1421
1648
|
}
|
|
1422
1649
|
|
|
@@ -1459,14 +1686,14 @@ function defineLivdCatalog(entries) {
|
|
|
1459
1686
|
var LIVD_WARNING_CODES = {
|
|
1460
1687
|
/**
|
|
1461
1688
|
* 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
|
|
1689
|
+
* entry for it. The code stays surfaced verbatim and the mapping is `unmapped`: a LOINC is
|
|
1463
1690
|
* **never** guessed. Purely advisory: the raw code and value are untouched.
|
|
1464
1691
|
*/
|
|
1465
1692
|
ASTM_LIVD_UNMAPPED_CODE: "ASTM_LIVD_UNMAPPED_CODE",
|
|
1466
1693
|
/**
|
|
1467
1694
|
* A vendor/local test code matched **more than one distinct LOINC** in the catalog (e.g. the same
|
|
1468
1695
|
* 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
|
|
1696
|
+
* candidate LOINCs are surfaced for inspection, but **none is chosen**: refusing to pick is the
|
|
1470
1697
|
* fail-safe, since guessing wrong mis-identifies the test.
|
|
1471
1698
|
*/
|
|
1472
1699
|
ASTM_LIVD_AMBIGUOUS_MAPPING: "ASTM_LIVD_AMBIGUOUS_MAPPING"
|
|
@@ -1474,14 +1701,14 @@ var LIVD_WARNING_CODES = {
|
|
|
1474
1701
|
function livdUnmappedCode(position) {
|
|
1475
1702
|
return {
|
|
1476
1703
|
code: LIVD_WARNING_CODES.ASTM_LIVD_UNMAPPED_CODE,
|
|
1477
|
-
message: "Reported test code had no LIVD mapping
|
|
1704
|
+
message: "Reported test code had no LIVD mapping, surfaced unmapped, never a guessed LOINC.",
|
|
1478
1705
|
position
|
|
1479
1706
|
};
|
|
1480
1707
|
}
|
|
1481
1708
|
function livdAmbiguousMapping(position) {
|
|
1482
1709
|
return {
|
|
1483
1710
|
code: LIVD_WARNING_CODES.ASTM_LIVD_AMBIGUOUS_MAPPING,
|
|
1484
|
-
message: "Reported test code mapped to multiple distinct LOINCs
|
|
1711
|
+
message: "Reported test code mapped to multiple distinct LOINCs, surfaced ambiguous, never one guessed.",
|
|
1485
1712
|
position
|
|
1486
1713
|
};
|
|
1487
1714
|
}
|
|
@@ -1925,7 +2152,7 @@ function encodeFrame(text, frameNumber, isFinal) {
|
|
|
1925
2152
|
}
|
|
1926
2153
|
function composeAstmFrames(records, options = {}) {
|
|
1927
2154
|
if (records.length === 0) {
|
|
1928
|
-
throw new AstmFrameEncodeError("Cannot frame an empty record list
|
|
2155
|
+
throw new AstmFrameEncodeError("Cannot frame an empty record list: nothing to transmit.");
|
|
1929
2156
|
}
|
|
1930
2157
|
const out = [];
|
|
1931
2158
|
let frameNumber = options.startFrameNumber ?? FIRST_FRAME_NUMBER;
|
|
@@ -1933,7 +2160,7 @@ function composeAstmFrames(records, options = {}) {
|
|
|
1933
2160
|
const bytes = toBytes(record);
|
|
1934
2161
|
if (bytes.length === 0) {
|
|
1935
2162
|
throw new AstmFrameEncodeError(
|
|
1936
|
-
"Cannot frame an empty record
|
|
2163
|
+
"Cannot frame an empty record: a frame must carry record bytes.",
|
|
1937
2164
|
recordIndex
|
|
1938
2165
|
);
|
|
1939
2166
|
}
|
|
@@ -1972,13 +2199,15 @@ function concatBytes3(chunks) {
|
|
|
1972
2199
|
}
|
|
1973
2200
|
|
|
1974
2201
|
// src/index.ts
|
|
1975
|
-
var VERSION = "0.0.
|
|
2202
|
+
var VERSION = "0.0.5";
|
|
1976
2203
|
|
|
1977
2204
|
exports.ALL_ASTM_WARNING_CODES = ALL_ASTM_WARNING_CODES;
|
|
2205
|
+
exports.AMBIGUOUS_CODES = AMBIGUOUS_CODES;
|
|
1978
2206
|
exports.ASTM_ACK = ACK;
|
|
1979
2207
|
exports.ASTM_ENQ = ENQ;
|
|
1980
2208
|
exports.ASTM_EOT = EOT;
|
|
1981
2209
|
exports.ASTM_NAK = NAK;
|
|
2210
|
+
exports.AstmAmbiguousStreamError = AstmAmbiguousStreamError;
|
|
1982
2211
|
exports.AstmFrameEncodeError = AstmFrameEncodeError;
|
|
1983
2212
|
exports.AstmFrameStrictError = AstmFrameStrictError;
|
|
1984
2213
|
exports.AstmParseError = AstmParseError;
|
|
@@ -2013,6 +2242,7 @@ exports.decodeEscapes = decodeEscapes;
|
|
|
2013
2242
|
exports.deepFreeze = deepFreeze;
|
|
2014
2243
|
exports.defineAstmProfile = defineAstmProfile;
|
|
2015
2244
|
exports.defineLivdCatalog = defineLivdCatalog;
|
|
2245
|
+
exports.delimitersRedeclared = delimitersRedeclared;
|
|
2016
2246
|
exports.detectFraming = detectFraming;
|
|
2017
2247
|
exports.encodeComponent = encodeComponent;
|
|
2018
2248
|
exports.fieldScalar = fieldScalar;
|
|
@@ -2035,6 +2265,7 @@ exports.ltpFrameRejected = ltpFrameRejected;
|
|
|
2035
2265
|
exports.ltpInitialState = ltpInitialState;
|
|
2036
2266
|
exports.ltpReduce = ltpReduce;
|
|
2037
2267
|
exports.ltpUnexpectedEvent = ltpUnexpectedEvent;
|
|
2268
|
+
exports.messages = messages;
|
|
2038
2269
|
exports.nonStandardDelimiters = nonStandardDelimiters;
|
|
2039
2270
|
exports.orders = orders;
|
|
2040
2271
|
exports.orphanComment = orphanComment;
|
|
@@ -2059,6 +2290,7 @@ exports.serializeFramedAstm = serializeFramedAstm;
|
|
|
2059
2290
|
exports.setDefaultAstmProfile = setDefaultAstmProfile;
|
|
2060
2291
|
exports.splitEscapeAware = splitEscapeAware;
|
|
2061
2292
|
exports.toChecksumHex = toChecksumHex;
|
|
2293
|
+
exports.tokenizeHeader = tokenizeHeader;
|
|
2062
2294
|
exports.tokenizeRecord = tokenizeRecord;
|
|
2063
2295
|
exports.undefinedAbnormalFlag = undefinedAbnormalFlag;
|
|
2064
2296
|
exports.undefinedResultStatus = undefinedResultStatus;
|
|
@@ -2067,5 +2299,6 @@ exports.unitsAbsent = unitsAbsent;
|
|
|
2067
2299
|
exports.unknownEscapeSequence = unknownEscapeSequence;
|
|
2068
2300
|
exports.unknownRecordType = unknownRecordType;
|
|
2069
2301
|
exports.unparseableReferenceRange = unparseableReferenceRange;
|
|
2302
|
+
exports.unreadableRedeclaration = unreadableRedeclaration;
|
|
2070
2303
|
//# sourceMappingURL=index.cjs.map
|
|
2071
2304
|
//# sourceMappingURL=index.cjs.map
|