// Planu — Validator: criteria extractors
// Parses HU.md and FICHA-TECNICA.md to derive acceptance criteria lists.
import { readFile } from 'node:fs/promises';
import { stripFrontmatter } from '../frontmatter-parser.js';
import { readSpecTechnicalSection } from '../spec-format/read-technical-section.js';
/**
 * Extract all acceptance criteria from a spec's source files.
 */
export async function extractCriteria(spec) {
    const criteria = [];
    // Extract from HU.md — look for acceptance criteria sections
    try {
        const rawHuContent = await readFile(spec.specPath, 'utf-8');
        const huContent = stripFrontmatter(rawHuContent);
        const acSection = extractSection(huContent, 'acceptance criteria', 'criterios de aceptaci');
        if (acSection) {
            const items = extractListItems(acSection);
            criteria.push(...items);
        }
        // Also check for explicit "Given/When/Then" patterns at line start.
        // Keep this anchored so narrative text containing "when" is not treated
        // as an acceptance criterion.
        const gwtMatches = huContent.matchAll(/^\s*(?:[-*+]\s+|\d+[.)]\s+)?(?:Given|Dado|When|Cuando|Then|Entonces)\s+(.+)$/gim);
        for (const match of gwtMatches) {
            /* v8 ignore next 3 -- defensive regex capture group guard */
            if (match[1]) {
                criteria.push(match[1].trim());
            }
        }
    }
    catch {
        // HU file not available
    }
    // Extract from FICHA-TECNICA.md — look for technical requirements
    // Only match specific section headings to avoid extracting narrative text
    // from sections like "Technical Security Considerations" or "Implementation Notes"
    try {
        const fichaContent = await readSpecTechnicalSection(spec);
        const reqSection = extractSection(fichaContent, 'requirements', 'requisitos', 'acceptance criteria', 'criterios de aceptaci', 'criterios de verificaci');
        if (reqSection) {
            const items = extractListItems(reqSection);
            criteria.push(...items);
        }
    }
    catch {
        // FICHA file not available
    }
    // If no criteria extracted from files, generate basic ones from spec metadata.
    // Log a warning so users know validation is running against placeholder criteria.
    if (criteria.length === 0) {
        console.warn(`[Planu] No acceptance criteria found in spec "${spec.title}" — ` +
            `using auto-generated placeholder criteria. Add explicit criteria to spec.md for accurate validation.`);
        criteria.push(`Feature "${spec.title}" is implemented`);
        if (spec.target === 'frontend' || spec.target === 'fullstack') {
            criteria.push('UI components render correctly');
        }
        if (spec.target === 'backend' || spec.target === 'fullstack') {
            criteria.push('API endpoints respond correctly');
        }
        criteria.push('No regression in existing tests');
    }
    return [...new Set(criteria)]; // Deduplicate
}
/**
 * Parse verify blocks from HTML comments following acceptance criteria checkboxes.
 * Format: `- [ ] criterion text` followed by `<!-- verify: {...} -->`
 * Returns a Map of criterion text -> VerifyBlock.
 * Invalid JSON blocks are silently skipped (forgiving parser).
 */
export function extractVerifyBlocks(content) {
    const blocks = new Map();
    const lines = content.split('\n');
    let lastCriterion = '';
    for (const line of lines) {
        const trimmed = line.trim();
        // Detect criterion line: - [ ] text or - [x] text (case-insensitive x)
        const criterionMatch = /^-\s*\[[ xX]\]\s*(.+)/i.exec(trimmed);
        if (criterionMatch) {
            lastCriterion = (criterionMatch[1] ?? '').trim();
            continue;
        }
        // Detect verify block: <!-- verify: {...} -->
        // The JSON object may span a single line within the comment
        const verifyMatch = /<!--\s*verify:\s*(\{[^}]+\})\s*-->/.exec(trimmed);
        if (verifyMatch && lastCriterion) {
            try {
                const block = JSON.parse(verifyMatch[1] ?? '{}');
                blocks.set(lastCriterion, block);
            }
            catch {
                // Invalid JSON — skip silently
            }
        }
    }
    return blocks;
}
/**
 * Extract the text content under a markdown heading that matches any of the given keywords.
 */
export function extractSection(content, ...headingKeywords) {
    const lines = content.split('\n');
    let capturing = false;
    let capturedLevel = 0;
    const captured = [];
    for (const line of lines) {
        const headingMatch = /^(#{1,6})\s+(.+)/.exec(line);
        if (headingMatch?.[1] !== undefined && headingMatch[2] !== undefined) {
            const level = headingMatch[1].length;
            const title = headingMatch[2].toLowerCase();
            if (capturing && level <= capturedLevel) {
                break; // End of section
            }
            if (!capturing && headingKeywords.some((kw) => title.includes(kw))) {
                capturing = true;
                capturedLevel = level;
                continue;
            }
        }
        if (capturing) {
            captured.push(line);
        }
    }
    return captured.length > 0 ? captured.join('\n') : null;
}
/**
 * Extract bullet/numbered list items from a markdown text block.
 */
export function extractListItems(content) {
    const items = [];
    const lines = content.split('\n');
    for (const line of lines) {
        const listMatch = /^\s*[-*+]\s+\[?\s*[xX ]?\s*\]?\s*(.+)/.exec(line);
        if (listMatch?.[1]) {
            const item = listMatch[1].trim();
            if (item.length > 5) {
                // Skip trivially short items
                items.push(item);
            }
        }
        // Also handle numbered lists
        const numberedMatch = /^\s*\d+[.)]\s+(.+)/.exec(line);
        if (numberedMatch?.[1]) {
            const item = numberedMatch[1].trim();
            if (item.length > 5) {
                items.push(item);
            }
        }
    }
    return items;
}
//# sourceMappingURL=extractors.js.map