bulltrackers-module 1.0.269 → 1.0.271
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,40 +1,91 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Build Reporter & Auto-Runner.
|
|
3
3
|
* Generates a "Pre-Flight" report of what the computation system WILL do.
|
|
4
|
-
*
|
|
5
|
-
* UPDATED:
|
|
4
|
+
* REFACTORED: Strict 5-category reporting with date-based exclusion logic.
|
|
5
|
+
* UPDATED: Added transactional locking to prevent duplicate reports on concurrent cold starts.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const { analyzeDateExecution } = require('../WorkflowOrchestrator');
|
|
9
9
|
const { fetchComputationStatus } = require('../persistence/StatusRepository');
|
|
10
10
|
const { normalizeName, getExpectedDateStrings, DEFINITIVE_EARLIEST_DATES } = require('../utils/utils');
|
|
11
11
|
const { checkRootDataAvailability } = require('../data/AvailabilityChecker');
|
|
12
|
-
const { commitBatchInChunks } = require('../persistence/FirestoreUtils');
|
|
12
|
+
const { commitBatchInChunks } = require('../persistence/FirestoreUtils');
|
|
13
13
|
const pLimit = require('p-limit');
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const packageJson = require(path.join(__dirname, '..', '..', '..', 'package.json'));
|
|
16
16
|
const packageVersion = packageJson.version;
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Helper: Determines if a calculation should be excluded from the report
|
|
20
|
+
* because the date is prior to the earliest possible data existence.
|
|
21
|
+
*/
|
|
22
|
+
function isDateBeforeAvailability(dateStr, calcManifest) {
|
|
23
|
+
const targetDate = new Date(dateStr + 'T00:00:00Z');
|
|
24
|
+
const deps = calcManifest.rootDataDependencies || [];
|
|
25
|
+
|
|
26
|
+
// If no data dependencies, it's always valid (e.g., pure math)
|
|
27
|
+
if (deps.length === 0) return false;
|
|
28
|
+
|
|
29
|
+
for (const dep of deps) {
|
|
30
|
+
// Map dependency name to start date
|
|
31
|
+
let startDate = null;
|
|
32
|
+
if (dep === 'portfolio') startDate = DEFINITIVE_EARLIEST_DATES.portfolio;
|
|
33
|
+
else if (dep === 'history') startDate = DEFINITIVE_EARLIEST_DATES.history;
|
|
34
|
+
else if (dep === 'social') startDate = DEFINITIVE_EARLIEST_DATES.social;
|
|
35
|
+
else if (dep === 'insights') startDate = DEFINITIVE_EARLIEST_DATES.insights;
|
|
36
|
+
else if (dep === 'price') startDate = DEFINITIVE_EARLIEST_DATES.price;
|
|
37
|
+
|
|
38
|
+
// If we have a start date and the target is BEFORE it, exclude this calc.
|
|
39
|
+
if (startDate && targetDate < startDate) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
18
46
|
/**
|
|
19
47
|
* AUTO-RUN ENTRY POINT
|
|
48
|
+
* UPDATED: Uses transactional locking to prevent race conditions.
|
|
20
49
|
*/
|
|
21
50
|
async function ensureBuildReport(config, dependencies, manifest) {
|
|
22
51
|
const { db, logger } = dependencies;
|
|
23
52
|
const now = new Date();
|
|
24
53
|
const buildId = `v${packageVersion}_${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')}_${String(now.getHours()).padStart(2,'0')}-${String(now.getMinutes()).padStart(2,'0')}-${String(now.getSeconds()).padStart(2,'0')}`;
|
|
25
|
-
|
|
54
|
+
|
|
55
|
+
// Lock document specific to this version
|
|
56
|
+
const lockRef = db.collection('computation_build_records').doc(`init_lock_v${packageVersion}`);
|
|
26
57
|
|
|
27
58
|
try {
|
|
28
|
-
|
|
29
|
-
const
|
|
59
|
+
// Transaction: "Hey I am deploying" check
|
|
60
|
+
const shouldRun = await db.runTransaction(async (t) => {
|
|
61
|
+
const doc = await t.get(lockRef);
|
|
62
|
+
|
|
63
|
+
if (doc.exists) {
|
|
64
|
+
// Someone else beat us to it
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
30
67
|
|
|
31
|
-
|
|
32
|
-
|
|
68
|
+
// Claim the lock
|
|
69
|
+
t.set(lockRef, {
|
|
70
|
+
status: 'IN_PROGRESS',
|
|
71
|
+
startedAt: new Date(),
|
|
72
|
+
workerId: process.env.K_REVISION || 'unknown',
|
|
73
|
+
buildId: buildId
|
|
74
|
+
});
|
|
75
|
+
return true;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (!shouldRun) {
|
|
79
|
+
logger.log('INFO', `[BuildReporter] 🔒 Report for v${packageVersion} is already being generated (Locked). Skipping.`);
|
|
33
80
|
return;
|
|
34
81
|
}
|
|
35
82
|
|
|
36
|
-
logger.log('INFO', `[BuildReporter] 🚀
|
|
83
|
+
logger.log('INFO', `[BuildReporter] 🚀 Lock Acquired. Running Pre-flight Report for v${packageVersion}...`);
|
|
84
|
+
|
|
37
85
|
await generateBuildReport(config, dependencies, manifest, 90, buildId);
|
|
86
|
+
|
|
87
|
+
// Optional: Update lock to completed (fire-and-forget update)
|
|
88
|
+
lockRef.update({ status: 'COMPLETED', completedAt: new Date() }).catch(() => {});
|
|
38
89
|
|
|
39
90
|
} catch (e) {
|
|
40
91
|
logger.log('ERROR', `[BuildReporter] Auto-run check failed: ${e.message}`);
|
|
@@ -57,17 +108,17 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
|
|
|
57
108
|
const datesToCheck = getExpectedDateStrings(startDate, today);
|
|
58
109
|
const manifestMap = new Map(manifest.map(c => [normalizeName(c.name), c]));
|
|
59
110
|
|
|
60
|
-
// Main Report Header
|
|
111
|
+
// Main Report Header
|
|
61
112
|
const reportHeader = {
|
|
62
113
|
buildId,
|
|
63
114
|
packageVersion: packageVersion,
|
|
64
115
|
generatedAt: new Date().toISOString(),
|
|
65
|
-
summary: {},
|
|
116
|
+
summary: {},
|
|
66
117
|
_sharded: true
|
|
67
118
|
};
|
|
68
119
|
|
|
69
|
-
let
|
|
70
|
-
let
|
|
120
|
+
let totalRun = 0;
|
|
121
|
+
let totalReRun = 0;
|
|
71
122
|
const detailWrites = [];
|
|
72
123
|
|
|
73
124
|
const limit = pLimit(20);
|
|
@@ -97,31 +148,78 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
|
|
|
97
148
|
|
|
98
149
|
const analysis = analyzeDateExecution(dateStr, manifest, rootDataStatus, dailyStatus, manifestMap, prevDailyStatus);
|
|
99
150
|
|
|
100
|
-
//
|
|
101
|
-
|
|
151
|
+
// ---------------------------------------------------------
|
|
152
|
+
// STRICT 5-CATEGORY MAPPING
|
|
153
|
+
// ---------------------------------------------------------
|
|
154
|
+
const dateSummary = {
|
|
155
|
+
run: [], // New / No Hash / "Runnable"
|
|
156
|
+
rerun: [], // Hash Mismatch / Category Migration
|
|
157
|
+
blocked: [], // Missing Data (Today) / Dependency Missing
|
|
158
|
+
impossible: [], // Missing Data (Historical) / Impossible Dependency
|
|
159
|
+
uptodate: [], // Hash Match (Previously "Skipped")
|
|
160
|
+
|
|
161
|
+
// [NEW] Metadata for Verification
|
|
162
|
+
meta: {
|
|
163
|
+
totalIncluded: 0,
|
|
164
|
+
totalExpected: 0,
|
|
165
|
+
match: false
|
|
166
|
+
}
|
|
167
|
+
};
|
|
102
168
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
169
|
+
// Calculate Expected Count (Computations in manifest that exist for this date)
|
|
170
|
+
const expectedCount = manifest.filter(c => !isDateBeforeAvailability(dateStr, c)).length;
|
|
171
|
+
dateSummary.meta.totalExpected = expectedCount;
|
|
172
|
+
|
|
173
|
+
// Helper to push only if date is valid for this specific calc
|
|
174
|
+
const pushIfValid = (targetArray, item, extraReason = null) => {
|
|
175
|
+
const calcManifest = manifestMap.get(item.name);
|
|
176
|
+
if (calcManifest && isDateBeforeAvailability(dateStr, calcManifest)) {
|
|
177
|
+
return; // EXCLUDED: Date is before data exists
|
|
178
|
+
}
|
|
179
|
+
targetArray.push({ name: item.name, reason: item.reason || extraReason });
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// 1. RUN (New)
|
|
183
|
+
analysis.runnable.forEach(item => pushIfValid(dateSummary.run, item, "New Calculation"));
|
|
184
|
+
|
|
185
|
+
// 2. RE-RUN (Hash Mismatch)
|
|
186
|
+
analysis.reRuns.forEach(item => pushIfValid(dateSummary.rerun, item, "Hash Mismatch"));
|
|
187
|
+
|
|
188
|
+
// 3. BLOCKED (Temporary Issues)
|
|
189
|
+
// Merging 'blocked' and 'failedDependency' as both are temporary blocks
|
|
190
|
+
analysis.blocked.forEach(item => pushIfValid(dateSummary.blocked, item));
|
|
191
|
+
analysis.failedDependency.forEach(item => pushIfValid(dateSummary.blocked, item, "Dependency Missing"));
|
|
192
|
+
|
|
193
|
+
// 4. IMPOSSIBLE (Permanent Issues)
|
|
194
|
+
analysis.impossible.forEach(item => pushIfValid(dateSummary.impossible, item));
|
|
195
|
+
|
|
196
|
+
// 5. UP-TO-DATE (Previously "Skipped")
|
|
197
|
+
analysis.skipped.forEach(item => pushIfValid(dateSummary.uptodate, item, "Up To Date"));
|
|
198
|
+
|
|
199
|
+
// Calculate Included Count
|
|
200
|
+
const includedCount = dateSummary.run.length +
|
|
201
|
+
dateSummary.rerun.length +
|
|
202
|
+
dateSummary.blocked.length +
|
|
203
|
+
dateSummary.impossible.length +
|
|
204
|
+
dateSummary.uptodate.length;
|
|
205
|
+
|
|
206
|
+
dateSummary.meta.totalIncluded = includedCount;
|
|
207
|
+
dateSummary.meta.match = (includedCount === expectedCount);
|
|
208
|
+
|
|
209
|
+
if (!dateSummary.meta.match) {
|
|
210
|
+
logger.log('WARN', `[BuildReporter] ⚠️ Mismatch on ${dateStr}: Expected ${expectedCount} but got ${includedCount}.`);
|
|
123
211
|
}
|
|
124
|
-
|
|
212
|
+
|
|
213
|
+
// ALWAYS WRITE THE REPORT (No filtering based on activity)
|
|
214
|
+
const detailRef = db.collection('computation_build_records').doc(buildId).collection('details').doc(dateStr);
|
|
215
|
+
detailWrites.push({
|
|
216
|
+
ref: detailRef,
|
|
217
|
+
data: dateSummary
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
stats: { run: dateSummary.run.length, rerun: dateSummary.rerun.length }
|
|
222
|
+
};
|
|
125
223
|
|
|
126
224
|
} catch (err) {
|
|
127
225
|
logger.log('ERROR', `[BuildReporter] Error analyzing date ${dateStr}: ${err.message}`);
|
|
@@ -133,12 +231,16 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
|
|
|
133
231
|
|
|
134
232
|
results.forEach(res => {
|
|
135
233
|
if (res) {
|
|
136
|
-
|
|
137
|
-
|
|
234
|
+
totalRun += res.stats.run;
|
|
235
|
+
totalReRun += res.stats.rerun;
|
|
138
236
|
}
|
|
139
237
|
});
|
|
140
238
|
|
|
141
|
-
reportHeader.summary = {
|
|
239
|
+
reportHeader.summary = {
|
|
240
|
+
totalReRuns: totalReRun,
|
|
241
|
+
totalNew: totalRun,
|
|
242
|
+
scanRange: `${datesToCheck[0]} to ${datesToCheck[datesToCheck.length-1]}`
|
|
243
|
+
};
|
|
142
244
|
|
|
143
245
|
const reportRef = db.collection('computation_build_records').doc(buildId);
|
|
144
246
|
await reportRef.set(reportHeader);
|
|
@@ -150,7 +252,7 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
|
|
|
150
252
|
|
|
151
253
|
await db.collection('computation_build_records').doc('latest').set({ ...reportHeader, note: "Latest build report pointer (See subcollection for details)." });
|
|
152
254
|
|
|
153
|
-
logger.log('SUCCESS', `[BuildReporter] Report ${buildId} saved. Re-runs: ${
|
|
255
|
+
logger.log('SUCCESS', `[BuildReporter] Report ${buildId} saved. Re-runs: ${totalReRun}, New: ${totalRun}.`);
|
|
154
256
|
|
|
155
257
|
return {
|
|
156
258
|
success: true,
|