@cosyte/synth 0.1.8 → 0.1.10

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 (55) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +28 -17
  3. package/dist/animal-code-Bdl_9Jma.d.cts +30 -0
  4. package/dist/animal-code-Bdl_9Jma.d.ts +30 -0
  5. package/dist/astm/index.cjs +150 -15
  6. package/dist/astm/index.cjs.map +1 -1
  7. package/dist/astm/index.d.cts +22 -9
  8. package/dist/astm/index.d.ts +22 -9
  9. package/dist/astm/index.mjs +150 -15
  10. package/dist/astm/index.mjs.map +1 -1
  11. package/dist/ccda/index.cjs +150 -15
  12. package/dist/ccda/index.cjs.map +1 -1
  13. package/dist/ccda/index.d.cts +12 -3
  14. package/dist/ccda/index.d.ts +12 -3
  15. package/dist/ccda/index.mjs +150 -15
  16. package/dist/ccda/index.mjs.map +1 -1
  17. package/dist/deid/index.cjs +540 -28
  18. package/dist/deid/index.cjs.map +1 -1
  19. package/dist/deid/index.d.cts +2 -1
  20. package/dist/deid/index.d.ts +2 -1
  21. package/dist/deid/index.mjs +540 -28
  22. package/dist/deid/index.mjs.map +1 -1
  23. package/dist/fhir/index.cjs +145 -12
  24. package/dist/fhir/index.cjs.map +1 -1
  25. package/dist/fhir/index.d.cts +4 -1
  26. package/dist/fhir/index.d.ts +4 -1
  27. package/dist/fhir/index.mjs +145 -12
  28. package/dist/fhir/index.mjs.map +1 -1
  29. package/dist/hl7/index.cjs +569 -37
  30. package/dist/hl7/index.cjs.map +1 -1
  31. package/dist/hl7/index.d.cts +402 -111
  32. package/dist/hl7/index.d.ts +402 -111
  33. package/dist/hl7/index.mjs +560 -38
  34. package/dist/hl7/index.mjs.map +1 -1
  35. package/dist/index.cjs +124 -13
  36. package/dist/index.cjs.map +1 -1
  37. package/dist/index.d.cts +183 -10
  38. package/dist/index.d.ts +183 -10
  39. package/dist/index.mjs +115 -14
  40. package/dist/index.mjs.map +1 -1
  41. package/dist/ncpdp/index.cjs +145 -12
  42. package/dist/ncpdp/index.cjs.map +1 -1
  43. package/dist/ncpdp/index.d.cts +4 -1
  44. package/dist/ncpdp/index.d.ts +4 -1
  45. package/dist/ncpdp/index.mjs +145 -12
  46. package/dist/ncpdp/index.mjs.map +1 -1
  47. package/dist/{quirk-BfObsiWV.d.ts → quirk-CzknuYUo.d.ts} +16 -5
  48. package/dist/{quirk-BCSs1jPt.d.cts → quirk-Q5AxzHSC.d.cts} +16 -5
  49. package/dist/x12/index.cjs +145 -12
  50. package/dist/x12/index.cjs.map +1 -1
  51. package/dist/x12/index.d.cts +4 -1
  52. package/dist/x12/index.d.ts +4 -1
  53. package/dist/x12/index.mjs +145 -12
  54. package/dist/x12/index.mjs.map +1 -1
  55. package/package.json +17 -6
@@ -314,9 +314,15 @@ interface CcdaQuirkCorpusOptions {
314
314
  readonly seed: number;
315
315
  /** How many quirk artifacts to generate. Defaults to the number of quirks. */
316
316
  readonly count?: number;
317
- /** The quirk names to cycle through. Defaults to every C-CDA quirk. Validated; unsupported ⇒ fatal. */
317
+ /**
318
+ * The quirk names to cycle through. Defaults to every C-CDA quirk. Validated; unsupported ⇒ fatal.
319
+ * An empty list, or a `profile` whose `quirks` is an empty list, throws `SYNTH_EMPTY_POOL`.
320
+ */
318
321
  readonly quirks?: readonly CcdaQuirkName[];
319
- /** A {@link SynthProfile} whose `quirks` drive the corpus (validated). Takes precedence over `quirks`. */
322
+ /**
323
+ * A {@link SynthProfile} whose `quirks` drive the corpus (validated). Its `quirks`, when set, take
324
+ * precedence over `quirks`; a profile that leaves `quirks` out leaves the selection to `quirks`.
325
+ */
320
326
  readonly profile?: SynthProfile;
321
327
  /** The base document type each quirk is injected into. Defaults to `"ccd"`. */
322
328
  readonly documentType?: CcdaDocumentType;
@@ -360,7 +366,10 @@ interface CcdaCorpusOptions {
360
366
  readonly seed: number;
361
367
  /** How many documents to generate. Defaults to `1`. */
362
368
  readonly count?: number;
363
- /** The document types to cycle through. Defaults to one CCD + one Referral Note. */
369
+ /**
370
+ * The document types to cycle through. Defaults to one CCD + one Referral Note. An empty list
371
+ * throws `SYNTH_EMPTY_POOL`.
372
+ */
364
373
  readonly mix?: readonly CcdaCorpusKind[];
365
374
  /**
366
375
  * Whether each artifact is read back through its parser and the parser's findings recorded as its
@@ -314,9 +314,15 @@ interface CcdaQuirkCorpusOptions {
314
314
  readonly seed: number;
315
315
  /** How many quirk artifacts to generate. Defaults to the number of quirks. */
316
316
  readonly count?: number;
317
- /** The quirk names to cycle through. Defaults to every C-CDA quirk. Validated; unsupported ⇒ fatal. */
317
+ /**
318
+ * The quirk names to cycle through. Defaults to every C-CDA quirk. Validated; unsupported ⇒ fatal.
319
+ * An empty list, or a `profile` whose `quirks` is an empty list, throws `SYNTH_EMPTY_POOL`.
320
+ */
318
321
  readonly quirks?: readonly CcdaQuirkName[];
319
- /** A {@link SynthProfile} whose `quirks` drive the corpus (validated). Takes precedence over `quirks`. */
322
+ /**
323
+ * A {@link SynthProfile} whose `quirks` drive the corpus (validated). Its `quirks`, when set, take
324
+ * precedence over `quirks`; a profile that leaves `quirks` out leaves the selection to `quirks`.
325
+ */
320
326
  readonly profile?: SynthProfile;
321
327
  /** The base document type each quirk is injected into. Defaults to `"ccd"`. */
322
328
  readonly documentType?: CcdaDocumentType;
@@ -360,7 +366,10 @@ interface CcdaCorpusOptions {
360
366
  readonly seed: number;
361
367
  /** How many documents to generate. Defaults to `1`. */
362
368
  readonly count?: number;
363
- /** The document types to cycle through. Defaults to one CCD + one Referral Note. */
369
+ /**
370
+ * The document types to cycle through. Defaults to one CCD + one Referral Note. An empty list
371
+ * throws `SYNTH_EMPTY_POOL`.
372
+ */
364
373
  readonly mix?: readonly CcdaCorpusKind[];
365
374
  /**
366
375
  * Whether each artifact is read back through its parser and the parser's findings recorded as its
@@ -70,7 +70,11 @@ var SYNTH_FATAL_CODES = Object.freeze({
70
70
  SYNTH_INVALID_DECIMAL: "SYNTH_INVALID_DECIMAL",
71
71
  /** An integer range was requested with its maximum below its minimum. Fatal. */
72
72
  SYNTH_INVALID_RANGE: "SYNTH_INVALID_RANGE",
73
- /** A value was drawn from an empty pool. Fatal, never a fabricated substitute. */
73
+ /**
74
+ * A value was drawn from an empty pool. Fatal, never a fabricated substitute. A corpus given an
75
+ * empty `mix`, `triggers` or `quirks` list (or a profile whose `quirks` is an empty list) raises it
76
+ * too: there is no kind to cycle through, and an empty list is never read as the default.
77
+ */
74
78
  SYNTH_EMPTY_POOL: "SYNTH_EMPTY_POOL",
75
79
  /** A `defineSynthProfile` spec was not usable (a missing or blank `name`). Fatal. */
76
80
  SYNTH_INVALID_PROFILE: "SYNTH_INVALID_PROFILE",
@@ -89,7 +93,21 @@ var SYNTH_FATAL_CODES = Object.freeze({
89
93
  * apart without matching message text. Fatal, and raised **before** anything is generated: a
90
94
  * mislabelled artifact is worse than no artifact.
91
95
  */
92
- SYNTH_PROFILE_NOT_GENERATED: "SYNTH_PROFILE_NOT_GENERATED"
96
+ SYNTH_PROFILE_NOT_GENERATED: "SYNTH_PROFILE_NOT_GENERATED",
97
+ /**
98
+ * The sibling parser a generator builds through is not installed. Raised on the first call that
99
+ * needs it, by both builds, only when the missing package is the parser itself: a missing
100
+ * dependency of an installed parser is thrown as Node.js raised it. The error carries the parser's
101
+ * package name on `peer`, a message from {@link SYNTH_PEER_MESSAGES} that names its install
102
+ * command, and the error Node.js raised on `cause`.
103
+ */
104
+ SYNTH_PEER_NOT_INSTALLED: "SYNTH_PEER_NOT_INSTALLED",
105
+ /**
106
+ * An HL7 v2 `ACK` was asked for an inbound message that carries no MSH-10 message control id.
107
+ * MSA-2 must echo that id, and an acknowledgment that cannot be matched to the message it answers
108
+ * is not a fixture worth having, so nothing is generated.
109
+ */
110
+ SYNTH_ACK_UNCORRELATED: "SYNTH_ACK_UNCORRELATED"
93
111
  });
94
112
  var SYNTH_FATAL_MESSAGES = Object.freeze({
95
113
  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.",
@@ -102,18 +120,38 @@ var SYNTH_FATAL_MESSAGES = Object.freeze({
102
120
  SYNTH_EMPTY_POOL: "A value was drawn from an empty pool.",
103
121
  SYNTH_INVALID_PROFILE: "defineSynthProfile requires a non-empty string name.",
104
122
  SYNTH_UNSUPPORTED_KIND: "The requested kind, document type, corpus mix entry, variant or profile is not one this generator supports. The supported set is the exported union for that option.",
105
- SYNTH_PROFILE_NOT_GENERATED: "The requested profile is published by the adopted implementation guide, and this build does not generate it. The coverage surface reports, per adopted profile, whether it is generated."
123
+ SYNTH_PROFILE_NOT_GENERATED: "The requested profile is published by the adopted implementation guide, and this build does not generate it. The coverage surface reports, per adopted profile, whether it is generated.",
124
+ SYNTH_PEER_NOT_INSTALLED: "A sibling @cosyte parser this generator builds through is not installed. Install it beside @cosyte/synth.",
125
+ SYNTH_ACK_UNCORRELATED: (
126
+ // Stryker disable next-line all: a module-level table runs once, at import; the docs test holds it.
127
+ "The inbound message has no MSH-10 control id for an ACK to echo in MSA-2."
128
+ )
129
+ });
130
+ var SYNTH_PEER_MESSAGES = Object.freeze({
131
+ "@cosyte/hl7": "@cosyte/hl7 is not installed, and this generator builds through it. Install it beside @cosyte/synth: npm install @cosyte/hl7",
132
+ "@cosyte/fhir": "@cosyte/fhir is not installed, and this generator builds through it. Install it beside @cosyte/synth: npm install @cosyte/fhir",
133
+ "@cosyte/ccda": "@cosyte/ccda is not installed, and this generator builds through it. Install it beside @cosyte/synth: npm install @cosyte/ccda",
134
+ "@cosyte/x12": "@cosyte/x12 is not installed, and this generator builds through it. Install it beside @cosyte/synth: npm install @cosyte/x12",
135
+ "@cosyte/ncpdp": "@cosyte/ncpdp is not installed, and this generator builds through it. Install it beside @cosyte/synth: npm install @cosyte/ncpdp",
136
+ "@cosyte/astm": "@cosyte/astm is not installed, and this generator builds through it. Install it beside @cosyte/synth: npm install @cosyte/astm",
137
+ "@cosyte/deid": "@cosyte/deid is not installed, and this generator builds through it. Install it beside @cosyte/synth: npm install @cosyte/deid"
106
138
  });
107
139
  var SynthError = class extends Error {
108
140
  /** The stable fatal code. */
109
141
  code;
110
142
  /**
111
143
  * @param code - The stable {@link SynthFatalCode}. The message comes from the frozen registry.
144
+ * @param options - The error that caused this one, and for a missing parser its package.
112
145
  */
113
- constructor(code) {
114
- super(SYNTH_FATAL_MESSAGES[code]);
146
+ constructor(code, options = {}) {
147
+ const peer = code === SYNTH_FATAL_CODES.SYNTH_PEER_NOT_INSTALLED ? options.peer : void 0;
148
+ super(
149
+ peer === void 0 ? SYNTH_FATAL_MESSAGES[code] : SYNTH_PEER_MESSAGES[peer],
150
+ "cause" in options ? { cause: options.cause } : void 0
151
+ );
115
152
  this.name = "SynthError";
116
153
  this.code = code;
154
+ if (peer !== void 0) Object.defineProperty(this, "peer", { value: peer, enumerable: true });
117
155
  }
118
156
  };
119
157
 
@@ -221,7 +259,7 @@ function luhnMod10(digits) {
221
259
  if (d < 0 || d > 9) continue;
222
260
  if (double) {
223
261
  d *= 2;
224
- if (d > 9) d -= 9;
262
+ if (d >= 10) d -= 9;
225
263
  }
226
264
  sum += d;
227
265
  double = !double;
@@ -318,7 +356,7 @@ var SYNTHETIC_CITY_NAMES = Object.freeze([
318
356
  ]);
319
357
 
320
358
  // src/safe/providers.ts
321
- function ssn(rng, block = "never-issued") {
359
+ function ssn(rng, block) {
322
360
  if (block === "advertising") {
323
361
  return `987-00-432${String(rng.int(0, 9))}`;
324
362
  }
@@ -445,6 +483,63 @@ var US_STATES = Object.freeze([
445
483
  "WY"
446
484
  ]);
447
485
 
486
+ // src/safe/animal-code.ts
487
+ var ANIMAL_CODE_SYSTEM = "99COSYTE";
488
+ function animalCode(value, text) {
489
+ return Object.freeze({ code: value, text, system: ANIMAL_CODE_SYSTEM });
490
+ }
491
+ function animalSpecies(value, text, breeds) {
492
+ return Object.freeze({ ...animalCode(value, text), breeds: Object.freeze([...breeds]) });
493
+ }
494
+
495
+ // src/safe/animal.ts
496
+ var SYNTHETIC_ANIMAL_SPECIES = Object.freeze([
497
+ animalSpecies("DOG", "Dog", [
498
+ animalCode("DOG-LAB", "Labrador Retriever"),
499
+ animalCode("DOG-BGL", "Beagle"),
500
+ animalCode("DOG-MIX", "Mixed breed dog")
501
+ ]),
502
+ animalSpecies("CAT", "Cat", [animalCode("CAT-DSH", "Domestic Shorthair"), animalCode("CAT-SIA", "Siamese")]),
503
+ animalSpecies("HORSE", "Horse", [animalCode("HRS-QTR", "Quarter Horse"), animalCode("HRS-TB", "Thoroughbred")]),
504
+ animalSpecies("CATTLE", "Cattle", [animalCode("CTL-HOL", "Holstein"), animalCode("CTL-ANG", "Angus")])
505
+ ]);
506
+ var SYNTHETIC_PET_NAMES = Object.freeze([
507
+ "Fixturepaw",
508
+ "Mockwhisker",
509
+ "Stubtail",
510
+ "Seedpup",
511
+ "Testbiscuit",
512
+ "Placepurr",
513
+ "Synthfur",
514
+ "Dummymane",
515
+ "Examplehoof",
516
+ "Fauxfluff",
517
+ "Samplesnout",
518
+ "Voidwag"
519
+ ]);
520
+ var MICROCHIP_TEST_PREFIX = "999";
521
+ function petName(rng) {
522
+ return rng.pick(SYNTHETIC_PET_NAMES);
523
+ }
524
+ function animalBreed(rng) {
525
+ const picked = rng.pick(SYNTHETIC_ANIMAL_SPECIES);
526
+ const { breeds, ...bare } = picked;
527
+ return { species: Object.freeze(bare), breed: rng.pick(breeds) };
528
+ }
529
+ function microchip(rng) {
530
+ return `${MICROCHIP_TEST_PREFIX}${rng.digits(12)}`;
531
+ }
532
+ function animalBirthDate(rng, onDate) {
533
+ const year = Number(onDate.slice(0, 4));
534
+ const month = Number(onDate.slice(4, 6)) - 1;
535
+ const day = Number(onDate.slice(6, 8));
536
+ const latest = Date.UTC(year, month, day);
537
+ const earliest = Date.UTC(year - 20, month, day);
538
+ const span = Math.round((latest - earliest) / 864e5);
539
+ const born = new Date(latest - rng.int(0, span) * 864e5);
540
+ return born.toISOString().slice(0, 10).replace(/-/g, "");
541
+ }
542
+
448
543
  // src/safe/index.ts
449
544
  var safe = Object.freeze({
450
545
  ssn,
@@ -458,7 +553,11 @@ var safe = Object.freeze({
458
553
  address,
459
554
  dateYmd,
460
555
  npi,
461
- dea
556
+ dea,
557
+ petName,
558
+ animalBreed,
559
+ microchip,
560
+ animalBirthDate
462
561
  });
463
562
  var SYSTEM = Object.freeze({
464
563
  /** FHIR `administrative-gender` (`Patient.gender`). */
@@ -853,18 +952,52 @@ function resolveMix(allowed, requested, fallback) {
853
952
  if (requested === void 0) return fallback;
854
953
  return requested.map((entry) => resolveKind(allowed, entry));
855
954
  }
955
+ function requireSelection(selection) {
956
+ if (selection.length === 0) throw new SynthError(SYNTH_FATAL_CODES.SYNTH_EMPTY_POOL);
957
+ return selection;
958
+ }
856
959
  function defaultHost() {
857
960
  if (typeof import.meta.resolve !== "function") return { require: __require, resolve: void 0 };
858
961
  return { require: createRequire(import.meta.url), resolve: (s) => import.meta.resolve(s) };
859
962
  }
963
+ function isDenoResolveRefusal(error) {
964
+ return error instanceof TypeError && !("code" in error) && error.message === "Invalid arguments";
965
+ }
860
966
  function isRequireEsmRefusal(error) {
861
967
  return error?.code === "ERR_REQUIRE_ESM";
862
968
  }
969
+ function peerPackage(specifier) {
970
+ const name2 = specifier.split("/").slice(0, 2).join("/");
971
+ return Object.hasOwn(SYNTH_PEER_MESSAGES, name2) ? name2 : void 0;
972
+ }
973
+ function isPeerNotFound(error, specifier, peer) {
974
+ const { code, message } = error ?? {};
975
+ if (typeof message !== "string") return false;
976
+ if (code === "ERR_MODULE_NOT_FOUND") return message.startsWith(`Cannot find package '${peer}'`);
977
+ if (code === "MODULE_NOT_FOUND") return message.startsWith(`Cannot find module '${specifier}'`);
978
+ return false;
979
+ }
863
980
  function loadPeer(specifier, host) {
981
+ try {
982
+ return loadFrom(specifier, host);
983
+ } catch (error) {
984
+ const peer = peerPackage(specifier);
985
+ if (peer === void 0 || !isPeerNotFound(error, specifier, peer)) throw error;
986
+ throw new SynthError(SYNTH_FATAL_CODES.SYNTH_PEER_NOT_INSTALLED, { peer, cause: error });
987
+ }
988
+ }
989
+ function loadFrom(specifier, host) {
864
990
  if (host.resolve === void 0) return host.require(specifier);
865
- const entry = fileURLToPath(host.resolve(specifier));
991
+ let url;
866
992
  try {
867
- return host.require(entry);
993
+ url = host.resolve(specifier);
994
+ } catch (error) {
995
+ if (!isDenoResolveRefusal(error)) throw error;
996
+ return host.require(specifier);
997
+ }
998
+ if (!url.startsWith("file:")) return host.require(specifier);
999
+ try {
1000
+ return host.require(fileURLToPath(url));
868
1001
  } catch (error) {
869
1002
  if (!isRequireEsmRefusal(error)) throw error;
870
1003
  return host.require(specifier);
@@ -1003,7 +1136,7 @@ function defineSynthProfile(spec) {
1003
1136
  name: spec.name,
1004
1137
  ...spec.givenNames ? { givenNames: Object.freeze([...spec.givenNames]) } : {},
1005
1138
  ...spec.familyNames ? { familyNames: Object.freeze([...spec.familyNames]) } : {},
1006
- quirks: Object.freeze([...spec.quirks ?? []])
1139
+ ...spec.quirks !== void 0 ? { quirks: Object.freeze([...spec.quirks]) } : {}
1007
1140
  });
1008
1141
  }
1009
1142
 
@@ -1045,6 +1178,7 @@ function assertIntendedWarnings(intendedWarnings, bareWarnings) {
1045
1178
  }
1046
1179
  }
1047
1180
  function validateProfileQuirks(profile, registry, format) {
1181
+ if (profile.quirks === void 0) return Object.keys(registry);
1048
1182
  for (const name2 of profile.quirks) resolveQuirk(registry, format, name2);
1049
1183
  return profile.quirks;
1050
1184
  }
@@ -1159,8 +1293,9 @@ var ALL_CCDA_QUIRKS = Object.freeze(
1159
1293
  Object.keys(CCDA_QUIRKS)
1160
1294
  );
1161
1295
  function ccdaQuirkCorpus(options) {
1162
- const quirks = options.profile ? validateProfileQuirks(options.profile, CCDA_QUIRKS, "ccda") : options.quirks ?? ALL_CCDA_QUIRKS;
1163
- const names = quirks.length > 0 ? quirks : ALL_CCDA_QUIRKS;
1296
+ const names = requireSelection(
1297
+ options.profile?.quirks !== void 0 ? validateProfileQuirks(options.profile, CCDA_QUIRKS, "ccda") : options.quirks ?? ALL_CCDA_QUIRKS
1298
+ );
1164
1299
  for (const name2 of names) resolveQuirk(CCDA_QUIRKS, "ccda", name2);
1165
1300
  const documentType = options.documentType ?? "ccd";
1166
1301
  const count = options.count ?? names.length;
@@ -1188,10 +1323,10 @@ var ALL_KINDS = Object.freeze(["ccd", "referralNote"]);
1188
1323
  var DEFAULT_MIX = ALL_KINDS;
1189
1324
  function ccdaCorpus(options) {
1190
1325
  const { seed, count = 1, verify = true } = options;
1191
- const mix = resolveMix(ALL_KINDS, options.mix, DEFAULT_MIX);
1326
+ const mix = requireSelection(resolveMix(ALL_KINDS, options.mix, DEFAULT_MIX));
1192
1327
  const seedStream = createRng(seed);
1193
1328
  const artifacts = Array.from({ length: count }, (_unused, i) => {
1194
- const documentType = mix[i % mix.length] ?? "ccd";
1329
+ const documentType = mix[i % mix.length];
1195
1330
  const docSeed = seedStream.nextUint32();
1196
1331
  const rt = ccdaCorpusArtifact(generateCcda({ seed: docSeed, documentType }), verify);
1197
1332
  return {