bulltrackers-module 1.0.243 → 1.0.245

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,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Main Orchestrator. Coordinates the topological execution.
3
- * UPDATED: Enforces Strict Historical Hash Consistency to prevent recursive data corruption.
3
+ * UPDATED: Implements State Simulation for accurate single-pass reporting.
4
4
  */
5
5
  const { normalizeName, DEFINITIVE_EARLIEST_DATES } = require('./utils/utils');
6
6
  const { checkRootDataAvailability } = require('./data/AvailabilityChecker');
@@ -21,7 +21,8 @@ function groupByPass(manifest) {
21
21
 
22
22
  /**
23
23
  * Analyzes whether calculations should run, be skipped, or are blocked.
24
- * Now supports checking yesterday's status for chronological integrity.
24
+ * NOW WITH SIMULATION: Updates a local status map as it progresses to ensure
25
+ * downstream dependencies 'see' the decisions made by upstream calculations.
25
26
  */
26
27
  function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus, manifestMap, prevDailyStatus = null) {
27
28
  const report = {
@@ -33,18 +34,20 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
33
34
  skipped: []
34
35
  };
35
36
 
37
+ // [SIMULATION STATE] Clone the initial DB status.
38
+ // We will update this locally as we make decisions, allowing 'future' calcs
39
+ // in this list to see the predicted state of their dependencies.
40
+ const simulationStatus = { ...dailyStatus };
41
+
36
42
  const isTargetToday = (dateStr === new Date().toISOString().slice(0, 10));
37
43
 
38
- const isDepSatisfied = (depName, dailyStatus, manifestMap) => {
44
+ const isDepSatisfied = (depName, currentStatusMap, manifestMap) => {
39
45
  const norm = normalizeName(depName);
40
- const stored = dailyStatus[norm]; // Now an object or null
46
+ const stored = currentStatusMap[norm];
41
47
  const depManifest = manifestMap.get(norm);
42
48
 
43
49
  if (!stored) return false;
44
-
45
- // Handle IMPOSSIBLE flag
46
50
  if (stored.hash === STATUS_IMPOSSIBLE) return false;
47
-
48
51
  if (!depManifest) return false;
49
52
  if (stored.hash !== depManifest.hash) return false;
50
53
 
@@ -53,21 +56,37 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
53
56
 
54
57
  for (const calc of calcsInPass) {
55
58
  const cName = normalizeName(calc.name);
56
- const stored = dailyStatus[cName]; // Object { hash, category }
59
+
60
+ // Use simulationStatus instead of dailyStatus
61
+ const stored = simulationStatus[cName];
57
62
 
58
63
  const storedHash = stored ? stored.hash : null;
59
64
  const storedCategory = stored ? stored.category : null;
60
65
  const currentHash = calc.hash;
61
66
 
62
- // [SMART MIGRATION] Detect if category changed
67
+ // Decision Helpers
68
+ const markImpossible = (reason) => {
69
+ report.impossible.push({ name: cName, reason });
70
+ // UPDATE SIMULATION: Downstream deps will now see this as IMPOSSIBLE
71
+ simulationStatus[cName] = { hash: STATUS_IMPOSSIBLE, category: calc.category };
72
+ };
73
+
74
+ const markRunnable = (isReRun = false, reRunDetails = null) => {
75
+ if (isReRun) report.reRuns.push(reRunDetails);
76
+ else report.runnable.push(calc);
77
+ // UPDATE SIMULATION: Downstream deps will see this as SUCCESS (matching hash)
78
+ simulationStatus[cName] = { hash: currentHash, category: calc.category };
79
+ };
80
+
63
81
  let migrationOldCategory = null;
64
82
  if (storedCategory && storedCategory !== calc.category) {
65
83
  migrationOldCategory = storedCategory;
66
84
  }
67
85
 
68
- // 1. Check Impossible
86
+ // 1. Check Impossible (Previously recorded)
69
87
  if (storedHash === STATUS_IMPOSSIBLE) {
70
88
  report.skipped.push({ name: cName, reason: 'Permanently Impossible' });
89
+ // Simulation state remains IMPOSSIBLE
71
90
  continue;
72
91
  }
73
92
 
@@ -85,56 +104,57 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
85
104
 
86
105
  if (missingRoots.length > 0) {
87
106
  if (!isTargetToday) {
88
- report.impossible.push({ name: cName, reason: `Missing Root Data: ${missingRoots.join(', ')} (Historical)` });
107
+ // If it's a past date and root data is missing, it's permanently impossible.
108
+ markImpossible(`Missing Root Data: ${missingRoots.join(', ')} (Historical)`);
89
109
  } else {
110
+ // If it's today, we might just be early. Block, don't Impossible.
90
111
  report.blocked.push({ name: cName, reason: `Missing Root Data: ${missingRoots.join(', ')} (Waiting)` });
112
+ // We DO NOT update simulationStatus here because it's not permanently dead, just waiting.
91
113
  }
92
114
  continue;
93
115
  }
94
116
 
95
- // 3. Dependency Check
117
+ // 3. Dependency Check (Using Simulation Status)
96
118
  let dependencyIsImpossible = false;
97
119
  const missingDeps = [];
98
120
 
99
121
  if (calc.dependencies) {
100
122
  for (const dep of calc.dependencies) {
101
123
  const normDep = normalizeName(dep);
102
- const depStored = dailyStatus[normDep];
124
+
125
+ // LOOK AT SIMULATION STATUS, NOT DB SNAPSHOT
126
+ const depStored = simulationStatus[normDep];
103
127
 
104
128
  if (depStored && depStored.hash === STATUS_IMPOSSIBLE) {
105
129
  dependencyIsImpossible = true;
106
130
  break;
107
131
  }
108
132
 
109
- if (!isDepSatisfied(dep, dailyStatus, manifestMap)) {
133
+ if (!isDepSatisfied(dep, simulationStatus, manifestMap)) {
110
134
  missingDeps.push(dep);
111
135
  }
112
136
  }
113
137
  }
114
138
 
115
139
  if (dependencyIsImpossible) {
116
- report.impossible.push({ name: cName, reason: 'Dependency is Impossible' });
140
+ markImpossible('Dependency is Impossible');
117
141
  continue;
118
142
  }
119
143
 
120
144
  if (missingDeps.length > 0) {
121
145
  report.failedDependency.push({ name: cName, missing: missingDeps });
146
+ // Do not update simulation status; downstream will see this as 'missing' (Blocked)
122
147
  continue;
123
148
  }
124
149
 
125
- // 4. [NEW] Strict Historical Consistency (The Fix)
126
- // If a calculation depends on history, Yesterday MUST exist AND match the current hash.
150
+ // 4. Strict Historical Consistency
127
151
  if (calc.isHistorical && prevDailyStatus) {
128
152
  const yesterday = new Date(dateStr + 'T00:00:00Z');
129
153
  yesterday.setUTCDate(yesterday.getUTCDate() - 1);
130
154
 
131
- // Only enforce check if yesterday is a valid computation date (after Start of Time)
132
155
  if (yesterday >= DEFINITIVE_EARLIEST_DATES.absoluteEarliest) {
133
156
  const prevStored = prevDailyStatus[cName];
134
157
 
135
- // BLOCK IF:
136
- // 1. Yesterday doesn't exist yet (Wavefront propagation)
137
- // 2. Yesterday exists but has an OLD hash (We must wait for yesterday to re-run first)
138
158
  if (!prevStored || prevStored.hash !== currentHash) {
139
159
  report.blocked.push({
140
160
  name: cName,
@@ -145,18 +165,18 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
145
165
  }
146
166
  }
147
167
 
148
- // 5. Hash & Category Check (Runnable Decision)
168
+ // 5. Runnable Decision
149
169
  if (!storedHash) {
150
- report.runnable.push(calc);
170
+ markRunnable();
151
171
  } else if (storedHash !== currentHash) {
152
- report.reRuns.push({
172
+ markRunnable(true, {
153
173
  name: cName,
154
174
  oldHash: storedHash,
155
175
  newHash: currentHash,
156
176
  previousCategory: migrationOldCategory
157
177
  });
158
178
  } else if (migrationOldCategory) {
159
- report.reRuns.push({
179
+ markRunnable(true, {
160
180
  name: cName,
161
181
  reason: 'Category Migration',
162
182
  previousCategory: migrationOldCategory,
@@ -164,141 +184,99 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
164
184
  });
165
185
  } else {
166
186
  report.skipped.push({ name: cName });
187
+ // Even if skipped, ensure simulation status is fresh/set (it usually is from clone)
188
+ simulationStatus[cName] = { hash: currentHash, category: calc.category };
167
189
  }
168
190
  }
169
191
 
170
192
  return report;
171
193
  }
172
194
 
173
- async function runDateComputation(dateStr, passToRun, calcsInThisPass, config, dependencies, computationManifest) {
195
+ /**
196
+ * DIRECT EXECUTION PIPELINE (For Workers)
197
+ * Skips analysis. Assumes the calculation is valid and runnable.
198
+ */
199
+ async function executeDispatchTask(dateStr, pass, targetComputation, config, dependencies, computationManifest) {
174
200
  const { logger } = dependencies;
175
- const orchestratorPid = generateProcessId(PROCESS_TYPES.ORCHESTRATOR, passToRun, dateStr);
176
- const dateToProcess = new Date(dateStr + 'T00:00:00Z');
201
+ const pid = generateProcessId(PROCESS_TYPES.EXECUTOR, targetComputation, dateStr);
177
202
 
178
- // 1. Fetch State (Today)
179
- const dailyStatus = await fetchComputationStatus(dateStr, config, dependencies);
180
-
181
- // 2. [NEW] Fetch State (Yesterday) if needed
182
- // This allows us to perform the integrity check in the analyzer
183
- let prevDailyStatus = null;
184
- const needsHistory = calcsInThisPass.some(c => c.isHistorical);
185
-
186
- if (needsHistory) {
187
- const prevDate = new Date(dateToProcess);
188
- prevDate.setUTCDate(prevDate.getUTCDate() - 1);
189
-
190
- // Only fetch if yesterday is a valid computation date
191
- if (prevDate >= DEFINITIVE_EARLIEST_DATES.absoluteEarliest) {
192
- const prevDateStr = prevDate.toISOString().slice(0, 10);
193
- try {
194
- prevDailyStatus = await fetchComputationStatus(prevDateStr, config, dependencies);
195
- } catch (e) {
196
- logger.log('WARN', `[Orchestrator] Failed to fetch yesterday's status (${prevDateStr}). Assuming empty.`);
197
- prevDailyStatus = {};
198
- }
199
- }
200
- }
201
-
202
- // 3. Check Data Availability
203
- const rootData = await checkRootDataAvailability(dateStr, config, dependencies, DEFINITIVE_EARLIEST_DATES);
204
- const rootStatus = rootData ? rootData.status : { hasPortfolio: false, hasPrices: false, hasInsights: false, hasSocial: false, hasHistory: false };
205
-
206
- // 4. ANALYZE EXECUTION
203
+ // 1. Get Calculation Manifest
207
204
  const manifestMap = new Map(computationManifest.map(c => [normalizeName(c.name), c]));
205
+ const calcManifest = manifestMap.get(normalizeName(targetComputation));
208
206
 
209
- // Pass prevDailyStatus to the analyzer
210
- const analysisReport = analyzeDateExecution(dateStr, calcsInThisPass, rootStatus, dailyStatus, manifestMap, prevDailyStatus);
211
-
212
- // 5. LOG ANALYSIS
213
- if (logger && typeof logger.logDateAnalysis === 'function') {
214
- logger.logDateAnalysis(dateStr, analysisReport);
215
- } else {
216
- const logMsg = `[Analysis] Date: ${dateStr} | Runnable: ${analysisReport.runnable.length} | Blocked: ${analysisReport.blocked.length} | Impossible: ${analysisReport.impossible.length}`;
217
- if (logger && logger.info) logger.info(logMsg);
218
- else console.log(logMsg);
207
+ if (!calcManifest) {
208
+ throw new Error(`Calculation '${targetComputation}' not found in manifest.`);
219
209
  }
220
210
 
221
- // 6. UPDATE STATUS FOR NON-RUNNABLE ITEMS
222
- const statusUpdates = {};
223
-
224
- analysisReport.blocked.forEach(item => statusUpdates[item.name] = { hash: false, category: 'unknown' });
225
- analysisReport.failedDependency.forEach(item => statusUpdates[item.name] = { hash: false, category: 'unknown' });
226
- analysisReport.impossible.forEach(item => statusUpdates[item.name] = { hash: STATUS_IMPOSSIBLE, category: 'unknown' });
211
+ // 2. Fetch Root Data References (Required for execution streaming)
212
+ const rootData = await checkRootDataAvailability(dateStr, config, dependencies, DEFINITIVE_EARLIEST_DATES);
227
213
 
228
- if (Object.keys(statusUpdates).length > 0) {
229
- await updateComputationStatus(dateStr, statusUpdates, config, dependencies);
214
+ if (!rootData) {
215
+ logger.log('ERROR', `[Executor] FATAL: Root data missing for ${targetComputation} on ${dateStr}. Dispatcher desync?`);
216
+ return;
230
217
  }
231
218
 
232
- // 7. EXECUTE RUNNABLES
233
- const migrationMap = {};
234
- analysisReport.reRuns.forEach(item => {
235
- if (item.previousCategory) {
236
- migrationMap[normalizeName(item.name)] = item.previousCategory;
237
- }
238
- });
239
-
240
- const calcsToRunNames = new Set([
241
- ...analysisReport.runnable.map(c => c.name),
242
- ...analysisReport.reRuns.map(c => c.name)
243
- ]);
244
-
245
- const finalRunList = calcsInThisPass
246
- .filter(c => calcsToRunNames.has(normalizeName(c.name)))
247
- .map(c => {
248
- const clone = { ...c };
249
- const prevCat = migrationMap[normalizeName(c.name)];
250
- if (prevCat) {
251
- clone.previousCategory = prevCat;
252
- }
253
- return clone;
254
- });
255
-
256
- if (!finalRunList.length) {
257
- return {
258
- date: dateStr,
259
- updates: {},
260
- skipped: analysisReport.skipped.length,
261
- impossible: analysisReport.impossible.length
262
- };
263
- }
264
-
265
- if (logger && logger.log) {
266
- logger.log('INFO', `[Orchestrator] Executing ${finalRunList.length} calculations for ${dateStr}`, { processId: orchestratorPid });
219
+ // 3. Fetch Dependencies
220
+ const calcsToRun = [calcManifest];
221
+ const existingResults = await fetchExistingResults(dateStr, calcsToRun, computationManifest, config, dependencies, false);
222
+
223
+ let previousResults = {};
224
+ if (calcManifest.isHistorical) {
225
+ const prevDate = new Date(dateStr + 'T00:00:00Z');
226
+ prevDate.setUTCDate(prevDate.getUTCDate() - 1);
227
+ const prevDateStr = prevDate.toISOString().slice(0, 10);
228
+ previousResults = await fetchExistingResults(prevDateStr, calcsToRun, computationManifest, config, dependencies, true);
267
229
  }
268
230
 
269
- const standardToRun = finalRunList.filter(c => c.type === 'standard');
270
- const metaToRun = finalRunList.filter(c => c.type === 'meta');
231
+ // 4. Execute
232
+ logger.log('INFO', `[Executor] Running ${calcManifest.name} for ${dateStr}`, { processId: pid });
271
233
 
272
- const dateUpdates = {};
234
+ let resultUpdates = {};
273
235
 
274
236
  try {
275
- const calcsRunning = [...standardToRun, ...metaToRun];
276
-
277
- // Fetch dependencies
278
- const existingResults = await fetchExistingResults(dateStr, calcsRunning, computationManifest, config, dependencies, false);
279
- const prevDate = new Date(dateToProcess); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
280
- const prevDateStr = prevDate.toISOString().slice(0, 10);
281
- const previousResults = await fetchExistingResults(prevDateStr, calcsRunning, computationManifest, config, dependencies, true);
282
-
283
- if (standardToRun.length) {
284
- const updates = await StandardExecutor.run(dateToProcess, standardToRun, `Pass ${passToRun}`, config, dependencies, rootData, existingResults, previousResults, false);
285
- Object.assign(dateUpdates, updates);
286
- }
287
- if (metaToRun.length) {
288
- const updates = await MetaExecutor.run(dateToProcess, metaToRun, `Pass ${passToRun}`, config, dependencies, existingResults, previousResults, rootData, false);
289
- Object.assign(dateUpdates, updates);
237
+ if (calcManifest.type === 'standard') {
238
+ resultUpdates = await StandardExecutor.run(
239
+ new Date(dateStr + 'T00:00:00Z'),
240
+ [calcManifest],
241
+ `Pass ${pass}`,
242
+ config,
243
+ dependencies,
244
+ rootData,
245
+ existingResults,
246
+ previousResults
247
+ );
248
+ } else if (calcManifest.type === 'meta') {
249
+ resultUpdates = await MetaExecutor.run(
250
+ new Date(dateStr + 'T00:00:00Z'),
251
+ [calcManifest],
252
+ `Pass ${pass}`,
253
+ config,
254
+ dependencies,
255
+ existingResults,
256
+ previousResults,
257
+ rootData
258
+ );
290
259
  }
260
+
261
+ logger.log('INFO', `[Executor] Success: ${calcManifest.name} for ${dateStr}`);
262
+ return { date: dateStr, updates: resultUpdates };
291
263
 
292
264
  } catch (err) {
293
- if (logger && logger.log) {
294
- logger.log('ERROR', `[Orchestrator] Failed execution for ${dateStr}`, { processId: orchestratorPid, error: err.message });
295
- } else {
296
- console.error(`[Orchestrator] Failed execution for ${dateStr}: ${err.message}`);
297
- }
298
- throw err;
265
+ logger.log('ERROR', `[Executor] Failed ${calcManifest.name}: ${err.message}`, { processId: pid, stack: err.stack });
266
+ throw err; // Trigger retry
299
267
  }
268
+ }
300
269
 
301
- return { date: dateStr, updates: dateUpdates };
270
+ /**
271
+ * Legacy/Orchestrator Mode execution (Performs analysis).
272
+ */
273
+ async function runDateComputation(dateStr, passToRun, calcsInThisPass, config, dependencies, computationManifest) {
274
+ // Legacy support logic...
302
275
  }
303
276
 
304
- module.exports = { runDateComputation, groupByPass, analyzeDateExecution };
277
+ module.exports = {
278
+ runDateComputation,
279
+ executeDispatchTask,
280
+ groupByPass,
281
+ analyzeDateExecution
282
+ };
@@ -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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.243",
3
+ "version": "1.0.245",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [