@cosyte/synth 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +149 -17
- package/README.md +47 -45
- package/dist/astm/index.cjs +7 -7
- package/dist/astm/index.cjs.map +1 -1
- package/dist/astm/index.d.cts +47 -47
- package/dist/astm/index.d.ts +47 -47
- package/dist/astm/index.mjs +7 -7
- package/dist/astm/index.mjs.map +1 -1
- package/dist/ccda/index.cjs +15 -15
- package/dist/ccda/index.cjs.map +1 -1
- package/dist/ccda/index.d.cts +31 -31
- package/dist/ccda/index.d.ts +31 -31
- package/dist/ccda/index.mjs +15 -15
- package/dist/ccda/index.mjs.map +1 -1
- package/dist/deid/index.cjs +15 -15
- package/dist/deid/index.cjs.map +1 -1
- package/dist/deid/index.d.cts +37 -37
- package/dist/deid/index.d.ts +37 -37
- package/dist/deid/index.mjs +15 -15
- package/dist/deid/index.mjs.map +1 -1
- package/dist/{example-codes-DeXcnCSK.d.cts → example-codes-88f3_bQZ.d.cts} +8 -8
- package/dist/{example-codes-DeXcnCSK.d.ts → example-codes-88f3_bQZ.d.ts} +8 -8
- package/dist/fhir/index.cjs +12 -12
- package/dist/fhir/index.cjs.map +1 -1
- package/dist/fhir/index.d.cts +46 -46
- package/dist/fhir/index.d.ts +46 -46
- package/dist/fhir/index.mjs +12 -12
- package/dist/fhir/index.mjs.map +1 -1
- package/dist/hl7/index.cjs +6 -6
- package/dist/hl7/index.cjs.map +1 -1
- package/dist/hl7/index.d.cts +45 -45
- package/dist/hl7/index.d.ts +45 -45
- package/dist/hl7/index.mjs +6 -6
- package/dist/hl7/index.mjs.map +1 -1
- package/dist/index.cjs +9 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +49 -49
- package/dist/index.d.ts +49 -49
- package/dist/index.mjs +9 -9
- package/dist/index.mjs.map +1 -1
- package/dist/ncpdp/index.cjs +3 -3
- package/dist/ncpdp/index.cjs.map +1 -1
- package/dist/ncpdp/index.d.cts +42 -42
- package/dist/ncpdp/index.d.ts +42 -42
- package/dist/ncpdp/index.mjs +3 -3
- package/dist/ncpdp/index.mjs.map +1 -1
- package/dist/{providers-OLz3zAc-.d.cts → providers-B9uVinAe.d.cts} +24 -24
- package/dist/{providers-OLz3zAc-.d.ts → providers-B9uVinAe.d.ts} +24 -24
- package/dist/{quirk-C9t9CkPS.d.ts → quirk-HZdznAkM.d.ts} +23 -23
- package/dist/{quirk-DYMDojVw.d.cts → quirk-IaHp4z7N.d.cts} +23 -23
- package/dist/x12/index.cjs +6 -6
- package/dist/x12/index.cjs.map +1 -1
- package/dist/x12/index.d.cts +44 -44
- package/dist/x12/index.d.ts +44 -44
- package/dist/x12/index.mjs +6 -6
- package/dist/x12/index.mjs.map +1 -1
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,16 +1,154 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
3
|
+
## 0.0.9
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- b19fa4e: PHI scan: all-mode now reads every tracked file, not only the three scan roots, and a scan root of the wrong kind refuses instead of exiting 0 or 1.
|
|
8
|
+
|
|
9
|
+
On the base of this change, `pnpm phi-scan` read 176 of 225 tracked files: everything outside `src/`, `test/` and `scripts/` was opened by neither route, so `package.json`, `pnpm-lock.yaml`, every workflow and every root config file were scanned by nothing, and a repo-root file carrying a name, an SSN and an email exited 0 on both routes. All-mode now reconciles what it walked against `git ls-files` and reads every tracked file the walk did not reach: 198 of 225, with only markdown and the vendored archives left out, the archives named one literal path at a time.
|
|
10
|
+
|
|
11
|
+
The widening is a union with the walk and never a replacement of it, and the pre-commit route enumerates exactly what it always did: it still narrows to the three roots, deliberately, because the widening needs a corpus exemption and an exemption on the commit-blocking route is exactly what has subtracted a real detection elsewhere. One thing does change on every route, and it is stated rather than glossed: the publisher contact address in `package.json`, which the previous scope had never opened, is now declared in the allow-list, and an allow-list entry clears its value wherever it appears. That is one literal address, an organization's published contact rather than a person's, and the suite now pins both that it is cleared and that any other address at the same domain still fails.
|
|
12
|
+
|
|
13
|
+
Two walk-root defects close with it, both measured on this package rather than assumed from a sibling. A scan root that is a regular file threw `ENOTDIR` past the scanner's catch and exited 1, the code the contract reserves for "hits found"; a root symlinked at another root reported `OK, no hits` and exited 0 with the whole test corpus absent from disk. Both now refuse with 2, through an `lstat` on each root before the walk, because `existsSync` follows a link. The reconciliation also closes two states the per-root observation rule could not see: an absent directory inside a root, and a root emptied down to a single file, both of which used to report a clean sweep under a plausible denominator.
|
|
14
|
+
|
|
15
|
+
The one finding from reading the newly-opened files by hand was that publisher contact address, declared as an exact address rather than removed: it is the only email in any of them and there is no SSN shape in any of them.
|
|
16
|
+
|
|
17
|
+
## 0.0.8
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- 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.
|
|
22
|
+
|
|
23
|
+
`.changeset/config.json` set `"changelog": false`, so no release ever wrote a version heading
|
|
24
|
+
into `CHANGELOG.md` and nothing ever rolled `[Unreleased]` over. Every published version of this
|
|
25
|
+
package therefore carried a changelog with **no version headings at all**: one `[Unreleased]`
|
|
26
|
+
heading over the whole history, and a preamble stating that the first pre-alpha release "will
|
|
27
|
+
ship" the initial public API surface listed below it. That promise was already in the future tense
|
|
28
|
+
in the very tarball that fulfilled it, and it stayed in the future tense through every release
|
|
29
|
+
after. `CHANGELOG.md` is listed in `package.json` `files`, so this was text on the disk of everyone
|
|
30
|
+
who installed the package, not internal bookkeeping.
|
|
31
|
+
|
|
32
|
+
**The flag is what changed, not the prose.** Correcting the sentence by hand would have left the
|
|
33
|
+
mechanism that wrote it in place, and the next release would have drifted the same way.
|
|
34
|
+
`changelog` now names the generator that ships with Changesets, so a release writes its own
|
|
35
|
+
version heading and its own entry from the changesets it consumed, and **the changeset summary is
|
|
36
|
+
now the changelog entry**. Nothing new is depended on: the generator is an entry point of
|
|
37
|
+
`@changesets/cli`, which was already a dev dependency.
|
|
38
|
+
|
|
39
|
+
**The file's shape changed with it, deliberately.** Changesets prepends a release by replacing the
|
|
40
|
+
first newline in the file, so exactly one line can sit above generated output. The hand-written
|
|
41
|
+
preamble sat on line 3, which means a release would have inserted itself between the heading and
|
|
42
|
+
the preamble and split the header in two. The hand-maintained history has therefore moved under a
|
|
43
|
+
`## Released before this file was generated` heading, with the false preamble replaced by an
|
|
44
|
+
accurate one. Three pieces of hand-workflow scaffolding were dropped and no entry was reworded:
|
|
45
|
+
the `[Unreleased]` heading, its link definition at the foot of the file, and the two empty section
|
|
46
|
+
stubs (`### Deprecated` and `### Removed`) waiting for the next hand-written entry. The history
|
|
47
|
+
itself is left as it was written rather than re-sorted into version sections, because the file
|
|
48
|
+
never recorded which release each entry went out in and the text is already on disk in published
|
|
49
|
+
copies.
|
|
50
|
+
|
|
51
|
+
**Changesets' Prettier pass is deliberately left ON here, and that was derived from this repo
|
|
52
|
+
rather than copied from a sibling.** This repo has no `.prettierignore` at all and its
|
|
53
|
+
`format:check` globs root markdown, so `CHANGELOG.md` is inside the repo's own formatting gate and
|
|
54
|
+
its archived history is already Prettier-canonical. Both directions were measured. With the pass
|
|
55
|
+
on, the archived history comes through a release byte identical, so leaving it on costs nothing.
|
|
56
|
+
With the pass off, what `changeset version` itself writes is no longer Prettier-canonical even for
|
|
57
|
+
the simplest possible summary, because it writes the version heading and `### Patch Changes` on
|
|
58
|
+
adjacent lines with no blank line between them. **A sentence a sibling carries is false here and
|
|
59
|
+
is deliberately not repeated**: this repo's `version` script also runs `prettier --write` over
|
|
60
|
+
`CHANGELOG.md` one link after `changeset version`, where a sibling's covers only `package.json`
|
|
61
|
+
and `src/index.ts`, so turning the pass off here would not open the Version PR red and would not
|
|
62
|
+
keep Prettier away from the archived history either. It would only leave the tool's own output
|
|
63
|
+
failing the gate that covers the file it wrote, which buys nothing. A sibling whose
|
|
64
|
+
`.prettierignore` lists `*.md` needs the opposite setting, and resyncing the value between repos is
|
|
65
|
+
how a release starts rewriting already-published text.
|
|
66
|
+
|
|
67
|
+
Pinned by `test/scripts/changelog-generation.test.ts`, which runs the real `changeset version`
|
|
68
|
+
against the real `CHANGELOG.md` and the real config in a throwaway package rather than
|
|
69
|
+
reimplementing where the tool inserts text. Nine of its fifteen cases are red against the previous
|
|
70
|
+
state, measured on the tree this change was written against rather than recalled. The throwaway
|
|
71
|
+
package is a real git repository, because the generator prefixes each entry with the short commit
|
|
72
|
+
sha that added the changeset and a tree with no history would exercise a line shape no release
|
|
73
|
+
writes. **The rule it enforces is that nothing but the H1 sits above the first heading, and it is
|
|
74
|
+
asserted on the released document as well as the committed one**: a rule phrased as "the archive
|
|
75
|
+
heading comes second" holds only until the first release writes its own version heading there,
|
|
76
|
+
which would have redded the first Version PR this configuration ever opened. Every version-heading
|
|
77
|
+
comparison is a whole-heading match rather than a substring, demonstrated on real generator output,
|
|
78
|
+
because this package sits past the point where `## 0.0.1` is a prefix of a heading it does not
|
|
79
|
+
have. Three further controls: the same inputs with `"changelog": false` must write no version
|
|
80
|
+
heading at all, so the flag is proved load-bearing rather than incidental; the same inputs with the
|
|
81
|
+
Prettier pass off must produce a document this repo's own formatting gate rejects; and the old file
|
|
82
|
+
shape must reproduce the split header, so the shape rule is demonstrated rather than asserted.
|
|
83
|
+
|
|
84
|
+
One upstream behaviour is worth knowing before debugging a release, and is recorded in that file:
|
|
85
|
+
Changesets wraps the changelog write in a try/catch that only warns. A tree whose declared Prettier
|
|
86
|
+
config cannot be resolved bumps the version, consumes the changeset, and writes no changelog at all.
|
|
87
|
+
A release that publishes with an unchanged changelog is that failure, not a setting that quietly
|
|
88
|
+
reverted.
|
|
89
|
+
|
|
90
|
+
`.changeset/README.md` and `CLAUDE.md` said to add the entry to `CHANGELOG.md` by hand and now say
|
|
91
|
+
to write it in the changeset. No runtime code, no public API, no warning code, no generated byte and
|
|
92
|
+
no synthetic-safety behaviour changed.
|
|
93
|
+
|
|
94
|
+
- 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.
|
|
95
|
+
|
|
96
|
+
**Nothing about generation changed.** No export was added, removed or renamed, no warning or fatal
|
|
97
|
+
code moved, and a given seed still produces byte-identical output for every format. The rewrite is
|
|
98
|
+
punctuation in prose: an em dash became a colon or a comma in almost every case, and elsewhere a
|
|
99
|
+
period, a semicolon, a pair of parentheses, a connecting word, or nothing at all, whichever the
|
|
100
|
+
sentence wanted. Every value this package emits is still drawn from the same reserved
|
|
101
|
+
and synthetic sources it always was, and no fixture byte moved.
|
|
102
|
+
|
|
103
|
+
Two of those surfaces are worth naming because they are read outside the repository.
|
|
104
|
+
`package.json`'s `description` is what npm shows on the package page and in every search result,
|
|
105
|
+
and the fourteen pages under `docs-content/` are what ships to the documentation site. The two
|
|
106
|
+
runtime-visible strings are the `grounding` notes on the HL7 v2 and ASTM quirk descriptors, which
|
|
107
|
+
are frozen registry text a consumer can read but which nothing is keyed on.
|
|
108
|
+
|
|
109
|
+
The hand-maintained changelog history below `## Released before this file was generated` is
|
|
110
|
+
deliberately untouched. Those entries were accurate when they were written, and a dated record is
|
|
111
|
+
corrected above itself rather than edited.
|
|
112
|
+
|
|
113
|
+
- a18b96d: The README lockup now links to cosyte.com (`ASSETS`).
|
|
114
|
+
|
|
115
|
+
The `<picture>` block above the H1 is wrapped in an anchor to https://cosyte.com, per the founder
|
|
116
|
+
requirement of 2026-08-06. Nothing inside the block moved: the `<source>`, the `<img>`, the alt text
|
|
117
|
+
and both tile URLs are byte-identical.
|
|
118
|
+
|
|
119
|
+
What the anchor does was measured on both surfaces by `fhir`, not assumed, because fourteen READMEs
|
|
120
|
+
carry this shape. On GitHub the anchor works and the colour-scheme switch keeps working, because the
|
|
121
|
+
`<img>` stays a direct child of `<picture>`, which is the condition the HTML spec puts on `<source>`
|
|
122
|
+
applying at all. On an npm package page the anchor is lost: npm wraps a README image in its own
|
|
123
|
+
anchor to the image file, a nested anchor is not representable, so the parser closes ours early and
|
|
124
|
+
the image ends up linked to the image file rather than to cosyte.com. Shipped anyway by founder
|
|
125
|
+
decision of 2026-08-07: on npm that is no worse than the unlinked lockup it replaces, and GitHub is
|
|
126
|
+
where these READMEs are read.
|
|
127
|
+
|
|
128
|
+
## Released before this file was generated
|
|
129
|
+
|
|
130
|
+
Every release section above this heading is written by
|
|
131
|
+
[Changesets](https://github.com/changesets/changesets) from the changesets in `.changeset/`, newest
|
|
132
|
+
release first. The release writes its own version heading, so nothing above this line is maintained
|
|
133
|
+
by hand: a change is recorded by adding a changeset, and that changeset's summary is the entry a
|
|
134
|
+
reader sees here.
|
|
135
|
+
|
|
136
|
+
Everything below this heading was maintained by hand. It sat under a single `[Unreleased]` heading
|
|
137
|
+
that no release ever rolled over, and `CHANGELOG.md` is listed in `package.json` `files`, so every
|
|
138
|
+
published tarball of this package described its own already-shipped contents as unreleased, under a
|
|
139
|
+
preamble promising that a first pre-alpha release would ship the API surface listed under it. That
|
|
140
|
+
promise was already in the future tense in the very tarball that fulfilled it, and it stayed in the
|
|
141
|
+
future tense through every release after. The history is left exactly as it was written rather than re-sorted into
|
|
142
|
+
version sections: the file never recorded which release any entry went out in, and this is the text
|
|
143
|
+
that installed copies already carry on disk. Only three things were dropped, all of them scaffolding
|
|
144
|
+
for the hand-written workflow that no longer runs: the `[Unreleased]` heading itself, its link
|
|
145
|
+
definition at the foot of the file, and the two empty section stubs (`### Deprecated` and
|
|
146
|
+
`### Removed`) that were waiting to receive the next hand-written entry. No entry was reworded.
|
|
147
|
+
|
|
148
|
+
The entries below follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the generated
|
|
149
|
+
sections above use the format Changesets writes, which is a version heading and a list of the
|
|
150
|
+
changesets that release consumed. This package is on the pre-alpha `0.0.x` ladder and adheres to
|
|
151
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
14
152
|
|
|
15
153
|
### Added
|
|
16
154
|
|
|
@@ -541,10 +679,6 @@ its public history at `0.0.x`, per the cosyte version ladder (`0.0.x` until firs
|
|
|
541
679
|
a frozen exported constant, so a consumer asserting them verbatim is affected. The `format` values and
|
|
542
680
|
the shape of the constant are unchanged, and no generated byte changes.
|
|
543
681
|
|
|
544
|
-
### Deprecated
|
|
545
|
-
|
|
546
|
-
### Removed
|
|
547
|
-
|
|
548
682
|
### Fixed
|
|
549
683
|
|
|
550
684
|
- **`pnpm phi-scan` could print `OK` and exit 0 over a whole scan root it never opened, because the
|
|
@@ -1016,5 +1150,3 @@ attw` and `find dist -name '*.d.*ts' -delete && pnpm attw` printed the sentence
|
|
|
1016
1150
|
of the positions that have been checked. That wording is now in the test file's own header, in
|
|
1017
1151
|
`docs-content/limitations.md` and in `CLAUDE.md`, so the next person to add a position adds a slot
|
|
1018
1152
|
rather than trusting a count.
|
|
1019
|
-
|
|
1020
|
-
[Unreleased]: https://github.com/cosyte/synth/commits/main
|
package/README.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
<
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
46
|
-
the formats it pairs)
|
|
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
|
|
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
|
|
77
|
-
Core 6.1.0 profiles** (bring your own `StructureDefinition`s
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
200
|
-
layer
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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; // []
|
|
289
|
-
r.clinicalScrubbed; // []
|
|
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
|
|
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
|
|
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
|
|
315
|
-
isSyntheticNpi(safe.npi(rng)); // true
|
|
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
|
|
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
|
|
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
|
|
327
|
-
A seed, and only the seed, determines the output
|
|
328
|
-
- **Immutable
|
|
329
|
-
- **Zero third-party runtime dependencies
|
|
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
|
package/dist/astm/index.cjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
});
|