bulltrackers-module 1.0.152 → 1.0.154

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/functions/appscript-api/index.js +8 -38
  2. package/functions/computation-system/helpers/computation_pass_runner.js +38 -183
  3. package/functions/computation-system/helpers/orchestration_helpers.js +105 -326
  4. package/functions/computation-system/utils/data_loader.js +38 -133
  5. package/functions/computation-system/utils/schema_capture.js +7 -41
  6. package/functions/computation-system/utils/utils.js +37 -124
  7. package/functions/core/utils/firestore_utils.js +8 -46
  8. package/functions/core/utils/intelligent_header_manager.js +26 -128
  9. package/functions/core/utils/intelligent_proxy_manager.js +33 -171
  10. package/functions/core/utils/pubsub_utils.js +7 -24
  11. package/functions/dispatcher/helpers/dispatch_helpers.js +9 -30
  12. package/functions/dispatcher/index.js +7 -30
  13. package/functions/etoro-price-fetcher/helpers/handler_helpers.js +12 -80
  14. package/functions/fetch-insights/helpers/handler_helpers.js +18 -70
  15. package/functions/generic-api/helpers/api_helpers.js +28 -167
  16. package/functions/generic-api/index.js +49 -188
  17. package/functions/invalid-speculator-handler/helpers/handler_helpers.js +10 -47
  18. package/functions/orchestrator/helpers/discovery_helpers.js +1 -5
  19. package/functions/orchestrator/index.js +1 -6
  20. package/functions/price-backfill/helpers/handler_helpers.js +13 -69
  21. package/functions/social-orchestrator/helpers/orchestrator_helpers.js +5 -37
  22. package/functions/social-task-handler/helpers/handler_helpers.js +29 -186
  23. package/functions/speculator-cleanup-orchestrator/helpers/cleanup_helpers.js +19 -78
  24. package/functions/task-engine/handler_creator.js +2 -8
  25. package/functions/task-engine/helpers/update_helpers.js +74 -100
  26. package/functions/task-engine/helpers/verify_helpers.js +11 -56
  27. package/functions/task-engine/utils/firestore_batch_manager.js +29 -65
  28. package/functions/task-engine/utils/task_engine_utils.js +14 -37
  29. package/index.js +45 -43
  30. package/package.json +1 -1
@@ -1,26 +1,22 @@
1
1
  const { FieldPath } = require('@google-cloud/firestore');
2
- // --- MODIFIED: Import streamPortfolioData ---
3
2
  const { getPortfolioPartRefs, loadFullDayMap, loadDataByRefs, loadDailyInsights, loadDailySocialPostInsights, getHistoryPartRefs, streamPortfolioData, streamHistoryData } = require('../utils/data_loader.js');
4
3
  const { normalizeName, commitBatchInChunks } = require('../utils/utils.js');
5
- // --- CHANGED: We only need batchStoreSchemas now ---
6
- const { batchStoreSchemas } = require('../utils/schema_capture');
4
+ const { batchStoreSchemas } = require('../utils/schema_capture.js');
7
5
 
8
6
  /** Stage 1: Group manifest by pass number */
9
7
  function groupByPass(manifest) { return manifest.reduce((acc, calc) => { (acc[calc.pass] = acc[calc.pass] || []).push(calc); return acc; }, {}); }
10
8
 
11
9
  /** * --- MODIFIED: Returns detailed missing dependencies for logging ---
12
10
  * Stage 2: Check root data dependencies for a calc
13
- * --- THIS FUNCTION IS NOW MORE GRANULAR ---
14
11
  */
15
12
  function checkRootDependencies(calcManifest, rootDataStatus) {
16
13
  const missing = [];
17
14
  if (!calcManifest.rootDataDependencies || !calcManifest.rootDataDependencies.length) { return { canRun: true, missing };}
18
15
  for (const dep of calcManifest.rootDataDependencies) {
19
- if (dep === 'portfolio' && !rootDataStatus.hasPortfolio) missing.push('portfolio');
20
- else if (dep === 'insights' && !rootDataStatus.hasInsights) missing.push('insights');
21
- else if (dep === 'social' && !rootDataStatus.hasSocial) missing.push('social');
22
- else if (dep === 'history' && !rootDataStatus.hasHistory) missing.push('history');
23
- }
16
+ if (dep === 'portfolio' && !rootDataStatus.hasPortfolio) missing.push('portfolio');
17
+ else if (dep === 'insights' && !rootDataStatus.hasInsights) missing.push('insights');
18
+ else if (dep === 'social' && !rootDataStatus.hasSocial) missing.push('social');
19
+ else if (dep === 'history' && !rootDataStatus.hasHistory) missing.push('history'); }
24
20
  return { canRun: missing.length === 0, missing };
25
21
  }
26
22
 
@@ -40,7 +36,7 @@ async function checkRootDataAvailability(dateStr, config, dependencies, earliest
40
36
  if (dateToProcess >= earliestDates.social) {tasks.push(loadDailySocialPostInsights(config, dependencies, dateStr).then(res => {socialData = res;hasSocial = !!res;}));}
41
37
  if (dateToProcess >= earliestDates.history) {tasks.push(getHistoryPartRefs(config, dependencies, dateStr).then(res => {historyRefs = res;hasHistory = !!(res?.length);}));}
42
38
  await Promise.all(tasks);
43
- logger.log('INFO', `[PassRunner] Data availability for ${dateStr}: P:${hasPortfolio}, I:${hasInsights}, S:${hasSocial}, H:${hasHistory}`); //Fixed bug, was logging insightsData and socialData objects
39
+ logger.log('INFO', `[PassRunner] Data availability for ${dateStr}: P:${hasPortfolio}, I:${hasInsights}, S:${hasSocial}, H:${hasHistory}`);
44
40
  if (!(hasPortfolio || hasInsights || hasSocial || hasHistory)) { logger.log('WARN', `[PassRunner] No root data at all for ${dateStr}.`); return null; }
45
41
  return { portfolioRefs, todayInsights: insightsData, todaySocialPostInsights: socialData, historyRefs, status: { hasPortfolio, hasInsights, hasSocial, hasHistory } };
46
42
  } catch (err) { logger.log('ERROR', `[PassRunner] Error checking data for ${dateStr}`, { errorMessage: err.message }); return null; }
@@ -77,9 +73,9 @@ function filterCalculations(standardCalcs, metaCalcs, rootDataStatus, existingRe
77
73
  const dependencies = calc.rootDataDependencies || [];
78
74
  for (const dep of dependencies) {
79
75
  if (dep === 'portfolio' && earliestDates.portfolio > earliestRunDate) earliestRunDate = earliestDates.portfolio;
80
- if (dep === 'history' && earliestDates.history > earliestRunDate) earliestRunDate = earliestDates.history;
81
- if (dep === 'social' && earliestDates.social > earliestRunDate) earliestRunDate = earliestDates.social;
82
- if (dep === 'insights' && earliestDates.insights > earliestRunDate) earliestRunDate = earliestDates.insights;
76
+ if (dep === 'history' && earliestDates.history > earliestRunDate) earliestRunDate = earliestDates.history;
77
+ if (dep === 'social' && earliestDates.social > earliestRunDate) earliestRunDate = earliestDates.social;
78
+ if (dep === 'insights' && earliestDates.insights > earliestRunDate) earliestRunDate = earliestDates.insights;
83
79
  }
84
80
  if (calc.isHistorical && earliestRunDate.getTime() > 0) { earliestRunDate.setUTCDate(earliestRunDate.getUTCDate() + 1); }
85
81
  return earliestRunDate;
@@ -103,21 +99,16 @@ function initializeCalculators(calcs, logger) { const state = {}; for (const c o
103
99
 
104
100
  /** * Stage 7: Load historical data required for calculations
105
101
  */
106
- // --- MODIFIED: Stage 7: Load ONLY non-streaming historical data ---
107
102
  async function loadHistoricalData(date, calcs, config, deps, rootData) {
108
103
  const { logger } = deps;
109
104
  const updated = {...rootData};
110
105
  const tasks = [];
111
106
  const needsYesterdayInsights = calcs.some(c => c.isHistorical && c.rootDataDependencies.includes('insights'));
112
107
  const needsYesterdaySocial = calcs.some(c => c.isHistorical && c.rootDataDependencies.includes('social'));
113
-
114
- // --- ADD THIS ---
115
108
  const needsYesterdayPortfolio = calcs.some(c => c.isHistorical && c.rootDataDependencies.includes('portfolio'));
116
109
  const prev = new Date(date);
117
110
  prev.setUTCDate(prev.getUTCDate() - 1);
118
111
  const prevStr = prev.toISOString().slice(0, 10);
119
- // --- END ADD ---
120
-
121
112
  if(needsYesterdayInsights) {
122
113
  tasks.push((async()=>{ const prev=new Date(date); prev.setUTCDate(prev.getUTCDate()-1); const prevStr=prev.toISOString().slice(0,10);
123
114
  logger.log('INFO', `[PassRunner] Loading YESTERDAY insights data for ${prevStr}`);
@@ -127,28 +118,18 @@ async function loadHistoricalData(date, calcs, config, deps, rootData) {
127
118
  logger.log('INFO', `[PassRunner] Loading YESTERDAY social data for ${prevStr}`);
128
119
  updated.yesterdaySocialPostInsights=await loadDailySocialPostInsights(config,deps,prevStr); })());}
129
120
 
130
- // --- ADD THIS BLOCK ---
131
121
  if(needsYesterdayPortfolio) {
132
122
  tasks.push((async()=>{
133
123
  logger.log('INFO', `[PassRunner] Getting YESTERDAY portfolio refs for ${prevStr}`);
134
- // This adds the refs to the 'fullRoot' object for later
135
124
  updated.yesterdayPortfolioRefs = await getPortfolioPartRefs(config, deps, prevStr);
136
125
  })());
137
126
  }
138
- // --- END ADD ---
139
-
140
127
  await Promise.all(tasks);
141
128
  return updated;
142
129
  }
143
130
 
144
131
  /**
145
132
  * --- REFACTORED: Stage 8: Stream and process data for standard calculations ---
146
- * This function now streams today's portfolios, yesterday's portfolios,
147
- * and today's history data in parallel to avoid OOM errors.
148
- * It loads chunks of all three streams, processes UIDs found in the
149
- * main (today's portfolio) stream, and then deletes processed users
150
- * from the historical maps to free memory.
151
- * --- FIX: Added portfolioRefs and historyRefs to signature ---
152
133
  */
153
134
  async function streamAndProcess(dateStr, state, passName, config, deps, rootData, portfolioRefs, historyRefs) {
154
135
  const { logger, calculationUtils } = deps;
@@ -156,231 +137,101 @@ async function streamAndProcess(dateStr, state, passName, config, deps, rootData
156
137
  const calcsThatStreamPortfolio = Object.values(state).filter(calc => calc && calc.manifest && (calc.manifest.rootDataDependencies.includes('portfolio') || calc.manifest.category === 'speculators'));
157
138
  const context={instrumentMappings:(await calculationUtils.loadInstrumentMappings()).instrumentToTicker, sectorMapping:(await calculationUtils.loadInstrumentMappings()).instrumentToSector, todayDateStr:dateStr, dependencies:deps, config};
158
139
  let firstUser=true;
159
-
160
- for(const name in state){
161
- const calc=state[name]; if(!calc||typeof calc.process!=='function') continue;
162
- const cat=calc.manifest.category;
163
- if(cat==='socialPosts'||cat==='insights') {
164
- if (firstUser) {
165
- logger.log('INFO', `[${passName}] Running non-streaming calc: ${name}`);
166
- let args=[null,null,null,{...context, userType: 'n/a'},todayInsights,yesterdayInsights,todaySocialPostInsights,yesterdaySocialPostInsights,null,null];
167
- if(calc.manifest.isHistorical) {
168
- args=[null,null,null,{...context, userType: 'n/a'},todayInsights,yesterdayInsights,todaySocialPostInsights,yesterdaySocialPostInsights,null,null];
169
- }
170
- try{ await Promise.resolve(calc.process(...args)); } catch(e){logger.log('WARN',`Process error ${name} (non-stream)`,{err:e.message});}
171
- }
172
- }
173
- }
174
-
175
- if (calcsThatStreamPortfolio.length === 0) {
176
- logger.log('INFO', `[${passName}] No portfolio-streaming calcs to run for ${dateStr}. Skipping stream.`);
177
- return;
178
- }
179
-
140
+ for(const name in state){ const calc=state[name]; if(!calc||typeof calc.process!=='function') continue;
141
+ const cat=calc.manifest.category;
142
+ if(cat==='socialPosts'||cat==='insights') {
143
+ if (firstUser) {
144
+ logger.log('INFO', `[${passName}] Running non-streaming calc: ${name}`);
145
+ let args=[null,null,null,{...context, userType: 'n/a'},todayInsights,yesterdayInsights,todaySocialPostInsights,yesterdaySocialPostInsights,null,null];
146
+ if(calc.manifest.isHistorical) { args=[null,null,null,{...context, userType: 'n/a'},todayInsights,yesterdayInsights,todaySocialPostInsights,yesterdaySocialPostInsights,null,null]; }
147
+ try{ await Promise.resolve(calc.process(...args)); } catch(e){logger.log('WARN',`Process error ${name} (non-stream)`,{err:e.message});} } } }
148
+ if (calcsThatStreamPortfolio.length === 0) { logger.log('INFO', `[${passName}] No portfolio-streaming calcs to run for ${dateStr}. Skipping stream.`); return; }
180
149
  logger.log('INFO', `[${passName}] Streaming portfolio & historical data for ${calcsThatStreamPortfolio.length} calcs...`);
181
-
182
150
  const prevDate = new Date(dateStr + 'T00:00:00Z');
183
151
  prevDate.setUTCDate(prevDate.getUTCDate() - 1);
184
152
  const prevDateStr = prevDate.toISOString().slice(0, 10);
185
-
186
153
  const needsYesterdayPortfolio = Object.values(state).some(c => c && c.manifest.isHistorical && c.manifest.rootDataDependencies.includes('portfolio'));
187
154
  const needsTodayHistory = Object.values(state).some(c => c && c.manifest.rootDataDependencies.includes('history'));
188
-
189
- // --- FIX: Pass pre-fetched refs to the generators ---
190
- // Use the refs from fullRoot (which is rootData + yesterdayPortfolioRefs)
191
155
  const yP_iterator = needsYesterdayPortfolio ? streamPortfolioData(config, deps, prevDateStr, rootData.yesterdayPortfolioRefs) : null;
192
- // Use the historyRefs argument
193
156
  const hT_iterator = needsTodayHistory ? streamHistoryData(config, deps, dateStr, historyRefs) : null;
194
- // --- END FIX ---
195
-
196
157
  let yesterdayPortfolios = {};
197
158
  let todayHistoryData = {};
198
-
199
- if (yP_iterator) {
200
- Object.assign(yesterdayPortfolios, (await yP_iterator.next()).value || {});
201
- logger.log('INFO', `[${passName}] Loaded first chunk of yesterday's portfolios.`);
202
- }
203
- if (hT_iterator) {
204
- Object.assign(todayHistoryData, (await hT_iterator.next()).value || {});
205
- logger.log('INFO', `[${passName}] Loaded first chunk of today's history.`);
206
- }
207
-
208
- // --- FIX: Pass pre-fetched portfolioRefs to the main loop generator ---
159
+ if (yP_iterator) { Object.assign(yesterdayPortfolios, (await yP_iterator.next()).value || {}); logger.log('INFO', `[${passName}] Loaded first chunk of yesterday's portfolios.`); }
160
+ if (hT_iterator) { Object.assign(todayHistoryData, (await hT_iterator.next()).value || {}); logger.log('INFO', `[${passName}] Loaded first chunk of today's history.`); }
209
161
  for await (const chunk of streamPortfolioData(config, deps, dateStr, portfolioRefs)) {
210
- // --- END FIX ---
211
-
212
- if (yP_iterator) {
213
- Object.assign(yesterdayPortfolios, (await yP_iterator.next()).value || {});
214
- }
215
- if (hT_iterator) {
216
- Object.assign(todayHistoryData, (await hT_iterator.next()).value || {});
217
- }
218
-
219
- for(const uid in chunk){
220
- const p = chunk[uid]; if(!p) continue;
221
- const userType=p.PublicPositions?'speculator':'normal';
222
- context.userType=userType;
223
-
224
- const pY = yesterdayPortfolios[uid] || null; // Yesterday's Portfolio
225
- const hT = todayHistoryData[uid] || null; // Today's History
226
- // (Note: yesterdayHistoryData (hY) would require another stream if needed)
227
-
228
- for(const name in state){
229
- const calc=state[name]; if(!calc||typeof calc.process!=='function') continue;
230
- const cat=calc.manifest.category, isSocialOrInsights=cat==='socialPosts'||cat==='insights', isHistorical=calc.manifest.isHistorical, isSpec=cat==='speculators';
231
-
232
- if(isSocialOrInsights) continue; // Skip non-streaming calcs
233
-
234
- let args=[p,null,uid,context,todayInsights,yesterdayInsights,todaySocialPostInsights,yesterdaySocialPostInsights,hT,null];
235
-
236
- if(isHistorical){
237
- if(!pY && (cat !== 'behavioural' && name !== 'historical-performance-aggregator')) continue;
238
- args=[p,pY,uid,context,todayInsights,yesterdayInsights,todaySocialPostInsights,yesterdaySocialPostInsights,hT,null];
239
- }
240
-
241
- if((userType==='normal'&&isSpec)||(userType==='speculator'&&!isSpec&&name!=='users-processed')) continue;
242
-
243
- try{ await Promise.resolve(calc.process(...args)); } catch(e){logger.log('WARN',`Process error ${name} for ${uid}`,{err:e.message});}
244
- }
245
- firstUser=false;
246
-
247
- if (pY) { delete yesterdayPortfolios[uid]; }
248
- if (hT) { delete todayHistoryData[uid]; }
249
- }
250
- }
162
+ if (yP_iterator) { Object.assign(yesterdayPortfolios, (await yP_iterator.next()).value || {}); }
163
+ if (hT_iterator) { Object.assign(todayHistoryData, (await hT_iterator.next()).value || {}); }
164
+ for(const uid in chunk){
165
+ const p = chunk[uid]; if(!p) continue;
166
+ const userType=p.PublicPositions?'speculator':'normal';
167
+ context.userType=userType;
168
+ const pY = yesterdayPortfolios[uid] || null;
169
+ const hT = todayHistoryData[uid] || null;
170
+ for(const name in state){
171
+ const calc=state[name]; if(!calc||typeof calc.process!=='function') continue;
172
+ const cat=calc.manifest.category, isSocialOrInsights=cat==='socialPosts'||cat==='insights', isHistorical=calc.manifest.isHistorical, isSpec=cat==='speculators';
173
+ if(isSocialOrInsights) continue;
174
+ let args=[p,null,uid,context,todayInsights,yesterdayInsights,todaySocialPostInsights,yesterdaySocialPostInsights,hT,null];
175
+ if(isHistorical){ if(!pY && (cat !== 'behavioural' && name !== 'historical-performance-aggregator')) continue; args=[p,pY,uid,context,todayInsights,yesterdayInsights,todaySocialPostInsights,yesterdaySocialPostInsights,hT,null]; }
176
+ if((userType==='normal'&&isSpec)||(userType==='speculator'&&!isSpec&&name!=='users-processed')) continue;
177
+ try{ await Promise.resolve(calc.process(...args)); } catch(e){logger.log('WARN',`Process error ${name} for ${uid}`,{err:e.message});} }
178
+ firstUser=false;
179
+ if (pY) { delete yesterdayPortfolios[uid]; }
180
+ if (hT) { delete todayHistoryData[uid]; } } }
251
181
  logger.log('INFO', `[${passName}] Finished streaming data for ${dateStr}.`);
252
182
  }
253
183
 
254
184
  /** Stage 9: Run standard computations */
255
185
  async function runStandardComputationPass(date, calcs, passName, config, deps, rootData) {
256
186
  const dStr = date.toISOString().slice(0, 10), logger = deps.logger;
257
- if (calcs.length === 0) {
258
- logger.log('INFO', `[${passName}] No standard calcs to run for ${dStr} after filtering.`);
259
- return;
260
- }
187
+ if (calcs.length === 0) { logger.log('INFO', `[${passName}] No standard calcs to run for ${dStr} after filtering.`); return; }
261
188
  logger.log('INFO', `[${passName}] Running ${dStr} with ${calcs.length} calcs.`);
262
189
  const fullRoot = await loadHistoricalData(date, calcs, config, deps, rootData);
263
190
  const state = initializeCalculators(calcs, logger);
264
-
265
- // --- FIX: Pass portfolioRefs and historyRefs from rootData ---
266
191
  await streamAndProcess(dStr, state, passName, config, deps, fullRoot, rootData.portfolioRefs, rootData.historyRefs);
267
- // --- END FIX ---
268
-
269
192
  let success = 0;
270
193
  const failedCalcs = [];
271
194
  const standardWrites = [];
272
195
  const shardedWrites = {};
273
-
274
- // === NEW: Collect schemas ===
275
196
  const schemasToStore = [];
276
-
277
- for (const name in state) {
278
- const calc = state[name];
279
- if (!calc || typeof calc.getResult !== 'function') continue;
280
- try {
281
- const result = await Promise.resolve(calc.getResult());
282
- if (result && Object.keys(result).length > 0) {
283
- const standardResult = {};
284
- for (const key in result) {
285
- if (key.startsWith('sharded_')) {
286
- const shardedData = result[key];
287
- for (const collectionName in shardedData) {
288
- if (!shardedWrites[collectionName]) shardedWrites[collectionName] = {};
289
- Object.assign(shardedWrites[collectionName], shardedData[collectionName]);
290
- }
291
- } else {
292
- standardResult[key] = result[key];
293
- }
294
- }
295
- if (Object.keys(standardResult).length > 0) {
296
- const docRef = deps.db.collection(config.resultsCollection).doc(dStr)
297
- .collection(config.resultsSubcollection).doc(calc.manifest.category)
298
- .collection(config.computationsSubcollection).doc(name);
299
- standardWrites.push({ ref: docRef, data: standardResult });
300
- }
301
-
302
- // === CHANGED: Capture static schema ===
303
- const calcClass = calc.manifest.class;
304
- let staticSchema = null;
305
- if (calcClass && typeof calcClass.getSchema === 'function') {
306
- try {
307
- staticSchema = calcClass.getSchema();
308
- } catch (e) {
309
- logger.log('WARN', `[SchemaCapture] Failed to get static schema for ${name}`, { err: e.message });
310
- }
311
- } else {
312
- logger.log('TRACE', `[SchemaCapture] No static schema found for ${name}. Skipping manifest entry.`);
313
- }
314
-
315
- if (staticSchema) {
316
- schemasToStore.push({
317
- name,
318
- category: calc.manifest.category,
319
- schema: staticSchema, // <-- Use the static schema
320
- metadata: {
321
- isHistorical: calc.manifest.isHistorical || false,
322
- dependencies: calc.manifest.dependencies || [],
323
- rootDataDependencies: calc.manifest.rootDataDependencies || [],
324
- pass: calc.manifest.pass,
325
- type: calc.manifest.type || 'standard'
326
- }
327
- });
328
- }
329
- // === END CHANGED SECTION ===
330
-
331
- success++;
332
- }
333
- } catch (e) {
334
- logger.log('ERROR', `getResult failed ${name} for ${dStr}`, { err: e.message, stack: e.stack });
335
- failedCalcs.push(name);
336
- }
337
- }
338
-
339
- // === NEW: Store schemas asynchronously (don't block computation) ===
340
- if (schemasToStore.length > 0) {
341
- // This function is now imported from the simplified schema_capture.js
342
- batchStoreSchemas(deps, config, schemasToStore).catch(err => {
343
- logger.log('WARN', '[SchemaCapture] Non-blocking schema storage failed', {
344
- errorMessage: err.message
345
- });
346
- });
347
- }
348
-
349
- if (standardWrites.length > 0) {
350
- await commitBatchInChunks(config, deps, standardWrites, `${passName} Standard ${dStr}`);
351
- }
352
-
197
+ for (const name in state) { const calc = state[name];
198
+ if (!calc || typeof calc.getResult !== 'function') continue;
199
+ try { const result = await Promise.resolve(calc.getResult());
200
+ if (result && Object.keys(result).length > 0) {
201
+ const standardResult = {};
202
+ for (const key in result) {
203
+ if (key.startsWith('sharded_')) {
204
+ const shardedData = result[key];
205
+ for (const collectionName in shardedData) {
206
+ if (!shardedWrites[collectionName]) shardedWrites[collectionName] = {};
207
+ Object.assign(shardedWrites[collectionName], shardedData[collectionName]); }
208
+ } else { standardResult[key] = result[key]; }}
209
+ if (Object.keys(standardResult).length > 0) {
210
+ const docRef = deps.db.collection(config.resultsCollection).doc(dStr) .collection(config.resultsSubcollection).doc(calc.manifest.category) .collection(config.computationsSubcollection).doc(name);
211
+ standardWrites.push({ ref: docRef, data: standardResult });}
212
+ const calcClass = calc.manifest.class;
213
+ let staticSchema = null;
214
+ if (calcClass && typeof calcClass.getSchema === 'function') {
215
+ try { staticSchema = calcClass.getSchema(); } catch (e) { logger.log('WARN', `[SchemaCapture] Failed to get static schema for ${name}`, { err: e.message }); }
216
+ } else { logger.log('TRACE', `[SchemaCapture] No static schema found for ${name}. Skipping manifest entry.`); }
217
+ if (staticSchema) {
218
+ schemasToStore.push({ name, category: calc.manifest.category, schema: staticSchema, metadata: { isHistorical: calc.manifest.isHistorical || false, dependencies: calc.manifest.dependencies || [], rootDataDependencies: calc.manifest.rootDataDependencies || [], pass: calc.manifest.pass, type: calc.manifest.type || 'standard' } }); }
219
+ success++; } } catch (e) { logger.log('ERROR', `getResult failed ${name} for ${dStr}`, { err: e.message, stack: e.stack }); failedCalcs.push(name); } }
220
+ if (schemasToStore.length > 0) { batchStoreSchemas(deps, config, schemasToStore).catch(err => { logger.log('WARN', '[SchemaCapture] Non-blocking schema storage failed', { errorMessage: err.message }); });}
221
+ if (standardWrites.length > 0) { await commitBatchInChunks(config, deps, standardWrites, `${passName} Standard ${dStr}`); }
353
222
  for (const docPath in shardedWrites) {
354
- const docData = shardedWrites[docPath];
355
- const shardedDocWrites = [];
356
- let docRef;
357
- if (docPath.includes('/')) {
358
- docRef = deps.db.doc(docPath);
359
- } else {
360
- const collection = (docPath.startsWith('user_profile_history'))
361
- ? config.shardedUserProfileCollection
362
- : config.shardedProfitabilityCollection;
363
- docRef = deps.db.collection(collection).doc(docPath);
364
- }
365
- if (docData && typeof docData === 'object' && !Array.isArray(docData)) {
366
- shardedDocWrites.push({ ref: docRef, data: docData });
367
- } else {
368
- logger.log('ERROR', `[${passName}] Invalid sharded document data for ${docPath}. Not an object.`, { data: docData });
369
- }
370
- if (shardedDocWrites.length > 0) {
371
- await commitBatchInChunks(config, deps, shardedDocWrites, `${passName} Sharded ${docPath} ${dStr}`);
372
- }
373
- }
374
-
223
+ const docData = shardedWrites[docPath];
224
+ const shardedDocWrites = [];
225
+ let docRef;
226
+ if (docPath.includes('/')) { docRef = deps.db.doc(docPath); } else {
227
+ const collection = (docPath.startsWith('user_profile_history')) ? config.shardedUserProfileCollection : config.shardedProfitabilityCollection;
228
+ docRef = deps.db.collection(collection).doc(docPath); }
229
+ if (docData && typeof docData === 'object' && !Array.isArray(docData)) {shardedDocWrites.push({ ref: docRef, data: docData });
230
+ } else { logger.log('ERROR', `[${passName}] Invalid sharded document data for ${docPath}. Not an object.`, { data: docData }); }
231
+ if (shardedDocWrites.length > 0) { await commitBatchInChunks(config, deps, shardedDocWrites, `${passName} Sharded ${docPath} ${dStr}`); } }
375
232
  const logMetadata = {};
376
- if (failedCalcs.length > 0) {
377
- logMetadata.failedComputations = failedCalcs;
378
- }
379
- logger.log(
380
- success === calcs.length ? 'SUCCESS' : 'WARN',
381
- `[${passName}] Completed ${dStr}. Success: ${success}/${calcs.length}`,
382
- logMetadata
383
- );
233
+ if (failedCalcs.length > 0) { logMetadata.failedComputations = failedCalcs; }
234
+ logger.log(success === calcs.length ? 'SUCCESS' : 'WARN', `[${passName}] Completed ${dStr}. Success: ${success}/${calcs.length}`, logMetadata );
384
235
  }
385
236
 
386
237
  /**
@@ -388,116 +239,44 @@ async function runStandardComputationPass(date, calcs, passName, config, deps, r
388
239
  */
389
240
  async function runMetaComputationPass(date, calcs, passName, config, deps, fetchedDeps, rootData) {
390
241
  const dStr = date.toISOString().slice(0, 10), logger = deps.logger;
391
- if (calcs.length === 0) {
392
- logger.log('INFO', `[${passName}] No meta calcs to run for ${dStr} after filtering.`);
393
- return;
394
- }
242
+ if (calcs.length === 0) { logger.log('INFO', `[${passName}] No meta calcs to run for ${dStr} after filtering.`); return; }
395
243
  logger.log('INFO', `[${passName}] Running ${dStr} with ${calcs.length} calcs.`);
396
244
  const fullRoot = await loadHistoricalData(date, calcs, config, deps, rootData);
397
-
398
245
  let success = 0;
399
246
  const failedCalcs = [];
400
247
  const standardWrites = [];
401
248
  const shardedWrites = {};
402
-
403
- // === NEW: Collect schemas ===
404
249
  const schemasToStore = [];
405
-
406
250
  for (const mCalc of calcs) {
407
- const name = normalizeName(mCalc.name), Cl = mCalc.class;
408
- if (typeof Cl !== 'function') {
409
- logger.log('ERROR', `Invalid class ${name}`);
410
- failedCalcs.push(name);
411
- continue;
412
- }
413
- const inst = new Cl();
414
- try {
415
- const result = await Promise.resolve(inst.process(dStr, { ...deps, rootData: fullRoot }, config, fetchedDeps));
416
- if (result && Object.keys(result).length > 0) {
417
- const standardResult = {};
418
- for (const key in result) {
419
- if (key.startsWith('sharded_')) {
420
- const shardedData = result[key];
421
- for (const collectionName in shardedData) {
422
- if (!shardedWrites[collectionName]) shardedWrites[collectionName] = {};
423
- Object.assign(shardedWrites[collectionName], shardedData[collectionName]);
424
- }
425
- } else {
426
- standardResult[key] = result[key];
427
- }
428
- }
429
- if (Object.keys(standardResult).length > 0) {
430
- const docRef = deps.db.collection(config.resultsCollection).doc(dStr)
431
- .collection(config.resultsSubcollection).doc(mCalc.category)
432
- .collection(config.computationsSubcollection).doc(name);
433
- standardWrites.push({ ref: docRef, data: standardResult });
434
- }
435
-
436
- // === CHANGED: Capture static schema ===
437
- const calcClass = mCalc.class;
438
- let staticSchema = null;
439
- if (calcClass && typeof calcClass.getSchema === 'function') {
440
- try {
441
- staticSchema = calcClass.getSchema();
442
- } catch (e) {
443
- logger.log('WARN', `[SchemaCapture] Failed to get static schema for ${name}`, { err: e.message });
444
- }
445
- } else {
446
- logger.log('TRACE', `[SchemaCapture] No static schema found for ${name}. Skipping manifest entry.`);
447
- }
448
-
449
- if (staticSchema) {
450
- schemasToStore.push({
451
- name,
452
- category: mCalc.category,
453
- schema: staticSchema, // <-- Use the static schema
454
- metadata: {
455
- isHistorical: mCalc.isHistorical || false,
456
- dependencies: mCalc.dependencies || [],
457
- rootDataDependencies: mCalc.rootDataDependencies || [],
458
- pass: mCalc.pass,
459
- type: 'meta'
460
- }
461
- });
462
- }
463
- // === END CHANGED SECTION ===
464
-
465
- success++;
466
- }
467
- } catch (e) {
468
- logger.log('ERROR', `Meta-calc failed ${name} for ${dStr}`, { err: e.message, stack: e.stack });
469
- failedCalcs.push(name);
470
- }
471
- }
472
-
473
- // === NEW: Store schemas asynchronously ===
474
- if (schemasToStore.length > 0) {
475
- // This function is now imported from the simplified schema_capture.js
476
- batchStoreSchemas(deps, config, schemasToStore).catch(err => {
477
- logger.log('WARN', '[SchemaCapture] Non-blocking schema storage failed', {
478
- errorMessage: err.message
479
- });
480
- });
481
- }
482
-
483
- if (standardWrites.length > 0) {
484
- await commitBatchInChunks(config, deps, standardWrites, `${passName} Meta ${dStr}`);
485
- }
486
-
251
+ const name = normalizeName(mCalc.name), Cl = mCalc.class;
252
+ if (typeof Cl !== 'function') { logger.log('ERROR', `Invalid class ${name}`); failedCalcs.push(name); continue; }
253
+ const inst = new Cl();
254
+ try { const result = await Promise.resolve(inst.process(dStr, { ...deps, rootData: fullRoot }, config, fetchedDeps));
255
+ if (result && Object.keys(result).length > 0) {
256
+ const standardResult = {};
257
+ for (const key in result) {
258
+ if (key.startsWith('sharded_')) { const shardedData = result[key]; for (const collectionName in shardedData) {
259
+ if (!shardedWrites[collectionName]) shardedWrites[collectionName] = {}; Object.assign(shardedWrites[collectionName], shardedData[collectionName]); }
260
+ } else { standardResult[key] = result[key]; } }
261
+ if (Object.keys(standardResult).length > 0) {
262
+ const docRef = deps.db.collection(config.resultsCollection).doc(dStr) .collection(config.resultsSubcollection).doc(mCalc.category) .collection(config.computationsSubcollection).doc(name);
263
+ standardWrites.push({ ref: docRef, data: standardResult }); }
264
+ const calcClass = mCalc.class;
265
+ let staticSchema = null;
266
+ if (calcClass && typeof calcClass.getSchema === 'function') {
267
+ try { staticSchema = calcClass.getSchema();
268
+ } catch (e) { logger.log('WARN', `[SchemaCapture] Failed to get static schema for ${name}`, { err: e.message }); }
269
+ } else { logger.log('TRACE', `[SchemaCapture] No static schema found for ${name}. Skipping manifest entry.`); }
270
+ if (staticSchema) { schemasToStore.push({ name, category: mCalc.category, schema: staticSchema, metadata: { isHistorical: mCalc.isHistorical || false, dependencies: mCalc.dependencies || [], rootDataDependencies: mCalc.rootDataDependencies || [], pass: mCalc.pass, type: 'meta' } }); }
271
+ success++; }
272
+ } catch (e) { logger.log('ERROR', `Meta-calc failed ${name} for ${dStr}`, { err: e.message, stack: e.stack }); failedCalcs.push(name); } }
273
+ if (schemasToStore.length > 0) { batchStoreSchemas(deps, config, schemasToStore).catch(err => { logger.log('WARN', '[SchemaCapture] Non-blocking schema storage failed', { errorMessage: err.message }); }); }
274
+ if (standardWrites.length > 0) { await commitBatchInChunks(config, deps, standardWrites, `${passName} Meta ${dStr}`);}
487
275
  for (const collectionName in shardedWrites) {
488
- const docs = shardedWrites[collectionName];
489
- const shardedDocWrites = [];
490
- for (const docId in docs) {
491
- const docRef = docId.includes('/')
492
- ? deps.db.doc(docId)
493
- : deps.db.collection(collectionName).doc(docId);
494
- shardedDocWrites.push({ ref: docRef, data: docs[docId] });
495
- }
496
- if (shardedDocWrites.length > 0) {
497
- await commitBatchInChunks(config, deps, shardedDocWrites, `${passName} Sharded ${collectionName} ${dStr}`);
498
- }
499
- }
500
-
276
+ const docs = shardedWrites[collectionName];
277
+ const shardedDocWrites = [];
278
+ for (const docId in docs) { const docRef = docId.includes('/') ? deps.db.doc(docId) : deps.db.collection(collectionName).doc(docId); shardedDocWrites.push({ ref: docRef, data: docs[docId] }); }
279
+ if (shardedDocWrites.length > 0) { await commitBatchInChunks(config, deps, shardedDocWrites, `${passName} Sharded ${collectionName} ${dStr}`); } }
501
280
  const logMetadata = {};
502
281
  if (failedCalcs.length > 0) { logMetadata.failedComputations = failedCalcs; }
503
282
  logger.log( success === calcs.length ? 'SUCCESS' : 'WARN', `[${passName}] Completed ${dStr}. Success: ${success}/${calcs.length}`, logMetadata );