// NERF Extension - File Parsing and I/O
// Parsers for roadmap, plan, summary, and continue files.
// Used by state derivation and the status widget.
// Pure functions, zero Pi dependencies - uses only Node built-ins.
import { promises as fs } from 'node:fs';
import { resolve } from 'node:path';
import { atomicWriteAsync } from './atomic-write.js';
import { resolveMilestoneFile, relMilestoneFile, resolveNerfRootFile } from './paths.js';
import { findMilestoneIds } from './milestone-ids.js';
import { checkExistingEnvKeys } from './env-utils.js';
import { nativeExtractSection, nativeParseSummaryFile, NATIVE_UNAVAILABLE } from './native-parser-bridge.js';
import { CACHE_MAX } from './constants.js';
import { splitFrontmatter, parseFrontmatterMap } from '../shared/frontmatter.js';
// Re-export for downstream consumers
export { splitFrontmatter, parseFrontmatterMap };
// ─── Parse Cache ──────────────────────────────────────────────────────────
/** Fast composite key: length + first/mid/last 100 chars. The middle sample
 *  prevents collisions when only a few characters change in the interior of
 *  a file (e.g., a checkbox [ ] → [x] that doesn't alter length or endpoints). */
function cacheKey(content) {
    const len = content.length;
    const head = content.slice(0, 100);
    const midStart = Math.max(0, Math.floor(len / 2) - 50);
    const mid = len > 200 ? content.slice(midStart, midStart + 100) : '';
    const tail = len > 100 ? content.slice(-100) : '';
    return `${len}:${head}:${mid}:${tail}`;
}
const _parseCache = new Map();
function cachedParse(content, tag, parseFn) {
    const key = tag + '|' + cacheKey(content);
    if (_parseCache.has(key))
        return _parseCache.get(key);
    if (_parseCache.size >= CACHE_MAX)
        _parseCache.delete(_parseCache.keys().next().value);
    const result = parseFn(content);
    _parseCache.set(key, result);
    return result;
}
// ─── Cross-module cache clear registry ────────────────────────────────────
// parsers-legacy.ts registers its cache-clear callback here at module init
// to avoid circular imports. clearParseCache() calls all registered callbacks.
const _cacheClearCallbacks = [];
/** Register a callback to be invoked when clearParseCache() is called.
 *  Used by parsers-legacy.ts to synchronously clear its own cache. */
export function registerCacheClearCallback(cb) {
    _cacheClearCallbacks.push(cb);
}
/** Clear the module-scoped parse cache. Call when files change on disk.
 *  Also clears any registered external caches (e.g. parsers-legacy.ts). */
export function clearParseCache() {
    _parseCache.clear();
    for (const cb of _cacheClearCallbacks)
        cb();
}
// ─── Helpers ───────────────────────────────────────────────────────────────
/** Extract the text after a heading at a given level, up to the next heading of same or higher level. */
export function extractSection(body, heading, level = 2) {
    // Try native parser first for better performance on large files
    const nativeResult = nativeExtractSection(body, heading, level);
    if (nativeResult !== NATIVE_UNAVAILABLE)
        return nativeResult;
    const prefix = '#'.repeat(level) + ' ';
    const regex = new RegExp(`^${prefix}${escapeRegex(heading)}\\s*$`, 'm');
    const match = regex.exec(body);
    if (!match)
        return null;
    const start = match.index + match[0].length;
    const rest = body.slice(start);
    const nextHeading = rest.match(new RegExp(`^#{1,${level}} `, 'm'));
    const end = nextHeading ? nextHeading.index : rest.length;
    return rest.slice(0, end).trim();
}
/** Extract all sections at a given level, returning heading → content map. */
export function extractAllSections(body, level = 2) {
    const prefix = '#'.repeat(level) + ' ';
    const regex = new RegExp(`^${prefix}(.+)$`, 'gm');
    const sections = new Map();
    const matches = [...body.matchAll(regex)];
    for (let i = 0; i < matches.length; i++) {
        const heading = matches[i][1].trim();
        const start = matches[i].index + matches[i][0].length;
        const end = i + 1 < matches.length ? matches[i + 1].index : body.length;
        sections.set(heading, body.slice(start, end).trim());
    }
    return sections;
}
function escapeRegex(s) {
    return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/** Parse bullet list items from a text block. */
export function parseBullets(text) {
    return text.split('\n')
        .map(l => l.replace(/^\s*[-*]\s+/, '').trim())
        .filter(l => l.length > 0 && !l.startsWith('#'));
}
/** Extract key: value from bold-prefixed lines like "**Key:** Value" */
export function extractBoldField(text, key) {
    const regex = new RegExp(`^\\*\\*${escapeRegex(key)}:\\*\\*\\s*(.+)$`, 'm');
    const match = regex.exec(text);
    return match ? match[1].trim() : null;
}
// ─── Secrets Manifest Parser ───────────────────────────────────────────────
const VALID_STATUSES = new Set(['pending', 'collected', 'skipped']);
export function parseSecretsManifest(content) {
    const milestone = extractBoldField(content, 'Milestone') || '';
    const generatedAt = extractBoldField(content, 'Generated') || '';
    const h3Sections = extractAllSections(content, 3);
    const entries = [];
    for (const [heading, sectionContent] of h3Sections) {
        const key = heading.trim();
        if (!key)
            continue;
        const service = extractBoldField(sectionContent, 'Service') || '';
        const dashboardUrl = extractBoldField(sectionContent, 'Dashboard') || '';
        const formatHint = extractBoldField(sectionContent, 'Format hint') || '';
        const rawStatus = (extractBoldField(sectionContent, 'Status') || 'pending').toLowerCase().trim();
        const status = VALID_STATUSES.has(rawStatus) ? rawStatus : 'pending';
        const destination = extractBoldField(sectionContent, 'Destination') || 'dotenv';
        // Extract numbered guidance list (lines matching "1. ...", "2. ...", etc.)
        const guidance = [];
        for (const line of sectionContent.split('\n')) {
            const numMatch = line.match(/^\s*\d+\.\s+(.+)/);
            if (numMatch) {
                guidance.push(numMatch[1].trim());
            }
        }
        entries.push({ key, service, dashboardUrl, guidance, formatHint, status, destination });
    }
    return { milestone, generatedAt, entries };
}
// ─── Secrets Manifest Formatter ───────────────────────────────────────────
export function formatSecretsManifest(manifest) {
    const lines = [];
    lines.push('# Secrets Manifest');
    lines.push('');
    lines.push(`**Milestone:** ${manifest.milestone}`);
    lines.push(`**Generated:** ${manifest.generatedAt}`);
    for (const entry of manifest.entries) {
        lines.push('');
        lines.push(`### ${entry.key}`);
        lines.push('');
        lines.push(`**Service:** ${entry.service}`);
        if (entry.dashboardUrl) {
            lines.push(`**Dashboard:** ${entry.dashboardUrl}`);
        }
        if (entry.formatHint) {
            lines.push(`**Format hint:** ${entry.formatHint}`);
        }
        lines.push(`**Status:** ${entry.status}`);
        lines.push(`**Destination:** ${entry.destination}`);
        lines.push('');
        for (let i = 0; i < entry.guidance.length; i++) {
            lines.push(`${i + 1}. ${entry.guidance[i]}`);
        }
    }
    return lines.join('\n') + '\n';
}
// ─── Slice Plan Parser ─────────────────────────────────────────────────────
function normalizeTaskPlanFrontmatter(frontmatter) {
    const estimatedStepsRaw = frontmatter.estimated_steps;
    const estimatedFilesRaw = frontmatter.estimated_files;
    const skillsUsedRaw = frontmatter.skills_used;
    const parseOptionalNumber = (value) => {
        if (typeof value === 'number' && Number.isFinite(value))
            return value;
        if (typeof value === 'string' && value.trim()) {
            const parsed = parseInt(value, 10);
            if (Number.isFinite(parsed))
                return parsed;
        }
        return undefined;
    };
    const estimated_steps = parseOptionalNumber(estimatedStepsRaw);
    const estimated_files = parseOptionalNumber(estimatedFilesRaw);
    const skills_used = Array.isArray(skillsUsedRaw)
        ? skillsUsedRaw.map(v => String(v).trim()).filter(Boolean)
        : typeof skillsUsedRaw === 'string' && skillsUsedRaw.trim()
            ? [skillsUsedRaw.trim()]
            : [];
    return {
        ...(estimated_steps !== undefined ? { estimated_steps } : {}),
        ...(estimated_files !== undefined ? { estimated_files } : {}),
        skills_used,
    };
}
export function parseTaskPlanFile(content) {
    const [fmLines] = splitFrontmatter(content);
    const fm = fmLines ? parseFrontmatterMap(fmLines) : {};
    return {
        frontmatter: normalizeTaskPlanFrontmatter(fm),
    };
}
// ─── Summary Parser ────────────────────────────────────────────────────────
export function parseSummary(content) {
    return cachedParse(content, 'summary', _parseSummaryImpl);
}
function _parseSummaryImpl(content) {
    // Try native parser first for better performance
    const nativeResult = nativeParseSummaryFile(content);
    if (nativeResult) {
        const nfm = nativeResult.frontmatter;
        return {
            frontmatter: {
                id: nfm.id,
                parent: nfm.parent,
                milestone: nfm.milestone,
                provides: nfm.provides,
                requires: nfm.requires,
                affects: nfm.affects,
                key_files: nfm.keyFiles,
                key_decisions: nfm.keyDecisions,
                patterns_established: nfm.patternsEstablished,
                drill_down_paths: nfm.drillDownPaths,
                observability_surfaces: nfm.observabilitySurfaces,
                duration: nfm.duration,
                verification_result: nfm.verificationResult,
                completed_at: nfm.completedAt,
                blocker_discovered: nfm.blockerDiscovered,
            },
            title: nativeResult.title,
            oneLiner: nativeResult.oneLiner,
            whatHappened: nativeResult.whatHappened,
            deviations: nativeResult.deviations,
            filesModified: nativeResult.filesModified,
            followUps: extractSection(content, 'Follow-ups') ?? '',
            knownLimitations: extractSection(content, 'Known Limitations') ?? '',
        };
    }
    const [fmLines, body] = splitFrontmatter(content);
    const fm = fmLines ? parseFrontmatterMap(fmLines) : {};
    const asStringArray = (v) => Array.isArray(v) ? v : (typeof v === 'string' && v ? [v] : []);
    const frontmatter = {
        id: fm.id || '',
        parent: fm.parent || '',
        milestone: fm.milestone || '',
        provides: asStringArray(fm.provides),
        requires: (fm.requires || []).map(r => ({
            slice: r.slice || '',
            provides: r.provides || '',
        })),
        affects: asStringArray(fm.affects),
        key_files: asStringArray(fm.key_files),
        key_decisions: asStringArray(fm.key_decisions),
        patterns_established: asStringArray(fm.patterns_established),
        drill_down_paths: asStringArray(fm.drill_down_paths),
        observability_surfaces: asStringArray(fm.observability_surfaces),
        duration: fm.duration || '',
        verification_result: fm.verification_result || 'untested',
        completed_at: fm.completed_at || '',
        blocker_discovered: fm.blocker_discovered === 'true' || fm.blocker_discovered === true,
    };
    const bodyLines = body.split('\n');
    const h1 = bodyLines.find(l => l.startsWith('# '));
    const title = h1 ? h1.slice(2).trim() : '';
    const h1Idx = bodyLines.indexOf(h1 || '');
    let oneLiner = '';
    for (let i = h1Idx + 1; i < bodyLines.length; i++) {
        const line = bodyLines[i].trim();
        if (!line)
            continue;
        if (line.startsWith('**') && line.endsWith('**')) {
            oneLiner = line.slice(2, -2);
        }
        break;
    }
    const whatHappened = extractSection(body, 'What Happened') || '';
    const deviations = extractSection(body, 'Deviations') || '';
    const filesSection = extractSection(body, 'Files Created/Modified') || extractSection(body, 'Files Modified');
    const filesModified = [];
    if (filesSection) {
        for (const line of filesSection.split('\n')) {
            const trimmed = line.replace(/^\s*[-*]\s+/, '').trim();
            if (!trimmed || trimmed.startsWith('#'))
                continue;
            const fileMatch = trimmed.match(/^`([^`]+)`\s*[—–-]\s*(.+)/);
            if (fileMatch) {
                filesModified.push({ path: fileMatch[1], description: fileMatch[2].trim() });
            }
        }
    }
    const followUps = extractSection(body, 'Follow-ups') ?? '';
    const knownLimitations = extractSection(body, 'Known Limitations') ?? '';
    return { frontmatter, title, oneLiner, whatHappened, deviations, filesModified, followUps, knownLimitations };
}
// ─── Continue Parser ───────────────────────────────────────────────────────
export function parseContinue(content) {
    return cachedParse(content, 'continue', _parseContinueImpl);
}
function _parseContinueImpl(content) {
    const [fmLines, body] = splitFrontmatter(content);
    const fm = fmLines ? parseFrontmatterMap(fmLines) : {};
    const frontmatter = {
        milestone: fm.milestone || '',
        slice: fm.slice || '',
        task: fm.task || '',
        step: typeof fm.step === 'string' ? parseInt(fm.step) : fm.step || 0,
        totalSteps: typeof fm.total_steps === 'string' ? parseInt(fm.total_steps) : fm.total_steps ||
            (typeof fm.totalSteps === 'string' ? parseInt(fm.totalSteps) : fm.totalSteps || 0),
        status: (fm.status || 'in_progress'),
        savedAt: fm.saved_at || fm.savedAt || '',
    };
    const completedWork = extractSection(body, 'Completed Work') || '';
    const remainingWork = extractSection(body, 'Remaining Work') || '';
    const decisions = extractSection(body, 'Decisions Made') || '';
    const context = extractSection(body, 'Context') || '';
    const nextAction = extractSection(body, 'Next Action') || '';
    return { frontmatter, completedWork, remainingWork, decisions, context, nextAction };
}
// ─── Continue Formatter ────────────────────────────────────────────────────
function formatFrontmatter(data) {
    const lines = ['---'];
    for (const [key, value] of Object.entries(data)) {
        if (value === undefined || value === null)
            continue;
        if (Array.isArray(value)) {
            if (value.length === 0) {
                lines.push(`${key}: []`);
            }
            else if (typeof value[0] === 'object' && value[0] !== null) {
                lines.push(`${key}:`);
                for (const obj of value) {
                    const entries = Object.entries(obj);
                    if (entries.length > 0) {
                        lines.push(`  - ${entries[0][0]}: ${entries[0][1]}`);
                        for (let i = 1; i < entries.length; i++) {
                            lines.push(`    ${entries[i][0]}: ${entries[i][1]}`);
                        }
                    }
                }
            }
            else {
                lines.push(`${key}:`);
                for (const item of value) {
                    lines.push(`  - ${item}`);
                }
            }
        }
        else {
            lines.push(`${key}: ${value}`);
        }
    }
    lines.push('---');
    return lines.join('\n');
}
export function formatContinue(cont) {
    const fm = cont.frontmatter;
    const fmData = {
        milestone: fm.milestone,
        slice: fm.slice,
        task: fm.task,
        step: fm.step,
        total_steps: fm.totalSteps,
        status: fm.status,
        saved_at: fm.savedAt,
    };
    const lines = [];
    lines.push(formatFrontmatter(fmData));
    lines.push('');
    lines.push('## Completed Work');
    lines.push(cont.completedWork);
    lines.push('');
    lines.push('## Remaining Work');
    lines.push(cont.remainingWork);
    lines.push('');
    lines.push('## Decisions Made');
    lines.push(cont.decisions);
    lines.push('');
    lines.push('## Context');
    lines.push(cont.context);
    lines.push('');
    lines.push('## Next Action');
    lines.push(cont.nextAction);
    return lines.join('\n');
}
// ─── File I/O ──────────────────────────────────────────────────────────────
/**
 * Load a file from disk. Returns content string or null if file doesn't exist.
 */
export async function loadFile(path) {
    try {
        return await fs.readFile(path, 'utf-8');
    }
    catch (err) {
        const code = err.code;
        if (code === 'ENOENT' || code === 'EISDIR')
            return null;
        throw err;
    }
}
/**
 * Save content to a file atomically (write to temp, then rename).
 * Creates parent directories if needed.
 */
export async function saveFile(path, content) {
    await atomicWriteAsync(path, content);
}
export function parseRequirementCounts(content) {
    const counts = {
        active: 0,
        validated: 0,
        deferred: 0,
        outOfScope: 0,
        blocked: 0,
        total: 0,
    };
    if (!content)
        return counts;
    const sections = [
        { key: 'active', heading: 'Active' },
        { key: 'validated', heading: 'Validated' },
        { key: 'deferred', heading: 'Deferred' },
        { key: 'outOfScope', heading: 'Out of Scope' },
    ];
    for (const section of sections) {
        const text = extractSection(content, section.heading, 2);
        if (!text)
            continue;
        const matches = text.match(/^###\s+[A-Z][\w-]*\d+\s+—/gm);
        counts[section.key] = matches ? matches.length : 0;
    }
    const blockedMatches = content.match(/^-\s+Status:\s+blocked\s*$/gim);
    counts.blocked = blockedMatches ? blockedMatches.length : 0;
    counts.total = counts.active + counts.validated + counts.deferred + counts.outOfScope;
    return counts;
}
// ─── Task Plan Must-Haves Parser ───────────────────────────────────────────
/**
 * Parse must-have items from a task plan's `## Must-Haves` section.
 * Returns structured items with checkbox state. Handles YAML frontmatter,
 * all common checkbox variants (`[ ]`, `[x]`, `[X]`), plain bullets (no checkbox),
 * and indented variants. Returns empty array when the section is missing or empty.
 */
export function parseTaskPlanMustHaves(content) {
    const [, body] = splitFrontmatter(content);
    const sectionText = extractSection(body, 'Must-Haves');
    if (!sectionText)
        return [];
    const bullets = parseBullets(sectionText);
    if (bullets.length === 0)
        return [];
    return bullets.map(line => {
        const cbMatch = line.match(/^\[([xX ])\]\s+(.+)/);
        if (cbMatch) {
            return {
                text: cbMatch[2].trim(),
                checked: cbMatch[1].toLowerCase() === 'x',
            };
        }
        // No checkbox - treat as unchecked with full line as text
        return { text: line.trim(), checked: false };
    });
}
// ─── Must-Have Summary Matching ────────────────────────────────────────────
/** Common short words to exclude from substring matching. */
const COMMON_WORDS = new Set([
    'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'can', 'had', 'her',
    'was', 'one', 'our', 'out', 'has', 'its', 'let', 'say', 'she', 'too', 'use',
    'with', 'have', 'from', 'this', 'that', 'they', 'been', 'each', 'when', 'will',
    'does', 'into', 'also', 'than', 'them', 'then', 'some', 'what', 'only', 'just',
    'more', 'make', 'like', 'made', 'over', 'such', 'take', 'most', 'very', 'must',
    'file', 'test', 'tests', 'task', 'new', 'add', 'added', 'existing',
]);
/**
 * Count how many must-have items are mentioned in a summary.
 *
 * Matching heuristic per must-have:
 * 1. Extract all backtick-enclosed code tokens (e.g. `inspectFoo`).
 *    If any code token appears case-insensitively in the summary, count as mentioned.
 * 2. If no code tokens exist, check if any significant word (≥4 chars, not a common word)
 *    from the must-have text appears in the summary (case-insensitive).
 *
 * Returns the count of must-haves that had at least one match.
 */
export function countMustHavesMentionedInSummary(mustHaves, summaryContent) {
    if (!summaryContent || mustHaves.length === 0)
        return 0;
    const summaryLower = summaryContent.toLowerCase();
    let count = 0;
    for (const mh of mustHaves) {
        // Extract backtick-enclosed code tokens
        const codeTokens = [];
        const codeRegex = /`([^`]+)`/g;
        let match;
        while ((match = codeRegex.exec(mh.text)) !== null) {
            codeTokens.push(match[1]);
        }
        if (codeTokens.length > 0) {
            // Strategy 1: any code token found in summary (case-insensitive)
            const found = codeTokens.some(token => summaryLower.includes(token.toLowerCase()));
            if (found)
                count++;
        }
        else {
            // Strategy 2: significant substring matching
            // Split into words, keep words ≥4 chars that aren't common
            const words = mh.text.replace(/[^\w\s]/g, ' ').split(/\s+/).filter(w => w.length >= 4 && !COMMON_WORDS.has(w.toLowerCase()));
            const found = words.some(word => summaryLower.includes(word.toLowerCase()));
            if (found)
                count++;
        }
    }
    return count;
}
// ─── Task Plan IO Extractor ────────────────────────────────────────────────
/**
 * Extract input and output file paths from a task plan's `## Inputs` and
 * `## Expected Output` sections. Looks for backtick-wrapped file paths on
 * each line (e.g. `` `src/foo.ts` ``).
 *
 * Returns empty arrays for missing/empty sections — callers should treat
 * tasks with no IO as ambiguous (sequential fallback trigger).
 */
export function parseTaskPlanIO(content) {
    const backtickPathRegex = /`([^`]+)`/g;
    function extractPaths(sectionText) {
        if (!sectionText)
            return [];
        const paths = [];
        for (const line of sectionText.split("\n")) {
            const trimmed = line.trim();
            if (!trimmed || trimmed.startsWith("#"))
                continue;
            let match;
            backtickPathRegex.lastIndex = 0;
            while ((match = backtickPathRegex.exec(trimmed)) !== null) {
                const candidate = match[1];
                // Filter out things that look like code tokens rather than file paths
                // (e.g. `true`, `false`, `npm run test`). A file path has at least one
                // dot or slash.
                if (candidate.includes("/") || candidate.includes(".")) {
                    paths.push(candidate);
                }
            }
        }
        return paths;
    }
    const [, body] = splitFrontmatter(content);
    const inputSection = extractSection(body, "Inputs");
    const outputSection = extractSection(body, "Expected Output");
    return {
        inputFiles: extractPaths(inputSection),
        outputFiles: extractPaths(outputSection),
    };
}
/**
 * Extract the UAT type from a UAT file's raw content.
 *
 * UAT files have no YAML frontmatter - pass raw file content directly.
 * Classification is leading-keyword-only: e.g. `mixed (artifact-driven + live-runtime)` → `'mixed'`.
 *
 * Returns `undefined` when:
 * - the `## UAT Type` section is absent
 * - no `UAT mode:` bullet is found in the section
 * - the value does not start with a recognised keyword
 */
export function extractUatType(content) {
    const sectionText = extractSection(content, 'UAT Type');
    if (!sectionText)
        return undefined;
    const bullets = parseBullets(sectionText);
    const modeBullet = bullets.find(b => b.startsWith('UAT mode:'));
    if (!modeBullet)
        return undefined;
    const rawValue = modeBullet.slice('UAT mode:'.length).trim().toLowerCase();
    if (rawValue.startsWith('artifact-driven'))
        return 'artifact-driven';
    if (rawValue.startsWith('browser-executable'))
        return 'browser-executable';
    if (rawValue.startsWith('runtime-executable'))
        return 'runtime-executable';
    if (rawValue.startsWith('live-runtime'))
        return 'live-runtime';
    if (rawValue.startsWith('human-experience'))
        return 'human-experience';
    if (rawValue.startsWith('mixed'))
        return 'mixed';
    return undefined;
}
/**
 * Extract the `depends_on` list from M00x-CONTEXT.md YAML frontmatter.
 * Returns [] when: content is null, no frontmatter block, field absent, or field is empty.
 * Normalizes each dep ID to uppercase (e.g. 'm001' → 'M001').
 */
export function parseContextDependsOn(content) {
    if (!content)
        return [];
    const [fmLines] = splitFrontmatter(content);
    if (!fmLines)
        return [];
    const fm = parseFrontmatterMap(fmLines);
    const raw = fm['depends_on'];
    if (!Array.isArray(raw) || raw.length === 0)
        return [];
    return raw.map(s => String(s).trim()).filter(Boolean);
}
/**
 * Inline the prior milestone's SUMMARY.md as context for the current milestone's planning prompt.
 * Returns null when: (1) `mid` is the first milestone, (2) prior milestone has no SUMMARY file.
 *
 * Uses the shared findMilestoneIds to scan the milestones directory.
 */
export async function inlinePriorMilestoneSummary(mid, base) {
    const sorted = findMilestoneIds(base);
    if (sorted.length === 0)
        return null;
    const idx = sorted.indexOf(mid);
    if (idx <= 0)
        return null;
    const prevMid = sorted[idx - 1];
    const absPath = resolveMilestoneFile(base, prevMid, "SUMMARY");
    const relPath = relMilestoneFile(base, prevMid, "SUMMARY");
    const content = absPath ? await loadFile(absPath) : null;
    if (!content)
        return null;
    return `### Prior Milestone Summary\nSource: \`${relPath}\`\n\n${content.trim()}`;
}
// ─── Manifest Status ──────────────────────────────────────────────────────
/**
 * Read a secrets manifest from disk and cross-reference each entry's status
 * with the current environment (.env + process.env).
 *
 * Returns `null` when no manifest file exists (path resolution failure or
 * file not on disk) - callers can distinguish "no manifest" from "empty manifest".
 */
export async function getManifestStatus(base, milestoneId, projectRoot) {
    const resolvedPath = resolveMilestoneFile(base, milestoneId, 'SECRETS');
    if (!resolvedPath)
        return null;
    const content = await loadFile(resolvedPath);
    if (!content)
        return null;
    const manifest = parseSecretsManifest(content);
    const keys = manifest.entries.map(e => e.key);
    // Check both the base path .env AND the project root .env (#1387).
    // In worktree mode, base is the worktree path which may not have .env.
    // The project root's .env is where the user actually defined their keys.
    const existingKeys = await checkExistingEnvKeys(keys, resolve(base, '.env'));
    const existingSet = new Set(existingKeys);
    if (projectRoot && projectRoot !== base) {
        const rootKeys = await checkExistingEnvKeys(keys, resolve(projectRoot, '.env'));
        for (const k of rootKeys)
            existingSet.add(k);
    }
    const result = {
        pending: [],
        collected: [],
        skipped: [],
        existing: [],
    };
    for (const entry of manifest.entries) {
        if (existingSet.has(entry.key)) {
            result.existing.push(entry.key);
        }
        else {
            result[entry.status].push(entry.key);
        }
    }
    return result;
}
export async function appendOverride(basePath, change, appliedAt) {
    const overridesPath = resolveNerfRootFile(basePath, "OVERRIDES");
    const timestamp = new Date().toISOString();
    const entry = [
        `## Override: ${timestamp}`,
        "",
        `**Change:** ${change}`,
        `**Scope:** active`,
        `**Applied-at:** ${appliedAt}`,
        "",
        "---",
        "",
    ].join("\n");
    const existing = await loadFile(overridesPath);
    if (existing) {
        await saveFile(overridesPath, existing.trimEnd() + "\n\n" + entry);
    }
    else {
        const header = [
            "# NERF Overrides",
            "",
            "User-issued overrides that supersede plan document content.",
            "",
            "---",
            "",
        ].join("\n");
        await saveFile(overridesPath, header + entry);
    }
}
export async function appendKnowledge(basePath, type, entry, scope) {
    const knowledgePath = resolveNerfRootFile(basePath, "KNOWLEDGE");
    const existing = await loadFile(knowledgePath);
    if (existing) {
        // Find the next ID for this type
        const prefix = type === "rule" ? "K" : type === "pattern" ? "P" : "L";
        const idPattern = new RegExp(`^\\| ${prefix}(\\d+)`, "gm");
        let maxId = 0;
        let match;
        while ((match = idPattern.exec(existing)) !== null) {
            const num = parseInt(match[1], 10);
            if (num > maxId)
                maxId = num;
        }
        const nextId = `${prefix}${String(maxId + 1).padStart(3, "0")}`;
        // Build the table row
        let row;
        if (type === "rule") {
            row = `| ${nextId} | ${scope} | ${entry} | — | manual |`;
        }
        else if (type === "pattern") {
            row = `| ${nextId} | ${entry} | — | ${scope} |`;
        }
        else {
            row = `| ${nextId} | ${entry} | — | — | ${scope} |`;
        }
        // Find the right section and append after the table header
        const sectionHeading = type === "rule" ? "## Rules" : type === "pattern" ? "## Patterns" : "## Lessons Learned";
        const sectionIdx = existing.indexOf(sectionHeading);
        if (sectionIdx !== -1) {
            // Find the end of the table header row (the |---|...| line)
            const afterHeading = existing.indexOf("\n", sectionIdx);
            // Find the next section or end
            const nextSection = existing.indexOf("\n## ", afterHeading + 1);
            const insertPoint = nextSection !== -1 ? nextSection : existing.length;
            // Insert row before the next section (or at end)
            const before = existing.slice(0, insertPoint).trimEnd();
            const after = existing.slice(insertPoint);
            await saveFile(knowledgePath, before + "\n" + row + "\n" + after);
        }
        else {
            // Section not found — append at end
            await saveFile(knowledgePath, existing.trimEnd() + "\n\n" + row + "\n");
        }
    }
    else {
        // Create file from scratch with template header
        const header = [
            "# Project Knowledge",
            "",
            "Append-only register of project-specific rules, patterns, and lessons learned.",
            "Agents read this before every unit. Add entries when you discover something worth remembering.",
            "",
        ].join("\n");
        let content;
        if (type === "rule") {
            content = header + [
                "## Rules",
                "",
                "| # | Scope | Rule | Why | Added |",
                "|---|-------|------|-----|-------|",
                `| K001 | ${scope} | ${entry} | — | manual |`,
                "",
                "## Patterns",
                "",
                "| # | Pattern | Where | Notes |",
                "|---|---------|-------|-------|",
                "",
                "## Lessons Learned",
                "",
                "| # | What Happened | Root Cause | Fix | Scope |",
                "|---|--------------|------------|-----|-------|",
                "",
            ].join("\n");
        }
        else if (type === "pattern") {
            content = header + [
                "## Rules",
                "",
                "| # | Scope | Rule | Why | Added |",
                "|---|-------|------|-----|-------|",
                "",
                "## Patterns",
                "",
                "| # | Pattern | Where | Notes |",
                "|---|---------|-------|-------|",
                `| P001 | ${entry} | — | ${scope} |`,
                "",
                "## Lessons Learned",
                "",
                "| # | What Happened | Root Cause | Fix | Scope |",
                "|---|--------------|------------|-----|-------|",
                "",
            ].join("\n");
        }
        else {
            content = header + [
                "## Rules",
                "",
                "| # | Scope | Rule | Why | Added |",
                "|---|-------|------|-----|-------|",
                "",
                "## Patterns",
                "",
                "| # | Pattern | Where | Notes |",
                "|---|---------|-------|-------|",
                "",
                "## Lessons Learned",
                "",
                "| # | What Happened | Root Cause | Fix | Scope |",
                "|---|--------------|------------|-----|-------|",
                `| L001 | ${entry} | — | — | ${scope} |`,
                "",
            ].join("\n");
        }
        await saveFile(knowledgePath, content);
    }
}
export async function loadActiveOverrides(basePath) {
    const overridesPath = resolveNerfRootFile(basePath, "OVERRIDES");
    const content = await loadFile(overridesPath);
    if (!content)
        return [];
    return parseOverrides(content).filter(o => o.scope === "active");
}
export function parseOverrides(content) {
    const overrides = [];
    const blocks = content.split(/^## Override: /m).slice(1);
    for (const block of blocks) {
        const lines = block.split("\n");
        const timestamp = lines[0]?.trim() ?? "";
        let change = "";
        let scope = "active";
        let appliedAt = "";
        for (const line of lines) {
            const changeMatch = line.match(/^\*\*Change:\*\*\s*(.+)$/);
            if (changeMatch)
                change = changeMatch[1].trim();
            const scopeMatch = line.match(/^\*\*Scope:\*\*\s*(.+)$/);
            if (scopeMatch)
                scope = scopeMatch[1].trim();
            const appliedMatch = line.match(/^\*\*Applied-at:\*\*\s*(.+)$/);
            if (appliedMatch)
                appliedAt = appliedMatch[1].trim();
        }
        if (change) {
            overrides.push({ timestamp, change, scope, appliedAt });
        }
    }
    return overrides;
}
export function formatOverridesSection(overrides) {
    if (overrides.length === 0)
        return "";
    const entries = overrides.map((o, i) => [
        `${i + 1}. **${o.change}**`,
        `   _Issued: ${o.timestamp} during ${o.appliedAt}_`,
    ].join("\n")).join("\n");
    return [
        "## Active Overrides (supersede plan content)",
        "",
        "The following overrides were issued by the user and supersede any conflicting content in plan documents below. Follow these overrides even if they contradict the inlined task plan.",
        "",
        entries,
        "",
    ].join("\n");
}
export async function resolveAllOverrides(basePath) {
    const overridesPath = resolveNerfRootFile(basePath, "OVERRIDES");
    const content = await loadFile(overridesPath);
    if (!content)
        return;
    const updated = content.replace(/\*\*Scope:\*\* active/g, "**Scope:** resolved");
    await saveFile(overridesPath, updated);
}
