@ak--47/dungeon-master 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.
Files changed (66) hide show
  1. package/README.md +518 -0
  2. package/dungeons/array-of-object-lookup-schema.json +327 -0
  3. package/dungeons/array-of-object-lookup.js +220 -0
  4. package/dungeons/ecommerce-schema.json +462 -0
  5. package/dungeons/ecommerce.js +447 -0
  6. package/dungeons/education-schema.json +2409 -0
  7. package/dungeons/education.js +768 -0
  8. package/dungeons/fintech-schema.json +14034 -0
  9. package/dungeons/fintech.js +696 -0
  10. package/dungeons/foobar-schema.json +403 -0
  11. package/dungeons/foobar.js +296 -0
  12. package/dungeons/food-delivery-schema.json +192 -0
  13. package/dungeons/food-delivery.js +602 -0
  14. package/dungeons/food-schema.json +1152 -0
  15. package/dungeons/food.js +754 -0
  16. package/dungeons/gaming-schema.json +1270 -0
  17. package/dungeons/gaming.js +508 -0
  18. package/dungeons/insurance-application-schema.json +204 -0
  19. package/dungeons/insurance-application.js +605 -0
  20. package/dungeons/media-schema.json +906 -0
  21. package/dungeons/media.js +790 -0
  22. package/dungeons/retention-cadence-schema.json +78 -0
  23. package/dungeons/retention-cadence.js +244 -0
  24. package/dungeons/rpg-schema.json +4526 -0
  25. package/dungeons/rpg.js +919 -0
  26. package/dungeons/sanity-schema.json +255 -0
  27. package/dungeons/sanity.js +152 -0
  28. package/dungeons/sass-schema.json +1291 -0
  29. package/dungeons/sass.js +795 -0
  30. package/dungeons/scd-schema.json +919 -0
  31. package/dungeons/scd.js +277 -0
  32. package/dungeons/simple-schema.json +608 -0
  33. package/dungeons/simple.js +285 -0
  34. package/dungeons/simplest-schema.json +1418 -0
  35. package/dungeons/simplest.js +392 -0
  36. package/dungeons/social-schema.json +1118 -0
  37. package/dungeons/social.js +686 -0
  38. package/dungeons/text-generation-schema.json +3096 -0
  39. package/dungeons/text-generation.js +812 -0
  40. package/index.js +567 -0
  41. package/lib/core/config-validator.js +395 -0
  42. package/lib/core/context.js +204 -0
  43. package/lib/core/dungeon-loader.js +337 -0
  44. package/lib/core/storage.js +379 -0
  45. package/lib/generators/adspend.js +132 -0
  46. package/lib/generators/events.js +271 -0
  47. package/lib/generators/funnels.js +407 -0
  48. package/lib/generators/mirror.js +167 -0
  49. package/lib/generators/product-lookup.js +262 -0
  50. package/lib/generators/product-names.js +195 -0
  51. package/lib/generators/profiles.js +93 -0
  52. package/lib/generators/scd.js +124 -0
  53. package/lib/generators/text.js +1192 -0
  54. package/lib/orchestrators/mixpanel-sender.js +266 -0
  55. package/lib/orchestrators/user-loop.js +335 -0
  56. package/lib/templates/abbreviated.d.ts +169 -0
  57. package/lib/templates/defaults.js +1405 -0
  58. package/lib/templates/phrases.js +2526 -0
  59. package/lib/templates/schema.d.ts +173 -0
  60. package/lib/templates/soup-presets.js +188 -0
  61. package/lib/utils/function-registry.js +302 -0
  62. package/lib/utils/json-evaluator.js +172 -0
  63. package/lib/utils/logger.js +34 -0
  64. package/lib/utils/utils.js +1490 -0
  65. package/package.json +89 -0
  66. package/types.d.ts +865 -0
@@ -0,0 +1,173 @@
1
+ /**
2
+ * A "ValueValid" can be:
3
+ * - A primitive value (string, number, boolean)
4
+ * - An array of primitives (the system picks one randomly)
5
+ * - A function call object: { "functionName": "...", "args": [...] }
6
+ * - An arrow function object: { "functionName": "arrow", "body": "..." }
7
+ *
8
+ * This is the building block for all property values in the dungeon.
9
+ */
10
+ type Primitives = string | number | boolean;
11
+ type FunctionCall = { functionName: string; args?: any[]; body?: string };
12
+ type ValueValid = Primitives | Primitives[] | FunctionCall;
13
+
14
+
15
+ /**
16
+ * The main configuration object for the entire data generation spec, known as a "Dungeon".
17
+ * This is the high-level object you will be constructing.
18
+ *
19
+ * REQUIRED fields: events, funnels, superProps, userProps
20
+ * OPTIONAL fields: scdProps, groupKeys, groupProps, groupEvents
21
+ */
22
+ export interface Dungeon {
23
+ /** REQUIRED: A list of all possible events that can occur in the simulation. */
24
+ events: EventConfig[];
25
+
26
+ /** REQUIRED: A list of event sequences that represent user journeys (e.g., sign-up, purchase). */
27
+ funnels: Funnel[];
28
+
29
+ /** REQUIRED: Properties that are attached to every event for all users. */
30
+ superProps: Record<string, ValueValid>;
31
+
32
+ /** REQUIRED: Properties that define the characteristics of individual users. */
33
+ userProps: Record<string, ValueValid>;
34
+
35
+ /** OPTIONAL: Properties that change for users or groups over time (Slowly Changing Dimensions). Only include when properties explicitly change over time. */
36
+ scdProps?: Record<string, SCDProp>;
37
+
38
+ /** OPTIONAL: Defines group entities (companies, teams). Format: [["group_key", count], ...]. ONLY for B2B/SaaS scenarios. */
39
+ groupKeys?: [string, number][];
40
+
41
+ /** OPTIONAL: Properties for groups defined in groupKeys. ONLY include if groupKeys is defined. */
42
+ groupProps?: Record<string, Record<string, ValueValid>>;
43
+
44
+ /** OPTIONAL: Events attributed to groups on a schedule (e.g., monthly billing). Rarely needed. */
45
+ groupEvents?: GroupEventConfig[];
46
+ }
47
+
48
+
49
+ /**
50
+ * Defines a single event, its properties, and its likelihood of occurring.
51
+ *
52
+ * The "weight" determines relative frequency - an event with weight 10 occurs
53
+ * roughly 10x more often than an event with weight 1.
54
+ */
55
+ interface EventConfig {
56
+ /** REQUIRED: The name of the event (e.g., "Page View", "Add to Cart", "checkout"). */
57
+ event: string;
58
+
59
+ /** OPTIONAL: The relative frequency of this event. Higher numbers = more frequent. Default: 1 */
60
+ weight?: number;
61
+
62
+ /** OPTIONAL: Properties associated with this event. Each property can be a value or array. */
63
+ properties?: Record<string, ValueValid>;
64
+
65
+ /** OPTIONAL: If true, this event will be the first event for a new user (e.g., "sign up"). Only one event should have this. */
66
+ isFirstEvent?: boolean;
67
+
68
+ /** OPTIONAL: If true, this event signifies that a user has churned (e.g., "account deleted"). */
69
+ isChurnEvent?: boolean;
70
+ }
71
+
72
+
73
+ /**
74
+ * Defines a sequence of events that represents a meaningful user journey or workflow.
75
+ *
76
+ * Funnels model how users progress through your product - from sign-up to purchase,
77
+ * from onboarding to activation, etc. The conversionRate determines what percentage
78
+ * of users who start the funnel will complete it.
79
+ */
80
+ interface Funnel {
81
+ /** REQUIRED: Event names that make up this journey. Must match event names in the events array. */
82
+ sequence: string[];
83
+
84
+ /** REQUIRED: Percentage (0-100) of users who complete the funnel. 15 means 15% conversion. */
85
+ conversionRate: number;
86
+
87
+ /** OPTIONAL: The name of the funnel (e.g., "Purchase Funnel", "Onboarding Flow"). */
88
+ name?: string;
89
+
90
+ /** OPTIONAL: The likelihood that a user will attempt this funnel vs others. Default: 1 */
91
+ weight?: number;
92
+
93
+ /** OPTIONAL: If true, this is an initial user experience funnel (e.g., onboarding). */
94
+ isFirstFunnel?: boolean;
95
+
96
+ /** OPTIONAL: Average hours to complete the funnel. Default: 1 */
97
+ timeToConvert?: number;
98
+
99
+ /**
100
+ * OPTIONAL: How events are ordered within the funnel.
101
+ * - "sequential" (default): Events happen in exact order
102
+ * - "random": Events can happen in any order
103
+ * - "first-fixed": First event is fixed, rest are random
104
+ * - "last-fixed": Last event is fixed, rest are random
105
+ * - "first-and-last-fixed": First and last are fixed, middle is random
106
+ */
107
+ order?: "sequential" | "random" | "first-fixed" | "last-fixed" | "first-and-last-fixed";
108
+
109
+ /** OPTIONAL: Properties attached to every event in this funnel (e.g., experiment_variant, traffic_source). */
110
+ props?: Record<string, ValueValid>;
111
+
112
+ /** OPTIONAL: User property conditions for eligibility. Only users matching these values run this funnel. */
113
+ conditions?: Record<string, ValueValid>;
114
+
115
+ /** OPTIONAL: If true, generates 3 variants with different conversion rates for A/B testing analysis. */
116
+ experiment?: boolean;
117
+ }
118
+
119
+
120
+ /**
121
+ * Defines a "Slowly Changing Dimension" - a property of a user or group
122
+ * that changes periodically over time (e.g., subscription plan, user role).
123
+ *
124
+ * ONLY include SCDs when properties explicitly need to change over time.
125
+ * For static properties, just use userProps or groupProps.
126
+ */
127
+ interface SCDProp {
128
+ /** OPTIONAL: The entity type - 'user' or a group key like 'company_id'. Default: 'user' */
129
+ type?: "user" | string;
130
+
131
+ /** REQUIRED: How often this property can change. */
132
+ frequency: "day" | "week" | "month" | "year";
133
+
134
+ /** REQUIRED: Possible values for this property. */
135
+ values: ValueValid;
136
+
137
+ /**
138
+ * REQUIRED: When changes occur.
139
+ * - "fixed": Changes occur exactly on the frequency interval
140
+ * - "fuzzy": Changes occur randomly around the interval
141
+ */
142
+ timing: "fixed" | "fuzzy";
143
+
144
+ /** OPTIONAL: Maximum number of times this property can change per entity. Default: 100 */
145
+ max?: number;
146
+ }
147
+
148
+
149
+ /**
150
+ * Defines an event attributed to a group on a regular schedule.
151
+ * Example: monthly subscription charges, weekly reports, etc.
152
+ *
153
+ * This is rarely needed - only use for B2B scenarios with recurring group-level events.
154
+ */
155
+ interface GroupEventConfig {
156
+ /** REQUIRED: The name of the event. */
157
+ event: string;
158
+
159
+ /** REQUIRED: How often the event occurs (in days). e.g., 30 for monthly. */
160
+ frequency: number;
161
+
162
+ /** REQUIRED: The group key this event belongs to (e.g., "company_id"). */
163
+ group_key: string;
164
+
165
+ /** OPTIONAL: If true, a random user in the group is also attributed to the event. */
166
+ attribute_to_user?: boolean;
167
+
168
+ /** OPTIONAL: Properties for this event. */
169
+ properties?: Record<string, ValueValid>;
170
+
171
+ /** OPTIONAL: Relative frequency of this event. */
172
+ weight?: number;
173
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * TimeSoup preset configurations
3
+ * Each preset defines time distribution parameters that produce distinct patterns.
4
+ *
5
+ * Parameters:
6
+ * - peaks(numDays): function returning number of Gaussian clusters
7
+ * - deviation: controls peak width (higher = tighter)
8
+ * - mean: offset from chunk center (0 = centered)
9
+ * - dayOfWeekWeights: 7-element array [Sun..Sat], max=1.0, null to disable
10
+ * - hourOfDayWeights: 24-element array [0h..23h UTC], max=1.0, null to disable
11
+ *
12
+ * Some presets also suggest bornRecentBias and percentUsersBornInDataset,
13
+ * but those are top-level dungeon config — presets only set them if not already specified.
14
+ */
15
+
16
+ // Real-world Mixpanel DOW pattern: weekday-heavy, Saturday valley
17
+ export const REAL_DOW = [0.637, 1.0, 0.999, 0.998, 0.966, 0.802, 0.528];
18
+
19
+ // Real-world Mixpanel HOD pattern: early-morning peak (UTC), afternoon valley
20
+ export const REAL_HOD = [
21
+ 0.949, 0.992, 0.998, 0.946, 0.895, 0.938, 1.0, 0.997,
22
+ 0.938, 0.894, 0.827, 0.786, 0.726, 0.699, 0.688, 0.643,
23
+ 0.584, 0.574, 0.554, 0.576, 0.604, 0.655, 0.722, 0.816
24
+ ];
25
+
26
+ // Flat weights (no cyclical pattern)
27
+ export const FLAT_DOW = [1, 1, 1, 1, 1, 1, 1];
28
+ export const FLAT_HOD = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
29
+
30
+ /** @type {Record<string, {peaks: (numDays: number) => number, deviation: number, mean: number, dayOfWeekWeights: number[]|null, hourOfDayWeights: number[]|null, bornRecentBias?: number, percentUsersBornInDataset?: number}>} */
31
+ export const SOUP_PRESETS = {
32
+ /**
33
+ * steady — Mature SaaS / Stable Product
34
+ * Nearly flat day-over-day, slight weekly pattern, minimal growth trend.
35
+ */
36
+ steady: {
37
+ peaks: (numDays) => Math.max(5, numDays * 2),
38
+ deviation: 1.5,
39
+ mean: 0,
40
+ dayOfWeekWeights: REAL_DOW,
41
+ hourOfDayWeights: REAL_HOD,
42
+ bornRecentBias: 0.1,
43
+ percentUsersBornInDataset: 10,
44
+ },
45
+
46
+ /**
47
+ * growth — Growing Startup (DEFAULT)
48
+ * Gradual uptrend with visible weekly peaks. This is the default behavior.
49
+ */
50
+ growth: {
51
+ peaks: (numDays) => Math.max(5, numDays * 2),
52
+ deviation: 2,
53
+ mean: 0,
54
+ dayOfWeekWeights: REAL_DOW,
55
+ hourOfDayWeights: REAL_HOD,
56
+ bornRecentBias: 0.3,
57
+ percentUsersBornInDataset: 15,
58
+ },
59
+
60
+ /**
61
+ * spiky — Event-Driven / Bursty
62
+ * Clear peaks and valleys, dramatic variation. Fewer Gaussian clusters + tight deviation.
63
+ */
64
+ spiky: {
65
+ peaks: (numDays) => Math.max(5, Math.ceil(numDays / 10)),
66
+ deviation: 3.5,
67
+ mean: 0,
68
+ dayOfWeekWeights: REAL_DOW,
69
+ hourOfDayWeights: REAL_HOD,
70
+ bornRecentBias: 0.3,
71
+ percentUsersBornInDataset: 20,
72
+ },
73
+
74
+ /**
75
+ * seasonal — Strong Cyclical Patterns
76
+ * 3-4 major waves across the dataset. Very few peaks create dramatic macro trends.
77
+ */
78
+ seasonal: {
79
+ peaks: () => 4,
80
+ deviation: 2.5,
81
+ mean: 0,
82
+ dayOfWeekWeights: REAL_DOW,
83
+ hourOfDayWeights: REAL_HOD,
84
+ bornRecentBias: 0.2,
85
+ percentUsersBornInDataset: 25,
86
+ },
87
+
88
+ /**
89
+ * global — Distributed Users Across Timezones
90
+ * Very flat hourly + daily distribution. No cyclical patterns.
91
+ */
92
+ global: {
93
+ peaks: (numDays) => Math.max(5, numDays * 2),
94
+ deviation: 1,
95
+ mean: 0,
96
+ dayOfWeekWeights: FLAT_DOW,
97
+ hourOfDayWeights: FLAT_HOD,
98
+ bornRecentBias: 0,
99
+ percentUsersBornInDataset: 10,
100
+ },
101
+
102
+ /**
103
+ * churny — High Churn / Declining Product
104
+ * Flat distribution (no growth trend). All users pre-exist the dataset,
105
+ * so there's no acceleration. Combine with an "everything" hook that
106
+ * filters late events to create a true declining shape.
107
+ */
108
+ churny: {
109
+ peaks: (numDays) => Math.max(5, numDays * 2),
110
+ deviation: 2,
111
+ mean: 0,
112
+ dayOfWeekWeights: REAL_DOW,
113
+ hourOfDayWeights: REAL_HOD,
114
+ bornRecentBias: 0,
115
+ percentUsersBornInDataset: 5,
116
+ },
117
+
118
+ /**
119
+ * chaotic — Unpredictable / Irregular Patterns
120
+ * Few peaks + very tight clustering = dramatic bursts separated by quiet stretches.
121
+ */
122
+ chaotic: {
123
+ peaks: (numDays) => Math.max(3, Math.ceil(numDays / 20)),
124
+ deviation: 4,
125
+ mean: 0,
126
+ dayOfWeekWeights: REAL_DOW,
127
+ hourOfDayWeights: REAL_HOD,
128
+ bornRecentBias: 0.5,
129
+ percentUsersBornInDataset: 40,
130
+ },
131
+ };
132
+
133
+ /** @type {string[]} */
134
+ export const PRESET_NAMES = Object.keys(SOUP_PRESETS);
135
+
136
+ /**
137
+ * Resolves a soup config — handles string presets, preset+overrides, and raw objects.
138
+ * @param {string | object} soup - Soup config from dungeon
139
+ * @param {number} numDays - Number of days in the dataset
140
+ * @returns {{ soup: object, suggestedBornRecentBias?: number, suggestedPercentUsersBornInDataset?: number }}
141
+ */
142
+ export function resolveSoup(soup, numDays) {
143
+ if (!soup) return { soup: {} };
144
+
145
+ // String preset: "growth", "spiky", etc.
146
+ if (typeof soup === 'string') {
147
+ const preset = SOUP_PRESETS[soup];
148
+ if (!preset) {
149
+ throw new Error(`Unknown soup preset: "${soup}". Valid presets: ${PRESET_NAMES.join(', ')}`);
150
+ }
151
+ return {
152
+ soup: {
153
+ peaks: preset.peaks(numDays),
154
+ deviation: preset.deviation,
155
+ mean: preset.mean,
156
+ dayOfWeekWeights: preset.dayOfWeekWeights,
157
+ hourOfDayWeights: preset.hourOfDayWeights,
158
+ },
159
+ suggestedBornRecentBias: preset.bornRecentBias,
160
+ suggestedPercentUsersBornInDataset: preset.percentUsersBornInDataset,
161
+ };
162
+ }
163
+
164
+ // Object with preset key: { preset: "growth", deviation: 3 }
165
+ if (typeof soup === 'object' && soup.preset) {
166
+ const preset = SOUP_PRESETS[soup.preset];
167
+ if (!preset) {
168
+ throw new Error(`Unknown soup preset: "${soup.preset}". Valid presets: ${PRESET_NAMES.join(', ')}`);
169
+ }
170
+ const base = {
171
+ peaks: preset.peaks(numDays),
172
+ deviation: preset.deviation,
173
+ mean: preset.mean,
174
+ dayOfWeekWeights: preset.dayOfWeekWeights,
175
+ hourOfDayWeights: preset.hourOfDayWeights,
176
+ };
177
+ // Apply overrides (excluding the 'preset' key itself)
178
+ const { preset: _, ...overrides } = soup;
179
+ return {
180
+ soup: { ...base, ...overrides },
181
+ suggestedBornRecentBias: preset.bornRecentBias,
182
+ suggestedPercentUsersBornInDataset: preset.percentUsersBornInDataset,
183
+ };
184
+ }
185
+
186
+ // Raw object: { peaks: 10, deviation: 2 } — pass through unchanged
187
+ return { soup };
188
+ }
@@ -0,0 +1,302 @@
1
+ /**
2
+ * Registry of valid functions that can be called in dungeon configurations
3
+ * This replaces the old string-based function parsing with a clean JSON structure
4
+ */
5
+
6
+ import { dataLogger as logger } from './logger.js';
7
+
8
+ export const FUNCTION_REGISTRY = {
9
+ // Utility functions from utils.js
10
+ weighNumRange: {
11
+ minArgs: 2,
12
+ maxArgs: 3,
13
+ description: 'Generate weighted random number in range'
14
+ },
15
+ range: {
16
+ minArgs: 2,
17
+ maxArgs: 2,
18
+ description: 'Generate array of numbers in range'
19
+ },
20
+ date: {
21
+ minArgs: 1,
22
+ maxArgs: 3,
23
+ description: 'Generate date string'
24
+ },
25
+ choose: {
26
+ minArgs: 1,
27
+ maxArgs: 1,
28
+ description: 'Choose from array'
29
+ },
30
+ integer: {
31
+ minArgs: 1,
32
+ maxArgs: 2,
33
+ description: 'Generate random integer'
34
+ },
35
+ exhaust: {
36
+ minArgs: 1,
37
+ maxArgs: 1,
38
+ description: 'Exhaust values from array'
39
+ },
40
+
41
+ // Additional utility functions
42
+ maybe: {
43
+ minArgs: 1,
44
+ maxArgs: 2,
45
+ description: 'Return value or null based on probability'
46
+ },
47
+ takeSome: {
48
+ minArgs: 1,
49
+ maxArgs: 3,
50
+ description: 'Take random subset from array'
51
+ },
52
+ randomElement: {
53
+ minArgs: 1,
54
+ maxArgs: 1,
55
+ description: 'Return random element from array'
56
+ },
57
+ randomInt: {
58
+ minArgs: 2,
59
+ maxArgs: 2,
60
+ description: 'Generate random integer'
61
+ },
62
+
63
+ // Text generation functions
64
+ createTextGenerator: {
65
+ minArgs: 1,
66
+ maxArgs: 1,
67
+ description: 'Create text generator with config'
68
+ },
69
+ generateBatch: {
70
+ minArgs: 2,
71
+ maxArgs: 2,
72
+ description: 'Generate batch of text'
73
+ },
74
+
75
+ // Chance.js functions (using dot notation)
76
+ 'chance.word': {
77
+ minArgs: 0,
78
+ maxArgs: 1,
79
+ description: 'Generate random word'
80
+ },
81
+ 'chance.sentence': {
82
+ minArgs: 0,
83
+ maxArgs: 1,
84
+ description: 'Generate random sentence'
85
+ },
86
+ 'chance.paragraph': {
87
+ minArgs: 0,
88
+ maxArgs: 1,
89
+ description: 'Generate random paragraph'
90
+ },
91
+ 'chance.name': {
92
+ minArgs: 0,
93
+ maxArgs: 1,
94
+ description: 'Generate random name'
95
+ },
96
+ 'chance.first': {
97
+ minArgs: 0,
98
+ maxArgs: 1,
99
+ description: 'Generate random first name'
100
+ },
101
+ 'chance.last': {
102
+ minArgs: 0,
103
+ maxArgs: 1,
104
+ description: 'Generate random last name'
105
+ },
106
+ 'chance.email': {
107
+ minArgs: 0,
108
+ maxArgs: 1,
109
+ description: 'Generate random email'
110
+ },
111
+ 'chance.company': {
112
+ minArgs: 0,
113
+ maxArgs: 0,
114
+ description: 'Generate random company name'
115
+ },
116
+ 'chance.profession': {
117
+ minArgs: 0,
118
+ maxArgs: 0,
119
+ description: 'Generate random profession'
120
+ },
121
+ 'chance.industry': {
122
+ minArgs: 0,
123
+ maxArgs: 0,
124
+ description: 'Generate random industry'
125
+ },
126
+ 'chance.country': {
127
+ minArgs: 0,
128
+ maxArgs: 1,
129
+ description: 'Generate random country'
130
+ },
131
+ 'chance.city': {
132
+ minArgs: 0,
133
+ maxArgs: 0,
134
+ description: 'Generate random city'
135
+ },
136
+ 'chance.state': {
137
+ minArgs: 0,
138
+ maxArgs: 1,
139
+ description: 'Generate random state'
140
+ },
141
+ 'chance.address': {
142
+ minArgs: 0,
143
+ maxArgs: 1,
144
+ description: 'Generate random address'
145
+ },
146
+ 'chance.phone': {
147
+ minArgs: 0,
148
+ maxArgs: 1,
149
+ description: 'Generate random phone number'
150
+ },
151
+ 'chance.url': {
152
+ minArgs: 0,
153
+ maxArgs: 1,
154
+ description: 'Generate random URL'
155
+ },
156
+ 'chance.domain': {
157
+ minArgs: 0,
158
+ maxArgs: 1,
159
+ description: 'Generate random domain'
160
+ },
161
+ 'chance.ip': {
162
+ minArgs: 0,
163
+ maxArgs: 0,
164
+ description: 'Generate random IP address'
165
+ },
166
+ 'chance.guid': {
167
+ minArgs: 0,
168
+ maxArgs: 1,
169
+ description: 'Generate random GUID'
170
+ },
171
+ 'chance.hash': {
172
+ minArgs: 0,
173
+ maxArgs: 1,
174
+ description: 'Generate random hash'
175
+ },
176
+ 'chance.integer': {
177
+ minArgs: 0,
178
+ maxArgs: 1,
179
+ description: 'Generate random integer with options'
180
+ },
181
+ 'chance.floating': {
182
+ minArgs: 0,
183
+ maxArgs: 1,
184
+ description: 'Generate random float'
185
+ },
186
+ 'chance.bool': {
187
+ minArgs: 0,
188
+ maxArgs: 1,
189
+ description: 'Generate random boolean'
190
+ },
191
+ 'chance.character': {
192
+ minArgs: 0,
193
+ maxArgs: 1,
194
+ description: 'Generate random character'
195
+ },
196
+ 'chance.string': {
197
+ minArgs: 0,
198
+ maxArgs: 1,
199
+ description: 'Generate random string'
200
+ },
201
+ 'chance.pick': {
202
+ minArgs: 1,
203
+ maxArgs: 2,
204
+ description: 'Pick random element from array'
205
+ },
206
+ 'chance.pickone': {
207
+ minArgs: 1,
208
+ maxArgs: 1,
209
+ description: 'Pick one random element from array'
210
+ },
211
+ 'chance.pickset': {
212
+ minArgs: 2,
213
+ maxArgs: 2,
214
+ description: 'Pick set of random elements from array'
215
+ },
216
+ 'chance.cc': {
217
+ minArgs: 0,
218
+ maxArgs: 1,
219
+ description: 'Generate credit card number'
220
+ },
221
+ 'chance.android_id': {
222
+ minArgs: 0,
223
+ maxArgs: 0,
224
+ description: 'Generate Android device ID'
225
+ },
226
+
227
+ // Commonly used utility functions from dungeons
228
+ pickAWinner: {
229
+ minArgs: 1,
230
+ maxArgs: 2,
231
+ description: 'Pick from array with power-law weighting (most common values first)'
232
+ },
233
+ weighChoices: {
234
+ minArgs: 1,
235
+ maxArgs: 1,
236
+ description: 'Weight choices by frequency in array (more duplicates = higher weight)'
237
+ },
238
+ decimal: {
239
+ minArgs: 0,
240
+ maxArgs: 3,
241
+ description: 'Generate random decimal (min, max, fixed decimal places)'
242
+ },
243
+
244
+ // Special function for arrow functions
245
+ arrow: {
246
+ minArgs: 1,
247
+ maxArgs: 1,
248
+ description: 'Raw arrow function with body',
249
+ special: true
250
+ }
251
+ };
252
+
253
+ /**
254
+ * Validate a function call structure
255
+ * @param {Object} funcCall - The function call object with functionName and args
256
+ * @returns {boolean} - Whether the function call is valid
257
+ */
258
+ export function validateFunctionCall(funcCall) {
259
+ if (!funcCall || typeof funcCall !== 'object') {
260
+ return false;
261
+ }
262
+
263
+ const { functionName, args, body } = funcCall;
264
+
265
+ if (!functionName || typeof functionName !== 'string') {
266
+ return false;
267
+ }
268
+
269
+ // Special handling for arrow functions
270
+ if (functionName === 'arrow') {
271
+ return typeof body === 'string' && body.length > 0;
272
+ }
273
+
274
+ const funcDef = FUNCTION_REGISTRY[functionName];
275
+ if (!funcDef) {
276
+ logger.warn({ functionName }, `Unknown function: ${functionName}`);
277
+ return false;
278
+ }
279
+
280
+ // Check args
281
+ if (!Array.isArray(args)) {
282
+ if (funcDef.minArgs === 0 && !args) {
283
+ return true; // No args required and none provided
284
+ }
285
+ return false;
286
+ }
287
+
288
+ if (args.length < funcDef.minArgs || args.length > funcDef.maxArgs) {
289
+ logger.warn({ functionName, expected: `${funcDef.minArgs}-${funcDef.maxArgs}`, actual: args.length }, `Function ${functionName} expects ${funcDef.minArgs}-${funcDef.maxArgs} args, got ${args.length}`);
290
+ return false;
291
+ }
292
+
293
+ return true;
294
+ }
295
+
296
+ /**
297
+ * Get list of all valid function names
298
+ * @returns {string[]} Array of function names
299
+ */
300
+ export function getValidFunctionNames() {
301
+ return Object.keys(FUNCTION_REGISTRY);
302
+ }