// engine/spec-repair.ts — Auto-detect and fix common spec problems (SPEC-164)
// Pure function module: no I/O, no side effects.
import { parseFrontmatter, updateFrontmatterField } from './frontmatter-parser.js';
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
/** Required YAML frontmatter fields for a well-formed spec. */
const REQUIRED_FRONTMATTER_FIELDS = ['id', 'title', 'type', 'status'];
/** Vague phrases that indicate an untestable acceptance criterion. */
const VAGUE_AC_PATTERNS = [
    /\bshould work( well)?\b/i,
    /\bworks? correctly\b/i,
    /\bworks? (as expected|properly|fine)\b/i,
    /\bfunctions? (correctly|properly|well)\b/i,
    /\bis (fast|quick|efficient|smooth)\b/i,
    /\buser.?friendly\b/i,
    /\beasy to use\b/i,
    /\bgood (performance|ux|experience)\b/i,
    /\bsomething\b/i,
    /\betc\.?\b/i,
];
/** Required markdown sections for a complete spec (old format). Lean format uses YAML criteria:. */
const REQUIRED_SECTIONS = ['## Description'];
/**
 * Tag keywords mapped to tag suggestions.
 * Each keyword is matched as a whole word (word boundary) in the spec content.
 */
const TAG_KEYWORD_MAP = [
    { pattern: /\bauth(entication)?\b/i, tag: 'authentication' },
    { pattern: /\blogin\b/i, tag: 'authentication' },
    { pattern: /\boauth\b/i, tag: 'authentication' },
    { pattern: /\bpayment\b/i, tag: 'billing' },
    { pattern: /\bbilling\b/i, tag: 'billing' },
    { pattern: /\bstripe\b/i, tag: 'billing' },
    { pattern: /\bnotification\b/i, tag: 'notifications' },
    { pattern: /\bdashboard\b/i, tag: 'ui' },
    { pattern: /\bfrontend\b/i, tag: 'frontend' },
    { pattern: /\bbackend\b/i, tag: 'backend' },
    { pattern: /\bapi\b/i, tag: 'api' },
    { pattern: /\bdatabase\b/i, tag: 'database' },
    { pattern: /\bmigration\b/i, tag: 'migration' },
    { pattern: /\bsecurity\b/i, tag: 'security' },
    { pattern: /\baudit\b/i, tag: 'security' },
    { pattern: /\bperformance\b/i, tag: 'performance' },
    { pattern: /\bcache\b/i, tag: 'performance' },
    { pattern: /\bci\/cd\b|\bpipeline\b/i, tag: 'ci' },
    { pattern: /\bdocker\b/i, tag: 'devops' },
    { pattern: /\bdeploy(ment)?\b/i, tag: 'devops' },
];
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
 * Find 1-based line number of the first line that matches a string pattern.
 * Returns 0 if not found.
 */
function findLineNumber(content, search) {
    const lines = content.split('\n');
    for (let i = 0; i < lines.length; i++) {
        if (lines[i]?.includes(search)) {
            return i + 1;
        }
    }
    return 0;
}
/**
 * Check whether the frontmatter block is syntactically malformed
 * (missing closing `---`, or opening delimiter not on first line).
 */
function detectMalformedFrontmatter(content) {
    const lines = content.split('\n');
    if (lines[0]?.trim() !== '---') {
        return null; // No frontmatter at all — handled by missing-field checks
    }
    const closingIdx = lines.findIndex((l, i) => i > 0 && l.trim() === '---');
    if (closingIdx === -1) {
        const fix = {
            type: 'frontmatter-malformed',
            description: 'Frontmatter block is missing closing --- delimiter',
            before: lines.slice(0, 5).join('\n'),
            after: '',
            line: 1,
        };
        return fix;
    }
    return null;
}
/**
 * Check for missing required frontmatter fields.
 * Returns one fix per missing field.
 */
function detectMissingFrontmatterFields(content, parsed) {
    const fixes = [];
    for (const field of REQUIRED_FRONTMATTER_FIELDS) {
        if (parsed.metadata[field] === undefined || parsed.metadata[field] === null) {
            fixes.push({
                type: 'frontmatter-missing-field',
                description: `Required frontmatter field "${field}" is missing`,
                before: '',
                after: `${field}: (fill in)`,
                line: findLineNumber(content, '---'),
            });
        }
    }
    return fixes;
}
/**
 * Detect acceptance criteria lines that use vague/untestable language.
 * Matches checkbox lines (`- [ ]`) containing vague patterns.
 */
