bulltrackers-module 1.0.295 → 1.0.296

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.
@@ -19,130 +19,129 @@ const createAdminRouter = (config, dependencies, unifiedCalculations) => {
19
19
  const router = express.Router();
20
20
  const { db, logger } = dependencies;
21
21
 
22
+ // Helper to get fresh manifest
23
+ const getFullManifest = () => getManifest([], unifiedCalculations, dependencies);
24
+
22
25
  // --- 1. TOPOLOGY VISUALIZER ---
23
- // Returns nodes/edges for React Flow or Cytoscape
24
26
  router.get('/topology', async (req, res) => {
25
27
  try {
26
- // Build manifest using the INJECTED calculations object
27
- // Passing [] for productLines ensures we get the FULL graph
28
- const manifest = getManifest([], unifiedCalculations, dependencies);
29
-
28
+ const manifest = getFullManifest();
30
29
  const nodes = [];
31
30
  const edges = [];
32
31
 
33
32
  manifest.forEach(calc => {
34
- // Nodes
35
33
  nodes.push({
36
34
  id: calc.name,
37
35
  data: {
38
36
  label: calc.name,
39
37
  layer: calc.category,
40
- pass: calc.pass, // Visualization can group columns by Pass
38
+ pass: calc.pass,
41
39
  isHistorical: calc.isHistorical,
42
40
  type: calc.type
43
41
  },
44
- position: { x: 0, y: 0 } // Frontend handles layout (e.g. Dagre)
42
+ position: { x: 0, y: 0 }
45
43
  });
46
44
 
47
- // Dependency Edges (Calc -> Calc)
48
45
  if (calc.dependencies) {
49
46
  calc.dependencies.forEach(dep => {
50
47
  edges.push({
51
48
  id: `e-${dep}-${calc.name}`,
52
49
  source: normalizeName(dep),
53
50
  target: calc.name,
54
- type: 'default',
55
- animated: false
51
+ type: 'smoothstep'
56
52
  });
57
53
  });
58
54
  }
59
55
 
60
- // Root Data Edges (Data -> Calc)
61
56
  if (calc.rootDataDependencies) {
62
57
  calc.rootDataDependencies.forEach(root => {
63
- // Ensure a node exists for the root data type
64
58
  const rootId = `ROOT_${root.toUpperCase()}`;
65
59
  if (!nodes.find(n => n.id === rootId)) {
66
60
  nodes.push({
67
61
  id: rootId,
68
- type: 'input', // Special React Flow type
62
+ type: 'input',
69
63
  data: { label: `${root.toUpperCase()} DB` },
70
- position: { x: 0, y: 0 }
64
+ position: { x: 0, y: 0 },
65
+ style: { background: '#f0f0f0', border: '1px solid #777' }
71
66
  });
72
67
  }
73
-
74
68
  edges.push({
75
69
  id: `e-root-${root}-${calc.name}`,
76
70
  source: rootId,
77
71
  target: calc.name,
78
72
  animated: true,
79
- style: { stroke: '#ff0072' } // Highlight data flow
73
+ style: { stroke: '#ff0072' }
80
74
  });
81
75
  });
82
76
  }
83
77
  });
84
78
 
85
- res.json({
86
- summary: {
87
- totalNodes: nodes.length,
88
- totalEdges: edges.length
89
- },
90
- nodes,
91
- edges
92
- });
79
+ res.json({ summary: { totalNodes: nodes.length, totalEdges: edges.length }, nodes, edges });
93
80
  } catch (e) {
94
81
  logger.log('ERROR', '[AdminAPI] Topology build failed', e);
95
82
  res.status(500).json({ error: e.message });
96
83
  }
97
84
  });
98
85
 
99
- // --- 2. STATUS MATRIX (Calendar View) ---
100
- // ?start=2023-01-01&end=2023-01-30
86
+ // --- 2. STATUS MATRIX (Calendar / State UI) ---
87
+ // Returns status of ALL computations across a date range.
88
+ // ENHANCED: Cross-references Manifest to detect "PENDING" (Not run yet) vs "MISSING".
101
89
  router.get('/matrix', async (req, res) => {
102
90
  const { start, end } = req.query;
103
- if (!start || !end) return res.status(400).json({ error: "Start (YYYY-MM-DD) and End dates required." });
91
+ if (!start || !end) return res.status(400).json({ error: "Start and End dates required." });
104
92
 
105
93
  try {
106
- const startDate = new Date(start);
107
- const endDate = new Date(end);
94
+ const startDate = new Date(String(start));
95
+ const endDate = new Date(String(end));
108
96
  const dates = [];
109
-
110
- // Generate date range
111
97
  for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
112
98
  dates.push(d.toISOString().slice(0, 10));
113
99
  }
114
100
 
115
- const limit = pLimit(20); // Concurrent Firestore reads
101
+ const manifest = getFullManifest();
102
+ const allCalcNames = new Set(manifest.map(c => c.name));
103
+
104
+ const limit = pLimit(20);
116
105
  const matrix = {};
117
106
 
118
107
  await Promise.all(dates.map(date => limit(async () => {
119
- // Fetch Global Status and Root Data Availability
108
+ // Fetch Status and Root Data Availability
120
109
  const [statusSnap, rootSnap] = await Promise.all([
121
110
  db.collection('computation_status').doc(date).get(),
122
111
  db.collection('system_root_data_index').doc(date).get()
123
112
  ]);
124
113
 
125
- // Flatten status for frontend (calcName -> { status: 'COMPLETED' | 'IMPOSSIBLE' })
126
114
  const statusData = statusSnap.exists ? statusSnap.data() : {};
127
115
  const rootData = rootSnap.exists ? rootSnap.data() : { status: { hasPortfolio: false } };
128
116
 
129
- // Clean up status map
130
- const cleanStatus = {};
131
- Object.keys(statusData).forEach(key => {
132
- const entry = statusData[key];
133
- if (typeof entry === 'object') {
134
- if (entry.hash && entry.hash.startsWith('IMPOSSIBLE')) cleanStatus[key] = 'IMPOSSIBLE';
135
- else cleanStatus[key] = 'COMPLETED';
117
+ const dateStatus = {};
118
+
119
+ // Check every calculation in the Manifest
120
+ allCalcNames.forEach(calcName => {
121
+ const entry = statusData[calcName];
122
+
123
+ if (!entry) {
124
+ // If root data exists but calc is missing -> PENDING
125
+ // If no root data -> WAITING_DATA
126
+ dateStatus[calcName] = rootData.status?.hasPortfolio ? 'PENDING' : 'WAITING_DATA';
127
+ } else if (typeof entry === 'object') {
128
+ if (entry.hash && typeof entry.hash === 'string' && entry.hash.startsWith('IMPOSSIBLE')) {
129
+ dateStatus[calcName] = 'IMPOSSIBLE';
130
+ } else if (entry.hash === false) {
131
+ dateStatus[calcName] = 'BLOCKED';
132
+ } else {
133
+ dateStatus[calcName] = 'COMPLETED';
134
+ }
136
135
  } else if (entry === 'IMPOSSIBLE') {
137
- cleanStatus[key] = 'IMPOSSIBLE';
138
- } else if (entry === true || typeof entry === 'string') {
139
- cleanStatus[key] = 'COMPLETED';
136
+ dateStatus[calcName] = 'IMPOSSIBLE';
137
+ } else {
138
+ dateStatus[calcName] = 'COMPLETED';
140
139
  }
141
140
  });
142
141
 
143
142
  matrix[date] = {
144
- dataAvailable: rootData.status || {}, // e.g. { hasPortfolio: true }
145
- calculations: cleanStatus
143
+ dataAvailable: rootData.status || {},
144
+ calculations: dateStatus
146
145
  };
147
146
  })));
148
147
 
@@ -153,74 +152,227 @@ const createAdminRouter = (config, dependencies, unifiedCalculations) => {
153
152
  }
154
153
  });
155
154
 
156
- // --- 3. FLIGHT RECORDER (Inspection) ---
157
- // Look up execution details for a specific Calc + Date
158
- router.get('/inspect/:date/:calcName', async (req, res) => {
159
- const { date, calcName } = req.params;
155
+ // --- 3. PIPELINE STATE (Progress Bar) ---
156
+ // Shows realtime status of the 5-pass system for a specific date
157
+ router.get('/pipeline/state', async (req, res) => {
158
+ const { date } = req.query;
159
+ if (!date) return res.status(400).json({ error: "Date required" });
160
+
160
161
  try {
161
- // We search across all potential passes (1-5) because we might not know which one it belongs to
162
162
  const passes = ['1', '2', '3', '4', '5'];
163
- let executionRecord = null;
163
+ const state = await Promise.all(passes.map(async (pass) => {
164
+ // We use the Audit Ledger which is the source of truth for execution state
165
+ const tasksSnap = await db.collection(`computation_audit_ledger/${date}/passes/${pass}/tasks`).get();
166
+
167
+ const stats = {
168
+ pending: 0,
169
+ inProgress: 0,
170
+ completed: 0,
171
+ failed: 0,
172
+ totalMemoryMB: 0,
173
+ avgDurationMs: 0
174
+ };
175
+
176
+ const durations = [];
177
+
178
+ tasksSnap.forEach(doc => {
179
+ const data = doc.data();
180
+ const s = (data.status || 'UNKNOWN').toLowerCase();
181
+ if (stats[s] !== undefined) stats[s]++;
182
+ else stats[s] = 1;
164
183
 
165
- // Run in parallel to find the record fast
166
- await Promise.all(passes.map(async (pass) => {
167
- if (executionRecord) return; // Optimization
168
- const ref = db.doc(`computation_audit_ledger/${date}/passes/${pass}/tasks/${calcName}`);
169
- const snap = await ref.get();
170
- if (snap.exists) {
171
- executionRecord = { pass, ...snap.data() };
172
- }
184
+ if (data.telemetry?.lastMemory?.rssMB) {
185
+ stats.totalMemoryMB += data.telemetry.lastMemory.rssMB;
186
+ }
187
+ if (data.completedAt && data.startedAt) {
188
+ durations.push(new Date(data.completedAt).getTime() - new Date(data.startedAt).getTime());
189
+ }
190
+ });
191
+
192
+ stats.avgDurationMs = durations.length ?
193
+ Math.round(durations.reduce((a, b) => a + b, 0) / durations.length) : 0;
194
+
195
+ return { pass, stats, totalTasks: tasksSnap.size };
173
196
  }));
174
197
 
175
- if (!executionRecord) {
176
- return res.status(404).json({
177
- status: 'NOT_FOUND',
178
- message: `No execution record found in ledger for ${calcName} on ${date}`
198
+ res.json({ date, passes: state });
199
+ } catch (e) {
200
+ res.status(500).json({ error: e.message });
201
+ }
202
+ });
203
+
204
+ // --- 4. DEPENDENCY TRACER (Blast Radius) ---
205
+ router.get('/trace/:calcName', async (req, res) => {
206
+ const { calcName } = req.params;
207
+ const mode = req.query.mode || 'downstream'; // 'upstream' or 'downstream'
208
+
209
+ try {
210
+ const manifest = getFullManifest();
211
+ const manifestMap = new Map(manifest.map(c => [c.name, c]));
212
+
213
+ if (!manifestMap.has(calcName)) return res.status(404).json({ error: 'Calculation not found' });
214
+
215
+ const trace = { root: calcName, chain: [] };
216
+
217
+ if (mode === 'upstream') {
218
+ // What does X depend on?
219
+ const visited = new Set();
220
+ const walk = (name, depth = 0) => {
221
+ if (visited.has(name) || depth > 10) return;
222
+ visited.add(name);
223
+ const calc = manifestMap.get(name);
224
+ if (!calc) return;
225
+
226
+ trace.chain.push({
227
+ name, depth, pass: calc.pass, type: calc.type
228
+ });
229
+
230
+ calc.dependencies?.forEach(dep => walk(dep, depth + 1));
231
+ };
232
+ walk(calcName);
233
+ } else {
234
+ // What depends on X? (Downstream / Impact)
235
+ const reverseGraph = new Map();
236
+ manifest.forEach(c => {
237
+ c.dependencies?.forEach(dep => {
238
+ const normDep = normalizeName(dep);
239
+ if (!reverseGraph.has(normDep)) reverseGraph.set(normDep, []);
240
+ reverseGraph.get(normDep).push(c.name);
241
+ });
179
242
  });
243
+
244
+ const visited = new Set();
245
+ const walk = (name, depth = 0) => {
246
+ if (visited.has(name) || depth > 10) return;
247
+ visited.add(name);
248
+
249
+ const calc = manifestMap.get(name);
250
+ trace.chain.push({
251
+ name, depth, pass: calc?.pass
252
+ });
253
+
254
+ reverseGraph.get(name)?.forEach(child => walk(child, depth + 1));
255
+ };
256
+ walk(calcName);
180
257
  }
181
258
 
182
- // Also fetch the "Contract" if it exists (for volatility analysis)
183
- const contractSnap = await db.collection('system_contracts').doc(calcName).get();
184
-
185
- res.json({
186
- execution: executionRecord,
187
- contract: contractSnap.exists ? contractSnap.data() : null
259
+ res.json(trace);
260
+ } catch (e) {
261
+ res.status(500).json({ error: e.message });
262
+ }
263
+ });
264
+
265
+ // --- 5. CONTRACT VIOLATIONS (Quality Gate) ---
266
+ router.get('/violations', async (req, res) => {
267
+ const days = parseInt(String(req.query.days)) || 7;
268
+ const cutoff = new Date();
269
+ cutoff.setDate(cutoff.getDate() - days);
270
+
271
+ try {
272
+ // Check DLQ for Semantic Failures (Hard Violations)
273
+ const dlqSnap = await db.collection('computation_dead_letter_queue')
274
+ .where('finalAttemptAt', '>', cutoff)
275
+ .where('error.stage', '==', 'SEMANTIC_GATE')
276
+ .limit(50)
277
+ .get();
278
+
279
+ const violations = [];
280
+ dlqSnap.forEach(doc => {
281
+ const data = doc.data();
282
+ violations.push({
283
+ id: doc.id,
284
+ computation: data.originalData.computation,
285
+ date: data.originalData.date,
286
+ reason: data.error.message,
287
+ type: 'HARD_VIOLATION',
288
+ timestamp: data.finalAttemptAt
289
+ });
290
+ });
291
+
292
+ // Check Audit Logs for Soft Anomalies (Statistical warnings)
293
+ const anomalySnap = await db.collectionGroup('history')
294
+ .where('triggerTime', '>', cutoff.toISOString())
295
+ .where('anomalies', '!=', []) // Firestore != operator
296
+ .limit(50)
297
+ .get();
298
+
299
+ anomalySnap.forEach(doc => {
300
+ const data = doc.data();
301
+ data.anomalies?.forEach(anomaly => {
302
+ violations.push({
303
+ id: doc.id,
304
+ computation: data.computationName,
305
+ date: data.targetDate,
306
+ reason: anomaly,
307
+ type: 'SOFT_ANOMALY',
308
+ timestamp: data.triggerTime
309
+ });
310
+ });
188
311
  });
189
312
 
313
+ // Sort by time desc
314
+ violations.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
315
+
316
+ res.json({ count: violations.length, violations });
190
317
  } catch (e) {
191
- logger.log('ERROR', `[AdminAPI] Inspect failed for ${calcName}`, e);
192
318
  res.status(500).json({ error: e.message });
193
319
  }
194
320
  });
195
321
 
196
- // --- 4. ANOMALY DETECTOR ---
197
- // Finds recent crashes and chronic failures
198
- router.get('/anomalies', async (req, res) => {
322
+ // --- 6. MEMORY HOTSPOTS (Forensics) ---
323
+ router.get('/memory/hotspots', async (req, res) => {
324
+ const thresholdMB = parseInt(String(req.query.threshold)) || 1000; // 1GB default
325
+
199
326
  try {
200
- const [dlqSnap, statsSnap] = await Promise.all([
201
- db.collection('computation_dead_letter_queue').orderBy('finalAttemptAt', 'desc').limit(50).get(),
202
- db.collection('computation_audit_logs').orderBy('failureCount', 'desc').limit(20).get()
203
- ]);
204
-
205
- const recentCrashes = [];
206
- dlqSnap.forEach(doc => recentCrashes.push({ id: doc.id, ...doc.data() }));
207
-
208
- const chronicFailures = [];
209
- statsSnap.forEach(doc => {
210
- const d = doc.data();
211
- if (d.failureCount > 0) {
212
- chronicFailures.push({
213
- computation: doc.id,
214
- failures: d.failureCount,
215
- successes: d.successCount || 0,
216
- lastError: d.lastRunStatus
217
- });
218
- }
327
+ // Ledger tasks maintain 'telemetry.lastMemory'
328
+ // We use collectionGroup to search across all dates/passes
329
+ const snapshot = await db.collectionGroup('tasks')
330
+ .where('telemetry.lastMemory.rssMB', '>', thresholdMB)
331
+ .orderBy('telemetry.lastMemory.rssMB', 'desc')
332
+ .limit(20)
333
+ .get();
334
+
335
+ const hotspots = [];
336
+ snapshot.forEach(doc => {
337
+ const data = doc.data();
338
+ hotspots.push({
339
+ computation: data.computation,
340
+ rssMB: data.telemetry.lastMemory.rssMB,
341
+ heapMB: data.telemetry.lastMemory.heapUsedMB,
342
+ status: data.status,
343
+ worker: data.workerId,
344
+ date: doc.ref.parent.parent.parent.parent.id // traversing path to get date
345
+ });
219
346
  });
347
+
348
+ res.json({ count: hotspots.length, hotspots });
349
+ } catch (e) {
350
+ res.status(500).json({ error: e.message });
351
+ }
352
+ });
353
+
354
+ // --- 7. FLIGHT RECORDER (Inspection) ---
355
+ // Existing inspection endpoint kept for drill-down
356
+ router.get('/inspect/:date/:calcName', async (req, res) => {
357
+ const { date, calcName } = req.params;
358
+ try {
359
+ const passes = ['1', '2', '3', '4', '5'];
360
+ let executionRecord = null;
220
361
 
362
+ await Promise.all(passes.map(async (pass) => {
363
+ if (executionRecord) return;
364
+ const ref = db.doc(`computation_audit_ledger/${date}/passes/${pass}/tasks/${calcName}`);
365
+ const snap = await ref.get();
366
+ if (snap.exists) executionRecord = { pass, ...snap.data() };
367
+ }));
368
+
369
+ if (!executionRecord) return res.status(404).json({ status: 'NOT_FOUND' });
370
+
371
+ const contractSnap = await db.collection('system_contracts').doc(calcName).get();
372
+
221
373
  res.json({
222
- recentCrashes,
223
- chronicFailures
374
+ execution: executionRecord,
375
+ contract: contractSnap.exists ? contractSnap.data() : null
224
376
  });
225
377
  } catch (e) {
226
378
  res.status(500).json({ error: e.message });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.295",
3
+ "version": "1.0.296",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [