// engine/convention-scanner/codebase-scanner.ts — SPEC-192
// Scans actual codebase files against parsed conventions
import { execSync } from 'node:child_process';
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { join, relative } from 'node:path';
const MAX_VIOLATIONS_PER_CATEGORY = 50;
// ── helpers ───────────────────────────────────────────────────────────────────
function execSafe(cmd, cwd) {
    try {
        return execSync(cmd, { cwd, stdio: ['pipe', 'pipe', 'pipe'] }).toString();
    }
    catch {
        return '';
    }
}
function lineCount(filePath) {
    try {
        const content = readFileSync(filePath, 'utf-8');
        return content.split('\n').length;
    }
    catch {
        return 0;
    }
}
function parseGrepLine(line) {
    const match = /^([^:]+):(\d+):(.*)$/.exec(line);
    if (!match) {
        return null;
    }
    const file = match[1];
    const lineNum = match[2];
    const code = match[3];
    if (!file || !lineNum || code === undefined) {
        return null;
    }
    return { file: file.trim(), lineNum: parseInt(lineNum, 10), code: code.trim() };
}
function walkSourceFiles(dir, projectPath, extensions, results = []) {
    let entries;
    try {
        entries = readdirSync(dir);
    }
    catch {
        return results;
    }
    for (const entry of entries) {
        if (entry.startsWith('.') || entry === 'node_modules' || entry === 'dist') {
            continue;
        }
        const full = join(dir, entry);
        let isDir;
        try {
            isDir = statSync(full).isDirectory();
        }
        catch {
            continue;
        }
        if (isDir) {
            walkSourceFiles(full, projectPath, extensions, results);
        }
        else if (extensions.some((ext) => full.endsWith(ext))) {
            results.push(relative(projectPath, full));
        }
    }
    return results;
}
// ── scanners per category ─────────────────────────────────────────────────────
function scanNaming(projectPath, rules) {
    const relevantRules = rules.filter((r) => r.category === 'naming');
    if (relevantRules.length === 0) {
        return [];
    }
    const violations = [];
    const rule = relevantRules[0];
    if (!rule) {
        return [];
    }
    const grepOut = execSafe(`grep -rn --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" -P "[À-ÿ]" src/ 2>/dev/null | head -${MAX_VIOLATIONS_PER_CATEGORY}`, projectPath);
    if (!grepOut.trim()) {
        return violations;
    }
    for (const raw of grepOut.trim().split('\n').filter(Boolean)) {
        const parsed = parseGrepLine(raw);
        if (!parsed) {
            continue;
        }
        const { file, lineNum, code } = parsed;
        if (code.startsWith('//') || code.startsWith('*')) {
            continue;
        }
        violations.push({
            ruleId: rule.id,
            category: 'naming',
            severity: rule.severity,
            file,
            line: lineNum,
            message: `Non-ASCII character in identifier: ${code.slice(0, 80)}`,
            suggestion: 'Use English-only variable/function names',
        });
    }
    return violations.slice(0, MAX_VIOLATIONS_PER_CATEGORY);
}
function scanI18n(projectPath, rules) {
    const relevantRules = rules.filter((r) => r.category === 'i18n');
    if (relevantRules.length === 0) {
        return [];
    }
    const rule = relevantRules[0];
    if (!rule) {
        return [];
    }
    const violations = [];
    const grepOut = execSafe(`grep -rn --include="*.tsx" --include="*.jsx" -E '>[^<{]{8,}</' src/ 2>/dev/null | grep -v '//' | head -${MAX_VIOLATIONS_PER_CATEGORY}`, projectPath);
    if (!grepOut.trim()) {
        return violations;
    }
    for (const raw of grepOut.trim().split('\n').filter(Boolean)) {
        const parsed = parseGrepLine(raw);
        if (!parsed) {
            continue;
        }
        const { file, lineNum, code } = parsed;
        violations.push({
            ruleId: rule.id,
            category: 'i18n',
            severity: rule.severity,
            file,
            line: lineNum,
            message: `Possible hardcoded string in JSX: ${code.slice(0, 80)}`,
            suggestion: 'Wrap user-facing strings with i18n translation function',
        });
    }
    return violations.slice(0, MAX_VIOLATIONS_PER_CATEGORY);
}
function extractLineLimit(rules) {
    let fileLimit = 400;
    let fnLimit = 80;
    for (const rule of rules) {
        const desc = rule.description.toLowerCase();
        const fileMatch = /(\d+)\s*lines?\s*(?:per\s*file|file\s*limit|max)/.exec(desc);
        if (fileMatch?.[1]) {
            fileLimit = parseInt(fileMatch[1], 10);
        }
        const fnMatch = /(\d+)\s*lines?\s*(?:per\s*function|function)/.exec(desc);
        if (fnMatch?.[1]) {
            fnLimit = parseInt(fnMatch[1], 10);
        }
    }
    return { fileLimit, fnLimit };
}
function scanFileLimits(projectPath, rules) {
    const relevantRules = rules.filter((r) => r.category === 'file-limits');
    if (relevantRules.length === 0) {
        return [];
    }
    const rule = relevantRules[0];
    if (!rule) {
        return [];
    }
    const { fileLimit } = extractLineLimit(relevantRules);
    const srcDir = join(projectPath, 'src');
    const files = walkSourceFiles(srcDir, projectPath, ['.ts', '.tsx']);
    const violations = [];
    for (const relFile of files) {
        const fullPath = join(projectPath, relFile);
        const count = lineCount(fullPath);
        if (count > fileLimit) {
            violations.push({
                ruleId: rule.id,
                category: 'file-limits',
                severity: count > fileLimit * 1.5 ? 'critical' : 'warning',
                file: relFile,
                message: `File has ${count} lines (limit: ${fileLimit})`,
                suggestion: 'Split into smaller modules — consider subdirectory pattern',
            });
        }
        if (violations.length >= MAX_VIOLATIONS_PER_CATEGORY) {
            break;
        }
    }
    return violations;
}
function parseViolationsFromGrep(grepOut, rule, category, severity, makeMessage, suggestion) {
    const violations = [];
    if (!grepOut.trim()) {
        return violations;
    }
    for (const raw of grepOut.trim().split('\n').filter(Boolean)) {
        const parsed = parseGrepLine(raw);
        if (!parsed) {
            continue;
        }
        const { file, lineNum, code } = parsed;
        violations.push({
            ruleId: rule.id,
            category,
            severity,
            file,
            line: lineNum,
            message: makeMessage(code),
            suggestion,
        });
    }
    return violations;
}
function scanArchitecture(projectPath, rules) {
    const relevantRules = rules.filter((r) => r.category === 'architecture');
    if (relevantRules.length === 0) {
        return [];
    }
    const rule = relevantRules[0];
    if (!rule) {
        return [];
    }
    const violations = [];
    const deepImportOut = execSafe(`grep -rn --include="*.ts" --exclude-dir=convention-scanner "from.*storage/.*/" src/tools/ 2>/dev/null | head -${MAX_VIOLATIONS_PER_CATEGORY}`, projectPath);
    violations.push(...parseViolationsFromGrep(deepImportOut, rule, 'architecture', 'warning', (code) => `Deep import into storage internals: ${code.slice(0, 80)}`, 'Import from barrel export (storage/index.js) instead'));
    const reverseDepOut = execSafe(`grep -rn --include="*.ts" --exclude-dir=convention-scanner "from.*tools/" src/engine/ 2>/dev/null | head -${MAX_VIOLATIONS_PER_CATEGORY}`, projectPath);
    violations.push(...parseViolationsFromGrep(reverseDepOut, rule, 'architecture', 'critical', (code) => `Engine imports from tools layer (violation): ${code.slice(0, 80)}`, 'Engine must not import from tools — invert the dependency'));
    return violations.slice(0, MAX_VIOLATIONS_PER_CATEGORY);
}
function scanCodeQuality(projectPath, rules) {
    const relevantRules = rules.filter((r) => r.category === 'code-quality');
    if (relevantRules.length === 0) {
        return [];
    }
    const rule = relevantRules[0];
    if (!rule) {
        return [];
    }
    const anyTypeOut = execSafe(`grep -rn --include="*.ts" --include="*.tsx" -P ":\\s*any\\b|as\\s+any\\b" src/ 2>/dev/null | grep -v ".d.ts" | head -${MAX_VIOLATIONS_PER_CATEGORY}`, projectPath);
    return parseViolationsFromGrep(anyTypeOut, rule, 'code-quality', 'warning', (code) => `Usage of \`any\` type detected: ${code.slice(0, 80)}`, 'Replace with explicit type or use `unknown` with type guard').slice(0, MAX_VIOLATIONS_PER_CATEGORY);
}
// ── public API ────────────────────────────────────────────────────────────────
/**
 * Scans the codebase against a set of parsed convention rules.
 * Never throws — returns partial results on error.
 */
export function scanConventions(projectPath, rules) {
    const violations = [];
    const scanners = [
        scanNaming,
        scanI18n,
        scanFileLimits,
        scanArchitecture,
        scanCodeQuality,
    ];
    for (const scanner of scanners) {
        try {
            violations.push(...scanner(projectPath, rules));
        }
        catch {
            // Scanner failed — continue with others
        }
    }
    return violations;
}
//# sourceMappingURL=codebase-scanner.js.map