function detectVagueAcs(content) {
    const fixes = [];
    const lines = content.split('\n');
    for (let i = 0; i < lines.length; i++) {
        const line = lines[i] ?? '';
        const isAcLine = /^\s*-\s*\[[ x]\]\s+/i.test(line);
        if (!isAcLine) {
            continue;
        }
        for (const pattern of VAGUE_AC_PATTERNS) {
            if (pattern.test(line)) {
                const trimmed = line.trim();
                const rewritten = trimmed.replace(/^(-\s*\[[ x]\]\s*)/i, '$1[REWRITE] Given <context>, when <action>, then <measurable outcome> — ');
                fixes.push({
                    type: 'ac-vague',
                    description: `Acceptance criterion on line ${String(i + 1)} uses vague language ("${pattern.source}")`,
                    before: trimmed,
                    after: rewritten,
                    line: i + 1,
                });
                break; // One fix per line
            }
        }
    }
    return fixes;
}
/**
 * Detect missing required markdown sections.
 */
function detectMissingSections(content) {
    const fixes = [];
    for (const section of REQUIRED_SECTIONS) {
        if (!content.includes(section)) {
            fixes.push({
                type: 'section-missing',
                description: `Required section "${section}" is missing from the spec`,
                before: '',
                after: `${section}\n\n(Add content here)\n`,
                line: 0,
            });
        }
    }
    return fixes;
}
/**
 * Extract array values from a frontmatter field that may have hyphenated keys.
 * Reads directly from the raw content because the frontmatter parser's KV regex
 * only supports \w+ keys (no hyphens).
 */
function extractFrontmatterArray(content, field) {
    const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n/;
    const match = FRONTMATTER_RE.exec(content);
    if (!match?.[1]) {
        return [];
    }
    const escaped = RegExp.escape(field);
    const fieldRe = new RegExp(`^${escaped}:\\s*\\[(.*)\\]`, 'm');
    const fieldMatch = fieldRe.exec(match[1]);
    if (!fieldMatch?.[1]) {
        return [];
    }
    return fieldMatch[1]
        .split(',')
        .map((s) => s.trim().replace(/^["']|["']$/g, ''))
        .filter((s) => s.length > 0);
}
/**
 * Detect broken cross-references: `depends-on` or `blockedBy` values pointing
 * to spec IDs that don't look like valid SPEC-NNN identifiers.
 */
function detectBrokenCrossReferences(content, parsed) {
    const fixes = [];
    const SPEC_ID_RE = /^SPEC-\d+$/;
    // Fields accessible via standard parser (no hyphens)
    const standardFields = [
        { field: 'blockedBy', values: parsed.metadata.blockedBy },
        { field: 'dependencies', values: parsed.metadata.dependencies },
    ];
    for (const { field, values } of standardFields) {
        if (!Array.isArray(values)) {
            continue;
        }
        for (const v of values) {
            if (typeof v === 'string' && v.length > 0 && !SPEC_ID_RE.test(v)) {
                fixes.push({
                    type: 'cross-reference-broken',
                    description: `Cross-reference "${v}" in "${field}" does not match SPEC-NNN format`,
                    before: v,
                    after: 'SPEC-??? (verify or remove)',
                    line: findLineNumber(content, v),
                });
            }
        }
    }
    // Handle hyphenated key `depends-on` via raw content parsing
    const dependsOnValues = extractFrontmatterArray(content, 'depends-on');
    for (const v of dependsOnValues) {
        if (!SPEC_ID_RE.test(v)) {
            fixes.push({
                type: 'cross-reference-broken',
                description: `Cross-reference "${v}" in "depends-on" does not match SPEC-NNN format`,
                before: v,
                after: 'SPEC-??? (verify or remove)',
                line: findLineNumber(content, v),
            });
        }
    }
    return fixes;
}
/**
 * Extract the searchable content for tag analysis: frontmatter + Description section only.
 * Excludes Acceptance Criteria and other sections to reduce false positives.
 */
function extractTagSearchContent(content) {
    // Use frontmatter block + Description section only
    const parts = [];
    // Frontmatter
    const fmMatch = /^---\n([\s\S]*?)\n---\n/.exec(content);
    if (fmMatch?.[1]) {
        parts.push(fmMatch[1]);
    }
    // Description section (up to the next ## heading)
    const descMatch = /##\s+Description\n([\s\S]*?)(?=\n##\s|$)/.exec(content);
    if (descMatch?.[1]) {
        parts.push(descMatch[1]);
    }
    // Fall back to first 30 lines if no description section found
    if (parts.length === 0) {
        parts.push(content.split('\n').slice(0, 30).join('\n'));
    }
    return parts.join('\n');
}
/**
 * Suggest missing tags based on content analysis.
 * Returns at most one fix listing all recommended tags.
 */
function detectMissingTags(content, parsed) {
    const existingTags = new Set(Array.isArray(parsed.metadata.tags) ? parsed.metadata.tags : []);
    const searchContent = extractTagSearchContent(content);
    const recommended = [];
    for (const { pattern, tag } of TAG_KEYWORD_MAP) {
        if (!existingTags.has(tag) && pattern.test(searchContent) && !recommended.includes(tag)) {
            recommended.push(tag);
        }
    }
    if (recommended.length === 0) {
        return [];
    }
    const allTags = [...existingTags, ...recommended];
    const tagsStr = `[${allTags.join(', ')}]`;
    return [
        {
            type: 'tags-missing',
            description: `Suggested tags based on content analysis: ${recommended.join(', ')}`,
            before: Array.isArray(parsed.metadata.tags)
                ? `[${parsed.metadata.tags.join(', ')}]`
                : '(none)',
            after: tagsStr,
            line: findLineNumber(content, 'tags:'),
        },
    ];
}
// ---------------------------------------------------------------------------
// Apply fixes helpers
// ---------------------------------------------------------------------------
/**
 * Apply a single fix to the content string.
 * Returns the updated content.
 */
function applyFix(content, fix) {
    switch (fix.type) {
        case 'frontmatter-malformed': {
            // Add closing delimiter after the last frontmatter line if missing
            const lines = content.split('\n');
            const startIdx = lines.findIndex((l) => l.trim() === '---');
            if (startIdx === -1) {
                return content;
            }
            // Find empty line or end of file after opening ---
            let insertAt = lines.length;
            for (let i = startIdx + 1; i < lines.length; i++) {
                if (lines[i]?.trim() === '' && i > startIdx + 1) {
                    insertAt = i;
                    break;
                }
            }
            lines.splice(insertAt, 0, '---');
            return lines.join('\n');
        }
        case 'frontmatter-missing-field': {
            // Extract field name from description
            const match = /field "(\w+)"/.exec(fix.description);
            if (!match?.[1]) {
                return content;
            }
            return updateFrontmatterField(content, match[1], '(fill in)');
        }
        case 'ac-vague': {
            // Replace vague AC line with the rewritten version
            const lines = content.split('\n');
            const lineIdx = fix.line - 1;
            const currentLine = lines[lineIdx];
            if (lineIdx >= 0 && lineIdx < lines.length && currentLine?.includes(fix.before)) {
                lines[lineIdx] = currentLine.replace(fix.before, fix.after);
            }
            return lines.join('\n');
        }
        case 'section-missing': {
            // Append missing section before the last heading or at end
            if (content.trimEnd().endsWith('\n')) {
                return `${content}${fix.after}`;
            }
            return `${content}\n\n${fix.after}`;
        }
        case 'cross-reference-broken': {
            // Cannot auto-fix — user must verify the referenced spec ID
            return content;
        }
        case 'tags-missing': {
            // Update tags in frontmatter
            return updateFrontmatterField(content, 'tags', fix.after);
        }
        case 'ac-format': {
            // Cannot auto-fix ac-format issues automatically
            return content;
        }
    }
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
 * Detect and optionally fix common spec problems.
 *
 * @param specContent - Raw markdown content of the spec.md file.
 * @param dryRun - When true, detects issues but does not apply fixes.
 * @returns RepairResult with all detected fixes and (if !dryRun) repaired content.
 */
export function repairSpec(specContent, dryRun = false) {
    const parsed = parseFrontmatter(specContent);
    const malformedFix = detectMalformedFrontmatter(specContent);
    const fixes = [
        ...(malformedFix ? [malformedFix] : []),
        ...detectMissingFrontmatterFields(specContent, parsed),
        ...detectVagueAcs(specContent),
        ...detectMissingSections(specContent),
        ...detectBrokenCrossReferences(specContent, parsed),
        ...detectMissingTags(specContent, parsed),
    ];
    if (dryRun || fixes.length === 0) {
        return {
            fixes,
            repairedContent: specContent,
            fixCount: fixes.length,
            dryRun,
        };
    }
    // Apply fixes sequentially — skip cross-reference-broken (manual fix required)
    let repaired = specContent;
    let applied = 0;
    for (const fix of fixes) {
        if (fix.type === 'cross-reference-broken') {
            continue; // Cannot auto-fix
        }
        const before = repaired;
        repaired = applyFix(repaired, fix);
        if (repaired !== before) {
            applied++;
        }
    }
    return {
        fixes,
        repairedContent: repaired,
        fixCount: applied,
        dryRun: false,
    };
}
//# sourceMappingURL=spec-repair.js.map