// @crash-shield-ignore-file — config/cache reader for Planu-controlled JSON; writer is this codebase, shape guaranteed by build/seed.
// engine/facilitate/context-loader.ts — SPEC-264 Loads project context from disk
import { readdir, readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { createHash } from 'node:crypto';
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
const ACTIVE_STATUSES = new Set(['implementing', 'approved', 'review']);
/**
 * Minimal frontmatter parser — reads YAML between `---` delimiters.
 * Supports string scalar fields only (sufficient for our use case).
 */
function parseFrontmatter(content) {
    const match = /^---\n([\s\S]*?)\n---/m.exec(content);
    if (!match?.[1]) {
        return {};
    }
    const result = {};
    for (const line of match[1].split('\n')) {
        const colonIdx = line.indexOf(':');
        if (colonIdx === -1) {
            continue;
        }
        const key = line.slice(0, colonIdx).trim();
        const value = line
            .slice(colonIdx + 1)
            .trim()
            .replace(/^["']|["']$/g, '');
        if (key === 'id') {
            result.id = value;
        }
        else if (key === 'title') {
            result.title = value;
        }
        else if (key === 'status') {
            result.status = value;
        }
    }
    return result;
}
/**
 * Compute project hash the same way base-store does (SHA-256, first 16 chars).
 */
function hashProjectPath(projectPath) {
    return createHash('sha256').update(projectPath).digest('hex').slice(0, 16);
}
/**
 * Check if two sets of keywords overlap (case-insensitive).
 */
function hasKeywordOverlap(title, keywords) {
    const lower = title.toLowerCase();
    return keywords.some((kw) => lower.includes(kw));
}
function toSpecRecord(s) {
    if (typeof s.id === 'string' && typeof s.title === 'string' && typeof s.status === 'string') {
        return { id: s.id, title: s.title, status: s.status };
    }
    return null;
}
async function loadSpecsFromFile(specsFile) {
    try {
        const raw = await readFile(specsFile, 'utf-8');
        const all = JSON.parse(raw);
        return all.flatMap((s) => {
            const rec = toSpecRecord(s);
            return rec ? [rec] : [];
        });
    }
    catch {
        return [];
    }
}
async function loadCheckpoint(checkpointFile) {
    try {
        const raw = await readFile(checkpointFile, 'utf-8');
        const state = JSON.parse(raw);
        return typeof state.lastCheckpoint === 'string' ? state.lastCheckpoint : null;
    }
    catch {
        return null;
    }
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
 * Load spec context directly from disk — no MCP tool calls.
 * Gracefully handles empty/missing project directories.
 */
export async function loadContext(projectPath, keywords) {
    const projectId = hashProjectPath(projectPath);
    const specsFile = join('data', 'projects', projectId, 'specs.json');
    const checkpointFile = join('data', 'projects', projectId, 'session-state.json');
    const specs = await loadSpecsFromFile(specsFile);
    const lastCheckpoint = await loadCheckpoint(checkpointFile);
    const activeSpecs = specs.filter((s) => ACTIVE_STATUSES.has(s.status));
    const relatedSpecs = specs
        .filter((s) => !ACTIVE_STATUSES.has(s.status) && hasKeywordOverlap(s.title, keywords))
        .slice(0, 5)
        .map((s) => ({
        id: s.id,
        title: s.title,
        relevanceReason: 'Title matches keyword from request',
    }));
    return {
        activeSpecs,
        relatedSpecs,
        hasActiveWork: activeSpecs.length > 0,
        lastCheckpoint,
    };
}
/**
 * Load context from a planu/ specs directory (alternative path — reads spec.md frontmatter).
 * Used when data/ is not available but planu/ is present.
 */
export async function loadContextFromPlanuDir(planuSpecsDir, keywords) {
    let entries;
    try {
        entries = await readdir(planuSpecsDir);
    }
    catch {
        return { activeSpecs: [], relatedSpecs: [], hasActiveWork: false, lastCheckpoint: null };
    }
    const activeSpecs = [];
    const relatedSpecs = [];
    for (const entry of entries) {
        const specMdPath = join(planuSpecsDir, entry, 'spec.md');
        try {
            const content = await readFile(specMdPath, 'utf-8');
            const fm = parseFrontmatter(content);
            if (!fm.id || !fm.title || !fm.status) {
                continue;
            }
            if (ACTIVE_STATUSES.has(fm.status)) {
                activeSpecs.push({ id: fm.id, title: fm.title, status: fm.status });
            }
            else if (hasKeywordOverlap(fm.title, keywords) && relatedSpecs.length < 5) {
                relatedSpecs.push({
                    id: fm.id,
                    title: fm.title,
                    relevanceReason: 'Title matches keyword from request',
                });
            }
        }
        catch {
            // Skip unreadable spec
        }
    }
    return {
        activeSpecs,
        relatedSpecs,
        hasActiveWork: activeSpecs.length > 0,
        lastCheckpoint: null,
    };
}
//# sourceMappingURL=context-loader.js.map