import assert from "node:assert/strict";
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import vm from "node:vm";

async function loadRetrieve(stubs) {
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
  const retrieverPath = path.resolve(__dirname, "../retrieval/retriever.js");
  const source = await fs.readFile(retrieverPath, "utf8");
  const transformed = `${source
    .replace(/^import[\s\S]*?from\s+["'][^"']+["'];\r?\n/gm, "")
    .replace("export async function retrieve", "async function retrieve")}
this.retrieve = retrieve;
`;

  const context = vm.createContext({
    console: { log() {}, error() {}, warn() {} },
    debugLog() {},
    ...stubs,
  });
  new vm.Script(transformed).runInContext(context);
  return context.retrieve;
}

function createGraph() {
  const nodes = [
    {
      id: "rule-1",
      type: "rule",
      importance: 9,
      createdTime: 1,
      archived: false,
      fields: { title: "规则一" },
      seqRange: [1, 1],
    },
    {
      id: "rule-2",
      type: "rule",
      importance: 7,
      createdTime: 2,
      archived: false,
      fields: { title: "规则二" },
      seqRange: [2, 2],
    },
    {
      id: "rule-3",
      type: "rule",
      importance: 3,
      createdTime: 3,
      archived: false,
      fields: { title: "规则三" },
      seqRange: [3, 3],
    },
  ];
  return { nodes, edges: [] };
}

function createGraphHelpers(graph) {
  return {
    getActiveNodes(target, type = null) {
      const source = target?.nodes || graph.nodes;
      return source.filter(
        (node) => !node.archived && (!type || node.type === type),
      );
    },
    getNode(target, id) {
      return (target?.nodes || graph.nodes).find((node) => node.id === id) || null;
    },
    getNodeEdges(target, nodeId) {
      return (target?.edges || graph.edges).filter(
        (edge) => edge.fromId === nodeId || edge.toId === nodeId,
      );
    },
    buildTemporalAdjacencyMap() {
      return new Map();
    },
  };
}

function getPromptNodeLabel(node = {}, { maxLength = 32 } = {}) {
  const raw = String(
    node?.fields?.title ||
      node?.fields?.name ||
      node?.fields?.summary ||
      node?.fields?.insight ||
      node?.fields?.belief ||
      node?.id ||
      "—",
  )
    .replace(/\s+/g, " ")
    .trim();
  if (!raw) return "—";
  if (!Number.isFinite(maxLength) || maxLength < 2 || raw.length <= maxLength) {
    return raw;
  }
  return `${raw.slice(0, Math.max(1, maxLength - 1)).trimEnd()}…`;
}

function createPromptNodeReferenceMap(entries = [], { prefix = "N", buildMeta = null } = {}) {
  const keyToNodeId = {};
  const keyToMeta = {};
  const nodeIdToKey = {};
  const references = [];
  for (const [index, entry] of (Array.isArray(entries) ? entries : []).entries()) {
    const node = entry?.node || entry || {};
    const nodeId = String(entry?.nodeId || node?.id || "").trim();
    if (!nodeId || nodeIdToKey[nodeId]) continue;
    const key = `${String(prefix || "N").trim() || "N"}${references.length + 1}`;
    keyToNodeId[key] = nodeId;
    nodeIdToKey[nodeId] = key;
    keyToMeta[key] = {
      nodeId,
      type: String(node?.type || ""),
      label: getPromptNodeLabel(node),
      ...((typeof buildMeta === "function"
        ? buildMeta({ entry, node, nodeId, key, index, label: getPromptNodeLabel(node) })
        : {}) || {}),
    };
    references.push({ key, nodeId, node, meta: keyToMeta[key] });
  }
  return {
    keyToNodeId,
    keyToMeta,
    nodeIdToKey,
    references,
  };
}

function normalizeQueryText(value, maxLength = 400) {
  const normalized = String(value ?? "")
    .replace(/\r\n/g, "\n")
    .replace(/\s+/g, " ")
    .trim();
  if (!normalized) return "";
  return normalized.slice(0, Math.max(1, maxLength));
}

function splitIntentSegments(text, { maxSegments = 4, minLength = 1 } = {}) {
  const raw = String(text || "").trim();
  if (!raw) return [];
  const segments = raw
    .split(/[，,。.；;！!？?\n]+|(?:和|顺便|另外|还有|对了|然后|而且|并且|同时)/)
    .map((item) => item.trim())
    .filter((item) => item.length >= minLength);
  return uniqueStrings(segments).slice(0, Math.max(1, maxSegments));
}

function uniqueStrings(values = [], maxLength = 400) {
  const result = [];
  const seen = new Set();
  for (const value of values) {
    const text = normalizeQueryText(value, maxLength);
    const key = text.toLowerCase();
    if (!text || seen.has(key)) continue;
    seen.add(key);
    result.push(text);
  }
  return result;
}

function mergeVectorResults(groups, limit) {
  const merged = new Map();
  let rawHitCount = 0;
  for (const group of groups) {
    for (const item of group) {
      rawHitCount += 1;
      const existing = merged.get(item.nodeId);
      if (!existing || item.score > existing.score) {
        merged.set(item.nodeId, item);
      }
    }
  }
  return {
    rawHitCount,
    results: [...merged.values()].slice(0, limit),
  };
}

function parseContextLine(line = "") {
  const raw = String(line ?? "").trim();
  if (!raw) return null;
  const bracketMatch = raw.match(/^\[(user|assistant)\]\s*:\s*([\s\S]*)$/i);
  if (bracketMatch) {
    const role = String(bracketMatch[1] || "").toLowerCase();
    const text = normalizeQueryText(bracketMatch[2] || "");
    return text ? { role, text } : null;
  }
  const plainMatch = raw.match(/^(user|assistant|用户|助手|ai)\s*[：:]\s*([\s\S]*)$/i);
  if (!plainMatch) return null;
  const roleToken = String(plainMatch[1] || "").toLowerCase();
  const role =
    roleToken === "assistant" || roleToken === "助手" || roleToken === "ai"
      ? "assistant"
      : "user";
  const text = normalizeQueryText(plainMatch[2] || "");
  return text ? { role, text } : null;
}

function buildContextQueryBlend(
  userMessage,
  recentMessages = [],
  {
    enabled = true,
    assistantWeight = 0.2,
    previousUserWeight = 0.1,
    maxTextLength = 400,
  } = {},
) {
  const currentText = normalizeQueryText(userMessage, maxTextLength);
  let assistantText = "";
  let previousUserText = "";
  const parsedMessages = Array.isArray(recentMessages)
    ? recentMessages.map((line) => parseContextLine(line)).filter(Boolean)
    : [];

  for (let index = parsedMessages.length - 1; index >= 0; index -= 1) {
    const item = parsedMessages[index];
    if (!assistantText && item.role === "assistant") {
      assistantText = normalizeQueryText(item.text, maxTextLength);
    }
    if (
      !previousUserText &&
      item.role === "user" &&
      normalizeQueryText(item.text, maxTextLength).toLowerCase() !==
        currentText.toLowerCase()
    ) {
      previousUserText = normalizeQueryText(item.text, maxTextLength);
    }
    if (assistantText && previousUserText) break;
  }

  const currentWeight = Math.max(
    0,
    1 - Number(assistantWeight || 0) - Number(previousUserWeight || 0),
  );
  const rawParts = [
    {
      kind: "currentUser",
      label: "当前用户消息",
      text: currentText,
      weight: enabled ? currentWeight : 1,
    },
  ];
  if (enabled && assistantText) {
    rawParts.push({
      kind: "assistantContext",
      label: "最近 assistant 回复",
      text: assistantText,
      weight: Number(assistantWeight || 0),
    });
  }
  if (enabled && previousUserText) {
    rawParts.push({
      kind: "previousUser",
      label: "上一条 user 消息",
      text: previousUserText,
      weight: Number(previousUserWeight || 0),
    });
  }

  const dedupedParts = [];
  const seen = new Set();
  for (const part of rawParts) {
    const text = normalizeQueryText(part.text, maxTextLength);
    const key = text.toLowerCase();
    if (!text || seen.has(key)) continue;
    seen.add(key);
    dedupedParts.push({ ...part, text });
  }

  const totalWeight = dedupedParts.reduce(
    (sum, part) => sum + Math.max(0, Number(part.weight) || 0),
    0,
  );
  const parts = dedupedParts.map((part) => ({
    ...part,
    weight:
      totalWeight > 0
        ? Math.round((Math.max(0, Number(part.weight) || 0) / totalWeight) * 1000) /
          1000
        : Math.round((1 / Math.max(1, dedupedParts.length)) * 1000) / 1000,
  }));

  return {
    active: enabled && parts.length > 1,
    parts,
    currentText: currentText || parts[0]?.text || "",
    assistantText,
    previousUserText,
    combinedText:
      parts.length <= 1
        ? parts[0]?.text || ""
        : parts.map((part) => `${part.label}:\n${part.text}`).join("\n\n"),
  };
}

function buildVectorQueryPlan(
  blendPlan,
  { enableMultiIntent = true, maxSegments = 4 } = {},
) {
  const plan = [];
  let currentSegments = [];
  for (const part of blendPlan?.parts || []) {
    let queries = [part.text];
    if (part.kind === "currentUser" && enableMultiIntent) {
      currentSegments = splitIntentSegments(part.text, { maxSegments });
      queries = uniqueStrings([
        part.text,
        ...currentSegments.filter((item) => item !== part.text),
      ]);
    } else {
      queries = uniqueStrings([part.text]);
    }
    plan.push({
      kind: part.kind,
      label: part.label,
      weight: part.weight,
      queries,
    });
  }
  return {
    plan,
    currentSegments,
  };
}

function buildLexicalQuerySources(
  userMessage,
  { enableMultiIntent = true, maxSegments = 4 } = {},
) {
  const currentText = normalizeQueryText(userMessage, 400);
  const segments = enableMultiIntent
    ? splitIntentSegments(currentText, { maxSegments })
    : [];
  return {
    sources: uniqueStrings([currentText, ...segments]),
    segments,
  };
}

function computeLexicalScoreForShared(node, querySources = []) {
  const haystack = String(
    node?.fields?.name || node?.fields?.title || node?.fields?.summary || "",
  ).toLowerCase();
  if (!haystack) return 0;
  for (const sourceText of querySources) {
    const normalizedSource = String(sourceText || "").toLowerCase();
    if (normalizedSource && haystack.includes(normalizedSource.split(/\s+/)[0])) {
      return 1;
    }
  }
  return 0;
}

function extractEntityAnchors(userMessage, activeNodes = []) {
  const anchors = [];
  const seen = new Set();
  for (const node of activeNodes) {
    const candidates = [node?.fields?.name, node?.fields?.title]
      .filter((value) => typeof value === "string")
      .map((value) => value.trim())
      .filter((value) => value.length >= 2);
    for (const candidate of candidates) {
      if (!String(userMessage || "").includes(candidate)) continue;
      const key = `${node.id}:${candidate}`;
      if (seen.has(key)) continue;
      seen.add(key);
      anchors.push({ nodeId: node.id, entity: candidate });
      break;
    }
  }
  return anchors;
}

