"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.KnowledgeGraphManager = exports.createKnowledgeGraph = void 0;
exports.proxyId = proxyId;
exports.isProxy = isProxy;
exports.cleanupProxies = cleanupProxies;
exports.createNote = createNote;
exports.updateNote = updateNote;
exports.deleteNote = deleteNote;
exports.getNote = getNote;
exports.listNotes = listNotes;
exports.createRelation = createRelation;
exports.deleteRelation = deleteRelation;
exports.listRelations = listRelations;
exports.findLinkedNotes = findLinkedNotes;
exports.createCrossRelation = createCrossRelation;
exports.deleteCrossRelation = deleteCrossRelation;
exports.saveKnowledgeGraph = saveKnowledgeGraph;
exports.loadKnowledgeGraph = loadKnowledgeGraph;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const knowledge_types_1 = require("../graphs/knowledge-types");
Object.defineProperty(exports, "createKnowledgeGraph", { enumerable: true, get: function () { return knowledge_types_1.createKnowledgeGraph; } });
const manager_types_1 = require("../graphs/manager-types");
const knowledge_1 = require("../lib/search/knowledge");
const bm25_1 = require("../lib/search/bm25");
const file_mirror_1 = require("../lib/file-mirror");
const embedding_codec_1 = require("../lib/embedding-codec");
const graph_persistence_1 = require("../lib/graph-persistence");
const defaults_1 = require("../lib/defaults");
const logger_1 = require("../lib/logger");
const log = (0, logger_1.createLogger)('knowledge-graph');
const attachment_types_1 = require("../graphs/attachment-types");
const file_import_1 = require("../lib/file-import");
// ---------------------------------------------------------------------------
// Proxy helpers
// ---------------------------------------------------------------------------
/** Build the proxy node ID. With projectId: `@docs::frontend::guide.md::Setup`, without: `@docs::guide.md::Setup` */
function proxyId(targetGraph, nodeId, projectId) {
    return projectId ? `@${targetGraph}::${projectId}::${nodeId}` : `@${targetGraph}::${nodeId}`;
}
/** Check whether a node is a cross-graph proxy. */
function isProxy(graph, nodeId) {
    if (!graph.hasNode(nodeId))
        return false;
    return graph.getNodeAttribute(nodeId, 'proxyFor') !== undefined;
}
/** Ensure a proxy node exists for the given external target. Returns its ID. */
function ensureProxyNode(graph, targetGraph, nodeId, projectId) {
    const id = proxyId(targetGraph, nodeId, projectId);
    if (!graph.hasNode(id)) {
        graph.addNode(id, {
            title: '',
            content: '',
            tags: [],
            embedding: [],
            attachments: [],
            createdAt: 0,
            updatedAt: 0,
            version: 0,
            proxyFor: { graph: targetGraph, nodeId, projectId },
        });
    }
    return id;
}
/** Remove a proxy node if it has zero incident edges. */
function cleanupProxy(graph, nodeId) {
    if (!graph.hasNode(nodeId))
        return;
    if (!isProxy(graph, nodeId))
        return;
    if (graph.degree(nodeId) === 0) {
        graph.dropNode(nodeId);
    }
}
/**
 * Remove all proxy nodes whose target no longer exists in the external graph.
 * Called after doc/code file removal in the indexer.
 */
