@cosyte/x12 0.0.8 → 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,60 @@ 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
+
57
+ - **`requireCallerString` / `makeCallerEscaper`** (internal), the single route a caller-supplied
58
+ element value takes **through a builder's `esc` helper**. Read that scope literally: it is not
59
+ every route into an emitted segment, and the `### Fixed` entry below says which positions bypass
60
+ it. All nine builders now build their `esc` helper through
61
+ `makeCallerEscaper`, which type-checks the value before escaping and refuses through the calling
62
+ module's own `refuse` callback, so a wrong-typed element draws that builder's existing typed error
63
+ and code rather than a new shared one. `buildInterchange`, `build999`, `build271` and `build278`
64
+ each gained the one-line `refuseSpec` thrower they needed for it.
65
+
12
66
  - **`renderCallerJson`** (internal), the type-preserving half of the caller-value bound, held to the
13
67
  same `BUILD_REFUSAL_VALUE_MAX_RENDERED` ceiling as `renderCallerValue`. It exists because
14
68
  `defineProfile()` reports a bad `name` / `id` / `effect` / `fixture` with `JSON.stringify`, and that
@@ -42,6 +96,81 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
42
96
  a warning `message` is a frozen-registry lookup with positional metadata only, but an orphan is
43
97
  document content, verbatim, exactly like `tx.rawSegments`. Log `context` and `segmentIndex`.
44
98
 
99
+ ### Changed
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
+
166
+ - **`escapeRelease` now throws `TypeError` on a non-string instead of returning `""`.** Previously it
167
+ gave three different wrong answers depending on what arrived: a number, a boolean or a plain object
168
+ returned the empty string silently; `null` and `undefined` threw on the property read; an array or
169
+ an array-like threw on `charAt`. All three now terminate the same way. A `TypeError` rather than a
170
+ code-tagged library error is deliberate: it is a pure text utility with no spec, element or caller
171
+ context to name. Nothing inside the library can reach it, because all nine builders refuse
172
+ first with their own typed, code-tagged error.
173
+
45
174
  ### Security
46
175
 
47
176
  - **The PHI scanner refuses an in-scope entry that is not a regular file, on both of its enumerating
@@ -111,6 +240,292 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
111
240
 
112
241
  ### Fixed
