thuban 0.3.0 → 0.3.2
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/cli.js +577 -67
- package/package.json +1 -1
- package/packages/scanner/code-scanner.js +3 -2
- package/packages/scanner/dependency-graph.js +1 -4
- package/packages/scanner/drift-detector.js +0 -2
- package/packages/scanner/hallucination-detector.js +160 -15
- package/packages/scanner/tech-debt-analyzer.js +82 -0
package/cli.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
const path = require('path');
|
|
22
22
|
const fs = require('fs');
|
|
23
|
+
const os = require('os');
|
|
23
24
|
|
|
24
25
|
// Import engine components
|
|
25
26
|
const CodeScanner = require('./packages/scanner/code-scanner.js');
|
|
@@ -75,51 +76,100 @@ function printHelp() {
|
|
|
75
76
|
banner();
|
|
76
77
|
const lm = new LicenseManager();
|
|
77
78
|
const status = lm.getStatus();
|
|
78
|
-
|
|
79
|
-
console.log('');
|
|
80
|
-
console.log(c('bold', ' USAGE'));
|
|
81
|
-
console.log('');
|
|
82
|
-
console.log(` ${c('cyan', 'thuban scan')} [path] Scan for code issues`);
|
|
83
|
-
console.log(` ${c('cyan', 'thuban deps')} [path] Map dependencies`);
|
|
84
|
-
console.log(` ${c('cyan', 'thuban drift')} [path] Detect drift from annotations`);
|
|
85
|
-
console.log(` ${c('cyan', 'thuban inject')} [path] Inject Mother Code DNA`);
|
|
86
|
-
console.log(` ${c('cyan', 'thuban health')} [path] Codebase health check`);
|
|
87
|
-
console.log(` ${c('cyan', 'thuban report')} [path] Full combined report`);
|
|
88
|
-
console.log(` ${c('cyan', 'thuban debt')} [path] Tech debt analysis with scores`);
|
|
89
|
-
console.log(` ${c('cyan', 'thuban fix')} [path] Auto-fix all fixable tech debt`);
|
|
90
|
-
console.log(` ${c('cyan', 'thuban hallucinate')} [path] Detect AI hallucinations`);
|
|
91
|
-
console.log(` ${c('cyan', 'thuban watch')} [path] Live sentinel monitoring`);
|
|
92
|
-
console.log(` ${c('cyan', 'thuban dashboard')} [path] Generate HTML report`);
|
|
93
|
-
console.log(` ${c('cyan', 'thuban diff')} [path] Scan only git-changed files`);
|
|
94
|
-
console.log(` ${c('cyan', 'thuban gate')} Pre-commit hallucination blocker`);
|
|
95
|
-
console.log(` ${c('cyan', 'thuban cost')} [path] Tech debt cost calculator`);
|
|
96
|
-
console.log(` ${c('cyan', 'thuban ghosts')} [path] Find dead AI-pasted code`);
|
|
97
|
-
console.log(` ${c('cyan', 'thuban ai-score')} [path] AI confidence score per function`);
|
|
98
|
-
console.log(` ${c('cyan', 'thuban clones')} [path] Copy-paste drift detection`);
|
|
99
|
-
console.log(` ${c('cyan', 'thuban passport')} [path] Generate codebase identity file`);
|
|
100
|
-
console.log(` ${c('cyan', 'thuban executive')} [path] CTO-ready PDF report (print from browser)`);
|
|
101
|
-
console.log(` ${c('cyan', 'thuban baseline')} [path] Create/check baseline (only fail on NEW issues)`);
|
|
79
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'));
|
|
80
|
+
console.log(c('gray', ` v${pkg.version} · ${status.tierName} tier`) + (status.licensed ? c('green', ' ✓') : '') + c('gray', ' · https://thuban.dev'));
|
|
102
81
|
console.log('');
|
|
82
|
+
|
|
83
|
+
// ─── QUICK START ──────────────────────────────────────────
|
|
84
|
+
console.log(c('bold', ' QUICK START'));
|
|
85
|
+
console.log('');
|
|
86
|
+
console.log(c('gray', ' Run these in order to see what Thuban does:'));
|
|
87
|
+
console.log('');
|
|
88
|
+
console.log(` ${c('cyan', 'npx thuban scan .')} Scan your project`);
|
|
89
|
+
console.log(` ${c('cyan', 'npx thuban fix .')} Preview what can be fixed`);
|
|
90
|
+
console.log(` ${c('cyan', 'npx thuban fix . --fix')} Apply all safe fixes`);
|
|
91
|
+
console.log(` ${c('cyan', 'npx thuban dashboard .')} Open visual HTML report`);
|
|
92
|
+
console.log('');
|
|
93
|
+
|
|
94
|
+
// ─── SCAN & DETECT ───────────────────────────────────────
|
|
95
|
+
console.log(c('bold', ' SCAN & DETECT'));
|
|
96
|
+
console.log('');
|
|
97
|
+
console.log(` ${c('cyan', 'scan')} [path|url] Full health scan — score, grade, all issues`);
|
|
98
|
+
console.log(` ${c('cyan', 'compare')} [a] [b] Compare two codebases side by side`);
|
|
99
|
+
console.log(` ${c('cyan', 'hallucinate')} [path] Find phantom APIs and invented imports`);
|
|
100
|
+
console.log(` ${c('cyan', 'ghosts')} [path] Dead functions nobody calls`);
|
|
101
|
+
console.log(` ${c('cyan', 'ai-score')} [path] Probability each function is AI-generated`);
|
|
102
|
+
console.log(` ${c('cyan', 'clones')} [path] Copy-pasted code that has drifted apart`);
|
|
103
|
+
console.log(` ${c('cyan', 'drift')} [path] Files that changed since Mother Code was set`);
|
|
104
|
+
console.log(` ${c('cyan', 'deps')} [path] Dependency map — circular, orphan, critical`);
|
|
105
|
+
console.log(` ${c('cyan', 'diff')} [path] Scan only git-changed files`);
|
|
106
|
+
console.log(` ${c('cyan', 'debt')} [path] Detailed tech debt breakdown`);
|
|
107
|
+
console.log(` ${c('cyan', 'cost')} [path] Tech debt translated to £ and hours`);
|
|
108
|
+
console.log('');
|
|
109
|
+
|
|
110
|
+
// ─── FIX & PROTECT ───────────────────────────────────────
|
|
111
|
+
console.log(c('bold', ' FIX & PROTECT'));
|
|
112
|
+
console.log('');
|
|
113
|
+
console.log(` ${c('cyan', 'fix')} [path] Preview fixable issues ${c('gray', '(safe, no changes)')}`);
|
|
114
|
+
console.log(` ${c('cyan', 'fix')} [path] ${c('gray', '--fix')} Apply all safe fixes`);
|
|
115
|
+
console.log(` ${c('cyan', 'fix')} [path] ${c('gray', '--fix --commit')} Each fix = 1 git commit ${c('gray', '(revert any)')}`);
|
|
116
|
+
console.log(` ${c('cyan', 'inject')} [path] Add Mother Code DNA to every file`);
|
|
117
|
+
console.log(` ${c('cyan', 'gate')} Install pre-commit hook ${c('gray', '(blocks hallucinations)')}`);
|
|
118
|
+
console.log(` ${c('cyan', 'watch')} [path] Live sentinel — scans files as you save`);
|
|
119
|
+
console.log(` ${c('cyan', 'baseline')} [path] Snapshot issues so future scans show only NEW ones`);
|
|
120
|
+
console.log('');
|
|
121
|
+
|
|
122
|
+
// ─── REPORTS ──────────────────────────────────────────────
|
|
123
|
+
console.log(c('bold', ' REPORTS'));
|
|
124
|
+
console.log('');
|
|
125
|
+
console.log(` ${c('cyan', 'dashboard')} [path] Interactive HTML report ${c('gray', '(share with your team)')}`);
|
|
126
|
+
console.log(` ${c('cyan', 'executive')} [path] CTO-ready report ${c('gray', '(print to PDF from browser)')}`);
|
|
127
|
+
console.log(` ${c('cyan', 'report')} [path] Combined scan + deps + drift summary`);
|
|
128
|
+
console.log(` ${c('cyan', 'passport')} [path] Codebase identity card ${c('gray', '(for new team members)')}`);
|
|
129
|
+
console.log('');
|
|
130
|
+
|
|
131
|
+
// ─── LICENSE ──────────────────────────────────────────────
|
|
103
132
|
console.log(c('bold', ' LICENSE'));
|
|
104
133
|
console.log('');
|
|
105
|
-
console.log(` ${c('cyan', '
|
|
106
|
-
console.log(` ${c('cyan', '
|
|
107
|
-
console.log(` ${c('cyan', '
|
|
134
|
+
console.log(` ${c('cyan', 'activate')} <key> Activate a license key`);
|
|
135
|
+
console.log(` ${c('cyan', 'status')} Show license, usage, and features`);
|
|
136
|
+
console.log(` ${c('cyan', 'upgrade')} Open pricing page`);
|
|
108
137
|
console.log('');
|
|
138
|
+
|
|
139
|
+
// ─── OPTIONS ──────────────────────────────────────────────
|
|
109
140
|
console.log(c('bold', ' OPTIONS'));
|
|
110
141
|
console.log('');
|
|
111
|
-
console.log(` ${c('gray', '--
|
|
112
|
-
console.log(` ${c('gray', '--
|
|
113
|
-
console.log(` ${c('gray', '--
|
|
114
|
-
console.log(` ${c('gray', '--
|
|
115
|
-
console.log(` ${c('gray', '--
|
|
116
|
-
console.log(` ${c('gray', '--
|
|
117
|
-
console.log(` ${c('gray', '--
|
|
118
|
-
console.log(` ${c('gray', '--
|
|
119
|
-
console.log(
|
|
120
|
-
|
|
142
|
+
console.log(` ${c('gray', '--fix')} Apply changes ${c('gray', '(default is dry-run preview)')}`);
|
|
143
|
+
console.log(` ${c('gray', '--commit')} Auto-commit each fix for easy rollback`);
|
|
144
|
+
console.log(` ${c('gray', '--unsafe')} Include fixes that change runtime behaviour`);
|
|
145
|
+
console.log(` ${c('gray', '--json')} Machine-readable JSON output`);
|
|
146
|
+
console.log(` ${c('gray', '--baseline')} Only show NEW issues vs saved baseline`);
|
|
147
|
+
console.log(` ${c('gray', '--verbose')} Show detailed output`);
|
|
148
|
+
console.log(` ${c('gray', '--ignore <pat>')} Skip files matching pattern`);
|
|
149
|
+
console.log(` ${c('gray', '--max-issues <n>')} Limit displayed issues ${c('gray', '(default: 50)')}`);
|
|
150
|
+
console.log('');
|
|
151
|
+
|
|
152
|
+
// ─── EXAMPLES ─────────────────────────────────────────────
|
|
153
|
+
console.log(c('bold', ' EXAMPLES'));
|
|
154
|
+
console.log('');
|
|
155
|
+
console.log(c('gray', ' # Scan the current project'));
|
|
156
|
+
console.log(` ${c('white', 'npx thuban scan .')}`);
|
|
157
|
+
console.log('');
|
|
158
|
+
console.log(c('gray', ' # Find and fix all AI hallucinations'));
|
|
159
|
+
console.log(` ${c('white', 'npx thuban hallucinate .')}`);
|
|
160
|
+
console.log(` ${c('white', 'npx thuban fix . --fix')}`);
|
|
161
|
+
console.log('');
|
|
162
|
+
console.log(c('gray', ' # Generate a report for your CTO'));
|
|
163
|
+
console.log(` ${c('white', 'npx thuban executive .')}`);
|
|
164
|
+
console.log('');
|
|
165
|
+
console.log(c('gray', ' # Block hallucinated code on every commit'));
|
|
166
|
+
console.log(` ${c('white', 'npx thuban gate --fix')}`);
|
|
167
|
+
console.log('');
|
|
168
|
+
console.log(c('gray', ' # Scan a specific folder, ignore tests'));
|
|
169
|
+
console.log(` ${c('white', 'npx thuban scan ./src --ignore "**/*.test.js"')}`);
|
|
121
170
|
console.log('');
|
|
122
|
-
console.log(c('gray', '
|
|
171
|
+
console.log(c('gray', ' Report a bug or request a feature:'));
|
|
172
|
+
console.log(` ${c('cyan', 'https://github.com/SilverwingsBenefitsGit/thuban/issues')}`);
|
|
123
173
|
console.log('');
|
|
124
174
|
}
|
|
125
175
|
|
|
@@ -159,7 +209,14 @@ function parseArgs(args) {
|
|
|
159
209
|
else if (arg === '--version' || arg === '-v') { opts.command = 'version'; }
|
|
160
210
|
else if (!opts.command) { opts.command = arg; }
|
|
161
211
|
else if (opts.command === 'activate' && !opts.licenseKey) { opts.licenseKey = arg; }
|
|
162
|
-
else if (!noPathCommands.includes(opts.command)) {
|
|
212
|
+
else if (!noPathCommands.includes(opts.command)) {
|
|
213
|
+
// Don't resolve git URLs as local paths
|
|
214
|
+
if (arg.match(/^(https?:\/\/|git@)/)) {
|
|
215
|
+
opts.targetPath = arg;
|
|
216
|
+
} else {
|
|
217
|
+
opts.targetPath = path.resolve(arg);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
163
220
|
i++;
|
|
164
221
|
}
|
|
165
222
|
|
|
@@ -171,7 +228,7 @@ function parseArgs(args) {
|
|
|
171
228
|
function collectFiles(dir, ignorePatterns = []) {
|
|
172
229
|
const defaultIgnore = ['node_modules', '.git', 'dist', 'build', '.next', '__pycache__', '.venv', 'venv', 'coverage', '.cache', '.turbo'];
|
|
173
230
|
const allIgnore = [...defaultIgnore, ...ignorePatterns];
|
|
174
|
-
const extensions = ['.js', '.ts', '.jsx', '.tsx', '.py', '.
|
|
231
|
+
const extensions = ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs', '.py', '.pyw', '.go', '.rs', '.java', '.kt', '.cs', '.json'];
|
|
175
232
|
const MAX_FILE_SIZE = 1024 * 1024; // 1MB per file — skip anything larger
|
|
176
233
|
const MAX_FILES = 50000; // Hard cap to prevent memory issues
|
|
177
234
|
const files = [];
|
|
@@ -211,7 +268,34 @@ function collectFiles(dir, ignorePatterns = []) {
|
|
|
211
268
|
|
|
212
269
|
async function cmdScan(opts) {
|
|
213
270
|
const startTime = Date.now();
|
|
214
|
-
const
|
|
271
|
+
const gitUrlPattern = /^(https?:\/\/|git@)/;
|
|
272
|
+
const isGitUrl = gitUrlPattern.test(opts.targetPath);
|
|
273
|
+
let targetPath = isGitUrl ? opts.targetPath : path.resolve(opts.targetPath);
|
|
274
|
+
let clonedRepo = null;
|
|
275
|
+
|
|
276
|
+
// ─── Git URL detection — clone and scan ─────────────────
|
|
277
|
+
if (isGitUrl) {
|
|
278
|
+
if (!opts.json) {
|
|
279
|
+
console.log('');
|
|
280
|
+
console.log(c('cyan', ' Cloning repository...'));
|
|
281
|
+
console.log(c('gray', ` ${opts.targetPath}`));
|
|
282
|
+
}
|
|
283
|
+
const tmpDir = path.join(os.tmpdir(), 'thuban-scan-' + Date.now());
|
|
284
|
+
const { execSync } = require('child_process');
|
|
285
|
+
try {
|
|
286
|
+
execSync(`git clone --depth 1 "${opts.targetPath}" "${tmpDir}"`, { stdio: 'pipe', timeout: 60000 });
|
|
287
|
+
targetPath = tmpDir;
|
|
288
|
+
clonedRepo = tmpDir;
|
|
289
|
+
if (!opts.json) {
|
|
290
|
+
console.log(c('cyan', ' Cloned successfully'));
|
|
291
|
+
console.log('');
|
|
292
|
+
}
|
|
293
|
+
} catch (e) {
|
|
294
|
+
console.error(c('red', ` Failed to clone repository: ${e.message}`));
|
|
295
|
+
console.error(c('gray', ' Check the URL is correct and the repo is accessible'));
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
215
299
|
|
|
216
300
|
// ─── License check ───────────────────────────────────────
|
|
217
301
|
const lm = new LicenseManager();
|
|
@@ -237,6 +321,23 @@ async function cmdScan(opts) {
|
|
|
237
321
|
|
|
238
322
|
if (!opts.json) {
|
|
239
323
|
banner();
|
|
324
|
+
|
|
325
|
+
// First-run welcome
|
|
326
|
+
if (lm.usage.totalScans === 0) {
|
|
327
|
+
console.log(c('cyan', ' ┌──────────────────────────────────────────────────────┐'));
|
|
328
|
+
console.log(c('cyan', ' │') + c('bold', ' Welcome to Thuban! ') + c('cyan', '│'));
|
|
329
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
|
|
330
|
+
console.log(c('cyan', ' │') + ' ' + c('gray', 'Thuban scans your codebase for AI hallucinations,') + ' ' + c('cyan', '│'));
|
|
331
|
+
console.log(c('cyan', ' │') + ' ' + c('gray', 'dead code, tech debt, and architecture drift.') + ' ' + c('cyan', '│'));
|
|
332
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
|
|
333
|
+
console.log(c('cyan', ' │') + ' ' + c('gray', 'After this scan, try:') + ' ' + c('cyan', '│'));
|
|
334
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban fix .') + ' ' + c('gray', 'Preview fixes') + ' ' + c('cyan', '│'));
|
|
335
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban dashboard .') + ' ' + c('gray', 'Visual report') + ' ' + c('cyan', '│'));
|
|
336
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban --help') + ' ' + c('gray', 'All commands') + ' ' + c('cyan', '│'));
|
|
337
|
+
console.log(c('cyan', ' └──────────────────────────────────────────────────────┘'));
|
|
338
|
+
console.log('');
|
|
339
|
+
}
|
|
340
|
+
|
|
240
341
|
console.log(c('bold', ' SCANNING: ') + c('cyan', targetPath));
|
|
241
342
|
console.log('');
|
|
242
343
|
}
|
|
@@ -360,18 +461,24 @@ async function cmdScan(opts) {
|
|
|
360
461
|
// THE GUT-PUNCH OUTPUT
|
|
361
462
|
// ═══════════════════════════════════════════════════════════
|
|
362
463
|
|
|
464
|
+
// ─── Count fixable items ─────────────────────────────────
|
|
465
|
+
const fixableCount = allIssues.filter(i => i.fixable !== false).length;
|
|
466
|
+
|
|
363
467
|
console.log(c('cyan', ' ╔══════════════════════════════════════════════════════╗'));
|
|
364
|
-
console.log(c('cyan', ' ║') + c('bold', ' THUBAN SCAN
|
|
468
|
+
console.log(c('cyan', ' ║') + c('bold', ' THUBAN SCAN RESULTS ') + c('cyan', '║'));
|
|
365
469
|
console.log(c('cyan', ' ╠══════════════════════════════════════════════════════╣'));
|
|
366
470
|
console.log(c('cyan', ' ║') + ` Files scanned: ${c('bold', String(files.length).padEnd(6))}` + (isFileCapped ? c('yellow', ` of ${totalFileCount}`) : ' ') + c('cyan', ' ║'));
|
|
367
471
|
console.log(c('cyan', ' ║') + ` Health score: ${c(scoreColor, (grade + ' (' + Math.round(score) + '/100)').padEnd(20))}` + c('cyan', ' ║'));
|
|
472
|
+
console.log(c('cyan', ' ║') + ` Issues found: ${c(allIssues.length > 0 ? 'yellow' : 'green', String(allIssues.length).padEnd(20))}` + c('cyan', ' ║'));
|
|
473
|
+
console.log(c('cyan', ' ║') + ` Auto-fixable: ${c('green', String(fixableCount).padEnd(20))}` + c('cyan', ' ║'));
|
|
368
474
|
console.log(c('cyan', ' ║') + ` Scan time: ${c('gray', (elapsed + 'ms').padEnd(20))}` + c('cyan', ' ║'));
|
|
369
475
|
console.log(c('cyan', ' ╚══════════════════════════════════════════════════════╝'));
|
|
370
476
|
console.log('');
|
|
371
477
|
|
|
372
478
|
// ─── CRITICAL: Hallucinated APIs (will crash) ────────────
|
|
373
479
|
if (hallucinations.length > 0) {
|
|
374
|
-
console.log(c('bgRed', ' CRITICAL ') + c('red', c('bold', ` ${hallucinations.length} Hallucinated API${hallucinations.length > 1 ? 's' : ''}`))
|
|
480
|
+
console.log(c('bgRed', ' CRITICAL ') + c('red', c('bold', ` ${hallucinations.length} Hallucinated API${hallucinations.length > 1 ? 's' : ''}`)));
|
|
481
|
+
console.log(c('gray', ' These imports or method calls do not exist. Your code WILL crash when it hits them.'));
|
|
375
482
|
console.log('');
|
|
376
483
|
|
|
377
484
|
const showCount = tier.showFullDetails ? hallucinations.length : Math.min(tier.teaserIssues, hallucinations.length);
|
|
@@ -403,7 +510,9 @@ async function cmdScan(opts) {
|
|
|
403
510
|
|
|
404
511
|
// ─── HIGH: Deprecated APIs ───────────────────────────────
|
|
405
512
|
if (deprecated.length > 0) {
|
|
406
|
-
console.log(c('red', c('bold', `
|
|
513
|
+
console.log(c('red', c('bold', ` DEPRECATED: ${deprecated.length} API${deprecated.length > 1 ? 's' : ''} that will break on upgrade`)));
|
|
514
|
+
console.log(c('gray', ' These work NOW but will stop working when you upgrade Node.js or your dependencies.'));
|
|
515
|
+
console.log(c('gray', ' Fix: ') + c('cyan', 'npx thuban fix . --fix --unsafe'));
|
|
407
516
|
console.log('');
|
|
408
517
|
|
|
409
518
|
const showCount = tier.showFullDetails ? deprecated.length : Math.min(tier.teaserIssues, deprecated.length);
|
|
@@ -443,17 +552,44 @@ async function cmdScan(opts) {
|
|
|
443
552
|
}
|
|
444
553
|
|
|
445
554
|
if (qualityIssues.length > 0) {
|
|
555
|
+
// Group quality issues by type for clearer output
|
|
556
|
+
const qualityByType = {};
|
|
557
|
+
for (const item of qualityIssues) {
|
|
558
|
+
const key = item.message || item.type || 'other';
|
|
559
|
+
// Normalize similar messages
|
|
560
|
+
const normalizedKey = key.replace(/\s*—.*$/, '').trim();
|
|
561
|
+
if (!qualityByType[normalizedKey]) qualityByType[normalizedKey] = [];
|
|
562
|
+
qualityByType[normalizedKey].push(item);
|
|
563
|
+
}
|
|
564
|
+
const typeEntries = Object.entries(qualityByType).sort((a, b) => b[1].length - a[1].length);
|
|
565
|
+
|
|
446
566
|
console.log(c('yellow', c('bold', ` CODE QUALITY: ${qualityIssues.length} issue${qualityIssues.length > 1 ? 's' : ''}`)));
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
567
|
+
console.log(c('gray', ' Patterns that reduce code quality, maintainability, or production readiness.'));
|
|
568
|
+
console.log('');
|
|
569
|
+
|
|
570
|
+
// Show grouped summary
|
|
571
|
+
const showTypes = tier.showFullDetails ? typeEntries.length : Math.min(3, typeEntries.length);
|
|
572
|
+
for (let t = 0; t < showTypes; t++) {
|
|
573
|
+
const [typeName, items] = typeEntries[t];
|
|
574
|
+
const sevColor = items[0].severity === 'high' || items[0].severity === 'error' ? 'red' : 'yellow';
|
|
575
|
+
console.log(` ${c(sevColor, '!')} ${c('bold', String(items.length))} x ${typeName}`);
|
|
576
|
+
// Show first example file
|
|
577
|
+
if (items[0].file) {
|
|
578
|
+
const lineRef = items[0].line ? `:${items[0].line}` : '';
|
|
579
|
+
console.log(` ${c('gray', 'e.g.')} ${c('cyan', items[0].file + lineRef)}`);
|
|
580
|
+
}
|
|
452
581
|
}
|
|
453
|
-
if (
|
|
454
|
-
console.log(c('gray', ` ... ${
|
|
582
|
+
if (typeEntries.length > showTypes) {
|
|
583
|
+
console.log(c('gray', ` ... ${typeEntries.length - showTypes} more issue types`));
|
|
455
584
|
}
|
|
456
585
|
console.log('');
|
|
586
|
+
|
|
587
|
+
// Show fix command if fixable
|
|
588
|
+
const fixableQuality = qualityIssues.filter(i => i.fixable !== false).length;
|
|
589
|
+
if (fixableQuality > 0) {
|
|
590
|
+
console.log(c('gray', ` ${fixableQuality} of these can be auto-fixed: `) + c('cyan', 'npx thuban fix . --fix --unsafe'));
|
|
591
|
+
console.log('');
|
|
592
|
+
}
|
|
457
593
|
}
|
|
458
594
|
|
|
459
595
|
// ─── THE VERDICT ─────────────────────────────────────────
|
|
@@ -501,10 +637,36 @@ async function cmdScan(opts) {
|
|
|
501
637
|
console.log(c('gray', ` Free scans remaining this month: ${remaining}/${scanStatus.limit}`));
|
|
502
638
|
console.log('');
|
|
503
639
|
} else {
|
|
504
|
-
// Pro/Team: show
|
|
640
|
+
// Pro/Team: show clear next steps
|
|
505
641
|
if (allIssues.length > 0) {
|
|
506
|
-
|
|
507
|
-
console.log(
|
|
642
|
+
const fixable = allIssues.filter(i => i.fixable !== false).length;
|
|
643
|
+
console.log(c('cyan', ' ┌──────────────────────────────────────────────────────┐'));
|
|
644
|
+
console.log(c('cyan', ' │') + c('bold', ' WHAT TO DO NEXT ') + c('cyan', '│'));
|
|
645
|
+
console.log(c('cyan', ' ├──────────────────────────────────────────────────────┤'));
|
|
646
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
|
|
647
|
+
console.log(c('cyan', ' │') + ' ' + c('bold', '1. See what would be fixed (safe preview):') + ' ' + c('cyan', '│'));
|
|
648
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban fix . ') + ' ' + c('cyan', '│'));
|
|
649
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
|
|
650
|
+
console.log(c('cyan', ' │') + ' ' + c('bold', '2. Apply all safe fixes:') + ' ' + c('cyan', '│'));
|
|
651
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban fix . --fix') + ' ' + c('cyan', '│'));
|
|
652
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
|
|
653
|
+
console.log(c('cyan', ' │') + ' ' + c('bold', '3. Apply fixes with git rollback:') + ' ' + c('cyan', '│'));
|
|
654
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban fix . --fix --commit') + ' ' + c('cyan', '│'));
|
|
655
|
+
console.log(c('cyan', ' │') + ' ' + c('gray', 'Each fix = 1 git commit. Undo any with git revert.') + c('cyan', '│'));
|
|
656
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
|
|
657
|
+
console.log(c('cyan', ' │') + ' ' + c('bold', '4. Generate visual report (HTML):') + ' ' + c('cyan', '│'));
|
|
658
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban dashboard .') + ' ' + c('cyan', '│'));
|
|
659
|
+
console.log(c('cyan', ' │') + ' ' + c('gray', 'Opens a dashboard you can share with your team.') + ' ' + c('cyan', '│'));
|
|
660
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
|
|
661
|
+
console.log(c('cyan', ' │') + ' ' + c('bold', '5. Show tech debt cost in £:') + ' ' + c('cyan', '│'));
|
|
662
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban cost .') + ' ' + c('cyan', '│'));
|
|
663
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
|
|
664
|
+
console.log(c('cyan', ' │') + ' ' + c('bold', '6. Block bad code on every commit:') + ' ' + c('cyan', '│'));
|
|
665
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban gate') + ' ' + c('cyan', '│'));
|
|
666
|
+
console.log(c('cyan', ' │') + ' ' + c('gray', 'Installs a pre-commit hook. Set once, forget.') + ' ' + c('cyan', '│'));
|
|
667
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
|
|
668
|
+
console.log(c('cyan', ' │') + ' ' + c('gray', 'All commands: npx thuban --help') + ' ' + c('cyan', '│'));
|
|
669
|
+
console.log(c('cyan', ' └──────────────────────────────────────────────────────┘'));
|
|
508
670
|
console.log('');
|
|
509
671
|
}
|
|
510
672
|
}
|
|
@@ -514,6 +676,19 @@ async function cmdScan(opts) {
|
|
|
514
676
|
console.log(c('gray', ` Your actual score may be worse. Upgrade to scan everything.`));
|
|
515
677
|
console.log('');
|
|
516
678
|
}
|
|
679
|
+
|
|
680
|
+
// ─── Continuous monitoring pitch ────────────────────────
|
|
681
|
+
if (allIssues.length > 0 && !isFileCapped) {
|
|
682
|
+
console.log(c('gray', ' ─────────────────────────────────────────────────────'));
|
|
683
|
+
console.log(c('gray', ' Thuban runs 100% on your machine. No data leaves.'));
|
|
684
|
+
console.log(c('gray', ' No cloud costs. As your codebase grows, Thuban grows with it.'));
|
|
685
|
+
console.log('');
|
|
686
|
+
console.log(c('gray', ' Keep your codebase healthy continuously:'));
|
|
687
|
+
console.log(c('gray', ' ') + c('cyan', 'npx thuban gate --fix') + c('gray', ' Block bad code on every commit'));
|
|
688
|
+
console.log(c('gray', ' ') + c('cyan', 'npx thuban watch .') + c('gray', ' Scan files as you save them'));
|
|
689
|
+
console.log(c('gray', ' ') + c('cyan', 'npx thuban baseline .') + c('gray', ' Only alert on NEW issues'));
|
|
690
|
+
console.log('');
|
|
691
|
+
}
|
|
517
692
|
}
|
|
518
693
|
|
|
519
694
|
async function cmdDeps(opts) {
|
|
@@ -830,6 +1005,11 @@ async function cmdReport(opts) {
|
|
|
830
1005
|
if (orphans.length > 5) console.log(` ${c('gray', '→')} Consider removing ${orphans.length} orphan files`);
|
|
831
1006
|
console.log('');
|
|
832
1007
|
}
|
|
1008
|
+
|
|
1009
|
+
// Clean up cloned repo
|
|
1010
|
+
if (clonedRepo) {
|
|
1011
|
+
try { fs.rmSync(clonedRepo, { recursive: true, force: true }); } catch {}
|
|
1012
|
+
}
|
|
833
1013
|
}
|
|
834
1014
|
|
|
835
1015
|
// ─── Tech Debt & Fix Commands ────────────────────────────
|
|
@@ -882,7 +1062,7 @@ async function cmdFix(opts) {
|
|
|
882
1062
|
|
|
883
1063
|
const grouped = {};
|
|
884
1064
|
const safeActions = ['inject_widget', 'regenerate_widget', 'remove_console_log', 'flag_for_removal'];
|
|
885
|
-
const unsafeActions = ['break_circular', 'extract_to_env', 'update_deprecated_api'];
|
|
1065
|
+
const unsafeActions = ['break_circular', 'extract_to_env', 'update_deprecated_api', 'wrap_localhost_url'];
|
|
886
1066
|
let safeCount = 0;
|
|
887
1067
|
let unsafeCount = 0;
|
|
888
1068
|
|
|
@@ -914,7 +1094,8 @@ async function cmdFix(opts) {
|
|
|
914
1094
|
const label = {
|
|
915
1095
|
'break_circular': 'Restructure circular dependencies',
|
|
916
1096
|
'extract_to_env': 'Extract hardcoded secrets to env vars',
|
|
917
|
-
'update_deprecated_api': 'Replace deprecated API calls',
|
|
1097
|
+
'update_deprecated_api': 'Replace deprecated API calls with modern equivalents',
|
|
1098
|
+
'wrap_localhost_url': 'Wrap hardcoded localhost URLs in env var fallback',
|
|
918
1099
|
}[action] || action;
|
|
919
1100
|
console.log(` ${c('yellow', '!')} ${label}: ${c('bold', count)}`);
|
|
920
1101
|
}
|
|
@@ -973,18 +1154,43 @@ async function cmdFix(opts) {
|
|
|
973
1154
|
if (opts.json) {
|
|
974
1155
|
console.log(JSON.stringify(result, null, 2));
|
|
975
1156
|
} else {
|
|
976
|
-
console.log(c('
|
|
977
|
-
console.log(c('
|
|
978
|
-
|
|
1157
|
+
console.log(c('cyan', ' ╔══════════════════════════════════════════════════════╗'));
|
|
1158
|
+
console.log(c('cyan', ' ║') + c('bold', ' FIX RESULTS ') + c('cyan', '║'));
|
|
1159
|
+
console.log(c('cyan', ' ╠══════════════════════════════════════════════════════╣'));
|
|
1160
|
+
console.log(c('cyan', ' ║') + ` Fixed: ${c('green', String(result.fixed).padEnd(20))}` + c('cyan', ' ║'));
|
|
1161
|
+
console.log(c('cyan', ' ║') + ` Flagged for review: ${c('yellow', String(result.flagged).padEnd(19))}` + c('cyan', ' ║'));
|
|
1162
|
+
if (result.failed > 0) {
|
|
1163
|
+
console.log(c('cyan', ' ║') + ` Failed: ${c('red', String(result.failed).padEnd(20))}` + c('cyan', ' ║'));
|
|
1164
|
+
}
|
|
979
1165
|
if (result.skippedUnsafe > 0) {
|
|
980
|
-
console.log(c('
|
|
1166
|
+
console.log(c('cyan', ' ║') + ` Skipped (unsafe): ${c('gray', String(result.skippedUnsafe).padEnd(20))}` + c('cyan', ' ║'));
|
|
981
1167
|
}
|
|
1168
|
+
console.log(c('cyan', ' ╚══════════════════════════════════════════════════════╝'));
|
|
982
1169
|
console.log('');
|
|
983
|
-
|
|
984
|
-
if (
|
|
985
|
-
console.log(c('
|
|
1170
|
+
|
|
1171
|
+
if (result.fixed > 0) {
|
|
1172
|
+
console.log(c('green', ' Your files have been updated. Changes are saved to disk.'));
|
|
1173
|
+
if (!opts.gitCommit) {
|
|
1174
|
+
console.log(c('gray', ' Tip: use --commit next time to auto-commit each fix for easy rollback.'));
|
|
1175
|
+
}
|
|
1176
|
+
console.log('');
|
|
1177
|
+
console.log(c('gray', ' Verify the changes:'));
|
|
1178
|
+
console.log(c('gray', ' ') + c('cyan', 'npx thuban scan .') + c('gray', ' Re-scan to see your new score'));
|
|
1179
|
+
console.log(c('gray', ' ') + c('cyan', 'npx thuban dashboard .') + c('gray', ' Generate updated report'));
|
|
1180
|
+
console.log('');
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
if (result.skippedUnsafe > 0) {
|
|
1184
|
+
console.log(c('gray', ` ${result.skippedUnsafe} unsafe fixes were skipped. These change runtime behaviour.`));
|
|
1185
|
+
console.log(c('gray', ' Review them with: ') + c('cyan', 'npx thuban fix . --fix --unsafe'));
|
|
1186
|
+
console.log('');
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
if (result.flagged > 0) {
|
|
1190
|
+
console.log(c('gray', ` ${result.flagged} items need human review (secrets, orphan files, complex patterns).`));
|
|
1191
|
+
console.log(c('gray', ' See details: ') + c('cyan', 'npx thuban dashboard .'));
|
|
1192
|
+
console.log('');
|
|
986
1193
|
}
|
|
987
|
-
console.log('');
|
|
988
1194
|
}
|
|
989
1195
|
}
|
|
990
1196
|
|
|
@@ -1786,6 +1992,292 @@ async function cmdBaseline(opts) {
|
|
|
1786
1992
|
}
|
|
1787
1993
|
}
|
|
1788
1994
|
|
|
1995
|
+
// ─── Telemetry (opt-in anonymous usage stats) ───────────────
|
|
1996
|
+
|
|
1997
|
+
function getTelemetryConfigPath() {
|
|
1998
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
1999
|
+
return path.join(home, '.thuban-telemetry.json');
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
function isTelemetryEnabled() {
|
|
2003
|
+
try {
|
|
2004
|
+
const config = JSON.parse(fs.readFileSync(getTelemetryConfigPath(), 'utf-8'));
|
|
2005
|
+
return config.enabled === true;
|
|
2006
|
+
} catch { return false; }
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
function sendAnonymousTelemetry(data) {
|
|
2010
|
+
if (!isTelemetryEnabled()) return;
|
|
2011
|
+
try {
|
|
2012
|
+
const payload = JSON.stringify({
|
|
2013
|
+
v: data.version || '0.3.1',
|
|
2014
|
+
cmd: data.command,
|
|
2015
|
+
files: data.fileCount,
|
|
2016
|
+
score: data.score,
|
|
2017
|
+
grade: data.grade,
|
|
2018
|
+
langs: data.languages,
|
|
2019
|
+
issues: data.issueCount,
|
|
2020
|
+
duration: data.duration,
|
|
2021
|
+
os: process.platform,
|
|
2022
|
+
node: process.version,
|
|
2023
|
+
ts: new Date().toISOString()
|
|
2024
|
+
});
|
|
2025
|
+
// Fire and forget — never blocks the CLI
|
|
2026
|
+
const https = require('https');
|
|
2027
|
+
const req = https.request({
|
|
2028
|
+
hostname: 'thuban-telemetry.silverwingsbenefits.workers.dev',
|
|
2029
|
+
path: '/report',
|
|
2030
|
+
method: 'POST',
|
|
2031
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
|
|
2032
|
+
timeout: 3000
|
|
2033
|
+
});
|
|
2034
|
+
req.on('error', () => {}); // silently ignore
|
|
2035
|
+
req.write(payload);
|
|
2036
|
+
req.end();
|
|
2037
|
+
} catch { /* never fail the CLI for telemetry */ }
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
async function cmdTelemetry(opts) {
|
|
2041
|
+
const configPath = getTelemetryConfigPath();
|
|
2042
|
+
const args = process.argv.slice(2);
|
|
2043
|
+
|
|
2044
|
+
if (args.includes('--on')) {
|
|
2045
|
+
fs.writeFileSync(configPath, JSON.stringify({ enabled: true, updated: new Date().toISOString() }));
|
|
2046
|
+
console.log('');
|
|
2047
|
+
console.log(c('cyan', ' Telemetry enabled'));
|
|
2048
|
+
console.log(c('gray', ' Anonymous usage stats will be sent (no code, no paths, no PII)'));
|
|
2049
|
+
console.log(c('gray', ' Turn off anytime: npx thuban telemetry --off'));
|
|
2050
|
+
console.log('');
|
|
2051
|
+
} else if (args.includes('--off')) {
|
|
2052
|
+
fs.writeFileSync(configPath, JSON.stringify({ enabled: false, updated: new Date().toISOString() }));
|
|
2053
|
+
console.log('');
|
|
2054
|
+
console.log(c('cyan', ' Telemetry disabled'));
|
|
2055
|
+
console.log(c('gray', ' No data will be collected'));
|
|
2056
|
+
console.log('');
|
|
2057
|
+
} else {
|
|
2058
|
+
const enabled = isTelemetryEnabled();
|
|
2059
|
+
console.log('');
|
|
2060
|
+
console.log(c('bold', ' TELEMETRY STATUS'));
|
|
2061
|
+
console.log('');
|
|
2062
|
+
console.log(` Status: ${enabled ? c('cyan', 'Enabled') : c('gray', 'Disabled (default)')}`);
|
|
2063
|
+
console.log('');
|
|
2064
|
+
console.log(c('gray', ' Thuban telemetry is 100% opt-in and anonymous.'));
|
|
2065
|
+
console.log(c('gray', ' We collect: file count, score, grade, languages, OS, scan duration.'));
|
|
2066
|
+
console.log(c('gray', ' We never collect: code, file names, paths, project names, or PII.'));
|
|
2067
|
+
console.log('');
|
|
2068
|
+
console.log(` Enable: ${c('cyan', 'npx thuban telemetry --on')}`);
|
|
2069
|
+
console.log(` Disable: ${c('cyan', 'npx thuban telemetry --off')}`);
|
|
2070
|
+
console.log('');
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
// ─── Internal scan (returns data without printing) ──────────
|
|
2075
|
+
|
|
2076
|
+
async function runScanSilent(targetInput) {
|
|
2077
|
+
const gitUrlPattern = /^(https?:\/\/|git@)/;
|
|
2078
|
+
const isGitUrl = gitUrlPattern.test(targetInput);
|
|
2079
|
+
let targetPath = isGitUrl ? targetInput : path.resolve(targetInput);
|
|
2080
|
+
let clonedRepo = null;
|
|
2081
|
+
let label = targetInput;
|
|
2082
|
+
|
|
2083
|
+
if (isGitUrl) {
|
|
2084
|
+
const tmpDir = path.join(os.tmpdir(), 'thuban-compare-' + Date.now() + '-' + Math.random().toString(36).slice(2,6));
|
|
2085
|
+
const { execSync } = require('child_process');
|
|
2086
|
+
execSync(`git clone --depth 1 "${targetInput}" "${tmpDir}"`, { stdio: 'pipe', timeout: 60000 });
|
|
2087
|
+
targetPath = tmpDir;
|
|
2088
|
+
clonedRepo = tmpDir;
|
|
2089
|
+
// Extract repo name for label
|
|
2090
|
+
const parts = targetInput.replace(/\.git$/, '').split('/');
|
|
2091
|
+
label = parts[parts.length - 1] || targetInput;
|
|
2092
|
+
} else {
|
|
2093
|
+
label = path.basename(path.resolve(targetInput)) || targetInput;
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
const startTime = Date.now();
|
|
2097
|
+
const scanner = new CodeScanner();
|
|
2098
|
+
const graph = new DependencyGraph();
|
|
2099
|
+
const hallDetector = new HallucinationDetector({ rootPath: targetPath });
|
|
2100
|
+
|
|
2101
|
+
const files = collectFiles(targetPath, []);
|
|
2102
|
+
const scanResults = {};
|
|
2103
|
+
let annotated = 0;
|
|
2104
|
+
let driftCount = 0;
|
|
2105
|
+
|
|
2106
|
+
for (const file of files) {
|
|
2107
|
+
try {
|
|
2108
|
+
const result = scanner.scanFile(file);
|
|
2109
|
+
const relPath = path.relative(targetPath, file);
|
|
2110
|
+
scanResults[file] = result;
|
|
2111
|
+
graph.addFile(file, targetPath);
|
|
2112
|
+
if (result.motherCode && result.motherCode.hasDNA) annotated++;
|
|
2113
|
+
if (result.drifts && result.drifts.length > 0) driftCount++;
|
|
2114
|
+
} catch (e) { /* skip */ }
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
// Run hallucination detection (same as main scan)
|
|
2118
|
+
const hallResults = hallDetector.scan(files, targetPath);
|
|
2119
|
+
|
|
2120
|
+
const elapsed = Date.now() - startTime;
|
|
2121
|
+
let totalIssues = 0, criticalCount = 0, highCount = 0;
|
|
2122
|
+
const issueByCat = {};
|
|
2123
|
+
|
|
2124
|
+
// Count code scanner issues
|
|
2125
|
+
for (const [, r] of Object.entries(scanResults)) {
|
|
2126
|
+
if (r.issues) {
|
|
2127
|
+
totalIssues += r.issues.length;
|
|
2128
|
+
for (const iss of r.issues) {
|
|
2129
|
+
if (iss.severity === 'critical') criticalCount++;
|
|
2130
|
+
if (iss.severity === 'high') highCount++;
|
|
2131
|
+
const cat = iss.category || iss.type || 'other';
|
|
2132
|
+
issueByCat[cat] = (issueByCat[cat] || 0) + 1;
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
// Count hallucination issues
|
|
2138
|
+
const hallIssueCount = (hallResults.phantomAPIs || []).length
|
|
2139
|
+
+ (hallResults.phantomImports || []).length
|
|
2140
|
+
+ (hallResults.deprecatedAPIs || []).length;
|
|
2141
|
+
totalIssues += hallIssueCount;
|
|
2142
|
+
criticalCount += (hallResults.phantomAPIs || []).length + (hallResults.phantomImports || []).length;
|
|
2143
|
+
highCount += (hallResults.deprecatedAPIs || []).length;
|
|
2144
|
+
if (hallIssueCount > 0) issueByCat['hallucination'] = hallIssueCount;
|
|
2145
|
+
|
|
2146
|
+
const orphans = graph.getOrphanFiles();
|
|
2147
|
+
const circular = graph.circularDeps || [];
|
|
2148
|
+
const coverage = files.length > 0 ? Math.round((annotated / files.length) * 100) : 0;
|
|
2149
|
+
|
|
2150
|
+
const score = Math.max(0, Math.round(
|
|
2151
|
+
100 - (criticalCount * 15) - (highCount * 5) - (circular.length * 10)
|
|
2152
|
+
- (orphans.length * 1) - ((100 - coverage) * 0.1) - (driftCount * 3)
|
|
2153
|
+
));
|
|
2154
|
+
|
|
2155
|
+
const grade = score >= 90 ? 'A' : score >= 80 ? 'B' : score >= 70 ? 'C' : score >= 60 ? 'D' : 'F';
|
|
2156
|
+
|
|
2157
|
+
if (clonedRepo) {
|
|
2158
|
+
try { fs.rmSync(clonedRepo, { recursive: true, force: true }); } catch {}
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
return {
|
|
2162
|
+
label,
|
|
2163
|
+
files: files.length,
|
|
2164
|
+
score,
|
|
2165
|
+
grade,
|
|
2166
|
+
elapsed,
|
|
2167
|
+
issues: { total: totalIssues, critical: criticalCount, high: highCount },
|
|
2168
|
+
categories: issueByCat,
|
|
2169
|
+
dependencies: { circular: circular.length, orphans: orphans.length },
|
|
2170
|
+
motherCode: { annotated, coverage, drifted: driftCount }
|
|
2171
|
+
};
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
// ─── Compare Command ────────────────────────────────────────
|
|
2175
|
+
|
|
2176
|
+
async function cmdCompare(opts) {
|
|
2177
|
+
const args = process.argv.slice(2);
|
|
2178
|
+
// Find the two paths after 'compare'
|
|
2179
|
+
const paths = args.filter(a => a !== 'compare' && !a.startsWith('--'));
|
|
2180
|
+
|
|
2181
|
+
if (paths.length < 2) {
|
|
2182
|
+
console.log('');
|
|
2183
|
+
console.log(c('red', ' Compare requires two paths or URLs'));
|
|
2184
|
+
console.log('');
|
|
2185
|
+
console.log(c('gray', ' Usage:'));
|
|
2186
|
+
console.log(` ${c('cyan', 'npx thuban compare ./project-a ./project-b')}`);
|
|
2187
|
+
console.log(` ${c('cyan', 'npx thuban compare . https://github.com/user/repo')}`);
|
|
2188
|
+
console.log(` ${c('cyan', 'npx thuban compare https://github.com/a/repo https://github.com/b/repo')}`);
|
|
2189
|
+
console.log('');
|
|
2190
|
+
process.exit(1);
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
console.log('');
|
|
2194
|
+
console.log(c('bold', ' ╔══════════════════════════════════════╗'));
|
|
2195
|
+
console.log(c('bold', ' ║ THUBAN — Codebase Comparison ║'));
|
|
2196
|
+
console.log(c('bold', ' ╚══════════════════════════════════════╝'));
|
|
2197
|
+
console.log('');
|
|
2198
|
+
|
|
2199
|
+
console.log(c('cyan', ` Scanning: ${paths[0]}`));
|
|
2200
|
+
const resultA = await runScanSilent(paths[0]);
|
|
2201
|
+
console.log(c('cyan', ` Scanning: ${paths[1]}`));
|
|
2202
|
+
const resultB = await runScanSilent(paths[1]);
|
|
2203
|
+
|
|
2204
|
+
console.log('');
|
|
2205
|
+
|
|
2206
|
+
// ─── Side by side comparison ────────────────────────────
|
|
2207
|
+
const pad = (str, len) => String(str).padEnd(len);
|
|
2208
|
+
const padL = (str, len) => String(str).padStart(len);
|
|
2209
|
+
const colW = 20;
|
|
2210
|
+
const labelA = resultA.label.length > colW ? resultA.label.slice(0, colW - 2) + '..' : resultA.label;
|
|
2211
|
+
const labelB = resultB.label.length > colW ? resultB.label.slice(0, colW - 2) + '..' : resultB.label;
|
|
2212
|
+
|
|
2213
|
+
const scoreColorA = resultA.score >= 80 ? 'green' : resultA.score >= 60 ? 'yellow' : 'red';
|
|
2214
|
+
const scoreColorB = resultB.score >= 80 ? 'green' : resultB.score >= 60 ? 'yellow' : 'red';
|
|
2215
|
+
|
|
2216
|
+
const arrow = (a, b) => {
|
|
2217
|
+
if (a === b) return c('gray', ' =');
|
|
2218
|
+
return a > b ? c('green', ' ↑') : c('red', ' ↓');
|
|
2219
|
+
};
|
|
2220
|
+
|
|
2221
|
+
const better = (a, b, lowerIsBetter) => {
|
|
2222
|
+
if (a === b) return c('gray', ' draw');
|
|
2223
|
+
if (lowerIsBetter) return a < b ? c('green', ' ← winner') : c('red', '');
|
|
2224
|
+
return a > b ? c('green', ' ← winner') : c('red', '');
|
|
2225
|
+
};
|
|
2226
|
+
|
|
2227
|
+
console.log(c('bold', ' ┌──────────────────────┬──────────────────────┬──────────────────────┐'));
|
|
2228
|
+
console.log(c('bold', ` │ ${pad('METRIC', colW)} │ ${pad(labelA, colW)} │ ${pad(labelB, colW)} │`));
|
|
2229
|
+
console.log(c('bold', ' ├──────────────────────┼──────────────────────┼──────────────────────┤'));
|
|
2230
|
+
|
|
2231
|
+
const row = (label, valA, valB) => {
|
|
2232
|
+
console.log(` │ ${c('gray', pad(label, colW))} │ ${pad(valA, colW)} │ ${pad(valB, colW)} │`);
|
|
2233
|
+
};
|
|
2234
|
+
|
|
2235
|
+
row('Health Score', `${c(scoreColorA, resultA.score + '/100')}`, `${c(scoreColorB, resultB.score + '/100')}`);
|
|
2236
|
+
row('Grade', `${c(scoreColorA, resultA.grade)}`, `${c(scoreColorB, resultB.grade)}`);
|
|
2237
|
+
row('Files Scanned', String(resultA.files), String(resultB.files));
|
|
2238
|
+
row('Total Issues', String(resultA.issues.total), String(resultB.issues.total));
|
|
2239
|
+
row('Critical Issues', String(resultA.issues.critical), String(resultB.issues.critical));
|
|
2240
|
+
row('High Issues', String(resultA.issues.high), String(resultB.issues.high));
|
|
2241
|
+
row('Circular Deps', String(resultA.dependencies.circular), String(resultB.dependencies.circular));
|
|
2242
|
+
row('Orphan Files', String(resultA.dependencies.orphans), String(resultB.dependencies.orphans));
|
|
2243
|
+
row('Mother Code DNA', `${resultA.motherCode.coverage}%`, `${resultB.motherCode.coverage}%`);
|
|
2244
|
+
row('Drift Issues', String(resultA.motherCode.drifted), String(resultB.motherCode.drifted));
|
|
2245
|
+
row('Scan Time', `${resultA.elapsed}ms`, `${resultB.elapsed}ms`);
|
|
2246
|
+
|
|
2247
|
+
console.log(c('bold', ' └──────────────────────┴──────────────────────┴──────────────────────┘'));
|
|
2248
|
+
console.log('');
|
|
2249
|
+
|
|
2250
|
+
// ─── Verdict ────────────────────────────────────────────
|
|
2251
|
+
const diff = resultA.score - resultB.score;
|
|
2252
|
+
if (diff === 0) {
|
|
2253
|
+
console.log(c('cyan', ` VERDICT: Both codebases score identically (${resultA.score}/100)`));
|
|
2254
|
+
} else {
|
|
2255
|
+
const winner = diff > 0 ? resultA : resultB;
|
|
2256
|
+
const loser = diff > 0 ? resultB : resultA;
|
|
2257
|
+
console.log(c('cyan', ` VERDICT: ${winner.label} is healthier`));
|
|
2258
|
+
console.log(c('gray', ` ${winner.label} scores ${winner.score}/100 (${winner.grade}) vs ${loser.label} at ${loser.score}/100 (${loser.grade})`));
|
|
2259
|
+
console.log(c('gray', ` ${Math.abs(diff)} point difference — ${Math.abs(diff) > 20 ? 'significant gap' : Math.abs(diff) > 10 ? 'moderate gap' : 'close race'}`));
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
// ─── Issue breakdown comparison ─────────────────────────
|
|
2263
|
+
const allCats = new Set([...Object.keys(resultA.categories), ...Object.keys(resultB.categories)]);
|
|
2264
|
+
if (allCats.size > 0) {
|
|
2265
|
+
console.log('');
|
|
2266
|
+
console.log(c('bold', ' ISSUE BREAKDOWN'));
|
|
2267
|
+
for (const cat of allCats) {
|
|
2268
|
+
const countA = resultA.categories[cat] || 0;
|
|
2269
|
+
const countB = resultB.categories[cat] || 0;
|
|
2270
|
+
const catLabel = cat.charAt(0).toUpperCase() + cat.slice(1);
|
|
2271
|
+
console.log(c('gray', ` ${pad(catLabel, 22)} ${padL(countA, 5)} vs ${padL(countB, 5)} ${countA < countB ? c('green', `← ${labelA}`) : countA > countB ? c('green', `→ ${labelB}`) : c('gray', 'tied')}`));
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
console.log('');
|
|
2276
|
+
console.log(c('gray', ` Full scan: npx thuban scan ${paths[0]}`));
|
|
2277
|
+
console.log(c('gray', ` Full scan: npx thuban scan ${paths[1]}`));
|
|
2278
|
+
console.log('');
|
|
2279
|
+
}
|
|
2280
|
+
|
|
1789
2281
|
async function main() {
|
|
1790
2282
|
const args = process.argv.slice(2);
|
|
1791
2283
|
const opts = parseArgs(args);
|
|
@@ -1802,9 +2294,9 @@ async function main() {
|
|
|
1802
2294
|
}
|
|
1803
2295
|
|
|
1804
2296
|
// Verify target path exists
|
|
1805
|
-
const noPathCommands2 = ['activate', 'status', 'upgrade'];
|
|
2297
|
+
const noPathCommands2 = ['activate', 'status', 'upgrade', 'telemetry', 'compare'];
|
|
1806
2298
|
|
|
1807
|
-
if (!noPathCommands2.includes(opts.command) && !fs.existsSync(opts.targetPath)) {
|
|
2299
|
+
if (!noPathCommands2.includes(opts.command) && !opts.targetPath.match(/^(https?:\/\/|git@)/) && !fs.existsSync(opts.targetPath)) {
|
|
1808
2300
|
console.error(c('red', ` Error: Path not found: ${opts.targetPath}`));
|
|
1809
2301
|
process.exit(1);
|
|
1810
2302
|
}
|
|
@@ -1887,9 +2379,27 @@ async function main() {
|
|
|
1887
2379
|
case 'baseline':
|
|
1888
2380
|
await cmdBaseline(opts);
|
|
1889
2381
|
break;
|
|
2382
|
+
case 'telemetry':
|
|
2383
|
+
await cmdTelemetry(opts);
|
|
2384
|
+
break;
|
|
2385
|
+
case 'compare':
|
|
2386
|
+
await cmdCompare(opts);
|
|
2387
|
+
break;
|
|
1890
2388
|
default:
|
|
2389
|
+
console.error('');
|
|
1891
2390
|
console.error(c('red', ` Unknown command: ${opts.command}`));
|
|
1892
|
-
console.error(
|
|
2391
|
+
console.error('');
|
|
2392
|
+
// Suggest closest match
|
|
2393
|
+
const allCmds = ['scan','compare','deps','drift','inject','health','report','debt','fix','hallucinate','watch','dashboard','diff','gate','cost','ghosts','ai-score','clones','passport','executive','baseline','activate','status','upgrade','telemetry'];
|
|
2394
|
+
const suggestions = allCmds.filter(cmd => cmd.startsWith(opts.command.slice(0,2)) || cmd.includes(opts.command));
|
|
2395
|
+
if (suggestions.length > 0) {
|
|
2396
|
+
console.error(c('gray', ` Did you mean: `) + c('cyan', suggestions.join(', ')) + c('gray', '?'));
|
|
2397
|
+
console.error('');
|
|
2398
|
+
}
|
|
2399
|
+
console.error(c('gray', ' Quick start:'));
|
|
2400
|
+
console.error(` ${c('cyan', 'npx thuban scan .')} Scan your project`);
|
|
2401
|
+
console.error(` ${c('cyan', 'npx thuban --help')} See all commands`);
|
|
2402
|
+
console.error('');
|
|
1893
2403
|
process.exit(1);
|
|
1894
2404
|
}
|
|
1895
2405
|
} catch (err) {
|
package/package.json
CHANGED
|
@@ -333,9 +333,10 @@ class CodeScanner extends EventEmitter {
|
|
|
333
333
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
334
334
|
const lines = content.split('\n');
|
|
335
335
|
const ext = path.extname(filePath).toLowerCase();
|
|
336
|
+
const supportedExts = ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs', '.py', '.pyw', '.go', '.rs', '.java', '.kt', '.cs'];
|
|
336
337
|
|
|
337
|
-
//
|
|
338
|
-
if (
|
|
338
|
+
// Scan supported source files with pattern matching
|
|
339
|
+
if (supportedExts.includes(ext)) {
|
|
339
340
|
// Security scan
|
|
340
341
|
issues.push(...this._runPatternScan(filePath, content, lines, this.securityPatterns, 'security'));
|
|
341
342
|
|
|
@@ -47,7 +47,7 @@ class DependencyGraph extends EventEmitter {
|
|
|
47
47
|
* Build the full dependency graph
|
|
48
48
|
*/
|
|
49
49
|
async build() {
|
|
50
|
-
|
|
50
|
+
// debug logging silenced for clean output
|
|
51
51
|
const startTime = Date.now();
|
|
52
52
|
|
|
53
53
|
// Clear existing graph
|
|
@@ -62,7 +62,6 @@ class DependencyGraph extends EventEmitter {
|
|
|
62
62
|
|
|
63
63
|
// Discover all files
|
|
64
64
|
const files = await this._discoverFiles(this.config.rootPath);
|
|
65
|
-
console.log(`[DEPENDENCY-GRAPH] Found ${files.length} files to analyze`);
|
|
66
65
|
|
|
67
66
|
// First pass: extract all imports
|
|
68
67
|
for (const file of files) {
|
|
@@ -76,8 +75,6 @@ class DependencyGraph extends EventEmitter {
|
|
|
76
75
|
this._detectCircularDependencies();
|
|
77
76
|
|
|
78
77
|
const duration = Date.now() - startTime;
|
|
79
|
-
console.log(`[DEPENDENCY-GRAPH] Graph built in ${duration}ms`);
|
|
80
|
-
console.log(`[DEPENDENCY-GRAPH] ${this.stats.filesAnalyzed} files, ${this.stats.totalImports} imports, ${this.stats.unresolvedImports} unresolved`);
|
|
81
78
|
|
|
82
79
|
return this.getStats();
|
|
83
80
|
}
|
|
@@ -36,7 +36,6 @@ class DriftDetector extends EventEmitter {
|
|
|
36
36
|
* Detect drift across all files
|
|
37
37
|
*/
|
|
38
38
|
async detectAll() {
|
|
39
|
-
console.log('[DRIFT-DETECTOR] Starting drift detection...');
|
|
40
39
|
const startTime = Date.now();
|
|
41
40
|
|
|
42
41
|
const result = {
|
|
@@ -86,7 +85,6 @@ class DriftDetector extends EventEmitter {
|
|
|
86
85
|
result.issues.push(...this._analyzeCrossFile(result.moduleMap));
|
|
87
86
|
|
|
88
87
|
result.duration = Date.now() - startTime;
|
|
89
|
-
console.log(`[DRIFT-DETECTOR] Detection complete: ${result.filesAnalyzed} files, ${result.issues.length} drift issues`);
|
|
90
88
|
|
|
91
89
|
return result;
|
|
92
90
|
} catch (error) {
|
|
@@ -69,6 +69,81 @@ class HallucinationDetector {
|
|
|
69
69
|
{ pattern: /process\.exit\s*\(\s*['"]/, suggestion: 'process.exit() takes a number, not a string', name: 'process.exit(string)', id: 'HALL_API018' },
|
|
70
70
|
];
|
|
71
71
|
|
|
72
|
+
// ─── Python phantom APIs ────────────────────────────────
|
|
73
|
+
this.pythonPhantomAPIs = [
|
|
74
|
+
{ pattern: /os\.path\.exists_sync\s*\(/, suggestion: 'Use os.path.exists() — exists_sync does not exist in Python', name: 'os.path.exists_sync', id: 'PY_HALL001' },
|
|
75
|
+
{ pattern: /json\.tryParse\s*\(/, suggestion: 'Use json.loads() with try/except', name: 'json.tryParse', id: 'PY_HALL002' },
|
|
76
|
+
{ pattern: /list\.flatMap\s*\(/, suggestion: 'Python lists have no flatMap — use list comprehension', name: 'list.flatMap', id: 'PY_HALL003' },
|
|
77
|
+
{ pattern: /dict\.merge\s*\(/, suggestion: 'Use {**dict1, **dict2} or dict1 | dict2 (3.9+)', name: 'dict.merge', id: 'PY_HALL004' },
|
|
78
|
+
{ pattern: /string\.format_map\s*\((?!.*\bself\b)/, suggestion: 'str.format_map exists but is rarely correct — did you mean .format()?', name: 'string.format_map misuse', id: 'PY_HALL005' },
|
|
79
|
+
{ pattern: /from\s+collections\s+import\s+OrderedDict.*#.*maintain\s+order/i, suggestion: 'Regular dict maintains order since Python 3.7 — OrderedDict is unnecessary', name: 'Unnecessary OrderedDict', id: 'PY_HALL006' },
|
|
80
|
+
{ pattern: /async\s+def\s+\w+.*asyncio\.sleep\s*\(\s*0\s*\)\s*#.*yield/i, suggestion: 'asyncio.sleep(0) to yield is a code smell — review async design', name: 'asyncio.sleep(0) hack', id: 'PY_HALL007' },
|
|
81
|
+
{ pattern: /import\s+tensorflow\.v2/, suggestion: 'tensorflow.v2 is not a real module — use import tensorflow', name: 'tensorflow.v2', id: 'PY_HALL008' },
|
|
82
|
+
{ pattern: /from\s+sklearn\.model_selection\s+import\s+train_test_split_v2/, suggestion: 'train_test_split_v2 does not exist — use train_test_split', name: 'sklearn v2 hallucination', id: 'PY_HALL009' },
|
|
83
|
+
{ pattern: /requests\.async_get\s*\(/, suggestion: 'requests has no async_get — use aiohttp or httpx', name: 'requests.async_get', id: 'PY_HALL010' },
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
this.pythonDeprecated = [
|
|
87
|
+
{ pattern: /from\s+distutils/, suggestion: 'distutils removed in Python 3.12 — use setuptools', since: 'Python 3.12', name: 'distutils', id: 'PY_DEPR001' },
|
|
88
|
+
{ pattern: /from\s+imp\s+import/, suggestion: 'imp removed in Python 3.12 — use importlib', since: 'Python 3.12', name: 'imp module', id: 'PY_DEPR002' },
|
|
89
|
+
{ pattern: /asyncio\.get_event_loop\(\)/, suggestion: 'Deprecated in 3.10+ — use asyncio.run() or get_running_loop()', since: 'Python 3.10', name: 'asyncio.get_event_loop()', id: 'PY_DEPR003' },
|
|
90
|
+
{ pattern: /collections\.MutableMapping/, suggestion: 'Moved to collections.abc.MutableMapping in Python 3.3+', since: 'Python 3.9', name: 'collections.MutableMapping', id: 'PY_DEPR004' },
|
|
91
|
+
{ pattern: /optparse\.OptionParser/, suggestion: 'optparse deprecated since Python 3.2 — use argparse', since: 'Python 3.2', name: 'optparse', id: 'PY_DEPR005' },
|
|
92
|
+
{ pattern: /cgi\.parse_header\s*\(/, suggestion: 'cgi module deprecated in Python 3.11, removed in 3.13', since: 'Python 3.11', name: 'cgi module', id: 'PY_DEPR006' },
|
|
93
|
+
{ pattern: /from\s+typing\s+import\s+(List|Dict|Tuple|Set)\b/, suggestion: 'Use built-in list, dict, tuple, set for type hints (Python 3.9+)', since: 'Python 3.9', name: 'typing.List/Dict/Tuple', id: 'PY_DEPR007' },
|
|
94
|
+
{ pattern: /unittest\.makeSuite\s*\(/, suggestion: 'makeSuite deprecated — use TestLoader.loadTestsFromTestCase', since: 'Python 3.11', name: 'unittest.makeSuite', id: 'PY_DEPR008' },
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
this.pythonSmells = [
|
|
98
|
+
{ pattern: /print\s*\(\s*f?['"]\s*debug/i, id: 'PY_SMELL001', name: 'Debug Print', message: 'Debug print statement left in code' },
|
|
99
|
+
{ pattern: /except\s*:\s*$|except\s+Exception\s*:\s*\n\s*pass/m, id: 'PY_SMELL002', name: 'Bare Except', message: 'Bare except or swallowed exception — hides real errors' },
|
|
100
|
+
{ pattern: /#\s*TODO:?\s*(implement|add|fix|complete|finish)/i, id: 'PY_SMELL003', name: 'Placeholder TODO', message: 'AI-generated placeholder — likely unfinished' },
|
|
101
|
+
{ pattern: /raise\s+NotImplementedError\s*\(?\s*\)?/, id: 'PY_SMELL004', name: 'Not Implemented', message: 'Stub implementation — function body was not generated' },
|
|
102
|
+
{ pattern: /['"]your.api.key.here['"]|['"]sk-[.]{3,}['"]/i, id: 'PY_SMELL005', name: 'Dummy Credential', message: 'Placeholder credential — will fail at runtime' },
|
|
103
|
+
{ pattern: /localhost:\d{4}(?!.*(?:dev|test|local|development))/i, id: 'PY_SMELL006', name: 'Hardcoded Localhost', message: 'Hardcoded localhost URL — will fail in production' },
|
|
104
|
+
{ pattern: /import\s+\*/, id: 'PY_SMELL007', name: 'Wildcard Import', message: 'Wildcard import — pollutes namespace, hides dependencies' },
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
// ─── Go phantom APIs ────────────────────────────────────
|
|
108
|
+
this.goPhantomAPIs = [
|
|
109
|
+
{ pattern: /ioutil\.ReadFile\s*\(/, suggestion: 'ioutil removed in Go 1.16 — use os.ReadFile()', name: 'ioutil.ReadFile', id: 'GO_HALL001' },
|
|
110
|
+
{ pattern: /ioutil\.WriteFile\s*\(/, suggestion: 'ioutil removed in Go 1.16 — use os.WriteFile()', name: 'ioutil.WriteFile', id: 'GO_HALL002' },
|
|
111
|
+
{ pattern: /ioutil\.TempDir\s*\(/, suggestion: 'ioutil removed in Go 1.16 — use os.MkdirTemp()', name: 'ioutil.TempDir', id: 'GO_HALL003' },
|
|
112
|
+
{ pattern: /ioutil\.ReadAll\s*\(/, suggestion: 'ioutil removed in Go 1.16 — use io.ReadAll()', name: 'ioutil.ReadAll', id: 'GO_HALL004' },
|
|
113
|
+
{ pattern: /strings\.Title\s*\(/, suggestion: 'strings.Title deprecated in Go 1.18 — use cases.Title from golang.org/x/text', name: 'strings.Title', id: 'GO_HALL005' },
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
this.goSmells = [
|
|
117
|
+
{ pattern: /\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i, id: 'GO_SMELL001', name: 'Placeholder TODO', message: 'AI-generated placeholder — likely unfinished' },
|
|
118
|
+
{ pattern: /panic\s*\(\s*["']not implemented["']\s*\)/, id: 'GO_SMELL002', name: 'Panic Not Implemented', message: 'Stub implementation — will panic at runtime' },
|
|
119
|
+
{ pattern: /localhost:\d{4}(?!.*(?:dev|test|local|development))/i, id: 'GO_SMELL003', name: 'Hardcoded Localhost', message: 'Hardcoded localhost URL — will fail in production' },
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
// ─── Rust phantom APIs ──────────────────────────────────
|
|
123
|
+
this.rustSmells = [
|
|
124
|
+
{ pattern: /todo!\s*\(\s*\)/, id: 'RS_SMELL001', name: 'todo! macro', message: 'todo!() macro — will panic at runtime' },
|
|
125
|
+
{ pattern: /unimplemented!\s*\(\s*\)/, id: 'RS_SMELL002', name: 'unimplemented! macro', message: 'unimplemented!() macro — will panic at runtime' },
|
|
126
|
+
{ pattern: /\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i, id: 'RS_SMELL003', name: 'Placeholder TODO', message: 'AI-generated placeholder — likely unfinished' },
|
|
127
|
+
{ pattern: /unwrap\(\).*unwrap\(\)/, id: 'RS_SMELL004', name: 'Double Unwrap', message: 'Chained unwrap() — will panic on None/Err' },
|
|
128
|
+
{ pattern: /localhost:\d{4}(?!.*(?:dev|test|local|development))/i, id: 'RS_SMELL005', name: 'Hardcoded Localhost', message: 'Hardcoded localhost URL — will fail in production' },
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
// ─── Java/C# common issues ──────────────────────────────
|
|
132
|
+
this.javaSmells = [
|
|
133
|
+
{ pattern: /System\.out\.println\s*\(.*debug/i, id: 'JAVA_SMELL001', name: 'Debug Println', message: 'Debug System.out.println — use a logger' },
|
|
134
|
+
{ pattern: /\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i, id: 'JAVA_SMELL002', name: 'Placeholder TODO', message: 'AI-generated placeholder — likely unfinished' },
|
|
135
|
+
{ pattern: /throw\s+new\s+(?:Runtime)?Exception\s*\(\s*["']Not\s+implemented["']/i, id: 'JAVA_SMELL003', name: 'Not Implemented', message: 'Stub implementation — will throw at runtime' },
|
|
136
|
+
{ pattern: /catch\s*\(\s*Exception\s+\w+\s*\)\s*\{\s*\}/, id: 'JAVA_SMELL004', name: 'Empty Catch', message: 'Empty catch block — swallows all exceptions' },
|
|
137
|
+
{ pattern: /localhost:\d{4}(?!.*(?:dev|test|local|development))/i, id: 'JAVA_SMELL005', name: 'Hardcoded Localhost', message: 'Hardcoded localhost URL — will fail in production' },
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
// Language extensions mapping
|
|
141
|
+
this.jsExtensions = new Set(['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs']);
|
|
142
|
+
this.pyExtensions = new Set(['.py', '.pyw']);
|
|
143
|
+
this.goExtensions = new Set(['.go']);
|
|
144
|
+
this.rustExtensions = new Set(['.rs']);
|
|
145
|
+
this.javaExtensions = new Set(['.java', '.kt', '.cs']);
|
|
146
|
+
|
|
72
147
|
// Patterns suggesting AI-generated code with common issues
|
|
73
148
|
this.aiCodeSmells = [
|
|
74
149
|
{ pattern: /\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)\s*(this|here|later)/i, id: 'AI_SMELL001', name: 'Placeholder TODO', message: 'AI-generated placeholder — likely unfinished implementation' },
|
|
@@ -122,7 +197,13 @@ class HallucinationDetector {
|
|
|
122
197
|
for (const file of files) {
|
|
123
198
|
try {
|
|
124
199
|
const ext = path.extname(file).toLowerCase();
|
|
125
|
-
|
|
200
|
+
const isJS = this.jsExtensions.has(ext);
|
|
201
|
+
const isPython = this.pyExtensions.has(ext);
|
|
202
|
+
const isGo = this.goExtensions.has(ext);
|
|
203
|
+
const isRust = this.rustExtensions.has(ext);
|
|
204
|
+
const isJava = this.javaExtensions.has(ext);
|
|
205
|
+
|
|
206
|
+
if (!isJS && !isPython && !isGo && !isRust && !isJava) continue;
|
|
126
207
|
|
|
127
208
|
const relPath = path.relative(this.config.rootPath, file);
|
|
128
209
|
|
|
@@ -147,20 +228,31 @@ class HallucinationDetector {
|
|
|
147
228
|
|
|
148
229
|
results.stats.filesScanned++;
|
|
149
230
|
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
231
|
+
// ─── Language-specific checks ──────────────────────
|
|
232
|
+
if (isJS) {
|
|
233
|
+
// 1. Phantom imports — packages that don't exist
|
|
234
|
+
this._checkPhantomImports(relPath, content, lines, declaredDeps, results, ignoredLines);
|
|
235
|
+
// 2. Phantom APIs — methods that don't exist on known objects
|
|
236
|
+
if (!isPatternFile) {
|
|
237
|
+
this._checkPhantomAPIs(relPath, content, lines, results, ignoredLines);
|
|
238
|
+
}
|
|
239
|
+
// 3. AI code smells
|
|
240
|
+
this._checkAISmells(relPath, content, lines, results, ignoredLines);
|
|
241
|
+
// 4. Deprecated APIs
|
|
242
|
+
if (!isPatternFile) {
|
|
243
|
+
this._checkDeprecatedAPIs(relPath, content, lines, results, ignoredLines);
|
|
244
|
+
}
|
|
245
|
+
} else if (isPython) {
|
|
246
|
+
this._checkLanguagePatterns(relPath, content, lines, results, ignoredLines, this.pythonPhantomAPIs, 'phantomAPIs');
|
|
247
|
+
this._checkLanguagePatterns(relPath, content, lines, results, ignoredLines, this.pythonDeprecated, 'deprecatedAPIs');
|
|
248
|
+
this._checkLanguageSmells(relPath, content, lines, results, ignoredLines, this.pythonSmells);
|
|
249
|
+
} else if (isGo) {
|
|
250
|
+
this._checkLanguagePatterns(relPath, content, lines, results, ignoredLines, this.goPhantomAPIs, 'phantomAPIs');
|
|
251
|
+
this._checkLanguageSmells(relPath, content, lines, results, ignoredLines, this.goSmells);
|
|
252
|
+
} else if (isRust) {
|
|
253
|
+
this._checkLanguageSmells(relPath, content, lines, results, ignoredLines, this.rustSmells);
|
|
254
|
+
} else if (isJava) {
|
|
255
|
+
this._checkLanguageSmells(relPath, content, lines, results, ignoredLines, this.javaSmells);
|
|
164
256
|
}
|
|
165
257
|
|
|
166
258
|
} catch (e) { /* skip unreadable */ }
|
|
@@ -178,6 +270,59 @@ class HallucinationDetector {
|
|
|
178
270
|
return results;
|
|
179
271
|
}
|
|
180
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Generic language pattern checker (for phantom APIs and deprecated APIs)
|
|
275
|
+
*/
|
|
276
|
+
_checkLanguagePatterns(relPath, content, lines, results, ignoredLines, patterns, resultKey) {
|
|
277
|
+
for (let i = 0; i < lines.length; i++) {
|
|
278
|
+
if (ignoredLines.has(i + 1)) continue;
|
|
279
|
+
const line = lines[i];
|
|
280
|
+
for (const check of patterns) {
|
|
281
|
+
if (check.pattern.test(line)) {
|
|
282
|
+
const entry = {
|
|
283
|
+
file: relPath,
|
|
284
|
+
line: i + 1,
|
|
285
|
+
name: check.name,
|
|
286
|
+
id: check.id,
|
|
287
|
+
};
|
|
288
|
+
if (check.suggestion) entry.suggestion = check.suggestion;
|
|
289
|
+
if (check.since) {
|
|
290
|
+
entry.since = check.since;
|
|
291
|
+
entry.fix = check.suggestion;
|
|
292
|
+
}
|
|
293
|
+
if (resultKey === 'deprecatedAPIs') {
|
|
294
|
+
results.deprecatedAPIs.push(entry);
|
|
295
|
+
} else {
|
|
296
|
+
results.phantomAPIs.push(entry);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Generic language smell checker
|
|
305
|
+
*/
|
|
306
|
+
_checkLanguageSmells(relPath, content, lines, results, ignoredLines, smells) {
|
|
307
|
+
for (let i = 0; i < lines.length; i++) {
|
|
308
|
+
if (ignoredLines.has(i + 1)) continue;
|
|
309
|
+
const line = lines[i];
|
|
310
|
+
for (const smell of smells) {
|
|
311
|
+
if (smell.pattern.test(line)) {
|
|
312
|
+
results.aiSmells.push({
|
|
313
|
+
file: relPath,
|
|
314
|
+
line: i + 1,
|
|
315
|
+
id: smell.id,
|
|
316
|
+
name: smell.name,
|
|
317
|
+
message: smell.message,
|
|
318
|
+
type: 'ai_smell',
|
|
319
|
+
severity: 'medium',
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
181
326
|
/**
|
|
182
327
|
* Format results for terminal output
|
|
183
328
|
*/
|
|
@@ -150,6 +150,38 @@ class TechDebtAnalyzer {
|
|
|
150
150
|
|
|
151
151
|
// ─── 3. Code Quality Debt ───────────────────────────────
|
|
152
152
|
const qualityIssues = [];
|
|
153
|
+
const HallucinationDetector = require('./hallucination-detector');
|
|
154
|
+
const hallDetector = new HallucinationDetector(this.config);
|
|
155
|
+
const hallResults = await hallDetector.scan(files);
|
|
156
|
+
|
|
157
|
+
// Add deprecated APIs as fixable items
|
|
158
|
+
for (const item of (hallResults.deprecatedAPIs || [])) {
|
|
159
|
+
qualityIssues.push({
|
|
160
|
+
file: path.relative(this.config.rootPath, item.file),
|
|
161
|
+
type: 'deprecated_api',
|
|
162
|
+
severity: 'high',
|
|
163
|
+
message: `${item.name} — deprecated since ${item.since}`,
|
|
164
|
+
fixable: true,
|
|
165
|
+
fixAction: 'update_deprecated_api',
|
|
166
|
+
suggestion: item.fix,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Add hardcoded localhost URLs as fixable items
|
|
171
|
+
for (const item of (hallResults.aiSmells || [])) {
|
|
172
|
+
if (item.message && item.message.includes('localhost')) {
|
|
173
|
+
qualityIssues.push({
|
|
174
|
+
file: path.relative(this.config.rootPath, item.file),
|
|
175
|
+
type: 'hardcoded_localhost',
|
|
176
|
+
severity: 'medium',
|
|
177
|
+
message: item.message,
|
|
178
|
+
fixable: true,
|
|
179
|
+
fixAction: 'wrap_localhost_url',
|
|
180
|
+
suggestion: 'Use environment variable with localhost fallback',
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
153
185
|
for (const [file, result] of Object.entries(scanResults)) {
|
|
154
186
|
if (result.issues) {
|
|
155
187
|
for (const issue of result.issues) {
|
|
@@ -320,6 +352,56 @@ class TechDebtAnalyzer {
|
|
|
320
352
|
break;
|
|
321
353
|
}
|
|
322
354
|
|
|
355
|
+
case 'update_deprecated_api': {
|
|
356
|
+
const fullPath = path.resolve(this.config.rootPath, item.file);
|
|
357
|
+
let content = fs.readFileSync(fullPath, 'utf-8');
|
|
358
|
+
const before = content;
|
|
359
|
+
// url.parse() → new URL()
|
|
360
|
+
content = content.replace(/url\.parse\(([^)]+)\)/g, 'new URL($1)');
|
|
361
|
+
// new Buffer() → Buffer.from()
|
|
362
|
+
content = content.replace(/new Buffer\(([^)]+)\)/g, 'Buffer.from($1)');
|
|
363
|
+
// require('sys') → require('util')
|
|
364
|
+
content = content.replace(/require\(['"]sys['"]\)/g, "require('util')");
|
|
365
|
+
// util.isArray() → Array.isArray()
|
|
366
|
+
content = content.replace(/util\.isArray\(/g, 'Array.isArray(');
|
|
367
|
+
// util.isFunction() → typeof x === 'function'
|
|
368
|
+
content = content.replace(/util\.isFunction\(([^)]+)\)/g, "typeof $1 === 'function'");
|
|
369
|
+
// fs.exists() callback → fs.existsSync or fs.access
|
|
370
|
+
content = content.replace(/fs\.exists\(([^,]+),\s*/g, 'fs.access($1, ');
|
|
371
|
+
if (content !== before) {
|
|
372
|
+
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
373
|
+
fixes.push({ ...item, status: 'fixed', detail: 'Replaced deprecated API with modern equivalent' });
|
|
374
|
+
} else {
|
|
375
|
+
fixes.push({ ...item, status: 'skipped', reason: 'Pattern not matched for auto-replacement' });
|
|
376
|
+
}
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
case 'wrap_localhost_url': {
|
|
381
|
+
const fullPath = path.resolve(this.config.rootPath, item.file);
|
|
382
|
+
let content = fs.readFileSync(fullPath, 'utf-8');
|
|
383
|
+
const before = content;
|
|
384
|
+
// Wrap hardcoded localhost URLs in env var fallback
|
|
385
|
+
// Match: 'http://localhost:NNNN' or "http://localhost:NNNN" or `http://localhost:NNNN`
|
|
386
|
+
content = content.replace(
|
|
387
|
+
/(['"`])(https?:\/\/localhost:\d{2,5}(?:\/[^'"`]*)?)\1/g,
|
|
388
|
+
(match, quote, url) => {
|
|
389
|
+
// Generate an env var name from the URL
|
|
390
|
+
const portMatch = url.match(/:(\d+)/);
|
|
391
|
+
const port = portMatch ? portMatch[1] : 'PORT';
|
|
392
|
+
const envVar = `SERVICE_URL_${port}`;
|
|
393
|
+
return `(process.env.${envVar} || ${quote}${url}${quote})`;
|
|
394
|
+
}
|
|
395
|
+
);
|
|
396
|
+
if (content !== before) {
|
|
397
|
+
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
398
|
+
fixes.push({ ...item, status: 'fixed', detail: 'Wrapped localhost URL in env var fallback' });
|
|
399
|
+
} else {
|
|
400
|
+
fixes.push({ ...item, status: 'skipped', reason: 'Pattern not matched for auto-replacement' });
|
|
401
|
+
}
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
|
|
323
405
|
case 'extract_to_env': {
|
|
324
406
|
// Don't auto-fix secrets — too risky. Flag for manual review.
|
|
325
407
|
fixes.push({ ...item, status: 'flagged_for_review', reason: 'Secrets require manual extraction to .env' });
|