@cosyte/x12 0.0.8 → 0.0.9

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,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ### Added
11
11
 
12
+ - **`requireCallerString` / `makeCallerEscaper`** (internal), the single route a caller-supplied
13
+ element value takes **through a builder's `esc` helper**. Read that scope literally: it is not
14
+ every route into an emitted segment, and the `### Fixed` entry below says which positions bypass
15
+ it. All nine builders now build their `esc` helper through
16
+ `makeCallerEscaper`, which type-checks the value before escaping and refuses through the calling
17
+ module's own `refuse` callback, so a wrong-typed element draws that builder's existing typed error
18
+ and code rather than a new shared one. `buildInterchange`, `build999`, `build271` and `build278`
19
+ each gained the one-line `refuseSpec` thrower they needed for it.
20
+
12
21
  - **`renderCallerJson`** (internal), the type-preserving half of the caller-value bound, held to the
13
22
  same `BUILD_REFUSAL_VALUE_MAX_RENDERED` ceiling as `renderCallerValue`. It exists because
14
23
  `defineProfile()` reports a bad `name` / `id` / `effect` / `fixture` with `JSON.stringify`, and that
@@ -42,6 +51,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
42
51
  a warning `message` is a frozen-registry lookup with positional metadata only, but an orphan is
43
52
  document content, verbatim, exactly like `tx.rawSegments`. Log `context` and `segmentIndex`.
44
53
 
54
+ ### Changed
55
+
56
+ - **`escapeRelease` now throws `TypeError` on a non-string instead of returning `""`.** Previously it
57
+ gave three different wrong answers depending on what arrived: a number, a boolean or a plain object
58
+ returned the empty string silently; `null` and `undefined` threw on the property read; an array or
59
+ an array-like threw on `charAt`. All three now terminate the same way. A `TypeError` rather than a
60
+ code-tagged library error is deliberate: it is a pure text utility with no spec, element or caller
61
+ context to name. Nothing inside the library can reach it, because all nine builders refuse
62
+ first with their own typed, code-tagged error.
63
+
45
64
  ### Security
46
65
 
47
66
  - **The PHI scanner refuses an in-scope entry that is not a regular file, on both of its enumerating
@@ -111,6 +130,70 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
111
130
 
112
131
  ### Fixed
113
132
 
133
+ - **A number passed where a builder's types say `string` no longer emits an EMPTY element. It is now
134
+ REFUSED, and it is deliberately not coerced.** `escapeRelease` opened with
135
+ `if (value.length === 0) return value;` and then looped to `value.length`. On a number `.length` is
136
+ `undefined`, so the early return did not fire, `i < undefined` was false, the loop body never ran,
137
+ and the function returned its empty accumulator. **The value vanished with no warning and no
138
+ error.** The types say `string`, so a TypeScript caller could not reach it; a JavaScript or
139
+ JSON-driven caller could, and `@cosyte/cli` is such a caller.
140
+
141
+ Measured at `0.0.8` on an otherwise valid `build835` spec with only `patientControlNumber` changed:
142
+ `CLP**1*500.00*450.00*50.00*MB*ICN-9001*11::1`, `ix.warnings.length === 0`, and a frozen interchange
143
+ that looks successful. CLP-01 is required by TR3 005010X221A1 Loop 2100 and is the key that
144
+ reassociates the remittance back to the 837's CLM-01. The builder's own
145
+ `patientControlNumber === ""` guard did not catch it, because the value was not yet a string when it
146
+ was checked.
147
+
148
+ **The same one line reached every escaped slot in all nine builders**, including the 837's own
149
+ CLM-01, the other end of that reassociation link. Measured at base by driving the shipped table
150
+ against a `143a6ea` worktree, one element each: `BPR*A1*450.00` became `BPR**450.00`,
151
+ `AK2*837*A1*005010X222A2` became `AK2*837**005010X222A2`, `NM1*IL*1*DOE*JANE****34*A1` became
152
+ `NM1*IL*1*DOE*JANE****34`, `ENT**2J*34*A1` became `ENT**2J*34`, `NM1*1P*2*A1` became `NM1*1P*2`,
153
+ `UM*HS*I*A1` became `UM*HS*I`, and `CLM*A1*150.00***11:B:1*Y*A*Y*Y` became
154
+ `CLM**150.00***11:B:1*Y*A*Y*Y`. Where the dropped element was trailing, the trailing-empty trim
155
+ removed it outright, so it is not even positionally recoverable.
156
+
157
+ **It refuses rather than coercing, and that choice is the substance of the fix.** Coercion would
158
+ mint a _different_ identifier: a JSON payload that carried `"0012345"` as a number has already lost
159
+ the leading zeros, so `String(12345)` emits a well-formed identifier that is not the one the caller
160
+ sent, and a remittance that reassociates to the wrong claim is worse than one that fails to
161
+ reassociate at all. `String(1e21)` is `"1e+21"`, `String(NaN)` is `"NaN"` and `String(0.1 + 0.2)` is
162
+ `"0.30000000000000004"`, none valid in an `AN`, `ID` or `Nn` element, and `X12Decimal` is already
163
+ the sanctioned route for numeric content. No working caller is broken, because the numeric path did
164
+ not work; it silently lost the field. The refusal message says why, so a caller is not nudged
165
+ straight into `String(value)` at the wrong boundary.
166
+
167
+ This is deliberately the OPPOSITE answer to `renderCallerValue`, which coerces for the same caller
168
+ mistake. A refusal message that throws replaces a typed, code-tagged error with an uncaught
169
+ `TypeError`, so its duty is to survive anything; an emitted document's duty is to invent nothing.
170
+
171
+ **The guard is on values routed through the escape helper, and not every element position goes
172
+ through it.** All of what follows is pre-existing, measured, unchanged here and disclosed in
173
+ `KNOWN-LIMITATIONS.md`. **36 `esc` slots read `.toString()` off what the types say is an
174
+ `X12Decimal`**, so a raw number arrives already a string and is passed through: a
175
+ `patientResponsibilityAmount` of `0.1 + 0.2` still emits `…*0.30000000000000004*…`, `1e21` still
176
+ emits `…*1e+21*…` and `NaN` still emits `…*NaN*…`, each with zero warnings, which are the exact
177
+ three renderings this entry names as disqualifying, and an 837 service-line `units` reaches SV1-04
178
+ the same way. **That is a set of examples too, not a count** - a draft of this entry said the class
179
+ was exactly 36 slots and closed, and adversarial review measured it open. **Some string-typed
180
+ positions never call the escape helper at all** and emit a
181
+ number, or an unescaped delimiter, verbatim: the 999's `groupControlNumber` (GS-06 / GE-02),
182
+ `transactionSetControlNumber` (ST-02 / SE-02) and `disposition` (AK9-01 and IK5-01), the 278's
183
+ `levelCode` (HL-03), `groupDate` / `groupTime` (GS-04 / GS-05), and the 837's `lineNumber`
184
+ (LX-01). **Those are examples and not a census, deliberately:** two drafts of this
185
+ entry published an exhaustive count and adversarial review measured both incomplete, so the claim
186
+ is cut back rather than grown a third time. AK9-01 is an `ID` element bound to X12 code source 715,
187
+ and `build999`'s own accept-with-errors guard compares it against `"A"`, so a number walks past it
188
+ the same way it walked past `patientControlNumber === ""`. **The fixed-width ISA slots** go through
189
+ `pad` / `padControl`, so a number throws an untyped `TypeError` and a numeric
190
+ `interchangeControlNumber` throws a typed refusal whose text misleadingly says "exceeds the 9-char
191
+ spec limit"; `buildTA1` has no escape helper at all.
192
+
193
+ **The "no working caller is broken" claim holds with one measured exception:** a boxed
194
+ `new String("PT-ACCT-001")` built cleanly at `0.0.8` and is refused now, because `typeof` it is
195
+ `"object"`.
196
+
114
197
  - **The `attw` publish gate no longer passes a tarball that carries no type declarations.** The
115
198
  `attw` script was the bare CLI, and `@arethetypeswrong/cli` returns 0 whenever its analysis found
116
199
  no types at all, before it reads the problem list. For a package that ships types, "This package
@@ -365,6 +448,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
365
448
 
366
449
  ### Tests
367
450
 
451
+ - **`test/builder-string-type.test.ts`**, the emit gate for the fix above and the third member of the
452
+ builder-gate family (`builder-refusal-bounds` guards what a refusal says, `builder-array-bounds`
453
+ guards whether a refusal happens, this one guards whether the caller's value reaches the document).
454
+ The source scan is the exhaustive half: it requires every builder module's `esc` to be built by
455
+ `makeCallerEscaper` and requires no builder module to call `escapeRelease` itself, so a tenth
456
+ builder writing the old one-liner reds it without anyone adding a case. Negative-controlled by
457
+ putting the defect back: reverting one module's `esc` reds the scan by file and line **and** reds
458
+ the behavioural half, which is a cleaner control than the array gate gets, since there is no loop
459
+ here to wedge the runner.
460
+
368
461
  - **The PHI-scanner suite stopped paying 30 of its 32 `tsx` start-ups, and the global `testTimeout`
369
462
  now says what it does and does not cover.** No library code, no public surface and no timeout value
370
463
  changed; `scripts/phi-scan.ts` and `scripts/attw.mjs` are untouched.
package/dist/index.cjs CHANGED
@@ -426,6 +426,9 @@ function unescapeRelease(input, delimiters, emit, position) {
426
426
  return out;
427
427
  }
428
428
  function escapeRelease(value, delimiters) {
429
+ if (typeof value !== "string") {
430
+ throw new TypeError(`escapeRelease: value must be a string, received ${typeof value}.`);
431
+ }
429
432
  if (value.length === 0) return value;
430
433
  let out = "";
431
434
  for (let i = 0; i < value.length; i++) {
@@ -2508,6 +2511,25 @@ function isRealArray(value) {
2508
2511
  return Array.isArray(value);
2509
2512
  }
2510
2513
 
2514
+ // src/builder/caller-string.ts
2515
+ function describeCallerValue(value) {
2516
+ if (value === null) return "null";
2517
+ if (value === void 0) return "undefined";
2518
+ const type = typeof value;
2519
+ if (type === "object") return Array.isArray(value) ? "an array" : "an object";
2520
+ if (type === "function") return "a function";
2521
+ return `a ${type} (${renderCallerValue(value)})`;
2522
+ }
2523
+ function requireCallerString(value, at, refuse) {
2524
+ if (typeof value === "string") return value;
2525
+ refuse(
2526
+ `${at}: every element value must be a string, but received ${describeCallerValue(value)}. Values are never coerced - a JavaScript number cannot carry a leading-zero identifier, so converting here could emit a different value than the caller sent. Convert at the call site.`
2527
+ );
2528
+ }
2529
+ function makeCallerEscaper(delimiters, at, refuse) {
2530
+ return (value) => escapeRelease(requireCallerString(value, at, refuse), delimiters);
2531
+ }
2532
+
2511
2533
  // src/transactions/remit/build-835.ts
2512
2534
  function refuseSpec(message) {
2513
2535
  throw new Remit835BuildError(REMIT_835_BUILD_ERROR_CODES.X12_835_BUILD_INVALID_SPEC, message);
@@ -2533,7 +2555,7 @@ function build835(spec) {
2533
2555
  component: componentSeparator,
2534
2556
  segment: segmentTerminator
2535
2557
  };
2536
- const esc = (value) => escapeRelease(value, delimiters);
2558
+ const esc = makeCallerEscaper(delimiters, "build835", refuseSpec);
2537
2559
  const seg = (parts) => {
2538
2560
  let end = parts.length;
2539
2561
  while (end > 1 && parts[end - 1] === "") end -= 1;
@@ -4210,7 +4232,7 @@ function buildClaim837(variant, spec) {
4210
4232
  component: componentSeparator,
4211
4233
  segment: segmentTerminator
4212
4234
  };
4213
- const esc = (value) => escapeRelease(value, delimiters);
4235
+ const esc = makeCallerEscaper(delimiters, "build837", refuseSpec2);
4214
4236
  const seg = (parts) => {
4215
4237
  let end = parts.length;
4216
4238
  while (end > 1 && parts[end - 1] === "") end -= 1;
@@ -5351,6 +5373,12 @@ function refuseHierarchy2(message) {
5351
5373
  message
5352
5374
  );
5353
5375
  }
5376
+ function refuseSpec3(message) {
5377
+ throw new Eligibility271BuildError(
5378
+ ELIGIBILITY_271_BUILD_ERROR_CODES.X12_271_BUILD_INVALID_SPEC,
5379
+ message
5380
+ );
5381
+ }
5354
5382
  var X279A1_VERSION_RELEASE = "005010X279A1";
5355
5383
  var X12_271_FUNCTIONAL_ID = "HB";
5356
5384
  var X12_AGENCY_CODE3 = "X";
@@ -5368,7 +5396,7 @@ function build271(spec) {
5368
5396
  component: componentSeparator,
5369
5397
  segment: segmentTerminator
5370
5398
  };
5371
- const esc = (value) => escapeRelease(value, delimiters);
5399
+ const esc = makeCallerEscaper(delimiters, "build271", refuseSpec3);
5372
5400
  const seg = (parts) => {
5373
5401
  let end = parts.length;
5374
5402
  while (end > 1 && parts[end - 1] === "") end -= 1;
@@ -6043,7 +6071,7 @@ function refuseHierarchy3(message) {
6043
6071
  message
6044
6072
  );
6045
6073
  }
6046
- function refuseSpec3(message) {
6074
+ function refuseSpec4(message) {
6047
6075
  throw new ClaimStatus277BuildError(
6048
6076
  CLAIM_STATUS_277_BUILD_ERROR_CODES.X12_277_BUILD_INVALID_SPEC,
6049
6077
  message
@@ -6077,7 +6105,7 @@ function buildClaimStatus(version, spec) {
6077
6105
  component: componentSeparator,
6078
6106
  segment: segmentTerminator
6079
6107
  };
6080
- const esc = (value) => escapeRelease(value, delimiters);
6108
+ const esc = makeCallerEscaper(delimiters, "build277", refuseSpec4);
6081
6109
  const seg = (parts) => {
6082
6110
  let end = parts.length;
6083
6111
  while (end > 1 && parts[end - 1] === "") end -= 1;
@@ -6252,12 +6280,12 @@ function enforceClaim2(claim, locator, isFirst) {
6252
6280
  const statuses = requireCallerArray(
6253
6281
  claim.statuses,
6254
6282
  `build277: claim at ${locator}: statuses`,
6255
- refuseSpec3
6283
+ refuseSpec4
6256
6284
  );
6257
6285
  const serviceLines = requireCallerArray(
6258
6286
  claim.serviceLines,
6259
6287
  `build277: claim at ${locator}: serviceLines`,
6260
- refuseSpec3
6288
+ refuseSpec4
6261
6289
  );
6262
6290
  if (claim.trace === void 0 && statuses.length === 0 && serviceLines.length === 0) {
6263
6291
  throw new ClaimStatus277BuildError(
@@ -6280,7 +6308,7 @@ function enforceClaim2(claim, locator, isFirst) {
6280
6308
  const lineStatuses = requireCallerArray(
6281
6309
  line.statuses,
6282
6310
  `build277: claim at ${locator}.line[${String(l)}]: statuses`,
6283
- refuseSpec3
6311
+ refuseSpec4
6284
6312
  );
6285
6313
  for (let i = 0; i < lineStatuses.length; i += 1) {
6286
6314
  enforceStatus(lineStatuses[i], `${locator}.line[${String(l)}].status[${String(i)}]`);
@@ -6831,7 +6859,7 @@ var Premium820BuildError = class extends Error {
6831
6859
  };
6832
6860
 
6833
6861
  // src/transactions/premium/build-820.ts
6834
- function refuseSpec4(message) {
6862
+ function refuseSpec5(message) {
6835
6863
  throw new Premium820BuildError(PREMIUM_820_BUILD_ERROR_CODES.X12_820_BUILD_INVALID_SPEC, message);
6836
6864
  }
6837
6865
  var X218_VERSION_RELEASE = "005010X218";
@@ -6850,7 +6878,7 @@ function build820(spec) {
6850
6878
  component: componentSeparator,
6851
6879
  segment: segmentTerminator
6852
6880
  };
6853
- const esc = (value) => escapeRelease(value, delimiters);
6881
+ const esc = makeCallerEscaper(delimiters, "build820", refuseSpec5);
6854
6882
  const seg = (parts) => {
6855
6883
  let end = parts.length;
6856
6884
  while (end > 1 && parts[end - 1] === "") end -= 1;
@@ -6977,7 +7005,7 @@ function build820(spec) {
6977
7005
  return parseX12(raw);
6978
7006
  }
6979
7007
  function enforceStructuralSpec5(spec) {
6980
- const traces = requireCallerArray(spec.traces, "build820: spec.traces", refuseSpec4);
7008
+ const traces = requireCallerArray(spec.traces, "build820: spec.traces", refuseSpec5);
6981
7009
  if (traces.length === 0) {
6982
7010
  throw new Premium820BuildError(
6983
7011
  PREMIUM_820_BUILD_ERROR_CODES.X12_820_BUILD_INVALID_SPEC,
@@ -6987,7 +7015,7 @@ function enforceStructuralSpec5(spec) {
6987
7015
  const remittances = requireCallerArray(
6988
7016
  spec.remittances,
6989
7017
  "build820: spec.remittances",
6990
- refuseSpec4
7018
+ refuseSpec5
6991
7019
  );
6992
7020
  if (remittances.length === 0) {
6993
7021
  throw new Premium820BuildError(
@@ -7007,7 +7035,7 @@ function enforceStructuralSpec5(spec) {
7007
7035
  const openItems = requireCallerArray(
7008
7036
  remittance.openItems,
7009
7037
  `build820: spec.remittances[${String(r)}].openItems`,
7010
- refuseSpec4
7038
+ refuseSpec5
7011
7039
  );
7012
7040
  if (openItems.length === 0) {
7013
7041
  throw new Premium820BuildError(
@@ -7527,7 +7555,7 @@ var Enrollment834BuildError = class extends Error {
7527
7555
  };
7528
7556
 
7529
7557
  // src/transactions/enrollment/build-834.ts
7530
- function refuseSpec5(message) {
7558
+ function refuseSpec6(message) {
7531
7559
  throw new Enrollment834BuildError(
7532
7560
  ENROLLMENT_834_BUILD_ERROR_CODES.X12_834_BUILD_INVALID_SPEC,
7533
7561
  message
@@ -7551,7 +7579,7 @@ function build834(spec) {
7551
7579
  component: componentSeparator,
7552
7580
  segment: segmentTerminator
7553
7581
  };
7554
- const esc = (value) => escapeRelease(value, delimiters);
7582
+ const esc = makeCallerEscaper(delimiters, "build834", refuseSpec6);
7555
7583
  const seg = (parts) => {
7556
7584
  let end = parts.length;
7557
7585
  while (end > 1 && parts[end - 1] === "") end -= 1;
@@ -7632,7 +7660,7 @@ function build834(spec) {
7632
7660
  return parseX12(raw);
7633
7661
  }
7634
7662
  function enforceStructuralSpec6(spec) {
7635
- const members = requireCallerArray(spec.members, "build834: spec.members", refuseSpec5);
7663
+ const members = requireCallerArray(spec.members, "build834: spec.members", refuseSpec6);
7636
7664
  if (members.length === 0) {
7637
7665
  throw new Enrollment834BuildError(
7638
7666
  ENROLLMENT_834_BUILD_ERROR_CODES.X12_834_BUILD_INVALID_SPEC,
@@ -7657,7 +7685,7 @@ function enforceStructuralSpec6(spec) {
7657
7685
  const coverages = requireCallerArray(
7658
7686
  member.healthCoverages,
7659
7687
  `build834: spec.members[${String(m)}].healthCoverages`,
7660
- refuseSpec5
7688
+ refuseSpec6
7661
7689
  );
7662
7690
  for (let c = 0; c < coverages.length; c += 1) {
7663
7691
  const coverage = coverages[c];
@@ -8184,6 +8212,12 @@ function refuseHierarchy4(message) {
8184
8212
  message
8185
8213
  );
8186
8214
  }
8215
+ function refuseSpec7(message) {
8216
+ throw new ServicesReview278BuildError(
8217
+ AUTH_278_BUILD_ERROR_CODES.X12_278_BUILD_INVALID_SPEC,
8218
+ message
8219
+ );
8220
+ }
8187
8221
  var X12_278_FUNCTIONAL_ID = "HI";
8188
8222
  var X12_AGENCY_CODE7 = "X";
8189
8223
  var HL_LEVEL4 = {
@@ -8212,7 +8246,7 @@ function buildServicesReview(version, direction, spec) {
8212
8246
  component: componentSeparator,
8213
8247
  segment: segmentTerminator
8214
8248
  };
8215
- const esc = (value) => escapeRelease(value, delimiters);
8249
+ const esc = makeCallerEscaper(delimiters, "build278", refuseSpec7);
8216
8250
  const seg = (parts) => {
8217
8251
  let end = parts.length;
8218
8252
  while (end > 1 && parts[end - 1] === "") end -= 1;
@@ -8963,6 +8997,9 @@ function narrowNoteCode(value) {
8963
8997
  }
8964
8998
 
8965
8999
  // src/transactions/ack/build-999.ts
9000
+ function refuseSpec8(message) {
9001
+ throw new AckBuildError(ACK_BUILD_ERROR_CODES.X12_ACK_INVALID_SPEC, message);
9002
+ }
8966
9003
  var X231A1_VERSION_RELEASE = "005010X231A1";
8967
9004
  var X12_AGENCY_CODE8 = "X";
8968
9005
  function build999(spec) {
@@ -8979,7 +9016,7 @@ function build999(spec) {
8979
9016
  component: componentSeparator,
8980
9017
  segment: segmentTerminator
8981
9018
  };
8982
- const esc = (value) => escapeRelease(value, delimiters);
9019
+ const esc = makeCallerEscaper(delimiters, "build999", refuseSpec8);
8983
9020
  const senderQualifier = envelope.senderQualifier ?? "ZZ";
8984
9021
  const receiverQualifier = envelope.receiverQualifier ?? "ZZ";
8985
9022
  const usageIndicator = envelope.usageIndicator ?? "P";
@@ -9459,6 +9496,9 @@ var X12BuildError = class extends Error {
9459
9496
  };
9460
9497
 
9461
9498
  // src/builder/build-interchange.ts
9499
+ function refuseSpec9(message) {
9500
+ throw new X12BuildError(X12_BUILD_ERROR_CODES.X12_BUILD_INVALID_SPEC, message);
9501
+ }
9462
9502
  function buildInterchange(spec) {
9463
9503
  const elementSeparator = spec.elementSeparator ?? "*";
9464
9504
  const repetitionSeparator = spec.repetitionSeparator ?? "^";
@@ -9470,7 +9510,7 @@ function buildInterchange(spec) {
9470
9510
  component: componentSeparator,
9471
9511
  segment: segmentTerminator
9472
9512
  };
9473
- const esc = (value) => escapeRelease(value, delimiters);
9513
+ const esc = makeCallerEscaper(delimiters, "buildInterchange", refuseSpec9);
9474
9514
  const senderQualifier = spec.senderQualifier ?? "ZZ";
9475
9515
  const receiverQualifier = spec.receiverQualifier ?? "ZZ";
9476
9516
  const usageIndicator = spec.usageIndicator ?? "P";
@@ -9892,7 +9932,7 @@ var profiles = Object.freeze({
9892
9932
  });
9893
9933
 
9894
9934
  // src/index.ts
9895
- var VERSION = "0.0.8";
9935
+ var VERSION = "0.0.9";
9896
9936
 
9897
9937
  exports.ACK_BUILD_ERROR_CODES = ACK_BUILD_ERROR_CODES;
9898
9938
  exports.ALL_WARNING_MESSAGES = ALL_WARNING_MESSAGES;