// Planu — Validator: code analyzer
// Scans the codebase to check whether spec criteria are satisfied.
import { readFile, stat } from 'node:fs/promises';
import { readSpecTechnicalSection } from '../spec-format/read-technical-section.js';
import { join, extname, relative } from 'node:path';
import { glob } from 'glob';
import { z } from 'zod';
const BROAD_SCAN_IGNORE = ['node_modules/**', 'dist/**', 'build/**', '.git/**'];
const BROAD_SCAN_MAX = 300;
import { fastReadFiles } from '../core-bridge.js';
/**
 * Build a file-contents cache for a broader search across the project.
 * Caps at BROAD_SCAN_MAX files to bound I/O on large repos.
 */
async function buildBroadCache(projectPath) {
    const cache = new Map();
    try {
        const files = await glob('**/*.{ts,tsx,js,jsx,py,go,rs,java,kt,rb,php}', {
            cwd: projectPath,
            nodir: true,
            ignore: BROAD_SCAN_IGNORE,
            maxDepth: 5,
        });
        const cappedFiles = files.slice(0, BROAD_SCAN_MAX);
        const absoluteFiles = cappedFiles.map((f) => join(projectPath, f));
        const results = fastReadFiles(absoluteFiles);
        const loaded = new Set();
        for (const res of results) {
            cache.set(relative(projectPath, res.path), res.content);
            loaded.add(res.path);
        }
        await Promise.all(cappedFiles.map(async (file, index) => {
            const absPath = absoluteFiles[index];
            if (!absPath || loaded.has(absPath)) {
                return;
            }
            try {
                const content = await readFile(absPath, 'utf-8');
                cache.set(file, content);
            }
            catch {
                // Skip unreadable files
            }
        }));
    }
    catch {
        // Glob error — return empty cache
    }
    return cache;
}
import { deepCheckCriterion } from './deep-code-checker.js';
import { ConfigLoader } from '../config-loader.js';
// ---------------------------------------------------------------------------
// SPEC-209: Drift severity config (loaded from src/config/drift-severity.json)
// ---------------------------------------------------------------------------
const DriftSeverityItemSchema = z.object({
    id: z.string().describe('Severity level identifier: critical | high | medium | low'),
    severity: z
        .enum(['critical', 'high', 'medium', 'low'])
        .describe('Drift severity level: critical, high, medium, or low'),
    keywords: z
        .array(z.string())
        .describe('Keywords in criterion text that trigger this severity level'),
});
const driftSeverityLoader = new ConfigLoader('drift-severity', z.array(DriftSeverityItemSchema));
/** Load severity rules sorted from most to least severe for first-match logic. */
function loadSeverityRules() {
    const items = driftSeverityLoader.load();
    const order = { critical: 0, high: 1, medium: 2, low: 3 };
    return items.sort((a, b) => (order[a.severity] ?? 99) - (order[b.severity] ?? 99));
}
/**
 * Scan the project directory to build a CodeState for the given spec.
 */
export async function scanCodeForSpec(spec, projectPath) {
    const affectedFiles = [];
    const unexpectedFiles = [];
    const fileContents = new Map();
    // Use impact analysis if available
    if (spec.impactAnalysis?.affectedFiles) {
        for (const file of spec.impactAnalysis.affectedFiles) {
            const fullPath = join(projectPath, file);
            try {
                const content = await readFile(fullPath, 'utf-8');
                affectedFiles.push(file);
                fileContents.set(file, content);
            }
            catch {
                // File doesn't exist — might be expected if not yet created
            }
        }
    }
    /* v8 ignore start -- fallback file discovery from technical.md */
    // If no impact analysis, try to extract file paths from technical.md
    if (affectedFiles.length === 0) {
        try {
            const techContent = await readSpecTechnicalSection(spec);
            const filePaths = extractFilePathsFromTechnical(techContent);
            for (const file of filePaths) {
                const fullPath = join(projectPath, file);
                try {
                    const content = await readFile(fullPath, 'utf-8');
                    affectedFiles.push(file);
                    fileContents.set(file, content);
                }
                catch {
                    // File listed in spec but doesn't exist yet
                }
            }
        }
        catch {
            // technical.md not available
        }
    }
    /* v8 ignore stop */
    // Scan for files matching spec slug
    try {
        const slugPattern = spec.slug.replace(/-/g, '[-_]');
        const matchingFiles = await glob(`**/*${slugPattern}*`, {
            cwd: projectPath,
            nodir: true,
            ignore: [
                'node_modules/**',
                'dist/**',
                'build/**',
                '.git/**',
                'docs-site/**',
                'planu/specs/**',
                'website/**',
                'coverage/**',
            ],
            maxDepth: 6,
        });
        for (const file of matchingFiles) {
            if (!affectedFiles.includes(file)) {
                // If we have no other affected files, treat slug matches as affected
                if (affectedFiles.length === 0) {
                    try {
                        const content = await readFile(join(projectPath, file), 'utf-8');
                        affectedFiles.push(file);
                        fileContents.set(file, content);
                    }
                    catch {
                        unexpectedFiles.push(file);
                    }
                }
                else {
                    unexpectedFiles.push(file);
                }
            }
        }
    }
    catch {
        // Glob error
    }
    return { affectedFiles, unexpectedFiles, fileContents };
}
/**
 * Check whether a single criterion is satisfied in the given code state.
 * When `verifyBlock` is provided it is forwarded to deepCheckCriterion,
 * bypassing keyword-based classification (explicit beats inferred).
 *
 * Pass `sharedFileContents` (pre-built via buildBroadCache or
 * buildFileContentsCache from criterion-matcher) to avoid a redundant
 * glob+readFile pass when checking multiple criteria in the same call.
 */
