Skip to main content
Version: v0.0.6

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

LayerStandardWhat it governsEntry points
RecordsASTM E1394-97 → CLSI LIS02-A2Message content: the H/P/O/R/C/Q/L/S/M record grammar with self-declaring delimitersparseAstmRecords, serializeAstmRecords, buildAstmMessage
FramesASTM E1381-02 → CLSI LIS01-A2Low-level transfer: STX-framed records, modulo-256 checksum, frame numbers, the ENQ/ACK/NAK/EOT handshakedecodeAstmFrames, composeAstmFrames, ltpReduce
CommonNoneShared vocabulary: the delimiter model, the escape codec, the date value, code-system provenance, the warning registriesCANONICAL_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