/**
 * config/coverage-ac.mjs — acceptance-criteria parser + spec-path resolver (C29).
 *
 * Pure helpers for plan-to-spec coverage validation. Extracted from
 * apt-tools.mjs so the CLI kernel stays thin and external tests can import
 * them directly.
 */
import { existsSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
/**
 * Parse acceptance criteria from a spec.md document.
 *
 * Pure, file-IO-free helper — takes spec content as a string and returns an
 * array of criterion strings. Handles both numbered items (`1. foo`) and
 * GFM checkbox items (`- [ ] foo`), folds continuation lines (soft-wraps)
 * into the current criterion, and flushes on headings / blank-break / EOF.
 * Each criterion is capped at 500 characters so the downstream fuzzy matcher
 * doesn't blow up on long prose.
 *
 * @param {string} specContent
 * @returns {string[]}
 */
export function parseAcceptanceCriteria(specContent) {
    if (typeof specContent !== 'string' || specContent.length === 0)
        return [];
    const MAX_LEN = 500;
    const lines = specContent.split('\n');
    const results = [];
    let current = null;
    let lastLineBlank = false;
    const NUMBERED_RE = /^(\d+)\.\s+(.*)$/;
    const CHECKBOX_RE = /^-\s*\[[ xX]\]\s*(.*)$/;
    const BULLET_RE = /^[-*+]\s+/;
    const HEADING_RE = /^(#{1,6})\s+(.*)$/;
    const ACCEPTANCE_HEADING_RE = /acceptance\s+criteria/i;
    // Section scoping: if the spec has a heading containing "Acceptance Criteria",
    // only parse items under that heading (flushed when any new heading of same or
    // higher level appears). Otherwise, parse items from the entire document
    // (legacy behavior for specs without a dedicated section).
    let hasAcceptanceSection = false;
    let acceptanceLevel = 0;
    for (const line of lines) {
        const m = line.match(HEADING_RE);
        if (m && ACCEPTANCE_HEADING_RE.test(m[2])) {
            hasAcceptanceSection = true;
            acceptanceLevel = m[1].length;
            break;
        }
    }
    let inAcceptanceSection = !hasAcceptanceSection; // if no section, parse everything
    const flush = () => {
        if (current !== null) {
            let c = current.replace(/\s+/g, ' ').trim();
            if (c.length > MAX_LEN)
                c = c.slice(0, MAX_LEN);
            if (c.length > 0)
                results.push(c);
        }
        current = null;
    };
    for (const rawLine of lines) {
        const trimmed = rawLine.trim();
        // Headings terminate any in-progress criterion and may toggle section scope.
        const headingMatch = trimmed.match(HEADING_RE);
        if (headingMatch) {
            flush();
            lastLineBlank = false;
            if (hasAcceptanceSection) {
                const level = headingMatch[1].length;
                if (ACCEPTANCE_HEADING_RE.test(headingMatch[2])) {
                    inAcceptanceSection = true;
                }
                else if (level <= acceptanceLevel) {
                    // A sibling or higher-level heading closes the section.
                    inAcceptanceSection = false;
                }
                // Lower-level (deeper) headings inside the acceptance section
                // do not close it.
            }
            continue;
        }
        // Outside the acceptance section, skip everything.
        if (!inAcceptanceSection) {
            lastLineBlank = trimmed === '';
            continue;
        }
        // Blank line: remember, but don't flush immediately — a criterion may
        // continue after a single blank separator. Two blanks in a row, or a
        // blank followed by a non-continuation, will flush.
        if (trimmed === '') {
            lastLineBlank = true;
            continue;
        }
        // New numbered criterion.
        const numberedMatch = trimmed.match(NUMBERED_RE);
        if (numberedMatch) {
            flush();
            current = numberedMatch[2];
            lastLineBlank = false;
            continue;
        }
        // New checkbox criterion.
        const checkboxMatch = trimmed.match(CHECKBOX_RE);
        if (checkboxMatch) {
            flush();
            current = checkboxMatch[1];
            lastLineBlank = false;
            continue;
        }
        // Non-numbered, non-checkbox bullet terminates (a new top-level bullet
        // is not a continuation of a numbered criterion).
        if (BULLET_RE.test(trimmed)) {
            flush();
            lastLineBlank = false;
            continue;
        }
        // Otherwise, treat as a continuation of the current criterion — but
        // only if we have one in progress, and only if this line is either
        // indented in the raw source or follows directly with no blank break.
        if (current !== null) {
            const isIndented = /^\s/.test(rawLine);
            if (lastLineBlank && !isIndented) {
                // Blank separator followed by non-indented text — treat as
                // end of the criterion.
                flush();
                lastLineBlank = false;
                continue;
            }
            current = current + ' ' + trimmed;
            lastLineBlank = false;
            continue;
        }
        lastLineBlank = false;
    }
    flush();
    return results;
}
/**
 * Resolve which spec.md path to read for coverage validation.
 *
 * Precedence (highest wins):
 *   1. --spec <path> explicit flag
 *   2. dirname(planPath)/spec.md if planPath is inside .aperant/tasks/
 *   3. projectDir/spec.md (legacy fallback)
 *   4. null (no spec found)
 *
 * @param {string} projectDir - already-resolved absolute project dir
 * @param {string} planPath - already-resolved absolute plan path
 * @param {string|null} explicitSpecFlag - value of --spec flag, or null
 * @returns {{ path: string|null, source: 'explicit'|'task-dir'|'project-root'|'none', checked: Array<{path: string, present: boolean, source: string}> }}
 */
export function resolveSpecPath(projectDir, planPath, explicitSpecFlag) {
    const checked = [];
    if (explicitSpecFlag) {
        const abs = resolve(explicitSpecFlag);
        const present = existsSync(abs);
        checked.push({ path: abs, present, source: 'explicit' });
        if (present)
            return { path: abs, source: 'explicit', checked };
        // Explicit path given but missing — fall through to other candidates.
    }
    // Task-dir heuristic: plan lives under .aperant/tasks/<id>/
    if (planPath) {
        const planDir = dirname(planPath);
        if (planDir.includes(`${'.aperant'}/tasks/`) || planDir.includes(`${'.aperant'}\\tasks\\`)) {
            const candidate = join(planDir, 'spec.md');
            const present = existsSync(candidate);
            checked.push({ path: candidate, present, source: 'task-dir' });
            if (present)
                return { path: candidate, source: 'task-dir', checked };
        }
    }
    // Legacy fallback: project root spec.md
    const legacy = join(projectDir, 'spec.md');
    const legacyPresent = existsSync(legacy);
    checked.push({ path: legacy, present: legacyPresent, source: 'project-root' });
    if (legacyPresent)
        return { path: legacy, source: 'project-root', checked };
    return { path: null, source: 'none', checked };
}
//# sourceMappingURL=coverage-ac.mjs.map