import { validateSpecFormat } from '../../core/spec-validator.js';
import { validateSpec } from '../../engine/validator.js';
import { generateDoD } from '../../engine/validator/dor-dod.js';
import { addLesson, getLessons } from '../../storage/lessons-store.js';
import { dispatchFeedbackEvent } from '../learn.js';
import { auditDeps } from '../../engine/dep-auditor/index.js';
import { getComplianceGateConfig } from '../../storage/compliance-gate-config-store.js';
import { readJson, writeJson, projectDataDir } from '../../storage/base-store.js';
import { randomUUID } from 'node:crypto';
import { withEscalation } from '../../engine/escalator/with-escalation.js';
import { writeImplementationReviewReport } from '../../engine/validator/validation-report-writer.js';
import { writeSpecReviewFeedback } from '../../engine/validator/spec-review-writer.js';
/**
 * SPEC-721 / SPEC-222 Trigger 1: Run validate engine before marking done.
 *
 * Fail-CLOSED: timeout, crash, or null score → blocked (not allowed through).
 * The only escape hatch is `forceStatus=true` with a `forceStatusReason` ≥100 chars.
 *
 * The timeout lives inside this function (Promise.race) so we own the reason string.
 * Callers must NOT wrap this with `withToolTimeout` — doing so would silently convert
 * timeout into `blocked: false` (the exact bypass this spec closes).
 *
 * @param spec            The spec being transitioned.
 * @param projectPath     Project root for the validate engine.
 * @param forceStatus     Whether the LLM explicitly wants to bypass the gate.
 * @param forceStatusReason  Why — required and ≥100 chars when forceStatus=true.
 * @param budgetMs        Max milliseconds to wait for validate. Default: 9000.
 */
export async function runValidateGate(spec, projectPath, forceStatus, forceStatusReason, budgetMs = 9_000, reviewContext) {
    // ---- Path A: explicit force bypass ----------------------------------------
    if (forceStatus) {
        if (!forceStatusReason || forceStatusReason.trim().length < 100) {
            return {
                blocked: true,
                score: null,
                reason: 'validate_gate_unreachable',
                errorMessage: 'forceStatus requires forceStatusReason (string ≥ 100 characters explaining why ' +
                    'the validate gate is being bypassed).',
            };
        }
        // Best-effort score for the audit record — do not block if validate is slow.
        let observedScore = null;
        try {
            const timeoutSignal = new Promise((resolve) => {
                setTimeout(() => {
                    resolve(null);
                }, Math.min(budgetMs, 5_000));
            });
            const r = await Promise.race([validateSpec(spec, projectPath), timeoutSignal]);
            if (r !== null && typeof r === 'object' && 'score' in r) {
                observedScore = typeof r.score === 'number' ? r.score : null;
            }
        }
        catch {
            /* swallow — forced bypass must never throw */
        }
        return { blocked: false, score: observedScore, forced: true, forcedReason: forceStatusReason };
    }
    // ---- Normal path — fail-CLOSED --------------------------------------------
    const TIMEOUT_SENTINEL = Symbol('timeout');
    let validateResult;
    try {
        validateResult = await Promise.race([
            validateSpec(spec, projectPath),
            new Promise((resolve) => {
                setTimeout(() => {
                    resolve(TIMEOUT_SENTINEL);
                }, budgetMs);
            }),
        ]);
    }
    catch (err) {
        // ---- Path C: crash -------------------------------------------------------
        return {
            blocked: true,
            score: null,
            reason: 'validate_gate_crash',
            errorMessage: (err instanceof Error ? err.message : String(err)).slice(0, 240),
        };
    }
    // ---- Path D: timeout -------------------------------------------------------
    if (validateResult === TIMEOUT_SENTINEL) {
        return {
            blocked: true,
            score: null,
            reason: 'validate_gate_timeout',
            errorMessage: `Validate gate timed out after ${String(budgetMs)}ms — could not produce a score. Add criteria evidence and retry, or use forceStatus:true with forceStatusReason explaining why.`,
        };
    }
    // ---- Path B: null score (no criteria) — also guard undefined from engines that omit the field
    const scoreValue = validateResult.score;
    if (scoreValue === null || scoreValue === undefined) {
        return {
            blocked: true,
            score: null,
            reason: 'validate_gate_no_criteria',
            errorMessage: 'Validate could not score this spec — no acceptance criteria found. Run check_readiness or add scenarios, then retry.',
        };
    }
    // ---- Normal score check — scoreValue is guaranteed number here (null/undefined filtered above) ---
    if (scoreValue < 100) {
        if (reviewContext) {
            await writeImplementationReviewReport({
                ...reviewContext,
                spec,
                projectPath,
                score: scoreValue,
            });
        }
        return {
            blocked: true,
            score: scoreValue,
            reason: 'validate_score_below_threshold',
        };
    }
    if (reviewContext) {
        await writeImplementationReviewReport({
            ...reviewContext,
            spec,
            projectPath,
            score: scoreValue,
        });
    }
    return { blocked: false, score: scoreValue, forced: false };
}
/**
 * Checks the DoD gate before transitioning to 'done'.
 * Returns an error ToolResult if blocked (unless force=true), null if passed.
 */
export async function checkDodGate(spec, specId, projectId, projectPath, _force) {
    const dod = await generateDoD(spec, undefined, undefined, projectPath);
    const requiredItems = dod.items.filter((i) => i.required);
    const blockingItems = requiredItems
        .filter((i) => i.status === 'failed')
        .map((i) => i.description);
    const warningItems = dod.items
        .filter((i) => !i.required && i.status !== 'passed')
        .map((i) => i.description);
    if (blockingItems.length === 0) {
        return null;
    }
    // SPEC-248: Auto-capture lesson when DoD gate blocks (fire-and-forget)
    if (projectPath) {
        void autoCaptureLessonDodGate(spec, specId, projectId, projectPath);
    }
    const humanMessage = `❌ DoD gate failed: ${blockingItems.slice(0, 3).join('; ')}${blockingItems.length > 3 ? ` (and ${blockingItems.length - 3} more)` : ''}`;
    const expertSummary = `DoD: ${dod.passedCount}/${dod.items.length} passed | Blocking: ${blockingItems.length} | Warnings: ${warningItems.length}`;
    const fixHints = blockingItems.map((item) => `💡 To fix: ${item}`);
    void dispatchFeedbackEvent(projectId, {
        type: 'checklist',
        specId,
        input: { gate: 'dod', specType: spec.type },
        outcome: { passed: false, failedItems: blockingItems },
        delta: { failedCount: blockingItems.length },
        timestamp: new Date().toISOString(),
        /* c8 ignore next */
    }).catch((err) => {
        console.error('[planu] DoD feedback event failed:', err);
    });
    const failedItems = requiredItems
        .filter((i) => i.status === 'failed')
        .map((i) => ({ id: i.id, description: i.description, category: i.category }));
    return {
        content: [
            {
                type: 'text',
                text: JSON.stringify({
                    error: 'DoD gate failed',
                    message: humanMessage,
                    expertSummary,
                    blockingItems,
                    failedItems,
                    warningItems,
                    fixHints,
                    gateResults: dod.gateResults ?? null,
                    hint: '💡 Alternatively: use force:true to bypass all gates',
                }, null, 2),
            },
        ],
        isError: true,
        structuredContent: {
            error: 'dod_gate_failed',
            code: 422,
            context: {
                specId,
                blockingItems,
                warningItems,
                passed: dod.passedCount,
                total: dod.items.length,
            },
            fixHint: blockingItems[0]
                ? `Fix: ${blockingItems[0]}. Use force:true to bypass all gates.`
                : 'Use force:true to bypass all gates.',
        },
    };
}
/**
 * SPEC-335: Security gate — blocks 'done' transition if critical CVEs are found.
 * Only runs when projectPath is available. Never throws — best-effort.
 * Returns an error ToolResult if blocked, null if passed or skipped.
 *
 * Exported for testing; callers should prefer checkDoneGates for the full gate suite.
 */
export async function checkSecurityGate(projectPath, specId, force) {
    if (!projectPath || force) {
        return null;
    }
    try {
        const report = await auditDeps(projectPath);
        const criticalVulns = report.critical.flatMap((entry) => entry.vulns
            .filter((v) => v.severity === 'critical')
            .map((v) => `${v.cveId} in ${entry.name}@${entry.currentVersion}`));
        if (criticalVulns.length === 0) {
            return null;
        }
        const cveList = criticalVulns.join(', ');
        const highVulns = report.critical.flatMap((entry) => entry.vulns.filter((v) => v.severity === 'high').map((v) => `${v.cveId} in ${entry.name}`));
        const highNote = highVulns.length > 0 ? `\n\nHigh severity (warning only): ${highVulns.join(', ')}` : '';
        return {
            content: [
                {
                    type: 'text',
                    text: `Security gate blocked: critical CVEs detected — ${cveList}.` +
                        ` Fix or upgrade affected packages before marking ${specId} as done.` +
                        ` Use force:true to bypass.${highNote}`,
                },
            ],
            isError: true,
        };
    }
    catch {
        /* best-effort — never block transition due to audit errors */
        return null;
    }
}
function forceDoneBypassLogPath(projectId) {
    return `${projectDataDir(projectId)}/force-bypass-log.json`;
}
/**
 * Writes an audit record when force=true bypasses DoD gates on 'done' transition.
 * Never throws — audit failures must not block transitions.
 */
async function recordForceDoneBypass(specId, projectId, failingItems, warningItems) {
    try {
        const logPath = forceDoneBypassLogPath(projectId);
        const entries = await readJson(logPath, []);
        entries.push({
            id: randomUUID(),
            timestamp: new Date().toISOString(),
            action: 'force_done_bypass',
            specId,
            projectId,
            failingItems,
            warningItems,
        });
        await writeJson(logPath, entries);
    }
    catch {
        /* best-effort — never block transition due to audit failures */
    }
}
/**
 * SPEC-725: Check the validation-report artifact gate before marking done.
 * SPEC-1050: Fail closed. A done transition requires a fresh validation-report
 * with reviewer evidence and all validation gates passing.
 */
