@cosyte/x12 0.0.18 → 0.1.0
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 +100 -4
- package/KNOWN-LIMITATIONS.md +435 -36
- package/README.md +268 -24
- package/dist/index.cjs +5864 -1152
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7975 -1457
- package/dist/index.d.ts +7975 -1457
- package/dist/index.mjs +5777 -1153
- package/dist/index.mjs.map +1 -1
- package/package.json +58 -27
package/README.md
CHANGED
|
@@ -1,63 +1,307 @@
|
|
|
1
1
|
<a href="https://cosyte.com">
|
|
2
2
|
<picture>
|
|
3
3
|
<source media="(prefers-color-scheme: dark)" srcset="https://cosyte.com/tile/cosyte-lockup-tile-on-dark-1200x300.png">
|
|
4
|
-
<img alt="Cosyte
|
|
4
|
+
<img alt="The Cosyte logo on its own white ground: the icon beside the word Cosyte." src="https://cosyte.com/tile/cosyte-lockup-tile-on-light-1200x300.png">
|
|
5
5
|
</picture>
|
|
6
6
|
</a>
|
|
7
7
|
|
|
8
8
|
# @cosyte/x12
|
|
9
9
|
|
|
10
|
-
>
|
|
10
|
+
> Read the money out of an 835, an 837 or a 271 without a TR3 open on your desk.
|
|
11
11
|
|
|
12
12
|
[](https://www.npmjs.com/package/@cosyte/x12)
|
|
13
13
|
[](https://github.com/cosyte/x12/actions/workflows/ci.yml)
|
|
14
14
|
[](./LICENSE)
|
|
15
15
|
[](https://nodejs.org)
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Developer-focused ASC X12 healthcare EDI parser, serializer, and builder for Node.js and TypeScript: lenient on parse, spec-clean on emit.
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
## Why this exists
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
Reading an X12 healthcare transaction correctly normally means buying the TR3 implementation guide
|
|
22
|
+
for it and then hand-mapping element positions that no compiler will ever check. This library
|
|
23
|
+
removes that step for the engineer who has one 835 to post, one 271 to answer or one 837 to send,
|
|
24
|
+
and who needs the amounts to be exactly right rather than approximately right. The nearest
|
|
25
|
+
alternatives are a general-purpose EDI translator, which frames segments correctly but knows nothing
|
|
26
|
+
about what CLP-04 means, and a hand-rolled split on the segment terminator, which works until the
|
|
27
|
+
first trading partner sends a repetition separator you did not plan for. This is neither: a
|
|
28
|
+
zero-dependency TypeScript library that decodes the healthcare transaction sets into typed models,
|
|
29
|
+
keeps every amount in exact decimal arithmetic, and reports each vendor deviation it tolerated with
|
|
30
|
+
a stable code instead of failing or guessing.
|
|
31
|
+
|
|
32
|
+
## Status
|
|
33
|
+
|
|
34
|
+
**Version 0.1.0**, published on npm from a public repository.
|
|
35
|
+
|
|
36
|
+
From `0.1.0` the public API is settled and safe to depend on: the exported readers, builders,
|
|
37
|
+
types and warning codes are the surface we keep stable, and the warning registry only ever gains
|
|
38
|
+
codes. Below 1.0 a breaking change bumps the minor version and is called out in the changelog with
|
|
39
|
+
its migration. On a `0.0.x` version, pin an exact version rather than a range. Typed read and typed
|
|
40
|
+
emit both ship for every transaction set
|
|
41
|
+
this package covers: 270, 271, 276, 277 and 277CA, 278 request and response, 820, 834, 835, 837P,
|
|
42
|
+
837I and 837D, 999 and TA1, each with a reader and a matching domain builder, and the two claims
|
|
43
|
+
attachments guides 45 CFR 162.2002 adopts for the period on and after 26 May 2028: the 277 request
|
|
44
|
+
for additional information (`006020X313`) and the 275 that answers it (`006020X314`). For those two the base X12 006020
|
|
45
|
+
structure is typed and no implementation-guide usage is checked, so a qualifier or segment the
|
|
46
|
+
guide requires is carried as sent and never validated; `KNOWN-LIMITATIONS.md` lists what else
|
|
47
|
+
they leave open. `X12_TR3_CONFORMANCE` is the machine-readable answer to which implementation
|
|
48
|
+
guide each one follows, and is a better source than this page.
|
|
49
|
+
|
|
50
|
+
Not covered, and deliberately so: a byte-exact round trip is not guaranteed in general (see
|
|
51
|
+
[Compatibility](#compatibility)), and non-healthcare transaction sets, EDIFACT, transport such as
|
|
52
|
+
AS2 and SFTP, pre-005010 revisions, 006020 guides other than the two attachments guides, and
|
|
53
|
+
decoding what an attachment carries are all out of scope.
|
|
54
|
+
|
|
55
|
+
## Install
|
|
22
56
|
|
|
23
57
|
```bash
|
|
24
|
-
# pnpm (recommended), also works with: npm install @cosyte/x12 | yarn add @cosyte/x12
|
|
25
58
|
pnpm add @cosyte/x12
|
|
26
59
|
```
|
|
27
60
|
|
|
28
|
-
|
|
61
|
+
`npm install @cosyte/x12` and `yarn add @cosyte/x12` work the same way.
|
|
29
62
|
|
|
30
|
-
|
|
63
|
+
Node `>=22.0.0` is the engine floor, and there are zero runtime dependencies. The package ships
|
|
64
|
+
dual ESM and CJS builds with type declarations for both conditions, so `import` and `require` each
|
|
65
|
+
resolve to the artifact meant for them.
|
|
66
|
+
|
|
67
|
+
## Usage
|
|
68
|
+
|
|
69
|
+
Parse an 835 remittance advice and read the money: the payment total, the claim's
|
|
70
|
+
charge/paid/patient-responsibility split, and the adjustment reason behind the difference. The
|
|
71
|
+
interchange below is synthetic, with a fabricated payer, provider, patient and amounts.
|
|
72
|
+
|
|
73
|
+
```ts runnable
|
|
31
74
|
import { parseX12, get835 } from "@cosyte/x12";
|
|
32
75
|
|
|
33
|
-
const
|
|
76
|
+
const raw = `ISA*00* *00* *ZZ*MEDICARE *ZZ*SUBMITTER *260601*1200*^*00501*000000001*0*P*:~
|
|
77
|
+
GS*HP*MEDICARE*SUBMITTER*20260601*1200*1*X*005010X221A1~
|
|
78
|
+
ST*835*0001~
|
|
79
|
+
BPR*I*450.00*C*ACH*CCP*01*123456789*DA*987654321*1512345678**01*111111111*DA*222222222*20260601~
|
|
80
|
+
TRN*1*0012345*1512345678~
|
|
81
|
+
DTM*405*20260601~
|
|
82
|
+
N1*PR*MEDICARE PART A~
|
|
83
|
+
N3*123 PAYER WAY~
|
|
84
|
+
N4*BALTIMORE*MD*21244~
|
|
85
|
+
PER*BL*JANE COORDINATOR*TE*5551234567~
|
|
86
|
+
N1*PE*SAMPLE CLINIC INC~
|
|
87
|
+
N3*456 PROVIDER LN~
|
|
88
|
+
N4*CLEVELAND*OH*44113~
|
|
89
|
+
REF*TJ*123456789~
|
|
90
|
+
LX*1~
|
|
91
|
+
CLP*PT-ACCT-001*1*500.00*450.00*50.00*MC*PAYER-CLAIM-001*11*1~
|
|
92
|
+
NM1*QC*1*PATIENT*TEST*A***MI*MEMBER001~
|
|
93
|
+
NM1*82*2*RENDERING PROVIDER INC*****XX*1234567890~
|
|
94
|
+
DTM*232*20260501~
|
|
95
|
+
DTM*233*20260501~
|
|
96
|
+
SVC*HC:99213*500.00*450.00**1~
|
|
97
|
+
DTM*472*20260501~
|
|
98
|
+
CAS*PR*1*50.00~
|
|
99
|
+
REF*6R*LINE-CTRL-001~
|
|
100
|
+
SE*23*0001~
|
|
101
|
+
GE*1*1~
|
|
102
|
+
IEA*1*000000001~`;
|
|
103
|
+
|
|
104
|
+
const ix = parseX12(raw);
|
|
34
105
|
const tx = ix.groups[0]?.transactions.find((t) => t.st.elements[1] === "835");
|
|
35
|
-
|
|
106
|
+
if (tx === undefined) throw new Error("no 835 in this interchange");
|
|
107
|
+
const remit = get835(ix.delimiters, tx);
|
|
108
|
+
if (remit === undefined) throw new Error("not an 835");
|
|
36
109
|
|
|
37
|
-
|
|
38
|
-
remit
|
|
39
|
-
|
|
110
|
+
// The payment, the claim's split, and who owes the difference and why.
|
|
111
|
+
const claim = remit.claims[0];
|
|
112
|
+
const adjustment = claim?.serviceLines[0]?.adjustments[0];
|
|
113
|
+
console.log("paid", remit.payment.totalActualPayment?.toString(), "by", remit.payment.method);
|
|
114
|
+
console.log("claim", claim?.patientControlNumber, "charged", claim?.totalChargeAmount?.toString());
|
|
115
|
+
console.log("patient owes", claim?.patientResponsibilityAmount?.toString());
|
|
116
|
+
console.log("reason", adjustment?.groupCode, adjustment?.reasonCode, adjustment?.reasonDescription);
|
|
117
|
+
console.log("warnings", ix.warnings.length);
|
|
118
|
+
|
|
119
|
+
// The values printed above, which the test suite asserts on every run:
|
|
120
|
+
remit.payment.totalActualPayment?.toString(); // => "450.00"
|
|
121
|
+
claim?.totalChargeAmount?.toString(); // => "500.00"
|
|
122
|
+
claim?.patientResponsibilityAmount?.toString(); // => "50.00"
|
|
123
|
+
adjustment?.groupCode; // => "PR"
|
|
124
|
+
adjustment?.reasonCode; // => "1"
|
|
125
|
+
adjustment?.reasonDescription; // => "Deductible Amount"
|
|
126
|
+
ix.warnings; // => []
|
|
40
127
|
```
|
|
41
128
|
|
|
42
|
-
|
|
129
|
+
It prints:
|
|
130
|
+
|
|
131
|
+
```text
|
|
132
|
+
paid 450.00 by ACH
|
|
133
|
+
claim PT-ACCT-001 charged 500.00
|
|
134
|
+
patient owes 50.00
|
|
135
|
+
reason PR 1 Deductible Amount
|
|
136
|
+
warnings 0
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The `groupCode` is what tells you who owes the money (`PR` patient responsibility, `CO` contractual
|
|
140
|
+
obligation, and so on). Read it; never infer it.
|
|
141
|
+
|
|
142
|
+
## PHI and safety
|
|
143
|
+
|
|
144
|
+
X12 healthcare transactions carry protected health information, so treat every interchange you hand
|
|
145
|
+
this library as PHI.
|
|
146
|
+
|
|
147
|
+
**What it does with your document.** It decodes it in memory and nothing else. No socket is opened,
|
|
148
|
+
nothing is written to disk, and nothing is fetched at run time: the bundled code lists are versioned
|
|
149
|
+
data snapshots, so updating one is a release rather than a network call. The acknowledgment builders
|
|
150
|
+
are pure functions and never auto-send.
|
|
151
|
+
|
|
152
|
+
**What it keeps out of diagnostics.** A warning `message` is a lookup into a frozen registry rather
|
|
153
|
+
than something built from your document, and no warning factory in this library takes a value
|
|
154
|
+
parameter, so no element of yours can reach a diagnostic. The code and the `position` say what and
|
|
155
|
+
where; the bytes stay on the model. `ALL_WARNING_MESSAGES` is exported so you can assert that.
|
|
156
|
+
|
|
157
|
+
**The one deliberate exception.** `X12ParseError.snippet` on the four structural fatals is a bounded
|
|
158
|
+
copy of the start of the input, so on real traffic it can carry PHI, and it is not redacted. Log
|
|
159
|
+
`err.code` and `err.position` instead, or redact at your call site.
|
|
160
|
+
|
|
161
|
+
**An attachment refuses to print.** The data of each BDS a 275 carries is read into
|
|
162
|
+
`X12AttachmentData`, whose string, JSON and `util.inspect` forms state its octet count and nothing
|
|
163
|
+
else; `readOctets()` is the one route to the octets.
|
|
164
|
+
|
|
165
|
+
**Builder refusals are a weaker surface, deliberately.** A `build*` refusal names the control
|
|
166
|
+
number, count or code you passed in, so that you can see what was refused. The rendered fragment is
|
|
167
|
+
bounded by an exported constant, but it is bounded rather than redacted. The two attachments
|
|
168
|
+
builders are the exception and name no value at all. Log `err.code`, not `err.message`, from a
|
|
169
|
+
builder.
|
|
170
|
+
|
|
171
|
+
**What you still own.** Transport, storage, retention, access control, audit logging, and every log
|
|
172
|
+
line your own code writes. This library makes no HIPAA compliance claim on your behalf.
|
|
173
|
+
|
|
174
|
+
## API
|
|
175
|
+
|
|
176
|
+
Everything ships from one entry point; there are no subpath imports.
|
|
177
|
+
|
|
178
|
+
- **Read.** `parseX12` decodes an interchange into a model, and a per-transaction reader turns a
|
|
179
|
+
transaction set into a typed one (`get835`, `get837Claims`, `get270Inquiry`, `get271Eligibility`,
|
|
180
|
+
`get276StatusInquiry`, `parse999`, `parseTA1`, and the rest).
|
|
181
|
+
- **Emit.** `serializeX12` and `buildInterchange` are the general path, and every transaction set
|
|
182
|
+
with a reader also has a domain builder (`build835`, `build837P`, `build271`, and the rest) that
|
|
183
|
+
layers that guide's own invariants on top.
|
|
184
|
+
- **Errors and warnings.** Parsing is lenient, and only **four** structural failures are ever fatal
|
|
185
|
+
(`X12_NO_ISA_HEADER`, `X12_ISA_TOO_SHORT`, `X12_INVALID_DELIMITERS`, `X12_EMPTY_INPUT`).
|
|
186
|
+
Everything else arrives on `ix.warnings` as a stable code with positional context, so a tolerated
|
|
187
|
+
deviation is visible rather than silent.
|
|
188
|
+
- **Money.** Every monetary, percent and quantity field decodes as `X12Decimal`, which is
|
|
189
|
+
string-backed with BigInt arithmetic, and this library never **`parseFloat`s** an EDI amount. A
|
|
190
|
+
slot the sender left empty reads `undefined` rather than zero, so "stated zero" and "stated
|
|
191
|
+
nothing" stay different readings.
|
|
192
|
+
- **Conformance.** `X12_TR3_CONFORMANCE` states which implementation guide each transaction set
|
|
193
|
+
follows and how that identifier stands against the federal incorporation by reference.
|
|
194
|
+
- **CORE Code Combinations.** `checkCoreCodeCombination` answers whether an 835 adjustment's group,
|
|
195
|
+
reason and remark codes are in a CORE Code Combinations table for a CORE-defined business scenario
|
|
196
|
+
you name. You supply the table and name its version, no CORE table ships with this package, and
|
|
197
|
+
an `unevaluated` answer is never permission to post.
|
|
198
|
+
|
|
199
|
+
Task-oriented recipes are in the [cookbook](./docs-content/cookbook.md), and
|
|
200
|
+
[KNOWN-LIMITATIONS.md](./KNOWN-LIMITATIONS.md) is the honest do-not-over-trust list.
|
|
201
|
+
|
|
202
|
+
### Dates and times
|
|
203
|
+
|
|
204
|
+
Every typed reader surfaces its dates verbatim, as the qualifier the sender used plus the bytes it
|
|
205
|
+
carried. `toObject`, `toISO` and `toDate` are the three ways to read one, and every `@cosyte/*`
|
|
206
|
+
parser exports the same three names with the same shapes and the same timezone rule.
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
import { toObject, toISO, toDate } from "@cosyte/x12";
|
|
210
|
+
|
|
211
|
+
// A DTP or DTM date off any typed reader: claim.dates[0], eligibility.dates[0], and so on.
|
|
212
|
+
const day = { formatQualifier: "D8", value: "20260601" };
|
|
213
|
+
|
|
214
|
+
toObject(day); // { year: 2026, month: 6, day: 1 }
|
|
215
|
+
toISO(day); // "2026-06-01"
|
|
216
|
+
|
|
217
|
+
// A calendar day is not an instant. Without a stated offset there is no answer.
|
|
218
|
+
toDate(day); // undefined
|
|
219
|
+
toDate(day, { assumeOffsetMinutes: 0 })?.toISOString(); // "2026-06-01T00:00:00.000Z"
|
|
220
|
+
toDate(day, { assumeOffsetMinutes: -300 })?.toISOString(); // "2026-06-01T05:00:00.000Z"
|
|
221
|
+
|
|
222
|
+
// A range converts to nothing, because an interval is not a point in time.
|
|
223
|
+
const span = { formatQualifier: "RD8", value: "20260601-20260605" };
|
|
224
|
+
toISO(span); // undefined
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**The decoded format qualifier set is `D8` and `RD8`, and nothing else.** Those are the two DTP-02
|
|
228
|
+
values this library already parses and builds: `D8` is a single `CCYYMMDD` day, and `RD8` is a
|
|
229
|
+
`CCYYMMDD-CCYYMMDD` range. A qualifier outside that pair, an absent one, a value whose digits do not
|
|
230
|
+
match the shape its qualifier declares, and a day that is not on the calendar all convert to
|
|
231
|
+
`undefined`. None of the three ever throws, so a lenient parse stays lenient all the way to the
|
|
232
|
+
value you read.
|
|
233
|
+
|
|
234
|
+
**`RD8` converts to `undefined` from all three, deliberately.** An interval is not a point in time,
|
|
235
|
+
so there is no single day to hand back for a service period or an eligibility span. Handing back the
|
|
236
|
+
first endpoint would be a quiet wrong answer with a right-looking shape. Split the range yourself
|
|
237
|
+
and convert whichever end you meant.
|
|
238
|
+
|
|
239
|
+
**`toDate` returns an instant only when you state the zone.** No X12 date element this library
|
|
240
|
+
decodes carries a UTC offset, so `assumeOffsetMinutes` (signed minutes east of UTC) is the only way
|
|
241
|
+
one reaches the result. Without it the answer is `undefined`: the host machine's timezone is never
|
|
242
|
+
read and UTC is never assumed, because the same midnight is two different instants either side of a
|
|
243
|
+
border and a date of birth read in the wrong zone is a day out.
|
|
244
|
+
|
|
245
|
+
`toObject` returns only the components the value stated, with `month` spec-native 1 to 12 rather
|
|
246
|
+
than the JavaScript `Date` 0 to 11, so `Object.keys()` of the result tells you the precision. Delete
|
|
247
|
+
`offsetMinutes` and what is left is accepted by `Temporal.PlainDateTime.from` and by luxon's
|
|
248
|
+
`DateTime.fromObject` with no renaming. Neither library is a dependency here; this package still has
|
|
249
|
+
zero of those.
|
|
250
|
+
|
|
251
|
+
`parseDocumentDate`, which reads the document date for the date-aware code-list queries, keeps its
|
|
252
|
+
own contract and still THROWS on input it refuses. That divergence is deliberate: it answers a
|
|
253
|
+
caller who asked a validity question and could not state the day, while these three read a document
|
|
254
|
+
that was already parsed leniently.
|
|
255
|
+
|
|
256
|
+
#### Using two @cosyte parsers in one file
|
|
257
|
+
|
|
258
|
+
The three names are identical in every `@cosyte/*` parser, so importing two of them means aliasing:
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
import { toISO as x12ToISO } from "@cosyte/x12";
|
|
262
|
+
import { toISO as hl7ToISO } from "@cosyte/hl7";
|
|
263
|
+
|
|
264
|
+
x12ToISO({ formatQualifier: "D8", value: "20260601" });
|
|
265
|
+
hl7ToISO(someParsedHl7Timestamp);
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
A namespace import works too: `import * as x12 from "@cosyte/x12"`, then `x12.toISO(day)`.
|
|
269
|
+
|
|
270
|
+
## Compatibility
|
|
271
|
+
|
|
272
|
+
- **005010 HIPAA transaction sets**, with hooks for the errata revisions the industry actually
|
|
273
|
+
exchanges. Non-healthcare sets such as 850, 856, 810 and 204, plus EDIFACT, AS2 and SFTP
|
|
274
|
+
transport, and pre-005010 revisions, are out of scope.
|
|
275
|
+
- **Vendor deviations become warnings, not exceptions.** Built-in profiles record whose
|
|
276
|
+
companion-guide deviation they accommodate; selecting one never changes an otherwise correct
|
|
277
|
+
parse, and profiles are authored through the same public `defineProfile()` API you have.
|
|
278
|
+
- **Round trips are not byte-exact in general.** `serializeX12` reproduces the segments on the model
|
|
279
|
+
verbatim, including element padding, composites and `?`-release escapes, but
|
|
280
|
+
**`serialize(parse(s)) === s` is not guaranteed in general**. Line breaks between segments are the
|
|
281
|
+
common case: any run of CR or LF between segments is absorbed at parse, so a pretty-printed file
|
|
282
|
+
emits compact. Most of the constructs that break a round trip are silent, so "my file has no line
|
|
283
|
+
breaks" is not sufficient either. [KNOWN-LIMITATIONS.md](./KNOWN-LIMITATIONS.md) is the canonical
|
|
284
|
+
list.
|
|
43
285
|
|
|
44
|
-
##
|
|
286
|
+
## Contributing
|
|
45
287
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
- **Money is exact, and never invented**: every monetary/percent/quantity field decodes as `X12Decimal` (string-backed, BigInt arithmetic), and the library **never `parseFloat`s** an EDI amount. **No slot holds an amount this library did not decode.** Where a reader used to substitute `X12Decimal.ZERO` the slot is now `X12Decimal | undefined` and reads `undefined`, so "the sender stated zero" and "the sender stated nothing" are different readings; through `0.0.12` they were the same `0`. That is a rule about the substitution, not a census of the model: some rows are dropped whole instead, which is a different and pre-existing shape, and no total is published here. Read [KNOWN-LIMITATIONS.md](./KNOWN-LIMITATIONS.md).
|
|
49
|
-
- **Safety-critical fidelity**: TRN reassociation traces, 835 balance invariants, 837 HL hierarchy integrity, 834 maintenance types, and 278 certification actions are preserved **verbatim** and never inferred; ambiguity yields a warning or a typed refusal, never a confident wrong answer.
|
|
50
|
-
- **PHI-disciplined**: synthetic-only fixtures, a PHI commit-gate, and a warning `message` that is a **lookup into a frozen registry**, not something built from your document. No warning factory in the library takes a value parameter, so no element can reach a diagnostic: the code and `position` say what and where, and the bytes stay on the model. `ALL_WARNING_MESSAGES` is exported so you can assert it. **Builder refusals are a different, and deliberately weaker, surface:** twenty-four refusal sites across ten builder modules name a value you passed in, so you can see which control number, count or code was refused. **What they will never name is a `claimId`, a member id, a member name, a trace or a diagnosis code** - a guarantee that held for the refusal templates and, until the release after `0.0.10`, did not hold for the shared type guards underneath them: a JSON-driven caller who sent a number where the types say string got `a number ("900412345678")` back, bounded but not redacted, from a guard standing on every element of every builder. Those guards report the type now and never the value. The array guard's primitive arm is redacted with them, but it still reports the `length` and the class tag of a forged array-like, bounded, because those describe the shape you forged rather than the contents of a document element. Each goes through `renderCallerValue`, capping the rendered **fragment** at `BUILD_REFUSAL_VALUE_MAX_RENDERED` (90 characters); all three names are exported so you can assert the ceiling. The whole `message` is that plus the site's own fixed text, so it is bounded by a constant but a larger one: a 120,000-character control number gave a 120,066-character `X12BuildError.message` before this and gives 150 now. That is robustness, not redaction: the value is one you supplied and bounding it hides nothing from you. It is also not escaped, and on the **ack** path it is not always strictly your own, since `build999`'s AK2-02 and `buildTA1` echo an inbound document's control numbers by design. Log `err.code`, not `err.message`, from a builder. **`defineProfile()` is bounded on the same terms since `0.0.6`** (twelve refusal sites, twenty-three caller values; the worst message measured **360,181 characters** before and **431** now, both at the `fixture` refusal, which names three caller values; the ceiling for that site is 443 and the suite asserts every site under 500; `X12ProfileError.profileName` is deliberately left unbounded so it still matches the name you passed). **And a forged non-array in a builder spec now refuses instead of hanging:** every indexed loop takes its bound from a checked array, so `{ length: "9".repeat(120000) }` draws a typed refusal rather than coercing to `Infinity` and looping forever. A few `for…of` reads still throw an untyped `TypeError` instead; both are in [KNOWN-LIMITATIONS.md](./KNOWN-LIMITATIONS.md). **The one deliberate exception:** `X12ParseError.snippet` on the four Tier-3 structural fatals is a bounded (≤ 64 character) copy of the start of the input, so on real traffic it can carry PHI. The library does not redact it. Redact at your call site, or log `err.code` and `err.position` instead. See [Keeping PHI out of logs](./docs-content/troubleshooting.md).
|
|
288
|
+
Questions, bug reports and trading-partner quirks all belong in
|
|
289
|
+
[GitHub issues](https://github.com/cosyte/x12/issues).
|
|
51
290
|
|
|
52
|
-
|
|
291
|
+
Pull requests are welcome. A contribution must clear `pnpm typecheck`, `pnpm lint` and `pnpm test`,
|
|
292
|
+
hold the per-directory coverage floors, pass the PHI commit gate (`pnpm phi-scan`) and the prose
|
|
293
|
+
gates (`pnpm check:no-emdash`, `pnpm check:no-internal-refs`), and carry a changeset. Every fixture
|
|
294
|
+
must be synthetic: never open a pull request carrying a real patient's data.
|
|
53
295
|
|
|
54
296
|
## Trademarks
|
|
55
297
|
|
|
56
|
-
Availity and Blue Cross Blue Shield are trademarks of their respective owners. cosyte is not
|
|
57
|
-
sponsored by any of them. The names identify the trading partners
|
|
298
|
+
Availity and Blue Cross Blue Shield are trademarks of their respective owners. cosyte is not
|
|
299
|
+
affiliated with, endorsed by, or sponsored by any of them. The names identify the trading partners
|
|
300
|
+
whose companion-guide deviations the built-in profiles accommodate. See
|
|
301
|
+
[TRADEMARKS.md](./TRADEMARKS.md).
|
|
58
302
|
|
|
59
303
|
## License
|
|
60
304
|
|
|
61
|
-
MIT. See [LICENSE](./LICENSE).
|
|
305
|
+
MIT, copyright Cosyte. See [LICENSE](./LICENSE).
|
|
62
306
|
|
|
63
307
|
Built by [Cosyte](https://cosyte.com).
|