@ghostry/fabricator 0.0.2 → 0.0.3
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 +18 -6
- package/dist/esm/Adapter/Core.js +3 -3
- package/dist/esm/Enumeration/Enumerate.js +16 -13
- package/dist/esm/Error/index.js +8 -8
- package/dist/esm/Fabricator/Constructor.js +12 -14
- package/dist/esm/Harnessing/Core.js +30 -0
- package/dist/esm/Harnessing/Salt.js +12 -0
- package/dist/esm/Harnessing/Types.js +1 -0
- package/dist/esm/Instance/Core.js +17 -24
- package/dist/esm/Primitive/recursive/Fabricator.js +1 -1
- package/dist/esm/Random/index.js +24 -79
- package/dist/esm/adapting.js +2 -0
- package/dist/esm/harnessing.js +1 -0
- package/dist/esm/index.js +1 -2
- package/dist/esm/internal.js +1 -2
- package/dist/types/Adapter/Core.d.ts +1 -1
- package/dist/types/Adapter/Types.d.ts +1 -1
- package/dist/types/Enumeration/Enumerate.d.ts +17 -15
- package/dist/types/Error/index.d.ts +15 -19
- package/dist/types/Fabricator/Constructor.d.ts +4 -4
- package/dist/types/Harnessing/Core.d.ts +43 -0
- package/dist/types/Harnessing/Salt.d.ts +31 -0
- package/dist/types/Harnessing/Types.d.ts +79 -0
- package/dist/types/Instance/Core.d.ts +14 -31
- package/dist/types/Instance/Types.d.ts +35 -41
- package/dist/types/Primitive/opaque/Registry.d.ts +1 -1
- package/dist/types/Primitive/recursive/Fabricator.d.ts +8 -8
- package/dist/types/Random/Types.d.ts +97 -217
- package/dist/types/Random/index.d.ts +30 -48
- package/dist/types/Types.d.ts +3 -3
- package/dist/types/adapting.d.ts +32 -0
- package/dist/types/harnessing.d.ts +30 -0
- package/dist/types/index.d.ts +40 -75
- package/dist/types/internal.d.ts +5 -23
- package/package.json +23 -3
- package/dist/esm/Random/CallSite.js +0 -70
- package/dist/types/Random/CallSite.d.ts +0 -78
|
@@ -37,25 +37,14 @@ export type Algorithm = (seed: string) => NumberGenerator;
|
|
|
37
37
|
*
|
|
38
38
|
* Slots, in encoded order:
|
|
39
39
|
*
|
|
40
|
-
* - `
|
|
41
|
-
* ({@link RandomSource.
|
|
40
|
+
* - `salt` — the _instance's_ salt, normalized to its parts
|
|
41
|
+
* ({@link RandomSource.salt}), not anything derived per leaf.
|
|
42
42
|
* - `clock` — this construction's resolved "now," epoch milliseconds
|
|
43
43
|
* (`Instance/Types.ts`'s `Config.clock`). Second, not after the per-leaf
|
|
44
|
-
* slots: blast radius matches `
|
|
45
|
-
* every leaf, unlike `
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* `Options.clock`).
|
|
49
|
-
* - `root` — how `file` and `ordinal` were resolved ({@link RootKind}). Sits
|
|
50
|
-
* immediately before `file` because that is what it explains. Distinguishes
|
|
51
|
-
* the three situations that all produce `file: undefined` (`{ kind: "none" }`
|
|
52
|
-
* attributed, an unattributed seeded construction, a counted recursive
|
|
53
|
-
* expansion), so a captured trace can be replayed faithfully via `new
|
|
54
|
-
* Fabricator(schema, trace)`.
|
|
55
|
-
* - `file` — the file this node's _construction_ (not the node) was attributed
|
|
56
|
-
* to, relative to the instance's {@link Attribution} root (absolute only if
|
|
57
|
-
* outside that root). `undefined` under `"counted"` / `"unattributed"`, or
|
|
58
|
-
* `"attributed"` with `{ kind: "none" }` — {@link RootKind} says which.
|
|
44
|
+
* slots: blast radius matches `salt` — both are instance-level and perturb
|
|
45
|
+
* every leaf, unlike `path`/`kind`/`ordinal`, which narrow to one leaf. Never
|
|
46
|
+
* `undefined`: clock is always a concrete instant before any leaf dispatches
|
|
47
|
+
* (`RandomSource`'s `Options.clock`).
|
|
59
48
|
* - `path` — structural position within the construction: field name, tuple slot,
|
|
60
49
|
* choice option, one segment per nesting level (`Constructor.ts`'s `make`).
|
|
61
50
|
* Distinguishes two leaves of the same kind in one construction. Stable under
|
|
@@ -63,50 +52,55 @@ export type Algorithm = (seed: string) => NumberGenerator;
|
|
|
63
52
|
* identifies it.
|
|
64
53
|
* - `kind` — schema kind the node was constructed as. Redundant given a unique
|
|
65
54
|
* `path`, but changing a field's kind must change its data.
|
|
66
|
-
* - `ordinal` — which construction
|
|
67
|
-
*
|
|
68
|
-
*
|
|
55
|
+
* - `ordinal` — which construction on this source this node belongs to: the next
|
|
56
|
+
* value of the source's one counter, or a pinned value
|
|
57
|
+
* (`ConstructorOptions.ordinal`), which is how a replay avoids advancing that
|
|
58
|
+
* counter. `null` for a build that deliberately takes no ordinal — a
|
|
59
|
+
* `combinatorial`/`coverage` rebuild. `null`, not absent: a pin counts when
|
|
60
|
+
* it is defined, so only a defined "no ordinal" survives being replayed.
|
|
69
61
|
*/
|
|
70
62
|
export type Trace = {
|
|
71
|
-
readonly
|
|
63
|
+
readonly salt: ReadonlyArray<string>;
|
|
72
64
|
readonly clock: number;
|
|
73
|
-
readonly root: RootKind;
|
|
74
|
-
readonly file: string | undefined;
|
|
75
65
|
readonly path: ReadonlyArray<string>;
|
|
76
66
|
readonly kind: string;
|
|
77
|
-
readonly ordinal: number |
|
|
67
|
+
readonly ordinal: number | null;
|
|
78
68
|
};
|
|
79
69
|
/**
|
|
80
70
|
* Caller-supplied overrides for the construction-owned {@link Trace} slots
|
|
81
|
-
* {@link RandomSource.toRoot} resolves. `
|
|
82
|
-
*
|
|
83
|
-
* when replaying), not something `toRoot` substitutes. `path`/`kind` are
|
|
84
|
-
* per-node and applied in `construct()`, not here.
|
|
71
|
+
* {@link RandomSource.toRoot} resolves. `path`/`kind` are the only slots absent:
|
|
72
|
+
* they are per-node and applied in `construct()`, not here.
|
|
85
73
|
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
74
|
+
* `salt` is a pin like the rest — it substitutes into that trace slot and does
|
|
75
|
+
* nothing else. It does not fork, so it neither resets nor sidesteps this
|
|
76
|
+
* source's construction counter: a salted build takes the next ordinal exactly
|
|
77
|
+
* as an unsalted one does. A caller who wants an isolated source, with its own
|
|
78
|
+
* counters, forks — that is what `fork`/`wrap` are for. Already normalized to
|
|
79
|
+
* parts here, since {@link Trace} carries the array form.
|
|
80
|
+
*
|
|
81
|
+
* Definedness, not `in`: a defined `ordinal` — a number, or `null` for "no
|
|
82
|
+
* ordinal" — is taken verbatim and does not advance the counter. That is all a
|
|
83
|
+
* replay needs, since every real {@link Trace} carries a defined ordinal.
|
|
88
84
|
*/
|
|
89
85
|
export type RootPins = {
|
|
86
|
+
salt?: ReadonlyArray<string> | undefined;
|
|
90
87
|
clock?: number | undefined;
|
|
91
|
-
|
|
92
|
-
file?: string | undefined;
|
|
93
|
-
ordinal?: number | undefined;
|
|
88
|
+
ordinal?: number | null | undefined;
|
|
94
89
|
};
|
|
95
90
|
/**
|
|
96
|
-
* A construction's root: every {@link Trace} slot a
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* dispatches.
|
|
91
|
+
* A construction's root: every {@link Trace} slot a construction fixes, before a
|
|
92
|
+
* leaf supplies its own `path`/`kind`. `RandomSource.toRoot` resolves this once
|
|
93
|
+
* per construction; callers spread it into a full {@link Trace} per leaf and
|
|
94
|
+
* hand that to `toStreamFromTrace`. One construction-ordinal bump is reused
|
|
95
|
+
* across every leaf that construction dispatches.
|
|
102
96
|
*/
|
|
103
97
|
export type ConstructionTrace = Omit<Trace, "path" | "kind">;
|
|
104
98
|
/**
|
|
105
99
|
* What a producer is told about the fabrication it is running inside. One
|
|
106
100
|
* object, not a positional list: this is the only channel a producer has, and a
|
|
107
101
|
* future addition must not change every kind's `.as(...)` arity. Curated, not
|
|
108
|
-
* the whole instance `Config` — a producer has no business reading `
|
|
109
|
-
* `
|
|
102
|
+
* the whole instance `Config` — a producer has no business reading `salt`, and
|
|
103
|
+
* `random` already carries this leaf's own derived seed.
|
|
110
104
|
*/
|
|
111
105
|
export type ProduceContext = {
|
|
112
106
|
/** This leaf's own seeded stream, keyed by its structural path. */
|
|
@@ -115,7 +109,7 @@ export type ProduceContext = {
|
|
|
115
109
|
* This construction's resolved "now," epoch milliseconds — the active `wrap`
|
|
116
110
|
* frame's `Config.clock` if one is active, else the instance's
|
|
117
111
|
* (`Instance/Types.ts`'s `Config.clock`). Defaults to an instant derived from
|
|
118
|
-
* the instance's
|
|
112
|
+
* the instance's salt, not `Date.now()`, so a producer that reads it replays
|
|
119
113
|
* like one that only reads `random`. A number, not a `Date`: the instant is
|
|
120
114
|
* fixed once resolved, and a `Date` handed to every producer would be a
|
|
121
115
|
* shared mutable — the same footgun `T.always([])` sharing one array
|
|
@@ -125,238 +119,124 @@ export type ProduceContext = {
|
|
|
125
119
|
};
|
|
126
120
|
/**
|
|
127
121
|
* A kind's opaque custom producer — `.as(produce)` — given a
|
|
128
|
-
* {@link ProduceContext} so its output replays under a
|
|
122
|
+
* {@link ProduceContext} so its output replays under a salt like every other
|
|
129
123
|
* primitive's draws. A zero-argument function (`() => $T`) is still assignable,
|
|
130
124
|
* so every existing `.as(() => ...)` call compiles.
|
|
131
125
|
*/
|
|
132
126
|
export type Produce<$T> = (context: ProduceContext) => $T;
|
|
133
127
|
/**
|
|
134
|
-
*
|
|
135
|
-
* before any of its leaves are dispatched — resolved once, not per leaf, at
|
|
136
|
-
* `Constructor.ts`'s `construct()`. Every leaf then derives its stream from
|
|
137
|
-
* this root plus its own structural path, so inserting or reordering a sibling
|
|
138
|
-
* field can never shift another field onto a different stream.
|
|
139
|
-
*
|
|
140
|
-
* Each variant is a decision about the two {@link Trace} slots a construction
|
|
141
|
-
* owns — `file` and `ordinal` — and nothing else:
|
|
142
|
-
*
|
|
143
|
-
* | variant | `file` | `ordinal` |
|
|
144
|
-
* | ---------------- | ----------------------- | ---------------------- |
|
|
145
|
-
* | `"attributed"` | resolved from the stack | next ordinal for it |
|
|
146
|
-
* | `"counted"` | `undefined` | next file-less ordinal |
|
|
147
|
-
* | `"unattributed"` | `undefined` | `undefined` |
|
|
148
|
-
*
|
|
149
|
-
* `"attributed"` walks the live call stack once (`resolveCallerFile()`) and
|
|
150
|
-
* relativizes it under the instance's `Attribution` root — or, under `{ kind:
|
|
151
|
-
* "none" }`, resolves no file — then assigns the next ordinal for that file.
|
|
152
|
-
* Two constructions in the same file diverge by default; a lone construction
|
|
153
|
-
* stays reproducible across however many times its file is imported or re-run.
|
|
154
|
-
* `ConstructorOptions.file` without `root` pins that file and draws the next
|
|
155
|
-
* ordinal for it (the wrapping- integration case). A replay (`pins.root` given)
|
|
156
|
-
* takes `file` and `ordinal` verbatim, including `undefined`.
|
|
157
|
-
* `test/fixtures/checkout-a`/`checkout-b` stand in for two checkouts using real
|
|
158
|
-
* frames instead.
|
|
159
|
-
*
|
|
160
|
-
* `"counted"` indexes without attributing. Under `{ kind: "none" }` an
|
|
161
|
-
* `"attributed"` scope resolves to precisely this, and the two share the one
|
|
162
|
-
* file-less bucket — reachable only if both kinds of scope were opened on a
|
|
163
|
-
* single source, which nothing does. `"counted"` is not a variant you choose
|
|
164
|
-
* when building — it is recorded on a node's {@link Trace} (and replayed) for
|
|
165
|
-
* expansions inside `T.recursive`, whose private fork uses it so each lazy
|
|
166
|
-
* expansion gets an ordinal (see `recursive/Fabricator.ts`). An identity that
|
|
167
|
-
* isn't a stack frame but should still vary with the instance's seed is
|
|
168
|
-
* `ConstructorOptions.seed`'s layered form (`{@link Layered}`, via
|
|
169
|
-
* `layer(...)`) — see `ConstructorOptions`.
|
|
170
|
-
*
|
|
171
|
-
* `"unattributed"` fixes neither slot — used only by a source already isolated
|
|
172
|
-
* for one construction (an explicitly seeded `new Fabricator(schema, { seed
|
|
173
|
-
* })`, which forks a new `RandomSource` for that one build). A layered seed
|
|
174
|
-
* (`{@link Layered}`, via `layer(...)`) still opens this same scope: composing
|
|
175
|
-
* onto a base seed is a statement about _what_ the fork's seed is, not how the
|
|
176
|
-
* fork itself should be rooted.
|
|
177
|
-
*
|
|
178
|
-
* A plain string union, not a discriminant object: none of the three variants
|
|
179
|
-
* carries data of its own. Caller-supplied overrides go through
|
|
180
|
-
* {@link RootPins} on {@link RandomSource.toRoot}, not through this union. A `{
|
|
181
|
-
* kind: RootKind }` wrapper would invite a field an object shape can carry and
|
|
182
|
-
* a switch can silently ignore.
|
|
183
|
-
*/
|
|
184
|
-
export type RootKind = "attributed" | "counted" | "unattributed";
|
|
185
|
-
/**
|
|
186
|
-
* A seed as a caller supplies it: one string, or several — several lets a
|
|
128
|
+
* A salt as a caller supplies it: one string, or several — several lets a
|
|
187
129
|
* caller compose independent parts (user id, session id, scenario label)
|
|
188
130
|
* without joining them first. Always normalized internally to
|
|
189
|
-
* `ReadonlyArray<string>` (`toRandomSource`'s `
|
|
131
|
+
* `ReadonlyArray<string>` (`toRandomSource`'s `normalizeSalt`); a single string
|
|
190
132
|
* is the one-element case.
|
|
191
133
|
*/
|
|
192
|
-
export type
|
|
134
|
+
export type Salt = string | ReadonlyArray<string>;
|
|
193
135
|
/**
|
|
194
|
-
* A {@link
|
|
195
|
-
* than replacing it — what `layer(
|
|
136
|
+
* A {@link Salt} tagged as composing onto whatever base is in effect, rather
|
|
137
|
+
* than replacing it — what `layer(salt)` (`Random/index.ts`) produces. Tagged
|
|
196
138
|
* with `[Layer]` exactly as `replace()` (`Utility/Core.ts`) tags a merge
|
|
197
139
|
* operand with `[Replace]`, so a caller never names the symbol and no ordinary
|
|
198
|
-
* `
|
|
140
|
+
* `Salt` — string or array — can be mistaken for one.
|
|
199
141
|
*/
|
|
200
142
|
export type Layered = {
|
|
201
|
-
readonly [Layer]:
|
|
143
|
+
readonly [Layer]: Salt;
|
|
202
144
|
};
|
|
203
145
|
/**
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
* `"rooted"` expresses every file relative to `root`, so a checkout at a
|
|
209
|
-
* different absolute path on a different machine derives the same seeds. `root`
|
|
210
|
-
* accepts an absolute path or a `file://` URL (e.g. `new URL("..",
|
|
211
|
-
* import.meta.url).href`, which needs no `node:path`), and is a normalization
|
|
212
|
-
* parameter only — it never enters the hashed material, so moving the root
|
|
213
|
-
* without moving the files under it changes nothing. A file outside `root` is
|
|
214
|
-
* expressed with a leading `..` run rather than left absolute, so it stays
|
|
215
|
-
* stable too, as long as both locations move together under the same checkout
|
|
216
|
-
* (see `relativize`, `Random/CallSite.ts`).
|
|
217
|
-
*
|
|
218
|
-
* `"call site"`, the default, is `"rooted"` at the directory of whichever file
|
|
219
|
-
* called `initialize()` — resolved once, at that call, from the live stack. Two
|
|
220
|
-
* `initialize()` calls in different files that share a seed and happen to
|
|
221
|
-
* produce the same file's-worth of relative paths (a symmetric monorepo layout
|
|
222
|
-
* — this repo's `pkg/fabricator` and `pkg/fabricator-adapter-typebox-v0` test
|
|
223
|
-
* suites) will collide; use `"rooted"` at a shared repository root instead.
|
|
224
|
-
*
|
|
225
|
-
* `"none"` attributes nothing: every construction in the instance draws its
|
|
226
|
-
* root from one shared, file-less counter. Maximally portable — no path can
|
|
227
|
-
* influence a seed — at the cost of every construction sharing one counter, so
|
|
228
|
-
* adding, removing, or reordering a _construction_ anywhere in the instance
|
|
229
|
-
* shifts every later one. Individual fields within one construction are
|
|
230
|
-
* unaffected: they're keyed by structural path, not dispatch order.
|
|
231
|
-
*/
|
|
232
|
-
export type Attribution = {
|
|
233
|
-
kind: "rooted";
|
|
234
|
-
root: string;
|
|
235
|
-
} | {
|
|
236
|
-
kind: "call site";
|
|
237
|
-
} | {
|
|
238
|
-
kind: "none";
|
|
239
|
-
};
|
|
240
|
-
/**
|
|
241
|
-
* {@link Attribution} after `"call site"` has been resolved against the live
|
|
242
|
-
* stack and `root` normalized — what actually drives construction-root
|
|
243
|
-
* resolution. This, not the caller-facing `Attribution`, is what
|
|
244
|
-
* `RandomSource.fork` threads through: re-resolving `"call site"` inside a fork
|
|
245
|
-
* would read the stack at whatever moment the fork happens to run (an
|
|
246
|
-
* explicitly seeded build, an enumeration's rebuild) and root the child
|
|
247
|
-
* somewhere unrelated to the instance that spawned it.
|
|
248
|
-
*/
|
|
249
|
-
export type ResolvedAttribution = {
|
|
250
|
-
kind: "rooted";
|
|
251
|
-
root: string;
|
|
252
|
-
} | {
|
|
253
|
-
kind: "none";
|
|
254
|
-
};
|
|
255
|
-
/**
|
|
256
|
-
* `clock` is required, unlike `seed`/`algorithm`/`attribution` — by the time a
|
|
257
|
-
* `RandomSource` is built, both the wall-clock default and the `"seeded"`
|
|
258
|
-
* policy (`Instance/Types.ts`'s `Config.clock`) have already been resolved to a
|
|
146
|
+
* `clock` is required, unlike `salt`/`algorithm` — by the time a `RandomSource`
|
|
147
|
+
* is built, both the wall-clock default and the `"derived"` policy
|
|
148
|
+
* (`Instance/Types.ts`'s `Config.clock`) have already been resolved to a
|
|
259
149
|
* concrete epoch-millisecond instant (`Instance/Core.ts`'s `resolveClock`), so
|
|
260
150
|
* `toRandomSource` has no default left to supply.
|
|
261
151
|
*/
|
|
262
152
|
export type Options = {
|
|
263
|
-
|
|
153
|
+
salt?: Salt | undefined;
|
|
264
154
|
algorithm?: Algorithm | undefined;
|
|
265
|
-
attribution?: Attribution | undefined;
|
|
266
155
|
clock: number;
|
|
267
156
|
};
|
|
268
157
|
/**
|
|
269
158
|
* `new Fabricator(schema, options)`'s own option shape — not `Options`.
|
|
270
159
|
* {@link Trace} is assignable to this (every slot optional here, required
|
|
271
|
-
* there,
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
* `ordinal` taken verbatim, `undefined` included, no stack walk and no counter
|
|
294
|
-
* bump). `root` absent but `file` given pins that file and draws `ordinal` from
|
|
295
|
-
* _that file's_ counter — the wrapping-integration case. Neither given resolves
|
|
296
|
-
* as an ordinary construction. A seeded construction is not, by default, asking
|
|
297
|
-
* for a different "now"; a replayed trace whose `clock` is present explicitly
|
|
298
|
-
* is.
|
|
160
|
+
* there), which is what makes `new Fabricator(schema, trace)` a legal replay.
|
|
161
|
+
*
|
|
162
|
+
* Every field here pins one {@link Trace} slot, `salt` included — it is not a
|
|
163
|
+
* special case, and it does not fork. A bare `salt` replaces the instance's own
|
|
164
|
+
* for this build; `salt: layer(...)` (via `layer()`, `Random/index.ts`)
|
|
165
|
+
* composes onto whichever base is in effect (`[...instance.salt, ...salt]`), so
|
|
166
|
+
* the construction still varies when the instance is re-salted, which the bare
|
|
167
|
+
* form does not. That is `fork`/`wrap`'s own `Overlay.salt` parity, one level
|
|
168
|
+
* down.
|
|
169
|
+
*
|
|
170
|
+
* Pinning a salt says nothing about anything else. The build takes the next
|
|
171
|
+
* ordinal on its source and inherits the instance's clock, exactly as an
|
|
172
|
+
* unsalted build does — so it shifts, and is shifted by, neighbouring
|
|
173
|
+
* constructions like any other. Two same-salt builds therefore diverge.
|
|
174
|
+
*
|
|
175
|
+
* `clock` / `ordinal` pin the construction-owned {@link Trace} slots
|
|
176
|
+
* {@link RandomSource.toRoot} would otherwise resolve. Definedness, not `in`: a
|
|
177
|
+
* given `ordinal` — a number, or `null` for "no ordinal" — is taken verbatim
|
|
178
|
+
* with no counter bump, which is all a replay needs; without it, the
|
|
179
|
+
* construction takes the source counter's next value. A salted construction is
|
|
180
|
+
* not, by default, asking for a different "now"; a replayed trace whose `clock`
|
|
181
|
+
* is present explicitly is.
|
|
299
182
|
*
|
|
300
183
|
* `path` is the base structural path `make` extends for descendants, so
|
|
301
184
|
* replaying a nested node's trace reproduces its subtree at the positions it
|
|
302
185
|
* originally occupied. `kind`, when a string, must match `schema`'s own
|
|
303
186
|
* `[Kind]` or `construct()` throws `TraceKindMismatchError`.
|
|
304
187
|
*
|
|
305
|
-
* No per-build algorithm override:
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
* `Options` — sets the algorithm.
|
|
188
|
+
* No per-build algorithm override: unlike `salt`, the algorithm is not a trace
|
|
189
|
+
* slot. Only `initialize({ salt, algorithm })` — instance-wide, via `Options` —
|
|
190
|
+
* sets it.
|
|
309
191
|
*/
|
|
310
192
|
export type ConstructorOptions = {
|
|
311
|
-
|
|
193
|
+
salt?: Salt | Layered | undefined;
|
|
312
194
|
clock?: number | undefined;
|
|
313
|
-
root?: RootKind | undefined;
|
|
314
|
-
file?: string | undefined;
|
|
315
195
|
path?: ReadonlyArray<string> | undefined;
|
|
316
196
|
kind?: string | undefined;
|
|
317
|
-
ordinal?: number | undefined;
|
|
197
|
+
ordinal?: number | null | undefined;
|
|
318
198
|
};
|
|
319
199
|
/**
|
|
320
200
|
* An isolated source of randomness: everything a single `initialize()` instance
|
|
321
201
|
* needs to derive private, reproducible seeds for the fabricators it builds.
|
|
322
|
-
* Each instance owns its own
|
|
202
|
+
* Each instance owns its own salt, builder, and per-construction counters —
|
|
323
203
|
* nothing here is shared module-level state, so independently initialized
|
|
324
204
|
* instances (e.g. parallel tests) can never perturb each other.
|
|
325
205
|
*/
|
|
326
206
|
export type RandomSource = {
|
|
327
207
|
/**
|
|
328
|
-
* Resolve one construction's root
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
208
|
+
* Resolve one construction's root: this source's own `salt`/`clock` and the
|
|
209
|
+
* next construction ordinal, each overridable by {@link RootPins}. Called once
|
|
210
|
+
* per `new Fabricator(...)` (or per lazy expansion of a `T.recursive` schema,
|
|
211
|
+
* each of which resolves its own root on a private forked source), never per
|
|
212
|
+
* leaf: the returned {@link ConstructionTrace} is what every leaf beneath it
|
|
332
213
|
* completes into a full {@link Trace} and hands to `toStreamFromTrace`. One
|
|
333
|
-
*
|
|
214
|
+
* construction-ordinal bump serves the whole construction.
|
|
334
215
|
*/
|
|
335
|
-
toRoot(
|
|
216
|
+
toRoot(pins?: RootPins): ConstructionTrace;
|
|
336
217
|
/**
|
|
337
218
|
* The algorithm this source (and every fork of it) hashes with. Stream
|
|
338
219
|
* derivation is _not_ a member: it depends on no per-source state, so it is
|
|
339
220
|
* the free function `toStreamFromTrace(algorithm, trace)`. `toRoot` is the
|
|
340
|
-
* only stateful member (
|
|
221
|
+
* only stateful member (the construction counter).
|
|
341
222
|
*/
|
|
342
223
|
readonly algorithm: Algorithm;
|
|
343
224
|
/**
|
|
344
|
-
* The
|
|
225
|
+
* The salt this instance currently derives every stream from, normalized to
|
|
345
226
|
* its parts — a single string becomes a one-element array. Always an array so
|
|
346
|
-
* a caller reading it back (e.g. `initialize({
|
|
347
|
-
* round-trips through the same `
|
|
227
|
+
* a caller reading it back (e.g. `initialize({ salt: instance.salt })`)
|
|
228
|
+
* round-trips through the same `Salt`-accepting surface it came from.
|
|
348
229
|
*/
|
|
349
|
-
readonly
|
|
230
|
+
readonly salt: ReadonlyArray<string>;
|
|
350
231
|
/**
|
|
351
|
-
* Create a new, fully isolated `RandomSource` — its own private
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
* expands.
|
|
232
|
+
* Create a new, fully isolated `RandomSource` — its own private construction
|
|
233
|
+
* counters, sharing only the algorithm — salted independently from this one.
|
|
234
|
+
* For a build whose randomness must stay entirely self-contained
|
|
235
|
+
* (`T.recursive`, whose expansion count is data-dependent, unlike every other
|
|
236
|
+
* kind's fixed, schema-determined dispatch count; and `coverage`'s
|
|
237
|
+
* permutation stream): forking means its internal draws can never perturb, or
|
|
238
|
+
* be perturbed by, anything else built from the same `initialize()` instance,
|
|
239
|
+
* no matter how many times or how deeply it expands.
|
|
360
240
|
*/
|
|
361
|
-
fork(
|
|
241
|
+
fork(salt: Salt): RandomSource;
|
|
362
242
|
};
|
|
@@ -1,79 +1,61 @@
|
|
|
1
|
-
import type { Algorithm,
|
|
1
|
+
import type { Algorithm, Layered, NumberGenerator, Options, RandomSource, Salt, Stream, Trace } from "./Types";
|
|
2
2
|
/**
|
|
3
|
-
* Build the library's built-in PRNG from a seed
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* Build the library's built-in PRNG from a seed — the genuine article, not a
|
|
4
|
+
* caller-supplied {@link Salt}: by the time anything reaches here the salt is
|
|
5
|
+
* one slot of an `encode`d {@link Trace}, and this string is that whole
|
|
6
|
+
* encoding. The same seed always yields the same stream, and `cyrb128` hashes
|
|
7
|
+
* it to fully seed sfc32's 128 bits of state.
|
|
6
8
|
*/
|
|
7
9
|
export declare function defaultAlgorithm(seed: string): NumberGenerator;
|
|
8
10
|
/**
|
|
9
|
-
* Mint a fresh
|
|
10
|
-
* default (an omitted
|
|
11
|
+
* Mint a fresh salt value — a uint32 label. Not used as `initialize()`'s
|
|
12
|
+
* default (an omitted salt is empty; wall-clock `clock` is the default
|
|
11
13
|
* entropy); kept for callers that want a generated mixer, and for tests that
|
|
12
14
|
* need one.
|
|
13
15
|
*/
|
|
14
|
-
export declare function
|
|
16
|
+
export declare function randomSalt(): string;
|
|
15
17
|
/**
|
|
16
|
-
* Derive the explicit `"
|
|
18
|
+
* Derive the explicit `"derived"` clock — an epoch-millisecond instant, drawn
|
|
17
19
|
* across the full representable `Date` span — from an instance's own
|
|
18
|
-
* `algorithm`/`
|
|
19
|
-
* (`Instance/Core.ts`'s `overlay`); this is the opt-in that makes `
|
|
20
|
+
* `algorithm`/`salt`. The unconfigured default is wall-clock time
|
|
21
|
+
* (`Instance/Core.ts`'s `overlay`); this is the opt-in that makes `salt` alone
|
|
20
22
|
* the reproducibility unit, at the cost of an implausible "now". A throwaway
|
|
21
|
-
* two-element encoding (`JSON.stringify([
|
|
23
|
+
* two-element encoding (`JSON.stringify([salt, "clock"])`), _not_ routed
|
|
22
24
|
* through `RandomSource`/`Trace`: after the clock is folded into stream
|
|
23
25
|
* derivation, a forked source's own stream derivation requires a clock, so
|
|
24
26
|
* deriving the clock from a fork would be circular. Kept below that layer,
|
|
25
27
|
* which also keeps this collision-free with every leaf's `encode(trace)` — that
|
|
26
|
-
* encoding is always
|
|
28
|
+
* encoding is always five elements, this is always two, and the two can never
|
|
27
29
|
* produce the same JSON array. `Math.trunc`ed because a `Date`'s precision is
|
|
28
30
|
* whole milliseconds.
|
|
29
31
|
*/
|
|
30
|
-
export declare function deriveClock(algorithm: Algorithm,
|
|
32
|
+
export declare function deriveClock(algorithm: Algorithm, salt: ReadonlyArray<string>): number;
|
|
31
33
|
/**
|
|
32
34
|
* Collapse a {@link Trace} into one string to hash — and, since
|
|
33
35
|
* `toStreamFromTrace` hashes exactly this output, the _definition_ of that
|
|
34
36
|
* leaf's stream seed. Concatenating fields with a delimiter would collide when
|
|
35
|
-
* a path/kind/
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* `undefined` (`file`, `ordinal`) is the right "this slot doesn't apply" rather
|
|
40
|
-
* than a sentinel string: `JSON.stringify` writes it as `null` in an array
|
|
41
|
-
* position, one unambiguous value, with no chance of colliding with a real file
|
|
42
|
-
* path or index.
|
|
37
|
+
* a path/kind/salt part contains that delimiter — silently: two leaves that
|
|
38
|
+
* should draw independently would share one stream. `JSON.stringify` as an
|
|
39
|
+
* array makes every field's and slot's boundaries unambiguous regardless of
|
|
40
|
+
* content or nesting depth.
|
|
43
41
|
*/
|
|
44
42
|
export declare function encode(trace: Trace): string;
|
|
45
43
|
/**
|
|
46
|
-
* Normalize a caller-supplied {@link
|
|
47
|
-
* a one-element array, an array passes through unchanged, and a missing
|
|
48
|
-
* empty — unless {@link
|
|
49
|
-
*
|
|
44
|
+
* Normalize a caller-supplied {@link Salt} to its parts: a single string becomes
|
|
45
|
+
* a one-element array, an array passes through unchanged, and a missing salt is
|
|
46
|
+
* empty — unless {@link envSalt} supplies one. No generated fallback: an omitted
|
|
47
|
+
* salt is not a second source of entropy beside the instance clock.
|
|
50
48
|
*/
|
|
51
|
-
export declare function
|
|
49
|
+
export declare function normalizeSalt(salt: Salt | undefined): ReadonlyArray<string>;
|
|
52
50
|
/**
|
|
53
|
-
* Tag a
|
|
54
|
-
* replacing it — the reading a bare `
|
|
51
|
+
* Tag a salt as composing onto whatever base is in effect, rather than
|
|
52
|
+
* replacing it — the reading a bare `salt` has everywhere else in this library.
|
|
55
53
|
* Mirrors `replace()`'s `[Replace]` tagging: the `[Layer]` directive is read
|
|
56
|
-
* (and, at every level that accepts one, consumed) by whoever resolves the
|
|
54
|
+
* (and, at every level that accepts one, consumed) by whoever resolves the salt
|
|
57
55
|
* against its base, so a caller never names the symbol.
|
|
58
56
|
*/
|
|
59
|
-
export declare function layer(
|
|
57
|
+
export declare function layer(salt: Salt): Layered;
|
|
60
58
|
export declare function isLayered(value: unknown): value is Layered;
|
|
61
|
-
/**
|
|
62
|
-
* Collapse a caller-facing {@link Attribution} to the
|
|
63
|
-
* {@link ResolvedAttribution} the stream machinery uses. `"call site"` must
|
|
64
|
-
* resolve _here_, and only here: `resolveCallerFile()` reads the live stack,
|
|
65
|
-
* and `toRandomSource` runs synchronously inside `initialize()`, so this is the
|
|
66
|
-
* one moment the first external frame genuinely is the file that called
|
|
67
|
-
* `initialize()`. Resolving lazily — on first construction, or again inside
|
|
68
|
-
* `fork` — would capture whichever file happened to call `new Fabricator(...)`,
|
|
69
|
-
* or whichever internal mechanism happened to trigger a fork.
|
|
70
|
-
*
|
|
71
|
-
* A directory, not the file itself, becomes the root: rooting at the file would
|
|
72
|
-
* relativize that one file to `""` while every sibling still carried a full
|
|
73
|
-
* relative path from a directory one level up, an arbitrary asymmetry with no
|
|
74
|
-
* reason to prefer it.
|
|
75
|
-
*/
|
|
76
|
-
export declare function resolveAttribution(attribution: Attribution | undefined): ResolvedAttribution;
|
|
77
59
|
export declare function toStream(algorithm: Algorithm, seed: string): Stream;
|
|
78
60
|
/**
|
|
79
61
|
* A leaf's stream: `toStream(algorithm, encode(trace))`. This is the
|
|
@@ -86,13 +68,13 @@ export declare function toStream(algorithm: Algorithm, seed: string): Stream;
|
|
|
86
68
|
* {@link deriveClock} cannot route through this: a {@link Trace} carries
|
|
87
69
|
* `clock`, and `deriveClock` is what produces it. That circularity is why
|
|
88
70
|
* `deriveClock` stays below the `RandomSource`/`Trace` layer, with a
|
|
89
|
-
* two-element encoding that can never collide with `encode`'s
|
|
71
|
+
* two-element encoding that can never collide with `encode`'s five.
|
|
90
72
|
*/
|
|
91
73
|
export declare function toStreamFromTrace(algorithm: Algorithm, trace: Trace): Stream;
|
|
92
74
|
/**
|
|
93
75
|
* Create a fresh, self-contained {@link RandomSource} — the randomness state a
|
|
94
76
|
* single `initialize()` instance owns for its lifetime. `options.clock` is
|
|
95
|
-
* baked in here, once, as a plain number — the `"
|
|
77
|
+
* baked in here, once, as a plain number — the `"derived"` policy is already
|
|
96
78
|
* resolved by the caller (`Instance/Core.ts`'s `resolveClock`) before a source
|
|
97
79
|
* is ever built, so every root this source resolves carries the identical
|
|
98
80
|
* instant, and `fork` threads it forward unchanged.
|
package/dist/types/Types.d.ts
CHANGED
|
@@ -6,11 +6,11 @@ export declare const Produces: unique symbol;
|
|
|
6
6
|
export declare const Replace: unique symbol;
|
|
7
7
|
export declare const Fixed: unique symbol;
|
|
8
8
|
/**
|
|
9
|
-
* Tags a `
|
|
10
|
-
* replacing it — the reading a bare `
|
|
9
|
+
* Tags a `Salt` as composing onto whatever base is in effect, rather than
|
|
10
|
+
* replacing it — the reading a bare `salt` has everywhere else in this library.
|
|
11
11
|
* `layer()` (`Random/index.ts`) is the only producer; a caller never names this
|
|
12
12
|
* symbol, mirroring `[Replace]`. Unlike every other symbol in this file, it
|
|
13
|
-
* tags a config value (a `
|
|
13
|
+
* tags a config value (a `Salt`), not a Schema or Fabricator — see
|
|
14
14
|
* `Random/Types.ts`'s `Layered`.
|
|
15
15
|
*/
|
|
16
16
|
export declare const Layer: unique symbol;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `@ghostry/fabricator/adapting` package export — what implementing a
|
|
3
|
+
* schema adapter (e.g. `@ghostry/fabricator-adapter-typebox-v0`) is built from.
|
|
4
|
+
* Named for the activity, not the noun (`Adapter` is one export among several
|
|
5
|
+
* here) — the same pattern `@ghostry/fabricator/harnessing` follows for
|
|
6
|
+
* supplying a test-framework integration.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately separate from the `.` export: `.` is for using fabricator,
|
|
9
|
+
* `./internal` is the structural-walking tools an adapter needs (`Kind`,
|
|
10
|
+
* `Meta`, `Primitive`, `Buildable`), and this is the contract an adapter
|
|
11
|
+
* declares and drives. `Adapting` — the parameter type of every kind's
|
|
12
|
+
* `.adapt(adapter, produce)` producer — stays on `.` instead, despite the name
|
|
13
|
+
* overlap with this module: calling `.adapt()` is an ordinary caller's
|
|
14
|
+
* business, not an adapter author's, the same reason `Stream`/`ProduceContext`
|
|
15
|
+
* live there for `.as(produce)`.
|
|
16
|
+
*
|
|
17
|
+
* This package names no external schema library and depends on none: every
|
|
18
|
+
* mapping, and every dependency it needs, belongs to the adapter.
|
|
19
|
+
*
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
export { walk } from "./Adapter/Core";
|
|
23
|
+
/**
|
|
24
|
+
* `Adapter`/`Recurse` are what an adapter declares; `AdaptationsOf` is how it
|
|
25
|
+
* reads what a Schema declared at the type level, keyed by its own `key`.
|
|
26
|
+
*/
|
|
27
|
+
export type { Adaptations, AdaptationsOf, Adapter, Recurse, } from "./Adapter/Types";
|
|
28
|
+
/**
|
|
29
|
+
* What `[Adaptation]` holds at runtime — a per-adapter map of overrides, read
|
|
30
|
+
* only by an adapter.
|
|
31
|
+
*/
|
|
32
|
+
export { Adaptation } from "./Types";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `@ghostry/fabricator/harnessing` package export — what supplying a
|
|
3
|
+
* `@ghostry/harness` integration is built from. Named for the activity, the
|
|
4
|
+
* same pattern `@ghostry/fabricator/adapting` follows.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately separate from the `.` export: `.` is for using fabricator,
|
|
7
|
+
* `./adapting` is implementing a schema adapter, `./internal` is the
|
|
8
|
+
* structural-walking tools an adapter needs, and this is the contract a
|
|
9
|
+
* test-framework wrapping library drives. Fabricator declares no dependency and
|
|
10
|
+
* no peer on `@ghostry/harness`; the types here describe the part of that
|
|
11
|
+
* package's contract this integration uses, satisfied structurally, so neither
|
|
12
|
+
* depends on the other.
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Decorate an existing `Instance` as a `@ghostry/harness` integration — `{
|
|
18
|
+
* name, provides, around }`. Takes an instance rather than minting one: the
|
|
19
|
+
* caller owns `initialize(...)` (and in particular the suite-wide `clock`), and
|
|
20
|
+
* this only wraps each test body in that instance's per-identity `wrap`.
|
|
21
|
+
*/
|
|
22
|
+
export { integration } from "./Harnessing/Core";
|
|
23
|
+
/**
|
|
24
|
+
* `Identity` is what identifies one registered test or suite; `Integration` is
|
|
25
|
+
* the `{ name, provides, around }` shape `integration(instance)` returns —
|
|
26
|
+
* `provides` is a `Provides<$Context>`, one `Provider` per context key;
|
|
27
|
+
* `FabricatorTestContext` is the `{ fabricator }` slice of the test context
|
|
28
|
+
* this integration contributes.
|
|
29
|
+
*/
|
|
30
|
+
export type { FabricatorTestContext, Identity, Integration, Provider, Provides, } from "./Harnessing/Types";
|