export async function checkValidationReportGate(specId, projectId, force) {
    if (force) {
        return null;
    }
    try {
        const { readArtifact } = await import('../../engine/handoff-artifacts/io.js');
        const result = await readArtifact({ projectId, specId, kind: 'validation-report' });
        if (!result.ok) {
            const firstErr = result.errors[0];
            if (firstErr?.code === 'ARTIFACT_NOT_FOUND') {
                return validationReportGateError({
                    specId,
                    error: 'validation_report_missing',
                    message: 'No validation-report artifact exists for this spec. Run validate to generate implementation-review evidence before marking done.',
                    gates: [],
                    fixHint: 'Run validate for this spec, fix any failing gates, then retry update_status(done). Use force:true only with an audited reason.',
                });
            }
            return validationReportGateError({
                specId,
                error: 'validation_report_invalid',
                message: 'The validation-report artifact is malformed or uses an obsolete schema. Re-run validate so Planu can generate reviewer evidence.',
                gates: [],
                fixHint: 'Re-run validate for this spec. The report must include reviewer evidence and passing gates.',
            });
        }
        if (!result.payload.passed) {
            return validationReportGateError({
                specId,
                error: 'validation_report_failed',
                message: 'Validation report has passed:false — fix all failing gates before marking done.',
                gates: result.payload.gates,
                fixHint: 'Fix failing gates and re-run validate before marking done.',
            });
        }
        if (result.payload.reviewer.verdict !== 'approved' ||
            result.payload.reviewer.agent.trim().length === 0) {
            return validationReportGateError({
                specId,
                error: 'validation_report_reviewer_not_approved',
                message: 'The implementation reviewer did not approve this spec. Fix the requested changes and re-run validate.',
                gates: result.payload.gates,
                fixHint: 'Fix reviewer findings and re-run validate before marking done.',
            });
        }
        return null;
    }
    catch (err) {
        return validationReportGateError({
            specId,
            error: 'validation_report_unreadable',
            message: `Could not read validation-report artifact: ${err instanceof Error ? err.message : String(err)}`,
            gates: [],
            fixHint: 'Re-run validate for this spec, then retry update_status(done).',
        });
    }
}
/** SPEC-1051: Write spec-review evidence when a spec enters review. */
export async function writeSpecReviewArtifact(spec, specId, projectId) {
    const result = await writeSpecReviewFeedback({ projectId, specId, spec });
    if (result.written) {
        return null;
    }
    return specReviewGateError({
        specId,
        error: 'spec_review_write_failed',
        message: `Could not write spec review feedback: ${result.error ?? 'unknown error'}`,
        blockers: result.payload.blockers,
        fixHint: 'Fix the artifact store issue, then retry update_status(review).',
    });
}
/** SPEC-1051: Approval requires a dedicated spec reviewer artifact. */
export async function checkSpecReviewGate(specId, projectId, _forceApprove) {
    try {
        const { readArtifact } = await import('../../engine/handoff-artifacts/io.js');
        const result = await readArtifact({ projectId, specId, kind: 'review_feedback' });
        if (!result.ok) {
            const firstErr = result.errors[0];
            return specReviewGateError({
                specId,
                error: firstErr?.code === 'ARTIFACT_NOT_FOUND' ? 'spec_review_missing' : 'spec_review_invalid',
                message: firstErr?.code === 'ARTIFACT_NOT_FOUND'
                    ? 'No spec review feedback exists for this spec. Move it to review so planu-spec-reviewer can review it before approval.'
                    : 'Spec review feedback is malformed or uses an obsolete schema. Re-run update_status(review).',
                blockers: [],
                fixHint: firstErr?.code === 'ARTIFACT_NOT_FOUND'
                    ? 'Run update_status(review), resolve any review blockers, then retry update_status(approved).'
                    : 'Re-run update_status(review) to regenerate reviewer evidence, then retry approval.',
            });
        }
        const review = result.payload;
        if (review.verdict !== 'approved' || review.reviewer.verdict !== 'approved') {
            return specReviewGateError({
                specId,
                error: 'spec_review_changes_requested',
                message: 'Spec reviewer requested changes. Approval is blocked until the review passes.',
                blockers: review.blockers,
                fixHint: 'Resolve the spec review blockers, rerun update_status(review), then retry approval.',
            });
        }
        if (review.reviewer.kind !== 'spec-review-agent' ||
            review.reviewer.agent !== 'planu-spec-reviewer') {
            return specReviewGateError({
                specId,
                error: 'spec_review_wrong_reviewer',
                message: 'Approval requires a dedicated spec reviewer. The implementation reviewer cannot approve the spec plan.',
                blockers: [`Reviewer was ${review.reviewer.agent}. Expected planu-spec-reviewer.`],
                fixHint: 'Run update_status(review) so planu-spec-reviewer writes a fresh review artifact.',
            });
        }
        return null;
    }
    catch (err) {
        return specReviewGateError({
            specId,
            error: 'spec_review_unreadable',
            message: `Could not read spec review feedback: ${err instanceof Error ? err.message : String(err)}`,
            blockers: [],
            fixHint: 'Re-run update_status(review), then retry update_status(approved).',
        });
    }
}
function validationReportGateError(args) {
    const artifactPath = `external Planu project data: handoffs/${args.specId}/validation-report.json`;
    return {
        content: [
            {
                type: 'text',
                text: JSON.stringify({
                    error: args.error,
                    message: args.message,
                    artifactPath,
                    gates: args.gates,
                    fixHint: args.fixHint,
                }, null, 2),
            },
        ],
        isError: true,
        structuredContent: {
            error: args.error,
            code: 422,
            context: { specId: args.specId, artifactPath, gates: args.gates },
            fixHint: args.fixHint,
        },
    };
}
function specReviewGateError(args) {
    const artifactPath = `external Planu project data: handoffs/${args.specId}/review_feedback.md`;
    return {
        content: [
            {
                type: 'text',
                text: JSON.stringify({
                    error: args.error,
                    message: args.message,
                    artifactPath,
                    blockers: args.blockers,
                    fixHint: args.fixHint,
                }, null, 2),
            },
        ],
        isError: true,
        structuredContent: {
            error: args.error,
            code: 422,
            context: { specId: args.specId, artifactPath, blockers: args.blockers },
            fixHint: args.fixHint,
        },
    };
}
/**
 * SPEC-335: Combined done-gates runner — DoD + security.
 * When force=true, gates are not blocking but failing items are recorded in
 * the project's force-bypass-log.json for traceability (guard: done ≠ implemented).
 */