async function rankNodesForTaskContext({
  graph,
  userMessage,
  recentMessages = [],
  embeddingConfig,
  options = {},
} = {}) {
  const activeNodes = Array.isArray(options.activeNodes)
    ? options.activeNodes.filter((node) => node && !node.archived)
    : (graph?.nodes || []).filter((node) => node && !node.archived);
  const topK = Math.max(1, Math.floor(Number(options.topK) || 20));
  const diffusionTopK = Math.max(1, Math.floor(Number(options.diffusionTopK) || 100));
  const enableVectorPrefilter = options.enableVectorPrefilter ?? true;
  const enableGraphDiffusion = options.enableGraphDiffusion ?? true;
  const enableContextQueryBlend = options.enableContextQueryBlend ?? true;
  const enableMultiIntent = options.enableMultiIntent ?? true;
  const multiIntentMaxSegments = Math.max(
    1,
    Math.floor(Number(options.multiIntentMaxSegments) || 4),
  );
  const contextQueryBlend = buildContextQueryBlend(userMessage, recentMessages, {
    enabled: enableContextQueryBlend,
    assistantWeight: Number(options.contextAssistantWeight ?? 0.2),
    previousUserWeight: Number(options.contextPreviousUserWeight ?? 0.1),
    maxTextLength: Number(options.maxTextLength || 400),
  });
  const queryPlan = buildVectorQueryPlan(contextQueryBlend, {
    enableMultiIntent,
    maxSegments: multiIntentMaxSegments,
  });
  const lexicalQuery = buildLexicalQuerySources(
    contextQueryBlend.currentText || userMessage,
    {
      enableMultiIntent,
      maxSegments: multiIntentMaxSegments,
    },
  );
  const diagnostics = {
    queryBlendActive: contextQueryBlend.active,
    queryBlendParts: (contextQueryBlend.parts || []).map((part) => ({
      kind: part.kind,
      label: part.label,
      weight: part.weight,
      text: part.text,
      length: part.text.length,
    })),
    queryBlendWeights: Object.fromEntries(
      (contextQueryBlend.parts || []).map((part) => [part.kind, part.weight]),
    ),
    segmentsUsed: [...(queryPlan.currentSegments || [])],
    vectorValidation: { valid: true },
    vectorHits: 0,
    vectorMergedHits: 0,
    seedCount: 0,
    diffusionHits: 0,
    temporalSyntheticEdgeCount: 0,
    teleportAlpha: Number(options.teleportAlpha ?? 0.15) || 0.15,
    lexicalBoostedNodes: 0,
    lexicalTopHits: [],
    skipReasons: [],
    timings: { vector: 0, diffusion: 0 },
  };
  const activeNodeIds = new Set(activeNodes.map((node) => node.id));

  let vectorResults = [];
  if (enableVectorPrefilter) {
    const groups = [];
    for (const part of queryPlan.plan) {
      for (const queryText of part.queries) {
        state.vectorCalls.push({ topK, message: queryText });
        const results = [
          { nodeId: "rule-1", score: 0.9 },
          { nodeId: "rule-2", score: 0.8 },
          { nodeId: "rule-3", score: 0.7 },
        ]
          .filter((item) => activeNodeIds.has(item.nodeId))
          .map((item) => ({
            ...item,
            score: item.score * Math.max(0, Number(part.weight) || 0),
          }));
        groups.push(results);
      }
    }
    const merged = mergeVectorResults(groups, Math.max(topK * 2, 24));
    diagnostics.vectorHits = merged.rawHitCount;
    diagnostics.vectorMergedHits = merged.results.length;
    vectorResults = merged.results;
  }

  const exactEntityAnchors = extractEntityAnchors(
    contextQueryBlend.currentText || userMessage,
    activeNodes,
  );
  let diffusionResults = [];
  if (enableGraphDiffusion) {
    const seedMap = new Map();
    for (const item of vectorResults) {
      seedMap.set(item.nodeId, Math.max(seedMap.get(item.nodeId) || 0, item.score));
    }
    for (const item of exactEntityAnchors) {
      seedMap.set(item.nodeId, Math.max(seedMap.get(item.nodeId) || 0, 2.0));
    }
    const uniqueSeeds = [...seedMap.entries()].map(([id, energy]) => ({ id, energy }));
    diagnostics.seedCount = uniqueSeeds.length;
    if (uniqueSeeds.length > 0) {
      state.diffusionCalls.push({
        seeds: uniqueSeeds,
        options: {
          maxSteps: 2,
          decayFactor: 0.6,
          topK: diffusionTopK,
          teleportAlpha: diagnostics.teleportAlpha,
        },
      });
      diffusionResults = [
        { nodeId: "rule-2", energy: 1.2 },
        { nodeId: "rule-3", energy: 0.9 },
      ].filter((item) => activeNodeIds.has(item.nodeId));
    }
  }
  diagnostics.diffusionHits = diffusionResults.length;

  const scoreMap = new Map();
  for (const item of vectorResults) {
    scoreMap.set(item.nodeId, {
      graphScore: scoreMap.get(item.nodeId)?.graphScore || 0,
      vectorScore: item.score,
    });
  }
  for (const item of diffusionResults) {
    scoreMap.set(item.nodeId, {
      graphScore: item.energy,
      vectorScore: scoreMap.get(item.nodeId)?.vectorScore || 0,
    });
  }
  if (scoreMap.size === 0) {
    for (const node of activeNodes) {
      scoreMap.set(node.id, { graphScore: 0, vectorScore: 0 });
    }
  }
  const scoredNodes = [...scoreMap.entries()].map(([nodeId, scores]) => {
    const node = activeNodes.find((item) => item.id === nodeId) || null;
    const lexicalScore = computeLexicalScoreForShared(node, lexicalQuery.sources);
    return {
      nodeId,
      node,
      graphScore: scores.graphScore,
      vectorScore: scores.vectorScore,
      lexicalScore,
      finalScore:
        Number(scores.graphScore || 0) +
        Number(scores.vectorScore || 0) +
        Number(lexicalScore || 0) +
        Number(node?.importance || 0),
      weightedScore:
        Number(scores.graphScore || 0) +
        Number(scores.vectorScore || 0) +
        Number(lexicalScore || 0) +
        Number(node?.importance || 0),
    };
  });
  diagnostics.lexicalBoostedNodes = scoredNodes.filter(
    (item) => (Number(item.lexicalScore) || 0) > 0,
  ).length;
  diagnostics.lexicalTopHits = scoredNodes
    .filter((item) => (Number(item.lexicalScore) || 0) > 0)
    .slice(0, 5)
    .map((item) => ({
      nodeId: item.nodeId,
      label: item.node?.fields?.name || item.node?.fields?.title || item.nodeId,
      lexicalScore: item.lexicalScore,
      finalScore: item.finalScore,
    }));

  return {
    activeNodes,
    contextQueryBlend,
    queryPlan,
    lexicalQuery,
    vectorResults,
    exactEntityAnchors,
    diffusionResults,
    scoredNodes,
    diagnostics,
  };
}

const schema = [{ id: "rule", label: "规则", alwaysInject: false }];

const state = {
  vectorCalls: [],
  diffusionCalls: [],
  llmCalls: [],
  llmCandidateCount: 0,
  llmResponse: { selected_keys: ["R1", "R2"] },
  llmOptions: [],
  authorityCandidateCalls: [],
  authorityCandidateEnabled: false,
  authorityCandidateNodeIds: [],
  authorityCandidateDiagnostics: null,
};

const graph = createGraph();
const helpers = createGraphHelpers(graph);
const retrieve = await loadRetrieve({
  ...helpers,
  createPromptNodeReferenceMap,
  getPromptNodeLabel,
  rankNodesForTaskContext,
  async resolveAuthorityRecallCandidates({
    availableNodes = [],
    activeRegion = "",
    activeStoryContext = {},
    activeRecallOwnerKeys = [],
    options = {},
  } = {}) {
    state.authorityCandidateCalls.push({
      availableNodeIds: availableNodes.map((node) => node.id),
      activeRegion,
      activeStorySegmentId: String(activeStoryContext?.activeSegmentId || ""),
      activeRecallOwnerKeys: [...(activeRecallOwnerKeys || [])],
      minimumUsedCandidateCount: Number(options.minimumUsedCandidateCount || 0) || 0,
    });
    if (!state.authorityCandidateEnabled) {
      return {
        available: false,
        used: false,
        candidateNodes: [],
        diagnostics: {
          provider: "authority-trivium",
          candidateCount: 0,
          filteredCount: 0,
          searchHits: 0,
          neighborCount: 0,
          queryTexts: [],
          fallbackReason: "authority-vector-unavailable",
          timings: {
            total: 0,
            filter: 0,
            search: 0,
            neighbors: 0,
          },
        },
      };
    }
    const requestedIds = Array.isArray(state.authorityCandidateNodeIds)
      ? state.authorityCandidateNodeIds
      : [];
    const candidateNodes = availableNodes.filter((node) => requestedIds.includes(node.id));
    const minimumUsedCandidateCount = Number(options.minimumUsedCandidateCount || 0) || 0;
    const used =
      candidateNodes.length > 0 &&
      candidateNodes.length < availableNodes.length &&
      candidateNodes.length >= minimumUsedCandidateCount;
    const diagnostics = {
      provider: "authority-trivium",
      candidateCount: candidateNodes.length,
      filteredCount: candidateNodes.length,
      searchHits: candidateNodes.length,
      neighborCount: 0,
      queryTexts: ["authority-candidate-query"],
      fallbackReason: used
        ? ""
        : candidateNodes.length === 0
          ? "authority-candidate-empty"
          : candidateNodes.length >= availableNodes.length
            ? "authority-candidate-not-reduced"
            : "authority-candidate-too-small",
      timings: {
        total: 1,
        filter: 0.2,
        search: 0.4,
        neighbors: 0,
      },
      ...(state.authorityCandidateDiagnostics || {}),
    };
    return {
      available: true,
      used,
      candidateNodes: used ? candidateNodes : [],
      diagnostics,
    };
  },
  STORY_TEMPORAL_BUCKETS: {
    CURRENT: "current",
    ADJACENT_PAST: "adjacentPast",
    DISTANT_PAST: "distantPast",
    FLASHBACK: "flashback",
    FUTURE: "future",
    UNDATED: "undated",
  },
  MEMORY_SCOPE_BUCKETS: {
    CHARACTER_POV: "characterPov",
    USER_POV: "userPov",
    OBJECTIVE_CURRENT_REGION: "objectiveCurrentRegion",
    OBJECTIVE_ADJACENT_REGION: "objectiveAdjacentRegion",
    OBJECTIVE_GLOBAL: "objectiveGlobal",
    OTHER_POV: "otherPov",
  },
  normalizeMemoryScope(scope = {}) {
    return {
      layer: scope.layer === "pov" ? "pov" : "objective",
      ownerType: scope.ownerType || "",
      ownerId: scope.ownerId || "",
      ownerName: scope.ownerName || "",
      regionPrimary: scope.regionPrimary || "",
      regionPath: Array.isArray(scope.regionPath) ? scope.regionPath : [],
      regionSecondary: Array.isArray(scope.regionSecondary)
        ? scope.regionSecondary
        : [],
    };
  },
  getScopeRegionKey(scope = {}) {
    return String(scope.regionPrimary || "");
  },
  classifyNodeScopeBucket(node, { activeRegion = "" } = {}) {
    if (node?.scope?.layer === "pov") {
      return node?.scope?.ownerType === "user"
        ? "userPov"
        : "characterPov";
    }
    if (
      activeRegion &&
      String(node?.scope?.regionPrimary || "").trim() === String(activeRegion).trim()
    ) {
      return "objectiveCurrentRegion";
    }
    return "objectiveGlobal";
  },
  resolveScopeBucketWeight(bucket, overrides = {}) {
    return Number(overrides?.[bucket] ?? 1) || 1;
  },
  computeKnowledgeGateForNode(_graph, _node, _ownerKey, options = {}) {
    return {
      visible: true,
      anchored: false,
      rescued: false,
      suppressed: false,
      suppressedReason: "",
      visibilityScore:
        options.scopeBucket === "objectiveCurrentRegion" ? 0.8 : 0.45,
      mode: "soft-visible",
      threshold: 0.4,
    };
  },
  resolveKnowledgeOwner(_graph, input = {}) {
    const ownerType = String(input.ownerType || "").trim();
    const ownerName = String(input.ownerName || input.ownerId || "").trim();
    return {
      ownerType,
      ownerName,
      nodeId: String(input.nodeId || "").trim(),
      aliases: ownerName ? [ownerName] : [],
      ownerKey: ownerType && ownerName ? `${ownerType}:${ownerName}` : "",
    };
  },
  resolveKnowledgeOwnerKeyFromScope(_graph, scope = {}) {
    const ownerType = String(scope.ownerType || "").trim();
    const ownerName = String(scope.ownerName || scope.ownerId || "").trim();
    return ownerType && ownerName ? `${ownerType}:${ownerName}` : "";
  },
  listKnowledgeOwners(targetGraph) {
    return (targetGraph?.nodes || [])
      .filter((node) => node?.type === "character" && !node?.archived)
      .map((node) => ({
        ownerKey: `character:${String(node?.fields?.name || "").trim()}`,
        ownerType: "character",
        ownerName: String(node?.fields?.name || "").trim(),
        nodeId: String(node?.id || "").trim(),
        aliases: [String(node?.fields?.name || "").trim()].filter(Boolean),
        updatedAt: 0,
      }))
      .filter((entry) => entry.ownerKey && entry.ownerName);
  },
  resolveActiveRegionContext(graph, preferredRegion = "") {
    return {
      activeRegion:
        String(preferredRegion || graph?.historyState?.activeRegion || "").trim(),
      source: preferredRegion ? "runtime" : "history",
    };
  },
  resolveAdjacentRegions() {
    return {
      canonicalRegion: "",
      adjacentRegions: [],
    };
  },
  resolveActiveStoryContext(targetGraph, preferred = {}) {
    const preferredLabel = String(preferred?.label || "").trim();
    const preferredSegmentId = String(preferred?.segmentId || "").trim();
    const segments = Array.isArray(targetGraph?.timelineState?.segments)
      ? targetGraph.timelineState.segments
      : [];
    const segment =
      segments.find((item) => item.id === preferredSegmentId) ||
      segments.find((item) => item.label === preferredLabel) ||
      segments.find(
        (item) =>
          item.id === String(targetGraph?.historyState?.activeStorySegmentId || "").trim(),
      ) ||
      null;
    return {
      activeSegmentId: String(
        segment?.id || targetGraph?.historyState?.activeStorySegmentId || "",
      ).trim(),
      activeStoryTimeLabel: String(
        segment?.label || targetGraph?.historyState?.activeStoryTimeLabel || "",
      ).trim(),
      source: segment ? "history" : "",
      segment,
      resolved: Boolean(segment),
    };
  },
  resolveStoryCueMode(userMessage = "", recentMessages = []) {
    const text = [userMessage, ...(Array.isArray(recentMessages) ? recentMessages : [])]
      .map((value) => String(value || ""))
      .join("\n");
    if (/回忆|以前|过去/.test(text)) return "flashback";
    if (/以后|未来|计划|打算/.test(text)) return "future";
    return "";
  },
  describeNodeStoryTime(node = {}) {
    return String(node?.storyTime?.label || node?.storyTimeSpan?.startLabel || "").trim();
  },
  classifyStoryTemporalBucket(_graph, node, { activeSegmentId = "", cueMode = "" } = {}) {
    const label = String(node?.storyTime?.label || node?.storyTimeSpan?.startLabel || "").trim();
    if (!label) {
      return {
        bucket: "undated",
        weight: 0.88,
        suppressed: false,
        rescued: false,
        reason: "undated",
      };
    }
    if (label === activeSegmentId || label === "当前") {
      return {
        bucket: "current",
        weight: 1.15,
        suppressed: false,
        rescued: false,
        reason: "current",
      };
    }
    if (label === "未来计划") {
      return {
        bucket: "future",
        weight: cueMode === "future" ? 0.74 : 0.22,
        suppressed: cueMode !== "future",
        rescued: false,
        reason: cueMode === "future" ? "future-cue" : "future-suppressed",
      };
    }
    if (label === "往事") {
      return {
        bucket: cueMode === "flashback" ? "flashback" : "distantPast",
        weight: cueMode === "flashback" ? 1.02 : 0.64,
        suppressed: false,
        rescued: cueMode === "flashback",
        reason: cueMode === "flashback" ? "flashback-rescued" : "distant-past",
      };
    }
    return {
      bucket: "adjacentPast",
      weight: 1.0,
      suppressed: false,
      rescued: false,
      reason: "adjacent-past",
    };
  },
  pushRecentRecallOwner(historyState, ownerKey = "") {
    historyState.activeRecallOwnerKey = ownerKey;
    historyState.recentRecallOwnerKeys = ownerKey ? [ownerKey] : [];
  },
  describeMemoryScope(scope = {}) {
    return `${scope.layer || "objective"}:${scope.ownerType || ""}:${scope.regionPrimary || ""}`;
  },
  describeScopeBucket(bucket = "") {
    return String(bucket || "");
  },
  EXTRACTION_CONTEXT_REVIEW_HEADER:
    "--- 以下是上下文回顾（已提取过），仅供理解剧情 ---",
  RECALL_TARGET_CONTENT_HEADER:
    "--- 以下是本次需要召回记忆的新对话内容 ---",
  buildTaskPrompt() {
    return { systemPrompt: "" };
  },
  applyTaskRegex(_settings, _taskType, _stage, text) {
    return text;
  },
  splitIntentSegments(text) {
    if (String(text).includes("和")) {
      return String(text).split("和").map((item) => item.trim());
    }
    return [];
  },
  mergeVectorResults(groups, limit) {
    const merged = new Map();
    let rawHitCount = 0;
    for (const group of groups) {
      for (const item of group) {
        rawHitCount += 1;
        const existing = merged.get(item.nodeId);
        if (!existing || item.score > existing.score) {
          merged.set(item.nodeId, item);
        }
      }
    }
    return {
      rawHitCount,
      results: [...merged.values()].slice(0, limit),
    };
  },
  collectSupplementalAnchorNodeIds() {
    return [];
  },
  isEligibleAnchorNode(node) {
    return Boolean(node?.fields?.title || node?.fields?.name);
  },
  createCooccurrenceIndex() {
    return { map: new Map(), source: "batchJournal", batchCount: 0, pairCount: 0 };
  },
  applyCooccurrenceBoost(baseScores) {
    return { scores: new Map(baseScores), boostedNodes: [] };
  },
  applyDiversitySampling(candidates, { k }) {
    return {
      applied: true,
      reason: "",
      selected: candidates.slice(0, k).reverse(),
      beforeCount: candidates.length,
      afterCount: Math.min(k, candidates.length),
    };
  },
  async runResidualRecall() {
    return { triggered: false, hits: [], skipReason: "residual-disabled-test" };
  },
  hybridScore: ({
    graphScore = 0,
    vectorScore = 0,
    lexicalScore = 0,
    importance = 0,
  }) => graphScore + vectorScore + lexicalScore + importance,
  reinforceAccessBatch() {},
  validateVectorConfig() {
    return { valid: true };
  },
  async findSimilarNodesByText(_graph, message, _embeddingConfig, topK) {
    state.vectorCalls.push({ topK, message });
    return [
      { nodeId: "rule-1", score: 0.9 },
      { nodeId: "rule-2", score: 0.8 },
      { nodeId: "rule-3", score: 0.7 },
    ];
  },
  diffuseAndRank(_adjacencyMap, seeds, options) {
    state.diffusionCalls.push({ seeds, options });
    return [
      { nodeId: "rule-2", energy: 1.2 },
      { nodeId: "rule-3", energy: 0.9 },
    ];
  },
  async callLLMForJSON(params = {}) {
    const { userPrompt = "" } = params;
    state.llmOptions.push({ ...params });
    state.llmCalls.push(userPrompt);
    state.llmCandidateCount = userPrompt
      .split("\n")
      .filter((line) => line.trim().startsWith("[")).length;
    if (params.returnFailureDetails) {
      if (state.llmResponse?.ok === false) {
        return state.llmResponse;
      }
      return {
        ok: true,
        data: state.llmResponse,
        errorType: "",
        failureReason: "",
        attempts: 1,
      };
    }
    return state.llmResponse;
  },
    getSTContextForPrompt() {
      return {};
  },
});

state.vectorCalls.length = 0;
state.diffusionCalls.length = 0;
state.llmCalls.length = 0;
const noStageResult = await retrieve({
  graph,
  userMessage: "只看当前规则",
  recentMessages: [],
  embeddingConfig: {},
  schema,
  options: {
    topK: 2,
    maxRecallNodes: 2,
    enableVectorPrefilter: false,
    enableGraphDiffusion: false,
    enableLLMRecall: false,
  },
});
assert.equal(state.vectorCalls.length, 0);
assert.equal(state.diffusionCalls.length, 0);
assert.equal(state.llmCalls.length, 0);
assert.deepEqual(Array.from(noStageResult.selectedNodeIds), ["rule-2", "rule-1"]);

state.authorityCandidateCalls.length = 0;
state.authorityCandidateEnabled = true;
state.authorityCandidateNodeIds = ["rule-2"];
state.authorityCandidateDiagnostics = null;
state.vectorCalls.length = 0;
state.diffusionCalls.length = 0;
const authorityCandidateResult = await retrieve({
  graph,
  userMessage: "只看规则二",
  recentMessages: ["assistant: 请聚焦最新规则。"],
  embeddingConfig: {
    mode: "authority",
    source: "authority-trivium",
    failOpen: true,
  },
  schema,
  options: {
    topK: 2,
    maxRecallNodes: 2,
    enableVectorPrefilter: true,
    enableGraphDiffusion: false,
    enableLLMRecall: false,
    authorityCandidateMinCount: 1,
  },
  settings: {
    authorityGraphQueryEnabled: true,
  },
});
assert.equal(state.authorityCandidateCalls.length, 1);
assert.deepEqual(state.authorityCandidateCalls[0].availableNodeIds, ["rule-1", "rule-2", "rule-3"]);
assert.equal(authorityCandidateResult.meta.retrieval.authorityCandidateUsed, true);
assert.equal(authorityCandidateResult.meta.retrieval.authorityCandidateCount, 1);
assert.equal(authorityCandidateResult.meta.retrieval.rankingNodeCount, 1);
assert.deepEqual(Array.from(authorityCandidateResult.selectedNodeIds), ["rule-2"]);
state.authorityCandidateEnabled = false;
state.authorityCandidateNodeIds = [];
state.authorityCandidateDiagnostics = null;

