thuban 0.3.2 → 0.3.4

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 (59) hide show
  1. package/README.md +137 -102
  2. package/dist/LICENSE +21 -0
  3. package/dist/README.md +185 -0
  4. package/dist/cli.js +2 -0
  5. package/dist/packages/scanner/ai-confidence-scorer.js +1 -0
  6. package/dist/packages/scanner/alert-manager.js +1 -0
  7. package/dist/packages/scanner/baseline-manager.js +1 -0
  8. package/dist/packages/scanner/code-scanner.js +1 -0
  9. package/dist/packages/scanner/codebase-passport.js +1 -0
  10. package/dist/packages/scanner/copy-paste-detector.js +1 -0
  11. package/dist/packages/scanner/dependency-graph.js +1 -0
  12. package/dist/packages/scanner/drift-detector.js +1 -0
  13. package/dist/packages/scanner/executive-report.js +1 -0
  14. package/dist/packages/scanner/file-collector.js +1 -0
  15. package/dist/packages/scanner/file-watcher.js +1 -0
  16. package/dist/packages/scanner/ghost-code-detector.js +1 -0
  17. package/dist/packages/scanner/hallucination-detector.js +1 -0
  18. package/dist/packages/scanner/health-checker.js +1 -0
  19. package/dist/packages/scanner/html-report.js +1 -0
  20. package/dist/packages/scanner/index.js +1 -0
  21. package/dist/packages/scanner/license-manager.js +1 -0
  22. package/dist/packages/scanner/master-health-checker.js +1 -0
  23. package/dist/packages/scanner/monitor-notifier.js +1 -0
  24. package/dist/packages/scanner/monitor-service.js +1 -0
  25. package/dist/packages/scanner/monitor-store.js +1 -0
  26. package/dist/packages/scanner/pre-commit-gate.js +1 -0
  27. package/dist/packages/scanner/scan-diff.js +1 -0
  28. package/dist/packages/scanner/scan-runner.js +1 -0
  29. package/dist/packages/scanner/secret-scanner.js +1 -0
  30. package/dist/packages/scanner/sentinel-core.js +1 -0
  31. package/dist/packages/scanner/sentinel-knowledge.js +1 -0
  32. package/dist/packages/scanner/tech-debt-analyzer.js +1 -0
  33. package/dist/packages/scanner/tech-debt-cost.js +1 -0
  34. package/dist/packages/scanner/widget-generator.js +1 -0
  35. package/package.json +16 -8
  36. package/cli.js +0 -2412
  37. package/packages/scanner/ai-confidence-scorer.js +0 -260
  38. package/packages/scanner/alert-manager.js +0 -398
  39. package/packages/scanner/baseline-manager.js +0 -109
  40. package/packages/scanner/code-scanner.js +0 -713
  41. package/packages/scanner/codebase-passport.js +0 -299
  42. package/packages/scanner/copy-paste-detector.js +0 -250
  43. package/packages/scanner/dependency-graph.js +0 -536
  44. package/packages/scanner/drift-detector.js +0 -423
  45. package/packages/scanner/executive-report.js +0 -774
  46. package/packages/scanner/file-watcher.js +0 -323
  47. package/packages/scanner/ghost-code-detector.js +0 -226
  48. package/packages/scanner/hallucination-detector.js +0 -652
  49. package/packages/scanner/health-checker.js +0 -586
  50. package/packages/scanner/html-report.js +0 -634
  51. package/packages/scanner/index.js +0 -92
  52. package/packages/scanner/license-manager.js +0 -318
  53. package/packages/scanner/master-health-checker.js +0 -513
  54. package/packages/scanner/pre-commit-gate.js +0 -216
  55. package/packages/scanner/sentinel-core.js +0 -608
  56. package/packages/scanner/sentinel-knowledge.js +0 -322
  57. package/packages/scanner/tech-debt-analyzer.js +0 -565
  58. package/packages/scanner/tech-debt-cost.js +0 -194
  59. package/packages/scanner/widget-generator.js +0 -415
@@ -1,260 +0,0 @@
1
- /**
2
- * Thuban AI Confidence Scorer
3
- *
4
- * @purpose Score each function's probability of being AI-generated
5
- * @module thuban
6
- * @layer scanner
7
- * @exports-to cli
8
- * @critical-for USP — nobody else gives per-function AI probability scores
9
- */
10
-
11
- const fs = require('fs');
12
- const path = require('path');
13
-
14
- // Signals that indicate AI-generated code
15
- const AI_SIGNALS = {
16
- // Naming patterns AI tends to use
17
- genericNames: ['data', 'result', 'response', 'output', 'input', 'temp', 'item', 'element', 'val', 'obj', 'arr', 'str', 'num', 'flag', 'status'],
18
-
19
- // Comment patterns AI adds
20
- aiCommentPatterns: [
21
- /\/\/\s*TODO:?\s*implement/i,
22
- /\/\/\s*handle\s+error/i,
23
- /\/\/\s*add\s+error\s+handling/i,
24
- /\/\/\s*you\s+(can|may|might|should)/i,
25
- /\/\/\s*replace\s+(this|with)/i,
26
- /\/\/\s*this\s+(function|method|class)\s+(takes|accepts|returns|handles)/i,
27
- /\/\/\s*example\s+usage/i,
28
- /\/\/\s*note:?\s*this/i,
29
- /\/\*\*[\s\S]*?@description\s/,
30
- /\/\/\s*import.*if\s+needed/i,
31
- ],
32
-
33
- // Structural patterns
34
- aiStructurePatterns: [
35
- /try\s*\{[\s\S]*?\}\s*catch\s*\(\s*(?:error|err|e)\s*\)\s*\{[\s\S]*?console\.(log|error)\s*\(\s*(?:error|err|e)\s*\)/, // Generic try/catch with console.log
36
- /if\s*\(!.*\)\s*\{?\s*(?:return|throw)\s/, // Guard clause pattern (AI loves these)
37
- ],
38
- };
39
-
40
- class AIConfidenceScorer {
41
- constructor(opts = {}) {
42
- this.rootPath = opts.rootPath || process.cwd();
43
- }
44
-
45
- /**
46
- * Score all functions in the given files
47
- */
48
- scan(files) {
49
- const results = [];
50
-
51
- for (const file of files) {
52
- let content;
53
- try {
54
- const stat = fs.statSync(file);
55
- if (stat.size > 512 * 1024) continue;
56
- content = fs.readFileSync(file, 'utf-8');
57
- } catch (e) {
58
- continue;
59
- }
60
-
61
- const relPath = path.relative(this.rootPath, file);
62
- if (this._isSkippable(relPath)) continue;
63
-
64
- const functions = this._extractFunctions(content, relPath);
65
- results.push(...functions);
66
- }
67
-
68
- // Sort by confidence descending
69
- results.sort((a, b) => b.confidence - a.confidence);
70
-
71
- const highConfidence = results.filter(r => r.confidence >= 70);
72
- const mediumConfidence = results.filter(r => r.confidence >= 40 && r.confidence < 70);
73
-
74
- return {
75
- functions: results,
76
- highConfidence,
77
- mediumConfidence,
78
- totalFunctions: results.length,
79
- aiLikelyCount: highConfidence.length,
80
- aiPossibleCount: mediumConfidence.length,
81
- aiPercentage: results.length > 0
82
- ? Math.round((highConfidence.length / results.length) * 100)
83
- : 0,
84
- };
85
- }
86
-
87
- // ─── Private ─────────────────────────────────────────────
88
-
89
- _extractFunctions(content, filePath) {
90
- const lines = content.split('\n');
91
- const functions = [];
92
-
93
- for (let i = 0; i < lines.length; i++) {
94
- const fn = this._detectFunction(lines, i);
95
- if (!fn) continue;
96
-
97
- const endLine = this._findFunctionEnd(lines, i);
98
- const body = lines.slice(i, endLine + 1).join('\n');
99
- const bodyLines = lines.slice(i, endLine + 1);
100
-
101
- const signals = this._scoreFunction(fn.name, body, bodyLines, lines, i);
102
-
103
- functions.push({
104
- file: filePath,
105
- name: fn.name,
106
- line: i + 1,
107
- lineCount: endLine - i + 1,
108
- confidence: signals.score,
109
- signals: signals.reasons,
110
- testCoverage: this._hasTests(fn.name, filePath) ? 'found' : 'none',
111
- category: 'ai_confidence',
112
- severity: signals.score >= 80 ? 'high' : signals.score >= 50 ? 'medium' : 'low',
113
- });
114
-
115
- i = endLine; // Skip past the function
116
- }
117
-
118
- return functions;
119
- }
120
-
121
- _scoreFunction(name, body, bodyLines, allLines, startLine) {
122
- let score = 0;
123
- const reasons = [];
124
-
125
- // Signal 1: Generic variable names (0-15 points)
126
- const genericCount = AI_SIGNALS.genericNames.filter(g => {
127
- const regex = new RegExp(`\\b${g}\\b`, 'g');
128
- return (body.match(regex) || []).length >= 2;
129
- }).length;
130
- if (genericCount >= 3) {
131
- score += 15;
132
- reasons.push(`${genericCount} generic variable names (data, result, response...)`);
133
- } else if (genericCount >= 1) {
134
- score += 8;
135
- reasons.push(`${genericCount} generic variable name(s)`);
136
- }
137
-
138
- // Signal 2: AI-style comments (0-20 points)
139
- const aiComments = AI_SIGNALS.aiCommentPatterns.filter(p => p.test(body)).length;
140
- if (aiComments >= 2) {
141
- score += 20;
142
- reasons.push(`${aiComments} AI-style comments (TODO: implement, handle error, etc.)`);
143
- } else if (aiComments >= 1) {
144
- score += 10;
145
- reasons.push('AI-style comment detected');
146
- }
147
-
148
- // Signal 3: Overly thorough JSDoc (0-10 points)
149
- if (startLine > 0) {
150
- const precedingLines = allLines.slice(Math.max(0, startLine - 10), startLine).join('\n');
151
- if (/\/\*\*[\s\S]*@param[\s\S]*@returns?[\s\S]*@(?:throws|example)/s.test(precedingLines)) {
152
- score += 10;
153
- reasons.push('Exhaustive JSDoc (params + returns + throws/example)');
154
- }
155
- }
156
-
157
- // Signal 4: Perfect error handling pattern (0-10 points)
158
- const tryCatchCount = (body.match(/try\s*\{/g) || []).length;
159
- if (tryCatchCount >= 2) {
160
- score += 10;
161
- reasons.push('Multiple try/catch blocks (AI over-handles errors)');
162
- }
163
-
164
- // Signal 5: Function is long and self-contained (0-15 points)
165
- const lineCount = bodyLines.length;
166
- if (lineCount >= 50) {
167
- score += 15;
168
- reasons.push(`${lineCount} lines — large self-contained function`);
169
- } else if (lineCount >= 25) {
170
- score += 8;
171
- reasons.push(`${lineCount} lines`);
172
- }
173
-
174
- // Signal 6: No references to other functions in the same file (0-10 points)
175
- // AI tends to generate isolated, self-contained functions
176
- const internalCalls = allLines
177
- .slice(0, startLine)
178
- .concat(allLines.slice(startLine + lineCount))
179
- .filter(l => l.includes(name)).length;
180
- if (internalCalls === 0 && lineCount > 10) {
181
- score += 10;
182
- reasons.push('Isolated — no internal references');
183
- }
184
-
185
- // Signal 7: console.log in error handling (0-10 points)
186
- if (/catch\s*\([^)]*\)\s*\{[^}]*console\.(log|error)\s*\(/.test(body)) {
187
- score += 8;
188
- reasons.push('console.log/error in catch block (AI default pattern)');
189
- }
190
-
191
- // Signal 8: Arrow functions with immediate destructuring (0-5 points)
192
- if (/=>\s*\{[\s\n\r]*const\s*\{/.test(body)) {
193
- score += 5;
194
- reasons.push('Immediate destructuring after arrow (common AI pattern)');
195
- }
196
-
197
- // Signal 9: Template literal heavy (0-5 points)
198
- const templateCount = (body.match(/`[^`]*\$\{/g) || []).length;
199
- if (templateCount >= 4) {
200
- score += 5;
201
- reasons.push('Heavy template literal usage');
202
- }
203
-
204
- // Cap at 100
205
- score = Math.min(100, score);
206
-
207
- return { score, reasons };
208
- }
209
-
210
- _detectFunction(lines, lineIndex) {
211
- const line = lines[lineIndex].trim();
212
-
213
- let match = line.match(/^(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/);
214
- if (match) return { name: match[1] };
215
-
216
- match = line.match(/^(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?(?:function|\()/);
217
- if (match && (line.includes('=>') || line.includes('function'))) return { name: match[1] };
218
-
219
- return null;
220
- }
221
-
222
- _findFunctionEnd(lines, startLine) {
223
- let depth = 0;
224
- let started = false;
225
-
226
- for (let i = startLine; i < lines.length && i < startLine + 300; i++) {
227
- for (const ch of lines[i]) {
228
- if (ch === '{') { depth++; started = true; }
229
- if (ch === '}') depth--;
230
- }
231
- if (started && depth <= 0) return i;
232
- }
233
-
234
- return Math.min(startLine + 20, lines.length - 1);
235
- }
236
-
237
- _hasTests(funcName, filePath) {
238
- // Check if a test file exists for this file
239
- const testVariants = [
240
- filePath.replace(/\.(js|ts)$/, '.test.$1'),
241
- filePath.replace(/\.(js|ts)$/, '.spec.$1'),
242
- filePath.replace(/src\//, 'test/'),
243
- filePath.replace(/src\//, '__tests__/'),
244
- ];
245
- return testVariants.some(t => {
246
- try { return fs.existsSync(path.resolve(this.rootPath, t)); } catch (e) { return false; }
247
- });
248
- }
249
-
250
- _isSkippable(relPath) {
251
- const skip = [
252
- /\.test\./i, /\.spec\./i, /test[\/\\]/i, /spec[\/\\]/i,
253
- /__test__/i, /__mocks__/i, /\.config\./i, /\.d\.ts$/,
254
- /node_modules/i, /\.min\./i,
255
- ];
256
- return skip.some(r => r.test(relPath));
257
- }
258
- }
259
-
260
- module.exports = AIConfidenceScorer;
@@ -1,398 +0,0 @@
1
- /**
2
- * Alert Manager
3
- *
4
- * @purpose Event-driven AlertManager component
5
- * @module sentinel
6
- * @layer application
7
- * @imports-from fs, path, events
8
- * @exports-to sentinel
9
- * @side-effects File system writes, File system reads, Console output, Event emission
10
- * @critical-for sentinel
11
- */
12
-
13
- /**
14
- * Sentinel Alert Manager
15
- *
16
- * Handles alerting and reporting for the Sentinel system.
17
- * Outputs: Console, files, and future integrations (Slack, email, GitHub issues)
18
- */
19
-
20
- const fs = require('fs');
21
- const path = require('path');
22
- const EventEmitter = require('events');
23
-
24
- class AlertManager extends EventEmitter {
25
- constructor(config = {}) {
26
- super();
27
-
28
- this.config = {
29
- rootPath: config.rootPath || process.cwd(),
30
- alertsDir: config.alertsDir || path.join(config.rootPath || process.cwd(), 'sentinel', 'alerts'),
31
- reportsDir: config.reportsDir || path.join(config.rootPath || process.cwd(), 'sentinel', 'reports'),
32
- consoleOutput: config.consoleOutput !== false,
33
- fileOutput: config.fileOutput !== false,
34
- maxAlertsPerFile: config.maxAlertsPerFile || 1000,
35
- slackWebhook: config.slackWebhook || null,
36
- emailConfig: config.emailConfig || null,
37
- ...config
38
- };
39
-
40
- // Ensure directories exist
41
- this._ensureDirectories();
42
-
43
- // Alert history for deduplication
44
- this.recentAlerts = new Map();
45
- this.alertDedupeWindowMs = config.alertDedupeWindowMs || 60000; // 1 minute
46
- }
47
-
48
- /**
49
- * Send an alert
50
- */
51
- async sendAlert(alert) {
52
- const normalizedAlert = this._normalizeAlert(alert);
53
-
54
- // Check for duplicate
55
- if (this._isDuplicate(normalizedAlert)) {
56
- return { sent: false, reason: 'duplicate' };
57
- }
58
-
59
- // Record alert
60
- this._recordAlert(normalizedAlert);
61
-
62
- // Output to configured channels
63
- const results = {
64
- console: false,
65
- file: false,
66
- slack: false,
67
- email: false
68
- };
69
-
70
- // Console output
71
- if (this.config.consoleOutput) {
72
- results.console = this._outputToConsole(normalizedAlert);
73
- }
74
-
75
- // File output
76
- if (this.config.fileOutput) {
77
- results.file = await this._outputToFile(normalizedAlert);
78
- }
79
-
80
- // Slack output (if configured and critical/error)
81
- if (this.config.slackWebhook && ['critical', 'error'].includes(normalizedAlert.level)) {
82
- results.slack = await this._outputToSlack(normalizedAlert);
83
- }
84
-
85
- this.emit('alertSent', { alert: normalizedAlert, results });
86
-
87
- return { sent: true, alert: normalizedAlert, results };
88
- }
89
-
90
- /**
91
- * Send daily digest
92
- */
93
- async sendDigest(digest) {
94
- console.log('\n' + '='.repeat(60));
95
- console.log('[SENTINEL] DAILY DIGEST - ' + digest.date);
96
- console.log('='.repeat(60));
97
-
98
- console.log(`\nSummary:`);
99
- console.log(` Issues Found: ${digest.summary.issuesFound}`);
100
- console.log(` Critical: ${digest.summary.criticalIssues}`);
101
- console.log(` Warnings: ${digest.summary.warningIssues}`);
102
- console.log(` Files Watched: ${digest.summary.filesWatched}`);
103
- console.log(` Last Scan: ${digest.summary.lastScan || 'N/A'}`);
104
-
105
- if (digest.topIssues && digest.topIssues.length > 0) {
106
- console.log(`\nTop Issues:`);
107
- for (const issue of digest.topIssues.slice(0, 5)) {
108
- console.log(` [${issue.severity.toUpperCase()}] ${issue.message}`);
109
- if (issue.file) {
110
- console.log(` File: ${path.relative(this.config.rootPath, issue.file)}`);
111
- }
112
- }
113
- }
114
-
115
- if (digest.recommendations && digest.recommendations.length > 0) {
116
- console.log(`\nRecommendations:`);
117
- for (const rec of digest.recommendations) {
118
- console.log(` [${rec.priority.toUpperCase()}] ${rec.message}`);
119
- }
120
- }
121
-
122
- console.log('\n' + '='.repeat(60) + '\n');
123
-
124
- // Save digest to file
125
- if (this.config.fileOutput) {
126
- const digestPath = path.join(
127
- this.config.reportsDir,
128
- `digest-${digest.date}.json`
129
- );
130
- fs.writeFileSync(digestPath, JSON.stringify(digest, null, 2));
131
- }
132
-
133
- this.emit('digestSent', digest);
134
- return { sent: true };
135
- }
136
-
137
- /**
138
- * Send weekly report
139
- */
140
- async sendWeeklyReport(report) {
141
- console.log('\n' + '='.repeat(70));
142
- console.log('[SENTINEL] WEEKLY REPORT - Week of ' + report.weekOf);
143
- console.log('='.repeat(70));
144
-
145
- console.log(`\nMetrics:`);
146
- console.log(` Total Scans: ${report.summary.totalScans}`);
147
- console.log(` Issues Detected: ${report.summary.issuesDetected}`);
148
- console.log(` Issues Resolved: ${report.summary.issuesResolved}`);
149
- console.log(` Avg Scan Time: ${report.summary.averageScanTime}ms`);
150
-
151
- console.log(`\nCode Quality:`);
152
- console.log(` Score: ${report.codeQuality.score}/100 (${report.codeQuality.grade})`);
153
- console.log(` Complexity: ${report.codeQuality.factors.complexity}`);
154
- console.log(` Maintainability: ${report.codeQuality.factors.maintainability}`);
155
- console.log(` Test Coverage: ${report.codeQuality.factors.testCoverage}%`);
156
-
157
- console.log(`\nTechnical Debt:`);
158
- console.log(` Estimated Hours: ${report.technicalDebt.estimatedHours}`);
159
- console.log(` Refactoring: ${report.technicalDebt.categories.refactoring}h`);
160
- console.log(` Documentation: ${report.technicalDebt.categories.documentation}h`);
161
- console.log(` Testing: ${report.technicalDebt.categories.testing}h`);
162
-
163
- if (report.recommendations && report.recommendations.length > 0) {
164
- console.log(`\nRecommendations:`);
165
- for (const rec of report.recommendations) {
166
- console.log(` [${rec.priority.toUpperCase()}] ${rec.message}`);
167
- }
168
- }
169
-
170
- console.log('\n' + '='.repeat(70) + '\n');
171
-
172
- // Save report to file
173
- if (this.config.fileOutput) {
174
- const reportPath = path.join(
175
- this.config.reportsDir,
176
- `weekly-report-${report.weekOf}.json`
177
- );
178
- fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
179
- }
180
-
181
- this.emit('weeklyReportSent', report);
182
- return { sent: true };
183
- }
184
-
185
- /**
186
- * Get alert history
187
- */
188
- getAlertHistory(options = {}) {
189
- const { days = 7, level = null, category = null } = options;
190
-
191
- const cutoff = new Date();
192
- cutoff.setDate(cutoff.getDate() - days);
193
-
194
- // Read from alert files
195
- const alerts = [];
196
-
197
- try {
198
- if (fs.existsSync(this.config.alertsDir)) {
199
- const files = fs.readdirSync(this.config.alertsDir)
200
- .filter(f => f.startsWith('alerts-') && f.endsWith('.json'))
201
- .sort()
202
- .reverse();
203
-
204
- for (const file of files) {
205
- const fileDate = file.match(/alerts-(\d{4}-\d{2}-\d{2})/);
206
- if (fileDate && new Date(fileDate[1]) < cutoff) {
207
- break;
208
- }
209
-
210
- try {
211
- const content = JSON.parse(fs.readFileSync(
212
- path.join(this.config.alertsDir, file),
213
- 'utf8'
214
- ));
215
- alerts.push(...(content.alerts || []));
216
- } catch (e) {
217
- // Skip corrupted files
218
- }
219
- }
220
- }
221
- } catch (error) {
222
- console.error('[ALERT-MANAGER] Error reading alert history:', error.message);
223
- }
224
-
225
- // Filter
226
- let filtered = alerts;
227
- if (level) {
228
- filtered = filtered.filter(a => a.level === level);
229
- }
230
- if (category) {
231
- filtered = filtered.filter(a => a.category === category);
232
- }
233
-
234
- return filtered;
235
- }
236
-
237
- /**
238
- * Clear old alerts
239
- */
240
- async clearOldAlerts(daysToKeep = 30) {
241
- const cutoff = new Date();
242
- cutoff.setDate(cutoff.getDate() - daysToKeep);
243
-
244
- let deletedCount = 0;
245
-
246
- try {
247
- if (fs.existsSync(this.config.alertsDir)) {
248
- const files = fs.readdirSync(this.config.alertsDir);
249
-
250
- for (const file of files) {
251
- const fileDate = file.match(/(\d{4}-\d{2}-\d{2})/);
252
- if (fileDate && new Date(fileDate[1]) < cutoff) {
253
- fs.unlinkSync(path.join(this.config.alertsDir, file));
254
- deletedCount++;
255
- }
256
- }
257
- }
258
- } catch (error) {
259
- console.error('[ALERT-MANAGER] Error clearing old alerts:', error.message);
260
- }
261
-
262
- return { deleted: deletedCount };
263
- }
264
-
265
- // Private methods
266
-
267
- _ensureDirectories() {
268
- try {
269
- if (!fs.existsSync(this.config.alertsDir)) {
270
- fs.mkdirSync(this.config.alertsDir, { recursive: true });
271
- }
272
- if (!fs.existsSync(this.config.reportsDir)) {
273
- fs.mkdirSync(this.config.reportsDir, { recursive: true });
274
- }
275
- } catch (error) {
276
- console.warn('[ALERT-MANAGER] Could not create directories:', error.message);
277
- }
278
- }
279
-
280
- _normalizeAlert(alert) {
281
- return {
282
- id: alert.id || this._generateAlertId(),
283
- timestamp: alert.timestamp || new Date().toISOString(),
284
- level: alert.level || 'info',
285
- category: alert.category || 'general',
286
- title: alert.title || 'Alert',
287
- message: alert.message || '',
288
- file: alert.file || null,
289
- line: alert.line || null,
290
- details: alert.details || {}
291
- };
292
- }
293
-
294
- _generateAlertId() {
295
- return `alert-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
296
- }
297
-
298
- _isDuplicate(alert) {
299
- const key = `${alert.level}:${alert.message}:${alert.file}`;
300
- const lastSeen = this.recentAlerts.get(key);
301
-
302
- if (lastSeen && (Date.now() - lastSeen) < this.alertDedupeWindowMs) {
303
- return true;
304
- }
305
-
306
- return false;
307
- }
308
-
309
- _recordAlert(alert) {
310
- const key = `${alert.level}:${alert.message}:${alert.file}`;
311
- this.recentAlerts.set(key, Date.now());
312
-
313
- // Cleanup old entries
314
- const cutoff = Date.now() - this.alertDedupeWindowMs;
315
- for (const [k, v] of this.recentAlerts) {
316
- if (v < cutoff) {
317
- this.recentAlerts.delete(k);
318
- }
319
- }
320
- }
321
-
322
- _outputToConsole(alert) {
323
- const levelColors = {
324
- critical: '\x1b[31m', // Red
325
- error: '\x1b[31m', // Red
326
- warning: '\x1b[33m', // Yellow
327
- info: '\x1b[36m' // Cyan
328
- };
329
-
330
- const reset = '\x1b[0m';
331
- const color = levelColors[alert.level] || '';
332
-
333
- console.log(`${color}[SENTINEL:${alert.level.toUpperCase()}]${reset} ${alert.title}`);
334
- console.log(` ${alert.message}`);
335
-
336
- if (alert.file) {
337
- const relPath = path.relative(this.config.rootPath, alert.file);
338
- console.log(` File: ${relPath}${alert.line ? ':' + alert.line : ''}`);
339
- }
340
-
341
- return true;
342
- }
343
-
344
- async _outputToFile(alert) {
345
- try {
346
- const dateStr = new Date().toISOString().split('T')[0];
347
- const filePath = path.join(this.config.alertsDir, `alerts-${dateStr}.json`);
348
-
349
- let fileData = { date: dateStr, alerts: [] };
350
-
351
- if (fs.existsSync(filePath)) {
352
- try {
353
- fileData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
354
- } catch (e) {
355
- // File corrupted, start fresh
356
- }
357
- }
358
-
359
- fileData.alerts.push(alert);
360
-
361
- // Limit alerts per file
362
- if (fileData.alerts.length > this.config.maxAlertsPerFile) {
363
- fileData.alerts = fileData.alerts.slice(-this.config.maxAlertsPerFile);
364
- }
365
-
366
- fs.writeFileSync(filePath, JSON.stringify(fileData, null, 2));
367
- return true;
368
- } catch (error) {
369
- console.error('[ALERT-MANAGER] File output failed:', error.message);
370
- return false;
371
- }
372
- }
373
-
374
- async _outputToSlack(alert) {
375
- if (!this.config.slackWebhook) {
376
- return false;
377
- }
378
-
379
- try {
380
- // Would use fetch or axios here
381
- // For now, just log that we would send to Slack
382
- console.log(`[ALERT-MANAGER] Would send to Slack: ${alert.title}`);
383
- return true;
384
- } catch (error) {
385
- console.error('[ALERT-MANAGER] Slack output failed:', error.message);
386
- return false;
387
- }
388
- }
389
-
390
- async _createGitHubIssue(alert) {
391
- // Future: Create GitHub issues for critical alerts
392
- // Would use GitHub API or gh CLI
393
- console.log(`[ALERT-MANAGER] Would create GitHub issue: ${alert.title}`);
394
- return false;
395
- }
396
- }
397
-
398
- module.exports = AlertManager;