"use strict";
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2024-2026 Gracker (Chris)
// This file is part of SmartPerfetto. See LICENSE for details.
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.CaseGraph = void 0;
/**
 * CaseGraph — directional edges between case nodes (Plan 54 M1).
 *
 * Edges live in their own store (`case_graph.json`) separate from
 * `case_library.json`. The `CaseGraphLibraryContract` joins both at
 * export time when the public bundle is generated; at runtime the two
 * services have independent CRUD lifecycles, so archiving a case does
 * not require an edge sweep and vice versa.
 *
 * Invariants enforced at insert time:
 * - No self-loops: `fromCaseId !== toCaseId` (a case cannot relate to
 *   itself in this graph; structural similarity is a content-level
 *   concern).
 * - Deduplication on the canonical key `(fromCaseId, toCaseId,
 *   relation)`. Re-inserting the same triplet replaces weight + note
 *   in place rather than adding a parallel edge.
 *
 * Foreign-key correctness against `caseLibrary.ts` is intentionally
 * NOT enforced. An edge can reference a case that does not exist (yet)
 * because the case is being archived, or because edges are imported
 * from another bundle ahead of the cases. Traversal callers handle
 * orphan edges by joining with the case library and reporting the
 * absent node — same pattern as `traceUnavailableReason` for archived
 * cases.
 *
 * Out of scope:
 * - MCP tools — `recall_similar_case`, `cite_case_in_report` land
 *   in Plan 54 M2.
 *
 * @module caseGraph
 */
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const scopedKnowledgeStore_1 = require("./scopedKnowledgeStore");
const KNOWLEDGE_KIND = 'case_edge';
const CASE_EDGE_ROW_SCOPE_PREFIX = 'case_edge:';
/** Build the canonical dedup key for an edge. */
function edgeKey(edge) {
    return `${edge.fromCaseId}|${edge.toCaseId}|${edge.relation}`;
}
/**
 * CaseGraph — local file-backed edge storage. Same persistence
 * contract as ragStore / baselineStore / projectMemory / caseLibrary.
 */
class CaseGraph {
    constructor(storagePath) {
        /** Map from canonical edge key to the stored edge. */
        this.edges = new Map();
        this.loaded = false;
        this.storagePath = storagePath;
    }
    load() {
        if (this.loaded)
            return;
        this.loaded = true;
        if (!fs.existsSync(this.storagePath))
            return;
        try {
            const raw = fs.readFileSync(this.storagePath, 'utf-8');
            const parsed = JSON.parse(raw);
            if (parsed.schemaVersion !== 1 || !Array.isArray(parsed.edges))
                return;
            for (const e of parsed.edges)
                this.edges.set(edgeKey(e), e);
        }
        catch {
            // Corrupted JSON: file preserved, in-memory cache stays empty.
        }
    }
    /**
     * Add or replace an edge. Throws on self-loop. Re-inserting the
     * same `(from, to, relation)` triplet replaces weight + note +
     * edgeId (so callers can keep the latest curator note).
     */
    addEdge(edge, scope) {
        this.load();
        if (edge.fromCaseId === edge.toCaseId) {
            throw new Error(`Self-loops are not permitted: edge '${edge.edgeId}' has fromCaseId === toCaseId === '${edge.fromCaseId}'`);
        }
        if ((0, scopedKnowledgeStore_1.legacyKnowledgeFilesystemWritesEnabled)()) {
            this.edges.set(edgeKey(edge), edge);
            this.persist();
        }
        if ((0, scopedKnowledgeStore_1.enterpriseKnowledgeDbWritesEnabled)()) {
            for (const existing of this.listEnterpriseEdges(scope)) {
                if (edgeKey(existing.record) === edgeKey(edge) &&
                    existing.record.edgeId !== edge.edgeId) {
                    (0, scopedKnowledgeStore_1.removeScopedKnowledgeRecord)(KNOWLEDGE_KIND, existing.record.edgeId, scope);
                }
            }
            (0, scopedKnowledgeStore_1.upsertScopedKnowledgeRecord)(KNOWLEDGE_KIND, edge.edgeId, caseEdgeRowScope(edge.relation), edge, scope);
        }
    }
    /** Remove an edge by canonical id. Returns whether it was present. */
    removeEdge(edgeId, scope) {
        let removed = false;
        if ((0, scopedKnowledgeStore_1.enterpriseKnowledgeDbWritesEnabled)()) {
            removed = (0, scopedKnowledgeStore_1.removeScopedKnowledgeRecord)(KNOWLEDGE_KIND, edgeId, scope) || removed;
        }
        if ((0, scopedKnowledgeStore_1.legacyKnowledgeFilesystemWritesEnabled)()) {
            this.load();
            let foundKey;
            for (const [k, e] of this.edges) {
                if (e.edgeId === edgeId) {
                    foundKey = k;
                    break;
                }
            }
            if (foundKey) {
                this.edges.delete(foundKey);
                this.persist();
                removed = true;
            }
        }
        return removed;
    }
    /** Get all edges originating at the case. */
    getEdgesFrom(caseId, scope) {
        if ((0, scopedKnowledgeStore_1.enterpriseKnowledgeStoreEnabled)()) {
            return this.listEnterpriseEdges(scope)
                .map(row => row.record)
                .filter(e => e.fromCaseId === caseId)
                .sort((a, b) => a.edgeId.localeCompare(b.edgeId));
        }
        this.load();
        return Array.from(this.edges.values())
            .filter(e => e.fromCaseId === caseId)
            .sort((a, b) => a.edgeId.localeCompare(b.edgeId));
    }
    /** Get all edges pointing at the case. */
    getEdgesTo(caseId, scope) {
        if ((0, scopedKnowledgeStore_1.enterpriseKnowledgeStoreEnabled)()) {
            return this.listEnterpriseEdges(scope)
                .map(row => row.record)
                .filter(e => e.toCaseId === caseId)
                .sort((a, b) => a.edgeId.localeCompare(b.edgeId));
        }
        this.load();
        return Array.from(this.edges.values())
            .filter(e => e.toCaseId === caseId)
            .sort((a, b) => a.edgeId.localeCompare(b.edgeId));
    }
    /**
     * Single-hop traversal. Returns related case ids paired with the
     * connecting edge, ordered by edge weight descending (unweighted
     * edges sort to the back). Optional relation filter narrows the
     * set; `direction` controls in/out/both.
     */
    findRelated(caseId, opts = {}) {
        this.load();
        const direction = opts.direction ?? 'both';
        const relations = opts.relations ? new Set(opts.relations) : null;
        const topK = opts.topK ?? 10;
        const candidates = [];
        const edges = (0, scopedKnowledgeStore_1.enterpriseKnowledgeStoreEnabled)()
            ? this.listEnterpriseEdges(opts.knowledgeScope).map(row => row.record)
            : Array.from(this.edges.values());
        for (const e of edges) {
            if (relations && !relations.has(e.relation))
                continue;
            if ((direction === 'out' || direction === 'both') &&
                e.fromCaseId === caseId &&
                e.toCaseId !== caseId) {
                candidates.push({ caseId: e.toCaseId, edge: e });
            }
            if ((direction === 'in' || direction === 'both') &&
                e.toCaseId === caseId &&
                e.fromCaseId !== caseId) {
                candidates.push({ caseId: e.fromCaseId, edge: e });
            }
        }
        candidates.sort((a, b) => {
            const wa = a.edge.weight ?? -1;
            const wb = b.edge.weight ?? -1;
            if (wa !== wb)
                return wb - wa;
            return a.edge.edgeId.localeCompare(b.edge.edgeId);
        });
        return candidates.slice(0, topK);
    }
    /** All edges, deterministically ordered by canonical key. Used by
     * the export bundler that joins this with the case library. */
    listEdges(scope) {
        if ((0, scopedKnowledgeStore_1.enterpriseKnowledgeStoreEnabled)()) {
            return this.listEnterpriseEdges(scope)
                .map(row => row.record)
                .sort((a, b) => edgeKey(a).localeCompare(edgeKey(b)));
        }
        this.load();
        return Array.from(this.edges.values()).sort((a, b) => edgeKey(a).localeCompare(edgeKey(b)));
    }
    /** Total edge count. */
    size(scope) {
        if ((0, scopedKnowledgeStore_1.enterpriseKnowledgeStoreEnabled)()) {
            return this.listEnterpriseEdges(scope).length;
        }
        this.load();
        return this.edges.size;
    }
    persist() {
        const dir = path.dirname(this.storagePath);
        if (!fs.existsSync(dir))
            fs.mkdirSync(dir, { recursive: true });
        // Per-process unique tmp suffix — Codex round E P1#5.
        const tmp = `${this.storagePath}.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`;
        const envelope = {
            schemaVersion: 1,
            edges: Array.from(this.edges.values()),
        };
        fs.writeFileSync(tmp, JSON.stringify(envelope, null, 2), 'utf-8');
        fs.renameSync(tmp, this.storagePath);
    }
    listEnterpriseEdges(scope) {
        return (0, scopedKnowledgeStore_1.listScopedKnowledgeRecords)(KNOWLEDGE_KIND, scope, { rowScopePrefix: CASE_EDGE_ROW_SCOPE_PREFIX });
    }
}
exports.CaseGraph = CaseGraph;
function caseEdgeRowScope(relation) {
    return `${CASE_EDGE_ROW_SCOPE_PREFIX}${relation}`;
}
//# sourceMappingURL=caseGraph.js.map