@cosyte/x12 0.0.13 → 0.0.15

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,145 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ### Added
11
11
 
12
+ - **🩺 `Build837EnvelopeSpec.implementationConventionReference` - the caller now states the ST-03 /
13
+ GS-08 implementation convention reference `build837P` / `build837I` / `build837D` declares**
14
+ (`X12-837-EMIT-IDENTIFIER-FIXED`). One value, **both elements**; omit it and the builders emit
15
+ exactly what they always have.
16
+
17
+ **What this fixes, and it is a rejected claim rather than a mis-read one.** The builders stamped
18
+ `005010X222A2` / `005010X223A3` / `005010X224A2` with no way to change them. Two of those are not
19
+ what CMS and several state Medicaid companion guides require in ST-03 and GS-08 on production
20
+ professional and institutional claims, which ask for `005010X222A1` and `005010X223A2`, so **a
21
+ partner on one of those guides rejected every 837 this library built and a caller had no remedy.**
22
+ The read side of the same fact was grounded in the release before this one; this is the emit side.
23
+
24
+ **🛑 The defaults are UNCHANGED, on purpose, and that is the whole shape of the fix.** Which
25
+ published guide identifier a trading partner accepts is a **partner fact, not a spec fact**.
26
+ Re-stamping the default would silently change bytes this library already puts on the wire and
27
+ break the partners it works with today, so the default stays and the caller states what its own
28
+ partner asked for. Nothing about an existing call changes.
29
+
30
+ **What it refuses of its own, all `Claim837BuildError` with code `X12_837_BUILD_INVALID_SPEC`, and
31
+ none of them echoes the value you passed.** These sit on top of the element-type guard every string
32
+ slot in every builder already has, which refuses a non-string with the same code, so read the list
33
+ as what this field adds and not as a closed account of everything that can refuse. No total is
34
+ published.
35
+ - **Empty.** A trailing empty element is not emitted at all, so an empty reference would remove
36
+ ST-03 and GS-08 rather than send them empty - a silent structural loss on two required elements.
37
+ - **Carrying an active delimiter or the release character.** The value is refused rather than
38
+ escaped. A trading partner's parser is not obliged to honour the release escape, and a published
39
+ guide identifier has no legitimate use for a delimiter, so the refusal is kept even now that
40
+ this library's own reader would carry it (see the envelope-splitter entry below).
41
+ - **A reference this library's own reader resolves to a DIFFERENT 837 variant** (`005010X223A2`
42
+ handed to `build837P`). The emitted file would declare one variant and carry another's service
43
+ segments, and `get837Claims` would decode none of its service lines. Same class as the existing
44
+ refusal for a service line whose `variant` disagrees with the builder's. The message names the
45
+ variant the builder emits and deliberately not the one the reference belongs to.
46
+
47
+ **A reference outside the read table is emitted as given, and that is deliberate.** Nothing makes
48
+ the published-errata set provably exhaustive, so refusing an unrecognised identifier would claim
49
+ an exhaustiveness this library does not claim when reading either. The honest cost is pinned as a
50
+ test: on such a file this library's own reader falls back to the `SVx` scan, exactly as it does
51
+ for any unrecognised ST-03. The **length** is not bounded either, and the two elements' maxima
52
+ differ: GS-08 is data element 480 (`AN 1/12`), ST-03 is element 1705 (`AN 1/35`).
53
+
54
+ **🩺 A guard on this element cannot make the element trustworthy, and the disclosure above is
55
+ therefore about the whole envelope segment rather than these two elements.** An **unescaped**
56
+ active delimiter in a _different_ `GS` or `ST` element still ends its own element and shifts every
57
+ element after it, so ST-03 and GS-08 are then read out of a neighbour's slot, and no refusal here
58
+ can reach it. A **release-escaped** one no longer does - see the entry below, which landed after
59
+ this one and retracts the part of it that said escaping could not help.
60
+
61
+ - **🩺 `X12_837_SERVICE_SEGMENT_REPEATED`, the 33rd Tier-2 warning code, plus the public factory
62
+ `serviceSegmentRepeated(position)`** (`X12-837-SV1-OVERWRITE`). A **second** `SV1` / `SV2` / `SV3`
63
+ arriving inside an **already-open** Loop 2400 now says so, at that repeated segment. Through
64
+ `0.0.13` it said nothing at all.
65
+
66
+ **🩺 What that cost, and it is money and a procedure code.** A service line carries **one** service
67
+ segment's worth of slots, and every decoder writes **all** of the slots its kind writes. Under an
68
+ `ST-03` of `005010X222A2`, `SV1*HC:99213*8500*UN*4***1~` followed by `SV1*HC:99999*12*UN*1***1~`
69
+ inside one `LX` left ONE line reading `charge` **`12`** and `procedureCode` **`99999`**, with
70
+ `warnings: []`. `8500` became `12`, CPT `99213` became `99999`, and nothing was raised on any
71
+ channel. **The worst corner is a repeat whose own charge element is ABSENT:** it writes `undefined`
72
+ over the amount the first one stated, and `X12_837_SERVICE_LINE_NOT_DECODED` does **not** fire
73
+ there, because a service segment did decode.
74
+
75
+ **🛑 THIS CLOSES ONLY THE SILENCE, AND THE RESTRAINT IS THE POINT.** The decode is **not** narrowed:
76
+ last-wins is unchanged, element for element, so which values a document decodes to are byte-for-byte
77
+ what they were at `0.0.13`. This reader cannot tell a stray service segment from a conformant one,
78
+ so choosing the first would be inventing; and changing which occurrence wins changes how
79
+ **already-published documents decode**. That is the same call made for the `SVx` variant fallback.
80
+
81
+ **It fires on a repeat of ANY kind, decoded or not.** One whose kind does not match the resolved
82
+ variant is read into nothing and overwrites nothing - what it carries reaches no part of the typed
83
+ model - and it is reported the same way, before or after the matching one.
84
+
85
+ **Anchored at the repeated service segment**, with **no `elementIndex`**: what is reported is a
86
+ second occurrence of the segment, not a defect in an element of it. **Once per repeat**, and the
87
+ count is scoped to the LINE and never latched, so a first service segment under a later `LX` is a
88
+ first. It can never name the same segment as `X12_837_SERVICE_SEGMENT_WITHOUT_LX`, which requires
89
+ that no Loop 2400 be open.
90
+
91
+ **It is additive and nothing moved onto it**, pinned by committed tests asserting the whole warning
92
+ channel with the new code filtered out. **No consumer predicate written against any existing code
93
+ changes meaning.** Read that as invariance, not as a list of what else you will see.
94
+
95
+ **🛑 The package's own documentation was a consumer, and it was blind.** The cookbook's "gate before
96
+ you post a line amount" recipe named four codes and **none** of them fires on the overwrite
97
+ document, so a consumer following it posted `12` for a line the sender also sent as `8500`. The
98
+ cookbook, the troubleshooting table and `KNOWN-LIMITATIONS.md` now name this code beside the other
99
+ four, and a committed test pins that the four-code gate misses what the five-code gate catches.
100
+ **The recipe was not the only page:** `spec-notes-money` named `X12_837_SERVICE_LINE_NOT_DECODED`
101
+ as "the known instance" of an 837 charge reading `undefined` from a slot no reader read, and the
102
+ repeat corner is a second instance by a different route on which that code does not fire. It now
103
+ says so.
104
+
105
+ - **🩺 `X12_837_AMBIGUOUS_VARIANT`, the 32nd Tier-2 warning code, plus the public factory
106
+ `ambiguous837Variant(position)`** (`X12-837-RESIDUALS`). An 837 whose variant was decided by the
107
+ `SVx` fallback, in a transaction body that carries service segments for **more than one variant**,
108
+ now says so at the `ST`. Through `0.0.13` it said nothing at all about that resolution.
109
+
110
+ **What that cost.** Variant resolution runs before the walk as
111
+ `explicitType ?? variantFromIcr ?? variantFromSegment`. Absent a caller `type` option, and where
112
+ `ST-03` names no implementation convention this reader recognises, the reader falls back to the
113
+ **first** `SV1` / `SV2` / `SV3` in the body, orphans included. One stray `SV2` ahead of a conformant
114
+ Professional claim therefore re-types the whole submission Institutional: `submission.variant` reads
115
+ `"I"`, and a consumer routing on that field sends a Professional claim down an Institutional path.
116
+ The line-level consequences were reported; **the submission-level typing that produced them was on
117
+ no channel**, so `submission.variant` carried a confident value with nothing to contradict it.
118
+
119
+ **🛑 THIS CLOSES ONLY THE SILENCE, AND THE RESTRAINT IS THE POINT.** The fallback is **not**
120
+ narrowed and first-wins is unchanged, so on every document that reaches the fallback the variant
121
+ resolved and the lines decoded are byte-for-byte what they were at `0.0.13`, and this code is added
122
+ beside whatever the walk already raised. (`X12-VARIANT-ICR-UNGROUNDED`, in this same release,
123
+ changed WHICH documents reach the fallback; read its entry for what decodes differently.) Excluding orphans from
124
+ the fallback would change how already-published documents decode and is its own slice.
125
+
126
+ **🩺 Which service segment is the stray one is NOT decided.** This reader cannot tell a stray
127
+ service segment from a conformant one, and the fallback takes the first whether or not a Loop 2400
128
+ was open at it. Reporting the conflict is honest; picking a winner would be inventing. Re-read with
129
+ `get837Claims(delimiters, tx, { type })` to decode against a variant you trust.
130
+
131
+ **It reports the RESOLUTION, never the document.** A caller `type`, or an `ST-03` naming a known
132
+ convention, means no guess was made and this code is **not** raised however mixed the body is.
133
+
134
+ **Anchored at the `ST`** (`tx.segments[0]`, which carries `ST-03`), with **no `elementIndex`**: the
135
+ conflict is a property of the body rather than of an element, and one route into it is an `ST-03`
136
+ that is absent altogether. Raised **once per transaction**, because there is one resolution per
137
+ transaction, and it can never travel with `X12_837_UNKNOWN_VARIANT`, which is the other outcome of
138
+ that same resolution.
139
+
140
+ **🩺 It is additive, and nothing moved onto it.** `X12_837_SERVICE_LINE_NOT_DECODED`,
141
+ `X12_837_SERVICE_SEGMENT_WITHOUT_LX` and `X12_837_SERVICE_LINE_DROPPED` fire on exactly the
142
+ documents they fired on before **this code was added**, in the same positions, pinned by committed
143
+ tests that assert the whole warning channel with the new code filtered out. **No consumer predicate
144
+ changes meaning because of this code.** (`X12-VARIANT-ICR-UNGROUNDED`, in this same release, DID
145
+ change which documents reach the `SVx` fallback; read its entry above.) Read that as **invariance and not as a list of what else you will
146
+ see** on a contested document: it does not promise that any particular loss on one is reported at
147
+ all, and one that is not was not reported before this code existed either. A stray `LX` that opened
148
+ no line, for one, already suppressed `X12_837_SERVICE_SEGMENT_WITHOUT_LX` for the service segments
149
+ inside it, and still does.
150
+
12
151
  - **🩺 `X12_STATED_AMOUNT_DISCARDED`, the 31st Tier-2 warning code, plus the public factory
