@cosyte/x12 0.0.5 → 0.0.6
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 +67 -0
- package/README.md +1 -1
- package/dist/index.cjs +304 -82
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -4
- package/dist/index.d.ts +19 -4
- package/dist/index.mjs +304 -82
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
11
|
|
|
12
|
+
- **`renderCallerJson`** (internal), the type-preserving half of the caller-value bound, held to the
|
|
13
|
+
same `BUILD_REFUSAL_VALUE_MAX_RENDERED` ceiling as `renderCallerValue`. It exists because
|
|
14
|
+
`defineProfile()` reports a bad `name` / `id` / `effect` / `fixture` with `JSON.stringify`, and that
|
|
15
|
+
distinction is diagnostically load-bearing: `null` and `"null"` are different mistakes, and a
|
|
16
|
+
coercing renderer would flatten them together. It bounds the JSON **text** rather than the argument,
|
|
17
|
+
never throws (a circular structure, a `BigInt`, a hostile `toJSON`), and fabricates no closing quote,
|
|
18
|
+
because JSON does not always open one.
|
|
19
|
+
- **`requireCallerArray`** (internal), the single route a caller-supplied array takes into a builder
|
|
20
|
+
loop. Each builder passes its own `refuse` callback, so a forged list draws that module's existing
|
|
21
|
+
typed error and code rather than a new shared one.
|
|
12
22
|
- **`renderCallerValue`**, plus the `BUILD_REFUSAL_VALUE_MAX_LENGTH` (63) and
|
|
13
23
|
`BUILD_REFUSAL_VALUE_MAX_RENDERED` (90) bounds. This is the single sanctioned route a
|
|
14
24
|
caller-supplied value takes into a `build*` refusal message, and both ceilings are exported so a
|
|
@@ -34,6 +44,63 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
34
44
|
|
|
35
45
|
### Fixed
|
|
36
46
|
|
|
47
|
+
- **A `defineProfile()` refusal message no longer grows with the value you passed in.** Twelve
|
|
48
|
+
refusal sites in `src/profiles/validate.ts` hold twenty-three caller-value holes between them, and
|
|
49
|
+
every one now routes through `renderCallerValue` or `renderCallerJson`. Re-derived on this tree
|
|
50
|
+
before the fix, driving the same thirteen cases the suite ships: the worst `X12ProfileError.message`
|
|
51
|
+
was **360,181 characters**, at the `fixture` refusal. Three of the thirteen exceed 360,000, and they
|
|
52
|
+
are the three that name **three** caller values (profile name + quirk id + a `JSON.stringify`d
|
|
53
|
+
value) rather than two; a 120,000-digit quirk id reaches them because `QUIRK_ID_RE` carries no
|
|
54
|
+
length bound. The same `fixture` refusal now measures **431 characters**.
|
|
55
|
+
|
|
56
|
+
**431 is a measurement, not a maximum.** The ` (N characters)` suffix widens with the decimal width
|
|
57
|
+
of the value's length, so the same refusal measures 434 at a 1,000,000-character value and 437 at
|
|
58
|
+
10,000,000. The site's ceiling, derived from its fixed text plus its three fragment ceilings, is
|
|
59
|
+
**443**; the suite asserts every one of the twelve under 500.
|
|
60
|
+
|
|
61
|
+
**The figure this was filed on, 120,093, did not reproduce**, the same way `X12-BUILDER-BOUNDS`'s
|
|
62
|
+
own filed figures did not: it depends on which site is hit and what the probe passes.
|
|
63
|
+
|
|
64
|
+
Scope it the way the builder half is scoped. This is **not** `PHI-WARNING-MESSAGE-LEAK`, where the
|
|
65
|
+
value was the document's: here you passed it in and still hold it, so bounding it **redacts
|
|
66
|
+
nothing**. What it buys is a fixed ceiling on anything reaching a log line, a crash report or a JSON
|
|
67
|
+
error envelope. The surviving characters are **not escaped**, and the bound is on UTF-16 **code
|
|
68
|
+
units, not bytes**. **`X12ProfileError.profileName` is deliberately left unbounded**, so it still
|
|
69
|
+
matches the name you passed.
|
|
70
|
+
|
|
71
|
+
- **A builder handed a forged non-array now refuses instead of hanging.** Every domain builder took
|
|
72
|
+
its loop bound from a caller-supplied `.length`, so `{ length: "9".repeat(120000) }` coerced to
|
|
73
|
+
`Infinity`, every element read `undefined`, every guard `continue`d, and the builder **spun forever
|
|
74
|
+
rather than refusing**. Measured at base over the nineteen probes that drive a forged list at a
|
|
75
|
+
builder ENTRY point (17 `FORGED_ARRAY_CASES` + 2 `RESIDUAL_CASES`), each in a child process under a
|
|
76
|
+
20-second wall-clock timeout because a hang cannot be observed in-process: **16 of 19 hung** with no
|
|
77
|
+
refusal and the other **3 threw an untyped `TypeError`**. The suite ships three further forged
|
|
78
|
+
probes on optional LEAF arrays, which are `TypeError` at base and at head alike and move nothing:
|
|
79
|
+
counting all 22, base is 16 hung / 6 untyped and head is 17 typed / 5 untyped. All **32 indexed
|
|
80
|
+
loops across 7 builder modules** now take their bound from a `requireCallerArray` binding, and at
|
|
81
|
+
head the same nineteen give **17 typed, code-tagged refusals** (messages 169 to 194 characters) and
|
|
82
|
+
**2 untyped `TypeError`s**. `build835`'s `spec.traces` is the one that moved from the untyped group
|
|
83
|
+
to the typed one, because its guard reads the list; it never hung, and this changelog does not claim
|
|
84
|
+
it did.
|
|
85
|
+
|
|
86
|
+
A hang is a worse failure than a refusal: a refusal hands control back with something to branch on,
|
|
87
|
+
a hang takes the worker with it. But state the class correctly. This is a **forged non-array input,
|
|
88
|
+
not a mis-read clinical value** - nothing decodes a document differently because of it, and the
|
|
89
|
+
reachable harm is availability. It is unreachable from TypeScript and reachable from JavaScript,
|
|
90
|
+
JSON, and therefore `@cosyte/cli`.
|
|
91
|
+
|
|
92
|
+
**Disclosed rather than fixed, and identical at base and head:** where a builder reads a caller array
|
|
93
|
+
with `for...of` - `buildInterchange`'s `spec.groups`, `build999`'s
|
|
94
|
+
`functionalGroup.transactionResponses`, and every optional leaf array such as `claim.dates` - a
|
|
95
|
+
forged list throws `TypeError: ... is not iterable`. That terminates, so it is not the hang, but it
|
|
96
|
+
carries **no `code`**. Pinned by a test so it cannot quietly become a hang.
|
|
97
|
+
|
|
98
|
+
- **The `QUIRK_ID_RE` comment claimed a length bound the pattern never had.** It said "2-64
|
|
99
|
+
lowercase-alphanumeric chars"; the regex accepts one character and it accepts 120,000, which was in
|
|
100
|
+
fact the path to the largest profile error message on the tree. The comment was corrected to the
|
|
101
|
+
code, not the grammar tightened: rejecting ids that define cleanly today is a separate decision from
|
|
102
|
+
bounding a message.
|
|
103
|
+
|
|
37
104
|
- **A `build*` refusal message no longer grows with the value you passed in.** All twenty-three
|
|
38
105
|
caller-value slots across the ten builder modules route through `renderCallerValue`, capping the
|
|
39
106
|
rendered fragment at 90 characters. Nine are the `control number "…" exceeds the N-char spec limit`
|
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ That's the pitch: no schema upload, no spec knowledge. The parser accepts vendor
|
|
|
45
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, 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-three refusal sites across ten builder modules name a value you passed in, so you can see which control number, count or code was refused. 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.
|
|
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 deliberately weaker, surface:** twenty-three refusal sites across ten builder modules name a value you passed in, so you can see which control number, count or code was refused. 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).
|
|
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
|
|