// engine/ears/spec-linter.ts — EARS-based spec linting (SPEC-410)
import { readFile } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { glob } from 'glob';
import { scoreCriterion, gradeSpec } from './criterion-scorer.js';
// Regex to extract acceptance criteria lines (checkbox format)
const AC_LINE_REGEX = /^\s*-\s+\[[ xX]\]\s+(.+)$/;
/**
 * Extract acceptance criteria texts from spec.md content.
 * Recognizes lines formatted as "- [ ] criterion" or "- [x] criterion".
 */
function extractCriteria(content) {
    return content
        .split('\n')
        .map((line) => AC_LINE_REGEX.exec(line))
        .filter((m) => m !== null)
        .map((m) => {
        const text = m[1];
        // This is safe: AC_LINE_REGEX always captures group 1 when match succeeds
        return (text ?? '').trim();
    })
        .filter((t) => t.length > 0);
}
function buildSpecReport(specId, criteria) {
    if (criteria.length === 0) {
        return {
            specId,
            criteriaCount: 0,
            overallScore: 0,
            compliantCount: 0,
            partialCount: 0,
            nonCompliantCount: 0,
            criteria: [],
            grade: 'F',
        };
    }
    const overallScore = Math.round((criteria.reduce((sum, c) => sum + c.overallScore, 0) / criteria.length) * 10) / 10;
    const compliantCount = criteria.filter((c) => c.earsCompliant).length;
    const nonCompliantCount = criteria.filter((c) => !c.earsCompliant).length;
    const partialCount = 0; // reserved for future partial-match detection
    return {
        specId,
        criteriaCount: criteria.length,
        overallScore,
        compliantCount,
        partialCount,
        nonCompliantCount,
        criteria,
        grade: gradeSpec(overallScore),
    };
}
/**
 * Lint a single spec by ID — reads spec.md from the planu/specs directory.
 */
export async function lintSpec(specId, projectPath) {
    const specDir = join(projectPath, 'planu', 'specs');
    const pattern = `${specId}-*/spec.md`;
    const matches = await glob(pattern, { cwd: specDir, absolute: false });
    if (matches.length === 0) {
        // Try exact match without trailing glob
        const directPath = join(specDir, specId, 'spec.md');
        if (existsSync(directPath)) {
            const content = await readFile(directPath, 'utf-8');
            const extracted = extractCriteria(content);
            return buildSpecReport(specId, extracted.map(scoreCriterion));
        }
        return buildSpecReport(specId, []);
    }
    const specPath = join(specDir, matches[0] ?? '');
    try {
        const content = await readFile(specPath, 'utf-8');
        const extracted = extractCriteria(content);
        return buildSpecReport(specId, extracted.map(scoreCriterion));
    }
    catch {
        // Unreadable — return empty report
        return buildSpecReport(specId, []);
    }
}
// Lint all specs in the project — reads all planu/specs/SPEC-NNN/spec.md files.
export async function lintAllSpecs(projectPath) {
    const specDir = join(projectPath, 'planu', 'specs');
    const specFiles = await glob('*/spec.md', { cwd: specDir, absolute: false });
    const specReports = [];
    for (const relPath of specFiles) {
        const specDirName = relPath.split('/')[0] ?? '';
        // Extract SPEC-XXX from directory name like "SPEC-042-some-slug"
        const specIdMatch = /^(SPEC-\d+)/i.exec(specDirName);
        const specId = specIdMatch?.[1] ?? specDirName;
        const fullPath = join(specDir, relPath);
        let extracted;
        try {
            const content = await readFile(fullPath, 'utf-8');
            extracted = extractCriteria(content);
        }
        catch {
            continue;
        }
        if (extracted.length > 0) {
            const report = buildSpecReport(specId, extracted.map(scoreCriterion));
            specReports.push(report);
        }
    }
    const totalSpecs = specReports.length;
    const averageScore = totalSpecs > 0
        ? Math.round((specReports.reduce((sum, r) => sum + r.overallScore, 0) / totalSpecs) * 10) / 10
        : 0;
    // Collect all criteria across specs and find the 10 worst by overallScore
    const allCriteria = [];
    for (const report of specReports) {
        for (const c of report.criteria) {
            allCriteria.push({ criterion: c.text, specId: report.specId, score: c.overallScore });
        }
    }
    allCriteria.sort((a, b) => a.score - b.score);
    const topWorstCriteria = allCriteria.slice(0, 10);
    return {
        projectPath,
        totalSpecs,
        averageScore,
        topWorstCriteria,
        specReports,
        generatedAt: new Date().toISOString(),
    };
}
//# sourceMappingURL=spec-linter.js.map