bulltrackers-module 1.0.256 → 1.0.258

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.
@@ -12,43 +12,35 @@ const { generateProcessId, PROCESS_TYPES } = require('./logger/l
12
12
 
13
13
  const STATUS_IMPOSSIBLE = 'IMPOSSIBLE';
14
14
 
15
- function groupByPass(manifest) {
16
- return manifest.reduce((acc, calc) => {
17
- (acc[calc.pass] = acc[calc.pass] || []).push(calc);
18
- return acc;
19
- }, {});
20
- }
15
+ function groupByPass(manifest) { return manifest.reduce((acc, calc) => { (acc[calc.pass] = acc[calc.pass] || []).push(calc); return acc; }, {}); }
21
16
 
22
17
  /**
23
18
  * Analyzes whether calculations should run, be skipped, or are blocked.
24
19
  */
25
20
  function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus, manifestMap, prevDailyStatus = null) {
26
- const report = { runnable: [], blocked: [], impossible: [], failedDependency: [], reRuns: [], skipped: [] };
21
+ const report = { runnable: [], blocked: [], impossible: [], failedDependency: [], reRuns: [], skipped: [] };
27
22
  const simulationStatus = { ...dailyStatus };
28
- const isTargetToday = (dateStr === new Date().toISOString().slice(0, 10));
23
+ const isTargetToday = (dateStr === new Date().toISOString().slice(0, 10));
29
24
 
30
25
  const isDepSatisfied = (depName, currentStatusMap, manifestMap) => {
31
- const norm = normalizeName(depName);
32
- const stored = currentStatusMap[norm];
26
+ const norm = normalizeName(depName);
27
+ const stored = currentStatusMap[norm];
33
28
  const depManifest = manifestMap.get(norm);
34
- if (!stored) return false;
29
+ if (!stored) return false;
35
30
  if (stored.hash === STATUS_IMPOSSIBLE) return false;
36
- if (!depManifest) return false;
37
- if (stored.hash !== depManifest.hash) return false;
31
+ if (!depManifest) return false;
32
+ if (stored.hash !== depManifest.hash) return false;
38
33
  return true;
39
34
  };
40
35
 
41
36
  for (const calc of calcsInPass) {
42
- const cName = normalizeName(calc.name);
43
- const stored = simulationStatus[cName];
44
- const storedHash = stored ? stored.hash : null;
37
+ const cName = normalizeName(calc.name);
38
+ const stored = simulationStatus[cName];
39
+ const storedHash = stored ? stored.hash : null;
45
40
  const storedCategory = stored ? stored.category : null;
46
- const currentHash = calc.hash;
41
+ const currentHash = calc.hash;
47
42
 
48
- const markImpossible = (reason) => {
49
- report.impossible.push({ name: cName, reason });
50
- simulationStatus[cName] = { hash: STATUS_IMPOSSIBLE, category: calc.category };
51
- };
43
+ const markImpossible = (reason) => { report.impossible.push({ name: cName, reason }); simulationStatus[cName] = { hash: STATUS_IMPOSSIBLE, category: calc.category }; };
52
44
 
53
45
  const markRunnable = (isReRun = false, reRunDetails = null) => {
54
46
  if (isReRun) report.reRuns.push(reRunDetails);
@@ -58,33 +50,33 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
58
50
 
59
51
  let migrationOldCategory = null;
60
52
  if (storedCategory && storedCategory !== calc.category) { migrationOldCategory = storedCategory; }
61
-
62
- if (storedHash === STATUS_IMPOSSIBLE) {
63
- report.skipped.push({ name: cName, reason: 'Permanently Impossible' });
64
- continue;
65
- }
66
-
67
- // [FIX] Use the shared, strict checking logic from AvailabilityChecker
68
- // This ensures 'speculator' calculations check 'speculatorPortfolio', etc.
53
+ if (storedHash === STATUS_IMPOSSIBLE) { report.skipped.push({ name: cName, reason: 'Permanently Impossible' }); continue; }
69
54
  const rootCheck = checkRootDependencies(calc, rootDataStatus);
70
55
 
56
+ // Check Root Data Availability
57
+ // LOGIC : Root data is essential for any calculation
58
+ // Therefore if a computation has a dependency on rootdata that does not exist for the dates the computation requires, then the computation is impossible to run.
59
+ // However, to handle edge cases where we might test trigger the computation system early, we do not mark impossible if the computation requires data for today, it might arrive later, we just block and skip.
60
+
71
61
  if (!rootCheck.canRun) {
72
62
  const missingStr = rootCheck.missing.join(', ');
73
63
  if (!isTargetToday) {
74
- // Historical missing data is usually permanent/impossible
75
64
  markImpossible(`Missing Root Data: ${missingStr} (Historical)`);
76
65
  } else {
77
- // Today's missing data might just be late (Blocked)
78
66
  report.blocked.push({ name: cName, reason: `Missing Root Data: ${missingStr} (Waiting)` });
79
67
  }
80
68
  continue;
81
69
  }
82
70
 
71
+ // Check Calculation Dependencies
72
+ // LOGIC : If a calc B depends on calc A, and calc A is impossible, then calc B is always impossible
73
+ // This has a cascading effect, if calc C depends on calc B and calc B depends on calc A and calc A is impossible, then calc B and calc C are also impossible.
74
+
83
75
  let dependencyIsImpossible = false;
84
76
  const missingDeps = [];
85
77
  if (calc.dependencies) {
86
78
  for (const dep of calc.dependencies) {
87
- const normDep = normalizeName(dep);
79
+ const normDep = normalizeName(dep);
88
80
  const depStored = simulationStatus[normDep];
89
81
  if (depStored && depStored.hash === STATUS_IMPOSSIBLE) { dependencyIsImpossible = true; break; }
90
82
  if (!isDepSatisfied(dep, simulationStatus, manifestMap)) { missingDeps.push(dep); }
@@ -94,6 +86,13 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
94
86
  if (dependencyIsImpossible) { markImpossible('Dependency is Impossible'); continue; }
95
87
  if (missingDeps.length > 0) { report.failedDependency.push({ name: cName, missing: missingDeps }); continue; }
96
88
 
89
+ // Historical Continuity Check
90
+ // LOGIC : For computations that require historical data, we process them chronologically
91
+ // This is to handle the edge case where calc B runs for Tuesday data, but requires Mondays results from calc B.
92
+ // If we triggered a hash mismatch through updating the code of calc B, it would overwrite the results for Tuesday and Monday but without this,
93
+ // it would never be guaranteed that Monday runs before Tuesday, and so Tuesday would run with the old Monday hash data, or no data.
94
+ // This fixes this edge case by ensuring that historical computations only run if the previous day's computation has run with the latest hash, if not, it blocks and waits.
95
+
97
96
  if (calc.isHistorical && prevDailyStatus) {
98
97
  const yesterday = new Date(dateStr + 'T00:00:00Z');
99
98
  yesterday.setUTCDate(yesterday.getUTCDate() - 1);
@@ -105,10 +104,13 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
105
104
  }
106
105
  }
107
106
  }
108
-
107
+ // Final Hash Comparison
108
+ // LOGIC : If the stored hash matches the current hash, we don't need to run the computation again, unless the category stored does not match the current computation category
109
+ // This is to handle the edge case where a developer changes the category of a computation, the stored results need to be moved into the new location so we trigger a re-run to move the data and also delete the old category stored data.
110
+
109
111
  if (!storedHash) { markRunnable(); }
110
112
  else if (storedHash !== currentHash) { markRunnable(true, { name: cName, oldHash: storedHash, newHash: currentHash, previousCategory: migrationOldCategory }); }
111
- else if (migrationOldCategory) { markRunnable(true, { name: cName, reason: 'Category Migration', previousCategory: migrationOldCategory, newCategory: calc.category }); }
113
+ else if (migrationOldCategory) { markRunnable(true, { name: cName, reason: 'Category Migration', previousCategory: migrationOldCategory, newCategory: calc.category }); }
112
114
  else { report.skipped.push({ name: cName }); simulationStatus[cName] = { hash: currentHash, category: calc.category }; }
113
115
  }
114
116
  return report;
@@ -117,8 +119,9 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
117
119
  /**
118
120
  * DIRECT EXECUTION PIPELINE (For Workers)
119
121
  * Skips analysis. Assumes the calculation is valid and runnable.
122
+ * [UPDATED] Accepted previousCategory argument to handle migrations.
120
123
  */
121
- async function executeDispatchTask(dateStr, pass, targetComputation, config, dependencies, computationManifest) {
124
+ async function executeDispatchTask(dateStr, pass, targetComputation, config, dependencies, computationManifest, previousCategory = null) {
122
125
  const { logger } = dependencies;
123
126
  const pid = generateProcessId(PROCESS_TYPES.EXECUTOR, targetComputation, dateStr);
124
127
 
@@ -128,8 +131,13 @@ async function executeDispatchTask(dateStr, pass, targetComputation, config, dep
128
131
 
129
132
  if (!calcManifest) { throw new Error(`Calculation '${targetComputation}' not found in manifest.`); }
130
133
 
134
+ // [UPDATED] Attach migration context if present
135
+ if (previousCategory) {
136
+ calcManifest.previousCategory = previousCategory;
137
+ logger.log('INFO', `[Executor] Migration detected for ${calcManifest.name}. Old data will be cleaned from: ${previousCategory}`);
138
+ }
139
+
131
140
  // 2. Fetch Root Data Availability
132
- // Note: this returns { status: {...}, portfolioRefs: null, historyRefs: null, ... }
133
141
  const rootData = await checkRootDataAvailability(dateStr, config, dependencies, DEFINITIVE_EARLIEST_DATES);
134
142
 
135
143
  if (!rootData) {
@@ -154,11 +162,8 @@ async function executeDispatchTask(dateStr, pass, targetComputation, config, dep
154
162
  let resultUpdates = {};
155
163
 
156
164
  try {
157
- if (calcManifest.type === 'standard') {
158
- // StandardExecutor handles the null refs in rootData by fetching on demand
159
- resultUpdates = await StandardExecutor.run(new Date(dateStr + 'T00:00:00Z'), [calcManifest], `Pass ${pass}`, config, dependencies, rootData, existingResults, previousResults);
160
- } else if (calcManifest.type === 'meta') {
161
- resultUpdates = await MetaExecutor.run(new Date(dateStr + 'T00:00:00Z'), [calcManifest], `Pass ${pass}`, config, dependencies, existingResults, previousResults, rootData);
165
+ if (calcManifest.type === 'standard') { resultUpdates = await StandardExecutor.run(new Date(dateStr + 'T00:00:00Z'), [calcManifest], `Pass ${pass}`, config, dependencies, rootData, existingResults, previousResults);
166
+ } else if (calcManifest.type === 'meta') { resultUpdates = await MetaExecutor.run (new Date(dateStr + 'T00:00:00Z'), [calcManifest], `Pass ${pass}`, config, dependencies, existingResults, previousResults, rootData);
162
167
  }
163
168
  logger.log('INFO', `[Executor] Success: ${calcManifest.name} for ${dateStr}`);
164
169
  return { date: dateStr, updates: resultUpdates };
@@ -168,6 +173,5 @@ async function executeDispatchTask(dateStr, pass, targetComputation, config, dep
168
173
  }
169
174
  }
170
175
 
171
- async function runDateComputation(dateStr, passToRun, calcsInThisPass, config, dependencies, computationManifest) { /* Legacy support stub */ }
172
176
 
173
- module.exports = { runDateComputation, executeDispatchTask, groupByPass, analyzeDateExecution };
177
+ module.exports = { executeDispatchTask, groupByPass, analyzeDateExecution };
@@ -0,0 +1,6 @@
1
+ module.exports = {
2
+ // Only add calculations here if the HeuristicValidator is too aggressive
3
+ // EXAMPLES :
4
+ // "bankruptcy-detector": { maxZeroPct: 100 }, // It's rare, so 100% 0s is fine
5
+ // "sparse-signal-generator": { maxNullPct: 99 }
6
+ };
@@ -104,6 +104,7 @@ async function dispatchComputationPass(config, dependencies, computationManifest
104
104
  pass: passToRun,
105
105
  computation: normalizeName(item.name),
106
106
  hash: item.hash || item.newHash, // [NEW] Ensure Hash is passed for Ledger
107
+ previousCategory: item.previousCategory || null, // [UPDATED] Pass migration context
107
108
  timestamp: Date.now()
108
109
  });
109
110
  });
@@ -1,12 +1,13 @@
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: Simplified "Dumb Worker" - Trusts Dispatcher validation.
4
+ * UPDATED: Integrated Run Ledger for per-run/per-date success/failure tracking.
5
5
  */
6
6
 
7
7
  const { executeDispatchTask } = require('../WorkflowOrchestrator.js');
8
8
  const { getManifest } = require('../topology/ManifestLoader');
9
9
  const { StructuredLogger } = require('../logger/logger');
10
+ const { recordRunAttempt } = require('../persistence/RunRecorder'); // [NEW IMPORT]
10
11
 
11
12
  // 1. IMPORT CALCULATIONS
12
13
  let calculationPackage;
@@ -21,7 +22,6 @@ const calculations = calculationPackage.calculations;
21
22
 
22
23
  /**
23
24
  * 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
 
@@ -33,7 +33,7 @@ async function handleComputationTask(message, config, dependencies) {
33
33
  });
34
34
 
35
35
  const runDependencies = { ...dependencies, logger: systemLogger };
36
- const { logger } = runDependencies;
36
+ const { logger, db } = runDependencies;
37
37
 
38
38
  // 3. PARSE PAYLOAD
39
39
  let data;
@@ -54,7 +54,8 @@ async function handleComputationTask(message, config, dependencies) {
54
54
 
55
55
  if (!data || data.action !== 'RUN_COMPUTATION_DATE') { return; }
56
56
 
57
- const { date, pass, computation } = data;
57
+ // [UPDATED] Destructure previousCategory from payload
58
+ const { date, pass, computation, previousCategory } = data;
58
59
 
59
60
  if (!date || !pass || !computation) {
60
61
  logger.log('ERROR', `[Worker] Invalid payload: Missing date, pass, or computation.`, data);
@@ -67,29 +68,67 @@ async function handleComputationTask(message, config, dependencies) {
67
68
  computationManifest = getManifest(config.activeProductLines || [], calculations, runDependencies);
68
69
  } catch (manifestError) {
69
70
  logger.log('FATAL', `[Worker] Failed to load Manifest: ${manifestError.message}`);
71
+ // Record Fatal Manifest Error
72
+ await recordRunAttempt(db, { date, computation, pass }, 'CRASH', {
73
+ message: manifestError.message,
74
+ stage: 'MANIFEST_LOAD'
75
+ });
70
76
  return;
71
77
  }
72
78
 
73
- // 5. EXECUTE (TRUSTED MODE)
74
- // We do not check DB status or analyze feasibility. We assume Dispatcher did its job.
79
+ // 5. EXECUTE (With Run Ledger)
75
80
  try {
76
81
  logger.log('INFO', `[Worker] 📥 Received: ${computation} for ${date}`);
77
82
 
83
+ const startTime = Date.now();
84
+ // [UPDATED] Pass previousCategory to executor
78
85
  const result = await executeDispatchTask(
79
86
  date,
80
87
  pass,
81
88
  computation,
82
89
  config,
83
90
  runDependencies,
84
- computationManifest
91
+ computationManifest,
92
+ previousCategory
85
93
  );
86
-
87
- if (result && result.updates) {
88
- logger.log('INFO', `[Worker] Stored: ${computation} for ${date}`);
94
+ const duration = Date.now() - startTime;
95
+
96
+ // CHECK FOR INTERNAL FAILURES (Trapped by ResultCommitter)
97
+ const failureReport = result?.updates?.failureReport || [];
98
+ const successUpdates = result?.updates?.successUpdates || {};
99
+
100
+ if (failureReport.length > 0) {
101
+ // Task ran, but logic or storage failed (e.g., Sharding Limit)
102
+ const failReason = failureReport[0]; // Assuming 1 calc per task
103
+
104
+ logger.log('ERROR', `[Worker] ❌ Failed logic/storage for ${computation}`, failReason.error);
105
+
106
+ await recordRunAttempt(db, { date, computation, pass }, 'FAILURE', failReason.error, { durationMs: duration });
107
+
108
+ // Throw error to ensure Pub/Sub retry (if transient) or Visibility (if permanent)
109
+ throw new Error(failReason.error.message || 'Computation Logic Failed');
110
+ }
111
+ else if (Object.keys(successUpdates).length > 0) {
112
+ // Success
113
+ logger.log('INFO', `[Worker] ✅ Stored: ${computation} for ${date}`);
114
+ await recordRunAttempt(db, { date, computation, pass }, 'SUCCESS', null, { durationMs: duration });
115
+ }
116
+ else {
117
+ // No updates, but no error (e.g. Empty Result) - Log as Success/Skipped
118
+ logger.log('WARN', `[Worker] ⚠️ No results produced for ${computation} (Empty?)`);
119
+ await recordRunAttempt(db, { date, computation, pass }, 'SUCCESS', { message: 'Empty Result' }, { durationMs: duration });
89
120
  }
90
121
 
91
122
  } catch (err) {
92
- logger.log('ERROR', `[Worker] Failed: ${computation} for ${date}: ${err.message}`);
123
+ // Catch System Crashes (OOM, Timeout, Unhandled Exception)
124
+ logger.log('ERROR', `[Worker] ❌ Crash: ${computation} for ${date}: ${err.message}`);
125
+
126
+ await recordRunAttempt(db, { date, computation, pass }, 'CRASH', {
127
+ message: err.message,
128
+ stack: err.stack,
129
+ stage: 'SYSTEM_CRASH'
130
+ });
131
+
93
132
  throw err; // Trigger Pub/Sub retry
94
133
  }
95
134
  }
@@ -1,45 +1,45 @@
1
1
  /**
2
2
  * @fileoverview Handles saving computation results with observability and Smart Cleanup.
3
- * UPDATED: Implements Audit Ledger completion logic ("Closing the Ledger").
3
+ * UPDATED: Returns detailed failure reports for the Run Ledger.
4
4
  */
5
5
  const { commitBatchInChunks } = require('./FirestoreUtils');
6
6
  const { updateComputationStatus } = require('./StatusRepository');
7
7
  const { batchStoreSchemas } = require('../utils/schema_capture');
8
8
  const { generateProcessId, PROCESS_TYPES } = require('../logger/logger');
9
- // Note: normalizeName is typically needed for doc IDs, but keys in stateObj are usually already normalized.
10
- // If not, ensure it is imported. Based on StandardExecutor, keys are normalized.
9
+
10
+ const { HeuristicValidator } = require('./ResultsValidator'); // Validator
11
+ const validationOverrides = require('../config/validation_overrides'); // Override file
11
12
 
12
13
  async function commitResults(stateObj, dStr, passName, config, deps, skipStatusWrite = false) {
13
14
  const successUpdates = {};
15
+ const failureReport = []; // [NEW] Track failures per calculation
14
16
  const schemas = [];
15
- const cleanupTasks = []; // Tasks to delete old data
17
+ const cleanupTasks = [];
16
18
  const { logger, db } = deps;
17
19
  const pid = generateProcessId(PROCESS_TYPES.STORAGE, passName, dStr);
18
20
 
19
- // [NEW] Extract numeric pass ID from string (e.g., "Pass 1" -> "1")
20
21
  const passNum = passName.replace(/[^0-9]/g, '');
21
22
 
22
23
  for (const name in stateObj) {
23
24
  const calc = stateObj[name];
24
25
  try {
25
26
  const result = await calc.getResult();
26
-
27
- // Validate Result: Check for Null, Empty Object, or Zero
28
- const isEmpty = !result ||
29
- (typeof result === 'object' && Object.keys(result).length === 0) ||
30
- (typeof result === 'number' && result === 0);
31
-
32
- if (isEmpty) {
33
- // Mark status as FALSE (Failed/Empty) so it re-runs or is flagged
34
- if (calc.manifest.hash) {
35
- successUpdates[name] = {
36
- hash: false,
37
- category: calc.manifest.category
38
- };
39
- }
40
- // Do not store empty results
41
- continue;
27
+
28
+ const overrides = validationOverrides[calc.manifest.name] || {};
29
+ const healthCheck = HeuristicValidator.analyze(calc.manifest.name, result, overrides);
30
+
31
+ if (!healthCheck.valid) {
32
+ // We throw a specific error stage so we know to BLOCK it, not retry it.
33
+ throw {
34
+ message: healthCheck.reason,
35
+ stage: 'QUALITY_CIRCUIT_BREAKER'
36
+ };
42
37
  }
38
+
39
+ // Validate Result
40
+ const isEmpty = !result || (typeof result === 'object' && Object.keys(result).length === 0) || (typeof result === 'number' && result === 0);
41
+
42
+ if (isEmpty) { if (calc.manifest.hash) { successUpdates[name] = { hash: false, category: calc.manifest.category }; } continue; }
43
43
 
44
44
  const mainDocRef = db.collection(config.resultsCollection)
45
45
  .doc(dStr)
@@ -48,9 +48,16 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
48
48
  .collection(config.computationsSubcollection)
49
49
  .doc(name);
50
50
 
51
- const updates = await prepareAutoShardedWrites(result, mainDocRef, logger);
51
+ // [CRITICAL UPDATE] Catch errors specifically during Sharding/Prep
52
+ let updates;
53
+ try {
54
+ updates = await prepareAutoShardedWrites(result, mainDocRef, logger);
55
+ } catch (prepError) {
56
+ // If this fails, it's likely a memory or logic issue before DB commit
57
+ throw { message: prepError.message, stack: prepError.stack, stage: 'PREPARE_SHARDS' };
58
+ }
52
59
 
53
- // --- [NEW] ADD AUDIT LEDGER COMPLETION TO BATCH ---
60
+ // Audit Ledger
54
61
  if (passNum && calc.manifest) {
55
62
  const ledgerRef = db.collection(`computation_audit_ledger/${dStr}/passes/${passNum}/tasks`).doc(name);
56
63
  updates.push({
@@ -64,7 +71,6 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
64
71
  options: { merge: true }
65
72
  });
66
73
  }
67
- // --------------------------------------------------
68
74
 
69
75
  // Capture Schema
70
76
  if (calc.manifest.class.getSchema) {
@@ -81,52 +87,59 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
81
87
  const totalSize = updates.reduce((acc, u) => acc + (u.data ? JSON.stringify(u.data).length : 0), 0);
82
88
  const isSharded = updates.some(u => u.data._sharded === true);
83
89
 
84
- await commitBatchInChunks(config, deps, updates, `${name} Results`);
85
-
86
- // Structured Storage Log
87
- if (logger && logger.logStorage) {
88
- logger.logStorage(pid, name, dStr, mainDocRef.path, totalSize, isSharded);
90
+ try {
91
+ await commitBatchInChunks(config, deps, updates, `${name} Results`);
92
+ } catch (commitErr) {
93
+ // Check for Firestore specific limits
94
+ let stage = 'COMMIT_BATCH';
95
+ let msg = commitErr.message;
96
+ if (msg.includes('Transaction too big') || msg.includes('payload is too large')) { stage = 'SHARDING_LIMIT_EXCEEDED'; msg = `Firestore Limit Exceeded: ${msg}`; }
97
+ throw { message: msg, stack: commitErr.stack, stage };
89
98
  }
90
99
 
91
- // Update success tracking (Include Category)
92
- if (calc.manifest.hash) {
93
- successUpdates[name] = {
94
- hash: calc.manifest.hash,
95
- category: calc.manifest.category
96
- };
97
- }
100
+ // Log Storage
101
+ if (logger && logger.logStorage) { logger.logStorage(pid, name, dStr, mainDocRef.path, totalSize, isSharded); }
102
+
103
+ // Mark Success
104
+ if (calc.manifest.hash) { successUpdates[name] = { hash: calc.manifest.hash, category: calc.manifest.category }; }
98
105
 
99
- // CHECK FOR MIGRATION CLEANUP
106
+ // Cleanup Migration
100
107
  if (calc.manifest.previousCategory && calc.manifest.previousCategory !== calc.manifest.category) {
101
- logger.log('INFO', `[Migration] Scheduled cleanup for ${name} from '${calc.manifest.previousCategory}'`);
102
108
  cleanupTasks.push(deleteOldCalculationData(dStr, calc.manifest.previousCategory, name, config, deps));
103
109
  }
104
110
  }
105
111
  } catch (e) {
106
- if (logger && logger.log) {
107
- logger.log('ERROR', `Commit failed for ${name}`, { processId: pid, error: e.message });
108
- }
112
+ // [NEW] Intelligent Failure Reporting
113
+ const stage = e.stage || 'EXECUTION';
114
+ const msg = e.message || 'Unknown error';
115
+
116
+ if (logger && logger.log) { logger.log('ERROR', `Commit failed for ${name} [${stage}]`, { processId: pid, error: msg }); }
117
+
118
+ failureReport.push({
119
+ name,
120
+ error: { message: msg, stack: e.stack, stage }
121
+ });
109
122
  }
110
123
  }
111
124
 
112
125
  if (schemas.length) batchStoreSchemas(deps, config, schemas).catch(() => {});
113
126
 
114
- // Execute Cleanup Tasks (orphaned data from category changes)
115
- if (cleanupTasks.length > 0) {
116
- await Promise.allSettled(cleanupTasks);
117
- }
127
+ if (cleanupTasks.length > 0) { await Promise.allSettled(cleanupTasks); }
118
128
 
119
- if (!skipStatusWrite && Object.keys(successUpdates).length > 0) {
120
- await updateComputationStatus(dStr, successUpdates, config, deps);
121
- }
122
- return successUpdates;
129
+ if (!skipStatusWrite && Object.keys(successUpdates).length > 0) { await updateComputationStatus(dStr, successUpdates, config, deps); }
130
+
131
+ // [UPDATE] Return both success and failures so the Worker can log them
132
+ return { successUpdates, failureReport };
123
133
  }
124
134
 
125
135
  /**
126
136
  * Deletes result documents from a previous category location.
127
- * Must handle standard docs AND sharded docs (subcollections).
137
+ * This function exists to handle the deleteion of old data,
138
+ * The general use case is that if a developer changes a calculations' category,
139
+ * We want to clean up and delete the old path, the change of a category would trigger a re-run of the calculation to naturally move the data to the new location
128
140
  */
129
141
  async function deleteOldCalculationData(dateStr, oldCategory, calcName, config, deps) {
142
+
130
143
  const { db, logger, calculationUtils } = deps;
131
144
  const { withRetry } = calculationUtils || { withRetry: (fn) => fn() };
132
145
 
@@ -138,20 +151,13 @@ async function deleteOldCalculationData(dateStr, oldCategory, calcName, config,
138
151
  .collection(config.computationsSubcollection)
139
152
  .doc(calcName);
140
153
 
141
- // 1. Check for Shards Subcollection
142
154
  const shardsCol = oldDocRef.collection('_shards');
143
155
  const shardsSnap = await withRetry(() => shardsCol.listDocuments(), 'ListOldShards');
144
156
 
145
157
  const batch = db.batch();
146
158
  let ops = 0;
147
159
 
148
- // Delete shards
149
- for (const shardDoc of shardsSnap) {
150
- batch.delete(shardDoc);
151
- ops++;
152
- }
153
-
154
- // Delete main doc
160
+ for (const shardDoc of shardsSnap) { batch.delete(shardDoc); ops++; }
155
161
  batch.delete(oldDocRef);
156
162
  ops++;
157
163
 
@@ -188,7 +194,6 @@ async function prepareAutoShardedWrites(result, docRef, logger) {
188
194
  let currentChunkSize = 0;
189
195
  let shardIndex = 0;
190
196
 
191
- // [UPDATE] Add _lastUpdated to non-sharded writes
192
197
  if ((totalSize + docPathSize) < CHUNK_LIMIT) {
193
198
  const data = {
194
199
  ...result,
@@ -0,0 +1,136 @@
1
+ /**
2
+ * @fileoverview HeuristicValidator.js
3
+ * "Grey Box" validation that infers health using statistical analysis and structural sanity checks.
4
+ * UPDATED: Added NaN detection, Flatline (Variance) checks, and Vector/Array depth checks.
5
+ */
6
+
7
+ class HeuristicValidator {
8
+ /**
9
+ * @param {string} calcName - Name for logging
10
+ * @param {Object} data - The result data to inspect
11
+ * @param {Object} [overrides] - Optional central config overrides
12
+ */
13
+ static analyze(calcName, data, overrides = {}) {
14
+ // 1. Structure Check
15
+ if (!data || typeof data !== 'object') return { valid: true }; // Let scalar types pass
16
+
17
+ const keys = Object.keys(data);
18
+ const totalItems = keys.length;
19
+
20
+ // Skip tiny datasets (statistically insignificant)
21
+ if (totalItems < 5) return { valid: true };
22
+
23
+ // 2. Sampling Configuration
24
+ const sampleSize = Math.min(totalItems, 100);
25
+ const step = Math.floor(totalItems / sampleSize);
26
+
27
+ let zeroCount = 0;
28
+ let nullCount = 0;
29
+ let nanCount = 0; // NEW: Track NaNs
30
+ let emptyVectorCount = 0; // NEW: Track empty arrays in complex objects
31
+ let analyzedCount = 0;
32
+
33
+ // For Variance/Flatline Check
34
+ const numericValues = [];
35
+
36
+ for (let i = 0; i < totalItems; i += step) {
37
+ const key = keys[i];
38
+ const val = data[key];
39
+ if (!val) { // Catch null/undefined immediately
40
+ nullCount++;
41
+ analyzedCount++;
42
+ continue;
43
+ }
44
+ analyzedCount++;
45
+
46
+ // --- TYPE A: Object / Complex Result ---
47
+ // Example: { "profile": [...], "current_price": 100 } or { "signal": "Buy", "score": 0.5 }
48
+ if (typeof val === 'object') {
49
+ const subValues = Object.values(val);
50
+
51
+ // Dead Object Check: All props are null/0/undefined
52
+ const isDeadObject = subValues.every(v => v === 0 || v === null || v === undefined);
53
+ if (isDeadObject) nullCount++;
54
+
55
+ // NaN Check in Properties
56
+ const hasNan = subValues.some(v => typeof v === 'number' && (isNaN(v) || !isFinite(v)));
57
+ if (hasNan) nanCount++;
58
+
59
+ // Vector/Profile Empty Check (Specific to your System)
60
+ // If result contains 'profile', 'history', 'sparkline', or 'buckets' arrays
61
+ const arrayProps = ['profile', 'history', 'sparkline', 'buckets', 'prices'];
62
+ for (const prop of arrayProps) {
63
+ if (Array.isArray(val[prop]) && val[prop].length === 0) {
64
+ emptyVectorCount++;
65
+ }
66
+ }
67
+
68
+ // Extract primary numeric score for Flatline check (heuristically guessing the 'main' metric)
69
+ const numericProp = subValues.find(v => typeof v === 'number' && v !== 0);
70
+ if (numericProp !== undefined) numericValues.push(numericProp);
71
+ }
72
+ // --- TYPE B: Scalar / Primitive Result ---
73
+ else if (typeof val === 'number') {
74
+ if (val === 0) zeroCount++;
75
+ if (isNaN(val) || !isFinite(val)) nanCount++;
76
+ else numericValues.push(val);
77
+ }
78
+ }
79
+
80
+ // 3. Thresholds
81
+ const thresholds = {
82
+ maxZeroPct: overrides.maxZeroPct ?? 99,
83
+ maxNullPct: overrides.maxNullPct ?? 90,
84
+ maxNanPct: overrides.maxNanPct ?? 0, // Strict: NaNs are usually bad bugs
85
+ maxFlatlinePct: 95 // If >95% of data is identical, it's suspicious
86
+ };
87
+
88
+ // 4. Calculate Stats
89
+ const zeroPct = (zeroCount / analyzedCount) * 100;
90
+ const nullPct = (nullCount / analyzedCount) * 100;
91
+ const nanPct = (nanCount / analyzedCount) * 100;
92
+
93
+ // 5. Variance / Flatline Analysis
94
+ // If we found numeric values, check if they are all the same
95
+ let isFlatline = false;
96
+ if (numericValues.length > 5) {
97
+ const first = numericValues[0];
98
+ const identicalCount = numericValues.filter(v => Math.abs(v - first) < 0.000001).length;
99
+ const flatlinePct = (identicalCount / numericValues.length) * 100;
100
+
101
+ // Only flag flatline if the value isn't 0 (0 is handled by maxZeroPct)
102
+ if (flatlinePct > thresholds.maxFlatlinePct && Math.abs(first) > 0.0001) {
103
+ isFlatline = true;
104
+ }
105
+ }
106
+
107
+ // 6. Evaluations
108
+ if (nanPct > thresholds.maxNanPct) {
109
+ return { valid: false, reason: `Mathematical Error: ${nanPct.toFixed(1)}% of sampled results contain NaN or Infinity.` };
110
+ }
111
+
112
+ if (zeroPct > thresholds.maxZeroPct) {
113
+ return { valid: false, reason: `Data Integrity: ${zeroPct.toFixed(1)}% of sampled results are 0. (Suspected Logic Failure)` };
114
+ }
115
+
116
+ if (nullPct > thresholds.maxNullPct) {
117
+ return { valid: false, reason: `Data Integrity: ${nullPct.toFixed(1)}% of sampled results are Empty/Null.` };
118
+ }
119
+
120
+ if (isFlatline) {
121
+ return { valid: false, reason: `Anomaly: Detected Result Flatline. >${thresholds.maxFlatlinePct}% of outputs are identical (non-zero).` };
122
+ }
123
+
124
+ // Special check for Distribution/Profile calculations
125
+ if (calcName.includes('profile') || calcName.includes('distribution')) {
126
+ const vectorEmptyPct = (emptyVectorCount / analyzedCount) * 100;
127
+ if (vectorEmptyPct > 90) {
128
+ return { valid: false, reason: `Data Integrity: ${vectorEmptyPct.toFixed(1)}% of distribution profiles are empty.` };
129
+ }
130
+ }
131
+
132
+ return { valid: true };
133
+ }
134
+ }
135
+
136
+ module.exports = { HeuristicValidator };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @fileoverview Utility for recording computation run attempts (The Run Ledger).
3
+ * Tracks success, failure, and error contexts for every execution pass.
4
+ */
5
+ const { generateProcessId } = require('../logger/logger');
6
+
7
+ /**
8
+ * Records a run attempt to the computation_run_history collection.
9
+ * * @param {Firestore} db - Firestore instance
10
+ * @param {Object} context - { date, computation, pass }
11
+ * @param {string} status - 'SUCCESS', 'FAILURE', or 'CRASH'
12
+ * @param {Object|null} error - Error object or null
13
+ * @param {Object} metrics - { durationMs, ... }
14
+ */
15
+ async function recordRunAttempt(db, context, status, error = null, metrics = {}) {
16
+ if (!db || !context) return;
17
+
18
+ const { date, computation, pass } = context;
19
+ // Generate a unique ID for this specific run attempt
20
+ const runId = `${Date.now()}_${generateProcessId('run', computation, date)}`;
21
+
22
+ const docRef = db.collection('computation_run_history')
23
+ .doc(date)
24
+ .collection('runs')
25
+ .doc(runId);
26
+
27
+ const entry = {
28
+ computationName: computation,
29
+ date: date,
30
+ pass: String(pass),
31
+ timestamp: new Date().toISOString(),
32
+ status: status,
33
+ metrics: metrics
34
+ };
35
+
36
+ if (error) {
37
+ entry.error = {
38
+ message: error.message || 'Unknown Error',
39
+ // Capture specific sharding/firestore stages if available
40
+ stage: error.stage || 'UNKNOWN',
41
+ code: error.code || null,
42
+ stack: error.stack || null
43
+ };
44
+ }
45
+
46
+ // Fire and forget (await but catch to ensure logging doesn't crash the worker)
47
+ try {
48
+ await docRef.set(entry);
49
+ } catch (e) {
50
+ console.error(`[RunRecorder] Failed to save history for ${computation}:`, e.message);
51
+ }
52
+ }
53
+
54
+ module.exports = { recordRunAttempt };
package/index.js CHANGED
@@ -27,7 +27,7 @@ const { handleUpdate } = require('./functions
27
27
 
28
28
  // Computation System
29
29
  const { build: buildManifest } = require('./functions/computation-system/context/ManifestBuilder');
30
- const { runDateComputation: runComputationPass } = require('./functions/computation-system/WorkflowOrchestrator');
30
+ // const { runDateComputation: runComputationPass } = require('./functions/computation-system/WorkflowOrchestrator'); Depreciated
31
31
  const { dispatchComputationPass } = require('./functions/computation-system/helpers/computation_dispatcher');
32
32
  const { handleComputationTask } = require('./functions/computation-system/helpers/computation_worker');
33
33
  // [NEW] Import Report Tools
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.256",
3
+ "version": "1.0.258",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [