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 CHANGED
@@ -75,51 +75,96 @@ function printHelp() {
75
75
  banner();
76
76
  const lm = new LicenseManager();
77
77
  const status = lm.getStatus();
78
- console.log(c('gray', ` License: ${status.tierName} tier`) + (status.licensed ? c('green', '') : ''));
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', 'thuban activate')} <key> Activate a license key`);
106
- console.log(` ${c('cyan', 'thuban status')} Show license and usage status`);
107
- console.log(` ${c('cyan', 'thuban upgrade')} Get a license key`);
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', '--json')} Output as JSON`);
112
- console.log(` ${c('gray', '--fix')} Apply fixes (default: dry-run preview)`);
113
- console.log(` ${c('gray', '--dry-run')} Preview changes without applying`);
114
- console.log(` ${c('gray', '--unsafe')} Allow runtime-changing fixes`);
115
- console.log(` ${c('gray', '--commit')} Auto-commit each fix for easy rollback`);
116
- console.log(` ${c('gray', '--baseline')} Only report NEW issues (vs baseline)`);
117
- console.log(` ${c('gray', '--baseline-create')} Snapshot current issues as baseline`);
118
- console.log(` ${c('gray', '--verbose')} Show detailed output`);
119
- console.log(` ${c('gray', '--ignore <pattern>')} Ignore files matching pattern`);
120
- console.log(` ${c('gray', '--max-issues <n>')} Limit issues shown (default: 50)`);
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', ' https://thuban.dev'));
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', '.json', '.mjs', '.cjs'];
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 ') + c('cyan', '║'));
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' : ''}`)) + c('red', ' (will crash at runtime)'));
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', ` HIGH: ${deprecated.length} Deprecated API${deprecated.length > 1 ? 's' : ''}`)) + c('gray', ' (will break on Node upgrade)'));
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
- const showCount = tier.showFullDetails ? Math.min(qualityIssues.length, 10) : Math.min(3, qualityIssues.length);
448
- for (const item of qualityIssues.slice(0, showCount)) {
449
- const lineRef = item.line ? `:${item.line}` : '';
450
- const sevColor = item.severity === 'high' || item.severity === 'error' ? 'red' : 'yellow';
451
- console.log(` ${c(sevColor, '!')} ${c('cyan', item.file + lineRef)} ${c('gray', item.message)}`);
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 (qualityIssues.length > showCount) {
454
- console.log(c('gray', ` ... ${qualityIssues.length - showCount} more`));
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 fix command
601
+ // Pro/Team: show clear next steps
505
602
  if (allIssues.length > 0) {
506
- console.log(` ${c('cyan', 'thuban fix . --fix')} Auto-fix all fixable issues`);
507
- console.log(` ${c('cyan', 'thuban dashboard .')} Generate full HTML report`);
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('green', ` ✓ Fixed ${result.fixed} items`));
977
- console.log(c('yellow', ` Flagged ${result.flagged} items for manual review`));
978
- if (result.failed > 0) console.log(c('red', ` ✗ Failed: ${result.failed}`));
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('gray', ` Skipped ${result.skippedUnsafe} unsafe fixes (use --unsafe to apply)`));
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
- console.log(c('gray', ` Run 'thuban report' to see updated scores`));
984
- if (!opts.gitCommit) {
985
- console.log(c('gray', ` Tip: use --commit to auto-commit fixes for easy rollback`));
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(c('gray', ` Run 'thuban --help' for usage`));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thuban",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "The safety layer for AI-coded software. Detect hallucinated APIs, ghost code, tech debt, and architecture drift. One command. Zero dependencies.",
5
5
  "bin": {
6
6
  "thuban": "./cli.js"
@@ -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
- // Only scan JS/TS files with pattern matching
338
- if (['.js', '.ts', '.jsx', '.tsx'].includes(ext)) {
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
- console.log('[DEPENDENCY-GRAPH] Building dependency graph...');
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
- if (!['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs'].includes(ext)) continue;
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
- // 1. Phantom imports — packages that don't exist
151
- this._checkPhantomImports(relPath, content, lines, declaredDeps, results, ignoredLines);
152
-
153
- // 2. Phantom APIs methods that don't exist on known objects
154
- if (!isPatternFile) {
155
- this._checkPhantomAPIs(relPath, content, lines, results, ignoredLines);
156
- }
157
-
158
- // 3. AI code smells — patterns typical of AI-generated code
159
- this._checkAISmells(relPath, content, lines, results, ignoredLines);
160
-
161
- // 4. Deprecated APIs — APIs that LLMs still suggest
162
- if (!isPatternFile) {
163
- this._checkDeprecatedAPIs(relPath, content, lines, results, ignoredLines);
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' });