import {
  extractJsonCandidates
} from "./chunk-UZB5KHKX.js";
import {
  normalizeProcedureSteps
} from "./chunk-QDW3E4RD.js";
import {
  log
} from "./chunk-2ODBA7MQ.js";

// src/extraction-judge.ts
import { createHash } from "crypto";
function getVerdictKind(verdict) {
  const raw = verdict.kind;
  if (raw === "accept" || raw === "reject" || raw === "defer") {
    return raw;
  }
  return verdict.durable ? "accept" : "reject";
}
function isDurableVerdict(verdict) {
  return getVerdictKind(verdict) === "accept";
}
function isValidCachedVerdict(value) {
  if (typeof value !== "object" || value === null) return false;
  const v = value;
  if (typeof v.durable !== "boolean") return false;
  if (typeof v.reason !== "string") return false;
  if (v.kind !== void 0) {
    if (v.kind !== "accept" && v.kind !== "reject" && v.kind !== "defer") {
      return false;
    }
  }
  return true;
}
function normalizeCachedVerdict(value) {
  if (typeof value !== "object" || value === null) return null;
  const v = value;
  if (typeof v.durable !== "boolean") return null;
  if (typeof v.reason !== "string") return null;
  let kind;
  if (v.kind !== void 0) {
    if (typeof v.kind !== "string") return null;
    if (v.kind === "accept" || v.kind === "reject" || v.kind === "defer") {
      kind = v.kind;
    }
  }
  const out = { durable: v.durable, reason: v.reason };
  if (kind !== void 0) out.kind = kind;
  return out;
}
var JUDGE_SYSTEM_PROMPT = `You are a memory curator evaluating whether extracted facts are **durable** \u2014 worth storing for long-term recall across sessions.

A fact is **durable** if it will still be useful 30+ days from now and is relevant across multiple sessions, not just the current task.

Return one of three verdicts per candidate:

ACCEPT \u2014 the fact is durable, persist it:
- Personal preferences, identities, or relationships
- Decisions with rationale that affect future work
- Corrections to previously held beliefs
- Principles, rules, or constraints the user wants respected
- Stable facts about projects, tools, or workflows
- Commitments, deadlines, or obligations

REJECT \u2014 the fact is not durable, drop it:
- Transient task details ("currently debugging line 42")
- Ephemeral state ("the build is running now")
- Routine operations ("ran npm install")
- Conversational filler or acknowledgements
- Information that will be stale within hours
- Step-by-step instructions for a one-time task

DEFER \u2014 the fact MIGHT be durable but needs more context to decide. The candidate will be re-evaluated on a later extraction pass with fresh context; if it cannot be resolved within a small number of re-evaluations it will be rejected.
- Ambiguous referents ("he said they'd follow up on it")
- Partial or in-progress statements that might become durable once completed
- Future-tense commitments whose subject or timeline is unclear
- Facts whose durability hinges on context not present in the candidate text

Do NOT use defer as a soft reject. Reject facts you are confident are transient. Only defer when another turn of context would genuinely change the verdict.

Return a JSON array of objects with these fields:
- index: number (the candidate index)
- kind: string \u2014 one of "accept", "reject", "defer"
- reason: string (brief explanation, under 80 characters)

You may also include durable (boolean) for backwards compatibility: true for accept, false for reject or defer. If kind is omitted, durable determines the verdict.

Rules:
1. Return exactly one verdict per input candidate, matched by index.
2. When in doubt between accept and reject, lean toward accept \u2014 false negatives (losing a useful fact) are worse than false positives (keeping a marginal one).
3. Use defer only when another turn of context would genuinely change the verdict.
4. Output valid JSON only. No markdown fences, no commentary.

Example output:
[{"index": 0, "kind": "accept", "durable": true, "reason": "Stable personal preference"}, {"index": 1, "kind": "reject", "durable": false, "reason": "Ephemeral build status"}, {"index": 2, "kind": "defer", "durable": false, "reason": "Ambiguous pronoun"}]`;
var VERDICT_CACHE_MAX_SIZE = 1e4;
var defaultVerdictCache = /* @__PURE__ */ new Map();
var defaultDeferCounts = /* @__PURE__ */ new Map();
var DEFER_COUNT_MAX_SIZE = 2e4;
function cacheKey(text, category) {
  return createHash("sha256").update(`${text}\0${category}`).digest("hex");
}
function resolveDeferCap(config) {
  const raw = config.extractionJudgeMaxDeferrals;
  if (typeof raw === "number" && Number.isFinite(raw) && raw >= 1) {
    return Math.floor(raw);
  }
  return 2;
}
function enforceMaxCacheSize(cache) {
  if (cache.size <= VERDICT_CACHE_MAX_SIZE) return;
  const deleteCount = Math.floor(cache.size / 2);
  let deleted = 0;
  for (const key of cache.keys()) {
    if (deleted >= deleteCount) break;
    cache.delete(key);
    deleted++;
  }
}
var AUTO_APPROVE_CATEGORIES = /* @__PURE__ */ new Set(["correction", "principle"]);
var PROCEDURE_TRIGGER_RE = /(when you|whenever|before you|before running|always\s|first\b.*\bthen|to deploy|to ship|run these steps|follow these steps|how (i|we)\s|recipe for|workflow|each time you)/i;
function validateProcedureExtraction(input) {
  const steps = normalizeProcedureSteps(input.procedureSteps);
  if (steps.length < 2) {
    return { durable: false, reason: "Procedure requires at least two steps with intents" };
  }
  const combined = [input.content, ...steps.map((s) => s.intent)].join(" ").toLowerCase();
  if (!PROCEDURE_TRIGGER_RE.test(combined)) {
    return { durable: false, reason: "Procedure missing explicit trigger phrasing" };
  }
  return { durable: true, reason: "Procedure structure validated" };
}
async function judgeFactDurability(candidates, config, localLlm, fallbackLlm, cache, deferCounts, onVerdict) {
  const startMs = Date.now();
  const verdicts = /* @__PURE__ */ new Map();
  let cached = 0;
  let judged = 0;
  let deferred = 0;
  let deferredCappedToReject = 0;
  const verdictCache = cache ?? defaultVerdictCache;
  const deferCountMap = deferCounts ?? defaultDeferCounts;
  const deferCap = resolveDeferCap(config);
  const emit = (build) => {
    if (!onVerdict) return;
    let observation;
    try {
      observation = build();
    } catch (err) {
      log.debug(
        `extraction-judge: onVerdict builder threw (non-fatal): ${err instanceof Error ? err.message : String(err)}`
      );
      return;
    }
    try {
      onVerdict(observation);
    } catch (err) {
      log.debug(
        `extraction-judge: onVerdict callback threw (non-fatal): ${err instanceof Error ? err.message : String(err)}`
      );
    }
  };
  if (candidates.length === 0) {
    return {
      verdicts,
      cached,
      judged,
      elapsed: 0,
      deferred,
      deferredCappedToReject
    };
  }
  const pendingIndices = [];
  for (let i = 0; i < candidates.length; i++) {
    const c = candidates[i];
    if (AUTO_APPROVE_CATEGORIES.has(c.category)) {
      const v = {
        durable: true,
        reason: `Auto-approved: ${c.category} category bypasses judge`
      };
      verdicts.set(i, v);
      emit(() => ({
        verdict: v,
        candidate: c,
        contentHash: cacheKey(c.text, c.category),
        source: "auto-approve",
        priorDeferrals: 0,
        elapsedMs: Date.now() - startMs
      }));
      continue;
    }
    if (c.importanceLevel === "critical") {
      const v = {
        durable: true,
        reason: "Auto-approved: critical importance"
      };
      verdicts.set(i, v);
      emit(() => ({
        verdict: v,
        candidate: c,
        contentHash: cacheKey(c.text, c.category),
        source: "auto-approve",
        priorDeferrals: 0,
        elapsedMs: Date.now() - startMs
      }));
      continue;
    }
    const key = cacheKey(c.text, c.category);
    const cachedVerdict = verdictCache.get(key);
    if (cachedVerdict) {
      verdicts.set(i, cachedVerdict);
      cached++;
      emit(() => ({
        verdict: cachedVerdict,
        candidate: c,
        contentHash: key,
        source: "cache",
        priorDeferrals: deferCountMap.get(key) ?? 0,
        elapsedMs: Date.now() - startMs
      }));
      continue;
    }
    pendingIndices.push(i);
  }
  if (pendingIndices.length === 0) {
    return {
      verdicts,
      cached,
      judged,
      elapsed: Date.now() - startMs,
      deferred,
      deferredCappedToReject
    };
  }
  const batchSize = config.extractionJudgeBatchSize;
  for (let batchStart = 0; batchStart < pendingIndices.length; batchStart += batchSize) {
    const batchIndices = pendingIndices.slice(batchStart, batchStart + batchSize);
    const batchPayload = batchIndices.map((idx) => ({
      index: idx,
      text: candidates[idx].text,
      category: candidates[idx].category,
      confidence: candidates[idx].confidence
    }));
    const userPrompt = JSON.stringify(batchPayload);
    try {
      const llmResponse = await callJudgeLlm(
        userPrompt,
        config,
        localLlm,
        fallbackLlm
      );
      if (llmResponse) {
        const parsed = parseJudgeResponse(llmResponse, batchIndices);
        const deferredThisBatch = /* @__PURE__ */ new Set();
        for (const [idx, rawVerdict] of parsed.entries()) {
          const c = candidates[idx];
          const key = cacheKey(c.text, c.category);
          let verdict = rawVerdict;
          let source = "llm";
          const priorDefers = deferCountMap.get(key) ?? 0;
          if (getVerdictKind(verdict) === "defer") {
            if (priorDefers >= deferCap) {
              verdict = {
                durable: false,
                reason: `Defer cap reached (${deferCap} prior defers); rejecting`,
                kind: "reject"
              };
              source = "llm-cap-rejected";
              if (!deferredThisBatch.has(key)) {
                deferCountMap.delete(key);
                deferredCappedToReject++;
                deferredThisBatch.add(key);
              }
            } else if (!deferredThisBatch.has(key)) {
              deferCountMap.set(key, priorDefers + 1);
              deferred++;
              deferredThisBatch.add(key);
              if (deferCountMap.size > DEFER_COUNT_MAX_SIZE) {
                const drop = Math.floor(deferCountMap.size / 2);
                let dropped = 0;
                for (const k of deferCountMap.keys()) {
                  if (dropped >= drop) break;
                  deferCountMap.delete(k);
                  dropped++;
                }
              }
            }
          } else {
            deferCountMap.delete(key);
          }
          verdicts.set(idx, verdict);
          judged++;
          if (getVerdictKind(verdict) !== "defer") {
            verdictCache.set(key, verdict);
          }
          emit(() => ({
            verdict,
            candidate: c,
            contentHash: key,
            source,
            priorDeferrals: priorDefers,
            elapsedMs: Date.now() - startMs
          }));
        }
        enforceMaxCacheSize(verdictCache);
      }
    } catch (err) {
      log.warn(
        `extraction-judge: LLM call failed, approving batch (fail-open): ${err instanceof Error ? err.message : String(err)}`
      );
    }
    for (const idx of batchIndices) {
      if (!verdicts.has(idx)) {
        const c = candidates[idx];
        const hash = cacheKey(c.text, c.category);
        const priorDefers = deferCountMap.get(hash) ?? 0;
        deferCountMap.delete(hash);
        const v = {
          durable: true,
          reason: "Approved by default (judge unavailable or parse error)"
        };
        verdicts.set(idx, v);
        emit(() => {
          return {
            verdict: v,
            candidate: c,
            contentHash: hash,
            source: "fail-open",
            priorDeferrals: priorDefers,
            elapsedMs: Date.now() - startMs
          };
        });
      }
    }
  }
  return {
    verdicts,
    cached,
    judged,
    elapsed: Date.now() - startMs,
    deferred,
    deferredCappedToReject
  };
}
async function callJudgeLlm(userPrompt, config, localLlm, fallbackLlm) {
  const messages = [
    { role: "system", content: JUDGE_SYSTEM_PROMPT },
    { role: "user", content: userPrompt }
  ];
  const modelOverride = config.extractionJudgeModel || void 0;
  const skipLocal = config.modelSource === "gateway";
  const agentId = config.modelSource === "gateway" ? config.gatewayAgentId || void 0 : void 0;
  if (localLlm && !skipLocal) {
    try {
      const result = await localLlm.chatCompletion(messages, {
        temperature: 0.1,
        maxTokens: 2048,
        responseFormat: { type: "json_object" },
        timeoutMs: 1500,
        operation: "extraction-judge",
        ...modelOverride ? { model: modelOverride } : {}
      });
      if (result?.content) {
        return result.content;
      }
    } catch (err) {
      log.debug(
        `extraction-judge: local LLM failed, trying fallback: ${err instanceof Error ? err.message : String(err)}`
      );
    }
  }
  if (fallbackLlm) {
    try {
      const result = await fallbackLlm.chatCompletion(
        messages,
        {
          temperature: 0.1,
          maxTokens: 2048,
          timeoutMs: 1500,
          ...modelOverride ? { model: modelOverride } : {},
          ...agentId ? { agentId } : {}
        }
      );
      if (result?.content) {
        return result.content;
      }
    } catch (err) {
      log.debug(
        `extraction-judge: fallback LLM failed: ${err instanceof Error ? err.message : String(err)}`
      );
    }
  }
  return null;
}
function parseJudgeResponse(raw, expectedIndices) {
  const result = /* @__PURE__ */ new Map();
  const expectedSet = new Set(expectedIndices);
  try {
    let parsed;
    try {
      parsed = JSON.parse(raw);
    } catch {
      const candidates = extractJsonCandidates(raw);
      if (candidates.length > 0) {
        parsed = JSON.parse(candidates[0]);
      }
    }
    if (!Array.isArray(parsed)) {
      if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
        const values = Object.values(parsed);
        for (const v of values) {
          if (Array.isArray(v)) {
            parsed = v;
            break;
          }
        }
      }
      if (!Array.isArray(parsed)) {
        log.debug("extraction-judge: response is not an array, cannot parse");
        return result;
      }
    }
    for (const item of parsed) {
      if (typeof item !== "object" || item === null || typeof item.index !== "number") {
        continue;
      }
      const idx = item.index;
      if (!expectedSet.has(idx)) continue;
      let kind;
      const rawKind = item.kind;
      const rawAction = item.action;
      if (rawKind === "accept" || rawKind === "reject" || rawKind === "defer") {
        kind = rawKind;
      } else if (rawAction === "accept" || rawAction === "reject" || rawAction === "defer") {
        kind = rawAction;
      }
      const hasDurable = typeof item.durable === "boolean";
      const durableFromModel = hasDurable ? item.durable : void 0;
      let durable;
      if (kind === "accept") {
        durable = true;
      } else if (kind === "reject" || kind === "defer") {
        durable = false;
      } else if (durableFromModel !== void 0) {
        durable = durableFromModel;
      } else {
        durable = true;
      }
      const reason = typeof item.reason === "string" ? item.reason.slice(0, 120) : "No reason provided";
      const verdict = { durable, reason };
      if (kind !== void 0) verdict.kind = kind;
      result.set(idx, verdict);
    }
  } catch (err) {
    log.debug(
      `extraction-judge: failed to parse response: ${err instanceof Error ? err.message : String(err)}`
    );
  }
  return result;
}
function clearVerdictCache() {
  defaultVerdictCache.clear();
  defaultDeferCounts.clear();
}
function verdictCacheSize() {
  return defaultVerdictCache.size;
}
function createVerdictCache() {
  return /* @__PURE__ */ new Map();
}
function createDeferCountMap() {
  return /* @__PURE__ */ new Map();
}

export {
  getVerdictKind,
  isDurableVerdict,
  isValidCachedVerdict,
  normalizeCachedVerdict,
  validateProcedureExtraction,
  judgeFactDurability,
  clearVerdictCache,
  verdictCacheSize,
  createVerdictCache,
  createDeferCountMap
};
//# sourceMappingURL=chunk-C4SQJZAF.js.map