@cosyte/astm 0.0.4 → 0.0.5

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 CHANGED
@@ -11,7 +11,7 @@ this file is maintained by hand (Changesets handles the version bump and publish
11
11
 
12
12
  ### Added
13
13
 
14
- - **`messages()` — read a stream as the sequence of messages it actually is**
14
+ - **`messages()`: read a stream as the sequence of messages it actually is**
15
15
  (`ASTM-PATIENT-RESULT-MISATTRIBUTION`). A parsed model has always been a whole record
16
16
  **stream**, and a stream may carry several messages back to back: a message runs from an `H`
17
17
  header to its `L` terminator. `messages(msg)` splits the stream into them and returns a
@@ -37,17 +37,17 @@ this file is maintained by hand (Changesets handles the version bump and publish
37
37
  being dropped. Grouping is **total**: every record lands in exactly one message, none
38
38
  duplicated and none lost, which is asserted directly.
39
39
 
40
- **Evidence, labelled.** The message unit is `H` … `L` — _verified primary_: CLSI LIS02-A2 §2 is
40
+ **Evidence, labelled.** The message unit is `H` … `L`, _verified primary_: CLSI LIS02-A2 §2 is
41
41
  definitional about the unit itself, bounding a message by the `H` record at one end and the `L`
42
42
  record at the other. Read directly in CLSI's own free sample, and stated here in our own words
43
43
  on purpose: we may read and cite the standard but never reproduce its prose, and this file ships
44
44
  inside the npm tarball. That clause is the whole of what is claimed from the standard here, and
45
45
  it does **not** settle which record _opens_ a new scope partway through a stream. So treating a
46
- second `H` as the start of the next message is recorded as a **reasoned choice, not a citation**
47
- — no clause number is claimed for it, and we do not assert the standard is silent either. It is
46
+ second `H` as the start of the next message is recorded as a **reasoned choice, not a citation**:
47
+ no clause number is claimed for it, and we do not assert the standard is silent either. It is
48
48
  chosen to agree with the delimiter scoping this parser already enforces, and because it drops no
49
49
  record. **The OSS corpus was checked rather than dismissed**, and unlike for delimiter questions
50
- it is informative here — but only negatively: neither commonly-cited reference implementation
50
+ it is informative here, but only negatively: neither commonly-cited reference implementation
51
51
  models the message unit at all. One dispatches records to per-type callbacks one at a time with
52
52
  no carried state, leaving the association to the integrator; the other merges every message of a
53
53
  transport session into a single envelope of flat per-record-type buckets, from which the
@@ -62,8 +62,8 @@ this file is maintained by hand (Changesets handles the version bump and publish
62
62
  message carries several) is **not** modeled: the clauses that would ground it (the
63
63
  message-level structure diagram, and the `P` sequence-number rule) are withheld from the free
64
64
  sample and paywalled, so the layer declines rather than inventing a rule. Multi-patient
65
- messages are real — at least one openly-published vendor interface grammar makes the patient
66
- group repeatable on the download direction — so this is a deferral with a known shape, not a
65
+ messages are real, at least one openly-published vendor interface grammar makes the patient
66
+ group repeatable on the download direction, so this is a deferral with a known shape, not a
67
67
  claim that the case does not arise, and not a claim that the standard is silent about it.
68
68
 
69
69
  - **`pnpm check:no-internal-refs` + a `no-internal-refs` CI job** (`PUBLIC-SURFACE-HYGIENE`,
@@ -103,7 +103,7 @@ phase 8` passes while `Phase 8` reds). An arm keyed on a following digit was wri
103
103
  produces output identical to a tool that read it and found it clean. This is not theoretical: the
104
104
  development container interposes a `grep` with `-I` forced, which drops such input without a word.
105
105
  An interposed shell function is also `unset` rather than assumed absent. Both directions are
106
- proven — an `-I`-forcing `grep` ahead of it on `PATH` makes the gate refuse, and an exported `grep`
106
+ proven: an `-I`-forcing `grep` ahead of it on `PATH` makes the gate refuse, and an exported `grep`
107
107
  function is neutralised. The companion `-G` (basic-regex) hazard needs no separate guard: every
108
108
  positive self-test uses alternation, so a BRE-forced `grep` fails them and refuses.
109
109
 
@@ -115,18 +115,51 @@ phase 8` passes while `Phase 8` reds). An arm keyed on a following digit was wri
115
115
 
116
116
  ### Changed
117
117
 
118
+ - **The em dash (U+2014) is gone from every tracked file, and a CI gate keeps it out**
119
+ (`EMDASH-CONFORMANCE`). The brand rule bans the character outright across every cosyte surface
120
+ and names commit messages explicitly; this package was the last one it had not reached, and the
121
+ only one where it did not arrive on a clean tree. **1,129 occurrences across 108 of the 142
122
+ tracked files** were rewritten with a comma, a colon, or a period. The sweep and the gate are one
123
+ change on purpose: a sweep with no gate grows back, and a gate with no sweep reds CI on arrival.
124
+
125
+ **What a consumer can observe.** The npm `description` no longer carries the character. The pages
126
+ published to docs.cosyte.com are rewritten, including the title of the limitations page, now
127
+ `What it does, and does not do`. Warning and error `message` text is repunctuated, and a
128
+ consumer's log prints that text. **No warning or fatal code changed**: codes are the stable
129
+ contract, and the snapshot tests that pin them are untouched. No runtime behaviour changed; the
130
+ parser, the frame codec, the LTP reducer, and the emit path are not part of this change.
131
+
132
+ **Two findings worth carrying, because neither is visible from a diff.** First, an em dash can be
133
+ a semantic **value**: `docs-content/architecture.md`'s layer table used a bare dash in the
134
+ **Standard** column to mean _this layer is governed by no standard_, and a bulk rewrite turned it
135
+ into a stray colon, so the cell read "unstated" rather than "none" on the page whose job is honest
136
+ disclosure. Nothing in CI could have caught that; it is now the explicit word `None`. Second, rewriting the
137
+ separator in the warning registries as a colon turned `test/records/multi-header-delimiters.test.ts`
138
+ red: that fixture declares `:` as its **component** delimiter, and the test asserts that no warning
139
+ message contains one of its delimiter characters. All 22 registry messages separate with a comma
140
+ now. The invariant that assertion really pins is that a warning message is a **constant carrying no
141
+ field data**, which is what makes it value-free; a comma is not safer than a colon in principle,
142
+ because ASTM delimiters self-declare and any character can be one. The existing test caught this,
143
+ not a reviewer.
144
+
145
+ **Where the rewrite needed a rewrite, not a substitution.** Where the character had bracketed an
146
+ aside that itself contains commas, replacing both ends with commas turns the aside into an
147
+ indistinguishable list item, and several sentences went false that way. Those now use
148
+ **parentheses**, which is the fourth option the rule always allowed and the one a mechanical
149
+ substitution never reaches for.
150
+
118
151
  - **BREAKING: `patient()`, `results()`, `orders()`, `comments()`, and `query()` now throw on a
119
152
  stream they cannot answer for, instead of answering across patients**
120
153
  (`ASTM-PATIENT-RESULT-MISATTRIBUTION`; shipped `0.0.1` through `0.0.3`).
121
154
 
122
155
  **The defect.** These accessors read the whole stream. On a stream carrying more than one
123
156
  message, `patient()` answered with the **first** `P` in the stream while `results()` answered
124
- with **every** `R` in it, so pairing them — which is exactly what this package's one-line north
125
- star does — attributed one patient's results to another. It needed no delimiter redeclaration
157
+ with **every** `R` in it, so pairing them, which is exactly what this package's one-line north
158
+ star does, attributed one patient's results to another. It needed no delimiter redeclaration
126
159
  and no unusual delimiters: an ordinary two-message stream in one canonical set reproduced it,
127
160
  with **zero** warnings, and `strict` mode raised no objection either. On an analyzer-to-LIS
128
- path that is a result filed against the wrong patient. Nothing was mis-_read_ — every `P` and
129
- every `R` was correct in `records` — but there was no way to ask which belonged together.
161
+ path that is a result filed against the wrong patient. Nothing was mis-_read_, every `P` and
162
+ every `R` was correct in `records`, but there was no way to ask which belonged together.
130
163
 
131
164
  **The change.** Those five accessors now throw `AstmAmbiguousStreamError` with code
132
165
  `ASTM_AMBIGUOUS_MULTI_MESSAGE` when the stream carries more than one message. `patient()` also
@@ -145,8 +178,8 @@ phase 8` passes while `Phase 8` reds). An arm keyed on a following digit was wri
145
178
  **Which callers are affected, stated exactly.** A stream that is one message carrying at most one
146
179
  `P` is **unchanged**, and so is a result-only message with no `P` at all: `patient()` still answers
147
180
  `undefined` there, which is an ordinary shape and not an error. But `ASTM_AMBIGUOUS_MULTI_PATIENT`
148
- **does reach single-message callers** — a lone message carrying several patients used to answer
149
- with the first of them and now refuses — so "single-message streams are unaffected" would be false
181
+ **does reach single-message callers**, a lone message carrying several patients used to answer
182
+ with the first of them and now refuses, so "single-message streams are unaffected" would be false
150
183
  and is not claimed. That second break is the same wrong-patient guess as the first, one level down,
151
184
  and it is disclosed on the README and in the docs for the same reason the first one is.
152
185
  `commentsFor()` is unchanged and works on any stream, single- or multi-message, because the parent
@@ -155,7 +188,7 @@ phase 8` passes while `Phase 8` reds). An arm keyed on a following digit was wri
155
188
  **Migrating** is mechanical: replace `results(msg)` and `patient(msg)` with a walk over
156
189
  `messages(msg)`, reading each message's own `patient` and `results`. Where a caller genuinely
157
190
  wants every result in a stream regardless of who they belong to, that is
158
- `messages(msg).flatMap((m) => m.results)` — written out, so it is a choice rather than a
191
+ `messages(msg).flatMap((m) => m.results)`: written out, so it is a choice rather than a
159
192
  default.
160
193
 
161
194
  ### Fixed
@@ -168,7 +201,7 @@ phase 8` passes while `Phase 8` reds). An arm keyed on a following digit was wri
168
201
  the same way after; neither was half-fixed by the two earlier slices.
169
202
 
170
203
  **1. A delimiter declaration longer than three characters lost its extra bytes.** Three characters
171
- of a header's declaration carry a role — repeat, component, escape, by position — and the reader
204
+ of a header's declaration carry a role (repeat, component, escape, by position) and the reader
172
205
  ignores any beyond them rather than refusing the stream. Emit regenerated the declaration from the
173
206
  three roles alone, so a header that arrived as `H|\^&#` went back out as `H|\^&`, with no warning
174
207
  and no way for a caller to notice. Measured before the fix: `serializeAstmRecords(msg, msg.delimiters)`
@@ -176,33 +209,33 @@ phase 8` passes while `Phase 8` reds). An arm keyed on a following digit was wri
176
209
  byte-exact.
177
210
 
178
211
  The surplus is now carried through from the modeled declaration field. Of the three available
179
- dispositions — preserve, refuse, report — **preserve** was chosen. Refusing would reject a stream
212
+ dispositions (preserve, refuse, report), **preserve** was chosen. Refusing would reject a stream
180
213
  the parser reads without complaint, on a published package, over bytes it has already decided are
181
214
  inert. Reporting is not available at all: emit returns a bare string and has no warning channel, so
182
215
  a warning could only be ignored while the truncated stream still shipped, which is the same reason
183
216
  #21 chose re-encoding. Preserving costs nothing and is the only one of the three that makes the
184
- round-trip byte-exact. What the surplus _means_ remains unresolved — the clauses that would settle
217
+ round-trip byte-exact. What the surplus _means_ remains unresolved (the clauses that would settle
185
218
  it are not in the freely published material and were not read, so no clause is cited here in either
186
- direction — but carrying bytes through unread is a strictly smaller claim than deleting them, and it
219
+ direction) but carrying bytes through unread is a strictly smaller claim than deleting them, and it
187
220
  stays coherent with a reader that now scopes delimiters forward from every header: the re-read
188
221
  declaration resolves to the same four roles either way. This lands on the **default** canonical
189
- path too, not only when a set is passed explicitly — normalizing a message replaces the four
190
- delimiter roles, and the surplus holds none of them — so a caller who was relying on emit to strip
222
+ path too, not only when a set is passed explicitly (normalizing a message replaces the four
223
+ delimiter roles, and the surplus holds none of them) so a caller who was relying on emit to strip
191
224
  those bytes will find it no longer does.
192
225
 
193
- The surplus is dropped in exactly the two cases where it could not be read back as surplus — the
226
+ The surplus is dropped in exactly the two cases where it could not be read back as surplus: the
194
227
  header is being transcoded into a different delimiter set, so the surplus belonged to the
195
228
  declaration being replaced; or the surplus is not inert on the wire, meaning it contains the field
196
229
  separator or **any control character**. The control rule is deliberately wider than the record
197
230
  layer alone needs. A `CR`/`LF` would end the record and shift every data field along, but this text
198
231
  also reaches the **frame** layer through `serializeFramedAstm`, where `STX`, `ETX` and `ETB` are
199
232
  structural: a surplus carrying one of those truncated the frame body, and re-reading the framed
200
- stream then dropped the **entire header record** — its sender, its receiver, its control ID —
233
+ stream then dropped the **entire header record** (its sender, its receiver, its control ID)
201
234
  behind nothing but an `ASTM_FRAME_BAD_CHECKSUM`. Rather than enumerate the bytes each layer happens
202
235
  to reserve, and re-derive that list whenever a layer is added, no control character is carried at
203
236
  all. Two things that rule does **not** do, said here rather than left to be found: it is keyed on
204
237
  the character while the frame layer's structure is keyed on the low byte (`charCodeAt(i) & 0xff`),
205
- so a non-control character truncating onto `STX`/`ETX`/`ETB` still breaks framing — loudly, with a
238
+ so a non-control character truncating onto `STX`/`ETX`/`ETB` still breaks framing, loudly, with a
206
239
  typed error or an unknown-record-type warning, never silently; and refusing a control character in
207
240
  a _surplus_ does not mean one cannot be a _delimiter role_, since only `CR`/`LF` are refused
208
241
  there. Those are
@@ -213,26 +246,26 @@ phase 8` passes while `Phase 8` reds). An arm keyed on a following digit was wri
213
246
  multi-character field separator, an empty escape character, a `field`/`escape` collision and a `CR`
214
247
  separator each emitted a stream `parseAstmRecords` then **threw** on; a `repeat`/`component`
215
248
  collision and a `component`/`escape` collision each emitted one it **re-read with a different field
216
- tree and zero warnings**. An empty escape additionally garbled the values themselves — `28.6` went
249
+ tree and zero warnings**. An empty escape additionally garbled the values themselves: `28.6` went
217
250
  out as `2E8E.E6`. On the analyzer-to-LIS path a field tree that changes under a re-read is a lost
218
251
  result or a lost specimen identifier, and the caller had no signal either way.
219
252
 
220
253
  `d` is now checked before any bytes are written, on all four public emit entry points
221
254
  (`serializeAstmRecords`, `serializeAstmRecord`, `serializeField`, `encodeComponent`): each separator
222
255
  exactly one character, none a `CR`/`LF`, no two the same character. A failing set is an
223
- `AstmSerializeError` with the new code `ASTM_EMIT_INVALID_DELIMITERS` — including a set that omits a
256
+ `AstmSerializeError` with the new code `ASTM_EMIT_INVALID_DELIMITERS`: including a set that omits a
224
257
  member or holds a non-string, which the types forbid but a JavaScript caller can still pass, and
225
258
  which previously surfaced as a raw `TypeError` from inside the serializer.
226
259
 
227
260
  **The three rules are necessary, not sufficient, and the docs say so rather than implying a
228
261
  guarantee.** A set can satisfy all three and still emit a stream that reads back wrong: a separator
229
262
  equal to a record's type letter (`field` of `R`) makes the type letter itself get escaped away, and
230
- the record re-reads as unsupported with its result lost. That corruption is unchanged by this slice
231
- — it reproduces byte-identically before it — and is recorded as a known defect rather than fixed
263
+ the record re-reads as unsupported with its result lost. That corruption is unchanged by this slice,
264
+ it reproduces byte-identically before it, and is recorded as a known defect rather than fixed
232
265
  here, because the rule that would catch it has to be derived rather than guessed. **A typed error rather than a
233
266
  warning, and the house rule from #21 is why**: with no warning channel on a `string` return, refusing
234
267
  at the call is the only disposition that reaches the caller at all. The counter-argument was weighed
235
- and is real — `@cosyte/astm` is published, and refusing rejects input previously accepted. It is
268
+ and is real: `@cosyte/astm` is published, and refusing rejects input previously accepted. It is
236
269
  accepted deliberately, because the input turned away is exactly the input that was being corrupted,
237
270
  and because the three rules are not stylistic: each names a case where the emitted bytes provably do
238
271
  not read back as the records that produced them. The narrowing has one consequence worth stating
@@ -260,49 +293,49 @@ phase 8` passes while `Phase 8` reds). An arm keyed on a following digit was wri
260
293
  delimiters for the records that follow it. Delimiters were read only from `records[0]` and applied
261
294
  to the whole stream, so every record after a redeclaring header **collapsed into a single field,
262
295
  with zero warnings**, and `{ strict: true }` accepted it. Measured by execution on `dc09a3a` before
263
- the fix: in a two-message stream whose second header declared `*~:#`, the second result — value
264
- `99.9`, units `mmol/L`, abnormal flag `H`, status `F` — came back with `value`, `units`, `flag` and
296
+ the fix: in a two-message stream whose second header declared `*~:#`, the second result, value
297
+ `99.9`, units `mmol/L`, abnormal flag `H`, status `F`, came back with `value`, `units`, `flag` and
265
298
  `resultStatus` **all absent** and `status` reading `unspecified`. On the analyzer↔LIS path that is a
266
299
  lost result with no signal to the caller.
267
300
 
268
301
  **The decision, and its grounding.** Delimiters are now scoped **forward from each header**: a later
269
302
  `H` governs itself and the records after it, until the next `H`. The asymmetry with the emit bug is
270
- real — on emit the library was corrupting its own output, so re-encoding was unambiguously safe,
303
+ real: on emit the library was corrupting its own output, so re-encoding was unambiguously safe,
271
304
  whereas on parse the bytes are the sender's and a second `H` is legal. What settles it is that
272
305
  forward scoping is the **only** reading that never reinterprets bytes already consumed: records
273
306
  before a redeclaration keep the set they were read with, so the two sets never disagree about the
274
307
  same bytes. "First header wins forever" is the collapse itself; "last header wins retroactively"
275
308
  would require re-reading records already delivered.
276
309
 
277
- **Evidence, labelled.** The message unit is `H` … `L` — _verified primary_: LIS02-A2 §2 makes the
310
+ **Evidence, labelled.** The message unit is `H` … `L`, _verified primary_: LIS02-A2 §2 makes the
278
311
  message a **bounded unit**, one record type opening it and another closing it, which is why a second
279
312
  `H` begins a new message rather than corrupting the current one. Read directly in CLSI's own free
280
313
  sample of the document. (Stated in our own words on purpose. We may read and cite the standard but
281
- never reproduce its prose, and `CHANGELOG.md` ships inside the npm tarball — an earlier draft of
314
+ never reproduce its prose, and `CHANGELOG.md` ships inside the npm tarball: an earlier draft of
282
315
  this entry quoted the clause verbatim and was caught in review.) That a header may follow a
283
- terminator to start another message — _verified secondary_, two independent restatements of the
316
+ terminator to start another message: _verified secondary_, two independent restatements of the
284
317
  terminator clause (Roche cobas b 121 ASTM interface description; Genaux, _Introduction to ASTM
285
318
  Message Formats_, 2024).
286
- That a header's delimiters govern "the message" — _verified secondary_ (Genaux; Stratford Software
319
+ That a header's delimiters govern "the message": _verified secondary_ (Genaux; Stratford Software
287
320
  interface spec), which leans per-message but **does not address a redeclaration that changes the
288
321
  set**. **We could not reach the normative text on that specific question**: LIS02-A2 §5.4
289
322
  (Delimiters) and §6.2 (Delimiter Definition) are precisely the clauses withheld from the free
290
323
  sample, and E1394-97 and LIS01-A2 stayed paywalled. So the forward-scoping rule is recorded as a
291
- **reasoned choice, not a citation** — no clause number is claimed for it, and we do not assert the
324
+ **reasoned choice, not a citation**: no clause number is claimed for it, and we do not assert the
292
325
  standard is silent either, because that would need the same evidence.
293
326
 
294
327
  The OSS reference corpus cannot ground this one and is reported as a negative result rather than
295
328
  dressed up: `kxepal/python-astm` and `senaite.astm` both hardcode `|\^&` as module constants, never
296
- read the header's declaration at all, and neither tracks `H` … `L` boundaries — so differential
329
+ read the header's declaration at all, and neither tracks `H` … `L` boundaries, so differential
297
330
  testing against them is uninformative here.
298
331
 
299
332
  **Behaviour.** A redeclaration that **changes** the set is honored and raises
300
333
  `ASTM_RECORD_DELIMITERS_REDECLARED`. A header that merely **restates** the set in force is a no-op
301
- and raises nothing — several messages in one delimiter set is an ordinary shape, and warning on it
334
+ and raises nothing: several messages in one delimiter set is an ordinary shape, and warning on it
302
335
  would be noise. A later header whose declaration is **unusable** (too short, or a field separator
303
336
  that also names another role) keeps the set already in force and raises
304
337
  `ASTM_RECORD_UNREADABLE_REDECLARATION`; a set is never guessed and no record is dropped. The same
305
- condition on the _first_ header remains the `ASTM_RECORD_UNDECLARED_DELIMITERS` fatal — there is no
338
+ condition on the _first_ header remains the `ASTM_RECORD_UNDECLARED_DELIMITERS` fatal: there is no
306
339
  earlier set to fall back to, and that is pinned by a test so it cannot be softened by accident.
307
340
 
308
341
  **Surface.** Two new stable warning codes (`ASTM_RECORD_DELIMITERS_REDECLARED`,
@@ -310,18 +343,18 @@ phase 8` passes while `Phase 8` reds). An arm keyed on a following digit was wri
310
343
  factories, both **safety-critical** by the profile gate's default-deny rule, so no profile can quiet
311
344
  them (asserted). `HeaderRecord.delimiters` now reports the set **that** header put into force rather
312
345
  than always the first header's; `AstmMessage.delimiters` is unchanged and stays the first header's.
313
- The sites that _stated_ the old single-header rule were swept — the `parseAstmRecords` JSDoc, the
346
+ The sites that _stated_ the old single-header rule were swept, the `parseAstmRecords` JSDoc, the
314
347
  module header, `HeaderRecord`/`AstmMessage` type docs, `readDelimiters` (whose doc comment said the
315
348
  caller escalates an unusable declaration to the fatal, true only of the first header now), and
316
- `docs-content/quickstart.md` — because on #21 the refuter's first pass was refused for exactly the
349
+ `docs-content/quickstart.md`, because on #21 the refuter's first pass was refused for exactly the
317
350
  opposite failure: correct code shipped behind documentation that steered consumers the wrong way.
318
351
  The `readDelimiters` comment also said it returns `{ ok: false }` when it has always returned
319
352
  `undefined`; that was already wrong before this slice and is corrected in the same paragraph.
320
353
 
321
354
  **Deliberately left for their own slices** (both recorded with this item, both **emit**-side, and
322
355
  both turning on questions this one does not answer): a delimiter declaration **longer than three
323
- characters** still loses its extra bytes on emit — what a fourth declaration byte even means is
324
- unresolved by the same withheld clauses — and `serializeAstmRecords(msg, d)` still does **not
356
+ characters** still loses its extra bytes on emit, what a fourth declaration byte even means is
357
+ unresolved by the same withheld clauses, and `serializeAstmRecords(msg, d)` still does **not
325
358
  validate a caller-supplied `d`**, so a malformed set emits a stream this library's own parser then
326
359
  rejects or mis-reads, with no typed error. Neither is touched here; folding an emit-side change into
327
360
  a parse-side fix would have widened the diff without answering either question.
@@ -330,7 +363,7 @@ phase 8` passes while `Phase 8` reds). An arm keyed on a following digit was wri
330
363
  over 216,699 synthetic streams (the 11 repo fixtures, 464 single-header streams, and multi-header
331
364
  pairs across a sampled sweep of delimiter sets): **394 moved accepted→rejected, 0 the other way.**
332
365
  All 11 fixtures and all 464 single-header streams were **unchanged**, in strict mode and in their
333
- lenient warning lists — which matches the code path, since the new warnings can only be raised at an
366
+ lenient warning lists, which matches the code path, since the new warnings can only be raised at an
334
367
  `H` record that is not `records[0]`.
335
368
 
336
369
  - **`serializeAstmRecords` no longer emits a mixed-delimiter stream that silently loses fields on the
@@ -339,14 +372,14 @@ phase 8` passes while `Phase 8` reds). An arm keyed on a following digit was wri
339
372
  (scientific) records byte-for-byte from `rawLine`. For a message that arrived under a vendor
340
373
  delimiter set the output was therefore **non-conformant**: a canonical header above `M`/`S` rows
341
374
  still written in the original delimiters. Re-parsing that output **collapsed every field of those
342
- rows into one, with zero warnings** — on the analyzer↔LIS path, a lost result or a lost
375
+ rows into one, with zero warnings**: on the analyzer↔LIS path, a lost result or a lost
343
376
  patient/specimen identifier with no signal to the caller. Verified by execution before and after,
344
377
  not by inspection: a five-field `M` row round-tripped to one field.
345
378
 
346
379
  **The semantics chosen, and why.** The two candidates were to re-encode every record to the
347
380
  delimiters the header declares, or to refuse/warn on a message whose records disagree.
348
- **Re-encoding was chosen.** It is what the serializer already promises — one spec-clean stream in
349
- the declared set — and a mixed-delimiter stream is not spec-clean. Emit returns a `string` and has
381
+ **Re-encoding was chosen.** It is what the serializer already promises, one spec-clean stream in
382
+ the declared set, and a mixed-delimiter stream is not spec-clean. Emit returns a `string` and has
350
383
  no warning channel, so a warning could only have been ignored while the corrupt stream still
351
384
  shipped, and a refusal would have rejected messages this library successfully parsed, a harder
352
385
  break on a published package. The invariant that had to hold either way is that **a round-trip
@@ -359,7 +392,7 @@ phase 8` passes while `Phase 8` reds). An arm keyed on a following digit was wri
359
392
  - **Editing a modeled `H` field and re-serializing no longer silently keeps the original value**
360
393
  (`PRE-EXISTING`, found in the same pass). The header was emitted by re-tokenizing its preserved
361
394
  `rawLine`, so the model was bypassed on emit. It is now emitted from `HeaderRecord.fields` like
362
- every other record type. The delimiter declaration itself is still never taken from the model — it
395
+ every other record type. The delimiter declaration itself is still never taken from the model: it
363
396
  always states the delimiters actually being emitted with, because a declaration that disagrees with
364
397
  the records around it is the very corruption above.
365
398
 
@@ -375,13 +408,13 @@ phase 8` passes while `Phase 8` reds). An arm keyed on a following digit was wri
375
408
  spurious `ASTM_UNKNOWN_ESCAPE_SEQUENCE` on some headers, which `{ strict: true }` rejected. Those
376
409
  warnings are gone, so a few messages that previously threw now parse. Measured over ~40,000
377
410
  differential messages: 1,176 moved from rejected to accepted, **0** moved the other way, and every
378
- warning delta is confined to `recordIndex 0` — no warning on any other record is added or dropped.
411
+ warning delta is confined to `recordIndex 0`, no warning on any other record is added or dropped.
379
412
 
380
413
  Three `PRE-EXISTING` neighbours were found while grading this and are deliberately **not** fixed
381
414
  here, to keep the slice the size of its item: `parseAstmRecords` reads delimiters only from the
382
415
  first header, so a **second `H` mid-stream that redeclares them** still yields the same silent
383
- field collapse (parse-side, not emit-side — the emitter faithfully reproduces what parse modeled)
384
- — **since fixed, see the parse-side entry above**;
416
+ field collapse (parse-side, not emit-side, the emitter faithfully reproduces what parse modeled),
417
+ **since fixed, see the parse-side entry above**;
385
418
  a delimiter declaration longer than three characters silently loses its extra bytes on emit; and
386
419
  `serializeAstmRecords(msg, d)` does not validate a caller-supplied `d`, so a malformed set (a
387
420
  multi-char delimiter, an empty escape, a `field`/`escape` collision) emits a stream this library's
@@ -482,12 +515,12 @@ ladder (`0.0.x` until first alpha).
482
515
  record stream and pull result value + units + flag in one line.
483
516
  - `parseAstmRecords(raw, opts?)` → an immutable, deeply-frozen `AstmMessage`; `results(msg)` /
484
517
  `patient(msg)` typed extractors.
485
- - **Delimiter self-declaration** — the four delimiters (field / repeat / component / escape) are
518
+ - **Delimiter self-declaration**: the four delimiters (field / repeat / component / escape) are
486
519
  read from each `H` record, never hardcoded, with ASTM's `\`=repeat and `&`=escape mapping.
487
- - **Escape codec** — `&F&`/`&S&`/`&R&`/`&E&` are decoded via escape-aware split-then-decode, so a
520
+ - **Escape codec**: `&F&`/`&S&`/`&R&`/`&E&` are decoded via escape-aware split-then-decode, so a
488
521
  value containing an escaped component delimiter reads as **one** component (the documented
489
522
  silent-misread class the OSS references exhibit). Re-escaping is deferred to the emit phase (P7).
490
- - Modeled records: `H` (delimiter provenance), `P` (identity — practice-assigned ID and
523
+ - Modeled records: `H` (delimiter provenance), `P` (identity, practice-assigned ID and
491
524
  laboratory-assigned ID kept **distinct**), `O` (accession + Universal Test ID), `R` (all 14
492
525
  fields; value / units / flags / status surfaced **raw**), `L`. Unknown record types surface as
493
526
  `unsupported` records with a warning, never dropped.
@@ -496,10 +529,10 @@ ladder (`0.0.x` until first alpha).
496
529
  recognition (`[OSS-derived]` field order), the deep-freeze base, and the warning/fatal registry.
497
530
  - Fatal codes: `EMPTY_INPUT` (shared), `ASTM_RECORD_NO_HEADER`, `ASTM_RECORD_UNDECLARED_DELIMITERS`.
498
531
  Warning codes: `ASTM_RECORD_UNKNOWN_TYPE`, `ASTM_NONSTANDARD_DELIMITERS`,
499
- `ASTM_UNKNOWN_ESCAPE_SEQUENCE`, `ASTM_RECORD_AMBIGUOUS_VALUE_SPLIT` — all carry stable code +
532
+ `ASTM_UNKNOWN_ESCAPE_SEQUENCE`, `ASTM_RECORD_AMBIGUOUS_VALUE_SPLIT`, all carry stable code +
500
533
  value-free positional context.
501
534
  - **Fail-safe on an unescaped component delimiter in a result value:** the full raw value and the
502
- component split are both surfaced and an `ASTM_RECORD_AMBIGUOUS_VALUE_SPLIT` warning fires — the
535
+ component split are both surfaced and an `ASTM_RECORD_AMBIGUOUS_VALUE_SPLIT` warning fires, the
503
536
  primary `value` is never silently truncated to the first component.
504
537
  - `scripts/phi-scan.ts` extended toward the P-record loci (name + DOB, delimiter-aware); synthetic
505
538
  fixtures declared in `scripts/phi-allow-list.txt`.
@@ -507,109 +540,109 @@ ladder (`0.0.x` until first alpha).
507
540
  `AstmParseError`, `AstmStrictError`, the record/value model types, and the `WARNING_CODES` /
508
541
  `FATAL_CODES` registries.
509
542
  - **Safety-critical result semantics (ASTM-2, roadmap Phase 2).** The raw `R`-record letters that
510
- Phase 1 surfaced are now modeled into fail-safe semantics, under one rule — _never a confident wrong
543
+ Phase 1 surfaced are now modeled into fail-safe semantics, under one rule: _never a confident wrong
511
544
  value_. The raw strings (`abnormalFlags`, `resultStatus`, `referenceRange`, `units`) still coexist
512
545
  with the modeled views; nothing is collapsed or reconciled.
513
546
  - **Abnormal flags (field 7) → HL7 Table 0078.** `interpretAbnormalFlag()` and the `flag` field on
514
547
  `ResultRecord` model the full value set: `L`/`H`, panic `LL`/`HH`, off-scale `<`/`>`, `N`, `A`/`AA`,
515
- the **directional** significant-change `U` (up) / `D` (down) — _not_ units/delta — `B`/`W`, and
548
+ the **directional** significant-change `U` (up) / `D` (down), _not_ units/delta, `B`/`W`, and
516
549
  microbiology `S`/`R`/`I`. An **unrecognized** flag is surfaced as `meaning: "undefined"` with an
517
- `ASTM_RECORD_UNDEFINED_ABNORMAL_FLAG` warning — **never dropped, never coerced to `normal`**.
550
+ `ASTM_RECORD_UNDEFINED_ABNORMAL_FLAG` warning, **never dropped, never coerced to `normal`**.
518
551
  - **Result status (field 9).** `interpretResultStatus()` and the always-present `status` field model
519
552
  `F`/`C`/`P`/`R`/`S`/`I`/`X`, with **`C` correction** (`supersedes: true`) and **`X` cancel**
520
- (`cancelled: true`) so a superseded/cancelled result can **never** read as current — `isActiveFinal`
553
+ (`cancelled: true`) so a superseded/cancelled result can **never** read as current, `isActiveFinal`
521
554
  is `true` only for a plain `F`. An **absent** status is typed `unspecified` (never assumed `final`);
522
555
  an unrecognized one is `undefined` + `ASTM_RECORD_UNDEFINED_RESULT_STATUS`.
523
556
  - **Reference range (field 6).** `parseReferenceRange()` and the `range` field parse `low-high`
524
557
  (closed), `<high` (open-low), and `>low` (open-high); bounds are surfaced as **verbatim numeric
525
558
  text** (never coerced to floats). The range is read from the **full field text**, so a
526
- component-delimited value (`low^high`) is preserved verbatim and read as `unparsed` — never
559
+ component-delimited value (`low^high`) is preserved verbatim and read as `unparsed`: never
527
560
  truncated to a single bound. An unparseable range is `kind: "unparsed"` +
528
- `ASTM_RECORD_UNPARSEABLE_REFERENCE_RANGE` — **no bound is fabricated**. The exact delimiter is
561
+ `ASTM_RECORD_UNPARSEABLE_REFERENCE_RANGE`, **no bound is fabricated**. The exact delimiter is
529
562
  `[OSS-derived]` pending the purchased CLSI LIS02-A2 (roadmap §10 Q1).
530
563
  - **Units discipline (field 5).** A _numeric_ result value with no units raises
531
564
  `ASTM_RECORD_UNITS_ABSENT`; units are vendor free text (not UCUM) and are **never defaulted,
532
565
  guessed, or converted**.
533
566
  - New warning codes (registry extended, snapshot locked): `ASTM_RECORD_UNDEFINED_ABNORMAL_FLAG`,
534
567
  `ASTM_RECORD_UNDEFINED_RESULT_STATUS`, `ASTM_RECORD_UNPARSEABLE_REFERENCE_RANGE`,
535
- `ASTM_RECORD_UNITS_ABSENT` — all value-free (code + record/field index only).
568
+ `ASTM_RECORD_UNITS_ABSENT`, all value-free (code + record/field index only).
536
569
  - **Patient/order identity depth, comments, and partial-timestamp hardening (ASTM-3, roadmap Phase 3).**
537
570
  The misfiling-prevention slice: model the identity that a result files against, and the context that
538
571
  qualifies it.
539
572
  - **Full patient (`P`) identity.** The **practice-assigned ID (field 3)**, the **laboratory-assigned
540
573
  ID (field 4)**, and a **third patient ID (field 5)** are modeled as **distinct** fields that never
541
- collapse into one — conflating them is the primary result-misfiling path. Adds mother's maiden name
574
+ collapse into one: conflating them is the primary result-misfiling path. Adds mother's maiden name
542
575
  (field 7) alongside the existing name components (field 6), birthdate (field 8), and sex (field 9).
543
576
  - **Full order (`O`).** `priority` (field 6), `actionCode` (field ~12), and `reportType` (field ~26)
544
577
  are surfaced **verbatim** on top of the existing specimen/accession + Universal Test ID. The `~`
545
- field indices and the code sets are `[OSS-derived]` (paywalled) — never mapped to a guessed meaning.
546
- - **The `C` (comment) record.** Modeled as `source` (field 3), `text` (field 4, component-capable —
578
+ field indices and the code sets are `[OSS-derived]` (paywalled): never mapped to a guessed meaning.
579
+ - **The `C` (comment) record.** Modeled as `source` (field 3), `text` (field 4, component-capable,
547
580
  the full text is surfaced plus the component split, never truncated), and `commentType` (field 5).
548
581
  Each comment is **attached by position** to the immediately-preceding `H`/`P`/`O`/`R` parent
549
582
  (`parentIndex`); consecutive comments share that parent. **Fail-safe:** an **orphan** comment with no
550
583
  valid parent is attached to the message root (`attachedToRoot: true`) with an
551
- `ASTM_RECORD_ORPHAN_COMMENT` warning — **never silently dropped**. New extractors `comments(msg)` /
584
+ `ASTM_RECORD_ORPHAN_COMMENT` warning, **never silently dropped**. New extractors `comments(msg)` /
552
585
  `commentsFor(msg, record)` / `orders(msg)`, and the pure `attachComments()` attachment pass.
553
586
  - **Comment-type codes are `[OSS-derived]`.** `I` (instrument) is the only value seen in the
554
587
  permissively-licensed real transcripts; `G`/`T`/`P` are defined only in the paywalled CLSI LIS02-A2
555
- and are **not** interpreted — `commentType` is surfaced raw, never mapped to a guessed meaning.
588
+ and are **not** interpreted, `commentType` is surfaced raw, never mapped to a guessed meaning.
556
589
  - **Partial-timestamp hardening.** A `YYYYMMDDHHMMSS` value with an odd digit run that truncates a
557
590
  two-digit component (lengths 5/7/9/11/13) sets `AstmDate.truncated`, is preserved verbatim in `raw`,
558
- and stops at the last **complete** component — the dangling digit is **never zero-filled into a
591
+ and stops at the last **complete** component: the dangling digit is **never zero-filled into a
559
592
  fabricated time**. A caller surfaces this as a value-free `ASTM_RECORD_PARTIAL_TIMESTAMP` warning
560
- (P field 8, R fields 12/13). No timezone is modeled — times stay instrument-local, never assumed UTC.
593
+ (P field 8, R fields 12/13). No timezone is modeled: times stay instrument-local, never assumed UTC.
561
594
  - New warning codes (registry extended, snapshot locked): `ASTM_RECORD_ORPHAN_COMMENT`,
562
- `ASTM_RECORD_PARTIAL_TIMESTAMP` — value-free (code + record/field index only).
595
+ `ASTM_RECORD_PARTIAL_TIMESTAMP`, value-free (code + record/field index only).
563
596
  - `scripts/phi-scan.ts` extended toward the mother's-maiden locus (P field 7), on top of the existing
564
597
  name (field 6) + DOB (field 8) detection; synthetic fixtures declared in `scripts/phi-allow-list.txt`.
565
598
  - **Query (`Q`) + host-query flow + `M`/`S` surfaced verbatim (ASTM-4, roadmap Phase 4).** Completes the
566
- record grammar — **the record-content layer is now feature-complete.**
599
+ record grammar: **the record-content layer is now feature-complete.**
567
600
  - **The `Q` (Request Information) record.** Modeled at the public ASTM E1394 field positions:
568
601
  `startingRangeId` (field 3) and `endingRangeId` (field 4) surfaced as the **full verbatim field**
569
602
  (never truncated to a component), the Universal Test ID (field 5, same caret structure as `O`/`R`),
570
603
  and `requestInformationStatus` (field 13) surfaced **verbatim**. The range component structure, the
571
604
  `ALL` universal-query keyword (`queriesAllTests`), and the request-information status code set are
572
- all **`[OSS-derived / paywalled]`** (roadmap §10 Q3) — surfaced, flagged, and **never interpreted or
605
+ all **`[OSS-derived / paywalled]`** (roadmap §10 Q3): surfaced, flagged, and **never interpreted or
573
606
  guessed**. New `query(msg)` extractor.
574
607
  - **The host-query flow.** Every message is classified up front (`msg.classification`): an `H/P/Q/L`
575
608
  **request** is `host-query`, an `R`-bearing message is `results`, an `O`-only message is `orders`,
576
- else `indeterminate`. **Fail-safe:** the `Q` **dominates** — a `Q`-bearing message is a request and
609
+ else `indeterminate`. **Fail-safe:** the `Q` **dominates**, a `Q`-bearing message is a request and
577
610
  is **never** read as a result set, even when a result record is also present (a contradiction flagged
578
611
  with `ASTM_RECORD_AMBIGUOUS_MESSAGE_KIND`). Gate on `classification.isHostQueryRequest`. Pure
579
612
  `classifyMessage(records)` exported.
580
613
  - **`M` (manufacturer) + `S` (scientific) records surfaced verbatim.** Vendor-defined free-form
581
614
  QC / calibration / maintenance data, preserved byte-for-byte on `record.rawLine` and **never**
582
- interpreted into typed clinical fields — a QC value can never be read as a patient result. Round-trip
615
+ interpreted into typed clinical fields: a QC value can never be read as a patient result. Round-trip
583
616
  byte-identical.
584
617
  - New warning codes (registry extended, snapshot locked): `ASTM_RECORD_UNINTERPRETED_QUERY_STATUS`
585
618
  (a Q request-information status surfaced verbatim; the code set is paywalled, so it is passed through
586
- uninterpreted) and `ASTM_RECORD_AMBIGUOUS_MESSAGE_KIND` — both value-free (code + position only).
619
+ uninterpreted) and `ASTM_RECORD_AMBIGUOUS_MESSAGE_KIND`, both value-free (code + position only).
587
620
  - `AstmMessage` gains a `classification` field; `AstmRecord` gains `QueryRecord` / `ManufacturerRecord`
588
621
  / `ScientificRecord` members (an unknown type letter is still an `UnsupportedRecord`, never dropped).
589
- - **E1381/CLSI-LIS01 frame codec (ASTM-5, roadmap Phase 5).** The **low-level framing layer** begins —
622
+ - **E1381/CLSI-LIS01 frame codec (ASTM-5, roadmap Phase 5).** The **low-level framing layer** begins:
590
623
  a separate, independent layer from the record layer, sharing only the payload boundary. `src/frames/`
591
624
  decodes a framed byte stream into frames + reassembled record bytes; `src/common/` and `src/records/`
592
625
  are untouched.
593
626
  - `decodeAstmFrames(bytes, opts?)` → `{ records: readonly Uint8Array[]; frames: readonly AstmFrame[];
594
627
  warnings: readonly AstmFrameWarning[] }`. A frame is `<STX> FN text <ETB|ETX> CS <CR><LF>`.
595
628
  - **Modulo-256 checksum** over the bytes after `STX` up to and **including** the `ETB`/`ETX`
596
- terminator, two hex chars — **verified on decode, emitted uppercase, accepted lowercase** (a real
629
+ terminator, two hex chars: **verified on decode, emitted uppercase, accepted lowercase** (a real
597
630
  vendor quirk). `computeChecksum` / `toChecksumHex` / `parseChecksumHex` exported.
598
631
  - **Frame-number `0`–`7` sequencing** (rolls over `7 → 0 → 1`, starts at `1`) and **multi-frame record
599
- reassembly** — text is capped at **240 bytes** (the seven control bytes are **not** counted), `ETB`
632
+ reassembly**: text is capped at **240 bytes** (the seven control bytes are **not** counted), `ETB`
600
633
  is intermediate / `ETX` final. `parseFramedAstm(bytes, opts?)` composes the framing and record layers
601
634
  at the edge (decode → reassemble trusted records → `parseAstmRecords`).
602
635
  - **Fail-safe (byte-level, safety-critical):** a **checksum mismatch** surfaces the frame flagged
603
- `trusted: false` and **never merges** it into a record (default warn in lenient / thrown in strict —
636
+ `trusted: false` and **never merges** it into a record (default warn in lenient / thrown in strict,
604
637
  the "checksums are routinely not validated" claim was _refuted_: we validate); a **frame-number gap**
605
638
  warns and is **never silently bridged**; an **unterminated** frame surfaces the partial bytes
606
639
  untrusted and **invents no partial record**; an **oversize** (>240) frame is flagged, never dropped.
607
640
  - New `ASTM_FRAME_*` warning registry (a **second** registry alongside `ASTM_RECORD_*`, sharing only
608
641
  the `EMPTY_INPUT` fatal; snapshot locked): `ASTM_FRAME_BAD_CHECKSUM`, `ASTM_FRAME_SEQUENCE_GAP`,
609
- `ASTM_FRAME_UNTERMINATED`, `ASTM_FRAME_OVERSIZE` — every warning **value-free**, carrying a **frame
642
+ `ASTM_FRAME_UNTERMINATED`, `ASTM_FRAME_OVERSIZE`, every warning **value-free**, carrying a **frame
610
643
  number + byte offset** only, never the record bytes a frame holds. `{ strict: true }` throws
611
644
  `AstmFrameStrictError`.
612
- - **Fuzz gate (required, part of `verify`):** a `fast-check` target over the codec — arbitrary /
645
+ - **Fuzz gate (required, part of `verify`):** a `fast-check` target over the codec, arbitrary /
613
646
  truncated / mixed / control-char-laden bytes never crash, hang, or OOM; they degrade to a typed
614
647
  error or a value-free warning. Plus property tests: N-frame reassembly equals the single-frame form,
615
648
  and every trusted frame's recomputed checksum matches its declared value.
@@ -617,13 +650,13 @@ warnings: readonly AstmFrameWarning[] }`. A frame is `<STX> FN text <ETB|ETX> CS
617
650
  `DecodeAstmFramesResult`, `FramedAstmResult`, `AstmFramePosition`, `AstmFrameWarning`,
618
651
  `FrameWarningCode`, `FRAME_WARNING_CODES`.
619
652
  - **Transport variants + pure LTP protocol reducer (ASTM-6, roadmap Phase 6).** The **LTP protocol
620
- layer** — `src/ltp/` — sits above the frame codec: transport auto-detection plus a deterministic,
653
+ layer**, `src/ltp/`, sits above the frame codec: transport auto-detection plus a deterministic,
621
654
  socket-free session state machine. No live I/O: the consumer owns the wire and clock; this layer
622
655
  decides.
623
656
  - **Transport auto-detection.** `detectFraming(bytes, opts?)` → `{ framing: "framed" | "raw";
624
657
  defaulted: boolean; warnings }`. A leading `STX`/`ENQ` ⇒ **framed** (serial, and the cobas 4800 /
625
658
  iNTERFACEWARE Iguana framed-over-TCP reality); a leading bare record letter (`H`/`P`/`O`/`R`/`C`/
626
- `Q`/`M`/`S`/`L`) ⇒ **raw** (the cobas b121 raw-TCP reality — framing dropped, records streamed
659
+ `Q`/`M`/`S`/`L`) ⇒ **raw** (the cobas b121 raw-TCP reality, framing dropped, records streamed
627
660
  directly). An unrecognizable lead **defaults to framed and warns**
628
661
  (`ASTM_LTP_AMBIGUOUS_TRANSPORT`), never guessing silently into data loss; an `override` forces the
629
662
  mode (the Phase-8 profile hook).
@@ -632,8 +665,8 @@ defaulted: boolean; warnings }`. A leading `STX`/`ENQ` ⇒ **framed** (serial, a
632
665
  codec-decoded `frame`; actions are `sendAck` / `sendNak` / `sendEot` / `deliverRecord`. It models
633
666
  the LIS01-A2 establishment → transfer → termination phases as `neutral ⇄ transfer`, reassembling
634
667
  `ETB…ETX` runs into delivered records and tracking the `0`–`7` frame sequence.
635
- - **ACK-failsafe (safety-critical, borrowed from `mllp`).** A frame the codec did not vouch for — a
636
- **bad checksum**, an **unterminated** frame, or one **out of sequence** — is answered with `NAK`,
668
+ - **ACK-failsafe (safety-critical, borrowed from `mllp`).** A frame the codec did not vouch for (a
669
+ **bad checksum**, an **unterminated** frame, or one **out of sequence**) is answered with `NAK`,
637
670
  **never** a fabricated positive `ACK`, and its bytes are **never** appended to a record or
638
671
  delivered. A `NAK` drives **retransmit, not acceptance** (`ASTM_LTP_FRAME_REJECTED`). A duplicate
639
672
  of the last-accepted frame is idempotently re-`ACK`ed without re-appending; a partial record open
@@ -648,33 +681,33 @@ EOT` session **reassembles exactly the source records**; a **raw-TCP stream equa
648
681
  `ltpInitialState`, `ltpReduce`, `LtpPhase`, `LtpState`, `LtpEvent`, `LtpAction`, `LtpTransition`,
649
682
  `AstmLtpWarning`, `LtpWarningCode`, `LTP_WARNING_CODES`, `ltpAmbiguousTransport`,
650
683
  `ltpUnexpectedEvent`, `ltpFrameRejected`.
651
- - **Spec-clean serializers + builders — both layers (ASTM-7, roadmap Phase 7).** The **emit** side: the
684
+ - **Spec-clean serializers + builders, both layers (ASTM-7, roadmap Phase 7).** The **emit** side: the
652
685
  conservative inverse of the parser and the frame codec, so **round-trip fidelity holds by
653
- construction**. Postel's Law's second half — liberal on parse, strict on emit.
686
+ construction**. Postel's Law's second half: liberal on parse, strict on emit.
654
687
  - **Record serializer.** `serializeAstmRecords(msg | records)` and `serializeAstmRecord(record)` emit a
655
688
  `CR`-terminated stream with the **canonical** `H|\^&` delimiters and every embedded delimiter
656
- re-escaped. `encodeComponent()` is the exact inverse of the Phase-1 escape codec — the escape char is
657
- encoded **first** (`&` → `&E&`), then the field / component / repeat delimiters (`&F&`/`&S&`/`&R&`) —
689
+ re-escaped. `encodeComponent()` is the exact inverse of the Phase-1 escape codec. The escape char is
690
+ encoded **first** (`&` → `&E&`), then the field / component / repeat delimiters (`&F&`/`&S&`/`&R&`),
658
691
  so a value containing a delimiter (a titre `1^40` → `1&S&40`) can never break framing and reads back
659
692
  as **one** component. A source parsed with **non-canonical** delimiters is **normalized** to the
660
693
  canonical set on emit (vendor-delimiter round-tripping is a Phase-8 profile concern). The header's
661
694
  delimiter declaration is emitted **literally** (never escaped); `M`/`S` records are re-emitted
662
695
  **byte-identically** from `rawLine`.
663
696
  - **Message builder.** `buildAstmMessage(input)` constructs a spec-clean stream from typed input under
664
- the **never-fabricate** discipline: it emits **only** the values the caller supplied — an omitted
697
+ the **never-fabricate** discipline: it emits **only** the values the caller supplied, an omitted
665
698
  field is left empty, **never a defaulted clinical value** (an unset result status reads back as
666
699
  `unspecified`, never `final`; units / abnormal flags / patient IDs are never defaulted). The
667
- **structure** — record type letters, the canonical delimiter declaration, per-record-type sequence
668
- counters, the `L` terminator — is **computed, not guessed** (a sequence number may be overridden).
700
+ **structure** (record type letters, the canonical delimiter declaration, per-record-type sequence
701
+ counters, the `L` terminator) is **computed, not guessed** (a sequence number may be overridden).
669
702
  - **Frame encoder.** `composeAstmFrames(records, opts?)` is the exact inverse of `decodeAstmFrames`:
670
703
  it wraps reassembled record bytes into `<STX> FN text <ETB|ETX> CS <CR><LF>` frames with the
671
704
  modulo-256 **checksum** and the `0`–`7` **frame number** **computed** (never accepted-as-given or
672
705
  faked; emitted uppercase), numbered continuously across the stream (start `1`, roll over `7 → 0`),
673
706
  and every record over **240** text bytes **split** `ETB…ETX` (the seven control bytes never counted).
674
- `serializeFramedAstm(msg | records)` composes both emit layers at the edge — the mirror of
707
+ `serializeFramedAstm(msg | records)` composes both emit layers at the edge: the mirror of
675
708
  `parseFramedAstm`.
676
- - **Framing-integrity guards (typed errors, conservative emit).** A value carrying a `CR`/`LF` — which
677
- no ASTM escape can encode — is refused with an `AstmSerializeError` (`ASTM_EMIT_UNENCODABLE_VALUE`)
709
+ - **Framing-integrity guards (typed errors, conservative emit).** A value carrying a `CR`/`LF`, which
710
+ no ASTM escape can encode, is refused with an `AstmSerializeError` (`ASTM_EMIT_UNENCODABLE_VALUE`)
678
711
  rather than emitted into a corrupted wire; an empty record or empty record list is an
679
712
  `AstmFrameEncodeError` (`ASTM_FRAME_EMPTY_RECORD`), never an empty frame.
680
713
  - **Round-trip proven.** The shared archetype `roundTripProperty` is now **live** (serialize is the
@@ -688,23 +721,23 @@ EOT` session **reassembles exactly the source records**; a **raw-TCP stream equa
688
721
  `MessageInput`, `HeaderInput`, `PatientInput`, `PatientNameInput`, `OrderInput`, `ResultInput`,
689
722
  `CommentInput`, `QueryInput`, `VerbatimInput`), `composeAstmFrames`, `AstmFrameEncodeError`,
690
723
  `ComposeFramesOptions`, `serializeFramedAstm`.
691
- - **Vendor profile system — engine + registry + quirk tolerance + definition-time safety gate
724
+ - **Vendor profile system, engine + registry + quirk tolerance + definition-time safety gate
692
725
  (ASTM-8, roadmap Phase 8).** `src/profiles/` mirrors the sibling `@cosyte/hl7` `defineProfile` /
693
726
  `@cosyte/ccda` `defineCcdaProfile` shape: `name` / `lineage` / `describe()` / `extends`-merge, a
694
727
  provenance-backed built-in registry, a runtime tolerance transform, and a definition-time safety gate.
695
728
  - `defineAstmProfile(opts)` builds a frozen, immutable profile declaring the **non-safety-critical**
696
729
  warning codes a class of streams is expected to trip (each with a grounded `rationale`), plus an
697
- optional `transport` override (`"framed"`/`"raw"`) — the raw-vs-framed-TCP knob a consumer feeds to
730
+ optional `transport` override (`"framed"`/`"raw"`): the raw-vs-framed-TCP knob a consumer feeds to
698
731
  `detectFraming(bytes, { override })` for a stream whose leading byte would auto-detect the wrong way.
699
732
  - **A profile never alters an extracted value.** The transform (`applyAstmProfileToWarnings`, run last
700
733
  in `parseAstmRecords`) only ever re-badges a warning it _expects_ to the new `PROFILE_QUIRK_APPLIED`
701
- code (flagged `expected: true`, carrying the original `toleratedCode` and position) — Postel's Law
734
+ code (flagged `expected: true`, carrying the original `toleratedCode` and position), Postel's Law
702
735
  with a receipt: nothing is dropped, and a spec-clean message parses byte-identically with or without
703
736
  a profile.
704
737
  - **The safety gate is default-deny and total.** Only four benign, value-preserving record codes are
705
738
  tolerable (`ASTM_RECORD_UNKNOWN_TYPE`, `ASTM_NONSTANDARD_DELIMITERS`, `ASTM_UNKNOWN_ESCAPE_SEQUENCE`,
706
- `ASTM_RECORD_UNINTERPRETED_QUERY_STATUS`); **every other code across all three registries — record,
707
- frame (`ASTM_FRAME_*`), and LTP (`ASTM_LTP_*`) — is safety-critical and refused at definition time**
739
+ `ASTM_RECORD_UNINTERPRETED_QUERY_STATUS`); **every other code across all three registries (record,
740
+ frame `ASTM_FRAME_*`, and LTP `ASTM_LTP_*`) is safety-critical and refused at definition time**
708
741
  with an `AstmProfileDefinitionError`. A profile therefore can never make a bad checksum "ok," a
709
742
  cancelled result read "final," or quiet a wrong value / flag / status / range / units / patient or
710
743
  comment context / message-kind ambiguity. Any warning code added in a future phase is
@@ -712,7 +745,7 @@ EOT` session **reassembles exactly the source records**; a **raw-TCP stream equa
712
745
  - `parseAstmRecords(raw, { profile })` accepts an explicit profile; `{ profile: null }` opts out of
713
746
  the process-scoped default (`setDefaultAstmProfile`); an `expected` quirk does **not** escalate in
714
747
  `strict` mode. `AstmMessage` gains an additive `profile?: { name, lineage }` attribution.
715
- - **Built-ins:** `astmProfiles.default` (tolerates nothing) + `astmProfiles.referenceCorpus` — a
748
+ - **Built-ins:** `astmProfiles.default` (tolerates nothing) + `astmProfiles.referenceCorpus`, a
716
749
  **non-vendor**, evidence-backed profile grounded firsthand in the redistributable OSS reference
717
750
  corpus (`kxepal/python-astm` `codec.py` (BSD) + `senaite.astm`, which split on raw delimiters and
718
751
  never un-escape `&F&`/`&S&`/`&R&`/`&E&`), tolerating only the resulting non-standard-escape
@@ -725,49 +758,49 @@ EOT` session **reassembles exactly the source records**; a **raw-TCP stream equa
725
758
  `resolveProfileTransport`, `profileQuirkApplied`, `SAFETY_CRITICAL_CODES`, `TOLERABLE_CODES`,
726
759
  `ALL_ASTM_WARNING_CODES`, `isSafetyCriticalCode`, and the `AstmProfile`, `DefineAstmProfileOptions`,
727
760
  `AstmQuirkTolerance`, `AstmQuirkMatch`, `AstmProfileProvenance`, `AnyAstmWarningCode` types.
728
- - **LIVD-aware LOINC recognition — bring-your-own catalog, zero bundled terminology data (ASTM-9,
761
+ - **LIVD-aware LOINC recognition: bring-your-own catalog, zero bundled terminology data (ASTM-9,
729
762
  roadmap Phase 9).** The `src/terminology/` layer maps an analyzer's local test code (the Universal
730
763
  Test ID's vendor/local code on `R`/`O` records) to a standard LOINC via a **consumer-supplied** IICC
731
- LIVD ("LOINC to Vendor IVD") catalog — **additive, advisory, and never a guessed LOINC** (a wrong
764
+ LIVD ("LOINC to Vendor IVD") catalog: **additive, advisory, and never a guessed LOINC** (a wrong
732
765
  LOINC mis-identifies a test).
733
766
  - `defineLivdCatalog(entries)` builds an immutable, frozen catalog indexed by the **Vendor Analyte
734
767
  Code** (the vendor transmission code the instrument sends), grounded firsthand on the IICC LIVD
735
768
  digital format / HL7 LIVD IG; `catalog.lookup(code)` returns `mapped` (one LOINC), `unmapped` (a
736
- miss), or `ambiguous` (a code matching more than one distinct LOINC — surfaced, **never resolved**).
769
+ miss), or `ambiguous` (a code matching more than one distinct LOINC, surfaced, **never resolved**).
737
770
  - `applyLivd(msg, catalog)` produces a **separate** layer of per-`R`/`O` `LivdAnnotation`s and never
738
771
  mutates, alters, or drops the raw reported code/value; a catalog hit is labeled `derived: true`
739
772
  (`source: "livd"`), an inline LOINC already on the wire is surfaced `source: "wire"` (never
740
773
  overwritten by the catalog), and a miss/conflict is `unmapped`/`ambiguous` with a **value-free**
741
- warning — a LOINC is **never** fabricated. `lookupLivdForRecord(record, catalog)` annotates one
774
+ warning, a LOINC is **never** fabricated. `lookupLivdForRecord(record, catalog)` annotates one
742
775
  record.
743
- - **No LOINC / SNOMED / LIVD data is bundled** (roadmap §5). Firsthand: LOINC is © Regenstrief —
776
+ - **No LOINC / SNOMED / LIVD data is bundled** (roadmap §5). Firsthand: LOINC is © Regenstrief,
744
777
  redistributable only _with its attribution notice_, not public-domain; and the public CDC LIVD file
745
778
  is a **SARS-CoV-2-specific** publication that also carries separately-licensed SNOMED CT, not a
746
779
  general-analyte, public-domain catalog. The package stays a structural recognizer, not a dictionary:
747
780
  the consumer supplies the LIVD data (and owns its license obligations).
748
- - New `ASTM_LIVD_*` warning registry (`ASTM_LIVD_UNMAPPED_CODE`, `ASTM_LIVD_AMBIGUOUS_MAPPING`) — a
781
+ - New `ASTM_LIVD_*` warning registry (`ASTM_LIVD_UNMAPPED_CODE`, `ASTM_LIVD_AMBIGUOUS_MAPPING`): a
749
782
  fourth, self-contained registry, deliberately outside the profile safety gate's universe (a LIVD
750
783
  non-mapping is a post-parse advisory, not a parse-time deviation a profile could tolerate). New
751
784
  exports: `defineLivdCatalog`, `applyLivd`, `lookupLivdForRecord`, `LIVD_WARNING_CODES`,
752
785
  `livdUnmappedCode`, `livdAmbiguousMapping`, and the `LivdCatalog`, `LivdEntry`, `LivdLookup`,
753
786
  `LivdAnnotation`, `LivdMapping`, `LivdResult`, `AstmLivdWarning`, `LivdWarningCode` types.
754
- - **Release hardening (ASTM-10, roadmap Phase 10 — the final phase).** Publish-readiness for the now
787
+ - **Release hardening (ASTM-10, roadmap Phase 10, the final phase).** Publish-readiness for the now
755
788
  feature-complete parser: coverage, fuzz, firsthand differential testing, the full docs spine, and a
756
789
  proven release shape. No new runtime API.
757
790
  - **Differential conformance vs [python-astm][pa]** (BSD-3-Clause reference codec, commit
758
791
  `4170ce0c`), grounded **firsthand** in `test/differential/`: outputs captured once from the
759
792
  reference (`generate-reference-vectors.py` → `reference-vectors.json`; **no reference code
760
- vendored**), then asserted against `@cosyte/astm` on three shared paths — the **modulo-256
793
+ vendored**), then asserted against `@cosyte/astm` on three shared paths, the **modulo-256
761
794
  checksum**, the **record field/component split** (escape-free, non-header), and a
762
795
  **cross-implementation frame decode** (python encodes + splits → our decoder verifies every
763
796
  checksum and reassembles the exact record bytes). The **deliberate divergences** are asserted on
764
797
  purpose: we un-escape `&F&`/`&S&`/`&R&`/`&E&` (python leaves them literal), we validate the frame
765
798
  checksum (python does not verify on decode), and we classify the `Q` host-query (python has no
766
- model). CI needs no Python — only the captured JSON.
767
- - **Per-directory ≥ 90 coverage gating extended to the whole `src/` surface** — `frames`, `ltp`,
799
+ model). CI needs no Python: only the captured JSON.
800
+ - **Per-directory ≥ 90 coverage gating extended to the whole `src/` surface**: `frames`, `ltp`,
768
801
  and `terminology` now gate per-dir alongside `common`/`records`/`profiles` (on top of the global
769
802
  gate), so the release bar holds directory by directory, not just in aggregate.
770
- - **Record-tokenizer fuzz** (`test/property/records-fuzz.property.test.ts`) — the companion to the
803
+ - **Record-tokenizer fuzz** (`test/property/records-fuzz.property.test.ts`), the companion to the
771
804
  frame-codec fuzz: arbitrary / truncated / delimiter- and escape-laden input into
772
805
  `parseAstmRecords` never crashes, hangs, or OOMs; lenient mode only ever throws a sanctioned
773
806
  Tier-3 fatal, strict only `AstmStrictError`, and every warning carries a registered code. Both
@@ -776,9 +809,9 @@ EOT` session **reassembles exactly the source records**; a **raw-TCP stream equa
776
809
  - **Publish dry-run proven release-shaped:** `attw` all-green (per-condition ESM/CJS types), a new
777
810
  `smoke` gate (`scripts/smoke.mjs`) that imports the **built** ESM and requires the **built** CJS
778
811
  entry and parses a result through each (now wired into `verify.sh`), and an `npm publish
779
- --dry-run` pack inspection (10 files — `dist/` + `README`/`LICENSE`/`CHANGELOG`/`package.json`,
812
+ --dry-run` pack inspection (10 files, `dist/` + `README`/`LICENSE`/`CHANGELOG`/`package.json`,
780
813
  no `src` or tests). Zero runtime dependencies; MIT.
781
- - **Full Diátaxis docs spine + honesty docs.** New `docs-content/limitations.md` (**What it does —
814
+ - **Full Diátaxis docs spine + honesty docs.** New `docs-content/limitations.md` (**What it does,
782
815
  and does not do**: no live I/O, units are verbatim free text not UCUM, no bundled terminology
783
816
  dictionary, `M`/`S` verbatim, the archived-standard status, and the MIT-vs-CLSI license posture)
784
817
  and `docs-content/architecture.md` (the two independent layers and their payload boundary); the
@@ -796,12 +829,12 @@ EOT` session **reassembles exactly the source records**; a **raw-TCP stream equa
796
829
 
797
830
  ### Deferred (later phases)
798
831
 
799
- - **Named per-vendor profiles** (cobas / Sysmex / ADVIA / Mindray / Snibe) stay `REAL-CORPUS`-gated —
832
+ - **Named per-vendor profiles** (cobas / Sysmex / ADVIA / Mindray / Snibe) stay `REAL-CORPUS`-gated:
800
833
  the Phase-8 engine supports them (tolerate + transport override), but no public vendor-attributed
801
834
  quirk document grounds a named one. **No bundled terminology
802
- dictionary** — LIVD-aware LOINC recognition is bring-your-own by design (Phase 9); the package ships
835
+ dictionary**: LIVD-aware LOINC recognition is bring-your-own by design (Phase 9); the package ships
803
836
  no LOINC / SNOMED / LIVD data and mapping quality is the consumer's catalog. The LTP reducer remains
804
- a pure state machine — no live I/O: wiring it to a real `SerialPort`/`net.Socket` (and the
837
+ a pure state machine, no live I/O: wiring it to a real `SerialPort`/`net.Socket` (and the
805
838
  interactive contention/timeout/retransmit **timing**) is a thin consumer adapter, and the standard's
806
839
  exact numeric timeouts / retry counts are deferred (we model transitions, not timers).
807
840