state.vectorCalls.length = 0;
await retrieve({
  graph,
  userMessage: "他后来怎么做？",
  recentMessages: [
    "[assistant]: 他提到了规则二的限制",
    "[user]: 我们先看规则一",
    "[user]: 他后来怎么做？",
  ],
  embeddingConfig: {},
  schema,
  options: {
    topK: 4,
    maxRecallNodes: 2,
    enableVectorPrefilter: true,
    enableGraphDiffusion: false,
    enableLLMRecall: false,
    enableMultiIntent: false,
    enableContextQueryBlend: true,
  },
});
assert.deepEqual(
  state.vectorCalls.map((item) => item.message),
  ["他后来怎么做？", "他提到了规则二的限制", "我们先看规则一"],
);

state.vectorCalls.length = 0;
state.diffusionCalls.length = 0;
state.llmCalls.length = 0;
state.llmOptions.length = 0;
state.llmCandidateCount = 0;
state.llmResponse = { selected_keys: ["R1", "R2"] };
const llmPoolResult = await retrieve({
  graph,
  userMessage: "请根据规则给出结论",
  recentMessages: ["用户：现在该怎么做？"],
  embeddingConfig: {},
  schema,
  options: {
    topK: 4,
    maxRecallNodes: 2,
    enableVectorPrefilter: true,
    enableGraphDiffusion: false,
    enableLLMRecall: true,
    llmCandidatePool: 2,
  },
});
assert.deepEqual(state.vectorCalls, [
  { topK: 4, message: "请根据规则给出结论" },
  { topK: 4, message: "现在该怎么做？" },
]);
assert.equal(state.diffusionCalls.length, 0);
assert.equal(state.llmCandidateCount, 2);
assert.deepEqual(Array.from(llmPoolResult.selectedNodeIds), ["rule-2", "rule-1"]);
assert.equal(llmPoolResult.meta.retrieval.llm.status, "llm");
assert.equal(
  llmPoolResult.meta.retrieval.llm.selectionProtocol,
  "candidate-keys-v1",
);
assert.deepEqual(
  Array.from(llmPoolResult.meta.retrieval.llm.rawSelectedKeys),
  ["R1", "R2"],
);
assert.deepEqual(
  Array.from(llmPoolResult.meta.retrieval.llm.resolvedSelectedNodeIds),
  ["rule-2", "rule-1"],
);
assert.equal(
  llmPoolResult.meta.retrieval.llm.candidateKeyMapPreview?.R1?.nodeId,
  "rule-2",
);
assert.equal(llmPoolResult.meta.retrieval.llm.legacySelectionUsed, false);
assert.equal(llmPoolResult.meta.retrieval.llm.candidatePool, 2);
assert.equal(llmPoolResult.meta.retrieval.vectorMergedHits, 3);
assert.equal(llmPoolResult.meta.retrieval.diversityApplied, true);
assert.equal(llmPoolResult.meta.retrieval.candidatePoolBeforeDpp, 3);
assert.equal(llmPoolResult.meta.retrieval.candidatePoolAfterDpp, 2);
assert.equal(state.llmOptions[0].returnFailureDetails, true);
assert.equal(state.llmOptions[0].maxRetries, 2);
assert.equal(state.llmOptions[0].maxCompletionTokens, 512);
assert.match(String(state.llmCalls[0] || ""), /\[R1\]/);
assert.doesNotMatch(String(state.llmCalls[0] || ""), /\[rule-1\]|\[rule-2\]/);

state.vectorCalls.length = 0;
state.diffusionCalls.length = 0;
state.llmCalls.length = 0;
state.llmOptions.length = 0;
state.llmResponse = {
  selected_keys: ["R2"],
  selected_ids: ["rule-2"],
};
const selectedKeysPriorityResult = await retrieve({
  graph,
  userMessage: "优先吃新协议",
  recentMessages: ["用户：测试 selected_keys 优先级"],
  embeddingConfig: {},
  schema,
  options: {
    topK: 4,
    maxRecallNodes: 2,
    enableVectorPrefilter: true,
    enableGraphDiffusion: false,
    enableLLMRecall: true,
    llmCandidatePool: 2,
  },
});
assert.deepEqual(Array.from(selectedKeysPriorityResult.selectedNodeIds), ["rule-1"]);
assert.equal(
  selectedKeysPriorityResult.meta.retrieval.llm.selectionProtocol,
  "candidate-keys-v1",
);
assert.equal(
  selectedKeysPriorityResult.meta.retrieval.llm.legacySelectionUsed,
  false,
);

state.vectorCalls.length = 0;
state.diffusionCalls.length = 0;
state.llmCalls.length = 0;
state.llmOptions.length = 0;
state.llmResponse = { selected_ids: ["rule-1"] };
const legacySelectionResult = await retrieve({
  graph,
  userMessage: "兼容旧 selected_ids",
  recentMessages: ["用户：测试 legacy 路径"],
  embeddingConfig: {},
  schema,
  options: {
    topK: 4,
    maxRecallNodes: 2,
    enableVectorPrefilter: true,
    enableGraphDiffusion: false,
    enableLLMRecall: true,
    llmCandidatePool: 2,
  },
});
assert.deepEqual(Array.from(legacySelectionResult.selectedNodeIds), ["rule-1"]);
assert.equal(
  legacySelectionResult.meta.retrieval.llm.selectionProtocol,
  "legacy-selected-ids",
);
assert.equal(
  legacySelectionResult.meta.retrieval.llm.legacySelectionUsed,
  true,
);

state.vectorCalls.length = 0;
state.diffusionCalls.length = 0;
state.llmCalls.length = 0;
state.llmOptions.length = 0;
state.llmResponse = { selected_keys: [] };
const emptySelectionFallbackResult = await retrieve({
  graph,
  userMessage: "这次故意空选",
  recentMessages: ["用户：测试空选回退"],
  embeddingConfig: {},
  schema,
  options: {
    topK: 4,
    maxRecallNodes: 2,
    enableVectorPrefilter: true,
    enableGraphDiffusion: false,
    enableLLMRecall: true,
    llmCandidatePool: 2,
  },
});
assert.equal(emptySelectionFallbackResult.meta.retrieval.llm.status, "fallback");
assert.equal(
  emptySelectionFallbackResult.meta.retrieval.llm.fallbackType,
  "empty-selection",
);
assert.equal(
  emptySelectionFallbackResult.meta.retrieval.llm.emptySelectionAccepted,
  false,
);
assert.deepEqual(
  Array.from(emptySelectionFallbackResult.selectedNodeIds),
  ["rule-2", "rule-1"],
);

