@cosyte/synth 0.0.2 → 0.0.4

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 (49) hide show
  1. package/CHANGELOG.md +321 -0
  2. package/README.md +13 -4
  3. package/dist/astm/index.cjs +83 -42
  4. package/dist/astm/index.cjs.map +1 -1
  5. package/dist/astm/index.d.cts +1 -1
  6. package/dist/astm/index.d.ts +1 -1
  7. package/dist/astm/index.mjs +83 -42
  8. package/dist/astm/index.mjs.map +1 -1
  9. package/dist/ccda/index.cjs +89 -44
  10. package/dist/ccda/index.cjs.map +1 -1
  11. package/dist/ccda/index.d.cts +5 -3
  12. package/dist/ccda/index.d.ts +5 -3
  13. package/dist/ccda/index.mjs +89 -44
  14. package/dist/ccda/index.mjs.map +1 -1
  15. package/dist/deid/index.cjs +92 -13
  16. package/dist/deid/index.cjs.map +1 -1
  17. package/dist/deid/index.d.cts +1 -1
  18. package/dist/deid/index.d.ts +1 -1
  19. package/dist/deid/index.mjs +93 -14
  20. package/dist/deid/index.mjs.map +1 -1
  21. package/dist/fhir/index.cjs +78 -5
  22. package/dist/fhir/index.cjs.map +1 -1
  23. package/dist/fhir/index.mjs +78 -5
  24. package/dist/fhir/index.mjs.map +1 -1
  25. package/dist/hl7/index.cjs +89 -41
  26. package/dist/hl7/index.cjs.map +1 -1
  27. package/dist/hl7/index.d.cts +1 -1
  28. package/dist/hl7/index.d.ts +1 -1
  29. package/dist/hl7/index.mjs +89 -41
  30. package/dist/hl7/index.mjs.map +1 -1
  31. package/dist/index.cjs +91 -40
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.d.cts +126 -8
  34. package/dist/index.d.ts +126 -8
  35. package/dist/index.mjs +89 -41
  36. package/dist/index.mjs.map +1 -1
  37. package/dist/ncpdp/index.cjs +56 -4
  38. package/dist/ncpdp/index.cjs.map +1 -1
  39. package/dist/ncpdp/index.mjs +56 -4
  40. package/dist/ncpdp/index.mjs.map +1 -1
  41. package/dist/{quirk-JLyO1Ncj.d.ts → quirk-C9t9CkPS.d.ts} +17 -6
  42. package/dist/{quirk-DmkgoZdh.d.cts → quirk-DYMDojVw.d.cts} +17 -6
  43. package/dist/x12/index.cjs +82 -13
  44. package/dist/x12/index.cjs.map +1 -1
  45. package/dist/x12/index.d.cts +2 -1
  46. package/dist/x12/index.d.ts +2 -1
  47. package/dist/x12/index.mjs +83 -14
  48. package/dist/x12/index.mjs.map +1 -1
  49. package/package.json +2 -2
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { s as ssn, p as phone, n as name, e as email, i as ipv4, c as ipv6, u as uuid, d as identifier, f as address, g as dateYmd, h as npi, j as dea } from './providers-OLz3zAc-.cjs';
2
2
  export { A as Artifact, C as Corpus, k as CorpusManifest, R as Rng, l as SsnBlock, m as SynthFormat, b as SyntheticAddress, a as SyntheticIdentifier, S as SyntheticName, o as createRng, q as makeCorpus } from './providers-OLz3zAc-.cjs';
3
- export { P as PROFILE_QUIRK_APPLIED, a as QuirkArtifact, Q as QuirkDescriptor, c as QuirkProfileDisposition, d as QuirkProfiledVerdict, b as QuirkRoundTripResult, S as SynthProfile, e as SynthProfileSpec, f as assertIntendedWarnings, g as defineSynthProfile, p as profileTolerated, r as resolveQuirk, s as sameCodeSet, v as validateProfileQuirks } from './quirk-DmkgoZdh.cjs';
3
+ export { P as PROFILE_QUIRK_APPLIED, a as QuirkArtifact, Q as QuirkDescriptor, c as QuirkProfileDisposition, d as QuirkProfiledVerdict, b as QuirkRoundTripResult, S as SynthProfile, e as SynthProfileSpec, f as assertIntendedWarnings, g as defineSynthProfile, p as profileTolerated, r as resolveQuirk, s as sameCodeSet, v as validateProfileQuirks } from './quirk-DYMDojVw.cjs';
4
4
 
5
5
  /**
6
6
  * The reserved / never-collide identifier facts that make a `@cosyte/synth` value **provably
@@ -345,8 +345,12 @@ declare function sfc32Next(s: Sfc32State): number;
345
345
  */
346
346
  declare const SYNTH_FATAL_CODES: {
347
347
  /**
348
- * A format was requested that this build cannot generate through a real parser builder/serializer
349
- * (e.g. ASTM before `@cosyte/astm`'s serializer ships). Fatal — never a hand-written byte fallback.
348
+ * A format was requested that this build cannot generate through a real parser builder/serializer.
349
+ * Fatal — never a hand-written byte fallback.
350
+ *
351
+ * **No code path in this build raises it.** All six formats generate, so it is reserved for a
352
+ * future format that does not, and is kept because removing a published code is a breaking change.
353
+ * An unsupported *kind* within a format that does generate is `SYNTH_UNSUPPORTED_KIND`.
350
354
  */
351
355
  readonly SYNTH_UNSUPPORTED_FORMAT: "SYNTH_UNSUPPORTED_FORMAT";
352
356
  /**
@@ -354,31 +358,145 @@ declare const SYNTH_FATAL_CODES: {
354
358
  * never a silent no-op and never a fabricated quirk.
355
359
  */
356
360
  readonly SYNTH_UNSUPPORTED_QUIRK: "SYNTH_UNSUPPORTED_QUIRK";
361
+ /**
362
+ * A quirk transform found no structural anchor to mutate, so the fixture would not carry the
363
+ * deviation it is labelled with. Fatal — a golden file that lies about its parser verdict is worse
364
+ * than no golden file.
365
+ */
366
+ readonly SYNTH_QUIRK_ANCHOR_ABSENT: "SYNTH_QUIRK_ANCHOR_ABSENT";
367
+ /**
368
+ * A bare parse of a freshly-generated quirk artifact did not produce exactly the declared intended
369
+ * warning code(s). Fatal — never emit a mislabeled fixture.
370
+ */
371
+ readonly SYNTH_INTENDED_WARNING_MISMATCH: "SYNTH_INTENDED_WARNING_MISMATCH";
372
+ /** A concept's code-system URI has no OID mapping in the C-CDA example-code table. Fatal. */
373
+ readonly SYNTH_UNMAPPED_CODE_SYSTEM: "SYNTH_UNMAPPED_CODE_SYSTEM";
374
+ /** A money value could not be read as an X12 decimal. Fatal — a generator never rounds to a float. */
375
+ readonly SYNTH_INVALID_DECIMAL: "SYNTH_INVALID_DECIMAL";
376
+ /** An integer range was requested with its maximum below its minimum. Fatal. */
377
+ readonly SYNTH_INVALID_RANGE: "SYNTH_INVALID_RANGE";
378
+ /** A value was drawn from an empty pool. Fatal — never a fabricated substitute. */
379
+ readonly SYNTH_EMPTY_POOL: "SYNTH_EMPTY_POOL";
380
+ /** A `defineSynthProfile` spec was not usable (a missing or blank `name`). Fatal. */
381
+ readonly SYNTH_INVALID_PROFILE: "SYNTH_INVALID_PROFILE";
382
+ /**
383
+ * A caller-supplied selector (a message kind, a document type, a corpus mix entry, a claim
384
+ * variant, a Bundle type, a resource profile) is not in the closed set that governs it. Fatal —
385
+ * see `resolveKind`: a selector union is erased at run time, and a selector that falls through
386
+ * either mislabels the fixture or hands the value to a peer builder that quotes it back.
387
+ */
388
+ readonly SYNTH_UNSUPPORTED_KIND: "SYNTH_UNSUPPORTED_KIND";
357
389
  };
358
390
  /**
359
391
  * A value from {@link SYNTH_FATAL_CODES} — the type carried by a thrown {@link SynthError}.
360
392
  */
361
393
  type SynthFatalCode = (typeof SYNTH_FATAL_CODES)[keyof typeof SYNTH_FATAL_CODES];
394
+ /**
395
+ * The **frozen message registry** — the only place a {@link SynthError} message can come from.
396
+ *
397
+ * A message here is a fixed string. It never quotes the request that produced it, and there is no
398
+ * parameter through which it could: {@link SynthError} takes a code and nothing else. That is the
399
+ * whole mechanism, and it is deliberately a mechanism rather than a habit. Every one of these
400
+ * messages used to be assembled by interpolating the caller's value into a template, and the reason
401
+ * that was safe was not the design — it was that the caller happened to be passing a quirk name.
402
+ *
403
+ * The trade is real and is accepted: a fatal no longer tells you *which* value it rejected. It tells
404
+ * you which rule refused, on `err.code`, and the stack frame tells you where. The caller already
405
+ * holds the value it passed.
406
+ *
407
+ * @example
408
+ * ```ts
409
+ * import { SYNTH_FATAL_CODES, SYNTH_FATAL_MESSAGES } from "@cosyte/synth";
410
+ * SYNTH_FATAL_MESSAGES[SYNTH_FATAL_CODES.SYNTH_EMPTY_POOL]; // => "A value was drawn from an empty pool."
411
+ * ```
412
+ */
413
+ declare const SYNTH_FATAL_MESSAGES: Readonly<Record<SynthFatalCode, string>>;
362
414
  /**
363
415
  * The typed error every fatal `@cosyte/synth` condition throws. Carries a stable
364
416
  * {@link SynthFatalCode} so callers branch on `err.code` without matching message text.
365
417
  *
418
+ * It takes **no value parameter**. The message is whatever {@link SYNTH_FATAL_MESSAGES} holds for the
419
+ * code, so no caller-supplied string can reach a diagnostic surface by any route — not `message`, not
420
+ * `stack`, not a field on the thrown object.
421
+ *
366
422
  * @example
367
423
  * ```ts
368
424
  * import { SynthError, SYNTH_FATAL_CODES } from "@cosyte/synth";
369
- * throw new SynthError(SYNTH_FATAL_CODES.SYNTH_UNSUPPORTED_FORMAT, "astm is not yet generable");
425
+ * throw new SynthError(SYNTH_FATAL_CODES.SYNTH_UNSUPPORTED_FORMAT);
370
426
  * ```
371
427
  */
372
428
  declare class SynthError extends Error {
373
429
  /** The stable fatal code. */
374
430
  readonly code: SynthFatalCode;
375
431
  /**
376
- * @param code - The stable {@link SynthFatalCode}.
377
- * @param message - A human-readable detail (never contains PHI — there is none).
432
+ * @param code - The stable {@link SynthFatalCode}. The message comes from the frozen registry.
378
433
  */
379
- constructor(code: SynthFatalCode, message: string);
434
+ constructor(code: SynthFatalCode);
380
435
  }
381
436
 
437
+ /**
438
+ * The **selector chokepoint**. A generator's options are almost all *selectors*: a message kind, a
439
+ * document type, a corpus mix, a claim variant, a Bundle type, a profile. Each is typed as a closed
440
+ * union, and every one of those unions is **erased at run time**, so a JavaScript caller (or a
441
+ * `as never` in someone's test) reaches the branch with any string at all.
442
+ *
443
+ * Three things went wrong when that was left unchecked, and they are all the same bug:
444
+ *
445
+ * 1. **The value reached a diagnostic.** An unrecognised `documentType` travelled into
446
+ * `@cosyte/ccda`'s `buildCcda`, which is entitled to quote it back in its own `TypeError` and
447
+ * does. This package then has a caller-supplied string on an `err.message` and an `err.stack`,
448
+ * through its own public entry point, having taken no care of it.
449
+ * 2. **The value reached the model.** A corpus mix entry becomes an `Artifact.kind` and a
450
+ * `manifest.counts` key, which is precisely the structural-identifier position a downstream
451
+ * package interpolates to describe a location.
452
+ * 3. **The fixture was silently mislabeled.** An exhaustive `switch` over an erased union takes no
453
+ * branch and returns `undefined`, or a trailing `else` quietly generates something else. A corpus
454
+ * whose manifest says it holds one transaction and holds another is a golden file that lies.
455
+ *
456
+ * So a selector is resolved against its own set, once, before anything is generated, and an
457
+ * unrecognised one is a fatal `SYNTH_UNSUPPORTED_KIND`. Like every fatal here it carries a code and a
458
+ * fixed message, and quotes neither the request nor the set.
459
+ *
460
+ * @module
461
+ */
462
+ /**
463
+ * Resolve one caller-supplied selector against the closed set that governs it, or **fail closed**.
464
+ *
465
+ * @param allowed - Every value the selector may take.
466
+ * @param requested - The selector the caller supplied.
467
+ * @returns `requested`, narrowed to the union.
468
+ * @throws SynthError `SYNTH_UNSUPPORTED_KIND` when `requested` is not in `allowed`.
469
+ * @example
470
+ * ```ts
471
+ * import { resolveKind } from "@cosyte/synth";
472
+ * resolveKind(["ccd", "referralNote"] as const, "ccd"); // "ccd"
473
+ * ```
474
+ */
475
+ declare function resolveKind<T extends string>(allowed: readonly T[], requested: string): T;
476
+ /**
477
+ * Resolve every entry of a caller-supplied corpus mix, in order, or **fail closed** on the first
478
+ * unrecognised one.
479
+ *
480
+ * It substitutes the default **only** when the caller supplied nothing, which is exactly what the
481
+ * `??` it replaced did. An empty array is a supplied mix and is returned as one. An earlier version
482
+ * of this function also treated `[]` as "nothing supplied", on the stated grounds that it matched the
483
+ * previous behaviour; it did not — `??` fires on `undefined` and never on `[]` — and it changed the
484
+ * result of six published entry points, turning an explicit empty selection into "generate one of
485
+ * everything". A convenience that fails open is not a convenience.
486
+ *
487
+ * @param allowed - Every kind the corpus may generate.
488
+ * @param requested - The mix the caller supplied, or `undefined` for the default.
489
+ * @param fallback - The default mix, used only when `requested` is `undefined`.
490
+ * @returns The resolved mix.
491
+ * @throws SynthError `SYNTH_UNSUPPORTED_KIND` on the first unrecognised entry.
492
+ * @example
493
+ * ```ts
494
+ * import { resolveMix } from "@cosyte/synth";
495
+ * resolveMix(["Result", "Order"] as const, ["Order"], ["Result", "Order"]); // ["Order"]
496
+ * ```
497
+ */
498
+ declare function resolveMix<T extends string>(allowed: readonly T[], requested: readonly string[] | undefined, fallback: readonly T[]): readonly T[];
499
+
382
500
  /**
383
501
  * `@cosyte/synth` — a deterministic, seedable **synthetic-data / test-fixture generator** for the
384
502
  * cosyte healthcare formats. A *consumer* of the parsers, not a parser: it builds artifacts **through
@@ -404,4 +522,4 @@ declare class SynthError extends Error {
404
522
  */
405
523
  declare const VERSION = "0.0.0";
406
524
 
407
- export { DEA_REGISTRANT_TYPES, DOC_V6_PREFIX, NPI_LUHN_PREFIX, RESERVED_EMAIL_DOMAINS, SYNTHETIC_ASSIGNING_AUTHORITY, SYNTHETIC_CITY_NAMES, SYNTHETIC_FAMILY_NAMES, SYNTHETIC_GIVEN_NAMES, SYNTHETIC_STREET_NAMES, SYNTH_FATAL_CODES, type Sfc32State, SynthError, type SynthFatalCode, TEST_NET_V4_PREFIXES, VERSION, address, dateYmd, dea, deaCheckDigit, email, identifier, ipv4, ipv6, isSyntheticDea, isSyntheticEmail, isSyntheticIp, isSyntheticNpi, isSyntheticPhone, isSyntheticSsn, luhnMod10, name, npi, npiCheckDigit, phone, safe, sfc32Next, splitmix32, ssn, uuid };
525
+ export { DEA_REGISTRANT_TYPES, DOC_V6_PREFIX, NPI_LUHN_PREFIX, RESERVED_EMAIL_DOMAINS, SYNTHETIC_ASSIGNING_AUTHORITY, SYNTHETIC_CITY_NAMES, SYNTHETIC_FAMILY_NAMES, SYNTHETIC_GIVEN_NAMES, SYNTHETIC_STREET_NAMES, SYNTH_FATAL_CODES, SYNTH_FATAL_MESSAGES, type Sfc32State, SynthError, type SynthFatalCode, TEST_NET_V4_PREFIXES, VERSION, address, dateYmd, dea, deaCheckDigit, email, identifier, ipv4, ipv6, isSyntheticDea, isSyntheticEmail, isSyntheticIp, isSyntheticNpi, isSyntheticPhone, isSyntheticSsn, luhnMod10, name, npi, npiCheckDigit, phone, resolveKind, resolveMix, safe, sfc32Next, splitmix32, ssn, uuid };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { s as ssn, p as phone, n as name, e as email, i as ipv4, c as ipv6, u as uuid, d as identifier, f as address, g as dateYmd, h as npi, j as dea } from './providers-OLz3zAc-.js';
2
2
  export { A as Artifact, C as Corpus, k as CorpusManifest, R as Rng, l as SsnBlock, m as SynthFormat, b as SyntheticAddress, a as SyntheticIdentifier, S as SyntheticName, o as createRng, q as makeCorpus } from './providers-OLz3zAc-.js';
3
- export { P as PROFILE_QUIRK_APPLIED, a as QuirkArtifact, Q as QuirkDescriptor, c as QuirkProfileDisposition, d as QuirkProfiledVerdict, b as QuirkRoundTripResult, S as SynthProfile, e as SynthProfileSpec, f as assertIntendedWarnings, g as defineSynthProfile, p as profileTolerated, r as resolveQuirk, s as sameCodeSet, v as validateProfileQuirks } from './quirk-JLyO1Ncj.js';
3
+ export { P as PROFILE_QUIRK_APPLIED, a as QuirkArtifact, Q as QuirkDescriptor, c as QuirkProfileDisposition, d as QuirkProfiledVerdict, b as QuirkRoundTripResult, S as SynthProfile, e as SynthProfileSpec, f as assertIntendedWarnings, g as defineSynthProfile, p as profileTolerated, r as resolveQuirk, s as sameCodeSet, v as validateProfileQuirks } from './quirk-C9t9CkPS.js';
4
4
 
5
5
  /**
6
6
  * The reserved / never-collide identifier facts that make a `@cosyte/synth` value **provably
@@ -345,8 +345,12 @@ declare function sfc32Next(s: Sfc32State): number;
345
345
  */
346
346
  declare const SYNTH_FATAL_CODES: {
347
347
  /**
348
- * A format was requested that this build cannot generate through a real parser builder/serializer
349
- * (e.g. ASTM before `@cosyte/astm`'s serializer ships). Fatal — never a hand-written byte fallback.
348
+ * A format was requested that this build cannot generate through a real parser builder/serializer.
349
+ * Fatal — never a hand-written byte fallback.
350
+ *
351
+ * **No code path in this build raises it.** All six formats generate, so it is reserved for a
352
+ * future format that does not, and is kept because removing a published code is a breaking change.
353
+ * An unsupported *kind* within a format that does generate is `SYNTH_UNSUPPORTED_KIND`.
350
354
  */
351
355
  readonly SYNTH_UNSUPPORTED_FORMAT: "SYNTH_UNSUPPORTED_FORMAT";
352
356
  /**
@@ -354,31 +358,145 @@ declare const SYNTH_FATAL_CODES: {
354
358
  * never a silent no-op and never a fabricated quirk.
355
359
  */
356
360
  readonly SYNTH_UNSUPPORTED_QUIRK: "SYNTH_UNSUPPORTED_QUIRK";
361
+ /**
362
+ * A quirk transform found no structural anchor to mutate, so the fixture would not carry the
363
+ * deviation it is labelled with. Fatal — a golden file that lies about its parser verdict is worse
364
+ * than no golden file.
365
+ */
366
+ readonly SYNTH_QUIRK_ANCHOR_ABSENT: "SYNTH_QUIRK_ANCHOR_ABSENT";
367
+ /**
368
+ * A bare parse of a freshly-generated quirk artifact did not produce exactly the declared intended
369
+ * warning code(s). Fatal — never emit a mislabeled fixture.
370
+ */
371
+ readonly SYNTH_INTENDED_WARNING_MISMATCH: "SYNTH_INTENDED_WARNING_MISMATCH";
372
+ /** A concept's code-system URI has no OID mapping in the C-CDA example-code table. Fatal. */
373
+ readonly SYNTH_UNMAPPED_CODE_SYSTEM: "SYNTH_UNMAPPED_CODE_SYSTEM";
374
+ /** A money value could not be read as an X12 decimal. Fatal — a generator never rounds to a float. */
375
+ readonly SYNTH_INVALID_DECIMAL: "SYNTH_INVALID_DECIMAL";
376
+ /** An integer range was requested with its maximum below its minimum. Fatal. */
377
+ readonly SYNTH_INVALID_RANGE: "SYNTH_INVALID_RANGE";
378
+ /** A value was drawn from an empty pool. Fatal — never a fabricated substitute. */
379
+ readonly SYNTH_EMPTY_POOL: "SYNTH_EMPTY_POOL";
380
+ /** A `defineSynthProfile` spec was not usable (a missing or blank `name`). Fatal. */
381
+ readonly SYNTH_INVALID_PROFILE: "SYNTH_INVALID_PROFILE";
382
+ /**
383
+ * A caller-supplied selector (a message kind, a document type, a corpus mix entry, a claim
384
+ * variant, a Bundle type, a resource profile) is not in the closed set that governs it. Fatal —
385
+ * see `resolveKind`: a selector union is erased at run time, and a selector that falls through
386
+ * either mislabels the fixture or hands the value to a peer builder that quotes it back.
387
+ */
388
+ readonly SYNTH_UNSUPPORTED_KIND: "SYNTH_UNSUPPORTED_KIND";
357
389
  };
358
390
  /**
359
391
  * A value from {@link SYNTH_FATAL_CODES} — the type carried by a thrown {@link SynthError}.
360
392
  */
361
393
  type SynthFatalCode = (typeof SYNTH_FATAL_CODES)[keyof typeof SYNTH_FATAL_CODES];
394
+ /**
395
+ * The **frozen message registry** — the only place a {@link SynthError} message can come from.
396
+ *
397
+ * A message here is a fixed string. It never quotes the request that produced it, and there is no
398
+ * parameter through which it could: {@link SynthError} takes a code and nothing else. That is the
399
+ * whole mechanism, and it is deliberately a mechanism rather than a habit. Every one of these
400
+ * messages used to be assembled by interpolating the caller's value into a template, and the reason
401
+ * that was safe was not the design — it was that the caller happened to be passing a quirk name.
402
+ *
403
+ * The trade is real and is accepted: a fatal no longer tells you *which* value it rejected. It tells
404
+ * you which rule refused, on `err.code`, and the stack frame tells you where. The caller already
405
+ * holds the value it passed.
406
+ *
407
+ * @example
408
+ * ```ts
409
+ * import { SYNTH_FATAL_CODES, SYNTH_FATAL_MESSAGES } from "@cosyte/synth";
410
+ * SYNTH_FATAL_MESSAGES[SYNTH_FATAL_CODES.SYNTH_EMPTY_POOL]; // => "A value was drawn from an empty pool."
411
+ * ```
412
+ */
413
+ declare const SYNTH_FATAL_MESSAGES: Readonly<Record<SynthFatalCode, string>>;
362
414
  /**
363
415
  * The typed error every fatal `@cosyte/synth` condition throws. Carries a stable
364
416
  * {@link SynthFatalCode} so callers branch on `err.code` without matching message text.
365
417
  *
418
+ * It takes **no value parameter**. The message is whatever {@link SYNTH_FATAL_MESSAGES} holds for the
419
+ * code, so no caller-supplied string can reach a diagnostic surface by any route — not `message`, not
420
+ * `stack`, not a field on the thrown object.
421
+ *
366
422
  * @example
367
423
  * ```ts
368
424
  * import { SynthError, SYNTH_FATAL_CODES } from "@cosyte/synth";
369
- * throw new SynthError(SYNTH_FATAL_CODES.SYNTH_UNSUPPORTED_FORMAT, "astm is not yet generable");
425
+ * throw new SynthError(SYNTH_FATAL_CODES.SYNTH_UNSUPPORTED_FORMAT);
370
426
  * ```
371
427
  */
372
428
  declare class SynthError extends Error {
373
429
  /** The stable fatal code. */
374
430
  readonly code: SynthFatalCode;
375
431
  /**
376
- * @param code - The stable {@link SynthFatalCode}.
377
- * @param message - A human-readable detail (never contains PHI — there is none).
432
+ * @param code - The stable {@link SynthFatalCode}. The message comes from the frozen registry.
378
433
  */
379
- constructor(code: SynthFatalCode, message: string);
434
+ constructor(code: SynthFatalCode);
380
435
  }
381
436
 
437
+ /**
438
+ * The **selector chokepoint**. A generator's options are almost all *selectors*: a message kind, a
439
+ * document type, a corpus mix, a claim variant, a Bundle type, a profile. Each is typed as a closed
440
+ * union, and every one of those unions is **erased at run time**, so a JavaScript caller (or a
441
+ * `as never` in someone's test) reaches the branch with any string at all.
442
+ *
443
+ * Three things went wrong when that was left unchecked, and they are all the same bug:
444
+ *
445
+ * 1. **The value reached a diagnostic.** An unrecognised `documentType` travelled into
446
+ * `@cosyte/ccda`'s `buildCcda`, which is entitled to quote it back in its own `TypeError` and
447
+ * does. This package then has a caller-supplied string on an `err.message` and an `err.stack`,
448
+ * through its own public entry point, having taken no care of it.
449
+ * 2. **The value reached the model.** A corpus mix entry becomes an `Artifact.kind` and a
450
+ * `manifest.counts` key, which is precisely the structural-identifier position a downstream
451
+ * package interpolates to describe a location.
452
+ * 3. **The fixture was silently mislabeled.** An exhaustive `switch` over an erased union takes no
453
+ * branch and returns `undefined`, or a trailing `else` quietly generates something else. A corpus
454
+ * whose manifest says it holds one transaction and holds another is a golden file that lies.
455
+ *
456
+ * So a selector is resolved against its own set, once, before anything is generated, and an
457
+ * unrecognised one is a fatal `SYNTH_UNSUPPORTED_KIND`. Like every fatal here it carries a code and a
458
+ * fixed message, and quotes neither the request nor the set.
459
+ *
460
+ * @module
461
+ */
462
+ /**
463
+ * Resolve one caller-supplied selector against the closed set that governs it, or **fail closed**.
464
+ *
465
+ * @param allowed - Every value the selector may take.
466
+ * @param requested - The selector the caller supplied.
467
+ * @returns `requested`, narrowed to the union.
468
+ * @throws SynthError `SYNTH_UNSUPPORTED_KIND` when `requested` is not in `allowed`.
469
+ * @example
470
+ * ```ts
471
+ * import { resolveKind } from "@cosyte/synth";
472
+ * resolveKind(["ccd", "referralNote"] as const, "ccd"); // "ccd"
473
+ * ```
474
+ */
475
+ declare function resolveKind<T extends string>(allowed: readonly T[], requested: string): T;
476
+ /**
477
+ * Resolve every entry of a caller-supplied corpus mix, in order, or **fail closed** on the first
478
+ * unrecognised one.
479
+ *
480
+ * It substitutes the default **only** when the caller supplied nothing, which is exactly what the
481
+ * `??` it replaced did. An empty array is a supplied mix and is returned as one. An earlier version
482
+ * of this function also treated `[]` as "nothing supplied", on the stated grounds that it matched the
483
+ * previous behaviour; it did not — `??` fires on `undefined` and never on `[]` — and it changed the
484
+ * result of six published entry points, turning an explicit empty selection into "generate one of
485
+ * everything". A convenience that fails open is not a convenience.
486
+ *
487
+ * @param allowed - Every kind the corpus may generate.
488
+ * @param requested - The mix the caller supplied, or `undefined` for the default.
489
+ * @param fallback - The default mix, used only when `requested` is `undefined`.
490
+ * @returns The resolved mix.
491
+ * @throws SynthError `SYNTH_UNSUPPORTED_KIND` on the first unrecognised entry.
492
+ * @example
493
+ * ```ts
494
+ * import { resolveMix } from "@cosyte/synth";
495
+ * resolveMix(["Result", "Order"] as const, ["Order"], ["Result", "Order"]); // ["Order"]
496
+ * ```
497
+ */
498
+ declare function resolveMix<T extends string>(allowed: readonly T[], requested: readonly string[] | undefined, fallback: readonly T[]): readonly T[];
499
+
382
500
  /**
383
501
  * `@cosyte/synth` — a deterministic, seedable **synthetic-data / test-fixture generator** for the
384
502
  * cosyte healthcare formats. A *consumer* of the parsers, not a parser: it builds artifacts **through
@@ -404,4 +522,4 @@ declare class SynthError extends Error {
404
522
  */
405
523
  declare const VERSION = "0.0.0";
406
524
 
407
- export { DEA_REGISTRANT_TYPES, DOC_V6_PREFIX, NPI_LUHN_PREFIX, RESERVED_EMAIL_DOMAINS, SYNTHETIC_ASSIGNING_AUTHORITY, SYNTHETIC_CITY_NAMES, SYNTHETIC_FAMILY_NAMES, SYNTHETIC_GIVEN_NAMES, SYNTHETIC_STREET_NAMES, SYNTH_FATAL_CODES, type Sfc32State, SynthError, type SynthFatalCode, TEST_NET_V4_PREFIXES, VERSION, address, dateYmd, dea, deaCheckDigit, email, identifier, ipv4, ipv6, isSyntheticDea, isSyntheticEmail, isSyntheticIp, isSyntheticNpi, isSyntheticPhone, isSyntheticSsn, luhnMod10, name, npi, npiCheckDigit, phone, safe, sfc32Next, splitmix32, ssn, uuid };
525
+ export { DEA_REGISTRANT_TYPES, DOC_V6_PREFIX, NPI_LUHN_PREFIX, RESERVED_EMAIL_DOMAINS, SYNTHETIC_ASSIGNING_AUTHORITY, SYNTHETIC_CITY_NAMES, SYNTHETIC_FAMILY_NAMES, SYNTHETIC_GIVEN_NAMES, SYNTHETIC_STREET_NAMES, SYNTH_FATAL_CODES, SYNTH_FATAL_MESSAGES, type Sfc32State, SynthError, type SynthFatalCode, TEST_NET_V4_PREFIXES, VERSION, address, dateYmd, dea, deaCheckDigit, email, identifier, ipv4, ipv6, isSyntheticDea, isSyntheticEmail, isSyntheticIp, isSyntheticNpi, isSyntheticPhone, isSyntheticSsn, luhnMod10, name, npi, npiCheckDigit, phone, resolveKind, resolveMix, safe, sfc32Next, splitmix32, ssn, uuid };
package/dist/index.mjs CHANGED
@@ -27,6 +27,76 @@ function sfc32Next(s) {
27
27
  return t >>> 0;
28
28
  }
29
29
 
30
+ // src/codes.ts
31
+ var SYNTH_FATAL_CODES = {
32
+ /**
33
+ * A format was requested that this build cannot generate through a real parser builder/serializer.
34
+ * Fatal — never a hand-written byte fallback.
35
+ *
36
+ * **No code path in this build raises it.** All six formats generate, so it is reserved for a
37
+ * future format that does not, and is kept because removing a published code is a breaking change.
38
+ * An unsupported *kind* within a format that does generate is `SYNTH_UNSUPPORTED_KIND`.
39
+ */
40
+ SYNTH_UNSUPPORTED_FORMAT: "SYNTH_UNSUPPORTED_FORMAT",
41
+ /**
42
+ * A vendor quirk was requested that the target format's profile system does not support. Fatal —
43
+ * never a silent no-op and never a fabricated quirk.
44
+ */
45
+ SYNTH_UNSUPPORTED_QUIRK: "SYNTH_UNSUPPORTED_QUIRK",
46
+ /**
47
+ * A quirk transform found no structural anchor to mutate, so the fixture would not carry the
48
+ * deviation it is labelled with. Fatal — a golden file that lies about its parser verdict is worse
49
+ * than no golden file.
50
+ */
51
+ SYNTH_QUIRK_ANCHOR_ABSENT: "SYNTH_QUIRK_ANCHOR_ABSENT",
52
+ /**
53
+ * A bare parse of a freshly-generated quirk artifact did not produce exactly the declared intended
54
+ * warning code(s). Fatal — never emit a mislabeled fixture.
55
+ */
56
+ SYNTH_INTENDED_WARNING_MISMATCH: "SYNTH_INTENDED_WARNING_MISMATCH",
57
+ /** A concept's code-system URI has no OID mapping in the C-CDA example-code table. Fatal. */
58
+ SYNTH_UNMAPPED_CODE_SYSTEM: "SYNTH_UNMAPPED_CODE_SYSTEM",
59
+ /** A money value could not be read as an X12 decimal. Fatal — a generator never rounds to a float. */
60
+ SYNTH_INVALID_DECIMAL: "SYNTH_INVALID_DECIMAL",
61
+ /** An integer range was requested with its maximum below its minimum. Fatal. */
62
+ SYNTH_INVALID_RANGE: "SYNTH_INVALID_RANGE",
63
+ /** A value was drawn from an empty pool. Fatal — never a fabricated substitute. */
64
+ SYNTH_EMPTY_POOL: "SYNTH_EMPTY_POOL",
65
+ /** A `defineSynthProfile` spec was not usable (a missing or blank `name`). Fatal. */
66
+ SYNTH_INVALID_PROFILE: "SYNTH_INVALID_PROFILE",
67
+ /**
68
+ * A caller-supplied selector (a message kind, a document type, a corpus mix entry, a claim
69
+ * variant, a Bundle type, a resource profile) is not in the closed set that governs it. Fatal —
70
+ * see `resolveKind`: a selector union is erased at run time, and a selector that falls through
71
+ * either mislabels the fixture or hands the value to a peer builder that quotes it back.
72
+ */
73
+ SYNTH_UNSUPPORTED_KIND: "SYNTH_UNSUPPORTED_KIND"
74
+ };
75
+ var SYNTH_FATAL_MESSAGES = Object.freeze({
76
+ 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.",
77
+ 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).",
78
+ 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.",
79
+ 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.",
80
+ SYNTH_UNMAPPED_CODE_SYSTEM: "The concept's code-system URI has no OID mapping in the C-CDA example-code table.",
81
+ SYNTH_INVALID_DECIMAL: "The value could not be read as an X12 decimal.",
82
+ SYNTH_INVALID_RANGE: "An integer range was requested with its maximum below its minimum.",
83
+ SYNTH_EMPTY_POOL: "A value was drawn from an empty pool.",
84
+ SYNTH_INVALID_PROFILE: "defineSynthProfile requires a non-empty string name.",
85
+ 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."
86
+ });
87
+ var SynthError = class extends Error {
88
+ /** The stable fatal code. */
89
+ code;
90
+ /**
91
+ * @param code - The stable {@link SynthFatalCode}. The message comes from the frozen registry.
92
+ */
93
+ constructor(code) {
94
+ super(SYNTH_FATAL_MESSAGES[code]);
95
+ this.name = "SynthError";
96
+ this.code = code;
97
+ }
98
+ };
99
+
30
100
  // src/rng/rng.ts
