thuban 0.3.3 → 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 (66) hide show
  1. package/dist/LICENSE +21 -0
  2. package/dist/README.md +185 -0
  3. package/dist/cli.js +2 -0
  4. package/dist/packages/scanner/ai-confidence-scorer.js +1 -0
  5. package/dist/packages/scanner/alert-manager.js +1 -0
  6. package/dist/packages/scanner/baseline-manager.js +1 -0
  7. package/dist/packages/scanner/code-scanner.js +1 -0
  8. package/dist/packages/scanner/codebase-passport.js +1 -0
  9. package/dist/packages/scanner/copy-paste-detector.js +1 -0
  10. package/dist/packages/scanner/dependency-graph.js +1 -0
  11. package/dist/packages/scanner/drift-detector.js +1 -0
  12. package/dist/packages/scanner/executive-report.js +1 -0
  13. package/dist/packages/scanner/file-collector.js +1 -0
  14. package/dist/packages/scanner/file-watcher.js +1 -0
  15. package/dist/packages/scanner/ghost-code-detector.js +1 -0
  16. package/dist/packages/scanner/hallucination-detector.js +1 -0
  17. package/dist/packages/scanner/health-checker.js +1 -0
  18. package/dist/packages/scanner/html-report.js +1 -0
  19. package/dist/packages/scanner/index.js +1 -0
  20. package/dist/packages/scanner/license-manager.js +1 -0
  21. package/dist/packages/scanner/master-health-checker.js +1 -0
  22. package/dist/packages/scanner/monitor-notifier.js +1 -0
  23. package/dist/packages/scanner/monitor-service.js +1 -0
  24. package/dist/packages/scanner/monitor-store.js +1 -0
  25. package/dist/packages/scanner/pre-commit-gate.js +1 -0
  26. package/dist/packages/scanner/scan-diff.js +1 -0
  27. package/dist/packages/scanner/scan-runner.js +1 -0
  28. package/dist/packages/scanner/secret-scanner.js +1 -0
  29. package/dist/packages/scanner/sentinel-core.js +1 -0
  30. package/dist/packages/scanner/sentinel-knowledge.js +1 -0
  31. package/dist/packages/scanner/tech-debt-analyzer.js +1 -0
  32. package/dist/packages/scanner/tech-debt-cost.js +1 -0
  33. package/dist/packages/scanner/widget-generator.js +1 -0
  34. package/package.json +12 -7
  35. package/cli.js +0 -2627
  36. package/packages/scanner/ai-confidence-scorer.js +0 -260
  37. package/packages/scanner/alert-manager.js +0 -398
  38. package/packages/scanner/baseline-manager.js +0 -109
  39. package/packages/scanner/code-scanner.js +0 -758
  40. package/packages/scanner/codebase-passport.js +0 -299
  41. package/packages/scanner/copy-paste-detector.js +0 -276
  42. package/packages/scanner/dependency-graph.js +0 -541
  43. package/packages/scanner/drift-detector.js +0 -423
  44. package/packages/scanner/executive-report.js +0 -774
  45. package/packages/scanner/file-collector.js +0 -64
  46. package/packages/scanner/file-watcher.js +0 -323
  47. package/packages/scanner/ghost-code-detector.js +0 -301
  48. package/packages/scanner/hallucination-detector.js +0 -822
  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 -100
  52. package/packages/scanner/license-manager.js +0 -331
  53. package/packages/scanner/master-health-checker.js +0 -513
  54. package/packages/scanner/monitor-notifier.js +0 -64
  55. package/packages/scanner/monitor-service.js +0 -103
  56. package/packages/scanner/monitor-store.js +0 -117
  57. package/packages/scanner/pre-commit-gate.js +0 -216
  58. package/packages/scanner/scan-diff.js +0 -50
  59. package/packages/scanner/scan-runner.js +0 -172
  60. package/packages/scanner/secret-scanner.js +0 -612
  61. package/packages/scanner/secret-scanner.test.js +0 -103
  62. package/packages/scanner/sentinel-core.js +0 -616
  63. package/packages/scanner/sentinel-knowledge.js +0 -322
  64. package/packages/scanner/tech-debt-analyzer.js +0 -583
  65. package/packages/scanner/tech-debt-cost.js +0 -194
  66. package/packages/scanner/widget-generator.js +0 -415
@@ -1,117 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const os = require('os');
4
- const crypto = require('crypto');
5
-
6
- const THUBAN_DIR = path.join(os.homedir(), '.thuban');
7
- const MONITOR_DIR = path.join(THUBAN_DIR, 'monitor');
8
- const CONFIG_FILE = path.join(MONITOR_DIR, 'config.json');
9
- const RUNS_DIR = path.join(MONITOR_DIR, 'runs');
10
- const NOTIFICATIONS_FILE = path.join(MONITOR_DIR, 'notifications.json');
11
-
12
- function ensureDir(dirPath) {
13
- if (!fs.existsSync(dirPath)) {
14
- fs.mkdirSync(dirPath, { recursive: true });
15
- }
16
- }
17
-
18
- function safeReadJson(filePath, fallback) {
19
- try {
20
- return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
21
- } catch (error) {
22
- return fallback;
23
- }
24
- }
25
-
26
- class MonitorStore {
27
- constructor() {
28
- ensureDir(THUBAN_DIR);
29
- ensureDir(MONITOR_DIR);
30
- ensureDir(RUNS_DIR);
31
- }
32
-
33
- getRepoId(repoPath) {
34
- return crypto.createHash('sha256').update(path.resolve(repoPath)).digest('hex').substring(0, 12);
35
- }
36
-
37
- loadConfig() {
38
- return safeReadJson(CONFIG_FILE, { version: 1, repos: [] });
39
- }
40
-
41
- saveConfig(config) {
42
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
43
- }
44
-
45
- upsertRepo(repoConfig) {
46
- const config = this.loadConfig();
47
- const repoId = repoConfig.repoId || this.getRepoId(repoConfig.repoPath);
48
- const nextRepo = {
49
- repoId,
50
- repoPath: path.resolve(repoConfig.repoPath),
51
- frequency: repoConfig.frequency || 'daily',
52
- intervalMs: repoConfig.intervalMs,
53
- notification: repoConfig.notification || { channels: ['inbox'] },
54
- createdAt: repoConfig.createdAt || new Date().toISOString(),
55
- updatedAt: new Date().toISOString(),
56
- enabled: repoConfig.enabled !== false,
57
- lastRunAt: repoConfig.lastRunAt || null,
58
- nextRunAt: repoConfig.nextRunAt || null,
59
- };
60
-
61
- const index = config.repos.findIndex(repo => repo.repoId === repoId);
62
- if (index >= 0) config.repos[index] = { ...config.repos[index], ...nextRepo };
63
- else config.repos.push(nextRepo);
64
-
65
- this.saveConfig(config);
66
- return nextRepo;
67
- }
68
-
69
- listRepos() {
70
- return this.loadConfig().repos || [];
71
- }
72
-
73
- getRepo(repoIdOrPath) {
74
- const resolved = repoIdOrPath ? path.resolve(repoIdOrPath) : null;
75
- return this.listRepos().find(repo => repo.repoId === repoIdOrPath || repo.repoPath === resolved) || null;
76
- }
77
-
78
- getRepoRunDir(repoId) {
79
- const dirPath = path.join(RUNS_DIR, repoId);
80
- ensureDir(dirPath);
81
- return dirPath;
82
- }
83
-
84
- saveRun(repoId, run) {
85
- const runDir = this.getRepoRunDir(repoId);
86
- const filePath = path.join(runDir, `${run.timestamp.replace(/[:.]/g, '-')}.json`);
87
- fs.writeFileSync(filePath, JSON.stringify(run, null, 2), 'utf-8');
88
- return filePath;
89
- }
90
-
91
- listRuns(repoId) {
92
- const runDir = this.getRepoRunDir(repoId);
93
- return fs.readdirSync(runDir)
94
- .filter(file => file.endsWith('.json'))
95
- .sort()
96
- .map(file => safeReadJson(path.join(runDir, file), null))
97
- .filter(Boolean);
98
- }
99
-
100
- getLatestRun(repoId) {
101
- const runs = this.listRuns(repoId);
102
- return runs.length ? runs[runs.length - 1] : null;
103
- }
104
-
105
- appendNotification(notification) {
106
- const payload = safeReadJson(NOTIFICATIONS_FILE, { notifications: [] });
107
- payload.notifications.push(notification);
108
- fs.writeFileSync(NOTIFICATIONS_FILE, JSON.stringify(payload, null, 2), 'utf-8');
109
- }
110
-
111
- getNotifications(limit = 20) {
112
- const payload = safeReadJson(NOTIFICATIONS_FILE, { notifications: [] });
113
- return payload.notifications.slice(-limit).reverse();
114
- }
115
- }
116
-
117
- module.exports = MonitorStore;
@@ -1,216 +0,0 @@
1
- /**
2
- * Thuban Pre-Commit Gate
3
- *
4
- * @purpose 3-second critical scan on staged files — blocks commits with hallucinations
5
- * @module thuban
6
- * @layer scanner
7
- * @exports-to cli
8
- * @critical-for Revenue retention — stickiest feature, devs never remove it
9
- */
10
-
11
- const fs = require('fs');
12
- const path = require('path');
13
- const { execSync } = require('child_process');
14
-
15
- class PreCommitGate {
16
- constructor(opts = {}) {
17
- this.rootPath = opts.rootPath || process.cwd();
18
- this.strict = opts.strict !== false; // Block commit on critical issues
19
- this.timeout = opts.timeout || 5000; // 5 second max
20
- }
21
-
22
- /**
23
- * Get staged files from git
24
- */
25
- getStagedFiles() {
26
- try {
27
- const output = execSync('git diff --cached --name-only --diff-filter=ACM', {
28
- cwd: this.rootPath,
29
- encoding: 'utf-8',
30
- timeout: 3000,
31
- });
32
- const extensions = ['.js', '.ts', '.jsx', '.tsx', '.py', '.mjs', '.cjs', '.php', '.rb'];
33
- return output
34
- .split('\n')
35
- .filter(f => f.trim() && extensions.some(ext => f.endsWith(ext)))
36
- .map(f => path.resolve(this.rootPath, f.trim()));
37
- } catch (e) {
38
- return [];
39
- }
40
- }
41
-
42
- /**
43
- * Quick hallucination check on a single file
44
- */
45
- checkFile(filePath) {
46
- const issues = [];
47
- let content;
48
- try {
49
- const stat = fs.statSync(filePath);
50
- if (stat.size > 512 * 1024) return issues; // Skip files > 512KB
51
- content = fs.readFileSync(filePath, 'utf-8');
52
- } catch (e) {
53
- return issues;
54
- }
55
-
56
- const lines = content.split('\n');
57
- const relPath = path.relative(this.rootPath, filePath);
58
-
59
- // Phantom API patterns — APIs that don't exist but AI commonly generates
60
- const phantomAPIs = [
61
- { pattern: /\bJSON\.tryParse\s*\(/g, name: 'JSON.tryParse()', fix: 'Use JSON.parse() with try/catch' },
62
- { pattern: /\bArray\.flatten\s*\(/g, name: 'Array.flatten()', fix: 'Use Array.flat()' },
63
- { pattern: /\bconsole\.success\s*\(/g, name: 'console.success()', fix: 'Use console.log()' },
64
- { pattern: /\bconsole\.fail\s*\(/g, name: 'console.fail()', fix: 'Use console.error()' },
65
- { pattern: /\bfs\.readFileAsync\s*\(/g, name: 'fs.readFileAsync()', fix: 'Use fs.promises.readFile()' },
66
- { pattern: /\bfs\.writeFileAsync\s*\(/g, name: 'fs.writeFileAsync()', fix: 'Use fs.promises.writeFile()' },
67
- { pattern: /\bfs\.existsAsync\s*\(/g, name: 'fs.existsAsync()', fix: 'Use fs.promises.access()' },
68
- { pattern: /\bObject\.deepClone\s*\(/g, name: 'Object.deepClone()', fix: 'Use structuredClone() or JSON.parse(JSON.stringify())' },
69
- { pattern: /\bString\.prototype\.contains\s*\(/g, name: 'String.contains()', fix: 'Use String.includes()' },
70
- { pattern: /\bPromise\.delay\s*\(/g, name: 'Promise.delay()', fix: 'Use new Promise(r => setTimeout(r, ms))' },
71
- { pattern: /\bprocess\.exit\s*\(\s*["'].*["']\s*\)/g, name: 'process.exit(string)', fix: 'process.exit() takes a number, not a string' },
72
- { pattern: /\brequire\s*\(\s*['"]node:(?:fetch|axios)['"]s*\)/g, name: 'require("node:fetch")', fix: 'fetch is global in Node 18+, or use node-fetch package' },
73
- { pattern: /\.validateSync\s*\(/g, name: '.validateSync()', fix: 'Check if this method exists on the object — AI commonly invents Sync variants' },
74
- { pattern: /\bBuffer\.from\s*\(.*,\s*['"]base64url['"]\)/g, name: 'Buffer.from(x, "base64url")', fix: 'Use "base64" encoding, not "base64url"' },
75
- { pattern: /\bresponse\.json\s*\(\s*{/g, name: 'response.json({...})', fix: 'res.json() takes data, but check you\'re not confusing fetch Response.json() (no args) with Express res.json()' },
76
- ];
77
-
78
- // Skip if this looks like a pattern definition file
79
- const patternDefLines = lines.filter(l => /pattern:\s*\//.test(l)).length;
80
- if (patternDefLines > 5) return issues;
81
-
82
- for (const api of phantomAPIs) {
83
- for (let i = 0; i < lines.length; i++) {
84
- const line = lines[i];
85
- // Skip comments and pattern definitions
86
- if (/^\s*(\/\/|\/\*|\*|#|pattern:|name:|suggestion:|message:)/.test(line)) continue;
87
- if (/thuban-ignore/.test(line)) continue;
88
-
89
- if (api.pattern.test(line)) {
90
- issues.push({
91
- file: relPath,
92
- line: i + 1,
93
- severity: 'critical',
94
- code: line.trim(),
95
- message: `${api.name} — this API does not exist`,
96
- fix: api.fix,
97
- });
98
- }
99
- // Reset regex lastIndex
100
- api.pattern.lastIndex = 0;
101
- }
102
- }
103
-
104
- return issues;
105
- }
106
-
107
- /**
108
- * Run the gate check on all staged files
109
- */
110
- run() {
111
- const startTime = Date.now();
112
- const stagedFiles = this.getStagedFiles();
113
-
114
- if (stagedFiles.length === 0) {
115
- return {
116
- passed: true,
117
- files: 0,
118
- issues: [],
119
- elapsed: Date.now() - startTime,
120
- message: 'No staged files to check',
121
- };
122
- }
123
-
124
- const allIssues = [];
125
- for (const file of stagedFiles) {
126
- if (Date.now() - startTime > this.timeout) break; // Respect timeout
127
- const fileIssues = this.checkFile(file);
128
- allIssues.push(...fileIssues);
129
- }
130
-
131
- const criticals = allIssues.filter(i => i.severity === 'critical');
132
- const passed = this.strict ? criticals.length === 0 : true;
133
-
134
- return {
135
- passed,
136
- files: stagedFiles.length,
137
- issues: allIssues,
138
- criticals: criticals.length,
139
- elapsed: Date.now() - startTime,
140
- message: passed
141
- ? `${stagedFiles.length} files checked. All clear.`
142
- : `BLOCKED: ${criticals.length} critical issue${criticals.length > 1 ? 's' : ''} found in staged files.`,
143
- };
144
- }
145
-
146
- /**
147
- * Install the git pre-commit hook
148
- */
149
- static install(rootPath = process.cwd()) {
150
- const gitDir = path.join(rootPath, '.git');
151
- if (!fs.existsSync(gitDir)) {
152
- return { success: false, error: 'Not a git repository' };
153
- }
154
-
155
- const hooksDir = path.join(gitDir, 'hooks');
156
- if (!fs.existsSync(hooksDir)) {
157
- fs.mkdirSync(hooksDir, { recursive: true });
158
- }
159
-
160
- const hookPath = path.join(hooksDir, 'pre-commit');
161
- const hookContent = `#!/bin/sh
162
- # Thuban Pre-Commit Gate — blocks commits with hallucinated APIs
163
- # Remove this file to disable, or run: thuban gate --uninstall
164
-
165
- npx --yes thuban gate --ci 2>&1
166
- exit $?
167
- `;
168
-
169
- // Check if hook already exists
170
- if (fs.existsSync(hookPath)) {
171
- const existing = fs.readFileSync(hookPath, 'utf-8');
172
- if (existing.includes('thuban gate')) {
173
- return { success: true, message: 'Thuban gate already installed' };
174
- }
175
- // Append to existing hook
176
- fs.appendFileSync(hookPath, '\n' + hookContent);
177
- } else {
178
- fs.writeFileSync(hookPath, hookContent);
179
- }
180
-
181
- // Make executable on Unix
182
- try { fs.chmodSync(hookPath, '755'); } catch (e) { /* Windows */ }
183
-
184
- return { success: true, message: 'Pre-commit hook installed' };
185
- }
186
-
187
- /**
188
- * Uninstall the git pre-commit hook
189
- */
190
- static uninstall(rootPath = process.cwd()) {
191
- const hookPath = path.join(rootPath, '.git', 'hooks', 'pre-commit');
192
- if (!fs.existsSync(hookPath)) {
193
- return { success: true, message: 'No pre-commit hook found' };
194
- }
195
-
196
- const content = fs.readFileSync(hookPath, 'utf-8');
197
- if (content.includes('thuban gate')) {
198
- // Remove thuban lines
199
- const cleaned = content
200
- .split('\n')
201
- .filter(l => !l.includes('thuban') && !l.includes('Thuban'))
202
- .join('\n')
203
- .trim();
204
-
205
- if (cleaned.length < 10) {
206
- fs.unlinkSync(hookPath);
207
- } else {
208
- fs.writeFileSync(hookPath, cleaned + '\n');
209
- }
210
- }
211
-
212
- return { success: true, message: 'Thuban gate uninstalled' };
213
- }
214
- }
215
-
216
- module.exports = PreCommitGate;
@@ -1,50 +0,0 @@
1
- function toMap(issues = []) {
2
- const map = new Map();
3
- for (const issue of issues) {
4
- map.set(issue.fingerprint, issue);
5
- }
6
- return map;
7
- }
8
-
9
- function diffRuns(previousRun, currentRun) {
10
- const previousIssues = previousRun?.issues || [];
11
- const currentIssues = currentRun?.issues || [];
12
- const previousMap = toMap(previousIssues);
13
- const currentMap = toMap(currentIssues);
14
-
15
- const added = [];
16
- const resolved = [];
17
-
18
- for (const issue of currentIssues) {
19
- if (!previousMap.has(issue.fingerprint)) added.push(issue);
20
- }
21
-
22
- for (const issue of previousIssues) {
23
- if (!currentMap.has(issue.fingerprint)) resolved.push(issue);
24
- }
25
-
26
- const previousMetrics = previousRun?.metrics || {};
27
- const currentMetrics = currentRun?.metrics || {};
28
-
29
- return {
30
- added,
31
- resolved,
32
- unchangedCount: currentIssues.length - added.length,
33
- metricsDelta: {
34
- issueCount: (currentMetrics.issueCount || 0) - (previousMetrics.issueCount || 0),
35
- hallucinations: (currentMetrics.hallucinations || 0) - (previousMetrics.hallucinations || 0),
36
- secrets: (currentMetrics.secrets || 0) - (previousMetrics.secrets || 0),
37
- techDebtScore: currentMetrics.techDebtScore == null || previousMetrics.techDebtScore == null
38
- ? null
39
- : currentMetrics.techDebtScore - previousMetrics.techDebtScore,
40
- techDebtHours: currentMetrics.techDebtHours == null || previousMetrics.techDebtHours == null
41
- ? null
42
- : currentMetrics.techDebtHours - previousMetrics.techDebtHours,
43
- },
44
- hasNewIssues: added.length > 0,
45
- };
46
- }
47
-
48
- module.exports = {
49
- diffRuns,
50
- };
@@ -1,172 +0,0 @@
1
- const path = require('path');
2
-
3
- const CodeScanner = require('./code-scanner.js');
4
- const HallucinationDetector = require('./hallucination-detector.js');
5
- const SecretScanner = require('./secret-scanner.js');
6
- const DriftDetector = require('./drift-detector.js');
7
- const TechDebtAnalyzer = require('./tech-debt-analyzer.js');
8
-
9
- function normalizeIssue(rootPath, issue) {
10
- const fileValue = issue.file || issue.path || '';
11
- const relativeFile = path.isAbsolute(fileValue)
12
- ? path.relative(rootPath, fileValue)
13
- : fileValue;
14
-
15
- return {
16
- file: relativeFile,
17
- line: issue.line || null,
18
- severity: issue.severity || 'unknown',
19
- category: issue.category || 'unknown',
20
- id: issue.id || null,
21
- message: issue.message || '',
22
- suggestion: issue.suggestion || null,
23
- fixable: issue.fixable !== false,
24
- };
25
- }
26
-
27
- function fingerprintIssue(issue) {
28
- return [
29
- issue.file || '',
30
- issue.category || '',
31
- issue.id || '',
32
- String(issue.line || ''),
33
- (issue.message || '').substring(0, 160),
34
- ].join('|');
35
- }
36
-
37
- function summarizeIssues(issues) {
38
- const summary = {
39
- total: issues.length,
40
- bySeverity: {},
41
- byCategory: {},
42
- };
43
-
44
- for (const issue of issues) {
45
- summary.bySeverity[issue.severity] = (summary.bySeverity[issue.severity] || 0) + 1;
46
- summary.byCategory[issue.category] = (summary.byCategory[issue.category] || 0) + 1;
47
- }
48
-
49
- return summary;
50
- }
51
-
52
- async function runScanPipeline({ rootPath, files, ignorePatterns = [] }) {
53
- const scanner = new CodeScanner({
54
- rootPath,
55
- ignorePatterns: ['node_modules/**', '.git/**', 'dist/**', ...ignorePatterns],
56
- });
57
- const hallucinationDetector = new HallucinationDetector({ rootPath });
58
- const secretScanner = new SecretScanner({ rootPath });
59
- const driftDetector = new DriftDetector({ rootPath });
60
- const techDebtAnalyzer = new TechDebtAnalyzer({ rootPath });
61
-
62
- const [scanResults, hallResults, secretResults, driftResults, debtResults] = await Promise.all([
63
- scanner.scanFiles(files),
64
- hallucinationDetector.scan(files),
65
- secretScanner.scanFiles(files),
66
- driftDetector.detectAll ? driftDetector.detectAll() : Promise.resolve({ issues: [] }),
67
- techDebtAnalyzer.analyze ? techDebtAnalyzer.analyze(files) : Promise.resolve(null),
68
- ]);
69
-
70
- const issues = [];
71
-
72
- for (const [file, fileResults] of Object.entries(scanResults || {})) {
73
- if (!fileResults || !fileResults.issues) continue;
74
- for (const issue of fileResults.issues) {
75
- issues.push(normalizeIssue(rootPath, { file, ...issue }));
76
- }
77
- }
78
-
79
- for (const issue of secretResults?.issues || []) {
80
- issues.push(normalizeIssue(rootPath, issue));
81
- }
82
-
83
- for (const issue of driftResults?.issues || []) {
84
- issues.push(normalizeIssue(rootPath, issue));
85
- }
86
-
87
- for (const item of hallResults?.phantomAPIs || []) {
88
- issues.push(normalizeIssue(rootPath, {
89
- file: item.file,
90
- line: item.line,
91
- severity: 'critical',
92
- category: 'hallucination',
93
- id: item.id || 'HALL_API',
94
- message: `${item.name} — this API does not exist`,
95
- suggestion: item.suggestion,
96
- fixable: false,
97
- }));
98
- }
99
-
100
- for (const item of hallResults?.phantomImports || []) {
101
- issues.push(normalizeIssue(rootPath, {
102
- file: item.file,
103
- line: item.line,
104
- severity: 'critical',
105
- category: 'hallucination',
106
- id: item.id || 'HALL_IMPORT',
107
- message: `${item.module} — ${item.reason}`,
108
- suggestion: item.suggestion || null,
109
- fixable: false,
110
- }));
111
- }
112
-
113
- for (const item of hallResults?.deprecatedAPIs || []) {
114
- issues.push(normalizeIssue(rootPath, {
115
- file: item.file,
116
- line: item.line,
117
- severity: 'high',
118
- category: 'deprecated',
119
- id: item.id || 'DEPR_API',
120
- message: `${item.name} — deprecated since ${item.since}`,
121
- suggestion: item.suggestion,
122
- }));
123
- }
124
-
125
- for (const item of hallResults?.aiSmells || []) {
126
- issues.push(normalizeIssue(rootPath, {
127
- file: item.file,
128
- line: item.line,
129
- severity: 'warning',
130
- category: 'ai_smell',
131
- id: item.id,
132
- message: item.message,
133
- }));
134
- }
135
-
136
- const uniqueIssues = [];
137
- const seen = new Set();
138
- for (const issue of issues) {
139
- const fingerprint = fingerprintIssue(issue);
140
- if (seen.has(fingerprint)) continue;
141
- seen.add(fingerprint);
142
- uniqueIssues.push({ ...issue, fingerprint });
143
- }
144
-
145
- const metrics = {
146
- filesScanned: files.length,
147
- issueCount: uniqueIssues.length,
148
- hallucinations: uniqueIssues.filter(issue => issue.category === 'hallucination').length,
149
- secrets: uniqueIssues.filter(issue => issue.category === 'secret' || String(issue.id || '').startsWith('SECRET')).length,
150
- techDebtScore: debtResults?.summary?.score ?? debtResults?.score ?? null,
151
- techDebtHours: debtResults?.summary?.estimatedHours ?? debtResults?.estimatedHours ?? null,
152
- };
153
-
154
- return {
155
- issues: uniqueIssues,
156
- summary: summarizeIssues(uniqueIssues),
157
- metrics,
158
- raw: {
159
- scanResults,
160
- hallResults,
161
- secretResults,
162
- driftResults,
163
- debtResults,
164
- },
165
- };
166
- }
167
-
168
- module.exports = {
169
- runScanPipeline,
170
- fingerprintIssue,
171
- summarizeIssues,
172
- };