bulltrackers-module 1.0.658 → 1.0.660

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 (22) hide show
  1. package/functions/computation-system/data/AvailabilityChecker.js +163 -317
  2. package/functions/computation-system/data/CachedDataLoader.js +158 -222
  3. package/functions/computation-system/data/DependencyFetcher.js +201 -406
  4. package/functions/computation-system/executors/MetaExecutor.js +176 -280
  5. package/functions/computation-system/executors/StandardExecutor.js +325 -383
  6. package/functions/computation-system/helpers/computation_dispatcher.js +306 -701
  7. package/functions/computation-system/helpers/computation_worker.js +3 -2
  8. package/functions/computation-system/legacy/AvailabilityCheckerOld.js +382 -0
  9. package/functions/computation-system/legacy/CachedDataLoaderOld.js +357 -0
  10. package/functions/computation-system/legacy/DependencyFetcherOld.js +478 -0
  11. package/functions/computation-system/legacy/MetaExecutorold.js +364 -0
  12. package/functions/computation-system/legacy/StandardExecutorold.js +476 -0
  13. package/functions/computation-system/legacy/computation_dispatcherold.js +944 -0
  14. package/functions/computation-system/persistence/ResultCommitter.js +137 -188
  15. package/functions/computation-system/services/SnapshotService.js +129 -0
  16. package/functions/computation-system/tools/BuildReporter.js +12 -7
  17. package/functions/computation-system/utils/data_loader.js +213 -238
  18. package/package.json +3 -2
  19. package/functions/computation-system/workflows/bulltrackers_pipeline.yaml +0 -163
  20. package/functions/computation-system/workflows/data_feeder_pipeline.yaml +0 -115
  21. package/functions/computation-system/workflows/datafeederpipelineinstructions.md +0 -30
  22. package/functions/computation-system/workflows/morning_prep_pipeline.yaml +0 -55
@@ -1,239 +1,186 @@
1
1
  /**
2
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.
3
+ * REFACTORED: Config-driven dependency checking and shared status normalization.
6
4
  */
7
5
  const { normalizeName } = require('../utils/utils');
8
6
  const { FieldPath } = require('@google-cloud/firestore');
9
7
 
10
8
  const INDEX_COLLECTION = process.env.ROOT_DATA_AVAILABILITY_COLLECTION || 'system_root_data_index';
11
9
 
10
+ // =============================================================================
11
+ // CONFIGURATION: Dependency Mappings
12
+ // =============================================================================
13
+
14
+ // Dependencies that map directly to a single status flag
15
+ const SIMPLE_DEP_MAP = {
16
+ rankings: 'piRankings',
17
+ verification: 'signedInUserVerification',
18
+ insights: 'hasInsights',
19
+ price: 'hasPrices',
20
+ ratings: 'piRatings',
21
+ pageViews: 'piPageViews',
22
+ watchlist: 'watchlistMembership',
23
+ alerts: 'piAlertHistory'
24
+ };
25
+
26
+ // Dependencies that vary based on the userType of the calculation
27
+ const COMPLEX_DEP_MAP = {
28
+ portfolio: {
29
+ speculator: 'speculatorPortfolio',
30
+ normal: 'normalPortfolio',
31
+ popular_investor: 'piPortfolios',
32
+ signed_in_user: 'signedInUserPortfolio',
33
+ _default: 'hasPortfolio'
34
+ },
35
+ history: {
36
+ speculator: 'speculatorHistory',
37
+ normal: 'normalHistory',
38
+ popular_investor: 'piHistory',
39
+ signed_in_user: 'signedInUserHistory',
40
+ _default: 'hasHistory'
41
+ },
42
+ social: {
43
+ popular_investor: 'hasPISocial',
44
+ signed_in_user: 'hasSignedInSocial',
45
+ _default: 'hasSocial'
46
+ }
47
+ };
48
+
49
+ // =============================================================================
50
+ // LOGIC: Dependency Checking
51
+ // =============================================================================
52
+
12
53
  /**
13
54
  * 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
55
  */
18
56
  function checkRootDependencies(calcManifest, rootDataStatus) {
19
57
  const missing = [];
20
58
  const available = [];
21
-
22
- if (!calcManifest.rootDataDependencies) return { canRun: true, missing, available };
59
+ const deps = calcManifest.rootDataDependencies || [];
23
60
 
24
- // Check if computation can run with missing rootdata
25
- const canHaveMissingRoots = calcManifest.canHaveMissingRoots === true;
61
+ if (!deps.length) return { canRun: true, missing, available };
26
62
 
27
- // Normalize userType to lowercase for comparison (computations use uppercase)
28
63
  const userType = (calcManifest.userType || 'all').toLowerCase();
64
+ const canHaveMissing = calcManifest.canHaveMissingRoots === true;
29
65
 
30
- for (const dep of calcManifest.rootDataDependencies) {
66
+ for (const dep of deps) {
31
67
  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
- }
68
+ let missingKey = dep;
69
+
70
+ // 1. Resolve Status Key
71
+ if (SIMPLE_DEP_MAP[dep]) {
72
+ const key = SIMPLE_DEP_MAP[dep];
73
+ if (rootDataStatus[key]) isAvailable = true;
74
+ else missingKey = key;
75
+ }
76
+ else if (COMPLEX_DEP_MAP[dep]) {
77
+ const map = COMPLEX_DEP_MAP[dep];
78
+ const key = map[userType] || map._default;
79
+ if (rootDataStatus[key]) isAvailable = true;
80
+ else missingKey = key;
55
81
  }
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
82
 
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');
83
+ // 2. Check Availability (with Optimistic Series Fallback)
84
+ if (isAvailable) {
85
+ available.push(dep);
86
+ } else {
87
+ // [OPTIMIZATION] If series data is requested, we can sometimes proceed without the daily snapshot
88
+ if (calcManifest.rootDataSeries?.[dep]) {
89
+ available.push(dep);
184
90
  } else {
185
- if (calcManifest.rootDataSeries && calcManifest.rootDataSeries[dep]) {
186
- available.push('alerts');
187
- } else {
188
- missing.push('piAlertHistory');
189
- }
91
+ missing.push(missingKey);
190
92
  }
191
93
  }
192
94
  }
193
95
 
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) {
96
+ // 3. Enforce Mandatory Roots (Granular Override)
97
+ if (calcManifest.mandatoryRoots?.length) {
98
+ if (calcManifest.mandatoryRoots.some(r => !available.includes(r))) {
199
99
  return { canRun: false, missing, available };
200
100
  }
201
101
  }
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 };
102
+
103
+ return {
104
+ canRun: canHaveMissing ? available.length > 0 : missing.length === 0,
105
+ missing,
106
+ available
107
+ };
210
108
  }
211
109
 
110
+ /**
111
+ * filters a list of calculations to those that can run given the current data.
112
+ */
212
113
  function getViableCalculations(candidates, fullManifest, rootDataStatus, dailyStatus) {
213
- const viable = [];
214
114
  const manifestMap = new Map(fullManifest.map(c => [normalizeName(c.name), c]));
215
115
 
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
- }
116
+ return candidates.filter(calc => {
117
+ // 1. Check Root Data
118
+ const { canRun } = checkRootDependencies(calc, rootDataStatus);
119
+ if (!canRun) return false;
120
+
121
+ // 2. Check Computed Dependencies (Hashes must match)
122
+ if (calc.dependencies?.length) {
123
+ return calc.dependencies.every(depName => {
124
+ const norm = normalizeName(depName);
125
+ const stored = dailyStatus[norm];
126
+ const ref = manifestMap.get(norm);
127
+ // Dependency must exist, have run, and match the current manifest hash
128
+ return ref && stored && stored.hash === ref.hash;
129
+ });
231
130
  }
131
+ return true;
132
+ });
133
+ }
232
134
 
233
- if (dependenciesMet) { viable.push(calc); }
234
- }
135
+ // =============================================================================
136
+ // DATA ACCESS: Status Normalization
137
+ // =============================================================================
235
138
 
236
- return viable;
139
+ /**
140
+ * Standardizes the Firestore document into a flat status object.
141
+ * Handles fallbacks and merges 'details' fields.
142
+ */
143
+ function normalizeStatus(data) {
144
+ // If data is null/undefined, return object with all false
145
+ const d = data || {};
146
+ const det = d.details || {};
147
+
148
+ // Helper to check both root level and details object
149
+ const val = (key, rootFallback) => !!det[key] || (rootFallback ? !!d[rootFallback] : false);
150
+
151
+ return {
152
+ // Core Flags
153
+ hasPortfolio: !!d.hasPortfolio,
154
+ hasHistory: !!d.hasHistory,
155
+ hasSocial: !!d.hasSocial,
156
+ hasInsights: !!d.hasInsights,
157
+ hasPrices: !!d.hasPrices,
158
+
159
+ // Granular Portfolio/History
160
+ speculatorPortfolio: !!det.speculatorPortfolio,
161
+ normalPortfolio: !!det.normalPortfolio,
162
+ piPortfolios: !!det.piPortfolios,
163
+ piDeepPortfolios: !!det.piDeepPortfolios,
164
+ signedInUserPortfolio: !!det.signedInUserPortfolio,
165
+
166
+ speculatorHistory: !!det.speculatorHistory,
167
+ normalHistory: !!det.normalHistory,
168
+ piHistory: !!det.piHistory,
169
+ signedInUserHistory: !!det.signedInUserHistory,
170
+
171
+ // Meta Types
172
+ piRankings: !!det.piRankings,
173
+ signedInUserVerification: !!det.signedInUserVerification,
174
+ hasPISocial: val('hasPISocial', 'hasPISocial'),
175
+ hasSignedInSocial: val('hasSignedInSocial', 'hasSignedInSocial'),
176
+
177
+ // Extended Root Types
178
+ piRatings: !!det.piRatings,
179
+ piPageViews: !!det.piPageViews,
180
+ watchlistMembership: !!det.watchlistMembership,
181
+ piAlertHistory: !!det.piAlertHistory,
182
+ piMasterList: !!det.piMasterList
183
+ };
237
184
  }
238
185
 
239
186
  /**
@@ -241,137 +188,36 @@ function getViableCalculations(candidates, fullManifest, rootDataStatus, dailySt
241
188
  */
242
189
  async function checkRootDataAvailability(dateStr, config, dependencies, earliestDates) {
243
190
  const { logger, db } = dependencies;
244
-
245
191
  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
- };
192
+ const doc = await db.collection(INDEX_COLLECTION).doc(dateStr).get();
193
+ if (!doc.exists) {
194
+ logger.log('WARN', `[Availability] Index not found for ${dateStr}.`);
195
+ return { status: normalizeStatus(null) };
312
196
  }
313
-
314
- } catch (err) {
315
- logger.log('ERROR', `Error checking availability index: ${err.message}`);
316
- return null;
197
+ return {
198
+ status: normalizeStatus(doc.data()),
199
+ // Legacy placeholders preserved for compatibility
200
+ portfolioRefs: null, historyRefs: null, todayInsights: null, todaySocialPostInsights: null, yesterdayPortfolioRefs: null
201
+ };
202
+ } catch (err) {
203
+ logger.log('ERROR', `Error checking availability index: ${err.message}`);
204
+ return null;
317
205
  }
318
206
  }
319
207
 
320
208
  /**
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
209
+ * Fetches availability status for a range of dates.
327
210
  */
328
211
  async function getAvailabilityWindow(deps, startDateStr, endDateStr) {
329
212
  const { db } = deps;
330
-
331
- // Perform Range Query on Document ID (Date String)
332
213
  const snapshot = await db.collection(INDEX_COLLECTION)
333
214
  .where(FieldPath.documentId(), '>=', startDateStr)
334
215
  .where(FieldPath.documentId(), '<=', endDateStr)
335
216
  .get();
336
217
 
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;
218
+ const map = new Map();
219
+ snapshot.forEach(doc => map.set(doc.id, normalizeStatus(doc.data())));
220
+ return map;
375
221
  }
376
222
 
377
223
  module.exports = {
@@ -379,4 +225,4 @@ module.exports = {
379
225
  checkRootDataAvailability,
380
226
  getViableCalculations,
381
227
  getAvailabilityWindow
382
- };
228
+ };