{"version":3,"file":"retrieve.mjs","names":[],"sources":["../../../../../../../ai/src/rag/retrieve.ts"],"sourcesContent":["import type { EmbedderContract } from \"../contracts/embedder.contract\";\nimport type {\n  Citation,\n  RetrievedChunk,\n  RetrieveOptions,\n  RetrieveResult,\n} from \"./contracts/citation.type\";\nimport type { RagReranker } from \"./rerank/reranker.contract\";\nimport type { VectorStore } from \"./store/vector-store.contract\";\n\n/** Default number of chunks returned after reranking. */\nexport const DEFAULT_TOP_K = 5;\n\n/** Default cosine floor applied at the vector-store stage. */\nexport const DEFAULT_THRESHOLD = 0.5;\n\n/**\n * Shape persisted per chunk in the vector store. The vector itself is held\n * by the driver's own index (passed via `set({ vector })`), so it is not\n * duplicated here.\n */\nexport type StoredChunk = {\n  sourceId: string;\n  chunkIndex: number;\n  span: [start: number, end: number];\n  text: string;\n  metadata?: Record<string, unknown>;\n};\n\n/** Dependencies the retrieve pipeline needs, resolved once by `rag()`. */\nexport type RetrieveDeps = {\n  embedder: EmbedderContract;\n  store: VectorStore;\n  /** Namespace prefix every stored key carries (e.g. `\"ai.rag.docs\"`). */\n  namespace: string;\n  /** Optional reranker; when absent the cosine order is kept. */\n  reranker?: RagReranker;\n  /** Pipeline-level retrieval defaults. */\n  defaults?: RetrieveOptions;\n  /**\n   * Dimension count captured at first index for the mismatch guard. When\n   * set, the query embedder's `dimensions` must equal it.\n   */\n  indexedDimensions?: number;\n};\n\n/**\n * The cite pipeline: embed the query → over-fetch candidates from the\n * store → filter to this rag's namespace → map to {@link RetrievedChunk}s\n * with a {@link Citation} → optionally rerank → slice `topK`.\n *\n * Behavior matches the design's failure modes:\n * - No hits clearing the threshold → `{ query, chunks: [] }`, never throws.\n * - Namespace-prefix filtering keeps two rags sharing one driver isolated.\n * - A reranker that throws is caught; the raw cosine order is used instead.\n * - A dimension mismatch (indexed with model A, queried with model B)\n *   throws a clear error rather than returning garbage hits.\n */\nexport async function retrieve(\n  query: string,\n  deps: RetrieveDeps,\n  options: RetrieveOptions = {},\n): Promise<RetrieveResult> {\n  const topK = options.topK ?? deps.defaults?.topK ?? DEFAULT_TOP_K;\n  const threshold = options.threshold ?? deps.defaults?.threshold ?? DEFAULT_THRESHOLD;\n  const tags = options.tags ?? deps.defaults?.tags;\n  const candidates = options.candidates ?? deps.defaults?.candidates ?? Math.max(topK * 4, topK);\n\n  const { vector, dimensions } = await deps.embedder.embed(query);\n\n  if (\n    deps.indexedDimensions !== undefined &&\n    dimensions !== 0 &&\n    deps.indexedDimensions !== 0 &&\n    dimensions !== deps.indexedDimensions\n  ) {\n    throw new Error(\n      `rag.retrieve(): query embedder dimensions (${dimensions}) do not match the dimensions captured at index time (${deps.indexedDimensions}); index and query must use the same embedding model`,\n    );\n  }\n\n  const hits = await deps.store.query<StoredChunk>(vector, {\n    topK: candidates,\n    threshold,\n    tags,\n  });\n\n  const prefix = `${deps.namespace}.`;\n\n  let retrieved: RetrievedChunk[] = hits\n    .filter((hit) => hit.key.startsWith(prefix))\n    .map((hit) => toRetrievedChunk(hit.value, hit.score));\n\n  retrieved = await applyReranker(query, retrieved, deps.reranker);\n\n  return { query, chunks: retrieved.slice(0, topK) };\n}\n\n/** Build a cited {@link RetrievedChunk} from a stored chunk + its cosine score. */\nfunction toRetrievedChunk(stored: StoredChunk, score: number): RetrievedChunk {\n  const citation: Citation = {\n    sourceId: stored.sourceId,\n    chunkIndex: stored.chunkIndex,\n    span: stored.span,\n    score,\n    metadata: stored.metadata,\n  };\n\n  return { text: stored.text, score, citation };\n}\n\n/**\n * Run the optional reranker, degrading to the raw cosine order if it\n * throws — a flaky optional reranker must never fail the whole retrieval.\n */\nasync function applyReranker(\n  query: string,\n  candidates: RetrievedChunk[],\n  reranker: RagReranker | undefined,\n): Promise<RetrievedChunk[]> {\n  if (!reranker) {\n    return candidates;\n  }\n\n  try {\n    return await reranker.rerank(query, candidates);\n  } catch {\n    // Logged at the call site in a richer build; here we degrade silently\n    // to vector-only ranking rather than aborting the retrieval.\n    return candidates;\n  }\n}\n"],"mappings":";;AAWA,MAAa,gBAAgB;;AAG7B,MAAa,oBAAoB;;;;;;;;;;;;;AA4CjC,eAAsB,SACpB,OACA,MACA,UAA2B,CAAC,GACH;CACzB,MAAM,OAAO,QAAQ,QAAQ,KAAK,UAAU;CAC5C,MAAM,YAAY,QAAQ,aAAa,KAAK,UAAU;CACtD,MAAM,OAAO,QAAQ,QAAQ,KAAK,UAAU;CAC5C,MAAM,aAAa,QAAQ,cAAc,KAAK,UAAU,cAAc,KAAK,IAAI,OAAO,GAAG,IAAI;CAE7F,MAAM,EAAE,QAAQ,eAAe,MAAM,KAAK,SAAS,MAAM,KAAK;CAE9D,IACE,KAAK,sBAAsB,UAC3B,eAAe,KACf,KAAK,sBAAsB,KAC3B,eAAe,KAAK,mBAEpB,MAAM,IAAI,MACR,8CAA8C,WAAW,wDAAwD,KAAK,kBAAkB,qDAC1I;CAGF,MAAM,OAAO,MAAM,KAAK,MAAM,MAAmB,QAAQ;EACvD,MAAM;EACN;EACA;CACF,CAAC;CAED,MAAM,SAAS,GAAG,KAAK,UAAU;CAEjC,IAAI,YAA8B,KAC/B,QAAQ,QAAQ,IAAI,IAAI,WAAW,MAAM,CAAC,CAAC,CAC3C,KAAK,QAAQ,iBAAiB,IAAI,OAAO,IAAI,KAAK,CAAC;CAEtD,YAAY,MAAM,cAAc,OAAO,WAAW,KAAK,QAAQ;CAE/D,OAAO;EAAE;EAAO,QAAQ,UAAU,MAAM,GAAG,IAAI;CAAE;AACnD;;AAGA,SAAS,iBAAiB,QAAqB,OAA+B;CAC5E,MAAM,WAAqB;EACzB,UAAU,OAAO;EACjB,YAAY,OAAO;EACnB,MAAM,OAAO;EACb;EACA,UAAU,OAAO;CACnB;CAEA,OAAO;EAAE,MAAM,OAAO;EAAM;EAAO;CAAS;AAC9C;;;;;AAMA,eAAe,cACb,OACA,YACA,UAC2B;CAC3B,IAAI,CAAC,UACH,OAAO;CAGT,IAAI;EACF,OAAO,MAAM,SAAS,OAAO,OAAO,UAAU;CAChD,QAAQ;EAGN,OAAO;CACT;AACF"}