@cosyte/astm 0.0.7 → 0.0.8
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 +67 -0
- package/README.md +20 -5
- package/dist/index.cjs +138 -134
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +136 -34
- package/dist/index.d.ts +136 -34
- package/dist/index.mjs +138 -134
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -129,6 +129,73 @@ phase 8` passes while `Phase 8` reds). An arm keyed on a following digit was wri
|
|
|
129
129
|
|
|
130
130
|
### Changed
|
|
131
131
|
|
|
132
|
+
- **BREAKING: a profile may no longer tolerate `ASTM_RECORD_UNKNOWN_TYPE`**
|
|
133
|
+
(`ASTM-UNKNOWN-RECORD-REMERGE`). The code moves off the profile safety gate's tolerable
|
|
134
|
+
allow-list and onto the safety-critical set, so `defineAstmProfile({ tolerate: [...] })` naming
|
|
135
|
+
it now throws `AstmProfileDefinitionError` at definition time instead of building a profile, and
|
|
136
|
+
the gate is re-checked when a warning would be downgraded, so a profile assembled as a plain
|
|
137
|
+
object rather than through the factory gets the same answer (it declines to downgrade rather than
|
|
138
|
+
throwing, so a hand-authored profile still parses, it just cannot quiet a safety-critical code).
|
|
139
|
+
`TOLERABLE_CODES` goes from four members to three (`ASTM_NONSTANDARD_DELIMITERS`,
|
|
140
|
+
`ASTM_UNKNOWN_ESCAPE_SEQUENCE`, `ASTM_RECORD_UNINTERPRETED_QUERY_STATUS`) and
|
|
141
|
+
`SAFETY_CRITICAL_CODES` / `isSafetyCriticalCode()` answer accordingly. No built-in profile
|
|
142
|
+
tolerated the code, so nothing in this package changes shape; a consumer profile that named it
|
|
143
|
+
will now fail to define, loudly, at the point of definition.
|
|
144
|
+
|
|
145
|
+
**Why, and it is a wrong-patient path.** A message runs from an `H` header to its `L`
|
|
146
|
+
terminator, and `messages()` decides where one starts by reading each record's **type letter**.
|
|
147
|
+
A header the reader cannot recognize as a header (one carrying a stray leading byte, for
|
|
148
|
+
instance) is surfaced as an unsupported record, opens no message, and the two messages either
|
|
149
|
+
side of it are grouped as one. The merged message pairs a patient with results that arrived
|
|
150
|
+
under a different header. The `ASTM_RECORD_UNKNOWN_TYPE` warning is the entire report that this
|
|
151
|
+
happened, so a profile downgrading it to `PROFILE_QUIRK_APPLIED` quieted the whole signal, and a
|
|
152
|
+
`{ strict: true }` parse then accepted the stream with no objection at all.
|
|
153
|
+
|
|
154
|
+
**The type letter turned out to have two load-bearing readers, not one.** `classifyMessage`
|
|
155
|
+
counts `Q` / `R` / `O` records by the same letter, and the rule that a message carrying a query
|
|
156
|
+
is never read as a result set is stated on that count. An unrecognized `Q` costs a host-query
|
|
157
|
+
request that guarantee: the message reads as `kind: "results"`, and the
|
|
158
|
+
`ASTM_RECORD_AMBIGUOUS_MESSAGE_KIND` warning that flags a query-plus-result contradiction
|
|
159
|
+
disappears along with the `Q` it was counting. There too `ASTM_RECORD_UNKNOWN_TYPE` is the only
|
|
160
|
+
warning left on the stream. Both readers are measured in the regression test. The
|
|
161
|
+
mis-classification itself is not fixed here, for the same reason the merge is not: it is a
|
|
162
|
+
property of reading the letter, and inferring the intended letter is the guess this package
|
|
163
|
+
declines to make.
|
|
164
|
+
|
|
165
|
+
**The variant that reaches furthest carries no second patient.** When the merged second message
|
|
166
|
+
has its own `P`, the merged message holds two and the multi-patient guard throws. When the
|
|
167
|
+
second message is **result-only**, the merged message still holds exactly one `P`, so `patient()`
|
|
168
|
+
and `results()` both answer confidently and the first patient silently acquires the second
|
|
169
|
+
message's results. That is the fixture the regression test pins.
|
|
170
|
+
|
|
171
|
+
**The parser is deliberately unchanged.** Recognizing a mangled header _as_ a header means
|
|
172
|
+
guessing at a byte the sender did not send, and that guess would split a stream a different way
|
|
173
|
+
just as silently. A lenient parse still merges and still warns; a strict parse refuses; and the
|
|
174
|
+
refusal can no longer be configured away. `messages()`, the warning's own message text, the
|
|
175
|
+
`UnsupportedRecord` type, the README and the quickstart all now say to read this code as a
|
|
176
|
+
possible lost message boundary rather than as cosmetic noise.
|
|
177
|
+
|
|
178
|
+
**The admission test for the allow-list gained a second clause**, which is the durable part of
|
|
179
|
+
this change. A code was admitted on one question: can it alter, drop, or fabricate an extracted
|
|
180
|
+
value? `ASTM_RECORD_UNKNOWN_TYPE` still passes that one, and it was still wrong to tolerate,
|
|
181
|
+
because the condition it reports had quietly become something the library reads. The rule is now
|
|
182
|
+
that a code is tolerable only while **nothing else in this package reads the condition the
|
|
183
|
+
warning reports**, and that second clause is a claim about the whole library which can stop being
|
|
184
|
+
true without anyone editing `safety.ts`. It is a review obligation, not a mechanical check, and
|
|
185
|
+
the file says so rather than implying a gate that does not exist. The three surviving codes were
|
|
186
|
+
each re-derived against both readers of record structure, and each is measured by comparing the
|
|
187
|
+
same logical stream with the reported condition present and absent. That is deliberately not a
|
|
188
|
+
comparison of a parse with a profile against one without: the profile transform runs after the
|
|
189
|
+
records are built, so the second comparison is identical for every code and would have passed for
|
|
190
|
+
the one just removed. The comparison actually used is exercised on the mangled-header pair as a
|
|
191
|
+
negative control, where it has to fail.
|
|
192
|
+
|
|
193
|
+
**Evidence, labelled.** The `H` … `L` message unit is _verified primary_ and its grounding is
|
|
194
|
+
recorded on `messages()`, unchanged here. The allow-list itself is **this library's own policy,
|
|
195
|
+
not the standard's**: no clause governs which of our warning codes a profile may downgrade, and
|
|
196
|
+
none is claimed for it. The behaviour above (the merge, the accessors answering, the strict
|
|
197
|
+
acceptance under a tolerating profile) is _measured_, on the tree before and after the change.
|
|
198
|
+
|
|
132
199
|
- **The em dash (U+2014) is gone from every tracked file, and a CI gate keeps it out**
|
|
133
200
|
(`EMDASH-CONFORMANCE`). The brand rule bans the character outright across every cosyte surface
|
|
134
201
|
and names commit messages explicitly; this package was the last one it had not reached, and the
|
package/README.md
CHANGED
|
@@ -42,11 +42,14 @@ reference parser, [`@cosyte/hl7`](https://github.com/cosyte/hl7).
|
|
|
42
42
|
and lost.
|
|
43
43
|
- **Vendor profiles.** `defineAstmProfile()` builds a provenance-backed profile whose tolerances
|
|
44
44
|
downgrade _expected_, non-safety-critical deviations to a `PROFILE_QUIRK_APPLIED` warning without
|
|
45
|
-
ever altering a value, behind a
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
ever altering a value, behind a safety gate that refuses to tolerate any result value, flag,
|
|
46
|
+
status, range, or units warning, any patient or comment context, any message-kind ambiguity, any
|
|
47
|
+
unrecognized record type, and any frame or transport integrity warning. The gate runs when a
|
|
48
|
+
profile is defined **and** again when a warning would be downgraded, so a profile assembled as a
|
|
49
|
+
plain object rather than through `defineAstmProfile()` gets the same answer. A profile can never
|
|
50
|
+
make a bad checksum "ok", a cancelled result read "final", or quiet the warning that says a
|
|
51
|
+
message boundary went unrecognized. Named per-vendor profiles await a public, vendor-attributed
|
|
52
|
+
quirk document.
|
|
50
53
|
- **Terminology, bring your own.** `applyLivd(msg, catalog)` maps an analyzer's local test code to a
|
|
51
54
|
LOINC from a consumer-supplied IICC LIVD catalog as an additive, advisory annotation that never
|
|
52
55
|
mutates the raw code or value and never guesses a LOINC. No LOINC, SNOMED, or LIVD dictionary is
|
|
@@ -170,6 +173,18 @@ patient is unchanged, and so is a result-only message with no `P` at all, which
|
|
|
170
173
|
`undefined`. `commentsFor()` is unchanged on every stream, because the parent record you hand it
|
|
171
174
|
already names the message.
|
|
172
175
|
|
|
176
|
+
Splitting reads each record's type letter, so check for an `ASTM_RECORD_UNKNOWN_TYPE` warning before
|
|
177
|
+
you trust the split. A header the reader does not recognize as a header, one carrying a stray leading
|
|
178
|
+
byte for instance, opens no message, and the messages either side of it merge back into one, so a
|
|
179
|
+
patient can end up holding results that arrived under a different header. Delimiters are re-read at
|
|
180
|
+
each header too, so if the unrecognized one declared a different set, the records after it are read
|
|
181
|
+
with the previous set and their fields can be lost rather than merely misfiled. The
|
|
182
|
+
parser warns on that record and a `{ strict: true }` parse refuses the stream. That warning is the
|
|
183
|
+
only report the merge produces, so a profile is not allowed to tolerate it: the code is refused when
|
|
184
|
+
a profile is defined, and a warning carrying it is not downgraded whatever profile is in force. Do
|
|
185
|
+
not gate on the warning count, though, because the records that merged in can raise warnings of
|
|
186
|
+
their own.
|
|
187
|
+
|
|
173
188
|
## Map local codes to LOINC (LIVD, bring-your-own)
|
|
174
189
|
|
|
175
190
|
An analyzer sends a proprietary local test code in the Universal Test ID; a standard LOINC is mapped
|
package/dist/index.cjs
CHANGED
|
@@ -121,7 +121,11 @@ function primaryCode(u) {
|
|
|
121
121
|
|
|
122
122
|
// src/common/warnings.ts
|
|
123
123
|
var WARNING_CODES = {
|
|
124
|
-
/**
|
|
124
|
+
/**
|
|
125
|
+
* A record's type letter is not one of the modeled types: surfaced as an unsupported record,
|
|
126
|
+
* never dropped. Treat it as a possible lost message boundary, because a header the reader did
|
|
127
|
+
* not recognize as one does not open a new message.
|
|
128
|
+
*/
|
|
125
129
|
ASTM_RECORD_UNKNOWN_TYPE: "ASTM_RECORD_UNKNOWN_TYPE",
|
|
126
130
|
/** The header declared delimiters other than the canonical `H|\^&`: tolerated, noted. */
|
|
127
131
|
ASTM_NONSTANDARD_DELIMITERS: "ASTM_NONSTANDARD_DELIMITERS",
|
|
@@ -214,7 +218,7 @@ var WARNING_CODES = {
|
|
|
214
218
|
function unknownRecordType(position) {
|
|
215
219
|
return {
|
|
216
220
|
code: WARNING_CODES.ASTM_RECORD_UNKNOWN_TYPE,
|
|
217
|
-
message: "Unrecognized record type, surfaced verbatim as an unsupported record.",
|
|
221
|
+
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.",
|
|
218
222
|
position
|
|
219
223
|
};
|
|
220
224
|
}
|
|
@@ -427,6 +431,135 @@ function classifyMessage(records) {
|
|
|
427
431
|
return { kind, hasQuery, hasResults, hasOrders, isHostQueryRequest: kind === "host-query" };
|
|
428
432
|
}
|
|
429
433
|
|
|
434
|
+
// src/frames/warnings.ts
|
|
435
|
+
var FRAME_WARNING_CODES = {
|
|
436
|
+
/**
|
|
437
|
+
* A frame's two-hex-char checksum did not match the modulo-256 sum recomputed over its bytes. The
|
|
438
|
+
* frame is surfaced with `trusted: false` and its text is **never merged** into a reassembled
|
|
439
|
+
* record (default **warn** in lenient mode, escalated to a thrown error in `strict`). Corruption is
|
|
440
|
+
* never silently trusted: the "checksums are routinely not validated" claim was refuted; we
|
|
441
|
+
* validate.
|
|
442
|
+
*/
|
|
443
|
+
ASTM_FRAME_BAD_CHECKSUM: "ASTM_FRAME_BAD_CHECKSUM",
|
|
444
|
+
/**
|
|
445
|
+
* A frame's sequence number was not the expected next value (`1 → … → 7 → 0 → …`): a frame was
|
|
446
|
+
* possibly dropped. The stream is **never silently concatenated across the gap** as if contiguous;
|
|
447
|
+
* the in-progress record is tainted and not emitted as a clean reassembly.
|
|
448
|
+
*/
|
|
449
|
+
ASTM_FRAME_SEQUENCE_GAP: "ASTM_FRAME_SEQUENCE_GAP",
|
|
450
|
+
/**
|
|
451
|
+
* A frame opened with `STX` but no valid terminator + checksum was found before the stream ended
|
|
452
|
+
* (or before the next `STX`), or a record's frames ended on an intermediate `ETB` with no final
|
|
453
|
+
* `ETX`. The partial bytes are surfaced flagged untrusted and **no partial record is invented**
|
|
454
|
+
* (warn in lenient, thrown in `strict`).
|
|
455
|
+
*/
|
|
456
|
+
ASTM_FRAME_UNTERMINATED: "ASTM_FRAME_UNTERMINATED",
|
|
457
|
+
/**
|
|
458
|
+
* A frame's record text exceeded the 240-byte limit without a split. The frame is still surfaced
|
|
459
|
+
* (and, if its checksum validates, reassembled): the deviation is flagged, not silently dropped
|
|
460
|
+
* (warn in lenient, thrown in `strict`).
|
|
461
|
+
*/
|
|
462
|
+
ASTM_FRAME_OVERSIZE: "ASTM_FRAME_OVERSIZE"
|
|
463
|
+
};
|
|
464
|
+
function frameBadChecksum(position) {
|
|
465
|
+
return {
|
|
466
|
+
code: FRAME_WARNING_CODES.ASTM_FRAME_BAD_CHECKSUM,
|
|
467
|
+
message: "Frame checksum mismatch, surfaced untrusted, never merged into a record.",
|
|
468
|
+
position
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
function frameSequenceGap(position) {
|
|
472
|
+
return {
|
|
473
|
+
code: FRAME_WARNING_CODES.ASTM_FRAME_SEQUENCE_GAP,
|
|
474
|
+
message: "Frame number out of sequence, possible dropped frame, never silently bridged.",
|
|
475
|
+
position
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
function frameUnterminated(position) {
|
|
479
|
+
return {
|
|
480
|
+
code: FRAME_WARNING_CODES.ASTM_FRAME_UNTERMINATED,
|
|
481
|
+
message: "Unterminated frame, partial bytes surfaced untrusted, no partial record invented.",
|
|
482
|
+
position
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
function frameOversize(position) {
|
|
486
|
+
return {
|
|
487
|
+
code: FRAME_WARNING_CODES.ASTM_FRAME_OVERSIZE,
|
|
488
|
+
message: "Frame text exceeded the 240-byte limit without a split, flagged, never dropped.",
|
|
489
|
+
position
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/ltp/warnings.ts
|
|
494
|
+
var LTP_WARNING_CODES = {
|
|
495
|
+
/**
|
|
496
|
+
* The transport detector could not tell a framed stream from a raw (unframed) one from its leading
|
|
497
|
+
* byte: it was neither `STX`/`ENQ` nor a bare record letter. It **defaults to framed** and warns,
|
|
498
|
+
* never guessing silently into data loss; a profile override forces the mode. (cobas b121 drops
|
|
499
|
+
* framing over TCP; cobas 4800 / Iguana retain it, both realities exist, so an unrecognizable lead
|
|
500
|
+
* is defaulted, not assumed.)
|
|
501
|
+
*/
|
|
502
|
+
ASTM_LTP_AMBIGUOUS_TRANSPORT: "ASTM_LTP_AMBIGUOUS_TRANSPORT",
|
|
503
|
+
/**
|
|
504
|
+
* A control event arrived in a protocol state that did not expect it: e.g. an inbound `ACK`/`NAK`
|
|
505
|
+
* at a receiver (which sends, never receives, those), or an `ENQ` mid-transfer. The event is
|
|
506
|
+
* surfaced and handled defensively (an unexpected `ACK`/`NAK` is **never** read as acceptance of
|
|
507
|
+
* data); it never advances the transfer as if valid.
|
|
508
|
+
*/
|
|
509
|
+
ASTM_LTP_UNEXPECTED_EVENT: "ASTM_LTP_UNEXPECTED_EVENT",
|
|
510
|
+
/**
|
|
511
|
+
* The receiver rejected a received frame with a `NAK` instead of accepting it: because its
|
|
512
|
+
* checksum failed, it was unterminated, or its frame number was out of sequence. The frame's text
|
|
513
|
+
* is **never** appended to the record and the transfer does **not** advance; the receiver awaits the
|
|
514
|
+
* sender's retransmit. This is the protocol-level face of the frame codec's fail-safe: a bad frame
|
|
515
|
+
* drives retransmit, not acceptance.
|
|
516
|
+
*/
|
|
517
|
+
ASTM_LTP_FRAME_REJECTED: "ASTM_LTP_FRAME_REJECTED"
|
|
518
|
+
};
|
|
519
|
+
function ltpAmbiguousTransport() {
|
|
520
|
+
return {
|
|
521
|
+
code: LTP_WARNING_CODES.ASTM_LTP_AMBIGUOUS_TRANSPORT,
|
|
522
|
+
message: "Leading byte was neither a frame start (STX/ENQ) nor a record letter, defaulted to framed; override with a profile to force raw."
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
function ltpUnexpectedEvent(frameNumber) {
|
|
526
|
+
return {
|
|
527
|
+
code: LTP_WARNING_CODES.ASTM_LTP_UNEXPECTED_EVENT,
|
|
528
|
+
message: "Control event arrived in a state that did not expect it, handled defensively, never read as acceptance.",
|
|
529
|
+
...frameNumber !== void 0 ? { frameNumber } : {}
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
function ltpFrameRejected(frameNumber) {
|
|
533
|
+
return {
|
|
534
|
+
code: LTP_WARNING_CODES.ASTM_LTP_FRAME_REJECTED,
|
|
535
|
+
message: "Frame rejected (bad checksum, unterminated, or out of sequence), NAK sent, retransmit expected, never accepted.",
|
|
536
|
+
...frameNumber !== void 0 ? { frameNumber } : {}
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// src/profiles/safety.ts
|
|
541
|
+
var TOLERABLE_CODES = Object.freeze(
|
|
542
|
+
/* @__PURE__ */ new Set([
|
|
543
|
+
WARNING_CODES.ASTM_NONSTANDARD_DELIMITERS,
|
|
544
|
+
WARNING_CODES.ASTM_UNKNOWN_ESCAPE_SEQUENCE,
|
|
545
|
+
WARNING_CODES.ASTM_RECORD_UNINTERPRETED_QUERY_STATUS
|
|
546
|
+
])
|
|
547
|
+
);
|
|
548
|
+
var ALL_WARNING_CODES = [
|
|
549
|
+
...Object.values(WARNING_CODES),
|
|
550
|
+
...Object.values(FRAME_WARNING_CODES),
|
|
551
|
+
...Object.values(LTP_WARNING_CODES)
|
|
552
|
+
];
|
|
553
|
+
var ALL_ASTM_WARNING_CODES = Object.freeze(
|
|
554
|
+
new Set(ALL_WARNING_CODES)
|
|
555
|
+
);
|
|
556
|
+
var SAFETY_CRITICAL_CODES = Object.freeze(
|
|
557
|
+
new Set(ALL_WARNING_CODES.filter((c) => !TOLERABLE_CODES.has(c)))
|
|
558
|
+
);
|
|
559
|
+
function isSafetyCriticalCode(code) {
|
|
560
|
+
return SAFETY_CRITICAL_CODES.has(code);
|
|
561
|
+
}
|
|
562
|
+
|
|
430
563
|
// src/profiles/apply.ts
|
|
431
564
|
function toleranceApplies(tolerance, warning) {
|
|
432
565
|
if (tolerance.code !== warning.code) return false;
|
|
@@ -442,6 +575,7 @@ function toleranceApplies(tolerance, warning) {
|
|
|
442
575
|
}
|
|
443
576
|
function applyAstmProfile(profile, warning) {
|
|
444
577
|
if (warning.expected === true) return warning;
|
|
578
|
+
if (isSafetyCriticalCode(warning.code)) return warning;
|
|
445
579
|
for (const tolerance of profile.tolerate) {
|
|
446
580
|
if (toleranceApplies(tolerance, warning)) {
|
|
447
581
|
return profileQuirkApplied(warning, profile.name);
|
|
@@ -563,136 +697,6 @@ var AstmProfileDefinitionError = class extends Error {
|
|
|
563
697
|
}
|
|
564
698
|
};
|
|
565
699
|
|
|
566
|
-
// src/frames/warnings.ts
|
|
567
|
-
var FRAME_WARNING_CODES = {
|
|
568
|
-
/**
|
|
569
|
-
* A frame's two-hex-char checksum did not match the modulo-256 sum recomputed over its bytes. The
|
|
570
|
-
* frame is surfaced with `trusted: false` and its text is **never merged** into a reassembled
|
|
571
|
-
* record (default **warn** in lenient mode, escalated to a thrown error in `strict`). Corruption is
|
|
572
|
-
* never silently trusted: the "checksums are routinely not validated" claim was refuted; we
|
|
573
|
-
* validate.
|
|
574
|
-
*/
|
|
575
|
-
ASTM_FRAME_BAD_CHECKSUM: "ASTM_FRAME_BAD_CHECKSUM",
|
|
576
|
-
/**
|
|
577
|
-
* A frame's sequence number was not the expected next value (`1 → … → 7 → 0 → …`): a frame was
|
|
578
|
-
* possibly dropped. The stream is **never silently concatenated across the gap** as if contiguous;
|
|
579
|
-
* the in-progress record is tainted and not emitted as a clean reassembly.
|
|
580
|
-
*/
|
|
581
|
-
ASTM_FRAME_SEQUENCE_GAP: "ASTM_FRAME_SEQUENCE_GAP",
|
|
582
|
-
/**
|
|
583
|
-
* A frame opened with `STX` but no valid terminator + checksum was found before the stream ended
|
|
584
|
-
* (or before the next `STX`), or a record's frames ended on an intermediate `ETB` with no final
|
|
585
|
-
* `ETX`. The partial bytes are surfaced flagged untrusted and **no partial record is invented**
|
|
586
|
-
* (warn in lenient, thrown in `strict`).
|
|
587
|
-
*/
|
|
588
|
-
ASTM_FRAME_UNTERMINATED: "ASTM_FRAME_UNTERMINATED",
|
|
589
|
-
/**
|
|
590
|
-
* A frame's record text exceeded the 240-byte limit without a split. The frame is still surfaced
|
|
591
|
-
* (and, if its checksum validates, reassembled): the deviation is flagged, not silently dropped
|
|
592
|
-
* (warn in lenient, thrown in `strict`).
|
|
593
|
-
*/
|
|
594
|
-
ASTM_FRAME_OVERSIZE: "ASTM_FRAME_OVERSIZE"
|
|
595
|
-
};
|
|
596
|
-
function frameBadChecksum(position) {
|
|
597
|
-
return {
|
|
598
|
-
code: FRAME_WARNING_CODES.ASTM_FRAME_BAD_CHECKSUM,
|
|
599
|
-
message: "Frame checksum mismatch, surfaced untrusted, never merged into a record.",
|
|
600
|
-
position
|
|
601
|
-
};
|
|
602
|
-
}
|
|
603
|
-
function frameSequenceGap(position) {
|
|
604
|
-
return {
|
|
605
|
-
code: FRAME_WARNING_CODES.ASTM_FRAME_SEQUENCE_GAP,
|
|
606
|
-
message: "Frame number out of sequence, possible dropped frame, never silently bridged.",
|
|
607
|
-
position
|
|
608
|
-
};
|
|
609
|
-
}
|
|
610
|
-
function frameUnterminated(position) {
|
|
611
|
-
return {
|
|
612
|
-
code: FRAME_WARNING_CODES.ASTM_FRAME_UNTERMINATED,
|
|
613
|
-
message: "Unterminated frame, partial bytes surfaced untrusted, no partial record invented.",
|
|
614
|
-
position
|
|
615
|
-
};
|
|
616
|
-
}
|
|
617
|
-
function frameOversize(position) {
|
|
618
|
-
return {
|
|
619
|
-
code: FRAME_WARNING_CODES.ASTM_FRAME_OVERSIZE,
|
|
620
|
-
message: "Frame text exceeded the 240-byte limit without a split, flagged, never dropped.",
|
|
621
|
-
position
|
|
622
|
-
};
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
// src/ltp/warnings.ts
|
|
626
|
-
var LTP_WARNING_CODES = {
|
|
627
|
-
/**
|
|
628
|
-
* The transport detector could not tell a framed stream from a raw (unframed) one from its leading
|
|
629
|
-
* byte: it was neither `STX`/`ENQ` nor a bare record letter. It **defaults to framed** and warns,
|
|
630
|
-
* never guessing silently into data loss; a profile override forces the mode. (cobas b121 drops
|
|
631
|
-
* framing over TCP; cobas 4800 / Iguana retain it, both realities exist, so an unrecognizable lead
|
|
632
|
-
* is defaulted, not assumed.)
|
|
633
|
-
*/
|
|
634
|
-
ASTM_LTP_AMBIGUOUS_TRANSPORT: "ASTM_LTP_AMBIGUOUS_TRANSPORT",
|
|
635
|
-
/**
|
|
636
|
-
* A control event arrived in a protocol state that did not expect it: e.g. an inbound `ACK`/`NAK`
|
|
637
|
-
* at a receiver (which sends, never receives, those), or an `ENQ` mid-transfer. The event is
|
|
638
|
-
* surfaced and handled defensively (an unexpected `ACK`/`NAK` is **never** read as acceptance of
|
|
639
|
-
* data); it never advances the transfer as if valid.
|
|
640
|
-
*/
|
|
641
|
-
ASTM_LTP_UNEXPECTED_EVENT: "ASTM_LTP_UNEXPECTED_EVENT",
|
|
642
|
-
/**
|
|
643
|
-
* The receiver rejected a received frame with a `NAK` instead of accepting it: because its
|
|
644
|
-
* checksum failed, it was unterminated, or its frame number was out of sequence. The frame's text
|
|
645
|
-
* is **never** appended to the record and the transfer does **not** advance; the receiver awaits the
|
|
646
|
-
* sender's retransmit. This is the protocol-level face of the frame codec's fail-safe: a bad frame
|
|
647
|
-
* drives retransmit, not acceptance.
|
|
648
|
-
*/
|
|
649
|
-
ASTM_LTP_FRAME_REJECTED: "ASTM_LTP_FRAME_REJECTED"
|
|
650
|
-
};
|
|
651
|
-
function ltpAmbiguousTransport() {
|
|
652
|
-
return {
|
|
653
|
-
code: LTP_WARNING_CODES.ASTM_LTP_AMBIGUOUS_TRANSPORT,
|
|
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."
|
|
655
|
-
};
|
|
656
|
-
}
|
|
657
|
-
function ltpUnexpectedEvent(frameNumber) {
|
|
658
|
-
return {
|
|
659
|
-
code: LTP_WARNING_CODES.ASTM_LTP_UNEXPECTED_EVENT,
|
|
660
|
-
message: "Control event arrived in a state that did not expect it, handled defensively, never read as acceptance.",
|
|
661
|
-
...frameNumber !== void 0 ? { frameNumber } : {}
|
|
662
|
-
};
|
|
663
|
-
}
|
|
664
|
-
function ltpFrameRejected(frameNumber) {
|
|
665
|
-
return {
|
|
666
|
-
code: LTP_WARNING_CODES.ASTM_LTP_FRAME_REJECTED,
|
|
667
|
-
message: "Frame rejected (bad checksum, unterminated, or out of sequence), NAK sent, retransmit expected, never accepted.",
|
|
668
|
-
...frameNumber !== void 0 ? { frameNumber } : {}
|
|
669
|
-
};
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
// src/profiles/safety.ts
|
|
673
|
-
var TOLERABLE_CODES = Object.freeze(
|
|
674
|
-
/* @__PURE__ */ new Set([
|
|
675
|
-
WARNING_CODES.ASTM_RECORD_UNKNOWN_TYPE,
|
|
676
|
-
WARNING_CODES.ASTM_NONSTANDARD_DELIMITERS,
|
|
677
|
-
WARNING_CODES.ASTM_UNKNOWN_ESCAPE_SEQUENCE,
|
|
678
|
-
WARNING_CODES.ASTM_RECORD_UNINTERPRETED_QUERY_STATUS
|
|
679
|
-
])
|
|
680
|
-
);
|
|
681
|
-
var ALL_WARNING_CODES = [
|
|
682
|
-
...Object.values(WARNING_CODES),
|
|
683
|
-
...Object.values(FRAME_WARNING_CODES),
|
|
684
|
-
...Object.values(LTP_WARNING_CODES)
|
|
685
|
-
];
|
|
686
|
-
var ALL_ASTM_WARNING_CODES = Object.freeze(
|
|
687
|
-
new Set(ALL_WARNING_CODES)
|
|
688
|
-
);
|
|
689
|
-
var SAFETY_CRITICAL_CODES = Object.freeze(
|
|
690
|
-
new Set(ALL_WARNING_CODES.filter((c) => !TOLERABLE_CODES.has(c)))
|
|
691
|
-
);
|
|
692
|
-
function isSafetyCriticalCode(code) {
|
|
693
|
-
return SAFETY_CRITICAL_CODES.has(code);
|
|
694
|
-
}
|
|
695
|
-
|
|
696
700
|
// src/profiles/validate.ts
|
|
697
701
|
var KNOWN_OPTION_KEYS = [
|
|
698
702
|
"name",
|
|
@@ -772,7 +776,7 @@ function validateTolerations(tolerate, profileName) {
|
|
|
772
776
|
}
|
|
773
777
|
if (isSafetyCriticalCode(t.code)) {
|
|
774
778
|
throw new AstmProfileDefinitionError(
|
|
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"
|
|
779
|
+
`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.`,
|
|
776
780
|
profileName
|
|
777
781
|
);
|
|
778
782
|
}
|
|
@@ -2199,7 +2203,7 @@ function concatBytes3(chunks) {
|
|
|
2199
2203
|
}
|
|
2200
2204
|
|
|
2201
2205
|
// src/index.ts
|
|
2202
|
-
var VERSION = "0.0.
|
|
2206
|
+
var VERSION = "0.0.8";
|
|
2203
2207
|
|
|
2204
2208
|
exports.ALL_ASTM_WARNING_CODES = ALL_ASTM_WARNING_CODES;
|
|
2205
2209
|
exports.AMBIGUOUS_CODES = AMBIGUOUS_CODES;
|