bulltrackers-module 1.0.267 → 1.0.269

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,14 +1,15 @@
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: Fixed 'latest' document overwrite bug.
5
- * UPDATED: Now reports specific reasons for Re-Runs.
4
+ * UPDATED: Shards report details to subcollections to bypass 1MB limit.
5
+ * UPDATED: Explicitly reports SKIPPED items for 100% visibility.
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
13
  const pLimit = require('p-limit');
13
14
  const path = require('path');
14
15
  const packageJson = require(path.join(__dirname, '..', '..', '..', 'package.json'));
@@ -16,18 +17,15 @@ const packageVersion = pac
16
17
 
17
18
  /**
18
19
  * AUTO-RUN ENTRY POINT
19
- * Checks if a report for the current version exists. If not, runs it.
20
20
  */
21
21
  async function ensureBuildReport(config, dependencies, manifest) {
22
22
  const { db, logger } = dependencies;
23
23
  const now = new Date();
24
- // Create a standardized build ID
25
24
  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')}`;
26
25
  const latestRef = db.collection('computation_build_records').doc('latest');
27
26
 
28
27
  try {
29
28
  const latestDoc = await latestRef.get();
30
- // Check using 'packageVersion' key to match what we store
31
29
  const priorVersion = latestDoc.exists ? latestDoc.data().packageVersion : null;
32
30
 
33
31
  if (priorVersion === packageVersion) {
@@ -36,8 +34,6 @@ async function ensureBuildReport(config, dependencies, manifest) {
36
34
  }
37
35
 
38
36
  logger.log('INFO', `[BuildReporter] 🚀 New Version Detected (${packageVersion}). Auto-running Pre-flight Report...`);
39
-
40
- // Run generation. This function handles writing the 'latest' document with FULL data.
41
37
  await generateBuildReport(config, dependencies, manifest, 90, buildId);
42
38
 
43
39
  } catch (e) {
@@ -46,7 +42,7 @@ async function ensureBuildReport(config, dependencies, manifest) {
46
42
  }
47
43
 
48
44
  /**
49
- * Generates the report and saves to Firestore.
45
+ * Generates the report and saves to Firestore (Sharded).
50
46
  */
51
47
  async function generateBuildReport(config, dependencies, manifest, daysBack = 90, customBuildId = null) {
52
48
  const { db, logger } = dependencies;
@@ -54,7 +50,6 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
54
50
 
55
51
  logger.log('INFO', `[BuildReporter] Generating Build Report: ${buildId} (Scope: ${daysBack} days)...`);
56
52
 
57
- // 1. Determine Date Range
58
53
  const today = new Date();
59
54
  const startDate = new Date();
60
55
  startDate.setDate(today.getDate() - daysBack);
@@ -62,37 +57,33 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
62
57
  const datesToCheck = getExpectedDateStrings(startDate, today);
63
58
  const manifestMap = new Map(manifest.map(c => [normalizeName(c.name), c]));
64
59
 
65
- const reportData = {
60
+ // Main Report Header (Summary Only)
61
+ const reportHeader = {
66
62
  buildId,
67
63
  packageVersion: packageVersion,
68
64
  generatedAt: new Date().toISOString(),
69
65
  summary: {},
70
- dates: {}
66
+ _sharded: true
71
67
  };
72
68
 
73
69
  let totalReRuns = 0;
74
70
  let totalNew = 0;
71
+ const detailWrites = [];
75
72
 
76
- // 2. PARALLEL PROCESSING
77
73
  const limit = pLimit(20);
78
74
 
79
75
  const processingPromises = datesToCheck.map(dateStr => limit(async () => {
80
76
  try {
81
- // [IMPROVED] Fetch all statuses in parallel
82
77
  const fetchPromises = [
83
- // A. Real status
84
78
  fetchComputationStatus(dateStr, config, dependencies),
85
- // C. Real Root Data
86
79
  checkRootDataAvailability(dateStr, config, dependencies, DEFINITIVE_EARLIEST_DATES)
87
80
  ];
88
81
 
89
- // B. Yesterday's Status (only if needed)
90
82
  let prevDateStr = null;
91
83
  if (manifest.some(c => c.isHistorical)) {
92
84
  const prevDate = new Date(dateStr + 'T00:00:00Z');
93
85
  prevDate.setUTCDate(prevDate.getUTCDate() - 1);
94
86
  prevDateStr = prevDate.toISOString().slice(0, 10);
95
-
96
87
  if (prevDate >= DEFINITIVE_EARLIEST_DATES.absoluteEarliest) {
97
88
  fetchPromises.push(fetchComputationStatus(prevDateStr, config, dependencies));
98
89
  }
@@ -101,31 +92,36 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
101
92
  const results = await Promise.all(fetchPromises);
102
93
  const dailyStatus = results[0];
103
94
  const availability = results[1];
104
- // If we fetched prevStatus, it's at index 2
105
95
  const prevDailyStatus = (prevDateStr && results[2]) ? results[2] : (prevDateStr ? {} : null);
106
-
107
96
  const rootDataStatus = availability ? availability.status : { hasPortfolio: false, hasHistory: false, hasSocial: false, hasInsights: false, hasPrices: false };
108
97
 
109
- // D. Run Logic Analysis
110
98
  const analysis = analyzeDateExecution(dateStr, manifest, rootDataStatus, dailyStatus, manifestMap, prevDailyStatus);
111
99
 
112
- // E. Format Findings
113
- const dateSummary = { willRun: [], willReRun: [], blocked: [], impossible: [] };
100
+ // [NEW] Added 'skipped' to the summary object
101
+ const dateSummary = { willRun: [], willReRun: [], blocked: [], impossible: [], skipped: [] };
114
102
 
115
- // Pass the generated "Reason" string through to the report
116
103
  analysis.runnable.forEach (item => dateSummary.willRun.push ({ name: item.name, reason: "New / No Previous Record" }));
117
104
  analysis.reRuns.forEach (item => dateSummary.willReRun.push ({ name: item.name, reason: item.reason || "Hash Mismatch" }));
118
105
  analysis.impossible.forEach (item => dateSummary.impossible.push ({ name: item.name, reason: item.reason }));
119
106
  [...analysis.blocked, ...analysis.failedDependency].forEach(item => dateSummary.blocked.push({ name: item.name, reason: item.reason || 'Dependency' }));
120
-
121
- const hasUpdates = dateSummary.willRun.length || dateSummary.willReRun.length || dateSummary.blocked.length || dateSummary.impossible.length;
122
107
 
123
- return {
124
- dateStr,
125
- dateSummary,
126
- hasUpdates,
127
- stats: { new: dateSummary.willRun.length, rerun: dateSummary.willReRun.length }
128
- };
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
+ };
123
+ }
124
+ return null;
129
125
 
130
126
  } catch (err) {
131
127
  logger.log('ERROR', `[BuildReporter] Error analyzing date ${dateStr}: ${err.message}`);
@@ -135,30 +131,31 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
135
131
 
136
132
  const results = await Promise.all(processingPromises);
137
133
 
138
- // 3. Aggregate Results
139
134
  results.forEach(res => {
140
- if (res && res.hasUpdates) {
141
- reportData.dates[res.dateStr] = res.dateSummary;
135
+ if (res) {
142
136
  totalNew += res.stats.new;
143
137
  totalReRuns += res.stats.rerun;
144
138
  }
145
139
  });
146
140
 
147
- reportData.summary = { totalReRuns, totalNew, scanRange: `${datesToCheck[0]} to ${datesToCheck[datesToCheck.length-1]}` };
141
+ reportHeader.summary = { totalReRuns, totalNew, scanRange: `${datesToCheck[0]} to ${datesToCheck[datesToCheck.length-1]}` };
148
142
 
149
- // 4. Store Report
150
143
  const reportRef = db.collection('computation_build_records').doc(buildId);
151
- await reportRef.set(reportData);
144
+ await reportRef.set(reportHeader);
145
+
146
+ if (detailWrites.length > 0) {
147
+ logger.log('INFO', `[BuildReporter] Writing ${detailWrites.length} detail records...`);
148
+ await commitBatchInChunks(config, dependencies, detailWrites, 'BuildReportDetails');
149
+ }
152
150
 
153
- // 5. Update 'latest' pointer
154
- await db.collection('computation_build_records').doc('latest').set({ ...reportData, note: "Latest build report pointer." });
151
+ await db.collection('computation_build_records').doc('latest').set({ ...reportHeader, note: "Latest build report pointer (See subcollection for details)." });
155
152
 
156
153
  logger.log('SUCCESS', `[BuildReporter] Report ${buildId} saved. Re-runs: ${totalReRuns}, New: ${totalNew}.`);
157
154
 
158
155
  return {
159
156
  success: true,
160
157
  reportId: buildId,
161
- summary: reportData.summary
158
+ summary: reportHeader.summary
162
159
  };
163
160
  }
164
161
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.267",
3
+ "version": "1.0.269",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [