/**
 * @internal
 * grep-claims.mjs — scan a markdown document for stale references that a
 * diff has just invalidated.
 *
 * Input:
 *   filepath:  absolute path to a markdown file
 *   diffFacts: {
 *     deletedPaths: string[]        // paths removed by the diff
 *     renamedPaths: {from, to}[]    // renames (old path = stale claim target)
 *     addedExports: {file, symbol}[]   // reserved for future symbol-level drift
 *     removedExports: {file, symbol}[] // reserved for future symbol-level drift
 *   }
 *
 * Output:
 *   Array of findings:
 *     { file, line, column, stale_claim, evidence, confidence, ... }
 *
 * Semantics:
 *   - Fenced code blocks (```…```) and HTML comments (<!-- … -->) are skipped
 *     entirely — references inside those are examples, not claims.
 *   - A whole-path match (e.g. `packages/foo` appears literally) emits a
 *     'high' confidence finding.
 *   - A parent-dir match (diff deleted `packages/foo/bar.ts`, doc mentions
 *     `packages/foo/`) emits a 'medium' confidence finding.
 *   - Renames carry a `suggested_rewrite` field with the new path.
 *
 * This module is deliberately narrow — it is a grep engine, not an
 * orchestrator. The orchestrator (`audit.mjs`) decides which surfaces to
 * scan and how to bucket the findings by policy.
 */
import { readFileSync } from 'node:fs';
/**
 * Walk a markdown file line-by-line and collect stale-reference findings.
 *
 * @returns {Array<{file, line, column, stale_claim, evidence, confidence, suggested_rewrite?}>}
 */
export function findStaleClaimsInMarkdown(filepath, diffFacts) {
    if (!filepath)
        return [];
    const facts = normalizeDiffFacts(diffFacts);
    let source;
    try {
        source = readFileSync(filepath, 'utf-8');
    }
    catch {
        return [];
    }
    const lines = source.split(/\r?\n/);
    const findings = [];
    const scanState = { inFence: false, inHtmlComment: false };
    for (let i = 0; i < lines.length; i++) {
        const line = lines[i];
        updateScanState(line, scanState);
        if (scanState.inFence)
            continue;
        // Skip the entire line when the fence opened on this same line —
        // updateScanState handles entry/exit but the line itself is fenced.
        if (isFenceLine(line))
            continue;
        if (scanState.inHtmlComment)
            continue;
        if (isHtmlCommentLine(line))
            continue;
        // Pure high-confidence deleted-path match.
        for (const deleted of facts.deletedPaths) {
            if (!deleted)
                continue;
            const match = matchPathLiteral(line, deleted);
            if (match) {
                findings.push({
                    file: filepath,
                    line: i + 1,
                    column: match.column,
                    stale_claim: deleted,
                    evidence: line.trim(),
                    confidence: 'high',
                    kind: 'deleted-path',
                });
                continue;
            }
            // Medium confidence: the doc mentions the PARENT of a deleted file.
            // e.g. diff deleted `packages/foo/bar.ts`; doc says `packages/foo`
            // or `packages/foo/`. Try both shapes — the literal match enforces
            // word boundaries so we don't over-match on `packages/foobar`.
            const parents = parentDirsOf(deleted);
            for (const parent of parents) {
                const pmatch = matchPathLiteral(line, parent) || matchPathLiteral(line, parent + '/');
                if (pmatch) {
                    findings.push({
                        file: filepath,
                        line: i + 1,
                        column: pmatch.column,
                        stale_claim: parent,
                        evidence: line.trim(),
                        confidence: 'medium',
                        kind: 'deleted-parent',
                    });
                    break;
                }
            }
        }
        // Renames — match the old path; suggest the new.
        for (const { from, to } of facts.renamedPaths) {
            if (!from)
                continue;
            const match = matchPathLiteral(line, from);
            if (match) {
                findings.push({
                    file: filepath,
                    line: i + 1,
                    column: match.column,
                    stale_claim: from,
                    evidence: line.trim(),
                    confidence: 'high',
                    kind: 'renamed-path',
                    suggested_rewrite: to,
                });
            }
        }
    }
    return findings;
}
function normalizeDiffFacts(facts) {
    return {
        deletedPaths: Array.isArray(facts?.deletedPaths) ? facts.deletedPaths.filter(Boolean) : [],
        renamedPaths: Array.isArray(facts?.renamedPaths)
            ? facts.renamedPaths.filter((r) => r?.from && r?.to)
            : [],
        addedExports: Array.isArray(facts?.addedExports) ? facts.addedExports : [],
        removedExports: Array.isArray(facts?.removedExports) ? facts.removedExports : [],
    };
}
function isFenceLine(line) {
    return /^```/.test(line);
}
function isHtmlCommentLine(line) {
    // Single-line HTML comment: <!-- ... -->
    return /^\s*<!--.*-->\s*$/.test(line);
}
function updateScanState(line, scanState) {
    // Toggle fence state on any line that opens or closes a fence.
    if (isFenceLine(line)) {
        scanState.inFence = !scanState.inFence;
        return;
    }
    // HTML comments can span multiple lines: track open/close independently.
    if (/<!--/.test(line) && !/-->/.test(line)) {
        scanState.inHtmlComment = true;
        return;
    }
    if (scanState.inHtmlComment && /-->/.test(line)) {
        scanState.inHtmlComment = false;
        return;
    }
}
/**
 * Match `needle` as a path-ish substring on `haystack`. Uses boundary chars so
 * `packages/foo` doesn't match `packages/foobar`. Returns `{column}` (1-based)
 * of the first match, or null.
 */
function matchPathLiteral(haystack, needle) {
    const escaped = needle.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
    // Path boundary: beginning-of-line OR a non-path char; followed by
    // end-of-string OR a non-path-continuation char.
    // Path-continuation chars we consider valid: letter, digit, dash, dot,
    // underscore, slash.
    const re = new RegExp(`(^|[^A-Za-z0-9_./-])(${escaped})(?![A-Za-z0-9_./-])`);
    const m = re.exec(haystack);
    if (!m)
        return null;
    const column = m.index + (m[1] ? m[1].length : 0) + 1;
    return { column };
}
/**
 * Return the immediate parent directory for a path. For
 * `packages/foo/bar/baz.ts` → ['packages/foo/bar']. Returns [] if the path has
 * no parent (single segment) or the parent is the repo root.
 *
 * We DON'T recurse upward: matching `packages/` for a deleted
 * `packages/foo/bar.ts` is too broad — `packages/` appears in prose that is
 * still accurate (e.g. "packages/ is the workspace root"). Only the immediate
 * parent carries useful signal.
 */
function parentDirsOf(path) {
    const norm = String(path).replace(/\\/g, '/');
    const parts = norm.split('/').filter(Boolean);
    if (parts.length <= 1)
        return [];
    const parent = parts.slice(0, parts.length - 1).join('/');
    // Don't return bare top-level segments like 'packages' — too broad.
    if (!parent.includes('/'))
        return [];
    return [parent];
}
//# sourceMappingURL=grep-claims.mjs.map