@synode/core 1.0.0

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine-CgLY6SKJ.cjs","names":["lines: string[]","bestMatch: string | undefined","parts: string[]","defaultIdGenerator: IdGenerator","idGenerator: IdGenerator","allLocales","Faker","keysToDelete: string[]","dataset: Dataset<TRow>","attributes: Record<string, unknown>","rows: DatasetRow[]","row: DatasetRow","journey: Journey","events: Event[]"],"sources":["../src/errors.ts","../src/state/ids.ts","../src/state/context.ts","../src/generators/persona.ts","../src/generators/dataset.ts","../src/monitoring/timing.ts","../src/execution/engine.ts"],"sourcesContent":["/**\n * Error codes for all Synode validation and runtime errors.\n */\nexport type ErrorCode =\n | 'INVALID_BOUNCE_CHANCE'\n | 'INVALID_TIME_SPAN'\n | 'INVALID_SUPPRESSION_PERIOD'\n | 'UNKNOWN_JOURNEY_REF'\n | 'CIRCULAR_DEPENDENCY'\n | 'DUPLICATE_ID'\n | 'DATASET_NOT_FOUND'\n | 'DATASET_EMPTY'\n | 'HANDLER_ERROR'\n | 'ADAPTER_WRITE_ERROR'\n | 'INVALID_HANDLER_RETURN'\n | 'TYPO_DETECTED'\n | 'INVALID_DATASET_COUNT';\n\n/**\n * Options for constructing a SynodeError.\n *\n * @param code - The structured error code identifying the error type\n * @param message - The human-readable error message\n * @param path - Hierarchical path segments (journey, adventure, action, step)\n * @param suggestion - Optional suggestion for fixing the error\n * @param expected - Optional description of the expected value\n * @param received - Optional description of the actual value received\n * @param cause - Optional underlying error that caused this one\n */\nexport interface SynodeErrorOptions {\n code: ErrorCode;\n message: string;\n path: string[];\n suggestion?: string;\n expected?: string;\n received?: string;\n cause?: unknown;\n}\n\nconst PATH_LABELS = ['Journey', 'Adventure', 'Action', 'Step'] as const;\n\n/**\n * Formats an error message with a hierarchical path prefix and optional suggestion.\n *\n * @param message - The base error message\n * @param path - Hierarchical path segments labeled Journey/Adventure/Action/Step\n * @param suggestion - Optional suggestion appended on a new line\n * @returns The formatted message string\n */\nexport function formatErrorMessage(message: string, path: string[], suggestion?: string): string {\n let formatted = message;\n\n if (path.length > 0) {\n const segments = path.map(\n (segment, i) => `${PATH_LABELS[Math.min(i, PATH_LABELS.length - 1)]} \"${segment}\"`,\n );\n formatted = `[${segments.join(' > ')}] ${message}`;\n }\n\n if (suggestion) {\n formatted += `\\n${suggestion}`;\n }\n\n return formatted;\n}\n\n/**\n * Structured error class for all Synode validation and runtime errors.\n *\n * @example\n * ```typescript\n * throw new SynodeError({\n * code: 'INVALID_BOUNCE_CHANCE',\n * message: 'Bounce chance must be between 0 and 1',\n * path: ['Purchase Flow', 'Checkout', 'Submit Order'],\n * expected: 'number between 0 and 1',\n * received: '1.5',\n * });\n * ```\n */\nexport class SynodeError extends Error {\n /** Structured error code identifying the error type. */\n readonly code: ErrorCode;\n\n /** Hierarchical path where the error occurred. */\n readonly path: string[];\n\n /** Optional suggestion for fixing the error. */\n readonly suggestion: string | undefined;\n\n /** The original unformatted error message. */\n readonly rawMessage: string;\n\n /** Optional description of the expected value. */\n readonly expected: string | undefined;\n\n /** Optional description of the actual value received. */\n readonly received: string | undefined;\n\n constructor(options: SynodeErrorOptions) {\n const formatted = formatErrorMessage(options.message, options.path, options.suggestion);\n super(formatted, { cause: options.cause });\n this.name = 'SynodeError';\n this.code = options.code;\n this.path = options.path;\n this.suggestion = options.suggestion;\n this.rawMessage = options.message;\n this.expected = options.expected;\n this.received = options.received;\n }\n\n /**\n * Produces a structured multi-line representation of the error for human-readable output.\n *\n * Format:\n * ```\n * [ERROR_CODE] rawMessage\n * Path: Journey 'x' > Adventure 'y' > Action 'z'\n * Expected: what was expected\n * Received: what was provided\n * Fix: suggestion\n * ```\n *\n * Path is omitted when path is empty. Expected/Received are omitted when not provided.\n * Fix is omitted when there is no suggestion.\n *\n * @returns A formatted string representation of the error.\n */\n format(): string {\n const lines: string[] = [`[${this.code}] ${this.rawMessage}`];\n\n if (this.path.length > 0) {\n const segments = this.path.map(\n (segment, i) => `${PATH_LABELS[Math.min(i, PATH_LABELS.length - 1)]} '${segment}'`,\n );\n lines.push(` Path: ${segments.join(' > ')}`);\n }\n\n if (this.expected !== undefined) {\n lines.push(` Expected: ${this.expected}`);\n }\n\n if (this.received !== undefined) {\n lines.push(` Received: ${this.received}`);\n }\n\n if (this.suggestion) {\n lines.push(` Fix: ${this.suggestion}`);\n }\n\n return lines.join('\\n');\n }\n}\n\n/**\n * Computes the Levenshtein edit distance between two strings using a space-efficient\n * two-row approach.\n *\n * @param a - First string\n * @param b - Second string\n * @returns The minimum number of single-character edits (insertions, deletions, substitutions)\n */\nexport function levenshtein(a: string, b: string): number {\n if (a === b) return 0;\n if (a.length === 0) return b.length;\n if (b.length === 0) return a.length;\n\n // Two-row approach: only keep previous and current rows\n let prev = new Array<number>(b.length + 1);\n let curr = new Array<number>(b.length + 1);\n\n for (let j = 0; j <= b.length; j++) {\n prev[j] = j;\n }\n\n for (let i = 1; i <= a.length; i++) {\n curr[0] = i;\n for (let j = 1; j <= b.length; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n curr[j] = Math.min(\n prev[j] + 1, // deletion\n curr[j - 1] + 1, // insertion\n prev[j - 1] + cost, // substitution\n );\n }\n [prev, curr] = [curr, prev];\n }\n\n return prev[b.length];\n}\n\n/**\n * Finds the closest matching string from a list of candidates using Levenshtein distance.\n * Returns undefined if no candidate is within the dynamic threshold.\n *\n * The threshold is `max(3, floor(maxLen * 0.4))` where maxLen is the longer of the\n * input and candidate strings.\n *\n * @param input - The string to match against candidates\n * @param candidates - List of valid strings to compare against\n * @returns The closest matching candidate, or undefined if none is close enough\n */\nexport function suggestClosest(input: string, candidates: string[]): string | undefined {\n if (candidates.length === 0) return undefined;\n\n let bestMatch: string | undefined;\n let bestDistance = Infinity;\n\n for (const candidate of candidates) {\n const distance = levenshtein(input, candidate);\n const maxLen = Math.max(input.length, candidate.length);\n const threshold = Math.max(3, Math.floor(maxLen * 0.4));\n\n if (distance <= threshold && distance < bestDistance) {\n bestDistance = distance;\n bestMatch = candidate;\n }\n }\n\n return bestMatch;\n}\n\n/**\n * Builds a human-readable suggestion string for a not-found error, including\n * the closest match (if any) and the full list of available options.\n *\n * @param kind - The kind of entity (e.g., \"dataset\", \"journey\")\n * @param requested - The name that was requested but not found\n * @param available - List of valid names\n * @returns A suggestion string with \"Did you mean...\" and/or available options\n */\nexport function buildNotFoundSuggestion(\n kind: string,\n requested: string,\n available: string[],\n): string {\n const closest = suggestClosest(requested, available);\n const parts: string[] = [];\n\n if (closest) {\n parts.push(`Did you mean '${closest}'?`);\n }\n\n parts.push(`Available ${kind}s: [${available.join(', ')}]`);\n\n return parts.join(' ');\n}\n","/**\n * Interface for ID generation strategies.\n */\nexport interface IdGenerator {\n generate(prefix?: string): string;\n}\n\n/**\n * Default ID generator using crypto.randomUUID.\n */\nexport const defaultIdGenerator: IdGenerator = {\n generate(prefix?: string) {\n const uuid = crypto.randomUUID();\n return prefix ? `${prefix}_${uuid}` : uuid;\n },\n};\n","import { Faker, allLocales } from '@faker-js/faker';\nimport {\n Context,\n Dataset,\n DatasetHandle,\n DatasetRow,\n ContextScope,\n ContextSetOptions,\n} from '../types.js';\nimport { SynodeError, buildNotFoundSuggestion } from '../errors.js';\nimport { IdGenerator, defaultIdGenerator } from './ids.js';\n\n/**\n * Internal metadata for scoped context fields.\n */\ninterface FieldMetadata {\n scope?: ContextScope;\n}\n\nexport class SynodeContext implements Context {\n private state = new Map<string, unknown>();\n private fieldMetadata = new Map<string, FieldMetadata>();\n private currentTime: Date;\n private _userId: string;\n private _sessionId: string;\n private _faker: Faker;\n private completedJourneys = new Set<string>();\n private datasets = new Map<string, Dataset>();\n\n constructor(\n startTime: Date = new Date(),\n private idGenerator: IdGenerator = defaultIdGenerator,\n public readonly locale = 'en',\n ) {\n this.currentTime = new Date(startTime);\n this._userId = idGenerator.generate('user');\n this._sessionId = idGenerator.generate('session');\n\n // Initialize localized faker\n const localeDef = allLocales[locale as keyof typeof allLocales];\n this._faker = new Faker({ locale: localeDef });\n }\n\n get faker(): Faker {\n return this._faker;\n }\n\n get userId(): string {\n return this._userId;\n }\n\n get sessionId(): string {\n return this._sessionId;\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters\n get<T>(key: string): T | undefined {\n return this.state.get(key) as T | undefined;\n }\n\n /**\n * Set a context field value with optional scope for automatic cleanup.\n *\n * @param key - The field name\n * @param value - The field value\n * @param options - Optional settings including lifecycle scope\n *\n * @remarks\n * When calling `set()` multiple times on the same field with different scopes,\n * the most recent scope setting will be used. For example, if a field is first\n * set with `scope: 'adventure'` and later set again with `scope: 'action'`,\n * it will be cleared at the end of the action rather than the adventure.\n *\n * @example\n * ```typescript\n * ctx.set('cartItems', [], { scope: 'adventure' });\n * // Later in the same adventure...\n * ctx.set('cartItems', ['item1'], { scope: 'action' }); // Now scoped to action\n * ```\n */\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters\n set<T>(key: string, value: T, options?: ContextSetOptions): void {\n this.state.set(key, value);\n if (options?.scope) {\n this.fieldMetadata.set(key, { scope: options.scope });\n } else {\n // If no scope is set, remove any existing metadata\n this.fieldMetadata.delete(key);\n }\n }\n\n now(): Date {\n return new Date(this.currentTime);\n }\n\n generateId(prefix?: string): string {\n return this.idGenerator.generate(prefix);\n }\n\n hasCompletedJourney(journeyId: string): boolean {\n return this.completedJourneys.has(journeyId);\n }\n\n markJourneyComplete(journeyId: string): void {\n this.completedJourneys.add(journeyId);\n }\n\n dataset(id: string): DatasetHandle {\n const ds = this.datasets.get(id);\n if (!ds) {\n const available = [...this.datasets.keys()];\n const suggestion =\n available.length > 0 ? buildNotFoundSuggestion('dataset', id, available) : undefined;\n throw new SynodeError({\n code: 'DATASET_NOT_FOUND',\n message: `Dataset '${id}' not found in context`,\n path: [],\n suggestion,\n });\n }\n return new DatasetHandleImpl(ds);\n }\n\n typedDataset<TRow extends DatasetRow>(id: string): DatasetHandle<TRow> {\n const ds = this.datasets.get(id);\n if (!ds) {\n const available = [...this.datasets.keys()];\n const suggestion =\n available.length > 0 ? buildNotFoundSuggestion('dataset', id, available) : undefined;\n throw new SynodeError({\n code: 'DATASET_NOT_FOUND',\n message: `Dataset '${id}' not found in context`,\n path: [],\n suggestion,\n });\n }\n return new DatasetHandleImpl<TRow>(ds as Dataset<TRow>);\n }\n\n /**\n * Internal: Register a dataset for use in this context.\n */\n registerDataset(dataset: Dataset): void {\n this.datasets.set(dataset.id, dataset);\n }\n\n /**\n * Internal: Advance the simulation time.\n */\n advanceTime(ms: number): void {\n this.currentTime = new Date(this.currentTime.getTime() + ms);\n }\n\n /**\n * Internal: Rotate the session ID.\n */\n rotateSession(): void {\n this._sessionId = this.idGenerator.generate('session');\n }\n\n /**\n * Internal: Clear all fields with the specified scope.\n * Called by the engine when a scope ends.\n */\n clearScope(scope: ContextScope): void {\n const keysToDelete: string[] = [];\n for (const [key, metadata] of this.fieldMetadata.entries()) {\n if (metadata.scope === scope) {\n keysToDelete.push(key);\n }\n }\n for (const key of keysToDelete) {\n this.state.delete(key);\n this.fieldMetadata.delete(key);\n }\n }\n}\n\n/**\n * Internal implementation of DatasetHandle.\n */\nclass DatasetHandleImpl<TRow extends DatasetRow = DatasetRow> implements DatasetHandle<TRow> {\n constructor(private dataset: Dataset<TRow>) {}\n\n randomRow(): TRow {\n if (this.dataset.rows.length === 0) {\n throw new Error(`Dataset '${this.dataset.id}' is empty.`);\n }\n const index = Math.floor(Math.random() * this.dataset.rows.length);\n return this.dataset.rows[index];\n }\n\n getRowById(id: string | number): TRow | undefined {\n // Find row by searching all fields for a matching value\n return this.dataset.rows.find((row) => {\n for (const key of Object.keys(row)) {\n if ((row as Record<string, unknown>)[key] === id) {\n return true;\n }\n }\n return false;\n });\n }\n\n getRowByIndex(index: number): TRow | undefined {\n return this.dataset.rows[index];\n }\n\n getAllRows(): TRow[] {\n return [...this.dataset.rows];\n }\n\n size(): number {\n return this.dataset.rows.length;\n }\n}\n","import { Context, FieldGenerator } from '../types.js';\n\n/**\n * Configuration for a user persona.\n */\nexport interface PersonaDefinition {\n id: string;\n name: string;\n /**\n * Attributes to initialize in the user's context.\n * Can include 'locale' to set the faker locale.\n */\n attributes: Record<string, FieldGenerator>;\n}\n\n/**\n * Represents an instantiated persona.\n */\nexport interface Persona {\n id: string;\n name: string;\n attributes: Record<string, unknown>;\n}\n\n/**\n * Defines a new persona.\n */\nexport function definePersona(config: PersonaDefinition): PersonaDefinition {\n return config;\n}\n\n/**\n * Generates a concrete persona instance from a definition.\n * This resolves all dynamic fields and weighted distributions.\n */\nexport async function generatePersona(\n definition: PersonaDefinition,\n baseContext: Context,\n): Promise<Persona> {\n const attributes: Record<string, unknown> = {};\n\n for (const [key, generator] of Object.entries(definition.attributes)) {\n if (typeof generator === 'function') {\n // Execute generator with base context (which might have default locale)\n const genFn = generator as (\n context: Context,\n payload: Record<string, unknown>,\n ) => Promise<unknown>;\n attributes[key] = await genFn(baseContext, attributes);\n } else {\n attributes[key] = generator;\n }\n }\n\n return {\n id: definition.id,\n name: definition.name,\n attributes,\n };\n}\n","import { Dataset, DatasetDefinition, DatasetRow, Context } from '../types.js';\n\n/**\n * Defines a new dataset with type inference support.\n *\n * @example\n * ```typescript\n * const products = defineDataset({\n * id: 'products',\n * name: 'Products',\n * count: 100,\n * fields: {\n * id: (ctx, row) => `prod-${row.index}`,\n * name: (ctx) => ctx.faker.commerce.productName(),\n * price: (ctx) => ctx.faker.number.float({ min: 10, max: 500 }),\n * },\n * });\n *\n * // Type inference works automatically\n * type Product = InferDatasetRow<typeof products>;\n * ```\n */\nexport function defineDataset<TFields extends Record<string, unknown>>(\n config: DatasetDefinition<TFields>,\n): DatasetDefinition<TFields> {\n return config;\n}\n\n/**\n * Generates a concrete dataset from a definition.\n * @param definition The dataset definition.\n * @param context Context providing access to faker, other datasets, and user data.\n */\nexport async function generateDataset<TFields extends Record<string, unknown>>(\n definition: DatasetDefinition<TFields>,\n context: Context,\n): Promise<Dataset> {\n if (!Number.isFinite(definition.count) || definition.count < 0 || definition.count > 10_000_000) {\n throw new Error(\n `Dataset '${definition.id}' count must be a finite number between 0 and 10,000,000, got ${String(definition.count)}`,\n );\n }\n\n const rows: DatasetRow[] = [];\n\n for (let index = 0; index < definition.count; index++) {\n const row: DatasetRow = {};\n\n for (const [key, generator] of Object.entries(definition.fields)) {\n if (typeof generator === 'function') {\n // Execute generator with context and row metadata\n const genFn = generator as (\n context: Context,\n rowMeta: { index: number; data: DatasetRow },\n ) => Promise<unknown>;\n row[key] = await genFn(context, { index, data: row });\n } else {\n // Use static value\n row[key] = generator;\n }\n }\n\n rows.push(row);\n }\n\n return {\n id: definition.id,\n name: definition.name,\n rows,\n };\n}\n","import { TimeSpan } from '../types.js';\n\n/**\n * Generates a delay in milliseconds based on a time span configuration.\n */\nexport function generateDelay(timeSpan: TimeSpan): number {\n const { min, max, distribution = 'uniform' } = timeSpan;\n\n switch (distribution) {\n case 'uniform':\n return min + Math.random() * (max - min);\n\n case 'gaussian': {\n // Box-Muller transform for Gaussian distribution\n const mean = (min + max) / 2;\n const stdDev = (max - min) / 6; // 99.7% within range\n const u1 = Math.random();\n const u2 = Math.random();\n const z0 = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2);\n const value = mean + z0 * stdDev;\n // Clamp to min/max\n return Math.max(min, Math.min(max, value));\n }\n\n case 'exponential': {\n // Exponential distribution (more shorter delays, fewer long delays)\n const lambda = 1 / (max - min);\n const value = min - Math.log(1 - Math.random()) / lambda;\n return Math.min(max, value);\n }\n\n default:\n return min + Math.random() * (max - min);\n }\n}\n\n/**\n * Checks if a bounce should occur based on probability.\n */\nexport function shouldBounce(bounceChance?: number): boolean {\n if (!bounceChance) return false;\n return Math.random() < bounceChance;\n}\n","import { Journey, Event } from '../types.js';\nimport { SynodeContext } from '../state/context.js';\nimport { SynodeError } from '../errors.js';\nimport { generateDelay, shouldBounce } from '../monitoring/timing.js';\n\nexport class Engine {\n constructor(private journey: Journey) {}\n\n /**\n * Executes the journey and yields events.\n * @param context Optional context to use. If not provided, a new one is created.\n */\n async *run(context?: SynodeContext): AsyncGenerator<Event> {\n // Initialize context for this run (user) if not provided\n const ctx = context ?? new SynodeContext();\n\n // Rotate session ID for this journey run\n ctx.rotateSession();\n\n // Check journey prerequisites\n if (this.journey.requires) {\n for (const requiredJourneyId of this.journey.requires) {\n if (!ctx.hasCompletedJourney(requiredJourneyId)) {\n // Prerequisites not met, skip this journey\n return;\n }\n }\n }\n\n // Check if journey should bounce before starting\n if (shouldBounce(this.journey.bounceChance)) {\n // Apply suppression period and return without generating events\n if (this.journey.suppressionPeriod) {\n const suppressionDelay = generateDelay(this.journey.suppressionPeriod);\n ctx.advanceTime(suppressionDelay);\n }\n return;\n }\n\n // Execute adventures in sequence\n for (const adventure of this.journey.adventures) {\n // Check adventure bounce\n if (shouldBounce(adventure.bounceChance)) {\n if (adventure.onBounce === 'skip') {\n // Clear adventure-scoped fields before skipping\n ctx.clearScope('adventure');\n // Skip to next adventure\n continue;\n } else {\n // Clear adventure-scoped fields before stopping\n ctx.clearScope('adventure');\n // Stop the entire journey\n break;\n }\n }\n\n // Execute actions in sequence\n for (const action of adventure.actions) {\n // Check action bounce\n if (shouldBounce(action.bounceChance)) {\n // Clear action-scoped fields before breaking\n ctx.clearScope('action');\n break; // Stop this adventure\n }\n\n // Apply delay before action (from adventure's timeSpan)\n if (adventure.timeSpan) {\n const delay = generateDelay(adventure.timeSpan);\n ctx.advanceTime(delay);\n }\n\n // Execute the action handler to get events\n const actionPath = [this.journey.id, adventure.id, action.id];\n let events: Event[];\n try {\n events = await action.handler(ctx);\n } catch (err) {\n if (err instanceof SynodeError) {\n throw err;\n }\n const errorName = err instanceof Error ? err.constructor.name : 'UnknownError';\n const errorMessage = err instanceof Error ? err.message : String(err);\n throw new SynodeError({\n code: 'HANDLER_ERROR',\n message: `${errorName}: ${errorMessage} (userId: ${ctx.userId}, sessionId: ${ctx.sessionId})`,\n path: actionPath,\n cause: err,\n });\n }\n\n if (!Array.isArray(events)) {\n throw new SynodeError({\n code: 'INVALID_HANDLER_RETURN',\n message: `Action handler must return an array of Events, got ${typeof events}`,\n path: actionPath,\n });\n }\n\n // Yield events with timing between them\n for (let i = 0; i < events.length; i++) {\n if (i > 0 && action.timeSpan) {\n // Apply delay between events within the same action\n const delay = generateDelay(action.timeSpan);\n ctx.advanceTime(delay);\n // Update event timestamp\n events[i].timestamp = ctx.now();\n }\n yield events[i];\n }\n\n // Clear action-scoped fields after action completes\n ctx.clearScope('action');\n }\n\n // Clear adventure-scoped fields after adventure completes\n ctx.clearScope('adventure');\n }\n\n // Mark journey as complete\n ctx.markJourneyComplete(this.journey.id);\n\n // Clear journey-scoped fields after journey completes\n ctx.clearScope('journey');\n\n // Apply suppression period after completion\n if (this.journey.suppressionPeriod) {\n const suppressionDelay = generateDelay(this.journey.suppressionPeriod);\n ctx.advanceTime(suppressionDelay);\n }\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,515 @@
1
+ import { Faker, allLocales } from "@faker-js/faker";
2
+
3
+ //#region src/errors.ts
4
+ const PATH_LABELS = [
5
+ "Journey",
6
+ "Adventure",
7
+ "Action",
8
+ "Step"
9
+ ];
10
+ /**
11
+ * Formats an error message with a hierarchical path prefix and optional suggestion.
12
+ *
13
+ * @param message - The base error message
14
+ * @param path - Hierarchical path segments labeled Journey/Adventure/Action/Step
15
+ * @param suggestion - Optional suggestion appended on a new line
16
+ * @returns The formatted message string
17
+ */
18
+ function formatErrorMessage(message, path, suggestion) {
19
+ let formatted = message;
20
+ if (path.length > 0) formatted = `[${path.map((segment, i) => `${PATH_LABELS[Math.min(i, PATH_LABELS.length - 1)]} "${segment}"`).join(" > ")}] ${message}`;
21
+ if (suggestion) formatted += `\n${suggestion}`;
22
+ return formatted;
23
+ }
24
+ /**
25
+ * Structured error class for all Synode validation and runtime errors.
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * throw new SynodeError({
30
+ * code: 'INVALID_BOUNCE_CHANCE',
31
+ * message: 'Bounce chance must be between 0 and 1',
32
+ * path: ['Purchase Flow', 'Checkout', 'Submit Order'],
33
+ * expected: 'number between 0 and 1',
34
+ * received: '1.5',
35
+ * });
36
+ * ```
37
+ */
38
+ var SynodeError = class extends Error {
39
+ /** Structured error code identifying the error type. */
40
+ code;
41
+ /** Hierarchical path where the error occurred. */
42
+ path;
43
+ /** Optional suggestion for fixing the error. */
44
+ suggestion;
45
+ /** The original unformatted error message. */
46
+ rawMessage;
47
+ /** Optional description of the expected value. */
48
+ expected;
49
+ /** Optional description of the actual value received. */
50
+ received;
51
+ constructor(options) {
52
+ const formatted = formatErrorMessage(options.message, options.path, options.suggestion);
53
+ super(formatted, { cause: options.cause });
54
+ this.name = "SynodeError";
55
+ this.code = options.code;
56
+ this.path = options.path;
57
+ this.suggestion = options.suggestion;
58
+ this.rawMessage = options.message;
59
+ this.expected = options.expected;
60
+ this.received = options.received;
61
+ }
62
+ /**
63
+ * Produces a structured multi-line representation of the error for human-readable output.
64
+ *
65
+ * Format:
66
+ * ```
67
+ * [ERROR_CODE] rawMessage
68
+ * Path: Journey 'x' > Adventure 'y' > Action 'z'
69
+ * Expected: what was expected
70
+ * Received: what was provided
71
+ * Fix: suggestion
72
+ * ```
73
+ *
74
+ * Path is omitted when path is empty. Expected/Received are omitted when not provided.
75
+ * Fix is omitted when there is no suggestion.
76
+ *
77
+ * @returns A formatted string representation of the error.
78
+ */
79
+ format() {
80
+ const lines = [`[${this.code}] ${this.rawMessage}`];
81
+ if (this.path.length > 0) {
82
+ const segments = this.path.map((segment, i) => `${PATH_LABELS[Math.min(i, PATH_LABELS.length - 1)]} '${segment}'`);
83
+ lines.push(` Path: ${segments.join(" > ")}`);
84
+ }
85
+ if (this.expected !== void 0) lines.push(` Expected: ${this.expected}`);
86
+ if (this.received !== void 0) lines.push(` Received: ${this.received}`);
87
+ if (this.suggestion) lines.push(` Fix: ${this.suggestion}`);
88
+ return lines.join("\n");
89
+ }
90
+ };
91
+ /**
92
+ * Computes the Levenshtein edit distance between two strings using a space-efficient
93
+ * two-row approach.
94
+ *
95
+ * @param a - First string
96
+ * @param b - Second string
97
+ * @returns The minimum number of single-character edits (insertions, deletions, substitutions)
98
+ */
99
+ function levenshtein(a, b) {
100
+ if (a === b) return 0;
101
+ if (a.length === 0) return b.length;
102
+ if (b.length === 0) return a.length;
103
+ let prev = new Array(b.length + 1);
104
+ let curr = new Array(b.length + 1);
105
+ for (let j = 0; j <= b.length; j++) prev[j] = j;
106
+ for (let i = 1; i <= a.length; i++) {
107
+ curr[0] = i;
108
+ for (let j = 1; j <= b.length; j++) {
109
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
110
+ curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
111
+ }
112
+ [prev, curr] = [curr, prev];
113
+ }
114
+ return prev[b.length];
115
+ }
116
+ /**
117
+ * Finds the closest matching string from a list of candidates using Levenshtein distance.
118
+ * Returns undefined if no candidate is within the dynamic threshold.
119
+ *
120
+ * The threshold is `max(3, floor(maxLen * 0.4))` where maxLen is the longer of the
121
+ * input and candidate strings.
122
+ *
123
+ * @param input - The string to match against candidates
124
+ * @param candidates - List of valid strings to compare against
125
+ * @returns The closest matching candidate, or undefined if none is close enough
126
+ */
127
+ function suggestClosest(input, candidates) {
128
+ if (candidates.length === 0) return void 0;
129
+ let bestMatch;
130
+ let bestDistance = Infinity;
131
+ for (const candidate of candidates) {
132
+ const distance = levenshtein(input, candidate);
133
+ const maxLen = Math.max(input.length, candidate.length);
134
+ if (distance <= Math.max(3, Math.floor(maxLen * .4)) && distance < bestDistance) {
135
+ bestDistance = distance;
136
+ bestMatch = candidate;
137
+ }
138
+ }
139
+ return bestMatch;
140
+ }
141
+ /**
142
+ * Builds a human-readable suggestion string for a not-found error, including
143
+ * the closest match (if any) and the full list of available options.
144
+ *
145
+ * @param kind - The kind of entity (e.g., "dataset", "journey")
146
+ * @param requested - The name that was requested but not found
147
+ * @param available - List of valid names
148
+ * @returns A suggestion string with "Did you mean..." and/or available options
149
+ */
150
+ function buildNotFoundSuggestion(kind, requested, available) {
151
+ const closest = suggestClosest(requested, available);
152
+ const parts = [];
153
+ if (closest) parts.push(`Did you mean '${closest}'?`);
154
+ parts.push(`Available ${kind}s: [${available.join(", ")}]`);
155
+ return parts.join(" ");
156
+ }
157
+
158
+ //#endregion
159
+ //#region src/state/ids.ts
160
+ /**
161
+ * Default ID generator using crypto.randomUUID.
162
+ */
163
+ const defaultIdGenerator = { generate(prefix) {
164
+ const uuid = crypto.randomUUID();
165
+ return prefix ? `${prefix}_${uuid}` : uuid;
166
+ } };
167
+
168
+ //#endregion
169
+ //#region src/state/context.ts
170
+ var SynodeContext = class {
171
+ state = /* @__PURE__ */ new Map();
172
+ fieldMetadata = /* @__PURE__ */ new Map();
173
+ currentTime;
174
+ _userId;
175
+ _sessionId;
176
+ _faker;
177
+ completedJourneys = /* @__PURE__ */ new Set();
178
+ datasets = /* @__PURE__ */ new Map();
179
+ constructor(startTime = /* @__PURE__ */ new Date(), idGenerator = defaultIdGenerator, locale = "en") {
180
+ this.idGenerator = idGenerator;
181
+ this.locale = locale;
182
+ this.currentTime = new Date(startTime);
183
+ this._userId = idGenerator.generate("user");
184
+ this._sessionId = idGenerator.generate("session");
185
+ const localeDef = allLocales[locale];
186
+ this._faker = new Faker({ locale: localeDef });
187
+ }
188
+ get faker() {
189
+ return this._faker;
190
+ }
191
+ get userId() {
192
+ return this._userId;
193
+ }
194
+ get sessionId() {
195
+ return this._sessionId;
196
+ }
197
+ get(key) {
198
+ return this.state.get(key);
199
+ }
200
+ /**
201
+ * Set a context field value with optional scope for automatic cleanup.
202
+ *
203
+ * @param key - The field name
204
+ * @param value - The field value
205
+ * @param options - Optional settings including lifecycle scope
206
+ *
207
+ * @remarks
208
+ * When calling `set()` multiple times on the same field with different scopes,
209
+ * the most recent scope setting will be used. For example, if a field is first
210
+ * set with `scope: 'adventure'` and later set again with `scope: 'action'`,
211
+ * it will be cleared at the end of the action rather than the adventure.
212
+ *
213
+ * @example
214
+ * ```typescript
215
+ * ctx.set('cartItems', [], { scope: 'adventure' });
216
+ * // Later in the same adventure...
217
+ * ctx.set('cartItems', ['item1'], { scope: 'action' }); // Now scoped to action
218
+ * ```
219
+ */
220
+ set(key, value, options) {
221
+ this.state.set(key, value);
222
+ if (options?.scope) this.fieldMetadata.set(key, { scope: options.scope });
223
+ else this.fieldMetadata.delete(key);
224
+ }
225
+ now() {
226
+ return new Date(this.currentTime);
227
+ }
228
+ generateId(prefix) {
229
+ return this.idGenerator.generate(prefix);
230
+ }
231
+ hasCompletedJourney(journeyId) {
232
+ return this.completedJourneys.has(journeyId);
233
+ }
234
+ markJourneyComplete(journeyId) {
235
+ this.completedJourneys.add(journeyId);
236
+ }
237
+ dataset(id) {
238
+ const ds = this.datasets.get(id);
239
+ if (!ds) {
240
+ const available = [...this.datasets.keys()];
241
+ const suggestion = available.length > 0 ? buildNotFoundSuggestion("dataset", id, available) : void 0;
242
+ throw new SynodeError({
243
+ code: "DATASET_NOT_FOUND",
244
+ message: `Dataset '${id}' not found in context`,
245
+ path: [],
246
+ suggestion
247
+ });
248
+ }
249
+ return new DatasetHandleImpl(ds);
250
+ }
251
+ typedDataset(id) {
252
+ const ds = this.datasets.get(id);
253
+ if (!ds) {
254
+ const available = [...this.datasets.keys()];
255
+ const suggestion = available.length > 0 ? buildNotFoundSuggestion("dataset", id, available) : void 0;
256
+ throw new SynodeError({
257
+ code: "DATASET_NOT_FOUND",
258
+ message: `Dataset '${id}' not found in context`,
259
+ path: [],
260
+ suggestion
261
+ });
262
+ }
263
+ return new DatasetHandleImpl(ds);
264
+ }
265
+ /**
266
+ * Internal: Register a dataset for use in this context.
267
+ */
268
+ registerDataset(dataset) {
269
+ this.datasets.set(dataset.id, dataset);
270
+ }
271
+ /**
272
+ * Internal: Advance the simulation time.
273
+ */
274
+ advanceTime(ms) {
275
+ this.currentTime = new Date(this.currentTime.getTime() + ms);
276
+ }
277
+ /**
278
+ * Internal: Rotate the session ID.
279
+ */
280
+ rotateSession() {
281
+ this._sessionId = this.idGenerator.generate("session");
282
+ }
283
+ /**
284
+ * Internal: Clear all fields with the specified scope.
285
+ * Called by the engine when a scope ends.
286
+ */
287
+ clearScope(scope) {
288
+ const keysToDelete = [];
289
+ for (const [key, metadata] of this.fieldMetadata.entries()) if (metadata.scope === scope) keysToDelete.push(key);
290
+ for (const key of keysToDelete) {
291
+ this.state.delete(key);
292
+ this.fieldMetadata.delete(key);
293
+ }
294
+ }
295
+ };
296
+ /**
297
+ * Internal implementation of DatasetHandle.
298
+ */
299
+ var DatasetHandleImpl = class {
300
+ constructor(dataset) {
301
+ this.dataset = dataset;
302
+ }
303
+ randomRow() {
304
+ if (this.dataset.rows.length === 0) throw new Error(`Dataset '${this.dataset.id}' is empty.`);
305
+ const index = Math.floor(Math.random() * this.dataset.rows.length);
306
+ return this.dataset.rows[index];
307
+ }
308
+ getRowById(id) {
309
+ return this.dataset.rows.find((row) => {
310
+ for (const key of Object.keys(row)) if (row[key] === id) return true;
311
+ return false;
312
+ });
313
+ }
314
+ getRowByIndex(index) {
315
+ return this.dataset.rows[index];
316
+ }
317
+ getAllRows() {
318
+ return [...this.dataset.rows];
319
+ }
320
+ size() {
321
+ return this.dataset.rows.length;
322
+ }
323
+ };
324
+
325
+ //#endregion
326
+ //#region src/generators/persona.ts
327
+ /**
328
+ * Defines a new persona.
329
+ */
330
+ function definePersona(config) {
331
+ return config;
332
+ }
333
+ /**
334
+ * Generates a concrete persona instance from a definition.
335
+ * This resolves all dynamic fields and weighted distributions.
336
+ */
337
+ async function generatePersona(definition, baseContext) {
338
+ const attributes = {};
339
+ for (const [key, generator] of Object.entries(definition.attributes)) if (typeof generator === "function") attributes[key] = await generator(baseContext, attributes);
340
+ else attributes[key] = generator;
341
+ return {
342
+ id: definition.id,
343
+ name: definition.name,
344
+ attributes
345
+ };
346
+ }
347
+
348
+ //#endregion
349
+ //#region src/generators/dataset.ts
350
+ /**
351
+ * Defines a new dataset with type inference support.
352
+ *
353
+ * @example
354
+ * ```typescript
355
+ * const products = defineDataset({
356
+ * id: 'products',
357
+ * name: 'Products',
358
+ * count: 100,
359
+ * fields: {
360
+ * id: (ctx, row) => `prod-${row.index}`,
361
+ * name: (ctx) => ctx.faker.commerce.productName(),
362
+ * price: (ctx) => ctx.faker.number.float({ min: 10, max: 500 }),
363
+ * },
364
+ * });
365
+ *
366
+ * // Type inference works automatically
367
+ * type Product = InferDatasetRow<typeof products>;
368
+ * ```
369
+ */
370
+ function defineDataset(config) {
371
+ return config;
372
+ }
373
+ /**
374
+ * Generates a concrete dataset from a definition.
375
+ * @param definition The dataset definition.
376
+ * @param context Context providing access to faker, other datasets, and user data.
377
+ */
378
+ async function generateDataset(definition, context) {
379
+ if (!Number.isFinite(definition.count) || definition.count < 0 || definition.count > 1e7) throw new Error(`Dataset '${definition.id}' count must be a finite number between 0 and 10,000,000, got ${String(definition.count)}`);
380
+ const rows = [];
381
+ for (let index = 0; index < definition.count; index++) {
382
+ const row = {};
383
+ for (const [key, generator] of Object.entries(definition.fields)) if (typeof generator === "function") row[key] = await generator(context, {
384
+ index,
385
+ data: row
386
+ });
387
+ else row[key] = generator;
388
+ rows.push(row);
389
+ }
390
+ return {
391
+ id: definition.id,
392
+ name: definition.name,
393
+ rows
394
+ };
395
+ }
396
+
397
+ //#endregion
398
+ //#region src/monitoring/timing.ts
399
+ /**
400
+ * Generates a delay in milliseconds based on a time span configuration.
401
+ */
402
+ function generateDelay(timeSpan) {
403
+ const { min, max, distribution = "uniform" } = timeSpan;
404
+ switch (distribution) {
405
+ case "uniform": return min + Math.random() * (max - min);
406
+ case "gaussian": {
407
+ const mean = (min + max) / 2;
408
+ const stdDev = (max - min) / 6;
409
+ const u1 = Math.random();
410
+ const u2 = Math.random();
411
+ const value = mean + Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2) * stdDev;
412
+ return Math.max(min, Math.min(max, value));
413
+ }
414
+ case "exponential": {
415
+ const lambda = 1 / (max - min);
416
+ const value = min - Math.log(1 - Math.random()) / lambda;
417
+ return Math.min(max, value);
418
+ }
419
+ default: return min + Math.random() * (max - min);
420
+ }
421
+ }
422
+ /**
423
+ * Checks if a bounce should occur based on probability.
424
+ */
425
+ function shouldBounce(bounceChance) {
426
+ if (!bounceChance) return false;
427
+ return Math.random() < bounceChance;
428
+ }
429
+
430
+ //#endregion
431
+ //#region src/execution/engine.ts
432
+ var Engine = class {
433
+ constructor(journey) {
434
+ this.journey = journey;
435
+ }
436
+ /**
437
+ * Executes the journey and yields events.
438
+ * @param context Optional context to use. If not provided, a new one is created.
439
+ */
440
+ async *run(context) {
441
+ const ctx = context ?? new SynodeContext();
442
+ ctx.rotateSession();
443
+ if (this.journey.requires) {
444
+ for (const requiredJourneyId of this.journey.requires) if (!ctx.hasCompletedJourney(requiredJourneyId)) return;
445
+ }
446
+ if (shouldBounce(this.journey.bounceChance)) {
447
+ if (this.journey.suppressionPeriod) {
448
+ const suppressionDelay = generateDelay(this.journey.suppressionPeriod);
449
+ ctx.advanceTime(suppressionDelay);
450
+ }
451
+ return;
452
+ }
453
+ for (const adventure of this.journey.adventures) {
454
+ if (shouldBounce(adventure.bounceChance)) if (adventure.onBounce === "skip") {
455
+ ctx.clearScope("adventure");
456
+ continue;
457
+ } else {
458
+ ctx.clearScope("adventure");
459
+ break;
460
+ }
461
+ for (const action of adventure.actions) {
462
+ if (shouldBounce(action.bounceChance)) {
463
+ ctx.clearScope("action");
464
+ break;
465
+ }
466
+ if (adventure.timeSpan) {
467
+ const delay = generateDelay(adventure.timeSpan);
468
+ ctx.advanceTime(delay);
469
+ }
470
+ const actionPath = [
471
+ this.journey.id,
472
+ adventure.id,
473
+ action.id
474
+ ];
475
+ let events;
476
+ try {
477
+ events = await action.handler(ctx);
478
+ } catch (err) {
479
+ if (err instanceof SynodeError) throw err;
480
+ throw new SynodeError({
481
+ code: "HANDLER_ERROR",
482
+ message: `${err instanceof Error ? err.constructor.name : "UnknownError"}: ${err instanceof Error ? err.message : String(err)} (userId: ${ctx.userId}, sessionId: ${ctx.sessionId})`,
483
+ path: actionPath,
484
+ cause: err
485
+ });
486
+ }
487
+ if (!Array.isArray(events)) throw new SynodeError({
488
+ code: "INVALID_HANDLER_RETURN",
489
+ message: `Action handler must return an array of Events, got ${typeof events}`,
490
+ path: actionPath
491
+ });
492
+ for (let i = 0; i < events.length; i++) {
493
+ if (i > 0 && action.timeSpan) {
494
+ const delay = generateDelay(action.timeSpan);
495
+ ctx.advanceTime(delay);
496
+ events[i].timestamp = ctx.now();
497
+ }
498
+ yield events[i];
499
+ }
500
+ ctx.clearScope("action");
501
+ }
502
+ ctx.clearScope("adventure");
503
+ }
504
+ ctx.markJourneyComplete(this.journey.id);
505
+ ctx.clearScope("journey");
506
+ if (this.journey.suppressionPeriod) {
507
+ const suppressionDelay = generateDelay(this.journey.suppressionPeriod);
508
+ ctx.advanceTime(suppressionDelay);
509
+ }
510
+ }
511
+ };
512
+
513
+ //#endregion
514
+ export { generateDataset as a, SynodeContext as c, formatErrorMessage as d, levenshtein as f, defineDataset as i, SynodeError as l, generateDelay as n, definePersona as o, suggestClosest as p, shouldBounce as r, generatePersona as s, Engine as t, buildNotFoundSuggestion as u };
515
+ //# sourceMappingURL=engine-SRByMZvP.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine-SRByMZvP.mjs","names":["lines: string[]","bestMatch: string | undefined","parts: string[]","defaultIdGenerator: IdGenerator","idGenerator: IdGenerator","keysToDelete: string[]","dataset: Dataset<TRow>","attributes: Record<string, unknown>","rows: DatasetRow[]","row: DatasetRow","journey: Journey","events: Event[]"],"sources":["../src/errors.ts","../src/state/ids.ts","../src/state/context.ts","../src/generators/persona.ts","../src/generators/dataset.ts","../src/monitoring/timing.ts","../src/execution/engine.ts"],"sourcesContent":["/**\n * Error codes for all Synode validation and runtime errors.\n */\nexport type ErrorCode =\n | 'INVALID_BOUNCE_CHANCE'\n | 'INVALID_TIME_SPAN'\n | 'INVALID_SUPPRESSION_PERIOD'\n | 'UNKNOWN_JOURNEY_REF'\n | 'CIRCULAR_DEPENDENCY'\n | 'DUPLICATE_ID'\n | 'DATASET_NOT_FOUND'\n | 'DATASET_EMPTY'\n | 'HANDLER_ERROR'\n | 'ADAPTER_WRITE_ERROR'\n | 'INVALID_HANDLER_RETURN'\n | 'TYPO_DETECTED'\n | 'INVALID_DATASET_COUNT';\n\n/**\n * Options for constructing a SynodeError.\n *\n * @param code - The structured error code identifying the error type\n * @param message - The human-readable error message\n * @param path - Hierarchical path segments (journey, adventure, action, step)\n * @param suggestion - Optional suggestion for fixing the error\n * @param expected - Optional description of the expected value\n * @param received - Optional description of the actual value received\n * @param cause - Optional underlying error that caused this one\n */\nexport interface SynodeErrorOptions {\n code: ErrorCode;\n message: string;\n path: string[];\n suggestion?: string;\n expected?: string;\n received?: string;\n cause?: unknown;\n}\n\nconst PATH_LABELS = ['Journey', 'Adventure', 'Action', 'Step'] as const;\n\n/**\n * Formats an error message with a hierarchical path prefix and optional suggestion.\n *\n * @param message - The base error message\n * @param path - Hierarchical path segments labeled Journey/Adventure/Action/Step\n * @param suggestion - Optional suggestion appended on a new line\n * @returns The formatted message string\n */\nexport function formatErrorMessage(message: string, path: string[], suggestion?: string): string {\n let formatted = message;\n\n if (path.length > 0) {\n const segments = path.map(\n (segment, i) => `${PATH_LABELS[Math.min(i, PATH_LABELS.length - 1)]} \"${segment}\"`,\n );\n formatted = `[${segments.join(' > ')}] ${message}`;\n }\n\n if (suggestion) {\n formatted += `\\n${suggestion}`;\n }\n\n return formatted;\n}\n\n/**\n * Structured error class for all Synode validation and runtime errors.\n *\n * @example\n * ```typescript\n * throw new SynodeError({\n * code: 'INVALID_BOUNCE_CHANCE',\n * message: 'Bounce chance must be between 0 and 1',\n * path: ['Purchase Flow', 'Checkout', 'Submit Order'],\n * expected: 'number between 0 and 1',\n * received: '1.5',\n * });\n * ```\n */\nexport class SynodeError extends Error {\n /** Structured error code identifying the error type. */\n readonly code: ErrorCode;\n\n /** Hierarchical path where the error occurred. */\n readonly path: string[];\n\n /** Optional suggestion for fixing the error. */\n readonly suggestion: string | undefined;\n\n /** The original unformatted error message. */\n readonly rawMessage: string;\n\n /** Optional description of the expected value. */\n readonly expected: string | undefined;\n\n /** Optional description of the actual value received. */\n readonly received: string | undefined;\n\n constructor(options: SynodeErrorOptions) {\n const formatted = formatErrorMessage(options.message, options.path, options.suggestion);\n super(formatted, { cause: options.cause });\n this.name = 'SynodeError';\n this.code = options.code;\n this.path = options.path;\n this.suggestion = options.suggestion;\n this.rawMessage = options.message;\n this.expected = options.expected;\n this.received = options.received;\n }\n\n /**\n * Produces a structured multi-line representation of the error for human-readable output.\n *\n * Format:\n * ```\n * [ERROR_CODE] rawMessage\n * Path: Journey 'x' > Adventure 'y' > Action 'z'\n * Expected: what was expected\n * Received: what was provided\n * Fix: suggestion\n * ```\n *\n * Path is omitted when path is empty. Expected/Received are omitted when not provided.\n * Fix is omitted when there is no suggestion.\n *\n * @returns A formatted string representation of the error.\n */\n format(): string {\n const lines: string[] = [`[${this.code}] ${this.rawMessage}`];\n\n if (this.path.length > 0) {\n const segments = this.path.map(\n (segment, i) => `${PATH_LABELS[Math.min(i, PATH_LABELS.length - 1)]} '${segment}'`,\n );\n lines.push(` Path: ${segments.join(' > ')}`);\n }\n\n if (this.expected !== undefined) {\n lines.push(` Expected: ${this.expected}`);\n }\n\n if (this.received !== undefined) {\n lines.push(` Received: ${this.received}`);\n }\n\n if (this.suggestion) {\n lines.push(` Fix: ${this.suggestion}`);\n }\n\n return lines.join('\\n');\n }\n}\n\n/**\n * Computes the Levenshtein edit distance between two strings using a space-efficient\n * two-row approach.\n *\n * @param a - First string\n * @param b - Second string\n * @returns The minimum number of single-character edits (insertions, deletions, substitutions)\n */\nexport function levenshtein(a: string, b: string): number {\n if (a === b) return 0;\n if (a.length === 0) return b.length;\n if (b.length === 0) return a.length;\n\n // Two-row approach: only keep previous and current rows\n let prev = new Array<number>(b.length + 1);\n let curr = new Array<number>(b.length + 1);\n\n for (let j = 0; j <= b.length; j++) {\n prev[j] = j;\n }\n\n for (let i = 1; i <= a.length; i++) {\n curr[0] = i;\n for (let j = 1; j <= b.length; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n curr[j] = Math.min(\n prev[j] + 1, // deletion\n curr[j - 1] + 1, // insertion\n prev[j - 1] + cost, // substitution\n );\n }\n [prev, curr] = [curr, prev];\n }\n\n return prev[b.length];\n}\n\n/**\n * Finds the closest matching string from a list of candidates using Levenshtein distance.\n * Returns undefined if no candidate is within the dynamic threshold.\n *\n * The threshold is `max(3, floor(maxLen * 0.4))` where maxLen is the longer of the\n * input and candidate strings.\n *\n * @param input - The string to match against candidates\n * @param candidates - List of valid strings to compare against\n * @returns The closest matching candidate, or undefined if none is close enough\n */\nexport function suggestClosest(input: string, candidates: string[]): string | undefined {\n if (candidates.length === 0) return undefined;\n\n let bestMatch: string | undefined;\n let bestDistance = Infinity;\n\n for (const candidate of candidates) {\n const distance = levenshtein(input, candidate);\n const maxLen = Math.max(input.length, candidate.length);\n const threshold = Math.max(3, Math.floor(maxLen * 0.4));\n\n if (distance <= threshold && distance < bestDistance) {\n bestDistance = distance;\n bestMatch = candidate;\n }\n }\n\n return bestMatch;\n}\n\n/**\n * Builds a human-readable suggestion string for a not-found error, including\n * the closest match (if any) and the full list of available options.\n *\n * @param kind - The kind of entity (e.g., \"dataset\", \"journey\")\n * @param requested - The name that was requested but not found\n * @param available - List of valid names\n * @returns A suggestion string with \"Did you mean...\" and/or available options\n */\nexport function buildNotFoundSuggestion(\n kind: string,\n requested: string,\n available: string[],\n): string {\n const closest = suggestClosest(requested, available);\n const parts: string[] = [];\n\n if (closest) {\n parts.push(`Did you mean '${closest}'?`);\n }\n\n parts.push(`Available ${kind}s: [${available.join(', ')}]`);\n\n return parts.join(' ');\n}\n","/**\n * Interface for ID generation strategies.\n */\nexport interface IdGenerator {\n generate(prefix?: string): string;\n}\n\n/**\n * Default ID generator using crypto.randomUUID.\n */\nexport const defaultIdGenerator: IdGenerator = {\n generate(prefix?: string) {\n const uuid = crypto.randomUUID();\n return prefix ? `${prefix}_${uuid}` : uuid;\n },\n};\n","import { Faker, allLocales } from '@faker-js/faker';\nimport {\n Context,\n Dataset,\n DatasetHandle,\n DatasetRow,\n ContextScope,\n ContextSetOptions,\n} from '../types.js';\nimport { SynodeError, buildNotFoundSuggestion } from '../errors.js';\nimport { IdGenerator, defaultIdGenerator } from './ids.js';\n\n/**\n * Internal metadata for scoped context fields.\n */\ninterface FieldMetadata {\n scope?: ContextScope;\n}\n\nexport class SynodeContext implements Context {\n private state = new Map<string, unknown>();\n private fieldMetadata = new Map<string, FieldMetadata>();\n private currentTime: Date;\n private _userId: string;\n private _sessionId: string;\n private _faker: Faker;\n private completedJourneys = new Set<string>();\n private datasets = new Map<string, Dataset>();\n\n constructor(\n startTime: Date = new Date(),\n private idGenerator: IdGenerator = defaultIdGenerator,\n public readonly locale = 'en',\n ) {\n this.currentTime = new Date(startTime);\n this._userId = idGenerator.generate('user');\n this._sessionId = idGenerator.generate('session');\n\n // Initialize localized faker\n const localeDef = allLocales[locale as keyof typeof allLocales];\n this._faker = new Faker({ locale: localeDef });\n }\n\n get faker(): Faker {\n return this._faker;\n }\n\n get userId(): string {\n return this._userId;\n }\n\n get sessionId(): string {\n return this._sessionId;\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters\n get<T>(key: string): T | undefined {\n return this.state.get(key) as T | undefined;\n }\n\n /**\n * Set a context field value with optional scope for automatic cleanup.\n *\n * @param key - The field name\n * @param value - The field value\n * @param options - Optional settings including lifecycle scope\n *\n * @remarks\n * When calling `set()` multiple times on the same field with different scopes,\n * the most recent scope setting will be used. For example, if a field is first\n * set with `scope: 'adventure'` and later set again with `scope: 'action'`,\n * it will be cleared at the end of the action rather than the adventure.\n *\n * @example\n * ```typescript\n * ctx.set('cartItems', [], { scope: 'adventure' });\n * // Later in the same adventure...\n * ctx.set('cartItems', ['item1'], { scope: 'action' }); // Now scoped to action\n * ```\n */\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters\n set<T>(key: string, value: T, options?: ContextSetOptions): void {\n this.state.set(key, value);\n if (options?.scope) {\n this.fieldMetadata.set(key, { scope: options.scope });\n } else {\n // If no scope is set, remove any existing metadata\n this.fieldMetadata.delete(key);\n }\n }\n\n now(): Date {\n return new Date(this.currentTime);\n }\n\n generateId(prefix?: string): string {\n return this.idGenerator.generate(prefix);\n }\n\n hasCompletedJourney(journeyId: string): boolean {\n return this.completedJourneys.has(journeyId);\n }\n\n markJourneyComplete(journeyId: string): void {\n this.completedJourneys.add(journeyId);\n }\n\n dataset(id: string): DatasetHandle {\n const ds = this.datasets.get(id);\n if (!ds) {\n const available = [...this.datasets.keys()];\n const suggestion =\n available.length > 0 ? buildNotFoundSuggestion('dataset', id, available) : undefined;\n throw new SynodeError({\n code: 'DATASET_NOT_FOUND',\n message: `Dataset '${id}' not found in context`,\n path: [],\n suggestion,\n });\n }\n return new DatasetHandleImpl(ds);\n }\n\n typedDataset<TRow extends DatasetRow>(id: string): DatasetHandle<TRow> {\n const ds = this.datasets.get(id);\n if (!ds) {\n const available = [...this.datasets.keys()];\n const suggestion =\n available.length > 0 ? buildNotFoundSuggestion('dataset', id, available) : undefined;\n throw new SynodeError({\n code: 'DATASET_NOT_FOUND',\n message: `Dataset '${id}' not found in context`,\n path: [],\n suggestion,\n });\n }\n return new DatasetHandleImpl<TRow>(ds as Dataset<TRow>);\n }\n\n /**\n * Internal: Register a dataset for use in this context.\n */\n registerDataset(dataset: Dataset): void {\n this.datasets.set(dataset.id, dataset);\n }\n\n /**\n * Internal: Advance the simulation time.\n */\n advanceTime(ms: number): void {\n this.currentTime = new Date(this.currentTime.getTime() + ms);\n }\n\n /**\n * Internal: Rotate the session ID.\n */\n rotateSession(): void {\n this._sessionId = this.idGenerator.generate('session');\n }\n\n /**\n * Internal: Clear all fields with the specified scope.\n * Called by the engine when a scope ends.\n */\n clearScope(scope: ContextScope): void {\n const keysToDelete: string[] = [];\n for (const [key, metadata] of this.fieldMetadata.entries()) {\n if (metadata.scope === scope) {\n keysToDelete.push(key);\n }\n }\n for (const key of keysToDelete) {\n this.state.delete(key);\n this.fieldMetadata.delete(key);\n }\n }\n}\n\n/**\n * Internal implementation of DatasetHandle.\n */\nclass DatasetHandleImpl<TRow extends DatasetRow = DatasetRow> implements DatasetHandle<TRow> {\n constructor(private dataset: Dataset<TRow>) {}\n\n randomRow(): TRow {\n if (this.dataset.rows.length === 0) {\n throw new Error(`Dataset '${this.dataset.id}' is empty.`);\n }\n const index = Math.floor(Math.random() * this.dataset.rows.length);\n return this.dataset.rows[index];\n }\n\n getRowById(id: string | number): TRow | undefined {\n // Find row by searching all fields for a matching value\n return this.dataset.rows.find((row) => {\n for (const key of Object.keys(row)) {\n if ((row as Record<string, unknown>)[key] === id) {\n return true;\n }\n }\n return false;\n });\n }\n\n getRowByIndex(index: number): TRow | undefined {\n return this.dataset.rows[index];\n }\n\n getAllRows(): TRow[] {\n return [...this.dataset.rows];\n }\n\n size(): number {\n return this.dataset.rows.length;\n }\n}\n","import { Context, FieldGenerator } from '../types.js';\n\n/**\n * Configuration for a user persona.\n */\nexport interface PersonaDefinition {\n id: string;\n name: string;\n /**\n * Attributes to initialize in the user's context.\n * Can include 'locale' to set the faker locale.\n */\n attributes: Record<string, FieldGenerator>;\n}\n\n/**\n * Represents an instantiated persona.\n */\nexport interface Persona {\n id: string;\n name: string;\n attributes: Record<string, unknown>;\n}\n\n/**\n * Defines a new persona.\n */\nexport function definePersona(config: PersonaDefinition): PersonaDefinition {\n return config;\n}\n\n/**\n * Generates a concrete persona instance from a definition.\n * This resolves all dynamic fields and weighted distributions.\n */\nexport async function generatePersona(\n definition: PersonaDefinition,\n baseContext: Context,\n): Promise<Persona> {\n const attributes: Record<string, unknown> = {};\n\n for (const [key, generator] of Object.entries(definition.attributes)) {\n if (typeof generator === 'function') {\n // Execute generator with base context (which might have default locale)\n const genFn = generator as (\n context: Context,\n payload: Record<string, unknown>,\n ) => Promise<unknown>;\n attributes[key] = await genFn(baseContext, attributes);\n } else {\n attributes[key] = generator;\n }\n }\n\n return {\n id: definition.id,\n name: definition.name,\n attributes,\n };\n}\n","import { Dataset, DatasetDefinition, DatasetRow, Context } from '../types.js';\n\n/**\n * Defines a new dataset with type inference support.\n *\n * @example\n * ```typescript\n * const products = defineDataset({\n * id: 'products',\n * name: 'Products',\n * count: 100,\n * fields: {\n * id: (ctx, row) => `prod-${row.index}`,\n * name: (ctx) => ctx.faker.commerce.productName(),\n * price: (ctx) => ctx.faker.number.float({ min: 10, max: 500 }),\n * },\n * });\n *\n * // Type inference works automatically\n * type Product = InferDatasetRow<typeof products>;\n * ```\n */\nexport function defineDataset<TFields extends Record<string, unknown>>(\n config: DatasetDefinition<TFields>,\n): DatasetDefinition<TFields> {\n return config;\n}\n\n/**\n * Generates a concrete dataset from a definition.\n * @param definition The dataset definition.\n * @param context Context providing access to faker, other datasets, and user data.\n */\nexport async function generateDataset<TFields extends Record<string, unknown>>(\n definition: DatasetDefinition<TFields>,\n context: Context,\n): Promise<Dataset> {\n if (!Number.isFinite(definition.count) || definition.count < 0 || definition.count > 10_000_000) {\n throw new Error(\n `Dataset '${definition.id}' count must be a finite number between 0 and 10,000,000, got ${String(definition.count)}`,\n );\n }\n\n const rows: DatasetRow[] = [];\n\n for (let index = 0; index < definition.count; index++) {\n const row: DatasetRow = {};\n\n for (const [key, generator] of Object.entries(definition.fields)) {\n if (typeof generator === 'function') {\n // Execute generator with context and row metadata\n const genFn = generator as (\n context: Context,\n rowMeta: { index: number; data: DatasetRow },\n ) => Promise<unknown>;\n row[key] = await genFn(context, { index, data: row });\n } else {\n // Use static value\n row[key] = generator;\n }\n }\n\n rows.push(row);\n }\n\n return {\n id: definition.id,\n name: definition.name,\n rows,\n };\n}\n","import { TimeSpan } from '../types.js';\n\n/**\n * Generates a delay in milliseconds based on a time span configuration.\n */\nexport function generateDelay(timeSpan: TimeSpan): number {\n const { min, max, distribution = 'uniform' } = timeSpan;\n\n switch (distribution) {\n case 'uniform':\n return min + Math.random() * (max - min);\n\n case 'gaussian': {\n // Box-Muller transform for Gaussian distribution\n const mean = (min + max) / 2;\n const stdDev = (max - min) / 6; // 99.7% within range\n const u1 = Math.random();\n const u2 = Math.random();\n const z0 = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2);\n const value = mean + z0 * stdDev;\n // Clamp to min/max\n return Math.max(min, Math.min(max, value));\n }\n\n case 'exponential': {\n // Exponential distribution (more shorter delays, fewer long delays)\n const lambda = 1 / (max - min);\n const value = min - Math.log(1 - Math.random()) / lambda;\n return Math.min(max, value);\n }\n\n default:\n return min + Math.random() * (max - min);\n }\n}\n\n/**\n * Checks if a bounce should occur based on probability.\n */\nexport function shouldBounce(bounceChance?: number): boolean {\n if (!bounceChance) return false;\n return Math.random() < bounceChance;\n}\n","import { Journey, Event } from '../types.js';\nimport { SynodeContext } from '../state/context.js';\nimport { SynodeError } from '../errors.js';\nimport { generateDelay, shouldBounce } from '../monitoring/timing.js';\n\nexport class Engine {\n constructor(private journey: Journey) {}\n\n /**\n * Executes the journey and yields events.\n * @param context Optional context to use. If not provided, a new one is created.\n */\n async *run(context?: SynodeContext): AsyncGenerator<Event> {\n // Initialize context for this run (user) if not provided\n const ctx = context ?? new SynodeContext();\n\n // Rotate session ID for this journey run\n ctx.rotateSession();\n\n // Check journey prerequisites\n if (this.journey.requires) {\n for (const requiredJourneyId of this.journey.requires) {\n if (!ctx.hasCompletedJourney(requiredJourneyId)) {\n // Prerequisites not met, skip this journey\n return;\n }\n }\n }\n\n // Check if journey should bounce before starting\n if (shouldBounce(this.journey.bounceChance)) {\n // Apply suppression period and return without generating events\n if (this.journey.suppressionPeriod) {\n const suppressionDelay = generateDelay(this.journey.suppressionPeriod);\n ctx.advanceTime(suppressionDelay);\n }\n return;\n }\n\n // Execute adventures in sequence\n for (const adventure of this.journey.adventures) {\n // Check adventure bounce\n if (shouldBounce(adventure.bounceChance)) {\n if (adventure.onBounce === 'skip') {\n // Clear adventure-scoped fields before skipping\n ctx.clearScope('adventure');\n // Skip to next adventure\n continue;\n } else {\n // Clear adventure-scoped fields before stopping\n ctx.clearScope('adventure');\n // Stop the entire journey\n break;\n }\n }\n\n // Execute actions in sequence\n for (const action of adventure.actions) {\n // Check action bounce\n if (shouldBounce(action.bounceChance)) {\n // Clear action-scoped fields before breaking\n ctx.clearScope('action');\n break; // Stop this adventure\n }\n\n // Apply delay before action (from adventure's timeSpan)\n if (adventure.timeSpan) {\n const delay = generateDelay(adventure.timeSpan);\n ctx.advanceTime(delay);\n }\n\n // Execute the action handler to get events\n const actionPath = [this.journey.id, adventure.id, action.id];\n let events: Event[];\n try {\n events = await action.handler(ctx);\n } catch (err) {\n if (err instanceof SynodeError) {\n throw err;\n }\n const errorName = err instanceof Error ? err.constructor.name : 'UnknownError';\n const errorMessage = err instanceof Error ? err.message : String(err);\n throw new SynodeError({\n code: 'HANDLER_ERROR',\n message: `${errorName}: ${errorMessage} (userId: ${ctx.userId}, sessionId: ${ctx.sessionId})`,\n path: actionPath,\n cause: err,\n });\n }\n\n if (!Array.isArray(events)) {\n throw new SynodeError({\n code: 'INVALID_HANDLER_RETURN',\n message: `Action handler must return an array of Events, got ${typeof events}`,\n path: actionPath,\n });\n }\n\n // Yield events with timing between them\n for (let i = 0; i < events.length; i++) {\n if (i > 0 && action.timeSpan) {\n // Apply delay between events within the same action\n const delay = generateDelay(action.timeSpan);\n ctx.advanceTime(delay);\n // Update event timestamp\n events[i].timestamp = ctx.now();\n }\n yield events[i];\n }\n\n // Clear action-scoped fields after action completes\n ctx.clearScope('action');\n }\n\n // Clear adventure-scoped fields after adventure completes\n ctx.clearScope('adventure');\n }\n\n // Mark journey as complete\n ctx.markJourneyComplete(this.journey.id);\n\n // Clear journey-scoped fields after journey completes\n ctx.clearScope('journey');\n\n // Apply suppression period after completion\n if (this.journey.suppressionPeriod) {\n const suppressionDelay = generateDelay(this.journey.suppressionPeriod);\n ctx.advanceTime(suppressionDelay);\n }\n }\n}\n"],"mappings":"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"}