bulltrackers-module 1.0.175 → 1.0.176
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/README.MD +1901 -2
- package/functions/computation-system/controllers/computation_controller.js +1 -1
- package/functions/computation-system/helpers/computation_manifest_builder.js +9 -39
- package/functions/computation-system/helpers/computation_pass_runner.js +72 -66
- package/functions/computation-system/helpers/orchestration_helpers.js +136 -99
- package/functions/task-engine/utils/firestore_batch_manager.js +224 -180
- package/package.json +1 -1
|
@@ -171,7 +171,7 @@ class ComputationExecutor {
|
|
|
171
171
|
|
|
172
172
|
const context = ContextBuilder.buildPerUserContext({
|
|
173
173
|
todayPortfolio, yesterdayPortfolio,
|
|
174
|
-
todayHistory,
|
|
174
|
+
todayHistory,
|
|
175
175
|
userId, userType: actualUserType, dateStr, metadata, mappings, insights,
|
|
176
176
|
computedDependencies: computedDeps,
|
|
177
177
|
previousComputedDependencies: prevDeps,
|
|
@@ -17,10 +17,8 @@ process.env.TMPDIR = '/tmp';
|
|
|
17
17
|
process.env.TMP = '/tmp';
|
|
18
18
|
process.env.TEMP = '/tmp';
|
|
19
19
|
const os = require('os');
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (!fs.existsSync(tmp)) fs.mkdirSync(tmp); temp.dir = tmp; temp.path = () => path.join(tmp, 'temp-' + Math.random().toString(36).slice(2)); } catch {}
|
|
23
|
-
const Viz = require('graphviz');
|
|
20
|
+
|
|
21
|
+
const path = require('path');
|
|
24
22
|
|
|
25
23
|
/* --------------------------------------------------
|
|
26
24
|
* Pretty Console Helpers
|
|
@@ -42,7 +40,7 @@ const log = {
|
|
|
42
40
|
const normalizeName = (name) => { if (typeof name !== 'string') return name; return name.trim().replace(/,$/, '').replace(/_/g, '-').toLowerCase(); };
|
|
43
41
|
|
|
44
42
|
/**
|
|
45
|
-
* Finds the closest string matches for a typo.
|
|
43
|
+
* Finds the closest string matches for a typo. // https://medium.com/@ethannam/understanding-the-levenshtein-distance-equation-for-beginners-c4285a5604f0
|
|
46
44
|
*/
|
|
47
45
|
function suggestClosest(name, candidates, n = 3) {
|
|
48
46
|
const levenshtein = (a = '', b = '') => {
|
|
@@ -125,7 +123,7 @@ function buildManifest(productLinesToRun = [], calculations) {
|
|
|
125
123
|
const metadata = Class.getMetadata();
|
|
126
124
|
const dependencies = Class.getDependencies().map(normalizeName);
|
|
127
125
|
// --- RULE 4: Check for isHistorical mismatch ---
|
|
128
|
-
if (metadata.isHistorical === true && !Class.toString().includes('yesterday')) { // UPDATED FOR MATH LAYER, THIS IS A LITTLE BRITTLE, BUT FOR NOW FINE...
|
|
126
|
+
if (metadata.isHistorical === true && !Class.toString().includes('yesterday')) { // UPDATED FOR MATH LAYER, TODO THIS IS A LITTLE BRITTLE, BUT FOR NOW FINE...
|
|
129
127
|
log.warn(`Calculation "${normalizedName}" is marked 'isHistorical: true' but does not seem to reference 'yesterday' data.`);
|
|
130
128
|
}
|
|
131
129
|
const manifestEntry = {
|
|
@@ -144,8 +142,6 @@ function buildManifest(productLinesToRun = [], calculations) {
|
|
|
144
142
|
inDegree.set(normalizedName, dependencies.length);
|
|
145
143
|
dependencies.forEach(dep => { if (!reverseAdjacency.has(dep)) reverseAdjacency.set(dep, []); reverseAdjacency.get(dep).push(normalizedName); }); }
|
|
146
144
|
|
|
147
|
-
// --- UPDATED ---
|
|
148
|
-
// This 'calculations' object is now the one passed in as an argument.
|
|
149
145
|
if (!calculations || typeof calculations !== 'object') {
|
|
150
146
|
log.fatal('Calculations object was not provided or is invalid.');
|
|
151
147
|
throw new Error('Manifest build failed: Invalid calculations object.');
|
|
@@ -185,14 +181,18 @@ function buildManifest(productLinesToRun = [], calculations) {
|
|
|
185
181
|
/* ---------------- 3. Filter for Product Lines ---------------- */
|
|
186
182
|
log.divider('Filtering by Product Line');
|
|
187
183
|
// 1. Find all "endpoint" calculations (the final signals) in the target product lines.
|
|
184
|
+
|
|
188
185
|
const productLineEndpoints = [];
|
|
189
186
|
for (const [name, entry] of manifestMap.entries()) { if (productLinesToRun.includes(entry.category)) { productLineEndpoints.push(name); } }
|
|
187
|
+
|
|
190
188
|
// 2. Add 'core' calculations as they are always included.
|
|
191
189
|
for (const [name, entry] of manifestMap.entries()) { if (entry.category === 'core') { productLineEndpoints.push(name); } }
|
|
190
|
+
|
|
192
191
|
// 3. Trace all dependencies upwards from these endpoints.
|
|
193
192
|
const requiredCalcs = getDependencySet(productLineEndpoints, adjacency);
|
|
194
193
|
log.info(`Identified ${productLineEndpoints.length} endpoint/core calculations.`);
|
|
195
194
|
log.info(`Traced dependencies: ${requiredCalcs.size} total calculations are required.`);
|
|
195
|
+
|
|
196
196
|
// 4. Create the final, filtered maps for sorting.
|
|
197
197
|
const filteredManifestMap = new Map();
|
|
198
198
|
const filteredInDegree = new Map();
|
|
@@ -233,37 +233,7 @@ function buildManifest(productLinesToRun = [], calculations) {
|
|
|
233
233
|
return sortedManifest;
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
-
/**
|
|
237
|
-
* Generates an SVG dependency graph for a given manifest.
|
|
238
|
-
* @param {object[]} manifest - The sorted manifest array.
|
|
239
|
-
* @param {string} filename - The output filename (e.g., "full-dependency-tree.svg").
|
|
240
|
-
*/
|
|
241
|
-
async function generateSvgGraph(manifest, filename = 'dependency-tree.svg') {
|
|
242
|
-
log.divider(`Generating SVG Graph: ${filename}`);
|
|
243
|
-
const viz = new Viz({ worker: false });
|
|
244
|
-
const categories = [...new Set(manifest.map(e => e.category))];
|
|
245
|
-
const colors = ['#e6194b', '#3cb44b', '#ffe119', '#4363d8', '#f58231', '#911eb4', '#46f0f0', '#f032e6', '#bcf60c', '#fabebe', '#008080', '#e6beff', '#9a6324', '#fffac8', '#800000', '#aaffc3', '#808000', '#ffd8b1', '#000075', '#808080'];
|
|
246
|
-
const colorMap = new Map(categories.map((cat, i) => [cat, colors[i % colors.length]]));
|
|
247
|
-
colorMap.set('core', '#a9a9a9');
|
|
248
|
-
let dot = `digraph Manifest {\n rankdir=LR;\n node [shape=box, style=filled, fontname="Helvetica"];\n layout=dot;\n overlap=false;\n splines=true;\n`;
|
|
249
|
-
for (const category of categories) {
|
|
250
|
-
if (category === 'core') continue;
|
|
251
|
-
dot += ` subgraph "cluster_${category}" {\n label="${category.toUpperCase()} Product Line";\n style=filled;\n color="#f0f0f0";\n`;
|
|
252
|
-
for (const entry of manifest.filter(e => e.category === category)) { dot += ` "${entry.name}" [label="${entry.name}\\n(Pass ${entry.pass})", fillcolor="${colorMap.get(category)}"];\n`; }
|
|
253
|
-
dot += ` }\n`; }
|
|
254
|
-
dot += ` subgraph "cluster_core" {\n label="CORE";\n style=filled;\n color="#e0e0e0";\n`;
|
|
255
|
-
for (const entry of manifest.filter(e => e.category === 'core')) { dot += ` "${entry.name}" [label="${entry.name}\\n(Pass ${entry.pass})", fillcolor="${colorMap.get('core')}"];\n`; }
|
|
256
|
-
dot += ` }\n`;
|
|
257
|
-
for (const entry of manifest) { for (const dep of entry.dependencies || []) { dot += ` "${dep}" -> "${entry.name}";\n`; } }
|
|
258
|
-
dot += '}\n';
|
|
259
|
-
try {
|
|
260
|
-
const svg = await viz.renderString(dot, { format: 'svg' });
|
|
261
|
-
const out = path.join('/tmp', filename);
|
|
262
|
-
fs.writeFileSync(out, svg);
|
|
263
|
-
log.success(`Dependency tree generated at ${out}`);
|
|
264
236
|
|
|
265
|
-
} catch (e) { log.error(`SVG generation failed: ${e.message}`); }
|
|
266
|
-
}
|
|
267
237
|
|
|
268
238
|
|
|
269
239
|
/**
|
|
@@ -282,4 +252,4 @@ function build(productLinesToRun, calculations) {
|
|
|
282
252
|
}
|
|
283
253
|
|
|
284
254
|
|
|
285
|
-
module.exports = { build
|
|
255
|
+
module.exports = { build};
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* V3.4: Optimized Orchestration using Status Document (Single Source of Truth).
|
|
4
|
-
* - Reads one 'computation_status' doc per day to decide what to run.
|
|
5
|
-
* - Reduces Firestore reads significantly.
|
|
6
|
-
* - explicitly marks failures as false in the status doc.
|
|
2
|
+
* FILENAME: bulltrackers-module/functions/computation-system/helpers/computation_pass_runner.js
|
|
7
3
|
*/
|
|
8
4
|
|
|
9
5
|
const {
|
|
10
6
|
groupByPass,
|
|
11
7
|
checkRootDataAvailability,
|
|
12
8
|
fetchExistingResults,
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
fetchGlobalComputationStatus, // <--- New Import
|
|
10
|
+
updateGlobalComputationStatus, // <--- New Import
|
|
15
11
|
runStandardComputationPass,
|
|
16
12
|
runMetaComputationPass,
|
|
17
13
|
checkRootDependencies
|
|
@@ -25,113 +21,123 @@ async function runComputationPass(config, dependencies, computationManifest) {
|
|
|
25
21
|
if (!passToRun)
|
|
26
22
|
return logger.log('ERROR', '[PassRunner] No pass defined. Aborting.');
|
|
27
23
|
|
|
28
|
-
logger.log('INFO', `🚀 Starting PASS ${passToRun} with
|
|
24
|
+
logger.log('INFO', `🚀 Starting PASS ${passToRun} with Global Status Check...`);
|
|
29
25
|
|
|
30
|
-
// Hardcoded earliest dates
|
|
31
|
-
const earliestDates
|
|
26
|
+
// Hardcoded earliest dates
|
|
27
|
+
const earliestDates = { portfolio: new Date('2025-09-25T00:00:00Z'), history: new Date('2025-11-05T00:00:00Z'), social: new Date('2025-10-30T00:00:00Z'), insights: new Date('2025-08-26T00:00:00Z') };
|
|
32
28
|
earliestDates.absoluteEarliest = Object.values(earliestDates).reduce((a,b) => a < b ? a : b);
|
|
33
29
|
|
|
34
|
-
const passes
|
|
30
|
+
const passes = groupByPass(computationManifest);
|
|
35
31
|
const calcsInThisPass = passes[passToRun] || [];
|
|
36
32
|
|
|
37
33
|
if (!calcsInThisPass.length)
|
|
38
34
|
return logger.log('WARN', `[PassRunner] No calcs for Pass ${passToRun}. Exiting.`);
|
|
39
35
|
|
|
40
|
-
// Determine date range to process
|
|
41
|
-
// We no longer need complex per-calc date derivation since the status doc handles "done-ness".
|
|
42
|
-
// We just iterate from absolute earliest to yesterday.
|
|
43
36
|
const passEarliestDate = earliestDates.absoluteEarliest;
|
|
44
|
-
const endDateUTC
|
|
37
|
+
const endDateUTC = new Date(Date.UTC(new Date().getUTCFullYear(), new Date().getUTCMonth(), new Date().getUTCDate() - 1));
|
|
45
38
|
const allExpectedDates = getExpectedDateStrings(passEarliestDate, endDateUTC);
|
|
46
39
|
|
|
47
|
-
const standardCalcs
|
|
48
|
-
const metaCalcs
|
|
40
|
+
const standardCalcs = calcsInThisPass.filter(c => c.type === 'standard');
|
|
41
|
+
const metaCalcs = calcsInThisPass.filter(c => c.type === 'meta');
|
|
49
42
|
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (statusData[calc.name] === true) return false;
|
|
43
|
+
// 1. Fetch Global Status ONCE (Memory Cache)
|
|
44
|
+
// Returns { "2023-10-27": { calcA: true, calcB: false }, ... }
|
|
45
|
+
const globalStatusData = await fetchGlobalComputationStatus(config, dependencies);
|
|
54
46
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
47
|
+
// Helper: Check status using in-memory data
|
|
48
|
+
const shouldRun = (calc, dateStr) => {
|
|
49
|
+
const dailyStatus = globalStatusData[dateStr] || {};
|
|
50
|
+
|
|
51
|
+
// 1. If explicitly TRUE, ignore.
|
|
52
|
+
if (dailyStatus[calc.name] === true) return false;
|
|
53
|
+
|
|
54
|
+
// 2. Check dependencies (using same in-memory status)
|
|
58
55
|
if (calc.dependencies && calc.dependencies.length > 0) {
|
|
59
|
-
const depsMet = calc.dependencies.every(depName =>
|
|
60
|
-
if (!depsMet) return false;
|
|
56
|
+
const depsMet = calc.dependencies.every(depName => dailyStatus[depName] === true);
|
|
57
|
+
if (!depsMet) return false;
|
|
61
58
|
}
|
|
62
|
-
|
|
63
59
|
return true;
|
|
64
60
|
};
|
|
65
61
|
|
|
62
|
+
// Process a single date and RETURN updates (do not write)
|
|
66
63
|
const processDate = async (dateStr) => {
|
|
67
|
-
// 1. Fetch the Single Status Document (Cheap Read)
|
|
68
|
-
const statusData = await fetchComputationStatus(dateStr, config, dependencies);
|
|
69
64
|
const dateToProcess = new Date(dateStr + 'T00:00:00Z');
|
|
70
65
|
|
|
71
|
-
//
|
|
72
|
-
const standardToRun = standardCalcs.filter(c => shouldRun(c,
|
|
73
|
-
const metaToRun = metaCalcs.filter(c => shouldRun(c,
|
|
66
|
+
// Filter using in-memory status
|
|
67
|
+
const standardToRun = standardCalcs.filter(c => shouldRun(c, dateStr));
|
|
68
|
+
const metaToRun = metaCalcs.filter(c => shouldRun(c, dateStr));
|
|
74
69
|
|
|
75
|
-
|
|
76
|
-
// No checking root data, no loading results.
|
|
77
|
-
if (!standardToRun.length && !metaToRun.length) {
|
|
78
|
-
return; // logger.log('INFO', `[PassRunner] ${dateStr} complete or waiting for deps.`);
|
|
79
|
-
}
|
|
70
|
+
if (!standardToRun.length && !metaToRun.length) return null; // No work
|
|
80
71
|
|
|
81
|
-
// 3. Check Root Data Availability (Only done if we have work to do)
|
|
82
|
-
// We filter standardToRun further based on root data requirements
|
|
83
|
-
// (e.g. if calc needs history but history isn't ready for this date)
|
|
84
72
|
const rootData = await checkRootDataAvailability(dateStr, config, dependencies, earliestDates);
|
|
85
|
-
|
|
86
|
-
if (!rootData) {
|
|
87
|
-
// If root data is completely missing for the day, we can't run anything.
|
|
88
|
-
// We do NOT mark as false, because it's not a failure, just data unavailability.
|
|
89
|
-
// We just skip.
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
73
|
+
if (!rootData) return null;
|
|
92
74
|
|
|
93
|
-
// Further filter based on specific root data needs (e.g. checks "hasHistory")
|
|
94
75
|
const finalStandardToRun = standardToRun.filter(c => checkRootDependencies(c, rootData.status).canRun);
|
|
95
76
|
const finalMetaToRun = metaToRun.filter(c => checkRootDependencies(c, rootData.status).canRun);
|
|
96
77
|
|
|
97
|
-
if (!finalStandardToRun.length && !finalMetaToRun.length) return;
|
|
78
|
+
if (!finalStandardToRun.length && !finalMetaToRun.length) return null;
|
|
79
|
+
|
|
80
|
+
logger.log('INFO', `[PassRunner] Running ${dateStr}: ${finalStandardToRun.length} std, ${finalMetaToRun.length} meta`);
|
|
98
81
|
|
|
99
|
-
|
|
82
|
+
const dateUpdates = {}; // { calcName: true/false }
|
|
100
83
|
|
|
101
84
|
try {
|
|
102
|
-
// 4. Fetch Data for Execution (Dependencies)
|
|
103
|
-
// We only fetch data required by the calcs that are actually running.
|
|
104
85
|
const calcsRunning = [...finalStandardToRun, ...finalMetaToRun];
|
|
105
86
|
const existingResults = await fetchExistingResults(dateStr, calcsRunning, computationManifest, config, dependencies, false);
|
|
106
87
|
|
|
107
|
-
|
|
108
|
-
const prevDate = new Date(dateToProcess); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
|
|
88
|
+
const prevDate = new Date(dateToProcess); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
|
|
109
89
|
const prevDateStr = prevDate.toISOString().slice(0, 10);
|
|
110
90
|
const previousResults = await fetchExistingResults(prevDateStr, calcsRunning, computationManifest, config, dependencies, true);
|
|
111
91
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
92
|
+
if (finalStandardToRun.length) {
|
|
93
|
+
const updates = await runStandardComputationPass(dateToProcess, finalStandardToRun, `Pass ${passToRun} (Std)`, config, dependencies, rootData, existingResults, previousResults, true); // skipStatusWrite=true
|
|
94
|
+
Object.assign(dateUpdates, updates);
|
|
95
|
+
}
|
|
115
96
|
|
|
116
|
-
if (finalMetaToRun.length)
|
|
117
|
-
await runMetaComputationPass(dateToProcess, finalMetaToRun, `Pass ${passToRun} (Meta)`, config, dependencies, existingResults, previousResults, rootData);
|
|
97
|
+
if (finalMetaToRun.length) {
|
|
98
|
+
const updates = await runMetaComputationPass(dateToProcess, finalMetaToRun, `Pass ${passToRun} (Meta)`, config, dependencies, existingResults, previousResults, rootData, true); // skipStatusWrite=true
|
|
99
|
+
Object.assign(dateUpdates, updates);
|
|
100
|
+
}
|
|
118
101
|
|
|
119
102
|
} catch (err) {
|
|
120
103
|
logger.log('ERROR', `[PassRunner] FAILED Pass ${passToRun} for ${dateStr}`, { errorMessage: err.message });
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const failedUpdates = {};
|
|
124
|
-
[...finalStandardToRun, ...finalMetaToRun].forEach(c => failedUpdates[c.name] = false);
|
|
125
|
-
await updateComputationStatus(dateStr, failedUpdates, config, dependencies);
|
|
104
|
+
// Mark failures
|
|
105
|
+
[...finalStandardToRun, ...finalMetaToRun].forEach(c => dateUpdates[c.name] = false);
|
|
126
106
|
}
|
|
107
|
+
|
|
108
|
+
// Return the updates for this date
|
|
109
|
+
return { date: dateStr, updates: dateUpdates };
|
|
127
110
|
};
|
|
128
111
|
|
|
129
112
|
// Batch process dates
|
|
130
113
|
for (let i = 0; i < allExpectedDates.length; i += PARALLEL_BATCH_SIZE) {
|
|
131
114
|
const batch = allExpectedDates.slice(i, i + PARALLEL_BATCH_SIZE);
|
|
132
|
-
|
|
115
|
+
|
|
116
|
+
// Run batch in parallel
|
|
117
|
+
const results = await Promise.all(batch.map(processDate));
|
|
118
|
+
|
|
119
|
+
// Aggregate updates from the batch
|
|
120
|
+
const batchUpdates = {};
|
|
121
|
+
let hasUpdates = false;
|
|
122
|
+
|
|
123
|
+
results.forEach(res => {
|
|
124
|
+
if (res && res.updates && Object.keys(res.updates).length > 0) {
|
|
125
|
+
batchUpdates[res.date] = res.updates;
|
|
126
|
+
hasUpdates = true;
|
|
127
|
+
|
|
128
|
+
// Also update our local in-memory copy so subsequent logic in this run sees it (though passes usually rely on prev days)
|
|
129
|
+
if (!globalStatusData[res.date]) globalStatusData[res.date] = {};
|
|
130
|
+
Object.assign(globalStatusData[res.date], res.updates);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Write status ONCE per batch
|
|
135
|
+
if (hasUpdates) {
|
|
136
|
+
await updateGlobalComputationStatus(batchUpdates, config, dependencies);
|
|
137
|
+
logger.log('INFO', `[PassRunner] Batched status update for ${Object.keys(batchUpdates).length} dates.`);
|
|
138
|
+
}
|
|
133
139
|
}
|
|
134
140
|
logger.log('INFO', `[PassRunner] Pass ${passToRun} orchestration finished.`);
|
|
135
141
|
}
|
|
136
142
|
|
|
137
|
-
module.exports = { runComputationPass };
|
|
143
|
+
module.exports = { runComputationPass };
|