// engine/cascade-hooks/state-drift-detector.ts — SPEC-776
// Detects drift between in-memory spec store and canonical Planu state files.
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { specStore } from '../../storage/index.js';
import { hashProjectPath, projectDataDir } from '../../storage/base-store.js';
function externalStatusPath(projectPath) {
    return join(projectDataDir(hashProjectPath(projectPath)), 'status.json');
}
/** Read and parse external status counts; returns null if missing or invalid. */
async function readStatusJsonDoneCounts(projectPath) {
    try {
        const raw = await readFile(externalStatusPath(projectPath), 'utf-8');
        const parsed = JSON.parse(raw);
        if (parsed === null || typeof parsed !== 'object') {
            return null;
        }
        const byStatus = parsed.byStatus;
        if (byStatus === null || typeof byStatus !== 'object') {
            return null;
        }
        const doneVal = byStatus.done;
        return typeof doneVal === 'number' ? doneVal : null;
    }
    catch {
        return null;
    }
}
/** Returns true if external status.json exists (even if counts are missing). */
async function statusJsonExists(projectPath) {
    try {
        await readFile(externalStatusPath(projectPath), 'utf-8');
        return true;
    }
    catch {
        return false;
    }
}
/** Read planu/session-context.md; returns null if missing. */
async function readSessionContextMd(projectPath) {
    try {
        return await readFile(join(projectPath, 'planu', 'session-context.md'), 'utf-8');
    }
    catch {
        return null;
    }
}
/**
 * Extract the last completed spec ID mentioned in "Recently Completed" section
 * of session-context.md. Returns null if not parseable.
 */
function extractLastDoneSpecFromContext(content) {
    const sectionMatch = /##\s*Recently Completed[\s\S]*?(?=##|$)/i.exec(content);
    if (!sectionMatch) {
        return null;
    }
    const section = sectionMatch[0];
    // Match lines like: | SPEC-NNN | ... or - SPEC-NNN ...
    const idMatch = /SPEC-\d+/i.exec(section);
    return idMatch ? idMatch[0] : null;
}
/**
 * SPEC-776: Compare in-memory spec counts vs external status.json and
 * recently-done spec vs planu/session-context.md.
 * Returns a DriftReport with any detected mismatches.
 */
export async function verifyStateFiles(projectPath) {
    const alerts = [];
    const projectId = hashProjectPath(projectPath);
    let specs;
    try {
        specs = await specStore.listSpecs(projectId);
    }
    catch {
        return { drifted: false, alerts: [] };
    }
    const expectedDoneCount = specs.filter((s) => s.status === 'done').length;
    // --- Check external status.json ---
    const exists = await statusJsonExists(projectPath);
    if (!exists) {
        alerts.push({
            kind: 'state_drift',
            message: 'state_drift: external status.json missing or unreadable',
            severity: 'warning',
            fix: 'reconcile_status_json(projectPath)',
        });
    }
    else {
        const foundDoneCount = await readStatusJsonDoneCounts(projectPath);
        if (foundDoneCount !== expectedDoneCount) {
            alerts.push({
                kind: 'state_drift',
                message: `state_drift: status.json out of sync (expected ${String(expectedDoneCount)} done, found ${String(foundDoneCount ?? 0)})`,
                severity: 'warning',
                fix: 'reconcile_status_json(projectPath)',
            });
        }
    }
    // --- Check planu/session-context.md ---
    const sessionMd = await readSessionContextMd(projectPath);
    if (sessionMd === null) {
        alerts.push({
            kind: 'state_drift',
            message: 'state_drift: planu/session-context.md missing',
            severity: 'warning',
            fix: 'reconcile_status_json(projectPath)',
        });
    }
    else {
        const lastDoneSpec = specs
            .filter((s) => s.status === 'done')
            .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))[0];
        if (lastDoneSpec !== undefined) {
            const mentionedId = extractLastDoneSpecFromContext(sessionMd);
            if (mentionedId !== null && mentionedId !== lastDoneSpec.id) {
                alerts.push({
                    kind: 'state_drift',
                    message: `state_drift: session-context.md last completed spec is ${mentionedId}, but actual most-recent done spec is ${lastDoneSpec.id}`,
                    severity: 'warning',
                    fix: 'reconcile_status_json(projectPath)',
                });
            }
        }
    }
    return { drifted: alerts.length > 0, alerts };
}
//# sourceMappingURL=state-drift-detector.js.map