make-mp-data 2.1.6 → 3.0.1
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 +31 -0
- package/dungeons/adspend.js +2 -2
- package/dungeons/ai-chat-analytics-ed.js +3 -2
- package/dungeons/anon.js +2 -2
- package/dungeons/array-of-object-loopup.js +181 -0
- package/dungeons/benchmark-heavy.js +241 -0
- package/dungeons/benchmark-light.js +141 -0
- package/dungeons/big.js +9 -8
- package/dungeons/business.js +2 -1
- package/dungeons/clinch-agi.js +632 -0
- package/dungeons/complex.js +3 -2
- package/dungeons/copilot.js +383 -0
- package/dungeons/ecommerce-store.js +0 -0
- package/dungeons/experiments.js +5 -4
- package/dungeons/foobar.js +101 -101
- package/dungeons/funnels.js +2 -2
- package/dungeons/gaming.js +3 -2
- package/dungeons/harness/harness-education.js +988 -0
- package/dungeons/harness/harness-fintech.js +976 -0
- package/dungeons/harness/harness-food.js +985 -0
- package/dungeons/harness/harness-gaming.js +1178 -0
- package/dungeons/harness/harness-media.js +961 -0
- package/dungeons/harness/harness-sass.js +923 -0
- package/dungeons/harness/harness-social.js +928 -0
- package/dungeons/kurby.js +211 -0
- package/dungeons/media.js +5 -4
- package/dungeons/mil.js +4 -3
- package/dungeons/mirror.js +2 -2
- package/dungeons/money2020-ed.js +8 -7
- package/dungeons/sanity.js +3 -2
- package/dungeons/scd.js +3 -2
- package/dungeons/simple.js +29 -14
- package/dungeons/strict-event-test.js +30 -0
- package/dungeons/student-teacher.js +3 -2
- package/dungeons/text-generation.js +84 -85
- package/dungeons/too-big-events.js +166 -0
- package/dungeons/uday-schema.json +220 -0
- package/dungeons/userAgent.js +4 -3
- package/index.js +41 -54
- package/lib/core/config-validator.js +122 -7
- package/lib/core/context.js +7 -14
- package/lib/core/storage.js +60 -30
- package/lib/generators/adspend.js +12 -27
- package/lib/generators/events.js +6 -7
- package/lib/generators/funnels.js +16 -5
- package/lib/generators/product-lookup.js +262 -0
- package/lib/generators/product-names.js +195 -0
- package/lib/generators/profiles.js +3 -3
- package/lib/generators/scd.js +13 -3
- package/lib/generators/text.js +17 -4
- package/lib/orchestrators/mixpanel-sender.js +251 -208
- package/lib/orchestrators/user-loop.js +57 -19
- package/lib/templates/funnels-instructions.txt +272 -0
- package/lib/templates/hook-examples.json +187 -0
- package/lib/templates/hooks-instructions.txt +295 -8
- package/lib/templates/phrases.js +473 -16
- package/lib/templates/refine-instructions.txt +485 -0
- package/lib/templates/schema-instructions.txt +239 -109
- package/lib/templates/schema.d.ts +173 -0
- package/lib/templates/verbose-schema.js +140 -206
- package/lib/utils/ai.js +853 -77
- package/lib/utils/chart.js +210 -0
- package/lib/utils/function-registry.js +285 -0
- package/lib/utils/json-evaluator.js +172 -0
- package/lib/utils/logger.js +38 -0
- package/lib/utils/mixpanel.js +101 -0
- package/lib/utils/project.js +3 -2
- package/lib/utils/utils.js +41 -4
- package/package.json +13 -19
- package/types.d.ts +15 -5
- package/lib/generators/text-bak-old.js +0 -1121
- package/lib/orchestrators/worker-manager.js +0 -203
- package/lib/templates/phrases-bak.js +0 -925
- package/lib/templates/prompt (old).txt +0 -98
- package/lib/templates/scratch-dungeon-template.js +0 -116
- package/lib/templates/textQuickTest.js +0 -172
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/** @typedef {import('../../types.js').EventSchema} EventSchema */
|
|
2
|
+
/** @typedef {import('../../types.js').Result} Result */
|
|
3
|
+
/** @typedef {import('../../types.js').Context} Context */
|
|
4
|
+
|
|
5
|
+
// import { ChartJSNodeCanvas } from 'chartjs-node-canvas';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import * as u from 'ak-tools';
|
|
8
|
+
import dayjs from 'dayjs';
|
|
9
|
+
import { openFinder } from './utils.js';
|
|
10
|
+
import { dataLogger as logger } from './logger.js';
|
|
11
|
+
const { existsSync } = fs;
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import 'dotenv/config';
|
|
14
|
+
const { NODE_ENV = "unknown" } = process.env;
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
let tempDir;
|
|
18
|
+
const dataFolder = path.resolve("./data");
|
|
19
|
+
if (existsSync(dataFolder)) tempDir = dataFolder;
|
|
20
|
+
else tempDir = path.resolve("./");
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
// Function to count events per day
|
|
24
|
+
function countDailyEvents(eventData) {
|
|
25
|
+
const dailyCounts = {};
|
|
26
|
+
|
|
27
|
+
eventData.forEach(event => {
|
|
28
|
+
const date = dayjs(event.time).format('YYYY-MM-DD');
|
|
29
|
+
if (!dailyCounts[date]) {
|
|
30
|
+
dailyCounts[date] = 0;
|
|
31
|
+
}
|
|
32
|
+
dailyCounts[date]++;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return dailyCounts;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Function to count daily users
|
|
39
|
+
function countDailyUsers(eventData) {
|
|
40
|
+
const dailyUsers = {};
|
|
41
|
+
|
|
42
|
+
eventData.forEach(event => {
|
|
43
|
+
const date = dayjs(event.time).format('YYYY-MM-DD');
|
|
44
|
+
if (!dailyUsers[date]) {
|
|
45
|
+
dailyUsers[date] = new Set();
|
|
46
|
+
}
|
|
47
|
+
dailyUsers[date].add(event.user_id);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return dailyUsers;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Function to count daily new users based on signup events
|
|
54
|
+
function countDailyNewUsers(eventData, signupEvents) {
|
|
55
|
+
const dailyNewUsers = {};
|
|
56
|
+
const seenUsers = new Set();
|
|
57
|
+
|
|
58
|
+
eventData.forEach(event => {
|
|
59
|
+
const date = dayjs(event.time).format('YYYY-MM-DD');
|
|
60
|
+
if (!dailyNewUsers[date]) {
|
|
61
|
+
dailyNewUsers[date] = new Set();
|
|
62
|
+
}
|
|
63
|
+
if (signupEvents.includes(event.event) && !seenUsers.has(event.user_id)) {
|
|
64
|
+
dailyNewUsers[date].add(event.user_id);
|
|
65
|
+
seenUsers.add(event.user_id);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return dailyNewUsers;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Function to generate line chart
|
|
73
|
+
async function generateLineChart(rawData, signupEvents = ["sign up"], fileName) {
|
|
74
|
+
// COMMENTED OUT: Canvas dependency removed
|
|
75
|
+
throw new Error("Chart generation is temporarily disabled - canvas dependency removed");
|
|
76
|
+
|
|
77
|
+
// const width = 1600;
|
|
78
|
+
// const height = 1200;
|
|
79
|
+
// const chartJSNodeCanvas = new ChartJSNodeCanvas({ width, height, backgroundColour: 'black' });
|
|
80
|
+
const eventData = countDailyEvents(rawData);
|
|
81
|
+
const userData = countDailyUsers(rawData);
|
|
82
|
+
const newUserData = countDailyNewUsers(rawData, signupEvents);
|
|
83
|
+
|
|
84
|
+
// @ts-ignore
|
|
85
|
+
const sortedEventEntries = Object.entries(eventData).sort((a, b) => new Date(a[0]) - new Date(b[0]));
|
|
86
|
+
const eventLabels = sortedEventEntries.map(entry => entry[0]);
|
|
87
|
+
const eventValues = sortedEventEntries.map(entry => entry[1]);
|
|
88
|
+
|
|
89
|
+
// @ts-ignore
|
|
90
|
+
const sortedUserEntries = Object.entries(userData).sort((a, b) => new Date(a[0]) - new Date(b[0]));
|
|
91
|
+
const userLabels = sortedUserEntries.map(entry => entry[0]);
|
|
92
|
+
const userValues = sortedUserEntries.map(entry => entry[1].size);
|
|
93
|
+
|
|
94
|
+
// @ts-ignore
|
|
95
|
+
const sortedNewUserEntries = Object.entries(newUserData).sort((a, b) => new Date(a[0]) - new Date(b[0]));
|
|
96
|
+
const newUserLabels = sortedNewUserEntries.map(entry => entry[0]);
|
|
97
|
+
const newUserValues = sortedNewUserEntries.map(entry => entry[1].size);
|
|
98
|
+
|
|
99
|
+
const configuration = {
|
|
100
|
+
type: 'line',
|
|
101
|
+
data: {
|
|
102
|
+
labels: eventLabels,
|
|
103
|
+
datasets: [
|
|
104
|
+
{
|
|
105
|
+
label: '# EVENTS',
|
|
106
|
+
data: eventValues,
|
|
107
|
+
yAxisID: 'y1',
|
|
108
|
+
fill: true,
|
|
109
|
+
borderColor: '#4F44E0',
|
|
110
|
+
tension: 0.1
|
|
111
|
+
},
|
|
112
|
+
// {
|
|
113
|
+
// label: '# USERS',
|
|
114
|
+
// data: userValues,
|
|
115
|
+
// yAxisID: 'y2',
|
|
116
|
+
// fill: true,
|
|
117
|
+
// borderColor: '#E34F2F',
|
|
118
|
+
// tension: 0.1
|
|
119
|
+
// },
|
|
120
|
+
{
|
|
121
|
+
label: '# NEW',
|
|
122
|
+
data: newUserValues,
|
|
123
|
+
yAxisID: 'y3',
|
|
124
|
+
fill: true,
|
|
125
|
+
borderColor: '#219464',
|
|
126
|
+
tension: 0.1
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
},
|
|
130
|
+
options: {
|
|
131
|
+
scales: {
|
|
132
|
+
x: {
|
|
133
|
+
title: {
|
|
134
|
+
display: true,
|
|
135
|
+
text: 'Date',
|
|
136
|
+
color: 'white'
|
|
137
|
+
},
|
|
138
|
+
ticks: {
|
|
139
|
+
color: 'white'
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
y1: {
|
|
143
|
+
type: 'linear',
|
|
144
|
+
display: true,
|
|
145
|
+
position: 'left',
|
|
146
|
+
title: {
|
|
147
|
+
display: true,
|
|
148
|
+
text: 'Count of Events',
|
|
149
|
+
color: '#4F44E0'
|
|
150
|
+
},
|
|
151
|
+
ticks: {
|
|
152
|
+
color: '#4F44E0'
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
y3: {
|
|
156
|
+
type: 'linear',
|
|
157
|
+
display: true,
|
|
158
|
+
position: 'right',
|
|
159
|
+
offset: true,
|
|
160
|
+
title: {
|
|
161
|
+
display: true,
|
|
162
|
+
text: 'Count of New Users',
|
|
163
|
+
color: '#219464'
|
|
164
|
+
},
|
|
165
|
+
ticks: {
|
|
166
|
+
color: '#219464'
|
|
167
|
+
},
|
|
168
|
+
grid: {
|
|
169
|
+
drawOnChartArea: false
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
plugins: {
|
|
174
|
+
legend: {
|
|
175
|
+
labels: {
|
|
176
|
+
color: 'white'
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// @ts-ignore
|
|
184
|
+
if (typeof fileName === undefined) fileName = 'chart';
|
|
185
|
+
if (typeof fileName !== 'string') fileName = 'chart';
|
|
186
|
+
// @ts-ignore
|
|
187
|
+
// const imageBuffer = await chartJSNodeCanvas.renderToBuffer(configuration);
|
|
188
|
+
// const filePath = path.join(tempDir, `${fileName}.png`);
|
|
189
|
+
// const removed = await u.rm(filePath);
|
|
190
|
+
// @ts-ignore - imageBuffer is a Buffer but touch accepts it
|
|
191
|
+
// const file = await u.touch(filePath, imageBuffer);
|
|
192
|
+
|
|
193
|
+
// logger.info({ filename: `${fileName}.png` }, `📊 Chart saved as ${fileName}.png`);
|
|
194
|
+
// openFinder(path)
|
|
195
|
+
// return file;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export { generateLineChart };
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
203
|
+
generateLineChart()
|
|
204
|
+
.then((result)=>{
|
|
205
|
+
if (NODE_ENV === "dev") debugger;
|
|
206
|
+
})
|
|
207
|
+
.catch((error)=>{
|
|
208
|
+
if (NODE_ENV === "dev") debugger;
|
|
209
|
+
})
|
|
210
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry of valid functions that can be called in dungeon configurations
|
|
3
|
+
* This replaces the old string-based function parsing with a clean JSON structure
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { dataLogger as logger } from './logger.js';
|
|
7
|
+
|
|
8
|
+
export const FUNCTION_REGISTRY = {
|
|
9
|
+
// Utility functions from utils.js
|
|
10
|
+
weighNumRange: {
|
|
11
|
+
minArgs: 2,
|
|
12
|
+
maxArgs: 3,
|
|
13
|
+
description: 'Generate weighted random number in range'
|
|
14
|
+
},
|
|
15
|
+
range: {
|
|
16
|
+
minArgs: 2,
|
|
17
|
+
maxArgs: 2,
|
|
18
|
+
description: 'Generate array of numbers in range'
|
|
19
|
+
},
|
|
20
|
+
date: {
|
|
21
|
+
minArgs: 1,
|
|
22
|
+
maxArgs: 3,
|
|
23
|
+
description: 'Generate date string'
|
|
24
|
+
},
|
|
25
|
+
choose: {
|
|
26
|
+
minArgs: 1,
|
|
27
|
+
maxArgs: 1,
|
|
28
|
+
description: 'Choose from array'
|
|
29
|
+
},
|
|
30
|
+
integer: {
|
|
31
|
+
minArgs: 1,
|
|
32
|
+
maxArgs: 2,
|
|
33
|
+
description: 'Generate random integer'
|
|
34
|
+
},
|
|
35
|
+
exhaust: {
|
|
36
|
+
minArgs: 1,
|
|
37
|
+
maxArgs: 1,
|
|
38
|
+
description: 'Exhaust values from array'
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// Additional utility functions
|
|
42
|
+
maybe: {
|
|
43
|
+
minArgs: 1,
|
|
44
|
+
maxArgs: 2,
|
|
45
|
+
description: 'Return value or null based on probability'
|
|
46
|
+
},
|
|
47
|
+
takeSome: {
|
|
48
|
+
minArgs: 1,
|
|
49
|
+
maxArgs: 3,
|
|
50
|
+
description: 'Take random subset from array'
|
|
51
|
+
},
|
|
52
|
+
randomElement: {
|
|
53
|
+
minArgs: 1,
|
|
54
|
+
maxArgs: 1,
|
|
55
|
+
description: 'Return random element from array'
|
|
56
|
+
},
|
|
57
|
+
randomInt: {
|
|
58
|
+
minArgs: 2,
|
|
59
|
+
maxArgs: 2,
|
|
60
|
+
description: 'Generate random integer'
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// Text generation functions
|
|
64
|
+
createTextGenerator: {
|
|
65
|
+
minArgs: 1,
|
|
66
|
+
maxArgs: 1,
|
|
67
|
+
description: 'Create text generator with config'
|
|
68
|
+
},
|
|
69
|
+
generateBatch: {
|
|
70
|
+
minArgs: 2,
|
|
71
|
+
maxArgs: 2,
|
|
72
|
+
description: 'Generate batch of text'
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
// Chance.js functions (using dot notation)
|
|
76
|
+
'chance.word': {
|
|
77
|
+
minArgs: 0,
|
|
78
|
+
maxArgs: 1,
|
|
79
|
+
description: 'Generate random word'
|
|
80
|
+
},
|
|
81
|
+
'chance.sentence': {
|
|
82
|
+
minArgs: 0,
|
|
83
|
+
maxArgs: 1,
|
|
84
|
+
description: 'Generate random sentence'
|
|
85
|
+
},
|
|
86
|
+
'chance.paragraph': {
|
|
87
|
+
minArgs: 0,
|
|
88
|
+
maxArgs: 1,
|
|
89
|
+
description: 'Generate random paragraph'
|
|
90
|
+
},
|
|
91
|
+
'chance.name': {
|
|
92
|
+
minArgs: 0,
|
|
93
|
+
maxArgs: 1,
|
|
94
|
+
description: 'Generate random name'
|
|
95
|
+
},
|
|
96
|
+
'chance.first': {
|
|
97
|
+
minArgs: 0,
|
|
98
|
+
maxArgs: 1,
|
|
99
|
+
description: 'Generate random first name'
|
|
100
|
+
},
|
|
101
|
+
'chance.last': {
|
|
102
|
+
minArgs: 0,
|
|
103
|
+
maxArgs: 1,
|
|
104
|
+
description: 'Generate random last name'
|
|
105
|
+
},
|
|
106
|
+
'chance.email': {
|
|
107
|
+
minArgs: 0,
|
|
108
|
+
maxArgs: 1,
|
|
109
|
+
description: 'Generate random email'
|
|
110
|
+
},
|
|
111
|
+
'chance.company': {
|
|
112
|
+
minArgs: 0,
|
|
113
|
+
maxArgs: 0,
|
|
114
|
+
description: 'Generate random company name'
|
|
115
|
+
},
|
|
116
|
+
'chance.profession': {
|
|
117
|
+
minArgs: 0,
|
|
118
|
+
maxArgs: 0,
|
|
119
|
+
description: 'Generate random profession'
|
|
120
|
+
},
|
|
121
|
+
'chance.industry': {
|
|
122
|
+
minArgs: 0,
|
|
123
|
+
maxArgs: 0,
|
|
124
|
+
description: 'Generate random industry'
|
|
125
|
+
},
|
|
126
|
+
'chance.country': {
|
|
127
|
+
minArgs: 0,
|
|
128
|
+
maxArgs: 1,
|
|
129
|
+
description: 'Generate random country'
|
|
130
|
+
},
|
|
131
|
+
'chance.city': {
|
|
132
|
+
minArgs: 0,
|
|
133
|
+
maxArgs: 0,
|
|
134
|
+
description: 'Generate random city'
|
|
135
|
+
},
|
|
136
|
+
'chance.state': {
|
|
137
|
+
minArgs: 0,
|
|
138
|
+
maxArgs: 1,
|
|
139
|
+
description: 'Generate random state'
|
|
140
|
+
},
|
|
141
|
+
'chance.address': {
|
|
142
|
+
minArgs: 0,
|
|
143
|
+
maxArgs: 1,
|
|
144
|
+
description: 'Generate random address'
|
|
145
|
+
},
|
|
146
|
+
'chance.phone': {
|
|
147
|
+
minArgs: 0,
|
|
148
|
+
maxArgs: 1,
|
|
149
|
+
description: 'Generate random phone number'
|
|
150
|
+
},
|
|
151
|
+
'chance.url': {
|
|
152
|
+
minArgs: 0,
|
|
153
|
+
maxArgs: 1,
|
|
154
|
+
description: 'Generate random URL'
|
|
155
|
+
},
|
|
156
|
+
'chance.domain': {
|
|
157
|
+
minArgs: 0,
|
|
158
|
+
maxArgs: 1,
|
|
159
|
+
description: 'Generate random domain'
|
|
160
|
+
},
|
|
161
|
+
'chance.ip': {
|
|
162
|
+
minArgs: 0,
|
|
163
|
+
maxArgs: 0,
|
|
164
|
+
description: 'Generate random IP address'
|
|
165
|
+
},
|
|
166
|
+
'chance.guid': {
|
|
167
|
+
minArgs: 0,
|
|
168
|
+
maxArgs: 1,
|
|
169
|
+
description: 'Generate random GUID'
|
|
170
|
+
},
|
|
171
|
+
'chance.hash': {
|
|
172
|
+
minArgs: 0,
|
|
173
|
+
maxArgs: 1,
|
|
174
|
+
description: 'Generate random hash'
|
|
175
|
+
},
|
|
176
|
+
'chance.integer': {
|
|
177
|
+
minArgs: 0,
|
|
178
|
+
maxArgs: 1,
|
|
179
|
+
description: 'Generate random integer with options'
|
|
180
|
+
},
|
|
181
|
+
'chance.floating': {
|
|
182
|
+
minArgs: 0,
|
|
183
|
+
maxArgs: 1,
|
|
184
|
+
description: 'Generate random float'
|
|
185
|
+
},
|
|
186
|
+
'chance.bool': {
|
|
187
|
+
minArgs: 0,
|
|
188
|
+
maxArgs: 1,
|
|
189
|
+
description: 'Generate random boolean'
|
|
190
|
+
},
|
|
191
|
+
'chance.character': {
|
|
192
|
+
minArgs: 0,
|
|
193
|
+
maxArgs: 1,
|
|
194
|
+
description: 'Generate random character'
|
|
195
|
+
},
|
|
196
|
+
'chance.string': {
|
|
197
|
+
minArgs: 0,
|
|
198
|
+
maxArgs: 1,
|
|
199
|
+
description: 'Generate random string'
|
|
200
|
+
},
|
|
201
|
+
'chance.pick': {
|
|
202
|
+
minArgs: 1,
|
|
203
|
+
maxArgs: 2,
|
|
204
|
+
description: 'Pick random element from array'
|
|
205
|
+
},
|
|
206
|
+
'chance.pickone': {
|
|
207
|
+
minArgs: 1,
|
|
208
|
+
maxArgs: 1,
|
|
209
|
+
description: 'Pick one random element from array'
|
|
210
|
+
},
|
|
211
|
+
'chance.pickset': {
|
|
212
|
+
minArgs: 2,
|
|
213
|
+
maxArgs: 2,
|
|
214
|
+
description: 'Pick set of random elements from array'
|
|
215
|
+
},
|
|
216
|
+
'chance.cc': {
|
|
217
|
+
minArgs: 0,
|
|
218
|
+
maxArgs: 1,
|
|
219
|
+
description: 'Generate credit card number'
|
|
220
|
+
},
|
|
221
|
+
'chance.android_id': {
|
|
222
|
+
minArgs: 0,
|
|
223
|
+
maxArgs: 0,
|
|
224
|
+
description: 'Generate Android device ID'
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
// Special function for arrow functions
|
|
228
|
+
arrow: {
|
|
229
|
+
minArgs: 1,
|
|
230
|
+
maxArgs: 1,
|
|
231
|
+
description: 'Raw arrow function with body',
|
|
232
|
+
special: true
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Validate a function call structure
|
|
238
|
+
* @param {Object} funcCall - The function call object with functionName and args
|
|
239
|
+
* @returns {boolean} - Whether the function call is valid
|
|
240
|
+
*/
|
|
241
|
+
export function validateFunctionCall(funcCall) {
|
|
242
|
+
if (!funcCall || typeof funcCall !== 'object') {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const { functionName, args, body } = funcCall;
|
|
247
|
+
|
|
248
|
+
if (!functionName || typeof functionName !== 'string') {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Special handling for arrow functions
|
|
253
|
+
if (functionName === 'arrow') {
|
|
254
|
+
return typeof body === 'string' && body.length > 0;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const funcDef = FUNCTION_REGISTRY[functionName];
|
|
258
|
+
if (!funcDef) {
|
|
259
|
+
logger.warn({ functionName }, `Unknown function: ${functionName}`);
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Check args
|
|
264
|
+
if (!Array.isArray(args)) {
|
|
265
|
+
if (funcDef.minArgs === 0 && !args) {
|
|
266
|
+
return true; // No args required and none provided
|
|
267
|
+
}
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (args.length < funcDef.minArgs || args.length > funcDef.maxArgs) {
|
|
272
|
+
logger.warn({ functionName, expected: `${funcDef.minArgs}-${funcDef.maxArgs}`, actual: args.length }, `Function ${functionName} expects ${funcDef.minArgs}-${funcDef.maxArgs} args, got ${args.length}`);
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Get list of all valid function names
|
|
281
|
+
* @returns {string[]} Array of function names
|
|
282
|
+
*/
|
|
283
|
+
export function getValidFunctionNames() {
|
|
284
|
+
return Object.keys(FUNCTION_REGISTRY);
|
|
285
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Evaluator for converting JSON function call format to JavaScript
|
|
3
|
+
* Replaces the old regex-based string parsing approach
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { validateFunctionCall } from './function-registry.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Evaluate a value that might contain function calls
|
|
10
|
+
* @param {any} value - The value to evaluate (could be object with functionName, array, or primitive)
|
|
11
|
+
* @returns {string} - JavaScript code string
|
|
12
|
+
*/
|
|
13
|
+
export function evaluateValue(value) {
|
|
14
|
+
// Handle function call objects
|
|
15
|
+
if (typeof value === 'object' && value !== null && 'functionName' in value) {
|
|
16
|
+
return evaluateFunctionCall(value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Handle arrays (return as-is)
|
|
20
|
+
if (Array.isArray(value)) {
|
|
21
|
+
return JSON.stringify(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Handle nested objects (recursively evaluate)
|
|
25
|
+
if (typeof value === 'object' && value !== null) {
|
|
26
|
+
const entries = Object.entries(value).map(([key, val]) => {
|
|
27
|
+
const evaluatedVal = evaluateValue(val);
|
|
28
|
+
// Use quotes for keys that need them
|
|
29
|
+
const quotedKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : `"${key}"`;
|
|
30
|
+
return `${quotedKey}: ${evaluatedVal}`;
|
|
31
|
+
});
|
|
32
|
+
return `{${entries.join(', ')}}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Handle primitives
|
|
36
|
+
return JSON.stringify(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Evaluate a function call object to JavaScript code
|
|
41
|
+
* @param {Object} funcCall - Object with functionName and args/body
|
|
42
|
+
* @returns {string} - JavaScript function call string
|
|
43
|
+
*/
|
|
44
|
+
export function evaluateFunctionCall(funcCall) {
|
|
45
|
+
if (!validateFunctionCall(funcCall)) {
|
|
46
|
+
throw new Error(`Invalid function call: ${JSON.stringify(funcCall)}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const { functionName, args, body } = funcCall;
|
|
50
|
+
|
|
51
|
+
// Special handling for arrow functions
|
|
52
|
+
if (functionName === 'arrow') {
|
|
53
|
+
return `() => ${body}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Handle chance.* functions
|
|
57
|
+
if (functionName.startsWith('chance.')) {
|
|
58
|
+
const method = functionName.split('.')[1];
|
|
59
|
+
if (!args || args.length === 0) {
|
|
60
|
+
return `chance.${method}.bind(chance)`;
|
|
61
|
+
} else if (args.length === 1 && typeof args[0] === 'object') {
|
|
62
|
+
// For methods that take an options object
|
|
63
|
+
return `() => chance.${method}(${JSON.stringify(args[0])})`;
|
|
64
|
+
} else {
|
|
65
|
+
// For methods with regular arguments
|
|
66
|
+
const argsStr = args.map(arg => evaluateValue(arg)).join(', ');
|
|
67
|
+
return `() => chance.${method}(${argsStr})`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Handle regular functions
|
|
72
|
+
if (!args || args.length === 0) {
|
|
73
|
+
return `${functionName}()`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Evaluate each argument
|
|
77
|
+
const evaluatedArgs = args.map(arg => {
|
|
78
|
+
// If arg is an object with functionName, evaluate it as a function call
|
|
79
|
+
if (typeof arg === 'object' && arg !== null && 'functionName' in arg) {
|
|
80
|
+
return evaluateFunctionCall(arg);
|
|
81
|
+
}
|
|
82
|
+
// For primitive values (strings, numbers, booleans), use JSON.stringify
|
|
83
|
+
// but strings need special handling to avoid double-escaping
|
|
84
|
+
if (typeof arg === 'string') {
|
|
85
|
+
return `"${arg}"`;
|
|
86
|
+
}
|
|
87
|
+
// For other primitives and arrays, use JSON.stringify
|
|
88
|
+
return JSON.stringify(arg);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return `${functionName}(${evaluatedArgs.join(', ')})`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Convert a complete dungeon config from JSON to JavaScript
|
|
96
|
+
* @param {Object} config - The dungeon configuration object
|
|
97
|
+
* @returns {Object} - Configuration with function strings
|
|
98
|
+
*/
|
|
99
|
+
export function convertDungeonConfig(config) {
|
|
100
|
+
const converted = {};
|
|
101
|
+
|
|
102
|
+
for (const [key, value] of Object.entries(config)) {
|
|
103
|
+
// Skip certain keys that shouldn't be processed
|
|
104
|
+
if (key === 'seed' || key === 'name' || typeof value === 'number' || typeof value === 'boolean' || typeof value === 'string') {
|
|
105
|
+
converted[key] = value;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Special handling for groupKeys - preserve as array of arrays
|
|
110
|
+
if (key === 'groupKeys' && Array.isArray(value)) {
|
|
111
|
+
converted[key] = value;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Process arrays of objects (like events)
|
|
116
|
+
if (Array.isArray(value)) {
|
|
117
|
+
converted[key] = value.map(item => {
|
|
118
|
+
if (typeof item === 'object' && item !== null) {
|
|
119
|
+
return processConfigObject(item);
|
|
120
|
+
}
|
|
121
|
+
return item;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
// Process objects
|
|
125
|
+
else if (typeof value === 'object' && value !== null) {
|
|
126
|
+
converted[key] = processConfigObject(value);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
converted[key] = value;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return converted;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Process a configuration object, converting function calls
|
|
138
|
+
* @param {Object} obj - Object to process
|
|
139
|
+
* @returns {Object} - Processed object
|
|
140
|
+
*/
|
|
141
|
+
function processConfigObject(obj) {
|
|
142
|
+
const processed = {};
|
|
143
|
+
|
|
144
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
145
|
+
// Check if this is a function call
|
|
146
|
+
if (typeof value === 'object' && value !== null && 'functionName' in value) {
|
|
147
|
+
// Store as string for the JS file generation
|
|
148
|
+
processed[key] = evaluateFunctionCall(value);
|
|
149
|
+
}
|
|
150
|
+
// Process nested arrays
|
|
151
|
+
else if (Array.isArray(value)) {
|
|
152
|
+
processed[key] = value.map(item => {
|
|
153
|
+
if (typeof item === 'object' && item !== null && 'functionName' in item) {
|
|
154
|
+
return evaluateFunctionCall(item);
|
|
155
|
+
}
|
|
156
|
+
if (typeof item === 'object' && item !== null) {
|
|
157
|
+
return processConfigObject(item);
|
|
158
|
+
}
|
|
159
|
+
return item;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
// Process nested objects
|
|
163
|
+
else if (typeof value === 'object' && value !== null) {
|
|
164
|
+
processed[key] = processConfigObject(value);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
processed[key] = value;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return processed;
|
|
172
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured logger using Pino
|
|
3
|
+
*
|
|
4
|
+
* Pretty-printed in development, JSON in production for Cloud Run
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import pino from 'pino';
|
|
8
|
+
|
|
9
|
+
const isDev = process.env.NODE_ENV !== 'production';
|
|
10
|
+
const isTest = process.env.NODE_ENV === 'test';
|
|
11
|
+
|
|
12
|
+
// Create the logger with appropriate transport
|
|
13
|
+
// In production, use 'message' instead of 'msg' for GCP Cloud Logging compatibility
|
|
14
|
+
const logger = pino({
|
|
15
|
+
level: process.env.LOG_LEVEL || (isTest ? 'silent' : isDev ? 'warn' : 'info'),
|
|
16
|
+
// Use 'message' instead of 'msg' for GCP Cloud Logging
|
|
17
|
+
messageKey: isDev ? 'msg' : 'message',
|
|
18
|
+
transport: isDev
|
|
19
|
+
? {
|
|
20
|
+
target: 'pino-pretty',
|
|
21
|
+
options: {
|
|
22
|
+
colorize: true,
|
|
23
|
+
translateTime: 'HH:MM:ss.l',
|
|
24
|
+
ignore: 'pid,hostname',
|
|
25
|
+
messageFormat: '{msg}',
|
|
26
|
+
errorLikeObjectKeys: ['err', 'error']
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
: undefined // JSON output in production for Cloud Run
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Add child loggers for different components
|
|
33
|
+
export const serverLogger = logger.child({ component: 'server' });
|
|
34
|
+
export const aiLogger = logger.child({ component: 'ai' });
|
|
35
|
+
export const dataLogger = logger.child({ component: 'data' });
|
|
36
|
+
export const importLogger = logger.child({ component: 'import' });
|
|
37
|
+
|
|
38
|
+
export default logger;
|