@cosyte/x12 0.0.11 → 0.0.13

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,137 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ### Added
11
11
 
12
+ - **🩺 `X12_STATED_AMOUNT_DISCARDED`, the 31st Tier-2 warning code, plus the public factory
13
+ `statedAmountDiscarded(position)`** (`X12-STATED-AMOUNT-DISCARDED`). The entry below reports a row
14
+ whose amount this library could not read. This one is the opposite case: **the reader discarded
15
+ the row for a reason that is not about the amount at all**, so the amount was never the problem
16
+ and, on one of the two routes, was never even looked at. The bytes stay verbatim on
17
+ `tx.segments[…].raw`; decode them yourself before posting. Through `0.0.12` both routes were
18
+ silent on every channel.
19
+
20
+ **The two routes, enumerated.** An **820 `RMR`** under an open remittance loop whose `RMR-01` and
21
+ `RMR-02` are **both empty** while `RMR-04` or `RMR-05` is populated: `decodeRmr` refuses the open
22
+ item on identity **before** either amount element is read, so `RMR****150.00*150.00~` gave
23
+ `openItems: []` and `warnings: []`, taking a stated payment, a stated amount due and `RMR-03`'s
24
+ payment action code together. And an **837 `AMT`** arriving while a Loop 2430 line adjudication
25
+ (`SVD`) is open, whose `AMT-02` decoded: the v1 adjudication model carries no amount row, and
26
+ attaching one to this submission's own service line would put another payer's figure on it, so
27
+ `AMT*EAF*75.00~` (Remaining Patient Liability) was skipped in silence.
28
+
29
+ **🩺 That second route is where the channel read BACKWARDS, and squaring it is the point.** Under
30
+ an open `SVD`, `AMT*EAF~` raised `X12_AMOUNT_ROW_DROPPED` and `AMT*EAF*75.00~` raised nothing: the
31
+ report was present exactly where **less** was lost. Both report now, and which code arrives is
32
+ what says which loss it was.
33
+
34
+ **It carries no `position.elementIndex`.** On the `RMR` route the loss spans `RMR-04` and
35
+ `RMR-05` and no single element names it; on the `AMT` route the element is fixed by the segment.
36
+
37
+ **🩺 It is additive, and nothing moved onto it.** `X12_AMOUNT_ROW_DROPPED` and
38
+ `X12_UNPARSEABLE_DECIMAL` fire on exactly the documents they fired on before, pinned by committed
39
+ tests. **The two amount-row codes are disjoint and can never name the same segment**, because this
40
+ one requires an amount element the sender populated and the other requires one that decoded no
41
+ value, so the code you get is the discriminant. **Gate on both.** `KNOWN-LIMITATIONS.md`, the
42
+ money spec-note and the troubleshooting table were corrected with the code.
43
+
44
+ **Bounds, stated as properties of the READ.** What is reported is a segment that populated its
45
+ amount element and arrived **while the loop that would carry its row was open**. One reaching a
46
+ reader with no such loop open is a different loss and is **still silent**: the 834's `AMT` with no
47
+ `HD` open, the 820's `ADX` with no remittance open, and the 835's and the 837's `AMT` before any
48
+ claim or service line. A bare `RMR~` states nothing and is silent, as is an identity-less `RMR`
49
+ carrying only a payment action code. And this code says **nothing** about whether the amount would
50
+ have decoded: the `RMR` route refuses the row before attempting the decode, so it is raised on
51
+ `RMR****1,234.56~` exactly as on `RMR****150.00*150.00~`, with **no `X12_UNPARSEABLE_DECIMAL`
52
+ beside it in either case**, on this release and on every earlier one. **Do not read an
53
+ unaccompanied instance as evidence the bytes are postable.** Only the `AMT` route guarantees a
54
+ decodable amount, because there `AMT-02` decoded before the row was skipped.
55
+
56
+ - **🩺 `X12_AMOUNT_ROW_DROPPED`, the 30th Tier-2 warning code, plus the public factory
57
+ `amountRowDropped(position)`** (`X12-AMT-ADX-ABSENT-AMOUNT`). An `AMT` or `ADX` is not a slot on a
58
+ bigger record: each one **is** a record, carrying an amount plus the thing the amount is about. So
59
+ when the amount element (`AMT-02`, `ADX-01`) decodes no value there is no row to build, and
60
+ `AMT-01`'s qualifier or `ADX-02`'s adjustment reason code is dropped with it. Through `0.0.12`
61
+ that happened with **no diagnostic on any channel**: `AMT*B6~` gave `claim.amounts: []` and
62
+ `warnings: []`, which reads exactly like a document that never carried the segment. It is now
63
+ reported, at the `AMT` / `ADX` itself.
64
+
65
+ Raised by four surfaces: the 835's `AMT`, the 837's `AMT`, the 834's coverage `AMT` and the 820's
66
+ `ADX`. The 834's lands on that **member's** own `warnings`, the same per-member scoping the
67
+ decimal sink beside it already used, because a roster-level report would say a premium was lost
68
+ without saying whose. **On the 835 and the 837 the `AMT` attaches to the open SERVICE LINE first
69
+ and to the claim only when there is none, so the row lost may be a line-level one** - an unchanged
70
+ `claim.amounts` is not evidence the warning is stale.
71
+
72
+ **It carries no `position.elementIndex`**, deliberately. One of its two routes is an absent
73
+ element, and an absent element has no index to name; the segment fixes which element was being
74
+ read anyway.
75
+
76
+ **🩺 It is additive, and no case moved onto it.** An amount that is present and does not decode
77
+ still raises `X12_UNPARSEABLE_DECIMAL` at its own `elementIndex`, now **alongside** this code
78
+ rather than instead of it, so a gate you already wrote against that code fires on exactly the
79
+ documents it fired on before. Whether one accompanies this code at the same `position.segmentIndex`
80
+ is what separates the absent route from the unparseable one, since this code is raised for both
81
+ and discriminates neither. What a one-code gate never caught, on any release, is the
82
+ absent-amount row: **gate on both.** `KNOWN-LIMITATIONS.md`, the money spec-note and the
83
+ troubleshooting table were corrected with the code, and a committed test pins both halves - that
84
+ the one-code gate misses the absent-amount document, and that it still fires where it always did.
85
+
86
+ **Bounds, stated because the wider reading is the tempting one, and stated as properties of the
87
+ READ rather than of the walker's control flow.** What is reported is a row whose amount was read
88
+ and decoded no value. A segment discarded **before** its amount is read is not on this channel
89
+ (the 834's `AMT` with no `HD` open, the 820's `ADX` with no remittance open), and neither is one
90
+ whose amount decoded and then found nothing open to attach the row to. **Do not shorten that to
91
+ "nothing open means silent"** - the 835 and the 837 decode first, so an `AMT` with an absent
92
+ amount and no claim open does raise this code. An 820 `RMR` is not on this channel either, and
93
+ **not** because its row survives: `decodeRmr` drops on open-item identity (`RMR-01` and `RMR-02`
94
+ both empty) **before** `RMR-04` is read, so an `RMR` stating an open item and no amount keeps its
95
+ row with `amountPaid` left `undefined`, while one stating an amount and **no** open item is
96
+ dropped whole. That second case, and the 837's Loop 2430 `AMT` which an open `SVD` discards
97
+ outright, are separate losses: nothing failed to decode in either, so neither is this code's
98
+ shape, and `X12_STATED_AMOUNT_DISCARDED` above is what reports them.
99
+
100
+ - **🩺 `X12_835_BALANCE_NOT_EVALUABLE`, the 29th Tier-2 warning code, plus the public factory
101
+ `balanceNotEvaluable(position, invariant)`** (`X12-837-SV-UNDEFINED-DECIMAL`). Raised where a term
102
+ of one of the three TR3 005010X221A1 §1.10.2 balance equations is `undefined` on the model, so the
103
+ equation has nothing to compare on one side. `invariant` is the same library-owned
104
+ `BALANCE_INVARIANTS` discriminant `X12_835_REMIT_BALANCE_MISMATCH` takes, and the message names the
105
+ equation and carries no amount, like every other warning in this registry.
106
+
107
+ **It is a different code from the mismatch on purpose.** The mismatch asserts a computed inequality
108
+ between amounts the sender supplied; this one asserts only that the comparison could not be made.
109
+ Through `0.0.12` an undecoded term collapsed to `X12Decimal.ZERO` and the equation then reported a
110
+ mismatch between the payer's own amounts and a zero this library invented, which is the reading
111
+ that made an absent `CLP-03` indistinguishable from a claim submitted at zero.
112
+
113
+ **An EMPTY list of adjustments is NOT an absent term** and does not raise it: a claim carrying no
114
+ `CAS` really did state no adjustments, so its sum is `X12Decimal.ZERO`. Only a term this library
115
+ decoded no value from stops the equation. `build835` reaches the same verdict as
116
+ `X12_835_BUILD_INVALID_SPEC` rather than as its balance-mismatch code, and is unreachable there
117
+ from TypeScript because every balance term on `Build835Spec` is a required `X12Decimal`.
118
+
119
+ **🩺 GATE ON BOTH CODES.** A posting gate written against `X12_835_REMIT_BALANCE_MISMATCH` alone
120
+ **stops firing** on these documents when you upgrade, because through `0.0.12` the undecoded term
121
+ collapsed to zero and raised the mismatch. This library warns either way; your gate has to look
122
+ for both. The recipes in `docs-content/quickstart.md` and `docs-content/cookbook.md` and the
123
+ triage table in `docs-content/troubleshooting.md` were corrected with the code, and a committed
124
+ test pins that the one-code gate misses a document the two-code gate catches.
125
+
126
+ - **🩺 `X12_837_PAY_TO_ADDRESS_REPEATED`, the 28th Tier-2 warning code, plus the public factory
127
+ `payToAddressRepeated(position)`** (`X12-PAY-TO-FUSION`). Raised at the **second and each
128
+ subsequent `NM1*87` within one Loop 2000A**, where the TR3s allow Loop 2010AB at most once.
129
+ `position.segmentIndex` names the repeated `NM1*87` itself and carries no `elementIndex`: what is
130
+ reported is a second occurrence of the segment, not a defect in any element of it. The counter
131
+ resets at the Loop 2000A `HL`, beside the pay-to slot it guards, so a first `NM1*87` under a later
132
+ billing provider is a first and not a repeat.
133
+
134
+ The model has **one** pay-to address slot, so it cannot carry two, and this code is the only thing
135
+ that says the document named two. It reports that the **document** repeated the loop; it does not
136
+ report that anything was mis-read, and it says nothing about the subjects of the other 837 codes.
137
+ An `NM1*87` arriving while a `CLM` is open never reaches the pay-to route at all, so it neither
138
+ warns nor arms the warning. **Where it lands instead is not stated as one destination, because two
139
+ were measured:** with a Loop 2400 open it joins that line's `serviceLine.providers` (TR3 Loop
140
+ 2420), and with a claim but no line open it joins `claim.providers` (Loop 2310). Both are
141
+ pre-existing and identical at `0.0.12`.
142
+
12
143
  - **🩺 `X12_837_ENTITY_SEGMENT_DISCARDED_AFTER_LX`, the 27th Tier-2 warning code, plus the public
13
144
  factory `entitySegmentDiscardedAfterLx(position)`** (`X12-DISCARD-AFTER-STRAY-LX`). Raised for
14
145
  each 837 `N3` / `N4` / `PER` / `REF` that reached **no party at all** because an earlier `LX` with
@@ -139,6 +270,62 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
139
270
 
140
271
  ### Changed
141
272
 
273
+ - **🩺 BREAKING (read model): every monetary, percent and quantity slot the readers used to fill with
274
+ a fabricated `X12Decimal.ZERO` is now `X12Decimal | undefined`** (`X12-837-SV-UNDEFINED-DECIMAL`).
275
+ Through `0.0.12` an **absent** `SV1-02` - a monetary field on an 837 claim service line - read back
276
+ as `X12Decimal.ZERO`, so a consumer could not tell "the sender stated zero" from "the sender stated
277
+ nothing" and this library presented the second as the first. Fourteen model slots carried that
278
+ fabrication: `X12Claim.totalCharge` (CLM-02), a service line's `charge` and `units` (SV1-02 /
279
+ SV2-03 / SV3-02 and SV1-04 / SV2-05 / SV3-06), `X12LineAdjudication.amountPaid` (SVD-02),
280
+ `X12RemitClaim.totalChargeAmount` / `totalPaymentAmount` / `patientResponsibilityAmount` (CLP-03 /
281
+ 04 / 05), `X12RemitServiceLine.chargeAmount` / `paymentAmount` (SVC-02 / SVC-03),
282
+ `X12RemitPaymentHeader.totalActualPayment` and `X12PremiumPaymentHeader.totalPremiumAmount`
283
+ (BPR-02), `X12PremiumOpenItem.amountPaid` (RMR-04), `X12RemitAdjustment.amount` (a `CAS` triple,
284
+ read by both the 835 and the 837) and `X12RemitProviderAdjustment.amount` (a `PLB` pair).
285
+
286
+ **`undefined` means "this library decoded no value from that element", NOT "the element was
287
+ absent."** A present element holding bytes that do not decode lands there too, and
288
+ `X12_UNPARSEABLE_DECIMAL` at that `position.elementIndex` is the only thing that separates them.
289
+ Read it in exactly that direction. A **stated** zero is untouched: it still decodes, still reads
290
+ `0`, and keeps its lexical form, so `0.00` is still `0.00`.
291
+
292
+ **Migrating:** `x.toString()` becomes `x?.toString()` at every one of those slots, and each site is
293
+ a decision rather than a rewrite - an `undefined` amount is not zero, and a `?? 0` reinstates the
294
+ defect. If you posted cash off a slot that read `0` on `0.0.12` or earlier without also gating on
295
+ `.warnings`, re-read those files.
296
+
297
+ Two things this did **not** do. `elementDecimalOrZero` is a public export and is unchanged: it
298
+ still substitutes `X12Decimal.ZERO`, because that is its documented behaviour and a consumer
299
+ walking segments itself may still want it. What changed is that no reader in this library calls it
300
+ any more. And the 837's `X12_837_SERVICE_LINE_NOT_DECODED` is still the only thing that says WHY a
301
+ line's `charge` and `units` are empty: `undefined` alone does not separate "no `SVx` was decoded
302
+ onto this line" from "an `SVx` was decoded and carried no charge element".
303
+
304
+ - **🩺 BREAKING (emit side): `Build837ServiceLineSpec.units` is now required, and `build837P` /
305
+ `build837I` / `build837D` refuse a service line without it** (`X12-837-SV-UNDEFINED-DECIMAL`).
306
+ Through `0.0.12` an omitted `units` was emitted as the literal `0` into SV1-04 / SV2-05 / SV3-06,
307
+ so the builder stated a service unit count no caller supplied - the read-side fabrication above,
308
+ running the other way. Measured on `0.0.12`: a Professional line with no `units` emitted
309
+ `SV1*HC:99213*8500*UN*0*11**1~`. The refusal is `X12_837_BUILD_INVALID_SPEC`, naming the structural
310
+ locator (`billing[0].subscriber[0].claim[0]`) and no caller value.
311
+
312
+ **Refusing rather than emitting an empty element is the stance, and it is not new here:**
313
+ `build277` already refuses a service line without `SVC-07` on the same grounds. The parser is
314
+ liberal and the serializer conservative, and a count is not something a serializer may leave for
315
+ the receiver to guess at. A caller that supplies `units` is unaffected in every respect, including
316
+ one that supplies a zero, which is emitted because the caller did state it.
317
+
318
+ - **`build835` refuses a JS caller's `undefined` balance term as an invalid spec rather than throwing
319
+ an untyped `TypeError`** (`X12-837-SV-UNDEFINED-DECIMAL`). Unreachable from TypeScript: every term
320
+ of the three §1.10.2 invariants is a required `X12Decimal` on `Build835Spec`. Through `0.0.12` a JS
321
+ caller passing `undefined` reached `undefined.add` inside the balance guard. It is now
322
+ `X12_835_BUILD_INVALID_SPEC` with the registry text, and deliberately **not**
323
+ `X12_835_BUILD_BALANCE_MISMATCH`: nothing was measured out of balance, a required amount is simply
324
+ missing. Passing a raw `number` into a balance term still throws **an** untyped `TypeError` from
325
+ the same guard, so the documented dichotomy did not move; on some slots the message is now
326
+ `X12Decimal`'s own tampering text where it used to be `.add is not a function`, which the pinned
327
+ regex already admitted. Still disclosed in `KNOWN-LIMITATIONS.md`.
328
+
142
329
  - **`KNOWN-LIMITATIONS.md` now ships in the published tarball** (`X12-837-LOOP-RESIDUALS`). Two
143
330
  shipped warning messages, `X12_837_SERVICE_LINE_DROPPED` and
144
331
  `X12_837_ENTITY_SEGMENT_DISCARDED_AFTER_LX`, name that document ("see KNOWN-LIMITATIONS.md", each
@@ -556,6 +743,57 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
556
743
 
557
744
  ### Fixed
558
745
 
746
+ - **🩺 An `AMT` / `ADX` row whose amount decodes nothing is no longer dropped in silence**
747
+ (`X12-AMT-ADX-ABSENT-AMOUNT`). Reproduced on the base tree at `9db104b` across all four readers,
748
+ and the report it now raises is the `X12_AMOUNT_ROW_DROPPED` entry under **Added** above. The
749
+ filing that named this defect stated the bound precisely and it is repeated here because the wider
750
+ form is false: an **absent** amount element was silent on every channel, while a **present** one
751
+ holding undecodable bytes was already reported by `X12_UNPARSEABLE_DECIMAL` at its own
752
+ `elementIndex`. Both now also raise the new code; nothing moved off the old one.
753
+
754
+ - **A cookbook sentence said an undecoded 837 service line ships `charge` and `units` as `0`.** It
755
+ reads `undefined` on the same release that made the rest of that page true, so the page contradicted
756
+ the money spec-note it links to. Corrected in passing while sweeping that paragraph for the new
757
+ code. This is a documentation correction only; no behaviour changed with it.
758
+
759
+ - **🩺 A repeated `NM1*87` in one 837 Loop 2000A no longer fuses two pay-to addresses into one the
760
+ sender never sent** (`X12-PAY-TO-FUSION`). Reproduced at `0.0.12`: `payToAddress` is a bare
761
+ accumulator with no entity object to own it, cleared only at the next Loop 2000A `HL`. Every other
762
+ party gets a fresh object at its `NM1`, so a trailing `N3` / `N4` found `address === undefined` and
763
+ its write replaced; the two pay-to arms had nothing replaced under them and wrote onto whatever the
764
+ previous `NM1*87` left, `withLines` appending and `mergeAddress` falling back. Two `NM1*87`s each
765
+ with an `N3` and an `N4` read back **a street line from each of two addresses** plus a
766
+ `countryCode` taken off the **first** `N4`, on an address whose own `N4` names no country, with
767
+ `warnings: []`. Re-emitted through `build837P` that became a single Loop 2010AB stating a payment
768
+ destination no sender had stated.
769
+
770
+ **The fix is the missing object identity, not a clear.** Each `NM1*87` now opens a fresh
771
+ accumulator that the address arms read and write, so values from two occurrences can never meet.
772
+
773
+ **🛑 Two earlier remedies were refuted, both for one reason, and it is why the emit side was in
774
+ scope from the start: on this slot an emptied value is not a neutral absence, because the emit side
775
+ reads it.** `build837P/I/D` gates Loop 2010AB on `payToAddress !== undefined` and `emitAddress`
776
+ writes `N3` only for non-empty `lines` and `N4` only for a defined field. Clearing the accumulator
777
+ at the `NM1*87` therefore erased an address a repeat carrying no `N3` / `N4` did state, re-emitting
778
+ as **no pay-to loop at all**; a flag consumed by the first write after it did the same on a
779
+ valueless `N3` or `N4`, re-emitting as a **bare `NM1*87`**. Both are positive statements about
780
+ where a payment goes that no sender made. So the rule is stated in the emit side's own terms: the
781
+ current occurrence takes the slot **only when it states an address the emit would write at least
782
+ one segment for**, and an occurrence that states none leaves a stated one alone. The predicate is a
783
+ single shared module (`src/transactions/claim/address-segments.ts`) that `emitAddress` also asks,
784
+ so the reader and the writer cannot drift apart. This is the discipline the 835 already uses, where
785
+ the emit guard reuses the read side's own balance validators.
786
+
787
+ **A document with at most one `NM1*87` per Loop 2000A is byte-for-byte unaffected**, warning
788
+ channel included, and that is pinned both ways.
789
+
790
+ **🩺 The cost, stated rather than argued away: a repeat that states only part of an address now
791
+ re-emits only that part.** A second `NM1*87` followed by an `N4` and no `N3` reads back that `N4`
792
+ with `lines: []` and re-emits a Loop 2010AB with no `N3`. Keeping the earlier occurrence's street
793
+ lines there **is** the fusion: one sender's street under another sender's city. The loop is still
794
+ present, the warning is on the channel, and what is gone is the fabricated street.
795
+ `KNOWN-LIMITATIONS.md` records it.
796
+
559
797
  - **🩺 `build278Request` / `build278Response` refuse a review whose HL-03 level code is outside `EV`
560
798
  and `SS`, instead of emitting a review its own reader cannot decode**
561
799
  (`REFUSAL-MESSAGE-PHI-ECHO`). `Build278ReviewSpec.levelCode` is the **one** caller-supplied HL-03 in
@@ -1166,6 +1404,29 @@ string, …` - which `esc` cannot, being unary.
1166
1404
  slot for a silent normalization; the tiers now name it, and note that a Tier-2 unexpected segment is
1167
1405
  warned about but not kept.
1168
1406
 
1407
+ - **The universal about a stray 837 `LX` is cut back in the `src/` comments and 837 test-file headers
1408
+ the release before this one disclosed it in** (`X12-837-LOOP-RESIDUALS`). That release deleted it
1409
+ from the documents this package publishes and disclosed, rather than claimed closed, that it still
1410
+ stood in those two places; this is the rest of that sweep. **No count and no completeness claim is
1411
+ published**, because both are what went stale twice here: the sites swept are the `NM1` and `LX`
1412
+ case comments in `src/transactions/claim/get-837.ts`, and the headers, section comments and case
1413
+ titles of `test/transactions-claim-837-loop-residuals.test.ts`,
1414
+ `test/transactions-claim-837-discard-after-stray-lx.test.ts` and
1415
+ `test/transactions-claim-837-variant-lookup.test.ts`. Each said a trailing `N3` / `N4` / `PER` /
1416
+ `REF` "attached to" or "landed on" whichever party the last `NM1` left active, or that a party
1417
+ named after the `LX` "is addressable again", all of which read as all four kinds reaching every
1418
+ party. They do not: this reader does not surface every one of those kinds on every party. Each copy
1419
+ takes the qualifier already graded on the shipped surfaces, "wherever this reader surfaces that
1420
+ segment kind on that party at all", or is cut back to the measured instance beside it and that
1421
+ instance is named - a payer in every case but one, where it is a Loop 2320 other subscriber; no
1422
+ copy is given a new wording and **no per-kind, per-party map is published.** Counterfactual headings ("no longer attaches", "no longer leaves the last `NM1`
1423
+ addressable") lose the counterfactual only. The bullet in `documentation/agent-notes.md` beginning
1424
+ "RESIDUAL 1, MEASURED AT `93b2428`", and the identically-scoped paragraph in the test header it was
1425
+ written from, are **not** copies - both are scoped by "every trailing segment that attaches to a
1426
+ named party" - and are deliberately left alone. Nothing a consumer reads at runtime changed:
1427
+ neither the warning registry nor any `docs-content/` page nor the README carried the wording, and
1428
+ the executable behaviour of the swept comments is nil.
1429
+
1169
1430
  ### Documented, not fixed
1170
1431
 
1171
1432
  > **Superseded in part by the "Fixed" entry above, which ships in the same release.** The model half
@@ -1174,6 +1435,20 @@ string, …` - which `esc` cannot, being unary.
1174
1435
  > not re-emit an orphan, so the warning still does not survive. The two entries are kept separate
1175
1436
  > because they were separate changes, and the sentences below describe the release before this one.
1176
1437
 
1438
+ - **🩺 A repeated `NM1*87` inside one 837 Loop 2000A fuses two pay-to addresses into one**
1439
+ (`X12-837-LOOP-RESIDUALS`). The `N3` line collector appends and the `N4` merge falls back, so
1440
+ `X12Claim.payToAddress` carries a street from each of two addresses and an `N4` field the second
1441
+ address omitted. Reproduces unchanged at `0.0.11` and is **not** fixed here: two remedies were
1442
+ built and both were refuted for moving the loss rather than removing it, so per ADR 0016 the unit
1443
+ was cut back rather than given a third. Clearing the accumulator at the `NM1*87` erases the
1444
+ address a repeat with no `N3` / `N4` of its own did state; replacing at the first following write
1445
+ erases it on a VALUELESS `N3~` / `N4~`. **Neither erasure is a neutral absence, and that is the
1446
+ constraint the next attempt starts from:** `build837P/I/D` emits Loop 2010AB only where the slot
1447
+ is defined, and `emitAddress` emits `N3` only for a non-empty `lines` and `N4` only for a defined
1448
+ field, so an emptied slot re-emits as **no pay-to loop at all** and a half-emptied one re-emits a
1449
+ bare `NM1*87` with neither. **The emit side is therefore in scope for that fix from the start**,
1450
+ which is what makes it its own unit. `documentation/agent-notes.md` carries the measurements.
1451
+
1177
1452
  - **A segment that falls outside a transaction is dropped from the model, and its warning does not
1178
1453
  survive a round trip.** The envelope walker keeps body segments only while an ST..SE transaction is
1179
1454
  open; anything else raises `X12_UNEXPECTED_SEGMENT` and is then discarded. A **blank** line between
@@ -56,28 +56,34 @@ model.
56
56
  on the emit side. All of it is pre-existing and reproduces at `e3cdf49`; widening the guard would
57
57
  have turned this into that audit, which is its own item.
58
58
 
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.
59
+ - **🩺 An unparseable decimal reads `undefined`, and it warns.** A decimal element that is present
60
+ and that this library cannot decode as a decimal (`1,234.56`, `$450.00`, `N/A`) yields no value,
61
+ and the reader has to put something in its place. **Through `0.0.12` a slot typed `X12Decimal` got
62
+ `X12Decimal.ZERO`**, so a consumer that read only the model saw a `0` where the payer sent
63
+ something unreadable; **as of `0.0.13` no reader substitutes `X12Decimal.ZERO` for a value it did
64
+ not decode** - every slot that used to get one is `X12Decimal | undefined` and reads `undefined`,
65
+ which is a breaking type change on the read model and is the point of it. **Read that as a rule
66
+ about the substitution, not as a census of the model:** an optional slot already read `undefined`,
67
+ and some rows are dropped whole, which is a different shape with a report of its own
68
+ (`X12_AMOUNT_ROW_DROPPED`, the next entry). **Every one of those
69
+ outcomes emits `X12_UNPARSEABLE_DECIMAL` at the failing `position.elementIndex`, because the
70
+ warning is a property of the READ rather than of what the reader then does with it.** No list of
71
+ the outcomes is published here, on purpose: a first draft enumerated three and a review measured a
72
+ fourth, and the rule is what holds, not the census.
72
73
 
73
74
  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
75
+ - **An ABSENT element does not raise this code.** It reads `undefined` and
76
+ `X12_UNPARSEABLE_DECIMAL` is not emitted for it, so `undefined` alone does not tell you which of
77
+ the two happened - the warning at that `elementIndex` does, and `readElementDecimal` gives you
78
+ the same distinction in band. Read that as a statement about **this code**, not as "an absent
79
+ element is silent": where the absent element takes a whole `AMT` / `ADX` row off the model, the
80
+ drop is reported by `X12_AMOUNT_ROW_DROPPED` at that segment (the next entry), which is
81
+ raised for both routes and therefore separates neither.
82
+ - **🩺 An unwarned value is not thereby trustworthy in general, and this is the one inversion to
77
83
  refuse.** The warning is a property of a decimal READ, not a property of a model slot. A slot
78
84
  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
85
+ with. **What this guarantees is narrower and exact: an unwarned value AT AN ELEMENT A READER
86
+ DECODED is what the sender sent.** The known slot of the other kind, an 837 service
81
87
  line whose `SVx` never decoded, is covered by a warning of its own (next entry) rather than by
82
88
  this one. **No census of never-read slots is published**, on purpose: the rule is what holds.
83
89
  - **The public helpers are silent without a sink.** `elementDecimal` / `elementDecimalOrZero` take
@@ -91,23 +97,120 @@ model.
91
97
  comments. It says nothing about a decimal decoded some other way, and no exhaustive census of
92
98
  such routes is published here, on purpose.
93
99
 
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
100
+ - **🩺 An `AMT` or `ADX` whose amount does not decode loses its WHOLE row, and says so as of
101
+ `0.0.13`.** These segments carry an amount plus the thing the amount is about - AMT-01's qualifier,
102
+ ADX-02's adjustment reason code, ADX-03 / ADX-04's reference. The readers build no row at all when
103
+ the amount (AMT-02, ADX-01) decodes no value, so what the sender did state goes with it: through
104
+ `0.0.12` `AMT*B6~` gave `claim.amounts: []` and `warnings: []`, which reads exactly like a document
105
+ that never carried the segment. **`X12_AMOUNT_ROW_DROPPED` now reports it**, anchored at the
106
+ `AMT` / `ADX` and carrying **no `elementIndex`**, because one of its two routes is an absent
107
+ element and an absent element has no index to name. Four surfaces raise it: the 835's `AMT`, the
108
+ 837's `AMT`, the 834's coverage `AMT` (on that **member's** own `warnings`, exactly as the decimal
109
+ sink beside it is scoped) and the 820's `ADX`. On the 835 and the 837 the `AMT` attaches to the
110
+ open **service line** first and to the claim only when there is none, so **the row lost may be a
111
+ line-level one** - do not read an unchanged `claim.amounts` as evidence the warning is stale. Four
112
+ bounds worth stating:
113
+ - **It is additive, and no case moved onto it.** A present-but-undecodable amount still raises
114
+ `X12_UNPARSEABLE_DECIMAL` at its own `elementIndex`, now alongside this code rather than instead
115
+ of it, so a predicate written against that code fires on exactly the documents it fired on
116
+ before. Whether one accompanies this code at the same `segmentIndex` is what tells the absent
117
+ route from the unparseable one, since this code is raised for both and discriminates neither.
118
+ - **It reports a row whose AMOUNT was read and decoded no value, and nothing wider.** State it as
119
+ a property of the READ, not of the walker's control flow. A segment discarded before its amount
120
+ is read is not on this channel (the 834's `AMT` with no `HD` open, the 820's `ADX` with no
121
+ remittance open), and neither is one whose amount decoded and then found nothing to attach to
122
+ (the 835's and the 837's `AMT` before any claim). **Do not write it as "nothing open means
123
+ silent":** the 835 and the 837 decode first, so an `AMT` with an absent amount and no claim open
124
+ does raise this code.
125
+ - **🔴 An 820 `RMR` is not on this channel, and the reason is NOT that its row survives.**
126
+ `decodeRmr` drops on open-item identity (RMR-01 and RMR-02 both empty), **before** RMR-04 is
127
+ read. So an `RMR` that states an open item and no amount keeps its row with `amountPaid` left
128
+ `undefined` and there is nothing to report - but an `RMR` that states an **amount and no open
129
+ item** is dropped whole, taking a stated payment, its payment-action code and its amount due
130
+ with it. Nothing failed to decode there, so it is not this code's shape;
131
+ `X12_STATED_AMOUNT_DISCARDED`, below, is what reports it.
132
+ - **A Loop 2430 `AMT` under an open `SVD` is discarded outright, and this code is not what says
133
+ so.** With a claim and a line open, the 837's adjudication skip drops an `AMT` that decoded
134
+ perfectly well. Through `0.0.12` that was silent while one that decoded nothing raised this
135
+ code, so the report was present exactly where less was lost;
136
+ `X12_STATED_AMOUNT_DISCARDED`, below, closed that.
137
+
138
+ - **🩺 A row the sender DID state can still be discarded, and says so as of `0.0.13`.** The code
139
+ above is about an amount this library could not read. This is the opposite case: **the reader
140
+ discarded the row for a reason that is not about the amount at all**, so the amount was never the
141
+ problem and, on one of the two routes, was never even looked at. The bytes stay verbatim on
142
+ `tx.segments[…].raw`; decode them yourself. Through `0.0.12` both routes were silent on every
143
+ channel. **`X12_STATED_AMOUNT_DISCARDED` reports them**, anchored at
144
+ the segment and carrying **no `elementIndex`**. The two routes, enumerated:
145
+ - An **820 `RMR`** under an open remittance loop whose RMR-01 and RMR-02 are **both empty** while
146
+ RMR-04 or RMR-05 is populated. `decodeRmr` refuses the open item on identity before either
147
+ amount element is read, so a stated payment, a stated amount due and RMR-03's payment action
148
+ code leave the model together. A bare `RMR~` states nothing and stays silent.
149
+ - An **837 `AMT`** arriving while a Loop 2430 line adjudication (`SVD`) is open, whose AMT-02
150
+ decoded. The v1 adjudication model carries no amount row, and attaching one to this
151
+ submission's own service line would put another payer's figure on it, so the row is skipped.
152
+ `AMT*EAF` (Remaining Patient Liability) on a Loop 2430 is exactly the shape being lost.
153
+
154
+ Three bounds worth stating:
155
+ - **It is additive, and nothing moved onto it.** `X12_AMOUNT_ROW_DROPPED` and
156
+ `X12_UNPARSEABLE_DECIMAL` fire on exactly the documents they fired on before, pinned by
157
+ committed tests.
158
+ - **The two amount-row codes are disjoint and can never name the same segment**, because this one
159
+ requires an amount element the sender populated and the other requires one that decoded no
160
+ value. The code you get is the discriminant.
161
+ - **It reports a segment that arrived while the loop that would carry its row was open.** An
162
+ `AMT` or `ADX` reaching a reader with **no such loop open** is a different loss and is **still
163
+ silent**: the 834's `AMT` with no `HD` open, the 820's `ADX` with no remittance open, and the
164
+ 835's and the 837's `AMT` before any claim or service line is open. **🔴 Read "still silent"
165
+ literally and no wider: at those last two sites the inversion above SURVIVES.** With no claim
166
+ open an 835 `AMT*B6~` raises `X12_AMOUNT_ROW_DROPPED` while `AMT*B6*500.00~` raises nothing, so
167
+ the channel still reports the smaller loss there. `PRE-EXISTING` and unchanged by this release. And on the `RMR` route this
168
+ code says nothing about whether the amount would have decoded, because the row is refused before
169
+ the decode is attempted, so **no `X12_UNPARSEABLE_DECIMAL` accompanies it even where the bytes
170
+ are unreadable.** It is raised on `RMR****1,234.56~` exactly as on `RMR****150.00*150.00~`, so
171
+ **never read an unaccompanied instance as evidence the bytes are postable**; only the `AMT`
172
+ route guarantees a decodable amount, because there AMT-02 decoded before the row was skipped.
173
+
174
+ - **🩺 An 835 balance invariant with an undecoded term is reported as UNEVALUABLE, not as a
175
+ mismatch.** The three TR3 005010X221A1 §1.10.2 equations (line, claim, top-of-remit) read amounts
176
+ that are all `X12Decimal | undefined` as of `0.0.13`. Where any term of an equation is
177
+ `undefined`, the equation is not run and `X12_835_BALANCE_NOT_EVALUABLE` is emitted instead of
178
+ `X12_835_REMIT_BALANCE_MISMATCH`, because nothing was measured out of balance: substituting `0`
179
+ for the missing term would be this library asserting a total nobody sent. **Through `0.0.12` that
180
+ substitution is exactly what happened**, so an 835 with an absent `CLP-03` reported a mismatch
181
+ between the payer's own amounts and an invented zero. Two bounds worth stating:
182
+ - **An EMPTY list is not an absent term.** A claim carrying no `CAS` really did state no
183
+ adjustments, and that sums to `X12Decimal.ZERO`. Only a term this library decoded no value from
184
+ stops the equation.
185
+ - **`build835` cannot reach it from TypeScript.** Every balance term on `Build835Spec` is a
186
+ required `X12Decimal`. A JS caller passing `undefined` gets `X12_835_BUILD_INVALID_SPEC` (an
187
+ untyped `TypeError` through `0.0.12`), deliberately NOT the build-side balance-mismatch code.
188
+
189
+ - **The public `elementDecimalOrZero` helper still substitutes `X12Decimal.ZERO`, and no reader in
190
+ this library calls it any more.** Its documented behaviour is unchanged and it stays exported, so
191
+ a consumer walking segments itself can still opt into the substitution - knowingly, rather than by
192
+ inheriting a convention the `get*` readers no longer follow. `elementDecimal` and
193
+ `readElementDecimal` are the honest routes.
194
+
195
+ - **🩺 An 837 service line whose `SVx` never decoded ships with `charge` and `units` `undefined`,
196
+ and it warns.** `get837Claims` resolves ONE variant for the submission, from the
96
197
  caller's `type` option, else ST-03's implementation-convention reference, else the first `SVx`
97
198
  segment present. A Loop 2400 line is then decoded only by the `SV1` / `SV2` / `SV3` that matches
98
199
  that variant. When none arrives, because the line carries an `SVx` for a different variant (an
99
200
  ST-03 of `005010X222A2` on a file whose lines are `SV2`, or a `{ type: "P" }` over the same) or
100
201
  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,
202
+ `charge` and `units` hold the accumulator's seeded `undefined`, and its procedure code,
102
203
  modifiers, unit of measure and place of service are equally undecoded. **That line now emits
103
204
  `X12_837_SERVICE_LINE_NOT_DECODED`, anchored at the `LX` that opened it.** Read the values off
104
205
  `tx.segments[…].raw` and decide which of the two disagreeing signals the sender meant.
105
206
  - **Ignoring the foreign `SVx` is deliberate and is not the limitation.** `SV1-02` and `SV2-03`
106
207
  are both the line charge, so decoding an `SV2` into a Professional line would mis-read money.
107
208
  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.
209
+ - **`undefined` does not say WHY, which is what the warning is for.** `charge` and `units` are
210
+ `X12Decimal | undefined` as of `0.0.13` (`X12Decimal` reading a fabricated `0` through
211
+ `0.0.12`), so a consumer that never reads `.warnings` at least sees that nothing was decoded -
212
+ but it cannot tell this case, where no `SVx` was read at all, from an `SVx` that WAS read and
213
+ carried no charge element. Only `X12_837_SERVICE_LINE_NOT_DECODED` separates them.
111
214
  - **The line is still retained, and so are the bytes.** Nothing is dropped: the service line is
112
215
  on the claim, and every segment, decoded or not, stays verbatim on `tx.segments`.
113
216
  - **Neither cause is attributed to the sender.** A `type` option that disagrees with a perfectly
@@ -214,14 +317,15 @@ model.
214
317
  - **The segment is NOT decoded into any line.** `SV1-02` and `SV2-03` are both the line charge, so
215
318
  reading a service segment into a line the walker never opened mis-reads money. Refusing to read
216
319
  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.
320
+ above:** an absent `SV1-02` on a line that DID open reads `undefined` as of `0.0.13`, a
321
+ confident `0` through `0.0.12`, and this code has nothing to do with either.
219
322
  - **🩺 IT SAYS NOTHING ABOUT THE VARIANT, AND A FIRST DRAFT OF THIS BOUND CLAIMED IT DID.** Variant
220
323
  resolution runs before the walk. A caller-supplied `type` option wins first; absent one, and
221
324
  where `ST-03` names none of the three known implementation conventions, it **falls back to the
222
325
  first `SVx` segment id anywhere in the transaction body - orphans included**. So a stray `SV2`
223
326
  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
327
+ conformant `SV1` line in it then reads `charge` `undefined`, `units` `undefined` (both a
328
+ fabricated `0` through `0.0.12`) and an `undefined` procedure
225
329
  code - `undefined`, not `""`, which on such a line is the `revenueCode`. Passing
226
330
  `{ type: "P" }` reads the same document correctly. Measured, both trees: `PRE-EXISTING`,
227
331
  identical at `0.0.10`, **not** introduced or changed by this code, and **not** narrowed here - excluding
@@ -265,6 +369,58 @@ model.
265
369
  following a **stray** `LX` belongs to is not derivable from the TR3s in either direction, so the
266
370
  reader refuses to attribute it rather than guessing.
267
371
 
372
+ - **🩺 An 837 Loop 2000A that names the pay-to address more than once has only ONE slot on the model
373
+ to put it in, and the model carries one of the addresses rather than both.** The TR3s allow Loop
374
+ 2010AB at most once per Loop 2000A, so a repeated `NM1*87` is a non-conformant document. Each one
375
+ raises `X12_837_PAY_TO_ADDRESS_REPEATED`, anchored at the repeated `NM1*87`, so the choice below is
376
+ never made in silence.
377
+
378
+ **🩺 Through `0.0.12` the two addresses were FUSED, and that is what changed.** `payToAddress` is a
379
+ bare accumulator with no entity object to own it, so unlike every other party there was nothing to
380
+ be replaced at the repeat: `withLines` appended and `mergeAddress` fell back, and two `NM1*87`s
381
+ each carrying an `N3` and an `N4` read back a street line from **each** address plus a
382
+ `countryCode` off the **first** `N4`, on an address whose own `N4` names no country, with
383
+ `warnings: []`. Re-emitted through `build837P` that was one Loop 2010AB naming a payment
384
+ destination no sender stated. **If you read a repeated `NM1*87` on `0.0.12` or earlier, treat that
385
+ claim's `payToAddress` as unreliable rather than as either sender's address.**
386
+
387
+ **The rule now applied, and it is stated in the emit side's terms because the emit side reads this
388
+ slot:** occurrences are never merged; the **last occurrence that states an address of its own**
389
+ takes the slot; and an occurrence that states none - one with no `N3` or `N4` at all, or only a
390
+ valueless `N3` / `N4` whose elements are empty - **does not blank one that did**. "States an
391
+ address" means exactly what `emitAddress` would write a segment for, and reader and writer share
392
+ one predicate so they cannot disagree. That last clause is not a nicety: `build837P/I/D` gates Loop
393
+ 2010AB on `payToAddress !== undefined`, so a slot cleared or blanked by a repeat that stated
394
+ nothing would re-emit as **no pay-to loop**, or as a **bare `NM1*87`** carrying neither `N3` nor
395
+ `N4`. Both say something about where a payment goes that the sender did not.
396
+
397
+ **🩺 The cost, which is real and is not argued away: a repeat that states only PART of an address
398
+ puts only that part on the model, and re-emits only that part.** A second `NM1*87` followed by an
399
+ `N4` and no `N3` reads back that `N4` with `lines: []` and re-emits a Loop 2010AB with no `N3`,
400
+ where `0.0.12` emitted a complete-looking address assembled from two. Keeping the earlier street
401
+ lines is the fusion itself, so they are not kept. **Which occurrence the sender meant is not
402
+ derivable from the TR3s**, and the losing occurrence's address is **not on the model in any form** -
403
+ `tx.segments` is where its bytes are, and remains the only complete account of the document.
404
+
405
+ **Two bounds, both measured.** This is scoped to the pay-to route, which lives in Loop 2000A: an
406
+ `NM1*87` arriving while a `CLM` is open never reaches that route, and is neither resolved nor
407
+ warned by this code. **Where it lands instead depends on whether a service line is open, and this
408
+ document does not state a single destination for it**, because two were measured: with a Loop 2400
409
+ open it joins that line's `serviceLine.providers` (a line-level provider is TR3 Loop 2420), and
410
+ with a claim but no line open it joins `claim.providers` (Loop 2310). Both are pre-existing and
411
+ identical at `0.0.12`. And a document with **at most one** `NM1*87` per Loop 2000A is unaffected in
412
+ every respect, warning channel included - including the pre-existing case of a lone `NM1*87` with a
413
+ valueless `N3`, which still reads an address with no elements and still re-emits a bare `NM1*87`.
414
+
415
+ **🩺 One thing the cost above shares with the shape that refuted remedy 2, stated so the two are
416
+ not read as different in kind.** In Loop 2010AB the TR3s make `NM1`, `N3` **and** `N4` all Required,
417
+ so a re-emitted loop carrying only the winning occurrence's `N4` is short a Required `N3`, exactly
418
+ as a bare `NM1*87` is short both. The difference is not conformance, it is provenance: every
419
+ element that IS emitted was stated by the occurrence the model kept, where the fused address
420
+ contained elements no single occurrence stated. Emitting a Loop 2010AB short a Required segment is
421
+ a pre-existing property of `emitAddress`, reachable at `0.0.12` from a lone `NM1*87` with an
422
+ `N4` and no `N3`, and is not introduced here.
423
+
268
424
  - **🩺 Through `0.0.9`, a lookup keyed by document bytes could be defeated by a key inherited from
269
425
  `Object.prototype`, and the affected code paths reported nothing.** The bundled code lists, the
270
426
  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