bulltrackers-module 1.0.766 → 1.0.769

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 (76) hide show
  1. package/functions/computation-system-v2/UserPortfolioMetrics.js +50 -0
  2. package/functions/computation-system-v2/computations/BehavioralAnomaly.js +559 -227
  3. package/functions/computation-system-v2/computations/GlobalAumPerAsset30D.js +103 -0
  4. package/functions/computation-system-v2/computations/NewSectorExposure.js +82 -35
  5. package/functions/computation-system-v2/computations/NewSocialPost.js +52 -24
  6. package/functions/computation-system-v2/computations/PIDailyAssetAUM.js +134 -0
  7. package/functions/computation-system-v2/computations/PiFeatureVectors.js +227 -0
  8. package/functions/computation-system-v2/computations/PiRecommender.js +359 -0
  9. package/functions/computation-system-v2/computations/PopularInvestorProfileMetrics.js +354 -641
  10. package/functions/computation-system-v2/computations/SignedInUserList.js +51 -0
  11. package/functions/computation-system-v2/computations/SignedInUserMirrorHistory.js +138 -0
  12. package/functions/computation-system-v2/computations/SignedInUserPIProfileMetrics.js +106 -0
  13. package/functions/computation-system-v2/computations/SignedInUserProfileMetrics.js +324 -0
  14. package/functions/computation-system-v2/config/bulltrackers.config.js +40 -126
  15. package/functions/computation-system-v2/core-api.js +17 -9
  16. package/functions/computation-system-v2/data_schema_reference.MD +108 -0
  17. package/functions/computation-system-v2/devtools/builder/builder.js +362 -0
  18. package/functions/computation-system-v2/devtools/builder/examples/user-metrics.yaml +26 -0
  19. package/functions/computation-system-v2/devtools/index.js +36 -0
  20. package/functions/computation-system-v2/devtools/shared/MockDataFactory.js +235 -0
  21. package/functions/computation-system-v2/devtools/shared/SchemaTemplates.js +475 -0
  22. package/functions/computation-system-v2/devtools/shared/SystemIntrospector.js +517 -0
  23. package/functions/computation-system-v2/devtools/shared/index.js +16 -0
  24. package/functions/computation-system-v2/devtools/simulation/DAGAnalyzer.js +243 -0
  25. package/functions/computation-system-v2/devtools/simulation/MockDataFetcher.js +306 -0
  26. package/functions/computation-system-v2/devtools/simulation/MockStorageManager.js +336 -0
  27. package/functions/computation-system-v2/devtools/simulation/SimulationEngine.js +525 -0
  28. package/functions/computation-system-v2/devtools/simulation/SimulationServer.js +581 -0
  29. package/functions/computation-system-v2/devtools/simulation/index.js +17 -0
  30. package/functions/computation-system-v2/devtools/simulation/simulate.js +324 -0
  31. package/functions/computation-system-v2/devtools/vscode-computation/package.json +90 -0
  32. package/functions/computation-system-v2/devtools/vscode-computation/snippets/computation.json +128 -0
  33. package/functions/computation-system-v2/devtools/vscode-computation/src/extension.ts +401 -0
  34. package/functions/computation-system-v2/devtools/vscode-computation/src/providers/codeActions.ts +152 -0
  35. package/functions/computation-system-v2/devtools/vscode-computation/src/providers/completions.ts +207 -0
  36. package/functions/computation-system-v2/devtools/vscode-computation/src/providers/diagnostics.ts +205 -0
  37. package/functions/computation-system-v2/devtools/vscode-computation/src/providers/hover.ts +205 -0
  38. package/functions/computation-system-v2/devtools/vscode-computation/tsconfig.json +22 -0
  39. package/functions/computation-system-v2/docs/HowToCreateComputations.MD +602 -0
  40. package/functions/computation-system-v2/framework/core/Manifest.js +9 -16
  41. package/functions/computation-system-v2/framework/core/RunAnalyzer.js +2 -1
  42. package/functions/computation-system-v2/framework/data/DataFetcher.js +330 -126
  43. package/functions/computation-system-v2/framework/data/MaterializedViewManager.js +84 -0
  44. package/functions/computation-system-v2/framework/data/QueryBuilder.js +38 -38
  45. package/functions/computation-system-v2/framework/execution/Orchestrator.js +226 -153
  46. package/functions/computation-system-v2/framework/scheduling/ScheduleValidator.js +17 -19
  47. package/functions/computation-system-v2/framework/storage/StateRepository.js +32 -2
  48. package/functions/computation-system-v2/framework/storage/StorageManager.js +111 -83
  49. package/functions/computation-system-v2/framework/testing/ComputationTester.js +161 -66
  50. package/functions/computation-system-v2/handlers/dispatcher.js +57 -29
  51. package/functions/computation-system-v2/legacy/PiAssetRecommender.js.old +115 -0
  52. package/functions/computation-system-v2/legacy/PiSimilarityMatrix.js +104 -0
  53. package/functions/computation-system-v2/legacy/PiSimilarityVector.js +71 -0
  54. package/functions/computation-system-v2/scripts/debug_aggregation.js +25 -0
  55. package/functions/computation-system-v2/scripts/test-computation-dag.js +109 -0
  56. package/functions/computation-system-v2/scripts/test-invalidation-scenarios.js +234 -0
  57. package/functions/task-engine/helpers/data_storage_helpers.js +6 -6
  58. package/package.json +1 -1
  59. package/functions/computation-system-v2/computations/PopularInvestorRiskAssessment.js +0 -176
  60. package/functions/computation-system-v2/computations/PopularInvestorRiskMetrics.js +0 -294
  61. package/functions/computation-system-v2/computations/UserPortfolioSummary.js +0 -172
  62. package/functions/computation-system-v2/scripts/migrate-sectors.js +0 -73
  63. package/functions/computation-system-v2/test/analyze-results.js +0 -238
  64. package/functions/computation-system-v2/test/other/test-dependency-cascade.js +0 -150
  65. package/functions/computation-system-v2/test/other/test-dispatcher.js +0 -317
  66. package/functions/computation-system-v2/test/other/test-framework.js +0 -500
  67. package/functions/computation-system-v2/test/other/test-real-execution.js +0 -166
  68. package/functions/computation-system-v2/test/other/test-real-integration.js +0 -194
  69. package/functions/computation-system-v2/test/other/test-refactor-e2e.js +0 -131
  70. package/functions/computation-system-v2/test/other/test-results.json +0 -31
  71. package/functions/computation-system-v2/test/other/test-risk-metrics-computation.js +0 -329
  72. package/functions/computation-system-v2/test/other/test-scheduler.js +0 -204
  73. package/functions/computation-system-v2/test/other/test-storage.js +0 -449
  74. package/functions/computation-system-v2/test/run-pipeline-test.js +0 -554
  75. package/functions/computation-system-v2/test/test-full-pipeline.js +0 -227
  76. package/functions/computation-system-v2/test/test-worker-pool.js +0 -266
@@ -0,0 +1,602 @@
1
+ # Developing Computations for the BullTrackers DAG System
2
+
3
+ ## Overview
4
+
5
+ The computation system is a **directed acyclic graph (DAG)** of data transformations that run on BigQuery data. Each computation is a self-contained class that declares its data requirements, dependencies, and processing logic.
6
+
7
+ ---
8
+
9
+ ## Core Concepts
10
+
11
+ ### 1. **Computation Types**
12
+
13
+ ```javascript
14
+ type: 'per-entity' // Runs once per entity (user, asset, etc.)
15
+ type: 'global' // Runs once for all data (aggregations, rankings)
16
+ ```
17
+
18
+ ### 2. **Result Keys**
19
+
20
+ ```javascript
21
+ // Per-entity computations
22
+ this.setResult(entityId, { ...data });
23
+
24
+ // Global computations
25
+ this.setResult('_global', { ...data });
26
+ ```
27
+
28
+ **Critical:** Global computations must use `'_global'` as the key, not `'global'`.
29
+
30
+ ---
31
+
32
+ ## Basic Computation Structure
33
+
34
+ ```javascript
35
+ const { Computation } = require('../framework');
36
+
37
+ class MyComputation extends Computation {
38
+ static getConfig() {
39
+ return {
40
+ name: 'MyComputation',
41
+ type: 'per-entity', // or 'global'
42
+ category: 'analytics',
43
+ isHistorical: false, // true if needs previous day's data
44
+
45
+ requires: {
46
+ 'table_name': {
47
+ lookback: 0, // days to fetch (0 = today only)
48
+ mandatory: true,
49
+ fields: ['col1', 'col2'], // REQUIRED for cost control
50
+ filter: { user_type: 'POPULAR_INVESTOR' }
51
+ }
52
+ },
53
+
54
+ dependencies: [], // Other computations needed
55
+
56
+ storage: {
57
+ bigquery: true,
58
+ firestore: {
59
+ enabled: true,
60
+ path: 'results/{entityId}',
61
+ merge: true
62
+ }
63
+ }
64
+ };
65
+ }
66
+
67
+ async process(context) {
68
+ const { data, entityId, date, rules, references } = context;
69
+
70
+ // Your logic here
71
+ const result = { /* ... */ };
72
+
73
+ this.setResult(entityId, result);
74
+ }
75
+ }
76
+
77
+ module.exports = MyComputation;
78
+ ```
79
+
80
+ ---
81
+
82
+ ## Cost Optimization Rules
83
+
84
+ ### **1. Always Specify Fields**
85
+
86
+ ```javascript
87
+ // ❌ BAD - Will throw LAZY SELECT BLOCKED error
88
+ requires: {
89
+ 'portfolio_snapshots': {
90
+ fields: [] // or null or '*'
91
+ }
92
+ }
93
+
94
+ // ✅ GOOD - Only fetch what you need
95
+ requires: {
96
+ 'portfolio_snapshots': {
97
+ fields: ['user_id', 'portfolio_data', 'date']
98
+ }
99
+ }
100
+ ```
101
+
102
+ ### **2. Always Use Partitioning**
103
+
104
+ ```javascript
105
+ // ❌ BAD - Full table scan
106
+ requires: {
107
+ 'portfolio_snapshots': {
108
+ lookback: 0 // Missing! Will throw UNSAFE QUERY error
109
+ }
110
+ }
111
+
112
+ // ✅ GOOD - Date partition filter
113
+ requires: {
114
+ 'portfolio_snapshots': {
115
+ lookback: 0, // Today only
116
+ fields: ['user_id', 'portfolio_data', 'date']
117
+ }
118
+ }
119
+ ```
120
+
121
+ ### **3. Limit Lookback**
122
+
123
+ ```javascript
124
+ // Maximum enforced: 60 days
125
+ lookback: 30 // 30 days of history (max enforced: 60)
126
+ ```
127
+
128
+ ### **4. Use Filters to Reduce Data**
129
+
130
+ ```javascript
131
+ requires: {
132
+ 'portfolio_snapshots': {
133
+ lookback: 0,
134
+ fields: ['user_id', 'portfolio_data', 'date'],
135
+ filter: {
136
+ user_type: 'POPULAR_INVESTOR' // Reduces rows significantly
137
+ }
138
+ }
139
+ }
140
+ ```
141
+
142
+ ---
143
+
144
+ ## Materialized Views
145
+
146
+ ### **Overview**
147
+
148
+ Materialized views enable efficient **metric-based queries** without requiring custom SQL. The system **automatically creates and manages** them.
149
+
150
+ ### **Requesting a Materialized View**
151
+
152
+ ```javascript
153
+ requires: {
154
+ 'aum_metric': {
155
+ type: 'metric', // Triggers MV creation
156
+ source: 'pi_rankings', // Base table
157
+ func: 'SUM', // Aggregation function
158
+ field: 'rankings_data', // Column to aggregate
159
+ jsonPath: '$.AUMValue', // JSON path (if needed)
160
+ lookback: 7,
161
+ series: true // true = time series, false = single value
162
+ }
163
+ }
164
+ ```
165
+
166
+ ### **What Happens**
167
+
168
+ 1. System generates MV name: `mv_pi_rankings_sum_rankings_data_by_day`
169
+ 2. Automatically creates MV with schema:
170
+ ```sql
171
+ CREATE MATERIALIZED VIEW mv_...
172
+ AS SELECT
173
+ entity_id,
174
+ date,
175
+ SUM(CAST(JSON_VALUE(field, '$.path') AS FLOAT64)) as value
176
+ FROM base_table
177
+ GROUP BY entity_id, date
178
+ ```
179
+ 3. Auto-refreshes every 60 minutes
180
+
181
+ ### **Accessing Results**
182
+
183
+ ```javascript
184
+ async process(context) {
185
+ const { data } = context;
186
+
187
+ // Series mode (series: true)
188
+ // Returns: { "entityId": { "2024-01-01": 123, "2024-01-02": 456 } }
189
+ const aumTimeSeries = data['aum_metric'];
190
+ const todayAum = aumTimeSeries[entityId][date];
191
+
192
+ // Single value mode (series: false)
193
+ // Returns: { "entityId": 123 }
194
+ const totalAum = data['aum_metric'][entityId];
195
+ }
196
+ ```
197
+
198
+ ### **Supported Functions**
199
+
200
+ - `SUM`, `AVG`, `MIN`, `MAX`, `COUNT`
201
+
202
+ ---
203
+
204
+ ## Dependencies Between Computations
205
+
206
+ ### **Basic Dependency**
207
+
208
+ ```javascript
209
+ class DownstreamComputation extends Computation {
210
+ static getConfig() {
211
+ return {
212
+ name: 'DownstreamComputation',
213
+ dependencies: ['UpstreamComputation'],
214
+ requires: {
215
+ 'computation_results': {
216
+ lookback: 0,
217
+ filter: {
218
+ computation_name: 'upstreamcomputation' // lowercase!
219
+ }
220
+ }
221
+ }
222
+ };
223
+ }
224
+
225
+ async process(context) {
226
+ const { getDependency, entityId } = context;
227
+
228
+ // Access dependency result
229
+ const upstreamResult = getDependency('UpstreamComputation', entityId);
230
+
231
+ // Use it
232
+ const value = upstreamResult.someField;
233
+ }
234
+ }
235
+ ```
236
+
237
+ ### **Conditional Dependencies**
238
+
239
+ ```javascript
240
+ conditionalDependencies: [
241
+ {
242
+ computation: 'SeasonalAdjustment',
243
+ condition: ({ date }) => {
244
+ const month = new Date(date).getMonth();
245
+ return month === 0; // Only in January
246
+ }
247
+ }
248
+ ]
249
+ ```
250
+
251
+ ---
252
+
253
+ ## Example Computations
254
+
255
+ ### **Example 1: Per-Entity Alert**
256
+
257
+ ```javascript
258
+ class RiskScoreIncrease extends Computation {
259
+ static getConfig() {
260
+ return {
261
+ name: 'RiskScoreIncrease',
262
+ type: 'per-entity',
263
+ category: 'alerts',
264
+ isHistorical: true, // Needs yesterday's data
265
+
266
+ requires: {
267
+ 'pi_rankings': {
268
+ lookback: 1, // Today + yesterday
269
+ mandatory: true,
270
+ fields: ['pi_id', 'rankings_data', 'date'],
271
+ filter: { user_type: 'POPULAR_INVESTOR' }
272
+ }
273
+ },
274
+
275
+ storage: {
276
+ bigquery: true,
277
+ firestore: {
278
+ enabled: true,
279
+ path: 'alerts/{date}/RiskScoreIncrease/{entityId}',
280
+ merge: true
281
+ }
282
+ }
283
+ };
284
+ }
285
+
286
+ async process(context) {
287
+ const { data, entityId, date, rules } = context;
288
+
289
+ const history = data['pi_rankings'] || [];
290
+ const todayRow = history.find(d => d.date === date);
291
+ const yesterdayRow = history.find(d => d.date === this._yesterday(date));
292
+
293
+ const todayRisk = rules.rankings.getRiskScore(
294
+ rules.rankings.extractRankingsData(todayRow)
295
+ );
296
+ const yesterdayRisk = rules.rankings.getRiskScore(
297
+ rules.rankings.extractRankingsData(yesterdayRow)
298
+ );
299
+
300
+ const change = todayRisk - (yesterdayRisk || 0);
301
+
302
+ this.setResult(entityId, {
303
+ currentRisk: todayRisk,
304
+ previousRisk: yesterdayRisk,
305
+ change,
306
+ triggered: change > 0
307
+ });
308
+ }
309
+
310
+ _yesterday(dateStr) {
311
+ const d = new Date(dateStr);
312
+ d.setDate(d.getDate() - 1);
313
+ return d.toISOString().slice(0, 10);
314
+ }
315
+ }
316
+ ```
317
+
318
+ ### **Example 2: Global Aggregation**
319
+
320
+ ```javascript
321
+ class GlobalAumPerAsset extends Computation {
322
+ static getConfig() {
323
+ return {
324
+ name: 'GlobalAumPerAsset',
325
+ type: 'global',
326
+ category: 'market_insights',
327
+
328
+ requires: {
329
+ 'computation_results': {
330
+ lookback: 0,
331
+ mandatory: true,
332
+ fields: ['result_data', 'computation_name', 'date'],
333
+ filter: {
334
+ computation_name: 'pidailyassetaum'
335
+ }
336
+ }
337
+ },
338
+
339
+ dependencies: ['PIDailyAssetAUM'],
340
+
341
+ storage: {
342
+ bigquery: true,
343
+ firestore: {
344
+ enabled: true,
345
+ path: 'global_metrics/aum_per_asset_{date}',
346
+ merge: true
347
+ }
348
+ }
349
+ };
350
+ }
351
+
352
+ async process(context) {
353
+ const { data } = context;
354
+
355
+ let rows = data['computation_results'];
356
+ if (!Array.isArray(rows)) {
357
+ rows = Object.values(rows);
358
+ }
359
+
360
+ const globalTotals = new Map();
361
+
362
+ for (const row of rows) {
363
+ let assetMap = row.result_data;
364
+ if (typeof assetMap === 'string') {
365
+ assetMap = JSON.parse(assetMap);
366
+ }
367
+
368
+ for (const [ticker, value] of Object.entries(assetMap)) {
369
+ const current = globalTotals.get(ticker) || 0;
370
+ globalTotals.set(ticker, current + value);
371
+ }
372
+ }
373
+
374
+ const result = Array.from(globalTotals.entries())
375
+ .map(([symbol, amount]) => ({ symbol, amount }))
376
+ .sort((a, b) => b.amount - a.amount);
377
+
378
+ this.setResult('_global', result); // Note: _global, not global
379
+ }
380
+ }
381
+ ```
382
+
383
+ ### **Example 3: Using Materialized Views**
384
+
385
+ ```javascript
386
+ class PortfolioVelocity extends Computation {
387
+ static getConfig() {
388
+ return {
389
+ name: 'PortfolioVelocity',
390
+ type: 'per-entity',
391
+ category: 'analytics',
392
+
393
+ requires: {
394
+ 'total_exposure_metric': {
395
+ type: 'metric',
396
+ source: 'portfolio_snapshots',
397
+ func: 'SUM',
398
+ field: 'portfolio_data',
399
+ jsonPath: '$.exposure',
400
+ lookback: 7,
401
+ series: true // Get 7 days of data
402
+ }
403
+ },
404
+
405
+ storage: { bigquery: true }
406
+ };
407
+ }
408
+
409
+ async process(context) {
410
+ const { data, entityId, date } = context;
411
+
412
+ const exposureSeries = data['total_exposure_metric'][entityId] || {};
413
+ const dates = Object.keys(exposureSeries).sort();
414
+
415
+ if (dates.length < 2) {
416
+ return this.setResult(entityId, { velocity: 0 });
417
+ }
418
+
419
+ // Calculate rate of change
420
+ const values = dates.map(d => exposureSeries[d]);
421
+ const diffs = [];
422
+ for (let i = 1; i < values.length; i++) {
423
+ diffs.push(values[i] - values[i-1]);
424
+ }
425
+
426
+ const avgVelocity = diffs.reduce((a, b) => a + b, 0) / diffs.length;
427
+
428
+ this.setResult(entityId, {
429
+ velocity: avgVelocity,
430
+ current: values[values.length - 1],
431
+ trend: avgVelocity > 0 ? 'increasing' : 'decreasing'
432
+ });
433
+ }
434
+ }
435
+ ```
436
+
437
+ ### **Example 4: Using Rules**
438
+
439
+ ```javascript
440
+ class PortfolioSummary extends Computation {
441
+ static getConfig() {
442
+ return {
443
+ name: 'PortfolioSummary',
444
+ type: 'per-entity',
445
+ category: 'analytics',
446
+
447
+ requires: {
448
+ 'portfolio_snapshots': {
449
+ lookback: 0,
450
+ mandatory: true,
451
+ fields: ['user_id', 'portfolio_data', 'date']
452
+ },
453
+ 'ticker_mappings': {
454
+ mandatory: false,
455
+ fields: ['instrument_id', 'ticker']
456
+ },
457
+ 'sector_mappings': {
458
+ mandatory: false,
459
+ fields: ['symbol', 'sector']
460
+ }
461
+ },
462
+
463
+ storage: { bigquery: true }
464
+ };
465
+ }
466
+
467
+ async process(context) {
468
+ const { data, entityId, rules, references } = context;
469
+
470
+ const portfolioRow = data['portfolio_snapshots'];
471
+ const portfolioData = rules.portfolio.extractPortfolioData(portfolioRow);
472
+ const positions = rules.portfolio.extractPositions(portfolioData);
473
+
474
+ // Use business rules for calculations
475
+ const totalValue = rules.portfolio.calculateTotalValue(positions);
476
+ const exposure = rules.portfolio.getExposure(portfolioData);
477
+ const winRatio = rules.portfolio.calculateWinRatio(positions);
478
+
479
+ // Access reference data
480
+ const tickerMap = this._buildTickerMap(references['ticker_mappings']);
481
+ const sectorMap = this._buildSectorMap(references['sector_mappings']);
482
+
483
+ // Calculate sector exposure
484
+ const sectorExposure = rules.portfolio.calculateSectorExposure(
485
+ positions,
486
+ this._buildInstrumentToSectorMap(tickerMap, sectorMap)
487
+ );
488
+
489
+ this.setResult(entityId, {
490
+ totalValue,
491
+ exposure,
492
+ winRatio,
493
+ sectorExposure,
494
+ positionCount: positions.length
495
+ });
496
+ }
497
+
498
+ _buildTickerMap(rows) {
499
+ const map = {};
500
+ Object.values(rows || {}).forEach(r => {
501
+ map[r.instrument_id] = r.ticker;
502
+ });
503
+ return map;
504
+ }
505
+
506
+ _buildSectorMap(rows) {
507
+ const map = {};
508
+ Object.values(rows || {}).forEach(r => {
509
+ map[r.symbol] = r.sector;
510
+ });
511
+ return map;
512
+ }
513
+
514
+ _buildInstrumentToSectorMap(tickerMap, sectorMap) {
515
+ const map = {};
516
+ for (const [instId, ticker] of Object.entries(tickerMap)) {
517
+ map[instId] = sectorMap[ticker] || 'Unknown';
518
+ }
519
+ return map;
520
+ }
521
+ }
522
+ ```
523
+
524
+ ---
525
+
526
+ ## Common Patterns
527
+
528
+ ### **Pattern 1: Date Comparison**
529
+
530
+ ```javascript
531
+ const toDateStr = (d) => {
532
+ if (d?.value) return d.value;
533
+ if (d instanceof Date) return d.toISOString().slice(0, 10);
534
+ return String(d);
535
+ };
536
+
537
+ const todayRow = history.find(d => toDateStr(d.date) === date);
538
+ ```
539
+
540
+ ### **Pattern 2: Safe Entity Row Extraction**
541
+
542
+ ```javascript
543
+ const getEntityRows = (dataset) => {
544
+ if (!dataset) return [];
545
+ if (dataset[entityId]) {
546
+ return Array.isArray(dataset[entityId])
547
+ ? dataset[entityId]
548
+ : [dataset[entityId]];
549
+ }
550
+ if (Array.isArray(dataset)) {
551
+ return dataset.filter(r =>
552
+ String(r.user_id || r.pi_id || r.cid) === String(entityId)
553
+ );
554
+ }
555
+ return [];
556
+ };
557
+ ```
558
+
559
+ ### **Pattern 3: JSON Parsing**
560
+
561
+ ```javascript
562
+ let data = row.data_field;
563
+ if (typeof data === 'string') {
564
+ try {
565
+ data = JSON.parse(data);
566
+ } catch (e) {
567
+ data = null;
568
+ }
569
+ }
570
+ ```
571
+
572
+ ---
573
+
574
+ ## Testing
575
+
576
+ ```javascript
577
+ const { IntegrationTester } = require('../framework/testing/ComputationTester');
578
+
579
+ async function testComputation() {
580
+ const tester = new IntegrationTester(config);
581
+
582
+ // Automatically finds runnable date
583
+ const date = await tester.findLatestRunnableDate('MyComputation');
584
+
585
+ // Runs computation + dependencies
586
+ const { result } = await tester.run('MyComputation', date);
587
+
588
+ console.log(result);
589
+ }
590
+ ```
591
+
592
+ ---
593
+
594
+ ## Key Takeaways
595
+
596
+ 1. **Always specify `fields`** - Prevents cost explosions
597
+ 2. **Always use date partitioning** - Required for safety
598
+ 3. **Use `_global` not `'global'`** - Critical for global computations
599
+ 4. **Leverage materialized views** - Automatic metric aggregation
600
+ 5. **Use business rules** - Centralized, tested logic
601
+ 6. **Reference data** - Use `references` for mappings
602
+ 7. **Test incrementally** - Use IntegrationTester
@@ -39,12 +39,9 @@ class ManifestBuilder {
39
39
  if (entry) {
40
40
  manifestMap.set(entry.name, entry);
41
41
 
42
- // CRITICAL FIX: Include conditional dependencies in the DAG for cycle detection and topological sort.
43
- // Even if the dependency is conditional at runtime, the execution order (Pass) must respect it.
44
42
  const graphDeps = [...entry.dependencies];
45
43
  if (entry.conditionalDependencies) {
46
44
  entry.conditionalDependencies.forEach(cd => {
47
- // Ensure we use the normalized name for the graph
48
45
  graphDeps.push(cd.computation);
49
46
  });
50
47
  }
@@ -60,7 +57,7 @@ class ManifestBuilder {
60
57
  throw new Error(`[Manifest] Circular dependency detected: ${cycle}`);
61
58
  }
62
59
 
63
- // 3. Topological Sort (calculates passes)
60
+ // 3. Topological Sort
64
61
  const sortedItems = Graph.topologicalSort(nodes, adjacency);
65
62
 
66
63
  // 4. Hydrate Sorted List
@@ -115,8 +112,6 @@ class ManifestBuilder {
115
112
  compositeHash += `|RULE:${mod}:${h}`;
116
113
  }
117
114
 
118
- // Normalize conditional dependencies if they exist
119
- // This ensures the Orchestrator can look them up by normalized name later
120
115
  const conditionalDependencies = (config.conditionalDependencies || []).map(cd => ({
121
116
  ...cd,
122
117
  computation: this._normalize(cd.computation)
@@ -128,16 +123,20 @@ class ManifestBuilder {
128
123
  class: ComputationClass,
129
124
  category: config.category || 'default',
130
125
  type: config.type || 'global',
126
+
127
+ outputTable: config.outputTable || null,
128
+ // --------------------------------------
129
+
131
130
  requires: config.requires || {},
132
131
  dependencies: (config.dependencies || []).map(d => this._normalize(d)),
133
- conditionalDependencies, // FIX: Pass this through to the manifest entry
132
+ conditionalDependencies,
134
133
  isHistorical: config.isHistorical || false,
135
134
  isTest: config.isTest || false,
136
135
  schedule: this.scheduleValidator.parseSchedule(config.schedule),
137
136
  storage: this._parseStorageConfig(config.storage),
138
137
  ttlDays: config.ttlDays,
139
- pass: 0, // Set later by Graph.js
140
- hash: this._hashCode(compositeHash), // Intrinsic hash
138
+ pass: 0,
139
+ hash: this._hashCode(compositeHash),
141
140
  weight: ComputationClass.getWeight ? ComputationClass.getWeight() : 1.0,
142
141
  composition: {
143
142
  epoch: this.epoch,
@@ -152,7 +151,6 @@ class ManifestBuilder {
152
151
  _computeFinalHashes(sorted, manifestMap) {
153
152
  for (const entry of sorted) {
154
153
  let hashInput = entry.hash;
155
- // Includes strict dependencies in the hash chain
156
154
  if (entry.dependencies.length > 0) {
157
155
  const depHashes = entry.dependencies.sort().map(d => {
158
156
  const h = manifestMap.get(d)?.hash;
@@ -161,10 +159,6 @@ class ManifestBuilder {
161
159
  });
162
160
  hashInput += `|DEPS:${depHashes.join('|')}`;
163
161
  }
164
- // Note: Conditional dependencies are currently excluded from the hash chain
165
- // because they might not be loaded. If strict versioning is required for them,
166
- // they should be added here too.
167
-
168
162
  entry.hash = this._hashCode(hashInput);
169
163
  }
170
164
  }
@@ -203,7 +197,7 @@ class ManifestBuilder {
203
197
  const used = {};
204
198
  for (const [name, exports] of Object.entries(this.sharedLayers)) {
205
199
  const found = Object.keys(exports).filter(exp =>
206
- code.includes(exp) // Simple include check, similar to original regex
200
+ code.includes(exp)
207
201
  );
208
202
  if (found.length) used[name] = found;
209
203
  }
@@ -240,7 +234,6 @@ class ManifestBuilder {
240
234
 
241
235
  _log(l, m) { this.logger ? this.logger.log(l, `[Manifest] ${m}`) : console.log(`[Manifest] ${m}`); }
242
236
 
243
- // Public alias for groupByPass matching the Interface
244
237
  groupByPass(m) { return this._groupByPass(m); }
245
238
  }
246
239
 
@@ -2,6 +2,7 @@
2
2
  * @fileoverview Run Analyzer
3
3
  * * Pure logic component that determines which computations need to run.
4
4
  * Decouples decision-making from execution and storage.
5
+ * * * UPDATE: Removed SQL bypass. All computations are now checked for data availability.
5
6
  */
6
7
 
7
8
  class RunAnalyzer {
@@ -57,7 +58,7 @@ class RunAnalyzer {
57
58
  }
58
59
 
59
60
  // 2. Data Availability Check
60
- // Note: This is the only async IO part (calls DataFetcher)
61
+ // UPDATE: Removed isSql check. All computations must have their raw data available.
61
62
  const availability = await this.dataFetcher.checkAvailability(requires, dateStr);
62
63
  if (!availability.canRun) {
63
64
  if (!isToday) {