@cosyte/x12 0.0.14 → 0.0.16

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
@@ -7,8 +7,276 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Documentation
11
+
12
+ - **The documentation carriers that told you to read an envelope element's logical value through
13
+ `getSegmentValue` are corrected** (`X12-ENVELOPE-VALUE-ROUTES`). **Documentation and tests only: no
14
+ runtime line changed and the emitted JS is byte-identical, so no behaviour moved.**
15
+
16
+ `getSegmentValue` takes an `X12Segment`, which requires `id`. `IsaSegment`, `IeaSegment`,
17
+ `GsSegment`, `GeSegment`, `Ta1Segment` and the inline ST/SE types on `X12TransactionSet` declare
18
+ only `raw` and `elements`, so passing any one of them is `TS2345`, "Property 'id' is missing".
19
+ **All seven are measured here.** The carriers include
20
+ `KNOWN-LIMITATIONS.md`'s _"Read through `getSegmentValue` if you want the logical value"_ for a
21
+ `gs`, which is the one that owed you a route and so is the one replaced rather than cut.
22
+
23
+ **Two routes reach the decoded text, both were already published in this package, and neither is
24
+ universal.** `unescapeRelease` on the element string is a public export. Adding an `id` and taking
25
+ the dot-path is what `buildTA1`'s own JSDoc prescribes ("add one to read a TA1 through it") and
26
+ what this repo's tests do. They are not interchangeable:
27
+
28
+ ```text
29
+ element raw unescapeRelease add id + dot-path transmitted
30
+ GS-04, a released `*` "2026?*0601" "2026*0601" "2026*0601" "2026*0601"
31
+ GS-07, a REAL repetition "A^B" "A^B" "A" "A^B"
32
+ TA1-01, a released `^` "0000?^0001" "0000^0001" "0000^0001" "0000^0001"
33
+ ```
34
+
35
+ Every row `warnings: []`. A bare dot-path means repetition 0, which is why the GS-07 row differs.
36
+
37
+ **The public signature was NOT widened, and that was the decision rather than the cheap way out.**
38
+ `getSegmentValue`'s body never reads `id`, so widening its parameter to an `elements`-only
39
+ structural type is free, non-breaking and emits nothing. It was refused because the function
40
+ unescapes unconditionally while the ISA is positional, where a `?` is content and never an escape.
41
+ Widening admits `IsaSegment` and makes a silently wrong read of ISA-13, the reassociation key,
42
+ compile:
43
+
44
+ ```text
45
+ transmitted ISA-13 arity elements[13] decoded (both routes) elements[16]
46
+ "000000??1" 17 "000000??1" "000000?1" ":"
47
+ "0000?*001" 18 "0000?" "0000?" "P"
48
+ "00000001?" 17 "00000001?" "00000001?" ":"
49
+ ```
50
+
51
+ No route reads an ISA element correctly across those rows. On the middle one `decodeIsa` split on
52
+ the element separator, so `elements[13]` is a prefix and ISA-16 re-indexed onto ISA-15's value.
53
+ **So no route is prescribed for the ISA at all**, and the missing arity check on that split is
54
+ filed rather than fixed here. A capability that is right on six envelope types and silently wrong
55
+ on the seventh is not an ergonomic win, so the claim was cut instead and no code grew.
56
+
57
+ `test/parser-envelope-value-routes.test.ts` pins all of it. Seven `@ts-expect-error` assertions
58
+ cover the seven types, and `typecheck` runs over `test/`, so **they red if the signature is ever
59
+ widened**: widening is not forbidden, it is made loud, because the assertions have to be deleted
60
+ and that is where the ISA question gets answered. Under a mutation that widens the signature the
61
+ file reports seven unused directives, not one, which is what distinguishes it from a green vacuous
62
+ test.
63
+
64
+ Two earlier statements are deleted rather than reworded, because both are measured false. _"There
65
+ is no decoded read for an envelope element; `unescapeRelease` on the string is the only route"_:
66
+ there are two, and the second is prescribed inside this package. _"A raw-vs-`unescapeRelease` cell
67
+ on the ISA is a tautology that detects nothing"_: `unescapeRelease` does not know the ISA is
68
+ exempt, so on `"000000??1"` the cell differs.
69
+
70
+ Unchanged and filed: `parse-ta1.ts` and `KNOWN-LIMITATIONS.md` still mis-cite `X12Segment.elements`
71
+ for types that are not one. That statement is true where these were false, and it has a twin, so it
72
+ stays its own slice. `docs-content`'s dot-path example is scoped to body segments and was already
73
+ correct.
74
+
75
+ - **Eleven JSDoc pointers that sent a consumer to an envelope `elements[n]` for a value are deleted**
76
+ (`X12-ENVELOPE-VALUE-POINTERS`). **Documentation only: the diff is comment-only and the emitted JS
77
+ is byte-identical, so no behaviour changed.**
78
+
79
+ `X12_CONTROL_NUMBER_MISMATCH`'s JSDoc told you to read `isa.elements[13]`, `iea.elements[2]`,
80
+ `gs.elements[6]`, `ge.elements[2]`, `st.elements[2]` and `se.elements[2]` "off the model when you
81
+ need the values". Envelope element values are stored RAW, pre-`?`-unescape, so on a document whose
82
+ control number carries a release escape those reads hand back the framed bytes and not the value:
83
+ `gs.elements[6]` reads `"000?*99"` where the value is `"000*99"`. The same shape sat in
84
+ `X12_GROUP_COUNT_MISMATCH`'s and `X12_TRANSACTION_COUNT_MISMATCH`'s "both numbers stay on the model
85
+ (`iea.elements[1]` …)" parentheticals and in `serializeX12`'s module doc, which named three more.
86
+ All of those are **deleted**; nothing was added to make one true, and no accessor, guard or test
87
+ assertion was minted.
88
+
89
+ The surviving sentences still say the bytes stay on the model, which is true. What is gone is the
90
+ promise that a raw element index yields the decoded value.
91
+
92
+ Pointers that **label** their surface as raw are correct and are untouched: `raw.elements[5]` "is
93
+ the byte surface", `gs.elements[8]` gets "the sender's bytes rather than a normalization",
94
+ `seg.elements[0]` stays "verbatim".
95
+
96
+ **This does not bound the class.** `src/parser/types.ts` still names `iea.elements[2]`,
97
+ `gs.elements[6]`, `ge.elements[2]` and `ta1.elements[1]` for values in the `@example` blocks of the
98
+ envelope types, with no raw label; those are unchanged here and filed. `ta1.elements[1]` is the one
99
+ to know about: TA1-01 is the reassociation key, so a consumer following that example compares
100
+ framed bytes to the ISA-13 it acknowledges.
101
+
102
+ Two more pre-existing limits, unchanged and filed. **`getSegmentValue` cannot read an envelope
103
+ segment**: it takes an `X12Segment`, which requires `id`, and `IsaSegment`, `IeaSegment`,
104
+ `GsSegment`, `GeSegment`, `Ta1Segment` and the ST/SE types declare only `raw` and `elements`, so
105
+ passing one is a `TS2345`. And **the ISA element
106
+ split has no arity check**: `decodeIsa` splits on the element separator, so an ISA-13 carrying a raw
107
+ separator yields 18 elements where 17 are documented and silently re-indexes ISA-14/15/16.
108
+
109
+ ### Fixed
110
+
111
+ - **🩺 `implementationConventionReference` is POST-`?`-unescape in every typed reader that publishes
112
+ it** (`X12-ST03-READ-NOT-RELEASE-AWARE`). A behaviour change on any document whose `ST-03` carries
113
+ a release escape.
114
+
115
+ `X12TransactionSet.st.elements` is the ST segment as framed - post-element-split and PRE-unescape -
116
+ and five public readers were handing one
117
+ of those strings straight back on the model: `get837Claims`, `get277Status`,
118
+ `get277CADisposition`, `get278Request` and `get278Response` (`walk277` and `walk278` are shared, so
119
+ three reads serve the five). A sender that escaped a delimiter inside `ST-03` got the escape back
120
+ rather than the value it stated. The cells that were run, framed element text on the left:
121
+
122
+ ```text
123
+ ST-03 as framed published through 0.0.15 published now
124
+ "A??B" "A??B" "A?B"
125
+ "A?*B" "A?*B" "A*B"
126
+ "A?:B" "A?:B" "A:B"
127
+ "A?~B" "A?~B" "A~B"
128
+ "A?^B" "A?^B" "A^B"
129
+ ```
130
+
131
+ The grounding is this package disagreeing with itself and nothing else: every dot-path read
132
+ (`elementValue`, `elementOptional`, `componentOptional`) already unescaped, and `parse999` already
133
+ decoded `AK2-03`, the identically-named field. No TR3 clause is claimed, on either side.
134
+
135
+ **🛑 What DECIDES an outcome did not move, and that was a deliberate scope call.** The 837 variant
136
+ lookup, the 277 / 277CA `transactionType` discriminator and `get277CADisposition`'s admission gate
137
+ all still key on the RAW element text, so **no document changes variant, discriminator or
138
+ admission because of this entry.** They can differ: a letter is an admissible delimiter, so with
139
+ `componentSeparator: "X"` an `ST-03` framed as `005010?X222A1` decodes to `005010X222A1`, which the
140
+ variant table holds. Keying on the decoded text would make the declaration beat the `SVx` fallback
141
+ there, and on a document whose only service segment is an `SV2` that stops the line decoding and
142
+ raises `X12_837_SERVICE_LINE_NOT_DECODED`. That is a change to how an already published document
143
+ decodes a service line rather than a decode fix, so it is left open. **The difference is one-way:
144
+ no document that resolved a variant, or was admitted by `get277CADisposition`, stops doing so** -
145
+ no identifier any of the three tests is keyed on carries a delimiter or the release character, so
146
+ raw text equal to one decodes to itself.
147
+
148
+ **🛑 The consumer-visible consequence, stated rather than left to be inferred: the published
149
+ reference can name a guide this reader did NOT resolve to, and nothing warns about the
150
+ divergence.** On the `componentSeparator: "X"` document above,
151
+ `submission.implementationConventionReference` reads `005010X222A1`, which the variant table
152
+ holds, while `submission.variant` is `I` from the `SVx` fallback. **"Nothing warns" is NOT the
153
+ boundary of it.** On the same delimiters, a body with no `SVx` at all publishes `005010X222A1`
154
+ with `variant: "unknown"` and raises `X12_837_UNKNOWN_VARIANT`, and a body naming more than one
155
+ variant publishes it with `variant: "P"` and raises `X12_837_AMBIGUOUS_VARIANT` - in both cases
156
+ the code that fired says `ST-03` named no identifier this reader recognises while the model field
157
+ holds one it does. **That is why `X12_837_UNKNOWN_VARIANT`'s closing pointer at the model is
158
+ deleted rather than reworded.** The 277 shape is the
159
+ same:
160
+ the model publishes `005010X214` while `transactionType` is `claim-status` and
161
+ `get277CADisposition` returns `undefined`. **Through `0.0.15` the published value WAS the keyed
162
+ value, so the model could not disagree with itself. Gate on `variant` / `transactionType`, never
163
+ on the published reference.** No code moved and no code was minted.
164
+
165
+ **No normalisation and no new warning.** Nothing is trimmed, case-folded or prefix-matched, and a
166
+ whitespace-only `ST-03` is still published untrimmed. A dangling `?` at the end of the element
167
+ still raises no `X12_DANGLING_RELEASE_CHAR` here: the sink is a no-op, matching `getSegmentValue`'s
168
+ default, `parseTA1` and `parse999`, so every other element these readers decode drops it too.
169
+ Each reader's own empty / absent mapping is unchanged - `walk278` still collapses `""` to
170
+ `undefined`, the other two still publish `""`. **`tx.st.elements` is untouched and is still the
171
+ verbatim framed surface.** If you were applying `unescapeRelease` to
172
+ `submission.implementationConventionReference` yourself, drop that call.
173
+
174
+ - **🩺 `parseTA1`'s five decoded fields are POST-`?`-unescape, and an empty TA1-02 / TA1-03 / TA1-04
175
+ / TA1-05 is refused on emit** (`X12-TA1-RESIDUALS`). Both are behaviour changes. They ship as one
176
+ entry because they are the two ends of the same disagreement: this package's emit half releases a
177
+ TA1 element and its read half decoded the escape rather than the value.
178
+
179
+ **The read half.** The entry below made `buildTA1` release all five caller elements; `parseTA1`
180
+ kept reading `elements` verbatim, so a round trip through this package's own halves was not an
181
+ identity for any value carrying a delimiter or the release character. Measured at `0.0.15`, over
182
+ `parseX12` + `parseTA1` of what `buildTA1` had just emitted:
183
+
184
+ ```text
185
+ interchangeControlNumber raw emitted parseTA1 read dot-path read
186
+ "00000001?" TA1*00000001??*260601*1200*A*000 "00000001??" "00000001?"
187
+ "0000*0001" TA1*0000?*0001*260601*1200*A*000 "0000?*0001" "0000*0001"
188
+ "0000~0001" TA1*0000?~0001*260601*1200*A*000 "0000?~0001" "0000~0001"
189
+ "0000:0001" TA1*0000?:0001*260601*1200*A*000 "0000?:0001" "0000:0001"
190
+ "0000^0001" TA1*0000?^0001*260601*1200*A*000 "0000?^0001" "0000^0001"
191
+ ```
192
+
193
+ Every row `warnings: []`. TA1-01 is the reassociation key, so the left column is a key that
194
+ matches no ISA-13. The right column is the same element. The grounding is that
195
+ disagreement and nothing else - no clause anyone here has read settles what a TA1 element may
196
+ contain.
197
+
198
+ **`raw` is untouched and is still the verbatim byte surface**, so `ta1.raw.elements` still answers
199
+ the bytes. A value with no `?` in it is unchanged on both surfaces, which is every conformant TA1.
200
+ **If you were applying `unescapeRelease` to `parseTA1`'s output yourself, as `KNOWN-LIMITATIONS.md`
201
+ advised while this was open, drop that call.** The dangling-`?` warning
202
+ `X12_DANGLING_RELEASE_CHAR` is not surfaced here, because `parseTA1` returns no warnings channel;
203
+ `parse999` drops the same warning from the same helper for the same reason.
204
+
205
+ **The emit half.** `escapeRelease` early-returns on `""` and `buildTA1` carried a required-field
206
+ guard for TA1-01 alone, so the other four slots emitted an absent element with `warnings: []`:
207
+
208
+ ```text
209
+ interchangeDate: "" TA1*000000001**1200*A*000 interchangeTime: "" TA1*000000001*260601**A*000
210
+ ackCode: "" TA1*000000001*260601*1200**000 noteCode: "" TA1*000000001*260601*1200*R*
211
+ ```
212
+
213
+ Each now draws `AckBuildError` with the existing code `X12_ACK_INVALID_SPEC`, naming the slot and
214
+ the spec property and never the value. **No new error code is minted and no warning code moves.**
215
+ The grounding is again inside the package: `BuildTA1Spec` declares all five properties as required
216
+ `string`s, `""` is what defeats that at run time, and the in-package answer to an empty required
217
+ element is uniformly refusal.
218
+
219
+ **🛑 It does NOT trim, at any of the five slots** - a whitespace-only element still builds, exactly
220
+ as at TA1-01, because trimming is a normalisation rule and no source consulted for this package
221
+ states one. **🛑 It does not narrow what a NON-empty element may contain**: an out-of-enum
222
+ `ackCode: "X"` still builds and the read half still applies its documented fail-safe narrow to
223
+ `R`, which is the same answer an empty one gave. **🛑 Every guard that stood before it keeps its
224
+ precedence** - `enforceAcceptIsClean` still runs first, TA1-01 still draws the control-number
225
+ refusal, and every wrong-TYPED element still draws the escape helper's refusal, because all five
226
+ escapes run before any emptiness test. No spec that was refused before this change is refused
227
+ differently by it.
228
+
10
229
  ### Added
11
230
 
231
+ - **🩺 `Build837EnvelopeSpec.implementationConventionReference` - the caller now states the ST-03 /
232
+ GS-08 implementation convention reference `build837P` / `build837I` / `build837D` declares**
233
+ (`X12-837-EMIT-IDENTIFIER-FIXED`). One value, **both elements**; omit it and the builders emit
234
+ exactly what they always have.
235
+
236
+ **What this fixes, and it is a rejected claim rather than a mis-read one.** The builders stamped
237
+ `005010X222A2` / `005010X223A3` / `005010X224A2` with no way to change them. Two of those are not
238
+ what CMS and several state Medicaid companion guides require in ST-03 and GS-08 on production
239
+ professional and institutional claims, which ask for `005010X222A1` and `005010X223A2`, so **a
240
+ partner on one of those guides rejected every 837 this library built and a caller had no remedy.**
241
+ The read side of the same fact was grounded in the release before this one; this is the emit side.
242
+
243
+ **🛑 The defaults are UNCHANGED, on purpose, and that is the whole shape of the fix.** Which
244
+ published guide identifier a trading partner accepts is a **partner fact, not a spec fact**.
245
+ Re-stamping the default would silently change bytes this library already puts on the wire and
246
+ break the partners it works with today, so the default stays and the caller states what its own
247
+ partner asked for. Nothing about an existing call changes.
248
+
249
+ **What it refuses of its own, all `Claim837BuildError` with code `X12_837_BUILD_INVALID_SPEC`, and
250
+ none of them echoes the value you passed.** These sit on top of the element-type guard every string
251
+ slot in every builder already has, which refuses a non-string with the same code, so read the list
252
+ as what this field adds and not as a closed account of everything that can refuse. No total is
253
+ published.
254
+ - **Empty.** A trailing empty element is not emitted at all, so an empty reference would remove
255
+ ST-03 and GS-08 rather than send them empty - a silent structural loss on two required elements.
256
+ - **Carrying an active delimiter or the release character.** The value is refused rather than
257
+ escaped. A trading partner's parser is not obliged to honour the release escape, and a published
258
+ guide identifier has no legitimate use for a delimiter, so the refusal is kept even now that
259
+ this library's own reader would carry it (see the envelope-splitter entry below).
260
+ - **A reference this library's own reader resolves to a DIFFERENT 837 variant** (`005010X223A2`
261
+ handed to `build837P`). The emitted file would declare one variant and carry another's service
262
+ segments, and `get837Claims` would decode none of its service lines. Same class as the existing
263
+ refusal for a service line whose `variant` disagrees with the builder's. The message names the
264
+ variant the builder emits and deliberately not the one the reference belongs to.
265
+
266
+ **A reference outside the read table is emitted as given, and that is deliberate.** Nothing makes
267
+ the published-errata set provably exhaustive, so refusing an unrecognised identifier would claim
268
+ an exhaustiveness this library does not claim when reading either. The honest cost is pinned as a
269
+ test: on such a file this library's own reader falls back to the `SVx` scan, exactly as it does
270
+ for any unrecognised ST-03. The **length** is not bounded either, and the two elements' maxima
271
+ differ: GS-08 is data element 480 (`AN 1/12`), ST-03 is element 1705 (`AN 1/35`).
272
+
273
+ **🩺 A guard on this element cannot make the element trustworthy, and the disclosure above is
274
+ therefore about the whole envelope segment rather than these two elements.** An **unescaped**
275
+ active delimiter in a _different_ `GS` or `ST` element still ends its own element and shifts every
276
+ element after it, so ST-03 and GS-08 are then read out of a neighbour's slot, and no refusal here
277
+ can reach it. A **release-escaped** one no longer does - see the entry below, which landed after
278
+ this one and retracts the part of it that said escaping could not help.
279
+
12
280
  - **🩺 `X12_837_SERVICE_SEGMENT_REPEATED`, the 33rd Tier-2 warning code, plus the public factory
