@cosyte/astm 0.0.7 → 0.0.9
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 +215 -0
- package/README.md +84 -5
- package/dist/index.cjs +258 -159
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +404 -57
- package/dist/index.d.ts +404 -57
- package/dist/index.mjs +257 -160
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -119,12 +119,65 @@ function primaryCode(u) {
|
|
|
119
119
|
|
|
120
120
|
// src/common/warnings.ts
|
|
121
121
|
var WARNING_CODES = {
|
|
122
|
-
/**
|
|
122
|
+
/**
|
|
123
|
+
* A record's type letter is not one of the modeled types: surfaced as an unsupported record,
|
|
124
|
+
* never dropped. Treat it as a possible lost message boundary, because a header the reader did
|
|
125
|
+
* not recognize as one does not open a new message.
|
|
126
|
+
*/
|
|
123
127
|
ASTM_RECORD_UNKNOWN_TYPE: "ASTM_RECORD_UNKNOWN_TYPE",
|
|
128
|
+
/**
|
|
129
|
+
* The delimiters in force found **no field separator at all** in a record that carries content
|
|
130
|
+
* beyond its type letter, so the whole record read back as a single field and none of its modeled
|
|
131
|
+
* fields could be recovered. The raw line is surfaced intact and nothing is dropped, but on a
|
|
132
|
+
* result record it means the value, the units and the status are all absent from the parsed model.
|
|
133
|
+
*
|
|
134
|
+
* This is one signature of a record being read with a delimiter set that does not belong to it,
|
|
135
|
+
* which is how a delimiter-scoping mistake turns into lost values. It is reported rather than
|
|
136
|
+
* repaired, because recovering the fields would mean guessing which set the sender meant.
|
|
137
|
+
*
|
|
138
|
+
* **Its absence is not evidence that a record was read in its own set.** This tests one of the
|
|
139
|
+
* four delimiter roles, the field separator, and only in its total form, where that separator
|
|
140
|
+
* occurs in the line (unescaped). Two whole classes of the same loss are outside it: a foreign set
|
|
141
|
+
* whose field separator happens to occur somewhere in the line still splits (on the wrong
|
|
142
|
+
* boundaries, silently, and this can happen to one record inside a run of these warnings); and a
|
|
143
|
+
* set differing in the repeat, component or escape role usually splits into fields normally,
|
|
144
|
+
* where a mis-split component can cost a test identity while the value survives, and where an
|
|
145
|
+
* `&X&` sequence whose body is a delimiter is an opaque atom, so that delimiter does not split
|
|
146
|
+
* and every field after it shifts. The escape role's worst case has narrowed and not
|
|
147
|
+
* disappeared: an escape character heading no sequence is now read as a literal and reported
|
|
148
|
+
* under {@link WARNING_CODES.ASTM_UNPAIRED_ESCAPE_CHARACTER} rather than merging the rest of the
|
|
149
|
+
* record, but a delimiter swallowed inside an `&X&` body is reported only by
|
|
150
|
+
* {@link WARNING_CODES.ASTM_UNKNOWN_ESCAPE_SEQUENCE}, which is tolerable. Treat this code as a
|
|
151
|
+
* report that one record definitely lost its fields, never as a sweep that would have fired if
|
|
152
|
+
* any had.
|
|
153
|
+
*/
|
|
154
|
+
ASTM_RECORD_FIELDS_UNSEPARATED: "ASTM_RECORD_FIELDS_UNSEPARATED",
|
|
124
155
|
/** The header declared delimiters other than the canonical `H|\^&`: tolerated, noted. */
|
|
125
156
|
ASTM_NONSTANDARD_DELIMITERS: "ASTM_NONSTANDARD_DELIMITERS",
|
|
126
157
|
/** An escape sequence body was not one of `&F&`/`&S&`/`&R&`/`&E&`: preserved verbatim. */
|
|
127
158
|
ASTM_UNKNOWN_ESCAPE_SEQUENCE: "ASTM_UNKNOWN_ESCAPE_SEQUENCE",
|
|
159
|
+
/**
|
|
160
|
+
* An escape character appeared where no escape sequence starts (an escape sequence is the escape
|
|
161
|
+
* character, one body character, and the escape character again). It is read as the **literal
|
|
162
|
+
* character it is** and kept byte-for-byte in the decoded value. Nothing is dropped and no byte is
|
|
163
|
+
* invented: this flags that the sender did not write the character the spec-clean way, which
|
|
164
|
+
* is `&E&`.
|
|
165
|
+
*
|
|
166
|
+
* **What it replaced is the reason it exists.** The codec used to read such a character as the
|
|
167
|
+
* opening of a sequence that never closed and merge the whole remainder of the record into the
|
|
168
|
+
* field it sat in, reporting nothing at all. One `&` in a result value cost the units, the
|
|
169
|
+
* abnormal flag and the status together and left the status reading `unspecified` rather than
|
|
170
|
+
* `final`; one in a surname cost the patient's birth date and sex. Emit then re-escaped the merged
|
|
171
|
+
* text into a spec-clean-looking line that read back as the same wrong value, so the mis-read
|
|
172
|
+
* survived a round trip without ever surfacing.
|
|
173
|
+
*
|
|
174
|
+
* **This code is not a statement about the rest of the record.** It reports one character. A
|
|
175
|
+
* *different* escape character in the same record may still head a real three-character sequence,
|
|
176
|
+
* and if that sequence's body happens to be a delimiter (`&|&` under the canonical set) the atom
|
|
177
|
+
* rule means that delimiter does not split. That case is reported separately, under
|
|
178
|
+
* {@link WARNING_CODES.ASTM_UNKNOWN_ESCAPE_SEQUENCE}, and it can still cost a field boundary.
|
|
179
|
+
*/
|
|
180
|
+
ASTM_UNPAIRED_ESCAPE_CHARACTER: "ASTM_UNPAIRED_ESCAPE_CHARACTER",
|
|
128
181
|
/**
|
|
129
182
|
* A result value field carried an *unescaped* component delimiter, so it split into more than one
|
|
130
183
|
* component. Both the full raw value and the split are surfaced and this warning fires: the
|
|
@@ -212,7 +265,14 @@ var WARNING_CODES = {
|
|
|
212
265
|
function unknownRecordType(position) {
|
|
213
266
|
return {
|
|
214
267
|
code: WARNING_CODES.ASTM_RECORD_UNKNOWN_TYPE,
|
|
215
|
-
message: "Unrecognized record type, surfaced verbatim as an unsupported record.",
|
|
268
|
+
message: "Unrecognized record type, surfaced verbatim as an unsupported record. If this record was a header, message grouping did not open a new message here.",
|
|
269
|
+
position
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function fieldsUnseparated(position) {
|
|
273
|
+
return {
|
|
274
|
+
code: WARNING_CODES.ASTM_RECORD_FIELDS_UNSEPARATED,
|
|
275
|
+
message: "The delimiters in force found no field separator in this record, so its whole content read back as one field and none of its modeled fields could be recovered. The raw line is surfaced intact, and no field is reconstructed, because the set the sender used is unknown.",
|
|
216
276
|
position
|
|
217
277
|
};
|
|
218
278
|
}
|
|
@@ -244,6 +304,13 @@ function unknownEscapeSequence(position) {
|
|
|
244
304
|
position
|
|
245
305
|
};
|
|
246
306
|
}
|
|
307
|
+
function unpairedEscapeCharacter(position) {
|
|
308
|
+
return {
|
|
309
|
+
code: WARNING_CODES.ASTM_UNPAIRED_ESCAPE_CHARACTER,
|
|
310
|
+
message: "An escape character headed no escape sequence, read as the literal character it is rather than opening a sequence that never closes. The spec-clean form is the escaped one.",
|
|
311
|
+
position
|
|
312
|
+
};
|
|
313
|
+
}
|
|
247
314
|
function ambiguousValueSplit(position) {
|
|
248
315
|
return {
|
|
249
316
|
code: WARNING_CODES.ASTM_RECORD_AMBIGUOUS_VALUE_SPLIT,
|
|
@@ -412,17 +479,157 @@ function classifyMessage(records) {
|
|
|
412
479
|
let hasQuery = false;
|
|
413
480
|
let hasResults = false;
|
|
414
481
|
let hasOrders = false;
|
|
482
|
+
let hasUnrecognized = false;
|
|
415
483
|
for (const r of records) {
|
|
416
484
|
if (r.type === "Q") hasQuery = true;
|
|
417
485
|
else if (r.type === "R") hasResults = true;
|
|
418
486
|
else if (r.type === "O") hasOrders = true;
|
|
487
|
+
else if (r.type === "unsupported") hasUnrecognized = true;
|
|
419
488
|
}
|
|
420
489
|
let kind;
|
|
421
490
|
if (hasQuery) kind = "host-query";
|
|
491
|
+
else if (hasUnrecognized) kind = "indeterminate";
|
|
422
492
|
else if (hasResults) kind = "results";
|
|
423
493
|
else if (hasOrders) kind = "orders";
|
|
424
494
|
else kind = "indeterminate";
|
|
425
|
-
return {
|
|
495
|
+
return {
|
|
496
|
+
kind,
|
|
497
|
+
hasQuery,
|
|
498
|
+
hasResults,
|
|
499
|
+
hasOrders,
|
|
500
|
+
hasUnrecognized,
|
|
501
|
+
isHostQueryRequest: kind === "host-query"
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/frames/warnings.ts
|
|
506
|
+
var FRAME_WARNING_CODES = {
|
|
507
|
+
/**
|
|
508
|
+
* A frame's two-hex-char checksum did not match the modulo-256 sum recomputed over its bytes. The
|
|
509
|
+
* frame is surfaced with `trusted: false` and its text is **never merged** into a reassembled
|
|
510
|
+
* record (default **warn** in lenient mode, escalated to a thrown error in `strict`). Corruption is
|
|
511
|
+
* never silently trusted: the "checksums are routinely not validated" claim was refuted; we
|
|
512
|
+
* validate.
|
|
513
|
+
*/
|
|
514
|
+
ASTM_FRAME_BAD_CHECKSUM: "ASTM_FRAME_BAD_CHECKSUM",
|
|
515
|
+
/**
|
|
516
|
+
* A frame's sequence number was not the expected next value (`1 → … → 7 → 0 → …`): a frame was
|
|
517
|
+
* possibly dropped. The stream is **never silently concatenated across the gap** as if contiguous;
|
|
518
|
+
* the in-progress record is tainted and not emitted as a clean reassembly.
|
|
519
|
+
*/
|
|
520
|
+
ASTM_FRAME_SEQUENCE_GAP: "ASTM_FRAME_SEQUENCE_GAP",
|
|
521
|
+
/**
|
|
522
|
+
* A frame opened with `STX` but no valid terminator + checksum was found before the stream ended
|
|
523
|
+
* (or before the next `STX`), or a record's frames ended on an intermediate `ETB` with no final
|
|
524
|
+
* `ETX`. The partial bytes are surfaced flagged untrusted and **no partial record is invented**
|
|
525
|
+
* (warn in lenient, thrown in `strict`).
|
|
526
|
+
*/
|
|
527
|
+
ASTM_FRAME_UNTERMINATED: "ASTM_FRAME_UNTERMINATED",
|
|
528
|
+
/**
|
|
529
|
+
* A frame's record text exceeded the 240-byte limit without a split. The frame is still surfaced
|
|
530
|
+
* (and, if its checksum validates, reassembled): the deviation is flagged, not silently dropped
|
|
531
|
+
* (warn in lenient, thrown in `strict`).
|
|
532
|
+
*/
|
|
533
|
+
ASTM_FRAME_OVERSIZE: "ASTM_FRAME_OVERSIZE"
|
|
534
|
+
};
|
|
535
|
+
function frameBadChecksum(position) {
|
|
536
|
+
return {
|
|
537
|
+
code: FRAME_WARNING_CODES.ASTM_FRAME_BAD_CHECKSUM,
|
|
538
|
+
message: "Frame checksum mismatch, surfaced untrusted, never merged into a record.",
|
|
539
|
+
position
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
function frameSequenceGap(position) {
|
|
543
|
+
return {
|
|
544
|
+
code: FRAME_WARNING_CODES.ASTM_FRAME_SEQUENCE_GAP,
|
|
545
|
+
message: "Frame number out of sequence, possible dropped frame, never silently bridged.",
|
|
546
|
+
position
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
function frameUnterminated(position) {
|
|
550
|
+
return {
|
|
551
|
+
code: FRAME_WARNING_CODES.ASTM_FRAME_UNTERMINATED,
|
|
552
|
+
message: "Unterminated frame, partial bytes surfaced untrusted, no partial record invented.",
|
|
553
|
+
position
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
function frameOversize(position) {
|
|
557
|
+
return {
|
|
558
|
+
code: FRAME_WARNING_CODES.ASTM_FRAME_OVERSIZE,
|
|
559
|
+
message: "Frame text exceeded the 240-byte limit without a split, flagged, never dropped.",
|
|
560
|
+
position
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// src/ltp/warnings.ts
|
|
565
|
+
var LTP_WARNING_CODES = {
|
|
566
|
+
/**
|
|
567
|
+
* The transport detector could not tell a framed stream from a raw (unframed) one from its leading
|
|
568
|
+
* byte: it was neither `STX`/`ENQ` nor a bare record letter. It **defaults to framed** and warns,
|
|
569
|
+
* never guessing silently into data loss; a profile override forces the mode. (cobas b121 drops
|
|
570
|
+
* framing over TCP; cobas 4800 / Iguana retain it, both realities exist, so an unrecognizable lead
|
|
571
|
+
* is defaulted, not assumed.)
|
|
572
|
+
*/
|
|
573
|
+
ASTM_LTP_AMBIGUOUS_TRANSPORT: "ASTM_LTP_AMBIGUOUS_TRANSPORT",
|
|
574
|
+
/**
|
|
575
|
+
* A control event arrived in a protocol state that did not expect it: e.g. an inbound `ACK`/`NAK`
|
|
576
|
+
* at a receiver (which sends, never receives, those), or an `ENQ` mid-transfer. The event is
|
|
577
|
+
* surfaced and handled defensively (an unexpected `ACK`/`NAK` is **never** read as acceptance of
|
|
578
|
+
* data); it never advances the transfer as if valid.
|
|
579
|
+
*/
|
|
580
|
+
ASTM_LTP_UNEXPECTED_EVENT: "ASTM_LTP_UNEXPECTED_EVENT",
|
|
581
|
+
/**
|
|
582
|
+
* The receiver rejected a received frame with a `NAK` instead of accepting it: because its
|
|
583
|
+
* checksum failed, it was unterminated, or its frame number was out of sequence. The frame's text
|
|
584
|
+
* is **never** appended to the record and the transfer does **not** advance; the receiver awaits the
|
|
585
|
+
* sender's retransmit. This is the protocol-level face of the frame codec's fail-safe: a bad frame
|
|
586
|
+
* drives retransmit, not acceptance.
|
|
587
|
+
*/
|
|
588
|
+
ASTM_LTP_FRAME_REJECTED: "ASTM_LTP_FRAME_REJECTED"
|
|
589
|
+
};
|
|
590
|
+
function ltpAmbiguousTransport() {
|
|
591
|
+
return {
|
|
592
|
+
code: LTP_WARNING_CODES.ASTM_LTP_AMBIGUOUS_TRANSPORT,
|
|
593
|
+
message: "Leading byte was neither a frame start (STX/ENQ) nor a record letter, defaulted to framed; override with a profile to force raw."
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
function ltpUnexpectedEvent(frameNumber) {
|
|
597
|
+
return {
|
|
598
|
+
code: LTP_WARNING_CODES.ASTM_LTP_UNEXPECTED_EVENT,
|
|
599
|
+
message: "Control event arrived in a state that did not expect it, handled defensively, never read as acceptance.",
|
|
600
|
+
...frameNumber !== void 0 ? { frameNumber } : {}
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
function ltpFrameRejected(frameNumber) {
|
|
604
|
+
return {
|
|
605
|
+
code: LTP_WARNING_CODES.ASTM_LTP_FRAME_REJECTED,
|
|
606
|
+
message: "Frame rejected (bad checksum, unterminated, or out of sequence), NAK sent, retransmit expected, never accepted.",
|
|
607
|
+
...frameNumber !== void 0 ? { frameNumber } : {}
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// src/profiles/safety.ts
|
|
612
|
+
var TOLERABLE_CODES = Object.freeze(
|
|
613
|
+
/* @__PURE__ */ new Set([
|
|
614
|
+
WARNING_CODES.ASTM_NONSTANDARD_DELIMITERS,
|
|
615
|
+
WARNING_CODES.ASTM_UNKNOWN_ESCAPE_SEQUENCE,
|
|
616
|
+
WARNING_CODES.ASTM_UNPAIRED_ESCAPE_CHARACTER,
|
|
617
|
+
WARNING_CODES.ASTM_RECORD_UNINTERPRETED_QUERY_STATUS
|
|
618
|
+
])
|
|
619
|
+
);
|
|
620
|
+
var ALL_WARNING_CODES = [
|
|
621
|
+
...Object.values(WARNING_CODES),
|
|
622
|
+
...Object.values(FRAME_WARNING_CODES),
|
|
623
|
+
...Object.values(LTP_WARNING_CODES)
|
|
624
|
+
];
|
|
625
|
+
var ALL_ASTM_WARNING_CODES = Object.freeze(
|
|
626
|
+
new Set(ALL_WARNING_CODES)
|
|
627
|
+
);
|
|
628
|
+
var SAFETY_CRITICAL_CODES = Object.freeze(
|
|
629
|
+
new Set(ALL_WARNING_CODES.filter((c) => !TOLERABLE_CODES.has(c)))
|
|
630
|
+
);
|
|
631
|
+
function isSafetyCriticalCode(code) {
|
|
632
|
+
return SAFETY_CRITICAL_CODES.has(code);
|
|
426
633
|
}
|
|
427
634
|
|
|
428
635
|
// src/profiles/apply.ts
|
|
@@ -440,6 +647,7 @@ function toleranceApplies(tolerance, warning) {
|
|
|
440
647
|
}
|
|
441
648
|
function applyAstmProfile(profile, warning) {
|
|
442
649
|
if (warning.expected === true) return warning;
|
|
650
|
+
if (isSafetyCriticalCode(warning.code)) return warning;
|
|
443
651
|
for (const tolerance of profile.tolerate) {
|
|
444
652
|
if (toleranceApplies(tolerance, warning)) {
|
|
445
653
|
return profileQuirkApplied(warning, profile.name);
|
|
@@ -561,136 +769,6 @@ var AstmProfileDefinitionError = class extends Error {
|
|
|
561
769
|
}
|
|
562
770
|
};
|
|
563
771
|
|
|
564
|
-
// src/frames/warnings.ts
|
|
565
|
-
var FRAME_WARNING_CODES = {
|
|
566
|
-
/**
|
|
567
|
-
* A frame's two-hex-char checksum did not match the modulo-256 sum recomputed over its bytes. The
|
|
568
|
-
* frame is surfaced with `trusted: false` and its text is **never merged** into a reassembled
|
|
569
|
-
* record (default **warn** in lenient mode, escalated to a thrown error in `strict`). Corruption is
|
|
570
|
-
* never silently trusted: the "checksums are routinely not validated" claim was refuted; we
|
|
571
|
-
* validate.
|
|
572
|
-
*/
|
|
573
|
-
ASTM_FRAME_BAD_CHECKSUM: "ASTM_FRAME_BAD_CHECKSUM",
|
|
574
|
-
/**
|
|
575
|
-
* A frame's sequence number was not the expected next value (`1 → … → 7 → 0 → …`): a frame was
|
|
576
|
-
* possibly dropped. The stream is **never silently concatenated across the gap** as if contiguous;
|
|
577
|
-
* the in-progress record is tainted and not emitted as a clean reassembly.
|
|
578
|
-
*/
|
|
579
|
-
ASTM_FRAME_SEQUENCE_GAP: "ASTM_FRAME_SEQUENCE_GAP",
|
|
580
|
-
/**
|
|
581
|
-
* A frame opened with `STX` but no valid terminator + checksum was found before the stream ended
|
|
582
|
-
* (or before the next `STX`), or a record's frames ended on an intermediate `ETB` with no final
|
|
583
|
-
* `ETX`. The partial bytes are surfaced flagged untrusted and **no partial record is invented**
|
|
584
|
-
* (warn in lenient, thrown in `strict`).
|
|
585
|
-
*/
|
|
586
|
-
ASTM_FRAME_UNTERMINATED: "ASTM_FRAME_UNTERMINATED",
|
|
587
|
-
/**
|
|
588
|
-
* A frame's record text exceeded the 240-byte limit without a split. The frame is still surfaced
|
|
589
|
-
* (and, if its checksum validates, reassembled): the deviation is flagged, not silently dropped
|
|
590
|
-
* (warn in lenient, thrown in `strict`).
|
|
591
|
-
*/
|
|
592
|
-
ASTM_FRAME_OVERSIZE: "ASTM_FRAME_OVERSIZE"
|
|
593
|
-
};
|
|
594
|
-
function frameBadChecksum(position) {
|
|
595
|
-
return {
|
|
596
|
-
code: FRAME_WARNING_CODES.ASTM_FRAME_BAD_CHECKSUM,
|
|
597
|
-
message: "Frame checksum mismatch, surfaced untrusted, never merged into a record.",
|
|
598
|
-
position
|
|
599
|
-
};
|
|
600
|
-
}
|
|
601
|
-
function frameSequenceGap(position) {
|
|
602
|
-
return {
|
|
603
|
-
code: FRAME_WARNING_CODES.ASTM_FRAME_SEQUENCE_GAP,
|
|
604
|
-
message: "Frame number out of sequence, possible dropped frame, never silently bridged.",
|
|
605
|
-
position
|
|
606
|
-
};
|
|
607
|
-
}
|
|
608
|
-
function frameUnterminated(position) {
|
|
609
|
-
return {
|
|
610
|
-
code: FRAME_WARNING_CODES.ASTM_FRAME_UNTERMINATED,
|
|
611
|
-
message: "Unterminated frame, partial bytes surfaced untrusted, no partial record invented.",
|
|
612
|
-
position
|
|
613
|
-
};
|
|
614
|
-
}
|
|
615
|
-
function frameOversize(position) {
|
|
616
|
-
return {
|
|
617
|
-
code: FRAME_WARNING_CODES.ASTM_FRAME_OVERSIZE,
|
|
618
|
-
message: "Frame text exceeded the 240-byte limit without a split, flagged, never dropped.",
|
|
619
|
-
position
|
|
620
|
-
};
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
// src/ltp/warnings.ts
|
|
624
|
-
var LTP_WARNING_CODES = {
|
|
625
|
-
/**
|
|
626
|
-
* The transport detector could not tell a framed stream from a raw (unframed) one from its leading
|
|
627
|
-
* byte: it was neither `STX`/`ENQ` nor a bare record letter. It **defaults to framed** and warns,
|
|
628
|
-
* never guessing silently into data loss; a profile override forces the mode. (cobas b121 drops
|
|
629
|
-
* framing over TCP; cobas 4800 / Iguana retain it, both realities exist, so an unrecognizable lead
|
|
630
|
-
* is defaulted, not assumed.)
|
|
631
|
-
*/
|
|
632
|
-
ASTM_LTP_AMBIGUOUS_TRANSPORT: "ASTM_LTP_AMBIGUOUS_TRANSPORT",
|
|
633
|
-
/**
|
|
634
|
-
* A control event arrived in a protocol state that did not expect it: e.g. an inbound `ACK`/`NAK`
|
|
635
|
-
* at a receiver (which sends, never receives, those), or an `ENQ` mid-transfer. The event is
|
|
636
|
-
* surfaced and handled defensively (an unexpected `ACK`/`NAK` is **never** read as acceptance of
|
|
637
|
-
* data); it never advances the transfer as if valid.
|
|
638
|
-
*/
|
|
639
|
-
ASTM_LTP_UNEXPECTED_EVENT: "ASTM_LTP_UNEXPECTED_EVENT",
|
|
640
|
-
/**
|
|
641
|
-
* The receiver rejected a received frame with a `NAK` instead of accepting it: because its
|
|
642
|
-
* checksum failed, it was unterminated, or its frame number was out of sequence. The frame's text
|
|
643
|
-
* is **never** appended to the record and the transfer does **not** advance; the receiver awaits the
|
|
644
|
-
* sender's retransmit. This is the protocol-level face of the frame codec's fail-safe: a bad frame
|
|
645
|
-
* drives retransmit, not acceptance.
|
|
646
|
-
*/
|
|
647
|
-
ASTM_LTP_FRAME_REJECTED: "ASTM_LTP_FRAME_REJECTED"
|
|
648
|
-
};
|
|
649
|
-
function ltpAmbiguousTransport() {
|
|
650
|
-
return {
|
|
651
|
-
code: LTP_WARNING_CODES.ASTM_LTP_AMBIGUOUS_TRANSPORT,
|
|
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."
|
|
653
|
-
};
|
|
654
|
-
}
|
|
655
|
-
function ltpUnexpectedEvent(frameNumber) {
|
|
656
|
-
return {
|
|
657
|
-
code: LTP_WARNING_CODES.ASTM_LTP_UNEXPECTED_EVENT,
|
|
658
|
-
message: "Control event arrived in a state that did not expect it, handled defensively, never read as acceptance.",
|
|
659
|
-
...frameNumber !== void 0 ? { frameNumber } : {}
|
|
660
|
-
};
|
|
661
|
-
}
|
|
662
|
-
function ltpFrameRejected(frameNumber) {
|
|
663
|
-
return {
|
|
664
|
-
code: LTP_WARNING_CODES.ASTM_LTP_FRAME_REJECTED,
|
|
665
|
-
message: "Frame rejected (bad checksum, unterminated, or out of sequence), NAK sent, retransmit expected, never accepted.",
|
|
666
|
-
...frameNumber !== void 0 ? { frameNumber } : {}
|
|
667
|
-
};
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// src/profiles/safety.ts
|
|
671
|
-
var TOLERABLE_CODES = Object.freeze(
|
|
672
|
-
/* @__PURE__ */ new Set([
|
|
673
|
-
WARNING_CODES.ASTM_RECORD_UNKNOWN_TYPE,
|
|
674
|
-
WARNING_CODES.ASTM_NONSTANDARD_DELIMITERS,
|
|
675
|
-
WARNING_CODES.ASTM_UNKNOWN_ESCAPE_SEQUENCE,
|
|
676
|
-
WARNING_CODES.ASTM_RECORD_UNINTERPRETED_QUERY_STATUS
|
|
677
|
-
])
|
|
678
|
-
);
|
|
679
|
-
var ALL_WARNING_CODES = [
|
|
680
|
-
...Object.values(WARNING_CODES),
|
|
681
|
-
...Object.values(FRAME_WARNING_CODES),
|
|
682
|
-
...Object.values(LTP_WARNING_CODES)
|
|
683
|
-
];
|
|
684
|
-
var ALL_ASTM_WARNING_CODES = Object.freeze(
|
|
685
|
-
new Set(ALL_WARNING_CODES)
|
|
686
|
-
);
|
|
687
|
-
var SAFETY_CRITICAL_CODES = Object.freeze(
|
|
688
|
-
new Set(ALL_WARNING_CODES.filter((c) => !TOLERABLE_CODES.has(c)))
|
|
689
|
-
);
|
|
690
|
-
function isSafetyCriticalCode(code) {
|
|
691
|
-
return SAFETY_CRITICAL_CODES.has(code);
|
|
692
|
-
}
|
|
693
|
-
|
|
694
772
|
// src/profiles/validate.ts
|
|
695
773
|
var KNOWN_OPTION_KEYS = [
|
|
696
774
|
"name",
|
|
@@ -770,7 +848,7 @@ function validateTolerations(tolerate, profileName) {
|
|
|
770
848
|
}
|
|
771
849
|
if (isSafetyCriticalCode(t.code)) {
|
|
772
850
|
throw new AstmProfileDefinitionError(
|
|
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"
|
|
851
|
+
`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, an unrecognized record type, or a frame / LTP integrity warning). A profile quiets benign structural noise, never a deviation that could change a clinical reading, report a lost message boundary, or corrupt the wire: it can never make a bad checksum "ok", a cancelled result read "final", or quiet the warning that says two messages were read as one.`,
|
|
774
852
|
profileName
|
|
775
853
|
);
|
|
776
854
|
}
|
|
@@ -820,7 +898,7 @@ var defaultProfile = defineAstmProfile({
|
|
|
820
898
|
// src/profiles/reference-corpus.ts
|
|
821
899
|
var referenceCorpus = defineAstmProfile({
|
|
822
900
|
name: "referenceCorpus",
|
|
823
|
-
description: "Non-standard-escape tolerance grounded firsthand in the escape-agnostic OSS reference corpus (python-astm / senaite).
|
|
901
|
+
description: "Non-standard-escape tolerance grounded firsthand in the escape-agnostic OSS reference corpus (python-astm / senaite). Mostly syntactic encoding noise, with one measured exception: an escape sequence whose body is itself a delimiter is an opaque atom, so that delimiter does not split and a result can lose its units and its status. Read the values; do not rely on this profile leaving only cosmetic deviations behind.",
|
|
824
902
|
provenance: {
|
|
825
903
|
source: "kxepal/python-astm codec.py (BSD) + senaite.astm / senaite.lis2a",
|
|
826
904
|
reference: "https://github.com/kxepal/python-astm/blob/master/astm/codec.py ; https://github.com/senaite/senaite.astm",
|
|
@@ -830,7 +908,7 @@ var referenceCorpus = defineAstmProfile({
|
|
|
830
908
|
tolerate: [
|
|
831
909
|
{
|
|
832
910
|
code: "ASTM_UNKNOWN_ESCAPE_SEQUENCE",
|
|
833
|
-
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
|
|
911
|
+
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. NOT always non-clinical, and this is measured rather than assumed: where the body is a delimiter, that delimiter never became a field boundary, so `28.6&|&U/L` reads as one value with no units and no final status. Tolerating this code hides that too."
|
|
834
912
|
}
|
|
835
913
|
]
|
|
836
914
|
});
|
|
@@ -855,7 +933,10 @@ function getDefaultAstmProfile() {
|
|
|
855
933
|
}
|
|
856
934
|
|
|
857
935
|
// src/common/escapes.ts
|
|
858
|
-
function
|
|
936
|
+
function isEscapeSequenceAt(text, i, escape) {
|
|
937
|
+
return text.charAt(i) === escape && text.charAt(i + 2) === escape;
|
|
938
|
+
}
|
|
939
|
+
function decodeEscapes(leaf, d, onUnknown, onUnpaired) {
|
|
859
940
|
const esc = d.escape;
|
|
860
941
|
if (!leaf.includes(esc)) return leaf;
|
|
861
942
|
let out = "";
|
|
@@ -867,12 +948,13 @@ function decodeEscapes(leaf, d, onUnknown) {
|
|
|
867
948
|
i += 1;
|
|
868
949
|
continue;
|
|
869
950
|
}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
951
|
+
if (!isEscapeSequenceAt(leaf, i, esc)) {
|
|
952
|
+
out += ch;
|
|
953
|
+
i += 1;
|
|
954
|
+
onUnpaired?.();
|
|
955
|
+
continue;
|
|
874
956
|
}
|
|
875
|
-
const body = leaf.
|
|
957
|
+
const body = leaf.charAt(i + 1);
|
|
876
958
|
const replacement = escapeBody(body, d);
|
|
877
959
|
if (replacement === void 0) {
|
|
878
960
|
out += esc + body + esc;
|
|
@@ -880,7 +962,7 @@ function decodeEscapes(leaf, d, onUnknown) {
|
|
|
880
962
|
} else {
|
|
881
963
|
out += replacement;
|
|
882
964
|
}
|
|
883
|
-
i
|
|
965
|
+
i += 3;
|
|
884
966
|
}
|
|
885
967
|
return out;
|
|
886
968
|
}
|
|
@@ -905,14 +987,9 @@ function splitEscapeAware(text, delimiter, escape) {
|
|
|
905
987
|
let i = 0;
|
|
906
988
|
while (i < text.length) {
|
|
907
989
|
const ch = text.charAt(i);
|
|
908
|
-
if (ch === escape) {
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
current += text.slice(i);
|
|
912
|
-
break;
|
|
913
|
-
}
|
|
914
|
-
current += text.slice(i, close + 1);
|
|
915
|
-
i = close + 1;
|
|
990
|
+
if (ch === escape && isEscapeSequenceAt(text, i, escape)) {
|
|
991
|
+
current += text.slice(i, i + 3);
|
|
992
|
+
i += 3;
|
|
916
993
|
continue;
|
|
917
994
|
}
|
|
918
995
|
if (ch === delimiter) {
|
|
@@ -929,26 +1006,38 @@ function splitEscapeAware(text, delimiter, escape) {
|
|
|
929
1006
|
}
|
|
930
1007
|
|
|
931
1008
|
// src/records/tokenize.ts
|
|
932
|
-
function tokenizeRecord(record, d, onUnknownEscape) {
|
|
1009
|
+
function tokenizeRecord(record, d, onUnknownEscape, onUnpairedEscape) {
|
|
933
1010
|
const rawFields = splitEscapeAware(record, d.field, d.escape);
|
|
934
|
-
return rawFields.map(
|
|
1011
|
+
return rawFields.map(
|
|
1012
|
+
(raw, fieldIndex) => toField(
|
|
1013
|
+
raw,
|
|
1014
|
+
d,
|
|
1015
|
+
() => onUnknownEscape?.(fieldIndex),
|
|
1016
|
+
() => onUnpairedEscape?.(fieldIndex)
|
|
1017
|
+
)
|
|
1018
|
+
);
|
|
935
1019
|
}
|
|
936
|
-
function tokenizeHeader(record, d, onUnknownEscape) {
|
|
1020
|
+
function tokenizeHeader(record, d, onUnknownEscape, onUnpairedEscape) {
|
|
937
1021
|
const defEnd = record.indexOf(d.field, 2);
|
|
938
1022
|
const definition = defEnd === -1 ? record.slice(2) : record.slice(2, defEnd);
|
|
939
1023
|
const head = [opaqueField(record.slice(0, Math.min(1, record.length))), opaqueField(definition)];
|
|
940
1024
|
if (defEnd === -1) return head;
|
|
941
|
-
const data = tokenizeRecord(
|
|
1025
|
+
const data = tokenizeRecord(
|
|
1026
|
+
record.slice(defEnd + 1),
|
|
1027
|
+
d,
|
|
1028
|
+
(i) => onUnknownEscape?.(i + 2),
|
|
1029
|
+
(i) => onUnpairedEscape?.(i + 2)
|
|
1030
|
+
);
|
|
942
1031
|
return [...head, ...data];
|
|
943
1032
|
}
|
|
944
1033
|
function opaqueField(raw) {
|
|
945
1034
|
return { raw, components: [raw], repeats: [[raw]] };
|
|
946
1035
|
}
|
|
947
|
-
function toField(raw, d, onUnknownEscape) {
|
|
1036
|
+
function toField(raw, d, onUnknownEscape, onUnpairedEscape) {
|
|
948
1037
|
const rawRepeats = splitEscapeAware(raw, d.repeat, d.escape);
|
|
949
1038
|
const repeats = rawRepeats.map(
|
|
950
1039
|
(rep) => splitEscapeAware(rep, d.component, d.escape).map(
|
|
951
|
-
(comp) => decodeEscapes(comp, d, onUnknownEscape)
|
|
1040
|
+
(comp) => decodeEscapes(comp, d, onUnknownEscape, onUnpairedEscape)
|
|
952
1041
|
)
|
|
953
1042
|
);
|
|
954
1043
|
const components = repeats[0] ?? [""];
|
|
@@ -1063,7 +1152,15 @@ function buildRecord(line2, recordIndex, d, warnings) {
|
|
|
1063
1152
|
unknownEscapeSequence({ recordIndex, recordType: rawType, fieldIndex: fieldIndex + 1 })
|
|
1064
1153
|
);
|
|
1065
1154
|
};
|
|
1066
|
-
const
|
|
1155
|
+
const onUnpairedEscape = (fieldIndex) => {
|
|
1156
|
+
warnings.push(
|
|
1157
|
+
unpairedEscapeCharacter({ recordIndex, recordType: rawType, fieldIndex: fieldIndex + 1 })
|
|
1158
|
+
);
|
|
1159
|
+
};
|
|
1160
|
+
const fields = rawType === "H" ? tokenizeHeader(line2, d, onUnknownEscape, onUnpairedEscape) : tokenizeRecord(line2, d, onUnknownEscape, onUnpairedEscape);
|
|
1161
|
+
if (fields.length === 1 && line2.slice(1).trim().length > 0) {
|
|
1162
|
+
warnings.push(fieldsUnseparated({ recordIndex, recordType: rawType }));
|
|
1163
|
+
}
|
|
1067
1164
|
switch (rawType) {
|
|
1068
1165
|
case "H":
|
|
1069
1166
|
return {
|
|
@@ -2197,8 +2294,8 @@ function concatBytes3(chunks) {
|
|
|
2197
2294
|
}
|
|
2198
2295
|
|
|
2199
2296
|
// src/index.ts
|
|
2200
|
-
var VERSION = "0.0.
|
|
2297
|
+
var VERSION = "0.0.9";
|
|
2201
2298
|
|
|
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 };
|
|
2299
|
+
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, fieldsUnseparated, 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, unpairedEscapeCharacter, unparseableReferenceRange, unreadableRedeclaration };
|
|
2203
2300
|
//# sourceMappingURL=index.mjs.map
|
|
2204
2301
|
//# sourceMappingURL=index.mjs.map
|