113
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
+
388
+ - **A number passed where a builder's types say `string` no longer emits an EMPTY element. It is now
389
+ REFUSED, and it is deliberately not coerced.** `escapeRelease` opened with
390
+ `if (value.length === 0) return value;` and then looped to `value.length`. On a number `.length` is
391
+ `undefined`, so the early return did not fire, `i < undefined` was false, the loop body never ran,
392
+ and the function returned its empty accumulator. **The value vanished with no warning and no
393
+ error.** The types say `string`, so a TypeScript caller could not reach it; a JavaScript or
394
+ JSON-driven caller could, and `@cosyte/cli` is such a caller.
395
+
396
+ Measured at `0.0.8` on an otherwise valid `build835` spec with only `patientControlNumber` changed:
397
+ `CLP**1*500.00*450.00*50.00*MB*ICN-9001*11::1`, `ix.warnings.length === 0`, and a frozen interchange
398
+ that looks successful. CLP-01 is required by TR3 005010X221A1 Loop 2100 and is the key that
399
+ reassociates the remittance back to the 837's CLM-01. The builder's own
400
+ `patientControlNumber === ""` guard did not catch it, because the value was not yet a string when it
401
+ was checked.
402
+
403
+ **The same one line reached every escaped slot in all nine builders**, including the 837's own
404
+ CLM-01, the other end of that reassociation link. Measured at base by driving the shipped table
405
+ against a `143a6ea` worktree, one element each: `BPR*A1*450.00` became `BPR**450.00`,
406
+ `AK2*837*A1*005010X222A2` became `AK2*837**005010X222A2`, `NM1*IL*1*DOE*JANE****34*A1` became
407
+ `NM1*IL*1*DOE*JANE****34`, `ENT**2J*34*A1` became `ENT**2J*34`, `NM1*1P*2*A1` became `NM1*1P*2`,
408
+ `UM*HS*I*A1` became `UM*HS*I`, and `CLM*A1*150.00***11:B:1*Y*A*Y*Y` became
409
+ `CLM**150.00***11:B:1*Y*A*Y*Y`. Where the dropped element was trailing, the trailing-empty trim
410
+ removed it outright, so it is not even positionally recoverable.
411
+
412
+ **It refuses rather than coercing, and that choice is the substance of the fix.** Coercion would
413
+ mint a _different_ identifier: a JSON payload that carried `"0012345"` as a number has already lost
414
+ the leading zeros, so `String(12345)` emits a well-formed identifier that is not the one the caller
415
+ sent, and a remittance that reassociates to the wrong claim is worse than one that fails to
416
+ reassociate at all. `String(1e21)` is `"1e+21"`, `String(NaN)` is `"NaN"` and `String(0.1 + 0.2)` is
417
+ `"0.30000000000000004"`, none valid in an `AN`, `ID` or `Nn` element, and `X12Decimal` is already
418
+ the sanctioned route for numeric content. No working caller is broken, because the numeric path did
419
+ not work; it silently lost the field. The refusal message says why, so a caller is not nudged
420
+ straight into `String(value)` at the wrong boundary.
421
+
422
+ This is deliberately the OPPOSITE answer to `renderCallerValue`, which coerces for the same caller
423
+ mistake. A refusal message that throws replaces a typed, code-tagged error with an uncaught
424
+ `TypeError`, so its duty is to survive anything; an emitted document's duty is to invent nothing.
425
+
426
+ **The guard is on values routed through the escape helper, and not every element position goes
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
431
+ `patientResponsibilityAmount` of `0.1 + 0.2` still emits `…*0.30000000000000004*…`, `1e21` still
432
+ emits `…*1e+21*…` and `NaN` still emits `…*NaN*…`, each with zero warnings, which are the exact
433
+ three renderings this entry names as disqualifying, and an 837 service-line `units` reaches SV1-04
434
+ the same way. **That is a set of examples too, not a count** - a draft of this entry said the class
435
+ was exactly 36 slots and closed, and adversarial review measured it open. **Some string-typed
436
+ positions never call the escape helper at all** and emit a
437
+ number, or an unescaped delimiter, verbatim: the 999's `groupControlNumber` (GS-06 / GE-02),
438
+ `transactionSetControlNumber` (ST-02 / SE-02) and `disposition` (AK9-01 and IK5-01), the 278's
439
+ `levelCode` (HL-03), `groupDate` / `groupTime` (GS-04 / GS-05), and the 837's `lineNumber`
440
+ (LX-01). **Those are examples and not a census, deliberately:** two drafts of this
441
+ entry published an exhaustive count and adversarial review measured both incomplete, so the claim
442
+ is cut back rather than grown a third time. AK9-01 is an `ID` element bound to X12 code list 715,
443
+ and `build999`'s own accept-with-errors guard compares it against `"A"`, so a number walks past it
444
+ the same way it walked past `patientControlNumber === ""`. **The fixed-width ISA slots** go through
445
+ `pad` / `padControl`, so a number throws an untyped `TypeError` and a numeric
446
+ `interchangeControlNumber` throws a typed refusal whose text misleadingly says "exceeds the 9-char
447
+ spec limit"; `buildTA1` has no escape helper at all.
448
+
449
+ **The "no working caller is broken" claim holds with one measured exception:** a boxed
450
+ `new String("PT-ACCT-001")` built cleanly at `0.0.8` and is refused now, because `typeof` it is
451
+ `"object"`.
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
+
114
529
  - **The `attw` publish gate no longer passes a tarball that carries no type declarations.** The
115
530
  `attw` script was the bare CLI, and `@arethetypeswrong/cli` returns 0 whenever its analysis found
116
531
  no types at all, before it reads the problem list. For a package that ships types, "This package
@@ -365,6 +780,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
365
780
 
366
781
  ### Tests
367
782
 
783
+ - **`test/builder-string-type.test.ts`**, the emit gate for the fix above and the third member of the
784
+ builder-gate family (`builder-refusal-bounds` guards what a refusal says, `builder-array-bounds`
785
+ guards whether a refusal happens, this one guards whether the caller's value reaches the document).
786
+ The source scan is the exhaustive half: it requires every builder module's `esc` to be built by
787
+ `makeCallerEscaper` and requires no builder module to call `escapeRelease` itself, so a tenth
788
+ builder writing the old one-liner reds it without anyone adding a case. Negative-controlled by
789
+ putting the defect back: reverting one module's `esc` reds the scan by file and line **and** reds
790
+ the behavioural half, which is a cleaner control than the array gate gets, since there is no loop
791
+ here to wedge the runner.
792
+
368
793
  - **The PHI-scanner suite stopped paying 30 of its 32 `tsx` start-ups, and the global `testTimeout`
369
794
  now says what it does and does not cover.** No library code, no public surface and no timeout value
370
795
  changed; `scripts/phi-scan.ts` and `scripts/attw.mjs` are untouched.