@cosyte/synth 0.1.9 → 0.1.11

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 +39 -0
  2. package/README.md +39 -19
  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 +147 -17
  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 +147 -17
  10. package/dist/astm/index.mjs.map +1 -1
  11. package/dist/ccda/index.cjs +147 -17
  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 +147 -17
  16. package/dist/ccda/index.mjs.map +1 -1
  17. package/dist/deid/index.cjs +537 -30
  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 +537 -30
  22. package/dist/deid/index.mjs.map +1 -1
  23. package/dist/fhir/index.cjs +142 -14
  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 +142 -14
  28. package/dist/fhir/index.mjs.map +1 -1
  29. package/dist/hl7/index.cjs +566 -39
  30. package/dist/hl7/index.cjs.map +1 -1
  31. package/dist/hl7/index.d.cts +416 -116
  32. package/dist/hl7/index.d.ts +416 -116
  33. package/dist/hl7/index.mjs +557 -40
  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 +142 -14
  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 +142 -14
  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 +142 -14
  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 +142 -14
  54. package/dist/x12/index.mjs.map +1 -1
  55. package/package.json +9 -5
package/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.11
4
+
5
+ ### Patch Changes
6
+
7
+ - c913601: Docs: no example prints a whole message or a value that identifies a person any more, and a new section, What is safe to log, lists the result fields never to log. No code changes.
8
+
9
+ The HL7 v2 generators' TSDoc examples, the README, the quickstart and the HL7 example program now print the message type, codes and booleans, and show an identifying value only in a comment marked synthetic. The README and the installation page say to pin the exact version, since below 1.0 a breaking change can ship in a patch. What is safe to log, on the fatal codes page, names `content`, `original`, `deidentified` and a sentinel's `token` as the fields that hold a whole artifact or a planted value. Nothing a caller does changes.
10
+
11
+ - 70c322c: Docs: the golden fixture page is now `pin-a-golden-fixture` (it was `guides-overview`), the limitations page moves to Reference, and the reason the deid pairing loop reads only the patient fields has its own explanation page. The README links follow. No code changes.
12
+
13
+ ## 0.1.10
14
+
15
+ ### Patch Changes
16
+
17
+ - 3156ab8: The README's Status section now points at the page that lists what is not generated yet, instead of repeating that list. Its Documentation section links the new pages: a tutorial for each of FHIR, C-CDA, X12, NCPDP and ASTM beside the HL7 v2 one, a reference of every generator option, corpus kind and profile, a page on how `@cosyte/synth` differs from Synthea, faker-style libraries and other healthcare generators, and a glossary. No code changed, and generated output is unchanged.
18
+ - 3156ab8: An empty selection list is now refused with `SYNTH_EMPTY_POOL`, in every format, before anything is generated and whatever the `count`. That covers an empty `mix` in `hl7Corpus`, `fhirCorpus`, `ccdaCorpus`, `x12Corpus`, `ncpdpCorpus` and `astmCorpus`, an empty `triggers` list in `hl7Corpus`, and an empty `quirks` list, or a `profile` whose `quirks` is an empty list, in `hl7QuirkCorpus`, `ccdaQuirkCorpus` and `astmQuirkCorpus`. Before, an empty `mix` with a count above zero quietly filled the corpus with one hard-coded kind (`hl7Corpus({ seed, mix: [] })` returned one `ADT^A01`), an empty `mix` with no count in the X12, NCPDP and ASTM corpora returned an empty corpus, and an empty `quirks` list was read as every quirk. What a caller changes: leave `mix`, `triggers`, `quirks` and `profile` out to get the default, instead of passing an empty list. Output for every non-empty selection is unchanged, byte for byte.
19
+ - 3156ab8: HL7 v2 now generates ten more kinds, each built through `@cosyte/hl7`, read back with zero warnings, and fixed by its seed: `ACK`, `ADT^A02` (transfer), `ADT^A03` (discharge), `ADT^A11` (cancel admit), `ADT^A13` (cancel discharge), `ADT^A28` and `ADT^A31` (add and update person information), `MDM^T02` (a document and its text), `DFT^P03` (a posted charge) and `OML^O21` (a laboratory order). New exports from `@cosyte/synth/hl7`: `generateAck`, which answers any message (generated or your own) with an `AA`, `AE` or `AR` acknowledgment whose MSA-2 echoes its control id; `generateMdm`, `generateDft` and `generateOml`; and `HL7_MESSAGE_KINDS`, every kind `generateHl7` and `hl7Corpus` accept. `generateAdt` and the `triggers` option take the six new ADT triggers. The new kinds name their message structure in MSH-9.3. `generateAck` refuses an inbound message with no MSH-10 with the new fatal code `SYNTH_ACK_UNCORRELATED`.
20
+
21
+ Every message is still stamped HL7 v2.5: there is no option to stamp another version, because `@cosyte/hl7` has no per-version structure check to prove such a message valid. `RDE^O11` is not generated yet, because `@cosyte/hl7` publishes no structure for it.
22
+
23
+ Output for every existing kind is unchanged, byte for byte, and the default `hl7Corpus` mix is still the first seven kinds, so a seed's default corpus is unchanged: name the new kinds in `mix`, or pass `HL7_MESSAGE_KINDS`, to include them. What a caller changes: nothing, unless your code switches exhaustively over `Hl7MessageKind`, `AdtTrigger` or `SynthFatalCode`, each of which now has more members; add cases for them, or a default branch.
24
+
25
+ - 3156ab8: HL7 v2 `generateAdt` and `generateOru` take a new option, `subject`: `"person"` (the default) or `"animal"`. An animal message carries the pet's name in PID-5 (the owner's family name, then the pet's), the record number and a 15-digit microchip with identifier type `MCN` in PID-3, a birth date from 0 to 20 years before the message date in PID-7, the species in PID-35 and the breed in PID-36 as `code^text^99COSYTE`, and the owner in an `NK1` after the `PID` with relationship `OWN^Owner^HL70063`. PID-19 stays empty. Every trigger and `ORU^R01` read back through `@cosyte/hl7` with zero warnings.
26
+
27
+ Species and breed come from our own short list (dog, cat, horse and cattle, two or three breeds each) under the local code system `99COSYTE`: no SNOMED CT, VetSCT or VeNom content. Microchips begin `999`. We have not verified three things against the standards: that `999` is outside every country and manufacturer code ISO 11784 gives a real chip, that `MCN` is the HL7 table 0203 identifier type for a microchip, and that `OWN` is a value of HL7 table 0063. We emit them as written; the how-to marks each as not verified.
28
+
29
+ New exports from `@cosyte/synth`: `petName`, `animalBreed`, `microchip`, `animalBirthDate` (also on `safe`), `isSyntheticMicrochip`, `SYNTHETIC_PET_NAMES`, `SYNTHETIC_ANIMAL_SPECIES`, `ANIMAL_CODE_SYSTEM`, `MICROCHIP_TEST_PREFIX`, and the types `AnimalCode`, `AnimalSpecies` and `AnimalBreedDraw`. New exports from `@cosyte/synth/hl7`: `animalIdentity`, `animalPidSegment`, `ownerNk1Segment`, `MICROCHIP_IDENTIFIER_TYPE`, `OWNER_RELATIONSHIP`, and the types `AnimalIdentity`, `AnimalOwner` and `Hl7Subject`. The animal identity is exported from the package root and `@cosyte/synth/hl7` because synth has no `./arbitraries` subpath yet; adding one, with a fast-check arbitrary for the animal identity, is left for a later change.
30
+
31
+ Output with `subject` left out is unchanged, byte for byte. What a caller changes: nothing, unless your code enumerates the keys of `safe`, which now has four more.
32
+
33
+ - 3156ab8: A profile that leaves `quirks` out is no longer read as an empty quirk list. `defineSynthProfile({ name, givenNames })` now returns a profile with no `quirks` field, and `hl7QuirkCorpus`, `ccdaQuirkCorpus` and `astmQuirkCorpus` given such a profile use their `quirks` option, or every quirk when that is left out too, the same as with no profile. A profile whose `quirks` is an empty list still selects nothing and throws `SYNTH_EMPTY_POOL`. Before, `defineSynthProfile` filled a missing `quirks` with an empty list, so a profile made only for its name pools threw `SYNTH_EMPTY_POOL` in a quirk corpus. The `defineSynthProfile` example passed `quirks: []`, which a quirk corpus refuses; it now builds a profile of name pools that leaves `quirks` out. `validateProfileQuirks` now returns every quirk in the registry for a profile that leaves `quirks` out. What a caller changes: `SynthProfile.quirks` is now optional, so code that reads it directly handles `undefined` (`profile.quirks ?? []` keeps the old reading). Output for every profile that sets `quirks` is unchanged, byte for byte.
34
+ - 3156ab8: A generator whose parser is not installed now throws a `SynthError` with the new code `SYNTH_PEER_NOT_INSTALLED`, the same from the ES module build and the CommonJS build. Its message names the package and the command that installs it ("@cosyte/hl7 is not installed, and this generator builds through it. Install it beside @cosyte/synth: npm install @cosyte/hl7"), `err.peer` is the package, and `err.cause` is the error Node.js raised. Before, the CommonJS build threw `require`'s "Cannot find module '@cosyte/hl7'" (`MODULE_NOT_FOUND`), and the ES module build threw the same until the change below, which would have made it throw the resolver's "Cannot find package '@cosyte/hl7' imported from ..." (`ERR_MODULE_NOT_FOUND`). A caller that matched either message, or either Node.js code, checks `err.code === "SYNTH_PEER_NOT_INSTALLED"` instead, or reads `err.cause.code`. A missing dependency of an installed parser is not reported as the parser missing: that error is thrown as Node.js raised it. `SynthError` takes an optional second argument, `{ cause, peer }`, and the package root exports `SYNTH_PEER_MESSAGES` (one fixed message for each parser package) and the types `SynthPeer` and `SynthErrorOptions`.
35
+
36
+ A generator falls back to loading a parser by name only where `import.meta.resolve` throws the error Deno's test runner throws (`TypeError: Invalid arguments`), or answers with a URL that is not a `file:` URL. Before, any error from resolving fell back, so on Node.js a resolve that failed for another reason could load the CommonJS copy of a parser, whose classes `instanceof` does not recognize. Now such an error is thrown, and so is a `file:` URL that names no file path, which also used to fall back. The Deno case matches on that error's message, and no CI job runs under Deno yet, so it is tested only against a stand-in that throws the same error.
37
+
38
+ The synthetic-safety providers are now tested against the published ranges they cite, and three functions in that module read more simply with the same results. `isSyntheticPhone` checks that the number's digits end in `55501NN` with one pattern where it used two, and gives the same answer for every input. `luhnMod10` tests a doubled digit for 10 or more before subtracting 9, where it tested for more than 9. `ssn(rng, block?)` declares its second parameter optional instead of giving it a default; leaving it out still draws the never-issued space, and its type is unchanged. Generated output is unchanged.
39
+
40
+ - 3156ab8: The release version script now regenerates the generated API reference, so the version shown on the API page matches the `VERSION` constant after a version bump. No change to the package contents or its API.
41
+
3
42
  ## 0.1.9
4
43
 
5
44
  ### Patch Changes
package/README.md CHANGED
@@ -34,10 +34,10 @@ date, phone, and address from a **guaranteed-non-colliding synthetic source**. I
34
34
  ## Status
35
35
 
36
36
  The 0.1 line, published to npm. The version shown on the npm package page is the one
37
- that is live; this page never repeats it. Below 1.0, a breaking change raises the minor version and the
38
- changelog says what broke. The generator is **feature-complete**: the
37
+ that is live; this page never repeats it. Every release is a patch, a breaking change included, and
38
+ the changelog says what broke and what a caller must change. The generator is **feature-complete**: the
39
39
  seeded-PRNG core, the synthetic-safety providers, and the round-trip harness; **generation across all
40
- six formats**, HL7 v2 (`ADT`/`ORU`/`ORM`/`SIU`/`VXU`), FHIR R4 / US Core (the full clinical
40
+ six formats**, HL7 v2 (`ADT`/`ORU`/`ORM`/`SIU`/`VXU`/`MDM`/`DFT`/`OML` and `ACK`), FHIR R4 / US Core (the full clinical
41
41
  set + `collection`/`transaction`/`document` Bundles), C-CDA R2.1 (CCD + Referral Note), X12 005010
42
42
  (837P/I/D, 835, 271), NCPDP (SCRIPT NewRx / RxRenewal / RxChange + Telecom B1/B2/B3), and ASTM (E1394
43
43
  record reports + E1381 framing), each built **through its parser's own builder/serializer**; **vendor-quirk
@@ -46,15 +46,22 @@ each round-tripping to exactly the intended parser warning (the intended-warning
46
46
  **`@cosyte/deid` pairing loop**, a closed-loop co-validation harness that de-identifies generated output
47
47
  and proves every planted synthetic PHI sentinel is removed.
48
48
 
49
- **Deferred (honestly out of scope for now):** quirk recipes for **FHIR / X12 / NCPDP** (and any quirk
50
- needing a private vendor corpus); the X12 **270** eligibility _request_ and NCPDP SCRIPT lifecycle
51
- **responses** (both track the parsers' builder surfaces); **NCPDP SCRIPT / ASTM / DICOM** de-identify
52
- pairing (no `@cosyte/deid` adapter, or not generated); and optional **Synthea** clinical-content ingestion.
49
+ What is not generated yet (quirks for some formats, a few transactions, some pairing loops) is
50
+ listed in one place, [what it does and does not do](https://docs.cosyte.com/synth/limitations),
51
+ so this page does not repeat it.
53
52
 
54
53
  ## Install
55
54
 
56
55
  ```bash
57
- npm install @cosyte/synth @cosyte/hl7 @cosyte/fhir @cosyte/ccda @cosyte/x12 @cosyte/ncpdp @cosyte/astm
56
+ npm install @cosyte/synth @cosyte/hl7 @cosyte/fhir @cosyte/ccda @cosyte/x12 @cosyte/ncpdp @cosyte/astm @cosyte/deid
57
+ ```
58
+
59
+ **Pin the exact version.** Below 1.0 a breaking change can ship in a patch release, so a caret
60
+ range (`^0.1.x`) does not protect you from one. Install with `--save-exact` (pnpm takes the same flag;
61
+ Yarn's is `--exact`) and read the changelog before you move the pin:
62
+
63
+ ```bash
64
+ npm install --save-exact @cosyte/synth
58
65
  ```
59
66
 
60
67
  Node.js 22.12 or later, tested on 22.12, 22, 24, and 26. A generator loads its parser on first use
@@ -68,10 +75,14 @@ Rollup and Vite `external`, webpack `externals`) and install them beside the bun
68
75
  `@cosyte/hl7`, `@cosyte/fhir`, `@cosyte/ccda`, `@cosyte/x12`, `@cosyte/ncpdp`, `@cosyte/astm`, and
69
76
  `@cosyte/deid` are **optional peer dependencies**, each needed only for its subpath (`@cosyte/synth/hl7`,
70
77
  `@cosyte/synth/fhir`, `@cosyte/synth/ccda`, `@cosyte/synth/x12`, `@cosyte/synth/ncpdp`,
71
- `@cosyte/synth/astm`, and `@cosyte/synth/deid`: the last loads `@cosyte/deid` **plus** the five
72
- parsers it pairs, `@cosyte/hl7`, `@cosyte/fhir`, `@cosyte/ccda`, `@cosyte/x12`, and `@cosyte/ncpdp`,
73
- as soon as it is imported): install only the packages whose fixtures you generate. The package core has
74
- **zero third-party runtime dependencies**.
78
+ `@cosyte/synth/astm`, and `@cosyte/synth/deid`, whose peer is `@cosyte/deid`). Importing a subpath
79
+ loads no parser: each loads the first time a generator needs it, so install only the packages whose
80
+ fixtures you generate. A `@cosyte/synth/deid` loop also loads the parser of its own format, so
81
+ `hl7DeidLoop` needs `@cosyte/deid` and `@cosyte/hl7`. A parser that is missing throws
82
+ `SYNTH_PEER_NOT_INSTALLED` on first use, except under Bun in a project with no `node_modules` folder,
83
+ where Bun downloads and loads it instead
84
+ ([Troubleshooting](https://docs.cosyte.com/synth/troubleshooting#a-parser-is-not-installed)). The
85
+ package core has **zero third-party runtime dependencies**.
75
86
 
76
87
  ## Usage
77
88
 
@@ -80,8 +91,10 @@ on [docs.cosyte.com/synth](https://docs.cosyte.com/synth).
80
91
 
81
92
  ### Generate a spec-clean HL7 v2 message
82
93
 
83
- The HL7 v2 set covers `ADT` (`A01`/`A04`/`A08`), `ORU^R01`, `ORM^O01`, `SIU^S12`, and `VXU^V04`:
84
- each built through `@cosyte/hl7`'s `buildMessage`, so it is spec-clean by construction.
94
+ The HL7 v2 set covers `ADT` (`A01`/`A02`/`A03`/`A04`/`A08`/`A11`/`A13`/`A28`/`A31`), `ORU^R01`,
95
+ `ORM^O01`, `SIU^S12`, `VXU^V04`, `MDM^T02`, `DFT^P03`, `OML^O21`, and an `ACK` for any of them:
96
+ each built through `@cosyte/hl7`'s `buildMessage`, so it is spec-clean by construction. Every
97
+ message is stamped HL7 v2.5.
85
98
 
86
99
  ```ts
87
100
  import { generateAdt, generateOru, generateHl7, hl7Corpus, roundTrip } from "@cosyte/synth/hl7";
@@ -99,7 +112,8 @@ const corpus = hl7Corpus({ seed: 42, count: 7 }); // one of each family, cycled
99
112
  console.log(corpus.artifacts.every((a) => a.warnings.length === 0)); // true, all spec-clean
100
113
 
101
114
  // Dispatch by kind when the message type is data:
102
- console.log(roundTrip(generateHl7("VXU^V04", 12345)).content.split("\r")[0]); // its MSH segment
115
+ const vxu = roundTrip(generateHl7("VXU^V04", 12345)).content;
116
+ console.log(vxu.split("\r")[0]?.split("|")[8]); // VXU^V04, the type from MSH-9, never the whole segment
103
117
  ```
104
118
 
105
119
  ### Generate a spec-clean FHIR R4 / US Core resource
@@ -375,14 +389,20 @@ isSyntheticNpi(safe.npi(rng)); // true, always a deliberately-invalid-Luhn NPI (
375
389
  The full reference is on [docs.cosyte.com/synth](https://docs.cosyte.com/synth):
376
390
 
377
391
  - [Installation](https://docs.cosyte.com/synth/installation), the [quickstart](https://docs.cosyte.com/synth/quickstart), and
378
- a tutorial: [test your HL7 handling with generated fixtures](https://docs.cosyte.com/synth/test-hl7-handling-with-fixtures).
392
+ a tutorial per format: test your [HL7 v2](https://docs.cosyte.com/synth/test-hl7-handling-with-fixtures),
393
+ [FHIR](https://docs.cosyte.com/synth/test-fhir-handling-with-fixtures), [C-CDA](https://docs.cosyte.com/synth/test-ccda-handling-with-fixtures),
394
+ [X12](https://docs.cosyte.com/synth/test-x12-handling-with-fixtures), [NCPDP](https://docs.cosyte.com/synth/test-ncpdp-handling-with-fixtures),
395
+ and [ASTM](https://docs.cosyte.com/synth/test-astm-handling-with-fixtures) handling with generated fixtures.
379
396
  - How-to guides: [HL7 v2](https://docs.cosyte.com/synth/guides-hl7), [FHIR R4 and US Core](https://docs.cosyte.com/synth/guides-fhir),
380
397
  [C-CDA](https://docs.cosyte.com/synth/guides-ccda), [X12](https://docs.cosyte.com/synth/guides-x12), [NCPDP](https://docs.cosyte.com/synth/guides-ncpdp),
381
398
  [ASTM](https://docs.cosyte.com/synth/guides-astm), [vendor quirks](https://docs.cosyte.com/synth/guides-quirks),
382
399
  [the `@cosyte/deid` pairing loop](https://docs.cosyte.com/synth/guides-deid), [synthetic values](https://docs.cosyte.com/synth/draw-synthetic-values),
383
- and [pinning a golden fixture](https://docs.cosyte.com/synth/guides-overview).
384
- - Explanation: [how it works](https://docs.cosyte.com/synth/concepts-archetype) and [what it does and does not do](https://docs.cosyte.com/synth/limitations).
385
- - Reference: [fatal codes](https://docs.cosyte.com/synth/fatal-codes), the [quirk catalog](https://docs.cosyte.com/synth/quirk-catalog), and the API reference.
400
+ and [pinning a golden fixture](https://docs.cosyte.com/synth/pin-a-golden-fixture).
401
+ - Explanation: [how it works](https://docs.cosyte.com/synth/concepts-archetype), [why the removal check reads only the patient fields](https://docs.cosyte.com/synth/why-deid-checks-patient-fields),
402
+ and [Synthea, faker and other generators](https://docs.cosyte.com/synth/synthea-faker-and-other-generators).
403
+ - Reference: [generator options, kinds and profiles](https://docs.cosyte.com/synth/generator-options), [fatal codes](https://docs.cosyte.com/synth/fatal-codes),
404
+ the [quirk catalog](https://docs.cosyte.com/synth/quirk-catalog), the [glossary](https://docs.cosyte.com/synth/glossary), [what it does and does not do](https://docs.cosyte.com/synth/limitations), and the
405
+ [API reference](https://github.com/cosyte/synth/blob/main/docs-content/api/README.md), generated from the source.
386
406
  - [Troubleshooting](https://docs.cosyte.com/synth/troubleshooting), [testing and quality](https://docs.cosyte.com/synth/testing-and-quality), and [performance](https://docs.cosyte.com/synth/performance).
387
407
  - [`CHANGELOG.md`](https://github.com/cosyte/synth/blob/main/CHANGELOG.md): every release.
388
408
 
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The two helpers that build the animal species and breed list in `./animal.ts`. They live in a
3
+ * module of their own, which the package does not export, so a test can call them directly and
4
+ * every mutant in them meets a test of its own.
5
+ *
6
+ * @module
7
+ */
8
+ /** A species or breed: our own code, its plain text, and the local code system. */
9
+ interface AnimalCode {
10
+ /** Our own short code, for example `DOG` or `DOG-BGL`. */
11
+ readonly code: string;
12
+ /** The plain text, for example `Dog` or `Beagle`. */
13
+ readonly text: string;
14
+ /** Always {@link ANIMAL_CODE_SYSTEM}. */
15
+ readonly system: string;
16
+ }
17
+ /** A species and the breeds this package draws for it. */
18
+ interface AnimalSpecies extends AnimalCode {
19
+ /** The breeds of this species, at least one. */
20
+ readonly breeds: readonly AnimalCode[];
21
+ }
22
+ /**
23
+ * The local code system for species and breed codes, in the HL7 table 0396 form for a local
24
+ * system (`99` followed by letters). The codes under it are ours and name nothing outside this
25
+ * package.
26
+ */
27
+ declare const ANIMAL_CODE_SYSTEM = "99COSYTE";
28
+
29
+ export { ANIMAL_CODE_SYSTEM as b };
30
+ export type { AnimalCode as A, AnimalSpecies as a };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The two helpers that build the animal species and breed list in `./animal.ts`. They live in a
3
+ * module of their own, which the package does not export, so a test can call them directly and
4
+ * every mutant in them meets a test of its own.
5
+ *
6
+ * @module
7
+ */
8
+ /** A species or breed: our own code, its plain text, and the local code system. */
9
+ interface AnimalCode {
10
+ /** Our own short code, for example `DOG` or `DOG-BGL`. */
11
+ readonly code: string;
12
+ /** The plain text, for example `Dog` or `Beagle`. */
13
+ readonly text: string;
14
+ /** Always {@link ANIMAL_CODE_SYSTEM}. */
15
+ readonly system: string;
16
+ }
17
+ /** A species and the breeds this package draws for it. */
18
+ interface AnimalSpecies extends AnimalCode {
19
+ /** The breeds of this species, at least one. */
20
+ readonly breeds: readonly AnimalCode[];
21
+ }
22
+ /**
23
+ * The local code system for species and breed codes, in the HL7 table 0396 form for a local
24
+ * system (`99` followed by letters). The codes under it are ours and name nothing outside this
25
+ * package.
26
+ */
27
+ declare const ANIMAL_CODE_SYSTEM = "99COSYTE";
28
+
29
+ export { ANIMAL_CODE_SYSTEM as b };
30
+ export type { AnimalCode as A, AnimalSpecies as a };
@@ -73,7 +73,11 @@ var SYNTH_FATAL_CODES = Object.freeze({
73
73
  SYNTH_INVALID_DECIMAL: "SYNTH_INVALID_DECIMAL",
74
74
  /** An integer range was requested with its maximum below its minimum. Fatal. */
75
75
  SYNTH_INVALID_RANGE: "SYNTH_INVALID_RANGE",
76
- /** A value was drawn from an empty pool. Fatal, never a fabricated substitute. */
76
+ /**
77
+ * A value was drawn from an empty pool. Fatal, never a fabricated substitute. A corpus given an
78
+ * empty `mix`, `triggers` or `quirks` list (or a profile whose `quirks` is an empty list) raises it
79
+ * too: there is no kind to cycle through, and an empty list is never read as the default.
80
+ */
77
81
  SYNTH_EMPTY_POOL: "SYNTH_EMPTY_POOL",
78
82
  /** A `defineSynthProfile` spec was not usable (a missing or blank `name`). Fatal. */
79
83
  SYNTH_INVALID_PROFILE: "SYNTH_INVALID_PROFILE",
@@ -92,7 +96,21 @@ var SYNTH_FATAL_CODES = Object.freeze({
92
96
  * apart without matching message text. Fatal, and raised **before** anything is generated: a
93
97
  * mislabelled artifact is worse than no artifact.
94
98
  */
95
- SYNTH_PROFILE_NOT_GENERATED: "SYNTH_PROFILE_NOT_GENERATED"
99
+ SYNTH_PROFILE_NOT_GENERATED: "SYNTH_PROFILE_NOT_GENERATED",
100
+ /**
101
+ * The sibling parser a generator builds through is not installed. Raised on the first call that
102
+ * needs it, by both builds, only when the missing package is the parser itself: a missing
103
+ * dependency of an installed parser is thrown as Node.js raised it. The error carries the parser's
104
+ * package name on `peer`, a message from {@link SYNTH_PEER_MESSAGES} that names its install
105
+ * command, and the error Node.js raised on `cause`.
106
+ */
107
+ SYNTH_PEER_NOT_INSTALLED: "SYNTH_PEER_NOT_INSTALLED",
108
+ /**
109
+ * An HL7 v2 `ACK` was asked for an inbound message that carries no MSH-10 message control id.
110
+ * MSA-2 must echo that id, and an acknowledgment that cannot be matched to the message it answers
111
+ * is not a fixture worth having, so nothing is generated.
112
+ */
113
+ SYNTH_ACK_UNCORRELATED: "SYNTH_ACK_UNCORRELATED"
96
114
  });
97
115
  var SYNTH_FATAL_MESSAGES = Object.freeze({
98
116
  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.",
@@ -105,18 +123,38 @@ var SYNTH_FATAL_MESSAGES = Object.freeze({
105
123
  SYNTH_EMPTY_POOL: "A value was drawn from an empty pool.",
106
124
  SYNTH_INVALID_PROFILE: "defineSynthProfile requires a non-empty string name.",
107
125
  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.",
108
- 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."
126
+ 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.",
127
+ SYNTH_PEER_NOT_INSTALLED: "A sibling @cosyte parser this generator builds through is not installed. Install it beside @cosyte/synth.",
128
+ SYNTH_ACK_UNCORRELATED: (
129
+ // Stryker disable next-line all: a module-level table runs once, at import; the docs test holds it.
130
+ "The inbound message has no MSH-10 control id for an ACK to echo in MSA-2."
131
+ )
132
+ });
133
+ var SYNTH_PEER_MESSAGES = Object.freeze({
134
+ "@cosyte/hl7": "@cosyte/hl7 is not installed, and this generator builds through it. Install it beside @cosyte/synth: npm install @cosyte/hl7",
135
+ "@cosyte/fhir": "@cosyte/fhir is not installed, and this generator builds through it. Install it beside @cosyte/synth: npm install @cosyte/fhir",
136
+ "@cosyte/ccda": "@cosyte/ccda is not installed, and this generator builds through it. Install it beside @cosyte/synth: npm install @cosyte/ccda",
137
+ "@cosyte/x12": "@cosyte/x12 is not installed, and this generator builds through it. Install it beside @cosyte/synth: npm install @cosyte/x12",
138
+ "@cosyte/ncpdp": "@cosyte/ncpdp is not installed, and this generator builds through it. Install it beside @cosyte/synth: npm install @cosyte/ncpdp",
139
+ "@cosyte/astm": "@cosyte/astm is not installed, and this generator builds through it. Install it beside @cosyte/synth: npm install @cosyte/astm",
140
+ "@cosyte/deid": "@cosyte/deid is not installed, and this generator builds through it. Install it beside @cosyte/synth: npm install @cosyte/deid"
109
141
  });
110
142
  var SynthError = class extends Error {
111
143
  /** The stable fatal code. */
112
144
  code;
113
145
  /**
114
146
  * @param code - The stable {@link SynthFatalCode}. The message comes from the frozen registry.
147
+ * @param options - The error that caused this one, and for a missing parser its package.
115
148
  */
116
- constructor(code) {
117
- super(SYNTH_FATAL_MESSAGES[code]);
149
+ constructor(code, options = {}) {
150
+ const peer = code === SYNTH_FATAL_CODES.SYNTH_PEER_NOT_INSTALLED ? options.peer : void 0;
151
+ super(
152
+ peer === void 0 ? SYNTH_FATAL_MESSAGES[code] : SYNTH_PEER_MESSAGES[peer],
153
+ "cause" in options ? { cause: options.cause } : void 0
154
+ );
118
155
  this.name = "SynthError";
119
156
  this.code = code;
157
+ if (peer !== void 0) Object.defineProperty(this, "peer", { value: peer, enumerable: true });
120
158
  }
121
159
  };
122
160
 
@@ -224,7 +262,7 @@ function luhnMod10(digits) {
224
262
  if (d < 0 || d > 9) continue;
225
263
  if (double) {
226
264
  d *= 2;
227
- if (d > 9) d -= 9;
265
+ if (d >= 10) d -= 9;
228
266
  }
229
267
  sum += d;
230
268
  double = !double;
@@ -321,7 +359,7 @@ var SYNTHETIC_CITY_NAMES = Object.freeze([
321
359
  ]);
322
360
 
323
361
  // src/safe/providers.ts
324
- function ssn(rng, block = "never-issued") {
362
+ function ssn(rng, block) {
325
363
  if (block === "advertising") {
326
364
  return `987-00-432${String(rng.int(0, 9))}`;
327
365
  }
@@ -448,6 +486,63 @@ var US_STATES = Object.freeze([
448
486
  "WY"
449
487
  ]);
450
488
 
489
+ // src/safe/animal-code.ts
490
+ var ANIMAL_CODE_SYSTEM = "99COSYTE";
491
+ function animalCode(value, text) {
492
+ return Object.freeze({ code: value, text, system: ANIMAL_CODE_SYSTEM });
493
+ }
494
+ function animalSpecies(value, text, breeds) {
495
+ return Object.freeze({ ...animalCode(value, text), breeds: Object.freeze([...breeds]) });
496
+ }
497
+
498
+ // src/safe/animal.ts
499
+ var SYNTHETIC_ANIMAL_SPECIES = Object.freeze([
500
+ animalSpecies("DOG", "Dog", [
501
+ animalCode("DOG-LAB", "Labrador Retriever"),
502
+ animalCode("DOG-BGL", "Beagle"),
503
+ animalCode("DOG-MIX", "Mixed breed dog")
504
+ ]),
505
+ animalSpecies("CAT", "Cat", [animalCode("CAT-DSH", "Domestic Shorthair"), animalCode("CAT-SIA", "Siamese")]),
506
+ animalSpecies("HORSE", "Horse", [animalCode("HRS-QTR", "Quarter Horse"), animalCode("HRS-TB", "Thoroughbred")]),
507
+ animalSpecies("CATTLE", "Cattle", [animalCode("CTL-HOL", "Holstein"), animalCode("CTL-ANG", "Angus")])
508
+ ]);
509
+ var SYNTHETIC_PET_NAMES = Object.freeze([
510
+ "Fixturepaw",
511
+ "Mockwhisker",
512
+ "Stubtail",
513
+ "Seedpup",
514
+ "Testbiscuit",
515
+ "Placepurr",
516
+ "Synthfur",
517
+ "Dummymane",
518
+ "Examplehoof",
519
+ "Fauxfluff",
520
+ "Samplesnout",
521
+ "Voidwag"
522
+ ]);
523
+ var MICROCHIP_TEST_PREFIX = "999";
524
+ function petName(rng) {
525
+ return rng.pick(SYNTHETIC_PET_NAMES);
526
+ }
527
+ function animalBreed(rng) {
528
+ const picked = rng.pick(SYNTHETIC_ANIMAL_SPECIES);
529
+ const { breeds, ...bare } = picked;
530
+ return { species: Object.freeze(bare), breed: rng.pick(breeds) };
531
+ }
532
+ function microchip(rng) {
533
+ return `${MICROCHIP_TEST_PREFIX}${rng.digits(12)}`;
534
+ }
535
+ function animalBirthDate(rng, onDate) {
536
+ const year = Number(onDate.slice(0, 4));
537
+ const month = Number(onDate.slice(4, 6)) - 1;
538
+ const day = Number(onDate.slice(6, 8));
539
+ const latest = Date.UTC(year, month, day);
540
+ const earliest = Date.UTC(year - 20, month, day);
541
+ const span = Math.round((latest - earliest) / 864e5);
542
+ const born = new Date(latest - rng.int(0, span) * 864e5);
543
+ return born.toISOString().slice(0, 10).replace(/-/g, "");
544
+ }
545
+
451
546
  // src/safe/index.ts
452
547
  var safe = Object.freeze({
453
548
  ssn,
@@ -461,7 +556,11 @@ var safe = Object.freeze({
461
556
  address,
462
557
  dateYmd,
463
558
  npi,
464
- dea
559
+ dea,
560
+ petName,
561
+ animalBreed,
562
+ microchip,
563
+ animalBirthDate
465
564
  });
466
565
 
467
566
  // src/astm/identity.ts
@@ -600,19 +699,44 @@ function defaultHost() {
600
699
  if (typeof undefined !== "function") return { require: __require, resolve: void 0 };
601
700
  return { require: module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))), resolve: (s) => undefined(s) };
602
701
  }
702
+ function isDenoResolveRefusal(error) {
703
+ return error instanceof TypeError && !("code" in error) && error.message === "Invalid arguments";
704
+ }
603
705
  function isRequireEsmRefusal(error) {
604
706
  return error?.code === "ERR_REQUIRE_ESM";
605
707
  }
708
+ function peerPackage(specifier) {
709
+ const name2 = specifier.split("/").slice(0, 2).join("/");
710
+ return Object.hasOwn(SYNTH_PEER_MESSAGES, name2) ? name2 : void 0;
711
+ }
712
+ function isPeerNotFound(error, specifier, peer) {
713
+ const { code, message } = error ?? {};
714
+ if (typeof message !== "string") return false;
715
+ if (code === "ERR_MODULE_NOT_FOUND") return message.startsWith(`Cannot find package '${peer}'`);
716
+ if (code === "MODULE_NOT_FOUND") return message.startsWith(`Cannot find module '${specifier}'`);
717
+ return false;
718
+ }
606
719
  function loadPeer(specifier, host) {
720
+ try {
721
+ return loadFrom(specifier, host);
722
+ } catch (error) {
723
+ const peer = peerPackage(specifier);
724
+ if (peer === void 0 || !isPeerNotFound(error, specifier, peer)) throw error;
725
+ throw new SynthError(SYNTH_FATAL_CODES.SYNTH_PEER_NOT_INSTALLED, { peer, cause: error });
726
+ }
727
+ }
728
+ function loadFrom(specifier, host) {
607
729
  if (host.resolve === void 0) return host.require(specifier);
608
- let entry;
730
+ let url$1;
609
731
  try {
610
- entry = url.fileURLToPath(host.resolve(specifier));
611
- } catch {
732
+ url$1 = host.resolve(specifier);
733
+ } catch (error) {
734
+ if (!isDenoResolveRefusal(error)) throw error;
612
735
  return host.require(specifier);
613
736
  }
737
+ if (!url$1.startsWith("file:")) return host.require(specifier);
614
738
  try {
615
- return host.require(entry);
739
+ return host.require(url.fileURLToPath(url$1));
616
740
  } catch (error) {
617
741
  if (!isRequireEsmRefusal(error)) throw error;
618
742
  return host.require(specifier);
@@ -735,6 +859,10 @@ function resolveMix(allowed, requested, fallback) {
735
859
  if (requested === void 0) return fallback;
736
860
  return requested.map((entry) => resolveKind(allowed, entry));
737
861
  }
862
+ function requireSelection(selection) {
863
+ if (selection.length === 0) throw new SynthError(SYNTH_FATAL_CODES.SYNTH_EMPTY_POOL);
864
+ return selection;
865
+ }
738
866
 
739
867
  // src/profile.ts
740
868
  function defineSynthProfile(spec) {
@@ -745,7 +873,7 @@ function defineSynthProfile(spec) {
745
873
  name: spec.name,
746
874
  ...spec.givenNames ? { givenNames: Object.freeze([...spec.givenNames]) } : {},
747
875
  ...spec.familyNames ? { familyNames: Object.freeze([...spec.familyNames]) } : {},
748
- quirks: Object.freeze([...spec.quirks ?? []])
876
+ ...spec.quirks !== void 0 ? { quirks: Object.freeze([...spec.quirks]) } : {}
749
877
  });
750
878
  }
751
879
 
@@ -787,6 +915,7 @@ function assertIntendedWarnings(intendedWarnings, bareWarnings) {
787
915
  }
788
916
  }
789
917
  function validateProfileQuirks(profile, registry, format) {
918
+ if (profile.quirks === void 0) return Object.keys(registry);
790
919
  for (const name2 of profile.quirks) resolveQuirk(registry, format, name2);
791
920
  return profile.quirks;
792
921
  }
@@ -895,8 +1024,9 @@ var ALL_ASTM_QUIRKS = Object.freeze(
895
1024
  Object.keys(ASTM_QUIRKS)
896
1025
  );
897
1026
  function astmQuirkCorpus(options) {
898
- const quirks = options.profile ? validateProfileQuirks(options.profile, ASTM_QUIRKS, "astm") : options.quirks ?? ALL_ASTM_QUIRKS;
899
- const names = quirks.length > 0 ? quirks : ALL_ASTM_QUIRKS;
1027
+ const names = requireSelection(
1028
+ options.profile?.quirks !== void 0 ? validateProfileQuirks(options.profile, ASTM_QUIRKS, "astm") : options.quirks ?? ALL_ASTM_QUIRKS
1029
+ );
900
1030
  for (const name2 of names) resolveQuirk(ASTM_QUIRKS, "astm", name2);
901
1031
  const count = options.count ?? names.length;
902
1032
  const seedStream = createRng(options.seed);
@@ -931,11 +1061,11 @@ function generateKind(kind, seed, verify) {
931
1061
  }
932
1062
  function astmCorpus(options) {
933
1063
  const { seed, verify = true } = options;
934
- const mix = resolveMix(ALL_KINDS, options.mix, DEFAULT_MIX);
1064
+ const mix = requireSelection(resolveMix(ALL_KINDS, options.mix, DEFAULT_MIX));
935
1065
  const count = options.count ?? mix.length;
936
1066
  const seedStream = createRng(seed);
937
1067
  const artifacts = Array.from({ length: count }, (_unused, i) => {
938
- const kind = mix[i % mix.length] ?? "Result";
1068
+ const kind = mix[i % mix.length];
939
1069
  const msgSeed = seedStream.nextUint32();
940
1070
  const rt = generateKind(kind, msgSeed, verify);
941
1071
  return {