export async function checkDoneGates(spec, specId, projectId, projectPath, force) {
    if (!force) {
        const dodError = await checkDodGate(spec, specId, projectId, projectPath, false);
        if (dodError) {
            return { blocked: dodError, forcedBypassWarning: null };
        }
        const secError = await checkSecurityGate(projectPath, specId, false);
        if (secError) {
            return { blocked: secError, forcedBypassWarning: null };
        }
        return { blocked: null, forcedBypassWarning: null };
    }
    // force=true: run DoD in audit mode — collect results but do not block
    let forcedBypassWarning = null;
    try {
        const dod = await generateDoD(spec, undefined, undefined, projectPath);
        const failingItems = dod.items
            .filter((i) => i.required && i.status === 'failed')
            .map((i) => i.description);
        const warningItems = dod.items
            .filter((i) => !i.required && i.status !== 'passed')
            .map((i) => i.description);
        if (failingItems.length > 0) {
            // Record bypass in audit log (fire-and-forget)
            void recordForceDoneBypass(specId, projectId, failingItems, warningItems);
            forcedBypassWarning =
                `⚠️ force bypass: ${String(failingItems.length)} DoD item(s) were failing ` +
                    `(${failingItems.slice(0, 2).join('; ')}${failingItems.length > 2 ? '…' : ''}). ` +
                    `Recorded in force-bypass-log.json. Ensure real implementation follows.`;
        }
    }
    catch {
        /* best-effort — never block transition */
    }
    return { blocked: null, forcedBypassWarning };
}
/**
 * SPEC-447: Runs the compliance check when transitioning to 'review'.
 * Never blocks the transition if the compliance engine is unavailable or fails.
 */
export async function checkComplianceGate(specId, projectId, projectPath) {
    if (!projectPath) {
        return { skipped: true, score: null, issues: [], blocked: false };
    }
    try {
        const { scoreCompliance } = await import('../../engine/verifier/compliance-scorer.js');
        const result = await scoreCompliance(specId, projectPath);
        const score = result.score;
        const issues = result.criteriaDetails
            .filter((d) => d.status === 'unverified')
            .map((d) => d.criterion)
            .slice(0, 5);
        const config = await getComplianceGateConfig(projectId);
        const mode = config?.mode ?? 'soft';
        const threshold = config?.threshold ?? 90;
        const blocked = mode === 'hard' && score < threshold;
        return { skipped: false, score, issues, blocked };
    }
    catch {
        /* best-effort — never block transition if compliance check fails */
        return { skipped: true, score: null, issues: [], blocked: false };
    }
}
/** SPEC-642: Block done if QA gate has not run and passed. */
export async function checkQaGate(specId, projectPath, force) {
    if (force || !projectPath) {
        return null;
    }
    const { getGateState } = await import('../../storage/qa-gate-store.js');
    const state = await getGateState(projectPath, specId).catch(() => null);
    if (state?.lastResult?.passed === true) {
        return null;
    }
    const reason = state === null ? 'QA gate not run' : 'QA gate did not pass';
    return {
        content: [
            {
                type: 'text',
                text: `❌ ${reason} — run the QA checks first or use force:true to bypass`,
            },
        ],
        isError: true,
        structuredContent: {
            error: 'qa_gate_not_passed',
            code: 422,
            context: { specId, reason },
            fixHint: 'Run the QA gate and record the result: typecheck + test by default, or typecheck + test:coverage when the spec declares a coverage threshold. Then retry update_status(done). Or use force:true to bypass.',
        },
    };
}
/**
 * SPEC-716 / SPEC-780: Format gate for transition to 'approved'.
 *
 * - newStatus !== 'approved' → returns `{ blockResult: null, qualityWarnings: [] }` (gate inert)
 * - validateSpecFormat returns valid → `{ blockResult: null, qualityWarnings: [] }`
 * - validateSpecFormat returns invalid AND `forceApprove !== true` → `{ blockResult: <SPEC_FORMAT_INVALID>, qualityWarnings: [] }` (block, legacy behavior)
 * - validateSpecFormat returns invalid AND `forceApprove === true` → `{ blockResult: null, qualityWarnings: <serialized errors> }` (proceed; warnings written to spec frontmatter by caller, mirroring SPEC-769 readiness gate)
 *
 * Best-effort: never throws — gate is skipped on unexpected errors.
 */
export async function checkApprovedFormatGate(spec, newStatus, forceApprove = false) {
    if (newStatus !== 'approved') {
        return { blockResult: null, qualityWarnings: [] };
    }
    let result;
    try {
        result = await validateSpecFormat(spec, {
            readinessMode: 'strict',
            bypassReason: forceApprove ? 'forceApprove' : undefined,
        });
    }
    catch {
        /* best-effort — never block transition due to unexpected errors */
        return { blockResult: null, qualityWarnings: [] };
    }
    if (result.valid) {
        // If it was bypassed, the errors are now in warnings
        const qualityWarnings = result.warnings
            .filter((w) => w.message.includes('[BYPASSED: forceApprove]'))
            .map((w) => `[${w.code}] ${w.message.replace('[BYPASSED: forceApprove] ', '')} (path: ${w.path})`);
        return { blockResult: null, qualityWarnings };
    }
    // forceApprove not set → block as before (SPEC-716 legacy contract).
    const blockResult = {
        content: [
            {
                type: 'text',
                text: JSON.stringify({
                    error: 'SPEC_FORMAT_INVALID',
                    message: `Spec format validation failed with ${String(result.errors.length)} error(s) — fix before approving, or pass forceApprove: true to bypass with warnings.`,
                    errors: result.errors,
                    warnings: result.warnings,
                }, null, 2),
            },
        ],
        isError: true,
        structuredContent: {
            error: 'SPEC_FORMAT_INVALID',
            code: 422,
            context: {
                specId: spec.id,
                errors: result.errors,
                warnings: result.warnings,
            },
            fixHint: result.errors[0]
                ? `Fix: ${result.errors[0].message}. Path: ${result.errors[0].path}. Or pass forceApprove: true.`
                : 'Fix all format errors before approving, or pass forceApprove: true to bypass with warnings.',
        },
    };
    return { blockResult, qualityWarnings: [] };
}
async function autoCaptureLessonDodGate(spec, specId, projectId, projectPath) {
    try {
        const existing = await getLessons(projectPath, {
            specId,
            category: 'testing',
            tags: ['auto-captured', 'dod-gate'],
        });
        if (existing.length > 0) {
            return;
        }
        await addLesson(projectPath, {
            projectId,
            specId,
            category: 'testing',
            severity: 'high',
            title: `DoD gate blocked for ${spec.title}`,
            description: 'Required DoD items were not passing when attempting to mark spec as done',
            prevention: 'Ensure tests pass and acceptance criteria are met before marking done',
            tags: [...spec.tags, 'auto-captured', 'dod-gate'],
        });
    }
    catch {
        /* best-effort — never throw */
    }
}
// ---------------------------------------------------------------------------
// SPEC-729: Validate gate wrapped with withEscalation
// ---------------------------------------------------------------------------
/**
 * Run the validate gate wrapped in withEscalation for bounded retries.
 *
 * On budget exhaustion, returns an escalation sentinel with decision='retry'|'abort'.
 * The caller is responsible for surfacing the decision as interactiveQuestions.
 *
 * @param spec            - The spec being transitioned.
 * @param projectPath     - Project root for the validate engine.
 * @param projectId       - Project ID for retry counter persistence.
 * @param specId          - Spec ID for retry counter keying.
 * @param frontmatterSha  - Frontmatter SHA for retry counter keying.
 * @param forceStatus     - Whether to bypass the gate.
 * @param forceStatusReason - Required when forceStatus=true.
 * @param budgetMs        - Max milliseconds for validate.
 */
export async function runValidateGateWithEscalation(spec, projectPath, projectId, specId, frontmatterSha, forceStatus, forceStatusReason, budgetMs) {
    return withEscalation({ role: 'validator', specId, frontmatterSha, projectId }, async () => {
        const gateResult = await runValidateGate(spec, projectPath, forceStatus, forceStatusReason, budgetMs);
        if (gateResult.blocked) {
            return {
                ok: false,
                reason: gateResult.reason,
            };
        }
        return { ok: true, result: gateResult };
    });
}
//# sourceMappingURL=dod-gates.js.map