/** * @module world/engine * The `WorldImpl` class — the central context for one data-generation session. * * Schemas are the primary anchor. Register schemas with `withSchema`, then * generate data with `generate`. All generation is deterministic given the seed. * * ## Registration modes * * Primary: `world.withSchema(Schema, { matchers })` * Relational: `world.withSchema(Schema, { relations: { name: OtherSchema }, matchers })` * Derived: `world.withSchema(Schema, { from: SourceSchema, matchers })` * * ## Generation pipeline (per field) * * The executable contract is the `PIPELINE` list in `src/pipeline.ts`. For every * field of a registered schema, the engine walks these seven steps in order; the * first step that produces a value wins. * * 0. **Eager overrides** — `options.overrides` primitive/array entries land in * `ctx.current` so sibling matchers can read them via `ctx.current.`. * 1. **Matchers** — user functions from `withSchema({ matchers })`. Explicit * per-field functions; first to win. * 2. **Per-schema key map** — entries from `withKeyMap({ ... })` matched on the * field name. * 3. **Unwrap optional** — strip `optional`/`nullable`/`default` and roll absent * per layer; sets `ctx.inner` for downstream steps. Internal — does not * produce a final value on its own. * 4. **World-level custom generators** — entries from `withGenerators({ ... })` * matched on the field name. * 5. **Key-based heuristics** — built-in `DEFAULT_KEY_MAP` exact-key + * `DEFAULT_KEY_PATTERNS` regex matches (`email` → realistic email, * `firstName` → first name, `createdAt` → date). * 6. **Schema-based fallback** — Zod type introspection (`z.enum([...])` → * random member, `z.number().int().min(1).max(100)` → integer in range, * etc.). Always resolves. * * After the pipeline returns, two wrapping passes finish the record: * * - **Override deep-merge** — `options.overrides` is deep-merged onto the * pipeline's value (covers nested-object slices step 0 didn't eagerly * consume; B12 contract). * - **Transform** — `options.transform` is called on the merged value. * * ## File layout (B28) * * This module owns the `WorldImpl` class, its constructor, its public methods, * and the B39 module-global stable schema identity machinery. Pure helpers * are split out: * - `./registration.js` — `SchemaReg`, `EMPTY_REG`, `normalizeRelationEntry`, * `findPrimaryRegs`, `findDerivedRegs`, `resolveMode`. * - `./derived.js` — derived-upsert map access helpers (B8). * - `./relations.js` — relation cache-key / fork-key / error-message helpers, * and the `RelationResolver` collaborator (B62) that owns * `resolveRelated` / `resolveRelatedMany` / `resolveRelationPool` / * `ensurePrimaryRecord`. `WorldImpl` calls via `this.relations.X(...)`. */ import { z } from "zod"; import type { ZodTypeAny, input } from "zod"; import type { World, WorldOptions, GenerateOptions, Registry, GeneratorContext, KeyGenerator, SchemaKeyMap, SchemaOpts, ExplainResult } from "../types.js"; import type { WorldTrace } from "../trace.js"; import { createPrng } from "../prng.js"; import { type DerivedUpsertMap } from "./derived.js"; export declare class WorldImpl implements World { private readonly options; readonly prng: ReturnType; readonly registry: Registry; private readonly schemaRegs; /** * B39 — round-robin index for the derived-without-source pair picker * (`generateSingleItem`'s derived-fallback branch). Was historically named * `generationCounter` and used for PRNG fork keys too; under B39 those * fork keys moved to per-schema slots (`schemaCallCounts`), leaving this * counter with one remaining consumer: `pairs[idx]` at the derived-without- * source path. Incremented at the top of `generateSingleItem` and rolled * back by the B8 upsert short-circuit (D9 cache neutrality). */ private derivedPairCounter; /** * B39 — per-schema call index, advanced exactly once at each top-level * `generateSingleItem` ad-hoc / `generateArray` / outer-wrapper roll site. * Keyed by the same `ZodTypeAny` reference the call was made with. Two * `generate(X)` calls on one world get slots 1, 2, ...; an intervening * `generate(Y)` on a different schema reference leaves X's slot * untouched (the B39-R1 invariant). Scoped per-world so two worlds with * the same call sequence on X both see X's first call at slot 1. * * NB: the schema *identity* component of the fork key (``) is a * module-global `WeakMap` (see `getSchemaId` above). The per-world map * holds only the call count. */ private readonly schemaCallCounts; private readonly rootSeed; /** * B97-R15 — five mutable maps lazily allocated on first write. A * freshly-constructed world that never calls `withSchema` / `withKeyMap` * / `withGenerators` and never resolves a relation leaves all five at * `null`. Reads check via `?.get(...)`; writes route through the * `ensureXxx()` helpers below. */ private customKeyGenerators; private schemaKeyMaps; private relationPools; private pendingCounts; /** * B8 — per-pair upsert map for derived schemas registered with `from:`. * Outer key: derived schema reference. Inner key: source identity (the * source reference itself, or `source[sourceKey]` when declared). The * stored value is the post-transform derived record — the same reference * that lives in the registry (D8 — see `wiki/decisions.md`). * * B97-R15 — lazy: starts `null`; first write goes through * `ensureDerivedUpsert()`. */ private derivedUpsert; private lazyCache; /** * Effective storage mode for the current outer `generate` call. When `false`, * `generateAndStorePrimary` and `generateDerivedRecord` skip their * `registry.store` side-effect. Propagates through nested recursion; scoped * to the outer call via try/finally in `WorldImpl.generate`. */ private effectiveStore; /** * B65 — per-call locale, set when the caller passes `{ locale }` to * `world.generate(...)`. Read by {@link makeFieldCtx} so matcher closures * see the per-call locale on `ctx.locale` (and therefore through * `ctx.gen.*` calls). When `undefined`, `makeFieldCtx` falls back to the * world-construction `options.locale`. Push/pop in * {@link withEffectiveLocale} mirrors {@link withEffectiveStore}. */ private effectiveLocale; /** * Per-call `defaultArrayLength`, set when the caller passes * `{ defaultArrayLength }` to `world.generate(...)`. Propagates through * nested recursion (object fields, nested arrays) via the * `withEffectiveDefaultArrayLength` push/pop scope so inner * `generateArray` calls honour the outer call's intent. When `undefined`, * `generateArray` falls back to `this.options.defaultArrayLength ?? [1, 5]`. */ private effectiveDefaultArrayLength; /** * B55-R6 — per-call unique-mode flag, set when the caller passes * `{ unique: true }` to `world.generate(...)`. Read by {@link makeFieldCtx} * so the field-bound `ctx.prng.pickZipf` substitutes `s = 0` for the * duration of the call (uniqueness wins over realism — per B51 Q-9). * Push/pop in {@link withEffectiveUniqueMode} mirrors {@link withEffectiveLocale}. */ private effectiveUniqueMode; /** * B62 — relation-resolution collaborator. Owns `relationPools` (shared with * this `WorldImpl` via the deps surface) and hosts `resolveRelated` / * `resolveRelatedMany` / `resolveRelationPool` / `ensurePrimaryRecord`. The * `WorldImpl` remains the public face; this field is module-private. */ private readonly relations; /** * B97 — mutable per-`generate()` holder threaded through the lazy generator * binder (see `./bind-generators.ts`). Allocated lazily at the top of the * outer `generate()` entry path; the field loop mutates `state.prng` / * `state.ctx` in place so bound closures observe per-field state at call * time without rebinding. * * Lifetime: scoped to the outer `generate()` call. Reused across nested * `generate()` recursion (the holder is overwritten each `makeFieldCtx` * call). When the call returns, the holder remains live until the next * outer call replaces it — that's fine because every `makeFieldCtx` call * rewrites both fields before passing the holder back through `ctx.gen`. */ private fieldState; private boundGen; /** * B86 — provenance field-capture gate + sink. `traceEnabled` mirrors * `createWorld({ trace: true })` (D26 opt-in gate). When `false` (the * default) the per-field capture in {@link generateObjectFields} is a no-op: * no Proxy is installed over `ctx.current` and no `CapturedField` is * allocated, so the off-path stays allocation- and PRNG-neutral (B86-R10/R11). * `captureSink` keys each produced record object to its captured fields; the * {@link trace} projection attaches them by reference. */ private readonly traceEnabled; private captureSink; constructor(options?: WorldOptions); private ensureCustomKeyGenerators; private ensureSchemaKeyMaps; private ensureRelationPools; private ensureCaptureSink; private ensurePendingCounts; private ensureDerivedUpsert; /** * B39 — assign a stable identity to `schema` (lazily, on first sight) and * return the next per-schema call slot for the three fork-key sites * (`generateSingleItem` ad-hoc, `generateArray`, outer-wrapper roll). * * @param schema The outer schema reference the caller invoked * `generate(...)` with — NOT a re-resolved inner type. Using * the outer reference preserves identity across `lazy` * re-resolutions and `.optional()` / `.array()` wrappers. * @param commit When `true` (default) the slot is consumed and the * per-schema counter advances. When `false`, the next slot * that *would* be assigned is returned without mutating the * counter — useful for peek-before-cache-check patterns. * B39 currently always commits and rolls back on cache hit * via `rollbackSchemaSlot`; the parameter is here for * future use. */ private nextSchemaSlot; withSchema = Record>(schema: TSchema, opts?: SchemaOpts): this; withGenerators(map: Record): this; withKeyMap(schema: T, map: SchemaKeyMap): this; populate(schema: TSchema, count: number, factory?: (index: number) => GenerateOptions>): this; populateFrom(derivedSchema: TDerived, sourceSchema: TSource, predicate?: (item: z.infer) => boolean, factory?: (source: z.infer) => GenerateOptions>): this; generate(schema: TSchema, options?: GenerateOptions>): z.infer; explain(schema: TSchema): ExplainResult; trace(): WorldTrace; get(schema: TSchema, predicate?: Partial>): z.infer; private findPrimaryRegs; private findDerivedRegs; private resolveMode; /** * B10 — push/pop the {@link effectiveStore} flag for the duration of `fn`, * encapsulating the state machine described in B10-R2/R4/R5. When `value` is * `undefined` no push/pop happens (the call inherits the ambient mode); when * `value` is `true` or `false` the flag is set for the duration of `fn` and * restored in `finally`. * * The try/finally is what makes the B10 transitive-suppression contract * scoped to one outer `generate` call. */ private withEffectiveStore; /** * B65 — push/pop {@link effectiveLocale} for the duration of `fn`. When * `value` is `undefined` no push/pop happens (the call inherits the ambient * locale); when `value` is provided the per-call locale is set for the * duration of `fn` and restored in `finally`. Mirrors * {@link withEffectiveStore}. */ private withEffectiveLocale; private withEffectiveDefaultArrayLength; /** * B55-R6 — push/pop {@link effectiveUniqueMode} for the duration of `fn`. * Only `value === true` activates the mode (the default is false, and an * explicit `false` is a no-op pass-through — matching B65's * "undefined ⇒ inherit" pattern but specialised for the boolean opt-in). */ private withEffectiveUniqueMode; /** * B97 — push/pop the lazy generator-binder holder for the duration of `fn`. * * The holder is allocated lazily by {@link ensureFieldState} on first * `makeFieldCtx` call within the outer `generate()`. Nested `ctx.generate` * calls would otherwise mutate the same holder and break a captured outer * `ctx.gen` reference; saving/restoring the holder + `boundGen` pair here * ensures each outer call gets its own pair while still amortising binding * across all fields of the same call. * * Also wires the test-only `__setCurrentWorld` seam so the * `bind-generators-lazy.test.ts` per-world bind counter can attribute * materialisations to this `WorldImpl` instance. */ private withFieldStateScope; /** * B97 — lazy generator binding via mutable holder. * * Returns the shared `BoundGenerators` for the current outer `generate()` * call, allocating the `FieldState` + `buildLazyGen` pair on first * invocation and reusing them on every subsequent `makeFieldCtx` within * the same call. Each namespace is a property-getter on the returned * object that materialises its closure set on first touch and caches the * result — see `./bind-generators.ts` for the binder. * * The bound closures read `state.prng` / `state.ctx` at call time, so the * field loop only needs to mutate the holder's two fields per iteration * instead of re-binding 14 namespaces × ~10 helpers per field. * * B40's ctx-forwarding contract is preserved byte-identically (see * `CTX_SLOTS` in `./bind-generators.ts`). */ private ensureFieldState; /** * Build a fresh per-field `GeneratorContext` literal. R13's * `makeRecordCtx` + `updateFieldCtx` split was reverted (see spec * Context → "R13 was also tried and reverted"): the per-field literal * preserves monomorphic shape for V8 inline caching, which dominates the * allocation cost on small (no-matcher) workloads. * * Per-field PRNG (`fieldPrng`) seeds all data generation; `recordPrng` * is the stable per-record PRNG used for relation resolution so * `ctx.related("owner")` returns the same owner across every field of * the record. The lazy-bind holder (`fieldState`) is updated to point * at this field's PRNG + ctx so any bound closures `ctx.gen..()` * triggers see the current field's state at call time. */ private makeFieldCtx; private generateAndStorePrimary; private generateDerivedRecord; private generateObjectFields; private resolveField; /** * Thin dispatcher: resolves recursion-depth, the array PRNG fork, and the * inner-schema mode, then delegates to one of three per-mode methods. The * shared trailing pass ({@link applyArrayTrailingPass}) closes the D14 * sequence (cap → overrides → transform) by applying `options.transform` * uniformly across all three arms — the cap and per-index overrides are * already applied AT PRODUCTION TIME inside each per-mode method, where * D8 (stored == returned) requires they precede `registry.store`. * * Mirrors {@link generateSingleItem}'s B24 decomposition: dispatcher + named * branch helpers, with the trailing tail in one shared place so any new * cross-arm behaviour added per D14 lands once. */ private generateArray; /** * Derived arm — one output per source record across all derived `mode.regs`. * * Preserved contracts: * - Auto-provisions sources via `generateAndStorePrimary` until pair count * reaches `minRequired` (B52-R1 framing). * - B52-R8 / D14: caps at `callerMax ?? defMax` BEFORE production so no * `generateDerivedRecord` runs and no record is stored past the cap * (preserves D8: `registry.count(Derived) === result.length`). * - B52-R4: applies per-index `options.overrides` via post-production * `deepMerge` — matching the existing semantics (the stored record is * the pre-merge value; returned record is post-merge). * - Returns the result PRE-transform; the dispatcher's shared trailing * pass applies `options.transform`. */ private generateArrayDerived; /** * Primary arm — generate N items, store each in the registry, return all. * * Preserved contracts: * - Target length is `max(existingCount, int(min, max))` so previously * populated records are respected. * - B43 / B52-R2 / D14: honours caller `.max()` / `.length()` on BOTH * store-on and store-off paths; does NOT slice on the library-side * `defMax` fallback. * - B44: under `store: false`, allocates `Array.from(length=callerMax-capped)` * directly (the while-loop would never terminate because * `registry.count` never advances). * - B53 / D8: per-index `options.overrides` are threaded into * `generateAndStorePrimary`, merging at field-level BEFORE * `registry.store` (stored == returned). * - Returns the result PRE-transform; the dispatcher's shared trailing * pass applies `options.transform`. */ private generateArrayPrimary; /** * Ad-hoc arm — no registration; pure schema-based generation. * * Preserved contracts: * - B52-R7: shares `resolveMinRequired` / `resolveMaxAllowed` with the * other arms. * - Element PRNG forks via `genPrng.fork("[i]")` so element order doesn't * disturb other fields' seeds (D4 / D10). * - B52-R4: applies per-index `options.overrides` via `deepMerge`. * - Returns the result PRE-transform; the dispatcher's shared trailing * pass applies `options.transform`. */ private generateArrayAdHoc; /** * D14 shared trailing pass for `generateArray`. The full D14 sequence is * cap → per-index overrides → transform; the cap and per-index overrides * are applied AT PRODUCTION TIME inside each per-mode method (required to * preserve D8 for derived/primary, where the cap gates `registry.store` * and overrides on the primary arm merge before store via * `generateAndStorePrimary`). This shared method closes the sequence with * `options.transform`, which is uniformly safe to apply post-production * across all three arms. */ private applyArrayTrailingPass; /** * Collect `(source, reg, sourceIndex)` triples across the given derived * registrations by iterating each `reg.from`'s registry sources. When * `captured` is provided and a `reg.from` registry is empty, falls back * to the captured map (B20 local-capture for `store: false` derived * auto-source). Shared between {@link generateArrayDerived} and * {@link generateDerivedAutoSource}. */ private collectSourcePairs; /** * Thin dispatcher: resolves the schema mode (with-source override / no-source * derived / primary / ad-hoc), routes to one of the four private branch * helpers, and applies the trailing `overrides` + `transform` pass when the * branch helper hasn't already done so internally. * * Responsibilities kept here (per B24-R6): * - recursion-depth guard, * - `derivedPairCounter++` increment at top, * - lazy-resolve `while` producing `targetSchema`, * - two-level `findDerivedRegs` / `findPrimaryRegs` detection, * - `sourceOverride` extraction, * - B8 upsert cache short-circuit (D9 / B8-R9 cache-neutral — rolls back * `derivedPairCounter--` so an upsert hit advances no counter), * - trailing `overrides` deep-merge + `transform` application. */ private generateSingleItem; /** * Branch 1 — `sourceOverride !== undefined`: B8 with-source per-(DerivedSchema, * source-identity) upsert path. * * Preserved contracts: * - B8-R1 / B8-R4: writes the derived record to the registry and records * the `(identity → derived)` entry in `derivedUpsert` when * `effectiveStore === true && isUnique === true`. * - B8-R7 / B10: under `effectiveStore === false`, both the `registry.store` * and the `derivedUpsert` write are suppressed. * - D8 / B14: `generateDerivedRecord` applies `options.overrides` and * `options.transform` internally; the dispatcher's trailing block is * gated by `transformApplied = true` so neither is re-applied here. * * The upsert cache short-circuit and `derivedPairCounter--` rollback live in * the dispatcher above (D9 / B8-R9). */ private generateWithSourceOverride; /** * Branch 2 — `derivedRegs.length > 0` with no explicit source: the no-source * derived auto-source path. * * Preserved contracts: * - Auto-provisions one source per distinct `reg.from` with an empty * source registry, via `generateAndStorePrimary`. * - B20 local-capture: under `store: false` the auto-provision write is * suppressed, so the freshly generated source is captured in a local * `Map` keyed by `reg.from`. Multiple derivedRegs * sharing the same `reg.from` reuse one captured source. * - Pair-pick by `(derivedPairCounter - 1) % pairs.length` round-robin. * - D8 / B14: `generateDerivedRecord` applies `options.overrides` / * `options.transform` internally; the dispatcher's trailing block is * gated by `transformApplied = true`. * - B24-R3 (closes B21): under `if (this.effectiveStore)`, write the * derived record to the registry. Mirrors the with-source branch's * existing line. The new store does NOT touch `derivedUpsert` — that * map is keyed on explicit source identity and the no-source path has * none. * - B10-R4 / B20-R2: the new store call is gated on `this.effectiveStore`, * preserving transitive `store: false` suppression. */ private generateDerivedAutoSource; /** * Branch 3 — `primaryRegs.length > 0`: registered-primary path. * * Delegates to `generateAndStorePrimary`, which applies `options.transform` * internally and stores under `if (this.effectiveStore)`. The dispatcher's * trailing `transform` is suppressed via `transformApplied = true`. */ private generatePrimary; /** * Branch 4 — ad-hoc fallback for an unregistered schema. * * Preserved contracts: * - D4 / D10 / B39 Site 1: keys the ad-hoc PRNG on the OUTER `schema` * reference via `nextSchemaSlot(schema)`, NOT on the lazy-resolved * `targetSchema`. The outer reference is stable across `z.lazy(...)` * re-resolutions, matching the spec's identity model. * - Fork key literal shape: `adhoc:${id}:${slot}` (D10 / B39 pins this). * - Dispatches on `def(targetSchema).type === "object"` to either * `generateObjectFields` (objects, threading through `options?.overrides`) * or `generateFromSchema` (non-objects). * - The dispatcher applies the trailing `overrides` + `transform` block * to the result of this branch (it does not set `transformApplied`). */ private generateAdHoc; } /** * Create a {@link World} — the central, deterministic generation session. * Chain `.withSchema` / `.withGenerators` / `.withKeyMap` to configure it, then * `.generate` / `.populate` to produce data. One world = one seed = one dataset. * * @param options - World-wide settings ({@link WorldOptions}); the `seed` * fixes determinism. * * @example * ```ts * import { createWorld } from "zod4-mock"; * * const world = createWorld({ seed: 1 }); * world.withSchema(UserSchema); * const user = world.generate(UserSchema); * ``` */ export declare function createWorld(options?: WorldOptions): World; interface LazyMapsSnapshot { customKeyGenerators: Map | null; schemaKeyMaps: Map unknown>> | null; relationPools: Map | null; pendingCounts: Map | null; derivedUpsert: DerivedUpsertMap | null; } export declare function __inspectLazyMaps(world: object): LazyMapsSnapshot; export {}; //# sourceMappingURL=engine.d.ts.map