// src/engine/release/stuck-spec-detector.ts — SPEC-774: detect specs stuck in approved/implementing
// with branches already merged to main, and optionally auto-close them.
import { execFile as nodeExecFile } from 'node:child_process';
import { promisify } from 'node:util';
const execFileAsync = promisify(nodeExecFile);
// ---------------------------------------------------------------------------
// buildMergedBranchSet
// ---------------------------------------------------------------------------
/**
 * Runs `git branch --merged <targetBranch>` ONCE and returns a Set of branch
 * names for O(1) lookup. Strips leading `* ` (current branch marker) and trims
 * whitespace from each line.
 *
 * @param projectPath  - Absolute path to the git repo.
 * @param targetBranch - Branch to check merges against (default: 'main').
 */
export async function buildMergedBranchSet(projectPath, targetBranch = 'main') {
    const { stdout } = await execFileAsync('git', ['branch', '--merged', targetBranch], {
        cwd: projectPath,
    });
    return new Set(stdout
        .split('\n')
        .map((b) => b.trim().replace(/^\*\s*/, ''))
        .filter(Boolean));
}
// ---------------------------------------------------------------------------
// detectStuckSpecs
// ---------------------------------------------------------------------------
/**
 * Detects specs that are stuck: status is 'approved' or 'implementing' AND
 * their `gitBranch` is non-empty AND that branch has been merged into
 * `targetBranch`.
 *
 * Uses a single `git branch --merged` call regardless of spec count — O(n)
 * scan with O(1) lookups per spec.
 *
 * @param projectPath  - Absolute path to the project.
 * @param targetBranch - Branch to compare against (default: 'main').
 */
export async function detectStuckSpecs(projectPath, targetBranch = 'main') {
    // Lazy-import storage to keep engine layer free of direct storage coupling
    const { specStore } = await import('../../storage/index.js');
    const { hashProjectPath } = await import('../../storage/base-store.js');
    const projectId = hashProjectPath(projectPath);
    const allSpecs = await specStore.listSpecs(projectId);
    const candidates = allSpecs.filter((s) => (s.status === 'approved' || s.status === 'implementing') &&
        typeof s.gitBranch === 'string' &&
        s.gitBranch.trim().length > 0);
    if (candidates.length === 0) {
        return [];
    }
    // Single git call — no matter how many candidates
    const mergedBranches = await buildMergedBranchSet(projectPath, targetBranch);
    return candidates
        .filter((s) => mergedBranches.has(s.gitBranch))
        .map((s) => ({
        id: s.id,
        title: s.title,
        branch: s.gitBranch,
        status: s.status,
    }));
}
// ---------------------------------------------------------------------------
// autoCloseStuckSpec
// ---------------------------------------------------------------------------
/**
 * Closes a stuck spec by calling `handleUpdateStatus` with `status: 'done'`
 * and `force: true`. Best-effort — failures are logged to the autopilot log
 * and swallowed so release can proceed.
 *
 * @param spec        - The stuck spec to close.
 * @param projectPath - Absolute path to the project.
 */
export async function autoCloseStuckSpec(spec, projectPath) {
    try {
        const { handleUpdateStatus } = await import('../../tools/update-status.js');
        await handleUpdateStatus({
            projectPath,
            specId: spec.id,
            status: 'done',
            reviewNotes: `Auto-closed by create_release stuck-spec gate: branch ${spec.branch} is merged.`,
            force: true,
        });
    }
    catch (err) {
        // Best-effort: log failure but don't block release
        void logAutoCloseFailure(spec.id, projectPath, err);
    }
}
async function logAutoCloseFailure(specId, projectPath, err) {
    try {
        const { appendAutopilotLogEntry } = await import('../../storage/autopilot-log-store.js');
        const { hashProjectPath } = await import('../../storage/base-store.js');
        const projectId = hashProjectPath(projectPath);
        await appendAutopilotLogEntry(projectId, {
            specId,
            hookName: 'stuck-spec-auto-close',
            result: 'fail',
            error: err instanceof Error ? err.message : String(err),
            timestamp: new Date().toISOString(),
            durationMs: 0,
        });
    }
    catch {
        // silently ignore log failures
    }
}
// ---------------------------------------------------------------------------
// logForceReleaseAudit
// ---------------------------------------------------------------------------
/**
 * Appends an audit-trail entry recording that a release was force-bypassed
 * despite stuck specs being detected.
 *
 * @param specIds     - IDs of the stuck specs that were bypassed.
 * @param projectPath - Absolute path to the project.
 */
export async function logForceReleaseAudit(specIds, projectPath) {
    try {
        const { appendAutopilotLogEntry } = await import('../../storage/autopilot-log-store.js');
        const { hashProjectPath } = await import('../../storage/base-store.js');
        const projectId = hashProjectPath(projectPath);
        await appendAutopilotLogEntry(projectId, {
            specId: specIds.join(','),
            hookName: 'force-release-audit',
            result: 'pass',
            error: `create_release forceRelease=true — stuck specs bypassed: ${specIds.join(', ')}`,
            timestamp: new Date().toISOString(),
            durationMs: 0,
        });
    }
    catch {
        // Best-effort — never block release
    }
}
//# sourceMappingURL=stuck-spec-detector.js.map