bulltrackers-module 1.0.277 → 1.0.279

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.
@@ -2,6 +2,7 @@
2
2
  * @fileoverview Handles saving computation results with observability and Smart Cleanup.
3
3
  * UPDATED: Implements GZIP Compression for efficient storage.
4
4
  * UPDATED: Implements Content-Based Hashing (ResultHash) for dependency short-circuiting.
5
+ * UPDATED: Auto-enforces Weekend Mode validation for 'Price-Only' computations.
5
6
  */
6
7
  const { commitBatchInChunks, generateDataHash } = require('../utils/utils');
7
8
  const { updateComputationStatus } = require('./StatusRepository');
@@ -10,7 +11,7 @@ const { generateProcessId, PROCESS_TYPES } = require('../logger/logger');
10
11
  const { HeuristicValidator } = require('./ResultsValidator');
11
12
  const validationOverrides = require('../config/validation_overrides');
12
13
  const pLimit = require('p-limit');
13
- const zlib = require('zlib'); // [NEW] Compression Lib
14
+ const zlib = require('zlib');
14
15
 
15
16
  const NON_RETRYABLE_ERRORS = [
16
17
  'PERMISSION_DENIED', 'DATA_LOSS', 'FAILED_PRECONDITION'
@@ -28,30 +29,50 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
28
29
  const pid = generateProcessId(PROCESS_TYPES.STORAGE, passName, dStr);
29
30
 
30
31
  // Options defaults
31
- const flushMode = options.flushMode || 'STANDARD';
32
- const shardIndexes = options.shardIndexes || {};
32
+ const flushMode = options.flushMode || 'STANDARD';
33
+ const shardIndexes = options.shardIndexes || {};
33
34
  const nextShardIndexes = {};
34
35
 
35
36
  const fanOutLimit = pLimit(10);
36
37
 
37
38
  for (const name in stateObj) {
38
- const calc = stateObj[name];
39
+ const calc = stateObj[name];
39
40
  const execStats = calc._executionStats || { processedUsers: 0, skippedUsers: 0 };
40
41
  const currentShardIndex = shardIndexes[name] || 0;
41
42
 
42
43
  const runMetrics = {
43
- storage: { sizeBytes: 0, isSharded: false, shardCount: 1, keys: 0 },
44
+ storage: { sizeBytes: 0, isSharded: false, shardCount: 1, keys: 0 },
44
45
  validation: { isValid: true, anomalies: [] },
45
46
  execution: execStats
46
47
  };
47
48
 
48
49
  try {
49
50
  const result = await calc.getResult();
50
- const overrides = validationOverrides[calc.manifest.name] || {};
51
+ const configOverrides = validationOverrides[calc.manifest.name] || {};
51
52
 
53
+ // --- [NEW] AUTO-ENFORCE WEEKEND MODE FOR PRICE-ONLY CALCS ---
54
+ // If a calculation depends SOLELY on 'price', we assume market closures
55
+ // will cause 0s/Flatlines on weekends, so we enforce lenient validation.
56
+ const dataDeps = calc.manifest.rootDataDependencies || [];
57
+ const isPriceOnly = (dataDeps.length === 1 && dataDeps[0] === 'price');
58
+
59
+ let effectiveOverrides = { ...configOverrides };
60
+
61
+ if (isPriceOnly && !effectiveOverrides.weekend) {
62
+ // Apply strict leniency for weekend/holiday price actions
63
+ effectiveOverrides.weekend = {
64
+ maxZeroPct: 100,
65
+ maxFlatlinePct: 100,
66
+ maxNullPct: 100
67
+ };
68
+ }
69
+ // -----------------------------------------------------------
70
+
52
71
  // Validation
53
72
  if (result && Object.keys(result).length > 0) {
54
- const healthCheck = HeuristicValidator.analyze(calc.manifest.name, result, overrides);
73
+ // [FIX] Added 'dStr' as 3rd argument to match HeuristicValidator signature
74
+ const healthCheck = HeuristicValidator.analyze(calc.manifest.name, result, dStr, effectiveOverrides);
75
+
55
76
  if (!healthCheck.valid) {
56
77
  runMetrics.validation.isValid = false;
57
78
  runMetrics.validation.anomalies.push(healthCheck.reason);
@@ -91,7 +112,7 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
91
112
  const isMultiDate = resultKeys.length > 0 && resultKeys.every(k => /^\d{4}-\d{2}-\d{2}$/.test(k));
92
113
 
93
114
  if (isMultiDate) {
94
- const datePromises = resultKeys.map((historicalDate) => fanOutLimit(async () => {
115
+ const datePromises = resultKeys.map((historicalDate) => fanOutLimit(async () => {
95
116
  const dailyData = result[historicalDate];
96
117
  if (!dailyData || Object.keys(dailyData).length === 0) return;
97
118
 
@@ -173,17 +194,14 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
173
194
 
174
195
  async function writeSingleResult(result, docRef, name, dateContext, logger, config, deps, startShardIndex = 0, flushMode = 'STANDARD') {
175
196
 
176
- // --- [NEW] COMPRESSION STRATEGY ---
177
- // Try to compress before falling back to complex sharding
197
+ // --- COMPRESSION STRATEGY ---
178
198
  try {
179
199
  const jsonString = JSON.stringify(result);
180
200
  const rawBuffer = Buffer.from(jsonString);
181
201
 
182
- // Only attempt if meaningful size (> 50KB)
183
202
  if (rawBuffer.length > 50 * 1024) {
184
203
  const compressedBuffer = zlib.gzipSync(rawBuffer);
185
204
 
186
- // If compressed fits in one document (< 900KB safety limit)
187
205
  if (compressedBuffer.length < 900 * 1024) {
188
206
  logger.log('INFO', `[Compression] ${name}: Compressed ${(rawBuffer.length/1024).toFixed(0)}KB -> ${(compressedBuffer.length/1024).toFixed(0)}KB. Saved as Blob.`);
189
207
 
@@ -194,7 +212,6 @@ async function writeSingleResult(result, docRef, name, dateContext, logger, conf
194
212
  payload: compressedBuffer
195
213
  };
196
214
 
197
- // Write immediately
198
215
  await docRef.set(compressedPayload, { merge: true });
199
216
 
200
217
  return {
@@ -24,11 +24,11 @@ class HeuristicValidator {
24
24
  const sampleSize = Math.min(totalItems, 100);
25
25
  const step = Math.floor(totalItems / sampleSize);
26
26
 
27
- let zeroCount = 0;
28
- let nullCount = 0;
29
- let nanCount = 0;
27
+ let zeroCount = 0;
28
+ let nullCount = 0;
29
+ let nanCount = 0;
30
30
  let emptyVectorCount = 0;
31
- let analyzedCount = 0;
31
+ let analyzedCount = 0;
32
32
 
33
33
  const numericValues = [];
34
34
 
@@ -82,9 +82,9 @@ class HeuristicValidator {
82
82
 
83
83
  // Default Thresholds
84
84
  let thresholds = {
85
- maxZeroPct: overrides.maxZeroPct ?? 99,
86
- maxNullPct: overrides.maxNullPct ?? 90,
87
- maxNanPct: overrides.maxNanPct ?? 0,
85
+ maxZeroPct: overrides.maxZeroPct ?? 99,
86
+ maxNullPct: overrides.maxNullPct ?? 90,
87
+ maxNanPct: overrides.maxNanPct ?? 0,
88
88
  maxFlatlinePct: overrides.maxFlatlinePct ?? 95
89
89
  };
90
90
 
@@ -2,15 +2,14 @@
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: Added transactional locking to prevent duplicate reports on concurrent cold starts.
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.
6
+ * FIXED: Ensures 'latest' pointer updates even if detail writes fail.
7
7
  */
8
8
 
9
9
  const { analyzeDateExecution } = require('../WorkflowOrchestrator');
10
10
  const { fetchComputationStatus } = require('../persistence/StatusRepository');
11
11
  const { normalizeName, getExpectedDateStrings, DEFINITIVE_EARLIEST_DATES } = require('../utils/utils');
12
12
  const { checkRootDataAvailability } = require('../data/AvailabilityChecker');
13
- const { commitBatchInChunks } = require('../persistence/FirestoreUtils');
14
13
  const pLimit = require('p-limit');
15
14
  const path = require('path');
16
15
  const packageJson = require(path.join(__dirname, '..', '..', '..', 'package.json'));
@@ -44,8 +43,7 @@ function isDateBeforeAvailability(dateStr, calcManifest) {
44
43
 
45
44
  /**
46
45
  * AUTO-RUN ENTRY POINT
47
- * UPDATED: Uses transactional locking to prevent race conditions.
48
- * If we deploy multiple computation pass nodes simultaneously, only one should run the report.
46
+ * Uses transactional locking to prevent race conditions.
49
47
  */
50
48
  async function ensureBuildReport(config, dependencies, manifest) {
51
49
  const { db, logger } = dependencies;
@@ -87,7 +85,7 @@ async function ensureBuildReport(config, dependencies, manifest) {
87
85
  }
88
86
 
89
87
  /**
90
- * Generates the report and saves to Firestore (Sharded).
88
+ * Generates the report and saves to Firestore.
91
89
  */
92
90
  async function generateBuildReport(config, dependencies, manifest, daysBack = 90, customBuildId = null) {
93
91
  const { db, logger } = dependencies;
@@ -152,7 +150,7 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
152
150
  impossible: [], // Missing Data (Historical) / Impossible Dependency
153
151
  uptodate: [], // Hash Match (Previously "Skipped")
154
152
 
155
- // [NEW] Metadata for Verification
153
+ // Metadata for Verification
156
154
  meta: {
157
155
  totalIncluded: 0,
158
156
  totalExpected: 0,
@@ -165,7 +163,6 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
165
163
  dateSummary.meta.totalExpected = expectedCount;
166
164
 
167
165
  // Helper to push only if date is valid for this specific calc
168
- // [UPDATED] Adds 'pass' number to the record
169
166
  const pushIfValid = (targetArray, item, extraReason = null) => {
170
167
  const calcManifest = manifestMap.get(item.name);
171
168
  if (calcManifest && isDateBeforeAvailability(dateStr, calcManifest)) {
@@ -186,7 +183,6 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
186
183
  analysis.reRuns.forEach(item => pushIfValid(dateSummary.rerun, item, "Hash Mismatch"));
187
184
 
188
185
  // 3. BLOCKED (Temporary Issues)
189
- // Merging 'blocked' and 'failedDependency' as both are temporary blocks
190
186
  analysis.blocked.forEach(item => pushIfValid(dateSummary.blocked, item));
191
187
  analysis.failedDependency.forEach(item => pushIfValid(dateSummary.blocked, item, "Dependency Missing"));
192
188
 
@@ -210,7 +206,7 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
210
206
  logger.log('WARN', `[BuildReporter] ⚠️ Mismatch on ${dateStr}: Expected ${expectedCount} but got ${includedCount}.`);
211
207
  }
212
208
 
213
- // ALWAYS WRITE THE REPORT (No filtering based on activity)
209
+ // QUEUE THE WRITE (Don't write yet)
214
210
  const detailRef = db.collection('computation_build_records').doc(buildId).collection('details').doc(dateStr);
215
211
  detailWrites.push({
216
212
  ref: detailRef,
@@ -242,17 +238,50 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
242
238
  scanRange: `${datesToCheck[0]} to ${datesToCheck[datesToCheck.length-1]}`
243
239
  };
244
240
 
241
+ // 1. Write the main report header (Specific Version)
245
242
  const reportRef = db.collection('computation_build_records').doc(buildId);
246
243
  await reportRef.set(reportHeader);
247
244
 
245
+ // 2. Write Details (Protected & Parallelized)
246
+ // FIX: Using parallel individual writes instead of Batch to avoid DEADLINE_EXCEEDED
247
+ let detailsSuccess = true;
248
248
  if (detailWrites.length > 0) {
249
- logger.log('INFO', `[BuildReporter] Writing ${detailWrites.length} detail records...`);
250
- await commitBatchInChunks(config, dependencies, detailWrites, 'BuildReportDetails');
251
- }
249
+ logger.log('INFO', `[BuildReporter] Writing ${detailWrites.length} detail records (Parallel Strategy)...`);
250
+
251
+ try {
252
+ // Concurrency limit of 15 to be safe
253
+ const writeLimit = pLimit(15);
254
+ const writePromises = detailWrites.map(w => writeLimit(() =>
255
+ w.ref.set(w.data).catch(e => {
256
+ logger.log('WARN', `[BuildReporter] Failed to write detail for ${w.ref.path}: ${e.message}`);
257
+ throw e;
258
+ })
259
+ ));
260
+
261
+ await Promise.all(writePromises);
262
+ logger.log('INFO', `[BuildReporter] Successfully wrote all detail records.`);
252
263
 
253
- await db.collection('computation_build_records').doc('latest').set({ ...reportHeader, note: "Latest build report pointer (See subcollection for details)." });
264
+ } catch (detailErr) {
265
+ detailsSuccess = false;
266
+ logger.log('ERROR', `[BuildReporter] ⚠️ Failed to write some details. Report Header is preserved.`, detailErr);
267
+ }
268
+ }
254
269
 
255
- logger.log('SUCCESS', `[BuildReporter] Report ${buildId} saved. Re-runs: ${totalReRun}, New: ${totalRun}.`);
270
+ // 3. Update 'latest' pointer
271
+ // This runs regardless of detail write success/failure
272
+ const latestMetadata = {
273
+ ...reportHeader,
274
+ note: detailsSuccess
275
+ ? "Latest build report pointer (See subcollection for details)."
276
+ : "Latest build report pointer (WARNING: Partial detail records due to write error)."
277
+ };
278
+
279
+ try {
280
+ await db.collection('computation_build_records').doc('latest').set(latestMetadata);
281
+ logger.log('SUCCESS', `[BuildReporter] Report ${buildId} saved. Re-runs: ${totalReRun}, New: ${totalRun}. Pointer Updated.`);
282
+ } catch (pointerErr) {
283
+ logger.log('FATAL', `[BuildReporter] Failed to update 'latest' pointer!`, pointerErr);
284
+ }
256
285
 
257
286
  return {
258
287
  success: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.277",
3
+ "version": "1.0.279",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [