// core/spec-validator.ts — Unified spec format validator (SPEC-716)
//
// Pure function: no global side effects, no MCP SDK dependency, no telemetry.
// Callers (MCP tools, CLI, hooks, CI) handle their own formatting/persistence.
import { readFile } from 'node:fs/promises';
import { checkSpecReadiness } from '../engine/readiness-checker.js';
import { scoreSpecQuality } from '../engine/spec-quality-scorer.js';
import { parseFrontmatter, stripFrontmatter } from '../engine/frontmatter-parser.js';
// ── Required frontmatter fields ──────────────────────────────────────────────
const REQUIRED_FRONTMATTER_FIELDS = ['id', 'title', 'status', 'type', 'target', 'scope'];
// ── Required body sections (SPEC-785 aligned with lean-spec-generator output) ─
const REQUIRED_SECTIONS = ['## Problem', '## Acceptance criteria', '## Technical'];
/**
 * Optional sections: when absent, emit an info-severity warning (not a blocker).
 * Legacy specs WITH these sections still pass silently.
 */
const OPTIONAL_SECTIONS_WITH_INFO = ['## Goal', '## Out of scope'];
// ── Internal helpers ─────────────────────────────────────────────────────────
function makeError(code, message, path, source) {
    return { code, message, path, source };
}
function makeWarning(code, message, path, source) {
    return { code, message, path, source };
}
async function readSpecRaw(specPath) {
    try {
        return await readFile(specPath, 'utf-8');
    }
    catch {
        return null;
    }
}
/**
 * Validates frontmatter presence and required fields.
 * Returns errors for hard blocks and warnings for soft issues.
 */
function validateFrontmatter(raw) {
    const errors = [];
    const warnings = [];
    const hasFrontmatter = raw.startsWith('---\n');
    if (!hasFrontmatter) {
        errors.push(makeError('MISSING_FRONTMATTER', 'spec.md has no YAML frontmatter block (---)', 'frontmatter', 'frontmatter'));
        return { errors, warnings, found: {} };
    }
    const parsed = parseFrontmatter(raw);
    const found = parsed.metadata;
    for (const field of REQUIRED_FRONTMATTER_FIELDS) {
        const value = found[field];
        const valueStr = typeof value === 'string'
            ? value
            : typeof value === 'number' || typeof value === 'boolean'
                ? String(value)
                : '';
        if (value === undefined || value === null || valueStr.trim() === '') {
            errors.push(makeError('MISSING_FRONTMATTER_FIELD', `Required frontmatter field '${field}' is missing or empty`, `frontmatter.${field}`, 'frontmatter'));
        }
    }
    return { errors, warnings, found };
}
/**
 * Validates SPEC-785 aligned unified format: required ## sections in the body.
 * Optional sections emit info warnings (not blockers) when absent.
 * outOfScope: frontmatter field satisfies the ## Out of scope section requirement.
 */
function validateUnifiedFormat(body, frontmatterMetadata = {}) {
    const errors = [];
    const warnings = [];
    for (const section of REQUIRED_SECTIONS) {
        // Case-insensitive match for section heading
        const pattern = new RegExp(`^${section.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')}`, 'im');
        if (!pattern.test(body)) {
            const code = section === '## Technical' ? 'UNIFIED_TECHNICAL_MISSING' : 'MISSING_REQUIRED_SECTION';
            errors.push(makeError(code, `Required section '${section}' is missing from spec.md (SPEC-630 unified format)`, `body.${section.replace('## ', '').replace(/\s+/g, '_').toLowerCase()}`, 'unified-format'));
        }
    }
    // Optional sections: emit info warnings when absent (not blockers)
    for (const section of OPTIONAL_SECTIONS_WITH_INFO) {
        const pattern = new RegExp(`^${section.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')}`, 'im');
        if (pattern.test(body)) {
            // Present in body → satisfied silently (legacy or explicit)
            continue;
        }
        // Special case: ## Out of scope can be satisfied by frontmatter outOfScope field
        if (section === '## Out of scope') {
            const outOfScope = frontmatterMetadata.outOfScope;
            const hasYamlOutOfScope = Array.isArray(outOfScope)
                ? outOfScope.length > 0
                : typeof outOfScope === 'string' && outOfScope.trim().length > 0;
            if (hasYamlOutOfScope) {
                // Satisfied via YAML frontmatter → no warning needed
                continue;
            }
        }
        // Emit info-level warning (not a blocker)
        warnings.push(makeWarning('OPTIONAL_SECTION_MISSING', `Optional section '${section}' is absent — consider adding it for clarity`, `body.${section.replace('## ', '').replace(/\s+/g, '_').toLowerCase()}`, 'unified-format'));
    }
    return { errors, warnings };
}
/**
 * Maps readiness blockers → ValidationError and readiness warnings → ValidationWarning.
 */
function mapReadinessToValidation(blockers, warnings) {
    const errors = blockers.map((msg) => {
        // Determine more specific source for EARS and specificity blockers
        const source = msg.toLowerCase().includes('modal')
            ? 'ears-gate'
            : msg.toLowerCase().includes('difficulty')
                ? 'specificity-gate'
                : 'readiness';
        const code = msg.toLowerCase().includes('modal')
            ? 'EARS_VIOLATION'
            : msg.toLowerCase().includes('difficulty')
                ? 'SPECIFICITY_BLOCKER'
                : msg.toLowerCase().includes('acceptance criteria') ||
                    msg.toLowerCase().includes('criteria')
                    ? 'NO_ACCEPTANCE_CRITERIA'
                    : 'READINESS_BLOCKER';
        return makeError(code, msg, 'body.acceptanceCriteria', source);
    });
    const mappedWarnings = warnings.map((msg) => makeWarning('READINESS_WARNING', msg, 'body', 'readiness'));
    return { errors, warnings: mappedWarnings };
}
/**
 * Maps quality scorer issues → warnings (never errors; scoring formula changes
 * are out of scope per SPEC-716).
 */
function mapQualityToValidation(dimensions) {
    const warnings = [];
    for (const [dim, detail] of Object.entries(dimensions)) {
        for (const issue of detail.issues ?? []) {
            warnings.push(makeWarning('QUALITY_ISSUE', issue, `quality.${dim}`, 'quality'));
        }
    }
    return warnings;
}
// ── Public API ───────────────────────────────────────────────────────────────
/**
 * Validate a spec's format against the unified spec.md schema (SPEC-630).
 * Reads spec.specPath from disk, runs structural + semantic checks, returns
 * a single ValidationResult.
 *
 * Pure function: no global side effects, no MCP SDK dependency, no telemetry.
 */
// eslint-disable-next-line max-lines-per-function, complexity -- comprehensive spec validator: each dimension is a discrete check; splitting would obscure the validation surface
export async function validateSpecFormat(spec, opts) {
    const allErrors = [];
    const allWarnings = [];
    const readinessMode = opts?.readinessMode ?? 'strict';
    const skipQuality = opts?.skipQuality ?? opts?.fast ?? false;
    // ── 1. Read spec file ────────────────────────────────────────────────────
    if (!spec.specPath) {
        allErrors.push(makeError('MISSING_FRONTMATTER', 'specPath is not set — cannot read spec.md', 'specPath', 'frontmatter'));
        return { valid: false, errors: allErrors, warnings: allWarnings };
    }
    const raw = await readSpecRaw(spec.specPath);
    if (raw === null) {
        allErrors.push(makeError('MISSING_FRONTMATTER', `spec.md not found at ${spec.specPath}`, 'specPath', 'frontmatter'));
        return { valid: false, errors: allErrors, warnings: allWarnings };
    }
    // ── 2. Frontmatter validation ────────────────────────────────────────────
    const fmResult = validateFrontmatter(raw);
    allErrors.push(...fmResult.errors);
    allWarnings.push(...fmResult.warnings);
    // ── 3. Unified format validation (SPEC-785) ──────────────────────────────
    const body = stripFrontmatter(raw);
    const unifiedResult = validateUnifiedFormat(body, fmResult.found);
    allErrors.push(...unifiedResult.errors);
    allWarnings.push(...unifiedResult.warnings);
    // ── 4. Readiness check ───────────────────────────────────────────────────
    let readinessScore = 0;
    let bddScenarioCount = 0;
    let criteriaCount = 0;
    try {
        // Cast to Spec since checkSpecReadiness needs the full type but only reads
        // the fields available on the Pick type we receive.
        const report = await checkSpecReadiness(spec, readinessMode);
        readinessScore = report.score;
        // Count scenarios from frontmatter block
        bddScenarioCount = (raw.match(/^\s+-\s+title:/gm) ?? []).length;
        // Count criteria lines (checkbox or GIVEN/WHEN/THEN)
        const bodyLines = body.split('\n');
        criteriaCount = bodyLines.filter((l) => /^- \[[ x]\]/i.test(l.trim()) || /^- .*\bgiven\b.*\bwhen\b.*\bthen\b/i.test(l.trim())).length;
        if (criteriaCount === 0) {
            criteriaCount = bddScenarioCount;
        }
        const mapped = mapReadinessToValidation(report.issues.blockers, report.issues.warnings);
        allErrors.push(...mapped.errors);
        allWarnings.push(...mapped.warnings);
    }
    catch {
        // Best-effort — don't hard-block if readiness checker throws
        allWarnings.push(makeWarning('READINESS_CHECK_FAILED', 'Readiness checker could not run — skipping readiness validation', 'readiness', 'readiness'));
    }
    // Early return for fast mode (skip quality)
    if (opts?.fast) {
        const valid = allErrors.length === 0;
        return { valid, errors: allErrors, warnings: allWarnings };
    }
    // ── 5. Quality scoring ───────────────────────────────────────────────────
    let qualityScore = 0;
    let qualityGrade = 'F';
    if (!skipQuality) {
        try {
            const qualityReport = await scoreSpecQuality(spec);
            qualityScore = qualityReport.score.total;
            qualityGrade = qualityReport.score.grade;
            const qualityWarnings = mapQualityToValidation(qualityReport.dimensions);
            allWarnings.push(...qualityWarnings);
        }
        catch {
            // Best-effort — quality issues never hard-block
        }
    }
    // ── 6. Bypass logic (SPEC-780) ───────────────────────────────────────────
    if (opts?.bypassReason && allErrors.length > 0) {
        allWarnings.push(...allErrors.map((e) => ({
            ...e,
            message: `[BYPASSED: ${opts.bypassReason}] ${e.message}`,
        })));
        allErrors.length = 0;
    }
    const valid = allErrors.length === 0;
    return {
        valid,
        errors: allErrors,
        warnings: allWarnings,
        metrics: {
            readinessScore,
            qualityScore,
            qualityGrade,
            bddScenarioCount,
            criteriaCount,
        },
    };
}
//# sourceMappingURL=spec-validator.js.map