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