bulltrackers-module 1.0.157 → 1.0.159
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.
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview
|
|
3
|
+
* Dynamic Manifest Builder (v3 - Refactored)
|
|
4
|
+
*
|
|
5
|
+
* This script builds the computation manifest dynamically by introspecting all
|
|
6
|
+
* calculation classes from the new product-line structure (core, gem, gauss, etc.).
|
|
7
|
+
* It validates metadata, resolves dependencies, and performs a topological sort
|
|
8
|
+
* to determine execution passes.
|
|
9
|
+
*
|
|
10
|
+
* It explicitly skips the 'legacy' package.
|
|
11
|
+
* It has removed all logic for deprecated folder structures (e.g., /historical/).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { calculations } = require('aiden-shared-calculations-unified');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const Viz = require('viz.js');
|
|
18
|
+
const { Module, render } = require('viz.js/full.render.js');
|
|
19
|
+
|
|
20
|
+
/* --------------------------------------------------
|
|
21
|
+
* Pretty Console Helpers
|
|
22
|
+
* -------------------------------------------------- */
|
|
23
|
+
const log = {
|
|
24
|
+
info: (msg) => console.log('ℹ︎ ' + msg),
|
|
25
|
+
step: (msg) => console.log('› ' + msg),
|
|
26
|
+
warn: (msg) => console.warn('⚠︎ ' + msg),
|
|
27
|
+
success: (msg) => console.log('✔︎ ' + msg),
|
|
28
|
+
error: (msg) => console.error('✖ ' + msg),
|
|
29
|
+
fatal: (msg) => { console.error('✖ FATAL ✖ ' + msg); console.error('✖ FATAL ✖ Manifest build FAILED.'); },
|
|
30
|
+
divider: (label) => { const line = ''.padEnd(60, '─'); console.log(`\n${line}\n${label}\n${line}\n`); },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/* --------------------------------------------------
|
|
34
|
+
* Helper Utilities
|
|
35
|
+
* -------------------------------------------------- */
|
|
36
|
+
|
|
37
|
+
const normalizeName = (name) => { if (typeof name !== 'string') return name; return name.trim().replace(/,$/, '').replace(/_/g, '-').toLowerCase(); };
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Finds the closest string matches for a typo.
|
|
41
|
+
*/
|
|
42
|
+
function suggestClosest(name, candidates, n = 3) {
|
|
43
|
+
const levenshtein = (a = '', b = '') => {
|
|
44
|
+
const m = a.length, n = b.length;
|
|
45
|
+
if (!m) return n; if (!n) return m;
|
|
46
|
+
const dp = Array.from({ length: m + 1 }, (_, i) => Array(n + 1).fill(i));
|
|
47
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
48
|
+
for (let i = 1; i <= m; i++)
|
|
49
|
+
for (let j = 1; j <= n; j++)
|
|
50
|
+
dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : Math.min(dp[i - 1][j - 1], dp[i][j - 1], dp[i - 1][j]) + 1;
|
|
51
|
+
return dp[m][n];
|
|
52
|
+
};
|
|
53
|
+
const scores = candidates.map(c => [c, levenshtein(name, c)]);
|
|
54
|
+
scores.sort((a, b) => a[1] - b[1]);
|
|
55
|
+
return scores.slice(0, n).map(s => s[0]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Checks for a cycle using Depth First Search.
|
|
60
|
+
*/
|
|
61
|
+
function findCycles(manifestMap, adjacencyList) {
|
|
62
|
+
const visited = new Set(), stack = new Set(), cycles = [];
|
|
63
|
+
const dfs = (node, path) => {
|
|
64
|
+
if (stack.has(node)) { const idx = path.indexOf(node); cycles.push([...path.slice(idx), node]); return; }
|
|
65
|
+
if (visited.has(node)) return;
|
|
66
|
+
visited.add(node); stack.add(node);
|
|
67
|
+
for (const nb of adjacencyList.get(node) || []) dfs(nb, [...path, nb]);
|
|
68
|
+
stack.delete(node); };
|
|
69
|
+
for (const name of manifestMap.keys()) dfs(name, [name]);
|
|
70
|
+
return cycles;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Recursively traces all dependencies for a given set of "endpoint" calcs.
|
|
75
|
+
*/
|
|
76
|
+
function getDependencySet(endpoints, adjacencyList) {
|
|
77
|
+
const required = new Set(endpoints);
|
|
78
|
+
const queue = [...endpoints];
|
|
79
|
+
while (queue.length > 0) { const calcName = queue.shift(); const dependencies = adjacencyList.get(calcName);
|
|
80
|
+
if (dependencies) { for (const dep of dependencies) { if (!required.has(dep)) { required.add(dep); queue.push(dep); } } } }
|
|
81
|
+
return required;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* --------------------------------------------------
|
|
85
|
+
* Core Manifest Builder
|
|
86
|
+
* -------------------------------------------------- */
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Builds the full computation manifest.
|
|
90
|
+
* @param {string[]} productLinesToRun - Array of product line categories (folder names) to build for.
|
|
91
|
+
* @returns {object[]} The final sorted manifest array.
|
|
92
|
+
*/
|
|
93
|
+
function buildManifest(productLinesToRun = []) {
|
|
94
|
+
log.divider('Building Dynamic Manifest');
|
|
95
|
+
log.info(`Target Product Lines: [${productLinesToRun.join(', ')}]`);
|
|
96
|
+
const manifestMap = new Map();
|
|
97
|
+
const adjacency = new Map();
|
|
98
|
+
const reverseAdjacency = new Map();
|
|
99
|
+
const inDegree = new Map();
|
|
100
|
+
let hasFatalError = false;
|
|
101
|
+
/* ---------------- 1. Load All Calculations ---------------- */
|
|
102
|
+
log.step('Loading and validating all calculation classes…');
|
|
103
|
+
const allCalculationClasses = new Map();
|
|
104
|
+
/**
|
|
105
|
+
* Processes a single calculation class, validates it, and adds to maps.
|
|
106
|
+
* --- REFACTOR: Simplified signature, isHistorical is now read from metadata ---
|
|
107
|
+
*/
|
|
108
|
+
function processCalc(Class, name, category) {
|
|
109
|
+
if (!Class || typeof Class !== 'function') return;
|
|
110
|
+
const normalizedName = normalizeName(name);
|
|
111
|
+
allCalculationClasses.set(normalizedName, Class);
|
|
112
|
+
// --- RULE 1: Check for static getMetadata() ---
|
|
113
|
+
if (typeof Class.getMetadata !== 'function') { log.fatal(`Calculation "${normalizedName}" is missing the static getMetadata() method. Build FAILED.`); hasFatalError = true; return; }
|
|
114
|
+
// --- RULE 2: Check for static getDependencies() ---
|
|
115
|
+
if (typeof Class.getDependencies !== 'function') { log.fatal(`Calculation "${normalizedName}" is missing the static getDependencies() method. Build FAILED.`); hasFatalError = true;return; }
|
|
116
|
+
// --- RULE 3: Check for static getSchema() ---
|
|
117
|
+
if (typeof Class.getSchema !== 'function') {log.warn(`Calculation "${normalizedName}" is missing the static getSchema() method. (Recommended)`); }
|
|
118
|
+
const metadata = Class.getMetadata();
|
|
119
|
+
const dependencies = Class.getDependencies().map(normalizeName);
|
|
120
|
+
// --- RULE 4: Check for isHistorical mismatch ---
|
|
121
|
+
if (metadata.isHistorical === true && !Class.toString().includes('yesterdayPortfolio')) { log.warn(`Calculation "${normalizedName}" is marked 'isHistorical: true' but does not seem to use 'yesterdayPortfolio'.`); }
|
|
122
|
+
const manifestEntry = {
|
|
123
|
+
name: normalizedName,
|
|
124
|
+
class: Class,
|
|
125
|
+
category: metadata.category || category,
|
|
126
|
+
type: metadata.type,
|
|
127
|
+
isHistorical: metadata.isHistorical,
|
|
128
|
+
rootDataDependencies: metadata.rootDataDependencies || [],
|
|
129
|
+
userType: metadata.userType,
|
|
130
|
+
dependencies: dependencies,
|
|
131
|
+
pass: 0,
|
|
132
|
+
};
|
|
133
|
+
manifestMap.set(normalizedName, manifestEntry);
|
|
134
|
+
adjacency.set(normalizedName, dependencies);
|
|
135
|
+
inDegree.set(normalizedName, dependencies.length);
|
|
136
|
+
dependencies.forEach(dep => { if (!reverseAdjacency.has(dep)) reverseAdjacency.set(dep, []); reverseAdjacency.get(dep).push(normalizedName); }); }
|
|
137
|
+
for (const category in calculations) {
|
|
138
|
+
if (category === 'legacy') { log.info('Skipping "legacy" calculations package.'); continue; }
|
|
139
|
+
const group = calculations[category];
|
|
140
|
+
for (const key in group) {
|
|
141
|
+
const entry = group[key];
|
|
142
|
+
if (typeof entry === 'function') { processCalc(entry, key, category); } } }
|
|
143
|
+
|
|
144
|
+
if (hasFatalError) { throw new Error('Manifest build failed due to missing static methods in calculations.'); }
|
|
145
|
+
log.success(`Loaded and validated ${manifestMap.size} total calculations.`);
|
|
146
|
+
/* ---------------- 2. Validate Dependency Links ---------------- */
|
|
147
|
+
log.divider('Validating Dependency Links');
|
|
148
|
+
const allNames = new Set(manifestMap.keys());
|
|
149
|
+
let invalidLinks = false;
|
|
150
|
+
for (const [name, entry] of manifestMap) {
|
|
151
|
+
for (const dep of entry.dependencies) {
|
|
152
|
+
if (!allNames.has(dep)) {
|
|
153
|
+
invalidLinks = true;
|
|
154
|
+
const guesses = suggestClosest(dep, Array.from(allNames));
|
|
155
|
+
log.error(`${name} depends on unknown calculation "${dep}"`);
|
|
156
|
+
if (guesses.length) log.info(`Did you mean: ${guesses.join(', ')} ?`);
|
|
157
|
+
}
|
|
158
|
+
if (dep === name) {
|
|
159
|
+
invalidLinks = true;
|
|
160
|
+
log.error(`${name} has a circular dependency on *itself*!`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (invalidLinks) { throw new Error('Manifest validation failed. Fix missing or self-referencing dependencies.'); }
|
|
165
|
+
log.success('All dependency links are valid.');
|
|
166
|
+
/* ---------------- 3. Filter for Product Lines ---------------- */
|
|
167
|
+
log.divider('Filtering by Product Line');
|
|
168
|
+
// 1. Find all "endpoint" calculations (the final signals) in the target product lines.
|
|
169
|
+
const productLineEndpoints = [];
|
|
170
|
+
for (const [name, entry] of manifestMap.entries()) { if (productLinesToRun.includes(entry.category)) { productLineEndpoints.push(name); } }
|
|
171
|
+
// 2. Add 'core' calculations as they are always included.
|
|
172
|
+
for (const [name, entry] of manifestMap.entries()) { if (entry.category === 'core') { productLineEndpoints.push(name); } }
|
|
173
|
+
// 3. Trace all dependencies upwards from these endpoints.
|
|
174
|
+
const requiredCalcs = getDependencySet(productLineEndpoints, adjacency);
|
|
175
|
+
log.info(`Identified ${productLineEndpoints.length} endpoint/core calculations.`);
|
|
176
|
+
log.info(`Traced dependencies: ${requiredCalcs.size} total calculations are required.`);
|
|
177
|
+
// 4. Create the final, filtered maps for sorting.
|
|
178
|
+
const filteredManifestMap = new Map();
|
|
179
|
+
const filteredInDegree = new Map();
|
|
180
|
+
const filteredReverseAdjacency = new Map();
|
|
181
|
+
for (const name of requiredCalcs) { filteredManifestMap.set(name, manifestMap.get(name)); filteredInDegree.set(name, inDegree.get(name));
|
|
182
|
+
const consumers = (reverseAdjacency.get(name) || []).filter(consumer => requiredCalcs.has(consumer)); filteredReverseAdjacency.set(name, consumers); }
|
|
183
|
+
log.success(`Filtered manifest to ${filteredManifestMap.size} calculations.`);
|
|
184
|
+
/* ---------------- 4. Topological Sort (Kahn's Algorithm) ---------------- */
|
|
185
|
+
log.divider('Topological Sorting (Kahn\'s Algorithm)');
|
|
186
|
+
const sortedManifest = [];
|
|
187
|
+
const queue = [];
|
|
188
|
+
let maxPass = 0;
|
|
189
|
+
for (const [name, degree] of filteredInDegree) { if (degree === 0) { queue.push(name); filteredManifestMap.get(name).pass = 1; maxPass = 1; } }
|
|
190
|
+
queue.sort();
|
|
191
|
+
while (queue.length) {
|
|
192
|
+
const currentName = queue.shift();
|
|
193
|
+
const currentEntry = filteredManifestMap.get(currentName);
|
|
194
|
+
sortedManifest.push(currentEntry);
|
|
195
|
+
for (const neighborName of (filteredReverseAdjacency.get(currentName) || [])) { const newDegree = filteredInDegree.get(neighborName) - 1; filteredInDegree.set(neighborName, newDegree);
|
|
196
|
+
const neighborEntry = filteredManifestMap.get(neighborName);
|
|
197
|
+
if (neighborEntry.pass <= currentEntry.pass) { neighborEntry.pass = currentEntry.pass + 1; if (neighborEntry.pass > maxPass) maxPass = neighborEntry.pass; }
|
|
198
|
+
if (newDegree === 0) { queue.push(neighborName); } }
|
|
199
|
+
queue.sort(); }
|
|
200
|
+
if (sortedManifest.length !== filteredManifestMap.size) {
|
|
201
|
+
log.divider('Circular Dependency Detected');
|
|
202
|
+
const cycles = findCycles(filteredManifestMap, adjacency);
|
|
203
|
+
for (const c of cycles) log.error('Cycle: ' + c.join(' → '));
|
|
204
|
+
throw new Error('Circular dependency detected. Manifest build failed.'); }
|
|
205
|
+
/* ---------------- 5. Summary ---------------- */
|
|
206
|
+
log.divider('Manifest Summary');
|
|
207
|
+
log.success(`Total calculations in build: ${sortedManifest.length}`);
|
|
208
|
+
log.success(`Total passes required: ${maxPass}\n`);
|
|
209
|
+
const grouped = [...sortedManifest.reduce((m, e) => { if (!m.has(e.pass)) m.set(e.pass, []); m.get(e.pass).push(e); return m; }, new Map())].sort((a, b) => a[0] - b[0]);
|
|
210
|
+
for (const [pass, list] of grouped) { console.log(`Pass ${pass} (${list.length} calcs):`); console.log(' ' + list.map(l => l.name).join(', ')); }
|
|
211
|
+
log.divider('Build Complete');
|
|
212
|
+
return sortedManifest;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Generates an SVG dependency graph for a given manifest.
|
|
217
|
+
* @param {object[]} manifest - The sorted manifest array.
|
|
218
|
+
* @param {string} filename - The output filename (e.g., "full-dependency-tree.svg").
|
|
219
|
+
*/
|
|
220
|
+
async function generateSvgGraph(manifest, filename = 'dependency-tree.svg') {
|
|
221
|
+
log.divider(`Generating SVG Graph: ${filename}`);
|
|
222
|
+
const viz = new Viz({ Module, render });
|
|
223
|
+
const categories = [...new Set(manifest.map(e => e.category))];
|
|
224
|
+
const colors = ['#e6194b', '#3cb44b', '#ffe119', '#4363d8', '#f58231', '#911eb4', '#46f0f0', '#f032e6', '#bcf60c', '#fabebe', '#008080', '#e6beff', '#9a6324', '#fffac8', '#800000', '#aaffc3', '#808000', '#ffd8b1', '#000075', '#808080'];
|
|
225
|
+
const colorMap = new Map(categories.map((cat, i) => [cat, colors[i % colors.length]]));
|
|
226
|
+
colorMap.set('core', '#a9a9a9');
|
|
227
|
+
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`;
|
|
228
|
+
for (const category of categories) {
|
|
229
|
+
if (category === 'core') continue;
|
|
230
|
+
dot += ` subgraph "cluster_${category}" {\n label="${category.toUpperCase()} Product Line";\n style=filled;\n color="#f0f0f0";\n`;
|
|
231
|
+
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`; }
|
|
232
|
+
dot += ` }\n`; }
|
|
233
|
+
dot += ` subgraph "cluster_core" {\n label="CORE";\n style=filled;\n color="#e0e0e0";\n`;
|
|
234
|
+
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`; }
|
|
235
|
+
dot += ` }\n`;
|
|
236
|
+
for (const entry of manifest) { for (const dep of entry.dependencies || []) { dot += ` "${dep}" -> "${entry.name}";\n`; } }
|
|
237
|
+
dot += '}\n';
|
|
238
|
+
try {
|
|
239
|
+
const svg = await viz.renderString(dot, { format: 'svg' });
|
|
240
|
+
const out = path.join(__dirname, '..', '..', '..', '..', 'config', filename);
|
|
241
|
+
fs.writeFileSync(out, svg);
|
|
242
|
+
log.success(`Dependency tree generated at ${out}`);
|
|
243
|
+
} catch (e) { log.error(`SVG generation failed: ${e.message}`); }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Main entry point for building and exporting the manifest.
|
|
249
|
+
* @param {string[]} productLinesToRun - Array of product line categories (folder names) to build for.
|
|
250
|
+
*/
|
|
251
|
+
async function build(productLinesToRun) {
|
|
252
|
+
try {
|
|
253
|
+
const manifest = buildManifest(productLinesToRun);
|
|
254
|
+
await generateSvgGraph(manifest, `dependency-tree-filtered.svg`);
|
|
255
|
+
const allProductLines = Object.keys(calculations).filter(c => c !== 'legacy');
|
|
256
|
+
const fullManifest = buildManifest(allProductLines);
|
|
257
|
+
await generateSvgGraph(fullManifest, 'dependency-tree-full.svg');
|
|
258
|
+
return manifest;
|
|
259
|
+
} catch (error) { log.error(error.message); return null; }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
module.exports = { build };
|
|
263
|
+
|
|
264
|
+
if (require.main === module) { (async () => { log.info('Running manifest builder in local debug mode...'); const productLines = Object.keys(calculations).filter(c => c !== 'legacy');await build(productLines); })(); }
|
|
@@ -42,23 +42,81 @@ async function checkRootDataAvailability(dateStr, config, dependencies, earliest
|
|
|
42
42
|
} catch (err) { logger.log('ERROR', `[PassRunner] Error checking data for ${dateStr}`, { errorMessage: err.message }); return null; }
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
45
|
+
/**
|
|
46
|
+
* --- REFACTORED: Stage 4: Fetch ALL existing computed results for the pass AND their dependencies ---
|
|
47
|
+
* This function is the core fix for the user's problem.
|
|
48
|
+
*
|
|
49
|
+
* It now fetches results for:
|
|
50
|
+
* 1. All calculations IN THIS PASS (to allow skipping completed work).
|
|
51
|
+
* 2. All *dependencies* of calculations in this pass (to feed meta-calcs).
|
|
52
|
+
*
|
|
53
|
+
* This resolves the bug where Pass 2 would run but could not find
|
|
54
|
+
* the Pass 1 results it depended on.
|
|
48
55
|
*/
|
|
49
56
|
async function fetchExistingResults(dateStr, calcsInPass, fullManifest, config, { db, logger }) {
|
|
50
57
|
const manifestMap = new Map(fullManifest.map(c => [normalizeName(c.name), c]));
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
const calcsToFetch = new Set();
|
|
59
|
+
|
|
60
|
+
for (const calc of calcsInPass) {
|
|
61
|
+
const calcName = normalizeName(calc.name);
|
|
62
|
+
calcsToFetch.add(calcName);
|
|
63
|
+
|
|
64
|
+
if (calc.dependencies && calc.dependencies.length > 0) {
|
|
65
|
+
for (const depName of calc.dependencies) {
|
|
66
|
+
calcsToFetch.add(normalizeName(depName));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!calcsToFetch.size) {
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
logger.log('INFO', `[PassRunner] Checking for ${calcsToFetch.size} existing results and dependencies for ${dateStr}...`);
|
|
76
|
+
|
|
77
|
+
const docRefs = [];
|
|
78
|
+
const depNames = [];
|
|
79
|
+
|
|
80
|
+
// --- FIX: Iterate the Set of all calcs to fetch ---
|
|
81
|
+
for (const calcName of calcsToFetch) {
|
|
82
|
+
const calcManifest = manifestMap.get(calcName);
|
|
83
|
+
if (!calcManifest) {
|
|
84
|
+
logger.log('ERROR', `[PassRunner] Missing manifest for dependency: ${calcName}`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
docRefs.push(
|
|
89
|
+
db.collection(config.resultsCollection)
|
|
90
|
+
.doc(dateStr)
|
|
91
|
+
.collection(config.resultsSubcollection)
|
|
92
|
+
.doc(calcManifest.category || 'unknown')
|
|
93
|
+
.collection(config.computationsSubcollection)
|
|
94
|
+
.doc(calcName)
|
|
95
|
+
);
|
|
57
96
|
depNames.push(calcName);
|
|
58
97
|
}
|
|
59
|
-
|
|
98
|
+
|
|
99
|
+
const fetched = {};
|
|
100
|
+
if (docRefs.length) {
|
|
101
|
+
(await db.getAll(...docRefs)).forEach((doc, i) => {
|
|
102
|
+
fetched[depNames[i]] = doc.exists ? doc.data() : null;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Log what dependencies were found vs. not found (for debugging)
|
|
107
|
+
const foundDeps = Object.entries(fetched).filter(([, data]) => data !== null).map(([key]) => key);
|
|
108
|
+
const missingDeps = Object.entries(fetched).filter(([, data]) => data === null).map(([key]) => key);
|
|
109
|
+
if (foundDeps.length > 0) {
|
|
110
|
+
logger.log('TRACE', `[PassRunner] Found ${foundDeps.length} existing results: [${foundDeps.join(', ')}]`);
|
|
111
|
+
}
|
|
112
|
+
if (missingDeps.length > 0) {
|
|
113
|
+
logger.log('TRACE', `[PassRunner] Did not find ${missingDeps.length} results: [${missingDeps.join(', ')}]`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return fetched;
|
|
60
117
|
}
|
|
61
118
|
|
|
119
|
+
|
|
62
120
|
/**
|
|
63
121
|
* --- Stage 5: Filter calculations ---
|
|
64
122
|
* This function now implements your "even better design".
|
|
@@ -86,7 +144,20 @@ function filterCalculations(standardCalcs, metaCalcs, rootDataStatus, existingRe
|
|
|
86
144
|
if (dateToProcess < earliestRunDate) {logger.log('TRACE', `[Pass ${passToRun}] Skipping ${calc.name} for ${dateStr}. Date is before true earliest run date (${earliestRunDate.toISOString().slice(0, 10)}).`); skipped.add(calc.name); return false; }
|
|
87
145
|
const { canRun, missing: missingRoot } = checkRootDependencies(calc, rootDataStatus);
|
|
88
146
|
if (!canRun) {logger.log('INFO', `[Pass ${passToRun}] Skipping ${calc.name} for ${dateStr}. Data missing for this date: [${missingRoot.join(', ')}]`);skipped.add(calc.name); return false;}
|
|
89
|
-
|
|
147
|
+
|
|
148
|
+
// --- FIX: This check is now robust ---
|
|
149
|
+
// 'existingResults' now contains all dependencies, so this check
|
|
150
|
+
// will correctly find 'pnl_distribution_per_stock' and *not* skip.
|
|
151
|
+
if (calc.type === 'meta') {
|
|
152
|
+
const missingDeps = (calc.dependencies || [])
|
|
153
|
+
.map(normalizeName)
|
|
154
|
+
.filter(d => !existingResults[d]);
|
|
155
|
+
if (missingDeps.length > 0) {
|
|
156
|
+
logger.log('WARN', `[Pass ${passToRun} Meta] Skipping ${calc.name} for ${dateStr}. Missing computed deps: [${missingDeps.join(', ')}]`);
|
|
157
|
+
skipped.add(calc.name);
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
90
161
|
return true;
|
|
91
162
|
};
|
|
92
163
|
const standardCalcsToRun = standardCalcs.filter(filterCalc);
|
|
@@ -251,24 +322,31 @@ async function runMetaComputationPass(date, calcs, passName, config, deps, fetch
|
|
|
251
322
|
const name = normalizeName(mCalc.name), Cl = mCalc.class;
|
|
252
323
|
if (typeof Cl !== 'function') { logger.log('ERROR', `Invalid class ${name}`); failedCalcs.push(name); continue; }
|
|
253
324
|
const inst = new Cl();
|
|
254
|
-
|
|
255
|
-
if (
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
325
|
+
|
|
326
|
+
try { if (typeof inst.process !== 'function') { logger.log('ERROR', `Meta-calc ${name} is missing a 'process' method.`); failedCalcs.push(name); continue; }
|
|
327
|
+
|
|
328
|
+
const result = await Promise.resolve(inst.process(dStr, { ...deps, rootData: fullRoot }, config, fetchedDeps));
|
|
329
|
+
|
|
330
|
+
if (result && Object.keys(result).length > 0) {
|
|
331
|
+
const standardResult = {};
|
|
332
|
+
for (const key in result) {
|
|
333
|
+
if (key.startsWith('sharded_')) { const shardedData = result[key]; for (const collectionName in shardedData) {
|
|
334
|
+
if (!shardedWrites[collectionName]) shardedWrites[collectionName] = {}; Object.assign(shardedWrites[collectionName], shardedData[collectionName]); }
|
|
335
|
+
} else { standardResult[key] = result[key]; }
|
|
336
|
+
}
|
|
337
|
+
if (Object.keys(standardResult).length > 0) {
|
|
338
|
+
const docRef = deps.db.collection(config.resultsCollection).doc(dStr) .collection(config.resultsSubcollection).doc(mCalc.category) .collection(config.computationsSubcollection).doc(name);
|
|
339
|
+
standardWrites.push({ ref: docRef, data: standardResult });
|
|
340
|
+
}
|
|
341
|
+
const calcClass = mCalc.class;
|
|
342
|
+
let staticSchema = null;
|
|
343
|
+
if (calcClass && typeof calcClass.getSchema === 'function') {
|
|
344
|
+
try { staticSchema = calcClass.getSchema();
|
|
345
|
+
} catch (e) { logger.log('WARN', `[SchemaCapture] Failed to get static schema for ${name}`, { err: e.message }); }
|
|
346
|
+
} else { logger.log('TRACE', `[SchemaCapture] No static schema found for ${name}. Skipping manifest entry.`); }
|
|
347
|
+
if (staticSchema) { schemasToStore.push({ name, category: mCalc.category, schema: staticSchema, metadata: { isHistorical: mCalc.isHistorical || false, dependencies: mCalc.dependencies || [], rootDataDependencies: mCalc.rootDataDependencies || [], pass: mCalc.pass, type: 'meta' } }); }
|
|
348
|
+
success++;
|
|
349
|
+
}
|
|
272
350
|
} catch (e) { logger.log('ERROR', `Meta-calc failed ${name} for ${dStr}`, { err: e.message, stack: e.stack }); failedCalcs.push(name); } }
|
|
273
351
|
if (schemasToStore.length > 0) { batchStoreSchemas(deps, config, schemasToStore).catch(err => { logger.log('WARN', '[SchemaCapture] Non-blocking schema storage failed', { errorMessage: err.message }); }); }
|
|
274
352
|
if (standardWrites.length > 0) { await commitBatchInChunks(config, deps, standardWrites, `${passName} Meta ${dStr}`);}
|
package/index.js
CHANGED
|
@@ -29,10 +29,15 @@ const taskEngine = { handleRequest : require('./functions/task
|
|
|
29
29
|
handleVerify : require('./functions/task-engine/helpers/verify_helpers') .handleVerify ,
|
|
30
30
|
handleUpdate : require('./functions/task-engine/helpers/update_helpers') .handleUpdate };
|
|
31
31
|
|
|
32
|
+
// --- NEW IMPORT ---
|
|
33
|
+
const { build: buildManifestFunc } = require('./functions/computation-system/helpers/computation_manifest_builder');
|
|
34
|
+
|
|
32
35
|
// Computation System
|
|
33
36
|
const computationSystem = { runComputationPass : require('./functions/computation-system/helpers/computation_pass_runner') .runComputationPass,
|
|
34
37
|
dataLoader : require('./functions/computation-system/utils/data_loader'),
|
|
35
|
-
computationUtils : require('./functions/computation-system/utils/utils')
|
|
38
|
+
computationUtils : require('./functions/computation-system/utils/utils'),
|
|
39
|
+
buildManifest : buildManifestFunc // <--- NEW PIPE EXPORT
|
|
40
|
+
};
|
|
36
41
|
|
|
37
42
|
// API
|
|
38
43
|
const api = { createApiApp : require('./functions/generic-api/index') .createApiApp,
|
|
@@ -50,4 +55,4 @@ const maintenance = { runSpeculatorCleanup : require('./functions/spec
|
|
|
50
55
|
// Proxy
|
|
51
56
|
const proxy = { handlePost : require('./functions/appscript-api/index') .handlePost };
|
|
52
57
|
|
|
53
|
-
module.exports = { pipe: { core, orchestrator, dispatcher, taskEngine, computationSystem, api, maintenance, proxy } };
|
|
58
|
+
module.exports = { pipe: { core, orchestrator, dispatcher, taskEngine, computationSystem, api, maintenance, proxy } };
|