function cleanupProxies(graph, targetGraph, externalGraph, projectId) {
    const toRemove = [];
    graph.forEachNode((id, attrs) => {
        if (attrs.proxyFor && attrs.proxyFor.graph === targetGraph) {
            // In workspace mode, only clean proxies belonging to this project
            if (projectId && attrs.proxyFor.projectId && attrs.proxyFor.projectId !== projectId)
                return;
            if (!externalGraph.hasNode(attrs.proxyFor.nodeId)) {
                toRemove.push(id);
            }
        }
    });
    for (const id of toRemove) {
        graph.dropNode(id); // also drops incident edges
    }
}
// ---------------------------------------------------------------------------
// CRUD — Notes
// ---------------------------------------------------------------------------
/** Create a note, return its slug ID. */
function createNote(graph, title, content, tags, embedding, author = '') {
    const id = (0, knowledge_types_1.generateId)();
    const now = Date.now();
    graph.addNode(id, {
        title,
        content,
        tags,
        embedding,
        attachments: [],
        createdAt: now,
        updatedAt: now,
        version: 1,
        createdBy: author || undefined,
        updatedBy: author || undefined,
    });
    return id;
}
/** Partial update of a note. Returns true if found and updated. Throws VersionConflictError if expectedVersion is provided and doesn't match. */
function updateNote(graph, noteId, patch, embedding, author = '', expectedVersion) {
    if (!graph.hasNode(noteId))
        return false;
    if (expectedVersion !== undefined) {
        const current = graph.getNodeAttribute(noteId, 'version');
        if (current !== expectedVersion)
            throw new manager_types_1.VersionConflictError(current, expectedVersion);
    }
    if (patch.title !== undefined)
        graph.setNodeAttribute(noteId, 'title', patch.title);
    if (patch.content !== undefined)
        graph.setNodeAttribute(noteId, 'content', patch.content);
    if (patch.tags !== undefined)
        graph.setNodeAttribute(noteId, 'tags', patch.tags);
    if (embedding !== undefined)
        graph.setNodeAttribute(noteId, 'embedding', embedding);
    if (author)
        graph.setNodeAttribute(noteId, 'updatedBy', author);
    graph.setNodeAttribute(noteId, 'version', graph.getNodeAttribute(noteId, 'version') + 1);
    graph.setNodeAttribute(noteId, 'updatedAt', Date.now());
    return true;
}
/** Delete a note and all its incident edges. Also cleans up orphaned proxy nodes. */
function deleteNote(graph, noteId) {
    if (!graph.hasNode(noteId))
        return false;
    // Collect proxy neighbors before dropping the note
    const proxyNeighbors = [];
    graph.forEachNeighbor(noteId, (neighbor) => {
        if (isProxy(graph, neighbor))
            proxyNeighbors.push(neighbor);
    });
    graph.dropNode(noteId);
    // Cleanup orphaned proxies (they lost an edge when the note was dropped)
    for (const p of proxyNeighbors) {
        cleanupProxy(graph, p);
    }
    return true;
}
/** Get a note by ID, or null if not found. Excludes proxy nodes. */
function getNote(graph, noteId) {
    if (!graph.hasNode(noteId))
        return null;
    if (isProxy(graph, noteId))
        return null;
    return { id: noteId, ...graph.getNodeAttributes(noteId) };
}
/** List notes with optional filter (substring in title/id) and tag filter. Excludes proxy nodes. */
function listNotes(graph, filter, tag, limit = defaults_1.LIST_PAGE_SIZE, offset = 0) {
    const lowerFilter = filter?.toLowerCase();
    const lowerTag = tag?.toLowerCase();
    const results = [];
    graph.forEachNode((id, attrs) => {
        if (attrs.proxyFor)
            return; // skip proxy nodes
        if (lowerFilter) {
            const match = id.toLowerCase().includes(lowerFilter) ||
                attrs.title.toLowerCase().includes(lowerFilter);
            if (!match)
                return;
        }
        if (lowerTag) {
            if (!attrs.tags.some(t => t.toLowerCase() === lowerTag))
                return;
        }
        results.push({ id, title: attrs.title, content: attrs.content.slice(0, defaults_1.CONTENT_PREVIEW_LEN), tags: attrs.tags, updatedAt: attrs.updatedAt });
    });
    const sorted = results.sort((a, b) => b.updatedAt - a.updatedAt);
    return { results: sorted.slice(offset, offset + limit), total: sorted.length };
}
// ---------------------------------------------------------------------------
// CRUD — Relations (note ↔ note)
// ---------------------------------------------------------------------------
/** Create a directed relation between two notes. Returns true if created. */
function createRelation(graph, fromId, toId, kind) {
    if (!graph.hasNode(fromId) || !graph.hasNode(toId))
        return false;
    if (graph.hasEdge(fromId, toId))
        return false;
    graph.addEdgeWithKey(`${fromId}→${toId}`, fromId, toId, { kind });
    return true;
}
/** Delete a relation. Cleans up orphaned proxy nodes. Returns true if it existed. */
function deleteRelation(graph, fromId, toId) {
    if (!graph.hasEdge(fromId, toId))
        return false;
    graph.dropEdge(fromId, toId);
    // Clean up proxy if it became orphaned
    cleanupProxy(graph, fromId);
    cleanupProxy(graph, toId);
    return true;
}
/** List all relations for a note (both incoming and outgoing). Resolves proxy IDs and titles. */
function listRelations(graph, noteId, externalGraphs) {
    if (!graph.hasNode(noteId))
        return [];
    const results = [];
    function resolveTitle(nodeId, targetGraph) {
        if (!targetGraph) {
            // Same-graph note
            if (graph.hasNode(nodeId) && !isProxy(graph, nodeId)) {
                return graph.getNodeAttribute(nodeId, 'title') || undefined;
            }
            return undefined;
        }
        if (!externalGraphs)
            return undefined;
        const extGraph = (0, manager_types_1.resolveExternalGraph)(externalGraphs, targetGraph);
        if (!extGraph || !extGraph.hasNode(nodeId))
            return undefined;
        const attrs = extGraph.getNodeAttributes(nodeId);
        return attrs.title || attrs.name || undefined;
    }
    function resolveEntry(source, target, kind) {
        // Check if either end is a proxy node and resolve it
        const sourceProxy = graph.hasNode(source) ? graph.getNodeAttribute(source, 'proxyFor') : undefined;
        const targetProxy = graph.hasNode(target) ? graph.getNodeAttribute(target, 'proxyFor') : undefined;
        if (targetProxy) {
            const title = resolveTitle(targetProxy.nodeId, targetProxy.graph);
            return { fromId: source, toId: targetProxy.nodeId, kind, targetGraph: targetProxy.graph, ...(title ? { title } : {}) };
        }
        if (sourceProxy) {
            const title = resolveTitle(sourceProxy.nodeId, sourceProxy.graph);
            return { fromId: sourceProxy.nodeId, toId: target, kind, targetGraph: sourceProxy.graph, ...(title ? { title } : {}) };
        }
        // Same-graph note↔note: resolve the "other" side's title
        const otherId = source === noteId ? target : source;
        const title = resolveTitle(otherId);
        return { fromId: source, toId: target, kind, ...(title ? { title } : {}) };
    }
    graph.forEachOutEdge(noteId, (_edge, attrs, source, target) => {
        results.push(resolveEntry(source, target, attrs.kind));
    });
    graph.forEachInEdge(noteId, (_edge, attrs, source, target) => {
        results.push(resolveEntry(source, target, attrs.kind));
    });
    return results;
}
/**
 * Find all notes that have a cross-graph relation to the given target node.
 * Optionally filter by relation kind.
 */
function findLinkedNotes(graph, targetGraph, targetNodeId, kind, projectId) {
    // Check both project-scoped and legacy proxy IDs
    const candidates = [proxyId(targetGraph, targetNodeId, projectId)];
    if (projectId)
        candidates.push(proxyId(targetGraph, targetNodeId));
    const results = [];
    const seen = new Set();
    for (const pId of candidates) {
        if (!graph.hasNode(pId))
            continue;
        graph.forEachInEdge(pId, (_edge, attrs, source) => {
            if (seen.has(source))
                return;
            if (isProxy(graph, source))
                return;
            if (kind && attrs.kind !== kind)
                return;
            const noteAttrs = graph.getNodeAttributes(source);
            seen.add(source);
            results.push({
                noteId: source,
                title: noteAttrs.title,
                kind: attrs.kind,
                tags: noteAttrs.tags,
            });
        });
    }
    return results;
}
// ---------------------------------------------------------------------------
// Cross-graph relations (note → doc/code node)
// ---------------------------------------------------------------------------
/**
 * Create a cross-graph relation from a note to a node in the doc or code graph.
 * Optionally validates that the target exists in the external graph.
 */
function createCrossRelation(graph, fromNoteId, targetGraph, targetNodeId, kind, externalGraph, projectId) {
    // Source must be a real note (not a proxy)
    if (!graph.hasNode(fromNoteId) || isProxy(graph, fromNoteId))
        return false;
    // Validate target exists in external graph if provided
    if (externalGraph && !externalGraph.hasNode(targetNodeId))
        return false;
    const pId = ensureProxyNode(graph, targetGraph, targetNodeId, projectId);
    if (graph.hasEdge(fromNoteId, pId))
        return false;
    graph.addEdgeWithKey(`${fromNoteId}→${pId}`, fromNoteId, pId, { kind });
    return true;
}
/**
 * Delete a cross-graph relation. Cleans up orphaned proxy node.
 */
