@ak--47/dungeon-master 1.0.0 → 1.1.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.
- package/README.md +7 -7
- package/dungeons/technical/ad-spend-schema.json +128 -0
- package/dungeons/technical/ad-spend.js +116 -0
- package/dungeons/technical/anonymous-users-schema.json +91 -0
- package/dungeons/technical/anonymous-users.js +102 -0
- package/dungeons/technical/array-of-object-lookup-schema.json +191 -0
- package/dungeons/{array-of-object-lookup.js → technical/array-of-object-lookup.js} +11 -4
- package/dungeons/technical/experiments-schema.json +197 -0
- package/dungeons/technical/experiments.js +168 -0
- package/dungeons/technical/foobar-schema.json +362 -0
- package/dungeons/{foobar.js → technical/foobar.js} +2 -2
- package/dungeons/technical/group-analytics-schema.json +149 -0
- package/dungeons/technical/group-analytics.js +151 -0
- package/dungeons/technical/mirror-strategies-schema.json +84 -0
- package/dungeons/technical/mirror-strategies.js +126 -0
- package/dungeons/technical/nested-objects-schema.json +145 -0
- package/dungeons/technical/nested-objects.js +207 -0
- package/dungeons/technical/retention-cadence-schema.json +37 -0
- package/dungeons/{retention-cadence.js → technical/retention-cadence.js} +1 -1
- package/dungeons/technical/sanity-schema.json +185 -0
- package/dungeons/{sanity.js → technical/sanity.js} +3 -2
- package/dungeons/technical/scale-test-schema.json +70 -0
- package/dungeons/technical/scale-test.js +90 -0
- package/dungeons/technical/scd-schema.json +270 -0
- package/dungeons/{scd.js → technical/scd.js} +19 -10
- package/dungeons/technical/simple-schema.json +278 -0
- package/dungeons/{simple.js → technical/simple.js} +6 -6
- package/dungeons/technical/simplest-schema.json +635 -0
- package/dungeons/{simplest.js → technical/simplest.js} +6 -6
- package/dungeons/technical/text-generation-schema.json +1059 -0
- package/dungeons/{text-generation.js → technical/text-generation.js} +23 -9
- package/dungeons/vertical/community-schema.json +557 -0
- package/dungeons/vertical/community.js +788 -0
- package/dungeons/vertical/devtools-schema.json +580 -0
- package/dungeons/vertical/devtools.js +837 -0
- package/dungeons/vertical/ecommerce-schema.json +586 -0
- package/dungeons/{ecommerce.js → vertical/ecommerce.js} +34 -3
- package/dungeons/vertical/education-schema.json +697 -0
- package/dungeons/{education.js → vertical/education.js} +54 -36
- package/dungeons/vertical/fintech-schema.json +563 -0
- package/dungeons/{fintech.js → vertical/fintech.js} +91 -53
- package/dungeons/vertical/fitness-schema.json +508 -0
- package/dungeons/vertical/fitness.js +802 -0
- package/dungeons/vertical/food-delivery-schema.json +678 -0
- package/dungeons/{food.js → vertical/food-delivery.js} +51 -23
- package/dungeons/vertical/gaming-schema.json +394 -0
- package/dungeons/{gaming.js → vertical/gaming.js} +9 -7
- package/dungeons/vertical/healthcare-schema.json +527 -0
- package/dungeons/vertical/healthcare.js +769 -0
- package/dungeons/vertical/insurance-application-schema.json +466 -0
- package/dungeons/{insurance-application.js → vertical/insurance-application.js} +33 -29
- package/dungeons/vertical/logistics-schema.json +552 -0
- package/dungeons/vertical/logistics.js +793 -0
- package/dungeons/vertical/marketplace-schema.json +511 -0
- package/dungeons/vertical/marketplace.js +844 -0
- package/dungeons/vertical/media-schema.json +787 -0
- package/dungeons/{media.js → vertical/media.js} +94 -16
- package/dungeons/vertical/rpg-schema.json +654 -0
- package/dungeons/{rpg.js → vertical/rpg.js} +78 -57
- package/dungeons/vertical/sass-schema.json +653 -0
- package/dungeons/{sass.js → vertical/sass.js} +49 -23
- package/dungeons/vertical/social-schema.json +552 -0
- package/dungeons/{social.js → vertical/social.js} +60 -14
- package/dungeons/vertical/travel-schema.json +558 -0
- package/dungeons/vertical/travel.js +736 -0
- package/index.js +19 -15
- package/lib/core/config-validator.js +286 -1
- package/lib/core/context.js +7 -5
- package/lib/generators/adspend.js +3 -3
- package/lib/generators/events.js +141 -14
- package/lib/generators/funnels.js +67 -7
- package/lib/generators/text.js +2 -2
- package/lib/orchestrators/user-loop.js +441 -22
- package/lib/templates/soup-presets.js +2 -2
- package/lib/utils/utils.js +4 -5
- package/package.json +4 -3
- package/scripts/dungeon-to-json.mjs +163 -0
- package/scripts/extract-dungeon-schema.mjs +297 -0
- package/scripts/json-to-dungeon.mjs +200 -0
- package/scripts/run-dungeon.mjs +76 -0
- package/scripts/run-many.mjs +118 -0
- package/scripts/verify-runner.mjs +42 -0
- package/types.d.ts +313 -1
- package/dungeons/array-of-object-lookup-schema.json +0 -327
- package/dungeons/ecommerce-schema.json +0 -462
- package/dungeons/education-schema.json +0 -2409
- package/dungeons/fintech-schema.json +0 -14034
- package/dungeons/foobar-schema.json +0 -403
- package/dungeons/food-delivery-schema.json +0 -192
- package/dungeons/food-delivery.js +0 -602
- package/dungeons/food-schema.json +0 -1152
- package/dungeons/gaming-schema.json +0 -1270
- package/dungeons/insurance-application-schema.json +0 -204
- package/dungeons/media-schema.json +0 -906
- package/dungeons/retention-cadence-schema.json +0 -78
- package/dungeons/rpg-schema.json +0 -4526
- package/dungeons/sanity-schema.json +0 -255
- package/dungeons/sass-schema.json +0 -1291
- package/dungeons/scd-schema.json +0 -919
- package/dungeons/simple-schema.json +0 -608
- package/dungeons/simplest-schema.json +0 -1418
- package/dungeons/social-schema.json +0 -1118
- package/dungeons/text-generation-schema.json +0 -3096
|
@@ -62,8 +62,8 @@ export const SOUP_PRESETS = {
|
|
|
62
62
|
* Clear peaks and valleys, dramatic variation. Fewer Gaussian clusters + tight deviation.
|
|
63
63
|
*/
|
|
64
64
|
spiky: {
|
|
65
|
-
peaks: (numDays) => Math.max(
|
|
66
|
-
deviation:
|
|
65
|
+
peaks: (numDays) => Math.max(4, Math.ceil(numDays / 15)),
|
|
66
|
+
deviation: 5,
|
|
67
67
|
mean: 0,
|
|
68
68
|
dayOfWeekWeights: REAL_DOW,
|
|
69
69
|
hourOfDayWeights: REAL_HOD,
|
package/lib/utils/utils.js
CHANGED
|
@@ -44,11 +44,10 @@ RNG
|
|
|
44
44
|
* @returns {Chance}
|
|
45
45
|
*/
|
|
46
46
|
function initChance(seed) {
|
|
47
|
-
|
|
48
|
-
if (!
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
47
|
+
// Use env SEED only as fallback when no seed is explicitly passed
|
|
48
|
+
if (!seed && process.env.SEED) seed = process.env.SEED;
|
|
49
|
+
globalChance = new Chance(seed);
|
|
50
|
+
chanceInitialized = true;
|
|
52
51
|
return globalChance;
|
|
53
52
|
}
|
|
54
53
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ak--47/dungeon-master",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "generate fancy datasets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"lib/",
|
|
20
20
|
"dungeons/",
|
|
21
21
|
"!dungeons/customers/",
|
|
22
|
+
"scripts/",
|
|
22
23
|
"package.json",
|
|
23
24
|
"README.md"
|
|
24
25
|
],
|
|
@@ -32,10 +33,10 @@
|
|
|
32
33
|
"typecheck": "tsc --noEmit",
|
|
33
34
|
"dev": "nodemon scratch.mjs --ignore ./data/*",
|
|
34
35
|
"prune": "rm -f ./data/* && rm -f ./tmp/* && rm -f vscode-profile-*",
|
|
35
|
-
"deps": "./scripts/update-deps.sh",
|
|
36
36
|
"dungeon:run": "node ./scripts/run-dungeon.mjs",
|
|
37
37
|
"dungeon:to-json": "node ./scripts/dungeon-to-json.mjs",
|
|
38
|
-
"dungeon:from-json": "node ./scripts/json-to-dungeon.mjs"
|
|
38
|
+
"dungeon:from-json": "node ./scripts/json-to-dungeon.mjs",
|
|
39
|
+
"dungeon:schema": "node ./scripts/extract-dungeon-schema.mjs"
|
|
39
40
|
},
|
|
40
41
|
"repository": {
|
|
41
42
|
"type": "git",
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Converts a JavaScript dungeon file to JSON that can be loaded into the UI
|
|
5
|
+
* Usage: node scripts/dungeon-to-json.js <input.js> [output-name]
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { writeFileSync } from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
|
|
12
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
|
|
14
|
+
// Get command line arguments
|
|
15
|
+
const args = process.argv.slice(2);
|
|
16
|
+
|
|
17
|
+
if (args.length === 0) {
|
|
18
|
+
console.error('❌ Error: Please provide an input JavaScript file');
|
|
19
|
+
console.log('Usage: node scripts/dungeon-to-json.js <input.js> [output-name]');
|
|
20
|
+
console.log('Example: node scripts/dungeon-to-json.js ./dungeons/my-dungeon.js my-dungeon');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const inputPath = path.resolve(args[0]);
|
|
25
|
+
const outputName = args[1] || path.basename(inputPath, '.js');
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
// Import the JavaScript module
|
|
29
|
+
console.log(`📖 Loading ${inputPath}...`);
|
|
30
|
+
const module = await import(`file://${inputPath}`);
|
|
31
|
+
const config = module.default;
|
|
32
|
+
|
|
33
|
+
if (!config) {
|
|
34
|
+
throw new Error('No default export found in the module');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Extract hooks if they exist
|
|
38
|
+
const hooksFunction = config.hook;
|
|
39
|
+
const hooksString = hooksFunction ? hooksFunction.toString() : null;
|
|
40
|
+
|
|
41
|
+
// Create a clean config without the hook function
|
|
42
|
+
const cleanConfig = { ...config };
|
|
43
|
+
delete cleanConfig.hook;
|
|
44
|
+
|
|
45
|
+
// Convert to JSON-serializable format
|
|
46
|
+
console.log('🔄 Converting to JSON format...');
|
|
47
|
+
const schema = convertToJSON(cleanConfig);
|
|
48
|
+
|
|
49
|
+
// Create the dungeon state object (same format as UI saves)
|
|
50
|
+
const dungeonState = {
|
|
51
|
+
schema,
|
|
52
|
+
hooks: hooksString,
|
|
53
|
+
timestamp: new Date().toISOString(),
|
|
54
|
+
version: '4.0'
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Write to JSON file
|
|
58
|
+
const outputPath = path.join(path.dirname(inputPath), `${outputName}.json`);
|
|
59
|
+
writeFileSync(outputPath, JSON.stringify(dungeonState, null, 2), 'utf-8');
|
|
60
|
+
|
|
61
|
+
console.log(`✅ Successfully created ${outputPath}`);
|
|
62
|
+
console.log(`\n💡 You can now load this file in the dungeon-master UI:`);
|
|
63
|
+
console.log(` Click "Load" and select ${outputName}.json`);
|
|
64
|
+
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error('❌ Error:', error.message);
|
|
67
|
+
console.error(error.stack);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Convert JavaScript config to JSON-serializable format
|
|
73
|
+
* Detects functions and converts them to object representation
|
|
74
|
+
*/
|
|
75
|
+
function convertToJSON(value) {
|
|
76
|
+
// Null/undefined
|
|
77
|
+
if (value === null || value === undefined) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Primitives
|
|
82
|
+
if (typeof value === 'boolean' || typeof value === 'number' || typeof value === 'string') {
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Functions - convert to object representation
|
|
87
|
+
if (typeof value === 'function') {
|
|
88
|
+
return convertFunctionToObject(value);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Arrays
|
|
92
|
+
if (Array.isArray(value)) {
|
|
93
|
+
return value.map(item => convertToJSON(item));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Objects
|
|
97
|
+
if (typeof value === 'object') {
|
|
98
|
+
const result = {};
|
|
99
|
+
for (const [key, val] of Object.entries(value)) {
|
|
100
|
+
result[key] = convertToJSON(val);
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Fallback
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Convert a function to its object representation
|
|
111
|
+
* Tries to detect the function type and extract parameters
|
|
112
|
+
*/
|
|
113
|
+
function convertFunctionToObject(func) {
|
|
114
|
+
const funcString = func.toString();
|
|
115
|
+
|
|
116
|
+
// Arrow function
|
|
117
|
+
if (funcString.startsWith('(') || funcString.startsWith('_') || funcString.includes('=>')) {
|
|
118
|
+
return {
|
|
119
|
+
functionName: 'arrow',
|
|
120
|
+
body: funcString
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Bound chance methods (e.g., chance.name.bind(chance))
|
|
125
|
+
if (funcString.includes('.bind(')) {
|
|
126
|
+
const match = funcString.match(/chance\.(\w+)\.bind/);
|
|
127
|
+
if (match) {
|
|
128
|
+
return {
|
|
129
|
+
functionName: `chance.${match[1]}`,
|
|
130
|
+
args: []
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Try to detect common utility functions
|
|
136
|
+
// This is a best-effort approach - some complex functions might not be detected
|
|
137
|
+
const commonFunctions = [
|
|
138
|
+
'weighNumRange',
|
|
139
|
+
'weighArray',
|
|
140
|
+
'weighChoices',
|
|
141
|
+
'pickAWinner',
|
|
142
|
+
'date',
|
|
143
|
+
'integer',
|
|
144
|
+
'uid',
|
|
145
|
+
'comma'
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
for (const fnName of commonFunctions) {
|
|
149
|
+
if (funcString.includes(fnName)) {
|
|
150
|
+
// Extract args (this is simplified - real parsing would be more complex)
|
|
151
|
+
return {
|
|
152
|
+
functionName: fnName,
|
|
153
|
+
args: [] // Args would need to be extracted, but that's complex
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Generic function - just store as arrow function
|
|
159
|
+
return {
|
|
160
|
+
functionName: 'arrow',
|
|
161
|
+
body: funcString
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extracts an abbreviated schema from a JavaScript dungeon file.
|
|
5
|
+
*
|
|
6
|
+
* Output contains only the event/property relationships, superProps, userProps,
|
|
7
|
+
* and funnels — no hooks, no config flags, no functions.
|
|
8
|
+
*
|
|
9
|
+
* Property values are simplified:
|
|
10
|
+
* - Plain arrays → deduplicated
|
|
11
|
+
* - weighNumRange(min,max) arrays → { "$range": [min, max] }
|
|
12
|
+
* - Functions (pickAWinner, weighChoices, etc.) → sampled and deduplicated
|
|
13
|
+
* - High-cardinality / complex values → omitted
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* node scripts/extract-dungeon-schema.mjs <input.js> [output.json]
|
|
17
|
+
* node scripts/extract-dungeon-schema.mjs dungeons/vertical/*.js # batch mode
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { writeFileSync } from 'fs';
|
|
21
|
+
import path from 'path';
|
|
22
|
+
import { fileURLToPath } from 'url';
|
|
23
|
+
import { initChance } from '../lib/utils/utils.js';
|
|
24
|
+
|
|
25
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const args = process.argv.slice(2);
|
|
27
|
+
|
|
28
|
+
if (args.length === 0) {
|
|
29
|
+
console.error('Usage: node scripts/extract-dungeon-schema.mjs <dungeon.js> [output.json]');
|
|
30
|
+
console.error(' node scripts/extract-dungeon-schema.mjs dungeons/vertical/*.js');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Batch mode: multiple files
|
|
35
|
+
if (args.length > 2 || (args.length > 1 && args[1].endsWith('.js'))) {
|
|
36
|
+
for (const file of args) {
|
|
37
|
+
if (!file.endsWith('.js') && !file.endsWith('.mjs')) continue;
|
|
38
|
+
try {
|
|
39
|
+
await processOne(file);
|
|
40
|
+
} catch (e) {
|
|
41
|
+
console.error(` ❌ ${path.basename(file)}: ${e.message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
await processOne(args[0], args[1]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function processOne(inputFile, outputFile) {
|
|
49
|
+
const inputPath = path.resolve(inputFile);
|
|
50
|
+
const baseName = path.basename(inputPath, '.js');
|
|
51
|
+
const outputPath = outputFile
|
|
52
|
+
? path.resolve(outputFile)
|
|
53
|
+
: path.join(path.dirname(inputPath), `${baseName}-schema.json`);
|
|
54
|
+
|
|
55
|
+
// Seed RNG for deterministic sampling of function-based properties
|
|
56
|
+
initChance('schema-extraction');
|
|
57
|
+
|
|
58
|
+
const mod = await import(`file://${inputPath}`);
|
|
59
|
+
const config = mod.default;
|
|
60
|
+
if (!config) throw new Error('No default export');
|
|
61
|
+
|
|
62
|
+
const schema = {};
|
|
63
|
+
|
|
64
|
+
// Events
|
|
65
|
+
if (config.events?.length) {
|
|
66
|
+
schema.events = config.events.map(evt => {
|
|
67
|
+
const entry = { event: evt.event };
|
|
68
|
+
if (evt.weight && evt.weight !== 1) entry.weight = evt.weight;
|
|
69
|
+
if (evt.isFirstEvent) entry.isFirstEvent = true;
|
|
70
|
+
if (evt.properties && Object.keys(evt.properties).length) {
|
|
71
|
+
const props = extractProps(evt.properties);
|
|
72
|
+
if (Object.keys(props).length) entry.properties = props;
|
|
73
|
+
}
|
|
74
|
+
return entry;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Super props
|
|
79
|
+
if (config.superProps && Object.keys(config.superProps).length) {
|
|
80
|
+
const sp = extractProps(config.superProps);
|
|
81
|
+
if (Object.keys(sp).length) schema.superProps = sp;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// User props
|
|
85
|
+
if (config.userProps && Object.keys(config.userProps).length) {
|
|
86
|
+
const up = extractProps(config.userProps);
|
|
87
|
+
if (Object.keys(up).length) schema.userProps = up;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Funnels
|
|
91
|
+
if (config.funnels?.length) {
|
|
92
|
+
schema.funnels = config.funnels.map(f => {
|
|
93
|
+
const entry = { sequence: f.sequence };
|
|
94
|
+
if (f.isFirstFunnel) entry.isFirstFunnel = true;
|
|
95
|
+
if (f.conversionRate !== undefined) entry.conversionRate = f.conversionRate;
|
|
96
|
+
if (f.timeToConvert !== undefined) entry.timeToConvert = f.timeToConvert;
|
|
97
|
+
if (f.order && f.order !== 'sequential') entry.order = f.order;
|
|
98
|
+
if (f.weight && f.weight !== 1) entry.weight = f.weight;
|
|
99
|
+
if (f.requireRepeats) entry.requireRepeats = true;
|
|
100
|
+
if (f.experiment) entry.experiment = true;
|
|
101
|
+
if (f.props && Object.keys(f.props).length) {
|
|
102
|
+
const fp = extractProps(f.props);
|
|
103
|
+
if (Object.keys(fp).length) entry.props = fp;
|
|
104
|
+
}
|
|
105
|
+
return entry;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
writeFileSync(outputPath, JSON.stringify(schema, null, 2) + '\n', 'utf-8');
|
|
110
|
+
console.log(` ✅ ${baseName}-schema.json`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Property extraction ──────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Extracts a simplified property map from a dungeon properties object.
|
|
117
|
+
* Returns { key: simplifiedValue } for each property.
|
|
118
|
+
* Keys are always preserved — complex values get a type hint instead of being omitted.
|
|
119
|
+
*/
|
|
120
|
+
function extractProps(propsObj) {
|
|
121
|
+
const result = {};
|
|
122
|
+
for (const [key, value] of Object.entries(propsObj)) {
|
|
123
|
+
const simplified = simplifyValue(value);
|
|
124
|
+
if (simplified !== null) {
|
|
125
|
+
result[key] = simplified;
|
|
126
|
+
} else {
|
|
127
|
+
// Always keep the key — use a type hint for unresolvable values
|
|
128
|
+
result[key] = inferTypeHint(value);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Infer a type hint for a value we can't fully resolve.
|
|
136
|
+
* Tries calling the function first; falls back to string analysis.
|
|
137
|
+
*/
|
|
138
|
+
function inferTypeHint(value) {
|
|
139
|
+
if (typeof value === 'function') {
|
|
140
|
+
// Try calling to detect actual output type
|
|
141
|
+
try {
|
|
142
|
+
const result = value();
|
|
143
|
+
if (typeof result === 'function') {
|
|
144
|
+
try {
|
|
145
|
+
const inner = result();
|
|
146
|
+
if (Array.isArray(inner)) {
|
|
147
|
+
// Drill through nested arrays to find the first object
|
|
148
|
+
let sample = inner;
|
|
149
|
+
while (Array.isArray(sample) && sample.length > 0 && Array.isArray(sample[0])) {
|
|
150
|
+
sample = sample[0];
|
|
151
|
+
}
|
|
152
|
+
if (sample.length > 0 && typeof sample[0] === 'object' && sample[0] !== null && !Array.isArray(sample[0])) {
|
|
153
|
+
return { "$type": "object[]", "$keys": Object.keys(sample[0]) };
|
|
154
|
+
}
|
|
155
|
+
return { "$type": "array" };
|
|
156
|
+
}
|
|
157
|
+
return { "$type": typeof inner };
|
|
158
|
+
} catch { return { "$type": "function" }; }
|
|
159
|
+
}
|
|
160
|
+
if (typeof result === 'string') return { "$type": "string" };
|
|
161
|
+
if (typeof result === 'number') return { "$type": "number" };
|
|
162
|
+
if (typeof result === 'boolean') return { "$type": "boolean" };
|
|
163
|
+
if (Array.isArray(result)) return { "$type": "array" };
|
|
164
|
+
return { "$type": typeof result };
|
|
165
|
+
} catch { /* can't call — fall through to string analysis */ }
|
|
166
|
+
|
|
167
|
+
// String analysis fallback
|
|
168
|
+
const fnStr = value.toString();
|
|
169
|
+
if (fnStr.includes('guid') || fnStr.includes('uuid')) return { "$type": "string" };
|
|
170
|
+
if (fnStr.includes('integer')) return { "$type": "number" };
|
|
171
|
+
return { "$type": "unknown" };
|
|
172
|
+
}
|
|
173
|
+
if (Array.isArray(value)) {
|
|
174
|
+
if (value.length > 0 && typeof value[0] === 'object') return { "$type": "object[]" };
|
|
175
|
+
return { "$type": "array" };
|
|
176
|
+
}
|
|
177
|
+
return { "$type": typeof value };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Simplify a property value to its schema representation.
|
|
182
|
+
* Returns null if the value is too complex or high-cardinality to represent.
|
|
183
|
+
*/
|
|
184
|
+
function simplifyValue(value) {
|
|
185
|
+
// Primitives
|
|
186
|
+
if (typeof value === 'string' || typeof value === 'boolean') return [value];
|
|
187
|
+
if (typeof value === 'number') return [value];
|
|
188
|
+
|
|
189
|
+
// Arrays (plain values or weighNumRange output)
|
|
190
|
+
if (Array.isArray(value)) {
|
|
191
|
+
return simplifyArray(value);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Functions: pickAWinner, weighChoices, chance bindings, custom generators
|
|
195
|
+
if (typeof value === 'function') {
|
|
196
|
+
return simplifyFunction(value);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Simplify an array value. Detects numeric ranges vs categorical arrays.
|
|
204
|
+
*/
|
|
205
|
+
function simplifyArray(arr) {
|
|
206
|
+
if (arr.length === 0) return null;
|
|
207
|
+
|
|
208
|
+
// Check element types
|
|
209
|
+
const types = new Set(arr.map(v => typeof v));
|
|
210
|
+
|
|
211
|
+
// All numbers — likely weighNumRange output if large, or small explicit list
|
|
212
|
+
if (types.size === 1 && types.has('number')) {
|
|
213
|
+
if (arr.length > 10) {
|
|
214
|
+
// Numeric range (weighNumRange produces 50 elements)
|
|
215
|
+
const min = Math.min(...arr);
|
|
216
|
+
const max = Math.max(...arr);
|
|
217
|
+
// Round to clean integers if values are close to integers
|
|
218
|
+
const roundedMin = Number.isInteger(min) ? min : Math.round(min * 100) / 100;
|
|
219
|
+
const roundedMax = Number.isInteger(max) ? max : Math.round(max * 100) / 100;
|
|
220
|
+
return { "$range": [roundedMin, roundedMax] };
|
|
221
|
+
}
|
|
222
|
+
// Small explicit numeric list — deduplicate
|
|
223
|
+
return [...new Set(arr)];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// All strings — deduplicate
|
|
227
|
+
if (types.size === 1 && types.has('string')) {
|
|
228
|
+
return [...new Set(arr)];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// All booleans — deduplicate
|
|
232
|
+
if (types.size === 1 && types.has('boolean')) {
|
|
233
|
+
return [...new Set(arr)];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Mixed primitives (e.g. [true, false, 0.75, 1.0]) — deduplicate
|
|
237
|
+
if ([...types].every(t => t === 'string' || t === 'number' || t === 'boolean')) {
|
|
238
|
+
return [...new Set(arr)];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Contains objects/arrays (nested items like makeProducts)
|
|
242
|
+
if (arr.some(v => typeof v === 'object' && v !== null && !Array.isArray(v))) {
|
|
243
|
+
// Extract keys from first object to show the shape
|
|
244
|
+
const firstObj = arr.find(v => typeof v === 'object' && v !== null);
|
|
245
|
+
if (firstObj) {
|
|
246
|
+
const keys = Object.keys(firstObj);
|
|
247
|
+
return { "$type": "object[]", "$keys": keys };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Simplify a function-based value by sampling it.
|
|
255
|
+
* Tries calling first; string analysis only for bound methods.
|
|
256
|
+
*/
|
|
257
|
+
function simplifyFunction(fn) {
|
|
258
|
+
try {
|
|
259
|
+
// Try calling the function first
|
|
260
|
+
const result = fn();
|
|
261
|
+
|
|
262
|
+
// Double-wrapped function (e.g. makeProducts returns fn → fn → array)
|
|
263
|
+
if (typeof result === 'function') {
|
|
264
|
+
try {
|
|
265
|
+
const inner = result();
|
|
266
|
+
if (Array.isArray(inner)) {
|
|
267
|
+
if (inner.some(v => typeof v === 'object' && v !== null)) return null;
|
|
268
|
+
return simplifyArray(inner);
|
|
269
|
+
}
|
|
270
|
+
} catch { /* inner call failed */ }
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Function returned an array (pickAWinner, weighChoices)
|
|
275
|
+
if (Array.isArray(result)) {
|
|
276
|
+
return simplifyArray(result);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Function returned a primitive (chance.animal, chance.profession, etc.)
|
|
280
|
+
if (typeof result === 'string') {
|
|
281
|
+
// Sample a few times to see if it's low-cardinality
|
|
282
|
+
const samples = new Set([result]);
|
|
283
|
+
for (let i = 0; i < 30; i++) {
|
|
284
|
+
samples.add(fn());
|
|
285
|
+
}
|
|
286
|
+
// If < 15 unique values in 30 samples, it's low cardinality — include
|
|
287
|
+
if (samples.size < 15) {
|
|
288
|
+
return [...samples].sort();
|
|
289
|
+
}
|
|
290
|
+
return null; // high cardinality
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return null;
|
|
294
|
+
} catch {
|
|
295
|
+
return null; // function can't be called standalone — skip
|
|
296
|
+
}
|
|
297
|
+
}
|