@cosyte/astm 0.0.9 → 0.0.10
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 +72 -5
- package/README.md +4 -1
- package/dist/index.cjs +25 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +78 -8
- package/dist/index.d.ts +78 -8
- package/dist/index.mjs +25 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,73 @@ this file is maintained by hand (Changesets handles the version bump and publish
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **The frame encoder no longer writes a different character than the one it was handed**
|
|
15
|
+
(`ASTM-FRAME-BYTE-RESIDUALS`, closing known defect 7). `composeAstmFrames` turned a record given
|
|
16
|
+
as a `string` into bytes with `charCodeAt(i) & 0xff`, so any character above `U+00FF` was replaced
|
|
17
|
+
by its low byte. That low byte is another perfectly ordinary character, which is why the
|
|
18
|
+
substitution was **silent**: it reached the wire, checksummed clean, and came back with an empty
|
|
19
|
+
warnings array from **both** layers.
|
|
20
|
+
|
|
21
|
+
Measured, on this package's own round trip, three shapes on the canonical set:
|
|
22
|
+
- `R|1|^^^687|28.6|μmol/L||N||F` read back `28.6` in `¼mol/L` (`U+03BC` low byte `0xBC`). A lab
|
|
23
|
+
number whose **units** silently changed.
|
|
24
|
+
- `P|1||LAB-0001||ŁUKASZ^JAN||19800101|M` read back the name spelled `AUKASZ` (`U+0141` low byte
|
|
25
|
+
`0x41`).
|
|
26
|
+
- `P|1||LAB-0001||GRAżYNA^ANNA||19800101|F` read back as **ten fields where the record models
|
|
27
|
+
nine** (`U+017C` low byte `0x7C`, the field separator). Every field after the split shifted one
|
|
28
|
+
along, so `patient().sex` answered `19800101` and the birth date was gone. That is **silent
|
|
29
|
+
re-read divergence**: a different field tree, `warnings: []` at both layers.
|
|
30
|
+
|
|
31
|
+
The known-defect entry said this class fails loudly in every case measured, and for the cases it
|
|
32
|
+
had measured that was true: they all truncated onto `STX`/`ETX`/`ETB`/`CR`. Nothing had measured a
|
|
33
|
+
code point whose low byte is an ordinary character, and that is the larger and quieter class.
|
|
34
|
+
|
|
35
|
+
**The disposition is a refusal, and it invents nothing.** `composeAstmFrames` now throws
|
|
36
|
+
`AstmFrameEncodeError` with the new `ASTM_FRAME_UNENCODABLE_CHARACTER` code, carrying the record
|
|
37
|
+
index and the character's index, never the character and never its code point. The alternative,
|
|
38
|
+
encoding as UTF-8, was **considered and rejected**: turning a character into bytes requires a
|
|
39
|
+
character encoding, this library reads no character-set declaration from any ASTM record, and
|
|
40
|
+
picking one would be a guess at bytes the caller never supplied, in a package that declines to
|
|
41
|
+
infer a delimiter set, a record type letter or a code system. Emit also has no warning channel, so
|
|
42
|
+
a warning could only have been ignored while the wrong bytes still shipped. That is the same
|
|
43
|
+
argument already written for `ASTM_EMIT_UNENCODABLE_VALUE` and `ASTM_EMIT_INVALID_DELIMITERS`.
|
|
44
|
+
|
|
45
|
+
**What grounds the Latin-1 boundary**, since no clause is claimed for it and none was read. Two
|
|
46
|
+
things, both firsthand. (1) This package's own byte-to-string boundary is already Latin-1, on the
|
|
47
|
+
read side, deliberately: `parseAstmRecords` decodes with one `String.fromCharCode` per byte "so
|
|
48
|
+
every byte survives 1:1". The encoder is documented as the exact inverse of the decoder, and the
|
|
49
|
+
decoder can never produce a code point above `U+00FF`, so a string carrying one is outside the
|
|
50
|
+
codec's domain by construction. (2) The redistributable OSS reference implementation
|
|
51
|
+
(`kxepal/python-astm`, BSD) parameterizes the codec on `encoding` throughout
|
|
52
|
+
(`encode(records, encoding=ENCODING, ...)` / `decode(data, encoding=ENCODING)`) and defines
|
|
53
|
+
`ENCODING = 'latin-1'` in `astm/constants.py`: the wire code page there is caller-supplied
|
|
54
|
+
out-of-band knowledge with a latin-1 default, not something a reader discovers from the stream.
|
|
55
|
+
**The normative text was not reachable and no clause is cited**: LIS02-A2 is withheld from CLSI's
|
|
56
|
+
free sample and E1394-97 / LIS01-A2 are paywalled, so this is a reasoned choice, exactly as the
|
|
57
|
+
delimiter-scoping and surplus-declaration choices before it were.
|
|
58
|
+
|
|
59
|
+
**The refusal removes no capability.** `composeAstmFrames` already accepted `Uint8Array`, so a
|
|
60
|
+
consumer who knows their instrument's code page encodes with their own encoder and passes bytes,
|
|
61
|
+
which are written through untouched. A stream whose every character is Latin-1 is byte-identical
|
|
62
|
+
to before, the full `0x00`–`0xFF` range included. The **record** layer is deliberately unchanged:
|
|
63
|
+
`serializeAstmRecords` returns a `string`, which is not yet bytes, so the caller keeps the choice.
|
|
64
|
+
|
|
65
|
+
**Stated no wider than it was measured.** This closes the string-to-bytes step only. It is not a
|
|
66
|
+
claim that every accepted record reads back: a record already carrying a raw `STX`/`ETX`/`ETB`
|
|
67
|
+
byte is framed as given and re-decodes wrong (known defect 6), which this change does not touch.
|
|
68
|
+
**Both** of that defect's branches are now pinned as out of scope in
|
|
69
|
+
`test/frames/unencodable-character.test.ts`, the warned one and the **silent** one: where the two
|
|
70
|
+
bytes following the embedded control character happen to be that truncated frame's checksum, the
|
|
71
|
+
short frame verifies and a whole result record is absorbed into the previous record's text with
|
|
72
|
+
`warnings: []` at both layers. That branch was found grading this change, reproduces byte-identically
|
|
73
|
+
on the base, and is why known defect 6's "it fails loudly" ranking was withdrawn rather than restated. A surrogate pair needs no
|
|
74
|
+
separate rule, each half being above `U+00FF` itself. New export `AstmFrameEncodeErrorCode`;
|
|
75
|
+
`AstmFrameEncodeError.code` is now that union and gains a `characterIndex`. The property "either
|
|
76
|
+
refuse the record string or reproduce it byte for byte" is asserted over 2,000 generated cases,
|
|
77
|
+
with its bound written into the test and a non-vacuity check that both sides of it are reached.
|
|
78
|
+
|
|
12
79
|
### Added
|
|
13
80
|
|
|
14
81
|
- **`ASTM_RECORD_FIELDS_UNSEPARATED`: a record that lost its fields no longer loses them in
|
|
@@ -481,11 +548,11 @@ phase 8` passes while `Phase 8` reds). An arm keyed on a following digit was wri
|
|
|
481
548
|
behind nothing but an `ASTM_FRAME_BAD_CHECKSUM`. Rather than enumerate the bytes each layer happens
|
|
482
549
|
to reserve, and re-derive that list whenever a layer is added, no control character is carried at
|
|
483
550
|
all. Two things that rule does **not** do, said here rather than left to be found: it is keyed on
|
|
484
|
-
the character
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
a _surplus_ does not mean one cannot be a _delimiter role_,
|
|
488
|
-
there. Those are
|
|
551
|
+
the character, so a surplus character above `U+00FF` passes it (the frame layer is where that is
|
|
552
|
+
settled now, and refuses such a character rather than truncating it, see the entry above; when
|
|
553
|
+
this was written the frame encoder truncated to the low byte and the case broke framing instead);
|
|
554
|
+
and refusing a control character in a _surplus_ does not mean one cannot be a _delimiter role_,
|
|
555
|
+
since only `CR`/`LF` are refused there. Those are
|
|
489
556
|
structural losses; dropping inert bytes is not.
|
|
490
557
|
|
|
491
558
|
**2. A caller-supplied delimiter set was never validated.** `serializeAstmRecords(msg, d)` and its
|
package/README.md
CHANGED
|
@@ -39,7 +39,10 @@ reference parser, [`@cosyte/hl7`](https://github.com/cosyte/hl7).
|
|
|
39
39
|
frame them with computed checksums, frame numbers, and the 240-byte split. Both layers round-trip by
|
|
40
40
|
construction, and a delimiter set that fails any of the three conditions readback requires (one
|
|
41
41
|
character per separator, no `CR`/`LF`, no two the same) is a typed error rather than bytes written
|
|
42
|
-
and lost.
|
|
42
|
+
and lost. A frame carries **bytes**, so a record handed to `composeAstmFrames` as a `string` is one
|
|
43
|
+
byte per character and a character above `U+00FF` is a typed error too: the encoder will not pick a
|
|
44
|
+
character encoding for you, and it never quietly writes a different character than the one you gave
|
|
45
|
+
it. Encode such content yourself and pass the `Uint8Array`.
|
|
43
46
|
- **Vendor profiles.** `defineAstmProfile()` builds a provenance-backed profile whose tolerances
|
|
44
47
|
downgrade _expected_, non-safety-critical deviations to a `PROFILE_QUIRK_APPLIED` warning without
|
|
45
48
|
ever altering a value, behind a safety gate that refuses to tolerate any result value, flag,
|
package/dist/index.cjs
CHANGED
|
@@ -2226,18 +2226,37 @@ var AstmFrameEncodeError = class extends Error {
|
|
|
2226
2226
|
code;
|
|
2227
2227
|
/** Index of the offending record within the input, when applicable. */
|
|
2228
2228
|
recordIndex;
|
|
2229
|
+
/**
|
|
2230
|
+
* Position of the offending character within that record, when applicable:
|
|
2231
|
+
* its index in the string, never the character itself and never its code
|
|
2232
|
+
* point. Enough to find it in the caller's own data.
|
|
2233
|
+
*/
|
|
2234
|
+
characterIndex;
|
|
2229
2235
|
/** @internal */
|
|
2230
|
-
constructor(message, recordIndex) {
|
|
2236
|
+
constructor(message, recordIndex, code = "ASTM_FRAME_EMPTY_RECORD", characterIndex) {
|
|
2231
2237
|
super(message);
|
|
2232
2238
|
this.name = "AstmFrameEncodeError";
|
|
2233
|
-
this.code =
|
|
2239
|
+
this.code = code;
|
|
2234
2240
|
if (recordIndex !== void 0) this.recordIndex = recordIndex;
|
|
2241
|
+
if (characterIndex !== void 0) this.characterIndex = characterIndex;
|
|
2235
2242
|
}
|
|
2236
2243
|
};
|
|
2237
|
-
|
|
2244
|
+
var MAX_SINGLE_BYTE_CODE_POINT = 255;
|
|
2245
|
+
function toBytes(input, recordIndex) {
|
|
2238
2246
|
if (typeof input !== "string") return input;
|
|
2239
2247
|
const out = new Uint8Array(input.length);
|
|
2240
|
-
for (let i = 0; i < input.length; i += 1)
|
|
2248
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
2249
|
+
const code = input.charCodeAt(i);
|
|
2250
|
+
if (code > MAX_SINGLE_BYTE_CODE_POINT) {
|
|
2251
|
+
throw new AstmFrameEncodeError(
|
|
2252
|
+
"A record contains a character above U+00FF, which is not a single byte. Encode the record with the character encoding your instrument uses and pass the bytes.",
|
|
2253
|
+
recordIndex,
|
|
2254
|
+
"ASTM_FRAME_UNENCODABLE_CHARACTER",
|
|
2255
|
+
i
|
|
2256
|
+
);
|
|
2257
|
+
}
|
|
2258
|
+
out[i] = code;
|
|
2259
|
+
}
|
|
2241
2260
|
return out;
|
|
2242
2261
|
}
|
|
2243
2262
|
function encodeFrame(text, frameNumber, isFinal) {
|
|
@@ -2254,7 +2273,7 @@ function composeAstmFrames(records, options = {}) {
|
|
|
2254
2273
|
const out = [];
|
|
2255
2274
|
let frameNumber = options.startFrameNumber ?? FIRST_FRAME_NUMBER;
|
|
2256
2275
|
records.forEach((record, recordIndex) => {
|
|
2257
|
-
const bytes = toBytes(record);
|
|
2276
|
+
const bytes = toBytes(record, recordIndex);
|
|
2258
2277
|
if (bytes.length === 0) {
|
|
2259
2278
|
throw new AstmFrameEncodeError(
|
|
2260
2279
|
"Cannot frame an empty record: a frame must carry record bytes.",
|
|
@@ -2296,7 +2315,7 @@ function concatBytes3(chunks) {
|
|
|
2296
2315
|
}
|
|
2297
2316
|
|
|
2298
2317
|
// src/index.ts
|
|
2299
|
-
var VERSION = "0.0.
|
|
2318
|
+
var VERSION = "0.0.10";
|
|
2300
2319
|
|
|
2301
2320
|
exports.ALL_ASTM_WARNING_CODES = ALL_ASTM_WARNING_CODES;
|
|
2302
2321
|
exports.AMBIGUOUS_CODES = AMBIGUOUS_CODES;
|