// engine/sdd-model-routing.ts — Hard gates for SDD model tiers and context continuity.
import { createHash } from 'node:crypto';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { classifyModelTier } from './model-tier-resolver.js';
const FORCE_REASON_MIN_LENGTH = 100;
export function requiredTierForPhase(phase) {
    if (phase === 'analysis' || phase === 'arbitration') {
        return 'max';
    }
    if (phase === 'review') {
        return 'review';
    }
    return 'implementation';
}
export function phaseForTransition(status) {
    if (status === 'approved') {
        return 'analysis';
    }
    if (status === 'implementing') {
        return 'implementation';
    }
    if (status === 'done') {
        return 'arbitration';
    }
    return null;
}
export function classifySddModelTier(modelId) {
    if (!modelId) {
        return 'unknown';
    }
    const id = modelId.toLowerCase();
    if (/opus|ultra|o1|o3|gemini.*(?:ultra|pro)|gpt-5(?:[._-]?5)?(?!.*(?:mini|nano|small|flash))/.test(id)) {
        return 'max';
    }
    const modelTier = classifyModelTier(modelId);
    if (modelTier === 'haiku') {
        return 'implementation';
    }
    if (modelTier === 'opus') {
        return 'max';
    }
    return 'review';
}
function hasTierEvidence(params, required) {
    if (params.modelTierUsed === required) {
        return true;
    }
    const classified = classifySddModelTier(params.modelId);
    if (required === 'max') {
        return classified === 'max';
    }
    if (required === 'implementation') {
        return classified === 'implementation';
    }
    return classified === 'review' || classified === 'max';
}
function forceReason(params) {
    const reason = params.forceStatusReason ?? params.reason ?? '';
    return reason.trim().length >= FORCE_REASON_MIN_LENGTH ? reason.trim() : null;
}
function canForce(params, status) {
    const forceFlag = status === 'approved'
        ? params.forceApprove === true
        : status === 'done'
            ? params.forceStatus === true || params.force === true
            : params.force === true;
    return forceFlag && forceReason(params) !== null;
}
function buildBlock(status, blockers, fixHint) {
    return {
        content: [
            {
                type: 'text',
                text: JSON.stringify({
                    error: 'SDD_MODEL_ROUTING_GATE_FAILED',
                    status,
                    blockers,
                    fixHint,
                }, null, 2),
            },
        ],
        isError: true,
        structuredContent: {
            error: 'SDD_MODEL_ROUTING_GATE_FAILED',
            status,
            blockers,
            fixHint,
        },
    };
}
export async function hashContextPackage(input) {
    const parts = [];
    const sessionContextPath = join(input.projectPath, 'planu', 'session-context.md');
    parts.push(await readFile(sessionContextPath, 'utf-8'));
    if (input.handoffPath) {
        parts.push(await readFile(input.handoffPath, 'utf-8'));
    }
    return createHash('sha256').update(parts.join('\n---HANDOFF---\n'), 'utf8').digest('hex');
}
async function contextCanBeReconstructed(params) {
    const blockers = [];
    if (!params.projectPath) {
        blockers.push('projectPath is required to reconstruct planu/session-context.md.');
        return blockers;
    }
    const sessionContextPath = join(params.projectPath, 'planu', 'session-context.md');
    try {
        const raw = await readFile(sessionContextPath, 'utf-8');
        if (raw.trim().length < 80) {
            blockers.push('planu/session-context.md exists but is too small to reconstruct context.');
        }
    }
    catch {
        blockers.push('planu/session-context.md is missing or unreadable.');
    }
    if (params.handoffPath) {
        try {
            const raw = await readFile(params.handoffPath, 'utf-8');
            if (raw.trim().length < 200) {
                blockers.push('handoffPath exists but is too small to be an operational package.');
            }
        }
        catch {
            blockers.push('handoffPath is missing or unreadable.');
        }
    }
    else if (!params.handoffArtifactId) {
        blockers.push('handoffPath or handoffArtifactId is required.');
    }
    return blockers;
}
export async function checkSddModelRoutingGate(input) {
    const { params, status, projectPath } = input;
    const phase = phaseForTransition(status);
    const gateResults = {};
    const forcedReasons = [];
    if (phase === null) {
        return { blockResult: null, gateResults: { sddModelRouting: 'skip' }, forcedReasons };
    }
    const requiredTier = requiredTierForPhase(phase);
    const blockers = [];
    if (!hasTierEvidence(params, requiredTier)) {
        blockers.push(`Transition to ${status} requires model tier "${requiredTier}" evidence. Received modelTierUsed="${params.modelTierUsed ?? 'missing'}", modelId="${params.modelId ?? 'missing'}".`);
    }
    if (status === 'implementing' || status === 'done') {
        if (!params.contextHash) {
            blockers.push('contextHash is required so the next agent can verify context continuity.');
        }
        blockers.push(...(await contextCanBeReconstructed({
            projectPath,
            handoffPath: params.handoffPath,
            handoffArtifactId: params.handoffArtifactId,
        })));
    }
    if (status === 'done') {
        if (!params.reviewedBy || params.reviewedBy.trim().length === 0) {
            blockers.push('reviewedBy is required before done.');
        }
        if (!params.arbitratedBy || params.arbitratedBy.trim().length === 0) {
            blockers.push('arbitratedBy is required before done.');
        }
        if (params.reconcileRequired === true) {
            blockers.push('reconcileRequired=true blocks done until reconcile_spec records the intentional drift.');
        }
    }
    if (blockers.length === 0) {
        gateResults.sddModelRouting = 'pass';
        return { blockResult: null, gateResults, forcedReasons };
    }
    if (canForce(params, status)) {
        gateResults.sddModelRouting = 'forced';
        forcedReasons.push(forceReason(params) ?? 'Forced SDD model/context gate');
        return { blockResult: null, gateResults, forcedReasons };
    }
    gateResults.sddModelRouting = 'fail';
    return {
        blockResult: buildBlock(status, blockers, 'Provide modelTierUsed/modelId evidence plus package_handoff context evidence, or force with a >=100 character audited reason.'),
        gateResults,
        forcedReasons,
    };
}
export function buildTransitionEvidenceMeta(params) {
    return {
        ...(params.modelTierUsed !== undefined && { modelTierUsed: params.modelTierUsed }),
        ...(params.modelId !== undefined && { modelId: params.modelId }),
        ...(params.contextHash !== undefined && { contextHash: params.contextHash }),
        ...(params.handoffPath !== undefined && { handoffPath: params.handoffPath }),
        ...(params.handoffArtifactId !== undefined && { handoffArtifactId: params.handoffArtifactId }),
        ...(params.reviewedBy !== undefined && { reviewedBy: params.reviewedBy }),
        ...(params.arbitratedBy !== undefined && { arbitratedBy: params.arbitratedBy }),
        ...(params.reconcileRequired !== undefined && { reconcileRequired: params.reconcileRequired }),
    };
}
//# sourceMappingURL=sdd-model-routing.js.map