@cosyte/x12 0.0.9 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +756 -17
- package/KNOWN-LIMITATIONS.md +804 -0
- package/README.md +7 -5
- package/dist/index.cjs +440 -174
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +331 -16
- package/dist/index.d.ts +331 -16
- package/dist/index.mjs +435 -175
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,92 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
11
|
|
|
12
|
+
- **🩺 `X12_837_ENTITY_SEGMENT_DISCARDED_AFTER_LX`, the 27th Tier-2 warning code, plus the public
|
|
13
|
+
factory `entitySegmentDiscardedAfterLx(position)`** (`X12-DISCARD-AFTER-STRAY-LX`). Raised for
|
|
14
|
+
each 837 `N3` / `N4` / `PER` / `REF` that reached **no party at all** because an earlier `LX` with
|
|
15
|
+
**no `CLM` open** dropped its Loop 2400 and closed the entity loop those segments belonged to.
|
|
16
|
+
`position.segmentIndex` names the **discarded segment itself**, not the `LX`: the loss is per
|
|
17
|
+
segment, so two `N3`s are two warnings, and the segment is what a consumer resolves back through
|
|
18
|
+
`tx.segments`. It ends the silence disclosed under `### Fixed` by the change that introduced that
|
|
19
|
+
discard, which is in this same unreleased block, so no published release ever had the discard
|
|
20
|
+
without the warning.
|
|
21
|
+
|
|
22
|
+
**🩺 Read its bound literally: this is NOT a general "entity segment reached no party" code**, and
|
|
23
|
+
nothing about it should be restated that way. It reports only a segment discarded after such an
|
|
24
|
+
`LX`, and only until the next `NM1` / `HL` / `CLM` opens a loop - a party named after that `LX` is
|
|
25
|
+
outside this code's scope again, and stays silent whether or not this reader surfaces its trailing segments on it. Every other route to an
|
|
26
|
+
unattached `N3` / `N4` / `PER` / `REF` is exactly as silent as before: no entity loop open at the
|
|
27
|
+
`LX` at all, an `NM1` this reader cannot route, an intervening `HL` or `CLM`, and the other
|
|
28
|
+
dropped-`LX` route where a claim **is** open. A `DTP` / `AMT` / `NTE` on that route is discarded
|
|
29
|
+
too and is deliberately **not** reported, because those never attach to a party on any route.
|
|
30
|
+
Each of those bounds is a test, and each is one a widened guard would fail.
|
|
31
|
+
|
|
32
|
+
**It reports that a segment reached NO party, not that it would have reached one.** This reader
|
|
33
|
+
does not surface every one of the four kinds on every party (a `PER` on a patient, or a pay-to
|
|
34
|
+
address), so the code can fire where nothing this library's own reset lost. That is fail-safe and
|
|
35
|
+
is stated rather than narrowed.
|
|
36
|
+
|
|
37
|
+
Nothing about the model changed: the segments are still discarded, still verbatim on
|
|
38
|
+
`tx.segments`, and the message carries positional metadata only, never a value off the wire.
|
|
39
|
+
|
|
40
|
+
- **🩺 `X12_837_SERVICE_SEGMENT_WITHOUT_LX`, the 26th Tier-2 warning code, plus the public factory
|
|
41
|
+
`serviceSegmentWithoutLx(position)`** (`X12-837-LOOP-RESIDUALS`). Raised when an 837 `SV1` / `SV2`
|
|
42
|
+
/ `SV3` arrives with **no Loop 2400 open**, so there is no service line to decode it into and
|
|
43
|
+
**nothing the segment carries is read** - not its charge, units, procedure code, modifiers, unit of measure or
|
|
44
|
+
place of service. `position.segmentIndex` names the **service segment itself**, which is the whole
|
|
45
|
+
reason it is a new code rather than a widening of an existing one: the two service-line codes this
|
|
46
|
+
library already had are both anchored at an `LX`, and there is no `LX` in scope here to anchor to.
|
|
47
|
+
**Read that condition literally - it is "no line open", not "the file contains no `LX`":** an `LX`
|
|
48
|
+
in an earlier claim is still an `LX`. Nothing is fabricated to stand in and no line or claim is
|
|
49
|
+
synthesized; the segments stay verbatim on `tx.segments`. See `### Fixed` for the silence it ends,
|
|
50
|
+
and `KNOWN-LIMITATIONS.md` for the measured bounds, including what it does **not** say about how
|
|
51
|
+
the submission's variant resolved.
|
|
52
|
+
|
|
53
|
+
- **🩺 `X12ServiceLineStatus.unitsOfService` and `Build277ServiceLineSpec.unitsOfService`, the 277
|
|
54
|
+
Loop 2220 SVC-07 units of service count** (`X12-277-SVC07-NOT-DECODED`). X12 element 380
|
|
55
|
+
(Quantity), decoded as an `X12Decimal` like every other quantity in this library and never via
|
|
56
|
+
`parseFloat`. It had no representation on either side before, which is why an X212 277 this
|
|
57
|
+
library emitted was short a required element; see `### Fixed`. `undefined` still means **not
|
|
58
|
+
decoded** rather than absent, and an SVC-07 that is present but does not decode raises
|
|
59
|
+
`X12_UNPARSEABLE_DECIMAL` at `position.elementIndex` 7 while an absent one raises nothing. **SVC-05
|
|
60
|
+
stays unread on the 277 on purpose:** it is usage N in both 277 TR3s, unlike the 835 where it is
|
|
61
|
+
the Units of Service **Paid** Count. The warning registry is unchanged at 25 codes plus 4 Tier-3
|
|
62
|
+
fatals.
|
|
63
|
+
|
|
64
|
+
- **🩺 `X12_837_SERVICE_LINE_DROPPED`, the 25th Tier-2 warning code, plus the public factory
|
|
65
|
+
`serviceLineDropped(position)`** (`X12-VARIANT-LOOKUP-PROTOTYPE`). Raised when an 837 `LX` opens
|
|
66
|
+
no Loop 2400 at all, so the service line reaches **no claim's** `serviceLines` - either because no
|
|
67
|
+
`CLM` is open at that point in the walk, or because the submission's variant never resolved to
|
|
68
|
+
`P` / `I` / `D`. Distinct from `X12_837_SERVICE_LINE_NOT_DECODED`, where the line is retained and
|
|
69
|
+
only its service segment went unread. `position.segmentIndex` names the `LX`, the same anchor and
|
|
70
|
+
for the same reason: it is the one segment present in every case. Nothing is fabricated to stand
|
|
71
|
+
in for the missing line and no claim is synthesized; the segments stay verbatim on `tx.segments`.
|
|
72
|
+
The registry stays additions-only.
|
|
73
|
+
|
|
74
|
+
- **🩺 `X12_837_SERVICE_LINE_NOT_DECODED`, the 24th Tier-2 warning code, plus the public factory
|
|
75
|
+
`serviceLineNotDecoded(position)`** (`X12-837-SV-SILENT-ZERO`). Raised when an 837 Loop 2400
|
|
76
|
+
service line is closed without ever having decoded an `SV1` / `SV2` / `SV3` for the variant the
|
|
77
|
+
submission resolved to; see `### Fixed` for what that line was reporting instead.
|
|
78
|
+
`position.segmentIndex` names the `LX` that opened the line rather than the `SVx`, because the
|
|
79
|
+
no-`SVx`-at-all case has no `SVx` to point at. The registry stays additions-only.
|
|
80
|
+
|
|
81
|
+
- **🩺 `X12_UNPARSEABLE_DECIMAL`, the 23rd Tier-2 warning code, plus `readElementDecimal` and
|
|
82
|
+
`X12DecimalWarningSink`** (`X12-QUANTITY-SILENT-DEFAULTS`). A decimal element that is **present**
|
|
83
|
+
and is not an X12 R-type decimal now says so; see `### Fixed` for what it was doing instead.
|
|
84
|
+
`readElementDecimal(seg, n, delimiters)` is the pure primitive underneath both existing helpers and
|
|
85
|
+
returns `{ value, status }` with `status` one of `"decoded"` / `"absent"` / `"unparseable"`, which
|
|
86
|
+
is the distinction a bare `undefined` could not carry. `elementDecimal` and `elementDecimalOrZero`
|
|
87
|
+
each gained an optional 4th `X12DecimalWarningSink` argument (`{ warnings, position }`); the helper
|
|
88
|
+
narrows the position to the failing `elementIndex` itself, so a caller passes the position of the
|
|
89
|
+
segment and never has to remember to do it per element. Existing 3-argument calls still compile and
|
|
90
|
+
are still silent, on purpose.
|
|
91
|
+
|
|
92
|
+
- **`X12RemitServiceLine.originalUnitsOfService` / `Build835ServiceLineSpec.originalUnitsOfService`**,
|
|
93
|
+
the 835 SVC-07 Original Units of Service Count: the units as **submitted**, which a payer sends only
|
|
94
|
+
when they differ from the paid count in SVC-05. It is not a convenience field - without it the
|
|
95
|
+
corrected SVC map (see `### Changed`) would have left SVC-07 unread and unwritten, converting a
|
|
96
|
+
mis-read into a fresh silent drop. `undefined` means "same as paid", not "zero submitted".
|
|
97
|
+
|
|
12
98
|
- **`requireCallerString` / `makeCallerEscaper`** (internal), the single route a caller-supplied
|
|
13
99
|
element value takes **through a builder's `esc` helper**. Read that scope literally: it is not
|
|
14
100
|
every route into an emitted segment, and the `### Fixed` entry below says which positions bypass
|
|
@@ -53,6 +139,111 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
53
139
|
|
|
54
140
|
### Changed
|
|
55
141
|
|
|
142
|
+
- **`KNOWN-LIMITATIONS.md` now ships in the published tarball** (`X12-837-LOOP-RESIDUALS`). Two
|
|
143
|
+
shipped warning messages, `X12_837_SERVICE_LINE_DROPPED` and
|
|
144
|
+
`X12_837_ENTITY_SEGMENT_DISCARDED_AFTER_LX`, name that document ("see KNOWN-LIMITATIONS.md", each
|
|
145
|
+
one clause before its close), but it was absent from `package.json`'s `files`. An installed copy
|
|
146
|
+
therefore carried a runtime message naming a file that was not beside it. **The citation is what
|
|
147
|
+
was kept and the packaging is what moved:** that document is the canonical account of what this
|
|
148
|
+
reader does not reproduce, and a consumer holding a code, a `position` and a message is precisely
|
|
149
|
+
the consumer who needs it. **The cost is named rather than argued away:** a file in the tarball is
|
|
150
|
+
a published artifact, permanent at the version carrying it, so every claim in that document is now
|
|
151
|
+
held to a published surface's bar. The line is drawn at what the library says to a caller **at
|
|
152
|
+
runtime**, so `docs-content/` is deliberately still not in `files`; the shipped `README.md`'s
|
|
153
|
+
relative links to `./docs-content/cookbook.md` and `./docs-content/troubleshooting.md` therefore
|
|
154
|
+
still do not resolve inside an install, which is pre-existing, unchanged here, and its own
|
|
155
|
+
decision. `test/package-files-cite.test.ts` refuses a shipped warning message that names a
|
|
156
|
+
repo-root `.md` file `files` does not carry, and separately asserts the cited file is on disk, so
|
|
157
|
+
the gate cannot pass on a `files` entry that names nothing. It reads the exported warning registry
|
|
158
|
+
and nothing else, not JSDoc, not source comments, and not the `build*` refusal templates. No code,
|
|
159
|
+
type, warning code or warning message changed.
|
|
160
|
+
|
|
161
|
+
- **A statement about pre-`0.0.10` behaviour that was false as written is cut back in the documents
|
|
162
|
+
this package publishes** (`X12-837-LOOP-RESIDUALS`). "The trailing segments were filed against
|
|
163
|
+
it", "they attached to whichever party the last `NM1` left active", and the counterfactual carried
|
|
164
|
+
by "no longer attaches itself to the last named party" all read as all four of `N3` / `N4` /
|
|
165
|
+
`PER` / `REF` having reached every party through `0.0.10`. They did not: `attachContact` has no
|
|
166
|
+
route for a patient or a pay-to address and `attachReference` none for a pay-to address, so those
|
|
167
|
+
segments reached no party on any release. That is why
|
|
168
|
+
`X12_837_ENTITY_SEGMENT_DISCARDED_AFTER_LX` reports a segment reached **no** party and never that
|
|
169
|
+
it would otherwise have reached one. `KNOWN-LIMITATIONS.md` additionally carried, ninety lines
|
|
170
|
+
from the entry contradicting it, a fourth wording of the same universal in the present tense: "a
|
|
171
|
+
`NM1` arriving AFTER that `LX` names a party normally, and its own trailing segments attach".
|
|
172
|
+
**That parenthetical is deleted rather than given a fifth wording**, and the past-tense copies
|
|
173
|
+
either take the qualifier already written beside the code's own entry or are cut back to the
|
|
174
|
+
measurement beside them, rather than being given a new wording. What a party named after such an
|
|
175
|
+
`LX` does is unchanged, and that code's own entry still states its scope and says outright that
|
|
176
|
+
whether a later party's trailing segments attach is a separate question it does not answer. **No
|
|
177
|
+
per-kind, per-party map is published, here or in that entry.** Nothing about the
|
|
178
|
+
parser changed, and the measured instances each statement rested on, every one of them on a payer,
|
|
179
|
+
are unchanged and stay. **The same wording survives in `src/` comments and in test-file headers,
|
|
180
|
+
neither of which this change swept or fixed.**
|
|
181
|
+
|
|
182
|
+
- **🩺 BREAKING: `build277` now REFUSES a Loop 2220 service line that carries no `unitsOfService`
|
|
183
|
+
(`X12-277-SVC07-NOT-DECODED`).** SVC-07 is usage **R** in TR3 `005010X212`, so a service line
|
|
184
|
+
without it cannot be emitted as a conformant 277. The refusal is the existing
|
|
185
|
+
`ClaimStatus277BuildError` with code `X12_277_BUILD_INVALID_SPEC`, raised as a precondition before
|
|
186
|
+
any segment is built, and its message names `SVC-07`, the TR3, and the structural locator of the
|
|
187
|
+
offending line (indices only, never a member id, name, trace or claim id). **`build277CA` is
|
|
188
|
+
deliberately unaffected:** in `005010X214` the same element is usage **S**, so the identical spec
|
|
189
|
+
still builds and simply omits the element. **The count is never defaulted in either direction** -
|
|
190
|
+
a quantity the caller did not supply is a quantity nobody sent, and a units figure is one a payer
|
|
191
|
+
reprices against. If you emit X212 277s with service lines, the first build after upgrading throws
|
|
192
|
+
until you supply the submitted count. Usage taken from the pyx12 005010 maps
|
|
193
|
+
(`277.5010.X212.xml`, `277.5010.X214.xml`), outside this repository, rather than from this
|
|
194
|
+
library's own reader.
|
|
195
|
+
|
|
196
|
+
- **The committed canonical 277 fixture and its serializer golden now carry SVC-07**
|
|
197
|
+
(`test/fixtures/status/277-canonical.edi`, `test/fixtures/golden/277.edi`). The X212 fixture was
|
|
198
|
+
itself short the required element, which is part of why nothing in the suite noticed. The other
|
|
199
|
+
twelve goldens regenerate byte-identically.
|
|
200
|
+
|
|
201
|
+
- **🩺 BREAKING: the 835 Loop 2110 SVC element map is corrected, in both directions
|
|
202
|
+
(`X12-SVC-ELEMENT-MAP-OFF-BY-ONE`).** Through `0.0.9` `get835` read `revenueCode` from **SVC-05**
|
|
203
|
+
and `paidUnitsOfService` from **SVC-07**, and `build835` wrote them to those same two positions
|
|
204
|
+
while hard-coding SVC-04 empty behind a comment asserting "revenue code is SVC-05 in X221A1;
|
|
205
|
+
SVC-04 unused". That comment was wrong. SVC-04 is the NUBC revenue code (X12 element 234, a
|
|
206
|
+
string), SVC-05 is the Units of Service **Paid** Count (element 380, a Quantity), and SVC-07 is
|
|
207
|
+
the **Original** Units of Service Count (element 380) - a different quantity, sent only when the
|
|
208
|
+
submitted count differs from the paid one. `revenueCode` now reads and writes **SVC-04** and
|
|
209
|
+
`paidUnitsOfService` now reads and writes **SVC-05**.
|
|
210
|
+
|
|
211
|
+
**The harm was a mis-read code system and a mis-read quantity, in both directions, silently.**
|
|
212
|
+
Measured across the six committed remit fixtures plus the golden, **8 of 8 service lines** read
|
|
213
|
+
back `revenueCode: "1"` at `0.0.9` - `1` is not a valid NUBC revenue code, it is the paid-unit
|
|
214
|
+
count from SVC-05 - while `paidUnitsOfService` came back `undefined` because SVC-07 was absent.
|
|
215
|
+
On emit, a line with revenue code `0300` and 2 paid units produced
|
|
216
|
+
`SVC*HC:99213*600.00*550.00**0300*HC:99212*2`, putting a revenue code into a **Quantity**
|
|
217
|
+
element, so a conformant receiver read `0300` as 300 units of service. It now produces
|
|
218
|
+
`SVC*HC:99213*600.00*550.00*0300*2*HC:99212`.
|
|
219
|
+
|
|
220
|
+
**Nothing in the suite could detect this and that is why it shipped.** Every existing assertion
|
|
221
|
+
was a `build835` -> `get835` round trip, which is green for any pair of positions the two modules
|
|
222
|
+
agree on. All 1,227 tests stayed green with the fix applied. The map is now pinned against literal
|
|
223
|
+
bytes in `test/transactions-remit-835-svc-element-map.test.ts`; all 11 of its cases fail on
|
|
224
|
+
`e3cdf49` and pass at head.
|
|
225
|
+
|
|
226
|
+
**The repo already contradicted itself:** `build277` / `get277Status` read and write the revenue
|
|
227
|
+
code at SVC-04, `build-277-types.ts` says so in prose, and every committed 835 fixture is written
|
|
228
|
+
to the correct map (`SVC*…**1` is an empty SVC-04 and one unit paid). Only the 835 module
|
|
229
|
+
disagreed, and only with itself.
|
|
230
|
+
|
|
231
|
+
**Sources, and what was not read. TR3 005010X221A1 is a paid X12 document; nobody here has read
|
|
232
|
+
it**, and every claim is traceable to something publicly checkable: pyx12's machine-readable
|
|
233
|
+
`835.5010.X221.A1.xml`, an independent open-source implementation of the same guide, which carries
|
|
234
|
+
the whole table and is **the source for SVC-04**; X12's own RFI #2163 for SVC-05; the base 005010
|
|
235
|
+
element dictionary, where SVC-04 is a string and SVC-05/07 are Quantities, which rules out a
|
|
236
|
+
revenue code at SVC-05 on type alone; and two published payer companion guides. **Agreement with
|
|
237
|
+
this repo's own 277 modules is corroborating, not a source** - checking a spec claim against the
|
|
238
|
+
implementation only proves the two agree, which is how the wrong map survived. Listed with links
|
|
239
|
+
in `KNOWN-LIMITATIONS.md`.
|
|
240
|
+
|
|
241
|
+
**Also disclosed rather than fixed:** `undefined` on either quantity means "not decoded", not
|
|
242
|
+
"absent" - the element may have been present and unparseable, which raises no warning and is
|
|
243
|
+
pre-existing at every quantity site. And **835s this library emitted at `0.0.9` or earlier are
|
|
244
|
+
non-conformant on the wire and should be re-emitted**: their revenue code sits in SVC-05, so this
|
|
245
|
+
release reads it back as a paid quantity and reports no revenue code.
|
|
246
|
+
|
|
56
247
|
- **`escapeRelease` now throws `TypeError` on a non-string instead of returning `""`.** Previously it
|
|
57
248
|
gave three different wrong answers depending on what arrived: a number, a boolean or a plain object
|
|
58
249
|
returned the empty string silently; `null` and `undefined` threw on the property read; an array or
|
|
@@ -63,6 +254,69 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
63
254
|
|
|
64
255
|
### Security
|
|
65
256
|
|
|
257
|
+
- **🩺 A builder refusal no longer echoes the value it refused, so a `claimId` or a member id from a
|
|
258
|
+
JSON-driven caller cannot reach `Error.message`** (`REFUSAL-MESSAGE-PHI-ECHO`). Every domain builder
|
|
259
|
+
documents, on its own error codes, that its refusal message carries structural locators, counts and
|
|
260
|
+
X12 control codes only and **never a `claimId` (patient-account number), member id, member name,
|
|
261
|
+
trace or diagnosis code**. That held for the refusal TEMPLATES and did not hold underneath them. A
|
|
262
|
+
guarantee that is true on one path and false on another is not a guarantee, so it was made true
|
|
263
|
+
rather than reworded.
|
|
264
|
+
|
|
265
|
+
**Measured in the source, not inferred from the prose.** The four shared caller guards
|
|
266
|
+
(`caller-string.ts`, `caller-segment.ts`, `caller-decimal.ts`, `caller-array.ts`) described a
|
|
267
|
+
wrong-typed value by rendering it through `renderCallerValue` - bounded to 90 characters, and **not
|
|
268
|
+
redacted**. On `4a5a943`, with the kind of spec `@cosyte/cli` builds from `JSON.parse`:
|
|
269
|
+
|
|
270
|
+
```text
|
|
271
|
+
build835({ claims: [{ patientControlNumber: 900412345678, ... }] })
|
|
272
|
+
-> build835: every element value must be a string, but received a number ("900412345678"). ...
|
|
273
|
+
build834({ members: [{ member: { idCode: 700998877, ... } }] })
|
|
274
|
+
-> build834: every element value must be a string, but received a number ("700998877"). ...
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
**The shipped disclosure named the wrong guard.** It said `requireCallerSegment` echoes the
|
|
278
|
+
primitive and quoted `build835: CLP-01 must be a string, but received a number (...)`. CLP-01 routes
|
|
279
|
+
through `esc`, so `requireCallerString` refuses first and its message names only the builder. The
|
|
280
|
+
echo was on both, and on the two other guards as well.
|
|
281
|
+
|
|
282
|
+
**The remedy is a property, not a list: no caller guard echoes what a caller put in a document
|
|
283
|
+
ELEMENT.** The string, segment and decimal guards report the offending TYPE, and so does the array
|
|
284
|
+
guard's primitive arm; the segment guard keeps its spec-shaped slot locator beside it
|
|
285
|
+
(`build999: "AK9"-01 must be a string, but received a number.`). A guard standing on every element
|
|
286
|
+
of every builder cannot know whether the primitive in front of it is a control number or a patient
|
|
287
|
+
identifier, which is exactly why it may not echo one. The decimal guard went with them because an
|
|
288
|
+
`X12Decimal` slot IS an element slot, and because its message's own fixed text already names
|
|
289
|
+
`0.30000000000000004` / `1e+21` / `NaN` as what a raw number does, so no diagnosis was lost.
|
|
290
|
+
"An `X12Decimal` slot holds no identifier today" would have been the wrong kind of argument: a fact
|
|
291
|
+
about today's slots rather than a property of the guard.
|
|
292
|
+
|
|
293
|
+
**Two things that property does NOT say, and both were drafted as absolutes first.** The array
|
|
294
|
+
guard still renders a forged array-like's `length` and its class tag through `renderCallerValue`,
|
|
295
|
+
bounded: those describe the SHAPE a caller forged rather than an element's contents, and they are
|
|
296
|
+
the whole diagnostic for `{ length: "9".repeat(120000) }`. And **only the segment-join guard names
|
|
297
|
+
the SLOT** - `esc` and `escDec` name the BUILDER, a limit `caller-string.ts` already recorded, so on
|
|
298
|
+
those two the echoed value used to stand in for a locator and now nothing does. That is a real
|
|
299
|
+
diagnostic cost and it is disclosed rather than argued away.
|
|
300
|
+
|
|
301
|
+
**The segment guard's slot locator is now bounded by GRAMMAR rather than by length.** `parts[0]` is
|
|
302
|
+
caller-supplied in `buildInterchange`, which takes `[segmentId, ...elements]` wholesale, so it is
|
|
303
|
+
admitted only when it matches the X12 segment-id grammar (two or three uppercase alphanumerics
|
|
304
|
+
opening with a letter) and otherwise degrades to `element N`. A length bound redacts nothing when
|
|
305
|
+
the thing being bounded has a grammar.
|
|
306
|
+
|
|
307
|
+
**What did NOT change, stated because deleting a claim leaves a new one in its place.** The caller
|
|
308
|
+
values a builder's own template names by field are still rendered, still bounded, still not escaped,
|
|
309
|
+
and still not redacted: control numbers, the 834's INS-03 / HD-01 maintenance type, the 837's
|
|
310
|
+
service-line variant, the TA1-05 note code, the 999's AK9 counts and acknowledged ST-02. That has
|
|
311
|
+
always been documented as robustness and log hygiene rather than redaction, and it remains so.
|
|
312
|
+
`renderCallerValue`, `BUILD_REFUSAL_VALUE_MAX_LENGTH` and `BUILD_REFUSAL_VALUE_MAX_RENDERED` are
|
|
313
|
+
unchanged and still exported. `X12ParseError.snippet` on the four Tier-3 fatals is unchanged and
|
|
314
|
+
still the one deliberate place a document's bytes are copied.
|
|
315
|
+
|
|
316
|
+
**Behaviour change for callers who read a value back out of one of those four messages.** Nothing in
|
|
317
|
+
the library did; the values are gone from the message and unchanged on the spec the caller still
|
|
318
|
+
holds.
|
|
319
|
+
|
|
66
320
|
- **The PHI scanner refuses an in-scope entry that is not a regular file, on both of its enumerating
|
|
67
321
|
routes.** A symbolic link under a scan root pointing at a PHI-bearing file used to scan CLEAN on
|
|
68
322
|
both, so the pre-commit gate and CI both reported "no hits" over a capture the scan never read.
|
|
@@ -94,7 +348,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
94
348
|
exited 1 and printed that shape, because `git show` handed the path text straight to the
|
|
95
349
|
cross-cutting shape pass.
|
|
96
350
|
|
|
97
|
-
|
|
351
|
+
**`T` was added to the `--staged` filter, and it is the one letter that made the mode check
|
|
98
352
|
reachable.** Replacing a TRACKED regular file with a link is neither an add nor a modify: measured
|
|
99
353
|
on this tree, `git diff --cached --raw --diff-filter=AM` returned zero rows for that change while
|
|
100
354
|
the unfiltered `--raw` showed `:100644 120000 <sha> <sha> T`. Without `T` the record died before
|
|
@@ -115,21 +369,429 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
115
369
|
`readFileSync`, which follows a link, so an explicitly named path that is a link to a PHI-bearing
|
|
116
370
|
file is scanned and hits (measured, exit 1).
|
|
117
371
|
|
|
118
|
-
**Not closed
|
|
119
|
-
enumerated by `--staged` at all,
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
the
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
372
|
+
**Not closed by THIS change, and stated rather than implied**: `R` (rename) and `C` (copy) were
|
|
373
|
+
not enumerated by `--staged` at all, so a fixture renamed while a real name was substituted into
|
|
374
|
+
it, and a `git mv` of an already-committed link into `test/fixtures/`, both passed at exit 0. **That
|
|
375
|
+
gap was LIVE at pre-commit through `0.0.7`, `0.0.8`, `0.0.9` and `0.0.10`**, every version published
|
|
376
|
+
after this change landed, with the all-mode sweep as the backstop. It is closed by the change below,
|
|
377
|
+
which is unreleased alongside this one. **Do not read "the same unreleased block" as "no published
|
|
378
|
+
release had it"**: this whole file sits under one `[Unreleased]` heading, so that inference is true
|
|
379
|
+
of the file and false of the registry. Also unclosed at the time: a scan that observed nothing was
|
|
380
|
+
reported clean rather than refused, which the last entry in this section closes, unreleased
|
|
381
|
+
alongside this one and subject to the same warning about what "the same unreleased block" does and
|
|
382
|
+
does not prove. Still open: the enumerate-then-read window in all mode is untouched, because
|
|
383
|
+
tolerating a failed read pulls the opposite way from narrowing what the enumeration admits and
|
|
384
|
+
belongs in its own change. No library code changed and no published type changed.
|
|
385
|
+
|
|
386
|
+
- **🩺 The PHI scanner's `--staged` route stops trusting the caller's git config, so five kinds of
|
|
387
|
+
staged change can no longer disappear from the pre-commit gate's list without a byte of the index
|
|
388
|
+
changing** (`PHI-SCAN-RENAME-BLIND-AT-PRECOMMIT`). `pnpm phi-scan --staged` is the pre-commit hook.
|
|
389
|
+
It enumerated the index with `git diff --cached --raw -z --diff-filter=AMT`, and each of the five
|
|
390
|
+
below returned exit 0 and `OK - no hits`. Every one is measured on a throwaway repository laid out
|
|
391
|
+
like this one, against a synthetic `.edi` payload whose NM1 person name, DMG date of birth, PER
|
|
392
|
+
phone and `REF*SY` SSN are all hits at exit 1 when the same bytes arrive as an ordinary add:
|
|
393
|
+
- **rename.** Detection is on by default and neither `AM` nor `AMT` returns `R`, so `git mv` of an
|
|
394
|
+
already-committed symbolic link into `test/fixtures/` staged as a single TWO-PATH record at mode
|
|
395
|
+
`120000` and the record was deleted before any mode could be read. Renaming a fixture while
|
|
396
|
+
substituting a real-looking surname into it passed identically, over bytes that are two hits as
|
|
397
|
+
an ordinary add. **No similarity score is recorded anywhere in this change**: a score moves with
|
|
398
|
+
the fixture, so a number carried over from one is wrong for the next. What is load-bearing is
|
|
399
|
+
that the record carries TWO PATHS;
|
|
400
|
+
- **copy.** Under `diff.renames=copies`, copying a PHI-bearing file from outside the scan roots
|
|
401
|
+
INTO `test/fixtures/` staged as a genuine two-path `C` record and was dropped the same way. It is
|
|
402
|
+
a distinct hole rather than the same one, because nothing is moved and the source stays put;
|
|
403
|
+
- **gitlink.** With `diff.ignoreSubmodules=all` in the caller's git config, a staged gitlink under
|
|
404
|
+
`test/fixtures/` vanished from `--raw` outright, where the same index without that config is
|
|
405
|
+
refused at exit 2 by the existing mode check;
|
|
406
|
+
- **unmerged path.** Returned by neither `AM` nor `AMT`. Such a path is recorded at one or more of
|
|
407
|
+
stages 1/2/3 and never at stage 0, so `git show :<path>` fails outright and the route attested
|
|
408
|
+
clean over an index it could not read. Git refuses to commit while a path is unmerged, so this
|
|
409
|
+
was never a route to a committed leak; what it was is a gate reporting on a state it never
|
|
410
|
+
observed, and `pnpm phi-scan --staged` is run by hand and from scripts as well as from the hook;
|
|
411
|
+
- **broken pair.** A pair broken by `-B` prints status letter **`M`** with a break score, one path,
|
|
412
|
+
which the record parser reads happily, **but `--diff-filter` classifies a broken pair as `B`
|
|
413
|
+
whatever letter it prints**, so an `AMTU` filter deletes it and a reader checking the raw output
|
|
414
|
+
concludes the opposite.
|
|
415
|
+
|
|
416
|
+
**The remedy is one rule rather than five fixes.** The argv is now
|
|
417
|
+
`git diff --cached --raw -z --no-renames --ignore-submodules=none --diff-filter=AMTUB`.
|
|
418
|
+
`--no-renames` makes a two-path record unemittable whatever `diff.renames` says, so both the rename
|
|
419
|
+
and the copy destination arrive as an ordinary single-path `A` and the source as a `D` the filter
|
|
420
|
+
drops; the two-field stride is therefore STRUCTURAL rather than conditional on the caller's
|
|
421
|
+
configuration. Verified under `diff.renames=true|copies|false|1` and `diff.renameLimit=1`.
|
|
422
|
+
`-M`, `-C` and `--find-copies-harder` each turn detection back on over the top of it and empty the
|
|
423
|
+
route again, which is pinned as a test rather than left to a comment.
|
|
424
|
+
|
|
425
|
+
**An unmerged path is refused (exit 2) with its own message**, separate from the not-a-regular-file
|
|
426
|
+
refusal, because its destination mode is `000000` and that refusal's sentence about symbolic links
|
|
427
|
+
and gitlinks would be false for it. `B` in the filter costs the enumeration nothing today, since
|
|
428
|
+
git only breaks a pair when `-B` is given; it is there so the flag cannot become a silent
|
|
429
|
+
blindfold later, which is why it is a remedy rather than a warning.
|
|
430
|
+
|
|
431
|
+
**The two enumerations are EQUAL when nothing is renamed, copied, unmerged, or a gitlink under
|
|
432
|
+
`diff.ignoreSubmodules`**, and larger only when one of those is present. State the precondition in
|
|
433
|
+
full: `--no-renames` ALONE would make the equality hold on renames and copies only, and the other
|
|
434
|
+
two flags widen it further. This is a superset and NOT a strictly larger set: nothing the previous
|
|
435
|
+
argv
|
|
436
|
+
enumerated stopped being enumerated, and that equality is asserted as a test.
|
|
437
|
+
|
|
438
|
+
**Not closed here, and measured rather than assumed**: a scan that observed nothing was still
|
|
439
|
+
reported clean, and an all-mode walk root replaced by a regular file still died on an unhandled
|
|
440
|
+
directory read rather than refusing cleanly. Both are closed by the entry below, unreleased
|
|
441
|
+
alongside this one. **Still open after both**: a tracked file directly under `test/` is enumerated
|
|
442
|
+
by neither route, and an index entry at exactly a scan root's own path matches no `--staged`
|
|
443
|
+
clause, because every clause tests a `<root>/` prefix. Each is a scope decision belonging in its
|
|
444
|
+
own change. No library code changed and no published type changed.
|
|
445
|
+
|
|
446
|
+
- **🩺 The PHI scanner's all-mode sweep can no longer report clean over the files it never opened
|
|
447
|
+
within its declared roots**
|
|
448
|
+
(`PHI-SCAN-OBSERVED-NOTHING-IS-GLOBAL`). `pnpm phi-scan` with no arguments walks two roots,
|
|
449
|
+
`test/fixtures` and `src`, and prints `OK - no hits` at exit 0 when it finds nothing. **Finding
|
|
450
|
+
nothing and opening nothing were indistinguishable.** Each shape is measured on a throwaway
|
|
451
|
+
repository laid out like this one, against a synthetic `.edi` payload whose NM1 person name, DMG
|
|
452
|
+
date of birth, PER phone and `REF*SY` SSN are hits at exit 1 when the same bytes are read (the
|
|
453
|
+
hit COUNT is deliberately not recorded: a refuter measured the drafted figure short, and this
|
|
454
|
+
repo's rule is to delete a drifting number rather than correct it):
|
|
455
|
+
- **a missing root.** With BOTH walk roots absent, and with `test/fixtures` alone absent, the walk
|
|
456
|
+
returned immediately and the sweep printed `OK - no hits` at **exit 0**. **A root a repository
|
|
457
|
+
never had is the worst shape of it**, because the gate then reads clean on every run it ever
|
|
458
|
+
makes and no run looks wrong;
|
|
459
|
+
- **an emptied root.** With the roots present but emptied on disk while their files stayed in the
|
|
460
|
+
index, the sweep printed `OK - no hits` at **exit 0** over a corpus whose committed bytes are
|
|
461
|
+
hits;
|
|
462
|
+
- **a root that is not a directory.** With a walk root replaced by a regular file, `readdirSync`
|
|
463
|
+
threw an **uncaught `ENOTDIR`** and the process ended at **exit 1**, which is this scanner's own
|
|
464
|
+
code for "hits found", as a stack trace rather than a refusal. A root that is a FIFO ended the
|
|
465
|
+
same way. **That exit code is NOT portable and neither is the mechanism** (`hl7` measures **2**
|
|
466
|
+
for its version of this shape, `terminology` **1** by a different route), so it was re-measured
|
|
467
|
+
here rather than carried over.
|
|
468
|
+
|
|
469
|
+
**The remedy is two rules, and the second is not implied by the first, because existence is not
|
|
470
|
+
observation.** A declared walk root must BE a directory (`refuseUnusableRoots`), and every
|
|
471
|
+
tracked, non-`.md` file under a root must have been one of the files the walk actually enumerated
|
|
472
|
+
(`reconcileObserved`). Both refuse at **exit 2** and name **every** offender, the same rule the
|
|
473
|
+
not-a-regular-file refusal already follows. **Each is independently load-bearing**: of the 17 new
|
|
474
|
+
cases, dropping the first reds **4** and dropping the second reds **5**, and the two sets are
|
|
475
|
+
disjoint. `--deduplicate` on the `git ls-files` argv is pinned separately, by its own case: an
|
|
476
|
+
unmerged path is returned once per STAGE, so without it one conflicted fixture is named three
|
|
477
|
+
times and counted as three.
|
|
478
|
+
|
|
479
|
+
**Naming every offender needs one thing more than the rule.** `git ls-files` returns an unmerged
|
|
480
|
+
path **once per merge stage**, so the listing is taken with `--deduplicate`; without it a single
|
|
481
|
+
conflicted fixture is named three times and counted as three, which falsifies "names every
|
|
482
|
+
offender" in the direction that teaches a developer to distrust the gate. And a refusal here
|
|
483
|
+
**never echoes git's own message**: `execFileSync` appends the child's stderr verbatim, and git's
|
|
484
|
+
fatals in this class carry absolute filesystem paths, so only the engine-owned exit status is
|
|
485
|
+
reported - the same rule `rootProblem` follows with `err.code`.
|
|
486
|
+
|
|
487
|
+
**🩺 SAY "BE A DIRECTORY", NEVER "BE ENUMERABLE".** A draft of this entry said the second and a
|
|
488
|
+
refuter measured it false: a root that IS a directory the process cannot open (mode `000`) passes
|
|
489
|
+
the type check and then throws an **uncaught `EACCES`** out of `readdirSync`, at **exit 1**,
|
|
490
|
+
identically at base and at head. An unreadable **subdirectory** under a root, and the window
|
|
491
|
+
between the root check and the walk, end the same way. **That class is PRE-EXISTING, disclosed and
|
|
492
|
+
NOT closed here**, because closing it means tolerating or classifying a failed directory read,
|
|
493
|
+
which is the deferred enumerate-then-read remedy pulling the same way. The claim was **cut back to
|
|
494
|
+
what the code checks**, not the guard grown.
|
|
495
|
+
|
|
496
|
+
**A root that is itself a symbolic link to a directory is still followed**, as before, and where
|
|
497
|
+
that link is TRACKED, `reconcileObserved` **exempts the root's own index entry**. Without that
|
|
498
|
+
exemption the sweep refused (exit 2) over a tree the base scanner scans as a documented superset at
|
|
499
|
+
exit 1, because `git ls-files` returns the link's own path `test/fixtures` while the walk only ever
|
|
500
|
+
yields `test/fixtures/<name>`. **The case that holds this commits its corpus**, because on an
|
|
501
|
+
uncommitted one `git ls-files` is empty and the case would be green by construction.
|
|
502
|
+
|
|
503
|
+
**🩺 "IT OPENS NO CLEAN PATH" WAS DRAFTED AND IS REFUTED, SO THE CLAIM IS CUT BACK RATHER THAN THE
|
|
504
|
+
GUARD GROWN.** The draft argued the exemption opened none, because a root that is a tracked regular
|
|
505
|
+
file, or a link to one, is refused by the root check first. True, and not the whole of it: when a
|
|
506
|
+
root is a tracked link to a DIRECTORY, everything the walk reads through it lives under the
|
|
507
|
+
target's own names, OUTSIDE the `git ls-files -- test/fixtures src` pathspec, so the whole
|
|
508
|
+
link-target corpus is unreconciled rather than just the root entry. Measured at head with
|
|
509
|
+
`test/fixtures -> ../elsewhere` and a committed `elsewhere/violator.edi`: present, exit 1; removed
|
|
510
|
+
from disk but still in the index, `OK - no hits` at **exit 0** - verbatim the emptied-root shape
|
|
511
|
+
these rules exist to close. It is **PRE-EXISTING** (base is exit 0 over the same tree), so it is
|
|
512
|
+
not a regression; it is **disclosed and not closed**, because covering it means reconciling against
|
|
513
|
+
a second pathspec derived from the link target, which is the same scope decision as the two below.
|
|
514
|
+
**State the closure as "within the declared roots, as git names them", never as a universal.**
|
|
515
|
+
|
|
516
|
+
**A COUNT DOES NOT CLOSE THIS, WHICH IS WHY THE SECOND RULE READS THE INDEX.** An emptied root
|
|
517
|
+
contributes zero and a total still looks like a total, so a denominator measures the roots that DID
|
|
518
|
+
exist. Only naming the corpus from a source OUTSIDE the walk separates "read it and found nothing"
|
|
519
|
+
from "never opened it".
|
|
520
|
+
|
|
521
|
+
**The ignore rule is stated exactly, because its short form is false.** `git check-ignore` consults
|
|
522
|
+
the index by default, so it answers NOT-ignored for a path that is tracked even when a `.gitignore`
|
|
523
|
+
rule names it (only `--no-index` says otherwise, and this scanner does not pass it). So the walk
|
|
524
|
+
SCANS a tracked-and-ignored file and this check correspondingly REFUSES when one is missing from
|
|
525
|
+
disk. Both halves of that pair are asserted; what the rule really exempts is the UNTRACKED ignored
|
|
526
|
+
file, which is never in `git ls-files` at all.
|
|
527
|
+
|
|
528
|
+
**One behaviour outside a git checkout changed and is stated rather than buried**: where
|
|
529
|
+
`git ls-files` cannot answer, the sweep now refuses at exit 2 instead of reporting clean, because
|
|
530
|
+
"git could not tell me" and "git told me there is nothing" are the two answers this check exists to
|
|
531
|
+
keep apart. `scripts/` is not in the published tarball, so every caller is inside a checkout.
|
|
532
|
+
|
|
533
|
+
**Not closed here, measured rather than assumed, and both a scope decision rather than this one**:
|
|
534
|
+
a tracked file directly under `test/` is enumerated by NEITHER route (exit 0 over a payload that
|
|
535
|
+
hits as an ordinary fixture), and an index entry at exactly a scan root's own path matches no
|
|
536
|
+
`--staged` clause (exit 0 over the same payload). **NEITHER IS CLOSED BY THE RECONCILIATION ABOVE,
|
|
537
|
+
and the reason is worth stating because it reads as though it should be**: the reconciliation
|
|
538
|
+
compares the walk against the index WITHIN the declared scope, so a path nothing declares in scope
|
|
539
|
+
is absent from both sides of the comparison. Widening the scope is its own change, because
|
|
540
|
+
**enumerating those files buys only the `scanCommonShapes` floor**: they are `.ts` sources whose
|
|
541
|
+
fixtures are string literals, so `looksLikeX12` is false and the NM1 / DMG / PER / service-date
|
|
542
|
+
recognisers never run. **🩺 NAME THAT FLOOR AS THREE DETECTORS AND NEVER AS TWO.** A draft of this
|
|
543
|
+
entry said "the dashed-SSN and email floor" and a refuter measured it false: the `REF*SY` undashed
|
|
544
|
+
nine-digit SSN recogniser is **not segment-aware either** and fires on a bare string literal
|
|
545
|
+
exactly as the other two do. The two-detector wording understated the deferred scope by precisely
|
|
546
|
+
the shape a dashed-SSN regex cannot see, which is the worst direction to be wrong in. Derive the
|
|
547
|
+
set from `scanCommonShapes`, never from prose - this sentence included. **Widening the enumeration and widening the recogniser are two sides of it,
|
|
548
|
+
each in addition to the other.** Measured on this package's own corpus, the current recogniser over
|
|
549
|
+
the tracked non-fixture files under `test/` finds 8 shapes in exactly one file,
|
|
550
|
+
`test/scripts/phi-scan.test.ts`, which is this scanner's own negative-control corpus; excusing it
|
|
551
|
+
needs an exclusion surface that does not exist, because a bare `--allow-fixture` seeds the
|
|
552
|
+
positional path set and selects `paths` mode. The **enumerate-then-read window** is likewise
|
|
553
|
+
untouched, and the reconciliation does not change its reachability: it runs on the enumeration,
|
|
554
|
+
before any target is read, and it neither widens a root nor reads a file. No library code changed
|
|
555
|
+
and no published type changed.
|
|
130
556
|
|
|
131
557
|
### Fixed
|
|
132
558
|
|
|
559
|
+
- **🩺 `build278Request` / `build278Response` refuse a review whose HL-03 level code is outside `EV`
|
|
560
|
+
and `SS`, instead of emitting a review its own reader cannot decode**
|
|
561
|
+
(`REFUSAL-MESSAGE-PHI-ECHO`). `Build278ReviewSpec.levelCode` is the **one** caller-supplied HL-03 in
|
|
562
|
+
the library: every other level on every builder's spine is a module constant selected by tree
|
|
563
|
+
position. It is typed `"EV" | "SS"`, but a JS or JSON caller reaches it with anything, and `esc`
|
|
564
|
+
type-checks and escapes without constraining the value.
|
|
565
|
+
|
|
566
|
+
**State the failure mode precisely rather than escalating it: it FAILS TO DECODE, it does not decode
|
|
567
|
+
WRONGLY.** The read side is deliberately tolerant at these two levels (they attach under a
|
|
568
|
+
subscriber or a dependent, so they are absent from the expected-parent map), so an out-of-enum HL-03
|
|
569
|
+
falls to the walker's `else` arm, the review loop never opens, and `get278Response` returns the
|
|
570
|
+
review **and its HCR-01 certification decision** as absent, with `warnings: []`. Nothing is
|
|
571
|
+
mis-read: no decision comes back as a different decision, and the bytes stay on `tx.segments`. That
|
|
572
|
+
is the better of the two failure modes. It is still not one to emit, because HCR-01 is a
|
|
573
|
+
safety-critical field this library places verbatim and never infers, so the builder refuses exactly
|
|
574
|
+
as `build834` refuses a maintenance type it cannot name. Reuses `X12_278_BUILD_INVALID_SPEC`; no
|
|
575
|
+
error code was added.
|
|
576
|
+
|
|
577
|
+
**The guard resolves the level through the same `?? "EV"` expression the emitter uses**, rather than
|
|
578
|
+
testing `!== undefined`, so a `null` from a `JSON.parse`d spec is absent rather than forged. Testing
|
|
579
|
+
`undefined` alone refused a spec the emitter would have defaulted and built, which is
|
|
580
|
+
`X12-CALLER-VALUE-RESIDUALS`' recorded regression running the other way; the slice's own test caught
|
|
581
|
+
it before it shipped. It reaches nested service reviews and dependent reviews, not only the first.
|
|
582
|
+
|
|
583
|
+
**No caller who was getting the review into the document is broken.** An out-of-enum level never
|
|
584
|
+
produced a decodable review, so there is no value that worked and stops working, and a TypeScript
|
|
585
|
+
caller could not reach the arm at all. `PRE-EXISTING` at `4a5a943` and disclosed there as filed not
|
|
586
|
+
fixed; this closes it.
|
|
587
|
+
|
|
588
|
+
- **🩺 A `REF`, `N3`, `N4` or `PER` after a dropped 837 `LX` does not attach itself to the last
|
|
589
|
+
named party** (`X12-837-LOOP-RESIDUALS`). Through `0.0.10`, the release published as this was
|
|
590
|
+
written, an `LX` arriving with **no `CLM` open** reported the dropped service line and then left
|
|
591
|
+
the previous `NM1`'s party still addressable. Because the payer accumulator is what the **next**
|
|
592
|
+
`CLM` opens against, the values surfaced on a **later claim**: measured, a line-item control
|
|
593
|
+
number in `payer.references`, a street address in `payer.address`, and a contact in
|
|
594
|
+
`payer.contacts`, none of which the sender put there. On that
|
|
595
|
+
route all seven of `DTP` / `AMT` / `NTE` / `REF` / `N3` / `N4` / `PER` are discarded; the
|
|
596
|
+
`DTP` / `AMT` / `NTE` already were at `0.0.10`, and the other four are what changed.
|
|
597
|
+
**The route-dependence is unchanged and still matters:** with a `CLM` open, a trailing `DTP` /
|
|
598
|
+
`AMT` / `NTE` / `REF` still lands on the enclosing claim.
|
|
599
|
+
**If you parsed with `0.0.10` or earlier and read an entity's `address`, `contacts` or
|
|
600
|
+
`references`, those slots could be carrying line-level values from a dropped Loop 2400.**
|
|
601
|
+
|
|
602
|
+
**🩺 This is a trade, and the cost is that a conformant entity segment can now be dropped.** The
|
|
603
|
+
TR3s nest Loop 2400 inside Loop 2300 and say nothing about an `LX` elsewhere, so which party a
|
|
604
|
+
segment following a **stray** `LX` belongs to is not derivable from the spec in either direction.
|
|
605
|
+
Where the `LX` was injected into an **entity** loop, the segments after it really were that
|
|
606
|
+
entity's: measured, a payer that kept its `PO BOX` address, its `2U` secondary id and its contact
|
|
607
|
+
at `0.0.10` now comes back with `address: undefined`, `references: []` and `contacts: []`. The
|
|
608
|
+
direction was chosen because a mis-attribution puts a value on an object the sender never put it
|
|
609
|
+
on and is indistinguishable from real data, whereas the bytes of a discarded segment are still on
|
|
610
|
+
`tx.segments`. **That discard shipped SILENT in the change above and no longer is:** it now raises
|
|
611
|
+
`X12_837_ENTITY_SEGMENT_DISCARDED_AFTER_LX` at each discarded segment, listed under `### Added`
|
|
612
|
+
and never released without it. `X12_837_SERVICE_LINE_DROPPED` at that `LX` still reports the
|
|
613
|
+
**service line** and not an entity's address, id or contact, so it never named this loss. The loss
|
|
614
|
+
itself is unchanged and is still pinned by tests; `KNOWN-LIMITATIONS.md` states both.
|
|
615
|
+
|
|
616
|
+
- **🩺 `X12_837_UNKNOWN_VARIANT` now anchors at the `ST` instead of the `BHT`**
|
|
617
|
+
(`X12-837-LOOP-RESIDUALS`). The warning's `position.segmentIndex` was `1`, which in a
|
|
618
|
+
transaction-scoped position is `tx.segments[1]` and in an 837 is the **BHT**, a segment with no
|
|
619
|
+
part in resolving a variant. It is now `0`, the **ST**, which carries the ST-03 the resolution
|
|
620
|
+
reads. A consumer joining this warning back to the document therefore gets a different segment
|
|
621
|
+
than it did on `0.0.10`. No `elementIndex` is set, deliberately: one of the two routes into this
|
|
622
|
+
warning is an ST-03 that is absent entirely, and on that route the `ST` has no element 3 to name.
|
|
623
|
+
|
|
624
|
+
- **🩺 An 837 service segment with no Loop 2400 open no longer takes a charge, a quantity and a
|
|
625
|
+
procedure code off the model in silence** (`X12-837-LOOP-RESIDUALS`). Through `0.0.10` - the
|
|
626
|
+
release published as this was written - an `SV1` / `SV2` / `SV3`
|
|
627
|
+
arriving with no Loop 2400 open found no service line to decode into, was read into nothing, and
|
|
628
|
+
reported on **no channel at all**: the claim came back with an empty `serviceLines` and
|
|
629
|
+
`warnings: []`, indistinguishable from a claim that genuinely had no lines. It now raises
|
|
630
|
+
`X12_837_SERVICE_SEGMENT_WITHOUT_LX` at the service segment itself.
|
|
631
|
+
|
|
632
|
+
**The segment is still not decoded into any line, and that is deliberate.** `SV1-02` and `SV2-03`
|
|
633
|
+
are both the line charge, so reading a service segment into a line the walker never opened
|
|
634
|
+
mis-reads money. Refusing to read is the safe half; doing it silently was the defect. **This says
|
|
635
|
+
nothing about how the variant resolved**, and `KNOWN-LIMITATIONS.md` now discloses why: a caller's
|
|
636
|
+
`type` option wins first, and absent one, where `ST-03` names none of the three known
|
|
637
|
+
implementation conventions, the reader falls back to the first `SVx` in the transaction body,
|
|
638
|
+
orphans included, so a stray `SV2` re-types the whole submission. That is pre-existing behaviour, measured identical at `0.0.10`, and is deliberately
|
|
639
|
+
not narrowed here.
|
|
640
|
+
|
|
641
|
+
**Nothing else about the walk changed, and the other two codes are unmoved.** An `LX` that opened
|
|
642
|
+
no Loop 2400 still reports once, at the `LX`, and the service segments inside that dropped loop
|
|
643
|
+
stay quiet rather than naming the same loss twice under two codes; the suppression is scoped to
|
|
644
|
+
that loop, so a later orphan in the same transaction is still reported. A line that IS on the
|
|
645
|
+
model with an undecoded `SVx` still raises `X12_837_SERVICE_LINE_NOT_DECODED` and nothing else.
|
|
646
|
+
**This does not touch the model:** `charge` and `units` are still typed `X12Decimal`, and an
|
|
647
|
+
absent `SV1-02` on a line that DID open still reads a confident `0`, which closes only with the
|
|
648
|
+
deferred `X12Decimal | undefined` change.
|
|
649
|
+
|
|
650
|
+
- **🩺 An X212 277 this library emitted with a service line was short a required element, and every
|
|
651
|
+
277 it read silently discarded the submitted units** (`X12-277-SVC07-NOT-DECODED`). `get277Status`
|
|
652
|
+
read SVC-01 through SVC-04 and stopped; `build277` emitted exactly those four. SVC-07, the units of
|
|
653
|
+
service count, is usage **R** in `005010X212`, so **every X212 277 this library produced with a
|
|
654
|
+
Loop 2220 service line was non-conformant on the wire**, and the count a submitter sent never
|
|
655
|
+
reached the model on the way back in. Both directions now carry it, and `build277` refuses rather
|
|
656
|
+
than emit a line without it (see `### Changed`).
|
|
657
|
+
|
|
658
|
+
**Only bytes could have caught this.** Every service-line assertion in the suite was a `build277`
|
|
659
|
+
to `get277Status` round trip through one self-consistent four-element map, so it was green for any
|
|
660
|
+
subset the two modules agreed on, including a subset missing a required element. A round trip
|
|
661
|
+
cannot test an element map, and it cannot test an element usage at all. The new pins in
|
|
662
|
+
`test/transactions-status-277-svc07.test.ts` parse literal EDI and compare literal segment strings:
|
|
663
|
+
**16 of its 21 cases are red against a clean `c34770c` checkout**, and the five that are green are
|
|
664
|
+
the negative pins that were already true (SVC-05 stays unread, an absent SVC-07 warns nothing, a
|
|
665
|
+
277CA line with no units emits no placeholder).
|
|
666
|
+
|
|
667
|
+
**🩺 Read the scope literally: ONE element's usage was fixed, and an emitted service line is NOT
|
|
668
|
+
thereby conformant.** This was not a 277 usage audit and **no census of what remains is published**,
|
|
669
|
+
on purpose: other required elements of the same `SVC` are still unguarded and finding another is
|
|
670
|
+
expected rather than a new defect. To name two, `SVC-01` and `SVC-02` are both usage **R** in X212
|
|
671
|
+
and both optional on `Build277ServiceLineSpec`, so a line supplying only `unitsOfService` still
|
|
672
|
+
emits `SVC*******1~` with no refusal. **The missing guard is what is pre-existing, not that byte
|
|
673
|
+
string** - at base the same spec emitted a bare `SVC~`, because the slot did not exist. `SVC-03` is usage **R**
|
|
674
|
+
in X212 and usage **N** in X214 and is optional in both builders. And the read side still raises
|
|
675
|
+
**no** warning for an X212 277 that arrives with no SVC-07, because a lenient reader saying so
|
|
676
|
+
needs a new Tier-2 registry code. All of it is pre-existing and reproduces at `e3cdf49`; widening
|
|
677
|
+
the guard would have turned this into that audit, which is its own item.
|
|
678
|
+
`KNOWN-LIMITATIONS.md` carries the same statement.
|
|
679
|
+
|
|
680
|
+
- **🩺 A lookup keyed by document bytes can no longer be defeated by a key inherited from
|
|
681
|
+
`Object.prototype` (`X12-VARIANT-LOOKUP-PROTOTYPE`).** Several lookup tables were plain object
|
|
682
|
+
literals, which inherit `Object.prototype`, so an inbound value matching **any own property of
|
|
683
|
+
`Object.prototype`** resolved TRUTHY against them. That set is engine- and version-dependent and
|
|
684
|
+
is deliberately not enumerated here; on the Node 22 this package targets it is twelve members, and
|
|
685
|
+
a draft of this entry listed eight. `Object.freeze` did not help: it seals the own properties and
|
|
686
|
+
changes nothing about the prototype chain. What that cost, measured at `a33c208`:
|
|
687
|
+
- **🩺 An ST-03 of `constructor` took every 837 service line off the model, silently.**
|
|
688
|
+
`VARIANT_BY_ICR` answered the `Object` constructor, so `submission.variant` was a **function**
|
|
689
|
+
rather than one of `P` / `I` / `D` / `unknown`, `X12_837_UNKNOWN_VARIANT` never fired, and
|
|
690
|
+
`openServiceLine` - which answers `undefined` for anything that is not a known variant - dropped
|
|
691
|
+
**every Loop 2400**, its charge, its units and its procedure code, with `warnings: []`.
|
|
692
|
+
- **🩺 `lookupCarc("constructor")` answered a `CodeListEntry` whose `description` was a function**,
|
|
693
|
+
on a field typed `string`, and suppressed `X12_UNKNOWN_CARC`. The guard is now in `makeLookup`,
|
|
694
|
+
the single factory every bundled snapshot is read through, so the other bundled lists are
|
|
695
|
+
covered by the same fix.
|
|
696
|
+
- **🩺 An HI qualifier of `constructor` suppressed `X12_UNKNOWN_HI_QUALIFIER`** while still
|
|
697
|
+
landing the code with `codeSystem: "unknown"` - an unresolvable code system on a diagnosis, with
|
|
698
|
+
nothing on any channel to say so.
|
|
699
|
+
- **An HL-03 of `constructor` raised `X12_HL_PARENT_LEVEL_INVALID`** against a level code the
|
|
700
|
+
walker has no expectation for: a structural violation the document never made. The 271 / 277 /
|
|
701
|
+
278 readers were never exposed - they share `src/transactions/shared/hl.ts`, which has always
|
|
702
|
+
guarded this read with `hasOwnProperty`; the 837's local copy did not.
|
|
703
|
+
- **`isClaimAdjustmentGroupCode("constructor")` answered `true`** and narrowed a non-code to
|
|
704
|
+
`ClaimAdjustmentGroupCode`. It used `in`, which **walks the prototype chain**: the safe-looking
|
|
705
|
+
form, not the safe form. It is now `Object.hasOwn`.
|
|
706
|
+
|
|
707
|
+
Every one of these now behaves exactly as it does for any other unrecognized value, which is the
|
|
708
|
+
whole of the claim: each fixed case is pinned against an honest-control case in the same slot, and
|
|
709
|
+
the suite derives its key list from `Object.getOwnPropertyNames(Object.prototype)` at run time
|
|
710
|
+
rather than from a list anyone maintains.
|
|
711
|
+
Tables this package declares itself are built through the new internal
|
|
712
|
+
`wireLookup` (`Object.create(null)`, then frozen), so the absent prototype protects future read
|
|
713
|
+
sites too; tables it receives from a caller are guarded with `Object.hasOwn` at the read, the form
|
|
714
|
+
`shared/hl.ts` already used. **No model shape and no message text changed**, and no code-list
|
|
715
|
+
content changed.
|
|
716
|
+
|
|
717
|
+
- **🩺 An 837 `LX` that opens no Loop 2400 no longer drops the whole service line in silence
|
|
718
|
+
(`X12-VARIANT-LOOKUP-PROTOTYPE`).** Two routes reached it: an `LX` arriving before any `CLM`,
|
|
719
|
+
which produced `claims: []` and `warnings: []` even with a charge and units on the wire; and a
|
|
720
|
+
submission whose variant is not one of `P` / `I` / `D`, where `openServiceLine` answers
|
|
721
|
+
`undefined` and the line was never opened. Both now raise `X12_837_SERVICE_LINE_DROPPED` at the
|
|
722
|
+
`LX`. **Retention is unchanged and nothing is invented**: no claim is synthesized to hold an
|
|
723
|
+
orphan line and no variant is guessed, because either would put structure on the model that the
|
|
724
|
+
sender did not send. The segments were, and remain, verbatim on `tx.segments`.
|
|
725
|
+
**Read the scope of this code literally, because three things sit just outside it and all three
|
|
726
|
+
are unchanged.** It is anchored at the `LX`, so an `SVx` arriving with **no `LX` at all** is still
|
|
727
|
+
dropped in silence (`PRE-EXISTING`, disclosed not fixed). It does **not** travel with
|
|
728
|
+
`X12_837_UNKNOWN_VARIANT`: a caller-supplied `type` outside `"P" | "I" | "D"`, which only a
|
|
729
|
+
JavaScript or `JSON.parse`d caller can pass, reaches the second route without it, so read
|
|
730
|
+
`submission.variant` to tell the routes apart. And what becomes of a `DTP` / `AMT` / `NTE` /
|
|
731
|
+
`REF` following a dropped `LX` is route-dependent and is **not** simply "absent": see
|
|
732
|
+
`KNOWN-LIMITATIONS.md`, which states both routes.
|
|
733
|
+
|
|
734
|
+
- **🩺 An 837 service line whose `SVx` never decoded no longer reads `0` / `0` in silence
|
|
735
|
+
(`X12-837-SV-SILENT-ZERO`).** `get837Claims` resolves ONE variant for the whole submission (the
|
|
736
|
+
caller's `type` option, else ST-03's implementation-convention reference, else the first `SVx`
|
|
737
|
+
present). `openServiceLine` seeds a line's `charge` and `units` at `X12Decimal.ZERO`, and
|
|
738
|
+
`decodeSv1` / `decodeSv2` / `decodeSv3` each **return before reading anything** when the line's
|
|
739
|
+
variant is not theirs. So an `SV2` line on a submission that resolved to Professional read back as
|
|
740
|
+
a `$0.00` charge for `0` units with `warnings: []`, with the procedure code, modifiers, unit of
|
|
741
|
+
measure and place of service equally undecoded; a line carrying no `SVx` at all did the same. This
|
|
742
|
+
is the residual `X12-QUANTITY-SILENT-DEFAULTS` disclosed rather than fixed, and it is the same
|
|
743
|
+
fabrication one level up: a slot a reader never read cannot warn on the decimal channel.
|
|
744
|
+
|
|
745
|
+
Measured against `d8b5085`: a conformant 837I whose ST-03 is flipped to `005010X222A2` read back
|
|
746
|
+
`0` / `0` on both service lines with `warnings: []`, where the same bytes read with their own
|
|
747
|
+
variant give `1500` / `1`. Three further probes were equally silent at base (the same file with
|
|
748
|
+
`{ type: "P" }`, an `LX` with no `SVx` at all, and the 837D fixture with `{ type: "I" }`). All four
|
|
749
|
+
now raise `X12_837_SERVICE_LINE_NOT_DECODED`; the honest 837P and 837I controls stay silent.
|
|
750
|
+
|
|
751
|
+
**Not decoding the foreign `SVx` is unchanged and is not the defect.** `SV1-02` and `SV2-03` are
|
|
752
|
+
both the line charge, so reading an `SV2` into a Professional-shaped line would mis-read money
|
|
753
|
+
rather than fail to read it. **The model is unchanged too:** `charge` and `units` are still typed
|
|
754
|
+
`X12Decimal` and still read `0`, so a consumer that never looks at `.warnings` sees what it saw
|
|
755
|
+
before. Making those slots `X12Decimal | undefined` remains a breaking change and its own slice.
|
|
756
|
+
The line is still retained, every segment stays verbatim on `tx.segments`, and a variant that
|
|
757
|
+
resolves to nothing at all is still the separate `X12_837_UNKNOWN_VARIANT` case, where no line is
|
|
758
|
+
opened and no `0` is fabricated. `KNOWN-LIMITATIONS.md` says how far that second case reaches.
|
|
759
|
+
|
|
760
|
+
- **🩺 An unparseable decimal no longer becomes a confident zero in silence
|
|
761
|
+
(`X12-QUANTITY-SILENT-DEFAULTS`).** `elementDecimalOrZero` returned `X12Decimal.ZERO` for an
|
|
762
|
+
element that was **present** and did not decode, with no warning on any channel. A payer amount of
|
|
763
|
+
`1,234.56` (a thousands separator, which X12 forbids in an R-type element), `$450.00`, `450.00USD`
|
|
764
|
+
or `N/A` read back as `0` and was indistinguishable from a payer that paid nothing. A fabricated
|
|
765
|
+
amount presented as read is the same harm class as a mis-read quantity: a number nobody sent,
|
|
766
|
+
arriving as though somebody had.
|
|
767
|
+
|
|
768
|
+
The same root cause one type away: `elementDecimal` answered `undefined` for **both** "the sender
|
|
769
|
+
omitted this element" and "the sender sent bytes this library could not read", also unwarned, so
|
|
770
|
+
`undefined` at a quantity site meant "not decoded" rather than "absent" and no consumer could tell
|
|
771
|
+
which.
|
|
772
|
+
|
|
773
|
+
Every decimal read in all six transaction readers (835, 837P/I/D, 277 / 277CA, 271, 834, 820) now
|
|
774
|
+
routes through a sink and emits `X12_UNPARSEABLE_DECIMAL` at the failing `position.elementIndex`.
|
|
775
|
+
Measured on nine probes across those readers, one per site class, each substituting a single
|
|
776
|
+
numeric token in a committed fixture: at `0.0.9` **seven of the nine were completely silent**, and
|
|
777
|
+
the two that were not were the 835's `BPR-02` and `CLP-04`, which produced only
|
|
778
|
+
`X12_835_REMIT_BALANCE_MISMATCH` - a warning that names an equation, never an element, and that
|
|
779
|
+
exists in no other reader. All nine now carry `X12_UNPARSEABLE_DECIMAL` naming the element.
|
|
780
|
+
|
|
781
|
+
**What did NOT change, deliberately.** The model is unchanged: a slot typed `X12Decimal` still
|
|
782
|
+
reads `X12Decimal.ZERO`, an optional slot still reads `undefined`, and some rows are dropped whole.
|
|
783
|
+
The warning is a property of the READ rather than of what the reader then does with the result, so
|
|
784
|
+
every one of those outcomes carries it; no list of them is published, because a first draft
|
|
785
|
+
enumerated three and a review measured a fourth. A slot typed `X12Decimal` cannot express "did not
|
|
786
|
+
decode", and changing every such slot to `X12Decimal | undefined` is a breaking model change that
|
|
787
|
+
belongs in its own slice. So a consumer that reads only the model and never looks at `.warnings`
|
|
788
|
+
sees exactly what it saw before. **Gate on the warning.** Also unchanged: an **absent** element
|
|
789
|
+
still returns `X12Decimal.ZERO` and still does **not** warn, because "missing means zero" is the
|
|
790
|
+
documented convention of those slots. That does **not** make every unwarned `0` trustworthy: the
|
|
791
|
+
guarantee is exactly that an unwarned `0` **at an element a reader decoded** is a zero the sender
|
|
792
|
+
sent or omitted, and a slot a reader never read cannot warn. `KNOWN-LIMITATIONS.md` carries the
|
|
793
|
+
residual and the one measured instance of that inversion in full.
|
|
794
|
+
|
|
133
795
|
- **A number passed where a builder's types say `string` no longer emits an EMPTY element. It is now
|
|
134
796
|
REFUSED, and it is deliberately not coerced.** `escapeRelease` opened with
|
|
135
797
|
`if (value.length === 0) return value;` and then looped to `value.length`. On a number `.length` is
|
|
@@ -169,9 +831,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
169
831
|
`TypeError`, so its duty is to survive anything; an emitted document's duty is to invent nothing.
|
|
170
832
|
|
|
171
833
|
**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
|
-
|
|
834
|
+
through it.** All of what follows was pre-existing, measured and unchanged here; it is **closed by
|
|
835
|
+
the `X12-DECIMAL-BYPASSES-THE-GUARD` entry below**, in the same unreleased window, so the residual
|
|
836
|
+
described here never reached a published version. **`esc` slots read `.toString()` off what the
|
|
837
|
+
types say is an `X12Decimal`**, so a raw number arrives already a string and is passed through: a
|
|
175
838
|
`patientResponsibilityAmount` of `0.1 + 0.2` still emits `…*0.30000000000000004*…`, `1e21` still
|
|
176
839
|
emits `…*1e+21*…` and `NaN` still emits `…*NaN*…`, each with zero warnings, which are the exact
|
|
177
840
|
three renderings this entry names as disqualifying, and an 837 service-line `units` reaches SV1-04
|
|
@@ -183,7 +846,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
183
846
|
`levelCode` (HL-03), `groupDate` / `groupTime` (GS-04 / GS-05), and the 837's `lineNumber`
|
|
184
847
|
(LX-01). **Those are examples and not a census, deliberately:** two drafts of this
|
|
185
848
|
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
|
|
849
|
+
is cut back rather than grown a third time. AK9-01 is an `ID` element bound to X12 code list 715,
|
|
187
850
|
and `build999`'s own accept-with-errors guard compares it against `"A"`, so a number walks past it
|
|
188
851
|
the same way it walked past `patientControlNumber === ""`. **The fixed-width ISA slots** go through
|
|
189
852
|
`pad` / `padControl`, so a number throws an untyped `TypeError` and a numeric
|
|
@@ -194,6 +857,82 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
194
857
|
`new String("PT-ACCT-001")` built cleanly at `0.0.8` and is refused now, because `typeof` it is
|
|
195
858
|
`"object"`.
|
|
196
859
|
|
|
860
|
+
- **A raw `number` in an `X12Decimal` slot is refused instead of rendered, and the type check now
|
|
861
|
+
covers every element of every segment emitted through a builder's segment joiner**
|
|
862
|
+
(`X12-DECIMAL-BYPASSES-THE-GUARD`). This closes the
|
|
863
|
+
two classes the entry above disclosed and deliberately did not fix.
|
|
864
|
+
|
|
865
|
+
**The decimal half.** `makeCallerEscaper` type-checks what reaches `esc`, but an `X12Decimal` slot
|
|
866
|
+
hands `esc` a `value.toString()`, and a raw `number` answers that with a perfectly good string. So
|
|
867
|
+
the value arrived already a string and the guard never applied. Measured at `15abbd4` with
|
|
868
|
+
`warnings.length === 0` in every case: a `patientResponsibilityAmount` of `0.1 + 0.2` emitted
|
|
869
|
+
`CLP*PT-ACCT-001*1*500.00*450.00*0.30000000000000004*…`, `1e21` emitted `…*1e+21*…`, `NaN` emitted
|
|
870
|
+
`…*NaN*…`, an 837 service-line `units` of `0.1 + 0.2` emitted
|
|
871
|
+
`SV1*HC:99213*150.00*UN*0.30000000000000004***1`, and a diagnosis `monetaryAmount` reached
|
|
872
|
+
`HI*ABK:J20.9:::0.30000000000000004`. **Two of those three renderings the library cannot parse
|
|
873
|
+
back** - `X12_DECIMAL_RE` rejects exponent notation and `NaN` - so they did not round-trip; the
|
|
874
|
+
IEEE-754 artifact is worse in the other direction, being well-formed enough that nothing downstream
|
|
875
|
+
refuses it. Every such slot now emits through that builder's `escDec`, over `requireCallerDecimal`.
|
|
876
|
+
|
|
877
|
+
**Why refuse and not round, which is the decision:** rounding `0.1 + 0.2` to `0.30` guesses cents
|
|
878
|
+
and to `0.3` guesses tenths, and guessing the scale of a monetary amount is what `X12Decimal` exists
|
|
879
|
+
to prevent. Same answer as the entry above, for a reason specific to this slot: every one of these
|
|
880
|
+
is _typed_ `X12Decimal` already, so a raw `number` is a caller who defeated their own type checker
|
|
881
|
+
and no supported numeric path is taken away.
|
|
882
|
+
|
|
883
|
+
**The raw-slot half.** The string-typed positions that never called the escape helper at all are
|
|
884
|
+
routed through it: `build999`'s `groupControlNumber` (GS-06 / GE-02), `transactionSetControlNumber`
|
|
885
|
+
(ST-02 / SE-02), `disposition` (AK9-01 and IK5-01) and `groupResponsibleAgency` (GS-07);
|
|
886
|
+
`groupDate` / `groupTime` (GS-04 / GS-05) in all seven domain builders; `build278`'s `levelCode`
|
|
887
|
+
(HL-03); and `build837`'s `lineNumber` (LX-01). That closes the delimiter hole on them too -
|
|
888
|
+
`build999` with a `groupControlNumber` of `"1*BOGUS"` emitted `GS*FA*…*1*BOGUS*X*005010X231A1`,
|
|
889
|
+
shifting GS-07 and GS-08 by one, and now emits `1?*BOGUS`.
|
|
890
|
+
|
|
891
|
+
**And the part that is a property rather than a list.** Three consecutive drafts of the entry above
|
|
892
|
+
published an exhaustive counted census of the slots that bypass `esc`, and adversarial review
|
|
893
|
+
measured all three false, each time by finding one more. Counting a fourth time would repeat that,
|
|
894
|
+
so the check moved to the one place every element must pass: **the segment join**.
|
|
895
|
+
`requireCallerSegment` type-checks every element of every segment emitted **through a builder's
|
|
896
|
+
`seg` / `joinSeg` helper**, on every route in, `escDec` included. `esc` is optional on a slot; the
|
|
897
|
+
join is not. It also names the slot the way the spec does - `build999: "AK9"-01 must be a
|
|
898
|
+
string, …` - which `esc` cannot, being unary.
|
|
899
|
+
|
|
900
|
+
**What is deliberately still NOT claimed.** Type safety is structural here; **delimiter safety is
|
|
901
|
+
per-slot**. A `string` carrying an active delimiter in a slot that skipped `esc` is still emitted
|
|
902
|
+
verbatim, because the segment guard passes it - only the slots named above were routed. And the
|
|
903
|
+
fixed-width ISA slots go through `pad` / `padControl` and not through the segment joiner either, so
|
|
904
|
+
they remain as the entry above describes them: an untyped `TypeError`, or for
|
|
905
|
+
`interchangeControlNumber` a typed refusal whose text misleadingly says "exceeds the 9-char spec
|
|
906
|
+
limit". Both terminate; neither is silent; neither is improved here.
|
|
907
|
+
|
|
908
|
+
**Two more exclusions, both found by the refuter against a draft that claimed more than the code
|
|
909
|
+
did, both `PRE-EXISTING` and both now pinned by tests rather than argued away.** `buildTA1` uses no
|
|
910
|
+
segment joiner and no escape helper - it joins its five caller-supplied elements directly - so a
|
|
911
|
+
numeric or `undefined` `interchangeControlNumber` still emits silently as `TA1**250101*1200*A*000`;
|
|
912
|
+
TA1-01 is the reassociation key back to the acknowledged interchange, so it is filed as its own
|
|
913
|
+
item rather than widened into here. And **`build835`'s balance-equation amounts refuse UNTYPED**:
|
|
914
|
+
`enforceBalance(spec)` runs before the escaper is built and calls `X12Decimal` methods on the
|
|
915
|
+
caller's value, so `requireCallerDecimal` is unreachable on them and the caller gets a plain
|
|
916
|
+
`TypeError` with no `code` (some saying the value was "tampered with") instead of the typed
|
|
917
|
+
refusal. **The rule, rather than a list, because a first draft of this disclosure published a
|
|
918
|
+
closed list of four and a refuter measured it incomplete:** a slot refuses untyped exactly when the
|
|
919
|
+
balance guard reads it as a term of one of the three TR3 X221A1 §1.10.2 invariants in
|
|
920
|
+
`src/transactions/remit/balance.ts`. **Named by SPEC FIELD and not by element number, because the
|
|
921
|
+
next draft used element numbers and got one wrong:** `payment.totalActualPayment`,
|
|
922
|
+
`claim.totalChargeAmount`, `claim.totalPaymentAmount`, every `adjustments[].amount` at claim and
|
|
923
|
+
line level, `serviceLine.chargeAmount`, `serviceLine.paymentAmount` and
|
|
924
|
+
`providerAdjustments[].amount`. Every other `X12Decimal` field refuses typed, including
|
|
925
|
+
`claim.patientResponsibilityAmount`, `serviceLine.paidUnitsOfService` and every `amounts[].amount`.
|
|
926
|
+
Both arms are pinned. Reordering the balance guard changes the refusal precedence of an
|
|
927
|
+
out-of-balance remit, which is its own decision.
|
|
928
|
+
|
|
929
|
+
**AK9-01 was the sharpest of the raw slots** - an `ID` element bound to X12 code list **715**
|
|
930
|
+
(a data element number, and its values are a code _list_; this repo's own `src/transactions/ack/codes.ts`
|
|
931
|
+
had it right and four other places said "code source"), so a number there told a receiver nothing
|
|
932
|
+
about whether the functional group was accepted, and `build999`'s own `X12_ACK_ACCEPT_WITH_ERRORS`
|
|
933
|
+
guard compares `disposition === "A"`, which a number walked past exactly as it walked past
|
|
934
|
+
`patientControlNumber === ""`.
|
|
935
|
+
|
|
197
936
|
- **The `attw` publish gate no longer passes a tarball that carries no type declarations.** The
|
|
198
937
|
`attw` script was the bare CLI, and `@arethetypeswrong/cli` returns 0 whenever its analysis found
|
|
199
938
|
no types at all, before it reads the problem list. For a package that ships types, "This package
|