@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 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.