// Copyright (c) 2026 Hellmai Ltd
// SPDX-License-Identifier: AGPL-3.0-only
/**
 * Orchestration integrity detectors (WU-2634 / INIT-056 Phase 5).
 *
 * Pure helpers that turn raw execution evidence into the
 * `contaminatedPaths` and `stalled` signals consumed by
 * {@link @lumenflow/initiatives/orchestrator#reconcileInitiativeExecution}.
 *
 * These functions are intentionally side-effect-free so callers can inject
 * test fixtures without spawning `git` or mutating the real filesystem.
 * The thin `getMainDirtyPaths` helper is the one place we shell out, and it
 * is exported separately so tests can substitute their own dirty list.
 *
 * @module orchestrate-integrity
 */
import { execFileSync } from 'node:child_process';
export const DEFAULT_STALL_THRESHOLD_MS = 4 * 60 * 60 * 1000; // 4 hours
function getOrchestrationConfig(config) {
    if (typeof config !== 'object' || config === null) {
        return undefined;
    }
    const sd = config.software_delivery;
    if (typeof sd !== 'object' || sd === null) {
        return undefined;
    }
    const orch = sd.orchestration;
    return typeof orch === 'object' && orch !== null ? orch : undefined;
}
/**
 * Read `software_delivery.orchestration.max_active_workers` from the resolved
 * workspace config, defaulting to undefined (no capacity ceiling).
 */
export function readMaxActiveWorkersFromConfig(config) {
    const raw = getOrchestrationConfig(config)?.max_active_workers;
    if (typeof raw === 'number' && Number.isInteger(raw) && raw >= 0) {
        return raw;
    }
    return undefined;
}
/**
 * Read `software_delivery.orchestration.stall_threshold_hours` from the resolved
 * workspace config. Falls back to {@link DEFAULT_STALL_THRESHOLD_MS}.
 */
export function readStallThresholdMs(config) {
    const raw = getOrchestrationConfig(config)?.stall_threshold_hours;
    if (typeof raw === 'number' && Number.isFinite(raw) && raw > 0) {
        return raw * 60 * 60 * 1000;
    }
    return DEFAULT_STALL_THRESHOLD_MS;
}
/**
 * Return the set of dirty paths in the main checkout at `baseDir`. Uses
 * `git status --porcelain=v1` so the output is stable. Returns [] when the
 * command fails or the directory is not a git repository — callers should
 * treat a missing result as "no contamination detected" rather than fatal.
 */
export function getMainDirtyPaths(baseDir) {
    try {
        // `git` is a first-class LumenFlow runtime dependency; other CLI entry
        // points resolve it from PATH the same way.
        // eslint-disable-next-line sonarjs/no-os-command-from-path
        const raw = execFileSync('git', ['status', '--porcelain=v1'], {
            cwd: baseDir,
            encoding: 'utf8',
            stdio: ['ignore', 'pipe', 'ignore'],
        });
        return raw
            .split('\n')
            .map((line) => line.trim())
            .filter((line) => line.length > 0)
            .map((line) => {
            // Porcelain format: "XY path" or "XY orig -> new" for renames.
            const withoutStatus = line.slice(2).trim();
            const arrowIdx = withoutStatus.indexOf(' -> ');
            return arrowIdx >= 0 ? withoutStatus.slice(arrowIdx + 4) : withoutStatus;
        })
            .filter((path) => path.length > 0);
    }
    catch {
        return [];
    }
}
/**
 * Return the subset of `wuCodePaths` whose path (or any prefix directory)
 * appears in `dirtyPaths`. Matching is exact-string; glob expansion is the
 * caller's responsibility because code_paths in WU specs may already use
 * shell patterns that the orchestrator does not expand on its own.
 */
export function detectContaminatedCodePaths(dirtyPaths, wuCodePaths) {
    if (!wuCodePaths || wuCodePaths.length === 0 || dirtyPaths.length === 0) {
        return [];
    }
    const dirtySet = new Set(dirtyPaths);
    const contaminated = [];
    for (const codePath of wuCodePaths) {
        if (dirtySet.has(codePath)) {
            contaminated.push(codePath);
            continue;
        }
        // If a WU declares a directory, any dirty file under it counts as contamination.
        const dirPrefix = codePath.endsWith('/') ? codePath : `${codePath}/`;
        if (dirtyPaths.some((dirty) => dirty.startsWith(dirPrefix))) {
            contaminated.push(codePath);
        }
    }
    return contaminated;
}
/**
 * A claimed worktree is stalled when it has been claimed but no checkpoint
 * or signal activity has been recorded within the threshold. Never returns
 * `true` for WUs that have no worktree — those are classified as
 * needs_relaunch via the worker-returned path, not stall.
 */
export function isWorktreeStalled(evidence, thresholdMs, now = new Date()) {
    if (!evidence.worktreePresent) {
        return false;
    }
    const nowMs = now.getTime();
    const latestActivityMs = Math.max(parseTimestamp(evidence.latestCheckpointAt), parseTimestamp(evidence.latestSignalAt), parseTimestamp(evidence.claimedAt));
    if (latestActivityMs === 0) {
        return false; // No evidence at all — can't make a stall claim.
    }
    return nowMs - latestActivityMs > thresholdMs;
}
function parseTimestamp(value) {
    if (!value) {
        return 0;
    }
    const parsed = Date.parse(value);
    return Number.isNaN(parsed) ? 0 : parsed;
}
//# sourceMappingURL=orchestrate-integrity.js.map