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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
160
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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 };
|
|
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
|
-
*
|
|
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.
|
|
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.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
//
|
|
177
|
+
// 4. Store Report
|
|
155
178
|
const reportRef = db.collection('computation_build_records').doc(buildId);
|
|
156
179
|
await reportRef.set(reportData);
|
|
157
180
|
|
|
158
|
-
//
|
|
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."
|