@cosyte/synth 0.0.3 → 0.0.5
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 +335 -0
- package/README.md +6 -2
- package/dist/astm/index.cjs +83 -42
- package/dist/astm/index.cjs.map +1 -1
- package/dist/astm/index.d.cts +1 -1
- package/dist/astm/index.d.ts +1 -1
- package/dist/astm/index.mjs +83 -42
- package/dist/astm/index.mjs.map +1 -1
- package/dist/ccda/index.cjs +89 -44
- package/dist/ccda/index.cjs.map +1 -1
- package/dist/ccda/index.d.cts +5 -3
- package/dist/ccda/index.d.ts +5 -3
- package/dist/ccda/index.mjs +89 -44
- package/dist/ccda/index.mjs.map +1 -1
- package/dist/deid/index.cjs +92 -13
- package/dist/deid/index.cjs.map +1 -1
- package/dist/deid/index.d.cts +1 -1
- package/dist/deid/index.d.ts +1 -1
- package/dist/deid/index.mjs +93 -14
- package/dist/deid/index.mjs.map +1 -1
- package/dist/fhir/index.cjs +78 -5
- package/dist/fhir/index.cjs.map +1 -1
- package/dist/fhir/index.mjs +78 -5
- package/dist/fhir/index.mjs.map +1 -1
- package/dist/hl7/index.cjs +89 -41
- package/dist/hl7/index.cjs.map +1 -1
- package/dist/hl7/index.d.cts +1 -1
- package/dist/hl7/index.d.ts +1 -1
- package/dist/hl7/index.mjs +89 -41
- package/dist/hl7/index.mjs.map +1 -1
- package/dist/index.cjs +91 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +126 -8
- package/dist/index.d.ts +126 -8
- package/dist/index.mjs +89 -41
- package/dist/index.mjs.map +1 -1
- package/dist/ncpdp/index.cjs +56 -4
- package/dist/ncpdp/index.cjs.map +1 -1
- package/dist/ncpdp/index.mjs +56 -4
- package/dist/ncpdp/index.mjs.map +1 -1
- package/dist/{quirk-JLyO1Ncj.d.ts → quirk-C9t9CkPS.d.ts} +17 -6
- package/dist/{quirk-DmkgoZdh.d.cts → quirk-DYMDojVw.d.cts} +17 -6
- package/dist/x12/index.cjs +82 -13
- package/dist/x12/index.cjs.map +1 -1
- package/dist/x12/index.d.cts +2 -1
- package/dist/x12/index.d.ts +2 -1
- package/dist/x12/index.mjs +83 -14
- package/dist/x12/index.mjs.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -460,6 +460,56 @@ its public history at `0.0.x`, per the cosyte version ladder (`0.0.x` until firs
|
|
|
460
460
|
|
|
461
461
|
### Changed
|
|
462
462
|
|
|
463
|
+
- **Fatal messages come from a frozen registry, and `SynthError` takes no value parameter.** Every
|
|
464
|
+
message is a fixed entry in the newly exported `SYNTH_FATAL_MESSAGES` table, keyed by code, and the
|
|
465
|
+
constructor is `SynthError(code)`: there is no longer a position through which a caller-supplied
|
|
466
|
+
string can reach `message`, `stack`, or any field on the thrown object.
|
|
467
|
+
|
|
468
|
+
**The severity, stated honestly.** This is not a leak of patient data and describing it as one
|
|
469
|
+
would be its own dishonesty. `@cosyte/synth` generates synthetic fixtures, so the values a refusal
|
|
470
|
+
used to quote were quirk names, format labels, code-system URIs and money strings. What was wrong
|
|
471
|
+
was the _shape_: every fatal took a value parameter, and the only thing keeping PHI out of a
|
|
472
|
+
diagnostic was that callers happened to be passing harmless values. The audit of the thirteen
|
|
473
|
+
cosyte repos found that the single property separating the packages that leak from the ones that
|
|
474
|
+
are genuinely prevented is exactly this: whether the message factory takes a value parameter at
|
|
475
|
+
all. It now does not.
|
|
476
|
+
|
|
477
|
+
The prose mattered as much as the code here. The claim that warning messages are PHI-free by
|
|
478
|
+
construction spread through this ecosystem as a _sentence_, not as shared runtime code, and this
|
|
479
|
+
package had inherited the sentence and used it as a reason not to bound anything. Every surface
|
|
480
|
+
carrying it has been corrected in the same change rather than restated in fresher words:
|
|
481
|
+
`src/codes.ts` (whose `@param message` read "never contains PHI, there is none"),
|
|
482
|
+
`docs-content/troubleshooting.md`, `docs-content/concepts-archetype.md`,
|
|
483
|
+
`docs-content/limitations.md`, `docs-content/guides-quirks.md` and `README.md`.
|
|
484
|
+
|
|
485
|
+
- **Every caller-supplied selector is resolved against its closed set, not trusted.** `resolveKind`
|
|
486
|
+
and `resolveMix` (new, exported, `src/select.ts`) sit at the entry point of every option that names
|
|
487
|
+
a member of a union: a message kind, an `ADT` trigger, a document type, a corpus mix entry, an `837`
|
|
488
|
+
variant, a Bundle type, a `Patient` profile, a quirk kind. An unrecognised one is a fatal
|
|
489
|
+
`SYNTH_UNSUPPORTED_KIND`.
|
|
490
|
+
|
|
491
|
+
**This is the same defect as the message one, not a separate tidy-up**, and it was found by the
|
|
492
|
+
refuter after the message fix had been called done. A selector union does not exist at run time, and
|
|
493
|
+
an unresolved selector did three things at once. It travelled into an optional peer builder, which is
|
|
494
|
+
entitled to quote it back in its own `TypeError` and does, so a caller-supplied string reached an
|
|
495
|
+
`err.message` and an `err.stack` through a `@cosyte/synth` entry point (`generateCcda`,
|
|
496
|
+
`generateCcdaQuirk`, `ccdaCorpus`, `ccdaQuirkCorpus`). It became an `Artifact.kind` and a
|
|
497
|
+
`manifest.counts` key, which is the structural-identifier position the whole model half of this work
|
|
498
|
+
is about (`x12Corpus({ mix })`). And it fell out of an exhaustive `switch` as `undefined`, which
|
|
499
|
+
neither reads as an error nor is one.
|
|
500
|
+
|
|
501
|
+
- **`assertIntendedWarnings` loses its leading `quirk` parameter**, and is now
|
|
502
|
+
`assertIntendedWarnings(intendedWarnings, bareWarnings)`. It existed only to be interpolated into
|
|
503
|
+
the refusal. A parameter whose sole job is to reach a message is the shape being removed, so it was
|
|
504
|
+
deleted rather than left in place and ignored.
|
|
505
|
+
|
|
506
|
+
- **`resolveQuirk` refuses a descriptor found under a different format's registry**, on the same
|
|
507
|
+
`SYNTH_UNSUPPORTED_QUIRK` code. The `format` argument used to be read only to build the message
|
|
508
|
+
text; it is now compared, never rendered.
|
|
509
|
+
|
|
510
|
+
- **`defineSynthProfile` raises `SynthError` with `SYNTH_INVALID_PROFILE`** for a missing or blank
|
|
511
|
+
`name`, where it previously raised a `TypeError`.
|
|
512
|
+
|
|
463
513
|
- Replaced the parser-archetype scaffold stubs (`parseSynth`, `WARNING_CODES`, `FATAL_CODES`) with the
|
|
464
514
|
generator surface — `@cosyte/synth` is a synthetic-fixture **generator**, not a parser.
|
|
465
515
|
- **Docs:** refreshed the `README.md` status block to describe the **feature-complete** generator surface
|
|
@@ -497,6 +547,234 @@ its public history at `0.0.x`, per the cosyte version ladder (`0.0.x` until firs
|
|
|
497
547
|
|
|
498
548
|
### Fixed
|
|
499
549
|
|
|
550
|
+
- **A file that appeared and disappeared while the PHI gate was running made it refuse the whole
|
|
551
|
+
sweep.** A full scan lists every file under `src/`, `test/` and `scripts/` and then reads them one
|
|
552
|
+
by one. Anything created and deleted between those two steps was read after it was gone, and the
|
|
553
|
+
scanner stopped with an error rather than a verdict. That is reachable in this repository rather
|
|
554
|
+
than theoretical: the gate's own test suite writes short-lived files into two of the three
|
|
555
|
+
directories it scans, and sweeping the working tree while that suite ran stopped 8 of 165 sweeps.
|
|
556
|
+
A sibling package hit the same shape from its build tool and it blocked a release.
|
|
557
|
+
|
|
558
|
+
**The refusal was right; the file list was wrong, so the list is what changed.** Exactly one case
|
|
559
|
+
is now tolerated: a file the scan listed itself, that is not committed, and that is missing when
|
|
560
|
+
the scan reaches it. It is reported on stderr as skipped, never dropped in silence, and the
|
|
561
|
+
"files scanned" figure counts what was actually read, so a skip lowers it instead of padding it.
|
|
562
|
+
|
|
563
|
+
**Everything else still stops the scan.** A committed file that cannot be read, a read that fails
|
|
564
|
+
for any other reason (unreadable is not the same as absent), a skipped file that is back on disk
|
|
565
|
+
when the scan finishes, and a repository that cannot report what it tracks. A full scan that ends
|
|
566
|
+
up having read nothing at all is refused outright, so tolerating a missing file can never turn
|
|
567
|
+
into a clean report of a tree nothing was read from. Pre-commit scans read committed content
|
|
568
|
+
directly and never depended on any of this.
|
|
569
|
+
|
|
570
|
+
Each of those bounds is pinned by a test that reds when the bound is widened, verified by widening
|
|
571
|
+
each one in turn. **One is not pinned and is named here rather than implied:** the check that a
|
|
572
|
+
skipped file has not come back. Reaching it needs a timing-dependent test, which is the failure
|
|
573
|
+
this defect teaches, so it is left uncovered deliberately; losing it would cost that re-check, not
|
|
574
|
+
the tolerance's limits. **One residual is disclosed rather than closed:** the re-check matches on
|
|
575
|
+
the file's path, not its contents, so an uncommitted file _renamed_ mid-scan goes unread under a
|
|
576
|
+
clean report. Committing it makes it tracked and no longer tolerable, and pre-commit reads
|
|
577
|
+
committed content either way. Closing it in general needs a content-addressed scan; re-listing the
|
|
578
|
+
scanned directories afterwards would close the in-directory half more cheaply, at the cost of a
|
|
579
|
+
second walk and a new way to refuse. Both are a design trade for a later change rather than
|
|
580
|
+
something impossible.
|
|
581
|
+
|
|
582
|
+
**Two smaller limits are recorded rather than closed, and both fail in the refusing direction.**
|
|
583
|
+
When the repository cannot report what it tracks the scan still stops, but the message names the
|
|
584
|
+
file it could not read rather than that reason. And the tracked-file list is read through a 1 MiB
|
|
585
|
+
buffer, so a repository large enough to exceed it would quietly stop applying the tolerance —
|
|
586
|
+
measured here at 6,556 bytes, three orders of magnitude of headroom.
|
|
587
|
+
|
|
588
|
+
- **The test suite could fail on a busy machine while the code under test was correct.** The
|
|
589
|
+
per-test timeout is a wall-clock budget, so it measures the machine as much as the code. These
|
|
590
|
+
suites are CPU-bound, in that their running time is set by how much processor they actually get
|
|
591
|
+
rather than by the work the code does, so the two heaviest C-CDA sweeps ran within about 2x of the 10 s limit
|
|
592
|
+
with a core to themselves, and crossed it when sharing one, reporting a failure that reproduced
|
|
593
|
+
nowhere else. A failure that means "the machine was busy" costs more than it saves, because the
|
|
594
|
+
next real one is read as noise too.
|
|
595
|
+
|
|
596
|
+
Two fixes, chosen so that a genuine hang still fails fast. The C-CDA quirk sweeps, the heaviest
|
|
597
|
+
cases in the package and the ones that build a document and parse it twice, now carry their own explicit
|
|
598
|
+
generous ceilings, matching the sibling C-CDA suites that already had them. And the PHI-gate suite,
|
|
599
|
+
which runs the scanner in a real subprocess ~65 times, now starts those subprocesses with `node`
|
|
600
|
+
rather than the on-the-fly TypeScript runner: measured on one box, 0.6 s against 2.1 s per start,
|
|
601
|
+
taking the file from ~145 s to ~75 s. Nothing about the scanner changed, and one test still runs it
|
|
602
|
+
the original way so the two invocations are held to the same verdict.
|
|
603
|
+
|
|
604
|
+
**The global timeout is deliberately unchanged.** Raising it would have been the shorter fix and
|
|
605
|
+
would have traded a false failure for a false pass, letting a genuinely stuck test sit for a minute
|
|
606
|
+
before anyone heard about it.
|
|
607
|
+
|
|
608
|
+
- **Three of the PHI gate's seven structured detectors decided what to inspect from the file's NAME,
|
|
609
|
+
so byte-identical content was refused as `probe.xml` and passed as `probe.ts`.** `scanCcda` and
|
|
610
|
+
`scanNcpdpScript` returned early unless the path ended `.xml`; `scanFhir` unless it ended `.json`.
|
|
611
|
+
That mattered in this repo specifically, because it builds documents as inline string literals in
|
|
612
|
+
TypeScript tests — the scan enumerated those files and then structurally read nothing in them.
|
|
613
|
+
|
|
614
|
+
**Every arm now keys off the bytes.** Each of the three admits a target by its extension **or** what
|
|
615
|
+
the bytes say — a CDA root/namespace, a SCRIPT `<Message>` envelope, FHIR's `resourceType` for its
|
|
616
|
+
textual route. **Admission is additive**: no target admitted before is refused admission now.
|
|
617
|
+
Extensions are matched case-insensitively, so `.XML` is an XML file too. `scanFhir` gained a second
|
|
618
|
+
route for the hard half: a resource in a TypeScript object literal is not valid JSON, so a `.json`
|
|
619
|
+
gate plus `JSON.parse` could never have reached it however the extension check was widened. Its
|
|
620
|
+
**structural** route is not marker-gated — it runs whenever `JSON.parse` succeeds, with no
|
|
621
|
+
`resourceType` check, exactly as it did for `.json` before.
|
|
622
|
+
|
|
623
|
+
**The marker is the false-positive defence for the CONTENT routes**, which is narrower than "nothing
|
|
624
|
+
is examined without a marker" — an `.xml` path still reaches the C-CDA arm with no marker at all, and
|
|
625
|
+
FHIR's structural route still reads any parseable JSON. A gate that flagged every `family:` in every
|
|
626
|
+
source file would be turned off, which is worse than the gap it closes.
|
|
627
|
+
|
|
628
|
+
**NOTHING SUBTRACTS. No detector was taught to skip anything**, and getting to that took three
|
|
629
|
+
review passes. Widening the arms made this scanner's own test suite one of its targets, and that
|
|
630
|
+
suite necessarily writes name elements. The first three attempts answered it by teaching the
|
|
631
|
+
**scanner** to skip template placeholders — and on a PHI detector a skip rule has to be exactly
|
|
632
|
+
right, which it twice was not. It silenced `Anderson ...` (a containment test), then `${"Anderson"}`
|
|
633
|
+
(any interpolation body elided, left open on a claim that closing it would break an existing idiom
|
|
634
|
+
when `git show main` shows the same branch had just introduced that idiom), then `{{Anderson ${s}}`
|
|
635
|
+
(one regex pass matching straight across two constructs the source never nested). Each remedy bought
|
|
636
|
+
one more evasion shape, which is the signature of a rule that does not belong in a PHI gate.
|
|
637
|
+
|
|
638
|
+
The rule is **gone**. The suite assembles its fixture elements at run time instead, so the detector
|
|
639
|
+
stays maximally literal and there is no subtraction to audit. Measured across sixteen shapes —
|
|
640
|
+
including every one a refuter used to break the earlier drafts — `main` and this branch return the
|
|
641
|
+
**identical** verdict; the only differences anywhere are files the widened arms now reach that
|
|
642
|
+
`main` never read.
|
|
643
|
+
|
|
644
|
+
**The measurement that justified the deleted predicate is corrected here too**, because the wrong
|
|
645
|
+
version had been written down as the evidence: the first run over 170 files returned four hits across
|
|
646
|
+
two files — **two real names** the widening had just made visible (now declared in the allow-list) and
|
|
647
|
+
two name elements in this scanner's own fixtures. Not "four placeholders, none a name".
|
|
648
|
+
|
|
649
|
+
Two things the change surfaced, both fixed here. The C-CDA name/telecom sweep was always
|
|
650
|
+
**document-wide** but labelled every hit `recordTarget/…`; a name in `<author>` came back labelled
|
|
651
|
+
with a location it was not in. The sweep is kept (an informant's name in a committed fixture is as
|
|
652
|
+
real as a patient's) and only the false claim about _where_ is withdrawn. And two hand-written
|
|
653
|
+
tokens in a de-identifier test became visible for the first time; they are **declared in the
|
|
654
|
+
allow-list**, with what that costs written next to them, rather than edited out of a test whose
|
|
655
|
+
point requires exactly those shapes.
|
|
656
|
+
|
|
657
|
+
The characterization test that pinned the old behaviour asserted the **opposite** of what is true
|
|
658
|
+
now. It was rewritten in place, not deleted — the same probe bytes, the same two paths, the opposite
|
|
659
|
+
expectation — alongside new pins for the limits this did **not** close: admission is file-scoped
|
|
660
|
+
rather than object-scoped, the C-CDA name loci remain
|
|
661
|
+
namespace-prefix-naive, and the textual FHIR route misses a phone whose `system` and `value` are
|
|
662
|
+
separated by another key (the structural route catches it — the two are not at parity, and the
|
|
663
|
+
routes are mutually exclusive).
|
|
664
|
+
|
|
665
|
+
- **The two scan modes disagreed about a git-ignored file and neither direction was tested.** All-mode
|
|
666
|
+
drops git-ignored files; `--staged` does not. Both directions are now exercised. **Neither is
|
|
667
|
+
changed**: all-mode walks the working tree, where an ignored path is build output; `--staged`
|
|
668
|
+
enumerates the index, which a file reaches only via `git add -f` and which a commit will therefore
|
|
669
|
+
carry. Refusing to read that one is the shipping direction.
|
|
670
|
+
|
|
671
|
+
- **The PHI commit gate could be argued into scanning nothing and still print `OK`.**
|
|
672
|
+
`scripts/phi-scan.ts` treated an `--allow-fixture <path>` as both a subtraction _and_ a scan
|
|
673
|
+
target, so `pnpm phi-scan --allow-fixture X` with no other argument built the target set `[X]`,
|
|
674
|
+
subtracted `X`, scanned zero files and exited 0 with the same `OK — no hits` line a real pass
|
|
675
|
+
prints. `--allow-fixture` is now purely subtractive and never seeds the target set; the mode is
|
|
676
|
+
decided by `--staged` and positional paths alone. Three invariants (`enforceObservation`) are
|
|
677
|
+
checked before any hit counting: an override that subtracts no enumerated target is an error
|
|
678
|
+
rather than an inert entry that reads as a live bypass, a target set emptied by overrides is
|
|
679
|
+
refused, and an enumeration that finds nothing is refused in every mode but `--staged` (where
|
|
680
|
+
"nothing staged" is legitimate). Both **summary** lines now carry the **denominator** — the count
|
|
681
|
+
of files scanned — so an `OK` is never read without the number it is an `OK` over. Ported from the
|
|
682
|
+
same defect closed in `ncpdp`.
|
|
683
|
+
|
|
684
|
+
- **The staged enumerator dropped whole classes of staged file, because its filter was the wrong
|
|
685
|
+
polarity.** `git diff --cached --diff-filter=AM` is an **allow-list of git status letters**: every
|
|
686
|
+
letter it does not name is skipped by default and silently. That is how it missed `R` — a fixture
|
|
687
|
+
`git mv`'d and edited to add PHI in one commit stages as a single rename entry — and `T`, a
|
|
688
|
+
tracked symlink replaced by a regular file carrying PHI. Each was found by a separate refuter pass
|
|
689
|
+
in `ncpdp`, which is the argument for the general fix rather than the two patches: the enumerator
|
|
690
|
+
now asks for `--diff-filter=d` (everything **except** deletions, which have no blob to read), so an
|
|
691
|
+
unknown or future status letter costs a wasted scan instead of a missed file. It also passes
|
|
692
|
+
`--no-renames`, which decomposes a rename into `D` + `A` so the **destination** path — the one
|
|
693
|
+
carrying the new content — is the path that gets read. Both directions are measured in the suite:
|
|
694
|
+
the tests assert the new flags catch the `R` and `T` cases **and** that the superseded `AM`
|
|
695
|
+
allow-list did not list the violator's path for either. That second assertion is also what keeps
|
|
696
|
+
the first honest — had git degraded the fixture to a plain `A`/`M`, `AM` would have listed it and
|
|
697
|
+
the test would red rather than pass for the wrong reason.
|
|
698
|
+
|
|
699
|
+
- **The scan stopped at `test/fixtures/`, leaving most of the repository unswept.** `SCAN_ROOTS` is
|
|
700
|
+
now `src/`, `test/` and `scripts/`, walked whole and shared by both all-mode and `--staged` through
|
|
701
|
+
a single `isScannable` predicate, so the **roots** narrow in one place. The gap mattered here
|
|
702
|
+
specifically: this repo builds messages as inline string literals in tests **outside** `fixtures/`,
|
|
703
|
+
and `scripts/` is hand-written tracked text that can carry a real address as easily as a fixture
|
|
704
|
+
can. Measured on the widening: **114 in-scope files became 170**. Markdown stays out of scope in
|
|
705
|
+
every mode, because documentation — including this scanner's own override log and allow-list —
|
|
706
|
+
legitimately quotes violator values.
|
|
707
|
+
|
|
708
|
+
**What the widening does NOT buy, stated because the obvious reading is wrong.** Over a `.ts` file
|
|
709
|
+
it delivers the format-agnostic floor and the content-gated arms (HL7 v2, X12, ASTM, NCPDP
|
|
710
|
+
Telecom), which sniff their own framing. It delivers nothing from the three **extension-gated**
|
|
711
|
+
arms: `scanCcda` and `scanNcpdpScript` return early unless the path ends `.xml`, and `scanFhir`
|
|
712
|
+
unless it ends `.json`. A byte-identical payload is caught as `probe.xml` and missed as `probe.ts`.
|
|
713
|
+
That gating predates this change and is deliberately left alone — content-sniffing XML and JSON out
|
|
714
|
+
of arbitrary TypeScript is a much larger job with its own false-positive surface, and doing it as a
|
|
715
|
+
side effect of a root change would grow the gate's teeth without review. It is recorded as a known
|
|
716
|
+
gap in the scanner's header rather than implied away, and the roots list no longer claims otherwise.
|
|
717
|
+
|
|
718
|
+
**SUPERSEDED — see "Three of the PHI gate's seven structured detectors…" under Fixed below.** The
|
|
719
|
+
paragraph above described the state of the gate when the roots were widened, and is kept as that
|
|
720
|
+
record. The three extension gates were subsequently removed as the separate job this paragraph said
|
|
721
|
+
they needed, so its present tense no longer holds.
|
|
722
|
+
|
|
723
|
+
- **The widened scan surfaced one real finding, and it is declared rather than suppressed.** The ten
|
|
724
|
+
vendored US Core 6.1.0 StructureDefinitions under `test/us-core-profiles/` each carry their
|
|
725
|
+
publisher's work-group contact address in `contact.telecom`. It is a real address, so it is a real
|
|
726
|
+
hit; it is not PHI, because it denotes a standards-body mailing list rather than a person receiving
|
|
727
|
+
care. It is cleared with a new, narrower allow-list tag — `EMAIL <address>`, one exact address —
|
|
728
|
+
rather than an `EMAILDOMAIN` entry that would clear every address at a live organizational domain,
|
|
729
|
+
and rather than an `--allow-fixture` bypass that would silence every other check on those ten files.
|
|
730
|
+
|
|
731
|
+
- **A missing allow-list exited 1, which reads as "hits found".** `loadAllowList` now runs inside the
|
|
732
|
+
invocation-error boundary, so an absent `scripts/phi-allow-list.txt` exits **2** like every other
|
|
733
|
+
invocation error.
|
|
734
|
+
|
|
735
|
+
- **The override log was parsed file-wide, so its own format template was a bypass.**
|
|
736
|
+
`phi-scan-overrides.md` documents itself with a literal `### <path>` heading above the entries
|
|
737
|
+
section; only `### <path>` subsections **under `## Entries`** are read as approvals now.
|
|
738
|
+
|
|
739
|
+
- **A false assertion in the scanner's test suite, deleted rather than reworded.** Its docblock said
|
|
740
|
+
the suite "deliberately does NOT test structured, field-level PHI detection". That was untrue when
|
|
741
|
+
written and stayed untrue for six formats — the HL7, FHIR, X12, NCPDP, ASTM and quirk suites had
|
|
742
|
+
been exercising exactly that. The dangerous half of a wrong assertion is not the code around it but
|
|
743
|
+
the next reader trusting it, so the sentence is gone. Two more went with it: the `STARTER` banner —
|
|
744
|
+
carried in `scripts/phi-scan.ts`, `phi-scan-overrides.md` and `.github/workflows/ci.yml`, all three
|
|
745
|
+
still describing a scanner that detects "SSN and email only" after every structured arm had landed —
|
|
746
|
+
and a claim in `docs-content/limitations.md` that this project's own fixtures carry a
|
|
747
|
+
`# synthetic: true` header. **No fixture does**; two shipped value-pool sources do.
|
|
748
|
+
|
|
749
|
+
- **A corpus could report a transaction it did not contain.** `x12Corpus({ seed: 9, mix: ["270"] })`
|
|
750
|
+
returned a corpus whose manifest said `{"270": 1}` and whose bytes were an `837` professional claim,
|
|
751
|
+
byte-identical to `mix: ["837P"]` at the same seed, because the kind dispatcher ended in an
|
|
752
|
+
unguarded fallback. Every corpus dispatcher is now an exhaustive `switch` behind a resolved
|
|
753
|
+
selector, so an unrecognised kind is a fatal `SYNTH_UNSUPPORTED_KIND` and can no longer be silently
|
|
754
|
+
relabelled. A golden file that lies about what it holds is the failure the intended-warning contract
|
|
755
|
+
exists to prevent, in the one format that had no equivalent check.
|
|
756
|
+
|
|
757
|
+
- **Five exhaustive `switch`es over run-time-erased unions returned `undefined` typed as a value.**
|
|
758
|
+
`generateHl7`, `generateHl7Quirk`'s base-message dispatcher, `fhirCorpus`, `ncpdpCorpus` and
|
|
759
|
+
`astmCorpus` each took no branch for a kind outside its union and handed the result on, surfacing
|
|
760
|
+
later as a `TypeError` from the runtime with no code to branch on. All five are now guarded by a
|
|
761
|
+
resolved selector.
|
|
762
|
+
|
|
763
|
+
**Three more of the same shape are known and deliberately left**, in `src/deid/`:
|
|
764
|
+
`x12DeidLoop({ variant })` and `ncpdpTelecomDeidLoop({ transaction })` fall through to an uncoded
|
|
765
|
+
`TypeError`, and `ccdaDeidLoop({ documentType })` silently generates a Referral Note for anything but
|
|
766
|
+
`"ccd"`. None echoes a caller value into a message, so none is a diagnostic leak. They are named here
|
|
767
|
+
rather than swept in, because two review passes had each found one more position after this change
|
|
768
|
+
described its own coverage as general, and widening the change a third time is the wrong response to
|
|
769
|
+
that.
|
|
770
|
+
|
|
771
|
+
- **`injectCcdaQuirk` returned `undefined` for an unrecognised quirk name, as though it were a
|
|
772
|
+
document.** `CcdaQuirkName` is a union that does not exist at run time, so a JavaScript caller (or
|
|
773
|
+
a cast) reaching the function with any other string fell out of the transform's switch with no
|
|
774
|
+
branch taken and no error. The name is now resolved against `CCDA_QUIRKS` first and an unknown one
|
|
775
|
+
is a fatal `SYNTH_UNSUPPORTED_QUIRK`. Found by the diagnostic-surface slot table below, which
|
|
776
|
+
recorded it as the one slot that produced neither a value nor a diagnostic.
|
|
777
|
+
|
|
500
778
|
- **`README.md` said the package was not published to npm. It is.** The summary blockquote opened
|
|
501
779
|
with "pre-alpha (`0.0.x`), not yet published to npm", so the npm package page asserted that the
|
|
502
780
|
package did not exist on npm, directly under the version npm renders in its own header. A reader
|
|
@@ -541,4 +819,61 @@ its public history at `0.0.x`, per the cosyte version ladder (`0.0.x` until firs
|
|
|
541
819
|
|
|
542
820
|
### Security
|
|
543
821
|
|
|
822
|
+
- **A per-slot diagnostic-surface gate, run red before it was run green.**
|
|
823
|
+
`test/phi/diagnostic-surface.test.ts` drives `assertNoDiagnosticPhiLeak` from `@cosyte/test-utils`
|
|
824
|
+
over a table of **44** consumer-controlled positions: the quirk name, format label and registry key
|
|
825
|
+
on `resolveQuirk`; the quirk selector on all three `generate*Quirk`, all three `*QuirkRoundTrip` and
|
|
826
|
+
all three `*QuirkCorpus` entry points, by explicit list and through a `defineSynthProfile` profile;
|
|
827
|
+
both code lists on `assertIntendedWarnings`; both parameters of `injectCcdaQuirk`; the code-system
|
|
828
|
+
URI on `toBuildCode`; the money string on `dec`; **eighteen selector positions** across the six
|
|
829
|
+
formats (message kind, `ADT` trigger, document type, corpus mix, `837` variant, Bundle type,
|
|
830
|
+
`Patient` profile, quirk kind); and the artifact `content` each quirk round-trip harness hands to a
|
|
831
|
+
sibling parser. Each slot plants an eight-byte marker and a 32 KiB one, names the code it must
|
|
832
|
+
reach, and fails if any four-byte run of the marker turns up in a thrown value, a `message`, a
|
|
833
|
+
`stack`, or a structural identifier on a returned model.
|
|
834
|
+
|
|
835
|
+
**Measured against the unfixed source first, because a suite never seen red is indistinguishable
|
|
836
|
+
from one that cannot go red.** Of the 44 slots, **3 were clean and 41 failed**. The failure
|
|
837
|
+
categories **overlap** — a slot can both echo the marker and throw an uncoded error, and 6 do — so
|
|
838
|
+
they are counted per slot per category and do not sum to 41: **20** echoed the planted value
|
|
839
|
+
verbatim into an error message or stack, **4** put it on a model identifier, **16** threw an error
|
|
840
|
+
carrying no code to branch on, and **7** accepted it with neither an error nor a usable value. The
|
|
841
|
+
method: check out the base tree's `src/`, add the new codes and the frozen table as a purely
|
|
842
|
+
additive patch with an _optional_ message parameter so no base call site changes, then invoke each
|
|
843
|
+
slot's plant directly with both probe sizes.
|
|
844
|
+
|
|
845
|
+
**The model half of that sweep is vacuous, and the file says so rather than letting a reader infer
|
|
846
|
+
coverage.** Every slot throws, because every position fails closed before a model exists, so
|
|
847
|
+
`getModelIdentifiers` is never reached during the sweep. That is the result of the fix, not a gap in
|
|
848
|
+
the table, and it is carried instead by a separate assertion that runs the three identifier helpers
|
|
849
|
+
over real corpora and real quirk round-trips and checks that every identifier they yield comes from
|
|
850
|
+
a set this package controls. Between them: the slots prove no caller value survives to a model, and
|
|
851
|
+
the closed-set test proves the identifiers that exist were derived rather than passed through.
|
|
852
|
+
|
|
853
|
+
It also asserts the structural half: no `throw new` under `src/` names anything but `SynthError`,
|
|
854
|
+
and no construction of one carries an interpolation. The scanner is positive-controlled against a
|
|
855
|
+
constructed string containing the thing it hunts, not against a file that happens to be nearby, and
|
|
856
|
+
**its title says what a regex can see while its docblock says what it cannot** — it cannot see a
|
|
857
|
+
`TypeError` the runtime raises on its own, which is exactly the route the selector chokepoint
|
|
858
|
+
closes.
|
|
859
|
+
|
|
860
|
+
What the gate does not prove is in the runner's own documentation and is not restated more
|
|
861
|
+
favourably here: it does not catch a re-encoded echo, an echo under four bytes, a leak carried only
|
|
862
|
+
as a number, or a leak through a position nobody declared. The slot table is the deliverable. It was
|
|
863
|
+
derived by enumerating the exported types with the TypeScript checker rather than by sweeping the
|
|
864
|
+
source from memory. **Even so it was refuted twice, on the same class both times.** The first
|
|
865
|
+
version covered only `throw` sites and missed eighteen selector positions. The second called the
|
|
866
|
+
selectors "generalised, not patched" and missed the tail of a quirk list a `count` never reaches,
|
|
867
|
+
which lands on `manifest.quirks` unresolved. Enumerating the types was necessary and not sufficient:
|
|
868
|
+
both misses were judgements about which positions "reach a diagnostic", made before it was
|
|
869
|
+
established that an unresolved selector reaches a peer builder's diagnostic and a derived manifest
|
|
870
|
+
key.
|
|
871
|
+
|
|
872
|
+
**So the table is no longer described as exhaustive, on any surface.** A third claim of completeness
|
|
873
|
+
would be worth exactly what the first two were. What holds generally is the mechanism — the error
|
|
874
|
+
type has no value parameter, and a selector is read in one place — and the table is an enumeration
|
|
875
|
+
of the positions that have been checked. That wording is now in the test file's own header, in
|
|
876
|
+
`docs-content/limitations.md` and in `CLAUDE.md`, so the next person to add a position adds a slot
|
|
877
|
+
rather than trusting a count.
|
|
878
|
+
|
|
544
879
|
[Unreleased]: https://github.com/cosyte/synth/commits/main
|
package/README.md
CHANGED
|
@@ -255,8 +255,12 @@ astmQuirkRoundTrip(generateAstmQuirk({ seed: 1, quirk: "unknown-escape" })).inte
|
|
|
255
255
|
```
|
|
256
256
|
|
|
257
257
|
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
|
|
259
|
-
|
|
258
|
+
diagnostic — never a silently-wrong fixture — and so do the other selectors: a message kind, a
|
|
259
|
+
document type, a corpus mix entry, an `837` variant, a Bundle type. Each is checked against its own set
|
|
260
|
+
before anything is generated, so an unrecognised one is a fatal `SYNTH_UNSUPPORTED_KIND` rather than a
|
|
261
|
+
mislabelled corpus. Every refusal carries a code and a fixed message from a frozen table, and quotes
|
|
262
|
+
nothing you passed. Every quirk is grounded in a **publicly-documented** deviation or a parser's
|
|
263
|
+
**public** profile, never a private vendor corpus.
|
|
260
264
|
|
|
261
265
|
**Deferred:** quirk recipes for **FHIR, X12, and NCPDP**, and any quirk that would need a **private,
|
|
262
266
|
vendor-attributed corpus** to ground.
|
package/dist/astm/index.cjs
CHANGED
|
@@ -31,6 +31,63 @@ function sfc32Next(s) {
|
|
|
31
31
|
return t >>> 0;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// src/codes.ts
|
|
35
|
+
var SYNTH_FATAL_CODES = {
|
|
36
|
+
/**
|
|
37
|
+
* A vendor quirk was requested that the target format's profile system does not support. Fatal —
|
|
38
|
+
* never a silent no-op and never a fabricated quirk.
|
|
39
|
+
*/
|
|
40
|
+
SYNTH_UNSUPPORTED_QUIRK: "SYNTH_UNSUPPORTED_QUIRK",
|
|
41
|
+
/**
|
|
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
|
|
44
|
+
* than no golden file.
|
|
45
|
+
*/
|
|
46
|
+
SYNTH_QUIRK_ANCHOR_ABSENT: "SYNTH_QUIRK_ANCHOR_ABSENT",
|
|
47
|
+
/**
|
|
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.
|
|
50
|
+
*/
|
|
51
|
+
SYNTH_INTENDED_WARNING_MISMATCH: "SYNTH_INTENDED_WARNING_MISMATCH",
|
|
52
|
+
/** An integer range was requested with its maximum below its minimum. Fatal. */
|
|
53
|
+
SYNTH_INVALID_RANGE: "SYNTH_INVALID_RANGE",
|
|
54
|
+
/** A value was drawn from an empty pool. Fatal — never a fabricated substitute. */
|
|
55
|
+
SYNTH_EMPTY_POOL: "SYNTH_EMPTY_POOL",
|
|
56
|
+
/** A `defineSynthProfile` spec was not usable (a missing or blank `name`). Fatal. */
|
|
57
|
+
SYNTH_INVALID_PROFILE: "SYNTH_INVALID_PROFILE",
|
|
58
|
+
/**
|
|
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 —
|
|
61
|
+
* see `resolveKind`: a selector union is erased at run time, and a selector that falls through
|
|
62
|
+
* either mislabels the fixture or hands the value to a peer builder that quotes it back.
|
|
63
|
+
*/
|
|
64
|
+
SYNTH_UNSUPPORTED_KIND: "SYNTH_UNSUPPORTED_KIND"
|
|
65
|
+
};
|
|
66
|
+
var SYNTH_FATAL_MESSAGES = Object.freeze({
|
|
67
|
+
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.",
|
|
68
|
+
SYNTH_UNSUPPORTED_QUIRK: "The requested vendor quirk is not in the target format's quirk registry. Compare the request against that format's exported registry (HL7_QUIRKS, CCDA_QUIRKS, ASTM_QUIRKS).",
|
|
69
|
+
SYNTH_QUIRK_ANCHOR_ABSENT: "The quirk transform found no structural anchor to mutate, so the fixture would not carry the deviation it is labelled with. Refusing to emit a mislabeled fixture.",
|
|
70
|
+
SYNTH_INTENDED_WARNING_MISMATCH: "A bare parse of the generated quirk artifact did not produce exactly the declared intended warning code(s). Refusing to emit a mislabeled fixture.",
|
|
71
|
+
SYNTH_UNMAPPED_CODE_SYSTEM: "The concept's code-system URI has no OID mapping in the C-CDA example-code table.",
|
|
72
|
+
SYNTH_INVALID_DECIMAL: "The value could not be read as an X12 decimal.",
|
|
73
|
+
SYNTH_INVALID_RANGE: "An integer range was requested with its maximum below its minimum.",
|
|
74
|
+
SYNTH_EMPTY_POOL: "A value was drawn from an empty pool.",
|
|
75
|
+
SYNTH_INVALID_PROFILE: "defineSynthProfile requires a non-empty string name.",
|
|
76
|
+
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."
|
|
77
|
+
});
|
|
78
|
+
var SynthError = class extends Error {
|
|
79
|
+
/** The stable fatal code. */
|
|
80
|
+
code;
|
|
81
|
+
/**
|
|
82
|
+
* @param code - The stable {@link SynthFatalCode}. The message comes from the frozen registry.
|
|
83
|
+
*/
|
|
84
|
+
constructor(code) {
|
|
85
|
+
super(SYNTH_FATAL_MESSAGES[code]);
|
|
86
|
+
this.name = "SynthError";
|
|
87
|
+
this.code = code;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
34
91
|
// src/rng/rng.ts
|
|
35
92
|
var Sfc32Rng = class {
|
|
36
93
|
seed;
|
|
@@ -48,7 +105,7 @@ var Sfc32Rng = class {
|
|
|
48
105
|
return this.nextUint32() / 4294967296;
|
|
49
106
|
}
|
|
50
107
|
int(min, max) {
|
|
51
|
-
if (max < min) throw new
|
|
108
|
+
if (max < min) throw new SynthError(SYNTH_FATAL_CODES.SYNTH_INVALID_RANGE);
|
|
52
109
|
const span = max - min + 1;
|
|
53
110
|
return min + Math.floor(this.float() * span);
|
|
54
111
|
}
|
|
@@ -56,7 +113,7 @@ var Sfc32Rng = class {
|
|
|
56
113
|
return this.float() < p;
|
|
57
114
|
}
|
|
58
115
|
pick(items) {
|
|
59
|
-
if (items.length === 0) throw new
|
|
116
|
+
if (items.length === 0) throw new SynthError(SYNTH_FATAL_CODES.SYNTH_EMPTY_POOL);
|
|
60
117
|
return items[this.int(0, items.length - 1)];
|
|
61
118
|
}
|
|
62
119
|
digits(n) {
|
|
@@ -584,10 +641,21 @@ function latin1(bytes) {
|
|
|
584
641
|
return out;
|
|
585
642
|
}
|
|
586
643
|
|
|
644
|
+
// src/select.ts
|
|
645
|
+
function resolveKind(allowed, requested) {
|
|
646
|
+
const match = allowed.find((value) => value === requested);
|
|
647
|
+
if (match === void 0) throw new SynthError(SYNTH_FATAL_CODES.SYNTH_UNSUPPORTED_KIND);
|
|
648
|
+
return match;
|
|
649
|
+
}
|
|
650
|
+
function resolveMix(allowed, requested, fallback) {
|
|
651
|
+
if (requested === void 0) return fallback;
|
|
652
|
+
return requested.map((entry) => resolveKind(allowed, entry));
|
|
653
|
+
}
|
|
654
|
+
|
|
587
655
|
// src/profile.ts
|
|
588
656
|
function defineSynthProfile(spec) {
|
|
589
657
|
if (typeof spec.name !== "string" || spec.name.trim().length === 0) {
|
|
590
|
-
throw new
|
|
658
|
+
throw new SynthError(SYNTH_FATAL_CODES.SYNTH_INVALID_PROFILE);
|
|
591
659
|
}
|
|
592
660
|
return Object.freeze({
|
|
593
661
|
name: spec.name,
|
|
@@ -597,28 +665,6 @@ function defineSynthProfile(spec) {
|
|
|
597
665
|
});
|
|
598
666
|
}
|
|
599
667
|
|
|
600
|
-
// src/codes.ts
|
|
601
|
-
var SYNTH_FATAL_CODES = {
|
|
602
|
-
/**
|
|
603
|
-
* A vendor quirk was requested that the target format's profile system does not support. Fatal —
|
|
604
|
-
* never a silent no-op and never a fabricated quirk.
|
|
605
|
-
*/
|
|
606
|
-
SYNTH_UNSUPPORTED_QUIRK: "SYNTH_UNSUPPORTED_QUIRK"
|
|
607
|
-
};
|
|
608
|
-
var SynthError = class extends Error {
|
|
609
|
-
/** The stable fatal code. */
|
|
610
|
-
code;
|
|
611
|
-
/**
|
|
612
|
-
* @param code - The stable {@link SynthFatalCode}.
|
|
613
|
-
* @param message - A human-readable detail (never contains PHI — there is none).
|
|
614
|
-
*/
|
|
615
|
-
constructor(code, message) {
|
|
616
|
-
super(message);
|
|
617
|
-
this.name = "SynthError";
|
|
618
|
-
this.code = code;
|
|
619
|
-
}
|
|
620
|
-
};
|
|
621
|
-
|
|
622
668
|
// src/quirk.ts
|
|
623
669
|
var PROFILE_QUIRK_APPLIED = "PROFILE_QUIRK_APPLIED";
|
|
624
670
|
function sameCodeSet(a, b) {
|
|
@@ -635,12 +681,8 @@ function sameCodeSet(a, b) {
|
|
|
635
681
|
}
|
|
636
682
|
function resolveQuirk(registry, format, name2) {
|
|
637
683
|
const descriptor = registry[name2];
|
|
638
|
-
if (descriptor === void 0) {
|
|
639
|
-
|
|
640
|
-
throw new SynthError(
|
|
641
|
-
SYNTH_FATAL_CODES.SYNTH_UNSUPPORTED_QUIRK,
|
|
642
|
-
`${format}: unsupported quirk "${name2}". The ${format} profile system supports: ${supported}.`
|
|
643
|
-
);
|
|
684
|
+
if (descriptor === void 0 || descriptor.format !== format) {
|
|
685
|
+
throw new SynthError(SYNTH_FATAL_CODES.SYNTH_UNSUPPORTED_QUIRK);
|
|
644
686
|
}
|
|
645
687
|
return descriptor;
|
|
646
688
|
}
|
|
@@ -655,11 +697,9 @@ function profileTolerated(disposition, intendedWarnings, warningsUnderProfile) {
|
|
|
655
697
|
return false;
|
|
656
698
|
}
|
|
657
699
|
}
|
|
658
|
-
function assertIntendedWarnings(
|
|
700
|
+
function assertIntendedWarnings(intendedWarnings, bareWarnings) {
|
|
659
701
|
if (!sameCodeSet(bareWarnings, intendedWarnings)) {
|
|
660
|
-
throw new
|
|
661
|
-
`quirk "${quirk}": the intended-warning contract does not hold \u2014 expected exactly [${intendedWarnings.join(", ")}] but a bare parse produced [${bareWarnings.join(", ")}]. Refusing to emit a mislabeled fixture.`
|
|
662
|
-
);
|
|
702
|
+
throw new SynthError(SYNTH_FATAL_CODES.SYNTH_INTENDED_WARNING_MISMATCH);
|
|
663
703
|
}
|
|
664
704
|
}
|
|
665
705
|
function validateProfileQuirks(profile, registry, format) {
|
|
@@ -668,6 +708,7 @@ function validateProfileQuirks(profile, registry, format) {
|
|
|
668
708
|
}
|
|
669
709
|
|
|
670
710
|
// src/astm/quirk.ts
|
|
711
|
+
var ALL_QUIRK_KINDS = Object.freeze(["Result"]);
|
|
671
712
|
var ASTM_QUIRKS = Object.freeze({
|
|
672
713
|
"unknown-escape": Object.freeze({
|
|
673
714
|
name: "unknown-escape",
|
|
@@ -721,17 +762,14 @@ function applyQuirk(quirk, records) {
|
|
|
721
762
|
}
|
|
722
763
|
function generateAstmQuirk(options) {
|
|
723
764
|
const seed = options.seed ?? 0;
|
|
724
|
-
const kind = "Result";
|
|
765
|
+
const kind = resolveKind(ALL_QUIRK_KINDS, options.kind ?? "Result");
|
|
725
766
|
const descriptor = resolveQuirk(ASTM_QUIRKS, "astm", options.quirk);
|
|
726
767
|
const clean = generateAstmResult({ seed });
|
|
727
768
|
const content = applyQuirk(options.quirk, clean);
|
|
728
769
|
if (content === clean) {
|
|
729
|
-
throw new
|
|
730
|
-
`generateAstmQuirk: quirk "${options.quirk}" found no structural anchor in a ${kind} report at seed ${seed} \u2014 refusing to emit a fixture that does not carry the intended deviation.`
|
|
731
|
-
);
|
|
770
|
+
throw new SynthError(SYNTH_FATAL_CODES.SYNTH_QUIRK_ANCHOR_ABSENT);
|
|
732
771
|
}
|
|
733
772
|
assertIntendedWarnings(
|
|
734
|
-
descriptor.name,
|
|
735
773
|
descriptor.intendedWarnings,
|
|
736
774
|
astm.parseAstmRecords(content).warnings.map((w) => String(w.code))
|
|
737
775
|
);
|
|
@@ -777,6 +815,7 @@ var ALL_ASTM_QUIRKS = Object.freeze(
|
|
|
777
815
|
function astmQuirkCorpus(options) {
|
|
778
816
|
const quirks = options.profile ? validateProfileQuirks(options.profile, ASTM_QUIRKS, "astm") : options.quirks ?? ALL_ASTM_QUIRKS;
|
|
779
817
|
const names = quirks.length > 0 ? quirks : ALL_ASTM_QUIRKS;
|
|
818
|
+
for (const name2 of names) resolveQuirk(ASTM_QUIRKS, "astm", name2);
|
|
780
819
|
const count = options.count ?? names.length;
|
|
781
820
|
const seedStream = createRng(options.seed);
|
|
782
821
|
const artifacts = Array.from({ length: count }, (_unused, i) => {
|
|
@@ -798,7 +837,8 @@ var astmQuirkProfile = defineSynthProfile({
|
|
|
798
837
|
});
|
|
799
838
|
|
|
800
839
|
// src/astm/index.ts
|
|
801
|
-
var
|
|
840
|
+
var ALL_KINDS = Object.freeze(["Result", "Order"]);
|
|
841
|
+
var DEFAULT_MIX = ALL_KINDS;
|
|
802
842
|
function generateKind(kind, seed) {
|
|
803
843
|
switch (kind) {
|
|
804
844
|
case "Result":
|
|
@@ -808,7 +848,8 @@ function generateKind(kind, seed) {
|
|
|
808
848
|
}
|
|
809
849
|
}
|
|
810
850
|
function astmCorpus(options) {
|
|
811
|
-
const { seed
|
|
851
|
+
const { seed } = options;
|
|
852
|
+
const mix = resolveMix(ALL_KINDS, options.mix, DEFAULT_MIX);
|
|
812
853
|
const count = options.count ?? mix.length;
|
|
813
854
|
const seedStream = createRng(seed);
|
|
814
855
|
const artifacts = Array.from({ length: count }, (_unused, i) => {
|