// engine/living-spec-analyzer.ts — Heuristic analysis of spec criteria vs codebase
// Compares acceptance criteria against actual code to produce a living spec report.
// No LLM calls — purely file-existence and pattern-matching heuristics.
import { readFile, stat } from 'node:fs/promises';
import { replaceSectionInSpec } from './spec-format/replace-section.js';
import { join } from 'node:path';
import { glob } from 'glob';
import { extractCriteria } from './validator/extractors.js';
const SOURCE_EXTS = '*.{ts,tsx,js,jsx,py,go,rs,java,kt,rb,php,vue,svelte}';
const IGNORE_DIRS = ['node_modules/**', 'dist/**', 'build/**', '.git/**'];
const MAX_FILES_TO_SCAN = 80;
/**
 * Analyze a spec's acceptance criteria against the codebase.
 * Returns a per-criterion assessment and overall completion percentage.
 */
export async function analyzeLivingSpec(spec, projectPath) {
    const criteria = await extractCriteria(spec);
    const assessments = [];
    // Pre-load file index for the project (cached for all criteria)
    const fileIndex = await buildFileIndex(projectPath);
    for (const criterion of criteria) {
        const assessment = await assessCriterion(criterion, projectPath, fileIndex, spec);
        assessments.push(assessment);
    }
    const metCount = assessments.filter((a) => a.status === 'met').length;
    const pendingCount = assessments.filter((a) => a.status === 'pending').length;
    const driftCount = assessments.filter((a) => a.status === 'drift').length;
    const total = assessments.length;
    const completionPercent = total > 0 ? Math.round((metCount / total) * 100) : 0;
    // SPEC-1010 PR-C: Write living spec analysis into the unified spec.md
    // ## Progress section instead of a standalone progress.md file.
    let progressUpdated = false;
    if (spec.specPath) {
        progressUpdated = await updateProgressInSpec(spec.specPath, assessments, completionPercent);
    }
    else if (spec.progressPath) {
        // Legacy progress.md paths are read-only compatibility metadata in unified spec mode.
        progressUpdated = false;
    }
    return {
        specId: spec.id,
        criteria: assessments,
        completionPercent,
        metCount,
        pendingCount,
        driftCount,
        analyzedAt: new Date().toISOString(),
        progressUpdated,
    };
}
/**
 * Build a searchable file index: paths and a sample of file contents.
 */
async function buildFileIndex(projectPath) {
    const paths = [];
    const contentCache = new Map();
    try {
        const files = await glob(`**/${SOURCE_EXTS}`, {
            cwd: projectPath,
            nodir: true,
            ignore: IGNORE_DIRS,
            maxDepth: 6,
        });
        paths.push(...files);
        // Cache content for a limited number of files
        for (const file of files.slice(0, MAX_FILES_TO_SCAN)) {
            try {
                const content = await readFile(join(projectPath, file), 'utf-8');
                contentCache.set(file, content);
            }
            catch {
                // Skip unreadable files
            }
        }
    }
    catch {
        // Glob error — return empty index
    }
    return { paths, contentCache };
}
/**
 * Assess a single criterion using multiple heuristics.
 */
async function assessCriterion(criterion, projectPath, fileIndex, spec) {
    // Heuristic 1: File existence check
    const fileResult = await checkFileExistence(criterion, projectPath);
    if (fileResult) {
        return fileResult;
    }
    // Heuristic 2: Test file existence (criterion mentions "test" or "spec")
    const testResult = checkTestEvidence(criterion, fileIndex.paths, spec);
    if (testResult) {
        return testResult;
    }
    // Heuristic 3: Pattern matching in code (function/class/component names)
    const patternResult = checkCodePatterns(criterion, fileIndex);
    if (patternResult) {
        return patternResult;
    }
    // Heuristic 4: API endpoint / route detection
    const endpointResult = checkEndpointEvidence(criterion, fileIndex);
    if (endpointResult) {
        return endpointResult;
    }
    // Heuristic 5: Keyword presence in codebase
    const keywordResult = checkKeywordPresence(criterion, fileIndex);
    if (keywordResult) {
        return keywordResult;
    }
    // No evidence found
    return {
        criterion,
        status: 'pending',
        evidence: 'No matching implementation found in codebase',
    };
}
/**
 * Check if criterion references a specific file path and whether it exists.
 */
async function checkFileExistence(criterion, projectPath) {
    // Match explicit file references like "src/foo/bar.ts" or "create file X.ts"
    const filePatterns = [
        /(?:create|add|implement|modify)\s+(?:file\s+)?["']?([^\s"',]+\.\w{1,5})["']?/i,
        /((?:src|lib|app|pages|components|routes|api)\/[\w/.-]+\.\w{1,5})/,
    ];
    for (const pattern of filePatterns) {
        const match = pattern.exec(criterion);
        if (match?.[1]) {
            const filePath = match[1];
            try {
                await stat(join(projectPath, filePath));
                return {
                    criterion,
                    status: 'met',
                    evidence: `File exists: ${filePath}`,
                    matchedFile: filePath,
                };
            }
            catch {
                return {
                    criterion,
                    status: 'pending',
                    evidence: `Expected file not found: ${filePath}`,
                };
            }
        }
    }
    return null;
}
/**
 * Check if test files exist that correspond to the criterion.
 */
function checkTestEvidence(criterion, filePaths, spec) {
    const lower = criterion.toLowerCase();
    const isTestCriterion = lower.includes('test') || lower.includes('spec') || lower.includes('coverage');
    if (!isTestCriterion) {
        return null;
    }
    // Look for test files related to the spec slug
    const slugPattern = spec.slug.replace(/-/g, '[-_.]');
    const testRegex = new RegExp(`(?:test|spec).*${slugPattern}|${slugPattern}.*(?:test|spec)`, 'i');
    const matchingTest = filePaths.find((f) => testRegex.test(f));
    if (matchingTest) {
        return {
            criterion,
            status: 'met',
            evidence: `Test file found: ${matchingTest}`,
            matchedFile: matchingTest,
        };
    }
    // Also check for generic test patterns
    const genericTestFiles = filePaths.filter((f) => f.includes('.test.') || f.includes('.spec.') || f.includes('__tests__'));
    if (lower.includes('no regression') && genericTestFiles.length > 0) {
        return {
            criterion,
            status: 'met',
            evidence: `${genericTestFiles.length} test file(s) found in project`,
        };
    }
    return null;
}
/**
 * Check if code contains function/class/component names mentioned in the criterion.
 */
function checkCodePatterns(criterion, fileIndex) {
    const namePatterns = [
        /(?:component|function|class|module|service|handler|controller)\s+["']?(\w{3,})["']?/i,
        /["'`](\w{3,}(?:Component|Service|Handler|Controller|Module|Store|Hook|Provider))["'`]/,
        /implement\s+(?:the\s+)?["']?(\w{3,})["']?/i,
    ];
    for (const pattern of namePatterns) {
        const match = pattern.exec(criterion);
        if (match?.[1]) {
            const name = match[1];
            for (const [file, content] of fileIndex.contentCache) {
                if (content.includes(name)) {
                    return {
                        criterion,
                        status: 'met',
                        evidence: `Symbol "${name}" found in ${file}`,
                        matchedFile: file,
                    };
                }
            }
            // Name not found in any file — this is drift if spec says it should exist
            return {
                criterion,
                status: 'drift',
                evidence: `Symbol "${name}" not found in any scanned file`,
            };
        }
    }
    return null;
}
/**
 * Check for API endpoint / route definitions mentioned in the criterion.
 */
function checkEndpointEvidence(criterion, fileIndex) {
    // Match patterns like "GET /users", "endpoint /api/foo", "route /bar"
    const routeMatch = /(?:endpoint|route|api|path)\s+(?:for\s+)?["']?((?:\/[\w-]+)+)["']?/i.exec(criterion);
    const verbMatch = /(?:GET|POST|PUT|DELETE|PATCH)\s+["']?((?:\/[\w-]+)+)["']?/i.exec(criterion);
    const match = routeMatch ?? verbMatch;
    if (!match?.[1]) {
        return null;
    }
    const routePath = match[1];
    const routeSegments = routePath.split('/').filter(Boolean);
    for (const [file, content] of fileIndex.contentCache) {
        const lower = content.toLowerCase();
        // Check if route path or key segments appear in code
        if (lower.includes(routePath.toLowerCase())) {
            return {
                criterion,
                status: 'met',
                evidence: `Route "${routePath}" found in ${file}`,
                matchedFile: file,
            };
        }
        // Check for route segments (e.g., "users" for "/api/users")
        const lastSegment = routeSegments[routeSegments.length - 1];
        if (lastSegment && lower.includes(lastSegment.toLowerCase())) {
            return {
                criterion,
                status: 'met',
                evidence: `Route segment "${lastSegment}" found in ${file}`,
                matchedFile: file,
            };
        }
    }
    return {
        criterion,
        status: 'pending',
        evidence: `Route "${routePath}" not found in scanned files`,
    };
}
/**
 * Extract meaningful keywords from criterion and check if they appear in code.
 */
function checkKeywordPresence(criterion, fileIndex) {
    // Extract words that look like identifiers (camelCase, PascalCase, snake_case)
    const identifiers = criterion.match(/\b[a-zA-Z_]\w{4,}\b/g) ?? [];
    const STOP_WORDS = new Set([
        'should',
        'shall',
        'given',
        'when',
        'then',
        'must',
        'have',
        'been',
        'that',
        'this',
        'with',
        'from',
        'which',
        'there',
        'their',
        'about',
        'after',
        'before',
        'correctly',
        'implemented',
        'feature',
        'system',
        'render',
        'display',
        'respond',
        'return',
        'include',
    ]);
    const keywords = identifiers.filter((w) => !STOP_WORDS.has(w.toLowerCase())).slice(0, 5);
    if (keywords.length === 0) {
        return null;
    }
    // Check if multiple keywords appear in the same file
    for (const [file, content] of fileIndex.contentCache) {
        const matchCount = keywords.filter((kw) => content.toLowerCase().includes(kw.toLowerCase())).length;
        if (matchCount >= 2 || (keywords.length === 1 && matchCount === 1)) {
            return {
                criterion,
                status: 'met',
                evidence: `Keywords [${keywords.slice(0, 3).join(', ')}] found in ${file}`,
                matchedFile: file,
            };
        }
    }
    return null;
}
/**
 * SPEC-1010 PR-C: Write the living spec analysis into the ## Progress section
 * of the unified spec.md instead of a standalone progress.md.
 */
async function updateProgressInSpec(specPath, assessments, completionPercent) {
    try {
        const timestamp = new Date().toISOString();
        const newSection = buildProgressSection(assessments, completionPercent, timestamp);
        // buildProgressSection emits `### Living Spec Analysis` (h3) so the
        // subsection lives under `## Progress` without truncating the parent
        // section on the next read. (SPEC-1010 PR-C dual-Opus review.)
        const bodyWithSubheading = newSection.trim();
        // Read existing ## Progress body (may already have prior analysis)
        let progressBody;
        try {
            const specContent = await readFile(specPath, 'utf-8');
            progressBody = extractSectionBodyLocal(specContent, 'Progress');
        }
        catch {
            progressBody = '';
        }
        let updatedBody;
        if (progressBody.includes('### Living Spec Analysis')) {
            // Replace existing analysis subsection (terminates at next h3, h2, hr, or EOF).
            updatedBody = progressBody.replace(/### Living Spec Analysis[\s\S]*?(?=\n### |\n## |\n---\n|$)/, bodyWithSubheading);
        }
        else {
            updatedBody =
                progressBody.trimEnd() +
                    (progressBody.length > 0 ? '\n\n---\n\n' : '') +
                    bodyWithSubheading +
                    '\n';
        }
        await replaceSectionInSpec(specPath, 'Progress', updatedBody);
        return true;
    }
    catch {
        return false;
    }
}
/** Local copy of extractSectionBody to avoid a circular import with spec-format. */
function extractSectionBodyLocal(content, sectionName) {
    const normalized = content.replace(/\r\n/g, '\n');
    const masked = normalized.replace(/(```|~~~)[\s\S]*?\1/g, (block) => block.replace(/[^\n]/g, ' '));
    const headingRe = new RegExp(`^##\\s+${sectionName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b.*$`, 'm');
    const match = headingRe.exec(masked);
    if (!match) {
        return '';
    }
    const afterHeading = match.index + match[0].length;
    const nextRe = /^##\s+\S/gm;
    nextRe.lastIndex = afterHeading;
    const next = nextRe.exec(masked);
    const end = next ? next.index : normalized.length;
    return normalized.slice(afterHeading, end).trim();
}
/**
 * Build the markdown section for living spec progress.
 */
function buildProgressSection(assessments, completionPercent, timestamp) {
    const met = assessments.filter((a) => a.status === 'met');
    const pending = assessments.filter((a) => a.status === 'pending');
    const drift = assessments.filter((a) => a.status === 'drift');
    const lines = [
        '### Living Spec Analysis',
        '',
        `> Last analyzed: ${timestamp}`,
        `> Completion: **${completionPercent}%** (${met.length}/${assessments.length} criteria met)`,
        '',
        '| Status | Criterion | Evidence |',
        '|--------|-----------|----------|',
        ...met.map((a) => `| MET | ${escapePipe(a.criterion)} | ${escapePipe(a.evidence)} |`),
        ...pending.map((a) => `| PENDING | ${escapePipe(a.criterion)} | ${escapePipe(a.evidence)} |`),
        ...drift.map((a) => `| DRIFT | ${escapePipe(a.criterion)} | ${escapePipe(a.evidence)} |`),
        '',
    ];
    if (drift.length > 0) {
        lines.push(`> **Warning:** ${drift.length} criterion(s) show drift — code diverges from spec.`, '');
    }
    return lines.join('\n');
}
/** Escape pipe characters in markdown table cells. */
function escapePipe(text) {
    return text.replace(/\|/g, '\\|');
}
//# sourceMappingURL=living-spec-analyzer.js.map