@cosyte/astm 0.0.3 → 0.0.4
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 +403 -0
- package/README.md +33 -1
- package/dist/index.cjs +251 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +547 -98
- package/dist/index.d.ts +547 -98
- package/dist/index.mjs +246 -19
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
package/dist/index.d.ts
CHANGED
|
@@ -107,6 +107,28 @@ declare const WARNING_CODES: {
|
|
|
107
107
|
* this warning flags the anomaly. Positional context only; no field value.
|
|
108
108
|
*/
|
|
109
109
|
readonly ASTM_RECORD_AMBIGUOUS_MESSAGE_KIND: "ASTM_RECORD_AMBIGUOUS_MESSAGE_KIND";
|
|
110
|
+
/**
|
|
111
|
+
* A later `H` (header) record declared a **different** delimiter set from the one in force, and the
|
|
112
|
+
* parser **followed it**: that header and every record after it are read with the newly-declared
|
|
113
|
+
* delimiters, until the next `H`. Records already read keep the set that was in force when they were
|
|
114
|
+
* read — a redeclaration never reinterprets bytes that have already been consumed.
|
|
115
|
+
*
|
|
116
|
+
* A stream carrying several messages back to back is ordinary, and a header that simply **repeats**
|
|
117
|
+
* the delimiters already in force is a no-op that warns nothing. This code fires only when the set
|
|
118
|
+
* actually changes, because that is the point at which a reader still using the old set would begin
|
|
119
|
+
* merging fields together.
|
|
120
|
+
*/
|
|
121
|
+
readonly ASTM_RECORD_DELIMITERS_REDECLARED: "ASTM_RECORD_DELIMITERS_REDECLARED";
|
|
122
|
+
/**
|
|
123
|
+
* A later `H` (header) record could not declare a usable delimiter set — it was too short, or the
|
|
124
|
+
* field separator it named also appeared among the other three, leaving the four roles
|
|
125
|
+
* indistinguishable. The delimiters **already in force are kept** and every record is still surfaced;
|
|
126
|
+
* a set is never guessed and no record is dropped.
|
|
127
|
+
*
|
|
128
|
+
* The same condition on the *first* header is unrecoverable and remains the
|
|
129
|
+
* `ASTM_RECORD_UNDECLARED_DELIMITERS` fatal — there is no earlier set to fall back to.
|
|
130
|
+
*/
|
|
131
|
+
readonly ASTM_RECORD_UNREADABLE_REDECLARATION: "ASTM_RECORD_UNREADABLE_REDECLARATION";
|
|
110
132
|
/**
|
|
111
133
|
* The downgraded form an active vendor {@link AstmProfile} produces from a deviation it *expects*
|
|
112
134
|
* (see `src/profiles/`). The original warning is **never dropped**: its code moves to
|
|
@@ -181,6 +203,31 @@ declare function unknownRecordType(position: AstmPosition): AstmRecordWarning;
|
|
|
181
203
|
* ```
|
|
182
204
|
*/
|
|
183
205
|
declare function nonStandardDelimiters(position: AstmPosition): AstmRecordWarning;
|
|
206
|
+
/**
|
|
207
|
+
* Build an `ASTM_RECORD_DELIMITERS_REDECLARED` warning. Emitted when a later `H`
|
|
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 — never the delimiters
|
|
210
|
+
* themselves.
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* ```ts
|
|
214
|
+
* import { delimitersRedeclared } from "@cosyte/astm";
|
|
215
|
+
* delimitersRedeclared({ recordIndex: 5, recordType: "H" });
|
|
216
|
+
* ```
|
|
217
|
+
*/
|
|
218
|
+
declare function delimitersRedeclared(position: AstmPosition): AstmRecordWarning;
|
|
219
|
+
/**
|
|
220
|
+
* Build an `ASTM_RECORD_UNREADABLE_REDECLARATION` warning. Emitted when a later
|
|
221
|
+
* `H` record cannot declare a usable delimiter set; the set already in force is
|
|
222
|
+
* kept and every record is still surfaced.
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* ```ts
|
|
226
|
+
* import { unreadableRedeclaration } from "@cosyte/astm";
|
|
227
|
+
* unreadableRedeclaration({ recordIndex: 2, recordType: "H" });
|
|
228
|
+
* ```
|
|
229
|
+
*/
|
|
230
|
+
declare function unreadableRedeclaration(position: AstmPosition): AstmRecordWarning;
|
|
184
231
|
/**
|
|
185
232
|
* Build an `ASTM_UNKNOWN_ESCAPE_SEQUENCE` warning. The sequence is preserved
|
|
186
233
|
* verbatim in the decoded value; the warning body carries neither the sequence
|
|
@@ -383,10 +430,18 @@ declare const CANONICAL_DELIMITERS: Delimiters;
|
|
|
383
430
|
* Read the four delimiters from a header record's raw text (a single `H`
|
|
384
431
|
* record, its terminator already stripped).
|
|
385
432
|
*
|
|
386
|
-
* Returns `
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
433
|
+
* Returns `undefined` when the record cannot declare all four delimiters — it is
|
|
434
|
+
* shorter than `H` + a field separator + a 3-char delimiter definition, or the
|
|
435
|
+
* field separator it names also appears among the other three.
|
|
436
|
+
*
|
|
437
|
+
* What the caller does with that depends on **which** header it is. On the first
|
|
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 — a stream may carry several
|
|
440
|
+
* messages, each declaring its own set — the delimiters already in force are kept
|
|
441
|
+
* and an `ASTM_RECORD_UNREADABLE_REDECLARATION` warning is raised instead; a set is
|
|
442
|
+
* never guessed and no record is dropped.
|
|
443
|
+
*
|
|
444
|
+
* This function does not throw: delimiter resolution and the escalation decision are
|
|
390
445
|
* kept separate so the reader stays pure and testable.
|
|
391
446
|
*
|
|
392
447
|
* @param headerRecord - The raw `H` record text (no trailing CR/LF).
|
|
@@ -870,8 +925,8 @@ type AstmFraming = "framed" | "raw";
|
|
|
870
925
|
interface DetectFramingOptions {
|
|
871
926
|
/**
|
|
872
927
|
* A profile-supplied override. When set, detection is bypassed entirely and this
|
|
873
|
-
* value is returned with no warning — the way a
|
|
874
|
-
*
|
|
928
|
+
* value is returned with no warning — the way a vendor profile forces raw for
|
|
929
|
+
* a cobas b121 or framed for a cobas 4800 regardless of the leading byte.
|
|
875
930
|
*/
|
|
876
931
|
readonly override?: AstmFraming;
|
|
877
932
|
}
|
|
@@ -1084,9 +1139,9 @@ interface DefineAstmProfileOptions {
|
|
|
1084
1139
|
}
|
|
1085
1140
|
|
|
1086
1141
|
/**
|
|
1087
|
-
* Safety-critical result semantics for the ASTM `R` (result) record
|
|
1142
|
+
* Safety-critical result semantics for the ASTM `R` (result) record.
|
|
1088
1143
|
*
|
|
1089
|
-
*
|
|
1144
|
+
* The parser surfaces the `R` record's flag, status, units, and reference-range
|
|
1090
1145
|
* fields **raw**. This module turns those raw letters into modeled, **fail-safe**
|
|
1091
1146
|
* semantics, under one rule: **never a confident wrong value.**
|
|
1092
1147
|
*
|
|
@@ -1244,7 +1299,7 @@ type ReferenceRangeKind = "closed" | "open-low" | "open-high" | "unparsed";
|
|
|
1244
1299
|
* **`[OSS-derived]` — the exact lower/upper delimiter (`-`) and the open-ended
|
|
1245
1300
|
* `<x`/`>x` forms are taken from the permissively-licensed OSS reference parsers
|
|
1246
1301
|
* and cross-verified vendor transcripts; they are not confirmed against the
|
|
1247
|
-
* purchased CLSI LIS02-A2
|
|
1302
|
+
* purchased CLSI LIS02-A2. Anything that does not match these
|
|
1248
1303
|
* forms is surfaced verbatim as `unparsed`, never guessed into a bound.**
|
|
1249
1304
|
*
|
|
1250
1305
|
* @example
|
|
@@ -1297,10 +1352,9 @@ declare function parseReferenceRange(raw: string): ReferenceRange;
|
|
|
1297
1352
|
* accessors relevant to its type — the raw tree is always present so nothing the
|
|
1298
1353
|
* parser did not model is lost.
|
|
1299
1354
|
*
|
|
1300
|
-
*
|
|
1301
|
-
* {@link UnsupportedRecord} (surfaced, never dropped).
|
|
1302
|
-
*
|
|
1303
|
-
* phases — this layer surfaces the safety-critical values **raw**.
|
|
1355
|
+
* `H`/`P`/`O`/`R`/`C`/`Q`/`M`/`S`/`L` are modeled; every other type letter is an
|
|
1356
|
+
* {@link UnsupportedRecord} (surfaced, never dropped). Alongside the modeled
|
|
1357
|
+
* views, this layer always surfaces the safety-critical values **raw**.
|
|
1304
1358
|
*/
|
|
1305
1359
|
|
|
1306
1360
|
/**
|
|
@@ -1333,14 +1387,25 @@ interface RecordBase {
|
|
|
1333
1387
|
*/
|
|
1334
1388
|
interface HeaderRecord extends RecordBase {
|
|
1335
1389
|
readonly type: "H";
|
|
1336
|
-
/**
|
|
1390
|
+
/**
|
|
1391
|
+
* The four delimiters **this** header put into force — the set its own fields, and every record
|
|
1392
|
+
* after it, were read with.
|
|
1393
|
+
*
|
|
1394
|
+
* A stream may carry several messages (`H` … `L`) and each header declares its own set, so on a
|
|
1395
|
+
* multi-header stream these can differ between headers; {@link AstmMessage.delimiters} is the
|
|
1396
|
+
* first header's. When a later header cannot declare a usable set, the set already in force is
|
|
1397
|
+
* kept and reported here (with an `ASTM_RECORD_UNREADABLE_REDECLARATION` warning) rather than a
|
|
1398
|
+
* guessed one.
|
|
1399
|
+
*/
|
|
1337
1400
|
readonly delimiters: Delimiters;
|
|
1338
1401
|
/**
|
|
1339
|
-
* The header's exact wire text (terminator excluded),
|
|
1340
|
-
*
|
|
1341
|
-
*
|
|
1342
|
-
*
|
|
1343
|
-
*
|
|
1402
|
+
* The header's exact wire text (terminator excluded), kept as **provenance**: the
|
|
1403
|
+
* delimiters are read from this raw text, never from {@link RecordBase.fields}.
|
|
1404
|
+
*
|
|
1405
|
+
* It is **not** what the serializer emits from. To change a header field, edit
|
|
1406
|
+
* {@link RecordBase.fields} — `fields[0]` is the type letter, `fields[1]` is the
|
|
1407
|
+
* delimiter declaration, and the header's ASTM data fields follow from `fields[2]`.
|
|
1408
|
+
* Editing this string instead has no effect on emit.
|
|
1344
1409
|
*/
|
|
1345
1410
|
readonly rawLine: string;
|
|
1346
1411
|
}
|
|
@@ -1417,7 +1482,7 @@ interface OrderRecord extends RecordBase {
|
|
|
1417
1482
|
*
|
|
1418
1483
|
* The raw safety-critical fields (value, `units`, `referenceRange`,
|
|
1419
1484
|
* `abnormalFlags`, `resultStatus`) are always surfaced exactly as received.
|
|
1420
|
-
*
|
|
1485
|
+
* The **modeled, fail-safe** semantics sit alongside them: `flag`
|
|
1421
1486
|
* (Table 0078, `undefined` never coerced to normal), `status` (a `C`/`X` never
|
|
1422
1487
|
* reads as active-final; an absent status is `unspecified`, never `final`), and
|
|
1423
1488
|
* `range` (open/closed bounds surfaced verbatim, never fabricated). The raw
|
|
@@ -1529,7 +1594,7 @@ interface CommentRecord extends RecordBase {
|
|
|
1529
1594
|
* fields — the starting/ending range ID and the request-information status — are
|
|
1530
1595
|
* surfaced **verbatim**; the field *positions* are the public ASTM E1394 layout,
|
|
1531
1596
|
* but their internal structure and code meanings are **`[OSS-derived / paywalled]`**
|
|
1532
|
-
*
|
|
1597
|
+
* and are therefore **never interpreted or guessed**.
|
|
1533
1598
|
*/
|
|
1534
1599
|
interface QueryRecord extends RecordBase {
|
|
1535
1600
|
/** Field 2 — sequence number. */
|
|
@@ -1559,7 +1624,7 @@ interface QueryRecord extends RecordBase {
|
|
|
1559
1624
|
readonly queriesAllTests: boolean;
|
|
1560
1625
|
/**
|
|
1561
1626
|
* Field 13 — request-information status code(s), surfaced **verbatim**. The status code *set* is
|
|
1562
|
-
* **`[OSS-derived / paywalled]
|
|
1627
|
+
* **`[OSS-derived / paywalled]`**: with no publicly-groundable enumeration, this
|
|
1563
1628
|
* parser recognizes **none** of them and interprets nothing — every present status is surfaced raw
|
|
1564
1629
|
* and flagged with a value-free `ASTM_RECORD_UNINTERPRETED_QUERY_STATUS` warning. Never mapped to a
|
|
1565
1630
|
* guessed meaning.
|
|
@@ -1578,7 +1643,16 @@ interface QueryRecord extends RecordBase {
|
|
|
1578
1643
|
*/
|
|
1579
1644
|
interface ManufacturerRecord extends RecordBase {
|
|
1580
1645
|
readonly type: "M";
|
|
1581
|
-
/**
|
|
1646
|
+
/**
|
|
1647
|
+
* The record's exact wire text (terminator excluded), preserved byte-for-byte.
|
|
1648
|
+
*
|
|
1649
|
+
* Emit reproduces these bytes exactly whenever a reader using the delimiters being
|
|
1650
|
+
* emitted against would recover the fields this record models — always the case when
|
|
1651
|
+
* the record is already in those delimiters, and also when it carries no delimiter
|
|
1652
|
+
* either set would split on. Otherwise the record is re-encoded from
|
|
1653
|
+
* {@link RecordBase.fields}, so the row can never go out in delimiters the header does
|
|
1654
|
+
* not declare. To change a value, edit `fields`; editing this string has no effect on emit.
|
|
1655
|
+
*/
|
|
1582
1656
|
readonly rawLine: string;
|
|
1583
1657
|
}
|
|
1584
1658
|
/**
|
|
@@ -1588,7 +1662,16 @@ interface ManufacturerRecord extends RecordBase {
|
|
|
1588
1662
|
*/
|
|
1589
1663
|
interface ScientificRecord extends RecordBase {
|
|
1590
1664
|
readonly type: "S";
|
|
1591
|
-
/**
|
|
1665
|
+
/**
|
|
1666
|
+
* The record's exact wire text (terminator excluded), preserved byte-for-byte.
|
|
1667
|
+
*
|
|
1668
|
+
* Emit reproduces these bytes exactly whenever a reader using the delimiters being
|
|
1669
|
+
* emitted against would recover the fields this record models — always the case when
|
|
1670
|
+
* the record is already in those delimiters, and also when it carries no delimiter
|
|
1671
|
+
* either set would split on. Otherwise the record is re-encoded from
|
|
1672
|
+
* {@link RecordBase.fields}, so the row can never go out in delimiters the header does
|
|
1673
|
+
* not declare. To change a value, edit `fields`; editing this string has no effect on emit.
|
|
1674
|
+
*/
|
|
1592
1675
|
readonly rawLine: string;
|
|
1593
1676
|
}
|
|
1594
1677
|
/**
|
|
@@ -1599,8 +1682,8 @@ interface TerminatorRecord extends RecordBase {
|
|
|
1599
1682
|
}
|
|
1600
1683
|
/**
|
|
1601
1684
|
* Any record whose type letter is not modeled (a genuinely unknown letter).
|
|
1602
|
-
* `H`/`P`/`O`/`R`/`C`/`
|
|
1603
|
-
*
|
|
1685
|
+
* `H`/`P`/`O`/`R`/`C`/`Q`/`M`/`S`/`L` are all modeled; anything else is surfaced
|
|
1686
|
+
* with its raw fields intact and flagged with an
|
|
1604
1687
|
* `ASTM_RECORD_UNKNOWN_TYPE` warning — never dropped.
|
|
1605
1688
|
*/
|
|
1606
1689
|
interface UnsupportedRecord extends RecordBase {
|
|
@@ -1667,8 +1750,17 @@ interface AstmMessageClassification {
|
|
|
1667
1750
|
* ```
|
|
1668
1751
|
*/
|
|
1669
1752
|
interface AstmMessage {
|
|
1753
|
+
/** The **first** header in the stream. A multi-header stream's later headers are in {@link AstmMessage.records}. */
|
|
1670
1754
|
readonly header: HeaderRecord;
|
|
1671
1755
|
readonly records: readonly AstmRecord[];
|
|
1756
|
+
/**
|
|
1757
|
+
* The delimiters the **first** header declared.
|
|
1758
|
+
*
|
|
1759
|
+
* A stream may carry several messages (`H` … `L`), and a later header may declare a different set,
|
|
1760
|
+
* which is honored from that header onward. So this is not necessarily the set every record was
|
|
1761
|
+
* read with — each header reports its own on {@link HeaderRecord.delimiters}, and a change is
|
|
1762
|
+
* flagged `ASTM_RECORD_DELIMITERS_REDECLARED`.
|
|
1763
|
+
*/
|
|
1672
1764
|
readonly delimiters: Delimiters;
|
|
1673
1765
|
/**
|
|
1674
1766
|
* The host-query classification of this message — whether it is a request (`Q`
|
|
@@ -1714,11 +1806,11 @@ interface AstmParseOptions {
|
|
|
1714
1806
|
/**
|
|
1715
1807
|
* The record-layer entry point: {@link parseAstmRecords}.
|
|
1716
1808
|
*
|
|
1717
|
-
*
|
|
1718
|
-
* layer
|
|
1719
|
-
* every record escape-aware, and builds the immutable
|
|
1720
|
-
* by default: vendor quirks become typed warnings, and
|
|
1721
|
-
* structural conditions are fatal.
|
|
1809
|
+
* It assumes **already-de-framed** record bytes; {@link parseFramedAstm} composes
|
|
1810
|
+
* the E1381/LIS01 framing layer with this one. It reads the four delimiters from each
|
|
1811
|
+
* `H` record, tokenizes every record escape-aware, and builds the immutable
|
|
1812
|
+
* {@link AstmMessage} — lenient by default: vendor quirks become typed warnings, and
|
|
1813
|
+
* only three unrecoverable structural conditions are fatal.
|
|
1722
1814
|
*/
|
|
1723
1815
|
|
|
1724
1816
|
/**
|
|
@@ -1746,8 +1838,18 @@ declare class AstmStrictError extends Error {
|
|
|
1746
1838
|
*
|
|
1747
1839
|
* The stream is a sequence of records separated by `CR` (with `LF`/`CRLF`
|
|
1748
1840
|
* tolerated); the first record must be an `H` header, which declares the
|
|
1749
|
-
* delimiters
|
|
1750
|
-
*
|
|
1841
|
+
* delimiters. Lenient by default — set `strict` to reject any tolerated deviation.
|
|
1842
|
+
*
|
|
1843
|
+
* **Several messages in one stream.** A message runs `H` … `L`, so a stream may carry
|
|
1844
|
+
* more than one, and each `H` declares the delimiters for the records that follow it.
|
|
1845
|
+
* A later header that declares a **different** set is honored from that header onward
|
|
1846
|
+
* and flagged `ASTM_RECORD_DELIMITERS_REDECLARED`; records already read keep the set
|
|
1847
|
+
* they were read with, so a redeclaration never reinterprets earlier bytes. A header
|
|
1848
|
+
* that merely restates the set in force is a no-op and warns nothing. A later header
|
|
1849
|
+
* that cannot declare a usable set keeps the delimiters already in force and warns
|
|
1850
|
+
* `ASTM_RECORD_UNREADABLE_REDECLARATION` — a set is never guessed, and no record is
|
|
1851
|
+
* dropped. Per-header sets are on each {@link HeaderRecord.delimiters};
|
|
1852
|
+
* {@link AstmMessage.delimiters} is the first header's.
|
|
1751
1853
|
*
|
|
1752
1854
|
* @param raw - The de-framed record bytes, as a string or `Uint8Array` (decoded
|
|
1753
1855
|
* latin1 so byte values survive 1:1).
|
|
@@ -1789,28 +1891,58 @@ declare function parseAstmRecords(raw: string | Uint8Array, options?: AstmParseO
|
|
|
1789
1891
|
declare function attachComments(records: readonly AstmRecord[], warnings: AstmRecordWarning[]): AstmRecord[];
|
|
1790
1892
|
|
|
1791
1893
|
/**
|
|
1792
|
-
* The record-layer **emit** side: {@link serializeAstmRecords}
|
|
1894
|
+
* The record-layer **emit** side: {@link serializeAstmRecords}.
|
|
1793
1895
|
*
|
|
1794
1896
|
* The inverse of `parseAstmRecords`. Where the parser is liberal (vendor quirks
|
|
1795
1897
|
* become warnings), the serializer is **conservative**: it always emits a
|
|
1796
1898
|
* spec-clean stream with the **canonical** delimiter set (`H|\^&`), re-escapes
|
|
1797
1899
|
* every embedded delimiter so an embedded `|`/`^`/`\`/`&` in a value can never
|
|
1798
|
-
* break framing (the exact inverse of the
|
|
1900
|
+
* break framing (the exact inverse of the escape codec), and terminates
|
|
1799
1901
|
* each record with a `CR`.
|
|
1800
1902
|
*
|
|
1801
|
-
* **Round-trip
|
|
1802
|
-
*
|
|
1803
|
-
*
|
|
1804
|
-
*
|
|
1805
|
-
*
|
|
1806
|
-
*
|
|
1807
|
-
*
|
|
1903
|
+
* **Round-trip.** `parseAstmRecords(serializeAstmRecords(msg))` reproduces the modeled
|
|
1904
|
+
* records — the same components, the same typed accessors, and the canonical delimiter
|
|
1905
|
+
* set (a non-canonical source is normalized to `H|\^&` by default). Normalizing sets the
|
|
1906
|
+
* four delimiter **roles**; it does not delete a declaration's surplus characters, so a
|
|
1907
|
+
* canonically-declared header that arrived carrying surplus keeps it on the default path
|
|
1908
|
+
* too.
|
|
1909
|
+
*
|
|
1910
|
+
* **One delimiter set for the whole stream.** Every record — including `H`, `M` and `S` —
|
|
1911
|
+
* is emitted against the delimiters being emitted with, and the header declares that same
|
|
1912
|
+
* set. `M` and `S` carry vendor free-form data and are reproduced **byte-for-byte** from
|
|
1913
|
+
* their preserved `rawLine` whenever a reader using those delimiters would recover exactly
|
|
1914
|
+
* the fields they model, which is the case for any stream already in the emit delimiter
|
|
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
|
+
* survive as fields rather than collapsing into one on the next read.
|
|
1918
|
+
*
|
|
1919
|
+
* **Emit follows the model.** Every record type, `H` included, is emitted from its decoded
|
|
1920
|
+
* `AstmField.repeats` tree, so an edit to a record's `fields` is reflected on emit. The one
|
|
1921
|
+
* part of a record that is never taken from the model is the header's delimiter
|
|
1922
|
+
* declaration, and only in the three positions that carry a role: those always state the
|
|
1923
|
+
* repeat, component and escape characters actually in use, whatever the model says. Any
|
|
1924
|
+
* further characters the declaration carried — a reader assigns them no role — come from
|
|
1925
|
+
* the model like everything else, so a header that arrived as `H|\^&#` goes back out as
|
|
1926
|
+
* `H|\^&#` rather than losing the `#`. That holds on the **default** canonical path as
|
|
1927
|
+
* well as when a set is passed explicitly: the surplus of a canonically-declared header
|
|
1928
|
+
* survives normalization, because normalization is about the four roles and the surplus
|
|
1929
|
+
* has none.
|
|
1808
1930
|
*
|
|
1809
1931
|
* **Never break framing.** A component leaf that contains a record terminator
|
|
1810
1932
|
* (`CR`/`LF`) cannot be escaped by the ASTM escape codec (only the four declared
|
|
1811
1933
|
* delimiters have mnemonics), so emitting it would silently corrupt the wire.
|
|
1812
1934
|
* The serializer refuses: an embedded `CR`/`LF` is a typed {@link AstmSerializeError},
|
|
1813
1935
|
* never emitted raw.
|
|
1936
|
+
*
|
|
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 — each separator exactly
|
|
1939
|
+
* one character, no separator a `CR`/`LF`, no two separators the same character — and a
|
|
1940
|
+
* set failing any of them is a typed {@link AstmSerializeError}
|
|
1941
|
+
* (`ASTM_EMIT_INVALID_DELIMITERS`) rather than output this library's own parser would
|
|
1942
|
+
* reject or silently re-read with a different field tree. These are **necessary**
|
|
1943
|
+
* conditions, not a guarantee of readback: a set can pass all three and still produce a
|
|
1944
|
+
* stream that reads back wrong — a separator that collides with a record's type letter
|
|
1945
|
+
* is the known case, and it corrupts identically with or without this check.
|
|
1814
1946
|
*/
|
|
1815
1947
|
|
|
1816
1948
|
/**
|
|
@@ -1832,13 +1964,34 @@ declare function attachComments(records: readonly AstmRecord[], warnings: AstmRe
|
|
|
1832
1964
|
* ```
|
|
1833
1965
|
*/
|
|
1834
1966
|
declare class AstmSerializeError extends Error {
|
|
1835
|
-
/**
|
|
1836
|
-
|
|
1967
|
+
/**
|
|
1968
|
+
* Stable discriminant. `ASTM_EMIT_UNENCODABLE_VALUE` for a `CR`/`LF` in a value;
|
|
1969
|
+
* `ASTM_EMIT_INVALID_DELIMITERS` for a delimiter set that cannot be emitted
|
|
1970
|
+
* reversibly.
|
|
1971
|
+
*/
|
|
1972
|
+
readonly code: AstmSerializeErrorCode;
|
|
1837
1973
|
/** 0-based ordinal of the record within the message, when known. */
|
|
1838
1974
|
readonly recordIndex?: number;
|
|
1839
1975
|
/** @internal */
|
|
1840
|
-
constructor(message: string, recordIndex?: number);
|
|
1976
|
+
constructor(message: string, recordIndex?: number, code?: AstmSerializeErrorCode);
|
|
1841
1977
|
}
|
|
1978
|
+
/**
|
|
1979
|
+
* The reasons emit refuses rather than writing a stream that cannot be read back.
|
|
1980
|
+
*
|
|
1981
|
+
* - `ASTM_EMIT_UNENCODABLE_VALUE` — a component holds a record terminator
|
|
1982
|
+
* (`CR`/`LF`), which the escape codec has no mnemonic for.
|
|
1983
|
+
* - `ASTM_EMIT_INVALID_DELIMITERS` — the delimiter set to emit against failed one
|
|
1984
|
+
* of the three conditions readback requires (see {@link serializeAstmRecords}).
|
|
1985
|
+
* Those conditions are necessary rather than sufficient, so this code means a
|
|
1986
|
+
* set was rejected, not that every unreversible set is.
|
|
1987
|
+
*
|
|
1988
|
+
* @example
|
|
1989
|
+
* ```ts
|
|
1990
|
+
* import type { AstmSerializeErrorCode } from "@cosyte/astm";
|
|
1991
|
+
* const code: AstmSerializeErrorCode = "ASTM_EMIT_INVALID_DELIMITERS";
|
|
1992
|
+
* ```
|
|
1993
|
+
*/
|
|
1994
|
+
type AstmSerializeErrorCode = "ASTM_EMIT_UNENCODABLE_VALUE" | "ASTM_EMIT_INVALID_DELIMITERS";
|
|
1842
1995
|
/**
|
|
1843
1996
|
* Escape-encode one component leaf for spec-clean emit — the inverse of
|
|
1844
1997
|
* `decodeEscapes`. The **escape character itself is encoded first** (`&` → `&E&`)
|
|
@@ -1847,12 +2000,17 @@ declare class AstmSerializeError extends Error {
|
|
|
1847
2000
|
* mnemonics.
|
|
1848
2001
|
*
|
|
1849
2002
|
* A `CR`/`LF` in the leaf has no escape mnemonic and would break framing, so it
|
|
1850
|
-
* is rejected with an {@link AstmSerializeError} rather than emitted raw
|
|
2003
|
+
* is rejected with an {@link AstmSerializeError} rather than emitted raw, as is a
|
|
2004
|
+
* delimiter set the result could not be read back with.
|
|
1851
2005
|
*
|
|
1852
2006
|
* @param leaf - One already-decoded component string.
|
|
1853
2007
|
* @param d - The delimiters to emit against (canonical for spec-clean output).
|
|
1854
2008
|
* @param recordIndex - The enclosing record's index, for error context.
|
|
1855
2009
|
* @returns The escaped component text.
|
|
2010
|
+
* @throws {@link AstmSerializeError} for a `CR`/`LF` in the leaf
|
|
2011
|
+
* (`ASTM_EMIT_UNENCODABLE_VALUE`), or for a delimiter set failing one of the
|
|
2012
|
+
* three conditions readback requires (`ASTM_EMIT_INVALID_DELIMITERS`) — which
|
|
2013
|
+
* are necessary, not sufficient, so a set can pass them and still not reverse.
|
|
1856
2014
|
* @example
|
|
1857
2015
|
* ```ts
|
|
1858
2016
|
* import { encodeComponent, CANONICAL_DELIMITERS } from "@cosyte/astm";
|
|
@@ -1865,15 +2023,20 @@ declare function encodeComponent(leaf: string, d: Delimiters, recordIndex?: numb
|
|
|
1865
2023
|
* terminator). Emits with the given delimiters, defaulting to the canonical set.
|
|
1866
2024
|
*
|
|
1867
2025
|
* The header (`H`) is special-cased: its delimiter-definition field is emitted as
|
|
1868
|
-
* the **literal**
|
|
1869
|
-
*
|
|
1870
|
-
*
|
|
1871
|
-
* `
|
|
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
|
+
* declaration carried beyond the three a reader takes its roles from. Manufacturer
|
|
2029
|
+
* (`M`) and scientific (`S`) records are reproduced **byte-identically** from their
|
|
2030
|
+
* preserved `rawLine` when they are already in `d`, and re-encoded from their fields
|
|
2031
|
+
* when they are not, so their fields never collapse into one on the next read.
|
|
1872
2032
|
*
|
|
1873
2033
|
* @param record - The record to serialize.
|
|
1874
2034
|
* @param d - The delimiters to emit against; defaults to `H|\^&`.
|
|
1875
2035
|
* @returns The record's wire text, terminator excluded.
|
|
1876
|
-
* @throws {@link AstmSerializeError} when a component contains an unencodable `CR`/`LF
|
|
2036
|
+
* @throws {@link AstmSerializeError} when a component contains an unencodable `CR`/`LF`
|
|
2037
|
+
* (`ASTM_EMIT_UNENCODABLE_VALUE`), or when `d` fails one of the three conditions
|
|
2038
|
+
* readback requires (`ASTM_EMIT_INVALID_DELIMITERS`) — necessary conditions, not
|
|
2039
|
+
* sufficient ones, so a set can pass them and still not reverse.
|
|
1877
2040
|
* @example
|
|
1878
2041
|
* ```ts
|
|
1879
2042
|
* import { serializeAstmRecord, parseAstmRecords } from "@cosyte/astm";
|
|
@@ -1888,12 +2051,32 @@ declare function serializeAstmRecord(record: AstmRecord, d?: Delimiters): string
|
|
|
1888
2051
|
*
|
|
1889
2052
|
* Emit is **conservative**: the canonical `H|\^&` delimiters, every embedded
|
|
1890
2053
|
* delimiter re-escaped, each record closed with a `CR`. A message parsed with
|
|
1891
|
-
* non-canonical delimiters is **normalized** to the canonical set on emit
|
|
1892
|
-
*
|
|
2054
|
+
* non-canonical delimiters is **normalized** to the canonical set on emit — every
|
|
2055
|
+
* record, `M` and `S` included, so the emitted stream is in one delimiter set and
|
|
2056
|
+
* re-parsing it recovers every field. Passing `d` explicitly emits against that set
|
|
2057
|
+
* instead, and the header declares it. Normalization replaces the four delimiter
|
|
2058
|
+
* **roles**; a header declaration carrying characters beyond the three that hold a
|
|
2059
|
+
* role keeps them, on this path as on any other, rather than being truncated.
|
|
2060
|
+
*
|
|
2061
|
+
* **`d` is checked before anything is written.** Each of the four separators must be
|
|
2062
|
+
* exactly one character, none may be a record terminator, and no two may share a
|
|
2063
|
+
* character — otherwise the emitted bytes cannot be read back as the records that
|
|
2064
|
+
* produced them, and emit has no warning channel with which to say so. A set that
|
|
2065
|
+
* fails is an {@link AstmSerializeError} with code `ASTM_EMIT_INVALID_DELIMITERS`.
|
|
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 —
|
|
2068
|
+
* in exactly the cases where it used to emit a stream that read back wrong. The
|
|
2069
|
+
* three conditions are necessary rather than sufficient: a set that passes them can
|
|
2070
|
+
* still read back wrong if a separator collides with a record's type letter, which
|
|
2071
|
+
* this check does not claim to catch.
|
|
1893
2072
|
*
|
|
1894
2073
|
* @param input - A parsed {@link AstmMessage} or a list of {@link AstmRecord}s.
|
|
2074
|
+
* @param d - The delimiters to emit against; defaults to the canonical `H|\^&` set.
|
|
1895
2075
|
* @returns The serialized record stream (`CR` after every record).
|
|
1896
|
-
* @throws {@link AstmSerializeError} when a component contains an unencodable `CR`/`LF
|
|
2076
|
+
* @throws {@link AstmSerializeError} when a component contains an unencodable `CR`/`LF`
|
|
2077
|
+
* (`ASTM_EMIT_UNENCODABLE_VALUE`), or when `d` fails one of the three conditions
|
|
2078
|
+
* readback requires (`ASTM_EMIT_INVALID_DELIMITERS`) — necessary conditions, not
|
|
2079
|
+
* sufficient ones, so a set can pass them and still not reverse.
|
|
1897
2080
|
* @example
|
|
1898
2081
|
* ```ts
|
|
1899
2082
|
* import { parseAstmRecords, serializeAstmRecords } from "@cosyte/astm";
|
|
@@ -1910,6 +2093,10 @@ declare function serializeAstmRecords(input: AstmMessage | readonly AstmRecord[]
|
|
|
1910
2093
|
* @param field - The field to serialize.
|
|
1911
2094
|
* @param d - The delimiters to emit against; defaults to `H|\^&`.
|
|
1912
2095
|
* @returns The escaped field text.
|
|
2096
|
+
* @throws {@link AstmSerializeError} when a component contains an unencodable `CR`/`LF`
|
|
2097
|
+
* (`ASTM_EMIT_UNENCODABLE_VALUE`), or when `d` fails one of the three conditions
|
|
2098
|
+
* readback requires (`ASTM_EMIT_INVALID_DELIMITERS`) — necessary conditions, not
|
|
2099
|
+
* sufficient ones, so a set can pass them and still not reverse.
|
|
1913
2100
|
* @example
|
|
1914
2101
|
* ```ts
|
|
1915
2102
|
* import { serializeField, tokenizeRecord, CANONICAL_DELIMITERS } from "@cosyte/astm";
|
|
@@ -1920,7 +2107,7 @@ declare function serializeAstmRecords(input: AstmMessage | readonly AstmRecord[]
|
|
|
1920
2107
|
declare function serializeField(field: AstmField, d?: Delimiters): string;
|
|
1921
2108
|
|
|
1922
2109
|
/**
|
|
1923
|
-
* The record-layer **builder**: {@link buildAstmMessage}
|
|
2110
|
+
* The record-layer **builder**: {@link buildAstmMessage}.
|
|
1924
2111
|
*
|
|
1925
2112
|
* A builder produces spec-clean E1394 records **by construction** from typed
|
|
1926
2113
|
* caller input, under two disciplines:
|
|
@@ -2094,17 +2281,29 @@ declare function buildAstmMessage(input: MessageInput): string;
|
|
|
2094
2281
|
* Typed extractors over an {@link AstmMessage} — the one-line "pull the value out"
|
|
2095
2282
|
* surface that is the package's north star.
|
|
2096
2283
|
*
|
|
2097
|
-
* They are thin
|
|
2098
|
-
* never
|
|
2099
|
-
*
|
|
2100
|
-
*
|
|
2284
|
+
* They are thin and immutable: they read the already-parsed model and never re-parse,
|
|
2285
|
+
* never mutate, and never fabricate.
|
|
2286
|
+
*
|
|
2287
|
+
* **They are scoped to a single message, and they enforce it.** An `AstmMessage` models a
|
|
2288
|
+
* whole record **stream**, which may carry several `H` … `L` messages back to back. These
|
|
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 —
|
|
2291
|
+
* pairing the two attributed one patient's results to another, silently and with no warning.
|
|
2292
|
+
* They now **refuse** that stream: an {@link AstmAmbiguousStreamError} instead of a confident
|
|
2293
|
+
* wrong answer, which is the same rule the rest of this parser follows. {@link messages} is
|
|
2294
|
+
* the path that works on any stream, and `commentsFor()` keeps working on every stream
|
|
2295
|
+
* because its parent record already names the message.
|
|
2296
|
+
*
|
|
2297
|
+
* @see {@link messages}
|
|
2101
2298
|
*/
|
|
2102
2299
|
|
|
2103
2300
|
/**
|
|
2104
2301
|
* Every result (`R`) record in the message, in wire order.
|
|
2105
2302
|
*
|
|
2106
|
-
* @param msg - A parsed message.
|
|
2303
|
+
* @param msg - A parsed **single-message** stream.
|
|
2107
2304
|
* @returns The result records (possibly empty).
|
|
2305
|
+
* @throws {@link AstmAmbiguousStreamError} (`ASTM_AMBIGUOUS_MULTI_MESSAGE`) when the stream
|
|
2306
|
+
* carries more than one message — see {@link messages}.
|
|
2108
2307
|
* @example
|
|
2109
2308
|
* ```ts
|
|
2110
2309
|
* import { parseAstmRecords, results } from "@cosyte/astm";
|
|
@@ -2114,13 +2313,18 @@ declare function buildAstmMessage(input: MessageInput): string;
|
|
|
2114
2313
|
*/
|
|
2115
2314
|
declare function results(msg: AstmMessage): readonly ResultRecord[];
|
|
2116
2315
|
/**
|
|
2117
|
-
* The
|
|
2316
|
+
* The message's patient (`P`) record, or `undefined` when the message carries none.
|
|
2118
2317
|
*
|
|
2119
|
-
*
|
|
2120
|
-
*
|
|
2318
|
+
* This is the identity a result files against, so it is answered only when the stream
|
|
2319
|
+
* determines exactly one. It **refuses** the two shapes where "the patient" is a guess: a
|
|
2320
|
+
* stream carrying several messages, and a single message carrying several `P` records. In
|
|
2321
|
+
* both, the old behaviour — the first `P` in the stream — is the wrong-patient path itself.
|
|
2121
2322
|
*
|
|
2122
|
-
* @param msg - A parsed message.
|
|
2123
|
-
* @returns The
|
|
2323
|
+
* @param msg - A parsed single-message stream carrying at most one patient.
|
|
2324
|
+
* @returns The patient record, or `undefined` when the message carries none.
|
|
2325
|
+
* @throws {@link AstmAmbiguousStreamError} (`ASTM_AMBIGUOUS_MULTI_MESSAGE`) when the stream
|
|
2326
|
+
* carries more than one message, or (`ASTM_AMBIGUOUS_MULTI_PATIENT`) when its one message
|
|
2327
|
+
* carries more than one `P`. Use {@link messages} and read `patients` in both cases.
|
|
2124
2328
|
* @example
|
|
2125
2329
|
* ```ts
|
|
2126
2330
|
* import { parseAstmRecords, patient } from "@cosyte/astm";
|
|
@@ -2132,8 +2336,10 @@ declare function patient(msg: AstmMessage): PatientRecord | undefined;
|
|
|
2132
2336
|
/**
|
|
2133
2337
|
* Every order (`O`) record in the message, in wire order.
|
|
2134
2338
|
*
|
|
2135
|
-
* @param msg - A parsed message.
|
|
2339
|
+
* @param msg - A parsed **single-message** stream.
|
|
2136
2340
|
* @returns The order records (possibly empty).
|
|
2341
|
+
* @throws {@link AstmAmbiguousStreamError} (`ASTM_AMBIGUOUS_MULTI_MESSAGE`) when the stream
|
|
2342
|
+
* carries more than one message — see {@link messages}.
|
|
2137
2343
|
* @example
|
|
2138
2344
|
* ```ts
|
|
2139
2345
|
* import { parseAstmRecords, orders } from "@cosyte/astm";
|
|
@@ -2147,8 +2353,10 @@ declare function orders(msg: AstmMessage): readonly OrderRecord[];
|
|
|
2147
2353
|
* `parentIndex` of the `H`/`P`/`O`/`R` it attaches to (or `attachedToRoot` when
|
|
2148
2354
|
* it is an orphan) — use {@link commentsFor} to get the comments of one record.
|
|
2149
2355
|
*
|
|
2150
|
-
* @param msg - A parsed message.
|
|
2356
|
+
* @param msg - A parsed **single-message** stream.
|
|
2151
2357
|
* @returns The comment records (possibly empty).
|
|
2358
|
+
* @throws {@link AstmAmbiguousStreamError} (`ASTM_AMBIGUOUS_MULTI_MESSAGE`) when the stream
|
|
2359
|
+
* carries more than one message — see {@link messages}.
|
|
2152
2360
|
* @example
|
|
2153
2361
|
* ```ts
|
|
2154
2362
|
* import { parseAstmRecords, comments } from "@cosyte/astm";
|
|
@@ -2159,6 +2367,9 @@ declare function orders(msg: AstmMessage): readonly OrderRecord[];
|
|
|
2159
2367
|
declare function comments(msg: AstmMessage): readonly CommentRecord[];
|
|
2160
2368
|
/**
|
|
2161
2369
|
* The comment (`C`) records attached to a given parent record, in wire order.
|
|
2370
|
+
*
|
|
2371
|
+
* Unlike the other extractors this works on **any** stream, single- or multi-message: the
|
|
2372
|
+
* parent record it is handed already names the message, so there is nothing to disambiguate.
|
|
2162
2373
|
* Returns the comments whose `parentIndex` is that record's `recordIndex`, so a
|
|
2163
2374
|
* comment carrying (e.g.) QC context is read against the record it modifies —
|
|
2164
2375
|
* never floated to the wrong one.
|
|
@@ -2179,8 +2390,10 @@ declare function commentsFor(msg: AstmMessage, parent: AstmRecord): readonly Com
|
|
|
2179
2390
|
* result means the message is a **host-query request**, not a result set — see
|
|
2180
2391
|
* {@link AstmMessage.classification} (`isHostQueryRequest`).
|
|
2181
2392
|
*
|
|
2182
|
-
* @param msg - A parsed message.
|
|
2393
|
+
* @param msg - A parsed **single-message** stream.
|
|
2183
2394
|
* @returns The query records (possibly empty).
|
|
2395
|
+
* @throws {@link AstmAmbiguousStreamError} (`ASTM_AMBIGUOUS_MULTI_MESSAGE`) when the stream
|
|
2396
|
+
* carries more than one message — see {@link messages}.
|
|
2184
2397
|
* @example
|
|
2185
2398
|
* ```ts
|
|
2186
2399
|
* import { parseAstmRecords, query } from "@cosyte/astm";
|
|
@@ -2190,9 +2403,195 @@ declare function commentsFor(msg: AstmMessage, parent: AstmRecord): readonly Com
|
|
|
2190
2403
|
*/
|
|
2191
2404
|
declare function query(msg: AstmMessage): readonly QueryRecord[];
|
|
2192
2405
|
|
|
2406
|
+
/**
|
|
2407
|
+
* Message grouping — the safe replacement for the stream-scoped extractors.
|
|
2408
|
+
*
|
|
2409
|
+
* A parsed {@link AstmMessage} models a whole **record stream**, and a stream may carry
|
|
2410
|
+
* several messages back to back: a message runs from an `H` header to its `L` terminator.
|
|
2411
|
+
* The flat extractors in `./extractors.js` read the whole stream, so on a multi-message
|
|
2412
|
+
* stream `patient(msg)` answered with the first `P` in the stream while `results(msg)`
|
|
2413
|
+
* answered with every `R` in it — pairing the two, which is exactly what the package's
|
|
2414
|
+
* one-line north star does, attributed one patient's results to another. It needed no
|
|
2415
|
+
* redeclaration and no unusual delimiters, and it warned about nothing.
|
|
2416
|
+
*
|
|
2417
|
+
* {@link messages} is the fix: it splits the stream into the messages it actually
|
|
2418
|
+
* contains, and each one carries only its own records, so a patient and a result are only
|
|
2419
|
+
* ever paired inside the message that carried both.
|
|
2420
|
+
*
|
|
2421
|
+
* **Where a message begins.** A new message opens at every `H` record and runs to the
|
|
2422
|
+
* record before the next `H` (or the end of the stream). That is deliberately the *same*
|
|
2423
|
+
* boundary the parser already uses to scope delimiters: the active delimiter set is
|
|
2424
|
+
* re-read at each `H` and governs that header and the records that follow it, so message
|
|
2425
|
+
* boundaries and delimiter scope cannot disagree. The `L` terminator **closes** a message
|
|
2426
|
+
* but does not open a scope, so anything between an `L` and the next `H` stays with the
|
|
2427
|
+
* message its header opened rather than being dropped. Grouping is therefore **total**:
|
|
2428
|
+
* every record of the stream lands in exactly one message, none duplicated, none lost.
|
|
2429
|
+
*
|
|
2430
|
+
* **What the standard grounds, and what it does not.** CLSI LIS02-A2 §2 is definitional
|
|
2431
|
+
* about the unit itself: a message is bounded by the `H` record at one end and the `L`
|
|
2432
|
+
* record at the other. That clause is in CLSI's free sample and is the whole of what is
|
|
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` — is a reasoned choice, not a citation. It
|
|
2436
|
+
* is made this way because it is the boundary this parser already enforces for delimiters,
|
|
2437
|
+
* and because it never drops a record.
|
|
2438
|
+
*
|
|
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
|
+
* here. The clauses that would ground it (the message-level structure diagram, and the `P`
|
|
2442
|
+
* sequence-number rule) are withheld from the free sample and are paywalled, so rather than
|
|
2443
|
+
* guess a scoping rule this layer refuses to answer: a message carrying more than one `P`
|
|
2444
|
+
* leaves {@link AstmStreamMessage.patient} `undefined` and surfaces all of them on
|
|
2445
|
+
* {@link AstmStreamMessage.patients}. Multi-patient messages are real — at least one
|
|
2446
|
+
* openly-published vendor interface grammar makes the patient group repeatable on the
|
|
2447
|
+
* download direction — so this is a deferral with a known shape, not a claim that the case
|
|
2448
|
+
* does not arise, and not a claim that the standard is silent about it.
|
|
2449
|
+
*
|
|
2450
|
+
* **The OSS corpus was checked and offers no prior art.** It is informative here, unlike
|
|
2451
|
+
* for delimiter questions, but only negatively: the two commonly-cited reference
|
|
2452
|
+
* implementations both dissolve the message unit. One dispatches records to per-type
|
|
2453
|
+
* callbacks one at a time with no carried state, leaving the grouping to the integrator;
|
|
2454
|
+
* the other merges every message of a transport session into one envelope of flat
|
|
2455
|
+
* per-record-type buckets, from which the association cannot be recovered at all. A flat
|
|
2456
|
+
* model is the incumbent shape, and the misattribution above is what it costs.
|
|
2457
|
+
*/
|
|
2458
|
+
|
|
2459
|
+
/**
|
|
2460
|
+
* Stable codes for the two ways a flat, stream-scoped accessor can fail to name a single
|
|
2461
|
+
* answer. Renaming a code is a **breaking change**.
|
|
2462
|
+
*
|
|
2463
|
+
* @example
|
|
2464
|
+
* ```ts
|
|
2465
|
+
* import { parseAstmRecords, patient, AMBIGUOUS_CODES, AstmAmbiguousStreamError } from "@cosyte/astm";
|
|
2466
|
+
* try {
|
|
2467
|
+
* patient(parseAstmRecords(raw));
|
|
2468
|
+
* } catch (err) {
|
|
2469
|
+
* if (err instanceof AstmAmbiguousStreamError) {
|
|
2470
|
+
* err.code === AMBIGUOUS_CODES.ASTM_AMBIGUOUS_MULTI_MESSAGE;
|
|
2471
|
+
* }
|
|
2472
|
+
* }
|
|
2473
|
+
* ```
|
|
2474
|
+
*/
|
|
2475
|
+
declare const AMBIGUOUS_CODES: {
|
|
2476
|
+
/** The stream carries more than one `H` … `L` message, so a stream-wide answer spans patients. */
|
|
2477
|
+
readonly ASTM_AMBIGUOUS_MULTI_MESSAGE: "ASTM_AMBIGUOUS_MULTI_MESSAGE";
|
|
2478
|
+
/** The one message in the stream carries more than one `P`, so "the patient" is not determined. */
|
|
2479
|
+
readonly ASTM_AMBIGUOUS_MULTI_PATIENT: "ASTM_AMBIGUOUS_MULTI_PATIENT";
|
|
2480
|
+
};
|
|
2481
|
+
/** A value from {@link AMBIGUOUS_CODES} — the discriminant on {@link AstmAmbiguousStreamError}. */
|
|
2482
|
+
type AmbiguousCode = (typeof AMBIGUOUS_CODES)[keyof typeof AMBIGUOUS_CODES];
|
|
2483
|
+
/**
|
|
2484
|
+
* Thrown by a flat extractor when the stream it was handed does not determine a single
|
|
2485
|
+
* answer — either because it carries several messages, or because its one message carries
|
|
2486
|
+
* several patients.
|
|
2487
|
+
*
|
|
2488
|
+
* The throw **is** the fix. A caller who gets this error is strictly better off than one
|
|
2489
|
+
* who silently received another patient's results, and the remedy is mechanical: walk
|
|
2490
|
+
* {@link messages} and read each message's own records.
|
|
2491
|
+
*
|
|
2492
|
+
* **PHI:** carries a stable code, a position, and two counts. Never a field value, never a
|
|
2493
|
+
* patient identifier.
|
|
2494
|
+
*
|
|
2495
|
+
* @example
|
|
2496
|
+
* ```ts
|
|
2497
|
+
* import { parseAstmRecords, messages, results, AstmAmbiguousStreamError } from "@cosyte/astm";
|
|
2498
|
+
* const msg = parseAstmRecords(raw);
|
|
2499
|
+
* try {
|
|
2500
|
+
* results(msg);
|
|
2501
|
+
* } catch (err) {
|
|
2502
|
+
* if (err instanceof AstmAmbiguousStreamError) {
|
|
2503
|
+
* for (const m of messages(msg)) m.results.length;
|
|
2504
|
+
* }
|
|
2505
|
+
* }
|
|
2506
|
+
* ```
|
|
2507
|
+
*/
|
|
2508
|
+
declare class AstmAmbiguousStreamError extends Error {
|
|
2509
|
+
/** The stable discriminant — see {@link AMBIGUOUS_CODES}. */
|
|
2510
|
+
readonly code: AmbiguousCode;
|
|
2511
|
+
/** Where the ambiguity became visible: the second header, or the second `P`. Value-free. */
|
|
2512
|
+
readonly position: AstmPosition;
|
|
2513
|
+
/** How many `H` … `L` messages the stream carries. */
|
|
2514
|
+
readonly messageCount: number;
|
|
2515
|
+
/** How many `P` records the message in question carries. */
|
|
2516
|
+
readonly patientCount: number;
|
|
2517
|
+
/** @internal */
|
|
2518
|
+
constructor(code: AmbiguousCode, message: string, position: AstmPosition, counts: {
|
|
2519
|
+
readonly messageCount: number;
|
|
2520
|
+
readonly patientCount: number;
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
/**
|
|
2524
|
+
* One `H` … `L` message inside a parsed stream, with its own records and nothing else.
|
|
2525
|
+
*
|
|
2526
|
+
* This is the unit the flat extractors were always assumed to be reading. `AstmMessage`
|
|
2527
|
+
* models the whole stream; this models a message within it.
|
|
2528
|
+
*/
|
|
2529
|
+
interface AstmStreamMessage {
|
|
2530
|
+
/** 0-based ordinal of this message within the stream. */
|
|
2531
|
+
readonly index: number;
|
|
2532
|
+
/** This message's header record. */
|
|
2533
|
+
readonly header: HeaderRecord;
|
|
2534
|
+
/**
|
|
2535
|
+
* The delimiter set in force for this message — the header's own resolved set, which is
|
|
2536
|
+
* the set its records were actually read with. When a later header's declaration was
|
|
2537
|
+
* unusable, this is the set that stayed in force, never a guessed one.
|
|
2538
|
+
*/
|
|
2539
|
+
readonly delimiters: Delimiters;
|
|
2540
|
+
/** Every record of this message in wire order, the header first. */
|
|
2541
|
+
readonly records: readonly AstmRecord[];
|
|
2542
|
+
/**
|
|
2543
|
+
* This message's patient — but **only when the message determines one**: the single `P`
|
|
2544
|
+
* when it carries exactly one, and `undefined` when it carries none **or** several.
|
|
2545
|
+
* `patients.length` distinguishes those two cases; there is no third meaning.
|
|
2546
|
+
*
|
|
2547
|
+
* A message carrying several patients is not answered with the first one, because
|
|
2548
|
+
* "the first `P`" is precisely the guess that files a result against the wrong person.
|
|
2549
|
+
*/
|
|
2550
|
+
readonly patient: PatientRecord | undefined;
|
|
2551
|
+
/** Every `P` record in this message, in wire order (usually zero or one). */
|
|
2552
|
+
readonly patients: readonly PatientRecord[];
|
|
2553
|
+
/** Every `R` (result) record in this message, in wire order. */
|
|
2554
|
+
readonly results: readonly ResultRecord[];
|
|
2555
|
+
/** Every `O` (order) record in this message, in wire order. */
|
|
2556
|
+
readonly orders: readonly OrderRecord[];
|
|
2557
|
+
/** Every `C` (comment) record in this message, in wire order. */
|
|
2558
|
+
readonly comments: readonly CommentRecord[];
|
|
2559
|
+
/**
|
|
2560
|
+
* Every `Q` (request-information) record in this message, in wire order.
|
|
2561
|
+
*
|
|
2562
|
+
* For this message's host-query classification, call `classifyMessage(m.records)`. The
|
|
2563
|
+
* `classification` on the parsed model is folded over the **whole stream**, so it is not
|
|
2564
|
+
* per-message and is not mirrored here; deriving it from a message's own records is.
|
|
2565
|
+
*/
|
|
2566
|
+
readonly queries: readonly QueryRecord[];
|
|
2567
|
+
}
|
|
2568
|
+
/**
|
|
2569
|
+
* Split a parsed stream into the `H` … `L` messages it carries.
|
|
2570
|
+
*
|
|
2571
|
+
* This is the safe path and it never throws: a single-message stream yields exactly one
|
|
2572
|
+
* entry, and pairing a patient with a result inside one entry is correct by construction.
|
|
2573
|
+
*
|
|
2574
|
+
* Returns an **array**, not an iterator: the stream is already fully parsed and frozen, so
|
|
2575
|
+
* there is no work to defer, and every other extractor in this package returns a
|
|
2576
|
+
* `readonly` array.
|
|
2577
|
+
*
|
|
2578
|
+
* @param msg - A parsed stream.
|
|
2579
|
+
* @returns The messages, in wire order; never empty for a parsed stream.
|
|
2580
|
+
* @example
|
|
2581
|
+
* ```ts
|
|
2582
|
+
* import { parseAstmRecords, messages } from "@cosyte/astm";
|
|
2583
|
+
* const stream = parseAstmRecords("H|\\^&\rP|1|PRAC-1\rR|1|^^^687|1.0|U/L||N||F\rL|1\r");
|
|
2584
|
+
* for (const m of messages(stream)) {
|
|
2585
|
+
* m.patient?.practiceAssignedId; // "PRAC-1"
|
|
2586
|
+
* m.results[0]?.value; // "1.0"
|
|
2587
|
+
* }
|
|
2588
|
+
* ```
|
|
2589
|
+
*/
|
|
2590
|
+
declare function messages(msg: AstmMessage): readonly AstmStreamMessage[];
|
|
2591
|
+
|
|
2193
2592
|
/**
|
|
2194
2593
|
* The **host-query flow**: classify a message as a request, a result upload, an
|
|
2195
|
-
* order download, or indeterminate
|
|
2594
|
+
* order download, or indeterminate.
|
|
2196
2595
|
*
|
|
2197
2596
|
* On real analyzers the host-query mode is first-class and sometimes mandatory
|
|
2198
2597
|
* (on the Roche cobas 4800 there is *no* results-sending-only option): the
|
|
@@ -2254,6 +2653,36 @@ declare function classifyMessage(records: readonly AstmRecord[]): AstmMessageCla
|
|
|
2254
2653
|
* ```
|
|
2255
2654
|
*/
|
|
2256
2655
|
declare function tokenizeRecord(record: string, d: Delimiters, onUnknownEscape?: (fieldIndex: number) => void): AstmField[];
|
|
2656
|
+
/**
|
|
2657
|
+
* Tokenize an `H` (header) record into its fields.
|
|
2658
|
+
*
|
|
2659
|
+
* A header cannot go through {@link tokenizeRecord}: its second field is the
|
|
2660
|
+
* **delimiter declaration**, which carries the escape character *literally*
|
|
2661
|
+
* rather than as an escape sequence. The generic escape-aware split treats that
|
|
2662
|
+
* lone escape character as the opening of an unterminated escape and swallows
|
|
2663
|
+
* the whole remainder of the record into one field, so every data field after
|
|
2664
|
+
* the declaration is lost. This tokenizer instead takes the declaration verbatim
|
|
2665
|
+
* as one opaque field and applies the ordinary escape-aware tokenizer to the
|
|
2666
|
+
* data portion that follows it.
|
|
2667
|
+
*
|
|
2668
|
+
* `fields[0]` is the type-letter field and `fields[1]` is the delimiter
|
|
2669
|
+
* declaration (verbatim, never escape-decoded); the header's ASTM data fields
|
|
2670
|
+
* follow from `fields[2]`.
|
|
2671
|
+
*
|
|
2672
|
+
* @param record - The raw `H` record text (terminator already stripped).
|
|
2673
|
+
* @param d - The delimiters declared by this header.
|
|
2674
|
+
* @param onUnknownEscape - Called with the 0-based whole-record field index for
|
|
2675
|
+
* each unrecognized escape sequence in the data portion.
|
|
2676
|
+
* @returns The header's fields.
|
|
2677
|
+
* @example
|
|
2678
|
+
* ```ts
|
|
2679
|
+
* import { tokenizeHeader, CANONICAL_DELIMITERS } from "@cosyte/astm";
|
|
2680
|
+
* const fields = tokenizeHeader("H|\\^&|||sender", CANONICAL_DELIMITERS);
|
|
2681
|
+
* fields[1].raw; // "\\^&"
|
|
2682
|
+
* fields[4].raw; // "sender"
|
|
2683
|
+
* ```
|
|
2684
|
+
*/
|
|
2685
|
+
declare function tokenizeHeader(record: string, d: Delimiters, onUnknownEscape?: (fieldIndex: number) => void): AstmField[];
|
|
2257
2686
|
/**
|
|
2258
2687
|
* The primary scalar of a field: its first repeat's first component, decoded.
|
|
2259
2688
|
* Returns `undefined` for a truly empty field so callers can distinguish
|
|
@@ -2274,9 +2703,13 @@ declare function fieldScalar(field: AstmField | undefined): string | undefined;
|
|
|
2274
2703
|
* Tier-3 **fatal** codes mark input the parser cannot recover into a structured
|
|
2275
2704
|
* `AstmMessage`; anything less severe is a Tier-2 warning (see `./warnings.ts`).
|
|
2276
2705
|
* `AstmParseError` is thrown directly and consumers narrow via the `code`
|
|
2277
|
-
* discriminant.
|
|
2278
|
-
*
|
|
2279
|
-
*
|
|
2706
|
+
* discriminant. {@link FatalCode} is **closed**: these three are all of it. The frame
|
|
2707
|
+
* codec reuses `EMPTY_INPUT` (an empty stream is fatal in both its lenient and strict
|
|
2708
|
+
* modes) but does not widen the union — its own thrown errors are separate types with
|
|
2709
|
+
* their own discriminants: {@link AstmFrameEncodeError} carries
|
|
2710
|
+
* `ASTM_FRAME_EMPTY_RECORD`, and {@link AstmFrameStrictError} carries the rejected
|
|
2711
|
+
* warnings rather than a `code`. So narrowing an `AstmParseError` on `code` will only
|
|
2712
|
+
* ever see a value from this set.
|
|
2280
2713
|
*/
|
|
2281
2714
|
|
|
2282
2715
|
/**
|
|
@@ -2341,11 +2774,11 @@ declare class AstmParseError extends Error {
|
|
|
2341
2774
|
}
|
|
2342
2775
|
|
|
2343
2776
|
/**
|
|
2344
|
-
* The ASTM escape codec — the
|
|
2777
|
+
* The ASTM escape codec — the fix for a documented silent-misread class.
|
|
2345
2778
|
*
|
|
2346
2779
|
* ASTM escapes an embedded delimiter with the escape character on both sides of
|
|
2347
|
-
* a single mnemonic letter (the InterSystems mnemonics, cross-verified
|
|
2348
|
-
*
|
|
2780
|
+
* a single mnemonic letter (the InterSystems mnemonics, cross-verified against
|
|
2781
|
+
* the OSS reference implementations):
|
|
2349
2782
|
*
|
|
2350
2783
|
* ```
|
|
2351
2784
|
* &F& → field delimiter (default "|")
|
|
@@ -2365,8 +2798,8 @@ declare class AstmParseError extends Error {
|
|
|
2365
2798
|
* leaf**: the escape sequence survives the split intact and only then decodes to
|
|
2366
2799
|
* a single literal, so `1&S&40` reads as exactly one component.
|
|
2367
2800
|
*
|
|
2368
|
-
* Re-escaping (the inverse, for spec-clean emit)
|
|
2369
|
-
*
|
|
2801
|
+
* Re-escaping (the inverse, for spec-clean emit) lives in the serializer and is
|
|
2802
|
+
* deliberately not implemented here.
|
|
2370
2803
|
*/
|
|
2371
2804
|
|
|
2372
2805
|
/**
|
|
@@ -2683,7 +3116,7 @@ declare function resolveProfileTransport(profile: AstmProfile | undefined): Astm
|
|
|
2683
3116
|
* safety gate must not have), this module enumerates the tiny set of **explicitly
|
|
2684
3117
|
* tolerable** codes and treats *every other* code, across all three registries
|
|
2685
3118
|
* (record `ASTM_RECORD_*`, frame `ASTM_FRAME_*`, protocol `ASTM_LTP_*`), as
|
|
2686
|
-
* safety-critical. A warning code added
|
|
3119
|
+
* safety-critical. A warning code added later is therefore
|
|
2687
3120
|
* safety-critical **by default** until someone deliberately, reviewably adds it to
|
|
2688
3121
|
* the allow-list below. Widening the allow-list can only ever be an explicit,
|
|
2689
3122
|
* argued act; forgetting to touch this file can never *weaken* the gate.
|
|
@@ -2800,8 +3233,7 @@ declare const defaultProfile: AstmProfile;
|
|
|
2800
3233
|
* grounded firsthand in the **redistributable OSS reference corpus**, not an
|
|
2801
3234
|
* invented per-vendor deviation matrix.
|
|
2802
3235
|
*
|
|
2803
|
-
* Grounding (firsthand-verified 2026-07-21; the corpus is permissively licensed
|
|
2804
|
-
* in scope for grounding, per the roadmap):
|
|
3236
|
+
* Grounding (firsthand-verified 2026-07-21; the corpus is permissively licensed):
|
|
2805
3237
|
*
|
|
2806
3238
|
* - **`kxepal/python-astm` `codec.py`** (BSD) — its `decode_record` /
|
|
2807
3239
|
* `decode_component` / `decode_repeated_component` split fields, components, and
|
|
@@ -2814,7 +3246,7 @@ declare const defaultProfile: AstmProfile;
|
|
|
2814
3246
|
* or non-standard `&…&` body inside a free-text field (units, comments, test
|
|
2815
3247
|
* names) that our **escape-aware** tokenizer flags as
|
|
2816
3248
|
* `ASTM_UNKNOWN_ESCAPE_SEQUENCE`. We are deliberately stricter than the OSS
|
|
2817
|
-
* references here
|
|
3249
|
+
* references here, so the warning is *expected*
|
|
2818
3250
|
* noise when talking to that ecosystem — recognizable and non-clinical.
|
|
2819
3251
|
*
|
|
2820
3252
|
* **Why this is safe.** The escape codec **preserves an unrecognized `&…&` body
|
|
@@ -2828,8 +3260,7 @@ declare const defaultProfile: AstmProfile;
|
|
|
2828
3260
|
*
|
|
2829
3261
|
* **Honesty.** This is a *reference-corpus* profile, not a claim about any named
|
|
2830
3262
|
* analyzer vendor. No per-vendor ("cobas does X") behaviour is asserted; a named
|
|
2831
|
-
* vendor profile awaits a firsthand vendor-attributed quirk document
|
|
2832
|
-
* deferred (`REAL-CORPUS`).
|
|
3263
|
+
* vendor profile awaits a firsthand vendor-attributed quirk document.
|
|
2833
3264
|
*/
|
|
2834
3265
|
|
|
2835
3266
|
/**
|
|
@@ -2883,13 +3314,13 @@ declare const astmProfiles: {
|
|
|
2883
3314
|
* The **consumer-supplied** LIVD catalog: a vendor-test-code → LOINC index built
|
|
2884
3315
|
* from an IICC LIVD ("LOINC to Vendor IVD") mapping the consumer provides.
|
|
2885
3316
|
*
|
|
2886
|
-
* **Bring-your-own, by design
|
|
3317
|
+
* **Bring-your-own, by design.** `@cosyte/astm` bundles
|
|
2887
3318
|
* **no** LOINC, SNOMED, or LIVD data — LOINC is © Regenstrief (redistributable
|
|
2888
3319
|
* only with its attribution notice) and the public CDC LIVD publication is a
|
|
2889
3320
|
* SARS-CoV-2-specific file that also carries SNOMED CT (separately licensed), not
|
|
2890
3321
|
* a general-analyte, public-domain catalog. So the package stays a **structural
|
|
2891
3322
|
* recognizer, not a dictionary**: it recognizes the Universal Test ID's LOINC slot
|
|
2892
|
-
* and surfaces vendor codes verbatim
|
|
3323
|
+
* and surfaces vendor codes verbatim, and this module lets a consumer
|
|
2893
3324
|
* *supply* their own LIVD file to map those codes — the terminology data, and its
|
|
2894
3325
|
* license obligations, are the consumer's.
|
|
2895
3326
|
*
|
|
@@ -3011,8 +3442,8 @@ interface LivdCatalog {
|
|
|
3011
3442
|
declare function defineLivdCatalog(entries: readonly LivdEntry[]): LivdCatalog;
|
|
3012
3443
|
|
|
3013
3444
|
/**
|
|
3014
|
-
* The **terminology** warning registry (`ASTM_LIVD_*`) for the
|
|
3015
|
-
*
|
|
3445
|
+
* The **terminology** warning registry (`ASTM_LIVD_*`) for the LIVD-aware LOINC
|
|
3446
|
+
* recognition layer — the fourth of the package's registries,
|
|
3016
3447
|
* alongside the record layer's `ASTM_RECORD_*`, the frame codec's `ASTM_FRAME_*`,
|
|
3017
3448
|
* and the protocol reducer's `ASTM_LTP_*`.
|
|
3018
3449
|
*
|
|
@@ -3111,7 +3542,7 @@ declare function livdUnmappedCode(position: AstmPosition): AstmLivdWarning;
|
|
|
3111
3542
|
declare function livdAmbiguousMapping(position: AstmPosition): AstmLivdWarning;
|
|
3112
3543
|
|
|
3113
3544
|
/**
|
|
3114
|
-
* The **additive, advisory** LIVD annotation layer
|
|
3545
|
+
* The **additive, advisory** LIVD annotation layer. {@link applyLivd}
|
|
3115
3546
|
* reads a parsed {@link AstmMessage} and a consumer-supplied {@link LivdCatalog}
|
|
3116
3547
|
* and returns a **separate** layer of per-record LOINC annotations — it **never
|
|
3117
3548
|
* mutates, alters, or drops** the raw reported code or value. Recognition is an
|
|
@@ -3540,8 +3971,8 @@ declare const EOT = 4;
|
|
|
3540
3971
|
*
|
|
3541
3972
|
* Bytes outside a frame (before the first `STX`, or between a frame's trailing
|
|
3542
3973
|
* `LF` and the next `STX`) are skipped: in ASTM they are low-level transfer
|
|
3543
|
-
* control (`ENQ`/`ACK`/`NAK`/`EOT`
|
|
3544
|
-
*
|
|
3974
|
+
* control (`ENQ`/`ACK`/`NAK`/`EOT`), never record content, so skipping them is
|
|
3975
|
+
* not data loss.
|
|
3545
3976
|
*
|
|
3546
3977
|
* @param bytes - The framed byte stream.
|
|
3547
3978
|
* @param options - Decode options; lenient unless `strict` is set.
|
|
@@ -3561,7 +3992,7 @@ declare const EOT = 4;
|
|
|
3561
3992
|
declare function decodeAstmFrames(bytes: Uint8Array, options?: FrameOptions): DecodeAstmFramesResult;
|
|
3562
3993
|
|
|
3563
3994
|
/**
|
|
3564
|
-
* The frame-layer **emit** side: {@link composeAstmFrames}
|
|
3995
|
+
* The frame-layer **emit** side: {@link composeAstmFrames}.
|
|
3565
3996
|
*
|
|
3566
3997
|
* The exact inverse of {@link decodeAstmFrames}. Given the reassembled record
|
|
3567
3998
|
* byte-strings (one entry per complete record — the same shape `decodeAstmFrames`
|
|
@@ -3824,12 +4255,18 @@ declare class AstmFrameStrictError extends Error {
|
|
|
3824
4255
|
* — **never a confident wrong value**. Delimiters are read from each header,
|
|
3825
4256
|
* embedded escapes are decoded before a value is split, the practice- and
|
|
3826
4257
|
* laboratory-assigned patient IDs stay distinct, and every deviation is a stable,
|
|
3827
|
-
* value-free warning. Result flag/status semantics
|
|
3828
|
-
* depth + the `C` comment record + partial-timestamp hardening
|
|
4258
|
+
* value-free warning. Result flag/status semantics, patient/order identity
|
|
4259
|
+
* depth + the `C` comment record + partial-timestamp hardening, and the
|
|
3829
4260
|
* request-information (`Q`) record + host-query classification + verbatim `M`/`S`
|
|
3830
|
-
* records
|
|
4261
|
+
* records are all modeled — the **record-content layer is now feature-complete**.
|
|
4262
|
+
*
|
|
4263
|
+
* A parsed model is a whole record **stream**, and a stream may carry several `H` … `L`
|
|
4264
|
+
* messages. {@link messages} splits it into them, so a patient and a result are only ever
|
|
4265
|
+
* paired inside the message that carried both; the flat extractors ({@link patient},
|
|
4266
|
+
* {@link results}, …) read the whole stream and therefore **refuse** one they cannot answer
|
|
4267
|
+
* for, with an {@link AstmAmbiguousStreamError}, rather than answering across patients.
|
|
3831
4268
|
*
|
|
3832
|
-
* The E1381/CLSI-LIS01 **framing** layer
|
|
4269
|
+
* The E1381/CLSI-LIS01 **framing** layer lives alongside the record layer and
|
|
3833
4270
|
* shares nothing but the payload boundary: {@link decodeAstmFrames} decodes a framed
|
|
3834
4271
|
* byte stream (`<STX> FN text <ETB|ETX> CS <CR><LF>`) into frames + reassembled
|
|
3835
4272
|
* record bytes — verifying the modulo-256 checksum (a bad frame is surfaced
|
|
@@ -3837,7 +4274,7 @@ declare class AstmFrameStrictError extends Error {
|
|
|
3837
4274
|
* silently bridged), and reassembling the 240-byte-limited multi-frame records.
|
|
3838
4275
|
* {@link parseFramedAstm} composes the two layers at the edge.
|
|
3839
4276
|
*
|
|
3840
|
-
* The LTP **protocol** layer
|
|
4277
|
+
* The LTP **protocol** layer sits above the frame codec: {@link detectFraming}
|
|
3841
4278
|
* auto-detects the transport reality — framed (serial / cobas 4800 / Iguana) vs raw
|
|
3842
4279
|
* (cobas b121, framing dropped) — from the stream's leading byte, and
|
|
3843
4280
|
* {@link ltpReduce} is a **pure, socket-free** receiver-side state machine over
|
|
@@ -3845,7 +4282,19 @@ declare class AstmFrameStrictError extends Error {
|
|
|
3845
4282
|
* reducer decides. Its inviolable rule mirrors `mllp`'s ACK-failsafe: a frame the
|
|
3846
4283
|
* codec did not vouch for is answered with `NAK`, **never** a fabricated positive
|
|
3847
4284
|
* `ACK`, and never merged into a record — a `NAK` drives retransmit, not acceptance.
|
|
3848
|
-
*
|
|
4285
|
+
*
|
|
4286
|
+
* The **emit** half is the conservative inverse of the record and framing layers.
|
|
4287
|
+
* {@link buildAstmMessage} constructs records from typed caller input, emitting only
|
|
4288
|
+
* the values the caller supplied; {@link serializeAstmRecords} writes them out with the
|
|
4289
|
+
* canonical `H|\^&` delimiters by default, re-escaping every embedded delimiter in a
|
|
4290
|
+
* decoded component leaf (every record type is emitted from its decoded `fields`, except that
|
|
4291
|
+
* `M`/`S` bytes pass through untouched when they already read correctly under the emit set, so
|
|
4292
|
+
* an edit to the model is reflected on emit and the whole stream goes out in the one
|
|
4293
|
+
* delimiter set the header declares); {@link composeAstmFrames} wraps them into frames whose
|
|
4294
|
+
* modulo-256 checksum is
|
|
4295
|
+
* computed rather than accepted; and {@link serializeFramedAstm} composes the last two.
|
|
4296
|
+
* The protocol layer has no emit inverse by design: {@link ltpReduce} returns the actions
|
|
4297
|
+
* to take, and the consumer owns the wire.
|
|
3849
4298
|
*/
|
|
3850
4299
|
/**
|
|
3851
4300
|
* Library version string, kept in lockstep with `package.json#version`.
|
|
@@ -3863,4 +4312,4 @@ declare class AstmFrameStrictError extends Error {
|
|
|
3863
4312
|
*/
|
|
3864
4313
|
declare const VERSION: string;
|
|
3865
4314
|
|
|
3866
|
-
export { ALL_ASTM_WARNING_CODES, ACK as ASTM_ACK, ENQ as ASTM_ENQ, EOT as ASTM_EOT, NAK as ASTM_NAK, type AbnormalFlag, type AbnormalFlagCode, type AbnormalFlagMeaning, type AnyAstmWarningCode, type AstmDate, type AstmDatePrecision, type AstmField, type AstmFrame, AstmFrameEncodeError, type AstmFramePosition, AstmFrameStrictError, type AstmFrameWarning, type AstmFraming, type AstmLivdWarning, type AstmLtpWarning, type AstmMessage, type AstmMessageClassification, type AstmMessageKind, AstmParseError, type AstmParseOptions, type AstmPosition, type AstmProfile, AstmProfileDefinitionError, type AstmProfileProvenance, type AstmQuirkMatch, type AstmQuirkTolerance, type AstmRecord, type AstmRecordInput, type AstmRecordWarning, AstmSerializeError, AstmStrictError, CANONICAL_DELIMITERS, type CommentInput, type CommentRecord, type ComposeFramesOptions, type DecodeAstmFramesResult, type DefineAstmProfileOptions, type Delimiters, type DetectFramingOptions, type DetectFramingResult, FATAL_CODES, FRAME_WARNING_CODES, type FatalCode, type FrameChecksum, type FrameOptions, type FrameTerminator, type FrameWarningCode, type FramedAstmResult, type HeaderInput, type HeaderRecord, LIVD_WARNING_CODES, LTP_WARNING_CODES, type LivdAnnotation, type LivdCatalog, type LivdEntry, type LivdLookup, type LivdMapping, type LivdResult, type LivdWarningCode, type LtpAction, type LtpEvent, type LtpPhase, type LtpState, type LtpTransition, type LtpWarningCode, type ManufacturerRecord, type MessageInput, type OrderInput, type OrderRecord, type PatientInput, type PatientName, type PatientNameInput, type PatientRecord, type QueryInput, type QueryRecord, type ReferenceRange, type ReferenceRangeKind, type ResultInput, type ResultRecord, type ResultStatus, type ResultStatusCode, type ResultStatusMeaning, SAFETY_CRITICAL_CODES, type ScientificRecord, TOLERABLE_CODES, type TerminatorRecord, type UniversalTestId, type UniversalTestIdProvenance, type UnknownEscapeSink, type UnsupportedRecord, VERSION, type VerbatimInput, WARNING_CODES, type WarningCode, ambiguousMessageKind, ambiguousValueSplit, applyAstmProfile, applyAstmProfileToWarnings, applyLivd, astmDateToLocalISO, astmProfiles, attachComments, buildAstmMessage, classifyMessage, comments, commentsFor, composeAstmFrames, computeChecksum, decodeAstmFrames, decodeEscapes, deepFreeze, defineAstmProfile, defineLivdCatalog, detectFraming, encodeComponent, fieldScalar, frameBadChecksum, frameOversize, frameSequenceGap, frameUnterminated, getAstmProfile, getDefaultAstmProfile, interpretAbnormalFlag, interpretResultStatus, isNonStandard, isSafetyCriticalCode, listAstmProfiles, livdAmbiguousMapping, livdUnmappedCode, lookupLivdForRecord, ltpAmbiguousTransport, ltpFrameRejected, ltpInitialState, ltpReduce, ltpUnexpectedEvent, nonStandardDelimiters, orders, orphanComment, parseAstmDate, parseAstmRecords, parseChecksumHex, parseFramedAstm, parseReferenceRange, partialTimestamp, patient, primaryCode, profileQuirkApplied, query, readDelimiters, recognizeUniversalTestId, resolveProfileTransport, results, serializeAstmRecord, serializeAstmRecords, serializeField, serializeFramedAstm, setDefaultAstmProfile, splitEscapeAware, toChecksumHex, tokenizeRecord, undefinedAbnormalFlag, undefinedResultStatus, uninterpretedQueryStatus, unitsAbsent, unknownEscapeSequence, unknownRecordType, unparseableReferenceRange };
|
|
4315
|
+
export { ALL_ASTM_WARNING_CODES, AMBIGUOUS_CODES, ACK as ASTM_ACK, ENQ as ASTM_ENQ, EOT as ASTM_EOT, NAK as ASTM_NAK, type AbnormalFlag, type AbnormalFlagCode, type AbnormalFlagMeaning, type AmbiguousCode, type AnyAstmWarningCode, AstmAmbiguousStreamError, type AstmDate, type AstmDatePrecision, type AstmField, type AstmFrame, AstmFrameEncodeError, type AstmFramePosition, AstmFrameStrictError, type AstmFrameWarning, type AstmFraming, type AstmLivdWarning, type AstmLtpWarning, type AstmMessage, type AstmMessageClassification, type AstmMessageKind, AstmParseError, type AstmParseOptions, type AstmPosition, type AstmProfile, AstmProfileDefinitionError, type AstmProfileProvenance, type AstmQuirkMatch, type AstmQuirkTolerance, type AstmRecord, type AstmRecordInput, type AstmRecordWarning, AstmSerializeError, type AstmSerializeErrorCode, type AstmStreamMessage, AstmStrictError, CANONICAL_DELIMITERS, type CommentInput, type CommentRecord, type ComposeFramesOptions, type DecodeAstmFramesResult, type DefineAstmProfileOptions, type Delimiters, type DetectFramingOptions, type DetectFramingResult, FATAL_CODES, FRAME_WARNING_CODES, type FatalCode, type FrameChecksum, type FrameOptions, type FrameTerminator, type FrameWarningCode, type FramedAstmResult, type HeaderInput, type HeaderRecord, LIVD_WARNING_CODES, LTP_WARNING_CODES, type LivdAnnotation, type LivdCatalog, type LivdEntry, type LivdLookup, type LivdMapping, type LivdResult, type LivdWarningCode, type LtpAction, type LtpEvent, type LtpPhase, type LtpState, type LtpTransition, type LtpWarningCode, type ManufacturerRecord, type MessageInput, type OrderInput, type OrderRecord, type PatientInput, type PatientName, type PatientNameInput, type PatientRecord, type QueryInput, type QueryRecord, type ReferenceRange, type ReferenceRangeKind, type ResultInput, type ResultRecord, type ResultStatus, type ResultStatusCode, type ResultStatusMeaning, SAFETY_CRITICAL_CODES, type ScientificRecord, TOLERABLE_CODES, type TerminatorRecord, type UniversalTestId, type UniversalTestIdProvenance, type UnknownEscapeSink, type UnsupportedRecord, VERSION, type VerbatimInput, WARNING_CODES, type WarningCode, ambiguousMessageKind, ambiguousValueSplit, applyAstmProfile, applyAstmProfileToWarnings, applyLivd, astmDateToLocalISO, astmProfiles, attachComments, buildAstmMessage, classifyMessage, comments, commentsFor, composeAstmFrames, computeChecksum, decodeAstmFrames, decodeEscapes, deepFreeze, defineAstmProfile, defineLivdCatalog, delimitersRedeclared, detectFraming, encodeComponent, fieldScalar, frameBadChecksum, frameOversize, frameSequenceGap, frameUnterminated, getAstmProfile, getDefaultAstmProfile, interpretAbnormalFlag, interpretResultStatus, isNonStandard, isSafetyCriticalCode, listAstmProfiles, livdAmbiguousMapping, livdUnmappedCode, lookupLivdForRecord, ltpAmbiguousTransport, ltpFrameRejected, ltpInitialState, ltpReduce, ltpUnexpectedEvent, messages, nonStandardDelimiters, orders, orphanComment, parseAstmDate, parseAstmRecords, parseChecksumHex, parseFramedAstm, parseReferenceRange, partialTimestamp, patient, primaryCode, profileQuirkApplied, query, readDelimiters, recognizeUniversalTestId, resolveProfileTransport, results, serializeAstmRecord, serializeAstmRecords, serializeField, serializeFramedAstm, setDefaultAstmProfile, splitEscapeAware, toChecksumHex, tokenizeHeader, tokenizeRecord, undefinedAbnormalFlag, undefinedResultStatus, uninterpretedQueryStatus, unitsAbsent, unknownEscapeSequence, unknownRecordType, unparseableReferenceRange, unreadableRedeclaration };
|