@cosyte/astm 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +215 -0
- package/README.md +84 -5
- package/dist/index.cjs +258 -159
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +404 -57
- package/dist/index.d.ts +404 -57
- package/dist/index.mjs +257 -160
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,59 @@ this file is maintained by hand (Changesets handles the version bump and publish
|
|
|
11
11
|
|
|
12
12
|
### Added
|
|
13
13
|
|
|
14
|
+
- **`ASTM_RECORD_FIELDS_UNSEPARATED`: a record that lost its fields no longer loses them in
|
|
15
|
+
silence** (`ASTM-TYPE-LETTER-SECOND-READER`, finding 2, the one that outranked the other).
|
|
16
|
+
A record carries its type letter and then its fields, separated by the field delimiter the header
|
|
17
|
+
declared, so a record that carries content beyond its type letter and still yields exactly **one**
|
|
18
|
+
field contains no field separator at all: the delimiters in force are not the set that record was
|
|
19
|
+
written with. Every modeled field of it is then absent. On an `R` that is the **value, the units
|
|
20
|
+
and the status at once**, and the result reads back as though it simply never carried them.
|
|
21
|
+
|
|
22
|
+
The reachable route is a header whose type letter the reader could not recognize. Delimiters are
|
|
23
|
+
re-read at every `H`, keyed on that same letter, so an unrecognized header does not re-scope them
|
|
24
|
+
and the whole following message is tokenized with the previous header's set. Measured: a
|
|
25
|
+
`99.9 mmol/L` final result read back with no value, no units and status `unspecified`, filed under
|
|
26
|
+
the previous message's patient, with the unrecognized-type warning as the **only** report on the
|
|
27
|
+
stream. That warning says a letter was unreadable; it never said a value had gone.
|
|
28
|
+
|
|
29
|
+
The detector is keyed on the **observed collapse**, not on that one cause of it, because
|
|
30
|
+
identifying the mangled header would itself require guessing which byte the sender meant. It
|
|
31
|
+
therefore also fires on the same silent collapse reached without any mangled header at all, which
|
|
32
|
+
reproduced on the previous release with **zero** warnings: a lone record written in another set
|
|
33
|
+
(`H|\^&` then `R*1*:::688*99.9*mmol/L**H**F`) parsed clean and answered `undefined` for the value.
|
|
34
|
+
|
|
35
|
+
**Reported, never repaired.** The fields are not re-split on a set no header declared, because
|
|
36
|
+
that would invent data; the raw line is surfaced intact and nothing is dropped. One warning per
|
|
37
|
+
affected record, each at its own position and carrying no field data. The code is **safety-critical
|
|
38
|
+
by construction** (the forbidden set is computed as every known code minus the tolerable
|
|
39
|
+
allow-list), so no profile can quiet it and `{ strict: true }` refuses. A header is exempt by
|
|
40
|
+
construction rather than by exception: it is always read with the set it declares itself. Content
|
|
41
|
+
after the type letter that is entirely whitespace is excluded, since no field is at stake there.
|
|
42
|
+
|
|
43
|
+
**Deliberately partial, and its absence certifies nothing.** This tests one of the four delimiter
|
|
44
|
+
roles, the **field** separator, and only in its total form, where that separator occurs nowhere in
|
|
45
|
+
the line. Two classes of the same loss stay silent and are documented as such on the code, in
|
|
46
|
+
`README.md` and in the quickstart rather than left to be inferred. A foreign set whose **field**
|
|
47
|
+
separator happens to occur anywhere in the line still splits, on the wrong boundaries: one stray
|
|
48
|
+
`|` inside an otherwise `*`-separated result loses the value, the units and the status with no
|
|
49
|
+
warning, and this can happen to one record **inside** a run of these warnings, so a run is not a
|
|
50
|
+
sweep of the records it spans. A set differing in the **repeat, component or escape** role usually
|
|
51
|
+
splits into fields normally, with the damage varying: a mis-split component can cost a test
|
|
52
|
+
identity while the value survives, but an **escape** character occurring literally in a record
|
|
53
|
+
merges every field after it and costs the value, the units and the status together (a lone `&`
|
|
54
|
+
under the canonical set leaves a nine-field `R` reading as four, in silence). All of them
|
|
55
|
+
reproduce identically on the previous release, and the last is recorded as a defect in its own
|
|
56
|
+
right. Widening the check would mean deciding which set a record ought to have had, which is the
|
|
57
|
+
same guess declined above, so the boundary is written down instead of chased.
|
|
58
|
+
|
|
59
|
+
The exported factory `fieldsUnseparated` joins the record registry, and `WARNING_CODES` goes from
|
|
60
|
+
15 members to 16. **That a record's fields are separated by the declared field delimiter is read
|
|
61
|
+
off this package's own emit contract and off every fixture in this repository, not off a normative
|
|
62
|
+
sentence, and no clause is claimed for it**: the relevant CLSI text is withheld from the free
|
|
63
|
+
sample and the surrounding standards are paywalled, and the redistributable reference corpus
|
|
64
|
+
hardcodes the canonical delimiters and never reads the declaration, so it cannot ground a
|
|
65
|
+
delimiter question either.
|
|
66
|
+
|
|
14
67
|
- **`messages()`: read a stream as the sequence of messages it actually is**
|
|
15
68
|
(`ASTM-PATIENT-RESULT-MISATTRIBUTION`). A parsed model has always been a whole record
|
|
16
69
|
**stream**, and a stream may carry several messages back to back: a message runs from an `H`
|
|
@@ -129,6 +182,106 @@ phase 8` passes while `Phase 8` reds). An arm keyed on a following digit was wri
|
|
|
129
182
|
|
|
130
183
|
### Changed
|
|
131
184
|
|
|
185
|
+
- **BREAKING: a record type letter the reader could not recognize no longer lets a host-query
|
|
186
|
+
request read as a result set** (`ASTM-TYPE-LETTER-SECOND-READER`, finding 1). `classifyMessage` counts `Q` / `R` /
|
|
187
|
+
`O` records **by letter**, and the `Q`-dominates guarantee (a message carrying a query is never
|
|
188
|
+
read as a result set) was stated on that count, so it only ever held while every letter was
|
|
189
|
+
legible. Measured: `H|\^&` plus a `Q` carrying one stray leading byte plus an `R` classified
|
|
190
|
+
`kind: "results"` with `isHostQueryRequest: false`, and the `ASTM_RECORD_AMBIGUOUS_MESSAGE_KIND`
|
|
191
|
+
warning that flags a `Q` + `R` contradiction vanished along with the `Q` it was counting. A stray
|
|
192
|
+
byte turned the fail-safe off.
|
|
193
|
+
|
|
194
|
+
The intended letter is still **never inferred**: that is the guess this package declines to make,
|
|
195
|
+
and inferring it would split a stream a different way just as silently. What the classifier does
|
|
196
|
+
instead is decline the positive answer. An unsupported record with no `Q` read alongside it now
|
|
197
|
+
yields `kind: "indeterminate"` rather than `"results"` or `"orders"`, and the new
|
|
198
|
+
`AstmMessageClassification.hasUnrecognized` reports why. A `Q` that **was** read still dominates,
|
|
199
|
+
because an unreadable letter can only ever add a kind, never remove a query already on the wire.
|
|
200
|
+
|
|
201
|
+
`hasQuery` / `hasResults` / `hasOrders` stay truthful, so a caller wanting the raw tally still has
|
|
202
|
+
it, and `results()` and the rest are untouched. Breaking for a consumer branching on `kind`: on a
|
|
203
|
+
stream carrying an unrecognized record letter, a previously positive `kind` now reads
|
|
204
|
+
`indeterminate`. That is the fix, and it moves in the fail-safe direction. A conformant stream,
|
|
205
|
+
where every letter is legible, is unchanged. Kept on the `0.0.x` pre-alpha ladder as a patch, per
|
|
206
|
+
the repo's version policy.
|
|
207
|
+
|
|
208
|
+
Two limits, stated rather than implied. `msg.classification` is still folded over the **whole
|
|
209
|
+
stream** (a known, separately-recorded defect), so one unrecognized letter anywhere now withholds
|
|
210
|
+
`kind` for the entire stream **unless a `Q` was read**, in which case the `Q` still dominates as
|
|
211
|
+
before. That widens the existing over-trigger; the per-message answer,
|
|
212
|
+
`classifyMessage(m.records)` on a `messages()` entry, is unaffected and is the reading to prefer.
|
|
213
|
+
And the fix lands on `kind`, not on `isHostQueryRequest`, which `README.md` calls the safety
|
|
214
|
+
surface: a mangled `Q` still reports `false` there, because the parser genuinely did not read a
|
|
215
|
+
query. `false` therefore means "no query was read", never "this is a result set", and both the
|
|
216
|
+
type doc and the quickstart now say so and point at `kind`.
|
|
217
|
+
|
|
218
|
+
- **BREAKING: a profile may no longer tolerate `ASTM_RECORD_UNKNOWN_TYPE`**
|
|
219
|
+
(`ASTM-UNKNOWN-RECORD-REMERGE`). The code moves off the profile safety gate's tolerable
|
|
220
|
+
allow-list and onto the safety-critical set, so `defineAstmProfile({ tolerate: [...] })` naming
|
|
221
|
+
it now throws `AstmProfileDefinitionError` at definition time instead of building a profile, and
|
|
222
|
+
the gate is re-checked when a warning would be downgraded, so a profile assembled as a plain
|
|
223
|
+
object rather than through the factory gets the same answer (it declines to downgrade rather than
|
|
224
|
+
throwing, so a hand-authored profile still parses, it just cannot quiet a safety-critical code).
|
|
225
|
+
`TOLERABLE_CODES` goes from four members to three (`ASTM_NONSTANDARD_DELIMITERS`,
|
|
226
|
+
`ASTM_UNKNOWN_ESCAPE_SEQUENCE`, `ASTM_RECORD_UNINTERPRETED_QUERY_STATUS`) and
|
|
227
|
+
`SAFETY_CRITICAL_CODES` / `isSafetyCriticalCode()` answer accordingly. No built-in profile
|
|
228
|
+
tolerated the code, so nothing in this package changes shape; a consumer profile that named it
|
|
229
|
+
will now fail to define, loudly, at the point of definition.
|
|
230
|
+
|
|
231
|
+
**Why, and it is a wrong-patient path.** A message runs from an `H` header to its `L`
|
|
232
|
+
terminator, and `messages()` decides where one starts by reading each record's **type letter**.
|
|
233
|
+
A header the reader cannot recognize as a header (one carrying a stray leading byte, for
|
|
234
|
+
instance) is surfaced as an unsupported record, opens no message, and the two messages either
|
|
235
|
+
side of it are grouped as one. The merged message pairs a patient with results that arrived
|
|
236
|
+
under a different header. The `ASTM_RECORD_UNKNOWN_TYPE` warning is the entire report that this
|
|
237
|
+
happened, so a profile downgrading it to `PROFILE_QUIRK_APPLIED` quieted the whole signal, and a
|
|
238
|
+
`{ strict: true }` parse then accepted the stream with no objection at all.
|
|
239
|
+
|
|
240
|
+
**The type letter turned out to have two load-bearing readers, not one.** `classifyMessage`
|
|
241
|
+
counts `Q` / `R` / `O` records by the same letter, and the rule that a message carrying a query
|
|
242
|
+
is never read as a result set is stated on that count. An unrecognized `Q` costs a host-query
|
|
243
|
+
request that guarantee: the message reads as `kind: "results"`, and the
|
|
244
|
+
`ASTM_RECORD_AMBIGUOUS_MESSAGE_KIND` warning that flags a query-plus-result contradiction
|
|
245
|
+
disappears along with the `Q` it was counting. There too `ASTM_RECORD_UNKNOWN_TYPE` is the only
|
|
246
|
+
warning left on the stream. Both readers are measured in the regression test. The
|
|
247
|
+
mis-classification itself is not fixed here, for the same reason the merge is not: it is a
|
|
248
|
+
property of reading the letter, and inferring the intended letter is the guess this package
|
|
249
|
+
declines to make.
|
|
250
|
+
|
|
251
|
+
**The variant that reaches furthest carries no second patient.** When the merged second message
|
|
252
|
+
has its own `P`, the merged message holds two and the multi-patient guard throws. When the
|
|
253
|
+
second message is **result-only**, the merged message still holds exactly one `P`, so `patient()`
|
|
254
|
+
and `results()` both answer confidently and the first patient silently acquires the second
|
|
255
|
+
message's results. That is the fixture the regression test pins.
|
|
256
|
+
|
|
257
|
+
**The parser is deliberately unchanged.** Recognizing a mangled header _as_ a header means
|
|
258
|
+
guessing at a byte the sender did not send, and that guess would split a stream a different way
|
|
259
|
+
just as silently. A lenient parse still merges and still warns; a strict parse refuses; and the
|
|
260
|
+
refusal can no longer be configured away. `messages()`, the warning's own message text, the
|
|
261
|
+
`UnsupportedRecord` type, the README and the quickstart all now say to read this code as a
|
|
262
|
+
possible lost message boundary rather than as cosmetic noise.
|
|
263
|
+
|
|
264
|
+
**The admission test for the allow-list gained a second clause**, which is the durable part of
|
|
265
|
+
this change. A code was admitted on one question: can it alter, drop, or fabricate an extracted
|
|
266
|
+
value? `ASTM_RECORD_UNKNOWN_TYPE` still passes that one, and it was still wrong to tolerate,
|
|
267
|
+
because the condition it reports had quietly become something the library reads. The rule is now
|
|
268
|
+
that a code is tolerable only while **nothing else in this package reads the condition the
|
|
269
|
+
warning reports**, and that second clause is a claim about the whole library which can stop being
|
|
270
|
+
true without anyone editing `safety.ts`. It is a review obligation, not a mechanical check, and
|
|
271
|
+
the file says so rather than implying a gate that does not exist. The three surviving codes were
|
|
272
|
+
each re-derived against both readers of record structure, and each is measured by comparing the
|
|
273
|
+
same logical stream with the reported condition present and absent. That is deliberately not a
|
|
274
|
+
comparison of a parse with a profile against one without: the profile transform runs after the
|
|
275
|
+
records are built, so the second comparison is identical for every code and would have passed for
|
|
276
|
+
the one just removed. The comparison actually used is exercised on the mangled-header pair as a
|
|
277
|
+
negative control, where it has to fail.
|
|
278
|
+
|
|
279
|
+
**Evidence, labelled.** The `H` … `L` message unit is _verified primary_ and its grounding is
|
|
280
|
+
recorded on `messages()`, unchanged here. The allow-list itself is **this library's own policy,
|
|
281
|
+
not the standard's**: no clause governs which of our warning codes a profile may downgrade, and
|
|
282
|
+
none is claimed for it. The behaviour above (the merge, the accessors answering, the strict
|
|
283
|
+
acceptance under a tolerating profile) is _measured_, on the tree before and after the change.
|
|
284
|
+
|
|
132
285
|
- **The em dash (U+2014) is gone from every tracked file, and a CI gate keeps it out**
|
|
133
286
|
(`EMDASH-CONFORMANCE`). The brand rule bans the character outright across every cosyte surface
|
|
134
287
|
and names commit messages explicitly; this package was the last one it had not reached, and the
|
|
@@ -225,6 +378,68 @@ phase 8` passes while `Phase 8` reds). An arm keyed on a following digit was wri
|
|
|
225
378
|
|
|
226
379
|
### Fixed
|
|
227
380
|
|
|
381
|
+
- **One unescaped `&` in a value no longer swallows every field after it**
|
|
382
|
+
(`ASTM-UNESCAPED-ESCAPE-SWALLOWS-TAIL`; `PRE-EXISTING`, shipped `0.0.1` through `0.0.8`, not a
|
|
383
|
+
regression). The escape codec read an escape character with no closing escape character as the
|
|
384
|
+
opening of a sequence and copied from it to the end of the record, so every field after it merged
|
|
385
|
+
into the field it sat in, **and there was no warning code for the condition at all**. Measured on
|
|
386
|
+
`064c078`: `R|1|^^^687|28.6&|U/L||N||F` read back `value = "28.6&|U/L||N||F"` with `units`,
|
|
387
|
+
`abnormalFlags` and `flag` all `undefined` and `status` `unspecified` rather than `final`, on a
|
|
388
|
+
stream declaring the canonical `H|\^&`, with `warnings: []`. On the patient side
|
|
389
|
+
`P|1||LAB-0001||O&BRIEN^JANE||19800101|F` read the surname as the whole rest of the record and
|
|
390
|
+
came back with **no birth date and no sex**, again silently. A result that reads `28.6` with its
|
|
391
|
+
units gone is the harm this package exists to prevent, and an ampersand in a surname is not exotic.
|
|
392
|
+
|
|
393
|
+
**The round trip made it worse rather than surfacing it.** Emit re-escaped the merged text into
|
|
394
|
+
`R|1|^^^687|28.6&E&&F&U/L&F&&F&N&F&&F&F`: a spec-clean-looking line that re-parsed to the same
|
|
395
|
+
wrong value, with zero warnings, byte-stable across further trips. Silent re-read divergence, with
|
|
396
|
+
no malformed delimiter set anywhere in it.
|
|
397
|
+
|
|
398
|
+
**The fix is a bound, and it infers nothing.** An escape sequence is now exactly three characters
|
|
399
|
+
(the escape character, **one** body character, the escape character), which is all the four
|
|
400
|
+
mnemonics `&F&` `&S&` `&R&` `&E&` ever need, and both `splitEscapeAware` and `decodeEscapes` share
|
|
401
|
+
that one definition. An escape character that heads no such sequence is read as the **literal
|
|
402
|
+
character it is**: the value keeps the byte that arrived, it opens no atom, and nothing is
|
|
403
|
+
invented to close a sequence the sender did not open. The same fixture now
|
|
404
|
+
reads `value = "28.6&"` with `units: "U/L"` and `status.meaning: "final"`, and the patient keeps
|
|
405
|
+
their birth date and sex. Emit writes the literal character as `&E&`, so the emitted line is
|
|
406
|
+
spec-clean **and** structurally faithful, and re-parses to the same tree.
|
|
407
|
+
|
|
408
|
+
The bound also removes a non-local behavior: under the unbounded scan, whether a field kept its
|
|
409
|
+
value depended on whether some **later** field in the record happened to carry an escape character
|
|
410
|
+
too, which paired across the field separator between them. Two bare ampersands in one record now
|
|
411
|
+
cost nothing.
|
|
412
|
+
|
|
413
|
+
New warning code `ASTM_UNPAIRED_ESCAPE_CHARACTER` and exported factory `unpairedEscapeCharacter`
|
|
414
|
+
report each such character, one per occurrence, positioned on the record and field, carrying no
|
|
415
|
+
field text (PHI discipline). The code is **tolerable**: a profile may expect it, because reading
|
|
416
|
+
the character as a literal is unconditional and the parsed value is byte-identical with the profile
|
|
417
|
+
and without it. Untolerated, `{ strict: true }` refuses.
|
|
418
|
+
|
|
419
|
+
**The scope is the bare character, and only that.** The atom rule is unchanged, so an `&X&`
|
|
420
|
+
sequence whose body **is** a delimiter still swallows that delimiter and still costs the value, the
|
|
421
|
+
units and the status together: `R|1|^^^687|28.6&|&U/L||||F` reads `28.6&|&U/L` with no units and
|
|
422
|
+
status `unspecified`. It is never silent, but its only report is `ASTM_UNKNOWN_ESCAPE_SEQUENCE`,
|
|
423
|
+
which is tolerable, so a profile expecting that code lets `{ strict: true }` accept it. That case
|
|
424
|
+
is recorded as a known defect and deliberately not closed here: narrowing the atom would break the
|
|
425
|
+
guarantee it exists for, which is that `&F&` stays one token under a set naming `F` as a delimiter.
|
|
426
|
+
Both halves are pinned, so neither can drift.
|
|
427
|
+
|
|
428
|
+
`ASTM_UNKNOWN_ESCAPE_SEQUENCE` is unchanged for a single unrecognized body (`&Z&`). A multi-
|
|
429
|
+
character body is no longer treated as one atom: its bytes are all preserved, but a **delimiter
|
|
430
|
+
inside** such a body now splits, which moves every field after it, and the report becomes one
|
|
431
|
+
unpaired-character warning per loose escape character instead of one unknown-sequence warning. Over
|
|
432
|
+
an exhaustive corpus of bodies up to four characters in a result-value slot, that is 1,085 records
|
|
433
|
+
read correctly where the previous release read them wrongly, against 27 read differently in the
|
|
434
|
+
other direction.
|
|
435
|
+
|
|
436
|
+
Measured red on base: 10 of the 18 new tests fail against `064c078` extracted into a clean tree.
|
|
437
|
+
Of the 8 that pass, 6 measure behavior this change does not touch and 2 are the boundary pins on
|
|
438
|
+
what it deliberately does **not** fix.
|
|
439
|
+
`test/records/unseparated-fields.test.ts` had pinned this loss as a documented limit of
|
|
440
|
+
`ASTM_RECORD_FIELDS_UNSEPARATED`; that pin is inverted rather than deleted, and the prose stating
|
|
441
|
+
the limit moved with it in `README.md`, the quickstart and the warning code's own docs.
|
|
442
|
+
|
|
228
443
|
- **Emit no longer writes a stream it cannot read back** (`ASTM-EMIT-RESIDUALS`; both gaps shipped
|
|
229
444
|
`0.0.1` through `0.0.3`, `PRE-EXISTING`, neither a regression). Two residual emit-side holes, both
|
|
230
445
|
recorded and deliberately deferred by `ASTM-MIXED-DELIMITER-EMIT` (#21) and again by
|
package/README.md
CHANGED
|
@@ -42,11 +42,14 @@ reference parser, [`@cosyte/hl7`](https://github.com/cosyte/hl7).
|
|
|
42
42
|
and lost.
|
|
43
43
|
- **Vendor profiles.** `defineAstmProfile()` builds a provenance-backed profile whose tolerances
|
|
44
44
|
downgrade _expected_, non-safety-critical deviations to a `PROFILE_QUIRK_APPLIED` warning without
|
|
45
|
-
ever altering a value, behind a
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
ever altering a value, behind a safety gate that refuses to tolerate any result value, flag,
|
|
46
|
+
status, range, or units warning, any patient or comment context, any message-kind ambiguity, any
|
|
47
|
+
unrecognized record type, and any frame or transport integrity warning. The gate runs when a
|
|
48
|
+
profile is defined **and** again when a warning would be downgraded, so a profile assembled as a
|
|
49
|
+
plain object rather than through `defineAstmProfile()` gets the same answer. A profile can never
|
|
50
|
+
make a bad checksum "ok", a cancelled result read "final", or quiet the warning that says a
|
|
51
|
+
message boundary went unrecognized. Named per-vendor profiles await a public, vendor-attributed
|
|
52
|
+
quirk document.
|
|
50
53
|
- **Terminology, bring your own.** `applyLivd(msg, catalog)` maps an analyzer's local test code to a
|
|
51
54
|
LOINC from a consumer-supplied IICC LIVD catalog as an additive, advisory annotation that never
|
|
52
55
|
mutates the raw code or value and never guesses a LOINC. No LOINC, SNOMED, or LIVD dictionary is
|
|
@@ -140,6 +143,30 @@ produce a confident wrong value: an embedded escaped delimiter reads as one comp
|
|
|
140
143
|
record type is surfaced (never dropped), and a missing unit is flagged (never defaulted). A
|
|
141
144
|
`{ strict: true }` mode escalates every tolerated deviation to a thrown error.
|
|
142
145
|
|
|
146
|
+
### An unescaped ampersand does not cost you the rest of the record
|
|
147
|
+
|
|
148
|
+
An escape sequence is the escape character, **one** body character, and the escape character again
|
|
149
|
+
(`&F&` `&S&` `&R&` `&E&`). An escape character that heads no such sequence is not an escape: it is
|
|
150
|
+
read as the literal character it is, it opens no atom, and `ASTM_UNPAIRED_ESCAPE_CHARACTER` reports
|
|
151
|
+
it. So `R|1|^^^687|28.6&|U/L||N||F` reads a value of `28.6&` with units `U/L` and status `final`,
|
|
152
|
+
and `O&Brien` in a surname keeps the patient's birth date and sex.
|
|
153
|
+
|
|
154
|
+
The parser does not decide what the sender meant by the character: it keeps the byte that arrived
|
|
155
|
+
and says so. The spec-clean way to send a literal escape character is `&E&`, which is what this
|
|
156
|
+
package's serializer emits, so a stream it produced never trips the code. The code is **tolerable**,
|
|
157
|
+
so a vendor profile can expect it on a feed that sends bare ampersands and still parse
|
|
158
|
+
`{ strict: true }`.
|
|
159
|
+
|
|
160
|
+
**One escape shape still costs a field boundary, and it is a different code.** A real three-character
|
|
161
|
+
sequence is opaque by design, which is what keeps `&F&` one token under a set that names `F` as a
|
|
162
|
+
delimiter. So where the body is itself a delimiter (`&|&` under the canonical set) that delimiter
|
|
163
|
+
does not split, and every field after it shifts: `R|1|^^^687|28.6&|&U/L||||F` reads a value of
|
|
164
|
+
`28.6&|&U/L` with no units and status `unspecified`. It is never silent, but the only report is
|
|
165
|
+
`ASTM_UNKNOWN_ESCAPE_SEQUENCE`, which is **tolerable**, so a profile expecting that code (including
|
|
166
|
+
the shipped `referenceCorpus`) will let a `{ strict: true }` parse accept it. Narrowing the atom to
|
|
167
|
+
close this would break the guarantee the atom exists for, so it is written down rather than papered
|
|
168
|
+
over.
|
|
169
|
+
|
|
143
170
|
### Several messages in one stream
|
|
144
171
|
|
|
145
172
|
A message runs from its `H` header to its `L` terminator, so a stream can carry several. `messages()`
|
|
@@ -170,6 +197,58 @@ patient is unchanged, and so is a result-only message with no `P` at all, which
|
|
|
170
197
|
`undefined`. `commentsFor()` is unchanged on every stream, because the parent record you hand it
|
|
171
198
|
already names the message.
|
|
172
199
|
|
|
200
|
+
Splitting reads each record's type letter, so check for an `ASTM_RECORD_UNKNOWN_TYPE` warning before
|
|
201
|
+
you trust the split. A header the reader does not recognize as a header, one carrying a stray leading
|
|
202
|
+
byte for instance, opens no message, and the messages either side of it merge back into one, so a
|
|
203
|
+
patient can end up holding results that arrived under a different header. The
|
|
204
|
+
parser warns on that record and a `{ strict: true }` parse refuses the stream. That warning is the
|
|
205
|
+
only report the merge produces, so a profile is not allowed to tolerate it: the code is refused when
|
|
206
|
+
a profile is defined, and a warning carrying it is not downgraded whatever profile is in force. Do
|
|
207
|
+
not gate on the warning count, though, because the records that merged in can raise warnings of
|
|
208
|
+
their own.
|
|
209
|
+
|
|
210
|
+
Delimiters are re-read at each header too, so if the unrecognized one declared a different set, the
|
|
211
|
+
records after it are read with the previous set and their fields can be lost rather than merely
|
|
212
|
+
misfiled. `ASTM_RECORD_FIELDS_UNSEPARATED` reports a record that suffered the total form of that:
|
|
213
|
+
the delimiters in force found no field separator in it at all, so the whole line read back as one
|
|
214
|
+
field and none of its modeled fields survived. On a result record that is the value, the units and
|
|
215
|
+
the status at once, so treat it as a lost result, not a formatting nit. The fields are never
|
|
216
|
+
reconstructed, because the set the sender used is unknown and guessing at it would invent data. The
|
|
217
|
+
code is safety-critical, and it does not need a mangled header to fire: a lone record written in
|
|
218
|
+
another set trips it too.
|
|
219
|
+
|
|
220
|
+
**Its absence does not certify that a record was read in its own set**, and this is the important
|
|
221
|
+
half. The check tests one of the four delimiter roles, the **field** separator, and only in its
|
|
222
|
+
total form, where no unescaped separator occurs in the line. Two classes of the same loss sit
|
|
223
|
+
outside it:
|
|
224
|
+
|
|
225
|
+
- A foreign set whose **field** separator happens to occur somewhere in the line still splits, on
|
|
226
|
+
the wrong boundaries and in silence. A single stray `|` in an otherwise `*`-separated result loses
|
|
227
|
+
the value, the units and the status with no warning at all, while the identical record without
|
|
228
|
+
that one byte is reported. This also happens **inside** a run of these warnings, so even a run
|
|
229
|
+
does not mean every record in it was checked.
|
|
230
|
+
- A set differing in the **repeat, component or escape** role usually splits into fields normally,
|
|
231
|
+
and the damage then varies. A mis-split component can cost a test identity while the value and
|
|
232
|
+
units survive. The escape role's worst case has **narrowed, not gone**: a bare escape character no
|
|
233
|
+
longer merges the rest of the record (it reads as a literal and raises
|
|
234
|
+
`ASTM_UNPAIRED_ESCAPE_CHARACTER`), but an `&X&` sequence whose body is a delimiter is an opaque
|
|
235
|
+
atom, so that delimiter does not split and the value, the units and the status can still go
|
|
236
|
+
together. That one is reported only by the tolerable `ASTM_UNKNOWN_ESCAPE_SEQUENCE`.
|
|
237
|
+
|
|
238
|
+
All are accepted limits, for two different reasons: widening the field-separator check would mean
|
|
239
|
+
deciding which set a record ought to have had, which is the same guess the parser declines to make
|
|
240
|
+
elsewhere, and narrowing the escape atom would break the guarantee it exists for. So they are
|
|
241
|
+
written down rather than papered over. Read the warning as "this record definitely lost its
|
|
242
|
+
fields", never as "no other record did". If
|
|
243
|
+
delimiter drift is a real risk on your feed, parse with `{ strict: true }`, which refuses both an
|
|
244
|
+
outright collapse and an unrecognized type letter, and treat `ASTM_RECORD_UNKNOWN_TYPE` as
|
|
245
|
+
invalidating what follows it rather than expecting this warning to enumerate the damage.
|
|
246
|
+
|
|
247
|
+
An unrecognized type letter also makes the message **kind** unknowable, because the letter that
|
|
248
|
+
could not be read may have been the very `Q` that decides it. `classification.kind` is
|
|
249
|
+
`indeterminate` in that case rather than `results` or `orders`, and `classification.hasUnrecognized`
|
|
250
|
+
says why. A `Q` that was read still wins outright.
|
|
251
|
+
|
|
173
252
|
## Map local codes to LOINC (LIVD, bring-your-own)
|
|
174
253
|
|
|
175
254
|
An analyzer sends a proprietary local test code in the Universal Test ID; a standard LOINC is mapped
|