export async function checkCriterion(criterion, projectPath, codeState, verifyBlock, sharedFileContents) {
    // Check for file existence criteria
    const fileMatch = /(?:create|add|implement)\s+(?:file\s+)?["']?([^\s"']+\.\w+)["']?/i.exec(criterion);
    if (fileMatch?.[1]) {
        try {
            await stat(join(projectPath, fileMatch[1]));
            return true;
        }
        catch {
            return false;
        }
    }
    // Check for component/function existence in code
    const componentMatch = /(?:component|function|class|module|service)\s+["']?(\w+)["']?/i.exec(criterion);
    if (componentMatch?.[1]) {
        const name = componentMatch[1];
        // Search in already-loaded codeState contents first (zero extra I/O)
        for (const [, content] of codeState.fileContents) {
            if (content.includes(name)) {
                return true;
            }
        }
        // Broader search: reuse sharedFileContents when provided to avoid a
        // second glob+readFile pass. Falls back to a fresh (capped) read only
        // when no shared cache is available.
        const broadContents = sharedFileContents ?? (await buildBroadCache(projectPath));
        for (const [, content] of broadContents) {
            if (content.includes(name)) {
                return true;
            }
        }
        return false;
    }
    // Check for API endpoint criteria
    const endpointMatch = /(?:endpoint|route|api)\s+(?:for\s+)?["']?(\w+)["']?/i.exec(criterion);
    if (endpointMatch?.[1]) {
        for (const [, content] of codeState.fileContents) {
            if (content.toLowerCase().includes(endpointMatch[1].toLowerCase())) {
                return true;
            }
        }
    }
    // Deep code check: grep/metric/coverage analysis on the actual filesystem.
    // If a verify block is present it is passed explicitly (bypasses keyword inference).
    // For non-manual checks, the deep result is authoritative.
    // For manual checks (type === 'manual'), we fall back to the optimistic heuristic.
    const deepResult = await deepCheckCriterion(criterion, projectPath, verifyBlock);
    if (deepResult.checkType !== 'manual') {
        return deepResult.passed;
    }
    // If we have affected files, assume basic criteria are met (optimistic fallback)
    if (codeState.affectedFiles.length > 0) {
        return true;
    }
    return false;
}
/**
 * Classify the severity of a missing criterion based on its content.
 * Keywords and severity levels are loaded from src/config/drift-severity.json (SPEC-209).
 */
export function classifyDriftSeverity(criterion) {
    const lower = criterion.toLowerCase();
    const rules = loadSeverityRules();
    for (const rule of rules) {
        if (rule.severity === 'low') {
            // low is the catch-all fallback — skip keyword matching
            continue;
        }
        if (rule.keywords.some((kw) => lower.includes(kw))) {
            return rule.severity;
        }
    }
    return 'low';
}
/**
 * Run lightweight quality checks on a single file and return any findings.
 */
export async function quickQualityCheck(file, projectPath) {
    const findings = [];
    const ext = extname(file);
    const codeExts = [
        '.ts',
        '.tsx',
        '.js',
        '.jsx',
        '.py',
        '.go',
        '.rs',
        '.java',
        '.kt',
        '.rb',
        '.php',
    ];
    if (!codeExts.includes(ext)) {
        return findings;
    }
    try {
        const content = await readFile(join(projectPath, file), 'utf-8');
        const lines = content.split('\n');
        // Check file length
        if (lines.length > 500) {
            findings.push({
                file,
                category: 'clean-code',
                severity: 'warning',
                rule: 'file-length',
                message: `File has ${lines.length} lines. Consider splitting into smaller modules.`,
                suggestion: 'Extract related functionality into separate files/modules.',
            });
        }
        // Check for TODO/FIXME/HACK
        for (let i = 0; i < lines.length; i++) {
            const line = lines[i];
            if (line !== undefined && /\b(TODO|FIXME|HACK|XXX)\b/.exec(line)) {
                findings.push({
                    file,
                    line: i + 1,
                    category: 'clean-code',
                    severity: 'info',
                    rule: 'pending-work',
                    message: `Found TODO/FIXME/HACK marker at line ${i + 1}`,
                    suggestion: 'Address the pending work or create a spec for it.',
                });
            }
        }
        // Check for console.log / print statements in code
        for (let i = 0; i < lines.length; i++) {
            const line = lines[i];
            if (line !== undefined &&
                /console\.(log|debug|info)\(/.exec(line) &&
                !file.includes('test') &&
                !file.includes('spec')) {
                findings.push({
                    file,
                    line: i + 1,
                    category: 'clean-code',
                    severity: 'warning',
                    rule: 'no-console',
                    message: `Console statement found at line ${i + 1}`,
                    suggestion: 'Use a proper logger instead of console statements.',
                });
            }
        }
    }
    catch {
        // Can't read file
    }
    return findings;
}
/**
 * Extract file paths mentioned in technical.md content.
 * Looks for code-like paths (e.g., src/tools/foo.ts, tests/bar.test.ts).
 */
function extractFilePathsFromTechnical(content) {
    const paths = new Set();
    // Match file paths in backticks, table cells, or bullet points
    const pattern = /(?:^|\s|`|\/|\|)\s*((?:src|tests|lib|app|pages|components)\/[\w./-]+\.\w+)/gm;
    for (const match of content.matchAll(pattern)) {
        if (match[1]) {
            paths.add(match[1]);
        }
    }
    return [...paths];
}
/**
 * Check if scope and difficulty rating are consistent with each other.
 */
export function validateScopeDifficulty(scope, difficulty) {
    const scopeDiffRange = {
        trivial: [1, 2],
        feature: [1, 3],
        'cross-module': [2, 4],
        architectural: [3, 5],
    };
    const range = scopeDiffRange[scope];
    if (!range) {
        return true;
    }
    return difficulty >= range[0] && difficulty <= range[1];
}
//# sourceMappingURL=analyzer.js.map