bulltrackers-module 1.0.657 → 1.0.659

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 (23) hide show
  1. package/functions/api-v2/routes/popular_investors.js +80 -0
  2. package/functions/computation-system/data/AvailabilityChecker.js +163 -317
  3. package/functions/computation-system/data/CachedDataLoader.js +158 -222
  4. package/functions/computation-system/data/DependencyFetcher.js +201 -406
  5. package/functions/computation-system/executors/MetaExecutor.js +176 -280
  6. package/functions/computation-system/executors/StandardExecutor.js +325 -383
  7. package/functions/computation-system/helpers/computation_dispatcher.js +294 -699
  8. package/functions/computation-system/helpers/computation_worker.js +3 -2
  9. package/functions/computation-system/legacy/AvailabilityCheckerOld.js +382 -0
  10. package/functions/computation-system/legacy/CachedDataLoaderOld.js +357 -0
  11. package/functions/computation-system/legacy/DependencyFetcherOld.js +478 -0
  12. package/functions/computation-system/legacy/MetaExecutorold.js +364 -0
  13. package/functions/computation-system/legacy/StandardExecutorold.js +476 -0
  14. package/functions/computation-system/legacy/computation_dispatcherold.js +944 -0
  15. package/functions/computation-system/persistence/ResultCommitter.js +137 -188
  16. package/functions/computation-system/services/SnapshotService.js +129 -0
  17. package/functions/computation-system/tools/BuildReporter.js +12 -7
  18. package/functions/computation-system/utils/data_loader.js +213 -238
  19. package/package.json +3 -2
  20. package/functions/computation-system/workflows/bulltrackers_pipeline.yaml +0 -163
  21. package/functions/computation-system/workflows/data_feeder_pipeline.yaml +0 -115
  22. package/functions/computation-system/workflows/datafeederpipelineinstructions.md +0 -30
  23. package/functions/computation-system/workflows/morning_prep_pipeline.yaml +0 -55
@@ -249,10 +249,11 @@ async function handleComputationTask(message, config, dependencies) {
249
249
 
250
250
  // [FIX] Declare manifest here so it is accessible in both try and catch blocks
251
251
  let manifest;
252
+ const taskStartTime = Date.now(); // Declare outside try/catch for access in error handler
252
253
 
253
254
  try {
254
255
  manifest = getManifest(config.activeProductLines || [], calculations, runDeps);
255
- const startTime = Date.now();
256
+ const startTime = taskStartTime;
256
257
 
257
258
  const result = await executeDispatchTask(
258
259
  date, pass, computation, config, runDeps,
@@ -336,7 +337,7 @@ async function handleComputationTask(message, config, dependencies) {
336
337
  resourceTier: resourceTier
337
338
  }, { merge: true });
338
339
 
339
- await recordRunAttempt(db, { date, computation, pass }, 'FAILURE', { message: err.message, stage: err.stage || 'FATAL' }, { peakMemoryMB: heartbeats.getPeak() }, triggerReason, resourceTier);
340
+ await recordRunAttempt(db, { date, computation, pass }, 'FAILURE', { message: err.message, stage: err.stage || 'FATAL' }, { durationMs: Date.now() - taskStartTime, peakMemoryMB: heartbeats.getPeak() }, triggerReason, resourceTier);
340
341
 
341
342
  // Send error notification if this was an on-demand computation
342
343
  if (metadata?.onDemand && metadata?.requestId && metadata?.requestingUserCid) {
@@ -0,0 +1,382 @@
1
+ /**
2
+ * @fileoverview Checks availability of root data via the Root Data Index.
3
+ * REFACTORED: Fully supports granular flags for PI, Signed-In Users, Rankings, and Verification.
4
+ * UPDATED: Enforces 'mandatoryRoots' metadata to override permissive flags.
5
+ * NEW: Added 'getAvailabilityWindow' for efficient batch availability lookups using range queries.
6
+ */
7
+ const { normalizeName } = require('../utils/utils');
8
+ const { FieldPath } = require('@google-cloud/firestore');
9
+
10
+ const INDEX_COLLECTION = process.env.ROOT_DATA_AVAILABILITY_COLLECTION || 'system_root_data_index';
11
+
12
+ /**
13
+ * Checks if a specific calculation can run based on its dependencies and the current data status.
14
+ * @param {Object} calcManifest - The calculation manifest.
15
+ * @param {Object} rootDataStatus - The availability status object.
16
+ * @returns {Object} { canRun: boolean, missing: Array, available: Array }
17
+ */
18
+ function checkRootDependencies(calcManifest, rootDataStatus) {
19
+ const missing = [];
20
+ const available = [];
21
+
22
+ if (!calcManifest.rootDataDependencies) return { canRun: true, missing, available };
23
+
24
+ // Check if computation can run with missing rootdata
25
+ const canHaveMissingRoots = calcManifest.canHaveMissingRoots === true;
26
+
27
+ // Normalize userType to lowercase for comparison (computations use uppercase)
28
+ const userType = (calcManifest.userType || 'all').toLowerCase();
29
+
30
+ for (const dep of calcManifest.rootDataDependencies) {
31
+ let isAvailable = false;
32
+
33
+ if (dep === 'portfolio') {
34
+ if (userType === 'speculator' && rootDataStatus.speculatorPortfolio) isAvailable = true;
35
+ else if (userType === 'normal' && rootDataStatus.normalPortfolio) isAvailable = true;
36
+ else if (userType === 'popular_investor' && rootDataStatus.piPortfolios) isAvailable = true;
37
+ else if (userType === 'signed_in_user' && rootDataStatus.signedInUserPortfolio) isAvailable = true;
38
+ else if (userType === 'all' && rootDataStatus.hasPortfolio) isAvailable = true;
39
+
40
+ if (!isAvailable) {
41
+ // [OPTIMIZATION] Optimistic Series Check
42
+ if (calcManifest.rootDataSeries && calcManifest.rootDataSeries[dep]) {
43
+ available.push('portfolio');
44
+ continue;
45
+ }
46
+
47
+ if (userType === 'speculator') missing.push('speculatorPortfolio');
48
+ else if (userType === 'normal') missing.push('normalPortfolio');
49
+ else if (userType === 'popular_investor') missing.push('piPortfolios');
50
+ else if (userType === 'signed_in_user') missing.push('signedInUserPortfolio');
51
+ else missing.push('portfolio');
52
+ } else {
53
+ available.push('portfolio');
54
+ }
55
+ }
56
+ else if (dep === 'history') {
57
+ if (userType === 'speculator' && rootDataStatus.speculatorHistory) isAvailable = true;
58
+ else if (userType === 'normal' && rootDataStatus.normalHistory) isAvailable = true;
59
+ else if (userType === 'popular_investor' && rootDataStatus.piHistory) isAvailable = true;
60
+ else if (userType === 'signed_in_user' && rootDataStatus.signedInUserHistory) isAvailable = true;
61
+ else if (userType === 'all' && rootDataStatus.hasHistory) isAvailable = true;
62
+
63
+ if (!isAvailable) {
64
+ if (calcManifest.rootDataSeries && calcManifest.rootDataSeries[dep]) {
65
+ available.push('history');
66
+ continue;
67
+ }
68
+
69
+ if (userType === 'speculator') missing.push('speculatorHistory');
70
+ else if (userType === 'normal') missing.push('normalHistory');
71
+ else if (userType === 'popular_investor') missing.push('piHistory');
72
+ else if (userType === 'signed_in_user') missing.push('signedInUserHistory');
73
+ else missing.push('history');
74
+ } else {
75
+ available.push('history');
76
+ }
77
+ }
78
+ else if (dep === 'rankings') {
79
+ if (rootDataStatus.piRankings) {
80
+ isAvailable = true;
81
+ available.push('rankings');
82
+ } else {
83
+ if (calcManifest.rootDataSeries && calcManifest.rootDataSeries[dep]) {
84
+ available.push('rankings');
85
+ } else {
86
+ missing.push('piRankings');
87
+ }
88
+ }
89
+ }
90
+ else if (dep === 'verification') {
91
+ if (rootDataStatus.signedInUserVerification) {
92
+ isAvailable = true;
93
+ available.push('verification');
94
+ } else {
95
+ missing.push('signedInUserVerification');
96
+ }
97
+ }
98
+ else if (dep === 'insights') {
99
+ if (rootDataStatus.hasInsights) {
100
+ isAvailable = true;
101
+ available.push('insights');
102
+ } else {
103
+ if (calcManifest.rootDataSeries && calcManifest.rootDataSeries[dep]) {
104
+ available.push('insights');
105
+ } else {
106
+ missing.push('insights');
107
+ }
108
+ }
109
+ }
110
+ else if (dep === 'social') {
111
+ if (userType === 'popular_investor') {
112
+ if (rootDataStatus.hasPISocial) isAvailable = true;
113
+ } else if (userType === 'signed_in_user') {
114
+ if (rootDataStatus.hasSignedInSocial) isAvailable = true;
115
+ } else {
116
+ if (rootDataStatus.hasSocial) isAvailable = true;
117
+ }
118
+
119
+ if (isAvailable) {
120
+ available.push('social');
121
+ } else {
122
+ if (calcManifest.rootDataSeries && calcManifest.rootDataSeries[dep]) {
123
+ available.push('social');
124
+ continue;
125
+ }
126
+
127
+ if (userType === 'popular_investor') missing.push('piSocial');
128
+ else if (userType === 'signed_in_user') missing.push('signedInSocial');
129
+ else missing.push('social');
130
+ }
131
+ }
132
+ else if (dep === 'price') {
133
+ if (rootDataStatus.hasPrices) {
134
+ isAvailable = true;
135
+ available.push('price');
136
+ } else {
137
+ if (calcManifest.rootDataSeries && calcManifest.rootDataSeries[dep]) {
138
+ available.push('price');
139
+ } else {
140
+ missing.push('price');
141
+ }
142
+ }
143
+ }
144
+ else if (dep === 'ratings') {
145
+ if (rootDataStatus.piRatings) {
146
+ isAvailable = true;
147
+ available.push('ratings');
148
+ } else {
149
+ if (calcManifest.rootDataSeries && calcManifest.rootDataSeries[dep]) {
150
+ available.push('ratings');
151
+ } else {
152
+ missing.push('piRatings');
153
+ }
154
+ }
155
+ }
156
+ else if (dep === 'pageViews') {
157
+ if (rootDataStatus.piPageViews) {
158
+ isAvailable = true;
159
+ available.push('pageViews');
160
+ } else {
161
+ if (calcManifest.rootDataSeries && calcManifest.rootDataSeries[dep]) {
162
+ available.push('pageViews');
163
+ } else {
164
+ missing.push('piPageViews');
165
+ }
166
+ }
167
+ }
168
+ else if (dep === 'watchlist') {
169
+ if (rootDataStatus.watchlistMembership) {
170
+ isAvailable = true;
171
+ available.push('watchlist');
172
+ } else {
173
+ if (calcManifest.rootDataSeries && calcManifest.rootDataSeries[dep]) {
174
+ available.push('watchlist');
175
+ } else {
176
+ missing.push('watchlistMembership');
177
+ }
178
+ }
179
+ }
180
+ else if (dep === 'alerts') {
181
+ if (rootDataStatus.piAlertHistory) {
182
+ isAvailable = true;
183
+ available.push('alerts');
184
+ } else {
185
+ if (calcManifest.rootDataSeries && calcManifest.rootDataSeries[dep]) {
186
+ available.push('alerts');
187
+ } else {
188
+ missing.push('piAlertHistory');
189
+ }
190
+ }
191
+ }
192
+ }
193
+
194
+ // [NEW] Enforce Mandatory Roots (defined by computation)
195
+ // This allows granular control: "I need portfolio, but ratings are optional"
196
+ if (calcManifest.mandatoryRoots && Array.isArray(calcManifest.mandatoryRoots) && calcManifest.mandatoryRoots.length > 0) {
197
+ const missingMandatory = calcManifest.mandatoryRoots.filter(r => !available.includes(r));
198
+ if (missingMandatory.length > 0) {
199
+ return { canRun: false, missing, available };
200
+ }
201
+ }
202
+
203
+ if (canHaveMissingRoots) {
204
+ const canRun = available.length > 0;
205
+ return { canRun, missing, available };
206
+ }
207
+
208
+ // Strict mode: all required rootdata must be available
209
+ return { canRun: missing.length === 0, missing, available };
210
+ }
211
+
212
+ function getViableCalculations(candidates, fullManifest, rootDataStatus, dailyStatus) {
213
+ const viable = [];
214
+ const manifestMap = new Map(fullManifest.map(c => [normalizeName(c.name), c]));
215
+
216
+ for (const calc of candidates) {
217
+ const rootCheck = checkRootDependencies(calc, rootDataStatus);
218
+ if (!rootCheck.canRun) continue;
219
+
220
+ let dependenciesMet = true;
221
+ if (calc.dependencies && calc.dependencies.length > 0) {
222
+ for (const depName of calc.dependencies) {
223
+ const normDep = normalizeName(depName);
224
+ const storedHash = dailyStatus[normDep];
225
+ const depManifest = manifestMap.get(normDep);
226
+
227
+ if (!depManifest) { dependenciesMet = false; break; }
228
+ if (!storedHash) { dependenciesMet = false; break; }
229
+ if (storedHash.hash !== depManifest.hash) { dependenciesMet = false; break; }
230
+ }
231
+ }
232
+
233
+ if (dependenciesMet) { viable.push(calc); }
234
+ }
235
+
236
+ return viable;
237
+ }
238
+
239
+ /**
240
+ * Checks root data availability for a single date.
241
+ */
242
+ async function checkRootDataAvailability(dateStr, config, dependencies, earliestDates) {
243
+ const { logger, db } = dependencies;
244
+
245
+ try {
246
+ const indexDoc = await db.collection(INDEX_COLLECTION).doc(dateStr).get();
247
+
248
+ if (indexDoc.exists) {
249
+ const data = indexDoc.data();
250
+ const details = data.details || {};
251
+
252
+ return {
253
+ status: {
254
+ hasPortfolio: !!data.hasPortfolio,
255
+ hasHistory: !!data.hasHistory,
256
+ hasSocial: !!data.hasSocial,
257
+ hasInsights: !!data.hasInsights,
258
+ hasPrices: !!data.hasPrices,
259
+ speculatorPortfolio: !!details.speculatorPortfolio,
260
+ normalPortfolio: !!details.normalPortfolio,
261
+ speculatorHistory: !!details.speculatorHistory,
262
+ normalHistory: !!details.normalHistory,
263
+ piRankings: !!details.piRankings,
264
+ piPortfolios: !!details.piPortfolios,
265
+ piDeepPortfolios: !!details.piDeepPortfolios,
266
+ piHistory: !!details.piHistory,
267
+ signedInUserPortfolio: !!details.signedInUserPortfolio,
268
+ signedInUserHistory: !!details.signedInUserHistory,
269
+ signedInUserVerification: !!details.signedInUserVerification,
270
+ hasPISocial: !!details.hasPISocial || !!data.hasPISocial,
271
+ hasSignedInSocial: !!details.hasSignedInSocial || !!data.hasSignedInSocial,
272
+ piRatings: !!details.piRatings,
273
+ piPageViews: !!details.piPageViews,
274
+ watchlistMembership: !!details.watchlistMembership,
275
+ piAlertHistory: !!details.piAlertHistory,
276
+ piMasterList: !!details.piMasterList
277
+ },
278
+ portfolioRefs: null,
279
+ historyRefs: null,
280
+ todayInsights: null,
281
+ todaySocialPostInsights: null,
282
+ yesterdayPortfolioRefs: null
283
+ };
284
+ } else {
285
+ logger.log('WARN', `[Availability] Index not found for ${dateStr}. Assuming NO data.`);
286
+ return {
287
+ status: {
288
+ hasPortfolio: false,
289
+ hasHistory: false,
290
+ hasSocial: false,
291
+ hasInsights: false,
292
+ hasPrices: false,
293
+ speculatorPortfolio: false,
294
+ normalPortfolio: false,
295
+ speculatorHistory: false,
296
+ normalHistory: false,
297
+ piRankings: false,
298
+ piPortfolios: false,
299
+ piDeepPortfolios: false,
300
+ piHistory: false,
301
+ signedInUserPortfolio: false,
302
+ signedInUserHistory: false,
303
+ signedInUserVerification: false,
304
+ hasPISocial: false,
305
+ hasSignedInSocial: false,
306
+ piRatings: false,
307
+ piPageViews: false,
308
+ watchlistMembership: false,
309
+ piAlertHistory: false
310
+ }
311
+ };
312
+ }
313
+
314
+ } catch (err) {
315
+ logger.log('ERROR', `Error checking availability index: ${err.message}`);
316
+ return null;
317
+ }
318
+ }
319
+
320
+ /**
321
+ * [NEW] Fetches availability status for a range of dates.
322
+ * Uses a range query to only retrieve indices that actually exist, preventing wasted reads on empty days.
323
+ * @param {Object} deps - Dependencies (must include db)
324
+ * @param {string} startDateStr - ISO Date string (YYYY-MM-DD) inclusive start
325
+ * @param {string} endDateStr - ISO Date string (YYYY-MM-DD) inclusive end
326
+ * @returns {Promise<Map<string, Object>>} Map of dateStr -> status object
327
+ */
328
+ async function getAvailabilityWindow(deps, startDateStr, endDateStr) {
329
+ const { db } = deps;
330
+
331
+ // Perform Range Query on Document ID (Date String)
332
+ const snapshot = await db.collection(INDEX_COLLECTION)
333
+ .where(FieldPath.documentId(), '>=', startDateStr)
334
+ .where(FieldPath.documentId(), '<=', endDateStr)
335
+ .get();
336
+
337
+ const availabilityMap = new Map();
338
+
339
+ snapshot.forEach(doc => {
340
+ const data = doc.data();
341
+ const details = data.details || {};
342
+ const dateStr = doc.id;
343
+
344
+ // Construct status object matching checkRootDataAvailability structure
345
+ const status = {
346
+ hasPortfolio: !!data.hasPortfolio,
347
+ hasHistory: !!data.hasHistory,
348
+ hasSocial: !!data.hasSocial,
349
+ hasInsights: !!data.hasInsights,
350
+ hasPrices: !!data.hasPrices,
351
+ speculatorPortfolio: !!details.speculatorPortfolio,
352
+ normalPortfolio: !!details.normalPortfolio,
353
+ speculatorHistory: !!details.speculatorHistory,
354
+ normalHistory: !!details.normalHistory,
355
+ piRankings: !!details.piRankings,
356
+ piPortfolios: !!details.piPortfolios,
357
+ piDeepPortfolios: !!details.piDeepPortfolios,
358
+ piHistory: !!details.piHistory,
359
+ signedInUserPortfolio: !!details.signedInUserPortfolio,
360
+ signedInUserHistory: !!details.signedInUserHistory,
361
+ signedInUserVerification: !!details.signedInUserVerification,
362
+ hasPISocial: !!details.hasPISocial || !!data.hasPISocial,
363
+ hasSignedInSocial: !!details.hasSignedInSocial || !!data.hasSignedInSocial,
364
+ piRatings: !!details.piRatings,
365
+ piPageViews: !!details.piPageViews,
366
+ watchlistMembership: !!details.watchlistMembership,
367
+ piAlertHistory: !!details.piAlertHistory,
368
+ piMasterList: !!details.piMasterList
369
+ };
370
+
371
+ availabilityMap.set(dateStr, status);
372
+ });
373
+
374
+ return availabilityMap;
375
+ }
376
+
377
+ module.exports = {
378
+ checkRootDependencies,
379
+ checkRootDataAvailability,
380
+ getViableCalculations,
381
+ getAvailabilityWindow
382
+ };