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
- * UPDATED: Shards report details to subcollections to bypass 1MB limit.
5
- * UPDATED: Explicitly reports SKIPPED items for 100% visibility.
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'); // Reuse chunker
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
- const latestRef = db.collection('computation_build_records').doc('latest');
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
- const latestDoc = await latestRef.get();
29
- const priorVersion = latestDoc.exists ? latestDoc.data().packageVersion : null;
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
- if (priorVersion === packageVersion) {
32
- logger.log('INFO', `[BuildReporter] ✅ Version ${packageVersion} already has a report. Skipping.`);
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] 🚀 New Version Detected (${packageVersion}). Auto-running Pre-flight Report...`);
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 (Summary Only)
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 totalReRuns = 0;
70
- let totalNew = 0;
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
- // [NEW] Added 'skipped' to the summary object
101
- const dateSummary = { willRun: [], willReRun: [], blocked: [], impossible: [], skipped: [] };
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
- analysis.runnable.forEach (item => dateSummary.willRun.push ({ name: item.name, reason: "New / No Previous Record" }));
104
- analysis.reRuns.forEach (item => dateSummary.willReRun.push ({ name: item.name, reason: item.reason || "Hash Mismatch" }));
105
- analysis.impossible.forEach (item => dateSummary.impossible.push ({ name: item.name, reason: item.reason }));
106
- [...analysis.blocked, ...analysis.failedDependency].forEach(item => dateSummary.blocked.push({ name: item.name, reason: item.reason || 'Dependency' }));
107
-
108
- // [NEW] Map skipped items so the math adds up to 92
109
- analysis.skipped.forEach (item => dateSummary.skipped.push ({ name: item.name, reason: item.reason || "Up To Date" }));
110
-
111
- // Update: We write the report if there is ANY data, not just updates
112
- // This ensures full visibility into the day's state
113
- if (dateSummary.willRun.length || dateSummary.willReRun.length || dateSummary.blocked.length || dateSummary.impossible.length || dateSummary.skipped.length) {
114
- const detailRef = db.collection('computation_build_records').doc(buildId).collection('details').doc(dateStr);
115
- detailWrites.push({
116
- ref: detailRef,
117
- data: dateSummary
118
- });
119
-
120
- return {
121
- stats: { new: dateSummary.willRun.length, rerun: dateSummary.willReRun.length }
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
- return null;
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
- totalNew += res.stats.new;
137
- totalReRuns += res.stats.rerun;
234
+ totalRun += res.stats.run;
235
+ totalReRun += res.stats.rerun;
138
236
  }
139
237
  });
140
238
 
141
- reportHeader.summary = { totalReRuns, totalNew, scanRange: `${datesToCheck[0]} to ${datesToCheck[datesToCheck.length-1]}` };
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: ${totalReRuns}, New: ${totalNew}.`);
255
+ logger.log('SUCCESS', `[BuildReporter] Report ${buildId} saved. Re-runs: ${totalReRun}, New: ${totalRun}.`);
154
256
 
155
257
  return {
156
258
  success: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.269",
3
+ "version": "1.0.271",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [