bulltrackers-module 1.0.224 → 1.0.226

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,18 +1,15 @@
1
1
  /**
2
2
  * @fileoverview Main Orchestrator. Coordinates the topological execution.
3
- * UPDATED: Includes comprehensive Date Analysis logic.
3
+ * UPDATED: Strict Dependency & Hash Cascade Logic with Explicit Failure Marking.
4
4
  */
5
- const { normalizeName, getExpectedDateStrings } = require('./utils/utils');
5
+ const { normalizeName } = require('./utils/utils');
6
6
  const { checkRootDataAvailability } = require('./data/AvailabilityChecker');
7
7
  const { fetchExistingResults } = require('./data/DependencyFetcher');
8
8
  const { fetchComputationStatus, updateComputationStatus } = require('./persistence/StatusRepository');
9
- const { runBatchPriceComputation } = require('./executors/PriceBatchExecutor');
10
9
  const { StandardExecutor } = require('./executors/StandardExecutor');
11
10
  const { MetaExecutor } = require('./executors/MetaExecutor');
12
11
  const { generateProcessId, PROCESS_TYPES } = require('./logger/logger');
13
12
 
14
- const PARALLEL_BATCH_SIZE = 7;
15
-
16
13
  function groupByPass(manifest) {
17
14
  return manifest.reduce((acc, calc) => {
18
15
  (acc[calc.pass] = acc[calc.pass] || []).push(calc);
@@ -21,22 +18,42 @@ function groupByPass(manifest) {
21
18
  }
22
19
 
23
20
  /**
24
- * Performs the logical analysis requested by the user.
25
- * Determines exactly what can run, what is blocked, and why.
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
26
28
  */
27
29
  function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus, manifestMap) {
28
30
  const report = {
29
31
  runnable: [],
30
- blocked: [], // Missing Root Data
31
- resolving: [], // Missing Dependency (but might be fixed by run)
32
- reRuns: [] // Hash Mismatch
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
33
36
  };
34
37
 
35
- // Helper: Is a dependency satisfied?
36
- const isDepSatisfied = (depName, dailyStatus) => {
38
+ // Helper: Is a dependency satisfied AND valid (matching hash)?
39
+ const isDepSatisfied = (depName, dailyStatus, manifestMap) => {
37
40
  const norm = normalizeName(depName);
38
- if (dailyStatus[norm]) return true; // It exists
39
- return false;
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;
40
57
  };
41
58
 
42
59
  for (const calc of calcsInPass) {
@@ -44,7 +61,7 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
44
61
  const storedHash = dailyStatus[cName];
45
62
  const currentHash = calc.hash;
46
63
 
47
- // 1. Root Data Check
64
+ // 1. Root Data Check (FATAL)
48
65
  const missingRoots = [];
49
66
  if (calc.rootDataDependencies) {
50
67
  for (const dep of calc.rootDataDependencies) {
@@ -58,54 +75,41 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
58
75
 
59
76
  if (missingRoots.length > 0) {
60
77
  report.blocked.push({ name: cName, reason: `Missing Root Data: ${missingRoots.join(', ')}` });
61
- continue;
78
+ continue; // Cannot proceed
62
79
  }
63
80
 
64
- // 2. Hash / Version Check
65
- let isReRun = false;
66
- if (storedHash && storedHash !== currentHash) {
67
- report.reRuns.push({ name: cName, oldHash: storedHash, newHash: currentHash });
68
- isReRun = true;
69
- } else if (storedHash === true) {
70
- // Legacy upgrade
71
- report.reRuns.push({ name: cName, reason: 'Legacy Upgrade' });
72
- isReRun = true;
73
- } else if (storedHash && storedHash === currentHash) {
74
- // Already done, and hashes match.
75
- // Check if we need to run implies we ignore this?
76
- // Usually we skip if done. But for "Analysis" log, we treat it as "Skipped/Done".
77
- // If the user wants to FORCE run, that's different.
78
- // Assuming standard flow: if done & hash match, we don't run.
79
- // report.blocked.push({ name: cName, reason: 'Already up to date' });
80
- // continue;
81
- }
82
-
83
- // 3. Dependency Check
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
84
  const missingDeps = [];
85
85
  if (calc.dependencies) {
86
86
  for (const dep of calc.dependencies) {
87
- // If it's a historical dependency (yesterday's data), we assume availability or check elsewhere
88
- // If it's a current pass dependency:
89
- if (!isDepSatisfied(dep, dailyStatus)) {
87
+ if (!isDepSatisfied(dep, dailyStatus, manifestMap)) {
90
88
  missingDeps.push(dep);
91
89
  }
92
90
  }
93
91
  }
94
92
 
95
93
  if (missingDeps.length > 0) {
96
- // It's missing a dependency. Is it fatal?
97
- // Since we are inside a Pass, typically dependencies are from PREVIOUS passes.
98
- // If it's from a previous pass and missing, it's blocked.
99
- // But for the sake of the report:
100
- report.resolving.push({ name: cName, missingDeps });
101
- // In strict execution, this might be "runnable" if we assume the dep runs first in this batch,
102
- // but usually dependencies are strictly lower passes.
103
- // We'll mark it as resolving/blocked.
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' });
104
110
  } else {
105
- // Dependencies met.
106
- if (!dailyStatus[cName] || isReRun) {
107
- report.runnable.push(calc);
108
- }
111
+ // Case D: Result exists, Hash Matches -> SKIP
112
+ report.skipped.push({ name: cName });
109
113
  }
110
114
  }
111
115
 
@@ -124,7 +128,7 @@ async function runDateComputation(dateStr, passToRun, calcsInThisPass, config, d
124
128
  // 2. Check Data Availability (One shot)
125
129
  const earliestDates = {
126
130
  portfolio: new Date('2025-09-25T00:00:00Z'),
127
- history: new Date('2025-11-05T00:00:00Z'), // Configurable in prod
131
+ history: new Date('2025-11-05T00:00:00Z'),
128
132
  social: new Date('2025-10-30T00:00:00Z'),
129
133
  insights: new Date('2025-08-26T00:00:00Z'),
130
134
  price: new Date('2025-08-01T00:00:00Z')
@@ -133,39 +137,48 @@ async function runDateComputation(dateStr, passToRun, calcsInThisPass, config, d
133
137
  const rootData = await checkRootDataAvailability(dateStr, config, dependencies, earliestDates);
134
138
  const rootStatus = rootData ? rootData.status : { hasPortfolio: false, hasPrices: false, hasInsights: false, hasSocial: false, hasHistory: false };
135
139
 
136
- // 3. PERFORM DATE ANALYSIS (Log the report)
140
+ // 3. ANALYZE EXECUTION
137
141
  const manifestMap = new Map(computationManifest.map(c => [normalizeName(c.name), c]));
138
142
  const analysisReport = analyzeDateExecution(dateStr, calcsInThisPass, rootStatus, dailyStatus, manifestMap);
139
143
 
140
- // LOG THE REPORT
144
+ // 4. LOG ANALYSIS
141
145
  logger.logDateAnalysis(dateStr, analysisReport);
142
146
 
143
- // 4. Filter Run List based on Analysis
144
- // We combine 'runnable' and 'reRuns'. 'resolving' are skipped (unless we support intra-pass resolution).
145
- // Note: analyzeDateExecution returns plain objects or calc objects. We need the calc objects.
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);
152
+
153
+ if (Object.keys(failureUpdates).length > 0) {
154
+ await updateComputationStatus(dateStr, failureUpdates, config, dependencies);
155
+ }
156
+
157
+ // 6. EXECUTE RUNNABLES
146
158
  const calcsToRunNames = new Set([
147
159
  ...analysisReport.runnable.map(c => c.name),
148
- ...analysisReport.reRuns.map(c => c.name) // Re-runs are valid to run
160
+ ...analysisReport.reRuns.map(c => c.name)
149
161
  ]);
150
162
 
151
163
  const finalRunList = calcsInThisPass.filter(c => calcsToRunNames.has(normalizeName(c.name)));
152
164
 
153
165
  if (!finalRunList.length) {
154
- return null;
166
+ // Nothing to run (everything either skipped, blocked, or failed)
167
+ return { date: dateStr, updates: {}, skipped: analysisReport.skipped.length };
155
168
  }
156
169
 
157
170
  logger.log('INFO', `[Orchestrator] Executing ${finalRunList.length} calculations for ${dateStr}`, { processId: orchestratorPid });
158
171
 
159
- // 5. Execution
160
172
  const standardToRun = finalRunList.filter(c => c.type === 'standard');
161
173
  const metaToRun = finalRunList.filter(c => c.type === 'meta');
162
174
 
163
175
  const dateUpdates = {};
164
176
 
165
177
  try {
166
- const calcsRunning = [...standardToRun, ...metaToRun];
178
+ const calcsRunning = [...standardToRun, ...metaToRun];
167
179
 
168
180
  // Fetch dependencies (Previous pass results)
181
+ // includeSelf=false: We are re-running, so we ignore our own old results.
169
182
  const existingResults = await fetchExistingResults(dateStr, calcsRunning, computationManifest, config, dependencies, false);
170
183
 
171
184
  // Fetch Yesterday's results (if Historical)
@@ -183,15 +196,12 @@ async function runDateComputation(dateStr, passToRun, calcsInThisPass, config, d
183
196
  }
184
197
 
185
198
  } catch (err) {
199
+ // If execution CRASHES (code bug, timeout), we log and throw.
200
+ // Pub/Sub will retry this message. This is correct for transient errors.
186
201
  logger.log('ERROR', `[Orchestrator] Failed execution for ${dateStr}`, { processId: orchestratorPid, error: err.message });
187
202
  throw err;
188
203
  }
189
204
 
190
- // 6. Status Update happens inside Executors, but we can log final success here
191
- if (Object.keys(dateUpdates).length > 0) {
192
- // await updateComputationStatus... (Handled by Executors currently)
193
- }
194
-
195
205
  return { date: dateStr, updates: dateUpdates };
196
206
  }
197
207
 
@@ -1,36 +1,41 @@
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: Correctly points to layers/index.js and wraps it for the ManifestBuilder.
4
+ * UPDATED: Fixed import source. Points to actual Calculations package, not layers.
5
5
  */
6
6
 
7
7
  const { runDateComputation, groupByPass } = require('../WorkflowOrchestrator.js');
8
8
  const { getManifest } = require('../topology/ManifestLoader');
9
9
 
10
- // 1. IMPORT CALCULATIONS
11
- // User confirmation: Calculations are barrel-loaded into layers/index.js
12
- const rawLayers = require('../layers/index');
10
+ // 1. IMPORT CALCULATIONS (CORRECTED)
11
+ // We import the specific package containing your strategies (gem, pyro, core, etc.)
12
+ // Adjust the require path if you are using a local relative path instead of node_modules.
13
+ let calculationPackage;
14
+ try {
15
+ // Primary: Try to load from the installed npm package
16
+ calculationPackage = require('aiden-shared-calculations-unified');
17
+ } catch (e) {
18
+ console.error("FATAL: Could not load 'aiden-shared-calculations-unified'. Ensure it is installed or linked.");
19
+ throw e;
20
+ }
21
+
13
22
 
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
- };
23
+ // The package exports { calculations: { ... }, utils: { ... } }
24
+ // We only need the 'calculations' object (which contains the folder-grouped classes)
25
+ const calculations = calculationPackage.calculations;
20
26
 
21
27
  /**
22
28
  * Handles a single Pub/Sub message for a computation task.
23
29
  * Supports both Gen 1 (Message) and Gen 2 (CloudEvent) formats.
24
- * * @param {object} message - The Pub/Sub message payload.
30
+ * @param {object} message - The Pub/Sub message payload.
25
31
  * @param {object} config - System configuration (must include activeProductLines).
26
32
  * @param {object} dependencies - System dependencies (logger, db, etc.).
27
33
  */
28
34
  async function handleComputationTask(message, config, dependencies) {
29
35
  const { logger } = dependencies;
30
36
 
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.
37
+ // 2. LAZY LOAD MANIFEST
38
+ // We pass the CORRECT 'calculations' object here (e.g. { core: {...}, pyro: {...} })
34
39
  let computationManifest;
35
40
  try {
36
41
  computationManifest = getManifest(
@@ -43,7 +48,7 @@ async function handleComputationTask(message, config, dependencies) {
43
48
  return;
44
49
  }
45
50
 
46
- // 4. PARSE PUB/SUB MESSAGE
51
+ // 3. PARSE PUB/SUB MESSAGE
47
52
  let data;
48
53
  try {
49
54
  if (message.data && message.data.message && message.data.message.data) {
@@ -62,7 +67,7 @@ async function handleComputationTask(message, config, dependencies) {
62
67
  return;
63
68
  }
64
69
 
65
- // 5. EXECUTE TASK
70
+ // 4. EXECUTE TASK
66
71
  try {
67
72
  if (!data || data.action !== 'RUN_COMPUTATION_DATE') {
68
73
  if (data) logger.log('WARN', `[Worker] Unknown or missing action: ${data?.action}. Ignoring.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.224",
3
+ "version": "1.0.226",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [