bulltrackers-module 1.0.158 → 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); })(); }
@@ -55,17 +55,12 @@ async function checkRootDataAvailability(dateStr, config, dependencies, earliest
55
55
  */
56
56
  async function fetchExistingResults(dateStr, calcsInPass, fullManifest, config, { db, logger }) {
57
57
  const manifestMap = new Map(fullManifest.map(c => [normalizeName(c.name), c]));
58
-
59
- // --- FIX: Create a Set of *all* calcs we need to fetch ---
60
- // This includes the calcs in this pass AND their dependencies.
61
58
  const calcsToFetch = new Set();
62
59
 
63
60
  for (const calc of calcsInPass) {
64
61
  const calcName = normalizeName(calc.name);
65
- // Add the calc itself (for skipping)
66
62
  calcsToFetch.add(calcName);
67
63
 
68
- // Add all its dependencies (for processing)
69
64
  if (calc.dependencies && calc.dependencies.length > 0) {
70
65
  for (const depName of calc.dependencies) {
71
66
  calcsToFetch.add(normalizeName(depName));
@@ -327,25 +322,8 @@ async function runMetaComputationPass(date, calcs, passName, config, deps, fetch
327
322
  const name = normalizeName(mCalc.name), Cl = mCalc.class;
328
323
  if (typeof Cl !== 'function') { logger.log('ERROR', `Invalid class ${name}`); failedCalcs.push(name); continue; }
329
324
  const inst = new Cl();
330
-
331
- // --- FIX: This is the critical change for the "Structural Bug" ---
332
- // The original code assumed a 'meta' calc *only* has a .process() method.
333
- // We now check for .process() first, and if it's not there,
334
- // we fall back to calling .getResult() and pass it the dependencies.
335
- //
336
- // This file, however, expects `crowd_sharpe_ratio_proxy` to be
337
- // refactored to use `process()`. See the update to that file.
338
- // This `runMetaComputationPass` function remains as-is,
339
- // as the fix is in refactoring the calculation file itself.
340
-
341
- try {
342
-
343
- // --- FIX: Refactored `crowd_sharpe_ratio_proxy` will now have this method ---
344
- if (typeof inst.process !== 'function') {
345
- logger.log('ERROR', `Meta-calc ${name} is missing a 'process' method.`);
346
- failedCalcs.push(name);
347
- continue;
348
- }
325
+
326
+ try { if (typeof inst.process !== 'function') { logger.log('ERROR', `Meta-calc ${name} is missing a 'process' method.`); failedCalcs.push(name); continue; }
349
327
 
350
328
  const result = await Promise.resolve(inst.process(dStr, { ...deps, rootData: fullRoot }, config, fetchedDeps));
351
329
 
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 } };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.158",
3
+ "version": "1.0.159",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [