// tools/validate.ts — Compare spec vs implementation (SPEC-595: elicitation on failures)
// Uses the validator engine to check coverage, missing items, quality issues.
import { execSync, execFileSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { elicitOrFallback, buildEnumSchema } from '../engine/elicitation/elicit-helper.js';
import { compactObj } from '../engine/compact-obj.js';
import { resolveProjectId, missingProjectIdError } from './resolve-project-id.js';
import { specStore, knowledgeStore } from '../storage/index.js';
import { validateSpec, generateDoR, generateDoD } from '../engine/validator.js';
import { calcQualityScore } from '../engine/auditor-scoring.js';
import { dispatchFeedbackEvent } from './learn.js';
import { t, ti } from '../i18n/index.js';
import { createBasicPhaseEvent } from '../engine/phase-tracker.js';
import { validateContractCompliance } from '../engine/test-scaffold-generator.js';
import { parseConventions, scanConventions } from '../engine/convention-scanner/index.js';
import { validateScopeCompliance } from '../engine/scope-boundaries/index.js';
import { compareWithBaseline } from '../storage/convention-baseline.js';
import { writeImplementationReviewReport } from '../engine/validator/validation-report-writer.js';
import { evaluateNewCodeGate } from '../engine/ai-assurance/index.js';
import { queryProjectGraphSlice } from '../engine/project-graph/index.js';
// Re-export for external use (SPEC-018)
export { validateContractCompliance };
function graphCoverageGaps(slice) {
    if (slice === null) {
        return [];
    }
    const criteria = slice.nodes.filter((node) => node.type === 'criterion');
    return criteria
        .filter((criterion) => {
        const outgoing = slice.edges.filter((edge) => edge.from === criterion.id);
        return !outgoing.some((edge) => edge.type === 'implements' || edge.type === 'tests');
    })
        .map((criterion) => criterion.label)
        .slice(0, 10);
}
async function buildGraphCoverageReport(args) {
    const graphSlice = await queryProjectGraphSlice(args).catch(() => null);
    return {
        freshness: graphSlice?.freshness,
        gaps: graphCoverageGaps(graphSlice),
        compactNodes: graphSlice?.nodes.length ?? 0,
        compactEdges: graphSlice?.edges.length ?? 0,
    };
}
function formatGraphCoverageText(report) {
    return report.gaps.length > 0
        ? `\nGRAPH     ${String(report.gaps.length)} graph-backed coverage gap(s)`
        : '';
}
async function validateStrictLayoutOrError(args) {
    try {
        const { validateStrictPlanuLayout } = await import('../engine/spec-migrator/index.js');
        const layout = await validateStrictPlanuLayout(args.projectPath, {
            specPath: args.specPath,
            includeRoot: false,
        });
        if (layout.ok) {
            return null;
        }
        return {
            content: [
                {
                    type: 'text',
                    text: `Strict Planu layout validation failed for ${args.specId}.\n\n` +
                        `Non-canonical paths:\n${layout.offenders.map((p) => `- ${p}`).join('\n')}\n\n` +
                        `Canonical contract:\n${layout.contract}`,
                },
            ],
            isError: true,
            structuredContent: {
                error: 'strict_planu_layout_violation',
                offenders: layout.offenders,
                contract: layout.contract,
            },
        };
    }
    catch {
        return null;
    }
}
/** Build the fallback interactiveQuestion for validation failures. */
function buildValidateFailureFallback(failedCount) {
    return {
        header: 'Failures',
        question: `${failedCount} criteria failed. How do you want to proceed?`,
        multiSelect: false,
        options: [
            {
                label: 're-implement',
                description: 'Fix the failing criteria and run validate again (Recommended)',
            },
            { label: 'ignore', description: 'Ignore failures and proceed (not recommended)' },
            { label: 'mark-manual', description: 'Mark as manually reviewed — will bypass auto-check' },
        ],
    };
}
// SPEC-595: server? param enables elicitation on failures
export async function handleValidate(args, server) {
    const { specId } = args;
    const projectId = resolveProjectId(args);
    if (!projectId) {
        return missingProjectIdError;
    }
    // 1. Load spec
    const spec = await specStore.getSpec(projectId, specId);
    if (!spec) {
        return {
            content: [{ type: 'text', text: ti('spec.notFound', { id: specId }) }],
            isError: true,
            structuredContent: {
                error: 'spec_not_found',
                code: 404,
                context: { specId, projectId },
                fixHint: `Verify the specId is correct and the spec exists in project ${projectId}.`,
            },
        };
    }
    // 2. Load project knowledge for project path
    const knowledge = await knowledgeStore.getKnowledge(projectId);
    if (!knowledge) {
        return {
            content: [{ type: 'text', text: t('project.notFound') }],
            isError: true,
            structuredContent: {
                error: 'project_not_found',
                code: 404,
                context: { projectId },
                fixHint: 'Run init_project first to initialize the project knowledge.',
            },
        };
    }
    const projectPath = knowledge.projectPath;
    // SPEC-1017: fail closed when planu/ contains non-canonical artifacts.
    const strictLayoutError = await validateStrictLayoutOrError({
        projectPath,
        specId,
        specPath: spec.specPath,
    });
    if (strictLayoutError !== null) {
        return strictLayoutError;
    }
    // 3. Run validation engine
    const result = await validateSpec(spec, projectPath);
    const graphCoverage = await buildGraphCoverageReport({ projectId, projectPath, specId });
    // 4. Generate DoR and DoD checklists
    const dor = generateDoR(spec);
    const dod = await generateDoD(spec, result, undefined, projectPath);
    // 5. Compute implementation quality score from validated files (SPEC-010 S47)
    const implementationQualityScore = calcQualityScore(result.qualityIssues);
    const auditedFiles = [...new Set(result.qualityIssues.map((i) => i.file))];
    // 6. SPEC-190: Convention compliance check
    let conventionViolations = [];
    let regressionDetected = false;
    try {
        const rules = parseConventions(projectPath);
        if (rules.length > 0) {
            conventionViolations = scanConventions(projectPath, rules);
            // Check regression against baseline
            const comparison = await compareWithBaseline(projectId, {
                rules,
                violations: conventionViolations,
                summary: {
                    totalRules: rules.length,
                    totalViolations: conventionViolations.length,
                    bySeverity: {},
                    byCategory: {},
                },
                scannedAt: new Date().toISOString(),
            });
            regressionDetected = comparison.regressionDetected;
        }
    }
    catch {
        /* best-effort — don't fail validation */
    }
    // 7. Lint check (best-effort — non-blocking)
    const lintCheck = runLintCheck(projectPath, knowledge.lintCommand ?? null);
    const assuranceGates = runAssuranceGates(projectPath);
    // SPEC-1050: Generate a mandatory implementation-review artifact.
    const validationReport = await writeImplementationReviewReport({
        projectId,
        specId,
        spec,
        projectPath,
        score: result.score,
        lintPassed: lintCheck.passed,
        conventionRegression: regressionDetected,
    });
    // 8. Build output
    const output = {
        specId,
        title: spec.title,
        score: result.score,
        fieldsImplemented: result.fieldsImplemented,
        fieldsTotal: result.fieldsTotal,
        matches: result.matches,
        missing: result.missing,
        extra: result.extra,
        qualityIssues: result.qualityIssues.map((issue) => ({
            file: issue.file,
            line: issue.line,
            severity: issue.severity,
            rule: issue.rule,
            message: issue.message,
            suggestion: issue.suggestion,
        })),
        implementationQualityScore: {
            overall: implementationQualityScore.overall,
            gradeLetter: scoreToGrade(implementationQualityScore.overall),
            filesAudited: auditedFiles.length,
            categories: implementationQualityScore.categories.map((c) => ({
                category: c.category,
                score: c.score,
                weight: c.weight,
                issueCount: c.issueCount,
                worstSeverity: c.worstSeverity,
            })),
        },
        definitionOfReady: {
            isReady: dor.isReady,
            passed: dor.passedCount,
            required: dor.requiredCount,
            items: dor.items
                .filter((item) => item.status === 'failed')
                .map((item) => ({
                id: item.id,
                description: item.description,
                status: item.status,
            })),
        },
        definitionOfDone: {
            isDone: dod.isDone,
            passed: dod.passedCount,
            required: dod.requiredCount,
            failedItems: dod.items
                .filter((i) => i.required && i.status !== 'passed')
                .map((i) => ({
                id: i.id,
                description: i.description,
                category: i.category,
                status: i.status,
            })),
            items: dod.items
                .filter((item) => item.status !== 'passed')
                .map((item) => ({
                id: item.id,
                description: item.description,
                status: item.status,
            })),
        },
        conventionCompliance: {
            totalViolations: conventionViolations.length,
            regressionDetected,
            violations: conventionViolations.slice(0, 20).map((v) => ({
                category: v.category,
                file: v.file,
                line: v.line,
                message: v.message,
                severity: v.severity,
            })),
        },
        lintCheck,
        assuranceGates,
        validationReport,
        graphCoverage,
    };
    // SPEC-612: Scope boundary validation — warn if impl files match outOfScope items
    if (spec.outOfScope !== undefined && spec.outOfScope.length > 0) {
        const scopeResult = validateScopeCompliance(spec.outOfScope, auditedFiles);
        for (const warning of scopeResult.warnings) {
            output.qualityIssues.push({
                file: warning.filePath,
                line: 0,
                severity: 'warning',
                rule: 'scope-boundary-violation',
                message: warning.reason,
                suggestion: `Review whether this file should exist — it matches the out-of-scope item: "${warning.violates}"`,
            });
        }
    }
    // Auto-suggest corrective actions based on validation results
    // score is null when no criteria can be extracted — treat as 0 for comparisons.
    const scoreValue = result.score ?? 0;
    const suggestions = [];
    if (scoreValue < 50) {
        suggestions.push('Critical: Many acceptance criteria not implemented. Review spec requirements.');
    }
    if (result.qualityIssues.length > 10) {
        suggestions.push('Run audit for detailed code quality analysis.');
    }
    if (!dor.isReady && spec.status === 'draft') {
        suggestions.push('Spec is not ready for implementation. Complete missing DoR items first.');
    }
    if (!dod.isDone && spec.status === 'implementing') {
        suggestions.push('Implementation incomplete. Review DoD checklist before marking as done.');
    }
    if (result.missing.length > 0) {
        suggestions.push(`${result.missing.length} criteria still missing. Run detect_drift to identify gaps.`);
    }
    if (graphCoverage.gaps.length > 0) {
        suggestions.push(`${graphCoverage.gaps.length} graph-backed coverage gap(s) found without broad repository scan.`);
    }
    if (conventionViolations.length > 0) {
        suggestions.push(`${conventionViolations.length} convention violation(s) detected. Fix before marking as done.`);
    }
    if (regressionDetected) {
        suggestions.push('Quality regression detected: more violations than baseline. New code introduced convention violations.');
    }
    if (!lintCheck.passed) {
        suggestions.push(`Lint check failed: ${lintCheck.issueCount} issue(s) found via \`${lintCheck.command}\`. Fix before marking as done.`);
    }
    if (!assuranceGates.newCode.passed) {
        suggestions.push(`Assurance gate failed: ${String(assuranceGates.newCode.findings.length)} new-code domain inference issue(s) found.`);
    }
    const outputWithSuggestions = compactObj({
        ...output,
        suggestions: suggestions.length > 0 ? suggestions : undefined,
    });
    // `lintCheck.output` is part of the MCP output contract. Keep it even when
    // empty because compactObj removes empty strings recursively.
    outputWithSuggestions.lintCheck = lintCheck;
    // SPEC-012: Dispatch feedback event for auto-improvement loop (best-effort)
    void dispatchFeedbackEvent(projectId, {
        type: 'validate',
        specId,
        input: { specType: spec.type, scope: spec.scope },
        outcome: {
            score: result.score,
            missingCount: result.missing.length,
            qualityIssueCount: result.qualityIssues.length,
            dorPassed: dor.isReady,
            dodPassed: dod.isDone,
        },
        delta: { failedCriteria: output.definitionOfReady.items.map((i) => i.id) },
        timestamp: new Date().toISOString(),
    }).catch((err) => {
        console.error('[planu] validate feedback event failed:', err);
    });
    // Auto-capture antipattern when quality is consistently low
    if (scoreValue < 70 && result.qualityIssues.length > 0) {
        const topIssueRules = [...new Set(result.qualityIssues.map((i) => i.rule))].slice(0, 5);
        void dispatchFeedbackEvent(projectId, {
            type: 'validate',
            specId,
            input: { trigger: 'auto-quality-learn', rules: topIssueRules },
            outcome: { score: result.score, antipatterns: topIssueRules },
            delta: { qualityGap: 100 - scoreValue },
            timestamp: new Date().toISOString(),
        }).catch((err) => {
            console.error('[planu] validate quality feedback event failed:', err);
        });
    }
    const commitReminder = scoreValue >= 80
        ? `\n\n> 💡 Good score! Remember to save progress with git: \`git add planu/specs/ planu/conventions.json && git commit -m "chore: ${specId} progress checkpoint"\``
        : '';
    // SPEC-326: Pending HTML changes notification
    const planuChanges = checkPlanuUncommittedChanges(projectPath);
    const planuReminder = planuChanges
        ? '\n\n📁 HTML reports updated — commit to save: `git add planu/specs/ planu/conventions.json && git commit -m "chore(planu): update reports"`'
        : '';
    // SPEC-512: Compact output — only surface failures, cap to 200 tokens
    const totalCriteria = result.fieldsTotal;
    const passedCount = result.fieldsImplemented;
    const failedCount = totalCriteria - passedCount;
    const compactText = buildCompactValidateText({
        score: scoreValue,
        passedCount,
        failedCount,
        totalCriteria,
        missing: result.missing,
        commitReminder,
        planuReminder,
    });
    const graphText = formatGraphCoverageText(graphCoverage);
    // SPEC-512: Compact structuredContent — essential fields only at top level
    const compactSummary = {
        specId,
        score: result.score,
        passed: passedCount,
        failed: failedCount,
        total: totalCriteria,
        status: failedCount === 0 ? 'passing' : 'failing',
    };
    // SPEC-595: Elicit next action when validation fails and a server is available
    if (server !== undefined && failedCount > 0) {
        const { field, property } = buildEnumSchema('action', ['re-implement', 'ignore', 'mark-manual'], ['Fix failing criteria (Recommended)', 'Ignore failures', 'Mark as manually reviewed'], 'Next action', 're-implement');
        const schema = { type: 'object', properties: { [field]: property } };
        const fallback = buildValidateFailureFallback(failedCount);
        const outcome = await elicitOrFallback(server, `Validation: ${String(failedCount)} criteria failed. How do you want to proceed?`, schema, [fallback]);
        if (outcome.mode === 'fallback') {
            return {
                phaseEvent: createBasicPhaseEvent('validate', 'completed', 100, 'validating'),
                content: [
                    { type: 'text', text: compactText + graphText },
                    {
                        type: 'text',
                        text: '⚡ INTERACTIVE — call AskUserQuestion now with the interactiveQuestions from structuredContent',
                    },
                ],
                structuredContent: {
                    ...outputWithSuggestions,
                    summary: compactSummary,
                    interactiveQuestions: outcome.interactiveQuestions,
                },
            };
        }
        const elicitedAction = outcome.action === 'accept'
            ? (outcome.content?.action ?? 're-implement')
            : 're-implement';
        return {
            phaseEvent: createBasicPhaseEvent('validate', 'completed', 100, 'validating'),
            content: [{ type: 'text', text: compactText + graphText }],
            structuredContent: {
                ...outputWithSuggestions,
                summary: compactSummary,
                elicitedAction,
            },
        };
    }
    return {
        phaseEvent: createBasicPhaseEvent('validate', 'completed', 100, 'validating'),
        content: [{ type: 'text', text: compactText + graphText }],
        structuredContent: {
            ...outputWithSuggestions,
            summary: compactSummary,
        },
    };
}
function buildCompactValidateText(args) {
    const { score, passedCount, failedCount, totalCriteria, missing, commitReminder, planuReminder } = args;
    if (failedCount === 0) {
        // All passing — single line
        return (`✅ Validate: ${String(score)}/100 — ${String(passedCount)}/${String(totalCriteria)} criteria passing` +
            commitReminder +
            planuReminder);
    }
    // Failures exist — list only the failing ones (max 5)
    const MAX_FAILURES = 5;
    const shownFailures = missing.slice(0, MAX_FAILURES);
    const remainingCount = missing.length - shownFailures.length;
    const failureList = shownFailures.map((f) => `  - ${f}`).join('\n');
    const moreLine = remainingCount > 0 ? `\n  ... and ${String(remainingCount)} more` : '';
    const failNames = shownFailures.slice(0, 3).join(', ');
    const summaryLine = `❌ Validate: ${String(score)}/100 — ${String(failedCount)} failing: [${failNames}${remainingCount + shownFailures.length > 3 ? '...' : ''}]`;
    return summaryLine + '\n' + failureList + moreLine + commitReminder + planuReminder;
}
/** Allowlist: alphanumerics, spaces, and safe shell chars. Rejects ; | $ ` & < > */
const SAFE_COMMAND_RE = /^[\w\s./:@~=-]+$/;
const LINT_CHECK_TIMEOUT_MS = 50_000;
function isSafeCommand(cmd) {
    return SAFE_COMMAND_RE.test(cmd);
}
function isCommandTimeout(err) {
    if (!(err instanceof Error)) {
        return false;
    }
    const errorWithProcessFields = err;
    return (errorWithProcessFields.signal === 'SIGTERM' ||
        errorWithProcessFields.killed === true ||
        errorWithProcessFields.code === 'ETIMEDOUT' ||
        /timed out|timeout/i.test(err.message));
}
function commandOutput(err) {
    if (!(err instanceof Error)) {
        return String(err);
    }
    const errorWithOutput = err;
    return [errorWithOutput.stdout, errorWithOutput.stderr]
        .filter((chunk) => chunk !== undefined)
        .map((chunk) => String(chunk))
        .join('\n')
        .trim();
}
function runAssuranceGates(projectPath) {
    return {
        newCode: evaluateNewCodeGate(readChangedSpecLogicFiles(projectPath)),
    };
}
function readChangedSpecLogicFiles(projectPath) {
    let changedFiles;
    try {
        const output = execFileSync('git', [
            '-C',
            projectPath,
            'diff',
            '--name-only',
            'HEAD',
            '--',
            'src/tools/create-spec',
            'src/tools/validate.ts',
            'src/engine',
        ], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
        changedFiles = output
            .split('\n')
            .map((line) => line.trim())
            .filter((line) => line.endsWith('.ts'));
    }
    catch {
        return [];
    }
    return changedFiles.flatMap((path) => {
        try {
            return [{ path, content: readFileSync(`${projectPath}/${path}`, 'utf-8') }];
        }
        catch {
            return [];
        }
    });
}
function runLintCheck(projectPath, lintCommand) {
    if (lintCommand !== null && !isSafeCommand(lintCommand)) {
        console.warn(`[Planu] validate: lintCommand contains unsafe characters — skipping execution`);
        return {
            passed: true,
            command: lintCommand,
            issueCount: 0,
            output: '(skipped — unsafe command)',
        };
    }
    // SPEC-787: use pnpm lint (same as `pnpm lint` in package.json) to match what
    // the developer runs manually. The old `npx eslint src ... || true` never raised
    // an error (|| true suppressed exit code) and ran against a different scope.
    const command = lintCommand ?? 'pnpm lint';
    try {
        execSync(command, {
            cwd: projectPath,
            stdio: ['pipe', 'pipe', 'pipe'],
            timeout: LINT_CHECK_TIMEOUT_MS,
        });
        return { passed: true, command, issueCount: 0, output: '' };
    }
    catch (err) {
        if (isCommandTimeout(err)) {
            const seconds = Math.round(LINT_CHECK_TIMEOUT_MS / 1000);
            const output = `Lint command timed out after ${String(seconds)}s and was skipped so validate can complete within MCP request limits. ` +
                `Run \`${command}\` manually before marking the spec done.`;
            console.warn(`[Planu] lintCheck timed out: command="${command}" cwd="${projectPath}" timeoutMs=${String(LINT_CHECK_TIMEOUT_MS)}`);
            return { passed: true, command, issueCount: 0, output };
        }
        const raw = commandOutput(err);
        // SPEC-787: never truncate — preserve full output so caller sees real issues
        const output = raw.trim();
        const issueCount = parseLintIssueCount(output);
        // SPEC-787: log exact command + cwd for debuggability
        console.warn(`[Planu] lintCheck failed: command="${command}" cwd="${projectPath}" issues=${String(issueCount)}`);
        return { passed: false, command, issueCount, output };
    }
}
function parseLintIssueCount(output) {
    const m = /(\d+)\s+problem/i.exec(output);
    if (m) {
        return parseInt(m[1] ?? '0', 10);
    }
    return output.split('\n').filter((l) => l.trim().length > 0).length;
}
/** Check if planu/ directory has uncommitted changes via git status --porcelain */
function checkPlanuUncommittedChanges(projectPath) {
    try {
        const output = execFileSync('git', ['status', '--porcelain', 'planu/'], {
            cwd: projectPath,
            encoding: 'utf-8',
            timeout: 5_000,
            stdio: ['pipe', 'pipe', 'pipe'],
        });
        return output.trim().length > 0;
    }
    catch {
        return false;
    }
}
function scoreToGrade(score) {
    if (score >= 90) {
        return 'A';
    }
    if (score >= 80) {
        return 'B';
    }
    if (score >= 70) {
        return 'C';
    }
    if (score >= 60) {
        return 'D';
    }
    return 'F';
}
//# sourceMappingURL=validate.js.map