bulltrackers-module 1.0.174 → 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.
@@ -171,7 +171,7 @@ class ComputationExecutor {
171
171
 
172
172
  const context = ContextBuilder.buildPerUserContext({
173
173
  todayPortfolio, yesterdayPortfolio,
174
- todayHistory, // Passed to context
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
- os.tmpdir = () => '/tmp';
21
- try { const temp = require('temp'); const path = require('path'); const fs = require('fs'); const tmp = '/tmp';
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, generateSvgGraph };
255
+ module.exports = { build};
@@ -1,9 +1,17 @@
1
1
  /**
2
- * FIXED: computation_pass_runner.js
3
- * V3: Fetches Previous Day's Results to enable State Persistence.
2
+ * FILENAME: bulltrackers-module/functions/computation-system/helpers/computation_pass_runner.js
4
3
  */
5
4
 
6
- const { groupByPass, checkRootDataAvailability, fetchExistingResults, runStandardComputationPass, runMetaComputationPass } = require('./orchestration_helpers.js');
5
+ const {
6
+ groupByPass,
7
+ checkRootDataAvailability,
8
+ fetchExistingResults,
9
+ fetchGlobalComputationStatus, // <--- New Import
10
+ updateGlobalComputationStatus, // <--- New Import
11
+ runStandardComputationPass,
12
+ runMetaComputationPass,
13
+ checkRootDependencies
14
+ } = require('./orchestration_helpers.js');
7
15
  const { getExpectedDateStrings } = require('../utils/utils.js');
8
16
  const PARALLEL_BATCH_SIZE = 7;
9
17
 
@@ -13,93 +21,121 @@ async function runComputationPass(config, dependencies, computationManifest) {
13
21
  if (!passToRun)
14
22
  return logger.log('ERROR', '[PassRunner] No pass defined. Aborting.');
15
23
 
16
- logger.log('INFO', `🚀 Starting PASS ${passToRun}...`);
24
+ logger.log('INFO', `🚀 Starting PASS ${passToRun} with Global Status Check...`);
17
25
 
18
- 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') };
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') };
19
28
  earliestDates.absoluteEarliest = Object.values(earliestDates).reduce((a,b) => a < b ? a : b);
20
29
 
21
- const passes = groupByPass(computationManifest);
30
+ const passes = groupByPass(computationManifest);
22
31
  const calcsInThisPass = passes[passToRun] || [];
23
32
 
24
33
  if (!calcsInThisPass.length)
25
34
  return logger.log('WARN', `[PassRunner] No calcs for Pass ${passToRun}. Exiting.`);
26
35
 
27
- const calcEarliestDates = new Map();
36
+ const passEarliestDate = earliestDates.absoluteEarliest;
37
+ const endDateUTC = new Date(Date.UTC(new Date().getUTCFullYear(), new Date().getUTCMonth(), new Date().getUTCDate() - 1));
38
+ const allExpectedDates = getExpectedDateStrings(passEarliestDate, endDateUTC);
28
39
 
29
- for (const calc of calcsInThisPass) {
30
- const deps = calc.rootDataDependencies || [];
40
+ const standardCalcs = calcsInThisPass.filter(c => c.type === 'standard');
41
+ const metaCalcs = calcsInThisPass.filter(c => c.type === 'meta');
31
42
 
32
- if (!deps.length)
33
- { calcEarliestDates.set(calc.name, earliestDates.absoluteEarliest); continue; }
43
+ // 1. Fetch Global Status ONCE (Memory Cache)
44
+ // Returns { "2023-10-27": { calcA: true, calcB: false }, ... }
45
+ const globalStatusData = await fetchGlobalComputationStatus(config, dependencies);
34
46
 
35
- const latestDep = new Date(Math.max(...deps.map(d => earliestDates[d]?.getTime() || 0)));
36
- const calcDate = calc.isHistorical
37
- ? new Date(latestDep.getTime() + 86400000)
38
- : latestDep;
47
+ // Helper: Check status using in-memory data
48
+ const shouldRun = (calc, dateStr) => {
49
+ const dailyStatus = globalStatusData[dateStr] || {};
39
50
 
40
- calcEarliestDates.set(calc.name, calcDate);
41
- }
51
+ // 1. If explicitly TRUE, ignore.
52
+ if (dailyStatus[calc.name] === true) return false;
53
+
54
+ // 2. Check dependencies (using same in-memory status)
55
+ if (calc.dependencies && calc.dependencies.length > 0) {
56
+ const depsMet = calc.dependencies.every(depName => dailyStatus[depName] === true);
57
+ if (!depsMet) return false;
58
+ }
59
+ return true;
60
+ };
42
61
 
43
- const passEarliestDate = new Date(Math.min(...Array.from(calcEarliestDates.values()).map(d => d.getTime())));
44
- const endDateUTC = new Date(Date.UTC(new Date().getUTCFullYear(), new Date().getUTCMonth(), new Date().getUTCDate() - 1));
45
- const allExpectedDates = getExpectedDateStrings(passEarliestDate, endDateUTC);
46
- const standardCalcs = calcsInThisPass.filter(c => c.type === 'standard');
47
- const metaCalcs = calcsInThisPass.filter(c => c.type === 'meta');
62
+ // Process a single date and RETURN updates (do not write)
63
+ const processDate = async (dateStr) => {
64
+ const dateToProcess = new Date(dateStr + 'T00:00:00Z');
48
65
 
49
- const checkDeps = (calc, rootData, existingResults, dateToProcess) => {
50
- if (existingResults[calc.name])
51
- return false;
66
+ // Filter using in-memory status
67
+ const standardToRun = standardCalcs.filter(c => shouldRun(c, dateStr));
68
+ const metaToRun = metaCalcs.filter(c => shouldRun(c, dateStr));
69
+
70
+ if (!standardToRun.length && !metaToRun.length) return null; // No work
52
71
 
53
- const earliest = calcEarliestDates.get(calc.name);
72
+ const rootData = await checkRootDataAvailability(dateStr, config, dependencies, earliestDates);
73
+ if (!rootData) return null;
54
74
 
55
- if (earliest && dateToProcess < earliest)
56
- return false;
75
+ const finalStandardToRun = standardToRun.filter(c => checkRootDependencies(c, rootData.status).canRun);
76
+ const finalMetaToRun = metaToRun.filter(c => checkRootDependencies(c, rootData.status).canRun);
57
77
 
58
- const missingRoot = (calc.rootDataDependencies || []).filter(dep => !rootData.status[`has${dep[0].toUpperCase() + dep.slice(1)}`]);
78
+ if (!finalStandardToRun.length && !finalMetaToRun.length) return null;
59
79
 
60
- if (missingRoot.length)
61
- return false;
80
+ logger.log('INFO', `[PassRunner] Running ${dateStr}: ${finalStandardToRun.length} std, ${finalMetaToRun.length} meta`);
62
81
 
63
- if (calc.type === 'meta')
64
- { const missingComputed = (calc.dependencies || []).filter(d => !existingResults[d]);
65
- if (missingComputed.length)
66
- return false; }
67
- return true;
68
- };
82
+ const dateUpdates = {}; // { calcName: true/false }
69
83
 
70
- const processDate = async (dateStr) => {
71
- const dateToProcess = new Date(dateStr + 'T00:00:00Z');
72
84
  try {
73
- const rootData = await checkRootDataAvailability(dateStr, config, dependencies, earliestDates);
74
- if (!rootData) return logger.log('WARN', `[PassRunner] Skipping ${dateStr}: No root data.`);
75
-
76
- // 1. Fetch TODAY'S results (Check what is already done)
77
- const existingResults = await fetchExistingResults(dateStr, calcsInThisPass, computationManifest, config, dependencies, false);
85
+ const calcsRunning = [...finalStandardToRun, ...finalMetaToRun];
86
+ const existingResults = await fetchExistingResults(dateStr, calcsRunning, computationManifest, config, dependencies, false);
78
87
 
79
- // 2. Fetch YESTERDAY'S results (For State Persistence) We calculate T-1 date string
80
- const prevDate = new Date(dateToProcess); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
88
+ const prevDate = new Date(dateToProcess); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
81
89
  const prevDateStr = prevDate.toISOString().slice(0, 10);
90
+ const previousResults = await fetchExistingResults(prevDateStr, calcsRunning, computationManifest, config, dependencies, true);
82
91
 
83
- // We pass 'true' to includeSelf, because we need the calc's OWN history (e.g. mimetic-latency needs previous mimetic-latency)
84
- const previousResults = await fetchExistingResults(prevDateStr, calcsInThisPass, computationManifest, config, dependencies, true);
85
-
86
- const standardToRun = standardCalcs.filter(c => checkDeps(c, rootData, existingResults, dateToProcess));
87
- const metaToRun = metaCalcs.filter(c => checkDeps(c, rootData, existingResults, dateToProcess));
88
-
89
- if (!standardToRun.length && !metaToRun.length) return logger.log('INFO', `[PassRunner] All calcs complete for ${dateStr}. Skipping.`);
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
+ }
90
96
 
91
- logger.log('INFO', `[PassRunner] Running ${dateStr}: ${standardToRun.length} standard, ${metaToRun.length} meta`);
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
+ }
92
101
 
93
- // Pass 'previousResults' to the runners
94
- if (standardToRun.length) await runStandardComputationPass(dateToProcess, standardToRun, `Pass ${passToRun} (Standard)`, config, dependencies, rootData, existingResults, previousResults);
95
- if (metaToRun.length) await runMetaComputationPass(dateToProcess, metaToRun, `Pass ${passToRun} (Meta)`, config, dependencies, existingResults, previousResults, rootData);
96
-
97
- } catch (err) { logger.log('ERROR', `[PassRunner] FAILED Pass ${passToRun} for ${dateStr}`, { errorMessage: err.message, stack: err.stack }); }
102
+ } catch (err) {
103
+ logger.log('ERROR', `[PassRunner] FAILED Pass ${passToRun} for ${dateStr}`, { errorMessage: err.message });
104
+ // Mark failures
105
+ [...finalStandardToRun, ...finalMetaToRun].forEach(c => dateUpdates[c.name] = false);
106
+ }
107
+
108
+ // Return the updates for this date
109
+ return { date: dateStr, updates: dateUpdates };
98
110
  };
99
111
 
112
+ // Batch process dates
100
113
  for (let i = 0; i < allExpectedDates.length; i += PARALLEL_BATCH_SIZE) {
101
114
  const batch = allExpectedDates.slice(i, i + PARALLEL_BATCH_SIZE);
102
- await Promise.all(batch.map(processDate));
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
+ }
103
139
  }
104
140
  logger.log('INFO', `[PassRunner] Pass ${passToRun} orchestration finished.`);
105
141
  }