bulltrackers-module 1.0.768 โ†’ 1.0.770

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 (52) hide show
  1. package/functions/computation-system-v2/UserPortfolioMetrics.js +50 -0
  2. package/functions/computation-system-v2/computations/BehavioralAnomaly.js +557 -337
  3. package/functions/computation-system-v2/computations/GlobalAumPerAsset30D.js +103 -0
  4. package/functions/computation-system-v2/computations/PIDailyAssetAUM.js +134 -0
  5. package/functions/computation-system-v2/computations/PiFeatureVectors.js +227 -0
  6. package/functions/computation-system-v2/computations/PiRecommender.js +359 -0
  7. package/functions/computation-system-v2/computations/RiskScoreIncrease.js +13 -13
  8. package/functions/computation-system-v2/computations/SignedInUserMirrorHistory.js +138 -0
  9. package/functions/computation-system-v2/computations/SignedInUserPIProfileMetrics.js +106 -0
  10. package/functions/computation-system-v2/computations/SignedInUserProfileMetrics.js +324 -0
  11. package/functions/computation-system-v2/config/bulltrackers.config.js +30 -128
  12. package/functions/computation-system-v2/core-api.js +17 -9
  13. package/functions/computation-system-v2/data_schema_reference.MD +108 -0
  14. package/functions/computation-system-v2/devtools/builder/builder.js +362 -0
  15. package/functions/computation-system-v2/devtools/builder/examples/user-metrics.yaml +26 -0
  16. package/functions/computation-system-v2/devtools/index.js +36 -0
  17. package/functions/computation-system-v2/devtools/shared/MockDataFactory.js +235 -0
  18. package/functions/computation-system-v2/devtools/shared/SchemaTemplates.js +475 -0
  19. package/functions/computation-system-v2/devtools/shared/SystemIntrospector.js +517 -0
  20. package/functions/computation-system-v2/devtools/shared/index.js +16 -0
  21. package/functions/computation-system-v2/devtools/simulation/DAGAnalyzer.js +243 -0
  22. package/functions/computation-system-v2/devtools/simulation/MockDataFetcher.js +306 -0
  23. package/functions/computation-system-v2/devtools/simulation/MockStorageManager.js +336 -0
  24. package/functions/computation-system-v2/devtools/simulation/SimulationEngine.js +525 -0
  25. package/functions/computation-system-v2/devtools/simulation/SimulationServer.js +581 -0
  26. package/functions/computation-system-v2/devtools/simulation/index.js +17 -0
  27. package/functions/computation-system-v2/devtools/simulation/simulate.js +324 -0
  28. package/functions/computation-system-v2/devtools/vscode-computation/package.json +90 -0
  29. package/functions/computation-system-v2/devtools/vscode-computation/snippets/computation.json +128 -0
  30. package/functions/computation-system-v2/devtools/vscode-computation/src/extension.ts +401 -0
  31. package/functions/computation-system-v2/devtools/vscode-computation/src/providers/codeActions.ts +152 -0
  32. package/functions/computation-system-v2/devtools/vscode-computation/src/providers/completions.ts +207 -0
  33. package/functions/computation-system-v2/devtools/vscode-computation/src/providers/diagnostics.ts +205 -0
  34. package/functions/computation-system-v2/devtools/vscode-computation/src/providers/hover.ts +205 -0
  35. package/functions/computation-system-v2/devtools/vscode-computation/tsconfig.json +22 -0
  36. package/functions/computation-system-v2/docs/HowToCreateComputations.MD +602 -0
  37. package/functions/computation-system-v2/framework/data/DataFetcher.js +250 -184
  38. package/functions/computation-system-v2/framework/data/MaterializedViewManager.js +84 -0
  39. package/functions/computation-system-v2/framework/data/QueryBuilder.js +38 -38
  40. package/functions/computation-system-v2/framework/execution/Orchestrator.js +215 -129
  41. package/functions/computation-system-v2/framework/scheduling/ScheduleValidator.js +17 -19
  42. package/functions/computation-system-v2/framework/storage/StateRepository.js +32 -2
  43. package/functions/computation-system-v2/framework/storage/StorageManager.js +105 -67
  44. package/functions/computation-system-v2/framework/testing/ComputationTester.js +12 -6
  45. package/functions/computation-system-v2/handlers/dispatcher.js +57 -29
  46. package/functions/computation-system-v2/handlers/scheduler.js +172 -203
  47. package/functions/computation-system-v2/legacy/PiAssetRecommender.js.old +115 -0
  48. package/functions/computation-system-v2/legacy/PiSimilarityMatrix.js +104 -0
  49. package/functions/computation-system-v2/legacy/PiSimilarityVector.js +71 -0
  50. package/functions/computation-system-v2/scripts/debug_aggregation.js +25 -0
  51. package/functions/computation-system-v2/scripts/test-invalidation-scenarios.js +234 -0
  52. package/package.json +1 -1
