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,45 @@
1
+ /**
2
+ * @fileoverview Framework exports
3
+ * * This is the public API of the computation framework.
4
+ * Computations only need to import from here.
5
+ */
6
+
7
+ // Core
8
+ const { Computation } = require('./core/Computation');
9
+ const { Orchestrator } = require('./execution/Orchestrator'); // <--- CHANGED
10
+ const { ManifestBuilder } = require('./core/Manifest');
11
+ const { RulesRegistry } = require('./core/Rules');
12
+
13
+ // Scheduling
14
+ const { ScheduleValidator } = require('./scheduling/ScheduleValidator');
15
+
16
+ // Data Layer
17
+ const { SchemaRegistry } = require('./data/SchemaRegistry');
18
+ const { QueryBuilder } = require('./data/QueryBuilder');
19
+ const { DataFetcher } = require('./data/DataFetcher');
20
+
21
+ // Storage
22
+ const { StorageManager } = require('./storage/StorageManager');
23
+
24
+ module.exports = {
25
+ // Base class for computations
26
+ Computation,
27
+
28
+ // Execution engine
29
+ Orchestrator, // <--- CHANGED
30
+ ManifestBuilder,
31
+
32
+ // Business rules
33
+ RulesRegistry,
34
+
35
+ // Scheduling
36
+ ScheduleValidator,
37
+
38
+ // Data layer (for advanced use cases)
39
+ SchemaRegistry,
40
+ QueryBuilder,
41
+ DataFetcher,
42
+
43
+ // Storage
44
+ StorageManager
45
+ };
@@ -0,0 +1,147 @@
1
+ /**
2
+ * @fileoverview Data Lineage Tracker
3
+ * * Tracks the provenance of computation results.
4
+ * * Records which source data (tables, record counts) contributed to specific entity results.
5
+ * * Uses internal buffering to handle high-volume writes efficiently.
6
+ */
7
+
8
+ const { BigQuery } = require('@google-cloud/bigquery');
9
+ const crypto = require('crypto');
10
+
11
+ class LineageTracker {
12
+ /**
13
+ * @param {Object} config - System configuration
14
+ * @param {Object} [logger] - Logger instance
15
+ */
16
+ constructor(config, logger = null) {
17
+ this.config = config;
18
+ this.logger = logger || console;
19
+
20
+ this.bigquery = new BigQuery({
21
+ projectId: config.bigquery.projectId,
22
+ location: config.bigquery.location || 'US'
23
+ });
24
+
25
+ this.datasetId = config.bigquery.dataset;
26
+ this.tableName = 'data_lineage';
27
+
28
+ this.buffer = [];
29
+ this.BUFFER_SIZE = config.execution?.lineageBatchSize || 500;
30
+ this._tableChecked = false;
31
+ }
32
+
33
+ /**
34
+ * Track lineage for a specific computation result.
35
+ * @param {Object} params
36
+ * @param {string} params.computation - Computation name
37
+ * @param {string} params.date - Execution date
38
+ * @param {string} params.entityId - Entity ID
39
+ * @param {Object} params.sourceData - The actual input data object used
40
+ * @param {Object} params.result - The result object produced
41
+ */
42
+ async track({ computation, date, entityId, sourceData, result }) {
43
+ if (!sourceData) return;
44
+
45
+ const sourceSummary = this._summarizeSources(sourceData);
46
+ const resultHash = this._hash(result);
47
+
48
+ this.buffer.push({
49
+ date,
50
+ computation_name: computation,
51
+ entity_id: String(entityId),
52
+ sources_json: JSON.stringify(sourceSummary),
53
+ result_hash: resultHash,
54
+ timestamp: new Date().toISOString()
55
+ });
56
+
57
+ if (this.buffer.length >= this.BUFFER_SIZE) {
58
+ await this.flush();
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Force write any buffered records to BigQuery.
64
+ */
65
+ async flush() {
66
+ if (this.buffer.length === 0) return;
67
+
68
+ const batch = [...this.buffer];
69
+ this.buffer = []; // Clear immediately
70
+
71
+ try {
72
+ await this._ensureTable();
73
+
74
+ await this.bigquery
75
+ .dataset(this.datasetId)
76
+ .table(this.tableName)
77
+ .insert(batch);
78
+
79
+ this._log('DEBUG', `Flushed ${batch.length} lineage records`);
80
+ } catch (e) {
81
+ this._log('ERROR', `Failed to flush lineage buffer: ${e.message}`);
82
+ // In a production system, we might want to retry or dump to a dead-letter file
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Analyze source data to create a lightweight summary.
88
+ * @param {Object} data - Input data map { tableName: data }
89
+ * @returns {Array} Summary of sources [{ table, count, ... }]
90
+ */
91
+ _summarizeSources(data) {
92
+ return Object.entries(data).map(([table, content]) => {
93
+ let count = 0;
94
+ let meta = null;
95
+
96
+ if (Array.isArray(content)) {
97
+ count = content.length;
98
+ } else if (content && typeof content === 'object') {
99
+ count = Object.keys(content).length;
100
+ }
101
+
102
+ // Optional: If data has specific metadata fields (like 'version' or 'snapshot_id'), extract them
103
+ if (content && content._metadata) {
104
+ meta = content._metadata;
105
+ }
106
+
107
+ return { table, count, meta };
108
+ });
109
+ }
110
+
111
+ _hash(data) {
112
+ const str = typeof data === 'string' ? data : JSON.stringify(data);
113
+ return crypto.createHash('md5').update(str || '').digest('hex').substring(0, 16);
114
+ }
115
+
116
+ async _ensureTable() {
117
+ if (this._tableChecked) return;
118
+
119
+ const dataset = this.bigquery.dataset(this.datasetId);
120
+ const table = dataset.table(this.tableName);
121
+ const [exists] = await table.exists();
122
+
123
+ if (!exists) {
124
+ this._log('INFO', `Creating lineage table: ${this.tableName}`);
125
+ await dataset.createTable(this.tableName, {
126
+ schema: [
127
+ { name: 'date', type: 'DATE', mode: 'REQUIRED' },
128
+ { name: 'computation_name', type: 'STRING', mode: 'REQUIRED' },
129
+ { name: 'entity_id', type: 'STRING', mode: 'REQUIRED' },
130
+ { name: 'sources_json', type: 'STRING', mode: 'REQUIRED' }, // JSON array of source summaries
131
+ { name: 'result_hash', type: 'STRING', mode: 'NULLABLE' },
132
+ { name: 'timestamp', type: 'TIMESTAMP', mode: 'REQUIRED' }
133
+ ],
134
+ timePartitioning: { type: 'DAY', field: 'date' },
135
+ clustering: { fields: ['computation_name', 'entity_id'] }
136
+ });
137
+ }
138
+ this._tableChecked = true;
139
+ }
140
+
141
+ _log(level, message) {
142
+ const prefix = '[LineageTracker]';
143
+ this.logger?.log ? this.logger.log(level, `${prefix} ${message}`) : console.log(`${level}: ${prefix} ${message}`);
144
+ }
145
+ }
146
+
147
+ module.exports = { LineageTracker };
@@ -0,0 +1,80 @@
1
+ /**
2
+ * @fileoverview Computation Profiler
3
+ * Tracks execution metrics (duration, memory) for computations.
4
+ */
5
+
6
+ class ComputationProfiler {
7
+ constructor() {
8
+ this.profiles = new Map();
9
+ }
10
+
11
+ startProfile(computationName, entityId = null) {
12
+ const key = entityId ? `${computationName}:${entityId}` : computationName;
13
+
14
+ this.profiles.set(key, {
15
+ startTime: Date.now(),
16
+ startMemory: process.memoryUsage().heapUsed,
17
+ queryCount: 0,
18
+ bytesProcessed: 0
19
+ });
20
+
21
+ return key;
22
+ }
23
+
24
+ endProfile(key, metadata = {}) {
25
+ const profile = this.profiles.get(key);
26
+ if (!profile) return;
27
+
28
+ const endTime = Date.now();
29
+ const endMemory = process.memoryUsage().heapUsed;
30
+
31
+ return {
32
+ duration: endTime - profile.startTime,
33
+ memoryDelta: endMemory - profile.startMemory,
34
+ queriesExecuted: profile.queryCount,
35
+ bytesProcessed: profile.bytesProcessed,
36
+ ...metadata
37
+ };
38
+ }
39
+
40
+ async generateReport(dateStr, manifest) {
41
+ const report = {
42
+ date: dateStr,
43
+ totalDuration: 0,
44
+ totalQueries: 0,
45
+ totalBytes: 0,
46
+ computations: []
47
+ };
48
+
49
+ for (const entry of manifest) {
50
+ const compProfiles = Array.from(this.profiles.entries())
51
+ .filter(([k]) => k.startsWith(entry.name));
52
+
53
+ if (compProfiles.length === 0) continue;
54
+
55
+ const durations = compProfiles.map(([, p]) => Date.now() - p.startTime);
56
+ const memories = compProfiles.map(([, p]) => process.memoryUsage().heapUsed - p.startMemory);
57
+
58
+ report.computations.push({
59
+ name: entry.name,
60
+ entityCount: compProfiles.length,
61
+ avgDuration: durations.reduce((a, b) => a + b, 0) / durations.length,
62
+ maxDuration: Math.max(...durations),
63
+ p95Duration: this._percentile(durations, 0.95),
64
+ avgMemory: memories.reduce((a, b) => a + b, 0) / memories.length,
65
+ totalDuration: durations.reduce((a, b) => a + b, 0)
66
+ });
67
+ }
68
+
69
+ report.computations.sort((a, b) => b.totalDuration - a.totalDuration);
70
+ return report;
71
+ }
72
+
73
+ _percentile(arr, p) {
74
+ const sorted = [...arr].sort((a, b) => a - b);
75
+ const index = Math.ceil(sorted.length * p) - 1;
76
+ return sorted[index];
77
+ }
78
+ }
79
+
80
+ module.exports = { ComputationProfiler };
@@ -0,0 +1,66 @@
1
+ /**
2
+ * @fileoverview Computation Checkpointer
3
+ * Manages save/resume states for long-running computations using lightweight batch tracking.
4
+ */
5
+
6
+ const crypto = require('crypto');
7
+
8
+ class Checkpointer {
9
+ constructor(config, storage) {
10
+ this.config = config;
11
+ this.storage = storage;
12
+ this.enabled = config.checkpointing?.enabled !== false;
13
+ }
14
+
15
+ async initCheckpoint(dateStr, computationName, totalEntities = 0) {
16
+ if (!this.enabled) return null;
17
+
18
+ // Check if there is an existing running checkpoint we can resume
19
+ const existing = await this.storage.getLatestCheckpoint(dateStr, computationName);
20
+
21
+ if (existing && existing.status === 'running') {
22
+ return {
23
+ id: existing.checkpoint_id,
24
+ processedCount: existing.processed_count || 0,
25
+ completedBatches: new Set(existing.completed_batches || []),
26
+ lastEntityId: existing.last_entity_id,
27
+ isResumed: true
28
+ };
29
+ } else if (existing && existing.status === 'completed') {
30
+ // Already fully done
31
+ return { isCompleted: true };
32
+ }
33
+
34
+ // Start new
35
+ const checkpointId = crypto.randomUUID();
36
+ await this.storage.initCheckpoint(dateStr, computationName, checkpointId, totalEntities);
37
+
38
+ return {
39
+ id: checkpointId,
40
+ processedCount: 0,
41
+ completedBatches: new Set(),
42
+ lastEntityId: null,
43
+ isResumed: false
44
+ };
45
+ }
46
+
47
+ async markBatchComplete(dateStr, computationName, checkpointId, batchIndex, batchSize, lastEntityId) {
48
+ if (!this.enabled || !checkpointId) return;
49
+
50
+ // We update processed count approximately based on batch size
51
+ const processedCount = (batchIndex + 1) * batchSize;
52
+
53
+ await this.storage.updateCheckpoint(dateStr, computationName, checkpointId, {
54
+ processedCount,
55
+ lastEntityId,
56
+ batchIndex
57
+ });
58
+ }
59
+
60
+ async complete(dateStr, computationName, checkpointId) {
61
+ if (!this.enabled || !checkpointId) return;
62
+ await this.storage.completeCheckpoint(dateStr, computationName, checkpointId);
63
+ }
64
+ }
65
+
66
+ module.exports = { Checkpointer };
@@ -0,0 +1,327 @@
1
+ /**
2
+ * @fileoverview Schedule Validator
3
+ *
4
+ * Validates computation schedules and enforces timing rules:
5
+ * 1. Parses schedule declarations
6
+ * 2. Validates schedule format
7
+ * 3. Checks 15-minute rule between dependent computations
8
+ * 4. Generates warnings/errors for problematic schedules
9
+ */
10
+
11
+ /**
12
+ * @typedef {Object} Schedule
13
+ * @property {string} frequency - 'hourly' | 'daily' | 'weekly' | 'monthly'
14
+ * @property {string} [time] - Time in HH:MM format (UTC)
15
+ * @property {number} [dayOfWeek] - 0-6 (Sun-Sat) for weekly
16
+ * @property {number} [dayOfMonth] - 1-31 for monthly
17
+ * @property {string} [timezone] - Timezone (default: UTC)
18
+ */
19
+
20
+ /**
21
+ * @typedef {Object} ValidationResult
22
+ * @property {boolean} valid - Whether all schedules are valid
23
+ * @property {Array} errors - Critical errors (must fix)
24
+ * @property {Array} warnings - Non-critical issues (should fix)
25
+ */
26
+
27
+ class ScheduleValidator {
28
+ /**
29
+ * @param {Object} config - System configuration
30
+ * @param {Object} [logger] - Logger instance
31
+ */
32
+ constructor(config, logger = null) {
33
+ this.config = config;
34
+ this.logger = logger;
35
+ this.defaultSchedule = config.scheduling?.default || {
36
+ frequency: 'daily',
37
+ time: '02:00',
38
+ timezone: 'UTC'
39
+ };
40
+ this.dependencyGapMinutes = config.scheduling?.dependencyGapMinutes || 15;
41
+ }
42
+
43
+ /**
44
+ * Validate all computation schedules in the manifest.
45
+ * @param {Array} manifest - Array of manifest entries
46
+ * @returns {ValidationResult}
47
+ */
48
+ validate(manifest) {
49
+ const errors = [];
50
+ const warnings = [];
51
+
52
+ // Build lookup map
53
+ const entryMap = new Map();
54
+ for (const entry of manifest) {
55
+ entryMap.set(entry.name, entry);
56
+ }
57
+
58
+ for (const entry of manifest) {
59
+ // 1. Validate schedule format
60
+ const scheduleErrors = this._validateScheduleFormat(entry);
61
+ errors.push(...scheduleErrors);
62
+
63
+ // 2. Check dependency timing
64
+ const timingIssues = this._checkDependencyTiming(entry, entryMap);
65
+ for (const issue of timingIssues) {
66
+ if (issue.severity === 'error') {
67
+ errors.push(issue);
68
+ } else {
69
+ warnings.push(issue);
70
+ }
71
+ }
72
+ }
73
+
74
+ return {
75
+ valid: errors.length === 0,
76
+ errors,
77
+ warnings
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Parse and normalize a schedule, applying defaults.
83
+ * @param {Object|undefined} schedule - Raw schedule from computation config
84
+ * @returns {Schedule} Normalized schedule
85
+ */
86
+ parseSchedule(schedule) {
87
+ if (!schedule) {
88
+ return { ...this.defaultSchedule };
89
+ }
90
+
91
+ return {
92
+ frequency: schedule.frequency || this.defaultSchedule.frequency,
93
+ time: schedule.time || this.defaultSchedule.time,
94
+ dayOfWeek: schedule.dayOfWeek,
95
+ dayOfMonth: schedule.dayOfMonth,
96
+ timezone: schedule.timezone || 'UTC'
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Convert schedule to cron expression.
102
+ * @param {Schedule} schedule - Normalized schedule
103
+ * @returns {string} Cron expression
104
+ */
105
+ toCron(schedule) {
106
+ const [hour, minute] = (schedule.time || '00:00').split(':').map(Number);
107
+
108
+ switch (schedule.frequency) {
109
+ case 'hourly':
110
+ return `${minute} * * * *`;
111
+ case 'daily':
112
+ return `${minute} ${hour} * * *`;
113
+ case 'weekly':
114
+ const dow = schedule.dayOfWeek ?? 0;
115
+ return `${minute} ${hour} * * ${dow}`;
116
+ case 'monthly':
117
+ const dom = schedule.dayOfMonth ?? 1;
118
+ return `${minute} ${hour} ${dom} * *`;
119
+ default:
120
+ return `${minute} ${hour} * * *`; // Default to daily
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Calculate minutes between two schedules on a typical day.
126
+ * Returns null if schedules don't overlap (different frequencies).
127
+ * @param {Schedule} scheduleA - First schedule
128
+ * @param {Schedule} scheduleB - Second schedule
129
+ * @returns {number|null} Minutes between A and B (positive if B is after A)
130
+ */
131
+ calculateGap(scheduleA, scheduleB) {
132
+ // Only compare schedules with same or compatible frequencies
133
+ if (!this._frequenciesCompatible(scheduleA.frequency, scheduleB.frequency)) {
134
+ return null;
135
+ }
136
+
137
+ const timeA = this._parseTime(scheduleA.time || '00:00');
138
+ const timeB = this._parseTime(scheduleB.time || '00:00');
139
+
140
+ // For weekly, also check day of week
141
+ if (scheduleA.frequency === 'weekly' || scheduleB.frequency === 'weekly') {
142
+ const dowA = scheduleA.dayOfWeek ?? 0;
143
+ const dowB = scheduleB.dayOfWeek ?? 0;
144
+ if (dowA !== dowB) {
145
+ // Different days - calculate day gap
146
+ const dayDiff = ((dowB - dowA) + 7) % 7;
147
+ return dayDiff * 24 * 60 + (timeB - timeA);
148
+ }
149
+ }
150
+
151
+ // For monthly, also check day of month
152
+ if (scheduleA.frequency === 'monthly' || scheduleB.frequency === 'monthly') {
153
+ const domA = scheduleA.dayOfMonth ?? 1;
154
+ const domB = scheduleB.dayOfMonth ?? 1;
155
+ if (domA !== domB) {
156
+ const dayDiff = domB - domA;
157
+ return dayDiff * 24 * 60 + (timeB - timeA);
158
+ }
159
+ }
160
+
161
+ return timeB - timeA;
162
+ }
163
+
164
+ /**
165
+ * Check if a computation should run on a given date.
166
+ * @param {Schedule} schedule - Computation schedule
167
+ * @param {Date|string} date - Date to check
168
+ * @returns {boolean}
169
+ */
170
+ shouldRunOnDate(schedule, date) {
171
+ const d = typeof date === 'string' ? new Date(date) : date;
172
+
173
+ switch (schedule.frequency) {
174
+ case 'hourly':
175
+ case 'daily':
176
+ return true;
177
+ case 'weekly':
178
+ return d.getUTCDay() === (schedule.dayOfWeek ?? 0);
179
+ case 'monthly':
180
+ return d.getUTCDate() === (schedule.dayOfMonth ?? 1);
181
+ default:
182
+ return true;
183
+ }
184
+ }
185
+
186
+ // =========================================================================
187
+ // PRIVATE METHODS
188
+ // =========================================================================
189
+
190
+ _validateScheduleFormat(entry) {
191
+ const errors = [];
192
+ const schedule = entry.schedule;
193
+
194
+ if (!schedule) return errors; // Will use default
195
+
196
+ // Validate frequency
197
+ const validFrequencies = ['hourly', 'daily', 'weekly', 'monthly'];
198
+ if (schedule.frequency && !validFrequencies.includes(schedule.frequency)) {
199
+ errors.push({
200
+ computation: entry.name,
201
+ field: 'schedule.frequency',
202
+ message: `Invalid frequency: ${schedule.frequency}. Must be one of: ${validFrequencies.join(', ')}`
203
+ });
204
+ }
205
+
206
+ // Validate time format
207
+ if (schedule.time && !/^\d{2}:\d{2}$/.test(schedule.time)) {
208
+ errors.push({
209
+ computation: entry.name,
210
+ field: 'schedule.time',
211
+ message: `Invalid time format: ${schedule.time}. Must be HH:MM`
212
+ });
213
+ }
214
+
215
+ // Validate time values
216
+ if (schedule.time) {
217
+ const [hour, minute] = schedule.time.split(':').map(Number);
218
+ if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
219
+ errors.push({
220
+ computation: entry.name,
221
+ field: 'schedule.time',
222
+ message: `Invalid time values: ${schedule.time}`
223
+ });
224
+ }
225
+ }
226
+
227
+ // Validate day of week
228
+ if (schedule.dayOfWeek !== undefined) {
229
+ if (schedule.dayOfWeek < 0 || schedule.dayOfWeek > 6) {
230
+ errors.push({
231
+ computation: entry.name,
232
+ field: 'schedule.dayOfWeek',
233
+ message: `Invalid dayOfWeek: ${schedule.dayOfWeek}. Must be 0-6`
234
+ });
235
+ }
236
+ }
237
+
238
+ // Validate day of month
239
+ if (schedule.dayOfMonth !== undefined) {
240
+ if (schedule.dayOfMonth < 1 || schedule.dayOfMonth > 31) {
241
+ errors.push({
242
+ computation: entry.name,
243
+ field: 'schedule.dayOfMonth',
244
+ message: `Invalid dayOfMonth: ${schedule.dayOfMonth}. Must be 1-31`
245
+ });
246
+ }
247
+ }
248
+
249
+ return errors;
250
+ }
251
+
252
+ _checkDependencyTiming(entry, entryMap) {
253
+ const issues = [];
254
+ const entrySchedule = this.parseSchedule(entry.schedule);
255
+
256
+ for (const depName of entry.dependencies) {
257
+ const dep = entryMap.get(depName);
258
+ if (!dep) continue;
259
+
260
+ const depSchedule = this.parseSchedule(dep.schedule);
261
+ const gap = this.calculateGap(depSchedule, entrySchedule);
262
+
263
+ if (gap === null) {
264
+ // Different frequencies - can't directly compare
265
+ issues.push({
266
+ severity: 'warning',
267
+ computation: entry.name,
268
+ dependency: depName,
269
+ message: `${entry.name} has different schedule frequency than dependency ${depName}. Manual verification recommended.`,
270
+ suggestion: 'Ensure the dependency runs before this computation on all relevant dates'
271
+ });
272
+ continue;
273
+ }
274
+
275
+ if (gap < 0) {
276
+ // Dependent runs BEFORE its dependency
277
+ issues.push({
278
+ severity: 'error',
279
+ computation: entry.name,
280
+ dependency: depName,
281
+ gap,
282
+ message: `${entry.name} is scheduled BEFORE its dependency ${depName} (${Math.abs(gap)} minutes earlier)`,
283
+ suggestion: `Move ${entry.name} to at least ${this.dependencyGapMinutes} minutes after ${depName}`
284
+ });
285
+ } else if (gap < this.dependencyGapMinutes) {
286
+ // Too close - race condition risk
287
+ issues.push({
288
+ severity: 'warning',
289
+ computation: entry.name,
290
+ dependency: depName,
291
+ gap,
292
+ message: `${entry.name} scheduled only ${gap} minutes after dependency ${depName}`,
293
+ suggestion: `Increase gap to at least ${this.dependencyGapMinutes} minutes`
294
+ });
295
+ }
296
+ }
297
+
298
+ return issues;
299
+ }
300
+
301
+ _frequenciesCompatible(freqA, freqB) {
302
+ // Hourly is compatible with everything (runs every hour anyway)
303
+ if (freqA === 'hourly' || freqB === 'hourly') return true;
304
+
305
+ // Same frequency is always compatible
306
+ if (freqA === freqB) return true;
307
+
308
+ // Daily is compatible with weekly/monthly (runs every day)
309
+ if (freqA === 'daily' || freqB === 'daily') return true;
310
+
311
+ // Weekly and monthly are not directly comparable
312
+ return false;
313
+ }
314
+
315
+ _parseTime(timeStr) {
316
+ const [hour, minute] = timeStr.split(':').map(Number);
317
+ return hour * 60 + minute;
318
+ }
319
+
320
+ _log(level, message) {
321
+ if (this.logger && typeof this.logger.log === 'function') {
322
+ this.logger.log(level, `[ScheduleValidator] ${message}`);
323
+ }
324
+ }
325
+ }
326
+
327
+ module.exports = { ScheduleValidator };