@cosyte/x12 0.0.9 → 0.0.10
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 +336 -4
- package/dist/index.cjs +360 -171
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +241 -7
- package/dist/index.d.ts +241 -7
- package/dist/index.mjs +357 -172
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
11
|
|
|
12
|
+
- **🩺 `X12ServiceLineStatus.unitsOfService` and `Build277ServiceLineSpec.unitsOfService`, the 277
|
|
13
|
+
Loop 2220 SVC-07 units of service count** (`X12-277-SVC07-NOT-DECODED`). X12 element 380
|
|
14
|
+
(Quantity), decoded as an `X12Decimal` like every other quantity in this library and never via
|
|
15
|
+
`parseFloat`. It had no representation on either side before, which is why an X212 277 this
|
|
16
|
+
library emitted was short a required element; see `### Fixed`. `undefined` still means **not
|
|
17
|
+
decoded** rather than absent, and an SVC-07 that is present but does not decode raises
|
|
18
|
+
`X12_UNPARSEABLE_DECIMAL` at `position.elementIndex` 7 while an absent one raises nothing. **SVC-05
|
|
19
|
+
stays unread on the 277 on purpose:** it is usage N in both 277 TR3s, unlike the 835 where it is
|
|
20
|
+
the Units of Service **Paid** Count. The warning registry is unchanged at 25 codes plus 4 Tier-3
|
|
21
|
+
fatals.
|
|
22
|
+
|
|
23
|
+
- **🩺 `X12_837_SERVICE_LINE_DROPPED`, the 25th Tier-2 warning code, plus the public factory
|
|
24
|
+
`serviceLineDropped(position)`** (`X12-VARIANT-LOOKUP-PROTOTYPE`). Raised when an 837 `LX` opens
|
|
25
|
+
no Loop 2400 at all, so the service line reaches **no claim's** `serviceLines` - either because no
|
|
26
|
+
`CLM` is open at that point in the walk, or because the submission's variant never resolved to
|
|
27
|
+
`P` / `I` / `D`. Distinct from `X12_837_SERVICE_LINE_NOT_DECODED`, where the line is retained and
|
|
28
|
+
only its service segment went unread. `position.segmentIndex` names the `LX`, the same anchor and
|
|
29
|
+
for the same reason: it is the one segment present in every case. Nothing is fabricated to stand
|
|
30
|
+
in for the missing line and no claim is synthesized; the segments stay verbatim on `tx.segments`.
|
|
31
|
+
The registry stays additions-only.
|
|
32
|
+
|
|
33
|
+
- **🩺 `X12_837_SERVICE_LINE_NOT_DECODED`, the 24th Tier-2 warning code, plus the public factory
|
|
34
|
+
`serviceLineNotDecoded(position)`** (`X12-837-SV-SILENT-ZERO`). Raised when an 837 Loop 2400
|
|
35
|
+
service line is closed without ever having decoded an `SV1` / `SV2` / `SV3` for the variant the
|
|
36
|
+
submission resolved to; see `### Fixed` for what that line was reporting instead.
|
|
37
|
+
`position.segmentIndex` names the `LX` that opened the line rather than the `SVx`, because the
|
|
38
|
+
no-`SVx`-at-all case has no `SVx` to point at. The registry stays additions-only.
|
|
39
|
+
|
|
40
|
+
- **🩺 `X12_UNPARSEABLE_DECIMAL`, the 23rd Tier-2 warning code, plus `readElementDecimal` and
|
|
41
|
+
`X12DecimalWarningSink`** (`X12-QUANTITY-SILENT-DEFAULTS`). A decimal element that is **present**
|
|
42
|
+
and is not an X12 R-type decimal now says so; see `### Fixed` for what it was doing instead.
|
|
43
|
+
`readElementDecimal(seg, n, delimiters)` is the pure primitive underneath both existing helpers and
|
|
44
|
+
returns `{ value, status }` with `status` one of `"decoded"` / `"absent"` / `"unparseable"`, which
|
|
45
|
+
is the distinction a bare `undefined` could not carry. `elementDecimal` and `elementDecimalOrZero`
|
|
46
|
+
each gained an optional 4th `X12DecimalWarningSink` argument (`{ warnings, position }`); the helper
|
|
47
|
+
narrows the position to the failing `elementIndex` itself, so a caller passes the position of the
|
|
48
|
+
segment and never has to remember to do it per element. Existing 3-argument calls still compile and
|
|
49
|
+
are still silent, on purpose.
|
|
50
|
+
|
|
51
|
+
- **`X12RemitServiceLine.originalUnitsOfService` / `Build835ServiceLineSpec.originalUnitsOfService`**,
|
|
52
|
+
the 835 SVC-07 Original Units of Service Count: the units as **submitted**, which a payer sends only
|
|
53
|
+
when they differ from the paid count in SVC-05. It is not a convenience field - without it the
|
|
54
|
+
corrected SVC map (see `### Changed`) would have left SVC-07 unread and unwritten, converting a
|
|
55
|
+
mis-read into a fresh silent drop. `undefined` means "same as paid", not "zero submitted".
|
|
56
|
+
|
|
12
57
|
- **`requireCallerString` / `makeCallerEscaper`** (internal), the single route a caller-supplied
|
|
13
58
|
element value takes **through a builder's `esc` helper**. Read that scope literally: it is not
|
|
14
59
|
every route into an emitted segment, and the `### Fixed` entry below says which positions bypass
|
|
@@ -53,6 +98,71 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
53
98
|
|
|
54
99
|
### Changed
|
|
55
100
|
|
|
101
|
+
- **🩺 BREAKING: `build277` now REFUSES a Loop 2220 service line that carries no `unitsOfService`
|
|
102
|
+
(`X12-277-SVC07-NOT-DECODED`).** SVC-07 is usage **R** in TR3 `005010X212`, so a service line
|
|
103
|
+
without it cannot be emitted as a conformant 277. The refusal is the existing
|
|
104
|
+
`ClaimStatus277BuildError` with code `X12_277_BUILD_INVALID_SPEC`, raised as a precondition before
|
|
105
|
+
any segment is built, and its message names `SVC-07`, the TR3, and the structural locator of the
|
|
106
|
+
offending line (indices only, never a member id, name, trace or claim id). **`build277CA` is
|
|
107
|
+
deliberately unaffected:** in `005010X214` the same element is usage **S**, so the identical spec
|
|
108
|
+
still builds and simply omits the element. **The count is never defaulted in either direction** -
|
|
109
|
+
a quantity the caller did not supply is a quantity nobody sent, and a units figure is one a payer
|
|
110
|
+
reprices against. If you emit X212 277s with service lines, the first build after upgrading throws
|
|
111
|
+
until you supply the submitted count. Usage taken from the pyx12 005010 maps
|
|
112
|
+
(`277.5010.X212.xml`, `277.5010.X214.xml`), outside this repository, rather than from this
|
|
113
|
+
library's own reader.
|
|
114
|
+
|
|
115
|
+
- **The committed canonical 277 fixture and its serializer golden now carry SVC-07**
|
|
116
|
+
(`test/fixtures/status/277-canonical.edi`, `test/fixtures/golden/277.edi`). The X212 fixture was
|
|
117
|
+
itself short the required element, which is part of why nothing in the suite noticed. The other
|
|
118
|
+
twelve goldens regenerate byte-identically.
|
|
119
|
+
|
|
120
|
+
- **🩺 BREAKING: the 835 Loop 2110 SVC element map is corrected, in both directions
|
|
121
|
+
(`X12-SVC-ELEMENT-MAP-OFF-BY-ONE`).** Through `0.0.9` `get835` read `revenueCode` from **SVC-05**
|
|
122
|
+
and `paidUnitsOfService` from **SVC-07**, and `build835` wrote them to those same two positions
|
|
123
|
+
while hard-coding SVC-04 empty behind a comment asserting "revenue code is SVC-05 in X221A1;
|
|
124
|
+
SVC-04 unused". That comment was wrong. SVC-04 is the NUBC revenue code (X12 element 234, a
|
|
125
|
+
string), SVC-05 is the Units of Service **Paid** Count (element 380, a Quantity), and SVC-07 is
|
|
126
|
+
the **Original** Units of Service Count (element 380) - a different quantity, sent only when the
|
|
127
|
+
submitted count differs from the paid one. `revenueCode` now reads and writes **SVC-04** and
|
|
128
|
+
`paidUnitsOfService` now reads and writes **SVC-05**.
|
|
129
|
+
|
|
130
|
+
**The harm was a mis-read code system and a mis-read quantity, in both directions, silently.**
|
|
131
|
+
Measured across the six committed remit fixtures plus the golden, **8 of 8 service lines** read
|
|
132
|
+
back `revenueCode: "1"` at `0.0.9` - `1` is not a valid NUBC revenue code, it is the paid-unit
|
|
133
|
+
count from SVC-05 - while `paidUnitsOfService` came back `undefined` because SVC-07 was absent.
|
|
134
|
+
On emit, a line with revenue code `0300` and 2 paid units produced
|
|
135
|
+
`SVC*HC:99213*600.00*550.00**0300*HC:99212*2`, putting a revenue code into a **Quantity**
|
|
136
|
+
element, so a conformant receiver read `0300` as 300 units of service. It now produces
|
|
137
|
+
`SVC*HC:99213*600.00*550.00*0300*2*HC:99212`.
|
|
138
|
+
|
|
139
|
+
**Nothing in the suite could detect this and that is why it shipped.** Every existing assertion
|
|
140
|
+
was a `build835` -> `get835` round trip, which is green for any pair of positions the two modules
|
|
141
|
+
agree on. All 1,227 tests stayed green with the fix applied. The map is now pinned against literal
|
|
142
|
+
bytes in `test/transactions-remit-835-svc-element-map.test.ts`; all 11 of its cases fail on
|
|
143
|
+
`e3cdf49` and pass at head.
|
|
144
|
+
|
|
145
|
+
**The repo already contradicted itself:** `build277` / `get277Status` read and write the revenue
|
|
146
|
+
code at SVC-04, `build-277-types.ts` says so in prose, and every committed 835 fixture is written
|
|
147
|
+
to the correct map (`SVC*…**1` is an empty SVC-04 and one unit paid). Only the 835 module
|
|
148
|
+
disagreed, and only with itself.
|
|
149
|
+
|
|
150
|
+
**Sources, and what was not read. TR3 005010X221A1 is a paid X12 document; nobody here has read
|
|
151
|
+
it**, and every claim is traceable to something publicly checkable: pyx12's machine-readable
|
|
152
|
+
`835.5010.X221.A1.xml`, an independent open-source implementation of the same guide, which carries
|
|
153
|
+
the whole table and is **the source for SVC-04**; X12's own RFI #2163 for SVC-05; the base 005010
|
|
154
|
+
element dictionary, where SVC-04 is a string and SVC-05/07 are Quantities, which rules out a
|
|
155
|
+
revenue code at SVC-05 on type alone; and two published payer companion guides. **Agreement with
|
|
156
|
+
this repo's own 277 modules is corroborating, not a source** - checking a spec claim against the
|
|
157
|
+
implementation only proves the two agree, which is how the wrong map survived. Listed with links
|
|
158
|
+
in `KNOWN-LIMITATIONS.md`.
|
|
159
|
+
|
|
160
|
+
**Also disclosed rather than fixed:** `undefined` on either quantity means "not decoded", not
|
|
161
|
+
"absent" - the element may have been present and unparseable, which raises no warning and is
|
|
162
|
+
pre-existing at every quantity site. And **835s this library emitted at `0.0.9` or earlier are
|
|
163
|
+
non-conformant on the wire and should be re-emitted**: their revenue code sits in SVC-05, so this
|
|
164
|
+
release reads it back as a paid quantity and reports no revenue code.
|
|
165
|
+
|
|
56
166
|
- **`escapeRelease` now throws `TypeError` on a non-string instead of returning `""`.** Previously it
|
|
57
167
|
gave three different wrong answers depending on what arrived: a number, a boolean or a plain object
|
|
58
168
|
returned the empty string silently; `null` and `undefined` threw on the property read; an array or
|
|
@@ -130,6 +240,151 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
130
240
|
|
|
131
241
|
### Fixed
|
|
132
242
|
|
|
243
|
+
- **🩺 An X212 277 this library emitted with a service line was short a required element, and every
|
|
244
|
+
277 it read silently discarded the submitted units** (`X12-277-SVC07-NOT-DECODED`). `get277Status`
|
|
245
|
+
read SVC-01 through SVC-04 and stopped; `build277` emitted exactly those four. SVC-07, the units of
|
|
246
|
+
service count, is usage **R** in `005010X212`, so **every X212 277 this library produced with a
|
|
247
|
+
Loop 2220 service line was non-conformant on the wire**, and the count a submitter sent never
|
|
248
|
+
reached the model on the way back in. Both directions now carry it, and `build277` refuses rather
|
|
249
|
+
than emit a line without it (see `### Changed`).
|
|
250
|
+
|
|
251
|
+
**Only bytes could have caught this.** Every service-line assertion in the suite was a `build277`
|
|
252
|
+
to `get277Status` round trip through one self-consistent four-element map, so it was green for any
|
|
253
|
+
subset the two modules agreed on, including a subset missing a required element. A round trip
|
|
254
|
+
cannot test an element map, and it cannot test an element usage at all. The new pins in
|
|
255
|
+
`test/transactions-status-277-svc07.test.ts` parse literal EDI and compare literal segment strings:
|
|
256
|
+
**16 of its 21 cases are red against a clean `c34770c` checkout**, and the five that are green are
|
|
257
|
+
the negative pins that were already true (SVC-05 stays unread, an absent SVC-07 warns nothing, a
|
|
258
|
+
277CA line with no units emits no placeholder).
|
|
259
|
+
|
|
260
|
+
**🩺 Read the scope literally: ONE element's usage was fixed, and an emitted service line is NOT
|
|
261
|
+
thereby conformant.** This was not a 277 usage audit and **no census of what remains is published**,
|
|
262
|
+
on purpose: other required elements of the same `SVC` are still unguarded and finding another is
|
|
263
|
+
expected rather than a new defect. To name two, `SVC-01` and `SVC-02` are both usage **R** in X212
|
|
264
|
+
and both optional on `Build277ServiceLineSpec`, so a line supplying only `unitsOfService` still
|
|
265
|
+
emits `SVC*******1~` with no refusal. **The missing guard is what is pre-existing, not that byte
|
|
266
|
+
string** - at base the same spec emitted a bare `SVC~`, because the slot did not exist. `SVC-03` is usage **R**
|
|
267
|
+
in X212 and usage **N** in X214 and is optional in both builders. And the read side still raises
|
|
268
|
+
**no** warning for an X212 277 that arrives with no SVC-07, because a lenient reader saying so
|
|
269
|
+
needs a new Tier-2 registry code. All of it is pre-existing and reproduces at `e3cdf49`; widening
|
|
270
|
+
the guard would have turned this into that audit, which is its own item.
|
|
271
|
+
`KNOWN-LIMITATIONS.md` carries the same statement.
|
|
272
|
+
|
|
273
|
+
- **🩺 A lookup keyed by document bytes can no longer be defeated by a key inherited from
|
|
274
|
+
`Object.prototype` (`X12-VARIANT-LOOKUP-PROTOTYPE`).** Several lookup tables were plain object
|
|
275
|
+
literals, which inherit `Object.prototype`, so an inbound value matching **any own property of
|
|
276
|
+
`Object.prototype`** resolved TRUTHY against them. That set is engine- and version-dependent and
|
|
277
|
+
is deliberately not enumerated here; on the Node 22 this package targets it is twelve members, and
|
|
278
|
+
a draft of this entry listed eight. `Object.freeze` did not help: it seals the own properties and
|
|
279
|
+
changes nothing about the prototype chain. What that cost, measured at `a33c208`:
|
|
280
|
+
- **🩺 An ST-03 of `constructor` took every 837 service line off the model, silently.**
|
|
281
|
+
`VARIANT_BY_ICR` answered the `Object` constructor, so `submission.variant` was a **function**
|
|
282
|
+
rather than one of `P` / `I` / `D` / `unknown`, `X12_837_UNKNOWN_VARIANT` never fired, and
|
|
283
|
+
`openServiceLine` - which answers `undefined` for anything that is not a known variant - dropped
|
|
284
|
+
**every Loop 2400**, its charge, its units and its procedure code, with `warnings: []`.
|
|
285
|
+
- **🩺 `lookupCarc("constructor")` answered a `CodeListEntry` whose `description` was a function**,
|
|
286
|
+
on a field typed `string`, and suppressed `X12_UNKNOWN_CARC`. The guard is now in `makeLookup`,
|
|
287
|
+
the single factory every bundled snapshot is read through, so the other bundled lists are
|
|
288
|
+
covered by the same fix.
|
|
289
|
+
- **🩺 An HI qualifier of `constructor` suppressed `X12_UNKNOWN_HI_QUALIFIER`** while still
|
|
290
|
+
landing the code with `codeSystem: "unknown"` - an unresolvable code system on a diagnosis, with
|
|
291
|
+
nothing on any channel to say so.
|
|
292
|
+
- **An HL-03 of `constructor` raised `X12_HL_PARENT_LEVEL_INVALID`** against a level code the
|
|
293
|
+
walker has no expectation for: a structural violation the document never made. The 271 / 277 /
|
|
294
|
+
278 readers were never exposed - they share `src/transactions/shared/hl.ts`, which has always
|
|
295
|
+
guarded this read with `hasOwnProperty`; the 837's local copy did not.
|
|
296
|
+
- **`isClaimAdjustmentGroupCode("constructor")` answered `true`** and narrowed a non-code to
|
|
297
|
+
`ClaimAdjustmentGroupCode`. It used `in`, which **walks the prototype chain**: the safe-looking
|
|
298
|
+
form, not the safe form. It is now `Object.hasOwn`.
|
|
299
|
+
|
|
300
|
+
Every one of these now behaves exactly as it does for any other unrecognized value, which is the
|
|
301
|
+
whole of the claim: each fixed case is pinned against an honest-control case in the same slot, and
|
|
302
|
+
the suite derives its key list from `Object.getOwnPropertyNames(Object.prototype)` at run time
|
|
303
|
+
rather than from a list anyone maintains.
|
|
304
|
+
Tables this package declares itself are built through the new internal
|
|
305
|
+
`wireLookup` (`Object.create(null)`, then frozen), so the absent prototype protects future read
|
|
306
|
+
sites too; tables it receives from a caller are guarded with `Object.hasOwn` at the read, the form
|
|
307
|
+
`shared/hl.ts` already used. **No model shape and no message text changed**, and no code-list
|
|
308
|
+
content changed.
|
|
309
|
+
|
|
310
|
+
- **🩺 An 837 `LX` that opens no Loop 2400 no longer drops the whole service line in silence
|
|
311
|
+
(`X12-VARIANT-LOOKUP-PROTOTYPE`).** Two routes reached it: an `LX` arriving before any `CLM`,
|
|
312
|
+
which produced `claims: []` and `warnings: []` even with a charge and units on the wire; and a
|
|
313
|
+
submission whose variant is not one of `P` / `I` / `D`, where `openServiceLine` answers
|
|
314
|
+
`undefined` and the line was never opened. Both now raise `X12_837_SERVICE_LINE_DROPPED` at the
|
|
315
|
+
`LX`. **Retention is unchanged and nothing is invented**: no claim is synthesized to hold an
|
|
316
|
+
orphan line and no variant is guessed, because either would put structure on the model that the
|
|
317
|
+
sender did not send. The segments were, and remain, verbatim on `tx.segments`.
|
|
318
|
+
**Read the scope of this code literally, because three things sit just outside it and all three
|
|
319
|
+
are unchanged.** It is anchored at the `LX`, so an `SVx` arriving with **no `LX` at all** is still
|
|
320
|
+
dropped in silence (`PRE-EXISTING`, disclosed not fixed). It does **not** travel with
|
|
321
|
+
`X12_837_UNKNOWN_VARIANT`: a caller-supplied `type` outside `"P" | "I" | "D"`, which only a
|
|
322
|
+
JavaScript or `JSON.parse`d caller can pass, reaches the second route without it, so read
|
|
323
|
+
`submission.variant` to tell the routes apart. And what becomes of a `DTP` / `AMT` / `NTE` /
|
|
324
|
+
`REF` following a dropped `LX` is route-dependent and is **not** simply "absent": see
|
|
325
|
+
`KNOWN-LIMITATIONS.md`, which states both routes.
|
|
326
|
+
|
|
327
|
+
- **🩺 An 837 service line whose `SVx` never decoded no longer reads `0` / `0` in silence
|
|
328
|
+
(`X12-837-SV-SILENT-ZERO`).** `get837Claims` resolves ONE variant for the whole submission (the
|
|
329
|
+
caller's `type` option, else ST-03's implementation-convention reference, else the first `SVx`
|
|
330
|
+
present). `openServiceLine` seeds a line's `charge` and `units` at `X12Decimal.ZERO`, and
|
|
331
|
+
`decodeSv1` / `decodeSv2` / `decodeSv3` each **return before reading anything** when the line's
|
|
332
|
+
variant is not theirs. So an `SV2` line on a submission that resolved to Professional read back as
|
|
333
|
+
a `$0.00` charge for `0` units with `warnings: []`, with the procedure code, modifiers, unit of
|
|
334
|
+
measure and place of service equally undecoded; a line carrying no `SVx` at all did the same. This
|
|
335
|
+
is the residual `X12-QUANTITY-SILENT-DEFAULTS` disclosed rather than fixed, and it is the same
|
|
336
|
+
fabrication one level up: a slot a reader never read cannot warn on the decimal channel.
|
|
337
|
+
|
|
338
|
+
Measured against `d8b5085`: a conformant 837I whose ST-03 is flipped to `005010X222A2` read back
|
|
339
|
+
`0` / `0` on both service lines with `warnings: []`, where the same bytes read with their own
|
|
340
|
+
variant give `1500` / `1`. Three further probes were equally silent at base (the same file with
|
|
341
|
+
`{ type: "P" }`, an `LX` with no `SVx` at all, and the 837D fixture with `{ type: "I" }`). All four
|
|
342
|
+
now raise `X12_837_SERVICE_LINE_NOT_DECODED`; the honest 837P and 837I controls stay silent.
|
|
343
|
+
|
|
344
|
+
**Not decoding the foreign `SVx` is unchanged and is not the defect.** `SV1-02` and `SV2-03` are
|
|
345
|
+
both the line charge, so reading an `SV2` into a Professional-shaped line would mis-read money
|
|
346
|
+
rather than fail to read it. **The model is unchanged too:** `charge` and `units` are still typed
|
|
347
|
+
`X12Decimal` and still read `0`, so a consumer that never looks at `.warnings` sees what it saw
|
|
348
|
+
before. Making those slots `X12Decimal | undefined` remains a breaking change and its own slice.
|
|
349
|
+
The line is still retained, every segment stays verbatim on `tx.segments`, and a variant that
|
|
350
|
+
resolves to nothing at all is still the separate `X12_837_UNKNOWN_VARIANT` case, where no line is
|
|
351
|
+
opened and no `0` is fabricated. `KNOWN-LIMITATIONS.md` says how far that second case reaches.
|
|
352
|
+
|
|
353
|
+
- **🩺 An unparseable decimal no longer becomes a confident zero in silence
|
|
354
|
+
(`X12-QUANTITY-SILENT-DEFAULTS`).** `elementDecimalOrZero` returned `X12Decimal.ZERO` for an
|
|
355
|
+
element that was **present** and did not decode, with no warning on any channel. A payer amount of
|
|
356
|
+
`1,234.56` (a thousands separator, which X12 forbids in an R-type element), `$450.00`, `450.00USD`
|
|
357
|
+
or `N/A` read back as `0` and was indistinguishable from a payer that paid nothing. A fabricated
|
|
358
|
+
amount presented as read is the same harm class as a mis-read quantity: a number nobody sent,
|
|
359
|
+
arriving as though somebody had.
|
|
360
|
+
|
|
361
|
+
The same root cause one type away: `elementDecimal` answered `undefined` for **both** "the sender
|
|
362
|
+
omitted this element" and "the sender sent bytes this library could not read", also unwarned, so
|
|
363
|
+
`undefined` at a quantity site meant "not decoded" rather than "absent" and no consumer could tell
|
|
364
|
+
which.
|
|
365
|
+
|
|
366
|
+
Every decimal read in all six transaction readers (835, 837P/I/D, 277 / 277CA, 271, 834, 820) now
|
|
367
|
+
routes through a sink and emits `X12_UNPARSEABLE_DECIMAL` at the failing `position.elementIndex`.
|
|
368
|
+
Measured on nine probes across those readers, one per site class, each substituting a single
|
|
369
|
+
numeric token in a committed fixture: at `0.0.9` **seven of the nine were completely silent**, and
|
|
370
|
+
the two that were not were the 835's `BPR-02` and `CLP-04`, which produced only
|
|
371
|
+
`X12_835_REMIT_BALANCE_MISMATCH` - a warning that names an equation, never an element, and that
|
|
372
|
+
exists in no other reader. All nine now carry `X12_UNPARSEABLE_DECIMAL` naming the element.
|
|
373
|
+
|
|
374
|
+
**What did NOT change, deliberately.** The model is unchanged: a slot typed `X12Decimal` still
|
|
375
|
+
reads `X12Decimal.ZERO`, an optional slot still reads `undefined`, and some rows are dropped whole.
|
|
376
|
+
The warning is a property of the READ rather than of what the reader then does with the result, so
|
|
377
|
+
every one of those outcomes carries it; no list of them is published, because a first draft
|
|
378
|
+
enumerated three and a review measured a fourth. A slot typed `X12Decimal` cannot express "did not
|
|
379
|
+
decode", and changing every such slot to `X12Decimal | undefined` is a breaking model change that
|
|
380
|
+
belongs in its own slice. So a consumer that reads only the model and never looks at `.warnings`
|
|
381
|
+
sees exactly what it saw before. **Gate on the warning.** Also unchanged: an **absent** element
|
|
382
|
+
still returns `X12Decimal.ZERO` and still does **not** warn, because "missing means zero" is the
|
|
383
|
+
documented convention of those slots. That does **not** make every unwarned `0` trustworthy: the
|
|
384
|
+
guarantee is exactly that an unwarned `0` **at an element a reader decoded** is a zero the sender
|
|
385
|
+
sent or omitted, and a slot a reader never read cannot warn. `KNOWN-LIMITATIONS.md` carries the
|
|
386
|
+
residual and the one measured instance of that inversion in full.
|
|
387
|
+
|
|
133
388
|
- **A number passed where a builder's types say `string` no longer emits an EMPTY element. It is now
|
|
134
389
|
REFUSED, and it is deliberately not coerced.** `escapeRelease` opened with
|
|
135
390
|
`if (value.length === 0) return value;` and then looped to `value.length`. On a number `.length` is
|
|
@@ -169,9 +424,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
169
424
|
`TypeError`, so its duty is to survive anything; an emitted document's duty is to invent nothing.
|
|
170
425
|
|
|
171
426
|
**The guard is on values routed through the escape helper, and not every element position goes
|
|
172
|
-
through it.** All of what follows
|
|
173
|
-
`
|
|
174
|
-
|
|
427
|
+
through it.** All of what follows was pre-existing, measured and unchanged here; it is **closed by
|
|
428
|
+
the `X12-DECIMAL-BYPASSES-THE-GUARD` entry below**, in the same unreleased window, so the residual
|
|
429
|
+
described here never reached a published version. **`esc` slots read `.toString()` off what the
|
|
430
|
+
types say is an `X12Decimal`**, so a raw number arrives already a string and is passed through: a
|
|
175
431
|
`patientResponsibilityAmount` of `0.1 + 0.2` still emits `…*0.30000000000000004*…`, `1e21` still
|
|
176
432
|
emits `…*1e+21*…` and `NaN` still emits `…*NaN*…`, each with zero warnings, which are the exact
|
|
177
433
|
three renderings this entry names as disqualifying, and an 837 service-line `units` reaches SV1-04
|
|
@@ -183,7 +439,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
183
439
|
`levelCode` (HL-03), `groupDate` / `groupTime` (GS-04 / GS-05), and the 837's `lineNumber`
|
|
184
440
|
(LX-01). **Those are examples and not a census, deliberately:** two drafts of this
|
|
185
441
|
entry published an exhaustive count and adversarial review measured both incomplete, so the claim
|
|
186
|
-
is cut back rather than grown a third time. AK9-01 is an `ID` element bound to X12 code
|
|
442
|
+
is cut back rather than grown a third time. AK9-01 is an `ID` element bound to X12 code list 715,
|
|
187
443
|
and `build999`'s own accept-with-errors guard compares it against `"A"`, so a number walks past it
|
|
188
444
|
the same way it walked past `patientControlNumber === ""`. **The fixed-width ISA slots** go through
|
|
189
445
|
`pad` / `padControl`, so a number throws an untyped `TypeError` and a numeric
|
|
@@ -194,6 +450,82 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
194
450
|
`new String("PT-ACCT-001")` built cleanly at `0.0.8` and is refused now, because `typeof` it is
|
|
195
451
|
`"object"`.
|
|
196
452
|
|
|
453
|
+
- **A raw `number` in an `X12Decimal` slot is refused instead of rendered, and the type check now
|
|
454
|
+
covers every element of every segment emitted through a builder's segment joiner**
|
|
455
|
+
(`X12-DECIMAL-BYPASSES-THE-GUARD`). This closes the
|
|
456
|
+
two classes the entry above disclosed and deliberately did not fix.
|
|
457
|
+
|
|
458
|
+
**The decimal half.** `makeCallerEscaper` type-checks what reaches `esc`, but an `X12Decimal` slot
|
|
459
|
+
hands `esc` a `value.toString()`, and a raw `number` answers that with a perfectly good string. So
|
|
460
|
+
the value arrived already a string and the guard never applied. Measured at `15abbd4` with
|
|
461
|
+
`warnings.length === 0` in every case: a `patientResponsibilityAmount` of `0.1 + 0.2` emitted
|
|
462
|
+
`CLP*PT-ACCT-001*1*500.00*450.00*0.30000000000000004*…`, `1e21` emitted `…*1e+21*…`, `NaN` emitted
|
|
463
|
+
`…*NaN*…`, an 837 service-line `units` of `0.1 + 0.2` emitted
|
|
464
|
+
`SV1*HC:99213*150.00*UN*0.30000000000000004***1`, and a diagnosis `monetaryAmount` reached
|
|
465
|
+
`HI*ABK:J20.9:::0.30000000000000004`. **Two of those three renderings the library cannot parse
|
|
466
|
+
back** - `X12_DECIMAL_RE` rejects exponent notation and `NaN` - so they did not round-trip; the
|
|
467
|
+
IEEE-754 artifact is worse in the other direction, being well-formed enough that nothing downstream
|
|
468
|
+
refuses it. Every such slot now emits through that builder's `escDec`, over `requireCallerDecimal`.
|
|
469
|
+
|
|
470
|
+
**Why refuse and not round, which is the decision:** rounding `0.1 + 0.2` to `0.30` guesses cents
|
|
471
|
+
and to `0.3` guesses tenths, and guessing the scale of a monetary amount is what `X12Decimal` exists
|
|
472
|
+
to prevent. Same answer as the entry above, for a reason specific to this slot: every one of these
|
|
473
|
+
is _typed_ `X12Decimal` already, so a raw `number` is a caller who defeated their own type checker
|
|
474
|
+
and no supported numeric path is taken away.
|
|
475
|
+
|
|
476
|
+
**The raw-slot half.** The string-typed positions that never called the escape helper at all are
|
|
477
|
+
routed through it: `build999`'s `groupControlNumber` (GS-06 / GE-02), `transactionSetControlNumber`
|
|
478
|
+
(ST-02 / SE-02), `disposition` (AK9-01 and IK5-01) and `groupResponsibleAgency` (GS-07);
|
|
479
|
+
`groupDate` / `groupTime` (GS-04 / GS-05) in all seven domain builders; `build278`'s `levelCode`
|
|
480
|
+
(HL-03); and `build837`'s `lineNumber` (LX-01). That closes the delimiter hole on them too -
|
|
481
|
+
`build999` with a `groupControlNumber` of `"1*BOGUS"` emitted `GS*FA*…*1*BOGUS*X*005010X231A1`,
|
|
482
|
+
shifting GS-07 and GS-08 by one, and now emits `1?*BOGUS`.
|
|
483
|
+
|
|
484
|
+
**And the part that is a property rather than a list.** Three consecutive drafts of the entry above
|
|
485
|
+
published an exhaustive counted census of the slots that bypass `esc`, and adversarial review
|
|
486
|
+
measured all three false, each time by finding one more. Counting a fourth time would repeat that,
|
|
487
|
+
so the check moved to the one place every element must pass: **the segment join**.
|
|
488
|
+
`requireCallerSegment` type-checks every element of every segment emitted **through a builder's
|
|
489
|
+
`seg` / `joinSeg` helper**, on every route in, `escDec` included. `esc` is optional on a slot; the
|
|
490
|
+
join is not. It also names the slot the way the spec does - `build999: "AK9"-01 must be a
|
|
491
|
+
string, …` - which `esc` cannot, being unary.
|
|
492
|
+
|
|
493
|
+
**What is deliberately still NOT claimed.** Type safety is structural here; **delimiter safety is
|
|
494
|
+
per-slot**. A `string` carrying an active delimiter in a slot that skipped `esc` is still emitted
|
|
495
|
+
verbatim, because the segment guard passes it - only the slots named above were routed. And the
|
|
496
|
+
fixed-width ISA slots go through `pad` / `padControl` and not through the segment joiner either, so
|
|
497
|
+
they remain as the entry above describes them: an untyped `TypeError`, or for
|
|
498
|
+
`interchangeControlNumber` a typed refusal whose text misleadingly says "exceeds the 9-char spec
|
|
499
|
+
limit". Both terminate; neither is silent; neither is improved here.
|
|
500
|
+
|
|
501
|
+
**Two more exclusions, both found by the refuter against a draft that claimed more than the code
|
|
502
|
+
did, both `PRE-EXISTING` and both now pinned by tests rather than argued away.** `buildTA1` uses no
|
|
503
|
+
segment joiner and no escape helper - it joins its five caller-supplied elements directly - so a
|
|
504
|
+
numeric or `undefined` `interchangeControlNumber` still emits silently as `TA1**250101*1200*A*000`;
|
|
505
|
+
TA1-01 is the reassociation key back to the acknowledged interchange, so it is filed as its own
|
|
506
|
+
item rather than widened into here. And **`build835`'s balance-equation amounts refuse UNTYPED**:
|
|
507
|
+
`enforceBalance(spec)` runs before the escaper is built and calls `X12Decimal` methods on the
|
|
508
|
+
caller's value, so `requireCallerDecimal` is unreachable on them and the caller gets a plain
|
|
509
|
+
`TypeError` with no `code` (some saying the value was "tampered with") instead of the typed
|
|
510
|
+
refusal. **The rule, rather than a list, because a first draft of this disclosure published a
|
|
511
|
+
closed list of four and a refuter measured it incomplete:** a slot refuses untyped exactly when the
|
|
512
|
+
balance guard reads it as a term of one of the three TR3 X221A1 §1.10.2 invariants in
|
|
513
|
+
`src/transactions/remit/balance.ts`. **Named by SPEC FIELD and not by element number, because the
|
|
514
|
+
next draft used element numbers and got one wrong:** `payment.totalActualPayment`,
|
|
515
|
+
`claim.totalChargeAmount`, `claim.totalPaymentAmount`, every `adjustments[].amount` at claim and
|
|
516
|
+
line level, `serviceLine.chargeAmount`, `serviceLine.paymentAmount` and
|
|
517
|
+
`providerAdjustments[].amount`. Every other `X12Decimal` field refuses typed, including
|
|
518
|
+
`claim.patientResponsibilityAmount`, `serviceLine.paidUnitsOfService` and every `amounts[].amount`.
|
|
519
|
+
Both arms are pinned. Reordering the balance guard changes the refusal precedence of an
|
|
520
|
+
out-of-balance remit, which is its own decision.
|
|
521
|
+
|
|
522
|
+
**AK9-01 was the sharpest of the raw slots** - an `ID` element bound to X12 code list **715**
|
|
523
|
+
(a data element number, and its values are a code _list_; this repo's own `src/transactions/ack/codes.ts`
|
|
524
|
+
had it right and four other places said "code source"), so a number there told a receiver nothing
|
|
525
|
+
about whether the functional group was accepted, and `build999`'s own `X12_ACK_ACCEPT_WITH_ERRORS`
|
|
526
|
+
guard compares `disposition === "A"`, which a number walked past exactly as it walked past
|
|
527
|
+
`patientControlNumber === ""`.
|
|
528
|
+
|
|
197
529
|
- **The `attw` publish gate no longer passes a tarball that carries no type declarations.** The
|
|
198
530
|
`attw` script was the bare CLI, and `@arethetypeswrong/cli` returns 0 whenever its analysis found
|
|
199
531
|
no types at all, before it reads the problem list. For a package that ships types, "This package
|