@@ -0,0 +1,115 @@
1
+ const { Computation } = require('../framework');
2
+
3
+ class PiAssetRecommender extends Computation {
4
+ static getConfig() {
5
+ return {
6
+ name: 'PiAssetRecommender',
7
+ type: 'per-entity', // Recommendation is specific to THIS user
8
+ category: 'recommendations',
9
+
10
+ // 1. We rely on Global computations that pre-calculate the similarity graph
11
+ dependencies: ['PiSimilarityMatrix', 'PiTopHoldings'],
12
+
13
+ requires: {
14
+ 'portfolio_snapshots': {
15
+ lookback: 0,
16
+ mandatory: true,
17
+ fields: ['user_id', 'portfolio_data'] // Needed to get Mirrors
18
+ },
19
+ 'user_watchlists': { // Assuming table existence
20
+ lookback: 0,
21
+ mandatory: false,
22
+ fields: ['user_id', 'watched_pi_ids']
23
+ }
24
+ },
25
+
26
+ storage: {
27
+ bigquery: true,
28
+ firestore: { enabled: true, path: 'users/{entityId}/recommendations' }
29
+ }
30
+ };
31
+ }
32
+
33
+ async process(context) {
34
+ const { data, entityId, rules, getDependency } = context;
35
+
36
+ // 1. Get User's Signals (Mirrors + Watchlist)
37
+ const seedPIs = new Set();
38
+
39
+ // A. From Mirrors
40
+ const portfolioRow = data['portfolio_snapshots'];
41
+ if (portfolioRow) {
42
+ const pData = rules.portfolio.extractPortfolioData(portfolioRow);
43
+ const mirrors = rules.portfolio.extractMirrors(pData);
44
+ mirrors.forEach(m => {
45
+ if (m.ParentCID) seedPIs.add(String(m.ParentCID));
46
+ });
47
+ }
48
+
49
+ // B. From Watchlist
50
+ const watchlistRow = data['user_watchlists'];
51
+ if (watchlistRow && watchlistRow.watched_pi_ids) {
52
+ // Assuming array of IDs in column
53
+ const ids = Array.isArray(watchlistRow.watched_pi_ids)
54
+ ? watchlistRow.watched_pi_ids
55
+ : JSON.parse(watchlistRow.watched_pi_ids);
56
+ ids.forEach(id => seedPIs.add(String(id)));
57
+ }
58
+
59
+ if (seedPIs.size === 0) return; // No signals, no recommendations
60
+
61
+ // 2. Load Global Context (The Graph) via Dependencies
62
+ // These are large objects, so typically we fetch specific parts or the whole thing
63
+ // Assuming getDependency returns a Map of PI_ID -> Data
64
+ const similarityGraph = getDependency('PiSimilarityMatrix');
65
+ const piHoldings = getDependency('PiTopHoldings');
66
+
67
+ if (!similarityGraph || !piHoldings) return;
68
+
69
+ // 3. Find Similar PIs (Collaborative Filtering)
70
+ const candidateAssets = new Map(); // AssetID -> Score
71
+
72
+ seedPIs.forEach(seedId => {
73
+ // Find PIs similar to the ones I already copy/watch
74
+ const neighbors = similarityGraph[seedId] || []; // List of { id, score }
75
+
76
+ neighbors.forEach(neighbor => {
77
+ const neighborId = String(neighbor.id);
78
+ const similarityScore = neighbor.score;
79
+
80
+ // What does this neighbor hold?
81
+ const holdings = piHoldings[neighborId] || []; // List of { assetId, weight }
82
+
83
+ holdings.forEach(holding => {
84
+ const currentScore = candidateAssets.get(holding.assetId) || 0;
85
+ // Boost score based on PI similarity and holding weight
86
+ candidateAssets.set(holding.assetId, currentScore + (similarityScore * holding.weight));
87
+ });
88
+ });
89
+ });
90
+
91
+ // 4. Filter Assets User Already Owns
92
+ // (Re-use portfolio logic)
93
+ const myAssets = new Set();
94
+ if (portfolioRow) {
95
+ const pData = rules.portfolio.extractPortfolioData(portfolioRow);
96
+ const positions = rules.portfolio.extractPositions(pData);
97
+ positions.forEach(p => myAssets.add(String(rules.portfolio.getInstrumentId(p))));
98
+ }
99
+
100
+ // 5. Final Ranking
101
+ const recommendations = Array.from(candidateAssets.entries())
102
+ .filter(([assetId]) => !myAssets.has(String(assetId))) // Exclude owned
103
+ .sort((a, b) => b[1] - a[1]) // Sort by score DESC
104
+ .slice(0, 5) // Top 5
105
+ .map(([assetId, score]) => ({
106
+ assetId: Number(assetId),
107
+ score: Number(score.toFixed(4)),
108
+ reason: 'Held by investors similar to those you copy'
109
+ }));
110
+
111
+ this.setResult(entityId, { recommendations });
112
+ }
113
+ }
114
+
115
+ module.exports = PiAssetRecommender;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * @fileoverview Similarity Matrix Meta-Computation.
3
+ * Calculates cosine similarity between all PI vectors to find nearest neighbors.
4
+ */
5
+ class PiSimilarityMatrix {
6
+ constructor() { this.results = {}; }
7
+
8
+ static getMetadata() {
9
+ return {
10
+ type: 'meta', // Runs once per date
11
+ category: 'popular_investor_meta',
12
+ userType: 'POPULAR_INVESTOR'
13
+ };
14
+ }
15
+
16
+ // DEPENDENCY: Forces this to run in Pass 2
17
+ static getDependencies() { return ['PiSimilarityVector']; }
18
+
19
+ static getSchema() {
20
+ return {
21
+ type: 'object', // Map<UserId, Array<SimilarUser>>
22
+ description: 'Top 5 similar users for each PI'
23
+ };
24
+ }
25
+
26
+ async process(context) {
27
+ const vectorMap = context.computed['PiSimilarityVector'];
28
+
29
+ // [FIX] Ensure we have enough peers to actually compare
30
+ if (!vectorMap || Object.keys(vectorMap).length < 2) {
31
+ this.results = {}; // Explicitly set empty
32
+ return this.results;
33
+ }
34
+
35
+ const userIds = Object.keys(vectorMap);
36
+ const matrix = {};
37
+
38
+ for (let i = 0; i < userIds.length; i++) {
39
+ const u1 = userIds[i];
40
+ const v1 = vectorMap[u1];
41
+
42
+ // Skip empty vectors
43
+ if (!v1 || Object.keys(v1).length === 0) continue;
44
+
45
+ const matches = [];
46
+
47
+ // Compare against every other user
48
+ for (let j = 0; j < userIds.length; j++) {
49
+ if (i === j) continue; // Don't compare self
50
+
51
+ const u2 = userIds[j];
52
+ const v2 = vectorMap[u2];
53
+
54
+ if (!v2 || Object.keys(v2).length === 0) continue;
55
+
56
+ // Calculate Cosine Similarity
57
+ let dotProduct = 0;
58
+ let mag1 = 0;
59
+ let mag2 = 0;
60
+
61
+ // v1 magnitude & dot product
62
+ for (const [k, val] of Object.entries(v1)) {
63
+ mag1 += (val * val);
64
+ if (v2[k]) dotProduct += (val * v2[k]);
65
+ }
66
+
67
+ // v2 magnitude
68
+ for (const val of Object.values(v2)) {
69
+ mag2 += (val * val);
70
+ }
71
+
72
+ mag1 = Math.sqrt(mag1);
73
+ mag2 = Math.sqrt(mag2);
74
+
75
+ if (mag1 > 0 && mag2 > 0) {
76
+ const similarity = dotProduct / (mag1 * mag2);
77
+ if (similarity > 0.1) { // Threshold to filter noise
78
+ matches.push({
79
+ id: u2,
80
+ score: Number(similarity.toFixed(4)),
81
+ reason: "Portfolio Composition Match"
82
+ });
83
+ }
84
+ }
85
+ }
86
+
87
+ // Sort by score desc and take top 5
88
+ matrix[u1] = matches.sort((a,b) => b.score - a.score).slice(0, 5);
89
+ }
90
+
91
+ // --- CRITICAL FIX ---
92
+ // 1. Assign to class property (for getResult)
93
+ this.results = matrix;
94
+
95
+ // 2. Return to MetaExecutor (for wrapping)
96
+ return this.results;
97
+ }
98
+
99
+ async getResult() {
100
+ return this.results;
101
+ }
102
+ }
103
+
104
+ module.exports = PiSimilarityMatrix;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * @fileoverview Similarity Vector for Popular Investors.
3
+ * Generates a normalized asset allocation vector.
4
+ */
5
+
6
+ class PiSimilarityVector {
7
+ constructor() {
8
+ this.results = {};
9
+ }
10
+
11
+ static getMetadata() {
12
+ return {
13
+ type: 'standard', // MUST be 'standard'
14
+ category: 'popular_investor',
15
+ rootDataDependencies: ['portfolio'],
16
+ userType: 'POPULAR_INVESTOR'
17
+ };
18
+ }
19
+
20
+ static getDependencies() { return []; }
21
+
22
+ static getSchema() {
23
+ return {
24
+ type: 'object',
25
+ patternProperties: {
26
+ "^[0-9]+$": { type: 'number' } // InstrumentID -> Weight
27
+ },
28
+ description: 'Normalized Asset Allocation Vector'
29
+ };
30
+ }
31
+
32
+ process(context) {
33
+ const { DataExtractor } = context.math;
34
+ const userId = context.user.id;
35
+ const portfolio = context.user.portfolio.today;
36
+
37
+ if (!portfolio) {
38
+ this.results[userId] = {};
39
+ return;
40
+ }
41
+
42
+ const positions = DataExtractor.getPositions(portfolio, 'POPULAR_INVESTOR');
43
+ const vector = {};
44
+ let totalValue = 0;
45
+
46
+ for (const pos of positions) {
47
+ const id = DataExtractor.getInstrumentId(pos);
48
+ const weight = DataExtractor.getPositionWeight(pos);
49
+
50
+ if (id && weight > 0) {
51
+ vector[id] = (vector[id] || 0) + weight;
52
+ totalValue += weight;
53
+ }
54
+ }
55
+
56
+ // Normalize
57
+ if (totalValue > 0) {
58
+ for (const key in vector) {
59
+ vector[key] = Number((vector[key] / totalValue).toFixed(6));
60
+ }
61
+ }
62
+
63
+ this.results[userId] = vector;
64
+ }
65
+
66
+ async getResult() {
67
+ return this.results;
68
+ }
69
+ }
70
+
71
+ module.exports = PiSimilarityVector;
@@ -0,0 +1,25 @@
1
+ const { BigQuery } = require('@google-cloud/bigquery');
2
+ const config = require('../config/bulltrackers.config');
3
+
4
+ async function checkAggregation() {
5
+ const bq = new BigQuery({ projectId: config.bigquery.projectId });
6
+ const date = '2026-01-30';
7
+
8
+ console.log(`Checking Aggregation on ${date}`);
9
+
10
+ const query = `
11
+ SELECT user_type, COUNT(*) as count
12
+ FROM \`${config.bigquery.projectId}.${config.bigquery.dataset}.portfolio_snapshots\`
13
+ WHERE date = @date
14
+ GROUP BY 1
15
+ `;
16
+
17
+ const [rows] = await bq.query({
18
+ query,
19
+ params: { date }
20
+ });
21
+
22
+ console.log('Result:', rows);
23
+ }
24
+
25
+ checkAggregation().catch(console.error);
@@ -0,0 +1,234 @@
1
+ /**
2
+ * @fileoverview Real-World Invalidation Simulator
3
+ * Dynamically loads ALL computation AND rule code to verify:
4
+ * 1. Code Hash Generation (using real files).
5
+ * 2. Invalidation logic (Simulated DB vs Real Disk Hash).
6
+ * 3. Dependency chains (Dynamic discovery of chains).
7
+ * * Usage: node scripts/test-real-scenarios.js
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const assert = require('assert');
13
+
14
+ // Framework Components
15
+ const { RunAnalyzer } = require('../framework/core/RunAnalyzer');
16
+ const { Orchestrator } = require('../framework/execution/Orchestrator');
17
+ const { ManifestBuilder } = require('../framework/core/Manifest');
18
+
19
+ // -----------------------------------------------------------------------------
20
+ // 1. MOCKS (Virtual Environment)
21
+ // -----------------------------------------------------------------------------
22
+
23
+ class MockDataFetcher {
24
+ async checkAvailability() { return { canRun: true, missing: [] }; }
25
+ }
26
+
27
+ class MockStateRepository {
28
+ constructor() {
29
+ this.statusMap = new Map();
30
+ this.results = new Map();
31
+ }
32
+
33
+ setDailyStatus(date, compName, status) {
34
+ if (!this.statusMap.has(date)) this.statusMap.set(date, new Map());
35
+ this.statusMap.get(date).set(compName.toLowerCase(), status);
36
+ }
37
+
38
+ async getDailyStatus(date) {
39
+ return this.statusMap.get(date) || new Map();
40
+ }
41
+
42
+ // Simulate finding history for backfills
43
+ async getRunDates(compName) { return ['2024-01-01', '2024-01-02']; }
44
+
45
+ // Mock Result fetching
46
+ async getResult(date, compName) { return this.results.get(`${date}:${compName}`); }
47
+ }
48
+
49
+ class TestOrchestrator extends Orchestrator {
50
+ constructor(config, stateRepo) {
51
+ super(config);
52
+ this.stateRepository = stateRepo;
53
+ this.scheduledTasks = [];
54
+ }
55
+ async _scheduleCloudTask(name, date, source, delay) {
56
+ this.scheduledTasks.push({ name, date, source });
57
+ }
58
+ _log(level, msg) { /* console.log(`[${level}] ${msg}`); */ }
59
+ }
60
+
61
+ // -----------------------------------------------------------------------------
62
+ // 2. HELPER: Dynamic Loader
63
+ // -----------------------------------------------------------------------------
64
+ function loadModulesFromDir(directory, label) {
65
+ let modules = [];
66
+ try {
67
+ if (fs.existsSync(directory)) {
68
+ const files = fs.readdirSync(directory).filter(f => f.endsWith('.js') && f !== 'index.js');
69
+ console.log(`INFO: Found ${files.length} ${label} files in ${directory}`);
70
+
71
+ modules = files.map(file => {
72
+ try {
73
+ return require(path.join(directory, file));
74
+ } catch (e) {
75
+ console.warn(`WARN: Failed to load ${file}: ${e.message}`);
76
+ return null;
77
+ }
78
+ }).filter(c => c !== null);
79
+ } else {
80
+ console.warn(`WARN: ${label} directory not found at ${directory}`);
81
+ }
82
+ } catch (e) {
83
+ console.error(`ERROR: Could not load ${label}: ${e.message}`);
84
+ }
85
+ return modules;
86
+ }
87
+
88
+ // -----------------------------------------------------------------------------
89
+ // 3. TEST SUITE
90
+ // -----------------------------------------------------------------------------
91
+
92
+ async function runRealWorldTests() {
93
+ console.log('โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”');
94
+ console.log('โ”‚ REAL CODE SIMULATION SUITE โ”‚');
95
+ console.log('โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜');
96
+
97
+ // 1. LOAD ARTIFACTS
98
+ const computationsDir = path.join(__dirname, '../computations');
99
+ const rulesDir = path.join(__dirname, '../rules');
100
+
101
+ const computationClasses = loadModulesFromDir(computationsDir, 'computation');
102
+ const ruleModules = loadModulesFromDir(rulesDir, 'rule'); // <--- NEW: Load Rules
103
+
104
+ if (computationClasses.length === 0) {
105
+ console.error("โŒ ABORT: No valid computations found to test.");
106
+ return;
107
+ }
108
+
109
+ // 2. BUILD REAL MANIFEST
110
+ const config = {
111
+ computations: computationClasses,
112
+ rules: ruleModules // <--- NEW: Pass Rules to Config
113
+ };
114
+
115
+ // Initialize ManifestBuilder (this internally initializes RulesRegistry too)
116
+ const builder = new ManifestBuilder(config, console);
117
+ const realManifest = builder.build(config.computations);
118
+
119
+ console.log(`INFO: Successfully built manifest with ${realManifest.length} entries.`);
120
+
121
+ // 3. RUN TESTS
122
+ await test_RealCodeInvalidation(realManifest);
123
+ await test_DynamicDependencyCascading(realManifest, config);
124
+ }
125
+
126
+ /**
127
+ * SCENARIO 1: Simulate a Deployment (Code Change)
128
+ */
129
+ async function test_RealCodeInvalidation(manifest) {
130
+ // Pick 'RiskScoreIncrease' if available, otherwise the first one
131
+ let target = manifest.find(c => c.name === 'riskscoreincrease');
132
+ if (!target) target = manifest[0];
133
+
134
+ console.log(`\n๐Ÿงช Test 1: Real Code Hash Mismatch (${target.originalName})`);
135
+
136
+ const analyzer = new RunAnalyzer(manifest, new MockDataFetcher());
137
+ const date = '2024-06-01';
138
+
139
+ // CONDITION: The DB has an old/different hash
140
+ const dailyStatus = new Map();
141
+ dailyStatus.set(target.name, {
142
+ hash: 'OLD_DEPLOYMENT_HASH_123',
143
+ resultHash: 'res_v1',
144
+ updatedAt: new Date().toISOString()
145
+ });
146
+
147
+ // ANALYZE
148
+ const result = await analyzer._evaluateEntry(target, date, true, dailyStatus, new Map());
149
+
150
+ // ASSERT
151
+ try {
152
+ assert.strictEqual(result.type, 'reRuns', 'Should trigger re-run');
153
+ assert.strictEqual(result.payload.reason, 'Code changed', 'Reason must be code change');
154
+
155
+ console.log(` [Current Disk Hash]: ${target.hash.substring(0, 16)}...`);
156
+ console.log(` [Simulated DB Hash]: OLD_DEPLOYMENT_HASH_123`);
157
+ console.log('โœ… PASS: System correctly detected the code mismatch.');
158
+ } catch (e) {
159
+ console.error('โŒ FAIL:', e.message);
160
+ console.log(' Decision:', result);
161
+ }
162
+ }
163
+
164
+ /**
165
+ * SCENARIO 2: Dynamic Dependency Propagation
166
+ */
167
+ async function test_DynamicDependencyCascading(manifest, config) {
168
+ console.log('\n๐Ÿงช Test 2: Dependency Propagation (Dynamic Chain Detection)');
169
+
170
+ // 1. Find a valid dependency chain (Parent -> Child)
171
+ let upstream = null;
172
+ let downstream = null;
173
+
174
+ for (const child of manifest) {
175
+ if (child.dependencies && child.dependencies.length > 0) {
176
+ // Find the parent object
177
+ const parentName = child.dependencies[0]; // normalized name usually
178
+ const parent = manifest.find(c => c.name === parentName); // match normalized names
179
+
180
+ if (parent) {
181
+ upstream = parent;
182
+ downstream = child;
183
+ break; // Found a pair!
184
+ }
185
+ }
186
+ }
187
+
188
+ // 2. Handle Case: No Chains Found
189
+ if (!upstream || !downstream) {
190
+ console.log('โš ๏ธ SKIPPED: No valid dependency chains found in the loaded computations.');
191
+ console.log(' (This is NOT a failure. It simply means all loaded computations are independent)');
192
+ console.log('โœ… PASS: Skipped due to lack of testable chains.');
193
+ return;
194
+ }
195
+
196
+ console.log(` Found chain: ${upstream.originalName} -> ${downstream.originalName}`);
197
+
198
+ // 3. Setup Orchestrator
199
+ const mockState = new MockStateRepository();
200
+ const orch = new TestOrchestrator({ ...config, bigquery: {}, tables: {} }, mockState);
201
+ orch.manifest = manifest;
202
+ orch._buildDependentsIndex();
203
+
204
+ const date = '2024-06-01';
205
+
206
+ // 4. Simulate Upstream Finishing
207
+ mockState.setDailyStatus(date, upstream.name, {
208
+ hash: upstream.hash,
209
+ resultHash: 'NEW_RESULT_HASH_XYZ',
210
+ updatedAt: new Date().toISOString()
211
+ });
212
+
213
+ // 5. Trigger Cascading Logic
214
+ await orch._scheduleDependents(upstream, date);
215
+
216
+ // 6. Assert
217
+ try {
218
+ const task = orch.scheduledTasks.find(t => t.name === downstream.originalName);
219
+ if (task) {
220
+ console.log(` [Triggered]: ${task.name} due to update in ${upstream.name}`);
221
+ console.log('โœ… PASS: Dependency chain resolved and task scheduled.');
222
+ } else {
223
+ console.error(`โŒ FAIL: Downstream task (${downstream.name}) was NOT scheduled.`);
224
+ console.log(' Dependents Map keys:', Array.from(orch.dependentsByName.keys()));
225
+ }
226
+ } catch (e) {
227
+ console.error('โŒ FAIL:', e.message);
228
+ }
229
+ }
230
+
231
+ // EXECUTE
232
+ runRealWorldTests().catch(e => {
233
+ console.error('FATAL:', e);
234
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.768",
3
+ "version": "1.0.770",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [