automagik-genie
Version:
Self-evolving AI agent orchestration framework with Model Context Protocol support
268 lines (231 loc) • 6.58 kB
JavaScript
/**
* TODO/FIXME Marker Detection
*
* Scans markdown files for incomplete work markers:
* - TODO, FIXME, XXX, HACK, TBD, WIP
* - "Coming soon", placeholder text
* - Git merge conflict markers
*
* Usage:
* node detect-todos.js [path]
* (defaults to .genie/ if no path provided)
*/
const fs = require('fs');
const path = require('path');
// Marker patterns (case-insensitive)
const MARKERS = {
todo: /\bTODO:?/i,
fixme: /\bFIXME:?/i,
xxx: /\bXXX:?/i,
hack: /\bHACK:?/i,
tbd: /\bTBD:?/i,
wip: /\bWIP:?/i,
};
const PLACEHOLDER_PATTERNS = [
/\bcoming soon\b/i,
/\bto be documented\b/i,
/\bfill this in later\b/i,
/\[placeholder\]/i,
/\blorem ipsum\b/i,
/\bTODO:\s*write this section\b/i,
];
const CONFLICT_MARKERS = [
/^<<<<<<< /m,
/^=======$/m,
/^>>>>>>> /m,
/^\|\|\|\|\|\|\| /m,
];
// Results tracking
const results = {
totalFiles: 0,
scannedFiles: 0,
issues: [],
skipped: [],
};
/**
* Check if file should be scanned
*/
function shouldScan(filePath) {
const excludePatterns = [
'node_modules',
'.git',
'dist',
'build',
'coverage',
'/backups/',
'detect-todos.js', // Don't scan self
'GARBAGE-DETECTION-IDEAS.md', // Ideas doc has intentional TODOs
];
return !excludePatterns.some(pattern => filePath.includes(pattern));
}
/**
* Check if line is inside code block
*/
function isInCodeBlock(lines, lineIndex) {
let inCodeBlock = false;
for (let i = 0; i < lineIndex; i++) {
if (lines[i].trim().startsWith('```')) {
inCodeBlock = !inCodeBlock;
}
}
return inCodeBlock;
}
/**
* Scan file for markers
*/
function scanFile(filePath) {
results.totalFiles++;
if (!shouldScan(filePath)) {
results.skipped.push({ file: filePath, reason: 'Excluded path' });
return;
}
try {
const content = fs.readFileSync(filePath, 'utf-8');
const lines = content.split('\n');
results.scannedFiles++;
lines.forEach((line, index) => {
const lineNum = index + 1;
const inCodeBlock = isInCodeBlock(lines, index);
// Skip code blocks for markers (but not conflict markers)
if (!inCodeBlock) {
// Check TODO/FIXME/etc markers
for (const [type, pattern] of Object.entries(MARKERS)) {
if (pattern.test(line)) {
results.issues.push({
file: filePath,
line: lineNum,
type: `marker_${type}`,
severity: 'warning',
content: line.trim(),
message: `${type.toUpperCase()} marker found`,
suggestion: 'Complete the work or remove the marker',
});
}
}
// Check placeholder patterns
for (const pattern of PLACEHOLDER_PATTERNS) {
if (pattern.test(line)) {
results.issues.push({
file: filePath,
line: lineNum,
type: 'placeholder_text',
severity: 'warning',
content: line.trim(),
message: 'Placeholder text found',
suggestion: 'Complete the content or remove the placeholder',
});
break; // Only report first match per line
}
}
}
// Check conflict markers (even in code blocks)
for (const pattern of CONFLICT_MARKERS) {
if (pattern.test(line)) {
results.issues.push({
file: filePath,
line: lineNum,
type: 'merge_conflict',
severity: 'error',
content: line.trim(),
message: 'Git merge conflict marker found',
suggestion: 'Resolve the conflict immediately',
});
break;
}
}
});
} catch (err) {
results.issues.push({
file: filePath,
type: 'read_error',
message: `Failed to read file: ${err.message}`,
severity: 'error',
});
}
}
/**
* Recursively scan directory
*/
function scanDirectory(dir) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
scanDirectory(fullPath);
} else if (entry.isFile() && entry.name.endsWith('.md')) {
scanFile(fullPath);
}
}
}
/**
* Generate report
*/
function generateReport() {
console.log('\n=== TODO/FIXME Detection Report ===\n');
console.log(`Total files: ${results.totalFiles}`);
console.log(`Scanned: ${results.scannedFiles}`);
console.log(`Issues found: ${results.issues.length}`);
console.log(`Skipped: ${results.skipped.length}\n`);
if (results.issues.length === 0) {
console.log('✅ No TODO/FIXME markers found!\n');
return 0;
}
// Group by severity
const errors = results.issues.filter(i => i.severity === 'error');
const warnings = results.issues.filter(i => i.severity === 'warning');
if (errors.length > 0) {
console.log(`🔴 ERRORS (${errors.length}):\n`);
errors.forEach(issue => {
console.log(` ${issue.file}:${issue.line || '?'}`);
console.log(` Type: ${issue.type}`);
console.log(` Message: ${issue.message}`);
console.log(` Content: ${issue.content}`);
console.log(` Suggestion: ${issue.suggestion}`);
console.log('');
});
}
if (warnings.length > 0) {
console.log(`⚠️ WARNINGS (${warnings.length}):\n`);
warnings.forEach(issue => {
console.log(` ${issue.file}:${issue.line || '?'}`);
console.log(` Type: ${issue.type}`);
console.log(` Content: ${issue.content}`);
console.log('');
});
}
// Summary by type
console.log('\n=== Summary by Type ===\n');
const byType = {};
results.issues.forEach(issue => {
byType[issue.type] = (byType[issue.type] || 0) + 1;
});
Object.entries(byType).forEach(([type, count]) => {
console.log(` ${type}: ${count}`);
});
console.log('');
return errors.length > 0 ? 1 : 0;
}
/**
* Main
*/
function main() {
const targetPath = process.argv[2] || '.genie';
if (!fs.existsSync(targetPath)) {
console.error(`Error: Path not found: ${targetPath}`);
process.exit(1);
}
console.log(`Scanning: ${targetPath}\n`);
const stat = fs.statSync(targetPath);
if (stat.isDirectory()) {
scanDirectory(targetPath);
} else if (targetPath.endsWith('.md')) {
scanFile(targetPath);
} else {
console.error('Error: Target must be a directory or .md file');
process.exit(1);
}
const exitCode = generateReport();
process.exit(exitCode);
}
main();