13
152
  `statedAmountDiscarded(position)`** (`X12-STATED-AMOUNT-DISCARDED`). The entry below reports a row
14
153
  whose amount this library could not read. This one is the opposite case: **the reader discarded
@@ -270,6 +409,74 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
270
409
 
271
410
  ### Changed
272
411
 
412
+ - **🩺 `get837Claims` now recognises every published `ST-03` implementation-convention reference for
413
+ the three 837 guides, and some already-published files therefore decode differently**
414
+ (`X12-VARIANT-ICR-UNGROUNDED`). This is a grounding unit: the table `VARIANT_BY_ICR` had three keys
415
+ and they were grounded against nothing.
416
+
417
+ **🩺 What that cost, measured at `668afea`, which is `main` at published `0.0.13`.** The table held exactly
418
+ `005010X222A2`, `005010X223A3` and `005010X224A2`. **It contained none of the identifiers HIPAA
419
+ adopts at 45 CFR 162.1102** (`005010X222`; `005010X223` with its `005010X223A1` Type 1 errata;
420
+ `005010X224` with `005010X224A1`), **and it was missing `005010X222A1` and `005010X223A2`**, which
421
+ CMS and state Medicaid companion guides require in ST-03 and GS-08 on production professional and
422
+ institutional claims. So a conformant, HIPAA-mandated 837P declaring `005010X222A1` resolved to no
423
+ variant at all, fell through to the `SVx` scan, and one stray `SV2` anywhere in the body re-typed
424
+ the whole submission Institutional. **The `SVx` fallback was the NORMAL path on production
425
+ professional and institutional traffic rather than the exception**, and `X12_837_UNKNOWN_VARIANT`
426
+ on such a file was a fabricated non-conformance claim about a document that was not
427
+ non-conformant. The sources for every key are named beside the table and in
428
+ `documentation/agent-notes/x12-variant-icr-ungrounded.md`; the three later published errata guides
429
+ are the weakest leg and say so in place.
430
+
431
+ **🛑 This IS a behaviour change on already-published decoding, and it is disclosed rather than
432
+ buried.** On a file whose `ST-03` is now recognised: `submission.variant` can differ from what
433
+ `0.0.13` read, wherever the first `SVx` in the body disagreed with the declaration; and
434
+ **`X12_837_AMBIGUOUS_VARIANT` and `X12_837_UNKNOWN_VARIANT` no longer fire on it at all**, because
435
+ no guess was made. **A predicate written against either code goes quiet on such a file.**
436
+
437
+ **🩺 And a service line whose `SVx` kind disagrees with the declaration is no longer DECODED, so a
438
+ code STARTS firing on a document that may have carried `warnings: []`.** Under an `ST-03` of
439
+ `005010X222A1` with a body whose only service segment is an `SV2`, `0.0.13` read `variant` `"I"`,
440
+ `charge` `7300`, `units` `2` and `warnings: []`; this release reads `variant` `"P"`, `charge` and
441
+ `units` `undefined` with the rest of the service segment undecoded, and
442
+ `X12_837_SERVICE_LINE_NOT_DECODED` at that line's `LX`. **Read only the decimal slots as
443
+ `undefined`:** an undecoded line SEEDS its identity fields, so `procedureCode` is `""` on a P or D
444
+ line and `revenueCode` is `""` on an I one. A predicate of `procedureCode === undefined` does NOT
445
+ detect this.
446
+ A mis-stamped envelope is an ordinary vendor variant and this reader can no more tell one from a
447
+ conformant document than it can tell a stray `SVx` from a conformant one, so the loss is **warned
448
+ rather than silent**, and the cookbook's post-a-line-amount gate already names that code first.
449
+
450
+ **🛑 Read all of that as ONE property and never as a closed list of consequences:** where `ST-03`
451
+ is now recognised, **the document's own declaration decides the variant instead of its first
452
+ service segment**, and everything downstream follows from that single substitution. A first draft
453
+ of this entry published a census of three and a refuter measured it false by finding a fourth.
454
+
455
+ That is
456
+ the hazard a widening onto a new code carries, taken here in the opposite direction from the two
457
+ slices before it: they refused to narrow a fallback because the reader had no evidence beyond the
458
+ segments, and here the reader had the evidence in ST-03 and was ignoring it. Re-check any routing
459
+ driven off `submission.variant` for 837s you read on `0.0.13` or earlier.
460
+
461
+ **🛑 The `SVx` fallback is NOT narrowed.** First-wins still takes the first `SV1` / `SV2` / `SV3` in
462
+ the body, orphans included, on every document that still reaches it, and precedence is unchanged:
463
+ a caller `type` still wins ahead of ST-03, and ST-03 ahead of the segments. What changed is which
464
+ documents reach the fallback.
465
+
466
+ **It is a LIST of cited identifiers, never a pattern.** A reference outside the set, in a different
467
+ case, or carrying leading whitespace still falls through exactly as before. The set is **not
468
+ claimed exhaustive and no count of it is published**: both variant-resolution messages named the
469
+ three old keys literally, so both were wrong the moment the table was corrected, and neither
470
+ enumerates the set any more. A committed tripwire reds if any registry message quotes a TR3
471
+ identifier again.
472
+
473
+ **🩺 OPEN, and deliberately not fixed here: the EMIT side still stamps the old three.** `build837P`
474
+ / `build837I` / `build837D` write `005010X222A2` / `005010X223A3` / `005010X224A2` and a caller
475
+ cannot override them, so **a partner that requires `005010X222A1` or `005010X223A2` will reject an
476
+ 837 this builder emits.** Which published guide identifier a partner accepts is a partner fact
477
+ rather than a spec fact, and changing bytes this library already emitted would break the partners
478
+ it works with today. `KNOWN-LIMITATIONS.md` carries it as an open residual.
479
+
273
480
  - **🩺 BREAKING (read model): every monetary, percent and quantity slot the readers used to fill with
