// @crash-shield-ignore-file — config/cache reader for Planu-controlled JSON; writer is this codebase, shape guaranteed by build/seed.
// engine/hooks/context-injector.ts — Reads project data and writes .planu/plan-context.md (SPEC-262)
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { join } from 'node:path';
import { createHash } from 'node:crypto';
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const ACTIVE_STATUSES = ['implementing', 'approved'];
const MAX_RELATED_SPECS = 3;
const MAX_RECENT_DECISIONS = 5;
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
function hashProjectPath(projectPath) {
    return createHash('sha256').update(projectPath).digest('hex').slice(0, 16);
}
/** Safely extract a string from an unknown value — returns '' when not a string */
function str(val) {
    return typeof val === 'string' ? val : '';
}
async function loadSpecs(projectPath) {
    const projectId = hashProjectPath(projectPath);
    const specsFile = join(projectPath, 'data', 'projects', projectId, 'specs.json');
    try {
        const raw = await readFile(specsFile, 'utf-8');
        return JSON.parse(raw);
    }
    catch {
        return [];
    }
}
async function loadDecisions(projectPath) {
    const projectId = hashProjectPath(projectPath);
    const decisionsFile = join(projectPath, 'data', 'projects', projectId, 'decisions.json');
    try {
        const raw = await readFile(decisionsFile, 'utf-8');
        const data = JSON.parse(raw);
        return data.decisions ?? [];
    }
    catch {
        return [];
    }
}
/**
 * Scores a spec's relevance to the task description.
 * Returns a score >= 0; higher is more relevant.
 */
function relevanceScore(spec, taskDescription) {
    if (!taskDescription.trim()) {
        return 0;
    }
    const specText = [str(spec.id), str(spec.title)].join(' ').toLowerCase();
    const words = taskDescription
        .toLowerCase()
        .split(/\W+/)
        .filter((w) => w.length > 2);
    return words.reduce((score, word) => {
        return specText.includes(word) ? score + 1 : score;
    }, 0);
}
function formatDecisionSummary(d) {
    const title = str(d.title);
    const detail = str(d.summary) || str(d.rationale);
    return [title, detail].filter(Boolean).join(': ');
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
 * Reads project specs and decisions from data/ and writes
 * .planu/plan-context.md in the project root.
 */
export async function injectPlanContext(projectPath, taskDescription) {
    const md = await buildContextMarkdown(projectPath, taskDescription);
    const planuDir = join(projectPath, '.planu');
    await mkdir(planuDir, { recursive: true });
    await writeFile(join(planuDir, 'plan-context.md'), md, 'utf-8');
}
/**
 * Builds the markdown content for .planu/plan-context.md.
 * Pure function for testability — does not write to disk.
 */
export async function buildContextMarkdown(projectPath, taskDescription) {
    const [allSpecs, allDecisions] = await Promise.all([
        loadSpecs(projectPath),
        loadDecisions(projectPath),
    ]);
    // Active specs: implementing or approved
    const activeSpecs = allSpecs
        .filter((s) => ACTIVE_STATUSES.includes(str(s.status)))
        .map((s) => ({ id: str(s.id), title: str(s.title), status: str(s.status) }));
    // Related specs: non-active specs scored by keyword match
    const desc = taskDescription ?? '';
    const relatedSpecs = allSpecs
        .filter((s) => !ACTIVE_STATUSES.includes(str(s.status)))
        .map((s) => ({ spec: s, score: relevanceScore(s, desc) }))
        .filter(({ score }) => score > 0)
        .sort((a, b) => b.score - a.score)
        .slice(0, MAX_RELATED_SPECS)
        .map(({ spec, score }) => ({
        id: str(spec.id),
        title: str(spec.title),
        status: str(spec.status),
        relevanceReason: `keyword match (score: ${score})`,
    }));
    // Recent decisions
    const recentDecisions = allDecisions
        .slice(-MAX_RECENT_DECISIONS)
        .reverse()
        .map(formatDecisionSummary)
        .filter(Boolean);
    const context = {
        activeSpecs,
        relatedSpecs,
        recentDecisions,
        generatedAt: new Date().toISOString(),
    };
    return renderMarkdown(context, taskDescription);
}
function renderMarkdown(ctx, taskDescription) {
    const lines = [
        '# Planu Context — Plan Mode',
        '> Auto-generated by Planu. Claude: read this before exploring.',
        '',
    ];
    if (taskDescription) {
        lines.push('## Task', taskDescription, '');
    }
    lines.push('## Active Specs');
    if (ctx.activeSpecs.length === 0) {
        lines.push('_No specs currently implementing or approved._');
    }
    else {
        for (const s of ctx.activeSpecs) {
            lines.push(`- ${s.id}: ${s.title} (${s.status})`);
        }
    }
    lines.push('');
    lines.push('## Related Specs (semantic match)');
    if (ctx.relatedSpecs.length === 0) {
        lines.push('_No related specs found._');
    }
    else {
        for (const s of ctx.relatedSpecs) {
            const reason = s.relevanceReason ? ` — ${s.relevanceReason}` : '';
            lines.push(`- ${s.id}: ${s.title}${reason}`);
        }
    }
    lines.push('');
    lines.push('## Recent Decisions');
    if (ctx.recentDecisions.length === 0) {
        lines.push('_No recent decisions recorded._');
    }
    else {
        for (const d of ctx.recentDecisions) {
            lines.push(`- ${d}`);
        }
    }
    lines.push('');
    lines.push('## Suggested next step', 'After exploring, call `facilitate` with your findings to create a spec.', '', `> Generated at: ${ctx.generatedAt}`);
    return lines.join('\n');
}
//# sourceMappingURL=context-injector.js.map