bulltrackers-module 1.0.223 → 1.0.225

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,15 +1,14 @@
1
1
  /**
2
- * @fileoverview Main Orchestrator. Coordinates the topological execution of calculations.
2
+ * @fileoverview Main Orchestrator. Coordinates the topological execution.
3
+ * UPDATED: Strict Dependency & Hash Cascade Logic with Explicit Failure Marking.
3
4
  */
4
- const { normalizeName, getExpectedDateStrings } = require('./utils/utils');
5
- const { checkRootDataAvailability, getViableCalculations } = require('./data/AvailabilityChecker');
5
+ const { normalizeName } = require('./utils/utils');
6
+ const { checkRootDataAvailability } = require('./data/AvailabilityChecker');
6
7
  const { fetchExistingResults } = require('./data/DependencyFetcher');
7
8
  const { fetchComputationStatus, updateComputationStatus } = require('./persistence/StatusRepository');
8
- const { runBatchPriceComputation } = require('./executors/PriceBatchExecutor');
9
9
  const { StandardExecutor } = require('./executors/StandardExecutor');
10
10
  const { MetaExecutor } = require('./executors/MetaExecutor');
11
-
12
- const PARALLEL_BATCH_SIZE = 7;
11
+ const { generateProcessId, PROCESS_TYPES } = require('./logger/logger');
13
12
 
14
13
  function groupByPass(manifest) {
15
14
  return manifest.reduce((acc, calc) => {
@@ -18,135 +17,192 @@ function groupByPass(manifest) {
18
17
  }, {});
19
18
  }
20
19
 
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}...`);
20
+ /**
21
+ * Performs strict analysis of what can run based on availability and hash states.
22
+ * AIRTIGHT LOGIC:
23
+ * 1. Missing Root Data -> Blocked (Writes 'false' to DB)
24
+ * 2. Missing/Stale Dependency -> FailedDependency (Writes 'false' to DB)
25
+ * 3. Hash Mismatch -> ReRun (Cascade or Code Change)
26
+ * 4. No Result -> Run
27
+ * 5. Result Exists & Hash Match -> Skip
28
+ */
29
+ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus, manifestMap) {
30
+ const report = {
31
+ runnable: [],
32
+ blocked: [], // Missing Root Data
33
+ failedDependency: [], // Missing Dependency OR Stale Dependency
34
+ reRuns: [], // Hash Mismatch (Triggered by self code change OR upstream cascade)
35
+ skipped: [] // Already done & valid
36
+ };
27
37
 
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')
38
+ // Helper: Is a dependency satisfied AND valid (matching hash)?
39
+ const isDepSatisfied = (depName, dailyStatus, manifestMap) => {
40
+ const norm = normalizeName(depName);
41
+ const storedDepHash = dailyStatus[norm];
42
+ const depManifest = manifestMap.get(norm);
43
+
44
+ // 1. Must exist in DB. If missing, we cannot run.
45
+ if (!storedDepHash) return false;
46
+
47
+ // 2. Must exist in Manifest (Sanity check)
48
+ if (!depManifest) return false;
49
+
50
+ // 3. STRICT: The dependency's stored hash must match its current manifest hash.
51
+ // If 'A' changed code, 'A' has a new hash. If we are running 'B' (Pass 2),
52
+ // we expect 'A' (Pass 1) to have already run and updated the DB with the NEW hash.
53
+ // If DB still has OLD hash, 'A' failed or didn't run. 'B' is unsafe to run.
54
+ if (storedDepHash !== depManifest.hash) return false;
55
+
56
+ return true;
34
57
  };
35
- earliestDates.absoluteEarliest = Object.values(earliestDates).reduce((a, b) => a < b ? a : b);
36
- const passes = groupByPass(computationManifest);
37
- const calcsInThisPass = passes[passToRun] || [];
38
58
 
39
- if (!calcsInThisPass.length) return logger.log('WARN', `[PassRunner] No calcs for Pass ${passToRun}. Exiting.`);
59
+ for (const calc of calcsInPass) {
60
+ const cName = normalizeName(calc.name);
61
+ const storedHash = dailyStatus[cName];
62
+ const currentHash = calc.hash;
40
63
 
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);
64
+ // 1. Root Data Check (FATAL)
65
+ const missingRoots = [];
66
+ if (calc.rootDataDependencies) {
67
+ for (const dep of calc.rootDataDependencies) {
68
+ if (dep === 'portfolio' && !rootDataStatus.hasPortfolio) missingRoots.push('portfolio');
69
+ if (dep === 'insights' && !rootDataStatus.hasInsights) missingRoots.push('insights');
70
+ if (dep === 'social' && !rootDataStatus.hasSocial) missingRoots.push('social');
71
+ if (dep === 'history' && !rootDataStatus.hasHistory) missingRoots.push('history');
72
+ if (dep === 'price' && !rootDataStatus.hasPrices) missingRoots.push('price');
73
+ }
54
74
  }
55
- }
56
75
 
57
- if (standardAndOtherMetaCalcs.length === 0) return;
76
+ if (missingRoots.length > 0) {
77
+ report.blocked.push({ name: cName, reason: `Missing Root Data: ${missingRoots.join(', ')}` });
78
+ continue; // Cannot proceed
79
+ }
58
80
 
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
- ));
81
+ // 2. Dependency Check (FATAL)
82
+ // Since we are in a topological pass system, all dependencies SHOULD be satisfied
83
+ // by previous passes. If not, it is a fatal error for this calculation.
84
+ const missingDeps = [];
85
+ if (calc.dependencies) {
86
+ for (const dep of calc.dependencies) {
87
+ if (!isDepSatisfied(dep, dailyStatus, manifestMap)) {
88
+ missingDeps.push(dep);
89
+ }
90
+ }
91
+ }
92
+
93
+ if (missingDeps.length > 0) {
94
+ report.failedDependency.push({ name: cName, missing: missingDeps });
95
+ continue; // Cannot proceed
96
+ }
97
+
98
+ // 3. Hash / State Check
99
+ if (!storedHash) {
100
+ // Case A: No result exists (or status is 'false' from previous failure) -> RUN
101
+ // Note: If storedHash is boolean false, !storedHash is true, so we retry.
102
+ report.runnable.push(calc);
103
+ } else if (storedHash !== currentHash) {
104
+ // Case B: Result exists, but hash mismatch -> RE-RUN
105
+ // This covers code changes in THIS calc, AND cascading changes from dependencies
106
+ report.reRuns.push({ name: cName, oldHash: storedHash, newHash: currentHash });
107
+ } else if (storedHash === true) {
108
+ // Case C: Legacy boolean status -> RE-RUN (Upgrade to hash)
109
+ report.reRuns.push({ name: cName, reason: 'Legacy Upgrade' });
110
+ } else {
111
+ // Case D: Result exists, Hash Matches -> SKIP
112
+ report.skipped.push({ name: cName });
113
+ }
64
114
  }
115
+
116
+ return report;
65
117
  }
66
118
 
67
119
  async function runDateComputation(dateStr, passToRun, calcsInThisPass, config, dependencies, computationManifest) {
68
120
  const { logger } = dependencies;
121
+ const orchestratorPid = generateProcessId(PROCESS_TYPES.ORCHESTRATOR, passToRun, dateStr);
122
+
69
123
  const dateToProcess = new Date(dateStr + 'T00:00:00Z');
70
124
 
71
- // 1. Version Check
72
- const dailyStatus = await fetchComputationStatus(dateStr, config, dependencies);
73
- const calcsToAttempt = [];
125
+ // 1. Fetch State
126
+ const dailyStatus = await fetchComputationStatus(dateStr, config, dependencies);
74
127
 
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
- calcsToAttempt.push(calc); continue;
82
- }
83
- if (typeof storedStatus === 'string' && currentHash && storedStatus !== currentHash) {
84
- logger.log('INFO', `[Versioning] ${cName}: Code Changed.`);
85
- calcsToAttempt.push(calc); continue;
86
- }
87
- if (storedStatus === true && currentHash) {
88
- logger.log('INFO', `[Versioning] ${cName}: Upgrading legacy status.`);
89
- calcsToAttempt.push(calc); continue;
90
- }
91
- }
92
-
93
- if (!calcsToAttempt.length) return null;
94
-
95
- // 2. Data Availability Check
128
+ // 2. Check Data Availability (One shot)
96
129
  const earliestDates = {
97
130
  portfolio: new Date('2025-09-25T00:00:00Z'),
98
- history: new Date('2025-11-05T00:00:00Z'),
131
+ history: new Date('2025-11-05T00:00:00Z'),
99
132
  social: new Date('2025-10-30T00:00:00Z'),
100
133
  insights: new Date('2025-08-26T00:00:00Z'),
101
134
  price: new Date('2025-08-01T00:00:00Z')
102
135
  };
103
-
136
+
104
137
  const rootData = await checkRootDataAvailability(dateStr, config, dependencies, earliestDates);
105
- if (!rootData) { logger.log('INFO', `[DateRunner] Root data missing for ${dateStr}. Skipping.`); return null; }
138
+ const rootStatus = rootData ? rootData.status : { hasPortfolio: false, hasPrices: false, hasInsights: false, hasSocial: false, hasHistory: false };
139
+
140
+ // 3. ANALYZE EXECUTION
141
+ const manifestMap = new Map(computationManifest.map(c => [normalizeName(c.name), c]));
142
+ const analysisReport = analyzeDateExecution(dateStr, calcsInThisPass, rootStatus, dailyStatus, manifestMap);
143
+
144
+ // 4. LOG ANALYSIS
145
+ logger.logDateAnalysis(dateStr, analysisReport);
146
+
147
+ // 5. MARK FAILURES (Explicitly write 'false' to DB for blocked items)
148
+ // This prevents UI/downstream consumers from waiting indefinitely.
149
+ const failureUpdates = {};
150
+ analysisReport.blocked.forEach(item => failureUpdates[item.name] = false);
151
+ analysisReport.failedDependency.forEach(item => failureUpdates[item.name] = false);
106
152
 
107
- // 3. Viability Check (Smart Execution Map)
108
- // Filter candidates: Remove any calculation that misses Root Data OR Matches Stale Dependencies
109
- // PASSED: computationManifest (needed for hash lookup)
110
- const runnableCalcs = getViableCalculations(calcsToAttempt, computationManifest, rootData.status, dailyStatus);
111
-
112
- if (!runnableCalcs.length) {
113
- // logger.log('INFO', `[DateRunner] ${dateStr}: Candidates pruned due to missing deps/data/stale hashes.`);
114
- return null;
153
+ if (Object.keys(failureUpdates).length > 0) {
154
+ await updateComputationStatus(dateStr, failureUpdates, config, dependencies);
155
+ }
156
+
157
+ // 6. EXECUTE RUNNABLES
158
+ const calcsToRunNames = new Set([
159
+ ...analysisReport.runnable.map(c => c.name),
160
+ ...analysisReport.reRuns.map(c => c.name)
161
+ ]);
162
+
163
+ const finalRunList = calcsInThisPass.filter(c => calcsToRunNames.has(normalizeName(c.name)));
164
+
165
+ if (!finalRunList.length) {
166
+ // Nothing to run (everything either skipped, blocked, or failed)
167
+ return { date: dateStr, updates: {}, skipped: analysisReport.skipped.length };
115
168
  }
116
169
 
117
- const standardToRun = runnableCalcs.filter(c => c.type === 'standard');
118
- const metaToRun = runnableCalcs.filter(c => c.type === 'meta');
119
- logger.log('INFO', `[DateRunner] Running ${dateStr}: ${standardToRun.length} std, ${metaToRun.length} meta`);
170
+ logger.log('INFO', `[Orchestrator] Executing ${finalRunList.length} calculations for ${dateStr}`, { processId: orchestratorPid });
171
+
172
+ const standardToRun = finalRunList.filter(c => c.type === 'standard');
173
+ const metaToRun = finalRunList.filter(c => c.type === 'meta');
120
174
 
121
175
  const dateUpdates = {};
122
176
 
123
177
  try {
124
- const calcsRunning = [...standardToRun, ...metaToRun];
178
+ const calcsRunning = [...standardToRun, ...metaToRun];
179
+
180
+ // Fetch dependencies (Previous pass results)
181
+ // includeSelf=false: We are re-running, so we ignore our own old results.
125
182
  const existingResults = await fetchExistingResults(dateStr, calcsRunning, computationManifest, config, dependencies, false);
183
+
184
+ // Fetch Yesterday's results (if Historical)
126
185
  const prevDate = new Date(dateToProcess); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
127
186
  const prevDateStr = prevDate.toISOString().slice(0, 10);
128
187
  const previousResults = await fetchExistingResults(prevDateStr, calcsRunning, computationManifest, config, dependencies, true);
129
188
 
130
189
  if (standardToRun.length) {
131
- const updates = await StandardExecutor.run(dateToProcess, standardToRun, `Pass ${passToRun} (Std)`, config, dependencies, rootData, existingResults, previousResults, false);
190
+ const updates = await StandardExecutor.run(dateToProcess, standardToRun, `Pass ${passToRun}`, config, dependencies, rootData, existingResults, previousResults, false);
132
191
  Object.assign(dateUpdates, updates);
133
192
  }
134
193
  if (metaToRun.length) {
135
- const updates = await MetaExecutor.run(dateToProcess, metaToRun, `Pass ${passToRun} (Meta)`, config, dependencies, existingResults, previousResults, rootData, false);
194
+ const updates = await MetaExecutor.run(dateToProcess, metaToRun, `Pass ${passToRun}`, config, dependencies, existingResults, previousResults, rootData, false);
136
195
  Object.assign(dateUpdates, updates);
137
196
  }
138
197
 
139
198
  } catch (err) {
140
- logger.log('ERROR', `[DateRunner] FAILED Pass ${passToRun} for ${dateStr}`, { errorMessage: err.message });
141
- [...standardToRun, ...metaToRun].forEach(c => dateUpdates[normalizeName(c.name)] = false);
199
+ // If execution CRASHES (code bug, timeout), we log and throw.
200
+ // Pub/Sub will retry this message. This is correct for transient errors.
201
+ logger.log('ERROR', `[Orchestrator] Failed execution for ${dateStr}`, { processId: orchestratorPid, error: err.message });
142
202
  throw err;
143
203
  }
144
204
 
145
- if (Object.keys(dateUpdates).length > 0) {
146
- await updateComputationStatus(dateStr, dateUpdates, config, dependencies);
147
- }
148
-
149
205
  return { date: dateStr, updates: dateUpdates };
150
206
  }
151
207
 
152
- module.exports = { runComputationPass, runDateComputation, groupByPass };
208
+ 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 };
@@ -52,29 +52,18 @@ function getViableCalculations(candidates, fullManifest, rootDataStatus, dailySt
52
52
  const depManifest = manifestMap.get(normDep);
53
53
 
54
54
  // If dependency is missing from manifest, we can't verify it (shouldn't happen)
55
- if (!depManifest) {
56
- dependenciesMet = false;
57
- break;
58
- }
55
+ if (!depManifest) { dependenciesMet = false; break; }
59
56
 
60
57
  // CHECK: Does the dependency exist in DB?
61
- if (!storedHash) {
62
- dependenciesMet = false;
63
- break;
64
- }
58
+ if (!storedHash) { dependenciesMet = false; break; }
65
59
 
66
60
  // CHECK: Does the stored hash match the current code hash?
67
61
  // This prevents running on stale data if a dependency failed to update.
68
- if (storedHash !== depManifest.hash) {
69
- dependenciesMet = false;
70
- break;
71
- }
62
+ if (storedHash !== depManifest.hash) { dependenciesMet = false; break; }
72
63
  }
73
64
  }
74
65
 
75
- if (dependenciesMet) {
76
- viable.push(calc);
77
- }
66
+ if (dependenciesMet) { viable.push(calc); }
78
67
  }
79
68
 
80
69
  return viable;
@@ -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.223",
3
+ "version": "1.0.225",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [