@cosyte/x12 0.0.10 → 0.0.12
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 +457 -13
- package/KNOWN-LIMITATIONS.md +804 -0
- package/README.md +7 -5
- package/dist/index.cjs +88 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +91 -10
- package/dist/index.d.ts +91 -10
- package/dist/index.mjs +87 -12
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,47 @@ 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
|
+
|
|
12
53
|
- **🩺 `X12ServiceLineStatus.unitsOfService` and `Build277ServiceLineSpec.unitsOfService`, the 277
|
|
13
54
|
Loop 2220 SVC-07 units of service count** (`X12-277-SVC07-NOT-DECODED`). X12 element 380
|
|
14
55
|
(Quantity), decoded as an `X12Decimal` like every other quantity in this library and never via
|
|
@@ -98,6 +139,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
98
139
|
|
|
99
140
|
### Changed
|
|
100
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
|
+
|
|
101
182
|
- **🩺 BREAKING: `build277` now REFUSES a Loop 2220 service line that carries no `unitsOfService`
|
|
102
183
|
(`X12-277-SVC07-NOT-DECODED`).** SVC-07 is usage **R** in TR3 `005010X212`, so a service line
|
|
103
184
|
without it cannot be emitted as a conformant 277. The refusal is the existing
|
|
@@ -173,6 +254,69 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
173
254
|
|
|
174
255
|
### Security
|
|
175
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
|
+
|
|
176
320
|
- **The PHI scanner refuses an in-scope entry that is not a regular file, on both of its enumerating
|
|
177
321
|
routes.** A symbolic link under a scan root pointing at a PHI-bearing file used to scan CLEAN on
|
|
178
322
|
both, so the pre-commit gate and CI both reported "no hits" over a capture the scan never read.
|
|
@@ -204,7 +348,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
204
348
|
exited 1 and printed that shape, because `git show` handed the path text straight to the
|
|
205
349
|
cross-cutting shape pass.
|
|
206
350
|
|
|
207
|
-
|
|
351
|
+
**`T` was added to the `--staged` filter, and it is the one letter that made the mode check
|
|
208
352
|
reachable.** Replacing a TRACKED regular file with a link is neither an add nor a modify: measured
|
|
209
353
|
on this tree, `git diff --cached --raw --diff-filter=AM` returned zero rows for that change while
|
|
210
354
|
the unfiltered `--raw` showed `:100644 120000 <sha> <sha> T`. Without `T` the record died before
|
|
@@ -225,21 +369,284 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
225
369
|
`readFileSync`, which follows a link, so an explicitly named path that is a link to a PHI-bearing
|
|
226
370
|
file is scanned and hits (measured, exit 1).
|
|
227
371
|
|
|
228
|
-
**Not closed
|
|
229
|
-
enumerated by `--staged` at all,
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
the
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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.
|
|
240
556
|
|
|
241
557
|
### Fixed
|
|
242
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
|
+
|
|
243
650
|
- **🩺 An X212 277 this library emitted with a service line was short a required element, and every
|
|
244
651
|
277 it read silently discarded the submitted units** (`X12-277-SVC07-NOT-DECODED`). `get277Status`
|
|
245
652
|
read SVC-01 through SVC-04 and stopped; `build277` emitted exactly those four. SVC-07, the units of
|
|
@@ -759,6 +1166,29 @@ string, …` - which `esc` cannot, being unary.
|
|
|
759
1166
|
slot for a silent normalization; the tiers now name it, and note that a Tier-2 unexpected segment is
|
|
760
1167
|
warned about but not kept.
|
|
761
1168
|
|
|
1169
|
+
- **The universal about a stray 837 `LX` is cut back in the `src/` comments and 837 test-file headers
|
|
1170
|
+
the release before this one disclosed it in** (`X12-837-LOOP-RESIDUALS`). That release deleted it
|
|
1171
|
+
from the documents this package publishes and disclosed, rather than claimed closed, that it still
|
|
1172
|
+
stood in those two places; this is the rest of that sweep. **No count and no completeness claim is
|
|
1173
|
+
published**, because both are what went stale twice here: the sites swept are the `NM1` and `LX`
|
|
1174
|
+
case comments in `src/transactions/claim/get-837.ts`, and the headers, section comments and case
|
|
1175
|
+
titles of `test/transactions-claim-837-loop-residuals.test.ts`,
|
|
1176
|
+
`test/transactions-claim-837-discard-after-stray-lx.test.ts` and
|
|
1177
|
+
`test/transactions-claim-837-variant-lookup.test.ts`. Each said a trailing `N3` / `N4` / `PER` /
|
|
1178
|
+
`REF` "attached to" or "landed on" whichever party the last `NM1` left active, or that a party
|
|
1179
|
+
named after the `LX` "is addressable again", all of which read as all four kinds reaching every
|
|
1180
|
+
party. They do not: this reader does not surface every one of those kinds on every party. Each copy
|
|
1181
|
+
takes the qualifier already graded on the shipped surfaces, "wherever this reader surfaces that
|
|
1182
|
+
segment kind on that party at all", or is cut back to the measured instance beside it and that
|
|
1183
|
+
instance is named - a payer in every case but one, where it is a Loop 2320 other subscriber; no
|
|
1184
|
+
copy is given a new wording and **no per-kind, per-party map is published.** Counterfactual headings ("no longer attaches", "no longer leaves the last `NM1`
|
|
1185
|
+
addressable") lose the counterfactual only. The bullet in `documentation/agent-notes.md` beginning
|
|
1186
|
+
"RESIDUAL 1, MEASURED AT `93b2428`", and the identically-scoped paragraph in the test header it was
|
|
1187
|
+
written from, are **not** copies - both are scoped by "every trailing segment that attaches to a
|
|
1188
|
+
named party" - and are deliberately left alone. Nothing a consumer reads at runtime changed:
|
|
1189
|
+
neither the warning registry nor any `docs-content/` page nor the README carried the wording, and
|
|
1190
|
+
the executable behaviour of the swept comments is nil.
|
|
1191
|
+
|
|
762
1192
|
### Documented, not fixed
|
|
763
1193
|
|
|
764
1194
|
> **Superseded in part by the "Fixed" entry above, which ships in the same release.** The model half
|
|
@@ -767,6 +1197,20 @@ string, …` - which `esc` cannot, being unary.
|
|
|
767
1197
|
> not re-emit an orphan, so the warning still does not survive. The two entries are kept separate
|
|
768
1198
|
> because they were separate changes, and the sentences below describe the release before this one.
|
|
769
1199
|
|
|
1200
|
+
- **🩺 A repeated `NM1*87` inside one 837 Loop 2000A fuses two pay-to addresses into one**
|
|
1201
|
+
(`X12-837-LOOP-RESIDUALS`). The `N3` line collector appends and the `N4` merge falls back, so
|
|
1202
|
+
`X12Claim.payToAddress` carries a street from each of two addresses and an `N4` field the second
|
|
1203
|
+
address omitted. Reproduces unchanged at `0.0.11` and is **not** fixed here: two remedies were
|
|
1204
|
+
built and both were refuted for moving the loss rather than removing it, so per ADR 0016 the unit
|
|
1205
|
+
was cut back rather than given a third. Clearing the accumulator at the `NM1*87` erases the
|
|
1206
|
+
address a repeat with no `N3` / `N4` of its own did state; replacing at the first following write
|
|
1207
|
+
erases it on a VALUELESS `N3~` / `N4~`. **Neither erasure is a neutral absence, and that is the
|
|
1208
|
+
constraint the next attempt starts from:** `build837P/I/D` emits Loop 2010AB only where the slot
|
|
1209
|
+
is defined, and `emitAddress` emits `N3` only for a non-empty `lines` and `N4` only for a defined
|
|
1210
|
+
field, so an emptied slot re-emits as **no pay-to loop at all** and a half-emptied one re-emits a
|
|
1211
|
+
bare `NM1*87` with neither. **The emit side is therefore in scope for that fix from the start**,
|
|
1212
|
+
which is what makes it its own unit. `documentation/agent-notes.md` carries the measurements.
|
|
1213
|
+
|
|
770
1214
|
- **A segment that falls outside a transaction is dropped from the model, and its warning does not
|
|
771
1215
|
survive a round trip.** The envelope walker keeps body segments only while an ST..SE transaction is
|
|
772
1216
|
open; anything else raises `X12_UNEXPECTED_SEGMENT` and is then discarded. A **blank** line between
|