@cosyte/x12 0.0.1
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 +1067 -0
- package/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/index.cjs +9777 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +12482 -0
- package/dist/index.d.ts +12482 -0
- package/dist/index.mjs +9595 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +103 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,1067 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@cosyte/x12` will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **The Release workflow can actually start.** `.github/workflows/release.yml` calls the shared
|
|
13
|
+
`cosyte/.github` pipeline, which requests `contents`/`id-token`/`pull-requests: write`, but declared
|
|
14
|
+
no `permissions:` of its own — so it inherited the repo default of `contents: read`. A called
|
|
15
|
+
workflow may only downgrade the caller's `GITHUB_TOKEN`, never escalate it, so GitHub rejected the
|
|
16
|
+
workflow at startup (~1s, no jobs, no logs). Every Release run from June 2026 until now failed this
|
|
17
|
+
way, unnoticed, because a `startup_failure` produces no logs to read. The caller job now declares
|
|
18
|
+
the three scopes explicitly. CI-only — no runtime or API change.
|
|
19
|
+
|
|
20
|
+
- **The `VERSION` export now tracks `package.json`, and the missing `version`
|
|
21
|
+
script is restored (VERSION-SYNC).** Two latent release bugs, both of which
|
|
22
|
+
would have bitten at the first publish. (1) `VERSION` was hardcoded `"0.0.0"`
|
|
23
|
+
in `src/index.ts` while `changeset version` bumps only `package.json`, so a
|
|
24
|
+
published `0.0.1` would have shipped an export reading `"0.0.0"` — every
|
|
25
|
+
consumer asserting on or logging `VERSION` told the wrong version of the
|
|
26
|
+
parser they were running. New `scripts/sync-version.mjs` rewrites the constant
|
|
27
|
+
from `package.json` (idempotent; exits non-zero if the declaration is renamed
|
|
28
|
+
rather than silently no-op'ing). (2) **No `version` script existed at all** —
|
|
29
|
+
the shared `cosyte/.github` release workflow drives Changesets with
|
|
30
|
+
`version: pnpm run version`, which would have failed with `ERR_PNPM_NO_SCRIPT`,
|
|
31
|
+
so the "Version Packages" PR could never have been opened. The guard in
|
|
32
|
+
`test/sanity.test.ts` was **inverted**: it asserted
|
|
33
|
+
`expect(VERSION).toBe("0.0.0")` — literal against literal — which stays green
|
|
34
|
+
through exactly this drift and goes red on a _correct_ bump. It now compares
|
|
35
|
+
`VERSION` against `package.json` at test time. Ported from `@cosyte/mllp`
|
|
36
|
+
(MLLP-10), in the canonical form `@cosyte/hl7` carries. No version bumped,
|
|
37
|
+
nothing published — still `0.0.0`.
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- **Phase 10 — release hardening.** The v1 close-out; no new parser
|
|
42
|
+
surface, just the gates, tooling, and docs that make the package
|
|
43
|
+
trustworthy to publish.
|
|
44
|
+
- **Publish-pipeline proof.** A new `release-dry-run` CI job proves a
|
|
45
|
+
real release would succeed without burning a version or needing
|
|
46
|
+
registry auth — `pnpm publish --dry-run` exercises the publish command
|
|
47
|
+
path and `npm pack --dry-run` asserts the publishable tarball assembles
|
|
48
|
+
with the right `files` set + built `dist/`. The real provenance publish
|
|
49
|
+
stays gated on the public launch.
|
|
50
|
+
- **Nightly amplified fuzz** (`.github/workflows/fuzz.yml`). Re-runs the
|
|
51
|
+
byte-flip / never-throw property targets at a higher iteration count
|
|
52
|
+
(`X12_FUZZ_RUNS`) with a rotating seed (`X12_FUZZ_SEED`) — the deep
|
|
53
|
+
search that would slow the per-commit run — and opens/auto-closes a
|
|
54
|
+
sticky issue on failure. The per-commit suite is unchanged (pinned
|
|
55
|
+
seed, base counts, coverage-stable); a finding is replayable via the
|
|
56
|
+
printed seed. New test helper `fuzzRuns()` scales only the true fuzz
|
|
57
|
+
targets.
|
|
58
|
+
- **`pnpm refresh:code-lists`** (`scripts/refresh-code-lists.ts`). A
|
|
59
|
+
release-event tool that validates every bundled code-list snapshot
|
|
60
|
+
(well-formed `meta` ISO dates, non-empty unique codes + descriptions)
|
|
61
|
+
and prints a freshness audit; its `validateCodeLists()` also runs on
|
|
62
|
+
every `pnpm test`. Full regeneration from the canonical WPC / X12
|
|
63
|
+
sources (`--fetch`) is a redistribution-terms-gated release step that
|
|
64
|
+
prints the source manifest rather than fabricating unreviewed
|
|
65
|
+
descriptions.
|
|
66
|
+
- **Docs.** A task-oriented `docs-content/cookbook.md` and a
|
|
67
|
+
`KNOWN-LIMITATIONS.md` do-not-over-trust statement; the README is now a
|
|
68
|
+
real Quickstart. JSDoc `@example` completeness closed on the last three
|
|
69
|
+
public value-exports (`ISA_MIN_LENGTH`, `DELIMITER_POSITIONS`,
|
|
70
|
+
`RELEASE_CHAR`).
|
|
71
|
+
- **Known limitation carried forward:** an external-oracle differential
|
|
72
|
+
corpus (vs CMS Medicare 835) is not yet wired, pending a
|
|
73
|
+
redistribution-terms review — see `KNOWN-LIMITATIONS.md`.
|
|
74
|
+
|
|
75
|
+
### Security
|
|
76
|
+
|
|
77
|
+
- **Dev-dependency advisory remediation (no runtime impact —
|
|
78
|
+
`@cosyte/x12` ships zero runtime dependencies).** Added scoped
|
|
79
|
+
`pnpm.overrides` pinning two transitive **dev/build-time** packages to
|
|
80
|
+
their patched releases: `esbuild` (`>=0.27.3 <0.28.1` → `0.28.1`,
|
|
81
|
+
GHSA dev-server path-traversal — unreachable here: a library build
|
|
82
|
+
via `tsup`/`vitest`, never `esbuild serve`) and the
|
|
83
|
+
`@changesets/parse` copy of `js-yaml` (`>=4.0.0 <4.2.0` → `4.2.0`,
|
|
84
|
+
GHSA-h67p-54hq-rp68 merge-key DoS). The `js-yaml@3.14.2` pulled by
|
|
85
|
+
`read-yaml-file@1.1.0` (via `@manypkg/get-packages` →
|
|
86
|
+
`@changesets/cli`) is **intentionally left** — it calls
|
|
87
|
+
`yaml.safeLoad`, removed/throwing in js-yaml 4, so it cannot be
|
|
88
|
+
force-upgraded without breaking the release tooling; it only parses
|
|
89
|
+
trusted local repo YAML at release time. Verify gate green on the
|
|
90
|
+
upgraded tree.
|
|
91
|
+
|
|
92
|
+
### Fixed
|
|
93
|
+
|
|
94
|
+
- **Segment splitting now honours the `?`-release-escaped terminator.**
|
|
95
|
+
`splitSegments` (the envelope tokenizer) used a naive `indexOf` for the
|
|
96
|
+
segment terminator, so a value carrying a literal terminator byte —
|
|
97
|
+
emitted by `escapeRelease` as `?~` — was split mid-value: the segment
|
|
98
|
+
was truncated at the `?`, a phantom empty segment was injected, and the
|
|
99
|
+
round-trip silently corrupted the value (the element splitter
|
|
100
|
+
`splitWithRelease` had always been release-aware; only the segment
|
|
101
|
+
splitter was not). The fix mirrors the element-splitter scan
|
|
102
|
+
(`?` consumes the next byte) so an escaped terminator stays inside its
|
|
103
|
+
value. The Phase 8 `serialize(parse(s)) === s` fixed point and the new
|
|
104
|
+
`build835` round-trip both depend on this. A degenerate delimiter set
|
|
105
|
+
where the terminator IS the release character falls back to the literal
|
|
106
|
+
scan, preserving prior behaviour. Surfaced by the `build835` round-trip
|
|
107
|
+
review.
|
|
108
|
+
|
|
109
|
+
### Added
|
|
110
|
+
|
|
111
|
+
- **Profile system — descriptive, fixture-grounded clearinghouse / payer
|
|
112
|
+
companion-guide quirk attribution.** A `defineProfile()` API mirroring
|
|
113
|
+
the sibling `@cosyte/hl7` profile shape, plus a `profiles` namespace of
|
|
114
|
+
built-ins. The parser is already lenient and lossless, so a **v1 profile
|
|
115
|
+
is DESCRIPTIVE**: it attaches attribution metadata to the returned
|
|
116
|
+
`X12Interchange` (`ix.profile`) and powers `partitionWarnings`, but
|
|
117
|
+
NEVER alters the parse — `groups`, `warnings`, and `isa` are
|
|
118
|
+
byte-identical with and without a profile (proven by a divergence test).
|
|
119
|
+
- **`defineProfile(spec)`** validates the spec (fail-fast on name, then
|
|
120
|
+
Levenshtein "did you mean?" hints on unknown option keys, then the
|
|
121
|
+
quirk set), merges any `extends` lineage (flatten + dedupe
|
|
122
|
+
first-occurrence; child wins on quirk-id collision keeping first-seen
|
|
123
|
+
position; scalar `description` last-wins), re-validates the composed
|
|
124
|
+
set, and returns a frozen `X12Profile` whose `describe()` yields a
|
|
125
|
+
structured `X12ProfileDescription` bucketed by effect
|
|
126
|
+
(`relaxes` / `adds` / `requires`) — structured DATA, not hl7's
|
|
127
|
+
formatted string, so consumers can program against it.
|
|
128
|
+
- **The locked HARD RULE — no invented quirks.** Every quirk MUST cite a
|
|
129
|
+
`fixture` (a relative path under `test/fixtures/`) that actually
|
|
130
|
+
EXHIBITS the deviation; the field is required at the type level and
|
|
131
|
+
enforced in `defineProfile()`. The accuracy suite goes further: a
|
|
132
|
+
per-quirk DEMONSTRATOR registry asserts each cited fixture exhibits its
|
|
133
|
+
claimed deviation, and a shipped quirk with no demonstrator FAILS the
|
|
134
|
+
suite — so a real-but-irrelevant fixture cannot slip past.
|
|
135
|
+
- **`setDefaultProfile()` / `getDefaultProfile()`** set a process-scoped
|
|
136
|
+
default applied when a `parseX12` call passes no `profile`. An explicit
|
|
137
|
+
`{ profile }` wins; `{ profile: null }` opts out of the default for
|
|
138
|
+
that call. `partitionWarnings(warnings, profile)` splits a parse's
|
|
139
|
+
warnings into `{ expected, unexpected }` on the union of the profile's
|
|
140
|
+
quirk `expectedWarnings` — the one behavioural hook a v1 profile
|
|
141
|
+
offers.
|
|
142
|
+
- **Built-ins ship ONLY where a Tier-2 fixture grounds them:**
|
|
143
|
+
`profiles.availity` (payer-loop `REF*2U` + service-line `REF*F8`
|
|
144
|
+
additions, grounded in `remit/835-availity-quirk.edi`) and
|
|
145
|
+
`profiles.bcbsCommon` (backslash component separator, grounded in
|
|
146
|
+
`envelope/bcbs-subelement.edi`). Profiles whose only "deviation" would
|
|
147
|
+
be a canonical `:` baseline (e.g. a generic Medicare FFS profile) are
|
|
148
|
+
deliberately DEFERRED rather than invented — shipping them would
|
|
149
|
+
violate the hard rule. Built-ins are reachable only through the
|
|
150
|
+
`profiles` namespace, never the top-level export (mirrors hl7).
|
|
151
|
+
- **API divergence from `@cosyte/hl7`, by design:** `describe()` returns
|
|
152
|
+
structured data (not a string); the input type is `X12ProfileSpec`; and
|
|
153
|
+
`partitionWarnings` is x12-only. These are conscious departures driven
|
|
154
|
+
by x12's lossless-lenient reality, not drift.
|
|
155
|
+
- New public exports: `defineProfile`, `setDefaultProfile`,
|
|
156
|
+
`getDefaultProfile`, `partitionWarnings`, `profiles`, `X12ProfileError`,
|
|
157
|
+
and the `X12Profile` / `X12ProfileSpec` / `X12ProfileQuirk` /
|
|
158
|
+
`X12ProfileDescription` / `X12ProfileEffect` / `X12WarningPartition`
|
|
159
|
+
type tree.
|
|
160
|
+
- **Domain builders — `build820` (005010X218 Premium Payment) and
|
|
161
|
+
`build834` (005010X220A1 Benefit Enrollment and Maintenance).** The emit
|
|
162
|
+
counterparts to `get820Payments` and `get834Header` /
|
|
163
|
+
`get834Enrollments`, layered on the Phase 8 general builder and
|
|
164
|
+
mirroring the pure-function `build835` pattern — they NEVER auto-send,
|
|
165
|
+
open a socket, or touch the filesystem, and return a frozen
|
|
166
|
+
`X12Interchange`. Completes the v1 emit scope: every v1 transaction now
|
|
167
|
+
has a domain builder.
|
|
168
|
+
- **`build820(spec)`** assembles a complete interchange (one GS..GE
|
|
169
|
+
group, GS-01 `RA`; one ST..SE 820, ST-03 `005010X218`) from a typed
|
|
170
|
+
`Build820Spec` whose monetary fields are `X12Decimal` throughout
|
|
171
|
+
(BigInt-exact, never `parseFloat`). Segments emit in TR3 loop order
|
|
172
|
+
(BPR → TRN → Loop 1000A receiver `N1*PE` → Loop 1000B remitter
|
|
173
|
+
`N1*PR`/`N1*RM` → Loop 2000 remittances: ENT / NM1 → REF → DTM → RMR →
|
|
174
|
+
ADX), and the output round-trips through `parseX12` so a well-formed
|
|
175
|
+
spec is reproduced field-for-field. **The 820 carries no TR3 balance
|
|
176
|
+
equation** (BPR-02 is not required to equal Σ of the RMR open items),
|
|
177
|
+
so the builder emits all monetary amounts VERBATIM and never raises a
|
|
178
|
+
balance-mismatch refusal — a deliberate contrast with `build835`.
|
|
179
|
+
- **`build834(spec)`** assembles a complete interchange (one GS..GE
|
|
180
|
+
group, GS-01 `BE`; one ST..SE 834, ST-03 `005010X220A1`) from a typed
|
|
181
|
+
`Build834Spec` (envelope + BGN header + sponsor `N1*P5` / payer
|
|
182
|
+
`N1*IN` + the member roster). Segments emit in TR3 loop order (BGN →
|
|
183
|
+
N1 parties → REF → DTP, then per member: INS → NM1\*IL + DMG + N3/N4 →
|
|
184
|
+
REF → DTP → COB → Loop 2300 HD → DTP → AMT). Member DTPs emit BEFORE
|
|
185
|
+
the first HD so the read side binds them to the member, not the
|
|
186
|
+
coverage loop. The output round-trips through `get834Header` /
|
|
187
|
+
`get834Enrollments` field-for-field.
|
|
188
|
+
- **Maintenance type is the 834's safety primitive — emit verbatim,
|
|
189
|
+
refuse the unknown.** The builder places the caller-supplied INS-03 /
|
|
190
|
+
HD-01 code (X12 Code Source 875) into the segment VERBATIM and NEVER
|
|
191
|
+
infers or normalizes it; where the lenient read side only WARNS on an
|
|
192
|
+
unknown code (it must surface what arrived), the builder REFUSES to
|
|
193
|
+
EMIT an action it cannot name, rather than write a maintenance code a
|
|
194
|
+
downstream enrollment system would mis-apply. A build-side property
|
|
195
|
+
test asserts every known code round-trips byte-for-byte and every code
|
|
196
|
+
outside the validated subset is refused.
|
|
197
|
+
- **Refusal, not silent corruption.** `build820` REFUSES a structurally
|
|
198
|
+
impossible spec via a typed `Premium820BuildError`
|
|
199
|
+
(`X12_820_BUILD_INVALID_SPEC` — no TRN trace, no remittance, a
|
|
200
|
+
remittance with neither an `ENT` nor an `NM1` to open its loop, a
|
|
201
|
+
remittance with no `RMR` open item, an open item with no identity, an
|
|
202
|
+
over-long control number). `build834` REFUSES via a typed
|
|
203
|
+
`Enrollment834BuildError`
|
|
204
|
+
(`X12_834_BUILD_UNKNOWN_MAINTENANCE_TYPE` — an INS-03 / HD-01 code
|
|
205
|
+
outside the X12 875 subset; `X12_834_BUILD_INVALID_SPEC` — no member
|
|
206
|
+
loop, an empty required INS-03, an over-long control number). Both
|
|
207
|
+
messages carry structural indices / counts only — `build834`
|
|
208
|
+
additionally names the offending maintenance code (an X12 control
|
|
209
|
+
code, never PHI), but never a member id or name (PHI discipline).
|
|
210
|
+
- New public exports: `build820`, `Premium820BuildError`,
|
|
211
|
+
`PREMIUM_820_BUILD_ERROR_CODES`, `Premium820BuildErrorCode`, the
|
|
212
|
+
`Build820Spec` type tree; `build834`, `Enrollment834BuildError`,
|
|
213
|
+
`ENROLLMENT_834_BUILD_ERROR_CODES`, `Enrollment834BuildErrorCode`, and
|
|
214
|
+
the `Build834Spec` type tree.
|
|
215
|
+
- **Domain builders — `build278Request` (005010X217 Health Care Services
|
|
216
|
+
Review — Request for Review) and `build278Response` (005010X216 Services
|
|
217
|
+
Review — Response).** The emit counterparts to `get278Request` /
|
|
218
|
+
`get278Response`, layered on the Phase 8 general builder and mirroring
|
|
219
|
+
the pure-function `build277` / `build277CA` pattern — they NEVER
|
|
220
|
+
auto-send, open a socket, or touch the filesystem, and return a frozen
|
|
221
|
+
`X12Interchange`.
|
|
222
|
+
- **`build278Request(spec)` / `build278Response(spec)`** share one
|
|
223
|
+
`buildServicesReview` body (GS-01 `HI`, ST-01 `278`) and differ only
|
|
224
|
+
in ST-03 / GS-08 (`005010X217` vs `005010X216`) and the HCR direction
|
|
225
|
+
gate. They assemble a complete interchange from a typed `Build278Spec`
|
|
226
|
+
(envelope + BHT header + the UMO → requester → subscriber →
|
|
227
|
+
(dependent) → reviews tree). Segments emit in TR3 loop order (BHT →
|
|
228
|
+
HL 20 UMO → HL 21 requester → HL 22 subscriber NM1/DMG → [HL 23
|
|
229
|
+
dependent] → HL EV/SS review: TRN → UM → HCR → REF → DTP → HI → MSG →
|
|
230
|
+
provider NM1s, recursing SS service reviews under their EV event), and
|
|
231
|
+
the output round-trips through `parseX12` so a well-formed spec is
|
|
232
|
+
reproduced field-for-field.
|
|
233
|
+
- **The certification decision is the safety-critical, response-only
|
|
234
|
+
surface.** `build278Response` places the caller-supplied HCR-01
|
|
235
|
+
`actionCode` (`A1` certified / `A3` not-certified / `A4` pended / `A6`
|
|
236
|
+
modified / …) into the segment VERBATIM and NEVER infers, normalizes,
|
|
237
|
+
or upgrades it — the round-tripped `decision.actionCode` is
|
|
238
|
+
byte-for-byte the input. `build278Request` REFUSES a review carrying a
|
|
239
|
+
decision (HCR is response-only); `build278Response` refuses a decision
|
|
240
|
+
with an empty action code.
|
|
241
|
+
- **The HL spine is computed, never caller-supplied.** The builder
|
|
242
|
+
computes every HL-01 id, HL-02 parent pointer (`20 → 21 → 22 → 23 →
|
|
243
|
+
EV/SS`), and HL-04 has-child flag from the nested input tree, so an
|
|
244
|
+
inconsistent hierarchy is unrepresentable and SE-01 is correct by
|
|
245
|
+
construction.
|
|
246
|
+
- **Refusal, not silent corruption.** The builder REFUSES a
|
|
247
|
+
structurally impossible spec via a typed `ServicesReview278BuildError`
|
|
248
|
+
(`X12_278_BUILD_INVALID_HIERARCHY` — a subscriber with neither a
|
|
249
|
+
review nor a dependent, a dependent with no review;
|
|
250
|
+
`X12_278_BUILD_INVALID_SPEC` — a review with no request category code,
|
|
251
|
+
a request review carrying an HCR decision, a response decision with an
|
|
252
|
+
empty action code, an over-long control number). The message carries
|
|
253
|
+
structural locators only (`subscriber.review[0]`, level codes) — never
|
|
254
|
+
a member name, member id, trace, or diagnosis code (PHI discipline).
|
|
255
|
+
- New public exports: `build278Request`, `build278Response`,
|
|
256
|
+
`ServicesReview278BuildError`, `AUTH_278_BUILD_ERROR_CODES`,
|
|
257
|
+
`ServicesReview278BuildErrorCode`, and the `Build278Spec` type tree.
|
|
258
|
+
- **Domain builders — `build271` (005010X279A1 Eligibility Benefit
|
|
259
|
+
Response) and `build277` / `build277CA` (005010X212 Claim Status
|
|
260
|
+
Response / 005010X214 Claim Acknowledgment).** The response-side emit
|
|
261
|
+
counterparts to `get271Eligibility` / `get277Status` /
|
|
262
|
+
`get277CADisposition`, layered on the Phase 8 general builder and
|
|
263
|
+
mirroring the pure-function `build835` / `build837` pattern — they
|
|
264
|
+
NEVER auto-send, open a socket, or touch the filesystem, and return a
|
|
265
|
+
frozen `X12Interchange`.
|
|
266
|
+
- **`build271(spec)`** assembles a complete interchange (one GS..GE
|
|
267
|
+
group, GS-01 `HB`; one ST..SE 271, ST-03 `005010X279A1`) from a typed
|
|
268
|
+
`Build271Spec` whose monetary / percent / quantity fields are
|
|
269
|
+
`X12Decimal` throughout (BigInt-exact, never `parseFloat`).
|
|
270
|
+
**`build277(spec)` / `build277CA(spec)`** share one `buildClaimStatus`
|
|
271
|
+
body (GS-01 `HN`) and differ only in ST-03 / GS-08 (`005010X212` vs
|
|
272
|
+
`005010X214`). Segments emit in TR3 loop order (271: HL spine → TRN →
|
|
273
|
+
NM1 → N3/N4 → DMG → REF → DTP → EB + nested NM1 / REF / DTP / MSG;
|
|
274
|
+
277: HL spine → NM1 member → Loop 2200 claim TRN → STC → REF → DTP →
|
|
275
|
+
Loop 2220 SVC → STC / REF / DTP), STC C043 composites carry the
|
|
276
|
+
category : status : entity triples, and the output round-trips
|
|
277
|
+
through `parseX12` so a well-formed spec is reproduced field-for-field.
|
|
278
|
+
- **TRN echo is the safety-critical reassociation invariant.** The
|
|
279
|
+
builder places the caller-supplied trace into TRN-02 verbatim and
|
|
280
|
+
NEVER fabricates, normalizes, or mutates it — a build-side property
|
|
281
|
+
test feeds random trace tokens through all three builders and asserts
|
|
282
|
+
the round-tripped `referenceId` is byte-for-byte the input.
|
|
283
|
+
- **The HL spine is computed, never caller-supplied.** The builder
|
|
284
|
+
computes every HL-01 id, HL-02 parent pointer, and HL-04 has-child
|
|
285
|
+
flag from the nested input tree (271 spine `20 → 21 → 22 → 23`;
|
|
286
|
+
277 / 277CA spine `20 → 21 → 19 → 22 → 23`), so an inconsistent
|
|
287
|
+
hierarchy is unrepresentable and SE-01 is correct by construction.
|
|
288
|
+
- **Refusal, not silent corruption.** The builder REFUSES a
|
|
289
|
+
structurally impossible spec via a typed `Eligibility271BuildError`
|
|
290
|
+
(`X12_271_BUILD_INVALID_HIERARCHY` — no source / a childless source /
|
|
291
|
+
a childless receiver; `X12_271_BUILD_INVALID_SPEC` — over-long control
|
|
292
|
+
number) or `ClaimStatus277BuildError`
|
|
293
|
+
(`X12_277_BUILD_INVALID_HIERARCHY` — no source / a childless source /
|
|
294
|
+
receiver / provider / a subscriber with neither claim nor dependent /
|
|
295
|
+
a childless dependent; `X12_277_BUILD_INVALID_SPEC` — a claim with no
|
|
296
|
+
trace / status / service line, an STC with no category code, an
|
|
297
|
+
over-long control number). The message carries structural locators
|
|
298
|
+
only (`source[0].receiver[0].provider[0].subscriber[0]`, level codes,
|
|
299
|
+
counts) — never a member name, member id, or trace (PHI discipline).
|
|
300
|
+
- New public exports: `build271`, `Eligibility271BuildError`,
|
|
301
|
+
`ELIGIBILITY_271_BUILD_ERROR_CODES`, `Eligibility271BuildErrorCode`,
|
|
302
|
+
the `Build271Spec` type tree; `build277`, `build277CA`,
|
|
303
|
+
`ClaimStatus277BuildError`, `CLAIM_STATUS_277_BUILD_ERROR_CODES`,
|
|
304
|
+
`ClaimStatus277BuildErrorCode`, and the `Build277Spec` type tree.
|
|
305
|
+
- **Domain builders — `build837P` / `build837I` / `build837D` (005010
|
|
306
|
+
837 Health Care Claim: Professional `X222A2`, Institutional `X223A3`,
|
|
307
|
+
Dental `X224A2`).** The claim-submission emit counterpart to
|
|
308
|
+
`get837Claims`, layered on the Phase 8 general builder and mirroring
|
|
309
|
+
the pure-function `build835` pattern — they NEVER auto-send, open a
|
|
310
|
+
socket, or touch the filesystem.
|
|
311
|
+
- **`build837P/I/D(spec)`** each assemble a complete `X12Interchange`
|
|
312
|
+
(one GS..GE group, GS-01 `HC`; one ST..SE 837, ST-03 per variant)
|
|
313
|
+
from a typed `Build837Spec` whose monetary fields are `X12Decimal`
|
|
314
|
+
throughout (BigInt-exact, never `parseFloat`). Segments emit in TR3
|
|
315
|
+
loop order (BHT → Loop 1000A/1000B parties → Loop 2000A/B/C HL spine
|
|
316
|
+
→ Loop 2300 claim → Loop 2400 service lines, incl. 2410 drug / TOO /
|
|
317
|
+
2430 line adjudication) and the output round-trips through `parseX12`
|
|
318
|
+
so a well-formed spec is reproduced by `get837Claims`
|
|
319
|
+
field-for-field. One HI composite emits per HI segment so the read
|
|
320
|
+
side's per-bucket diagnosis/procedure order is preserved; same-group
|
|
321
|
+
line-adjudication CAS triples pack into one CAS segment (≤ 6 each).
|
|
322
|
+
- **The HL spine is computed, never caller-supplied.** The builder
|
|
323
|
+
computes every HL-01 id, HL-02 parent pointer (20 → 22 → 23), and
|
|
324
|
+
HL-04 has-child flag from the nested billing-provider → subscriber →
|
|
325
|
+
(claims | patient) tree, so an inconsistent hierarchy is
|
|
326
|
+
unrepresentable and SE-01 is correct by construction.
|
|
327
|
+
- **Refusal, not silent corruption.** Where `get837Claims` only WARNS
|
|
328
|
+
on a broken HL parent pointer, the builder REFUSES a structurally
|
|
329
|
+
impossible spec via a typed `Claim837BuildError`. Codes:
|
|
330
|
+
`X12_837_BUILD_INVALID_HIERARCHY` (no billing providers / a childless
|
|
331
|
+
billing provider / a subscriber with neither claim nor dependent
|
|
332
|
+
patient / a childless dependent patient) and
|
|
333
|
+
`X12_837_BUILD_INVALID_SPEC` (empty `claimId`, no service line, a
|
|
334
|
+
line whose `variant` mismatches the builder, an empty procedure /
|
|
335
|
+
revenue code, an over-long control number). The message carries
|
|
336
|
+
structural locators only (`billing[0].subscriber[0].claim[0]`, level
|
|
337
|
+
codes, counts) — never the `claimId` or a member id (PHI discipline).
|
|
338
|
+
- New public exports: `build837P`, `build837I`, `build837D`,
|
|
339
|
+
`Claim837BuildError`, `CLAIM_837_BUILD_ERROR_CODES`,
|
|
340
|
+
`Claim837BuildErrorCode`, and the `Build837Spec` type tree.
|
|
341
|
+
- Known limitation: claim-/line-level provider addresses (Loop
|
|
342
|
+
2310/2420 N3/N4) are a documented read-side limitation — the NM1
|
|
343
|
+
fields round-trip, the address does not.
|
|
344
|
+
- **Domain builder — `build835` (005010X221A1 ERA).** The first
|
|
345
|
+
per-transaction emit helper layers the safety-critical TR3 invariants
|
|
346
|
+
on top of the Phase 8 general builder, mirroring the pure-function
|
|
347
|
+
`build999` / `buildTA1` pattern — it NEVER auto-sends, opens a socket,
|
|
348
|
+
or touches the filesystem.
|
|
349
|
+
- **`build835(spec)`** assembles a complete `X12Interchange` (one
|
|
350
|
+
GS..GE group, GS-01 `HP`; one ST..SE 835, ST-03 `005010X221A1`) from
|
|
351
|
+
a typed `Build835Spec` whose monetary fields are `X12Decimal`
|
|
352
|
+
throughout (BigInt-exact, never `parseFloat`). Segments emit in TR3
|
|
353
|
+
loop order (BPR → TRN\* → Loop 1000A/1000B parties → LX → Loop 2100
|
|
354
|
+
claims → Loop 2110 service lines → PLB) and the output round-trips
|
|
355
|
+
through `parseX12` so a balanced spec is reproduced by `get835`
|
|
356
|
+
field-for-field. Composites (CLP-08, SVC-01, SVC-06, PLB) escape
|
|
357
|
+
each component then join with the raw component separator — the
|
|
358
|
+
envelope is emitted inline (not via `buildInterchange`) to avoid
|
|
359
|
+
double-escaping a pre-composed element. Same-group CAS and
|
|
360
|
+
same-provider/period PLB adjustments pack into one segment (≤ 6
|
|
361
|
+
triples / pairs); PLB carries the raw EDI sign
|
|
362
|
+
(`BPR-02 == Σ(CLP-04) − Σ(PLB)`).
|
|
363
|
+
- **Refusal, not silent corruption.** Where `get835` only WARNS on an
|
|
364
|
+
out-of-balance payer artifact, the builder REFUSES via a typed
|
|
365
|
+
`Remit835BuildError`, reusing the authoritative read-side validators
|
|
366
|
+
(`checkServiceLineBalance` / `checkClaimBalance` /
|
|
367
|
+
`checkRemitTotalBalance`) against a materialized read model so emit
|
|
368
|
+
guard and parse warning share one source of truth. Codes:
|
|
369
|
+
`X12_835_BUILD_BALANCE_MISMATCH` (any §1.10.2 invariant — line,
|
|
370
|
+
claim, or top-of-remit) and `X12_835_BUILD_INVALID_SPEC` (no TRN
|
|
371
|
+
trace, an empty CLP-01, an over-long ISA-13). The thrown message
|
|
372
|
+
carries numeric totals only — never a patient-control number or
|
|
373
|
+
member id (PHI discipline).
|
|
374
|
+
- **New exports.** `build835`, `Remit835BuildError`,
|
|
375
|
+
`REMIT_835_BUILD_ERROR_CODES`, `Remit835BuildErrorCode`, and the
|
|
376
|
+
`Build835Spec` type tree (`Build835EnvelopeSpec` / `…PaymentSpec` /
|
|
377
|
+
`…TraceSpec` / `…PartySpec` / `…AddressSpec` / `…ReferenceSpec` /
|
|
378
|
+
`…ContactSpec` / `…PersonSpec` / `…ProviderSpec` / `…AdjustmentSpec` /
|
|
379
|
+
`…RemarkSpec` / `…AmountSpec` / `…ServiceLineSpec` / `…ClaimSpec` /
|
|
380
|
+
`…ProviderAdjustmentSpec`).
|
|
381
|
+
- **Known limitation (deferred).** The remaining domain builders
|
|
382
|
+
(`build837P/I/D` / `build271` / `build277` / `build278` /
|
|
383
|
+
`build820` / `build834`) layer on the same general surface and are
|
|
384
|
+
NOT in this change.
|
|
385
|
+
|
|
386
|
+
- **Phase 8 — spec-clean serializer + general interchange builder (the
|
|
387
|
+
emit half lands).** Two new public surfaces close the read↔write loop.
|
|
388
|
+
- **`serializeX12(interchange, opts?)`** turns any parsed
|
|
389
|
+
`X12Interchange` back into an X12 byte stream. Default mode is
|
|
390
|
+
byte-faithful — reconstructed purely from the verbatim `.raw`
|
|
391
|
+
strings the parser preserved (ISA + terminator, then each
|
|
392
|
+
TA1 / GS / segment / GE / IEA terminator-joined, then any
|
|
393
|
+
`trailingBytes`) — so for a Tier-1 input it reproduces the source
|
|
394
|
+
bytes exactly: the idempotency fixed point
|
|
395
|
+
`serialize(parse(s)) === s`. With `{ specClean: true }` it ALSO
|
|
396
|
+
reconciles the envelope (SE-01 / GE-01 / IEA-01 counts + the
|
|
397
|
+
ISA-13↔IEA-02 / GS-06↔GE-02 / ST-02↔SE-02 control pairs),
|
|
398
|
+
surfacing every mismatch via `opts.onWarning` and NEVER silently
|
|
399
|
+
correcting it. Corrected counts emit only with
|
|
400
|
+
`{ recomputeCounts: true }`; control NUMBERS are identity and are
|
|
401
|
+
NEVER rewritten, only flagged.
|
|
402
|
+
- **`buildInterchange(spec)`** is the general-purpose, segment-level
|
|
403
|
+
builder: given an `InterchangeSpec` it owns every envelope mechanic
|
|
404
|
+
(the 106-byte fixed-width ISA, the GS/GE/SE/IEA control segments,
|
|
405
|
+
and the SE-01 / GE-01 / IEA-01 counts), escapes active delimiters in
|
|
406
|
+
body values via the `?` release char, and round-trips its output
|
|
407
|
+
back through `parseX12` so the returned interchange is bit-identical
|
|
408
|
+
to the parsed form. Structurally impossible specs are REFUSED with a
|
|
409
|
+
typed `X12BuildError` (`X12_BUILD_INVALID_SPEC`) — an over-long
|
|
410
|
+
ISA-13, a body segment with no id.
|
|
411
|
+
- **New warning + exports.** `X12_SEGMENT_COUNT_MISMATCH` is a
|
|
412
|
+
serializer-only diagnostic (the parser never validated SE-01);
|
|
413
|
+
registry expands 21 → 22, additions-only, bounded metadata only
|
|
414
|
+
(H-PHI invariant). New public exports: `serializeX12`,
|
|
415
|
+
`SerializeOptions`, `buildInterchange`, `InterchangeSpec`,
|
|
416
|
+
`FunctionalGroupSpec`, `TransactionSetSpec`, `SegmentSpec`,
|
|
417
|
+
`X12BuildError`, `X12_BUILD_ERROR_CODES`, `X12BuildErrorCode`, and
|
|
418
|
+
the `segmentCountMismatch` factory.
|
|
419
|
+
- **Round-trip goldens** lock the emit surface across all v1
|
|
420
|
+
transactions: 13 committed `test/fixtures/golden/<name>.edi` files
|
|
421
|
+
regenerated by `test/scripts/gen-serialize-goldens.ts`, asserting
|
|
422
|
+
`serializeX12(parseX12(fixture))` reproduces the golden
|
|
423
|
+
byte-for-byte. `roundTripProperty` (300 runs) + a builder property
|
|
424
|
+
(200 runs) assert serialize idempotency and that the builder never
|
|
425
|
+
emits a self-inconsistent envelope.
|
|
426
|
+
- **Latent fixture defects caught + fixed.** The new reconciliation
|
|
427
|
+
surfaced four hand-authored deviations the lenient parser never
|
|
428
|
+
validated (it checks GE-01 / IEA-01 / control pairs but not SE-01):
|
|
429
|
+
SE-01 miscounts in `837i-canonical` (30→33), `837d-canonical`
|
|
430
|
+
(25→26), `999-accept` (5→6), and a GS-06/GE-02 mismatch in
|
|
431
|
+
`278-response` (GS-06 2→1) — an accuracy-gate win.
|
|
432
|
+
- **Known limitation (deferred).** Domain per-transaction builders
|
|
433
|
+
(`build835` / `build837P/I/D` / `build271` / …, the safety-critical
|
|
434
|
+
emit code enforcing per-TR3 balance + certification invariants) are
|
|
435
|
+
NOT in this phase; the general envelope surface they layer on top of
|
|
436
|
+
is.
|
|
437
|
+
|
|
438
|
+
- **Phase 7 — 278 services review + 834 enrollment + 820 premium
|
|
439
|
+
payment (the v1 transaction scope rounds out).** Four new read-side
|
|
440
|
+
helpers: `get278Request` / `get278Response` (TR3 `005010X217` /
|
|
441
|
+
`005010X216`), `get820Payments` (TR3 `005010X218`), and the streaming
|
|
442
|
+
pair `get834Header` + `get834Enrollments` (TR3 `005010X220A1`).
|
|
443
|
+
- **Safety-critical fields preserved verbatim, never inferred.** The
|
|
444
|
+
278 response `HCR-01` certification action (certified /
|
|
445
|
+
not-certified / pended / modified) is captured as-is on each event /
|
|
446
|
+
service review; the 834 `INS-03` / `HD-01` maintenance type (X12 0875) is preserved and an unknown code raises
|
|
447
|
+
`X12_834_UNKNOWN_MAINTENANCE_TYPE` on the affected member only — no
|
|
448
|
+
action is ever synthesized.
|
|
449
|
+
- **834 streaming.** `get834Enrollments` is an
|
|
450
|
+
`AsyncIterable<X12Enrollment>` yielding one member per `INS` loop;
|
|
451
|
+
a streaming property test drives a 10MB+ synthetic roster with
|
|
452
|
+
early-break. (Honest limitation: v1 still parses into `tx.segments`
|
|
453
|
+
up front — a true file→iterator source is a v2 item.)
|
|
454
|
+
- **278 HL spine** `20 → 21 → 22 → 23` validated via the shared
|
|
455
|
+
`validateHl`; the `EV` / `SS` event + service levels are
|
|
456
|
+
deliberately tolerant (omitted from the expected-parent map).
|
|
457
|
+
- **820** surfaces the BPR payment header, TRN traces, receiver
|
|
458
|
+
(`N1*PE`) + remitter (`N1*PR` / `N1*RM`) parties with addresses, and
|
|
459
|
+
both `ENT` organization-summary and bare-`NM1` individual
|
|
460
|
+
remittances with RMR open items, DTM dates, and ADX adjustments.
|
|
461
|
+
- All monetary fields decode as `X12Decimal` (BigInt-exact, never
|
|
462
|
+
`parseFloat`). 12 dogfooded `LoopSpec` artifacts ship through the
|
|
463
|
+
public `defineLoopSpec()` (6 × 278 + 3 × 820 + 3 × 834). Warning
|
|
464
|
+
registry expanded by `X12_834_UNKNOWN_MAINTENANCE_TYPE`
|
|
465
|
+
(additions-only); its factory shape-validates the echoed code
|
|
466
|
+
(H-PHI invariant). Synthetic fixtures across all three surfaces,
|
|
467
|
+
unit tests, and the 834 streaming property. Serialization is
|
|
468
|
+
Phase 8.
|
|
469
|
+
- **PHI commit-gate — a zero-dependency, X12-shape-aware PHI scanner
|
|
470
|
+
(`scripts/phi-scan.ts`, run via `pnpm phi-scan`).** Guards the
|
|
471
|
+
synthetic fixture corpus: it refuses any test fixture or `src/` file
|
|
472
|
+
carrying real-PHI-shaped tokens so a developer cannot commit a
|
|
473
|
+
real-looking interchange by accident. Wired into the pre-commit hook
|
|
474
|
+
(`simple-git-hooks` → `phi-scan --staged`) and CI (the reusable
|
|
475
|
+
`cosyte/.github` pipeline's `run-phi-scan: true`); flips the local
|
|
476
|
+
`scripts/verify.sh` summary from `phi-scan SKIP` to `phi-scan ✓`.
|
|
477
|
+
- **Synthetic allow-list, not an inline header.** X12 `.edi` is
|
|
478
|
+
byte-strict (ISA must start at byte 0), so an inline
|
|
479
|
+
`# synthetic: true` marker is impossible — it would break every
|
|
480
|
+
parser test. Same constraint DICOM hits with binary `.dcm`, so the
|
|
481
|
+
same proven solution: `scripts/phi-allow-list.txt` positively
|
|
482
|
+
declares which names / dates-of-birth / ids / email-domains are
|
|
483
|
+
fake. Any realistic-PHI token outside the allow-list is a hit.
|
|
484
|
+
- **Segment-aware scan** for ISA-detected files: NM1 person-name
|
|
485
|
+
tokens (entity-type-1) and SSN qualifier `34`, MI member-id and XX
|
|
486
|
+
NPI shapes, DMG date-of-birth (any format qualifier, not just
|
|
487
|
+
`D8`), and DTP / DTM / BHT / GS service/transaction dates before 2024. Every file also gets a cross-cutting shape pass (dashed SSN,
|
|
488
|
+
`REF*SY` SSN, non-test email). Non-X12 targets (hand-written
|
|
489
|
+
`src/`, plain text) get the conservative shape pass only, so JSDoc
|
|
490
|
+
`@example` snippets don't trip it.
|
|
491
|
+
- **Audited bypass.** A whole-file `--allow-fixture <path>` is
|
|
492
|
+
rejected unless `phi-scan-overrides.md` carries a matching
|
|
493
|
+
`### <path>` entry, so a silenced file is always a recorded act.
|
|
494
|
+
Every subprocess is `git` via `execFileSync` array args — no shell
|
|
495
|
+
form. Unit tests cover the clean interchange, each violator class,
|
|
496
|
+
the plain-text pass, and both arms of the override gate.
|
|
497
|
+
|
|
498
|
+
- **Phase 6 — 271 Eligibility Benefit Response + 277 / 277CA Claim
|
|
499
|
+
Status — TR3s `005010X279A1` (270/271), `005010X212` (276/277),
|
|
500
|
+
`005010X214` (277CA).** Three new public walkers:
|
|
501
|
+
`get271Eligibility(delimiters, tx)`, `get277Status(delimiters, tx)`,
|
|
502
|
+
and `get277CADisposition(delimiters, tx)`. 277 and 277CA share one
|
|
503
|
+
internal walk disambiguated by the `ST-03` implementation-convention
|
|
504
|
+
reference — `get277CADisposition` admits only `005010X214`;
|
|
505
|
+
`get277Status` admits either. Each returns `undefined` only on a
|
|
506
|
+
mis-routed call (wrong `ST-01`); every recoverable deviation is a
|
|
507
|
+
warning, never a throw.
|
|
508
|
+
- **TRN echo (safety-critical reassociation).** A 271 echoes the
|
|
509
|
+
requesting 270's `TRN-02` trace verbatim onto its enclosing
|
|
510
|
+
subscriber / dependent, and a 277 echoes the 276's onto its claim,
|
|
511
|
+
so the provider can re-associate the answer with the request it
|
|
512
|
+
sent — the walkers NEVER mutate, normalize, or drop the trace. A
|
|
513
|
+
round-trip property test asserts byte-for-byte echo across an
|
|
514
|
+
arbitrary trace grammar.
|
|
515
|
+
- **Status-code fidelity (277 family).** Each STC composite
|
|
516
|
+
(STC-01 / STC-10 / STC-11, C043) decodes into a verbatim CSCC
|
|
517
|
+
(Claim Status Category Code, X12 source 507) + CSC (Claim Status
|
|
518
|
+
Code, source 508) + responsible-entity triple. Bundled snapshot
|
|
519
|
+
descriptions resolve when known; codes outside the subset preserve
|
|
520
|
+
their verbatim value and emit `X12_UNKNOWN_CLAIM_STATUS_CATEGORY` /
|
|
521
|
+
`X12_UNKNOWN_CLAIM_STATUS`. A 277CA provider-level batch
|
|
522
|
+
acknowledgment opens a claim on a standalone STC (no TRN).
|
|
523
|
+
- **HL parent-pointer integrity.** Enforced through the shared
|
|
524
|
+
`validateHl` primitive — 271 spine `20 → 21 → 22 → 23`; 277 / 277CA
|
|
525
|
+
spine `20 → 21 → 19 → 22 → 23`. A dangling or mis-levelled parent
|
|
526
|
+
emits `X12_HL_PARENT_MISMATCH` / `X12_HL_PARENT_LEVEL_INVALID`; the
|
|
527
|
+
walker NEVER silently re-numbers and the verbatim declared parent id
|
|
528
|
+
is preserved.
|
|
529
|
+
- **Bundled code-list snapshots.** `CLAIM_STATUS_CATEGORY_CODES`,
|
|
530
|
+
`CLAIM_STATUS_CODES`, and `SERVICE_TYPE_CODES` ship as dated,
|
|
531
|
+
versioned data artifacts alongside the CARC / RARC family, with
|
|
532
|
+
`lookupClaimStatusCategory` / `lookupClaimStatus` /
|
|
533
|
+
`lookupServiceType`.
|
|
534
|
+
- All monetary fields (EB amounts, STC charge / payment, SVC line
|
|
535
|
+
charge / payment) decode as `X12Decimal`, never `parseFloat`. 13
|
|
536
|
+
dogfooded `LoopSpec` artifacts ship through `defineLoopSpec()`
|
|
537
|
+
(7 eligibility + 7 status; Loop 2200 / 2220 reused across the
|
|
538
|
+
subscriber + dependent branches). Warning registry expanded 18 → 20
|
|
539
|
+
(additions-only); both new factories shape-validate the echoed code
|
|
540
|
+
(H-PHI invariant). Shared `X12Hl` HL primitive exported for the
|
|
541
|
+
result types. Six synthetic fixtures + unit tests + byte-flip fuzz
|
|
542
|
+
(never-throw outside the 4 Tier-3 fatals) across every Phase 6
|
|
543
|
+
fixture.
|
|
544
|
+
- **Known limitations (deferred):** AAA request-validation segments,
|
|
545
|
+
HSD detail, and III / LS / LE markers in the 271, plus QTY / AMT
|
|
546
|
+
claim-summary roll-ups in a 277CA Loop 2200, are preserved on
|
|
547
|
+
`tx.segments` verbatim but not yet typed onto the model.
|
|
548
|
+
- **Phase 5 — 837 Healthcare Claim — TR3s `005010X222A2` (Professional),
|
|
549
|
+
`005010X223A3` (Institutional), `005010X224A2` (Dental).** The
|
|
550
|
+
claim-creation surface — the volume side of HIPAA EDI traffic.
|
|
551
|
+
`get837Claims(delimiters, tx, opts?)` walks a parsed 837 transaction
|
|
552
|
+
set into the typed `X12_837Submission` model: variant detection (from
|
|
553
|
+
ST-03 implementation-convention reference, falling back to SVx
|
|
554
|
+
segment id, then to `"unknown"` with `X12_837_UNKNOWN_VARIANT`),
|
|
555
|
+
submitter (Loop 1000A NM1\*41) + receiver (Loop 1000B NM1\*40), the
|
|
556
|
+
full HL hierarchy (Loops 2000A / 2000B / 2000C), every claim header
|
|
557
|
+
(Loop 2300 — CLM with patient account number, total charge,
|
|
558
|
+
composite POS / facility-code-qualifier / claim-frequency-code,
|
|
559
|
+
signature / assignment / benefits / release-of-information
|
|
560
|
+
indicators), and every service line typed by variant (`SV1` →
|
|
561
|
+
professional, `SV2` → institutional, `SV3` → dental).
|
|
562
|
+
- **HL parent-pointer integrity.** The 837 family's safety primitive
|
|
563
|
+
is the HL hierarchy (`HL-01` own id, `HL-02` parent id, `HL-03`
|
|
564
|
+
level code: `20` Information Source / `22` Subscriber / `23`
|
|
565
|
+
Dependent). An off-by-one in `HL-02` is THE #1 837 bug — the
|
|
566
|
+
walker validates that every non-top-level HL's `HL-02` references
|
|
567
|
+
an earlier-emitted `HL-01` AND that the parent's level matches the
|
|
568
|
+
TR3-required parent for this level (`22` → parent `20`; `23` →
|
|
569
|
+
parent `22`). Violations emit `X12_HL_PARENT_MISMATCH` or
|
|
570
|
+
`X12_HL_PARENT_LEVEL_INVALID` — the parser NEVER silently
|
|
571
|
+
re-numbers. The verbatim declared parent id stays on the
|
|
572
|
+
`X12HierarchicalLevel` entry.
|
|
573
|
+
- **HI qualifier → code-system provenance.** `HI` carries
|
|
574
|
+
diagnoses, principal procedures, external cause of injury,
|
|
575
|
+
condition codes, occurrence codes, value codes, and DRG / PR
|
|
576
|
+
groupings under one segment id — with the qualifier (first
|
|
577
|
+
component) governing the code system. The new
|
|
578
|
+
`src/code-lists/hi-qualifiers.ts` ships a frozen `HI_QUALIFIERS`
|
|
579
|
+
registry covering the qualifiers cited across the three TR3s
|
|
580
|
+
(ICD-10-CM diagnoses: `ABK` principal / `ABF` other / `ABJ`
|
|
581
|
+
admitting / `ABN` reason-for-visit / `APR` external-cause;
|
|
582
|
+
legacy ICD-9-CM: `BK` / `BF` / `BJ` / `BN` / `BR`; ICD-10-PCS
|
|
583
|
+
procedures: `BBQ` principal / `BBR` other; legacy ICD-9-PCS:
|
|
584
|
+
`BQ` / `BBA`; DRG: `DR`; NUBC institutional code sets:
|
|
585
|
+
`BG` condition / `BH` occurrence / `BI` occurrence-span / `BE`
|
|
586
|
+
value / `PR` patient-reason). Each `X12ClaimHiCode` carries the
|
|
587
|
+
verbatim qualifier AND the resolved {@link X12HiCodeSystem} +
|
|
588
|
+
{@link X12HiCategory}; unknown qualifiers emit
|
|
589
|
+
`X12_UNKNOWN_HI_QUALIFIER`, preserve the verbatim
|
|
590
|
+
qualifier + code, and resolve to `codeSystem: "unknown"`. Helpers
|
|
591
|
+
`resolveHiQualifier` / `isDiagnosisQualifier` /
|
|
592
|
+
`isProcedureQualifier` ship in the public surface so consumers
|
|
593
|
+
never re-derive the mapping.
|
|
594
|
+
- **Money + identity discipline.** All monetary fields decode as
|
|
595
|
+
`X12Decimal` (CLM-02 total charge, SV1-02 / SV2-03 / SV3-02 line
|
|
596
|
+
charge, AMT amounts, SVD-02 adjudicated amount, CTP-04 drug
|
|
597
|
+
quantity, line SV2-06 service-line rate, SV2-07 non-covered
|
|
598
|
+
charge). All identifiers (NPI on `NM1*..*..*XX*<NPI>`, member id
|
|
599
|
+
on `NM1*IL*..*MI*<MEMBER>`, claim id on CLM-01, patient/subscriber
|
|
600
|
+
relationship code on PAT/SBR) are surfaced verbatim on the model;
|
|
601
|
+
warnings NEVER echo their values (H-PHI invariant inherited from
|
|
602
|
+
`@cosyte/hl7`). All dates carry their format qualifier (`D8`
|
|
603
|
+
single-date `CCYYMMDD`, `RD8` date-range, `DT` for `DTP-435`/`096`
|
|
604
|
+
admission/discharge timestamps) so a consumer can branch without
|
|
605
|
+
re-parsing the literal.
|
|
606
|
+
- **Variant-specific service-line types.** The
|
|
607
|
+
{@link X12_837ServiceLine} discriminated union holds three shapes:
|
|
608
|
+
- `X12_837ServiceLineProfessional` — `procedureQualifier` /
|
|
609
|
+
`procedureCode` / `modifiers` from SV1-01 composite; 1-4
|
|
610
|
+
`diagnosisPointers` from SV1-07; emergency / EPSDT / family-
|
|
611
|
+
planning indicators; optional `drug` (Loop 2410 LIN + CTP NDC +
|
|
612
|
+
UCUM unit).
|
|
613
|
+
- `X12_837ServiceLineInstitutional` — `revenueCode` (NUBC 4-digit
|
|
614
|
+
from SV2-01); optional procedure / modifiers from SV2-02
|
|
615
|
+
composite; `serviceLineRate` (SV2-06); `nonCoveredCharge`
|
|
616
|
+
(SV2-07).
|
|
617
|
+
- `X12_837ServiceLineDental` — ADA CDT `procedureCode` from
|
|
618
|
+
SV3-01; `oralCavityArea` composite from SV3-04; per-line
|
|
619
|
+
`toothInformation` from `TOO*JP` (Universal Tooth Numbering)
|
|
620
|
+
with surface codes from TOO-03's composite components;
|
|
621
|
+
`prosthesisCrownInlayCode` (SV3-05).
|
|
622
|
+
- **Loop 2430 Line Adjudication (COB).** SVD + CAS + DTP land on
|
|
623
|
+
`serviceLine.adjudications` as `X12LineAdjudication[]`. Each
|
|
624
|
+
adjudication ships the other-payer id (SVD-01), amount paid as
|
|
625
|
+
`X12Decimal` (SVD-02), the other payer's procedure code, paid
|
|
626
|
+
units, and any CAS adjustments — re-using `X12RemitAdjustment` /
|
|
627
|
+
`lookupCarc` from the 835 helper since CAS semantics are
|
|
628
|
+
identical.
|
|
629
|
+
- **Loop 2320 Other Subscriber (COB).** Captured at the surface
|
|
630
|
+
level: SBR-01 payer-responsibility code (`P` / `S` / `T`),
|
|
631
|
+
individual relationship, claim filing indicator, and the
|
|
632
|
+
other-subscriber + other-payer NM1 entities. Detailed CAS / OI /
|
|
633
|
+
MOA breakdown inside Loop 2320 is deferred to Phase 9 (companion-
|
|
634
|
+
guide tolerance) — verbatim segments remain on `tx.segments`.
|
|
635
|
+
- **Eleven dogfooded `LoopSpec` artifacts** ship through the public
|
|
636
|
+
`defineLoopSpec()` API — the dogfooding gate locked in Phase 2.
|
|
637
|
+
`CLAIM_837_LOOP_1000A` / `_1000B` (submitter / receiver),
|
|
638
|
+
`CLAIM_837_LOOP_2010AA` (billing provider name), `_2010BA`
|
|
639
|
+
(subscriber name), `_2010BB` (payer name), `_2010CA` (patient
|
|
640
|
+
name), `CLAIM_837P_LOOP_2410` (drug identification), `_LOOP_2430`
|
|
641
|
+
(line adjudication), plus variant-specific
|
|
642
|
+
`CLAIM_837{P,I,D}_LOOP_2000A` / `_2300` / `_2400` trees.
|
|
643
|
+
- **Bundled HI qualifier registry under
|
|
644
|
+
`src/code-lists/hi-qualifiers.ts`** alongside the existing CARC /
|
|
645
|
+
RARC / CLP_STATUS / CAGC snapshots — formally part of the
|
|
646
|
+
code-list family, not a transaction-local table.
|
|
647
|
+
- **Two new exported constants for safety + ergonomics:**
|
|
648
|
+
`HL_LEVEL_CODES` (`INFORMATION_SOURCE` `"20"` / `INFORMATION_RECEIVER`
|
|
649
|
+
`"21"` / `SUBSCRIBER` `"22"` / `DEPENDENT` `"23"`) and
|
|
650
|
+
`NM1_QUALIFIERS` (`SUBMITTER` `"41"` / `RECEIVER` `"40"` /
|
|
651
|
+
`BILLING_PROVIDER` `"85"` / `PAY_TO_ADDRESS` `"87"` /
|
|
652
|
+
`PAY_TO_PLAN` `"PE"` / `SUBSCRIBER` `"IL"` / `PAYER` `"PR"` /
|
|
653
|
+
`PATIENT` `"QC"`) — so the walker (and any consumer Phase 8
|
|
654
|
+
builder) never has to magic-string the safety-critical
|
|
655
|
+
discriminators.
|
|
656
|
+
- **Six new shared element-read helpers in `parser/segment.ts`** —
|
|
657
|
+
`elementValue` / `elementOptional` / `componentOptional` /
|
|
658
|
+
`elementDecimal` / `elementDecimalOrZero` / `collectElementValues`
|
|
659
|
+
— extracted out of the 835 and 837 walkers (both walkers had
|
|
660
|
+
byte-identical copies). New transaction walkers (Phase 6+ 270/271,
|
|
661
|
+
277, 834) inherit them. Public surface — exported via
|
|
662
|
+
`@cosyte/x12`.
|
|
663
|
+
- **Public-surface additions** to the warning stability snapshot:
|
|
664
|
+
`X12_HL_PARENT_MISMATCH`, `X12_HL_PARENT_LEVEL_INVALID`,
|
|
665
|
+
`X12_UNKNOWN_HI_QUALIFIER`, `X12_MISSING_REQUIRED_LOOP`,
|
|
666
|
+
`X12_837_UNKNOWN_VARIANT` (13 → 18 Tier-2 codes; additions-only
|
|
667
|
+
— fatal registry stays at 4). All new warning factories
|
|
668
|
+
(`hlParentMismatch` / `hlParentLevelInvalid` /
|
|
669
|
+
`unknownHiQualifier` / `missingRequiredLoop` /
|
|
670
|
+
`unknown837Variant`) shape-validate echoed values through
|
|
671
|
+
dedicated regex patterns (`/^[0-9]{1,4}$/u` for HL ids,
|
|
672
|
+
`/^[0-9]{2}$/u` for level codes, `/^[A-Z][A-Z0-9]{1,2}$/u` for HI
|
|
673
|
+
qualifiers, `/^[0-9A-Z]{3,6}$/u` for loop ids,
|
|
674
|
+
`/^[0-9A-Z]{3,16}$/u` for ICR) and substitute `(non-spec)` for
|
|
675
|
+
hostile inputs — the H-PHI invariant from `@cosyte/hl7`.
|
|
676
|
+
- **PHI discipline.** Warnings NEVER echo field VALUES; the
|
|
677
|
+
`missingRequiredLoop` rationale strings are hard-coded literals
|
|
678
|
+
(no element interpolation). Patient names / member IDs / NPIs /
|
|
679
|
+
claim numbers are surfaced verbatim on the typed model only — the
|
|
680
|
+
documented consumer-redaction boundary (mirrors hl7 + the 835
|
|
681
|
+
helper). The `X12ClaimNote` JSDoc explicitly flags NTE-02 as
|
|
682
|
+
PHI-bearing (provider-supplied free text). Every Phase 5 fixture
|
|
683
|
+
is synthetic (test names `TEST PATIENT` / `SUB LAST` / `PATIENT
|
|
684
|
+
CHILD`; sequential member IDs `MEMBER001`–`MEMBER011` etc.; NPI-
|
|
685
|
+
shaped sequential numbers; obvious test addresses) and matches
|
|
686
|
+
the established 835 fixture conventions.
|
|
687
|
+
- **Known limitations after this phase** (deliberate v1 scope; none
|
|
688
|
+
silent — verbatim segments remain on `tx.segments` for raw
|
|
689
|
+
access):
|
|
690
|
+
- Loop 2320/2330 Other Subscriber / Other Payer captured at the
|
|
691
|
+
surface level only — detailed CAS / OI / MOA inside Loop 2320
|
|
692
|
+
deferred to Phase 9 (companion-guide profile system).
|
|
693
|
+
- Loop 2420 service-line provider names captured verbatim on
|
|
694
|
+
`serviceLine.providers`; per-provider PRV + address not yet
|
|
695
|
+
typed at the line level.
|
|
696
|
+
- CN1 contract information preserved verbatim on `tx.segments`,
|
|
697
|
+
not typed onto the model.
|
|
698
|
+
- Companion-guide enforcement (e.g. Availity's required `REF*EA`
|
|
699
|
+
at the billing provider) deferred to Phase 9 (profile system).
|
|
700
|
+
- 837 **builder** (`build837P` / `I` / `D`) deferred to Phase 8.
|
|
701
|
+
- **Fixtures (10 synthetic).** Three Tier-1 canonical files (one per
|
|
702
|
+
variant). Six Tier-2 quirk fixtures covering HL-orphan (parent id
|
|
703
|
+
missing), unknown HI qualifier, patient HL (Loop 2000C with
|
|
704
|
+
patient ≠ subscriber), institutional pay-to-plan (NM1\*PE),
|
|
705
|
+
unknown variant (ST-03 outside snapshot), empty optionals (NTE /
|
|
706
|
+
AMT / DTP with missing fields, 2320 SBR with empty payer-
|
|
707
|
+
responsibility code), and one comprehensive fixture exercising
|
|
708
|
+
every walker branch (pay-to-address, submitter PER + N3/N4/REF,
|
|
709
|
+
subscriber DMG + REF + PER, 2310 rendering + referring providers,
|
|
710
|
+
2320 other-subscriber + other-payer, 2410 LIN + CTP drug, 2430
|
|
711
|
+
SVD + CAS + DTP adjudication).
|
|
712
|
+
- **Tests.** 56 new tests across 4 new files: unit tests for the
|
|
713
|
+
three Tier-1 variants + HL parent integrity + HI qualifier
|
|
714
|
+
resolution; HI qualifier table unit tests (registry shape,
|
|
715
|
+
diagnosis / procedure classification disjointness); HL hierarchy
|
|
716
|
+
property tests (verbatim preservation, never-throw on every
|
|
717
|
+
fixture); 837 byte-flip fuzz target (300 runs per fixture × 6
|
|
718
|
+
claim fixtures = 1800 mutated inputs, never throws outside the
|
|
719
|
+
four Tier-3 envelope fatals); comprehensive coverage tests
|
|
720
|
+
exercising every walker branch on the comprehensive fixture +
|
|
721
|
+
edge cases. **325 tests total** (up from 269).
|
|
722
|
+
- **Coverage.** Verify gate green: typecheck + lint + format +
|
|
723
|
+
coverage (96.91% stmts / 90.61% branches / 97.67% funcs / 98.49%
|
|
724
|
+
lines globally; per-dir ≥90 on `parser/` + `loops/` +
|
|
725
|
+
`transactions/` + `code-lists/`) + build + attw + verify:exports.
|
|
726
|
+
- **`phi-scan` SKIP** — unchanged from Phase 4. The runtime H-PHI
|
|
727
|
+
invariant is necessary but not sufficient; static fixture
|
|
728
|
+
scanning is tracked as the `X12-PHI-SCAN` backlog follow-up.
|
|
729
|
+
|
|
730
|
+
### Changed
|
|
731
|
+
|
|
732
|
+
- **`parser/segment.ts` gains 6 element-read helpers** as Public API:
|
|
733
|
+
`elementValue` / `elementOptional` / `componentOptional` /
|
|
734
|
+
`elementDecimal` / `elementDecimalOrZero` / `collectElementValues`.
|
|
735
|
+
Re-used by the 835 helper (`get835`) and the new 837 helper
|
|
736
|
+
(`get837Claims`) — both walkers previously defined byte-identical
|
|
737
|
+
copies of these inline. Additive; no breaking change.
|
|
738
|
+
|
|
739
|
+
- **`src/code-lists/` gains `hi-qualifiers.ts`** with `HI_QUALIFIERS`
|
|
740
|
+
/ `resolveHiQualifier` / `isDiagnosisQualifier` /
|
|
741
|
+
`isProcedureQualifier` and the `X12HiCategory` / `X12HiCodeSystem`
|
|
742
|
+
/ `X12HiQualifier` types. Re-exported from `@cosyte/x12` root.
|
|
743
|
+
|
|
744
|
+
- **Phase 4 — 835 Healthcare Claim Payment/Advice (ERA) — TR3
|
|
745
|
+
`005010X221A1`.** The cash-posting surface — money, the consultant ask.
|
|
746
|
+
`get835(delimiters, tx)` walks a parsed 835 transaction set into the
|
|
747
|
+
typed `X12Remittance` model: payment header (BPR), trace numbers (TRN),
|
|
748
|
+
payer / payee parties (Loops 1000A / 1000B with address / contact /
|
|
749
|
+
additional identifiers), every claim payment (Loop 2100 — CLP plus
|
|
750
|
+
patient / subscriber / service-provider NM1s, statement-period DTMs,
|
|
751
|
+
CAS adjustments at both claim and service-line scope, MIA / MOA / LQ
|
|
752
|
+
remarks, REF / AMT supplemental amounts), every service line (Loop
|
|
753
|
+
2110 — SVC with HCPCS / CPT / NDC / revenue-code / modifier
|
|
754
|
+
destructuring, service-date DTMs, line-level CAS / REF / AMT / LQ),
|
|
755
|
+
and provider-level adjustments (PLB with multi-pair flattening). The
|
|
756
|
+
loop hierarchy ships as three frozen `LoopSpec` artifacts
|
|
757
|
+
(`REMIT_835_LOOP_2000`, `REMIT_835_LOOP_2100`, `REMIT_835_LOOP_2110`)
|
|
758
|
+
authored through the public `defineLoopSpec()` API — the **dogfooding
|
|
759
|
+
gate** locked in Phase 2. Two payer-side loop specs (1000A / 1000B)
|
|
760
|
+
also ship as introspection artifacts.
|
|
761
|
+
- **Money discipline.** All monetary fields decode as the new
|
|
762
|
+
`X12Decimal` (`src/decimal.ts`): a string-backed decimal type with
|
|
763
|
+
`BigInt`-exact arithmetic. **NEVER `parseFloat`** — float
|
|
764
|
+
representation silently destroys cents at scale; on an 835 a dropped
|
|
765
|
+
decimal is the wrong dollar amount in someone's cash post.
|
|
766
|
+
`X12Decimal` preserves the inbound lexical form for byte-exact
|
|
767
|
+
round-trip (`X12Decimal.fromString("0050.00").toString()` →
|
|
768
|
+
`"0050.00"`), exposes mathematical equality across scales
|
|
769
|
+
(`"0.00".equals("0")` → true), and ships `add` / `subtract` /
|
|
770
|
+
`compareTo` / `abs` / `negate` / `signum` / `isZero` plus a lossy
|
|
771
|
+
`toNumber()` whose JSDoc warns about precision loss. `fromBigInt(value,
|
|
772
|
+
scale)` renders canonically with zero-padded fractions; the canonical
|
|
773
|
+
`X12Decimal.ZERO` is the additive identity. Empty inbound element →
|
|
774
|
+
`undefined` (not zero) — "not supplied" and "zero dollars" are
|
|
775
|
+
spec-distinct.
|
|
776
|
+
- **Balance invariants (per TR3 X221A1 §1.10.2 — "Balancing the 835").**
|
|
777
|
+
Three checks run after the walk and emit
|
|
778
|
+
`X12_835_REMIT_BALANCE_MISMATCH` on mismatch — the model is **NEVER
|
|
779
|
+
silently rebalanced**: 1. Line: `SVC-02 === SVC-03 + Σ(line CAS)` per Loop 2110. 2. Claim: `CLP-03 === CLP-04 + Σ(all CAS in claim, claim AND line
|
|
780
|
+
level)` — the X12 spec balance. CLP-05 (patient responsibility)
|
|
781
|
+
is informational, NOT part of the balance equation. The
|
|
782
|
+
implementation matches the TR3 §1.10.2 text directly; an earlier
|
|
783
|
+
roadmap sketch (`operations/roadmaps/x12.md` §4) used a slightly
|
|
784
|
+
different decomposition — `src/transactions/remit/balance.ts`
|
|
785
|
+
documents the divergence so the contract stays consistent. 3. Top-of-remit: `BPR-02 === Σ(CLP-04) - Σ(PLB amounts)`. PLB
|
|
786
|
+
amounts are stored with the **raw EDI sign** (positive = take-back
|
|
787
|
+
from provider; negative = credit to provider), so the equation
|
|
788
|
+
_subtracts_ PLB to balance.
|
|
789
|
+
Warning messages echo only the invariant label and `X12Decimal`
|
|
790
|
+
decimal text — never patient identifiers, member ids, or account
|
|
791
|
+
numbers (H-PHI invariant).
|
|
792
|
+
- **CAS triple flattening.** A single CAS segment can carry up to 6
|
|
793
|
+
`(reason, amount, quantity)` triples under one `CAS-01` group code;
|
|
794
|
+
the walker flattens them into individual `X12RemitAdjustment`
|
|
795
|
+
entries. Different group codes (CO / PR / OA / PI) require separate
|
|
796
|
+
CAS segments — they cannot mix inside one — and the decoder honors
|
|
797
|
+
that contract.
|
|
798
|
+
- **Bundled WPC + X12-internal code-list snapshots** (initial
|
|
799
|
+
subsets, pre-launch). Versioned data artifacts at
|
|
800
|
+
`src/code-lists/`; the Phase 10 `pnpm refresh:code-lists` script
|
|
801
|
+
will regen the full lists from canonical sources for the first real
|
|
802
|
+
publish. Each snapshot ships `meta.id` / `meta.snapshotDate` /
|
|
803
|
+
`meta.publishedDate` / `meta.source` so consumers can decide
|
|
804
|
+
whether a stale description matters. Helpers `lookupCarc(code)` /
|
|
805
|
+
`lookupRarc(code)` / `lookupClpStatus(code)` return `{ code,
|
|
806
|
+
description }` for known codes, `undefined` otherwise; unknown
|
|
807
|
+
codes preserve the verbatim value on the parsed adjustment AND
|
|
808
|
+
emit `X12_UNKNOWN_CARC` / `X12_UNKNOWN_RARC`. - `CARC` (Claim Adjustment Reason Codes) — ~30 most commonly
|
|
809
|
+
observed codes (WPC, snapshotDate 2026-06-27). - `RARC` (Remittance Advice Remark Codes) — ~15 most commonly
|
|
810
|
+
observed codes covering both `M`- and `N`-prefix conventions
|
|
811
|
+
(WPC, snapshotDate 2026-06-27). - `CLP_STATUS` (CLP-02 Claim Status Code, X12 Code Source 65) —
|
|
812
|
+
10 dispositions (1 Processed as Primary, 4 Denied, 22 Reversal,
|
|
813
|
+
…). X12-internal list, stable. - `CLAIM_ADJUSTMENT_GROUP_CODES` — the spec-fixed 4 values
|
|
814
|
+
(`CO` / `PR` / `OA` / `PI`) as a frozen literal-union map,
|
|
815
|
+
not a snapshot (this list never grows). `isClaimAdjustmentGroupCode`
|
|
816
|
+
narrows inbound strings.
|
|
817
|
+
- **Public-surface additions** to the warning / fatal stability
|
|
818
|
+
snapshot: `X12_835_REMIT_BALANCE_MISMATCH`,
|
|
819
|
+
`X12_UNKNOWN_CARC`, `X12_UNKNOWN_RARC` (10 → 13 Tier-2 codes;
|
|
820
|
+
additions-only — fatal registry stays at 4). New warning factories
|
|
821
|
+
`remitBalanceMismatch` / `unknownCarc` / `unknownRarc` carry the
|
|
822
|
+
shape-validated echo discipline (CARC / RARC echoes pass
|
|
823
|
+
`/^[A-Z0-9]{1,5}$/u` or collapse to `(non-spec)`).
|
|
824
|
+
- **PHI discipline (H-PHI invariant holds suite-wide).** Warning
|
|
825
|
+
messages never echo field VALUES — only positional context, the
|
|
826
|
+
invariant label, the shape-validated CARC / RARC code, or numeric
|
|
827
|
+
X12Decimal text. Patient names, member ids, NPIs, payer claim
|
|
828
|
+
control numbers, and account numbers are held verbatim on the
|
|
829
|
+
parsed model (consumer-redaction boundary, mirroring hl7's H-PHI
|
|
830
|
+
posture) but never routed through warnings or errors. Every fixture
|
|
831
|
+
is synthetic (Greek-letter patient names, `MEMBER-*` member ids,
|
|
832
|
+
repetitive-digit NPIs); `phi-redaction-review` passed at commit time.
|
|
833
|
+
- **Six fixtures under `test/fixtures/remit/`.** Five Tier-1
|
|
834
|
+
synthetic spec-clean (`835-medicare-canonical.edi`,
|
|
835
|
+
`835-multi-claim.edi`, `835-with-plb.edi`,
|
|
836
|
+
`835-carc-rarc-mix.edi`, `835-imbalance.edi`) and one Tier-2
|
|
837
|
+
synthetic quirk shape (`835-availity-quirk.edi` — REF*2U + REF*F8
|
|
838
|
+
placements). The imbalance fixture is deliberately off-by-$10 to
|
|
839
|
+
prove the balance warning fires and the model preserves the
|
|
840
|
+
verbatim amounts.
|
|
841
|
+
- **Property tests.** `decimal.property.test.ts` locks lexical
|
|
842
|
+
round-trip + additive identity + commutativity + subtraction-by-
|
|
843
|
+
addition + negation involution + sign-consistency invariants (over
|
|
844
|
+
500 runs each). `remit-835-balance.property.test.ts` synthesizes
|
|
845
|
+
balanced and deliberately-imbalanced single-line claims and asserts
|
|
846
|
+
the balance warning fires iff out of balance (100 + 50 runs).
|
|
847
|
+
`remit-835-fuzz.property.test.ts` byte-flips every committed
|
|
848
|
+
fixture 300 times per fixture and asserts `get835` never throws
|
|
849
|
+
outside the 4 Tier-3 fatals — the byte-level fuzz target the
|
|
850
|
+
roadmap calls for.
|
|
851
|
+
- **Coverage gates expanded** to per-directory ≥90 on `parser/`,
|
|
852
|
+
`loops/`, `transactions/`, `code-lists/`. Phase 4 lands the gate
|
|
853
|
+
at **97.7% statements / 91.97% branches / 99.24% functions /
|
|
854
|
+
99.38% lines** globally.
|
|
855
|
+
- **Spec traceability:** TR3 `005010X221A1` for the 835 itself; X12
|
|
856
|
+
Code Source 65 for CLP-02; WPC public-domain lists for CARC / RARC;
|
|
857
|
+
X12 Data Element 1033 for the Claim Adjustment Group Code.
|
|
858
|
+
- **Known limitations after Phase 4:** no 835 _building_ yet (that's
|
|
859
|
+
Phase 8 — round-trip + spec-clean serializer + builder); the
|
|
860
|
+
bundled CARC / RARC are an **initial subset** (`pnpm
|
|
861
|
+
refresh:code-lists` arrives in Phase 10); no per-payer profile
|
|
862
|
+
yet (Phase 9); CPT / ICD-10 / NDC descriptions are deliberately
|
|
863
|
+
NOT bundled (license-gated — see `operations/roadmaps/x12.md` §5);
|
|
864
|
+
`X12Decimal` does not yet expose multiply / divide (no balance
|
|
865
|
+
invariant needs them in v1). `phi-scan` script not yet wired for
|
|
866
|
+
x12 — the H-PHI property tests provide runtime coverage; an
|
|
867
|
+
explicit pre-commit phi-scan ships in a future slice (tracked in
|
|
868
|
+
`operations/prompts/x12-phi-scan.md`).
|
|
869
|
+
|
|
870
|
+
- **Phase 3 — 999 + TA1 acknowledgments (TR3 005010X231A1).** Two
|
|
871
|
+
pure-function ack surfaces ship side-by-side; neither auto-sends, opens
|
|
872
|
+
a socket, or touches the filesystem. The cosyte ack archetype: the
|
|
873
|
+
library MECHANICALLY builds the disposition it is told and REFUSES to
|
|
874
|
+
fabricate an Accept against a non-empty error list. Mirrors hl7's
|
|
875
|
+
upcoming `buildAck` boundary and mllp's commit-contract pattern.
|
|
876
|
+
- **999 (Implementation Acknowledgment) — TR3 005010X231A1.**
|
|
877
|
+
`parse999(raw, opts?)` decodes the AK1 → AK2 → (IK3 [→ CTX] (IK4 [→
|
|
878
|
+
CTX])\*)\* → IK5 → AK9 hierarchy into the typed `X12Ack999`. Standard
|
|
879
|
+
X12 / pre-X231A1 legacy senders that emit `AK3`/`AK4`/`AK5` instead
|
|
880
|
+
of `IK3`/`IK4`/`IK5` are lenient-accepted on parse (normalized onto
|
|
881
|
+
the X231A1 names) per Postel's Law; `build999` always emits the
|
|
882
|
+
X231A1 names. `build999(spec)` assembles a complete `X12Interchange`
|
|
883
|
+
wrapping a single ISA → GS → ST..SE → GE → IEA with one 999 inside,
|
|
884
|
+
spec-clean and round-trippable through `parseX12`.
|
|
885
|
+
- **TA1 (Interchange Acknowledgment) — ASC X12 standard, envelope
|
|
886
|
+
level.** The Phase 1 envelope walker now captures envelope-level
|
|
887
|
+
TA1 segments verbatim onto `X12Interchange.ta1Segments` — TA1
|
|
888
|
+
between ISA and the first GS (the canonical position) is recognized
|
|
889
|
+
as spec-conformant and NO `X12_UNEXPECTED_SEGMENT` warning fires;
|
|
890
|
+
a TA1 inside an open functional group is still flagged as unexpected
|
|
891
|
+
(non-spec). `parseTA1(interchange)` returns the typed `X12AckTA1`
|
|
892
|
+
for the first captured TA1 (or `undefined`). `buildTA1(spec)` emits
|
|
893
|
+
a fixed-position 5-element `Ta1Segment` (`TA101`–`TA105`) — caller
|
|
894
|
+
wraps it in their preferred envelope. Both standalone TA1-only
|
|
895
|
+
interchanges (ISA → TA1 → IEA, no GS) and embedded TA1s round-trip.
|
|
896
|
+
- **Safety guards (refused via `AckBuildError`):** `build999` refuses
|
|
897
|
+
a functional `AK9-01 = 'A'` paired with any per-transaction non-`A`
|
|
898
|
+
response OR any error payload anywhere
|
|
899
|
+
(`X12_ACK_ACCEPT_WITH_ERRORS`); refuses internally inconsistent AK9
|
|
900
|
+
counts (`0 ≤ accepted ≤ received ≤ declared`,
|
|
901
|
+
`responses.length == received`, ≤ 5 syntax error codes on IK5/AK9)
|
|
902
|
+
(`X12_ACK_COUNT_MISMATCH`); refuses an ISA-13 longer than 9 chars
|
|
903
|
+
(`X12_ACK_INVALID_SPEC`). `buildTA1` refuses `TA1-04 = 'A'` paired
|
|
904
|
+
with a non-`000` TA1-05 note code (`X12_TA1_ACCEPT_WITH_NOTE`).
|
|
905
|
+
Four stable `ACK_BUILD_ERROR_CODES` typed as `AckBuildErrorCode`
|
|
906
|
+
discriminate the cases.
|
|
907
|
+
- **Public code-list registries:** `X12_ACK_DISPOSITION_CODES`
|
|
908
|
+
(code list 715: `A`/`E`/`P`/`R`/`M`/`W`/`X`),
|
|
909
|
+
`IK3_SYNTAX_ERROR_CODES` (code list 716, 13 codes),
|
|
910
|
+
`IK4_SYNTAX_ERROR_CODES` (code list 723, 18 codes),
|
|
911
|
+
`TA1_ACK_CODES` (code list I13: `A`/`E`/`R`),
|
|
912
|
+
`TA1_NOTE_CODES` (code list I18: `000`–`028`). String-literal
|
|
913
|
+
unions are exported for exhaustive narrowing. The helper
|
|
914
|
+
`isAcceptDisposition(code)` returns true for `A`/`E`/`P` and false
|
|
915
|
+
for the four reject codes.
|
|
916
|
+
- **PHI discipline (acks are structurally PHI-free by design):**
|
|
917
|
+
Control numbers, segment IDs, position counters, and structural
|
|
918
|
+
error codes ONLY. The one variable-shape surface that COULD carry
|
|
919
|
+
PHI — `IK4-04` (`copyOfBadDataElement`) — is documented on both the
|
|
920
|
+
parsed-model type AND the build-spec type as a caller-supplied
|
|
921
|
+
field that callers SHOULD omit when the offending bytes are PHI.
|
|
922
|
+
The library NEVER auto-populates `IK4-04`. Error messages
|
|
923
|
+
interpolate only control numbers, disposition codes, and count
|
|
924
|
+
integers; no PHI-shape paths. The `phi-redaction-review` crew gate
|
|
925
|
+
passed at commit time; locked `999 — PHI safety` and `TA1 — PHI
|
|
926
|
+
safety` test blocks assert no SSN / ISO-date / long-digit-run
|
|
927
|
+
shapes appear in built output.
|
|
928
|
+
- **Three Tier-1 999 fixtures** (`999-accept.edi`,
|
|
929
|
+
`999-accept-with-errors.edi`,
|
|
930
|
+
`999-reject-control-number-mismatch.edi`) and **three Tier-1 TA1
|
|
931
|
+
fixtures** (`ta1-accept.edi`, `ta1-accept-with-errors.edi`,
|
|
932
|
+
`ta1-reject-control-mismatch.edi`). All synthetic, no PHI.
|
|
933
|
+
- **Property tests:** `parse999(build999(spec))` round-trips
|
|
934
|
+
dispositions, counts, and AK1 echo on every clean accept (200
|
|
935
|
+
runs); functional `A` + any non-`A` per-transaction disposition
|
|
936
|
+
throws `AckBuildError` with code `X12_ACK_ACCEPT_WITH_ERRORS` (100
|
|
937
|
+
runs); functional `A` + non-empty AK9 syntax error codes throws the
|
|
938
|
+
same code (100 runs). Locks the Phase 3 safety invariant.
|
|
939
|
+
- **Public-surface additions** to the warning / fatal stability
|
|
940
|
+
snapshot: `Ta1Segment` type on the envelope-level surface;
|
|
941
|
+
`X12Interchange.ta1Segments: readonly Ta1Segment[]` (additive, no
|
|
942
|
+
rename); no new entries to `WARNING_CODES` or `FATAL_CODES`
|
|
943
|
+
(Phase 3 keeps both registries at the Phase-2-locked sizes of 10
|
|
944
|
+
and 4 — additions-only thereafter).
|
|
945
|
+
- **Spec traceability:** TR3 `005010X231A1` (999); ASC X12 standard §
|
|
946
|
+
TA1 Interchange Acknowledgment; code lists 715 / 716 / 723 / I13 / I18.
|
|
947
|
+
- **Known limitations after Phase 3:** Acks reference STRUCTURAL
|
|
948
|
+
errors only — they cannot report semantic / payment errors (those
|
|
949
|
+
live in `277CA` Phase 6 / `835` Phase 4). No multi-TA1 fan-out
|
|
950
|
+
helper (consumers iterate `ta1Segments` directly when more than
|
|
951
|
+
one inbound interchange is being acknowledged). The 999
|
|
952
|
+
transaction-set surface does not yet expose a public Loop-spec
|
|
953
|
+
artifact — Phase 3 hand-walks the AK1/AK2/IK3/IK4/IK5/AK9 hierarchy
|
|
954
|
+
in `parse-999.ts`; the dogfooding gate for `defineLoopSpec` lands
|
|
955
|
+
fully with Phase 4's 835 + Phase 5's 837 work.
|
|
956
|
+
|
|
957
|
+
- **Phase 2 — syntactic core: segment / element / composite / repetition
|
|
958
|
+
decode + warning registry + `defineLoopSpec`.** Every body segment inside
|
|
959
|
+
a transaction is now decoded into an immutable `X12Segment` carrying its
|
|
960
|
+
id, raw text, and 1-indexed element array. The verbatim source survives
|
|
961
|
+
on `X12TransactionSet.rawSegments` so a byte-exact round-trip is still
|
|
962
|
+
achievable independently of any downstream consumer's reads.
|
|
963
|
+
- **`?`-release-character escape** (`?~` → literal `~`, `?*` → literal
|
|
964
|
+
`*`, `?:` → literal `:`, `?^` → literal `^`, `??` → literal `?`)
|
|
965
|
+
implemented in `unescapeRelease` / `escapeRelease` / `splitWithRelease`
|
|
966
|
+
(zero-dep, single-pass). Pair has a lossless round-trip property:
|
|
967
|
+
`unescapeRelease(escapeRelease(v, d), d) === v` for any value `v` and
|
|
968
|
+
any 4-distinct-delimiter set `d` (500 fast-check runs). An unpaired
|
|
969
|
+
trailing `?` is preserved verbatim AND warned as
|
|
970
|
+
`X12_DANGLING_RELEASE_CHAR`; a `?` followed by a non-delimiter is
|
|
971
|
+
preserved verbatim with no warning (Postel's Law).
|
|
972
|
+
- **Dot-path traversal** — `getSegmentValue(seg, "03-1")` resolves
|
|
973
|
+
composite sub-element 1 of element 3 (both 1-indexed, matching TR3);
|
|
974
|
+
`"03[2]"` resolves the 3rd repetition (0-indexed); `"03[2]-1"` combines
|
|
975
|
+
them. Returns `undefined` for out-of-range paths, throws `TypeError`
|
|
976
|
+
only on malformed path strings (consumer bug). `getAllSegmentValues`
|
|
977
|
+
returns every repetition (or every Nth component) as `readonly string[]`.
|
|
978
|
+
- **Public `defineLoopSpec()` API** for TR3 loop authoring, ships with
|
|
979
|
+
structural validation + a typed `LoopSpecDefinitionError`. Phase 3+
|
|
980
|
+
transaction extractors author their built-in 999 / TA1 / 835 / 837
|
|
981
|
+
loops through the SAME public API consumers use for payer-specific
|
|
982
|
+
loops — the dogfooding gate locked in `documentation/repos/x12.md`.
|
|
983
|
+
- **Warning registry expanded 8 → 10** (additions-only):
|
|
984
|
+
`X12_DANGLING_RELEASE_CHAR` (unpaired `?` at element/segment end;
|
|
985
|
+
bytes are preserved on the parent element) and
|
|
986
|
+
`X12_UNEXPECTED_SEGMENT` (a `GE` with no open `GS`, `SE` with no open
|
|
987
|
+
`ST`, body segment outside any `ST..SE` — cases the Phase 1 walker
|
|
988
|
+
dropped silently). The PUBLIC `WARNING_CODES` snapshot test is the
|
|
989
|
+
breaking-change tripwire — renaming a code is breaking, additions
|
|
990
|
+
are not.
|
|
991
|
+
- **PHI discipline (mirrors hl7's H-PHI invariant):**
|
|
992
|
+
`X12_UNEXPECTED_SEGMENT` SHAPE-VALIDATES the echoed segment id
|
|
993
|
+
against `/^[A-Z][A-Z0-9]{1,2}$/u` and substitutes the literal
|
|
994
|
+
`(non-spec)` for anything else, so hostile input that puts PHI in
|
|
995
|
+
the first slot of a malformed "segment" never has those bytes
|
|
996
|
+
echoed into a warning message. The bytes themselves are preserved
|
|
997
|
+
on the parent container so consumers that want to inspect them can.
|
|
998
|
+
- **Tier-1 fixture** (`syntactic-core-body.edi`) exercises every Phase 2
|
|
999
|
+
surface end-to-end: composites (`HI*ABK:J45.50`), repetitions
|
|
1000
|
+
(`EQ*30^35^88`), `?`-release-character escape (`REF*EA*ID?*WITH?*STAR`),
|
|
1001
|
+
and straight-element segments (BHT, NM1). Real-world synthetic — no
|
|
1002
|
+
PHI. Parses with zero warnings.
|
|
1003
|
+
- **Properties:** release-escape round-trip (any value, any delimiters),
|
|
1004
|
+
escapeRelease output is fully decodable as `?<reserved>` pairs +
|
|
1005
|
+
non-reserved bytes (500 runs each), and a streaming-decode invariant
|
|
1006
|
+
(parser output is independent of input chunking — locks the v2
|
|
1007
|
+
streaming surface as a non-breaking future addition).
|
|
1008
|
+
- **`X12TransactionSet.segments` shape changed** from
|
|
1009
|
+
`readonly string[]` to `readonly X12Segment[]`; the raw form moves to
|
|
1010
|
+
`X12TransactionSet.rawSegments`. **Pre-alpha `0.0.x` consumers should
|
|
1011
|
+
migrate.** Library-internal change; no impact on `ix.warnings`,
|
|
1012
|
+
`ix.delimiters`, or the envelope-level accessors.
|
|
1013
|
+
|
|
1014
|
+
- **Phase 1 — envelope decoder.** `parseX12()` decodes the ISA / GS / GE / IEA
|
|
1015
|
+
interchange envelope from a raw `string` or `Buffer`, detecting all four
|
|
1016
|
+
delimiters (`element` byte 4, `repetition` ISA-11, `component` ISA-16,
|
|
1017
|
+
`segment` post-ISA-16) from fixed positions inside the ISA itself — the parser
|
|
1018
|
+
NEVER assumes a delimiter. Transaction-set bodies inside each ST..SE are kept
|
|
1019
|
+
opaque at this phase (raw segment strings, terminator stripped); Phase 2 adds
|
|
1020
|
+
segment / element / composite / repetition decode on top.
|
|
1021
|
+
- 4 Tier-3 fatal codes (locked, additions-only thereafter): `X12_EMPTY_INPUT`,
|
|
1022
|
+
`X12_NO_ISA_HEADER`, `X12_ISA_TOO_SHORT`, `X12_INVALID_DELIMITERS`.
|
|
1023
|
+
- 8 Tier-2 warning codes (additions-only): `X12_PRE_005010`,
|
|
1024
|
+
`X12_CONTROL_NUMBER_MISMATCH` (ISA-13↔IEA-02, GS-06↔GE-02, ST-02↔SE-02),
|
|
1025
|
+
`X12_GROUP_COUNT_MISMATCH`, `X12_TRANSACTION_COUNT_MISMATCH`,
|
|
1026
|
+
`X12_MISSING_IEA`, `X12_MISSING_GE`, `X12_MISSING_SE`, `X12_TRAILING_GARBAGE`
|
|
1027
|
+
(with verbatim `trailingBytes` preserved on the returned interchange).
|
|
1028
|
+
- `X12ParseError` carries `code`, `position` (interchange/group/transaction/
|
|
1029
|
+
segment/element indices), and a bounded `snippet` (≤ 64 chars) that is the
|
|
1030
|
+
documented consumer-redaction boundary. Warning messages NEVER echo field
|
|
1031
|
+
values — they carry positional context plus bounded metadata (counts,
|
|
1032
|
+
control-number pairs) — mirroring the hl7 H-PHI invariant.
|
|
1033
|
+
- Strict mode (`parseX12(raw, { strict: true })`) escalates the first Tier-2
|
|
1034
|
+
warning into a thrown `X12ParseError` carrying the warning code.
|
|
1035
|
+
- 4 Tier-1 envelope fixtures committed under `test/fixtures/envelope/`
|
|
1036
|
+
(canonical Medicare `*^:~`, Availity `^` repetition, BCBS `\` sub-element,
|
|
1037
|
+
no-trailing-CRLF). Plus property tests (lenient never throws outside the 4
|
|
1038
|
+
fatals, round-trip ISA byte-exact preservation), warning-codes snapshot,
|
|
1039
|
+
and a byte-flip envelope fuzz target.
|
|
1040
|
+
- Per-directory ≥90 coverage gate armed on `src/parser/` (current: 100%
|
|
1041
|
+
statements / 98.75% branches / 100% functions / 100% lines).
|
|
1042
|
+
|
|
1043
|
+
### Changed
|
|
1044
|
+
|
|
1045
|
+
- Inherits `@cosyte/test-utils` and `fast-check` as devDependencies — the
|
|
1046
|
+
conformance-kit runners (`lenientNeverThrowsProperty`) and the property/fuzz
|
|
1047
|
+
arbitraries land alongside the Phase 1 envelope code.
|
|
1048
|
+
|
|
1049
|
+
### Previously
|
|
1050
|
+
|
|
1051
|
+
- Initial repo scaffolding: package metadata, dual ESM + CJS build via `tsup`,
|
|
1052
|
+
strict TypeScript, type-checked ESLint with a JSDoc/`@example` gate on public
|
|
1053
|
+
exports, Prettier, and Vitest.
|
|
1054
|
+
|
|
1055
|
+
### Changed
|
|
1056
|
+
|
|
1057
|
+
- Migrated onto the shared cosyte engineering standard (Phase E). The toolchain
|
|
1058
|
+
is now inherited from the published `@cosyte/*` config packages instead of
|
|
1059
|
+
per-repo copies: `tsup.config.ts` uses `cosyteTsup`, `vitest.config.ts` uses
|
|
1060
|
+
`cosyteVitest`, and `eslint.config.js` is the three-line `cosyte` wrapper.
|
|
1061
|
+
Bumped to ESLint 10, Vitest 4 (+ `@vitest/coverage-v8` 4), Vite 7, and
|
|
1062
|
+
`@types/node` 22; added `@arethetypeswrong/cli` with an `attw --pack .` gate
|
|
1063
|
+
wired into `prepublishOnly`. CI and release are now thin callers of the
|
|
1064
|
+
reusable `cosyte/.github` workflows (the shared pipeline runs the Node 22 + 24
|
|
1065
|
+
matrix). The shared `@cosyte/tsconfig` base sets `verbatimModuleSyntax: false`.
|
|
1066
|
+
- Removed `.github/dependabot.yml`; org-wide dependency updates will be handled
|
|
1067
|
+
by Renovate when it is rolled out.
|