// @crash-shield-ignore-file — config/cache reader for Planu-controlled JSON; writer is this codebase, shape guaranteed by build/seed.
// engine/spec-conflict-graph.ts — SPEC-657: Pre-execution file overlap detection
import { readFile, writeFile, mkdir, readdir } from 'node:fs/promises';
import { readSpecTechnicalSection } from './spec-format/read-technical-section.js';
import { join, resolve } from 'node:path';
const MAX_PARALLEL = 2;
const CONFLICT_GRAPH_PATH = 'planu/state/conflict-graph.json';
const FILE_PATTERN = /\b([\w./-]+\.(?:ts|tsx|js|jsx|py|go|rs|java|rb|cs|json|yaml|yml))\b/g;
function extractFilesFromContent(content, projectPath) {
    const found = new Set();
    const lines = content.split('\n');
    for (const line of lines) {
        if (line.trimStart().startsWith('#')) {
            continue;
        }
        FILE_PATTERN.lastIndex = 0;
        let match;
        while ((match = FILE_PATTERN.exec(line)) !== null) {
            const raw = match[1];
            if (!raw || raw.length < 5) {
                continue;
            }
            found.add(resolve(projectPath, raw));
        }
    }
    return [...found];
}
async function readTechnicalFiles(projectPath, specId) {
    const specsDir = join(projectPath, 'planu', 'specs');
    let entries;
    try {
        entries = await readdir(specsDir);
    }
    catch {
        return [];
    }
    const folder = entries.find((e) => e.startsWith(`${specId}-`));
    if (!folder) {
        return [];
    }
    const specPath = join(specsDir, folder, 'spec.md');
    const content = await readSpecTechnicalSection({ specPath });
    if (content.length === 0) {
        return [];
    }
    return extractFilesFromContent(content, projectPath);
}
async function loadGraph(projectPath) {
    try {
        const raw = await readFile(join(projectPath, CONFLICT_GRAPH_PATH), 'utf-8');
        return JSON.parse(raw);
    }
    catch {
        return { updatedAt: new Date().toISOString(), entries: {} };
    }
}
async function saveGraph(projectPath, graph) {
    const graphPath = join(projectPath, CONFLICT_GRAPH_PATH);
    await mkdir(join(graphPath, '..'), { recursive: true });
    await writeFile(graphPath, JSON.stringify(graph, null, 2) + '\n', 'utf-8');
}
/**
 * SPEC-657: Update the conflict graph entry for a single spec.
 * Called whenever a spec's inline ## Technical section changes.
 */
export async function updateConflictGraphEntry(projectPath, specId) {
    const files = await readTechnicalFiles(projectPath, specId);
    const graph = await loadGraph(projectPath);
    const entry = { specId, files, updatedAt: new Date().toISOString() };
    graph.entries[specId] = entry;
    graph.updatedAt = new Date().toISOString();
    await saveGraph(projectPath, graph);
}
/**
 * SPEC-657: Check whether two or more specs can safely run in parallel.
 * Returns safe/warning/blocked based on file overlap.
 */
export async function checkSpecConflicts(projectPath, specIds) {
    if (specIds.length > MAX_PARALLEL) {
        const queued = specIds.slice(MAX_PARALLEL);
        return {
            safety: 'blocked',
            overlappingFiles: [],
            conflictingSpecIds: queued,
            message: `Hard limit: max ${MAX_PARALLEL} parallel specs. ${queued.join(', ')} must be queued.`,
            suggestion: 'Run the first 2 specs, then start the remaining ones after they complete.',
        };
    }
    const graph = await loadGraph(projectPath);
    const fileMap = {};
    for (const specId of specIds) {
        const cached = graph.entries[specId];
        fileMap[specId] = cached ? cached.files : await readTechnicalFiles(projectPath, specId);
    }
    const ownersByFile = new Map();
    for (const [specId, files] of Object.entries(fileMap)) {
        for (const file of files) {
            const owners = ownersByFile.get(file) ?? [];
            owners.push(specId);
            ownersByFile.set(file, owners);
        }
    }
    const overlapping = [...ownersByFile.entries()]
        .filter(([, owners]) => owners.length > 1)
        .map(([file]) => file);
    const conflictingSpecIds = [...new Set(overlapping.flatMap((f) => ownersByFile.get(f) ?? []))];
    if (overlapping.length === 0) {
        return {
            safety: 'safe',
            overlappingFiles: [],
            conflictingSpecIds: [],
            message: 'No file overlaps detected. Parallel execution is safe.',
            suggestion: 'Proceed with parallel execution.',
        };
    }
    return {
        safety: 'blocked',
        overlappingFiles: overlapping,
        conflictingSpecIds,
        message: `File overlap between ${conflictingSpecIds.join(' and ')}: ${overlapping.length} shared file(s).`,
        suggestion: 'Run specs sequentially, or use split_spec to separate the overlapping file responsibilities.',
    };
}
//# sourceMappingURL=spec-conflict-graph.js.map