@ak--47/dungeon-master 1.0.0 → 1.2.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 (104) hide show
  1. package/README.md +7 -7
  2. package/dungeons/technical/ad-spend-schema.json +128 -0
  3. package/dungeons/technical/ad-spend.js +116 -0
  4. package/dungeons/technical/anonymous-users-schema.json +92 -0
  5. package/dungeons/technical/anonymous-users.js +102 -0
  6. package/dungeons/technical/array-of-object-lookup-schema.json +191 -0
  7. package/dungeons/{array-of-object-lookup.js → technical/array-of-object-lookup.js} +12 -5
  8. package/dungeons/technical/experiments-schema.json +203 -0
  9. package/dungeons/technical/experiments.js +168 -0
  10. package/dungeons/technical/foobar-schema.json +362 -0
  11. package/dungeons/{foobar.js → technical/foobar.js} +3 -3
  12. package/dungeons/technical/group-analytics-schema.json +241 -0
  13. package/dungeons/technical/group-analytics.js +151 -0
  14. package/dungeons/technical/mirror-strategies-schema.json +84 -0
  15. package/dungeons/technical/mirror-strategies.js +126 -0
  16. package/dungeons/technical/nested-objects-schema.json +145 -0
  17. package/dungeons/technical/nested-objects.js +207 -0
  18. package/dungeons/technical/retention-cadence-schema.json +37 -0
  19. package/dungeons/{retention-cadence.js → technical/retention-cadence.js} +2 -3
  20. package/dungeons/technical/sanity-schema.json +185 -0
  21. package/dungeons/{sanity.js → technical/sanity.js} +4 -2
  22. package/dungeons/technical/scale-test-schema.json +70 -0
  23. package/dungeons/technical/scale-test.js +90 -0
  24. package/dungeons/technical/scd-schema.json +467 -0
  25. package/dungeons/{scd.js → technical/scd.js} +20 -10
  26. package/dungeons/technical/simple-schema.json +362 -0
  27. package/dungeons/{simple.js → technical/simple.js} +7 -7
  28. package/dungeons/technical/simplest-schema.json +724 -0
  29. package/dungeons/{simplest.js → technical/simplest.js} +7 -7
  30. package/dungeons/technical/text-generation-schema.json +1062 -0
  31. package/dungeons/{text-generation.js → technical/text-generation.js} +24 -11
  32. package/dungeons/vertical/community-schema.json +579 -0
  33. package/dungeons/vertical/community.js +794 -0
  34. package/dungeons/vertical/devtools-schema.json +601 -0
  35. package/dungeons/vertical/devtools.js +843 -0
  36. package/dungeons/vertical/ecommerce-schema.json +604 -0
  37. package/dungeons/{ecommerce.js → vertical/ecommerce.js} +43 -5
  38. package/dungeons/vertical/education-schema.json +5686 -0
  39. package/dungeons/{education.js → vertical/education.js} +70 -39
  40. package/dungeons/vertical/fintech-schema.json +630 -0
  41. package/dungeons/{fintech.js → vertical/fintech.js} +107 -56
  42. package/dungeons/vertical/fitness-schema.json +530 -0
  43. package/dungeons/vertical/fitness.js +808 -0
  44. package/dungeons/vertical/food-delivery-schema.json +36728 -0
  45. package/dungeons/{food.js → vertical/food-delivery.js} +67 -26
  46. package/dungeons/vertical/gaming-schema.json +438 -0
  47. package/dungeons/{gaming.js → vertical/gaming.js} +29 -16
  48. package/dungeons/vertical/healthcare-schema.json +549 -0
  49. package/dungeons/vertical/healthcare.js +775 -0
  50. package/dungeons/vertical/insurance-application-schema.json +485 -0
  51. package/dungeons/{insurance-application.js → vertical/insurance-application.js} +41 -31
  52. package/dungeons/vertical/logistics-schema.json +574 -0
  53. package/dungeons/vertical/logistics.js +799 -0
  54. package/dungeons/vertical/marketplace-schema.json +533 -0
  55. package/dungeons/vertical/marketplace.js +850 -0
  56. package/dungeons/vertical/media-schema.json +4749 -0
  57. package/dungeons/{media.js → vertical/media.js} +103 -19
  58. package/dungeons/vertical/rpg-schema.json +2491 -0
  59. package/dungeons/{rpg.js → vertical/rpg.js} +87 -60
  60. package/dungeons/vertical/sass-schema.json +3128 -0
  61. package/dungeons/{sass.js → vertical/sass.js} +65 -26
  62. package/dungeons/vertical/social-schema.json +620 -0
  63. package/dungeons/{social.js → vertical/social.js} +76 -17
  64. package/dungeons/vertical/travel-schema.json +580 -0
  65. package/dungeons/vertical/travel.js +742 -0
  66. package/index.js +20 -16
  67. package/lib/core/config-validator.js +286 -1
  68. package/lib/core/context.js +16 -5
  69. package/lib/core/storage.js +2 -0
  70. package/lib/generators/adspend.js +3 -3
  71. package/lib/generators/events.js +141 -14
  72. package/lib/generators/funnels.js +67 -7
  73. package/lib/generators/text.js +2 -2
  74. package/lib/orchestrators/user-loop.js +444 -22
  75. package/lib/templates/soup-presets.js +2 -2
  76. package/lib/utils/utils.js +4 -5
  77. package/package.json +4 -3
  78. package/scripts/dungeon-to-json.mjs +163 -0
  79. package/scripts/extract-dungeon-schema.mjs +343 -0
  80. package/scripts/json-to-dungeon.mjs +200 -0
  81. package/scripts/run-dungeon.mjs +76 -0
  82. package/scripts/run-many.mjs +118 -0
  83. package/scripts/verify-runner.mjs +42 -0
  84. package/types.d.ts +316 -1
  85. package/dungeons/array-of-object-lookup-schema.json +0 -327
  86. package/dungeons/ecommerce-schema.json +0 -462
  87. package/dungeons/education-schema.json +0 -2409
  88. package/dungeons/fintech-schema.json +0 -14034
  89. package/dungeons/foobar-schema.json +0 -403
  90. package/dungeons/food-delivery-schema.json +0 -192
  91. package/dungeons/food-delivery.js +0 -602
  92. package/dungeons/food-schema.json +0 -1152
  93. package/dungeons/gaming-schema.json +0 -1270
  94. package/dungeons/insurance-application-schema.json +0 -204
  95. package/dungeons/media-schema.json +0 -906
  96. package/dungeons/retention-cadence-schema.json +0 -78
  97. package/dungeons/rpg-schema.json +0 -4526
  98. package/dungeons/sanity-schema.json +0 -255
  99. package/dungeons/sass-schema.json +0 -1291
  100. package/dungeons/scd-schema.json +0 -919
  101. package/dungeons/simple-schema.json +0 -608
  102. package/dungeons/simplest-schema.json +0 -1418
  103. package/dungeons/social-schema.json +0 -1118
  104. package/dungeons/text-generation-schema.json +0 -3096
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from 'path';
4
+ import generate from '../index.js';
5
+ const { NODE_ENV = "unknown" } = process.env;
6
+
7
+ // Get the dungeon file path from command line arguments
8
+ const dungeonPath = process.argv[2];
9
+
10
+ if (!dungeonPath) {
11
+ console.error('❌ Error: No dungeon file specified');
12
+ console.error('Usage: node run-dungeon.js <path-to-dungeon.js>');
13
+ process.exit(1);
14
+ }
15
+
16
+ // Resolve the absolute path
17
+ const absolutePath = path.isAbsolute(dungeonPath)
18
+ ? dungeonPath
19
+ : path.resolve(process.cwd(), dungeonPath);
20
+
21
+ // Handle Ctrl+C gracefully — let user-loop finish current user, then exit
22
+ process.on('SIGINT', () => {
23
+ // Second Ctrl+C forces immediate exit
24
+ process.on('SIGINT', () => process.exit(1));
25
+ });
26
+
27
+ console.log(`\n🎲 Running dungeon: ${path.basename(absolutePath)}\n`);
28
+
29
+ try {
30
+ // Dynamically import the dungeon configuration
31
+ const { default: config } = await import(absolutePath);
32
+
33
+ if (!config) {
34
+ console.error('❌ Error: Dungeon file must export a default configuration object');
35
+ process.exit(1);
36
+ }
37
+
38
+ // Run the dungeon (always verbose when run locally)
39
+ config.verbose = true;
40
+ const results = await generate(config);
41
+
42
+ // Display results
43
+ console.log('\n✅ Dungeon run complete!');
44
+ console.log('═'.repeat(50));
45
+ console.log(`📊 Results:`);
46
+ console.log(` Users: ${results.userCount.toLocaleString()}`);
47
+ console.log(` Events: ${results.eventCount.toLocaleString()}`);
48
+ if (results.groupCount) {
49
+ console.log(` Groups: ${results.groupCount.toLocaleString()}`);
50
+ }
51
+ if (results.time) {
52
+ console.log(` Duration: ${results.time.human || results.time.delta + 'ms'}`);
53
+ }
54
+
55
+ if (results.avgEPS) {
56
+ console.log(` Avg EPS: ${results.avgEPS.toLocaleString()}`);
57
+ }
58
+
59
+ if (results.files && results.files.length > 0) {
60
+ console.log(`\n📁 Files written:`);
61
+ results.files.forEach(file => {
62
+ console.log(` - ${file}`);
63
+ });
64
+ }
65
+
66
+ console.log('═'.repeat(50) + '\n');
67
+
68
+ // if (NODE_ENV === 'dev') debugger;
69
+
70
+
71
+
72
+ } catch (error) {
73
+ console.error('❌ Error running dungeon:', error.message);
74
+ console.error(error.stack);
75
+ process.exit(1);
76
+ }
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+ import { fork } from 'child_process';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import dotenv from "dotenv";
6
+ dotenv.config();
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+
11
+ import main from "../index.js";
12
+ const CONCURRENCY = 10
13
+
14
+ /**
15
+ * Run multiple dungeons concurrently from the command line.
16
+ *
17
+ * Usage:
18
+ * node scripts/run-many.mjs dungeons/vertical/gaming.js dungeons/vertical/media.js dungeons/vertical/food-delivery.js
19
+ * node scripts/run-many.mjs dungeons/vertical/*.js
20
+ */
21
+
22
+ const args = process.argv.slice(2);
23
+
24
+ if (process.env.__SIM_CHILD) {
25
+ // CHILD: run the assigned dungeon
26
+ const dungeonPath = process.env.__SIM_PATH;
27
+ (async () => {
28
+ try {
29
+ const mod = await import(path.resolve(dungeonPath));
30
+ const spec = mod.default;
31
+ spec.name = path.basename(dungeonPath, '.js');
32
+ spec.verbose = false;
33
+ const result = await main(spec);
34
+ const summary = { eventCount: result.eventCount, userCount: result.userCount, time: result.time };
35
+ if (process.send) process.send({ name: spec.name, success: true, summary }, undefined, undefined, () => process.exit(0));
36
+ else process.exit(0);
37
+ } catch (err) {
38
+ console.error(`[${dungeonPath}] Error:`, err.message);
39
+ if (process.send) process.send({ name: dungeonPath, success: false, error: err.message });
40
+ process.exit(1);
41
+ }
42
+ })();
43
+ } else {
44
+ // PARENT: fork children with concurrency limit
45
+ if (args.length === 0) {
46
+ console.error("Usage: node scripts/run-many.mjs <dungeon1.js> <dungeon2.js> ...");
47
+ process.exit(1);
48
+ }
49
+
50
+ const limit = CONCURRENCY;
51
+ let running = 0;
52
+ const queue = args.map(p => ({ path: p, name: path.basename(p, '.js') }));
53
+ const results = [];
54
+ const total = queue.length;
55
+
56
+ console.log(`\n${"═".repeat(60)}`);
57
+ console.log(`🎲 Running ${total} dungeon(s) with concurrency ${limit}`);
58
+ console.log(`${"═".repeat(60)}\n`);
59
+
60
+ function runOne(dungeon) {
61
+ return new Promise((resolve, reject) => {
62
+ console.log(`▶ Starting: ${dungeon.name}`);
63
+ const startTime = Date.now();
64
+ const child = fork(__filename, [], {
65
+ env: { ...process.env, __SIM_CHILD: "1", __SIM_PATH: dungeon.path },
66
+ stdio: ['inherit', 'inherit', 'inherit', 'ipc']
67
+ });
68
+ child.on('message', (msg) => {
69
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
70
+ if (msg.success) {
71
+ console.log(`✅ Finished: ${dungeon.name} (${elapsed}s)`);
72
+ } else {
73
+ console.error(`❌ Failed: ${dungeon.name} - ${msg.error} (${elapsed}s)`);
74
+ }
75
+ resolve(msg);
76
+ });
77
+ child.on('error', (err) => {
78
+ console.error(`❌ Fork error: ${dungeon.name}`, err.message);
79
+ reject(err);
80
+ });
81
+ child.on('exit', (code) => {
82
+ if (code !== 0 && code !== null) {
83
+ reject(new Error(`${dungeon.name} exited with code ${code}`));
84
+ }
85
+ });
86
+ });
87
+ }
88
+
89
+ function runNext() {
90
+ while (running < limit && queue.length > 0) {
91
+ const dungeon = queue.shift();
92
+ running++;
93
+ runOne(dungeon)
94
+ .then(res => results.push(res))
95
+ .catch(e => results.push({ name: dungeon.name, success: false, error: e.message }))
96
+ .finally(() => { running--; runNext(); });
97
+ }
98
+ }
99
+ runNext();
100
+
101
+ const wait = () => new Promise(resolve => {
102
+ const check = () => {
103
+ if (results.length === total) resolve(results);
104
+ else setTimeout(check, 250);
105
+ };
106
+ check();
107
+ });
108
+
109
+ const all = await wait();
110
+ const succeeded = all.filter(r => r.success).length;
111
+ const failed = all.filter(r => !r.success).length;
112
+
113
+ console.log(`\n${"═".repeat(60)}`);
114
+ console.log(`🏁 Done: ${succeeded} succeeded, ${failed} failed out of ${total}`);
115
+ console.log(`${"═".repeat(60)}\n`);
116
+
117
+ if (failed > 0) process.exit(1);
118
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Verify Runner — runs a dungeon at constrained params for hook verification.
3
+ *
4
+ * Usage: node scripts/verify-runner.mjs <dungeon-path> [run-name]
5
+ *
6
+ * Example: node scripts/verify-runner.mjs dungeons/streaming.js verify-streaming
7
+ */
8
+ import generate from '../index.js';
9
+ import path from 'path';
10
+
11
+ const dungeonPath = process.argv[2];
12
+ if (!dungeonPath) {
13
+ console.error('Usage: node scripts/verify-runner.mjs <dungeon-path> [run-name]');
14
+ process.exit(1);
15
+ }
16
+
17
+ const runName = process.argv[3] || 'verify-hooks';
18
+ const absolutePath = path.isAbsolute(dungeonPath)
19
+ ? dungeonPath
20
+ : path.resolve(process.cwd(), dungeonPath);
21
+
22
+ const { default: config } = await import(absolutePath);
23
+
24
+ const results = await generate({
25
+ ...config,
26
+ token: "",
27
+ numUsers: 1000,
28
+ numEvents: 100_000,
29
+ format: "json",
30
+ gzip: false,
31
+ writeToDisk: true,
32
+ name: runName,
33
+ concurrency: 1,
34
+ verbose: false,
35
+ });
36
+
37
+ console.log(JSON.stringify({
38
+ eventCount: results.eventCount,
39
+ userCount: results.userCount,
40
+ files: results.files,
41
+ duration: results.time?.human || results.time?.delta + 'ms'
42
+ }));
package/types.d.ts CHANGED
@@ -109,6 +109,26 @@ export interface Dungeon {
109
109
  /** Hook function called on every data point. The primary mechanism for engineering deliberate trends and patterns. */
110
110
  hook?: Hook<any>;
111
111
 
112
+ // ── Advanced Features ──
113
+ /** User persona/archetype definitions. Each persona defines a behavioral segment with distinct event volumes, conversion rates, and properties. */
114
+ personas?: Persona[];
115
+ /** World events that affect all users simultaneously (outages, campaigns, product launches). */
116
+ worldEvents?: WorldEvent[] | ResolvedWorldEvent[];
117
+ /** Engagement decay configuration. Controls how user activity decreases over their lifetime. */
118
+ engagementDecay?: EngagementDecay;
119
+ /** Data quality imperfections to inject (nulls, duplicates, bots, late-arriving events). */
120
+ dataQuality?: DataQuality;
121
+ /** Subscription/revenue lifecycle configuration. */
122
+ subscription?: Subscription;
123
+ /** Connected attribution configuration linking campaigns to user acquisition. */
124
+ attribution?: Attribution;
125
+ /** Geographic intelligence: sticky locations, timezone-aware activity, regional launches. */
126
+ geo?: GeoConfig;
127
+ /** Progressive feature adoption: features that launch mid-dataset with S-curve adoption. */
128
+ features?: FeatureConfig[];
129
+ /** Anomaly/outlier injection: extreme values, bursts, coordinated spikes. */
130
+ anomalies?: AnomalyConfig[];
131
+
112
132
  /** Allow arbitrary additional properties on the config. */
113
133
  [key: string]: any;
114
134
 
@@ -246,6 +266,7 @@ export interface Storage {
246
266
  export interface RuntimeState {
247
267
  operations: number;
248
268
  eventCount: number;
269
+ storedEventCount: number;
249
270
  userCount: number;
250
271
  isBatchMode: boolean;
251
272
  verbose: boolean;
@@ -263,7 +284,7 @@ export interface Defaults {
263
284
  browsers: () => any[];
264
285
  campaigns: () => any[];
265
286
  devicePools: { android: any[]; ios: any[]; desktop: any[] };
266
- allDevices:any[];
287
+ allDevices: any[];
267
288
  }
268
289
 
269
290
  /**
@@ -285,11 +306,13 @@ export interface Context {
285
306
  incrementOperations(): void;
286
307
  incrementEvents(): void;
287
308
  incrementUsers(): void;
309
+ incrementStoredEvents(count?: number): void;
288
310
  setStorage(storage: Storage): void;
289
311
 
290
312
  // State getter methods
291
313
  getOperations(): number;
292
314
  getEventCount(): number;
315
+ getStoredEventCount(): number;
293
316
  getUserCount(): number;
294
317
  incrementUserCount(): void;
295
318
  incrementEventCount(): void;
@@ -520,6 +543,298 @@ export type Result = {
520
543
  avgEPS?: number;
521
544
  };
522
545
 
546
+ // ============= Advanced Feature Types =============
547
+
548
+ /**
549
+ * User persona/archetype definition.
550
+ * Personas define behavioral segments with distinct event volumes, conversion rates, and properties.
551
+ */
552
+ export interface Persona {
553
+ /** Unique name for this persona (e.g., "power_user", "casual", "churner"). */
554
+ name: string;
555
+ /** Relative weight for persona assignment (higher = more users get this persona). */
556
+ weight: number;
557
+ /** Multiplier for number of events this persona generates (1.0 = normal). */
558
+ eventMultiplier?: number;
559
+ /** Multiplier for funnel conversion rates (1.0 = normal, 1.3 = 30% better). */
560
+ conversionModifier?: number;
561
+ /** Base churn rate for this persona (0-1). */
562
+ churnRate?: number;
563
+ /** Properties merged into user profiles for this persona. */
564
+ properties?: Record<string, ValueValid>;
565
+ /** Limit how long this persona is active (e.g., trial users active for 14 days). */
566
+ activeWindow?: { maxDays: number };
567
+ /** Per-persona engagement decay override. */
568
+ engagementDecay?: EngagementDecay;
569
+ /** Per-persona soup/timing override. */
570
+ soupOverride?: SoupConfig;
571
+ }
572
+
573
+ /**
574
+ * World event that affects all users simultaneously.
575
+ */
576
+ export interface WorldEvent {
577
+ /** Name of the world event (e.g., "black_friday", "platform_outage"). */
578
+ name: string;
579
+ /** Type category for the event. */
580
+ type?: "campaign" | "outage" | "product_launch" | "holiday" | "incident" | string;
581
+ /** Start day relative to dataset start (e.g., 60 = day 60). */
582
+ startDay: number;
583
+ /** Duration in days (0.25 = 6 hours, null = permanent from startDay onward). */
584
+ duration?: number | null;
585
+ /** Volume multiplier during this event (3.0 = 3x events, 0.1 = 90% drop). */
586
+ volumeMultiplier?: number;
587
+ /** Conversion rate modifier during this event. */
588
+ conversionModifier?: number;
589
+ /** Properties injected into affected events. */
590
+ injectProps?: Record<string, any>;
591
+ /** Which events are affected ("*" for all, or array of event names). */
592
+ affectsEvents?: string[] | "*";
593
+ /** Aftermath period after the event ends. */
594
+ aftermath?: { duration: number; volumeMultiplier: number };
595
+ }
596
+
597
+ /**
598
+ * Resolved world event with absolute timestamps (internal use).
599
+ */
600
+ export interface ResolvedWorldEvent extends WorldEvent {
601
+ /** Absolute start time (unix seconds). */
602
+ startUnix: number;
603
+ /** Absolute end time (unix seconds), or Infinity for permanent events. */
604
+ endUnix: number;
605
+ /** Aftermath end time (unix seconds), if applicable. */
606
+ aftermathEndUnix?: number;
607
+ }
608
+
609
+ /**
610
+ * Engagement decay configuration.
611
+ */
612
+ export interface EngagementDecay {
613
+ /** Decay model type. "none" preserves flat engagement (default). */
614
+ model: "exponential" | "linear" | "step" | "none";
615
+ /** Days until engagement halves (for exponential model). */
616
+ halfLife?: number;
617
+ /** Minimum engagement ratio (0 = can fully churn, 0.1 = never below 10%). */
618
+ floor?: number;
619
+ /** Per-day chance of re-engagement spike after decay. */
620
+ reactivationChance?: number;
621
+ /** Multiplier for engagement during reactivation. */
622
+ reactivationMultiplier?: number;
623
+ }
624
+
625
+ /**
626
+ * Data quality imperfection configuration.
627
+ */
628
+ export interface DataQuality {
629
+ /** Fraction of property values that become null (0-1). */
630
+ nullRate?: number;
631
+ /** Which properties to null ("*" for any, or array of property names). */
632
+ nullProps?: string[] | "*";
633
+ /** Fraction of events that get duplicated (0-1). */
634
+ duplicateRate?: number;
635
+ /** Fraction of events that arrive late (shifted 1-7 days backward) (0-1). */
636
+ lateArrivingRate?: number;
637
+ /** Number of synthetic bot users to inject. */
638
+ botUsers?: number;
639
+ /** Events per bot user (bots generate repetitive, machine-like patterns). */
640
+ botEventsPerUser?: number;
641
+ /** Fraction of events with timezone offset errors (0-1). */
642
+ timezoneConfusion?: number;
643
+ /** Fraction of events missing their event name (0-1). */
644
+ emptyEvents?: number;
645
+ }
646
+
647
+ /**
648
+ * Subscription plan definition.
649
+ */
650
+ export interface SubscriptionPlan {
651
+ /** Plan name (e.g., "free", "starter", "pro"). */
652
+ name: string;
653
+ /** Monthly price. 0 for free tier. */
654
+ price: number;
655
+ /** If true, users start on this plan. */
656
+ default?: boolean;
657
+ /** Trial period in days before requiring payment. */
658
+ trialDays?: number;
659
+ }
660
+
661
+ /**
662
+ * Subscription lifecycle rates.
663
+ */
664
+ export interface SubscriptionLifecycle {
665
+ /** Rate of trial-to-paid conversion (0-1). */
666
+ trialToPayRate?: number;
667
+ /** Monthly upgrade rate (0-1). */
668
+ upgradeRate?: number;
669
+ /** Monthly downgrade rate (0-1). */
670
+ downgradeRate?: number;
671
+ /** Monthly churn/cancellation rate (0-1). */
672
+ churnRate?: number;
673
+ /** Rate of churned users who come back (0-1). */
674
+ winBackRate?: number;
675
+ /** Days before win-back attempt. */
676
+ winBackDelay?: number;
677
+ /** Rate of payment failures (0-1). */
678
+ paymentFailureRate?: number;
679
+ }
680
+
681
+ /**
682
+ * Subscription configuration.
683
+ */
684
+ export interface Subscription {
685
+ /** Available plans, ordered from lowest to highest tier. */
686
+ plans: SubscriptionPlan[];
687
+ /** Lifecycle transition rates. */
688
+ lifecycle?: SubscriptionLifecycle;
689
+ /** Event names for subscription lifecycle events. */
690
+ events?: {
691
+ trialStarted?: string;
692
+ subscribed?: string;
693
+ upgraded?: string;
694
+ downgraded?: string;
695
+ renewed?: string;
696
+ cancelled?: string;
697
+ paymentFailed?: string;
698
+ wonBack?: string;
699
+ };
700
+ }
701
+
702
+ /**
703
+ * Attribution campaign definition.
704
+ */
705
+ export interface AttributionCampaign {
706
+ /** Campaign name. */
707
+ name: string;
708
+ /** UTM source (e.g., "google", "facebook"). */
709
+ source: string;
710
+ /** UTM medium (e.g., "search_ad", "social"). */
711
+ medium?: string;
712
+ /** UTM content (e.g., "variant_a", "hero_image"). */
713
+ utm_content?: string;
714
+ /** UTM term (e.g., "running+shoes", "best+deals"). */
715
+ utm_term?: string;
716
+ /** Active days range [startDay, endDay] relative to dataset start. */
717
+ activeDays: [number, number];
718
+ /** Daily budget range [min, max]. */
719
+ dailyBudget?: [number, number];
720
+ /** Fraction of impressions that become users (0-1). */
721
+ acquisitionRate?: number;
722
+ /** Persona weight biases for users acquired by this campaign. */
723
+ userPersonaBias?: Record<string, number>;
724
+ }
725
+
726
+ /**
727
+ * Connected attribution configuration.
728
+ */
729
+ export interface Attribution {
730
+ /** Attribution model type. */
731
+ model?: "last_touch" | "first_touch" | "linear" | "time_decay";
732
+ /** Attribution window in days. */
733
+ window?: number;
734
+ /** Campaign definitions. */
735
+ campaigns: AttributionCampaign[];
736
+ /** Fraction of users who arrive organically (no campaign) (0-1). */
737
+ organicRate?: number;
738
+ }
739
+
740
+ /**
741
+ * Geographic region definition.
742
+ */
743
+ export interface GeoRegion {
744
+ /** Region name (e.g., "north_america"). */
745
+ name: string;
746
+ /** Country codes in this region. */
747
+ countries: string[];
748
+ /** Weight for user assignment (higher = more users). */
749
+ weight: number;
750
+ /** UTC timezone offset for this region (e.g., -5 for EST). */
751
+ timezoneOffset: number;
752
+ /** Properties injected for users in this region. */
753
+ properties?: Record<string, any>;
754
+ }
755
+
756
+ /**
757
+ * Regional feature launch definition.
758
+ */
759
+ export interface RegionalLaunch {
760
+ /** Region name to match. */
761
+ region: string;
762
+ /** Feature name. */
763
+ featureName: string;
764
+ /** Day the feature launches in this region. */
765
+ startDay: number;
766
+ }
767
+
768
+ /**
769
+ * Geographic intelligence configuration.
770
+ */
771
+ export interface GeoConfig {
772
+ /** If true, users keep their location across all events (default: false for backwards compat). */
773
+ sticky?: boolean;
774
+ /** Region definitions with timezone offsets and properties. */
775
+ regions?: GeoRegion[];
776
+ /** Regional feature launches. */
777
+ regionalLaunches?: RegionalLaunch[];
778
+ }
779
+
780
+ /**
781
+ * Progressive feature adoption configuration.
782
+ */
783
+ export interface FeatureConfig {
784
+ /** Feature name (e.g., "dark_mode", "ai_recommendations"). */
785
+ name: string;
786
+ /** Day the feature launches (relative to dataset start). */
787
+ launchDay: number;
788
+ /** Adoption curve speed or custom logistic params. */
789
+ adoptionCurve?: "fast" | "slow" | "instant" | { k: number; midpoint: number };
790
+ /** Property name to inject on events. */
791
+ property: string;
792
+ /** Possible values for the property. First value is the "before" default if defaultBefore not set. */
793
+ values: any[];
794
+ /** Default value before the feature launches. If not set, property doesn't exist before launch. */
795
+ defaultBefore?: any;
796
+ /** Which events are affected ("*" for all, or array of event names). */
797
+ affectsEvents?: string[] | "*";
798
+ /** Conversion rate lift for users who adopted the feature. */
799
+ conversionLift?: number;
800
+ /** Resolved logistic curve params (set by config-validator). */
801
+ _resolvedCurve?: { k: number; midpoint: number };
802
+ /** Pre-computed adopted values (set by config-validator). */
803
+ _adoptedValues?: any[];
804
+ }
805
+
806
+ /**
807
+ * Anomaly/outlier configuration.
808
+ */
809
+ export interface AnomalyConfig {
810
+ /** Type of anomaly. */
811
+ type: "extreme_value" | "burst" | "coordinated";
812
+ /** Event name this anomaly applies to. */
813
+ event: string;
814
+ /** For extreme_value: property to modify. */
815
+ property?: string;
816
+ /** For extreme_value: fraction of events affected (0-1). */
817
+ frequency?: number;
818
+ /** For extreme_value: multiplier applied to the property value. */
819
+ multiplier?: number;
820
+ /** Tag property added to anomalous events. */
821
+ tag?: string;
822
+ /** For burst/coordinated: day when the anomaly occurs. */
823
+ day?: number;
824
+ /** For burst: duration in days (0.083 = ~2 hours). */
825
+ duration?: number;
826
+ /** For burst/coordinated: time window in days (0.01 = ~15 minutes). */
827
+ window?: number;
828
+ /** For burst/coordinated: number of events to inject. */
829
+ count?: number;
830
+ /** Properties injected on anomalous events. */
831
+ properties?: Record<string, any>;
832
+ /** Resolved absolute start time in unix seconds (set by config-validator). */
833
+ _startUnix?: number;
834
+ /** Resolved absolute end time in unix seconds (set by config-validator). */
835
+ _endUnix?: number;
836
+ }
837
+
523
838
  /**
524
839
  * dungeon-master: generate realistic Mixpanel data at scale
525
840
  *