@cosyte/x12 0.0.4 → 0.0.5

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 CHANGED
@@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ### Added
11
11
 
12
+ - **`renderCallerValue`**, plus the `BUILD_REFUSAL_VALUE_MAX_LENGTH` (63) and
13
+ `BUILD_REFUSAL_VALUE_MAX_RENDERED` (90) bounds. This is the single sanctioned route a
14
+ caller-supplied value takes into a `build*` refusal message, and both ceilings are exported so a
15
+ consumer can assert them rather than take them on trust - the builder-side counterpart to
16
+ `ALL_WARNING_MESSAGES` on the parse side. `BUILD_REFUSAL_VALUE_MAX_RENDERED` bounds the rendered
17
+ **fragment**, not the whole message: a message is that plus the site's own fixed template text.
12
18
  - **`X12OrphanSegment.anchor`**, plus the `X12OrphanAnchor` / `X12OrphanAnchorKind` types. Every
13
19
  retained orphan now records **where it sat in the structure** rather than only where it sat in the
14
20
  byte stream: `{ kind: "interchange", groupIndex }` for a segment outside every functional group,
@@ -28,6 +34,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
28
34
 
29
35
  ### Fixed
30
36
 
37
+ - **A `build*` refusal message no longer grows with the value you passed in.** All twenty-three
38
+ caller-value slots across the ten builder modules route through `renderCallerValue`, capping the
39
+ rendered fragment at 90 characters. Nine are the `control number "…" exceeds the N-char spec limit`
40
+ refusal, where the branch fires _because_ the value is over-long; seven had no length gate at all
41
+ (`build999`'s ST-02 trace twice, `buildInterchange`'s transaction-set id, `build837`'s service-line
42
+ variant, `build834`'s INS-03 and HD-01 maintenance types, `buildTA1`'s note code); and seven are in
43
+ `build999`, found by adversarial review rather than by the census - the AK9-02 / AK9-03 / AK9-04
44
+ counts, typed `number` but reachable with a string from a `JSON.parse`d spec at 120,063 characters,
45
+ and three `.length` reads on caller-supplied arrays that a forged `{ length: … }` drove to 120,152.
46
+ Measured: a 120,000-character control number produced a **120,066-character**
47
+ `X12BuildError.message` from `buildInterchange` and now produces a **150-character** one. (150, not
48
+ 90: the 90 is the ceiling on the interpolated fragment, not on the message.) **This is robustness
49
+ and log hygiene, not redaction, and the docs now say so** - you passed the value in and still hold
50
+ it, so bounding it hides nothing from you; what it buys is a fixed ceiling on anything that reaches
51
+ a log line or a JSON error envelope. The surviving characters are bounded but **not escaped**, and
52
+ on the ack path the value is not always strictly your own (TR3 005010X231A1 has AK2-02 echo the
53
+ acknowledged ST-02 verbatim). `err.code` remains the thing to branch on.
54
+ - **A builder refusal handed a non-string where the types say `string` stays a typed error.**
55
+ `renderCallerValue` coerces rather than reading `.length` off whatever it is given, so a spec built
56
+ from `JSON.parse` that carries a numeric control number still raises `X12BuildError` with its
57
+ `code`, instead of an uncaught `TypeError` with none.
58
+ - **The 835 remit-total balance warning points at the BPR instead of at the ST.** Its
59
+ `position.segmentIndex` was a literal `0`, which reads like "no segment" but is not one:
60
+ `tx.segments[0]` is the `ST`, so a consumer resolving the position landed on a segment with nothing
61
+ to do with the invariant. It is now the BPR's own 1-based body index, so
62
+ `tx.segments[w.position.segmentIndex]` is the segment carrying the BPR-02 the equation compares
63
+ against, matching the treatment claim-level and service-line warnings already had. The only
64
+ remaining `0` is a transaction that carries no BPR at all. The corresponding position inside
65
+ `build835` stays synthetic and is now named and documented as such: the builder has no parsed
66
+ segment stream to index into, and it consumes only the warning's `message`, which is a
67
+ registry lookup keyed by the invariant and therefore position-independent.
31
68
  - **A segment outside a transaction now survives a round trip, and so does its warning.**
32
69
  `serializeX12` re-emits every entry of `ix.orphanSegments` at its structural `anchor`, so a
33
70
  consumer who serializes an interchange and re-derives warnings from the copy no longer loses the
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 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).
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. 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
 
package/dist/index.cjs CHANGED
@@ -1888,6 +1888,7 @@ function get835(delimiters, tx) {
1888
1888
  const warnings = [];
1889
1889
  const body = tx.se === void 0 ? tx.segments.slice(1) : tx.segments.slice(1, -1);
1890
1890
  let payment = EMPTY_HEADER;
1891
+ let bprSegmentIndex = 0;
1891
1892
  const traces = [];
1892
1893
  let payer;
1893
1894
  let payee;
@@ -1916,6 +1917,7 @@ function get835(delimiters, tx) {
1916
1917
  switch (seg.id) {
1917
1918
  case "BPR": {
1918
1919
  payment = decodeBpr(seg, delimiters);
1920
+ bprSegmentIndex = position.segmentIndex;
1919
1921
  break;
1920
1922
  }
1921
1923
  case "TRN": {
@@ -2083,7 +2085,7 @@ function get835(delimiters, tx) {
2083
2085
  payment.totalActualPayment,
2084
2086
  finalClaims,
2085
2087
  providerAdjustments,
2086
- { segmentIndex: 0, transactionIndex: 0 }
2088
+ { segmentIndex: bprSegmentIndex, transactionIndex: 0 }
2087
2089
  );
2088
2090
  if (remitTotalWarn !== void 0) warnings.push(remitTotalWarn);
2089
2091
  return Object.freeze({
@@ -2444,10 +2446,32 @@ var Remit835BuildError = class extends Error {
2444
2446
  }
2445
2447
  };
2446
2448
 
2449
+ // src/builder/caller-value.ts
2450
+ var BUILD_REFUSAL_VALUE_MAX_LENGTH = 63;
2451
+ var MAX_LENGTH_DIGITS = 10;
2452
+ var BUILD_REFUSAL_VALUE_MAX_RENDERED = BUILD_REFUSAL_VALUE_MAX_LENGTH + 2 + 1 + " ( characters)".length + MAX_LENGTH_DIGITS;
2453
+ function renderCallerValue(value) {
2454
+ const text = coerce(value);
2455
+ if (text.length <= BUILD_REFUSAL_VALUE_MAX_LENGTH) return `"${text}"`;
2456
+ return `"${text.slice(0, BUILD_REFUSAL_VALUE_MAX_LENGTH)}\u2026" (${String(text.length)} characters)`;
2457
+ }
2458
+ function coerce(value) {
2459
+ if (typeof value === "string") return value;
2460
+ try {
2461
+ return String(value);
2462
+ } catch {
2463
+ return "[unrenderable value]";
2464
+ }
2465
+ }
2466
+
2447
2467
  // src/transactions/remit/build-835.ts
2448
2468
  var X221A1_VERSION_RELEASE = "005010X221A1";
2449
2469
  var X12_835_FUNCTIONAL_ID = "HP";
2450
2470
  var X12_AGENCY_CODE = "X";
2471
+ var UNANCHORED_BUILD_POSITION = Object.freeze({
2472
+ segmentIndex: 0,
2473
+ transactionIndex: 0
2474
+ });
2451
2475
  function build835(spec) {
2452
2476
  const { envelope } = spec;
2453
2477
  enforceStructuralSpec(spec);
@@ -2621,7 +2645,7 @@ function enforceStructuralSpec(spec) {
2621
2645
  }
2622
2646
  }
2623
2647
  function enforceBalance(spec) {
2624
- const position = { segmentIndex: 0, transactionIndex: 0 };
2648
+ const position = UNANCHORED_BUILD_POSITION;
2625
2649
  const claims = spec.claims.map(materializeClaim);
2626
2650
  for (const claim of claims) {
2627
2651
  const claimWarn = checkClaimBalance(claim, position);
@@ -2927,7 +2951,7 @@ function padControl(value, width) {
2927
2951
  if (value.length < width) return "0".repeat(width - value.length) + value;
2928
2952
  throw new Remit835BuildError(
2929
2953
  REMIT_835_BUILD_ERROR_CODES.X12_835_BUILD_INVALID_SPEC,
2930
- `build835: control number "${value}" exceeds the ${String(width)}-char spec limit.`
2954
+ `build835: control number ${renderCallerValue(value)} exceeds the ${String(width)}-char spec limit.`
2931
2955
  );
2932
2956
  }
2933
2957
  function expandYY(yymmdd) {
@@ -4276,7 +4300,7 @@ function enforceClaim(variant, claim, locator) {
4276
4300
  if (line.variant !== variant) {
4277
4301
  throw new Claim837BuildError(
4278
4302
  CLAIM_837_BUILD_ERROR_CODES.X12_837_BUILD_INVALID_SPEC,
4279
- `build837${variant}: claim at ${locator} has a "${line.variant}" service line at index ${String(l)}; every line must be "${variant}".`
4303
+ `build837${variant}: claim at ${locator} has a ${renderCallerValue(line.variant)} service line at index ${String(l)}; every line must be "${variant}".`
4280
4304
  );
4281
4305
  }
4282
4306
  const code = line.variant === "I" ? line.revenueCode : line.procedureCode;
@@ -4620,7 +4644,7 @@ function padControl2(value, width) {
4620
4644
  if (value.length < width) return "0".repeat(width - value.length) + value;
4621
4645
  throw new Claim837BuildError(
4622
4646
  CLAIM_837_BUILD_ERROR_CODES.X12_837_BUILD_INVALID_SPEC,
4623
- `build837: control number "${value}" exceeds the ${String(width)}-char spec limit.`
4647
+ `build837: control number ${renderCallerValue(value)} exceeds the ${String(width)}-char spec limit.`
4624
4648
  );
4625
4649
  }
4626
4650
  function expandYY2(yymmdd) {
@@ -5485,7 +5509,7 @@ function padControl3(value, width) {
5485
5509
  if (value.length < width) return "0".repeat(width - value.length) + value;
5486
5510
  throw new Eligibility271BuildError(
5487
5511
  ELIGIBILITY_271_BUILD_ERROR_CODES.X12_271_BUILD_INVALID_SPEC,
5488
- `build271: control number "${value}" exceeds the ${String(width)}-char spec limit.`
5512
+ `build271: control number ${renderCallerValue(value)} exceeds the ${String(width)}-char spec limit.`
5489
5513
  );
5490
5514
  }
5491
5515
  function expandYY3(yymmdd) {
@@ -6259,7 +6283,7 @@ function padControl4(value, width) {
6259
6283
  if (value.length < width) return "0".repeat(width - value.length) + value;
6260
6284
  throw new ClaimStatus277BuildError(
6261
6285
  CLAIM_STATUS_277_BUILD_ERROR_CODES.X12_277_BUILD_INVALID_SPEC,
6262
- `build277: control number "${value}" exceeds the ${String(width)}-char spec limit.`
6286
+ `build277: control number ${renderCallerValue(value)} exceeds the ${String(width)}-char spec limit.`
6263
6287
  );
6264
6288
  }
6265
6289
  function expandYY4(yymmdd) {
@@ -6916,7 +6940,7 @@ function padControl5(value, width) {
6916
6940
  if (value.length < width) return "0".repeat(width - value.length) + value;
6917
6941
  throw new Premium820BuildError(
6918
6942
  PREMIUM_820_BUILD_ERROR_CODES.X12_820_BUILD_INVALID_SPEC,
6919
- `build820: control number "${value}" exceeds the ${String(width)}-char spec limit.`
6943
+ `build820: control number ${renderCallerValue(value)} exceeds the ${String(width)}-char spec limit.`
6920
6944
  );
6921
6945
  }
6922
6946
  function expandYY5(yymmdd) {
@@ -7431,7 +7455,7 @@ function enforceStructuralSpec6(spec) {
7431
7455
  if (lookupMaintenanceType(member.maintenanceTypeCode) === void 0) {
7432
7456
  throw new Enrollment834BuildError(
7433
7457
  ENROLLMENT_834_BUILD_ERROR_CODES.X12_834_BUILD_UNKNOWN_MAINTENANCE_TYPE,
7434
- `build834: member at index ${String(m)} has an unknown INS-03 maintenance type code "${member.maintenanceTypeCode}" (outside X12 Code Source 875).`
7458
+ `build834: member at index ${String(m)} has an unknown INS-03 maintenance type code ${renderCallerValue(member.maintenanceTypeCode)} (outside X12 Code Source 875).`
7435
7459
  );
7436
7460
  }
7437
7461
  for (let c = 0; c < (member.healthCoverages ?? []).length; c += 1) {
@@ -7441,7 +7465,7 @@ function enforceStructuralSpec6(spec) {
7441
7465
  if (hd01 !== void 0 && hd01 !== "" && lookupMaintenanceType(hd01) === void 0) {
7442
7466
  throw new Enrollment834BuildError(
7443
7467
  ENROLLMENT_834_BUILD_ERROR_CODES.X12_834_BUILD_UNKNOWN_MAINTENANCE_TYPE,
7444
- `build834: coverage at member[${String(m)}].healthCoverages[${String(c)}] has an unknown HD-01 maintenance type code "${hd01}" (outside X12 Code Source 875).`
7468
+ `build834: coverage at member[${String(m)}].healthCoverages[${String(c)}] has an unknown HD-01 maintenance type code ${renderCallerValue(hd01)} (outside X12 Code Source 875).`
7445
7469
  );
7446
7470
  }
7447
7471
  }
@@ -7590,7 +7614,7 @@ function padControl6(value, width) {
7590
7614
  if (value.length < width) return "0".repeat(width - value.length) + value;
7591
7615
  throw new Enrollment834BuildError(
7592
7616
  ENROLLMENT_834_BUILD_ERROR_CODES.X12_834_BUILD_INVALID_SPEC,
7593
- `build834: control number "${value}" exceeds the ${String(width)}-char spec limit.`
7617
+ `build834: control number ${renderCallerValue(value)} exceeds the ${String(width)}-char spec limit.`
7594
7618
  );
7595
7619
  }
7596
7620
  function expandYY6(yymmdd) {
@@ -8246,7 +8270,7 @@ function padControl7(value, width) {
8246
8270
  if (value.length < width) return "0".repeat(width - value.length) + value;
8247
8271
  throw new ServicesReview278BuildError(
8248
8272
  AUTH_278_BUILD_ERROR_CODES.X12_278_BUILD_INVALID_SPEC,
8249
- `build278: control number "${value}" exceeds the ${String(width)}-char spec limit.`
8273
+ `build278: control number ${renderCallerValue(value)} exceeds the ${String(width)}-char spec limit.`
8250
8274
  );
8251
8275
  }
8252
8276
  function expandYY7(yymmdd) {
@@ -8848,7 +8872,7 @@ function enforceAcceptIsClean(group) {
8848
8872
  if (functionalIsAccept && functionalHasErrorCodes) {
8849
8873
  throw new AckBuildError(
8850
8874
  ACK_BUILD_ERROR_CODES.X12_ACK_ACCEPT_WITH_ERRORS,
8851
- `build999: AK9-01 was 'A' (Accept) but AK9-05..AK9-09 carried ${String(
8875
+ `build999: AK9-01 was 'A' (Accept) but AK9-05..AK9-09 carried ${renderCallerValue(
8852
8876
  (group.syntaxErrorCodes ?? []).length
8853
8877
  )} syntax error code(s). Use 'E' (Accept, with errors noted) or a reject disposition instead.`
8854
8878
  );
@@ -8859,13 +8883,13 @@ function enforceAcceptIsClean(group) {
8859
8883
  if (responseIsAccept && responseHasErrors) {
8860
8884
  throw new AckBuildError(
8861
8885
  ACK_BUILD_ERROR_CODES.X12_ACK_ACCEPT_WITH_ERRORS,
8862
- `build999: AK2 (ST-02 ${response.transactionSetControlNumber}) IK5-01 was 'A' (Accept) but the response carried error payload. Use 'E' (Accept, with errors noted) or a reject disposition instead.`
8886
+ `build999: AK2 (ST-02 ${renderCallerValue(response.transactionSetControlNumber)}) IK5-01 was 'A' (Accept) but the response carried error payload. Use 'E' (Accept, with errors noted) or a reject disposition instead.`
8863
8887
  );
8864
8888
  }
8865
8889
  if (functionalIsAccept && (!responseIsAccept || responseHasErrors)) {
8866
8890
  throw new AckBuildError(
8867
8891
  ACK_BUILD_ERROR_CODES.X12_ACK_ACCEPT_WITH_ERRORS,
8868
- `build999: AK9-01 was 'A' (Accept) but transaction response (ST-02 ${response.transactionSetControlNumber}) reported a non-accept disposition or carried errors. Lift the functional disposition to 'P' (Partial), 'E' (Accept, errors noted), or a reject.`
8892
+ `build999: AK9-01 was 'A' (Accept) but transaction response (ST-02 ${renderCallerValue(response.transactionSetControlNumber)}) reported a non-accept disposition or carried errors. Lift the functional disposition to 'P' (Partial), 'E' (Accept, errors noted), or a reject.`
8869
8893
  );
8870
8894
  }
8871
8895
  }
@@ -8877,25 +8901,25 @@ function enforceCountInvariants(group) {
8877
8901
  if (declared < 0 || received < 0 || accepted < 0) {
8878
8902
  throw new AckBuildError(
8879
8903
  ACK_BUILD_ERROR_CODES.X12_ACK_COUNT_MISMATCH,
8880
- `build999: AK9 counts must be non-negative (declared=${String(declared)}, received=${String(received)}, accepted=${String(accepted)}).`
8904
+ `build999: AK9 counts must be non-negative (declared=${renderCallerValue(declared)}, received=${renderCallerValue(received)}, accepted=${renderCallerValue(accepted)}).`
8881
8905
  );
8882
8906
  }
8883
8907
  if (accepted > received) {
8884
8908
  throw new AckBuildError(
8885
8909
  ACK_BUILD_ERROR_CODES.X12_ACK_COUNT_MISMATCH,
8886
- `build999: AK9-04 accepted (${String(accepted)}) cannot exceed AK9-03 received (${String(received)}).`
8910
+ `build999: AK9-04 accepted (${renderCallerValue(accepted)}) cannot exceed AK9-03 received (${renderCallerValue(received)}).`
8887
8911
  );
8888
8912
  }
8889
8913
  if (received > declared) {
8890
8914
  throw new AckBuildError(
8891
8915
  ACK_BUILD_ERROR_CODES.X12_ACK_COUNT_MISMATCH,
8892
- `build999: AK9-03 received (${String(received)}) cannot exceed AK9-02 declared (${String(declared)}).`
8916
+ `build999: AK9-03 received (${renderCallerValue(received)}) cannot exceed AK9-02 declared (${renderCallerValue(declared)}).`
8893
8917
  );
8894
8918
  }
8895
8919
  if (group.transactionResponses.length !== received) {
8896
8920
  throw new AckBuildError(
8897
8921
  ACK_BUILD_ERROR_CODES.X12_ACK_COUNT_MISMATCH,
8898
- `build999: AK9-03 received (${String(received)}) must equal the number of supplied transaction responses (${String(group.transactionResponses.length)}).`
8922
+ `build999: AK9-03 received (${renderCallerValue(received)}) must equal the number of supplied transaction responses (${renderCallerValue(group.transactionResponses.length)}).`
8899
8923
  );
8900
8924
  }
8901
8925
  }
@@ -8964,7 +8988,7 @@ function buildIk5(response, esc, elementSeparator, segmentTerminator) {
8964
8988
  if (codes.length > 5) {
8965
8989
  throw new AckBuildError(
8966
8990
  ACK_BUILD_ERROR_CODES.X12_ACK_COUNT_MISMATCH,
8967
- `build999: IK5 accepts up to 5 syntax error codes (IK5-02..IK5-06); ${String(codes.length)} supplied.`
8991
+ `build999: IK5 accepts up to 5 syntax error codes (IK5-02..IK5-06); ${renderCallerValue(codes.length)} supplied.`
8968
8992
  );
8969
8993
  }
8970
8994
  for (const code of codes) parts.push(esc(code));
@@ -8982,7 +9006,7 @@ function buildAk9(group, esc, elementSeparator, segmentTerminator) {
8982
9006
  if (codes.length > 5) {
8983
9007
  throw new AckBuildError(
8984
9008
  ACK_BUILD_ERROR_CODES.X12_ACK_COUNT_MISMATCH,
8985
- `build999: AK9 accepts up to 5 syntax error codes (AK9-05..AK9-09); ${String(codes.length)} supplied.`
9009
+ `build999: AK9 accepts up to 5 syntax error codes (AK9-05..AK9-09); ${renderCallerValue(codes.length)} supplied.`
8986
9010
  );
8987
9011
  }
8988
9012
  for (const code of codes) parts.push(esc(code));
@@ -9001,7 +9025,7 @@ function padControl8(value, width) {
9001
9025
  if (value.length < width) return "0".repeat(width - value.length) + value;
9002
9026
  throw new AckBuildError(
9003
9027
  ACK_BUILD_ERROR_CODES.X12_ACK_INVALID_SPEC,
9004
- `build999: control number "${value}" exceeds the ${String(width)}-char spec limit.`
9028
+ `build999: control number ${renderCallerValue(value)} exceeds the ${String(width)}-char spec limit.`
9005
9029
  );
9006
9030
  }
9007
9031
  function expandYY8(yymmdd) {
@@ -9031,7 +9055,7 @@ function enforceAcceptIsClean2(spec) {
9031
9055
  if (spec.ackCode === TA1_ACK_CODES.A && spec.noteCode !== "000") {
9032
9056
  throw new AckBuildError(
9033
9057
  ACK_BUILD_ERROR_CODES.X12_TA1_ACCEPT_WITH_NOTE,
9034
- `buildTA1: TA1-04 was "A" (Accept) but TA1-05 carried note "${spec.noteCode}". An accept must cite "000" (no error). Use ackCode "E" (accept, errors noted) when the inbound had defects you elected to ignore.`
9058
+ `buildTA1: TA1-04 was "A" (Accept) but TA1-05 carried note ${renderCallerValue(spec.noteCode)}. An accept must cite "000" (no error). Use ackCode "E" (accept, errors noted) when the inbound had defects you elected to ignore.`
9035
9059
  );
9036
9060
  }
9037
9061
  }
@@ -9322,7 +9346,11 @@ function buildTransaction(tx, esc, elementSeparator, segmentTerminator) {
9322
9346
  if (segment.length === 0 || (segment[0] ?? "") === "") {
9323
9347
  throw new X12BuildError(
9324
9348
  X12_BUILD_ERROR_CODES.X12_BUILD_INVALID_SPEC,
9325
- `buildInterchange: a segment spec in transaction "${esc(tx.transactionSetIdCode)}" has no segment id.`
9349
+ // Rendered from the RAW id, not the `?`-escaped one. `esc` can double
9350
+ // the length, so escaping first made the reported "(N characters)" the
9351
+ // escaped length rather than the caller's: a 100-character all-`?` id
9352
+ // reported 200. The escape exists for the wire, and this is a message.
9353
+ `buildInterchange: a segment spec in transaction ${renderCallerValue(tx.transactionSetIdCode)} has no segment id.`
9326
9354
  );
9327
9355
  }
9328
9356
  bodySegments += joinSeg2(segment.map(esc), elementSeparator, segmentTerminator);
@@ -9348,7 +9376,7 @@ function padControl9(value, width) {
9348
9376
  if (value.length < width) return "0".repeat(width - value.length) + value;
9349
9377
  throw new X12BuildError(
9350
9378
  X12_BUILD_ERROR_CODES.X12_BUILD_INVALID_SPEC,
9351
- `buildInterchange: control number "${value}" exceeds the ${String(width)}-char spec limit.`
9379
+ `buildInterchange: control number ${renderCallerValue(value)} exceeds the ${String(width)}-char spec limit.`
9352
9380
  );
9353
9381
  }
9354
9382
  function expandYY9(yymmdd) {
@@ -9642,7 +9670,7 @@ var profiles = Object.freeze({
9642
9670
  });
9643
9671
 
9644
9672
  // src/index.ts
9645
- var VERSION = "0.0.4";
9673
+ var VERSION = "0.0.5";
9646
9674
 
9647
9675
  exports.ACK_BUILD_ERROR_CODES = ACK_BUILD_ERROR_CODES;
9648
9676
  exports.ALL_WARNING_MESSAGES = ALL_WARNING_MESSAGES;
@@ -9655,6 +9683,8 @@ exports.AUTH_278_LOOP_2000E = AUTH_278_LOOP_2000E;
9655
9683
  exports.AUTH_278_LOOP_2000F = AUTH_278_LOOP_2000F;
9656
9684
  exports.AckBuildError = AckBuildError;
9657
9685
  exports.BALANCE_INVARIANTS = BALANCE_INVARIANTS;
9686
+ exports.BUILD_REFUSAL_VALUE_MAX_LENGTH = BUILD_REFUSAL_VALUE_MAX_LENGTH;
9687
+ exports.BUILD_REFUSAL_VALUE_MAX_RENDERED = BUILD_REFUSAL_VALUE_MAX_RENDERED;
9658
9688
  exports.CARC = CARC;
9659
9689
  exports.CLAIM_837D_LOOP_2000A = CLAIM_837D_LOOP_2000A;
9660
9690
  exports.CLAIM_837D_LOOP_2300 = CLAIM_837D_LOOP_2300;
@@ -9816,6 +9846,7 @@ exports.partitionWarnings = partitionWarnings;
9816
9846
  exports.pre005010 = pre005010;
9817
9847
  exports.profiles = profiles;
9818
9848
  exports.remitBalanceMismatch = remitBalanceMismatch;
9849
+ exports.renderCallerValue = renderCallerValue;
9819
9850
  exports.resolveHiQualifier = resolveHiQualifier;
9820
9851
  exports.segmentCountMismatch = segmentCountMismatch;
9821
9852
  exports.serializeX12 = serializeX12;