import {
  readEdgeConfidence
} from "./chunk-2LSZVONP.js";
import {
  emitGraphEvent
} from "./chunk-WIICJPET.js";

// src/graph.ts
import { mkdir, appendFile, readFile } from "fs/promises";
import * as path from "path";
var DEFAULT_GRAPH_TRAVERSAL_CONFIDENCE_FLOOR = 0.2;
var DEFAULT_GRAPH_TRAVERSAL_PAGERANK_ITERATIONS = 8;
var CAUSAL_PHRASES = [
  "as a result",
  "led to",
  "because of",
  "therefore",
  "caused",
  "because"
];
function graphsDir(memoryDir) {
  return path.join(memoryDir, "state", "graphs");
}
function graphFilePath(memoryDir, type) {
  return path.join(graphsDir(memoryDir), `${type}.jsonl`);
}
async function ensureGraphsDir(memoryDir) {
  await mkdir(graphsDir(memoryDir), { recursive: true });
}
var graphWriteLocks = /* @__PURE__ */ new Map();
function withGraphWriteLock(filePath, fn) {
  const prev = graphWriteLocks.get(filePath) ?? Promise.resolve();
  const next = prev.then(fn, fn);
  graphWriteLocks.set(
    filePath,
    next.then(
      () => void 0,
      () => void 0
    )
  );
  return next;
}
async function appendEdge(memoryDir, edge) {
  await ensureGraphsDir(memoryDir);
  const filePath = graphFilePath(memoryDir, edge.type);
  const line = JSON.stringify(edge) + "\n";
  await withGraphWriteLock(filePath, async () => {
    await appendFile(filePath, line, "utf8");
  });
  emitGraphEvent(memoryDir, "edge-added", {
    source: edge.from,
    target: edge.to,
    kind: edge.type,
    weight: edge.weight,
    label: edge.label,
    confidence: typeof edge.confidence === "number" ? edge.confidence : 1
  });
}
function isNodeError(err) {
  return typeof err === "object" && err !== null && "code" in err;
}
function parseEdgesJsonl(raw) {
  const edges = [];
  for (const line of raw.split("\n")) {
    const trimmed = line.trim();
    if (!trimmed) continue;
    try {
      edges.push(JSON.parse(trimmed));
    } catch {
    }
  }
  return edges;
}
async function readEdges(memoryDir, type) {
  const filePath = graphFilePath(memoryDir, type);
  try {
    const raw = await readFile(filePath, "utf8");
    return parseEdgesJsonl(raw);
  } catch {
    return [];
  }
}
async function readEdgesStrict(memoryDir, type) {
  const filePath = graphFilePath(memoryDir, type);
  try {
    const raw = await readFile(filePath, "utf8");
    return parseEdgesJsonl(raw);
  } catch (err) {
    if (isNodeError(err) && err.code === "ENOENT") {
      return [];
    }
    throw err;
  }
}
async function readAllEdges(memoryDir, config) {
  const parts = await Promise.all([
    config.entityGraphEnabled ? readEdges(memoryDir, "entity") : Promise.resolve([]),
    config.timeGraphEnabled ? readEdges(memoryDir, "time") : Promise.resolve([]),
    config.causalGraphEnabled ? readEdges(memoryDir, "causal") : Promise.resolve([])
  ]);
  return parts.flat();
}
function isValidGraphEdge(raw, expectedType) {
  if (!raw || typeof raw !== "object") return false;
  const edge = raw;
  return edge.type === expectedType && typeof edge.from === "string" && edge.from.length > 0 && typeof edge.to === "string" && edge.to.length > 0 && typeof edge.weight === "number" && Number.isFinite(edge.weight) && typeof edge.label === "string" && typeof edge.ts === "string";
}
async function analyzeGraphHealth(memoryDir, options) {
  const enabledTypes = [];
  if (options?.entityGraphEnabled !== false) enabledTypes.push("entity");
  if (options?.timeGraphEnabled !== false) enabledTypes.push("time");
  if (options?.causalGraphEnabled !== false) enabledTypes.push("causal");
  const files = [];
  const globalNodes = /* @__PURE__ */ new Set();
  for (const type of enabledTypes) {
    const filePath = graphFilePath(memoryDir, type);
    let exists = true;
    let totalLines = 0;
    let validEdges = 0;
    let corruptLines = 0;
    const nodes = /* @__PURE__ */ new Set();
    try {
      const raw = await readFile(filePath, "utf8");
      for (const line of raw.split("\n")) {
        const trimmed = line.trim();
        if (!trimmed) continue;
        totalLines += 1;
        try {
          const parsed = JSON.parse(trimmed);
          if (!isValidGraphEdge(parsed, type)) {
            corruptLines += 1;
            continue;
          }
          validEdges += 1;
          nodes.add(parsed.from);
          nodes.add(parsed.to);
          globalNodes.add(parsed.from);
          globalNodes.add(parsed.to);
        } catch {
          corruptLines += 1;
        }
      }
    } catch {
      exists = false;
    }
    files.push({
      type,
      filePath,
      exists,
      totalLines,
      validEdges,
      corruptLines,
      uniqueNodes: nodes.size
    });
  }
  const totals = files.reduce(
    (acc, item) => {
      acc.totalLines += item.totalLines;
      acc.validEdges += item.validEdges;
      acc.corruptLines += item.corruptLines;
      return acc;
    },
    {
      totalLines: 0,
      validEdges: 0,
      corruptLines: 0,
      uniqueNodes: globalNodes.size
    }
  );
  totals.uniqueNodes = globalNodes.size;
  const report = {
    generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
    enabledTypes,
    totals,
    files
  };
  if (options?.includeRepairGuidance === true) {
    const guidance = [];
    if (totals.corruptLines > 0) {
      guidance.push("Corrupt graph lines detected: back up memory/state/graphs, then rebuild graphs from clean memory replay/extraction runs.");
    }
    if (totals.validEdges === 0) {
      guidance.push("No valid edges detected yet: run normal extraction traffic (or replay ingestion) to seed graph files.");
    }
    if (guidance.length > 0) report.repairGuidance = guidance;
  }
  return report;
}
function detectCausalPhrase(text) {
  const lower = text.toLowerCase();
  for (const phrase of CAUSAL_PHRASES) {
    if (lower.includes(phrase)) return phrase;
  }
  return null;
}
var GraphIndex = class _GraphIndex {
  memoryDir;
  cfg;
  // Cache for readAllEdges() result.  With 30k+ entity edges (6 MB JSONL) the
  // file read + JSON parse takes 2-4 s per call.  This instance-level cache
  // eliminates that overhead on every spreadingActivation() call; it is
  // invalidated (set to null) in onMemoryWritten() so new edges appear promptly.
  edgeCache = null;
  static EDGE_CACHE_TTL_MS = 3e5;
  // 5 minutes
  constructor(memoryDir, cfg) {
    this.memoryDir = memoryDir;
    this.cfg = cfg;
  }
  /** Clear the edge cache so the next spreadingActivation() re-reads from disk.
   *  Call after any code path that appends edges outside of onMemoryWritten(). */
  invalidateEdgeCache() {
    this.edgeCache = null;
  }
  async loadEdgesCached() {
    if (this.edgeCache && Date.now() - this.edgeCache.loadedAt < _GraphIndex.EDGE_CACHE_TTL_MS) {
      return this.edgeCache.allEdges;
    }
    const allEdges = await readAllEdges(this.memoryDir, {
      entityGraphEnabled: this.cfg.entityGraphEnabled,
      timeGraphEnabled: this.cfg.timeGraphEnabled,
      causalGraphEnabled: this.cfg.causalGraphEnabled
    });
    this.edgeCache = { allEdges, loadedAt: Date.now() };
    return allEdges;
  }
  /**
   * Called after a memory is written to disk.
   *
   * @param memoryPath - relative path from memoryDir (e.g. "facts/2026-02-22/abc.md")
   * @param entityRef  - entityRef frontmatter field (if any)
   * @param content    - full memory text (for causal detection)
   * @param created    - ISO timestamp of this memory
   * @param threadId   - current thread ID (for time graph)
   * @param recentInThread - paths of the N most-recent memories in this thread (for time graph)
   * @param entitySiblings - paths of other memories that share the same entityRef (for entity graph)
   */
  async onMemoryWritten(opts) {
    if (!this.cfg.multiGraphMemoryEnabled) return;
    const ts = (/* @__PURE__ */ new Date()).toISOString();
    try {
      if (this.cfg.entityGraphEnabled && opts.entityRef && opts.entitySiblings?.length) {
        const siblings = opts.entitySiblings.slice(0, this.cfg.maxEntityGraphEdgesPerMemory);
        for (const sibling of siblings) {
          await appendEdge(this.memoryDir, {
            from: opts.memoryPath,
            to: sibling,
            type: "entity",
            weight: 1,
            label: opts.entityRef,
            ts
          });
        }
      }
      if (this.cfg.timeGraphEnabled && opts.threadId && opts.recentInThread?.length) {
        const predecessor = opts.recentInThread[opts.recentInThread.length - 1];
        if (predecessor && predecessor !== opts.memoryPath) {
          await appendEdge(this.memoryDir, {
            from: predecessor,
            to: opts.memoryPath,
            type: "time",
            weight: 1,
            label: opts.threadId,
            ts
          });
        }
      }
      if (this.cfg.causalGraphEnabled && opts.causalPredecessor) {
        const phrase = detectCausalPhrase(opts.content);
        if (phrase) {
          await appendEdge(this.memoryDir, {
            from: opts.causalPredecessor,
            to: opts.memoryPath,
            type: "causal",
            weight: 1,
            label: phrase,
            ts
          });
        }
      }
    } catch (err) {
      const { log } = await import("./logger.js");
      log.warn(`[graph] onMemoryWritten error: ${err}`);
    } finally {
      this.edgeCache = null;
    }
  }
  /**
   * Spreading activation BFS (SYNAPSE-inspired).
   *
   * Starting from `seeds`, traverse the combined graph for up to `maxSteps` hops.
   * Each candidate gets an activation score = edge.weight × edgeConfidence × decay^hop.
   *
   * Issue #681 PR 3/3 — confidence-aware traversal:
   *   - Each edge's `weight` is multiplied by its `confidence` (legacy edges
   *     missing `confidence` are treated as 1.0, preserving prior behavior).
   *   - Edges with `confidence < graphTraversalConfidenceFloor` are pruned and
   *     contribute neither activation nor downstream neighbors.
   *   - When `graphTraversalPageRankIterations > 0`, an additional PageRank-
   *     style refinement pass redistributes activation along confidence-weighted
   *     edges, sharpening the ranking among multi-hop candidates.
   *   - Per-result provenance includes the highest-confidence edge that landed
   *     on each candidate, so the X-ray surface can attribute pruning and
   *     ranking decisions back to specific edges.
   *
   * @param seeds    - initial memory paths to expand from (e.g. QMD top results)
   * @param maxSteps - max BFS hops (from config: maxGraphTraversalSteps)
   * @returns Array of {path, score, edgeConfidence, ...} sorted descending, not including seed paths
   */
  async spreadingActivation(seeds, maxSteps, opts) {
    if (!this.cfg.multiGraphMemoryEnabled) return [];
    const steps = maxSteps ?? this.cfg.maxGraphTraversalSteps;
    const decay = this.cfg.graphActivationDecay;
    const floor = opts?.includeLowConfidence === true ? 0 : clampConfidenceFloor(this.cfg.graphTraversalConfidenceFloor);
    const iterations = clampPageRankIterations(
      this.cfg.graphTraversalPageRankIterations
    );
    try {
      const allEdges = await this.loadEdgesCached();
      const adj = /* @__PURE__ */ new Map();
      for (const edge of allEdges) {
        const conf = readEdgeConfidence(edge);
        if (conf < floor) continue;
        if (!adj.has(edge.from)) adj.set(edge.from, []);
        adj.get(edge.from).push(edge);
        if (edge.type !== "causal") {
          if (!adj.has(edge.to)) adj.set(edge.to, []);
          adj.get(edge.to).push({ ...edge, from: edge.to, to: edge.from });
        }
      }
      const seedSet = new Set(seeds);
      const scores = /* @__PURE__ */ new Map();
      const provenance = /* @__PURE__ */ new Map();
      const visited = new Set(seeds);
      const queue = seeds.map((s) => [s, 0, s]);
      while (queue.length > 0) {
        const [node, hop, sourceSeed] = queue.shift();
        if (hop >= steps) continue;
        const edges = adj.get(node) ?? [];
        for (const edge of edges) {
          const neighbor = edge.to === node ? edge.from : edge.to;
          const conf = readEdgeConfidence(edge);
          if (conf < floor) continue;
          const score = edge.weight * conf * Math.pow(decay, hop + 1);
          if (!seedSet.has(neighbor)) {
            const existing = scores.get(neighbor) ?? 0;
            scores.set(neighbor, existing + score);
            const prev = provenance.get(neighbor);
            if (!prev || hop + 1 < prev.hopDepth || hop + 1 === prev.hopDepth && score > prev.decayedWeight) {
              provenance.set(neighbor, {
                seed: sourceSeed,
                hopDepth: hop + 1,
                decayedWeight: score,
                graphType: edge.type,
                edgeConfidence: conf
              });
            }
          }
          if (!visited.has(neighbor)) {
            visited.add(neighbor);
            queue.push([neighbor, hop + 1, sourceSeed]);
          }
        }
      }
      if (iterations > 0 && scores.size > 1) {
        applyPageRankRefinement(scores, adj, {
          iterations,
          floor,
          damping: 0.85
        });
      }
      if (this.cfg.graphLateralInhibitionEnabled && scores.size > 1) {
        const inhibited = applyLateralInhibition(scores, {
          beta: this.cfg.graphLateralInhibitionBeta,
          topM: this.cfg.graphLateralInhibitionTopM
        });
        for (const [k, v] of inhibited) {
          scores.set(k, v);
        }
      }
      return Array.from(scores.entries()).map(([p, score]) => ({
        path: p,
        score,
        seed: provenance.get(p)?.seed ?? "",
        hopDepth: provenance.get(p)?.hopDepth ?? 0,
        decayedWeight: provenance.get(p)?.decayedWeight ?? 0,
        graphType: provenance.get(p)?.graphType ?? "entity",
        edgeConfidence: provenance.get(p)?.edgeConfidence ?? 1
      })).sort((a, b) => b.score - a.score);
    } catch (err) {
      const { log } = await import("./logger.js");
      log.warn(`[graph] spreadingActivation error: ${err}`);
      return [];
    }
  }
};
function clampConfidenceFloor(raw) {
  if (typeof raw !== "number" || !Number.isFinite(raw)) {
    return DEFAULT_GRAPH_TRAVERSAL_CONFIDENCE_FLOOR;
  }
  if (raw < 0) return 0;
  if (raw > 1) return 1;
  return raw;
}
function clampPageRankIterations(raw) {
  if (typeof raw !== "number" || !Number.isFinite(raw)) return 0;
  if (raw <= 0) return 0;
  return Math.floor(raw);
}
function applyPageRankRefinement(scores, adj, opts) {
  const { iterations, floor, damping } = opts;
  if (iterations <= 0 || scores.size === 0) return;
  const safeDamping = Math.min(1, Math.max(0, damping));
  const eligible = (edge, fromNode) => {
    if (readEdgeConfidence(edge) < floor) return false;
    const neighbor = edge.to === fromNode ? edge.from : edge.to;
    return scores.has(neighbor);
  };
  const outboundTotal = /* @__PURE__ */ new Map();
  for (const [node, edges] of adj.entries()) {
    if (!scores.has(node)) continue;
    let sum = 0;
    for (const edge of edges) {
      if (!eligible(edge, node)) continue;
      sum += readEdgeConfidence(edge) * edge.weight;
    }
    if (sum > 0) outboundTotal.set(node, sum);
  }
  for (let i = 0; i < iterations; i += 1) {
    const next = /* @__PURE__ */ new Map();
    for (const [node, score] of scores) {
      next.set(node, (1 - safeDamping) * score);
    }
    for (const [node, score] of scores) {
      const outEdges = adj.get(node);
      const total = outboundTotal.get(node);
      if (!outEdges || outEdges.length === 0 || !total || total <= 0) {
        next.set(node, (next.get(node) ?? 0) + safeDamping * score);
        continue;
      }
      for (const edge of outEdges) {
        if (!eligible(edge, node)) continue;
        const conf = readEdgeConfidence(edge);
        const neighbor = edge.to === node ? edge.from : edge.to;
        const flow = safeDamping * score * (conf * edge.weight / total);
        next.set(neighbor, (next.get(neighbor) ?? 0) + flow);
      }
    }
    for (const [node, score] of next) {
      scores.set(node, score);
    }
  }
}
function applyLateralInhibition(scores, opts) {
  const { beta, topM } = opts;
  if (beta === 0 || topM === 0) return new Map(scores);
  const sorted = Array.from(scores.entries()).sort((a, b) => b[1] - a[1]);
  const topCompetitors = sorted.slice(0, topM);
  const result = /* @__PURE__ */ new Map();
  for (const [node, u] of scores) {
    let inhibition = 0;
    for (const [, uK] of topCompetitors) {
      if (uK > u) {
        inhibition += uK - u;
      }
    }
    result.set(node, Math.max(0, u - beta * inhibition));
  }
  return result;
}

export {
  DEFAULT_GRAPH_TRAVERSAL_CONFIDENCE_FLOOR,
  DEFAULT_GRAPH_TRAVERSAL_PAGERANK_ITERATIONS,
  CAUSAL_PHRASES,
  graphsDir,
  graphFilePath,
  ensureGraphsDir,
  withGraphWriteLock,
  appendEdge,
  readEdges,
  readEdgesStrict,
  readAllEdges,
  analyzeGraphHealth,
  detectCausalPhrase,
  GraphIndex,
  clampConfidenceFloor,
  clampPageRankIterations,
  applyPageRankRefinement,
  applyLateralInhibition
};
//# sourceMappingURL=chunk-RK2Y4XOM.js.map