bulltrackers-module 1.0.278 → 1.0.280
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/executors/StandardExecutor.js +40 -5
- package/functions/computation-system/persistence/ResultCommitter.js +4 -7
- package/functions/computation-system/persistence/RunRecorder.js +17 -3
- package/functions/computation-system/tools/BuildReporter.js +80 -22
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* UPDATED: Implements Batch Flushing to prevent OOM on large datasets.
|
|
4
4
|
* UPDATED: Removes manual global.gc() calls.
|
|
5
5
|
* UPDATED: Manages incremental sharding states.
|
|
6
|
+
* UPDATED (IDEA 2): Implemented Computation Profiler (timings).
|
|
6
7
|
*/
|
|
7
8
|
const { normalizeName } = require('../utils/utils');
|
|
8
9
|
const { streamPortfolioData, streamHistoryData, getPortfolioPartRefs } = require('../utils/data_loader');
|
|
@@ -10,6 +11,7 @@ const { CachedDataLoader } = require
|
|
|
10
11
|
const { ContextFactory } = require('../context/ContextFactory');
|
|
11
12
|
const { commitResults } = require('../persistence/ResultCommitter');
|
|
12
13
|
const mathLayer = require('../layers/index');
|
|
14
|
+
const { performance } = require('perf_hooks');
|
|
13
15
|
|
|
14
16
|
class StandardExecutor {
|
|
15
17
|
static async run(date, calcs, passName, config, deps, rootData, fetchedDeps, previousFetchedDeps, skipStatusWrite = false) {
|
|
@@ -53,20 +55,31 @@ class StandardExecutor {
|
|
|
53
55
|
|
|
54
56
|
logger.log('INFO', `[${passName}] Streaming for ${streamingCalcs.length} computations...`);
|
|
55
57
|
|
|
56
|
-
// Metrics & State Tracking
|
|
58
|
+
// [IDEA 2] Metrics & State Tracking
|
|
57
59
|
const executionStats = {};
|
|
58
60
|
const shardIndexMap = {}; // Tracks sharding offsets per calculation
|
|
59
61
|
const aggregatedSuccess = {};
|
|
60
62
|
const aggregatedFailures = [];
|
|
61
63
|
|
|
64
|
+
// Initialize Timing Stats per calculation
|
|
62
65
|
Object.keys(state).forEach(name => {
|
|
63
|
-
executionStats[name] = {
|
|
66
|
+
executionStats[name] = {
|
|
67
|
+
processedUsers: 0,
|
|
68
|
+
skippedUsers: 0,
|
|
69
|
+
timings: { setup: 0, stream: 0, processing: 0 } // New
|
|
70
|
+
};
|
|
64
71
|
shardIndexMap[name] = 0;
|
|
65
72
|
});
|
|
66
73
|
|
|
74
|
+
// [IDEA 2] Measure Setup Time
|
|
75
|
+
const startSetup = performance.now();
|
|
67
76
|
const cachedLoader = new CachedDataLoader(config, deps);
|
|
68
77
|
await cachedLoader.loadMappings();
|
|
78
|
+
const setupDuration = performance.now() - startSetup;
|
|
69
79
|
|
|
80
|
+
// Distribute setup time
|
|
81
|
+
Object.keys(executionStats).forEach(name => executionStats[name].timings.setup += setupDuration);
|
|
82
|
+
|
|
70
83
|
const prevDate = new Date(dateStr + 'T00:00:00Z'); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
|
|
71
84
|
const prevDateStr = prevDate.toISOString().slice(0, 10);
|
|
72
85
|
|
|
@@ -83,13 +96,19 @@ class StandardExecutor {
|
|
|
83
96
|
let usersSinceLastFlush = 0;
|
|
84
97
|
|
|
85
98
|
try {
|
|
99
|
+
// [IDEA 2] Loop wrapper for profiling
|
|
86
100
|
for await (const tP_chunk of tP_iter) {
|
|
101
|
+
// [IDEA 2] Measure Streaming Time (Gap between processing chunks)
|
|
102
|
+
const startStream = performance.now();
|
|
87
103
|
if (yP_iter) yP_chunk = (await yP_iter.next()).value || {};
|
|
88
104
|
if (tH_iter) tH_chunk = (await tH_iter.next()).value || {};
|
|
89
|
-
|
|
105
|
+
const streamDuration = performance.now() - startStream;
|
|
106
|
+
Object.keys(executionStats).forEach(name => executionStats[name].timings.stream += streamDuration);
|
|
107
|
+
|
|
90
108
|
const chunkSize = Object.keys(tP_chunk).length;
|
|
91
109
|
|
|
92
|
-
//
|
|
110
|
+
// [IDEA 2] Measure Processing Time
|
|
111
|
+
const startProcessing = performance.now();
|
|
93
112
|
const promises = streamingCalcs.map(calc =>
|
|
94
113
|
StandardExecutor.executePerUser(
|
|
95
114
|
calc, calc.manifest, dateStr, tP_chunk, yP_chunk, tH_chunk,
|
|
@@ -98,6 +117,10 @@ class StandardExecutor {
|
|
|
98
117
|
)
|
|
99
118
|
);
|
|
100
119
|
await Promise.all(promises);
|
|
120
|
+
const procDuration = performance.now() - startProcessing;
|
|
121
|
+
|
|
122
|
+
// Assign processing time (Note: Parallel execution means total wall time is shared)
|
|
123
|
+
Object.keys(executionStats).forEach(name => executionStats[name].timings.processing += procDuration);
|
|
101
124
|
|
|
102
125
|
usersSinceLastFlush += chunkSize;
|
|
103
126
|
|
|
@@ -161,7 +184,7 @@ class StandardExecutor {
|
|
|
161
184
|
transformedState[name] = {
|
|
162
185
|
manifest: inst.manifest,
|
|
163
186
|
getResult: async () => dataToCommit,
|
|
164
|
-
_executionStats: executionStats[name] // Attach current stats
|
|
187
|
+
_executionStats: executionStats[name] // Attach current stats including timings
|
|
165
188
|
};
|
|
166
189
|
|
|
167
190
|
// ⚠️ CRITICAL: CLEAR MEMORY
|
|
@@ -196,6 +219,18 @@ class StandardExecutor {
|
|
|
196
219
|
successAcc[name].metrics.storage.keys += (update.metrics.storage.keys || 0);
|
|
197
220
|
successAcc[name].metrics.storage.shardCount = Math.max(successAcc[name].metrics.storage.shardCount, update.metrics.storage.shardCount || 1);
|
|
198
221
|
}
|
|
222
|
+
|
|
223
|
+
// [IDEA 2] Sum timing metrics
|
|
224
|
+
if (update.metrics?.execution?.timings) {
|
|
225
|
+
if (!successAcc[name].metrics.execution) successAcc[name].metrics.execution = { timings: { setup:0, stream:0, processing:0 }};
|
|
226
|
+
const tDest = successAcc[name].metrics.execution.timings;
|
|
227
|
+
const tSrc = update.metrics.execution.timings;
|
|
228
|
+
|
|
229
|
+
tDest.setup += (tSrc.setup || 0);
|
|
230
|
+
tDest.stream += (tSrc.stream || 0);
|
|
231
|
+
tDest.processing += (tSrc.processing || 0);
|
|
232
|
+
}
|
|
233
|
+
|
|
199
234
|
// Keep the latest hash/composition info
|
|
200
235
|
successAcc[name].hash = update.hash;
|
|
201
236
|
}
|
|
@@ -11,7 +11,7 @@ const { generateProcessId, PROCESS_TYPES } = require('../logger/logger');
|
|
|
11
11
|
const { HeuristicValidator } = require('./ResultsValidator');
|
|
12
12
|
const validationOverrides = require('../config/validation_overrides');
|
|
13
13
|
const pLimit = require('p-limit');
|
|
14
|
-
const zlib = require('zlib');
|
|
14
|
+
const zlib = require('zlib');
|
|
15
15
|
|
|
16
16
|
const NON_RETRYABLE_ERRORS = [
|
|
17
17
|
'PERMISSION_DENIED', 'DATA_LOSS', 'FAILED_PRECONDITION'
|
|
@@ -59,10 +59,11 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
|
|
|
59
59
|
let effectiveOverrides = { ...configOverrides };
|
|
60
60
|
|
|
61
61
|
if (isPriceOnly && !effectiveOverrides.weekend) {
|
|
62
|
+
// Apply strict leniency for weekend/holiday price actions
|
|
62
63
|
effectiveOverrides.weekend = {
|
|
63
64
|
maxZeroPct: 100,
|
|
64
65
|
maxFlatlinePct: 100,
|
|
65
|
-
maxNullPct: 100
|
|
66
|
+
maxNullPct: 100
|
|
66
67
|
};
|
|
67
68
|
}
|
|
68
69
|
// -----------------------------------------------------------
|
|
@@ -193,17 +194,14 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
|
|
|
193
194
|
|
|
194
195
|
async function writeSingleResult(result, docRef, name, dateContext, logger, config, deps, startShardIndex = 0, flushMode = 'STANDARD') {
|
|
195
196
|
|
|
196
|
-
// ---
|
|
197
|
-
// Try to compress before falling back to complex sharding
|
|
197
|
+
// --- COMPRESSION STRATEGY ---
|
|
198
198
|
try {
|
|
199
199
|
const jsonString = JSON.stringify(result);
|
|
200
200
|
const rawBuffer = Buffer.from(jsonString);
|
|
201
201
|
|
|
202
|
-
// Only attempt if meaningful size (> 50KB)
|
|
203
202
|
if (rawBuffer.length > 50 * 1024) {
|
|
204
203
|
const compressedBuffer = zlib.gzipSync(rawBuffer);
|
|
205
204
|
|
|
206
|
-
// If compressed fits in one document (< 900KB safety limit)
|
|
207
205
|
if (compressedBuffer.length < 900 * 1024) {
|
|
208
206
|
logger.log('INFO', `[Compression] ${name}: Compressed ${(rawBuffer.length/1024).toFixed(0)}KB -> ${(compressedBuffer.length/1024).toFixed(0)}KB. Saved as Blob.`);
|
|
209
207
|
|
|
@@ -214,7 +212,6 @@ async function writeSingleResult(result, docRef, name, dateContext, logger, conf
|
|
|
214
212
|
payload: compressedBuffer
|
|
215
213
|
};
|
|
216
214
|
|
|
217
|
-
// Write immediately
|
|
218
215
|
await docRef.set(compressedPayload, { merge: true });
|
|
219
216
|
|
|
220
217
|
return {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Utility for recording computation run attempts (The Audit Logger).
|
|
3
3
|
* UPDATED: Stores 'trigger' reason and 'execution' stats.
|
|
4
|
+
* UPDATED (IDEA 2): Stores granular timing profiles.
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
const { FieldValue } = require('../utils/utils');
|
|
@@ -37,6 +38,10 @@ async function recordRunAttempt(db, context, status, error = null, detailedMetri
|
|
|
37
38
|
const anomalies = detailedMetrics.validation?.anomalies || [];
|
|
38
39
|
if (error && error.message && error.message.includes('Data Integrity')) { anomalies.push(error.message); }
|
|
39
40
|
|
|
41
|
+
// [IDEA 2] Prepare Execution Stats & Timings
|
|
42
|
+
const rawExecStats = detailedMetrics.execution || {};
|
|
43
|
+
const timings = rawExecStats.timings || {};
|
|
44
|
+
|
|
40
45
|
const runEntry = {
|
|
41
46
|
runId: runId,
|
|
42
47
|
computationName: computation,
|
|
@@ -53,8 +58,17 @@ async function recordRunAttempt(db, context, status, error = null, detailedMetri
|
|
|
53
58
|
type: (triggerReason && triggerReason.includes('Layer')) ? 'CASCADE' : ((triggerReason && triggerReason.includes('New')) ? 'INIT' : 'UPDATE')
|
|
54
59
|
},
|
|
55
60
|
|
|
56
|
-
// [
|
|
57
|
-
executionStats:
|
|
61
|
+
// [IDEA 2] Enhanced Execution Stats
|
|
62
|
+
executionStats: {
|
|
63
|
+
processedUsers: rawExecStats.processedUsers || 0,
|
|
64
|
+
skippedUsers: rawExecStats.skippedUsers || 0,
|
|
65
|
+
// Explicitly break out timings for BigQuery/Analysis
|
|
66
|
+
timings: {
|
|
67
|
+
setupMs: Math.round(timings.setup || 0),
|
|
68
|
+
streamMs: Math.round(timings.stream || 0),
|
|
69
|
+
processingMs: Math.round(timings.processing || 0)
|
|
70
|
+
}
|
|
71
|
+
},
|
|
58
72
|
|
|
59
73
|
outputStats: {
|
|
60
74
|
sizeMB: sizeMB,
|
|
@@ -64,7 +78,7 @@ async function recordRunAttempt(db, context, status, error = null, detailedMetri
|
|
|
64
78
|
},
|
|
65
79
|
|
|
66
80
|
anomalies: anomalies,
|
|
67
|
-
_schemaVersion: '2.
|
|
81
|
+
_schemaVersion: '2.2' // Bumped for profiler
|
|
68
82
|
};
|
|
69
83
|
|
|
70
84
|
if (error) {
|
|
@@ -2,16 +2,15 @@
|
|
|
2
2
|
* @fileoverview Build Reporter & Auto-Runner.
|
|
3
3
|
* Generates a "Pre-Flight" report of what the computation system WILL do.
|
|
4
4
|
* REFACTORED: Strict 5-category reporting with date-based exclusion logic.
|
|
5
|
-
* UPDATED:
|
|
6
|
-
* UPDATED: Adds 'pass' number to detail records for better waterfall visibility.
|
|
5
|
+
* UPDATED: Replaced Batch Writes with Parallel Writes to prevent DEADLINE_EXCEEDED timeouts.
|
|
7
6
|
* FIXED: Ensures 'latest' pointer updates even if detail writes fail.
|
|
7
|
+
* UPDATED (IDEA 1): Added Dependency Impact Analysis ("Blast Radius").
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
const { analyzeDateExecution } = require('../WorkflowOrchestrator');
|
|
11
11
|
const { fetchComputationStatus } = require('../persistence/StatusRepository');
|
|
12
12
|
const { normalizeName, getExpectedDateStrings, DEFINITIVE_EARLIEST_DATES } = require('../utils/utils');
|
|
13
13
|
const { checkRootDataAvailability } = require('../data/AvailabilityChecker');
|
|
14
|
-
const { commitBatchInChunks } = require('../persistence/FirestoreUtils');
|
|
15
14
|
const pLimit = require('p-limit');
|
|
16
15
|
const path = require('path');
|
|
17
16
|
const packageJson = require(path.join(__dirname, '..', '..', '..', 'package.json'));
|
|
@@ -43,10 +42,37 @@ function isDateBeforeAvailability(dateStr, calcManifest) {
|
|
|
43
42
|
return false;
|
|
44
43
|
}
|
|
45
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Helper: Calculates the transitive closure of dependents (Blast Radius).
|
|
47
|
+
* Returns the count of direct and total cascading dependents.
|
|
48
|
+
*/
|
|
49
|
+
function calculateBlastRadius(targetCalcName, reverseGraph) {
|
|
50
|
+
const impactSet = new Set();
|
|
51
|
+
const queue = [targetCalcName];
|
|
52
|
+
|
|
53
|
+
// BFS Traversal
|
|
54
|
+
while(queue.length > 0) {
|
|
55
|
+
const current = queue.shift();
|
|
56
|
+
const dependents = reverseGraph.get(current) || [];
|
|
57
|
+
|
|
58
|
+
dependents.forEach(child => {
|
|
59
|
+
if (!impactSet.has(child)) {
|
|
60
|
+
impactSet.add(child);
|
|
61
|
+
queue.push(child);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
directDependents: (reverseGraph.get(targetCalcName) || []).length,
|
|
68
|
+
totalCascadingDependents: impactSet.size,
|
|
69
|
+
affectedCalculations: Array.from(impactSet).slice(0, 50) // Cap list size for storage safety
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
46
73
|
/**
|
|
47
74
|
* AUTO-RUN ENTRY POINT
|
|
48
|
-
*
|
|
49
|
-
* If we deploy multiple computation pass nodes simultaneously, only one should run the report.
|
|
75
|
+
* Uses transactional locking to prevent race conditions.
|
|
50
76
|
*/
|
|
51
77
|
async function ensureBuildReport(config, dependencies, manifest) {
|
|
52
78
|
const { db, logger } = dependencies;
|
|
@@ -88,7 +114,7 @@ async function ensureBuildReport(config, dependencies, manifest) {
|
|
|
88
114
|
}
|
|
89
115
|
|
|
90
116
|
/**
|
|
91
|
-
* Generates the report and saves to Firestore
|
|
117
|
+
* Generates the report and saves to Firestore.
|
|
92
118
|
*/
|
|
93
119
|
async function generateBuildReport(config, dependencies, manifest, daysBack = 90, customBuildId = null) {
|
|
94
120
|
const { db, logger } = dependencies;
|
|
@@ -103,6 +129,19 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
|
|
|
103
129
|
const datesToCheck = getExpectedDateStrings(startDate, today);
|
|
104
130
|
const manifestMap = new Map(manifest.map(c => [normalizeName(c.name), c]));
|
|
105
131
|
|
|
132
|
+
// [IDEA 1] Build Reverse Dependency Graph (Parent -> Children)
|
|
133
|
+
const reverseGraph = new Map();
|
|
134
|
+
manifest.forEach(c => {
|
|
135
|
+
const parentName = normalizeName(c.name);
|
|
136
|
+
if (c.dependencies) {
|
|
137
|
+
c.dependencies.forEach(dep => {
|
|
138
|
+
const depName = normalizeName(dep);
|
|
139
|
+
if (!reverseGraph.has(depName)) reverseGraph.set(depName, []);
|
|
140
|
+
reverseGraph.get(depName).push(parentName);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
106
145
|
// Main Report Header
|
|
107
146
|
const reportHeader = {
|
|
108
147
|
buildId,
|
|
@@ -153,7 +192,7 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
|
|
|
153
192
|
impossible: [], // Missing Data (Historical) / Impossible Dependency
|
|
154
193
|
uptodate: [], // Hash Match (Previously "Skipped")
|
|
155
194
|
|
|
156
|
-
//
|
|
195
|
+
// Metadata for Verification
|
|
157
196
|
meta: {
|
|
158
197
|
totalIncluded: 0,
|
|
159
198
|
totalExpected: 0,
|
|
@@ -166,18 +205,24 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
|
|
|
166
205
|
dateSummary.meta.totalExpected = expectedCount;
|
|
167
206
|
|
|
168
207
|
// Helper to push only if date is valid for this specific calc
|
|
169
|
-
// [UPDATED] Adds 'pass' number to the record
|
|
170
208
|
const pushIfValid = (targetArray, item, extraReason = null) => {
|
|
171
209
|
const calcManifest = manifestMap.get(item.name);
|
|
172
210
|
if (calcManifest && isDateBeforeAvailability(dateStr, calcManifest)) {
|
|
173
211
|
return; // EXCLUDED: Date is before data exists
|
|
174
212
|
}
|
|
175
213
|
|
|
176
|
-
|
|
214
|
+
const entry = {
|
|
177
215
|
name: item.name,
|
|
178
216
|
reason: item.reason || extraReason,
|
|
179
217
|
pass: calcManifest ? calcManifest.pass : '?'
|
|
180
|
-
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// [IDEA 1] If this is a Re-Run, calculate Blast Radius
|
|
221
|
+
if (targetArray === dateSummary.rerun) {
|
|
222
|
+
entry.impact = calculateBlastRadius(item.name, reverseGraph);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
targetArray.push(entry);
|
|
181
226
|
};
|
|
182
227
|
|
|
183
228
|
// 1. RUN (New)
|
|
@@ -187,7 +232,6 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
|
|
|
187
232
|
analysis.reRuns.forEach(item => pushIfValid(dateSummary.rerun, item, "Hash Mismatch"));
|
|
188
233
|
|
|
189
234
|
// 3. BLOCKED (Temporary Issues)
|
|
190
|
-
// Merging 'blocked' and 'failedDependency' as both are temporary blocks
|
|
191
235
|
analysis.blocked.forEach(item => pushIfValid(dateSummary.blocked, item));
|
|
192
236
|
analysis.failedDependency.forEach(item => pushIfValid(dateSummary.blocked, item, "Dependency Missing"));
|
|
193
237
|
|
|
@@ -211,7 +255,7 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
|
|
|
211
255
|
logger.log('WARN', `[BuildReporter] ⚠️ Mismatch on ${dateStr}: Expected ${expectedCount} but got ${includedCount}.`);
|
|
212
256
|
}
|
|
213
257
|
|
|
214
|
-
//
|
|
258
|
+
// QUEUE THE WRITE (Don't write yet)
|
|
215
259
|
const detailRef = db.collection('computation_build_records').doc(buildId).collection('details').doc(dateStr);
|
|
216
260
|
detailWrites.push({
|
|
217
261
|
ref: detailRef,
|
|
@@ -247,22 +291,33 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
|
|
|
247
291
|
const reportRef = db.collection('computation_build_records').doc(buildId);
|
|
248
292
|
await reportRef.set(reportHeader);
|
|
249
293
|
|
|
250
|
-
// 2. Write Details (Protected)
|
|
251
|
-
//
|
|
252
|
-
// we still update the 'latest' pointer to the new version.
|
|
294
|
+
// 2. Write Details (Protected & Parallelized)
|
|
295
|
+
// FIX: Using parallel individual writes instead of Batch to avoid DEADLINE_EXCEEDED
|
|
253
296
|
let detailsSuccess = true;
|
|
254
297
|
if (detailWrites.length > 0) {
|
|
255
|
-
logger.log('INFO', `[BuildReporter] Writing ${detailWrites.length} detail records...`);
|
|
298
|
+
logger.log('INFO', `[BuildReporter] Writing ${detailWrites.length} detail records (Parallel Strategy)...`);
|
|
299
|
+
|
|
256
300
|
try {
|
|
257
|
-
|
|
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
|
+
|
|
258
313
|
} catch (detailErr) {
|
|
259
314
|
detailsSuccess = false;
|
|
260
|
-
logger.log('ERROR', `[BuildReporter] ⚠️ Failed to write
|
|
315
|
+
logger.log('ERROR', `[BuildReporter] ⚠️ Failed to write some details. Report Header is preserved.`, detailErr);
|
|
261
316
|
}
|
|
262
317
|
}
|
|
263
318
|
|
|
264
319
|
// 3. Update 'latest' pointer
|
|
265
|
-
// This
|
|
320
|
+
// This runs regardless of detail write success/failure
|
|
266
321
|
const latestMetadata = {
|
|
267
322
|
...reportHeader,
|
|
268
323
|
note: detailsSuccess
|
|
@@ -270,9 +325,12 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
|
|
|
270
325
|
: "Latest build report pointer (WARNING: Partial detail records due to write error)."
|
|
271
326
|
};
|
|
272
327
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
328
|
+
try {
|
|
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
|
+
}
|
|
276
334
|
|
|
277
335
|
return {
|
|
278
336
|
success: true,
|