@cosyte/astm 0.0.13 → 0.0.14

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,6 +11,75 @@ this file is maintained by hand (Changesets handles the version bump and publish
11
11
 
12
12
  ### Fixed
13
13
 
14
+ - **`composeAstmFrames` wrote an out-of-range `startFrameNumber` straight into the frame-number
15
+ position, so a value that was not a frame number produced a stream whose records the decoder
16
+ refused to emit** (`ASTM-FRAME-RESIDUALS`, defect 13). `PRE-EXISTING`, byte-identical on `64c2fd5`.
17
+ The option is now checked before `composeAstmFrames` reads a record, and refused with the new code
18
+ `ASTM_FRAME_INVALID_START_FRAME_NUMBER` (a fourth member of `AstmFrameEncodeErrorCode`) unless it is
19
+ a whole number from `0` to `7`.
20
+
21
+ **What went out instead, measured on this package's own round trip.** A frame's number is one ASCII
22
+ digit, written as `FN_ZERO + n`, and nothing checked `n`. `-1` put a `/` in the frame-number
23
+ position, and `NaN`, `Infinity` and `-Infinity` each put a `NUL` there in **every** frame of the
24
+ stream, after which `decodeAstmFrames` recognised no frame number at all, warned
25
+ `ASTM_FRAME_SEQUENCE_GAP` on every frame, and emitted **none** of the records: a whole four-record
26
+ message in, zero out, and `parseFramedAstm` throwing on empty input. **The recorded defect said an
27
+ out-of-range value writes a non-digit byte and named a space for `NaN`. It is a `NUL`**, which is
28
+ the difference between an ordinary inter-frame byte the decoder skips and a byte that costs the
29
+ stream every one of its records.
30
+
31
+ **The quieter half is why the check covers the whole domain rather than the ends.** Values that
32
+ truncated back onto a digit were accepted silently and behaved as some other start value: `1.5` and
33
+ `257` each produced the byte-for-byte stream a `startFrameNumber` of `1` produces. The option
34
+ documented `0`-`7` and enforced neither bound nor integrality in either direction.
35
+
36
+ **The domain is derived from what a frame can carry, and the refusal is not a clamp.** The writable
37
+ numbers are exactly the whole numbers `0`-`7`, the modulo-8 sequence `decodeAstmFrames` reads and
38
+ rolls over. Clamping or taking a modulo would pick a frame number the caller did not ask for, and
39
+ the frame number is the decoder's only evidence that no frame was dropped, so a stream numbered from
40
+ a value nobody chose is a stream whose sequence check certifies the wrong thing. The refusal is
41
+ raised before `composeAstmFrames` reads a record, so on that entry point it never depends on the
42
+ caller's data (`serializeFramedAstm` serializes every record first, so a record that cannot be
43
+ serialized at all is refused ahead of it on that route). Its message names the value received: that
44
+ value is the caller's own option, not stream content, and nothing from the records reaches it.
45
+
46
+ **The whole `0`-`7` range is still accepted, because a non-default start has a real use that was
47
+ measured rather than assumed.** It composes a **continuation** of a transfer already in progress:
48
+ `composeAstmFrames(head)` joined with `composeAstmFrames(tail, { startFrameNumber: n })`, where `n`
49
+ is the number after the last frame `head` used, is **byte-identical** to composing the whole list in
50
+ one call and decodes with an empty warnings array, across the `7 → 0` rollover included. Narrowing
51
+ the option to `1` would have removed that.
52
+
53
+ **What a continuation is not, now stated on the option rather than guarded against.** Read on its
54
+ own, a stream that starts anywhere but `1` opens on a sequence gap; the decoder never bridges a gap
55
+ silently, so it warns and does not emit that first record. **What `parseFramedAstm` does after that
56
+ varies with the message shape, and no rule is offered for it:** it may throw, under more than one
57
+ code (`ASTM_RECORD_NO_HEADER`, `EMPTY_INPUT` and `ASTM_RECORD_UNDECLARED_DELIMITERS` were each
58
+ measured), and it may return a message one record short. **The one statement that generalizes is
59
+ that the record layer never reports the loss**, because `parseFramedAstm` hands the record parser
60
+ only the frames the codec vouched for, so `message.warnings` carries what the surviving records
61
+ warrant and nothing about the record that did not survive. Read `frameWarnings`. Nothing returns the number to continue a sequence from either, and the frame count
62
+ is not it once a record splits, so the supported computation is named on the option. The
63
+ documented-valid `0` has always behaved that way. It is the cost of the option, not a defect in the
64
+ caller's records, and it is why `1` is the default.
65
+
66
+ **This is a narrowing on a published package, in a small population.** `1.5` and `257` used to
67
+ produce a working stream, the byte-for-byte start-at-`1` one, and now throw. Every value the check
68
+ turns away was already outside what the option documented: a frame number, a whole number `0`-`7`.
69
+
70
+ Both the old bytes and the new refusal are pinned in `test/frames/start-frame-number.test.ts`,
71
+ asserted on what the old encoder produced (rebuilt with the test-only `frame()` builder, so nothing
72
+ is transcribed twice), with a biconditional property: a value is refused **if and only if** it is
73
+ not a whole number in `0`-`7`. 14 of the 32 new tests are red against `64c2fd5`.
74
+
75
+ - **Three `{@link}` targets in the published `.d.ts` did not name a symbol declared there**
76
+ (`ASTM-FRAME-RESIDUALS`, the sibling minors). `QuirkTolerance` is `AstmQuirkTolerance` (twice, in
77
+ `src/profiles/types.ts`); the `startFrameNumber` doc linked `FIRST_FRAME_NUMBER`, which this
78
+ package does not export, and that sentence was rewritten and no longer links it; and
79
+ `AstmMessage.profile` linked a bare `warnings`, meaning its sibling member, which resolves only for
80
+ a tool that resolves against the enclosing declaration. Every `{@link}` target in
81
+ `dist/index.d.ts` now names a symbol declared in that file.
82
+
14
83
  - **A delimiter set colliding with a record's type letter emitted a stream that read back as
15
84
  DIFFERENT records, and in its silent branch it fabricated a final lab result out of patient
16
85
  identifiers** (`ASTM-FRAME-RESIDUALS`, defect 5). `PRE-EXISTING`, byte-identical on `7253098`,
package/README.md CHANGED
@@ -53,7 +53,13 @@ reference parser, [`@cosyte/hl7`](https://github.com/cosyte/hl7).
53
53
  of a frame, framing has no escape sequence for them, and writing one through truncated the frame at
54
54
  that byte, silently losing a whole record whenever the two bytes after it happened to be the short
55
55
  frame's checksum. The **record** layer still carries them, because a returned string is not yet on a
56
- wire and a raw-transport consumer round-trips such a value exactly.
56
+ wire and a raw-transport consumer round-trips such a value exactly. `startFrameNumber` lets you
57
+ compose one transfer across several calls, and `composeAstmFrames` checks it before it reads a
58
+ record: a frame's number is a single ASCII digit, so a value that is not a whole number from `0` to
59
+ `7` is a typed error rather than whatever byte the arithmetic truncated to. The round trip above is
60
+ the **default** start; a non-default one writes a continuation of a sequence already in progress,
61
+ and a continuation read on its own opens on a frame-sequence gap, so its first record is warned
62
+ about and not emitted.
57
63
  - **Vendor profiles.** `defineAstmProfile()` builds a provenance-backed profile whose tolerances
58
64
  downgrade _expected_, non-safety-critical deviations to a `PROFILE_QUIRK_APPLIED` warning without
59
65
  ever altering a value, behind a safety gate that refuses to tolerate any result value, flag,
package/dist/index.cjs CHANGED
@@ -2290,6 +2290,15 @@ function assertFramable(bytes, recordIndex) {
2290
2290
  }
2291
2291
  }
2292
2292
  }
2293
+ function assertStartFrameNumber(value) {
2294
+ if (!Number.isInteger(value) || value < 0 || value > FRAME_NUMBER_MODULUS - 1) {
2295
+ throw new AstmFrameEncodeError(
2296
+ `startFrameNumber must be a whole number from 0 to 7: a frame's number is one ASCII digit and the sequence is modulo 8. Received: ${String(value)}.`,
2297
+ void 0,
2298
+ "ASTM_FRAME_INVALID_START_FRAME_NUMBER"
2299
+ );
2300
+ }
2301
+ }
2293
2302
  function encodeFrame(text, frameNumber, isFinal) {
2294
2303
  const term = isFinal ? ETX : ETB;
2295
2304
  const bytes = [STX, FN_ZERO + frameNumber, ...text, term];
@@ -2298,11 +2307,13 @@ function encodeFrame(text, frameNumber, isFinal) {
2298
2307
  return [...bytes, hex.charCodeAt(0), hex.charCodeAt(1), CR, LF];
2299
2308
  }
2300
2309
  function composeAstmFrames(records, options = {}) {
2310
+ const start = options.startFrameNumber ?? FIRST_FRAME_NUMBER;
2311
+ assertStartFrameNumber(start);
2301
2312
  if (records.length === 0) {
2302
2313
  throw new AstmFrameEncodeError("Cannot frame an empty record list: nothing to transmit.");
2303
2314
  }
2304
2315
  const out = [];
2305
- let frameNumber = options.startFrameNumber ?? FIRST_FRAME_NUMBER;
2316
+ let frameNumber = start;
2306
2317
  records.forEach((record, recordIndex) => {
2307
2318
  const bytes = toBytes(record, recordIndex);
2308
2319
  if (bytes.length === 0) {
@@ -2347,7 +2358,7 @@ function concatBytes3(chunks) {
2347
2358
  }
2348
2359
 
2349
2360
  // src/index.ts
2350
- var VERSION = "0.0.13";
2361
+ var VERSION = "0.0.14";
2351
2362
 
2352
2363
  exports.ALL_ASTM_WARNING_CODES = ALL_ASTM_WARNING_CODES;
2353
2364
  exports.AMBIGUOUS_CODES = AMBIGUOUS_CODES;