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,469 @@
1
+ /**
2
+ * @fileoverview StorageManager - Unified storage for computation results
3
+ * Includes Optimized Checkpointing and Performance Monitoring.
4
+ */
5
+
6
+ const { Firestore } = require('@google-cloud/firestore');
7
+ const { BigQuery } = require('@google-cloud/bigquery');
8
+ const pLimit = require('p-limit');
9
+
10
+ class StorageManager {
11
+ constructor(config, logger = null) {
12
+ this.config = config;
13
+ this.logger = logger;
14
+
15
+ this.bigquery = new BigQuery({
16
+ projectId: config.bigquery?.projectId,
17
+ location: config.bigquery?.location || 'EU'
18
+ });
19
+
20
+ this._firestore = null;
21
+ this.tableExists = new Map();
22
+ }
23
+
24
+ get firestore() {
25
+ if (!this._firestore) {
26
+ this._firestore = new Firestore({
27
+ projectId: this.config.bigquery?.projectId
28
+ });
29
+ }
30
+ return this._firestore;
31
+ }
32
+
33
+ async commitResults(dateStr, entry, results, depResultHashes = {}) {
34
+ const storageConfig = this._resolveStorageConfig(entry);
35
+ const writeResults = { bigquery: null, firestore: null };
36
+ const startTime = Date.now();
37
+
38
+ if (storageConfig.bigquery !== false) {
39
+ try {
40
+ writeResults.bigquery = await this._writeToBigQuery(
41
+ dateStr, entry, results, depResultHashes
42
+ );
43
+ } catch (error) {
44
+ this._log('ERROR', `BigQuery write failed for ${entry.name}: ${error.message}`);
45
+ throw error;
46
+ }
47
+ }
48
+
49
+ if (storageConfig.firestore?.enabled) {
50
+ try {
51
+ writeResults.firestore = await this._writeToFirestore(
52
+ dateStr, entry, results, storageConfig.firestore
53
+ );
54
+ } catch (error) {
55
+ this._log('WARN', `Firestore write failed for ${entry.name}: ${error.message}`);
56
+ writeResults.firestore = { error: error.message };
57
+ }
58
+ }
59
+
60
+ const duration = Date.now() - startTime;
61
+ this._log('INFO', `Committed ${entry.name} results in ${duration}ms`);
62
+ return writeResults;
63
+ }
64
+
65
+ // =========================================================================
66
+ // OPTIMIZED CHECKPOINTING (Lightweight State)
67
+ // =========================================================================
68
+
69
+ async initCheckpoint(dateStr, computationName, checkpointId, totalEntities) {
70
+ const table = 'computation_checkpoints';
71
+ const dataset = this.config.bigquery.dataset;
72
+
73
+ await this._ensureCheckpointTable(table);
74
+
75
+ const row = {
76
+ date: dateStr,
77
+ computation_name: computationName,
78
+ checkpoint_id: checkpointId,
79
+ status: 'running',
80
+ processed_count: 0,
81
+ total_entities: totalEntities,
82
+ last_entity_id: null,
83
+ completed_batches: [],
84
+ // FIX: Use instance method, remove 'new', pass Date object
85
+ started_at: this.bigquery.timestamp(new Date()),
86
+ last_updated: this.bigquery.timestamp(new Date())
87
+ };
88
+
89
+ // Standard insert for initialization
90
+ await this.bigquery.dataset(dataset).table(table).insert([row]);
91
+ }
92
+
93
+ async updateCheckpoint(dateStr, computationName, checkpointId, state) {
94
+ const table = 'computation_checkpoints';
95
+ const fullTable = `\`${this.config.bigquery.projectId}.${this.config.bigquery.dataset}.${table}\``;
96
+
97
+ // Efficient UPDATE using structured fields
98
+ const query = `
99
+ UPDATE ${fullTable}
100
+ SET
101
+ processed_count = @processedCount,
102
+ last_entity_id = @lastEntityId,
103
+ completed_batches = ARRAY_CONCAT(completed_batches, [@batchIndex]),
104
+ last_updated = CURRENT_TIMESTAMP()
105
+ WHERE date = @date
106
+ AND computation_name = @computationName
107
+ AND checkpoint_id = @checkpointId
108
+ `;
109
+
110
+ await this.bigquery.query({
111
+ query,
112
+ params: {
113
+ date: dateStr,
114
+ computationName,
115
+ checkpointId,
116
+ processedCount: state.processedCount,
117
+ lastEntityId: state.lastEntityId,
118
+ batchIndex: state.batchIndex // Assuming we add one batch at a time
119
+ },
120
+ location: this.config.bigquery.location
121
+ });
122
+ }
123
+
124
+ async completeCheckpoint(dateStr, computationName, checkpointId) {
125
+ const table = 'computation_checkpoints';
126
+ const fullTable = `\`${this.config.bigquery.projectId}.${this.config.bigquery.dataset}.${table}\``;
127
+
128
+ const query = `
129
+ UPDATE ${fullTable}
130
+ SET status = 'completed', last_updated = CURRENT_TIMESTAMP()
131
+ WHERE date = @date AND computation_name = @computationName AND checkpoint_id = @checkpointId
132
+ `;
133
+
134
+ await this.bigquery.query({
135
+ query,
136
+ params: { date: dateStr, computationName, checkpointId },
137
+ location: this.config.bigquery.location
138
+ });
139
+ }
140
+
141
+ async getLatestCheckpoint(dateStr, computationName) {
142
+ const table = 'computation_checkpoints';
143
+ const fullTable = `\`${this.config.bigquery.projectId}.${this.config.bigquery.dataset}.${table}\``;
144
+
145
+ try {
146
+ // Get the most recent attempt
147
+ const query = `
148
+ SELECT checkpoint_id, status, processed_count, last_entity_id, completed_batches
149
+ FROM ${fullTable}
150
+ WHERE date = @date AND computation_name = @computationName
151
+ ORDER BY started_at DESC
152
+ LIMIT 1
153
+ `;
154
+
155
+ const [rows] = await this.bigquery.query({
156
+ query,
157
+ params: { date: dateStr, computationName },
158
+ location: this.config.bigquery.location
159
+ });
160
+
161
+ if (rows.length === 0) return null;
162
+ return rows[0];
163
+ } catch (e) {
164
+ return null; // Table might not exist yet
165
+ }
166
+ }
167
+
168
+ async _ensureCheckpointTable(tableName) {
169
+ if (this.tableExists.get(tableName)) return;
170
+ const dataset = this.bigquery.dataset(this.config.bigquery.dataset);
171
+ const table = dataset.table(tableName);
172
+ const [exists] = await table.exists();
173
+
174
+ if (!exists) {
175
+ await dataset.createTable(tableName, {
176
+ schema: [
177
+ { name: 'date', type: 'DATE', mode: 'REQUIRED' },
178
+ { name: 'computation_name', type: 'STRING', mode: 'REQUIRED' },
179
+ { name: 'checkpoint_id', type: 'STRING', mode: 'REQUIRED' },
180
+ { name: 'status', type: 'STRING', mode: 'REQUIRED' }, // running, completed, failed
181
+ { name: 'processed_count', type: 'INTEGER', mode: 'NULLABLE' },
182
+ { name: 'total_entities', type: 'INTEGER', mode: 'NULLABLE' },
183
+ { name: 'last_entity_id', type: 'STRING', mode: 'NULLABLE' },
184
+ { name: 'completed_batches', type: 'INTEGER', mode: 'REPEATED' }, // ARRAY<INT64>
185
+ { name: 'started_at', type: 'TIMESTAMP', mode: 'REQUIRED' },
186
+ { name: 'last_updated', type: 'TIMESTAMP', mode: 'REQUIRED' }
187
+ ],
188
+ timePartitioning: { type: 'DAY', field: 'date' },
189
+ clustering: { fields: ['computation_name', 'status'] }
190
+ });
191
+ }
192
+ this.tableExists.set(tableName, true);
193
+ }
194
+
195
+ // =========================================================================
196
+ // PERFORMANCE REPORTING
197
+ // =========================================================================
198
+
199
+ async savePerformanceReport(dateStr, report) {
200
+ const table = 'computation_performance';
201
+
202
+ await this._ensurePerformanceTable(table);
203
+
204
+ const rows = report.computations.map(c => ({
205
+ date: dateStr,
206
+ computation_name: c.name,
207
+ entity_count: c.entityCount,
208
+ avg_duration_ms: c.avgDuration,
209
+ max_duration_ms: c.maxDuration,
210
+ p95_duration_ms: c.p95Duration,
211
+ avg_memory_delta: c.avgMemory,
212
+ total_duration_ms: c.totalDuration,
213
+ recorded_at: new Date().toISOString()
214
+ }));
215
+
216
+ if (rows.length === 0) return;
217
+ await this.bigquery.dataset(this.config.bigquery.dataset).table(table).insert(rows);
218
+ }
219
+
220
+ async _ensurePerformanceTable(tableName) {
221
+ if (this.tableExists.get(tableName)) return;
222
+ const dataset = this.bigquery.dataset(this.config.bigquery.dataset);
223
+ const table = dataset.table(tableName);
224
+ const [exists] = await table.exists();
225
+
226
+ if (!exists) {
227
+ await dataset.createTable(tableName, {
228
+ schema: [
229
+ { name: 'date', type: 'DATE', mode: 'REQUIRED' },
230
+ { name: 'computation_name', type: 'STRING', mode: 'REQUIRED' },
231
+ { name: 'entity_count', type: 'INTEGER', mode: 'NULLABLE' },
232
+ { name: 'avg_duration_ms', type: 'FLOAT', mode: 'NULLABLE' },
233
+ { name: 'max_duration_ms', type: 'FLOAT', mode: 'NULLABLE' },
234
+ { name: 'p95_duration_ms', type: 'FLOAT', mode: 'NULLABLE' },
235
+ { name: 'avg_memory_delta', type: 'FLOAT', mode: 'NULLABLE' },
236
+ { name: 'total_duration_ms', type: 'FLOAT', mode: 'NULLABLE' },
237
+ { name: 'recorded_at', type: 'TIMESTAMP', mode: 'REQUIRED' }
238
+ ],
239
+ timePartitioning: { type: 'DAY', field: 'date' }
240
+ });
241
+ }
242
+ this.tableExists.set(tableName, true);
243
+ }
244
+
245
+ // =========================================================================
246
+ // INTERNAL HELPERS
247
+ // =========================================================================
248
+
249
+ _resolveStorageConfig(entry) {
250
+ const entryStorage = entry.storage || {};
251
+ return {
252
+ bigquery: entryStorage.bigquery !== false,
253
+ firestore: {
254
+ enabled: entryStorage.firestore?.enabled === true,
255
+ path: entryStorage.firestore?.path || null,
256
+ merge: entryStorage.firestore?.merge || false,
257
+ includeMetadata: entryStorage.firestore?.includeMetadata !== false
258
+ }
259
+ };
260
+ }
261
+
262
+ async _writeToBigQuery(dateStr, entry, results, depResultHashes) {
263
+ const table = this.config.resultStore?.table || 'computation_results';
264
+ const fullTable = `\`${this.config.bigquery.projectId}.${this.config.bigquery.dataset}.${table}\``;
265
+
266
+ const rows = this._buildBigQueryRows(dateStr, entry, results, depResultHashes);
267
+ if (rows.length === 0) return { rowCount: 0 };
268
+
269
+ await this._ensureBigQueryTable(table);
270
+
271
+ const batchSize = this.config.execution?.insertBatchSize || 500;
272
+ for (let i = 0; i < rows.length; i += batchSize) {
273
+ const batch = rows.slice(i, i + batchSize);
274
+ await this._upsertBatch(fullTable, batch);
275
+ }
276
+ return { rowCount: rows.length };
277
+ }
278
+
279
+ _buildBigQueryRows(dateStr, entry, results, depResultHashes) {
280
+ const rows = [];
281
+ const timestamp = new Date().toISOString();
282
+ const depResultHashesJson = JSON.stringify(depResultHashes);
283
+
284
+ if (entry.type === 'per-entity' && typeof results === 'object') {
285
+ for (const [entityId, data] of Object.entries(results)) {
286
+ rows.push({
287
+ date: dateStr,
288
+ computation_name: entry.name,
289
+ category: entry.category || 'uncategorized',
290
+ entity_id: String(entityId),
291
+ code_hash: entry.hash,
292
+ result_hash: this._hashResult(data),
293
+ dependency_result_hashes: depResultHashesJson,
294
+ entity_count: 1,
295
+ result_data: JSON.stringify(data),
296
+ updated_at: timestamp
297
+ });
298
+ }
299
+ } else {
300
+ rows.push({
301
+ date: dateStr,
302
+ computation_name: entry.name,
303
+ category: entry.category || 'uncategorized',
304
+ entity_id: '_global',
305
+ code_hash: entry.hash,
306
+ result_hash: this._hashResult(results),
307
+ dependency_result_hashes: depResultHashesJson,
308
+ entity_count: typeof results === 'object' ? Object.keys(results).length : 1,
309
+ result_data: JSON.stringify(results),
310
+ updated_at: timestamp
311
+ });
312
+ }
313
+ return rows;
314
+ }
315
+
316
+ async _upsertBatch(fullTable, rows) {
317
+ const dates = rows.map(r => r.date);
318
+ const names = rows.map(r => r.computation_name);
319
+ const entityIds = rows.map(r => r.entity_id);
320
+ const codeHashes = rows.map(r => r.code_hash);
321
+ const resultHashes = rows.map(r => r.result_hash);
322
+ const depHashes = rows.map(r => r.dependency_result_hashes);
323
+ const entityCounts = rows.map(r => r.entity_count);
324
+ const resultData = rows.map(r => r.result_data);
325
+ const updatedAts = rows.map(r => r.updated_at);
326
+
327
+ const mergeQuery = `
328
+ MERGE INTO ${fullTable} T
329
+ USING (
330
+ SELECT
331
+ PARSE_DATE('%Y-%m-%d', date) as date,
332
+ computation_name, entity_id, code_hash, result_hash,
333
+ dependency_result_hashes, entity_count, result_data,
334
+ PARSE_TIMESTAMP('%Y-%m-%dT%H:%M:%E*SZ', updated_at) as updated_at
335
+ FROM UNNEST(@dates) AS date WITH OFFSET pos
336
+ JOIN UNNEST(@names) AS computation_name WITH OFFSET USING(pos)
337
+ JOIN UNNEST(@entity_ids) AS entity_id WITH OFFSET USING(pos)
338
+ JOIN UNNEST(@code_hashes) AS code_hash WITH OFFSET USING(pos)
339
+ JOIN UNNEST(@result_hashes) AS result_hash WITH OFFSET USING(pos)
340
+ JOIN UNNEST(@dep_hashes) AS dependency_result_hashes WITH OFFSET USING(pos)
341
+ JOIN UNNEST(@entity_counts) AS entity_count WITH OFFSET USING(pos)
342
+ JOIN UNNEST(@result_data) AS result_data WITH OFFSET USING(pos)
343
+ JOIN UNNEST(@updated_ats) AS updated_at WITH OFFSET USING(pos)
344
+ ) S
345
+ ON T.date = S.date AND T.computation_name = S.computation_name AND T.entity_id = S.entity_id
346
+ WHEN MATCHED THEN
347
+ UPDATE SET code_hash = S.code_hash, result_hash = S.result_hash,
348
+ dependency_result_hashes = S.dependency_result_hashes,
349
+ entity_count = S.entity_count, result_data = S.result_data,
350
+ updated_at = S.updated_at
351
+ WHEN NOT MATCHED THEN
352
+ INSERT (date, computation_name, category, entity_id, code_hash, result_hash,
353
+ dependency_result_hashes, entity_count, result_data, updated_at)
354
+ VALUES (S.date, S.computation_name, @category, S.entity_id, S.code_hash, S.result_hash,
355
+ S.dependency_result_hashes, S.entity_count, S.result_data, S.updated_at)
356
+ `;
357
+
358
+ await this.bigquery.query({
359
+ query: mergeQuery,
360
+ params: {
361
+ dates, names, entity_ids: entityIds, code_hashes: codeHashes,
362
+ result_hashes: resultHashes, dep_hashes: depHashes,
363
+ entity_counts: entityCounts, result_data: resultData,
364
+ updated_ats: updatedAts, category: rows[0].category
365
+ },
366
+ location: this.config.bigquery.location
367
+ });
368
+ }
369
+
370
+ async _ensureBigQueryTable(tableName) {
371
+ if (this.tableExists.get(tableName)) return;
372
+ const dataset = this.bigquery.dataset(this.config.bigquery.dataset);
373
+ const table = dataset.table(tableName);
374
+ const [exists] = await table.exists();
375
+ if (!exists) {
376
+ await dataset.createTable(tableName, {
377
+ schema: [
378
+ { name: 'date', type: 'DATE', mode: 'REQUIRED' },
379
+ { name: 'computation_name', type: 'STRING', mode: 'REQUIRED' },
380
+ { name: 'category', type: 'STRING', mode: 'NULLABLE' },
381
+ { name: 'entity_id', type: 'STRING', mode: 'REQUIRED' },
382
+ { name: 'code_hash', type: 'STRING', mode: 'NULLABLE' },
383
+ { name: 'result_hash', type: 'STRING', mode: 'NULLABLE' },
384
+ { name: 'dependency_result_hashes', type: 'STRING', mode: 'NULLABLE' },
385
+ { name: 'entity_count', type: 'INTEGER', mode: 'NULLABLE' },
386
+ { name: 'result_data', type: 'STRING', mode: 'NULLABLE' },
387
+ { name: 'updated_at', type: 'TIMESTAMP', mode: 'NULLABLE' }
388
+ ],
389
+ timePartitioning: { type: 'DAY', field: 'date' },
390
+ clustering: { fields: ['computation_name', 'category'] }
391
+ });
392
+ }
393
+ this.tableExists.set(tableName, true);
394
+ }
395
+
396
+ async _writeToFirestore(dateStr, entry, results, firestoreConfig) {
397
+ const { path, merge, includeMetadata } = firestoreConfig;
398
+ if (!path) throw new Error(`Firestore path not configured for ${entry.name}`);
399
+
400
+ const timestamp = new Date();
401
+ const metadata = includeMetadata ? {
402
+ _computedAt: timestamp, _computationDate: dateStr,
403
+ _computationName: entry.name, _codeHash: entry.hash
404
+ } : {};
405
+
406
+ let docCount = 0;
407
+
408
+ if (entry.type === 'per-entity' && typeof results === 'object') {
409
+ const batches = [];
410
+ let currentBatch = this.firestore.batch();
411
+ let batchCount = 0;
412
+ const MAX_BATCH = 500;
413
+
414
+ for (const [entityId, data] of Object.entries(results)) {
415
+ const docPath = this._resolvePath(path, {
416
+ entityId, date: dateStr, computationName: entry.name, category: entry.category || 'uncategorized'
417
+ });
418
+
419
+ const docRef = this.firestore.doc(docPath);
420
+ const docData = { ...data, ...metadata };
421
+
422
+ merge ? currentBatch.set(docRef, docData, { merge: true }) : currentBatch.set(docRef, docData);
423
+
424
+ batchCount++; docCount++;
425
+ if (batchCount >= MAX_BATCH) {
426
+ batches.push(currentBatch);
427
+ currentBatch = this.firestore.batch();
428
+ batchCount = 0;
429
+ }
430
+ }
431
+ if (batchCount > 0) batches.push(currentBatch);
432
+
433
+ const limit = pLimit(10);
434
+ await Promise.all(batches.map(b => limit(() => b.commit())));
435
+
436
+ } else {
437
+ const docPath = this._resolvePath(path, {
438
+ entityId: '_global', date: dateStr, computationName: entry.name, category: entry.category || 'uncategorized'
439
+ });
440
+ const docRef = this.firestore.doc(docPath);
441
+ const docData = { ...results, ...metadata };
442
+ merge ? await docRef.set(docData, { merge: true }) : await docRef.set(docData);
443
+ docCount = 1;
444
+ }
445
+ return { docCount };
446
+ }
447
+
448
+ _resolvePath(pathTemplate, values) {
449
+ let resolved = pathTemplate;
450
+ resolved = resolved.replace(/\{entityId\}/g, values.entityId || '_global');
451
+ resolved = resolved.replace(/\{date\}/g, values.date || '');
452
+ resolved = resolved.replace(/\{computationName\}/g, values.computationName || '');
453
+ resolved = resolved.replace(/\{category\}/g, values.category || '');
454
+ resolved = resolved.replace(/^\/+/, '');
455
+ return resolved;
456
+ }
457
+
458
+ _hashResult(data) {
459
+ const crypto = require('crypto');
460
+ const str = typeof data === 'string' ? data : JSON.stringify(data);
461
+ return crypto.createHash('md5').update(str).digest('hex').substring(0, 16);
462
+ }
463
+
464
+ _log(level, message) {
465
+ this.logger?.log ? this.logger.log(level, `[StorageManager] ${message}`) : console.log(`[${level}] [StorageManager] ${message}`);
466
+ }
467
+ }
468
+
469
+ module.exports = { StorageManager };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @fileoverview Storage module exports
3
+ */
4
+
5
+ const { StorageManager } = require('./StorageManager');
6
+
7
+ module.exports = {
8
+ StorageManager
9
+ };
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @fileoverview Unit Testing Utility for Computations
3
+ */
4
+
5
+ class ComputationTester {
6
+ constructor(ComputationClass, config) {
7
+ this.ComputationClass = ComputationClass;
8
+ this.config = config;
9
+ this.mockData = {};
10
+ this.mockDependencies = {};
11
+ }
12
+
13
+ withMockData(tableName, data) {
14
+ this.mockData[tableName] = data;
15
+ return this;
16
+ }
17
+
18
+ withMockDependency(computationName, results) {
19
+ this.mockDependencies[computationName] = results;
20
+ return this;
21
+ }
22
+
23
+ async run(entityId = 'test_entity') {
24
+ const instance = new this.ComputationClass();
25
+
26
+ const context = {
27
+ date: '2026-01-24',
28
+ entityId,
29
+ data: this.mockData,
30
+ dependencies: this.mockDependencies,
31
+ rules: this.config.rules,
32
+ config: this.config,
33
+ logger: { log: () => {} }
34
+ };
35
+
36
+ instance._meta = { logger: context.logger, config: this.config };
37
+
38
+ await instance.process(context);
39
+
40
+ const allResults = await instance.getResult();
41
+ return allResults[entityId] || allResults;
42
+ }
43
+
44
+ async expect(entityId = 'test_entity') {
45
+ const result = await this.run(entityId);
46
+
47
+ return {
48
+ result,
49
+
50
+ toHaveProperty(prop, value) {
51
+ const actual = this._getNestedProperty(result, prop);
52
+ if (value !== undefined && actual !== value) {
53
+ throw new Error(`Expected ${prop} to be ${value}, got ${actual}`);
54
+ }
55
+ if (actual === undefined) {
56
+ throw new Error(`Expected ${prop} to exist`);
57
+ }
58
+ return this;
59
+ },
60
+
61
+ toMatchSchema(schema) {
62
+ const errors = this._validateSchema(result, schema);
63
+ if (errors.length > 0) {
64
+ throw new Error(`Schema validation failed: ${errors.join(', ')}`);
65
+ }
66
+ return this;
67
+ },
68
+
69
+ _getNestedProperty(obj, path) {
70
+ return path.split('.').reduce((curr, key) => curr?.[key], obj);
71
+ },
72
+
73
+ _validateSchema(data, schema) {
74
+ const errors = [];
75
+ for (const [key, type] of Object.entries(schema)) {
76
+ if (typeof data[key] !== type) {
77
+ errors.push(`${key}: expected ${type}, got ${typeof data[key]}`);
78
+ }
79
+ }
80
+ return errors;
81
+ }
82
+ };
83
+ }
84
+ }
85
+
86
+ module.exports = { ComputationTester };