import {
  computePairId,
  isCoolingDown,
  listPairs,
  writePairs
} from "./chunk-YNB73F22.js";
import {
  extractJsonCandidates
} from "./chunk-UZB5KHKX.js";
import {
  log
} from "./chunk-2ODBA7MQ.js";
import "./chunk-PZ5AY32C.js";

// src/contradiction/contradiction-judge.ts
import { createHash } from "crypto";
var CONTRADICTION_JUDGE_PROMPT = `You are a memory contradiction classifier. You will receive pairs of stored memories and must classify their semantic relationship.

For each pair, respond with a JSON array where each element has:
- "pairKey": the pairKey provided in the input
- "verdict": one of "contradicts", "independent", "duplicates", "needs-user"
- "rationale": one sentence explaining why
- "confidence": number between 0 and 1

VERDICT DEFINITIONS:
- "contradicts": The two memories make claims that cannot both be true. One must be wrong or outdated.
- "duplicates": The two memories convey essentially the same information (near-paraphrase).
- "independent": The memories are topically similar but do not conflict or duplicate.
- "needs-user": Cannot determine with sufficient confidence; requires human review.

IMPORTANT:
- Be conservative. When in doubt, prefer "needs-user" over a wrong classification.
- Two memories about the same entity/topic are NOT necessarily contradictory.
- Temporal changes ("Joshua uses pnpm" vs "Joshua switched to npm") ARE contradictions.
- Different aspects of the same entity ("Joshua uses pnpm" vs "Joshua works on Remnic") are "independent".`;
var defaultVerdictCache = /* @__PURE__ */ new Map();
var CACHE_MAX = 1e4;
function pairKey(idA, idB) {
  const sorted = [idA, idB].sort();
  return `${sorted[0]}:${sorted[1]}`;
}
function contentHash(a) {
  const sides = [
    [a.textA.trim(), (a.categoryA ?? "").trim()].join("|"),
    [a.textB.trim(), (a.categoryB ?? "").trim()].join("|")
  ].sort();
  const normalized = sides.join("|||");
  return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
}
async function judgeContradictionPairs(pairs, config, localLlm, fallbackLlm, cache) {
  const startTime = Date.now();
  const results = /* @__PURE__ */ new Map();
  const activeCache = cache ?? defaultVerdictCache;
  let cached = 0;
  let judged = 0;
  const toJudge = [];
  for (const pair of pairs) {
    const key = pairKey(pair.memoryIdA, pair.memoryIdB);
    const hash = contentHash(pair);
    const cachedResult = activeCache.get(hash);
    if (cachedResult) {
      results.set(key, { ...cachedResult, memoryIdA: pair.memoryIdA, memoryIdB: pair.memoryIdB });
      cached++;
    } else {
      toJudge.push(pair);
    }
  }
  if (toJudge.length === 0) {
    return { results, cached, judged, elapsed: Date.now() - startTime };
  }
  const pairDescriptions = toJudge.map((p, i) => {
    const pk = pairKey(p.memoryIdA, p.memoryIdB);
    const catA = p.categoryA ? ` [${p.categoryA}]` : "";
    const catB = p.categoryB ? ` [${p.categoryB}]` : "";
    return `Pair ${i + 1} (pairKey: "${pk}"):${catA} "${p.textA}"${catB} "${p.textB}"`;
  });
  const userMessage = `Classify these ${toJudge.length} memory pair(s):

${pairDescriptions.join("\n\n")}`;
  let llmResponse = null;
  if (localLlm) {
    try {
      llmResponse = await callLlm(localLlm, config, userMessage);
    } catch (err) {
      log.warn("[contradiction-judge] local LLM call failed: %s", err instanceof Error ? err.message : err);
    }
  }
  if (!llmResponse && fallbackLlm) {
    try {
      llmResponse = await callLlm(fallbackLlm, config, userMessage);
    } catch (err) {
      log.warn("[contradiction-judge] fallback LLM call failed: %s", err instanceof Error ? err.message : err);
    }
  }
  if (llmResponse) {
    const candidates = extractJsonCandidates(llmResponse);
    const parsed = parseJudgeResponse(candidates, toJudge);
    for (const result of parsed) {
      const key = pairKey(result.memoryIdA, result.memoryIdB);
      results.set(key, result);
      const input = toJudge.find(
        (p) => pairKey(p.memoryIdA, p.memoryIdB) === key
      );
      if (input) {
        const hash = contentHash(input);
        if (activeCache.size >= CACHE_MAX) {
          const firstKey = activeCache.keys().next().value;
          if (firstKey !== void 0) activeCache.delete(firstKey);
        }
        activeCache.set(hash, result);
      }
      judged++;
    }
  } else {
    for (const pair of toJudge) {
      const key = pairKey(pair.memoryIdA, pair.memoryIdB);
      const result = {
        memoryIdA: pair.memoryIdA,
        memoryIdB: pair.memoryIdB,
        verdict: "needs-user",
        rationale: "LLM call failed; requires manual review",
        confidence: 0
      };
      results.set(key, result);
      judged++;
    }
  }
  return { results, cached, judged, elapsed: Date.now() - startTime };
}
async function callLlm(client, config, userMessage) {
  const messages = [
    { role: "system", content: CONTRADICTION_JUDGE_PROMPT },
    { role: "user", content: userMessage }
  ];
  if ("chatCompletion" in client && typeof client.chatCompletion === "function") {
    const result = await client.chatCompletion(messages, {
      temperature: 0.1,
      maxTokens: 4096
    });
    return result?.content ?? "";
  }
  if ("complete" in client && typeof client.complete === "function") {
    const result = await client.complete(messages);
    return result.content ?? "";
  }
  return "";
}
function parseJudgeResponse(candidates, inputs) {
  const VALID_VERDICTS = ["contradicts", "independent", "duplicates", "needs-user"];
  for (const candidate of candidates) {
    try {
      const parsed = JSON.parse(candidate);
      const items = Array.isArray(parsed) ? parsed : [parsed];
      const results = [];
      const matchedKeys = /* @__PURE__ */ new Set();
      for (const item of items) {
        if (!item || typeof item !== "object") continue;
        const verdict = typeof item.verdict === "string" && VALID_VERDICTS.includes(item.verdict) ? item.verdict : "needs-user";
        const pairKeyVal = typeof item.pairKey === "string" ? item.pairKey : null;
        const input = pairKeyVal ? inputs.find((p) => pairKey(p.memoryIdA, p.memoryIdB) === pairKeyVal) : null;
        const fallbackInput = input ?? inputs[results.length] ?? inputs[0];
        if (!fallbackInput) continue;
        matchedKeys.add(pairKey(fallbackInput.memoryIdA, fallbackInput.memoryIdB));
        const confidence = typeof item.confidence === "number" ? Math.min(1, Math.max(0, item.confidence)) : 0.5;
        results.push({
          memoryIdA: fallbackInput.memoryIdA,
          memoryIdB: fallbackInput.memoryIdB,
          verdict,
          rationale: typeof item.rationale === "string" ? item.rationale : "No rationale provided",
          confidence
        });
      }
      for (const inp of inputs) {
        const key = pairKey(inp.memoryIdA, inp.memoryIdB);
        if (!matchedKeys.has(key)) {
          results.push({
            memoryIdA: inp.memoryIdA,
            memoryIdB: inp.memoryIdB,
            verdict: "needs-user",
            rationale: "LLM response omitted this pair",
            confidence: 0
          });
        }
      }
      if (results.length > 0) return results;
    } catch {
      continue;
    }
  }
  return inputs.map((p) => ({
    memoryIdA: p.memoryIdA,
    memoryIdB: p.memoryIdB,
    verdict: "needs-user",
    rationale: "Failed to parse judge response",
    confidence: 0
  }));
}

// src/contradiction/contradiction-scan.ts
var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["active"]);
var SCAN_CATEGORIES = /* @__PURE__ */ new Set([
  "decision",
  "principle",
  "rule",
  "entity",
  "fact",
  "preference"
]);
async function runContradictionScan(deps) {
  const startTime = Date.now();
  const { storage, config, memoryDir, embeddingLookup, embeddingLookupFactory, localLlm, fallbackLlm, namespace } = deps;
  const scanConfig = config.contradictionScan;
  const scopedEmbeddingLookup = embeddingLookupFactory ? embeddingLookupFactory(storage) : embeddingLookup;
  if (!scanConfig.enabled) {
    log.info("[contradiction-scan] disabled by config");
    return { scanned: 0, candidates: 0, judged: 0, queued: 0, cooledDown: 0, elapsedMs: 0 };
  }
  const memories = await loadEligibleMemories(storage, namespace);
  log.info("[contradiction-scan] loaded %d eligible memories", memories.length);
  if (memories.length < 2) {
    return { scanned: memories.length, candidates: 0, judged: 0, queued: 0, cooledDown: 0, elapsedMs: Date.now() - startTime };
  }
  const existingPairs = listPairs(memoryDir, { filter: "all", namespace, limit: 1e4 }).pairs;
  const existingMap = /* @__PURE__ */ new Map();
  for (const p of existingPairs) {
    existingMap.set(p.pairId, p);
  }
  const candidates = await generatePairs(memories, existingMap, scanConfig, scopedEmbeddingLookup);
  const cooledDown = candidates.skipped;
  log.info("[contradiction-scan] generated %d candidates (%d cooled down)", candidates.pairs.length, cooledDown);
  if (candidates.pairs.length === 0) {
    return {
      scanned: memories.length,
      candidates: 0,
      judged: 0,
      queued: 0,
      cooledDown,
      elapsedMs: Date.now() - startTime
    };
  }
  const capped = candidates.pairs.sort((a, b) => computePairId(a.idA, a.idB).localeCompare(computePairId(b.idA, b.idB))).slice(0, scanConfig.maxPairsPerRun);
  const judgeInputs = capped.map((pair) => ({
    memoryIdA: pair.idA,
    memoryIdB: pair.idB,
    textA: pair.textA,
    textB: pair.textB,
    categoryA: pair.categoryA,
    categoryB: pair.categoryB
  }));
  const scanCache = /* @__PURE__ */ new Map();
  const judgeResult = await judgeContradictionPairs(judgeInputs, config, localLlm, fallbackLlm, scanCache);
  log.info("[contradiction-scan] judge completed: %d judged, %d cached in %dms", judgeResult.judged, judgeResult.cached, judgeResult.elapsed);
  const queueEntries = [];
  for (const [key, result] of judgeResult.results) {
    queueEntries.push({
      memoryIds: [result.memoryIdA, result.memoryIdB],
      verdict: result.verdict,
      rationale: result.rationale,
      confidence: result.confidence,
      detectedAt: (/* @__PURE__ */ new Date()).toISOString(),
      // Set lastReviewedAt for non-actionable verdicts so cooldown prevents re-judging
      lastReviewedAt: result.verdict === "independent" ? (/* @__PURE__ */ new Date()).toISOString() : void 0,
      namespace
    });
  }
  const written = writePairs(memoryDir, queueEntries);
  const elapsed = Date.now() - startTime;
  log.info("[contradiction-scan] complete: %d queued in %dms", written.length, elapsed);
  return {
    scanned: memories.length,
    candidates: candidates.pairs.length,
    judged: judgeResult.judged,
    queued: written.length,
    cooledDown,
    elapsedMs: elapsed
  };
}
async function generatePairs(memories, existingPairs, scanConfig, embeddingLookup) {
  const pairs = [];
  const embeddingPairs = [];
  let skipped = 0;
  const seen = /* @__PURE__ */ new Set();
  const byEntity = /* @__PURE__ */ new Map();
  for (const mem of memories) {
    const entity = mem.frontmatter.entityRef;
    if (entity) {
      const existing = byEntity.get(entity) ?? [];
      existing.push(mem);
      byEntity.set(entity, existing);
    }
  }
  for (const [, group] of byEntity) {
    if (group.length < 2) continue;
    for (let i = 0; i < group.length; i++) {
      for (let j = i + 1; j < group.length; j++) {
        const a = group[i];
        const b = group[j];
        const pairId = computePairId(a.frontmatter.id, b.frontmatter.id);
        if (seen.has(pairId)) continue;
        seen.add(pairId);
        const existing = existingPairs.get(pairId);
        if (existing && isCoolingDown(existing, scanConfig.cooldownDays)) {
          skipped++;
          continue;
        }
        pairs.push({
          idA: a.frontmatter.id,
          idB: b.frontmatter.id,
          textA: a.content,
          textB: b.content,
          categoryA: a.frontmatter.category,
          categoryB: b.frontmatter.category
        });
      }
    }
  }
  const noEntity = memories.filter((m) => !m.frontmatter.entityRef);
  for (let i = 0; i < noEntity.length; i++) {
    for (let j = i + 1; j < noEntity.length; j++) {
      const a = noEntity[i];
      const b = noEntity[j];
      const overlap = jaccardOverlap(
        a.frontmatter.tags ?? [],
        b.frontmatter.tags ?? []
      );
      if (overlap < scanConfig.topicOverlapFloor) continue;
      const pairId = computePairId(a.frontmatter.id, b.frontmatter.id);
      if (seen.has(pairId)) continue;
      seen.add(pairId);
      const existing = existingPairs.get(pairId);
      if (existing && isCoolingDown(existing, scanConfig.cooldownDays)) {
        skipped++;
        continue;
      }
      pairs.push({
        idA: a.frontmatter.id,
        idB: b.frontmatter.id,
        textA: a.content,
        textB: b.content,
        categoryA: a.frontmatter.category,
        categoryB: b.frontmatter.category
      });
    }
  }
  if (embeddingLookup) {
    const memoryById = new Map(memories.map((m) => [m.frontmatter.id, m]));
    for (const mem of memories) {
      const id = mem.frontmatter.id;
      try {
        const hits = await embeddingLookup(mem.content, 20);
        for (const hit of hits) {
          if (hit.score < scanConfig.similarityFloor) continue;
          if (hit.id === id) continue;
          const peer = memoryById.get(hit.id);
          if (!peer) continue;
          const pairId = computePairId(id, hit.id);
          if (seen.has(pairId)) continue;
          seen.add(pairId);
          const existing = existingPairs.get(pairId);
          if (existing && isCoolingDown(existing, scanConfig.cooldownDays)) {
            skipped++;
            continue;
          }
          embeddingPairs.push({
            idA: id,
            idB: hit.id,
            textA: mem.content,
            textB: peer.content,
            categoryA: mem.frontmatter.category,
            categoryB: peer.frontmatter.category
          });
        }
      } catch {
      }
    }
  }
  pairs.push(...embeddingPairs);
  return { pairs, skipped };
}
async function loadEligibleMemories(storage, namespace) {
  let all;
  try {
    all = await storage.readAllMemories();
  } catch {
    return [];
  }
  return all.filter((mem) => {
    const fm = mem.frontmatter;
    const status = fm.status ?? "active";
    if (!ACTIVE_STATUSES.has(status)) return false;
    const category = fm.category;
    if (category && !SCAN_CATEGORIES.has(category)) return false;
    if (!mem.content || mem.content.trim().length === 0) return false;
    if (!fm.id) return false;
    return true;
  });
}
function jaccardOverlap(a, b) {
  if (a.length === 0 && b.length === 0) return 0;
  const setA = new Set(a.map((t) => t.toLowerCase()));
  const setB = new Set(b.map((t) => t.toLowerCase()));
  let intersection = 0;
  for (const item of setA) {
    if (setB.has(item)) intersection++;
  }
  const union = setA.size + setB.size - intersection;
  return union === 0 ? 0 : intersection / union;
}
export {
  ACTIVE_STATUSES,
  runContradictionScan
};
//# sourceMappingURL=contradiction-scan-3Z6YW7YA.js.map