@cosyte/astm 0.0.11 → 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/dist/index.d.cts CHANGED
@@ -1103,7 +1103,7 @@ declare function detectFraming(bytes: Uint8Array, options?: DetectFramingOptions
1103
1103
  * Any warning code from any of the three registries: the record layer
1104
1104
  * (`ASTM_RECORD_*` / `ASTM_NONSTANDARD_DELIMITERS` / `ASTM_UNKNOWN_ESCAPE_SEQUENCE`),
1105
1105
  * the frame codec (`ASTM_FRAME_*`), or the LTP protocol layer (`ASTM_LTP_*`). A
1106
- * {@link QuirkTolerance} may *name* any of them, but the safety gate refuses all
1106
+ * {@link AstmQuirkTolerance} may *name* any of them, but the safety gate refuses all
1107
1107
  * but the small, benign, record-layer subset, so the union exists mainly to let
1108
1108
  * the gate reject a frame/LTP code with a precise, typed message.
1109
1109
  */
@@ -1136,7 +1136,7 @@ interface AstmProfileProvenance {
1136
1136
  readonly note?: string;
1137
1137
  }
1138
1138
  /**
1139
- * Optional structural narrowing for a {@link QuirkTolerance}. When present, the
1139
+ * Optional structural narrowing for an {@link AstmQuirkTolerance}. When present, the
1140
1140
  * tolerance applies only to warnings whose PHI-free {@link AstmPosition} matches
1141
1141
  * every provided field, so a profile can expect a deviation on one record type
1142
1142
  * (e.g. an unknown escape only inside `R` result values) without blanket-tolerating
@@ -1911,7 +1911,7 @@ interface AstmMessage {
1911
1911
  * The vendor profile that was active for this parse, when one applied: its
1912
1912
  * `name` and resolved `lineage` (attribution only; the profile object itself is
1913
1913
  * not embedded). Absent when no profile applied. A profile that tolerated a
1914
- * deviation shows up as a `PROFILE_QUIRK_APPLIED` entry in {@link warnings}, not
1914
+ * deviation shows up as a `PROFILE_QUIRK_APPLIED` entry in {@link AstmMessage.warnings}, not
1915
1915
  * here.
1916
1916
  */
1917
1917
  readonly profile?: {
@@ -2071,15 +2071,43 @@ declare function attachComments(records: readonly AstmRecord[], warnings: AstmRe
2071
2071
  * The serializer refuses: an embedded `CR`/`LF` is a typed {@link AstmSerializeError},
2072
2072
  * never emitted raw.
2073
2073
  *
2074
+ * **A value keeps the bytes the *frame* layer reserves, on purpose.** `STX`, `ETB`
2075
+ * and `ETX` are deliberately **not** refused here. They break a record only once it
2076
+ * is framed, and this layer returns a `string`, which is not yet on any wire: a
2077
+ * consumer on a raw transport, which is a real ASTM deployment this library models
2078
+ * (`detectFraming`), gets such a value back byte for byte through parse and emit,
2079
+ * measured on every modeled value. Refusing it here would take a
2080
+ * byte the caller genuinely supplied away from consumers who never frame anything.
2081
+ * `composeAstmFrames` is the total gate on the framed route, including through
2082
+ * `serializeFramedAstm`, and refuses it there (`ASTM_FRAME_RESERVED_BYTE`).
2083
+ * `CR`/`LF` are different: they end a *record*, so they corrupt this layer's own
2084
+ * output, which is why they are refused at this one.
2085
+ *
2086
+ * **One position is not a modeled value and does not keep the byte:** the surplus
2087
+ * of a header's delimiter declaration, where any control character is dropped
2088
+ * silently (see `declarationResidual` below, where that disposition is argued and
2089
+ * where it long predates the frame-layer refusal). So "the record layer carries
2090
+ * these bytes" is a statement about values, not about every byte of every line.
2091
+ *
2074
2092
  * **Check the delimiter set before writing.** Three conditions are required for the
2075
2093
  * emitted bytes to read back as the records that produced them (each separator exactly
2076
2094
  * one character, no separator a `CR`/`LF`, no two separators the same character) and a
2077
2095
  * set failing any of them is a typed {@link AstmSerializeError}
2078
2096
  * (`ASTM_EMIT_INVALID_DELIMITERS`) rather than output this library's own parser would
2079
- * reject or silently re-read with a different field tree. These are **necessary**
2080
- * conditions, not a guarantee of readback: a set can pass all three and still produce a
2081
- * stream that reads back wrong, a separator that collides with a record's type letter
2082
- * is the known case, and it corrupts identically with or without this check.
2097
+ * reject or silently re-read with a different field tree. These are conditions on the
2098
+ * **set alone**, so they are not a readback guarantee on their own: a set can pass all
2099
+ * three and still collide with a particular record's type letter, which is why that is
2100
+ * checked per record, on the bytes actually written
2101
+ * (`ASTM_EMIT_TYPE_LETTER_COLLISION`). That one is a **transcoding** condition, not
2102
+ * merely a caller's choice of set, so it is reachable on the default canonical path
2103
+ * too, when a record was read under a set other than the one being emitted.
2104
+ *
2105
+ * **What is still not guaranteed.** Together the two checks guarantee that every
2106
+ * record re-reads as its own *type*. They do not guarantee that every *field* of it
2107
+ * lands where it did: an escape sequence whose body is itself a delimiter is read as
2108
+ * one opaque atom, so that delimiter never becomes a boundary and the fields after it
2109
+ * shift. That is reported on the parse side (`ASTM_UNKNOWN_ESCAPE_SEQUENCE`) and is
2110
+ * not what these refusals cover.
2083
2111
  */
2084
2112
 
2085
2113
  /**
@@ -2104,7 +2132,8 @@ declare class AstmSerializeError extends Error {
2104
2132
  /**
2105
2133
  * Stable discriminant. `ASTM_EMIT_UNENCODABLE_VALUE` for a `CR`/`LF` in a value;
2106
2134
  * `ASTM_EMIT_INVALID_DELIMITERS` for a delimiter set that cannot be emitted
2107
- * reversibly.
2135
+ * reversibly; `ASTM_EMIT_TYPE_LETTER_COLLISION` for a record whose own type
2136
+ * letter the set being emitted with would escape away.
2108
2137
  */
2109
2138
  readonly code: AstmSerializeErrorCode;
2110
2139
  /** 0-based ordinal of the record within the message, when known. */
@@ -2119,8 +2148,14 @@ declare class AstmSerializeError extends Error {
2119
2148
  * (`CR`/`LF`), which the escape codec has no mnemonic for.
2120
2149
  * - `ASTM_EMIT_INVALID_DELIMITERS`: the delimiter set to emit against failed one
2121
2150
  * of the three conditions readback requires (see {@link serializeAstmRecords}).
2122
- * Those conditions are necessary rather than sufficient, so this code means a
2151
+ * Those conditions are checked against the *set alone*, so this code means a
2123
2152
  * set was rejected, not that every unreversible set is.
2153
+ * - `ASTM_EMIT_TYPE_LETTER_COLLISION`: this record's type letter would not be the
2154
+ * first character of its own emitted line, so the record would read back as a
2155
+ * **different** record. Raised per record rather than per set, because whether
2156
+ * a set collides depends on which record is being written: a record read under
2157
+ * one delimiter set and written under another can hit it with **no** set passed
2158
+ * at all, so it is not avoided by emitting on the default canonical path.
2124
2159
  *
2125
2160
  * @example
2126
2161
  * ```ts
@@ -2128,7 +2163,7 @@ declare class AstmSerializeError extends Error {
2128
2163
  * const code: AstmSerializeErrorCode = "ASTM_EMIT_INVALID_DELIMITERS";
2129
2164
  * ```
2130
2165
  */
2131
- type AstmSerializeErrorCode = "ASTM_EMIT_UNENCODABLE_VALUE" | "ASTM_EMIT_INVALID_DELIMITERS";
2166
+ type AstmSerializeErrorCode = "ASTM_EMIT_UNENCODABLE_VALUE" | "ASTM_EMIT_INVALID_DELIMITERS" | "ASTM_EMIT_TYPE_LETTER_COLLISION";
2132
2167
  /**
2133
2168
  * Escape-encode one component leaf for spec-clean emit: the inverse of
2134
2169
  * `decodeEscapes`. The **escape character itself is encoded first** (`&` → `&E&`)
@@ -2146,8 +2181,10 @@ type AstmSerializeErrorCode = "ASTM_EMIT_UNENCODABLE_VALUE" | "ASTM_EMIT_INVALID
2146
2181
  * @returns The escaped component text.
2147
2182
  * @throws {@link AstmSerializeError} for a `CR`/`LF` in the leaf
2148
2183
  * (`ASTM_EMIT_UNENCODABLE_VALUE`), or for a delimiter set failing one of the
2149
- * three conditions readback requires (`ASTM_EMIT_INVALID_DELIMITERS`), which
2150
- * are necessary, not sufficient, so a set can pass them and still not reverse.
2184
+ * three conditions readback requires (`ASTM_EMIT_INVALID_DELIMITERS`). This
2185
+ * helper takes a leaf and no record, so it never raises
2186
+ * `ASTM_EMIT_TYPE_LETTER_COLLISION`: a caller assembling a record line out of it
2187
+ * is outside that check and gets no guarantee that a type letter survives.
2151
2188
  * @example
2152
2189
  * ```ts
2153
2190
  * import { encodeComponent, CANONICAL_DELIMITERS } from "@cosyte/astm";
@@ -2171,9 +2208,13 @@ declare function encodeComponent(leaf: string, d: Delimiters, recordIndex?: numb
2171
2208
  * @param d - The delimiters to emit against; defaults to `H|\^&`.
2172
2209
  * @returns The record's wire text, terminator excluded.
2173
2210
  * @throws {@link AstmSerializeError} when a component contains an unencodable `CR`/`LF`
2174
- * (`ASTM_EMIT_UNENCODABLE_VALUE`), or when `d` fails one of the three conditions
2175
- * readback requires (`ASTM_EMIT_INVALID_DELIMITERS`): necessary conditions, not
2176
- * sufficient ones, so a set can pass them and still not reverse.
2211
+ * (`ASTM_EMIT_UNENCODABLE_VALUE`), when `d` fails one of the three conditions
2212
+ * readback requires (`ASTM_EMIT_INVALID_DELIMITERS`), or when this record's type
2213
+ * letter would not be the first character of its own emitted line
2214
+ * (`ASTM_EMIT_TYPE_LETTER_COLLISION`), which would make it read back as a
2215
+ * different record. That last one is a transcoding condition and needs no `d`
2216
+ * argument to fire: a record read under a different delimiter set can carry a
2217
+ * type letter the canonical set escapes away.
2177
2218
  * @example
2178
2219
  * ```ts
2179
2220
  * import { serializeAstmRecord, parseAstmRecords } from "@cosyte/astm";
@@ -2202,18 +2243,29 @@ declare function serializeAstmRecord(record: AstmRecord, d?: Delimiters): string
2202
2243
  * fails is an {@link AstmSerializeError} with code `ASTM_EMIT_INVALID_DELIMITERS`.
2203
2244
  * This is stricter than the parser, which reads some sets it cannot reverse, so
2204
2245
  * `serializeAstmRecords(msg, msg.delimiters)` can refuse a message that parsed:
2205
- * in exactly the cases where it used to emit a stream that read back wrong. The
2206
- * three conditions are necessary rather than sufficient: a set that passes them can
2207
- * still read back wrong if a separator collides with a record's type letter, which
2208
- * this check does not claim to catch.
2246
+ * in exactly the cases where it used to emit a stream that read back wrong.
2247
+ *
2248
+ * **And each record is checked against the set it is being written with.** Those
2249
+ * three conditions read the set alone, so they cannot see that `field` = `R`
2250
+ * escapes an `R` record's own type letter away. Every record's emitted line is
2251
+ * therefore checked to start with the letter the record models, and one that does
2252
+ * not is `ASTM_EMIT_TYPE_LETTER_COLLISION` rather than a stream that reads back as
2253
+ * different records. What neither check promises is that every *field* lands where
2254
+ * it did: an escape sequence whose body is a delimiter is an opaque atom, so that
2255
+ * delimiter never becomes a boundary, and the parse side reports that
2256
+ * (`ASTM_UNKNOWN_ESCAPE_SEQUENCE`) rather than emit refusing it.
2209
2257
  *
2210
2258
  * @param input - A parsed {@link AstmMessage} or a list of {@link AstmRecord}s.
2211
2259
  * @param d - The delimiters to emit against; defaults to the canonical `H|\^&` set.
2212
2260
  * @returns The serialized record stream (`CR` after every record).
2213
2261
  * @throws {@link AstmSerializeError} when a component contains an unencodable `CR`/`LF`
2214
- * (`ASTM_EMIT_UNENCODABLE_VALUE`), or when `d` fails one of the three conditions
2215
- * readback requires (`ASTM_EMIT_INVALID_DELIMITERS`): necessary conditions, not
2216
- * sufficient ones, so a set can pass them and still not reverse.
2262
+ * (`ASTM_EMIT_UNENCODABLE_VALUE`), when `d` fails one of the three conditions
2263
+ * readback requires (`ASTM_EMIT_INVALID_DELIMITERS`), or when a record's type
2264
+ * letter would not be the first character of its own emitted line
2265
+ * (`ASTM_EMIT_TYPE_LETTER_COLLISION`). The last of the three fires on the
2266
+ * **default** canonical path as well: it compares each record against the set
2267
+ * being emitted with, so a record read under a different set can carry a type
2268
+ * letter the canonical set escapes away. Omitting `d` is not a way around it.
2217
2269
  * @example
2218
2270
  * ```ts
2219
2271
  * import { parseAstmRecords, serializeAstmRecords } from "@cosyte/astm";
@@ -2232,8 +2284,10 @@ declare function serializeAstmRecords(input: AstmMessage | readonly AstmRecord[]
2232
2284
  * @returns The escaped field text.
2233
2285
  * @throws {@link AstmSerializeError} when a component contains an unencodable `CR`/`LF`
2234
2286
  * (`ASTM_EMIT_UNENCODABLE_VALUE`), or when `d` fails one of the three conditions
2235
- * readback requires (`ASTM_EMIT_INVALID_DELIMITERS`): necessary conditions, not
2236
- * sufficient ones, so a set can pass them and still not reverse.
2287
+ * readback requires (`ASTM_EMIT_INVALID_DELIMITERS`). Like
2288
+ * {@link encodeComponent} this takes no record, so it never raises
2289
+ * `ASTM_EMIT_TYPE_LETTER_COLLISION`: encoding a record's type-letter field
2290
+ * through it will escape that letter away without objecting.
2237
2291
  * @example
2238
2292
  * ```ts
2239
2293
  * import { serializeField, tokenizeRecord, CANONICAL_DELIMITERS } from "@cosyte/astm";
@@ -4353,7 +4407,8 @@ declare function decodeAstmFrames(bytes: Uint8Array, options?: FrameOptions): De
4353
4407
  * case of);
4354
4408
  * - the **frame number** `0`–`7`, starting at `1` and rolling over `7 → 0 → 1`,
4355
4409
  * continuous across every frame in the stream (as the decoder's sequence check
4356
- * expects);
4410
+ * expects); a caller may start the sequence elsewhere to compose one transfer
4411
+ * across several calls, but only at a number a frame can actually carry;
4357
4412
  * - the **240-byte text split**: a record longer than 240 bytes is split across
4358
4413
  * frames closed by `ETB` (intermediate) with a final `ETX`; the seven framing
4359
4414
  * control bytes are never counted toward the 240.
@@ -4375,6 +4430,12 @@ declare function decodeAstmFrames(bytes: Uint8Array, options?: FrameOptions): De
4375
4430
  * empty frame.
4376
4431
  * - `ASTM_FRAME_UNENCODABLE_CHARACTER`: a record given as a `string` holds a
4377
4432
  * character above `U+00FF`, which has no single byte to stand for.
4433
+ * - `ASTM_FRAME_RESERVED_BYTE`: a record holds a byte this layer reads as frame
4434
+ * structure (`STX`, `ETB` or `ETX`). Framing has no escape mechanism, so such
4435
+ * a byte cannot be carried inside a frame at all.
4436
+ * - `ASTM_FRAME_INVALID_START_FRAME_NUMBER`: `options.startFrameNumber` is not a
4437
+ * whole number from `0` to `7`. A frame's number is one ASCII digit, so a
4438
+ * value outside that range has no digit to be written as.
4378
4439
  *
4379
4440
  * @example
4380
4441
  * ```ts
@@ -4382,7 +4443,7 @@ declare function decodeAstmFrames(bytes: Uint8Array, options?: FrameOptions): De
4382
4443
  * const code: AstmFrameEncodeErrorCode = "ASTM_FRAME_UNENCODABLE_CHARACTER";
4383
4444
  * ```
4384
4445
  */
4385
- type AstmFrameEncodeErrorCode = "ASTM_FRAME_EMPTY_RECORD" | "ASTM_FRAME_UNENCODABLE_CHARACTER";
4446
+ type AstmFrameEncodeErrorCode = "ASTM_FRAME_EMPTY_RECORD" | "ASTM_FRAME_UNENCODABLE_CHARACTER" | "ASTM_FRAME_RESERVED_BYTE" | "ASTM_FRAME_INVALID_START_FRAME_NUMBER";
4386
4447
  /**
4387
4448
  * Thrown by {@link composeAstmFrames} when the input cannot be framed into a
4388
4449
  * spec-clean stream. Carries a stable code + positional context, never the
@@ -4404,6 +4465,30 @@ type AstmFrameEncodeErrorCode = "ASTM_FRAME_EMPTY_RECORD" | "ASTM_FRAME_UNENCODA
4404
4465
  * The refusal removes no capability, it routes you to the parameter that already
4405
4466
  * carried it.
4406
4467
  *
4468
+ * **The reserved-byte case, and why it has no escape hatch.** `STX`, `ETB` and
4469
+ * `ETX` are what {@link decodeAstmFrames} reads as frame structure: `STX` opens
4470
+ * a frame, and the first `ETB`/`ETX` after it *is* the end of that frame's text.
4471
+ * A record carrying one of those bytes therefore cannot be framed and read back,
4472
+ * whichever form it arrives in, and this layer has no escape sequence to hide one
4473
+ * behind. Passing bytes instead of a string does **not** route around it, because
4474
+ * the byte is the problem rather than the encoding. Take the byte out of the value
4475
+ * before framing: which byte belongs in a clinical value is the sender's call, not
4476
+ * this library's, so it refuses rather than substituting or deleting one.
4477
+ *
4478
+ * **The start-frame-number case, and why an option gets an error of its own.** A
4479
+ * frame's number is a single ASCII digit, `FN_ZERO + n` for `n` in `0`-`7`, and
4480
+ * nothing used to check that `options.startFrameNumber` was one. What went out
4481
+ * instead was whatever byte that sum truncated to: measured on this package's own
4482
+ * round trip, `-1` wrote `/` into the frame-number position and `NaN`, `Infinity`
4483
+ * and `-Infinity` each wrote a `NUL` byte into every frame of the stream, after
4484
+ * which the decoder read no frame number at all and emitted **none** of the
4485
+ * records. Out-of-domain values that happened to land back on a digit were worse
4486
+ * in a quieter way: `1.5` and `257` both produced the byte-for-byte stream a
4487
+ * `startFrameNumber` of `1` produces, so the option silently accepted a value it
4488
+ * documented as invalid. This one refusal is about the caller's own option rather
4489
+ * than about record content, so its message names the value received. Nothing
4490
+ * from the stream is quoted.
4491
+ *
4407
4492
  * @example
4408
4493
  * ```ts
4409
4494
  * import { composeAstmFrames, AstmFrameEncodeError } from "@cosyte/astm";
@@ -4422,7 +4507,9 @@ declare class AstmFrameEncodeError extends Error {
4422
4507
  /**
4423
4508
  * Position of the offending character within that record, when applicable:
4424
4509
  * its index in the string, never the character itself and never its code
4425
- * point. Enough to find it in the caller's own data.
4510
+ * point. Enough to find it in the caller's own data. For a record supplied as
4511
+ * a `Uint8Array` this is the offending byte's index, which is the same
4512
+ * position: a record string is a byte string, so the two indices coincide.
4426
4513
  */
4427
4514
  readonly characterIndex?: number;
4428
4515
  /** @internal */
@@ -4431,8 +4518,42 @@ declare class AstmFrameEncodeError extends Error {
4431
4518
  /** Options for {@link composeAstmFrames}. */
4432
4519
  interface ComposeFramesOptions {
4433
4520
  /**
4434
- * The frame number to start the sequence at (`0`–`7`). Defaults to
4435
- * {@link FIRST_FRAME_NUMBER} (`1`): the ASTM convention.
4521
+ * The frame number to start the sequence at: a whole number from `0` to `7`.
4522
+ * Defaults to `1`, the number ASTM gives the first frame of a transfer and the
4523
+ * one {@link decodeAstmFrames} expects to read first.
4524
+ *
4525
+ * **Any other value writes a continuation rather than the start of a transfer,
4526
+ * and that is what the option is for**: composing one transfer across more than
4527
+ * one call. Concatenating `composeAstmFrames(head)` with
4528
+ * `composeAstmFrames(tail, { startFrameNumber: n })`, where `n` is the number
4529
+ * after the last frame `head` used, is byte-identical to composing the whole
4530
+ * list in a single call and decodes with an empty warnings array, across the
4531
+ * `7 → 0` rollover included.
4532
+ *
4533
+ * **Decoded on its own, a stream that starts anywhere but `1` opens on a
4534
+ * sequence gap.** The decoder never bridges a gap silently, so it warns
4535
+ * (`ASTM_FRAME_SEQUENCE_GAP`) and does not emit that first record.
4536
+ * **What {@link parseFramedAstm} does after that varies with the message
4537
+ * shape and no rule is offered for it here.** It may throw, under more than
4538
+ * one code, and it may return a message that is simply one record short. **The
4539
+ * one thing that does hold is that the record layer never reports the loss**:
4540
+ * `parseFramedAstm` hands the record parser only the frames the codec
4541
+ * vouched for, so `message.warnings` carries what the surviving records
4542
+ * warrant and nothing about the record that did not survive. **Read
4543
+ * `frameWarnings`.** That is the cost of the option rather than a defect in
4544
+ * the caller's records, and it is why `1` is the default.
4545
+ *
4546
+ * **Finding the number to continue from.** Nothing here returns it, and the
4547
+ * frame count is not it once a record splits: decode the part you just
4548
+ * composed and read the last frame's number, then add one modulo 8, as in
4549
+ * `((decodeAstmFrames(part).frames.at(-1)?.frameNumber ?? 0) + 1) % 8`. A frame
4550
+ * carries no number only when the stream ends immediately after its `STX`,
4551
+ * which is not something this encoder writes, so that fallback never fires on
4552
+ * a part composed here. Getting the number wrong costs the record at the join,
4553
+ * warned rather than silently.
4554
+ *
4555
+ * A value outside `0`-`7`, or one that is not a whole number, is refused with
4556
+ * `ASTM_FRAME_INVALID_START_FRAME_NUMBER`.
4436
4557
  */
4437
4558
  readonly startFrameNumber?: number;
4438
4559
  }
@@ -4456,20 +4577,45 @@ interface ComposeFramesOptions {
4456
4577
  * resulting `Uint8Array`. Those two forms are the whole of what `records` accepts.
4457
4578
  * A caller reaching this from JavaScript with some **other** typed array is outside
4458
4579
  * the signature and gets no such refusal: each element is still written as its low
4459
- * byte, the same substitution described above. Pass a `Uint8Array` or a string.
4460
- *
4461
- * That covers the string-to-bytes step only. It is **not** a guarantee that any
4462
- * accepted record reads back: a record already carrying an `STX`, `ETX` or `ETB`
4463
- * byte is framed as given and re-decodes wrong, which this refusal does not touch
4464
- * and does not claim to.
4580
+ * byte, the same substitution described above. The reserved-byte check below does
4581
+ * not reach that route either, because it compares elements against the three byte
4582
+ * values and an element like `0x0103` is not one of them, though its low byte
4583
+ * becomes an `ETX` on the wire. Pass a `Uint8Array` or a string.
4584
+ *
4585
+ * **A record carrying a frame-structure byte is refused too, in either form.**
4586
+ * `STX`, `ETB` and `ETX` are what the decoder reads as the shape of a frame, and
4587
+ * framing has no escape sequence to hide one behind, so a record holding one is
4588
+ * `ASTM_FRAME_RESERVED_BYTE` rather than a frame truncated at that byte. It used
4589
+ * to be written as given, and the result was not reliably loud: with the two
4590
+ * following bytes matching, the truncated frame verifies and a whole record is
4591
+ * absorbed into the previous one with an empty warnings array at both layers.
4592
+ * Supplying the record as a `Uint8Array` does not route around this check.
4593
+ *
4594
+ * **`options.startFrameNumber` is checked before *this function* reads a record**,
4595
+ * so on this entry point the refusal never depends on the caller's data.
4596
+ * ({@link serializeFramedAstm} serializes every record *before* it gets here, so
4597
+ * a record that cannot be serialized is refused first on that route.) It has to
4598
+ * be a whole number from `0` to `7`, because a frame's number is one ASCII digit;
4599
+ * anything else is `ASTM_FRAME_INVALID_START_FRAME_NUMBER` rather than whatever
4600
+ * byte the arithmetic truncated to. A value other than the default `1` writes a
4601
+ * **continuation** of a sequence already in progress, which is what the option is
4602
+ * for, and a continuation decoded on its own opens on a sequence gap: see
4603
+ * {@link ComposeFramesOptions}.
4604
+ *
4605
+ * What is still **not** guaranteed: that an accepted record round-trips through
4606
+ * the *record* layer. A delimiter colliding with a record's type letter, for one,
4607
+ * frames and de-frames byte-exactly and still re-reads as a different record.
4465
4608
  *
4466
4609
  * @param records - The reassembled record byte-strings (`Uint8Array` or latin1
4467
4610
  * `string`), one entry per complete record.
4468
4611
  * @param options - Encode options.
4469
4612
  * @returns The framed byte stream.
4470
- * @throws {@link AstmFrameEncodeError} when the list is empty, a record has no
4471
- * bytes (`ASTM_FRAME_EMPTY_RECORD`), or a record string holds a character above
4472
- * `U+00FF` (`ASTM_FRAME_UNENCODABLE_CHARACTER`).
4613
+ * @throws {@link AstmFrameEncodeError} when `options.startFrameNumber` is not a
4614
+ * whole number from `0` to `7` (`ASTM_FRAME_INVALID_START_FRAME_NUMBER`), the
4615
+ * list is empty, a record has no bytes (`ASTM_FRAME_EMPTY_RECORD`), a record
4616
+ * string holds a character above `U+00FF`
4617
+ * (`ASTM_FRAME_UNENCODABLE_CHARACTER`), or a record holds an `STX`, `ETB` or
4618
+ * `ETX` byte (`ASTM_FRAME_RESERVED_BYTE`).
4473
4619
  * @example
4474
4620
  * ```ts
4475
4621
  * import { composeAstmFrames, decodeAstmFrames } from "@cosyte/astm";
@@ -4544,20 +4690,35 @@ declare function parseFramedAstm(bytes: Uint8Array, options?: FrameOptions & Ast
4544
4690
  * delimiters, embedded delimiters re-escaped) and then framed **independently**
4545
4691
  * (one record per `ETX`-closed frame run), so the framing exactly mirrors what
4546
4692
  * {@link decodeAstmFrames} reassembles: `parseFramedAstm(serializeFramedAstm(msg))`
4547
- * yields an equal message.
4693
+ * yields an equal message **with the default `startFrameNumber`**. That clause is
4694
+ * load-bearing rather than pedantic: a non-default start writes a continuation of
4695
+ * a sequence already in progress, and a continuation read on its own opens on a
4696
+ * sequence gap, so the decoder does not emit its first record and this round trip
4697
+ * does not hold for it. See {@link ComposeFramesOptions}.
4548
4698
  *
4549
4699
  * A value carrying a character above `U+00FF` is **refused** here rather than
4550
4700
  * framed: the record layer is happy to hold one, but a frame carries bytes and
4551
- * nothing in the message says which character encoding to turn it into. A message
4552
- * whose every value is Latin-1 is unaffected.
4701
+ * nothing in the message says which character encoding to turn it into.
4702
+ *
4703
+ * A value carrying a raw `STX`, `ETB` or `ETX` is refused too, and being Latin-1
4704
+ * is no exemption: those three are what {@link decodeAstmFrames} reads as the
4705
+ * shape of a frame, and framing has no escape sequence to hide one behind. The
4706
+ * record layer is happy to hold those as well, so this is a message that
4707
+ * serializes to records perfectly well and cannot be framed.
4553
4708
  *
4554
4709
  * @param input - A parsed {@link AstmMessage} or a list of {@link AstmRecord}s.
4555
4710
  * @param options - Frame-encode options.
4556
4711
  * @returns The framed byte stream.
4557
- * @throws {@link AstmSerializeError} when a value contains an unencodable `CR`/`LF`.
4558
- * @throws {@link AstmFrameEncodeError} when there are no records to frame
4559
- * (`ASTM_FRAME_EMPTY_RECORD`), or a value holds a character above `U+00FF`
4560
- * (`ASTM_FRAME_UNENCODABLE_CHARACTER`).
4712
+ * @throws {@link AstmSerializeError} when a value contains an unencodable `CR`/`LF`
4713
+ * (`ASTM_EMIT_UNENCODABLE_VALUE`), or when a record's own type letter would not
4714
+ * survive being written in the canonical set this function serializes with
4715
+ * (`ASTM_EMIT_TYPE_LETTER_COLLISION`). The second reaches messages parsed under a
4716
+ * **different** delimiter set, since this function passes no set of its own.
4717
+ * @throws {@link AstmFrameEncodeError} when `options.startFrameNumber` is not a
4718
+ * whole number from `0` to `7` (`ASTM_FRAME_INVALID_START_FRAME_NUMBER`), there
4719
+ * are no records to frame (`ASTM_FRAME_EMPTY_RECORD`), a value holds a
4720
+ * character above `U+00FF` (`ASTM_FRAME_UNENCODABLE_CHARACTER`), or a record
4721
+ * holds an `STX`, `ETB` or `ETX` byte (`ASTM_FRAME_RESERVED_BYTE`).
4561
4722
  * @example
4562
4723
  * ```ts
4563
4724
  * import { parseAstmRecords, serializeFramedAstm, parseFramedAstm } from "@cosyte/astm";