@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,132 @@
1
+ /**
2
+ * Ad Spend generator module
3
+ * Creates realistic advertising spend events with UTM parameters and metrics
4
+ */
5
+
6
+ /** @typedef {import('../../types').Context} Context */
7
+
8
+ import dayjs from "dayjs";
9
+ import * as u from "../utils/utils.js";
10
+
11
+ /**
12
+ * Creates ad spend events for a given day and campaign configurations
13
+ * @param {Context} context - Context object containing config, defaults, etc.
14
+ * @param {string} day - ISO date string for the ad spend day
15
+ * @param {Array} campaigns - Array of campaign configurations (optional, uses context.campaigns if not provided)
16
+ * @returns {Promise<Array>} Array of ad spend event objects
17
+ */
18
+ export async function makeAdSpend(context, day, campaigns = null) {
19
+ // Update operation counter
20
+ context.incrementOperations();
21
+
22
+ // Use campaigns from context if not provided
23
+ const campaignConfigs = campaigns || context.campaigns;
24
+
25
+ if (!campaignConfigs || campaignConfigs.length === 0) {
26
+ return [];
27
+ }
28
+
29
+ const chance = u.getChance();
30
+ const adSpendEvents = [];
31
+
32
+ for (const network of campaignConfigs) {
33
+ const networkCampaigns = network.utm_campaign;
34
+
35
+ for (const campaign of networkCampaigns) {
36
+ // Skip organic campaigns
37
+ if (campaign === "$organic") continue;
38
+
39
+ // Generate realistic ad spend metrics
40
+ const adSpendEvent = createAdSpendEvent(network, campaign, day, chance);
41
+ adSpendEvents.push(adSpendEvent);
42
+ }
43
+ }
44
+
45
+ return adSpendEvents;
46
+ }
47
+
48
+ /**
49
+ * Creates a single ad spend event with realistic metrics
50
+ * @param {Object} network - Network configuration object
51
+ * @param {string} campaign - Campaign name
52
+ * @param {string} day - ISO date string
53
+ * @param {Object} chance - Chance.js instance
54
+ * @returns {Object} Ad spend event object
55
+ */
56
+ function createAdSpendEvent(network, campaign, day, chance) {
57
+ // Generate realistic cost
58
+ const cost = chance.floating({ min: 10, max: 250, fixed: 2 });
59
+
60
+ // Generate realistic CPC and CTR
61
+ const avgCPC = chance.floating({ min: 0.33, max: 2.00, fixed: 4 });
62
+ const avgCTR = chance.floating({ min: 0.05, max: 0.25, fixed: 4 });
63
+
64
+ // Calculate derived metrics
65
+ const clicks = Math.floor(cost / avgCPC);
66
+ const impressions = Math.floor(clicks / avgCTR);
67
+ const views = Math.floor(impressions * avgCTR);
68
+
69
+ // Generate UTM parameters
70
+ const utm_medium = u.choose(u.pickAWinner(network.utm_medium)());
71
+ const utm_content = u.choose(u.pickAWinner(network.utm_content)());
72
+ const utm_term = u.choose(u.pickAWinner(network.utm_term)());
73
+
74
+ // Create unique identifiers
75
+ const id = network.utm_source[0] + '-' + campaign;
76
+ const uid = u.quickHash(id);
77
+
78
+ return {
79
+ event: "$ad_spend",
80
+ time: day,
81
+ // source: 'dm4',
82
+ utm_campaign: campaign,
83
+ campaign_id: id,
84
+ insert_id: uid,
85
+ network: network.utm_source[0].toUpperCase(),
86
+ distinct_id: network.utm_source[0].toUpperCase(),
87
+ utm_source: network.utm_source[0],
88
+ utm_medium,
89
+ utm_content,
90
+ utm_term,
91
+ clicks,
92
+ views,
93
+ impressions,
94
+ cost,
95
+ date: dayjs(day).format("YYYY-MM-DD"),
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Validates campaign configuration
101
+ * @param {Array} campaigns - Campaign configurations to validate
102
+ * @returns {boolean} True if valid, throws error if invalid
103
+ */
104
+ export function validateCampaigns(campaigns) {
105
+ if (!Array.isArray(campaigns)) {
106
+ throw new Error("Campaigns must be an array");
107
+ }
108
+
109
+ for (const network of campaigns) {
110
+ if (!network.utm_source || !Array.isArray(network.utm_source)) {
111
+ throw new Error("Each campaign network must have utm_source array");
112
+ }
113
+
114
+ if (!network.utm_campaign || !Array.isArray(network.utm_campaign)) {
115
+ throw new Error("Each campaign network must have utm_campaign array");
116
+ }
117
+
118
+ if (!network.utm_medium || !Array.isArray(network.utm_medium)) {
119
+ throw new Error("Each campaign network must have utm_medium array");
120
+ }
121
+
122
+ if (!network.utm_content || !Array.isArray(network.utm_content)) {
123
+ throw new Error("Each campaign network must have utm_content array");
124
+ }
125
+
126
+ if (!network.utm_term || !Array.isArray(network.utm_term)) {
127
+ throw new Error("Each campaign network must have utm_term array");
128
+ }
129
+ }
130
+
131
+ return true;
132
+ }
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Event generator module
3
+ * Creates individual Mixpanel events with realistic properties and timing
4
+ */
5
+
6
+ /** @typedef {import('../../types').Dungeon} Config */
7
+ /** @typedef {import('../../types').EventConfig} EventConfig */
8
+ /** @typedef {import('../../types').ValueValid} ValueValid */
9
+ /** @typedef {import('../../types').EventSchema} EventSchema */
10
+ /** @typedef {import('../../types').Context} Context */
11
+
12
+ import dayjs from "dayjs";
13
+ import * as u from "../utils/utils.js";
14
+ import { dataLogger as logger } from "../utils/logger.js";
15
+
16
+ /**
17
+ * Creates a Mixpanel event with a flat shape
18
+ * @param {Context} context - Context object containing config, defaults, etc.
19
+ * @param {string} distinct_id - User identifier
20
+ * @param {number} earliestTime - Unix timestamp for earliest possible event time
21
+ * @param {Object} chosenEvent - Event configuration object
22
+ * @param {string[]} [anonymousIds] - Array of anonymous/device IDs
23
+ * @param {string[]} [sessionIds] - Array of session IDs
24
+ * @param {Object} [superProps] - Super properties to add to event
25
+ * @param {Array} [groupKeys] - Group key configurations
26
+ * @param {boolean} [isFirstEvent] - Whether this is the user's first event
27
+ * @param {boolean} [skipDefaults] - Whether to skip adding default properties
28
+ * @returns {Promise<Object>} Generated event object
29
+ */
30
+ export async function makeEvent(
31
+ context,
32
+ distinct_id,
33
+ earliestTime,
34
+ chosenEvent,
35
+ anonymousIds = [],
36
+ sessionIds = [],
37
+ superProps = {},
38
+ groupKeys = [],
39
+ isFirstEvent = false,
40
+ skipDefaults = false
41
+ ) {
42
+ // Validate required parameters
43
+ if (!distinct_id) throw new Error("no distinct_id");
44
+ if (!earliestTime) throw new Error("no earliestTime");
45
+ if (!chosenEvent) throw new Error("no chosenEvent");
46
+
47
+ // Update context metrics
48
+ context.incrementOperations();
49
+ context.incrementEvents();
50
+
51
+ const { config, defaults } = context;
52
+ const chance = u.getChance();
53
+
54
+ // Extract soup configuration for time distribution
55
+ // Dynamic peaks: enough to flatten DOW interference from chunk boundaries
56
+ const defaultPeaks = Math.max(5, (config.numDays || 30) * 2);
57
+ const { mean = 0, deviation = 2, peaks = defaultPeaks, dayOfWeekWeights, hourOfDayWeights } = /** @type {import('../../types').SoupConfig} */ (config.soup) || {};
58
+
59
+ // Extract feature flags from config
60
+ const {
61
+ hasAndroidDevices = false,
62
+ hasBrowser = false,
63
+ hasCampaigns = false,
64
+ hasDesktopDevices = false,
65
+ hasIOSDevices = false,
66
+ hasLocation = false
67
+ } = config;
68
+
69
+ // Create base event template
70
+ const eventTemplate = {
71
+ event: chosenEvent.event,
72
+ // source: "dm4",
73
+ time: "",
74
+ insert_id: "",
75
+ };
76
+
77
+ let defaultProps = {};
78
+
79
+ // Add default properties based on configuration
80
+ if (hasLocation) {
81
+ defaultProps.location = u.pickRandom(defaults.locationsEvents());
82
+ }
83
+
84
+ if (hasBrowser) {
85
+ defaultProps.browser = u.choose(defaults.browsers());
86
+ }
87
+
88
+ // Add campaigns with attribution likelihood
89
+ if (hasCampaigns && chance.bool({ likelihood: 25 })) {
90
+ defaultProps.campaigns = u.pickRandom(defaults.campaigns());
91
+ }
92
+
93
+ // PERFORMANCE: Use pre-computed device pool instead of rebuilding every time
94
+ if (defaults.allDevices.length) {
95
+ defaultProps.device = u.pickRandom(defaults.allDevices);
96
+ }
97
+
98
+ // Set event time using TimeSoup for realistic distribution
99
+ if (earliestTime) {
100
+ let shiftedTimestamp;
101
+ if (isFirstEvent) {
102
+ shiftedTimestamp = earliestTime + context.TIME_SHIFT_SECONDS;
103
+ } else {
104
+ // TimeSoup returns unix seconds; shift and convert to ISO once
105
+ const soupTimestamp = u.TimeSoup(earliestTime, context.FIXED_NOW, peaks, deviation, mean, dayOfWeekWeights, hourOfDayWeights, context.TIME_SHIFT_SECONDS);
106
+ shiftedTimestamp = soupTimestamp + context.TIME_SHIFT_SECONDS;
107
+ }
108
+ // Drop events that would land in the future (Mixpanel rewrites these to "now", causing pile-ups)
109
+ if (shiftedTimestamp > context.MAX_TIME) {
110
+ eventTemplate._drop = true;
111
+ }
112
+ eventTemplate.time = dayjs.unix(Math.min(shiftedTimestamp, context.MAX_TIME)).toISOString();
113
+ }
114
+
115
+ // Add anonymous and session identifiers
116
+ if (anonymousIds.length) {
117
+ eventTemplate.device_id = u.pickRandom(anonymousIds);
118
+ }
119
+
120
+ if (sessionIds.length) {
121
+ eventTemplate.session_id = u.pickRandom(sessionIds);
122
+ }
123
+
124
+ // Sometimes add user_id (for attribution modeling)
125
+ if (!isFirstEvent && chance.bool({ likelihood: 42 })) {
126
+ eventTemplate.user_id = distinct_id;
127
+ }
128
+
129
+ // Ensure we have either user_id or device_id
130
+ if (!eventTemplate.user_id && !eventTemplate.device_id) {
131
+ eventTemplate.user_id = distinct_id;
132
+ }
133
+
134
+ // PERFORMANCE: Process properties directly without creating intermediate object
135
+ // Add custom properties from event configuration
136
+ if (chosenEvent.properties) {
137
+ const eventKeys = Object.keys(chosenEvent.properties);
138
+ for (let i = 0; i < eventKeys.length; i++) {
139
+ const key = eventKeys[i];
140
+ try {
141
+ eventTemplate[key] = u.choose(chosenEvent.properties[key]);
142
+ } catch (e) {
143
+ logger.error({ err: e, key, event: chosenEvent.event }, `Error processing property ${key} in ${chosenEvent.event} event`);
144
+ // Continue processing other properties
145
+ }
146
+ }
147
+ }
148
+
149
+ // Add super properties (override event properties if needed)
150
+ if (superProps) {
151
+ const superKeys = Object.keys(superProps);
152
+ for (let i = 0; i < superKeys.length; i++) {
153
+ const key = superKeys[i];
154
+ try {
155
+ eventTemplate[key] = u.choose(superProps[key]);
156
+ } catch (e) {
157
+ logger.error({ err: e, key }, `Error processing super property ${key}`);
158
+ // Continue processing other properties
159
+ }
160
+ }
161
+ }
162
+
163
+ // Add default properties if not skipped
164
+ if (!skipDefaults) {
165
+ addDefaultProperties(eventTemplate, defaultProps);
166
+ }
167
+
168
+ // Add group properties
169
+ addGroupProperties(eventTemplate, groupKeys);
170
+
171
+ // Generate unique insert_id
172
+ const distinctId = eventTemplate.user_id || eventTemplate.device_id || eventTemplate.distinct_id || distinct_id;
173
+ const tuple = `${eventTemplate.event}-${eventTemplate.time}-${distinctId}`;
174
+ eventTemplate.insert_id = u.quickHash(tuple);
175
+
176
+ // Call hook if configured (before returning the event)
177
+ const { hook } = config;
178
+ if (hook) {
179
+ const hookedEvent = await hook(eventTemplate, "event", {
180
+ user: { distinct_id },
181
+ config
182
+ });
183
+ // If hook returns a modified event, use it; otherwise use original
184
+ if (hookedEvent && typeof hookedEvent === 'object') {
185
+ return hookedEvent;
186
+ }
187
+ }
188
+
189
+ // Note: Time shift already applied above during timestamp calculation
190
+
191
+ return eventTemplate;
192
+ }
193
+
194
+ /**
195
+ * Adds default properties to an event template
196
+ * Handles complex nested property structures
197
+ * @param {Object} eventTemplate - Event object to modify
198
+ * @param {Object} defaultProps - Default properties to add
199
+ */
200
+ function addDefaultProperties(eventTemplate, defaultProps) {
201
+ for (const key in defaultProps) {
202
+ if (Array.isArray(defaultProps[key])) {
203
+ const choice = u.choose(defaultProps[key]);
204
+
205
+ if (typeof choice === "string") {
206
+ if (!eventTemplate[key]) eventTemplate[key] = choice;
207
+ }
208
+ else if (Array.isArray(choice)) {
209
+ for (const subChoice of choice) {
210
+ if (!eventTemplate[key]) eventTemplate[key] = subChoice;
211
+ }
212
+ }
213
+ else if (typeof choice === "object") {
214
+ addNestedObjectProperties(eventTemplate, choice);
215
+ }
216
+ }
217
+ else if (typeof defaultProps[key] === "object") {
218
+ addNestedObjectProperties(eventTemplate, defaultProps[key]);
219
+ }
220
+ else {
221
+ if (!eventTemplate[key]) eventTemplate[key] = defaultProps[key];
222
+ }
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Adds nested object properties to event template
228
+ * @param {Object} eventTemplate - Event object to modify
229
+ * @param {Object} obj - Object with properties to add
230
+ */
231
+ function addNestedObjectProperties(eventTemplate, obj) {
232
+ for (const subKey in obj) {
233
+ if (typeof obj[subKey] === "string") {
234
+ if (!eventTemplate[subKey]) eventTemplate[subKey] = obj[subKey];
235
+ }
236
+ else if (Array.isArray(obj[subKey])) {
237
+ const subChoice = u.choose(obj[subKey]);
238
+ if (!eventTemplate[subKey]) eventTemplate[subKey] = subChoice;
239
+ }
240
+ else if (typeof obj[subKey] === "object") {
241
+ for (const subSubKey in obj[subKey]) {
242
+ if (!eventTemplate[subSubKey]) {
243
+ eventTemplate[subSubKey] = obj[subKey][subSubKey];
244
+ }
245
+ }
246
+ }
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Adds group properties to event based on group key configuration
252
+ * @param {Object} eventTemplate - Event object to modify
253
+ * @param {Array} groupKeys - Array of group key configurations
254
+ */
255
+ function addGroupProperties(eventTemplate, groupKeys) {
256
+ for (const groupPair of groupKeys) {
257
+ const groupKey = groupPair[0];
258
+ const groupCardinality = groupPair[1];
259
+ const groupEvents = groupPair[2] || [];
260
+
261
+ // Empty array for group events means all events get the group property
262
+ if (!groupEvents.length) {
263
+ eventTemplate[groupKey] = String(u.pick(u.weighNumRange(1, groupCardinality)));
264
+ }
265
+
266
+ // Only add group property if event is in the specified group events
267
+ if (groupEvents.includes(eventTemplate.event)) {
268
+ eventTemplate[groupKey] = String(u.pick(u.weighNumRange(1, groupCardinality)));
269
+ }
270
+ }
271
+ }