@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 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 is pre-existing, measured, unchanged here and disclosed in
173
- `KNOWN-LIMITATIONS.md`. **36 `esc` slots read `.toString()` off what the types say is an
174
- `X12Decimal`**, so a raw number arrives already a string and is passed through: a
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 source 715,
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