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.
- package/dist/LICENSE +21 -0
- package/dist/README.md +185 -0
- package/dist/cli.js +2 -0
- package/dist/packages/scanner/ai-confidence-scorer.js +1 -0
- package/dist/packages/scanner/alert-manager.js +1 -0
- package/dist/packages/scanner/baseline-manager.js +1 -0
- package/dist/packages/scanner/code-scanner.js +1 -0
- package/dist/packages/scanner/codebase-passport.js +1 -0
- package/dist/packages/scanner/copy-paste-detector.js +1 -0
- package/dist/packages/scanner/dependency-graph.js +1 -0
- package/dist/packages/scanner/drift-detector.js +1 -0
- package/dist/packages/scanner/executive-report.js +1 -0
- package/dist/packages/scanner/file-collector.js +1 -0
- package/dist/packages/scanner/file-watcher.js +1 -0
- package/dist/packages/scanner/ghost-code-detector.js +1 -0
- package/dist/packages/scanner/hallucination-detector.js +1 -0
- package/dist/packages/scanner/health-checker.js +1 -0
- package/dist/packages/scanner/html-report.js +1 -0
- package/dist/packages/scanner/index.js +1 -0
- package/dist/packages/scanner/license-manager.js +1 -0
- package/dist/packages/scanner/master-health-checker.js +1 -0
- package/dist/packages/scanner/monitor-notifier.js +1 -0
- package/dist/packages/scanner/monitor-service.js +1 -0
- package/dist/packages/scanner/monitor-store.js +1 -0
- package/dist/packages/scanner/pre-commit-gate.js +1 -0
- package/dist/packages/scanner/scan-diff.js +1 -0
- package/dist/packages/scanner/scan-runner.js +1 -0
- package/dist/packages/scanner/secret-scanner.js +1 -0
- package/dist/packages/scanner/sentinel-core.js +1 -0
- package/dist/packages/scanner/sentinel-knowledge.js +1 -0
- package/dist/packages/scanner/tech-debt-analyzer.js +1 -0
- package/dist/packages/scanner/tech-debt-cost.js +1 -0
- package/dist/packages/scanner/widget-generator.js +1 -0
- package/package.json +12 -7
- package/cli.js +0 -2627
- package/packages/scanner/ai-confidence-scorer.js +0 -260
- package/packages/scanner/alert-manager.js +0 -398
- package/packages/scanner/baseline-manager.js +0 -109
- package/packages/scanner/code-scanner.js +0 -758
- package/packages/scanner/codebase-passport.js +0 -299
- package/packages/scanner/copy-paste-detector.js +0 -276
- package/packages/scanner/dependency-graph.js +0 -541
- package/packages/scanner/drift-detector.js +0 -423
- package/packages/scanner/executive-report.js +0 -774
- package/packages/scanner/file-collector.js +0 -64
- package/packages/scanner/file-watcher.js +0 -323
- package/packages/scanner/ghost-code-detector.js +0 -301
- package/packages/scanner/hallucination-detector.js +0 -822
- package/packages/scanner/health-checker.js +0 -586
- package/packages/scanner/html-report.js +0 -634
- package/packages/scanner/index.js +0 -100
- package/packages/scanner/license-manager.js +0 -331
- package/packages/scanner/master-health-checker.js +0 -513
- package/packages/scanner/monitor-notifier.js +0 -64
- package/packages/scanner/monitor-service.js +0 -103
- package/packages/scanner/monitor-store.js +0 -117
- package/packages/scanner/pre-commit-gate.js +0 -216
- package/packages/scanner/scan-diff.js +0 -50
- package/packages/scanner/scan-runner.js +0 -172
- package/packages/scanner/secret-scanner.js +0 -612
- package/packages/scanner/secret-scanner.test.js +0 -103
- package/packages/scanner/sentinel-core.js +0 -616
- package/packages/scanner/sentinel-knowledge.js +0 -322
- package/packages/scanner/tech-debt-analyzer.js +0 -583
- package/packages/scanner/tech-debt-cost.js +0 -194
- 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
|
-
};
|