// engine/drift-detection/release-status-drift.ts — SPEC-1012
// Detects specs that were released on main but still have non-terminal status.
import { execFile as execFileCb } from 'node:child_process';
import { readdir, readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { promisify } from 'node:util';
import { parseSpecIdsFromCommitMessage } from '../release-pipeline/parse-spec-refs.js';
import { closeSpecsOnReleasePublish } from '../release-pipeline/release-spec-closer.js';
const execFile = promisify(execFileCb);
const TERMINAL_STATUSES = new Set(['done', 'discarded']);
// Require both `chore(release):` (or `chore:`) AND a `vX.Y.Z` sigil so plain
// chores like `chore: refactor SPEC-200 helpers` do NOT trigger auto-close.
// Pairs with the tag-points-at gate in `applyReleaseStatusDriftFix`.
const RELEASE_COMMIT_RE = /^chore(\(release\))?:.*\bv\d+\.\d+\.\d+\b/i;
const MAX_COMMITS = 50;
// ---------------------------------------------------------------------------
// Git helpers
// ---------------------------------------------------------------------------
async function fetchMainLog(projectPath) {
    let stdout;
    try {
        const result = await execFile('git', ['log', 'origin/main', `--max-count=${String(MAX_COMMITS)}`, '--format=%H|%s|%ai'], { cwd: projectPath });
        stdout = result.stdout;
    }
    catch {
        // Try without origin/ prefix for repos without a remote
        try {
            const result = await execFile('git', ['log', 'main', `--max-count=${String(MAX_COMMITS)}`, '--format=%H|%s|%ai'], { cwd: projectPath });
            stdout = result.stdout;
        }
        catch {
            return [];
        }
    }
    return stdout
        .trim()
        .split('\n')
        .filter(Boolean)
        .map((line) => {
        const parts = line.split('|');
        return {
            sha: parts[0]?.trim() ?? '',
            subject: parts[1]?.trim() ?? '',
            authorDate: parts[2]?.trim() ?? '',
        };
    })
        .filter((e) => e.sha.length > 0);
}
async function readFullCommitMessage(sha, projectPath) {
    try {
        const { stdout } = await execFile('git', ['log', '-1', '--format=%B', sha], {
            cwd: projectPath,
        });
        return stdout.trim();
    }
    catch {
        return '';
    }
}
// ---------------------------------------------------------------------------
// Spec status reader
// ---------------------------------------------------------------------------
async function readSpecStatus(specDir) {
    try {
        const content = await readFile(join(specDir, 'spec.md'), 'utf-8');
        const m = /^status:\s*(\S+)/m.exec(content);
        return m?.[1] ?? null;
    }
    catch {
        return null;
    }
}
async function resolveSpecDir(specsRoot, specId) {
    let entries;
    try {
        entries = await readdir(specsRoot);
    }
    catch {
        return null;
    }
    for (const name of entries) {
        if (name.startsWith(`${specId}-`) || name === specId) {
            return join(specsRoot, name);
        }
    }
    return null;
}
// ---------------------------------------------------------------------------
// Exports
// ---------------------------------------------------------------------------
/**
 * Walk the last 50 commits on main and detect specs that were mentioned in
 * a release commit but still have a non-terminal status in their spec.md.
 */
export async function detectReleaseStatusDrift(projectPath) {
    const specsRoot = join(projectPath, 'planu', 'specs');
    const commits = await fetchMainLog(projectPath);
    // Only consider release commits
    const releaseCommits = commits.filter((c) => RELEASE_COMMIT_RE.test(c.subject));
    const reports = [];
    const checkedSpecIds = new Set();
    for (const commit of releaseCommits) {
        // Extract version from subject line (e.g. "chore(release): bump v3.3.0")
        const versionMatch = /v(\d+\.\d+\.\d+)/.exec(commit.subject);
        const version = versionMatch ? `v${versionMatch[1]}` : commit.sha.slice(0, 8);
        const fullMessage = await readFullCommitMessage(commit.sha, projectPath);
        const specIds = parseSpecIdsFromCommitMessage(fullMessage || commit.subject);
        for (const specId of specIds) {
            if (checkedSpecIds.has(specId)) {
                continue;
            }
            checkedSpecIds.add(specId);
            const specDir = await resolveSpecDir(specsRoot, specId);
            if (!specDir) {
                continue;
            }
            const status = await readSpecStatus(specDir);
            if (status === null || TERMINAL_STATUSES.has(status)) {
                continue;
            }
            reports.push({
                specId,
                currentStatus: status,
                releasedInVersion: version,
                commitSha: commit.sha,
                suggestedAction: 'auto-fix-available',
            });
        }
    }
    return reports;
}
/**
 * Apply a drift fix for a single report by closing the spec via
 * the release-spec-closer pipeline. Gated on the commit having an
 * actual git tag pointing at it — only real released commits qualify.
 */
export async function applyReleaseStatusDriftFix(report, projectPath) {
    if (!(await commitHasReleaseTag(report.commitSha, projectPath))) {
        return;
    }
    await closeSpecsOnReleasePublish({
        commitSha: report.commitSha,
        version: report.releasedInVersion,
        projectPath,
    });
}
async function commitHasReleaseTag(sha, projectPath) {
    try {
        const { stdout } = await execFile('git', ['tag', '--points-at', sha], { cwd: projectPath });
        return stdout
            .trim()
            .split('\n')
            .some((tag) => /^v\d+\.\d+\.\d+/.test(tag.trim()));
    }
    catch {
        return false;
    }
}
//# sourceMappingURL=release-status-drift.js.map