@cosyte/astm 0.0.4 → 0.0.6
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 +166 -119
- package/README.md +28 -23
- package/dist/index.cjs +57 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +394 -394
- package/dist/index.d.ts +394 -394
- package/dist/index.mjs +57 -57
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
package/dist/index.d.cts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* record layer.
|
|
4
4
|
*
|
|
5
5
|
* **PHI discipline (the whole point).** A position identifies *where* a
|
|
6
|
-
* deviation occurred
|
|
7
|
-
* 1-based field / component indices
|
|
6
|
+
* deviation occurred (the record's ordinal index, its type letter, and the
|
|
7
|
+
* 1-based field / component indices) and **never** carries a field value. A
|
|
8
8
|
* warning or error may be logged verbatim without leaking a patient name, an
|
|
9
9
|
* identifier, or a result value.
|
|
10
10
|
*/
|
|
@@ -51,21 +51,21 @@ interface AstmPosition {
|
|
|
51
51
|
* ```
|
|
52
52
|
*/
|
|
53
53
|
declare const WARNING_CODES: {
|
|
54
|
-
/** A record's type letter is not one of the modeled types
|
|
54
|
+
/** A record's type letter is not one of the modeled types: surfaced as an unsupported record. */
|
|
55
55
|
readonly ASTM_RECORD_UNKNOWN_TYPE: "ASTM_RECORD_UNKNOWN_TYPE";
|
|
56
|
-
/** The header declared delimiters other than the canonical `H
|
|
56
|
+
/** The header declared delimiters other than the canonical `H|\^&`: tolerated, noted. */
|
|
57
57
|
readonly ASTM_NONSTANDARD_DELIMITERS: "ASTM_NONSTANDARD_DELIMITERS";
|
|
58
|
-
/** An escape sequence body was not one of `&F&`/`&S&`/`&R&`/`&E
|
|
58
|
+
/** An escape sequence body was not one of `&F&`/`&S&`/`&R&`/`&E&`: preserved verbatim. */
|
|
59
59
|
readonly ASTM_UNKNOWN_ESCAPE_SEQUENCE: "ASTM_UNKNOWN_ESCAPE_SEQUENCE";
|
|
60
60
|
/**
|
|
61
61
|
* A result value field carried an *unescaped* component delimiter, so it split into more than one
|
|
62
|
-
* component. Both the full raw value and the split are surfaced and this warning fires
|
|
62
|
+
* component. Both the full raw value and the split are surfaced and this warning fires: the
|
|
63
63
|
* ambiguity is never resolved silently into a truncated value.
|
|
64
64
|
*/
|
|
65
65
|
readonly ASTM_RECORD_AMBIGUOUS_VALUE_SPLIT: "ASTM_RECORD_AMBIGUOUS_VALUE_SPLIT";
|
|
66
66
|
/**
|
|
67
67
|
* A result's abnormal-flag field (`R` field 7) carried a letter outside HL7 Table 0078. The flag is
|
|
68
|
-
* surfaced as `undefined
|
|
68
|
+
* surfaced as `undefined`: never dropped, and **never coerced to `normal`** (a clinical error).
|
|
69
69
|
*/
|
|
70
70
|
readonly ASTM_RECORD_UNDEFINED_ABNORMAL_FLAG: "ASTM_RECORD_UNDEFINED_ABNORMAL_FLAG";
|
|
71
71
|
/**
|
|
@@ -75,7 +75,7 @@ declare const WARNING_CODES: {
|
|
|
75
75
|
readonly ASTM_RECORD_UNDEFINED_RESULT_STATUS: "ASTM_RECORD_UNDEFINED_RESULT_STATUS";
|
|
76
76
|
/**
|
|
77
77
|
* A result's reference-range field (`R` field 6) did not match a recognized form (`low-high`,
|
|
78
|
-
* `<high`, `>low`). The text is surfaced verbatim as `unparsed
|
|
78
|
+
* `<high`, `>low`). The text is surfaced verbatim as `unparsed`: a bound is **never fabricated**.
|
|
79
79
|
*/
|
|
80
80
|
readonly ASTM_RECORD_UNPARSEABLE_REFERENCE_RANGE: "ASTM_RECORD_UNPARSEABLE_REFERENCE_RANGE";
|
|
81
81
|
/**
|
|
@@ -84,25 +84,25 @@ declare const WARNING_CODES: {
|
|
|
84
84
|
*/
|
|
85
85
|
readonly ASTM_RECORD_UNITS_ABSENT: "ASTM_RECORD_UNITS_ABSENT";
|
|
86
86
|
/**
|
|
87
|
-
* A `C` (comment) record had no valid preceding `H`/`P`/`O`/`R` parent
|
|
87
|
+
* A `C` (comment) record had no valid preceding `H`/`P`/`O`/`R` parent: an **orphan**. The comment
|
|
88
88
|
* is attached to the message root (`attachedToRoot: true`) and surfaced, **never dropped**.
|
|
89
89
|
*/
|
|
90
90
|
readonly ASTM_RECORD_ORPHAN_COMMENT: "ASTM_RECORD_ORPHAN_COMMENT";
|
|
91
91
|
/**
|
|
92
92
|
* A `YYYYMMDDHHMMSS` timestamp had an odd digit run that truncates a two-digit component in half
|
|
93
93
|
* (e.g. a partial day/hour). The raw run is preserved and the structured value stops at the last
|
|
94
|
-
* **complete** component
|
|
94
|
+
* **complete** component: the dangling digit is **never zero-filled into a fabricated time**.
|
|
95
95
|
*/
|
|
96
96
|
readonly ASTM_RECORD_PARTIAL_TIMESTAMP: "ASTM_RECORD_PARTIAL_TIMESTAMP";
|
|
97
97
|
/**
|
|
98
98
|
* A `Q` (request-information) record carried a request-information status code (field 13). The code
|
|
99
99
|
* *set* is `[OSS-derived / paywalled]` with no publicly-groundable enumeration, so the parser
|
|
100
100
|
* interprets **none** of them: the status is surfaced verbatim and this value-free warning flags that
|
|
101
|
-
* it was passed through **uninterpreted
|
|
101
|
+
* it was passed through **uninterpreted**, never mapped to a guessed meaning.
|
|
102
102
|
*/
|
|
103
103
|
readonly ASTM_RECORD_UNINTERPRETED_QUERY_STATUS: "ASTM_RECORD_UNINTERPRETED_QUERY_STATUS";
|
|
104
104
|
/**
|
|
105
|
-
* A message carried **both** a `Q` (request) and an `R` (result) record
|
|
105
|
+
* A message carried **both** a `Q` (request) and an `R` (result) record: a contradictory shape. The
|
|
106
106
|
* message is classified `host-query` (the `Q` **dominates**, so it is never read as a result set) and
|
|
107
107
|
* this warning flags the anomaly. Positional context only; no field value.
|
|
108
108
|
*/
|
|
@@ -111,7 +111,7 @@ declare const WARNING_CODES: {
|
|
|
111
111
|
* A later `H` (header) record declared a **different** delimiter set from the one in force, and the
|
|
112
112
|
* parser **followed it**: that header and every record after it are read with the newly-declared
|
|
113
113
|
* delimiters, until the next `H`. Records already read keep the set that was in force when they were
|
|
114
|
-
* read
|
|
114
|
+
* read: a redeclaration never reinterprets bytes that have already been consumed.
|
|
115
115
|
*
|
|
116
116
|
* A stream carrying several messages back to back is ordinary, and a header that simply **repeats**
|
|
117
117
|
* the delimiters already in force is a no-op that warns nothing. This code fires only when the set
|
|
@@ -120,13 +120,13 @@ declare const WARNING_CODES: {
|
|
|
120
120
|
*/
|
|
121
121
|
readonly ASTM_RECORD_DELIMITERS_REDECLARED: "ASTM_RECORD_DELIMITERS_REDECLARED";
|
|
122
122
|
/**
|
|
123
|
-
* A later `H` (header) record could not declare a usable delimiter set
|
|
123
|
+
* A later `H` (header) record could not declare a usable delimiter set: it was too short, or the
|
|
124
124
|
* field separator it named also appeared among the other three, leaving the four roles
|
|
125
125
|
* indistinguishable. The delimiters **already in force are kept** and every record is still surfaced;
|
|
126
126
|
* a set is never guessed and no record is dropped.
|
|
127
127
|
*
|
|
128
128
|
* The same condition on the *first* header is unrecoverable and remains the
|
|
129
|
-
* `ASTM_RECORD_UNDECLARED_DELIMITERS` fatal
|
|
129
|
+
* `ASTM_RECORD_UNDECLARED_DELIMITERS` fatal: there is no earlier set to fall back to.
|
|
130
130
|
*/
|
|
131
131
|
readonly ASTM_RECORD_UNREADABLE_REDECLARATION: "ASTM_RECORD_UNREADABLE_REDECLARATION";
|
|
132
132
|
/**
|
|
@@ -134,7 +134,7 @@ declare const WARNING_CODES: {
|
|
|
134
134
|
* (see `src/profiles/`). The original warning is **never dropped**: its code moves to
|
|
135
135
|
* {@link AstmRecordWarning.toleratedCode}, the warning is re-badged `PROFILE_QUIRK_APPLIED` with
|
|
136
136
|
* `expected: true` and the tolerating profile named, so a consumer can filter known, grounded noise
|
|
137
|
-
* while the fact of the deviation
|
|
137
|
+
* while the fact of the deviation, and where it was, survives. A profile can only ever reach this
|
|
138
138
|
* path for a **non-safety-critical** code (enforced at profile-definition time); a safety-critical
|
|
139
139
|
* deviation (a result value, flag, status, patient identifier, code system, or a frame-integrity
|
|
140
140
|
* warning) can **never** be tolerated, so it can never be re-badged here.
|
|
@@ -170,14 +170,14 @@ interface AstmRecordWarning {
|
|
|
170
170
|
* `true` when an active vendor {@link AstmProfile} *expected* this deviation and re-badged it as a
|
|
171
171
|
* {@link WARNING_CODES.PROFILE_QUIRK_APPLIED}. An `expected` warning does **not** escalate to a
|
|
172
172
|
* thrown `AstmStrictError` in strict mode (the whole point of the profile is that this deviation is
|
|
173
|
-
* known and benign)
|
|
173
|
+
* known and benign): it is still recorded, so nothing is hidden. Absent on an untolerated warning.
|
|
174
174
|
*/
|
|
175
175
|
readonly expected?: boolean;
|
|
176
176
|
/** The name of the {@link AstmProfile} that tolerated this warning, when `expected`. */
|
|
177
177
|
readonly profile?: string;
|
|
178
178
|
/**
|
|
179
179
|
* When `code` is {@link WARNING_CODES.PROFILE_QUIRK_APPLIED}, the original warning code the profile
|
|
180
|
-
* tolerated
|
|
180
|
+
* tolerated, so a consumer can still see *which* deviation was re-badged as expected.
|
|
181
181
|
*/
|
|
182
182
|
readonly toleratedCode?: WarningCode;
|
|
183
183
|
}
|
|
@@ -206,7 +206,7 @@ declare function nonStandardDelimiters(position: AstmPosition): AstmRecordWarnin
|
|
|
206
206
|
/**
|
|
207
207
|
* Build an `ASTM_RECORD_DELIMITERS_REDECLARED` warning. Emitted when a later `H`
|
|
208
208
|
* record declares a delimiter set different from the one in force; the new set is
|
|
209
|
-
* honored from that header onward. Positional context only
|
|
209
|
+
* honored from that header onward. Positional context only: never the delimiters
|
|
210
210
|
* themselves.
|
|
211
211
|
*
|
|
212
212
|
* @example
|
|
@@ -242,7 +242,7 @@ declare function unreadableRedeclaration(position: AstmPosition): AstmRecordWarn
|
|
|
242
242
|
declare function unknownEscapeSequence(position: AstmPosition): AstmRecordWarning;
|
|
243
243
|
/**
|
|
244
244
|
* Build an `ASTM_RECORD_AMBIGUOUS_VALUE_SPLIT` warning. Emitted when a result
|
|
245
|
-
* value field split on an unescaped component delimiter
|
|
245
|
+
* value field split on an unescaped component delimiter: the full raw value and
|
|
246
246
|
* the split are both surfaced, never a silent truncation.
|
|
247
247
|
*
|
|
248
248
|
* @example
|
|
@@ -346,7 +346,7 @@ declare function uninterpretedQueryStatus(position: AstmPosition): AstmRecordWar
|
|
|
346
346
|
*/
|
|
347
347
|
declare function ambiguousMessageKind(position: AstmPosition): AstmRecordWarning;
|
|
348
348
|
/**
|
|
349
|
-
* Build a `PROFILE_QUIRK_APPLIED` warning
|
|
349
|
+
* Build a `PROFILE_QUIRK_APPLIED` warning: the downgraded form an active vendor profile produces from
|
|
350
350
|
* a deviation it *expects*. The original warning is **not dropped**: its `code` moves to
|
|
351
351
|
* `toleratedCode`, the warning is re-badged `PROFILE_QUIRK_APPLIED`, `expected` is set, and the
|
|
352
352
|
* tolerating profile is named. The original `position` and `message` are preserved (both PHI-free by
|
|
@@ -373,7 +373,7 @@ declare function profileQuirkApplied(original: AstmRecordWarning, profileName: s
|
|
|
373
373
|
*
|
|
374
374
|
* ASTM/CLSI-LIS02 messages are **self-describing**: every `H` (header) record
|
|
375
375
|
* declares the four delimiters it uses, in the bytes immediately after the `H`.
|
|
376
|
-
* A parser MUST read them from the record and MUST NOT hardcode them
|
|
376
|
+
* A parser MUST read them from the record and MUST NOT hardcode them: a vendor
|
|
377
377
|
* is free to declare non-standard delimiters and a conformant reader follows.
|
|
378
378
|
*
|
|
379
379
|
* The canonical declaration is `H|\^&`:
|
|
@@ -405,18 +405,18 @@ declare function profileQuirkApplied(original: AstmRecordWarning, profileName: s
|
|
|
405
405
|
* ```
|
|
406
406
|
*/
|
|
407
407
|
interface Delimiters {
|
|
408
|
-
/** Field separator
|
|
408
|
+
/** Field separator: the char immediately after `H` in the header. */
|
|
409
409
|
readonly field: string;
|
|
410
|
-
/** Repeat (repetition) separator
|
|
410
|
+
/** Repeat (repetition) separator: ASTM `\` by default. */
|
|
411
411
|
readonly repeat: string;
|
|
412
|
-
/** Component separator
|
|
412
|
+
/** Component separator: ASTM `^` by default. */
|
|
413
413
|
readonly component: string;
|
|
414
|
-
/** Escape character
|
|
414
|
+
/** Escape character: ASTM `&` by default (introduces `&F&`/`&S&`/`&R&`/`&E&`). */
|
|
415
415
|
readonly escape: string;
|
|
416
416
|
}
|
|
417
417
|
/**
|
|
418
418
|
* The canonical ASTM delimiter set (`H|\^&`). Used only as documentation and as
|
|
419
|
-
* a comparison baseline for the non-standard-delimiter warning
|
|
419
|
+
* a comparison baseline for the non-standard-delimiter warning: never as a
|
|
420
420
|
* parse-time default (delimiters are always read from the header).
|
|
421
421
|
*
|
|
422
422
|
* @example
|
|
@@ -430,14 +430,14 @@ declare const CANONICAL_DELIMITERS: Delimiters;
|
|
|
430
430
|
* Read the four delimiters from a header record's raw text (a single `H`
|
|
431
431
|
* record, its terminator already stripped).
|
|
432
432
|
*
|
|
433
|
-
* Returns `undefined` when the record cannot declare all four delimiters
|
|
433
|
+
* Returns `undefined` when the record cannot declare all four delimiters: it is
|
|
434
434
|
* shorter than `H` + a field separator + a 3-char delimiter definition, or the
|
|
435
435
|
* field separator it names also appears among the other three.
|
|
436
436
|
*
|
|
437
437
|
* What the caller does with that depends on **which** header it is. On the first
|
|
438
438
|
* header it is the `ASTM_RECORD_UNDECLARED_DELIMITERS` fatal, because there is no
|
|
439
|
-
* earlier set to fall back to. On any later header
|
|
440
|
-
* messages, each declaring its own set
|
|
439
|
+
* earlier set to fall back to. On any later header (a stream may carry several
|
|
440
|
+
* messages, each declaring its own set) the delimiters already in force are kept
|
|
441
441
|
* and an `ASTM_RECORD_UNREADABLE_REDECLARATION` warning is raised instead; a set is
|
|
442
442
|
* never guessed and no record is dropped.
|
|
443
443
|
*
|
|
@@ -468,12 +468,12 @@ declare function readDelimiters(headerRecord: string): Delimiters | undefined;
|
|
|
468
468
|
declare function isNonStandard(d: Delimiters): boolean;
|
|
469
469
|
|
|
470
470
|
/**
|
|
471
|
-
* The ASTM date/time value
|
|
471
|
+
* The ASTM date/time value: `YYYYMMDDHHMMSS`, precision-preserving, no timezone.
|
|
472
472
|
*
|
|
473
473
|
* ASTM timestamps are a run of digits, most-significant first, truncated at
|
|
474
474
|
* whatever precision the instrument reports. There is **no timezone field**:
|
|
475
475
|
* the value is local to the instrument and this parser never assumes UTC. A
|
|
476
|
-
* date-only value (`YYYYMMDD`) is a normal, complete value at day precision
|
|
476
|
+
* date-only value (`YYYYMMDD`) is a normal, complete value at day precision:
|
|
477
477
|
* **not** an error and never zero-filled into a fake time.
|
|
478
478
|
*/
|
|
479
479
|
/** The precision to which an {@link AstmDate} is populated. */
|
|
@@ -482,7 +482,7 @@ type AstmDatePrecision = "year" | "month" | "day" | "hour" | "minute" | "second"
|
|
|
482
482
|
* A parsed ASTM date/time. Immutable plain data; the populated fields extend
|
|
483
483
|
* exactly as far as {@link AstmDate.precision}. Absent components are left
|
|
484
484
|
* `undefined` rather than defaulted, so a consumer can tell "midnight" from
|
|
485
|
-
* "no time given". No timezone is modeled
|
|
485
|
+
* "no time given". No timezone is modeled: the value is instrument-local.
|
|
486
486
|
*
|
|
487
487
|
* @example
|
|
488
488
|
* ```ts
|
|
@@ -504,10 +504,10 @@ interface AstmDate {
|
|
|
504
504
|
/** How far the components are populated. */
|
|
505
505
|
readonly precision: AstmDatePrecision;
|
|
506
506
|
/**
|
|
507
|
-
* `true` when the digit run does **not** align to a whole-component boundary
|
|
507
|
+
* `true` when the digit run does **not** align to a whole-component boundary: an odd number of
|
|
508
508
|
* digits that cuts a two-digit component (month/day/hour/minute/second) in half (lengths 5, 7, 9,
|
|
509
509
|
* 11, 13). The full run is preserved in {@link AstmDate.raw} and the structured value is truncated
|
|
510
|
-
* to the last **complete** component
|
|
510
|
+
* to the last **complete** component: the dangling digit is **never zero-filled into a fabricated
|
|
511
511
|
* time**. Absent (never `false`) for a clean value. A caller surfaces this as a value-free
|
|
512
512
|
* `ASTM_RECORD_PARTIAL_TIMESTAMP` warning.
|
|
513
513
|
*/
|
|
@@ -516,7 +516,7 @@ interface AstmDate {
|
|
|
516
516
|
/**
|
|
517
517
|
* Parse an ASTM `YYYYMMDDHHMMSS` value, preserving whatever precision is
|
|
518
518
|
* present. Returns `undefined` for a value that is not a usable timestamp (fewer
|
|
519
|
-
* than four leading digits for the year, or non-digit content)
|
|
519
|
+
* than four leading digits for the year, or non-digit content): a caller keeps
|
|
520
520
|
* the raw field text either way, so nothing is lost. A partial value is parsed,
|
|
521
521
|
* never rejected.
|
|
522
522
|
*
|
|
@@ -535,7 +535,7 @@ interface AstmDate {
|
|
|
535
535
|
declare function parseAstmDate(raw: string): AstmDate | undefined;
|
|
536
536
|
/**
|
|
537
537
|
* Render an {@link AstmDate} as an ISO-8601-*like* string truncated to its
|
|
538
|
-
* precision, with **no** `Z` and **no** offset
|
|
538
|
+
* precision, with **no** `Z` and **no** offset: because ASTM carries no
|
|
539
539
|
* timezone and appending one would fabricate information. A consumer that knows
|
|
540
540
|
* the instrument's zone can attach it; this function never assumes UTC.
|
|
541
541
|
*
|
|
@@ -556,9 +556,9 @@ declare function astmDateToLocalISO(d: AstmDate): string;
|
|
|
556
556
|
* spec's component order is LOINC-slot / test-name / coding-scheme / local-code.
|
|
557
557
|
* In the field the way real analyzers emit it:
|
|
558
558
|
*
|
|
559
|
-
* - **component 1** is a LOINC slot
|
|
559
|
+
* - **component 1** is a LOINC slot: *almost always empty*; a few vendors place
|
|
560
560
|
* an inline LOINC when one exists;
|
|
561
|
-
* - **component 4** is the vendor/local code
|
|
561
|
+
* - **component 4** is the vendor/local code: the identifier that is actually
|
|
562
562
|
* present, and therefore the **primary** identifier here.
|
|
563
563
|
*
|
|
564
564
|
* Following the sibling parsers' conservative posture, this module is a
|
|
@@ -568,11 +568,11 @@ declare function astmDateToLocalISO(d: AstmDate): string;
|
|
|
568
568
|
*/
|
|
569
569
|
/** Where a Universal Test ID's usable identifier came from. */
|
|
570
570
|
type UniversalTestIdProvenance =
|
|
571
|
-
/** Component 1 (the LOINC slot) is populated
|
|
571
|
+
/** Component 1 (the LOINC slot) is populated: a candidate LOINC, recognized not validated. */
|
|
572
572
|
"inline-loinc-candidate"
|
|
573
573
|
/** Component 4 (the vendor/local code) carries the identifier. */
|
|
574
574
|
| "local-code"
|
|
575
|
-
/** Only the test name (component 2) is present
|
|
575
|
+
/** Only the test name (component 2) is present: no code. */
|
|
576
576
|
| "name-only"
|
|
577
577
|
/** Nothing usable in the field. */
|
|
578
578
|
| "empty";
|
|
@@ -591,13 +591,13 @@ type UniversalTestIdProvenance =
|
|
|
591
591
|
interface UniversalTestId {
|
|
592
592
|
/** The field's components, verbatim and in order. */
|
|
593
593
|
readonly components: readonly string[];
|
|
594
|
-
/** Component 1 when populated
|
|
594
|
+
/** Component 1 when populated: a *candidate* LOINC (provenance only, never validated). */
|
|
595
595
|
readonly loincCandidate?: string;
|
|
596
|
-
/** Component 2
|
|
596
|
+
/** Component 2: the test / battery name, when present. */
|
|
597
597
|
readonly testName?: string;
|
|
598
|
-
/** Component 3
|
|
598
|
+
/** Component 3: the coding-scheme selector, when present. */
|
|
599
599
|
readonly codingScheme?: string;
|
|
600
|
-
/** Component 4
|
|
600
|
+
/** Component 4, the vendor/local code: the primary identifier when no inline LOINC is given. */
|
|
601
601
|
readonly localCode?: string;
|
|
602
602
|
/** Where the primary identifier came from. */
|
|
603
603
|
readonly provenance: UniversalTestIdProvenance;
|
|
@@ -617,7 +617,7 @@ declare function recognizeUniversalTestId(components: readonly string[]): Univer
|
|
|
617
617
|
/**
|
|
618
618
|
* The primary code to key a result on: the inline LOINC candidate when a vendor
|
|
619
619
|
* supplied one, otherwise the local (vendor) code. Returns `undefined` when the
|
|
620
|
-
* field carries no code at all (name-only or empty)
|
|
620
|
+
* field carries no code at all (name-only or empty): never a guess.
|
|
621
621
|
*
|
|
622
622
|
* @param u - A recognized Universal Test ID.
|
|
623
623
|
* @returns The primary code, or `undefined`.
|
|
@@ -633,7 +633,7 @@ declare function primaryCode(u: UniversalTestId): string | undefined;
|
|
|
633
633
|
* Positional context for frame-codec warnings and errors.
|
|
634
634
|
*
|
|
635
635
|
* **PHI discipline (the whole point).** A frame deviation is located by **frame
|
|
636
|
-
* number + byte offset** and nothing else
|
|
636
|
+
* number + byte offset** and nothing else: never the record text a frame
|
|
637
637
|
* carries. A frame warning or error may be logged verbatim without ever leaking a
|
|
638
638
|
* result value, a patient identifier, or any other field the reassembled record
|
|
639
639
|
* bytes contain.
|
|
@@ -659,12 +659,12 @@ interface AstmFramePosition {
|
|
|
659
659
|
}
|
|
660
660
|
|
|
661
661
|
/**
|
|
662
|
-
* The **frame** warning registry (`ASTM_FRAME_*`) for the ASTM/CLSI-LIS01 codec
|
|
662
|
+
* The **frame** warning registry (`ASTM_FRAME_*`) for the ASTM/CLSI-LIS01 codec:
|
|
663
663
|
* the second of the package's two registries, alongside the record layer's
|
|
664
664
|
* `ASTM_RECORD_*` (both share only the `EMPTY_INPUT` fatal).
|
|
665
665
|
*
|
|
666
666
|
* A frame warning is the lenient codec's record of a tolerated byte-level
|
|
667
|
-
* deviation: it never crashes, never invents a value, and
|
|
667
|
+
* deviation: it never crashes, never invents a value, and, critically, never
|
|
668
668
|
* lets an untrusted frame be silently merged into a record. Every warning carries
|
|
669
669
|
* a stable code plus an {@link AstmFramePosition} (frame number + byte offset) and
|
|
670
670
|
* **never** the record bytes a frame carries (PHI discipline). Consumers compare
|
|
@@ -689,12 +689,12 @@ declare const FRAME_WARNING_CODES: {
|
|
|
689
689
|
* A frame's two-hex-char checksum did not match the modulo-256 sum recomputed over its bytes. The
|
|
690
690
|
* frame is surfaced with `trusted: false` and its text is **never merged** into a reassembled
|
|
691
691
|
* record (default **warn** in lenient mode, escalated to a thrown error in `strict`). Corruption is
|
|
692
|
-
* never silently trusted
|
|
692
|
+
* never silently trusted: the "checksums are routinely not validated" claim was refuted; we
|
|
693
693
|
* validate.
|
|
694
694
|
*/
|
|
695
695
|
readonly ASTM_FRAME_BAD_CHECKSUM: "ASTM_FRAME_BAD_CHECKSUM";
|
|
696
696
|
/**
|
|
697
|
-
* A frame's sequence number was not the expected next value (`1 → … → 7 → 0 → …`)
|
|
697
|
+
* A frame's sequence number was not the expected next value (`1 → … → 7 → 0 → …`): a frame was
|
|
698
698
|
* possibly dropped. The stream is **never silently concatenated across the gap** as if contiguous;
|
|
699
699
|
* the in-progress record is tainted and not emitted as a clean reassembly.
|
|
700
700
|
*/
|
|
@@ -708,7 +708,7 @@ declare const FRAME_WARNING_CODES: {
|
|
|
708
708
|
readonly ASTM_FRAME_UNTERMINATED: "ASTM_FRAME_UNTERMINATED";
|
|
709
709
|
/**
|
|
710
710
|
* A frame's record text exceeded the 240-byte limit without a split. The frame is still surfaced
|
|
711
|
-
* (and, if its checksum validates, reassembled)
|
|
711
|
+
* (and, if its checksum validates, reassembled): the deviation is flagged, not silently dropped
|
|
712
712
|
* (warn in lenient, thrown in `strict`).
|
|
713
713
|
*/
|
|
714
714
|
readonly ASTM_FRAME_OVERSIZE: "ASTM_FRAME_OVERSIZE";
|
|
@@ -785,14 +785,14 @@ declare function frameOversize(position: AstmFramePosition): AstmFrameWarning;
|
|
|
785
785
|
|
|
786
786
|
/**
|
|
787
787
|
* The **LTP** warning registry (`ASTM_LTP_*`) for the ASTM/CLSI-LIS01 protocol
|
|
788
|
-
* layer
|
|
788
|
+
* layer: the third of the package's registries, alongside the record layer's
|
|
789
789
|
* `ASTM_RECORD_*` and the frame codec's `ASTM_FRAME_*`.
|
|
790
790
|
*
|
|
791
791
|
* An LTP warning is the protocol reducer's (or the transport detector's) record
|
|
792
792
|
* of a tolerated session-level deviation: an ambiguous framed/raw stream, a
|
|
793
793
|
* control event that arrived in a state that did not expect it, or a frame the
|
|
794
794
|
* receiver had to reject (`NAK`) rather than accept. Every warning carries a
|
|
795
|
-
* stable code plus, at most, a **frame number
|
|
795
|
+
* stable code plus, at most, a **frame number**: never the record bytes a frame
|
|
796
796
|
* carries (PHI discipline). Consumers compare `warning.code ===
|
|
797
797
|
* LTP_WARNING_CODES.<CODE>`; renaming a code is a **breaking change**.
|
|
798
798
|
*/
|
|
@@ -809,21 +809,21 @@ declare function frameOversize(position: AstmFramePosition): AstmFrameWarning;
|
|
|
809
809
|
declare const LTP_WARNING_CODES: {
|
|
810
810
|
/**
|
|
811
811
|
* The transport detector could not tell a framed stream from a raw (unframed) one from its leading
|
|
812
|
-
* byte
|
|
812
|
+
* byte: it was neither `STX`/`ENQ` nor a bare record letter. It **defaults to framed** and warns,
|
|
813
813
|
* never guessing silently into data loss; a profile override forces the mode. (cobas b121 drops
|
|
814
|
-
* framing over TCP; cobas 4800 / Iguana retain it
|
|
814
|
+
* framing over TCP; cobas 4800 / Iguana retain it, both realities exist, so an unrecognizable lead
|
|
815
815
|
* is defaulted, not assumed.)
|
|
816
816
|
*/
|
|
817
817
|
readonly ASTM_LTP_AMBIGUOUS_TRANSPORT: "ASTM_LTP_AMBIGUOUS_TRANSPORT";
|
|
818
818
|
/**
|
|
819
|
-
* A control event arrived in a protocol state that did not expect it
|
|
819
|
+
* A control event arrived in a protocol state that did not expect it: e.g. an inbound `ACK`/`NAK`
|
|
820
820
|
* at a receiver (which sends, never receives, those), or an `ENQ` mid-transfer. The event is
|
|
821
821
|
* surfaced and handled defensively (an unexpected `ACK`/`NAK` is **never** read as acceptance of
|
|
822
822
|
* data); it never advances the transfer as if valid.
|
|
823
823
|
*/
|
|
824
824
|
readonly ASTM_LTP_UNEXPECTED_EVENT: "ASTM_LTP_UNEXPECTED_EVENT";
|
|
825
825
|
/**
|
|
826
|
-
* The receiver rejected a received frame with a `NAK` instead of accepting it
|
|
826
|
+
* The receiver rejected a received frame with a `NAK` instead of accepting it: because its
|
|
827
827
|
* checksum failed, it was unterminated, or its frame number was out of sequence. The frame's text
|
|
828
828
|
* is **never** appended to the record and the transfer does **not** advance; the receiver awaits the
|
|
829
829
|
* sender's retransmit. This is the protocol-level face of the frame codec's fail-safe: a bad frame
|
|
@@ -838,7 +838,7 @@ declare const LTP_WARNING_CODES: {
|
|
|
838
838
|
type LtpWarningCode = (typeof LTP_WARNING_CODES)[keyof typeof LTP_WARNING_CODES];
|
|
839
839
|
/**
|
|
840
840
|
* A single LTP protocol / transport warning: a stable code, a value-free
|
|
841
|
-
* human-readable message, and
|
|
841
|
+
* human-readable message, and, for a frame-scoped deviation, the frame number
|
|
842
842
|
* only. Never carries a frame's record bytes.
|
|
843
843
|
*
|
|
844
844
|
* @example
|
|
@@ -846,7 +846,7 @@ type LtpWarningCode = (typeof LTP_WARNING_CODES)[keyof typeof LTP_WARNING_CODES]
|
|
|
846
846
|
* import type { AstmLtpWarning } from "@cosyte/astm";
|
|
847
847
|
* const w: AstmLtpWarning = {
|
|
848
848
|
* code: "ASTM_LTP_FRAME_REJECTED",
|
|
849
|
-
* message: "Frame rejected
|
|
849
|
+
* message: "Frame rejected, NAK sent, retransmit expected.",
|
|
850
850
|
* frameNumber: 2,
|
|
851
851
|
* };
|
|
852
852
|
* ```
|
|
@@ -901,22 +901,22 @@ declare function ltpFrameRejected(frameNumber?: number): AstmLtpWarning;
|
|
|
901
901
|
* (RS-232) always uses full E1381 framing. Over **TCP it varies within a single
|
|
902
902
|
* vendor**: the Roche cobas 4800 and iNTERFACEWARE Iguana retain the full
|
|
903
903
|
* `ENQ`/`ACK` + `STX`/checksum framing over TCP (**framed-TCP**), while the cobas
|
|
904
|
-
* b121 drops all low-level framing
|
|
904
|
+
* b121 drops all low-level framing, "TCP itself ensures correctness", and streams
|
|
905
905
|
* de-framed record bytes directly (**raw-TCP**). A reader must handle both and must
|
|
906
906
|
* not assume the handshake is present.
|
|
907
907
|
*
|
|
908
908
|
* {@link detectFraming} decides which it is from the stream's leading byte: `STX`
|
|
909
909
|
* or `ENQ` ⇒ framed (feed it to the frame codec + {@link ltpReduce}); a bare record
|
|
910
910
|
* letter ⇒ raw (feed it straight to `parseAstmRecords`). Anything else is
|
|
911
|
-
* **ambiguous**, and the fail-safe is to **default to framed** and warn
|
|
912
|
-
* guess silently into data loss
|
|
911
|
+
* **ambiguous**, and the fail-safe is to **default to framed** and warn, never to
|
|
912
|
+
* guess silently into data loss, with an explicit override for a profile that
|
|
913
913
|
* knows better.
|
|
914
914
|
*/
|
|
915
915
|
|
|
916
916
|
/**
|
|
917
917
|
* The transport framing of an ASTM byte stream: `"framed"` (E1381 `STX`/checksum
|
|
918
918
|
* frames, the serial and framed-TCP realities) or `"raw"` (framing dropped,
|
|
919
|
-
* de-framed record bytes streamed directly
|
|
919
|
+
* de-framed record bytes streamed directly, the raw-TCP reality).
|
|
920
920
|
*/
|
|
921
921
|
type AstmFraming = "framed" | "raw";
|
|
922
922
|
/**
|
|
@@ -925,7 +925,7 @@ type AstmFraming = "framed" | "raw";
|
|
|
925
925
|
interface DetectFramingOptions {
|
|
926
926
|
/**
|
|
927
927
|
* A profile-supplied override. When set, detection is bypassed entirely and this
|
|
928
|
-
* value is returned with no warning
|
|
928
|
+
* value is returned with no warning: the way a vendor profile forces raw for
|
|
929
929
|
* a cobas b121 or framed for a cobas 4800 regardless of the leading byte.
|
|
930
930
|
*/
|
|
931
931
|
readonly override?: AstmFraming;
|
|
@@ -975,7 +975,7 @@ declare function detectFraming(bytes: Uint8Array, options?: DetectFramingOptions
|
|
|
975
975
|
* plus an optional **transport override** (the raw-vs-framed-TCP knob). It mirrors
|
|
976
976
|
* the sibling `@cosyte/hl7` `defineProfile` and `@cosyte/ccda` `defineCcdaProfile`
|
|
977
977
|
* shape (`name` / `lineage` / `describe()` / `extends`-merge) but models ASTM's
|
|
978
|
-
* quirks
|
|
978
|
+
* quirks (a warning code the stream is expected to trip, and its framing reality)
|
|
979
979
|
* rather than HL7 v2 Z-segments or C-CDA template deviations.
|
|
980
980
|
*
|
|
981
981
|
* A profile **never changes an extracted value**. It operates purely at the
|
|
@@ -984,29 +984,29 @@ declare function detectFraming(bytes: Uint8Array, options?: DetectFramingOptions
|
|
|
984
984
|
* layer (forcing framed/raw before a single byte is parsed). The two load-bearing
|
|
985
985
|
* safety rules, enforced by `defineAstmProfile`:
|
|
986
986
|
*
|
|
987
|
-
* 1. **A profile can never tolerate a safety-critical warning code
|
|
987
|
+
* 1. **A profile can never tolerate a safety-critical warning code**: a result
|
|
988
988
|
* value, abnormal flag, result status, reference range, units, patient/comment
|
|
989
989
|
* context, message-kind ambiguity, code system, or any frame/LTP integrity
|
|
990
990
|
* warning (see `src/profiles/safety.ts`). Naming one is a *definition-time
|
|
991
|
-
* throw*, not a silent relaxation
|
|
991
|
+
* throw*, not a silent relaxation, so a profile can never make a bad checksum
|
|
992
992
|
* "ok" or a cancelled result read "final."
|
|
993
993
|
* 2. **A tolerated deviation is downgraded, never dropped.** The parser still
|
|
994
994
|
* records it (re-coded {@link WARNING_CODES.PROFILE_QUIRK_APPLIED}, flagged
|
|
995
995
|
* `expected: true`, carrying the original `toleratedCode`) so nothing is
|
|
996
|
-
* silently hidden
|
|
996
|
+
* silently hidden, Postel's Law with a receipt.
|
|
997
997
|
*/
|
|
998
998
|
|
|
999
999
|
/**
|
|
1000
|
-
* Any warning code from any of the three registries
|
|
1000
|
+
* Any warning code from any of the three registries: the record layer
|
|
1001
1001
|
* (`ASTM_RECORD_*` / `ASTM_NONSTANDARD_DELIMITERS` / `ASTM_UNKNOWN_ESCAPE_SEQUENCE`),
|
|
1002
1002
|
* the frame codec (`ASTM_FRAME_*`), or the LTP protocol layer (`ASTM_LTP_*`). A
|
|
1003
1003
|
* {@link QuirkTolerance} may *name* any of them, but the safety gate refuses all
|
|
1004
|
-
* but the small, benign, record-layer subset
|
|
1004
|
+
* but the small, benign, record-layer subset, so the union exists mainly to let
|
|
1005
1005
|
* the gate reject a frame/LTP code with a precise, typed message.
|
|
1006
1006
|
*/
|
|
1007
1007
|
type AnyAstmWarningCode = WarningCode | FrameWarningCode | LtpWarningCode;
|
|
1008
1008
|
/**
|
|
1009
|
-
* Provenance for an {@link AstmProfile}
|
|
1009
|
+
* Provenance for an {@link AstmProfile}: the **real, cited public artifact** a
|
|
1010
1010
|
* profile's quirks are grounded in. A quirk is encoded only when a real
|
|
1011
1011
|
* (de-identified) document or a redistributable reference corpus grounds it; this
|
|
1012
1012
|
* record is where that grounding is stated, so a reviewer can trace every
|
|
@@ -1025,7 +1025,7 @@ type AnyAstmWarningCode = WarningCode | FrameWarningCode | LtpWarningCode;
|
|
|
1025
1025
|
interface AstmProfileProvenance {
|
|
1026
1026
|
/** Short human-readable name of the grounding source (corpus, transcript, or spec). */
|
|
1027
1027
|
readonly source: string;
|
|
1028
|
-
/** A citation the grounding can be traced to
|
|
1028
|
+
/** A citation the grounding can be traced to: a URL, DOI, or repo+path. */
|
|
1029
1029
|
readonly reference: string;
|
|
1030
1030
|
/** When the grounding was last verified (ISO date) or the pinned commit SHA. */
|
|
1031
1031
|
readonly retrieved?: string;
|
|
@@ -1035,7 +1035,7 @@ interface AstmProfileProvenance {
|
|
|
1035
1035
|
/**
|
|
1036
1036
|
* Optional structural narrowing for a {@link QuirkTolerance}. When present, the
|
|
1037
1037
|
* tolerance applies only to warnings whose PHI-free {@link AstmPosition} matches
|
|
1038
|
-
* every provided field
|
|
1038
|
+
* every provided field, so a profile can expect a deviation on one record type
|
|
1039
1039
|
* (e.g. an unknown escape only inside `R` result values) without blanket-tolerating
|
|
1040
1040
|
* it everywhere. Matching is on **structural identifiers only** (record-type
|
|
1041
1041
|
* letter, 1-based field index); there is no matching on any field *value*, by
|
|
@@ -1072,7 +1072,7 @@ interface AstmQuirkMatch {
|
|
|
1072
1072
|
interface AstmQuirkTolerance {
|
|
1073
1073
|
/** The existing, non-safety-critical warning code this profile expects. */
|
|
1074
1074
|
readonly code: AnyAstmWarningCode;
|
|
1075
|
-
/** Why the profile expects this deviation
|
|
1075
|
+
/** Why the profile expects this deviation: grounded in its provenance. */
|
|
1076
1076
|
readonly rationale: string;
|
|
1077
1077
|
/** Optional structural narrowing (record type / field index). */
|
|
1078
1078
|
readonly match?: AstmQuirkMatch;
|
|
@@ -1082,7 +1082,7 @@ interface AstmQuirkTolerance {
|
|
|
1082
1082
|
* {@link defineAstmProfile}; consumers pass it to `parseAstmRecords(raw, { profile })`
|
|
1083
1083
|
* (or register it as the process default), and feed its {@link AstmProfile.transport}
|
|
1084
1084
|
* to `detectFraming(bytes, { override })`. Hand-authoring the object literal is
|
|
1085
|
-
* supported but discouraged
|
|
1085
|
+
* supported but discouraged: the factory validates the safety rules and attaches
|
|
1086
1086
|
* `describe()`.
|
|
1087
1087
|
*
|
|
1088
1088
|
* @example
|
|
@@ -1097,13 +1097,13 @@ interface AstmProfile {
|
|
|
1097
1097
|
readonly name: string;
|
|
1098
1098
|
/** Optional human-readable description. */
|
|
1099
1099
|
readonly description?: string;
|
|
1100
|
-
/** Resolved lineage
|
|
1100
|
+
/** Resolved lineage: `[...parents, name]`, first-occurrence deduped. */
|
|
1101
1101
|
readonly lineage: readonly string[];
|
|
1102
1102
|
/** The expected, non-safety-critical deviations this profile tolerates. */
|
|
1103
1103
|
readonly tolerate: readonly AstmQuirkTolerance[];
|
|
1104
1104
|
/**
|
|
1105
1105
|
* The raw-vs-framed-TCP override. When set, a consumer forces this framing via
|
|
1106
|
-
* `detectFraming(bytes, { override: profile.transport })
|
|
1106
|
+
* `detectFraming(bytes, { override: profile.transport })`, the way a profile
|
|
1107
1107
|
* that knows a vendor's transport reality (e.g. framing dropped over raw TCP)
|
|
1108
1108
|
* bypasses leading-byte auto-detection. Absent means "let detection decide."
|
|
1109
1109
|
*/
|
|
@@ -1150,33 +1150,33 @@ interface DefineAstmProfileOptions {
|
|
|
1150
1150
|
* coerced to `normal`**.
|
|
1151
1151
|
* - A **result status** (field 9) is modeled so a **correction (`C`)** or a
|
|
1152
1152
|
* **cancellation (`X`)** can never read as an active-final result, and an
|
|
1153
|
-
* **absent** status is a typed `unspecified
|
|
1153
|
+
* **absent** status is a typed `unspecified`: **never assumed `F` (final)**.
|
|
1154
1154
|
* - A **reference range** (field 6) is parsed into a low/high (or open-ended)
|
|
1155
|
-
* pair; an unparseable range is surfaced **verbatim
|
|
1155
|
+
* pair; an unparseable range is surfaced **verbatim**, a bound is **never
|
|
1156
1156
|
* fabricated**.
|
|
1157
1157
|
*
|
|
1158
1158
|
* Every bound and code is surfaced verbatim; nothing is collapsed, reconciled,
|
|
1159
1159
|
* or converted. Units are handled at the record layer (a numeric value without
|
|
1160
|
-
* units warns; units are never defaulted, guessed, or converted
|
|
1160
|
+
* units warns; units are never defaulted, guessed, or converted, see
|
|
1161
1161
|
* `./parse.ts`).
|
|
1162
1162
|
*/
|
|
1163
1163
|
/**
|
|
1164
1164
|
* The recognized abnormal-flag letters, from **HL7 v2.2 Table 0078** (the flag
|
|
1165
|
-
* *value* set
|
|
1165
|
+
* *value* set, a published fact set, not CLSI prose). Read in the context of an
|
|
1166
1166
|
* `R` record's field 7: here `S`/`R`/`I` are the microbiology susceptibility
|
|
1167
1167
|
* codes and `R` is **not** the repeat delimiter.
|
|
1168
1168
|
*/
|
|
1169
1169
|
type AbnormalFlagCode = "L" | "H" | "LL" | "HH" | "<" | ">" | "N" | "A" | "AA" | "U" | "D" | "B" | "W" | "S" | "R" | "I";
|
|
1170
1170
|
/**
|
|
1171
1171
|
* The modeled meaning of a recognized {@link AbnormalFlagCode}, plus the two
|
|
1172
|
-
* fail-safe sentinels: `undefined` (a flag was present but is not in Table 0078)
|
|
1173
|
-
*
|
|
1172
|
+
* fail-safe sentinels: `undefined` (a flag was present but is not in Table 0078),
|
|
1173
|
+
* which is **never** collapsed to `normal`.
|
|
1174
1174
|
*/
|
|
1175
1175
|
type AbnormalFlagMeaning = "below-normal" | "above-normal" | "critically-below-normal" | "critically-above-normal" | "below-scale" | "above-scale" | "normal" | "abnormal" | "very-abnormal" | "significant-change-up" | "significant-change-down" | "better" | "worse" | "susceptible" | "resistant" | "intermediate" | "undefined";
|
|
1176
1176
|
/**
|
|
1177
1177
|
* A recognized (or explicitly *un*recognized) abnormal flag. The raw field text
|
|
1178
1178
|
* is always preserved; `recognized` is `false` and `meaning` is `"undefined"`
|
|
1179
|
-
* for any letter outside Table 0078
|
|
1179
|
+
* for any letter outside Table 0078, the flag is surfaced, never dropped, and
|
|
1180
1180
|
* **never coerced to `normal`**.
|
|
1181
1181
|
*
|
|
1182
1182
|
* @example
|
|
@@ -1202,7 +1202,7 @@ interface AbnormalFlag {
|
|
|
1202
1202
|
*
|
|
1203
1203
|
* Leading/trailing whitespace is ignored for the lookup but the `raw` text is
|
|
1204
1204
|
* preserved verbatim. An unrecognized flag yields `{ recognized: false, meaning:
|
|
1205
|
-
* "undefined" }
|
|
1205
|
+
* "undefined" }`, surfaced, never dropped, and **never `"normal"`**.
|
|
1206
1206
|
*
|
|
1207
1207
|
* @param raw - The verbatim field-7 text.
|
|
1208
1208
|
* @returns The interpreted flag.
|
|
@@ -1216,13 +1216,13 @@ interface AbnormalFlag {
|
|
|
1216
1216
|
declare function interpretAbnormalFlag(raw: string): AbnormalFlag;
|
|
1217
1217
|
/**
|
|
1218
1218
|
* The recognized result-status letters (`R`-record field 9). The clinically
|
|
1219
|
-
* load-bearing members are **`C` (correction
|
|
1219
|
+
* load-bearing members are **`C` (correction, supersedes a previously
|
|
1220
1220
|
* transmitted value)** and **`X` (cannot be done / cancelled)**.
|
|
1221
1221
|
*/
|
|
1222
1222
|
type ResultStatusCode = "F" | "C" | "P" | "R" | "S" | "I" | "X";
|
|
1223
1223
|
/**
|
|
1224
1224
|
* The modeled meaning of a result status, plus two fail-safe sentinels:
|
|
1225
|
-
* `unspecified` (the field was absent
|
|
1225
|
+
* `unspecified` (the field was absent, **never assumed `final`**) and
|
|
1226
1226
|
* `undefined` (a letter was present but is not a recognized status).
|
|
1227
1227
|
*/
|
|
1228
1228
|
type ResultStatusMeaning = "final" | "correction" | "preliminary" | "previously-transmitted" | "partial" | "pending" | "cancelled" | "unspecified" | "undefined";
|
|
@@ -1232,11 +1232,11 @@ type ResultStatusMeaning = "final" | "correction" | "preliminary" | "previously-
|
|
|
1232
1232
|
*
|
|
1233
1233
|
* - **`isActiveFinal`** is `true` **only** for a plain `F` (final). It is `false`
|
|
1234
1234
|
* for a correction (`C`), a cancellation (`X`), a preliminary/partial/pending
|
|
1235
|
-
* result, an absent status, and an unrecognized one
|
|
1235
|
+
* result, an absent status, and an unrecognized one, so a superseded or
|
|
1236
1236
|
* cancelled result can **never** read as current/final.
|
|
1237
|
-
* - **`supersedes`** is `true` for `C
|
|
1237
|
+
* - **`supersedes`** is `true` for `C`: this value replaces a previously
|
|
1238
1238
|
* transmitted one.
|
|
1239
|
-
* - **`cancelled`** is `true` for `X
|
|
1239
|
+
* - **`cancelled`** is `true` for `X`: the result cannot be done / was cancelled.
|
|
1240
1240
|
*
|
|
1241
1241
|
* @example
|
|
1242
1242
|
* ```ts
|
|
@@ -1255,17 +1255,17 @@ interface ResultStatus {
|
|
|
1255
1255
|
readonly meaning: ResultStatusMeaning;
|
|
1256
1256
|
/** Whether the raw text matched a recognized status letter. */
|
|
1257
1257
|
readonly recognized: boolean;
|
|
1258
|
-
/** `true` **only** for a plain `F` (final)
|
|
1258
|
+
/** `true` **only** for a plain `F` (final): never for `C`, `X`, absent, or unrecognized. */
|
|
1259
1259
|
readonly isActiveFinal: boolean;
|
|
1260
|
-
/** `true` for `C
|
|
1260
|
+
/** `true` for `C`: this value supersedes a previously transmitted result. */
|
|
1261
1261
|
readonly supersedes: boolean;
|
|
1262
|
-
/** `true` for `X
|
|
1262
|
+
/** `true` for `X`: the result cannot be done / was cancelled. */
|
|
1263
1263
|
readonly cancelled: boolean;
|
|
1264
1264
|
}
|
|
1265
1265
|
/**
|
|
1266
1266
|
* Interpret an `R`-record result-status field (field 9).
|
|
1267
1267
|
*
|
|
1268
|
-
* An **absent** status (`undefined` or empty) is typed `unspecified
|
|
1268
|
+
* An **absent** status (`undefined` or empty) is typed `unspecified`: **never
|
|
1269
1269
|
* assumed `final`**. A **present but unrecognized** letter is `undefined`
|
|
1270
1270
|
* (recognized `false`). In every non-`F` case `isActiveFinal` is `false`, so a
|
|
1271
1271
|
* correction, a cancellation, or an unknown status can never read as current.
|
|
@@ -1284,10 +1284,10 @@ declare function interpretResultStatus(raw: string | undefined): ResultStatus;
|
|
|
1284
1284
|
/**
|
|
1285
1285
|
* The shape of a parsed reference range.
|
|
1286
1286
|
*
|
|
1287
|
-
* - `closed
|
|
1288
|
-
* - `open-low
|
|
1289
|
-
* - `open-high
|
|
1290
|
-
* - `unparsed
|
|
1287
|
+
* - `closed`: both a low and a high bound (`3.5-5.0`).
|
|
1288
|
+
* - `open-low`: an upper bound only (`<5`), so everything at or below `high`.
|
|
1289
|
+
* - `open-high`: a lower bound only (`>10`), so everything at or above `low`.
|
|
1290
|
+
* - `unparsed`: the text did not match a recognized form; both bounds are
|
|
1291
1291
|
* absent and the raw text is surfaced. **A bound is never fabricated.**
|
|
1292
1292
|
*/
|
|
1293
1293
|
type ReferenceRangeKind = "closed" | "open-low" | "open-high" | "unparsed";
|
|
@@ -1296,7 +1296,7 @@ type ReferenceRangeKind = "closed" | "open-low" | "open-high" | "unparsed";
|
|
|
1296
1296
|
* (never coerced to a float, never rounded, never converted) so nothing is lost
|
|
1297
1297
|
* or fabricated.
|
|
1298
1298
|
*
|
|
1299
|
-
* **`[OSS-derived]
|
|
1299
|
+
* **`[OSS-derived]`: the exact lower/upper delimiter (`-`) and the open-ended
|
|
1300
1300
|
* `<x`/`>x` forms are taken from the permissively-licensed OSS reference parsers
|
|
1301
1301
|
* and cross-verified vendor transcripts; they are not confirmed against the
|
|
1302
1302
|
* purchased CLSI LIS02-A2. Anything that does not match these
|
|
@@ -1326,8 +1326,8 @@ interface ReferenceRange {
|
|
|
1326
1326
|
* open-ended) pair.
|
|
1327
1327
|
*
|
|
1328
1328
|
* Recognized forms are `low-high` (closed), `<high` (open-low), and `>low`
|
|
1329
|
-
* (open-high). Anything else
|
|
1330
|
-
* bare non-numeric token
|
|
1329
|
+
* (open-high). Anything else, including an ambiguous multi-dash string or a
|
|
1330
|
+
* bare non-numeric token, is returned as `kind: "unparsed"` with the raw text
|
|
1331
1331
|
* preserved and **no bound invented**. Bounds are surfaced as verbatim text, not
|
|
1332
1332
|
* coerced to numbers.
|
|
1333
1333
|
*
|
|
@@ -1349,7 +1349,7 @@ declare function parseReferenceRange(raw: string): ReferenceRange;
|
|
|
1349
1349
|
* A message is a header (which carries the resolved delimiters as provenance), an
|
|
1350
1350
|
* ordered list of typed records, and the Tier-2 warnings the lenient parser
|
|
1351
1351
|
* accumulated. Every record keeps its raw {@link AstmField}s **and** the typed
|
|
1352
|
-
* accessors relevant to its type
|
|
1352
|
+
* accessors relevant to its type: the raw tree is always present so nothing the
|
|
1353
1353
|
* parser did not model is lost.
|
|
1354
1354
|
*
|
|
1355
1355
|
* `H`/`P`/`O`/`R`/`C`/`Q`/`M`/`S`/`L` are modeled; every other type letter is an
|
|
@@ -1388,7 +1388,7 @@ interface RecordBase {
|
|
|
1388
1388
|
interface HeaderRecord extends RecordBase {
|
|
1389
1389
|
readonly type: "H";
|
|
1390
1390
|
/**
|
|
1391
|
-
* The four delimiters **this** header put into force
|
|
1391
|
+
* The four delimiters **this** header put into force: the set its own fields, and every record
|
|
1392
1392
|
* after it, were read with.
|
|
1393
1393
|
*
|
|
1394
1394
|
* A stream may carry several messages (`H` … `L`) and each header declares its own set, so on a
|
|
@@ -1403,7 +1403,7 @@ interface HeaderRecord extends RecordBase {
|
|
|
1403
1403
|
* delimiters are read from this raw text, never from {@link RecordBase.fields}.
|
|
1404
1404
|
*
|
|
1405
1405
|
* It is **not** what the serializer emits from. To change a header field, edit
|
|
1406
|
-
* {@link RecordBase.fields}
|
|
1406
|
+
* {@link RecordBase.fields}: `fields[0]` is the type letter, `fields[1]` is the
|
|
1407
1407
|
* delimiter declaration, and the header's ASTM data fields follow from `fields[2]`.
|
|
1408
1408
|
* Editing this string instead has no effect on emit.
|
|
1409
1409
|
*/
|
|
@@ -1422,63 +1422,63 @@ interface PatientName {
|
|
|
1422
1422
|
* The `P` (patient) record.
|
|
1423
1423
|
*
|
|
1424
1424
|
* **Safety:** the practice-assigned ID (field 3) and the laboratory-assigned ID
|
|
1425
|
-
* (field 4) are modeled as **distinct** fields and never collapsed
|
|
1425
|
+
* (field 4) are modeled as **distinct** fields and never collapsed, conflating
|
|
1426
1426
|
* them is the primary result-misfiling path.
|
|
1427
1427
|
*/
|
|
1428
1428
|
interface PatientRecord extends RecordBase {
|
|
1429
1429
|
readonly type: "P";
|
|
1430
|
-
/** Field 2
|
|
1430
|
+
/** Field 2: sequence number. */
|
|
1431
1431
|
readonly seq?: string;
|
|
1432
|
-
/** Field 3
|
|
1432
|
+
/** Field 3: practice-assigned patient ID. Distinct from {@link PatientRecord.laboratoryAssignedId}. */
|
|
1433
1433
|
readonly practiceAssignedId?: string;
|
|
1434
|
-
/** Field 4
|
|
1434
|
+
/** Field 4: laboratory-assigned patient ID. Distinct from {@link PatientRecord.practiceAssignedId}. */
|
|
1435
1435
|
readonly laboratoryAssignedId?: string;
|
|
1436
1436
|
/**
|
|
1437
|
-
* Field 5
|
|
1438
|
-
* separate from the practice- and laboratory-assigned IDs
|
|
1437
|
+
* Field 5: a third patient identifier (e.g. a national/alternate ID), surfaced verbatim. Kept
|
|
1438
|
+
* separate from the practice- and laboratory-assigned IDs: the three never collapse into one.
|
|
1439
1439
|
*/
|
|
1440
1440
|
readonly patientIdThree?: string;
|
|
1441
|
-
/** Field 6
|
|
1441
|
+
/** Field 6: patient name (`Last^First^Middle`). */
|
|
1442
1442
|
readonly name?: PatientName;
|
|
1443
|
-
/** Field 7
|
|
1443
|
+
/** Field 7: mother's maiden name, surfaced verbatim (a surname component; PHI). */
|
|
1444
1444
|
readonly mothersMaidenName?: string;
|
|
1445
|
-
/** Field 8
|
|
1445
|
+
/** Field 8: birthdate (`YYYYMMDDHHMMSS`, precision-preserving; a truncated run sets `truncated`). */
|
|
1446
1446
|
readonly birthDate?: AstmDate;
|
|
1447
|
-
/** Field 9
|
|
1447
|
+
/** Field 9: sex, surfaced raw (`M`/`F`/`U`/vendor value). */
|
|
1448
1448
|
readonly sex?: string;
|
|
1449
1449
|
}
|
|
1450
1450
|
/**
|
|
1451
|
-
* The `O` (order) record
|
|
1451
|
+
* The `O` (order) record: binds a result to a specimen.
|
|
1452
1452
|
*/
|
|
1453
1453
|
interface OrderRecord extends RecordBase {
|
|
1454
1454
|
readonly type: "O";
|
|
1455
|
-
/** Field 2
|
|
1455
|
+
/** Field 2: sequence number. */
|
|
1456
1456
|
readonly seq?: string;
|
|
1457
|
-
/** Field 3
|
|
1457
|
+
/** Field 3: specimen / accession ID. */
|
|
1458
1458
|
readonly specimenId?: string;
|
|
1459
|
-
/** Field 4
|
|
1459
|
+
/** Field 4: instrument specimen ID. */
|
|
1460
1460
|
readonly instrumentSpecimenId?: string;
|
|
1461
|
-
/** Field 5
|
|
1461
|
+
/** Field 5: Universal Test ID (same caret structure as a result's). */
|
|
1462
1462
|
readonly universalTestId?: UniversalTestId;
|
|
1463
1463
|
/**
|
|
1464
|
-
* Field 6
|
|
1464
|
+
* Field 6: priority, surfaced raw (e.g. `S` STAT, `R` routine, `A` ASAP). Vendor letters vary;
|
|
1465
1465
|
* the code set is `[OSS-derived]` (the exact enumeration is in the paywalled CLSI LIS02-A2), so the
|
|
1466
1466
|
* value is surfaced verbatim and **never mapped to a guessed meaning**.
|
|
1467
1467
|
*/
|
|
1468
1468
|
readonly priority?: string;
|
|
1469
1469
|
/**
|
|
1470
|
-
* Field 12
|
|
1471
|
-
* (`~12`) and the code set are `[OSS-derived]` (paywalled)
|
|
1470
|
+
* Field 12: action code, surfaced raw (e.g. `C` cancel, `A` add, `N` new). The exact field index
|
|
1471
|
+
* (`~12`) and the code set are `[OSS-derived]` (paywalled): surfaced verbatim, never interpreted.
|
|
1472
1472
|
*/
|
|
1473
1473
|
readonly actionCode?: string;
|
|
1474
1474
|
/**
|
|
1475
|
-
* Field 26
|
|
1476
|
-
* field index (`~26`) and the code set are `[OSS-derived]` (paywalled)
|
|
1475
|
+
* Field 26: report type, surfaced raw (e.g. `F` final, `P` preliminary, `X` cancel). The exact
|
|
1476
|
+
* field index (`~26`) and the code set are `[OSS-derived]` (paywalled): surfaced verbatim.
|
|
1477
1477
|
*/
|
|
1478
1478
|
readonly reportType?: string;
|
|
1479
1479
|
}
|
|
1480
1480
|
/**
|
|
1481
|
-
* The `R` (result) record
|
|
1481
|
+
* The `R` (result) record: the value itself.
|
|
1482
1482
|
*
|
|
1483
1483
|
* The raw safety-critical fields (value, `units`, `referenceRange`,
|
|
1484
1484
|
* `abnormalFlags`, `resultStatus`) are always surfaced exactly as received.
|
|
@@ -1486,94 +1486,94 @@ interface OrderRecord extends RecordBase {
|
|
|
1486
1486
|
* (Table 0078, `undefined` never coerced to normal), `status` (a `C`/`X` never
|
|
1487
1487
|
* reads as active-final; an absent status is `unspecified`, never `final`), and
|
|
1488
1488
|
* `range` (open/closed bounds surfaced verbatim, never fabricated). The raw
|
|
1489
|
-
* strings and the modeled views coexist
|
|
1489
|
+
* strings and the modeled views coexist: nothing is collapsed or reconciled.
|
|
1490
1490
|
*/
|
|
1491
1491
|
interface ResultRecord extends RecordBase {
|
|
1492
1492
|
readonly type: "R";
|
|
1493
|
-
/** Field 2
|
|
1493
|
+
/** Field 2: sequence number. */
|
|
1494
1494
|
readonly seq?: string;
|
|
1495
|
-
/** Field 3
|
|
1495
|
+
/** Field 3: Universal Test ID (local code in component 4 is the primary identifier). */
|
|
1496
1496
|
readonly universalTestId?: UniversalTestId;
|
|
1497
1497
|
/**
|
|
1498
|
-
* Field 4
|
|
1498
|
+
* Field 4: the measured value. In the ordinary single-component case this is the decoded scalar.
|
|
1499
1499
|
* When the value field carried an *unescaped* component delimiter (an ambiguity), this is the
|
|
1500
|
-
* **full raw field text
|
|
1500
|
+
* **full raw field text**, never a truncated component, and {@link ResultRecord.valueComponents}
|
|
1501
1501
|
* plus an `ASTM_RECORD_AMBIGUOUS_VALUE_SPLIT` warning are also present.
|
|
1502
1502
|
*/
|
|
1503
1503
|
readonly value?: string;
|
|
1504
1504
|
/**
|
|
1505
1505
|
* Field 4 split into components, present only when the value field carried an *unescaped* component
|
|
1506
|
-
* delimiter
|
|
1506
|
+
* delimiter: i.e. it read as more than one component. Surfaced alongside the full raw value (and a
|
|
1507
1507
|
* warning) so an ambiguous split is visible, never resolved silently.
|
|
1508
1508
|
*/
|
|
1509
1509
|
readonly valueComponents?: readonly string[];
|
|
1510
|
-
/** Field 5
|
|
1510
|
+
/** Field 5: units (vendor free text; a missing unit is not defaulted). */
|
|
1511
1511
|
readonly units?: string;
|
|
1512
|
-
/** Field 6
|
|
1512
|
+
/** Field 6: reference range, surfaced raw. */
|
|
1513
1513
|
readonly referenceRange?: string;
|
|
1514
1514
|
/**
|
|
1515
|
-
* Field 6
|
|
1515
|
+
* Field 6: the reference range parsed into low/high (or open-ended) bounds, present only when the
|
|
1516
1516
|
* field carried a value. An unparseable range is `kind: "unparsed"` with the raw text preserved and
|
|
1517
1517
|
* **no bound fabricated**. Bounds are verbatim numeric text, never coerced to floats.
|
|
1518
1518
|
*/
|
|
1519
1519
|
readonly range?: ReferenceRange;
|
|
1520
|
-
/** Field 7
|
|
1520
|
+
/** Field 7: abnormal flags, surfaced raw (HL7 Table 0078 values). */
|
|
1521
1521
|
readonly abnormalFlags?: string;
|
|
1522
1522
|
/**
|
|
1523
|
-
* Field 7
|
|
1524
|
-
* carried a value. An unrecognized flag is `{ recognized: false, meaning: "undefined" }
|
|
1523
|
+
* Field 7: the abnormal flag interpreted against HL7 Table 0078, present only when the field
|
|
1524
|
+
* carried a value. An unrecognized flag is `{ recognized: false, meaning: "undefined" }`, surfaced,
|
|
1525
1525
|
* never dropped, and **never coerced to `normal`**.
|
|
1526
1526
|
*/
|
|
1527
1527
|
readonly flag?: AbnormalFlag;
|
|
1528
|
-
/** Field 9
|
|
1528
|
+
/** Field 9: result status, surfaced raw (`F`/`C`/`X`/…). */
|
|
1529
1529
|
readonly resultStatus?: string;
|
|
1530
1530
|
/**
|
|
1531
|
-
* Field 9
|
|
1531
|
+
* Field 9: the modeled result status. **Always present** (an absent field yields a typed
|
|
1532
1532
|
* `unspecified`, never assumed `final`), so `status.isActiveFinal` is always a reliable boolean: it
|
|
1533
1533
|
* is `true` only for a plain `F`, and `false` for a correction (`C`), a cancellation (`X`), a
|
|
1534
1534
|
* partial/preliminary/pending, an absent, or an unrecognized status.
|
|
1535
1535
|
*/
|
|
1536
1536
|
readonly status: ResultStatus;
|
|
1537
|
-
/** Field 11
|
|
1537
|
+
/** Field 11: operator. */
|
|
1538
1538
|
readonly operator?: string;
|
|
1539
|
-
/** Field 12
|
|
1539
|
+
/** Field 12: test started timestamp. */
|
|
1540
1540
|
readonly startedAt?: AstmDate;
|
|
1541
|
-
/** Field 13
|
|
1541
|
+
/** Field 13: test completed timestamp. */
|
|
1542
1542
|
readonly completedAt?: AstmDate;
|
|
1543
|
-
/** Field 14
|
|
1543
|
+
/** Field 14: instrument identifier. */
|
|
1544
1544
|
readonly instrument?: string;
|
|
1545
1545
|
}
|
|
1546
1546
|
/**
|
|
1547
|
-
* The `C` (comment) record
|
|
1547
|
+
* The `C` (comment) record: free-text context attached to a parent record.
|
|
1548
1548
|
*
|
|
1549
1549
|
* A comment is **attached by position** to the immediately-preceding `H`/`P`/`O`/`R` record
|
|
1550
1550
|
* ({@link CommentRecord.parentIndex}); consecutive comments share that parent. **Fail-safe:** a
|
|
1551
|
-
* comment with no valid preceding parent is an **orphan
|
|
1551
|
+
* comment with no valid preceding parent is an **orphan**: it is attached to the message root
|
|
1552
1552
|
* ({@link CommentRecord.attachedToRoot} `true`) and a value-free `ASTM_RECORD_ORPHAN_COMMENT` warning
|
|
1553
|
-
* fires
|
|
1553
|
+
* fires, **never silently dropped**, so a comment carrying (e.g.) "QC / non-compliant" context can
|
|
1554
1554
|
* never float away unnoticed.
|
|
1555
1555
|
*/
|
|
1556
1556
|
interface CommentRecord extends RecordBase {
|
|
1557
1557
|
readonly type: "C";
|
|
1558
|
-
/** Field 2
|
|
1558
|
+
/** Field 2: sequence number. */
|
|
1559
1559
|
readonly seq?: string;
|
|
1560
|
-
/** Field 3
|
|
1560
|
+
/** Field 3: comment source (who/what produced it), surfaced verbatim. */
|
|
1561
1561
|
readonly source?: string;
|
|
1562
1562
|
/**
|
|
1563
|
-
* Field 4
|
|
1563
|
+
* Field 4: the comment text, surfaced as the **full** field text (all components), never
|
|
1564
1564
|
* truncated to the first component. The component structure is in {@link CommentRecord.textComponents}.
|
|
1565
1565
|
*/
|
|
1566
1566
|
readonly text?: string;
|
|
1567
1567
|
/**
|
|
1568
|
-
* Field 4
|
|
1568
|
+
* Field 4: the comment text split into its decoded components (comment text is component-capable;
|
|
1569
1569
|
* multiple components are a normal structured comment, **not** an ambiguity). Present only when the
|
|
1570
1570
|
* field carried more than one component.
|
|
1571
1571
|
*/
|
|
1572
1572
|
readonly textComponents?: readonly string[];
|
|
1573
1573
|
/**
|
|
1574
|
-
* Field 5
|
|
1574
|
+
* Field 5: comment type code, surfaced verbatim. **`[OSS-derived]`:** `I` (instrument) is the only
|
|
1575
1575
|
* value seen in the permissively-licensed real transcripts; other values (e.g. `G`/`T`/`P`) are
|
|
1576
|
-
* defined only in the paywalled CLSI LIS02-A2 and are **not** interpreted here
|
|
1576
|
+
* defined only in the paywalled CLSI LIS02-A2 and are **not** interpreted here: the raw code is
|
|
1577
1577
|
* surfaced, never mapped to a guessed meaning.
|
|
1578
1578
|
*/
|
|
1579
1579
|
readonly commentType?: string;
|
|
@@ -1582,57 +1582,57 @@ interface CommentRecord extends RecordBase {
|
|
|
1582
1582
|
* the comment is an orphan attached to the message root (see {@link CommentRecord.attachedToRoot}).
|
|
1583
1583
|
*/
|
|
1584
1584
|
readonly parentIndex?: number;
|
|
1585
|
-
/** `true` when no valid parent preceded
|
|
1585
|
+
/** `true` when no valid parent preceded: the comment is attached to the message root (and warned). */
|
|
1586
1586
|
readonly attachedToRoot: boolean;
|
|
1587
1587
|
}
|
|
1588
1588
|
/**
|
|
1589
|
-
* The `Q` (Request Information) record
|
|
1589
|
+
* The `Q` (Request Information) record: the host-query request.
|
|
1590
1590
|
*
|
|
1591
1591
|
* A `Q` record asks the LIS for information (e.g. outstanding orders for a
|
|
1592
1592
|
* specimen); its **presence classifies the whole message as a request, never a
|
|
1593
1593
|
* result set** (see {@link AstmMessage.classification}). Its safety-relevant
|
|
1594
|
-
* fields
|
|
1594
|
+
* fields, the starting/ending range ID and the request-information status, are
|
|
1595
1595
|
* surfaced **verbatim**; the field *positions* are the public ASTM E1394 layout,
|
|
1596
1596
|
* but their internal structure and code meanings are **`[OSS-derived / paywalled]`**
|
|
1597
1597
|
* and are therefore **never interpreted or guessed**.
|
|
1598
1598
|
*/
|
|
1599
1599
|
interface QueryRecord extends RecordBase {
|
|
1600
|
-
/** Field 2
|
|
1600
|
+
/** Field 2: sequence number. */
|
|
1601
1601
|
readonly seq?: string;
|
|
1602
1602
|
readonly type: "Q";
|
|
1603
1603
|
/**
|
|
1604
|
-
* Field 3
|
|
1604
|
+
* Field 3: starting range ID number, surfaced as the **full** verbatim field text (never truncated
|
|
1605
1605
|
* to a component). Its caret component structure (e.g. patient ID ^ specimen ID) is
|
|
1606
|
-
* **`[OSS-derived / paywalled]
|
|
1606
|
+
* **`[OSS-derived / paywalled]`**: the raw split is available via {@link RecordBase.fields}, but the
|
|
1607
1607
|
* *meaning* of each component is **never** assigned here.
|
|
1608
1608
|
*/
|
|
1609
1609
|
readonly startingRangeId?: string;
|
|
1610
|
-
/** Field 4
|
|
1610
|
+
/** Field 4: ending range ID number, surfaced verbatim (same `[OSS-derived]` caveat as field 3). */
|
|
1611
1611
|
readonly endingRangeId?: string;
|
|
1612
1612
|
/**
|
|
1613
|
-
* Field 5
|
|
1613
|
+
* Field 5: Universal Test ID (the same caret structure as an `O`/`R` record's), recognized by
|
|
1614
1614
|
* provenance only. When the field is the literal universal-query keyword, {@link QueryRecord.queriesAllTests}
|
|
1615
|
-
* is set instead
|
|
1615
|
+
* is set instead: see its caveat.
|
|
1616
1616
|
*/
|
|
1617
1617
|
readonly universalTestId?: UniversalTestId;
|
|
1618
1618
|
/**
|
|
1619
1619
|
* `true` when field 5 is the literal `ALL` universal-query keyword (case-insensitive). **`[OSS-derived
|
|
1620
1620
|
* / paywalled]`:** the token is surfaced because it appears in the OSS references, but its exact
|
|
1621
1621
|
* host-query *behavior* (which tests a bare `ALL` selects, and whether the vendor answers with a full
|
|
1622
|
-
* `H/P/O/L` or a `P/O`-only response) is paywalled and vendor-specific
|
|
1622
|
+
* `H/P/O/L` or a `P/O`-only response) is paywalled and vendor-specific, **not decided here**.
|
|
1623
1623
|
*/
|
|
1624
1624
|
readonly queriesAllTests: boolean;
|
|
1625
1625
|
/**
|
|
1626
|
-
* Field 13
|
|
1626
|
+
* Field 13: request-information status code(s), surfaced **verbatim**. The status code *set* is
|
|
1627
1627
|
* **`[OSS-derived / paywalled]`**: with no publicly-groundable enumeration, this
|
|
1628
|
-
* parser recognizes **none** of them and interprets nothing
|
|
1628
|
+
* parser recognizes **none** of them and interprets nothing, every present status is surfaced raw
|
|
1629
1629
|
* and flagged with a value-free `ASTM_RECORD_UNINTERPRETED_QUERY_STATUS` warning. Never mapped to a
|
|
1630
1630
|
* guessed meaning.
|
|
1631
1631
|
*/
|
|
1632
1632
|
readonly requestInformationStatus?: string;
|
|
1633
1633
|
}
|
|
1634
1634
|
/**
|
|
1635
|
-
* The `M` (manufacturer) record
|
|
1635
|
+
* The `M` (manufacturer) record: vendor-defined free-form data
|
|
1636
1636
|
* (QC / calibration / maintenance), surfaced **VERBATIM** and **never interpreted
|
|
1637
1637
|
* into typed clinical fields**.
|
|
1638
1638
|
*
|
|
@@ -1647,7 +1647,7 @@ interface ManufacturerRecord extends RecordBase {
|
|
|
1647
1647
|
* The record's exact wire text (terminator excluded), preserved byte-for-byte.
|
|
1648
1648
|
*
|
|
1649
1649
|
* Emit reproduces these bytes exactly whenever a reader using the delimiters being
|
|
1650
|
-
* emitted against would recover the fields this record models
|
|
1650
|
+
* emitted against would recover the fields this record models: always the case when
|
|
1651
1651
|
* the record is already in those delimiters, and also when it carries no delimiter
|
|
1652
1652
|
* either set would split on. Otherwise the record is re-encoded from
|
|
1653
1653
|
* {@link RecordBase.fields}, so the row can never go out in delimiters the header does
|
|
@@ -1656,7 +1656,7 @@ interface ManufacturerRecord extends RecordBase {
|
|
|
1656
1656
|
readonly rawLine: string;
|
|
1657
1657
|
}
|
|
1658
1658
|
/**
|
|
1659
|
-
* The `S` (scientific) record
|
|
1659
|
+
* The `S` (scientific) record: vendor-defined free-form data, surfaced
|
|
1660
1660
|
* **VERBATIM** and **never interpreted into typed clinical fields** (same posture
|
|
1661
1661
|
* as {@link ManufacturerRecord}).
|
|
1662
1662
|
*/
|
|
@@ -1666,7 +1666,7 @@ interface ScientificRecord extends RecordBase {
|
|
|
1666
1666
|
* The record's exact wire text (terminator excluded), preserved byte-for-byte.
|
|
1667
1667
|
*
|
|
1668
1668
|
* Emit reproduces these bytes exactly whenever a reader using the delimiters being
|
|
1669
|
-
* emitted against would recover the fields this record models
|
|
1669
|
+
* emitted against would recover the fields this record models: always the case when
|
|
1670
1670
|
* the record is already in those delimiters, and also when it carries no delimiter
|
|
1671
1671
|
* either set would split on. Otherwise the record is re-encoded from
|
|
1672
1672
|
* {@link RecordBase.fields}, so the row can never go out in delimiters the header does
|
|
@@ -1675,7 +1675,7 @@ interface ScientificRecord extends RecordBase {
|
|
|
1675
1675
|
readonly rawLine: string;
|
|
1676
1676
|
}
|
|
1677
1677
|
/**
|
|
1678
|
-
* The `L` (terminator) record
|
|
1678
|
+
* The `L` (terminator) record: closes a message.
|
|
1679
1679
|
*/
|
|
1680
1680
|
interface TerminatorRecord extends RecordBase {
|
|
1681
1681
|
readonly type: "L";
|
|
@@ -1684,7 +1684,7 @@ interface TerminatorRecord extends RecordBase {
|
|
|
1684
1684
|
* Any record whose type letter is not modeled (a genuinely unknown letter).
|
|
1685
1685
|
* `H`/`P`/`O`/`R`/`C`/`Q`/`M`/`S`/`L` are all modeled; anything else is surfaced
|
|
1686
1686
|
* with its raw fields intact and flagged with an
|
|
1687
|
-
* `ASTM_RECORD_UNKNOWN_TYPE` warning
|
|
1687
|
+
* `ASTM_RECORD_UNKNOWN_TYPE` warning, never dropped.
|
|
1688
1688
|
*/
|
|
1689
1689
|
interface UnsupportedRecord extends RecordBase {
|
|
1690
1690
|
readonly type: "unsupported";
|
|
@@ -1696,30 +1696,30 @@ type AstmRecord = HeaderRecord | PatientRecord | OrderRecord | ResultRecord | Co
|
|
|
1696
1696
|
/**
|
|
1697
1697
|
* How a message is classified by the host-query flow.
|
|
1698
1698
|
*
|
|
1699
|
-
* - `host-query`
|
|
1699
|
+
* - `host-query` (the message carries at least one `Q` record): it is a **request
|
|
1700
1700
|
* for information**, and **must never be read as a result set** (the load-bearing
|
|
1701
1701
|
* safety distinction of this layer). `Q` **dominates**: a `Q` present classifies
|
|
1702
1702
|
* the message as a request even if a result record is also present (an anomaly,
|
|
1703
|
-
* separately warned)
|
|
1703
|
+
* separately warned), so a `Q`-bearing message is never silently treated as a
|
|
1704
1704
|
* result upload.
|
|
1705
|
-
* - `results`
|
|
1706
|
-
* - `orders`
|
|
1705
|
+
* - `results` (no `Q`, at least one `R` result record): a result upload / response.
|
|
1706
|
+
* - `orders` (no `Q`, no `R`, at least one `O` order record): an order download,
|
|
1707
1707
|
* or a query response before results are attached.
|
|
1708
|
-
* - `indeterminate
|
|
1708
|
+
* - `indeterminate`: none of the above (e.g. header + terminator only). Not
|
|
1709
1709
|
* guessed into one of the other kinds.
|
|
1710
1710
|
*/
|
|
1711
1711
|
type AstmMessageKind = "host-query" | "results" | "orders" | "indeterminate";
|
|
1712
1712
|
/**
|
|
1713
1713
|
* The message-level classification from the host-query flow. `isHostQueryRequest`
|
|
1714
1714
|
* is the single boolean a consumer should gate on before treating records as
|
|
1715
|
-
* results
|
|
1715
|
+
* results: it is `true` **iff** a `Q` record is present.
|
|
1716
1716
|
*
|
|
1717
1717
|
* @example
|
|
1718
1718
|
* ```ts
|
|
1719
1719
|
* import { parseAstmRecords } from "@cosyte/astm";
|
|
1720
1720
|
* const req = parseAstmRecords("H|\\^&\rP|1\rQ|1|^SPEC-7||ALL\rL|1\r");
|
|
1721
1721
|
* req.classification.kind; // "host-query"
|
|
1722
|
-
* req.classification.isHostQueryRequest; // true
|
|
1722
|
+
* req.classification.isHostQueryRequest; // true, never read its records as results
|
|
1723
1723
|
* ```
|
|
1724
1724
|
*/
|
|
1725
1725
|
interface AstmMessageClassification {
|
|
@@ -1732,7 +1732,7 @@ interface AstmMessageClassification {
|
|
|
1732
1732
|
/** At least one `O` (order) record is present. */
|
|
1733
1733
|
readonly hasOrders: boolean;
|
|
1734
1734
|
/**
|
|
1735
|
-
* `true` **iff** `kind === "host-query"` (a `Q` record is present)
|
|
1735
|
+
* `true` **iff** `kind === "host-query"` (a `Q` record is present), the safety surface: gate on this
|
|
1736
1736
|
* before treating records as results, so a query is never misread as a result upload.
|
|
1737
1737
|
*/
|
|
1738
1738
|
readonly isHostQueryRequest: boolean;
|
|
@@ -1758,12 +1758,12 @@ interface AstmMessage {
|
|
|
1758
1758
|
*
|
|
1759
1759
|
* A stream may carry several messages (`H` … `L`), and a later header may declare a different set,
|
|
1760
1760
|
* which is honored from that header onward. So this is not necessarily the set every record was
|
|
1761
|
-
* read with
|
|
1761
|
+
* read with: each header reports its own on {@link HeaderRecord.delimiters}, and a change is
|
|
1762
1762
|
* flagged `ASTM_RECORD_DELIMITERS_REDECLARED`.
|
|
1763
1763
|
*/
|
|
1764
1764
|
readonly delimiters: Delimiters;
|
|
1765
1765
|
/**
|
|
1766
|
-
* The host-query classification of this message
|
|
1766
|
+
* The host-query classification of this message: whether it is a request (`Q`
|
|
1767
1767
|
* present), a result upload, an order download, or indeterminate. Gate on
|
|
1768
1768
|
* {@link AstmMessageClassification.isHostQueryRequest} before reading records as
|
|
1769
1769
|
* results.
|
|
@@ -1771,7 +1771,7 @@ interface AstmMessage {
|
|
|
1771
1771
|
readonly classification: AstmMessageClassification;
|
|
1772
1772
|
readonly warnings: readonly AstmRecordWarning[];
|
|
1773
1773
|
/**
|
|
1774
|
-
* The vendor profile that was active for this parse, when one applied
|
|
1774
|
+
* The vendor profile that was active for this parse, when one applied: its
|
|
1775
1775
|
* `name` and resolved `lineage` (attribution only; the profile object itself is
|
|
1776
1776
|
* not embedded). Absent when no profile applied. A profile that tolerated a
|
|
1777
1777
|
* deviation shows up as a `PROFILE_QUIRK_APPLIED` entry in {@link warnings}, not
|
|
@@ -1789,7 +1789,7 @@ interface AstmParseOptions {
|
|
|
1789
1789
|
/**
|
|
1790
1790
|
* When `true`, escalate any Tier-2 deviation to a thrown {@link AstmStrictError}
|
|
1791
1791
|
* instead of accumulating a warning. Off by default. A deviation a profile
|
|
1792
|
-
* *expected* (a `PROFILE_QUIRK_APPLIED`) does **not** escalate
|
|
1792
|
+
* *expected* (a `PROFILE_QUIRK_APPLIED`) does **not** escalate: it is known,
|
|
1793
1793
|
* benign, and still recorded.
|
|
1794
1794
|
*/
|
|
1795
1795
|
readonly strict?: boolean;
|
|
@@ -1809,7 +1809,7 @@ interface AstmParseOptions {
|
|
|
1809
1809
|
* It assumes **already-de-framed** record bytes; {@link parseFramedAstm} composes
|
|
1810
1810
|
* the E1381/LIS01 framing layer with this one. It reads the four delimiters from each
|
|
1811
1811
|
* `H` record, tokenizes every record escape-aware, and builds the immutable
|
|
1812
|
-
* {@link AstmMessage}
|
|
1812
|
+
* {@link AstmMessage}, lenient by default: vendor quirks become typed warnings, and
|
|
1813
1813
|
* only three unrecoverable structural conditions are fatal.
|
|
1814
1814
|
*/
|
|
1815
1815
|
|
|
@@ -1838,7 +1838,7 @@ declare class AstmStrictError extends Error {
|
|
|
1838
1838
|
*
|
|
1839
1839
|
* The stream is a sequence of records separated by `CR` (with `LF`/`CRLF`
|
|
1840
1840
|
* tolerated); the first record must be an `H` header, which declares the
|
|
1841
|
-
* delimiters. Lenient by default
|
|
1841
|
+
* delimiters. Lenient by default: set `strict` to reject any tolerated deviation.
|
|
1842
1842
|
*
|
|
1843
1843
|
* **Several messages in one stream.** A message runs `H` … `L`, so a stream may carry
|
|
1844
1844
|
* more than one, and each `H` declares the delimiters for the records that follow it.
|
|
@@ -1847,7 +1847,7 @@ declare class AstmStrictError extends Error {
|
|
|
1847
1847
|
* they were read with, so a redeclaration never reinterprets earlier bytes. A header
|
|
1848
1848
|
* that merely restates the set in force is a no-op and warns nothing. A later header
|
|
1849
1849
|
* that cannot declare a usable set keeps the delimiters already in force and warns
|
|
1850
|
-
* `ASTM_RECORD_UNREADABLE_REDECLARATION
|
|
1850
|
+
* `ASTM_RECORD_UNREADABLE_REDECLARATION`: a set is never guessed, and no record is
|
|
1851
1851
|
* dropped. Per-header sets are on each {@link HeaderRecord.delimiters};
|
|
1852
1852
|
* {@link AstmMessage.delimiters} is the first header's.
|
|
1853
1853
|
*
|
|
@@ -1867,9 +1867,9 @@ declare class AstmStrictError extends Error {
|
|
|
1867
1867
|
*/
|
|
1868
1868
|
declare function parseAstmRecords(raw: string | Uint8Array, options?: AstmParseOptions): AstmMessage;
|
|
1869
1869
|
/**
|
|
1870
|
-
* Attach every `C` (comment) record to its parent by position
|
|
1870
|
+
* Attach every `C` (comment) record to its parent by position: the immediately-preceding
|
|
1871
1871
|
* `H`/`P`/`O`/`R` record; consecutive comments share that parent. **Fail-safe:** a comment with no
|
|
1872
|
-
* valid preceding parent is an **orphan
|
|
1872
|
+
* valid preceding parent is an **orphan**, it is attached to the message root (`attachedToRoot:
|
|
1873
1873
|
* true`) with an `ASTM_RECORD_ORPHAN_COMMENT` warning, **never dropped**. Returns a new array; the
|
|
1874
1874
|
* non-comment records pass through unchanged.
|
|
1875
1875
|
*
|
|
@@ -1901,19 +1901,19 @@ declare function attachComments(records: readonly AstmRecord[], warnings: AstmRe
|
|
|
1901
1901
|
* each record with a `CR`.
|
|
1902
1902
|
*
|
|
1903
1903
|
* **Round-trip.** `parseAstmRecords(serializeAstmRecords(msg))` reproduces the modeled
|
|
1904
|
-
* records
|
|
1904
|
+
* records: the same components, the same typed accessors, and the canonical delimiter
|
|
1905
1905
|
* set (a non-canonical source is normalized to `H|\^&` by default). Normalizing sets the
|
|
1906
1906
|
* four delimiter **roles**; it does not delete a declaration's surplus characters, so a
|
|
1907
1907
|
* canonically-declared header that arrived carrying surplus keeps it on the default path
|
|
1908
1908
|
* too.
|
|
1909
1909
|
*
|
|
1910
|
-
* **One delimiter set for the whole stream.** Every record
|
|
1910
|
+
* **One delimiter set for the whole stream.** Every record (including `H`, `M` and `S`)
|
|
1911
1911
|
* is emitted against the delimiters being emitted with, and the header declares that same
|
|
1912
1912
|
* set. `M` and `S` carry vendor free-form data and are reproduced **byte-for-byte** from
|
|
1913
1913
|
* their preserved `rawLine` whenever a reader using those delimiters would recover exactly
|
|
1914
1914
|
* the fields they model, which is the case for any stream already in the emit delimiter
|
|
1915
|
-
* set. When it is not
|
|
1916
|
-
* under another
|
|
1915
|
+
* set. When it is not, the record arrived under one delimiter set and is being emitted
|
|
1916
|
+
* under another, the record is re-encoded from its decoded tree instead, so its fields
|
|
1917
1917
|
* survive as fields rather than collapsing into one on the next read.
|
|
1918
1918
|
*
|
|
1919
1919
|
* **Emit follows the model.** Every record type, `H` included, is emitted from its decoded
|
|
@@ -1921,7 +1921,7 @@ declare function attachComments(records: readonly AstmRecord[], warnings: AstmRe
|
|
|
1921
1921
|
* part of a record that is never taken from the model is the header's delimiter
|
|
1922
1922
|
* declaration, and only in the three positions that carry a role: those always state the
|
|
1923
1923
|
* repeat, component and escape characters actually in use, whatever the model says. Any
|
|
1924
|
-
* further characters the declaration carried
|
|
1924
|
+
* further characters the declaration carried, a reader assigns them no role, come from
|
|
1925
1925
|
* the model like everything else, so a header that arrived as `H|\^&#` goes back out as
|
|
1926
1926
|
* `H|\^&#` rather than losing the `#`. That holds on the **default** canonical path as
|
|
1927
1927
|
* well as when a set is passed explicitly: the surplus of a canonically-declared header
|
|
@@ -1935,19 +1935,19 @@ declare function attachComments(records: readonly AstmRecord[], warnings: AstmRe
|
|
|
1935
1935
|
* never emitted raw.
|
|
1936
1936
|
*
|
|
1937
1937
|
* **Check the delimiter set before writing.** Three conditions are required for the
|
|
1938
|
-
* emitted bytes to read back as the records that produced them
|
|
1939
|
-
* one character, no separator a `CR`/`LF`, no two separators the same character
|
|
1938
|
+
* emitted bytes to read back as the records that produced them (each separator exactly
|
|
1939
|
+
* one character, no separator a `CR`/`LF`, no two separators the same character) and a
|
|
1940
1940
|
* set failing any of them is a typed {@link AstmSerializeError}
|
|
1941
1941
|
* (`ASTM_EMIT_INVALID_DELIMITERS`) rather than output this library's own parser would
|
|
1942
1942
|
* reject or silently re-read with a different field tree. These are **necessary**
|
|
1943
1943
|
* conditions, not a guarantee of readback: a set can pass all three and still produce a
|
|
1944
|
-
* stream that reads back wrong
|
|
1944
|
+
* stream that reads back wrong, a separator that collides with a record's type letter
|
|
1945
1945
|
* is the known case, and it corrupts identically with or without this check.
|
|
1946
1946
|
*/
|
|
1947
1947
|
|
|
1948
1948
|
/**
|
|
1949
1949
|
* Thrown by the record/frame emit side when a value cannot be serialized into a
|
|
1950
|
-
* spec-clean stream
|
|
1950
|
+
* spec-clean stream: specifically when a component contains a record terminator
|
|
1951
1951
|
* (`CR`/`LF`), which the ASTM escape codec cannot encode and which would break
|
|
1952
1952
|
* framing if emitted raw. Carries a stable code + positional context, never the
|
|
1953
1953
|
* offending value (PHI discipline).
|
|
@@ -1978,9 +1978,9 @@ declare class AstmSerializeError extends Error {
|
|
|
1978
1978
|
/**
|
|
1979
1979
|
* The reasons emit refuses rather than writing a stream that cannot be read back.
|
|
1980
1980
|
*
|
|
1981
|
-
* - `ASTM_EMIT_UNENCODABLE_VALUE
|
|
1981
|
+
* - `ASTM_EMIT_UNENCODABLE_VALUE`: a component holds a record terminator
|
|
1982
1982
|
* (`CR`/`LF`), which the escape codec has no mnemonic for.
|
|
1983
|
-
* - `ASTM_EMIT_INVALID_DELIMITERS
|
|
1983
|
+
* - `ASTM_EMIT_INVALID_DELIMITERS`: the delimiter set to emit against failed one
|
|
1984
1984
|
* of the three conditions readback requires (see {@link serializeAstmRecords}).
|
|
1985
1985
|
* Those conditions are necessary rather than sufficient, so this code means a
|
|
1986
1986
|
* set was rejected, not that every unreversible set is.
|
|
@@ -1993,7 +1993,7 @@ declare class AstmSerializeError extends Error {
|
|
|
1993
1993
|
*/
|
|
1994
1994
|
type AstmSerializeErrorCode = "ASTM_EMIT_UNENCODABLE_VALUE" | "ASTM_EMIT_INVALID_DELIMITERS";
|
|
1995
1995
|
/**
|
|
1996
|
-
* Escape-encode one component leaf for spec-clean emit
|
|
1996
|
+
* Escape-encode one component leaf for spec-clean emit: the inverse of
|
|
1997
1997
|
* `decodeEscapes`. The **escape character itself is encoded first** (`&` → `&E&`)
|
|
1998
1998
|
* so a later delimiter substitution can never double-encode the `&` it just
|
|
1999
1999
|
* introduced; then the field / component / repeat delimiters map to their
|
|
@@ -2009,7 +2009,7 @@ type AstmSerializeErrorCode = "ASTM_EMIT_UNENCODABLE_VALUE" | "ASTM_EMIT_INVALID
|
|
|
2009
2009
|
* @returns The escaped component text.
|
|
2010
2010
|
* @throws {@link AstmSerializeError} for a `CR`/`LF` in the leaf
|
|
2011
2011
|
* (`ASTM_EMIT_UNENCODABLE_VALUE`), or for a delimiter set failing one of the
|
|
2012
|
-
* three conditions readback requires (`ASTM_EMIT_INVALID_DELIMITERS`)
|
|
2012
|
+
* three conditions readback requires (`ASTM_EMIT_INVALID_DELIMITERS`), which
|
|
2013
2013
|
* are necessary, not sufficient, so a set can pass them and still not reverse.
|
|
2014
2014
|
* @example
|
|
2015
2015
|
* ```ts
|
|
@@ -2023,8 +2023,8 @@ declare function encodeComponent(leaf: string, d: Delimiters, recordIndex?: numb
|
|
|
2023
2023
|
* terminator). Emits with the given delimiters, defaulting to the canonical set.
|
|
2024
2024
|
*
|
|
2025
2025
|
* The header (`H`) is special-cased: its delimiter-definition field is emitted as
|
|
2026
|
-
* the **literal** declaration of `d`, never escaped
|
|
2027
|
-
* very declaration a reader depends on
|
|
2026
|
+
* the **literal** declaration of `d`, never escaped, escaping it would corrupt the
|
|
2027
|
+
* very declaration a reader depends on, followed by any characters the modeled
|
|
2028
2028
|
* declaration carried beyond the three a reader takes its roles from. Manufacturer
|
|
2029
2029
|
* (`M`) and scientific (`S`) records are reproduced **byte-identically** from their
|
|
2030
2030
|
* preserved `rawLine` when they are already in `d`, and re-encoded from their fields
|
|
@@ -2035,7 +2035,7 @@ declare function encodeComponent(leaf: string, d: Delimiters, recordIndex?: numb
|
|
|
2035
2035
|
* @returns The record's wire text, terminator excluded.
|
|
2036
2036
|
* @throws {@link AstmSerializeError} when a component contains an unencodable `CR`/`LF`
|
|
2037
2037
|
* (`ASTM_EMIT_UNENCODABLE_VALUE`), or when `d` fails one of the three conditions
|
|
2038
|
-
* readback requires (`ASTM_EMIT_INVALID_DELIMITERS`)
|
|
2038
|
+
* readback requires (`ASTM_EMIT_INVALID_DELIMITERS`): necessary conditions, not
|
|
2039
2039
|
* sufficient ones, so a set can pass them and still not reverse.
|
|
2040
2040
|
* @example
|
|
2041
2041
|
* ```ts
|
|
@@ -2047,11 +2047,11 @@ declare function encodeComponent(leaf: string, d: Delimiters, recordIndex?: numb
|
|
|
2047
2047
|
declare function serializeAstmRecord(record: AstmRecord, d?: Delimiters): string;
|
|
2048
2048
|
/**
|
|
2049
2049
|
* Serialize a whole ASTM message (or a bare record list) to a spec-clean,
|
|
2050
|
-
* `CR`-terminated record stream
|
|
2050
|
+
* `CR`-terminated record stream: the inverse of `parseAstmRecords`.
|
|
2051
2051
|
*
|
|
2052
2052
|
* Emit is **conservative**: the canonical `H|\^&` delimiters, every embedded
|
|
2053
2053
|
* delimiter re-escaped, each record closed with a `CR`. A message parsed with
|
|
2054
|
-
* non-canonical delimiters is **normalized** to the canonical set on emit
|
|
2054
|
+
* non-canonical delimiters is **normalized** to the canonical set on emit: every
|
|
2055
2055
|
* record, `M` and `S` included, so the emitted stream is in one delimiter set and
|
|
2056
2056
|
* re-parsing it recovers every field. Passing `d` explicitly emits against that set
|
|
2057
2057
|
* instead, and the header declares it. Normalization replaces the four delimiter
|
|
@@ -2060,11 +2060,11 @@ declare function serializeAstmRecord(record: AstmRecord, d?: Delimiters): string
|
|
|
2060
2060
|
*
|
|
2061
2061
|
* **`d` is checked before anything is written.** Each of the four separators must be
|
|
2062
2062
|
* exactly one character, none may be a record terminator, and no two may share a
|
|
2063
|
-
* character
|
|
2063
|
+
* character: otherwise the emitted bytes cannot be read back as the records that
|
|
2064
2064
|
* produced them, and emit has no warning channel with which to say so. A set that
|
|
2065
2065
|
* fails is an {@link AstmSerializeError} with code `ASTM_EMIT_INVALID_DELIMITERS`.
|
|
2066
2066
|
* This is stricter than the parser, which reads some sets it cannot reverse, so
|
|
2067
|
-
* `serializeAstmRecords(msg, msg.delimiters)` can refuse a message that parsed
|
|
2067
|
+
* `serializeAstmRecords(msg, msg.delimiters)` can refuse a message that parsed:
|
|
2068
2068
|
* in exactly the cases where it used to emit a stream that read back wrong. The
|
|
2069
2069
|
* three conditions are necessary rather than sufficient: a set that passes them can
|
|
2070
2070
|
* still read back wrong if a separator collides with a record's type letter, which
|
|
@@ -2075,7 +2075,7 @@ declare function serializeAstmRecord(record: AstmRecord, d?: Delimiters): string
|
|
|
2075
2075
|
* @returns The serialized record stream (`CR` after every record).
|
|
2076
2076
|
* @throws {@link AstmSerializeError} when a component contains an unencodable `CR`/`LF`
|
|
2077
2077
|
* (`ASTM_EMIT_UNENCODABLE_VALUE`), or when `d` fails one of the three conditions
|
|
2078
|
-
* readback requires (`ASTM_EMIT_INVALID_DELIMITERS`)
|
|
2078
|
+
* readback requires (`ASTM_EMIT_INVALID_DELIMITERS`): necessary conditions, not
|
|
2079
2079
|
* sufficient ones, so a set can pass them and still not reverse.
|
|
2080
2080
|
* @example
|
|
2081
2081
|
* ```ts
|
|
@@ -2095,7 +2095,7 @@ declare function serializeAstmRecords(input: AstmMessage | readonly AstmRecord[]
|
|
|
2095
2095
|
* @returns The escaped field text.
|
|
2096
2096
|
* @throws {@link AstmSerializeError} when a component contains an unencodable `CR`/`LF`
|
|
2097
2097
|
* (`ASTM_EMIT_UNENCODABLE_VALUE`), or when `d` fails one of the three conditions
|
|
2098
|
-
* readback requires (`ASTM_EMIT_INVALID_DELIMITERS`)
|
|
2098
|
+
* readback requires (`ASTM_EMIT_INVALID_DELIMITERS`): necessary conditions, not
|
|
2099
2099
|
* sufficient ones, so a set can pass them and still not reverse.
|
|
2100
2100
|
* @example
|
|
2101
2101
|
* ```ts
|
|
@@ -2114,13 +2114,13 @@ declare function serializeField(field: AstmField, d?: Delimiters): string;
|
|
|
2114
2114
|
*
|
|
2115
2115
|
* - **Never fabricate.** A builder emits **only** the values the caller supplied.
|
|
2116
2116
|
* An unsupplied field is left **empty**, never filled with a default clinical
|
|
2117
|
-
* value
|
|
2117
|
+
* value: no defaulted result status, abnormal flag, units, value, or patient
|
|
2118
2118
|
* identifier. (Leaving a field empty is not a guess; inventing an `F` status or
|
|
2119
2119
|
* an `N` flag would be.) A consumer that parses a built result whose status the
|
|
2120
2120
|
* caller omitted reads `status.meaning === "unspecified"`, never `"final"`.
|
|
2121
2121
|
* - **Compute, don't guess, the structure.** The record type letters, the
|
|
2122
2122
|
* delimiter declaration (canonical `H|\^&`), the message terminator (`L`), and
|
|
2123
|
-
* the per-record sequence counters are **structural
|
|
2123
|
+
* the per-record sequence counters are **structural**: computed by the builder,
|
|
2124
2124
|
* not clinical values. The caller may override any sequence number.
|
|
2125
2125
|
*
|
|
2126
2126
|
* Every value is escape-encoded on emit (via the serializer), so an embedded
|
|
@@ -2133,98 +2133,98 @@ interface PatientNameInput {
|
|
|
2133
2133
|
readonly first?: string;
|
|
2134
2134
|
readonly middle?: string;
|
|
2135
2135
|
}
|
|
2136
|
-
/** Input for a `P` (patient) record. The three IDs stay distinct
|
|
2136
|
+
/** Input for a `P` (patient) record. The three IDs stay distinct: none defaults from another. */
|
|
2137
2137
|
interface PatientInput {
|
|
2138
2138
|
readonly type: "P";
|
|
2139
2139
|
/** Structural sequence number; auto-computed when omitted. */
|
|
2140
2140
|
readonly seq?: string;
|
|
2141
|
-
/** Field 3
|
|
2141
|
+
/** Field 3: practice-assigned patient ID. */
|
|
2142
2142
|
readonly practiceAssignedId?: string;
|
|
2143
|
-
/** Field 4
|
|
2143
|
+
/** Field 4: laboratory-assigned patient ID. */
|
|
2144
2144
|
readonly laboratoryAssignedId?: string;
|
|
2145
|
-
/** Field 5
|
|
2145
|
+
/** Field 5: a third patient identifier. */
|
|
2146
2146
|
readonly patientIdThree?: string;
|
|
2147
|
-
/** Field 6
|
|
2147
|
+
/** Field 6: patient name (`Last^First^Middle`). */
|
|
2148
2148
|
readonly name?: PatientNameInput;
|
|
2149
|
-
/** Field 7
|
|
2149
|
+
/** Field 7: mother's maiden name. */
|
|
2150
2150
|
readonly mothersMaidenName?: string;
|
|
2151
|
-
/** Field 8
|
|
2151
|
+
/** Field 8: birthdate (`YYYYMMDDHHMMSS`), emitted verbatim, never reformatted. */
|
|
2152
2152
|
readonly birthDate?: string;
|
|
2153
|
-
/** Field 9
|
|
2153
|
+
/** Field 9: sex, emitted verbatim (never defaulted). */
|
|
2154
2154
|
readonly sex?: string;
|
|
2155
2155
|
}
|
|
2156
2156
|
/** Input for an `O` (order) record. */
|
|
2157
2157
|
interface OrderInput {
|
|
2158
2158
|
readonly type: "O";
|
|
2159
2159
|
readonly seq?: string;
|
|
2160
|
-
/** Field 3
|
|
2160
|
+
/** Field 3: specimen / accession ID. */
|
|
2161
2161
|
readonly specimenId?: string;
|
|
2162
|
-
/** Field 4
|
|
2162
|
+
/** Field 4: instrument specimen ID. */
|
|
2163
2163
|
readonly instrumentSpecimenId?: string;
|
|
2164
|
-
/** Field 5
|
|
2164
|
+
/** Field 5: Universal Test ID components (verbatim, in order; e.g. `["", "", "", "687"]`). */
|
|
2165
2165
|
readonly universalTestId?: readonly string[];
|
|
2166
|
-
/** Field 6
|
|
2166
|
+
/** Field 6: priority, emitted verbatim. */
|
|
2167
2167
|
readonly priority?: string;
|
|
2168
|
-
/** Field 12
|
|
2168
|
+
/** Field 12: action code, emitted verbatim. */
|
|
2169
2169
|
readonly actionCode?: string;
|
|
2170
|
-
/** Field 26
|
|
2170
|
+
/** Field 26: report type, emitted verbatim. */
|
|
2171
2171
|
readonly reportType?: string;
|
|
2172
2172
|
}
|
|
2173
2173
|
/** Input for an `R` (result) record. No clinical field is defaulted; unsupplied ⇒ empty. */
|
|
2174
2174
|
interface ResultInput {
|
|
2175
2175
|
readonly type: "R";
|
|
2176
2176
|
readonly seq?: string;
|
|
2177
|
-
/** Field 3
|
|
2177
|
+
/** Field 3: Universal Test ID components (verbatim). */
|
|
2178
2178
|
readonly universalTestId?: readonly string[];
|
|
2179
|
-
/** Field 4
|
|
2179
|
+
/** Field 4: the measured value, emitted verbatim (never defaulted). */
|
|
2180
2180
|
readonly value?: string;
|
|
2181
|
-
/** Field 5
|
|
2181
|
+
/** Field 5: units (vendor free text; never defaulted, guessed, or converted). */
|
|
2182
2182
|
readonly units?: string;
|
|
2183
|
-
/** Field 6
|
|
2183
|
+
/** Field 6: reference range, emitted verbatim. */
|
|
2184
2184
|
readonly referenceRange?: string;
|
|
2185
|
-
/** Field 7
|
|
2185
|
+
/** Field 7: abnormal flags, emitted verbatim (never defaulted to `N`). */
|
|
2186
2186
|
readonly abnormalFlags?: string;
|
|
2187
|
-
/** Field 9
|
|
2187
|
+
/** Field 9: result status, emitted verbatim (never defaulted to `F`). */
|
|
2188
2188
|
readonly resultStatus?: string;
|
|
2189
|
-
/** Field 11
|
|
2189
|
+
/** Field 11: operator. */
|
|
2190
2190
|
readonly operator?: string;
|
|
2191
|
-
/** Field 12
|
|
2191
|
+
/** Field 12: test-started timestamp (`YYYYMMDDHHMMSS`), verbatim. */
|
|
2192
2192
|
readonly startedAt?: string;
|
|
2193
|
-
/** Field 13
|
|
2193
|
+
/** Field 13: test-completed timestamp, verbatim. */
|
|
2194
2194
|
readonly completedAt?: string;
|
|
2195
|
-
/** Field 14
|
|
2195
|
+
/** Field 14: instrument identifier. */
|
|
2196
2196
|
readonly instrument?: string;
|
|
2197
2197
|
}
|
|
2198
2198
|
/** Input for a `C` (comment) record. */
|
|
2199
2199
|
interface CommentInput {
|
|
2200
2200
|
readonly type: "C";
|
|
2201
2201
|
readonly seq?: string;
|
|
2202
|
-
/** Field 3
|
|
2202
|
+
/** Field 3: comment source. */
|
|
2203
2203
|
readonly source?: string;
|
|
2204
|
-
/** Field 4
|
|
2204
|
+
/** Field 4: comment text (a single component). Use {@link CommentInput.textComponents} for a structured comment. */
|
|
2205
2205
|
readonly text?: string;
|
|
2206
|
-
/** Field 4
|
|
2206
|
+
/** Field 4: comment text as explicit components (takes precedence over `text` when set). */
|
|
2207
2207
|
readonly textComponents?: readonly string[];
|
|
2208
|
-
/** Field 5
|
|
2208
|
+
/** Field 5: comment type code, emitted verbatim. */
|
|
2209
2209
|
readonly commentType?: string;
|
|
2210
2210
|
}
|
|
2211
2211
|
/** Input for a `Q` (request-information) record. */
|
|
2212
2212
|
interface QueryInput {
|
|
2213
2213
|
readonly type: "Q";
|
|
2214
2214
|
readonly seq?: string;
|
|
2215
|
-
/** Field 3
|
|
2215
|
+
/** Field 3: starting range ID, emitted verbatim. */
|
|
2216
2216
|
readonly startingRangeId?: string;
|
|
2217
|
-
/** Field 4
|
|
2217
|
+
/** Field 4: ending range ID, emitted verbatim. */
|
|
2218
2218
|
readonly endingRangeId?: string;
|
|
2219
|
-
/** Field 5
|
|
2219
|
+
/** Field 5: Universal Test ID components; ignored when {@link QueryInput.queriesAllTests} is set. */
|
|
2220
2220
|
readonly universalTestId?: readonly string[];
|
|
2221
|
-
/** Field 5
|
|
2221
|
+
/** Field 5: emit the literal `ALL` universal-query keyword instead of a Universal Test ID. */
|
|
2222
2222
|
readonly queriesAllTests?: boolean;
|
|
2223
|
-
/** Field 13
|
|
2223
|
+
/** Field 13: request-information status, emitted verbatim. */
|
|
2224
2224
|
readonly requestInformationStatus?: string;
|
|
2225
2225
|
}
|
|
2226
2226
|
/**
|
|
2227
|
-
* Input for an `M` (manufacturer) or `S` (scientific) record
|
|
2227
|
+
* Input for an `M` (manufacturer) or `S` (scientific) record: vendor-defined
|
|
2228
2228
|
* free-form data. Emitted verbatim from the caller's fields; never interpreted.
|
|
2229
2229
|
*/
|
|
2230
2230
|
interface VerbatimInput {
|
|
@@ -2246,7 +2246,7 @@ interface MessageInput {
|
|
|
2246
2246
|
/** The body records, in order. `H` and `L` are supplied by the builder. */
|
|
2247
2247
|
readonly records: readonly AstmRecordInput[];
|
|
2248
2248
|
/**
|
|
2249
|
-
* Field 3 of the auto-appended `L` (terminator) record
|
|
2249
|
+
* Field 3 of the auto-appended `L` (terminator) record: the termination code
|
|
2250
2250
|
* (e.g. `N` normal). Omitted by default (not defaulted to a value the caller
|
|
2251
2251
|
* did not choose); the `L` seq is always emitted.
|
|
2252
2252
|
*/
|
|
@@ -2258,7 +2258,7 @@ interface MessageInput {
|
|
|
2258
2258
|
* The builder emits the canonical `H|\^&` header (plus any header fields), then
|
|
2259
2259
|
* every body record with each supplied field at its correct 1-based position and
|
|
2260
2260
|
* every value escape-encoded, then an `L` terminator. **Nothing clinical is
|
|
2261
|
-
* defaulted**
|
|
2261
|
+
* defaulted** (an unsupplied field is empty, never a guessed value) and the
|
|
2262
2262
|
* structural pieces (delimiters, record types, sequence counters, terminator) are
|
|
2263
2263
|
* computed. The result parses back to an equal message: build → parse fidelity by
|
|
2264
2264
|
* construction.
|
|
@@ -2278,7 +2278,7 @@ interface MessageInput {
|
|
|
2278
2278
|
declare function buildAstmMessage(input: MessageInput): string;
|
|
2279
2279
|
|
|
2280
2280
|
/**
|
|
2281
|
-
* Typed extractors over an {@link AstmMessage}
|
|
2281
|
+
* Typed extractors over an {@link AstmMessage}: the one-line "pull the value out"
|
|
2282
2282
|
* surface that is the package's north star.
|
|
2283
2283
|
*
|
|
2284
2284
|
* They are thin and immutable: they read the already-parsed model and never re-parse,
|
|
@@ -2287,7 +2287,7 @@ declare function buildAstmMessage(input: MessageInput): string;
|
|
|
2287
2287
|
* **They are scoped to a single message, and they enforce it.** An `AstmMessage` models a
|
|
2288
2288
|
* whole record **stream**, which may carry several `H` … `L` messages back to back. These
|
|
2289
2289
|
* extractors read across the whole stream, so on a multi-message stream `patient()` used to
|
|
2290
|
-
* answer with the first `P` in the stream while `results()` answered with every `R` in it
|
|
2290
|
+
* answer with the first `P` in the stream while `results()` answered with every `R` in it:
|
|
2291
2291
|
* pairing the two attributed one patient's results to another, silently and with no warning.
|
|
2292
2292
|
* They now **refuse** that stream: an {@link AstmAmbiguousStreamError} instead of a confident
|
|
2293
2293
|
* wrong answer, which is the same rule the rest of this parser follows. {@link messages} is
|
|
@@ -2303,7 +2303,7 @@ declare function buildAstmMessage(input: MessageInput): string;
|
|
|
2303
2303
|
* @param msg - A parsed **single-message** stream.
|
|
2304
2304
|
* @returns The result records (possibly empty).
|
|
2305
2305
|
* @throws {@link AstmAmbiguousStreamError} (`ASTM_AMBIGUOUS_MULTI_MESSAGE`) when the stream
|
|
2306
|
-
* carries more than one message
|
|
2306
|
+
* carries more than one message: see {@link messages}.
|
|
2307
2307
|
* @example
|
|
2308
2308
|
* ```ts
|
|
2309
2309
|
* import { parseAstmRecords, results } from "@cosyte/astm";
|
|
@@ -2318,7 +2318,7 @@ declare function results(msg: AstmMessage): readonly ResultRecord[];
|
|
|
2318
2318
|
* This is the identity a result files against, so it is answered only when the stream
|
|
2319
2319
|
* determines exactly one. It **refuses** the two shapes where "the patient" is a guess: a
|
|
2320
2320
|
* stream carrying several messages, and a single message carrying several `P` records. In
|
|
2321
|
-
* both, the old behaviour
|
|
2321
|
+
* both, the old behaviour, the first `P` in the stream, is the wrong-patient path itself.
|
|
2322
2322
|
*
|
|
2323
2323
|
* @param msg - A parsed single-message stream carrying at most one patient.
|
|
2324
2324
|
* @returns The patient record, or `undefined` when the message carries none.
|
|
@@ -2339,7 +2339,7 @@ declare function patient(msg: AstmMessage): PatientRecord | undefined;
|
|
|
2339
2339
|
* @param msg - A parsed **single-message** stream.
|
|
2340
2340
|
* @returns The order records (possibly empty).
|
|
2341
2341
|
* @throws {@link AstmAmbiguousStreamError} (`ASTM_AMBIGUOUS_MULTI_MESSAGE`) when the stream
|
|
2342
|
-
* carries more than one message
|
|
2342
|
+
* carries more than one message: see {@link messages}.
|
|
2343
2343
|
* @example
|
|
2344
2344
|
* ```ts
|
|
2345
2345
|
* import { parseAstmRecords, orders } from "@cosyte/astm";
|
|
@@ -2351,12 +2351,12 @@ declare function orders(msg: AstmMessage): readonly OrderRecord[];
|
|
|
2351
2351
|
/**
|
|
2352
2352
|
* Every comment (`C`) record in the message, in wire order. Each carries the
|
|
2353
2353
|
* `parentIndex` of the `H`/`P`/`O`/`R` it attaches to (or `attachedToRoot` when
|
|
2354
|
-
* it is an orphan)
|
|
2354
|
+
* it is an orphan): use {@link commentsFor} to get the comments of one record.
|
|
2355
2355
|
*
|
|
2356
2356
|
* @param msg - A parsed **single-message** stream.
|
|
2357
2357
|
* @returns The comment records (possibly empty).
|
|
2358
2358
|
* @throws {@link AstmAmbiguousStreamError} (`ASTM_AMBIGUOUS_MULTI_MESSAGE`) when the stream
|
|
2359
|
-
* carries more than one message
|
|
2359
|
+
* carries more than one message: see {@link messages}.
|
|
2360
2360
|
* @example
|
|
2361
2361
|
* ```ts
|
|
2362
2362
|
* import { parseAstmRecords, comments } from "@cosyte/astm";
|
|
@@ -2371,7 +2371,7 @@ declare function comments(msg: AstmMessage): readonly CommentRecord[];
|
|
|
2371
2371
|
* Unlike the other extractors this works on **any** stream, single- or multi-message: the
|
|
2372
2372
|
* parent record it is handed already names the message, so there is nothing to disambiguate.
|
|
2373
2373
|
* Returns the comments whose `parentIndex` is that record's `recordIndex`, so a
|
|
2374
|
-
* comment carrying (e.g.) QC context is read against the record it modifies
|
|
2374
|
+
* comment carrying (e.g.) QC context is read against the record it modifies:
|
|
2375
2375
|
* never floated to the wrong one.
|
|
2376
2376
|
*
|
|
2377
2377
|
* @param msg - A parsed message.
|
|
@@ -2387,13 +2387,13 @@ declare function comments(msg: AstmMessage): readonly CommentRecord[];
|
|
|
2387
2387
|
declare function commentsFor(msg: AstmMessage, parent: AstmRecord): readonly CommentRecord[];
|
|
2388
2388
|
/**
|
|
2389
2389
|
* Every request-information (`Q`) record in the message, in wire order. A non-empty
|
|
2390
|
-
* result means the message is a **host-query request**, not a result set
|
|
2390
|
+
* result means the message is a **host-query request**, not a result set: see
|
|
2391
2391
|
* {@link AstmMessage.classification} (`isHostQueryRequest`).
|
|
2392
2392
|
*
|
|
2393
2393
|
* @param msg - A parsed **single-message** stream.
|
|
2394
2394
|
* @returns The query records (possibly empty).
|
|
2395
2395
|
* @throws {@link AstmAmbiguousStreamError} (`ASTM_AMBIGUOUS_MULTI_MESSAGE`) when the stream
|
|
2396
|
-
* carries more than one message
|
|
2396
|
+
* carries more than one message: see {@link messages}.
|
|
2397
2397
|
* @example
|
|
2398
2398
|
* ```ts
|
|
2399
2399
|
* import { parseAstmRecords, query } from "@cosyte/astm";
|
|
@@ -2404,13 +2404,13 @@ declare function commentsFor(msg: AstmMessage, parent: AstmRecord): readonly Com
|
|
|
2404
2404
|
declare function query(msg: AstmMessage): readonly QueryRecord[];
|
|
2405
2405
|
|
|
2406
2406
|
/**
|
|
2407
|
-
* Message grouping
|
|
2407
|
+
* Message grouping: the safe replacement for the stream-scoped extractors.
|
|
2408
2408
|
*
|
|
2409
2409
|
* A parsed {@link AstmMessage} models a whole **record stream**, and a stream may carry
|
|
2410
2410
|
* several messages back to back: a message runs from an `H` header to its `L` terminator.
|
|
2411
2411
|
* The flat extractors in `./extractors.js` read the whole stream, so on a multi-message
|
|
2412
2412
|
* stream `patient(msg)` answered with the first `P` in the stream while `results(msg)`
|
|
2413
|
-
* answered with every `R` in it
|
|
2413
|
+
* answered with every `R` in it: pairing the two, which is exactly what the package's
|
|
2414
2414
|
* one-line north star does, attributed one patient's results to another. It needed no
|
|
2415
2415
|
* redeclaration and no unusual delimiters, and it warned about nothing.
|
|
2416
2416
|
*
|
|
@@ -2431,20 +2431,20 @@ declare function query(msg: AstmMessage): readonly QueryRecord[];
|
|
|
2431
2431
|
* about the unit itself: a message is bounded by the `H` record at one end and the `L`
|
|
2432
2432
|
* record at the other. That clause is in CLSI's free sample and is the whole of what is
|
|
2433
2433
|
* claimed from the standard here. It does **not** settle which record *opens* a new scope
|
|
2434
|
-
* partway through a stream, so treating a second `H` as the start of the next message
|
|
2435
|
-
* rather than, say, requiring an intervening `L`
|
|
2434
|
+
* partway through a stream, so treating a second `H` as the start of the next message
|
|
2435
|
+
* (rather than, say, requiring an intervening `L`) is a reasoned choice, not a citation. It
|
|
2436
2436
|
* is made this way because it is the boundary this parser already enforces for delimiters,
|
|
2437
2437
|
* and because it never drops a record.
|
|
2438
2438
|
*
|
|
2439
|
-
* **What is deliberately not modeled.** ASTM's within-message record hierarchy
|
|
2440
|
-
* a given `R` files against when one message carries several patients
|
|
2439
|
+
* **What is deliberately not modeled.** ASTM's within-message record hierarchy, which `P`
|
|
2440
|
+
* a given `R` files against when one message carries several patients, is not modeled
|
|
2441
2441
|
* here. The clauses that would ground it (the message-level structure diagram, and the `P`
|
|
2442
2442
|
* sequence-number rule) are withheld from the free sample and are paywalled, so rather than
|
|
2443
2443
|
* guess a scoping rule this layer refuses to answer: a message carrying more than one `P`
|
|
2444
2444
|
* leaves {@link AstmStreamMessage.patient} `undefined` and surfaces all of them on
|
|
2445
|
-
* {@link AstmStreamMessage.patients}. Multi-patient messages are real
|
|
2445
|
+
* {@link AstmStreamMessage.patients}. Multi-patient messages are real, at least one
|
|
2446
2446
|
* openly-published vendor interface grammar makes the patient group repeatable on the
|
|
2447
|
-
* download direction
|
|
2447
|
+
* download direction, so this is a deferral with a known shape, not a claim that the case
|
|
2448
2448
|
* does not arise, and not a claim that the standard is silent about it.
|
|
2449
2449
|
*
|
|
2450
2450
|
* **The OSS corpus was checked and offers no prior art.** It is informative here, unlike
|
|
@@ -2478,11 +2478,11 @@ declare const AMBIGUOUS_CODES: {
|
|
|
2478
2478
|
/** The one message in the stream carries more than one `P`, so "the patient" is not determined. */
|
|
2479
2479
|
readonly ASTM_AMBIGUOUS_MULTI_PATIENT: "ASTM_AMBIGUOUS_MULTI_PATIENT";
|
|
2480
2480
|
};
|
|
2481
|
-
/** A value from {@link AMBIGUOUS_CODES}
|
|
2481
|
+
/** A value from {@link AMBIGUOUS_CODES}: the discriminant on {@link AstmAmbiguousStreamError}. */
|
|
2482
2482
|
type AmbiguousCode = (typeof AMBIGUOUS_CODES)[keyof typeof AMBIGUOUS_CODES];
|
|
2483
2483
|
/**
|
|
2484
2484
|
* Thrown by a flat extractor when the stream it was handed does not determine a single
|
|
2485
|
-
* answer
|
|
2485
|
+
* answer: either because it carries several messages, or because its one message carries
|
|
2486
2486
|
* several patients.
|
|
2487
2487
|
*
|
|
2488
2488
|
* The throw **is** the fix. A caller who gets this error is strictly better off than one
|
|
@@ -2506,7 +2506,7 @@ type AmbiguousCode = (typeof AMBIGUOUS_CODES)[keyof typeof AMBIGUOUS_CODES];
|
|
|
2506
2506
|
* ```
|
|
2507
2507
|
*/
|
|
2508
2508
|
declare class AstmAmbiguousStreamError extends Error {
|
|
2509
|
-
/** The stable discriminant
|
|
2509
|
+
/** The stable discriminant: see {@link AMBIGUOUS_CODES}. */
|
|
2510
2510
|
readonly code: AmbiguousCode;
|
|
2511
2511
|
/** Where the ambiguity became visible: the second header, or the second `P`. Value-free. */
|
|
2512
2512
|
readonly position: AstmPosition;
|
|
@@ -2532,7 +2532,7 @@ interface AstmStreamMessage {
|
|
|
2532
2532
|
/** This message's header record. */
|
|
2533
2533
|
readonly header: HeaderRecord;
|
|
2534
2534
|
/**
|
|
2535
|
-
* The delimiter set in force for this message
|
|
2535
|
+
* The delimiter set in force for this message: the header's own resolved set, which is
|
|
2536
2536
|
* the set its records were actually read with. When a later header's declaration was
|
|
2537
2537
|
* unusable, this is the set that stayed in force, never a guessed one.
|
|
2538
2538
|
*/
|
|
@@ -2540,7 +2540,7 @@ interface AstmStreamMessage {
|
|
|
2540
2540
|
/** Every record of this message in wire order, the header first. */
|
|
2541
2541
|
readonly records: readonly AstmRecord[];
|
|
2542
2542
|
/**
|
|
2543
|
-
* This message's patient
|
|
2543
|
+
* This message's patient, but **only when the message determines one**: the single `P`
|
|
2544
2544
|
* when it carries exactly one, and `undefined` when it carries none **or** several.
|
|
2545
2545
|
* `patients.length` distinguishes those two cases; there is no third meaning.
|
|
2546
2546
|
*
|
|
@@ -2600,14 +2600,14 @@ declare function messages(msg: AstmMessage): readonly AstmStreamMessage[];
|
|
|
2600
2600
|
* order flow, so this module makes the distinction **explicit**:
|
|
2601
2601
|
*
|
|
2602
2602
|
* **The `Q` record dominates.** Any message carrying a `Q` (request-information)
|
|
2603
|
-
* record is classified `host-query
|
|
2603
|
+
* record is classified `host-query`, a request, and is **never** treated as a
|
|
2604
2604
|
* result set, even if a result record is also present (an anomaly, which the
|
|
2605
2605
|
* parser flags separately with `ASTM_RECORD_AMBIGUOUS_MESSAGE_KIND`). This is the
|
|
2606
2606
|
* whole point: a `Q`-bearing message can never silently read as a result upload.
|
|
2607
2607
|
*/
|
|
2608
2608
|
|
|
2609
2609
|
/**
|
|
2610
|
-
* Classify a record stream by the host-query flow. Pure and total
|
|
2610
|
+
* Classify a record stream by the host-query flow. Pure and total: it only reads
|
|
2611
2611
|
* the record type letters, never a field value.
|
|
2612
2612
|
*
|
|
2613
2613
|
* `Q` dominates: a message with any `Q` record is a `host-query` request even when
|
|
@@ -2686,7 +2686,7 @@ declare function tokenizeHeader(record: string, d: Delimiters, onUnknownEscape?:
|
|
|
2686
2686
|
/**
|
|
2687
2687
|
* The primary scalar of a field: its first repeat's first component, decoded.
|
|
2688
2688
|
* Returns `undefined` for a truly empty field so callers can distinguish
|
|
2689
|
-
* "absent" from a value
|
|
2689
|
+
* "absent" from a value: never defaulting a missing value.
|
|
2690
2690
|
*
|
|
2691
2691
|
* @example
|
|
2692
2692
|
* ```ts
|
|
@@ -2705,7 +2705,7 @@ declare function fieldScalar(field: AstmField | undefined): string | undefined;
|
|
|
2705
2705
|
* `AstmParseError` is thrown directly and consumers narrow via the `code`
|
|
2706
2706
|
* discriminant. {@link FatalCode} is **closed**: these three are all of it. The frame
|
|
2707
2707
|
* codec reuses `EMPTY_INPUT` (an empty stream is fatal in both its lenient and strict
|
|
2708
|
-
* modes) but does not widen the union
|
|
2708
|
+
* modes) but does not widen the union, its own thrown errors are separate types with
|
|
2709
2709
|
* their own discriminants: {@link AstmFrameEncodeError} carries
|
|
2710
2710
|
* `ASTM_FRAME_EMPTY_RECORD`, and {@link AstmFrameStrictError} carries the rejected
|
|
2711
2711
|
* warnings rather than a `code`. So narrowing an `AstmParseError` on `code` will only
|
|
@@ -2730,15 +2730,15 @@ declare function fieldScalar(field: AstmField | undefined): string | undefined;
|
|
|
2730
2730
|
* ```
|
|
2731
2731
|
*/
|
|
2732
2732
|
declare const FATAL_CODES: {
|
|
2733
|
-
/** Input was empty or whitespace-only
|
|
2733
|
+
/** Input was empty or whitespace-only: there is nothing to parse. Shared across layers. */
|
|
2734
2734
|
readonly EMPTY_INPUT: "EMPTY_INPUT";
|
|
2735
|
-
/** The first record is not an `H` (header) record
|
|
2735
|
+
/** The first record is not an `H` (header) record: an ASTM message must lead with `H`. */
|
|
2736
2736
|
readonly ASTM_RECORD_NO_HEADER: "ASTM_RECORD_NO_HEADER";
|
|
2737
2737
|
/** The `H` record is too short to declare the four delimiters (field/repeat/component/escape). */
|
|
2738
2738
|
readonly ASTM_RECORD_UNDECLARED_DELIMITERS: "ASTM_RECORD_UNDECLARED_DELIMITERS";
|
|
2739
2739
|
};
|
|
2740
2740
|
/**
|
|
2741
|
-
* A value from {@link FATAL_CODES}
|
|
2741
|
+
* A value from {@link FATAL_CODES}: the type carried by a thrown {@link AstmParseError}.
|
|
2742
2742
|
*/
|
|
2743
2743
|
type FatalCode = (typeof FATAL_CODES)[keyof typeof FATAL_CODES];
|
|
2744
2744
|
/**
|
|
@@ -2774,7 +2774,7 @@ declare class AstmParseError extends Error {
|
|
|
2774
2774
|
}
|
|
2775
2775
|
|
|
2776
2776
|
/**
|
|
2777
|
-
* The ASTM escape codec
|
|
2777
|
+
* The ASTM escape codec: the fix for a documented silent-misread class.
|
|
2778
2778
|
*
|
|
2779
2779
|
* ASTM escapes an embedded delimiter with the escape character on both sides of
|
|
2780
2780
|
* a single mnemonic letter (the InterSystems mnemonics, cross-verified against
|
|
@@ -2788,12 +2788,12 @@ declare class AstmParseError extends Error {
|
|
|
2788
2788
|
* ```
|
|
2789
2789
|
*
|
|
2790
2790
|
* **Why this is a requirement, not a nicety.** A result value can legitimately
|
|
2791
|
-
* contain a component delimiter
|
|
2791
|
+
* contain a component delimiter: e.g. a titre written `1^40`. On the wire that
|
|
2792
2792
|
* is escaped as `1&S&40`. A parser that splits a field into components on the
|
|
2793
2793
|
* raw component delimiter and *never decodes the escape* is safe here (the
|
|
2794
2794
|
* escape body `&S&` contains no literal `^`), but a parser that **decodes first
|
|
2795
2795
|
* and splits second** turns `1&S&40` into `1^40` and then mis-splits it into two
|
|
2796
|
-
* components
|
|
2796
|
+
* components: the silent misread `python-astm` / `Chistousov` exhibit. The
|
|
2797
2797
|
* correct order, implemented here, is **escape-aware split, then decode each
|
|
2798
2798
|
* leaf**: the escape sequence survives the split intact and only then decodes to
|
|
2799
2799
|
* a single literal, so `1&S&40` reads as exactly one component.
|
|
@@ -2832,7 +2832,7 @@ declare function decodeEscapes(leaf: string, d: Delimiters, onUnknown?: UnknownE
|
|
|
2832
2832
|
/**
|
|
2833
2833
|
* Split `text` on `delimiter`, treating any escape sequence (`escape …
|
|
2834
2834
|
* escape`) as an opaque atom so a delimiter that appears *inside* an escape body
|
|
2835
|
-
* never causes a split. Returns the raw (still-encoded) segments
|
|
2835
|
+
* never causes a split. Returns the raw (still-encoded) segments: decoding is
|
|
2836
2836
|
* the caller's next step, per the escape-aware-split-then-decode contract.
|
|
2837
2837
|
*
|
|
2838
2838
|
* For the four canonical mnemonics this is belt-and-suspenders (their bodies are
|
|
@@ -2877,12 +2877,12 @@ declare function splitEscapeAware(text: string, delimiter: string, escape: strin
|
|
|
2877
2877
|
declare function deepFreeze<T>(value: T): T;
|
|
2878
2878
|
|
|
2879
2879
|
/**
|
|
2880
|
-
* `defineAstmProfile()
|
|
2880
|
+
* `defineAstmProfile()`: the public factory for building immutable
|
|
2881
2881
|
* {@link AstmProfile} objects with the safety rules enforced and `describe()`
|
|
2882
2882
|
* attached. Mirrors the sibling `@cosyte/hl7` `defineProfile()` / `@cosyte/ccda`
|
|
2883
2883
|
* `defineCcdaProfile()` shape (name / lineage / `extends`-merge / `describe`) while
|
|
2884
|
-
* modelling ASTM quirks
|
|
2885
|
-
* optional raw-vs-framed transport override
|
|
2884
|
+
* modelling ASTM quirks (a tolerated warning code with provenance, plus the
|
|
2885
|
+
* optional raw-vs-framed transport override) rather than HL7 v2 Z-segments or
|
|
2886
2886
|
* C-CDA template deviations.
|
|
2887
2887
|
*
|
|
2888
2888
|
* Zero runtime deps. No `any`; immutability at the return boundary via
|
|
@@ -2892,7 +2892,7 @@ declare function deepFreeze<T>(value: T): T;
|
|
|
2892
2892
|
/**
|
|
2893
2893
|
* Build a frozen {@link AstmProfile} from a validated options object. Throws
|
|
2894
2894
|
* {@link AstmProfileDefinitionError} on a bad name, an unknown option key, an
|
|
2895
|
-
* invalid `transport`, or an invalid `tolerate` entry
|
|
2895
|
+
* invalid `transport`, or an invalid `tolerate` entry, including the **safety
|
|
2896
2896
|
* rule**: a profile may never tolerate a safety-critical warning code (default-deny
|
|
2897
2897
|
* across all three registries).
|
|
2898
2898
|
*
|
|
@@ -2925,18 +2925,18 @@ declare function defineAstmProfile(opts: DefineAstmProfileOptions): AstmProfile;
|
|
|
2925
2925
|
/**
|
|
2926
2926
|
* The definition-time error for the `@cosyte/astm` vendor-profile subsystem.
|
|
2927
2927
|
*
|
|
2928
|
-
* A profile is validated when it is *defined*, not when it is applied
|
|
2928
|
+
* A profile is validated when it is *defined*, not when it is applied, so a
|
|
2929
2929
|
* misconfigured profile (a bad name, an unknown option key, a tolerated code that
|
|
2930
2930
|
* is unknown or **safety-critical**) fails loudly at construction, long before it
|
|
2931
2931
|
* could quiet a real deviation. This is the class that failure throws. It is a
|
|
2932
2932
|
* programming error (a bad profile literal), distinct from `AstmParseError` (bad
|
|
2933
|
-
* input)
|
|
2933
|
+
* input), so consumers narrow on it separately.
|
|
2934
2934
|
*/
|
|
2935
2935
|
/**
|
|
2936
2936
|
* Thrown by `defineAstmProfile` when a profile definition is invalid: a missing or
|
|
2937
2937
|
* empty `name`, an unknown option key, an invalid `transport` value, or a
|
|
2938
|
-
* `tolerate` entry whose code is unknown, whose `rationale` is empty, or
|
|
2939
|
-
* load-bearing safety rule
|
|
2938
|
+
* `tolerate` entry whose code is unknown, whose `rationale` is empty, or, the
|
|
2939
|
+
* load-bearing safety rule, whose code is **safety-critical** (a result value,
|
|
2940
2940
|
* abnormal flag, result status, reference range, units, patient/comment context,
|
|
2941
2941
|
* message-kind, code system, or any frame/LTP integrity warning). Carries the
|
|
2942
2942
|
* offending profile's `name` when it is known, so a multi-profile build names the
|
|
@@ -2976,14 +2976,14 @@ declare class AstmProfileDefinitionError extends Error {
|
|
|
2976
2976
|
* option always wins, and `profile: null` opts out of the default for a single call.
|
|
2977
2977
|
*
|
|
2978
2978
|
* The single mutable module-scoped `let` is the only such state in the profile
|
|
2979
|
-
* subsystem
|
|
2979
|
+
* subsystem: an intentional, documented trade-off (identical to the siblings).
|
|
2980
2980
|
* Tests that set a default MUST clear it in teardown (`setDefaultAstmProfile(null)`)
|
|
2981
2981
|
* to avoid cross-test bleed.
|
|
2982
2982
|
*/
|
|
2983
2983
|
|
|
2984
2984
|
/**
|
|
2985
2985
|
* Look up a built-in profile by name. Returns `undefined` when no built-in has that
|
|
2986
|
-
* name (a user-defined profile is not in this registry
|
|
2986
|
+
* name (a user-defined profile is not in this registry, pass it directly).
|
|
2987
2987
|
*
|
|
2988
2988
|
* @param name - The built-in profile name.
|
|
2989
2989
|
* @returns The profile, or `undefined`.
|
|
@@ -3011,7 +3011,7 @@ declare function listAstmProfiles(): readonly string[];
|
|
|
3011
3011
|
* clear. An explicit `parseAstmRecords(raw, { profile })` always wins;
|
|
3012
3012
|
* `{ profile: null }` opts out of the default for a single call.
|
|
3013
3013
|
*
|
|
3014
|
-
* **Test hygiene:** the only mutable module-scoped state here
|
|
3014
|
+
* **Test hygiene:** the only mutable module-scoped state here, tests that call this
|
|
3015
3015
|
* MUST clear it in teardown or default-profile bleed infects later tests.
|
|
3016
3016
|
*
|
|
3017
3017
|
* @param profile - The profile to register as default, or `null` to clear.
|
|
@@ -3044,7 +3044,7 @@ declare function getDefaultAstmProfile(): AstmProfile | undefined;
|
|
|
3044
3044
|
* plus the transport-override accessor the framing detector consumes.
|
|
3045
3045
|
*
|
|
3046
3046
|
* A profile **never touches an extracted value**. The warning transform operates
|
|
3047
|
-
* purely at the warning layer
|
|
3047
|
+
* purely at the warning layer: downgrading a deviation it *expects* into a
|
|
3048
3048
|
* `PROFILE_QUIRK_APPLIED` warning (flagged `expected`, carrying the original
|
|
3049
3049
|
* `toleratedCode`) while leaving every un-expected warning untouched. Because the
|
|
3050
3050
|
* transform only ever rewrites a warning object (never a record, field, or value),
|
|
@@ -3076,7 +3076,7 @@ declare function applyAstmProfile(profile: AstmProfile, warning: AstmRecordWarni
|
|
|
3076
3076
|
* Apply a profile across a whole warning list, returning a NEW array (the input is
|
|
3077
3077
|
* never mutated). Returned unchanged (same reference contents, a shallow copy) when
|
|
3078
3078
|
* `profile` is `undefined`, so the no-profile path pays only a copy. Every tolerated
|
|
3079
|
-
* warning is re-badged; every other warning passes through by identity
|
|
3079
|
+
* warning is re-badged; every other warning passes through by identity, nothing is
|
|
3080
3080
|
* dropped, reordered, or reallocated beyond the re-badge.
|
|
3081
3081
|
*
|
|
3082
3082
|
* @param warnings - The accumulated record warnings.
|
|
@@ -3090,7 +3090,7 @@ declare function applyAstmProfile(profile: AstmProfile, warning: AstmRecordWarni
|
|
|
3090
3090
|
*/
|
|
3091
3091
|
declare function applyAstmProfileToWarnings(warnings: readonly AstmRecordWarning[], profile: AstmProfile | undefined): AstmRecordWarning[];
|
|
3092
3092
|
/**
|
|
3093
|
-
* The profile's transport override, if any
|
|
3093
|
+
* The profile's transport override, if any: the value a consumer feeds to
|
|
3094
3094
|
* `detectFraming(bytes, { override })` to force framed/raw and bypass leading-byte
|
|
3095
3095
|
* auto-detection. `undefined` means "let detection decide."
|
|
3096
3096
|
*
|
|
@@ -3108,11 +3108,11 @@ declare function resolveProfileTransport(profile: AstmProfile | undefined): Astm
|
|
|
3108
3108
|
/**
|
|
3109
3109
|
* The safety spine of the profile subsystem: the set of warning codes a profile is
|
|
3110
3110
|
* **forbidden** to tolerate. A vendor profile exists to quiet known, benign
|
|
3111
|
-
* structural noise
|
|
3111
|
+
* structural noise: never to hide a deviation that could change a clinical
|
|
3112
3112
|
* reading or corrupt the wire.
|
|
3113
3113
|
*
|
|
3114
3114
|
* **Design: default-deny.** Rather than enumerate the *forbidden* codes (a list you
|
|
3115
|
-
* can forget to extend when a new warning ships
|
|
3115
|
+
* can forget to extend when a new warning ships, the exact incompleteness a
|
|
3116
3116
|
* safety gate must not have), this module enumerates the tiny set of **explicitly
|
|
3117
3117
|
* tolerable** codes and treats *every other* code, across all three registries
|
|
3118
3118
|
* (record `ASTM_RECORD_*`, frame `ASTM_FRAME_*`, protocol `ASTM_LTP_*`), as
|
|
@@ -3123,32 +3123,32 @@ declare function resolveProfileTransport(profile: AstmProfile | undefined): Astm
|
|
|
3123
3123
|
*
|
|
3124
3124
|
* The four tolerable codes are the only deviations that (a) are structural/
|
|
3125
3125
|
* syntactic vendor noise and (b) **cannot alter, drop, or fabricate an extracted
|
|
3126
|
-
* value
|
|
3126
|
+
* value**: because a profile only ever re-badges a *warning*, never re-parses. In
|
|
3127
3127
|
* every one, the underlying datum is already surfaced verbatim regardless of the
|
|
3128
3128
|
* profile:
|
|
3129
3129
|
*
|
|
3130
|
-
* - `ASTM_RECORD_UNKNOWN_TYPE
|
|
3130
|
+
* - `ASTM_RECORD_UNKNOWN_TYPE`: an unmodeled record type, surfaced verbatim as an
|
|
3131
3131
|
* `unsupported` record carrying no interpreted clinical field.
|
|
3132
|
-
* - `ASTM_NONSTANDARD_DELIMITERS
|
|
3132
|
+
* - `ASTM_NONSTANDARD_DELIMITERS`: the header's own declared delimiters are read
|
|
3133
3133
|
* and honored either way; this only notes they differ from the canonical set.
|
|
3134
|
-
* - `ASTM_UNKNOWN_ESCAPE_SEQUENCE
|
|
3134
|
+
* - `ASTM_UNKNOWN_ESCAPE_SEQUENCE`: an unrecognized `&…&` body is **preserved
|
|
3135
3135
|
* byte-for-byte** in the decoded value (the escape codec never guesses); the
|
|
3136
3136
|
* warning is a purely syntactic advisory, the value is identical with or without
|
|
3137
3137
|
* the profile.
|
|
3138
|
-
* - `ASTM_RECORD_UNINTERPRETED_QUERY_STATUS
|
|
3138
|
+
* - `ASTM_RECORD_UNINTERPRETED_QUERY_STATUS`: a `Q`-record request-information
|
|
3139
3139
|
* status surfaced verbatim on a *request* record; the code set is paywalled and
|
|
3140
3140
|
* is never interpreted, profile or not.
|
|
3141
3141
|
*
|
|
3142
|
-
* Everything a wrong value could hide from
|
|
3142
|
+
* Everything a wrong value could hide from: a result value split ambiguity, an
|
|
3143
3143
|
* undefined abnormal flag or result status, an unparseable reference range, absent
|
|
3144
3144
|
* units, a mis-attached comment, a partial timestamp, a query-vs-result ambiguity,
|
|
3145
3145
|
* a bad frame checksum / sequence gap / unterminated / oversize frame, an
|
|
3146
|
-
* ambiguous transport, an unexpected protocol event, or a rejected frame
|
|
3146
|
+
* ambiguous transport, an unexpected protocol event, or a rejected frame: is
|
|
3147
3147
|
* forbidden.
|
|
3148
3148
|
*/
|
|
3149
3149
|
|
|
3150
3150
|
/**
|
|
3151
|
-
* The **only** warning codes a profile may list in its `tolerate` set
|
|
3151
|
+
* The **only** warning codes a profile may list in its `tolerate` set: benign,
|
|
3152
3152
|
* structural/syntactic vendor noise that cannot alter, drop, or fabricate an
|
|
3153
3153
|
* extracted value. Frozen so it cannot be mutated at runtime to smuggle a code in.
|
|
3154
3154
|
* Adding to this set is a deliberate, reviewable act (and each addition must be an
|
|
@@ -3174,7 +3174,7 @@ declare const TOLERABLE_CODES: ReadonlySet<AnyAstmWarningCode>;
|
|
|
3174
3174
|
*/
|
|
3175
3175
|
declare const ALL_ASTM_WARNING_CODES: ReadonlySet<AnyAstmWarningCode>;
|
|
3176
3176
|
/**
|
|
3177
|
-
* The forbidden set
|
|
3177
|
+
* The forbidden set: computed as **every known code minus the tolerable
|
|
3178
3178
|
* allow-list**, so it is complete by construction. Frozen. A code appears here iff
|
|
3179
3179
|
* it is a real warning code that is not in {@link TOLERABLE_CODES}.
|
|
3180
3180
|
*
|
|
@@ -3187,7 +3187,7 @@ declare const ALL_ASTM_WARNING_CODES: ReadonlySet<AnyAstmWarningCode>;
|
|
|
3187
3187
|
*/
|
|
3188
3188
|
declare const SAFETY_CRITICAL_CODES: ReadonlySet<AnyAstmWarningCode>;
|
|
3189
3189
|
/**
|
|
3190
|
-
* True when `code` is a known warning code that is **not** tolerable
|
|
3190
|
+
* True when `code` is a known warning code that is **not** tolerable: i.e. a
|
|
3191
3191
|
* profile may never list it. A code that is not a real warning code at all returns
|
|
3192
3192
|
* `false` here (the validator reports "unknown code" separately, a distinct
|
|
3193
3193
|
* failure with a distinct message).
|
|
@@ -3205,14 +3205,14 @@ declare function isSafetyCriticalCode(code: string): boolean;
|
|
|
3205
3205
|
|
|
3206
3206
|
/**
|
|
3207
3207
|
* The `default` conservative baseline profile. Tolerates **nothing** and forces no
|
|
3208
|
-
* transport
|
|
3208
|
+
* transport: every deviation surfaces as its own warning, unmodified, and framing
|
|
3209
3209
|
* is auto-detected. It exists so "no profile" and "the default profile" are the
|
|
3210
3210
|
* same, explicit behaviour, and so a consumer can name the baseline when composing
|
|
3211
3211
|
* (`extends: astmProfiles.default`) without importing a special sentinel. Absence of
|
|
3212
|
-
* a profile means this conservative default applied
|
|
3212
|
+
* a profile means this conservative default applied: not that a stream was fully
|
|
3213
3213
|
* understood.
|
|
3214
3214
|
*
|
|
3215
|
-
* Authored through the public `defineAstmProfile()` API
|
|
3215
|
+
* Authored through the public `defineAstmProfile()` API: zero privileged coupling;
|
|
3216
3216
|
* it is exactly what a user would write.
|
|
3217
3217
|
*/
|
|
3218
3218
|
|
|
@@ -3229,13 +3229,13 @@ declare function isSafetyCriticalCode(code: string): boolean;
|
|
|
3229
3229
|
declare const defaultProfile: AstmProfile;
|
|
3230
3230
|
|
|
3231
3231
|
/**
|
|
3232
|
-
* The `referenceCorpus` profile
|
|
3232
|
+
* The `referenceCorpus` profile: a **non-vendor**, evidence-backed tolerance
|
|
3233
3233
|
* grounded firsthand in the **redistributable OSS reference corpus**, not an
|
|
3234
3234
|
* invented per-vendor deviation matrix.
|
|
3235
3235
|
*
|
|
3236
3236
|
* Grounding (firsthand-verified 2026-07-21; the corpus is permissively licensed):
|
|
3237
3237
|
*
|
|
3238
|
-
* - **`kxepal/python-astm` `codec.py`** (BSD)
|
|
3238
|
+
* - **`kxepal/python-astm` `codec.py`** (BSD), its `decode_record` /
|
|
3239
3239
|
* `decode_component` / `decode_repeated_component` split fields, components, and
|
|
3240
3240
|
* repeats on the raw delimiters and **never un-escape** ASTM escape sequences: the
|
|
3241
3241
|
* source contains zero handling of `&F&`/`&S&`/`&R&`/`&E&`, and the escape
|
|
@@ -3247,15 +3247,15 @@ declare const defaultProfile: AstmProfile;
|
|
|
3247
3247
|
* names) that our **escape-aware** tokenizer flags as
|
|
3248
3248
|
* `ASTM_UNKNOWN_ESCAPE_SEQUENCE`. We are deliberately stricter than the OSS
|
|
3249
3249
|
* references here, so the warning is *expected*
|
|
3250
|
-
* noise when talking to that ecosystem
|
|
3250
|
+
* noise when talking to that ecosystem: recognizable and non-clinical.
|
|
3251
3251
|
*
|
|
3252
3252
|
* **Why this is safe.** The escape codec **preserves an unrecognized `&…&` body
|
|
3253
3253
|
* byte-for-byte** in the decoded value (`decodeEscapes` never guesses); the value a
|
|
3254
3254
|
* consumer reads is identical whether or not this profile is active. The profile
|
|
3255
|
-
* only re-badges the *warning
|
|
3255
|
+
* only re-badges the *warning*: no value, flag, status, or identifier is altered
|
|
3256
3256
|
* or dropped. And by the safety gate in `defineAstmProfile`, it **cannot** tolerate
|
|
3257
3257
|
* a result value / flag / status / range / units, a patient or comment context, a
|
|
3258
|
-
* message-kind ambiguity, or any frame / LTP integrity warning
|
|
3258
|
+
* message-kind ambiguity, or any frame / LTP integrity warning: those always
|
|
3259
3259
|
* surface, profile or not.
|
|
3260
3260
|
*
|
|
3261
3261
|
* **Honesty.** This is a *reference-corpus* profile, not a claim about any named
|
|
@@ -3265,7 +3265,7 @@ declare const defaultProfile: AstmProfile;
|
|
|
3265
3265
|
|
|
3266
3266
|
/**
|
|
3267
3267
|
* Tolerates the non-standard-escape noise that streams from the escape-agnostic OSS
|
|
3268
|
-
* reference stack (`python-astm` / `senaite`) carry
|
|
3268
|
+
* reference stack (`python-astm` / `senaite`) carry: the value is preserved
|
|
3269
3269
|
* verbatim; only the syntactic warning is expected.
|
|
3270
3270
|
*
|
|
3271
3271
|
* @example
|
|
@@ -3285,7 +3285,7 @@ declare const referenceCorpus: AstmProfile;
|
|
|
3285
3285
|
* set, the definition-time error, and the profile types.
|
|
3286
3286
|
*
|
|
3287
3287
|
* The individual built-ins are reached via `astmProfiles.referenceCorpus` etc., not
|
|
3288
|
-
* as top-level named exports
|
|
3288
|
+
* as top-level named exports: mirrors the sibling `profiles` namespace convention
|
|
3289
3289
|
* ("default" is too generic for a top-level export).
|
|
3290
3290
|
*/
|
|
3291
3291
|
|
|
@@ -3295,7 +3295,7 @@ declare const referenceCorpus: AstmProfile;
|
|
|
3295
3295
|
* redistributable OSS reference corpus), each authored via the public
|
|
3296
3296
|
* `defineAstmProfile()` API and carrying its cited provenance. Named per-vendor
|
|
3297
3297
|
* profiles (cobas / Sysmex / …) are **deferred** pending a firsthand
|
|
3298
|
-
* vendor-attributed quirk document
|
|
3298
|
+
* vendor-attributed quirk document: the engine fully supports them; we do not ship
|
|
3299
3299
|
* ungrounded ones.
|
|
3300
3300
|
*
|
|
3301
3301
|
* @example
|
|
@@ -3315,17 +3315,17 @@ declare const astmProfiles: {
|
|
|
3315
3315
|
* from an IICC LIVD ("LOINC to Vendor IVD") mapping the consumer provides.
|
|
3316
3316
|
*
|
|
3317
3317
|
* **Bring-your-own, by design.** `@cosyte/astm` bundles
|
|
3318
|
-
* **no** LOINC, SNOMED, or LIVD data
|
|
3318
|
+
* **no** LOINC, SNOMED, or LIVD data: LOINC is © Regenstrief (redistributable
|
|
3319
3319
|
* only with its attribution notice) and the public CDC LIVD publication is a
|
|
3320
3320
|
* SARS-CoV-2-specific file that also carries SNOMED CT (separately licensed), not
|
|
3321
3321
|
* a general-analyte, public-domain catalog. So the package stays a **structural
|
|
3322
3322
|
* recognizer, not a dictionary**: it recognizes the Universal Test ID's LOINC slot
|
|
3323
3323
|
* and surfaces vendor codes verbatim, and this module lets a consumer
|
|
3324
|
-
* *supply* their own LIVD file to map those codes
|
|
3324
|
+
* *supply* their own LIVD file to map those codes, the terminology data, and its
|
|
3325
3325
|
* license obligations, are the consumer's.
|
|
3326
3326
|
*
|
|
3327
3327
|
* **Grounded firsthand on the IICC LIVD digital format / HL7 LIVD IG.** The
|
|
3328
|
-
* mapping key is the **Vendor Analyte Code
|
|
3328
|
+
* mapping key is the **Vendor Analyte Code**: the vendor *transmission code* the
|
|
3329
3329
|
* instrument sends for an automated test, which is exactly the local code an ASTM
|
|
3330
3330
|
* analyzer puts in the Universal Test ID (`R`/`O` field, component 4). The mapping
|
|
3331
3331
|
* target is the **LOINC Code** (with the LOINC Long Common Name as an optional
|
|
@@ -3342,7 +3342,7 @@ declare const astmProfiles: {
|
|
|
3342
3342
|
* a catalog-hygiene concern the consumer owns.
|
|
3343
3343
|
*/
|
|
3344
3344
|
/**
|
|
3345
|
-
* One LIVD mapping row a consumer supplies
|
|
3345
|
+
* One LIVD mapping row a consumer supplies: a vendor test code and the LOINC it
|
|
3346
3346
|
* maps to, plus optional human-readable / provenance fields. Modeled on the IICC
|
|
3347
3347
|
* LIVD digital format's data elements.
|
|
3348
3348
|
*
|
|
@@ -3354,20 +3354,20 @@ declare const astmProfiles: {
|
|
|
3354
3354
|
*/
|
|
3355
3355
|
interface LivdEntry {
|
|
3356
3356
|
/**
|
|
3357
|
-
* The **Vendor Analyte Code
|
|
3357
|
+
* The **Vendor Analyte Code**: the vendor transmission code the instrument sends (the local code
|
|
3358
3358
|
* in an ASTM Universal Test ID, component 4). The mapping key; compared **verbatim** (exact,
|
|
3359
|
-
* case-sensitive) against the reported code
|
|
3359
|
+
* case-sensitive) against the reported code, never normalized or fuzzy-matched.
|
|
3360
3360
|
*/
|
|
3361
3361
|
readonly vendorCode: string;
|
|
3362
3362
|
/**
|
|
3363
3363
|
* The **LOINC Code** this vendor code maps to (e.g. `"1920-8"`). Taken from the consumer's catalog
|
|
3364
|
-
* as-is and **never validated, altered, or invented
|
|
3364
|
+
* as-is and **never validated, altered, or invented**: the parser does not ship a LOINC table and
|
|
3365
3365
|
* cannot check it; it only carries what the catalog says.
|
|
3366
3366
|
*/
|
|
3367
3367
|
readonly loinc: string;
|
|
3368
|
-
/** The **LOINC Long Common Name**, when the catalog supplies it
|
|
3368
|
+
/** The **LOINC Long Common Name**, when the catalog supplies it: an optional human-readable label. */
|
|
3369
3369
|
readonly loincLongName?: string;
|
|
3370
|
-
/** The **Vendor Analyte Name
|
|
3370
|
+
/** The **Vendor Analyte Name**: the vendor's human-readable analyte label, when supplied. */
|
|
3371
3371
|
readonly vendorAnalyteName?: string;
|
|
3372
3372
|
/** The device **Manufacturer**, when the catalog scopes the mapping to a device (optional provenance). */
|
|
3373
3373
|
readonly manufacturer?: string;
|
|
@@ -3376,7 +3376,7 @@ interface LivdEntry {
|
|
|
3376
3376
|
}
|
|
3377
3377
|
/**
|
|
3378
3378
|
* The outcome of looking a vendor code up in a {@link LivdCatalog}. A distinct
|
|
3379
|
-
* value per safe disposition
|
|
3379
|
+
* value per safe disposition: a hit is `mapped`; a miss is `unmapped`; a code that
|
|
3380
3380
|
* matched more than one **distinct** LOINC is `ambiguous` with the candidates
|
|
3381
3381
|
* surfaced but **none chosen**. There is deliberately no "guessed" case.
|
|
3382
3382
|
*/
|
|
@@ -3387,11 +3387,11 @@ type LivdLookup =
|
|
|
3387
3387
|
readonly loinc: string;
|
|
3388
3388
|
readonly loincLongName?: string;
|
|
3389
3389
|
}
|
|
3390
|
-
/** No entry for this code
|
|
3390
|
+
/** No entry for this code: a miss. The code stays verbatim; no LOINC is invented. */
|
|
3391
3391
|
| {
|
|
3392
3392
|
readonly status: "unmapped";
|
|
3393
3393
|
}
|
|
3394
|
-
/** More than one distinct LOINC
|
|
3394
|
+
/** More than one distinct LOINC: surfaced for inspection, never resolved to one. */
|
|
3395
3395
|
| {
|
|
3396
3396
|
readonly status: "ambiguous";
|
|
3397
3397
|
readonly candidates: readonly string[];
|
|
@@ -3407,7 +3407,7 @@ interface LivdCatalog {
|
|
|
3407
3407
|
/**
|
|
3408
3408
|
* Look a vendor code up, verbatim (exact, case-sensitive). Returns `mapped` on a
|
|
3409
3409
|
* single-LOINC hit, `unmapped` on a miss, and `ambiguous` when the code carries
|
|
3410
|
-
* more than one distinct LOINC
|
|
3410
|
+
* more than one distinct LOINC: never a guess.
|
|
3411
3411
|
*
|
|
3412
3412
|
* @param vendorCode - The reported vendor/local test code.
|
|
3413
3413
|
* @returns The lookup outcome.
|
|
@@ -3443,13 +3443,13 @@ declare function defineLivdCatalog(entries: readonly LivdEntry[]): LivdCatalog;
|
|
|
3443
3443
|
|
|
3444
3444
|
/**
|
|
3445
3445
|
* The **terminology** warning registry (`ASTM_LIVD_*`) for the LIVD-aware LOINC
|
|
3446
|
-
* recognition layer
|
|
3446
|
+
* recognition layer: the fourth of the package's registries,
|
|
3447
3447
|
* alongside the record layer's `ASTM_RECORD_*`, the frame codec's `ASTM_FRAME_*`,
|
|
3448
3448
|
* and the protocol reducer's `ASTM_LTP_*`.
|
|
3449
3449
|
*
|
|
3450
3450
|
* A terminology warning is the record of a **safe non-mapping**: a reported
|
|
3451
3451
|
* vendor test code that a consumer-supplied LIVD catalog did not map to a single
|
|
3452
|
-
* LOINC. It never carries a LOINC the catalog did not vouch for
|
|
3452
|
+
* LOINC. It never carries a LOINC the catalog did not vouch for: the whole point
|
|
3453
3453
|
* of the layer is that an unmapped or ambiguous code is surfaced as *unmapped*,
|
|
3454
3454
|
* **never a guessed LOINC** (mis-identifying a test is safety-critical). Every
|
|
3455
3455
|
* warning carries a stable code plus positional context ({@link AstmPosition}) and
|
|
@@ -3460,7 +3460,7 @@ declare function defineLivdCatalog(entries: readonly LivdEntry[]): LivdCatalog;
|
|
|
3460
3460
|
* profile safety gate's universe (`ALL_ASTM_WARNING_CODES`): a LIVD non-mapping is
|
|
3461
3461
|
* a post-parse advisory produced by the opt-in {@link applyLivd} helper, not a
|
|
3462
3462
|
* parse-time deviation a vendor profile could ever "tolerate". A profile referencing
|
|
3463
|
-
* one is rejected as an unknown code
|
|
3463
|
+
* one is rejected as an unknown code: correctly, since there is nothing here to
|
|
3464
3464
|
* quiet.
|
|
3465
3465
|
*/
|
|
3466
3466
|
|
|
@@ -3477,14 +3477,14 @@ declare function defineLivdCatalog(entries: readonly LivdEntry[]): LivdCatalog;
|
|
|
3477
3477
|
declare const LIVD_WARNING_CODES: {
|
|
3478
3478
|
/**
|
|
3479
3479
|
* A record carried a vendor/local test code, but the consumer-supplied LIVD catalog held **no**
|
|
3480
|
-
* entry for it. The code stays surfaced verbatim and the mapping is `unmapped
|
|
3480
|
+
* entry for it. The code stays surfaced verbatim and the mapping is `unmapped`: a LOINC is
|
|
3481
3481
|
* **never** guessed. Purely advisory: the raw code and value are untouched.
|
|
3482
3482
|
*/
|
|
3483
3483
|
readonly ASTM_LIVD_UNMAPPED_CODE: "ASTM_LIVD_UNMAPPED_CODE";
|
|
3484
3484
|
/**
|
|
3485
3485
|
* A vendor/local test code matched **more than one distinct LOINC** in the catalog (e.g. the same
|
|
3486
3486
|
* transmission code used by two devices for different analytes). The mapping is `ambiguous` and the
|
|
3487
|
-
* candidate LOINCs are surfaced for inspection, but **none is chosen
|
|
3487
|
+
* candidate LOINCs are surfaced for inspection, but **none is chosen**: refusing to pick is the
|
|
3488
3488
|
* fail-safe, since guessing wrong mis-identifies the test.
|
|
3489
3489
|
*/
|
|
3490
3490
|
readonly ASTM_LIVD_AMBIGUOUS_MAPPING: "ASTM_LIVD_AMBIGUOUS_MAPPING";
|
|
@@ -3503,7 +3503,7 @@ type LivdWarningCode = (typeof LIVD_WARNING_CODES)[keyof typeof LIVD_WARNING_COD
|
|
|
3503
3503
|
* import type { AstmLivdWarning } from "@cosyte/astm";
|
|
3504
3504
|
* const w: AstmLivdWarning = {
|
|
3505
3505
|
* code: "ASTM_LIVD_UNMAPPED_CODE",
|
|
3506
|
-
* message: "Reported test code had no LIVD mapping
|
|
3506
|
+
* message: "Reported test code had no LIVD mapping, surfaced unmapped, never a guessed LOINC.",
|
|
3507
3507
|
* position: { recordIndex: 3, recordType: "R" },
|
|
3508
3508
|
* };
|
|
3509
3509
|
* ```
|
|
@@ -3544,13 +3544,13 @@ declare function livdAmbiguousMapping(position: AstmPosition): AstmLivdWarning;
|
|
|
3544
3544
|
/**
|
|
3545
3545
|
* The **additive, advisory** LIVD annotation layer. {@link applyLivd}
|
|
3546
3546
|
* reads a parsed {@link AstmMessage} and a consumer-supplied {@link LivdCatalog}
|
|
3547
|
-
* and returns a **separate** layer of per-record LOINC annotations
|
|
3547
|
+
* and returns a **separate** layer of per-record LOINC annotations: it **never
|
|
3548
3548
|
* mutates, alters, or drops** the raw reported code or value. Recognition is an
|
|
3549
3549
|
* annotation, not a rewrite: the wire stays exactly as parsed.
|
|
3550
3550
|
*
|
|
3551
3551
|
* **The never-fabricate rule (the safety-critical point).** A vendor code with no
|
|
3552
3552
|
* single LIVD mapping surfaces as a typed `unmapped`/`ambiguous`/`no-code`
|
|
3553
|
-
* annotation
|
|
3553
|
+
* annotation: **never** a guessed LOINC. A wrong LOINC mis-identifies a test, so
|
|
3554
3554
|
* the layer only ever reports a LOINC the wire itself carried (an inline LOINC
|
|
3555
3555
|
* candidate) or one the consumer's catalog vouched for (labeled `derived`), and
|
|
3556
3556
|
* otherwise reports nothing but the miss.
|
|
@@ -3560,14 +3560,14 @@ declare function livdAmbiguousMapping(position: AstmPosition): AstmLivdWarning;
|
|
|
3560
3560
|
* The outcome of annotating one record's Universal Test ID against a LIVD catalog.
|
|
3561
3561
|
* A distinct case per disposition; there is no case in which a LOINC is guessed.
|
|
3562
3562
|
*
|
|
3563
|
-
* - `mapped
|
|
3563
|
+
* - `mapped`, the vendor/local code resolved to a single LOINC **via the catalog**
|
|
3564
3564
|
* (labeled `derived: true`, `source: "livd"`).
|
|
3565
|
-
* - `inline-loinc
|
|
3565
|
+
* - `inline-loinc`, the wire itself carried a LOINC in the Universal Test ID's slot
|
|
3566
3566
|
* (component 1); surfaced `source: "wire"`, **not** derived and **not** validated.
|
|
3567
|
-
* - `unmapped
|
|
3568
|
-
* - `ambiguous
|
|
3567
|
+
* - `unmapped`: a vendor/local code with no catalog entry.
|
|
3568
|
+
* - `ambiguous`: a vendor/local code matching more than one distinct LOINC; the
|
|
3569
3569
|
* candidates are surfaced but **none is chosen**.
|
|
3570
|
-
* - `no-code
|
|
3570
|
+
* - `no-code`: the record carried no usable test code at all (name-only/empty), so
|
|
3571
3571
|
* there was nothing to map.
|
|
3572
3572
|
*/
|
|
3573
3573
|
type LivdMapping = {
|
|
@@ -3589,7 +3589,7 @@ type LivdMapping = {
|
|
|
3589
3589
|
readonly status: "no-code";
|
|
3590
3590
|
};
|
|
3591
3591
|
/**
|
|
3592
|
-
* One record's LIVD annotation
|
|
3592
|
+
* One record's LIVD annotation, which record, the code that was looked up
|
|
3593
3593
|
* (verbatim), how it was recognized, and the mapping outcome. Additive: it points
|
|
3594
3594
|
* at the record by index and never replaces it.
|
|
3595
3595
|
*
|
|
@@ -3614,7 +3614,7 @@ interface LivdAnnotation {
|
|
|
3614
3614
|
readonly reportedCode?: string;
|
|
3615
3615
|
/** How the reported code was recognized in the Universal Test ID (provenance only, never a lookup). */
|
|
3616
3616
|
readonly provenance: UniversalTestIdProvenance;
|
|
3617
|
-
/** The mapping outcome
|
|
3617
|
+
/** The mapping outcome: never a guessed LOINC. */
|
|
3618
3618
|
readonly mapping: LivdMapping;
|
|
3619
3619
|
}
|
|
3620
3620
|
/**
|
|
@@ -3625,7 +3625,7 @@ interface LivdAnnotation {
|
|
|
3625
3625
|
interface LivdResult {
|
|
3626
3626
|
/** One annotation per `R`/`O` record, in wire order. */
|
|
3627
3627
|
readonly annotations: readonly LivdAnnotation[];
|
|
3628
|
-
/** A value-free warning per `unmapped`/`ambiguous` code
|
|
3628
|
+
/** A value-free warning per `unmapped`/`ambiguous` code: never per `mapped`/`inline-loinc`/`no-code`. */
|
|
3629
3629
|
readonly warnings: readonly AstmLivdWarning[];
|
|
3630
3630
|
}
|
|
3631
3631
|
/**
|
|
@@ -3701,7 +3701,7 @@ interface FrameChecksum {
|
|
|
3701
3701
|
}
|
|
3702
3702
|
/**
|
|
3703
3703
|
* One decoded ASTM frame. `text` is the frame's record-byte payload (the bytes
|
|
3704
|
-
* between the frame number and the terminator, escapes untouched
|
|
3704
|
+
* between the frame number and the terminator, escapes untouched, this is the
|
|
3705
3705
|
* framing layer, not the record layer). `trusted` is the single flag a consumer
|
|
3706
3706
|
* gates on: it is `true` only for a fully-terminated frame whose checksum
|
|
3707
3707
|
* validated, and such frames are the only ones reassembled into `records`.
|
|
@@ -3739,7 +3739,7 @@ interface AstmFrame {
|
|
|
3739
3739
|
*/
|
|
3740
3740
|
interface DecodeAstmFramesResult {
|
|
3741
3741
|
/**
|
|
3742
|
-
* The reassembled record byte-strings
|
|
3742
|
+
* The reassembled record byte-strings: one entry per complete record (a run of
|
|
3743
3743
|
* frames closed by an `ETX`), in wire order. **Only** clean reassemblies appear:
|
|
3744
3744
|
* a record whose frames included a bad checksum, a sequence gap, or an
|
|
3745
3745
|
* unterminated frame is **omitted** (its frames are still in {@link DecodeAstmFramesResult.frames},
|
|
@@ -3767,16 +3767,16 @@ interface FrameOptions {
|
|
|
3767
3767
|
* The value model for the pure LTP protocol reducer ({@link ltpReduce}).
|
|
3768
3768
|
*
|
|
3769
3769
|
* The reducer models the **receiver** side of an ASTM/CLSI-LIS01 (LIS01-A2)
|
|
3770
|
-
* session
|
|
3770
|
+
* session, the host/LIS receiving an upload, because that is where the one
|
|
3771
3771
|
* safety decision lives: whether to `ACK` (accept) or `NAK` (reject) a frame that
|
|
3772
3772
|
* just arrived. It is a **pure function** of the current {@link LtpState} and one
|
|
3773
3773
|
* inbound {@link LtpEvent}, returning an {@link LtpTransition} (the next state plus
|
|
3774
3774
|
* the actions the consumer should take and any warnings). No sockets, no timers,
|
|
3775
|
-
* no I/O
|
|
3775
|
+
* no I/O: the consumer owns the wire and the clock; the reducer owns the logic.
|
|
3776
3776
|
*/
|
|
3777
3777
|
|
|
3778
3778
|
/**
|
|
3779
|
-
* The protocol phase. The line is either **neutral** (idle
|
|
3779
|
+
* The protocol phase. The line is either **neutral** (idle, awaiting an `ENQ` to
|
|
3780
3780
|
* establish, or reset there after an `EOT`) or in **transfer** (establishment
|
|
3781
3781
|
* accepted; frames and their per-frame `ACK`/`NAK` flow until the sender's `EOT`).
|
|
3782
3782
|
* The classic three-phase LIS01 model (establishment → transfer → termination)
|
|
@@ -3814,7 +3814,7 @@ interface LtpState {
|
|
|
3814
3814
|
/** `true` when a record is mid-reassembly (an `ETB` frame was accepted, awaiting its `ETX`). */
|
|
3815
3815
|
readonly recordOpen: boolean;
|
|
3816
3816
|
/**
|
|
3817
|
-
* The bytes accumulated so far for the in-progress record
|
|
3817
|
+
* The bytes accumulated so far for the in-progress record: the concatenation of
|
|
3818
3818
|
* the `ETB` frames accepted since the last `ETX`. Empty when no record is open.
|
|
3819
3819
|
* These bytes are **never** delivered on their own: only an `ETX` completes a
|
|
3820
3820
|
* record, at which point they (plus the final frame's text) become one entry in
|
|
@@ -3826,7 +3826,7 @@ interface LtpState {
|
|
|
3826
3826
|
* An inbound protocol event the consumer feeds the reducer: one of the four LTP
|
|
3827
3827
|
* control signals (`ENQ`/`ACK`/`NAK`/`EOT`) read off the wire, or a `frame` the
|
|
3828
3828
|
* consumer already decoded with `decodeAstmFrames` (the reducer reuses the codec's
|
|
3829
|
-
* `trusted`/`checksum` verdict
|
|
3829
|
+
* `trusted`/`checksum` verdict, it never re-derives it).
|
|
3830
3830
|
*/
|
|
3831
3831
|
type LtpEvent = {
|
|
3832
3832
|
readonly type: "enq";
|
|
@@ -3873,14 +3873,14 @@ interface LtpTransition {
|
|
|
3873
3873
|
* The pure LTP protocol reducer: {@link ltpReduce} and {@link ltpInitialState}.
|
|
3874
3874
|
*
|
|
3875
3875
|
* A deterministic, socket-free receiver-side state machine for ASTM/CLSI-LIS01
|
|
3876
|
-
* (LIS01-A2). The consumer owns the wire
|
|
3877
|
-
* `decodeAstmFrames`, and drives this reducer one {@link LtpEvent} at a time
|
|
3876
|
+
* (LIS01-A2). The consumer owns the wire (it reads bytes, decodes frames with
|
|
3877
|
+
* `decodeAstmFrames`, and drives this reducer one {@link LtpEvent} at a time) and
|
|
3878
3878
|
* the reducer decides, purely, what happens next: whether to `ACK` or `NAK`, when a
|
|
3879
3879
|
* record is complete, and when the session returns to neutral.
|
|
3880
3880
|
*
|
|
3881
3881
|
* **The one inviolable rule (borrowed from `mllp`'s ACK-failsafe discipline):** a
|
|
3882
|
-
* frame the codec did not vouch for
|
|
3883
|
-
* out of sequence
|
|
3882
|
+
* frame the codec did not vouch for (a bad checksum, an unterminated frame, or one
|
|
3883
|
+
* out of sequence) is answered with `NAK`, **never** a fabricated positive `ACK`,
|
|
3884
3884
|
* and its bytes are **never** appended to a record or delivered. A `NAK` drives
|
|
3885
3885
|
* retransmit, not acceptance. This is the protocol-level twin of the frame codec's
|
|
3886
3886
|
* "a bad-checksum frame is never merged into a record."
|
|
@@ -3903,14 +3903,14 @@ interface LtpTransition {
|
|
|
3903
3903
|
*/
|
|
3904
3904
|
declare function ltpInitialState(): LtpState;
|
|
3905
3905
|
/**
|
|
3906
|
-
* Advance the LTP session by one inbound event
|
|
3906
|
+
* Advance the LTP session by one inbound event: a pure function, no I/O.
|
|
3907
3907
|
*
|
|
3908
3908
|
* Behaviour by phase:
|
|
3909
|
-
* - **neutral
|
|
3909
|
+
* - **neutral**: `enq` accepts establishment (`sendAck`, enter transfer); `eot` is
|
|
3910
3910
|
* a benign line reset (no-op); `ack`/`nak` are unexpected at a receiver (surfaced,
|
|
3911
3911
|
* never read as acceptance); a `frame` before establishment is tolerated
|
|
3912
|
-
* (Postel's Law)
|
|
3913
|
-
* - **transfer
|
|
3912
|
+
* (Postel's Law), the session auto-establishes and processes it, with a warning.
|
|
3913
|
+
* - **transfer**: a `frame` is accepted (`sendAck`, appended, sequence advanced) only
|
|
3914
3914
|
* when it is trusted *and* carries the expected number; a duplicate of the last
|
|
3915
3915
|
* accepted frame is idempotently re-`ACK`ed without re-appending; a bad or
|
|
3916
3916
|
* out-of-sequence frame is `NAK`ed and dropped. `eot` terminates the transfer and
|
|
@@ -3936,18 +3936,18 @@ declare function ltpReduce(state: LtpState, event: LtpEvent): LtpTransition;
|
|
|
3936
3936
|
* (was E1381), plus the bare-record-letter set the transport detector keys on.
|
|
3937
3937
|
*
|
|
3938
3938
|
* The frame codec (`../frames`) owns the *inside* of a frame (`STX FN text
|
|
3939
|
-
* ETB/ETX CS CRLF`); this module owns the *handshake around* frames
|
|
3939
|
+
* ETB/ETX CS CRLF`); this module owns the *handshake around* frames, the
|
|
3940
3940
|
* establishment/transfer/termination signalling that the {@link ltpReduce}
|
|
3941
3941
|
* reducer consumes. These are the raw byte values; the reducer and the transport
|
|
3942
3942
|
* detector are the only consumers.
|
|
3943
3943
|
*/
|
|
3944
|
-
/** Enquiry (`0x05`)
|
|
3944
|
+
/** Enquiry (`0x05`): the sender's request to establish a transfer. */
|
|
3945
3945
|
declare const ENQ = 5;
|
|
3946
|
-
/** Acknowledge (`0x06`)
|
|
3946
|
+
/** Acknowledge (`0x06`): the receiver accepted the last establishment or frame. */
|
|
3947
3947
|
declare const ACK = 6;
|
|
3948
|
-
/** Negative acknowledge (`0x15`)
|
|
3948
|
+
/** Negative acknowledge (`0x15`): the receiver rejected the last frame; retransmit, do not accept. */
|
|
3949
3949
|
declare const NAK = 21;
|
|
3950
|
-
/** End of transmission (`0x04`)
|
|
3950
|
+
/** End of transmission (`0x04`): the sender terminated the transfer; the line returns to neutral. */
|
|
3951
3951
|
declare const EOT = 4;
|
|
3952
3952
|
|
|
3953
3953
|
/**
|
|
@@ -3961,7 +3961,7 @@ declare const EOT = 4;
|
|
|
3961
3961
|
* **both** modes: a bad-checksum frame is surfaced flagged untrusted and **never**
|
|
3962
3962
|
* merged into a record, and a sequence gap is **never** silently bridged.
|
|
3963
3963
|
*
|
|
3964
|
-
* This is the framing layer only
|
|
3964
|
+
* This is the framing layer only: it does not parse record grammar. Hand a
|
|
3965
3965
|
* reassembled record to `parseAstmRecords`, or use `parseFramedAstm` to compose
|
|
3966
3966
|
* both layers at the edge.
|
|
3967
3967
|
*/
|
|
@@ -3982,7 +3982,7 @@ declare const EOT = 4;
|
|
|
3982
3982
|
* @example
|
|
3983
3983
|
* ```ts
|
|
3984
3984
|
* import { decodeAstmFrames } from "@cosyte/astm";
|
|
3985
|
-
* // One final frame carrying the record text "R|1|"
|
|
3985
|
+
* // One final frame carrying the record text "R|1|": checksum "AF" over FN..ETX (mod 256).
|
|
3986
3986
|
* const bytes = new Uint8Array([0x02, 0x31, 0x52, 0x7c, 0x31, 0x7c, 0x03, 0x41, 0x46, 0x0d, 0x0a]);
|
|
3987
3987
|
* const { records, frames } = decodeAstmFrames(bytes);
|
|
3988
3988
|
* frames[0]?.frameNumber; // 1
|
|
@@ -3995,7 +3995,7 @@ declare function decodeAstmFrames(bytes: Uint8Array, options?: FrameOptions): De
|
|
|
3995
3995
|
* The frame-layer **emit** side: {@link composeAstmFrames}.
|
|
3996
3996
|
*
|
|
3997
3997
|
* The exact inverse of {@link decodeAstmFrames}. Given the reassembled record
|
|
3998
|
-
* byte-strings (one entry per complete record
|
|
3998
|
+
* byte-strings (one entry per complete record, the same shape `decodeAstmFrames`
|
|
3999
3999
|
* *produces*), it wraps each into one or more `<STX> FN text <ETB|ETX> CS <CR><LF>`
|
|
4000
4000
|
* frames, so `decodeAstmFrames(composeAstmFrames(records)).records` reproduces the
|
|
4001
4001
|
* input records exactly.
|
|
@@ -4007,7 +4007,7 @@ declare function decodeAstmFrames(bytes: Uint8Array, options?: FrameOptions): De
|
|
|
4007
4007
|
* - the **frame number** `0`–`7`, starting at `1` and rolling over `7 → 0 → 1`,
|
|
4008
4008
|
* continuous across every frame in the stream (as the decoder's sequence check
|
|
4009
4009
|
* expects);
|
|
4010
|
-
* - the **240-byte text split
|
|
4010
|
+
* - the **240-byte text split**: a record longer than 240 bytes is split across
|
|
4011
4011
|
* frames closed by `ETB` (intermediate) with a final `ETX`; the seven framing
|
|
4012
4012
|
* control bytes are never counted toward the 240.
|
|
4013
4013
|
*
|
|
@@ -4016,7 +4016,7 @@ declare function decodeAstmFrames(bytes: Uint8Array, options?: FrameOptions): De
|
|
|
4016
4016
|
*/
|
|
4017
4017
|
/**
|
|
4018
4018
|
* Thrown by {@link composeAstmFrames} when the input cannot be framed into a
|
|
4019
|
-
* spec-clean stream
|
|
4019
|
+
* spec-clean stream: an empty record list, or a record with no bytes (a frame
|
|
4020
4020
|
* must carry a record; an empty one is a structural error, never an empty frame).
|
|
4021
4021
|
* Carries a stable code + the offending record index, never record bytes.
|
|
4022
4022
|
*
|
|
@@ -4042,12 +4042,12 @@ declare class AstmFrameEncodeError extends Error {
|
|
|
4042
4042
|
interface ComposeFramesOptions {
|
|
4043
4043
|
/**
|
|
4044
4044
|
* The frame number to start the sequence at (`0`–`7`). Defaults to
|
|
4045
|
-
* {@link FIRST_FRAME_NUMBER} (`1`)
|
|
4045
|
+
* {@link FIRST_FRAME_NUMBER} (`1`): the ASTM convention.
|
|
4046
4046
|
*/
|
|
4047
4047
|
readonly startFrameNumber?: number;
|
|
4048
4048
|
}
|
|
4049
4049
|
/**
|
|
4050
|
-
* Frame reassembled record bytes into a spec-clean ASTM/CLSI-LIS01 byte stream
|
|
4050
|
+
* Frame reassembled record bytes into a spec-clean ASTM/CLSI-LIS01 byte stream:
|
|
4051
4051
|
* the inverse of {@link decodeAstmFrames}.
|
|
4052
4052
|
*
|
|
4053
4053
|
* Each record is split at 240 text bytes into `ETB`-closed intermediate frames
|
|
@@ -4074,11 +4074,11 @@ declare function composeAstmFrames(records: readonly (Uint8Array | string)[], op
|
|
|
4074
4074
|
* Composing the two layers at the edge: {@link parseFramedAstm}.
|
|
4075
4075
|
*
|
|
4076
4076
|
* The framing layer ({@link decodeAstmFrames}) and the record layer
|
|
4077
|
-
* (`parseAstmRecords`) share nothing but the payload boundary
|
|
4077
|
+
* (`parseAstmRecords`) share nothing but the payload boundary: a frame carries
|
|
4078
4078
|
* record bytes; a record knows nothing about frames. This helper joins them at the
|
|
4079
4079
|
* one point they meet: decode a framed stream into trusted, reassembled record
|
|
4080
4080
|
* bytes, then parse those bytes into an {@link AstmMessage}. It is deliberately
|
|
4081
|
-
* thin
|
|
4081
|
+
* thin: a consumer that already holds de-framed record bytes calls
|
|
4082
4082
|
* `parseAstmRecords` directly, and a consumer that only wants frames calls
|
|
4083
4083
|
* `decodeAstmFrames` directly.
|
|
4084
4084
|
*/
|
|
@@ -4115,7 +4115,7 @@ interface FramedAstmResult {
|
|
|
4115
4115
|
* @example
|
|
4116
4116
|
* ```ts
|
|
4117
4117
|
* import { parseFramedAstm } from "@cosyte/astm";
|
|
4118
|
-
* // A single final frame carrying "H|\^&\r"
|
|
4118
|
+
* // A single final frame carrying "H|\^&\r": checksum "E5" over FN..ETX (mod 256).
|
|
4119
4119
|
* const bytes = new Uint8Array([
|
|
4120
4120
|
* 0x02, 0x31, 0x48, 0x7c, 0x5c, 0x5e, 0x26, 0x0d, 0x03, 0x45, 0x35, 0x0d, 0x0a,
|
|
4121
4121
|
* ]);
|
|
@@ -4127,7 +4127,7 @@ interface FramedAstmResult {
|
|
|
4127
4127
|
declare function parseFramedAstm(bytes: Uint8Array, options?: FrameOptions & AstmParseOptions): FramedAstmResult;
|
|
4128
4128
|
/**
|
|
4129
4129
|
* Serialize an ASTM message (or a bare record list) and frame it into a spec-clean
|
|
4130
|
-
* byte stream in one call
|
|
4130
|
+
* byte stream in one call: the inverse of {@link parseFramedAstm}, composing the
|
|
4131
4131
|
* two emit layers at the edge.
|
|
4132
4132
|
*
|
|
4133
4133
|
* Each record is serialized to spec-clean, `CR`-terminated wire text (canonical
|
|
@@ -4155,15 +4155,15 @@ declare function serializeFramedAstm(input: AstmMessage | readonly AstmRecord[],
|
|
|
4155
4155
|
* The ASTM/CLSI-LIS01 modulo-256 frame checksum.
|
|
4156
4156
|
*
|
|
4157
4157
|
* The checksum is the sum, modulo 256, of every byte **after** the `STX` up to
|
|
4158
|
-
* and **including** the `ETB`/`ETX` terminator
|
|
4158
|
+
* and **including** the `ETB`/`ETX` terminator: i.e. the frame number, the record
|
|
4159
4159
|
* text, and the terminator byte. It is carried on the wire as **two hexadecimal
|
|
4160
4160
|
* characters**. This module computes it (`computeChecksum`), formats it for emit
|
|
4161
|
-
* (`toChecksumHex
|
|
4162
|
-
* (`parseChecksumHex
|
|
4161
|
+
* (`toChecksumHex`, always **uppercase**), and reads it back leniently
|
|
4162
|
+
* (`parseChecksumHex`, **accepts lowercase**), matching the real-vendor quirk that
|
|
4163
4163
|
* some analyzers emit a lowercase checksum.
|
|
4164
4164
|
*/
|
|
4165
4165
|
/**
|
|
4166
|
-
* Sum bytes `[start, endInclusive]` of `bytes` modulo 256
|
|
4166
|
+
* Sum bytes `[start, endInclusive]` of `bytes` modulo 256: the ASTM frame
|
|
4167
4167
|
* checksum span (frame number through the terminator, inclusive).
|
|
4168
4168
|
*
|
|
4169
4169
|
* @param bytes - The full decoded byte stream.
|
|
@@ -4194,7 +4194,7 @@ declare function toChecksumHex(checksum: number): string;
|
|
|
4194
4194
|
/**
|
|
4195
4195
|
* Read the two-hex-char checksum at `bytes[i0]`/`bytes[i1]`, **case-insensitively**
|
|
4196
4196
|
* (a lowercase checksum is a tolerated real-vendor quirk). Returns `undefined` when
|
|
4197
|
-
* either position is out of range or not a hex digit
|
|
4197
|
+
* either position is out of range or not a hex digit: the caller then treats the
|
|
4198
4198
|
* frame as having no readable declared checksum, never as a match.
|
|
4199
4199
|
*
|
|
4200
4200
|
* @param bytes - The full decoded byte stream.
|
|
@@ -4211,7 +4211,7 @@ declare function toChecksumHex(checksum: number): string;
|
|
|
4211
4211
|
declare function parseChecksumHex(bytes: Uint8Array, i0: number, i1: number): number | undefined;
|
|
4212
4212
|
|
|
4213
4213
|
/**
|
|
4214
|
-
* The frame codec's thrown error
|
|
4214
|
+
* The frame codec's thrown error: the `strict`-mode counterpart to its lenient
|
|
4215
4215
|
* warnings.
|
|
4216
4216
|
*
|
|
4217
4217
|
* The frame layer is lenient by default (byte-level deviations become
|
|
@@ -4246,19 +4246,19 @@ declare class AstmFrameStrictError extends Error {
|
|
|
4246
4246
|
}
|
|
4247
4247
|
|
|
4248
4248
|
/**
|
|
4249
|
-
* Public entry point for the `@cosyte/astm` package
|
|
4249
|
+
* Public entry point for the `@cosyte/astm` package: the ASTM/CLSI-LIS02
|
|
4250
4250
|
* **record** layer.
|
|
4251
4251
|
*
|
|
4252
4252
|
* The north star: hand a de-framed ASTM record stream to {@link parseAstmRecords}
|
|
4253
|
-
* and pull a result's value, units, and flag out in one line
|
|
4254
|
-
* (vendor quirks become typed warnings, never silent loss)
|
|
4255
|
-
*
|
|
4253
|
+
* and pull a result's value, units, and flag out in one line. Parsing is lenient
|
|
4254
|
+
* (vendor quirks become typed warnings, never silent loss) and, the whole point,
|
|
4255
|
+
* **never returns a confident wrong value**. Delimiters are read from each header,
|
|
4256
4256
|
* embedded escapes are decoded before a value is split, the practice- and
|
|
4257
4257
|
* laboratory-assigned patient IDs stay distinct, and every deviation is a stable,
|
|
4258
4258
|
* value-free warning. Result flag/status semantics, patient/order identity
|
|
4259
4259
|
* depth + the `C` comment record + partial-timestamp hardening, and the
|
|
4260
4260
|
* request-information (`Q`) record + host-query classification + verbatim `M`/`S`
|
|
4261
|
-
* records are all modeled
|
|
4261
|
+
* records are all modeled: the **record-content layer is now feature-complete**.
|
|
4262
4262
|
*
|
|
4263
4263
|
* A parsed model is a whole record **stream**, and a stream may carry several `H` … `L`
|
|
4264
4264
|
* messages. {@link messages} splits it into them, so a patient and a result are only ever
|
|
@@ -4269,19 +4269,19 @@ declare class AstmFrameStrictError extends Error {
|
|
|
4269
4269
|
* The E1381/CLSI-LIS01 **framing** layer lives alongside the record layer and
|
|
4270
4270
|
* shares nothing but the payload boundary: {@link decodeAstmFrames} decodes a framed
|
|
4271
4271
|
* byte stream (`<STX> FN text <ETB|ETX> CS <CR><LF>`) into frames + reassembled
|
|
4272
|
-
* record bytes
|
|
4272
|
+
* record bytes: verifying the modulo-256 checksum (a bad frame is surfaced
|
|
4273
4273
|
* untrusted, never merged), tracking frame-number sequencing (a gap is never
|
|
4274
4274
|
* silently bridged), and reassembling the 240-byte-limited multi-frame records.
|
|
4275
4275
|
* {@link parseFramedAstm} composes the two layers at the edge.
|
|
4276
4276
|
*
|
|
4277
4277
|
* The LTP **protocol** layer sits above the frame codec: {@link detectFraming}
|
|
4278
|
-
* auto-detects the transport reality
|
|
4279
|
-
* (cobas
|
|
4278
|
+
* auto-detects the transport reality from the stream's leading byte, framed
|
|
4279
|
+
* (serial / cobas 4800 / Iguana) vs raw (cobas b121, framing dropped).
|
|
4280
4280
|
* {@link ltpReduce} is a **pure, socket-free** receiver-side state machine over
|
|
4281
4281
|
* `ENQ`/`ACK`/`NAK`/`EOT` + frame-received events. The consumer owns the wire; the
|
|
4282
4282
|
* reducer decides. Its inviolable rule mirrors `mllp`'s ACK-failsafe: a frame the
|
|
4283
4283
|
* codec did not vouch for is answered with `NAK`, **never** a fabricated positive
|
|
4284
|
-
* `ACK`, and never merged into a record
|
|
4284
|
+
* `ACK`, and never merged into a record, a `NAK` drives retransmit, not acceptance.
|
|
4285
4285
|
*
|
|
4286
4286
|
* The **emit** half is the conservative inverse of the record and framing layers.
|
|
4287
4287
|
* {@link buildAstmMessage} constructs records from typed caller input, emitting only
|