bulltrackers-module 1.0.281 → 1.0.282
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.
- package/functions/computation-system/onboarding.md +154 -869
- package/functions/computation-system/persistence/ResultCommitter.js +29 -12
- package/functions/computation-system/simulation/Fabricator.js +285 -0
- package/functions/computation-system/simulation/SeededRandom.js +41 -0
- package/functions/computation-system/simulation/SimRunner.js +51 -0
- package/functions/computation-system/tools/BuildReporter.js +147 -161
- package/package.json +1 -1
|
@@ -1,34 +1,32 @@
|
|
|
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
|
-
*
|
|
6
|
-
* FIXED: Ensures 'latest' pointer updates even if detail writes fail.
|
|
7
|
-
* UPDATED (IDEA 1): Added Dependency Impact Analysis ("Blast Radius").
|
|
4
|
+
* UPGRADED: Implements Behavioral Hashing (SimHash) to detect Cosmetic vs Logic changes.
|
|
5
|
+
* OPTIMIZED: Caches SimHashes and actively updates status for Stable items to prevent re-runs.
|
|
8
6
|
*/
|
|
9
7
|
|
|
10
8
|
const { analyzeDateExecution } = require('../WorkflowOrchestrator');
|
|
11
|
-
const { fetchComputationStatus }
|
|
9
|
+
const { fetchComputationStatus, updateComputationStatus } = require('../persistence/StatusRepository');
|
|
12
10
|
const { normalizeName, getExpectedDateStrings, DEFINITIVE_EARLIEST_DATES } = require('../utils/utils');
|
|
13
11
|
const { checkRootDataAvailability } = require('../data/AvailabilityChecker');
|
|
12
|
+
const SimRunner = require('../simulation/SimRunner');
|
|
14
13
|
const pLimit = require('p-limit');
|
|
15
14
|
const path = require('path');
|
|
16
15
|
const packageJson = require(path.join(__dirname, '..', '..', '..', 'package.json'));
|
|
17
16
|
const packageVersion = packageJson.version;
|
|
18
17
|
|
|
18
|
+
// Persistent Registry for SimHashes (so Workers don't have to recalc)
|
|
19
|
+
const SIMHASH_REGISTRY_COLLECTION = 'system_simhash_registry';
|
|
20
|
+
|
|
19
21
|
/**
|
|
20
|
-
* Helper: Determines if a calculation should be excluded from the report
|
|
21
|
-
* because the date is prior to the earliest possible data existence.
|
|
22
|
+
* Helper: Determines if a calculation should be excluded from the report.
|
|
22
23
|
*/
|
|
23
24
|
function isDateBeforeAvailability(dateStr, calcManifest) {
|
|
24
25
|
const targetDate = new Date(dateStr + 'T00:00:00Z');
|
|
25
26
|
const deps = calcManifest.rootDataDependencies || [];
|
|
26
|
-
|
|
27
|
-
// If no data dependencies, it's always valid (e.g., pure math)
|
|
28
27
|
if (deps.length === 0) return false;
|
|
29
28
|
|
|
30
29
|
for (const dep of deps) {
|
|
31
|
-
// Map dependency name to start date
|
|
32
30
|
let startDate = null;
|
|
33
31
|
if (dep === 'portfolio') startDate = DEFINITIVE_EARLIEST_DATES.portfolio;
|
|
34
32
|
else if (dep === 'history') startDate = DEFINITIVE_EARLIEST_DATES.history;
|
|
@@ -36,7 +34,6 @@ function isDateBeforeAvailability(dateStr, calcManifest) {
|
|
|
36
34
|
else if (dep === 'insights') startDate = DEFINITIVE_EARLIEST_DATES.insights;
|
|
37
35
|
else if (dep === 'price') startDate = DEFINITIVE_EARLIEST_DATES.price;
|
|
38
36
|
|
|
39
|
-
// If we have a start date and the target is BEFORE it, exclude this calc.
|
|
40
37
|
if (startDate && targetDate < startDate) { return true; }
|
|
41
38
|
}
|
|
42
39
|
return false;
|
|
@@ -44,17 +41,14 @@ function isDateBeforeAvailability(dateStr, calcManifest) {
|
|
|
44
41
|
|
|
45
42
|
/**
|
|
46
43
|
* Helper: Calculates the transitive closure of dependents (Blast Radius).
|
|
47
|
-
* Returns the count of direct and total cascading dependents.
|
|
48
44
|
*/
|
|
49
45
|
function calculateBlastRadius(targetCalcName, reverseGraph) {
|
|
50
46
|
const impactSet = new Set();
|
|
51
47
|
const queue = [targetCalcName];
|
|
52
48
|
|
|
53
|
-
// BFS Traversal
|
|
54
49
|
while(queue.length > 0) {
|
|
55
50
|
const current = queue.shift();
|
|
56
51
|
const dependents = reverseGraph.get(current) || [];
|
|
57
|
-
|
|
58
52
|
dependents.forEach(child => {
|
|
59
53
|
if (!impactSet.has(child)) {
|
|
60
54
|
impactSet.add(child);
|
|
@@ -66,46 +60,96 @@ function calculateBlastRadius(targetCalcName, reverseGraph) {
|
|
|
66
60
|
return {
|
|
67
61
|
directDependents: (reverseGraph.get(targetCalcName) || []).length,
|
|
68
62
|
totalCascadingDependents: impactSet.size,
|
|
69
|
-
affectedCalculations: Array.from(impactSet).slice(0, 50)
|
|
63
|
+
affectedCalculations: Array.from(impactSet).slice(0, 50)
|
|
70
64
|
};
|
|
71
65
|
}
|
|
72
66
|
|
|
67
|
+
/**
|
|
68
|
+
* [NEW] Helper: Runs SimHash check with Caching and Registry Persistence.
|
|
69
|
+
*/
|
|
70
|
+
async function verifyBehavioralStability(candidates, manifestMap, dailyStatus, logger, simHashCache, db) {
|
|
71
|
+
const trueReRuns = [];
|
|
72
|
+
const stableUpdates = [];
|
|
73
|
+
|
|
74
|
+
// Limit concurrency for simulations
|
|
75
|
+
const limit = pLimit(10);
|
|
76
|
+
|
|
77
|
+
const checks = candidates.map(item => limit(async () => {
|
|
78
|
+
try {
|
|
79
|
+
const manifest = manifestMap.get(item.name);
|
|
80
|
+
const stored = dailyStatus[item.name];
|
|
81
|
+
|
|
82
|
+
if (!stored || !stored.simHash || !manifest) {
|
|
83
|
+
trueReRuns.push(item);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 1. Check Cache first (Avoid re-simulating the same code for 100 different dates)
|
|
88
|
+
let newSimHash = simHashCache.get(manifest.hash);
|
|
89
|
+
|
|
90
|
+
// 2. If Miss, Run Simulation & Persist to Registry
|
|
91
|
+
if (!newSimHash) {
|
|
92
|
+
newSimHash = await SimRunner.run(manifest, manifestMap);
|
|
93
|
+
simHashCache.set(manifest.hash, newSimHash);
|
|
94
|
+
|
|
95
|
+
// Write to Registry so Production Workers can find it without running SimRunner
|
|
96
|
+
// Fire-and-forget write to reduce latency
|
|
97
|
+
db.collection(SIMHASH_REGISTRY_COLLECTION).doc(manifest.hash).set({
|
|
98
|
+
simHash: newSimHash,
|
|
99
|
+
createdAt: new Date(),
|
|
100
|
+
calcName: manifest.name
|
|
101
|
+
}).catch(err => logger.log('WARN', `Failed to write SimHash registry for ${manifest.name}: ${err.message}`));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 3. Compare
|
|
105
|
+
if (newSimHash === stored.simHash) {
|
|
106
|
+
// BEHAVIORAL MATCH: Code changed, but output is identical.
|
|
107
|
+
stableUpdates.push({
|
|
108
|
+
...item,
|
|
109
|
+
reason: "Code Updated (Logic Stable)",
|
|
110
|
+
simHash: newSimHash, // New SimHash (same as old)
|
|
111
|
+
newHash: manifest.hash // New Code Hash
|
|
112
|
+
});
|
|
113
|
+
} else {
|
|
114
|
+
// BEHAVIORAL MISMATCH: Logic changed.
|
|
115
|
+
trueReRuns.push({
|
|
116
|
+
...item,
|
|
117
|
+
reason: item.reason + ` [SimHash Mismatch]`,
|
|
118
|
+
oldSimHash: stored.simHash,
|
|
119
|
+
newSimHash: newSimHash
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
} catch (e) {
|
|
123
|
+
logger.log('WARN', `[BuildReporter] SimHash check failed for ${item.name}: ${e.message}`);
|
|
124
|
+
trueReRuns.push(item);
|
|
125
|
+
}
|
|
126
|
+
}));
|
|
127
|
+
|
|
128
|
+
await Promise.all(checks);
|
|
129
|
+
return { trueReRuns, stableUpdates };
|
|
130
|
+
}
|
|
131
|
+
|
|
73
132
|
/**
|
|
74
133
|
* AUTO-RUN ENTRY POINT
|
|
75
|
-
* Uses transactional locking to prevent race conditions.
|
|
76
134
|
*/
|
|
77
135
|
async function ensureBuildReport(config, dependencies, manifest) {
|
|
78
136
|
const { db, logger } = dependencies;
|
|
79
137
|
const now = new Date();
|
|
80
|
-
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')}
|
|
81
|
-
|
|
82
|
-
// Lock document specific to this version
|
|
138
|
+
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')}`;
|
|
83
139
|
const lockRef = db.collection('computation_build_records').doc(`init_lock_v${packageVersion}`);
|
|
84
140
|
|
|
85
141
|
try {
|
|
86
|
-
// Transaction: "Hey I am deploying" check
|
|
87
142
|
const shouldRun = await db.runTransaction(async (t) => {
|
|
88
|
-
const doc
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// Claim the lock
|
|
93
|
-
t.set(lockRef, {
|
|
94
|
-
status: 'IN_PROGRESS',
|
|
95
|
-
startedAt: new Date(),
|
|
96
|
-
workerId: process.env.K_REVISION || 'unknown',
|
|
97
|
-
buildId: buildId
|
|
98
|
-
});
|
|
143
|
+
const doc = await t.get(lockRef);
|
|
144
|
+
if (doc.exists) { return false; }
|
|
145
|
+
t.set(lockRef, { status: 'IN_PROGRESS', startedAt: new Date(), buildId: buildId });
|
|
99
146
|
return true;
|
|
100
147
|
});
|
|
101
148
|
|
|
102
|
-
if (!shouldRun) { logger.log('INFO', `[BuildReporter] 🔒 Report for v${packageVersion}
|
|
149
|
+
if (!shouldRun) { logger.log('INFO', `[BuildReporter] 🔒 Report for v${packageVersion} locked. Skipping.`); return; }
|
|
103
150
|
|
|
104
|
-
logger.log('INFO', `[BuildReporter] 🚀
|
|
105
|
-
|
|
151
|
+
logger.log('INFO', `[BuildReporter] 🚀 Running Pre-flight Report for v${packageVersion}...`);
|
|
106
152
|
await generateBuildReport(config, dependencies, manifest, 90, buildId);
|
|
107
|
-
|
|
108
|
-
// Optional: Update lock to completed (fire-and-forget update)
|
|
109
153
|
lockRef.update({ status: 'COMPLETED', completedAt: new Date() }).catch(() => {});
|
|
110
154
|
|
|
111
155
|
} catch (e) {
|
|
@@ -114,7 +158,7 @@ async function ensureBuildReport(config, dependencies, manifest) {
|
|
|
114
158
|
}
|
|
115
159
|
|
|
116
160
|
/**
|
|
117
|
-
* Generates the report
|
|
161
|
+
* Generates the report, writes to Firestore, AND FIXES STABLE UPDATES.
|
|
118
162
|
*/
|
|
119
163
|
async function generateBuildReport(config, dependencies, manifest, daysBack = 90, customBuildId = null) {
|
|
120
164
|
const { db, logger } = dependencies;
|
|
@@ -129,7 +173,9 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
|
|
|
129
173
|
const datesToCheck = getExpectedDateStrings(startDate, today);
|
|
130
174
|
const manifestMap = new Map(manifest.map(c => [normalizeName(c.name), c]));
|
|
131
175
|
|
|
132
|
-
// [
|
|
176
|
+
// [OPTIMIZATION] Cache SimHashes across dates so we only calculate once per code version
|
|
177
|
+
const simHashCache = new Map();
|
|
178
|
+
|
|
133
179
|
const reverseGraph = new Map();
|
|
134
180
|
manifest.forEach(c => {
|
|
135
181
|
const parentName = normalizeName(c.name);
|
|
@@ -142,17 +188,9 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
|
|
|
142
188
|
}
|
|
143
189
|
});
|
|
144
190
|
|
|
145
|
-
|
|
146
|
-
const reportHeader = {
|
|
147
|
-
buildId,
|
|
148
|
-
packageVersion: packageVersion,
|
|
149
|
-
generatedAt: new Date().toISOString(),
|
|
150
|
-
summary: {},
|
|
151
|
-
_sharded: true
|
|
152
|
-
};
|
|
191
|
+
const reportHeader = { buildId, packageVersion, generatedAt: new Date().toISOString(), summary: {}, _sharded: true };
|
|
153
192
|
|
|
154
|
-
let totalRun = 0;
|
|
155
|
-
let totalReRun = 0;
|
|
193
|
+
let totalRun = 0, totalReRun = 0, totalStable = 0;
|
|
156
194
|
const detailWrites = [];
|
|
157
195
|
|
|
158
196
|
const limit = pLimit(20);
|
|
@@ -178,93 +216,89 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
|
|
|
178
216
|
const dailyStatus = results[0];
|
|
179
217
|
const availability = results[1];
|
|
180
218
|
const prevDailyStatus = (prevDateStr && results[2]) ? results[2] : (prevDateStr ? {} : null);
|
|
181
|
-
const rootDataStatus = availability ? availability.status : { hasPortfolio: false, hasHistory: false
|
|
219
|
+
const rootDataStatus = availability ? availability.status : { hasPortfolio: false, hasHistory: false };
|
|
182
220
|
|
|
183
221
|
const analysis = analyzeDateExecution(dateStr, manifest, rootDataStatus, dailyStatus, manifestMap, prevDailyStatus);
|
|
184
222
|
|
|
185
|
-
// ---------------------------------------------------------
|
|
186
|
-
// STRICT 5-CATEGORY MAPPING
|
|
187
|
-
// ---------------------------------------------------------
|
|
188
223
|
const dateSummary = {
|
|
189
|
-
run:
|
|
190
|
-
|
|
191
|
-
blocked: [], // Missing Data (Today) / Dependency Missing
|
|
192
|
-
impossible: [], // Missing Data (Historical) / Impossible Dependency
|
|
193
|
-
uptodate: [], // Hash Match (Previously "Skipped")
|
|
194
|
-
|
|
195
|
-
// Metadata for Verification
|
|
196
|
-
meta: {
|
|
197
|
-
totalIncluded: 0,
|
|
198
|
-
totalExpected: 0,
|
|
199
|
-
match: false
|
|
200
|
-
}
|
|
224
|
+
run: [], rerun: [], stable: [], blocked: [], impossible: [], uptodate: [],
|
|
225
|
+
meta: { totalIncluded: 0, totalExpected: 0, match: false }
|
|
201
226
|
};
|
|
202
227
|
|
|
203
|
-
// Calculate Expected Count (Computations in manifest that exist for this date)
|
|
204
228
|
const expectedCount = manifest.filter(c => !isDateBeforeAvailability(dateStr, c)).length;
|
|
205
229
|
dateSummary.meta.totalExpected = expectedCount;
|
|
206
230
|
|
|
207
|
-
// Helper to push only if date is valid for this specific calc
|
|
208
231
|
const pushIfValid = (targetArray, item, extraReason = null) => {
|
|
209
232
|
const calcManifest = manifestMap.get(item.name);
|
|
210
|
-
if (calcManifest && isDateBeforeAvailability(dateStr, calcManifest))
|
|
211
|
-
return; // EXCLUDED: Date is before data exists
|
|
212
|
-
}
|
|
233
|
+
if (calcManifest && isDateBeforeAvailability(dateStr, calcManifest)) return;
|
|
213
234
|
|
|
214
235
|
const entry = {
|
|
215
|
-
name:
|
|
236
|
+
name: item.name,
|
|
216
237
|
reason: item.reason || extraReason,
|
|
217
|
-
pass:
|
|
238
|
+
pass: calcManifest ? calcManifest.pass : '?'
|
|
218
239
|
};
|
|
219
|
-
|
|
220
|
-
// [IDEA 1] If this is a Re-Run, calculate Blast Radius
|
|
221
240
|
if (targetArray === dateSummary.rerun) {
|
|
222
241
|
entry.impact = calculateBlastRadius(item.name, reverseGraph);
|
|
223
242
|
}
|
|
224
|
-
|
|
225
243
|
targetArray.push(entry);
|
|
226
244
|
};
|
|
227
245
|
|
|
228
|
-
// 1. RUN
|
|
246
|
+
// 1. RUN
|
|
229
247
|
analysis.runnable.forEach(item => pushIfValid(dateSummary.run, item, "New Calculation"));
|
|
230
248
|
|
|
231
|
-
// 2. RE-RUN (
|
|
232
|
-
analysis.reRuns.
|
|
249
|
+
// 2. RE-RUN & STABLE Analysis (SimHash Integration)
|
|
250
|
+
if (analysis.reRuns.length > 0) {
|
|
251
|
+
// Pass simHashCache and db for registry writes
|
|
252
|
+
const { trueReRuns, stableUpdates } = await verifyBehavioralStability(analysis.reRuns, manifestMap, dailyStatus, logger, simHashCache, db);
|
|
253
|
+
|
|
254
|
+
trueReRuns.forEach(item => pushIfValid(dateSummary.rerun, item, "Logic Changed"));
|
|
255
|
+
stableUpdates.forEach(item => pushIfValid(dateSummary.stable, item, "Cosmetic Change"));
|
|
256
|
+
|
|
257
|
+
// [CRITICAL FIX] "Fix the Blast Radius"
|
|
258
|
+
// If updates are STABLE, we update the status NOW.
|
|
259
|
+
// This implies: Code Hash changes, but Sim Hash stays same.
|
|
260
|
+
// The Dispatcher will see the new Code Hash in status matches the Manifest, so it won't dispatch.
|
|
261
|
+
if (stableUpdates.length > 0) {
|
|
262
|
+
const updatesPayload = {};
|
|
263
|
+
for (const stable of stableUpdates) {
|
|
264
|
+
const m = manifestMap.get(stable.name);
|
|
265
|
+
// We preserve the *existing* resultHash because the logic is proven stable.
|
|
266
|
+
// We update the 'hash' to the NEW code hash.
|
|
267
|
+
if (m && dailyStatus[stable.name]) {
|
|
268
|
+
updatesPayload[stable.name] = {
|
|
269
|
+
hash: m.hash, // New Code Hash
|
|
270
|
+
simHash: stable.simHash, // Same Sim Hash
|
|
271
|
+
resultHash: dailyStatus[stable.name].resultHash, // Same Result Hash
|
|
272
|
+
dependencyResultHashes: dailyStatus[stable.name].dependencyResultHashes || {},
|
|
273
|
+
category: m.category,
|
|
274
|
+
composition: m.composition,
|
|
275
|
+
lastUpdated: new Date()
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// Perform the "Fix"
|
|
280
|
+
if (Object.keys(updatesPayload).length > 0) {
|
|
281
|
+
await updateComputationStatus(dateStr, updatesPayload, config, dependencies);
|
|
282
|
+
logger.log('INFO', `[BuildReporter] 🩹 Fixed ${Object.keys(updatesPayload).length} stable items for ${dateStr}. They will NOT re-run.`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
233
286
|
|
|
234
|
-
// 3. BLOCKED
|
|
287
|
+
// 3. BLOCKED / IMPOSSIBLE / UPTODATE
|
|
235
288
|
analysis.blocked.forEach(item => pushIfValid(dateSummary.blocked, item));
|
|
236
289
|
analysis.failedDependency.forEach(item => pushIfValid(dateSummary.blocked, item, "Dependency Missing"));
|
|
237
|
-
|
|
238
|
-
// 4. IMPOSSIBLE (Permanent Issues)
|
|
239
290
|
analysis.impossible.forEach(item => pushIfValid(dateSummary.impossible, item));
|
|
240
|
-
|
|
241
|
-
// 5. UP-TO-DATE (Previously "Skipped")
|
|
242
291
|
analysis.skipped.forEach(item => pushIfValid(dateSummary.uptodate, item, "Up To Date"));
|
|
243
292
|
|
|
244
|
-
//
|
|
245
|
-
const includedCount = dateSummary.run.length +
|
|
246
|
-
dateSummary.
|
|
247
|
-
dateSummary.blocked.length +
|
|
248
|
-
dateSummary.impossible.length +
|
|
249
|
-
dateSummary.uptodate.length;
|
|
250
|
-
|
|
293
|
+
// Meta stats
|
|
294
|
+
const includedCount = dateSummary.run.length + dateSummary.rerun.length + dateSummary.stable.length +
|
|
295
|
+
dateSummary.blocked.length + dateSummary.impossible.length + dateSummary.uptodate.length;
|
|
251
296
|
dateSummary.meta.totalIncluded = includedCount;
|
|
252
|
-
dateSummary.meta.match
|
|
253
|
-
|
|
254
|
-
if (!dateSummary.meta.match) {
|
|
255
|
-
logger.log('WARN', `[BuildReporter] ⚠️ Mismatch on ${dateStr}: Expected ${expectedCount} but got ${includedCount}.`);
|
|
256
|
-
}
|
|
297
|
+
dateSummary.meta.match = (includedCount === expectedCount);
|
|
257
298
|
|
|
258
|
-
|
|
259
|
-
const detailRef = db.collection('computation_build_records').doc(buildId).collection('details').doc(dateStr);
|
|
260
|
-
detailWrites.push({
|
|
261
|
-
ref: detailRef,
|
|
262
|
-
data: dateSummary
|
|
263
|
-
});
|
|
299
|
+
detailWrites.push({ ref: db.collection('computation_build_records').doc(buildId).collection('details').doc(dateStr), data: dateSummary });
|
|
264
300
|
|
|
265
|
-
return {
|
|
266
|
-
stats: { run: dateSummary.run.length, rerun: dateSummary.rerun.length }
|
|
267
|
-
};
|
|
301
|
+
return { stats: { run: dateSummary.run.length, rerun: dateSummary.rerun.length, stable: dateSummary.stable.length } };
|
|
268
302
|
|
|
269
303
|
} catch (err) {
|
|
270
304
|
logger.log('ERROR', `[BuildReporter] Error analyzing date ${dateStr}: ${err.message}`);
|
|
@@ -274,69 +308,21 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
|
|
|
274
308
|
|
|
275
309
|
const results = await Promise.all(processingPromises);
|
|
276
310
|
|
|
277
|
-
results.forEach(res => {
|
|
278
|
-
if (res) {
|
|
279
|
-
totalRun += res.stats.run;
|
|
280
|
-
totalReRun += res.stats.rerun;
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
reportHeader.summary = {
|
|
285
|
-
totalReRuns: totalReRun,
|
|
286
|
-
totalNew: totalRun,
|
|
287
|
-
scanRange: `${datesToCheck[0]} to ${datesToCheck[datesToCheck.length-1]}`
|
|
288
|
-
};
|
|
311
|
+
results.forEach(res => { if (res) { totalRun += res.stats.run; totalReRun += res.stats.rerun; totalStable += res.stats.stable; } });
|
|
289
312
|
|
|
290
|
-
|
|
291
|
-
const reportRef = db.collection('computation_build_records').doc(buildId);
|
|
292
|
-
await reportRef.set(reportHeader);
|
|
313
|
+
reportHeader.summary = { totalReRuns: totalReRun, totalNew: totalRun, totalStable: totalStable, scanRange: `${datesToCheck[0]} to ${datesToCheck[datesToCheck.length-1]}` };
|
|
293
314
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
try {
|
|
301
|
-
// Concurrency limit of 15 to be safe
|
|
302
|
-
const writeLimit = pLimit(15);
|
|
303
|
-
const writePromises = detailWrites.map(w => writeLimit(() =>
|
|
304
|
-
w.ref.set(w.data).catch(e => {
|
|
305
|
-
logger.log('WARN', `[BuildReporter] Failed to write detail for ${w.ref.path}: ${e.message}`);
|
|
306
|
-
throw e;
|
|
307
|
-
})
|
|
308
|
-
));
|
|
309
|
-
|
|
310
|
-
await Promise.all(writePromises);
|
|
311
|
-
logger.log('INFO', `[BuildReporter] Successfully wrote all detail records.`);
|
|
312
|
-
|
|
313
|
-
} catch (detailErr) {
|
|
314
|
-
detailsSuccess = false;
|
|
315
|
-
logger.log('ERROR', `[BuildReporter] ⚠️ Failed to write some details. Report Header is preserved.`, detailErr);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
315
|
+
await db.collection('computation_build_records').doc(buildId).set(reportHeader);
|
|
316
|
+
|
|
317
|
+
// Parallel write details
|
|
318
|
+
const writeLimit = pLimit(15);
|
|
319
|
+
await Promise.all(detailWrites.map(w => writeLimit(() => w.ref.set(w.data))));
|
|
318
320
|
|
|
319
|
-
|
|
320
|
-
// This runs regardless of detail write success/failure
|
|
321
|
-
const latestMetadata = {
|
|
322
|
-
...reportHeader,
|
|
323
|
-
note: detailsSuccess
|
|
324
|
-
? "Latest build report pointer (See subcollection for details)."
|
|
325
|
-
: "Latest build report pointer (WARNING: Partial detail records due to write error)."
|
|
326
|
-
};
|
|
321
|
+
await db.collection('computation_build_records').doc('latest').set({ ...reportHeader, note: "Latest build report pointer." });
|
|
327
322
|
|
|
328
|
-
|
|
329
|
-
await db.collection('computation_build_records').doc('latest').set(latestMetadata);
|
|
330
|
-
logger.log('SUCCESS', `[BuildReporter] Report ${buildId} saved. Re-runs: ${totalReRun}, New: ${totalRun}. Pointer Updated.`);
|
|
331
|
-
} catch (pointerErr) {
|
|
332
|
-
logger.log('FATAL', `[BuildReporter] Failed to update 'latest' pointer!`, pointerErr);
|
|
333
|
-
}
|
|
323
|
+
logger.log('SUCCESS', `[BuildReporter] Report ${buildId} saved. Re-runs: ${totalReRun}, Stable (Fixed): ${totalStable}, New: ${totalRun}.`);
|
|
334
324
|
|
|
335
|
-
return {
|
|
336
|
-
success: true,
|
|
337
|
-
reportId: buildId,
|
|
338
|
-
summary: reportHeader.summary
|
|
339
|
-
};
|
|
325
|
+
return { success: true, reportId: buildId, summary: reportHeader.summary };
|
|
340
326
|
}
|
|
341
327
|
|
|
342
328
|
module.exports = { ensureBuildReport, generateBuildReport };
|