bulltrackers-module 1.0.102 → 1.0.103

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.
@@ -1,38 +1,120 @@
1
1
  {
2
- "dependencies": {
3
- "smart-cohort-flow": ["user-investment-profile"],
4
- "dumb-cohort-flow": ["user-investment-profile"],
5
- "crowd_sharpe_ratio_proxy": ["pnl-distribution-per-stock"],
6
- "social_flow_correlation": [
7
- "social_sentiment_aggregation",
8
- "asset_crowd_flow"
9
- ],
10
- "cash-flow-deployment": [
11
- "crowd-cash-flow-proxy",
12
- "asset_crowd_flow"
13
- ],
14
- "cash-flow-liquidation": [
15
- "crowd-cash-flow-proxy",
16
- "asset-crowd-flow"
17
- ],
18
- "capital_deployment_strategy": [
19
- "crowd-cash-flow-proxy",
20
- "new-allocation-percentage",
21
- "reallocation-increase-percentage"
22
- ],
23
- "profit_cohort_divergence": [
24
- "in-profit-asset-crowd-flow",
25
- "in-loss-asset-crowd-flow"
26
- ],
27
- "smart-dumb-divergence-index": [
28
- "smart-cohort-flow",
29
- "dumb-cohort-flow"
30
- ],
31
- "capital_vintage_performance": ["cash-flow-deployment"],
32
- "capital_liquidation_performance": ["cash-flow-liquidation"],
33
- "strategy-performance": [
34
- "smart-dumb-divergence-index",
35
- "profit_cohort_divergence"
36
- ]
37
- }
2
+ "dependencies": {
3
+ "smart-cohort-flow": ["user-investment-profile"],
4
+ "dumb-cohort-flow": ["user-investment-profile"],
5
+ "crowd_sharpe_ratio_proxy": ["pnl-distribution-per-stock"],
6
+ "social_flow_correlation": [
7
+ "social_sentiment_aggregation",
8
+ "asset_crowd_flow"
9
+ ],
10
+ "cash-flow-deployment": [
11
+ "crowd-cash-flow-proxy",
12
+ "asset_crowd_flow"
13
+ ],
14
+ "cash-flow-liquidation": [
15
+ "crowd-cash-flow-proxy",
16
+ "asset_crowd_flow"
17
+ ],
18
+ "capital_deployment_strategy": [
19
+ "crowd-cash-flow-proxy",
20
+ "new-allocation-percentage",
21
+ "reallocation-increase-percentage"
22
+ ],
23
+ "profit_cohort_divergence": [
24
+ "in-profit-asset-crowd-flow",
25
+ "in-loss-asset-crowd-flow"
26
+ ],
27
+ "smart-dumb-divergence-index": [
28
+ "smart-cohort-flow",
29
+ "dumb-cohort-flow"
30
+ ],
31
+ "capital_vintage_performance": ["cash-flow-deployment"],
32
+ "capital_liquidation_performance": ["cash-flow-liquidation"],
33
+ "strategy-performance": [
34
+ "smart-dumb-divergence-index",
35
+ "profit_cohort_divergence"
36
+ ]
37
+ },
38
+ "requirements": {
39
+ "activity_by_pnl_status": ["today_portfolio", "yesterday_portfolio"],
40
+ "daily_asset_activity": ["today_portfolio", "yesterday_portfolio"],
41
+ "daily_user_activity_tracker": ["today_portfolio", "yesterday_portfolio"],
42
+ "speculator_adjustment_activity": ["today_portfolio", "yesterday_portfolio"],
43
+ "asset_position_size": ["today_portfolio"],
44
+ "strategy-performance": ["meta"],
45
+ "asset_crowd_flow": ["today_portfolio", "yesterday_portfolio"],
46
+ "drawdown_response": ["today_portfolio", "yesterday_portfolio"],
47
+ "dumb-cohort-flow": ["today_portfolio", "yesterday_portfolio"],
48
+ "gain_response": ["today_portfolio", "yesterday_portfolio"],
49
+ "in_loss_asset_crowd_flow": ["today_portfolio", "yesterday_portfolio"],
50
+ "in_profit_asset_crowd_flow": ["today_portfolio", "yesterday_portfolio"],
51
+ "paper_vs_diamond_hands": ["today_portfolio", "yesterday_portfolio"],
52
+ "position_count_pnl": ["today_portfolio"],
53
+ "smart-cohort-flow": ["today_portfolio", "yesterday_portfolio"],
54
+ "smart_money_flow": ["today_portfolio", "yesterday_portfolio"],
55
+ "user-investment-profile": ["today_portfolio", "yesterday_portfolio"],
56
+ "crowd-cash-flow-proxy": ["today_portfolio", "yesterday_portfolio"],
57
+ "deposit_withdrawal_percentage": ["today_portfolio", "yesterday_portfolio"],
58
+ "new_allocation_percentage": ["today_portfolio", "yesterday_portfolio"],
59
+ "reallocation_increase_percentage": ["today_portfolio", "yesterday_portfolio"],
60
+ "daily_bought_vs_sold_count": ["today_insights", "yesterday_insights"],
61
+ "daily_buy_sell_sentiment_count": ["today_insights"],
62
+ "daily_ownership_delta": ["today_insights", "yesterday_insights"],
63
+ "daily_total_positions_held": ["today_insights"],
64
+ "capital_deployment_strategy": ["meta"],
65
+ "cash-flow-deployment": ["meta"],
66
+ "cash-flow-liquidation": ["meta"],
67
+ "crowd_sharpe_ratio_proxy": ["meta"],
68
+ "profit_cohort_divergence": ["meta"],
69
+ "smart-dumb-divergence-index": ["meta"],
70
+ "social_flow_correlation": ["meta"],
71
+ "capital_liquidation_performance": ["meta"],
72
+ "capital_vintage_performance": ["meta"],
73
+ "profitability_migration": ["today_portfolio", "yesterday_portfolio"],
74
+ "user_profitability_tracker": ["today_portfolio"],
75
+ "asset_pnl_status": ["today_portfolio"],
76
+ "average_daily_pnl_all_users": ["today_portfolio"],
77
+ "average_daily_pnl_per_sector": ["today_portfolio"],
78
+ "average_daily_pnl_per_stock": ["today_portfolio"],
79
+ "average_daily_position_pnl": ["today_portfolio"],
80
+ "pnl_distribution_per_stock": ["today_portfolio"],
81
+ "profitability_ratio_per_stock": ["today_portfolio"],
82
+ "profitability_skew_per_stock": ["today_portfolio"],
83
+ "profitable_and_unprofitable_status": ["today_portfolio"],
84
+ "users_processed": ["today_portfolio"],
85
+ "diversification_pnl": ["today_portfolio", "yesterday_portfolio"],
86
+ "sector_rotation": ["today_portfolio", "yesterday_portfolio"],
87
+ "total_long_per_sector": ["today_portfolio"],
88
+ "total_short_per_sector": ["today_portfolio"],
89
+ "crowd_conviction_score": ["today_portfolio", "yesterday_portfolio"],
90
+ "long_position_per_stock": ["today_portfolio"],
91
+ "sentiment_per_stock": ["today_portfolio"],
92
+ "short_position_per_stock": ["today_portfolio"],
93
+ "total_long_figures": ["today_portfolio"],
94
+ "total_short_figures": ["today_portfolio"],
95
+ "social-asset-posts-trend": ["today_social"],
96
+ "social-top-mentioned-words": ["today_social", "yesterday_social"],
97
+ "social-topic-interest-evolution": ["today_social"],
98
+ "social-word-mentions-trend": ["today_social"],
99
+ "social_activity_aggregation": ["today_social"],
100
+ "social_event_correlation": ["today_social", "yesterday_social"],
101
+ "social_sentiment_aggregation": ["today_social"],
102
+ "risk_appetite_change": ["today_portfolio", "yesterday_portfolio"],
103
+ "tsl_effectiveness": ["today_portfolio"],
104
+ "distance_to_stop_loss_per_leverage": ["today_portfolio"],
105
+ "distance_to_tp_per_leverage": ["today_portfolio"],
106
+ "entry_distance_to_sl_per_leverage": ["today_portfolio"],
107
+ "entry_distance_to_tp_per_leverage": ["today_portfolio"],
108
+ "holding_duration_per_asset": ["today_portfolio"],
109
+ "leverage_per_asset": ["today_portfolio"],
110
+ "leverage_per_sector": ["today_portfolio"],
111
+ "risk_reward_ratio_per_asset": ["today_portfolio"],
112
+ "speculator_asset_sentiment": ["today_portfolio"],
113
+ "speculator_danger_zone": ["today_portfolio"],
114
+ "stop_loss_distance_by_sector_short_long_breakdown": ["today_portfolio"],
115
+ "stop_loss_distance_by_ticker_short_long_breakdown": ["today_portfolio"],
116
+ "stop_loss_per_asset": ["today_portfolio"],
117
+ "take_profit_per_asset": ["today_portfolio"],
118
+ "tsl_per_asset": ["today_portfolio"]
119
+ }
38
120
  }
@@ -44,56 +44,68 @@ function findCalcClass(calculations, category, name) {
44
44
  return null;
45
45
  }
46
46
 
47
+ // --- NEW: Internal helper for previous date string ---
48
+ function getPrevStr(dateStr) {
49
+ const prev = new Date(dateStr + 'T00:00:00Z');
50
+ prev.setUTCDate(prev.getUTCDate() - 1);
51
+ return prev.toISOString().slice(0, 10);
52
+ }
53
+
47
54
  /**
48
55
  * Streams data and runs a *single* Batch 1-style computation.
56
+ * --- THIS IS THE FULLY UPDATED FUNCTION ---
49
57
  */
50
58
  async function runStreamingComputation(dateStr, node, config, dependencies, calculations) {
51
59
  const { db, logger } = dependencies;
52
- const { category, name: calcName, calcClass: CalculationClass } = node;
60
+
61
+ // --- MODIFIED: Destructure 'requires' from the node ---
62
+ const { category, name: calcName, calcClass: CalculationClass, requires } = node;
53
63
  const passName = `[Worker ${dateStr}/${calcName}]`;
54
64
 
55
- logger.log('INFO', `${passName} Starting streaming computation...`);
56
-
57
- // --- Data Loading ---
58
- const todayInsightsData = await loadDailyInsights(config, dependencies, dateStr);
59
- const todaySocialPostInsightsData = await loadDailySocialPostInsights(config, dependencies, dateStr);
60
- const todayRefs = await getPortfolioPartRefs(config, dependencies, dateStr);
61
-
62
- if (todayRefs.length === 0 && !todayInsightsData && !todaySocialPostInsightsData) {
63
- logger.log('WARN', `${passName} No portfolio, instrument insights, OR social post data found for ${dateStr}. Skipping.`);
64
- // Return null to indicate "no result, but not an error"
65
- return null;
66
- }
65
+ logger.log('INFO', `${passName} Starting streaming computation... (Requires: ${requires.join(', ') || 'none'})`);
67
66
 
68
- const { HISTORICAL_CALC_NAMES } = categorizeCalculations(calculations);
69
- let yesterdayPortfolios = {};
67
+ // --- Dynamic Data Loading ---
68
+ let todayInsightsData = null;
70
69
  let yesterdayInsightsData = null;
70
+ let todaySocialPostInsightsData = null;
71
71
  let yesterdaySocialPostInsightsData = null;
72
+ let todayRefs = [];
73
+ let yesterdayPortfolios = {};
74
+ const prevStr = getPrevStr(dateStr); // Get yesterday's date string
72
75
 
73
- const requiresYesterdayPortfolio = HISTORICAL_CALC_NAMES.has(calcName);
74
- const requiresYesterdayInsights = category === 'insights';
75
- const requiresYesterdaySocialPosts = category === 'socialPosts';
76
-
77
- const prev = new Date(dateStr + 'T00:00:00Z');
78
- prev.setUTCDate(prev.getUTCDate() - 1);
79
- const prevStr = prev.toISOString().slice(0, 10);
80
-
81
- if (requiresYesterdayPortfolio) {
76
+ if (requires.includes('today_insights')) {
77
+ todayInsightsData = await loadDailyInsights(config, dependencies, dateStr);
78
+ }
79
+ if (requires.includes('yesterday_insights')) {
80
+ yesterdayInsightsData = await loadDailyInsights(config, dependencies, prevStr);
81
+ }
82
+ if (requires.includes('today_social')) {
83
+ todaySocialPostInsightsData = await loadDailySocialPostInsights(config, dependencies, dateStr);
84
+ }
85
+ if (requires.includes('yesterday_social')) {
86
+ yesterdaySocialPostInsightsData = await loadDailySocialPostInsights(config, dependencies, prevStr);
87
+ }
88
+ if (requires.includes('today_portfolio')) {
89
+ todayRefs = await getPortfolioPartRefs(config, dependencies, dateStr);
90
+ }
91
+ if (requires.includes('yesterday_portfolio')) {
82
92
  const yesterdayRefs = await getPortfolioPartRefs(config, dependencies, prevStr);
83
93
  if (yesterdayRefs.length > 0) {
84
94
  yesterdayPortfolios = await loadFullDayMap(config, dependencies, yesterdayRefs);
85
- } else {
86
- logger.log('WARN', `${passName} Yesterday's (${prevStr}) portfolio data not found.`);
95
+ } else if (requires.includes('today_portfolio')) {
96
+ // Only warn if today_portfolio was also expected, otherwise this might be normal
97
+ logger.log('WARN', `${passName} Requires 'yesterday_portfolio' but no data was found for ${prevStr}.`);
87
98
  }
88
99
  }
89
- if (requiresYesterdayInsights) {
90
- yesterdayInsightsData = await loadDailyInsights(config, dependencies, prevStr);
91
- }
92
- if (requiresYesterdaySocialPosts) {
93
- yesterdaySocialPostInsightsData = await loadDailySocialPostInsights(config, dependencies, prevStr);
100
+ // --- End Dynamic Data Loading ---
101
+
102
+ // Check if any primary data source was loaded. If not, we can't run.
103
+ if (todayRefs.length === 0 && !todayInsightsData && !todaySocialPostInsightsData) {
104
+ logger.log('WARN', `${passName} No primary data sources (portfolio, insights, social) found for ${dateStr}. Skipping.`);
105
+ return null;
94
106
  }
95
- // --- End Data Loading ---
96
107
 
108
+ // --- Context Object (no change) ---
97
109
  const { instrumentToTicker, instrumentToSector } = await dependencies.calculationUtils.loadInstrumentMappings();
98
110
  const context = {
99
111
  instrumentMappings: instrumentToTicker,
@@ -106,55 +118,52 @@ async function runStreamingComputation(dateStr, node, config, dependencies, calc
106
118
 
107
119
  const calc = new CalculationClass();
108
120
  const batchSize = config.partRefBatchSize || 10;
109
- let isFirstUser = true;
110
-
111
- // --- Stream and Process ---
112
- for (let i = 0; i < todayRefs.length; i += batchSize) {
113
- const batchRefs = todayRefs.slice(i, i + batchSize);
114
- const todayPortfoliosChunk = await loadDataByRefs(config, dependencies, batchRefs);
115
-
116
- for (const uid in todayPortfoliosChunk) {
117
- const p = todayPortfoliosChunk[uid];
118
- if (!p) continue;
119
- const userType = p.PublicPositions ? 'speculator' : 'normal';
120
-
121
- let processArgs;
122
- const allContextArgs = [context, todayInsightsData, yesterdayInsightsData, todaySocialPostInsightsData, yesterdaySocialPostInsightsData];
123
-
124
- if (category === 'insights' || category === 'socialPosts') {
125
- if (isFirstUser) {
126
- processArgs = [null, null, null, ...allContextArgs];
127
- } else {
128
- continue; // Only run once
129
- }
130
- } else if (requiresYesterdayPortfolio) {
131
- const pYesterday = yesterdayPortfolios[uid];
132
- if (!pYesterday) continue;
133
- processArgs = [p, pYesterday, uid, ...allContextArgs];
134
- } else {
121
+
122
+ // --- MODIFIED: Stream and Process ---
123
+ let processedOnce = false; // Flag for insights/social calcs
124
+ const allContextArgs = [context, todayInsightsData, yesterdayInsightsData, todaySocialPostInsightsData, yesterdaySocialPostInsightsData];
125
+
126
+ // Only stream portfolios if they are required and were found
127
+ if (requires.includes('today_portfolio') && todayRefs.length > 0) {
128
+ for (let i = 0; i < todayRefs.length; i += batchSize) {
129
+ const batchRefs = todayRefs.slice(i, i + batchSize);
130
+ const todayPortfoliosChunk = await loadDataByRefs(config, dependencies, batchRefs);
131
+
132
+ for (const uid in todayPortfoliosChunk) {
133
+ const p = todayPortfoliosChunk[uid];
134
+ if (!p) continue;
135
+
136
+ // User-type filtering
137
+ const userType = p.PublicPositions ? 'speculator' : 'normal';
135
138
  if ((userType === 'normal' && category === 'speculators') || (userType === 'speculator' && !['speculators', 'sanity'].includes(category))) {
136
139
  continue;
137
140
  }
138
- processArgs = [p, null, uid, ...allContextArgs];
141
+
142
+ let processArgs;
143
+ if (requires.includes('yesterday_portfolio')) {
144
+ const pYesterday = yesterdayPortfolios[uid];
145
+ if (!pYesterday) continue; // Skip if user is missing yesterday's data
146
+ processArgs = [p, pYesterday, uid, ...allContextArgs];
147
+ } else {
148
+ processArgs = [p, null, uid, ...allContextArgs];
149
+ }
150
+
151
+ await Promise.resolve(calc.process(...processArgs));
152
+ processedOnce = true;
139
153
  }
140
- await Promise.resolve(calc.process(...processArgs));
141
- isFirstUser = false;
142
154
  }
143
155
  }
144
156
 
145
- if (todayRefs.length === 0 && isFirstUser && (category === 'insights' || category === 'socialPosts')) {
146
- logger.log('INFO', `${passName} No user portfolios. Running ${category} calc once.`);
147
- const allContextArgs = [context, todayInsightsData, yesterdayInsightsData, todaySocialPostInsightsData, yesterdaySocialPostInsightsData];
157
+ // If this is an insights/social calc and no portfolios ran (or were needed), run it once.
158
+ if (!processedOnce && (requires.includes('today_insights') || requires.includes('today_social'))) {
159
+ logger.log('INFO', `${passName} Running once for insights/social data.`);
148
160
  await Promise.resolve(calc.process(null, null, null, ...allContextArgs));
149
161
  }
150
162
  // --- End Stream ---
151
163
 
152
- // --- Get Result and Commit ---
164
+ // --- Get Result and Commit (no change) ---
153
165
  const result = await Promise.resolve(calc.getResult());
154
166
 
155
- // If a calc returns null (e.g., dependency missing), it's a "soft fail".
156
- // We throw an error to be caught by the main handler, which will log it
157
- // and NOT mark the node as "success", allowing for a re-trigger.
158
167
  if (result === null) {
159
168
  throw new Error(`Calculation ${calcName} returned null. Dependency likely missing.`);
160
169
  }
@@ -173,7 +182,6 @@ async function runStreamingComputation(dateStr, node, config, dependencies, calc
173
182
  for (const resultKey in shardedCollections) {
174
183
  if (result[resultKey]) {
175
184
  isSharded = true;
176
- // ... (sharding logic from old worker_helpers.js, no changes) ...
177
185
  const shardCollectionName = shardedCollections[resultKey];
178
186
  const shardedData = result[resultKey];
179
187
  for (const shardId in shardedData) {
@@ -212,6 +220,7 @@ async function runStreamingComputation(dateStr, node, config, dependencies, calc
212
220
 
213
221
  /**
214
222
  * Runs a *single* meta/backtest computation.
223
+ * (This function remains unchanged as 'meta' calcs handle their own data loading)
215
224
  */
216
225
  async function runMetaComputation(dateStr, node, config, dependencies) {
217
226
  const { db, logger } = dependencies;
@@ -253,13 +262,7 @@ async function runMetaComputation(dateStr, node, config, dependencies) {
253
262
 
254
263
  /**
255
264
  * Main pipe: pipe.computationSystem.handleNodeTask
256
- * This is the Pub/Sub triggered handler for the worker.
257
- *
258
- * @param {object} message - The Pub/Sub message { date, nodeName }.
259
- * @param {object} context - The message context.
260
- * @param {object} config - The task-engine-specific config.
261
- * @param {object} dependencies - Contains all clients: db, pubsub, logger, etc.
262
- * @param {object} calculations - The injected calculations object from 'aiden-shared-calculations-unified'.
265
+ * (This function remains unchanged)
263
266
  */
264
267
  async function handleNodeTask(message, context, config, dependencies, calculations) {
265
268
  const { logger, pubsubUtils } = dependencies;
@@ -291,7 +294,8 @@ async function handleNodeTask(message, context, config, dependencies, calculatio
291
294
 
292
295
  try {
293
296
  // 1. Route to the correct execution helper
294
- if (node.category === 'meta' || node.category === 'backtests') {
297
+ // --- MODIFIED: Check for 'meta' requirement key ---
298
+ if (node.requires.includes('meta')) {
295
299
  await runMetaComputation(date, node, config, dependencies);
296
300
  } else {
297
301
  // All other calcs (pnl, behavioural, etc.) use the streaming helper
@@ -317,22 +321,13 @@ async function handleNodeTask(message, context, config, dependencies, calculatio
317
321
 
318
322
  } else {
319
323
  logger.log('SUCCESS', `${taskId} Complete. No new children are ready.`);
320
- // Optional: Check if the *entire* DAG is now complete for this day
321
- // This is a "nice-to-have" but good for logging.
322
324
  }
323
325
 
324
326
  } catch (error) {
325
- // --- THIS IS THE SELF-HEALING LOGIC ---
326
- // A calc failed (e.g., dependency missing). We log it and stop.
327
- // We do *not* mark it as "success" in the state manager.
328
- // We do *not* trigger its children.
329
- // The system will wait until the dependency that *caused* this
330
- // failure is fixed and re-triggers this node.
331
327
  logger.log('WARN', `${taskId} FAILED (Will retry when dependencies are met).`, {
332
328
  err: error.message,
333
329
  stack: error.stack
334
330
  });
335
- // Acknowledge the Pub/Sub message so it doesn't retry immediately.
336
331
  return;
337
332
  }
338
333
  }
@@ -16,6 +16,7 @@ class ComputationNode {
16
16
  this.calcClass = calcClass;
17
17
  this.children = new Set(); // Nodes that depend on this one
18
18
  this.parents = new Set(); // Nodes this one depends on
19
+ this.requires = []; // <-- ADDED: Stores data requirements
19
20
  }
20
21
  }
21
22
 
@@ -34,14 +35,16 @@ class ComputationGraph {
34
35
  this.sortedNodes = []; // Array of node names, in valid execution order
35
36
  this.isBuilt = false;
36
37
 
37
- this._registerNodes(unifiedCalculations);
38
+ // --- MODIFIED: Pass requirements config to _registerNodes ---
39
+ this._registerNodes(unifiedCalculations, dependencyConfig.requirements || {});
38
40
  this._buildAdjacencyLists(dependencyConfig.dependencies || {});
39
41
  }
40
42
 
41
43
  /**
42
44
  * Pass 1: Register all calculations as nodes in the graph.
45
+ * --- MODIFIED: Added requirementsConfig parameter ---
43
46
  */
44
- _registerNodes(unifiedCalculations) {
47
+ _registerNodes(unifiedCalculations, requirementsConfig) {
45
48
  this.logger.log('INFO', '[DepGraph] Pass 1: Registering all calculation nodes...');
46
49
  for (const category in unifiedCalculations) {
47
50
  if (category === 'utils') continue;
@@ -53,14 +56,22 @@ class ComputationGraph {
53
56
  if (typeof item === 'object' && item !== null && !Array.isArray(item)) {
54
57
  for (const calcName in item) {
55
58
  if (typeof item[calcName] === 'function') {
56
- this.nodes.set(calcName, new ComputationNode(category, calcName, item[calcName]));
59
+ // --- MODIFIED: Create node, set requirements ---
60
+ const node = new ComputationNode(category, calcName, item[calcName]);
61
+ node.requires = requirementsConfig[calcName] || [];
62
+ this.nodes.set(calcName, node);
63
+ // --- END MODIFICATION ---
57
64
  }
58
65
  }
59
66
  }
60
67
  // Handle regular calcs at the root of the category
61
68
  else if (typeof item === 'function') {
62
69
  const calcName = subKey;
63
- this.nodes.set(calcName, new ComputationNode(category, calcName, item[calcName]));
70
+ // --- MODIFIED: Create node, set requirements ---
71
+ const node = new ComputationNode(category, calcName, item[calcName]);
72
+ node.requires = requirementsConfig[calcName] || [];
73
+ this.nodes.set(calcName, node);
74
+ // --- END MODIFICATION ---
64
75
  }
65
76
  }
66
77
  }
@@ -69,6 +80,7 @@ class ComputationGraph {
69
80
 
70
81
  /**
71
82
  * Pass 2: Build parent/child relationships from the dependency config.
83
+ * (This function remains unchanged as it only reads the 'dependencies' key)
72
84
  */
73
85
  _buildAdjacencyLists(dependencies) {
74
86
  this.logger.log('INFO', '[DepGraph] Pass 2: Building adjacency lists...');
@@ -99,6 +111,7 @@ class ComputationGraph {
99
111
  /**
100
112
  * Pass 3: Perform Topological Sort (Kahn's Algorithm) to find execution order
101
113
  * and detect cycles. This is the "build" step.
114
+ * (This function remains unchanged)
102
115
  */
103
116
  build() {
104
117
  if (this.isBuilt) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.102",
3
+ "version": "1.0.103",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [