import YAML from 'yaml';
import { hashText } from '../cache.js';
import { redactGraphText } from '../query.js';
function frontmatter(content) {
    if (!content.startsWith('---')) {
        return {};
    }
    const end = content.indexOf('\n---', 3);
    if (end === -1) {
        return {};
    }
    try {
        const parsed = YAML.parse(content.slice(3, end));
        return parsed !== null && typeof parsed === 'object' ? parsed : {};
    }
    catch {
        return {};
    }
}
function asString(value) {
    return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
}
function criterionTexts(fm, content) {
    const raw = Array.isArray(fm.criteria) ? fm.criteria : [];
    const fromFrontmatter = raw
        .map((item) => typeof item === 'object' && item !== null && 'text' in item
        ? asString(item.text)
        : undefined)
        .filter((item) => item !== undefined);
    if (fromFrontmatter.length > 0) {
        return fromFrontmatter;
    }
    return [...content.matchAll(/^- \[[ x]\]\s+(.+)$/gim)]
        .map((match) => (match[1] ?? '').trim())
        .filter((item) => item.length > 0);
}
function extractBacktickPaths(text, prefix) {
    const idx = text.indexOf(prefix);
    if (idx === -1) {
        return [];
    }
    const fragment = text.slice(idx + prefix.length);
    return [...fragment.matchAll(/`([^`]+)`/g)]
        .map((match) => (match[1] ?? '').trim())
        .filter((item) => item.length > 0);
}
function uniqueById(items) {
    return [...new Map(items.map((item) => [item.id, item])).values()];
}
function node(args) {
    return {
        id: args.id,
        type: args.type,
        label: redactGraphText(args.label, args.policy),
        source: 'spec-extractor',
        evidence: [{ path: args.source.path, hash: args.source.hash }],
        metadata: args.metadata,
        updatedAt: new Date().toISOString(),
    };
}
function edge(args) {
    const id = `edge:${hashText(`${args.from}:${args.type}:${args.to}:${args.selector ?? ''}`)}`;
    return {
        id,
        from: args.from,
        to: args.to,
        type: args.type,
        source: 'spec-extractor',
        confidence: args.confidence ?? 'high',
        classification: args.classification ?? 'extracted',
        evidence: { path: args.source.path, selector: args.selector, hash: args.source.hash },
        updatedAt: new Date().toISOString(),
    };
}
function sectionBullets(content, heading) {
    const pattern = new RegExp(`^##+\\s+${heading}\\s*$`, 'im');
    const match = pattern.exec(content);
    if (!match) {
        return [];
    }
    const start = match.index + match[0].length;
    const rest = content.slice(start);
    const nextHeading = rest.search(/^##+\s+/m);
    const section = nextHeading === -1 ? rest : rest.slice(0, nextHeading);
    return [...section.matchAll(/^-+\s+(.+)$/gm)]
        .map((item) => (item[1] ?? '').trim())
        .filter((item) => item.length > 0)
        .slice(0, 20);
}
function appendRiskAndDecisionNodes(args) {
    for (const [index, risk] of [
        ...sectionBullets(args.source.content, 'Risks'),
        ...sectionBullets(args.source.content, 'Edge Cases And Failure Modes'),
    ].entries()) {
        const riskId = `risk:${args.specId}:${String(index + 1)}`;
        args.nodes.push(node({
            id: riskId,
            type: 'risk',
            label: risk,
            source: args.source,
            policy: args.policy,
            metadata: { specId: args.specId },
        }));
        args.edges.push(edge({ from: args.specNodeId, to: riskId, type: 'has_risk', source: args.source }));
    }
    for (const [index, decision] of [
        ...sectionBullets(args.source.content, 'Hard constraints'),
        ...sectionBullets(args.source.content, 'Non-Goals And Forbidden Approaches'),
    ].entries()) {
        const decisionId = `decision:${args.specId}:${String(index + 1)}`;
        args.nodes.push(node({
            id: decisionId,
            type: 'decision',
            label: decision,
            source: args.source,
            policy: args.policy,
            metadata: { specId: args.specId },
        }));
        args.edges.push(edge({ from: args.specNodeId, to: decisionId, type: 'decides', source: args.source }));
    }
}
function appendToolNodes(args) {
    for (const toolName of args.policy.toolNamePatterns) {
        if (args.source.content.includes(toolName)) {
            const toolId = `tool:${toolName}`;
            args.nodes.push(node({
                id: toolId,
                type: 'tool',
                label: toolName,
                source: args.source,
                policy: args.policy,
            }));
            args.edges.push(edge({
                from: args.specNodeId,
                to: toolId,
                type: 'uses_tool',
                source: args.source,
                confidence: 'medium',
                classification: 'inferred',
            }));
        }
    }
}
export function extractSpecGraph(source, policy) {
    const fm = frontmatter(source.content);
    const specId = asString(fm.id) ?? source.path.split('/').find((part) => /^SPEC-\d+/.test(part));
    if (specId === undefined) {
        return { nodes: [], edges: [] };
    }
    const title = asString(fm.title) ?? specId;
    const specNodeId = `spec:${specId}`;
    const nodes = [
        node({
            id: specNodeId,
            type: 'spec',
            label: `${specId} ${title}`,
            source,
            policy,
            metadata: { specId, status: asString(fm.status) ?? null },
        }),
    ];
    const edges = [];
    for (const [index, text] of criterionTexts(fm, source.content).entries()) {
        const criterionId = `criterion:${specId}:${String(index + 1)}`;
        nodes.push(node({
            id: criterionId,
            type: 'criterion',
            label: text,
            source,
            policy,
            metadata: { specId, index: index + 1 },
        }));
        edges.push(edge({ from: specNodeId, to: criterionId, type: 'contains', source }));
        for (const file of extractBacktickPaths(text, 'FILES:')) {
            const fileId = `file:${file}`;
            nodes.push(node({ id: fileId, type: 'file', label: file, source, policy }));
            edges.push(edge({ from: criterionId, to: fileId, type: 'implements', source }));
        }
        for (const test of extractBacktickPaths(text, 'TEST:')) {
            const testId = `test:${test}`;
            nodes.push(node({ id: testId, type: 'test', label: test, source, policy }));
            edges.push(edge({ from: criterionId, to: testId, type: 'tests', source }));
        }
    }
    appendRiskAndDecisionNodes({ nodes, edges, specNodeId, specId, source, policy });
    appendToolNodes({ nodes, edges, specNodeId, source, policy });
    return { nodes: uniqueById(nodes), edges: uniqueById(edges) };
}
//# sourceMappingURL=spec-extractor.js.map