bulltrackers-module 1.0.733 → 1.0.734

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.
Files changed (56) hide show
  1. package/functions/computation-system-v2/README.md +152 -0
  2. package/functions/computation-system-v2/computations/PopularInvestorProfileMetrics.js +720 -0
  3. package/functions/computation-system-v2/computations/PopularInvestorRiskAssessment.js +176 -0
  4. package/functions/computation-system-v2/computations/PopularInvestorRiskMetrics.js +294 -0
  5. package/functions/computation-system-v2/computations/TestComputation.js +46 -0
  6. package/functions/computation-system-v2/computations/UserPortfolioSummary.js +172 -0
  7. package/functions/computation-system-v2/config/bulltrackers.config.js +317 -0
  8. package/functions/computation-system-v2/framework/core/Computation.js +73 -0
  9. package/functions/computation-system-v2/framework/core/Manifest.js +223 -0
  10. package/functions/computation-system-v2/framework/core/RuleInjector.js +53 -0
  11. package/functions/computation-system-v2/framework/core/Rules.js +231 -0
  12. package/functions/computation-system-v2/framework/core/RunAnalyzer.js +163 -0
  13. package/functions/computation-system-v2/framework/cost/CostTracker.js +154 -0
  14. package/functions/computation-system-v2/framework/data/DataFetcher.js +399 -0
  15. package/functions/computation-system-v2/framework/data/QueryBuilder.js +232 -0
  16. package/functions/computation-system-v2/framework/data/SchemaRegistry.js +287 -0
  17. package/functions/computation-system-v2/framework/execution/Orchestrator.js +498 -0
  18. package/functions/computation-system-v2/framework/execution/TaskRunner.js +35 -0
  19. package/functions/computation-system-v2/framework/execution/middleware/CostTrackerMiddleware.js +32 -0
  20. package/functions/computation-system-v2/framework/execution/middleware/LineageMiddleware.js +32 -0
  21. package/functions/computation-system-v2/framework/execution/middleware/Middleware.js +14 -0
  22. package/functions/computation-system-v2/framework/execution/middleware/ProfilerMiddleware.js +47 -0
  23. package/functions/computation-system-v2/framework/index.js +45 -0
  24. package/functions/computation-system-v2/framework/lineage/LineageTracker.js +147 -0
  25. package/functions/computation-system-v2/framework/monitoring/Profiler.js +80 -0
  26. package/functions/computation-system-v2/framework/resilience/Checkpointer.js +66 -0
  27. package/functions/computation-system-v2/framework/scheduling/ScheduleValidator.js +327 -0
  28. package/functions/computation-system-v2/framework/storage/StateRepository.js +286 -0
  29. package/functions/computation-system-v2/framework/storage/StorageManager.js +469 -0
  30. package/functions/computation-system-v2/framework/storage/index.js +9 -0
  31. package/functions/computation-system-v2/framework/testing/ComputationTester.js +86 -0
  32. package/functions/computation-system-v2/framework/utils/Graph.js +205 -0
  33. package/functions/computation-system-v2/handlers/dispatcher.js +109 -0
  34. package/functions/computation-system-v2/handlers/index.js +23 -0
  35. package/functions/computation-system-v2/handlers/onDemand.js +289 -0
  36. package/functions/computation-system-v2/handlers/scheduler.js +327 -0
  37. package/functions/computation-system-v2/index.js +163 -0
  38. package/functions/computation-system-v2/rules/index.js +49 -0
  39. package/functions/computation-system-v2/rules/instruments.js +465 -0
  40. package/functions/computation-system-v2/rules/metrics.js +304 -0
  41. package/functions/computation-system-v2/rules/portfolio.js +534 -0
  42. package/functions/computation-system-v2/rules/rankings.js +655 -0
  43. package/functions/computation-system-v2/rules/social.js +562 -0
  44. package/functions/computation-system-v2/rules/trades.js +545 -0
  45. package/functions/computation-system-v2/scripts/migrate-sectors.js +73 -0
  46. package/functions/computation-system-v2/test/test-dispatcher.js +317 -0
  47. package/functions/computation-system-v2/test/test-framework.js +500 -0
  48. package/functions/computation-system-v2/test/test-real-execution.js +166 -0
  49. package/functions/computation-system-v2/test/test-real-integration.js +194 -0
  50. package/functions/computation-system-v2/test/test-refactor-e2e.js +131 -0
  51. package/functions/computation-system-v2/test/test-results.json +31 -0
  52. package/functions/computation-system-v2/test/test-risk-metrics-computation.js +329 -0
  53. package/functions/computation-system-v2/test/test-scheduler.js +204 -0
  54. package/functions/computation-system-v2/test/test-storage.js +449 -0
  55. package/functions/orchestrator/index.js +18 -26
  56. package/package.json +3 -2
@@ -0,0 +1,498 @@
1
+ /**
2
+ * @fileoverview Orchestrator (Refactored Executor)
3
+ * * Responsibilities:
4
+ * 1. Graph & Schedule Management (Manifest, Passes, DAG)
5
+ * 2. Data Provisioning (Fetching Data, Loading Dependencies, Reference Data)
6
+ * 3. Execution Strategy (Streaming vs. In-Memory)
7
+ * 4. Delegation (Hands off actual 'work' to TaskRunner + Middleware)
8
+ */
9
+
10
+ const crypto = require('crypto');
11
+ const pLimit = require('p-limit');
12
+
13
+ // Core Components
14
+ const { ManifestBuilder } = require('../core/Manifest');
15
+ const { RulesRegistry } = require('../core/Rules');
16
+ const { RuleInjector } = require('../core/RuleInjector');
17
+ const { RunAnalyzer } = require('../core/RunAnalyzer');
18
+
19
+ // Data & Storage
20
+ const { SchemaRegistry } = require('../data/SchemaRegistry');
21
+ const { QueryBuilder } = require('../data/QueryBuilder');
22
+ const { DataFetcher } = require('../data/DataFetcher');
23
+ const { StorageManager } = require('../storage/StorageManager');
24
+ const { StateRepository } = require('../storage/StateRepository');
25
+ const { Checkpointer } = require('../resilience/Checkpointer');
26
+
27
+ // Execution Components
28
+ const { TaskRunner } = require('./TaskRunner');
29
+ const { ProfilerMiddleware } = require('./middleware/ProfilerMiddleware');
30
+ const { CostTrackerMiddleware } = require('./middleware/CostTrackerMiddleware');
31
+ const { LineageMiddleware } = require('./middleware/LineageMiddleware');
32
+
33
+ const DEFAULT_CONCURRENCY = 20;
34
+ const BATCH_SIZE = 1000;
35
+
36
+ class Orchestrator {
37
+ constructor(config, logger = null) {
38
+ this.config = config;
39
+ this.logger = logger || console;
40
+
41
+ // 1. Initialize Base Services
42
+ this.schemaRegistry = new SchemaRegistry(config.bigquery, this.logger);
43
+ this.queryBuilder = new QueryBuilder(config.bigquery, this.schemaRegistry, this.logger);
44
+ this.dataFetcher = new DataFetcher({ ...config.bigquery, tables: config.tables }, this.queryBuilder, this.logger);
45
+ this.storageManager = new StorageManager(config, this.logger);
46
+ this.stateRepository = new StateRepository(config, this.logger);
47
+
48
+ // 2. Initialize Logic & Rules
49
+ this.manifestBuilder = new ManifestBuilder(config, this.logger);
50
+ const rulesRegistry = new RulesRegistry(config, this.logger);
51
+ this.ruleInjector = new RuleInjector(rulesRegistry);
52
+
53
+ // 3. Initialize Execution Stack (Middleware)
54
+ const profiler = new ProfilerMiddleware(config);
55
+ profiler.setStorage(this.storageManager);
56
+
57
+ this.lineageMiddleware = new LineageMiddleware(config);
58
+ const costTracker = new CostTrackerMiddleware(config);
59
+
60
+ // The "Onion": Cost( Lineage( Profiler( Task ) ) )
61
+ this.runner = new TaskRunner([
62
+ costTracker,
63
+ this.lineageMiddleware,
64
+ profiler
65
+ ]);
66
+
67
+ // State
68
+ this.manifest = null;
69
+ this.runAnalyzer = null;
70
+ this.referenceDataCache = {};
71
+ }
72
+
73
+ async initialize() {
74
+ this._log('INFO', 'Initializing Orchestrator...');
75
+
76
+ // Build Manifest
77
+ this.manifest = this.manifestBuilder.build(this.config.computations || []);
78
+
79
+ // Initialize Analyzer
80
+ this.runAnalyzer = new RunAnalyzer(this.manifest, this.dataFetcher, this.logger);
81
+
82
+ // Warm Schema Cache
83
+ await this.schemaRegistry.warmCache(this._getAllTables());
84
+
85
+ // Load Reference Data (e.g. sectors, holidays)
86
+ await this._loadReferenceData();
87
+
88
+ this._log('INFO', `Initialized with ${this.manifest.length} computations`);
89
+ }
90
+
91
+ /**
92
+ * Analyze what needs to run for a given date.
93
+ */
94
+ async analyze(options) {
95
+ const { date } = options;
96
+ if (!this.manifest) await this.initialize();
97
+
98
+ const dailyStatus = await this.stateRepository.getDailyStatus(date);
99
+ const prevStatus = await this.stateRepository.getDailyStatus(this._subtractDay(date));
100
+
101
+ const report = await this.runAnalyzer.analyze(date, dailyStatus, prevStatus);
102
+
103
+ // Compatibility: Merge reRuns into runnable
104
+ report.runnable = [...report.runnable, ...report.reRuns];
105
+ return report;
106
+ }
107
+
108
+ /**
109
+ * Main Execution Loop
110
+ */
111
+ async execute(options) {
112
+ const { date, pass = null, computation = null, dryRun = false, entities = null } = options;
113
+ if (!this.manifest) await this.initialize();
114
+
115
+ this._log('INFO', `Starting execution for ${date}...`);
116
+
117
+ // 1. Filter Manifest
118
+ let toRun = this.manifest;
119
+ if (computation) {
120
+ const norm = computation.toLowerCase().replace(/[^a-z0-9]/g, '');
121
+ toRun = this.manifest.filter(e => e.name === norm);
122
+ if (!toRun.length) throw new Error(`Computation not found: ${computation}`);
123
+ }
124
+
125
+ // 2. Group by Pass
126
+ const passes = this.manifestBuilder.groupByPass(toRun);
127
+ const passNumbers = Object.keys(passes).map(Number).sort((a,b) => a-b);
128
+ const passesToRun = pass ? [parseInt(pass, 10)] : passNumbers;
129
+
130
+ const summary = {
131
+ date,
132
+ summary: { completed: 0, skipped: 0, blocked: 0, impossible: 0, errors: 0 },
133
+ completed: [], skipped: [], blocked: [], impossible: [], errors: []
134
+ };
135
+
136
+ // 3. Execute Passes
137
+ for (const passNum of passesToRun) {
138
+ const passComputations = passes[passNum] || [];
139
+ this._log('INFO', `Executing Pass ${passNum}: ${passComputations.length} computations`);
140
+
141
+ // Note: In a strict DAG, items in the same pass are parallelizable.
142
+ // We use Promise.all to run them concurrently.
143
+ await Promise.all(passComputations.map(async (entry) => {
144
+ try {
145
+ // Pass specific options like "entities" (force entities) down
146
+ const res = await this._executeComputation(entry, date, { dryRun, entities });
147
+
148
+ summary[res.status].push(res);
149
+ summary.summary[res.status]++;
150
+ } catch (e) {
151
+ summary.errors.push({ name: entry.name, error: e.message });
152
+ summary.summary.errors++;
153
+ this._log('ERROR', `${entry.name} failed: ${e.message}`);
154
+ }
155
+ }));
156
+ }
157
+
158
+ return summary;
159
+ }
160
+
161
+ async runSingle(entry, dateStr, options = {}) {
162
+ if (!this.manifest) await this.initialize();
163
+ return this._executeComputation(entry, dateStr, {
164
+ dryRun: options.dryRun || false,
165
+ entities: options.entityIds
166
+ });
167
+ }
168
+
169
+ // =========================================================================
170
+ // INTERNAL EXECUTION LOGIC
171
+ // =========================================================================
172
+
173
+ async _executeComputation(entry, dateStr, options) {
174
+ const { name } = entry;
175
+ const forceEntities = options.entities;
176
+
177
+ // 1. Logic Check (Skip if unnecessary)
178
+ if (!forceEntities) {
179
+ const decision = await this._analyzeEntry(entry, dateStr);
180
+ if (decision.type !== 'runnable' && decision.type !== 'reRuns') {
181
+ return { name, status: decision.type, reason: decision.payload.reason };
182
+ }
183
+ }
184
+
185
+ this._log('INFO', `Running ${name} (Type: ${entry.type})...`);
186
+ const startTime = Date.now();
187
+
188
+ // 2. Load Dependencies & Previous Results
189
+ const { depResults, depResultHashes } = await this._loadDependencies(entry, dateStr);
190
+
191
+ let previousResult = null;
192
+ if (entry.isHistorical) {
193
+ previousResult = await this.stateRepository.getResult(this._subtractDay(dateStr), name);
194
+ }
195
+
196
+ // 3. Select Execution Strategy
197
+ let stats = { count: 0, hash: null, skipped: false };
198
+
199
+ if (entry.type === 'per-entity' && !forceEntities) {
200
+ // STRATEGY A: Streaming (Low Memory, Checkpointing)
201
+ stats = await this._executeStreaming(entry, dateStr, depResults, previousResult, options);
202
+ } else {
203
+ // STRATEGY B: In-Memory (Global, Aggregates, or Forced Entities)
204
+ stats = await this._executeGlobal(entry, dateStr, depResults, previousResult, options, forceEntities);
205
+ }
206
+
207
+ if (stats.skipped) {
208
+ return { name, status: 'skipped', reason: 'Results unchanged', duration: Date.now() - startTime };
209
+ }
210
+
211
+ // 4. Update State (If real run)
212
+ if (!options.dryRun) {
213
+ await this.stateRepository.updateStatusCache(dateStr, name, {
214
+ hash: entry.hash,
215
+ resultHash: stats.hash,
216
+ dependencyResultHashes: depResultHashes,
217
+ entityCount: stats.count
218
+ });
219
+
220
+ // Flush any buffered lineage logs
221
+ await this.lineageMiddleware.flush();
222
+ }
223
+
224
+ return { name, status: 'completed', duration: Date.now() - startTime, resultCount: stats.count };
225
+ }
226
+
227
+ // --- STRATEGY A: STREAMING ---
228
+ async _executeStreaming(entry, dateStr, depResults, previousResult, options) {
229
+ // 1. Setup Checkpoint
230
+ const checkpointer = new Checkpointer(this.config, this.storageManager);
231
+ let cp = null;
232
+ if (!options.dryRun) {
233
+ cp = await checkpointer.initCheckpoint(dateStr, entry.name, 0); // 0 = unknown total
234
+ if (cp?.isCompleted) return { count: 0, hash: 'cached', skipped: true };
235
+ if (cp?.isResumed) this._log('INFO', `Resuming ${entry.name} from checkpoint...`);
236
+ }
237
+
238
+ // 2. Initialize Stream
239
+ const batchSize = this.config.execution?.batchSize || BATCH_SIZE;
240
+ const batchStream = this.dataFetcher.fetchComputationBatched(entry.requires, dateStr, batchSize);
241
+
242
+ const rollingHash = crypto.createHash('sha256');
243
+ let totalCount = 0;
244
+ let batchIndex = 0;
245
+
246
+ const concurrency = this.config.execution?.entityConcurrency || DEFAULT_CONCURRENCY;
247
+ const limit = pLimit(concurrency);
248
+
249
+ // 3. Iterate Batches
250
+ for await (const batch of batchStream) {
251
+ // Resume Logic: Skip completed batches
252
+ if (cp && cp.completedBatches.has(batchIndex)) {
253
+ batchIndex++;
254
+ continue;
255
+ }
256
+
257
+ const { data: batchData, entityIds } = batch;
258
+
259
+ // 4. PREFETCH DEPENDENCIES
260
+ const batchDeps = await this._prefetchBatchDependencies(entry, dateStr, depResults, entityIds);
261
+
262
+ // 5. Dynamic Context Injection
263
+ const { rules } = this.ruleInjector.createContext(); // Used is implicit via Proxy
264
+
265
+ // 6. Execute Batch Concurrently
266
+ const batchResults = {};
267
+
268
+ await Promise.all(entityIds.map(entityId => limit(async () => {
269
+ const instance = new entry.class();
270
+ const entityData = this._filterDataForEntity(batchData, entityId);
271
+
272
+ const context = {
273
+ computation: entry,
274
+ date: dateStr,
275
+ entityId,
276
+ data: entityData,
277
+
278
+ // Dependency Injector
279
+ getDependency: (depName, targetId) => {
280
+ if (batchDeps[depName] && batchDeps[depName].has(targetId || entityId)) {
281
+ return batchDeps[depName].get(targetId || entityId);
282
+ }
283
+ return this._lazyLoadDependency(dateStr, depName, targetId || entityId, depResults);
284
+ },
285
+
286
+ previousResult,
287
+ rules,
288
+ references: this.referenceDataCache,
289
+ config: this.config,
290
+ dataFetcher: this.dataFetcher // <--- ADDED: Required by CostTrackerMiddleware
291
+ };
292
+
293
+ // DELEGATE TO RUNNER
294
+ const result = await this.runner.run(instance, context);
295
+
296
+ if (result !== undefined) {
297
+ batchResults[entityId] = result;
298
+ this._updateRollingHash(rollingHash, result);
299
+ }
300
+ })));
301
+
302
+ // 7. Commit Batch
303
+ if (!options.dryRun) {
304
+ await this.storageManager.commitResults(dateStr, entry, batchResults, {});
305
+ const lastId = entityIds[entityIds.length - 1];
306
+ await checkpointer.markBatchComplete(dateStr, entry.name, cp?.id, batchIndex, batchSize, lastId);
307
+ }
308
+
309
+ totalCount += Object.keys(batchResults).length;
310
+ batchIndex++;
311
+ }
312
+
313
+ if (!options.dryRun && cp) await checkpointer.complete(dateStr, entry.name, cp.id);
314
+
315
+ return { count: totalCount, hash: rollingHash.digest('hex').substring(0, 16) };
316
+ }
317
+
318
+ // --- STRATEGY B: GLOBAL / IN-MEMORY ---
319
+ async _executeGlobal(entry, dateStr, depResults, previousResult, options, forceEntities) {
320
+ // 1. Fetch Full Data
321
+ const data = await this.dataFetcher.fetchForComputation(entry.requires, dateStr, forceEntities);
322
+ const { rules } = this.ruleInjector.createContext();
323
+
324
+ const instance = new entry.class();
325
+ const context = {
326
+ computation: entry,
327
+ date: dateStr,
328
+ data,
329
+ getDependency: (dep, ent) => this._lazyLoadDependency(dateStr, dep, ent, depResults),
330
+ previousResult,
331
+ rules,
332
+ references: this.referenceDataCache,
333
+ config: this.config,
334
+ entityId: forceEntities ? null : '_global',
335
+ dataFetcher: this.dataFetcher // <--- ADDED: Required by CostTrackerMiddleware
336
+ };
337
+
338
+ // 2. Delegate to Runner
339
+ let results = {};
340
+
341
+ if (entry.type === 'per-entity') {
342
+ const ids = forceEntities || this._extractEntityIds(data);
343
+ const limit = pLimit(DEFAULT_CONCURRENCY);
344
+
345
+ await Promise.all(ids.map(id => limit(async () => {
346
+ const subCtx = {
347
+ ...context,
348
+ entityId: id,
349
+ data: this._filterDataForEntity(data, id)
350
+ };
351
+ const res = await this.runner.run(instance, subCtx);
352
+ if (res) results[id] = res;
353
+ })));
354
+ } else {
355
+ results = await this.runner.run(instance, context);
356
+ }
357
+
358
+ // 3. Smart Invalidation Check
359
+ const finalHash = this._hashResults(results);
360
+
361
+ if (!options.dryRun && !forceEntities) {
362
+ const currentStatus = await this.stateRepository.getDailyStatus(dateStr);
363
+ const status = currentStatus.get(entry.name.toLowerCase());
364
+
365
+ if (status && status.resultHash === finalHash) {
366
+ return { count: Object.keys(results || {}).length, hash: finalHash, skipped: true };
367
+ }
368
+
369
+ await this.storageManager.commitResults(dateStr, entry, results, {});
370
+ }
371
+
372
+ return { count: Object.keys(results || {}).length, hash: finalHash };
373
+ }
374
+
375
+ // =========================================================================
376
+ // HELPER METHODS
377
+ // =========================================================================
378
+
379
+ async _analyzeEntry(entry, dateStr) {
380
+ const d = await this.stateRepository.getDailyStatus(dateStr);
381
+ const p = await this.stateRepository.getDailyStatus(this._subtractDay(dateStr));
382
+ return this.runAnalyzer._evaluateEntry(entry, dateStr, false, d, p);
383
+ }
384
+
385
+ async _loadDependencies(entry, dateStr) {
386
+ const depResults = {};
387
+ const depResultHashes = {};
388
+ const dailyStatus = await this.stateRepository.getDailyStatus(dateStr);
389
+
390
+ for (const dep of entry.dependencies) {
391
+ const stat = dailyStatus.get(dep);
392
+ if (stat?.resultHash) depResultHashes[dep] = stat.resultHash;
393
+
394
+ if (stat?.entityCount > 50000) {
395
+ depResults[dep] = null;
396
+ } else {
397
+ depResults[dep] = await this.stateRepository.getResult(dateStr, dep);
398
+ }
399
+ }
400
+
401
+ if (entry.conditionalDependencies) {
402
+ for (const condDep of entry.conditionalDependencies) {
403
+ const shouldLoad = condDep.condition({ date: dateStr, config: this.config });
404
+ if (shouldLoad) {
405
+ const depStatus = dailyStatus.get(condDep.computation.toLowerCase());
406
+ if (depStatus) {
407
+ depResults[condDep.computation] = await this.stateRepository.getResult(dateStr, condDep.computation);
408
+ depResultHashes[condDep.computation] = depStatus.resultHash;
409
+ }
410
+ }
411
+ }
412
+ }
413
+
414
+ return { depResults, depResultHashes };
415
+ }
416
+
417
+ async _prefetchBatchDependencies(entry, dateStr, loadedDeps, batchEntityIds) {
418
+ const prefetched = {};
419
+
420
+ for (const depName of entry.dependencies) {
421
+ if (loadedDeps[depName] === null) {
422
+ const batchRes = await this.stateRepository.getBatchEntityResults(dateStr, depName, batchEntityIds);
423
+ prefetched[depName] = new Map(Object.entries(batchRes));
424
+ }
425
+ }
426
+ return prefetched;
427
+ }
428
+
429
+ async _lazyLoadDependency(dateStr, depName, entityId, preloaded) {
430
+ if (preloaded[depName] && !entityId) return preloaded[depName];
431
+ if (preloaded[depName] && entityId) return preloaded[depName][entityId];
432
+
433
+ if (entityId) {
434
+ return this.stateRepository.getEntityResult(dateStr, depName, entityId);
435
+ }
436
+ return this.stateRepository.getResult(dateStr, depName);
437
+ }
438
+
439
+ async _loadReferenceData() {
440
+ if (!this.config.referenceData) return;
441
+ await Promise.all(this.config.referenceData.map(async (table) => {
442
+ try {
443
+ const data = await this.dataFetcher.fetch({
444
+ table,
445
+ targetDate: new Date().toISOString().slice(0, 10),
446
+ mandatory: false
447
+ });
448
+ this.referenceDataCache[table] = data || {};
449
+ } catch (e) {
450
+ this._log('WARN', `Failed to load Ref Data ${table}: ${e.message}`);
451
+ }
452
+ }));
453
+ }
454
+
455
+ _extractEntityIds(data) {
456
+ const ids = new Set();
457
+ Object.entries(data).forEach(([tbl, d]) => {
458
+ const conf = this.config.tables[tbl] || {};
459
+ if (conf.entityField && d && !Array.isArray(d)) Object.keys(d).forEach(k => ids.add(k));
460
+ });
461
+ return Array.from(ids);
462
+ }
463
+
464
+ _filterDataForEntity(data, id) {
465
+ const out = {};
466
+ Object.entries(data).forEach(([tbl, d]) => {
467
+ const conf = this.config.tables[tbl] || {};
468
+ if (conf.entityField && d && !Array.isArray(d)) out[tbl] = d[id] || null;
469
+ else out[tbl] = d;
470
+ });
471
+ return out;
472
+ }
473
+
474
+ _updateRollingHash(hasher, result) {
475
+ if (result) hasher.update(JSON.stringify(result));
476
+ }
477
+
478
+ _hashResults(results) {
479
+ const canonical = JSON.stringify(results, Object.keys(results || {}).sort());
480
+ return crypto.createHash('sha256').update(canonical).digest('hex').substring(0, 16);
481
+ }
482
+
483
+ _subtractDay(dateStr) {
484
+ const d = new Date(dateStr + 'T00:00:00Z');
485
+ d.setUTCDate(d.getUTCDate() - 1);
486
+ return d.toISOString().slice(0, 10);
487
+ }
488
+
489
+ _getAllTables() {
490
+ const s = new Set();
491
+ if (this.manifest) this.manifest.forEach(e => Object.keys(e.requires).forEach(t => s.add(t)));
492
+ return Array.from(s);
493
+ }
494
+
495
+ _log(l, m) { this.logger.log(l, `[Orchestrator] ${m}`); }
496
+ }
497
+
498
+ module.exports = { Orchestrator };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @fileoverview Task Runner
3
+ * Executes a single unit of work (global or per-entity) through a middleware chain.
4
+ */
5
+
6
+ class TaskRunner {
7
+ constructor(middlewares = []) {
8
+ this.middlewares = middlewares;
9
+ }
10
+
11
+ /**
12
+ * Run the computation instance process method wrapped in middleware.
13
+ * @param {Object} instance - The computation class instance
14
+ * @param {Object} context - The execution context
15
+ */
16
+ async run(instance, context) {
17
+ // The Core Kernel: The actual computation logic
18
+ const coreKernel = async (ctx) => {
19
+ await instance.process(ctx);
20
+ // Retrieve result based on mode
21
+ if (ctx.entityId) return instance.results[ctx.entityId];
22
+ return instance.results;
23
+ };
24
+
25
+ // Compose Middleware: Reduce Right (Inner -> Outer)
26
+ const chain = this.middlewares.reduceRight((next, mw) => {
27
+ return async (ctx) => mw.execute(ctx, next);
28
+ }, coreKernel);
29
+
30
+ // Execute
31
+ return await chain(context);
32
+ }
33
+ }
34
+
35
+ module.exports = { TaskRunner };
@@ -0,0 +1,32 @@
1
+ const { Middleware } = require('./Middleware');
2
+ const { CostTracker } = require('../../cost/CostTracker');
3
+
4
+ class CostTrackerMiddleware extends Middleware {
5
+ constructor(config) {
6
+ super();
7
+ this.tracker = new CostTracker(config);
8
+ }
9
+
10
+ async execute(context, next) {
11
+ const { computation, date, dataFetcher } = context;
12
+
13
+ // Snapshot bytes before
14
+ const startBytes = dataFetcher.getStats().bytesProcessed || 0;
15
+
16
+ const result = await next(context);
17
+
18
+ // Snapshot bytes after
19
+ const endBytes = dataFetcher.getStats().bytesProcessed || 0;
20
+ const delta = endBytes - startBytes;
21
+
22
+ if (delta > 0) {
23
+ // Fire and forget
24
+ this.tracker.trackCost(computation.name, date, delta)
25
+ .catch(e => console.error('Cost tracking failed', e));
26
+ }
27
+
28
+ return result;
29
+ }
30
+ }
31
+
32
+ module.exports = { CostTrackerMiddleware };
@@ -0,0 +1,32 @@
1
+ const { Middleware } = require('./Middleware');
2
+ const { LineageTracker } = require('../../lineage/LineageTracker');
3
+
4
+ class LineageMiddleware extends Middleware {
5
+ constructor(config) {
6
+ super();
7
+ this.tracker = new LineageTracker(config);
8
+ }
9
+
10
+ async execute(context, next) {
11
+ const result = await next(context);
12
+
13
+ // Only track if we have a valid result and entity
14
+ if (result && context.entityId) {
15
+ this.tracker.track({
16
+ computation: context.computation.name,
17
+ date: context.date,
18
+ entityId: context.entityId,
19
+ sourceData: context.data, // The slice of data used
20
+ result: result
21
+ }).catch(e => console.error('Lineage tracking failed', e));
22
+ }
23
+
24
+ return result;
25
+ }
26
+
27
+ async flush() {
28
+ await this.tracker.flush();
29
+ }
30
+ }
31
+
32
+ module.exports = { LineageMiddleware };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @fileoverview Middleware Interface
3
+ */
4
+ class Middleware {
5
+ /**
6
+ * @param {Object} context - The execution context
7
+ * @param {Function} next - The next function in the chain
8
+ */
9
+ async execute(context, next) {
10
+ return await next(context);
11
+ }
12
+ }
13
+
14
+ module.exports = { Middleware };
@@ -0,0 +1,47 @@
1
+ const { Middleware } = require('./Middleware');
2
+ const { ComputationProfiler } = require('../../monitoring/Profiler');
3
+
4
+ class ProfilerMiddleware extends Middleware {
5
+ constructor(config) {
6
+ super();
7
+ this.profiler = new ComputationProfiler();
8
+ this.storageManager = null; // Injected by Orchestrator
9
+ }
10
+
11
+ setStorage(storageManager) {
12
+ this.storageManager = storageManager;
13
+ }
14
+
15
+ async execute(context, next) {
16
+ const { computation, entityId, date } = context;
17
+
18
+ // Start Profile
19
+ const key = this.profiler.startProfile(computation.name, entityId || 'global');
20
+
21
+ try {
22
+ // Run Next
23
+ const result = await next(context);
24
+ return result;
25
+ } finally {
26
+ // End Profile (runs even if error)
27
+ const resultSize = context.results ? JSON.stringify(context.results).length : 0;
28
+ const profile = this.profiler.endProfile(key, {
29
+ entityId: entityId || 'global',
30
+ resultSize
31
+ });
32
+
33
+ // Persist Profile if storage available
34
+ if (this.storageManager && profile) {
35
+ // Async save (don't block)
36
+ this.storageManager.savePerformanceReport(date, {
37
+ computations: [{
38
+ name: computation.name,
39
+ ...profile
40
+ }]
41
+ }).catch(err => console.error('Failed to save profile', err));
42
+ }
43
+ }
44
+ }
45
+ }
46
+
47
+ module.exports = { ProfilerMiddleware };