@ghostry/fabricator 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 (44) hide show
  1. package/README.md +18 -6
  2. package/dist/esm/Adapter/Core.js +3 -3
  3. package/dist/esm/Enumeration/Enumerate.js +22 -18
  4. package/dist/esm/Error/index.js +8 -8
  5. package/dist/esm/Fabricator/Constructor.js +22 -23
  6. package/dist/esm/Harnessing/Core.js +30 -0
  7. package/dist/esm/Harnessing/Salt.js +12 -0
  8. package/dist/esm/Harnessing/Types.js +1 -0
  9. package/dist/esm/Instance/Core.js +65 -38
  10. package/dist/esm/Instance/Stack/Async.js +10 -2
  11. package/dist/esm/Instance/Stack/Sync.js +2 -1
  12. package/dist/esm/Instance/Stack/Visible.js +13 -0
  13. package/dist/esm/Primitive/recursive/Fabricator.js +1 -1
  14. package/dist/esm/Random/index.js +25 -80
  15. package/dist/esm/adapting.js +2 -0
  16. package/dist/esm/harnessing.js +1 -0
  17. package/dist/esm/index.js +1 -2
  18. package/dist/esm/internal.js +1 -2
  19. package/dist/types/Adapter/Core.d.ts +1 -1
  20. package/dist/types/Adapter/Types.d.ts +1 -1
  21. package/dist/types/Enumeration/Enumerate.d.ts +23 -21
  22. package/dist/types/Error/index.d.ts +15 -19
  23. package/dist/types/Fabricator/Constructor.d.ts +14 -11
  24. package/dist/types/Fabricator/Types.d.ts +15 -14
  25. package/dist/types/Harnessing/Core.d.ts +43 -0
  26. package/dist/types/Harnessing/Salt.d.ts +31 -0
  27. package/dist/types/Harnessing/Types.d.ts +79 -0
  28. package/dist/types/Instance/Core.d.ts +31 -37
  29. package/dist/types/Instance/Stack/Async.d.ts +7 -0
  30. package/dist/types/Instance/Stack/Sync.d.ts +5 -3
  31. package/dist/types/Instance/Stack/Visible.d.ts +22 -0
  32. package/dist/types/Instance/Types.d.ts +202 -58
  33. package/dist/types/Primitive/opaque/Registry.d.ts +1 -1
  34. package/dist/types/Primitive/recursive/Fabricator.d.ts +8 -9
  35. package/dist/types/Random/Types.d.ts +103 -221
  36. package/dist/types/Random/index.d.ts +33 -51
  37. package/dist/types/Types.d.ts +3 -3
  38. package/dist/types/adapting.d.ts +32 -0
  39. package/dist/types/harnessing.d.ts +30 -0
  40. package/dist/types/index.d.ts +51 -77
  41. package/dist/types/internal.d.ts +5 -23
  42. package/package.json +23 -3
  43. package/dist/esm/Random/CallSite.js +0 -70
  44. package/dist/types/Random/CallSite.d.ts +0 -78
@@ -1,85 +1,53 @@
1
- import { FabricatorError } from "../Error/index.js";
2
1
  import { Layer, MAX_TIME } from "../Types.js";
3
- import { inline } from "../Utility/Core.js";
4
2
  import { cyrb128 } from "../Utility/Digest.js";
5
- import { directoryOf, normalizeLocation, relativize, resolveCallerFile } from "./CallSite.js";
6
3
  import { sfc32 } from "./Generator/sfc32.js";
7
4
  function defaultAlgorithm(seed) {
8
5
  return sfc32(...cyrb128(seed));
9
6
  }