13
281
  `serviceSegmentRepeated(position)`** (`X12-837-SV1-OVERWRITE`). A **second** `SV1` / `SV2` / `SV3`
14
282
  arriving inside an **already-open** Loop 2400 now says so, at that repeated segment. Through
@@ -360,6 +628,80 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
360
628
 
361
629
  ### Changed
362
630
 
631
+ - **🩺 An EMPTY control number is now REFUSED on emit, where ISA-13 / IEA-02 used to be FABRICATED
632
+ out of it** (`X12-EMPTY-CONTROL-NUMBER-FABRICATED`). Reproduced on the base tree at `28b417f`,
633
+ which is `main` at published `0.0.15`.
634
+
635
+ Every builder that assembles an ISA zero-pads its control number to the nine characters ASC X12 .5
636
+ fixes ISA-13 at. `padControl("1", 9)` answering `"000000001"` is the point of that helper;
637
+ `padControl("", 9)` answering `"000000000"` was not, and nothing stood in front of it. So
638
+ `interchangeControlNumber: ""` produced a frozen, well-formed interchange carrying a nine-digit
639
+ control number the caller never supplied, ISA-13 reconciling perfectly against IEA-02, with an
640
+ empty warning array. **A control number is how an interchange is reconciled and acknowledged, so a
641
+ fabricated one does not fail: it succeeds against the wrong thing, and the sender gets no signal.**
642
+
643
+ **The class is wider than that slot, and it has two mechanisms rather than one.** The group and
644
+ transaction-set control numbers reach the wire through the release escaper, which early-returns on
645
+ `""`, so an empty one lost the required element. **The bytes are not the same in every builder**:
646
+ `buildInterchange` and `build999` join without trimming, so the element goes out empty, while the
647
+ seven domain builders share a segment helper that drops a trailing empty element, so the trailer
648
+ loses it outright. Measured one variable at a time, through `buildInterchange` and `build834`:
649
+
650
+ ```text
651
+ buildInterchange interchangeControlNumber: "" ISA*…*00501*000000000*0*P*:~ … ~IEA*1*000000000~
652
+ buildInterchange groupControlNumber: "" GS*HC*…*1200**X*005010X222A2~ … ~GE*1*~
653
+ buildInterchange transactionSetControlNumber: ST*837**005010X222A2~ … ~SE*3*~
654
+ build834 groupControlNumber: "" GS*BE*…*1200**X*005010X220A1~ … ~GE*1~
655
+ build834 transactionSetControlNumber: ST*834**005010X220A1~ … ~SE*21~
656
+ ```
657
+
658
+ Every one of those emitted `warnings: []`, which is the property that holds across both families:
659
+ no diagnostic on any channel separated an absent control number from a supplied one. The acknowledgment builders carried the same class at the slots
660
+ where they **echo** the document being acknowledged, which is the whole reason a sender can match
661
+ an ack to what they sent: `build999` emitted `AK1*HC**005010X222A2~` and `AK2*837*~`, and
662
+ `buildTA1` emitted `TA1**260601*1200*A*000`. The `buildTA1` case was already disclosed in
663
+ `KNOWN-LIMITATIONS.md` as tracked and open; `AK1-02` and `AK2-02` were not disclosed anywhere.
664
+
665
+ All of them now draw that builder's own typed, code-tagged refusal before anything is emitted,
666
+ naming the slot and the spec property: `build837: groupControlNumber is empty. GS-06 / GE-02 is a
667
+ required control number and this builder never invents one, so nothing is emitted.` **No new error
668
+ code is minted and no warning code moves.** What changes is that a call which used to hand you a
669
+ document now throws, and that a spec which is wrong in two ways can now report a different one of
670
+ the two. See the precedence bullet below: this is not "nothing a consumer branches on changes", and
671
+ a draft of this entry said exactly that.
672
+
673
+ **Why refuse rather than warn, stated as a decision because the standard does not settle it.**
674
+ 005010 says nothing about what a builder should do with an absent control number. The tiebreak is
675
+ CONSISTENCY inside this package: every other empty required element on the emit side is already
676
+ refused by name (`build835`'s `patientControlNumber`, `build837`'s `claimId`, `build834`'s
677
+ `maintenanceTypeCode`, `build278`'s `requestCategoryCode`, `build277`'s `categoryCode`), emit is
678
+ the deliberately strict half of this library, and a warning would have had to travel the parse
679
+ channel a builder returns, which is the registry consumers use to grade INBOUND documents.
680
+
681
+ **What it costs, and it is a real break:** a caller relying on `""` to mean "pad me a control
682
+ number" is broken by this, deliberately. What they were shipping was `000000000`, a real value a
683
+ trading partner may already have assigned to something else.
684
+ - **🛑 The guard is byte-strict `=== ""` and does NOT trim.** A whitespace-only control number is
685
+ still accepted and still padded: `" "` emits ISA-13 as `00000000 `. `buildTA1` does no padding at
686
+ all, so it emits whatever whitespace it was handed, verbatim. Trimming would be a normalisation
687
+ rule, no source consulted for this package states one, and the five in-package guards this
688
+ mirrors are byte-strict for the same reason. Pinned as a test and disclosed in
689
+ `KNOWN-LIMITATIONS.md` as the one shape still worth screening at your own boundary.
690
+ - **A SHORT control number still zero-pads.** The guard is not "ISA-13 must be nine characters":
691
+ `"1"` still emits `000000001`, which is what the padding is for.
692
+ - **🛑 Every guard sits at the envelope-assembly site, so every guard that runs BEFORE it keeps its
693
+ precedence, and that is the whole claim.** `build835`'s balance equation, `build999`'s AK9 count
694
+ reconciliation, `buildTA1`'s accept-must-mean-accept check, the 837's and 277's hierarchy checks
695
+ and `build834`'s unknown-maintenance-type refusal all still fire ahead of this one, each
696
+ verified. **What it does NOT preserve is an ordering against a defect detected LATER**, during
697
+ body assembly, which now reports the control-number refusal instead. Measured: `build999` with an
698
+ empty `interchangeControlNumber` and six AK9 syntax error codes threw `X12_ACK_COUNT_MISMATCH` at
699
+ `0.0.15` and throws `X12_ACK_INVALID_SPEC` here. If you branch on a specific builder error code,
700
+ that pairing moved.
701
+ - **No census of the slots that are NOT routed through the guard is published**, here or anywhere
702
+ else in the package. The claim is the property: a control number routed through the new guard is
703
+ refused when empty.
704
+
363
705
  - **🩺 `get837Claims` now recognises every published `ST-03` implementation-convention reference for
364
706
  the three 837 guides, and some already-published files therefore decode differently**
365
707
  (`X12-VARIANT-ICR-UNGROUNDED`). This is a grounding unit: the table `VARIANT_BY_ICR` had three keys
@@ -635,13 +977,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
635
977
  "An `X12Decimal` slot holds no identifier today" would have been the wrong kind of argument: a fact
636
978
  about today's slots rather than a property of the guard.
637
979
 
638
- **Two things that property does NOT say, and both were drafted as absolutes first.** The array
980
+ **What that property does NOT say, and it was drafted as an absolute first.** The array
639
981
  guard still renders a forged array-like's `length` and its class tag through `renderCallerValue`,
640
982
  bounded: those describe the SHAPE a caller forged rather than an element's contents, and they are
641
- the whole diagnostic for `{ length: "9".repeat(120000) }`. And **only the segment-join guard names
642
- the SLOT** - `esc` and `escDec` name the BUILDER, a limit `caller-string.ts` already recorded, so on
643
- those two the echoed value used to stand in for a locator and now nothing does. That is a real
644
- diagnostic cost and it is disclosed rather than argued away.
983
+ the whole diagnostic for `{ length: "9".repeat(120000) }`.
645
984
 
646
985
  **The segment guard's slot locator is now bounded by GRAMMAR rather than by length.** `parts[0]` is
647
986
  caller-supplied in `buildInterchange`, which takes `[segmentId, ...elements]` wholesale, so it is
@@ -901,6 +1240,462 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
901
1240
 
902
1241
  ### Fixed
903
1242
 
1243
+ - **🩺 BREAKING (emit side): a delimiter that is not SHAPED like a delimiter is REFUSED by every
1244
+ builder** (`X12-EMIT-DELIMITER-SHAPE-UNCHECKED`). Each of the four roles must be a **string of
1245
+ exactly one visible character**, and the four must be **mutually distinct**. Reproduced on the base
1246
+ tree at `a21f8ea`.
1247
+
1248
+ **The rule is not invented for emit.** `detectDelimiters` already decides what a delimiter is for
1249
+ this package, and decides it as a Tier-3 fatal: one character at each of four fixed ISA positions, each
1250
+ visible, the four distinct, else `X12_INVALID_DELIMITERS` thrown even in lenient mode. The
1251
+ builders now import that predicate rather than restating it, so the emit refusal and the read fatal
1252
+ cannot drift apart into two ideas of what a delimiter is. **A builder composing a document its own
1253
+ parser refuses to read was disagreeing with itself.** Nothing is trimmed, coerced or substituted:
1254
+ the set is refused. The one-character requirement is structural rather than conventional, because
1255
+ the ISA is fixed-width per ASC X12 .5 and `Delimiters` records exactly one character per role.
1256
+
1257
+ **🛑 It is a UTF-16 code-unit rule and NOT a byte rule, and that difference is a residual it does
1258
+ not close.** A character that is one code unit but several bytes on the wire satisfies it and still
1259
+ displaces every ISA position after it: `componentSeparator: "\u00a7"` builds with `warnings: []`,
1260
+ and a byte-oriented receiver reads ISA-16 as `0xC2` and the terminator as `0xA7`. The smart quote
1261
+ `"\u2019"` that a companion-guide PDF gives you instead of `'` does the same. Disclosed, not
1262
+ guarded: the read side counts code units too, so moving one side alone would put them back out of
1263
+ step.
1264
+
1265
+ **Three mechanisms, and they are not one defect.**
1266
+ - **LENGTH.** `build837P` with `segmentTerminator: "~~"` built with `warnings: []` and put 31
1267
+ segment rows on a transaction whose `SE-01` declares 16, every other row a phantom the caller
1268
+ never wrote. **No claim is published about which roles were silent** - two drafts of this entry
1269
+ stated an asymmetry and both were measured false, the second inside the fix for the first.
1270
+ `buildTA1` with `{ elementSeparator: "||" }` returned `TA1||000000001||260601||1200||A||000`,
1271
+ which inside an ISA reads back with `TA1-01` empty and `ackCode: "R"` - an Accept emitted as a
1272
+ Reject. And `buildInterchange` with `componentSeparator: ":~"` built with `warnings: []` too,
1273
+ reading back through a well-formed ISA while the builder's own terminator became an uncounted
1274
+ empty segment, with no element value escaped against `:`.
1275
+ - **TYPE, where the join coerces and the escape does not.** `Array.prototype.join` coerces a
1276
+ non-string delimiter and the document frames on the coerced byte, but `escapeRelease` compares
1277
+ delimiters with `===`, so no element value is escaped against it. `build837P` with
1278
+ `componentSeparator: 1` emitted `SV1*HC199213`, which reads `SV1-01-2` back as `992` rather than
1279
+ the procedure code `99213`, and `CLM-05`'s place-of-service composite as `111B11`; `build271`
1280
+ with `repetitionSeparator: 1` emitted `EB*1**3011`, and `EB-03`'s two service type codes stopped
1281
+ reading back as two. `warnings: []` on both. 🩺 No unusual caller value is involved, and a
1282
+ length rule cannot reach this at all.
1283
+ - **`buildTA1` had no net.** It is the only builder that does not end in `parseX12`, so
1284
+ `elementSeparator: ""` RETURNED `TA10000000012606011200A000`: the reassociation key, the date,
1285
+ the time, the disposition and the note code in one undelimited blob.
1286
+
1287
+ **⚖️ Refuse rather than warn, and no code is minted.** The tiebreak is consistency with the two
1288
+ guards that shipped before it, not a 005010 clause; a warning would have to travel the READ
1289
+ registry a builder returns, which `#83` was refuted for. Each builder refuses with its own existing
1290
+ typed error. The check runs immediately after the release-character guard, so nothing that guard
1291
+ pinned moves and its equality test is byte for byte unchanged.
1292
+
1293
+ **🛑 What a caller catches MOVES, in both directions.** At base most mis-shaped sets failed as an
1294
+ `X12ParseError` with `X12_INVALID_DELIMITERS` escaping out of the `build*` call, carrying a 64-byte
1295
+ `snippet` of the interchange just composed. They now refuse earlier with that builder's own error
1296
+ and its existing code, **so a consumer catching the parse class around a `build*` call stops
1297
+ catching and one catching the build class starts.**
1298
+
1299
+ **🛑 It refuses shapes that built with `warnings: []`, and no count of them is published** - a
1300
+ draft said "two" and one more turned up the moment a shape the census had not enumerated was tried.
1301
+ **The plausible one is `segmentTerminator: "~\r\n"`.** If you declared that to get line-broken output it never produced
1302
+ any: CR/LF between segments is tolerated on READ, so the model recorded `~` and `serializeX12`
1303
+ emitted no line breaks. Reading a file written that way is unaffected; only declaring it on emit
1304
+ is.
1305
+
1306
+ **🛑 The read side and `serializeX12` are untouched.** `parseX12` accepts everything it accepted
1307
+ before, because documents declaring these sets exist and Postel's Law puts them on the lenient
1308
+ half. A letter or digit is still an admissible delimiter: ASC X12 constrains a delimiter by
1309
+ position, not by character class, and a delimiter that collides with the DATA is a different
1310
+ family that this does not close.
1311
+
1312
+ - **🩺 BREAKING (emit side): a delimiter set in which the release character `?` is also a delimiter
1313
+ is REFUSED by every builder, in all FOUR roles** (`X12-EMIT-DEGENERATE-RELEASE-DELIMITER`).
1314
+ Reproduced on the base tree at `51de7b2`.
1315
+
1316
+ **The property, stated so one more trigger byte cannot falsify it.** `escapeRelease` protects a
1317
+ byte by **prefixing** `?` to it, in whatever role `?` was declared, so when `?` is one of the four
1318
+ delimiters the protection is emitted as structure. The inverse holds at the same time: a builder
1319
+ joins composites with the component separator and repetitions with the repetition separator, so
1320
+ where either of those IS `?` the library's own structural join is emitted as an escape sequence.
1321
+
1322
+ **Two mechanisms, and only the first involves a caller's value.** The filed defect named the first
1323
+ and reached three roles; measured, the class is four roles, and the second mechanism fires on
1324
+ documents in which no value carries any trigger byte at all:
1325
+
1326
+ ```text
1327
+ elementSeparator "?" buildInterchange ["CLM","PATIENT?ACCT","150.00"]
1328
+ reads ["CLM","PATIENT","","ACCT","150.00"]
1329
+ segmentTerminator "?" buildInterchange ["CLM","PAT*ACCT","150.00"]
1330
+ CLM-01 reads "PAT", a phantom segment follows
1331
+ componentSeparator "?" build837P, EVERY document, no trigger byte:
1332
+ SV1-01-2 (the procedure code) reads undefined
1333
+ HI-01-2 (the diagnosis code) reads undefined
1334
+ repetitionSeparator "?" build271, EVERY document, no trigger byte:
1335
+ EB-03 "30" + "1" reads back as one code "30?1"
1336
+ warnings: [] on every row.
1337
+ ```
1338
+
1339
+ 🩺 The second mechanism is the sharper one: a procedure code and a diagnosis code are what a claim
1340
+ is adjudicated on, and neither the caller nor the receiver had any signal that they were fused.
1341
+
1342
+ **⚖️ REFUSE rather than warn, and the whole SET rather than the values that trip.** 005010 does not
1343
+ transmit a release character at all and settles none of this, so the tiebreak is CONSISTENCY with
1344
+ the guards this package already carries on emit and with emit being the strict half of Postel's
1345
+ Law here - the same call `X12-EMPTY-CONTROL-NUMBER-FABRICATED` made one slice earlier. A
1346
+ value-level guard was rejected on measurement: it cannot reach the second mechanism at all, and
1347
+ _"keep `?` out of your values"_ was already refuted once in this arc for protecting nobody. **No
1348
+ warning code is minted and no case moves onto a new code** - each builder refuses with its own
1349
+ existing typed error.
1350
+
1351
+ **🛑 It refuses specs that built at `0.0.15`, including ones that round-tripped through this
1352
+ library's own parser** - `componentSeparator: "?"` with a scalar value did. That round trip is not
1353
+ the bar: ISA-11 and ISA-16 transmit the declared set, so a conformant receiver splits on it, and
1354
+ what this library could read back says nothing about what they read.
1355
+
1356
+ **🛑 A message moves, never a code, and no count of what moved is published.** The check runs where
1357
+ a builder resolves its delimiters, so every guard a builder runs earlier keeps precedence
1358
+ (`build835`'s balance equations, `build837`'s spine, `build999`'s AK9 counts and `buildTA1`'s
1359
+ `enforceAcceptIsClean` all still report first, measured base vs head) and everything later yields.
1360
+ `requireControlNumber` is later in **every** builder that has one, so on a degenerate set both the
1361
+ empty-control-number and the non-string-control-number refusals above are preempted at every one of
1362
+ their slots - same code, different message. A draft of this bullet said _"one report moved"_ and a
1363
+ refuter measured that false, so the ordering is stated and the sites are not counted.
1364
+
1365
+ **🛑 It is an equality test on the value you declare, not a guarantee about the documents this
1366
+ library can compose.** State the bound as a property of the SET, never of the document. Whether a
1367
+ declared value is even shaped like a delimiter is the separate refusal below.
1368
+
1369
+ **🛑 The READ side is deliberately untouched, and so is `serializeX12`.** `parseX12` still accepts
1370
+ every degenerate set and still frames a degenerate body segment; `serializeX12` re-emits one byte
1371
+ for byte, because it works from a model a SENDER's bytes produced. Documents this library emitted
1372
+ before this guard exist, and Postel's Law puts them on the lenient half.
1373
+
1374
+ - **🩺 A control number that is not a STRING is refused on emit, so `interchangeControlNumber: []`
1375
+ and `new String("")` no longer fabricate ISA-13 as `000000000`**
1376
+ (`X12-CONTROL-NUMBER-GUARD-NOT-TYPE-CHECKED`). Reproduced on the base tree at `a226595`. The
1377
+ empty-control-number guard above is byte-strict, and byte-strict means a value that is not a string
1378
+ is not `""`, so a non-string walked past it and reached `padControl`, which reads `.length` and
1379
+ then concatenates. Measured through `buildInterchange`:
1380
+
1381
+ ```text
1382
+ interchangeControlNumber: [] ISA-13 = "000000000" warnings: []
1383
+ interchangeControlNumber: new String("") ISA-13 = "000000000" warnings: []
1384
+ interchangeControlNumber: new String("ABC") ISA-13 = "000000ABC" warnings: []
1385
+ interchangeControlNumber: new String(" ") ISA-13 = "00000000 " warnings: []
1386
+ ```
1387
+
1388
+ The first two are the **same fabricated `000000000`** the empty guard closed, reached through a
1389
+ different input type. The other two are silent **coercions** of a boxed string, a value this
1390
+ library refuses by name wherever it escapes one. `requireControlNumber` refuses a non-string ahead
1391
+ of the empty test now, reporting the TYPE through `caller-string.ts`'s describer and never echoing
1392
+ the value.
1393
+ - **🛑 No error code is minted and no warning code moves, but DIAGNOSTICS DO MOVE, so "nothing
1394
+ else changed" would be false.** `undefined` and `null` threw a bare `TypeError` with no `code`
1395
+ and now throw the builder's typed refusal, so **a consumer catching `TypeError` there no longer
1396
+ catches**. Shapes that used to make the builder write a malformed fixed-width ISA its own
1397
+ re-parse rejected as `X12_INVALID_DELIMITERS` refuse before anything is written now, so **a
1398
+ predicate on that code stops firing for them**. And **the MESSAGE moved at the control-number
1399
+ slots that already refused a non-string**, because they refuse from this guard one step earlier:
1400
+ same class, same code, wording that names the slot and the spec property. Read that as the
1401
+ property and not as a claim about what the old message said. **If you match on message text at a
1402
+ control-number slot, re-read it.**
1403
+ - **It narrows what a control number may BE, never what it may CONTAIN.** A whitespace-only
1404
+ control number is still accepted, unchanged and by design: trimming would be a
1405
+ normalisation rule and no source consulted for this package states one. The asymmetry that
1406
+ creates is stated rather than smoothed over - `new String(" ")` is refused because it is not a
1407
+ string, and the primitive `" "` is not. A SHORT control number still zero-pads. The ISA's other
1408
+ fixed-width slots go through `pad` rather than `padControl`, are guarded by no control-number
1409
+ test, and a numeric one still throws a bare `TypeError`; disclosed rather than closed.
1410
+
1411
+ - **🩺 A BODY segment in an interchange whose ELEMENT SEPARATOR is `?` now frames its elements,
1412
+ where it used to come back as ONE element with an id of `(non-spec)`**
1413
+ (`X12-BODY-DEGENERATE-RELEASE-SEPARATOR`). Reproduced on the base tree at `72bafc2`.
1414
+ `detectDelimiters` reads the element separator positionally out of ISA byte 4 and rejects only
1415
+ control characters, whitespace and a non-distinct set, so a sender may declare `?` there, and
1416
+ `buildInterchange` accepts `elementSeparator: "?"` from a caller. `src/parser/envelope.ts` guarded
1417
+ that degenerate set in both of its own splitters, once for the segment terminator and once for an
1418
+ envelope segment's elements. `decodeSegment` - which every BODY segment plus the `ST`, the `SE`
1419
+ and every retained orphan goes through - did not. It used the release-aware splitter, where a `?`
1420
+ consumes the byte after it, so on such an interchange no split ever happened:
1421
+
1422
+ ```text
1423
+ ST?837?0001?005010X222A1 id "(non-spec)", 1 element
1424
+ NM1?85?2?ACME CLINIC?????XX?1234567893 id "(non-spec)", 1 element
1425
+ SE?3?0001 id "(non-spec)", 1 element
1426
+ warnings: []
1427
+ ```
1428
+
1429
+ **🩺 The envelope framed correctly the whole time, which is what made it silent.** One group, one
1430
+ transaction, `GE-01`, `IEA-01`, `GS-06`/`GE-02` and `ST-02`/`SE-02` all reconciling, an empty
1431
+ warning array - and a transaction body no reader could see, because every reader in this package
1432
+ dispatches on `seg.id`. A consumer got an empty claim list out of a well-formed document.
1433
+ `buildInterchange` disagreed with itself the same way: it returns `parseX12` of the bytes it just
1434
+ wrote, so a caller passing `elementSeparator: "?"` got back a model holding none of the segments
1435
+ it had supplied. **It now reports the segments it wrote, for a value with no `?` in it - read that
1436
+ qualifier, it is load-bearing and the paragraph on the emit side below says why.**
1437
+
1438
+ **🛑 It changes how an already-published document decodes, deliberately**, exactly as the envelope
1439
+ splitter did in the release before it, and on the same tiebreak: CONSISTENCY with the guard this
1440
+ package already carried twice, not a spec clause. 005010 does not transmit a release character at
1441
+ all, so nothing in it says what a `?` means once a sender has declared `?` as structure. **What is
1442
+ NOT the same: this class is not symmetric.** A one-element segment with an id of `(non-spec)` is
1443
+ not a second reading of `NM1?85?2?ACME CLINIC`, so unlike that slice there is no direction in
1444
+ which the old framing was the right one.
1445
+
1446
+ **No warning code is added and no case moves onto a new code.** One is SUBTRACTED, in one place:
1447
+ `X12_DANGLING_RELEASE_CHAR` fired on any degenerate segment ending in an empty last element,
1448
+ because the check keys on a trailing `?`. With `?` as the separator that trailing byte is an empty
1449
+ element and not an unpaired escape, so `PER?IC?NAME?TE?5551234?` is silent now.
1450
+
1451
+ **🛑 The guard is per ROLE, and it is a READ-side guard.** A `?` REPETITION or COMPONENT separator
1452
+ still does not split. `escapeRelease` writes `??` for a literal `?` whatever role `?` was declared
1453
+ in, so documents this library emitted through `0.0.15` carry `CLM*PATIENT??ACCT*150.00` and
1454
+ `getSegmentValue(clm, "01")` reads `"PATIENT?ACCT"` back out of them. A literal split of those two
1455
+ roles would re-frame that as two empty components, so it would stop reading a value this library
1456
+ itself wrote. **That reason survives the emit-side entry above: those documents exist.**
1457
+
1458
+ **🩺 What else it does not close, pinned rather than left to be rediscovered.** On a degenerate set
1459
+ a `?~` still swallows the segment terminator: `findUnescapedTerminator` guards its own role only,
1460
+ so a segment ending in an empty last element puts a `?` immediately before the terminator and
1461
+ merges with its successor (`PER?IC?NAME?TE?5551234?EX?~SE?3?0001~` frames as one segment and raises
1462
+ `X12_MISSING_SE`). Framing is untouched - but the READ of that merged blob did move, so do not take
1463
+ it as "nothing moved": it now frames, so `~SE` and the SE's control number land in `PER`'s
1464
+ communication-number slots where at base they sat inside one `(non-spec)` element. Values are still
1465
+ RAW, `elements.join(sep)` still reproduces the segment byte for byte, and the ISA stays positional.
1466
+
1467
+ - **🩺 `buildInterchange` now release-escapes GS-04, GS-05 and GS-07, so the interchange it hands
1468
+ back reports the group date, group time and responsible agency code you passed**
1469
+ (`X12-INTERCHANGE-GS-EMIT-NOT-RELEASE-AWARE`). Reproduced on the base tree at `837d4bc`. The
1470
+ builder mapped its escaper over GS-01, GS-02, GS-03, GS-06 and GS-08 and wrote `groupDate`,
1471
+ `groupTime` and `responsibleAgencyCode` raw, and it returns `parseX12` of the bytes it just wrote,
1472
+ so a value carrying an active delimiter in one of those three took a slot of its own and shifted
1473
+ every element after it down one - inside a single call. Measured on one group with
1474
+ `versionRelease: "005010X222A2"` and `groupControlNumber: "1"`:
1475
+
1476
+ | spec field | read GS-06 | read GS-08 | warnings |
1477
+ | ------------------------------ | ---------- | ---------- | ------------------------------------------------------- |
1478
+ | `groupDate: "2026*0601"` | `"1200"` | `"X"` | `X12_CONTROL_NUMBER_MISMATCH` |
1479
+ | `groupTime: "12*00"` | `"00"` | `"X"` | `X12_CONTROL_NUMBER_MISMATCH` |
1480
+ | `responsibleAgencyCode: "X*Y"` | `"1"` | `"Y"` | none |
1481
+ | `groupTime: "12~00"` | absent | absent | `X12_UNEXPECTED_SEGMENT`, `X12_CONTROL_NUMBER_MISMATCH` |
1482
+ | `groupDate: "20260601?"` | `"X"` | absent | `X12_CONTROL_NUMBER_MISMATCH` |
1483
+
1484
+ **🩺 The `responsibleAgencyCode` row is the one to know about, because nothing was raised on any
1485
+ channel.** GS-06 kept its own slot, so it still reconciled against GE-02; what moved was GS-08, the
1486
+ version / release / industry identifier code, which is the element the entry above lets a caller
1487
+ state. All five rows now read the values the caller passed, with an empty warning array.
1488
+
1489
+ **The grounding is inside this package rather than in a spec clause**, the same tiebreak the two
1490
+ entries below record: one function disagreed with itself. It returns `parseX12` of bytes it wrote
1491
+ and then answers a slot out of its neighbour's, and `SegmentSpec`'s documented contract already
1492
+ said the builder applies the release escape so an active delimiter inside a value survives.
1493
+
1494
+ **🛑 It changes bytes this library already put on the wire, and that is the cost.** A value
1495
+ containing none of the four delimiters and no `?` is emitted byte-for-byte as before, which is
1496
+ every conformant GS-04 / GS-05 / GS-07.
1497
+
1498
+ **🛑 No warning code is added and no case moves onto a new code. Read the property rather than a
1499
+ direction list:** the interchange the call returns now reports the values the caller passed, where
1500
+ before it reported whatever the shift left in each slot. **What is narrower here than in the two
1501
+ entries below, and is the part to carry away: no reader moved.** No executable line under
1502
+ `src/parser/` changed, so an inbound document from a trading partner decodes exactly as it did at
1503
+ `0.0.15`; what changed is what this library emits, and therefore how its own output reads back.
1504
+ **Say it that way rather than "the parser is untouched"** - this slice's own graded review forced a
1505
+ JSDoc correction in `src/parser/envelope.ts`, where a stale census of the released GS/ST slots had
1506
+ been published.
1507
+
1508
+ **State the delimiter set by ROLE, never by byte.** `InterchangeSpec` lets you declare all four, so
1509
+ which BYTES shift is a property of the set you declared: with `elementSeparator: "|"` a GS-07 of
1510
+ `"X|Y"` is what took GS-08's slot and `"X*Y"` was inert. **Only the ELEMENT SEPARATOR and the
1511
+ SEGMENT TERMINATOR ever shifted the segment's own framing, plus a `?` immediately before the
1512
+ element separator.** The **repetition** and **component** separators moved the dot-path reader
1513
+ instead, and releasing them is a **gain** there: the composite read `"07-1"`
1514
+ answered `"X"` for `"X:Y"`. **The measured cost is a mid-string `?`, and only on the surfaces
1515
+ documented as raw** - `gs.elements[4]` reads `"2026??0601"` where it read `"2026?0601"`, while the
1516
+ dot-path read of that value unescapes and is unchanged. No total is published: that is what was
1517
+ measured, not a closed account. **A caller who was pre-releasing these values themselves is now
1518
+ escaping twice** and should drop the hand-rolled escape.
1519
+
1520
+ **A wrong-typed GS element still names its slot.** The type check runs over the unescaped parts, so
1521
+ a numeric `groupDate` refuses with `buildInterchange: "GS"-04 must be a string` rather than
1522
+ degrading to the builder-named message the escaper alone would produce; the five slots that already
1523
+ escaped gained the slot name with it. Same class, same `X12_BUILD_INVALID_SPEC`, and still no echo
1524
+ of the value. **`null` and `undefined` in these three fields are ABSENT, not refused** - each
1525
+ resolves through a default before either guard sees it.
1526
+
1527
+ **A LITERAL segment id this library writes is never escaped.** `esc` releases against the
1528
+ delimiter set the CALLER declared, and a `componentSeparator` of `"S"` is admissible, so mapping
1529
+ the escaper over element 0 would turn the literal `"GS"` into `G?S` and the group header would stop
1530
+ being a `GS`. `GE` / `ST` / `SE` / `IEA` already followed that rule. **Read "literal" strictly:** a
1531
+ `SegmentSpec` body segment carries a CALLER-supplied id, `buildTransaction` has released it since
1532
+ before this slice, and `SegmentSpec`'s own JSDoc still says it is emitted verbatim. That
1533
+ disagreement predates this slice, is unchanged by it, and is filed rather than closed here.
1534
+
1535
+ **What this does not close:** `buildInterchange`'s IEA-02 is padded rather than escaped and has to
1536
+ stay byte-equal to the fixed-width ISA-13 it reconciles against, so that is a decision of its own;
1537
+ the ISA fixed-width slots remain outside both guards; and an unescaped active delimiter is still
1538
+ not safe anywhere, because that is what a delimiter is.
1539
+
1540
+ - **🩺 `buildTA1` now release-escapes its five caller-supplied elements, so an Accept acknowledgment
1541
+ this library emits no longer reads back as a Reject** (`X12-TA1-EMIT-NOT-RELEASE-AWARE`).
1542
+ Reproduced on the base tree at `e8f34b9`. `buildTA1` joined the five values with the element
1543
+ separator and escaped none of them, so a value carrying an active delimiter took a slot of its own
1544
+ and shifted every element after it down one. TA1-04 is the disposition, TA1-05 the note, and
1545
+ `parseTA1` narrows an out-of-enum TA1-04 to `R`. Measured with `parseX12` + `parseTA1` over
1546
+ `ISA … <what buildTA1 returned> … IEA`, `ackCode` `"A"` and `noteCode` `"000"` throughout:
1547
+
1548
+ | `interchangeControlNumber` | emitted at `0.0.14` | read `ackCode` | read TA1-01 | warnings |
1549
+ | -------------------------- | --------------------------------- | -------------- | -------------------- | ------------------------ |
1550
+ | `"000000001"` | `TA1*000000001*260601*1200*A*000` | `"A"` | `"000000001"` | none |
1551
+ | `"00000001?"` | `TA1*00000001?*260601*1200*A*000` | `"R"` | `"00000001?*260601"` | none |
1552
+ | `"0000*0001"` | `TA1*0000*0001*260601*1200*A*000` | `"R"` | `"0000"` | none |
1553
+ | `"0000~0001"` | `TA1*0000~0001*260601*1200*A*000` | `"R"` | `"0000"` | `X12_UNEXPECTED_SEGMENT` |
1554
+
1555
+ **An Accept acknowledgment this library emitted read back as a Reject**, on the element that
1556
+ reassociates it, with nothing raised on any channel. The `*` and `~` rows did that on every
1557
+ released version; the `?` row is the one the entry below opened. **🩺 The inverse exists and is the
1558
+ less safe direction:** the read narrows an out-of-enum TA1-04 to `R`, so a well-typed shift always
1559
+ lands on Reject, but `noteCode` is checked by the type system and by nothing at run time, so a
1560
+ `noteCode` of literally `"A"` shifted onto TA1-04 and made a **Reject read back as an Accept**, and
1561
+ a sender who reads an Accept does not resubmit. All four now read back the disposition emitted.
1562
+
1563
+ **The grounding is inside this package rather than in a spec clause**, the same tiebreak the entry
1564
+ below records: `buildTA1` emitted bytes that this package's own reader decoded into a different
1565
+ disposition than the caller asked for, while every other builder already released the same class of
1566
+ element through the same helper.
1567
+
1568
+ **🛑 It changes bytes this library already put on the wire, and that is the cost.** A value
1569
+ containing none of the four delimiters and no `?` is emitted byte-for-byte as before, which is
1570
+ every conformant TA1: TA1-01 echoes ISA-13, TA1-02 / TA1-03 echo ISA-09 / ISA-10, and TA1-04 /
1571
+ TA1-05 are code list values.
1572
+
1573
+ **🛑 No warning code is added and no case moves onto a new code, but the consumer predicate MOVES
1574
+ IN BOTH DIRECTIONS.** `parseTA1` of a `buildTA1` output now reports the disposition and note the
1575
+ caller passed; before, it reported whatever element the shift left in TA1-04, which could be the
1576
+ caller's, a coincidental in-enum value, or an out-of-enum one narrowed to `"R"`. So
1577
+ `ackCode === "R"` **stops** firing where an Accept had been shifted onto it, and **starts** firing
1578
+ where a Reject had been shifted off it: `interchangeTime: "12*A"` with `ackCode: "R"` read `"A"`
1579
+ before and reads `"R"` now, with every field a valid member of its union. `ackCode === "A"` moves
1580
+ the same two ways. What is one-directional is the safety, which is a different statement: nothing
1581
+ now reports a disposition the caller did not ask for.
1582
+
1583
+ **What releasing the rest of the set costs, and where it does not cost.** Only `*`, `~` and a `?`
1584
+ immediately before the separator ever shifted the segment's own element framing. **`^` and `:`
1585
+ moved the dot-path reader instead, and releasing them is a gain there:**
1586
+ `getSegmentValue(ta1, "01")` answered `"0000"` at `0.0.14` for a control number of `"0000^0001"`,
1587
+ silently truncating the reassociation key to the first repetition, and answers `"0000^0001"` now;
1588
+ the composite read `"01-1"` answered `"0000"` for `"0000:0001"` and answers the whole value now.
1589
+ **The measured pure cost is a mid-string `?`, and only on the surfaces documented as raw**: `raw`
1590
+ and `elements` read `"0000??0001"` where they read `"0000?0001"`, while every
1591
+ dot-path read unescapes and answered `"0000?0001"` on both. No total is published: that is what was
1592
+ measured, not a closed account. `getSegmentValue` takes an `X12Segment` and `Ta1Segment` carries no
1593
+ `id`, so add one to read a TA1 through it. If you were escaping the value yourself, as `KNOWN-LIMITATIONS.md`
1594
+ advised while this was open, drop that - you are now escaping twice on both kinds of surface
1595
+ (`"00000001??"` in, `TA1*00000001????*…` out, `getSegmentValue` answering `"00000001??"`).
1596
+
1597
+ **The read half did not move in THIS entry**, so a control number of `"00000001?"` read back as
1598
+ `"00000001??"` rather than as `"00000001?*260601"`. The `X12-TA1-RESIDUALS` entry above is the one
1599
+ that moved it, and the clause that stood here telling you to apply `unescapeRelease` yourself is
1600
+ **deleted** rather than reworded: the library does it now.
1601
+
1602
+ **`BuildTA1Options` gained `repetitionSeparator`, `componentSeparator` and `segmentTerminator`**
1603
+ beside the existing `elementSeparator`, the same four `build999` already takes. They exist for
1604
+ escaping and nothing else - `buildTA1` still emits no segment terminator, no repetition and no
1605
+ composite. Escaping against a guessed delimiter set is a value corruption rather than a safe
1606
+ default: `unescapeRelease` preserves `?X` verbatim for any `X` outside the reader's declared set,
1607
+ so a value released against the wrong delimiter comes back carrying a stray `?`. The defaults are
1608
+ unchanged and are the cosyte archetype, which this function cannot verify, so state the separators
1609
+ if you embed a TA1 in an envelope that declares different ones.
1610
+
1611
+ **A non-string element now refuses** with `AckBuildError` and the existing `X12_ACK_INVALID_SPEC`
1612
+ code, and that is a prerequisite of the escape rather than a separate guard: releasing a value
1613
+ routes it through the escape helper, and the bare `escapeRelease` underneath it returns its empty
1614
+ accumulator for a `number`, so escaping without the type check would have replaced a shifted TA1-01
1615
+ with a vanished one. A numeric `interchangeControlNumber` emitted `TA1*12345*260601*1200*A*000`
1616
+ before, with the number surviving onto `elements` inside a value typed `readonly string[]`, and an
1617
+ absent one emitted `TA1**250101*1200*A*000`. The accept-with-note refusal still runs first and
1618
+ still reports `X12_TA1_ACCEPT_WITH_NOTE`, so no existing refusal moves code.
1619
+
1620
+ **What this does not close:** `buildTA1` still uses no segment joiner, so its refusal names the
1621
+ builder and never `TA1-01`; `buildInterchange` still does not escape GS-04, GS-05 or GS-07; and an
1622
+ unescaped active delimiter is still not safe anywhere. Measured in `KNOWN-LIMITATIONS.md`.
1623
+
1624
+ - **🩺 A `?` immediately before the element separator inside an envelope segment now frames as ONE
1625
+ element, which fixes a released delimiter and is a SYMMETRIC change**
1626
+ (`X12-ENVELOPE-SPLITTER-NOT-RELEASE-AWARE`). Reproduced on the base
1627
+ tree at `1b71733`. `src/parser/envelope.ts`'s `splitElements` - the splitter for `GS`, `GE`, `ST`,
1628
+ `SE`, `IEA` and `TA1` - was a plain `String.prototype.split` on the element separator, so a `?*`
1629
+ ended the element anyway and every element after it moved down a slot:
1630
+
1631
+ ```text
1632
+ GS*HC*S*R*20260601*1200*1*X*005010?*X222A1~ ten elements, GS-08 read "005010?"
1633
+ applicationSenderCode "SEND*ER" GS-08 read "X", the GS-07 agency code
1634
+ groupControlNumber "1*2" GS-08 read "X"; warnings: []
1635
+ transactionSetControlNumber "00*01" ST-03 read "01"; warnings: []
1636
+ CLM*PT?*ACCT*150.00~ three elements - the BODY control, always correct
1637
+ ```
1638
+
1639
+ **What made it a defect rather than a tolerance is that this package's own two halves disagreed
1640
+ inside a single call:** `buildInterchange` maps its release escaper over GS-02 / GS-03 / GS-06 /
1641
+ GS-08 and ST-01 / ST-02 / ST-03, then returns `parseX12` of the bytes it just wrote, so
1642
+ `applicationSenderCode: "SEND*ER"` emitted a correct `GS*HC*SEND?*ER*...` and handed back a model
1643
+ whose GS-08 answered `"X"`, with `warnings: []`.
1644
+
1645
+ **🛑 READ THE CHANGE AS SYMMETRIC. IT IS NOT ONLY A CORRECTION.** A `?` before the separator has
1646
+ two readings and 005010 does not transmit which the sender meant. Where the sender **escaped** a
1647
+ delimiter, `0.0.14` framed it wrongly and this release frames it correctly: a correction, the rows
1648
+ above. Where the sender sent a **literal `?`** as the element's last byte, `0.0.14` framed it
1649
+ correctly and this release merges the element with its successor, so **the segment loses its LAST
1650
+ element**: a regression. `GS*HC*SUB1*RCV?*20260601*1200*000000123*X*005010X222A1~` read nine
1651
+ entries and reads **eight** here, GS-06 answering `"X"` and GS-08 gone. Every other release
1652
+ sequence in an envelope element (`??`, `?:`, `?^`, `?~`, `?A`) framed identically before and after
1653
+ and still does, pinned as invariance controls.
1654
+
1655
+ **No warning code is added, and `X12_CONTROL_NUMBER_MISMATCH` moves in BOTH DIRECTIONS.** Where the
1656
+ old shift displaced a control number it stops firing, so **a consumer that rejects on that code
1657
+ will now accept such a document**; where a literal `?` newly displaces one it starts firing, so
1658
+ that same consumer **will now reject a document `0.0.14` accepted**. Both are pinned, alongside a
1659
+ genuine mismatch that raises it on either tree.
1660
+
1661
+ **🩺 The regression direction reaches an 837's variant and its money, by one route.** `ST-03`
1662
+ decides the 837 variant, so an `ST-02` ending in a literal `?` destroys `ST-03` and the document
1663
+ falls back to the `SVx` scan. An 837 declaring `005010X222A1` whose only service segment is an
1664
+ `SV2` read `variant` `"P"`, `charge` `undefined` and `X12_837_SERVICE_LINE_NOT_DECODED` at
1665
+ `0.0.14`, and reads `variant` `"I"` with `charge` `150.00` and that warning silent here. **A warned
1666
+ non-decode becomes a decoded amount.** Re-check any routing driven off `submission.variant` if your
1667
+ partners send envelope elements that can end in a literal `?`.
1668
+
1669
+ **Why it was taken anyway, and it is CONSISTENCY rather than a spec clause.** The two readings are
1670
+ mutually exclusive and nothing in 005010 picks between them. `decodeSegment` has read BODY elements
1671
+ the escape-wins way on every released version (`REF*EA*RCV?*NEXT` has always been two elements), so
1672
+ the envelope now obeys the one rule the rest of the package already obeyed.
1673
+
1674
+ **🩺 The exposure is NOT inbound bytes only.** Envelope slots routed through the builders' release
1675
+ escaper are safe. Not every emit slot is routed through it, and no total is published: what follows
1676
+ is the routes measured to reach the regression direction, not a closed account of what bypasses the
1677
+ escaper. **`buildInterchange` does not escape GS-04, GS-05 or GS-07**: a `groupDate` of
1678
+ `"2026060?"` read nine GS elements at `0.0.14` and reads eight here, GS-08 gone. If you emit one
1679
+ of those, escape or reject a `?` yourself.
1680
+
1681
+ **Values are still RAW, pre-`?`-unescape**, exactly as `X12Segment.elements` has always documented:
1682
+ `gs.elements[2]` reads `"SEND?*ER"`, not `"SEND*ER"`. `elements.join(separator)` therefore still
1683
+ reproduces the segment byte for byte, which is what `serializeX12` relies on when it substitutes a
1684
+ recomputed `SE-01` / `GE-01` / `IEA-01` into a control segment.
1685
+
1686
+ **🩺 The ISA is deliberately exempt.** ASC X12 .5 makes it fixed-width, which is what lets the
1687
+ delimiter set be recovered from it before anything is parsed, so a `?` in an ISA element is content
1688
+ and never an escape; release-splitting it would collapse a well-formed ISA below its 17 entries.
1689
+ A degenerate delimiter set whose element separator IS `?` also falls back to the literal split,
1690
+ the same guard the segment-terminator scanner already carried. Both are pinned, each with a red
1691
+ negative control.
1692
+
1693
+ **🛑 An UNESCAPED delimiter is still not safe and nothing here claims otherwise** - a bare active
1694
+ delimiter ends its element, because that is what a delimiter is. `build837`'s
1695
+ `implementationConventionReference` still refuses a value carrying one, deliberately not relaxed:
1696
+ a partner's parser is not obliged to be release-aware either, and widening an emit surface is its
1697
+ own decision rather than a side effect of a reader fix.
1698
+
904
1699
  - **🩺 An `AMT` / `ADX` row whose amount decodes nothing is no longer dropped in silence**
905
1700
  (`X12-AMT-ADX-ABSENT-AMOUNT`). Reproduced on the base tree at `9db104b` across all four readers,
906
1701
  and the report it now raises is the `X12_AMOUNT_ROW_DROPPED` entry under **Added** above. The