bulltrackers-module 1.0.171 → 1.0.173

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.
@@ -1,800 +1,268 @@
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
- * Stage 1: Group manifest by pass number
17
- * @param {Array} manifest - The full manifest array.
18
- * @returns {object} An object with pass numbers as keys and calc arrays as values.
2
+ * FIXED: orchestration_helpers.js
3
+ * V3.2: Enabled streaming of Trading History data (tH_iter) for computations
4
+ * that require it, alongside Portfolio data.
19
5
  */
20
- function groupByPass(manifest) { return manifest.reduce((acc, calc) => { (acc[calc.pass] = acc[calc.pass] || []).push(calc); return acc; }, {}); }
21
6
 
22
- /**
23
- * Stage 2: Check root data dependencies for a calc
24
- * @param {object} calcManifest - A single calculation's manifest entry.
25
- * @param {object} rootDataStatus - The status object from checkRootDataAvailability.
26
- * @returns {{canRun: boolean, missing: string[]}}
27
- */
7
+ const { ComputationController } = require('../controllers/computation_controller');
8
+ const {
9
+ getPortfolioPartRefs, loadDailyInsights, loadDailySocialPostInsights,
10
+ getHistoryPartRefs, streamPortfolioData, streamHistoryData
11
+ } = require('../utils/data_loader');
12
+ const { batchStoreSchemas } = require('../utils/schema_capture');
13
+ const { normalizeName, commitBatchInChunks } = require('../utils/utils');
14
+
15
+ // --- Helpers ---
16
+
17
+ function groupByPass(manifest) {
18
+ return manifest.reduce((acc, calc) => { (acc[calc.pass] = acc[calc.pass] || []).push(calc); return acc; }, {});
19
+ }
20
+
28
21
  function checkRootDependencies(calcManifest, rootDataStatus) {
29
22
  const missing = [];
30
- if (!calcManifest.rootDataDependencies || !calcManifest.rootDataDependencies.length) { return { canRun: true, missing }; }
23
+ if (!calcManifest.rootDataDependencies) return { canRun: true, missing };
31
24
  for (const dep of calcManifest.rootDataDependencies) {
32
- if (dep === 'portfolio' && !rootDataStatus.hasPortfolio) missing.push('portfolio');
33
- else if (dep === 'insights' && !rootDataStatus.hasInsights) missing.push('insights');
34
- else if (dep === 'social' && !rootDataStatus.hasSocial) missing.push('social');
35
- else if (dep === 'history' && !rootDataStatus.hasHistory) missing.push('history');
25
+ if (dep === 'portfolio' && !rootDataStatus.hasPortfolio) missing.push('portfolio');
26
+ else if (dep === 'insights' && !rootDataStatus.hasInsights) missing.push('insights');
27
+ else if (dep === 'social' && !rootDataStatus.hasSocial) missing.push('social');
28
+ else if (dep === 'history' && !rootDataStatus.hasHistory) missing.push('history');
36
29
  }
37
30
  return { canRun: missing.length === 0, missing };
38
31
  }
39
32
 
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
33
  async function checkRootDataAvailability(dateStr, config, dependencies, earliestDates) {
49
34
  const { logger } = dependencies;
50
- logger.log('INFO', `[PassRunner] Checking root data for ${dateStr}...`);
51
35
  const dateToProcess = new Date(dateStr + 'T00:00:00Z');
52
36
 
53
- let portfolioRefs = [], insightsData = null, socialData = null, historyRefs = [];
37
+ let portfolioRefs = [], historyRefs = [];
54
38
  let hasPortfolio = false, hasInsights = false, hasSocial = false, hasHistory = false;
39
+ let insightsData = null, socialData = null;
55
40
 
56
41
  try {
57
42
  const tasks = [];
58
- // Only query for data if the processing date is on or after the earliest possible date
59
- if (dateToProcess >= earliestDates.portfolio) {
60
- tasks.push(getPortfolioPartRefs(config, dependencies, dateStr).then(res => { portfolioRefs = res; hasPortfolio = !!(res?.length); }));
61
- }
62
- if (dateToProcess >= earliestDates.insights) {
63
- tasks.push(loadDailyInsights(config, dependencies, dateStr).then(res => { insightsData = res; hasInsights = !!res; }));
64
- }
65
- if (dateToProcess >= earliestDates.social) {
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
- }
43
+ if (dateToProcess >= earliestDates.portfolio)
44
+ tasks.push(getPortfolioPartRefs(config, dependencies, dateStr).then(r => { portfolioRefs = r; hasPortfolio = !!r.length; }));
45
+ if (dateToProcess >= earliestDates.insights)
46
+ tasks.push(loadDailyInsights(config, dependencies, dateStr).then(r => { insightsData = r; hasInsights = !!r; }));
47
+ if (dateToProcess >= earliestDates.social)
48
+ tasks.push(loadDailySocialPostInsights(config, dependencies, dateStr).then(r => { socialData = r; hasSocial = !!r; }));
49
+ if (dateToProcess >= earliestDates.history)
50
+ tasks.push(getHistoryPartRefs(config, dependencies, dateStr).then(r => { historyRefs = r; hasHistory = !!r.length; }));
71
51
 
72
52
  await Promise.all(tasks);
73
53
 
74
- logger.log('INFO', `[PassRunner] Data availability for ${dateStr}: P:${hasPortfolio}, I:${hasInsights}, S:${hasSocial}, H:${hasHistory}`);
54
+ if (!(hasPortfolio || hasInsights || hasSocial || hasHistory)) return null;
75
55
 
76
- if (!(hasPortfolio || hasInsights || hasSocial || hasHistory)) { logger.log('WARN', `[PassRunner] No root data at all for ${dateStr}.`); return null; }
77
-
78
- return {
79
- portfolioRefs,
80
- todayInsights: insightsData,
81
- todaySocialPostInsights: socialData,
82
- historyRefs,
83
- status: { hasPortfolio, hasInsights, hasSocial, hasHistory }
56
+ return {
57
+ portfolioRefs, historyRefs,
58
+ todayInsights: insightsData, todaySocialPostInsights: socialData,
59
+ status: { hasPortfolio, hasInsights, hasSocial, hasHistory }
84
60
  };
85
61
  } catch (err) {
86
- logger.log('ERROR', `[PassRunner] Error checking data for ${dateStr}`, { errorMessage: err.message });
62
+ logger.log('ERROR', `Error checking data: ${err.message}`);
87
63
  return null;
88
64
  }
89
65
  }
90
66
 
91
- /**
92
- * Stage 4: Fetch ALL existing computed results for the pass AND their dependencies.
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 }) {
67
+ // --- OPTIMIZED FETCH ---
68
+ async function fetchExistingResults(dateStr, calcsInPass, fullManifest, config, { db }, includeSelf = false) {
101
69
  const manifestMap = new Map(fullManifest.map(c => [normalizeName(c.name), c]));
102
70
  const calcsToFetch = new Set();
103
71
 
104
- // Add all calcs in this pass (to check for completion)
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
- }
72
+ for (const calc of calcsInPass) {
73
+ if (calc.dependencies) {
74
+ calc.dependencies.forEach(d => calcsToFetch.add(normalizeName(d)));
75
+ }
76
+ if (includeSelf && calc.isHistorical) {
77
+ calcsToFetch.add(normalizeName(calc.name));
112
78
  }
113
79
  }
114
80
 
115
- if (!calcsToFetch.size) { return {}; }
116
-
117
- logger.log('INFO', `[PassRunner] Checking for ${calcsToFetch.size} existing results and dependencies for ${dateStr}...`);
81
+ if (!calcsToFetch.size) return {};
118
82
 
83
+ const fetched = {};
119
84
  const docRefs = [];
120
- const depNames = [];
85
+ const names = [];
121
86
 
122
- for (const calcName of calcsToFetch) {
123
- const calcManifest = manifestMap.get(calcName);
124
- if (!calcManifest) { logger.log('ERROR', `[PassRunner] Missing manifest for dependency: ${calcName} on ${dateStr}`); continue; }
125
-
126
- docRefs.push(
127
- db.collection(config.resultsCollection)
128
- .doc(dateStr)
129
- .collection(config.resultsSubcollection)
130
- .doc(calcManifest.category || 'unknown')
131
- .collection(config.computationsSubcollection)
132
- .doc(calcName)
133
- );
134
- depNames.push(calcName);
87
+ for (const name of calcsToFetch) {
88
+ const m = manifestMap.get(name);
89
+ if (m) {
90
+ docRefs.push(db.collection(config.resultsCollection).doc(dateStr)
91
+ .collection(config.resultsSubcollection).doc(m.category || 'unknown')
92
+ .collection(config.computationsSubcollection).doc(name));
93
+ names.push(name);
94
+ }
135
95
  }
136
-
137
- const fetched = {};
96
+
138
97
  if (docRefs.length) {
139
- const snapshots = await db.getAll(...docRefs);
140
- snapshots.forEach((doc, i) => {
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
98
+ const snaps = await db.getAll(...docRefs);
99
+ snaps.forEach((doc, i) => {
100
+ if(doc.exists && doc.data()._completed) {
101
+ fetched[names[i]] = doc.data();
145
102
  }
146
103
  });
147
104
  }
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
105
  return fetched;
156
106
  }
157
107
 
158
- /**
159
- * Stage 5: Filter calculations based on data availability and completion status.
160
- */
161
- function filterCalculations(standardCalcs, metaCalcs, rootDataStatus, existingResults, passToRun, dateStr, earliestDates, logger) {
162
- const skipped = new Set();
163
- const dateToProcess = new Date(dateStr + 'T00:00:00Z');
164
-
165
- // Helper to find the true earliest date a calc can run
166
- const getTrueEarliestRunDate = (calc) => {
167
- let earliestRunDate = new Date('1970-01-01T00:00:00Z');
168
- const dependencies = calc.rootDataDependencies || [];
169
-
170
- for (const dep of dependencies) {
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
- }
108
+ function filterCalculations(standardCalcs, metaCalcs, rootDataStatus, existingResults, passToRun, dateStr, earliestDates) { // TODO passtorun is unused, why?
109
+ const filter = (c) => { // Since we are using toplogical sorting for deps, surely the pass being run is implicit
110
+ if (existingResults[c.name]) return false; // Am a bit confused here. Feel this may be a bug.
111
+ let earliest = new Date('1970-01-01');
112
+ (c.rootDataDependencies || []).forEach(d => { if(earliestDates[d] > earliest) earliest = earliestDates[d]; });
113
+ if (c.isHistorical) earliest.setUTCDate(earliest.getUTCDate() + 1);
114
+ if (new Date(dateStr) < earliest) return false;
115
+ if (!checkRootDependencies(c, rootDataStatus).canRun) return false;
116
+ if (c.type === 'meta' && c.dependencies && c.dependencies.some(d => !existingResults[normalizeName(d)])) return false;
214
117
  return true;
215
118
  };
216
-
217
- const standardCalcsToRun = standardCalcs.filter(filterCalc);
218
- const metaCalcsToRun = metaCalcs.filter(filterCalc);
219
-
220
- return { standardCalcsToRun, metaCalcsToRun };
119
+ return { standardCalcsToRun: standardCalcs.filter(filter), metaCalcsToRun: metaCalcs.filter(filter) };
221
120
  }
222
121
 
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
- }
122
+ // --- EXECUTION DELEGATES ---
248
123
 
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) {
124
+ async function streamAndProcess(dateStr, state, passName, config, deps, rootData, portfolioRefs, historyRefs, fetchedDeps, previousFetchedDeps) {
254
125
  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);
126
+ const controller = new ComputationController(config, deps);
268
127
 
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
- }
128
+ const calcs = Object.values(state).filter(c => c && c.manifest);
129
+ const streamingCalcs = calcs.filter(c =>
130
+ c.manifest.rootDataDependencies.includes('portfolio') ||
131
+ c.manifest.rootDataDependencies.includes('history')
132
+ );
300
133
 
301
- await Promise.all(tasks);
302
- return updated;
303
- }
134
+ if (streamingCalcs.length === 0) return;
304
135
 
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
- async function streamAndProcess(dateStr, state, passName, config, deps, rootData, portfolioRefs, historyRefs, fetchedDeps) {
311
- const { logger, calculationUtils } = deps;
312
- const { todayInsights, yesterdayInsights, todaySocialPostInsights, yesterdaySocialPostInsights, yesterdayDependencyData } = rootData;
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
- };
136
+ logger.log('INFO', `[${passName}] Streaming for ${streamingCalcs.length} computations...`);
324
137
 
325
- // --- Run non-streaming (meta) calcs once ---
326
- // This logic is 1:1 with the test harness
327
- let firstUser = true;
328
- for (const name in state) {
329
- const calc = state[name];
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
138
+ await controller.loader.loadMappings();
139
+ const prevDate = new Date(dateStr + 'T00:00:00Z'); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
140
+ const prevDateStr = prevDate.toISOString().slice(0, 10);
342
141
 
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
- }
142
+ // 1. Today's Portfolio Stream
143
+ const tP_iter = streamPortfolioData(config, deps, dateStr, portfolioRefs);
357
144
 
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
- )
366
- );
367
-
368
- if (calcsThatStream.length === 0) {
369
- logger.log('INFO', `[${passName}] No portfolio or history streaming calcs to run for ${dateStr}. Skipping stream.`);
370
- return;
145
+ // 2. Yesterday's Portfolio Stream (for 'isHistorical' calcs)
146
+ const needsYesterdayPortfolio = streamingCalcs.some(c => c.manifest.isHistorical);
147
+ const yP_iter = (needsYesterdayPortfolio && rootData.yesterdayPortfolioRefs)
148
+ ? streamPortfolioData(config, deps, prevDateStr, rootData.yesterdayPortfolioRefs)
149
+ : null;
150
+
151
+ // 3. Today's History Stream (NEW)
152
+ const needsTradingHistory = streamingCalcs.some(c => c.manifest.rootDataDependencies.includes('history'));
153
+ const tH_iter = (needsTradingHistory && historyRefs)
154
+ ? streamHistoryData(config, deps, dateStr, historyRefs)
155
+ : null;
156
+
157
+ let yP_chunk = {};
158
+ let tH_chunk = {};
159
+
160
+ for await (const tP_chunk of tP_iter) {
161
+ if (yP_iter) yP_chunk = (await yP_iter.next()).value || {};
162
+ if (tH_iter) tH_chunk = (await tH_iter.next()).value || {};
163
+
164
+ const promises = streamingCalcs.map(calc =>
165
+ controller.executor.executePerUser(
166
+ calc,
167
+ calc.manifest,
168
+ dateStr,
169
+ tP_chunk,
170
+ yP_chunk, // Yesterday Portfolio
171
+ tH_chunk, // Today History (NEW ARGUMENT)
172
+ fetchedDeps,
173
+ previousFetchedDeps
174
+ )
175
+ );
176
+ await Promise.all(promises);
371
177
  }
372
-
373
- // --- FIX 2: THE TYPO (From friend) ---
374
- // This log message now correctly references 'calcsThatStream'.
375
- logger.log('INFO', `[${passName}] Streaming portfolio & historical data for ${calcsThatStream.length} calcs for ${dateStr}...`);
376
-
377
- const prevDate = new Date(dateStr + 'T00:00:00Z');
378
- prevDate.setUTCDate(prevDate.getUTCDate() - 1);
379
- const prevDateStr = prevDate.toISOString().slice(0, 10);
380
-
381
- // Check which iterators we need
382
- const needsYesterdayPortfolio = Object.values(state).some(c => c && c.manifest.isHistorical && c.manifest.rootDataDependencies.includes('portfolio'));
383
- const needsTodayHistory = Object.values(state).some(c => c && c.manifest.rootDataDependencies.includes('history'));
384
- const needsYesterdayHistory = Object.values(state).some(c => c && c.manifest.isHistorical && c.manifest.rootDataDependencies.includes('history'));
385
-
386
- // --- Create all necessary iterators ---
387
- // (This code is now correct because rootData.yesterdayHistoryRefs will be populated by Fix 1)
388
- const tP_iterator = streamPortfolioData(config, deps, dateStr, portfolioRefs);
389
- const yP_iterator = needsYesterdayPortfolio ? streamPortfolioData(config, deps, prevDateStr, rootData.yesterdayPortfolioRefs) : null;
390
- const hT_iterator = needsTodayHistory ? streamHistoryData(config, deps, dateStr, historyRefs) : null;
391
- const hY_iterator = needsYesterdayHistory ? streamHistoryData(config, deps, prevDateStr, rootData.yesterdayHistoryRefs) : null;
392
-
393
- let yesterdayPortfolios = {};
394
- let todayHistoryData = {};
395
- let yesterdayHistoryData = {};
396
-
397
- // Pre-load the first chunk of historical data
398
- if (yP_iterator) { Object.assign(yesterdayPortfolios, (await yP_iterator.next()).value || {}); }
399
- if (hT_iterator) { Object.assign(todayHistoryData, (await hT_iterator.next()).value || {}); }
400
- if (hY_iterator) { Object.assign(yesterdayHistoryData, (await hY_iterator.next()).value || {}); }
401
-
402
- for await (const chunk of tP_iterator) {
403
- // Load the *next* chunk of historical data to stay in sync
404
- if (yP_iterator) { Object.assign(yesterdayPortfolios, (await yP_iterator.next()).value || {}); }
405
- if (hT_iterator) { Object.assign(todayHistoryData, (await hT_iterator.next()).value || {}); }
406
- if (hY_iterator) { Object.assign(yesterdayHistoryData, (await hY_iterator.next()).value || {}); }
407
-
408
- for (const uid in chunk) {
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}.`);
178
+ logger.log('INFO', `[${passName}] Streaming complete.`);
500
179
  }
501
180
 
181
+ // --- RUNNERS ---
502
182
 
503
- /**
504
- * Stage 9: Run standard computations
505
- * --- MODIFIED: Now accepts 'fetchedDeps' and passes it to 'streamAndProcess' ---
506
- */
507
- async function runStandardComputationPass(date, calcs, passName, config, deps, rootData, fetchedDeps) {
508
- const dStr = date.toISOString().slice(0, 10), logger = deps.logger;
509
- if (calcs.length === 0) {
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);
183
+ async function runStandardComputationPass(date, calcs, passName, config, deps, rootData, fetchedDeps, previousFetchedDeps) {
184
+ const dStr = date.toISOString().slice(0, 10);
185
+ const logger = deps.logger;
518
186
 
519
- // Initialize calcs
520
- const state = initializeCalculators(calcs, logger);
521
-
522
- // Stream T and T-1 data and process
523
- // --- THIS IS THE CHANGE ---
524
- await streamAndProcess(dStr, state, passName, config, deps, fullRoot, rootData.portfolioRefs, rootData.historyRefs, fetchedDeps);
525
- // --- END CHANGE ---
526
-
527
- // --- Verbose Logging Setup ---
528
- const successCalcs = [];
529
- const failedCalcs = [];
530
-
531
- const standardWrites = [];
532
- const shardedWrites = {};
533
- const schemasToStore = [];
534
-
535
- // --- Get Results ---
536
- for (const name in state) {
537
- const calc = state[name];
538
- if (!calc || typeof calc.getResult !== 'function') continue;
187
+ const fullRoot = { ...rootData };
188
+ if (calcs.some(c => c.isHistorical)) {
189
+ const prev = new Date(date); prev.setUTCDate(prev.getUTCDate() - 1);
190
+ const prevStr = prev.toISOString().slice(0, 10);
191
+ fullRoot.yesterdayPortfolioRefs = await getPortfolioPartRefs(config, deps, prevStr);
192
+ }
539
193
 
194
+ const state = {};
195
+ for (const c of calcs) {
540
196
  try {
541
- // --- THIS IS THE CHANGE ---
542
- // Pass 'fetchedDeps' to getResult, just like the test harness
543
- const result = await Promise.resolve(calc.getResult(fetchedDeps));
544
- // --- END CHANGE ---
545
-
546
- if (result && Object.keys(result).length > 0) {
547
- const standardResult = {};
548
-
549
- // --- Handle Sharded Writes ---
550
- for (const key in result) {
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 });
602
- }
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}`); }
197
+ const inst = new c.class();
198
+ inst.manifest = c;
199
+ state[normalizeName(c.name)] = inst;
200
+ } catch(e) { logger.log('WARN', `Failed to init ${c.name}`); }
624
201
  }
625
202
 
626
- // --- Final Verbose Log ---
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 );
203
+ await streamAndProcess(dStr, state, passName, config, deps, fullRoot, rootData.portfolioRefs, rootData.historyRefs, fetchedDeps, previousFetchedDeps);
204
+ await commitResults(state, dStr, passName, config, deps);
635
205
  }
636
206
 
637
- /**
638
- * Stage 10: Run meta computations
639
- * @param {Date} date - The date to run for.
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; }
650
-
651
- logger.log('INFO', `[${passName}] Running ${dStr} with ${calcs.length} meta calcs: [${calcs.map(c => c.name).join(', ')}]`);
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 = [];
207
+ async function runMetaComputationPass(date, calcs, passName, config, deps, fetchedDeps, previousFetchedDeps, rootData) {
208
+ const controller = new ComputationController(config, deps);
209
+ const dStr = date.toISOString().slice(0, 10);
210
+ const state = {};
663
211
 
664
212
  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
-
676
213
  try {
677
- if (typeof inst.process !== 'function') {
678
- logger.log('ERROR', `Meta-calc ${name} is missing a 'process' method.`);
679
- failedCalcs.push({ name, error: "Missing process method" });
680
- continue;
681
- }
682
-
683
- // Meta-calc `process` is different: it receives the date, full dependencies, config, and fetched dependencies.
684
- // It *also* gets the T-1 root data via the `dependencies.rootData` object.
685
-
686
- // --- REPLICATE 5-ARGUMENT "HACK" SIGNATURE (from test harness) ---
687
- // This is the "hack" fix for meta calcs from worker.js.
688
- const metaPayload = {
689
- social: rootData.todaySocialPostInsights,
690
- insights: rootData.todayInsights,
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
- };
214
+ const inst = new mCalc.class();
215
+ inst.manifest = mCalc;
216
+ await controller.executor.executeOncePerDay(inst, mCalc, dStr, fetchedDeps, previousFetchedDeps);
217
+ state[normalizeName(mCalc.name)] = inst;
218
+ } catch (e) { deps.logger.log('ERROR', `Meta calc failed ${mCalc.name}: ${e.message}`); }
219
+ }
696
220
 
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 ---
221
+ await commitResults(state, dStr, passName, config, deps);
222
+ }
705
223
 
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);
224
+ async function commitResults(stateObj, dStr, passName, config, deps) {
225
+ const writes = [], schemas = [], sharded = {};
226
+ for (const name in stateObj) {
227
+ const calc = stateObj[name];
228
+ try {
229
+ const result = await calc.getResult();
230
+ if (!result) continue;
231
+ const standardRes = {};
232
+ for (const key in result) {
233
+ if (key.startsWith('sharded_')) { // TODO - This should iedally become redundant, computations themselves should NEVER return an object so large it requires sharding...
234
+ const sData = result[key];
235
+ for (const c in sData) { sharded[c] = sharded[c] || {}; Object.assign(sharded[c], sData[c]); }
236
+ } else standardRes[key] = result[key];
759
237
  }
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
-
765
- // --- Commit Writes ---
766
- if (schemasToStore.length > 0) {
767
- batchStoreSchemas(deps, config, schemasToStore).catch(err => {
768
- logger.log('WARN', `[SchemaCapture] Non-blocking schema storage failed for ${dStr}`, { errorMessage: err.message });
769
- });
238
+ if (Object.keys(standardRes).length) {
239
+ standardRes._completed = true;
240
+ writes.push({
241
+ ref: deps.db.collection(config.resultsCollection).doc(dStr)
242
+ .collection(config.resultsSubcollection).doc(calc.manifest.category)
243
+ .collection(config.computationsSubcollection).doc(name),
244
+ data: standardRes
245
+ });
246
+ }
247
+ if (calc.manifest.class.getSchema) {
248
+ schemas.push({ name, category: calc.manifest.category, schema: calc.manifest.class.getSchema(), metadata: calc.manifest });
249
+ }
250
+ } catch (e) { deps.logger.log('ERROR', `Commit failed ${name}: ${e.message}`); }
770
251
  }
252
+
253
+ if (schemas.length) batchStoreSchemas(deps, config, schemas).catch(()=>{});
254
+ if (writes.length) await commitBatchInChunks(config, deps, writes, `${passName} Results`);
771
255
 
772
- if (standardWrites.length > 0) { await commitBatchInChunks(config, deps, standardWrites, `${passName} Meta ${dStr}`); }
773
-
774
- for (const collectionName in shardedWrites) {
775
- const docs = shardedWrites[collectionName];
776
- const shardedDocWrites = [];
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 });
256
+ for (const col in sharded) {
257
+ const sWrites = [];
258
+ for (const id in sharded[col]) {
259
+ const ref = id.includes('/') ? deps.db.doc(id) : deps.db.collection(col).doc(id);
260
+ sWrites.push({ ref, data: { ...sharded[col][id], _completed: true } });
782
261
  }
783
- if (shardedDocWrites.length > 0) { await commitBatchInChunks(config, deps, shardedDocWrites, `${passName} Sharded ${collectionName} ${dStr}`); }
262
+ if (sWrites.length) await commitBatchInChunks(config, deps, sWrites, `${passName} Sharded ${col}`);
784
263
  }
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
264
  }
796
265
 
797
-
798
266
  module.exports = {
799
267
  groupByPass,
800
268
  checkRootDataAvailability,