@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,262 @@
1
+ /**
2
+ * Product Lookup Table Generator
3
+ * Creates a comprehensive product catalog with unique IDs
4
+ * @module product-lookup
5
+ */
6
+
7
+ import Chance from 'chance';
8
+ import { GENERATION_PATTERNS } from '../templates/phrases.js';
9
+
10
+ /**
11
+ * Generate a unique product lookup table
12
+ * @param {number} count - Number of products to generate
13
+ * @param {string} seed - Seed for reproducible results
14
+ * @returns {Map<string, Object>} Map of product_id to product details
15
+ */
16
+ export function generateProductLookup(count = 10000, seed = 'product-catalog') {
17
+ const chance = new Chance(seed);
18
+ const productMap = new Map();
19
+
20
+ // Get all product categories
21
+ const categories = GENERATION_PATTERNS.store_products;
22
+ const categoryNames = Object.keys(categories);
23
+
24
+ // Flatten all products with their categories
25
+ const allProducts = [];
26
+ for (const [categoryKey, products] of Object.entries(categories)) {
27
+ const categoryName = getCategoryDisplayName(categoryKey);
28
+ products.forEach(productName => {
29
+ allProducts.push({
30
+ name: productName,
31
+ category: categoryName,
32
+ categoryKey: categoryKey
33
+ });
34
+ });
35
+ }
36
+
37
+ // Brand names for variety
38
+ const brands = [
39
+ "TechPro", "HomeEssentials", "FitLife", "StyleCo", "BeautyPlus",
40
+ "OfficeMax", "EcoGoods", "PremiumChoice", "ValueBrand", "GenericCo",
41
+ "ProGear", "UltraLife", "SmartHome", "ActiveWear", "LuxeBeauty",
42
+ "WorkSpace", "GreenLiving", "EliteChoice", "BudgetBuy", "QualityFirst",
43
+ "TechnoEdge", "HomeComfort", "FitnessPro", "FashionForward", "GlowUp",
44
+ "OfficeElite", "EarthFriendly", "PremiumPro", "SmartBuy", "TopTier",
45
+ "InnoTech", "CozyHome", "PowerFit", "TrendSet", "PureBeauty",
46
+ "DeskMaster", "NaturalChoice", "LuxuryLine", "BestValue", "PrimeSelect"
47
+ ];
48
+
49
+ // Generate products
50
+ for (let i = 0; i < count; i++) {
51
+ const productId = `PROD-${String(i + 1).padStart(6, '0')}`;
52
+
53
+ // Select a base product (reuse and vary)
54
+ const baseProduct = chance.pickone(allProducts);
55
+
56
+ // Add variation to product name for uniqueness
57
+ let productName = baseProduct.name;
58
+ if (chance.bool({ likelihood: 30 })) {
59
+ // Add color/size/model variations
60
+ const variations = [
61
+ ` - ${chance.pickone(['Black', 'White', 'Blue', 'Red', 'Gray', 'Silver', 'Gold', 'Rose Gold', 'Navy', 'Green'])}`,
62
+ ` - ${chance.pickone(['Small', 'Medium', 'Large', 'X-Large', 'One Size'])}`,
63
+ ` - ${chance.pickone(['Pro', 'Plus', 'Lite', 'Max', 'Mini', 'Ultra', 'Premium', 'Basic', 'Deluxe'])}`,
64
+ ` (${chance.pickone(['2024 Model', '2025 Edition', 'Updated Version', 'Gen 2', 'v2.0', 'Series 2'])})`,
65
+ ` - ${chance.integer({ min: 1, max: 50 })} Pack`
66
+ ];
67
+
68
+ if (chance.bool({ likelihood: 60 })) {
69
+ productName += chance.pickone(variations);
70
+ }
71
+ }
72
+
73
+ // Generate price based on category
74
+ const priceRange = getPriceRange(baseProduct.categoryKey);
75
+ const basePrice = chance.floating({
76
+ min: priceRange.min,
77
+ max: priceRange.max,
78
+ fixed: 2
79
+ });
80
+
81
+ // Generate rating
82
+ const rating = chance.weighted(
83
+ [5, 4.5, 4, 3.5, 3, 2.5, 2, 1.5, 1],
84
+ [35, 25, 20, 10, 5, 3, 1, 0.5, 0.5]
85
+ );
86
+
87
+ // Stock levels
88
+ const stockLevel = chance.weighted(
89
+ [500, 250, 100, 50, 25, 0],
90
+ [40, 25, 20, 10, 4, 1]
91
+ );
92
+
93
+ const product = {
94
+ product_id: productId,
95
+ name: productName,
96
+ brand: chance.pickone(brands),
97
+ category: baseProduct.category,
98
+ subcategory: baseProduct.categoryKey,
99
+ price: basePrice,
100
+ msrp: chance.bool({ likelihood: 40 }) ? basePrice * chance.floating({ min: 1.1, max: 1.5, fixed: 2 }) : basePrice,
101
+ rating: rating,
102
+ review_count: chance.integer({ min: 0, max: 5000 }),
103
+ stock_level: stockLevel,
104
+ stock_status: getStockStatus(stockLevel),
105
+ sku: `SKU-${chance.string({ length: 8, pool: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' })}`,
106
+ weight_oz: chance.floating({ min: 0.5, max: 50, fixed: 2 }),
107
+ is_featured: chance.bool({ likelihood: 10 }),
108
+ is_bestseller: chance.bool({ likelihood: 15 }),
109
+ is_new_arrival: chance.bool({ likelihood: 20 }),
110
+ is_on_sale: chance.bool({ likelihood: 25 }),
111
+ discount_percent: chance.bool({ likelihood: 25 }) ? chance.integer({ min: 5, max: 50 }) : 0,
112
+ shipping_eligible: chance.bool({ likelihood: 90 }),
113
+ free_shipping: chance.bool({ likelihood: 40 }),
114
+ prime_eligible: chance.bool({ likelihood: 60 }),
115
+ returnable: chance.bool({ likelihood: 85 }),
116
+ warranty_months: chance.pickone([0, 3, 6, 12, 24, 36]),
117
+ release_date: chance.date({
118
+ year: chance.integer({ min: 2020, max: 2025 })
119
+ }).toISOString().split('T')[0],
120
+ supplier_id: `SUP-${chance.integer({ min: 1, max: 500 })}`,
121
+ tags: generateTags(baseProduct.categoryKey, chance)
122
+ };
123
+
124
+ productMap.set(productId, product);
125
+ }
126
+
127
+ return productMap;
128
+ }
129
+
130
+ /**
131
+ * Convert a product lookup Map to a plain object for export
132
+ */
133
+ export function productLookupToObject(productMap) {
134
+ const obj = {};
135
+ for (const [id, product] of productMap.entries()) {
136
+ obj[id] = product;
137
+ }
138
+ return obj;
139
+ }
140
+
141
+ /**
142
+ * Get a random product from the lookup
143
+ */
144
+ export function getRandomProduct(productMap, chance) {
145
+ const keys = Array.from(productMap.keys());
146
+ const randomKey = chance.pickone(keys);
147
+ return productMap.get(randomKey);
148
+ }
149
+
150
+ /**
151
+ * Get products by category
152
+ */
153
+ export function getProductsByCategory(productMap, category) {
154
+ return Array.from(productMap.values()).filter(p => p.category === category);
155
+ }
156
+
157
+ /**
158
+ * Get products by price range
159
+ */
160
+ export function getProductsByPriceRange(productMap, minPrice, maxPrice) {
161
+ return Array.from(productMap.values()).filter(p => p.price >= minPrice && p.price <= maxPrice);
162
+ }
163
+
164
+ /**
165
+ * Search products by name
166
+ */
167
+ export function searchProducts(productMap, query) {
168
+ const lowerQuery = query.toLowerCase();
169
+ return Array.from(productMap.values()).filter(p =>
170
+ p.name.toLowerCase().includes(lowerQuery) ||
171
+ p.brand.toLowerCase().includes(lowerQuery) ||
172
+ p.category.toLowerCase().includes(lowerQuery)
173
+ );
174
+ }
175
+
176
+ // Helper functions
177
+
178
+ function getCategoryDisplayName(categoryKey) {
179
+ const displayNames = {
180
+ electronics: 'Electronics',
181
+ home_kitchen: 'Home & Kitchen',
182
+ clothing: 'Clothing',
183
+ beauty: 'Beauty & Personal Care',
184
+ sports: 'Sports & Outdoors',
185
+ books: 'Books & Media',
186
+ toys: 'Toys & Games',
187
+ pet_supplies: 'Pet Supplies',
188
+ office: 'Office Supplies',
189
+ baby: 'Baby Products',
190
+ automotive: 'Automotive',
191
+ garden: 'Garden & Outdoor'
192
+ };
193
+ return displayNames[categoryKey] || categoryKey;
194
+ }
195
+
196
+ function getPriceRange(categoryKey) {
197
+ const ranges = {
198
+ electronics: { min: 9.99, max: 799.99 },
199
+ home_kitchen: { min: 4.99, max: 399.99 },
200
+ clothing: { min: 9.99, max: 199.99 },
201
+ beauty: { min: 3.99, max: 89.99 },
202
+ sports: { min: 7.99, max: 299.99 },
203
+ books: { min: 4.99, max: 49.99 },
204
+ toys: { min: 5.99, max: 149.99 },
205
+ pet_supplies: { min: 3.99, max: 99.99 },
206
+ office: { min: 1.99, max: 499.99 },
207
+ baby: { min: 4.99, max: 299.99 },
208
+ automotive: { min: 9.99, max: 199.99 },
209
+ garden: { min: 5.99, max: 249.99 }
210
+ };
211
+ return ranges[categoryKey] || { min: 9.99, max: 99.99 };
212
+ }
213
+
214
+ function getStockStatus(stockLevel) {
215
+ if (stockLevel === 0) return 'Out of Stock';
216
+ if (stockLevel < 10) return 'Low Stock';
217
+ if (stockLevel < 50) return 'Limited Stock';
218
+ return 'In Stock';
219
+ }
220
+
221
+ function generateTags(categoryKey, chance) {
222
+ const commonTags = ['popular', 'trending', 'recommended', 'verified', 'quality'];
223
+ const categoryTags = {
224
+ electronics: ['tech', 'gadget', 'smart', 'wireless', 'digital'],
225
+ home_kitchen: ['home', 'kitchen', 'cooking', 'storage', 'organization'],
226
+ clothing: ['fashion', 'apparel', 'style', 'comfort', 'casual'],
227
+ beauty: ['skincare', 'makeup', 'grooming', 'wellness', 'organic'],
228
+ sports: ['fitness', 'outdoor', 'active', 'training', 'exercise'],
229
+ books: ['reading', 'literature', 'educational', 'entertainment'],
230
+ toys: ['kids', 'fun', 'educational', 'creative', 'play'],
231
+ pet_supplies: ['pets', 'animals', 'care', 'feeding', 'play'],
232
+ office: ['work', 'productivity', 'organization', 'supplies'],
233
+ baby: ['infant', 'nursery', 'feeding', 'safety', 'care'],
234
+ automotive: ['car', 'vehicle', 'maintenance', 'accessories'],
235
+ garden: ['outdoor', 'plants', 'gardening', 'landscaping']
236
+ };
237
+
238
+ const tags = [];
239
+ const catTags = categoryTags[categoryKey] || [];
240
+
241
+ // Add 2-4 category tags
242
+ for (let i = 0; i < chance.integer({ min: 2, max: 4 }); i++) {
243
+ const tag = chance.pickone(catTags);
244
+ if (!tags.includes(tag)) tags.push(tag);
245
+ }
246
+
247
+ // Maybe add a common tag
248
+ if (chance.bool({ likelihood: 40 })) {
249
+ tags.push(chance.pickone(commonTags));
250
+ }
251
+
252
+ return tags;
253
+ }
254
+
255
+ export default {
256
+ generateProductLookup,
257
+ productLookupToObject,
258
+ getRandomProduct,
259
+ getProductsByCategory,
260
+ getProductsByPriceRange,
261
+ searchProducts
262
+ };
@@ -0,0 +1,195 @@
1
+ /**
2
+ * E-commerce Product Name Generator
3
+ * Generates realistic product names for store inventories, reviews, and search queries
4
+ * @module product-names
5
+ */
6
+
7
+ import { createTextGenerator } from './text.js';
8
+
9
+ // Product Review Generator - For customer reviews mentioning specific products
10
+ export const productReviewGen = createTextGenerator({
11
+ style: "review",
12
+ tone: "pos",
13
+ intensity: "medium",
14
+ formality: "casual",
15
+ keywords: {
16
+ products: [
17
+ // Electronics
18
+ "Wireless Bluetooth Headphones", "USB-C Charging Cable", "Portable Power Bank 20000mAh",
19
+ "Smart Watch Series 7", "Wireless Gaming Mouse", "4K Webcam Pro",
20
+ "Bluetooth Speaker Waterproof", "Ring Light for Streaming",
21
+
22
+ // Home & Kitchen
23
+ "Stainless Steel Cookware Set", "Air Fryer 6 Quart Digital", "Instant Pot 8 Quart",
24
+ "Coffee Maker Programmable", "Blender High Speed 1200W",
25
+
26
+ // Clothing
27
+ "Men's Cotton T-Shirt Basic", "Women's Yoga Pants High Waist", "Athletic Shorts",
28
+ "Sneakers Running Lightweight", "Fleece Jacket Zip Up"
29
+ ],
30
+ features: [
31
+ "battery life", "sound quality", "build quality", "comfort", "durability",
32
+ "ease of use", "design", "value for money", "performance", "fit",
33
+ "material quality", "charging speed", "connectivity", "portability",
34
+ "warranty", "customer service", "packaging", "instructions",
35
+ "compatibility", "functionality", "aesthetics", "weight",
36
+ "size options", "color accuracy", "temperature control", "noise level"
37
+ ],
38
+ positive_aspects: [
39
+ "works great", "perfect for my needs", "exceeded expectations",
40
+ "highly recommend", "best purchase ever", "worth every penny",
41
+ "can't live without it", "game changer", "solid quality",
42
+ "does exactly what it claims", "super reliable", "love it so far",
43
+ "impressed with quality", "fast shipping too", "great value"
44
+ ],
45
+ negative_aspects: [
46
+ "a bit pricey", "could be better", "takes getting used to",
47
+ "wish it had more features", "broke after a month", "not as advertised",
48
+ "quality could be improved", "too bulky", "battery drains quickly",
49
+ "doesn't fit well", "cheaply made", "stopped working"
50
+ ]
51
+ },
52
+ mixedSentiment: true,
53
+ authenticityLevel: 0.6,
54
+ typos: true,
55
+ typoRate: 0.03,
56
+ min: 50,
57
+ max: 200,
58
+ includeMetadata: false
59
+ });
60
+
61
+ // Product Search Query Generator - For realistic search terms
62
+ export const productSearchGen = createTextGenerator({
63
+ style: "search",
64
+ tone: "neu",
65
+ formality: "casual",
66
+ keywords: {
67
+ products: [
68
+ "headphones", "charging cable", "power bank", "smart watch", "gaming mouse",
69
+ "webcam", "speaker", "ring light", "cookware set", "air fryer",
70
+ "instant pot", "coffee maker", "blender", "yoga pants", "running shoes",
71
+ "fleece jacket", "facial cleanser", "moisturizer", "shampoo", "toothbrush",
72
+ "yoga mat", "dumbbells", "water bottle", "camping tent", "sleeping bag",
73
+ "notebook", "pens", "desk chair", "monitor stand", "baby monitor"
74
+ ],
75
+ attributes: [
76
+ "wireless", "bluetooth", "USB-C", "waterproof", "portable", "lightweight",
77
+ "rechargeable", "adjustable", "foldable", "compact", "ergonomic",
78
+ "stainless steel", "non-stick", "organic", "eco-friendly", "sustainable",
79
+ "durable", "heavy duty", "professional", "premium", "budget",
80
+ "best rated", "top seller", "on sale", "discounted", "clearance"
81
+ ],
82
+ modifiers: [
83
+ "cheap", "affordable", "best", "good quality", "reliable",
84
+ "for beginners", "professional grade", "commercial", "industrial",
85
+ "for small spaces", "travel size", "family size", "bulk",
86
+ "name brand", "generic", "off brand", "authentic", "genuine",
87
+ "refurbished", "open box", "like new", "gently used"
88
+ ]
89
+ },
90
+ typos: true,
91
+ typoRate: 0.05,
92
+ min: 2,
93
+ max: 40,
94
+ includeMetadata: false
95
+ });
96
+
97
+ // Product Comparison Generator - For side-by-side product comparisons
98
+ export const productComparisonGen = createTextGenerator({
99
+ style: "feedback",
100
+ tone: "neu",
101
+ formality: "business",
102
+ keywords: {
103
+ products: [
104
+ "Wireless Bluetooth Headphones", "Air Fryer 6 Quart Digital",
105
+ "Smart Watch Series 7", "Coffee Maker Programmable",
106
+ "Yoga Mat Non-Slip 6mm", "Instant Pot 8 Quart"
107
+ ],
108
+ comparison_terms: [
109
+ "compared to", "versus", "better than", "not as good as",
110
+ "similar to", "different from", "cheaper alternative to",
111
+ "premium version of", "budget option for", "upgraded from"
112
+ ],
113
+ metrics: [
114
+ "price point", "durability", "features", "warranty coverage",
115
+ "brand reputation", "user reviews", "availability",
116
+ "customer support", "build quality", "performance metrics"
117
+ ]
118
+ },
119
+ authenticityLevel: 0.7,
120
+ specificityLevel: 0.8,
121
+ min: 100,
122
+ max: 300,
123
+ includeMetadata: false
124
+ });
125
+
126
+ // Product Question Generator - For Q&A sections
127
+ export const productQuestionGen = createTextGenerator({
128
+ style: "forum",
129
+ tone: "neu",
130
+ formality: "casual",
131
+ keywords: {
132
+ products: [
133
+ "Smart Watch Series 7", "Air Fryer 6 Quart", "Wireless Headphones",
134
+ "Yoga Mat", "Power Bank", "Instant Pot", "Bluetooth Speaker"
135
+ ],
136
+ question_starters: [
137
+ "Does this work with", "Is this compatible with", "Can I use this for",
138
+ "Will this fit", "How long does", "What's the difference between",
139
+ "Is this better than", "Should I get", "Any recommendations for",
140
+ "Has anyone tried", "Is it worth", "Does anyone know if"
141
+ ],
142
+ concerns: [
143
+ "battery life", "warranty", "return policy", "shipping time",
144
+ "compatibility", "size", "color options", "durability",
145
+ "maintenance", "cleaning", "storage", "safety",
146
+ "noise level", "power consumption", "assembly required"
147
+ ]
148
+ },
149
+ typos: true,
150
+ typoRate: 0.04,
151
+ min: 20,
152
+ max: 100,
153
+ includeMetadata: false
154
+ });
155
+
156
+ // Product Description Generator - For store listings
157
+ export const productDescriptionGen = createTextGenerator({
158
+ style: "feedback",
159
+ tone: "pos",
160
+ formality: "business",
161
+ keywords: {
162
+ products: [
163
+ "Premium Wireless Headphones", "Professional Air Fryer", "Advanced Smart Watch",
164
+ "Deluxe Coffee Maker", "Ultra Comfort Yoga Mat", "Multi-Function Instant Pot"
165
+ ],
166
+ features: [
167
+ "state-of-the-art technology", "innovative design", "superior quality",
168
+ "advanced features", "user-friendly interface", "premium materials",
169
+ "long-lasting durability", "exceptional performance", "modern aesthetics",
170
+ "versatile functionality", "energy efficient", "space-saving design",
171
+ "easy maintenance", "comprehensive warranty", "fast setup",
172
+ "multiple settings", "customizable options", "safety certified"
173
+ ],
174
+ benefits: [
175
+ "saves time", "improves efficiency", "enhances experience",
176
+ "provides convenience", "offers flexibility", "ensures reliability",
177
+ "delivers results", "maximizes value", "simplifies tasks",
178
+ "reduces effort", "increases productivity", "optimizes performance"
179
+ ]
180
+ },
181
+ authenticityLevel: 0.5,
182
+ specificityLevel: 0.7,
183
+ min: 80,
184
+ max: 250,
185
+ includeMetadata: false
186
+ });
187
+
188
+ // Export all generators
189
+ export default {
190
+ productReviewGen,
191
+ productSearchGen,
192
+ productComparisonGen,
193
+ productQuestionGen,
194
+ productDescriptionGen
195
+ };
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Profile generator module
3
+ * Creates user and group profiles with realistic properties
4
+ */
5
+
6
+ /** @typedef {import('../../types').Context} Context */
7
+
8
+ import * as u from "../utils/utils.js";
9
+ import { dataLogger as logger } from "../utils/logger.js";
10
+
11
+ /**
12
+ * Creates a user or group profile by choosing from available property values
13
+ * @param {Context} context - Context object containing config, defaults, etc.
14
+ * @param {Object} props - Properties to include in the profile
15
+ * @param {Object} defaults - Default values to merge with props
16
+ * @returns {Promise<Object>} Generated profile object
17
+ */
18
+ export async function makeProfile(context, props = {}, defaults = {}) {
19
+ // Update operation counter
20
+ context.incrementOperations();
21
+
22
+ // Keys that should not be processed with the choose function
23
+ const keysToNotChoose = ["anonymousIds", "sessionIds"];
24
+
25
+ // Start with defaults
26
+ const profile = { ...defaults };
27
+
28
+ // Process default values first
29
+ for (const key in profile) {
30
+ if (keysToNotChoose.includes(key)) continue;
31
+
32
+ try {
33
+ profile[key] = u.choose(profile[key]);
34
+ } catch (e) {
35
+ logger.error({ err: e, key }, `Error processing default property ${key}`);
36
+ // Keep original value on error
37
+ }
38
+ }
39
+
40
+ // Process provided props (these override defaults)
41
+ for (const key in props) {
42
+ try {
43
+ profile[key] = u.choose(props[key]);
44
+ } catch (e) {
45
+ logger.error({ err: e, key }, `Error processing property ${key}`);
46
+ // Keep original value on error
47
+ }
48
+ }
49
+
50
+ return profile;
51
+ }
52
+
53
+ /**
54
+ * Creates a user profile with typical user properties
55
+ * @param {Context} context - Context object
56
+ * @param {Object} userProps - User-specific properties
57
+ * @param {Object} baseProfile - Base profile to extend
58
+ * @returns {Promise<Object>} Generated user profile
59
+ */
60
+ export async function makeUserProfile(context, userProps = {}, baseProfile = {}) {
61
+ const { config } = context;
62
+
63
+ // Combine user props with any configured user properties
64
+ const combinedProps = {
65
+ ...config.userProps,
66
+ ...userProps
67
+ };
68
+
69
+ return makeProfile(context, combinedProps, baseProfile);
70
+ }
71
+
72
+ /**
73
+ * Creates a group profile with group-specific properties
74
+ * @param {Context} context - Context object
75
+ * @param {string} groupKey - Group identifier
76
+ * @param {Object} groupProps - Group-specific properties
77
+ * @param {Object} baseProfile - Base profile to extend
78
+ * @returns {Promise<Object>} Generated group profile
79
+ */
80
+ export async function makeGroupProfile(context, groupKey, groupProps = {}, baseProfile = {}) {
81
+ const { config } = context;
82
+
83
+ // Get group properties from config for this specific group
84
+ const configGroupProps = config.groupProps?.[groupKey] || {};
85
+
86
+ // Combine with provided props
87
+ const combinedProps = {
88
+ ...configGroupProps,
89
+ ...groupProps,
90
+ };
91
+
92
+ return makeProfile(context, combinedProps, baseProfile);
93
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Slowly Changing Dimensions (SCD) generator module
3
+ * Creates time-series data showing how properties change over time
4
+ */
5
+
6
+ /** @typedef {import('../../types').Context} Context */
7
+
8
+ import dayjs from "dayjs";
9
+ import * as u from "../utils/utils.js";
10
+ import { makeProfile } from "./profiles.js";
11
+
12
+ /**
13
+ * Creates SCD (Slowly Changing Dimensions) entries for a given property
14
+ * @param {Context} context - Context object containing config, defaults, etc.
15
+ * @param {Array|Object} scdProp - SCD property configuration or simple array of values
16
+ * @param {string} scdKey - Key name for the SCD property
17
+ * @param {string} distinct_id - User/entity identifier
18
+ * @param {number} mutations - Number of mutations to create
19
+ * @param {string | number} created - Creation timestamp for the entity
20
+ * @returns {Promise<Array>} Array of SCD entries
21
+ */
22
+ export async function makeSCD(context, scdProp, scdKey, distinct_id, mutations, created) {
23
+ // Convert simple array to full configuration object
24
+ if (Array.isArray(scdProp)) {
25
+ scdProp = {
26
+ values: scdProp,
27
+ frequency: 'week',
28
+ max: 10,
29
+ timing: 'fuzzy',
30
+ type: 'user'
31
+ };
32
+ }
33
+ if (typeof created === 'number') created = dayjs.unix(created).toISOString();
34
+
35
+ const {
36
+ frequency = 'day', // Default to daily frequency
37
+ max = 10, // Default max mutations
38
+ timing = 'fuzzy', // Default to fuzzy timing
39
+ values,
40
+ type = "user"
41
+ } = scdProp;
42
+
43
+ // Return empty array if no values provided
44
+ if (!values || (Array.isArray(values) && values.length === 0) ||
45
+ (typeof values === 'object' && Object.keys(values).length === 0)) {
46
+ return [];
47
+ }
48
+
49
+ const scdEntries = [];
50
+ let lastInserted = dayjs(created);
51
+ const now = dayjs();
52
+ const deltaDays = now.diff(lastInserted, "day");
53
+ const uuidKeyName = type === 'user' ? 'distinct_id' : type;
54
+ let lastStartTime = null; // Track for monotonic ordering
55
+
56
+ for (let i = 0; i < mutations; i++) {
57
+ // Stop if we've reached the current time
58
+ if (lastInserted.isAfter(now)) break;
59
+
60
+ // Create profile with the SCD property
61
+ const scd = await makeProfile(context, { [scdKey]: values }, { [uuidKeyName]: distinct_id });
62
+
63
+ // Create SCD entry with all required properties
64
+ const scdEntry = {
65
+ ...scd,
66
+ [uuidKeyName]: scd.distinct_id || distinct_id,
67
+ startTime: null,
68
+ insertTime: null,
69
+ time: null // Required field for Mixpanel SCD import
70
+ };
71
+
72
+ // Set start time based on timing strategy
73
+ if (timing === 'fixed') {
74
+ switch (frequency) {
75
+ case "day":
76
+ scdEntry.startTime = lastInserted.add(1, "day").startOf('day').toISOString();
77
+ break;
78
+ case "week":
79
+ scdEntry.startTime = lastInserted.add(1, "week").startOf('week').toISOString();
80
+ break;
81
+ case "month":
82
+ scdEntry.startTime = lastInserted.add(1, "month").startOf('month').toISOString();
83
+ break;
84
+ case "year":
85
+ scdEntry.startTime = lastInserted.add(1, "year").startOf('year').toISOString();
86
+ break;
87
+ }
88
+ }
89
+
90
+ if (timing === 'fuzzy') {
91
+ scdEntry.startTime = lastInserted.toISOString();
92
+ }
93
+
94
+ // Enforce monotonic ordering — startTime must advance
95
+ if (lastStartTime && scdEntry.startTime && scdEntry.startTime <= lastStartTime) {
96
+ scdEntry.startTime = dayjs(lastStartTime).add(1, "second").toISOString();
97
+ }
98
+ lastStartTime = scdEntry.startTime;
99
+
100
+ // Set insert time (slightly after start time)
101
+ const insertTime = dayjs(scdEntry.startTime).add(u.integer(1, 9000), "seconds");
102
+ scdEntry.insertTime = insertTime.toISOString();
103
+
104
+ // Set the time field for Mixpanel SCD import (uses startTime)
105
+ scdEntry.time = scdEntry.startTime;
106
+
107
+ // Only add entry if all required properties are set
108
+ if (scdEntry.startTime && scdEntry.insertTime && scdEntry.time) {
109
+ scdEntries.push(scdEntry);
110
+ }
111
+
112
+ // Advance time for next entry — scale step size to available range
113
+ if (deltaDays > 1) {
114
+ const advance = Math.max(1, u.integer(0, Math.ceil(deltaDays / mutations)));
115
+ lastInserted = dayjs(scdEntry.startTime).add(advance, "day");
116
+ } else {
117
+ // Short range: advance by hours instead of days
118
+ const advance = Math.max(1, u.integer(1, 12));
119
+ lastInserted = dayjs(scdEntry.startTime).add(advance, "hour");
120
+ }
121
+ }
122
+
123
+ return scdEntries;
124
+ }