// tools/update-status/transition-guard.ts — Valid state transitions and DoR gate
import { readFile } from 'node:fs/promises';
import { ti } from '../../i18n/index.js';
import { validateDoR } from '../../engine/dor-dod.js';
import { dispatchFeedbackEvent } from '../learn.js';
import { scoreAmbiguityFromPath } from '../../engine/ambiguity-scorer.js';
import { checkReadinessInternal } from '../../engine/readiness-checker.js';
import { validateEnglishOnlySpecText } from '../../engine/spec-language/english-only.js';
import { checkGroundedSpecContract } from '../../engine/spec-grounding/contract.js';
import { checkGenericSpecOutput } from '../../engine/spec-quality/generic-output-gate.js';
/**
 * Valid state transitions for spec lifecycle.
 * draft -> review -> approved -> implementing -> done
 * Also allow: review -> draft (send back), approved -> review (reconsider)
 *
 * SPEC-733: Terminal states (done, discarded) now support reverse transitions
 * with a mandatory reason. See REVERSE_TRANSITIONS.
 */
export const VALID_TRANSITIONS = {
    draft: ['review', 'discarded'],
    review: ['draft', 'approved', 'discarded'],
    approved: ['review', 'implementing', 'discarded'],
    implementing: ['approved', 'done', 'discarded'],
    done: ['implementing'], // SPEC-733: reverse transition with mandatory reason
    discarded: ['draft'], // SPEC-733: reverse transition with mandatory reason
};
// ---------------------------------------------------------------------------
// SPEC-733: Reverse transitions
// ---------------------------------------------------------------------------
/**
 * Allowed reverse transition pairs (from terminal state back to active).
 * Only these two pairs are supported. Any other reverse pair is rejected.
 */
export const REVERSE_TRANSITIONS = [
    ['done', 'implementing'],
    ['discarded', 'draft'],
];
/**
 * Returns true if the (from, to) pair is a whitelisted reverse transition.
 */
export function isReverseTransition(from, to) {
    return REVERSE_TRANSITIONS.some(([f, t]) => f === from && t === to);
}
/**
 * Validates that a reverse transition has the required reason.
 *
 * @returns `{ ok: true }` when valid, or `{ ok: false, error, fixHint }` when invalid.
 */
export function validateReverseTransition(input) {
    const { from, to, reason } = input;
    if (!isReverseTransition(from, to)) {
        return {
            ok: false,
            error: `Reverse transition ${from} → ${to} is not supported. Allowed: done→implementing, discarded→draft.`,
            fixHint: 'Only done→implementing and discarded→draft are supported reverse transitions.',
        };
    }
    if (!reason || reason.trim().length < 30) {
        return {
            ok: false,
            error: `Reverse transition ${from} → ${to} requires reason (≥ 30 chars). Received: "${reason ?? ''}" (${String(reason?.trim().length ?? 0)} chars).`,
            fixHint: 'Reverse transitions require reason (>= 30 chars). Example: "Regression found in prod traced to AC#3 — reopening to fix."',
        };
    }
    return { ok: true };
}
/**
 * Forward lifecycle order (no backward transitions here).
 * Used to resolve auto-advance intermediate steps (SPEC-280).
 */
const LIFECYCLE_ORDER = ['draft', 'review', 'approved', 'implementing', 'done'];
/**
 * SPEC-280: Returns the intermediate states to silently traverse when the
 * caller requests a forward jump (e.g. draft → approved skips review).
 *
 * Returns an array of intermediate statuses to execute before the target,
 * or null when no auto-advance is needed (direct transition or backward).
 */
export function resolveAutoAdvanceSteps(currentStatus, targetStatus) {
    const currentIdx = LIFECYCLE_ORDER.indexOf(currentStatus);
    const targetIdx = LIFECYCLE_ORDER.indexOf(targetStatus);
    if (currentIdx === -1 || targetIdx === -1) {
        return null;
    } // not in forward chain
    if (targetIdx <= currentIdx + 1) {
        return null;
    } // direct or backward — no intermediates
    return LIFECYCLE_ORDER.slice(currentIdx + 1, targetIdx);
}
/**
 * Validates that a status transition is allowed.
 * Returns an error ToolResult if invalid, null if valid.
 */
