bulltrackers-module 1.0.658 → 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 (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 +294 -699
  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
@@ -0,0 +1,364 @@
1
+ /**
2
+ * @fileoverview Executor for "Meta" (global) calculations.
3
+ * UPDATED: Uses CachedDataLoader for all data access.
4
+ * UPDATED: Tracks processed shard/item counts.
5
+ * UPDATED: Sends 'isInitialWrite: true' for robust cleanup.
6
+ * UPDATED: Support for historical rankings in Meta Context.
7
+ * UPDATED: Added support for loading Series Data (Root & Results) for lookbacks.
8
+ * UPDATED: Builds Manifest Lookup for DependencyFetcher.
9
+ */
10
+ const { normalizeName } = require('../utils/utils');
11
+ const { CachedDataLoader } = require('../data/CachedDataLoader');
12
+ const { ContextFactory } = require('../context/ContextFactory');
13
+ const { commitResults } = require('../persistence/ResultCommitter');
14
+ const { fetchResultSeries, fetchDependencies } = require('../data/DependencyFetcher');
15
+ const { getManifest } = require('../topology/ManifestLoader');
16
+
17
+ class MetaExecutor {
18
+ static async run(date, calcs, passName, config, deps, rootData, fetchedDeps, previousFetchedDeps) {
19
+ const dStr = date.toISOString().slice(0, 10);
20
+ const { logger, db } = deps;
21
+ const loader = new CachedDataLoader(config, deps);
22
+
23
+ // --- [FIXED] Build Global Manifest Lookup using getManifest ---
24
+ // getManifest is typically cached, so this is cheap.
25
+ const allManifests = getManifest(config.productLines, config.calculationsDirectory, deps);
26
+ const manifestLookup = {};
27
+ allManifests.forEach(m => { manifestLookup[normalizeName(m.name)] = m.category; });
28
+
29
+ // [FIX] Check if any meta calculation needs history
30
+ const needsHistory = calcs.some(c => c.isHistorical);
31
+ let rankingsYesterday = null;
32
+
33
+ if (needsHistory) {
34
+ const prevDate = new Date(date);
35
+ prevDate.setUTCDate(prevDate.getUTCDate() - 1);
36
+ const prevStr = prevDate.toISOString().slice(0, 10);
37
+ logger.log('INFO', `[MetaExecutor] Loading historical rankings for ${prevStr}`);
38
+ rankingsYesterday = await loader.loadRankings(prevStr);
39
+ }
40
+
41
+ // 1. Load Global Dependencies
42
+ const [mappings, rankings, verifications] = await Promise.all([
43
+ loader.loadMappings(),
44
+ loader.loadRankings(dStr),
45
+ loader.loadVerifications()
46
+ ]);
47
+
48
+ // [NEW] Load New Root Data Types for Profile Metrics (if any calc needs them)
49
+ const needsNewRootData = calcs.some(c => {
50
+ const deps = c.rootDataDependencies || [];
51
+ return deps.includes('ratings') || deps.includes('pageViews') ||
52
+ deps.includes('watchlist') || deps.includes('alerts');
53
+ });
54
+
55
+ let ratings = null, pageViews = null, watchlistMembership = null, alertHistory = null;
56
+ if (needsNewRootData) {
57
+ const loadPromises = [];
58
+
59
+ // Ratings
60
+ if (calcs.some(c => c.rootDataDependencies?.includes('ratings'))) {
61
+ loadPromises.push(loader.loadRatings(dStr).then(r => { ratings = r; }).catch(e => {
62
+ const allAllowMissing = calcs.every(c => {
63
+ const needsRatings = c.rootDataDependencies?.includes('ratings');
64
+ return !needsRatings || c.canHaveMissingRoots === true;
65
+ });
66
+ if (allAllowMissing) { ratings = null; } else { throw e; }
67
+ }));
68
+ }
69
+
70
+ // PageViews
71
+ if (calcs.some(c => c.rootDataDependencies?.includes('pageViews'))) {
72
+ loadPromises.push(loader.loadPageViews(dStr).then(pv => { pageViews = pv; }).catch(e => {
73
+ const allAllowMissing = calcs.every(c => {
74
+ const needsPageViews = c.rootDataDependencies?.includes('pageViews');
75
+ return !needsPageViews || c.canHaveMissingRoots === true;
76
+ });
77
+ if (allAllowMissing) { pageViews = null; } else { throw e; }
78
+ }));
79
+ }
80
+
81
+ // Watchlist
82
+ if (calcs.some(c => c.rootDataDependencies?.includes('watchlist'))) {
83
+ loadPromises.push(loader.loadWatchlistMembership(dStr).then(w => { watchlistMembership = w; }).catch(e => {
84
+ const allAllowMissing = calcs.every(c => {
85
+ const needsWatchlist = c.rootDataDependencies?.includes('watchlist');
86
+ return !needsWatchlist || c.canHaveMissingRoots === true;
87
+ });
88
+ if (allAllowMissing) { watchlistMembership = null; } else { throw e; }
89
+ }));
90
+ }
91
+
92
+ // Alerts
93
+ if (calcs.some(c => c.rootDataDependencies?.includes('alerts'))) {
94
+ loadPromises.push(loader.loadAlertHistory(dStr).then(a => { alertHistory = a; }).catch(e => {
95
+ const allAllowMissing = calcs.every(c => {
96
+ const needsAlerts = c.rootDataDependencies?.includes('alerts');
97
+ return !needsAlerts || c.canHaveMissingRoots === true;
98
+ });
99
+ if (allAllowMissing) { alertHistory = null; } else { throw e; }
100
+ }));
101
+ }
102
+
103
+ await Promise.all(loadPromises);
104
+
105
+ // [FIX] Enforce canHaveMissingRoots - validate after loading
106
+ for (const c of calcs) {
107
+ const deps = c.rootDataDependencies || [];
108
+ const canSkip = c.canHaveMissingRoots === true;
109
+ const isMissing = (key, val) => deps.includes(key) && (val === null || val === undefined);
110
+
111
+ if (!canSkip) {
112
+ if (isMissing('ratings', ratings)) throw new Error(`[MetaExecutor] Missing required root 'ratings' for ${c.name}`);
113
+ if (isMissing('pageViews', pageViews)) throw new Error(`[MetaExecutor] Missing required root 'pageViews' for ${c.name}`);
114
+ if (isMissing('watchlist', watchlistMembership)) throw new Error(`[MetaExecutor] Missing required root 'watchlist' for ${c.name}`);
115
+ if (isMissing('alerts', alertHistory)) throw new Error(`[MetaExecutor] Missing required root 'alerts' for ${c.name}`);
116
+ }
117
+ }
118
+ }
119
+
120
+ // --- [NEW] Series / Lookback Loading Logic ---
121
+ const rootSeriesRequests = {};
122
+ const dependencySeriesRequests = {};
123
+
124
+ // 1. Identify requirements from manifests
125
+ calcs.forEach(c => {
126
+ if (c.rootDataSeries) {
127
+ Object.entries(c.rootDataSeries).forEach(([type, conf]) => {
128
+ const days = typeof conf === 'object' ? conf.lookback : conf;
129
+ if (!rootSeriesRequests[type] || days > rootSeriesRequests[type]) {
130
+ rootSeriesRequests[type] = days;
131
+ }
132
+ });
133
+ }
134
+ if (c.dependencySeries) {
135
+ Object.entries(c.dependencySeries).forEach(([depName, conf]) => {
136
+ const days = typeof conf === 'object' ? conf.lookback : conf;
137
+ const normalized = normalizeName(depName);
138
+ if (!dependencySeriesRequests[normalized] || days > dependencySeriesRequests[normalized]) {
139
+ dependencySeriesRequests[normalized] = days;
140
+ }
141
+ });
142
+ }
143
+ });
144
+
145
+ const seriesData = { root: {}, results: {} };
146
+
147
+ // 2. Load Root Series
148
+ for (const [type, days] of Object.entries(rootSeriesRequests)) {
149
+ let loaderMethod = null;
150
+ if (type === 'alerts') loaderMethod = 'loadAlertHistory';
151
+ else if (type === 'insights') loaderMethod = 'loadInsights';
152
+ else if (type === 'ratings') loaderMethod = 'loadRatings';
153
+ else if (type === 'watchlist') loaderMethod = 'loadWatchlistMembership';
154
+ // [CRITICAL UPDATE] Add rankings support for Meta lookbacks
155
+ else if (type === 'rankings') loaderMethod = 'loadRankings';
156
+
157
+ if (loaderMethod) {
158
+ logger.log('INFO', `[MetaExecutor] Loading ${days}-day series for Root Data '${type}'...`);
159
+ const series = await loader.loadSeries(loaderMethod, dStr, days);
160
+ seriesData.root[type] = series.data;
161
+ }
162
+ }
163
+
164
+ // 3. Load Computation Result Series
165
+ const calcNamesToFetch = Object.keys(dependencySeriesRequests);
166
+ if (calcNamesToFetch.length > 0) {
167
+ const maxDays = Math.max(...Object.values(dependencySeriesRequests));
168
+ logger.log('INFO', `[MetaExecutor] Loading up to ${maxDays}-day series for Dependencies: ${calcNamesToFetch.join(', ')}`);
169
+
170
+ // Pass manifestLookup to fetcher
171
+ const resultsSeries = await fetchResultSeries(dStr, calcNamesToFetch, manifestLookup, config, deps, maxDays);
172
+ seriesData.results = resultsSeries;
173
+ }
174
+
175
+ const state = {};
176
+ for (const c of calcs) {
177
+ const inst = new c.class();
178
+ inst.manifest = c;
179
+
180
+ // DEBUG: Log what dependencies were fetched
181
+ const fetchedDepKeys = Object.keys(fetchedDeps || {});
182
+ logger.log('INFO', `[MetaExecutor] 📦 Fetched dependencies available: ${fetchedDepKeys.length > 0 ? fetchedDepKeys.join(', ') : 'NONE'}`);
183
+
184
+ const context = ContextFactory.buildMetaContext({
185
+ dateStr: dStr,
186
+ metadata: c,
187
+ mappings,
188
+ insights: { today: rootData.todayInsights },
189
+ socialData: { today: rootData.todaySocialPostInsights },
190
+ computedDependencies: fetchedDeps,
191
+ previousComputedDependencies: previousFetchedDeps,
192
+ config, deps,
193
+ allRankings: rankings,
194
+ allRankingsYesterday: rankingsYesterday,
195
+ allVerifications: verifications,
196
+ ratings: ratings || {},
197
+ pageViews: pageViews || {},
198
+ watchlistMembership: watchlistMembership || {},
199
+ alertHistory: alertHistory || {},
200
+ seriesData
201
+ });
202
+
203
+ // DEBUG: Log dependency availability
204
+ // CRITICAL: Use class.getDependencies() to get original case-sensitive names
205
+ const depNames = (c.class && typeof c.class.getDependencies === 'function')
206
+ ? c.class.getDependencies()
207
+ : (c.getDependencies ? c.getDependencies() : (c.dependencies || []));
208
+ depNames.forEach(depName => {
209
+ const depData = context.computed[depName];
210
+ if (depData) {
211
+ const keys = Object.keys(depData);
212
+ logger.log('INFO', `[MetaExecutor] ✅ Dependency '${depName}' available for ${c.name}. Keys: ${keys.length} (sample: ${keys.slice(0, 5).join(', ')})`);
213
+ } else {
214
+ logger.log('ERROR', `[MetaExecutor] ❌ Dependency '${depName}' MISSING for ${c.name} in context.computed`);
215
+ }
216
+ });
217
+
218
+ try {
219
+ const result = await inst.process(context);
220
+
221
+ // DEBUG: Log result before saving
222
+ if (result && typeof result === 'object') {
223
+ const resultKeys = Object.keys(result);
224
+ logger.log('INFO', `[MetaExecutor] ✅ ${c.name} computed result. Keys: ${resultKeys.length} (sample: ${resultKeys.slice(0, 10).join(', ')})`);
225
+ if (resultKeys.length === 0) {
226
+ logger.log('WARN', `[MetaExecutor] ⚠️ ${c.name} returned EMPTY result object!`);
227
+ }
228
+ } else {
229
+ logger.log('WARN', `[MetaExecutor] ⚠️ ${c.name} returned non-object result: ${typeof result} - ${result}`);
230
+ }
231
+
232
+ // CRITICAL FIX: Do NOT overwrite this.results - the computation already sets it correctly
233
+ // The computation's process() method sets this.results, and getResult() returns it
234
+ // We only use the return value for logging/debugging
235
+ // inst.results should already be set by the computation's process() method
236
+ if (!inst.results) {
237
+ logger.log('WARN', `[MetaExecutor] ⚠️ ${c.name} did not set this.results - using return value as fallback`);
238
+ inst.results = result;
239
+ }
240
+
241
+ // DEBUG: Verify what getResult() will return
242
+ const finalResult = await inst.getResult();
243
+ if (finalResult && typeof finalResult === 'object') {
244
+ const finalKeys = Object.keys(finalResult);
245
+ logger.log('INFO', `[MetaExecutor] ✅ ${c.name} getResult() will return ${finalKeys.length} keys`);
246
+ } else {
247
+ logger.log('WARN', `[MetaExecutor] ⚠️ ${c.name} getResult() returns: ${typeof finalResult}`);
248
+ }
249
+
250
+ state[c.name] = inst;
251
+ } catch (e) {
252
+ logger.log('ERROR', `Meta calc ${c.name} failed: ${e.message}`);
253
+ }
254
+ }
255
+
256
+ // CRITICAL FIX: Pass 'isInitialWrite: true' to ensure proper cleanup of old meta data
257
+ return await commitResults(state, dStr, passName, config, deps, false, { isInitialWrite: true });
258
+ }
259
+
260
+ static async executeOncePerDay(calcInstance, metadata, dateStr, computedDeps, prevDeps, config, deps, loader) {
261
+ const mappings = await loader.loadMappings();
262
+ const { logger } = deps;
263
+ const stats = { processedShards: 0, processedItems: 0 };
264
+
265
+ const insights = metadata.rootDataDependencies?.includes('insights') ? { today: await loader.loadInsights(dateStr) } : null;
266
+ const social = metadata.rootDataDependencies?.includes('social') ? { today: await loader.loadSocial(dateStr) } : null;
267
+
268
+ let rankingsYesterday = null;
269
+ if (metadata.isHistorical) {
270
+ const prevDate = new Date(dateStr);
271
+ prevDate.setUTCDate(prevDate.getUTCDate() - 1);
272
+ const prevStr = prevDate.toISOString().slice(0, 10);
273
+ rankingsYesterday = await loader.loadRankings(prevStr);
274
+ }
275
+
276
+ const rankings = await loader.loadRankings(dateStr);
277
+ const allowMissing = metadata.canHaveMissingRoots === true;
278
+
279
+ let ratings = null;
280
+ if (metadata.rootDataDependencies?.includes('ratings')) {
281
+ try { ratings = await loader.loadRatings(dateStr); }
282
+ catch (e) { if (!allowMissing) throw e; ratings = null; }
283
+ if (!ratings && !allowMissing) throw new Error(`Missing ratings`);
284
+ }
285
+
286
+ let pageViews = null;
287
+ if (metadata.rootDataDependencies?.includes('pageViews')) {
288
+ try { pageViews = await loader.loadPageViews(dateStr); }
289
+ catch (e) { if (!allowMissing) throw e; pageViews = null; }
290
+ if (!pageViews && !allowMissing) throw new Error(`Missing pageViews`);
291
+ }
292
+
293
+ let watchlistMembership = null;
294
+ if (metadata.rootDataDependencies?.includes('watchlist')) {
295
+ try { watchlistMembership = await loader.loadWatchlistMembership(dateStr); }
296
+ catch (e) { if (!allowMissing) throw e; watchlistMembership = null; }
297
+ if (!watchlistMembership && !allowMissing) throw new Error(`Missing watchlist`);
298
+ }
299
+
300
+ let alertHistory = null;
301
+ if (metadata.rootDataDependencies?.includes('alerts')) {
302
+ try { alertHistory = await loader.loadAlertHistory(dateStr); }
303
+ catch (e) { if (!allowMissing) throw e; alertHistory = null; }
304
+ if (!alertHistory && !allowMissing) throw new Error(`Missing alerts`);
305
+ }
306
+
307
+ const seriesData = { root: {}, results: {} };
308
+
309
+ if (metadata.rootDataDependencies?.includes('price')) {
310
+ logger.log('INFO', `[Executor] Running Batched/Sharded Execution for ${metadata.name}`);
311
+ const shardRefs = await loader.getPriceShardReferences();
312
+ if (shardRefs.length === 0) { logger.log('WARN', '[Executor] No price shards found.'); return {}; }
313
+
314
+ let processedCount = 0;
315
+ for (const ref of shardRefs) {
316
+ const shardData = await loader.loadPriceShard(ref);
317
+ const partialContext = ContextFactory.buildMetaContext({
318
+ dateStr, metadata, mappings, insights, socialData: social,
319
+ prices: { history: shardData }, computedDependencies: computedDeps,
320
+ previousComputedDependencies: prevDeps, config, deps,
321
+ allRankings: rankings,
322
+ allRankingsYesterday: rankingsYesterday,
323
+ ratings: ratings || {},
324
+ pageViews: pageViews || {},
325
+ watchlistMembership: watchlistMembership || {},
326
+ alertHistory: alertHistory || {},
327
+ seriesData
328
+ });
329
+
330
+ await calcInstance.process(partialContext);
331
+ partialContext.prices = null;
332
+ processedCount++;
333
+
334
+ stats.processedShards++;
335
+ stats.processedItems += Object.keys(shardData).length;
336
+ }
337
+ logger.log('INFO', `[Executor] Finished Batched Execution for ${metadata.name} (${processedCount} shards).`);
338
+
339
+ calcInstance._executionStats = stats;
340
+ return calcInstance.getResult ? await calcInstance.getResult() : {};
341
+ } else {
342
+ const context = ContextFactory.buildMetaContext({
343
+ dateStr, metadata, mappings, insights, socialData: social,
344
+ prices: {}, computedDependencies: computedDeps,
345
+ previousComputedDependencies: prevDeps, config, deps,
346
+ allRankings: rankings,
347
+ allRankingsYesterday: rankingsYesterday,
348
+ ratings: ratings || {},
349
+ pageViews: pageViews || {},
350
+ watchlistMembership: watchlistMembership || {},
351
+ alertHistory: alertHistory || {},
352
+ seriesData
353
+ });
354
+ const res = await calcInstance.process(context);
355
+
356
+ stats.processedItems = 1;
357
+ calcInstance._executionStats = stats;
358
+
359
+ return res;
360
+ }
361
+ }
362
+ }
363
+
364
+ module.exports = { MetaExecutor };