bulltrackers-module 1.0.732 → 1.0.734

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.
Files changed (106) hide show
  1. package/functions/computation-system-v2/README.md +152 -0
  2. package/functions/computation-system-v2/computations/PopularInvestorProfileMetrics.js +720 -0
  3. package/functions/computation-system-v2/computations/PopularInvestorRiskAssessment.js +176 -0
  4. package/functions/computation-system-v2/computations/PopularInvestorRiskMetrics.js +294 -0
  5. package/functions/computation-system-v2/computations/TestComputation.js +46 -0
  6. package/functions/computation-system-v2/computations/UserPortfolioSummary.js +172 -0
  7. package/functions/computation-system-v2/config/bulltrackers.config.js +317 -0
  8. package/functions/computation-system-v2/framework/core/Computation.js +73 -0
  9. package/functions/computation-system-v2/framework/core/Manifest.js +223 -0
  10. package/functions/computation-system-v2/framework/core/RuleInjector.js +53 -0
  11. package/functions/computation-system-v2/framework/core/Rules.js +231 -0
  12. package/functions/computation-system-v2/framework/core/RunAnalyzer.js +163 -0
  13. package/functions/computation-system-v2/framework/cost/CostTracker.js +154 -0
  14. package/functions/computation-system-v2/framework/data/DataFetcher.js +399 -0
  15. package/functions/computation-system-v2/framework/data/QueryBuilder.js +232 -0
  16. package/functions/computation-system-v2/framework/data/SchemaRegistry.js +287 -0
  17. package/functions/computation-system-v2/framework/execution/Orchestrator.js +498 -0
  18. package/functions/computation-system-v2/framework/execution/TaskRunner.js +35 -0
  19. package/functions/computation-system-v2/framework/execution/middleware/CostTrackerMiddleware.js +32 -0
  20. package/functions/computation-system-v2/framework/execution/middleware/LineageMiddleware.js +32 -0
  21. package/functions/computation-system-v2/framework/execution/middleware/Middleware.js +14 -0
  22. package/functions/computation-system-v2/framework/execution/middleware/ProfilerMiddleware.js +47 -0
  23. package/functions/computation-system-v2/framework/index.js +45 -0
  24. package/functions/computation-system-v2/framework/lineage/LineageTracker.js +147 -0
  25. package/functions/computation-system-v2/framework/monitoring/Profiler.js +80 -0
  26. package/functions/computation-system-v2/framework/resilience/Checkpointer.js +66 -0
  27. package/functions/computation-system-v2/framework/scheduling/ScheduleValidator.js +327 -0
  28. package/functions/computation-system-v2/framework/storage/StateRepository.js +286 -0
  29. package/functions/computation-system-v2/framework/storage/StorageManager.js +469 -0
  30. package/functions/computation-system-v2/framework/storage/index.js +9 -0
  31. package/functions/computation-system-v2/framework/testing/ComputationTester.js +86 -0
  32. package/functions/computation-system-v2/framework/utils/Graph.js +205 -0
  33. package/functions/computation-system-v2/handlers/dispatcher.js +109 -0
  34. package/functions/computation-system-v2/handlers/index.js +23 -0
  35. package/functions/computation-system-v2/handlers/onDemand.js +289 -0
  36. package/functions/computation-system-v2/handlers/scheduler.js +327 -0
  37. package/functions/computation-system-v2/index.js +163 -0
  38. package/functions/computation-system-v2/rules/index.js +49 -0
  39. package/functions/computation-system-v2/rules/instruments.js +465 -0
  40. package/functions/computation-system-v2/rules/metrics.js +304 -0
  41. package/functions/computation-system-v2/rules/portfolio.js +534 -0
  42. package/functions/computation-system-v2/rules/rankings.js +655 -0
  43. package/functions/computation-system-v2/rules/social.js +562 -0
  44. package/functions/computation-system-v2/rules/trades.js +545 -0
  45. package/functions/computation-system-v2/scripts/migrate-sectors.js +73 -0
  46. package/functions/computation-system-v2/test/test-dispatcher.js +317 -0
  47. package/functions/computation-system-v2/test/test-framework.js +500 -0
  48. package/functions/computation-system-v2/test/test-real-execution.js +166 -0
  49. package/functions/computation-system-v2/test/test-real-integration.js +194 -0
  50. package/functions/computation-system-v2/test/test-refactor-e2e.js +131 -0
  51. package/functions/computation-system-v2/test/test-results.json +31 -0
  52. package/functions/computation-system-v2/test/test-risk-metrics-computation.js +329 -0
  53. package/functions/computation-system-v2/test/test-scheduler.js +204 -0
  54. package/functions/computation-system-v2/test/test-storage.js +449 -0
  55. package/functions/orchestrator/index.js +24 -30
  56. package/index.js +8 -29
  57. package/package.json +3 -2
  58. package/functions/computation-system/WorkflowOrchestrator.js +0 -213
  59. package/functions/computation-system/config/monitoring_config.js +0 -31
  60. package/functions/computation-system/config/validation_overrides.js +0 -10
  61. package/functions/computation-system/context/ContextFactory.js +0 -143
  62. package/functions/computation-system/context/ManifestBuilder.js +0 -379
  63. package/functions/computation-system/data/AvailabilityChecker.js +0 -236
  64. package/functions/computation-system/data/CachedDataLoader.js +0 -325
  65. package/functions/computation-system/data/DependencyFetcher.js +0 -455
  66. package/functions/computation-system/executors/MetaExecutor.js +0 -279
  67. package/functions/computation-system/executors/PriceBatchExecutor.js +0 -108
  68. package/functions/computation-system/executors/StandardExecutor.js +0 -465
  69. package/functions/computation-system/helpers/computation_dispatcher.js +0 -750
  70. package/functions/computation-system/helpers/computation_worker.js +0 -375
  71. package/functions/computation-system/helpers/monitor.js +0 -64
  72. package/functions/computation-system/helpers/on_demand_helpers.js +0 -154
  73. package/functions/computation-system/layers/extractors.js +0 -1097
  74. package/functions/computation-system/layers/index.js +0 -40
  75. package/functions/computation-system/layers/mathematics.js +0 -522
  76. package/functions/computation-system/layers/profiling.js +0 -537
  77. package/functions/computation-system/layers/validators.js +0 -170
  78. package/functions/computation-system/legacy/AvailabilityCheckerOld.js +0 -388
  79. package/functions/computation-system/legacy/CachedDataLoaderOld.js +0 -357
  80. package/functions/computation-system/legacy/DependencyFetcherOld.js +0 -478
  81. package/functions/computation-system/legacy/MetaExecutorold.js +0 -364
  82. package/functions/computation-system/legacy/StandardExecutorold.js +0 -476
  83. package/functions/computation-system/legacy/computation_dispatcherold.js +0 -944
  84. package/functions/computation-system/logger/logger.js +0 -297
  85. package/functions/computation-system/persistence/ContractValidator.js +0 -81
  86. package/functions/computation-system/persistence/FirestoreUtils.js +0 -56
  87. package/functions/computation-system/persistence/ResultCommitter.js +0 -283
  88. package/functions/computation-system/persistence/ResultsValidator.js +0 -130
  89. package/functions/computation-system/persistence/RunRecorder.js +0 -142
  90. package/functions/computation-system/persistence/StatusRepository.js +0 -52
  91. package/functions/computation-system/reporter_epoch.js +0 -6
  92. package/functions/computation-system/scripts/UpdateContracts.js +0 -128
  93. package/functions/computation-system/services/SnapshotService.js +0 -148
  94. package/functions/computation-system/simulation/Fabricator.js +0 -285
  95. package/functions/computation-system/simulation/SeededRandom.js +0 -41
  96. package/functions/computation-system/simulation/SimRunner.js +0 -51
  97. package/functions/computation-system/system_epoch.js +0 -2
  98. package/functions/computation-system/tools/BuildReporter.js +0 -531
  99. package/functions/computation-system/tools/ContractDiscoverer.js +0 -144
  100. package/functions/computation-system/tools/DeploymentValidator.js +0 -536
  101. package/functions/computation-system/tools/FinalSweepReporter.js +0 -322
  102. package/functions/computation-system/topology/HashManager.js +0 -55
  103. package/functions/computation-system/topology/ManifestLoader.js +0 -47
  104. package/functions/computation-system/utils/data_loader.js +0 -675
  105. package/functions/computation-system/utils/schema_capture.js +0 -121
  106. package/functions/computation-system/utils/utils.js +0 -188
@@ -1,455 +0,0 @@
1
- /**
2
- * FILENAME: computation-system/data/DependencyFetcher.js
3
- * @fileoverview Fetches dependencies for computations.
4
- * REFACTORED: Unified fetch logic, streamlined decompression/sharding/GCS.
5
- * UPDATED: Properly checks isPage/isAlert flags to determine BigQuery vs Firestore routing.
6
- */
7
- const { normalizeName } = require('../utils/utils');
8
- const zlib = require('zlib');
9
- const { Storage } = require('@google-cloud/storage');
10
-
11
- const storage = new Storage(); // Singleton Client
12
-
13
- /**
14
- * Helper to check if a computation is isPage or isAlert by looking up manifest
15
- * @param {string} normalizedName - Normalized computation name
16
- * @param {object} config - Config object that may contain manifest info
17
- * @param {string} category - Computation category (fallback for alerts)
18
- * @returns {Promise<{isPage: boolean, isAlert: boolean}>}
19
- */
20
- async function checkComputationFlags(normalizedName, config, category) {
21
- let isPage = false;
22
- let isAlert = false;
23
-
24
- // Try to get manifest from config if available
25
- if (config.getCalculations && typeof config.getCalculations === 'function') {
26
- try {
27
- const calculations = config.getCalculations(config);
28
- const manifest = calculations.find(c => normalizeName(c.name) === normalizedName);
29
- if (manifest) {
30
- isPage = manifest.isPage === true;
31
- isAlert = manifest.isAlertComputation === true;
32
- return { isPage, isAlert };
33
- }
34
- } catch (e) {
35
- // Fall through to category-based detection
36
- }
37
- }
38
-
39
- // Fallback: infer from category (alerts category = alert computation)
40
- // Note: isPage cannot be inferred from category alone, so defaults to false
41
- isAlert = category === 'alerts';
42
-
43
- return { isPage, isAlert };
44
- }
45
-
46
- // =============================================================================
47
- // HELPERS
48
- // =============================================================================
49
-
50
- /** Checks if data is effectively empty (null or only metadata keys) */
51
- function isDataEmpty(data) {
52
- if (data == null) return true;
53
- if (Array.isArray(data)) return data.length === 0;
54
- if (typeof data === 'object') {
55
- // Return true if NO keys exist that DON'T start with '_'
56
- return !Object.keys(data).some(k => !k.startsWith('_'));
57
- }
58
- return false;
59
- }
60
-
61
- /** Robust decompression helper (Buffer, Base64, or Firestore Binary) */
62
- function tryDecompress(payload) {
63
- if (!payload) return null;
64
- try {
65
- const buffer = (payload instanceof Buffer) ? payload :
66
- (payload._byteString ? Buffer.from(payload._byteString, 'base64') :
67
- (payload.toDate ? payload.toDate() : Buffer.from(payload)));
68
-
69
- return JSON.parse(zlib.gunzipSync(buffer).toString('utf8'));
70
- } catch (e) {
71
- throw new Error(`Decompression failed: ${e.message}`);
72
- }
73
- }
74
-
75
- // =============================================================================
76
- // CORE FETCH LOGIC
77
- // =============================================================================
78
-
79
- /**
80
- * Fetches, decompresses, and reassembles (if sharded or on GCS) a single result document.
81
- * NEW: For non-alert, non-page computations, tries BigQuery first (cheaper, no sharding/compression).
82
- * @param {object} db - Firestore database instance
83
- * @param {object} config - Configuration object (may contain logger, manifestLookup, etc.)
84
- * @param {string} dateStr - Date string in YYYY-MM-DD format
85
- * @param {string} name - Computation name (normalized)
86
- * @param {string} category - Computation category
87
- * @returns {Promise<object|null>} Result data or null if not found
88
- */
89
- async function fetchSingleResult(db, config, dateStr, name, category) {
90
- const { resultsCollection = 'computation_results', resultsSubcollection = 'results', computationsSubcollection = 'computations' } = config;
91
- const log = config.logger || console;
92
-
93
- // Check if this is an alert or page computation by looking up manifest
94
- const { isPage: isPageComputation, isAlert: isAlertComputation } = await checkComputationFlags(name, config, category);
95
-
96
- // Try BigQuery first for non-alert, non-page computations (reduces Firestore reads)
97
- // isPage and isAlert computations are always written to Firestore (in addition to BigQuery),
98
- // so we should check Firestore for them, but can also try BigQuery as a fallback
99
- if (!isAlertComputation && !isPageComputation && process.env.BIGQUERY_ENABLED !== 'false') {
100
- try {
101
- const { queryComputationResult } = require('../../core/utils/bigquery_utils');
102
- const bigqueryResult = await queryComputationResult(name, category, dateStr, log);
103
-
104
- if (bigqueryResult && !isDataEmpty(bigqueryResult)) {
105
- log.log('INFO', `[DependencyFetcher] ✅ Using BigQuery for ${name} (${dateStr}, ${category})`);
106
- return bigqueryResult;
107
- }
108
- } catch (bqError) {
109
- log.log('WARN', `[DependencyFetcher] BigQuery fetch failed for ${name}, falling back to Firestore: ${bqError.message}`);
110
- // Fall through to Firestore
111
- }
112
- } else if (isAlertComputation || isPageComputation) {
113
- log.log('INFO', `[DependencyFetcher] 📄 Using Firestore for ${isAlertComputation ? 'alert' : 'page'} computation ${name} (${dateStr})`);
114
- }
115
-
116
- // Fallback to Firestore (for alerts, pages, or if BigQuery fails)
117
- const docRef = db.collection(resultsCollection).doc(dateStr)
118
- .collection(resultsSubcollection).doc(category)
119
- .collection(computationsSubcollection).doc(name);
120
-
121
- const snap = await docRef.get();
122
- if (!snap.exists) return null;
123
-
124
- let data = snap.data();
125
-
126
- // -------------------------------------------------------------------------
127
- // 1. GCS POINTER HANDLER (New)
128
- // -------------------------------------------------------------------------
129
- if (data.gcsUri || (data._gcs && data.gcsBucket && data.gcsPath)) {
130
- try {
131
- const bucketName = data.gcsBucket || data.gcsUri.split('/')[2];
132
- const fileName = data.gcsPath || data.gcsUri.split('/').slice(3).join('/');
133
-
134
- // Stream download is memory efficient for large files
135
- const [fileContent] = await storage.bucket(bucketName).file(fileName).download();
136
-
137
- // Assume Gzip (as writer does it), if fails try plain
138
- try {
139
- return JSON.parse(zlib.gunzipSync(fileContent).toString('utf8'));
140
- } catch (gzipErr) {
141
- // Fallback for uncompressed GCS files
142
- return JSON.parse(fileContent.toString('utf8'));
143
- }
144
- } catch (e) {
145
- log.log('ERROR', `[DependencyFetcher] ❌ GCS Fetch Failed for ${name}: ${e.message}`);
146
- // Depending on strictness, we might return null here or allow it to fail hard.
147
- // Returning null allows 'isDataEmpty' to catch it as "MISSING"
148
- return null;
149
- }
150
- }
151
-
152
- // -------------------------------------------------------------------------
153
- // 2. FIRESTORE COMPRESSED HANDLER
154
- // -------------------------------------------------------------------------
155
- if (data._compressed && data.payload) {
156
- try {
157
- const realData = tryDecompress(data.payload);
158
- data = { ...data, ...realData }; // Merge payload into base
159
- delete data.payload;
160
- } catch (e) {
161
- log.log('ERROR', `[DependencyFetcher] ❌ ${e.message} at ${docRef.path}`);
162
- return null;
163
- }
164
- }
165
-
166
- // -------------------------------------------------------------------------
167
- // 3. FIRESTORE SHARDED HANDLER
168
- // -------------------------------------------------------------------------
169
- if (data._sharded) {
170
- const shardSnaps = await docRef.collection('_shards').get();
171
- if (shardSnaps.empty) {
172
- log.log('ERROR', `[DependencyFetcher] ❌ Sharded doc has no shards: ${docRef.path}`);
173
- return null;
174
- }
175
-
176
- // Initialize merged data with any non-metadata fields from the pointer doc
177
- const merged = {};
178
- Object.entries(data).forEach(([k, v]) => { if (!k.startsWith('_')) merged[k] = v; });
179
-
180
- for (const shard of shardSnaps.docs) {
181
- let sData = shard.data();
182
-
183
- // Decompress Shard if needed
184
- if (sData._compressed && sData.payload) {
185
- try {
186
- const decomp = tryDecompress(sData.payload);
187
- sData = (typeof decomp === 'string') ? JSON.parse(decomp) : decomp;
188
- } catch (e) {
189
- log.log('ERROR', `[DependencyFetcher] ❌ Shard decompression failed (${shard.id}): ${e.message}`);
190
- continue;
191
- }
192
- }
193
-
194
- // Merge Shard Content
195
- Object.entries(sData).forEach(([k, v]) => {
196
- if (!k.startsWith('_')) merged[k] = v;
197
- });
198
- }
199
-
200
- // Return null if result is empty after merging
201
- if (Object.keys(merged).length === 0) return null;
202
- return merged;
203
- }
204
-
205
- // -------------------------------------------------------------------------
206
- // 4. STANDARD DOCUMENT HANDLER
207
- // -------------------------------------------------------------------------
208
- const clean = {};
209
- let hasContent = false;
210
- Object.entries(data).forEach(([k, v]) => {
211
- if (!k.startsWith('_')) { clean[k] = v; hasContent = true; }
212
- });
213
-
214
- return hasContent ? clean : null;
215
- }
216
-
217
- // =============================================================================
218
- // PUBLIC METHODS
219
- // =============================================================================
220
-
221
- /**
222
- * Fetches dependencies for a specific date (Standard pass).
223
- */
224
- async function fetchDependencies(date, calcs, config, deps, manifestLookup = {}, allowMissing = false) {
225
- const { db, logger } = deps;
226
- const dStr = date.toISOString().slice(0, 10);
227
-
228
- // 1. Resolve Dependencies (Normalize Name -> Original Name)
229
- const needed = new Map();
230
- calcs.forEach(c => {
231
- // Priority: class.getDependencies() > instance.getDependencies() > manifest.dependencies
232
- const getDeps = c.class?.getDependencies || c.getDependencies;
233
- const reqs = (typeof getDeps === 'function') ? getDeps.call(c.class || c) : c.dependencies;
234
-
235
- if (Array.isArray(reqs)) {
236
- reqs.forEach(r => needed.set(normalizeName(r), r));
237
- }
238
- });
239
-
240
- if (needed.size === 0) return {};
241
-
242
- logger.log('INFO', `[DependencyFetcher] 🔍 Fetching ${needed.size} dependencies for ${dStr}`);
243
-
244
- const results = {};
245
- const errors = [];
246
-
247
- // 2. Fetch All Dependencies in Parallel
248
- await Promise.all(Array.from(needed.entries()).map(async ([norm, orig]) => {
249
- const category = manifestLookup[norm] || 'analytics';
250
- const path = `${config.resultsCollection || 'computation_results'}/${dStr}/.../${category}/${norm}`;
251
-
252
- try {
253
- const data = await fetchSingleResult(db, { ...config, logger }, dStr, norm, category);
254
-
255
- if (!data || isDataEmpty(data)) {
256
- // Determine severity based on context
257
- const status = data ? 'EMPTY' : 'MISSING';
258
- errors.push({ name: orig, path, reason: status });
259
-
260
- // Log immediately for visibility
261
- const level = allowMissing ? 'INFO' : 'ERROR';
262
- logger.log(level, `[DependencyFetcher] ⚠️ Dependency '${orig}' ${status} at ${path}`);
263
- } else {
264
- results[orig] = data; // Store using Original Name
265
- }
266
- } catch (e) {
267
- errors.push({ name: orig, path, reason: e.message });
268
- logger.log('ERROR', `[DependencyFetcher] ❌ Error loading '${orig}': ${e.message}`);
269
- }
270
- }));
271
-
272
- // 3. Final Validation
273
- if (errors.length > 0 && !allowMissing) {
274
- throw new Error(`[DependencyFetcher] CRITICAL: Missing required dependencies: ${errors.map(e => e.name).join(', ')}`);
275
- } else if (errors.length > 0) {
276
- logger.log('INFO', `[DependencyFetcher] ⚠️ Allowed missing/empty dependencies in Historical context: ${errors.map(e => e.name).join(', ')}`);
277
- }
278
-
279
- return results;
280
- }
281
-
282
- /**
283
- * Fetches result series (Historical data) for lookbacks.
284
- * @param {string} endDateStr - The end date for the series
285
- * @param {string[]} calcNames - Original (case-sensitive) computation names
286
- * @param {Object} manifestLookup - Map of normalizedName -> category
287
- * @param {Object} config - Configuration object
288
- * @param {Object} deps - Dependencies (db, logger)
289
- * @param {number} lookbackDays - Number of days to look back
290
- */
291
- async function fetchResultSeries(endDateStr, calcNames, manifestLookup, config, deps, lookbackDays) {
292
- const { db, logger } = deps;
293
- const results = {}; // normalizedName -> { date -> data }
294
- const { resultsCollection = 'computation_results', resultsSubcollection = 'results', computationsSubcollection = 'computations' } = config;
295
-
296
- // Initialize results structure
297
- calcNames.forEach(n => results[normalizeName(n)] = {});
298
-
299
- // Generate Date List (going backwards from endDate)
300
- const dates = [];
301
- const d = new Date(endDateStr);
302
- for (let i = 0; i < lookbackDays; i++) {
303
- d.setUTCDate(d.getUTCDate() - 1);
304
- dates.push(d.toISOString().slice(0, 10));
305
- }
306
-
307
- const startDateStr = dates[dates.length - 1]; // Oldest date
308
- const queryEndDateStr = dates[0]; // Newest date (for BigQuery query)
309
-
310
- // [DEBUG] Log the manifest lookup and resolved categories
311
- logger.log('INFO', `[DependencyFetcher] 🔍 ManifestLookup has ${Object.keys(manifestLookup).length} entries`);
312
- for (const rawName of calcNames) {
313
- const norm = normalizeName(rawName);
314
- const category = manifestLookup[norm] || 'analytics';
315
- const samplePath = `${resultsCollection}/${dates[0]}/${resultsSubcollection}/${category}/${computationsSubcollection}/${rawName}`;
316
- logger.log('INFO', `[DependencyFetcher] 📍 '${rawName}' -> category='${category}' -> Path: ${samplePath}`);
317
- }
318
-
319
- // =========================================================================
320
- // BIGQUERY FIRST: Try batch query for all dates at once
321
- // =========================================================================
322
- if (process.env.BIGQUERY_ENABLED !== 'false') {
323
- try {
324
- const { queryComputationResultsRange } = require('../../core/utils/bigquery_utils');
325
-
326
- // Query each computation in parallel
327
- const bigqueryPromises = calcNames.map(async (rawName) => {
328
- const norm = normalizeName(rawName);
329
- const category = manifestLookup[norm] || 'analytics';
330
-
331
- const bigqueryRows = await queryComputationResultsRange(
332
- rawName,
333
- category,
334
- startDateStr,
335
- queryEndDateStr,
336
- logger
337
- );
338
-
339
- if (bigqueryRows && bigqueryRows.length > 0) {
340
- logger.log('INFO', `[DependencyFetcher] ✅ Using BigQuery for ${rawName} series: ${bigqueryRows.length} dates`);
341
-
342
- // Map BigQuery results to results structure
343
- for (const row of bigqueryRows) {
344
- if (row.data && !isDataEmpty(row.data)) {
345
- results[norm][row.date] = row.data;
346
- }
347
- }
348
-
349
- return { name: rawName, found: bigqueryRows.length };
350
- }
351
-
352
- return { name: rawName, found: 0 };
353
- });
354
-
355
- const bigqueryResults = await Promise.all(bigqueryPromises);
356
- const totalFound = bigqueryResults.reduce((sum, r) => sum + r.found, 0);
357
-
358
- if (totalFound > 0) {
359
- logger.log('INFO', `[DependencyFetcher] ✅ BigQuery retrieved ${totalFound} computation result records across ${calcNames.length} computations`);
360
-
361
- // Fill in any missing dates from Firestore (fallback)
362
- const missingOps = [];
363
- for (const dateStr of dates) {
364
- for (const rawName of calcNames) {
365
- const norm = normalizeName(rawName);
366
- // Only fetch if we don't have this date already
367
- if (!results[norm] || !results[norm][dateStr]) {
368
- const category = manifestLookup[norm] || 'analytics';
369
- missingOps.push(async () => {
370
- const val = await fetchSingleResult(db, { ...config, logger }, dateStr, rawName, category);
371
- if (val && !isDataEmpty(val)) {
372
- results[norm][dateStr] = val;
373
- }
374
- });
375
- }
376
- }
377
- }
378
-
379
- // Fetch missing dates from Firestore
380
- if (missingOps.length > 0) {
381
- logger.log('INFO', `[DependencyFetcher] 📂 Fetching ${missingOps.length} missing dates from Firestore (fallback)`);
382
- const BATCH_SIZE = 20;
383
- for (let i = 0; i < missingOps.length; i += BATCH_SIZE) {
384
- await Promise.all(missingOps.slice(i, i + BATCH_SIZE).map(fn => fn()));
385
- }
386
- }
387
-
388
- // Log final summary
389
- for (const rawName of calcNames) {
390
- const norm = normalizeName(rawName);
391
- const foundDates = Object.keys(results[norm] || {});
392
- logger.log('INFO', `[DependencyFetcher] ✅ '${rawName}' found data for ${foundDates.length}/${lookbackDays} days (BigQuery + Firestore)`);
393
- }
394
-
395
- return results;
396
- } else {
397
- logger.log('INFO', `[DependencyFetcher] ⚠️ BigQuery returned no results, falling back to Firestore`);
398
- }
399
- } catch (bqError) {
400
- logger.log('WARN', `[DependencyFetcher] BigQuery series query failed, falling back to Firestore: ${bqError.message}`);
401
- }
402
- }
403
-
404
- // =========================================================================
405
- // FIRESTORE FALLBACK: Original logic (backwards compatibility)
406
- // =========================================================================
407
- logger.log('INFO', `[DependencyFetcher] 📂 Using Firestore for computation result series: ${calcNames.length} calcs x ${lookbackDays} days`);
408
-
409
- // Build Fetch Operations
410
- const ops = [];
411
- for (const dateStr of dates) {
412
- for (const rawName of calcNames) {
413
- const norm = normalizeName(rawName);
414
- const category = manifestLookup[norm] || 'analytics';
415
-
416
- ops.push(async () => {
417
- const val = await fetchSingleResult(db, { ...config, logger }, dateStr, rawName, category);
418
- if (val && !isDataEmpty(val)) {
419
- results[norm][dateStr] = val;
420
- }
421
- });
422
- }
423
- }
424
-
425
- // Execute in Batches (Limit Concurrency)
426
- logger.log('INFO', `[DependencyFetcher] 📚 Loading series: ${calcNames.length} calcs x ${lookbackDays} days (${ops.length} ops)`);
427
- const BATCH_SIZE = 20;
428
- for (let i = 0; i < ops.length; i += BATCH_SIZE) {
429
- await Promise.all(ops.slice(i, i + BATCH_SIZE).map(fn => fn()));
430
- }
431
-
432
- // [DEBUG] Log results summary
433
- for (const rawName of calcNames) {
434
- const norm = normalizeName(rawName);
435
- const foundDates = Object.keys(results[norm] || {});
436
- logger.log('INFO', `[DependencyFetcher] ✅ '${rawName}' found data for ${foundDates.length}/${lookbackDays} days`);
437
- }
438
-
439
- return results;
440
- }
441
-
442
- /**
443
- * Bridge function for WorkflowOrchestrator compatibility.
444
- */
445
- async function fetchExistingResults(dateStr, calcs, fullManifest, config, deps, isHistoricalContext) {
446
- const lookup = {};
447
- if (Array.isArray(fullManifest)) {
448
- fullManifest.forEach(c => lookup[normalizeName(c.name)] = c.category || 'analytics');
449
- }
450
- // Ensure Date Object
451
- const dateObj = new Date(dateStr.includes('T') ? dateStr : dateStr + 'T00:00:00Z');
452
- return fetchDependencies(dateObj, calcs, config, deps, lookup, isHistoricalContext);
453
- }
454
-
455
- module.exports = { fetchDependencies, fetchResultSeries, fetchExistingResults };