@cosyte/astm 0.0.4 → 0.0.6

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