bulltrackers-module 1.0.170 → 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/computation_pass_runner.js +10 -2
- package/functions/computation-system/helpers/orchestration_helpers.js +191 -652
- package/functions/computation-system/layers/math_primitives.js +346 -0
- package/functions/task-engine/helpers/discover_helpers.js +33 -51
- package/functions/task-engine/helpers/update_helpers.js +57 -159
- package/functions/task-engine/helpers/verify_helpers.js +27 -44
- package/package.json +1 -5
|
@@ -1,732 +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
|
-
}
|
|
116
|
+
// --- NEW: Execution Delegates ---
|
|
248
117
|
|
|
249
|
-
|
|
250
|
-
* Stage 7: Load T-1 (yesterday) data needed by historical calculations.
|
|
251
|
-
* --- THIS FUNCTION IS NOW FIXED ---
|
|
252
|
-
*/
|
|
253
|
-
async function loadHistoricalData(date, calcs, config, deps, rootData) {
|
|
118
|
+
async function streamAndProcess(dateStr, state, passName, config, deps, rootData, portfolioRefs, historyRefs, fetchedDeps) {
|
|
254
119
|
const { logger } = deps;
|
|
255
|
-
const
|
|
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 FIX
|
|
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
|
-
// --- FIX: Load yesterday's history refs ---
|
|
285
|
-
if (needsYesterdayHistory) {
|
|
286
|
-
tasks.push((async () => { logger.log('INFO', `[PassRunner] Getting YESTERDAY history refs for ${prevStr}`);
|
|
287
|
-
updated.yesterdayHistoryRefs = await getHistoryPartRefs(config, deps, prevStr);
|
|
288
|
-
})());
|
|
289
|
-
}
|
|
290
|
-
// --- END FIX ---
|
|
291
|
-
|
|
292
|
-
if (needsYesterdayDependencies) {
|
|
293
|
-
tasks.push((async () => { logger.log('INFO', `[PassRunner] Loading YESTERDAY computed dependencies for ${prevStr}`);
|
|
294
|
-
// This fetches T-1 results for *all* calcs in the current pass,
|
|
295
|
-
// which is robust and covers all historical dependency needs.
|
|
296
|
-
updated.yesterdayDependencyData = await fetchExistingResults(prevStr, calcs, calcs.map(c => c.manifest), config, deps);
|
|
297
|
-
})());
|
|
298
|
-
}
|
|
120
|
+
const controller = new ComputationController(config, deps);
|
|
299
121
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
* Stage 8: Stream and process data for standard calculations.
|
|
306
|
-
* --- THIS FUNCTION IS NOW FIXED ---
|
|
307
|
-
*/
|
|
308
|
-
async function streamAndProcess(dateStr, state, passName, config, deps, rootData, portfolioRefs, historyRefs) {
|
|
309
|
-
const { logger, calculationUtils } = deps;
|
|
310
|
-
const { todayInsights, yesterdayInsights, todaySocialPostInsights, yesterdaySocialPostInsights, yesterdayDependencyData } = rootData;
|
|
311
|
-
|
|
312
|
-
// Create the shared context object
|
|
313
|
-
const mappings = await calculationUtils.loadInstrumentMappings();
|
|
314
|
-
const context = {
|
|
315
|
-
instrumentMappings: mappings.instrumentToTicker,
|
|
316
|
-
sectorMapping: mappings.instrumentToSector,
|
|
317
|
-
todayDateStr: dateStr,
|
|
318
|
-
dependencies: deps,
|
|
319
|
-
config,
|
|
320
|
-
yesterdaysDependencyData: yesterdayDependencyData
|
|
321
|
-
};
|
|
322
|
-
|
|
323
|
-
// --- Run non-streaming (meta) calcs once ---
|
|
324
|
-
let firstUser = true;
|
|
325
|
-
for (const name in state) {
|
|
326
|
-
const calc = state[name];
|
|
327
|
-
if (!calc || typeof calc.process !== 'function') continue;
|
|
328
|
-
|
|
329
|
-
const cat = calc.manifest.category;
|
|
330
|
-
if (cat === 'socialPosts' || cat === 'insights') {
|
|
331
|
-
if (firstUser) {
|
|
332
|
-
logger.log('INFO', `[${passName}] Running non-streaming calc: ${name} for ${dateStr}`);
|
|
333
|
-
const args = [null, null, null, { ...context, userType: 'n/a' }, todayInsights, yesterdayInsights, todaySocialPostInsights, yesterdaySocialPostInsights, null, null];
|
|
334
|
-
try {
|
|
335
|
-
await Promise.resolve(calc.process(...args));
|
|
336
|
-
} catch (e) { logger.log('WARN', `Process error on ${name} (non-stream) for ${dateStr}`, { err: e.message }); }
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
const calcsThatStreamPortfolio = Object.values(state).filter(calc =>
|
|
342
|
-
calc && calc.manifest && (calc.manifest.rootDataDependencies.includes('portfolio'))
|
|
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')
|
|
343
127
|
);
|
|
344
128
|
|
|
345
|
-
if (
|
|
346
|
-
logger.log('INFO', `[${passName}] No portfolio-streaming calcs to run for ${dateStr}. Skipping stream.`);
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
129
|
+
if (streamingCalcs.length === 0) return;
|
|
349
130
|
|
|
350
|
-
logger.log('INFO', `[${passName}] Streaming
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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);
|
|
354
136
|
const prevDateStr = prevDate.toISOString().slice(0, 10);
|
|
355
137
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
if (!p) continue;
|
|
385
|
-
|
|
386
|
-
const userType = p.PublicPositions ? 'speculator' : 'normal';
|
|
387
|
-
context.userType = userType;
|
|
388
|
-
|
|
389
|
-
// Get corresponding T-1 data
|
|
390
|
-
const pY = yesterdayPortfolios[uid] || null;
|
|
391
|
-
const hT = todayHistoryData[uid] || null;
|
|
392
|
-
const hY = yesterdayHistoryData[uid] || null; // <-- THE FIX
|
|
393
|
-
|
|
394
|
-
for (const name in state) {
|
|
395
|
-
const calc = state[name];
|
|
396
|
-
if (!calc || typeof calc.process !== 'function') continue;
|
|
397
|
-
|
|
398
|
-
// --- Refactored Filter Block ---
|
|
399
|
-
const manifest = calc.manifest;
|
|
400
|
-
const cat = manifest.category;
|
|
401
|
-
const isSocialOrInsights = cat === 'socialPosts' || cat === 'insights';
|
|
402
|
-
if (isSocialOrInsights) continue; // Handled above
|
|
403
|
-
|
|
404
|
-
const isSpeculatorCalc = cat === 'speculators';
|
|
405
|
-
const isUserProcessed = name === 'users-processed';
|
|
406
|
-
|
|
407
|
-
// Skip if user type doesn't match calc type
|
|
408
|
-
if (userType === 'normal' && isSpeculatorCalc) continue;
|
|
409
|
-
if (userType === 'speculator' && !isSpeculatorCalc && !isUserProcessed) continue;
|
|
410
|
-
|
|
411
|
-
// Skip historical calcs if T-1 portfolio is missing (with exceptions)
|
|
412
|
-
if (manifest.isHistorical && !pY) {
|
|
413
|
-
// These calcs only need T-1 *history*, not portfolio
|
|
414
|
-
if (cat !== 'behavioural' && name !== 'historical-performance-aggregator') {
|
|
415
|
-
continue;
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
// --- End Filter Block ---
|
|
419
|
-
|
|
420
|
-
let args;
|
|
421
|
-
if (manifest.isHistorical) {
|
|
422
|
-
// Full 10-argument array for historical calcs
|
|
423
|
-
args = [p, pY, uid, context, todayInsights, yesterdayInsights, todaySocialPostInsights, yesterdaySocialPostInsights, hT, hY]; // <-- hY is 10th arg
|
|
424
|
-
} else {
|
|
425
|
-
// 10-argument array for non-historical (T-1 args are null)
|
|
426
|
-
args = [p, null, uid, context, todayInsights, null, todaySocialPostInsights, null, hT, null];
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
try {
|
|
430
|
-
await Promise.resolve(calc.process(...args));
|
|
431
|
-
} catch (e) {
|
|
432
|
-
logger.log('WARN', `Process error on ${name} for ${uid} on ${dateStr}`, { err: e.message });
|
|
433
|
-
}
|
|
434
|
-
} // end for(calc)
|
|
435
|
-
|
|
436
|
-
firstUser = false;
|
|
437
|
-
|
|
438
|
-
// Clear processed users from memory
|
|
439
|
-
if (pY) { delete yesterdayPortfolios[uid]; }
|
|
440
|
-
if (hT) { delete todayHistoryData[uid]; }
|
|
441
|
-
if (hY) { delete yesterdayHistoryData[uid]; }
|
|
442
|
-
} // end for(uid in chunk)
|
|
443
|
-
} // end for await(chunk)
|
|
444
|
-
|
|
445
|
-
// Clear stale data to prevent memory leaks
|
|
446
|
-
yesterdayPortfolios = {};
|
|
447
|
-
todayHistoryData = {};
|
|
448
|
-
yesterdayHistoryData = {};
|
|
449
|
-
|
|
450
|
-
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.`);
|
|
451
166
|
}
|
|
452
167
|
|
|
168
|
+
// --- Pass Runners ---
|
|
453
169
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
* @param {Array} calcs - The calculations to run.
|
|
458
|
-
* @param {string} passName - The name of the pass (for logging).
|
|
459
|
-
* @param {object} config - Computation system config.
|
|
460
|
-
* @param {object} deps - Shared dependencies.
|
|
461
|
-
* @param {object} rootData - The loaded root data for today.
|
|
462
|
-
*/
|
|
463
|
-
async function runStandardComputationPass(date, calcs, passName, config, deps, rootData) {
|
|
464
|
-
const dStr = date.toISOString().slice(0, 10), logger = deps.logger;
|
|
465
|
-
if (calcs.length === 0) {
|
|
466
|
-
logger.log('INFO', `[${passName}] No standard calcs to run for ${dStr} after filtering.`);
|
|
467
|
-
return;
|
|
468
|
-
}
|
|
170
|
+
async function runStandardComputationPass(date, calcs, passName, config, deps, rootData, fetchedDeps) {
|
|
171
|
+
const dStr = date.toISOString().slice(0, 10);
|
|
172
|
+
const logger = deps.logger;
|
|
469
173
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
//
|
|
473
|
-
const fullRoot =
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
await streamAndProcess(dStr, state, passName, config, deps, fullRoot, rootData.portfolioRefs, rootData.historyRefs);
|
|
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
|
+
}
|
|
480
183
|
|
|
481
|
-
//
|
|
482
|
-
const
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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)
|
|
195
|
+
await streamAndProcess(dStr, state, passName, config, deps, fullRoot, rootData.portfolioRefs, rootData.historyRefs, fetchedDeps);
|
|
488
196
|
|
|
489
|
-
//
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
if (!calc || typeof calc.getResult !== 'function') continue;
|
|
197
|
+
// 4. Commit Results
|
|
198
|
+
await commitResults(state, dStr, passName, config, deps);
|
|
199
|
+
}
|
|
493
200
|
|
|
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 = {};
|
|
205
|
+
|
|
206
|
+
for (const mCalc of calcs) {
|
|
494
207
|
try {
|
|
495
|
-
const
|
|
208
|
+
const inst = new mCalc.class();
|
|
209
|
+
inst.manifest = mCalc;
|
|
496
210
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
if (key.startsWith('sharded_')) {
|
|
503
|
-
const shardedData = result[key];
|
|
504
|
-
for (const collectionName in shardedData) {
|
|
505
|
-
if (!shardedWrites[collectionName]) shardedWrites[collectionName] = {};
|
|
506
|
-
Object.assign(shardedWrites[collectionName], shardedData[collectionName]);
|
|
507
|
-
}
|
|
508
|
-
} else {
|
|
509
|
-
standardResult[key] = result[key];
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
// --- Handle Standard Writes ---
|
|
514
|
-
if (Object.keys(standardResult).length > 0) {
|
|
515
|
-
const docRef = deps.db.collection(config.resultsCollection).doc(dStr)
|
|
516
|
-
.collection(config.resultsSubcollection).doc(calc.manifest.category)
|
|
517
|
-
.collection(config.computationsSubcollection).doc(name);
|
|
518
|
-
|
|
519
|
-
standardResult._completed = true; // Mark as complete
|
|
520
|
-
standardWrites.push({ ref: docRef, data: standardResult });
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// --- Capture Schema ---
|
|
524
|
-
const calcClass = calc.manifest.class;
|
|
525
|
-
let staticSchema = null;
|
|
526
|
-
if (calcClass && typeof calcClass.getSchema === 'function') {
|
|
527
|
-
try {
|
|
528
|
-
staticSchema = calcClass.getSchema();
|
|
529
|
-
} catch (e) { logger.log('WARN', `[SchemaCapture] Failed to get static schema for ${name} on ${dStr}`, { err: e.message }); }
|
|
530
|
-
} else { logger.log('TRACE', `[SchemaCapture] No static schema found for ${name}. Skipping manifest entry.`); }
|
|
531
|
-
|
|
532
|
-
if (staticSchema) {
|
|
533
|
-
schemasToStore.push({
|
|
534
|
-
name,
|
|
535
|
-
category: calc.manifest.category,
|
|
536
|
-
schema: staticSchema,
|
|
537
|
-
metadata: {
|
|
538
|
-
isHistorical: calc.manifest.isHistorical || false,
|
|
539
|
-
dependencies: calc.manifest.dependencies || [],
|
|
540
|
-
rootDataDependencies: calc.manifest.rootDataDependencies || [],
|
|
541
|
-
pass: calc.manifest.pass,
|
|
542
|
-
type: calc.manifest.type || 'standard'
|
|
543
|
-
}
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
successCalcs.push(name);
|
|
547
|
-
} else {
|
|
548
|
-
// Calc ran but returned no data
|
|
549
|
-
successCalcs.push(name);
|
|
550
|
-
}
|
|
551
|
-
} catch (e) { logger.log('ERROR', `getResult failed for ${name} on ${dStr}`, { err: e.message, stack: e.stack });
|
|
552
|
-
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}`);
|
|
553
216
|
}
|
|
554
|
-
} // --- End Get Results Loop ---
|
|
555
|
-
|
|
556
|
-
// --- Commit Writes ---
|
|
557
|
-
if (schemasToStore.length > 0) {
|
|
558
|
-
batchStoreSchemas(deps, config, schemasToStore).catch(err => { logger.log('WARN', `[SchemaCapture] Non-blocking schema storage failed for ${dStr}`, { errorMessage: err.message }); }); }
|
|
559
|
-
|
|
560
|
-
if (standardWrites.length > 0) { await commitBatchInChunks(config, deps, standardWrites, `${passName} Standard ${dStr}`); }
|
|
561
|
-
|
|
562
|
-
for (const docPath in shardedWrites) {
|
|
563
|
-
const docData = shardedWrites[docPath];
|
|
564
|
-
const shardedDocWrites = [];
|
|
565
|
-
let docRef;
|
|
566
|
-
if (docPath.includes('/')) { docRef = deps.db.doc(docPath);
|
|
567
|
-
} else { const collection = (docPath.startsWith('user_profile_history')) ? config.shardedUserProfileCollection : config.shardedProfitabilityCollection; docRef = deps.db.collection(collection).doc(docPath); }
|
|
568
|
-
|
|
569
|
-
if (docData && typeof docData === 'object' && !Array.isArray(docData)) {
|
|
570
|
-
docData._completed = true;
|
|
571
|
-
shardedDocWrites.push({ ref: docRef, data: docData });
|
|
572
|
-
} else { logger.log('ERROR', `[${passName}] Invalid sharded document data for ${docPath} on ${dStr}. Not an object.`, { data: docData }); }
|
|
573
|
-
|
|
574
|
-
if (shardedDocWrites.length > 0) { await commitBatchInChunks(config, deps, shardedDocWrites, `${passName} Sharded ${docPath} ${dStr}`); }
|
|
575
217
|
}
|
|
576
218
|
|
|
577
|
-
|
|
578
|
-
const logMetadata = {
|
|
579
|
-
total_expected: calcs.length,
|
|
580
|
-
success_count: successCalcs.length,
|
|
581
|
-
failed_count: failedCalcs.length,
|
|
582
|
-
successful_calcs: successCalcs,
|
|
583
|
-
failed_calcs: failedCalcs
|
|
584
|
-
};
|
|
585
|
-
logger.log( failedCalcs.length === 0 ? 'SUCCESS' : 'WARN', `[${passName}] Completed ${dStr}.`, logMetadata );
|
|
219
|
+
await commitResults(state, dStr, passName, config, deps);
|
|
586
220
|
}
|
|
587
221
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
* @param {Array} calcs - The meta calculations to run.
|
|
592
|
-
* @param {string} passName - The name of the pass (for logging).
|
|
593
|
-
* @param {object} config - Computation system config.
|
|
594
|
-
* @param {object} deps - Shared dependencies.
|
|
595
|
-
* @param {object} fetchedDeps - In-memory results from *previous* passes.
|
|
596
|
-
* @param {object} rootData - The loaded root data for today.
|
|
597
|
-
*/
|
|
598
|
-
async function runMetaComputationPass(date, calcs, passName, config, deps, fetchedDeps, rootData) {
|
|
599
|
-
const dStr = date.toISOString().slice(0, 10), logger = deps.logger;
|
|
600
|
-
if (calcs.length === 0) { logger.log('INFO', `[${passName}] No meta calcs to run for ${dStr} after filtering.`); return; }
|
|
601
|
-
|
|
602
|
-
logger.log('INFO', `[${passName}] Running ${dStr} with ${calcs.length} meta calcs: [${calcs.map(c => c.name).join(', ')}]`);
|
|
603
|
-
|
|
604
|
-
// Load T-1 data (needed for stateful meta calcs)
|
|
605
|
-
const fullRoot = await loadHistoricalData(date, calcs, config, deps, rootData);
|
|
606
|
-
|
|
607
|
-
// --- Verbose Logging Setup ---
|
|
608
|
-
const successCalcs = [];
|
|
609
|
-
const failedCalcs = [];
|
|
222
|
+
// --- Commit Helper (Standardized) ---
|
|
223
|
+
async function commitResults(stateObj, dStr, passName, config, deps) {
|
|
224
|
+
const writes = [], schemas = [], sharded = {};
|
|
610
225
|
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
const schemasToStore = [];
|
|
614
|
-
|
|
615
|
-
for (const mCalc of calcs) {
|
|
616
|
-
const name = normalizeName(mCalc.name);
|
|
617
|
-
const Cl = mCalc.class;
|
|
618
|
-
|
|
619
|
-
if (typeof Cl !== 'function') {
|
|
620
|
-
logger.log('ERROR', `Invalid class ${name} on ${dStr}`);
|
|
621
|
-
failedCalcs.push({ name, error: "Invalid class" });
|
|
622
|
-
continue;
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
const inst = new Cl();
|
|
626
|
-
|
|
226
|
+
for (const name in stateObj) {
|
|
227
|
+
const calc = stateObj[name];
|
|
627
228
|
try {
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
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];
|
|
632
238
|
}
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
for (const key in result) {
|
|
643
|
-
if (key.startsWith('sharded_')) {
|
|
644
|
-
const shardedData = result[key];
|
|
645
|
-
for (const collectionName in shardedData) {
|
|
646
|
-
if (!shardedWrites[collectionName]) shardedWrites[collectionName] = {};
|
|
647
|
-
Object.assign(shardedWrites[collectionName], shardedData[collectionName]);
|
|
648
|
-
}
|
|
649
|
-
} else {
|
|
650
|
-
standardResult[key] = result[key];
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
// --- Handle Standard Writes ---
|
|
655
|
-
if (Object.keys(standardResult).length > 0) {
|
|
656
|
-
const docRef = deps.db.collection(config.resultsCollection).doc(dStr)
|
|
657
|
-
.collection(config.resultsSubcollection).doc(mCalc.category)
|
|
658
|
-
.collection(config.computationsSubcollection).doc(name);
|
|
659
|
-
|
|
660
|
-
standardResult._completed = true; // Mark as complete
|
|
661
|
-
standardWrites.push({ ref: docRef, data: standardResult });
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// --- Capture Schema ---
|
|
665
|
-
const calcClass = mCalc.class;
|
|
666
|
-
let staticSchema = null;
|
|
667
|
-
if (calcClass && typeof calcClass.getSchema === 'function') {
|
|
668
|
-
try {
|
|
669
|
-
staticSchema = calcClass.getSchema();
|
|
670
|
-
} catch (e) { logger.log('WARN', `[SchemaCapture] Failed to get static schema for ${name} on ${dStr}`, { err: e.message }); }
|
|
671
|
-
} else { logger.log('TRACE', `[SchemaCapture] No static schema found for ${name}. Skipping manifest entry.`); }
|
|
672
|
-
|
|
673
|
-
if (staticSchema) {
|
|
674
|
-
schemasToStore.push({
|
|
675
|
-
name,
|
|
676
|
-
category: mCalc.category,
|
|
677
|
-
schema: staticSchema,
|
|
678
|
-
metadata: {
|
|
679
|
-
isHistorical: mCalc.isHistorical || false,
|
|
680
|
-
dependencies: mCalc.dependencies || [],
|
|
681
|
-
rootDataDependencies: mCalc.rootDataDependencies || [],
|
|
682
|
-
pass: mCalc.pass,
|
|
683
|
-
type: 'meta'
|
|
684
|
-
}
|
|
685
|
-
});
|
|
686
|
-
}
|
|
687
|
-
successCalcs.push(name);
|
|
688
|
-
} else {
|
|
689
|
-
// Calc ran but returned no data
|
|
690
|
-
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
|
+
});
|
|
691
248
|
}
|
|
692
|
-
} catch (e) { logger.log('ERROR', `Meta-calc failed ${name} for ${dStr}`, { err: e.message, stack: e.stack });
|
|
693
|
-
failedCalcs.push({ name, error: e.message });
|
|
694
|
-
}
|
|
695
|
-
} // --- End Meta-Calc Loop ---
|
|
696
249
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
});
|
|
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}`); }
|
|
702
254
|
}
|
|
255
|
+
|
|
256
|
+
if (schemas.length) batchStoreSchemas(deps, config, schemas).catch(()=>{});
|
|
257
|
+
if (writes.length) await commitBatchInChunks(config, deps, writes, `${passName} Results`);
|
|
703
258
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
for (const docId in docs) {
|
|
710
|
-
const docRef = docId.includes('/') ? deps.db.doc(docId) : deps.db.collection(collectionName).doc(docId);
|
|
711
|
-
const docData = docs[docId];
|
|
712
|
-
docData._completed = true; // Mark as complete
|
|
713
|
-
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 } });
|
|
714
264
|
}
|
|
715
|
-
if (
|
|
265
|
+
if (sWrites.length) await commitBatchInChunks(config, deps, sWrites, `${passName} Sharded ${col}`);
|
|
716
266
|
}
|
|
717
|
-
|
|
718
|
-
// --- Final Verbose Log ---
|
|
719
|
-
const logMetadata = {
|
|
720
|
-
total_expected: calcs.length,
|
|
721
|
-
success_count: successCalcs.length,
|
|
722
|
-
failed_count: failedCalcs.length,
|
|
723
|
-
successful_calcs: successCalcs,
|
|
724
|
-
failed_calcs: failedCalcs
|
|
725
|
-
};
|
|
726
|
-
logger.log( failedCalcs.length === 0 ? 'SUCCESS' : 'WARN', `[${passName}] Completed ${dStr}.`,logMetadata );
|
|
727
267
|
}
|
|
728
268
|
|
|
729
|
-
|
|
730
269
|
module.exports = {
|
|
731
270
|
groupByPass,
|
|
732
271
|
checkRootDataAvailability,
|