thuban 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +137 -102
  2. package/dist/LICENSE +21 -0
  3. package/dist/README.md +185 -0
  4. package/dist/cli.js +2 -0
  5. package/dist/packages/scanner/ai-confidence-scorer.js +1 -0
  6. package/dist/packages/scanner/alert-manager.js +1 -0
  7. package/dist/packages/scanner/baseline-manager.js +1 -0
  8. package/dist/packages/scanner/code-scanner.js +1 -0
  9. package/dist/packages/scanner/codebase-passport.js +1 -0
  10. package/dist/packages/scanner/copy-paste-detector.js +1 -0
  11. package/dist/packages/scanner/dependency-graph.js +1 -0
  12. package/dist/packages/scanner/drift-detector.js +1 -0
  13. package/dist/packages/scanner/executive-report.js +1 -0
  14. package/dist/packages/scanner/file-collector.js +1 -0
  15. package/dist/packages/scanner/file-watcher.js +1 -0
  16. package/dist/packages/scanner/ghost-code-detector.js +1 -0
  17. package/dist/packages/scanner/hallucination-detector.js +1 -0
  18. package/dist/packages/scanner/health-checker.js +1 -0
  19. package/dist/packages/scanner/html-report.js +1 -0
  20. package/dist/packages/scanner/index.js +1 -0
  21. package/dist/packages/scanner/license-manager.js +1 -0
  22. package/dist/packages/scanner/master-health-checker.js +1 -0
  23. package/dist/packages/scanner/monitor-notifier.js +1 -0
  24. package/dist/packages/scanner/monitor-service.js +1 -0
  25. package/dist/packages/scanner/monitor-store.js +1 -0
  26. package/dist/packages/scanner/pre-commit-gate.js +1 -0
  27. package/dist/packages/scanner/scan-diff.js +1 -0
  28. package/dist/packages/scanner/scan-runner.js +1 -0
  29. package/dist/packages/scanner/secret-scanner.js +1 -0
  30. package/dist/packages/scanner/sentinel-core.js +1 -0
  31. package/dist/packages/scanner/sentinel-knowledge.js +1 -0
  32. package/dist/packages/scanner/tech-debt-analyzer.js +1 -0
  33. package/dist/packages/scanner/tech-debt-cost.js +1 -0
  34. package/dist/packages/scanner/widget-generator.js +1 -0
  35. package/package.json +16 -8
  36. package/cli.js +0 -2412
  37. package/packages/scanner/ai-confidence-scorer.js +0 -260
  38. package/packages/scanner/alert-manager.js +0 -398
  39. package/packages/scanner/baseline-manager.js +0 -109
  40. package/packages/scanner/code-scanner.js +0 -713
  41. package/packages/scanner/codebase-passport.js +0 -299
  42. package/packages/scanner/copy-paste-detector.js +0 -250
  43. package/packages/scanner/dependency-graph.js +0 -536
  44. package/packages/scanner/drift-detector.js +0 -423
  45. package/packages/scanner/executive-report.js +0 -774
  46. package/packages/scanner/file-watcher.js +0 -323
  47. package/packages/scanner/ghost-code-detector.js +0 -226
  48. package/packages/scanner/hallucination-detector.js +0 -652
  49. package/packages/scanner/health-checker.js +0 -586
  50. package/packages/scanner/html-report.js +0 -634
  51. package/packages/scanner/index.js +0 -92
  52. package/packages/scanner/license-manager.js +0 -318
  53. package/packages/scanner/master-health-checker.js +0 -513
  54. package/packages/scanner/pre-commit-gate.js +0 -216
  55. package/packages/scanner/sentinel-core.js +0 -608
  56. package/packages/scanner/sentinel-knowledge.js +0 -322
  57. package/packages/scanner/tech-debt-analyzer.js +0 -565
  58. package/packages/scanner/tech-debt-cost.js +0 -194
  59. package/packages/scanner/widget-generator.js +0 -415
@@ -1,565 +0,0 @@
1
- /**
2
- * Thuban Tech Debt Analyzer & Auto-Fixer
3
- *
4
- * @purpose Score, visualise, and auto-fix technical debt
5
- * @module thuban
6
- * @layer application
7
- * @imports-from code-scanner, dependency-graph, drift-detector, widget-generator
8
- * @exports-to cli, dashboard, reports
9
- * @side-effects File system reads and writes when fixing
10
- * @critical-for Core Thuban value proposition — the "fix it" button
11
- */
12
-
13
- const fs = require('fs');
14
- const path = require('path');
15
- const CodeScanner = require('./code-scanner');
16
- const DependencyGraph = require('./dependency-graph');
17
- const DriftDetector = require('./drift-detector');
18
- const WidgetGenerator = require('./widget-generator');
19
-
20
- class TechDebtAnalyzer {
21
- constructor(config = {}) {
22
- this.config = {
23
- rootPath: config.rootPath || process.cwd(),
24
- ...config
25
- };
26
-
27
- this.scanner = new CodeScanner(this.config);
28
- this.depGraph = new DependencyGraph(this.config);
29
- this.driftDetector = new DriftDetector(this.config);
30
- this.widgetGen = new WidgetGenerator({ ...this.config, dryRun: true });
31
- }
32
-
33
- /**
34
- * Full tech debt analysis — the complete health picture
35
- * Returns a structured report with scores, categories, and fixable items
36
- */
37
- async analyze(files) {
38
- const startTime = Date.now();
39
-
40
- // Build dependency graph
41
- await this.depGraph.build(files);
42
- this.widgetGen.dependencyGraph = this.depGraph;
43
-
44
- // Run all analyses
45
- const scanResults = await this.scanner.scanFiles(files);
46
- const driftResults = await this.driftDetector.detectAll();
47
-
48
- // Categorise all issues into tech debt buckets
49
- const debt = {
50
- // Category scores (0-100, lower = more debt)
51
- scores: {
52
- overall: 100,
53
- security: 100,
54
- architecture: 100,
55
- codeQuality: 100,
56
- documentation: 100,
57
- dependencies: 100,
58
- motherCode: 100,
59
- },
60
-
61
- // Fixable items (can be auto-fixed)
62
- fixable: [],
63
- // Manual items (need human decision)
64
- manual: [],
65
- // Info items (awareness only)
66
- info: [],
67
-
68
- // Summary stats
69
- stats: {
70
- totalFiles: files.length,
71
- totalIssues: 0,
72
- fixableCount: 0,
73
- manualCount: 0,
74
- estimatedFixTime: '0 minutes',
75
- scanTime: 0,
76
- },
77
-
78
- // Detailed categories for dashboard
79
- categories: {},
80
- };
81
-
82
- // ─── 1. Security Debt ───────────────────────────────────
83
- const securityIssues = [];
84
- for (const [file, result] of Object.entries(scanResults)) {
85
- if (result.issues) {
86
- for (const issue of result.issues) {
87
- if (issue.id?.startsWith('SEC') || issue.category === 'security') {
88
- securityIssues.push({
89
- file: path.relative(this.config.rootPath, file),
90
- ...issue,
91
- fixable: issue.id === 'SEC001' || issue.id === 'SEC002', // Can extract to env vars
92
- fixAction: issue.id === 'SEC001' ? 'extract_to_env' : issue.id === 'SEC002' ? 'extract_to_env' : null,
93
- });
94
- }
95
- }
96
- }
97
- }
98
- debt.scores.security = Math.max(0, 100 - (securityIssues.filter(i => i.severity === 'critical').length * 25) - (securityIssues.filter(i => i.severity === 'high').length * 10));
99
- debt.categories.security = { score: debt.scores.security, issues: securityIssues, label: 'Security', icon: 'shield' };
100
-
101
- // ─── 2. Architecture Debt ───────────────────────────────
102
- const archIssues = [];
103
- const circular = this.depGraph.circularDeps || [];
104
- const orphans = this.depGraph.getOrphanFiles() || [];
105
- const critical = this.depGraph.getMostCriticalFiles(5) || [];
106
-
107
- for (const cycle of circular) {
108
- archIssues.push({
109
- type: 'circular_dependency',
110
- severity: 'high',
111
- files: Array.isArray(cycle) ? cycle.map(f => path.relative(this.config.rootPath, f)) : [String(cycle)],
112
- message: 'Circular dependency detected',
113
- fixable: true,
114
- fixAction: 'break_circular',
115
- suggestion: 'Extract shared code into a new module to break the cycle',
116
- });
117
- }
118
-
119
- for (const orphan of orphans) {
120
- const orphanPath = typeof orphan === 'string' ? orphan : (orphan.file || orphan.path || String(orphan));
121
- archIssues.push({
122
- type: 'orphan_file',
123
- severity: 'low',
124
- file: path.relative(this.config.rootPath, orphanPath),
125
- message: 'File has no imports and no exports consumed — potential dead code',
126
- fixable: true,
127
- fixAction: 'flag_for_removal',
128
- suggestion: 'Review and remove if truly unused',
129
- });
130
- }
131
-
132
- for (const crit of critical) {
133
- const critPath = typeof crit === 'string' ? crit : (crit.file || crit.path || String(crit));
134
- const dependents = typeof crit === 'object' ? (crit.dependents || crit.score || crit.count || 0) : 0;
135
- if (dependents > 10) {
136
- archIssues.push({
137
- type: 'god_file',
138
- severity: 'medium',
139
- file: path.relative(this.config.rootPath, critPath),
140
- dependents,
141
- message: `${dependents} files depend on this — high blast radius if changed`,
142
- fixable: false,
143
- suggestion: 'Consider splitting into smaller, focused modules',
144
- });
145
- }
146
- }
147
-
148
- debt.scores.architecture = Math.max(0, 100 - (circular.length * 15) - (orphans.length * 1) - (archIssues.filter(i => i.type === 'god_file').length * 5));
149
- debt.categories.architecture = { score: debt.scores.architecture, issues: archIssues, label: 'Architecture', icon: 'building' };
150
-
151
- // ─── 3. Code Quality Debt ───────────────────────────────
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
-
185
- for (const [file, result] of Object.entries(scanResults)) {
186
- if (result.issues) {
187
- for (const issue of result.issues) {
188
- if (issue.id?.startsWith('QUAL') || issue.category === 'quality' || issue.category === 'maintainability') {
189
- qualityIssues.push({
190
- file: path.relative(this.config.rootPath, file),
191
- ...issue,
192
- fixable: ['QUAL001', 'QUAL003', 'QUAL005'].includes(issue.id), // console.log removal, etc.
193
- fixAction: issue.id === 'QUAL001' ? 'remove_console_log' : null,
194
- });
195
- }
196
- }
197
- }
198
- }
199
- debt.scores.codeQuality = Math.max(0, 100 - (qualityIssues.filter(i => i.severity === 'high').length * 5) - (qualityIssues.filter(i => i.severity === 'medium').length * 2));
200
- debt.categories.codeQuality = { score: debt.scores.codeQuality, issues: qualityIssues, label: 'Code Quality', icon: 'code' };
201
-
202
- // ─── 4. Documentation / Mother Code Debt ────────────────
203
- const docIssues = [];
204
- let filesWithWidget = 0;
205
- let filesWithoutWidget = 0;
206
- let filesWithDrift = 0;
207
-
208
- for (const file of files) {
209
- try {
210
- const content = fs.readFileSync(file, 'utf-8');
211
- const hasWidget = /\/\*\*[\s\S]*?@(purpose|module|layer)[\s\S]*?\*\//.test(content);
212
- if (hasWidget) {
213
- filesWithWidget++;
214
- // Check for drift
215
- const analysis = await this.driftDetector.analyzeFile(file);
216
- if (analysis.issues && analysis.issues.length > 0) {
217
- filesWithDrift++;
218
- for (const issue of analysis.issues) {
219
- docIssues.push({
220
- file: path.relative(this.config.rootPath, file),
221
- ...issue,
222
- fixable: true,
223
- fixAction: 'regenerate_widget',
224
- });
225
- }
226
- }
227
- } else {
228
- filesWithoutWidget++;
229
- docIssues.push({
230
- file: path.relative(this.config.rootPath, file),
231
- type: 'missing_mother_code',
232
- severity: 'info',
233
- message: 'No Mother Code DNA — file lacks contextual awareness',
234
- fixable: true,
235
- fixAction: 'inject_widget',
236
- });
237
- }
238
- } catch (e) { /* skip unreadable */ }
239
- }
240
-
241
- const coverage = files.length > 0 ? Math.round((filesWithWidget / files.length) * 100) : 0;
242
- debt.scores.motherCode = Math.min(100, coverage + (filesWithDrift > 0 ? -10 : 0));
243
- debt.scores.documentation = debt.scores.motherCode;
244
- debt.categories.motherCode = {
245
- score: debt.scores.motherCode,
246
- coverage,
247
- annotated: filesWithWidget,
248
- missing: filesWithoutWidget,
249
- drifted: filesWithDrift,
250
- issues: docIssues,
251
- label: 'Mother Code',
252
- icon: 'dna',
253
- };
254
-
255
- // ─── 5. Dependency Health ───────────────────────────────
256
- const depIssues = [];
257
- // Check for very deep dependency chains
258
- const summary = this.depGraph.getModuleSummary ? this.depGraph.getModuleSummary() : {};
259
- debt.scores.dependencies = Math.max(0, 100 - (circular.length * 10) - (orphans.length * 0.5));
260
- debt.categories.dependencies = { score: debt.scores.dependencies, issues: depIssues, label: 'Dependencies', icon: 'link' };
261
-
262
- // ─── Calculate overall score ────────────────────────────
263
- const weights = { security: 0.25, architecture: 0.2, codeQuality: 0.2, motherCode: 0.2, dependencies: 0.15 };
264
- debt.scores.overall = Math.round(
265
- debt.scores.security * weights.security +
266
- debt.scores.architecture * weights.architecture +
267
- debt.scores.codeQuality * weights.codeQuality +
268
- debt.scores.motherCode * weights.motherCode +
269
- debt.scores.dependencies * weights.dependencies
270
- );
271
-
272
- // ─── Sort into fixable / manual / info ──────────────────
273
- for (const cat of Object.values(debt.categories)) {
274
- for (const issue of cat.issues || []) {
275
- debt.stats.totalIssues++;
276
- if (issue.fixable) {
277
- debt.fixable.push(issue);
278
- debt.stats.fixableCount++;
279
- } else if (issue.severity === 'info') {
280
- debt.info.push(issue);
281
- } else {
282
- debt.manual.push(issue);
283
- debt.stats.manualCount++;
284
- }
285
- }
286
- }
287
-
288
- // Estimate fix time
289
- const fixMinutes = debt.stats.fixableCount * 0.5 + debt.stats.manualCount * 15;
290
- debt.stats.estimatedFixTime = fixMinutes < 60
291
- ? `${Math.round(fixMinutes)} minutes`
292
- : `${Math.round(fixMinutes / 60)} hours`;
293
-
294
- debt.stats.scanTime = Date.now() - startTime;
295
-
296
- return debt;
297
- }
298
-
299
- /**
300
- * THE FIX BUTTON
301
- * Auto-fixes all fixable tech debt items
302
- * Returns a report of what was fixed
303
- */
304
- async fix(files, options = {}) {
305
- const debt = await this.analyze(files);
306
- const fixes = [];
307
- const failures = [];
308
-
309
- for (const item of debt.fixable) {
310
- if (options.dryRun) {
311
- fixes.push({ ...item, status: 'would_fix' });
312
- continue;
313
- }
314
-
315
- try {
316
- switch (item.fixAction) {
317
- case 'inject_widget': {
318
- const fullPath = path.resolve(this.config.rootPath, item.file);
319
- const content = fs.readFileSync(fullPath, 'utf-8');
320
- const widget = await this.widgetGen.generateWidget(fullPath, content);
321
- if (widget && widget.widgetString) {
322
- fs.writeFileSync(fullPath, widget.widgetString + '\n\n' + content, 'utf-8');
323
- fixes.push({ ...item, status: 'fixed' });
324
- }
325
- break;
326
- }
327
-
328
- case 'regenerate_widget': {
329
- const fullPath = path.resolve(this.config.rootPath, item.file);
330
- const content = fs.readFileSync(fullPath, 'utf-8');
331
- // Remove old widget
332
- const stripped = content.replace(/\/\*\*[\s\S]*?@(purpose|module|layer)[\s\S]*?\*\/\s*\n?\s*\n?/, '');
333
- // Generate new widget with current state
334
- const widget = await this.widgetGen.generateWidget(fullPath, stripped);
335
- if (widget && widget.widgetString) {
336
- fs.writeFileSync(fullPath, widget.widgetString + '\n\n' + stripped, 'utf-8');
337
- fixes.push({ ...item, status: 'fixed' });
338
- }
339
- break;
340
- }
341
-
342
- case 'remove_console_log': {
343
- const fullPath = path.resolve(this.config.rootPath, item.file);
344
- let content = fs.readFileSync(fullPath, 'utf-8');
345
- const before = content;
346
- // Remove console.log lines (but not console.error or console.warn)
347
- content = content.replace(/^\s*console\.log\(.*?\);\s*$/gm, '');
348
- if (content !== before) {
349
- fs.writeFileSync(fullPath, content, 'utf-8');
350
- fixes.push({ ...item, status: 'fixed' });
351
- }
352
- break;
353
- }
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
-
405
- case 'extract_to_env': {
406
- // Don't auto-fix secrets — too risky. Flag for manual review.
407
- fixes.push({ ...item, status: 'flagged_for_review', reason: 'Secrets require manual extraction to .env' });
408
- break;
409
- }
410
-
411
- case 'flag_for_removal': {
412
- // Don't auto-delete files — just confirm they're orphans
413
- fixes.push({ ...item, status: 'confirmed_orphan', reason: 'Verified no imports. Safe to remove manually.' });
414
- break;
415
- }
416
-
417
- default:
418
- fixes.push({ ...item, status: 'skipped', reason: 'No auto-fix available' });
419
- }
420
- } catch (err) {
421
- failures.push({ ...item, status: 'failed', error: err.message });
422
- }
423
- }
424
-
425
- return {
426
- totalFixable: debt.fixable.length,
427
- fixed: fixes.filter(f => f.status === 'fixed').length,
428
- flagged: fixes.filter(f => f.status === 'flagged_for_review' || f.status === 'confirmed_orphan').length,
429
- skipped: fixes.filter(f => f.status === 'skipped').length,
430
- failed: failures.length,
431
- fixes,
432
- failures,
433
- debtBefore: debt.scores.overall,
434
- // Re-analyze after fixes to show improvement
435
- ...(options.dryRun ? {} : { note: 'Run thuban report again to see updated scores' }),
436
- };
437
- }
438
-
439
- /**
440
- * Generate a human-readable tech debt report
441
- * Suitable for dashboard display or terminal output
442
- */
443
- formatReport(debt) {
444
- const C = {
445
- reset: '\x1b[0m', bold: '\x1b[1m',
446
- red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m',
447
- blue: '\x1b[34m', cyan: '\x1b[36m', gray: '\x1b[90m',
448
- bgRed: '\x1b[41m', bgGreen: '\x1b[42m', bgYellow: '\x1b[43m',
449
- };
450
-
451
- const scoreBar = (score) => {
452
- const filled = Math.round(score / 5);
453
- const empty = 20 - filled;
454
- const color = score >= 80 ? C.green : score >= 60 ? C.yellow : C.red;
455
- return `${color}${'█'.repeat(filled)}${C.gray}${'░'.repeat(empty)}${C.reset} ${color}${score}%${C.reset}`;
456
- };
457
-
458
- const grade = (score) => {
459
- if (score >= 90) return `${C.green}A${C.reset}`;
460
- if (score >= 80) return `${C.green}B${C.reset}`;
461
- if (score >= 70) return `${C.yellow}C${C.reset}`;
462
- if (score >= 60) return `${C.yellow}D${C.reset}`;
463
- return `${C.red}F${C.reset}`;
464
- };
465
-
466
- let output = '';
467
- output += `\n${C.cyan} ╔══════════════════════════════════════════════╗${C.reset}\n`;
468
- output += `${C.cyan} ║${C.bold} THUBAN TECH DEBT REPORT ${C.reset}${C.cyan}║${C.reset}\n`;
469
- output += `${C.cyan} ╚══════════════════════════════════════════════╝${C.reset}\n\n`;
470
-
471
- output += ` ${C.bold}Overall Health:${C.reset} ${scoreBar(debt.scores.overall)} Grade: ${grade(debt.scores.overall)}\n\n`;
472
-
473
- // Category breakdown
474
- output += ` ${C.bold}Category Breakdown:${C.reset}\n\n`;
475
- const categories = [
476
- ['Security', debt.scores.security, 'shield'],
477
- ['Architecture', debt.scores.architecture, 'building'],
478
- ['Code Quality', debt.scores.codeQuality, 'code'],
479
- ['Mother Code', debt.scores.motherCode, 'dna'],
480
- ['Dependencies', debt.scores.dependencies, 'link'],
481
- ];
482
-
483
- for (const [name, score] of categories) {
484
- output += ` ${name.padEnd(18)} ${scoreBar(score)}\n`;
485
- }
486
- output += '\n';
487
-
488
- // Mother Code specific
489
- if (debt.categories.motherCode) {
490
- const mc = debt.categories.motherCode;
491
- output += ` ${C.bold}Mother Code Coverage:${C.reset}\n`;
492
- output += ` Annotated: ${C.cyan}${mc.annotated}${C.reset} | Missing: ${C.yellow}${mc.missing}${C.reset} | Drifted: ${mc.drifted > 0 ? C.red : C.green}${mc.drifted}${C.reset} | Coverage: ${mc.coverage >= 80 ? C.green : mc.coverage >= 40 ? C.yellow : C.red}${mc.coverage}%${C.reset}\n\n`;
493
- }
494
-
495
- // Summary
496
- output += ` ${C.bold}Tech Debt Summary:${C.reset}\n`;
497
- output += ` Total issues: ${C.bold}${debt.stats.totalIssues}${C.reset}\n`;
498
- output += ` Auto-fixable: ${C.green}${debt.stats.fixableCount}${C.reset} ${C.gray}← run 'thuban fix' to resolve${C.reset}\n`;
499
- output += ` Manual review: ${C.yellow}${debt.stats.manualCount}${C.reset}\n`;
500
- output += ` Est. manual time: ${C.gray}${debt.stats.estimatedFixTime}${C.reset}\n`;
501
- output += ` Scan time: ${C.gray}${debt.stats.scanTime}ms${C.reset}\n\n`;
502
-
503
- // Top fixable items
504
- if (debt.fixable.length > 0) {
505
- output += ` ${C.bold}Top Auto-Fixable Items:${C.reset}\n`;
506
- const grouped = {};
507
- for (const item of debt.fixable) {
508
- const action = item.fixAction || 'unknown';
509
- if (!grouped[action]) grouped[action] = { count: 0, label: action };
510
- grouped[action].count++;
511
- }
512
- for (const [action, data] of Object.entries(grouped).sort((a, b) => b[1].count - a[1].count)) {
513
- const label = {
514
- 'inject_widget': 'Inject Mother Code DNA',
515
- 'regenerate_widget': 'Update stale annotations',
516
- 'remove_console_log': 'Remove debug console.logs',
517
- 'extract_to_env': 'Extract hardcoded secrets to .env',
518
- 'flag_for_removal': 'Remove orphan files',
519
- 'break_circular': 'Break circular dependencies',
520
- }[action] || action;
521
- output += ` ${C.green}→${C.reset} ${label}: ${C.bold}${data.count}${C.reset} items\n`;
522
- }
523
- output += `\n ${C.cyan}Run 'thuban fix [path]' to auto-fix all ${debt.stats.fixableCount} items${C.reset}\n\n`;
524
- }
525
-
526
- // Manual items (top 5)
527
- if (debt.manual.length > 0) {
528
- output += ` ${C.bold}Manual Review Required:${C.reset}\n`;
529
- for (const item of debt.manual.slice(0, 5)) {
530
- const sevColor = item.severity === 'critical' ? C.red : item.severity === 'high' ? C.red : C.yellow;
531
- output += ` ${sevColor}${(item.severity || '').toUpperCase().padEnd(8)}${C.reset} ${C.cyan}${item.file || ''}${C.reset}\n`;
532
- output += ` ${C.gray} ${item.message || item.suggestion || ''}${C.reset}\n`;
533
- }
534
- if (debt.manual.length > 5) {
535
- output += ` ${C.gray} ... and ${debt.manual.length - 5} more${C.reset}\n`;
536
- }
537
- output += '\n';
538
- }
539
-
540
- return output;
541
- }
542
-
543
- /**
544
- * Generate JSON report for dashboard / API consumption
545
- */
546
- toJSON(debt) {
547
- return {
548
- timestamp: new Date().toISOString(),
549
- scores: debt.scores,
550
- categories: Object.fromEntries(
551
- Object.entries(debt.categories).map(([key, cat]) => [key, {
552
- score: cat.score,
553
- label: cat.label,
554
- issueCount: cat.issues?.length || 0,
555
- coverage: cat.coverage,
556
- }])
557
- ),
558
- stats: debt.stats,
559
- fixable: debt.fixable.map(i => ({ file: i.file, action: i.fixAction, severity: i.severity })),
560
- manual: debt.manual.map(i => ({ file: i.file, message: i.message, severity: i.severity })),
561
- };
562
- }
563
- }
564
-
565
- module.exports = TechDebtAnalyzer;