function deleteCrossRelation(graph, fromNoteId, targetGraph, targetNodeId, projectId) {
    // Try project-scoped first, then legacy
    const candidates = [proxyId(targetGraph, targetNodeId, projectId)];
    if (projectId)
        candidates.push(proxyId(targetGraph, targetNodeId));
    for (const pId of candidates) {
        // Outgoing: noteId → proxy(targetId) — note created this link
        if (graph.hasEdge(fromNoteId, pId)) {
            graph.dropEdge(fromNoteId, pId);
            cleanupProxy(graph, pId);
            return true;
        }
        // Incoming: proxy(targetId) → noteId — mirror from another graph
        if (graph.hasEdge(pId, fromNoteId)) {
            graph.dropEdge(pId, fromNoteId);
            cleanupProxy(graph, pId);
            return true;
        }
    }
    // Also check reverse proxy direction: proxy(fromId) → targetId
    // Handles the case when resolveEntry swaps fromId/toId for incoming mirrors
    const reverseCandidates = [proxyId(targetGraph, fromNoteId, projectId)];
    if (projectId)
        reverseCandidates.push(proxyId(targetGraph, fromNoteId));
    for (const pId of reverseCandidates) {
        if (graph.hasEdge(pId, targetNodeId)) {
            graph.dropEdge(pId, targetNodeId);
            cleanupProxy(graph, pId);
            return true;
        }
        if (graph.hasEdge(targetNodeId, pId)) {
            graph.dropEdge(targetNodeId, pId);
            cleanupProxy(graph, pId);
            return true;
        }
    }
    return false;
}
// ---------------------------------------------------------------------------
// Persistence
// ---------------------------------------------------------------------------
function saveKnowledgeGraph(graph, graphMemory, embeddingFingerprint) {
    fs_1.default.mkdirSync(graphMemory, { recursive: true });
    const file = path_1.default.join(graphMemory, 'knowledge.json');
    const tmp = file + '.tmp';
    try {
        const exported = graph.export();
        (0, embedding_codec_1.compressEmbeddings)(exported);
        fs_1.default.writeFileSync(tmp, JSON.stringify({ version: defaults_1.GRAPH_DATA_VERSION, embeddingModel: embeddingFingerprint, graph: exported }));
        fs_1.default.renameSync(tmp, file);
    }
    catch (err) {
        try {
            fs_1.default.unlinkSync(tmp);
        }
        catch { /* ignore cleanup error */ }
        throw err;
    }
}
function loadKnowledgeGraph(graphMemory, fresh = false, embeddingFingerprint) {
    const graph = (0, knowledge_types_1.createKnowledgeGraph)();
    if (fresh)
        return graph;
    const file = path_1.default.join(graphMemory, 'knowledge.json');
    const data = (0, graph_persistence_1.readJsonWithTmpFallback)(file);
    if (!data)
        return graph;
    try {
        const storedVersion = data.version;
        const stored = data.embeddingModel;
        const needsReEmbed = (storedVersion !== defaults_1.GRAPH_DATA_VERSION) ||
            (embeddingFingerprint != null && stored !== embeddingFingerprint);
        if (needsReEmbed && storedVersion !== defaults_1.GRAPH_DATA_VERSION) {
            log.warn({ storedVersion: storedVersion ?? 'none', currentVersion: defaults_1.GRAPH_DATA_VERSION }, 'Data version changed, preserving user data, clearing embeddings');
        }
        else if (needsReEmbed) {
            log.warn('Embedding config changed, preserving user data, clearing embeddings');
        }
        if (!(0, graph_persistence_1.validateGraphStructure)(data.graph)) {
            log.warn({ file }, 'Invalid graph structure, starting fresh');
            return graph;
        }
        (0, embedding_codec_1.decompressEmbeddings)(data.graph);
        graph.import(data.graph);
        if (needsReEmbed) {
            graph.forEachNode((id, attrs) => {
                if (!attrs.proxyFor)
                    graph.setNodeAttribute(id, 'embedding', []);
            });
            log.info({ nodes: graph.order }, 'Loaded graph (embeddings cleared for re-generation)');
        }
        else {
            log.info({ nodes: graph.order, edges: graph.size }, 'Loaded graph');
        }
    }
    catch (err) {
        log.error({ err }, 'Failed to load graph, starting fresh');
    }
    return graph;
}
// ---------------------------------------------------------------------------
// Bidirectional mirror helpers (Knowledge ↔ Task)
// ---------------------------------------------------------------------------
/**
 * Create a mirror proxy in TaskGraph when a note links to a task.
 * Creates `@knowledge::noteId` proxy node + edge proxy→taskId in TaskGraph.
 */
function createMirrorInTaskGraph(taskGraph, noteId, taskId, kind, projectId) {
    const mirrorProxyId = projectId ? `@knowledge::${projectId}::${noteId}` : `@knowledge::${noteId}`;
    if (!taskGraph.hasNode(mirrorProxyId)) {
        taskGraph.addNode(mirrorProxyId, {
            title: '',
            description: '',
            status: 'backlog',
            priority: 'low',
            tags: [],
            dueDate: null,
            estimate: null,
            completedAt: null,
            assignee: null,
            embedding: [],
            attachments: [],
            createdAt: 0,
            updatedAt: 0,
            version: 0,
            proxyFor: { graph: 'knowledge', nodeId: noteId, ...(projectId ? { projectId } : {}) },
        });
    }
    if (!taskGraph.hasNode(taskId))
        return;
    const edgeKey = `${mirrorProxyId}→${taskId}`;
    if (!taskGraph.hasEdge(edgeKey)) {
        taskGraph.addEdgeWithKey(edgeKey, mirrorProxyId, taskId, { kind });
    }
}
/**
 * Remove the mirror proxy edge/node from TaskGraph when a note→task relation is deleted.
 */
function deleteMirrorFromTaskGraph(taskGraph, noteId, taskId, projectId) {
    const candidates = projectId
        ? [`@knowledge::${projectId}::${noteId}`, `@knowledge::${noteId}`]
        : [`@knowledge::${noteId}`];
    for (const mirrorProxyId of candidates) {
        const edgeKey = `${mirrorProxyId}→${taskId}`;
        if (taskGraph.hasEdge(edgeKey)) {
            taskGraph.dropEdge(edgeKey);
        }
        // Cleanup orphan proxy
        if (taskGraph.hasNode(mirrorProxyId)) {
            const proxyFor = taskGraph.getNodeAttribute(mirrorProxyId, 'proxyFor');
            if (proxyFor && taskGraph.degree(mirrorProxyId) === 0) {
                taskGraph.dropNode(mirrorProxyId);
            }
        }
    }
}
// ---------------------------------------------------------------------------
// KnowledgeGraphManager — unified API for knowledge graph operations
// ---------------------------------------------------------------------------
class KnowledgeGraphManager {
    _graph;
    embedFns;
    ctx;
    ext;
    taskGraph;
    mirrorTracker;
    _bm25Index;
    get externalGraphs() { return this.ext; }
    get projectDir() { return this.ctx.projectDir; }
    constructor(_graph, embedFns, ctx, ext = {}) {
        this._graph = _graph;
        this.embedFns = embedFns;
        this.ctx = ctx;
        this.ext = ext;
        this.taskGraph = ext.taskGraph;
        this._bm25Index = new bm25_1.BM25Index((attrs) => `${attrs.title} ${attrs.content} ${attrs.tags.join(' ')}`);
        this._graph.forEachNode((id, attrs) => {
            if (!attrs.proxyFor)
                this._bm25Index.addDocument(id, attrs);
        });
    }
    get graph() { return this._graph; }
    get bm25Index() { return this._bm25Index; }
    rebuildBm25Index() {
        this._bm25Index.clear();
        this._graph.forEachNode((id, attrs) => {
            if (!attrs.proxyFor)
                this._bm25Index.addDocument(id, attrs);
        });
    }
    setMirrorTracker(tracker) {
        this.mirrorTracker = tracker;
    }
    /** Returns updatedAt for a node, or null if not found. Used by startup scan. */
    getNodeUpdatedAt(noteId) {
        if (!this._graph.hasNode(noteId))
            return null;
        if (isProxy(this._graph, noteId))
            return null;
        return this._graph.getNodeAttribute(noteId, 'updatedAt') ?? null;
    }
    get notesDir() {
        const base = this.ctx.mirrorDir ?? this.ctx.projectDir;
        return base ? path_1.default.join(base, '.notes') : undefined;
    }
    recordMirrorWrites(noteId) {
        const dir = this.notesDir;
        if (!dir || !this.mirrorTracker)
            return;
        const entityDir = path_1.default.join(dir, noteId);
        this.mirrorTracker.recordWrite(path_1.default.join(entityDir, 'events.jsonl'));
        this.mirrorTracker.recordWrite(path_1.default.join(entityDir, 'note.md'));
        this.mirrorTracker.recordWrite(path_1.default.join(entityDir, 'content.md'));
    }
    // -- Write (mutations with embed + dirty + emit + cross-graph cleanup) --
    async createNote(title, content, tags = []) {
        const embedding = await this.embedFns.document(`${title} ${content}`);
        const noteId = createNote(this._graph, title, content, tags, embedding, this.ctx.author);
        this._bm25Index.addDocument(noteId, this._graph.getNodeAttributes(noteId));
        this.ctx.markDirty();
        this.ctx.emit('note:created', { projectId: this.ctx.projectId, noteId });
        const dir = this.notesDir;
        if (dir) {
            const attrs = this._graph.getNodeAttributes(noteId);
            (0, file_mirror_1.mirrorNoteCreate)(dir, noteId, attrs, []);
            this.recordMirrorWrites(noteId);
        }
        return noteId;
    }
    async updateNote(noteId, patch, expectedVersion) {
        const existing = getNote(this._graph, noteId);
        if (!existing)
            return false;
        const embedText = `${patch.title ?? existing.title} ${patch.content ?? existing.content}`;
        const embedding = await this.embedFns.document(embedText);
        updateNote(this._graph, noteId, patch, embedding, this.ctx.author, expectedVersion);
        this._bm25Index.updateDocument(noteId, this._graph.getNodeAttributes(noteId));
        this.ctx.markDirty();
        this.ctx.emit('note:updated', { projectId: this.ctx.projectId, noteId });
        const dir = this.notesDir;
        if (dir) {
            const attrs = this._graph.getNodeAttributes(noteId);
            const relations = listRelations(this._graph, noteId, this.ext);
            (0, file_mirror_1.mirrorNoteUpdate)(dir, noteId, { ...patch, by: this.ctx.author }, attrs, relations);
            this.recordMirrorWrites(noteId);
        }
        return true;
    }
    deleteNote(noteId) {
        if (this.notesDir)
            (0, file_mirror_1.deleteMirrorDir)(this.notesDir, noteId);
        this._bm25Index.removeDocument(noteId);
        const ok = deleteNote(this._graph, noteId);
        if (!ok)
            return false;
        // Clean up proxy in TaskGraph if any task links to this note
        if (this.taskGraph) {
            const toRemove = [];
            this.taskGraph.forEachNode((id, attrs) => {
                if (attrs.proxyFor?.graph === 'knowledge' && attrs.proxyFor.nodeId === noteId) {
                    toRemove.push(id);
                }
            });
            for (const id of toRemove)
                this.taskGraph.dropNode(id);
        }
        this.ctx.markDirty();
        this.ctx.emit('note:deleted', { projectId: this.ctx.projectId, noteId });
        return true;
    }
    createRelation(fromId, toId, kind, targetGraph, projectId) {
        const pid = projectId || this.ctx.projectId;
        let ok;
        if (targetGraph) {
            const extGraph = (0, manager_types_1.resolveExternalGraph)(this.ext, targetGraph, pid);
            ok = createCrossRelation(this._graph, fromId, targetGraph, toId, kind, extGraph, pid);
            // Bidirectional: create mirror proxy in TaskGraph
            if (ok && targetGraph === 'tasks' && this.taskGraph) {
                createMirrorInTaskGraph(this.taskGraph, fromId, toId, kind, pid);
            }
        }
        else {
            ok = createRelation(this._graph, fromId, toId, kind);
        }
        if (ok) {
            this.ctx.markDirty();
            this.ctx.emit('note:relation:added', { projectId: this.ctx.projectId, noteId: fromId, toId, kind, targetGraph });
            const dir = this.notesDir;
            if (dir) {
                const attrs = this._graph.getNodeAttributes(fromId);
                const relations = listRelations(this._graph, fromId, this.ext);
                (0, file_mirror_1.mirrorNoteRelation)(dir, fromId, 'add', kind, toId, attrs, relations, targetGraph);
                this.recordMirrorWrites(fromId);
            }
        }
        return ok;
    }
    deleteRelation(fromId, toId, targetGraph, projectId) {
        const pid = projectId || this.ctx.projectId;
        // Read edge kind before deleting — check both directions for cross-graph
        let kind = '';
        try {
            if (targetGraph) {
                for (const tid of [toId, fromId]) {
                    const pnId = proxyId(targetGraph, tid, pid);
                    const other = tid === toId ? fromId : toId;
                    if (this._graph.hasEdge(other, pnId)) {
                        const ek = this._graph.edge(other, pnId);
                        if (ek) {
                            kind = this._graph.getEdgeAttribute(ek, 'kind') ?? '';
                            break;
                        }
                    }
                    if (this._graph.hasEdge(pnId, other)) {
                        const ek = this._graph.edge(pnId, other);
                        if (ek) {
                            kind = this._graph.getEdgeAttribute(ek, 'kind') ?? '';
                            break;
                        }
                    }
                }
            }
            else {
                if (this._graph.hasEdge(fromId, toId)) {
                    const ek = this._graph.edge(fromId, toId);
                    if (ek)
                        kind = this._graph.getEdgeAttribute(ek, 'kind') ?? '';
                }
            }
        }
        catch { /* ignore */ }
        let ok;
        if (targetGraph) {
            ok = deleteCrossRelation(this._graph, fromId, targetGraph, toId, pid);
            // Bidirectional: remove mirror proxy from TaskGraph
            if (ok && targetGraph === 'tasks' && this.taskGraph) {
                deleteMirrorFromTaskGraph(this.taskGraph, fromId, toId, pid);
                // Also clean up any reverse-direction edges (try both project-scoped and legacy)
                for (const [noteCandidate, taskCandidate] of [[toId, fromId], [fromId, toId]]) {
                    const proxyCandidates = pid
                        ? [`@knowledge::${pid}::${noteCandidate}`, `@knowledge::${noteCandidate}`]
                        : [`@knowledge::${noteCandidate}`];
                    for (const knowledgeProxy of proxyCandidates) {
                        if (this.taskGraph.hasNode(knowledgeProxy)) {
                            if (this.taskGraph.hasEdge(knowledgeProxy, taskCandidate)) {
                                this.taskGraph.dropEdge(knowledgeProxy, taskCandidate);
                                if (this.taskGraph.degree(knowledgeProxy) === 0)
                                    this.taskGraph.dropNode(knowledgeProxy);
                            }
                            if (this.taskGraph.hasEdge(taskCandidate, knowledgeProxy)) {
                                this.taskGraph.dropEdge(taskCandidate, knowledgeProxy);
                                if (this.taskGraph.degree(knowledgeProxy) === 0)
                                    this.taskGraph.dropNode(knowledgeProxy);
                            }
                        }
                    }
                }
            }
        }
        else {
            ok = deleteRelation(this._graph, fromId, toId);
        }
        if (ok) {
            this.ctx.markDirty();
            this.ctx.emit('note:relation:deleted', { projectId: this.ctx.projectId, noteId: fromId, toId, kind, targetGraph });
            const dir = this.notesDir;
            if (dir) {
                const realNoteId = this._graph.hasNode(fromId) && !isProxy(this._graph, fromId) ? fromId : toId;
                if (this._graph.hasNode(realNoteId) && !isProxy(this._graph, realNoteId)) {
                    const attrs = this._graph.getNodeAttributes(realNoteId);
                    const relations = listRelations(this._graph, realNoteId, this.ext);
                    (0, file_mirror_1.mirrorNoteRelation)(dir, realNoteId, 'remove', kind, toId, attrs, relations, targetGraph);
                    this.recordMirrorWrites(realNoteId);
                }
            }
        }
        return ok;
    }
    // -- Attachments --
    addAttachment(noteId, filename, data) {
        const dir = this.notesDir;
        if (!dir)
            return null;
        if (!this._graph.hasNode(noteId) || isProxy(this._graph, noteId))
            return null;
        if (data.length > attachment_types_1.MAX_ATTACHMENT_SIZE)
            return null;
        const existing = (0, attachment_types_1.scanAttachments)(path_1.default.join(dir, noteId));
        if (existing.length >= attachment_types_1.MAX_ATTACHMENTS_PER_ENTITY)
            return null;
        const safe = (0, file_mirror_1.sanitizeFilename)(filename);
        if (!safe)
            return null;
        (0, file_mirror_1.writeAttachment)(dir, noteId, safe, data);
        this.mirrorTracker?.recordWrite(path_1.default.join(dir, noteId, 'attachments', safe));
        (0, file_mirror_1.mirrorAttachmentEvent)(path_1.default.join(dir, noteId), 'add', safe);
        this.mirrorTracker?.recordWrite(path_1.default.join(dir, noteId, 'events.jsonl'));
        const attachments = (0, attachment_types_1.scanAttachments)(path_1.default.join(dir, noteId));
        this._graph.setNodeAttribute(noteId, 'attachments', attachments);
        this._graph.setNodeAttribute(noteId, 'updatedAt', Date.now());
        this.ctx.markDirty();
        this.ctx.emit('note:attachment:added', { projectId: this.ctx.projectId, noteId, filename: safe });
        return attachments.find(a => a.filename === safe) ?? null;
    }
    removeAttachment(noteId, filename) {
        const dir = this.notesDir;
        if (!dir)
            return false;
        if (!this._graph.hasNode(noteId) || isProxy(this._graph, noteId))
            return false;
        const safe = (0, file_mirror_1.sanitizeFilename)(filename);
        const deleted = (0, file_mirror_1.deleteAttachment)(dir, noteId, safe);
        if (!deleted)
            return false;
        this.mirrorTracker?.recordWrite(path_1.default.join(dir, noteId, 'attachments', safe));
        (0, file_mirror_1.mirrorAttachmentEvent)(path_1.default.join(dir, noteId), 'remove', safe);
        this.mirrorTracker?.recordWrite(path_1.default.join(dir, noteId, 'events.jsonl'));
        const attachments = (0, attachment_types_1.scanAttachments)(path_1.default.join(dir, noteId));
        this._graph.setNodeAttribute(noteId, 'attachments', attachments);
        this._graph.setNodeAttribute(noteId, 'updatedAt', Date.now());
        this.ctx.markDirty();
        this.ctx.emit('note:attachment:deleted', { projectId: this.ctx.projectId, noteId, filename: safe });
        return true;
    }
    syncAttachments(noteId) {
        const dir = this.notesDir;
        if (!dir)
            return;
        if (!this._graph.hasNode(noteId) || isProxy(this._graph, noteId))
            return;
        const attachments = (0, attachment_types_1.scanAttachments)(path_1.default.join(dir, noteId));
        this._graph.setNodeAttribute(noteId, 'attachments', attachments);
        this._graph.setNodeAttribute(noteId, 'updatedAt', Date.now());
        this._graph.setNodeAttribute(noteId, 'version', (this._graph.getNodeAttribute(noteId, 'version') ?? 0) + 1);
        this.ctx.markDirty();
    }
    listAttachments(noteId) {
        if (!this._graph.hasNode(noteId) || isProxy(this._graph, noteId))
            return [];
        return this._graph.getNodeAttribute(noteId, 'attachments') ?? [];
    }
    getAttachmentPath(noteId, filename) {
        const dir = this.notesDir;
        if (!dir)
            return null;
        return (0, file_mirror_1.getAttachmentPath)(dir, noteId, filename);
    }
    // -- Import from file (reverse mirror — does NOT write back to file) --
    async importFromFile(parsed) {
        const exists = this._graph.hasNode(parsed.id) && !isProxy(this._graph, parsed.id);
        const embedding = await this.embedFns.document(`${parsed.title} ${parsed.content}`);
        const now = Date.now();
        if (exists) {
            const existing = this._graph.getNodeAttributes(parsed.id);
            this._graph.mergeNodeAttributes(parsed.id, {
                title: parsed.title,
                content: parsed.content,
                tags: parsed.tags,
                embedding,
                attachments: parsed.attachments,
                updatedAt: now,
                createdAt: existing.createdAt,
                version: parsed.version ?? existing.version + 1,
                // preserve createdBy from graph if file doesn't have it
                ...(parsed.createdBy != null ? { createdBy: parsed.createdBy } : {}),
                ...(parsed.updatedBy != null ? { updatedBy: parsed.updatedBy } : {}),
            });
        }
        else {
            this._graph.addNode(parsed.id, {
                title: parsed.title,
                content: parsed.content,
                tags: parsed.tags,
                embedding,
                attachments: parsed.attachments ?? [],
                createdAt: parsed.createdAt ?? now,
                updatedAt: now,
                version: parsed.version ?? 1,
                createdBy: parsed.createdBy ?? undefined,
                updatedBy: parsed.updatedBy ?? undefined,
            });
        }
        this._bm25Index.updateDocument(parsed.id, this._graph.getNodeAttributes(parsed.id));
        // Sync relations
        this.syncRelationsFromFile(parsed.id, parsed.relations);
        this.ctx.markDirty();
        this.ctx.emit(exists ? 'note:updated' : 'note:created', { projectId: this.ctx.projectId, noteId: parsed.id });
    }
    updateContentFromFile(noteId, content) {
        if (!this._graph.hasNode(noteId) || isProxy(this._graph, noteId))
            return;
        this._graph.setNodeAttribute(noteId, 'content', content);
        this._graph.setNodeAttribute(noteId, 'updatedAt', Date.now());
        this._graph.setNodeAttribute(noteId, 'version', (this._graph.getNodeAttribute(noteId, 'version') ?? 0) + 1);
        this.ctx.markDirty();
        this.ctx.emit('note:updated', { projectId: this.ctx.projectId, noteId });
    }
    deleteFromFile(noteId) {
        if (!this._graph.hasNode(noteId))
            return;
        if (isProxy(this._graph, noteId))
            return;
        this._bm25Index.removeDocument(noteId);
        deleteNote(this._graph, noteId);
        if (this.taskGraph) {
            const pId = `@knowledge::${noteId}`;
            if (this.taskGraph.hasNode(pId))
                this.taskGraph.dropNode(pId);
        }
        this.ctx.markDirty();
        this.ctx.emit('note:deleted', { projectId: this.ctx.projectId, noteId });
    }
    syncRelationsFromFile(noteId, desired) {
        // Build current outgoing relations from graph
        const current = [];
        this._graph.forEachOutEdge(noteId, (_edge, attrs, _src, target) => {
            const proxy = this._graph.hasNode(target) ? this._graph.getNodeAttribute(target, 'proxyFor') : undefined;
            if (proxy) {
                current.push({ to: proxy.nodeId, kind: attrs.kind, graph: proxy.graph });
            }
            else {
                current.push({ to: target, kind: attrs.kind });
            }
        });
        const diff = (0, file_import_1.diffRelations)(current, desired);
        for (const rel of diff.toRemove) {
            if (rel.graph) {
                deleteCrossRelation(this._graph, noteId, rel.graph, rel.to);
                if (rel.graph === 'tasks' && this.taskGraph) {
                    deleteMirrorFromTaskGraph(this.taskGraph, noteId, rel.to);
                }
            }
            else {
                deleteRelation(this._graph, noteId, rel.to);
            }
        }
        for (const rel of diff.toAdd) {
            if (rel.graph) {
                const extGraph = (0, manager_types_1.resolveExternalGraph)(this.ext, rel.graph);
                createCrossRelation(this._graph, noteId, rel.graph, rel.to, rel.kind, extGraph);
                if (rel.graph === 'tasks' && this.taskGraph) {
                    createMirrorInTaskGraph(this.taskGraph, noteId, rel.to, rel.kind);
                }
            }
            else {
                createRelation(this._graph, noteId, rel.to, rel.kind);
            }
        }
    }
    // -- Read --
    getNote(noteId) {
        const note = getNote(this._graph, noteId);
        if (!note)
            return null;
        const relations = listRelations(this._graph, noteId, this.ext);
        return { ...note, relations };
    }
    listNotes(filter, tag, limit, offset) {
        return listNotes(this._graph, filter, tag, limit, offset);
    }
    async searchNotes(query, opts) {
        const embedding = opts?.searchMode === 'keyword' ? [] : await this.embedFns.query(query);
        return (0, knowledge_1.searchKnowledge)(this._graph, embedding, { ...opts, queryText: query, bm25Index: this._bm25Index });
    }
    listRelations(noteId) {
        return listRelations(this._graph, noteId, this.ext);
    }
    findLinkedNotes(targetGraph, targetNodeId, kind, projectId) {
        return findLinkedNotes(this._graph, targetGraph, targetNodeId, kind, projectId || this.ctx.projectId);
    }
}
exports.KnowledgeGraphManager = KnowledgeGraphManager;