31
101
  var Sfc32Rng = class {
32
102
  seed;
@@ -44,7 +114,7 @@ var Sfc32Rng = class {
44
114
  return this.nextUint32() / 4294967296;
45
115
  }
46
116
  int(min, max) {
47
- if (max < min) throw new RangeError(`Rng.int: max (${String(max)}) < min (${String(min)})`);
117
+ if (max < min) throw new SynthError(SYNTH_FATAL_CODES.SYNTH_INVALID_RANGE);
48
118
  const span = max - min + 1;
49
119
  return min + Math.floor(this.float() * span);
50
120
  }
@@ -52,7 +122,7 @@ var Sfc32Rng = class {
52
122
  return this.float() < p;
53
123
  }
54
124
  pick(items) {
55
- if (items.length === 0) throw new RangeError("Rng.pick: empty array");
125
+ if (items.length === 0) throw new SynthError(SYNTH_FATAL_CODES.SYNTH_EMPTY_POOL);
56
126
  return items[this.int(0, items.length - 1)];
57
127
  }
58
128
  digits(n) {
@@ -395,7 +465,7 @@ function makeCorpus(seed, artifacts, quirks = []) {
395
465
  // src/profile.ts
396
466
  function defineSynthProfile(spec) {
397
467
  if (typeof spec.name !== "string" || spec.name.trim().length === 0) {
398
- throw new TypeError("defineSynthProfile: `name` is required and must be a non-empty string.");
468
+ throw new SynthError(SYNTH_FATAL_CODES.SYNTH_INVALID_PROFILE);
399
469
  }
400
470
  return Object.freeze({
401
471
  name: spec.name,
@@ -405,33 +475,6 @@ function defineSynthProfile(spec) {
405
475
  });
406
476
  }
407
477
 
408
- // src/codes.ts
409
- var SYNTH_FATAL_CODES = {
410
- /**
411
- * A format was requested that this build cannot generate through a real parser builder/serializer
412
- * (e.g. ASTM before `@cosyte/astm`'s serializer ships). Fatal — never a hand-written byte fallback.
413
- */
414
- SYNTH_UNSUPPORTED_FORMAT: "SYNTH_UNSUPPORTED_FORMAT",
415
- /**
416
- * A vendor quirk was requested that the target format's profile system does not support. Fatal —
417
- * never a silent no-op and never a fabricated quirk.
418
- */
419
- SYNTH_UNSUPPORTED_QUIRK: "SYNTH_UNSUPPORTED_QUIRK"
420
- };
421
- var SynthError = class extends Error {
422
- /** The stable fatal code. */
423
- code;
424
- /**
425
- * @param code - The stable {@link SynthFatalCode}.
426
- * @param message - A human-readable detail (never contains PHI — there is none).
427
- */
428
- constructor(code, message) {
429
- super(message);
430
- this.name = "SynthError";
431
- this.code = code;
432
- }
433
- };
434
-
435
478
  // src/quirk.ts
436
479
  var PROFILE_QUIRK_APPLIED = "PROFILE_QUIRK_APPLIED";
437
480
  function sameCodeSet(a, b) {
@@ -448,12 +491,8 @@ function sameCodeSet(a, b) {
448
491
  }
449
492
  function resolveQuirk(registry, format, name2) {
450
493
  const descriptor = registry[name2];
451
- if (descriptor === void 0) {
452
- const supported = Object.keys(registry).sort().join(", ");
453
- throw new SynthError(
454
- SYNTH_FATAL_CODES.SYNTH_UNSUPPORTED_QUIRK,
455
- `${format}: unsupported quirk "${name2}". The ${format} profile system supports: ${supported}.`
456
- );
494
+ if (descriptor === void 0 || descriptor.format !== format) {
495
+ throw new SynthError(SYNTH_FATAL_CODES.SYNTH_UNSUPPORTED_QUIRK);
457
496
  }
458
497
  return descriptor;
459
498
  }
@@ -468,11 +507,9 @@ function profileTolerated(disposition, intendedWarnings, warningsUnderProfile) {
468
507
  return false;
469
508
  }
470
509
  }
471
- function assertIntendedWarnings(quirk, intendedWarnings, bareWarnings) {
510
+ function assertIntendedWarnings(intendedWarnings, bareWarnings) {
472
511
  if (!sameCodeSet(bareWarnings, intendedWarnings)) {
473
- throw new Error(
474
- `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.`
475
- );
512
+ throw new SynthError(SYNTH_FATAL_CODES.SYNTH_INTENDED_WARNING_MISMATCH);
476
513
  }
477
514
  }
478
515
  function validateProfileQuirks(profile, registry, format) {
@@ -480,9 +517,20 @@ function validateProfileQuirks(profile, registry, format) {
480
517
  return profile.quirks;
481
518
  }
482
519
 
520
+ // src/select.ts
521
+ function resolveKind(allowed, requested) {
522
+ const match = allowed.find((value) => value === requested);
523
+ if (match === void 0) throw new SynthError(SYNTH_FATAL_CODES.SYNTH_UNSUPPORTED_KIND);
524
+ return match;
525
+ }
526
+ function resolveMix(allowed, requested, fallback) {
527
+ if (requested === void 0) return fallback;
528
+ return requested.map((entry) => resolveKind(allowed, entry));
529
+ }
530
+
483
531
  // src/index.ts
484
532
  var VERSION = "0.0.0";
485
533
 
486
- export { DEA_REGISTRANT_TYPES, DOC_V6_PREFIX, NPI_LUHN_PREFIX, PROFILE_QUIRK_APPLIED, RESERVED_EMAIL_DOMAINS, SYNTHETIC_ASSIGNING_AUTHORITY, SYNTHETIC_CITY_NAMES, SYNTHETIC_FAMILY_NAMES, SYNTHETIC_GIVEN_NAMES, SYNTHETIC_STREET_NAMES, SYNTH_FATAL_CODES, SynthError, TEST_NET_V4_PREFIXES, VERSION, address, assertIntendedWarnings, createRng, dateYmd, dea, deaCheckDigit, defineSynthProfile, email, identifier, ipv4, ipv6, isSyntheticDea, isSyntheticEmail, isSyntheticIp, isSyntheticNpi, isSyntheticPhone, isSyntheticSsn, luhnMod10, makeCorpus, name, npi, npiCheckDigit, phone, profileTolerated, resolveQuirk, safe, sameCodeSet, sfc32Next, splitmix32, ssn, uuid, validateProfileQuirks };
534
+ export { DEA_REGISTRANT_TYPES, DOC_V6_PREFIX, NPI_LUHN_PREFIX, PROFILE_QUIRK_APPLIED, RESERVED_EMAIL_DOMAINS, SYNTHETIC_ASSIGNING_AUTHORITY, SYNTHETIC_CITY_NAMES, SYNTHETIC_FAMILY_NAMES, SYNTHETIC_GIVEN_NAMES, SYNTHETIC_STREET_NAMES, SYNTH_FATAL_CODES, SYNTH_FATAL_MESSAGES, SynthError, TEST_NET_V4_PREFIXES, VERSION, address, assertIntendedWarnings, createRng, dateYmd, dea, deaCheckDigit, defineSynthProfile, email, identifier, ipv4, ipv6, isSyntheticDea, isSyntheticEmail, isSyntheticIp, isSyntheticNpi, isSyntheticPhone, isSyntheticSsn, luhnMod10, makeCorpus, name, npi, npiCheckDigit, phone, profileTolerated, resolveKind, resolveMix, resolveQuirk, safe, sameCodeSet, sfc32Next, splitmix32, ssn, uuid, validateProfileQuirks };
487
535
  //# sourceMappingURL=index.mjs.map
488
536
  //# sourceMappingURL=index.mjs.map