@mark1russell7/coeval 0.4.1 → 0.4.2
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/README.md +1 -0
- package/build/index.d.ts +208 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1096 -25
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/agree.ts +231 -0
- package/src/errors.ts +6 -1
- package/src/evaluate.ts +24 -8
- package/src/formula.ts +10 -8
- package/src/generate.ts +367 -0
- package/src/index.ts +3 -0
- package/src/reference.ts +571 -0
package/src/generate.ts
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { type Json, type Key, docEquals, isEnvelope } from "./doc.ts";
|
|
2
|
+
import { type Kind, type ScopeRule, type Shape, type World, type WorldEntry, isAlias, isKind, isNamespace, lookupKind } from "./world.ts";
|
|
3
|
+
import { pathMatches } from "./parts.ts";
|
|
4
|
+
import { ROOTS } from "./deref.ts";
|
|
5
|
+
|
|
6
|
+
/** A source of random numbers with a seed. The same seed gives the same numbers. */
|
|
7
|
+
export interface Random {
|
|
8
|
+
/** This function gives a number from 0 up to 1, without 1. */
|
|
9
|
+
next(): number;
|
|
10
|
+
/** This function gives a whole number from 0 up to `below`, without `below`. */
|
|
11
|
+
int(below: number): number;
|
|
12
|
+
/** This function gives `true` with the chance `p`. */
|
|
13
|
+
chance(p: number): boolean;
|
|
14
|
+
/** This function gives one item of a list that is not empty. */
|
|
15
|
+
pick<T>(items: readonly T[]): T;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* This function makes a source of random numbers with a seed (the algorithm mulberry32). It needs no package.
|
|
20
|
+
*
|
|
21
|
+
* @param seed - The seed: a whole number.
|
|
22
|
+
* @returns The source.
|
|
23
|
+
*/
|
|
24
|
+
export function random(seed: number): Random {
|
|
25
|
+
let state = seed >>> 0;
|
|
26
|
+
const next = (): number => {
|
|
27
|
+
state = (state + 0x6d2b79f5) >>> 0;
|
|
28
|
+
let t = state;
|
|
29
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
30
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
31
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
32
|
+
};
|
|
33
|
+
const int = (below: number): number => Math.floor(next() * below);
|
|
34
|
+
return { next, int, chance: (p) => next() < p, pick: (items) => items[int(items.length)] as never };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The options of `generate`. */
|
|
38
|
+
export interface GenerateOptions {
|
|
39
|
+
/** The seed. The same seed and the same options give the same document. */
|
|
40
|
+
readonly seed: number;
|
|
41
|
+
/** The types that a document can have. The default is `typesOf(world)`. A namespace with a fallback needs its keys here. */
|
|
42
|
+
readonly types?: readonly (readonly Key[])[];
|
|
43
|
+
/** The type of a bare root value. */
|
|
44
|
+
readonly declaredType?: string;
|
|
45
|
+
/** The names that the host binds at the root. */
|
|
46
|
+
readonly names?: readonly string[];
|
|
47
|
+
/** The deepest nesting of documents. The default is 4. */
|
|
48
|
+
readonly maxDepth?: number;
|
|
49
|
+
/** The most documents in one document. The default is 24. */
|
|
50
|
+
readonly maxSize?: number;
|
|
51
|
+
/** The chance of a malformed choice at each document. The default is 0.05. */
|
|
52
|
+
readonly malformed?: number;
|
|
53
|
+
/**
|
|
54
|
+
* This function gives the data of a document of a type without parts, for example a number for the kind `number`.
|
|
55
|
+
*
|
|
56
|
+
* @param type - The keys of the type.
|
|
57
|
+
* @param random - The source of random numbers.
|
|
58
|
+
* @returns The data, or `undefined` for a random JSON value.
|
|
59
|
+
*/
|
|
60
|
+
readonly data?: (type: readonly Key[], random: Random) => Json | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* This function gives the types to prefer for a part, for example a function for the field `fn` of `call`.
|
|
63
|
+
*
|
|
64
|
+
* @param type - The keys of the type of the parent.
|
|
65
|
+
* @param path - The path of the part in the value of the parent.
|
|
66
|
+
* @returns The types, or `undefined` for no preference.
|
|
67
|
+
*/
|
|
68
|
+
readonly hints?: (type: readonly Key[], path: readonly Key[]) => readonly (readonly Key[])[] | undefined;
|
|
69
|
+
/** The evaluator names for own entries. The default is none. */
|
|
70
|
+
readonly own?: readonly string[];
|
|
71
|
+
/** The keys for the steps of a path after its root and its name. */
|
|
72
|
+
readonly keys?: readonly Key[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* This function lists the types of a world: each kind and each alias, also in a namespace. A namespace with a
|
|
77
|
+
* fallback gives no types, because each key names a kind.
|
|
78
|
+
*
|
|
79
|
+
* @param world - The world.
|
|
80
|
+
* @returns The keys of each type.
|
|
81
|
+
*/
|
|
82
|
+
export function typesOf(world: World): readonly (readonly Key[])[] {
|
|
83
|
+
const out: (readonly Key[])[] = [];
|
|
84
|
+
const visit = (entries: Readonly<Record<string, WorldEntry>>, prefix: readonly Key[]): void => {
|
|
85
|
+
for (const key of Object.keys(entries)) {
|
|
86
|
+
const entry = entries[key];
|
|
87
|
+
if (isKind(entry) || isAlias(entry)) out.push([...prefix, key]);
|
|
88
|
+
else if (isNamespace(entry)) visit(entry.kinds, [...prefix, key]);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
visit(world.kinds, []);
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const NAMES = ["a", "b", "x", "n", "last", "__proto__"] as const;
|
|
96
|
+
const FIELDS = ["a", "b", "c", "1", "__proto__"] as const;
|
|
97
|
+
const KEYS: readonly Key[] = ["a", "b", "x", 0, 1, "0", "length", "type", "value", "parent"];
|
|
98
|
+
|
|
99
|
+
/** This function tells if a kind can be a leaf: it needs no part. */
|
|
100
|
+
function leafKind(kind: Kind | undefined): boolean {
|
|
101
|
+
const shape = kind?.parts;
|
|
102
|
+
return shape === undefined || shape.shape === "none" || shape.shape === "list" || shape.shape === "map" || shape.shape === "text" || shape.shape === "number" || shape.shape === "one";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** A position of a document: the type that it declares, the kinds that it permits, the names that it sees and its depth. */
|
|
106
|
+
interface Position {
|
|
107
|
+
readonly declared: string | undefined;
|
|
108
|
+
readonly kinds: readonly (readonly Key[])[] | undefined;
|
|
109
|
+
readonly names: readonly string[];
|
|
110
|
+
readonly depth: number;
|
|
111
|
+
readonly hint: readonly (readonly Key[])[] | undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The names that the scope rules of one document bind, and the parts that see them. */
|
|
115
|
+
interface Scope {
|
|
116
|
+
readonly rules: readonly ScopeRule[];
|
|
117
|
+
/** The names of each rule, or the names of the headers so far for a rule `headers`. */
|
|
118
|
+
readonly names: string[][];
|
|
119
|
+
/** The number of parts that each rule covered so far. */
|
|
120
|
+
readonly covered: number[];
|
|
121
|
+
/** The data of a source of names, by its path as one text. */
|
|
122
|
+
readonly sources: Map<string, { readonly keys: readonly string[] } | { readonly list: readonly string[] } | { readonly text: string }>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* This function builds a random document for a world from the contracts of its kinds. It reads the shapes of the
|
|
127
|
+
* parts, the declared types, the permitted kinds and the names that the scope rules bind. Some choices are malformed
|
|
128
|
+
* on purpose. Examples are an unknown kind, a value without its shape, a bare value without a type and a name that
|
|
129
|
+
* nothing binds. The depth and the size have limits.
|
|
130
|
+
*
|
|
131
|
+
* @param world - The world.
|
|
132
|
+
* @param options - The seed, the types, the limits and the data of the leaves.
|
|
133
|
+
* @returns The document, as JSON.
|
|
134
|
+
*/
|
|
135
|
+
export function generate(world: World, options: GenerateOptions): unknown {
|
|
136
|
+
const rng = random(options.seed);
|
|
137
|
+
const types = options.types ?? typesOf(world);
|
|
138
|
+
const maxDepth = options.maxDepth ?? 4;
|
|
139
|
+
const maxSize = options.maxSize ?? 24;
|
|
140
|
+
const malformed = options.malformed ?? 0.05;
|
|
141
|
+
const keys = options.keys ?? KEYS;
|
|
142
|
+
const stepTypes = types.filter((type) => {
|
|
143
|
+
const found = lookupKind(world, type);
|
|
144
|
+
return found.ok && Object.hasOwn(found.kind.verbs, "step");
|
|
145
|
+
});
|
|
146
|
+
let size = 0;
|
|
147
|
+
|
|
148
|
+
const kindOf = (type: readonly Key[]): Kind | undefined => {
|
|
149
|
+
const found = lookupKind(world, type);
|
|
150
|
+
return found.ok ? found.kind : undefined;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const anyJson = (depth = 0): Json => {
|
|
154
|
+
const roll = rng.int(depth > 1 ? 6 : 8);
|
|
155
|
+
if (roll === 0) return null;
|
|
156
|
+
if (roll === 1) return rng.chance(0.5);
|
|
157
|
+
if (roll === 2) return rng.pick([0, 1, 2, -1, 0.5, 1e21]);
|
|
158
|
+
if (roll === 3) return rng.pick(["", "a", "text", "0"]);
|
|
159
|
+
if (roll === 4) return rng.int(10);
|
|
160
|
+
if (roll === 5) return rng.pick(NAMES);
|
|
161
|
+
if (roll === 6) return Array.from({ length: rng.int(3) }, () => anyJson(depth + 1));
|
|
162
|
+
const out: Record<string, Json> = {};
|
|
163
|
+
for (let index = rng.int(3); index > 0; index -= 1) Object.defineProperty(out, rng.pick(FIELDS), { value: anyJson(depth + 1), enumerable: true, writable: true, configurable: true });
|
|
164
|
+
return out;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const dataOf = (type: readonly Key[]): Json => options.data?.(type, rng) ?? anyJson();
|
|
168
|
+
|
|
169
|
+
const renderType = (type: readonly Key[]): Json => {
|
|
170
|
+
const roll = rng.next();
|
|
171
|
+
if (roll < 0.06) return { type: "ref", value: ["world", ...type] };
|
|
172
|
+
if (type.length === 1 && roll < 0.8) return type[0] as Json;
|
|
173
|
+
return [...type];
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const permitted = (type: readonly Key[], kinds: readonly (readonly Key[])[] | undefined): boolean =>
|
|
177
|
+
kinds === undefined || kinds.some((kind) => kind.length <= type.length && kind.every((key, index) => String(key) === String(type[index])));
|
|
178
|
+
|
|
179
|
+
const pickName = (names: readonly string[]): string => (names.length === 0 || rng.chance(malformed) ? rng.pick(["unbound", "zz"]) : rng.pick(names));
|
|
180
|
+
|
|
181
|
+
const someNames = (): string[] => {
|
|
182
|
+
const out = new Set<string>();
|
|
183
|
+
for (let index = 1 + rng.int(2); index > 0; index -= 1) out.add(rng.pick(NAMES));
|
|
184
|
+
return [...out];
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const envelope = (type: readonly Key[], value: Json, header?: string): Json => {
|
|
188
|
+
const out: Record<string, Json> = { type: renderType(type), value };
|
|
189
|
+
if (header !== undefined) out["name"] = header;
|
|
190
|
+
if (options.own !== undefined && options.own.length > 0 && rng.chance(0.03)) out["own"] = { eval: rng.pick(options.own) };
|
|
191
|
+
return out;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/** A path from a root (D10). It uses the names in scope, and sometimes a step document. */
|
|
195
|
+
const pathFor = (pos: Position, refKind: Kind | undefined): Json => {
|
|
196
|
+
if (rng.chance(malformed)) return rng.pick<Json>(["names", [], { names: "a" }, ["nowhere", "a"]]);
|
|
197
|
+
const root = pos.names.length > 0 && rng.chance(0.75) ? "names" : rng.pick(ROOTS.filter((name) => name !== "names"));
|
|
198
|
+
const path: Json[] = [root];
|
|
199
|
+
if (root === "names") path.push(pickName(pos.names));
|
|
200
|
+
if (root === "world") path.push(String(rng.pick(types)[0]));
|
|
201
|
+
for (let index = rng.int(3); index > 0; index -= 1) {
|
|
202
|
+
if (stepTypes.length > 0 && rng.chance(0.2)) {
|
|
203
|
+
const type = rng.pick(stepTypes);
|
|
204
|
+
const kind = kindOf(type);
|
|
205
|
+
// With a deep shape of the kind ref, the value of a step document can hold parts.
|
|
206
|
+
const value = refKind?.parts?.shape === "deep" && kind?.parts !== undefined ? valueOf(kind.parts, [], { ...pos, depth: pos.depth + 1 }, emptyScope()) : dataOf(type);
|
|
207
|
+
path.push({ type: renderType(type), value });
|
|
208
|
+
} else {
|
|
209
|
+
path.push(rng.pick(keys) as Json);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return path;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const emptyScope = (): Scope => ({ rules: [], names: [], covered: [], sources: new Map() });
|
|
216
|
+
|
|
217
|
+
/** This function decides the names of the scope rules before the parts, so that each part knows what it sees. */
|
|
218
|
+
const scopeOf = (kind: Kind): Scope => {
|
|
219
|
+
const rules = kind.scope ?? [];
|
|
220
|
+
const scope: Scope = { rules, names: [], covered: rules.map(() => 0), sources: new Map() };
|
|
221
|
+
for (const rule of rules) {
|
|
222
|
+
const source = rule.names;
|
|
223
|
+
let names: string[] = [];
|
|
224
|
+
if ("fixed" in source) names = [...source.fixed];
|
|
225
|
+
else if ("keysAt" in source) {
|
|
226
|
+
names = someNames();
|
|
227
|
+
scope.sources.set(source.keysAt.join("."), { keys: names });
|
|
228
|
+
} else if ("listAt" in source) {
|
|
229
|
+
names = someNames();
|
|
230
|
+
scope.sources.set(source.listAt.join("."), { list: rng.chance(malformed) ? [...names, ...names] : names });
|
|
231
|
+
} else if ("textAt" in source) {
|
|
232
|
+
names = [rng.pick(NAMES)];
|
|
233
|
+
scope.sources.set(source.textAt.join("."), { text: names[0] as string });
|
|
234
|
+
}
|
|
235
|
+
scope.names.push(names);
|
|
236
|
+
}
|
|
237
|
+
return scope;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
/** The names that a part sees from the scope rules of its parent. A rule `sequential` shows only earlier names. */
|
|
241
|
+
const seenBy = (scope: Scope, path: readonly Key[]): { readonly names: readonly string[]; readonly headers: boolean } => {
|
|
242
|
+
const names: string[] = [];
|
|
243
|
+
let headers = false;
|
|
244
|
+
scope.rules.forEach((rule, index) => {
|
|
245
|
+
if (!rule.in.some((pattern) => pathMatches(path, pattern))) return;
|
|
246
|
+
const ruleNames = scope.names[index] ?? [];
|
|
247
|
+
if ("headers" in rule.names) headers = true;
|
|
248
|
+
if (rule.sequential !== true || "headers" in rule.names || (scope.covered[index] ?? 0) > 0) names.push(...ruleNames);
|
|
249
|
+
});
|
|
250
|
+
return { names, headers };
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
/** This function records the part at a path, and the header name that it got, for the rules that cover it. */
|
|
254
|
+
const saw = (scope: Scope, path: readonly Key[], part: Json): void => {
|
|
255
|
+
const header = isEnvelope(part) && typeof (part as { readonly name?: unknown }).name === "string" ? (part as { readonly name: string }).name : undefined;
|
|
256
|
+
scope.rules.forEach((rule, index) => {
|
|
257
|
+
if (!rule.in.some((pattern) => pathMatches(path, pattern))) return;
|
|
258
|
+
scope.covered[index] = (scope.covered[index] ?? 0) + 1;
|
|
259
|
+
if ("headers" in rule.names && header !== undefined) scope.names[index]?.push(header);
|
|
260
|
+
});
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const partAt = (pos: Position, scope: Scope, path: readonly Key[], spec: Extract<Shape, { readonly shape: "term" }> | { readonly shape: "term"; readonly of?: string }, parentType: readonly Key[] | undefined): Json => {
|
|
264
|
+
const seen = seenBy(scope, path);
|
|
265
|
+
const header = seen.headers && rng.chance(0.5) ? rng.pick(NAMES) : undefined;
|
|
266
|
+
const part = docAt(
|
|
267
|
+
{
|
|
268
|
+
declared: spec.of,
|
|
269
|
+
kinds: "kinds" in spec ? spec.kinds : undefined,
|
|
270
|
+
names: seen.names.length === 0 ? pos.names : [...pos.names, ...seen.names],
|
|
271
|
+
depth: pos.depth + 1,
|
|
272
|
+
hint: parentType === undefined ? undefined : options.hints?.(parentType, path),
|
|
273
|
+
},
|
|
274
|
+
header,
|
|
275
|
+
);
|
|
276
|
+
saw(scope, path, part);
|
|
277
|
+
return part;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
/** This function builds a value of a shape (SHAPE.*). A part is a document at the next depth. */
|
|
281
|
+
const valueOf = (shape: Shape | undefined, path: readonly Key[], pos: Position, scope: Scope, type?: readonly Key[]): Json => {
|
|
282
|
+
const source = scope.sources.get(path.join("."));
|
|
283
|
+
if (source !== undefined && "list" in source) return [...source.list];
|
|
284
|
+
if (source !== undefined && "text" in source) return source.text;
|
|
285
|
+
const term = (of: string | undefined): { readonly shape: "term"; readonly of?: string } => (of === undefined ? { shape: "term" } : { shape: "term", of });
|
|
286
|
+
const small = (): number => (pos.depth >= maxDepth ? 0 : size >= maxSize ? rng.int(2) : rng.int(4));
|
|
287
|
+
if (shape === undefined || shape.shape === "none") return path.length === 0 && type !== undefined ? dataOf(type) : anyJson();
|
|
288
|
+
switch (shape.shape) {
|
|
289
|
+
case "term":
|
|
290
|
+
return partAt(pos, scope, path, shape, type);
|
|
291
|
+
case "list": {
|
|
292
|
+
const items = Array.from({ length: small() }, (_, index) => valueOf(shape.item ?? term(shape.of), [...path, index], pos, scope, type));
|
|
293
|
+
if (shape.distinct !== true || rng.chance(malformed)) return items;
|
|
294
|
+
return items.filter((item, index) => !items.slice(0, index).some((before) => docEquals(before, item)));
|
|
295
|
+
}
|
|
296
|
+
case "map": {
|
|
297
|
+
const out: Record<string, Json> = {};
|
|
298
|
+
const names = source !== undefined && "keys" in source ? source.keys : Array.from({ length: small() }, () => rng.pick(FIELDS));
|
|
299
|
+
for (const key of names) Object.defineProperty(out, key, { value: valueOf(shape.item ?? term(shape.of), [...path, key], pos, scope, type), enumerable: true, writable: true, configurable: true });
|
|
300
|
+
return out;
|
|
301
|
+
}
|
|
302
|
+
case "record": {
|
|
303
|
+
const named: Readonly<Record<string, Shape>> = Array.isArray(shape.fields)
|
|
304
|
+
? Object.fromEntries((shape.fields as readonly string[]).map((name) => [name, term(shape.of)]))
|
|
305
|
+
: (shape.fields as Readonly<Record<string, Shape>>);
|
|
306
|
+
const fieldKeys = new Set<string>([...(shape.required ?? []), ...Object.keys(named).filter(() => rng.chance(0.75))]);
|
|
307
|
+
if (shape.closed !== true && shape.rest === undefined && rng.chance(0.15)) fieldKeys.add("extra");
|
|
308
|
+
const ordered = rng.chance(0.2) ? [...fieldKeys].reverse() : [...fieldKeys];
|
|
309
|
+
const out: Record<string, Json> = {};
|
|
310
|
+
for (const key of ordered) {
|
|
311
|
+
if (rng.chance(malformed / 2) && (shape.required ?? []).includes(key)) continue;
|
|
312
|
+
const fieldShape = Object.hasOwn(named, key) ? named[key] : shape.rest;
|
|
313
|
+
Object.defineProperty(out, key, { value: valueOf(fieldShape, [...path, key], pos, scope, type), enumerable: true, writable: true, configurable: true });
|
|
314
|
+
}
|
|
315
|
+
if (shape.closed === true && rng.chance(malformed)) out["stray"] = 1;
|
|
316
|
+
return out;
|
|
317
|
+
}
|
|
318
|
+
case "deep":
|
|
319
|
+
return rng.chance(0.5)
|
|
320
|
+
? [partAt(pos, scope, [...path, 0], term(shape.of), type), anyJson()]
|
|
321
|
+
: { at: partAt(pos, scope, [...path, "at"], term(shape.of), type), data: anyJson() };
|
|
322
|
+
case "text":
|
|
323
|
+
return rng.chance(malformed) ? 1 : rng.pick(["", "a", "label"]);
|
|
324
|
+
case "number": {
|
|
325
|
+
if (rng.chance(malformed)) return "1";
|
|
326
|
+
const low = shape.min ?? -2;
|
|
327
|
+
const value = low + rng.int(5) + (shape.whole === true ? 0 : rng.pick([0, 0.5]));
|
|
328
|
+
return shape.max !== undefined && value > shape.max ? shape.max : value;
|
|
329
|
+
}
|
|
330
|
+
case "one":
|
|
331
|
+
return rng.chance(malformed) ? "other" : (rng.pick(shape.values) as Json);
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
/** A malformed document: an unknown kind, a bad shape, a bare value without a type, or a bad reference. */
|
|
336
|
+
const brokenAt = (pos: Position): Json => {
|
|
337
|
+
const roll = rng.int(5);
|
|
338
|
+
if (roll === 0) return { type: rng.pick<Json>(["nothing", ["no", "such"]]), value: 1 };
|
|
339
|
+
if (roll === 1) return pos.declared === undefined ? rng.pick<Json>([7, "bare", [1]]) : { type: "ref", value: ["names", "unbound"] };
|
|
340
|
+
if (roll === 2) {
|
|
341
|
+
const shaped = types.filter((type) => kindOf(type)?.parts !== undefined);
|
|
342
|
+
if (shaped.length > 0) return envelope(rng.pick(shaped), rng.pick<Json>([7, "x", null]));
|
|
343
|
+
}
|
|
344
|
+
if (roll === 3) return { type: "ref", value: rng.pick<Json>([["names", "unbound"], ["nowhere"], "names"]) };
|
|
345
|
+
return { type: rng.pick(types) as Json, value: anyJson() };
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const docAt = (pos: Position, header?: string): Json => {
|
|
349
|
+
size += 1;
|
|
350
|
+
const leaf = pos.depth >= maxDepth || size >= maxSize;
|
|
351
|
+
if (rng.chance(malformed)) return brokenAt(pos);
|
|
352
|
+
if (pos.declared !== undefined && rng.chance(leaf ? 0.7 : 0.3)) return dataOf([pos.declared]);
|
|
353
|
+
let candidates = types.filter((type) => permitted(type, pos.kinds) && (!leaf || leafKind(kindOf(type))));
|
|
354
|
+
if (pos.hint !== undefined && rng.chance(0.6)) {
|
|
355
|
+
const hinted = candidates.filter((type) => pos.hint?.some((hint) => hint.length === type.length && hint.every((key, index) => key === type[index])));
|
|
356
|
+
if (hinted.length > 0) candidates = hinted;
|
|
357
|
+
}
|
|
358
|
+
if (candidates.length === 0) candidates = [...types];
|
|
359
|
+
const type = rng.pick(candidates);
|
|
360
|
+
const kind = kindOf(type);
|
|
361
|
+
if (type.length === 1 && type[0] === "ref") return envelope(type, pathFor(pos, kind), header);
|
|
362
|
+
const value = kind === undefined ? dataOf(type) : valueOf(kind.parts, [], pos, scopeOf(kind), type);
|
|
363
|
+
return envelope(type, value, header);
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
return docAt({ declared: options.declaredType, kinds: undefined, names: options.names ?? [], depth: 0, hint: undefined });
|
|
367
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -185,3 +185,6 @@ export {
|
|
|
185
185
|
isClosure,
|
|
186
186
|
jsTruthy,
|
|
187
187
|
} from "./formula.ts";
|
|
188
|
+
export { type Definitions, type ReferenceOptions, formulaDefinitions, reference } from "./reference.ts";
|
|
189
|
+
export { type AgreeOptions, type Agreement, type Tier, type TierResult, agree, sameValue } from "./agree.ts";
|
|
190
|
+
export { type GenerateOptions, type Random, generate, random, typesOf } from "./generate.ts";
|