thuban 0.3.3 → 0.3.4

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