@cosyte/synth 0.0.6 → 0.0.8

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 (57) hide show
  1. package/CHANGELOG.md +165 -17
  2. package/README.md +47 -45
  3. package/dist/astm/index.cjs +7 -7
  4. package/dist/astm/index.cjs.map +1 -1
  5. package/dist/astm/index.d.cts +47 -47
  6. package/dist/astm/index.d.ts +47 -47
  7. package/dist/astm/index.mjs +7 -7
  8. package/dist/astm/index.mjs.map +1 -1
  9. package/dist/ccda/index.cjs +15 -15
  10. package/dist/ccda/index.cjs.map +1 -1
  11. package/dist/ccda/index.d.cts +31 -31
  12. package/dist/ccda/index.d.ts +31 -31
  13. package/dist/ccda/index.mjs +15 -15
  14. package/dist/ccda/index.mjs.map +1 -1
  15. package/dist/deid/index.cjs +15 -15
  16. package/dist/deid/index.cjs.map +1 -1
  17. package/dist/deid/index.d.cts +37 -37
  18. package/dist/deid/index.d.ts +37 -37
  19. package/dist/deid/index.mjs +15 -15
  20. package/dist/deid/index.mjs.map +1 -1
  21. package/dist/{example-codes-DeXcnCSK.d.cts → example-codes-88f3_bQZ.d.cts} +8 -8
  22. package/dist/{example-codes-DeXcnCSK.d.ts → example-codes-88f3_bQZ.d.ts} +8 -8
  23. package/dist/fhir/index.cjs +12 -12
  24. package/dist/fhir/index.cjs.map +1 -1
  25. package/dist/fhir/index.d.cts +46 -46
  26. package/dist/fhir/index.d.ts +46 -46
  27. package/dist/fhir/index.mjs +12 -12
  28. package/dist/fhir/index.mjs.map +1 -1
  29. package/dist/hl7/index.cjs +6 -6
  30. package/dist/hl7/index.cjs.map +1 -1
  31. package/dist/hl7/index.d.cts +45 -45
  32. package/dist/hl7/index.d.ts +45 -45
  33. package/dist/hl7/index.mjs +6 -6
  34. package/dist/hl7/index.mjs.map +1 -1
  35. package/dist/index.cjs +9 -9
  36. package/dist/index.cjs.map +1 -1
  37. package/dist/index.d.cts +49 -49
  38. package/dist/index.d.ts +49 -49
  39. package/dist/index.mjs +9 -9
  40. package/dist/index.mjs.map +1 -1
  41. package/dist/ncpdp/index.cjs +3 -3
  42. package/dist/ncpdp/index.cjs.map +1 -1
  43. package/dist/ncpdp/index.d.cts +42 -42
  44. package/dist/ncpdp/index.d.ts +42 -42
  45. package/dist/ncpdp/index.mjs +3 -3
  46. package/dist/ncpdp/index.mjs.map +1 -1
  47. package/dist/{providers-OLz3zAc-.d.cts → providers-B9uVinAe.d.cts} +24 -24
  48. package/dist/{providers-OLz3zAc-.d.ts → providers-B9uVinAe.d.ts} +24 -24
  49. package/dist/{quirk-C9t9CkPS.d.ts → quirk-HZdznAkM.d.ts} +23 -23
  50. package/dist/{quirk-DYMDojVw.d.cts → quirk-IaHp4z7N.d.cts} +23 -23
  51. package/dist/x12/index.cjs +6 -6
  52. package/dist/x12/index.cjs.map +1 -1
  53. package/dist/x12/index.d.cts +44 -44
  54. package/dist/x12/index.d.ts +44 -44
  55. package/dist/x12/index.mjs +6 -6
  56. package/dist/x12/index.mjs.map +1 -1
  57. package/package.json +3 -2
package/CHANGELOG.md CHANGED
@@ -1,16 +1,140 @@
1
1
  # Changelog
2
2
 
