@cosyte/x12 0.0.12 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,6 +16,52 @@ model.
16
16
 
17
17
  ## Data / decode boundaries
18
18
 
19
+ - **🩺 Which `ST-03` implementation-convention references resolve to an 837 variant CHANGED in this
20
+ release, and some already-published files therefore decode differently** (`X12-VARIANT-ICR-UNGROUNDED`).
21
+ Through `0.0.13` `get837Claims` recognised exactly three references: `005010X222A2`, `005010X223A3`
22
+ and `005010X224A2`. **That set contained none of the identifiers HIPAA adopts at 45 CFR 162.1102**
23
+ (`005010X222`, `005010X223` + `005010X223A1`, `005010X224` + `005010X224A1`), **and it was missing
24
+ `005010X222A1` and `005010X223A2`**, which CMS and state Medicaid companion guides require in ST-03
25
+ and GS-08 on production professional and institutional claims. A conformant 837P declaring
26
+ `005010X222A1` therefore resolved to no variant at all and fell through to the `SVx` scan, where a
27
+ single stray `SV2` anywhere in the body re-typed the whole submission, and
28
+ `X12_837_UNKNOWN_VARIANT` accused a document that was not non-conformant. The reader now recognises
29
+ each base guide and each of its published errata. Sources for every key are named beside the table
30
+ in `src/transactions/claim/get-837.ts` and in
31
+ `documentation/agent-notes/x12-variant-icr-ungrounded.md`.
32
+ - **🛑 What this changes for a consumer, and it is a behaviour change on already-published
33
+ decoding.** On a file whose `ST-03` is now recognised: `submission.variant` can differ from what
34
+ `0.0.13` read, where the first `SVx` in the body disagreed with the declaration; and
35
+ **`X12_837_AMBIGUOUS_VARIANT` and `X12_837_UNKNOWN_VARIANT` no longer fire at all**, because no
36
+ guess was made. A predicate written against either code goes quiet on such a file. **🩺 And a
37
+ service line whose `SVx` kind disagrees with the declaration is no longer DECODED, so a code
38
+ STARTS firing on a document that may have carried `warnings: []`**: under `005010X222A1` with a
39
+ body whose only service segment is an `SV2`, `0.0.13` read `variant` `"I"` and `charge` `7300`
40
+ with no warnings, and this release reads `variant` `"P"`, `charge` `undefined`, and
41
+ `X12_837_SERVICE_LINE_NOT_DECODED` at that line's `LX`. A mis-stamped envelope is an ordinary
42
+ vendor variant and this reader cannot tell one from a conformant document; the loss is warned
43
+ rather than silent. **Read all of this as ONE property and never as a closed list: where `ST-03`
44
+ is now recognised the document's own declaration decides the variant instead of its first
45
+ service segment, and everything downstream follows from that.** Re-check any routing driven off
46
+ `submission.variant` for 837s read on `0.0.13` or earlier.
47
+ - **🛑 The `SVx` fallback is NOT narrowed.** First-wins still takes the first service segment in the
48
+ body, orphans included, on every document that still reaches it. What changed is which documents
49
+ reach it.
50
+ - **It is a LIST of cited identifiers, never a pattern.** A reference outside the set, in a
51
+ different case, or carrying leading whitespace, still falls through exactly as before. Nothing
52
+ trims, lower-cases or prefix-matches `ST-03`, because no source says to.
53
+ - **The set is not claimed exhaustive and no count of it is published**, here or in any warning
54
+ message. Both variant messages named the three old keys literally and were wrong the moment the
55
+ table was corrected, so neither enumerates the set any more.
56
+ - **🩺 OPEN, and NOT fixed here: the EMIT side still stamps the old three.** `build837P` /
57
+ `build837I` / `build837D` write `005010X222A2` / `005010X223A3` / `005010X224A2` into ST-03 and
58
+ GS-08, and **a caller cannot override the value**. Two of those are not what the companion guides
59
+ above require, so **a trading partner that requires `005010X222A1` or `005010X223A2` will reject
60
+ an 837 this builder emits.** Deliberately not re-stamped: which published guide identifier a
61
+ partner accepts is a partner fact rather than a spec fact, and changing bytes this library
62
+ already emitted would break the partners it works with today. The remedy is a caller-supplied
63
+ override, which is a public-surface addition and its own slice.
64
+
19
65
  - **🩺 BREAKING for `build277` callers: a 277 service line now REQUIRES `unitsOfService`, because
20
66
  SVC-07 is a required element in `005010X212` and this library was not emitting it at all**
21
67
  (`X12-277-SVC07-NOT-DECODED`). Through the release before this one `get277Status` read SVC-01
@@ -56,28 +102,34 @@ model.
56
102
  on the emit side. All of it is pre-existing and reproduces at `e3cdf49`; widening the guard would
57
103
  have turned this into that audit, which is its own item.
58
104
 
59
- - **🩺 An unparseable decimal still lands on the model as a stand-in; what changed is that it warns.**
60
- A decimal element that is present and that this library cannot decode as a decimal (`1,234.56`,
61
- `$450.00`, `N/A`) yields no value, and the reader has to put something in its place: a slot typed
62
- `X12Decimal` gets `X12Decimal.ZERO`, an optional slot gets `undefined`, and some rows are dropped
63
- whole. **Every one of those outcomes now emits `X12_UNPARSEABLE_DECIMAL` at the failing
64
- `position.elementIndex`, because the warning is a property of the READ rather than of what the
65
- reader then does with it. No outcome is otherwise changed.** No list of the outcomes is published
66
- here, on purpose: a first draft enumerated three and a review measured a fourth, and the rule is
67
- what holds, not the census. So a consumer that reads only the model, and never looks at
68
- `.warnings`, sees exactly what it saw before: a `0` where the payer sent something unreadable.
69
- **That is the residual, stated plainly.** Closing it further means changing every `X12Decimal`
70
- model slot to `X12Decimal | undefined`, which is a breaking model change and is deliberately not in
71
- this slice. Gate on the warning, or read `readElementDecimal` yourself.
105
+ - **🩺 An unparseable decimal reads `undefined`, and it warns.** A decimal element that is present
106
+ and that this library cannot decode as a decimal (`1,234.56`, `$450.00`, `N/A`) yields no value,
107
+ and the reader has to put something in its place. **Through `0.0.12` a slot typed `X12Decimal` got
108
+ `X12Decimal.ZERO`**, so a consumer that read only the model saw a `0` where the payer sent
109
+ something unreadable; **as of `0.0.13` no reader substitutes `X12Decimal.ZERO` for a value it did
110
+ not decode** - every slot that used to get one is `X12Decimal | undefined` and reads `undefined`,
111
+ which is a breaking type change on the read model and is the point of it. **Read that as a rule
112
+ about the substitution, not as a census of the model:** an optional slot already read `undefined`,
113
+ and some rows are dropped whole, which is a different shape with a report of its own
114
+ (`X12_AMOUNT_ROW_DROPPED`, the next entry). **Every one of those
115
+ outcomes emits `X12_UNPARSEABLE_DECIMAL` at the failing `position.elementIndex`, because the
116
+ warning is a property of the READ rather than of what the reader then does with it.** No list of
117
+ the outcomes is published here, on purpose: a first draft enumerated three and a review measured a
118
+ fourth, and the rule is what holds, not the census.
72
119
 
73
120
  Three scoping facts that are easy to get wrong in the other direction:
74
- - **An ABSENT element does not warn.** "Missing means zero" is the documented convention of the
75
- slots that use `elementDecimalOrZero` and is unchanged.
76
- - **🩺 That does NOT make an unwarned `0` trustworthy in general, and this is the one inversion to
121
+ - **An ABSENT element does not raise this code.** It reads `undefined` and
122
+ `X12_UNPARSEABLE_DECIMAL` is not emitted for it, so `undefined` alone does not tell you which of
123
+ the two happened - the warning at that `elementIndex` does, and `readElementDecimal` gives you
124
+ the same distinction in band. Read that as a statement about **this code**, not as "an absent
125
+ element is silent": where the absent element takes a whole `AMT` / `ADX` row off the model, the
126
+ drop is reported by `X12_AMOUNT_ROW_DROPPED` at that segment (the next entry), which is
127
+ raised for both routes and therefore separates neither.
128
+ - **🩺 An unwarned value is not thereby trustworthy in general, and this is the one inversion to
77
129
  refuse.** The warning is a property of a decimal READ, not a property of a model slot. A slot
78
130
  the reader never read at all cannot warn, and it still holds whatever the accumulator was seeded
79
- with. **What this guarantees is narrower and exact: an unwarned `0` AT AN ELEMENT A READER
80
- DECODED is a zero the sender sent or omitted.** The known slot of the other kind, an 837 service
131
+ with. **What this guarantees is narrower and exact: an unwarned value AT AN ELEMENT A READER
132
+ DECODED is what the sender sent.** The known slot of the other kind, an 837 service
81
133
  line whose `SVx` never decoded, is covered by a warning of its own (next entry) rather than by
82
134
  this one. **No census of never-read slots is published**, on purpose: the rule is what holds.
83
135
  - **The public helpers are silent without a sink.** `elementDecimal` / `elementDecimalOrZero` take
@@ -91,23 +143,120 @@ model.
91
143
  comments. It says nothing about a decimal decoded some other way, and no exhaustive census of
92
144
  such routes is published here, on purpose.
93
145
 
94
- - **🩺 An 837 service line whose `SVx` never decoded still ships with `charge` and `units` at `0`;
95
- what changed is that it warns.** `get837Claims` resolves ONE variant for the submission, from the
146
+ - **🩺 An `AMT` or `ADX` whose amount does not decode loses its WHOLE row, and says so as of
147
+ `0.0.13`.** These segments carry an amount plus the thing the amount is about - AMT-01's qualifier,
148
+ ADX-02's adjustment reason code, ADX-03 / ADX-04's reference. The readers build no row at all when
149
+ the amount (AMT-02, ADX-01) decodes no value, so what the sender did state goes with it: through
150
+ `0.0.12` `AMT*B6~` gave `claim.amounts: []` and `warnings: []`, which reads exactly like a document
151
+ that never carried the segment. **`X12_AMOUNT_ROW_DROPPED` now reports it**, anchored at the
152
+ `AMT` / `ADX` and carrying **no `elementIndex`**, because one of its two routes is an absent
153
+ element and an absent element has no index to name. Four surfaces raise it: the 835's `AMT`, the
154
+ 837's `AMT`, the 834's coverage `AMT` (on that **member's** own `warnings`, exactly as the decimal
155
+ sink beside it is scoped) and the 820's `ADX`. On the 835 and the 837 the `AMT` attaches to the
156
+ open **service line** first and to the claim only when there is none, so **the row lost may be a
157
+ line-level one** - do not read an unchanged `claim.amounts` as evidence the warning is stale. Four
158
+ bounds worth stating:
159
+ - **It is additive, and no case moved onto it.** A present-but-undecodable amount still raises
160
+ `X12_UNPARSEABLE_DECIMAL` at its own `elementIndex`, now alongside this code rather than instead
161
+ of it, so a predicate written against that code fires on exactly the documents it fired on
162
+ before. Whether one accompanies this code at the same `segmentIndex` is what tells the absent
163
+ route from the unparseable one, since this code is raised for both and discriminates neither.
164
+ - **It reports a row whose AMOUNT was read and decoded no value, and nothing wider.** State it as
165
+ a property of the READ, not of the walker's control flow. A segment discarded before its amount
166
+ is read is not on this channel (the 834's `AMT` with no `HD` open, the 820's `ADX` with no
167
+ remittance open), and neither is one whose amount decoded and then found nothing to attach to
168
+ (the 835's and the 837's `AMT` before any claim). **Do not write it as "nothing open means
169
+ silent":** the 835 and the 837 decode first, so an `AMT` with an absent amount and no claim open
170
+ does raise this code.
171
+ - **🔴 An 820 `RMR` is not on this channel, and the reason is NOT that its row survives.**
172
+ `decodeRmr` drops on open-item identity (RMR-01 and RMR-02 both empty), **before** RMR-04 is
173
+ read. So an `RMR` that states an open item and no amount keeps its row with `amountPaid` left
174
+ `undefined` and there is nothing to report - but an `RMR` that states an **amount and no open
175
+ item** is dropped whole, taking a stated payment, its payment-action code and its amount due
176
+ with it. Nothing failed to decode there, so it is not this code's shape;
177
+ `X12_STATED_AMOUNT_DISCARDED`, below, is what reports it.
178
+ - **A Loop 2430 `AMT` under an open `SVD` is discarded outright, and this code is not what says
179
+ so.** With a claim and a line open, the 837's adjudication skip drops an `AMT` that decoded
180
+ perfectly well. Through `0.0.12` that was silent while one that decoded nothing raised this
181
+ code, so the report was present exactly where less was lost;
182
+ `X12_STATED_AMOUNT_DISCARDED`, below, closed that.
183
+
184
+ - **🩺 A row the sender DID state can still be discarded, and says so as of `0.0.13`.** The code
185
+ above is about an amount this library could not read. This is the opposite case: **the reader
186
+ discarded the row for a reason that is not about the amount at all**, so the amount was never the
187
+ problem and, on one of the two routes, was never even looked at. The bytes stay verbatim on
188
+ `tx.segments[…].raw`; decode them yourself. Through `0.0.12` both routes were silent on every
189
+ channel. **`X12_STATED_AMOUNT_DISCARDED` reports them**, anchored at
190
+ the segment and carrying **no `elementIndex`**. The two routes, enumerated:
191
+ - An **820 `RMR`** under an open remittance loop whose RMR-01 and RMR-02 are **both empty** while
192
+ RMR-04 or RMR-05 is populated. `decodeRmr` refuses the open item on identity before either
193
+ amount element is read, so a stated payment, a stated amount due and RMR-03's payment action
194
+ code leave the model together. A bare `RMR~` states nothing and stays silent.
195
+ - An **837 `AMT`** arriving while a Loop 2430 line adjudication (`SVD`) is open, whose AMT-02
196
+ decoded. The v1 adjudication model carries no amount row, and attaching one to this
197
+ submission's own service line would put another payer's figure on it, so the row is skipped.
198
+ `AMT*EAF` (Remaining Patient Liability) on a Loop 2430 is exactly the shape being lost.
199
+
200
+ Three bounds worth stating:
201
+ - **It is additive, and nothing moved onto it.** `X12_AMOUNT_ROW_DROPPED` and
202
+ `X12_UNPARSEABLE_DECIMAL` fire on exactly the documents they fired on before, pinned by
203
+ committed tests.
204
+ - **The two amount-row codes are disjoint and can never name the same segment**, because this one
205
+ requires an amount element the sender populated and the other requires one that decoded no
206
+ value. The code you get is the discriminant.
207
+ - **It reports a segment that arrived while the loop that would carry its row was open.** An
208
+ `AMT` or `ADX` reaching a reader with **no such loop open** is a different loss and is **still
209
+ silent**: the 834's `AMT` with no `HD` open, the 820's `ADX` with no remittance open, and the
210
+ 835's and the 837's `AMT` before any claim or service line is open. **🔴 Read "still silent"
211
+ literally and no wider: at those last two sites the inversion above SURVIVES.** With no claim
212
+ open an 835 `AMT*B6~` raises `X12_AMOUNT_ROW_DROPPED` while `AMT*B6*500.00~` raises nothing, so
213
+ the channel still reports the smaller loss there. `PRE-EXISTING` and unchanged by this release. And on the `RMR` route this
214
+ code says nothing about whether the amount would have decoded, because the row is refused before
215
+ the decode is attempted, so **no `X12_UNPARSEABLE_DECIMAL` accompanies it even where the bytes
216
+ are unreadable.** It is raised on `RMR****1,234.56~` exactly as on `RMR****150.00*150.00~`, so
217
+ **never read an unaccompanied instance as evidence the bytes are postable**; only the `AMT`
218
+ route guarantees a decodable amount, because there AMT-02 decoded before the row was skipped.
219
+
220
+ - **🩺 An 835 balance invariant with an undecoded term is reported as UNEVALUABLE, not as a
221
+ mismatch.** The three TR3 005010X221A1 §1.10.2 equations (line, claim, top-of-remit) read amounts
222
+ that are all `X12Decimal | undefined` as of `0.0.13`. Where any term of an equation is
223
+ `undefined`, the equation is not run and `X12_835_BALANCE_NOT_EVALUABLE` is emitted instead of
224
+ `X12_835_REMIT_BALANCE_MISMATCH`, because nothing was measured out of balance: substituting `0`
225
+ for the missing term would be this library asserting a total nobody sent. **Through `0.0.12` that
226
+ substitution is exactly what happened**, so an 835 with an absent `CLP-03` reported a mismatch
227
+ between the payer's own amounts and an invented zero. Two bounds worth stating:
228
+ - **An EMPTY list is not an absent term.** A claim carrying no `CAS` really did state no
229
+ adjustments, and that sums to `X12Decimal.ZERO`. Only a term this library decoded no value from
230
+ stops the equation.
231
+ - **`build835` cannot reach it from TypeScript.** Every balance term on `Build835Spec` is a
232
+ required `X12Decimal`. A JS caller passing `undefined` gets `X12_835_BUILD_INVALID_SPEC` (an
233
+ untyped `TypeError` through `0.0.12`), deliberately NOT the build-side balance-mismatch code.
234
+
235
+ - **The public `elementDecimalOrZero` helper still substitutes `X12Decimal.ZERO`, and no reader in
236
+ this library calls it any more.** Its documented behaviour is unchanged and it stays exported, so
237
+ a consumer walking segments itself can still opt into the substitution - knowingly, rather than by
238
+ inheriting a convention the `get*` readers no longer follow. `elementDecimal` and
239
+ `readElementDecimal` are the honest routes.
240
+
241
+ - **🩺 An 837 service line whose `SVx` never decoded ships with `charge` and `units` `undefined`,
242
+ and it warns.** `get837Claims` resolves ONE variant for the submission, from the
96
243
  caller's `type` option, else ST-03's implementation-convention reference, else the first `SVx`
97
244
  segment present. A Loop 2400 line is then decoded only by the `SV1` / `SV2` / `SV3` that matches
98
245
  that variant. When none arrives, because the line carries an `SVx` for a different variant (an
99
246
  ST-03 of `005010X222A2` on a file whose lines are `SV2`, or a `{ type: "P" }` over the same) or
100
247
  because it carries no `SVx` at all, **nothing on the service segment is read**: the line's
101
- `charge` and `units` hold the accumulator's seeded `X12Decimal.ZERO`, and its procedure code,
248
+ `charge` and `units` hold the accumulator's seeded `undefined`, and its procedure code,
102
249
  modifiers, unit of measure and place of service are equally undecoded. **That line now emits
103
250
  `X12_837_SERVICE_LINE_NOT_DECODED`, anchored at the `LX` that opened it.** Read the values off
104
251
  `tx.segments[…].raw` and decide which of the two disagreeing signals the sender meant.
105
252
  - **Ignoring the foreign `SVx` is deliberate and is not the limitation.** `SV1-02` and `SV2-03`
106
253
  are both the line charge, so decoding an `SV2` into a Professional line would mis-read money.
107
254
  Refusing to read is the safe half; doing it silently was the defect.
108
- - **The model is unchanged.** `charge` and `units` are still typed `X12Decimal`, so the stand-in
109
- `0` is still what a consumer that never reads `.warnings` sees. Making them
110
- `X12Decimal | undefined` is a breaking model change and is deliberately not in this slice.
255
+ - **`undefined` does not say WHY, which is what the warning is for.** `charge` and `units` are
256
+ `X12Decimal | undefined` as of `0.0.13` (`X12Decimal` reading a fabricated `0` through
257
+ `0.0.12`), so a consumer that never reads `.warnings` at least sees that nothing was decoded -
258
+ but it cannot tell this case, where no `SVx` was read at all, from an `SVx` that WAS read and
259
+ carried no charge element. Only `X12_837_SERVICE_LINE_NOT_DECODED` separates them.
111
260
  - **The line is still retained, and so are the bytes.** Nothing is dropped: the service line is
112
261
  on the claim, and every segment, decoded or not, stays verbatim on `tx.segments`.
113
262
  - **Neither cause is attributed to the sender.** A `type` option that disagrees with a perfectly
@@ -214,20 +363,102 @@ model.
214
363
  - **The segment is NOT decoded into any line.** `SV1-02` and `SV2-03` are both the line charge, so
215
364
  reading a service segment into a line the walker never opened mis-reads money. Refusing to read
216
365
  is the safe half; doing it silently was the defect. **This does not change the `SV1-02` case
217
- above:** an absent `SV1-02` on a line that DID open still reads a confident `0`, which closes
218
- only with the deferred `X12Decimal | undefined` model change.
366
+ above:** an absent `SV1-02` on a line that DID open reads `undefined` as of `0.0.13`, a
367
+ confident `0` through `0.0.12`, and this code has nothing to do with either.
219
368
  - **🩺 IT SAYS NOTHING ABOUT THE VARIANT, AND A FIRST DRAFT OF THIS BOUND CLAIMED IT DID.** Variant
220
369
  resolution runs before the walk. A caller-supplied `type` option wins first; absent one, and
221
- where `ST-03` names none of the three known implementation conventions, it **falls back to the
370
+ where `ST-03` names no implementation convention this reader recognises, it **falls back to the
222
371
  first `SVx` segment id anywhere in the transaction body - orphans included**. So a stray `SV2`
223
- under an `ST-03` of `005010X222A1` re-types the whole submission as Institutional, and every
224
- conformant `SV1` line in it then reads `charge` `0`, `units` `0` and an `undefined` procedure
372
+ under an unrecognised `ST-03` re-types the whole submission as Institutional, and every
373
+ conformant `SV1` line in it then reads `charge` `undefined`, `units` `undefined` (both a
374
+ fabricated `0` through `0.0.12`) and an `undefined` procedure
225
375
  code - `undefined`, not `""`, which on such a line is the `revenueCode`. Passing
226
376
  `{ type: "P" }` reads the same document correctly. Measured, both trees: `PRE-EXISTING`,
227
377
  identical at `0.0.10`, **not** introduced or changed by this code, and **not** narrowed here - excluding
228
378
  orphans from the fallback would change how existing documents decode and is its own slice. It
229
379
  is warned rather than silent (`X12_837_SERVICE_LINE_NOT_DECODED` at each `LX`), and
230
- `submission.variant` is the field that tells you.
380
+ `submission.variant` is the field that tells you. **The resolution itself is now reported too
381
+ wherever it was contested**, which is the entry below; the fallback is still not narrowed.
382
+
383
+ - **🩺 An 837 variant the `SVx` fallback resolved in a body that names more than one variant raises
384
+ `X12_837_AMBIGUOUS_VARIANT`, anchored at the `ST`.** The fallback is unchanged and is deliberately
385
+ NOT narrowed: it still takes the **first** `SV1` / `SV2` / `SV3` in the body, orphans included, so on
386
+ every document that reaches it the variant resolved is byte-for-byte what it was through `0.0.13`.
387
+ **WHICH documents reach it changed in this release** - see the ST-03 entry at the top of this list. What this code
388
+ adds is that the resolution said so. Through `0.0.13` a stray `SV2` re-typed a whole Professional
389
+ submission Institutional and **only the line-level consequences were on any channel** - a consumer
390
+ routing on `submission.variant` saw a confident `"I"` with nothing to contradict it. Four bounds,
391
+ each a committed test:
392
+ - **It reports the RESOLUTION, never the document.** A caller-supplied `type` wins ahead of the
393
+ fallback, and so does an `ST-03` this reader turns into a variant; in
394
+ either case no guess was made and this code is not raised **however mixed the body is**. Never
395
+ restate it as "the file carries more than one kind of service segment".
396
+ - **🩺 Which service segment is the stray one is NOT decided, and nothing here should ever start
397
+ deciding it.** This reader cannot tell a stray service segment from a conformant one, and the
398
+ fallback takes the first whether or not a Loop 2400 was open at it, so an orphan decides the
399
+ variant like any other. Reporting the conflict is honest; picking a winner would be inventing.
400
+ - **It is ADDITIVE and nothing moved onto it.** `X12_837_SERVICE_LINE_NOT_DECODED`,
401
+ `X12_837_SERVICE_SEGMENT_WITHOUT_LX` and `X12_837_SERVICE_LINE_DROPPED` fire on exactly the
402
+ documents they fired on before **this code was added**, in the same positions, and no predicate
403
+ changes meaning because of this code. **That is a claim about this code and NOT about the
404
+ release:** the ST-03 entry at the top of this list changed which documents reach the fallback at
405
+ all, and a line whose `SVx` kind disagrees with a now-recognised `ST-03` stops decoding and
406
+ starts raising `X12_837_SERVICE_LINE_NOT_DECODED`. Read that entry beside this bound.
407
+ - **It can never travel with `X12_837_UNKNOWN_VARIANT`**, which is the other outcome of the same
408
+ resolution: a body with conflicting service segments has, by construction, at least one to fall
409
+ back on. It fires **once per transaction**, because there is one resolution per transaction.
410
+ - **What it does NOT close:** narrowing the fallback, which is deliberately left alone. It no
411
+ longer leaves a second `SVx` inside an already-open Loop 2400 unreported: that was its own
412
+ slice, and it is the entry below. **Read the earlier wording as withdrawn** - it said such a
413
+ segment is "read into nothing", which was true only of one whose kind does not match the
414
+ resolved variant; a DUPLICATE of the matching kind is read into the line and REPLACES what the
415
+ first one wrote.
416
+
417
+ - **🩺 A second `SV1` / `SV2` / `SV3` inside an ALREADY-OPEN Loop 2400 raises
418
+ `X12_837_SERVICE_SEGMENT_REPEATED`, anchored at the repeated service segment itself.** A service
419
+ line carries **one** service segment's worth of slots, and every decoder writes **all** of the
420
+ slots its kind writes, so the model holds what the **last** service segment matching the
421
+ submission's resolved variant wrote and nothing of any earlier one. **Through `0.0.13` that
422
+ happened in complete silence, and it is money and a procedure code:** under an `ST-03` of
423
+ `005010X222A2`, `SV1*HC:99213*8500*UN*4***1~` followed by `SV1*HC:99999*12*UN*1***1~` inside one
424
+ `LX` left ONE line reading `charge` `12` and `procedureCode` `99999`, with `warnings: []`. `8500`
425
+ became `12`, CPT `99213` became `99999`, and nothing was raised on any channel. Six bounds, each a
426
+ committed test:
427
+ - **🛑 The decode is NOT narrowed, and that is deliberate.** Last-wins is unchanged, element for
428
+ element, so which values a document decodes to are byte-for-byte what they were at `0.0.13`.
429
+ This reader cannot tell a stray service segment from a conformant one, so choosing the first
430
+ instead would be inventing; and changing which occurrence wins changes how **already-published
431
+ documents decode**, the same call made about the variant fallback in the entry above. **This
432
+ closes only the silence.**
433
+ - **🩺 The worst corner is a repeat whose own charge element is ABSENT.** It writes `undefined`
434
+ over the amount the first one stated, and `X12_837_SERVICE_LINE_NOT_DECODED` does **not** fire
435
+ on that line, because a service segment _did_ decode. This code is then the only thing on the
436
+ channel that says why the charge is empty.
437
+ - **It fires on a repeat of ANY kind, decoded or not.** A service segment whose kind does not
438
+ match the resolved variant is read into nothing and overwrites nothing - the loss there is that
439
+ what it carries reaches no part of the typed model - and it is reported the same way, whether it
440
+ arrives before or after the matching one. That case was silent at the segment through `0.0.13`
441
+ too, and the entry above recorded it as a deferred residual.
442
+ - **Once per repeat, and the count is scoped to the LINE, never latched.** Three service segments
443
+ in one Loop 2400 are two warnings at two positions; a first service segment under a later `LX`,
444
+ or in a later claim, is a first and never a repeat. A latching flag would report a conformant
445
+ second line.
446
+ - **It can never name the same segment as `X12_837_SERVICE_SEGMENT_WITHOUT_LX`**, which requires
447
+ that NO Loop 2400 be open where this one requires that one is. It can travel with
448
+ `X12_837_SERVICE_LINE_NOT_DECODED` (at the `LX`), with `X12_837_AMBIGUOUS_VARIANT` (at the
449
+ `ST`), and with `X12_UNPARSEABLE_DECIMAL` raised by the repeat's own amount bytes. **That last
450
+ pair sits at the SAME `position.segmentIndex`, separated only by the decimal code's own
451
+ `position.elementIndex`** - measured, and pinned by a committed test. Never read a co-occurring
452
+ pair here as two segments.
453
+ - **It is ADDITIVE and nothing moved onto it.** Every code this reader raised on a document of
454
+ this shape before **this code was added**, it still raises, at the same position, and no
455
+ predicate changes meaning because of this code. Read that as invariance and **not** as a list of
456
+ what else you will see, and **not** as a claim about the release: the ST-03 entry at the top of
457
+ this list changed which documents reach the variant fallback.
458
+ **The package's own documentation was a consumer that needed updating**: the cookbook's
459
+ "gate before you post a line amount" recipe named four codes, none of which fires on the
460
+ overwrite document, so it was blind to it - it now names this one too, and a committed test
461
+ pins that the four-code gate misses what the five-code gate catches.
231
462
 
232
463
  - **🩺 An `N3` / `N4` / `PER` / `REF` discarded after a stray `LX` raises
233
464
  `X12_837_ENTITY_SEGMENT_DISCARDED_AFTER_LX`, anchored at the discarded segment itself.** This is
@@ -265,6 +496,58 @@ model.
265
496
  following a **stray** `LX` belongs to is not derivable from the TR3s in either direction, so the
266
497
  reader refuses to attribute it rather than guessing.
267
498
 
499
+ - **🩺 An 837 Loop 2000A that names the pay-to address more than once has only ONE slot on the model
500
+ to put it in, and the model carries one of the addresses rather than both.** The TR3s allow Loop
501
+ 2010AB at most once per Loop 2000A, so a repeated `NM1*87` is a non-conformant document. Each one
502
+ raises `X12_837_PAY_TO_ADDRESS_REPEATED`, anchored at the repeated `NM1*87`, so the choice below is
503
+ never made in silence.
504
+
505
+ **🩺 Through `0.0.12` the two addresses were FUSED, and that is what changed.** `payToAddress` is a
506
+ bare accumulator with no entity object to own it, so unlike every other party there was nothing to
507
+ be replaced at the repeat: `withLines` appended and `mergeAddress` fell back, and two `NM1*87`s
508
+ each carrying an `N3` and an `N4` read back a street line from **each** address plus a
509
+ `countryCode` off the **first** `N4`, on an address whose own `N4` names no country, with
510
+ `warnings: []`. Re-emitted through `build837P` that was one Loop 2010AB naming a payment
511
+ destination no sender stated. **If you read a repeated `NM1*87` on `0.0.12` or earlier, treat that
512
+ claim's `payToAddress` as unreliable rather than as either sender's address.**
513
+
514
+ **The rule now applied, and it is stated in the emit side's terms because the emit side reads this
515
+ slot:** occurrences are never merged; the **last occurrence that states an address of its own**
516
+ takes the slot; and an occurrence that states none - one with no `N3` or `N4` at all, or only a
517
+ valueless `N3` / `N4` whose elements are empty - **does not blank one that did**. "States an
518
+ address" means exactly what `emitAddress` would write a segment for, and reader and writer share
519
+ one predicate so they cannot disagree. That last clause is not a nicety: `build837P/I/D` gates Loop
520
+ 2010AB on `payToAddress !== undefined`, so a slot cleared or blanked by a repeat that stated
521
+ nothing would re-emit as **no pay-to loop**, or as a **bare `NM1*87`** carrying neither `N3` nor
522
+ `N4`. Both say something about where a payment goes that the sender did not.
523
+
524
+ **🩺 The cost, which is real and is not argued away: a repeat that states only PART of an address
525
+ puts only that part on the model, and re-emits only that part.** A second `NM1*87` followed by an
526
+ `N4` and no `N3` reads back that `N4` with `lines: []` and re-emits a Loop 2010AB with no `N3`,
527
+ where `0.0.12` emitted a complete-looking address assembled from two. Keeping the earlier street
528
+ lines is the fusion itself, so they are not kept. **Which occurrence the sender meant is not
529
+ derivable from the TR3s**, and the losing occurrence's address is **not on the model in any form** -
530
+ `tx.segments` is where its bytes are, and remains the only complete account of the document.
531
+
532
+ **Two bounds, both measured.** This is scoped to the pay-to route, which lives in Loop 2000A: an
533
+ `NM1*87` arriving while a `CLM` is open never reaches that route, and is neither resolved nor
534
+ warned by this code. **Where it lands instead depends on whether a service line is open, and this
535
+ document does not state a single destination for it**, because two were measured: with a Loop 2400
536
+ open it joins that line's `serviceLine.providers` (a line-level provider is TR3 Loop 2420), and
537
+ with a claim but no line open it joins `claim.providers` (Loop 2310). Both are pre-existing and
538
+ identical at `0.0.12`. And a document with **at most one** `NM1*87` per Loop 2000A is unaffected in
539
+ every respect, warning channel included - including the pre-existing case of a lone `NM1*87` with a
540
+ valueless `N3`, which still reads an address with no elements and still re-emits a bare `NM1*87`.
541
+
542
+ **🩺 One thing the cost above shares with the shape that refuted remedy 2, stated so the two are
543
+ not read as different in kind.** In Loop 2010AB the TR3s make `NM1`, `N3` **and** `N4` all Required,
544
+ so a re-emitted loop carrying only the winning occurrence's `N4` is short a Required `N3`, exactly
545
+ as a bare `NM1*87` is short both. The difference is not conformance, it is provenance: every
546
+ element that IS emitted was stated by the occurrence the model kept, where the fused address
547
+ contained elements no single occurrence stated. Emitting a Loop 2010AB short a Required segment is
548
+ a pre-existing property of `emitAddress`, reachable at `0.0.12` from a lone `NM1*87` with an
549
+ `N4` and no `N3`, and is not introduced here.
550
+
268
551
  - **🩺 Through `0.0.9`, a lookup keyed by document bytes could be defeated by a key inherited from
269
552
  `Object.prototype`, and the affected code paths reported nothing.** The bundled code lists, the
270
553
  837's variant resolution, and the 837's HL parent-level map were built as plain object literals,
package/README.md CHANGED
@@ -34,7 +34,7 @@ const ix = parseX12(rawEdi); // never throws except on 4 structural fatals
34
34
  const tx = ix.groups[0]?.transactions.find((t) => t.st.elements[1] === "835");
35
35
  const remit = tx ? get835(ix.delimiters, tx) : undefined;
36
36
 
37
- remit?.payment.totalActualPayment.toString(); // "450.00": BigInt-exact, never a float
37
+ remit?.payment.totalActualPayment?.toString(); // "450.00": BigInt-exact, never a float
38
38
  remit?.claims[0]?.patientControlNumber; // "PT-ACCT-001": your account number, echoed back
39
39
  remit?.claims[0]?.serviceLines[0]?.adjustments[0]?.reasonCode; // "1": CARC (why it was adjusted)
40
40
  ```
@@ -45,7 +45,7 @@ That's the pitch: no schema upload, no spec knowledge. The parser accepts vendor
45
45
 
46
46
  - **Typed read + emit, per transaction**: 271, 277/277CA, 278, 820, 834, 835, 837P/I/D, plus 999/TA1 acknowledgments. Per-transaction helpers (`get835`, `get837Claims`, `get271Eligibility`, …) and a matching domain builder for each (`build835`, `build837P/I/D`, `build271`, …). The 270 and 276 inquiries parse as generic segments; they have no typed model yet.
47
47
  - **Postel's Law**: a lenient parser (deviations → warnings with a stable code + positional context) and a conservative serializer. `serializeX12` is byte-faithful **for the segments on the model** by default: each comes back verbatim, including element padding, composites, and `?`-release escapes. **`serialize(parse(s)) === s` is not guaranteed in general.** Six constructs are known not to survive: line breaks between segments (any run of CR / LF between segments is absorbed at parse, so a pretty-printed or double-spaced file emits compact), a doubled terminator outside a transaction, a missing final terminator (the emit supplies one), post-IEA `trailingBytes` (re-joined, not verbatim), a TA1 that followed a functional group (emitted right after the ISA, so it is **reordered**, though nothing is lost), and a segment whose first element is empty outside a transaction (skipped entirely, with no warning at all). A **segment outside a transaction is not on that list**: it is reported as `X12_UNEXPECTED_SEGMENT`, kept on the model at `ix.orphanSegments` with a structural `anchor`, and re-emitted at that anchor, so the segment, its value and its warning all survive the round trip - placement is by the anchor and never by `segmentIndex`, which indexes the input stream the emit does not follow. The last five break the round trip on inputs with no line breaks at all, so do not treat "no line breaks" as sufficient; five of the six are silent, so a clean warnings list does not mean byte-exact either. Measured across this repo's 56 fixtures: every emit is a fixed point and re-parses to an identical model with an identical warning stream, the 14 with no line breaks return byte-identical, and the other 42 differ by line breaks and nothing else. See [Line endings between segments](./docs-content/spec-notes-envelope.md). `{ specClean: true }` reconciles the envelope, and `{ specClean: true, recomputeCounts: true }` also emits the corrected counts (`recomputeCounts` does nothing on its own). A mismatch is always warned, never silently corrected. Only **4 structural failures** are ever fatal.
48
- - **Money is exact**: every monetary/percent/quantity field decodes as `X12Decimal` (string-backed, BigInt arithmetic). The library **never `parseFloat`s** an EDI amount.
48
+ - **Money is exact, and never invented**: every monetary/percent/quantity field decodes as `X12Decimal` (string-backed, BigInt arithmetic), and the library **never `parseFloat`s** an EDI amount. **No slot holds an amount this library did not decode.** Where a reader used to substitute `X12Decimal.ZERO` the slot is now `X12Decimal | undefined` and reads `undefined`, so "the sender stated zero" and "the sender stated nothing" are different readings; through `0.0.12` they were the same `0`. That is a rule about the substitution, not a census of the model: some rows are dropped whole instead, which is a different and pre-existing shape, and no total is published here. Read [KNOWN-LIMITATIONS.md](./KNOWN-LIMITATIONS.md).
49
49
  - **Safety-critical fidelity**: TRN reassociation traces, 835 balance invariants, 837 HL hierarchy integrity, 834 maintenance types, and 278 certification actions are preserved **verbatim** and never inferred; ambiguity yields a warning or a typed refusal, never a confident wrong answer.
50
50
  - **PHI-disciplined**: synthetic-only fixtures, a PHI commit-gate, and a warning `message` that is a **lookup into a frozen registry**, not something built from your document. No warning factory in the library takes a value parameter, so no element can reach a diagnostic: the code and `position` say what and where, and the bytes stay on the model. `ALL_WARNING_MESSAGES` is exported so you can assert it. **Builder refusals are a different, and deliberately weaker, surface:** twenty-four refusal sites across ten builder modules name a value you passed in, so you can see which control number, count or code was refused. **What they will never name is a `claimId`, a member id, a member name, a trace or a diagnosis code** - a guarantee that held for the refusal templates and, until the release after `0.0.10`, did not hold for the shared type guards underneath them: a JSON-driven caller who sent a number where the types say string got `a number ("900412345678")` back, bounded but not redacted, from a guard standing on every element of every builder. Those guards report the type now and never the value. Only the segment-join guard names the slot as well; the `esc` and `X12Decimal` guards name the builder, which is a diagnostic cost stated rather than hidden. The array guard's primitive arm is redacted with them, but it still reports the `length` and the class tag of a forged array-like, bounded, because those describe the shape you forged rather than the contents of a document element. Each goes through `renderCallerValue`, capping the rendered **fragment** at `BUILD_REFUSAL_VALUE_MAX_RENDERED` (90 characters); all three names are exported so you can assert the ceiling. The whole `message` is that plus the site's own fixed text, so it is bounded by a constant but a larger one: a 120,000-character control number gave a 120,066-character `X12BuildError.message` before this and gives 150 now. That is robustness, not redaction: the value is one you supplied and bounding it hides nothing from you. It is also not escaped, and on the **ack** path it is not always strictly your own, since `build999`'s AK2-02 and `buildTA1` echo an inbound document's control numbers by design. Log `err.code`, not `err.message`, from a builder. **`defineProfile()` is bounded on the same terms since `0.0.6`** (twelve refusal sites, twenty-three caller values; the worst message measured **360,181 characters** before and **431** now, both at the `fixture` refusal, which names three caller values; the ceiling for that site is 443 and the suite asserts every site under 500; `X12ProfileError.profileName` is deliberately left unbounded so it still matches the name you passed). **And a forged non-array in a builder spec now refuses instead of hanging:** every indexed loop takes its bound from a checked array, so `{ length: "9".repeat(120000) }` draws a typed refusal rather than coercing to `Infinity` and looping forever. A few `for…of` reads still throw an untyped `TypeError` instead; both are in [KNOWN-LIMITATIONS.md](./KNOWN-LIMITATIONS.md). **The one deliberate exception:** `X12ParseError.snippet` on the four Tier-3 structural fatals is a bounded (≤ 64 character) copy of the start of the input, so on real traffic it can carry PHI. The library does not redact it. Redact at your call site, or log `err.code` and `err.position` instead. See [Keeping PHI out of logs](./docs-content/troubleshooting.md).
51
51