export function checkTransition(currentStatus, newStatus) {
    const validNextStates = VALID_TRANSITIONS[currentStatus];
    if (!validNextStates.includes(newStatus)) {
        return {
            content: [
                {
                    type: 'text',
                    text: ti('spec.invalidTransition', {
                        from: currentStatus,
                        to: newStatus,
                    }),
                },
            ],
            isError: true,
        };
    }
    return null;
}
/**
 * Validates DoR gate before transitioning to 'implementing'.
 * Returns an error ToolResult if blocked, null if passed.
 */
export function checkDorGate(spec, specId, projectId, newStatus) {
    if (newStatus !== 'implementing') {
        return null;
    }
    const dorResult = validateDoR(spec);
    if (!dorResult.isValid) {
        void dispatchFeedbackEvent(projectId, {
            type: 'checklist',
            specId,
            input: { gate: 'dor', specType: spec.type },
            outcome: { passed: false, failedItems: dorResult.blockingItems },
            delta: { failedCount: dorResult.blockingItems.length },
            timestamp: new Date().toISOString(),
        }).catch((err) => {
            console.error('[planu] DoR feedback event failed:', err);
        });
        return {
            content: [
                {
                    type: 'text',
                    text: JSON.stringify({
                        error: 'DoR gate failed',
                        message: dorResult.humanMessage,
                        expertSummary: dorResult.expertSummary,
                        blockingItems: dorResult.blockingItems,
                        warningItems: dorResult.warningItems,
                    }, null, 2),
                },
            ],
            isError: true,
        };
    }
    return null;
}
/**
 * SPEC-632: Block transition to 'approved' if ambiguity score < 70.
 * Best-effort: if spec file cannot be read, gate is skipped.
 */
export async function checkAmbiguityGate(spec, newStatus) {
    if (newStatus !== 'approved' || !spec.specPath) {
        return null;
    }
    const report = await scoreAmbiguityFromPath(spec.specPath);
    if (!report || report.score >= 70) {
        return null;
    }
    return {
        content: [
            {
                type: 'text',
                text: JSON.stringify({
                    error: 'Ambiguity gate failed',
                    message: `Ambiguity score ${String(report.score)}/100 — below 70. Fix vague terms in criteria before approving.`,
                    ambiguityScore: report.score,
                    violations: report.violations.slice(0, 5),
                    blockers: report.blockers.slice(0, 5),
                }, null, 2),
            },
        ],
        isError: true,
    };
}
/**
 * SPEC-769/SPEC-492: Readiness gate — block 'review'/'approved' when spec
 * quality is too low or the persisted spec document is not English.
 * Best-effort: if spec.md cannot be read, the gate is skipped (returns no block).
 *
 * - 0 criteria → InteractiveQuestion (hard block, no force path)
 * - score < 70 on approved && !forceApprove → InteractiveQuestion with 3 options
 * - score < 70 on approved && forceApprove → proceed, qualityWarnings populated
 * - score < 70 on review → block and keep draft
 * - non-English prose → hard block
 * - score >= 70 → proceed, no warnings
 */
export async function checkReadinessGate(spec, newStatus, forceApprove) {
    if ((newStatus !== 'review' && newStatus !== 'approved') || !spec.specPath) {
        return { blockResult: null, qualityWarnings: [] };
    }
    let body;
    try {
        body = await readFile(spec.specPath, 'utf-8');
    }
    catch {
        // Best-effort — if file is unreadable, skip gate
        return { blockResult: null, qualityWarnings: [] };
    }
    // Guard: mock environments return undefined; unreadable files return non-string
    if (typeof body !== 'string') {
        return { blockResult: null, qualityWarnings: [] };
    }
    const languageValidation = validateEnglishOnlySpecText(body);
    if (!languageValidation.ok) {
        return {
            blockResult: {
                content: [
                    {
                        type: 'text',
                        text: `English-only spec gate blocked transition to ${newStatus}.\n\n` +
                            `${languageValidation.reason ?? 'Non-English prose detected.'}\n\n` +
                            'Rewrite spec.md in English before moving it forward.',
                    },
                ],
                isError: true,
                structuredContent: {
                    error: 'SPEC_LANGUAGE_GATE_BLOCKED',
                    detectedLanguage: languageValidation.detectedLanguage,
                    signals: languageValidation.signals,
                    fixHint: 'Rewrite spec.md in English before review or approval.',
                },
            },
            qualityWarnings: [],
        };
    }
    const groundingResult = checkGroundedSpecContract(spec, body);
    if (!groundingResult.passed) {
        return {
            blockResult: {
                content: [
                    {
                        type: 'text',
                        text: `Grounded spec contract blocked transition to ${newStatus}. ` +
                            groundingResult.issues.slice(0, 3).join('; '),
                    },
                ],
                isError: true,
                structuredContent: {
                    error: 'GROUNDED_SPEC_CONTRACT_BLOCKED',
                    sddConstitutionRuleId: 'sdd.grounded-contract',
                    issues: groundingResult.issues,
                    fixHint: 'Add grounding metadata for every contract criterion or move ungrounded criteria to advisory/discovery before review or approval.',
                },
            },
            qualityWarnings: [],
        };
    }
    const genericOutputGate = checkGenericSpecOutput(body);
    if (!genericOutputGate.passed) {
        return {
            blockResult: {
                content: [
                    {
                        type: 'text',
                        text: `Spec quality gate blocked transition to ${newStatus}. ` +
                            genericOutputGate.issues
                                .slice(0, 3)
                                .map((issue) => `${issue.phrase}: ${issue.reason}`)
                                .join('; '),
                    },
                ],
                isError: true,
                structuredContent: {
                    error: 'GENERIC_SPEC_OUTPUT_BLOCKED',
                    sddConstitutionRuleId: 'sdd.no-generic-output',
                    issues: genericOutputGate.issues,
                    fixHint: 'Replace generic criteria or placeholder references with grounded, testable behavior before review or approval.',
                },
            },
            qualityWarnings: [],
        };
    }
    let score;
    let warnings;
    let criteriaCount;
    try {
        ({ score, warnings, criteriaCount } = checkReadinessInternal(body));
    }
    catch {
        return { blockResult: null, qualityWarnings: [] };
    }
    if (criteriaCount === 0) {
        if (newStatus === 'review') {
            return {
                blockResult: {
                    content: [
                        {
                            type: 'text',
                            text: 'Readiness gate blocked transition to review: spec has 0 acceptance criteria.',
                        },
                    ],
                    isError: true,
                    structuredContent: {
                        error: 'READINESS_GATE_BLOCKED',
                        score,
                        threshold: 70,
                        warnings,
                        fixHint: 'Add at least 3 testable acceptance criteria before moving to review.',
                    },
                },
                qualityWarnings: [],
            };
        }
        const question = {
            header: 'Spec incomplete',
            question: 'This spec has 0 acceptance criteria. Add criteria before approving — transition blocked.',
            multiSelect: false,
            options: [
                {
                    label: 'cancel',
                    description: 'Keep current status and complete the spec first (recommended)',
                },
                {
                    label: 'return_to_draft',
                    description: 'Return spec to draft to add acceptance criteria',
                },
            ],
        };
        return {
            blockResult: {
                content: [
                    {
                        type: 'text',
                        text: `Readiness gate: spec has 0 acceptance criteria — cannot approve an empty spec.`,
                    },
                    {
                        type: 'text',
                        text: '⚡ INTERACTIVE — call AskUserQuestion now with the interactiveQuestions from structuredContent',
                    },
                ],
                structuredContent: {
                    interactiveQuestions: [question],
                },
            },
            qualityWarnings: [],
        };
    }
    if (score >= 70) {
        return { blockResult: null, qualityWarnings: [] };
    }
    if (newStatus === 'review') {
        const warningsSummary = warnings.slice(0, 3).join('; ');
        return {
            blockResult: {
                content: [
                    {
                        type: 'text',
                        text: `Readiness gate blocked transition to review: score ${String(score)}/100 — below 70 threshold. ` +
                            `Warnings: ${warningsSummary}`,
                    },
                ],
                isError: true,
                structuredContent: {
                    error: 'READINESS_GATE_BLOCKED',
                    score,
                    threshold: 70,
                    warnings,
                    fixHint: 'Complete the spec before moving from draft to review.',
                },
            },
            qualityWarnings: [],
        };
    }
    // Score < 70
    if (forceApprove) {
        // Caller wants to force through — proceed with warnings attached
        return { blockResult: null, qualityWarnings: warnings };
    }
    const warningsSummary = warnings.slice(0, 3).join('; ');
    const question = {
        header: 'Readiness gate',
        question: `Spec readiness score ${String(score)}/100 (threshold: 70). How to proceed?`,
        multiSelect: false,
        options: [
            {
                label: 'return_to_draft',
                description: 'Keep current status — fix the spec and re-approve (recommended)',
            },
            {
                label: 'view_missing',
                description: `View missing items: ${warningsSummary}`,
            },
            {
                label: 'force_approve',
                description: 'Force-approve anyway — quality warnings will be stored in spec.md frontmatter. Re-call update_status(approved, forceApprove: true).',
            },
        ],
    };
    return {
        blockResult: {
            content: [
                {
                    type: 'text',
                    text: `Readiness gate: score ${String(score)}/100 — below 70 threshold. Warnings: ${warningsSummary}`,
                },
                {
                    type: 'text',
                    text: '⚡ INTERACTIVE — call AskUserQuestion now with the interactiveQuestions from structuredContent',
                },
            ],
            structuredContent: {
                interactiveQuestions: [question],
            },
        },
        qualityWarnings: [],
    };
}
/**
 * SPEC-964: Challenge gate — block 'review' if challenge_spec was never run
 * or did not generate enough addressed scenarios.
 *
 * - Requires challengeReport to exist on the spec
 * - Requires at least 3 scenarios addressed
 * - High-risk specs (high/critical) require all 5 focus areas
 */
export function checkChallengeGate(spec, newStatus) {
    if (newStatus !== 'review') {
        return null;
    }
    const report = spec.challengeReport;
    if (!report) {
        return {
            content: [
                {
                    type: 'text',
                    text: `Challenge gate blocked: spec has not been stress-tested. Run challenge_spec(specId="${spec.id}") before transitioning to review.`,
                },
            ],
            isError: true,
            structuredContent: {
                error: 'CHALLENGE_GATE_BLOCKED',
                code: 'MISSING_CHALLENGE_REPORT',
                fixHint: `Run challenge_spec(specId="${spec.id}") to generate failure scenarios and validate resilience.`,
            },
        };
    }
    if (report.addressedCount < 3) {
        return {
            content: [
                {
                    type: 'text',
                    text: `Challenge gate blocked: only ${String(report.addressedCount)} scenario(s) addressed (minimum 3 required). Re-run challenge_spec(specId="${spec.id}") with broader focus areas.`,
                },
            ],
            isError: true,
            structuredContent: {
                error: 'CHALLENGE_GATE_BLOCKED',
                code: 'INSUFFICIENT_CHALLENGES',
                addressedCount: report.addressedCount,
                requiredCount: 3,
                fixHint: `Run challenge_spec(specId="${spec.id}") to generate more failure scenarios.`,
            },
        };
    }
    const isHighRisk = spec.risk === 'high' || spec.risk === 'critical';
    const allFocusAreas = ['failures', 'concurrency', 'scale', 'security', 'data-consistency'];
    const missingFocus = allFocusAreas.filter((f) => !report.focusAreas.includes(f));
    if (isHighRisk && missingFocus.length > 0) {
        return {
            content: [
                {
                    type: 'text',
                    text: `Challenge gate blocked: high-risk spec must address all focus areas. Missing: ${missingFocus.join(', ')}. Run challenge_spec(specId="${spec.id}", focus=[${allFocusAreas.map((f) => `"${f}"`).join(', ')}]).`,
                },
            ],
            isError: true,
            structuredContent: {
                error: 'CHALLENGE_GATE_BLOCKED',
                code: 'HIGH_RISK_MISSING_FOCUS',
                missingFocusAreas: missingFocus,
                fixHint: `Run challenge_spec(specId="${spec.id}", focus=[${allFocusAreas.map((f) => `"${f}"`).join(', ')}])`,
            },
        };
    }
    return null;
}
//# sourceMappingURL=transition-guard.js.map