bulltrackers-module 1.0.276 → 1.0.278

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,5 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Execution-scoped data loader with caching.
3
+ * UPDATED: Handles Decompression of Shards.
3
4
  */
4
5
  const {
5
6
  loadDailyInsights,
@@ -7,6 +8,7 @@ const {
7
8
  getRelevantShardRefs,
8
9
  getPriceShardRefs
9
10
  } = require('../utils/data_loader');
11
+ const zlib = require('zlib'); // [NEW]
10
12
 
11
13
  class CachedDataLoader {
12
14
  constructor(config, dependencies) {
@@ -19,6 +21,19 @@ class CachedDataLoader {
19
21
  };
20
22
  }
21
23
 
24
+ // [NEW] Decompression Helper
25
+ _tryDecompress(data) {
26
+ if (data && data._compressed === true && data.payload) {
27
+ try {
28
+ return JSON.parse(zlib.gunzipSync(data.payload).toString());
29
+ } catch (e) {
30
+ console.error('[CachedDataLoader] Decompression failed', e);
31
+ return {};
32
+ }
33
+ }
34
+ return data;
35
+ }
36
+
22
37
  async loadMappings() {
23
38
  if (this.cache.mappings) return this.cache.mappings;
24
39
  const { calculationUtils } = this.deps;
@@ -52,7 +67,8 @@ class CachedDataLoader {
52
67
  try {
53
68
  const snap = await docRef.get();
54
69
  if (!snap.exists) return {};
55
- return snap.data();
70
+ // [UPDATED] Use decompression helper
71
+ return this._tryDecompress(snap.data());
56
72
  } catch (e) {
57
73
  console.error(`Error loading shard ${docRef.path}:`, e);
58
74
  return {};
@@ -1,7 +1,8 @@
1
1
  /**
2
- * @fileoverview Fetches results from previous computations, handling auto-sharding hydration.
2
+ * @fileoverview Fetches results from previous computations, handling auto-sharding and decompression.
3
3
  */
4
4
  const { normalizeName } = require('../utils/utils');
5
+ const zlib = require('zlib'); // [NEW]
5
6
 
6
7
  async function fetchExistingResults(dateStr, calcsInPass, fullManifest, config, { db }, includeSelf = false) {
7
8
  const manifestMap = new Map(fullManifest.map(c => [normalizeName(c.name), c]));
@@ -39,7 +40,20 @@ async function fetchExistingResults(dateStr, calcsInPass, fullManifest, config,
39
40
  const name = names[i];
40
41
  if (!doc.exists) return;
41
42
  const data = doc.data();
42
- if (data._sharded === true) {
43
+
44
+ // --- [NEW] DECOMPRESSION LOGIC ---
45
+ if (data._compressed === true && data.payload) {
46
+ try {
47
+ // Firestore returns Buffers automatically
48
+ const unzipped = zlib.gunzipSync(data.payload);
49
+ fetched[name] = JSON.parse(unzipped.toString());
50
+ } catch (e) {
51
+ console.error(`[Hydration] Failed to decompress ${name}:`, e);
52
+ fetched[name] = {};
53
+ }
54
+ }
55
+ // --- END NEW LOGIC ---
56
+ else if (data._sharded === true) {
43
57
  hydrationPromises.push(hydrateAutoShardedResult(doc.ref, name));
44
58
  } else if (data._completed) {
45
59
  fetched[name] = data;
@@ -1,14 +1,17 @@
1
1
  /**
2
2
  * @fileoverview Handles saving computation results with observability and Smart Cleanup.
3
+ * UPDATED: Implements GZIP Compression for efficient storage.
3
4
  * UPDATED: Implements Content-Based Hashing (ResultHash) for dependency short-circuiting.
5
+ * UPDATED: Auto-enforces Weekend Mode validation for 'Price-Only' computations.
4
6
  */
5
- const { commitBatchInChunks, generateDataHash } = require('../utils/utils'); // [UPDATED] Import generateDataHash
7
+ const { commitBatchInChunks, generateDataHash } = require('../utils/utils');
6
8
  const { updateComputationStatus } = require('./StatusRepository');
7
9
  const { batchStoreSchemas } = require('../utils/schema_capture');
8
10
  const { generateProcessId, PROCESS_TYPES } = require('../logger/logger');
9
11
  const { HeuristicValidator } = require('./ResultsValidator');
10
12
  const validationOverrides = require('../config/validation_overrides');
11
13
  const pLimit = require('p-limit');
14
+ const zlib = require('zlib'); // [NEW] Compression Lib
12
15
 
13
16
  const NON_RETRYABLE_ERRORS = [
14
17
  'PERMISSION_DENIED', 'DATA_LOSS', 'FAILED_PRECONDITION'
@@ -26,30 +29,49 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
26
29
  const pid = generateProcessId(PROCESS_TYPES.STORAGE, passName, dStr);
27
30
 
28
31
  // Options defaults
29
- const flushMode = options.flushMode || 'STANDARD';
30
- const shardIndexes = options.shardIndexes || {};
32
+ const flushMode = options.flushMode || 'STANDARD';
33
+ const shardIndexes = options.shardIndexes || {};
31
34
  const nextShardIndexes = {};
32
35
 
33
36
  const fanOutLimit = pLimit(10);
34
37
 
35
38
  for (const name in stateObj) {
36
- const calc = stateObj[name];
39
+ const calc = stateObj[name];
37
40
  const execStats = calc._executionStats || { processedUsers: 0, skippedUsers: 0 };
38
41
  const currentShardIndex = shardIndexes[name] || 0;
39
42
 
40
43
  const runMetrics = {
41
- storage: { sizeBytes: 0, isSharded: false, shardCount: 1, keys: 0 },
44
+ storage: { sizeBytes: 0, isSharded: false, shardCount: 1, keys: 0 },
42
45
  validation: { isValid: true, anomalies: [] },
43
46
  execution: execStats
44
47
  };
45
48
 
46
49
  try {
47
50
  const result = await calc.getResult();
48
- const overrides = validationOverrides[calc.manifest.name] || {};
51
+ const configOverrides = validationOverrides[calc.manifest.name] || {};
49
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
+ effectiveOverrides.weekend = {
63
+ maxZeroPct: 100,
64
+ maxFlatlinePct: 100,
65
+ maxNullPct: 100 // Allow full nulls (e.g. holidays)
66
+ };
67
+ }
68
+ // -----------------------------------------------------------
69
+
50
70
  // Validation
51
71
  if (result && Object.keys(result).length > 0) {
52
- const healthCheck = HeuristicValidator.analyze(calc.manifest.name, result, overrides);
72
+ // [FIX] Added 'dStr' as 3rd argument to match HeuristicValidator signature
73
+ const healthCheck = HeuristicValidator.analyze(calc.manifest.name, result, dStr, effectiveOverrides);
74
+
53
75
  if (!healthCheck.valid) {
54
76
  runMetrics.validation.isValid = false;
55
77
  runMetrics.validation.anomalies.push(healthCheck.reason);
@@ -61,7 +83,7 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
61
83
 
62
84
  const isEmpty = !result || (typeof result === 'object' && Object.keys(result).length === 0);
63
85
 
64
- // [NEW] Calculate Result Hash (Content-Based)
86
+ // Calculate Result Hash (Content-Based)
65
87
  const resultHash = isEmpty ? 'empty' : generateDataHash(result);
66
88
 
67
89
  // Handle Empty Results
@@ -73,8 +95,8 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
73
95
  if (calc.manifest.hash) {
74
96
  successUpdates[name] = {
75
97
  hash: calc.manifest.hash,
76
- resultHash: resultHash, // [NEW] Store result hash
77
- dependencyResultHashes: calc.manifest.dependencyResultHashes || {}, // [NEW] Capture dep context
98
+ resultHash: resultHash,
99
+ dependencyResultHashes: calc.manifest.dependencyResultHashes || {},
78
100
  category: calc.manifest.category,
79
101
  composition: calc.manifest.composition,
80
102
  metrics: runMetrics
@@ -89,7 +111,7 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
89
111
  const isMultiDate = resultKeys.length > 0 && resultKeys.every(k => /^\d{4}-\d{2}-\d{2}$/.test(k));
90
112
 
91
113
  if (isMultiDate) {
92
- const datePromises = resultKeys.map((historicalDate) => fanOutLimit(async () => {
114
+ const datePromises = resultKeys.map((historicalDate) => fanOutLimit(async () => {
93
115
  const dailyData = result[historicalDate];
94
116
  if (!dailyData || Object.keys(dailyData).length === 0) return;
95
117
 
@@ -107,8 +129,8 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
107
129
  if (calc.manifest.hash) {
108
130
  successUpdates[name] = {
109
131
  hash: calc.manifest.hash,
110
- resultHash: resultHash, // [NEW]
111
- dependencyResultHashes: calc.manifest.dependencyResultHashes || {}, // [NEW]
132
+ resultHash: resultHash,
133
+ dependencyResultHashes: calc.manifest.dependencyResultHashes || {},
112
134
  category: calc.manifest.category,
113
135
  composition: calc.manifest.composition,
114
136
  metrics: runMetrics
@@ -134,8 +156,8 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
134
156
  if (calc.manifest.hash) {
135
157
  successUpdates[name] = {
136
158
  hash: calc.manifest.hash,
137
- resultHash: resultHash, // [NEW]
138
- dependencyResultHashes: calc.manifest.dependencyResultHashes || {}, // [NEW]
159
+ resultHash: resultHash,
160
+ dependencyResultHashes: calc.manifest.dependencyResultHashes || {},
139
161
  category: calc.manifest.category,
140
162
  composition: calc.manifest.composition,
141
163
  metrics: runMetrics
@@ -170,6 +192,44 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
170
192
  }
171
193
 
172
194
  async function writeSingleResult(result, docRef, name, dateContext, logger, config, deps, startShardIndex = 0, flushMode = 'STANDARD') {
195
+
196
+ // --- [NEW] COMPRESSION STRATEGY ---
197
+ // Try to compress before falling back to complex sharding
198
+ try {
199
+ const jsonString = JSON.stringify(result);
200
+ const rawBuffer = Buffer.from(jsonString);
201
+
202
+ // Only attempt if meaningful size (> 50KB)
203
+ if (rawBuffer.length > 50 * 1024) {
204
+ const compressedBuffer = zlib.gzipSync(rawBuffer);
205
+
206
+ // If compressed fits in one document (< 900KB safety limit)
207
+ if (compressedBuffer.length < 900 * 1024) {
208
+ logger.log('INFO', `[Compression] ${name}: Compressed ${(rawBuffer.length/1024).toFixed(0)}KB -> ${(compressedBuffer.length/1024).toFixed(0)}KB. Saved as Blob.`);
209
+
210
+ const compressedPayload = {
211
+ _compressed: true,
212
+ _completed: true,
213
+ _lastUpdated: new Date().toISOString(),
214
+ payload: compressedBuffer
215
+ };
216
+
217
+ // Write immediately
218
+ await docRef.set(compressedPayload, { merge: true });
219
+
220
+ return {
221
+ totalSize: compressedBuffer.length,
222
+ isSharded: false,
223
+ shardCount: 1,
224
+ nextShardIndex: startShardIndex
225
+ };
226
+ }
227
+ }
228
+ } catch (compErr) {
229
+ logger.log('WARN', `[Compression] Failed to compress ${name}. Falling back to standard sharding.`, compErr);
230
+ }
231
+ // --- END COMPRESSION STRATEGY ---
232
+
173
233
  const strategies = [
174
234
  { bytes: 900 * 1024, keys: null },
175
235
  { bytes: 450 * 1024, keys: 10000 },
@@ -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
 
@@ -4,6 +4,7 @@
4
4
  * REFACTORED: Strict 5-category reporting with date-based exclusion logic.
5
5
  * UPDATED: Added transactional locking to prevent duplicate reports on concurrent cold starts.
6
6
  * UPDATED: Adds 'pass' number to detail records for better waterfall visibility.
7
+ * FIXED: Ensures 'latest' pointer updates even if detail writes fail.
7
8
  */
8
9
 
9
10
  const { analyzeDateExecution } = require('../WorkflowOrchestrator');
@@ -242,15 +243,34 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
242
243
  scanRange: `${datesToCheck[0]} to ${datesToCheck[datesToCheck.length-1]}`
243
244
  };
244
245
 
246
+ // 1. Write the main report header (Specific Version)
245
247
  const reportRef = db.collection('computation_build_records').doc(buildId);
246
248
  await reportRef.set(reportHeader);
247
249
 
250
+ // 2. Write Details (Protected)
251
+ // [FIX] We wrap this in try-catch so that if the massive detail write fails,
252
+ // we still update the 'latest' pointer to the new version.
253
+ let detailsSuccess = true;
248
254
  if (detailWrites.length > 0) {
249
255
  logger.log('INFO', `[BuildReporter] Writing ${detailWrites.length} detail records...`);
250
- await commitBatchInChunks(config, dependencies, detailWrites, 'BuildReportDetails');
256
+ try {
257
+ await commitBatchInChunks(config, dependencies, detailWrites, 'BuildReportDetails');
258
+ } catch (detailErr) {
259
+ detailsSuccess = false;
260
+ logger.log('ERROR', `[BuildReporter] ⚠️ Failed to write all details, but Report Header is saved.`, detailErr);
261
+ }
251
262
  }
252
263
 
253
- await db.collection('computation_build_records').doc('latest').set({ ...reportHeader, note: "Latest build report pointer (See subcollection for details)." });
264
+ // 3. Update 'latest' pointer
265
+ // This now runs even if details failed, preventing the version mismatch bug.
266
+ const latestMetadata = {
267
+ ...reportHeader,
268
+ note: detailsSuccess
269
+ ? "Latest build report pointer (See subcollection for details)."
270
+ : "Latest build report pointer (WARNING: Partial detail records due to write error)."
271
+ };
272
+
273
+ await db.collection('computation_build_records').doc('latest').set(latestMetadata);
254
274
 
255
275
  logger.log('SUCCESS', `[BuildReporter] Report ${buildId} saved. Re-runs: ${totalReRun}, New: ${totalRun}.`);
256
276
 
@@ -4,7 +4,22 @@
4
4
  * --- NEW: Added streamPortfolioData async generator ---
5
5
  * --- FIXED: streamPortfolioData and streamHistoryData now accept optional 'providedRefs' ---
6
6
  * --- UPDATE: Added Smart Shard Indexing for specific ticker lookups ---
7
+ * --- UPDATE: Added GZIP Decompression Support for robust data loading ---
7
8
  */
9
+ const zlib = require('zlib'); // [NEW]
10
+
11
+ // [NEW] Helper for decompressing any doc if needed
12
+ function tryDecompress(data) {
13
+ if (data && data._compressed === true && data.payload) {
14
+ try {
15
+ return JSON.parse(zlib.gunzipSync(data.payload).toString());
16
+ } catch (e) {
17
+ console.error('[DataLoader] Decompression failed', e);
18
+ return {};
19
+ }
20
+ }
21
+ return data;
22
+ }
8
23
 
9
24
  /** --- Data Loader Sub-Pipes (Stateless, Dependency-Injection) --- */
10
25
 
@@ -39,7 +54,10 @@ async function loadDataByRefs(config, deps, refs) {
39
54
  const snapshots = await withRetry(() => db.getAll(...batchRefs), `getAll(batch ${Math.floor(i / batchSize)})`);
40
55
  for (const doc of snapshots) {
41
56
  if (!doc.exists) continue;
42
- const data = doc.data();
57
+ const rawData = doc.data();
58
+ // [UPDATED] Decompress if needed
59
+ const data = tryDecompress(rawData);
60
+
43
61
  if (data && typeof data === 'object') Object.assign(mergedPortfolios, data);
44
62
  else logger.log('WARN', `Doc ${doc.id} exists but data is not an object`, data);
45
63
  }
@@ -68,7 +86,8 @@ async function loadDailyInsights(config, deps, dateString) {
68
86
  const docSnap = await withRetry(() => docRef.get(), `getInsights(${dateString})`);
69
87
  if (!docSnap.exists) { logger.log('WARN', `Insights not found for ${dateString}`); return null; }
70
88
  logger.log('TRACE', `Successfully loaded insights for ${dateString}`);
71
- return docSnap.data();
89
+ // [UPDATED] Decompress
90
+ return tryDecompress(docSnap.data());
72
91
  } catch (error) {
73
92
  logger.log('ERROR', `Failed to load daily insights for ${dateString}`, { errorMessage: error.message });
74
93
  return null;
@@ -86,7 +105,10 @@ async function loadDailySocialPostInsights(config, deps, dateString) {
86
105
  const querySnapshot = await withRetry(() => postsCollectionRef.get(), `getSocialPosts(${dateString})`);
87
106
  if (querySnapshot.empty) { logger.log('WARN', `No social post insights for ${dateString}`); return null; }
88
107
  const postsMap = {};
89
- querySnapshot.forEach(doc => { postsMap[doc.id] = doc.data(); });
108
+ querySnapshot.forEach(doc => {
109
+ // [UPDATED] Decompress individual posts if needed
110
+ postsMap[doc.id] = tryDecompress(doc.data());
111
+ });
90
112
  logger.log('TRACE', `Loaded ${Object.keys(postsMap).length} social post insights`);
91
113
  return postsMap;
92
114
  } catch (error) {
@@ -168,12 +190,6 @@ async function getPriceShardRefs(config, deps) {
168
190
  * when only specific tickers are needed.
169
191
  */
170
192
 
171
- /**
172
- * Ensures the Price Shard Index exists. If not, builds it by scanning all shards.
173
- * @param {object} config
174
- * @param {object} deps
175
- * @returns {Promise<Object>} The lookup map { "instrumentId": "shardDocId" }
176
- */
177
193
  /**
178
194
  * Ensures the Price Shard Index exists. If not, builds it by scanning all shards.
179
195
  * [FIX] Added TTL check to ensure new instruments are discovered.
@@ -205,7 +221,10 @@ async function ensurePriceShardIndex(config, deps) {
205
221
 
206
222
  snapshot.forEach(doc => {
207
223
  shardCount++;
208
- const data = doc.data();
224
+ // [UPDATED] Robustly handle compressed shards during indexing
225
+ const rawData = doc.data();
226
+ const data = tryDecompress(rawData);
227
+
209
228
  if (data.history) {
210
229
  Object.keys(data.history).forEach(instId => {
211
230
  index[instId] = doc.id;
@@ -273,4 +292,4 @@ module.exports = {
273
292
  getPriceShardRefs,
274
293
  ensurePriceShardIndex,
275
294
  getRelevantShardRefs
276
- };
295
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.276",
3
+ "version": "1.0.278",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [