thuban 0.3.0 → 0.3.1
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 +237 -63
- 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
|
@@ -75,51 +75,96 @@ function printHelp() {
|
|
|
75
75
|
banner();
|
|
76
76
|
const lm = new LicenseManager();
|
|
77
77
|
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)`);
|
|
78
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'));
|
|
79
|
+
console.log(c('gray', ` v${pkg.version} · ${status.tierName} tier`) + (status.licensed ? c('green', ' ✓') : '') + c('gray', ' · https://thuban.dev'));
|
|
102
80
|
console.log('');
|
|
81
|
+
|
|
82
|
+
// ─── QUICK START ──────────────────────────────────────────
|
|
83
|
+
console.log(c('bold', ' QUICK START'));
|
|
84
|
+
console.log('');
|
|
85
|
+
console.log(c('gray', ' Run these in order to see what Thuban does:'));
|
|
86
|
+
console.log('');
|
|
87
|
+
console.log(` ${c('cyan', 'npx thuban scan .')} Scan your project`);
|
|
88
|
+
console.log(` ${c('cyan', 'npx thuban fix .')} Preview what can be fixed`);
|
|
89
|
+
console.log(` ${c('cyan', 'npx thuban fix . --fix')} Apply all safe fixes`);
|
|
90
|
+
console.log(` ${c('cyan', 'npx thuban dashboard .')} Open visual HTML report`);
|
|
91
|
+
console.log('');
|
|
92
|
+
|
|
93
|
+
// ─── SCAN & DETECT ───────────────────────────────────────
|
|
94
|
+
console.log(c('bold', ' SCAN & DETECT'));
|
|
95
|
+
console.log('');
|
|
96
|
+
console.log(` ${c('cyan', 'scan')} [path] Full health scan — score, grade, all issues`);
|
|
97
|
+
console.log(` ${c('cyan', 'hallucinate')} [path] Find phantom APIs and invented imports`);
|
|
98
|
+
console.log(` ${c('cyan', 'ghosts')} [path] Dead functions nobody calls`);
|
|
99
|
+
console.log(` ${c('cyan', 'ai-score')} [path] Probability each function is AI-generated`);
|
|
100
|
+
console.log(` ${c('cyan', 'clones')} [path] Copy-pasted code that has drifted apart`);
|
|
101
|
+
console.log(` ${c('cyan', 'drift')} [path] Files that changed since Mother Code was set`);
|
|
102
|
+
console.log(` ${c('cyan', 'deps')} [path] Dependency map — circular, orphan, critical`);
|
|
103
|
+
console.log(` ${c('cyan', 'diff')} [path] Scan only git-changed files`);
|
|
104
|
+
console.log(` ${c('cyan', 'debt')} [path] Detailed tech debt breakdown`);
|
|
105
|
+
console.log(` ${c('cyan', 'cost')} [path] Tech debt translated to £ and hours`);
|
|
106
|
+
console.log('');
|
|
107
|
+
|
|
108
|
+
// ─── FIX & PROTECT ───────────────────────────────────────
|
|
109
|
+
console.log(c('bold', ' FIX & PROTECT'));
|
|
110
|
+
console.log('');
|
|
111
|
+
console.log(` ${c('cyan', 'fix')} [path] Preview fixable issues ${c('gray', '(safe, no changes)')}`);
|
|
112
|
+
console.log(` ${c('cyan', 'fix')} [path] ${c('gray', '--fix')} Apply all safe fixes`);
|
|
113
|
+
console.log(` ${c('cyan', 'fix')} [path] ${c('gray', '--fix --commit')} Each fix = 1 git commit ${c('gray', '(revert any)')}`);
|
|
114
|
+
console.log(` ${c('cyan', 'inject')} [path] Add Mother Code DNA to every file`);
|
|
115
|
+
console.log(` ${c('cyan', 'gate')} Install pre-commit hook ${c('gray', '(blocks hallucinations)')}`);
|
|
116
|
+
console.log(` ${c('cyan', 'watch')} [path] Live sentinel — scans files as you save`);
|
|
117
|
+
console.log(` ${c('cyan', 'baseline')} [path] Snapshot issues so future scans show only NEW ones`);
|
|
118
|
+
console.log('');
|
|
119
|
+
|
|
120
|
+
// ─── REPORTS ──────────────────────────────────────────────
|
|
121
|
+
console.log(c('bold', ' REPORTS'));
|
|
122
|
+
console.log('');
|
|
123
|
+
console.log(` ${c('cyan', 'dashboard')} [path] Interactive HTML report ${c('gray', '(share with your team)')}`);
|
|
124
|
+
console.log(` ${c('cyan', 'executive')} [path] CTO-ready report ${c('gray', '(print to PDF from browser)')}`);
|
|
125
|
+
console.log(` ${c('cyan', 'report')} [path] Combined scan + deps + drift summary`);
|
|
126
|
+
console.log(` ${c('cyan', 'passport')} [path] Codebase identity card ${c('gray', '(for new team members)')}`);
|
|
127
|
+
console.log('');
|
|
128
|
+
|
|
129
|
+
// ─── LICENSE ──────────────────────────────────────────────
|
|
103
130
|
console.log(c('bold', ' LICENSE'));
|
|
104
131
|
console.log('');
|
|
105
|
-
console.log(` ${c('cyan', '
|
|
106
|
-
console.log(` ${c('cyan', '
|
|
107
|
-
console.log(` ${c('cyan', '
|
|
132
|
+
console.log(` ${c('cyan', 'activate')} <key> Activate a license key`);
|
|
133
|
+
console.log(` ${c('cyan', 'status')} Show license, usage, and features`);
|
|
134
|
+
console.log(` ${c('cyan', 'upgrade')} Open pricing page`);
|
|
108
135
|
console.log('');
|
|
136
|
+
|
|
137
|
+
// ─── OPTIONS ──────────────────────────────────────────────
|
|
109
138
|
console.log(c('bold', ' OPTIONS'));
|
|
110
139
|
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
|
-
|
|
140
|
+
console.log(` ${c('gray', '--fix')} Apply changes ${c('gray', '(default is dry-run preview)')}`);
|
|
141
|
+
console.log(` ${c('gray', '--commit')} Auto-commit each fix for easy rollback`);
|
|
142
|
+
console.log(` ${c('gray', '--unsafe')} Include fixes that change runtime behaviour`);
|
|
143
|
+
console.log(` ${c('gray', '--json')} Machine-readable JSON output`);
|
|
144
|
+
console.log(` ${c('gray', '--baseline')} Only show NEW issues vs saved baseline`);
|
|
145
|
+
console.log(` ${c('gray', '--verbose')} Show detailed output`);
|
|
146
|
+
console.log(` ${c('gray', '--ignore <pat>')} Skip files matching pattern`);
|
|
147
|
+
console.log(` ${c('gray', '--max-issues <n>')} Limit displayed issues ${c('gray', '(default: 50)')}`);
|
|
148
|
+
console.log('');
|
|
149
|
+
|
|
150
|
+
// ─── EXAMPLES ─────────────────────────────────────────────
|
|
151
|
+
console.log(c('bold', ' EXAMPLES'));
|
|
152
|
+
console.log('');
|
|
153
|
+
console.log(c('gray', ' # Scan the current project'));
|
|
154
|
+
console.log(` ${c('white', 'npx thuban scan .')}`);
|
|
155
|
+
console.log('');
|
|
156
|
+
console.log(c('gray', ' # Find and fix all AI hallucinations'));
|
|
157
|
+
console.log(` ${c('white', 'npx thuban hallucinate .')}`);
|
|
158
|
+
console.log(` ${c('white', 'npx thuban fix . --fix')}`);
|
|
159
|
+
console.log('');
|
|
160
|
+
console.log(c('gray', ' # Generate a report for your CTO'));
|
|
161
|
+
console.log(` ${c('white', 'npx thuban executive .')}`);
|
|
162
|
+
console.log('');
|
|
163
|
+
console.log(c('gray', ' # Block hallucinated code on every commit'));
|
|
164
|
+
console.log(` ${c('white', 'npx thuban gate --fix')}`);
|
|
121
165
|
console.log('');
|
|
122
|
-
console.log(c('gray', '
|
|
166
|
+
console.log(c('gray', ' # Scan a specific folder, ignore tests'));
|
|
167
|
+
console.log(` ${c('white', 'npx thuban scan ./src --ignore "**/*.test.js"')}`);
|
|
123
168
|
console.log('');
|
|
124
169
|
}
|
|
125
170
|
|
|
@@ -171,7 +216,7 @@ function parseArgs(args) {
|
|
|
171
216
|
function collectFiles(dir, ignorePatterns = []) {
|
|
172
217
|
const defaultIgnore = ['node_modules', '.git', 'dist', 'build', '.next', '__pycache__', '.venv', 'venv', 'coverage', '.cache', '.turbo'];
|
|
173
218
|
const allIgnore = [...defaultIgnore, ...ignorePatterns];
|
|
174
|
-
const extensions = ['.js', '.ts', '.jsx', '.tsx', '.py', '.
|
|
219
|
+
const extensions = ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs', '.py', '.pyw', '.go', '.rs', '.java', '.kt', '.cs', '.json'];
|
|
175
220
|
const MAX_FILE_SIZE = 1024 * 1024; // 1MB per file — skip anything larger
|
|
176
221
|
const MAX_FILES = 50000; // Hard cap to prevent memory issues
|
|
177
222
|
const files = [];
|
|
@@ -237,6 +282,23 @@ async function cmdScan(opts) {
|
|
|
237
282
|
|
|
238
283
|
if (!opts.json) {
|
|
239
284
|
banner();
|
|
285
|
+
|
|
286
|
+
// First-run welcome
|
|
287
|
+
if (lm.usage.totalScans === 0) {
|
|
288
|
+
console.log(c('cyan', ' ┌──────────────────────────────────────────────────────┐'));
|
|
289
|
+
console.log(c('cyan', ' │') + c('bold', ' Welcome to Thuban! ') + c('cyan', '│'));
|
|
290
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
|
|
291
|
+
console.log(c('cyan', ' │') + ' ' + c('gray', 'Thuban scans your codebase for AI hallucinations,') + ' ' + c('cyan', '│'));
|
|
292
|
+
console.log(c('cyan', ' │') + ' ' + c('gray', 'dead code, tech debt, and architecture drift.') + ' ' + c('cyan', '│'));
|
|
293
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
|
|
294
|
+
console.log(c('cyan', ' │') + ' ' + c('gray', 'After this scan, try:') + ' ' + c('cyan', '│'));
|
|
295
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban fix .') + ' ' + c('gray', 'Preview fixes') + ' ' + c('cyan', '│'));
|
|
296
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban dashboard .') + ' ' + c('gray', 'Visual report') + ' ' + c('cyan', '│'));
|
|
297
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban --help') + ' ' + c('gray', 'All commands') + ' ' + c('cyan', '│'));
|
|
298
|
+
console.log(c('cyan', ' └──────────────────────────────────────────────────────┘'));
|
|
299
|
+
console.log('');
|
|
300
|
+
}
|
|
301
|
+
|
|
240
302
|
console.log(c('bold', ' SCANNING: ') + c('cyan', targetPath));
|
|
241
303
|
console.log('');
|
|
242
304
|
}
|
|
@@ -360,18 +422,24 @@ async function cmdScan(opts) {
|
|
|
360
422
|
// THE GUT-PUNCH OUTPUT
|
|
361
423
|
// ═══════════════════════════════════════════════════════════
|
|
362
424
|
|
|
425
|
+
// ─── Count fixable items ─────────────────────────────────
|
|
426
|
+
const fixableCount = allIssues.filter(i => i.fixable !== false).length;
|
|
427
|
+
|
|
363
428
|
console.log(c('cyan', ' ╔══════════════════════════════════════════════════════╗'));
|
|
364
|
-
console.log(c('cyan', ' ║') + c('bold', ' THUBAN SCAN
|
|
429
|
+
console.log(c('cyan', ' ║') + c('bold', ' THUBAN SCAN RESULTS ') + c('cyan', '║'));
|
|
365
430
|
console.log(c('cyan', ' ╠══════════════════════════════════════════════════════╣'));
|
|
366
431
|
console.log(c('cyan', ' ║') + ` Files scanned: ${c('bold', String(files.length).padEnd(6))}` + (isFileCapped ? c('yellow', ` of ${totalFileCount}`) : ' ') + c('cyan', ' ║'));
|
|
367
432
|
console.log(c('cyan', ' ║') + ` Health score: ${c(scoreColor, (grade + ' (' + Math.round(score) + '/100)').padEnd(20))}` + c('cyan', ' ║'));
|
|
433
|
+
console.log(c('cyan', ' ║') + ` Issues found: ${c(allIssues.length > 0 ? 'yellow' : 'green', String(allIssues.length).padEnd(20))}` + c('cyan', ' ║'));
|
|
434
|
+
console.log(c('cyan', ' ║') + ` Auto-fixable: ${c('green', String(fixableCount).padEnd(20))}` + c('cyan', ' ║'));
|
|
368
435
|
console.log(c('cyan', ' ║') + ` Scan time: ${c('gray', (elapsed + 'ms').padEnd(20))}` + c('cyan', ' ║'));
|
|
369
436
|
console.log(c('cyan', ' ╚══════════════════════════════════════════════════════╝'));
|
|
370
437
|
console.log('');
|
|
371
438
|
|
|
372
439
|
// ─── CRITICAL: Hallucinated APIs (will crash) ────────────
|
|
373
440
|
if (hallucinations.length > 0) {
|
|
374
|
-
console.log(c('bgRed', ' CRITICAL ') + c('red', c('bold', ` ${hallucinations.length} Hallucinated API${hallucinations.length > 1 ? 's' : ''}`))
|
|
441
|
+
console.log(c('bgRed', ' CRITICAL ') + c('red', c('bold', ` ${hallucinations.length} Hallucinated API${hallucinations.length > 1 ? 's' : ''}`)));
|
|
442
|
+
console.log(c('gray', ' These imports or method calls do not exist. Your code WILL crash when it hits them.'));
|
|
375
443
|
console.log('');
|
|
376
444
|
|
|
377
445
|
const showCount = tier.showFullDetails ? hallucinations.length : Math.min(tier.teaserIssues, hallucinations.length);
|
|
@@ -403,7 +471,9 @@ async function cmdScan(opts) {
|
|
|
403
471
|
|
|
404
472
|
// ─── HIGH: Deprecated APIs ───────────────────────────────
|
|
405
473
|
if (deprecated.length > 0) {
|
|
406
|
-
console.log(c('red', c('bold', `
|
|
474
|
+
console.log(c('red', c('bold', ` DEPRECATED: ${deprecated.length} API${deprecated.length > 1 ? 's' : ''} that will break on upgrade`)));
|
|
475
|
+
console.log(c('gray', ' These work NOW but will stop working when you upgrade Node.js or your dependencies.'));
|
|
476
|
+
console.log(c('gray', ' Fix: ') + c('cyan', 'npx thuban fix . --fix --unsafe'));
|
|
407
477
|
console.log('');
|
|
408
478
|
|
|
409
479
|
const showCount = tier.showFullDetails ? deprecated.length : Math.min(tier.teaserIssues, deprecated.length);
|
|
@@ -443,17 +513,44 @@ async function cmdScan(opts) {
|
|
|
443
513
|
}
|
|
444
514
|
|
|
445
515
|
if (qualityIssues.length > 0) {
|
|
516
|
+
// Group quality issues by type for clearer output
|
|
517
|
+
const qualityByType = {};
|
|
518
|
+
for (const item of qualityIssues) {
|
|
519
|
+
const key = item.message || item.type || 'other';
|
|
520
|
+
// Normalize similar messages
|
|
521
|
+
const normalizedKey = key.replace(/\s*—.*$/, '').trim();
|
|
522
|
+
if (!qualityByType[normalizedKey]) qualityByType[normalizedKey] = [];
|
|
523
|
+
qualityByType[normalizedKey].push(item);
|
|
524
|
+
}
|
|
525
|
+
const typeEntries = Object.entries(qualityByType).sort((a, b) => b[1].length - a[1].length);
|
|
526
|
+
|
|
446
527
|
console.log(c('yellow', c('bold', ` CODE QUALITY: ${qualityIssues.length} issue${qualityIssues.length > 1 ? 's' : ''}`)));
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
528
|
+
console.log(c('gray', ' Patterns that reduce code quality, maintainability, or production readiness.'));
|
|
529
|
+
console.log('');
|
|
530
|
+
|
|
531
|
+
// Show grouped summary
|
|
532
|
+
const showTypes = tier.showFullDetails ? typeEntries.length : Math.min(3, typeEntries.length);
|
|
533
|
+
for (let t = 0; t < showTypes; t++) {
|
|
534
|
+
const [typeName, items] = typeEntries[t];
|
|
535
|
+
const sevColor = items[0].severity === 'high' || items[0].severity === 'error' ? 'red' : 'yellow';
|
|
536
|
+
console.log(` ${c(sevColor, '!')} ${c('bold', String(items.length))} x ${typeName}`);
|
|
537
|
+
// Show first example file
|
|
538
|
+
if (items[0].file) {
|
|
539
|
+
const lineRef = items[0].line ? `:${items[0].line}` : '';
|
|
540
|
+
console.log(` ${c('gray', 'e.g.')} ${c('cyan', items[0].file + lineRef)}`);
|
|
541
|
+
}
|
|
452
542
|
}
|
|
453
|
-
if (
|
|
454
|
-
console.log(c('gray', ` ... ${
|
|
543
|
+
if (typeEntries.length > showTypes) {
|
|
544
|
+
console.log(c('gray', ` ... ${typeEntries.length - showTypes} more issue types`));
|
|
455
545
|
}
|
|
456
546
|
console.log('');
|
|
547
|
+
|
|
548
|
+
// Show fix command if fixable
|
|
549
|
+
const fixableQuality = qualityIssues.filter(i => i.fixable !== false).length;
|
|
550
|
+
if (fixableQuality > 0) {
|
|
551
|
+
console.log(c('gray', ` ${fixableQuality} of these can be auto-fixed: `) + c('cyan', 'npx thuban fix . --fix --unsafe'));
|
|
552
|
+
console.log('');
|
|
553
|
+
}
|
|
457
554
|
}
|
|
458
555
|
|
|
459
556
|
// ─── THE VERDICT ─────────────────────────────────────────
|
|
@@ -501,10 +598,36 @@ async function cmdScan(opts) {
|
|
|
501
598
|
console.log(c('gray', ` Free scans remaining this month: ${remaining}/${scanStatus.limit}`));
|
|
502
599
|
console.log('');
|
|
503
600
|
} else {
|
|
504
|
-
// Pro/Team: show
|
|
601
|
+
// Pro/Team: show clear next steps
|
|
505
602
|
if (allIssues.length > 0) {
|
|
506
|
-
|
|
507
|
-
console.log(
|
|
603
|
+
const fixable = allIssues.filter(i => i.fixable !== false).length;
|
|
604
|
+
console.log(c('cyan', ' ┌──────────────────────────────────────────────────────┐'));
|
|
605
|
+
console.log(c('cyan', ' │') + c('bold', ' WHAT TO DO NEXT ') + c('cyan', '│'));
|
|
606
|
+
console.log(c('cyan', ' ├──────────────────────────────────────────────────────┤'));
|
|
607
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
|
|
608
|
+
console.log(c('cyan', ' │') + ' ' + c('bold', '1. See what would be fixed (safe preview):') + ' ' + c('cyan', '│'));
|
|
609
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban fix . ') + ' ' + c('cyan', '│'));
|
|
610
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
|
|
611
|
+
console.log(c('cyan', ' │') + ' ' + c('bold', '2. Apply all safe fixes:') + ' ' + c('cyan', '│'));
|
|
612
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban fix . --fix') + ' ' + c('cyan', '│'));
|
|
613
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
|
|
614
|
+
console.log(c('cyan', ' │') + ' ' + c('bold', '3. Apply fixes with git rollback:') + ' ' + c('cyan', '│'));
|
|
615
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban fix . --fix --commit') + ' ' + c('cyan', '│'));
|
|
616
|
+
console.log(c('cyan', ' │') + ' ' + c('gray', 'Each fix = 1 git commit. Undo any with git revert.') + c('cyan', '│'));
|
|
617
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
|
|
618
|
+
console.log(c('cyan', ' │') + ' ' + c('bold', '4. Generate visual report (HTML):') + ' ' + c('cyan', '│'));
|
|
619
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban dashboard .') + ' ' + c('cyan', '│'));
|
|
620
|
+
console.log(c('cyan', ' │') + ' ' + c('gray', 'Opens a dashboard you can share with your team.') + ' ' + c('cyan', '│'));
|
|
621
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
|
|
622
|
+
console.log(c('cyan', ' │') + ' ' + c('bold', '5. Show tech debt cost in £:') + ' ' + c('cyan', '│'));
|
|
623
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban cost .') + ' ' + c('cyan', '│'));
|
|
624
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
|
|
625
|
+
console.log(c('cyan', ' │') + ' ' + c('bold', '6. Block bad code on every commit:') + ' ' + c('cyan', '│'));
|
|
626
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban gate') + ' ' + c('cyan', '│'));
|
|
627
|
+
console.log(c('cyan', ' │') + ' ' + c('gray', 'Installs a pre-commit hook. Set once, forget.') + ' ' + c('cyan', '│'));
|
|
628
|
+
console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
|
|
629
|
+
console.log(c('cyan', ' │') + ' ' + c('gray', 'All commands: npx thuban --help') + ' ' + c('cyan', '│'));
|
|
630
|
+
console.log(c('cyan', ' └──────────────────────────────────────────────────────┘'));
|
|
508
631
|
console.log('');
|
|
509
632
|
}
|
|
510
633
|
}
|
|
@@ -514,6 +637,19 @@ async function cmdScan(opts) {
|
|
|
514
637
|
console.log(c('gray', ` Your actual score may be worse. Upgrade to scan everything.`));
|
|
515
638
|
console.log('');
|
|
516
639
|
}
|
|
640
|
+
|
|
641
|
+
// ─── Continuous monitoring pitch ────────────────────────
|
|
642
|
+
if (allIssues.length > 0 && !isFileCapped) {
|
|
643
|
+
console.log(c('gray', ' ─────────────────────────────────────────────────────'));
|
|
644
|
+
console.log(c('gray', ' Thuban runs 100% on your machine. No data leaves.'));
|
|
645
|
+
console.log(c('gray', ' No cloud costs. As your codebase grows, Thuban grows with it.'));
|
|
646
|
+
console.log('');
|
|
647
|
+
console.log(c('gray', ' Keep your codebase healthy continuously:'));
|
|
648
|
+
console.log(c('gray', ' ') + c('cyan', 'npx thuban gate --fix') + c('gray', ' Block bad code on every commit'));
|
|
649
|
+
console.log(c('gray', ' ') + c('cyan', 'npx thuban watch .') + c('gray', ' Scan files as you save them'));
|
|
650
|
+
console.log(c('gray', ' ') + c('cyan', 'npx thuban baseline .') + c('gray', ' Only alert on NEW issues'));
|
|
651
|
+
console.log('');
|
|
652
|
+
}
|
|
517
653
|
}
|
|
518
654
|
|
|
519
655
|
async function cmdDeps(opts) {
|
|
@@ -882,7 +1018,7 @@ async function cmdFix(opts) {
|
|
|
882
1018
|
|
|
883
1019
|
const grouped = {};
|
|
884
1020
|
const safeActions = ['inject_widget', 'regenerate_widget', 'remove_console_log', 'flag_for_removal'];
|
|
885
|
-
const unsafeActions = ['break_circular', 'extract_to_env', 'update_deprecated_api'];
|
|
1021
|
+
const unsafeActions = ['break_circular', 'extract_to_env', 'update_deprecated_api', 'wrap_localhost_url'];
|
|
886
1022
|
let safeCount = 0;
|
|
887
1023
|
let unsafeCount = 0;
|
|
888
1024
|
|
|
@@ -914,7 +1050,8 @@ async function cmdFix(opts) {
|
|
|
914
1050
|
const label = {
|
|
915
1051
|
'break_circular': 'Restructure circular dependencies',
|
|
916
1052
|
'extract_to_env': 'Extract hardcoded secrets to env vars',
|
|
917
|
-
'update_deprecated_api': 'Replace deprecated API calls',
|
|
1053
|
+
'update_deprecated_api': 'Replace deprecated API calls with modern equivalents',
|
|
1054
|
+
'wrap_localhost_url': 'Wrap hardcoded localhost URLs in env var fallback',
|
|
918
1055
|
}[action] || action;
|
|
919
1056
|
console.log(` ${c('yellow', '!')} ${label}: ${c('bold', count)}`);
|
|
920
1057
|
}
|
|
@@ -973,18 +1110,43 @@ async function cmdFix(opts) {
|
|
|
973
1110
|
if (opts.json) {
|
|
974
1111
|
console.log(JSON.stringify(result, null, 2));
|
|
975
1112
|
} else {
|
|
976
|
-
console.log(c('
|
|
977
|
-
console.log(c('
|
|
978
|
-
|
|
1113
|
+
console.log(c('cyan', ' ╔══════════════════════════════════════════════════════╗'));
|
|
1114
|
+
console.log(c('cyan', ' ║') + c('bold', ' FIX RESULTS ') + c('cyan', '║'));
|
|
1115
|
+
console.log(c('cyan', ' ╠══════════════════════════════════════════════════════╣'));
|
|
1116
|
+
console.log(c('cyan', ' ║') + ` Fixed: ${c('green', String(result.fixed).padEnd(20))}` + c('cyan', ' ║'));
|
|
1117
|
+
console.log(c('cyan', ' ║') + ` Flagged for review: ${c('yellow', String(result.flagged).padEnd(19))}` + c('cyan', ' ║'));
|
|
1118
|
+
if (result.failed > 0) {
|
|
1119
|
+
console.log(c('cyan', ' ║') + ` Failed: ${c('red', String(result.failed).padEnd(20))}` + c('cyan', ' ║'));
|
|
1120
|
+
}
|
|
979
1121
|
if (result.skippedUnsafe > 0) {
|
|
980
|
-
console.log(c('
|
|
1122
|
+
console.log(c('cyan', ' ║') + ` Skipped (unsafe): ${c('gray', String(result.skippedUnsafe).padEnd(20))}` + c('cyan', ' ║'));
|
|
981
1123
|
}
|
|
1124
|
+
console.log(c('cyan', ' ╚══════════════════════════════════════════════════════╝'));
|
|
982
1125
|
console.log('');
|
|
983
|
-
|
|
984
|
-
if (
|
|
985
|
-
console.log(c('
|
|
1126
|
+
|
|
1127
|
+
if (result.fixed > 0) {
|
|
1128
|
+
console.log(c('green', ' Your files have been updated. Changes are saved to disk.'));
|
|
1129
|
+
if (!opts.gitCommit) {
|
|
1130
|
+
console.log(c('gray', ' Tip: use --commit next time to auto-commit each fix for easy rollback.'));
|
|
1131
|
+
}
|
|
1132
|
+
console.log('');
|
|
1133
|
+
console.log(c('gray', ' Verify the changes:'));
|
|
1134
|
+
console.log(c('gray', ' ') + c('cyan', 'npx thuban scan .') + c('gray', ' Re-scan to see your new score'));
|
|
1135
|
+
console.log(c('gray', ' ') + c('cyan', 'npx thuban dashboard .') + c('gray', ' Generate updated report'));
|
|
1136
|
+
console.log('');
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
if (result.skippedUnsafe > 0) {
|
|
1140
|
+
console.log(c('gray', ` ${result.skippedUnsafe} unsafe fixes were skipped. These change runtime behaviour.`));
|
|
1141
|
+
console.log(c('gray', ' Review them with: ') + c('cyan', 'npx thuban fix . --fix --unsafe'));
|
|
1142
|
+
console.log('');
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
if (result.flagged > 0) {
|
|
1146
|
+
console.log(c('gray', ` ${result.flagged} items need human review (secrets, orphan files, complex patterns).`));
|
|
1147
|
+
console.log(c('gray', ' See details: ') + c('cyan', 'npx thuban dashboard .'));
|
|
1148
|
+
console.log('');
|
|
986
1149
|
}
|
|
987
|
-
console.log('');
|
|
988
1150
|
}
|
|
989
1151
|
}
|
|
990
1152
|
|
|
@@ -1888,8 +2050,20 @@ async function main() {
|
|
|
1888
2050
|
await cmdBaseline(opts);
|
|
1889
2051
|
break;
|
|
1890
2052
|
default:
|
|
2053
|
+
console.error('');
|
|
1891
2054
|
console.error(c('red', ` Unknown command: ${opts.command}`));
|
|
1892
|
-
console.error(
|
|
2055
|
+
console.error('');
|
|
2056
|
+
// Suggest closest match
|
|
2057
|
+
const allCmds = ['scan','deps','drift','inject','health','report','debt','fix','hallucinate','watch','dashboard','diff','gate','cost','ghosts','ai-score','clones','passport','executive','baseline','activate','status','upgrade'];
|
|
2058
|
+
const suggestions = allCmds.filter(cmd => cmd.startsWith(opts.command.slice(0,2)) || cmd.includes(opts.command));
|
|
2059
|
+
if (suggestions.length > 0) {
|
|
2060
|
+
console.error(c('gray', ` Did you mean: `) + c('cyan', suggestions.join(', ')) + c('gray', '?'));
|
|
2061
|
+
console.error('');
|
|
2062
|
+
}
|
|
2063
|
+
console.error(c('gray', ' Quick start:'));
|
|
2064
|
+
console.error(` ${c('cyan', 'npx thuban scan .')} Scan your project`);
|
|
2065
|
+
console.error(` ${c('cyan', 'npx thuban --help')} See all commands`);
|
|
2066
|
+
console.error('');
|
|
1893
2067
|
process.exit(1);
|
|
1894
2068
|
}
|
|
1895
2069
|
} 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' });
|