3
- All notable changes to this project will be documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
- Versions and publishing are managed with [Changesets](https://github.com/changesets/changesets);
8
- this file is maintained by hand (Changesets handles the version bump and publish only).
9
-
10
- ## [Unreleased]
11
-
12
- The first pre-alpha release (`0.0.1`) will ship the initial public API surface. The package begins
13
- its public history at `0.0.x`, per the cosyte version ladder (`0.0.x` until first alpha).
3
+ ## 0.0.8
4
+
5
+ ### Patch Changes
6
+
7
+ - c63e3f0: `CHANGELOG.md`, which ships inside the tarball, is now written by the release instead of by hand, so it stops describing already-published code as unreleased.
8
+
9
+ `.changeset/config.json` set `"changelog": false`, so no release ever wrote a version heading
10
+ into `CHANGELOG.md` and nothing ever rolled `[Unreleased]` over. Every published version of this
11
+ package therefore carried a changelog with **no version headings at all**: one `[Unreleased]`
12
+ heading over the whole history, and a preamble stating that the first pre-alpha release "will
13
+ ship" the initial public API surface listed below it. That promise was already in the future tense
14
+ in the very tarball that fulfilled it, and it stayed in the future tense through every release
15
+ after. `CHANGELOG.md` is listed in `package.json` `files`, so this was text on the disk of everyone
16
+ who installed the package, not internal bookkeeping.
17
+
18
+ **The flag is what changed, not the prose.** Correcting the sentence by hand would have left the
19
+ mechanism that wrote it in place, and the next release would have drifted the same way.
20
+ `changelog` now names the generator that ships with Changesets, so a release writes its own
21
+ version heading and its own entry from the changesets it consumed, and **the changeset summary is
22
+ now the changelog entry**. Nothing new is depended on: the generator is an entry point of
23
+ `@changesets/cli`, which was already a dev dependency.
24
+
25
+ **The file's shape changed with it, deliberately.** Changesets prepends a release by replacing the
26
+ first newline in the file, so exactly one line can sit above generated output. The hand-written
27
+ preamble sat on line 3, which means a release would have inserted itself between the heading and
28
+ the preamble and split the header in two. The hand-maintained history has therefore moved under a
29
+ `## Released before this file was generated` heading, with the false preamble replaced by an
30
+ accurate one. Three pieces of hand-workflow scaffolding were dropped and no entry was reworded:
31
+ the `[Unreleased]` heading, its link definition at the foot of the file, and the two empty section
32
+ stubs (`### Deprecated` and `### Removed`) waiting for the next hand-written entry. The history
33
+ itself is left as it was written rather than re-sorted into version sections, because the file
34
+ never recorded which release each entry went out in and the text is already on disk in published
35
+ copies.
36
+
37
+ **Changesets' Prettier pass is deliberately left ON here, and that was derived from this repo
38
+ rather than copied from a sibling.** This repo has no `.prettierignore` at all and its
39
+ `format:check` globs root markdown, so `CHANGELOG.md` is inside the repo's own formatting gate and
40
+ its archived history is already Prettier-canonical. Both directions were measured. With the pass
41
+ on, the archived history comes through a release byte identical, so leaving it on costs nothing.
42
+ With the pass off, what `changeset version` itself writes is no longer Prettier-canonical even for
43
+ the simplest possible summary, because it writes the version heading and `### Patch Changes` on
44
+ adjacent lines with no blank line between them. **A sentence a sibling carries is false here and
45
+ is deliberately not repeated**: this repo's `version` script also runs `prettier --write` over
46
+ `CHANGELOG.md` one link after `changeset version`, where a sibling's covers only `package.json`
47
+ and `src/index.ts`, so turning the pass off here would not open the Version PR red and would not
48
+ keep Prettier away from the archived history either. It would only leave the tool's own output
49
+ failing the gate that covers the file it wrote, which buys nothing. A sibling whose
50
+ `.prettierignore` lists `*.md` needs the opposite setting, and resyncing the value between repos is
51
+ how a release starts rewriting already-published text.
52
+
53
+ Pinned by `test/scripts/changelog-generation.test.ts`, which runs the real `changeset version`
54
+ against the real `CHANGELOG.md` and the real config in a throwaway package rather than
55
+ reimplementing where the tool inserts text. Nine of its fifteen cases are red against the previous
56
+ state, measured on the tree this change was written against rather than recalled. The throwaway
57
+ package is a real git repository, because the generator prefixes each entry with the short commit
58
+ sha that added the changeset and a tree with no history would exercise a line shape no release
59
+ writes. **The rule it enforces is that nothing but the H1 sits above the first heading, and it is
60
+ asserted on the released document as well as the committed one**: a rule phrased as "the archive
61
+ heading comes second" holds only until the first release writes its own version heading there,
62
+ which would have redded the first Version PR this configuration ever opened. Every version-heading
63
+ comparison is a whole-heading match rather than a substring, demonstrated on real generator output,
64
+ because this package sits past the point where `## 0.0.1` is a prefix of a heading it does not
65
+ have. Three further controls: the same inputs with `"changelog": false` must write no version
66
+ heading at all, so the flag is proved load-bearing rather than incidental; the same inputs with the
67
+ Prettier pass off must produce a document this repo's own formatting gate rejects; and the old file
68
+ shape must reproduce the split header, so the shape rule is demonstrated rather than asserted.
69
+
70
+ One upstream behaviour is worth knowing before debugging a release, and is recorded in that file:
71
+ Changesets wraps the changelog write in a try/catch that only warns. A tree whose declared Prettier
72
+ config cannot be resolved bumps the version, consumes the changeset, and writes no changelog at all.
73
+ A release that publishes with an unchanged changelog is that failure, not a setting that quietly
74
+ reverted.
75
+
76
+ `.changeset/README.md` and `CLAUDE.md` said to add the entry to `CHANGELOG.md` by hand and now say
77
+ to write it in the changeset. No runtime code, no public API, no warning code, no generated byte and
78
+ no synthetic-safety behaviour changed.
79
+
80
+ - da7cbdb: The package description, the README, the documentation pages and the exported quirk-registry prose no longer use the em dash, matching the cosyte house voice.
81
+
82
+ **Nothing about generation changed.** No export was added, removed or renamed, no warning or fatal
83
+ code moved, and a given seed still produces byte-identical output for every format. The rewrite is
84
+ punctuation in prose: an em dash became a colon or a comma in almost every case, and elsewhere a
85
+ period, a semicolon, a pair of parentheses, a connecting word, or nothing at all, whichever the
86
+ sentence wanted. Every value this package emits is still drawn from the same reserved
87
+ and synthetic sources it always was, and no fixture byte moved.
88
+
89
+ Two of those surfaces are worth naming because they are read outside the repository.
90
+ `package.json`'s `description` is what npm shows on the package page and in every search result,
91
+ and the fourteen pages under `docs-content/` are what ships to the documentation site. The two
92
+ runtime-visible strings are the `grounding` notes on the HL7 v2 and ASTM quirk descriptors, which
93
+ are frozen registry text a consumer can read but which nothing is keyed on.
94
+
95
+ The hand-maintained changelog history below `## Released before this file was generated` is
96
+ deliberately untouched. Those entries were accurate when they were written, and a dated record is
97
+ corrected above itself rather than edited.
98
+
99
+ - a18b96d: The README lockup now links to cosyte.com (`ASSETS`).
100
+
101
+ The `<picture>` block above the H1 is wrapped in an anchor to https://cosyte.com, per the founder
102
+ requirement of 2026-08-06. Nothing inside the block moved: the `<source>`, the `<img>`, the alt text
103
+ and both tile URLs are byte-identical.
104
+
105
+ What the anchor does was measured on both surfaces by `fhir`, not assumed, because fourteen READMEs
106
+ carry this shape. On GitHub the anchor works and the colour-scheme switch keeps working, because the
107
+ `<img>` stays a direct child of `<picture>`, which is the condition the HTML spec puts on `<source>`
108
+ applying at all. On an npm package page the anchor is lost: npm wraps a README image in its own
109
+ anchor to the image file, a nested anchor is not representable, so the parser closes ours early and
110
+ the image ends up linked to the image file rather than to cosyte.com. Shipped anyway by founder
111
+ decision of 2026-08-07: on npm that is no worse than the unlinked lockup it replaces, and GitHub is
112
+ where these READMEs are read.
113
+
114
+ ## Released before this file was generated
115
+
116
+ Every release section above this heading is written by
117
+ [Changesets](https://github.com/changesets/changesets) from the changesets in `.changeset/`, newest
118
+ release first. The release writes its own version heading, so nothing above this line is maintained
119
+ by hand: a change is recorded by adding a changeset, and that changeset's summary is the entry a
120
+ reader sees here.
121
+
122
+ Everything below this heading was maintained by hand. It sat under a single `[Unreleased]` heading
123
+ that no release ever rolled over, and `CHANGELOG.md` is listed in `package.json` `files`, so every
124
+ published tarball of this package described its own already-shipped contents as unreleased, under a
125
+ preamble promising that a first pre-alpha release would ship the API surface listed under it. That
126
+ promise was already in the future tense in the very tarball that fulfilled it, and it stayed in the
127
+ future tense through every release after. The history is left exactly as it was written rather than re-sorted into
128
+ version sections: the file never recorded which release any entry went out in, and this is the text
129
+ that installed copies already carry on disk. Only three things were dropped, all of them scaffolding
130
+ for the hand-written workflow that no longer runs: the `[Unreleased]` heading itself, its link
131
+ definition at the foot of the file, and the two empty section stubs (`### Deprecated` and
132
+ `### Removed`) that were waiting to receive the next hand-written entry. No entry was reworded.
133
+
134
+ The entries below follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the generated
135
+ sections above use the format Changesets writes, which is a version heading and a list of the
136
+ changesets that release consumed. This package is on the pre-alpha `0.0.x` ladder and adheres to
137
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
14
138
 
15
139
  ### Added
16
140
 
@@ -541,12 +665,38 @@ its public history at `0.0.x`, per the cosyte version ladder (`0.0.x` until firs
541
665
  a frozen exported constant, so a consumer asserting them verbatim is affected. The `format` values and
542
666
  the shape of the constant are unchanged, and no generated byte changes.
543
667
 
544
- ### Deprecated
545
-
546
- ### Removed
547
-
548
668
  ### Fixed
549
669
 
670
+ - **`pnpm phi-scan` could print `OK` and exit 0 over a whole scan root it never opened, because the
671
+ observe-something rule was global instead of per-root.** The scanner walks three roots (`src/`,
672
+ `test/`, `scripts/`) and refused only when it had read _zero_ files in total, so any one surviving
673
+ file vouched for all three. Measured on `a4b249a` before the change, twice: with `test/` moved
674
+ aside, and again with `test` replaced by a dangling symlink, the sweep printed
675
+ `OK - no hits (76 file(s) scanned)` and exited **0** while all **98** files of the test corpus went
676
+ unobserved. Neither state trips anything else in the scanner, which is why the denominator was the
677
+ only signal and still read plausible: `walk()` returns early on a root `existsSync` cannot resolve,
678
+ so an absent root, a dangling one and an empty one are indistinguishable, and the not-a-regular-file
679
+ refusal never sees a root at all, because it only classifies entries found _inside_ one. The rule is
680
+ now **per-root**: all-mode refuses (exit 2) unless every declared root yielded at least one file that
681
+ was actually read, and the refusal names the starved roots. The superseded justification only ever
682
+ covered one root, which is how the shape survived review here: "all-mode always reaches at least the
683
+ allow-list itself" is true, and the allow-list lives under `scripts/`. It was an argument that
684
+ `scripts/` cannot be starved, doing duty as an argument about all three. Observing nothing at all is
685
+ now the all-starved case of the same rule rather than a second one beside it, and the wording
686
+ carries. Two behaviours are deliberately unchanged and are pinned as such: `--staged` and named-path
687
+ mode make no per-root promise (a commit that touches no file under `test/` is the normal case), and
688
+ hits found under the roots that _did_ yield are still reported before the refusal, so an incomplete
689
+ sweep never swallows a real finding. It exits 2 rather than 1 regardless, because an incomplete
690
+ sweep is not a verdict. This matters more here than in any sibling: `@cosyte/synth` exists to emit
691
+ PHI-shaped bytes, so a scan that silently observes nothing is the emptiest possible green. The rule
692
+ and its measurement live at the end of `main()` in `scripts/phi-scan.ts`. **Its granularity is the
693
+ declared root and nothing finer**, and that bound is recorded rather than glossed, because the natural
694
+ way to describe this fix overstates it: an absent directory _inside_ a root is still unobserved under a
695
+ plausible denominator (`mv test/fixtures ..` still returns `OK - no hits (128 file(s) scanned)` exit 0,
696
+ 46 files unread). That is unchanged here, and closing it needs a per-root floor derived from what git
697
+ tracks, which is a second moving part and a separate decision. The scanner's limits list now carries it
698
+ and three more residuals, each with the reading that produced it. Found independently by two siblings'
699
+ review passes, which is what made it an item rather than a note.
550
700
  - **The exported `VERSION` constant reported `0.0.0` on every published release, and the fix is the
551
701
  binding rather than the literal.** `src/index.ts` carried a hand-written constant while Changesets
552
702
  bumped `package.json`, so the two were never connected and the export went stale at the first
@@ -986,5 +1136,3 @@ attw` and `find dist -name '*.d.*ts' -delete && pnpm attw` printed the sentence
986
1136
  of the positions that have been checked. That wording is now in the test file's own header, in
987
1137
  `docs-content/limitations.md` and in `CLAUDE.md`, so the next person to add a position adds a slot
988
1138
  rather than trusting a count.
989
-
990
- [Unreleased]: https://github.com/cosyte/synth/commits/main
package/README.md CHANGED
@@ -1,11 +1,13 @@
1
- <picture>
2
- <source media="(prefers-color-scheme: dark)" srcset="https://cosyte.com/tile/cosyte-lockup-tile-on-dark-1200x300.png">
3
- <img alt="Cosyte: a plus mark set in two overlapping rounded squares, one solid and one outlined, beside the Cosyte wordmark" src="https://cosyte.com/tile/cosyte-lockup-tile-on-light-1200x300.png">
4
- </picture>
1
+ <a href="https://cosyte.com">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://cosyte.com/tile/cosyte-lockup-tile-on-dark-1200x300.png">
4
+ <img alt="Cosyte: a plus mark set in two overlapping rounded squares, one solid and one outlined, beside the Cosyte wordmark" src="https://cosyte.com/tile/cosyte-lockup-tile-on-light-1200x300.png">
5
+ </picture>
6
+ </a>
5
7
 
6
8
  # @cosyte/synth
7
9
 
8
- > Deterministic, seedable **synthetic healthcare-fixture generator** for Node.js and TypeScript —
10
+ > Deterministic, seedable **synthetic healthcare-fixture generator** for Node.js and TypeScript:
9
11
  > spec-clean by construction, and **never real PHI**.
10
12
 
11
13
  `@cosyte/synth` generates reproducible synthetic test corpora across the six cosyte formats (HL7 v2,
@@ -13,19 +15,19 @@ FHIR R4 / US Core, C-CDA, X12, NCPDP, and ASTM). It is a **consumer** of the
13
15
  cosyte parsers, not a parser: it builds each artifact **through the parser's own builder/serializer**
14
16
  (so the output is spec-clean by the same mechanism the parser proves) and draws every identifier, name,
15
17
  date, phone, and address from a **guaranteed-non-colliding synthetic source**. It is a
16
- **format/conformance generator, not a clinical simulator** — it does not model disease progression
18
+ **format/conformance generator, not a clinical simulator**: it does not model disease progression
17
19
  (that is Synthea).
18
20
 
19
21
  > **Status:** pre-alpha (`0.0.x`), published to npm. The version shown on the npm package page is the
20
22
  > one that is live; this page never repeats it. The generator is **feature-complete**: the
21
23
  > seeded-PRNG core, the synthetic-safety providers, and the round-trip harness; **spec-clean generation
22
- > across all six formats** — HL7 v2 (`ADT`/`ORU`/`ORM`/`SIU`/`VXU`), FHIR R4 / US Core (the full clinical
24
+ > across all six formats**, HL7 v2 (`ADT`/`ORU`/`ORM`/`SIU`/`VXU`), FHIR R4 / US Core (the full clinical
23
25
  > set + `collection`/`transaction`/`document` Bundles), C-CDA R2.1 (CCD + Referral Note), X12 005010
24
26
  > (837P/I/D, 835, 271), NCPDP (SCRIPT NewRx / RxRenewal / RxChange + Telecom B1/B2/B3), and ASTM (E1394
25
27
  > record reports + E1381 framing), each built **through its parser's own builder/serializer**; **vendor-quirk
26
- > mode** — profile-driven off-spec fixtures for the three richest profile systems (**HL7 v2, C-CDA, ASTM**),
28
+ > mode**, profile-driven off-spec fixtures for the three richest profile systems (**HL7 v2, C-CDA, ASTM**),
27
29
  > each round-tripping to exactly the intended parser warning (the intended-warning contract); and the
28
- > **`@cosyte/deid` pairing loop** — a closed-loop co-validation harness that de-identifies generated output
30
+ > **`@cosyte/deid` pairing loop**, a closed-loop co-validation harness that de-identifies generated output
29
31
  > and proves every planted synthetic PHI sentinel is removed.
30
32
  >
31
33
  > **Deferred (honestly out of scope for now):** quirk recipes for **FHIR / X12 / NCPDP** (and any quirk
@@ -42,13 +44,13 @@ npm install @cosyte/synth @cosyte/hl7 @cosyte/fhir @cosyte/ccda @cosyte/x12 @cos
42
44
  `@cosyte/hl7`, `@cosyte/fhir`, `@cosyte/ccda`, `@cosyte/x12`, `@cosyte/ncpdp`, `@cosyte/astm`, and
43
45
  `@cosyte/deid` are **optional peer dependencies**, each needed only for its subpath (`@cosyte/synth/hl7`,
44
46
  `@cosyte/synth/fhir`, `@cosyte/synth/ccda`, `@cosyte/synth/x12`, `@cosyte/synth/ncpdp`,
45
- `@cosyte/synth/astm`, and `@cosyte/synth/deid` — the last needs `@cosyte/deid` **plus** the parsers for
46
- the formats it pairs) — install only the packages whose fixtures you generate. The package core has
47
+ `@cosyte/synth/astm`, and `@cosyte/synth/deid`: the last needs `@cosyte/deid` **plus** the parsers for
48
+ the formats it pairs): install only the packages whose fixtures you generate. The package core has
47
49
  **zero third-party runtime dependencies**.
48
50
 
49
51
  ## Generate a spec-clean HL7 v2 message
50
52
 
51
- The HL7 v2 set covers `ADT` (`A01`/`A04`/`A08`), `ORU^R01`, `ORM^O01`, `SIU^S12`, and `VXU^V04` —
53
+ The HL7 v2 set covers `ADT` (`A01`/`A04`/`A08`), `ORU^R01`, `ORM^O01`, `SIU^S12`, and `VXU^V04`:
52
54
  each built through `@cosyte/hl7`'s `buildMessage`, so it is spec-clean by construction.
53
55
 
54
56
  ```ts
@@ -64,7 +66,7 @@ roundTrip(oru).specClean; // true
64
66
 
65
67
  // Or generate a reproducible mixed corpus across every family:
66
68
  const corpus = hl7Corpus({ seed: 42, count: 7 }); // one of each family, cycled
67
- corpus.artifacts.every((a) => a.warnings.length === 0); // true — all spec-clean
69
+ corpus.artifacts.every((a) => a.warnings.length === 0); // true, all spec-clean
68
70
 
69
71
  // Dispatch by kind when the message type is data:
70
72
  generateHl7("VXU^V04", 12345);
@@ -73,8 +75,8 @@ generateHl7("VXU^V04", 12345);
73
75
  ## Generate a spec-clean FHIR R4 / US Core resource
74
76
 
75
77
  The `@cosyte/synth/fhir` subpath builds resources **through `@cosyte/fhir`'s model constructors**, so
76
- they are spec-clean by construction — validating under `validateResource` and, against the **real US
77
- Core 6.1.0 profiles** (bring your own `StructureDefinition`s — none is bundled), conformant to US Core.
78
+ they are spec-clean by construction: validating under `validateResource` and, against the **real US
79
+ Core 6.1.0 profiles** (bring your own `StructureDefinition`s, none is bundled), conformant to US Core.
78
80
  The clinical set covers `Patient` (base + US Core), `Condition`, `Observation` (US Core Laboratory
79
81
  Result + Vital Signs), `MedicationRequest`, `Encounter`, `DiagnosticReport`, `Immunization`,
80
82
  `AllergyIntolerance`, and `Procedure`, assembled into a `collection`, `transaction`, or `document`
@@ -83,7 +85,7 @@ Result + Vital Signs), `MedicationRequest`, `Encounter`, `DiagnosticReport`, `Im
83
85
  ```ts
84
86
  import { generatePatient, generateBundle, fhirCorpus, roundTrip } from "@cosyte/synth/fhir";
85
87
 
86
- // A US Core Patient — same seed → byte-identical resource, anywhere.
88
+ // A US Core Patient: same seed → byte-identical resource, anywhere.
87
89
  const patient = generatePatient({ seed: 12345, profile: "us-core" });
88
90
 
89
91
  // Spec-clean by construction: it round-trips through @cosyte/fhir with zero errors, byte-stable.
@@ -94,7 +96,7 @@ const bundle = generateBundle({ seed: 42, type: "transaction" });
94
96
 
95
97
  // Or a reproducible mixed corpus across the whole spine:
96
98
  const corpus = fhirCorpus({ seed: 2026, count: 6 });
97
- corpus.artifacts.every((a) => a.warnings.length === 0); // true — all spec-clean
99
+ corpus.artifacts.every((a) => a.warnings.length === 0); // true, all spec-clean
98
100
  ```
99
101
 
100
102
  `@cosyte/fhir` is an **optional peer dependency**, needed only for the `@cosyte/synth/fhir` subpath.
@@ -103,7 +105,7 @@ corpus.artifacts.every((a) => a.warnings.length === 0); // true — all spec-cle
103
105
 
104
106
  The `@cosyte/synth/ccda` subpath builds Consolidated CDA R2.1 documents **through `@cosyte/ccda`'s
105
107
  `buildCcda`**, so template IDs, LOINC section codes, and structured/narrative agreement are the
106
- builder's own — the document round-trips through `parseCcda` with **zero warnings**. It emits a
108
+ builder's own: the document round-trips through `parseCcda` with **zero warnings**. It emits a
107
109
  **CCD** (`generateCcd`) or a **Referral Note** (`generateReferralNote`), each with the CCD sections
108
110
  (Problems, Allergies, Medications, Results, Vital Signs, Immunizations, Procedures, Social History)
109
111
  populated from the reused, license-clean example-code pools.
@@ -123,7 +125,7 @@ roundTrip(generateReferralNote({ seed: 12345 })).specClean; // true
123
125
 
124
126
  // Or a reproducible mixed corpus (CCD + Referral Note, cycled):
125
127
  const corpus = ccdaCorpus({ seed: 42, count: 4 });
126
- corpus.artifacts.every((a) => a.warnings.length === 0); // true — all spec-clean
128
+ corpus.artifacts.every((a) => a.warnings.length === 0); // true, all spec-clean
127
129
  ```
128
130
 
129
131
  `@cosyte/ccda` is an **optional peer dependency**, needed only for the `@cosyte/synth/ccda` subpath.
@@ -132,7 +134,7 @@ corpus.artifacts.every((a) => a.warnings.length === 0); // true — all spec-cle
132
134
 
133
135
  The `@cosyte/synth/x12` subpath builds HIPAA **005010** transactions **through `@cosyte/x12`'s domain
134
136
  builders** (`build837P/I/D`, `build835`, `build271`), so the ISA/GS/ST…SE/GE/IEA envelope, the
135
- computed HL spine, the control numbers, and every segment are the builder's own — each transaction
137
+ computed HL spine, the control numbers, and every segment are the builder's own: each transaction
136
138
  round-trips through `@cosyte/x12` with **zero warnings**. It emits **837** professional / institutional
137
139
  / dental claims, the **835** remittance (balance-checked by construction), and the **271** eligibility
138
140
  response.
@@ -145,13 +147,13 @@ import { generate837P, generate835, generate271, x12Corpus, roundTrip } from "@c
145
147
  // never be a NPPES-issued NPI), the provider tax id is an SSA never-issued 900-range SSN, member ids
146
148
  // live under a synthetic assigning authority, and names come from the shipped fake-name pool.
147
149
  const claim = generate837P({ seed: 12345 });
148
- roundTrip(claim).specClean; // true — re-parses through @cosyte/x12 with zero warnings, byte-stable
150
+ roundTrip(claim).specClean; // true, re-parses through @cosyte/x12 with zero warnings, byte-stable
149
151
  roundTrip(generate835({ seed: 7 })).specClean; // true
150
152
  roundTrip(generate271({ seed: 3 })).specClean; // true
151
153
 
152
154
  // Or a reproducible mixed corpus (837P/I/D + 835 + 271):
153
155
  const corpus = x12Corpus({ seed: 42 });
154
- corpus.artifacts.every((a) => a.warnings.length === 0); // true — all spec-clean
156
+ corpus.artifacts.every((a) => a.warnings.length === 0); // true, all spec-clean
155
157
  ```
156
158
 
157
159
  `@cosyte/x12` is an **optional peer dependency**, needed only for the `@cosyte/synth/x12` subpath.
@@ -180,12 +182,12 @@ import {
180
182
  // has a deliberately-INVALID Luhn check digit and the prescriber DEA a deliberately-INVALID checksum
181
183
  // (so neither can denote a real provider); patient/cardholder ids live under a synthetic assigning
182
184
  // authority, phones are reserved 555-01xx, and names come from the shipped fake-name pool.
183
- scriptRoundTrip(generateNewRx({ seed: 12345 })).specClean; // true — zero warnings, byte-stable
185
+ scriptRoundTrip(generateNewRx({ seed: 12345 })).specClean; // true, zero warnings, byte-stable
184
186
  telecomRoundTrip(generateB1({ seed: 777 })).specClean; // true
185
187
 
186
188
  // Or a reproducible mixed corpus (NewRx + RxRenewal + RxChange + B1 + B2 + B3):
187
189
  const corpus = ncpdpCorpus({ seed: 42 });
188
- corpus.artifacts.every((a) => a.warnings.length === 0); // true — all spec-clean
190
+ corpus.artifacts.every((a) => a.warnings.length === 0); // true, all spec-clean
189
191
  ```
190
192
 
191
193
  `@cosyte/ncpdp` is an **optional peer dependency**, needed only for the `@cosyte/synth/ncpdp` subpath.
@@ -196,8 +198,8 @@ renewal/change _responses_ land as `@cosyte/ncpdp` grows builders.
196
198
  ## Generate a spec-clean ASTM message
197
199
 
198
200
  The `@cosyte/synth/astm` subpath builds ASTM laboratory messages **through `@cosyte/astm`'s own emit
199
- surface** — `buildAstmMessage` for the E1394 record layer, `composeAstmFrames` for the E1381 frame
200
- layer — so each message round-trips through the parser with **zero warnings**. It emits the
201
+ surface**: `buildAstmMessage` for the E1394 record layer, `composeAstmFrames` for the E1381 frame
202
+ layer, so each message round-trips through the parser with **zero warnings**. It emits the
201
203
  `H`/`P`/`O`/`R`…/`C`/`L` **result report** (`generateAstmResult`), the `H`/`P`/`O`/`L` **order**
202
204
  (`generateAstmOrder`), and the **framed** twin (`generateAstmResultFramed`).
203
205
 
@@ -211,17 +213,17 @@ import {
211
213
  } from "@cosyte/synth/astm";
212
214
 
213
215
  // Same seed → byte-identical output. The P (patient) record carries the name, birthdate, and the
214
- // practice- and laboratory-assigned patient ids — all synthetic-by-construction: names from the shipped
216
+ // practice- and laboratory-assigned patient ids, all synthetic-by-construction: names from the shipped
215
217
  // fake-name pool, DOB seeded, and the two ids minted independently under a synthetic assigning authority
216
218
  // (so they stay DISTINCT, exactly as @cosyte/astm keeps them on parse).
217
- astmRoundTrip(generateAstmResult({ seed: 12345 })).specClean; // true — zero warnings, byte-stable
219
+ astmRoundTrip(generateAstmResult({ seed: 12345 })).specClean; // true, zero warnings, byte-stable
218
220
 
219
221
  // The E1381-framed twin: the modulo-256 checksum and 0–7 frame numbers are computed by @cosyte/astm.
220
222
  astmFramedRoundTrip(generateAstmResultFramed({ seed: 12345 })).specClean; // true
221
223
 
222
224
  // Or a reproducible mixed corpus (a result report + an order):
223
225
  const corpus = astmCorpus({ seed: 42 });
224
- corpus.artifacts.every((a) => a.warnings.length === 0); // true — all spec-clean
226
+ corpus.artifacts.every((a) => a.warnings.length === 0); // true, all spec-clean
225
227
  ```
226
228
 
227
229
  `@cosyte/astm` is an **optional peer dependency**, needed only for the `@cosyte/synth/astm` subpath.
@@ -229,14 +231,14 @@ corpus.artifacts.every((a) => a.warnings.length === 0); // true — all spec-cle
229
231
  ## Generate a vendor-quirk fixture
230
232
 
231
233
  Spec-clean fixtures test that a parser reads a _correct_ message; **quirk mode** tests that it tolerates
232
- the realistic vendor deviations real traffic carries — and surfaces exactly the right diagnostic. The
234
+ the realistic vendor deviations real traffic carries, and surfaces exactly the right diagnostic. The
233
235
  quirk vocabulary **is the parsers' own profile systems**: a quirk deviates the message _structure_ so it
234
236
  round-trips to **exactly one intended, stable warning code** (the **intended-warning contract**), and
235
237
  where a built-in **public** parser profile claims the deviation, it round-trips cleanly under it
236
- (suppressed, or re-badged to `PROFILE_QUIRK_APPLIED`). A quirk never introduces a real-looking value —
238
+ (suppressed, or re-badged to `PROFILE_QUIRK_APPLIED`). A quirk never introduces a real-looking value:
237
239
  it changes shape, never provenance, so the synthetic-safety gate still passes.
238
240
 
239
- Quirks ship for the three richest profile systems — **HL7 v2, C-CDA, and ASTM**:
241
+ Quirks ship for the three richest profile systems, **HL7 v2, C-CDA, and ASTM**:
240
242
 
241
243
  ```ts
242
244
  import { generateHl7Quirk, hl7QuirkRoundTrip } from "@cosyte/synth/hl7";
@@ -255,7 +257,7 @@ astmQuirkRoundTrip(generateAstmQuirk({ seed: 1, quirk: "unknown-escape" })).inte
255
257
  ```
256
258
 
257
259
  A quirk a format's profile system does not support fails closed with a stable `SYNTH_UNSUPPORTED_QUIRK`
258
- diagnostic — never a silently-wrong fixture — and so do the other selectors: a message kind, a
260
+ diagnostic, never a silently-wrong fixture, and so do the other selectors: a message kind, a
259
261
  document type, a corpus mix entry, an `837` variant, a Bundle type. Each is checked against its own set
260
262
  before anything is generated, so an unrecognised one is a fatal `SYNTH_UNSUPPORTED_KIND` rather than a
261
263
  mislabelled corpus. Every refusal carries a code and a fixed message from a frozen table, and quotes
@@ -285,8 +287,8 @@ import {
285
287
 
286
288
  const r = hl7DeidLoop({ seed: 42, kind: "ORU^R01" });
287
289
  r.pass; // true
288
- r.survivors; // [] — every planted synthetic PHI sentinel was removed
289
- r.clinicalScrubbed; // [] — no clinical value was over-scrubbed
290
+ r.survivors; // [], every planted synthetic PHI sentinel was removed
291
+ r.clinicalScrubbed; // [], no clinical value was over-scrubbed
290
292
 
291
293
  const summary = summarizeDeidCoverage([
292
294
  hl7DeidLoop({ seed: 1 }),
@@ -295,11 +297,11 @@ const summary = summarizeDeidCoverage([
295
297
  ncpdpTelecomDeidLoop({ seed: 1, transaction: "B1" }),
296
298
  ccdaDeidLoop({ seed: 1 }),
297
299
  ]);
298
- summary.allPass; // true — zero survivors, zero over-scrub, per format
300
+ summary.allPass; // true, zero survivors, zero over-scrub, per format
299
301
  ```
300
302
 
301
303
  This is a **co-validation harness, not an independent audit** of `@cosyte/deid` against real-world
302
- data — it proves the pair works on `synth`'s own output. The removal check is **locus-scoped**: it
304
+ data: it proves the pair works on `synth`'s own output. The removal check is **locus-scoped**: it
303
305
  sweeps only the de-identified values remaining at the former PHI loci, so provider/organization
304
306
  identity a de-identifier legitimately retains never reads as a false survivor. `@cosyte/deid` is an
305
307
  **optional peer dependency**, needed only for this subpath. **Skipped and named** (`DEID_LOOP_SKIPPED`):
@@ -311,22 +313,22 @@ NCPDP **SCRIPT** and **ASTM** (no `@cosyte/deid` adapter) and **DICOM** (not gen
311
313
  import { createRng, safe, isSyntheticSsn, isSyntheticNpi } from "@cosyte/synth";
312
314
 
313
315
  const rng = createRng(42);
314
- isSyntheticSsn(safe.ssn(rng)); // true — always an SSA never-issued SSN
315
- isSyntheticNpi(safe.npi(rng)); // true — always a deliberately-invalid-Luhn NPI (never a real NPI)
316
+ isSyntheticSsn(safe.ssn(rng)); // true, always an SSA never-issued SSN
317
+ isSyntheticNpi(safe.npi(rng)); // true, always a deliberately-invalid-Luhn NPI (never a real NPI)
316
318
  ```
317
319
 
318
320
  ## What makes it trustworthy
319
321
 
320
- - **Synthetic-by-construction** — no code path emits a value not drawn from a reserved range or the
322
+ - **Synthetic-by-construction**, no code path emits a value not drawn from a reserved range or the
321
323
  shipped fake-name pool (SSA never-issued SSNs, NANP `555-01xx` phones, RFC 2606/6761 `example.*`
322
324
  domains, RFC 5737/3849 TEST-NET IPs, a synthetic assigning authority for MRNs). A CI gate proves it.
323
325
  **No generated value can be real or plausibly-real PHI.**
324
- - **Spec-clean by the parser's own judgment** — built through the parser's conservative serializer, and
326
+ - **Spec-clean by the parser's own judgment**, built through the parser's conservative serializer, and
325
327
  checked by feeding the artifact straight back in: a spec-clean artifact re-parses with zero warnings.
326
- - **Deterministic** — a hand-rolled seeded PRNG (`sfc32`/`splitmix32`); `Math.random` is lint-banned.
327
- A seed, and only the seed, determines the output — byte-for-byte, anywhere.
328
- - **Immutable** — generated artifacts and the `Corpus` result are deep-frozen.
329
- - **Zero third-party runtime dependencies** — the parser peers are first-party cosyte packages,
328
+ - **Deterministic**, a hand-rolled seeded PRNG (`sfc32`/`splitmix32`); `Math.random` is lint-banned.
329
+ A seed, and only the seed, determines the output, byte-for-byte, anywhere.
330
+ - **Immutable**, generated artifacts and the `Corpus` result are deep-frozen.
331
+ - **Zero third-party runtime dependencies**, the parser peers are first-party cosyte packages,
330
332
  vendored for dev/test; dual ESM + CJS, validated with `attw`.
331
333
 
332
334
  ## License
@@ -34,30 +34,30 @@ function sfc32Next(s) {
34
34
  // src/codes.ts
35
35
  var SYNTH_FATAL_CODES = {
36
36
  /**
37
- * A vendor quirk was requested that the target format's profile system does not support. Fatal —
37
+ * A vendor quirk was requested that the target format's profile system does not support. Fatal,
38
38
  * never a silent no-op and never a fabricated quirk.
39
39
  */
40
40
  SYNTH_UNSUPPORTED_QUIRK: "SYNTH_UNSUPPORTED_QUIRK",
41
41
  /**
42
42
  * A quirk transform found no structural anchor to mutate, so the fixture would not carry the
43
- * deviation it is labelled with. Fatal — a golden file that lies about its parser verdict is worse
43
+ * deviation it is labelled with. Fatal: a golden file that lies about its parser verdict is worse
44
44
  * than no golden file.
45
45
  */
46
46
  SYNTH_QUIRK_ANCHOR_ABSENT: "SYNTH_QUIRK_ANCHOR_ABSENT",
47
47
  /**
48
48
  * A bare parse of a freshly-generated quirk artifact did not produce exactly the declared intended
49
- * warning code(s). Fatal — never emit a mislabeled fixture.
49
+ * warning code(s). Fatal, never emit a mislabeled fixture.
50
50
  */
51
51
  SYNTH_INTENDED_WARNING_MISMATCH: "SYNTH_INTENDED_WARNING_MISMATCH",
52
52
  /** An integer range was requested with its maximum below its minimum. Fatal. */
53
53
  SYNTH_INVALID_RANGE: "SYNTH_INVALID_RANGE",
54
- /** A value was drawn from an empty pool. Fatal — never a fabricated substitute. */
54
+ /** A value was drawn from an empty pool. Fatal, never a fabricated substitute. */
55
55
  SYNTH_EMPTY_POOL: "SYNTH_EMPTY_POOL",
56
56
  /** A `defineSynthProfile` spec was not usable (a missing or blank `name`). Fatal. */
57
57
  SYNTH_INVALID_PROFILE: "SYNTH_INVALID_PROFILE",
58
58
  /**
59
59
  * A caller-supplied selector (a message kind, a document type, a corpus mix entry, a claim
60
- * variant, a Bundle type, a resource profile) is not in the closed set that governs it. Fatal —
60
+ * variant, a Bundle type, a resource profile) is not in the closed set that governs it. Fatal:
61
61
  * see `resolveKind`: a selector union is erased at run time, and a selector that falls through
62
62
  * either mislabels the fixture or hands the value to a peer builder that quotes it back.
63
63
  */
@@ -151,7 +151,7 @@ function makeCorpus(seed, artifacts, quirks = []) {
151
151
  var SYNTHETIC_ASSIGNING_AUTHORITY = Object.freeze({
152
152
  /** The human-readable assigning-authority namespace id (HL7 HD.1). */
153
153
  namespaceId: "COSYTE-SYNTH",
154
- /** The universal id — an OID under HL7's example arc `2.16.840.1.113883.19` (HD.2). */
154
+ /** The universal id, an OID under HL7's example arc `2.16.840.1.113883.19` (HD.2). */
155
155
  universalId: "2.16.840.1.113883.19.999",
156
156
  /** The universal id type (HD.3). */
157
157
  universalIdType: "ISO"
@@ -722,7 +722,7 @@ var ASTM_QUIRKS = Object.freeze({
722
722
  name: "unknown-record-type",
723
723
  format: "astm",
724
724
  intendedWarnings: Object.freeze(["ASTM_RECORD_UNKNOWN_TYPE"]),
725
- grounding: "ASTM E1394 permits manufacturer/site-defined record types; a Z record is surfaced as unsupported. A tolerable code (in the parser's defineAstmProfile allow-list) \u2014 a consumer authors a profile to re-badge it; no built-in public profile does.",
725
+ grounding: "ASTM E1394 permits manufacturer/site-defined record types; a Z record is surfaced as unsupported. A tolerable code (in the parser's defineAstmProfile allow-list): a consumer authors a profile to re-badge it; no built-in public profile does.",
726
726
  disposition: "bare"
727
727
  })
728
728
  });