@cosyte/x12 0.0.3 → 0.0.4
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 +320 -3
- package/README.md +2 -2
- package/dist/index.cjs +309 -251
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +611 -230
- package/dist/index.d.ts +611 -230
- package/dist/index.mjs +304 -252
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,320 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
11
|
|
|
12
|
+
- **`X12OrphanSegment.anchor`**, plus the `X12OrphanAnchor` / `X12OrphanAnchorKind` types. Every
|
|
13
|
+
retained orphan now records **where it sat in the structure** rather than only where it sat in the
|
|
14
|
+
byte stream: `{ kind: "interchange", groupIndex }` for a segment outside every functional group,
|
|
15
|
+
`{ kind: "group", groupIndex, transactionIndex }` for one inside a group but outside every
|
|
16
|
+
transaction set, and `{ kind: "transaction", groupIndex, transactionIndex, segmentOffset }` for one
|
|
17
|
+
inside an open `ST..SE` - which only a `TA1` can be, since anything else arriving there is body
|
|
18
|
+
content. An index equal to the eventual length means "after the last one" (immediately before the
|
|
19
|
+
`GE` or the `IEA`), and `segmentOffset` is never `0` because `rawSegments[0]` is always the `ST`.
|
|
20
|
+
- **`X12Interchange.orphanSegments`** and the `X12OrphanSegment` type. Every segment that falls
|
|
21
|
+
outside an `ST..SE` transaction set is now retained verbatim (`raw`, the decoded `segment`, its
|
|
22
|
+
`segmentIndex`, and the library-owned `context` discriminant) instead of being discarded.
|
|
23
|
+
`segmentIndex` equals the `position.segmentIndex` of that segment's `X12_UNEXPECTED_SEGMENT`
|
|
24
|
+
warning, so the two surfaces join without string matching. Empty for a well-formed interchange.
|
|
25
|
+
**Treat it as PHI when logging.** It sits on the model side of this library's diagnostic boundary:
|
|
26
|
+
a warning `message` is a frozen-registry lookup with positional metadata only, but an orphan is
|
|
27
|
+
document content, verbatim, exactly like `tx.rawSegments`. Log `context` and `segmentIndex`.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- **A segment outside a transaction now survives a round trip, and so does its warning.**
|
|
32
|
+
`serializeX12` re-emits every entry of `ix.orphanSegments` at its structural `anchor`, so a
|
|
33
|
+
consumer who serializes an interchange and re-derives warnings from the copy no longer loses the
|
|
34
|
+
segment or the `X12_UNEXPECTED_SEGMENT` that described it. Placement is **by the anchor and never
|
|
35
|
+
by `segmentIndex`** - that index addresses the _input_ stream, which the emit does not follow, and
|
|
36
|
+
a replay keyed on it was measured splicing a stray segment into an 835's `ST..SE` body with no
|
|
37
|
+
warning on the re-parse. An anchor names a slot in the typed tree, which is invariant under both
|
|
38
|
+
the `ta1Segments` hoist and the skipped zero-length segment. Use `segmentIndex` to join an orphan
|
|
39
|
+
to its warning, never to place it.
|
|
40
|
+
|
|
41
|
+
Measured on a stray segment inserted at every position of a two-group, three-transaction
|
|
42
|
+
interchange, over five segment ids (`ZZ`, `SE`, `GE`, `ST`, `TA1`) covering all five orphan
|
|
43
|
+
`context` values and all three anchor kinds: **50 of the 50 insertions that produce an orphan
|
|
44
|
+
round-trip byte-exactly** on a base with no envelope-level `TA1`. On the same base _with_ one, all
|
|
45
|
+
**54** differ - and all 54 are byte-identical once the `TA1` is removed from both sides, so the
|
|
46
|
+
only thing that moved is the `TA1`, which moves on that base with no orphan present at all. Across
|
|
47
|
+
all 104: transaction bodies, `orphanSegments` (raw, context, anchor), `ta1Segments`,
|
|
48
|
+
`trailingBytes` and the warning multiset are unchanged by the round trip, and every emit is a fixed
|
|
49
|
+
point.
|
|
50
|
+
|
|
51
|
+
**SE-01 now counts an orphan re-emitted between the `ST` and the `SE`.** A `TA1` that arrived
|
|
52
|
+
inside an open transaction set is lifted off `tx.rawSegments` by the walker but is re-emitted
|
|
53
|
+
where it came from, so it is a segment of that transaction set for SE-01 purposes ("segments
|
|
54
|
+
included in the transaction set, including ST and SE", X12.6). Reconciling against the model alone
|
|
55
|
+
would describe bytes the serializer did not write: `{ specClean: true, recomputeCounts: true }`
|
|
56
|
+
would shrink a **correct** `SE*4*` to `SE*3*` over four emitted segments, and the inverse input
|
|
57
|
+
would draw no mismatch warning at all. Both counts now come from the emitted range, so recompute
|
|
58
|
+
is idempotent under the library's own reconciliation. An orphan emitted before the `ST` or after
|
|
59
|
+
the `SE` is outside the range and is not counted, and GE-01 / IEA-01 are unaffected because an
|
|
60
|
+
orphan is never a `GS` and never opens a transaction set.
|
|
61
|
+
|
|
62
|
+
**`KNOWN-LIMITATIONS.md` is therefore down from seven constructs to six**, and the remaining five
|
|
63
|
+
silent ones no longer include anything that loses a warning: line breaks, a doubled terminator, a
|
|
64
|
+
missing final terminator, the `TA1` reorder, and a segment whose first element is empty outside a
|
|
65
|
+
transaction (still the one construct that loses a value with no diagnostic at all - it is skipped
|
|
66
|
+
by the walker, so there is nothing on the model to re-emit, and it is deliberately unchanged here).
|
|
67
|
+
Retention and placement are still **not** promotion: no `get*` reader sees an orphan, and a `TA1`
|
|
68
|
+
inside a group still does not join `ta1Segments`. No new warning code (registry unchanged at 22
|
|
69
|
+
codes, 4 fatals), and no construct became fatal.
|
|
70
|
+
|
|
71
|
+
- **Silent data loss: a segment outside a transaction was dropped from the model, and a
|
|
72
|
+
double-spaced file lost its entire interchange body.** Two defects with one cause. The envelope
|
|
73
|
+
walker raised `X12_UNEXPECTED_SEGMENT` for a segment it could not place and then discarded it, so
|
|
74
|
+
the segment's bytes were unrecoverable. Separately the line-break tolerance was exactly one
|
|
75
|
+
optional CR then one optional LF, which admitted **4 of the 15** CR/LF sequences of length 0 to 3;
|
|
76
|
+
the other 11 left a break in the stream that opened an unrecognized segment, so via the first
|
|
77
|
+
defect a uniformly **double-spaced file returned `groups: []`**.
|
|
78
|
+
|
|
79
|
+
The parser now absorbs any run of CR / LF bytes between segments (safe because a CR or LF in the
|
|
80
|
+
segment-terminator position, the byte immediately after ISA-16, is refused as the Tier-3 fatal
|
|
81
|
+
`X12_INVALID_DELIMITERS` (as it is at all four delimiter positions), so such a run is never
|
|
82
|
+
structural), and every unplaceable segment is retained on `ix.orphanSegments` through a
|
|
83
|
+
single chokepoint that raises the warning and records the segment together, so the two can never
|
|
84
|
+
disagree. Measured before, then after: CR/LF sequences of length 0 to 3 that frame correctly, **4
|
|
85
|
+
of 15 then 15 of 15**; orphan cases that retain the segment, **0 of 10 then 10 of 10** (ten
|
|
86
|
+
constructed cases over nine distinct positions). Across the
|
|
87
|
+
56 committed fixtures nothing changed: zero model divergences, zero warning divergences, zero
|
|
88
|
+
fixed-point failures, and no fixture produces an orphan.
|
|
89
|
+
|
|
90
|
+
**This fixed the model; the emit was closed separately, by anchor.** See the round-trip entry
|
|
91
|
+
below. A positional replay keyed on `segmentIndex` was built and then removed during _this_ change
|
|
92
|
+
because it was unsound: the emit is not in input order (it hoists `ta1Segments`) and skips the
|
|
93
|
+
zero-length segment a doubled terminator produces, so replaying by input index spliced the orphan
|
|
94
|
+
into whatever occupied that slot. Measured on a two-group interchange with a TA1 after the first
|
|
95
|
+
group, that put a stray segment inside an 835's `ST..SE` body between `CLP` and `SE` with **no
|
|
96
|
+
warning at all** on the re-parse, made a stray `SE` close the transaction early and corrupt SE-01,
|
|
97
|
+
and carried an orphan across the IEA into `trailingBytes`. A documented omission was preferable to
|
|
98
|
+
silent structural corruption, and there are regression tests fencing each of those shapes.
|
|
99
|
+
|
|
100
|
+
**Retention is not placement.** An orphan is not decoded by any `get*` reader, and a `TA1` inside
|
|
101
|
+
an open group is not added to `ta1Segments` (that surface means "envelope-level TA1", and is what
|
|
102
|
+
`parseTA1` reads). Neither a doubled segment terminator nor a segment whose first element is empty
|
|
103
|
+
is recorded as an orphan; both are long-standing behaviour, now stated rather than implied.
|
|
104
|
+
|
|
105
|
+
Also in this change: a trailing CR/LF run after the final segment terminator is absorbed rather
|
|
106
|
+
than surfacing as `trailingBytes` (previously `~\n\n` there produced a `trailingBytes` of `"\n~"`,
|
|
107
|
+
a byte the input never contained, plus an `X12_TRAILING_GARBAGE` warning); a double-spaced file
|
|
108
|
+
that previously produced 7 warnings now parses cleanly with none; and the five
|
|
109
|
+
`X12_UNEXPECTED_SEGMENT` messages were rewritten, since each stated that the segment was not
|
|
110
|
+
retained. No warning code was added or removed (registry unchanged at 22 warnings and 4 fatals) and
|
|
111
|
+
nothing new throws.
|
|
112
|
+
|
|
113
|
+
### Documentation
|
|
114
|
+
|
|
115
|
+
> **Read the "Fixed" entry above first: it ships in the same release and supersedes two statements
|
|
116
|
+
> made below.** The entry below describes the state of the parser BEFORE that fix, and two of its
|
|
117
|
+
> present-tense sentences are no longer true as of this release: the parser now absorbs **any run of
|
|
118
|
+
> CR / LF** between segments, not "an optional CR then an optional LF"; and a segment outside a
|
|
119
|
+
> transaction is **retained on `ix.orphanSegments`**, not "discarded". What still holds from it, and
|
|
120
|
+
> is the reason it is kept, is the round-trip scope: `serialize(parse(s)) === s` is not guaranteed,
|
|
121
|
+
> the absence of line breaks is not sufficient, `KNOWN-LIMITATIONS.md` is the canonical list at
|
|
122
|
+
> **seven** constructs, and an orphan is still not re-emitted.
|
|
123
|
+
|
|
124
|
+
- **Corrected the round-trip claim: `serialize(parse(s)) === s` is NOT guaranteed, and the emit is
|
|
125
|
+
byte-faithful only for the segments the parser recorded on the model.** The README described the
|
|
126
|
+
default emit mode as "byte-faithful by default" with no qualification, and line-ending handling was
|
|
127
|
+
absent from the entire consumer surface (zero mentions across the README, all nine `docs-content/`
|
|
128
|
+
pages, and `KNOWN-LIMITATIONS.md`). Meanwhile the parser absorbs an optional CR then an optional LF
|
|
129
|
+
after every segment terminator and the model has nowhere to record it. Re-measured against the
|
|
130
|
+
committed corpus: **42 of the 56 fixtures do not return byte-identical, and all 42 differ from their
|
|
131
|
+
source by line breaks and nothing else**; the remaining 14 carry no line breaks and do return
|
|
132
|
+
byte-identical.
|
|
133
|
+
|
|
134
|
+
Nothing about the emit changed. What changed is the claim. Line breaks turned out to be only the
|
|
135
|
+
most common of **six** constructs the emit does not reproduce, and the other five fire on inputs
|
|
136
|
+
containing no line breaks at all, so "my file is compact" is not sufficient grounds to expect byte
|
|
137
|
+
equality: segments outside a transaction (raised as `X12_UNEXPECTED_SEGMENT` on the first parse, then
|
|
138
|
+
discarded, so the segment **and its warning** are absent from the emit), a doubled segment terminator
|
|
139
|
+
outside a transaction, a missing final terminator (the emit supplies one), post-IEA `trailingBytes`
|
|
140
|
+
(re-joined from segment slices rather than preserved verbatim), and a **TA1 that followed a
|
|
141
|
+
functional group**, which is collected onto `ix.ta1Segments` and emitted immediately after the ISA,
|
|
142
|
+
so the emit **reorders** it (nothing is lost there: the model and warning stream round-trip
|
|
143
|
+
identically, and no position is taken on where ASC X12 requires a TA1 to sit). **Four of the six are
|
|
144
|
+
silent**, so a clean `ix.warnings` is not evidence that a round trip will be byte-exact.
|
|
145
|
+
`KNOWN-LIMITATIONS.md` now holds the canonical list, and the other sites link to it and carry the
|
|
146
|
+
load-bearing warnings rather than restating a count: `serialize(parse(s)) === s` is not guaranteed,
|
|
147
|
+
the absence of line breaks is not sufficient, and `serializeX12(parseX12(source))` must not be used
|
|
148
|
+
as a normalization step before comparing warnings.
|
|
149
|
+
|
|
150
|
+
The properties that hold are now stated **as measured over the committed corpus** rather than as
|
|
151
|
+
universals: every emit is a fixed point and re-parses to an identical model with an identical warning
|
|
152
|
+
stream, the 14 line-break-free fixtures return byte-identical, and the other 42 differ by line breaks
|
|
153
|
+
and nothing else. Two caveats bound that sweep and are stated with it: the corpus contains no
|
|
154
|
+
instance of the five non-line-break cases, and 13 of the 14 byte-identical fixtures are
|
|
155
|
+
`golden/*.edi`, which are serializer output by construction, leaving `envelope/no-trailing-crlf.edi`
|
|
156
|
+
as the only independent witness.
|
|
157
|
+
|
|
158
|
+
Also corrected: `KNOWN-LIMITATIONS.md` and `troubleshooting.md` both opened by promising the lenient
|
|
159
|
+
parser "never silently drops or garbles data". Both now scope that to a **decoded value** and name
|
|
160
|
+
the two things that are discarded. `spec-notes-tolerance.md` presented a three-tier taxonomy with no
|
|
161
|
+
slot for a silent normalization; the tiers now name it, and note that a Tier-2 unexpected segment is
|
|
162
|
+
warned about but not kept.
|
|
163
|
+
|
|
164
|
+
### Documented, not fixed
|
|
165
|
+
|
|
166
|
+
> **Superseded in part by the "Fixed" entry above, which ships in the same release.** The model half
|
|
167
|
+
> of the entry below is now fixed: the segment is retained on `ix.orphanSegments`, and a double-spaced
|
|
168
|
+
> file no longer loses its interchange body. The **round-trip** half still stands: `serializeX12` does
|
|
169
|
+
> not re-emit an orphan, so the warning still does not survive. The two entries are kept separate
|
|
170
|
+
> because they were separate changes, and the sentences below describe the release before this one.
|
|
171
|
+
|
|
172
|
+
- **A segment that falls outside a transaction is dropped from the model, and its warning does not
|
|
173
|
+
survive a round trip.** The envelope walker keeps body segments only while an ST..SE transaction is
|
|
174
|
+
open; anything else raises `X12_UNEXPECTED_SEGMENT` and is then discarded. A **blank** line between
|
|
175
|
+
segments (`~\n\n`) exceeds the one-CR-plus-one-LF tolerance and triggers exactly this, so a uniformly
|
|
176
|
+
double-spaced file loses its **entire interchange body** and parses to `groups: []`. The first parse
|
|
177
|
+
does warn, so it is detectable rather than silent, but the warning is the only signal and it cannot
|
|
178
|
+
be recovered from the emit. Long-standing behaviour, unchanged here and reproducing on the previous
|
|
179
|
+
release; now disclosed in `README`, `KNOWN-LIMITATIONS.md`, `troubleshooting.md` and
|
|
180
|
+
`spec-notes-envelope.md` rather than latent, and pinned by tests, while the fix is scoped as its own
|
|
181
|
+
item.
|
|
182
|
+
|
|
183
|
+
### Tests
|
|
184
|
+
|
|
185
|
+
- **The default emit mode's guarantees are locked against the whole committed corpus, not just the 13
|
|
186
|
+
goldens.** The goldens are already in the serializer's image, so they could only ever demonstrate
|
|
187
|
+
the easy half of the round trip. `test/serialize.test.ts` now discovers every `.edi` fixture from
|
|
188
|
+
disk (so a fixture added later is covered without anyone remembering) and asserts four properties per
|
|
189
|
+
fixture: the emit differs from its source by line breaks and nothing else, it re-parses to an
|
|
190
|
+
identical model with an identical warning stream, it is a fixed point, and it is byte-identical to
|
|
191
|
+
its source exactly when the source has no line breaks. A guard test asserts the corpus really
|
|
192
|
+
contains both pretty-printed and compact fixtures, so the sweep cannot pass vacuously. Separate cases
|
|
193
|
+
lock that LF, CRLF, and bare CR all normalize to one identical compact form.
|
|
194
|
+
|
|
195
|
+
- **A `round-trip escape hatches` suite pins the five inputs that falsify the corpus sweep's
|
|
196
|
+
biconditional**, none of which contains a line break: a segment outside a transaction (asserting
|
|
197
|
+
both that the value is gone from the emit and that its warning does not recur on re-parse), a
|
|
198
|
+
doubled terminator, a missing final terminator, post-IEA trailing bytes, and a TA1 following a
|
|
199
|
+
functional group (asserting it is reordered ahead of the GS but not lost, and still a fixed point).
|
|
200
|
+
Each case asserts its own warning count rather than describing it, so the fact that three of them
|
|
201
|
+
are silent is pinned rather than claimed. The committed corpus contains no instance of any of them,
|
|
202
|
+
so without these the sweep would stay green while the prose around it claimed more than the sweep
|
|
203
|
+
could see. The blank-line case now asserts the actual outcome (`groups: []` and an empty emit body)
|
|
204
|
+
instead of only the warning; an earlier revision described that input as "reported not swallowed",
|
|
205
|
+
which is exactly backwards.
|
|
206
|
+
|
|
207
|
+
### Security
|
|
208
|
+
|
|
209
|
+
- **Warning messages are built from a frozen registry instead of from the document, closing a PHI
|
|
210
|
+
leak in every envelope control number and declared count.** Before this change
|
|
211
|
+
`X12_CONTROL_NUMBER_MISMATCH` rendered **both** sides of the disagreeing pair into `message`
|
|
212
|
+
verbatim and unbounded, on all six slots (`ISA-13`/`IEA-02`, `GS-06`/`GE-02`, `ST-02`/`SE-02`); a
|
|
213
|
+
300,000-byte trailer control number produced a 300,062-byte `message`. `X12_GROUP_COUNT_MISMATCH`,
|
|
214
|
+
`X12_TRANSACTION_COUNT_MISMATCH`, `X12_SEGMENT_COUNT_MISMATCH` and `X12_PRE_005010` did the same
|
|
215
|
+
with `IEA-01`, `GE-01`, `SE-01` and `ISA-12`, and `X12_835_REMIT_BALANCE_MISMATCH` rendered three
|
|
216
|
+
monetary amounts. Five of those six control-number slots are variable-width, so "it is only a
|
|
217
|
+
control number" was never a bound: it is free-form trading-partner text that routinely carries a
|
|
218
|
+
batch or patient-account identifier.
|
|
219
|
+
|
|
220
|
+
**No warning factory takes a value parameter any more.** Each takes a `X12Position` plus, where one
|
|
221
|
+
code covers several situations, a library-owned discriminant (`CONTROL_NUMBER_PAIRS`,
|
|
222
|
+
`UNEXPECTED_SEGMENT_CONTEXTS`, `BALANCE_INVARIANTS`, `REQUIRED_LOOPS`), and `message` is a lookup
|
|
223
|
+
into a frozen table. That replaces the previous posture, which shape-validated an echoed value
|
|
224
|
+
against a spec grammar and substituted `(non-spec)` when it did not match. The shape test held for
|
|
225
|
+
the code-list slots (CARC, RARC, HI qualifier, CSCC, CSC, maintenance type, 837 variant, segment
|
|
226
|
+
id) and could not hold for a control number, whose grammar is whatever the sender sent. Taking no
|
|
227
|
+
value at all is a property of the signature rather than a filter someone has to remember to apply.
|
|
228
|
+
|
|
229
|
+
Nothing is discarded: every value is still preserved verbatim on the model, which is where a
|
|
230
|
+
consumer that has decided it may handle PHI reads it.
|
|
231
|
+
|
|
232
|
+
- **A strict-mode escalation no longer carries a snippet.** `parseX12(raw, { strict: true })` turned
|
|
233
|
+
the first Tier-2 warning into an `X12ParseError` carrying 64 bytes of the interchange, which put
|
|
234
|
+
document bytes into `err.stack` and from there into any error reporter. The escalated error now
|
|
235
|
+
carries `snippet: ""`; its `message` is the registry entry the warning carried and `position`
|
|
236
|
+
locates it. `snippet` remains on the four Tier-3 structural fatals, which are raised before the
|
|
237
|
+
envelope is readable, and remains the library's one deliberate exception.
|
|
238
|
+
|
|
239
|
+
- **`X12Segment.id` is bounded to the X12 segment-id grammar.** It is a derived structural
|
|
240
|
+
identifier, the field a downstream package interpolates to say where something is, and it was a
|
|
241
|
+
verbatim copy of the segment's first element. A sender that put a 300,000-byte value there had it
|
|
242
|
+
copied into any locus built from `seg.id`. A first element outside the grammar now yields the
|
|
243
|
+
exported `NON_SPEC_SEGMENT_ID` sentinel; `seg.raw` and `seg.elements[0]` keep the bytes, so
|
|
244
|
+
round-trip stays byte-exact.
|
|
245
|
+
|
|
246
|
+
- **`X12_INVALID_DELIMITERS` no longer echoes the detected element separator byte.** The message names
|
|
247
|
+
the fixed ISA position that broke instead.
|
|
248
|
+
|
|
249
|
+
### Added
|
|
250
|
+
|
|
251
|
+
- **`ALL_WARNING_MESSAGES`**, the frozen set of every message string the library can emit, so a
|
|
252
|
+
consumer or a conformance gate can assert `ix.warnings.every((w) => ALL_WARNING_MESSAGES.has(w.message))`.
|
|
253
|
+
- **`CONTROL_NUMBER_PAIRS`, `UNEXPECTED_SEGMENT_CONTEXTS`, `BALANCE_INVARIANTS`, `REQUIRED_LOOPS`**
|
|
254
|
+
and their `X12ControlNumberPair` / `X12UnexpectedSegmentContext` / `X12BalanceInvariant` /
|
|
255
|
+
`X12RequiredLoop` types: the library-owned discriminants the warning factories now take in place of
|
|
256
|
+
a value.
|
|
257
|
+
- **`NON_SPEC_SEGMENT_ID`**, the sentinel `X12Segment.id` takes when the first element is not a spec
|
|
258
|
+
segment id.
|
|
259
|
+
|
|
260
|
+
### Changed
|
|
261
|
+
|
|
262
|
+
- **BREAKING (pre-alpha): every exported warning factory changed signature.**
|
|
263
|
+
`controlNumberMismatch(position, pair)`, `unexpectedSegment(position, context)`,
|
|
264
|
+
`remitBalanceMismatch(position, invariant)` and `missingRequiredLoop(position, loop)` now take a
|
|
265
|
+
discriminant from the constants above; `pre005010`, `groupCountMismatch`,
|
|
266
|
+
`transactionCountMismatch`, `segmentCountMismatch`, `trailingGarbage`, `unknownCarc`,
|
|
267
|
+
`unknownRarc`, `hlParentMismatch`, `hlParentLevelInvalid`, `unknownHiQualifier`,
|
|
268
|
+
`unknown837Variant`, `unknownClaimStatusCategory`, `unknownClaimStatus` and
|
|
269
|
+
`unknownMaintenanceType` now take a `position` only.
|
|
270
|
+
- **Warning messages no longer carry the declared-versus-actual counts or the balance amounts.** Both
|
|
271
|
+
sides of each are on the model (`iea.elements[1]` against `ix.groups.length`,
|
|
272
|
+
`claim.totalChargeAmount` against `claim.totalPaymentAmount` and the CAS adjustments), so the
|
|
273
|
+
information is one dereference away rather than rendered into a string a consumer logs by default.
|
|
274
|
+
|
|
275
|
+
### Fixed
|
|
276
|
+
|
|
277
|
+
- **Every out-of-balance claim and service line now gets a distinct warning position.** `CLP`
|
|
278
|
+
segment positions were hard-coded to `0` (`clpSegmentIndex: 0, // populated in the future`), which
|
|
279
|
+
was harmless while the message rendered the amounts and is not once the message names only the
|
|
280
|
+
equation: two claims failing the same invariant produced byte-identical warnings at byte-identical
|
|
281
|
+
positions. `position.segmentIndex` is now the CLP's own 1-based index in the transaction body, so
|
|
282
|
+
`tx.segments[position.segmentIndex]` is that exact CLP. A service-line warning's is that index plus
|
|
283
|
+
the line's zero-based ordinal plus one, which is a **unique locator rather than a pointer at the
|
|
284
|
+
SVC**: read it as "the claim whose CLP is at `segmentIndex - (ordinal + 1)`, service line
|
|
285
|
+
`ordinal`". Those cannot collide across claims: a claim with `n` service lines spans at least
|
|
286
|
+
`n + 1` body segments.
|
|
287
|
+
|
|
288
|
+
- **Five `X12_UNEXPECTED_SEGMENT` messages claimed the segment was preserved; it is not.** A `GE`
|
|
289
|
+
with no open `GS`, an `ST` with no open group, an `SE` with no open transaction set, a `TA1` inside
|
|
290
|
+
a group, and a body segment outside any transaction set are all warned about and then dropped, so
|
|
291
|
+
"preserved on the prior open container" (and, in the last case, "its bytes are on `seg.raw`") named
|
|
292
|
+
a field that does not exist. Each message now says the segment is not retained and points at
|
|
293
|
+
`position.segmentIndex` in the input.
|
|
294
|
+
|
|
295
|
+
- **The disclosure overclaimed on the builders, and the first correction of it still did.** It said
|
|
296
|
+
builder refusals carry structural locators and numeric totals only. Measured against the source:
|
|
297
|
+
at least **sixteen** refusal sites across **ten** `build*` modules interpolate a caller-supplied
|
|
298
|
+
value verbatim and unbounded. Nine are the shared over-long-control-number template, where the
|
|
299
|
+
branch fires **because** the value is over-long. Seven are not gated on length at all:
|
|
300
|
+
`build999` echoes the supplied ST-02 transaction-set control number in two refusals,
|
|
301
|
+
`buildInterchange` the supplied transaction-set id code, `build837` a service line's `variant`,
|
|
302
|
+
`build834` an unrecognized INS-03 and an unrecognized HD-01 maintenance type, and `buildTA1` an
|
|
303
|
+
unrecognized TA1-05 note code. Measured with a 120,000-byte value: a 120,155-byte
|
|
304
|
+
`AckBuildError.message` from `build999` (with a longer `stack` still, whose exact size depends on
|
|
305
|
+
the frame text and so is not quoted), and a 120,069-byte
|
|
306
|
+
`X12BuildError.message` from `buildInterchange`. The README, `docs-content/troubleshooting.md` and
|
|
307
|
+
`KNOWN-LIMITATIONS.md` now name the whole surface, including the ungated sites, and say to log
|
|
308
|
+
`err.code` from a builder. Bounding the builders is a separate change.
|
|
309
|
+
|
|
310
|
+
- **The shipped PHI disclosure said the opposite of what the code did, in five places.**
|
|
311
|
+
`README.md`, `docs-content/troubleshooting.md`, `docs-content/spec-notes-tolerance.md`,
|
|
312
|
+
`docs-content/cookbook.md` and `KNOWN-LIMITATIONS.md` described warning messages as "bounded and
|
|
313
|
+
PHI-free by construction" and told consumers "you can log the full `.warnings` array without
|
|
314
|
+
leaking", naming `.snippet` as the one exception. The leak was in `.message`, and `.snippet` is not
|
|
315
|
+
a field on a warning at all, so the disclosure was actively green-lighting bulk logging of the
|
|
316
|
+
field that leaked. Each now describes the frozen registry, and `troubleshooting.md` states plainly
|
|
317
|
+
what `0.0.3` and earlier did so a reader on an older version is not misled.
|
|
318
|
+
- **The PHI tests were green over unreachable space.** `test/transactions-remit-835.test.ts` swept a
|
|
319
|
+
fixture whose CARC and RARC values were clean for shapes that could never have appeared there, and
|
|
320
|
+
its balance-mismatch assertion **required** the leak (it matched `/spec="\d/` and `/computed=/`).
|
|
321
|
+
`test/parser-envelope.test.ts` proved the hostile segment id was filtered and said nothing about
|
|
322
|
+
the spec-shaped one. All three are replaced.
|
|
323
|
+
|
|
324
|
+
### Added
|
|
325
|
+
|
|
12
326
|
- **Brand lockup on the README, following the reader's colour scheme (ASSETS-P8).** The README now
|
|
13
327
|
opens with the shared Cosyte lockup, above the `# @cosyte/x12` heading. It is a `<picture>`
|
|
14
328
|
element: a `<source>` carrying the on-dark cut for `prefers-color-scheme: dark`
|
|
@@ -540,9 +854,12 @@ EV/SS`), and HL-04 has-child flag from the nested input tree, so an
|
|
|
540
854
|
byte-faithful: reconstructed purely from the verbatim `.raw`
|
|
541
855
|
strings the parser preserved (ISA + terminator, then each
|
|
542
856
|
TA1 / GS / segment / GE / IEA terminator-joined, then any
|
|
543
|
-
`trailingBytes`), so for
|
|
544
|
-
|
|
545
|
-
|
|
857
|
+
`trailingBytes`), so for an input carrying none of the known
|
|
858
|
+
unrecorded constructs catalogued in `KNOWN-LIMITATIONS.md` it
|
|
859
|
+
reproduces the source bytes exactly. (As shipped, this
|
|
860
|
+
entry said "for a Tier-1 input", which later proved too weak: a
|
|
861
|
+
pretty-printed file is Tier 1 and does NOT reproduce exactly.)
|
|
862
|
+
With `{ specClean: true }` it ALSO
|
|
546
863
|
reconciles the envelope (SE-01 / GE-01 / IEA-01 counts + the
|
|
547
864
|
ISA-13↔IEA-02 / GS-06↔GE-02 / ST-02↔SE-02 control pairs),
|
|
548
865
|
surfacing every mismatch via `opts.onWarning` and NEVER silently
|
package/README.md
CHANGED
|
@@ -42,10 +42,10 @@ That's the pitch: no schema upload, no spec knowledge. The parser accepts vendor
|
|
|
42
42
|
## What's inside
|
|
43
43
|
|
|
44
44
|
- **Typed read + emit, per transaction**: 271, 277/277CA, 278, 820, 834, 835, 837P/I/D, plus 999/TA1 acknowledgments. Per-transaction helpers (`get835`, `get837Claims`, `get271Eligibility`, …) and a matching domain builder for each (`build835`, `build837P/I/D`, `build271`, …). The 270 and 276 inquiries parse as generic segments; they have no typed model yet.
|
|
45
|
-
- **Postel's Law**: a lenient parser (deviations → warnings with a stable code + positional context) and a conservative serializer. `serializeX12` is byte-faithful by default; `{ specClean: true }` reconciles the envelope, and `{ specClean: true, recomputeCounts: true }` also emits the corrected counts (`recomputeCounts` does nothing on its own). A mismatch is always warned, never silently corrected. Only **4 structural failures** are ever fatal.
|
|
45
|
+
- **Postel's Law**: a lenient parser (deviations → warnings with a stable code + positional context) and a conservative serializer. `serializeX12` is byte-faithful **for the segments on the model** by default: each comes back verbatim, including element padding, composites, and `?`-release escapes. **`serialize(parse(s)) === s` is not guaranteed in general.** Six constructs are known not to survive: line breaks between segments (any run of CR / LF between segments is absorbed at parse, so a pretty-printed or double-spaced file emits compact), a doubled terminator outside a transaction, a missing final terminator (the emit supplies one), post-IEA `trailingBytes` (re-joined, not verbatim), a TA1 that followed a functional group (emitted right after the ISA, so it is **reordered**, though nothing is lost), and a segment whose first element is empty outside a transaction (skipped entirely, with no warning at all). A **segment outside a transaction is not on that list**: it is reported as `X12_UNEXPECTED_SEGMENT`, kept on the model at `ix.orphanSegments` with a structural `anchor`, and re-emitted at that anchor, so the segment, its value and its warning all survive the round trip - placement is by the anchor and never by `segmentIndex`, which indexes the input stream the emit does not follow. The last five break the round trip on inputs with no line breaks at all, so do not treat "no line breaks" as sufficient; five of the six are silent, so a clean warnings list does not mean byte-exact either. Measured across this repo's 56 fixtures: every emit is a fixed point and re-parses to an identical model with an identical warning stream, the 14 with no line breaks return byte-identical, and the other 42 differ by line breaks and nothing else. See [Line endings between segments](./docs-content/spec-notes-envelope.md). `{ specClean: true }` reconciles the envelope, and `{ specClean: true, recomputeCounts: true }` also emits the corrected counts (`recomputeCounts` does nothing on its own). A mismatch is always warned, never silently corrected. Only **4 structural failures** are ever fatal.
|
|
46
46
|
- **Money is exact**: every monetary/percent/quantity field decodes as `X12Decimal` (string-backed, BigInt arithmetic). The library **never `parseFloat`s** an EDI amount.
|
|
47
47
|
- **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.
|
|
48
|
-
- **PHI-disciplined**: synthetic-only fixtures, a PHI commit-gate, warning
|
|
48
|
+
- **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 weaker, surface:** a `build*` function that refuses a spec carries structural locators and numeric totals in most cases, but at least sixteen refusal sites across ten builder modules interpolate a caller-supplied value into the thrown message verbatim and unbounded. Nine of them are the over-long control number, where the branch fires because the value is over-long; seven are not gated on length at all (`build999`'s ST-02 trace, `buildInterchange`'s transaction-set id, `build837`'s line variant, `build834`'s maintenance types, `buildTA1`'s note code). Measured: a 120,000-byte value gives a 120,155-byte `Error.message` from `build999`. Log `err.code`, not `err.message`, from a builder. Tracked 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).
|
|
49
49
|
|
|
50
50
|
See the [**Cookbook**](./docs-content/cookbook.md) for task-oriented recipes (post an 835, route 277CA rejections, round-trip a 271, walk an 837, read a 999) and [**KNOWN-LIMITATIONS.md**](./KNOWN-LIMITATIONS.md) for the honest do-not-over-trust list.
|
|
51
51
|
|