bulltrackers-module 1.0.242 → 1.0.244

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,7 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Main Orchestrator. Coordinates the topological execution.
3
- * UPDATED: Exports analyzeDateExecution for Build Reporting tools.
4
- * UPDATED: Uses centralized DEFINITIVE_EARLIEST_DATES.
3
+ * UPDATED: Added 'executeDispatchTask' for trusted execution from Smart Dispatcher.
5
4
  */
6
5
  const { normalizeName, DEFINITIVE_EARLIEST_DATES } = require('./utils/utils');
7
6
  const { checkRootDataAvailability } = require('./data/AvailabilityChecker');
@@ -20,7 +19,10 @@ function groupByPass(manifest) {
20
19
  }, {});
21
20
  }
22
21
 
23
- function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus, manifestMap) {
22
+ /**
23
+ * Analyzes whether calculations should run, be skipped, or are blocked.
24
+ */
25
+ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus, manifestMap, prevDailyStatus = null) {
24
26
  const report = {
25
27
  runnable: [],
26
28
  blocked: [],
@@ -34,14 +36,11 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
34
36
 
35
37
  const isDepSatisfied = (depName, dailyStatus, manifestMap) => {
36
38
  const norm = normalizeName(depName);
37
- const stored = dailyStatus[norm]; // Now an object or null
39
+ const stored = dailyStatus[norm];
38
40
  const depManifest = manifestMap.get(norm);
39
41
 
40
42
  if (!stored) return false;
41
-
42
- // Handle IMPOSSIBLE flag (stored as object property or legacy string check)
43
43
  if (stored.hash === STATUS_IMPOSSIBLE) return false;
44
-
45
44
  if (!depManifest) return false;
46
45
  if (stored.hash !== depManifest.hash) return false;
47
46
 
@@ -50,13 +49,12 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
50
49
 
51
50
  for (const calc of calcsInPass) {
52
51
  const cName = normalizeName(calc.name);
53
- const stored = dailyStatus[cName]; // Object { hash, category }
52
+ const stored = dailyStatus[cName];
54
53
 
55
54
  const storedHash = stored ? stored.hash : null;
56
55
  const storedCategory = stored ? stored.category : null;
57
56
  const currentHash = calc.hash;
58
57
 
59
- // [SMART MIGRATION] Detect if category changed, independent of hash check
60
58
  let migrationOldCategory = null;
61
59
  if (storedCategory && storedCategory !== calc.category) {
62
60
  migrationOldCategory = storedCategory;
@@ -119,12 +117,28 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
119
117
  continue;
120
118
  }
121
119
 
122
- // 4. Hash & Category Check (Smart Migration Logic)
120
+ // 4. Strict Historical Consistency
121
+ if (calc.isHistorical && prevDailyStatus) {
122
+ const yesterday = new Date(dateStr + 'T00:00:00Z');
123
+ yesterday.setUTCDate(yesterday.getUTCDate() - 1);
124
+
125
+ if (yesterday >= DEFINITIVE_EARLIEST_DATES.absoluteEarliest) {
126
+ const prevStored = prevDailyStatus[cName];
127
+
128
+ if (!prevStored || prevStored.hash !== currentHash) {
129
+ report.blocked.push({
130
+ name: cName,
131
+ reason: `Waiting for historical continuity (Yesterday ${!prevStored ? 'Missing' : 'Hash Mismatch'})`
132
+ });
133
+ continue;
134
+ }
135
+ }
136
+ }
137
+
138
+ // 5. Runnable Decision
123
139
  if (!storedHash) {
124
140
  report.runnable.push(calc);
125
141
  } else if (storedHash !== currentHash) {
126
- // Hash Mismatch (Code Changed).
127
- // Pass migration info here too, in case category ALSO changed.
128
142
  report.reRuns.push({
129
143
  name: cName,
130
144
  oldHash: storedHash,
@@ -132,7 +146,6 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
132
146
  previousCategory: migrationOldCategory
133
147
  });
134
148
  } else if (migrationOldCategory) {
135
- // Hash Matches, BUT category changed. Force Re-run.
136
149
  report.reRuns.push({
137
150
  name: cName,
138
151
  reason: 'Category Migration',
@@ -140,7 +153,6 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
140
153
  newCategory: calc.category
141
154
  });
142
155
  } else {
143
- // Stored Hash === Current Hash AND Category matches
144
156
  report.skipped.push({ name: cName });
145
157
  }
146
158
  }
@@ -148,117 +160,97 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
148
160
  return report;
149
161
  }
150
162
 
151
- async function runDateComputation(dateStr, passToRun, calcsInThisPass, config, dependencies, computationManifest) {
163
+ /**
164
+ * DIRECT EXECUTION PIPELINE (For Workers)
165
+ * Skips analysis. Assumes the calculation is valid and runnable.
166
+ */
167
+ async function executeDispatchTask(dateStr, pass, targetComputation, config, dependencies, computationManifest) {
152
168
  const { logger } = dependencies;
153
- const orchestratorPid = generateProcessId(PROCESS_TYPES.ORCHESTRATOR, passToRun, dateStr);
154
- const dateToProcess = new Date(dateStr + 'T00:00:00Z');
169
+ const pid = generateProcessId(PROCESS_TYPES.EXECUTOR, targetComputation, dateStr);
155
170
 
156
- // 1. Fetch State
157
- const dailyStatus = await fetchComputationStatus(dateStr, config, dependencies);
158
-
159
- // 2. Check Data Availability
160
- // [UPDATE] Using centralized dates to ensure consistency with BuildReporter
161
- const rootData = await checkRootDataAvailability(dateStr, config, dependencies, DEFINITIVE_EARLIEST_DATES);
162
- const rootStatus = rootData ? rootData.status : { hasPortfolio: false, hasPrices: false, hasInsights: false, hasSocial: false, hasHistory: false };
163
-
164
- // 3. ANALYZE EXECUTION
171
+ // 1. Get Calculation Manifest
165
172
  const manifestMap = new Map(computationManifest.map(c => [normalizeName(c.name), c]));
166
- const analysisReport = analyzeDateExecution(dateStr, calcsInThisPass, rootStatus, dailyStatus, manifestMap);
173
+ const calcManifest = manifestMap.get(normalizeName(targetComputation));
167
174
 
168
- // 4. LOG ANALYSIS
169
- if (logger && typeof logger.logDateAnalysis === 'function') {
170
- logger.logDateAnalysis(dateStr, analysisReport);
171
- } else {
172
- const logMsg = `[Analysis] Date: ${dateStr} | Runnable: ${analysisReport.runnable.length} | Blocked: ${analysisReport.blocked.length} | Impossible: ${analysisReport.impossible.length}`;
173
- if (logger && logger.info) logger.info(logMsg);
174
- else console.log(logMsg);
175
+ if (!calcManifest) {
176
+ throw new Error(`Calculation '${targetComputation}' not found in manifest.`);
175
177
  }
176
178
 
177
- // 5. UPDATE STATUS FOR NON-RUNNABLE ITEMS
178
- const statusUpdates = {};
179
-
180
- analysisReport.blocked.forEach(item => statusUpdates[item.name] = { hash: false, category: 'unknown' });
181
- analysisReport.failedDependency.forEach(item => statusUpdates[item.name] = { hash: false, category: 'unknown' });
182
- analysisReport.impossible.forEach(item => statusUpdates[item.name] = { hash: STATUS_IMPOSSIBLE, category: 'unknown' });
179
+ // 2. Fetch Root Data References (Required for execution streaming)
180
+ // Even though Dispatcher checked existence, we need the actual Refs/Data objects now.
181
+ const rootData = await checkRootDataAvailability(dateStr, config, dependencies, DEFINITIVE_EARLIEST_DATES);
183
182
 
184
- if (Object.keys(statusUpdates).length > 0) {
185
- await updateComputationStatus(dateStr, statusUpdates, config, dependencies);
183
+ // Safety Fallback (Should be impossible if Dispatcher is working)
184
+ if (!rootData) {
185
+ logger.log('ERROR', `[Executor] FATAL: Root data missing for ${targetComputation} on ${dateStr}. Dispatcher desync?`);
186
+ return;
186
187
  }
187
188
 
188
- // 6. EXECUTE RUNNABLES
189
+ // 3. Fetch Dependencies
190
+ const calcsToRun = [calcManifest];
191
+ const existingResults = await fetchExistingResults(dateStr, calcsToRun, computationManifest, config, dependencies, false);
189
192
 
190
- // [SMART MIGRATION] Build map of items needing cleanup
191
- const migrationMap = {};
192
- analysisReport.reRuns.forEach(item => {
193
- if (item.previousCategory) {
194
- migrationMap[normalizeName(item.name)] = item.previousCategory;
195
- }
196
- });
197
-
198
- const calcsToRunNames = new Set([
199
- ...analysisReport.runnable.map(c => c.name),
200
- ...analysisReport.reRuns.map(c => c.name)
201
- ]);
202
-
203
- // [SMART MIGRATION] Create Safe Copies with previousCategory attached
204
- // We clone the manifest object so we don't pollute the global cache with run-specific flags
205
- const finalRunList = calcsInThisPass
206
- .filter(c => calcsToRunNames.has(normalizeName(c.name)))
207
- .map(c => {
208
- const clone = { ...c }; // Shallow copy
209
- const prevCat = migrationMap[normalizeName(c.name)];
210
- if (prevCat) {
211
- clone.previousCategory = prevCat;
212
- }
213
- return clone;
214
- });
215
-
216
- if (!finalRunList.length) {
217
- return {
218
- date: dateStr,
219
- updates: {},
220
- skipped: analysisReport.skipped.length,
221
- impossible: analysisReport.impossible.length
222
- };
193
+ let previousResults = {};
194
+ if (calcManifest.isHistorical) {
195
+ const prevDate = new Date(dateStr + 'T00:00:00Z');
196
+ prevDate.setUTCDate(prevDate.getUTCDate() - 1);
197
+ const prevDateStr = prevDate.toISOString().slice(0, 10);
198
+ previousResults = await fetchExistingResults(prevDateStr, calcsToRun, computationManifest, config, dependencies, true);
223
199
  }
224
200
 
225
- if (logger && logger.log) {
226
- logger.log('INFO', `[Orchestrator] Executing ${finalRunList.length} calculations for ${dateStr}`, { processId: orchestratorPid });
227
- }
228
-
229
- const standardToRun = finalRunList.filter(c => c.type === 'standard');
230
- const metaToRun = finalRunList.filter(c => c.type === 'meta');
201
+ // 4. Execute
202
+ logger.log('INFO', `[Executor] Running ${calcManifest.name} for ${dateStr}`, { processId: pid });
231
203
 
232
- const dateUpdates = {};
204
+ let resultUpdates = {};
233
205
 
234
206
  try {
235
- const calcsRunning = [...standardToRun, ...metaToRun];
236
-
237
- // Fetch dependencies
238
- const existingResults = await fetchExistingResults(dateStr, calcsRunning, computationManifest, config, dependencies, false);
239
- const prevDate = new Date(dateToProcess); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
240
- const prevDateStr = prevDate.toISOString().slice(0, 10);
241
- const previousResults = await fetchExistingResults(prevDateStr, calcsRunning, computationManifest, config, dependencies, true);
242
-
243
- if (standardToRun.length) {
244
- const updates = await StandardExecutor.run(dateToProcess, standardToRun, `Pass ${passToRun}`, config, dependencies, rootData, existingResults, previousResults, false);
245
- Object.assign(dateUpdates, updates);
246
- }
247
- if (metaToRun.length) {
248
- const updates = await MetaExecutor.run(dateToProcess, metaToRun, `Pass ${passToRun}`, config, dependencies, existingResults, previousResults, rootData, false);
249
- Object.assign(dateUpdates, updates);
207
+ if (calcManifest.type === 'standard') {
208
+ resultUpdates = await StandardExecutor.run(
209
+ new Date(dateStr + 'T00:00:00Z'),
210
+ [calcManifest],
211
+ `Pass ${pass}`,
212
+ config,
213
+ dependencies,
214
+ rootData,
215
+ existingResults,
216
+ previousResults
217
+ );
218
+ } else if (calcManifest.type === 'meta') {
219
+ resultUpdates = await MetaExecutor.run(
220
+ new Date(dateStr + 'T00:00:00Z'),
221
+ [calcManifest],
222
+ `Pass ${pass}`,
223
+ config,
224
+ dependencies,
225
+ existingResults,
226
+ previousResults,
227
+ rootData
228
+ );
250
229
  }
230
+
231
+ logger.log('INFO', `[Executor] Success: ${calcManifest.name} for ${dateStr}`);
232
+ return { date: dateStr, updates: resultUpdates };
251
233
 
252
234
  } catch (err) {
253
- if (logger && logger.log) {
254
- logger.log('ERROR', `[Orchestrator] Failed execution for ${dateStr}`, { processId: orchestratorPid, error: err.message });
255
- } else {
256
- console.error(`[Orchestrator] Failed execution for ${dateStr}: ${err.message}`);
257
- }
258
- throw err;
235
+ logger.log('ERROR', `[Executor] Failed ${calcManifest.name}: ${err.message}`, { processId: pid, stack: err.stack });
236
+ throw err; // Trigger retry
259
237
  }
238
+ }
260
239
 
261
- return { date: dateStr, updates: dateUpdates };
240
+ /**
241
+ * Legacy/Orchestrator Mode execution (Performs analysis).
242
+ * Kept for manual runs or full-system validation.
243
+ */
244
+ async function runDateComputation(dateStr, passToRun, calcsInThisPass, config, dependencies, computationManifest) {
245
+ // ... [Previous implementation of runDateComputation can remain here if needed for backward compatibility,
246
+ // ... or can be removed if the system is fully migrated. Keeping logic for "Dispatcher uses analyzeDateExecution"]
247
+
248
+ // Re-exporting executeDispatchTask as the primary worker entry point.
262
249
  }
263
250
 
264
- module.exports = { runDateComputation, groupByPass, analyzeDateExecution };
251
+ module.exports = {
252
+ runDateComputation,
253
+ executeDispatchTask, // <--- NEW EXPORT
254
+ groupByPass,
255
+ analyzeDateExecution
256
+ };
@@ -1,18 +1,22 @@
1
1
  /**
2
- * FILENAME: bulltrackers-module/functions/computation-system/helpers/computation_dispatcher.js
3
- * PURPOSE: Dispatches granular computation tasks (1 task = 1 calculation/date).
4
- * UPDATED: Implements "Atomic Task Dispatch" and uses DEFINITIVE dates to prevent waste.
2
+ * FILENAME: computation-system/helpers/computation_dispatcher.js
3
+ * PURPOSE: "Smart Dispatcher" - Analyzes state and only dispatches valid, runnable tasks.
4
+ * UPDATED: Implements pre-dispatch analysis to guarantee worker success.
5
5
  */
6
6
 
7
7
  const { getExpectedDateStrings, normalizeName, DEFINITIVE_EARLIEST_DATES } = require('../utils/utils.js');
8
- const { groupByPass } = require('../WorkflowOrchestrator.js');
8
+ const { groupByPass, analyzeDateExecution } = require('../WorkflowOrchestrator.js');
9
9
  const { PubSubUtils } = require('../../core/utils/pubsub_utils');
10
+ const { fetchComputationStatus, updateComputationStatus } = require('../persistence/StatusRepository');
11
+ const { checkRootDataAvailability } = require('../data/AvailabilityChecker');
12
+ const pLimit = require('p-limit');
10
13
 
11
14
  const TOPIC_NAME = 'computation-tasks';
15
+ const STATUS_IMPOSSIBLE = 'IMPOSSIBLE';
12
16
 
13
17
  /**
14
18
  * Dispatches computation tasks for a specific pass.
15
- * Generates one Pub/Sub message per calculation per date.
19
+ * Performs full pre-flight checks (Root Data, Dependencies, History) before emitting.
16
20
  */
17
21
  async function dispatchComputationPass(config, dependencies, computationManifest) {
18
22
  const { logger } = dependencies;
@@ -28,44 +32,104 @@ async function dispatchComputationPass(config, dependencies, computationManifest
28
32
  if (!calcsInThisPass.length) { return logger.log('WARN', `[Dispatcher] No calcs for Pass ${passToRun}. Exiting.`); }
29
33
 
30
34
  const calcNames = calcsInThisPass.map(c => c.name);
31
- logger.log('INFO', `🚀 [Dispatcher] Preparing PASS ${passToRun} (Granular Mode).`);
35
+ logger.log('INFO', `🚀 [Dispatcher] Smart-Dispatching PASS ${passToRun}`);
32
36
  logger.log('INFO', `[Dispatcher] Target Calculations: [${calcNames.join(', ')}]`);
33
37
 
34
38
  // 2. Determine Date Range
35
- // [UPDATE] Using DEFINITIVE_EARLIEST_DATES ensures we don't dispatch tasks
36
- // for years before data existed (e.g. 2023), saving massive Pub/Sub costs.
37
39
  const passEarliestDate = Object.values(DEFINITIVE_EARLIEST_DATES).reduce((a, b) => a < b ? a : b);
38
40
  const endDateUTC = new Date(Date.UTC(new Date().getUTCFullYear(), new Date().getUTCMonth(), new Date().getUTCDate() - 1));
39
41
  const allExpectedDates = getExpectedDateStrings(passEarliestDate, endDateUTC);
40
42
 
41
- // 3. Generate Granular Tasks (Cartesian Product: Dates x Calculations)
42
- const allTasks = [];
43
-
44
- for (const dateStr of allExpectedDates) {
45
- for (const calc of calcsInThisPass) {
46
- allTasks.push({
47
- action: 'RUN_COMPUTATION_DATE', // Maintained for compatibility
48
- date: dateStr,
49
- pass: passToRun,
50
- computation: normalizeName(calc.name), // CRITICAL: Target specific calc
51
- timestamp: Date.now()
43
+ const manifestMap = new Map(computationManifest.map(c => [normalizeName(c.name), c]));
44
+ const tasksToDispatch = [];
45
+ const limit = pLimit(20); // Process 20 days in parallel
46
+
47
+ logger.log('INFO', `[Dispatcher] Analyzing ${allExpectedDates.length} dates for viability...`);
48
+
49
+ // 3. Analyze Each Date (Concurrent)
50
+ const analysisPromises = allExpectedDates.map(dateStr => limit(async () => {
51
+ try {
52
+ // A. Fetch Status (Today)
53
+ const dailyStatus = await fetchComputationStatus(dateStr, config, dependencies);
54
+
55
+ // B. Fetch Status (Yesterday) - Only if historical continuity is needed
56
+ let prevDailyStatus = null;
57
+ if (calcsInThisPass.some(c => c.isHistorical)) {
58
+ const prevDate = new Date(dateStr + 'T00:00:00Z');
59
+ prevDate.setUTCDate(prevDate.getUTCDate() - 1);
60
+ const prevDateStr = prevDate.toISOString().slice(0, 10);
61
+ // We only care if yesterday is within valid system time
62
+ if (prevDate >= DEFINITIVE_EARLIEST_DATES.absoluteEarliest) {
63
+ prevDailyStatus = await fetchComputationStatus(prevDateStr, config, dependencies);
64
+ } else {
65
+ prevDailyStatus = {}; // Pre-epoch is effectively empty/valid context
66
+ }
67
+ }
68
+
69
+ // C. Check Root Data Availability (Real Check)
70
+ const availability = await checkRootDataAvailability(dateStr, config, dependencies, DEFINITIVE_EARLIEST_DATES);
71
+ const rootDataStatus = availability ? availability.status : {
72
+ hasPortfolio: false, hasHistory: false, hasSocial: false, hasInsights: false, hasPrices: false
73
+ };
74
+
75
+ // D. Run Core Analysis Logic
76
+ const report = analyzeDateExecution(dateStr, calcsInThisPass, rootDataStatus, dailyStatus, manifestMap, prevDailyStatus);
77
+
78
+ // E. Handle Non-Runnable States (Write directly to DB, don't dispatch)
79
+ const statusUpdates = {};
80
+
81
+ // Mark Impossible (Permanent Failure)
82
+ report.impossible.forEach(item => {
83
+ if (dailyStatus[item.name]?.hash !== STATUS_IMPOSSIBLE) {
84
+ statusUpdates[item.name] = { hash: STATUS_IMPOSSIBLE, category: 'unknown', reason: item.reason };
85
+ }
52
86
  });
53
- }
54
- }
55
87
 
56
- logger.log('INFO', `[Dispatcher] Generated ${allTasks.length} atomic tasks (${allExpectedDates.length} days × ${calcsInThisPass.length} calcs).`);
88
+ // Mark Blocked/Failed Deps (Temporary Failure)
89
+ // We write these so the status reflects reality, but we DO NOT dispatch them.
90
+ [...report.blocked, ...report.failedDependency].forEach(item => {
91
+ statusUpdates[item.name] = { hash: false, category: 'unknown', reason: item.reason };
92
+ });
93
+
94
+ if (Object.keys(statusUpdates).length > 0) {
95
+ await updateComputationStatus(dateStr, statusUpdates, config, dependencies);
96
+ }
97
+
98
+ // F. Queue Runnables
99
+ const validToRun = [...report.runnable, ...report.reRuns];
100
+ validToRun.forEach(item => {
101
+ tasksToDispatch.push({
102
+ action: 'RUN_COMPUTATION_DATE',
103
+ date: dateStr,
104
+ pass: passToRun,
105
+ computation: normalizeName(item.name),
106
+ timestamp: Date.now()
107
+ });
108
+ });
109
+
110
+ } catch (e) {
111
+ logger.log('ERROR', `[Dispatcher] Failed analysis for ${dateStr}: ${e.message}`);
112
+ }
113
+ }));
57
114
 
58
- // 4. Batch Dispatch
59
- // We send tasks in batches to Pub/Sub to be efficient,
60
- // but the WORKERS will process them individually.
61
- await pubsubUtils.batchPublishTasks(dependencies, {
62
- topicName: TOPIC_NAME,
63
- tasks: allTasks,
64
- taskType: `computation-pass-${passToRun}`,
65
- maxPubsubBatchSize: 100 // Safe batch size
66
- });
115
+ await Promise.all(analysisPromises);
67
116
 
68
- return { dispatched: allTasks.length };
117
+ // 4. Batch Dispatch Valid Tasks
118
+ if (tasksToDispatch.length > 0) {
119
+ logger.log('INFO', `[Dispatcher] ✅ Generated ${tasksToDispatch.length} VALID tasks. Dispatching...`);
120
+
121
+ await pubsubUtils.batchPublishTasks(dependencies, {
122
+ topicName: TOPIC_NAME,
123
+ tasks: tasksToDispatch,
124
+ taskType: `computation-pass-${passToRun}`,
125
+ maxPubsubBatchSize: 100
126
+ });
127
+
128
+ return { dispatched: tasksToDispatch.length };
129
+ } else {
130
+ logger.log('INFO', `[Dispatcher] No valid tasks found. System is up to date.`);
131
+ return { dispatched: 0 };
132
+ }
69
133
  }
70
134
 
71
135
  module.exports = { dispatchComputationPass };
@@ -1,27 +1,27 @@
1
1
  /**
2
2
  * FILENAME: computation-system/helpers/computation_worker.js
3
3
  * PURPOSE: Consumes computation tasks from Pub/Sub and executes them.
4
- * UPDATED: Supports Granular Execution (Single Calculation filtering).
4
+ * UPDATED: Simplified "Dumb Worker" - Trusts Dispatcher validation.
5
5
  */
6
6
 
7
- const { runDateComputation, groupByPass } = require('../WorkflowOrchestrator.js');
8
- const { getManifest } = require('../topology/ManifestLoader');
9
- const { StructuredLogger } = require('../logger/logger');
10
- const { normalizeName } = require('../utils/utils');
7
+ const { executeDispatchTask } = require('../WorkflowOrchestrator.js');
8
+ const { getManifest } = require('../topology/ManifestLoader');
9
+ const { StructuredLogger } = require('../logger/logger');
11
10
 
12
11
  // 1. IMPORT CALCULATIONS
13
12
  let calculationPackage;
14
13
  try {
15
14
  calculationPackage = require('aiden-shared-calculations-unified');
16
15
  } catch (e) {
17
- console.error("FATAL: Could not load 'aiden-shared-calculations-unified'. Ensure it is installed or linked.");
16
+ console.error("FATAL: Could not load 'aiden-shared-calculations-unified'.");
18
17
  throw e;
19
18
  }
20
19
 
21
20
  const calculations = calculationPackage.calculations;
22
21
 
23
22
  /**
24
- * Handles a single Pub/Sub message for a computation task.
23
+ * Handles a single Pub/Sub message.
24
+ * Assumes the message contains a VALID, RUNNABLE task from the Smart Dispatcher.
25
25
  */
26
26
  async function handleComputationTask(message, config, dependencies) {
27
27
 
@@ -32,36 +32,16 @@ async function handleComputationTask(message, config, dependencies) {
32
32
  ...config
33
33
  });
34
34
 
35
- // 3. OVERRIDE DEPENDENCIES
36
- const runDependencies = {
37
- ...dependencies,
38
- logger: systemLogger
39
- };
40
-
35
+ const runDependencies = { ...dependencies, logger: systemLogger };
41
36
  const { logger } = runDependencies;
42
37
 
43
- // 4. LAZY LOAD MANIFEST
44
- let computationManifest;
45
- try {
46
- computationManifest = getManifest(
47
- config.activeProductLines || [],
48
- calculations,
49
- runDependencies
50
- );
51
- } catch (manifestError) {
52
- logger.log('FATAL', `[Worker] Failed to load Manifest: ${manifestError.message}`);
53
- return;
54
- }
55
-
56
- // 5. PARSE PUB/SUB MESSAGE
38
+ // 3. PARSE PAYLOAD
57
39
  let data;
58
40
  try {
59
41
  if (message.data && message.data.message && message.data.message.data) {
60
- const buffer = Buffer.from(message.data.message.data, 'base64');
61
- data = JSON.parse(buffer.toString());
42
+ data = JSON.parse(Buffer.from(message.data.message.data, 'base64').toString());
62
43
  } else if (message.data && typeof message.data === 'string') {
63
- const buffer = Buffer.from(message.data, 'base64');
64
- data = JSON.parse(buffer.toString());
44
+ data = JSON.parse(Buffer.from(message.data, 'base64').toString());
65
45
  } else if (message.json) {
66
46
  data = message.json;
67
47
  } else {
@@ -72,66 +52,45 @@ async function handleComputationTask(message, config, dependencies) {
72
52
  return;
73
53
  }
74
54
 
75
- // 6. EXECUTE TASK
55
+ if (!data || data.action !== 'RUN_COMPUTATION_DATE') { return; }
56
+
57
+ const { date, pass, computation } = data;
58
+
59
+ if (!date || !pass || !computation) {
60
+ logger.log('ERROR', `[Worker] Invalid payload: Missing date, pass, or computation.`, data);
61
+ return;
62
+ }
63
+
64
+ // 4. LOAD MANIFEST
65
+ let computationManifest;
76
66
  try {
77
- if (!data || data.action !== 'RUN_COMPUTATION_DATE') {
78
- if (data) logger.log('WARN', `[Worker] Unknown or missing action: ${data?.action}. Ignoring.`);
79
- return;
80
- }
81
-
82
- const { date, pass, computation } = data; // Extract 'computation'
83
-
84
- if (!date || !pass) {
85
- logger.log('ERROR', `[Worker] Missing date or pass in payload: ${JSON.stringify(data)}`);
86
- return;
87
- }
88
-
89
- // Load Full Pass
90
- const passes = groupByPass(computationManifest);
91
- let calcsInThisPass = passes[pass] || [];
92
-
93
- // --- GRANULAR FILTERING ---
94
- if (computation) {
95
- const targetName = normalizeName(computation);
96
- const targetCalc = calcsInThisPass.find(c => normalizeName(c.name) === targetName);
97
-
98
- if (!targetCalc) {
99
- logger.log('WARN', `[Worker] Targeted computation '${computation}' not found in Pass ${pass}. Skipping.`);
100
- return;
101
- }
102
-
103
- // We run ONLY this calculation
104
- calcsInThisPass = [targetCalc];
105
- logger.log('INFO', `[Worker] Granular Mode: Running ONLY ${targetCalc.name} for ${date}`);
106
- } else {
107
- logger.log('INFO', `[Worker] Bulk Mode: Running ${calcsInThisPass.length} calculations for ${date}`);
108
- }
109
- // ---------------------------
67
+ computationManifest = getManifest(config.activeProductLines || [], calculations, runDependencies);
68
+ } catch (manifestError) {
69
+ logger.log('FATAL', `[Worker] Failed to load Manifest: ${manifestError.message}`);
70
+ return;
71
+ }
110
72
 
111
- if (!calcsInThisPass.length) {
112
- logger.log('WARN', `[Worker] No calculations found to run.`);
113
- return;
114
- }
73
+ // 5. EXECUTE (TRUSTED MODE)
74
+ // We do not check DB status or analyze feasibility. We assume Dispatcher did its job.
75
+ try {
76
+ logger.log('INFO', `[Worker] 📥 Received: ${computation} for ${date}`);
115
77
 
116
- const result = await runDateComputation(
78
+ const result = await executeDispatchTask(
117
79
  date,
118
80
  pass,
119
- calcsInThisPass,
81
+ computation,
120
82
  config,
121
- runDependencies,
83
+ runDependencies,
122
84
  computationManifest
123
85
  );
124
86
 
125
- if (result && result.updates && Object.keys(result.updates).length > 0) {
126
- logger.log('INFO', `[Worker] Success ${date}. Updated: ${Object.keys(result.updates).join(', ')}`);
127
- } else {
128
- // In Granular Mode, this is common (e.g. if hash matched)
129
- logger.log('INFO', `[Worker] Completed ${date} - No DB Writes (Up to date or skipped).`);
87
+ if (result && result.updates) {
88
+ logger.log('INFO', `[Worker] Stored: ${computation} for ${date}`);
130
89
  }
131
-
90
+
132
91
  } catch (err) {
133
- logger.log('ERROR', `[Worker] Fatal error processing task: ${err.message}`, { stack: err.stack });
134
- throw err; // Throwing ensures Pub/Sub retries this specific computation
92
+ logger.log('ERROR', `[Worker] Failed: ${computation} for ${date}: ${err.message}`);
93
+ throw err; // Trigger Pub/Sub retry
135
94
  }
136
95
  }
137
96
 
@@ -1,18 +1,18 @@
1
1
  /**
2
2
  * @fileoverview Build Reporter & Auto-Runner.
3
3
  * Generates a "Pre-Flight" report of what the computation system WILL do.
4
- * Simulates execution logic (Hash Mismatches) respecting DEFINITIVE start dates.
5
- * UPDATED: Implements Parallel Execution to prevent DEADLINE_EXCEEDED on 90-day scans.
4
+ * UPDATED: Removed "Smart Mocking" in favor of REAL data availability checks to detect gaps/impossible dates.
6
5
  */
7
6
 
8
7
  const { analyzeDateExecution } = require('../WorkflowOrchestrator');
9
8
  const { fetchComputationStatus } = require('../persistence/StatusRepository');
10
9
  const { normalizeName, getExpectedDateStrings, DEFINITIVE_EARLIEST_DATES } = require('../utils/utils');
10
+ const { checkRootDataAvailability } = require('../data/AvailabilityChecker');
11
11
  const { FieldValue } = require('@google-cloud/firestore');
12
12
  const pLimit = require('p-limit');
13
13
 
14
14
  // Attempt to load package.json to get version. Path depends on where this is invoked.
15
- let packageVersion = '1.0.300';
15
+ let packageVersion = '1.0.301'; // Bumped version to reflect logic change
16
16
 
17
17
 
18
18
  /**
@@ -78,7 +78,7 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
78
78
 
79
79
  // 2. PARALLEL PROCESSING (Fix for DEADLINE_EXCEEDED)
80
80
  // Run 20 reads in parallel.
81
- // 90 days / 20 concurrent = ~5 batches = ~2-3 seconds total vs 45+ seconds sequentially.
81
+ // This is now slightly heavier because we verify root data existence, but necessary for accuracy.
82
82
  const limit = pLimit(20);
83
83
 
84
84
  const processingPromises = datesToCheck.map(dateStr => limit(async () => {
@@ -86,18 +86,21 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
86
86
  // A. Fetch REAL status from DB (What ran previously?)
87
87
  const dailyStatus = await fetchComputationStatus(dateStr, config, dependencies);
88
88
 
89
- // B. SMART MOCK Root Data
90
- const dateObj = new Date(dateStr + 'T00:00:00Z');
91
- const mockRootDataStatus = {
92
- hasPortfolio: dateObj >= DEFINITIVE_EARLIEST_DATES.portfolio,
93
- hasHistory: dateObj >= DEFINITIVE_EARLIEST_DATES.history,
94
- hasSocial: dateObj >= DEFINITIVE_EARLIEST_DATES.social,
95
- hasInsights: dateObj >= DEFINITIVE_EARLIEST_DATES.insights,
96
- hasPrices: dateObj >= DEFINITIVE_EARLIEST_DATES.price
89
+ // B. REAL Root Data Check [FIXED]
90
+ // Previously we mocked this based on dates. Now we check if the data ACTUALLY exists.
91
+ // This ensures missing social data (even if after the start date) is flagged as IMPOSSIBLE.
92
+ const availability = await checkRootDataAvailability(dateStr, config, dependencies, DEFINITIVE_EARLIEST_DATES);
93
+
94
+ const rootDataStatus = availability ? availability.status : {
95
+ hasPortfolio: false,
96
+ hasHistory: false,
97
+ hasSocial: false,
98
+ hasInsights: false,
99
+ hasPrices: false
97
100
  };
98
101
 
99
102
  // C. Run Logic Analysis
100
- const analysis = analyzeDateExecution(dateStr, manifest, mockRootDataStatus, dailyStatus, manifestMap);
103
+ const analysis = analyzeDateExecution(dateStr, manifest, rootDataStatus, dailyStatus, manifestMap);
101
104
 
102
105
  // D. Format Findings
103
106
  const dateSummary = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.242",
3
+ "version": "1.0.244",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [