UNPKG

@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
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