The two-layer architecture
ASTM is not one standard but two independent layers under one interface, and @cosyte/astm
mirrors that split exactly. Understanding the boundary is the key to using the library well: you
decode the layers independently and compose them only at the one point they meet.
Two standards, one domain
| Layer | Standard | What it governs | Entry points |
|---|---|---|---|
| Records | ASTM E1394-97 → CLSI LIS02-A2 | Message content: the H/P/O/R/C/Q/L/S/M record grammar with self-declaring delimiters | parseAstmRecords, serializeAstmRecords, buildAstmMessage |
| Frames | ASTM E1381-02 → CLSI LIS01-A2 | Low-level transfer: STX-framed records, modulo-256 checksum, frame numbers, the ENQ/ACK/NAK/EOT handshake | decodeAstmFrames, composeAstmFrames, ltpReduce |
| Common | None | Shared vocabulary: the delimiter model, the escape codec, the date value, code-system provenance, the warning registries | CANONICAL_DELIMITERS, value types |
The two standards share nothing but the domain and the payload boundary. A frame carries record bytes; a record knows nothing about frames. That is why the package is one repo, two composable layers, and a thin common core.
Decode the layers independently
Middleware often hands you already-de-framed record bytes (the framing was stripped upstream, or the vendor drops framing over raw TCP entirely). In that case you never touch the frame layer:
import { parseAstmRecords, results } from "@cosyte/astm";
// De-framed record bytes straight into the record parser.
const msg = parseAstmRecords(deFramedBytes);
results(msg)[0]?.value;
When you receive a raw byte stream off a serial line or socket, the frame layer decodes it first, and
parseFramedAstm composes the two at the edge, only frames the framing layer vouched for (checksum
verified, in sequence) ever reach the record parser:
import { parseFramedAstm, results } from "@cosyte/astm";
const { message, frames, frameWarnings } = parseFramedAstm(framedBytes);
results(message)[0]?.value; // parsed only from trusted, reassembled record bytes
The transport reality the frame layer handles
Over a serial line, records always arrive in full E1381 frames. Over TCP it varies within a single
vendor: some analyzers keep the full ENQ/ACK + STX/checksum framing, others drop all
low-level framing and stream records directly ("TCP itself ensures correctness"). detectFraming
auto-detects framed vs raw from the leading byte and defaults to framed on an ambiguous lead (with
a profile override): never a silent guess into data loss.
ltpReduce models the establishment → transfer → termination state machine as a pure reducer over
transport events, so it is deterministic and fully testable without a socket. The library never owns
the wire or the clock: it models the state transitions, and you drive them with your own I/O.
Why this shape
- Safety lives in the payload. The record layer leads because that is where a wrong value, flag, status, or patient ID causes clinical harm, so it gets the earliest, most rigorous treatment.
- Independent testing. Each layer is fuzzed and property-tested on its own; the record tokenizer
and the frame codec are separate byte-level surfaces with separate warning registries
(
WARNING_CODES,FRAME_WARNING_CODES,LTP_WARNING_CODES). - Composability. A consumer takes exactly the layer they need. The two only meet in
parseFramedAstm/serializeFramedAstm, and that seam is deliberately thin.
Where to go next
- Quickstart: parse a result, decode a framed stream, serialize and build.
- Core Concepts: the shared parser archetype and the tolerance tiers.
- What it does, and does not do: the honest boundary before you rely on it.