state.vectorCalls.length = 0;
state.diffusionCalls.length = 0;
state.llmCalls.length = 0;
state.llmOptions.length = 0;
state.llmResponse = { selected_keys: ["R99"] };
const invalidKeyFallbackResult = await retrieve({
  graph,
  userMessage: "这次给无效 key",
  recentMessages: ["用户：测试无效候选回退"],
  embeddingConfig: {},
  schema,
  options: {
    topK: 4,
    maxRecallNodes: 2,
    enableVectorPrefilter: true,
    enableGraphDiffusion: false,
    enableLLMRecall: true,
    llmCandidatePool: 2,
  },
});
assert.equal(invalidKeyFallbackResult.meta.retrieval.llm.status, "fallback");
assert.equal(
  invalidKeyFallbackResult.meta.retrieval.llm.fallbackType,
  "invalid-candidate",
);
assert.deepEqual(
  Array.from(invalidKeyFallbackResult.selectedNodeIds),
  ["rule-2", "rule-1"],
);

state.vectorCalls.length = 0;
state.diffusionCalls.length = 0;
state.llmCalls.length = 0;
state.llmOptions.length = 0;
await retrieve({
  graph,
  userMessage: "规则一和规则二有什么关联",
  recentMessages: [],
  embeddingConfig: {},
  schema,
  options: {
    topK: 3,
    maxRecallNodes: 2,
    enableVectorPrefilter: true,
    enableGraphDiffusion: true,
    diffusionTopK: 7,
    enableLLMRecall: false,
    enableMultiIntent: true,
    multiIntentMaxSegments: 4,
    enableTemporalLinks: true,
    temporalLinkStrength: 0.2,
    teleportAlpha: 0.15,
  },
});
assert.equal(state.vectorCalls.length, 3);
assert.deepEqual(
  state.vectorCalls.map((item) => item.topK),
  [3, 3, 3],
);
assert.equal(state.diffusionCalls.length, 1);
assert.equal(state.diffusionCalls[0].options.topK, 7);
assert.equal(state.diffusionCalls[0].options.teleportAlpha, 0.15);
assert.equal(noStageResult.meta.retrieval.llm.status, "disabled");

state.vectorCalls.length = 0;
state.diffusionCalls.length = 0;
state.llmCalls.length = 0;
state.llmOptions.length = 0;
state.llmResponse = {
  ok: false,
  errorType: "invalid-json",
  failureReason: "输出不是有效 JSON，请严格返回紧凑 JSON 对象",
};
const fallbackResult = await retrieve({
  graph,
  userMessage: "LLM 这次会坏掉",
  recentMessages: ["用户：请回忆相关规则"],
  embeddingConfig: {},
  schema,
  options: {
    topK: 4,
    maxRecallNodes: 2,
    enableVectorPrefilter: true,
    enableGraphDiffusion: false,
    enableLLMRecall: true,
    llmCandidatePool: 2,
  },
});
assert.equal(fallbackResult.meta.retrieval.llm.status, "fallback");
assert.match(fallbackResult.meta.retrieval.llm.reason, /有效 JSON|回退到评分排序/);
assert.equal(fallbackResult.meta.retrieval.llm.fallbackType, "invalid-json");

const sceneGraph = {
  nodes: [
    {
      id: "event-1",
      type: "event",
      importance: 10,
      createdTime: 1,
      archived: false,
      fields: { title: "事件一" },
      seqRange: [1, 1],
    },
    {
      id: "character-1",
      type: "character",
      importance: 6,
      createdTime: 2,
      archived: false,
      fields: { name: "Alice" },
      seqRange: [1, 1],
    },
    {
      id: "location-1",
      type: "location",
      importance: 5,
      createdTime: 3,
      archived: false,
      fields: { title: "大厅" },
      seqRange: [1, 1],
    },
  ],
  edges: [
    { fromId: "event-1", toId: "character-1", relation: "mentions" },
    { fromId: "event-1", toId: "location-1", relation: "occurs_at" },
  ],
};
const sceneSchema = [
  { id: "event", label: "事件", alwaysInject: false },
  { id: "character", label: "角色", alwaysInject: false },
  { id: "location", label: "地点", alwaysInject: false },
];
const cappedResult = await retrieve({
  graph: sceneGraph,
  userMessage: "只看这一个场景",
  recentMessages: [],
  embeddingConfig: {},
  schema: sceneSchema,
  options: {
    topK: 3,
    maxRecallNodes: 1,
    enableVectorPrefilter: false,
    enableGraphDiffusion: false,
    enableLLMRecall: false,
    enableProbRecall: false,
  },
});
assert.equal(cappedResult.selectedNodeIds.length, 1);

const lexicalGraph = {
  nodes: [
    {
      id: "char-1",
      type: "character",
      importance: 1,
      createdTime: 1,
      archived: false,
      fields: { name: "Alice", summary: "常驻角色" },
      seqRange: [1, 1],
    },
    {
      id: "char-2",
      type: "character",
      importance: 1,
      createdTime: 1,
      archived: false,
      fields: { name: "Bob", summary: "常驻角色" },
      seqRange: [1, 1],
    },
  ],
  edges: [],
};
const lexicalSchema = [{ id: "character", label: "角色", alwaysInject: false }];
const lexicalResult = await retrieve({
  graph: lexicalGraph,
  userMessage: "Alice 现在怎么样了",
  recentMessages: [],
  embeddingConfig: {},
  schema: lexicalSchema,
  options: {
    topK: 2,
    maxRecallNodes: 1,
    enableVectorPrefilter: false,
    enableGraphDiffusion: false,
    enableLLMRecall: false,
    enableDiversitySampling: false,
    enableLexicalBoost: true,
  },
});
assert.deepEqual(Array.from(lexicalResult.selectedNodeIds), ["char-1"]);
assert.equal(lexicalResult.meta.retrieval.queryBlendActive, false);
assert.equal(lexicalResult.meta.retrieval.lexicalBoostedNodes, 1);
assert.equal(lexicalResult.meta.retrieval.lexicalTopHits[0]?.nodeId, "char-1");

const scopedGraph = {
  nodes: [
    {
      id: "obj-global",
      type: "event",
      importance: 8,
      createdTime: 1,
      archived: false,
      fields: { title: "旧王都事件" },
      seqRange: [1, 1],
      scope: { layer: "objective", regionPrimary: "旧城区" },
    },
    {
      id: "char-pov",
      type: "pov_memory",
      importance: 4,
      createdTime: 2,
      archived: false,
      fields: { summary: "艾琳觉得钟楼入口非常可疑" },
      seqRange: [2, 2],
      scope: {
        layer: "pov",
        ownerType: "character",
        ownerId: "艾琳",
        ownerName: "艾琳",
        regionPrimary: "钟楼",
      },
    },
  ],
  edges: [],
  historyState: {
    activeRegion: "钟楼",
    activeCharacterPovOwner: "艾琳",
    activeUserPovOwner: "玩家",
  },
};
const scopedSchema = [
  { id: "event", label: "事件", alwaysInject: true },
  { id: "pov_memory", label: "主观记忆", alwaysInject: false },
];
const scopedResult = await retrieve({
  graph: scopedGraph,
  userMessage: "钟楼里到底有什么",
  recentMessages: [],
  embeddingConfig: {},
  schema: scopedSchema,
  options: {
    topK: 2,
    maxRecallNodes: 1,
    enableVectorPrefilter: false,
    enableGraphDiffusion: false,
    enableLLMRecall: false,
    enableDiversitySampling: false,
    enableScopedMemory: true,
    activeRegion: "钟楼",
    activeCharacterPovOwner: "艾琳",
  },
});
assert.deepEqual(Array.from(scopedResult.selectedNodeIds), ["char-pov"]);
assert.equal(scopedResult.meta.retrieval.activeRegion, "钟楼");
assert.ok(Array.isArray(scopedResult.scopeBuckets.characterPov));
assert.equal(scopedResult.scopeBuckets.characterPov[0]?.id, "char-pov");

const multiOwnerGraph = {
  nodes: [
    {
      id: "char-node-a",
      type: "character",
      importance: 6,
      createdTime: 1,
      archived: false,
      fields: { name: "艾琳" },
      seqRange: [1, 1],
    },
    {
      id: "char-node-b",
      type: "character",
      importance: 6,
      createdTime: 1,
      archived: false,
      fields: { name: "露西亚" },
      seqRange: [1, 1],
    },
    {
      id: "pov-a",
      type: "pov_memory",
      importance: 8,
      createdTime: 2,
      archived: false,
      fields: { summary: "艾琳觉得钟楼里还有第二条暗道" },
      seqRange: [2, 2],
      scope: {
        layer: "pov",
        ownerType: "character",
        ownerId: "艾琳",
        ownerName: "艾琳",
      },
    },
    {
      id: "pov-b",
      type: "pov_memory",
      importance: 7,
      createdTime: 3,
      archived: false,
      fields: { summary: "露西亚认为钟楼守卫在故意拖时间" },
      seqRange: [3, 3],
      scope: {
        layer: "pov",
        ownerType: "character",
        ownerId: "露西亚",
        ownerName: "露西亚",
      },
    },
  ],
  edges: [],
  historyState: {
    activeRegion: "",
    activeCharacterPovOwner: "",
    activeUserPovOwner: "玩家",
  },
};
const multiOwnerSchema = [
  { id: "character", label: "角色", alwaysInject: false },
  { id: "pov_memory", label: "主观记忆", alwaysInject: false },
];
state.llmResponse = {
  selected_ids: ["pov-a", "pov-b"],
  active_owner_keys: ["character:艾琳", "character:露西亚"],
  active_owner_scores: [
    { ownerKey: "character:艾琳", score: 0.91, reason: "她的 POV 直接命中当前追问" },
    { ownerKey: "character:露西亚", score: 0.83, reason: "她也在同一场景并提供互补判断" },
  ],
};
const multiOwnerResult = await retrieve({
  graph: multiOwnerGraph,
  userMessage: "艾琳和露西亚现在各自怎么看钟楼这件事",
  recentMessages: ["[assistant]: 她们刚刚一起进入钟楼大厅"],
  embeddingConfig: {},
  schema: multiOwnerSchema,
  options: {
    topK: 4,
    maxRecallNodes: 2,
    enableVectorPrefilter: false,
    enableGraphDiffusion: false,
    enableLLMRecall: true,
    llmCandidatePool: 4,
  },
});
assert.equal(
  multiOwnerResult.meta.retrieval.llm.selectionProtocol,
  "legacy-selected-ids",
);
assert.equal(
  multiOwnerResult.meta.retrieval.llm.legacySelectionUsed,
  true,
);
assert.deepEqual(
  Array.from(multiOwnerResult.meta.retrieval.activeRecallOwnerKeys),
  ["character:艾琳", "character:露西亚"],
);
assert.equal(multiOwnerResult.meta.retrieval.sceneOwnerResolutionMode, "llm");
assert.deepEqual(
  Array.from(multiOwnerResult.scopeBuckets.characterPovOwnerOrder),
  ["character:艾琳", "character:露西亚"],
);
assert.equal(
  multiOwnerResult.scopeBuckets.characterPovByOwner["character:艾琳"]?.[0]?.id,
  "pov-a",
);
assert.equal(
  multiOwnerResult.scopeBuckets.characterPovByOwner["character:露西亚"]?.[0]?.id,
  "pov-b",
);
assert.equal(
  multiOwnerResult.meta.retrieval.selectedByOwner["character:艾琳"]?.[0],
  "pov-a",
);

const temporalGraph = {
  nodes: [
    {
      id: "evt-current",
      type: "event",
      importance: 5,
      createdTime: 1,
      archived: false,
      fields: { title: "当前调查" },
      seqRange: [10, 10],
      storyTime: { label: "当前" },
    },
    {
      id: "evt-past",
      type: "event",
      importance: 6,
      createdTime: 2,
      archived: false,
      fields: { title: "旧冲突" },
      seqRange: [8, 8],
      storyTime: { label: "往事" },
    },
    {
      id: "evt-future",
      type: "event",
      importance: 10,
      createdTime: 3,
      archived: false,
      fields: { title: "未来计划" },
      seqRange: [12, 12],
      storyTime: { label: "未来计划", tense: "future" },
    },
  ],
  edges: [],
  historyState: {
    activeStorySegmentId: "当前",
    activeStoryTimeLabel: "当前",
    activeStoryTimeSource: "test",
  },
  timelineState: {
    segments: [
      { id: "当前", label: "当前", order: 2 },
      { id: "往事", label: "往事", order: 1 },
      { id: "未来计划", label: "未来计划", order: 3 },
    ],
  },
};
const temporalSchema = [{ id: "event", label: "事件", alwaysInject: false }];
const temporalResult = await retrieve({
  graph: temporalGraph,
  userMessage: "现在现场怎么样",
  recentMessages: [],
  embeddingConfig: {},
  schema: temporalSchema,
  options: {
    topK: 3,
    maxRecallNodes: 2,
    enableVectorPrefilter: false,
    enableGraphDiffusion: false,
    enableLLMRecall: false,
    enableDiversitySampling: false,
    enableStoryTimeline: true,
    storyTimeSoftDirecting: true,
    activeStorySegmentId: "当前",
    activeStoryTimeLabel: "当前",
  },
});
assert.equal(temporalResult.meta.retrieval.activeStorySegmentId, "当前");
assert.equal(temporalResult.meta.retrieval.activeStoryTimeLabel, "当前");
assert.ok(Array.isArray(temporalResult.meta.retrieval.temporalSuppressedNodes));
assert.ok(
  Array.isArray(temporalResult.meta.retrieval.temporalBuckets?.future) ||
    Array.isArray(temporalResult.meta.retrieval.temporalBuckets?.["future"]),
);
assert.ok(
  !Array.from(temporalResult.selectedNodeIds).includes("evt-future"),
);
assert.equal(
  temporalResult.meta.retrieval.temporalTopHits[0]?.nodeId,
  "evt-current",
);

console.log("retrieval-config tests passed");
