bulltrackers-module 1.0.171 → 1.0.172
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/functions/computation-system/controllers/computation_controller.js +215 -0
- package/functions/computation-system/helpers/orchestration_helpers.js +189 -718
- package/functions/computation-system/layers/math_primitives.js +346 -0
- package/functions/task-engine/helpers/update_helpers.js +1 -1
- package/package.json +1 -1
|
@@ -1,800 +1,271 @@
|
|
|
1
|
-
const { FieldPath } = require('@google-cloud/firestore');
|
|
2
|
-
const {
|
|
3
|
-
getPortfolioPartRefs,
|
|
4
|
-
loadFullDayMap,
|
|
5
|
-
loadDataByRefs,
|
|
6
|
-
loadDailyInsights,
|
|
7
|
-
loadDailySocialPostInsights,
|
|
8
|
-
getHistoryPartRefs,
|
|
9
|
-
streamPortfolioData,
|
|
10
|
-
streamHistoryData
|
|
11
|
-
} = require('../utils/data_loader.js');
|
|
12
|
-
const { normalizeName, commitBatchInChunks } = require('../utils/utils.js');
|
|
13
|
-
const { batchStoreSchemas } = require('../utils/schema_capture.js');
|
|
14
|
-
|
|
15
1
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* @returns {object} An object with pass numbers as keys and calc arrays as values.
|
|
2
|
+
* @fileoverview Orchestration Helpers (V2 Refactor)
|
|
3
|
+
* Delegates all execution logic to the ComputationController.
|
|
19
4
|
*/
|
|
20
|
-
function groupByPass(manifest) { return manifest.reduce((acc, calc) => { (acc[calc.pass] = acc[calc.pass] || []).push(calc); return acc; }, {}); }
|
|
21
5
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
6
|
+
const { ComputationController } = require('../controllers/computation_controller');
|
|
7
|
+
const {
|
|
8
|
+
getPortfolioPartRefs, loadDailyInsights, loadDailySocialPostInsights,
|
|
9
|
+
getHistoryPartRefs, streamPortfolioData, streamHistoryData
|
|
10
|
+
} = require('../utils/data_loader');
|
|
11
|
+
const { batchStoreSchemas } = require('../utils/schema_capture');
|
|
12
|
+
const { normalizeName, commitBatchInChunks } = require('../utils/utils');
|
|
13
|
+
|
|
14
|
+
// --- Helpers (Unchanged) ---
|
|
15
|
+
|
|
16
|
+
function groupByPass(manifest) {
|
|
17
|
+
return manifest.reduce((acc, calc) => { (acc[calc.pass] = acc[calc.pass] || []).push(calc); return acc; }, {});
|
|
18
|
+
}
|
|
19
|
+
|
|
28
20
|
function checkRootDependencies(calcManifest, rootDataStatus) {
|
|
29
21
|
const missing = [];
|
|
30
|
-
if (!calcManifest.rootDataDependencies
|
|
22
|
+
if (!calcManifest.rootDataDependencies) return { canRun: true, missing };
|
|
31
23
|
for (const dep of calcManifest.rootDataDependencies) {
|
|
32
|
-
if (dep === 'portfolio'
|
|
33
|
-
else if (dep === 'insights' && !rootDataStatus.hasInsights)
|
|
34
|
-
else if (dep === 'social'
|
|
35
|
-
else if (dep === 'history'
|
|
24
|
+
if (dep === 'portfolio' && !rootDataStatus.hasPortfolio) missing.push('portfolio');
|
|
25
|
+
else if (dep === 'insights' && !rootDataStatus.hasInsights) missing.push('insights');
|
|
26
|
+
else if (dep === 'social' && !rootDataStatus.hasSocial) missing.push('social');
|
|
27
|
+
else if (dep === 'history' && !rootDataStatus.hasHistory) missing.push('history');
|
|
36
28
|
}
|
|
37
29
|
return { canRun: missing.length === 0, missing };
|
|
38
30
|
}
|
|
39
31
|
|
|
40
|
-
/**
|
|
41
|
-
* Stage 3: Check root data availability for a date
|
|
42
|
-
* @param {string} dateStr - The date to check (YYYY-MM-DD).
|
|
43
|
-
* @param {object} config - The computation system config.
|
|
44
|
-
* @param {object} dependencies - All shared dependencies (db, logger, etc.).
|
|
45
|
-
* @param {object} earliestDates - The map of earliest data dates.
|
|
46
|
-
* @returns {Promise<object|null>} The root data object or null.
|
|
47
|
-
*/
|
|
48
32
|
async function checkRootDataAvailability(dateStr, config, dependencies, earliestDates) {
|
|
49
33
|
const { logger } = dependencies;
|
|
50
|
-
logger.log('INFO', `[PassRunner] Checking root data for ${dateStr}...`);
|
|
51
34
|
const dateToProcess = new Date(dateStr + 'T00:00:00Z');
|
|
52
35
|
|
|
53
|
-
|
|
36
|
+
// Quick check for existence of required root collections
|
|
37
|
+
// (Implementation preserved from original for consistency)
|
|
38
|
+
let portfolioRefs = [], historyRefs = [];
|
|
54
39
|
let hasPortfolio = false, hasInsights = false, hasSocial = false, hasHistory = false;
|
|
40
|
+
let insightsData = null, socialData = null;
|
|
55
41
|
|
|
56
42
|
try {
|
|
57
43
|
const tasks = [];
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (dateToProcess >= earliestDates.
|
|
63
|
-
tasks.push(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
tasks.push(loadDailySocialPostInsights(config, dependencies, dateStr).then(res => { socialData = res; hasSocial = !!res; }));
|
|
67
|
-
}
|
|
68
|
-
if (dateToProcess >= earliestDates.history) {
|
|
69
|
-
tasks.push(getHistoryPartRefs(config, dependencies, dateStr).then(res => { historyRefs = res; hasHistory = !!(res?.length); }));
|
|
70
|
-
}
|
|
44
|
+
if (dateToProcess >= earliestDates.portfolio)
|
|
45
|
+
tasks.push(getPortfolioPartRefs(config, dependencies, dateStr).then(r => { portfolioRefs = r; hasPortfolio = !!r.length; }));
|
|
46
|
+
if (dateToProcess >= earliestDates.insights)
|
|
47
|
+
tasks.push(loadDailyInsights(config, dependencies, dateStr).then(r => { insightsData = r; hasInsights = !!r; }));
|
|
48
|
+
if (dateToProcess >= earliestDates.social)
|
|
49
|
+
tasks.push(loadDailySocialPostInsights(config, dependencies, dateStr).then(r => { socialData = r; hasSocial = !!r; }));
|
|
50
|
+
if (dateToProcess >= earliestDates.history)
|
|
51
|
+
tasks.push(getHistoryPartRefs(config, dependencies, dateStr).then(r => { historyRefs = r; hasHistory = !!r.length; }));
|
|
71
52
|
|
|
72
53
|
await Promise.all(tasks);
|
|
73
54
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (!(hasPortfolio || hasInsights || hasSocial || hasHistory)) { logger.log('WARN', `[PassRunner] No root data at all for ${dateStr}.`); return null; }
|
|
55
|
+
if (!(hasPortfolio || hasInsights || hasSocial || hasHistory)) return null;
|
|
77
56
|
|
|
78
|
-
return {
|
|
79
|
-
portfolioRefs,
|
|
80
|
-
todayInsights: insightsData,
|
|
81
|
-
|
|
82
|
-
historyRefs,
|
|
83
|
-
status: { hasPortfolio, hasInsights, hasSocial, hasHistory }
|
|
57
|
+
return {
|
|
58
|
+
portfolioRefs, historyRefs,
|
|
59
|
+
todayInsights: insightsData, todaySocialPostInsights: socialData,
|
|
60
|
+
status: { hasPortfolio, hasInsights, hasSocial, hasHistory }
|
|
84
61
|
};
|
|
85
62
|
} catch (err) {
|
|
86
|
-
logger.log('ERROR', `
|
|
63
|
+
logger.log('ERROR', `Error checking data: ${err.message}`);
|
|
87
64
|
return null;
|
|
88
65
|
}
|
|
89
66
|
}
|
|
90
67
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
* @param {string} dateStr - The date to fetch (YYYY-MM-DD).
|
|
94
|
-
* @param {Array} calcsInPass - The calculations in the *current* pass.
|
|
95
|
-
* @param {Array} fullManifest - The *entire* computation manifest.
|
|
96
|
-
* @param {object} config - The computation system config.
|
|
97
|
-
* @param {object} dependencies - Shared dependencies.
|
|
98
|
-
* @returns {Promise<object>} A map of { [calcName]: result } for all found dependencies.
|
|
99
|
-
*/
|
|
100
|
-
async function fetchExistingResults(dateStr, calcsInPass, fullManifest, config, { db, logger }) {
|
|
68
|
+
async function fetchExistingResults(dateStr, calcsInPass, fullManifest, config, { db }) {
|
|
69
|
+
// (Implementation preserved: loads dependencies for computations)
|
|
101
70
|
const manifestMap = new Map(fullManifest.map(c => [normalizeName(c.name), c]));
|
|
102
71
|
const calcsToFetch = new Set();
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
for (const calc of calcsInPass) { calcsToFetch.add(normalizeName(calc.name));
|
|
106
|
-
|
|
107
|
-
// Add all dependencies of those calcs (for meta-calcs)
|
|
108
|
-
if (calc.dependencies && calc.dependencies.length > 0) {
|
|
109
|
-
for (const depName of calc.dependencies) {
|
|
110
|
-
calcsToFetch.add(normalizeName(depName));
|
|
111
|
-
}
|
|
112
|
-
}
|
|
72
|
+
for (const calc of calcsInPass) {
|
|
73
|
+
if (calc.dependencies) calc.dependencies.forEach(d => calcsToFetch.add(normalizeName(d)));
|
|
113
74
|
}
|
|
75
|
+
if (!calcsToFetch.size) return {};
|
|
114
76
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
logger.log('INFO', `[PassRunner] Checking for ${calcsToFetch.size} existing results and dependencies for ${dateStr}...`);
|
|
118
|
-
|
|
77
|
+
const fetched = {};
|
|
119
78
|
const docRefs = [];
|
|
120
|
-
const
|
|
79
|
+
const names = [];
|
|
121
80
|
|
|
122
|
-
for (const
|
|
123
|
-
const
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
.doc(calcManifest.category || 'unknown')
|
|
131
|
-
.collection(config.computationsSubcollection)
|
|
132
|
-
.doc(calcName)
|
|
133
|
-
);
|
|
134
|
-
depNames.push(calcName);
|
|
81
|
+
for (const name of calcsToFetch) {
|
|
82
|
+
const m = manifestMap.get(name);
|
|
83
|
+
if (m) {
|
|
84
|
+
docRefs.push(db.collection(config.resultsCollection).doc(dateStr)
|
|
85
|
+
.collection(config.resultsSubcollection).doc(m.category || 'unknown')
|
|
86
|
+
.collection(config.computationsSubcollection).doc(name));
|
|
87
|
+
names.push(name);
|
|
88
|
+
}
|
|
135
89
|
}
|
|
136
|
-
|
|
137
|
-
const fetched = {};
|
|
90
|
+
|
|
138
91
|
if (docRefs.length) {
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
const data = doc.exists ? doc.data() : null;
|
|
142
|
-
// A result only "exists" if it was marked as completed
|
|
143
|
-
if (data && data._completed === true) { fetched[depNames[i]] = data;
|
|
144
|
-
} else { fetched[depNames[i]] = null; // Treat as not existing
|
|
145
|
-
}
|
|
146
|
-
});
|
|
92
|
+
const snaps = await db.getAll(...docRefs);
|
|
93
|
+
snaps.forEach((doc, i) => { if(doc.exists && doc.data()._completed) fetched[names[i]] = doc.data(); });
|
|
147
94
|
}
|
|
148
|
-
|
|
149
|
-
// Verbose logging for what was found/missing
|
|
150
|
-
const foundDeps = Object.entries(fetched).filter(([, data]) => data !== null).map(([key]) => key);
|
|
151
|
-
const missingDeps = Object.entries(fetched).filter(([, data]) => data === null).map(([key]) => key);
|
|
152
|
-
if (foundDeps.length > 0) { logger.log('TRACE', `[PassRunner] Found ${foundDeps.length} existing results for ${dateStr}: [${foundDeps.join(', ')}]`); }
|
|
153
|
-
if (missingDeps.length > 0) { logger.log('TRACE', `[PassRunner] Did not find ${missingDeps.length} results for ${dateStr}: [${missingDeps.join(', ')}]`); }
|
|
154
|
-
|
|
155
95
|
return fetched;
|
|
156
96
|
}
|
|
157
97
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (dep === 'portfolio' && earliestDates.portfolio > earliestRunDate) earliestRunDate = earliestDates.portfolio;
|
|
172
|
-
if (dep === 'history' && earliestDates.history > earliestRunDate) earliestRunDate = earliestDates.history;
|
|
173
|
-
if (dep === 'social' && earliestDates.social > earliestRunDate) earliestRunDate = earliestDates.social;
|
|
174
|
-
if (dep === 'insights' && earliestDates.insights > earliestRunDate) earliestRunDate = earliestDates.insights;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// If it's historical, it needs T-1 data, so add one day
|
|
178
|
-
if (calc.isHistorical && earliestRunDate.getTime() > 0) { earliestRunDate.setUTCDate(earliestRunDate.getUTCDate() + 1); }
|
|
179
|
-
return earliestRunDate;
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
const filterCalc = (calc) => {
|
|
183
|
-
// 1. Skip if already completed
|
|
184
|
-
if (existingResults[calc.name]) { logger.log('TRACE', `[Pass ${passToRun}] Skipping ${calc.name} for ${dateStr}. Result already exists (and is complete).`);
|
|
185
|
-
skipped.add(calc.name);
|
|
186
|
-
return false;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// 2. Skip if date is before this calc's earliest possible run date
|
|
190
|
-
const earliestRunDate = getTrueEarliestRunDate(calc);
|
|
191
|
-
if (dateToProcess < earliestRunDate) { logger.log('TRACE', `[Pass ${passToRun}] Skipping ${calc.name} for ${dateStr}. Date is before true earliest run date (${earliestRunDate.toISOString().slice(0, 10)}).`);
|
|
192
|
-
skipped.add(calc.name);
|
|
193
|
-
return false;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// 3. Skip if missing root data
|
|
197
|
-
const { canRun, missing: missingRoot } = checkRootDependencies(calc, rootDataStatus);
|
|
198
|
-
if (!canRun) { logger.log('INFO', `[Pass ${passToRun}] Skipping ${calc.name} for ${dateStr}. Data missing for this date: [${missingRoot.join(', ')}]`);
|
|
199
|
-
skipped.add(calc.name);
|
|
200
|
-
return false;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// 4. (Meta Calcs) Skip if missing computed dependencies
|
|
204
|
-
if (calc.type === 'meta') {
|
|
205
|
-
const missingDeps = (calc.dependencies || [])
|
|
206
|
-
.map(normalizeName)
|
|
207
|
-
.filter(d => !existingResults[d]); // This check is now robust
|
|
208
|
-
|
|
209
|
-
if (missingDeps.length > 0) { logger.log('WARN', `[Pass ${passToRun} Meta] Skipping ${calc.name} for ${dateStr}. Missing computed deps: [${missingDeps.join(', ')}]`);
|
|
210
|
-
skipped.add(calc.name);
|
|
211
|
-
return false;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
98
|
+
function filterCalculations(standardCalcs, metaCalcs, rootDataStatus, existingResults, passToRun, dateStr, earliestDates) {
|
|
99
|
+
// (Implementation preserved)
|
|
100
|
+
const filter = (c) => {
|
|
101
|
+
if (existingResults[c.name]) return false;
|
|
102
|
+
// Date check
|
|
103
|
+
let earliest = new Date('1970-01-01');
|
|
104
|
+
(c.rootDataDependencies || []).forEach(d => { if(earliestDates[d] > earliest) earliest = earliestDates[d]; });
|
|
105
|
+
if (c.isHistorical) earliest.setUTCDate(earliest.getUTCDate() + 1);
|
|
106
|
+
if (new Date(dateStr) < earliest) return false;
|
|
107
|
+
// Data check
|
|
108
|
+
if (!checkRootDependencies(c, rootDataStatus).canRun) return false;
|
|
109
|
+
// Dependency check
|
|
110
|
+
if (c.type === 'meta' && c.dependencies && c.dependencies.some(d => !existingResults[normalizeName(d)])) return false;
|
|
214
111
|
return true;
|
|
215
112
|
};
|
|
216
|
-
|
|
217
|
-
const standardCalcsToRun = standardCalcs.filter(filterCalc);
|
|
218
|
-
const metaCalcsToRun = metaCalcs.filter(filterCalc);
|
|
219
|
-
|
|
220
|
-
return { standardCalcsToRun, metaCalcsToRun };
|
|
113
|
+
return { standardCalcsToRun: standardCalcs.filter(filter), metaCalcsToRun: metaCalcs.filter(filter) };
|
|
221
114
|
}
|
|
222
115
|
|
|
223
|
-
|
|
224
|
-
* Stage 6: Initialize calculator instances
|
|
225
|
-
* @param {Array} calcs - The calculations to initialize.
|
|
226
|
-
* @param {object} logger - The logger instance.
|
|
227
|
-
* @returns {object} A map of { [calcName]: instance }.
|
|
228
|
-
*/
|
|
229
|
-
function initializeCalculators(calcs, logger) {
|
|
230
|
-
const state = {};
|
|
231
|
-
for (const c of calcs) {
|
|
232
|
-
const name = normalizeName(c.name);
|
|
233
|
-
const Cl = c.class;
|
|
234
|
-
if (typeof Cl === 'function') {
|
|
235
|
-
try {
|
|
236
|
-
const inst = new Cl();
|
|
237
|
-
inst.manifest = c; // Attach manifest for context
|
|
238
|
-
state[name] = inst;
|
|
239
|
-
} catch (e) { logger.warn(`Initialization failed for ${name}`, { errorMessage: e.message });
|
|
240
|
-
state[name] = null;
|
|
241
|
-
}
|
|
242
|
-
} else { logger.warn(`Class is missing for ${name}`);
|
|
243
|
-
state[name] = null;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
return state;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Stage 7: Load T-1 (yesterday) data needed by historical calculations.
|
|
251
|
-
* --- THIS IS THE FULLY CORRECTED FUNCTION (WITH FRIEND'S BUG FIX) ---
|
|
252
|
-
*/
|
|
253
|
-
async function loadHistoricalData(date, calcs, config, deps, rootData) {
|
|
254
|
-
const { logger } = deps;
|
|
255
|
-
const updated = { ...rootData };
|
|
256
|
-
const tasks = [];
|
|
257
|
-
|
|
258
|
-
// Check what T-1 data is needed by any calc in this pass
|
|
259
|
-
const needsYesterdayInsights = calcs.some(c => c.isHistorical && c.rootDataDependencies.includes('insights'));
|
|
260
|
-
const needsYesterdaySocial = calcs.some(c => c.isHistorical && c.rootDataDependencies.includes('social'));
|
|
261
|
-
const needsYesterdayPortfolio = calcs.some(c => c.isHistorical && c.rootDataDependencies.includes('portfolio'));
|
|
262
|
-
const needsYesterdayHistory = calcs.some(c => c.isHistorical && c.rootDataDependencies.includes('history')); // <-- The check
|
|
263
|
-
const needsYesterdayDependencies = calcs.some(c => c.isHistorical && c.dependencies && c.dependencies.length > 0);
|
|
264
|
-
|
|
265
|
-
const prev = new Date(date);
|
|
266
|
-
prev.setUTCDate(prev.getUTCDate() - 1);
|
|
267
|
-
const prevStr = prev.toISOString().slice(0, 10);
|
|
268
|
-
|
|
269
|
-
if (needsYesterdayInsights) {
|
|
270
|
-
tasks.push((async () => { logger.log('INFO', `[PassRunner] Loading YESTERDAY insights data for ${prevStr}`);
|
|
271
|
-
updated.yesterdayInsights = await loadDailyInsights(config, deps, prevStr);
|
|
272
|
-
})());
|
|
273
|
-
}
|
|
274
|
-
if (needsYesterdaySocial) {
|
|
275
|
-
tasks.push((async () => { logger.log('INFO', `[PassRunner] Loading YESTERDAY social data for ${prevStr}`);
|
|
276
|
-
updated.yesterdaySocialPostInsights = await loadDailySocialPostInsights(config, deps, prevStr);
|
|
277
|
-
})());
|
|
278
|
-
}
|
|
279
|
-
if (needsYesterdayPortfolio) {
|
|
280
|
-
tasks.push((async () => { logger.log('INFO', `[PassRunner] Getting YESTERDAY portfolio refs for ${prevStr}`);
|
|
281
|
-
updated.yesterdayPortfolioRefs = await getPortfolioPartRefs(config, deps, prevStr);
|
|
282
|
-
})());
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// --- THIS IS THE MISSING LOGIC BLOCK (FIXED) ---
|
|
286
|
-
if (needsYesterdayHistory) {
|
|
287
|
-
tasks.push((async () => { logger.log('INFO', `[PassRunner] Getting YESTERDAY history refs for ${prevStr}`);
|
|
288
|
-
updated.yesterdayHistoryRefs = await getHistoryPartRefs(config, deps, prevStr);
|
|
289
|
-
})());
|
|
290
|
-
}
|
|
291
|
-
// --- END MISSING LOGIC BLOCK ---
|
|
292
|
-
|
|
293
|
-
if (needsYesterdayDependencies) {
|
|
294
|
-
tasks.push((async () => { logger.log('INFO', `[PassRunner] Loading YESTERDAY computed dependencies for ${prevStr}`);
|
|
295
|
-
// This fetches T-1 results for *all* calcs in the current pass,
|
|
296
|
-
// which is robust and covers all historical dependency needs.
|
|
297
|
-
updated.yesterdayDependencyData = await fetchExistingResults(prevStr, calcs, calcs.map(c => c.manifest), config, deps);
|
|
298
|
-
})());
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
await Promise.all(tasks);
|
|
302
|
-
return updated;
|
|
303
|
-
}
|
|
116
|
+
// --- NEW: Execution Delegates ---
|
|
304
117
|
|
|
305
|
-
/**
|
|
306
|
-
* Stage 8: Stream and process data for standard calculations.
|
|
307
|
-
* --- THIS IS THE FULLY CORRECTED FUNCTION (WITH ALL FIXES) ---
|
|
308
|
-
* --- REPLICATES THE 7-ARGUMENT "HACK" SIGNATURE FROM TEST HARNESS ---
|
|
309
|
-
*/
|
|
310
118
|
async function streamAndProcess(dateStr, state, passName, config, deps, rootData, portfolioRefs, historyRefs, fetchedDeps) {
|
|
311
|
-
const { logger
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
// Create the shared context object
|
|
315
|
-
const mappings = await calculationUtils.loadInstrumentMappings();
|
|
316
|
-
const context = {
|
|
317
|
-
instrumentMappings: mappings.instrumentToTicker,
|
|
318
|
-
sectorMapping: mappings.instrumentToSector,
|
|
319
|
-
todayDateStr: dateStr,
|
|
320
|
-
dependencies: deps,
|
|
321
|
-
config,
|
|
322
|
-
yesterdaysDependencyData: yesterdayDependencyData
|
|
323
|
-
};
|
|
119
|
+
const { logger } = deps;
|
|
120
|
+
const controller = new ComputationController(config, deps);
|
|
324
121
|
|
|
325
|
-
//
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
if (!calc || typeof calc.process !== 'function') continue;
|
|
331
|
-
|
|
332
|
-
const cat = calc.manifest.category;
|
|
333
|
-
if (cat === 'socialPosts' || cat === 'insights') {
|
|
334
|
-
if (firstUser) {
|
|
335
|
-
logger.log('INFO', `[${passName}] Running non-streaming calc: ${name} for ${dateStr}`);
|
|
336
|
-
|
|
337
|
-
// --- CALLING 7-ARGUMENT "HACK" SIGNATURE (for non-streaming) ---
|
|
338
|
-
// We emulate the test harness's `process` call for social/insights
|
|
339
|
-
const userContext = { ...context, userType: 'n/a' };
|
|
340
|
-
const todayPayload = null; // No user data
|
|
341
|
-
const yesterdayPayload = null; // No user data
|
|
342
|
-
|
|
343
|
-
try {
|
|
344
|
-
await Promise.resolve(calc.process(
|
|
345
|
-
todayPayload, // Arg 1: The data object
|
|
346
|
-
yesterdayPayload, // Arg 2: Yesterday's data
|
|
347
|
-
null, // Arg 3: User ID
|
|
348
|
-
userContext, // Arg 4: Context
|
|
349
|
-
todayInsights, // Arg 5: Today Insights
|
|
350
|
-
yesterdayInsights, // Arg 6: Yesterday Insights
|
|
351
|
-
fetchedDeps // Arg 7: Fetched Dependencies
|
|
352
|
-
));
|
|
353
|
-
} catch (e) { logger.log('WARN', `Process error on ${name} (non-stream) for ${dateStr}`, { err: e.message }); }
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// --- FIX 1: THE FAULTY GUARD CLAUSE (From friend) ---
|
|
359
|
-
// This now correctly checks for calcs that need 'portfolio' OR 'history',
|
|
360
|
-
// matching the test harness behavior of running history-only passes.
|
|
361
|
-
const calcsThatStream = Object.values(state).filter(calc =>
|
|
362
|
-
calc && calc.manifest && (
|
|
363
|
-
calc.manifest.rootDataDependencies.includes('portfolio') ||
|
|
364
|
-
calc.manifest.rootDataDependencies.includes('history')
|
|
365
|
-
)
|
|
122
|
+
// 1. Filter for Standard Calculations (that need streaming)
|
|
123
|
+
const calcs = Object.values(state).filter(c => c && c.manifest);
|
|
124
|
+
const streamingCalcs = calcs.filter(c =>
|
|
125
|
+
c.manifest.rootDataDependencies.includes('portfolio') ||
|
|
126
|
+
c.manifest.rootDataDependencies.includes('history')
|
|
366
127
|
);
|
|
367
128
|
|
|
368
|
-
if (
|
|
369
|
-
logger.log('INFO', `[${passName}] No portfolio or history streaming calcs to run for ${dateStr}. Skipping stream.`);
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
129
|
+
if (streamingCalcs.length === 0) return;
|
|
372
130
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
const prevDate = new Date(dateStr + 'T00:00:00Z');
|
|
378
|
-
prevDate.setUTCDate(prevDate.getUTCDate() - 1);
|
|
131
|
+
logger.log('INFO', `[${passName}] Streaming for ${streamingCalcs.length} computations...`);
|
|
132
|
+
|
|
133
|
+
// 2. Prepare Streams
|
|
134
|
+
await controller.loader.loadMappings(); // Pre-cache mappings
|
|
135
|
+
const prevDate = new Date(dateStr + 'T00:00:00Z'); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
|
|
379
136
|
const prevDateStr = prevDate.toISOString().slice(0, 10);
|
|
380
137
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const p = chunk[uid];
|
|
410
|
-
if (!p) continue;
|
|
411
|
-
|
|
412
|
-
const userType = p.PublicPositions ? 'speculator' : 'normal';
|
|
413
|
-
const userContext = { ...context, userType };
|
|
414
|
-
|
|
415
|
-
// Get corresponding T-1 data
|
|
416
|
-
const pY = yesterdayPortfolios[uid] || null;
|
|
417
|
-
const hT = todayHistoryData[uid] || null;
|
|
418
|
-
const hY = yesterdayHistoryData[uid] || null; // <-- This will now have data
|
|
419
|
-
|
|
420
|
-
for (const name in state) {
|
|
421
|
-
const calc = state[name];
|
|
422
|
-
if (!calc || typeof calc.process !== 'function') continue;
|
|
423
|
-
|
|
424
|
-
const manifest = calc.manifest;
|
|
425
|
-
const cat = manifest.category;
|
|
426
|
-
const isSocialOrInsights = cat === 'socialPosts' || cat === 'insights';
|
|
427
|
-
if (isSocialOrInsights) continue; // Handled above
|
|
428
|
-
|
|
429
|
-
const isSpeculatorCalc = cat === 'speculators';
|
|
430
|
-
const isUserProcessed = name === 'users-processed';
|
|
431
|
-
|
|
432
|
-
if (userType === 'normal' && isSpeculatorCalc) continue;
|
|
433
|
-
if (userType === 'speculator' && !isSpeculatorCalc && !isUserProcessed) continue;
|
|
434
|
-
|
|
435
|
-
if (manifest.isHistorical && !pY) {
|
|
436
|
-
if (cat !== 'behavioural' && name !== 'historical-performance-aggregator') {
|
|
437
|
-
continue;
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// --- FIX 3: REPLICATE 7-ARGUMENT "HACK" SIGNATURE ---
|
|
442
|
-
// This logic block replicates the test harness's 'todayPayload'
|
|
443
|
-
// and 'yesterdayPayload' construction.
|
|
444
|
-
const rootDataDeps = manifest.rootDataDependencies || ['portfolio'];
|
|
445
|
-
const needsHistoricalData = manifest.isHistorical || false;
|
|
446
|
-
|
|
447
|
-
let todayPayload = null;
|
|
448
|
-
let yesterdayPayload = null;
|
|
449
|
-
|
|
450
|
-
if (rootDataDeps.includes('portfolio')) {
|
|
451
|
-
todayPayload = p || {}; // Start with portfolio
|
|
452
|
-
yesterdayPayload = needsHistoricalData ? (pY || {}) : null;
|
|
453
|
-
// Nest history if also requested
|
|
454
|
-
if (rootDataDeps.includes('history')) {
|
|
455
|
-
todayPayload.history = hT;
|
|
456
|
-
if (yesterdayPayload) yesterdayPayload.history = hY;
|
|
457
|
-
}
|
|
458
|
-
} else if (rootDataDeps.includes('history')) {
|
|
459
|
-
// If *only* history is requested, it becomes Arg 1
|
|
460
|
-
todayPayload = hT;
|
|
461
|
-
yesterdayPayload = needsHistoricalData ? hY : null;
|
|
462
|
-
} else {
|
|
463
|
-
// Fallback for calcs like price-metrics
|
|
464
|
-
todayPayload = p || {};
|
|
465
|
-
yesterdayPayload = needsHistoricalData ? (pY || {}) : null;
|
|
466
|
-
}
|
|
467
|
-
// --- END PAYLOAD CONSTRUCTION ---
|
|
468
|
-
|
|
469
|
-
try {
|
|
470
|
-
// Call with the 7-argument signature
|
|
471
|
-
await Promise.resolve(calc.process(
|
|
472
|
-
todayPayload, // Arg 1: The data object (built above)
|
|
473
|
-
yesterdayPayload, // Arg 2: Yesterday's data
|
|
474
|
-
uid, // Arg 3: User ID
|
|
475
|
-
userContext, // Arg 4: Context
|
|
476
|
-
todayInsights, // Arg 5: Today Insights
|
|
477
|
-
yesterdayInsights, // Arg 6: Yesterday Insights
|
|
478
|
-
fetchedDeps // Arg 7: Fetched Dependencies
|
|
479
|
-
));
|
|
480
|
-
} catch (e) {
|
|
481
|
-
logger.log('WARN', `Process error on ${name} for ${uid} on ${dateStr}`, { err: e.message });
|
|
482
|
-
}
|
|
483
|
-
} // end for(calc)
|
|
484
|
-
|
|
485
|
-
firstUser = false;
|
|
486
|
-
|
|
487
|
-
// Clear processed users from memory
|
|
488
|
-
if (pY) { delete yesterdayPortfolios[uid]; }
|
|
489
|
-
if (hT) { delete todayHistoryData[uid]; }
|
|
490
|
-
if (hY) { delete yesterdayHistoryData[uid]; }
|
|
491
|
-
} // end for(uid in chunk)
|
|
492
|
-
} // end for await(chunk)
|
|
493
|
-
|
|
494
|
-
// Clear stale data to prevent memory leaks
|
|
495
|
-
yesterdayPortfolios = {};
|
|
496
|
-
todayHistoryData = {};
|
|
497
|
-
yesterdayHistoryData = {};
|
|
498
|
-
|
|
499
|
-
logger.log('INFO', `[${passName}] Finished streaming data for ${dateStr}.`);
|
|
138
|
+
const tP_iter = streamPortfolioData(config, deps, dateStr, portfolioRefs);
|
|
139
|
+
// Only stream yesterday if we have refs and computations need historical data
|
|
140
|
+
const needsHistory = streamingCalcs.some(c => c.manifest.isHistorical);
|
|
141
|
+
const yP_iter = (needsHistory && rootData.yesterdayPortfolioRefs)
|
|
142
|
+
? streamPortfolioData(config, deps, prevDateStr, rootData.yesterdayPortfolioRefs)
|
|
143
|
+
: null;
|
|
144
|
+
|
|
145
|
+
let yP_chunk = {};
|
|
146
|
+
|
|
147
|
+
// 3. Stream & Execute
|
|
148
|
+
for await (const tP_chunk of tP_iter) {
|
|
149
|
+
if (yP_iter) yP_chunk = (await yP_iter.next()).value || {};
|
|
150
|
+
|
|
151
|
+
// Execute Batch
|
|
152
|
+
const promises = streamingCalcs.map(calc =>
|
|
153
|
+
controller.executor.executePerUser(
|
|
154
|
+
calc,
|
|
155
|
+
calc.manifest,
|
|
156
|
+
dateStr,
|
|
157
|
+
tP_chunk, // Today's Portfolio
|
|
158
|
+
yP_chunk, // Yesterday's Portfolio
|
|
159
|
+
fetchedDeps
|
|
160
|
+
)
|
|
161
|
+
);
|
|
162
|
+
await Promise.all(promises);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
logger.log('INFO', `[${passName}] Streaming complete.`);
|
|
500
166
|
}
|
|
501
167
|
|
|
168
|
+
// --- Pass Runners ---
|
|
502
169
|
|
|
503
|
-
/**
|
|
504
|
-
* Stage 9: Run standard computations
|
|
505
|
-
* --- MODIFIED: Now accepts 'fetchedDeps' and passes it to 'streamAndProcess' ---
|
|
506
|
-
*/
|
|
507
170
|
async function runStandardComputationPass(date, calcs, passName, config, deps, rootData, fetchedDeps) {
|
|
508
|
-
const dStr = date.toISOString().slice(0, 10)
|
|
509
|
-
|
|
510
|
-
logger.log('INFO', `[${passName}] No standard calcs to run for ${dStr} after filtering.`);
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
logger.log('INFO', `[${passName}] Running ${dStr} with ${calcs.length} standard calcs: [${calcs.map(c => c.name).join(', ')}]`);
|
|
515
|
-
|
|
516
|
-
// Load T-1 data (portfolio, insights, social, history, computed)
|
|
517
|
-
const fullRoot = await loadHistoricalData(date, calcs, config, deps, rootData);
|
|
171
|
+
const dStr = date.toISOString().slice(0, 10);
|
|
172
|
+
const logger = deps.logger;
|
|
518
173
|
|
|
519
|
-
//
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
174
|
+
// 1. Load additional historical context if needed (e.g. previous day's insights)
|
|
175
|
+
// Note: Portfolio/History streams are handled inside streamAndProcess
|
|
176
|
+
// We just need to attach references if they exist
|
|
177
|
+
const fullRoot = { ...rootData };
|
|
178
|
+
if (calcs.some(c => c.isHistorical)) {
|
|
179
|
+
const prev = new Date(date); prev.setUTCDate(prev.getUTCDate() - 1);
|
|
180
|
+
const prevStr = prev.toISOString().slice(0, 10);
|
|
181
|
+
fullRoot.yesterdayPortfolioRefs = await getPortfolioPartRefs(config, deps, prevStr);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 2. Initialize Calculation Instances
|
|
185
|
+
const state = {};
|
|
186
|
+
for (const c of calcs) {
|
|
187
|
+
try {
|
|
188
|
+
const inst = new c.class();
|
|
189
|
+
inst.manifest = c;
|
|
190
|
+
state[normalizeName(c.name)] = inst;
|
|
191
|
+
} catch(e) { logger.log('WARN', `Failed to init ${c.name}`); }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 3. Execute (Delegated to Controller via Helper)
|
|
524
195
|
await streamAndProcess(dStr, state, passName, config, deps, fullRoot, rootData.portfolioRefs, rootData.historyRefs, fetchedDeps);
|
|
525
|
-
// --- END CHANGE ---
|
|
526
196
|
|
|
527
|
-
//
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
const standardWrites = [];
|
|
532
|
-
const shardedWrites = {};
|
|
533
|
-
const schemasToStore = [];
|
|
197
|
+
// 4. Commit Results
|
|
198
|
+
await commitResults(state, dStr, passName, config, deps);
|
|
199
|
+
}
|
|
534
200
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
201
|
+
async function runMetaComputationPass(date, calcs, passName, config, deps, fetchedDeps, rootData) {
|
|
202
|
+
const controller = new ComputationController(config, deps);
|
|
203
|
+
const dStr = date.toISOString().slice(0, 10);
|
|
204
|
+
const state = {};
|
|
539
205
|
|
|
206
|
+
for (const mCalc of calcs) {
|
|
540
207
|
try {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
const result = await Promise.resolve(calc.getResult(fetchedDeps));
|
|
544
|
-
// --- END CHANGE ---
|
|
208
|
+
const inst = new mCalc.class();
|
|
209
|
+
inst.manifest = mCalc;
|
|
545
210
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
if (key.startsWith('sharded_')) {
|
|
552
|
-
const shardedData = result[key];
|
|
553
|
-
for (const collectionName in shardedData) {
|
|
554
|
-
if (!shardedWrites[collectionName]) shardedWrites[collectionName] = {};
|
|
555
|
-
Object.assign(shardedWrites[collectionName], shardedData[collectionName]);
|
|
556
|
-
}
|
|
557
|
-
} else {
|
|
558
|
-
standardResult[key] = result[key];
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// --- Handle Standard Writes ---
|
|
563
|
-
if (Object.keys(standardResult).length > 0) {
|
|
564
|
-
const docRef = deps.db.collection(config.resultsCollection).doc(dStr)
|
|
565
|
-
.collection(config.resultsSubcollection).doc(calc.manifest.category)
|
|
566
|
-
.collection(config.computationsSubcollection).doc(name);
|
|
567
|
-
|
|
568
|
-
standardResult._completed = true; // Mark as complete
|
|
569
|
-
standardWrites.push({ ref: docRef, data: standardResult });
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// --- Capture Schema ---
|
|
573
|
-
const calcClass = calc.manifest.class;
|
|
574
|
-
let staticSchema = null;
|
|
575
|
-
if (calcClass && typeof calcClass.getSchema === 'function') {
|
|
576
|
-
try {
|
|
577
|
-
staticSchema = calcClass.getSchema();
|
|
578
|
-
} catch (e) { logger.log('WARN', `[SchemaCapture] Failed to get static schema for ${name} on ${dStr}`, { err: e.message }); }
|
|
579
|
-
} else { logger.log('TRACE', `[SchemaCapture] No static schema found for ${name}. Skipping manifest entry.`); }
|
|
580
|
-
|
|
581
|
-
if (staticSchema) {
|
|
582
|
-
schemasToStore.push({
|
|
583
|
-
name,
|
|
584
|
-
category: calc.manifest.category,
|
|
585
|
-
schema: staticSchema,
|
|
586
|
-
metadata: {
|
|
587
|
-
isHistorical: calc.manifest.isHistorical || false,
|
|
588
|
-
dependencies: calc.manifest.dependencies || [],
|
|
589
|
-
rootDataDependencies: calc.manifest.rootDataDependencies || [],
|
|
590
|
-
pass: calc.manifest.pass,
|
|
591
|
-
type: calc.manifest.type || 'standard'
|
|
592
|
-
}
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
successCalcs.push(name);
|
|
596
|
-
} else {
|
|
597
|
-
// Calc ran but returned no data
|
|
598
|
-
successCalcs.push(name);
|
|
599
|
-
}
|
|
600
|
-
} catch (e) { logger.log('ERROR', `getResult failed for ${name} on ${dStr}`, { err: e.message, stack: e.stack });
|
|
601
|
-
failedCalcs.push({ name, error: e.message });
|
|
211
|
+
await controller.executor.executeOncePerDay(inst, mCalc, dStr, fetchedDeps);
|
|
212
|
+
|
|
213
|
+
state[normalizeName(mCalc.name)] = inst;
|
|
214
|
+
} catch (e) {
|
|
215
|
+
deps.logger.log('ERROR', `Meta calc failed ${mCalc.name}: ${e.message}`);
|
|
602
216
|
}
|
|
603
|
-
} // --- End Get Results Loop ---
|
|
604
|
-
|
|
605
|
-
// --- Commit Writes ---
|
|
606
|
-
if (schemasToStore.length > 0) {
|
|
607
|
-
batchStoreSchemas(deps, config, schemasToStore).catch(err => { logger.log('WARN', `[SchemaCapture] Non-blocking schema storage failed for ${dStr}`, { errorMessage: err.message }); }); }
|
|
608
|
-
|
|
609
|
-
if (standardWrites.length > 0) { await commitBatchInChunks(config, deps, standardWrites, `${passName} Standard ${dStr}`); }
|
|
610
|
-
|
|
611
|
-
for (const docPath in shardedWrites) {
|
|
612
|
-
const docData = shardedWrites[docPath];
|
|
613
|
-
const shardedDocWrites = [];
|
|
614
|
-
let docRef;
|
|
615
|
-
if (docPath.includes('/')) { docRef = deps.db.doc(docPath);
|
|
616
|
-
} else { const collection = (docPath.startsWith('user_profile_history')) ? config.shardedUserProfileCollection : config.shardedProfitabilityCollection; docRef = deps.db.collection(collection).doc(docPath); }
|
|
617
|
-
|
|
618
|
-
if (docData && typeof docData === 'object' && !Array.isArray(docData)) {
|
|
619
|
-
docData._completed = true;
|
|
620
|
-
shardedDocWrites.push({ ref: docRef, data: docData });
|
|
621
|
-
} else { logger.log('ERROR', `[${passName}] Invalid sharded document data for ${docPath} on ${dStr}. Not an object.`, { data: docData }); }
|
|
622
|
-
|
|
623
|
-
if (shardedDocWrites.length > 0) { await commitBatchInChunks(config, deps, shardedDocWrites, `${passName} Sharded ${docPath} ${dStr}`); }
|
|
624
217
|
}
|
|
625
218
|
|
|
626
|
-
|
|
627
|
-
const logMetadata = {
|
|
628
|
-
total_expected: calcs.length,
|
|
629
|
-
success_count: successCalcs.length,
|
|
630
|
-
failed_count: failedCalcs.length,
|
|
631
|
-
successful_calcs: successCalcs,
|
|
632
|
-
failed_calcs: failedCalcs
|
|
633
|
-
};
|
|
634
|
-
logger.log( failedCalcs.length === 0 ? 'SUCCESS' : 'WARN', `[${passName}] Completed ${dStr}.`, logMetadata );
|
|
219
|
+
await commitResults(state, dStr, passName, config, deps);
|
|
635
220
|
}
|
|
636
221
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
* @param {Array} calcs - The meta calculations to run.
|
|
641
|
-
* @param {string} passName - The name of the pass (for logging).
|
|
642
|
-
* @param {object} config - Computation system config.
|
|
643
|
-
* @param {object} deps - Shared dependencies.
|
|
644
|
-
* @param {object} fetchedDeps - In-memory results from *previous* passes.
|
|
645
|
-
* @param {object} rootData - The loaded root data for today.
|
|
646
|
-
*/
|
|
647
|
-
async function runMetaComputationPass(date, calcs, passName, config, deps, fetchedDeps, rootData) {
|
|
648
|
-
const dStr = date.toISOString().slice(0, 10), logger = deps.logger;
|
|
649
|
-
if (calcs.length === 0) { logger.log('INFO', `[${passName}] No meta calcs to run for ${dStr} after filtering.`); return; }
|
|
222
|
+
// --- Commit Helper (Standardized) ---
|
|
223
|
+
async function commitResults(stateObj, dStr, passName, config, deps) {
|
|
224
|
+
const writes = [], schemas = [], sharded = {};
|
|
650
225
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
// Load T-1 data (needed for stateful meta calcs)
|
|
654
|
-
const fullRoot = await loadHistoricalData(date, calcs, config, deps, rootData);
|
|
655
|
-
|
|
656
|
-
// --- Verbose Logging Setup ---
|
|
657
|
-
const successCalcs = [];
|
|
658
|
-
const failedCalcs = [];
|
|
659
|
-
|
|
660
|
-
const standardWrites = [];
|
|
661
|
-
const shardedWrites = {};
|
|
662
|
-
const schemasToStore = [];
|
|
663
|
-
|
|
664
|
-
for (const mCalc of calcs) {
|
|
665
|
-
const name = normalizeName(mCalc.name);
|
|
666
|
-
const Cl = mCalc.class;
|
|
667
|
-
|
|
668
|
-
if (typeof Cl !== 'function') {
|
|
669
|
-
logger.log('ERROR', `Invalid class ${name} on ${dStr}`);
|
|
670
|
-
failedCalcs.push({ name, error: "Invalid class" });
|
|
671
|
-
continue;
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
const inst = new Cl();
|
|
675
|
-
|
|
226
|
+
for (const name in stateObj) {
|
|
227
|
+
const calc = stateObj[name];
|
|
676
228
|
try {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
229
|
+
const result = await calc.getResult();
|
|
230
|
+
if (!result) continue;
|
|
231
|
+
|
|
232
|
+
const standardRes = {};
|
|
233
|
+
for (const key in result) {
|
|
234
|
+
if (key.startsWith('sharded_')) {
|
|
235
|
+
const sData = result[key];
|
|
236
|
+
for (const c in sData) { sharded[c] = sharded[c] || {}; Object.assign(sharded[c], sData[c]); }
|
|
237
|
+
} else standardRes[key] = result[key];
|
|
681
238
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
priceData: null, // You don't load this yet, but test harness has it
|
|
692
|
-
yesterdayInsights: fullRoot.yesterdayInsights,
|
|
693
|
-
yesterdayPriceData: null, // You don't load this yet
|
|
694
|
-
date: dStr
|
|
695
|
-
};
|
|
696
|
-
|
|
697
|
-
const result = await Promise.resolve(inst.process(
|
|
698
|
-
metaPayload, // Arg 1: The data object (for your hacks)
|
|
699
|
-
fullRoot, // Arg 2: (rootData)
|
|
700
|
-
deps, // Arg 3: (dependencies)
|
|
701
|
-
config, // Arg 4: (config)
|
|
702
|
-
fetchedDeps // Arg 5: (fetchedDependencies)
|
|
703
|
-
));
|
|
704
|
-
// --- END SIGNATURE REPLICATION ---
|
|
705
|
-
|
|
706
|
-
if (result && Object.keys(result).length > 0) {
|
|
707
|
-
const standardResult = {};
|
|
708
|
-
|
|
709
|
-
// --- Handle Sharded Writes ---
|
|
710
|
-
for (const key in result) {
|
|
711
|
-
if (key.startsWith('sharded_')) {
|
|
712
|
-
const shardedData = result[key];
|
|
713
|
-
for (const collectionName in shardedData) {
|
|
714
|
-
if (!shardedWrites[collectionName]) shardedWrites[collectionName] = {};
|
|
715
|
-
Object.assign(shardedWrites[collectionName], shardedData[collectionName]);
|
|
716
|
-
}
|
|
717
|
-
} else {
|
|
718
|
-
standardResult[key] = result[key];
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
// --- Handle Standard Writes ---
|
|
723
|
-
if (Object.keys(standardResult).length > 0) {
|
|
724
|
-
const docRef = deps.db.collection(config.resultsCollection).doc(dStr)
|
|
725
|
-
.collection(config.resultsSubcollection).doc(mCalc.category)
|
|
726
|
-
.collection(config.computationsSubcollection).doc(name);
|
|
727
|
-
|
|
728
|
-
standardResult._completed = true; // Mark as complete
|
|
729
|
-
standardWrites.push({ ref: docRef, data: standardResult });
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
// --- Capture Schema ---
|
|
733
|
-
const calcClass = mCalc.class;
|
|
734
|
-
let staticSchema = null;
|
|
735
|
-
if (calcClass && typeof calcClass.getSchema === 'function') {
|
|
736
|
-
try {
|
|
737
|
-
staticSchema = calcClass.getSchema();
|
|
738
|
-
} catch (e) { logger.log('WARN', `[SchemaCapture] Failed to get static schema for ${name} on ${dStr}`, { err: e.message }); }
|
|
739
|
-
} else { logger.log('TRACE', `[SchemaCapture] No static schema found for ${name}. Skipping manifest entry.`); }
|
|
740
|
-
|
|
741
|
-
if (staticSchema) {
|
|
742
|
-
schemasToStore.push({
|
|
743
|
-
name,
|
|
744
|
-
category: mCalc.category,
|
|
745
|
-
schema: staticSchema,
|
|
746
|
-
metadata: {
|
|
747
|
-
isHistorical: mCalc.isHistorical || false,
|
|
748
|
-
dependencies: mCalc.dependencies || [],
|
|
749
|
-
rootDataDependencies: mCalc.rootDataDependencies || [],
|
|
750
|
-
pass: mCalc.pass,
|
|
751
|
-
type: 'meta'
|
|
752
|
-
}
|
|
753
|
-
});
|
|
754
|
-
}
|
|
755
|
-
successCalcs.push(name);
|
|
756
|
-
} else {
|
|
757
|
-
// Calc ran but returned no data
|
|
758
|
-
successCalcs.push(name);
|
|
239
|
+
|
|
240
|
+
if (Object.keys(standardRes).length) {
|
|
241
|
+
standardRes._completed = true;
|
|
242
|
+
writes.push({
|
|
243
|
+
ref: deps.db.collection(config.resultsCollection).doc(dStr)
|
|
244
|
+
.collection(config.resultsSubcollection).doc(calc.manifest.category)
|
|
245
|
+
.collection(config.computationsSubcollection).doc(name),
|
|
246
|
+
data: standardRes
|
|
247
|
+
});
|
|
759
248
|
}
|
|
760
|
-
} catch (e) { logger.log('ERROR', `Meta-calc failed ${name} for ${dStr}`, { err: e.message, stack: e.stack });
|
|
761
|
-
failedCalcs.push({ name, error: e.message });
|
|
762
|
-
}
|
|
763
|
-
} // --- End Meta-Calc Loop ---
|
|
764
249
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
});
|
|
250
|
+
if (calc.manifest.class.getSchema) {
|
|
251
|
+
schemas.push({ name, category: calc.manifest.category, schema: calc.manifest.class.getSchema(), metadata: calc.manifest });
|
|
252
|
+
}
|
|
253
|
+
} catch (e) { deps.logger.log('ERROR', `Commit failed ${name}: ${e.message}`); }
|
|
770
254
|
}
|
|
255
|
+
|
|
256
|
+
if (schemas.length) batchStoreSchemas(deps, config, schemas).catch(()=>{});
|
|
257
|
+
if (writes.length) await commitBatchInChunks(config, deps, writes, `${passName} Results`);
|
|
771
258
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
for (const docId in docs) {
|
|
778
|
-
const docRef = docId.includes('/') ? deps.db.doc(docId) : deps.db.collection(collectionName).doc(docId);
|
|
779
|
-
const docData = docs[docId];
|
|
780
|
-
docData._completed = true; // Mark as complete
|
|
781
|
-
shardedDocWrites.push({ ref: docRef, data: docData });
|
|
259
|
+
for (const col in sharded) {
|
|
260
|
+
const sWrites = [];
|
|
261
|
+
for (const id in sharded[col]) {
|
|
262
|
+
const ref = id.includes('/') ? deps.db.doc(id) : deps.db.collection(col).doc(id);
|
|
263
|
+
sWrites.push({ ref, data: { ...sharded[col][id], _completed: true } });
|
|
782
264
|
}
|
|
783
|
-
if (
|
|
265
|
+
if (sWrites.length) await commitBatchInChunks(config, deps, sWrites, `${passName} Sharded ${col}`);
|
|
784
266
|
}
|
|
785
|
-
|
|
786
|
-
// --- Final Verbose Log ---
|
|
787
|
-
const logMetadata = {
|
|
788
|
-
total_expected: calcs.length,
|
|
789
|
-
success_count: successCalcs.length,
|
|
790
|
-
failed_count: failedCalcs.length,
|
|
791
|
-
successful_calcs: successCalcs,
|
|
792
|
-
failed_calcs: failedCalcs
|
|
793
|
-
};
|
|
794
|
-
logger.log( failedCalcs.length === 0 ? 'SUCCESS' : 'WARN', `[${passName}] Completed ${dStr}.`,logMetadata );
|
|
795
267
|
}
|
|
796
268
|
|
|
797
|
-
|
|
798
269
|
module.exports = {
|
|
799
270
|
groupByPass,
|
|
800
271
|
checkRootDataAvailability,
|