@cosyte/x12 0.0.15 → 0.0.17

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
@@ -9,6 +9,327 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ### Added
11
11
 
12
+ - **🩺 `X12_ISA_EXTRA_ELEMENT_SEPARATOR` - the ISA element split now has an arity check**
13
+ (`X12-ISA-ELEMENT-ARITY`). Additions-only: no decoding moved, no existing warning was suppressed or
14
+ narrowed, and nothing is re-framed.
15
+
16
+ `decodeIsa` splits the 105-byte ISA element area on the element separator, and the comment above it
17
+ asserted the result was "exactly 17 entries by construction because the element-separator-position
18
+ guard in `delimiters.ts` already verified the layout". That guard verifies the separator at all 16
19
+ fixed 005010 positions, which bounds the split from BELOW - it can never come out short. It was
20
+ never bounded from above. An ISA element value carrying that same byte splits again, so the element
21
+ comes back a prefix and everything after it is displaced. **How far is not derivable from
22
+ `isa.elements`**: more than one element can carry an extra separator, and one sitting between two
23
+ of them is displaced less than one sitting after both. `isa.raw` plus the ISA's fixed widths is the
24
+ only route back.
25
+
26
+ Planting the element separator inside each of the 16 fixed elements of one conformant interchange,
27
+ measured at `0.0.16`:
28
+
29
+ ```text
30
+ planted in split parts warnings at 0.0.16
31
+ ISA-01..10 18 X12_PRE_005010, X12_CONTROL_NUMBER_MISMATCH
32
+ ISA-11 - X12_INVALID_DELIMITERS (Tier-3, reached first)
33
+ ISA-12 18 X12_PRE_005010, X12_CONTROL_NUMBER_MISMATCH
34
+ ISA-13 18 X12_CONTROL_NUMBER_MISMATCH
35
+ ISA-14 18 (none)
36
+ ISA-15 18 (none)
37
+ ISA-16 - X12_INVALID_DELIMITERS (Tier-3, reached first)
38
+ ```
39
+
40
+ ISA-11 and ISA-16 ARE the in-band repetition and component separator declarations, so the plant
41
+ collides with them and the Tier-3 refusal reaches it before any of this. That is a boundary of the
42
+ probe, not a property of those two elements.
43
+
44
+ On the `ISA-01..10` and `ISA-12` rows the document declares `00501` at ISA-12's own fixed offset and
45
+ `X12_PRE_005010` fires anyway; `elements[13]`, the interchange control number and the reassociation
46
+ key, answers `"00501"`; and `elements[15]`, the test/production usage indicator, answers `"0"` on a
47
+ document whose ISA-15 says `P`. The `ISA-14` and `ISA-15` rows were reported on no channel at all.
48
+
49
+ `parseX12` raises the new warning BEFORE the ISA-derived checks that read `elements[12]` and
50
+ `elements[13]` by index, because when it fires those two may be reading a displaced element rather
51
+ than the one they name. `{ strict: true }` therefore escalates on it rather than on the warning that
52
+ follows. **That is a statement about `ix.warnings` and `onWarning` only.** `serializeX12(ix,
53
+ { specClean: true })` reconciles ISA-13 against IEA-02 off `isa.elements[13]` with no arity
54
+ awareness and never raises this code, so on that channel a lone `X12_CONTROL_NUMBER_MISMATCH` can be
55
+ a displaced read of a control number that matches byte for byte at its fixed span, and the absence
56
+ of this warning there is not evidence the header framed. Pre-existing, disclosed, not fixed.
57
+
58
+ **It is a report, not a repair.** A byte that is both an element's content under the ISA's fixed
59
+ widths and the separator that same segment declares in-band has two readings; the interchange is
60
+ not 005010-conformant either way, and nothing anyone here has read settles which reading to take,
61
+ so `isa.elements` is left exactly as the split produced it and `isa.raw` still carries all 106
62
+ bytes, which is the route back.
63
+
64
+ **The emit side is untouched, and this is not a build-side guard.** The fixed-width ISA slots go
65
+ through `pad` / `padControl` and never through the caller escaper, so `buildInterchange` still
66
+ writes such a caller value straight onto the wire. It does now surface there, because
67
+ `buildInterchange` returns `parseX12` of the bytes it just wrote:
68
+ `interchangeControlNumber: "0000*0001"` came back with ISA-13 reading `"0000"`, ISA-15 reading
69
+ `"0"` instead of `"P"`, IEA-02 displaced the same way so the control-number reconciliation agreed
70
+ with the misreading, and `warnings: []`.
71
+
72
+ ### Documentation
73
+
74
+ - **`IeaSegment`, `GsSegment`, `GeSegment`, `Ta1Segment` and `X12FunctionalGroup` no longer document
75
+ a raw envelope element as the value it holds** (`X12-ENVELOPE-VALUE-EXAMPLES`). **Documentation
76
+ only: the diff is comment-only, and `dist/index.mjs` and `dist/index.cjs` are byte-identical
77
+ before and after, so no behaviour moved.** The corrected text ships in `dist/index.d.ts`.
78
+
79
+ **Element values on those segment types are stored RAW, pre-`?`-unescape.** Their JSDoc named the
80
+ index and then named the value it holds - "IEA-02 interchange control number - must match ISA-13",
81
+ "GS-06 group control number", "TA1-01 (echoes the prior interchange's ISA-13 control number)" -
82
+ and repeated the promise inside an `@example` fence, which is the form a consumer copies. Measured
83
+ on one interchange where each documented non-ISA slot carries the sender-intended value `A*B`,
84
+ written on the wire in released form `A?*B`, **every one of those cells reads back `"A?*B"` and
85
+ not `"A*B"`.**
86
+
87
+ The same correction is applied to `parseX12`'s own `@example`, which handed out `// "HC"` at
88
+ `gs.elements[1]`.
89
+
90
+ On a `TA1` that `buildTA1` emitted and `parseX12` read back:
91
+
92
+ ```text
93
+ stated TA1-01 emitted TA1 elements[1] parseTA1()
94
+ "000000001" TA1*000000001*260601*1200*A*000 "000000001" "000000001"
95
+ "0000*0001" TA1*0000?*0001*260601*1200*A*000 "0000?*0001" "0000*0001"
96
+ "0000~0001" TA1*0000?~0001*260601*1200*A*000 "0000?~0001" "0000~0001"
97
+ "00000001?" TA1*00000001??*260601*1200*A*000 "00000001??" "00000001?"
98
+ ```
99
+
100
+ Every row `warnings: []`. TA1-01 is the reassociation key, so a consumer following the shipped
101
+ `@example` compared framed bytes against the ISA-13 it was acknowledging. `parseTA1` already read
102
+ the same bytes correctly, so the package disagreed with itself.
103
+
104
+ **Each block now carries the label `X12Segment` has always carried** - "Element values are stored
105
+ RAW, pre-`?`-unescape" - and the `@example` pointers keep their index and lose the promise. The
106
+ slot mapping (`elements[2]` = IEA-02) was true and is kept, and so are the element names, the
107
+ formats and the code-list citations, restated where they are true: as facts about the segment's
108
+ fields rather than about a raw element. No accessor, guard or test was added, and **no route is
109
+ named as the route.**
110
+
111
+ **The `IsaSegment` block is deliberately unchanged**, and no rule about the ISA is stated here in
112
+ either direction.
113
+
114
+ - **The documentation carriers that told you to read an envelope element's logical value through
115
+ `getSegmentValue` are corrected** (`X12-ENVELOPE-VALUE-ROUTES`). **Documentation and tests only: no
116
+ runtime line changed and the emitted JS is byte-identical, so no behaviour moved.**
117
+
118
+ `getSegmentValue` takes an `X12Segment`, which requires `id`. `IsaSegment`, `IeaSegment`,
119
+ `GsSegment`, `GeSegment`, `Ta1Segment` and the inline ST/SE types on `X12TransactionSet` declare
120
+ only `raw` and `elements`, so passing any one of them is `TS2345`, "Property 'id' is missing".
121
+ **All seven are measured here.** The carriers include
122
+ `KNOWN-LIMITATIONS.md`'s _"Read through `getSegmentValue` if you want the logical value"_ for a
123
+ `gs`, which is the one that owed you a route and so is the one replaced rather than cut.
124
+
125
+ **Two routes reach the decoded text, both were already published in this package, and neither is
126
+ universal.** `unescapeRelease` on the element string is a public export. Adding an `id` and taking
127
+ the dot-path is what `buildTA1`'s own JSDoc prescribes ("add one to read a TA1 through it") and
128
+ what this repo's tests do. They are not interchangeable:
129
+
130
+ ```text
131
+ element raw unescapeRelease add id + dot-path transmitted
132
+ GS-04, a released `*` "2026?*0601" "2026*0601" "2026*0601" "2026*0601"
133
+ GS-07, a REAL repetition "A^B" "A^B" "A" "A^B"
134
+ TA1-01, a released `^` "0000?^0001" "0000^0001" "0000^0001" "0000^0001"
135
+ ```
136
+
137
+ Every row `warnings: []`. A bare dot-path means repetition 0, which is why the GS-07 row differs.
138
+
139
+ **The public signature was NOT widened, and that was the decision rather than the cheap way out.**
140
+ `getSegmentValue`'s body never reads `id`, so widening its parameter to an `elements`-only
141
+ structural type is free, non-breaking and emits nothing. It was refused because the function
142
+ unescapes unconditionally while the ISA is positional, where a `?` is content and never an escape.
143
+ Widening admits `IsaSegment` and makes a silently wrong read of ISA-13, the reassociation key,
144
+ compile:
145
+
146
+ ```text
147
+ transmitted ISA-13 arity elements[13] decoded (both routes) elements[16]
148
+ "000000??1" 17 "000000??1" "000000?1" ":"
149
+ "0000?*001" 18 "0000?" "0000?" "P"
150
+ "00000001?" 17 "00000001?" "00000001?" ":"
151
+ ```
152
+
153
+ No route reads an ISA element correctly across those rows. On the middle one `decodeIsa` split on
154
+ the element separator, so `elements[13]` is a prefix and ISA-16 re-indexed onto ISA-15's value.
155
+ **So no route is prescribed for the ISA at all**, and the missing arity check on that split is
156
+ filed rather than fixed here. A capability that is right on six envelope types and silently wrong
157
+ on the seventh is not an ergonomic win, so the claim was cut instead and no code grew.
158
+
159
+ `test/parser-envelope-value-routes.test.ts` pins all of it. Seven `@ts-expect-error` assertions
160
+ cover the seven types, and `typecheck` runs over `test/`, so **they red if the signature is ever
161
+ widened**: widening is not forbidden, it is made loud, because the assertions have to be deleted
162
+ and that is where the ISA question gets answered. Under a mutation that widens the signature the
163
+ file reports seven unused directives, not one, which is what distinguishes it from a green vacuous
164
+ test.
165
+
166
+ Two earlier statements are deleted rather than reworded, because both are measured false. _"There
167
+ is no decoded read for an envelope element; `unescapeRelease` on the string is the only route"_:
168
+ there are two, and the second is prescribed inside this package. _"A raw-vs-`unescapeRelease` cell
169
+ on the ISA is a tautology that detects nothing"_: `unescapeRelease` does not know the ISA is
170
+ exempt, so on `"000000??1"` the cell differs.
171
+
172
+ Unchanged and filed: `parse-ta1.ts` and `KNOWN-LIMITATIONS.md` still mis-cite `X12Segment.elements`
173
+ for types that are not one. That statement is true where these were false, and it has a twin, so it
174
+ stays its own slice. `docs-content`'s dot-path example is scoped to body segments and was already
175
+ correct.
176
+
177
+ - **Eleven JSDoc pointers that sent a consumer to an envelope `elements[n]` for a value are deleted**
178
+ (`X12-ENVELOPE-VALUE-POINTERS`). **Documentation only: the diff is comment-only and the emitted JS
179
+ is byte-identical, so no behaviour changed.**
180
+
181
+ `X12_CONTROL_NUMBER_MISMATCH`'s JSDoc told you to read `isa.elements[13]`, `iea.elements[2]`,
182
+ `gs.elements[6]`, `ge.elements[2]`, `st.elements[2]` and `se.elements[2]` "off the model when you
183
+ need the values". Envelope element values are stored RAW, pre-`?`-unescape, so on a document whose
184
+ control number carries a release escape those reads hand back the framed bytes and not the value:
185
+ `gs.elements[6]` reads `"000?*99"` where the value is `"000*99"`. The same shape sat in
186
+ `X12_GROUP_COUNT_MISMATCH`'s and `X12_TRANSACTION_COUNT_MISMATCH`'s "both numbers stay on the model
187
+ (`iea.elements[1]` …)" parentheticals and in `serializeX12`'s module doc, which named three more.
188
+ All of those are **deleted**; nothing was added to make one true, and no accessor, guard or test
189
+ assertion was minted.
190
+
191
+ The surviving sentences still say the bytes stay on the model, which is true. What is gone is the
192
+ promise that a raw element index yields the decoded value.
193
+
194
+ Pointers that **label** their surface as raw are correct and are untouched: `raw.elements[5]` "is
195
+ the byte surface", `gs.elements[8]` gets "the sender's bytes rather than a normalization",
196
+ `seg.elements[0]` stays "verbatim".
197
+
198
+ **This does not bound the class.** `src/parser/types.ts` still names `iea.elements[2]`,
199
+ `gs.elements[6]`, `ge.elements[2]` and `ta1.elements[1]` for values in the `@example` blocks of the
200
+ envelope types, with no raw label; those are unchanged here and filed. `ta1.elements[1]` is the one
201
+ to know about: TA1-01 is the reassociation key, so a consumer following that example compares
202
+ framed bytes to the ISA-13 it acknowledges.
203
+
204
+ Two more pre-existing limits, unchanged and filed. **`getSegmentValue` cannot read an envelope
205
+ segment**: it takes an `X12Segment`, which requires `id`, and `IsaSegment`, `IeaSegment`,
206
+ `GsSegment`, `GeSegment`, `Ta1Segment` and the ST/SE types declare only `raw` and `elements`, so
207
+ passing one is a `TS2345`. And **the ISA element
208
+ split has no arity check**: `decodeIsa` splits on the element separator, so an ISA-13 carrying a raw
209
+ separator yields 18 elements where 17 are documented and silently re-indexes ISA-14/15/16.
210
+
211
+ ### Fixed
212
+
213
+ - **🩺 `implementationConventionReference` is POST-`?`-unescape in every typed reader that publishes
214
+ it** (`X12-ST03-READ-NOT-RELEASE-AWARE`). A behaviour change on any document whose `ST-03` carries
215
+ a release escape.
216
+
217
+ `X12TransactionSet.st.elements` is the ST segment as framed - post-element-split and PRE-unescape -
218
+ and five public readers were handing one
219
+ of those strings straight back on the model: `get837Claims`, `get277Status`,
220
+ `get277CADisposition`, `get278Request` and `get278Response` (`walk277` and `walk278` are shared, so
221
+ three reads serve the five). A sender that escaped a delimiter inside `ST-03` got the escape back
222
+ rather than the value it stated. The cells that were run, framed element text on the left:
223
+
224
+ ```text
225
+ ST-03 as framed published through 0.0.15 published now
226
+ "A??B" "A??B" "A?B"
227
+ "A?*B" "A?*B" "A*B"
228
+ "A?:B" "A?:B" "A:B"
229
+ "A?~B" "A?~B" "A~B"
230
+ "A?^B" "A?^B" "A^B"
231
+ ```
232
+
233
+ The grounding is this package disagreeing with itself and nothing else: every dot-path read
234
+ (`elementValue`, `elementOptional`, `componentOptional`) already unescaped, and `parse999` already
235
+ decoded `AK2-03`, the identically-named field. No TR3 clause is claimed, on either side.
236
+
237
+ **🛑 What DECIDES an outcome did not move, and that was a deliberate scope call.** The 837 variant
238
+ lookup, the 277 / 277CA `transactionType` discriminator and `get277CADisposition`'s admission gate
239
+ all still key on the RAW element text, so **no document changes variant, discriminator or
240
+ admission because of this entry.** They can differ: a letter is an admissible delimiter, so with
241
+ `componentSeparator: "X"` an `ST-03` framed as `005010?X222A1` decodes to `005010X222A1`, which the
242
+ variant table holds. Keying on the decoded text would make the declaration beat the `SVx` fallback
243
+ there, and on a document whose only service segment is an `SV2` that stops the line decoding and
244
+ raises `X12_837_SERVICE_LINE_NOT_DECODED`. That is a change to how an already published document
245
+ decodes a service line rather than a decode fix, so it is left open. **The difference is one-way:
246
+ no document that resolved a variant, or was admitted by `get277CADisposition`, stops doing so** -
247
+ no identifier any of the three tests is keyed on carries a delimiter or the release character, so
248
+ raw text equal to one decodes to itself.
249
+
250
+ **🛑 The consumer-visible consequence, stated rather than left to be inferred: the published
251
+ reference can name a guide this reader did NOT resolve to, and nothing warns about the
252
+ divergence.** On the `componentSeparator: "X"` document above,
253
+ `submission.implementationConventionReference` reads `005010X222A1`, which the variant table
254
+ holds, while `submission.variant` is `I` from the `SVx` fallback. **"Nothing warns" is NOT the
255
+ boundary of it.** On the same delimiters, a body with no `SVx` at all publishes `005010X222A1`
256
+ with `variant: "unknown"` and raises `X12_837_UNKNOWN_VARIANT`, and a body naming more than one
257
+ variant publishes it with `variant: "P"` and raises `X12_837_AMBIGUOUS_VARIANT` - in both cases
258
+ the code that fired says `ST-03` named no identifier this reader recognises while the model field
259
+ holds one it does. **That is why `X12_837_UNKNOWN_VARIANT`'s closing pointer at the model is
260
+ deleted rather than reworded.** The 277 shape is the
261
+ same:
262
+ the model publishes `005010X214` while `transactionType` is `claim-status` and
263
+ `get277CADisposition` returns `undefined`. **Through `0.0.15` the published value WAS the keyed
264
+ value, so the model could not disagree with itself. Gate on `variant` / `transactionType`, never
265
+ on the published reference.** No code moved and no code was minted.
266
+
267
+ **No normalisation and no new warning.** Nothing is trimmed, case-folded or prefix-matched, and a
268
+ whitespace-only `ST-03` is still published untrimmed. A dangling `?` at the end of the element
269
+ still raises no `X12_DANGLING_RELEASE_CHAR` here: the sink is a no-op, matching `getSegmentValue`'s
270
+ default, `parseTA1` and `parse999`, so every other element these readers decode drops it too.
271
+ Each reader's own empty / absent mapping is unchanged - `walk278` still collapses `""` to
272
+ `undefined`, the other two still publish `""`. **`tx.st.elements` is untouched and is still the
273
+ verbatim framed surface.** If you were applying `unescapeRelease` to
274
+ `submission.implementationConventionReference` yourself, drop that call.
275
+
276
+ - **🩺 `parseTA1`'s five decoded fields are POST-`?`-unescape, and an empty TA1-02 / TA1-03 / TA1-04
277
+ / TA1-05 is refused on emit** (`X12-TA1-RESIDUALS`). Both are behaviour changes. They ship as one
278
+ entry because they are the two ends of the same disagreement: this package's emit half releases a
279
+ TA1 element and its read half decoded the escape rather than the value.
280
+
281
+ **The read half.** The entry below made `buildTA1` release all five caller elements; `parseTA1`
282
+ kept reading `elements` verbatim, so a round trip through this package's own halves was not an
283
+ identity for any value carrying a delimiter or the release character. Measured at `0.0.15`, over
284
+ `parseX12` + `parseTA1` of what `buildTA1` had just emitted:
285
+
286
+ ```text
287
+ interchangeControlNumber raw emitted parseTA1 read dot-path read
288
+ "00000001?" TA1*00000001??*260601*1200*A*000 "00000001??" "00000001?"
289
+ "0000*0001" TA1*0000?*0001*260601*1200*A*000 "0000?*0001" "0000*0001"
290
+ "0000~0001" TA1*0000?~0001*260601*1200*A*000 "0000?~0001" "0000~0001"
291
+ "0000:0001" TA1*0000?:0001*260601*1200*A*000 "0000?:0001" "0000:0001"
292
+ "0000^0001" TA1*0000?^0001*260601*1200*A*000 "0000?^0001" "0000^0001"
293
+ ```
294
+
295
+ Every row `warnings: []`. TA1-01 is the reassociation key, so the left column is a key that
296
+ matches no ISA-13. The right column is the same element. The grounding is that
297
+ disagreement and nothing else - no clause anyone here has read settles what a TA1 element may
298
+ contain.
299
+
300
+ **`raw` is untouched and is still the verbatim byte surface**, so `ta1.raw.elements` still answers
301
+ the bytes. A value with no `?` in it is unchanged on both surfaces, which is every conformant TA1.
302
+ **If you were applying `unescapeRelease` to `parseTA1`'s output yourself, as `KNOWN-LIMITATIONS.md`
303
+ advised while this was open, drop that call.** The dangling-`?` warning
304
+ `X12_DANGLING_RELEASE_CHAR` is not surfaced here, because `parseTA1` returns no warnings channel;
305
+ `parse999` drops the same warning from the same helper for the same reason.
306
+
307
+ **The emit half.** `escapeRelease` early-returns on `""` and `buildTA1` carried a required-field
308
+ guard for TA1-01 alone, so the other four slots emitted an absent element with `warnings: []`:
309
+
310
+ ```text
311
+ interchangeDate: "" TA1*000000001**1200*A*000 interchangeTime: "" TA1*000000001*260601**A*000
312
+ ackCode: "" TA1*000000001*260601*1200**000 noteCode: "" TA1*000000001*260601*1200*R*
313
+ ```
314
+
315
+ Each now draws `AckBuildError` with the existing code `X12_ACK_INVALID_SPEC`, naming the slot and
316
+ the spec property and never the value. **No new error code is minted and no warning code moves.**
317
+ The grounding is again inside the package: `BuildTA1Spec` declares all five properties as required
318
+ `string`s, `""` is what defeats that at run time, and the in-package answer to an empty required
319
+ element is uniformly refusal.
320
+
321
+ **🛑 It does NOT trim, at any of the five slots** - a whitespace-only element still builds, exactly
322
+ as at TA1-01, because trimming is a normalisation rule and no source consulted for this package
323
+ states one. **🛑 It does not narrow what a NON-empty element may contain**: an out-of-enum
324
+ `ackCode: "X"` still builds and the read half still applies its documented fail-safe narrow to
325
+ `R`, which is the same answer an empty one gave. **🛑 Every guard that stood before it keeps its
326
+ precedence** - `enforceAcceptIsClean` still runs first, TA1-01 still draws the control-number
327
+ refusal, and every wrong-TYPED element still draws the escape helper's refusal, because all five
328
+ escapes run before any emptiness test. No spec that was refused before this change is refused
329
+ differently by it.
330
+
331
+ ### Added
332
+
12
333
  - **🩺 `Build837EnvelopeSpec.implementationConventionReference` - the caller now states the ST-03 /
13
334
  GS-08 implementation convention reference `build837P` / `build837I` / `build837D` declares**
14
335
  (`X12-837-EMIT-IDENTIFIER-FIXED`). One value, **both elements**; omit it and the builders emit
@@ -409,6 +730,80 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
409
730
 
410
731
  ### Changed
411
732
 
733
+ - **🩺 An EMPTY control number is now REFUSED on emit, where ISA-13 / IEA-02 used to be FABRICATED
734
+ out of it** (`X12-EMPTY-CONTROL-NUMBER-FABRICATED`). Reproduced on the base tree at `28b417f`,
735
+ which is `main` at published `0.0.15`.
736
+
737
+ Every builder that assembles an ISA zero-pads its control number to the nine characters ASC X12 .5
738
+ fixes ISA-13 at. `padControl("1", 9)` answering `"000000001"` is the point of that helper;
739
+ `padControl("", 9)` answering `"000000000"` was not, and nothing stood in front of it. So
740
+ `interchangeControlNumber: ""` produced a frozen, well-formed interchange carrying a nine-digit
741
+ control number the caller never supplied, ISA-13 reconciling perfectly against IEA-02, with an
742
+ empty warning array. **A control number is how an interchange is reconciled and acknowledged, so a
743
+ fabricated one does not fail: it succeeds against the wrong thing, and the sender gets no signal.**
744
+
745
+ **The class is wider than that slot, and it has two mechanisms rather than one.** The group and
746
+ transaction-set control numbers reach the wire through the release escaper, which early-returns on
747
+ `""`, so an empty one lost the required element. **The bytes are not the same in every builder**:
748
+ `buildInterchange` and `build999` join without trimming, so the element goes out empty, while the
749
+ seven domain builders share a segment helper that drops a trailing empty element, so the trailer
750
+ loses it outright. Measured one variable at a time, through `buildInterchange` and `build834`:
751
+
752
+ ```text
753
+ buildInterchange interchangeControlNumber: "" ISA*…*00501*000000000*0*P*:~ … ~IEA*1*000000000~
754
+ buildInterchange groupControlNumber: "" GS*HC*…*1200**X*005010X222A2~ … ~GE*1*~
755
+ buildInterchange transactionSetControlNumber: ST*837**005010X222A2~ … ~SE*3*~
756
+ build834 groupControlNumber: "" GS*BE*…*1200**X*005010X220A1~ … ~GE*1~
757
+ build834 transactionSetControlNumber: ST*834**005010X220A1~ … ~SE*21~
758
+ ```
759
+
760
+ Every one of those emitted `warnings: []`, which is the property that holds across both families:
761
+ no diagnostic on any channel separated an absent control number from a supplied one. The acknowledgment builders carried the same class at the slots
762
+ where they **echo** the document being acknowledged, which is the whole reason a sender can match
763
+ an ack to what they sent: `build999` emitted `AK1*HC**005010X222A2~` and `AK2*837*~`, and
764
+ `buildTA1` emitted `TA1**260601*1200*A*000`. The `buildTA1` case was already disclosed in
765
+ `KNOWN-LIMITATIONS.md` as tracked and open; `AK1-02` and `AK2-02` were not disclosed anywhere.
766
+
767
+ All of them now draw that builder's own typed, code-tagged refusal before anything is emitted,
768
+ naming the slot and the spec property: `build837: groupControlNumber is empty. GS-06 / GE-02 is a
769
+ required control number and this builder never invents one, so nothing is emitted.` **No new error
770
+ code is minted and no warning code moves.** What changes is that a call which used to hand you a
771
+ document now throws, and that a spec which is wrong in two ways can now report a different one of
772
+ the two. See the precedence bullet below: this is not "nothing a consumer branches on changes", and
773
+ a draft of this entry said exactly that.
774
+
775
+ **Why refuse rather than warn, stated as a decision because the standard does not settle it.**
776
+ 005010 says nothing about what a builder should do with an absent control number. The tiebreak is
777
+ CONSISTENCY inside this package: every other empty required element on the emit side is already
778
+ refused by name (`build835`'s `patientControlNumber`, `build837`'s `claimId`, `build834`'s
779
+ `maintenanceTypeCode`, `build278`'s `requestCategoryCode`, `build277`'s `categoryCode`), emit is
780
+ the deliberately strict half of this library, and a warning would have had to travel the parse
781
+ channel a builder returns, which is the registry consumers use to grade INBOUND documents.
782
+
783
+ **What it costs, and it is a real break:** a caller relying on `""` to mean "pad me a control
784
+ number" is broken by this, deliberately. What they were shipping was `000000000`, a real value a
785
+ trading partner may already have assigned to something else.
786
+ - **🛑 The guard is byte-strict `=== ""` and does NOT trim.** A whitespace-only control number is
787
+ still accepted and still padded: `" "` emits ISA-13 as `00000000 `. `buildTA1` does no padding at
788
+ all, so it emits whatever whitespace it was handed, verbatim. Trimming would be a normalisation
789
+ rule, no source consulted for this package states one, and the five in-package guards this
790
+ mirrors are byte-strict for the same reason. Pinned as a test and disclosed in
791
+ `KNOWN-LIMITATIONS.md` as the one shape still worth screening at your own boundary.
792
+ - **A SHORT control number still zero-pads.** The guard is not "ISA-13 must be nine characters":
793
+ `"1"` still emits `000000001`, which is what the padding is for.
794
+ - **🛑 Every guard sits at the envelope-assembly site, so every guard that runs BEFORE it keeps its
795
+ precedence, and that is the whole claim.** `build835`'s balance equation, `build999`'s AK9 count
796
+ reconciliation, `buildTA1`'s accept-must-mean-accept check, the 837's and 277's hierarchy checks
797
+ and `build834`'s unknown-maintenance-type refusal all still fire ahead of this one, each
798
+ verified. **What it does NOT preserve is an ordering against a defect detected LATER**, during
799
+ body assembly, which now reports the control-number refusal instead. Measured: `build999` with an
800
+ empty `interchangeControlNumber` and six AK9 syntax error codes threw `X12_ACK_COUNT_MISMATCH` at
801
+ `0.0.15` and throws `X12_ACK_INVALID_SPEC` here. If you branch on a specific builder error code,
802
+ that pairing moved.
803
+ - **No census of the slots that are NOT routed through the guard is published**, here or anywhere
804
+ else in the package. The claim is the property: a control number routed through the new guard is
805
+ refused when empty.
806
+
412
807
  - **🩺 `get837Claims` now recognises every published `ST-03` implementation-convention reference for
413
808
  the three 837 guides, and some already-published files therefore decode differently**
414
809
  (`X12-VARIANT-ICR-UNGROUNDED`). This is a grounding unit: the table `VARIANT_BY_ICR` had three keys
@@ -684,13 +1079,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
684
1079
  "An `X12Decimal` slot holds no identifier today" would have been the wrong kind of argument: a fact
685
1080
  about today's slots rather than a property of the guard.
686
1081
 
687
- **Two things that property does NOT say, and both were drafted as absolutes first.** The array
1082
+ **What that property does NOT say, and it was drafted as an absolute first.** The array
688
1083
  guard still renders a forged array-like's `length` and its class tag through `renderCallerValue`,
689
1084
  bounded: those describe the SHAPE a caller forged rather than an element's contents, and they are
690
- the whole diagnostic for `{ length: "9".repeat(120000) }`. And **only the segment-join guard names
691
- the SLOT** - `esc` and `escDec` name the BUILDER, a limit `caller-string.ts` already recorded, so on
692
- those two the echoed value used to stand in for a locator and now nothing does. That is a real
693
- diagnostic cost and it is disclosed rather than argued away.
1085
+ the whole diagnostic for `{ length: "9".repeat(120000) }`.
694
1086
 
695
1087
  **The segment guard's slot locator is now bounded by GRAMMAR rather than by length.** `parts[0]` is
696
1088
  caller-supplied in `buildInterchange`, which takes `[segmentId, ...elements]` wholesale, so it is
@@ -950,6 +1342,303 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
950
1342
 
951
1343
  ### Fixed
952
1344
 
1345
+ - **🩺 BREAKING (emit side): a delimiter that is not SHAPED like a delimiter is REFUSED by every
1346
+ builder** (`X12-EMIT-DELIMITER-SHAPE-UNCHECKED`). Each of the four roles must be a **string of
1347
+ exactly one visible character**, and the four must be **mutually distinct**. Reproduced on the base
1348
+ tree at `a21f8ea`.
1349
+
1350
+ **The rule is not invented for emit.** `detectDelimiters` already decides what a delimiter is for
1351
+ this package, and decides it as a Tier-3 fatal: one character at each of four fixed ISA positions, each
1352
+ visible, the four distinct, else `X12_INVALID_DELIMITERS` thrown even in lenient mode. The
1353
+ builders now import that predicate rather than restating it, so the emit refusal and the read fatal
1354
+ cannot drift apart into two ideas of what a delimiter is. **A builder composing a document its own
1355
+ parser refuses to read was disagreeing with itself.** Nothing is trimmed, coerced or substituted:
1356
+ the set is refused. The one-character requirement is structural rather than conventional, because
1357
+ the ISA is fixed-width per ASC X12 .5 and `Delimiters` records exactly one character per role.
1358
+
1359
+ **🛑 It is a UTF-16 code-unit rule and NOT a byte rule, and that difference is a residual it does
1360
+ not close.** A character that is one code unit but several bytes on the wire satisfies it and still
1361
+ displaces every ISA position after it: `componentSeparator: "\u00a7"` builds with `warnings: []`,
1362
+ and a byte-oriented receiver reads ISA-16 as `0xC2` and the terminator as `0xA7`. The smart quote
1363
+ `"\u2019"` that a companion-guide PDF gives you instead of `'` does the same. Disclosed, not
1364
+ guarded: the read side counts code units too, so moving one side alone would put them back out of
1365
+ step.
1366
+
1367
+ **Three mechanisms, and they are not one defect.**
1368
+ - **LENGTH.** `build837P` with `segmentTerminator: "~~"` built with `warnings: []` and put 31
1369
+ segment rows on a transaction whose `SE-01` declares 16, every other row a phantom the caller
1370
+ never wrote. **No claim is published about which roles were silent** - two drafts of this entry
1371
+ stated an asymmetry and both were measured false, the second inside the fix for the first.
1372
+ `buildTA1` with `{ elementSeparator: "||" }` returned `TA1||000000001||260601||1200||A||000`,
1373
+ which inside an ISA reads back with `TA1-01` empty and `ackCode: "R"` - an Accept emitted as a
1374
+ Reject. And `buildInterchange` with `componentSeparator: ":~"` built with `warnings: []` too,
1375
+ reading back through a well-formed ISA while the builder's own terminator became an uncounted
1376
+ empty segment, with no element value escaped against `:`.
1377
+ - **TYPE, where the join coerces and the escape does not.** `Array.prototype.join` coerces a
1378
+ non-string delimiter and the document frames on the coerced byte, but `escapeRelease` compares
1379
+ delimiters with `===`, so no element value is escaped against it. `build837P` with
1380
+ `componentSeparator: 1` emitted `SV1*HC199213`, which reads `SV1-01-2` back as `992` rather than
1381
+ the procedure code `99213`, and `CLM-05`'s place-of-service composite as `111B11`; `build271`
1382
+ with `repetitionSeparator: 1` emitted `EB*1**3011`, and `EB-03`'s two service type codes stopped
1383
+ reading back as two. `warnings: []` on both. 🩺 No unusual caller value is involved, and a
1384
+ length rule cannot reach this at all.
1385
+ - **`buildTA1` had no net.** It is the only builder that does not end in `parseX12`, so
1386
+ `elementSeparator: ""` RETURNED `TA10000000012606011200A000`: the reassociation key, the date,
1387
+ the time, the disposition and the note code in one undelimited blob.
1388
+
1389
+ **⚖️ Refuse rather than warn, and no code is minted.** The tiebreak is consistency with the two
1390
+ guards that shipped before it, not a 005010 clause; a warning would have to travel the READ
1391
+ registry a builder returns, which `#83` was refuted for. Each builder refuses with its own existing
1392
+ typed error. The check runs immediately after the release-character guard, so nothing that guard
1393
+ pinned moves and its equality test is byte for byte unchanged.
1394
+
1395
+ **🛑 What a caller catches MOVES, in both directions.** At base most mis-shaped sets failed as an
1396
+ `X12ParseError` with `X12_INVALID_DELIMITERS` escaping out of the `build*` call, carrying a 64-byte
1397
+ `snippet` of the interchange just composed. They now refuse earlier with that builder's own error
1398
+ and its existing code, **so a consumer catching the parse class around a `build*` call stops
1399
+ catching and one catching the build class starts.**
1400
+
1401
+ **🛑 It refuses shapes that built with `warnings: []`, and no count of them is published** - a
1402
+ draft said "two" and one more turned up the moment a shape the census had not enumerated was tried.
1403
+ **The plausible one is `segmentTerminator: "~\r\n"`.** If you declared that to get line-broken output it never produced
1404
+ any: CR/LF between segments is tolerated on READ, so the model recorded `~` and `serializeX12`
1405
+ emitted no line breaks. Reading a file written that way is unaffected; only declaring it on emit
1406
+ is.
1407
+
1408
+ **🛑 The read side and `serializeX12` are untouched.** `parseX12` accepts everything it accepted
1409
+ before, because documents declaring these sets exist and Postel's Law puts them on the lenient
1410
+ half. A letter or digit is still an admissible delimiter: ASC X12 constrains a delimiter by
1411
+ position, not by character class, and a delimiter that collides with the DATA is a different
1412
+ family that this does not close.
1413
+
1414
+ - **🩺 BREAKING (emit side): a delimiter set in which the release character `?` is also a delimiter
1415
+ is REFUSED by every builder, in all FOUR roles** (`X12-EMIT-DEGENERATE-RELEASE-DELIMITER`).
1416
+ Reproduced on the base tree at `51de7b2`.
1417
+
1418
+ **The property, stated so one more trigger byte cannot falsify it.** `escapeRelease` protects a
1419
+ byte by **prefixing** `?` to it, in whatever role `?` was declared, so when `?` is one of the four
1420
+ delimiters the protection is emitted as structure. The inverse holds at the same time: a builder
1421
+ joins composites with the component separator and repetitions with the repetition separator, so
1422
+ where either of those IS `?` the library's own structural join is emitted as an escape sequence.
1423
+
1424
+ **Two mechanisms, and only the first involves a caller's value.** The filed defect named the first
1425
+ and reached three roles; measured, the class is four roles, and the second mechanism fires on
1426
+ documents in which no value carries any trigger byte at all:
1427
+
1428
+ ```text
1429
+ elementSeparator "?" buildInterchange ["CLM","PATIENT?ACCT","150.00"]
1430
+ reads ["CLM","PATIENT","","ACCT","150.00"]
1431
+ segmentTerminator "?" buildInterchange ["CLM","PAT*ACCT","150.00"]
1432
+ CLM-01 reads "PAT", a phantom segment follows
1433
+ componentSeparator "?" build837P, EVERY document, no trigger byte:
1434
+ SV1-01-2 (the procedure code) reads undefined
1435
+ HI-01-2 (the diagnosis code) reads undefined
1436
+ repetitionSeparator "?" build271, EVERY document, no trigger byte:
1437
+ EB-03 "30" + "1" reads back as one code "30?1"
1438
+ warnings: [] on every row.
1439
+ ```
1440
+
1441
+ 🩺 The second mechanism is the sharper one: a procedure code and a diagnosis code are what a claim
1442
+ is adjudicated on, and neither the caller nor the receiver had any signal that they were fused.
1443
+
1444
+ **⚖️ REFUSE rather than warn, and the whole SET rather than the values that trip.** 005010 does not
1445
+ transmit a release character at all and settles none of this, so the tiebreak is CONSISTENCY with
1446
+ the guards this package already carries on emit and with emit being the strict half of Postel's
1447
+ Law here - the same call `X12-EMPTY-CONTROL-NUMBER-FABRICATED` made one slice earlier. A
1448
+ value-level guard was rejected on measurement: it cannot reach the second mechanism at all, and
1449
+ _"keep `?` out of your values"_ was already refuted once in this arc for protecting nobody. **No
1450
+ warning code is minted and no case moves onto a new code** - each builder refuses with its own
1451
+ existing typed error.
1452
+
1453
+ **🛑 It refuses specs that built at `0.0.15`, including ones that round-tripped through this
1454
+ library's own parser** - `componentSeparator: "?"` with a scalar value did. That round trip is not
1455
+ the bar: ISA-11 and ISA-16 transmit the declared set, so a conformant receiver splits on it, and
1456
+ what this library could read back says nothing about what they read.
1457
+
1458
+ **🛑 A message moves, never a code, and no count of what moved is published.** The check runs where
1459
+ a builder resolves its delimiters, so every guard a builder runs earlier keeps precedence
1460
+ (`build835`'s balance equations, `build837`'s spine, `build999`'s AK9 counts and `buildTA1`'s
1461
+ `enforceAcceptIsClean` all still report first, measured base vs head) and everything later yields.
1462
+ `requireControlNumber` is later in **every** builder that has one, so on a degenerate set both the
1463
+ empty-control-number and the non-string-control-number refusals above are preempted at every one of
1464
+ their slots - same code, different message. A draft of this bullet said _"one report moved"_ and a
1465
+ refuter measured that false, so the ordering is stated and the sites are not counted.
1466
+
1467
+ **🛑 It is an equality test on the value you declare, not a guarantee about the documents this
1468
+ library can compose.** State the bound as a property of the SET, never of the document. Whether a
1469
+ declared value is even shaped like a delimiter is the separate refusal below.
1470
+
1471
+ **🛑 The READ side is deliberately untouched, and so is `serializeX12`.** `parseX12` still accepts
1472
+ every degenerate set and still frames a degenerate body segment; `serializeX12` re-emits one byte
1473
+ for byte, because it works from a model a SENDER's bytes produced. Documents this library emitted
1474
+ before this guard exist, and Postel's Law puts them on the lenient half.
1475
+
1476
+ - **🩺 A control number that is not a STRING is refused on emit, so `interchangeControlNumber: []`
1477
+ and `new String("")` no longer fabricate ISA-13 as `000000000`**
1478
+ (`X12-CONTROL-NUMBER-GUARD-NOT-TYPE-CHECKED`). Reproduced on the base tree at `a226595`. The
1479
+ empty-control-number guard above is byte-strict, and byte-strict means a value that is not a string
1480
+ is not `""`, so a non-string walked past it and reached `padControl`, which reads `.length` and
1481
+ then concatenates. Measured through `buildInterchange`:
1482
+
1483
+ ```text
1484
+ interchangeControlNumber: [] ISA-13 = "000000000" warnings: []
1485
+ interchangeControlNumber: new String("") ISA-13 = "000000000" warnings: []
1486
+ interchangeControlNumber: new String("ABC") ISA-13 = "000000ABC" warnings: []
1487
+ interchangeControlNumber: new String(" ") ISA-13 = "00000000 " warnings: []
1488
+ ```
1489
+
1490
+ The first two are the **same fabricated `000000000`** the empty guard closed, reached through a
1491
+ different input type. The other two are silent **coercions** of a boxed string, a value this
1492
+ library refuses by name wherever it escapes one. `requireControlNumber` refuses a non-string ahead
1493
+ of the empty test now, reporting the TYPE through `caller-string.ts`'s describer and never echoing
1494
+ the value.
1495
+ - **🛑 No error code is minted and no warning code moves, but DIAGNOSTICS DO MOVE, so "nothing
1496
+ else changed" would be false.** `undefined` and `null` threw a bare `TypeError` with no `code`
1497
+ and now throw the builder's typed refusal, so **a consumer catching `TypeError` there no longer
1498
+ catches**. Shapes that used to make the builder write a malformed fixed-width ISA its own
1499
+ re-parse rejected as `X12_INVALID_DELIMITERS` refuse before anything is written now, so **a
1500
+ predicate on that code stops firing for them**. And **the MESSAGE moved at the control-number
1501
+ slots that already refused a non-string**, because they refuse from this guard one step earlier:
1502
+ same class, same code, wording that names the slot and the spec property. Read that as the
1503
+ property and not as a claim about what the old message said. **If you match on message text at a
1504
+ control-number slot, re-read it.**
1505
+ - **It narrows what a control number may BE, never what it may CONTAIN.** A whitespace-only
1506
+ control number is still accepted, unchanged and by design: trimming would be a
1507
+ normalisation rule and no source consulted for this package states one. The asymmetry that
1508
+ creates is stated rather than smoothed over - `new String(" ")` is refused because it is not a
1509
+ string, and the primitive `" "` is not. A SHORT control number still zero-pads. The ISA's other
1510
+ fixed-width slots go through `pad` rather than `padControl`, are guarded by no control-number
1511
+ test, and a numeric one still throws a bare `TypeError`; disclosed rather than closed.
1512
+
1513
+ - **🩺 A BODY segment in an interchange whose ELEMENT SEPARATOR is `?` now frames its elements,
1514
+ where it used to come back as ONE element with an id of `(non-spec)`**
1515
+ (`X12-BODY-DEGENERATE-RELEASE-SEPARATOR`). Reproduced on the base tree at `72bafc2`.
1516
+ `detectDelimiters` reads the element separator positionally out of ISA byte 4 and rejects only
1517
+ control characters, whitespace and a non-distinct set, so a sender may declare `?` there, and
1518
+ `buildInterchange` accepts `elementSeparator: "?"` from a caller. `src/parser/envelope.ts` guarded
1519
+ that degenerate set in both of its own splitters, once for the segment terminator and once for an
1520
+ envelope segment's elements. `decodeSegment` - which every BODY segment plus the `ST`, the `SE`
1521
+ and every retained orphan goes through - did not. It used the release-aware splitter, where a `?`
1522
+ consumes the byte after it, so on such an interchange no split ever happened:
1523
+
1524
+ ```text
1525
+ ST?837?0001?005010X222A1 id "(non-spec)", 1 element
1526
+ NM1?85?2?ACME CLINIC?????XX?1234567893 id "(non-spec)", 1 element
1527
+ SE?3?0001 id "(non-spec)", 1 element
1528
+ warnings: []
1529
+ ```
1530
+
1531
+ **🩺 The envelope framed correctly the whole time, which is what made it silent.** One group, one
1532
+ transaction, `GE-01`, `IEA-01`, `GS-06`/`GE-02` and `ST-02`/`SE-02` all reconciling, an empty
1533
+ warning array - and a transaction body no reader could see, because every reader in this package
1534
+ dispatches on `seg.id`. A consumer got an empty claim list out of a well-formed document.
1535
+ `buildInterchange` disagreed with itself the same way: it returns `parseX12` of the bytes it just
1536
+ wrote, so a caller passing `elementSeparator: "?"` got back a model holding none of the segments
1537
+ it had supplied. **It now reports the segments it wrote, for a value with no `?` in it - read that
1538
+ qualifier, it is load-bearing and the paragraph on the emit side below says why.**
1539
+
1540
+ **🛑 It changes how an already-published document decodes, deliberately**, exactly as the envelope
1541
+ splitter did in the release before it, and on the same tiebreak: CONSISTENCY with the guard this
1542
+ package already carried twice, not a spec clause. 005010 does not transmit a release character at
1543
+ all, so nothing in it says what a `?` means once a sender has declared `?` as structure. **What is
1544
+ NOT the same: this class is not symmetric.** A one-element segment with an id of `(non-spec)` is
1545
+ not a second reading of `NM1?85?2?ACME CLINIC`, so unlike that slice there is no direction in
1546
+ which the old framing was the right one.
1547
+
1548
+ **No warning code is added and no case moves onto a new code.** One is SUBTRACTED, in one place:
1549
+ `X12_DANGLING_RELEASE_CHAR` fired on any degenerate segment ending in an empty last element,
1550
+ because the check keys on a trailing `?`. With `?` as the separator that trailing byte is an empty
1551
+ element and not an unpaired escape, so `PER?IC?NAME?TE?5551234?` is silent now.
1552
+
1553
+ **🛑 The guard is per ROLE, and it is a READ-side guard.** A `?` REPETITION or COMPONENT separator
1554
+ still does not split. `escapeRelease` writes `??` for a literal `?` whatever role `?` was declared
1555
+ in, so documents this library emitted through `0.0.15` carry `CLM*PATIENT??ACCT*150.00` and
1556
+ `getSegmentValue(clm, "01")` reads `"PATIENT?ACCT"` back out of them. A literal split of those two
1557
+ roles would re-frame that as two empty components, so it would stop reading a value this library
1558
+ itself wrote. **That reason survives the emit-side entry above: those documents exist.**
1559
+
1560
+ **🩺 What else it does not close, pinned rather than left to be rediscovered.** On a degenerate set
1561
+ a `?~` still swallows the segment terminator: `findUnescapedTerminator` guards its own role only,
1562
+ so a segment ending in an empty last element puts a `?` immediately before the terminator and
1563
+ merges with its successor (`PER?IC?NAME?TE?5551234?EX?~SE?3?0001~` frames as one segment and raises
1564
+ `X12_MISSING_SE`). Framing is untouched - but the READ of that merged blob did move, so do not take
1565
+ it as "nothing moved": it now frames, so `~SE` and the SE's control number land in `PER`'s
1566
+ communication-number slots where at base they sat inside one `(non-spec)` element. Values are still
1567
+ RAW, `elements.join(sep)` still reproduces the segment byte for byte, and the ISA stays positional.
1568
+
1569
+ - **🩺 `buildInterchange` now release-escapes GS-04, GS-05 and GS-07, so the interchange it hands
1570
+ back reports the group date, group time and responsible agency code you passed**
1571
+ (`X12-INTERCHANGE-GS-EMIT-NOT-RELEASE-AWARE`). Reproduced on the base tree at `837d4bc`. The
1572
+ builder mapped its escaper over GS-01, GS-02, GS-03, GS-06 and GS-08 and wrote `groupDate`,
1573
+ `groupTime` and `responsibleAgencyCode` raw, and it returns `parseX12` of the bytes it just wrote,
1574
+ so a value carrying an active delimiter in one of those three took a slot of its own and shifted
1575
+ every element after it down one - inside a single call. Measured on one group with
1576
+ `versionRelease: "005010X222A2"` and `groupControlNumber: "1"`:
1577
+
1578
+ | spec field | read GS-06 | read GS-08 | warnings |
1579
+ | ------------------------------ | ---------- | ---------- | ------------------------------------------------------- |
1580
+ | `groupDate: "2026*0601"` | `"1200"` | `"X"` | `X12_CONTROL_NUMBER_MISMATCH` |
1581
+ | `groupTime: "12*00"` | `"00"` | `"X"` | `X12_CONTROL_NUMBER_MISMATCH` |
1582
+ | `responsibleAgencyCode: "X*Y"` | `"1"` | `"Y"` | none |
1583
+ | `groupTime: "12~00"` | absent | absent | `X12_UNEXPECTED_SEGMENT`, `X12_CONTROL_NUMBER_MISMATCH` |
1584
+ | `groupDate: "20260601?"` | `"X"` | absent | `X12_CONTROL_NUMBER_MISMATCH` |
1585
+
1586
+ **🩺 The `responsibleAgencyCode` row is the one to know about, because nothing was raised on any
1587
+ channel.** GS-06 kept its own slot, so it still reconciled against GE-02; what moved was GS-08, the
1588
+ version / release / industry identifier code, which is the element the entry above lets a caller
1589
+ state. All five rows now read the values the caller passed, with an empty warning array.
1590
+
1591
+ **The grounding is inside this package rather than in a spec clause**, the same tiebreak the two
1592
+ entries below record: one function disagreed with itself. It returns `parseX12` of bytes it wrote
1593
+ and then answers a slot out of its neighbour's, and `SegmentSpec`'s documented contract already
1594
+ said the builder applies the release escape so an active delimiter inside a value survives.
1595
+
1596
+ **🛑 It changes bytes this library already put on the wire, and that is the cost.** A value
1597
+ containing none of the four delimiters and no `?` is emitted byte-for-byte as before, which is
1598
+ every conformant GS-04 / GS-05 / GS-07.
1599
+
1600
+ **🛑 No warning code is added and no case moves onto a new code. Read the property rather than a
1601
+ direction list:** the interchange the call returns now reports the values the caller passed, where
1602
+ before it reported whatever the shift left in each slot. **What is narrower here than in the two
1603
+ entries below, and is the part to carry away: no reader moved.** No executable line under
1604
+ `src/parser/` changed, so an inbound document from a trading partner decodes exactly as it did at
1605
+ `0.0.15`; what changed is what this library emits, and therefore how its own output reads back.
1606
+ **Say it that way rather than "the parser is untouched"** - this slice's own graded review forced a
1607
+ JSDoc correction in `src/parser/envelope.ts`, where a stale census of the released GS/ST slots had
1608
+ been published.
1609
+
1610
+ **State the delimiter set by ROLE, never by byte.** `InterchangeSpec` lets you declare all four, so
1611
+ which BYTES shift is a property of the set you declared: with `elementSeparator: "|"` a GS-07 of
1612
+ `"X|Y"` is what took GS-08's slot and `"X*Y"` was inert. **Only the ELEMENT SEPARATOR and the
1613
+ SEGMENT TERMINATOR ever shifted the segment's own framing, plus a `?` immediately before the
1614
+ element separator.** The **repetition** and **component** separators moved the dot-path reader
1615
+ instead, and releasing them is a **gain** there: the composite read `"07-1"`
1616
+ answered `"X"` for `"X:Y"`. **The measured cost is a mid-string `?`, and only on the surfaces
1617
+ documented as raw** - `gs.elements[4]` reads `"2026??0601"` where it read `"2026?0601"`, while the
1618
+ dot-path read of that value unescapes and is unchanged. No total is published: that is what was
1619
+ measured, not a closed account. **A caller who was pre-releasing these values themselves is now
1620
+ escaping twice** and should drop the hand-rolled escape.
1621
+
1622
+ **A wrong-typed GS element still names its slot.** The type check runs over the unescaped parts, so
1623
+ a numeric `groupDate` refuses with `buildInterchange: "GS"-04 must be a string` rather than
1624
+ degrading to the builder-named message the escaper alone would produce; the five slots that already
1625
+ escaped gained the slot name with it. Same class, same `X12_BUILD_INVALID_SPEC`, and still no echo
1626
+ of the value. **`null` and `undefined` in these three fields are ABSENT, not refused** - each
1627
+ resolves through a default before either guard sees it.
1628
+
1629
+ **A LITERAL segment id this library writes is never escaped.** `esc` releases against the
1630
+ delimiter set the CALLER declared, and a `componentSeparator` of `"S"` is admissible, so mapping
1631
+ the escaper over element 0 would turn the literal `"GS"` into `G?S` and the group header would stop
1632
+ being a `GS`. `GE` / `ST` / `SE` / `IEA` already followed that rule. **Read "literal" strictly:** a
1633
+ `SegmentSpec` body segment carries a CALLER-supplied id, `buildTransaction` has released it since
1634
+ before this slice, and `SegmentSpec`'s own JSDoc still says it is emitted verbatim. That
1635
+ disagreement predates this slice, is unchanged by it, and is filed rather than closed here.
1636
+
1637
+ **What this does not close:** `buildInterchange`'s IEA-02 is padded rather than escaped and has to
1638
+ stay byte-equal to the fixed-width ISA-13 it reconciles against, so that is a decision of its own;
1639
+ the ISA fixed-width slots remain outside both guards; and an unescaped active delimiter is still
1640
+ not safe anywhere, because that is what a delimiter is.
1641
+
953
1642
  - **🩺 `buildTA1` now release-escapes its five caller-supplied elements, so an Accept acknowledgment
954
1643
  this library emits no longer reads back as a Reject** (`X12-TA1-EMIT-NOT-RELEASE-AWARE`).
955
1644
  Reproduced on the base tree at `e8f34b9`. `buildTA1` joined the five values with the element
@@ -999,19 +1688,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
999
1688
  `getSegmentValue(ta1, "01")` answered `"0000"` at `0.0.14` for a control number of `"0000^0001"`,
1000
1689
  silently truncating the reassociation key to the first repetition, and answers `"0000^0001"` now;
1001
1690
  the composite read `"01-1"` answered `"0000"` for `"0000:0001"` and answers the whole value now.
1002
- **The measured pure cost is a mid-string `?`, and only on the surfaces documented as raw**: `raw`,
1003
- `elements` and `parseTA1`'s fields read `"0000??0001"` where they read `"0000?0001"`, while every
1691
+ **The measured pure cost is a mid-string `?`, and only on the surfaces documented as raw**: `raw`
1692
+ and `elements` read `"0000??0001"` where they read `"0000?0001"`, while every
1004
1693
  dot-path read unescapes and answered `"0000?0001"` on both. No total is published: that is what was
1005
1694
  measured, not a closed account. `getSegmentValue` takes an `X12Segment` and `Ta1Segment` carries no
1006
1695
  `id`, so add one to read a TA1 through it. If you were escaping the value yourself, as `KNOWN-LIMITATIONS.md`
1007
1696
  advised while this was open, drop that - you are now escaping twice on both kinds of surface
1008
1697
  (`"00000001??"` in, `TA1*00000001????*…` out, `getSegmentValue` answering `"00000001??"`).
1009
1698
 
1010
- **The read half did not move.** `parseTA1` still reads elements RAW, pre-`?`-unescape, exactly as
1011
- `X12Segment.elements` has always documented, so a control number of `"00000001?"` now reads back as
1012
- `"00000001??"` rather than as `"00000001?*260601"`. Apply `unescapeRelease` if you need the value
1013
- rather than the bytes; unescaping on the read side would move every TA1 a consumer already reads
1014
- and is not done here.
1699
+ **The read half did not move in THIS entry**, so a control number of `"00000001?"` read back as
1700
+ `"00000001??"` rather than as `"00000001?*260601"`. The `X12-TA1-RESIDUALS` entry above is the one
1701
+ that moved it, and the clause that stood here telling you to apply `unescapeRelease` yourself is
1702
+ **deleted** rather than reworded: the library does it now.
1015
1703
 
1016
1704
  **`BuildTA1Options` gained `repetitionSeparator`, `componentSeparator` and `segmentTerminator`**
1017
1705
  beside the existing `elementSeparator`, the same four `build999` already takes. They exist for