bulltrackers-module 1.0.219 → 1.0.221
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/WorkflowOrchestrator.js +153 -0
- package/functions/computation-system/context/ContextFactory.js +63 -0
- package/functions/computation-system/context/ManifestBuilder.js +240 -0
- package/functions/computation-system/controllers/computation_controller.js +12 -4
- package/functions/computation-system/data/AvailabilityChecker.js +75 -0
- package/functions/computation-system/data/CachedDataLoader.js +63 -0
- package/functions/computation-system/data/DependencyFetcher.js +70 -0
- package/functions/computation-system/executors/MetaExecutor.js +68 -0
- package/functions/computation-system/executors/PriceBatchExecutor.js +99 -0
- package/functions/computation-system/executors/StandardExecutor.js +109 -0
- package/functions/computation-system/helpers/computation_dispatcher.js +7 -7
- package/functions/computation-system/helpers/computation_worker.js +44 -18
- package/functions/computation-system/layers/mathematics.js +1 -1
- package/functions/computation-system/persistence/FirestoreUtils.js +64 -0
- package/functions/computation-system/persistence/ResultCommitter.js +119 -0
- package/functions/computation-system/persistence/StatusRepository.js +29 -0
- package/functions/computation-system/topology/HashManager.js +35 -0
- package/functions/computation-system/utils/utils.js +39 -11
- package/index.js +8 -3
- package/package.json +1 -1
- package/functions/computation-system/helpers/computation_manifest_builder.js +0 -320
- package/functions/computation-system/helpers/computation_pass_runner.js +0 -119
- package/functions/computation-system/helpers/orchestration_helpers.js +0 -352
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Main Orchestrator. Coordinates the topological execution of calculations.
|
|
3
|
+
*/
|
|
4
|
+
const { normalizeName, getExpectedDateStrings } = require('./utils/utils');
|
|
5
|
+
const { checkRootDependencies, checkRootDataAvailability } = require('./data/AvailabilityChecker');
|
|
6
|
+
const { fetchExistingResults } = require('./data/DependencyFetcher');
|
|
7
|
+
const { fetchComputationStatus, updateComputationStatus } = require('./persistence/StatusRepository');
|
|
8
|
+
const { runBatchPriceComputation } = require('./executors/PriceBatchExecutor');
|
|
9
|
+
const { StandardExecutor } = require('./executors/StandardExecutor');
|
|
10
|
+
const { MetaExecutor } = require('./executors/MetaExecutor');
|
|
11
|
+
|
|
12
|
+
const PARALLEL_BATCH_SIZE = 7;
|
|
13
|
+
|
|
14
|
+
function groupByPass(manifest) {
|
|
15
|
+
return manifest.reduce((acc, calc) => {
|
|
16
|
+
(acc[calc.pass] = acc[calc.pass] || []).push(calc);
|
|
17
|
+
return acc;
|
|
18
|
+
}, {});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function runComputationPass(config, dependencies, computationManifest) {
|
|
22
|
+
const { logger } = dependencies;
|
|
23
|
+
const passToRun = String(config.COMPUTATION_PASS_TO_RUN);
|
|
24
|
+
if (!passToRun) return logger.log('ERROR', '[PassRunner] No pass defined. Aborting.');
|
|
25
|
+
|
|
26
|
+
logger.log('INFO', `🚀 Starting PASS ${passToRun}...`);
|
|
27
|
+
|
|
28
|
+
const earliestDates = {
|
|
29
|
+
portfolio: new Date('2025-09-25T00:00:00Z'),
|
|
30
|
+
history: new Date('2025-11-05T00:00:00Z'),
|
|
31
|
+
social: new Date('2025-10-30T00:00:00Z'),
|
|
32
|
+
insights: new Date('2025-08-26T00:00:00Z'),
|
|
33
|
+
price: new Date('2025-08-01T00:00:00Z')
|
|
34
|
+
};
|
|
35
|
+
earliestDates.absoluteEarliest = Object.values(earliestDates).reduce((a, b) => a < b ? a : b);
|
|
36
|
+
const passes = groupByPass(computationManifest);
|
|
37
|
+
const calcsInThisPass = passes[passToRun] || [];
|
|
38
|
+
|
|
39
|
+
if (!calcsInThisPass.length) return logger.log('WARN', `[PassRunner] No calcs for Pass ${passToRun}. Exiting.`);
|
|
40
|
+
|
|
41
|
+
const passEarliestDate = earliestDates.absoluteEarliest;
|
|
42
|
+
const endDateUTC = new Date(Date.UTC(new Date().getUTCFullYear(), new Date().getUTCMonth(), new Date().getUTCDate() - 1));
|
|
43
|
+
const allExpectedDates = getExpectedDateStrings(passEarliestDate, endDateUTC);
|
|
44
|
+
|
|
45
|
+
// Separate specialized batch calcs
|
|
46
|
+
const priceBatchCalcs = calcsInThisPass.filter(c => c.type === 'meta' && c.rootDataDependencies?.includes('price'));
|
|
47
|
+
const standardAndOtherMetaCalcs = calcsInThisPass.filter(c => !priceBatchCalcs.includes(c));
|
|
48
|
+
|
|
49
|
+
if (priceBatchCalcs.length > 0) {
|
|
50
|
+
try {
|
|
51
|
+
await runBatchPriceComputation(config, dependencies, allExpectedDates, priceBatchCalcs);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
logger.log('ERROR', 'Batch Price failed', e);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (standardAndOtherMetaCalcs.length === 0) return;
|
|
58
|
+
|
|
59
|
+
for (let i = 0; i < allExpectedDates.length; i += PARALLEL_BATCH_SIZE) {
|
|
60
|
+
const batch = allExpectedDates.slice(i, i + PARALLEL_BATCH_SIZE);
|
|
61
|
+
await Promise.all(batch.map(dateStr =>
|
|
62
|
+
runDateComputation(dateStr, passToRun, standardAndOtherMetaCalcs, config, dependencies, computationManifest)
|
|
63
|
+
));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function runDateComputation(dateStr, passToRun, calcsInThisPass, config, dependencies, computationManifest) {
|
|
68
|
+
const { logger } = dependencies;
|
|
69
|
+
const dateToProcess = new Date(dateStr + 'T00:00:00Z');
|
|
70
|
+
|
|
71
|
+
// 1. Version Check
|
|
72
|
+
const dailyStatus = await fetchComputationStatus(dateStr, config, dependencies);
|
|
73
|
+
const calcsToAttempt = [];
|
|
74
|
+
|
|
75
|
+
for (const calc of calcsInThisPass) {
|
|
76
|
+
const cName = normalizeName(calc.name);
|
|
77
|
+
const storedStatus = dailyStatus[cName];
|
|
78
|
+
const currentHash = calc.hash;
|
|
79
|
+
|
|
80
|
+
if (calc.dependencies && calc.dependencies.length > 0) {
|
|
81
|
+
const missing = calc.dependencies.filter(depName => !dailyStatus[normalizeName(depName)]);
|
|
82
|
+
if (missing.length > 0) {
|
|
83
|
+
logger.log('TRACE', `[Skip] ${cName} missing deps: ${missing.join(', ')}`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (!storedStatus) {
|
|
88
|
+
logger.log('INFO', `[Versioning] ${cName}: New run needed.`);
|
|
89
|
+
calcsToAttempt.push(calc); continue;
|
|
90
|
+
}
|
|
91
|
+
if (typeof storedStatus === 'string' && currentHash && storedStatus !== currentHash) {
|
|
92
|
+
logger.log('INFO', `[Versioning] ${cName}: Code Changed.`);
|
|
93
|
+
calcsToAttempt.push(calc); continue;
|
|
94
|
+
}
|
|
95
|
+
if (storedStatus === true && currentHash) {
|
|
96
|
+
logger.log('INFO', `[Versioning] ${cName}: Upgrading legacy status.`);
|
|
97
|
+
calcsToAttempt.push(calc); continue;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!calcsToAttempt.length) return null;
|
|
102
|
+
|
|
103
|
+
// 2. Data Check
|
|
104
|
+
const earliestDates = {
|
|
105
|
+
portfolio: new Date('2025-09-25T00:00:00Z'),
|
|
106
|
+
history: new Date('2025-11-05T00:00:00Z'),
|
|
107
|
+
social: new Date('2025-10-30T00:00:00Z'),
|
|
108
|
+
insights: new Date('2025-08-26T00:00:00Z'),
|
|
109
|
+
price: new Date('2025-08-01T00:00:00Z')
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const rootData = await checkRootDataAvailability(dateStr, config, dependencies, earliestDates);
|
|
113
|
+
if (!rootData) { logger.log('INFO', `[DateRunner] Root data missing for ${dateStr}. Skipping.`); return null; }
|
|
114
|
+
|
|
115
|
+
const runnableCalcs = calcsToAttempt.filter(c => checkRootDependencies(c, rootData.status).canRun);
|
|
116
|
+
if (!runnableCalcs.length) return null;
|
|
117
|
+
|
|
118
|
+
const standardToRun = runnableCalcs.filter(c => c.type === 'standard');
|
|
119
|
+
const metaToRun = runnableCalcs.filter(c => c.type === 'meta');
|
|
120
|
+
logger.log('INFO', `[DateRunner] Running ${dateStr}: ${standardToRun.length} std, ${metaToRun.length} meta`);
|
|
121
|
+
|
|
122
|
+
const dateUpdates = {};
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const calcsRunning = [...standardToRun, ...metaToRun];
|
|
126
|
+
const existingResults = await fetchExistingResults(dateStr, calcsRunning, computationManifest, config, dependencies, false);
|
|
127
|
+
const prevDate = new Date(dateToProcess); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
|
|
128
|
+
const prevDateStr = prevDate.toISOString().slice(0, 10);
|
|
129
|
+
const previousResults = await fetchExistingResults(prevDateStr, calcsRunning, computationManifest, config, dependencies, true);
|
|
130
|
+
|
|
131
|
+
if (standardToRun.length) {
|
|
132
|
+
const updates = await StandardExecutor.run(dateToProcess, standardToRun, `Pass ${passToRun} (Std)`, config, dependencies, rootData, existingResults, previousResults, false);
|
|
133
|
+
Object.assign(dateUpdates, updates);
|
|
134
|
+
}
|
|
135
|
+
if (metaToRun.length) {
|
|
136
|
+
const updates = await MetaExecutor.run(dateToProcess, metaToRun, `Pass ${passToRun} (Meta)`, config, dependencies, existingResults, previousResults, rootData, false);
|
|
137
|
+
Object.assign(dateUpdates, updates);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
} catch (err) {
|
|
141
|
+
logger.log('ERROR', `[DateRunner] FAILED Pass ${passToRun} for ${dateStr}`, { errorMessage: err.message });
|
|
142
|
+
[...standardToRun, ...metaToRun].forEach(c => dateUpdates[normalizeName(c.name)] = false);
|
|
143
|
+
throw err;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (Object.keys(dateUpdates).length > 0) {
|
|
147
|
+
await updateComputationStatus(dateStr, dateUpdates, config, dependencies);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { date: dateStr, updates: dateUpdates };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = { runComputationPass, runDateComputation, groupByPass };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Factory for creating the Computation Context.
|
|
3
|
+
*/
|
|
4
|
+
const mathLayer = require('../layers/index');
|
|
5
|
+
const { LEGACY_MAPPING } = require('../topology/HashManager');
|
|
6
|
+
|
|
7
|
+
class ContextFactory {
|
|
8
|
+
static buildMathContext() {
|
|
9
|
+
const mathContext = {};
|
|
10
|
+
for (const [key, value] of Object.entries(mathLayer)) {
|
|
11
|
+
mathContext[key] = value;
|
|
12
|
+
const legacyKey = LEGACY_MAPPING[key];
|
|
13
|
+
if (legacyKey) { mathContext[legacyKey] = value; }
|
|
14
|
+
}
|
|
15
|
+
return mathContext;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static buildPerUserContext(options) {
|
|
19
|
+
const {
|
|
20
|
+
todayPortfolio, yesterdayPortfolio, todayHistory, yesterdayHistory,
|
|
21
|
+
userId, userType, dateStr, metadata, mappings, insights, socialData,
|
|
22
|
+
computedDependencies, previousComputedDependencies, config, deps
|
|
23
|
+
} = options;
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
user: {
|
|
27
|
+
id: userId,
|
|
28
|
+
type: userType,
|
|
29
|
+
portfolio: { today: todayPortfolio, yesterday: yesterdayPortfolio },
|
|
30
|
+
history: { today: todayHistory, yesterday: yesterdayHistory }
|
|
31
|
+
},
|
|
32
|
+
date: { today: dateStr },
|
|
33
|
+
insights: { today: insights?.today, yesterday: insights?.yesterday },
|
|
34
|
+
social: { today: socialData?.today, yesterday: socialData?.yesterday },
|
|
35
|
+
mappings: mappings || {},
|
|
36
|
+
math: ContextFactory.buildMathContext(),
|
|
37
|
+
computed: computedDependencies || {},
|
|
38
|
+
previousComputed: previousComputedDependencies || {},
|
|
39
|
+
meta: metadata, config, deps
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static buildMetaContext(options) {
|
|
44
|
+
const {
|
|
45
|
+
dateStr, metadata, mappings, insights, socialData, prices,
|
|
46
|
+
computedDependencies, previousComputedDependencies, config, deps
|
|
47
|
+
} = options;
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
date: { today: dateStr },
|
|
51
|
+
insights: { today: insights?.today, yesterday: insights?.yesterday },
|
|
52
|
+
social: { today: socialData?.today, yesterday: socialData?.yesterday },
|
|
53
|
+
prices: prices || {},
|
|
54
|
+
mappings: mappings || {},
|
|
55
|
+
math: ContextFactory.buildMathContext(),
|
|
56
|
+
computed: computedDependencies || {},
|
|
57
|
+
previousComputed: previousComputedDependencies || {},
|
|
58
|
+
meta: metadata, config, deps
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { ContextFactory };
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Dynamic Manifest Builder - Handles Topological Sort and Auto-Discovery.
|
|
3
|
+
*/
|
|
4
|
+
const { generateCodeHash, LEGACY_MAPPING } = require('../topology/HashManager.js');
|
|
5
|
+
const { normalizeName } = require('../utils/utils');
|
|
6
|
+
|
|
7
|
+
// Import Layers
|
|
8
|
+
const MathematicsLayer = require('../layers/mathematics');
|
|
9
|
+
const ExtractorsLayer = require('../layers/extractors');
|
|
10
|
+
const ProfilingLayer = require('../layers/profiling');
|
|
11
|
+
const ValidatorsLayer = require('../layers/validators');
|
|
12
|
+
|
|
13
|
+
const LAYER_GROUPS = {
|
|
14
|
+
'mathematics': MathematicsLayer,
|
|
15
|
+
'extractors': ExtractorsLayer,
|
|
16
|
+
'profiling': ProfilingLayer,
|
|
17
|
+
'validators': ValidatorsLayer
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function generateLayerHashes(layerExports, layerName) {
|
|
21
|
+
const hashes = {};
|
|
22
|
+
const keys = Object.keys(layerExports).sort();
|
|
23
|
+
for (const key of keys) {
|
|
24
|
+
const item = layerExports[key];
|
|
25
|
+
let source = `LAYER:${layerName}:EXPORT:${key}`;
|
|
26
|
+
if (typeof item === 'function') source += item.toString();
|
|
27
|
+
else if (typeof item === 'object' && item !== null) source += JSON.stringify(item);
|
|
28
|
+
else source += String(item);
|
|
29
|
+
hashes[key] = generateCodeHash(source);
|
|
30
|
+
}
|
|
31
|
+
return hashes;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function buildDynamicTriggers() {
|
|
35
|
+
const triggers = {};
|
|
36
|
+
for (const [layerName, layerExports] of Object.entries(LAYER_GROUPS)) {
|
|
37
|
+
triggers[layerName] = {};
|
|
38
|
+
for (const exportName of Object.keys(layerExports)) {
|
|
39
|
+
const patterns = [];
|
|
40
|
+
patterns.push(exportName);
|
|
41
|
+
const alias = LEGACY_MAPPING[exportName];
|
|
42
|
+
if (alias) {
|
|
43
|
+
patterns.push(`math.${alias}`);
|
|
44
|
+
patterns.push(`${alias}.`);
|
|
45
|
+
}
|
|
46
|
+
triggers[layerName][exportName] = patterns;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return triggers;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const LAYER_HASHES = {};
|
|
53
|
+
for (const [name, exports] of Object.entries(LAYER_GROUPS)) {
|
|
54
|
+
LAYER_HASHES[name] = generateLayerHashes(exports, name);
|
|
55
|
+
}
|
|
56
|
+
const LAYER_TRIGGERS = buildDynamicTriggers();
|
|
57
|
+
|
|
58
|
+
const log = {
|
|
59
|
+
info: (msg) => console.log('ℹ︎ ' + msg),
|
|
60
|
+
step: (msg) => console.log('› ' + msg),
|
|
61
|
+
warn: (msg) => console.warn('⚠︎ ' + msg),
|
|
62
|
+
success: (msg) => console.log('✔︎ ' + msg),
|
|
63
|
+
error: (msg) => console.error('✖ ' + msg),
|
|
64
|
+
fatal: (msg) => { console.error('✖ FATAL ✖ ' + msg); console.error('✖ FATAL ✖ Manifest build FAILED.'); },
|
|
65
|
+
divider: (label) => { const line = ''.padEnd(60, '─'); console.log(`\n${line}\n${label}\n${line}\n`); },
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
function getDependencySet(endpoints, adjacencyList) {
|
|
69
|
+
const required = new Set(endpoints);
|
|
70
|
+
const queue = [...endpoints];
|
|
71
|
+
while (queue.length > 0) {
|
|
72
|
+
const calcName = queue.shift();
|
|
73
|
+
const dependencies = adjacencyList.get(calcName);
|
|
74
|
+
if (dependencies) {
|
|
75
|
+
for (const dep of dependencies) {
|
|
76
|
+
if (!required.has(dep)) { required.add(dep); queue.push(dep); }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return required;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function buildManifest(productLinesToRun = [], calculations) {
|
|
84
|
+
log.divider('Building Dynamic Manifest');
|
|
85
|
+
|
|
86
|
+
const manifestMap = new Map();
|
|
87
|
+
const adjacency = new Map();
|
|
88
|
+
const reverseAdjacency = new Map();
|
|
89
|
+
const inDegree = new Map();
|
|
90
|
+
let hasFatalError = false;
|
|
91
|
+
|
|
92
|
+
function processCalc(Class, name, folderName) {
|
|
93
|
+
if (!Class || typeof Class !== 'function') return;
|
|
94
|
+
const normalizedName = normalizeName(name);
|
|
95
|
+
|
|
96
|
+
if (typeof Class.getMetadata !== 'function') { log.fatal(`Calculation "${normalizedName}" missing static getMetadata().`); hasFatalError = true; return; }
|
|
97
|
+
if (typeof Class.getDependencies !== 'function') { log.fatal(`Calculation "${normalizedName}" missing static getDependencies().`); hasFatalError = true; return; }
|
|
98
|
+
|
|
99
|
+
const metadata = Class.getMetadata();
|
|
100
|
+
const dependencies = Class.getDependencies().map(normalizeName);
|
|
101
|
+
const codeStr = Class.toString();
|
|
102
|
+
|
|
103
|
+
let compositeHashString = generateCodeHash(codeStr);
|
|
104
|
+
const usedDeps = [];
|
|
105
|
+
|
|
106
|
+
for (const [layerName, exportsMap] of Object.entries(LAYER_TRIGGERS)) {
|
|
107
|
+
const layerHashes = LAYER_HASHES[layerName];
|
|
108
|
+
for (const [exportName, triggers] of Object.entries(exportsMap)) {
|
|
109
|
+
if (triggers.some(trigger => codeStr.includes(trigger))) {
|
|
110
|
+
const exportHash = layerHashes[exportName];
|
|
111
|
+
if (exportHash) {
|
|
112
|
+
compositeHashString += exportHash;
|
|
113
|
+
usedDeps.push(`${layerName}.${exportName}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Safe Mode Fallback
|
|
120
|
+
let isSafeMode = false;
|
|
121
|
+
if (usedDeps.length === 0) {
|
|
122
|
+
isSafeMode = true;
|
|
123
|
+
Object.values(LAYER_HASHES).forEach(layerObj => { Object.values(layerObj).forEach(h => compositeHashString += h); });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const baseHash = generateCodeHash(compositeHashString);
|
|
127
|
+
|
|
128
|
+
const manifestEntry = {
|
|
129
|
+
name: normalizedName,
|
|
130
|
+
class: Class,
|
|
131
|
+
category: folderName === 'core' && metadata.category ? metadata.category : folderName,
|
|
132
|
+
sourcePackage: folderName,
|
|
133
|
+
type: metadata.type,
|
|
134
|
+
isHistorical: metadata.isHistorical,
|
|
135
|
+
rootDataDependencies: metadata.rootDataDependencies || [],
|
|
136
|
+
userType: metadata.userType,
|
|
137
|
+
dependencies: dependencies,
|
|
138
|
+
pass: 0,
|
|
139
|
+
hash: baseHash,
|
|
140
|
+
debugUsedLayers: isSafeMode ? ['ALL (Safe Mode)'] : usedDeps
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
manifestMap.set(normalizedName, manifestEntry);
|
|
144
|
+
adjacency.set(normalizedName, dependencies);
|
|
145
|
+
inDegree.set(normalizedName, dependencies.length);
|
|
146
|
+
dependencies.forEach(dep => { if (!reverseAdjacency.has(dep)) reverseAdjacency.set(dep, []); reverseAdjacency.get(dep).push(normalizedName); });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const folderName in calculations) {
|
|
150
|
+
if (folderName === 'legacy') continue;
|
|
151
|
+
const group = calculations[folderName];
|
|
152
|
+
for (const key in group) {
|
|
153
|
+
const entry = group[key];
|
|
154
|
+
if (typeof entry === 'function') { processCalc(entry, key, folderName); }
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (hasFatalError) throw new Error('Manifest build failed due to missing static methods.');
|
|
159
|
+
log.success(`Loaded ${manifestMap.size} calculations.`);
|
|
160
|
+
|
|
161
|
+
// --- Topological Sort & Product Line Filtering ---
|
|
162
|
+
const allNames = new Set(manifestMap.keys());
|
|
163
|
+
for (const [name, entry] of manifestMap) {
|
|
164
|
+
for (const dep of entry.dependencies) {
|
|
165
|
+
if (!allNames.has(dep)) log.error(`${name} depends on unknown calculation "${dep}"`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const productLineEndpoints = [];
|
|
170
|
+
for (const [name, entry] of manifestMap.entries()) {
|
|
171
|
+
if (productLinesToRun.includes(entry.category) || entry.sourcePackage === 'core') {
|
|
172
|
+
productLineEndpoints.push(name);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const requiredCalcs = getDependencySet(productLineEndpoints, adjacency);
|
|
177
|
+
log.info(`Filtered down to ${requiredCalcs.size} active calculations.`);
|
|
178
|
+
|
|
179
|
+
const filteredManifestMap = new Map();
|
|
180
|
+
const filteredInDegree = new Map();
|
|
181
|
+
const filteredReverseAdjacency = new Map();
|
|
182
|
+
|
|
183
|
+
for (const name of requiredCalcs) {
|
|
184
|
+
filteredManifestMap.set(name, manifestMap.get(name));
|
|
185
|
+
filteredInDegree.set(name, inDegree.get(name));
|
|
186
|
+
const consumers = (reverseAdjacency.get(name) || []).filter(consumer => requiredCalcs.has(consumer));
|
|
187
|
+
filteredReverseAdjacency.set(name, consumers);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const sortedManifest = [];
|
|
191
|
+
const queue = [];
|
|
192
|
+
let maxPass = 0;
|
|
193
|
+
|
|
194
|
+
for (const [name, degree] of filteredInDegree) {
|
|
195
|
+
if (degree === 0) { queue.push(name); filteredManifestMap.get(name).pass = 1; maxPass = 1; }
|
|
196
|
+
}
|
|
197
|
+
queue.sort();
|
|
198
|
+
|
|
199
|
+
while (queue.length) {
|
|
200
|
+
const currentName = queue.shift();
|
|
201
|
+
const currentEntry = filteredManifestMap.get(currentName);
|
|
202
|
+
sortedManifest.push(currentEntry);
|
|
203
|
+
|
|
204
|
+
for (const neighborName of (filteredReverseAdjacency.get(currentName) || [])) {
|
|
205
|
+
const newDegree = filteredInDegree.get(neighborName) - 1;
|
|
206
|
+
filteredInDegree.set(neighborName, newDegree);
|
|
207
|
+
const neighborEntry = filteredManifestMap.get(neighborName);
|
|
208
|
+
if (neighborEntry.pass <= currentEntry.pass) {
|
|
209
|
+
neighborEntry.pass = currentEntry.pass + 1;
|
|
210
|
+
if (neighborEntry.pass > maxPass) maxPass = neighborEntry.pass;
|
|
211
|
+
}
|
|
212
|
+
if (newDegree === 0) queue.push(neighborName);
|
|
213
|
+
}
|
|
214
|
+
queue.sort();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (sortedManifest.length !== filteredManifestMap.size) throw new Error('Circular dependency detected.');
|
|
218
|
+
|
|
219
|
+
// --- Cascading Hash (Phase 2) ---
|
|
220
|
+
for (const entry of sortedManifest) {
|
|
221
|
+
let dependencySignature = entry.hash;
|
|
222
|
+
if (entry.dependencies && entry.dependencies.length > 0) {
|
|
223
|
+
const depHashes = entry.dependencies.map(depName => {
|
|
224
|
+
const depEntry = filteredManifestMap.get(depName);
|
|
225
|
+
return depEntry ? depEntry.hash : '';
|
|
226
|
+
}).join('|');
|
|
227
|
+
dependencySignature += `|DEPS:${depHashes}`;
|
|
228
|
+
}
|
|
229
|
+
entry.hash = generateCodeHash(dependencySignature);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return sortedManifest;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function build(productLinesToRun, calculations) {
|
|
236
|
+
try { return buildManifest(productLinesToRun, calculations); }
|
|
237
|
+
catch (error) { log.error(error.message); return null; }
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
module.exports = { build };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FIXED: computation_controller.js
|
|
3
|
-
* V5.
|
|
3
|
+
* V5.1: Exports LEGACY_MAPPING for Manifest Builder
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// Load all layers dynamically from the index
|
|
@@ -21,7 +21,13 @@ const LEGACY_MAPPING = {
|
|
|
21
21
|
TimeSeries: 'TimeSeries',
|
|
22
22
|
priceExtractor: 'priceExtractor',
|
|
23
23
|
InsightsExtractor: 'insights',
|
|
24
|
-
UserClassifier: 'classifier'
|
|
24
|
+
UserClassifier: 'classifier',
|
|
25
|
+
// Added based on your profiling.js file:
|
|
26
|
+
Psychometrics: 'psychometrics',
|
|
27
|
+
CognitiveBiases: 'bias',
|
|
28
|
+
SkillAttribution: 'skill',
|
|
29
|
+
ExecutionAnalytics: 'execution',
|
|
30
|
+
AdaptiveAnalytics: 'adaptive'
|
|
25
31
|
};
|
|
26
32
|
|
|
27
33
|
class DataLoader {
|
|
@@ -41,7 +47,7 @@ class ContextBuilder {
|
|
|
41
47
|
for (const [key, value] of Object.entries(mathLayer)) { mathContext[key] = value; const legacyKey = LEGACY_MAPPING[key]; if (legacyKey) { mathContext[legacyKey] = value; } }
|
|
42
48
|
return mathContext;
|
|
43
49
|
}
|
|
44
|
-
|
|
50
|
+
// ... (rest of class remains identical)
|
|
45
51
|
static buildPerUserContext(options) {
|
|
46
52
|
const { todayPortfolio, yesterdayPortfolio, todayHistory, yesterdayHistory, userId, userType, dateStr, metadata, mappings, insights, socialData, computedDependencies, previousComputedDependencies, config, deps } = options;
|
|
47
53
|
return {
|
|
@@ -74,6 +80,7 @@ class ContextBuilder {
|
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
class ComputationExecutor {
|
|
83
|
+
// ... (remains identical to previous version)
|
|
77
84
|
constructor(config, dependencies, dataLoader) {
|
|
78
85
|
this.config = config;
|
|
79
86
|
this.deps = dependencies;
|
|
@@ -139,4 +146,5 @@ class ComputationController {
|
|
|
139
146
|
}
|
|
140
147
|
}
|
|
141
148
|
|
|
142
|
-
|
|
149
|
+
// EXPORT LEGACY_MAPPING SO BUILDER CAN USE IT
|
|
150
|
+
module.exports = { ComputationController, LEGACY_MAPPING };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Checks availability of root data (Portfolios, Prices, etc).
|
|
3
|
+
*/
|
|
4
|
+
const {
|
|
5
|
+
getPortfolioPartRefs,
|
|
6
|
+
loadDailyInsights,
|
|
7
|
+
loadDailySocialPostInsights,
|
|
8
|
+
getHistoryPartRefs
|
|
9
|
+
} = require('../utils/data_loader');
|
|
10
|
+
|
|
11
|
+
function checkRootDependencies(calcManifest, rootDataStatus) {
|
|
12
|
+
const missing = [];
|
|
13
|
+
if (!calcManifest.rootDataDependencies) return { canRun: true, missing };
|
|
14
|
+
for (const dep of calcManifest.rootDataDependencies) {
|
|
15
|
+
if (dep === 'portfolio' && !rootDataStatus.hasPortfolio) missing.push('portfolio');
|
|
16
|
+
else if (dep === 'insights' && !rootDataStatus.hasInsights) missing.push('insights');
|
|
17
|
+
else if (dep === 'social' && !rootDataStatus.hasSocial) missing.push('social');
|
|
18
|
+
else if (dep === 'history' && !rootDataStatus.hasHistory) missing.push('history');
|
|
19
|
+
else if (dep === 'price' && !rootDataStatus.hasPrices) missing.push('price');
|
|
20
|
+
}
|
|
21
|
+
return { canRun: missing.length === 0, missing };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function checkRootDataAvailability(dateStr, config, dependencies, earliestDates) {
|
|
25
|
+
const { logger } = dependencies;
|
|
26
|
+
const dateToProcess = new Date(dateStr + 'T00:00:00Z');
|
|
27
|
+
let portfolioRefs = [], historyRefs = [];
|
|
28
|
+
let hasPortfolio = false, hasInsights = false, hasSocial = false, hasHistory = false, hasPrices = false;
|
|
29
|
+
let insightsData = null, socialData = null;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const tasks = [];
|
|
33
|
+
if (dateToProcess >= earliestDates.portfolio) {
|
|
34
|
+
tasks.push(getPortfolioPartRefs(config, dependencies, dateStr).then(r => { portfolioRefs = r; hasPortfolio = !!r.length; }));
|
|
35
|
+
}
|
|
36
|
+
if (dateToProcess >= earliestDates.insights) {
|
|
37
|
+
tasks.push(loadDailyInsights(config, dependencies, dateStr).then(r => { insightsData = r; hasInsights = !!r; }));
|
|
38
|
+
}
|
|
39
|
+
if (dateToProcess >= earliestDates.social) {
|
|
40
|
+
tasks.push(loadDailySocialPostInsights(config, dependencies, dateStr).then(r => { socialData = r; hasSocial = !!r; }));
|
|
41
|
+
}
|
|
42
|
+
if (dateToProcess >= earliestDates.history) {
|
|
43
|
+
tasks.push(getHistoryPartRefs(config, dependencies, dateStr).then(r => { historyRefs = r; hasHistory = !!r.length; }));
|
|
44
|
+
}
|
|
45
|
+
if (dateToProcess >= earliestDates.price) {
|
|
46
|
+
tasks.push(checkPriceDataAvailability(config, dependencies).then(r => { hasPrices = r; }));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
await Promise.all(tasks);
|
|
50
|
+
|
|
51
|
+
if (!(hasPortfolio || hasInsights || hasSocial || hasHistory || hasPrices)) return null;
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
portfolioRefs,
|
|
55
|
+
historyRefs,
|
|
56
|
+
todayInsights: insightsData,
|
|
57
|
+
todaySocialPostInsights: socialData,
|
|
58
|
+
status: { hasPortfolio, hasInsights, hasSocial, hasHistory, hasPrices },
|
|
59
|
+
yesterdayPortfolioRefs: null // Filled later if needed
|
|
60
|
+
};
|
|
61
|
+
} catch (err) {
|
|
62
|
+
logger.log('ERROR', `Error checking data: ${err.message}`);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function checkPriceDataAvailability(config, { db }) {
|
|
68
|
+
try {
|
|
69
|
+
const collection = config.priceCollection || 'asset_prices';
|
|
70
|
+
const snapshot = await db.collection(collection).limit(1).get();
|
|
71
|
+
return !snapshot.empty;
|
|
72
|
+
} catch (e) { return false; }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = { checkRootDependencies, checkRootDataAvailability };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Execution-scoped data loader with caching.
|
|
3
|
+
*/
|
|
4
|
+
const {
|
|
5
|
+
loadDailyInsights,
|
|
6
|
+
loadDailySocialPostInsights,
|
|
7
|
+
getRelevantShardRefs,
|
|
8
|
+
getPriceShardRefs
|
|
9
|
+
} = require('../utils/data_loader');
|
|
10
|
+
|
|
11
|
+
class CachedDataLoader {
|
|
12
|
+
constructor(config, dependencies) {
|
|
13
|
+
this.config = config;
|
|
14
|
+
this.deps = dependencies;
|
|
15
|
+
this.cache = {
|
|
16
|
+
mappings: null,
|
|
17
|
+
insights: new Map(),
|
|
18
|
+
social: new Map()
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async loadMappings() {
|
|
23
|
+
if (this.cache.mappings) return this.cache.mappings;
|
|
24
|
+
const { calculationUtils } = this.deps;
|
|
25
|
+
this.cache.mappings = await calculationUtils.loadInstrumentMappings();
|
|
26
|
+
return this.cache.mappings;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async loadInsights(dateStr) {
|
|
30
|
+
if (this.cache.insights.has(dateStr)) return this.cache.insights.get(dateStr);
|
|
31
|
+
const insights = await loadDailyInsights(this.config, this.deps, dateStr);
|
|
32
|
+
this.cache.insights.set(dateStr, insights);
|
|
33
|
+
return insights;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async loadSocial(dateStr) {
|
|
37
|
+
if (this.cache.social.has(dateStr)) return this.cache.social.get(dateStr);
|
|
38
|
+
const social = await loadDailySocialPostInsights(this.config, this.deps, dateStr);
|
|
39
|
+
this.cache.social.set(dateStr, social);
|
|
40
|
+
return social;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async getPriceShardReferences() {
|
|
44
|
+
return getPriceShardRefs(this.config, this.deps);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async getSpecificPriceShardReferences(targetInstrumentIds) {
|
|
48
|
+
return getRelevantShardRefs(this.config, this.deps, targetInstrumentIds);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async loadPriceShard(docRef) {
|
|
52
|
+
try {
|
|
53
|
+
const snap = await docRef.get();
|
|
54
|
+
if (!snap.exists) return {};
|
|
55
|
+
return snap.data();
|
|
56
|
+
} catch (e) {
|
|
57
|
+
console.error(`Error loading shard ${docRef.path}:`, e);
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { CachedDataLoader };
|