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.
- package/functions/computation-system-v2/UserPortfolioMetrics.js +50 -0
- package/functions/computation-system-v2/computations/BehavioralAnomaly.js +557 -337
- package/functions/computation-system-v2/computations/GlobalAumPerAsset30D.js +103 -0
- package/functions/computation-system-v2/computations/PIDailyAssetAUM.js +134 -0
- package/functions/computation-system-v2/computations/PiFeatureVectors.js +227 -0
- package/functions/computation-system-v2/computations/PiRecommender.js +359 -0
- package/functions/computation-system-v2/computations/RiskScoreIncrease.js +13 -13
- package/functions/computation-system-v2/computations/SignedInUserMirrorHistory.js +138 -0
- package/functions/computation-system-v2/computations/SignedInUserPIProfileMetrics.js +106 -0
- package/functions/computation-system-v2/computations/SignedInUserProfileMetrics.js +324 -0
- package/functions/computation-system-v2/config/bulltrackers.config.js +30 -128
- package/functions/computation-system-v2/core-api.js +17 -9
- package/functions/computation-system-v2/data_schema_reference.MD +108 -0
- package/functions/computation-system-v2/devtools/builder/builder.js +362 -0
- package/functions/computation-system-v2/devtools/builder/examples/user-metrics.yaml +26 -0
- package/functions/computation-system-v2/devtools/index.js +36 -0
- package/functions/computation-system-v2/devtools/shared/MockDataFactory.js +235 -0
- package/functions/computation-system-v2/devtools/shared/SchemaTemplates.js +475 -0
- package/functions/computation-system-v2/devtools/shared/SystemIntrospector.js +517 -0
- package/functions/computation-system-v2/devtools/shared/index.js +16 -0
- package/functions/computation-system-v2/devtools/simulation/DAGAnalyzer.js +243 -0
- package/functions/computation-system-v2/devtools/simulation/MockDataFetcher.js +306 -0
- package/functions/computation-system-v2/devtools/simulation/MockStorageManager.js +336 -0
- package/functions/computation-system-v2/devtools/simulation/SimulationEngine.js +525 -0
- package/functions/computation-system-v2/devtools/simulation/SimulationServer.js +581 -0
- package/functions/computation-system-v2/devtools/simulation/index.js +17 -0
- package/functions/computation-system-v2/devtools/simulation/simulate.js +324 -0
- package/functions/computation-system-v2/devtools/vscode-computation/package.json +90 -0
- package/functions/computation-system-v2/devtools/vscode-computation/snippets/computation.json +128 -0
- package/functions/computation-system-v2/devtools/vscode-computation/src/extension.ts +401 -0
- package/functions/computation-system-v2/devtools/vscode-computation/src/providers/codeActions.ts +152 -0
- package/functions/computation-system-v2/devtools/vscode-computation/src/providers/completions.ts +207 -0
- package/functions/computation-system-v2/devtools/vscode-computation/src/providers/diagnostics.ts +205 -0
- package/functions/computation-system-v2/devtools/vscode-computation/src/providers/hover.ts +205 -0
- package/functions/computation-system-v2/devtools/vscode-computation/tsconfig.json +22 -0
- package/functions/computation-system-v2/docs/HowToCreateComputations.MD +602 -0
- package/functions/computation-system-v2/framework/data/DataFetcher.js +250 -184
- package/functions/computation-system-v2/framework/data/MaterializedViewManager.js +84 -0
- package/functions/computation-system-v2/framework/data/QueryBuilder.js +38 -38
- package/functions/computation-system-v2/framework/execution/Orchestrator.js +215 -129
- package/functions/computation-system-v2/framework/scheduling/ScheduleValidator.js +17 -19
- package/functions/computation-system-v2/framework/storage/StateRepository.js +32 -2
- package/functions/computation-system-v2/framework/storage/StorageManager.js +105 -67
- package/functions/computation-system-v2/framework/testing/ComputationTester.js +12 -6
- package/functions/computation-system-v2/handlers/dispatcher.js +57 -29
- package/functions/computation-system-v2/handlers/scheduler.js +172 -203
- package/functions/computation-system-v2/legacy/PiAssetRecommender.js.old +115 -0
- package/functions/computation-system-v2/legacy/PiSimilarityMatrix.js +104 -0
- package/functions/computation-system-v2/legacy/PiSimilarityVector.js +71 -0
- package/functions/computation-system-v2/scripts/debug_aggregation.js +25 -0
- package/functions/computation-system-v2/scripts/test-invalidation-scenarios.js +234 -0
- 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
|
+
});
|