bulltrackers-module 1.0.241 → 1.0.243

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: Enforces Strict Historical Hash Consistency to prevent recursive data corruption.
5
4
  */
6
5
  const { normalizeName, DEFINITIVE_EARLIEST_DATES } = require('./utils/utils');
7
6
  const { checkRootDataAvailability } = require('./data/AvailabilityChecker');
@@ -20,7 +19,11 @@ 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
+ * Now supports checking yesterday's status for chronological integrity.
25
+ */
26
+ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus, manifestMap, prevDailyStatus = null) {
24
27
  const report = {
25
28
  runnable: [],
26
29
  blocked: [],
@@ -39,7 +42,7 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
39
42
 
40
43
  if (!stored) return false;
41
44
 
42
- // Handle IMPOSSIBLE flag (stored as object property or legacy string check)
45
+ // Handle IMPOSSIBLE flag
43
46
  if (stored.hash === STATUS_IMPOSSIBLE) return false;
44
47
 
45
48
  if (!depManifest) return false;
@@ -56,7 +59,7 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
56
59
  const storedCategory = stored ? stored.category : null;
57
60
  const currentHash = calc.hash;
58
61
 
59
- // [SMART MIGRATION] Detect if category changed, independent of hash check
62
+ // [SMART MIGRATION] Detect if category changed
60
63
  let migrationOldCategory = null;
61
64
  if (storedCategory && storedCategory !== calc.category) {
62
65
  migrationOldCategory = storedCategory;
@@ -119,12 +122,33 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
119
122
  continue;
120
123
  }
121
124
 
122
- // 4. Hash & Category Check (Smart Migration Logic)
125
+ // 4. [NEW] Strict Historical Consistency (The Fix)
126
+ // If a calculation depends on history, Yesterday MUST exist AND match the current hash.
127
+ if (calc.isHistorical && prevDailyStatus) {
128
+ const yesterday = new Date(dateStr + 'T00:00:00Z');
129
+ yesterday.setUTCDate(yesterday.getUTCDate() - 1);
130
+
131
+ // Only enforce check if yesterday is a valid computation date (after Start of Time)
132
+ if (yesterday >= DEFINITIVE_EARLIEST_DATES.absoluteEarliest) {
133
+ const prevStored = prevDailyStatus[cName];
134
+
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
+ if (!prevStored || prevStored.hash !== currentHash) {
139
+ report.blocked.push({
140
+ name: cName,
141
+ reason: `Waiting for historical continuity (Yesterday ${!prevStored ? 'Missing' : 'Hash Mismatch'})`
142
+ });
143
+ continue;
144
+ }
145
+ }
146
+ }
147
+
148
+ // 5. Hash & Category Check (Runnable Decision)
123
149
  if (!storedHash) {
124
150
  report.runnable.push(calc);
125
151
  } else if (storedHash !== currentHash) {
126
- // Hash Mismatch (Code Changed).
127
- // Pass migration info here too, in case category ALSO changed.
128
152
  report.reRuns.push({
129
153
  name: cName,
130
154
  oldHash: storedHash,
@@ -132,7 +156,6 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
132
156
  previousCategory: migrationOldCategory
133
157
  });
134
158
  } else if (migrationOldCategory) {
135
- // Hash Matches, BUT category changed. Force Re-run.
136
159
  report.reRuns.push({
137
160
  name: cName,
138
161
  reason: 'Category Migration',
@@ -140,7 +163,6 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
140
163
  newCategory: calc.category
141
164
  });
142
165
  } else {
143
- // Stored Hash === Current Hash AND Category matches
144
166
  report.skipped.push({ name: cName });
145
167
  }
146
168
  }
@@ -153,19 +175,41 @@ async function runDateComputation(dateStr, passToRun, calcsInThisPass, config, d
153
175
  const orchestratorPid = generateProcessId(PROCESS_TYPES.ORCHESTRATOR, passToRun, dateStr);
154
176
  const dateToProcess = new Date(dateStr + 'T00:00:00Z');
155
177
 
156
- // 1. Fetch State
178
+ // 1. Fetch State (Today)
157
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);
158
185
 
159
- // 2. Check Data Availability
160
- // [UPDATE] Using centralized dates to ensure consistency with BuildReporter
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
161
203
  const rootData = await checkRootDataAvailability(dateStr, config, dependencies, DEFINITIVE_EARLIEST_DATES);
162
204
  const rootStatus = rootData ? rootData.status : { hasPortfolio: false, hasPrices: false, hasInsights: false, hasSocial: false, hasHistory: false };
163
205
 
164
- // 3. ANALYZE EXECUTION
206
+ // 4. ANALYZE EXECUTION
165
207
  const manifestMap = new Map(computationManifest.map(c => [normalizeName(c.name), c]));
166
- const analysisReport = analyzeDateExecution(dateStr, calcsInThisPass, rootStatus, dailyStatus, manifestMap);
167
208
 
168
- // 4. LOG ANALYSIS
209
+ // Pass prevDailyStatus to the analyzer
210
+ const analysisReport = analyzeDateExecution(dateStr, calcsInThisPass, rootStatus, dailyStatus, manifestMap, prevDailyStatus);
211
+
212
+ // 5. LOG ANALYSIS
169
213
  if (logger && typeof logger.logDateAnalysis === 'function') {
170
214
  logger.logDateAnalysis(dateStr, analysisReport);
171
215
  } else {
@@ -174,7 +218,7 @@ async function runDateComputation(dateStr, passToRun, calcsInThisPass, config, d
174
218
  else console.log(logMsg);
175
219
  }
176
220
 
177
- // 5. UPDATE STATUS FOR NON-RUNNABLE ITEMS
221
+ // 6. UPDATE STATUS FOR NON-RUNNABLE ITEMS
178
222
  const statusUpdates = {};
179
223
 
180
224
  analysisReport.blocked.forEach(item => statusUpdates[item.name] = { hash: false, category: 'unknown' });
@@ -185,9 +229,7 @@ async function runDateComputation(dateStr, passToRun, calcsInThisPass, config, d
185
229
  await updateComputationStatus(dateStr, statusUpdates, config, dependencies);
186
230
  }
187
231
 
188
- // 6. EXECUTE RUNNABLES
189
-
190
- // [SMART MIGRATION] Build map of items needing cleanup
232
+ // 7. EXECUTE RUNNABLES
191
233
  const migrationMap = {};
192
234
  analysisReport.reRuns.forEach(item => {
193
235
  if (item.previousCategory) {
@@ -200,12 +242,10 @@ async function runDateComputation(dateStr, passToRun, calcsInThisPass, config, d
200
242
  ...analysisReport.reRuns.map(c => c.name)
201
243
  ]);
202
244
 
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
245
  const finalRunList = calcsInThisPass
206
246
  .filter(c => calcsToRunNames.has(normalizeName(c.name)))
207
247
  .map(c => {
208
- const clone = { ...c }; // Shallow copy
248
+ const clone = { ...c };
209
249
  const prevCat = migrationMap[normalizeName(c.name)];
210
250
  if (prevCat) {
211
251
  clone.previousCategory = prevCat;
@@ -1,16 +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.
4
+ * UPDATED: Removed "Smart Mocking" in favor of REAL data availability checks to detect gaps/impossible dates.
5
5
  */
6
6
 
7
7
  const { analyzeDateExecution } = require('../WorkflowOrchestrator');
8
8
  const { fetchComputationStatus } = require('../persistence/StatusRepository');
9
9
  const { normalizeName, getExpectedDateStrings, DEFINITIVE_EARLIEST_DATES } = require('../utils/utils');
10
+ const { checkRootDataAvailability } = require('../data/AvailabilityChecker');
10
11
  const { FieldValue } = require('@google-cloud/firestore');
12
+ const pLimit = require('p-limit');
11
13
 
12
14
  // Attempt to load package.json to get version. Path depends on where this is invoked.
13
- let packageVersion = '1.0.300';
15
+ let packageVersion = '1.0.301'; // Bumped version to reflect logic change
14
16
 
15
17
 
16
18
  /**
@@ -60,8 +62,6 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
60
62
  const startDate = new Date();
61
63
  startDate.setDate(today.getDate() - daysBack);
62
64
 
63
- // We check UP TO yesterday usually, as today might be partial.
64
- // But let's check today too to see immediate effects.
65
65
  const datesToCheck = getExpectedDateStrings(startDate, today);
66
66
  const manifestMap = new Map(manifest.map(c => [normalizeName(c.name), c]));
67
67
 
@@ -76,86 +76,109 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
76
76
  let totalReRuns = 0;
77
77
  let totalNew = 0;
78
78
 
79
- // 2. Iterate Dates & Simulate
80
- for (const dateStr of datesToCheck) {
81
- // A. Fetch REAL status from DB (What ran previously?)
82
- const dailyStatus = await fetchComputationStatus(dateStr, config, dependencies);
79
+ // 2. PARALLEL PROCESSING (Fix for DEADLINE_EXCEEDED)
80
+ // Run 20 reads in parallel.
81
+ // This is now slightly heavier because we verify root data existence, but necessary for accuracy.
82
+ const limit = pLimit(20);
83
83
 
84
- // B. SMART MOCK Root Data
85
- // We only mock "True" if the date is logically valid for that data type.
86
- // This prevents impossible calculations (e.g. 2025-09-11 requiring History which starts 2025-11-05)
87
- // from showing as "WillRun".
88
- const dateObj = new Date(dateStr + 'T00:00:00Z');
89
-
90
- const mockRootDataStatus = {
91
- hasPortfolio: dateObj >= DEFINITIVE_EARLIEST_DATES.portfolio,
92
- hasHistory: dateObj >= DEFINITIVE_EARLIEST_DATES.history,
93
- hasSocial: dateObj >= DEFINITIVE_EARLIEST_DATES.social,
94
- hasInsights: dateObj >= DEFINITIVE_EARLIEST_DATES.insights,
95
- hasPrices: dateObj >= DEFINITIVE_EARLIEST_DATES.price
96
- };
97
-
98
- // C. Run Logic Analysis
99
- // Pass ENTIRE manifest to see global state
100
- const analysis = analyzeDateExecution(dateStr, manifest, mockRootDataStatus, dailyStatus, manifestMap);
101
-
102
- // D. Format Findings
103
- const dateSummary = {
104
- willRun: [],
105
- willReRun: [],
106
- blocked: [],
107
- impossible: [] // New explicit category
108
- };
109
-
110
- // -- Runnable (New) --
111
- analysis.runnable.forEach(item => {
112
- dateSummary.willRun.push({ name: item.name, reason: "New / No Previous Record" });
113
- });
114
-
115
- // -- Re-Runs (Hash Mismatch / Migration) --
116
- analysis.reRuns.forEach(item => {
117
- let reason = "Hash Mismatch";
118
- let details = `Old: ${item.oldHash?.substring(0,6)}... New: ${item.newHash?.substring(0,6)}...`;
84
+ const processingPromises = datesToCheck.map(dateStr => limit(async () => {
85
+ try {
86
+ // A. Fetch REAL status from DB (What ran previously?)
87
+ const dailyStatus = await fetchComputationStatus(dateStr, config, dependencies);
88
+
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);
119
93
 
120
- if (item.previousCategory) {
121
- reason = "Migration";
122
- details = `Moving ${item.previousCategory} -> ${item.newCategory}`;
123
- }
124
-
125
- dateSummary.willReRun.push({ name: item.name, reason, details });
126
- });
127
-
128
- // -- Impossible (Permanent) --
129
- // Mapped from analysis.impossible (Missing Root Data or Explicit Impossible)
130
- analysis.impossible.forEach(item => {
131
- dateSummary.impossible.push({ name: item.name, reason: item.reason }); // e.g. "Permanently Impossible"
132
- });
133
-
134
- // -- Blocked (Retriable) --
135
- // Mapped from analysis.blocked (Waiting for data) AND failedDependency
136
- analysis.blocked.forEach(item => {
137
- dateSummary.blocked.push({ name: item.name, reason: item.reason });
138
- });
139
- analysis.failedDependency.forEach(item => {
140
- // If a dependency failed, it's blocked, not necessarily impossible (unless dependency is impossible)
141
- dateSummary.blocked.push({ name: item.name, reason: `Dependency Missing: ${item.missing.join(', ')}` });
142
- });
143
-
144
- // Only add date to report if something interesting is happening
145
- if (dateSummary.willRun.length || dateSummary.willReRun.length || dateSummary.blocked.length || dateSummary.impossible.length) {
146
- reportData.dates[dateStr] = dateSummary;
147
- totalNew += dateSummary.willRun.length;
148
- totalReRuns += dateSummary.willReRun.length;
94
+ const rootDataStatus = availability ? availability.status : {
95
+ hasPortfolio: false,
96
+ hasHistory: false,
97
+ hasSocial: false,
98
+ hasInsights: false,
99
+ hasPrices: false
100
+ };
101
+
102
+ // C. Run Logic Analysis
103
+ const analysis = analyzeDateExecution(dateStr, manifest, rootDataStatus, dailyStatus, manifestMap);
104
+
105
+ // D. Format Findings
106
+ const dateSummary = {
107
+ willRun: [],
108
+ willReRun: [],
109
+ blocked: [],
110
+ impossible: []
111
+ };
112
+
113
+ // -- Runnable (New) --
114
+ analysis.runnable.forEach(item => {
115
+ dateSummary.willRun.push({ name: item.name, reason: "New / No Previous Record" });
116
+ });
117
+
118
+ // -- Re-Runs (Hash Mismatch / Migration) --
119
+ analysis.reRuns.forEach(item => {
120
+ let reason = "Hash Mismatch";
121
+ let details = `Old: ${item.oldHash?.substring(0,6)}... New: ${item.newHash?.substring(0,6)}...`;
122
+
123
+ if (item.previousCategory) {
124
+ reason = "Migration";
125
+ details = `Moving ${item.previousCategory} -> ${item.newCategory}`;
126
+ }
127
+
128
+ dateSummary.willReRun.push({ name: item.name, reason, details });
129
+ });
130
+
131
+ // -- Impossible (Permanent) --
132
+ analysis.impossible.forEach(item => {
133
+ dateSummary.impossible.push({ name: item.name, reason: item.reason });
134
+ });
135
+
136
+ // -- Blocked (Retriable) --
137
+ analysis.blocked.forEach(item => {
138
+ dateSummary.blocked.push({ name: item.name, reason: item.reason });
139
+ });
140
+ analysis.failedDependency.forEach(item => {
141
+ dateSummary.blocked.push({ name: item.name, reason: `Dependency Missing: ${item.missing.join(', ')}` });
142
+ });
143
+
144
+ // Return result for aggregation
145
+ const hasUpdates = dateSummary.willRun.length || dateSummary.willReRun.length || dateSummary.blocked.length || dateSummary.impossible.length;
146
+
147
+ return {
148
+ dateStr,
149
+ dateSummary,
150
+ hasUpdates,
151
+ stats: {
152
+ new: dateSummary.willRun.length,
153
+ rerun: dateSummary.willReRun.length
154
+ }
155
+ };
156
+
157
+ } catch (err) {
158
+ logger.log('ERROR', `[BuildReporter] Error analyzing date ${dateStr}: ${err.message}`);
159
+ return null;
149
160
  }
150
- }
161
+ }));
162
+
163
+ // Wait for all dates to process
164
+ const results = await Promise.all(processingPromises);
165
+
166
+ // 3. Aggregate Results
167
+ results.forEach(res => {
168
+ if (res && res.hasUpdates) {
169
+ reportData.dates[res.dateStr] = res.dateSummary;
170
+ totalNew += res.stats.new;
171
+ totalReRuns += res.stats.rerun;
172
+ }
173
+ });
151
174
 
152
175
  reportData.summary = { totalReRuns, totalNew, scanRange: `${datesToCheck[0]} to ${datesToCheck[datesToCheck.length-1]}` };
153
176
 
154
- // 3. Store Report
177
+ // 4. Store Report
155
178
  const reportRef = db.collection('computation_build_records').doc(buildId);
156
179
  await reportRef.set(reportData);
157
180
 
158
- // 4. Update 'latest' pointer
181
+ // 5. Update 'latest' pointer
159
182
  await db.collection('computation_build_records').doc('latest').set({
160
183
  ...reportData,
161
184
  note: "Latest build report pointer."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.241",
3
+ "version": "1.0.243",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [