bulltrackers-module 1.0.268 → 1.0.270

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,7 @@
1
1
  /**
2
2
  * @fileoverview Main Orchestrator. Coordinates the topological execution.
3
- * UPDATED: Implements Smart Audit logic to detect WHY a hash mismatch occurred.
4
- * FIX: Added 'Audit Upgrade' check to force re-run if composition metadata is missing.
3
+ * UPDATED: Removed 'Permanently Impossible' optimization to ensure full visibility/recovery.
4
+ * UPDATED: Includes 'Audit Upgrade' check.
5
5
  */
6
6
  const { normalizeName, DEFINITIVE_EARLIEST_DATES } = require('./utils/utils');
7
7
  const { checkRootDataAvailability, checkRootDependencies } = require('./data/AvailabilityChecker');
@@ -17,7 +17,6 @@ function groupByPass(manifest) { return manifest.reduce((acc, calc) => { (acc[c
17
17
 
18
18
  /**
19
19
  * Analyzes whether calculations should run, be skipped, or are blocked.
20
- * Now performs Deep Hash Analysis to explain Re-Runs.
21
20
  */
22
21
  function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus, manifestMap, prevDailyStatus = null) {
23
22
  const report = { runnable: [], blocked: [], impossible: [], failedDependency: [], reRuns: [], skipped: [] };
@@ -58,16 +57,16 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
58
57
  let migrationOldCategory = null;
59
58
  if (storedCategory && storedCategory !== calc.category) { migrationOldCategory = storedCategory; }
60
59
 
61
- if (typeof storedHash === 'string' && storedHash.startsWith(STATUS_IMPOSSIBLE_PREFIX)) {
62
- report.skipped.push({ name: cName, reason: `Permanently Impossible (${storedHash})` });
63
- continue;
64
- }
60
+ // [REMOVED] The "Permanently Impossible" optimization block was here.
61
+ // Removal ensures we re-check Root Data every time, allowing for visibility and recovery.
65
62
 
63
+ // 1. Check Root Data (The Primary Gate)
66
64
  const rootCheck = checkRootDependencies(calc, rootDataStatus);
67
65
 
68
66
  if (!rootCheck.canRun) {
69
67
  const missingStr = rootCheck.missing.join(', ');
70
68
  if (!isTargetToday) {
69
+ // If previously impossible, this confirms it. If previously run, this is a regression.
71
70
  markImpossible(`Missing Root Data: ${missingStr} (Historical)`, 'NO_DATA');
72
71
  } else {
73
72
  report.blocked.push({ name: cName, reason: `Missing Root Data: ${missingStr} (Waiting)` });
@@ -75,6 +74,7 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
75
74
  continue;
76
75
  }
77
76
 
77
+ // 2. Check Dependencies
78
78
  let dependencyIsImpossible = false;
79
79
  const missingDeps = [];
80
80
  if (calc.dependencies) {
@@ -95,6 +95,7 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
95
95
  }
96
96
  if (missingDeps.length > 0) { report.failedDependency.push({ name: cName, missing: missingDeps }); continue; }
97
97
 
98
+ // 3. Check Historical Continuity
98
99
  if (calc.isHistorical && prevDailyStatus) {
99
100
  const yesterday = new Date(dateStr + 'T00:00:00Z');
100
101
  yesterday.setUTCDate(yesterday.getUTCDate() - 1);
@@ -107,12 +108,12 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
107
108
  }
108
109
  }
109
110
 
110
- // --- HASH CHECK LOGIC ---
111
+ // 4. Check Hash / Composition (The Audit Gate)
111
112
  if (!storedHash) {
112
113
  markRunnable(false, { reason: "New Calculation" });
113
114
  }
114
115
  else if (storedHash !== currentHash) {
115
- // Smart Logic: Why did it change?
116
+ // Smart Audit Logic
116
117
  let changeReason = "Hash Mismatch (Unknown)";
117
118
  const oldComp = stored.composition;
118
119
  const newComp = calc.composition;
@@ -153,7 +154,7 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
153
154
  else if (migrationOldCategory) {
154
155
  markRunnable(true, { name: cName, reason: 'Category Migration', previousCategory: migrationOldCategory, newCategory: calc.category });
155
156
  }
156
- // [CRITICAL FIX] Audit Upgrade Check: Force re-run if hash matches but composition is missing (Legacy Record)
157
+ // Audit Upgrade Check
157
158
  else if (!stored.composition) {
158
159
  markRunnable(true, {
159
160
  name: cName,
@@ -163,7 +164,7 @@ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus,
163
164
  });
164
165
  }
165
166
  else {
166
- report.skipped.push({ name: cName });
167
+ report.skipped.push({ name: cName, reason: "Up To Date" });
167
168
  simulationStatus[cName] = { hash: currentHash, category: calc.category, composition: calc.composition };
168
169
  }
169
170
  }
@@ -1,2 +1,2 @@
1
1
  // Change this string to force a global re-computation
2
- module.exports = "v1.0-epoch-2";
2
+ module.exports = "v2.0-epoch-2";
@@ -1,22 +1,50 @@
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 on 'Invalidate All' scenarios.
4
+ * REFACTORED: Strict 5-category reporting with date-based exclusion logic.
5
+ * UPDATED: Added meta stats to compare Included vs Expected computation counts per date.
5
6
  */
6
7
 
7
8
  const { analyzeDateExecution } = require('../WorkflowOrchestrator');
8
9
  const { fetchComputationStatus } = require('../persistence/StatusRepository');
9
10
  const { normalizeName, getExpectedDateStrings, DEFINITIVE_EARLIEST_DATES } = require('../utils/utils');
10
11
  const { checkRootDataAvailability } = require('../data/AvailabilityChecker');
11
- const { commitBatchInChunks } = require('../persistence/FirestoreUtils'); // Reuse chunker
12
+ const { commitBatchInChunks } = require('../persistence/FirestoreUtils');
12
13
  const pLimit = require('p-limit');
13
14
  const path = require('path');
14
15
  const packageJson = require(path.join(__dirname, '..', '..', '..', 'package.json'));
15
16
  const packageVersion = packageJson.version;
16
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
+
17
46
  /**
18
47
  * AUTO-RUN ENTRY POINT
19
- * Checks if a report for the current version exists. If not, runs it.
20
48
  */
21
49
  async function ensureBuildReport(config, dependencies, manifest) {
22
50
  const { db, logger } = dependencies;
@@ -34,8 +62,6 @@ async function ensureBuildReport(config, dependencies, manifest) {
34
62
  }
35
63
 
36
64
  logger.log('INFO', `[BuildReporter] 🚀 New Version Detected (${packageVersion}). Auto-running Pre-flight Report...`);
37
-
38
- // Scope: 90 days is fine now that we shard the output
39
65
  await generateBuildReport(config, dependencies, manifest, 90, buildId);
40
66
 
41
67
  } catch (e) {
@@ -59,18 +85,18 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
59
85
  const datesToCheck = getExpectedDateStrings(startDate, today);
60
86
  const manifestMap = new Map(manifest.map(c => [normalizeName(c.name), c]));
61
87
 
62
- // Main Report Header (Summary Only)
88
+ // Main Report Header
63
89
  const reportHeader = {
64
90
  buildId,
65
91
  packageVersion: packageVersion,
66
92
  generatedAt: new Date().toISOString(),
67
- summary: {},
68
- _sharded: true // Flag to tell UI/Tools to look in subcollection
93
+ summary: {},
94
+ _sharded: true
69
95
  };
70
96
 
71
- let totalReRuns = 0;
72
- let totalNew = 0;
73
- const detailWrites = []; // Accumulate writes for batching
97
+ let totalRun = 0;
98
+ let totalReRun = 0;
99
+ const detailWrites = [];
74
100
 
75
101
  const limit = pLimit(20);
76
102
 
@@ -99,28 +125,78 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
99
125
 
100
126
  const analysis = analyzeDateExecution(dateStr, manifest, rootDataStatus, dailyStatus, manifestMap, prevDailyStatus);
101
127
 
102
- const dateSummary = { willRun: [], willReRun: [], blocked: [], impossible: [] };
103
-
104
- analysis.runnable.forEach (item => dateSummary.willRun.push ({ name: item.name, reason: "New / No Previous Record" }));
105
- analysis.reRuns.forEach (item => dateSummary.willReRun.push ({ name: item.name, reason: item.reason || "Hash Mismatch" }));
106
- analysis.impossible.forEach (item => dateSummary.impossible.push ({ name: item.name, reason: item.reason }));
107
- [...analysis.blocked, ...analysis.failedDependency].forEach(item => dateSummary.blocked.push({ name: item.name, reason: item.reason || 'Dependency' }));
108
-
109
- const hasUpdates = dateSummary.willRun.length || dateSummary.willReRun.length || dateSummary.blocked.length || dateSummary.impossible.length;
110
-
111
- if (hasUpdates) {
112
- // Prepare Write for Subcollection
113
- const detailRef = db.collection('computation_build_records').doc(buildId).collection('details').doc(dateStr);
114
- detailWrites.push({
115
- ref: detailRef,
116
- data: dateSummary
117
- });
118
-
119
- return {
120
- stats: { new: dateSummary.willRun.length, rerun: dateSummary.willReRun.length }
121
- };
128
+ // ---------------------------------------------------------
129
+ // STRICT 5-CATEGORY MAPPING
130
+ // ---------------------------------------------------------
131
+ const dateSummary = {
132
+ run: [], // New / No Hash / "Runnable"
133
+ rerun: [], // Hash Mismatch / Category Migration
134
+ blocked: [], // Missing Data (Today) / Dependency Missing
135
+ impossible: [], // Missing Data (Historical) / Impossible Dependency
136
+ uptodate: [], // Hash Match (Previously "Skipped")
137
+
138
+ // [NEW] Metadata for Verification
139
+ meta: {
140
+ totalIncluded: 0,
141
+ totalExpected: 0,
142
+ match: false
143
+ }
144
+ };
145
+
146
+ // Calculate Expected Count (Computations in manifest that exist for this date)
147
+ const expectedCount = manifest.filter(c => !isDateBeforeAvailability(dateStr, c)).length;
148
+ dateSummary.meta.totalExpected = expectedCount;
149
+
150
+ // Helper to push only if date is valid for this specific calc
151
+ const pushIfValid = (targetArray, item, extraReason = null) => {
152
+ const calcManifest = manifestMap.get(item.name);
153
+ if (calcManifest && isDateBeforeAvailability(dateStr, calcManifest)) {
154
+ return; // EXCLUDED: Date is before data exists
155
+ }
156
+ targetArray.push({ name: item.name, reason: item.reason || extraReason });
157
+ };
158
+
159
+ // 1. RUN (New)
160
+ analysis.runnable.forEach(item => pushIfValid(dateSummary.run, item, "New Calculation"));
161
+
162
+ // 2. RE-RUN (Hash Mismatch)
163
+ analysis.reRuns.forEach(item => pushIfValid(dateSummary.rerun, item, "Hash Mismatch"));
164
+
165
+ // 3. BLOCKED (Temporary Issues)
166
+ // Merging 'blocked' and 'failedDependency' as both are temporary blocks
167
+ analysis.blocked.forEach(item => pushIfValid(dateSummary.blocked, item));
168
+ analysis.failedDependency.forEach(item => pushIfValid(dateSummary.blocked, item, "Dependency Missing"));
169
+
170
+ // 4. IMPOSSIBLE (Permanent Issues)
171
+ analysis.impossible.forEach(item => pushIfValid(dateSummary.impossible, item));
172
+
173
+ // 5. UP-TO-DATE (Previously "Skipped")
174
+ analysis.skipped.forEach(item => pushIfValid(dateSummary.uptodate, item, "Up To Date"));
175
+
176
+ // Calculate Included Count
177
+ const includedCount = dateSummary.run.length +
178
+ dateSummary.rerun.length +
179
+ dateSummary.blocked.length +
180
+ dateSummary.impossible.length +
181
+ dateSummary.uptodate.length;
182
+
183
+ dateSummary.meta.totalIncluded = includedCount;
184
+ dateSummary.meta.match = (includedCount === expectedCount);
185
+
186
+ if (!dateSummary.meta.match) {
187
+ logger.log('WARN', `[BuildReporter] ⚠️ Mismatch on ${dateStr}: Expected ${expectedCount} but got ${includedCount}.`);
122
188
  }
123
- return null;
189
+
190
+ // ALWAYS WRITE THE REPORT (No filtering based on activity)
191
+ const detailRef = db.collection('computation_build_records').doc(buildId).collection('details').doc(dateStr);
192
+ detailWrites.push({
193
+ ref: detailRef,
194
+ data: dateSummary
195
+ });
196
+
197
+ return {
198
+ stats: { run: dateSummary.run.length, rerun: dateSummary.rerun.length }
199
+ };
124
200
 
125
201
  } catch (err) {
126
202
  logger.log('ERROR', `[BuildReporter] Error analyzing date ${dateStr}: ${err.message}`);
@@ -132,27 +208,28 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
132
208
 
133
209
  results.forEach(res => {
134
210
  if (res) {
135
- totalNew += res.stats.new;
136
- totalReRuns += res.stats.rerun;
211
+ totalRun += res.stats.run;
212
+ totalReRun += res.stats.rerun;
137
213
  }
138
214
  });
139
215
 
140
- reportHeader.summary = { totalReRuns, totalNew, scanRange: `${datesToCheck[0]} to ${datesToCheck[datesToCheck.length-1]}` };
216
+ reportHeader.summary = {
217
+ totalReRuns: totalReRun,
218
+ totalNew: totalRun,
219
+ scanRange: `${datesToCheck[0]} to ${datesToCheck[datesToCheck.length-1]}`
220
+ };
141
221
 
142
- // 1. Write Header
143
222
  const reportRef = db.collection('computation_build_records').doc(buildId);
144
223
  await reportRef.set(reportHeader);
145
224
 
146
- // 2. Batch Write Details (Using FirestoreUtils to handle batching constraints)
147
225
  if (detailWrites.length > 0) {
148
226
  logger.log('INFO', `[BuildReporter] Writing ${detailWrites.length} detail records...`);
149
227
  await commitBatchInChunks(config, dependencies, detailWrites, 'BuildReportDetails');
150
228
  }
151
229
 
152
- // 3. Update 'latest' pointer (Summary only)
153
230
  await db.collection('computation_build_records').doc('latest').set({ ...reportHeader, note: "Latest build report pointer (See subcollection for details)." });
154
231
 
155
- logger.log('SUCCESS', `[BuildReporter] Report ${buildId} saved. Re-runs: ${totalReRuns}, New: ${totalNew}.`);
232
+ logger.log('SUCCESS', `[BuildReporter] Report ${buildId} saved. Re-runs: ${totalReRun}, New: ${totalRun}.`);
156
233
 
157
234
  return {
158
235
  success: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.268",
3
+ "version": "1.0.270",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [