{"version":3,"file":"cross-rerank.d.ts","sourceRoot":"","sources":["../../../src/core/search/cross-rerank.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAE1E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC,MAAM,WAAW,iBAAiB;IACjC,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,qDAAqD;IACrD,MAAM,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACvC,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,SAAS,cAAc,EAAE,EACrC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,gBAAgB,GACvB,OAAO,CAAC,iBAAiB,CAAC,CA4C5B","sourcesContent":["/**\n * Cross-encoder reranking via the embsearch daemon.\n *\n * The deterministic reranker in `rerank.ts` scores candidates with lexical\n * evidence — term coverage, path affinity, whether the window declares a query\n * term. That took Recall@1 from 0.10 to 0.24 and then stopped paying: a\n * term-proximity signal aimed at the classes it still handles worst moved\n * nothing (p = 1.00 on every metric).\n *\n * What is left is a ranking problem the lexical view cannot see. Across the\n * 62-query gold set the right span reaches the fused top-50 far more often\n * than the top-10, so the candidates are in hand and merely ordered badly. A\n * cross-encoder reads the query and the candidate *together*, which is exactly\n * the evidence term counting lacks.\n *\n * It is also far more expensive — one model pass per candidate, with no\n * precomputation possible — so it runs over a shortlist and its depth is\n * capped separately from the fused window.\n */\n\nimport type { EmbsearchService } from \"../embsearch/embsearch-service.js\";\nimport { readCandidateWindows } from \"./rerank.js\";\nimport type { FusedCandidate } from \"./types.js\";\n\n/**\n * Candidates sent to the cross-encoder per query.\n *\n * Each one is a model pass, so this is a latency dial, not a quality dial:\n * every candidate past here keeps its fused order rather than being scored.\n */\nexport const CROSS_ENCODER_DEPTH = 30;\n\nexport interface CrossRerankResult {\n\tcandidates: FusedCandidate[];\n\tlatencyMs: number;\n\t/** How many candidates the model actually scored. */\n\tscored: number;\n}\n\n/**\n * Reorder `candidates` by cross-encoder relevance.\n *\n * Only the first {@link CROSS_ENCODER_DEPTH} are scored; the remainder keep\n * their incoming order and follow. Candidates whose window cannot be read are\n * left unscored for the same reason — there is no text to give the model, and\n * inventing one would score a fiction.\n */\nexport async function crossEncoderRerank(\n\tquery: string,\n\tcandidates: readonly FusedCandidate[],\n\tcwd: string,\n\tservice: EmbsearchService,\n): Promise<CrossRerankResult> {\n\tconst startedMs = Date.now();\n\tif (candidates.length < 2) {\n\t\treturn { candidates: [...candidates], latencyMs: Date.now() - startedMs, scored: 0 };\n\t}\n\n\tconst head = candidates.slice(0, CROSS_ENCODER_DEPTH);\n\tconst tail = candidates.slice(CROSS_ENCODER_DEPTH);\n\tconst windows = readCandidateWindows(head, cwd);\n\n\tconst passages: Array<{ id: string; text: string }> = [];\n\tconst byId = new Map<string, FusedCandidate>();\n\thead.forEach((candidate, i) => {\n\t\tconst text = windows[i];\n\t\tif (text === undefined) return;\n\t\t// Fused ids are unique per query, so they round-trip as passage ids.\n\t\tpassages.push({ id: candidate.id, text });\n\t\tbyId.set(candidate.id, candidate);\n\t});\n\n\tif (passages.length === 0) {\n\t\treturn { candidates: [...candidates], latencyMs: Date.now() - startedMs, scored: 0 };\n\t}\n\n\tconst scored = await service.rerank(query, passages, passages.length);\n\n\t// Reassemble: scored candidates in model order, then anything the model did\n\t// not see, in fused order. An id the daemon did not return would otherwise\n\t// vanish from the results entirely.\n\tconst ordered: FusedCandidate[] = [];\n\tconst seen = new Set<string>();\n\tfor (const result of scored) {\n\t\tconst candidate = byId.get(result.id);\n\t\tif (candidate && !seen.has(result.id)) {\n\t\t\tordered.push(candidate);\n\t\t\tseen.add(result.id);\n\t\t}\n\t}\n\tfor (const candidate of head) {\n\t\tif (!seen.has(candidate.id)) ordered.push(candidate);\n\t}\n\tordered.push(...tail);\n\n\treturn { candidates: ordered, latencyMs: Date.now() - startedMs, scored: passages.length };\n}\n"]}