bulltrackers-module 1.0.519 → 1.0.521

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.
@@ -127,6 +127,11 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
127
127
  // 6. Commit batch
128
128
  await batch.commit();
129
129
 
130
+ // 7. Update global rootdata collection for computation system
131
+ const { updateAlertHistoryRootData } = require('../../generic-api/user-api/helpers/rootdata/rootdata_aggregation_helpers');
132
+ const triggeredUserCids = subscriptions.map(s => s.userCid);
133
+ await updateAlertHistoryRootData(db, logger, piCid, alertType, computationMetadata, computationDate, triggeredUserCids);
134
+
130
135
  logger.log('SUCCESS', `[processAlertForPI] Created ${notificationRefs.length} notifications for PI ${piCid}, alert type ${alertType.id}`);
131
136
 
132
137
  } catch (error) {
@@ -25,7 +25,9 @@ class ContextFactory {
25
25
  verification,
26
26
  rankings, yesterdayRankings, // User-specific rank entries
27
27
  allRankings, allRankingsYesterday, // Global rank lists
28
- allVerifications
28
+ allVerifications,
29
+ // [NEW] New Root Data Types for Profile Metrics
30
+ ratings, pageViews, watchlistMembership, alertHistory
29
31
  } = options;
30
32
 
31
33
  return {
@@ -49,7 +51,12 @@ class ContextFactory {
49
51
  globalData: {
50
52
  rankings: allRankings || [],
51
53
  rankingsYesterday: allRankingsYesterday || [],
52
- verifications: allVerifications || {}
54
+ verifications: allVerifications || {},
55
+ // [NEW] New Root Data Types for Profile Metrics
56
+ ratings: ratings || {},
57
+ pageViews: pageViews || {},
58
+ watchlistMembership: watchlistMembership || {},
59
+ alertHistory: alertHistory || {}
53
60
  }
54
61
  };
55
62
  }
@@ -59,7 +66,9 @@ class ContextFactory {
59
66
  dateStr, metadata, mappings, insights, socialData, prices,
60
67
  computedDependencies, previousComputedDependencies, config, deps,
61
68
  allRankings, allRankingsYesterday, // [UPDATED] Accepted here
62
- allVerifications
69
+ allVerifications,
70
+ // [NEW] New Root Data Types for Profile Metrics
71
+ ratings, pageViews, watchlistMembership, alertHistory
63
72
  } = options;
64
73
 
65
74
  return {
@@ -75,7 +84,12 @@ class ContextFactory {
75
84
  globalData: {
76
85
  rankings: allRankings || [],
77
86
  rankingsYesterday: allRankingsYesterday || [], // [UPDATED] Injected here
78
- verifications: allVerifications || {}
87
+ verifications: allVerifications || {},
88
+ // [NEW] New Root Data Types for Profile Metrics
89
+ ratings: ratings || {},
90
+ pageViews: pageViews || {},
91
+ watchlistMembership: watchlistMembership || {},
92
+ alertHistory: alertHistory || {}
79
93
  }
80
94
  };
81
95
  }
@@ -10,65 +10,150 @@ const INDEX_COLLECTION = process.env.ROOT_DATA_AVAILABILITY_COLLECTION || 'syste
10
10
 
11
11
  function checkRootDependencies(calcManifest, rootDataStatus) {
12
12
  const missing = [];
13
- if (!calcManifest.rootDataDependencies) return { canRun: true, missing };
13
+ const available = [];
14
+
15
+ if (!calcManifest.rootDataDependencies) return { canRun: true, missing, available };
16
+
17
+ // Check if computation can run with missing rootdata
18
+ const canHaveMissingRoots = calcManifest.canHaveMissingRoots === true;
14
19
 
15
20
  // Normalize userType to lowercase for comparison (computations use uppercase)
16
21
  const userType = (calcManifest.userType || 'all').toLowerCase();
17
22
 
18
23
  for (const dep of calcManifest.rootDataDependencies) {
24
+ let isAvailable = false;
25
+
19
26
  if (dep === 'portfolio') {
20
- if (userType === 'speculator' && !rootDataStatus.speculatorPortfolio) missing.push('speculatorPortfolio');
21
- else if (userType === 'normal' && !rootDataStatus.normalPortfolio) missing.push('normalPortfolio');
22
- else if (userType === 'popular_investor') {
23
- // [FIX] Must have PI-specific portfolio data, not just any portfolio
24
- if (!rootDataStatus.piPortfolios) missing.push('piPortfolios');
25
- // Note: piDeepPortfolios is optional, so we don't require it
26
- }
27
- else if (userType === 'signed_in_user') {
28
- // [FIX] Must have signed-in user portfolio data, not just any portfolio
29
- if (!rootDataStatus.signedInUserPortfolio) missing.push('signedInUserPortfolio');
30
- }
31
- else if (userType === 'all' && !rootDataStatus.hasPortfolio) missing.push('portfolio');
32
- else {
33
- // [FIX] Safety Block: If userType doesn't match known types (e.g. typo in metadata),
34
- // enforce the global 'hasPortfolio' check instead of silently allowing it.
35
- if (!rootDataStatus.hasPortfolio) missing.push('portfolio (fallback)');
27
+ if (userType === 'speculator' && rootDataStatus.speculatorPortfolio) isAvailable = true;
28
+ else if (userType === 'normal' && rootDataStatus.normalPortfolio) isAvailable = true;
29
+ else if (userType === 'popular_investor' && rootDataStatus.piPortfolios) isAvailable = true;
30
+ else if (userType === 'signed_in_user' && rootDataStatus.signedInUserPortfolio) isAvailable = true;
31
+ else if (userType === 'all' && rootDataStatus.hasPortfolio) isAvailable = true;
32
+ else if (rootDataStatus.hasPortfolio) isAvailable = true; // Fallback
33
+
34
+ if (!isAvailable) {
35
+ if (userType === 'speculator') missing.push('speculatorPortfolio');
36
+ else if (userType === 'normal') missing.push('normalPortfolio');
37
+ else if (userType === 'popular_investor') missing.push('piPortfolios');
38
+ else if (userType === 'signed_in_user') missing.push('signedInUserPortfolio');
39
+ else missing.push('portfolio');
40
+ } else {
41
+ available.push('portfolio');
36
42
  }
37
43
  }
38
44
  else if (dep === 'history') {
39
- if (userType === 'speculator' && !rootDataStatus.speculatorHistory) missing.push('speculatorHistory');
40
- else if (userType === 'normal' && !rootDataStatus.normalHistory) missing.push('normalHistory');
41
- else if (userType === 'popular_investor' && !rootDataStatus.piHistory) missing.push('piHistory');
42
- else if (userType === 'signed_in_user' && !rootDataStatus.signedInUserHistory) missing.push('signedInUserHistory');
43
- else if (userType === 'all' && !rootDataStatus.hasHistory) missing.push('history');
44
- else {
45
- // [FIX] Safety Block for History
46
- if (!rootDataStatus.hasHistory) missing.push('history (fallback)');
45
+ if (userType === 'speculator' && rootDataStatus.speculatorHistory) isAvailable = true;
46
+ else if (userType === 'normal' && rootDataStatus.normalHistory) isAvailable = true;
47
+ else if (userType === 'popular_investor' && rootDataStatus.piHistory) isAvailable = true;
48
+ else if (userType === 'signed_in_user' && rootDataStatus.signedInUserHistory) isAvailable = true;
49
+ else if (userType === 'all' && rootDataStatus.hasHistory) isAvailable = true;
50
+ else if (rootDataStatus.hasHistory) isAvailable = true; // Fallback
51
+
52
+ if (!isAvailable) {
53
+ if (userType === 'speculator') missing.push('speculatorHistory');
54
+ else if (userType === 'normal') missing.push('normalHistory');
55
+ else if (userType === 'popular_investor') missing.push('piHistory');
56
+ else if (userType === 'signed_in_user') missing.push('signedInUserHistory');
57
+ else missing.push('history');
58
+ } else {
59
+ available.push('history');
60
+ }
61
+ }
62
+ else if (dep === 'rankings') {
63
+ if (rootDataStatus.piRankings) {
64
+ isAvailable = true;
65
+ available.push('rankings');
66
+ } else {
67
+ missing.push('piRankings');
68
+ }
69
+ }
70
+ else if (dep === 'verification') {
71
+ if (rootDataStatus.signedInUserVerification) {
72
+ isAvailable = true;
73
+ available.push('verification');
74
+ } else {
75
+ missing.push('signedInUserVerification');
76
+ }
77
+ }
78
+ else if (dep === 'insights') {
79
+ if (rootDataStatus.hasInsights) {
80
+ isAvailable = true;
81
+ available.push('insights');
82
+ } else {
83
+ missing.push('insights');
47
84
  }
48
85
  }
49
- else if (dep === 'rankings' && !rootDataStatus.piRankings) missing.push('piRankings');
50
- else if (dep === 'verification' && !rootDataStatus.signedInUserVerification) missing.push('signedInUserVerification');
51
- else if (dep === 'insights' && !rootDataStatus.hasInsights) missing.push('insights');
52
-
53
86
  // [UPDATED] Strict Social Data Checking
54
87
  else if (dep === 'social') {
55
- if (userType === 'popular_investor') {
56
- // Strict check: Must have PI-specific social data
57
- if (!rootDataStatus.hasPISocial) missing.push('piSocial');
88
+ if (userType === 'popular_investor' && rootDataStatus.hasPISocial) {
89
+ isAvailable = true;
90
+ available.push('social');
91
+ } else if (userType === 'signed_in_user' && rootDataStatus.hasSignedInSocial) {
92
+ isAvailable = true;
93
+ available.push('social');
94
+ } else if (rootDataStatus.hasSocial) {
95
+ isAvailable = true;
96
+ available.push('social');
97
+ } else {
98
+ if (userType === 'popular_investor') missing.push('piSocial');
99
+ else if (userType === 'signed_in_user') missing.push('signedInSocial');
100
+ else missing.push('social');
101
+ }
102
+ }
103
+ else if (dep === 'price') {
104
+ if (rootDataStatus.hasPrices) {
105
+ isAvailable = true;
106
+ available.push('price');
107
+ } else {
108
+ missing.push('price');
109
+ }
110
+ }
111
+ // [NEW] New Root Data Types for Profile Metrics
112
+ else if (dep === 'ratings') {
113
+ if (rootDataStatus.piRatings) {
114
+ isAvailable = true;
115
+ available.push('ratings');
116
+ } else {
117
+ missing.push('piRatings');
58
118
  }
59
- else if (userType === 'signed_in_user') {
60
- // Strict check: Must have Signed-In User specific social data
61
- if (!rootDataStatus.hasSignedInSocial) missing.push('signedInSocial');
119
+ }
120
+ else if (dep === 'pageViews') {
121
+ if (rootDataStatus.piPageViews) {
122
+ isAvailable = true;
123
+ available.push('pageViews');
124
+ } else {
125
+ missing.push('piPageViews');
62
126
  }
63
- else {
64
- // Default behavior: Check for generic asset/market social data
65
- if (!rootDataStatus.hasSocial) missing.push('social');
127
+ }
128
+ else if (dep === 'watchlist') {
129
+ if (rootDataStatus.watchlistMembership) {
130
+ isAvailable = true;
131
+ available.push('watchlist');
132
+ } else {
133
+ missing.push('watchlistMembership');
66
134
  }
67
135
  }
68
-
69
- else if (dep === 'price' && !rootDataStatus.hasPrices) missing.push('price');
136
+ else if (dep === 'alerts') {
137
+ if (rootDataStatus.piAlertHistory) {
138
+ isAvailable = true;
139
+ available.push('alerts');
140
+ } else {
141
+ missing.push('piAlertHistory');
142
+ }
143
+ }
144
+ }
145
+
146
+ // [FIX] Enforce canHaveMissingRoots logic:
147
+ // - If canHaveMissingRoots: false, ALL required rootdata must be available
148
+ // - If canHaveMissingRoots: true, AT LEAST ONE required rootdata must be available (not zero)
149
+ if (canHaveMissingRoots) {
150
+ // Must have at least one available rootdata type
151
+ const canRun = available.length > 0;
152
+ return { canRun, missing, available };
70
153
  }
71
- return { canRun: missing.length === 0, missing };
154
+
155
+ // Strict mode: all required rootdata must be available
156
+ return { canRun: missing.length === 0, missing, available };
72
157
  }
73
158
 
74
159
  function getViableCalculations(candidates, fullManifest, rootDataStatus, dailyStatus) {
@@ -138,7 +223,13 @@ async function checkRootDataAvailability(dateStr, config, dependencies, earliest
138
223
 
139
224
  // [UPDATED] Granular Social Flags
140
225
  hasPISocial: !!details.hasPISocial || !!data.hasPISocial,
141
- hasSignedInSocial: !!details.hasSignedInSocial || !!data.hasSignedInSocial
226
+ hasSignedInSocial: !!details.hasSignedInSocial || !!data.hasSignedInSocial,
227
+
228
+ // [NEW] New Root Data Types for Profile Metrics
229
+ piRatings: !!details.piRatings,
230
+ piPageViews: !!details.piPageViews,
231
+ watchlistMembership: !!details.watchlistMembership,
232
+ piAlertHistory: !!details.piAlertHistory
142
233
  },
143
234
  portfolioRefs: null,
144
235
  historyRefs: null,
@@ -154,7 +245,9 @@ async function checkRootDataAvailability(dateStr, config, dependencies, earliest
154
245
  speculatorPortfolio: false, normalPortfolio: false, speculatorHistory: false, normalHistory: false,
155
246
  piRankings: false, piPortfolios: false, piDeepPortfolios: false, piHistory: false,
156
247
  signedInUserPortfolio: false, signedInUserHistory: false, signedInUserVerification: false,
157
- hasPISocial: false, hasSignedInSocial: false
248
+ hasPISocial: false, hasSignedInSocial: false,
249
+ // New Root Data Types
250
+ piRatings: false, piPageViews: false, watchlistMembership: false, piAlertHistory: false
158
251
  }
159
252
  };
160
253
  }
@@ -10,7 +10,11 @@ const {
10
10
  getRelevantShardRefs,
11
11
  getPriceShardRefs,
12
12
  loadVerificationProfiles, // [NEW]
13
- loadPopularInvestorRankings // [NEW]
13
+ loadPopularInvestorRankings, // [NEW]
14
+ loadPIRatings, // [NEW]
15
+ loadPIPageViews, // [NEW]
16
+ loadWatchlistMembership: loadWatchlistMembershipData, // [NEW] Renamed to avoid conflict
17
+ loadPIAlertHistory // [NEW]
14
18
  } = require('../utils/data_loader');
15
19
  const zlib = require('zlib');
16
20
 
@@ -23,7 +27,11 @@ class CachedDataLoader {
23
27
  insights: new Map(),
24
28
  social: new Map(),
25
29
  verifications: null, // [NEW]
26
- rankings: new Map() // [NEW]
30
+ rankings: new Map(), // [NEW]
31
+ ratings: new Map(), // [NEW]
32
+ pageViews: new Map(), // [NEW]
33
+ watchlistMembership: new Map(), // [NEW]
34
+ alertHistory: new Map() // [NEW]
27
35
  };
28
36
  }
29
37
 
@@ -46,18 +54,20 @@ class CachedDataLoader {
46
54
  return this.cache.mappings;
47
55
  }
48
56
 
57
+ // [FIX] Cache promises to prevent race conditions when multiple users request same data
49
58
  async loadInsights(dateStr) {
50
59
  if (this.cache.insights.has(dateStr)) return this.cache.insights.get(dateStr);
51
- const insights = await loadDailyInsights(this.config, this.deps, dateStr);
52
- this.cache.insights.set(dateStr, insights);
53
- return insights;
60
+ const promise = loadDailyInsights(this.config, this.deps, dateStr);
61
+ this.cache.insights.set(dateStr, promise);
62
+ return promise;
54
63
  }
55
64
 
65
+ // [FIX] Cache promises to prevent race conditions when multiple users request same data
56
66
  async loadSocial(dateStr) {
57
67
  if (this.cache.social.has(dateStr)) return this.cache.social.get(dateStr);
58
- const social = await loadDailySocialPostInsights(this.config, this.deps, dateStr);
59
- this.cache.social.set(dateStr, social);
60
- return social;
68
+ const promise = loadDailySocialPostInsights(this.config, this.deps, dateStr);
69
+ this.cache.social.set(dateStr, promise);
70
+ return promise;
61
71
  }
62
72
 
63
73
  // [NEW]
@@ -69,11 +79,48 @@ class CachedDataLoader {
69
79
  }
70
80
 
71
81
  // [NEW]
82
+ // [FIX] Cache promises to prevent race conditions when multiple users request same data
72
83
  async loadRankings(dateStr) {
73
84
  if (this.cache.rankings.has(dateStr)) return this.cache.rankings.get(dateStr);
74
- const rankings = await loadPopularInvestorRankings(this.config, this.deps, dateStr);
75
- this.cache.rankings.set(dateStr, rankings);
76
- return rankings;
85
+ const promise = loadPopularInvestorRankings(this.config, this.deps, dateStr);
86
+ this.cache.rankings.set(dateStr, promise);
87
+ return promise;
88
+ }
89
+
90
+ // [NEW] Load PI Ratings Data
91
+ // [FIX] Cache promises to prevent race conditions when multiple users request same data
92
+ async loadRatings(dateStr) {
93
+ if (this.cache.ratings.has(dateStr)) return this.cache.ratings.get(dateStr);
94
+ const promise = loadPIRatings(this.config, this.deps, dateStr);
95
+ this.cache.ratings.set(dateStr, promise);
96
+ return promise;
97
+ }
98
+
99
+ // [NEW] Load PI Page Views Data
100
+ // [FIX] Cache promises to prevent race conditions when multiple users request same data
101
+ async loadPageViews(dateStr) {
102
+ if (this.cache.pageViews.has(dateStr)) return this.cache.pageViews.get(dateStr);
103
+ const promise = loadPIPageViews(this.config, this.deps, dateStr);
104
+ this.cache.pageViews.set(dateStr, promise);
105
+ return promise;
106
+ }
107
+
108
+ // [NEW] Load Watchlist Membership Data
109
+ // [FIX] Cache promises to prevent race conditions when multiple users request same data
110
+ async loadWatchlistMembership(dateStr) {
111
+ if (this.cache.watchlistMembership.has(dateStr)) return this.cache.watchlistMembership.get(dateStr);
112
+ const promise = loadWatchlistMembershipData(this.config, this.deps, dateStr);
113
+ this.cache.watchlistMembership.set(dateStr, promise);
114
+ return promise;
115
+ }
116
+
117
+ // [NEW] Load PI Alert History Data
118
+ // [FIX] Cache promises to prevent race conditions when multiple users request same data
119
+ async loadAlertHistory(dateStr) {
120
+ if (this.cache.alertHistory.has(dateStr)) return this.cache.alertHistory.get(dateStr);
121
+ const promise = loadPIAlertHistory(this.config, this.deps, dateStr);
122
+ this.cache.alertHistory.set(dateStr, promise);
123
+ return promise;
77
124
  }
78
125
 
79
126
  async getPriceShardReferences() {
@@ -34,6 +34,96 @@ class MetaExecutor {
34
34
  loader.loadRankings(dStr),
35
35
  loader.loadVerifications()
36
36
  ]);
37
+
38
+ // [NEW] Load New Root Data Types for Profile Metrics (if any calc needs them)
39
+ const needsNewRootData = calcs.some(c => {
40
+ const deps = c.rootDataDependencies || [];
41
+ return deps.includes('ratings') || deps.includes('pageViews') ||
42
+ deps.includes('watchlist') || deps.includes('alerts');
43
+ });
44
+
45
+ let ratings = null, pageViews = null, watchlistMembership = null, alertHistory = null;
46
+ if (needsNewRootData) {
47
+ const loadPromises = [];
48
+ if (calcs.some(c => c.rootDataDependencies?.includes('ratings'))) {
49
+ loadPromises.push(loader.loadRatings(dStr).then(r => { ratings = r; }).catch(e => {
50
+ // Only catch if ALL calcs allow missing roots
51
+ const allAllowMissing = calcs.every(c => {
52
+ const needsRatings = c.rootDataDependencies?.includes('ratings');
53
+ return !needsRatings || c.canHaveMissingRoots === true;
54
+ });
55
+ if (allAllowMissing) {
56
+ ratings = null;
57
+ } else {
58
+ throw e; // Re-throw if any calc requires this data
59
+ }
60
+ }));
61
+ }
62
+ if (calcs.some(c => c.rootDataDependencies?.includes('pageViews'))) {
63
+ loadPromises.push(loader.loadPageViews(dStr).then(pv => { pageViews = pv; }).catch(e => {
64
+ const allAllowMissing = calcs.every(c => {
65
+ const needsPageViews = c.rootDataDependencies?.includes('pageViews');
66
+ return !needsPageViews || c.canHaveMissingRoots === true;
67
+ });
68
+ if (allAllowMissing) {
69
+ pageViews = null;
70
+ } else {
71
+ throw e;
72
+ }
73
+ }));
74
+ }
75
+ if (calcs.some(c => c.rootDataDependencies?.includes('watchlist'))) {
76
+ loadPromises.push(loader.loadWatchlistMembership(dStr).then(w => { watchlistMembership = w; }).catch(e => {
77
+ const allAllowMissing = calcs.every(c => {
78
+ const needsWatchlist = c.rootDataDependencies?.includes('watchlist');
79
+ return !needsWatchlist || c.canHaveMissingRoots === true;
80
+ });
81
+ if (allAllowMissing) {
82
+ watchlistMembership = null;
83
+ } else {
84
+ throw e;
85
+ }
86
+ }));
87
+ }
88
+ if (calcs.some(c => c.rootDataDependencies?.includes('alerts'))) {
89
+ loadPromises.push(loader.loadAlertHistory(dStr).then(a => { alertHistory = a; }).catch(e => {
90
+ const allAllowMissing = calcs.every(c => {
91
+ const needsAlerts = c.rootDataDependencies?.includes('alerts');
92
+ return !needsAlerts || c.canHaveMissingRoots === true;
93
+ });
94
+ if (allAllowMissing) {
95
+ alertHistory = null;
96
+ } else {
97
+ throw e;
98
+ }
99
+ }));
100
+ }
101
+ await Promise.all(loadPromises);
102
+
103
+ // [FIX] Enforce canHaveMissingRoots - validate after loading
104
+ for (const c of calcs) {
105
+ const deps = c.rootDataDependencies || [];
106
+ const canSkip = c.canHaveMissingRoots === true;
107
+
108
+ // Helper to check if a specific root is missing
109
+ const isMissing = (key, val) => deps.includes(key) && (val === null || val === undefined);
110
+
111
+ if (!canSkip) {
112
+ if (isMissing('ratings', ratings)) {
113
+ throw new Error(`[MetaExecutor] Missing required root 'ratings' for ${c.name}`);
114
+ }
115
+ if (isMissing('pageViews', pageViews)) {
116
+ throw new Error(`[MetaExecutor] Missing required root 'pageViews' for ${c.name}`);
117
+ }
118
+ if (isMissing('watchlist', watchlistMembership)) {
119
+ throw new Error(`[MetaExecutor] Missing required root 'watchlist' for ${c.name}`);
120
+ }
121
+ if (isMissing('alerts', alertHistory)) {
122
+ throw new Error(`[MetaExecutor] Missing required root 'alerts' for ${c.name}`);
123
+ }
124
+ }
125
+ }
126
+ }
37
127
 
38
128
  const state = {};
39
129
  for (const c of calcs) {
@@ -51,7 +141,12 @@ class MetaExecutor {
51
141
  config, deps,
52
142
  allRankings: rankings,
53
143
  allRankingsYesterday: rankingsYesterday, // [FIX] Injected
54
- allVerifications: verifications
144
+ allVerifications: verifications,
145
+ // [NEW] Pass New Root Data Types for Profile Metrics
146
+ ratings: ratings || {},
147
+ pageViews: pageViews || {},
148
+ watchlistMembership: watchlistMembership || {},
149
+ alertHistory: alertHistory || {}
55
150
  });
56
151
 
57
152
  try {
@@ -87,6 +182,62 @@ class MetaExecutor {
87
182
 
88
183
  // Load current rankings (often needed for ContextFactory.buildMetaContext)
89
184
  const rankings = await loader.loadRankings(dateStr);
185
+
186
+ // [NEW] Load New Root Data Types for Profile Metrics
187
+ // [FIX] Enforce canHaveMissingRoots
188
+ const allowMissing = metadata.canHaveMissingRoots === true;
189
+
190
+ let ratings = null;
191
+ if (metadata.rootDataDependencies?.includes('ratings')) {
192
+ try {
193
+ ratings = await loader.loadRatings(dateStr);
194
+ } catch (e) {
195
+ if (!allowMissing) throw new Error(`[MetaExecutor.executeOncePerDay] Required root 'ratings' failed to load for ${metadata.name}: ${e.message}`);
196
+ ratings = null;
197
+ }
198
+ if (!ratings && !allowMissing) {
199
+ throw new Error(`[MetaExecutor.executeOncePerDay] Required root 'ratings' is missing for ${metadata.name}`);
200
+ }
201
+ }
202
+
203
+ let pageViews = null;
204
+ if (metadata.rootDataDependencies?.includes('pageViews')) {
205
+ try {
206
+ pageViews = await loader.loadPageViews(dateStr);
207
+ } catch (e) {
208
+ if (!allowMissing) throw new Error(`[MetaExecutor.executeOncePerDay] Required root 'pageViews' failed to load for ${metadata.name}: ${e.message}`);
209
+ pageViews = null;
210
+ }
211
+ if (!pageViews && !allowMissing) {
212
+ throw new Error(`[MetaExecutor.executeOncePerDay] Required root 'pageViews' is missing for ${metadata.name}`);
213
+ }
214
+ }
215
+
216
+ let watchlistMembership = null;
217
+ if (metadata.rootDataDependencies?.includes('watchlist')) {
218
+ try {
219
+ watchlistMembership = await loader.loadWatchlistMembership(dateStr);
220
+ } catch (e) {
221
+ if (!allowMissing) throw new Error(`[MetaExecutor.executeOncePerDay] Required root 'watchlist' failed to load for ${metadata.name}: ${e.message}`);
222
+ watchlistMembership = null;
223
+ }
224
+ if (!watchlistMembership && !allowMissing) {
225
+ throw new Error(`[MetaExecutor.executeOncePerDay] Required root 'watchlist' is missing for ${metadata.name}`);
226
+ }
227
+ }
228
+
229
+ let alertHistory = null;
230
+ if (metadata.rootDataDependencies?.includes('alerts')) {
231
+ try {
232
+ alertHistory = await loader.loadAlertHistory(dateStr);
233
+ } catch (e) {
234
+ if (!allowMissing) throw new Error(`[MetaExecutor.executeOncePerDay] Required root 'alerts' failed to load for ${metadata.name}: ${e.message}`);
235
+ alertHistory = null;
236
+ }
237
+ if (!alertHistory && !allowMissing) {
238
+ throw new Error(`[MetaExecutor.executeOncePerDay] Required root 'alerts' is missing for ${metadata.name}`);
239
+ }
240
+ }
90
241
 
91
242
  if (metadata.rootDataDependencies?.includes('price')) {
92
243
  logger.log('INFO', `[Executor] Running Batched/Sharded Execution for ${metadata.name}`);
@@ -101,7 +252,12 @@ class MetaExecutor {
101
252
  prices: { history: shardData }, computedDependencies: computedDeps,
102
253
  previousComputedDependencies: prevDeps, config, deps,
103
254
  allRankings: rankings,
104
- allRankingsYesterday: rankingsYesterday
255
+ allRankingsYesterday: rankingsYesterday,
256
+ // [NEW] Pass New Root Data Types for Profile Metrics
257
+ ratings: ratings || {},
258
+ pageViews: pageViews || {},
259
+ watchlistMembership: watchlistMembership || {},
260
+ alertHistory: alertHistory || {}
105
261
  });
106
262
 
107
263
  await calcInstance.process(partialContext);
@@ -121,7 +277,12 @@ class MetaExecutor {
121
277
  prices: {}, computedDependencies: computedDeps,
122
278
  previousComputedDependencies: prevDeps, config, deps,
123
279
  allRankings: rankings,
124
- allRankingsYesterday: rankingsYesterday
280
+ allRankingsYesterday: rankingsYesterday,
281
+ // [NEW] Pass New Root Data Types for Profile Metrics
282
+ ratings: ratings || {},
283
+ pageViews: pageViews || {},
284
+ watchlistMembership: watchlistMembership || {},
285
+ alertHistory: alertHistory || {}
125
286
  });
126
287
  const res = await calcInstance.process(context);
127
288