"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SkillGraphManager = exports.createSkillGraph = void 0;
exports.proxyId = proxyId;
exports.isProxy = isProxy;
exports.cleanupProxies = cleanupProxies;
exports.createSkill = createSkill;
exports.updateSkill = updateSkill;
exports.bumpUsage = bumpUsage;
exports.deleteSkill = deleteSkill;
exports.getSkill = getSkill;
exports.listSkills = listSkills;
exports.createSkillRelation = createSkillRelation;
exports.deleteSkillRelation = deleteSkillRelation;
exports.listSkillRelations = listSkillRelations;
exports.findLinkedSkills = findLinkedSkills;
exports.createCrossRelation = createCrossRelation;
exports.deleteCrossRelation = deleteCrossRelation;
exports.saveSkillGraph = saveSkillGraph;
exports.loadSkillGraph = loadSkillGraph;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const skill_types_1 = require("../graphs/skill-types");
Object.defineProperty(exports, "createSkillGraph", { enumerable: true, get: function () { return skill_types_1.createSkillGraph; } });
const knowledge_types_1 = require("../graphs/knowledge-types");
const manager_types_1 = require("../graphs/manager-types");
const skills_1 = require("../lib/search/skills");
const bm25_1 = require("../lib/search/bm25");
const file_mirror_1 = require("../lib/file-mirror");
const logger_1 = require("../lib/logger");
const log = (0, logger_1.createLogger)('skill-graph');
const embedding_codec_1 = require("../lib/embedding-codec");
const graph_persistence_1 = require("../lib/graph-persistence");
const defaults_1 = require("../lib/defaults");
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: '',
            description: '',
            steps: [],
            triggers: [],
            inputHints: [],
            filePatterns: [],
            tags: [],
            source: 'user',
            confidence: 1,
            usageCount: 0,
            lastUsedAt: null,
            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) {
            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 — Skills
// ---------------------------------------------------------------------------
/** Create a skill, return its slug ID. */
function createSkill(graph, title, description, steps, triggers, inputHints, filePatterns, tags, source, confidence, embedding, author = '') {
    const id = (0, knowledge_types_1.generateId)();
    const now = Date.now();
    graph.addNode(id, {
        title,
        description,
        steps,
        triggers,
        inputHints,
        filePatterns,
        tags,
        source,
        confidence,
        usageCount: 0,
        lastUsedAt: null,
        embedding,
        attachments: [],
        createdAt: now,
        updatedAt: now,
        version: 1,
        createdBy: author || undefined,
        updatedBy: author || undefined,
    });
    return id;
}
/** Partial update of a skill. Returns true if found and updated. Throws VersionConflictError if expectedVersion is provided and doesn't match. */
function updateSkill(graph, skillId, patch, embedding, author = '', expectedVersion) {
    if (!graph.hasNode(skillId))
        return false;
    if (isProxy(graph, skillId))
        return false;
    if (expectedVersion !== undefined) {
        const current = graph.getNodeAttribute(skillId, 'version');
        if (current !== expectedVersion)
            throw new manager_types_1.VersionConflictError(current, expectedVersion);
    }
    if (patch.title !== undefined)
        graph.setNodeAttribute(skillId, 'title', patch.title);
    if (patch.description !== undefined)
        graph.setNodeAttribute(skillId, 'description', patch.description);
    if (patch.steps !== undefined)
        graph.setNodeAttribute(skillId, 'steps', patch.steps);
    if (patch.triggers !== undefined)
        graph.setNodeAttribute(skillId, 'triggers', patch.triggers);
    if (patch.inputHints !== undefined)
        graph.setNodeAttribute(skillId, 'inputHints', patch.inputHints);
    if (patch.filePatterns !== undefined)
        graph.setNodeAttribute(skillId, 'filePatterns', patch.filePatterns);
    if (patch.tags !== undefined)
        graph.setNodeAttribute(skillId, 'tags', patch.tags);
    if (patch.source !== undefined)
        graph.setNodeAttribute(skillId, 'source', patch.source);
    if (patch.confidence !== undefined)
        graph.setNodeAttribute(skillId, 'confidence', patch.confidence);
    if (embedding !== undefined)
        graph.setNodeAttribute(skillId, 'embedding', embedding);
    if (author)
        graph.setNodeAttribute(skillId, 'updatedBy', author);
    graph.setNodeAttribute(skillId, 'version', graph.getNodeAttribute(skillId, 'version') + 1);
    graph.setNodeAttribute(skillId, 'updatedAt', Date.now());
    return true;
}
/** Increment usageCount and set lastUsedAt. Returns true if found. */
function bumpUsage(graph, skillId) {
    if (!graph.hasNode(skillId))
        return false;
    if (isProxy(graph, skillId))
        return false;
    const count = graph.getNodeAttribute(skillId, 'usageCount');
    graph.setNodeAttribute(skillId, 'usageCount', count + 1);
    graph.setNodeAttribute(skillId, 'lastUsedAt', Date.now());
    graph.setNodeAttribute(skillId, 'version', graph.getNodeAttribute(skillId, 'version') + 1);
    graph.setNodeAttribute(skillId, 'updatedAt', Date.now());
    return true;
}
/** Delete a skill and all its incident edges. Also cleans up orphaned proxy nodes. */
function deleteSkill(graph, skillId) {
    if (!graph.hasNode(skillId))
        return false;
    if (isProxy(graph, skillId))
        return false;
    const proxyNeighbors = [];
    graph.forEachNeighbor(skillId, (neighbor) => {
        if (isProxy(graph, neighbor))
            proxyNeighbors.push(neighbor);
    });
    graph.dropNode(skillId);
    for (const p of proxyNeighbors) {
        cleanupProxy(graph, p);
    }
    return true;
}
/** Get a skill by ID, or null if not found. Excludes proxy nodes. */
function getSkill(graph, skillId) {
    if (!graph.hasNode(skillId))
        return null;
    if (isProxy(graph, skillId))
        return null;
    const attrs = graph.getNodeAttributes(skillId);
    const dependsOn = [];
    const dependedBy = [];
    const related = [];
    const variants = [];
    const crossLinks = [];
    // Incoming edges
    graph.forEachInEdge(skillId, (_edge, edgeAttrs, source) => {
        if (isProxy(graph, source)) {
            const proxyFor = graph.getNodeAttribute(source, 'proxyFor');
            if (proxyFor) {
                crossLinks.push({
                    nodeId: proxyFor.nodeId,
                    targetGraph: proxyFor.graph,
                    kind: edgeAttrs.kind,
                    direction: 'incoming',
                });
            }
            return;
        }
        const srcAttrs = graph.getNodeAttributes(source);
        if (edgeAttrs.kind === 'depends_on') {
            // source depends_on this skill → this skill is depended by source
            dependedBy.push({ id: source, title: srcAttrs.title });
        }
        else if (edgeAttrs.kind === 'related_to') {
            related.push({ id: source, title: srcAttrs.title });
        }
        else if (edgeAttrs.kind === 'variant_of') {
            variants.push({ id: source, title: srcAttrs.title });
        }
    });
    // Outgoing edges
    graph.forEachOutEdge(skillId, (_edge, edgeAttrs, _source, target) => {
        if (isProxy(graph, target)) {
            const proxyFor = graph.getNodeAttribute(target, 'proxyFor');
            if (proxyFor) {
                crossLinks.push({
                    nodeId: proxyFor.nodeId,
                    targetGraph: proxyFor.graph,
                    kind: edgeAttrs.kind,
                    direction: 'outgoing',
                });
            }
            return;
        }
        const tgtAttrs = graph.getNodeAttributes(target);
        if (edgeAttrs.kind === 'depends_on') {
            dependsOn.push({ id: target, title: tgtAttrs.title });
        }
        else if (edgeAttrs.kind === 'related_to') {
            if (!related.some(r => r.id === target)) {
                related.push({ id: target, title: tgtAttrs.title });
            }
        }
        else if (edgeAttrs.kind === 'variant_of') {
            if (!variants.some(v => v.id === target)) {
                variants.push({ id: target, title: tgtAttrs.title });
            }
        }
    });
    return {
        id: skillId,
        title: attrs.title,
        description: attrs.description,
        steps: attrs.steps,
        triggers: attrs.triggers,
        inputHints: attrs.inputHints,
        filePatterns: attrs.filePatterns,
        tags: attrs.tags,
        source: attrs.source,
        confidence: attrs.confidence,
        usageCount: attrs.usageCount,
        lastUsedAt: attrs.lastUsedAt,
        version: attrs.version,
        createdAt: attrs.createdAt,
        updatedAt: attrs.updatedAt,
        attachments: attrs.attachments ?? [],
        dependsOn,
        dependedBy,
        related,
        variants,
        crossLinks,
    };
}
/** List skills with optional filters. Excludes proxy nodes. */
function listSkills(graph, opts = {}) {
    const { source, tag, filter, limit = defaults_1.LIST_PAGE_SIZE, offset = 0 } = opts;
    const lowerFilter = filter?.toLowerCase();
    const lowerTag = tag?.toLowerCase();
    const results = [];
    graph.forEachNode((id, attrs) => {
        if (attrs.proxyFor)
            return;
        if (source && attrs.source !== source)
            return;
        if (lowerTag && !attrs.tags.some(t => t.toLowerCase() === lowerTag))
            return;
        if (lowerFilter) {
            const match = id.toLowerCase().includes(lowerFilter) ||
                attrs.title.toLowerCase().includes(lowerFilter);
            if (!match)
                return;
        }
        results.push({
            id,
            title: attrs.title,
            description: attrs.description?.slice(0, defaults_1.CONTENT_PREVIEW_LEN),
            steps: attrs.steps,
            triggers: attrs.triggers,
            inputHints: attrs.inputHints,
            filePatterns: attrs.filePatterns,
            tags: attrs.tags,
            source: attrs.source,
            confidence: attrs.confidence,
            usageCount: attrs.usageCount,
            lastUsedAt: attrs.lastUsedAt,
            version: attrs.version,
            createdAt: attrs.createdAt,
            updatedAt: attrs.updatedAt,
            attachments: attrs.attachments ?? [],
        });
    });
    const sorted = results.sort((a, b) => {
        // Sort by usageCount desc, then updatedAt desc
        if (a.usageCount !== b.usageCount)
            return b.usageCount - a.usageCount;
        return b.updatedAt - a.updatedAt;
    });
    return { results: sorted.slice(offset, offset + limit), total: sorted.length };
}
// ---------------------------------------------------------------------------
// CRUD — Skill Relations (skill ↔ skill)
// ---------------------------------------------------------------------------
/** Create a directed relation between two skills. Returns true if created. */
function createSkillRelation(graph, fromId, toId, kind) {
    if (!graph.hasNode(fromId) || !graph.hasNode(toId))
        return false;
    if (isProxy(graph, fromId) || isProxy(graph, toId))
        return false;
    if (graph.hasEdge(fromId, toId))
        return false;
    graph.addEdgeWithKey(`${fromId}→${toId}`, fromId, toId, { kind });
    return true;
}
/** Delete a skill relation. Returns true if it existed. */
function deleteSkillRelation(graph, fromId, toId) {
    if (!graph.hasEdge(fromId, toId))
        return false;
    graph.dropEdge(fromId, toId);
    return true;
}
/** List all relations for a skill (both incoming and outgoing). Resolves proxy IDs and titles. */
function listSkillRelations(graph, skillId, externalGraphs) {
    if (!graph.hasNode(skillId))
        return [];
    const results = [];
    function resolveTitle(nodeId, targetGraph) {
        if (!targetGraph) {
            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) {
        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 } : {}) };
        }
        const otherId = source === skillId ? target : source;
        const title = resolveTitle(otherId);
        return { fromId: source, toId: target, kind, ...(title ? { title } : {}) };
    }
    graph.forEachOutEdge(skillId, (_edge, attrs, source, target) => {
        results.push(resolveEntry(source, target, attrs.kind));
    });
    graph.forEachInEdge(skillId, (_edge, attrs, source, target) => {
        results.push(resolveEntry(source, target, attrs.kind));
    });
    return results;
}
/**
 * Find all skills that have a cross-graph relation to the given target node.
 * Optionally filter by relation kind.
 */
function findLinkedSkills(graph, targetGraph, targetNodeId, kind, projectId) {
    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 skillAttrs = graph.getNodeAttributes(source);
            seen.add(source);
            results.push({
                skillId: source,
                title: skillAttrs.title,
                kind: attrs.kind,
                source: skillAttrs.source,
                confidence: skillAttrs.confidence,
                tags: skillAttrs.tags,
            });
        });
    }
    return results;
}
// ---------------------------------------------------------------------------
// Cross-graph relations (skill → doc/code/file/knowledge/task node)
// ---------------------------------------------------------------------------
/**
 * Create a cross-graph relation from a skill to a node in an external graph.
 * Optionally validates that the target exists in the external graph.
 */
function createCrossRelation(graph, fromSkillId, targetGraph, targetNodeId, kind, externalGraph, projectId) {
    if (!graph.hasNode(fromSkillId) || isProxy(graph, fromSkillId))
        return false;
    if (externalGraph && !externalGraph.hasNode(targetNodeId))
        return false;
    const pId = ensureProxyNode(graph, targetGraph, targetNodeId, projectId);
    if (graph.hasEdge(fromSkillId, pId))
        return false;
    graph.addEdgeWithKey(`${fromSkillId}→${pId}`, fromSkillId, pId, { kind });
    return true;
}
/**
 * Delete a cross-graph relation. Cleans up orphaned proxy node.
 */
function deleteCrossRelation(graph, fromSkillId, targetGraph, targetNodeId, projectId) {
    const candidates = [proxyId(targetGraph, targetNodeId, projectId)];
    if (projectId)
        candidates.push(proxyId(targetGraph, targetNodeId));
    for (const pId of candidates) {
        // Outgoing: skillId → proxy(targetId) — skill created this link
        if (graph.hasEdge(fromSkillId, pId)) {
            graph.dropEdge(fromSkillId, pId);
            cleanupProxy(graph, pId);
            return true;
        }
        // Incoming: proxy(targetId) → skillId — mirror from another graph
        if (graph.hasEdge(pId, fromSkillId)) {
            graph.dropEdge(pId, fromSkillId);
            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, fromSkillId, projectId)];
    if (projectId)
        reverseCandidates.push(proxyId(targetGraph, fromSkillId));
    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 saveSkillGraph(graph, graphMemory, embeddingFingerprint) {
    fs_1.default.mkdirSync(graphMemory, { recursive: true });
    const file = path_1.default.join(graphMemory, 'skills.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 loadSkillGraph(graphMemory, fresh = false, embeddingFingerprint) {
    const graph = (0, skill_types_1.createSkillGraph)();
    if (fresh)
        return graph;
    const file = path_1.default.join(graphMemory, 'skills.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 (Skill ↔ Knowledge, Skill ↔ Task)
// ---------------------------------------------------------------------------
/**
 * Create a mirror proxy in KnowledgeGraph when a skill links to a note.
 */
function createMirrorInKnowledgeGraph(knowledgeGraph, skillId, noteId, kind, projectId) {
    const mirrorProxyId = projectId ? `@skills::${projectId}::${skillId}` : `@skills::${skillId}`;
    if (!knowledgeGraph.hasNode(mirrorProxyId)) {
        knowledgeGraph.addNode(mirrorProxyId, {
            title: '',
            content: '',
            tags: [],
            embedding: [],
            attachments: [],
            createdAt: 0,
            updatedAt: 0,
            version: 0,
            proxyFor: { graph: 'skills', nodeId: skillId, ...(projectId ? { projectId } : {}) },
        });
    }
    if (!knowledgeGraph.hasNode(noteId))
        return;
    const edgeKey = `${mirrorProxyId}→${noteId}`;
    if (!knowledgeGraph.hasEdge(edgeKey)) {
        knowledgeGraph.addEdgeWithKey(edgeKey, mirrorProxyId, noteId, { kind });
    }
}
function deleteMirrorFromKnowledgeGraph(knowledgeGraph, skillId, noteId, projectId) {
    const candidates = projectId
        ? [`@skills::${projectId}::${skillId}`, `@skills::${skillId}`]
        : [`@skills::${skillId}`];
    for (const mirrorProxyId of candidates) {
        const edgeKey = `${mirrorProxyId}→${noteId}`;
        if (knowledgeGraph.hasEdge(edgeKey)) {
            knowledgeGraph.dropEdge(edgeKey);
        }
        if (knowledgeGraph.hasNode(mirrorProxyId)) {
            const proxyFor = knowledgeGraph.getNodeAttribute(mirrorProxyId, 'proxyFor');
            if (proxyFor && knowledgeGraph.degree(mirrorProxyId) === 0) {
                knowledgeGraph.dropNode(mirrorProxyId);
            }
        }
    }
}
/**
 * Create a mirror proxy in TaskGraph when a skill links to a task.
 */
function createMirrorInTaskGraph(taskGraph, skillId, taskId, kind, projectId) {
    const mirrorProxyId = projectId ? `@skills::${projectId}::${skillId}` : `@skills::${skillId}`;
    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: 'skills', nodeId: skillId, ...(projectId ? { projectId } : {}) },
        });
    }
    if (!taskGraph.hasNode(taskId))
        return;
    const edgeKey = `${mirrorProxyId}→${taskId}`;
    if (!taskGraph.hasEdge(edgeKey)) {
        taskGraph.addEdgeWithKey(edgeKey, mirrorProxyId, taskId, { kind });
    }
}
function deleteMirrorFromTaskGraph(taskGraph, skillId, taskId, projectId) {
    const candidates = projectId
        ? [`@skills::${projectId}::${skillId}`, `@skills::${skillId}`]
        : [`@skills::${skillId}`];
    for (const mirrorProxyId of candidates) {
        const edgeKey = `${mirrorProxyId}→${taskId}`;
        if (taskGraph.hasEdge(edgeKey)) {
            taskGraph.dropEdge(edgeKey);
        }
        if (taskGraph.hasNode(mirrorProxyId)) {
            const proxyFor = taskGraph.getNodeAttribute(mirrorProxyId, 'proxyFor');
            if (proxyFor && taskGraph.degree(mirrorProxyId) === 0) {
                taskGraph.dropNode(mirrorProxyId);
            }
        }
    }
}
// ---------------------------------------------------------------------------
// SkillGraphManager — unified API for skill graph operations
// ---------------------------------------------------------------------------
class SkillGraphManager {
    _graph;
    embedFns;
    ctx;
    ext;
    knowledgeGraph;
    taskGraph;
    mirrorTracker;
    _bm25Index;
    get projectDir() { return this.ctx.projectDir; }
    constructor(_graph, embedFns, ctx, ext = {}) {
        this._graph = _graph;
        this.embedFns = embedFns;
        this.ctx = ctx;
        this.ext = ext;
        this.knowledgeGraph = ext.knowledgeGraph;
        this.taskGraph = ext.taskGraph;
        this._bm25Index = new bm25_1.BM25Index((attrs) => `${attrs.title} ${attrs.description} ${attrs.triggers.join(' ')} ${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(skillId) {
        if (!this._graph.hasNode(skillId))
            return null;
        if (isProxy(this._graph, skillId))
            return null;
        return this._graph.getNodeAttribute(skillId, 'updatedAt') ?? null;
    }
    get skillsDir() {
        const base = this.ctx.mirrorDir ?? this.ctx.projectDir;
        return base ? path_1.default.join(base, '.skills') : undefined;
    }
    recordMirrorWrites(skillId) {
        const dir = this.skillsDir;
        if (!dir || !this.mirrorTracker)
            return;
        const entityDir = path_1.default.join(dir, skillId);
        this.mirrorTracker.recordWrite(path_1.default.join(entityDir, 'events.jsonl'));
        this.mirrorTracker.recordWrite(path_1.default.join(entityDir, 'skill.md'));
        this.mirrorTracker.recordWrite(path_1.default.join(entityDir, 'description.md'));
    }
    // -- Write (mutations with embed + dirty + emit + cross-graph cleanup) --
    async createSkill(title, description, steps = [], triggers = [], inputHints = [], filePatterns = [], tags = [], source = 'user', confidence = 1) {
        const embedding = await this.embedFns.document(`${title} ${description}`);
        const skillId = createSkill(this._graph, title, description, steps, triggers, inputHints, filePatterns, tags, source, confidence, embedding, this.ctx.author);
        this._bm25Index.addDocument(skillId, this._graph.getNodeAttributes(skillId));
        this.ctx.markDirty();
        this.ctx.emit('skill:created', { projectId: this.ctx.projectId, skillId });
        const dir = this.skillsDir;
        if (dir) {
            const attrs = this._graph.getNodeAttributes(skillId);
            (0, file_mirror_1.mirrorSkillCreate)(dir, skillId, attrs, []);
            this.recordMirrorWrites(skillId);
        }
        return skillId;
    }
    async updateSkill(skillId, patch, expectedVersion) {
        const existing = getSkill(this._graph, skillId);
        if (!existing)
            return false;
        const embedText = `${patch.title ?? existing.title} ${patch.description ?? existing.description}`;
        const embedding = await this.embedFns.document(embedText);
        updateSkill(this._graph, skillId, patch, embedding, this.ctx.author, expectedVersion);
        this._bm25Index.updateDocument(skillId, this._graph.getNodeAttributes(skillId));
        this.ctx.markDirty();
        this.ctx.emit('skill:updated', { projectId: this.ctx.projectId, skillId });
        const dir = this.skillsDir;
        if (dir) {
            const attrs = this._graph.getNodeAttributes(skillId);
            const relations = listSkillRelations(this._graph, skillId, this.ext);
            (0, file_mirror_1.mirrorSkillUpdate)(dir, skillId, { ...patch, by: this.ctx.author }, attrs, relations);
            this.recordMirrorWrites(skillId);
        }
        return true;
    }
    deleteSkill(skillId) {
        if (this.skillsDir)
            (0, file_mirror_1.deleteMirrorDir)(this.skillsDir, skillId);
        this._bm25Index.removeDocument(skillId);
        const ok = deleteSkill(this._graph, skillId);
        if (!ok)
            return false;
        // Clean up proxy in KnowledgeGraph if any note links to this skill
        if (this.knowledgeGraph) {
            const pId = `@skills::${skillId}`;
            if (this.knowledgeGraph.hasNode(pId)) {
                this.knowledgeGraph.dropNode(pId);
            }
        }
        // Clean up proxy in TaskGraph if any task links to this skill
        if (this.taskGraph) {
            const pId = `@skills::${skillId}`;
            if (this.taskGraph.hasNode(pId)) {
                this.taskGraph.dropNode(pId);
            }
        }
        this.ctx.markDirty();
        this.ctx.emit('skill:deleted', { projectId: this.ctx.projectId, skillId });
        return true;
    }
    bumpUsage(skillId) {
        const ok = bumpUsage(this._graph, skillId);
        if (!ok)
            return false;
        this.ctx.markDirty();
        this.ctx.emit('skill:updated', { projectId: this.ctx.projectId, skillId });
        const dir = this.skillsDir;
        if (dir) {
            const attrs = this._graph.getNodeAttributes(skillId);
            const relations = listSkillRelations(this._graph, skillId, this.ext);
            (0, file_mirror_1.mirrorSkillUpdate)(dir, skillId, { usageCount: attrs.usageCount, lastUsedAt: attrs.lastUsedAt, by: this.ctx.author }, attrs, relations);
            this.recordMirrorWrites(skillId);
        }
        return true;
    }
    linkSkills(fromId, toId, kind) {
        const ok = createSkillRelation(this._graph, fromId, toId, kind);
        if (ok) {
            this.ctx.markDirty();
            this.ctx.emit('skill:relation:added', { projectId: this.ctx.projectId, skillId: fromId, toId, kind });
            const dir = this.skillsDir;
            if (dir) {
                const fromAttrs = this._graph.getNodeAttributes(fromId);
                const fromRels = listSkillRelations(this._graph, fromId, this.ext);
                (0, file_mirror_1.mirrorSkillRelation)(dir, fromId, 'add', kind, toId, fromAttrs, fromRels);
                this.recordMirrorWrites(fromId);
            }
        }
        return ok;
    }
    createCrossLink(skillId, targetId, targetGraph, kind, projectId) {
        const pid = projectId || this.ctx.projectId;
        const extGraph = (0, manager_types_1.resolveExternalGraph)(this.ext, targetGraph, pid);
        const ok = createCrossRelation(this._graph, skillId, targetGraph, targetId, kind, extGraph, pid);
        // Bidirectional: create mirror proxy in KnowledgeGraph
        if (ok && targetGraph === 'knowledge' && this.knowledgeGraph) {
            createMirrorInKnowledgeGraph(this.knowledgeGraph, skillId, targetId, kind, pid);
        }
        // Bidirectional: create mirror proxy in TaskGraph
        if (ok && targetGraph === 'tasks' && this.taskGraph) {
            createMirrorInTaskGraph(this.taskGraph, skillId, targetId, kind, pid);
        }
        if (ok) {
            this.ctx.markDirty();
            this.ctx.emit('skill:relation:added', { projectId: this.ctx.projectId, skillId, toId: targetId, kind, targetGraph });
            const dir = this.skillsDir;
            if (dir) {
                const attrs = this._graph.getNodeAttributes(skillId);
                const relations = listSkillRelations(this._graph, skillId, this.ext);
                (0, file_mirror_1.mirrorSkillRelation)(dir, skillId, 'add', kind, targetId, attrs, relations, targetGraph);
                this.recordMirrorWrites(skillId);
            }
        }
        return ok;
    }
    deleteCrossLink(skillId, targetId, targetGraph, projectId) {
        const pid = projectId || this.ctx.projectId;
        // Read edge kind before deleting — check both directions
        let kind = '';
        try {
            for (const tid of [targetId, skillId]) {
                const pnId = proxyId(targetGraph, tid, pid);
                const other = tid === targetId ? skillId : targetId;
                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;
                    }
                }
            }
        }
        catch { /* ignore */ }
        const ok = deleteCrossRelation(this._graph, skillId, targetGraph, targetId, pid);
        if (ok && targetGraph === 'knowledge' && this.knowledgeGraph) {
            // Remove mirror/original edge from KnowledgeGraph in both directions
            deleteMirrorFromKnowledgeGraph(this.knowledgeGraph, skillId, targetId, pid);
            // Also try removing original edge if link was created from knowledge side
            for (const [noteCandidate, skillCandidate] of [[targetId, skillId], [skillId, targetId]]) {
                const proxyCandidates = pid
                    ? [`@skills::${pid}::${skillCandidate}`, `@skills::${skillCandidate}`]
                    : [`@skills::${skillCandidate}`];
                for (const skillProxy of proxyCandidates) {
                    if (this.knowledgeGraph.hasNode(skillProxy)) {
                        if (this.knowledgeGraph.hasEdge(noteCandidate, skillProxy)) {
                            this.knowledgeGraph.dropEdge(noteCandidate, skillProxy);
                            if (this.knowledgeGraph.degree(skillProxy) === 0)
                                this.knowledgeGraph.dropNode(skillProxy);
                        }
                        if (this.knowledgeGraph.hasEdge(skillProxy, noteCandidate)) {
                            this.knowledgeGraph.dropEdge(skillProxy, noteCandidate);
                            if (this.knowledgeGraph.degree(skillProxy) === 0)
                                this.knowledgeGraph.dropNode(skillProxy);
                        }
                    }
                }
            }
        }
        if (ok && targetGraph === 'tasks' && this.taskGraph) {
            // Remove mirror/original edge from TaskGraph in both directions
            deleteMirrorFromTaskGraph(this.taskGraph, skillId, targetId, pid);
            // Also try removing original edge if link was created from task side
            for (const [taskCandidate, skillCandidate] of [[targetId, skillId], [skillId, targetId]]) {
                const proxyCandidates = pid
                    ? [`@skills::${pid}::${skillCandidate}`, `@skills::${skillCandidate}`]
                    : [`@skills::${skillCandidate}`];
                for (const skillProxy of proxyCandidates) {
                    if (this.taskGraph.hasNode(skillProxy)) {
                        if (this.taskGraph.hasEdge(taskCandidate, skillProxy)) {
                            this.taskGraph.dropEdge(taskCandidate, skillProxy);
                            if (this.taskGraph.degree(skillProxy) === 0)
                                this.taskGraph.dropNode(skillProxy);
                        }
                        if (this.taskGraph.hasEdge(skillProxy, taskCandidate)) {
                            this.taskGraph.dropEdge(skillProxy, taskCandidate);
                            if (this.taskGraph.degree(skillProxy) === 0)
                                this.taskGraph.dropNode(skillProxy);
                        }
                    }
                }
            }
        }
        if (ok) {
            this.ctx.markDirty();
            this.ctx.emit('skill:relation:deleted', { projectId: this.ctx.projectId, skillId, toId: targetId, kind, targetGraph });
            const dir = this.skillsDir;
            if (dir) {
                // skillId might actually be a noteId/taskId for incoming mirrors — use the real skill node
                const realSkillId = this._graph.hasNode(skillId) && !isProxy(this._graph, skillId) ? skillId : targetId;
                if (this._graph.hasNode(realSkillId) && !isProxy(this._graph, realSkillId)) {
                    const attrs = this._graph.getNodeAttributes(realSkillId);
                    const relations = listSkillRelations(this._graph, realSkillId, this.ext);
                    (0, file_mirror_1.mirrorSkillRelation)(dir, realSkillId, 'remove', kind, targetId, attrs, relations, targetGraph);
                    this.recordMirrorWrites(realSkillId);
                }
            }
        }
        return ok;
    }
    deleteSkillLink(fromId, toId) {
        // Read edge kind before deleting
        let kind = '';
        try {
            if (this._graph.hasEdge(fromId, toId)) {
                const ek = this._graph.edge(fromId, toId);
                if (ek)
                    kind = this._graph.getEdgeAttribute(ek, 'kind') ?? '';
            }
        }
        catch { /* ignore */ }
        const ok = deleteSkillRelation(this._graph, fromId, toId);
        if (ok) {
            this.ctx.markDirty();
            this.ctx.emit('skill:relation:deleted', { projectId: this.ctx.projectId, skillId: fromId, toId, kind });
            const dir = this.skillsDir;
            if (dir) {
                const fromAttrs = this._graph.getNodeAttributes(fromId);
                const fromRels = listSkillRelations(this._graph, fromId, this.ext);
                (0, file_mirror_1.mirrorSkillRelation)(dir, fromId, 'remove', kind, toId, fromAttrs, fromRels);
                this.recordMirrorWrites(fromId);
            }
        }
        return ok;
    }
    // -- Attachments --
    addAttachment(skillId, filename, data) {
        const dir = this.skillsDir;
        if (!dir)
            return null;
        if (!this._graph.hasNode(skillId) || isProxy(this._graph, skillId))
            return null;
        if (data.length > attachment_types_1.MAX_ATTACHMENT_SIZE)
            return null;
        const entityDir = path_1.default.join(dir, skillId);
        const existing = (0, attachment_types_1.scanAttachments)(entityDir);
        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, skillId, safe, data);
        this.mirrorTracker?.recordWrite(path_1.default.join(entityDir, 'attachments', safe));
        (0, file_mirror_1.mirrorAttachmentEvent)(entityDir, 'add', safe);
        this.mirrorTracker?.recordWrite(path_1.default.join(entityDir, 'events.jsonl'));
        const attachments = (0, attachment_types_1.scanAttachments)(entityDir);
        this._graph.setNodeAttribute(skillId, 'attachments', attachments);
        this._graph.setNodeAttribute(skillId, 'updatedAt', Date.now());
        this.ctx.markDirty();
        this.ctx.emit('skill:attachment:added', { projectId: this.ctx.projectId, skillId, filename: safe });
        return attachments.find(a => a.filename === safe) ?? null;
    }
    removeAttachment(skillId, filename) {
        const dir = this.skillsDir;
        if (!dir)
            return false;
        if (!this._graph.hasNode(skillId) || isProxy(this._graph, skillId))
            return false;
        const safe = (0, file_mirror_1.sanitizeFilename)(filename);
        const entityDir = path_1.default.join(dir, skillId);
        const deleted = (0, file_mirror_1.deleteAttachment)(dir, skillId, safe);
        if (!deleted)
            return false;
        this.mirrorTracker?.recordWrite(path_1.default.join(entityDir, 'attachments', safe));
        (0, file_mirror_1.mirrorAttachmentEvent)(entityDir, 'remove', safe);
        this.mirrorTracker?.recordWrite(path_1.default.join(entityDir, 'events.jsonl'));
        const attachments = (0, attachment_types_1.scanAttachments)(entityDir);
        this._graph.setNodeAttribute(skillId, 'attachments', attachments);
        this._graph.setNodeAttribute(skillId, 'updatedAt', Date.now());
        this.ctx.markDirty();
        this.ctx.emit('skill:attachment:deleted', { projectId: this.ctx.projectId, skillId, filename: safe });
        return true;
    }
    syncAttachments(skillId) {
        const dir = this.skillsDir;
        if (!dir)
            return;
        if (!this._graph.hasNode(skillId) || isProxy(this._graph, skillId))
            return;
        const attachments = (0, attachment_types_1.scanAttachments)(path_1.default.join(dir, skillId));
        this._graph.setNodeAttribute(skillId, 'attachments', attachments);
        this._graph.setNodeAttribute(skillId, 'updatedAt', Date.now());
        this._graph.setNodeAttribute(skillId, 'version', (this._graph.getNodeAttribute(skillId, 'version') ?? 0) + 1);
        this.ctx.markDirty();
    }
    listAttachments(skillId) {
        if (!this._graph.hasNode(skillId) || isProxy(this._graph, skillId))
            return [];
        return this._graph.getNodeAttribute(skillId, 'attachments') ?? [];
    }
    getAttachmentPath(skillId, filename) {
        const dir = this.skillsDir;
        if (!dir)
            return null;
        return (0, file_mirror_1.getAttachmentPath)(dir, skillId, 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.description}`);
        const now = Date.now();
        if (exists) {
            const existing = this._graph.getNodeAttributes(parsed.id);
            this._graph.mergeNodeAttributes(parsed.id, {
                title: parsed.title,
                description: parsed.description,
                steps: parsed.steps,
                triggers: parsed.triggers,
                inputHints: parsed.inputHints,
                filePatterns: parsed.filePatterns,
                tags: parsed.tags,
                source: parsed.source,
                confidence: parsed.confidence,
                usageCount: parsed.usageCount ?? existing.usageCount,
                lastUsedAt: parsed.lastUsedAt ?? existing.lastUsedAt,
                embedding,
                attachments: parsed.attachments,
                updatedAt: now,
                createdAt: existing.createdAt,
                version: parsed.version ?? existing.version + 1,
                ...(parsed.createdBy != null ? { createdBy: parsed.createdBy } : {}),
                ...(parsed.updatedBy != null ? { updatedBy: parsed.updatedBy } : {}),
            });
        }
        else {
            this._graph.addNode(parsed.id, {
                title: parsed.title,
                description: parsed.description,
                steps: parsed.steps,
                triggers: parsed.triggers,
                inputHints: parsed.inputHints,
                filePatterns: parsed.filePatterns,
                tags: parsed.tags,
                source: parsed.source,
                confidence: parsed.confidence,
                usageCount: parsed.usageCount ?? 0,
                lastUsedAt: parsed.lastUsedAt ?? null,
                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));
        this.syncRelationsFromFile(parsed.id, parsed.relations);
        this.ctx.markDirty();
        this.ctx.emit(exists ? 'skill:updated' : 'skill:created', { projectId: this.ctx.projectId, skillId: parsed.id });
    }
    updateDescriptionFromFile(skillId, description) {
        if (!this._graph.hasNode(skillId) || isProxy(this._graph, skillId))
            return;
        this._graph.setNodeAttribute(skillId, 'description', description);
        this._graph.setNodeAttribute(skillId, 'updatedAt', Date.now());
        this._graph.setNodeAttribute(skillId, 'version', (this._graph.getNodeAttribute(skillId, 'version') ?? 0) + 1);
        this.ctx.markDirty();
        this.ctx.emit('skill:updated', { projectId: this.ctx.projectId, skillId });
    }
    deleteFromFile(skillId) {
        if (!this._graph.hasNode(skillId))
            return;
        if (isProxy(this._graph, skillId))
            return;
        this._bm25Index.removeDocument(skillId);
        deleteSkill(this._graph, skillId);
        if (this.knowledgeGraph) {
            const pId = `@skills::${skillId}`;
            if (this.knowledgeGraph.hasNode(pId))
                this.knowledgeGraph.dropNode(pId);
        }
        if (this.taskGraph) {
            const pId = `@skills::${skillId}`;
            if (this.taskGraph.hasNode(pId))
                this.taskGraph.dropNode(pId);
        }
        this.ctx.markDirty();
        this.ctx.emit('skill:deleted', { projectId: this.ctx.projectId, skillId });
    }
    syncRelationsFromFile(skillId, desired) {
        const current = [];
        this._graph.forEachOutEdge(skillId, (_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, skillId, rel.graph, rel.to);
                if (rel.graph === 'knowledge' && this.knowledgeGraph) {
                    deleteMirrorFromKnowledgeGraph(this.knowledgeGraph, skillId, rel.to);
                }
                if (rel.graph === 'tasks' && this.taskGraph) {
                    deleteMirrorFromTaskGraph(this.taskGraph, skillId, rel.to);
                }
            }
            else {
                deleteSkillRelation(this._graph, skillId, 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, skillId, rel.graph, rel.to, rel.kind, extGraph);
                if (rel.graph === 'knowledge' && this.knowledgeGraph) {
                    createMirrorInKnowledgeGraph(this.knowledgeGraph, skillId, rel.to, rel.kind);
                }
                if (rel.graph === 'tasks' && this.taskGraph) {
                    createMirrorInTaskGraph(this.taskGraph, skillId, rel.to, rel.kind);
                }
            }
            else {
                createSkillRelation(this._graph, skillId, rel.to, rel.kind);
            }
        }
    }
    // -- Read --
    getSkill(skillId) {
        return getSkill(this._graph, skillId);
    }
    listSkills(opts) {
        return listSkills(this._graph, opts);
    }
    async searchSkills(query, opts) {
        const embedding = opts?.searchMode === 'keyword' ? [] : await this.embedFns.query(query);
        return (0, skills_1.searchSkills)(this._graph, embedding, { ...opts, queryText: query, bm25Index: this._bm25Index });
    }
    listRelations(skillId) {
        return listSkillRelations(this._graph, skillId, this.ext);
    }
    findLinkedSkills(targetGraph, targetNodeId, kind, projectId) {
        return findLinkedSkills(this._graph, targetGraph, targetNodeId, kind, projectId || this.ctx.projectId);
    }
}
exports.SkillGraphManager = SkillGraphManager;
