@puberty-labs/refuctor
Version:
AI-powered, snark-fueled technical debt cleansing suite with automatic snarky language detection that turns code cleanup into a darkly humorous financial metaphor.
1,318 lines (1,143 loc) ⢠75.5 kB
JavaScript
const { execSync } = require('child_process');
const fs = require('fs-extra');
const path = require('path');
const glob = require('glob');
const { DebtIgnoreParser } = require('./debt-ignore-parser');
const { DebtModeManager } = require('./debt-mode-manager');
const SnarkySpellHandler = require('./snarky-spell-handler');
/**
* Core debt detection engine for Refuctor
* Integrates markdownlint, cspell, npm audit, ESLint, TypeScript, and custom rules
*/
class DebtDetector {
constructor() {
// Mode-based thresholds managed by DebtModeManager (SSOT)
this.modeManager = new DebtModeManager();
this.ignoreParser = new DebtIgnoreParser();
this.mafiaMessages = [
"š“ļø Your debt has been purchased by... let's call them 'private investors'.",
"š° Congratulations! You now owe the family. VIGorish is 20% per day. Compounded.",
"š° The house always wins, but your code? Your code NEVER wins.",
"š Tony says hi. He also says pay up before he sends his nephew.",
"š *Ring ring* 'Is this about the debt?' 'What debt? We never had this conversation.'",
"š Nice development environment you got there. Shame if it suddenly... crashed.",
"š¼ Your debt collector quit. We bought the contract. Welcome to the big leagues.",
"š« We don't break legs anymore. We break build pipelines. Much more effective."
];
this.guidoMessages = [
"š¤ Guido here. You owe me big time, capisce? Your fingers might 'accidentally' forget how to type...",
"šØāš¼ *cracks knuckles* Nice coding setup you got here. Shame if something happened to it...",
"š¬ Listen here, wise guy. The Debt Collection Agency? They're AMATEURS compared to what I do.",
"š Your technical debt is so bad, even the grim reaper filed a complaint. Fix it or I fix YOU.",
"šØ I don't just break kneecaps - I break code compilation. Permanently.",
"šÆ You think P1 Critical was bad? Wait till you meet P0 'Thumb Crusher' priority.",
"šļø Your codebase is condemned. I'm here for the demolition... starting with your IDE.",
"šø The Collection Agency gave up on you. Now you deal with ME. Payment is due... in BLOOD... sugar. I'm diabetic.",
"ā” Your debt is so extreme, I had to come out of retirement. This better be worth my time.",
"š I've seen cleaner code in a dumpster fire. Actually, the dumpster fire had better documentation."
];
}
/**
* Main project scanning function
* @param {string} projectPath - Path to project root
* @param {boolean} verbose - Show detailed output
* @returns {Object} Debt report with P1-P4 categorization
*/
async scanProject(projectPath, verbose = false) {
const debtReport = {
timestamp: new Date().toISOString(),
projectPath,
totalDebt: 0,
guido: [], // Ultimate escalation - Thumb Crusher deployed
mafia: [], // Loan shark level - debt sold to family, vigorish charged
p1: [], // Critical - foreclosure imminent
p2: [], // High - repossession notice
p3: [], // Medium - liens filed
p4: [], // Low - interest accruing
summary: {},
ignoredFiles: verbose ? [] : null,
ignoredDebt: verbose ? { total: 0, files: [] } : null,
details: verbose ? {} : null,
mafiaStatus: null, // Loan shark takeover status
guidoAppearance: null, // Thumb crusher deployment
fileDebtMap: {},
topHotspots: [],
debtTrend: null
};
try {
// Load debt ignore patterns first
await this.ignoreParser.loadIgnorePatterns(projectPath);
if (verbose) {
console.log(`\nš Ignore patterns loaded: ${this.ignoreParser.getPatterns().length}`);
const customPatterns = this.ignoreParser.getPatterns().slice(6); // Skip default patterns
if (customPatterns.length > 0) {
console.log(`š« Custom ignore patterns: ${customPatterns.join(', ')}`);
}
}
// Run all debt detection methods with graceful fallbacks
const markdownDebt = await this.detectMarkdownDebt(projectPath, verbose);
const spellDebt = await this.detectSpellingDebt(projectPath);
const securityDebt = await this.detectSecurityDebt(projectPath);
const dependencyDebt = await this.detectDependencyDebt(projectPath);
// Enhanced detection methods (with fallbacks for older versions)
const eslintDebt = this.detectESLintDebt ? await this.detectESLintDebt(projectPath) : { total: 0, errors: 0, warnings: 0 };
const typescriptDebt = this.detectTypeScriptDebt ? await this.detectTypeScriptDebt(projectPath) : { total: 0, errors: [] };
const codeQualityDebt = this.detectCodeQualityDebt ? await this.detectCodeQualityDebt(projectPath, verbose) : { total: 0, consoleLogs: [], todos: [] };
const formattingDebt = this.detectFormattingDebt ? await this.detectFormattingDebt(projectPath) : { total: 0, issues: [] };
// Store ignored file information if verbose
if (verbose) {
debtReport.ignoredFiles = markdownDebt.ignoredFiles || [];
debtReport.ignoredDebt = markdownDebt.ignoredDebt || { total: 0, files: [] };
}
// Categorize and merge results (mode-aware)
await this.categorizeDebt(debtReport, 'markdown', markdownDebt, projectPath);
await this.categorizeDebt(debtReport, 'spelling', spellDebt, projectPath);
await this.categorizeDebt(debtReport, 'security', securityDebt, projectPath);
await this.categorizeDebt(debtReport, 'dependencies', dependencyDebt, projectPath);
await this.categorizeDebt(debtReport, 'eslint', eslintDebt, projectPath);
await this.categorizeDebt(debtReport, 'typescript', typescriptDebt, projectPath);
await this.categorizeDebt(debtReport, 'code-quality', codeQualityDebt, projectPath);
await this.categorizeDebt(debtReport, 'formatting', formattingDebt, projectPath);
// Calculate totals - count actual issues, not categories
// DEBT IGNORE RESPECT: Subtract ignored debt from total
const rawTotalDebt = markdownDebt.total + spellDebt.total + securityDebt.total + dependencyDebt.total +
eslintDebt.total + typescriptDebt.total + codeQualityDebt.total + formattingDebt.total;
const totalIgnoredDebt = (markdownDebt.ignoredDebt?.total || 0) +
(spellDebt.ignoredDebt?.total || 0) +
(eslintDebt.ignoredDebt?.total || 0) +
(typescriptDebt.ignoredDebt?.total || 0) +
(codeQualityDebt.ignoredDebt?.total || 0) +
(formattingDebt.ignoredDebt?.total || 0);
debtReport.totalDebt = Math.max(0, rawTotalDebt - totalIgnoredDebt);
debtReport.totalIgnoredDebt = totalIgnoredDebt;
// Check for mafia takeover and Guido escalation
await this.checkMafiaStatus(debtReport, projectPath);
await this.checkForGuidoDeployment(debtReport, projectPath);
// Generate summary
debtReport.summary = {
markdown: markdownDebt.total,
spelling: spellDebt.total,
security: securityDebt.total,
dependencies: dependencyDebt.total,
eslint: eslintDebt.total,
typescript: typescriptDebt.total,
codeQuality: codeQualityDebt.total,
formatting: formattingDebt.total,
snarkyProcessed: spellDebt.snarkyProcessed || false,
snarkyAdded: spellDebt.snarkyAdded || 0,
debtLevel: await this.calculateDebtLevel(debtReport, projectPath),
p1: debtReport.p1.length,
p2: debtReport.p2.length,
p3: debtReport.p3.length,
p4: debtReport.p4.length,
total: debtReport.totalDebt,
totalIgnored: debtReport.totalIgnoredDebt || 0,
rawTotal: rawTotalDebt
};
// Generate heat map data for dashboard visualization
debtReport.fileDebtMap = this.generateFileDebtMap(debtReport, {
markdown: markdownDebt,
spelling: spellDebt,
security: securityDebt,
dependencies: dependencyDebt,
eslint: eslintDebt,
typescript: typescriptDebt,
codeQuality: codeQualityDebt,
formatting: formattingDebt
});
debtReport.topHotspots = this.generateDebtHotspots(debtReport.fileDebtMap);
debtReport.debtTrend = this.calculateDebtTrend(debtReport);
// Show ignored debt summary if verbose
if (verbose && debtReport.ignoredDebt && debtReport.ignoredDebt.total > 0) {
console.log(` Ignored files: ${debtReport.ignoredFiles.join(', ')}`);
}
if (verbose) {
debtReport.details = {
markdown: markdownDebt,
spelling: spellDebt,
security: securityDebt,
dependencies: dependencyDebt,
eslint: eslintDebt,
typescript: typescriptDebt,
codeQuality: codeQualityDebt,
formatting: formattingDebt
};
}
return debtReport;
} catch (error) {
console.warn(`Warning during debt detection: ${error.message}`);
// Continue with partial results, ensure totalDebt is defined
debtReport.totalDebt = 0; // Will be 0 since all individual debt totals will be 0 in error case
debtReport.summary = {
markdown: 0,
spelling: 0,
security: 0,
dependencies: 0,
eslint: 0,
typescript: 0,
codeQuality: 0,
formatting: 0,
snarkyProcessed: false,
snarkyAdded: 0,
debtLevel: 'unknown'
};
return debtReport;
}
}
/**
* Detect markdown linting issues
*/
async detectMarkdownDebt(projectPath, verbose = false) {
const debt = { total: 0, issues: [], files: [], ignoredFiles: [], ignoredDebt: { total: 0, files: [] } };
const allMarkdownFiles = glob.sync('**/*.{md,mdc}', {
cwd: projectPath,
ignore: ['node_modules/**', '.git/**'] // Basic ignore only
});
// Separate ignored and non-ignored files
const markdownFiles = [];
const ignoredFiles = [];
for (const file of allMarkdownFiles) {
if (this.ignoreParser.shouldIgnore(file)) {
ignoredFiles.push(file);
} else {
markdownFiles.push(file);
}
}
debt.ignoredFiles = ignoredFiles;
if (verbose && ignoredFiles.length > 0) {
console.log(`\nš« Ignoring ${ignoredFiles.length} markdown files: ${ignoredFiles.join(', ')}`);
}
if (verbose && markdownFiles.length > 0) {
console.log(`š Scanning ${markdownFiles.length} markdown files: ${markdownFiles.join(', ')}`);
}
// Check ignored files for debt (for reporting purposes)
if (verbose && ignoredFiles.length > 0) {
// š§ FIX: Skip ignored files in massive directories to prevent hangs
const massiveDirectoryPatterns = [
/node_modules/,
/\.git/,
/build/,
/dist/,
/coverage/,
/\.cache/
];
const processableIgnoredFiles = ignoredFiles.filter(file => {
return !massiveDirectoryPatterns.some(pattern => pattern.test(file));
});
// Limit to reasonable number of files to prevent performance issues
const maxIgnoredFilesToProcess = 20;
const filesToProcess = processableIgnoredFiles.slice(0, maxIgnoredFilesToProcess);
if (processableIgnoredFiles.length > maxIgnoredFilesToProcess) {
console.log(` š Processing ${maxIgnoredFilesToProcess} of ${processableIgnoredFiles.length} ignored files for reporting (skipped ${processableIgnoredFiles.length - maxIgnoredFilesToProcess} files)`);
}
for (const file of filesToProcess) {
try {
const cmd = `npx --yes markdownlint-cli "${file}"`;
execSync(cmd, {
cwd: projectPath,
encoding: 'utf8',
stdio: 'pipe'
});
// No issues found in this ignored file
} catch (error) {
if (error.status === 1 && (error.stdout || error.stderr)) {
// markdownlint sends output to stderr, not stdout
const output = error.stderr || error.stdout;
const lines = output.trim().split('\n');
debt.ignoredDebt.total += lines.length;
if (!debt.ignoredDebt.files.includes(file)) {
debt.ignoredDebt.files.push(file);
}
console.log(` š« ${file} - ${lines.length} issues (ignored)`);
}
}
}
// Report skipped massive directories
const skippedMassiveFiles = ignoredFiles.filter(file => {
return massiveDirectoryPatterns.some(pattern => pattern.test(file));
});
if (skippedMassiveFiles.length > 0) {
console.log(` šāāļø Skipped ${skippedMassiveFiles.length} files in massive directories (node_modules, build, etc.) for performance`);
}
}
if (markdownFiles.length === 0) {
return debt;
}
// Run markdownlint on non-ignored files
const cmd = `npx --yes markdownlint-cli "${markdownFiles.join('" "')}"`;
try {
const result = execSync(cmd, {
cwd: projectPath,
encoding: 'utf8',
stdio: 'pipe'
});
// If we get here, no linting errors (markdownlint exits 0 for no errors)
debt.total = 0;
} catch (error) {
// markdownlint exits with code 1 when issues found
if (error.status === 1 && (error.stdout || error.stderr)) {
// markdownlint sends output to stderr, not stdout
const output = error.stderr || error.stdout;
const lines = output.trim().split('\n');
debt.total = lines.length;
debt.issues = lines.map(line => {
const match = line.match(/^(.+):(\d+):?\d*\s+(.+)\s+(.+)$/);
if (match) {
return {
file: match[1],
line: parseInt(match[2]),
rule: match[4],
message: match[3]
};
}
return { raw: line };
});
debt.files = [...new Set(debt.issues.map(i => i.file).filter(Boolean))];
} else {
throw error;
}
}
return debt;
}
/**
* Detect spelling issues with integrated snarky intelligence
*/
async detectSpellingDebt(projectPath) {
const debt = { total: 0, issues: [], files: [], snarkyProcessed: false, snarkyAdded: 0, ignoredFiles: [], ignoredDebt: { total: 0, files: [] } };
try {
// Check if cspell config exists
const configFiles = ['cspell.json', '.cspell.json', 'cspell.config.js'];
const hasConfig = configFiles.some(file => fs.existsSync(path.join(projectPath, file)));
// Get all files that could have spelling issues
const allSpellFiles = glob.sync('**/*.{md,js,ts,json,mdc}', {
cwd: projectPath,
ignore: ['node_modules/**', '.git/**'] // Basic ignore only
});
// Separate ignored and non-ignored files (like markdown detection does)
const spellFiles = [];
const ignoredFiles = [];
for (const file of allSpellFiles) {
if (this.ignoreParser.shouldIgnore(file)) {
ignoredFiles.push(file);
} else {
spellFiles.push(file);
}
}
debt.ignoredFiles = ignoredFiles;
// Check ignored files for debt (for reporting purposes)
if (ignoredFiles.length > 0) {
// š§ FIX: Skip ignored files in massive directories to prevent hangs
const massiveDirectoryPatterns = [
/node_modules/,
/\.git/,
/build/,
/dist/,
/coverage/,
/\.cache/
];
const processableIgnoredFiles = ignoredFiles.filter(file => {
return !massiveDirectoryPatterns.some(pattern => pattern.test(file));
});
// Limit to reasonable number of files to prevent performance issues
const maxIgnoredFilesToProcess = 15;
const filesToProcess = processableIgnoredFiles.slice(0, maxIgnoredFilesToProcess);
for (const file of filesToProcess) {
try {
const cmd = `npx --yes cspell "${file}" --no-progress --no-summary`;
const result = execSync(cmd, {
cwd: projectPath,
encoding: 'utf8',
stdio: 'pipe'
});
// No issues found in this ignored file
} catch (error) {
if (error.status === 1 && error.stdout) {
const lines = error.stdout.trim().split('\n').filter(line => line.includes('Unknown word'));
debt.ignoredDebt.total += lines.length;
if (!debt.ignoredDebt.files.includes(file)) {
debt.ignoredDebt.files.push(file);
}
}
}
}
}
if (spellFiles.length === 0) {
return debt;
}
// Run cspell only on non-ignored files
const cmd = `npx --yes cspell "${spellFiles.join('" "')}" --no-progress --no-summary`;
const result = execSync(cmd, {
cwd: projectPath,
encoding: 'utf8',
stdio: 'pipe'
});
// If we get here, no spelling errors
debt.total = 0;
} catch (error) {
// cspell exits with code 1 when issues found
if (error.status === 1 && error.stdout) {
const lines = error.stdout.trim().split('\n').filter(line => line.includes('Unknown word'));
const rawIssues = lines.map(line => {
const match = line.match(/^(.+):(\d+):(\d+)\s+-\s+Unknown word \((.+)\)/);
if (match) {
return {
file: match[1],
line: parseInt(match[2]),
column: parseInt(match[3]),
word: match[4]
};
}
return { raw: line };
});
// šÆ AUTOMATIC SNARKY INTELLIGENCE ACTIVATED!
if (rawIssues.length > 0) {
try {
const snarkyHandler = new SnarkySpellHandler();
const analysis = await snarkyHandler.analyzeSpellingIssues(projectPath, rawIssues);
// Auto-add obvious snarky terms to dictionary
if (analysis.likelySnarky.length > 0) {
const dictResult = await snarkyHandler.updateProjectDictionary(
projectPath,
analysis.likelySnarky.map(s => s.word)
);
debt.snarkyAdded = dictResult.wordsAdded;
debt.snarkyProcessed = true;
}
// Only report definite typos and uncertain cases as actual debt
const actualProblems = [...analysis.definiteTypos, ...analysis.unsure];
debt.issues = actualProblems;
debt.total = actualProblems.length;
debt.files = [...new Set(actualProblems.map(i => i.file).filter(Boolean))];
if (debt.snarkyAdded > 0) {
console.log(`ā
Reduced spelling debt from ${rawIssues.length} to ${debt.total} (${debt.snarkyAdded} snarky terms whitelisted)`);
}
} catch (snarkyError) {
// Fallback to original behavior if snarky analysis fails
debt.issues = rawIssues;
debt.total = rawIssues.length;
debt.files = [...new Set(rawIssues.map(i => i.file).filter(Boolean))];
}
}
} else if (!error.stdout || error.stdout.trim() === '') {
// No output usually means no issues
debt.total = 0;
} else {
throw error;
}
}
return debt;
}
/**
* Detect security vulnerabilities
*/
async detectSecurityDebt(projectPath) {
const debt = { total: 0, issues: [], severity: {} };
try {
// Check if package.json exists
if (!fs.existsSync(path.join(projectPath, 'package.json'))) {
return debt;
}
// Run npm audit
const cmd = 'npm audit --json';
const result = execSync(cmd, {
cwd: projectPath,
encoding: 'utf8',
stdio: 'pipe'
});
const auditData = JSON.parse(result);
if (auditData.vulnerabilities) {
Object.entries(auditData.vulnerabilities).forEach(([pkg, vuln]) => {
debt.issues.push({
package: pkg,
severity: vuln.severity,
title: vuln.title,
range: vuln.range
});
debt.severity[vuln.severity] = (debt.severity[vuln.severity] || 0) + 1;
});
debt.total = debt.issues.length;
}
} catch (error) {
// npm audit can exit with non-zero for vulnerabilities found
if (error.stdout) {
try {
const auditData = JSON.parse(error.stdout);
if (auditData.vulnerabilities) {
Object.entries(auditData.vulnerabilities).forEach(([pkg, vuln]) => {
debt.issues.push({
package: pkg,
severity: vuln.severity,
title: vuln.title || 'Security vulnerability',
range: vuln.range
});
debt.severity[vuln.severity] = (debt.severity[vuln.severity] || 0) + 1;
});
debt.total = debt.issues.length;
}
} catch (parseError) {
// If we can't parse JSON, skip security scan for now
debt.total = 0;
}
}
}
return debt;
}
/**
* Detect dependency issues
*/
async detectDependencyDebt(projectPath) {
const debt = { total: 0, unused: [], outdated: [] };
try {
// Check if package.json exists
const packagePath = path.join(projectPath, 'package.json');
if (!fs.existsSync(packagePath)) {
return debt;
}
// For now, we'll implement basic dependency checking
// Future: integrate with tools like depcheck or npm-check
const packageJson = await fs.readJson(packagePath);
const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
// Simple heuristic: if package.json has deps but no node_modules, flag it
const hasNodeModules = fs.existsSync(path.join(projectPath, 'node_modules'));
if (Object.keys(dependencies).length > 0 && !hasNodeModules) {
debt.unused.push('Dependencies defined but node_modules missing - run npm install');
debt.total = 1;
}
} catch (error) {
// Skip dependency analysis if it fails
debt.total = 0;
}
return debt;
}
/**
* Detect ESLint issues (JavaScript/TypeScript code quality)
*/
async detectESLintDebt(projectPath) {
const debt = { total: 0, errors: 0, warnings: 0, issues: [], files: [], ignoredFiles: [], ignoredDebt: { total: 0, files: [] } };
try {
// Check if ESLint config exists
const configFiles = ['.eslintrc.js', '.eslintrc.json', '.eslintrc.yml', 'eslint.config.js'];
const hasConfig = configFiles.some(file => fs.existsSync(path.join(projectPath, file)));
// Get all JS/TS files with basic ignore only (like other detection methods)
const allCodeFiles = glob.sync('**/*.{js,ts,jsx,tsx}', {
cwd: projectPath,
ignore: ['node_modules/**', '.git/**'] // Basic ignore only
});
// Separate ignored and non-ignored files (like markdown/spelling detection)
const codeFiles = [];
const ignoredFiles = [];
for (const file of allCodeFiles) {
if (this.ignoreParser.shouldIgnore(file)) {
ignoredFiles.push(file);
} else {
codeFiles.push(file);
}
}
debt.ignoredFiles = ignoredFiles;
if (codeFiles.length === 0) {
return debt; // No code files to lint after filtering
}
// š§ FIX: Process files in smaller batches to avoid command line length limits
const BATCH_SIZE = 10; // Process 10 files at a time to stay within system limits
const fileBatches = [];
for (let i = 0; i < codeFiles.length; i += BATCH_SIZE) {
fileBatches.push(codeFiles.slice(i, i + BATCH_SIZE));
}
// Process each batch of files
for (const batch of fileBatches) {
// Run ESLint on batch of files (respects .debtignore)
// Use --max-warnings 0 to ensure warnings trigger exit code 1 for proper error handling
const cmd = `npx --yes eslint ${batch.join(' ')} --format json --max-warnings 0`;
let eslintOutput;
try {
eslintOutput = execSync(cmd, {
cwd: projectPath,
encoding: 'utf8',
stdio: 'pipe',
maxBuffer: 10 * 1024 * 1024 // 10MB buffer to handle large ESLint output
});
} catch (error) {
// ESLint found issues (exit code 1) - get output from error.stdout
if (error.status === 1 && error.stdout) {
eslintOutput = error.stdout;
} else {
// Real error (missing ESLint, config issues, etc.) - skip this batch
console.warn(`ESLint batch failed: ${error.message}`);
continue;
}
}
// Parse ESLint JSON results for this batch (whether from success or error.stdout)
if (eslintOutput) {
const eslintResults = JSON.parse(eslintOutput);
for (const fileResult of eslintResults) {
if (fileResult.messages.length > 0) {
debt.files.push(fileResult.filePath);
for (const message of fileResult.messages) {
debt.issues.push({
file: fileResult.filePath,
line: message.line,
column: message.column,
severity: message.severity, // 1 = warning, 2 = error
rule: message.ruleId,
message: message.message
});
if (message.severity === 2) {
debt.errors++;
} else {
debt.warnings++;
}
}
}
}
}
}
debt.total = debt.errors + debt.warnings;
} catch (error) {
// ESLint not available, config issues, or other real errors
console.warn(`ESLint detection failed: ${error.message}`);
debt.total = 0;
}
return debt;
}
/**
* Detect TypeScript compilation errors
*/
async detectTypeScriptDebt(projectPath) {
const debt = { total: 0, errors: [], files: [] };
try {
// Check if TypeScript config exists
if (!fs.existsSync(path.join(projectPath, 'tsconfig.json'))) {
return debt; // No TypeScript project
}
// Check for TS files
const tsFiles = glob.sync('**/*.{ts,tsx}', {
cwd: projectPath,
ignore: ['node_modules/**', '.git/**', 'dist/**', 'build/**']
});
if (tsFiles.length === 0) {
return debt; // No TypeScript files
}
// Run TypeScript compiler check
const cmd = 'npx --yes tsc --noEmit --skipLibCheck';
const result = execSync(cmd, {
cwd: projectPath,
encoding: 'utf8',
stdio: 'pipe'
});
// If we get here, no TS errors
debt.total = 0;
} catch (error) {
// TypeScript errors found
if (error.stdout) {
const lines = error.stdout.trim().split('\n').filter(line => line.includes('error TS'));
debt.total = lines.length;
debt.errors = lines.map(line => {
const match = line.match(/^(.+)\((\d+),(\d+)\):\s+error\s+(TS\d+):\s+(.+)$/);
if (match) {
return {
file: match[1],
line: parseInt(match[2]),
column: parseInt(match[3]),
code: match[4],
message: match[5]
};
}
return { raw: line };
});
debt.files = [...new Set(debt.errors.map(e => e.file).filter(Boolean))];
}
}
return debt;
}
/**
* Detect code quality issues (console.logs, TODOs, dead code)
*/
async detectCodeQualityDebt(projectPath, verbose = false) {
const debt = {
total: 0,
consoleLogs: [],
todos: [],
deadCode: [],
files: [],
ignoredFiles: [],
ignoredDebt: { total: 0, files: [] },
smartConsoleProcessed: false,
debugConsoleLogsFound: 0,
interfaceConsoleLogsIgnored: 0
};
try {
// Find all code files (properly exclude ALL node_modules)
const allCodeFiles = glob.sync('**/*.{js,ts,jsx,tsx,vue}', {
cwd: projectPath,
ignore: ['**/node_modules/**', '.git/**', 'dist/**', 'build/**', 'coverage/**']
});
// Separate ignored and non-ignored files (like markdown detection does)
const codeFiles = [];
const ignoredFiles = [];
for (const file of allCodeFiles) {
if (this.ignoreParser.shouldIgnore(file)) {
ignoredFiles.push(file);
} else {
codeFiles.push(file);
}
}
debt.ignoredFiles = ignoredFiles;
// Add verbose logging like markdown detection
if (verbose && ignoredFiles.length > 0) {
console.log(`\nš« Ignoring ${ignoredFiles.length} code files: ${ignoredFiles.join(', ')}`);
}
if (verbose && codeFiles.length > 0) {
console.log(`š» Scanning ${codeFiles.length} code files for quality issues: ${codeFiles.join(', ')}`);
}
let totalDebugConsoles = 0;
let totalInterfaceConsoles = 0;
// Process only non-ignored files
for (const file of codeFiles) {
const filePath = path.join(projectPath, file);
const content = await fs.readFile(filePath, 'utf8');
const lines = content.split('\n');
let fileHasIssues = false;
// Smart console.log detection with context analysis
lines.forEach((line, index) => {
if (line.includes('console.log') || line.includes('console.warn') || line.includes('console.error')) {
const consoleStatement = {
file: file,
line: index + 1,
content: line.trim(),
context: this.getLineContext(lines, index, 3) // Get 3 lines of context
};
// š§ SMART DETECTION: Is this a debug statement or intentional UI output?
if (this.isActualDebugStatement(consoleStatement, file, content)) {
debt.consoleLogs.push(consoleStatement);
fileHasIssues = true;
totalDebugConsoles++;
} else {
// This is intentional UI output - ignore it
totalInterfaceConsoles++;
if (verbose) {
console.log(` ā¹ļø Interface console.log detected (ignored): ${consoleStatement.content}`);
}
}
}
// Smart TODO/FIXME/HACK detection with context analysis
if (line.includes('TODO') || line.includes('FIXME') || line.includes('HACK')) {
const todoStatement = {
file: file,
line: index + 1,
content: line.trim(),
context: this.getLineContext(lines, index, 3)
};
// š§ SMART TODO DETECTION: Is this an actual TODO comment or just documentation/code about TODOs?
if (this.isActualTodoComment(todoStatement, file, content)) {
debt.todos.push(todoStatement);
fileHasIssues = true;
}
}
});
if (fileHasIssues) {
debt.files.push(file);
}
}
debt.total = debt.consoleLogs.length + debt.todos.length + debt.deadCode.length;
debt.smartConsoleProcessed = true;
debt.debugConsoleLogsFound = totalDebugConsoles;
debt.interfaceConsoleLogsIgnored = totalInterfaceConsoles;
if (verbose && totalInterfaceConsoles > 0) {
console.log(`ā
Smart console.log detection: ${totalDebugConsoles} debug statements (debt), ${totalInterfaceConsoles} UI outputs (ignored)`);
}
} catch (error) {
// Skip code quality analysis if it fails
debt.total = 0;
}
return debt;
}
/**
* š§ SMART CONSOLE.LOG DETECTION: Determine if a console statement is debug code (debt) or intentional UI output
* @param {Object} consoleStatement - Console statement with file, line, content, and context
* @param {string} file - File path for additional context
* @param {string} fileContent - Full file content for broader analysis
* @returns {boolean} - True if this is a debug statement (should count as debt)
*/
isActualDebugStatement(consoleStatement, file, fileContent) {
const { content, context, line } = consoleStatement;
const lowerContent = content.toLowerCase();
// 1. DEFINITE UI OUTPUT PATTERNS (NOT debt)
// Setup wizards, CLI output, user-facing messages
if (this.isDefiniteUIOutput(content, file, context)) {
return false; // Not debt - intentional UI
}
// 2. DEFINITE DEBUG PATTERNS (IS debt)
if (this.isDefiniteDebugStatement(content, context)) {
return true; // Definitely debt
}
// 3. CONTEXT ANALYSIS - Check surrounding code
if (this.isDebugContext(context, file)) {
return true; // Likely debug code
}
// 4. FILE TYPE ANALYSIS
if (this.isUIFile(file) && this.hasUIPatterns(content)) {
return false; // UI file with UI patterns - not debt
}
// 5. MESSAGE CONTENT ANALYSIS
if (this.hasDebugMessagePatterns(content)) {
return true; // Debug-style message content
}
// 6. DEFAULT: If uncertain, lean towards NOT counting as debt
// Better to miss some debug statements than flag legitimate UI output
return false;
}
/**
* Check if this is definitely intentional UI output
*/
isDefiniteUIOutput(content, file, context) {
const lowerContent = content.toLowerCase();
// CLI/Setup wizard patterns
const uiPatterns = [
'refuctor',
'debt collector',
'setup wizard',
'installation',
'scanning',
'processing',
'analyzing',
'welcome to',
'configuration',
'initializing',
'creating',
'installing',
'detected',
'found',
'completed',
'success',
'error:',
'warning:',
'step',
'press any key',
'choose',
'select',
'enter',
'would you like',
'do you want'
];
for (const pattern of uiPatterns) {
if (lowerContent.includes(pattern)) {
return true;
}
}
// CLI files are usually UI output
if (file.includes('cli') || file.includes('setup') || file.includes('wizard')) {
return true;
}
// Professional error messages
if (content.includes('Error:') || content.includes('Warning:') || content.includes('Info:')) {
return true;
}
// Formatted output with emojis or special characters
const emojiPattern = /[\u{1F4CB}\u{1F6AB}\u{1F4BB}\u{2705}\u{26A0}\u{1F3AF}\u{1F4DD}]/u;
if (emojiPattern.test(content)) {
return true;
}
return false;
}
/**
* Check if this is definitely a debug statement
*/
isDefiniteDebugStatement(content, context) {
const lowerContent = content.toLowerCase();
// Debug keywords
const debugPatterns = [
'debug',
'testing',
'temp',
'todo',
'fixme',
'hack',
'wtf',
'xxx',
'remove this',
'delete this',
'placeholder',
'test123',
'hello world'
];
for (const pattern of debugPatterns) {
if (lowerContent.includes(pattern)) {
return true;
}
}
// Random values or test data
if (/console\.log\(['"`]\w{1,3}['"`]\)/.test(content)) { // Single words like 'hi', 'test'
return true;
}
// Variable dumps without context
if (/console\.log\(\w+\)$/.test(content.trim())) { // Just logging a variable
return true;
}
// Multiple console.logs in sequence (debugging pattern)
const contextLines = context.before.concat(context.after);
const nearbyConsoleLogs = contextLines.filter(line =>
line.includes('console.log') || line.includes('console.warn') || line.includes('console.error')
).length;
if (nearbyConsoleLogs >= 2) { // Multiple console statements nearby
return true;
}
return false;
}
/**
* Analyze context lines for debug patterns
*/
isDebugContext(context, file) {
const allContextLines = context.before.concat(context.after);
const contextText = allContextLines.join(' ').toLowerCase();
// Debug function or method names
const debugContextPatterns = [
'debug',
'test',
'temp',
'experiment',
'try',
'check',
'validate'
];
for (const pattern of debugContextPatterns) {
if (contextText.includes(pattern)) {
return true;
}
}
// Comments indicating debug code
if (contextText.includes('//') && (
contextText.includes('debug') ||
contextText.includes('test') ||
contextText.includes('remove') ||
contextText.includes('temp')
)) {
return true;
}
return false;
}
/**
* Check if this is a UI-focused file
*/
isUIFile(file) {
const uiFilePatterns = [
'cli',
'setup',
'wizard',
'interface',
'ui',
'dashboard',
'server',
'app',
'main'
];
const lowerFile = file.toLowerCase();
return uiFilePatterns.some(pattern => lowerFile.includes(pattern));
}
/**
* Check if content has UI patterns
*/
hasUIPatterns(content) {
// Professional message formatting
return content.includes('`') || // Template literals
content.includes('${') || // String interpolation
/console\.(log|warn|error)\(['"`][A-Z]/.test(content) || // Capitalized messages
content.length > 80; // Long descriptive messages
}
/**
* Check for debug-style message patterns
*/
hasDebugMessagePatterns(content) {
// Short, cryptic messages
if (content.length < 30 && /console\.log\(['"`]\w{1,10}['"`]\)/.test(content)) {
return true;
}
// Variable names or values
if (/console\.log\(\w+[,\s]*\w*\)/.test(content)) {
return true;
}
return false;
}
/**
* Get context lines around a specific line
*/
getLineContext(lines, lineIndex, contextSize = 3) {
const start = Math.max(0, lineIndex - contextSize);
const end = Math.min(lines.length, lineIndex + contextSize + 1);
return {
before: lines.slice(start, lineIndex),
current: lines[lineIndex],
after: lines.slice(lineIndex + 1, end)
};
}
/**
* Detect formatting and style issues
*/
async detectFormattingDebt(projectPath) {
const debt = { total: 0, issues: [], files: [] };
try {
// Check if Prettier config exists
const prettierConfigs = ['.prettierrc', '.prettierrc.json', '.prettierrc.js', 'prettier.config.js'];
const hasPrettierConfig = prettierConfigs.some(file => fs.existsSync(path.join(projectPath, file)));
if (!hasPrettierConfig) {
return debt; // No Prettier config, skip formatting checks
}
// Find files to check
const codeFiles = glob.sync('**/*.{js,ts,jsx,tsx,json,css,scss,md}', {
cwd: projectPath,
ignore: ['node_modules/**', '.git/**', 'dist/**', 'build/**']
});
if (codeFiles.length === 0) {
return debt;
}
// Check formatting with Prettier
const cmd = `npx --yes prettier --check ${codeFiles.join(' ')}`;
const result = execSync(cmd, {
cwd: projectPath,
encoding: 'utf8',
stdio: 'pipe'
});
// If we get here, all files are properly formatted
debt.total = 0;
} catch (error) {
// Prettier found formatting issues
if (error.stdout) {
const lines = error.stdout.trim().split('\n').filter(line => line.trim());
debt.total = lines.length;
debt.files = lines;
debt.issues = lines.map(file => ({
file: file,
type: 'formatting',
message: 'File needs formatting'
}));
}
}
return debt;
}
/**
* Categorize debt into Mafia/Guido/P1-P4 priorities
*/
async categorizeDebt(debtReport, category, debtData, projectPath) {
const { total, issues = [], severity = {}, errors = 0, warnings = 0, consoleLogs = [], todos = [] } = debtData;
if (total === 0) return;
// Get mode-specific thresholds and messages
const thresholds = await this.modeManager.getThresholds(projectPath);
const messages = await this.modeManager.getMessages(projectPath);
const currentMode = thresholds.mode;
const modeConfig = this.modeManager.getModeConfig(currentMode);
// Mode-aware debt level classification (replaces rigid Guido/Mafia)
if (category === 'markdown' && total >= thresholds.guido.markdownWarnings) {
const message = modeConfig.emoji + ' ' + modeConfig.name + `: "${messages.markdown}" (${total} markdown issues)`;
debtReport.guido.push(message);
} else if (category === 'spelling' && total >= thresholds.guido.spellErrors) {
const message = modeConfig.emoji + ' ' + modeConfig.name + `: "${messages.spelling}" (${total} spelling issues)`;
debtReport.guido.push(message);
} else if (category === 'security' && severity.critical >= thresholds.guido.securityCritical) {
const message = modeConfig.emoji + ' ' + modeConfig.name + `: "${messages.security}" (${severity.critical} security issues)`;
debtReport.guido.push(message);
} else if (category === 'eslint' && errors >= thresholds.guido.eslintErrors) {
const message = modeConfig.emoji + ' ' + modeConfig.name + `: "Code quality needs attention" (${errors} ESLint errors)`;
debtReport.guido.push(message);
} else if (category === 'typescript' && total >= thresholds.guido.tsErrors) {
const message = modeConfig.emoji + ' ' + modeConfig.name + `: "Type safety review needed" (${total} TypeScript errors)`;
debtReport.guido.push(message);
} else if (category === 'code-quality' && consoleLogs && consoleLogs.length >= thresholds.guido.consoleLogs) {
const message = modeConfig.emoji + ' ' + modeConfig.name + `: "${messages.console}" (${consoleLogs.length} console.log statements)`;
debtReport.guido.push(message);
} else if (category === 'code-quality' && todos && todos.length >= thresholds.guido.todos) {
const message = modeConfig.emoji + ' ' + modeConfig.name + `: "Task tracking in progress" (${todos.length} TODO comments)`;
debtReport.guido.push(message);
}
// Mafia Level (LOAN SHARK TAKEOVER) - mode-aware
else if (category === 'markdown' && total >= thresholds.mafia.markdownWarnings) {
const message = currentMode === 'DEV_CREW' ?
`${total} markdown issues - š„ Dev Crew: "Documentation refinement needed"` :
`${total} markdown errors - š“ļø The Family owns this debt now. VIGorish starts today.`;
debtReport.mafia.push(message);
} else if (category === 'spelling' && total >= thresholds.mafia.spellErrors) {
const message = currentMode === 'DEV_CREW' ?
`${total} spelling issues - š„ Dev Crew: "Dictionary updates recommended"` :
`${total} spelling errors - š° Tony's dictionary says you owe us. With interest.`;
debtReport.mafia.push(message);
} else if (category === 'security' && severity.critical >= thresholds.mafia.securityCritical) {
const message = currentMode === 'DEV_CREW' ?
`${severity.critical} security issues - š„ Dev Crew: "Security review in progress"` :
`${severity.critical} critical security holes - š Nice firewall. Shame if it 'malfunctioned'.`;
debtReport.mafia.push(message);
} else if (category === 'eslint' && errors >= thresholds.mafia.eslintErrors) {
const message = currentMode === 'DEV_CREW' ?
`${errors} ESLint errors - š„ Dev Crew: "Code quality improvements needed"` :
`${errors} ESLint errors - š“ļø Your linter quit. We bought the contract. Fix it OR ELSE.`;
debtReport.mafia.push(message);
} else if (category === 'typescript' && total >= thresholds.mafia.tsErrors) {
const message = currentMode === 'DEV_CREW' ?
`${total} TypeScript errors - š„ Dev Crew: "Type safety improvements needed"` :
`${total} TypeScript errors - Your types are so wrong, we're charging interest on EACH ONE.`;
debtReport.mafia.push(message);
} else if (category === 'code-quality' && consoleLogs && consoleLogs.length >= thresholds.mafia.consoleLogs) {
const message = currentMode === 'DEV_CREW' ?
`${consoleLogs.length} console.log statements - š„ Dev Crew: "Debug logging cleanup scheduled"` :
`${consoleLogs.length} console.log statements - š Nice debug logs. Shame if they... disappeared.`;
debtReport.mafia.push(message);
} else if (category === 'code-quality' && todos && todos.length >= thresholds.mafia.todos) {
const message = currentMode === 'DEV_CREW' ?
`${todos.length} TODOs - š„ Dev Crew: "Task completion in progress"` :
`${todos.length} TODOs - š° The Family doesn't do TODOs. We do DONE or DEAD.`;
debtReport.mafia.push(message);
}
// P1 Critical thresholds - mode-aware
else if (category === 'markdown' && total >= thresholds.p1.markdownWarnings) {
const message = currentMode === 'DEV_CREW' ?
`${total} markdown issues - š„ Dev Crew: "Documentation formatting needs attention"` :
`${total} markdown linting errors - This is fucking embarrassing. Fix it NOW.`;
debtReport.p1.push(message);
} else if (category === 'spelling' && total >= thresholds.p1.spellErrors) {
const message = currentMode === 'DEV_CREW' ?
`${total} spelling issues - š„ Dev Crew: "Terminology review needed"` :
`${total} spelling errors - Your spell checker filed for bankruptcy.`;
debtReport.p1.push(message);
} else if (category === 'security' && (severity.critical > 0 || severity.high >= thresholds.p1.securityHigh)) {
const message = currentMode === 'DEV_CREW' ?
`${severity.critical || 0} critical + ${severity.high || 0} high security issues - š„ Dev Crew: "Security review scheduled"` :
`${severity.critical || 0} critical + ${severity.high || 0} high security vulnerabilities - Call the cyber police.`;
debtReport.p1.push(message);
} else if (category === 'eslint' && errors >= thresholds.p1.eslintErrors) {
const message = currentMode === 'DEV_CREW' ?
`${errors} ESLint errors - š„ Dev Crew: "Code quality improvements in progress"` :
`${errors} ESLint errors - Your code is in FORECLOSURE. Fix it before we repossess your IDE.`;
debtReport.p1.push(message);
} else if (category === 'typescript' && total >= thresholds.p1.tsErrors) {
const message = currentMode === 'DEV_CREW' ?
`${total} TypeScript errors - š„ Dev Crew: "Type checking improvements needed"` :
`${total} TypeScript errors - Your types are so fucked, TypeScript is considering therapy.`;
debtReport.p1.push(message);
} else if (category === 'code-quality' && consoleLogs && consoleLogs.length >= thresholds.p1.consoleLogs) {
const message = currentMode === 'DEV_CREW' ?
`${consoleLogs.length} console.log statements - š„ Dev Crew: "Debug cleanup on roadmap"` :
`${consoleLogs.length} console.log statements - This is NOT production debugging. Clean this shit up!`;
debtReport.p1.push(message);
} else if (category === 'code-quality' && todos && todos.length >= thresholds.p1.todos) {
const message = currentMode === 'DEV_CREW' ?
`${todos.length} TODO comments - š„ Dev Crew: "Task completion in progress"` :
`${todos.length} TODO comments - If it's TODO, then FUCKING DO IT. Stop procrastinating.`;
debtReport.p1.push(message);
}
// P2 High thresholds - mode-aware
else if (category === 'markdown' && total >= thresholds.p2.markdownWarnings) {
const message = currentMode === 'DEV_CREW' ?
`${total} markdown issues - š„ Dev Crew: "Documentation polish recommended"` :
`${total} markdown linting errors - We're taking back the repo. Clean this today.`;
debtReport.p2.push(message);
} else if (category === 'spelling' && total >= thresholds.p2.spellErrors) {
const message = currentMode === 'DEV_CREW' ?
`${total} spelling issues - š„ Dev Crew: "Terminology consistency needed"` :
`${total} spelling errors - Dictionary.com is judging you.`;
debtReport.p2.push(message);
} else if (category === 'security' && severity.medium >= thresholds.p2.securityMedium) {
const message = currentMode === 'DEV_CREW' ?
`${severity.medium} medium security issues - š„ Dev Crew: "Security improvements planned"` :
`${severity.medium} medium security vulnerabilities - Not great, Bob.`;
debtReport.p2.push(message);
} else if (category === 'eslint' && errors >= thresholds.p2.eslintErrors) {
const message = currentMode === 'DEV_CREW' ?
`${errors} ESLint errors - š„ Dev Crew: "Code quality review scheduled"` :
`${errors} ESLint e