@cosyte/synth 0.1.4 → 0.1.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +14 -10
  3. package/dist/astm/index.cjs +164 -102
  4. package/dist/astm/index.cjs.map +1 -1
  5. package/dist/astm/index.d.cts +7 -0
  6. package/dist/astm/index.d.ts +7 -0
  7. package/dist/astm/index.mjs +163 -102
  8. package/dist/astm/index.mjs.map +1 -1
  9. package/dist/ccda/index.cjs +100 -28
  10. package/dist/ccda/index.cjs.map +1 -1
  11. package/dist/ccda/index.d.cts +6 -0
  12. package/dist/ccda/index.d.ts +6 -0
  13. package/dist/ccda/index.mjs +87 -16
  14. package/dist/ccda/index.mjs.map +1 -1
  15. package/dist/deid/index.cjs +305 -165
  16. package/dist/deid/index.cjs.map +1 -1
  17. package/dist/deid/index.mjs +292 -153
  18. package/dist/deid/index.mjs.map +1 -1
  19. package/dist/fhir/index.cjs +179 -76
  20. package/dist/fhir/index.cjs.map +1 -1
  21. package/dist/fhir/index.d.cts +6 -0
  22. package/dist/fhir/index.d.ts +6 -0
  23. package/dist/fhir/index.mjs +178 -76
  24. package/dist/fhir/index.mjs.map +1 -1
  25. package/dist/hl7/index.cjs +86 -12
  26. package/dist/hl7/index.cjs.map +1 -1
  27. package/dist/hl7/index.d.cts +6 -0
  28. package/dist/hl7/index.d.ts +6 -0
  29. package/dist/hl7/index.mjs +85 -12
  30. package/dist/hl7/index.mjs.map +1 -1
  31. package/dist/index.cjs +3 -5
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.d.cts +8 -4
  34. package/dist/index.d.ts +8 -4
  35. package/dist/index.mjs +3 -5
  36. package/dist/index.mjs.map +1 -1
  37. package/dist/ncpdp/index.cjs +131 -36
  38. package/dist/ncpdp/index.cjs.map +1 -1
  39. package/dist/ncpdp/index.d.cts +7 -0
  40. package/dist/ncpdp/index.d.ts +7 -0
  41. package/dist/ncpdp/index.mjs +130 -36
  42. package/dist/ncpdp/index.mjs.map +1 -1
  43. package/dist/x12/index.cjs +147 -50
  44. package/dist/x12/index.cjs.map +1 -1
  45. package/dist/x12/index.d.cts +6 -0
  46. package/dist/x12/index.d.ts +6 -0
  47. package/dist/x12/index.mjs +146 -50
  48. package/dist/x12/index.mjs.map +1 -1
  49. package/package.json +20 -15
@@ -158,6 +158,7 @@ declare function generateB3(options: GenerateTelecomOptions): string;
158
158
  *
159
159
  * @module
160
160
  */
161
+
161
162
  /** The verdict of one round-trip through `@cosyte/ncpdp`. */
162
163
  interface RoundTripResult {
163
164
  /** The serialized NCPDP wire text (the serializer's own conservative emit). */
@@ -412,6 +413,12 @@ interface NcpdpCorpusOptions {
412
413
  readonly count?: number;
413
414
  /** The transaction kinds to cycle through. Defaults to one of each. */
414
415
  readonly mix?: readonly NcpdpCorpusKind[];
416
+ /**
417
+ * Whether each artifact is read back through its parser and the parser's findings recorded as its
418
+ * `warnings`. Defaults to `true`. `false` skips the read, a large share of a corpus's time; every
419
+ * artifact's `warnings` is then empty without having been checked.
420
+ */
421
+ readonly verify?: boolean;
415
422
  }
416
423
  /**
417
424
  * Build a reproducible {@link Corpus} of spec-clean NCPDP transactions. Each transaction is generated
@@ -158,6 +158,7 @@ declare function generateB3(options: GenerateTelecomOptions): string;
158
158
  *
159
159
  * @module
160
160
  */
161
+
161
162
  /** The verdict of one round-trip through `@cosyte/ncpdp`. */
162
163
  interface RoundTripResult {
163
164
  /** The serialized NCPDP wire text (the serializer's own conservative emit). */
@@ -412,6 +413,12 @@ interface NcpdpCorpusOptions {
412
413
  readonly count?: number;
413
414
  /** The transaction kinds to cycle through. Defaults to one of each. */
414
415
  readonly mix?: readonly NcpdpCorpusKind[];
416
+ /**
417
+ * Whether each artifact is read back through its parser and the parser's findings recorded as its
418
+ * `warnings`. Defaults to `true`. `false` skips the read, a large share of a corpus's time; every
419
+ * artifact's `warnings` is then empty without having been checked.
420
+ */
421
+ readonly verify?: boolean;
415
422
  }
416
423
  /**
417
424
  * Build a reproducible {@link Corpus} of spec-clean NCPDP transactions. Each transaction is generated
@@ -1,6 +1,12 @@
1
- import { buildNewRx, serializeScript, ScriptMessage, parseScript } from '@cosyte/ncpdp/script';
2
- import { decimalValue, codedValue } from '@cosyte/ncpdp/common';
3
- import { serializeTelecom, buildTelecomRequest, parseTelecom } from '@cosyte/ncpdp/telecom';
1
+ import { createRequire } from 'module';
2
+ import { fileURLToPath } from 'url';
3
+
4
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
5
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
6
+ }) : x)(function(x) {
7
+ if (typeof require !== "undefined") return require.apply(this, arguments);
8
+ throw Error('Dynamic require of "' + x + '" is not supported');
9
+ });
4
10
 
5
11
  // src/rng/splitmix32.ts
6
12
  function splitmix32(seed) {
@@ -32,18 +38,59 @@ function sfc32Next(s) {
32
38
  }
33
39
 
34
40
  // src/codes.ts
35
- var SYNTH_FATAL_CODES = {
41
+ var SYNTH_FATAL_CODES = Object.freeze({
42
+ /**
43
+ * A format was requested that this build cannot generate through a real parser builder/serializer.
44
+ * Fatal, never a hand-written byte fallback.
45
+ *
46
+ * **No code path in this build raises it.** All six formats generate, so it is reserved for a
47
+ * future format that does not, and is kept because removing a published code is a breaking change.
48
+ * An unsupported *kind* within a format that does generate is `SYNTH_UNSUPPORTED_KIND`.
49
+ */
50
+ SYNTH_UNSUPPORTED_FORMAT: "SYNTH_UNSUPPORTED_FORMAT",
51
+ /**
52
+ * A vendor quirk was requested that the target format's profile system does not support. Fatal,
53
+ * never a silent no-op and never a fabricated quirk.
54
+ */
55
+ SYNTH_UNSUPPORTED_QUIRK: "SYNTH_UNSUPPORTED_QUIRK",
56
+ /**
57
+ * A quirk transform found no structural anchor to mutate, so the fixture would not carry the
58
+ * deviation it is labelled with. Fatal: a golden file that lies about its parser verdict is worse
59
+ * than no golden file.
60
+ */
61
+ SYNTH_QUIRK_ANCHOR_ABSENT: "SYNTH_QUIRK_ANCHOR_ABSENT",
62
+ /**
63
+ * A bare parse of a freshly-generated quirk artifact did not produce exactly the declared intended
64
+ * warning code(s). Fatal, never emit a mislabeled fixture.
65
+ */
66
+ SYNTH_INTENDED_WARNING_MISMATCH: "SYNTH_INTENDED_WARNING_MISMATCH",
67
+ /** A concept's code-system URI has no OID mapping in the C-CDA example-code table. Fatal. */
68
+ SYNTH_UNMAPPED_CODE_SYSTEM: "SYNTH_UNMAPPED_CODE_SYSTEM",
69
+ /** A money value could not be read as an X12 decimal. Fatal: a generator never rounds to a float. */
70
+ SYNTH_INVALID_DECIMAL: "SYNTH_INVALID_DECIMAL",
36
71
  /** An integer range was requested with its maximum below its minimum. Fatal. */
37
72
  SYNTH_INVALID_RANGE: "SYNTH_INVALID_RANGE",
38
73
  /** A value was drawn from an empty pool. Fatal, never a fabricated substitute. */
39
74
  SYNTH_EMPTY_POOL: "SYNTH_EMPTY_POOL",
75
+ /** A `defineSynthProfile` spec was not usable (a missing or blank `name`). Fatal. */
76
+ SYNTH_INVALID_PROFILE: "SYNTH_INVALID_PROFILE",
40
77
  /**
41
78
  * A caller-supplied selector (a message kind, a document type, a corpus mix entry, a claim
42
79
  * variant, a Bundle type, a resource profile) is not in the closed set that governs it. Fatal:
43
80
  * see `resolveKind`: a selector union is erased at run time, and a selector that falls through
44
81
  * either mislabels the fixture or hands the value to a peer builder that quotes it back.
45
82
  */
46
- SYNTH_UNSUPPORTED_KIND: "SYNTH_UNSUPPORTED_KIND"};
83
+ SYNTH_UNSUPPORTED_KIND: "SYNTH_UNSUPPORTED_KIND",
84
+ /**
85
+ * A requested profile IS published by the adopted implementation guide, and this build does not
86
+ * generate it. Deliberately distinct from `SYNTH_UNSUPPORTED_KIND`, which says the name is not in
87
+ * the adopted set at all: "the guide does not publish this" and "we do not generate this yet" are
88
+ * different answers, and a caller building to a regulatory profile set has to be able to tell them
89
+ * apart without matching message text. Fatal, and raised **before** anything is generated: a
90
+ * mislabelled artifact is worse than no artifact.
91
+ */
92
+ SYNTH_PROFILE_NOT_GENERATED: "SYNTH_PROFILE_NOT_GENERATED"
93
+ });
47
94
  var SYNTH_FATAL_MESSAGES = Object.freeze({
48
95
  SYNTH_UNSUPPORTED_FORMAT: "The requested format is not generable by this build. A generator has no byte fallback: it builds through a parser's own serializer or it refuses.",
49
96
  SYNTH_UNSUPPORTED_QUIRK: "The requested vendor quirk is not in the target format's quirk registry. Compare the request against that format's exported registry (HL7_QUIRKS, CCDA_QUIRKS, ASTM_QUIRKS).",
@@ -109,6 +156,7 @@ function createRng(seed) {
109
156
  }
110
157
 
111
158
  // src/corpus.ts
159
+ var UNCHECKED = Object.freeze([]);
112
160
  function makeCorpus(seed, artifacts, quirks = []) {
113
161
  const counts = {};
114
162
  const formats = /* @__PURE__ */ new Set();
@@ -465,18 +513,20 @@ function ncpdpScriptRouting(rng) {
465
513
  }
466
514
 
467
515
  // src/ncpdp/example-codes.ts
468
- var EXAMPLE_DRUGS = Object.freeze([
469
- { description: "Amoxicillin 500 MG Oral Capsule", ndc: "00000010101", form: "EA" },
470
- { description: "Lisinopril 10 MG Oral Tablet", ndc: "00000020202", form: "EA" },
471
- { description: "Atorvastatin 20 MG Oral Tablet", ndc: "00000030303", form: "EA" },
472
- { description: "Metformin 500 MG Oral Tablet", ndc: "00000040404", form: "EA" },
473
- { description: "Amlodipine 5 MG Oral Tablet", ndc: "00000050505", form: "EA" },
474
- { description: "Omeprazole 20 MG Delayed Release Capsule", ndc: "00000060606", form: "EA" },
475
- { description: "Levothyroxine 50 MCG Oral Tablet", ndc: "00000070707", form: "EA" },
476
- { description: "Azithromycin 250 MG Oral Tablet", ndc: "00000080808", form: "EA" },
477
- { description: "Sertraline 50 MG Oral Tablet", ndc: "00000090909", form: "EA" },
478
- { description: "Albuterol 90 MCG Metered Dose Inhaler", ndc: "00000101010", form: "EA" }
479
- ]);
516
+ var EXAMPLE_DRUGS = Object.freeze(
517
+ [
518
+ { description: "Amoxicillin 500 MG Oral Capsule", ndc: "00000010101", form: "EA" },
519
+ { description: "Lisinopril 10 MG Oral Tablet", ndc: "00000020202", form: "EA" },
520
+ { description: "Atorvastatin 20 MG Oral Tablet", ndc: "00000030303", form: "EA" },
521
+ { description: "Metformin 500 MG Oral Tablet", ndc: "00000040404", form: "EA" },
522
+ { description: "Amlodipine 5 MG Oral Tablet", ndc: "00000050505", form: "EA" },
523
+ { description: "Omeprazole 20 MG Delayed Release Capsule", ndc: "00000060606", form: "EA" },
524
+ { description: "Levothyroxine 50 MCG Oral Tablet", ndc: "00000070707", form: "EA" },
525
+ { description: "Azithromycin 250 MG Oral Tablet", ndc: "00000080808", form: "EA" },
526
+ { description: "Sertraline 50 MG Oral Tablet", ndc: "00000090909", form: "EA" },
527
+ { description: "Albuterol 90 MCG Metered Dose Inhaler", ndc: "00000101010", form: "EA" }
528
+ ].map((entry) => Object.freeze(entry))
529
+ );
480
530
  var EXAMPLE_SIG_TEXT = Object.freeze([
481
531
  "Take 1 tablet by mouth once daily",
482
532
  "Take 1 capsule by mouth twice daily with food",
@@ -485,6 +535,30 @@ var EXAMPLE_SIG_TEXT = Object.freeze([
485
535
  "Inhale 2 puffs by mouth every 4 to 6 hours as needed"
486
536
  ]);
487
537
  var DAW_CODES = Object.freeze(["0", "1", "2"]);
538
+ function defaultHost() {
539
+ if (typeof import.meta.resolve !== "function") return { require: __require, resolve: void 0 };
540
+ return { require: createRequire(import.meta.url), resolve: (s) => import.meta.resolve(s) };
541
+ }
542
+ function isRequireEsmRefusal(error) {
543
+ return error?.code === "ERR_REQUIRE_ESM";
544
+ }
545
+ function loadPeer(specifier, host) {
546
+ if (host.resolve === void 0) return host.require(specifier);
547
+ const entry = fileURLToPath(host.resolve(specifier));
548
+ try {
549
+ return host.require(entry);
550
+ } catch (error) {
551
+ if (!isRequireEsmRefusal(error)) throw error;
552
+ return host.require(specifier);
553
+ }
554
+ }
555
+ function lazyPeer(specifier, host) {
556
+ let loaded;
557
+ return () => loaded ??= loadPeer(specifier, defaultHost());
558
+ }
559
+ var ncpdpScriptPeer = lazyPeer("@cosyte/ncpdp/script");
560
+ var ncpdpTelecomPeer = lazyPeer("@cosyte/ncpdp/telecom");
561
+ var ncpdpCommonPeer = lazyPeer("@cosyte/ncpdp/common");
488
562
 
489
563
  // src/ncpdp/script.ts
490
564
  var SCRIPT_VERSION = "2017071";
@@ -515,13 +589,13 @@ function medicationModel(rng, routing) {
515
589
  const written = `${routing.date.slice(0, 4)}-${routing.date.slice(4, 6)}-${routing.date.slice(6, 8)}`;
516
590
  return {
517
591
  description: drug.description,
518
- coded: { productCode: codedValue(drug.ndc, "ND") },
592
+ coded: { productCode: ncpdpCommonPeer().codedValue(drug.ndc, "ND") },
519
593
  quantity: {
520
- value: decimalValue(String(rng.int(30, 90))),
594
+ value: ncpdpCommonPeer().decimalValue(String(rng.int(30, 90))),
521
595
  unitOfMeasure: drug.form,
522
596
  codeListQualifier: "38"
523
597
  },
524
- daysSupply: decimalValue(String(rng.pick([30, 60, 90]))),
598
+ daysSupply: ncpdpCommonPeer().decimalValue(String(rng.pick([30, 60, 90]))),
525
599
  numberOfRefills: String(rng.int(0, 5)),
526
600
  substitutions: rng.pick(["0", "1"]),
527
601
  writtenDate: written,
@@ -545,14 +619,14 @@ function generateNewRx(options) {
545
619
  const pharmacy = pharmacyModel(rng);
546
620
  const prescriber = prescriberModel(rng);
547
621
  const medication = medicationModel(rng, routing);
548
- const message = buildNewRx({
622
+ const message = ncpdpScriptPeer().buildNewRx({
549
623
  header: headerModel(routing),
550
624
  patient,
551
625
  pharmacy,
552
626
  prescriber,
553
627
  medication
554
628
  });
555
- return serializeScript(message);
629
+ return ncpdpScriptPeer().serializeScript(message);
556
630
  }
557
631
  function lifecycleFields(rng) {
558
632
  const routing = ncpdpScriptRouting(rng);
@@ -569,14 +643,20 @@ function generateRxRenewalRequest(options) {
569
643
  const rng = createRng(options.seed);
570
644
  const { header: header2, ...fields } = lifecycleFields(rng);
571
645
  const body = { kind: "RxRenewalRequest", ...fields };
572
- return serializeScript(new ScriptMessage({ header: header2, body, warnings: [] }));
646
+ return ncpdpScriptPeer().serializeScript(
647
+ new (ncpdpScriptPeer()).ScriptMessage({ header: header2, body, warnings: [] })
648
+ );
573
649
  }
574
650
  function generateRxChangeRequest(options) {
575
651
  const rng = createRng(options.seed);
576
652
  const { header: header2, ...fields } = lifecycleFields(rng);
577
653
  const body = { kind: "RxChangeRequest", ...fields };
578
- return serializeScript(new ScriptMessage({ header: header2, body, warnings: [] }));
654
+ return ncpdpScriptPeer().serializeScript(
655
+ new (ncpdpScriptPeer()).ScriptMessage({ header: header2, body, warnings: [] })
656
+ );
579
657
  }
658
+
659
+ // src/ncpdp/telecom.ts
580
660
  var SYNTHETIC_BIN = "999999";
581
661
  function digitsOnly(value) {
582
662
  return value.replace(/\D/g, "");
@@ -676,7 +756,9 @@ function generateTelecom(code, options) {
676
756
  prescriberSegment(rng),
677
757
  claimSegment(rng, false)
678
758
  ];
679
- return serializeTelecom(buildTelecomRequest({ header: head, segments }));
759
+ return ncpdpTelecomPeer().serializeTelecom(
760
+ ncpdpTelecomPeer().buildTelecomRequest({ header: head, segments })
761
+ );
680
762
  }
681
763
  function generateB1(options) {
682
764
  return generateTelecom("B1", options);
@@ -687,18 +769,30 @@ function generateB2(options) {
687
769
  function generateB3(options) {
688
770
  return generateTelecom("B3", options);
689
771
  }
772
+
773
+ // src/ncpdp/round-trip.ts
690
774
  function scriptRoundTrip(xml) {
691
- const message = parseScript(xml);
775
+ const message = ncpdpScriptPeer().parseScript(xml);
692
776
  const warnings = message.warnings.map((w) => String(w.code));
693
- const byteStable = serializeScript(message) === xml;
777
+ const byteStable = ncpdpScriptPeer().serializeScript(message) === xml;
694
778
  return { content: xml, warnings, byteStable, specClean: warnings.length === 0 && byteStable };
695
779
  }
696
780
  function telecomRoundTrip(wire) {
697
- const transaction = parseTelecom(wire);
781
+ const transaction = ncpdpTelecomPeer().parseTelecom(wire);
698
782
  const warnings = transaction.warnings.map((w) => String(w.code));
699
- const byteStable = serializeTelecom(transaction) === wire;
783
+ const byteStable = ncpdpTelecomPeer().serializeTelecom(transaction) === wire;
700
784
  return { content: wire, warnings, byteStable, specClean: warnings.length === 0 && byteStable };
701
785
  }
786
+ function scriptCorpusArtifact(xml, verify) {
787
+ if (!verify) return { content: xml, warnings: UNCHECKED };
788
+ const warnings = ncpdpScriptPeer().parseScript(xml).warnings.map((w) => String(w.code));
789
+ return { content: xml, warnings };
790
+ }
791
+ function telecomCorpusArtifact(wire, verify) {
792
+ if (!verify) return { content: wire, warnings: UNCHECKED };
793
+ const warnings = ncpdpTelecomPeer().parseTelecom(wire).warnings.map((w) => String(w.code));
794
+ return { content: wire, warnings };
795
+ }
702
796
 
703
797
  // src/select.ts
704
798
  function resolveKind(allowed, requested) {
@@ -721,29 +815,29 @@ var ALL_KINDS = Object.freeze([
721
815
  "B3"
722
816
  ]);
723
817
  var DEFAULT_MIX = ALL_KINDS;
724
- function generateKind(kind, seed) {
818
+ function generateKind(kind, seed, verify) {
725
819
  switch (kind) {
726
820
  case "NewRx":
727
- return scriptRoundTrip(generateNewRx({ seed }));
821
+ return scriptCorpusArtifact(generateNewRx({ seed }), verify);
728
822
  case "RxRenewalRequest":
729
- return scriptRoundTrip(generateRxRenewalRequest({ seed }));
823
+ return scriptCorpusArtifact(generateRxRenewalRequest({ seed }), verify);
730
824
  case "RxChangeRequest":
731
- return scriptRoundTrip(generateRxChangeRequest({ seed }));
825
+ return scriptCorpusArtifact(generateRxChangeRequest({ seed }), verify);
732
826
  case "B1":
733
827
  case "B2":
734
828
  case "B3":
735
- return telecomRoundTrip(generateTelecom(kind, { seed }));
829
+ return telecomCorpusArtifact(generateTelecom(kind, { seed }), verify);
736
830
  }
737
831
  }
738
832
  function ncpdpCorpus(options) {
739
- const { seed } = options;
833
+ const { seed, verify = true } = options;
740
834
  const mix = resolveMix(ALL_KINDS, options.mix, DEFAULT_MIX);
741
835
  const count = options.count ?? mix.length;
742
836
  const seedStream = createRng(seed);
743
837
  const artifacts = Array.from({ length: count }, (_unused, i) => {
744
838
  const kind = mix[i % mix.length] ?? "NewRx";
745
839
  const txSeed = seedStream.nextUint32();
746
- const rt = generateKind(kind, txSeed);
840
+ const rt = generateKind(kind, txSeed, verify);
747
841
  return {
748
842
  format: "ncpdp",
749
843
  kind,