@ghostry/fabricator 0.0.4 → 0.0.5

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.
@@ -201,7 +201,7 @@ class Error_FabricatorError extends Error {
201
201
  constructor(){
202
202
  super();
203
203
  this.name = "HarnessingProviderError";
204
- this.message = "The `fabricator` provider was called outside its integration's `around`, so there is no per-test scope to provide. A composer must run each integration's providers inside that integration's `around` frame, as `@ghostry/harness` does.";
204
+ this.message = "The `fabricator` provider was called outside its integration's frame, so there is no per-test scope to provide. A composer must run each integration's providers inside the frame it opened, passing forward what that frame's wrapper established, as `@ghostry/harness` does.";
205
205
  }
206
206
  }
207
207
  FabricatorError.HarnessingProviderError = HarnessingProviderError;
@@ -2,28 +2,19 @@ import { FabricatorError } from "../Error/index.js";
2
2
  import { layer as index_js_layer } from "../Random/index.js";
3
3
  import { saltFor } from "./Salt.js";
4
4
  function integration(instance) {
5
- let scope;
6
5
  return {
7
6
  name: "@ghostry/fabricator",
8
7
  provides: {
9
- fabricator: ()=>{
10
- if (void 0 === scope) throw new FabricatorError.HarnessingProviderError();
11
- return scope;
8
+ fabricator: ({ established })=>{
9
+ if (void 0 === established) throw new FabricatorError.HarnessingProviderError();
10
+ return established;
12
11
  }
13
12
  },
14
- around (identity, body) {
13
+ *frame ({ identity }) {
15
14
  const salt = index_js_layer(saltFor(identity));
16
- return instance.wrap({
17
- salt
18
- }, (entered)=>{
19
- const previous = scope;
20
- scope = entered;
21
- try {
22
- return body();
23
- } finally{
24
- scope = previous;
25
- }
26
- });
15
+ yield (body)=>instance.wrap({
16
+ salt
17
+ }, body);
27
18
  }
28
19
  };
29
20
  }
@@ -7,7 +7,7 @@ import type { PlainObject } from "../Utility/Types";
7
7
  * is the cartesian product — width is the product of every child's width;
8
8
  * `at(index)` mixed-radix decodes to a full combination (`combinatorial`).
9
9
  * `"cycle"` (`coverage`) takes the widest child as the composite's width,
10
- * cycling narrower children to fill it — see CLAUDE.md's "sum vs product" note
10
+ * cycling narrower children to fill it — see AGENTS.md's "sum vs product" note
11
11
  * for why cycling only ever applies to `object`/`tuple` and never to a sum node
12
12
  * (`choice`, the presence wrappers), which always total their children's widths
13
13
  * regardless of strategy.
@@ -17,7 +17,7 @@ export type Strategy = "product" | "cycle";
17
17
  * A fresh, reproducible permutation of `0..width-1` each call — one per
18
18
  * width-`>1` node `plan()` visits, in walk order, so two nodes of the same
19
19
  * width never receive the same permutation (which would otherwise iterate them
20
- * in lockstep — see CLAUDE.md's note on why a constant phase offset isn't
20
+ * in lockstep — see AGENTS.md's note on why a constant phase offset isn't
21
21
  * enough). Only consulted under `"cycle"`; `"product"`'s mixed-radix decode
22
22
  * already visits every combination, so permuting there would only reorder
23
23
  * identical output. Built in `Enumerate.ts` (which can reach `Random/`), then
@@ -540,14 +540,15 @@ export declare namespace FabricatorError {
540
540
  /**
541
541
  * The `fabricator` provider of `integration(instance)`
542
542
  * (`@ghostry/fabricator/harnessing`) was called outside that integration's
543
- * own `around`.
543
+ * own frame.
544
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.
545
+ * The provider hands back the per-test scope the frame's wrapper established,
546
+ * which reaches it as the provider's second argument, so there is no scope
547
+ * for it to return anywhere else. `@ghostry/harness` never does this it
548
+ * runs each provider inside the frame it opened and passes that scope forward
549
+ * so this means a composer that breaks the contract. Raised rather than
550
+ * returning the base instance, which would draw plausible data from the wrong
551
+ * configuration with no signal.
551
552
  */
552
553
  class HarnessingProviderError extends FabricatorError {
553
554
  constructor();
@@ -2,33 +2,33 @@ import type { Instance } from "../Instance/Types";
2
2
  import type { PlainObject } from "../Utility/Types";
3
3
  import type { FabricatorTestContext, Integration } from "./Types";
4
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`
5
+ * Decorate an existing `Instance` as a `@ghostry/harness` integration. The
6
+ * whole of it is `instance.wrap({ salt: layer(saltFor(identity)) })` — one line
7
+ * of real work. Construction ordinals therefore restart per test (each `wrap`
8
+ * re-instantiates), which is what makes `.only`, filters, shards, and
9
+ * `.concurrent` unable to shift a neighbor's data. That per-test partitioning
10
+ * is also why the salt needs no file in it; see `saltFor`
11
11
  * (`Harnessing/Salt.ts`).
12
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.
13
+ * `frame` is a generator so that `yield` can be both where the body runs and
14
+ * where this hook waits. There is nothing after the `yield` here: fabricator
15
+ * has no teardown, and the `AsyncLocalStorage` carrier keeps the ambient frame
16
+ * alive across the body's `await`s on its own. What the `yield` carries is the
17
+ * wrapper, because the ambient frame has to _enclose_ the body rather than
18
+ * merely precede it. On the synchronous carrier an async body still raises
19
+ * `SynchronousStackError` from `wrap`.
21
20
  *
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
21
+ * `instance.wrap` is handed to the wrapper directly rather than called inside a
22
+ * closure. `wrap(overlay, block)` already takes `(scope) => $Return`, which is
23
+ * exactly the wrapper's own `(established) => $Return`, so the scope `wrap`
24
+ * opens _is_ the established value with nothing in between to adapt it.
25
+ *
26
+ * `provides.fabricator` is then that scope the instance `wrap` gave its
27
+ * block, not the base instance so `context.fabricator.salt` is the per-test
28
+ * salt and `.fork()` forks from the test's configuration. Nothing mediates
29
+ * between the two: the wrapper hands the scope forward and the provider
30
+ * receives it. A provider reached with no established scope is a composer
31
+ * running providers outside the frame it opened, and raises
32
32
  * `HarnessingProviderError` rather than quietly providing the base instance.
33
33
  *
34
34
  * Nothing here reads `instance.context`. The integration is a pure function of
@@ -40,4 +40,4 @@ import type { FabricatorTestContext, Integration } from "./Types";
40
40
  * meaning is that the salt _is_ the reproducibility unit, so per-test clocks
41
41
  * are the request honored, not a bug to override.
42
42
  */
43
- export declare function integration<$Registry extends PlainObject>(instance: Instance<$Registry>): Integration<FabricatorTestContext<$Registry>>;
43
+ export declare function integration<$Registry extends PlainObject>(instance: Instance<$Registry>): Integration<FabricatorTestContext<$Registry>, Instance<$Registry>>;
@@ -38,34 +38,77 @@ export type Identity = {
38
38
  /**
39
39
  * One context key's value, as a function of the test's `Identity` rather than a
40
40
  * fixed value.
41
+ *
42
+ * `established` is whatever this integration's own wrapper handed forward. For
43
+ * this integration that is the scoped `Instance` `wrap` opened, which is the
44
+ * whole of what `provides.fabricator` returns — so the value travels from the
45
+ * wrapper to the provider directly, with no mutable slot written on the way in
46
+ * and read on the way out.
47
+ */
48
+ export type Provider<$Value, $Established = void> = (args: ProviderArgs<$Established>) => $Value;
49
+ /**
50
+ * What `@ghostry/harness` hands {@link Integration.frame}: one object, never
51
+ * positional arguments, so a field added to the contract later is a key an
52
+ * existing hook ignores rather than a parameter it has to thread past.
41
53
  */
42
- export type Provider<$Value> = (identity: Identity) => $Value;
54
+ export type FrameArgs = {
55
+ readonly identity: Identity;
56
+ };
57
+ /**
58
+ * What it hands each provider: everything {@link FrameArgs} carries, plus what
59
+ * this integration's own wrapper established — for this integration, the scoped
60
+ * `Instance` that `wrap` opened.
61
+ */
62
+ export type ProviderArgs<$Established = void> = FrameArgs & {
63
+ readonly established: $Established;
64
+ };
43
65
  /**
44
66
  * The keys an integration contributes, and how each is produced. Homomorphic
45
67
  * over `$Context`, so the context an integration contributes is read back out
46
68
  * of this object's shape with no separate key declaration to keep in sync.
47
69
  */
48
- export type Provides<$Context extends object> = {
49
- readonly [$Key in keyof $Context]: Provider<$Context[$Key]>;
70
+ export type Provides<$Context extends object, $Established = void> = {
71
+ readonly [$Key in keyof $Context]: Provider<$Context[$Key], $Established>;
50
72
  };
73
+ /**
74
+ * How an integration runs the body when the body must run _inside_ something.
75
+ * Here that is fabricator's own `wrap`, whose block parameter is already this
76
+ * shape — the scoped `Instance` it opens is what reaches `body`, and therefore
77
+ * what reaches the providers.
78
+ *
79
+ * Generic in its return and must hand the body's value back unchanged: that is
80
+ * what keeps a synchronous test synchronous and what lets frames nest.
81
+ */
82
+ export type Wrapper<$Established = void> = <$Return>(body: (established: $Established) => $Return) => $Return;
83
+ /**
84
+ * What {@link Integration.frame} returns: a generator with **one** suspension
85
+ * point. Everything before the `yield` is setup, the body runs at the `yield`,
86
+ * and everything after it is teardown, resumed when the body _settles_.
87
+ *
88
+ * `@ghostry/harness` accepts an `AsyncGenerator` here too. This declares only
89
+ * the synchronous half, because that is the half this integration uses and the
90
+ * narrower type still satisfies the wider one — the same reason `frame` is
91
+ * required below though it is optional there. Opening a fabricator scope is
92
+ * synchronous, and declaring the async arm would invite an integration that
93
+ * promotes every test in the suite to a promise for no reason.
94
+ */
95
+ export type Frame<$Established = void> = Generator<Wrapper<$Established> | void, void, unknown>;
51
96
  /**
52
97
  * What `integration(instance)` is, as `@ghostry/harness`'s `initialize` sees
53
98
  * 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.
99
+ * a copy of all of it: an object lacking an optional member still satisfies the
100
+ * contract structurally, so `frame` is required here though optional there,
101
+ * because this integration always declares it.
58
102
  *
59
103
  * `provides` is the _only_ source of context keys; `initialize` rejects a
60
104
  * 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.
105
+ * provider runs _inside_ that frame and receives the same `$Established` the
106
+ * wrapper handed to the body.
64
107
  */
65
- export type Integration<$Context extends object> = {
108
+ export type Integration<$Context extends object, $Established = void> = {
66
109
  readonly name: string;
67
- readonly provides: Provides<$Context>;
68
- around<$Return>(identity: Identity, body: () => $Return): $Return;
110
+ readonly provides: Provides<$Context, $Established>;
111
+ frame(args: FrameArgs): Frame<$Established>;
69
112
  };
70
113
  /**
71
114
  * The slice of the test context `integration(instance)` contributes — one key,
@@ -26,6 +26,6 @@ export type Fabricator<$Schema extends {
26
26
  * shared across every element. The weighted pick happens on this field's
27
27
  * private stream; only the chosen option's `fabricate()` is called, so an
28
28
  * unpicked option never advances its stream (safe for the same reason skipping
29
- * `object.omittable`'s inner draw is — see CLAUDE.md's "Randomness").
29
+ * `object.omittable`'s inner draw is — see AGENTS.md's "Randomness").
30
30
  */
31
31
  export declare function Fabricator<$Items extends Items>(context: FabricatorContext<Schema<$Items>>, weightings: ReadonlyArray<readonly [number, NaiveFabricator<any>]>): Fabricator<Schema<$Items>>;
@@ -24,7 +24,7 @@ export type Fabricator<$Schema extends {
24
24
  * `source` is already dispatched into its own independent stream by
25
25
  * `Constructor.ts` regardless of this roll, skipping its draw here can never
26
26
  * perturb any other field's reproducibility (see
27
- * `object/omittable/Fabricator.ts`, and CLAUDE.md's "Randomness"). 50/50 by
27
+ * `object/omittable/Fabricator.ts`, and AGENTS.md's "Randomness"). 50/50 by
28
28
  * default; `.weighted(...)` (`Schema.ts`) reweights either outcome relative to
29
29
  * that same default of `1`.
30
30
  */
@@ -24,12 +24,12 @@ export type Fabricator<$Schema extends {
24
24
  * by default; not achievable by composing `nullable(T.undefinable(inner))`'s
25
25
  * two independent 50/50 rolls (that is 50/25/25, not 33/33/33), hence the
26
26
  * dedicated three-way `weighted()` here — see `object/optional/Fabricator.ts`
27
- * and CLAUDE.md's "Why `T.optional` isn't `omittable(undefinable(inner))`".
27
+ * and AGENTS.md's "Why `T.optional` isn't `omittable(undefinable(inner))`".
28
28
  * `.weighted(...)` (`Schema.ts`) reweights individual outcomes relative to that
29
29
  * same default of `1`; an unspecified outcome keeps it. Skipping `source`'s
30
30
  * draw on the two non-"value" outcomes is safe for the same reason as
31
31
  * `object/omittable/Fabricator.ts`: `source` already has its own independent
32
- * stream, minted at build time regardless of this roll (see CLAUDE.md's
32
+ * stream, minted at build time regardless of this roll (see AGENTS.md's
33
33
  * "Randomness").
34
34
  */
35
35
  export declare function Fabricator<$Definition extends Definition>(context: FabricatorContext<Schema<$Definition>>, source: BaseFabricator<any>): Fabricator<Schema<$Definition>>;
@@ -32,12 +32,12 @@ export declare function isObjectOptionalFabricator(candidate: BaseFabricator<unk
32
32
  * Uniform (1/3 each) by default; not achievable by composing
33
33
  * `object.omittable(T.undefinable(inner))`'s two independent 50/50 rolls (that
34
34
  * is 50/25/25, not 33/33/33), hence the dedicated three-way `weighted()` here —
35
- * see CLAUDE.md's "Why `T.optional` isn't `omittable(undefinable(inner))`"
35
+ * see AGENTS.md's "Why `T.optional` isn't `omittable(undefinable(inner))`"
36
36
  * under "Compound / field-only kinds". `.weighted(...)` (`Schema.ts`) reweights
37
37
  * individual outcomes relative to that same default of `1`; an unspecified
38
38
  * outcome keeps it. Skipping `source`'s draw on the two non-"value" outcomes is
39
39
  * safe for the same reason as `object/omittable/Fabricator.ts`: `source`
40
40
  * already has its own independent stream, minted at build time regardless of
41
- * this roll (see CLAUDE.md's "Randomness").
41
+ * this roll (see AGENTS.md's "Randomness").
42
42
  */
43
43
  export declare function Fabricator<$Definition extends Definition>(context: FabricatorContext<Schema<$Definition>>, source: BaseFabricator<any>): Fabricator<Schema<$Definition>>;
@@ -6,7 +6,7 @@ import { Produces, type Adaptation, type Kind, type Meta } from "../../Types";
6
6
  * `TS2456` ("`RecursiveValue` circularly references itself") — legal _only_
7
7
  * because `ValueOf`'s second argument is read through an interface member
8
8
  * (`this["bindings"]` on every composite `Core`), which TypeScript defers. Do
9
- * not rewrite as a conditional; that reintroduces the error. See `CLAUDE.md`'s
9
+ * not rewrite as a conditional; that reintroduces the error. See `AGENTS.md`'s
10
10
  * "`ValueOf`'s `$Bindings`".
11
11
  *
12
12
  * Wherever `self` sits in `$Body` (nested through `array`/`object`/`tuple`/
@@ -15,7 +15,7 @@ export type Fabricated<$Bindings extends unknown[] = []> = $Bindings[0];
15
15
  export type Meta = Record<string, never>;
16
16
  /**
17
17
  * An `interface`, not a `type` alias, so `this["bindings"]` resolves — the
18
- * entire mechanism this kind exists for. See `CLAUDE.md`'s "`ValueOf`'s
18
+ * entire mechanism this kind exists for. See `AGENTS.md`'s "`ValueOf`'s
19
19
  * `$Bindings`" section for why threading requires this.
20
20
  */
21
21
  export interface Core<$Adaptations extends Adaptations = {}> {
@@ -24,7 +24,7 @@ export type Fabricator<$Schema extends {
24
24
  * since `source` is already dispatched into its own independent stream by
25
25
  * `Constructor.ts` regardless of this roll, skipping its draw here can never
26
26
  * perturb any other field's reproducibility (see
27
- * `object/omittable/Fabricator.ts`, and CLAUDE.md's "Randomness"). 50/50 by
27
+ * `object/omittable/Fabricator.ts`, and AGENTS.md's "Randomness"). 50/50 by
28
28
  * default; `.weighted(...)` (`Schema.ts`) reweights either outcome relative to
29
29
  * that same default of `1`.
30
30
  */
@@ -11,20 +11,32 @@
11
11
  * package's contract this integration uses, satisfied structurally, so neither
12
12
  * depends on the other.
13
13
  *
14
+ * **Mirrors the integration contract of `@ghostry/harness` 0.0.4.** Structural
15
+ * satisfaction is what keeps the two packages independent, and it is also what
16
+ * leaves nothing to check the pairing at install time: neither manifest names
17
+ * the other, so a mismatched pair is caught by `tsc` at the point a consumer
18
+ * passes `integration(instance)` to `initialize`, with an error about shapes
19
+ * rather than about versions. This line is the only record of which version the
20
+ * copy tracks, so move it whenever that contract does — the types below are
21
+ * where the drift actually lives.
22
+ *
14
23
  * @module
15
24
  */
16
25
  /**
17
26
  * Decorate an existing `Instance` as a `@ghostry/harness` integration — `{
18
- * name, provides, around }`. Takes an instance rather than minting one: the
27
+ * name, provides, frame }`. Takes an instance rather than minting one: the
19
28
  * caller owns `initialize(...)` (and in particular the suite-wide `clock`), and
20
29
  * this only wraps each test body in that instance's per-identity `wrap`.
21
30
  */
22
31
  export { integration } from "./Harnessing/Core";
23
32
  /**
24
33
  * `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.
34
+ * the `{ name, provides, frame }` shape `integration(instance)` returns —
35
+ * `provides` is a `Provides<$Context, $Established>`, one `Provider` per
36
+ * context key, each handed what the frame's wrapper established; `Frame` is the
37
+ * generator `frame` returns and `Wrapper` the optional value it yields; both
38
+ * hooks take a single object — `FrameArgs` for `frame`, and `ProviderArgs`, the
39
+ * same thing plus `established`, for a provider; `FabricatorTestContext` is the
40
+ * `{ fabricator }` slice of the test context this integration contributes.
29
41
  */
30
- export type { FabricatorTestContext, Identity, Integration, Provider, Provides, } from "./Harnessing/Types";
42
+ export type { FabricatorTestContext, Frame, FrameArgs, Identity, Integration, Provider, ProviderArgs, Provides, Wrapper, } from "./Harnessing/Types";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghostry/fabricator",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "license": "MIT",
5
5
  "description": "Fabricate typed data from composable schemas.",
6
6
  "keywords": [
@@ -64,6 +64,9 @@
64
64
  "access": "public",
65
65
  "provenance": true
66
66
  },
67
+ "devDependencies": {
68
+ "@ghostry/harness": "^0.0.4"
69
+ },
67
70
  "scripts": {
68
71
  "build:reset": "rm -rf ./dist",
69
72
  "build:compile": "bunx --no-install --bun rslib build --config ./rslib.config.ts",