10
- function randomSeed() {
7
+ function randomSalt() {
11
8
  return (0x100000000 * Math.random() >>> 0).toString(10);
12
9
  }
13
- function deriveClock(algorithm, seed) {
10
+ function deriveClock(algorithm, salt) {
14
11
  const stream = toStream(algorithm, JSON.stringify([
15
- seed,
12
+ salt,
16
13
  "clock"
17
14
  ]));
18
15
  return Math.trunc((2 * stream.next() - 1) * MAX_TIME);
19
16
  }
20
- function envSeed() {
21
- var _ref, _ref1;
17
+ function envSalt() {
22
18
  const env = "object" == typeof process ? process.env : void 0;
23
- return null != (_ref = null != (_ref1 = null == env ? void 0 : env["FABRICATOR_SEED"]) ? _ref1 : null == env ? void 0 : env["SEED"]) ? _ref : null == env ? void 0 : env["RANDOM_SEED"];
19
+ return null == env ? void 0 : env["FABRICATOR_SALT"];
24
20
  }
25
21
  function encode(trace) {
26
22
  return JSON.stringify([
27
- trace.seed,
23
+ trace.salt,
28
24
  trace.clock,
29
- trace.root,
30
- trace.file,
31
25
  trace.path,
32
26
  trace.kind,
33
27
  trace.ordinal
34
28
  ]);
35
29
  }
36
- function normalizeSeed(seed) {
37
- if (void 0 === seed) {
38
- const fromEnv = envSeed();
30
+ function normalizeSalt(salt) {
31
+ if (void 0 === salt) {
32
+ const fromEnv = envSalt();
39
33
  return void 0 === fromEnv ? [] : [
40
34
  fromEnv
41
35
  ];
42
36
  }
43
- return "string" == typeof seed ? [
44
- seed
37
+ return "string" == typeof salt ? [
38
+ salt
45
39
  ] : [
46
- ...seed
40
+ ...salt
47
41
  ];
48
42
  }
49
- function Random_layer(seed) {
43
+ function Random_layer(salt) {
50
44
  return {
51
- [Layer]: seed
45
+ [Layer]: salt
52
46
  };
53
47
  }
54
48
  function isLayered(value) {
55
49
  return "object" == typeof value && null !== value && Layer in value;
56
50
  }
57
- function resolveAttribution(attribution) {
58
- const policy = null != attribution ? attribution : {
59
- kind: "call site"
60
- };
61
- switch(policy.kind){
62
- case "none":
63
- return policy;
64
- case "rooted":
65
- {
66
- const root = normalizeLocation(policy.root);
67
- if (!root.startsWith("/")) throw new FabricatorError.InvalidAttributionRootError(policy.root);
68
- return toRooted(root);
69
- }
70
- case "call site":
71
- {
72
- const root = directoryOf(resolveCallerFile());
73
- return toRooted(root);
74
- }
75
- }
76
- }
77
- function toRooted(root) {
78
- return {
79
- kind: "rooted",
80
- root: root.endsWith("/") ? root : `${root}/`
81
- };
82
- }
83
51
  function toStream(algorithm, seed) {
84
52
  const generator = algorithm(seed);
85
53
  let iterations = 0;
@@ -99,53 +67,30 @@ function toStreamFromTrace(algorithm, trace) {
99
67
  }
100
68
  function toRandomSource(options) {
101
69
  var _options_algorithm;
102
- let seed = normalizeSeed(options.seed);
70
+ let salt = normalizeSalt(options.salt);
103
71
  let algorithm = null != (_options_algorithm = options.algorithm) ? _options_algorithm : defaultAlgorithm;
104
- let attribution = resolveAttribution(options.attribution);
105
72
  const clock = options.clock;
106
- let constructionOrdinals = new Map();
107
- function nextConstructionOrdinal(file) {
108
- var _constructionOrdinals_get;
109
- const ordinal = null != (_constructionOrdinals_get = constructionOrdinals.get(file)) ? _constructionOrdinals_get : 0;
110
- constructionOrdinals.set(file, ordinal + 1);
111
- return ordinal;
112
- }
113
- function toRoot(kind, pins = {}) {
114
- var _pins_root, _pins_clock;
115
- const replaying = void 0 !== pins.root;
116
- const root = null != (_pins_root = pins.root) ? _pins_root : kind;
117
- const file = replaying || void 0 !== pins.file ? pins.file : resolveRootFile(kind);
118
- const ordinal = inline(()=>{
119
- if (replaying || void 0 !== pins.ordinal) return pins.ordinal;
120
- if ("unattributed" === root) return;
121
- return nextConstructionOrdinal(file);
122
- });
73
+ let constructionOrdinal = 0;
74
+ function toConstructionTrace(pins = {}) {
75
+ var _pins_salt, _pins_clock;
123
76
  return {
124
- seed,
77
+ salt: null != (_pins_salt = pins.salt) ? _pins_salt : salt,
125
78
  clock: null != (_pins_clock = pins.clock) ? _pins_clock : clock,
126
- root,
127
- file,
128
- ordinal
79
+ ordinal: void 0 !== pins.ordinal ? pins.ordinal : constructionOrdinal++
129
80
  };
130
81
  }
131
- function resolveRootFile(kind) {
132
- if ("attributed" !== kind) return;
133
- if ("none" === attribution.kind) return;
134
- return relativize(attribution.root, resolveCallerFile());
135
- }
136
- function fork(childSeed) {
82
+ function fork(childSalt) {
137
83
  return toRandomSource({
138
- seed: childSeed,
84
+ salt: childSalt,
139
85
  algorithm,
140
- attribution,
141
86
  clock
142
87
  });
143
88
  }
144
89
  return {
145
- toRoot,
90
+ toConstructionTrace,
146
91
  algorithm,
147
- seed,
92
+ salt,
148
93
  fork
149
94
  };
150
95
  }
151
- export { Random_layer as layer, defaultAlgorithm, deriveClock, encode, isLayered, normalizeSeed, randomSeed, resolveAttribution, toRandomSource, toStream, toStreamFromTrace };
96
+ export { Random_layer as layer, defaultAlgorithm, deriveClock, encode, isLayered, normalizeSalt, randomSalt, toRandomSource, toStream, toStreamFromTrace };
@@ -0,0 +1,2 @@
1
+ export { walk } from "./Adapter/Core.js";
2
+ export { Adaptation } from "./Types.js";
@@ -0,0 +1 @@
1
+ export { integration } from "./Harnessing/Core.js";
package/dist/esm/index.js CHANGED
@@ -4,10 +4,9 @@ function initialize(config) {
4
4
  var _ref;
5
5
  return instantiate(overlay({}, null != config ? config : {}), null != (_ref = null == config ? void 0 : config.stack) ? _ref : toStack()).instance;
6
6
  }
7
- export { Adaptation, Omitted } from "./Types.js";
7
+ export { Omitted } from "./Types.js";
8
8
  export { layer } from "./Random/index.js";
9
9
  export { FabricatorError } from "./Error/index.js";
10
- export { drive } from "./Adapter/Core.js";
11
10
  export { registry } from "./Schema/Registry.js";
12
11
  export { effectiveDiscrete, toBound } from "./Bound.js";
13
12
  export { initialize };
@@ -1,6 +1,5 @@
1
1
  export { Children, Kind, Meta, Produces } from "./Types.js";
2
2
  export { isPlainObject } from "./Utility/Core.js";
3
3
  export { plan, resolve } from "./Enumeration/Plan.js";
4
- export { defaultAlgorithm, encode, randomSeed, toRandomSource, toStream, toStreamFromTrace } from "./Random/index.js";
5
- export { directoryOf, normalizeLocation, relativize, resolveCallerFile } from "./Random/CallSite.js";
4
+ export { defaultAlgorithm, encode, randomSalt, toRandomSource, toStream, toStreamFromTrace } from "./Random/index.js";
6
5
  export { toStack as toSynchronousStack } from "./Instance/Stack/Sync.js";
@@ -55,7 +55,7 @@ export declare function mergeAdaptations<$Prior extends Adaptations, const $Adap
55
55
  * previous one into the argument. Splitting the two across packages is how they
56
56
  * drift.
57
57
  */
58
- export declare function drive<$Context, $Returnable>(adapter: Adapter<string, $Context, $Returnable>, schema: {
58
+ export declare function walk<$Context, $Returnable>(adapter: Adapter<string, $Context, $Returnable>, schema: {
59
59
  [Adaptation]?: Adaptations;
60
60
  }, context: $Context): $Returnable;
61
61
  export {};
@@ -70,7 +70,7 @@ export type Patch = {
70
70
  };
71
71
  /**
72
72
  * How an adapter recurses into a nested schema — handed to
73
- * {@link Adapter.convert} by `Adapter/Core.ts`'s `drive` rather than being the
73
+ * {@link Adapter.convert} by `Adapter/Core.ts`'s `walk` rather than being the
74
74
  * adapter's own private recursion, so every nested node goes back through the
75
75
  * adaptation lookup, not only the outermost one.
76
76
  *
@@ -1,33 +1,35 @@
1
- import type { Stack } from "../Instance/Types";
1
+ import type { Ancestry, Stack } from "../Instance/Types";
2
2
  import type { RandomSource } from "../Random/Types";
3
3
  import type { Enumerable, Limits } from "./Types";
4
4
  /**
5
5
  * Typed `combinatorial`/`coverage` boundary, closing over one instance's
6
6
  * `source` and its already-validated `limits` — same shape as
7
- * `Constructor(source, stack)`. No separate `clock`: `source` already carries
7
+ * `Constructor(source, stack, ancestry)`. No separate `clock`: `source` carries
8
8
  * its resolved clock (`Random/Types.ts`'s `Options.clock`), so `Constructor`'s
9
- * `toConstructionContext` reads it off whichever root a construction resolves
10
- * against. `plan`/ `resolve` (`./Plan.ts`) do the untyped recursive work; this
11
- * is the one precisely-typed layer, mirroring `Constructor.ts`'s `make`/
12
- * `construct` split.
9
+ * `toConstructionContext` reads it off the resolved `ConstructionTrace`.
10
+ * `plan`/ `resolve` (`./Plan.ts`) do the untyped recursive work; this is the
11
+ * one precisely-typed layer, mirroring `Constructor.ts`'s `make`/`construct`
12
+ * split.
13
13
  *
14
- * Two derived seeds — one per API — each an independent, deterministic fork off
15
- * the _effective_ source's seed (`effectiveSource()` below — the active `wrap`
16
- * frame's, or this instance's `source`; read fresh on every
14
+ * Two derived salts — one per API — each composed from the _effective_ source's
15
+ * salt (`effectiveSource()` below — the innermost visible `wrap` frame's, or
16
+ * this instance's `source`; read fresh on every
17
17
  * `combinatorial(...)`/`coverage(...)` call, not once when `enumerables()` was
18
- * built, so the same `combinatorial` reference behaves differently inside an
19
- * active `wrap`). `new Fabricator(schema, { seed })` (see `Constructor.ts`'s
20
- * `construct()`) forks a fully isolated `RandomSource` and opens an
21
- * `"unattributed"` scope, so `resolveCallerFile()` is never called the
22
- * lazy-generator attribution hazard a plain `function*` would invite simply
23
- * doesn't arise — the instance's construction counters are never touched, and
24
- * the same seed reproduces regardless of which file
25
- * `combinatorial(...)`/`coverage(...)` is called from. Each `fork()` starts
26
- * from a fresh source, so reusing one seed across many builds (different
27
- * schemas, or the same schema rebuilt per iteration) never lets one build's
28
- * draws leak into another's.
18
+ * built, so the same `combinatorial` reference behaves differently inside a
19
+ * `wrap` this instance can see). Each build pins that salt via `new
20
+ * Fabricator(schema, { salt })` (see `Constructor.ts`'s `construct()`) a pin,
21
+ * not a fork — so every rebuild of one schema draws from the same universe,
22
+ * distinct from anything built under the instance's own salt.
23
+ *
24
+ * `ordinal: null` is pinned alongside it, and is not incidental: a salt says
25
+ * nothing about ordering, so without this pin each lazy rebuild would take the
26
+ * next ordinal from the effective source's counter advancing it for every
27
+ * later construction, and giving each pass over the `Iterable` a different
28
+ * ordinal. A pinned ordinal is taken verbatim, so the counter is untouched,
29
+ * every iteration rebuilds from the same explicit identity, and the `null` can
30
+ * never coincide with a counted construction.
29
31
  */
30
- export declare function enumerables(source: RandomSource, limits: Limits, stack: Stack): {
32
+ export declare function enumerables(source: RandomSource, limits: Limits, stack: Stack, ancestry: Ancestry): {
31
33
  combinatorial: Enumerable;
32
34
  coverage: Enumerable;
33
35
  };
@@ -45,25 +45,6 @@ export declare namespace FabricatorError {
45
45
  */
46
46
  limit: unknown);
47
47
  }
48
- /**
49
- * `initialize({ attribution: { kind: "rooted", root } })` when `root` is not
50
- * an absolute path or a `file://` URL. A relative root can never prefix a
51
- * resolved caller file, so `relativize` would leave every file unchanged —
52
- * the option would look configured while silently doing nothing. Thrown
53
- * eagerly at `initialize()`, not deferred to wherever that would first become
54
- * observable.
55
- */
56
- class InvalidAttributionRootError extends FabricatorError {
57
- /**
58
- * The rejected root, as given.
59
- */
60
- readonly root: string;
61
- constructor(
62
- /**
63
- * The rejected root, as given.
64
- */
65
- root: string);
66
- }
67
48
  /**
68
49
  * A `self` placeholder resolved with no `T.recursive` expanding around it —
69
50
  * only reachable by holding a `self` reference outside the `T.recursive(...)`
@@ -556,4 +537,19 @@ export declare namespace FabricatorError {
556
537
  class SynchronousStackError extends FabricatorError {
557
538
  constructor();
558
539
  }
540
+ /**
541
+ * The `fabricator` provider of `integration(instance)`
542
+ * (`@ghostry/fabricator/harnessing`) was called outside that integration's
543
+ * own `around`.
544
+ *
545
+ * The provider hands back the per-test scope `around` just entered, so there
546
+ * is no scope for it to return anywhere else. `@ghostry/harness` never does
547
+ * this — it runs each provider inside its integration's `around` — so this
548
+ * means a composer that breaks the contract. Raised rather than returning the
549
+ * base instance, which would draw plausible data from the wrong configuration
550
+ * with no signal.
551
+ */
552
+ class HarnessingProviderError extends FabricatorError {
553
+ constructor();
554
+ }
559
555
  }
@@ -1,4 +1,4 @@
1
- import type { Stack } from "../Instance/Types";
1
+ import type { Ancestry, Stack } from "../Instance/Types";
2
2
  import type { ConstructorOptions, RandomSource } from "../Random/Types";
3
3
  import { type Buildable } from "../Types";
4
4
  import { type AsFabricator } from "./Types";
@@ -11,9 +11,9 @@ import { type AsFabricator } from "./Types";
11
11
  * `function` that explicitly returns an object, so `new construct(schema)`
12
12
  * behaves identically to `construct(schema)` — a `new` call's returned object
13
13
  * always replaces the freshly-created `this`. The construct signature lets
14
- * callers spell `new T.Fabricator(schema)` — or `new Fabricator(schema, { seed
15
- * })` to pin this one build to an explicit seed, independent of the file it's
16
- * constructed in (see `construct()` for what `options.seed` does).
14
+ * callers spell `new T.Fabricator(schema)` — or `new Fabricator(schema, { salt
15
+ * })` to pin this one build's salt slot, leaving every other slot to resolve as
16
+ * usual (see `construct()` for what `options.salt` does).
17
17
  */
18
18
  export type Constructor = {
19
19
  new <const $Schema extends Buildable>(schema: $Schema, options?: ConstructorOptions): AsFabricator<$Schema>;
@@ -23,19 +23,22 @@ export type Constructor = {
23
23
  * internal `convert(schema: any)` — and the precisely-typed `construct()`
24
24
  * boundary around it, both closed over a single instance's `source` so every
25
25
  * fabricator this `construct()` produces draws from that instance's own
26
- * seed/streams and never another instance's.
26
+ * salt/streams and never another instance's.
27
27
  *
28
- * `stack` is the instance's own lineage-wide ambient stack
29
- * (`Instance/Core.ts`'s `toStack()`) — passed straight through to
28
+ * `stack` is the ambient carrier and `ancestry` is this instance's position in
29
+ * its lineage (`Instance/Core.ts`) — both passed straight through to
30
30
  * `resolveScope` on every `construct()` call, never read here directly, so a
31
- * build reached inside an active `wrap` resolves against that frame
32
- * automatically, with nothing threaded through by the caller.
31
+ * build reached inside a `wrap` this instance can see resolves against that
32
+ * frame automatically, with nothing threaded through by the caller. `ancestry`
33
+ * is what decides "can see": a frame entered on a sibling instance is not one
34
+ * this `construct()` will ever resolve against.
33
35
  *
34
36
  * No separate `clock` parameter: `source` already carries its own resolved
35
37
  * clock intrinsically (`Random/Types.ts`'s `Options.clock`, baked in when the
36
38
  * source was built), and `resolveScope`'s chosen source — the active `wrap`
37
39
  * frame's, or this one — is exactly the source whose clock a construction
38
40
  * should resolve "now" against. `toConstructionContext` reads it straight off
39
- * the resolved root rather than threading a second value alongside `source`.
41
+ * the resolved construction trace rather than threading a second value
42
+ * alongside `source`.
40
43
  */
41
- export declare function Constructor(source: RandomSource, stack: Stack): Constructor;
44
+ export declare function Constructor(source: RandomSource, stack: Stack, ancestry: Ancestry): Constructor;
@@ -14,19 +14,20 @@ import type { PlainObject } from "../Utility/Types";
14
14
  * `toTrace` records this node's {@link Trace} — a plain object literal, no
15
15
  * hashing. Hashing is paid only where a kind actually calls
16
16
  * `toStreamFromTrace(algorithm, trace)`. Bound once in `construct()` to this
17
- * one construction's already-resolved `RandomSource`/ `ConstructionTrace` pair
17
+ * one construction's already-resolved `RandomSource`/`ConstructionTrace` pair
18
18
  * (see `Constructor.ts`'s `resolveScope`) — every leaf calls `toTrace` with
19
19
  * only its own structural `path` and kind, never re-resolving the
20
- * construction's root itself. `T.recursive` is the one kind that rebinds
21
- * `toTrace`: each lazy expansion opens its own scope on the node's own private
22
- * forked `RandomSource` (see `recursive/Fabricator.ts`), so a data-dependent
23
- * expansion count can never perturb, or be perturbed by, anything else built
24
- * from the same `initialize()` instance — `RandomSource.fork`
25
- * (`Random/Types.ts`) is the isolation primitive.
20
+ * construction-owned slots itself. `T.recursive` is the one kind that rebinds
21
+ * `toTrace`: each lazy expansion resolves its construction trace on the node's
22
+ * own private forked `RandomSource` (see `recursive/Fabricator.ts`), so a
23
+ * data-dependent expansion count can never perturb, or be perturbed by,
24
+ * anything else built from the same `initialize()` instance —
25
+ * `RandomSource.fork` (`Random/Types.ts`) is the isolation primitive.
26
26
  *
27
27
  * `algorithm` rather than the `RandomSource` itself: stream derivation depends
28
- * on no per-source state, and a leaf has no business with `toRoot`/`fork`.
29
- * `clock` is not a field of its own — it is always `trace.clock`.
28
+ * on no per-source state, and a leaf has no business with
29
+ * `toConstructionTrace`/`fork`. `clock` is not a field of its own — it is
30
+ * always `trace.clock`.
30
31
  *
31
32
  * `self` is what makes `case "recursive.self"` resolve to "recurse one level
32
33
  * deeper, right now" — absent outside any active recursion, which is how `case
@@ -47,11 +48,11 @@ export type ConstructionContext = {
47
48
  * `toStreamFromTrace(algorithm, trace)`. The guard is the call site's own `if
48
49
  * (meta.produce)` branch (or the equivalent drawing path), not an unevaluated
49
50
  * closure. `algorithm` rather than the `RandomSource`: derivation depends on no
50
- * per-source state, and a leaf has no business with `toRoot`/`fork`. No
51
- * `clock`: it is `trace.clock`, always. A kind-specific extra — an array's
52
- * `element`, an object's `fields`, a choice's `weightings` — still follows as
53
- * its own trailing parameter: those vary per kind and were never part of the
54
- * shared prefix this replaces.
51
+ * per-source state, and a leaf has no business with
52
+ * `toConstructionTrace`/`fork`. No `clock`: it is `trace.clock`, always. A
53
+ * kind-specific extra — an array's `element`, an object's `fields`, a choice's
54
+ * `weightings` — still follows as its own trailing parameter: those vary per
55
+ * kind and were never part of the shared prefix this replaces.
55
56
  */
56
57
  export type FabricatorContext<$Schema> = {
57
58
  schema: $Schema;
@@ -0,0 +1,43 @@
1
+ import type { Instance } from "../Instance/Types";
2
+ import type { PlainObject } from "../Utility/Types";
3
+ import type { FabricatorTestContext, Integration } from "./Types";
4
+ /**
5
+ * Decorate an existing `Instance` as a `@ghostry/harness` integration.
6
+ * `around(identity, body)` is `instance.wrap({ salt: layer(saltFor(identity))
7
+ * })` — one line of real work. Construction ordinals therefore restart per test
8
+ * (each `wrap` re-instantiates), which is what makes `.only`, filters, shards,
9
+ * and `.concurrent` unable to shift a neighbor's data. That per-test
10
+ * partitioning is also why the salt needs no file in it; see `saltFor`
11
+ * (`Harnessing/Salt.ts`).
12
+ *
13
+ * `around`, not `setup`: the ambient frame has to enclose the body, and only
14
+ * `around` does. Its `finally` running at the call boundary rather than at test
15
+ * settlement costs nothing here — there is no teardown, and the
16
+ * `AsyncLocalStorage` carrier keeps the frame alive across the body's `await`s
17
+ * regardless of when `wrap` returns. On the synchronous carrier an async body
18
+ * still raises `SynchronousStackError` from `wrap`. Declaring no `setup` also
19
+ * keeps `@ghostry/harness` on its uninstrumented path, where a synchronous
20
+ * assertion failure is reported at the user's own line.
21
+ *
22
+ * `provides.fabricator` hands back the scope `wrap` gave its block, not the
23
+ * base instance, so `context.fabricator.salt` is the per-test salt and
24
+ * `.fork()` forks from the test's configuration. The two calls meet through
25
+ * `scope`: set just before `body()`, read by the provider, restored in a
26
+ * `finally`. That is safe under `.concurrent` because `@ghostry/harness` runs a
27
+ * provider synchronously inside its own integration's `around` — no other test
28
+ * can enter between the write and the read, and the context object already
29
+ * holds the scope by the body's first `await`. Restoring rather than clearing
30
+ * keeps a nested `around` from dropping its parent's scope. A provider reached
31
+ * with no scope is a composer breaking that contract, and raises
32
+ * `HarnessingProviderError` rather than quietly providing the base instance.
33
+ *
34
+ * Nothing here reads `instance.context`. The integration is a pure function of
35
+ * the `Identity` it is handed and the instance it decorates, and never sets
36
+ * `clock`. A pinned `Date` (the recommended setup) and the wall-clock default
37
+ * are already concrete numbers by the time `overlay()` sees them, so they
38
+ * inherit through every `wrap` unchanged: salt varies per test, "now" does not.
39
+ * `clock: "derived"` is left alone on purpose — that policy's documented
40
+ * meaning is that the salt _is_ the reproducibility unit, so per-test clocks
41
+ * are the request honored, not a bug to override.
42
+ */
43
+ export declare function integration<$Registry extends PlainObject>(instance: Instance<$Registry>): Integration<FabricatorTestContext<$Registry>>;
@@ -0,0 +1,31 @@
1
+ import type { Identity } from "./Types";
2
+ /**
3
+ * The salt `integration` layers onto the instance for one `Identity`.
4
+ *
5
+ * [ <kind>, ...<describe names>, <name>, <row?> ]
6
+ *
7
+ * **No file, deliberately.** Every test body runs inside its own
8
+ * `instance.wrap`, which builds a fresh `RandomSource` with a fresh
9
+ * construction counter, so constructions are already partitioned per test.
10
+ * Identity comes from a path, never from an execution counter or a location
11
+ * that moves when a file does.
12
+ *
13
+ * The accepted cost: two tests with the same `kind`, describe path, and name in
14
+ * _different files_ share a salt and therefore draw the same data. Each is
15
+ * still deterministic. That is the same trade already made in dropping
16
+ * duplicate-path detection, and the fix is the ordinary one — give them
17
+ * distinguishable names.
18
+ *
19
+ * `kind` leads. Without it, an empty-named test collides with its enclosing
20
+ * suite scope: `path` never carries a leaf's own name, only its describes, and
21
+ * `name` is `""` for both a `"suite"` identity (by design — see `Identity`) and
22
+ * any test a user happens to name `""`, so at the same `path` the two arrays
23
+ * would otherwise be identical. `kind` does not disambiguate two suite
24
+ * identities that share a `path` (a `beforeAll` and an `afterAll` in one
25
+ * `describe`, say) — that collision is intentional, a suite's setup and
26
+ * teardown sharing one deterministic scope, and a caller who wants otherwise
27
+ * already has `instance.wrap(...)` inside the hook body.
28
+ *
29
+ * The row is appended only for `.each`.
30
+ */
31
+ export declare function saltFor(identity: Identity): ReadonlyArray<string>;
@@ -0,0 +1,79 @@
1
+ import type { Instance } from "../Instance/Types";
2
+ import type { PlainObject } from "../Utility/Types";
3
+ /**
4
+ * What identifies one registered test or suite — the material a salt is derived
5
+ * from. Named to match how this codebase already talks about the concept:
6
+ * fabricator's own reproducibility guide reaches for `layer(...)` for "a tenant
7
+ * id, a test's own name" — this is that identity, structured.
8
+ *
9
+ * Declared here rather than imported from `@ghostry/harness`, so neither
10
+ * package depends on the other — the same arrangement `Adapter`/`walk` already
11
+ * uses for schema adapters. Satisfied structurally.
12
+ *
13
+ * **Carries no file, deliberately.** Nothing here identifies where the test was
14
+ * registered, which is what lets `@ghostry/harness` skip stack walking entirely
15
+ * — see `saltFor` (`Harnessing/Salt.ts`) for why the file turned out to be
16
+ * redundant against per-test `wrap` partitioning. The consequence for the
17
+ * contract: an integration that genuinely needs the registering file cannot get
18
+ * it from here, and adding it back means solving the frame-skip problem for
19
+ * every wrapper between the user's `it(...)` and the integration. Worth
20
+ * revisiting only against a real requirement, not speculatively.
21
+ */
22
+ export type Identity = {
23
+ /**
24
+ * Disambiguates an empty-named test from its enclosing suite scope — `name`
25
+ * is `""` for both, and `path` never carries a leaf's own name. Two suite
26
+ * identities that share a `path` (`beforeAll` and `afterAll` in one
27
+ * `describe`) are not disambiguated by this: that collision is intentional
28
+ * (see `saltFor`, `Harnessing/Salt.ts`).
29
+ */
30
+ readonly kind: "test" | "suite";
31
+ /** Enclosing describe names, outer → inner. */
32
+ readonly path: ReadonlyArray<string>;
33
+ /** The test name; empty for a suite-scoped callback. */
34
+ readonly name: string;
35
+ /** `.each` row index (0-based), `undefined` when the test is not from `.each`. */
36
+ readonly row: number | undefined;
37
+ };
38
+ /**
39
+ * One context key's value, as a function of the test's `Identity` rather than a
40
+ * fixed value.
41
+ */
42
+ export type Provider<$Value> = (identity: Identity) => $Value;
43
+ /**
44
+ * The keys an integration contributes, and how each is produced. Homomorphic
45
+ * over `$Context`, so the context an integration contributes is read back out
46
+ * of this object's shape with no separate key declaration to keep in sync.
47
+ */
48
+ export type Provides<$Context extends object> = {
49
+ readonly [$Key in keyof $Context]: Provider<$Context[$Key]>;
50
+ };
51
+ /**
52
+ * What `integration(instance)` is, as `@ghostry/harness`'s `initialize` sees
53
+ * it. The subset of that package's contract this integration actually uses, not
54
+ * a copy of all of it: `@ghostry/harness` also accepts an optional `setup`,
55
+ * which fabricator has no teardown to put in, and an object lacking an optional
56
+ * member still satisfies the contract structurally. `around` is required here,
57
+ * though optional there, because this integration always declares it.
58
+ *
59
+ * `provides` is the _only_ source of context keys; `initialize` rejects a
60
+ * collision across integrations by reading `Object.keys(provides)`. Each
61
+ * provider runs inside its integration's `around`. `around` is generic in its
62
+ * return and must return the body's value unchanged: that is what makes async
63
+ * work and what lets frames nest.
64
+ */
65
+ export type Integration<$Context extends object> = {
66
+ readonly name: string;
67
+ readonly provides: Provides<$Context>;
68
+ around<$Return>(identity: Identity, body: () => $Return): $Return;
69
+ };
70
+ /**
71
+ * The slice of the test context `integration(instance)` contributes — one key,
72
+ * `fabricator`, holding the per-test scoped `Instance`. The same instance is
73
+ * also ambient for the body's duration (`instance.wrap`), so a body that
74
+ * ignores this and uses the caller's own `instance` still sees the per-test
75
+ * salt.
76
+ */
77
+ export type FabricatorTestContext<$Registry extends PlainObject = PlainObject> = {
78
+ readonly fabricator: Instance<$Registry>;
79
+ };