bulltrackers-module 1.0.222 → 1.0.224

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,13 +1,15 @@
1
1
  /**
2
- * @fileoverview Main Orchestrator. Coordinates the topological execution of calculations.
2
+ * @fileoverview Main Orchestrator. Coordinates the topological execution.
3
+ * UPDATED: Includes comprehensive Date Analysis logic.
3
4
  */
4
5
  const { normalizeName, getExpectedDateStrings } = require('./utils/utils');
5
- const { checkRootDataAvailability, getViableCalculations } = require('./data/AvailabilityChecker');
6
+ const { checkRootDataAvailability } = require('./data/AvailabilityChecker');
6
7
  const { fetchExistingResults } = require('./data/DependencyFetcher');
7
8
  const { fetchComputationStatus, updateComputationStatus } = require('./persistence/StatusRepository');
8
9
  const { runBatchPriceComputation } = require('./executors/PriceBatchExecutor');
9
10
  const { StandardExecutor } = require('./executors/StandardExecutor');
10
11
  const { MetaExecutor } = require('./executors/MetaExecutor');
12
+ const { generateProcessId, PROCESS_TYPES } = require('./logger/logger');
11
13
 
12
14
  const PARALLEL_BATCH_SIZE = 7;
13
15
 
@@ -18,139 +20,179 @@ function groupByPass(manifest) {
18
20
  }, {});
19
21
  }
20
22
 
21
- async function runComputationPass(config, dependencies, computationManifest) {
22
- const { logger } = dependencies;
23
- const passToRun = String(config.COMPUTATION_PASS_TO_RUN);
24
- if (!passToRun) return logger.log('ERROR', '[PassRunner] No pass defined. Aborting.');
25
-
26
- logger.log('INFO', `🚀 Starting PASS ${passToRun}...`);
23
+ /**
24
+ * Performs the logical analysis requested by the user.
25
+ * Determines exactly what can run, what is blocked, and why.
26
+ */
27
+ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus, manifestMap) {
28
+ const report = {
29
+ runnable: [],
30
+ blocked: [], // Missing Root Data
31
+ resolving: [], // Missing Dependency (but might be fixed by run)
32
+ reRuns: [] // Hash Mismatch
33
+ };
27
34
 
28
- const earliestDates = {
29
- portfolio: new Date('2025-09-25T00:00:00Z'),
30
- history: new Date('2025-11-05T00:00:00Z'),
31
- social: new Date('2025-10-30T00:00:00Z'),
32
- insights: new Date('2025-08-26T00:00:00Z'),
33
- price: new Date('2025-08-01T00:00:00Z')
35
+ // Helper: Is a dependency satisfied?
36
+ const isDepSatisfied = (depName, dailyStatus) => {
37
+ const norm = normalizeName(depName);
38
+ if (dailyStatus[norm]) return true; // It exists
39
+ return false;
34
40
  };
35
- earliestDates.absoluteEarliest = Object.values(earliestDates).reduce((a, b) => a < b ? a : b);
36
- const passes = groupByPass(computationManifest);
37
- const calcsInThisPass = passes[passToRun] || [];
38
41
 
39
- if (!calcsInThisPass.length) return logger.log('WARN', `[PassRunner] No calcs for Pass ${passToRun}. Exiting.`);
42
+ for (const calc of calcsInPass) {
43
+ const cName = normalizeName(calc.name);
44
+ const storedHash = dailyStatus[cName];
45
+ const currentHash = calc.hash;
40
46
 
41
- const passEarliestDate = earliestDates.absoluteEarliest;
42
- const endDateUTC = new Date(Date.UTC(new Date().getUTCFullYear(), new Date().getUTCMonth(), new Date().getUTCDate() - 1));
43
- const allExpectedDates = getExpectedDateStrings(passEarliestDate, endDateUTC);
44
-
45
- // Separate specialized batch calcs
46
- const priceBatchCalcs = calcsInThisPass.filter(c => c.type === 'meta' && c.rootDataDependencies?.includes('price'));
47
- const standardAndOtherMetaCalcs = calcsInThisPass.filter(c => !priceBatchCalcs.includes(c));
48
-
49
- if (priceBatchCalcs.length > 0) {
50
- try {
51
- await runBatchPriceComputation(config, dependencies, allExpectedDates, priceBatchCalcs);
52
- } catch (e) {
53
- logger.log('ERROR', 'Batch Price failed', e);
47
+ // 1. Root Data Check
48
+ const missingRoots = [];
49
+ if (calc.rootDataDependencies) {
50
+ for (const dep of calc.rootDataDependencies) {
51
+ if (dep === 'portfolio' && !rootDataStatus.hasPortfolio) missingRoots.push('portfolio');
52
+ if (dep === 'insights' && !rootDataStatus.hasInsights) missingRoots.push('insights');
53
+ if (dep === 'social' && !rootDataStatus.hasSocial) missingRoots.push('social');
54
+ if (dep === 'history' && !rootDataStatus.hasHistory) missingRoots.push('history');
55
+ if (dep === 'price' && !rootDataStatus.hasPrices) missingRoots.push('price');
56
+ }
54
57
  }
55
- }
56
58
 
57
- if (standardAndOtherMetaCalcs.length === 0) return;
59
+ if (missingRoots.length > 0) {
60
+ report.blocked.push({ name: cName, reason: `Missing Root Data: ${missingRoots.join(', ')}` });
61
+ continue;
62
+ }
63
+
64
+ // 2. Hash / Version Check
65
+ let isReRun = false;
66
+ if (storedHash && storedHash !== currentHash) {
67
+ report.reRuns.push({ name: cName, oldHash: storedHash, newHash: currentHash });
68
+ isReRun = true;
69
+ } else if (storedHash === true) {
70
+ // Legacy upgrade
71
+ report.reRuns.push({ name: cName, reason: 'Legacy Upgrade' });
72
+ isReRun = true;
73
+ } else if (storedHash && storedHash === currentHash) {
74
+ // Already done, and hashes match.
75
+ // Check if we need to run implies we ignore this?
76
+ // Usually we skip if done. But for "Analysis" log, we treat it as "Skipped/Done".
77
+ // If the user wants to FORCE run, that's different.
78
+ // Assuming standard flow: if done & hash match, we don't run.
79
+ // report.blocked.push({ name: cName, reason: 'Already up to date' });
80
+ // continue;
81
+ }
82
+
83
+ // 3. Dependency Check
84
+ const missingDeps = [];
85
+ if (calc.dependencies) {
86
+ for (const dep of calc.dependencies) {
87
+ // If it's a historical dependency (yesterday's data), we assume availability or check elsewhere
88
+ // If it's a current pass dependency:
89
+ if (!isDepSatisfied(dep, dailyStatus)) {
90
+ missingDeps.push(dep);
91
+ }
92
+ }
93
+ }
58
94
 
59
- for (let i = 0; i < allExpectedDates.length; i += PARALLEL_BATCH_SIZE) {
60
- const batch = allExpectedDates.slice(i, i + PARALLEL_BATCH_SIZE);
61
- await Promise.all(batch.map(dateStr =>
62
- runDateComputation(dateStr, passToRun, standardAndOtherMetaCalcs, config, dependencies, computationManifest)
63
- ));
95
+ if (missingDeps.length > 0) {
96
+ // It's missing a dependency. Is it fatal?
97
+ // Since we are inside a Pass, typically dependencies are from PREVIOUS passes.
98
+ // If it's from a previous pass and missing, it's blocked.
99
+ // But for the sake of the report:
100
+ report.resolving.push({ name: cName, missingDeps });
101
+ // In strict execution, this might be "runnable" if we assume the dep runs first in this batch,
102
+ // but usually dependencies are strictly lower passes.
103
+ // We'll mark it as resolving/blocked.
104
+ } else {
105
+ // Dependencies met.
106
+ if (!dailyStatus[cName] || isReRun) {
107
+ report.runnable.push(calc);
108
+ }
109
+ }
64
110
  }
111
+
112
+ return report;
65
113
  }
66
114
 
67
115
  async function runDateComputation(dateStr, passToRun, calcsInThisPass, config, dependencies, computationManifest) {
68
116
  const { logger } = dependencies;
117
+ const orchestratorPid = generateProcessId(PROCESS_TYPES.ORCHESTRATOR, passToRun, dateStr);
118
+
69
119
  const dateToProcess = new Date(dateStr + 'T00:00:00Z');
70
120
 
71
- // 1. Version Check: Determine which calculations are *stale*
72
- const dailyStatus = await fetchComputationStatus(dateStr, config, dependencies);
73
- const calcsToAttempt = [];
121
+ // 1. Fetch State
122
+ const dailyStatus = await fetchComputationStatus(dateStr, config, dependencies);
74
123
 
75
- for (const calc of calcsInThisPass) {
76
- const cName = normalizeName(calc.name);
77
- const storedStatus = dailyStatus[cName];
78
- const currentHash = calc.hash;
79
-
80
- if (!storedStatus) {
81
- // New calculation
82
- calcsToAttempt.push(calc); continue;
83
- }
84
- if (typeof storedStatus === 'string' && currentHash && storedStatus !== currentHash) {
85
- // Code changed, must re-run
86
- logger.log('INFO', `[Versioning] ${cName}: Code Changed.`);
87
- calcsToAttempt.push(calc); continue;
88
- }
89
- if (storedStatus === true && currentHash) {
90
- // Migrating legacy status
91
- logger.log('INFO', `[Versioning] ${cName}: Upgrading legacy status.`);
92
- calcsToAttempt.push(calc); continue;
93
- }
94
- }
95
-
96
- if (!calcsToAttempt.length) return null;
97
-
98
- // 2. Data Availability Check
124
+ // 2. Check Data Availability (One shot)
99
125
  const earliestDates = {
100
126
  portfolio: new Date('2025-09-25T00:00:00Z'),
101
- history: new Date('2025-11-05T00:00:00Z'),
127
+ history: new Date('2025-11-05T00:00:00Z'), // Configurable in prod
102
128
  social: new Date('2025-10-30T00:00:00Z'),
103
129
  insights: new Date('2025-08-26T00:00:00Z'),
104
130
  price: new Date('2025-08-01T00:00:00Z')
105
131
  };
106
-
132
+
107
133
  const rootData = await checkRootDataAvailability(dateStr, config, dependencies, earliestDates);
108
- if (!rootData) { logger.log('INFO', `[DateRunner] Root data missing for ${dateStr}. Skipping.`); return null; }
134
+ const rootStatus = rootData ? rootData.status : { hasPortfolio: false, hasPrices: false, hasInsights: false, hasSocial: false, hasHistory: false };
135
+
136
+ // 3. PERFORM DATE ANALYSIS (Log the report)
137
+ const manifestMap = new Map(computationManifest.map(c => [normalizeName(c.name), c]));
138
+ const analysisReport = analyzeDateExecution(dateStr, calcsInThisPass, rootStatus, dailyStatus, manifestMap);
109
139
 
110
- // 3. Viability Check (Smart Execution Map)
111
- // Filter candidates: Remove any calculation that misses Root Data OR Dependencies
112
- const runnableCalcs = getViableCalculations(calcsToAttempt, rootData.status, dailyStatus);
140
+ // LOG THE REPORT
141
+ logger.logDateAnalysis(dateStr, analysisReport);
142
+
143
+ // 4. Filter Run List based on Analysis
144
+ // We combine 'runnable' and 'reRuns'. 'resolving' are skipped (unless we support intra-pass resolution).
145
+ // Note: analyzeDateExecution returns plain objects or calc objects. We need the calc objects.
146
+ const calcsToRunNames = new Set([
147
+ ...analysisReport.runnable.map(c => c.name),
148
+ ...analysisReport.reRuns.map(c => c.name) // Re-runs are valid to run
149
+ ]);
113
150
 
114
- if (!runnableCalcs.length) {
115
- // If we had candidates but they were pruned, it means they are blocked.
116
- // logger.log('INFO', `[DateRunner] ${dateStr}: Candidates pruned due to missing deps/data.`);
151
+ const finalRunList = calcsInThisPass.filter(c => calcsToRunNames.has(normalizeName(c.name)));
152
+
153
+ if (!finalRunList.length) {
117
154
  return null;
118
155
  }
119
156
 
120
- const standardToRun = runnableCalcs.filter(c => c.type === 'standard');
121
- const metaToRun = runnableCalcs.filter(c => c.type === 'meta');
122
- logger.log('INFO', `[DateRunner] Running ${dateStr}: ${standardToRun.length} std, ${metaToRun.length} meta`);
157
+ logger.log('INFO', `[Orchestrator] Executing ${finalRunList.length} calculations for ${dateStr}`, { processId: orchestratorPid });
158
+
159
+ // 5. Execution
160
+ const standardToRun = finalRunList.filter(c => c.type === 'standard');
161
+ const metaToRun = finalRunList.filter(c => c.type === 'meta');
123
162
 
124
163
  const dateUpdates = {};
125
164
 
126
165
  try {
127
166
  const calcsRunning = [...standardToRun, ...metaToRun];
128
- // Fetch dependencies for the *runnable* calculations
167
+
168
+ // Fetch dependencies (Previous pass results)
129
169
  const existingResults = await fetchExistingResults(dateStr, calcsRunning, computationManifest, config, dependencies, false);
170
+
171
+ // Fetch Yesterday's results (if Historical)
130
172
  const prevDate = new Date(dateToProcess); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
131
173
  const prevDateStr = prevDate.toISOString().slice(0, 10);
132
174
  const previousResults = await fetchExistingResults(prevDateStr, calcsRunning, computationManifest, config, dependencies, true);
133
175
 
134
176
  if (standardToRun.length) {
135
- const updates = await StandardExecutor.run(dateToProcess, standardToRun, `Pass ${passToRun} (Std)`, config, dependencies, rootData, existingResults, previousResults, false);
177
+ const updates = await StandardExecutor.run(dateToProcess, standardToRun, `Pass ${passToRun}`, config, dependencies, rootData, existingResults, previousResults, false);
136
178
  Object.assign(dateUpdates, updates);
137
179
  }
138
180
  if (metaToRun.length) {
139
- const updates = await MetaExecutor.run(dateToProcess, metaToRun, `Pass ${passToRun} (Meta)`, config, dependencies, existingResults, previousResults, rootData, false);
181
+ const updates = await MetaExecutor.run(dateToProcess, metaToRun, `Pass ${passToRun}`, config, dependencies, existingResults, previousResults, rootData, false);
140
182
  Object.assign(dateUpdates, updates);
141
183
  }
142
184
 
143
185
  } catch (err) {
144
- logger.log('ERROR', `[DateRunner] FAILED Pass ${passToRun} for ${dateStr}`, { errorMessage: err.message });
145
- [...standardToRun, ...metaToRun].forEach(c => dateUpdates[normalizeName(c.name)] = false);
186
+ logger.log('ERROR', `[Orchestrator] Failed execution for ${dateStr}`, { processId: orchestratorPid, error: err.message });
146
187
  throw err;
147
188
  }
148
189
 
190
+ // 6. Status Update happens inside Executors, but we can log final success here
149
191
  if (Object.keys(dateUpdates).length > 0) {
150
- await updateComputationStatus(dateStr, dateUpdates, config, dependencies);
192
+ // await updateComputationStatus... (Handled by Executors currently)
151
193
  }
152
194
 
153
195
  return { date: dateStr, updates: dateUpdates };
154
196
  }
155
197
 
156
- module.exports = { runComputationPass, runDateComputation, groupByPass };
198
+ module.exports = { runDateComputation, groupByPass };
@@ -50,18 +50,17 @@ function buildDynamicTriggers() {
50
50
  }
51
51
 
52
52
  const LAYER_HASHES = {};
53
- for (const [name, exports] of Object.entries(LAYER_GROUPS)) {
54
- LAYER_HASHES[name] = generateLayerHashes(exports, name);
55
- }
53
+ for (const [name, exports] of Object.entries(LAYER_GROUPS)) { LAYER_HASHES[name] = generateLayerHashes(exports, name); }
54
+
56
55
  const LAYER_TRIGGERS = buildDynamicTriggers();
57
56
 
58
57
  const log = {
59
- info: (msg) => console.log('ℹ︎ ' + msg),
60
- step: (msg) => console.log('› ' + msg),
61
- warn: (msg) => console.warn('⚠︎ ' + msg),
58
+ info: (msg) => console.log('ℹ︎ ' + msg),
59
+ step: (msg) => console.log('› ' + msg),
60
+ warn: (msg) => console.warn('⚠︎ ' + msg),
62
61
  success: (msg) => console.log('✔︎ ' + msg),
63
- error: (msg) => console.error('✖ ' + msg),
64
- fatal: (msg) => { console.error('✖ FATAL ✖ ' + msg); console.error('✖ FATAL ✖ Manifest build FAILED.'); },
62
+ error: (msg) => console.error('✖ ' + msg),
63
+ fatal: (msg) => { console.error('✖ FATAL ✖ ' + msg); console.error('✖ FATAL ✖ Manifest build FAILED.'); },
65
64
  divider: (label) => { const line = ''.padEnd(60, '─'); console.log(`\n${line}\n${label}\n${line}\n`); },
66
65
  };
67
66
 
@@ -22,7 +22,6 @@ const LEGACY_MAPPING = {
22
22
  priceExtractor: 'priceExtractor',
23
23
  InsightsExtractor: 'insights',
24
24
  UserClassifier: 'classifier',
25
- // Added based on your profiling.js file:
26
25
  Psychometrics: 'psychometrics',
27
26
  CognitiveBiases: 'bias',
28
27
  SkillAttribution: 'skill',
@@ -47,7 +46,6 @@ class ContextBuilder {
47
46
  for (const [key, value] of Object.entries(mathLayer)) { mathContext[key] = value; const legacyKey = LEGACY_MAPPING[key]; if (legacyKey) { mathContext[legacyKey] = value; } }
48
47
  return mathContext;
49
48
  }
50
- // ... (rest of class remains identical)
51
49
  static buildPerUserContext(options) {
52
50
  const { todayPortfolio, yesterdayPortfolio, todayHistory, yesterdayHistory, userId, userType, dateStr, metadata, mappings, insights, socialData, computedDependencies, previousComputedDependencies, config, deps } = options;
53
51
  return {
@@ -80,7 +78,6 @@ class ContextBuilder {
80
78
  }
81
79
 
82
80
  class ComputationExecutor {
83
- // ... (remains identical to previous version)
84
81
  constructor(config, dependencies, dataLoader) {
85
82
  this.config = config;
86
83
  this.deps = dependencies;
@@ -146,5 +143,4 @@ class ComputationController {
146
143
  }
147
144
  }
148
145
 
149
- // EXPORT LEGACY_MAPPING SO BUILDER CAN USE IT
150
146
  module.exports = { ComputationController, LEGACY_MAPPING };
@@ -26,40 +26,44 @@ function checkRootDependencies(calcManifest, rootDataStatus) {
26
26
  * Filters candidates to only those that are strictly "viable" to run.
27
27
  * A calculation is Viable if:
28
28
  * 1. All required Root Data is present.
29
- * 2. All required Dependencies (from previous passes) are present in dailyStatus.
29
+ * 2. All required Dependencies are present AND their stored hash matches their current code hash.
30
30
  * * @param {Array} candidates - Calculations attempting to run in this pass.
31
+ * @param {Array} fullManifest - The complete manifest (to lookup dependency current hashes).
31
32
  * @param {Object} rootDataStatus - { hasPortfolio: bool, hasPrices: bool... }
32
33
  * @param {Object} dailyStatus - Map of { "calc-name": "hash" } for completed items.
33
34
  */
34
- function getViableCalculations(candidates, rootDataStatus, dailyStatus) {
35
+ function getViableCalculations(candidates, fullManifest, rootDataStatus, dailyStatus) {
35
36
  const viable = [];
37
+ const manifestMap = new Map(fullManifest.map(c => [normalizeName(c.name), c]));
36
38
 
37
39
  for (const calc of candidates) {
38
40
  // 1. Check Root Data
39
41
  const rootCheck = checkRootDependencies(calc, rootDataStatus);
40
42
  if (!rootCheck.canRun) {
41
- // Root data missing -> Impossible to run.
42
- continue;
43
+ continue; // Root data missing -> Impossible.
43
44
  }
44
45
 
45
- // 2. Check Dependencies
46
+ // 2. Check Dependencies (Strict Hash Verification)
46
47
  let dependenciesMet = true;
47
48
  if (calc.dependencies && calc.dependencies.length > 0) {
48
49
  for (const depName of calc.dependencies) {
49
50
  const normDep = normalizeName(depName);
51
+ const storedHash = dailyStatus[normDep];
52
+ const depManifest = manifestMap.get(normDep);
50
53
 
51
- // If a dependency is missing from dailyStatus, it failed in a previous pass.
52
- // Therefore, the current calculation is impossible.
53
- if (!dailyStatus[normDep]) {
54
- dependenciesMet = false;
55
- break;
56
- }
54
+ // If dependency is missing from manifest, we can't verify it (shouldn't happen)
55
+ if (!depManifest) { dependenciesMet = false; break; }
56
+
57
+ // CHECK: Does the dependency exist in DB?
58
+ if (!storedHash) { dependenciesMet = false; break; }
59
+
60
+ // CHECK: Does the stored hash match the current code hash?
61
+ // This prevents running on stale data if a dependency failed to update.
62
+ if (storedHash !== depManifest.hash) { dependenciesMet = false; break; }
57
63
  }
58
64
  }
59
65
 
60
- if (dependenciesMet) {
61
- viable.push(calc);
62
- }
66
+ if (dependenciesMet) { viable.push(calc); }
63
67
  }
64
68
 
65
69
  return viable;
@@ -75,7 +79,6 @@ async function checkRootDataAvailability(dateStr, config, dependencies, earliest
75
79
 
76
80
  try {
77
81
  const tasks = [];
78
- // Only check data sources if the date is after the earliest known data point
79
82
  if (dateToProcess >= earliestDates.portfolio) {
80
83
  tasks.push(getPortfolioPartRefs(config, dependencies, dateStr).then(r => { portfolioRefs = r; hasPortfolio = !!r.length; }));
81
84
  }
@@ -94,7 +97,6 @@ async function checkRootDataAvailability(dateStr, config, dependencies, earliest
94
97
 
95
98
  await Promise.all(tasks);
96
99
 
97
- // If ABSOLUTELY NO data exists, we can return null early
98
100
  if (!(hasPortfolio || hasInsights || hasSocial || hasHistory || hasPrices)) return null;
99
101
 
100
102
  return {
@@ -103,7 +105,7 @@ async function checkRootDataAvailability(dateStr, config, dependencies, earliest
103
105
  todayInsights: insightsData,
104
106
  todaySocialPostInsights: socialData,
105
107
  status: { hasPortfolio, hasInsights, hasSocial, hasHistory, hasPrices },
106
- yesterdayPortfolioRefs: null // Filled later by StandardExecutor if needed
108
+ yesterdayPortfolioRefs: null
107
109
  };
108
110
  } catch (err) {
109
111
  logger.log('ERROR', `Error checking data: ${err.message}`);
@@ -111,10 +113,6 @@ async function checkRootDataAvailability(dateStr, config, dependencies, earliest
111
113
  }
112
114
  }
113
115
 
114
- /**
115
- * Checks if any price data exists in the collection.
116
- * Note: Uses a lightweight limit(1) query.
117
- */
118
116
  async function checkPriceAvailability(config, db) {
119
117
  try {
120
118
  const collection = config.priceCollection || 'asset_prices';
@@ -1,25 +1,57 @@
1
1
  /**
2
- * FILENAME: bulltrackers-module/functions/computation-system/helpers/computation_worker.js
2
+ * FILENAME: computation-system/helpers/computation_worker.js
3
3
  * PURPOSE: Consumes computation tasks from Pub/Sub and executes them.
4
- * REFACTORED: Now imports logic from the new WorkflowOrchestrator.
4
+ * UPDATED: Correctly points to layers/index.js and wraps it for the ManifestBuilder.
5
5
  */
6
6
 
7
7
  const { runDateComputation, groupByPass } = require('../WorkflowOrchestrator.js');
8
+ const { getManifest } = require('../topology/ManifestLoader');
9
+
10
+ // 1. IMPORT CALCULATIONS
11
+ // User confirmation: Calculations are barrel-loaded into layers/index.js
12
+ const rawLayers = require('../layers/index');
13
+
14
+ // 2. PREPARE FOR MANIFEST
15
+ // The ManifestBuilder expects a structure like: { packageName: { CalculationClass, ... } }
16
+ // Since layers/index.js returns a flat object, we wrap it in a 'core' group here.
17
+ const calculations = {
18
+ core: rawLayers
19
+ };
8
20
 
9
21
  /**
10
22
  * Handles a single Pub/Sub message for a computation task.
11
23
  * Supports both Gen 1 (Message) and Gen 2 (CloudEvent) formats.
24
+ * * @param {object} message - The Pub/Sub message payload.
25
+ * @param {object} config - System configuration (must include activeProductLines).
26
+ * @param {object} dependencies - System dependencies (logger, db, etc.).
12
27
  */
13
- async function handleComputationTask(message, config, dependencies, computationManifest) {
28
+ async function handleComputationTask(message, config, dependencies) {
14
29
  const { logger } = dependencies;
15
30
 
31
+ // 3. LAZY LOAD MANIFEST
32
+ // This ensures we only build the manifest when we actually receive a task.
33
+ // We pass our wrapped 'calculations' object here.
34
+ let computationManifest;
35
+ try {
36
+ computationManifest = getManifest(
37
+ config.activeProductLines || [],
38
+ calculations,
39
+ dependencies
40
+ );
41
+ } catch (manifestError) {
42
+ logger.log('FATAL', `[Worker] Failed to load Manifest: ${manifestError.message}`);
43
+ return;
44
+ }
45
+
46
+ // 4. PARSE PUB/SUB MESSAGE
16
47
  let data;
17
48
  try {
18
- // 1. Handle Cloud Functions Gen 2 (CloudEvent) -> Gen 1 -> Direct JSON -> Message
19
49
  if (message.data && message.data.message && message.data.message.data) {
20
- const buffer = Buffer.from(message.data.message.data, 'base64'); data = JSON.parse(buffer.toString());
50
+ const buffer = Buffer.from(message.data.message.data, 'base64');
51
+ data = JSON.parse(buffer.toString());
21
52
  } else if (message.data && typeof message.data === 'string') {
22
- const buffer = Buffer.from(message.data, 'base64'); data = JSON.parse(buffer.toString());
53
+ const buffer = Buffer.from(message.data, 'base64');
54
+ data = JSON.parse(buffer.toString());
23
55
  } else if (message.json) {
24
56
  data = message.json;
25
57
  } else {
@@ -30,8 +62,8 @@ async function handleComputationTask(message, config, dependencies, computationM
30
62
  return;
31
63
  }
32
64
 
65
+ // 5. EXECUTE TASK
33
66
  try {
34
- // Validate Action
35
67
  if (!data || data.action !== 'RUN_COMPUTATION_DATE') {
36
68
  if (data) logger.log('WARN', `[Worker] Unknown or missing action: ${data?.action}. Ignoring.`);
37
69
  return;
@@ -53,17 +85,24 @@ async function handleComputationTask(message, config, dependencies, computationM
53
85
  return;
54
86
  }
55
87
 
56
- const result = await runDateComputation(date, pass, calcsInThisPass, config, dependencies, computationManifest);
88
+ const result = await runDateComputation(
89
+ date,
90
+ pass,
91
+ calcsInThisPass,
92
+ config,
93
+ dependencies,
94
+ computationManifest
95
+ );
57
96
 
58
- if (result) {
59
- logger.log('INFO', `[Worker] Successfully processed ${date} (Pass ${pass}). Updates: ${Object.keys(result.updates || {}).length}`);
97
+ if (result && result.updates && Object.keys(result.updates).length > 0) {
98
+ logger.log('INFO', `[Worker] Successfully processed ${date} (Pass ${pass}). Updates: ${Object.keys(result.updates).length}`);
60
99
  } else {
61
- logger.log('INFO', `[Worker] Processed ${date} (Pass ${pass}) - Skipped (Dependencies missing or already done).`);
100
+ logger.log('INFO', `[Worker] Processed ${date} (Pass ${pass}) - No updates.`);
62
101
  }
63
102
 
64
103
  } catch (err) {
65
104
  logger.log('ERROR', `[Worker] Fatal error processing task: ${err.message}`, { stack: err.stack });
66
- throw err;
105
+ throw err;
67
106
  }
68
107
  }
69
108
 
@@ -0,0 +1,454 @@
1
+ /**
2
+ * @fileoverview Structured Logging System for Computation Engine
3
+ * Provides comprehensive tracking with process IDs, context, and filtering capabilities.
4
+ * UPDATED: Includes Pre-flight Date Analysis and Storage Observability.
5
+ */
6
+
7
+ const crypto = require('crypto');
8
+
9
+ /**
10
+ * Log Levels (Ordered by Severity)
11
+ */
12
+ const LOG_LEVELS = {
13
+ TRACE: 0,
14
+ DEBUG: 1,
15
+ INFO: 2,
16
+ WARN: 3,
17
+ ERROR: 4,
18
+ FATAL: 5
19
+ };
20
+
21
+ /**
22
+ * Process Types for Tracking
23
+ */
24
+ const PROCESS_TYPES = {
25
+ MANIFEST: 'manifest',
26
+ ORCHESTRATOR: 'orchestrator',
27
+ EXECUTOR: 'executor',
28
+ STORAGE: 'storage',
29
+ WORKER: 'worker',
30
+ ANALYSIS: 'analysis',
31
+ // Legacy / Specific aliases
32
+ SCHEMA_GENERATION: 'schema_generation',
33
+ COMPUTATION_EXECUTION: 'computation_execution',
34
+ DEPENDENCY_FETCH: 'dependency_fetch',
35
+ DATA_AVAILABILITY: 'data_availability',
36
+ DISPATCH: 'dispatch'
37
+ };
38
+
39
+ /**
40
+ * Generates a deterministic process ID from inputs.
41
+ * Ensures that logs for the same computation/date always have the same Trace ID.
42
+ * @param {string} type - Process type (e.g., 'orchestrator')
43
+ * @param {string} identifier - Unique key (e.g., pass name, calculation name)
44
+ * @param {string} date - Date string YYYY-MM-DD (optional)
45
+ * @returns {string} 16-character hexadecimal process ID
46
+ */
47
+ function generateProcessId(type, identifier, date = '') {
48
+ const input = `${type}|${identifier}|${date}`;
49
+ return crypto.createHash('sha256').update(input).digest('hex').substring(0, 16);
50
+ }
51
+
52
+ /**
53
+ * Formats a log entry into structured JSON
54
+ */
55
+ function formatLogEntry(entry) {
56
+ return JSON.stringify({
57
+ timestamp: entry.timestamp,
58
+ level: entry.level,
59
+ processType: entry.processType,
60
+ processId: entry.processId,
61
+ computationName: entry.computationName,
62
+ date: entry.date,
63
+ message: entry.message,
64
+ context: entry.context,
65
+ metadata: entry.metadata,
66
+ // Specific fields for specialized logs
67
+ stats: entry.stats,
68
+ storage: entry.storage,
69
+ details: entry.details
70
+ });
71
+ }
72
+
73
+ /**
74
+ * Main Structured Logger Class
75
+ */
76
+ class StructuredLogger {
77
+ constructor(config = {}) {
78
+ this.config = {
79
+ minLevel: config.minLevel || LOG_LEVELS.INFO,
80
+ enableConsole: config.enableConsole !== false,
81
+ enableStructured: config.enableStructured !== false,
82
+ includeStackTrace: config.includeStackTrace !== false,
83
+ ...config
84
+ };
85
+
86
+ this.activeProcesses = new Map();
87
+ }
88
+
89
+ /**
90
+ * Starts a tracked process and returns a ProcessLogger
91
+ * @param {string} processType - Type from PROCESS_TYPES
92
+ * @param {string} computationName - Optional computation name
93
+ * @param {string} date - Optional date string
94
+ * @returns {ProcessLogger}
95
+ */
96
+ startProcess(processType, computationName = null, date = null) {
97
+ // Use deterministic ID if components are present, else random/time-based fallback
98
+ const processId = (computationName || date)
99
+ ? generateProcessId(processType, computationName || 'general', date)
100
+ : crypto.randomBytes(8).toString('hex');
101
+
102
+ const processLogger = new ProcessLogger(this, processType, processId, computationName, date);
103
+
104
+ this.activeProcesses.set(processId, {
105
+ type: processType,
106
+ computationName,
107
+ date,
108
+ startTime: Date.now(),
109
+ logger: processLogger
110
+ });
111
+
112
+ return processLogger;
113
+ }
114
+
115
+ /**
116
+ * Ends a tracked process
117
+ */
118
+ endProcess(processId) {
119
+ const process = this.activeProcesses.get(processId);
120
+ if (process) {
121
+ const duration = Date.now() - process.startTime;
122
+ this.activeProcesses.delete(processId);
123
+ return duration;
124
+ }
125
+ return null;
126
+ }
127
+
128
+ /**
129
+ * The Main Date Analysis Logger (NEW)
130
+ * Aggregates the status of all calculations for a specific date into one readable report.
131
+ */
132
+ logDateAnalysis(dateStr, analysisReport) {
133
+ const { runnable, blocked, reRuns, resolving } = analysisReport;
134
+
135
+ // 1. Structured Output (Machine Readable)
136
+ if (this.config.enableStructured) {
137
+ console.log(JSON.stringify({
138
+ timestamp: new Date().toISOString(),
139
+ level: 'INFO',
140
+ processType: PROCESS_TYPES.ANALYSIS,
141
+ date: dateStr,
142
+ message: `Date Analysis for ${dateStr}`,
143
+ stats: {
144
+ runnable: runnable.length,
145
+ blocked: blocked.length,
146
+ reRuns: reRuns.length,
147
+ resolving: resolving.length
148
+ },
149
+ details: analysisReport
150
+ }));
151
+ }
152
+
153
+ // 2. Human Readable Output (Console)
154
+ if (this.config.enableConsole) {
155
+ const symbols = { info: 'ℹ️', warn: '⚠️', check: '✅', block: '⛔', link: '🔗', cycle: '🔄' };
156
+
157
+ console.log(`\n🔍 === DATE ANALYSIS REPORT: ${dateStr} ===`);
158
+
159
+ if (reRuns.length) {
160
+ console.log(`\n${symbols.cycle} [HASH MISMATCH / RE-RUNS]`);
161
+ reRuns.forEach(item => {
162
+ console.log(` • ${item.name}: Hash changed (Old: ${item.oldHash?.substring(0,6)}... New: ${item.newHash?.substring(0,6)}...). Cascade: ${item.cascade?.length || 0} dependents.`);
163
+ });
164
+ }
165
+
166
+ if (resolving.length) {
167
+ console.log(`\n${symbols.link} [DEPENDENCY RESOLUTION] (Will run after deps)`);
168
+ resolving.forEach(item => {
169
+ console.log(` • ${item.name}: Waiting for [${item.missingDeps.join(', ')}]`);
170
+ });
171
+ }
172
+
173
+ if (runnable.length) {
174
+ console.log(`\n${symbols.check} [READY TO RUN]`);
175
+ runnable.forEach(item => console.log(` • ${item.name}`));
176
+ }
177
+
178
+ if (blocked.length) {
179
+ console.log(`\n${symbols.block} [BLOCKED / SKIPPED]`);
180
+ blocked.forEach(item => {
181
+ console.log(` • ${item.name}: ${item.reason}`);
182
+ });
183
+ }
184
+ console.log(`\n=============================================\n`);
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Storage Observability Logger (NEW)
190
+ */
191
+ logStorage(processId, calcName, date, path, sizeBytes, isSharded) {
192
+ // Standard Log Call with extended metadata
193
+ this.log(LOG_LEVELS.INFO, `Results stored for ${calcName}`, {
194
+ storage: {
195
+ path,
196
+ sizeBytes,
197
+ isSharded,
198
+ sizeMB: (sizeBytes / 1024 / 1024).toFixed(2)
199
+ }
200
+ }, PROCESS_TYPES.STORAGE, processId, calcName, date);
201
+ }
202
+
203
+ /**
204
+ * Core logging method
205
+ */
206
+ log(level, message, context = {}, processType = null, processId = null, computationName = null, date = null) {
207
+ const numericLevel = typeof level === 'string' ? LOG_LEVELS[level] : level;
208
+
209
+ if (numericLevel < this.config.minLevel) return;
210
+
211
+ // Support passing "meta" object in place of context for newer calls
212
+ // Logic: If context contains 'processId' or 'storage', it's likely a meta object
213
+ let finalContext = context;
214
+ let finalMetadata = {};
215
+ let finalStats = undefined;
216
+ let finalStorage = undefined;
217
+
218
+ if (context && (context.processId || context.storage || context.stats)) {
219
+ if (context.processId) processId = context.processId;
220
+ if (context.processType) processType = context.processType;
221
+ if (context.computationName) computationName = context.computationName;
222
+ if (context.date) date = context.date;
223
+ if (context.stats) finalStats = context.stats;
224
+ if (context.storage) finalStorage = context.storage;
225
+ // Clean up context to be just the data remaining
226
+ finalContext = { ...context };
227
+ delete finalContext.processId; delete finalContext.processType;
228
+ delete finalContext.computationName; delete finalContext.date;
229
+ delete finalContext.stats; delete finalContext.storage;
230
+ }
231
+
232
+ const entry = {
233
+ timestamp: new Date().toISOString(),
234
+ level: Object.keys(LOG_LEVELS).find(k => LOG_LEVELS[k] === numericLevel) || 'INFO',
235
+ processType,
236
+ processId,
237
+ computationName,
238
+ date,
239
+ message,
240
+ context: typeof finalContext === 'string' ? { error: finalContext } : finalContext,
241
+ metadata: finalMetadata,
242
+ stats: finalStats,
243
+ storage: finalStorage
244
+ };
245
+
246
+ // Add stack trace for errors
247
+ if (numericLevel >= LOG_LEVELS.ERROR && this.config.includeStackTrace && finalContext.stack) {
248
+ entry.metadata.stackTrace = finalContext.stack;
249
+ }
250
+
251
+ // Console output (pretty-printed for development)
252
+ if (this.config.enableConsole) {
253
+ this._consoleLog(entry);
254
+ }
255
+
256
+ // Structured output (for log aggregation systems)
257
+ if (this.config.enableStructured) {
258
+ console.log(formatLogEntry(entry));
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Pretty console output for development
264
+ */
265
+ _consoleLog(entry) {
266
+ const symbols = { TRACE: '🔍', DEBUG: '🐛', INFO: 'ℹ️', WARN: '⚠️', ERROR: '❌', FATAL: '💀' };
267
+ const colors = {
268
+ TRACE: '\x1b[90m', DEBUG: '\x1b[36m', INFO: '\x1b[32m',
269
+ WARN: '\x1b[33m', ERROR: '\x1b[31m', FATAL: '\x1b[35m'
270
+ };
271
+ const reset = '\x1b[0m';
272
+ const color = colors[entry.level] || '';
273
+ const symbol = symbols[entry.level] || 'ℹ️';
274
+
275
+ let output = `${color}${symbol} [${entry.level}]${reset}`;
276
+
277
+ if (entry.processType) output += ` [${entry.processType}]`;
278
+ if (entry.processId) output += ` [${entry.processId.substring(0, 8)}]`;
279
+ if (entry.computationName) output += ` [${entry.computationName}]`;
280
+ if (entry.date) output += ` [${entry.date}]`;
281
+
282
+ output += ` ${entry.message}`;
283
+
284
+ console.log(output);
285
+
286
+ // Print context if present and not empty
287
+ if (entry.context && Object.keys(entry.context).length > 0) {
288
+ console.log(` ${color}Context:${reset}`, entry.context);
289
+ }
290
+ if (entry.storage) {
291
+ console.log(` ${color}Storage:${reset}`, entry.storage);
292
+ }
293
+ }
294
+
295
+ // Convenience methods
296
+ trace(message, context = {}) { this.log(LOG_LEVELS.TRACE, message, context); }
297
+ debug(message, context = {}) { this.log(LOG_LEVELS.DEBUG, message, context); }
298
+ info(message, context = {}) { this.log(LOG_LEVELS.INFO, message, context); }
299
+ warn(message, context = {}) { this.log(LOG_LEVELS.WARN, message, context); }
300
+ error(message, context = {}) { this.log(LOG_LEVELS.ERROR, message, context); }
301
+ fatal(message, context = {}) { this.log(LOG_LEVELS.FATAL, message, context); }
302
+ }
303
+
304
+ /**
305
+ * Process-scoped Logger
306
+ * Automatically includes process context in all log calls
307
+ */
308
+ class ProcessLogger {
309
+ constructor(parent, processType, processId, computationName, date) {
310
+ this.parent = parent;
311
+ this.processType = processType;
312
+ this.processId = processId;
313
+ this.computationName = computationName;
314
+ this.date = date;
315
+ this.startTime = Date.now();
316
+ this.metrics = {
317
+ operations: 0,
318
+ errors: 0,
319
+ warnings: 0
320
+ };
321
+ }
322
+
323
+ log(level, message, context = {}) {
324
+ const numericLevel = typeof level === 'string' ? LOG_LEVELS[level] : level;
325
+
326
+ this.metrics.operations++;
327
+ if (numericLevel === LOG_LEVELS.ERROR || numericLevel === LOG_LEVELS.FATAL) {
328
+ this.metrics.errors++;
329
+ }
330
+ if (numericLevel === LOG_LEVELS.WARN) {
331
+ this.metrics.warnings++;
332
+ }
333
+
334
+ this.parent.log(
335
+ level,
336
+ message,
337
+ context,
338
+ this.processType,
339
+ this.processId,
340
+ this.computationName,
341
+ this.date
342
+ );
343
+ }
344
+
345
+ /**
346
+ * Complete the process and log summary
347
+ */
348
+ complete(success = true, finalMessage = null) {
349
+ const duration = Date.now() - this.startTime;
350
+ const level = success ? LOG_LEVELS.INFO : LOG_LEVELS.ERROR;
351
+
352
+ const summaryMessage = finalMessage || (success
353
+ ? `Process completed successfully`
354
+ : `Process completed with errors`);
355
+
356
+ this.log(level, summaryMessage, {
357
+ stats: {
358
+ duration: `${duration}ms`,
359
+ durationMs: duration,
360
+ operations: this.metrics.operations,
361
+ errors: this.metrics.errors,
362
+ warnings: this.metrics.warnings,
363
+ success
364
+ }
365
+ });
366
+
367
+ this.parent.endProcess(this.processId);
368
+ return duration;
369
+ }
370
+
371
+ // Convenience methods
372
+ trace(message, context = {}) { this.log(LOG_LEVELS.TRACE, message, context); }
373
+ debug(message, context = {}) { this.log(LOG_LEVELS.DEBUG, message, context); }
374
+ info(message, context = {}) { this.log(LOG_LEVELS.INFO, message, context); }
375
+ warn(message, context = {}) { this.log(LOG_LEVELS.WARN, message, context); }
376
+ error(message, context = {}) { this.log(LOG_LEVELS.ERROR, message, context); }
377
+ fatal(message, context = {}) { this.log(LOG_LEVELS.FATAL, message, context); }
378
+ }
379
+
380
+ /**
381
+ * Log Message Templates
382
+ */
383
+ const LOG_TEMPLATES = {
384
+ // Schema Generation
385
+ SCHEMA_SUCCESS: (computationName) =>
386
+ `Schema generation successful for ${computationName}`,
387
+ SCHEMA_FAILURE: (computationName, reason) =>
388
+ `Schema generation failed for ${computationName}: ${reason}`,
389
+
390
+ // Computation Execution
391
+ COMPUTATION_START: (computationName, date) =>
392
+ `Starting computation ${computationName} for ${date}`,
393
+ COMPUTATION_SUCCESS: (computationName, date) =>
394
+ `Computation successful for ${computationName} on ${date}`,
395
+ COMPUTATION_FAILURE: (computationName, date, reason) =>
396
+ `Computation failed for ${computationName} on ${date}: ${reason}`,
397
+
398
+ // Storage
399
+ STORAGE_SUCCESS: (computationName, date, path, size) =>
400
+ `Results stored for ${computationName} on ${date} at ${path} (${size} bytes)`,
401
+ STORAGE_FAILURE: (computationName, date, path, reason) =>
402
+ `Failed to store results for ${computationName} on ${date} at ${path}: ${reason}`,
403
+
404
+ // Hash Validation
405
+ HASH_MISMATCH: (computationName, storedHash, currentHash) =>
406
+ `Hash mismatch for ${computationName}: stored=${storedHash}, current=${currentHash}`,
407
+ HASH_MATCH: (computationName) =>
408
+ `Hash match for ${computationName}, no code changes detected`,
409
+ HASH_CASCADE: (computationName, affectedComputations) =>
410
+ `Code change in ${computationName} will cascade to: ${affectedComputations.join(', ')}`,
411
+
412
+ // Manifest
413
+ MANIFEST_SUCCESS: (computationCount) =>
414
+ `Manifest built successfully with ${computationCount} computations`,
415
+ MANIFEST_TREE: (tree) =>
416
+ `Dependency tree:\n${tree}`,
417
+
418
+ // Date Analysis
419
+ DATE_ANALYSIS: (date, runnable, notRunnable) =>
420
+ `Date ${date}: ${runnable.length} runnable, ${notRunnable.length} blocked`,
421
+ DATE_MISSING_ROOTDATA: (date, computationName, missingData) =>
422
+ `${computationName} on ${date}: Cannot run due to missing root data: ${missingData.join(', ')}`,
423
+ DATE_MISSING_DEPENDENCY: (date, computationName, missingDep) =>
424
+ `${computationName} on ${date}: Will resolve after ${missingDep} completes`,
425
+ DATE_HASH_RERUN: (date, computationName, affectedDeps) =>
426
+ `${computationName} on ${date}: Hash mismatch, re-running (affects: ${affectedDeps.join(', ')})`,
427
+
428
+ // Availability
429
+ DATA_AVAILABLE: (date, types) =>
430
+ `Data available for ${date}: ${types.join(', ')}`,
431
+ DATA_MISSING: (date, types) =>
432
+ `Data missing for ${date}: ${types.join(', ')}`,
433
+
434
+ // Dispatch/Worker
435
+ DISPATCH_START: (pass, dateCount) =>
436
+ `Dispatching Pass ${pass} for ${dateCount} dates`,
437
+ DISPATCH_COMPLETE: (pass, dispatched) =>
438
+ `Dispatch complete: ${dispatched} tasks for Pass ${pass}`,
439
+ WORKER_TASK_START: (date, pass) =>
440
+ `Worker starting task: Date=${date}, Pass=${pass}`,
441
+ WORKER_TASK_COMPLETE: (date, pass, updateCount) =>
442
+ `Worker completed task: Date=${date}, Pass=${pass}, Updates=${updateCount}`,
443
+ WORKER_TASK_SKIP: (date, pass, reason) =>
444
+ `Worker skipped task: Date=${date}, Pass=${pass}, Reason=${reason}`
445
+ };
446
+
447
+ module.exports = {
448
+ StructuredLogger,
449
+ ProcessLogger,
450
+ LOG_LEVELS,
451
+ PROCESS_TYPES,
452
+ LOG_TEMPLATES,
453
+ generateProcessId
454
+ };
@@ -1,22 +1,22 @@
1
1
  /**
2
- * @fileoverview Handles saving computation results with transparent auto-sharding.
2
+ * @fileoverview Handles saving computation results with observability.
3
3
  */
4
4
  const { commitBatchInChunks } = require('./FirestoreUtils');
5
5
  const { updateComputationStatus } = require('./StatusRepository');
6
6
  const { batchStoreSchemas } = require('../utils/schema_capture');
7
+ const { generateProcessId, PROCESS_TYPES } = require('../logger/logger');
7
8
 
8
9
  async function commitResults(stateObj, dStr, passName, config, deps, skipStatusWrite = false) {
9
10
  const successUpdates = {};
10
11
  const schemas = [];
12
+ const { logger } = deps;
13
+ const pid = generateProcessId(PROCESS_TYPES.STORAGE, passName, dStr);
11
14
 
12
15
  for (const name in stateObj) {
13
16
  const calc = stateObj[name];
14
17
  try {
15
18
  const result = await calc.getResult();
16
- if (!result) {
17
- deps.logger.log('INFO', `${name} for ${dStr}: Skipped (Empty Result)`);
18
- continue;
19
- }
19
+ if (!result) continue;
20
20
 
21
21
  const mainDocRef = deps.db.collection(config.resultsCollection)
22
22
  .doc(dStr)
@@ -25,8 +25,9 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
25
25
  .collection(config.computationsSubcollection)
26
26
  .doc(name);
27
27
 
28
- const updates = await prepareAutoShardedWrites(result, mainDocRef, deps.logger);
28
+ const updates = await prepareAutoShardedWrites(result, mainDocRef, logger);
29
29
 
30
+ // Capture Schema
30
31
  if (calc.manifest.class.getSchema) {
31
32
  const { class: _cls, ...safeMetadata } = calc.manifest;
32
33
  schemas.push({
@@ -38,15 +39,18 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
38
39
  }
39
40
 
40
41
  if (updates.length > 0) {
42
+ const totalSize = updates.reduce((acc, u) => acc + (u.data ? JSON.stringify(u.data).length : 0), 0);
43
+ const isSharded = updates.some(u => u.data._sharded === true);
44
+
41
45
  await commitBatchInChunks(config, deps, updates, `${name} Results`);
46
+
47
+ // Structured Storage Log
48
+ logger.logStorage(pid, name, dStr, mainDocRef.path, totalSize, isSharded);
49
+
42
50
  successUpdates[name] = calc.manifest.hash || true;
43
- const isSharded = updates.some(u => u.data._sharded === true);
44
- deps.logger.log('INFO', `${name} for ${dStr}: \u2714 Success (Written ${isSharded ? 'Sharded' : 'Standard'})`);
45
- } else {
46
- deps.logger.log('INFO', `${name} for ${dStr}: - Empty Data`);
47
51
  }
48
52
  } catch (e) {
49
- deps.logger.log('ERROR', `${name} for ${dStr}: \u2716 FAILED Commit: ${e.message}`);
53
+ logger.log('ERROR', `Commit failed for ${name}`, { processId: pid, error: e.message });
50
54
  }
51
55
  }
52
56
 
@@ -54,12 +58,14 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
54
58
 
55
59
  if (!skipStatusWrite && Object.keys(successUpdates).length > 0) {
56
60
  await updateComputationStatus(dStr, successUpdates, config, deps);
57
- deps.logger.log('INFO', `[${passName}] Updated status document for ${Object.keys(successUpdates).length} successful computations.`);
58
61
  }
59
62
  return successUpdates;
60
63
  }
61
64
 
65
+ // ... rest of file (calculateFirestoreBytes, prepareAutoShardedWrites) remains same ...
66
+ // Just ensure prepareAutoShardedWrites uses the provided logger if it logs internal warnings.
62
67
  function calculateFirestoreBytes(value) {
68
+ // ... same as before
63
69
  if (value === null) return 1;
64
70
  if (value === undefined) return 0;
65
71
  if (typeof value === 'boolean') return 1;
@@ -73,7 +79,9 @@ function calculateFirestoreBytes(value) {
73
79
  }
74
80
 
75
81
  async function prepareAutoShardedWrites(result, docRef, logger) {
76
- const SAFETY_THRESHOLD_BYTES = 1000 * 1024; // 1MB Limit
82
+ // ... same logic, just ensure existing logs inside here use the logger properly if needed
83
+ // Copied from previous logic, essentially checks size > 900KB and splits
84
+ const SAFETY_THRESHOLD_BYTES = 1000 * 1024;
77
85
  const OVERHEAD_ALLOWANCE = 20 * 1024;
78
86
  const CHUNK_LIMIT = SAFETY_THRESHOLD_BYTES - OVERHEAD_ALLOWANCE;
79
87
  const totalSize = calculateFirestoreBytes(result);
@@ -84,19 +92,16 @@ async function prepareAutoShardedWrites(result, docRef, logger) {
84
92
  let currentChunkSize = 0;
85
93
  let shardIndex = 0;
86
94
 
87
- // If under limit, write directly
88
95
  if ((totalSize + docPathSize) < CHUNK_LIMIT) { const data = { ...result, _completed: true, _sharded: false }; return [{ ref: docRef, data, options: { merge: true } }]; }
89
96
 
90
- logger.log('INFO', `[AutoShard] Result size ~${Math.round(totalSize/1024)}KB exceeds limit. Sharding...`);
91
-
92
- // iF over limit, shard the document
97
+ // Note: We don't log "Sharding..." here anymore because we log the structured event in commitResults
98
+
93
99
  for (const [key, value] of Object.entries(result)) {
94
100
  if (key.startsWith('_')) continue;
95
101
  const keySize = Buffer.byteLength(key, 'utf8') + 1;
96
102
  const valueSize = calculateFirestoreBytes(value);
97
103
  const itemSize = keySize + valueSize;
98
104
 
99
- // If adding this item exceeds the chunk limit, commit current chunk
100
105
  if (currentChunkSize + itemSize > CHUNK_LIMIT) {
101
106
  writes.push({ ref: shardCollection.doc(`shard_${shardIndex}`), data: currentChunk, options: { merge: false } });
102
107
  shardIndex++;
@@ -107,10 +112,8 @@ async function prepareAutoShardedWrites(result, docRef, logger) {
107
112
  currentChunkSize += itemSize;
108
113
  }
109
114
 
110
- // Write the final chunk
111
115
  if (Object.keys(currentChunk).length > 0) { writes.push({ ref: shardCollection.doc(`shard_${shardIndex}`), data: currentChunk, options: { merge: false } }); }
112
116
 
113
- // Finally, write the pointer document
114
117
  const pointerData = { _completed: true, _sharded: true, _shardCount: shardIndex + 1, _lastUpdated: new Date().toISOString() };
115
118
  writes.push({ ref: docRef, data: pointerData, options: { merge: false } });
116
119
  return writes;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @fileoverview Singleton Loader for the Manifest.
3
+ * Prevents expensive manifest rebuilding on every function invocation if not needed.
4
+ */
5
+ const { build } = require('../context/ManifestBuilder');
6
+ const { StructuredLogger, PROCESS_TYPES, generateProcessId } = require('../logger/logger');
7
+
8
+ // Cache the manifest in global scope (warm start optimization)
9
+ let cachedManifest = null;
10
+
11
+ function getManifest(productLines = [], calculationsDir, dependencies = {}) {
12
+ if (cachedManifest) {
13
+ return cachedManifest;
14
+ }
15
+
16
+ const logger = dependencies.logger || new StructuredLogger();
17
+ const pid = generateProcessId(PROCESS_TYPES.MANIFEST, 'build', new Date().toISOString().slice(0,10));
18
+
19
+ logger.log('INFO', 'Starting Manifest Build...', { processId: pid });
20
+
21
+ const startTime = Date.now();
22
+ try {
23
+ cachedManifest = build(productLines, calculationsDir);
24
+
25
+ // Log Topology Stats
26
+ const passCounts = {};
27
+ cachedManifest.forEach(c => { passCounts[c.pass] = (passCounts[c.pass] || 0) + 1; });
28
+
29
+ logger.log('INFO', 'Manifest Build Success', {
30
+ processId: pid,
31
+ durationMs: Date.now() - startTime,
32
+ totalCalculations: cachedManifest.length,
33
+ topology: passCounts
34
+ });
35
+
36
+ return cachedManifest;
37
+ } catch (e) {
38
+ logger.log('FATAL', 'Manifest Build Failed', { processId: pid, error: e.message });
39
+ throw e;
40
+ }
41
+ }
42
+
43
+ module.exports = { getManifest };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.222",
3
+ "version": "1.0.224",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [