bulltrackers-module 1.0.158 → 1.0.160

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));
@@ -104,7 +99,13 @@ async function fetchExistingResults(dateStr, calcsInPass, fullManifest, config,
104
99
  const fetched = {};
105
100
  if (docRefs.length) {
106
101
  (await db.getAll(...docRefs)).forEach((doc, i) => {
107
- fetched[depNames[i]] = doc.exists ? doc.data() : null;
102
+ // --- FIX [PROBLEM 8]: Add completion marker check ---
103
+ const data = doc.exists ? doc.data() : null;
104
+ if (data && data._completed === true) {
105
+ fetched[depNames[i]] = data;
106
+ } else {
107
+ fetched[depNames[i]] = null; // Treat as not existing if incomplete
108
+ }
108
109
  });
109
110
  }
110
111
 
@@ -144,19 +145,18 @@ function filterCalculations(standardCalcs, metaCalcs, rootDataStatus, existingRe
144
145
  return earliestRunDate;
145
146
  };
146
147
  const filterCalc = (calc) => {
147
- if (existingResults[calc.name]) {logger.log('TRACE', `[Pass ${passToRun}] Skipping ${calc.name} for ${dateStr}. Result already exists.`); skipped.add(calc.name); return false;}
148
+ // --- FIX [PROBLEM 8]: The check for existingResults is now correct ---
149
+ if (existingResults[calc.name]) {logger.log('TRACE', `[Pass ${passToRun}] Skipping ${calc.name} for ${dateStr}. Result already exists (and is complete).`); skipped.add(calc.name); return false;}
150
+
148
151
  const earliestRunDate = getTrueEarliestRunDate(calc);
149
152
  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; }
150
153
  const { canRun, missing: missingRoot } = checkRootDependencies(calc, rootDataStatus);
151
154
  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;}
152
155
 
153
- // --- FIX: This check is now robust ---
154
- // 'existingResults' now contains all dependencies, so this check
155
- // will correctly find 'pnl_distribution_per_stock' and *not* skip.
156
156
  if (calc.type === 'meta') {
157
157
  const missingDeps = (calc.dependencies || [])
158
158
  .map(normalizeName)
159
- .filter(d => !existingResults[d]);
159
+ .filter(d => !existingResults[d]); // This check is now robust
160
160
  if (missingDeps.length > 0) {
161
161
  logger.log('WARN', `[Pass ${passToRun} Meta] Skipping ${calc.name} for ${dateStr}. Missing computed deps: [${missingDeps.join(', ')}]`);
162
162
  skipped.add(calc.name);
@@ -182,15 +182,20 @@ async function loadHistoricalData(date, calcs, config, deps, rootData) {
182
182
  const needsYesterdayInsights = calcs.some(c => c.isHistorical && c.rootDataDependencies.includes('insights'));
183
183
  const needsYesterdaySocial = calcs.some(c => c.isHistorical && c.rootDataDependencies.includes('social'));
184
184
  const needsYesterdayPortfolio = calcs.some(c => c.isHistorical && c.rootDataDependencies.includes('portfolio'));
185
+
186
+ // --- FIX: Add T-1 COMPUTED dependency loading ---
187
+ const needsYesterdayDependencies = calcs.some(c => c.isHistorical && c.dependencies && c.dependencies.length > 0);
188
+
185
189
  const prev = new Date(date);
186
190
  prev.setUTCDate(prev.getUTCDate() - 1);
187
191
  const prevStr = prev.toISOString().slice(0, 10);
192
+
188
193
  if(needsYesterdayInsights) {
189
- tasks.push((async()=>{ const prev=new Date(date); prev.setUTCDate(prev.getUTCDate()-1); const prevStr=prev.toISOString().slice(0,10);
194
+ tasks.push((async()=>{
190
195
  logger.log('INFO', `[PassRunner] Loading YESTERDAY insights data for ${prevStr}`);
191
196
  updated.yesterdayInsights=await loadDailyInsights(config,deps,prevStr); })());}
192
197
  if(needsYesterdaySocial) {
193
- tasks.push((async()=>{ const prev=new Date(date); prev.setUTCDate(prev.getUTCDate()-1); const prevStr=prev.toISOString().slice(0,10);
198
+ tasks.push((async()=>{
194
199
  logger.log('INFO', `[PassRunner] Loading YESTERDAY social data for ${prevStr}`);
195
200
  updated.yesterdaySocialPostInsights=await loadDailySocialPostInsights(config,deps,prevStr); })());}
196
201
 
@@ -200,6 +205,17 @@ async function loadHistoricalData(date, calcs, config, deps, rootData) {
200
205
  updated.yesterdayPortfolioRefs = await getPortfolioPartRefs(config, deps, prevStr);
201
206
  })());
202
207
  }
208
+
209
+ // --- FIX: Load T-1 COMPUTED dependencies ---
210
+ if(needsYesterdayDependencies) {
211
+ tasks.push((async()=>{
212
+ logger.log('INFO', `[PassRunner] Loading YESTERDAY computed dependencies for ${prevStr}`);
213
+ // This is a simplified fetch, assuming all calcs in this pass share the same T-1 deps
214
+ // A more robust solution would aggregate all unique T-1 deps.
215
+ updated.yesterdayDependencyData = await fetchExistingResults(prevStr, calcs, calcs.map(c => c.manifest), config, deps);
216
+ })());
217
+ }
218
+
203
219
  await Promise.all(tasks);
204
220
  return updated;
205
221
  }
@@ -209,9 +225,12 @@ async function loadHistoricalData(date, calcs, config, deps, rootData) {
209
225
  */
210
226
  async function streamAndProcess(dateStr, state, passName, config, deps, rootData, portfolioRefs, historyRefs) {
211
227
  const { logger, calculationUtils } = deps;
212
- const { todayInsights, yesterdayInsights, todaySocialPostInsights, yesterdaySocialPostInsights } = rootData;
228
+ const { todayInsights, yesterdayInsights, todaySocialPostInsights, yesterdaySocialPostInsights, yesterdayDependencyData } = rootData;
213
229
  const calcsThatStreamPortfolio = Object.values(state).filter(calc => calc && calc.manifest && (calc.manifest.rootDataDependencies.includes('portfolio') || calc.manifest.category === 'speculators'));
214
- const context={instrumentMappings:(await calculationUtils.loadInstrumentMappings()).instrumentToTicker, sectorMapping:(await calculationUtils.loadInstrumentMappings()).instrumentToSector, todayDateStr:dateStr, dependencies:deps, config};
230
+
231
+ // --- FIX: Add yesterday's computed data to context ---
232
+ const context={instrumentMappings:(await calculationUtils.loadInstrumentMappings()).instrumentToTicker, sectorMapping:(await calculationUtils.loadInstrumentMappings()).instrumentToSector, todayDateStr:dateStr, dependencies:deps, config, yesterdaysDependencyData: yesterdayDependencyData};
233
+
215
234
  let firstUser=true;
216
235
  for(const name in state){ const calc=state[name]; if(!calc||typeof calc.process!=='function') continue;
217
236
  const cat=calc.manifest.category;
@@ -254,6 +273,11 @@ async function streamAndProcess(dateStr, state, passName, config, deps, rootData
254
273
  firstUser=false;
255
274
  if (pY) { delete yesterdayPortfolios[uid]; }
256
275
  if (hT) { delete todayHistoryData[uid]; } } }
276
+
277
+ // --- FIX [PROBLEM 7]: Clear stale data to prevent memory leak ---
278
+ yesterdayPortfolios = {};
279
+ todayHistoryData = {};
280
+
257
281
  logger.log('INFO', `[${passName}] Finished streaming data for ${dateStr}.`);
258
282
  }
259
283
 
@@ -284,6 +308,8 @@ async function runStandardComputationPass(date, calcs, passName, config, deps, r
284
308
  } else { standardResult[key] = result[key]; }}
285
309
  if (Object.keys(standardResult).length > 0) {
286
310
  const docRef = deps.db.collection(config.resultsCollection).doc(dStr) .collection(config.resultsSubcollection).doc(calc.manifest.category) .collection(config.computationsSubcollection).doc(name);
311
+ // --- FIX [PROBLEM 8]: Add completion marker ---
312
+ standardResult._completed = true;
287
313
  standardWrites.push({ ref: docRef, data: standardResult });}
288
314
  const calcClass = calc.manifest.class;
289
315
  let staticSchema = null;
@@ -302,7 +328,10 @@ async function runStandardComputationPass(date, calcs, passName, config, deps, r
302
328
  if (docPath.includes('/')) { docRef = deps.db.doc(docPath); } else {
303
329
  const collection = (docPath.startsWith('user_profile_history')) ? config.shardedUserProfileCollection : config.shardedProfitabilityCollection;
304
330
  docRef = deps.db.collection(collection).doc(docPath); }
305
- if (docData && typeof docData === 'object' && !Array.isArray(docData)) {shardedDocWrites.push({ ref: docRef, data: docData });
331
+ if (docData && typeof docData === 'object' && !Array.isArray(docData)) {
332
+ // --- FIX [PROBLEM 8]: Add completion marker to sharded writes ---
333
+ docData._completed = true;
334
+ shardedDocWrites.push({ ref: docRef, data: docData });
306
335
  } else { logger.log('ERROR', `[${passName}] Invalid sharded document data for ${docPath}. Not an object.`, { data: docData }); }
307
336
  if (shardedDocWrites.length > 0) { await commitBatchInChunks(config, deps, shardedDocWrites, `${passName} Sharded ${docPath} ${dStr}`); } }
308
337
  const logMetadata = {};
@@ -327,25 +356,8 @@ async function runMetaComputationPass(date, calcs, passName, config, deps, fetch
327
356
  const name = normalizeName(mCalc.name), Cl = mCalc.class;
328
357
  if (typeof Cl !== 'function') { logger.log('ERROR', `Invalid class ${name}`); failedCalcs.push(name); continue; }
329
358
  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
- }
359
+
360
+ try { if (typeof inst.process !== 'function') { logger.log('ERROR', `Meta-calc ${name} is missing a 'process' method.`); failedCalcs.push(name); continue; }
349
361
 
350
362
  const result = await Promise.resolve(inst.process(dStr, { ...deps, rootData: fullRoot }, config, fetchedDeps));
351
363
 
@@ -358,6 +370,8 @@ async function runMetaComputationPass(date, calcs, passName, config, deps, fetch
358
370
  }
359
371
  if (Object.keys(standardResult).length > 0) {
360
372
  const docRef = deps.db.collection(config.resultsCollection).doc(dStr) .collection(config.resultsSubcollection).doc(mCalc.category) .collection(config.computationsSubcollection).doc(name);
373
+ // --- FIX [PROBLEM 8]: Add completion marker ---
374
+ standardResult._completed = true;
361
375
  standardWrites.push({ ref: docRef, data: standardResult });
362
376
  }
363
377
  const calcClass = mCalc.class;
@@ -375,7 +389,13 @@ async function runMetaComputationPass(date, calcs, passName, config, deps, fetch
375
389
  for (const collectionName in shardedWrites) {
376
390
  const docs = shardedWrites[collectionName];
377
391
  const shardedDocWrites = [];
378
- for (const docId in docs) { const docRef = docId.includes('/') ? deps.db.doc(docId) : deps.db.collection(collectionName).doc(docId); shardedDocWrites.push({ ref: docRef, data: docs[docId] }); }
392
+ for (const docId in docs) {
393
+ const docRef = docId.includes('/') ? deps.db.doc(docId) : deps.db.collection(collectionName).doc(docId);
394
+ // --- FIX [PROBLEM 8]: Add completion marker to sharded writes ---
395
+ const docData = docs[docId];
396
+ docData._completed = true;
397
+ shardedDocWrites.push({ ref: docRef, data: docData });
398
+ }
379
399
  if (shardedDocWrites.length > 0) { await commitBatchInChunks(config, deps, shardedDocWrites, `${passName} Sharded ${collectionName} ${dStr}`); } }
380
400
  const logMetadata = {};
381
401
  if (failedCalcs.length > 0) { logMetadata.failedComputations = failedCalcs; }
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.160",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [