import { createHash } from "crypto";
import { mkdir, readFile, rename, rm, writeFile } from "fs/promises";
import { dirname, resolve } from "path";
import { loadMemorySettings, summarizeMemorySettings, usesLocalGraphBackend, } from "./memory-config.js";
export const ONTOLOGY = {
    version: "omk-ontology-mindmap-v1",
    description: "Project-local ontology for Kimi/OMK memory. Memories are decomposed into mind-map nodes for goals, topics, decisions, tasks, risks, commands, files, evidence, provider routes, provider fallbacks, questions, answers, constraints, and concepts.",
    classes: [
        "Project",
        "Session",
        "Memory",
        "MemoryVersion",
        "Run",
        "Goal",
        "Topic",
        "Decision",
        "Task",
        "Risk",
        "Command",
        "File",
        "Evidence",
        "Provider",
        "ProviderRoute",
        "ProviderFallback",
        "AuditLink",
        "Constraint",
        "Question",
        "Answer",
        "Concept",
    ],
    relationTypes: [
        "HAS_SESSION",
        "HAS_MEMORY",
        "HAS_RUN",
        "WROTE",
        "UPDATES",
        "HAS_GOAL",
        "HAS_TOPIC",
        "HAS_DECISION",
        "HAS_TASK",
        "HAS_RISK",
        "HAS_COMMAND",
        "HAS_FILE",
        "HAS_EVIDENCE",
        "USES_PROVIDER",
        "HAS_PROVIDER_ROUTE",
        "ROUTES_TO",
        "FALLS_BACK_TO",
        "HAS_PROVIDER_FALLBACK",
        "HAS_AUDIT_LINK",
        "LINKS_TO",
        "HAS_CONSTRAINT",
        "HAS_QUESTION",
        "HAS_ANSWER",
        "HAS_CONCEPT",
        "CHILD_OF",
        "PART_OF",
        "DEPENDS_ON",
        "BLOCKED_BY",
        "EVIDENCED_BY",
        "TOUCHES_FILE",
    ],
};
const GENERATED_TYPES = new Set([
    "Run",
    "Goal",
    "Topic",
    "Decision",
    "Task",
    "Risk",
    "Command",
    "File",
    "Evidence",
    "Provider",
    "ProviderRoute",
    "ProviderFallback",
    "AuditLink",
    "Constraint",
    "Question",
    "Answer",
    "Concept",
]);
const CANONICAL_NODE_TYPES = new Set([
    "File",
    "Symbol",
    "Decision",
    "Risk",
    "Evidence",
    "Goal",
    "Run",
    "Task",
    "MCPServer",
    "Skill",
    "Provider",
    "ProviderRoute",
    "ProviderFallback",
    "AuditLink",
]);
const graphWriteQueues = new Map();
function enqueueGraphWrite(graphPath, operation) {
    const previous = graphWriteQueues.get(graphPath) ?? Promise.resolve();
    const running = previous.catch(() => undefined).then(operation);
    const release = running.then(() => undefined, () => undefined);
    graphWriteQueues.set(graphPath, release);
    return running.finally(() => {
        if (graphWriteQueues.get(graphPath) === release) {
            graphWriteQueues.delete(graphPath);
        }
    });
}
function errorCode(err) {
    if (typeof err !== "object" || err === null || !("code" in err))
        return undefined;
    const code = err.code;
    return typeof code === "string" ? code : undefined;
}
function errorMessage(err) {
    return err instanceof Error ? err.message : String(err);
}
function timestampForPath(date = new Date()) {
    return date.toISOString().replace(/[:.]/g, "-");
}
async function writeFileAtomic(path, payload) {
    await mkdir(dirname(path), { recursive: true });
    const tempPath = `${path}.${process.pid}.${Date.now()}.${hash(payload).slice(0, 8)}.tmp`;
    try {
        await writeFile(tempPath, payload, "utf-8");
        await rename(tempPath, path);
    }
    catch (err) {
        await rm(tempPath, { force: true }).catch(() => { });
        throw err;
    }
}
export class LocalGraphMemoryStore {
    settings;
    source;
    constructor(settings, source = "omk-local-graph-memory") {
        this.settings = settings;
        this.source = source;
    }
    static async create(options = {}) {
        const env = options.sessionId
            ? { ...(options.env ?? process.env), OMK_SESSION_ID: options.sessionId }
            : options.env ?? process.env;
        const settings = await loadMemorySettings(options.projectRoot, env);
        if (!usesLocalGraphBackend(settings.backend))
            return null;
        return new LocalGraphMemoryStore(settings, options.source ?? "omk-local-graph-memory");
    }
    get status() {
        return summarizeMemorySettings(this.settings);
    }
    get strict() {
        return this.settings.strict;
    }
    get mirrorFiles() {
        return this.settings.mirrorFiles;
    }
    get migrateFiles() {
        return this.settings.migrateFiles;
    }
    async read(path) {
        const state = await this.loadState();
        return this.readFromState(state, path);
    }
    async write(path, content) {
        await this.mutateState((state, now) => {
            this.applyMemoryWrite(state, path, content, now);
        });
    }
    applyMemoryWrite(state, path, content, now) {
        state.updatedAt = now;
        state.project = { ...this.settings.project };
        state.ontology = ONTOLOGY;
        const projectId = this.nodeId("Project", this.settings.project.key);
        const sessionId = this.nodeId("Session", this.settings.session.key);
        const memoryId = this.memoryNodeId(path);
        const versionId = this.nodeId("MemoryVersion", `${path}\n${now}\n${hash(content)}`);
        this.upsertNode(state, {
            id: projectId,
            type: "Project",
            labels: ["OmkProject", "Project"],
            label: this.settings.project.name,
            summary: this.settings.project.root,
            tags: ["project"],
            properties: {
                key: this.settings.project.key,
                root: this.settings.project.root,
            },
            createdAt: now,
            updatedAt: now,
        });
        this.upsertNode(state, {
            id: sessionId,
            type: "Session",
            labels: ["OmkSession", "Session"],
            label: this.settings.session.id,
            summary: `Session for ${this.settings.project.name}`,
            tags: ["session"],
            properties: {
                key: this.settings.session.key,
                sessionId: this.settings.session.id,
                projectKey: this.settings.project.key,
            },
            createdAt: now,
            updatedAt: now,
        });
        this.upsertNode(state, {
            id: memoryId,
            type: "Memory",
            labels: ["OmkMemory", "Memory"],
            label: path,
            path,
            content: truncate(content, 500),
            summary: summarize(content),
            tags: ["memory", ...pathTags(path)],
            properties: {
                key: this.memoryKey(path),
                path,
                projectKey: this.settings.project.key,
                sessionId: this.settings.session.id,
                source: this.source,
            },
            createdAt: now,
            updatedAt: now,
        });
        this.upsertNode(state, {
            id: versionId,
            type: "MemoryVersion",
            labels: ["OmkMemoryVersion", "MemoryVersion"],
            label: `${path}@${now}`,
            path,
            content,
            summary: summarize(content),
            tags: ["memory-version", ...pathTags(path)],
            properties: {
                key: `${this.settings.session.key}:${hash(`${path}\n${now}\n${content}`)}`,
                path,
                projectKey: this.settings.project.key,
                sessionId: this.settings.session.id,
                source: this.source,
            },
            createdAt: now,
            updatedAt: now,
        });
        this.upsertEdge(state, projectId, sessionId, "HAS_SESSION", now);
        this.upsertEdge(state, projectId, memoryId, "HAS_MEMORY", now);
        this.upsertEdge(state, sessionId, versionId, "WROTE", now);
        this.upsertEdge(state, versionId, memoryId, "UPDATES", now);
        this.replaceGeneratedMindmap(state, memoryId, content, path, now);
    }
    async writeMirrorFiles(state) {
        const mirrorDir = dirname(this.settings.localGraph.path);
        await mkdir(mirrorDir, { recursive: true });
        const projectNode = state.nodes.find((n) => n.type === "Project");
        const projectOverview = [
            `# ${projectNode?.label ?? state.project.name ?? "Project"}`,
            "",
            `**Key:** ${state.project.key}`,
            `**Root:** ${state.project.root}`,
            `**Updated:** ${state.updatedAt}`,
            "",
            `## Ontology`,
            `- Version: ${state.ontology.version}`,
            `- Classes: ${state.ontology.classes.join(", ")}`,
            `- Relations: ${state.ontology.relationTypes.join(", ")}`,
            "",
        ].join("\n");
        await writeFile(`${mirrorDir}/project.md`, projectOverview, "utf-8");
        const byType = (type) => state.nodes.filter((n) => n.type === type);
        const renderNodes = (nodes) => nodes
            .map((n) => `### ${n.label}\n${n.summary ?? ""}\n- Tags: ${n.tags.join(", ")}\n`)
            .join("\n");
        await writeFile(`${mirrorDir}/goals.md`, `# Goals\n\n${renderNodes(byType("Goal"))}`, "utf-8");
        await writeFile(`${mirrorDir}/decisions.md`, `# Decisions\n\n${renderNodes(byType("Decision"))}`, "utf-8");
        await writeFile(`${mirrorDir}/commands.md`, `# Commands\n\n${renderNodes(byType("Command"))}`, "utf-8");
        await writeFile(`${mirrorDir}/risks.md`, `# Risks\n\n${renderNodes(byType("Risk"))}`, "utf-8");
    }
    async append(path, content) {
        await this.mutateState((state, now) => {
            const existing = this.readFromState(state, path);
            this.applyMemoryWrite(state, path, existing ? `${existing}\n${content}` : content, now);
        });
    }
    async search(query, limit = 10) {
        const state = await this.loadState();
        const normalizedQuery = query.trim().toLowerCase();
        const safeLimit = Math.max(1, Math.min(50, Math.floor(limit)) || 10);
        return state.nodes
            .filter((node) => node.type === "Memory")
            .filter((node) => {
            const content = this.readFromState(state, node.path ?? node.label);
            if (!normalizedQuery)
                return true;
            return [node.path, node.label, node.summary, content]
                .filter((value) => typeof value === "string")
                .some((value) => value.toLowerCase().includes(normalizedQuery));
        })
            .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
            .slice(0, safeLimit)
            .map((node) => ({
            path: node.path ?? node.label,
            content: this.readFromState(state, node.path ?? node.label),
            sessionId: String(node.properties.sessionId ?? ""),
            updatedAt: node.updatedAt,
            source: String(node.properties.source ?? this.source),
        }));
    }
    async ontology() {
        return ONTOLOGY;
    }
    async mindmap(query = "", limit = 80) {
        const state = await this.loadState();
        const normalizedQuery = query.trim().toLowerCase();
        const safeLimit = Math.max(1, Math.min(250, Math.floor(limit)) || 80);
        const rootId = this.nodeId("Project", this.settings.project.key);
        const rootNode = state.nodes.find((node) => node.id === rootId) ?? this.defaultRootNode(rootId);
        const nodeMatches = (node) => {
            if (!normalizedQuery)
                return true;
            return [node.label, node.path, node.summary, node.content, ...node.tags]
                .filter((value) => typeof value === "string")
                .some((value) => value.toLowerCase().includes(normalizedQuery));
        };
        const seedIds = new Set([rootId]);
        for (const node of state.nodes) {
            if (nodeMatches(node))
                seedIds.add(node.id);
        }
        const includedIds = this.expandNeighborhood(state, seedIds, safeLimit);
        includedIds.add(rootId);
        const flatNodes = state.nodes
            .filter((node) => includedIds.has(node.id))
            .map((node) => this.toMindmapFlatNode(node));
        const edges = state.edges
            .filter((edge) => includedIds.has(edge.from) && includedIds.has(edge.to))
            .map((edge) => ({ from: edge.from, to: edge.to, type: edge.type, label: edge.label }));
        const root = this.buildMindmapTree(rootNode, state, includedIds, new Set());
        return { root, nodes: flatNodes, edges, ontology: ONTOLOGY };
    }
    /**
     * Idempotent finalizer that links a finalized run manifest into the local
     * graph. Emits run -> providerRoute -> provider, run -> evidence,
     * run -> decision, and run -> artifact nodes/edges.
     *
     * Privacy: only ids, status, paths, sha256, and aggregate counts are stored.
     * Raw evidence/decision message bodies and secrets are never persisted.
     *
     * Idempotency: node/edge ids are deterministic and prior run-generated
     * nodes/edges (tagged `generatedFromRun`) are pruned before re-inserting, so
     * re-running with the same (or a changed) manifest never duplicates state.
     */
    async linkRun(runId, manifest) {
        const canonicalRunId = manifest.runId || runId;
        if (runId && manifest.runId && runId !== manifest.runId) {
            throw new Error(`linkRun: runId mismatch (param=${runId}, manifest=${manifest.runId})`);
        }
        await this.mutateState((state, now) => {
            this.applyRunManifest(state, canonicalRunId, manifest, now);
        });
    }
    applyRunManifest(state, runId, manifest, now) {
        state.updatedAt = now;
        state.project = { ...this.settings.project };
        state.ontology = ONTOLOGY;
        // Prune prior run-generated child nodes/edges for idempotent re-runs. The
        // Run node and shared Provider nodes are intentionally not pruned (the Run
        // node is re-upserted preserving createdAt; Providers are shared by runs).
        const staleNodeIds = new Set(state.nodes.filter((node) => node.properties.generatedFromRun === runId).map((node) => node.id));
        state.nodes = state.nodes.filter((node) => !staleNodeIds.has(node.id));
        state.edges = state.edges.filter((edge) => edge.properties.generatedFromRun !== runId && !staleNodeIds.has(edge.from) && !staleNodeIds.has(edge.to));
        const tag = { generatedFromRun: runId };
        const projectId = this.nodeId("Project", this.settings.project.key);
        this.upsertNode(state, {
            id: projectId,
            type: "Project",
            labels: ["OmkProject", "Project"],
            label: this.settings.project.name,
            summary: this.settings.project.root,
            tags: ["project"],
            properties: { key: this.settings.project.key, root: this.settings.project.root },
            createdAt: now,
            updatedAt: now,
        });
        const runNodeId = this.nodeId("Run", runId);
        this.upsertNode(state, {
            id: runNodeId,
            type: "Run",
            labels: ["OmkRun", "Run"],
            label: runId,
            summary: `Run ${runId} (${manifest.status})`,
            tags: ["run", manifest.status],
            properties: {
                runId,
                status: manifest.status,
                schemaVersion: manifest.schemaVersion,
                createdAt: manifest.createdAt,
                completedAt: manifest.completedAt ?? null,
                promptHash: manifest.promptHash ?? null,
                decisionTracePath: manifest.decisionTracePath ?? null,
                evidenceRequired: manifest.evidenceSummary.required,
                evidencePassed: manifest.evidenceSummary.passed,
                evidenceFailed: manifest.evidenceSummary.failed,
                evidenceMissing: manifest.evidenceSummary.missing,
                nodeCount: manifest.nodes.length,
                artifactCount: manifest.artifacts.length,
                projectKey: this.settings.project.key,
                source: this.source,
            },
            createdAt: now,
            updatedAt: now,
        });
        this.upsertEdge(state, projectId, runNodeId, "HAS_RUN", now, tag);
        // Provider route -> provider.
        const provider = manifest.providerPolicy.provider;
        const mode = manifest.providerPolicy.mode ?? "auto";
        const routeNodeId = this.nodeId("ProviderRoute", `${runId}:${provider}:${mode}`);
        this.upsertNode(state, {
            id: routeNodeId,
            type: "ProviderRoute",
            labels: ["OmkProviderRoute", "ProviderRoute"],
            label: `${provider} (${mode})`,
            summary: `Provider route for run ${runId}`,
            tags: ["provider-route", provider, mode],
            properties: { runId, provider, mode, projectKey: this.settings.project.key, generatedFromRun: runId },
            createdAt: now,
            updatedAt: now,
        });
        this.upsertEdge(state, runNodeId, routeNodeId, "HAS_PROVIDER_ROUTE", now, tag);
        const providerNodeId = this.nodeId("Provider", provider);
        this.upsertNode(state, {
            id: providerNodeId,
            type: "Provider",
            labels: ["OmkProvider", "Provider"],
            label: provider,
            summary: `Provider ${provider}`,
            tags: ["provider", provider],
            properties: { provider, projectKey: this.settings.project.key },
            createdAt: now,
            updatedAt: now,
        });
        this.upsertEdge(state, routeNodeId, providerNodeId, "ROUTES_TO", now, tag);
        // Evidence summary node (counts/paths/sha only — never raw bodies).
        const evidenceArtifact = manifest.artifacts.find((artifact) => artifact.kind === "evidence" || /(^|\/)evidence\.jsonl$/i.test(artifact.path));
        const evidenceNodeId = this.nodeId("Evidence", `${runId}:evidence`);
        this.upsertNode(state, {
            id: evidenceNodeId,
            type: "Evidence",
            labels: ["OmkEvidence", "Evidence"],
            label: `evidence:${runId}`,
            summary: `Evidence ${manifest.evidenceSummary.passed}/${manifest.evidenceSummary.required} passed`,
            tags: ["evidence", manifest.status],
            properties: {
                runId,
                required: manifest.evidenceSummary.required,
                passed: manifest.evidenceSummary.passed,
                failed: manifest.evidenceSummary.failed,
                missing: manifest.evidenceSummary.missing,
                path: evidenceArtifact?.path ?? null,
                sha256: evidenceArtifact?.sha256 ?? null,
                projectKey: this.settings.project.key,
                generatedFromRun: runId,
            },
            createdAt: now,
            updatedAt: now,
        });
        this.upsertEdge(state, runNodeId, evidenceNodeId, "HAS_EVIDENCE", now, tag);
        // Decision trace node (path/sha only — never raw bodies).
        const decisionArtifact = manifest.artifacts.find((artifact) => artifact.kind === "decision" ||
            artifact.kind === "decisions" ||
            /(^|\/)decisions\.jsonl$/i.test(artifact.path));
        const decisionPath = manifest.decisionTracePath ?? decisionArtifact?.path ?? null;
        const decisionNodeId = this.nodeId("Decision", `${runId}:decision`);
        this.upsertNode(state, {
            id: decisionNodeId,
            type: "Decision",
            labels: ["OmkDecision", "Decision"],
            label: `decisions:${runId}`,
            summary: `Decision trace for run ${runId}`,
            tags: ["decision", manifest.status],
            properties: {
                runId,
                path: decisionPath,
                sha256: decisionArtifact?.sha256 ?? null,
                projectKey: this.settings.project.key,
                generatedFromRun: runId,
            },
            createdAt: now,
            updatedAt: now,
        });
        this.upsertEdge(state, runNodeId, decisionNodeId, "HAS_DECISION", now, tag);
        // Artifacts -> File/Artifact nodes (id/kind/path/sha only).
        for (const artifact of manifest.artifacts) {
            const artifactNodeId = this.nodeId("Artifact", `${runId}:${artifact.kind}:${artifact.path}`);
            this.upsertNode(state, {
                id: artifactNodeId,
                type: "Artifact",
                labels: ["OmkArtifact", "Artifact", "File"],
                label: artifact.path,
                path: artifact.path,
                summary: `${artifact.kind} artifact`,
                tags: ["artifact", artifact.kind, ...pathTags(artifact.path)],
                properties: {
                    runId,
                    kind: artifact.kind,
                    path: artifact.path,
                    sha256: artifact.sha256 ?? null,
                    projectKey: this.settings.project.key,
                    generatedFromRun: runId,
                },
                createdAt: now,
                updatedAt: now,
            });
            this.upsertEdge(state, runNodeId, artifactNodeId, "TOUCHES_FILE", now, tag);
        }
    }
    async graphQuery(query) {
        const normalized = query.replace(/\s+/g, " ").trim();
        const data = {};
        if (/\bontology\b/i.test(normalized)) {
            data.ontology = await this.ontology();
        }
        if (/\bmemory\s*\(/i.test(normalized)) {
            const path = readStringArg(normalized, "path");
            if (!path)
                throw new Error('omk_graph_query: memory(path: "...") requires a path argument');
            data.memory = {
                path,
                content: await this.read(path),
            };
        }
        if (/\bmemories\s*\(/i.test(normalized) || /\bmemories\b/i.test(normalized)) {
            const searchQuery = readStringArg(normalized, "query") ?? "";
            const limit = readNumberArg(normalized, "limit") ?? 10;
            data.memories = await this.search(searchQuery, limit);
        }
        if (/\bmindmap\s*\(/i.test(normalized) || /\bmindmap\b/i.test(normalized)) {
            const searchQuery = readStringArg(normalized, "query") ?? "";
            const limit = readNumberArg(normalized, "limit") ?? 80;
            data.mindmap = await this.mindmap(searchQuery, limit);
        }
        if (/\bnodes\s*\(/i.test(normalized) || /\bnodes\b/i.test(normalized)) {
            const state = await this.loadState();
            const type = readStringArg(normalized, "type");
            const searchQuery = (readStringArg(normalized, "query") ?? "").toLowerCase();
            const limit = readNumberArg(normalized, "limit") ?? 50;
            data.nodes = state.nodes
                .filter((node) => !type || node.type === type)
                .filter((node) => {
                if (!searchQuery)
                    return true;
                const content = node.type === "Memory" ? this.readFromState(state, node.path ?? node.label) : node.content;
                return [node.label, node.path, node.summary, content, ...node.tags]
                    .filter((value) => typeof value === "string")
                    .some((value) => value.toLowerCase().includes(searchQuery));
            })
                .slice(0, Math.max(1, Math.min(250, Math.floor(limit))))
                .map((node) => this.toMindmapFlatNode(node));
        }
        if (Object.keys(data).length === 0) {
            throw new Error('omk_graph_query supports GraphQL-lite fields: ontology, memory(path: "..."), memories(query: "...", limit: 10), mindmap(query: "...", limit: 80), nodes(type: "Task")');
        }
        return {
            data,
            extensions: {
                dialect: "omk-graphql-lite-v1",
                backend: "local_graph",
                statePath: this.settings.localGraph.path,
            },
        };
    }
    async loadState() {
        let raw;
        try {
            raw = await readFile(this.settings.localGraph.path, "utf-8");
        }
        catch (err) {
            if (errorCode(err) === "ENOENT")
                return this.emptyState();
            if (this.settings.strict)
                throw err;
            return this.emptyState();
        }
        if (raw.trim().length === 0) {
            return this.handleInvalidState(raw, "state file is empty");
        }
        try {
            const parsed = JSON.parse(raw);
            if (parsed.version === 1 && Array.isArray(parsed.nodes) && Array.isArray(parsed.edges)) {
                return {
                    version: 1,
                    ontology: ONTOLOGY,
                    project: parsed.project ?? { ...this.settings.project },
                    updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : new Date().toISOString(),
                    nodes: parsed.nodes,
                    edges: parsed.edges,
                };
            }
            return this.handleInvalidState(raw, "state JSON is missing version=1, nodes[], or edges[]");
        }
        catch (err) {
            return this.handleInvalidState(raw, `invalid JSON: ${errorMessage(err)}`);
        }
    }
    emptyState() {
        const now = new Date().toISOString();
        return {
            version: 1,
            ontology: ONTOLOGY,
            project: { ...this.settings.project },
            updatedAt: now,
            nodes: [],
            edges: [],
        };
    }
    async saveState(state) {
        const payload = `${JSON.stringify(state, null, 2)}\n`;
        await writeFileAtomic(this.settings.localGraph.path, payload);
        // Hardcoded fallback: always persist to the canonical path as the source of truth
        const fallbackPath = resolve(this.settings.project.root, ".omk/memory/graph-state.json");
        if (fallbackPath !== this.settings.localGraph.path) {
            await writeFileAtomic(fallbackPath, payload);
        }
    }
    async handleInvalidState(raw, reason) {
        const repair = await this.writeInvalidStateRepair(raw, reason).catch(() => undefined);
        if (this.settings.strict) {
            const repairText = repair
                ? ` Backup: ${repair.backupPath}; repair signal: ${repair.signalPath}.`
                : " Backup/repair signal could not be written.";
            throw new Error(`Local graph memory state is invalid; refusing to overwrite in strict mode.${repairText} Reason: ${reason}`);
        }
        return this.emptyState();
    }
    async writeInvalidStateRepair(raw, reason) {
        const statePath = this.settings.localGraph.path;
        const suffix = `${timestampForPath()}-${hash(raw).slice(0, 8)}`;
        const backupPath = `${statePath}.invalid-${suffix}.bak`;
        const signalPath = `${statePath}.repair.json`;
        const signal = {
            schemaVersion: 1,
            status: "repair-required",
            statePath,
            backupPath,
            reason,
            strict: this.settings.strict,
            createdAt: new Date().toISOString(),
        };
        await mkdir(dirname(statePath), { recursive: true });
        await writeFile(backupPath, raw, "utf-8");
        await writeFileAtomic(signalPath, `${JSON.stringify(signal, null, 2)}\n`);
        return { backupPath, signalPath, reason };
    }
    async mutateState(mutator) {
        await enqueueGraphWrite(this.settings.localGraph.path, async () => {
            const state = await this.loadState();
            mutator(state, new Date().toISOString());
            await this.saveState(state);
            if (this.settings.mirrorFiles) {
                await this.writeMirrorFiles(state);
            }
        });
    }
    readFromState(state, path) {
        const memory = this.findMemoryNode(state, path);
        const version = this.findLatestMemoryVersionNode(state, path, memory?.id);
        return version?.content ?? memory?.content ?? "";
    }
    findLatestMemoryVersionNode(state, path, memoryId) {
        const updateIds = memoryId
            ? new Set(state.edges.filter((edge) => edge.type === "UPDATES" && edge.to === memoryId).map((edge) => edge.from))
            : undefined;
        return state.nodes
            .filter((node) => node.type === "MemoryVersion" && node.path === path)
            .filter((node) => !updateIds || updateIds.size === 0 || updateIds.has(node.id))
            .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt) ||
            b.createdAt.localeCompare(a.createdAt) ||
            b.id.localeCompare(a.id))[0];
    }
    findMemoryNode(state, path) {
        const memoryId = this.memoryNodeId(path);
        return state.nodes.find((node) => node.id === memoryId && node.type === "Memory");
    }
    replaceGeneratedMindmap(state, memoryId, content, path, now) {
        const generatedIds = new Set(state.nodes
            .filter((node) => GENERATED_TYPES.has(node.type) && node.properties.generatedFrom === memoryId)
            .map((node) => node.id));
        state.nodes = state.nodes.filter((node) => !generatedIds.has(node.id));
        state.edges = state.edges.filter((edge) => !generatedIds.has(edge.from) && !generatedIds.has(edge.to) && edge.properties.generatedFrom !== memoryId);
        let concepts = extractConcepts(content);
        const MAX_CONCEPTS = 50;
        for (const concept of concepts) {
            if (!CANONICAL_NODE_TYPES.has(concept.type)) {
                concept.type = "Topic";
                concept.relation = relationForType("Topic");
            }
        }
        if (concepts.length > MAX_CONCEPTS) {
            const overflowCount = concepts.length - MAX_CONCEPTS;
            concepts = concepts.slice(0, MAX_CONCEPTS);
            concepts.push({
                type: "Topic",
                label: `And ${overflowCount} more concepts…`,
                summary: `${overflowCount} additional concepts were extracted but not stored to limit graph growth.`,
                relation: "HAS_TOPIC",
                tags: ["overflow", "limit"],
                line: 0,
                filePaths: [],
            });
        }
        const conceptIds = concepts.map((concept, index) => this.nodeId(concept.type, `${path}:${index}:${concept.label}`));
        const memoryFileNodes = new Map();
        for (const [index, concept] of concepts.entries()) {
            const conceptId = conceptIds[index];
            this.upsertNode(state, {
                id: conceptId,
                type: concept.type,
                labels: [`Omk${concept.type}`, concept.type],
                label: concept.label,
                summary: concept.summary,
                tags: concept.tags,
                properties: {
                    generatedFrom: memoryId,
                    line: concept.line,
                    level: concept.level ?? null,
                    path,
                    projectKey: this.settings.project.key,
                    sessionId: this.settings.session.id,
                },
                createdAt: now,
                updatedAt: now,
            });
            const ownerId = concept.parentIndex === undefined ? memoryId : conceptIds[concept.parentIndex] ?? memoryId;
            const relation = ownerId === memoryId ? concept.relation : nestedRelationForType(concept.type, concept.relation);
            this.upsertEdge(state, ownerId, conceptId, relation, now, { generatedFrom: memoryId });
            this.upsertEdge(state, conceptId, memoryId, "PART_OF", now, { generatedFrom: memoryId });
            for (const filePath of concept.filePaths) {
                const existingFileId = memoryFileNodes.get(filePath);
                const fileId = existingFileId ?? this.nodeId("File", `${path}:${filePath}`);
                memoryFileNodes.set(filePath, fileId);
                this.upsertNode(state, {
                    id: fileId,
                    type: "File",
                    labels: ["OmkFile", "File"],
                    label: filePath,
                    path: filePath,
                    summary: `Referenced by ${path}`,
                    tags: ["file", ...pathTags(filePath)],
                    properties: {
                        generatedFrom: memoryId,
                        path: filePath,
                        projectKey: this.settings.project.key,
                        sessionId: this.settings.session.id,
                    },
                    createdAt: now,
                    updatedAt: now,
                });
                this.upsertEdge(state, conceptId, fileId, "TOUCHES_FILE", now, { generatedFrom: memoryId });
                this.upsertEdge(state, memoryId, fileId, "HAS_FILE", now, { generatedFrom: memoryId });
            }
        }
    }
    expandNeighborhood(state, seedIds, limit) {
        const included = new Set();
        const queue = [...seedIds];
        while (queue.length > 0 && included.size < limit) {
            const id = queue.shift();
            if (!id || included.has(id))
                continue;
            included.add(id);
            for (const edge of state.edges) {
                if (edge.from === id && !included.has(edge.to))
                    queue.push(edge.to);
                if (edge.to === id && !included.has(edge.from))
                    queue.push(edge.from);
            }
        }
        return included;
    }
    buildMindmapTree(node, state, includedIds, seen) {
        const flat = this.toMindmapFlatNode(node);
        const tree = { ...flat, children: [] };
        if (seen.has(node.id))
            return tree;
        seen.add(node.id);
        const childEdges = state.edges.filter((edge) => includedIds.has(edge.to) && this.isMindmapForward(edge));
        const children = childEdges
            .filter((edge) => edge.from === node.id)
            .map((edge) => state.nodes.find((candidate) => candidate.id === edge.to))
            .filter((candidate) => Boolean(candidate))
            .sort((a, b) => sortRank(a.type) - sortRank(b.type) || a.label.localeCompare(b.label));
        for (const child of children) {
            tree.children.push(this.buildMindmapTree(child, state, includedIds, seen));
        }
        return tree;
    }
    isMindmapForward(edge) {
        return edge.type.startsWith("HAS_") || edge.type === "UPDATES" || edge.type === "WROTE" || edge.type === "TOUCHES_FILE" || edge.type === "USES_PROVIDER";
    }
    toMindmapFlatNode(node) {
        return {
            id: node.id,
            type: node.type,
            label: node.label,
            path: node.path,
            summary: node.summary,
        };
    }
    defaultRootNode(id) {
        const now = new Date().toISOString();
        return {
            id,
            type: "Project",
            labels: ["OmkProject", "Project"],
            label: this.settings.project.name,
            summary: this.settings.project.root,
            tags: ["project"],
            properties: { key: this.settings.project.key, root: this.settings.project.root },
            createdAt: now,
            updatedAt: now,
        };
    }
    upsertNode(state, node) {
        const existing = state.nodes.findIndex((candidate) => candidate.id === node.id);
        if (existing === -1) {
            state.nodes.push(node);
            return;
        }
        state.nodes[existing] = {
            ...state.nodes[existing],
            ...node,
            createdAt: state.nodes[existing].createdAt,
        };
    }
    upsertEdge(state, from, to, type, now, properties = {}) {
        const id = this.edgeId(type, from, to);
        const existing = state.edges.findIndex((candidate) => candidate.id === id);
        const edge = {
            id,
            type,
            from,
            to,
            label: type.toLowerCase().replace(/_/g, " "),
            properties,
            createdAt: now,
            updatedAt: now,
        };
        if (existing === -1) {
            state.edges.push(edge);
            return;
        }
        state.edges[existing] = {
            ...state.edges[existing],
            ...edge,
            createdAt: state.edges[existing].createdAt,
        };
    }
    memoryNodeId(path) {
        return this.nodeId("Memory", this.memoryKey(path));
    }
    memoryKey(path) {
        return `${this.settings.project.key}:${hash(path)}`;
    }
    nodeId(type, key) {
        return `${type}:${hash(`${this.settings.project.key}:${key}`).slice(0, 24)}`;
    }
    edgeId(type, from, to) {
        return `${type}:${hash(`${from}\n${type}\n${to}`).slice(0, 24)}`;
    }
}
export function extractConcepts(content) {
    const concepts = [];
    const topicStack = [];
    const lines = content.split(/\r?\n/);
    let inFence = false;
    for (let index = 0; index < lines.length; index += 1) {
        const raw = lines[index];
        const trimmed = raw.trim();
        if (!trimmed)
            continue;
        if (trimmed.startsWith("```") || trimmed.startsWith("~~~")) {
            inFence = !inFence;
            continue;
        }
        const heading = trimmed.match(/^(#{1,6})\s+(.+)$/);
        if (heading) {
            const level = heading[1].length;
            const label = cleanLabel(heading[2]);
            while (topicStack.length > 0 && topicStack[topicStack.length - 1].level >= level)
                topicStack.pop();
            const parentIndex = topicStack[topicStack.length - 1]?.index;
            const conceptIndex = concepts.length;
            topicStack.push({ level, index: conceptIndex });
            concepts.push({
                type: classify(label, false),
                label,
                summary: label,
                parentIndex,
                relation: relationForType(classify(label, false)),
                tags: tagsFor(label),
                line: index + 1,
                level,
                filePaths: extractFilePaths(label),
            });
            continue;
        }
        const bullet = trimmed.match(/^[-*+]\s+(.+)$/) ?? trimmed.match(/^\d+[.)]\s+(.+)$/);
        const text = cleanLabel(bullet?.[1] ?? trimmed.replace(/^>\s*/, ""));
        const commandLike = inFence || isCommandLike(text);
        if (text.length < 3)
            continue;
        const parentIndex = topicStack[topicStack.length - 1]?.index;
        const type = commandLike ? "Command" : classify(text, Boolean(bullet));
        concepts.push({
            type,
            label: truncate(text, 140),
            summary: text,
            parentIndex,
            relation: relationForType(type),
            tags: tagsFor(text),
            line: index + 1,
            filePaths: extractFilePaths(text),
        });
    }
    return concepts;
}
function classify(text, isBullet) {
    const lower = text.toLowerCase();
    if (/\b(audit link|audit trail|report link|evidence link|검증 링크|감사 링크)\b/i.test(text))
        return "AuditLink";
    if (/\[[^\]]+\]\((?:\.omk\/runs\/|\.omk\/goals\/|[^)]+(?:report|summary|evidence)[^)]*\.md)[^)]*\)/i.test(text))
        return "AuditLink";
    if (/\b(run\s*id|runid|run)\b/i.test(text) && /\b[\w.-]*run[\w.-]*\b|\.omk\/runs\//i.test(text))
        return "Run";
    if (/\b(goal|objective|목표|비전|north star)\b/i.test(text))
        return "Goal";
    if (/\b(decision|decide|chosen|결정|선택|합의)\b/i.test(text))
        return "Decision";
    if (/\b(todo|task|해야|할 일|next|action|implement|구현|작업)\b/i.test(text))
        return "Task";
    if (/\b(risk|blocked|blocker|fail|failure|warning|주의|리스크|위험|실패|차단)\b/i.test(text))
        return "Risk";
    if (/\b(evidence|verified|tested|pass|출처|근거|검증|통과)\b/i.test(text))
        return "Evidence";
    if (/\b(fallback|failover|fallback-to-kimi|대체|폴백)\b/i.test(text) && /\b(provider|kimi|deepseek|라우팅|공급자)\b/i.test(text))
        return "ProviderFallback";
    if (/\b(provider route|provider routing|routeProvider|provider router|deepseek|compatibility-first|라우팅|공급자)\b/i.test(text))
        return "ProviderRoute";
    if (/\b(provider|kimi|deepseek|runtime|worker pool|공급자|런타임)\b/i.test(text))
        return "Provider";
    if (/\b(constraint|must|must not|required|제약|필수|금지)\b/i.test(text))
        return "Constraint";
    if (text.endsWith("?") || /\b(question|질문|의문)\b/i.test(text))
        return "Question";
    if (/\b(answer|resolution|해결|답변)\b/i.test(text))
        return "Answer";
    if (isBullet && lower.length < 100)
        return "Concept";
    return "Topic";
}
function relationForType(type) {
    switch (type) {
        case "Goal":
            return "HAS_GOAL";
        case "Run":
            return "HAS_RUN";
        case "Topic":
            return "HAS_TOPIC";
        case "Decision":
            return "HAS_DECISION";
        case "Task":
            return "HAS_TASK";
        case "Risk":
            return "HAS_RISK";
        case "Command":
            return "HAS_COMMAND";
        case "Evidence":
            return "HAS_EVIDENCE";
        case "Provider":
            return "USES_PROVIDER";
        case "ProviderRoute":
            return "HAS_PROVIDER_ROUTE";
        case "ProviderFallback":
            return "HAS_PROVIDER_FALLBACK";
        case "AuditLink":
            return "HAS_AUDIT_LINK";
        case "Constraint":
            return "HAS_CONSTRAINT";
        case "Question":
            return "HAS_QUESTION";
        case "Answer":
            return "HAS_ANSWER";
        case "File":
            return "HAS_FILE";
        default:
            return "HAS_CONCEPT";
    }
}
function nestedRelationForType(type, relation) {
    if (["Run", "AuditLink", "ProviderRoute", "ProviderFallback", "Evidence"].includes(type)) {
        return relation;
    }
    return "HAS_CONCEPT";
}
function isCommandLike(text) {
    return /^(\$\s*)?(npm|pnpm|yarn|node|tsx|tsc|git|omk|kimi|python|python3|uv|pytest|ruff|docker|docker-compose|kubectl)\b/.test(text);
}
function extractFilePaths(text) {
    const markdownLinks = [...text.matchAll(/\]\(([^)]+)\)/g)]
        .map((match) => match[1].trim())
        .filter((target) => target && !/^[a-z][a-z0-9+.-]*:/i.test(target));
    const matches = text.match(/(?:\.?[A-Za-z0-9_.-]+\/)+(?:[A-Za-z0-9_.-]+)|(?:^|\s)(?:[A-Za-z0-9_.-]+\.(?:ts|tsx|js|mjs|cjs|json|md|toml|yaml|yml|py|sh|mjs))/g) ?? [];
    return [...new Set([...markdownLinks, ...matches.map((match) => match.trim())].filter(Boolean))].slice(0, 10);
}
function tagsFor(text) {
    const tags = new Set();
    for (const part of text.toLowerCase().match(/[a-z0-9가-힣_-]{3,}/g) ?? []) {
        if (tags.size >= 12)
            break;
        tags.add(part);
    }
    return [...tags];
}
function pathTags(path) {
    return path
        .split(/[\\/.:-]+/)
        .map((part) => part.trim().toLowerCase())
        .filter((part) => part.length >= 2)
        .slice(0, 8);
}
function cleanLabel(text) {
    return text.replace(/[`*_~]/g, "").replace(/\s+/g, " ").trim();
}
function summarize(content) {
    const firstUseful = content
        .split(/\r?\n/)
        .map((line) => cleanLabel(line.replace(/^#{1,6}\s+/, "").replace(/^[-*+]\s+/, "")))
        .find((line) => line.length > 0);
    return truncate(firstUseful ?? "", 180);
}
function truncate(value, max) {
    return value.length <= max ? value : `${value.slice(0, Math.max(0, max - 1))}…`;
}
function sortRank(type) {
    const order = ["Project", "Session", "Memory", "Goal", "Run", "Topic", "Decision", "Task", "Risk", "Evidence", "Provider", "ProviderRoute", "ProviderFallback", "AuditLink", "Constraint", "Command", "File", "Concept"];
    const index = order.indexOf(type);
    return index === -1 ? order.length : index;
}
function readStringArg(query, name) {
    const match = query.match(new RegExp(`${name}\\s*:\\s*["']([^"']*)["']`, "i"));
    return match?.[1];
}
function readNumberArg(query, name) {
    const match = query.match(new RegExp(`${name}\\s*:\\s*(\\d+)`, "i"));
    return match ? Number.parseInt(match[1], 10) : undefined;
}
function hash(value) {
    return createHash("sha256").update(value).digest("hex");
}
