@cosyte/x12 0.0.10 → 0.0.11
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 +420 -13
- package/KNOWN-LIMITATIONS.md +804 -0
- package/README.md +7 -5
- package/dist/index.cjs +88 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +91 -10
- package/dist/index.d.ts +91 -10
- package/dist/index.mjs +87 -12
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,804 @@
|
|
|
1
|
+
# Known limitations & non-goals
|
|
2
|
+
|
|
3
|
+
`@cosyte/x12` is built to be **correct and honest about its edges** rather than to claim more than it
|
|
4
|
+
delivers. Misreading a payer's remittance, a claim's diagnosis, or a member's coverage can cause real
|
|
5
|
+
financial or clinical harm, so this is the deliberate "do not over-trust" list. Everything here is a
|
|
6
|
+
documented, intentional boundary, not a bug. The lenient parser never silently drops or garbles a
|
|
7
|
+
**decoded value**: where a limitation applies, the raw value is preserved (often with a warning), it
|
|
8
|
+
is simply not further decoded. Three things it does discard are worth reading before you rely on a
|
|
9
|
+
round trip, and all three are **silent**: **line breaks between segments**, **a doubled segment
|
|
10
|
+
terminator** outside a transaction, and **a segment whose first element is empty** outside a
|
|
11
|
+
transaction. All three are in the first two entries below. A **segment outside a transaction** used
|
|
12
|
+
to belong on that list and no longer does: it is warned, kept on the model at `ix.orphanSegments`,
|
|
13
|
+
**and re-emitted at the structural anchor the walker recorded**, so it survives a round trip. The
|
|
14
|
+
empty-first-element case is now the sharpest of the three: no warning, and no copy anywhere on the
|
|
15
|
+
model.
|
|
16
|
+
|
|
17
|
+
## Data / decode boundaries
|
|
18
|
+
|
|
19
|
+
- **🩺 BREAKING for `build277` callers: a 277 service line now REQUIRES `unitsOfService`, because
|
|
20
|
+
SVC-07 is a required element in `005010X212` and this library was not emitting it at all**
|
|
21
|
+
(`X12-277-SVC07-NOT-DECODED`). Through the release before this one `get277Status` read SVC-01
|
|
22
|
+
through SVC-04 and stopped, and `build277` emitted exactly those four, so **every X212 277 this
|
|
23
|
+
library produced with a Loop 2220 service line was short a required element** and every 277 it read
|
|
24
|
+
silently discarded the submitted units. Both sides are fixed: `X12ServiceLineStatus.unitsOfService`
|
|
25
|
+
and `Build277ServiceLineSpec.unitsOfService` carry SVC-07 (X12 element 380, Quantity).
|
|
26
|
+
|
|
27
|
+
**The two TR3s disagree about the usage and so do the two builders, on purpose.** In `005010X212`
|
|
28
|
+
the element is usage **R**, so `build277` now throws `ClaimStatus277BuildError` with code
|
|
29
|
+
`X12_277_BUILD_INVALID_SPEC` for a service line that omits it. In `005010X214` it is usage **S**,
|
|
30
|
+
so `build277CA` accepts the identical spec and simply omits the element. Usage read from the pyx12
|
|
31
|
+
005010 maps (`277.5010.X212.xml`, `277.5010.X214.xml`), which are outside this repository; the two
|
|
32
|
+
TR3s also name it differently, "Units of Service Count" in X212 and "Original Units of Service
|
|
33
|
+
Count" in X214, and one model field carries both.
|
|
34
|
+
|
|
35
|
+
**The count is never defaulted, in either direction.** A quantity the caller did not supply is a
|
|
36
|
+
quantity nobody sent, and a units figure is one a payer reprices against; supplying a `1` would be
|
|
37
|
+
inventing data on the wire. If you are emitting X212 277s today, this is a compile-time-invisible,
|
|
38
|
+
run-time refusal on the first build after upgrading, and that is the intended shape.
|
|
39
|
+
|
|
40
|
+
**SVC-05 is deliberately still unread on the 277, and that is not the same gap.** It is usage **N**
|
|
41
|
+
in both 277 TR3s. On the **835** SVC-05 is the Units of Service **Paid** Count and is read; reading
|
|
42
|
+
it on a 277 "for symmetry" would put a quantity on the model that no 277 sender ever wrote. Same
|
|
43
|
+
element number, different TR3.
|
|
44
|
+
|
|
45
|
+
**🩺 Read the scope literally: ONE element's usage was fixed, and an emitted service line is NOT
|
|
46
|
+
thereby conformant.** This was not a 277 usage audit, and **no census of what remains is published
|
|
47
|
+
here**, on purpose: other required elements of the same `SVC` are still unguarded, and finding
|
|
48
|
+
another is expected rather than a new defect. To name two, `SVC-01` (the composite procedure
|
|
49
|
+
identifier) and `SVC-02` (Line Item Charge Amount) are both usage **R** in X212 and both optional
|
|
50
|
+
on `Build277ServiceLineSpec`, so a spec supplying only `unitsOfService` still emits
|
|
51
|
+
`SVC*******1~` with no refusal. **The missing guard is what is pre-existing, not that byte
|
|
52
|
+
string** - at base the same spec emitted a bare `SVC~`, because the SVC-07 slot did not exist. `SVC-03` (Line Item Payment
|
|
53
|
+
Amount) is usage **R** in X212 and usage **N** in X214 and is optional in both builders. And the
|
|
54
|
+
read side is unchanged and still lenient: an X212 277 arriving with no SVC-07 raises **no
|
|
55
|
+
warning**, because that would need a new Tier-2 registry code and the defect this slice closes is
|
|
56
|
+
on the emit side. All of it is pre-existing and reproduces at `e3cdf49`; widening the guard would
|
|
57
|
+
have turned this into that audit, which is its own item.
|
|
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.
|
|
72
|
+
|
|
73
|
+
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
|
|
77
|
+
refuse.** The warning is a property of a decimal READ, not a property of a model slot. A slot
|
|
78
|
+
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
|
|
81
|
+
line whose `SVx` never decoded, is covered by a warning of its own (next entry) rather than by
|
|
82
|
+
this one. **No census of never-read slots is published**, on purpose: the rule is what holds.
|
|
83
|
+
- **The public helpers are silent without a sink.** `elementDecimal` / `elementDecimalOrZero` take
|
|
84
|
+
an optional 4th `X12DecimalWarningSink`. Every reader inside this library passes one, and a
|
|
85
|
+
source scan in `test/parser-decimal-silent-defaults.test.ts` keeps it that way. **A consumer
|
|
86
|
+
calling them directly and omitting it gets the old silent behaviour**, which is deliberate: it
|
|
87
|
+
keeps the existing signature working. `readElementDecimal` is the sink-free way to get the
|
|
88
|
+
distinction in-band.
|
|
89
|
+
- **The scan is a syntactic tripwire, not a proof.** It counts the arguments of every
|
|
90
|
+
`elementDecimal` / `elementDecimalOrZero` call under `src/transactions/` after stripping
|
|
91
|
+
comments. It says nothing about a decimal decoded some other way, and no exhaustive census of
|
|
92
|
+
such routes is published here, on purpose.
|
|
93
|
+
|
|
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
|
|
96
|
+
caller's `type` option, else ST-03's implementation-convention reference, else the first `SVx`
|
|
97
|
+
segment present. A Loop 2400 line is then decoded only by the `SV1` / `SV2` / `SV3` that matches
|
|
98
|
+
that variant. When none arrives, because the line carries an `SVx` for a different variant (an
|
|
99
|
+
ST-03 of `005010X222A2` on a file whose lines are `SV2`, or a `{ type: "P" }` over the same) or
|
|
100
|
+
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,
|
|
102
|
+
modifiers, unit of measure and place of service are equally undecoded. **That line now emits
|
|
103
|
+
`X12_837_SERVICE_LINE_NOT_DECODED`, anchored at the `LX` that opened it.** Read the values off
|
|
104
|
+
`tx.segments[…].raw` and decide which of the two disagreeing signals the sender meant.
|
|
105
|
+
- **Ignoring the foreign `SVx` is deliberate and is not the limitation.** `SV1-02` and `SV2-03`
|
|
106
|
+
are both the line charge, so decoding an `SV2` into a Professional line would mis-read money.
|
|
107
|
+
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.
|
|
111
|
+
- **The line is still retained, and so are the bytes.** Nothing is dropped: the service line is
|
|
112
|
+
on the claim, and every segment, decoded or not, stays verbatim on `tx.segments`.
|
|
113
|
+
- **Neither cause is attributed to the sender.** A `type` option that disagrees with a perfectly
|
|
114
|
+
conformant document produces the same warning as a document that disagrees with itself, and the
|
|
115
|
+
library does not decide which side is wrong. A line with no `SVx` at all is short a Loop 2400
|
|
116
|
+
segment the 837 TR3s require, but that judgement is yours to make from the bytes.
|
|
117
|
+
- **An unresolvable variant is a different case, and it no longer goes unreported.** With no
|
|
118
|
+
`type`, an ST-03 that resolves to no known variant **and no `SVx` anywhere in the transaction to
|
|
119
|
+
fall back on**, no service line is opened and no `0` is fabricated on any line slot. That raises
|
|
120
|
+
`X12_837_UNKNOWN_VARIANT` for the submission, and now also
|
|
121
|
+
`X12_837_SERVICE_LINE_DROPPED` at each `LX` that consequently opened nothing - so the missing
|
|
122
|
+
lines are reported per line and not inferred from a count. The earlier warning here, that the
|
|
123
|
+
lookup was a plain object literal and its absence therefore proved nothing, is **withdrawn: that
|
|
124
|
+
hole is closed** (see the next entry). Checking `submission.variant` against the set you expect
|
|
125
|
+
is still worth doing, as a cheap assertion rather than as a defence.
|
|
126
|
+
**🩺 Its anchor moved, so a consumer joining on `position.segmentIndex` gets a different
|
|
127
|
+
segment than it did on `0.0.10`.** `X12_837_UNKNOWN_VARIANT` now points at the **`ST`**
|
|
128
|
+
(`tx.segments[0]`), which carries the ST-03 the resolution reads. Through `0.0.10` it pointed at
|
|
129
|
+
`tx.segments[1]`, the **`BHT`** - a segment with no part in variant resolution. No
|
|
130
|
+
`elementIndex` is set, deliberately: one of the two routes into this warning is an ST-03 that is
|
|
131
|
+
absent entirely, and on that route the `ST` has no element 3 to name.
|
|
132
|
+
|
|
133
|
+
- **🩺 An 837 `LX` that opens no Loop 2400 at all raises `X12_837_SERVICE_LINE_DROPPED`, the 25th
|
|
134
|
+
Tier-2 warning code.** Distinct from `X12_837_SERVICE_LINE_NOT_DECODED` above, where the line IS
|
|
135
|
+
on the model and only its service segment went unread: here the line reaches **no claim's**
|
|
136
|
+
`serviceLines`, so its charge, units, procedure code and modifiers are read into nothing. Two
|
|
137
|
+
causes: no Loop 2300 (`CLM`) is open at that `LX`, or the submission's variant is not one of
|
|
138
|
+
`P` / `I` / `D`. Nothing is fabricated to stand in and no claim is synthesized; the segments stay
|
|
139
|
+
verbatim on `tx.segments`. **Do not read an empty `serviceLines` as "the claim had no service
|
|
140
|
+
lines" without checking the warning channel.** Three bounds on that code, each measured:
|
|
141
|
+
- **It does not travel with `X12_837_UNKNOWN_VARIANT`.** A caller-supplied `type` outside
|
|
142
|
+
`"P" | "I" | "D"` - which only a JavaScript or `JSON.parse`d caller can pass - reaches the second
|
|
143
|
+
cause with no unknown-variant warning at all. Read `submission.variant`, not the other code.
|
|
144
|
+
- **What becomes of a `DTP` / `AMT` / `NTE` / `REF` / `N3` / `N4` / `PER` after a dropped `LX`
|
|
145
|
+
depends on the route, and is not simply "absent" on either. Two drafts stated it unqualified,
|
|
146
|
+
in opposite directions, and both were wrong.**
|
|
147
|
+
With a `CLM` open (the variant route), the line service date, amount and note land among the
|
|
148
|
+
**claim-level** ones, indistinguishable from them, and a trailing `REF` lands among the
|
|
149
|
+
claim-level references. With **no** `CLM` open, **all seven are discarded**: nothing following
|
|
150
|
+
that `LX` attaches to the party named BEFORE it. The `DTP` / `AMT` / `NTE` already were
|
|
151
|
+
discarded on that route at `0.0.10`; the `REF` / `N3` / `N4` / `PER` are what changed. **Read
|
|
152
|
+
the segments off `tx.segments[…].raw` rather than inferring either outcome.**
|
|
153
|
+
**🩺 CHANGED, and the change moves values off the model that `0.0.10` put on it.** Through
|
|
154
|
+
`0.0.10` - the current release as this was written, so a consumer on it has the old behaviour -
|
|
155
|
+
the no-claim route did not discard these. Measured, on a payer: a line-item control number
|
|
156
|
+
surfaced in a _later_ claim's `payer.references`, and an `N3` / `N4` / `PER` gave that payer a
|
|
157
|
+
street address and a contact it never had. **Never read that as all four kinds on every party** -
|
|
158
|
+
this reader does not surface every one of those kinds on every party, and a `PER` on a patient
|
|
159
|
+
or a pay-to address reached the model on no release. **No per-kind, per-party map is published
|
|
160
|
+
in this document, deliberately**, and the `X12_837_ENTITY_SEGMENT_DISCARDED_AFTER_LX` entry
|
|
161
|
+
below does not carry one either: it says outright that whether such a segment would have
|
|
162
|
+
attached is a separate question it does not answer. `tx.segments` is what settles it for a
|
|
163
|
+
given document. **If you read `0.0.10` or earlier and relied on an
|
|
164
|
+
entity's `address`, `contacts` or `references`, those slots could carry line-level values from a
|
|
165
|
+
dropped Loop 2400.**
|
|
166
|
+
- **🩺 That change is a TRADE, and its cost is that a conformant entity segment can now be
|
|
167
|
+
dropped.** The TR3s nest Loop 2400 inside Loop 2300 and say nothing about an `LX` elsewhere, so
|
|
168
|
+
which party a segment following a **stray** `LX` belongs to is not derivable from the spec in
|
|
169
|
+
either direction. Where the `LX` was injected into an **entity** loop, the `N3` / `N4` / `REF` /
|
|
170
|
+
`PER` after it really were that entity's, and they are now discarded: measured, a payer that
|
|
171
|
+
kept its `PO BOX` address, its `2U` secondary id and its contact at `0.0.10` comes back with
|
|
172
|
+
`address: undefined`, `references: []` and `contacts: []`. The direction was chosen on this
|
|
173
|
+
library's own invariant, not on a clause: a mis-attribution puts a value on an object the sender
|
|
174
|
+
never put it on, indistinguishable from real data, whereas the bytes of a discarded segment are
|
|
175
|
+
still on `tx.segments`.
|
|
176
|
+
**🩺 The discard is REPORTED, by a code of its own, at the segment itself.** Each such
|
|
177
|
+
`N3` / `N4` / `PER` / `REF` raises `X12_837_ENTITY_SEGMENT_DISCARDED_AFTER_LX`, so the loss is
|
|
178
|
+
on the warning channel once per discarded segment. `X12_837_SERVICE_LINE_DROPPED` at that `LX`
|
|
179
|
+
reports the **service line** and not the entity's address, id or contact, so it never named this
|
|
180
|
+
loss and does not now. **The channel is still not a complete account of the document** -
|
|
181
|
+
`tx.segments` is - and the loss itself is unchanged: reporting it did not restore it. Both are
|
|
182
|
+
pinned by tests. **Read the new code's bound in its own entry below before restating it:** it is
|
|
183
|
+
not a general "this segment reached no party" report.
|
|
184
|
+
- **An `SVx` with no `LX` at all raises `X12_837_SERVICE_SEGMENT_WITHOUT_LX`, the 26th Tier-2
|
|
185
|
+
code, anchored at the service segment itself.** Through `0.0.10` it was dropped in SILENCE:
|
|
186
|
+
both codes above are anchored at the `LX`, so a service segment that never had one had nothing
|
|
187
|
+
to anchor to and reported on no channel. See the next entry for what that code says and what it
|
|
188
|
+
still does not. **The general caveat stands even so: the warning channel is a report of the
|
|
189
|
+
losses this library knows how to name, and `tx.segments` is the only complete account of the
|
|
190
|
+
bytes.**
|
|
191
|
+
|
|
192
|
+
- **🩺 An `SV1` / `SV2` / `SV3` that arrives with no Loop 2400 open raises
|
|
193
|
+
`X12_837_SERVICE_SEGMENT_WITHOUT_LX`, anchored at the service segment itself.** The third code in
|
|
194
|
+
this family, and the one that needed a different anchor: the other two both name an `LX`, and this
|
|
195
|
+
case is defined by there being no line open to name one. **Read that condition literally.** It is
|
|
196
|
+
**not** "the file contains no `LX`" - an `LX` in an earlier claim is still an `LX`, and
|
|
197
|
+
`CLM*1~ LX*1~ SV1~ CLM*2~ SV1~` raises this code on the second `SV1` while the first claim keeps
|
|
198
|
+
its decoded line. Nothing the segment carries is read - not its charge, its
|
|
199
|
+
units, its procedure code, its modifiers, its unit of measure or its place of service - no line
|
|
200
|
+
reaches any claim's `serviceLines`, and nothing is fabricated to stand in. **Through `0.0.10` -
|
|
201
|
+
the release published as this was written, so a consumer who has not upgraded still has it - that
|
|
202
|
+
was the whole of the report: a charge, a quantity and a procedure code left the typed model with
|
|
203
|
+
`warnings: []` and the claim read as one that simply had no service lines.** Five bounds, each
|
|
204
|
+
measured:
|
|
205
|
+
- **The three codes never report the SAME service segment**, because the other two are raised at
|
|
206
|
+
an `LX` and this one only where no line is open. A document with several claims can still carry
|
|
207
|
+
all three, on three distinct segments; that case is a committed test.
|
|
208
|
+
- **An `LX` that opened nothing suppresses it for the segments inside that dropped loop**, which
|
|
209
|
+
is deliberate: the loss is already named, once, at the `LX`. The suppression is scoped to the
|
|
210
|
+
dropped loop and is cleared by the next flush, so a later orphan in the same transaction is
|
|
211
|
+
still reported. Both halves have a red negative control.
|
|
212
|
+
- **It reports once per service segment, not once per loop.** Two orphan `SVx` segments raise two
|
|
213
|
+
warnings at two positions, so a consumer can name both rather than infer a count from one.
|
|
214
|
+
- **The segment is NOT decoded into any line.** `SV1-02` and `SV2-03` are both the line charge, so
|
|
215
|
+
reading a service segment into a line the walker never opened mis-reads money. Refusing to read
|
|
216
|
+
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.
|
|
219
|
+
- **🩺 IT SAYS NOTHING ABOUT THE VARIANT, AND A FIRST DRAFT OF THIS BOUND CLAIMED IT DID.** Variant
|
|
220
|
+
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
|
|
222
|
+
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
|
|
225
|
+
code - `undefined`, not `""`, which on such a line is the `revenueCode`. Passing
|
|
226
|
+
`{ type: "P" }` reads the same document correctly. Measured, both trees: `PRE-EXISTING`,
|
|
227
|
+
identical at `0.0.10`, **not** introduced or changed by this code, and **not** narrowed here - excluding
|
|
228
|
+
orphans from the fallback would change how existing documents decode and is its own slice. It
|
|
229
|
+
is warned rather than silent (`X12_837_SERVICE_LINE_NOT_DECODED` at each `LX`), and
|
|
230
|
+
`submission.variant` is the field that tells you.
|
|
231
|
+
|
|
232
|
+
- **🩺 An `N3` / `N4` / `PER` / `REF` discarded after a stray `LX` raises
|
|
233
|
+
`X12_837_ENTITY_SEGMENT_DISCARDED_AFTER_LX`, anchored at the discarded segment itself.** This is
|
|
234
|
+
the code that names the trade recorded in the entry above: where an `LX` with **no `CLM` open**
|
|
235
|
+
landed inside an entity loop, it closes that loop, so those four segment kinds reach no party on
|
|
236
|
+
the model. Releases through `0.0.10` attached such a segment to whichever party the last `NM1`
|
|
237
|
+
left active **wherever this reader surfaces that segment kind on that party at all** - never
|
|
238
|
+
read that as all four kinds on every party, because a `PER` on a patient or a pay-to address
|
|
239
|
+
reached the model on no release. Discarding them instead is what stopped a line-level value
|
|
240
|
+
surfacing as a property of a party the sender never put it on, and this code is what stops that
|
|
241
|
+
discard being silent. **It reports that the segment reached no party, NOT that it would have
|
|
242
|
+
reached one**, so it can fire where nothing this library's own reset lost. The
|
|
243
|
+
anchor is the segment and not the `LX`, because the loss is per segment: two `N3`s are two
|
|
244
|
+
warnings at two positions, so a consumer can name both. Four bounds, each a committed test:
|
|
245
|
+
- **🩺 It is NOT a general "this entity segment reached no party" report, and it must never be
|
|
246
|
+
restated as one.** It reports one discarded after such an `LX`, and only until the next
|
|
247
|
+
`NM1` / `HL` / `CLM` opens a loop. A party named after that `LX` is outside this code's
|
|
248
|
+
scope again and its trailing segments are silent: the scope is a scope, not a latch.
|
|
249
|
+
**Whether they ATTACH is a separate question this code does not answer**, and for some party
|
|
250
|
+
and segment kinds the answer is no on every release - a `PER` reaches neither a patient nor a
|
|
251
|
+
pay-to address on any of them. **This code reports that a segment reached NO party, never that
|
|
252
|
+
it would otherwise have reached one.**
|
|
253
|
+
- **Every other route to an unattached `N3` / `N4` / `PER` / `REF` is exactly as silent as it was
|
|
254
|
+
before this code existed**, and that is deliberate rather than an oversight: no entity loop open
|
|
255
|
+
at the stray `LX` at all (nothing was lost by this library's doing, since those segments reached
|
|
256
|
+
no party at `0.0.10` either), an `NM1` this reader cannot route, an intervening `HL` or `CLM`,
|
|
257
|
+
and the other dropped-`LX` route where a claim **is** open. Widening it to those is a guard
|
|
258
|
+
change and would be its own decision.
|
|
259
|
+
- **The `DTP` / `AMT` / `NTE` of the seven trailing kinds are NOT reported by it.** They are
|
|
260
|
+
discarded on that route too, and were at `0.0.10` as well, because they never attach to a party
|
|
261
|
+
on any route. This code says nothing about them; `X12_837_SERVICE_LINE_DROPPED` at the `LX` is
|
|
262
|
+
what reports that loop's loss.
|
|
263
|
+
- **It does not restore anything.** The segments are still discarded, and are still verbatim on
|
|
264
|
+
`tx.segments`, which remains the only complete account of the document. Which party a segment
|
|
265
|
+
following a **stray** `LX` belongs to is not derivable from the TR3s in either direction, so the
|
|
266
|
+
reader refuses to attribute it rather than guessing.
|
|
267
|
+
|
|
268
|
+
- **🩺 Through `0.0.9`, a lookup keyed by document bytes could be defeated by a key inherited from
|
|
269
|
+
`Object.prototype`, and the affected code paths reported nothing.** The bundled code lists, the
|
|
270
|
+
837's variant resolution, and the 837's HL parent-level map were built as plain object literals,
|
|
271
|
+
which inherit `Object.prototype`. An inbound value matching **any own property of
|
|
272
|
+
`Object.prototype`** therefore resolved TRUTHY. That set is engine- and version-dependent and is
|
|
273
|
+
deliberately not enumerated here: on the Node 22 this package targets it is twelve members, and a
|
|
274
|
+
draft of this entry listed eight. `Object.freeze` did not help: it seals the own properties and
|
|
275
|
+
changes nothing about the prototype chain. Concretely, at `0.0.9`:
|
|
276
|
+
- An ST-03 of `constructor` made `submission.variant` a **function**, suppressed
|
|
277
|
+
`X12_837_UNKNOWN_VARIANT`, and took **every Loop 2400 off the model** with `warnings: []`.
|
|
278
|
+
- `lookupCarc("constructor")` answered a `CodeListEntry` whose `description`, typed `string`, was
|
|
279
|
+
a function, and suppressed `X12_UNKNOWN_CARC`. The same held for the other bundled lists.
|
|
280
|
+
- An HI qualifier of `constructor` suppressed `X12_UNKNOWN_HI_QUALIFIER` while still landing the
|
|
281
|
+
code with `codeSystem: "unknown"`.
|
|
282
|
+
- An HL-03 of `constructor` raised `X12_HL_PARENT_LEVEL_INVALID` against a level the walker has
|
|
283
|
+
no expectation for - a structural violation the document never made.
|
|
284
|
+
- `isClaimAdjustmentGroupCode("constructor")` answered `true` and narrowed the type. It used the
|
|
285
|
+
`in` operator, which **walks the prototype chain**; `in` is the safe-looking form and is not
|
|
286
|
+
the safe form.
|
|
287
|
+
|
|
288
|
+
Every one of these now behaves exactly as it does for any other unrecognized value. The 271 / 277
|
|
289
|
+
/ 278 readers were never exposed: their `EXPECTED_PARENT_LEVEL` tables have the same literal shape
|
|
290
|
+
but are read only through `src/transactions/shared/hl.ts`, which has always guarded with
|
|
291
|
+
`hasOwnProperty`. **What is not claimed:** there is no source-level scan enforcing this shape. A
|
|
292
|
+
syntactic scan cannot tell a table keyed by document bytes from one keyed by a library-owned
|
|
293
|
+
discriminant, and the message registries in `src/parser/warnings.ts` are legitimately the latter,
|
|
294
|
+
so such a scan would need a per-table allowlist. The defence is behavioural instead: the suite
|
|
295
|
+
derives its key list from `Object.getOwnPropertyNames(Object.prototype)` at run time, so a future
|
|
296
|
+
engine adding an inherited member widens it without anyone editing a list.
|
|
297
|
+
|
|
298
|
+
- **🩺 BREAKING, in the release after `0.0.9`: the 835 Loop 2110 SVC element map was off by one, in
|
|
299
|
+
BOTH directions, and is corrected.** Through `0.0.9` `get835` read the revenue code from **SVC-05**
|
|
300
|
+
and the paid units from **SVC-07**, and `build835` wrote them to the same two places while leaving
|
|
301
|
+
SVC-04 empty. The correct map, which the repo's own 277 modules already used:
|
|
302
|
+
|
|
303
|
+
| Element | X12 element | Meaning |
|
|
304
|
+
| ------- | ---------------------------------- | ----------------------------------- |
|
|
305
|
+
| SVC-04 | 234, Product/Service ID (a string) | NUBC revenue code |
|
|
306
|
+
| SVC-05 | 380, Quantity | Units of Service **Paid** Count |
|
|
307
|
+
| SVC-06 | C003 composite | original / submitted procedure |
|
|
308
|
+
| SVC-07 | 380, Quantity | **Original** Units of Service Count |
|
|
309
|
+
|
|
310
|
+
**What changes for you.** On parse, `serviceLine.revenueCode` now comes from SVC-04 and is
|
|
311
|
+
`undefined` on a professional line; through `0.0.9` it returned whatever sat in SVC-05, which on a
|
|
312
|
+
conformant 835 is the paid-unit count - measured across all six committed remit fixtures plus the
|
|
313
|
+
golden, **8 of 8 service lines returned `revenueCode: "1"`, which is not a valid NUBC revenue
|
|
314
|
+
code**, while `paidUnitsOfService` came back `undefined`. `serviceLine.paidUnitsOfService` now
|
|
315
|
+
comes from SVC-05; through `0.0.9` it read SVC-07, which is the _submitted_ count, so where a payer
|
|
316
|
+
sent both the field named "paid" carried the original. **`serviceLine.originalUnitsOfService` is
|
|
317
|
+
new** and carries SVC-07; without it the corrected map would have made SVC-07 unread, turning a
|
|
318
|
+
fixed mis-read into a fresh silent drop. On emit, the same three fields move to the same three
|
|
319
|
+
places: `build835` with a revenue code `0300` and 2 paid units emitted
|
|
320
|
+
`SVC*HC:99213*600.00*550.00**0300*HC:99212*2` at `0.0.9` and emits
|
|
321
|
+
`SVC*HC:99213*600.00*550.00*0300*2*HC:99212` now. **The old bytes put a revenue code in a Quantity
|
|
322
|
+
element**, so a conformant receiver read `0300` as 300 units of service.
|
|
323
|
+
|
|
324
|
+
**If you compensated for the old behaviour** - reading `paidUnitsOfService` off `revenueCode`, or
|
|
325
|
+
writing the revenue code into `paidUnitsOfService` - that workaround must be removed. Code that
|
|
326
|
+
only round-tripped through this library saw nothing wrong, because both halves were wrong together.
|
|
327
|
+
|
|
328
|
+
**Sources, and what was NOT read. The TR3 005010X221A1 itself is a paid X12 document; nobody here
|
|
329
|
+
has read it.** Sources 1, 2 and 4 below are free to check and are linked; source 3 is a paid X12
|
|
330
|
+
document, named rather than linked, and every fact taken from it is independently carried by
|
|
331
|
+
source 1 anyway. Read the qualifiers literally, because an unsourced assertion about this table
|
|
332
|
+
baked into a code comment is exactly what shipped the defect:
|
|
333
|
+
1. **The full X221A1 element table**, from [pyx12](https://github.com/azoner/pyx12)'s
|
|
334
|
+
machine-readable map `pyx12/map/835.5010.X221.A1.xml` - an independent open-source
|
|
335
|
+
implementation of this same guide: `SVC04 / 234 / "National Uniform Billing Committee Revenue
|
|
336
|
+
Code"`, `SVC05 / 380 / "Units of Service Paid Count"`, `SVC06 / C003`,
|
|
337
|
+
`SVC07 / 380 / "Original Units of Service Count"`. **This is the source for SVC-04**, and the
|
|
338
|
+
only one here that carries the whole map.
|
|
339
|
+
2. **X12's own [RFI #2163](https://x12.org/resources/requests-for-interpretation/rfi-2163-835-svc05-vs-837-svd05)**,
|
|
340
|
+
which names "the SVC05 'Units of Service Paid Count/Quantity' in the 835 guide" and states that
|
|
341
|
+
"a default has been included for SVC05 in guide 005010X221A1". Primary, from X12, naming the
|
|
342
|
+
guide - but it speaks only to SVC-05.
|
|
343
|
+
3. **The base X12 005010 SVC element dictionary**: SVC-04 is element 234, a string, and SVC-05 and
|
|
344
|
+
SVC-07 are both element 380, Quantity. This does not say what SVC-04 carries, but it **rules
|
|
345
|
+
out a revenue code at SVC-05** on type alone.
|
|
346
|
+
4. **Published payer companion guides implementing X221A1** ([Florida Health Care
|
|
347
|
+
Plans](https://www.fhcp.com/documents/edi-forms/Florida-Health-Care-Plans-835.pdf) p.44 gives
|
|
348
|
+
all three names; [South Dakota
|
|
349
|
+
Medicaid](https://dss.sd.gov/docs/medicaid/providers/billingmanuals/HIPAA/835_Healthcare_Payment.pdf)
|
|
350
|
+
p.9 gives SVC-04 and SVC-05).
|
|
351
|
+
|
|
352
|
+
**Agreement inside this repo is NOT one of the sources.** The 277 modules use SVC-04 and every
|
|
353
|
+
committed 835 fixture is written to the map above, which is corroborating and was what surfaced
|
|
354
|
+
the defect - but checking a spec claim against this repo's own implementation only proves the two
|
|
355
|
+
agree, which is precisely how the wrong map survived.
|
|
356
|
+
|
|
357
|
+
**An absent SVC-05 is NOT defaulted to one.** X221A1 is reported to assume the value is one when
|
|
358
|
+
not present - that is source 2's Description, quoted secondhand, not a clause read from the TR3.
|
|
359
|
+
This reader leaves `paidUnitsOfService` `undefined` regardless, because fabricating a count the
|
|
360
|
+
sender did not send is inventing data.
|
|
361
|
+
|
|
362
|
+
**`undefined` on either quantity still means "not decoded", not "absent".** The element may have
|
|
363
|
+
been missing, or present and unparseable as a decimal; the two share one `undefined` on the model.
|
|
364
|
+
They no longer share a silence: as of `X12-QUANTITY-SILENT-DEFAULTS` the unparseable case raises
|
|
365
|
+
`X12_UNPARSEABLE_DECIMAL` at that `position.elementIndex` and the absent case raises nothing, so a
|
|
366
|
+
warning at the element is what tells them apart. The verbatim element is still on `tx.segments`.
|
|
367
|
+
|
|
368
|
+
**If you archived 835s this library EMITTED at `0.0.9` or earlier, they are non-conformant on the
|
|
369
|
+
wire and should be re-emitted.** Their revenue code sits in SVC-05, so this library now reads it
|
|
370
|
+
back as a paid quantity (`0300` becomes 300 units) and reports no revenue code, silently and with
|
|
371
|
+
no warning. Re-emitting from the model with this release produces conformant bytes.
|
|
372
|
+
|
|
373
|
+
**The 277's SVC-07 is a different element in a different TR3 and is now decoded and emitted in its
|
|
374
|
+
own right** (`X12-277-SVC07-NOT-DECODED`, below). Do not read either field off the other.
|
|
375
|
+
|
|
376
|
+
- **Bundled code-list snapshots are pre-launch initial subsets, not the full WPC-published lists.**
|
|
377
|
+
CARC, RARC, Claim-Status-Category (CSCC), Claim-Status (CSC), service-type, CLP-status, and
|
|
378
|
+
maintenance-type ship as versioned data artifacts sized to the parser's Tier-1/Tier-2 fixtures plus
|
|
379
|
+
the long-tail codes most workflows branch on. An inbound code **outside** a snapshot still parses:
|
|
380
|
+
the verbatim code is preserved on the model and an `X12_UNKNOWN_*` warning is raised. Only the
|
|
381
|
+
human-readable **description** is absent (`undefined`). A stale or partial snapshot therefore yields
|
|
382
|
+
a missing description, **never a wrong code**. Run `pnpm refresh:code-lists` to audit snapshot
|
|
383
|
+
freshness; regenerating the full lists (`--fetch`) is a redistribution-terms-gated release step (see
|
|
384
|
+
below), not a runtime fetch.
|
|
385
|
+
|
|
386
|
+
- **`serialize(parse(s)) === s` is NOT guaranteed.** `serializeX12` rebuilds the interchange from the
|
|
387
|
+
model, so every segment the parser recorded comes back verbatim (element padding, composites, and
|
|
388
|
+
`?`-release escapes included), in the order the model holds it. Six constructs are known not to
|
|
389
|
+
survive:
|
|
390
|
+
1. **Line breaks between segments.** Most senders write one after each terminator; the parser
|
|
391
|
+
absorbs any run of CR / LF bytes between segments, so a pretty-printed, double-spaced, or
|
|
392
|
+
mixed-ending file all emit the same compact form. **Silent.**
|
|
393
|
+
2. **A doubled segment terminator** outside a transaction. It delimits a zero-length segment
|
|
394
|
+
carrying no elements, so there is nothing to retain. **Silent.**
|
|
395
|
+
3. **A missing final segment terminator.** The emit supplies one. **Silent.**
|
|
396
|
+
4. **Post-IEA `trailingBytes`**, re-joined from segment slices rather than preserved verbatim.
|
|
397
|
+
5. **TA1 position.** A TA1 that appeared **after** a functional group is collected onto
|
|
398
|
+
`ix.ta1Segments` and emitted immediately after the ISA, so the emit **reorders** it. **Silent**,
|
|
399
|
+
and unlike the others nothing is lost: the model and the warning stream both round-trip
|
|
400
|
+
identically. It is also the only construct that moves something _else_: a segment outside a
|
|
401
|
+
transaction is placed correctly relative to the groups, but not relative to a TA1 hoisted past
|
|
402
|
+
it. This library takes no position on where ASC X12 requires a TA1 to sit.
|
|
403
|
+
6. **A segment whose first element is empty (`*A*B~`), outside a transaction.** It has no id for
|
|
404
|
+
the envelope walker to dispatch on, so it is skipped: absent from the model, absent from the
|
|
405
|
+
emit, and it does not even raise `X12_UNEXPECTED_SEGMENT`. **Silent**, and the only case here
|
|
406
|
+
that loses a value with no diagnostic whatsoever. Inside an open transaction the same segment
|
|
407
|
+
is kept and re-emitted normally, so this is specific to the outside-a-transaction position.
|
|
408
|
+
|
|
409
|
+
**Cases 2 to 6 break the round trip on inputs containing no line breaks**, so "my file is compact"
|
|
410
|
+
is not sufficient grounds to expect byte equality. **Five of the six (1, 2, 3, 5, 6) produce no
|
|
411
|
+
warning at all**, so a clean `ix.warnings` is not evidence that a round trip will be byte-exact;
|
|
412
|
+
only case 4 warns.
|
|
413
|
+
|
|
414
|
+
**A segment outside a transaction is no longer on this list.** It used to be, in both halves: the
|
|
415
|
+
segment was discarded from the model, and the emit could not reproduce what the model did not
|
|
416
|
+
hold. It is now retained on `ix.orphanSegments` **and** re-emitted, so the bytes, the value and the
|
|
417
|
+
`X12_UNEXPECTED_SEGMENT` warning all survive. Placement is by the **structural anchor** on
|
|
418
|
+
`X12OrphanSegment.anchor` - which group, which transaction, which offset inside it - and never by
|
|
419
|
+
`segmentIndex`. That distinction is the entire correctness argument, because `segmentIndex` indexes
|
|
420
|
+
the **input** stream while the emit is not in input order (see case 5, and the zero-length segment
|
|
421
|
+
in case 2 occupies an input index that is never emitted). Measured on a two-group interchange with
|
|
422
|
+
a TA1 after the first group, an earlier attempt at _positional_ re-emit landed a stray segment
|
|
423
|
+
inside an 835's `ST..SE` body between `CLP` and `SE`, with no warning at all on the re-parse, and a
|
|
424
|
+
stray `SE` closed the transaction early and corrupted SE-01. An anchor names a slot in the typed
|
|
425
|
+
tree, which is invariant under both reorderings. **Use `segmentIndex` to join an orphan to its
|
|
426
|
+
warning, never to place it.** An anchor is a position into `groups` / `transactions` /
|
|
427
|
+
`rawSegments` as they stand on the interchange you pass in, so reshaping those by hand invalidates
|
|
428
|
+
it; an anchor that resolves to nothing is emitted at interchange level before the `IEA` rather
|
|
429
|
+
than dropped.
|
|
430
|
+
|
|
431
|
+
**One interaction with spec-clean mode is worth knowing.** A `TA1` that arrived between an `ST`
|
|
432
|
+
and its `SE` is lifted off `tx.rawSegments` by the walker but re-emitted where it came from, so it
|
|
433
|
+
IS a segment of that transaction set for SE-01 purposes ("segments included in the transaction
|
|
434
|
+
set, including ST and SE", X12.6). `{ specClean: true }` therefore counts it, and
|
|
435
|
+
`{ recomputeCounts: true }` writes the count that matches the emitted bytes. Counting the model
|
|
436
|
+
alone would shrink a correct SE-01 by one per such segment, which is a count corruption rather
|
|
437
|
+
than a correction. An orphan emitted before the `ST` or after the `SE` is outside the range and is
|
|
438
|
+
not counted. GE-01 and IEA-01 are unaffected: an orphan is never a `GS` and never opens a
|
|
439
|
+
transaction set.
|
|
440
|
+
|
|
441
|
+
What is **measured** for the orphan round trip: a stray segment inserted at every position of a
|
|
442
|
+
two-group, three-transaction interchange, over five segment ids - `ZZ`, `SE`, `GE`, `ST` and `TA1`,
|
|
443
|
+
covering all five orphan `context` values and all three anchor kinds. **50 of the 50 insertions
|
|
444
|
+
that produce an orphan round-trip byte-exactly** on a base with no envelope-level TA1. On the same
|
|
445
|
+
base _with_ one, all **54** differ from their source - and all 54 are byte-identical once the TA1
|
|
446
|
+
is removed from both sides, so the only thing that moved is the TA1 itself, which moves on that
|
|
447
|
+
base with no orphan present at all. Across all 104, the transaction bodies, `orphanSegments` (raw,
|
|
448
|
+
context and anchor), `ta1Segments`, `trailingBytes` and the warning multiset are unchanged by the
|
|
449
|
+
round trip, and every emit is a fixed point.
|
|
450
|
+
|
|
451
|
+
What is **measured** across the 56 committed fixtures: every emit is a fixed point (serializing it
|
|
452
|
+
again is a byte-level no-op) and re-parses to an identical model with an identical warning stream;
|
|
453
|
+
the 14 fixtures with no line breaks return byte-identical; and the other 42 differ from their source
|
|
454
|
+
by **line breaks and nothing else**, with no element value lost, altered, reordered, or re-escaped.
|
|
455
|
+
Two caveats on that corpus, both of which limit how far the sweep can be pushed: it contains **no
|
|
456
|
+
instance of cases 2 to 6** (zero fixtures produce an `orphanSegments` entry, which is why the
|
|
457
|
+
orphan sweep above is constructed rather than drawn from it), and **13 of the 14
|
|
458
|
+
byte-identical fixtures are `golden/*.edi`**, which
|
|
459
|
+
are serializer output by construction, leaving `envelope/no-trailing-crlf.edi` as the only
|
|
460
|
+
independent witness. Preserving the original framing byte-for-byte would need the model to carry
|
|
461
|
+
per-segment framing and TA1 position it does not have today; that is a tracked model change, not a
|
|
462
|
+
behaviour to assume.
|
|
463
|
+
|
|
464
|
+
- **A segment that falls outside a transaction is not decoded into the typed tree, but it is neither
|
|
465
|
+
dropped from the model nor dropped from the emit.** The envelope walker binds body segments
|
|
466
|
+
to an ST..SE transaction and has nowhere in `groups` to put one that arrives outside: a stray
|
|
467
|
+
segment between `GE` and `IEA`, a body segment between an `SE` and its group's `GE`, a body segment
|
|
468
|
+
between `GS` and the first `ST`, an `ST` with no open group, an `SE` or `GE` that closes nothing,
|
|
469
|
+
or a `TA1` inside an open group. Each raises `X12_UNEXPECTED_SEGMENT` **and** is retained verbatim
|
|
470
|
+
on `ix.orphanSegments`, whose `segmentIndex` is the join key back to the warning's
|
|
471
|
+
`position.segmentIndex`, and whose `anchor` is the structural slot `serializeX12` puts it back at.
|
|
472
|
+
|
|
473
|
+
**One of those is not an "outside a transaction" case at all.** `TA1` is envelope-level by spec, so
|
|
474
|
+
a `TA1` inside an open group goes to `ix.orphanSegments` even when it arrived **between an `ST` and
|
|
475
|
+
its `SE`**, and it is lifted out of that transaction's `segments` / `rawSegments`. So for a document
|
|
476
|
+
containing such a `TA1`, `ix.groups` is not the whole typed model. This is long-standing behaviour,
|
|
477
|
+
unchanged here except that the segment is now retained instead of discarded.
|
|
478
|
+
|
|
479
|
+
**One limitation remains, and it is decoding, not retention or re-emission.** No `get*` reader will
|
|
480
|
+
see an orphan: retention and placement are not promotion into the typed tree, and a segment the
|
|
481
|
+
envelope grammar could not place is not one a transaction reader should walk. Read these segments
|
|
482
|
+
from `ix.orphanSegments`. That array is empty for a well-formed
|
|
483
|
+
interchange, so a non-empty one is itself the signal that the sender's framing did not match the
|
|
484
|
+
envelope grammar. **It carries the sender's bytes verbatim, so treat it as PHI**: unlike
|
|
485
|
+
`ix.warnings`, whose messages come from a frozen registry and whose metadata is positional, an
|
|
486
|
+
orphan is document content. Log `context` and `segmentIndex`, not the whole entry.
|
|
487
|
+
|
|
488
|
+
Two related shapes are **not** captured here, both long-standing: a doubled segment terminator
|
|
489
|
+
(a zero-length segment with no elements, so there is nothing to retain) and a segment whose first
|
|
490
|
+
element is empty (`*A*B~`), which has no id for the walker to dispatch on and is skipped with no
|
|
491
|
+
warning.
|
|
492
|
+
|
|
493
|
+
This entry previously documented real silent data loss: the segment was discarded from the model
|
|
494
|
+
entirely, and because a blank line between segments exceeded the old one-CR-plus-one-LF tolerance,
|
|
495
|
+
a uniformly double-spaced file lost its **entire interchange body** and returned `groups: []`. Both
|
|
496
|
+
of those are fixed. The tolerance now absorbs any run of CR / LF bytes between segments, so all 15
|
|
497
|
+
CR/LF sequences of length 0 to 3 frame identically (the old bound admitted 4 of those 15).
|
|
498
|
+
|
|
499
|
+
- **837 claim-/line-level provider addresses (Loop 2310 / 2420 `N3`/`N4`) are not surfaced.** The
|
|
500
|
+
provider **identities** (`NM1`) round-trip, but the street-address lines do not decode onto the
|
|
501
|
+
model. Read them from the raw segments if you need them.
|
|
502
|
+
|
|
503
|
+
- **`get834Enrollments` streams members but still parses the whole file up front.** It yields one
|
|
504
|
+
decoded member per `INS` loop (so a consumer holds one member at a time), but the underlying
|
|
505
|
+
interchange is fully parsed into `tx.segments` before iteration begins. It is not a byte-streaming
|
|
506
|
+
reader for arbitrarily large files.
|
|
507
|
+
|
|
508
|
+
- **A builder refusal message shows at most 63 characters of a value you passed in, and it is bounded
|
|
509
|
+
but not escaped.** Twenty-four sites across ten builder modules interpolate such a value into the
|
|
510
|
+
thrown message. Every one routes through `renderCallerValue`, so the rendered **fragment** never exceeds
|
|
511
|
+
`BUILD_REFUSAL_VALUE_MAX_RENDERED` (**90** characters: 63 of your value, two quotes, an ellipsis, and
|
|
512
|
+
the ` (N characters)` suffix at its widest). Both constants and the function are exported, so you can
|
|
513
|
+
assert the ceiling rather than take it on trust.
|
|
514
|
+
|
|
515
|
+
**The ceiling is on the fragment, not on the message.** A refusal message is that fragment plus the
|
|
516
|
+
site's own fixed template text, which differs per site, so every message is bounded by a constant but
|
|
517
|
+
not by _that_ constant. Measured: a 120,000-character control number produced a **120,066-character**
|
|
518
|
+
`X12BuildError.message` from `buildInterchange` before this change and produces a **150-character**
|
|
519
|
+
one now. Do not read 90 as a message length.
|
|
520
|
+
|
|
521
|
+
Over-long values are the point of nine of the twenty-four sites (the `control number "…" exceeds the
|
|
522
|
+
N-char spec limit` refusal, one per emitting module, where the branch fires **because** the value is
|
|
523
|
+
over-long). Seven more had no length gate at all: `build999`'s ST-02 trace twice, `buildInterchange`'s
|
|
524
|
+
transaction-set id, `build837`'s service-line variant, `build834`'s INS-03 and HD-01 maintenance
|
|
525
|
+
types, and `buildTA1`'s note code. The last seven are all in `build999` and were found by adversarial
|
|
526
|
+
review rather than by the original census. Four are the AK9-02 / AK9-03 / AK9-04 counts, typed
|
|
527
|
+
`number` and so missed by a census of string-typed fields, though a spec built from `JSON.parse` can
|
|
528
|
+
still carry a string there (measured at 120,063 characters). Three read `.length` off a
|
|
529
|
+
caller-supplied array, which a forged `{ length: … }` drove to 120,152 characters.
|
|
530
|
+
|
|
531
|
+
**What this is and is not.** These are values **you** passed in, so bounding them redacts nothing:
|
|
532
|
+
you already hold the value, and if you put patient data in a control number the refusal will show up
|
|
533
|
+
to 63 characters of it. What the bound buys is that `Error.message` from a builder has a fixed
|
|
534
|
+
ceiling instead of growing with your input, which matters for log lines, crash reports, and JSON
|
|
535
|
+
error envelopes. The surviving characters are **not escaped** either: they are whatever you supplied,
|
|
536
|
+
including a newline or a segment terminator, so a refusal message is bounded but not guaranteed to be
|
|
537
|
+
a single log line.
|
|
538
|
+
|
|
539
|
+
**The sites that applies to are the ones whose own template names the field:** a control number, an
|
|
540
|
+
X12 control code, a count. A refusal will not show you a `claimId`, a member id, a member name, a
|
|
541
|
+
trace or a diagnosis code. That was true of the templates and **not** of the shared type guards
|
|
542
|
+
underneath them until the release after `0.0.10`: those describe a wrong-typed element, they stand
|
|
543
|
+
on **every element of every builder**, and they used to render the offending primitive
|
|
544
|
+
(`a number ("900412345678")` for a `JSON.parse`d `patientControlNumber`) bounded to 90 characters
|
|
545
|
+
and not redacted. A guard that cannot tell which slot it is standing on cannot decide that echoing
|
|
546
|
+
is safe, so the string, segment and decimal guards report the TYPE and never the value, and so does
|
|
547
|
+
the array guard's primitive arm. If you were reading a value back out of one of those messages, that
|
|
548
|
+
is a behaviour change.
|
|
549
|
+
|
|
550
|
+
**Two things that sentence does NOT say, both deliberate.** The array guard still reports the
|
|
551
|
+
`length` and the class tag of a forged array-like, bounded through the same renderer: those describe
|
|
552
|
+
the SHAPE you forged rather than the contents of a document element, and they are the whole
|
|
553
|
+
diagnostic for `{ length: "9".repeat(120000) }`. And only the segment-join guard names the SLOT
|
|
554
|
+
alongside the type (`build999: "AK9"-01 must be a string, ...`), because it holds the whole segment;
|
|
555
|
+
the escape-helper and `X12Decimal` guards name the **builder**, so on those two the value used to
|
|
556
|
+
stand in for a locator and now nothing does. That is a real diagnostic cost, and it is written down
|
|
557
|
+
rather than hidden.
|
|
558
|
+
|
|
559
|
+
- **`defineProfile()` refusals are bounded on the same terms, since `0.0.6`.** `X12ProfileError.message`
|
|
560
|
+
used to interpolate your profile name, quirk id, effect, fixture path and expected-warning codes
|
|
561
|
+
verbatim. Measured before the fix, the worst message was **360,181 characters**, at the `fixture`
|
|
562
|
+
refusal, which names three caller values (the profile name, the quirk id, and the `JSON.stringify`d
|
|
563
|
+
path). It reaches that size because the quirk-id pattern carries no length bound (its comment
|
|
564
|
+
claimed "2-64 characters"; the regex never said so, and the comment was corrected to the code rather
|
|
565
|
+
than the grammar tightened). Twelve refusal sites hold twenty-three such holes between them; all
|
|
566
|
+
twenty-three now route through `renderCallerValue` or, where the value's **type** is what is wrong,
|
|
567
|
+
`renderCallerJson`, which keeps `null` distinguishable from `"null"` and bounds the JSON text. The
|
|
568
|
+
same `fixture` refusal now measures **431 characters**.
|
|
569
|
+
|
|
570
|
+
**431 is a measurement at a 120,000-character value, not a maximum.** The ` (N characters)` suffix
|
|
571
|
+
widens with the decimal width of the length, so the same refusal measures 434 at 1,000,000 and 437
|
|
572
|
+
at 10,000,000 characters. That site's ceiling, derived from its fixed text plus its three fragment
|
|
573
|
+
ceilings, is **443**, and the suite asserts every one of the twelve under 500.
|
|
574
|
+
|
|
575
|
+
Everything the builder paragraph says about scope applies here unchanged: it redacts nothing (you
|
|
576
|
+
passed the value in), the surviving characters are **not escaped**, and the bound is on UTF-16 code
|
|
577
|
+
units rather than bytes.
|
|
578
|
+
|
|
579
|
+
**`X12ProfileError.profileName` is deliberately NOT bounded.** It exists so you can pinpoint which
|
|
580
|
+
of your definitions failed, and truncating it would stop it matching the name you passed. It is your
|
|
581
|
+
own string and you still hold it. If you log a caught `X12ProfileError`, log `err.message` (bounded)
|
|
582
|
+
rather than the whole error object.
|
|
583
|
+
|
|
584
|
+
- **A builder used to emit a NUMBER as an EMPTY element, silently. FIXED in `0.0.9`: it now
|
|
585
|
+
REFUSES, and does not coerce.** The escape helper every builder ran values through read
|
|
586
|
+
`value.length`, which is `undefined` for a number, so it returned the empty string. The types say
|
|
587
|
+
`string`, so TypeScript callers could not reach it; a JavaScript or JSON-driven caller could, and
|
|
588
|
+
one that reads a spec off `JSON.parse` did.
|
|
589
|
+
|
|
590
|
+
Measured at `0.0.8` on an otherwise valid `build835` spec whose `patientControlNumber` was a number
|
|
591
|
+
rather than a string:
|
|
592
|
+
|
|
593
|
+
```
|
|
594
|
+
CLP**1*500.00*450.00*50.00*MB*ICN-9001*11::1 ix.warnings.length === 0
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
CLP-01 is required by TR3 005010X221A1 Loop 2100 and is the key that reassociates the remittance
|
|
598
|
+
back to the 837's CLM-01. It was dropped, **no warning was raised, no refusal was thrown**, and the
|
|
599
|
+
builder returned a frozen interchange that looked successful. The `patientControlNumber === ""`
|
|
600
|
+
guard did not catch a number, because the value was not yet a string when it was checked. The same
|
|
601
|
+
one-line mechanism reached every `esc()`-rendered slot in all nine builders, including the 837's
|
|
602
|
+
`CLM-01`, the other end of that same reassociation link.
|
|
603
|
+
|
|
604
|
+
**From `0.0.9` a non-string reaching that escape helper draws that builder's own typed, code-tagged
|
|
605
|
+
refusal** (`X12_835_BUILD_INVALID_SPEC` and its eight siblings) before anything is emitted. It
|
|
606
|
+
covers `number`, `boolean`, `null`, `undefined`, arrays, objects, functions, symbols and bigints.
|
|
607
|
+
|
|
608
|
+
**The library deliberately does NOT coerce, and you should know why before you reach for
|
|
609
|
+
`String(value)`.** A JSON payload that carried `"0012345"` as a number has already lost the leading
|
|
610
|
+
zeros; coercing would emit `12345`, a well-formed identifier that is **not the one you sent**, and a
|
|
611
|
+
remittance that reassociates to the wrong claim is worse than one that fails to reassociate at all.
|
|
612
|
+
`String(1e21)` is `"1e+21"`, `String(NaN)` is `"NaN"` and `String(0.1 + 0.2)` is
|
|
613
|
+
`"0.30000000000000004"`. None are valid in an `AN`, `ID` or `Nn` element. **Convert at your own
|
|
614
|
+
boundary, where you can still see whether the leading zeros mattered.**
|
|
615
|
+
|
|
616
|
+
**THE TYPE CHECK IS NOW STRUCTURAL, AND THE DELIMITER ESCAPE IS NOT.** `0.0.9` guarded values
|
|
617
|
+
routed through the escape helper, and not every element position went through it - two drafts of
|
|
618
|
+
this entry published a counted enumeration of the gaps and **both were measured incomplete**.
|
|
619
|
+
Rather than count a third time, the library moved the check to the one place every element must
|
|
620
|
+
pass: **the segment join**. So the statement is now a property instead of a list:
|
|
621
|
+
|
|
622
|
+
> **No non-string value reaches an element of a segment emitted through a builder's segment
|
|
623
|
+
> joiner.** A number, `null`, `undefined`, a boolean or an object in an element slot draws that
|
|
624
|
+
> builder's own typed, code-tagged refusal, naming the slot the way the spec does
|
|
625
|
+
> (`build999: "AK9"-01 must be a string, …`).
|
|
626
|
+
|
|
627
|
+
The slot in that locator is itself admitted only when it matches the X12 segment-id grammar (two or
|
|
628
|
+
three uppercase alphanumerics opening with a letter), because `buildInterchange` takes
|
|
629
|
+
`[segmentId, ...elements]` from you wholesale. Free text in element 0 degrades to `element N` rather
|
|
630
|
+
than reaching the message.
|
|
631
|
+
|
|
632
|
+
**Read the "through a builder's segment joiner" qualifier literally.** `buildTA1` does not use one,
|
|
633
|
+
and is therefore **not covered**: it emits its five caller-supplied elements with a direct join, no
|
|
634
|
+
escape and no padding, so a numeric or `undefined` `interchangeControlNumber` is emitted silently
|
|
635
|
+
(`TA1**250101*1200*A*000`). TA1-01 is the reassociation key from the acknowledgment back to the
|
|
636
|
+
interchange it acknowledges, so that matters. Unchanged from `0.0.9` and tracked as its own item.
|
|
637
|
+
|
|
638
|
+
The monetary slots got their own guard on top of it: a slot typed `X12Decimal` refuses anything that
|
|
639
|
+
is not one, rather than rendering `number.toString()` into the document.
|
|
640
|
+
|
|
641
|
+
**What is still worth validating at your own boundary,** because it is a list and lists have the
|
|
642
|
+
weakness described above:
|
|
643
|
+
1. **A `string` carrying an active delimiter.** The segment guard passes it, because it is a
|
|
644
|
+
string. Slots that route through the escape helper release it correctly (`"1*BOGUS"` emits as
|
|
645
|
+
`1?*BOGUS`); the fixed-width ISA slots do not go through it at all.
|
|
646
|
+
2. **Whether an `X12Decimal` carries the SCALE you meant.** `fromString("0.3")` and
|
|
647
|
+
`fromString("0.30")` are both accepted and both emit verbatim. That choice is yours.
|
|
648
|
+
3. **Anything `buildTA1` emits**, per the paragraph above.
|
|
649
|
+
4. **`build835`'s balance-equation amounts, which refuse UNTYPED.** `build835` runs its balance
|
|
650
|
+
guard before it builds the escape helper, and that guard calls `X12Decimal` methods on your
|
|
651
|
+
value. So a raw `number` there throws a plain `TypeError` with **no `code`** - some saying the
|
|
652
|
+
value "has no internal state - was it tampered with?", which is a misleading thing to be told
|
|
653
|
+
when you passed a number. **The rule, not a list:** a slot refuses untyped exactly when the
|
|
654
|
+
balance guard reads it as a term of one of the three TR3 X221A1 §1.10.2 invariants (the claim,
|
|
655
|
+
service-line and remit-total equations). **Named by SPEC FIELD rather than element number,
|
|
656
|
+
because a draft that used element numbers got one wrong:** the untyped set is
|
|
657
|
+
`payment.totalActualPayment`, `claim.totalChargeAmount`, `claim.totalPaymentAmount`, every
|
|
658
|
+
`adjustments[].amount` at claim and line level, `serviceLine.chargeAmount`,
|
|
659
|
+
`serviceLine.paymentAmount` and `providerAdjustments[].amount`. Every other `X12Decimal` field
|
|
660
|
+
refuses typed, including `claim.patientResponsibilityAmount`, `serviceLine.paidUnitsOfService`,
|
|
661
|
+
every `amounts[].amount` and every `adjustments[].quantity`. Unchanged from `0.0.9`; `err.code` is still the thing to branch on, and the
|
|
662
|
+
balance terms are the case where there is not one.
|
|
663
|
+
|
|
664
|
+
**Closed in the release after `0.0.9`, and recorded because the behaviour changed for callers who
|
|
665
|
+
were relying on it.** Each of these emitted silently before and refuses now:
|
|
666
|
+
1. **Monetary and quantity slots read `.toString()`, so a raw number passed the check.** Measured
|
|
667
|
+
with `warnings.length === 0` in every case: a `patientResponsibilityAmount` of `0.1 + 0.2`
|
|
668
|
+
emitted `CLP*PT-ACCT-001*1*500.00*450.00*0.30000000000000004*…`, `1e21` emitted `…*1e+21*…`,
|
|
669
|
+
`NaN` emitted `…*NaN*…`, and an 837 service-line `units` of `0.1 + 0.2` emitted
|
|
670
|
+
`SV1*HC:99213*150.00*UN*0.30000000000000004***1`. Two of those three the library **cannot parse
|
|
671
|
+
back** - it rejects exponent notation and `NaN` outright - so they did not round-trip.
|
|
672
|
+
2. **Some string-typed positions never called the escape helper at all**, so a number was emitted
|
|
673
|
+
verbatim with no warning: `build999`'s `envelope.groupControlNumber` (GS-06 / GE-02),
|
|
674
|
+
`envelope.transactionSetControlNumber` (ST-02 / SE-02), `functionalGroup.disposition` (AK9-01)
|
|
675
|
+
and `transactionResponses[].disposition` (IK5-01); `build278`'s `review.levelCode` (HL-03);
|
|
676
|
+
`envelope.groupDate` / `envelope.groupTime` (GS-04 / GS-05) in every domain builder; and
|
|
677
|
+
`build837`'s `serviceLine.lineNumber` (LX-01). **AK9-01 was the one to know about:** it is an
|
|
678
|
+
`ID` element bound to X12 code list 715, so a number there told the receiver nothing about
|
|
679
|
+
whether the functional group was accepted, and the library's own accept-with-errors guard
|
|
680
|
+
compares it against `"A"` and did not fire. These positions also admitted an **unescaped
|
|
681
|
+
delimiter**: `build999` with a `groupControlNumber` of `"1*BOGUS"` emitted
|
|
682
|
+
`GS*FA*…*1*BOGUS*X*005010X231A1`, shifting GS-07 and GS-08 by one. They are routed through the
|
|
683
|
+
escape helper now, so that reads `1?*BOGUS`.
|
|
684
|
+
3. **The fixed-width ISA slots** (`senderId`, `receiverId`, `interchangeControlNumber`, …) go
|
|
685
|
+
through padding rather than escaping, and **are still not covered** - they do not pass through
|
|
686
|
+
the segment join either. A number there throws an untyped `TypeError` with no `code`
|
|
687
|
+
(`value.slice is not a function`), and a numeric `interchangeControlNumber` throws the builder's
|
|
688
|
+
typed refusal with the **misleading** text "exceeds the 9-char spec limit". Both terminate
|
|
689
|
+
rather than emitting silently, which is why they were the smaller hazard and why they are left
|
|
690
|
+
alone. GS-04 and GS-05 were the counter-example that made this distinction worth stating - they
|
|
691
|
+
are envelope elements and they WERE silent. They are covered now; ISA still is not.
|
|
692
|
+
|
|
693
|
+
**One other change, and it is a behaviour change:** the exported `escapeRelease(value, delimiters)`
|
|
694
|
+
now **throws `TypeError` on a non-string** rather than returning `""`. If you call it directly, it
|
|
695
|
+
is a `TypeError` and not a code-tagged library error, because it is a pure text utility with no
|
|
696
|
+
spec context to name. Nothing inside the library can reach it: the builders refuse first. A boxed
|
|
697
|
+
`new String("…")` is also refused now, where it built at `0.0.8`.
|
|
698
|
+
|
|
699
|
+
- **A forged non-array in a builder spec refuses; in a few places it throws an untyped `TypeError`.**
|
|
700
|
+
The types say `readonly T[]`, but a JavaScript or JSON caller can hand a builder something else. As
|
|
701
|
+
of `0.0.6` every indexed loop in every builder takes its bound from a checked array (32 loops across
|
|
702
|
+
7 modules), so an object like `{ length: "9".repeat(120000) }` draws that builder's own typed
|
|
703
|
+
refusal - previously the length coerced to `Infinity` and the builder **looped forever instead of
|
|
704
|
+
refusing**. Measured across the nineteen probes the suite ships: at base **16 hung** and 3 threw an
|
|
705
|
+
untyped `TypeError`; at head **17 refuse with a typed, coded error** and 2 still throw the untyped
|
|
706
|
+
`TypeError`.
|
|
707
|
+
|
|
708
|
+
**`null` is treated as absent, not forged**, exactly as the `?? []` this replaced did, so an
|
|
709
|
+
optional list you send as `null` still builds. On a required list `null` draws that builder's own
|
|
710
|
+
"at least one X is required" refusal instead of an untyped `TypeError` for five of the six:
|
|
711
|
+
`build834`, `build820`, `build837`, `build271` and `build277`. **`build835`'s `claims` is the
|
|
712
|
+
exception** and still throws an untyped `TypeError`, as it did at base.
|
|
713
|
+
|
|
714
|
+
Not covered, and disclosed rather than fixed: the places a builder reads a caller array with
|
|
715
|
+
`for…of` - `buildInterchange`'s `spec.groups`, `build999`'s `functionalGroup.transactionResponses`,
|
|
716
|
+
and every optional leaf array such as `claim.dates` or `member.references`. Those throw
|
|
717
|
+
`TypeError: … is not iterable` immediately. They terminate, so they are not the hang, but they carry
|
|
718
|
+
**no `code`**, so `err.code` is `undefined` and you cannot branch on it. Validate your spec shape at
|
|
719
|
+
your own boundary if it comes from JSON.
|
|
720
|
+
|
|
721
|
+
**One qualification worth stating precisely: on the acknowledgment path the value is not always
|
|
722
|
+
strictly your own.** TR3 005010X231A1 requires AK2-02 to be a verbatim copy of the acknowledged
|
|
723
|
+
transaction set's ST-02, and `buildTA1` exists to echo an inbound ISA-13, so a _document's_ control
|
|
724
|
+
numbers reach those refusals by the standard's own design. They are envelope control numbers rather
|
|
725
|
+
than clinical content, and they are bounded like everything else, but "the value is always the
|
|
726
|
+
caller's own" would be false and this library does not claim it.
|
|
727
|
+
|
|
728
|
+
Logging `err.code` rather than `err.message` remains the safest habit, and the parse side is stronger
|
|
729
|
+
still, where no factory takes a value parameter at all.
|
|
730
|
+
|
|
731
|
+
- **`X12ParseError.snippet` on a Tier-3 fatal can carry PHI, by design, and the library does not
|
|
732
|
+
redact it.** Warning messages come from a frozen registry and no factory takes a value parameter, so
|
|
733
|
+
they cannot echo an element; the four structural fatals are different, because they are raised
|
|
734
|
+
before the envelope is readable and each carries a bounded (≤ 64 character) copy of the start of
|
|
735
|
+
the input so the error is actionable. On real traffic those bytes can be patient data. Redact at
|
|
736
|
+
your call site, or log `err.code` and `err.position` and drop `err.snippet`. A **strict-mode
|
|
737
|
+
escalation carries no snippet**: the error it raises wraps a registry-built warning, so `err.snippet`
|
|
738
|
+
is `""`.
|
|
739
|
+
|
|
740
|
+
- **Two classes of locator-flavoured model field are left unbounded on purpose, and a downstream
|
|
741
|
+
package should not interpolate them.** `X12HierarchicalLevel.hlId` / `.parentHlId` / `.levelCode` /
|
|
742
|
+
`.hasChild` (and the shared `X12Hl`) stay byte-verbatim because collapsing two distinct
|
|
743
|
+
non-conformant HL ids to one sentinel would make them compare equal and silently merge two
|
|
744
|
+
subscribers' claims into one hierarchy; a wrong hierarchy is worse than a wide locator. The 999's
|
|
745
|
+
error report is the other class: `X12Ack999Ak1.functionalIdCode`,
|
|
746
|
+
`X12Ack999Ak2.transactionSetIdCode`, `X12Ack999Ik3.segmentIdCode` / `.loopIdentifier` and
|
|
747
|
+
`X12Ack999Ik4.dataElementReferenceNumber` are the trading partner's report of
|
|
748
|
+
where **they** found a problem, which is the content a 999 exists to deliver. `X12Segment.id` is
|
|
749
|
+
bounded to the X12 segment-id grammar (a non-conformant first element yields
|
|
750
|
+
`NON_SPEC_SEGMENT_ID`) precisely because it is a derived locator with no such argument; the bytes
|
|
751
|
+
stay on `seg.raw` and `seg.elements[0]`. **No warning code is raised for that substitution, and no
|
|
752
|
+
code distinguishes two different non-conformant ids through `seg.id`.** That is deliberate rather
|
|
753
|
+
than an oversight: it bounds a derived field, it is not a parse deviation, and every walker
|
|
754
|
+
dispatches on an exact segment-id match, so a first element outside the grammar could never have
|
|
755
|
+
matched a walker in the first place. Compare `seg.id === NON_SPEC_SEGMENT_ID` when you need to
|
|
756
|
+
detect one, and read `seg.elements[0]` for which one it was.
|
|
757
|
+
|
|
758
|
+
- **`X12Ack999Ik4.copyOfBadDataElement` is a copy of the offending bytes and can carry PHI.** It is
|
|
759
|
+
whatever the sender put in IK4-04; the library never auto-populates it, and senders SHOULD omit it
|
|
760
|
+
when the bytes are PHI.
|
|
761
|
+
|
|
762
|
+
- **Balance and integrity checks warn; they never rebalance or renumber.** The 835 TR3 §1.10.2 balance
|
|
763
|
+
invariants, 837 HL parent-pointer integrity, and envelope-count reconciliation surface a warning on
|
|
764
|
+
mismatch and preserve the inbound values verbatim. The library will not "fix" a payer artifact for
|
|
765
|
+
you. Gate your own posting/adjudication on the warning.
|
|
766
|
+
|
|
767
|
+
## Conformance testing not yet wired
|
|
768
|
+
|
|
769
|
+
- **No external-oracle differential corpus yet.** A best-effort differential harness against CMS
|
|
770
|
+
Medicare 835 public examples (and/or another external X12 reader) is planned for the first real
|
|
771
|
+
release but is **not yet wired**, pending a redistribution-terms review of the CMS sample material.
|
|
772
|
+
Conformance today rests on the three-tier synthetic corpus (spec-clean → vendor-quirk → round-trip
|
|
773
|
+
goldens), property/round-trip tests, and a nightly amplified byte-flip fuzz job, not on parity with
|
|
774
|
+
a third-party implementation. Do not assume byte-for-byte agreement with any specific vendor parser.
|
|
775
|
+
|
|
776
|
+
## Scope (non-goals for v1)
|
|
777
|
+
|
|
778
|
+
- **Healthcare HIPAA 005010 only.** Non-healthcare transaction sets (850/856/810/204, etc.), the
|
|
779
|
+
EDIFACT syntax family, and pre-005010 versions are out of v1 scope. Pre-005010 input is tolerated
|
|
780
|
+
and flagged (`X12_PRE_005010`), not decoded to those older field maps.
|
|
781
|
+
- **No transport.** AS2, SFTP, and MLLP-style delivery are out of scope. This is a parser/serializer,
|
|
782
|
+
not a communications stack.
|
|
783
|
+
- **Published, still pre-alpha.** The package is published on npm as `@cosyte/x12` from a public
|
|
784
|
+
repo, but it stays on the `0.0.x`-until-first-alpha ladder. `npm view @cosyte/x12 version` is the
|
|
785
|
+
only source of truth for the current version, so this page does not restate one. Treat the API as
|
|
786
|
+
pre-alpha and pin the exact version until the first alpha.
|
|
787
|
+
- **No typed model for the 270 and 276 inquiries.** Every other v1 transaction has both a
|
|
788
|
+
per-transaction reader and a domain builder. The 270 eligibility inquiry and the 276 claim-status
|
|
789
|
+
inquiry have neither: they parse into segments, composites, and dot-paths like any other X12 input,
|
|
790
|
+
and the responses (271, 277) decode fully, but the inquiry directions have no typed surface yet.
|
|
791
|
+
|
|
792
|
+
## Code-list `--fetch` regeneration
|
|
793
|
+
|
|
794
|
+
`pnpm refresh:code-lists` (default) validates the bundled snapshots and prints a freshness audit,
|
|
795
|
+
offline and CI-safe. The `--fetch` mode that would **regenerate** the full lists from their canonical
|
|
796
|
+
WPC / X12 sources is deliberately **not** run in automation: redistributing the full WPC code
|
|
797
|
+
descriptions requires a redistribution-terms review that has not cleared, and it needs outbound
|
|
798
|
+
network. The tool prints the canonical source manifest and exits rather than fabricating descriptions
|
|
799
|
+
the maintainers have not reviewed.
|
|
800
|
+
|
|
801
|
+
---
|
|
802
|
+
|
|
803
|
+
For the phase-by-phase surface and the exact fields each helper decodes, see the package's
|
|
804
|
+
[`CLAUDE.md`](./CLAUDE.md) status section and the [Cookbook](./docs-content/cookbook.md).
|