@johnhenry/graceholder 0.0.0-stage → 0.0.0
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/LICENSE +21 -0
- package/README.md +492 -2
- package/STABILITY.md +24 -0
- package/package.json +126 -4
- package/src/adapters/apollo.d.ts +13 -0
- package/src/adapters/apollo.js +48 -0
- package/src/adapters/fetch.d.ts +15 -0
- package/src/adapters/fetch.js +150 -0
- package/src/adapters/shared.d.ts +14 -0
- package/src/adapters/shared.js +65 -0
- package/src/adapters/urql.d.ts +8 -0
- package/src/adapters/urql.js +53 -0
- package/src/anchor.js +13 -0
- package/src/args.js +41 -0
- package/src/bulk/csv.js +118 -0
- package/src/bulk/ndjson.js +10 -0
- package/src/bulk/plan.js +195 -0
- package/src/bulk/shape.js +67 -0
- package/src/bulk/source.js +24 -0
- package/src/bulk/sql.js +366 -0
- package/src/bulk/stream.js +55 -0
- package/src/cursor.js +25 -0
- package/src/dbg.d.ts +720 -0
- package/src/detmath.js +74 -0
- package/src/directives.js +113 -0
- package/src/dist.js +90 -0
- package/src/errors.js +17 -0
- package/src/execute.js +577 -0
- package/src/explain.js +473 -0
- package/src/fast-check.d.ts +31 -0
- package/src/fast-check.js +58 -0
- package/src/generate.js +84 -0
- package/src/generator-types.d.ts +90 -0
- package/src/generators.js +361 -0
- package/src/image.js +55 -0
- package/src/index.d.ts +507 -0
- package/src/index.js +62 -0
- package/src/infer.js +888 -0
- package/src/key.js +30 -0
- package/src/learn.js +397 -0
- package/src/naming.js +74 -0
- package/src/pack-registry.js +59 -0
- package/src/packs/PROVENANCE.md +26 -0
- package/src/packs/commerce.d.ts +4 -0
- package/src/packs/commerce.js +83 -0
- package/src/packs/finance.d.ts +4 -0
- package/src/packs/finance.js +180 -0
- package/src/packs/geo.d.ts +4 -0
- package/src/packs/geo.js +227 -0
- package/src/packs/health.d.ts +4 -0
- package/src/packs/health.js +76 -0
- package/src/packs/index.d.ts +7 -0
- package/src/packs/index.js +9 -0
- package/src/rng.js +89 -0
- package/src/rules.js +228 -0
- package/src/scenario.js +120 -0
- package/src/schema.js +148 -0
- package/src/spec.js +235 -0
- package/src/suggest.js +138 -0
- package/src/typed.d.ts +723 -0
- package/src/vocab/PROVENANCE.md +12 -0
- package/src/vocab/en.js +106 -0
- package/src/world.js +461 -0
- package/src/xxh64.js +87 -0
package/src/detmath.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Deterministic elementary functions built only from + - * / and exact scaling by 2.
|
|
2
|
+
// IEEE-754 basic operations are correctly rounded on every engine, whereas Math.exp, Math.log,
|
|
3
|
+
// Math.pow, Math.sin and Math.cos may differ in the last bit between engines. Output that
|
|
4
|
+
// depends on these functions must be byte-identical everywhere (docs/design.md section 11), so
|
|
5
|
+
// distribution sampling uses these instead. Accuracy is far better than the 2-4 decimals that
|
|
6
|
+
// are ever emitted; determinism, not libm accuracy, is the point.
|
|
7
|
+
|
|
8
|
+
const LN2 = 0.6931471805599453;
|
|
9
|
+
const TWO_PI = 6.283185307179586;
|
|
10
|
+
|
|
11
|
+
/** Natural logarithm of x > 0. */
|
|
12
|
+
export function ln(x) {
|
|
13
|
+
if (!(x > 0)) throw new RangeError(`ln(${x}): need x > 0`);
|
|
14
|
+
let e = 0;
|
|
15
|
+
while (x >= 2) { x /= 2; e++; }
|
|
16
|
+
while (x < 1) { x *= 2; e--; }
|
|
17
|
+
if (x > 1.4142135623730951) { x /= 2; e++; }
|
|
18
|
+
const z = (x - 1) / (x + 1);
|
|
19
|
+
const z2 = z * z;
|
|
20
|
+
let term = z;
|
|
21
|
+
let sum = 0;
|
|
22
|
+
for (let k = 1; k <= 41; k += 2) {
|
|
23
|
+
sum += term / k;
|
|
24
|
+
term *= z2;
|
|
25
|
+
}
|
|
26
|
+
return 2 * sum + e * LN2;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** e^x for |x| below about 700. */
|
|
30
|
+
export function exp(x) {
|
|
31
|
+
const k = Math.round(x / LN2);
|
|
32
|
+
const r = x - k * LN2;
|
|
33
|
+
let term = 1;
|
|
34
|
+
let sum = 1;
|
|
35
|
+
for (let n = 1; n <= 24; n++) {
|
|
36
|
+
term = (term * r) / n;
|
|
37
|
+
sum += term;
|
|
38
|
+
}
|
|
39
|
+
const base = k < 0 ? 0.5 : 2;
|
|
40
|
+
let p = 1;
|
|
41
|
+
for (let i = Math.abs(k); i > 0; i--) p *= base;
|
|
42
|
+
return sum * p;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** x^y for x > 0. */
|
|
46
|
+
export const pow = (x, y) => exp(y * ln(x));
|
|
47
|
+
|
|
48
|
+
const reduce = (x) => x - Math.round(x / TWO_PI) * TWO_PI;
|
|
49
|
+
|
|
50
|
+
export function sin(x) {
|
|
51
|
+
const r = reduce(x);
|
|
52
|
+
const r2 = r * r;
|
|
53
|
+
let term = r;
|
|
54
|
+
let sum = r;
|
|
55
|
+
for (let n = 1; n <= 15; n++) {
|
|
56
|
+
term = (-term * r2) / ((2 * n) * (2 * n + 1));
|
|
57
|
+
sum += term;
|
|
58
|
+
}
|
|
59
|
+
return sum;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function cos(x) {
|
|
63
|
+
const r = reduce(x);
|
|
64
|
+
const r2 = r * r;
|
|
65
|
+
let term = 1;
|
|
66
|
+
let sum = 1;
|
|
67
|
+
for (let n = 1; n <= 15; n++) {
|
|
68
|
+
term = (-term * r2) / ((2 * n - 1) * (2 * n));
|
|
69
|
+
sum += term;
|
|
70
|
+
}
|
|
71
|
+
return sum;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export { TWO_PI };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Inline directives (docs/design.md 5.1): parsed into the same override properties as the
|
|
2
|
+
// side-car spec, then stripped from the document before validation and execution.
|
|
3
|
+
import { valueFromASTUntyped, Kind, visit } from "graphql";
|
|
4
|
+
import { GraceholderError } from "./errors.js";
|
|
5
|
+
import { normalizeCount, checkPick } from "./spec.js";
|
|
6
|
+
|
|
7
|
+
export const BUILTIN_DIRECTIVES = new Set(["skip", "include"]);
|
|
8
|
+
export const DIRECTIVES = ["list", "one", "count", "gen", "pattern", "pick", "nullRate", "derive", "type", "seed"];
|
|
9
|
+
|
|
10
|
+
const lev = (a, b) => {
|
|
11
|
+
const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
12
|
+
for (let j = 1; j <= b.length; j++) d[0][j] = j;
|
|
13
|
+
for (let i = 1; i <= a.length; i++) for (let j = 1; j <= b.length; j++) d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
14
|
+
return d[a.length][b.length];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const nearest = (name, names) => [...names].sort((x, y) => lev(name, x) - lev(name, y))[0];
|
|
18
|
+
|
|
19
|
+
function unknownDirective(name) {
|
|
20
|
+
const near = nearest(name, DIRECTIVES);
|
|
21
|
+
return new GraceholderError("unknown-directive", `unknown directive @${name}`, {
|
|
22
|
+
hint: `did you mean @${near}? Known: ${DIRECTIVES.map((d) => `@${d}`).join(", ")}, @skip, @include`,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Throw for any directive that is neither built in nor ours, and for graceholder directives outside fields. */
|
|
27
|
+
export function checkDirectives(roots) {
|
|
28
|
+
for (const root of roots) {
|
|
29
|
+
visit(root, {
|
|
30
|
+
Directive(node, _key, _parent, _path, /** @type {any} */ ancestors) {
|
|
31
|
+
const name = node.name.value;
|
|
32
|
+
if (BUILTIN_DIRECTIVES.has(name)) return;
|
|
33
|
+
if (!DIRECTIVES.includes(name)) throw unknownDirective(name);
|
|
34
|
+
const owner = ancestors[ancestors.length - 1];
|
|
35
|
+
const holder = Array.isArray(owner) ? ancestors[ancestors.length - 2] : owner;
|
|
36
|
+
const ok = holder?.kind === Kind.FIELD || (holder?.kind === Kind.OPERATION_DEFINITION && name === "seed");
|
|
37
|
+
if (!ok) {
|
|
38
|
+
throw new GraceholderError("unknown-directive", `@${name} is not allowed here`, { hint: name === "seed" ? "@seed goes on a field or the operation" : `@${name} goes on a field` });
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Remove graceholder directives (keeps @skip and @include). */
|
|
46
|
+
export function stripDirectives(node) {
|
|
47
|
+
return visit(node, {
|
|
48
|
+
Directive(d) {
|
|
49
|
+
return BUILTIN_DIRECTIVES.has(d.name.value) ? undefined : null;
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {readonly import("graphql").DirectiveNode[]} directives
|
|
56
|
+
* @param {Record<string, unknown>} variables
|
|
57
|
+
* @param {string} path
|
|
58
|
+
* @returns {Record<string, any>} override properties (same shape as a compiled spec entry)
|
|
59
|
+
*/
|
|
60
|
+
export function directiveProps(directives, variables, path) {
|
|
61
|
+
const props = {};
|
|
62
|
+
const fail = (code, name, msg) => new GraceholderError(code, `@${name}: ${msg}`, { path });
|
|
63
|
+
const badDir = (name) => (msg) => fail("bad-directive", name, msg);
|
|
64
|
+
const set = (prop, value, name) => {
|
|
65
|
+
if (props[prop] !== undefined && JSON.stringify(props[prop]) !== JSON.stringify(value)) {
|
|
66
|
+
throw fail("conflicting-overrides", name, `conflicts with another directive on the same field (${prop})`);
|
|
67
|
+
}
|
|
68
|
+
if (props[prop] !== undefined) throw fail("conflicting-overrides", name, `appears more than once on the same field`);
|
|
69
|
+
props[prop] = value;
|
|
70
|
+
};
|
|
71
|
+
for (const d of directives ?? []) {
|
|
72
|
+
const name = d.name.value;
|
|
73
|
+
if (BUILTIN_DIRECTIVES.has(name)) continue;
|
|
74
|
+
const args = {};
|
|
75
|
+
for (const a of d.arguments ?? []) args[a.name.value] = valueFromASTUntyped(a.value, variables);
|
|
76
|
+
const only = (...allowed) => {
|
|
77
|
+
for (const k of Object.keys(args)) if (!allowed.includes(k)) throw fail("bad-directive", name, `unknown argument ${JSON.stringify(k)} (expected ${allowed.join(", ") || "none"})`);
|
|
78
|
+
};
|
|
79
|
+
const fb = badDir(name);
|
|
80
|
+
switch (name) {
|
|
81
|
+
case "list": only(); set("list", true, name); break;
|
|
82
|
+
case "one": only(); set("one", true, name); break;
|
|
83
|
+
case "count": {
|
|
84
|
+
only("n", "min", "max");
|
|
85
|
+
const value = args.n !== undefined ? args.n : [args.min, args.max];
|
|
86
|
+
if (args.n !== undefined && (args.min !== undefined || args.max !== undefined)) throw fb("use n, or min and max, not both");
|
|
87
|
+
set("count", normalizeCount(value, fb), name);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
case "gen": {
|
|
91
|
+
const { name: genName, ...rest } = args;
|
|
92
|
+
if (typeof genName !== "string") throw fb("name must be a string");
|
|
93
|
+
set("gen", { name: genName, args: rest }, name);
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
case "pattern": only("p"); if (typeof args.p !== "string") throw fb("p must be a string"); set("gen", { name: "pattern", args: { p: args.p } }, name); break;
|
|
97
|
+
case "pick": only("values"); set("gen", { name: "oneOf", args: { values: checkPick(args.values, fb) } }, name); break;
|
|
98
|
+
case "nullRate":
|
|
99
|
+
only("p");
|
|
100
|
+
if (typeof args.p !== "number" || !(args.p >= 0 && args.p <= 1)) throw fb("p must be a number between 0 and 1");
|
|
101
|
+
set("nullRate", args.p, name);
|
|
102
|
+
break;
|
|
103
|
+
case "derive": only("from"); if (typeof args.from !== "string" || !args.from) throw fb("from must be a field name"); set("derive", args.from, name); break;
|
|
104
|
+
case "type": only("name"); if (typeof args.name !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(args.name)) throw fb("name must be a GraphQL type name"); set("type", args.name, name); break;
|
|
105
|
+
case "seed": only("s"); if (typeof args.s !== "string" && typeof args.s !== "number") throw fb("s must be a string or number"); set("seed", String(args.s), name); break;
|
|
106
|
+
default: throw unknownDirective(name);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (props.list && props.one) throw new GraceholderError("conflicting-overrides", "@list and @one on the same field", { path });
|
|
110
|
+
if (props.one) props.list = false;
|
|
111
|
+
delete props.one;
|
|
112
|
+
return props;
|
|
113
|
+
}
|
package/src/dist.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Distribution sampling (docs/design.md section 17). Every function draws only from the
|
|
2
|
+
// stream it is given (the per-value stream), so a value never depends on any other value.
|
|
3
|
+
// Only IEEE basic operations and src/detmath.js are used, so results are identical on every engine.
|
|
4
|
+
import { ln, exp, pow, cos, TWO_PI } from "./detmath.js";
|
|
5
|
+
import { GraceholderError } from "./errors.js";
|
|
6
|
+
|
|
7
|
+
export const DISTRIBUTIONS = Object.freeze(["uniform", "normal", "lognormal", "zipf", "powerlaw"]);
|
|
8
|
+
|
|
9
|
+
const badArgs = (name, msg) => new GraceholderError("bad-generator-args", `${name}: ${msg}`);
|
|
10
|
+
|
|
11
|
+
/** Standard normal by Box-Muller (the cosine branch); consumes exactly two floats. */
|
|
12
|
+
export function normal01(s) {
|
|
13
|
+
const u1 = 1 - s.float(); // (0, 1]
|
|
14
|
+
const u2 = s.float();
|
|
15
|
+
return Math.sqrt(-2 * ln(u1)) * cos(TWO_PI * u2);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Truncated normal on [lo, hi] by rejection (at most 12 attempts, then clamped). */
|
|
19
|
+
function truncNormal(s, mu, sigma, lo, hi) {
|
|
20
|
+
let x = mu;
|
|
21
|
+
for (let i = 0; i < 12; i++) {
|
|
22
|
+
x = mu + sigma * normal01(s);
|
|
23
|
+
if (x >= lo && x <= hi) return x;
|
|
24
|
+
}
|
|
25
|
+
return Math.min(hi, Math.max(lo, x));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Inverse CDF of the density proportional to y^-alpha on [1, top]; u in [0, 1). */
|
|
29
|
+
export function boundedPowerInverse(u, alpha, top) {
|
|
30
|
+
if (top <= 1) return 1;
|
|
31
|
+
if (alpha === 1) return exp(u * ln(top));
|
|
32
|
+
const a = 1 - alpha;
|
|
33
|
+
return pow(1 + u * (pow(top, a) - 1), 1 / a);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Validate and read the distribution arguments shared by int, float and money. */
|
|
37
|
+
export function distArgs(name, args) {
|
|
38
|
+
const dist = args.dist ?? "uniform";
|
|
39
|
+
if (!DISTRIBUTIONS.includes(dist)) throw badArgs(name, `unknown dist ${JSON.stringify(dist)} (known: ${DISTRIBUTIONS.join(", ")})`);
|
|
40
|
+
for (const k of ["mean", "sd", "mu", "sigma", "alpha", "s"]) {
|
|
41
|
+
if (args[k] !== undefined && !Number.isFinite(args[k])) throw badArgs(name, `${k} must be a number`);
|
|
42
|
+
}
|
|
43
|
+
if (args.sd !== undefined && !(args.sd > 0)) throw badArgs(name, "sd must be positive");
|
|
44
|
+
if (args.sigma !== undefined && !(args.sigma > 0)) throw badArgs(name, "sigma must be positive");
|
|
45
|
+
if (args.alpha !== undefined && !(args.alpha > 0)) throw badArgs(name, "alpha must be positive");
|
|
46
|
+
if (args.s !== undefined && !(args.s > 0)) throw badArgs(name, "s must be positive");
|
|
47
|
+
return dist;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A real number in [min, max] from the named distribution.
|
|
52
|
+
* - uniform: min + u (max - min)
|
|
53
|
+
* - normal: mean (default the midpoint), sd (default range / 6), truncated to [min, max]
|
|
54
|
+
* - lognormal: y = x - min + 1 in [1, range + 1]; ln y ~ N(mu, sigma) truncated to the support;
|
|
55
|
+
* defaults mu = 0.35 ln(range + 1), sigma = ln(range + 1) / 6.5 (median well below the middle, long right tail)
|
|
56
|
+
* - powerlaw: density proportional to y^-alpha on [1, range + 1] (default alpha 2), inverse CDF
|
|
57
|
+
* - zipf: only meaningful for integers, see {@link zipfRank}; here it is the same as powerlaw with exponent `s`
|
|
58
|
+
*/
|
|
59
|
+
export function sampleReal(s, dist, min, max, args = {}) {
|
|
60
|
+
const range = max - min;
|
|
61
|
+
switch (dist) {
|
|
62
|
+
case "uniform":
|
|
63
|
+
return min + s.float() * range;
|
|
64
|
+
case "normal":
|
|
65
|
+
return truncNormal(s, args.mean ?? min + range / 2, args.sd ?? range / 6, min, max);
|
|
66
|
+
case "lognormal": {
|
|
67
|
+
const top = ln(range + 1);
|
|
68
|
+
const y = exp(truncNormal(s, args.mu ?? 0.35 * top, args.sigma ?? top / 6.5, 0, top));
|
|
69
|
+
return Math.min(max, min + (y - 1));
|
|
70
|
+
}
|
|
71
|
+
case "powerlaw":
|
|
72
|
+
return Math.min(max, min + (boundedPowerInverse(s.float(), args.alpha ?? 2, range + 1) - 1));
|
|
73
|
+
default:
|
|
74
|
+
return Math.min(max, min + (boundedPowerInverse(s.float(), args.s ?? 1.1, range + 1) - 1));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Integer in [min, max] from the named distribution. zipf is the bounded power law discretised
|
|
80
|
+
* by flooring: rank k in 1..N has probability proportional to k^(1-s) - (k+1)^(1-s), and the
|
|
81
|
+
* result is min + k - 1, so the smallest value is the most likely.
|
|
82
|
+
*/
|
|
83
|
+
export function sampleInt(s, dist, min, max, args = {}) {
|
|
84
|
+
if (dist === "zipf") {
|
|
85
|
+
const n = max - min + 1;
|
|
86
|
+
const y = boundedPowerInverse(s.float(), args.s ?? 1.1, n + 1);
|
|
87
|
+
return min + Math.min(n, Math.floor(y)) - 1;
|
|
88
|
+
}
|
|
89
|
+
return Math.min(max, Math.max(min, Math.round(sampleReal(s, dist, min, max, args))));
|
|
90
|
+
}
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Error codes are part of the public API (docs/design.md section 9). */
|
|
2
|
+
export class GraceholderError extends Error {
|
|
3
|
+
/**
|
|
4
|
+
* @param {string} code
|
|
5
|
+
* @param {string} message
|
|
6
|
+
* @param {{ path?: string, hint?: string, cause?: unknown, errors?: readonly any[], data?: unknown }} [extra]
|
|
7
|
+
*/
|
|
8
|
+
constructor(code, message, extra = {}) {
|
|
9
|
+
super(message, extra.cause === undefined ? undefined : { cause: extra.cause });
|
|
10
|
+
this.name = "GraceholderError";
|
|
11
|
+
this.code = code;
|
|
12
|
+
if (extra.path !== undefined) this.path = extra.path;
|
|
13
|
+
if (extra.hint !== undefined) this.hint = extra.hint;
|
|
14
|
+
if (extra.errors !== undefined) this.errors = extra.errors;
|
|
15
|
+
if (extra.data !== undefined) this.data = extra.data;
|
|
16
|
+
}
|
|
17
|
+
}
|