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.
- package/dist/LICENSE +21 -0
- package/dist/README.md +185 -0
- package/dist/cli.js +2 -0
- package/dist/packages/scanner/ai-confidence-scorer.js +1 -0
- package/dist/packages/scanner/alert-manager.js +1 -0
- package/dist/packages/scanner/baseline-manager.js +1 -0
- package/dist/packages/scanner/code-scanner.js +1 -0
- package/dist/packages/scanner/codebase-passport.js +1 -0
- package/dist/packages/scanner/copy-paste-detector.js +1 -0
- package/dist/packages/scanner/dependency-graph.js +1 -0
- package/dist/packages/scanner/drift-detector.js +1 -0
- package/dist/packages/scanner/executive-report.js +1 -0
- package/dist/packages/scanner/file-collector.js +1 -0
- package/dist/packages/scanner/file-watcher.js +1 -0
- package/dist/packages/scanner/ghost-code-detector.js +1 -0
- package/dist/packages/scanner/hallucination-detector.js +1 -0
- package/dist/packages/scanner/health-checker.js +1 -0
- package/dist/packages/scanner/html-report.js +1 -0
- package/dist/packages/scanner/index.js +1 -0
- package/dist/packages/scanner/license-manager.js +1 -0
- package/dist/packages/scanner/master-health-checker.js +1 -0
- package/dist/packages/scanner/monitor-notifier.js +1 -0
- package/dist/packages/scanner/monitor-service.js +1 -0
- package/dist/packages/scanner/monitor-store.js +1 -0
- package/dist/packages/scanner/pre-commit-gate.js +1 -0
- package/dist/packages/scanner/scan-diff.js +1 -0
- package/dist/packages/scanner/scan-runner.js +1 -0
- package/dist/packages/scanner/secret-scanner.js +1 -0
- package/dist/packages/scanner/sentinel-core.js +1 -0
- package/dist/packages/scanner/sentinel-knowledge.js +1 -0
- package/dist/packages/scanner/tech-debt-analyzer.js +1 -0
- package/dist/packages/scanner/tech-debt-cost.js +1 -0
- package/dist/packages/scanner/widget-generator.js +1 -0
- package/package.json +12 -7
- package/cli.js +0 -2627
- package/packages/scanner/ai-confidence-scorer.js +0 -260
- package/packages/scanner/alert-manager.js +0 -398
- package/packages/scanner/baseline-manager.js +0 -109
- package/packages/scanner/code-scanner.js +0 -758
- package/packages/scanner/codebase-passport.js +0 -299
- package/packages/scanner/copy-paste-detector.js +0 -276
- package/packages/scanner/dependency-graph.js +0 -541
- package/packages/scanner/drift-detector.js +0 -423
- package/packages/scanner/executive-report.js +0 -774
- package/packages/scanner/file-collector.js +0 -64
- package/packages/scanner/file-watcher.js +0 -323
- package/packages/scanner/ghost-code-detector.js +0 -301
- package/packages/scanner/hallucination-detector.js +0 -822
- package/packages/scanner/health-checker.js +0 -586
- package/packages/scanner/html-report.js +0 -634
- package/packages/scanner/index.js +0 -100
- package/packages/scanner/license-manager.js +0 -331
- package/packages/scanner/master-health-checker.js +0 -513
- package/packages/scanner/monitor-notifier.js +0 -64
- package/packages/scanner/monitor-service.js +0 -103
- package/packages/scanner/monitor-store.js +0 -117
- package/packages/scanner/pre-commit-gate.js +0 -216
- package/packages/scanner/scan-diff.js +0 -50
- package/packages/scanner/scan-runner.js +0 -172
- package/packages/scanner/secret-scanner.js +0 -612
- package/packages/scanner/secret-scanner.test.js +0 -103
- package/packages/scanner/sentinel-core.js +0 -616
- package/packages/scanner/sentinel-knowledge.js +0 -322
- package/packages/scanner/tech-debt-analyzer.js +0 -583
- package/packages/scanner/tech-debt-cost.js +0 -194
- 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;
|