// Planu — Spec Validator Engine
// Public orchestrator: thin entry point that delegates to internal sub-modules.
// All imports from '../engine/validator.js' continue to work unchanged.
import { readFile } from 'node:fs/promises';
import { extractCriteria, extractVerifyBlocks } from './validator/extractors.js';
import { scanCodeForSpec, checkCriterion, classifyDriftSeverity, quickQualityCheck, } from './validator/analyzer.js';
import { resolveApplicableDimensions } from './validator/scope-resolver.js';
import { buildHolisticReportFromFlatScore } from './validator/holistic-report.js';
import { readEvidenceArtifacts } from './evidence-gates/artifact-reader.js';
// Re-export the full public API so external callers never need to know
// about the sub-module layout.
export { generateDoR, generateDoD } from './validator/dor-dod.js';
export { generateChecklist } from './validator/checklist.js';
// SPEC-730: export scope-resolver and holistic-report for consumers
export { resolveApplicableDimensions, normaliseScore } from './validator/scope-resolver.js';
export { buildHolisticReportFromFlatScore } from './validator/holistic-report.js';
function normalizeCriterionForEvidence(value) {
    return value
        .replace(/^-\s*\[[ xX]\]\s*/i, '')
        .replace(/\s+/g, ' ')
        .trim()
        .toLowerCase();
}
function rowHasValidationEvidence(row) {
    const hasEvidence = [
        row.testEvidence?.length,
        row.contractEvidence?.length,
        row.manualEvidence?.trim(),
        row.validationEvidence?.trim(),
        row.reviewerEvidence?.trim(),
    ].some((value) => Boolean(value));
    return row.changedFiles.length > 0 && hasEvidence;
}
async function readTraceabilityEvidence(spec, projectPath) {
    try {
        const artifacts = await readEvidenceArtifacts({
            spec,
            projectId: spec.projectId,
            specId: spec.id,
            projectPath,
        });
        const rows = artifacts.traceabilityMatrix?.rows ?? [];
        return new Map(rows
            .filter(rowHasValidationEvidence)
            .map((row) => [normalizeCriterionForEvidence(row.acceptanceCriterion), row]));
    }
    catch {
        return new Map();
    }
}
/**
 * Validate a spec against the actual codebase.
 * Reads the canonical spec.md, extracts criteria,
 * then checks the filesystem/code for compliance.
 *
 * SPEC-730: The returned ValidateResult is enriched with `holisticReport`
 * that exposes the applicable dimensions, skipped dimensions, and a score
 * normalised over the applicable set only.
 */
export async function validateSpec(spec, projectPath) {
    // SPEC-730: Resolve applicable dimensions BEFORE running any checks
    const { applicable, skipped } = resolveApplicableDimensions(spec);
    // Emit structured warning when no dimensions apply (malformed frontmatter or future edge case)
    if (applicable.length === 0) {
        console.warn('[planu:validator] holistic_validate_no_dimensions_applicable', {
            specId: spec.id,
            target: spec.target,
            scope: spec.scope,
        });
        const emptyReport = buildHolisticReportFromFlatScore(null, [], skipped);
        return {
            matches: [],
            missing: [],
            extra: [],
            fieldsImplemented: 0,
            fieldsTotal: 0,
            score: null,
            qualityIssues: [],
            holisticReport: emptyReport,
        };
    }
    const criteria = await extractCriteria(spec);
    const traceabilityEvidence = await readTraceabilityEvidence(spec, projectPath);
    const codeState = await scanCodeForSpec(spec, projectPath);
    // Extract verify blocks from spec file (forgiving — empty map if file unreadable)
    let verifyBlockMap = new Map();
    try {
        const specContent = await readFile(spec.specPath, 'utf-8');
        verifyBlockMap = extractVerifyBlocks(specContent);
    }
    catch {
        // spec file not available — proceed without verify blocks
    }
    const matches = [];
    const missing = [];
    const extra = [];
    const qualityIssues = [];
    for (const criterion of criteria) {
        const verifyBlock = verifyBlockMap.get(criterion);
        const evidenceRow = traceabilityEvidence.get(normalizeCriterionForEvidence(criterion));
        const found = evidenceRow !== undefined ||
            (await checkCriterion(criterion, projectPath, codeState, verifyBlock));
        if (found) {
            matches.push(criterion);
        }
        else {
            missing.push(criterion);
        }
    }
    // Detect extra files not covered by spec criteria
    for (const file of codeState.unexpectedFiles) {
        extra.push(`Unexpected file: ${file}`);
    }
    // Check for basic quality issues in affected files
    for (const file of codeState.affectedFiles) {
        const issues = await quickQualityCheck(file, projectPath);
        qualityIssues.push(...issues);
    }
    const fieldsTotal = criteria.length;
    const fieldsImplemented = matches.length;
    // SPEC-730: Normalise score over applicable dimensions only.
    // Return null when there are no criteria to evaluate — a zero score would
    // incorrectly block the DoD gate for specs whose criteria cannot be extracted.
    const rawScore = fieldsTotal > 0 ? Math.round((fieldsImplemented / fieldsTotal) * 100) : null;
    // Build holistic report with scope-aware metadata
    const holisticReport = buildHolisticReportFromFlatScore(rawScore, applicable, skipped, 
    // evidence: mark matching criteria as evidence for spec-compliance dimension
    applicable.includes('spec-compliance')
        ? { 'spec-compliance': matches.slice(0, 10) }
        : undefined);
    return {
        matches,
        missing,
        extra,
        fieldsImplemented,
        fieldsTotal,
        score: rawScore,
        qualityIssues,
        holisticReport,
    };
}
/**
 * Detect drift between spec and current code state.
 * Returns a report with all drifts found and an overall compliance score.
 */
export async function detectDrift(spec, projectPath, _mode = 'full', threshold = 80) {
    const validation = await validateSpec(spec, projectPath);
    const drifts = [];
    // Each missing criterion is a drift
    for (const criterion of validation.missing) {
        drifts.push({
            file: '',
            specCriterion: criterion,
            expected: 'Implemented',
            actual: 'Missing',
            severity: classifyDriftSeverity(criterion),
            autoFixable: false,
        });
    }
    // Quality issues as drift items
    for (const issue of validation.qualityIssues) {
        drifts.push({
            file: issue.file,
            specCriterion: issue.rule,
            expected: issue.suggestion,
            actual: issue.message,
            /* v8 ignore next 2 */
            severity: issue.severity === 'critical' ? 'critical' : issue.severity === 'error' ? 'high' : 'medium',
            autoFixable: false,
            suggestedFix: issue.suggestion,
        });
    }
    const driftScore = validation.score ?? 0;
    const isCompliant = driftScore >= threshold;
    return {
        specId: spec.id,
        checkType: 'on-demand',
        driftScore,
        drifts,
        lastChecked: new Date().toISOString(),
        isCompliant,
    };
}
//# sourceMappingURL=validator.js.map