274
481
  a fabricated `X12Decimal.ZERO` is now `X12Decimal | undefined`** (`X12-837-SV-UNDEFINED-DECIMAL`).
275
482
  Through `0.0.12` an **absent** `SV1-02` - a monetary field on an 837 claim service line - read back
@@ -743,6 +950,166 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
743
950
 
744
951
  ### Fixed
745
952
 
953
+ - **🩺 `buildTA1` now release-escapes its five caller-supplied elements, so an Accept acknowledgment
954
+ this library emits no longer reads back as a Reject** (`X12-TA1-EMIT-NOT-RELEASE-AWARE`).
955
+ Reproduced on the base tree at `e8f34b9`. `buildTA1` joined the five values with the element
956
+ separator and escaped none of them, so a value carrying an active delimiter took a slot of its own
957
+ and shifted every element after it down one. TA1-04 is the disposition, TA1-05 the note, and
958
+ `parseTA1` narrows an out-of-enum TA1-04 to `R`. Measured with `parseX12` + `parseTA1` over
959
+ `ISA … <what buildTA1 returned> … IEA`, `ackCode` `"A"` and `noteCode` `"000"` throughout:
960
+
961
+ | `interchangeControlNumber` | emitted at `0.0.14` | read `ackCode` | read TA1-01 | warnings |
962
+ | -------------------------- | --------------------------------- | -------------- | -------------------- | ------------------------ |
963
+ | `"000000001"` | `TA1*000000001*260601*1200*A*000` | `"A"` | `"000000001"` | none |
964
+ | `"00000001?"` | `TA1*00000001?*260601*1200*A*000` | `"R"` | `"00000001?*260601"` | none |
965
+ | `"0000*0001"` | `TA1*0000*0001*260601*1200*A*000` | `"R"` | `"0000"` | none |
966
+ | `"0000~0001"` | `TA1*0000~0001*260601*1200*A*000` | `"R"` | `"0000"` | `X12_UNEXPECTED_SEGMENT` |
967
+
968
+ **An Accept acknowledgment this library emitted read back as a Reject**, on the element that
969
+ reassociates it, with nothing raised on any channel. The `*` and `~` rows did that on every
970
+ released version; the `?` row is the one the entry below opened. **🩺 The inverse exists and is the
971
+ less safe direction:** the read narrows an out-of-enum TA1-04 to `R`, so a well-typed shift always
972
+ lands on Reject, but `noteCode` is checked by the type system and by nothing at run time, so a
973
+ `noteCode` of literally `"A"` shifted onto TA1-04 and made a **Reject read back as an Accept**, and
974
+ a sender who reads an Accept does not resubmit. All four now read back the disposition emitted.
975
+
976
+ **The grounding is inside this package rather than in a spec clause**, the same tiebreak the entry
977
+ below records: `buildTA1` emitted bytes that this package's own reader decoded into a different
978
+ disposition than the caller asked for, while every other builder already released the same class of
979
+ element through the same helper.
980
+
981
+ **🛑 It changes bytes this library already put on the wire, and that is the cost.** A value
982
+ containing none of the four delimiters and no `?` is emitted byte-for-byte as before, which is
983
+ every conformant TA1: TA1-01 echoes ISA-13, TA1-02 / TA1-03 echo ISA-09 / ISA-10, and TA1-04 /
984
+ TA1-05 are code list values.
985
+
986
+ **🛑 No warning code is added and no case moves onto a new code, but the consumer predicate MOVES
987
+ IN BOTH DIRECTIONS.** `parseTA1` of a `buildTA1` output now reports the disposition and note the
988
+ caller passed; before, it reported whatever element the shift left in TA1-04, which could be the
989
+ caller's, a coincidental in-enum value, or an out-of-enum one narrowed to `"R"`. So
990
+ `ackCode === "R"` **stops** firing where an Accept had been shifted onto it, and **starts** firing
991
+ where a Reject had been shifted off it: `interchangeTime: "12*A"` with `ackCode: "R"` read `"A"`
992
+ before and reads `"R"` now, with every field a valid member of its union. `ackCode === "A"` moves
993
+ the same two ways. What is one-directional is the safety, which is a different statement: nothing
994
+ now reports a disposition the caller did not ask for.
995
+
996
+ **What releasing the rest of the set costs, and where it does not cost.** Only `*`, `~` and a `?`
997
+ immediately before the separator ever shifted the segment's own element framing. **`^` and `:`
998
+ moved the dot-path reader instead, and releasing them is a gain there:**
999
+ `getSegmentValue(ta1, "01")` answered `"0000"` at `0.0.14` for a control number of `"0000^0001"`,
1000
+ silently truncating the reassociation key to the first repetition, and answers `"0000^0001"` now;
1001
+ 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
1004
+ dot-path read unescapes and answered `"0000?0001"` on both. No total is published: that is what was
1005
+ measured, not a closed account. `getSegmentValue` takes an `X12Segment` and `Ta1Segment` carries no
1006
+ `id`, so add one to read a TA1 through it. If you were escaping the value yourself, as `KNOWN-LIMITATIONS.md`
1007
+ advised while this was open, drop that - you are now escaping twice on both kinds of surface
1008
+ (`"00000001??"` in, `TA1*00000001????*…` out, `getSegmentValue` answering `"00000001??"`).
1009
+
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.
1015
+
1016
+ **`BuildTA1Options` gained `repetitionSeparator`, `componentSeparator` and `segmentTerminator`**
1017
+ beside the existing `elementSeparator`, the same four `build999` already takes. They exist for
1018
+ escaping and nothing else - `buildTA1` still emits no segment terminator, no repetition and no
1019
+ composite. Escaping against a guessed delimiter set is a value corruption rather than a safe
1020
+ default: `unescapeRelease` preserves `?X` verbatim for any `X` outside the reader's declared set,
1021
+ so a value released against the wrong delimiter comes back carrying a stray `?`. The defaults are
1022
+ unchanged and are the cosyte archetype, which this function cannot verify, so state the separators
1023
+ if you embed a TA1 in an envelope that declares different ones.
1024
+
1025
+ **A non-string element now refuses** with `AckBuildError` and the existing `X12_ACK_INVALID_SPEC`
1026
+ code, and that is a prerequisite of the escape rather than a separate guard: releasing a value
1027
+ routes it through the escape helper, and the bare `escapeRelease` underneath it returns its empty
1028
+ accumulator for a `number`, so escaping without the type check would have replaced a shifted TA1-01
1029
+ with a vanished one. A numeric `interchangeControlNumber` emitted `TA1*12345*260601*1200*A*000`
1030
+ before, with the number surviving onto `elements` inside a value typed `readonly string[]`, and an
1031
+ absent one emitted `TA1**250101*1200*A*000`. The accept-with-note refusal still runs first and
1032
+ still reports `X12_TA1_ACCEPT_WITH_NOTE`, so no existing refusal moves code.
1033
+
1034
+ **What this does not close:** `buildTA1` still uses no segment joiner, so its refusal names the
1035
+ builder and never `TA1-01`; `buildInterchange` still does not escape GS-04, GS-05 or GS-07; and an
1036
+ unescaped active delimiter is still not safe anywhere. Measured in `KNOWN-LIMITATIONS.md`.
1037
+
1038
+ - **🩺 A `?` immediately before the element separator inside an envelope segment now frames as ONE
1039
+ element, which fixes a released delimiter and is a SYMMETRIC change**
1040
+ (`X12-ENVELOPE-SPLITTER-NOT-RELEASE-AWARE`). Reproduced on the base
1041
+ tree at `1b71733`. `src/parser/envelope.ts`'s `splitElements` - the splitter for `GS`, `GE`, `ST`,
1042
+ `SE`, `IEA` and `TA1` - was a plain `String.prototype.split` on the element separator, so a `?*`
1043
+ ended the element anyway and every element after it moved down a slot:
1044
+
1045
+ ```text
1046
+ GS*HC*S*R*20260601*1200*1*X*005010?*X222A1~ ten elements, GS-08 read "005010?"
1047
+ applicationSenderCode "SEND*ER" GS-08 read "X", the GS-07 agency code
1048
+ groupControlNumber "1*2" GS-08 read "X"; warnings: []
1049
+ transactionSetControlNumber "00*01" ST-03 read "01"; warnings: []
1050
+ CLM*PT?*ACCT*150.00~ three elements - the BODY control, always correct
1051
+ ```
1052
+
1053
+ **What made it a defect rather than a tolerance is that this package's own two halves disagreed
1054
+ inside a single call:** `buildInterchange` maps its release escaper over GS-02 / GS-03 / GS-06 /
1055
+ GS-08 and ST-01 / ST-02 / ST-03, then returns `parseX12` of the bytes it just wrote, so
1056
+ `applicationSenderCode: "SEND*ER"` emitted a correct `GS*HC*SEND?*ER*...` and handed back a model
1057
+ whose GS-08 answered `"X"`, with `warnings: []`.
1058
+
1059
+ **🛑 READ THE CHANGE AS SYMMETRIC. IT IS NOT ONLY A CORRECTION.** A `?` before the separator has
1060
+ two readings and 005010 does not transmit which the sender meant. Where the sender **escaped** a
1061
+ delimiter, `0.0.14` framed it wrongly and this release frames it correctly: a correction, the rows
1062
+ above. Where the sender sent a **literal `?`** as the element's last byte, `0.0.14` framed it
1063
+ correctly and this release merges the element with its successor, so **the segment loses its LAST
1064
+ element**: a regression. `GS*HC*SUB1*RCV?*20260601*1200*000000123*X*005010X222A1~` read nine
1065
+ entries and reads **eight** here, GS-06 answering `"X"` and GS-08 gone. Every other release
1066
+ sequence in an envelope element (`??`, `?:`, `?^`, `?~`, `?A`) framed identically before and after
1067
+ and still does, pinned as invariance controls.
1068
+
1069
+ **No warning code is added, and `X12_CONTROL_NUMBER_MISMATCH` moves in BOTH DIRECTIONS.** Where the
1070
+ old shift displaced a control number it stops firing, so **a consumer that rejects on that code
1071
+ will now accept such a document**; where a literal `?` newly displaces one it starts firing, so
1072
+ that same consumer **will now reject a document `0.0.14` accepted**. Both are pinned, alongside a
1073
+ genuine mismatch that raises it on either tree.
1074
+
1075
+ **🩺 The regression direction reaches an 837's variant and its money, by one route.** `ST-03`
1076
+ decides the 837 variant, so an `ST-02` ending in a literal `?` destroys `ST-03` and the document
1077
+ falls back to the `SVx` scan. An 837 declaring `005010X222A1` whose only service segment is an
1078
+ `SV2` read `variant` `"P"`, `charge` `undefined` and `X12_837_SERVICE_LINE_NOT_DECODED` at
1079
+ `0.0.14`, and reads `variant` `"I"` with `charge` `150.00` and that warning silent here. **A warned
1080
+ non-decode becomes a decoded amount.** Re-check any routing driven off `submission.variant` if your
1081
+ partners send envelope elements that can end in a literal `?`.
1082
+
1083
+ **Why it was taken anyway, and it is CONSISTENCY rather than a spec clause.** The two readings are
1084
+ mutually exclusive and nothing in 005010 picks between them. `decodeSegment` has read BODY elements
1085
+ the escape-wins way on every released version (`REF*EA*RCV?*NEXT` has always been two elements), so
1086
+ the envelope now obeys the one rule the rest of the package already obeyed.
1087
+
1088
+ **🩺 The exposure is NOT inbound bytes only.** Envelope slots routed through the builders' release
1089
+ escaper are safe. Not every emit slot is routed through it, and no total is published: what follows
1090
+ is the routes measured to reach the regression direction, not a closed account of what bypasses the
1091
+ escaper. **`buildInterchange` does not escape GS-04, GS-05 or GS-07**: a `groupDate` of
1092
+ `"2026060?"` read nine GS elements at `0.0.14` and reads eight here, GS-08 gone. If you emit one
1093
+ of those, escape or reject a `?` yourself.
1094
+
1095
+ **Values are still RAW, pre-`?`-unescape**, exactly as `X12Segment.elements` has always documented:
1096
+ `gs.elements[2]` reads `"SEND?*ER"`, not `"SEND*ER"`. `elements.join(separator)` therefore still
1097
+ reproduces the segment byte for byte, which is what `serializeX12` relies on when it substitutes a
1098
+ recomputed `SE-01` / `GE-01` / `IEA-01` into a control segment.
1099
+
1100
+ **🩺 The ISA is deliberately exempt.** ASC X12 .5 makes it fixed-width, which is what lets the
1101
+ delimiter set be recovered from it before anything is parsed, so a `?` in an ISA element is content
1102
+ and never an escape; release-splitting it would collapse a well-formed ISA below its 17 entries.
1103
+ A degenerate delimiter set whose element separator IS `?` also falls back to the literal split,
1104
+ the same guard the segment-terminator scanner already carried. Both are pinned, each with a red
1105
+ negative control.
1106
+
1107
+ **🛑 An UNESCAPED delimiter is still not safe and nothing here claims otherwise** - a bare active
1108
+ delimiter ends its element, because that is what a delimiter is. `build837`'s
1109
+ `implementationConventionReference` still refuses a value carrying one, deliberately not relaxed:
1110
+ a partner's parser is not obliged to be release-aware either, and widening an emit surface is its
1111
+ own decision rather than a side effect of a reader fix.
1112
+
746
1113
  - **🩺 An `AMT` / `ADX` row whose amount decodes nothing is no longer dropped in silence**
747
1114
  (`X12-AMT-ADX-ABSENT-AMOUNT`). Reproduced on the base tree at `9db104b` across all four readers,
748
1115
  and the report it now raises is the `X12_AMOUNT_ROW_DROPPED` entry under **Added** above. The
@@ -871,8 +1238,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
871
1238
  are both the line charge, so reading a service segment into a line the walker never opened
872
1239
  mis-reads money. Refusing to read is the safe half; doing it silently was the defect. **This says
873
1240
  nothing about how the variant resolved**, and `KNOWN-LIMITATIONS.md` now discloses why: a caller's
874
- `type` option wins first, and absent one, where `ST-03` names none of the three known
875
- implementation conventions, the reader falls back to the first `SVx` in the transaction body,
1241
+ `type` option wins first, and absent one, where `ST-03` names no implementation convention this
1242
+ reader recognises, the reader falls back to the first `SVx` in the transaction body,
876
1243
  orphans included, so a stray `SV2` re-types the whole submission. That is pre-existing behaviour, measured identical at `0.0.10`, and is deliberately
877
1244
  not narrowed here.
878
1245