@kolisachint/hoocode-agent 0.4.164 → 0.4.166

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/core/embsearch/client.d.ts +39 -2
  3. package/dist/core/embsearch/client.d.ts.map +1 -1
  4. package/dist/core/embsearch/client.js +33 -3
  5. package/dist/core/embsearch/client.js.map +1 -1
  6. package/dist/core/embsearch/embsearch-service.d.ts +30 -1
  7. package/dist/core/embsearch/embsearch-service.d.ts.map +1 -1
  8. package/dist/core/embsearch/embsearch-service.js +78 -5
  9. package/dist/core/embsearch/embsearch-service.js.map +1 -1
  10. package/dist/core/search/cross-rerank.d.ts +44 -0
  11. package/dist/core/search/cross-rerank.d.ts.map +1 -0
  12. package/dist/core/search/cross-rerank.js +77 -0
  13. package/dist/core/search/cross-rerank.js.map +1 -0
  14. package/dist/core/search/eval-compare.d.ts +57 -0
  15. package/dist/core/search/eval-compare.d.ts.map +1 -0
  16. package/dist/core/search/eval-compare.js +114 -0
  17. package/dist/core/search/eval-compare.js.map +1 -0
  18. package/dist/core/search/eval-gold.d.ts +47 -0
  19. package/dist/core/search/eval-gold.d.ts.map +1 -0
  20. package/dist/core/search/eval-gold.js +172 -0
  21. package/dist/core/search/eval-gold.js.map +1 -0
  22. package/dist/core/search/eval-harness.d.ts +140 -0
  23. package/dist/core/search/eval-harness.d.ts.map +1 -0
  24. package/dist/core/search/eval-harness.js +225 -0
  25. package/dist/core/search/eval-harness.js.map +1 -0
  26. package/dist/core/search/eval-live.d.ts +50 -0
  27. package/dist/core/search/eval-live.d.ts.map +1 -0
  28. package/dist/core/search/eval-live.js +48 -0
  29. package/dist/core/search/eval-live.js.map +1 -0
  30. package/dist/core/search/eval.d.ts +69 -8
  31. package/dist/core/search/eval.d.ts.map +1 -1
  32. package/dist/core/search/eval.js +77 -12
  33. package/dist/core/search/eval.js.map +1 -1
  34. package/dist/core/search/hybrid-search.d.ts +22 -0
  35. package/dist/core/search/hybrid-search.d.ts.map +1 -1
  36. package/dist/core/search/hybrid-search.js +61 -3
  37. package/dist/core/search/hybrid-search.js.map +1 -1
  38. package/dist/core/search/mode.d.ts +21 -5
  39. package/dist/core/search/mode.d.ts.map +1 -1
  40. package/dist/core/search/mode.js +23 -10
  41. package/dist/core/search/mode.js.map +1 -1
  42. package/dist/core/search/rerank.d.ts +21 -0
  43. package/dist/core/search/rerank.d.ts.map +1 -1
  44. package/dist/core/search/rerank.js +208 -11
  45. package/dist/core/search/rerank.js.map +1 -1
  46. package/dist/core/search/rrf.d.ts +19 -6
  47. package/dist/core/search/rrf.d.ts.map +1 -1
  48. package/dist/core/search/rrf.js +19 -6
  49. package/dist/core/search/rrf.js.map +1 -1
  50. package/dist/core/search/types.d.ts +11 -1
  51. package/dist/core/search/types.d.ts.map +1 -1
  52. package/dist/core/search/types.js.map +1 -1
  53. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  54. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  55. package/examples/extensions/sandbox/package.json +1 -1
  56. package/examples/extensions/with-deps/package.json +1 -1
  57. package/package.json +7 -4
@@ -1 +1 @@
1
- {"version":3,"file":"embsearch-service.js","sourceRoot":"","sources":["../../../src/core/embsearch/embsearch-service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Orchestrates semantic indexing and search for a repository.\n *\n * Lifecycle (all behind --enable-embsearchtools):\n * 1. `start()` — resolve the embsearch binary, scan the repo (ignore-aware),\n * apply the byte threshold. Under threshold → dormant. Over → spawn the\n * daemon, verify the backend is not the mock embedder, then index changed\n * files in the background in small batches, reporting progress.\n * 2. `search()` — top-k semantic query, mapping chunk ids back to\n * `path:start-end` via the sidecar metadata.\n * 3. `dispose()` — save + close the daemon.\n *\n * Every failure degrades to `unavailable` with a reason; nothing here ever\n * blocks session startup or affects grep/find.\n */\n\nimport { readFileSync } from \"fs\";\nimport { minimatch } from \"minimatch\";\nimport { ensureTool } from \"../../utils/tools-manager.js\";\nimport { chunkFile } from \"./chunker.js\";\nimport { EmbSearchClient } from \"./client.js\";\nimport {\n\temptyIndexMeta,\n\ttype FileMeta,\n\tgetEmbsearchStoreDir,\n\tgetVectorStoreDir,\n\thashContent,\n\ttype IndexMeta,\n\tloadIndexMeta,\n\tsaveIndexMeta,\n} from \"./index-meta.js\";\nimport { type RepoScanFile, scanRepo } from \"./repo-scan.js\";\n\n/** Chunks per bulk request. Small enough that a concurrent query is never\n * stuck long behind one padded batch inference. */\nconst BULK_BATCH_SIZE = 48;\n/** Yield between batches so background indexing doesn't starve the session. */\nconst BATCH_YIELD_MS = 15;\n/** The Rust mock backend's model id — semantically meaningless, never index with it. */\nconst MOCK_MODEL_ID = \"mock-hash-v1\";\n\nexport type EmbsearchState =\n\t| { phase: \"idle\" }\n\t| { phase: \"skipped\"; reason: string }\n\t| { phase: \"downloading\"; receivedBytes: number; totalBytes: number | null }\n\t| { phase: \"indexing\"; done: number; total: number }\n\t| { phase: \"ready\"; chunkCount: number }\n\t| { phase: \"unavailable\"; reason: string };\n\nexport interface EmbsearchServiceOptions {\n\tcwd: string;\n\t/** Explicit binary path (settings override). Default: \"embsearch\" from PATH. */\n\tbinaryPath?: string;\n\t/** Minimum indexable bytes before indexing kicks in. */\n\tthresholdBytes: number;\n\t/** Progress callback for UI (footer / stderr lines). */\n\tonProgress?: (state: EmbsearchState) => void;\n}\n\nexport interface SemanticHit {\n\tpath: string;\n\tstartLine: number;\n\tendLine: number;\n\tscore: number;\n}\n\nexport interface SemanticChunkHit extends SemanticHit {\n\t/** Per-build chunk id (`relpath#index`) — the fusion identity for hybrid search. */\n\tid: string;\n}\n\nexport class EmbsearchService {\n\tprivate readonly options: EmbsearchServiceOptions;\n\tprivate client: EmbSearchClient | undefined;\n\tprivate meta: IndexMeta | undefined;\n\tprivate state: EmbsearchState = { phase: \"idle\" };\n\tprivate disposed = false;\n\n\tconstructor(options: EmbsearchServiceOptions) {\n\t\tthis.options = options;\n\t}\n\n\tgetState(): EmbsearchState {\n\t\treturn this.state;\n\t}\n\n\t/** Semantic search is usable (index ready, or still building with partial data). */\n\tisAvailable(): boolean {\n\t\treturn this.state.phase === \"ready\" || this.state.phase === \"indexing\";\n\t}\n\n\tprivate setState(state: EmbsearchState): void {\n\t\tthis.state = state;\n\t\tthis.options.onProgress?.(state);\n\t}\n\n\tprivate async resolveBinary(): Promise<string | undefined> {\n\t\tif (this.options.binaryPath) {\n\t\t\treturn this.options.binaryPath;\n\t\t}\n\t\t// Surface the on-demand binary download through the same progress channel as\n\t\t// indexing, so the first-run fetch renders a progress bar instead of a stall.\n\t\t// A cached binary resolves without ever invoking this callback.\n\t\treturn await ensureTool(\"embsearch\", true, (receivedBytes, totalBytes) => {\n\t\t\tthis.setState({ phase: \"downloading\", receivedBytes, totalBytes });\n\t\t});\n\t}\n\n\t/**\n\t * Scan, threshold-check, and (when needed) index in the background.\n\t * Resolves when indexing completes or the feature settles dormant.\n\t */\n\tasync start(signal?: AbortSignal): Promise<void> {\n\t\ttry {\n\t\t\tawait this.run(signal);\n\t\t} catch (e) {\n\t\t\tconst reason = e instanceof Error ? e.message : String(e);\n\t\t\tthis.setState({ phase: \"unavailable\", reason });\n\t\t\tawait this.closeClient();\n\t\t}\n\t}\n\n\tprivate async run(signal?: AbortSignal): Promise<void> {\n\t\tconst binary = await this.resolveBinary();\n\t\tif (!binary) {\n\t\t\tthis.setState({\n\t\t\t\tphase: \"unavailable\",\n\t\t\t\treason: \"embsearch binary not found (PATH or embsearchBinaryPath setting)\",\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\tconst scan = scanRepo(this.options.cwd, signal);\n\t\tif (scan.totalBytes < this.options.thresholdBytes) {\n\t\t\tthis.setState({\n\t\t\t\tphase: \"skipped\",\n\t\t\t\treason: `repo under threshold (${scan.totalBytes} < ${this.options.thresholdBytes} bytes)`,\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\tconst storeDir = getEmbsearchStoreDir(this.options.cwd);\n\t\tthis.client = new EmbSearchClient({ binaryPath: binary, storePath: getVectorStoreDir(storeDir) });\n\t\tawait this.client.ready();\n\n\t\tconst info = await this.client.info();\n\t\tif (info.modelId === MOCK_MODEL_ID) {\n\t\t\tthrow new Error(\"embsearch binary uses the mock embedder (not semantic); install an onnx build\");\n\t\t}\n\n\t\t// Missing/stale sidecar (format, chunker, or model changed) → clean rebuild.\n\t\tthis.meta = loadIndexMeta(storeDir, info.modelId) ?? emptyIndexMeta(this.options.cwd, info.modelId);\n\t\tthis.meta.lastUsedMs = Date.now();\n\n\t\tawait this.indexChangedFiles(scan.files, storeDir, signal);\n\t}\n\n\tprivate async indexChangedFiles(files: RepoScanFile[], storeDir: string, signal?: AbortSignal): Promise<void> {\n\t\tconst meta = this.meta!;\n\t\tconst client = this.client!;\n\n\t\t// Diff scan vs sidecar: cheap mtime+size check first, hash only on delta.\n\t\tconst toIndex: Array<{ file: RepoScanFile; content: string; hash: string }> = [];\n\t\tconst seen = new Set<string>();\n\t\tfor (const file of files) {\n\t\t\tseen.add(file.rel);\n\t\t\tconst known = meta.files[file.rel];\n\t\t\tif (known && known.mtimeMs === file.mtimeMs && known.size === file.size) continue;\n\t\t\tlet content: string;\n\t\t\ttry {\n\t\t\t\tcontent = readFileSync(file.abs, \"utf-8\");\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst hash = hashContent(content);\n\t\t\tif (known && known.hash === hash) {\n\t\t\t\t// Touched but unchanged — refresh stat info only.\n\t\t\t\tknown.mtimeMs = file.mtimeMs;\n\t\t\t\tknown.size = file.size;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\ttoIndex.push({ file, content, hash });\n\t\t}\n\t\tconst toRemove = Object.keys(meta.files).filter((rel) => !seen.has(rel));\n\n\t\tif (toIndex.length === 0 && toRemove.length === 0) {\n\t\t\tsaveIndexMeta(storeDir, meta);\n\t\t\tthis.setState({ phase: \"ready\", chunkCount: this.countChunks(meta) });\n\t\t\treturn;\n\t\t}\n\n\t\t// Chunk changed files; count total upserts for exact progress.\n\t\tconst work: Array<{ rel: string; fileMeta: FileMeta; chunks: Array<{ id: string; text: string }> }> = [];\n\t\tlet totalChunks = 0;\n\t\tfor (const { file, content, hash } of toIndex) {\n\t\t\tconst chunks = chunkFile(file.rel, content);\n\t\t\twork.push({\n\t\t\t\trel: file.rel,\n\t\t\t\tfileMeta: {\n\t\t\t\t\tmtimeMs: file.mtimeMs,\n\t\t\t\t\tsize: file.size,\n\t\t\t\t\thash,\n\t\t\t\t\tchunks: chunks.map((c) => [c.startLine, c.endLine]),\n\t\t\t\t},\n\t\t\t\tchunks: chunks.map((c) => ({ id: c.id, text: c.text })),\n\t\t\t});\n\t\t\ttotalChunks += chunks.length;\n\t\t}\n\n\t\tthis.setState({ phase: \"indexing\", done: 0, total: totalChunks });\n\n\t\t// Drop vectors of deleted files and superseded chunk tails.\n\t\tfor (const rel of toRemove) {\n\t\t\tfor (let i = 0; i < meta.files[rel].chunks.length; i++) await client.remove(`${rel}#${i}`);\n\t\t\tdelete meta.files[rel];\n\t\t}\n\n\t\tlet done = 0;\n\t\tfor (const item of work) {\n\t\t\tif (signal?.aborted || this.disposed) return;\n\t\t\tconst oldChunkCount = meta.files[item.rel]?.chunks.length ?? 0;\n\t\t\t// Remove old chunks beyond the new count (upsert covers the rest).\n\t\t\tfor (let i = item.chunks.length; i < oldChunkCount; i++) await client.remove(`${item.rel}#${i}`);\n\n\t\t\tfor (let offset = 0; offset < item.chunks.length; offset += BULK_BATCH_SIZE) {\n\t\t\t\tif (signal?.aborted || this.disposed) return;\n\t\t\t\tconst batch = item.chunks.slice(offset, offset + BULK_BATCH_SIZE);\n\t\t\t\tawait client.bulk(batch);\n\t\t\t\tdone += batch.length;\n\t\t\t\tthis.setState({ phase: \"indexing\", done, total: totalChunks });\n\t\t\t\t// Yield so queries and the event loop stay responsive.\n\t\t\t\tawait new Promise((resolve) => setTimeout(resolve, BATCH_YIELD_MS));\n\t\t\t}\n\t\t\tmeta.files[item.rel] = item.fileMeta;\n\t\t}\n\n\t\tawait client.compact();\n\t\tawait client.save();\n\t\tsaveIndexMeta(storeDir, meta);\n\t\tthis.setState({ phase: \"ready\", chunkCount: this.countChunks(meta) });\n\t}\n\n\tprivate countChunks(meta: IndexMeta): number {\n\t\tlet n = 0;\n\t\tfor (const rel of Object.keys(meta.files)) n += meta.files[rel].chunks.length;\n\t\treturn n;\n\t}\n\n\t/** Top-`k` semantic hits as `path` + line range + score. */\n\tasync search(query: string, k = 10): Promise<SemanticHit[]> {\n\t\treturn await this.searchChunks(query, k);\n\t}\n\n\t/** Top-`k` semantic hits including their chunk ids, for rank fusion. */\n\tasync searchChunks(query: string, k = 10, glob?: string): Promise<SemanticChunkHit[]> {\n\t\tif (!this.client || this.client.isClosed || !this.meta) {\n\t\t\tthrow new Error(\"semantic index is not available\");\n\t\t}\n\t\tconst results = await this.client.query(query, k);\n\t\tconst hits: SemanticChunkHit[] = [];\n\t\tconst matchGlob = (rel: string): boolean => {\n\t\t\tif (!glob) return true;\n\t\t\treturn minimatch(rel, glob, { dot: true, matchBase: !glob.includes(\"/\") });\n\t\t};\n\t\tfor (const result of results) {\n\t\t\tconst sep = result.id.lastIndexOf(\"#\");\n\t\t\tif (sep === -1) continue;\n\t\t\tconst rel = result.id.slice(0, sep);\n\t\t\tif (!matchGlob(rel)) continue;\n\t\t\tconst chunkIndex = Number.parseInt(result.id.slice(sep + 1), 10);\n\t\t\tconst range = this.meta.files[rel]?.chunks[chunkIndex];\n\t\t\tif (!range) continue;\n\t\t\thits.push({ id: result.id, path: rel, startLine: range[0], endLine: range[1], score: result.score });\n\t\t}\n\t\treturn hits;\n\t}\n\n\t/**\n\t * Resolve a repo-relative path + line to its enclosing indexed chunk, or\n\t * undefined when the file/line is not covered by the index. Chunks overlap\n\t * by a few lines; the first (lowest-index) containing chunk wins so the\n\t * mapping is deterministic.\n\t */\n\tfindEnclosingChunk(\n\t\trel: string,\n\t\tline: number,\n\t): { id: string; path: string; startLine: number; endLine: number } | undefined {\n\t\tconst file = this.meta?.files[rel];\n\t\tif (!file) return undefined;\n\t\tfor (let i = 0; i < file.chunks.length; i++) {\n\t\t\tconst [startLine, endLine] = file.chunks[i];\n\t\t\tif (line >= startLine && line <= endLine) {\n\t\t\t\treturn { id: `${rel}#${i}`, path: rel, startLine, endLine };\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\tprivate async closeClient(): Promise<void> {\n\t\tconst client = this.client;\n\t\tthis.client = undefined;\n\t\tif (client && !client.isClosed) {\n\t\t\ttry {\n\t\t\t\tawait client.close();\n\t\t\t} catch {\n\t\t\t\t// already dead\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Persist state and shut the daemon down. Safe to call twice. */\n\tasync dispose(): Promise<void> {\n\t\tif (this.disposed) return;\n\t\tthis.disposed = true;\n\t\tif (this.client && !this.client.isClosed) {\n\t\t\ttry {\n\t\t\t\tawait this.client.save();\n\t\t\t} catch {\n\t\t\t\t// daemon may have exited; nothing to save\n\t\t\t}\n\t\t}\n\t\tawait this.closeClient();\n\t}\n}\n\n// --- Per-cwd service registry ---\n//\n// The search tool is constructed by the generic tool factory table and\n// only receives `cwd`; the service is created later during session init (flag\n// gated). This registry connects the two without threading a service instance\n// through every layer between main.ts and the tool factories.\n\nconst services = new Map<string, EmbsearchService>();\n\nexport function registerEmbsearchService(cwd: string, service: EmbsearchService): void {\n\tconst old = services.get(cwd);\n\tif (old && old !== service) {\n\t\told.dispose().catch(() => {});\n\t}\n\tservices.set(cwd, service);\n}\n\nexport function getEmbsearchService(cwd: string): EmbsearchService | undefined {\n\treturn services.get(cwd);\n}\n\nexport function unregisterEmbsearchService(cwd: string): void {\n\tservices.delete(cwd);\n}\n"]}
1
+ {"version":3,"file":"embsearch-service.js","sourceRoot":"","sources":["../../../src/core/embsearch/embsearch-service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Orchestrates semantic indexing and search for a repository.\n *\n * Lifecycle (all behind --enable-embsearchtools):\n * 1. `start()` — resolve the embsearch binary, scan the repo (ignore-aware),\n * apply the byte threshold. Under threshold → dormant. Over → spawn the\n * daemon, verify the backend is not the mock embedder, then index changed\n * files in the background in small batches, reporting progress.\n * 2. `search()` — top-k semantic query, mapping chunk ids back to\n * `path:start-end` via the sidecar metadata.\n * 3. `dispose()` — save + close the daemon.\n *\n * Every failure degrades to `unavailable` with a reason; nothing here ever\n * blocks session startup or affects grep/find.\n */\n\nimport { execFileSync } from \"child_process\";\nimport { readFileSync } from \"fs\";\nimport { minimatch } from \"minimatch\";\nimport { ensureTool } from \"../../utils/tools-manager.js\";\nimport { chunkFile } from \"./chunker.js\";\nimport {\n\ttype DaemonRetriever,\n\tEmbSearchClient,\n\ttype EmbSearchRerankPassage,\n\ttype EmbSearchRerankResult,\n} from \"./client.js\";\nimport {\n\temptyIndexMeta,\n\ttype FileMeta,\n\tgetEmbsearchStoreDir,\n\tgetVectorStoreDir,\n\thashContent,\n\ttype IndexMeta,\n\tloadIndexMeta,\n\tsaveIndexMeta,\n} from \"./index-meta.js\";\nimport { type RepoScanFile, scanRepo } from \"./repo-scan.js\";\n\n/** Chunks per bulk request. Small enough that a concurrent query is never\n * stuck long behind one padded batch inference. */\nconst BULK_BATCH_SIZE = 48;\n/** Yield between batches so background indexing doesn't starve the session. */\nconst BATCH_YIELD_MS = 15;\n/** The Rust mock backend's model id — semantically meaningless, never index with it. */\nconst MOCK_MODEL_ID = \"mock-hash-v1\";\n/**\n * First embsearch release serving `retriever: \"lexical\"`.\n *\n * The guard matters because the daemon does not reject unknown request fields:\n * an older binary silently ignores `retriever` and answers with dense results.\n * Fusing that list a second time as a \"bm25\" leg would double-count it and\n * corrupt the ranking with no error anywhere — so refuse instead of degrading.\n */\nconst MIN_LEXICAL_RETRIEVER_VERSION = [0, 2, 0] as const;\n/** First embsearch release bundling cross-encoder reranker weights. */\nconst MIN_RERANK_VERSION = [0, 3, 0] as const;\n\n/** `embsearch 0.2.0` -> [0, 2, 0]; undefined when it cannot be parsed. */\nfunction parseBinaryVersion(output: string): number[] | undefined {\n\tconst match = output.match(/(\\d+)\\.(\\d+)\\.(\\d+)/);\n\treturn match ? [Number(match[1]), Number(match[2]), Number(match[3])] : undefined;\n}\n\nfunction atLeast(version: readonly number[], minimum: readonly number[]): boolean {\n\tfor (let i = 0; i < minimum.length; i++) {\n\t\tconst part = version[i] ?? 0;\n\t\tif (part !== minimum[i]) return part > minimum[i];\n\t}\n\treturn true;\n}\n\nexport type EmbsearchState =\n\t| { phase: \"idle\" }\n\t| { phase: \"skipped\"; reason: string }\n\t| { phase: \"downloading\"; receivedBytes: number; totalBytes: number | null }\n\t| { phase: \"indexing\"; done: number; total: number }\n\t| { phase: \"ready\"; chunkCount: number }\n\t| { phase: \"unavailable\"; reason: string };\n\nexport interface EmbsearchServiceOptions {\n\tcwd: string;\n\t/** Explicit binary path (settings override). Default: \"embsearch\" from PATH. */\n\tbinaryPath?: string;\n\t/** Minimum indexable bytes before indexing kicks in. */\n\tthresholdBytes: number;\n\t/**\n\t * Override the store location. Only the eval harness sets this, so a\n\t * second index (e.g. a BM25-hybrid store) can exist for the same repo\n\t * without colliding with the primary one.\n\t */\n\tstoreDir?: string;\n\t/**\n\t * Create the store with the daemon's BM25 lexical index. Fixed at store\n\t * creation, so this must pair with a distinct `storeDir`.\n\t */\n\thybridStore?: boolean;\n\t/** Progress callback for UI (footer / stderr lines). */\n\tonProgress?: (state: EmbsearchState) => void;\n}\n\nexport interface SemanticHit {\n\tpath: string;\n\tstartLine: number;\n\tendLine: number;\n\tscore: number;\n}\n\nexport interface SemanticChunkHit extends SemanticHit {\n\t/** Per-build chunk id (`relpath#index`) — the fusion identity for hybrid search. */\n\tid: string;\n}\n\nexport class EmbsearchService {\n\tprivate readonly options: EmbsearchServiceOptions;\n\tprivate client: EmbSearchClient | undefined;\n\tprivate meta: IndexMeta | undefined;\n\tprivate state: EmbsearchState = { phase: \"idle\" };\n\tprivate disposed = false;\n\t/** Whether the resolved binary serves `retriever: \"lexical\"`. */\n\tprivate lexicalRetriever = false;\n\t/** Whether the resolved binary serves the cross-encoder `rerank` op. */\n\tprivate crossEncoder = false;\n\n\tconstructor(options: EmbsearchServiceOptions) {\n\t\tthis.options = options;\n\t}\n\n\tgetState(): EmbsearchState {\n\t\treturn this.state;\n\t}\n\n\t/** Semantic search is usable (index ready, or still building with partial data). */\n\tisAvailable(): boolean {\n\t\treturn this.state.phase === \"ready\" || this.state.phase === \"indexing\";\n\t}\n\n\tprivate setState(state: EmbsearchState): void {\n\t\tthis.state = state;\n\t\tthis.options.onProgress?.(state);\n\t}\n\n\t/** Whether the running daemon can serve a BM25-only query. */\n\tsupportsLexicalRetriever(): boolean {\n\t\treturn this.lexicalRetriever;\n\t}\n\n\t/** Whether the running daemon can score with a cross-encoder. */\n\tsupportsCrossEncoder(): boolean {\n\t\treturn this.crossEncoder;\n\t}\n\n\t/**\n\t * Cross-encoder rerank of caller-supplied passages.\n\t *\n\t * Unlike the retrievers this does not consult the index at all — it scores\n\t * exactly the text passed in, which is why the caller sends its expanded\n\t * windows rather than chunk ids.\n\t */\n\tasync rerank(query: string, passages: EmbSearchRerankPassage[], k: number): Promise<EmbSearchRerankResult[]> {\n\t\tif (!this.client || this.client.isClosed) {\n\t\t\tthrow new Error(\"semantic index is not available\");\n\t\t}\n\t\tif (!this.crossEncoder) {\n\t\t\tthrow new Error(\n\t\t\t\t`embsearch is too old to rerank (needs >= ${MIN_RERANK_VERSION.join(\".\")}); ` +\n\t\t\t\t\t\"an older daemon has no cross-encoder weights\",\n\t\t\t);\n\t\t}\n\t\treturn await this.client.rerank(query, passages, k);\n\t}\n\n\tprivate probeBinaryVersion(binary: string): number[] | undefined {\n\t\ttry {\n\t\t\treturn parseBinaryVersion(execFileSync(binary, [\"--version\"], { encoding: \"utf-8\", timeout: 10_000 }));\n\t\t} catch {\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tprivate async resolveBinary(): Promise<string | undefined> {\n\t\tif (this.options.binaryPath) {\n\t\t\treturn this.options.binaryPath;\n\t\t}\n\t\t// Surface the on-demand binary download through the same progress channel as\n\t\t// indexing, so the first-run fetch renders a progress bar instead of a stall.\n\t\t// A cached binary resolves without ever invoking this callback.\n\t\treturn await ensureTool(\"embsearch\", true, (receivedBytes, totalBytes) => {\n\t\t\tthis.setState({ phase: \"downloading\", receivedBytes, totalBytes });\n\t\t});\n\t}\n\n\t/**\n\t * Scan, threshold-check, and (when needed) index in the background.\n\t * Resolves when indexing completes or the feature settles dormant.\n\t */\n\tasync start(signal?: AbortSignal): Promise<void> {\n\t\ttry {\n\t\t\tawait this.run(signal);\n\t\t} catch (e) {\n\t\t\tconst reason = e instanceof Error ? e.message : String(e);\n\t\t\tthis.setState({ phase: \"unavailable\", reason });\n\t\t\tawait this.closeClient();\n\t\t}\n\t}\n\n\tprivate async run(signal?: AbortSignal): Promise<void> {\n\t\tconst binary = await this.resolveBinary();\n\t\tif (!binary) {\n\t\t\tthis.setState({\n\t\t\t\tphase: \"unavailable\",\n\t\t\t\treason: \"embsearch binary not found (PATH or embsearchBinaryPath setting)\",\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\tconst scan = scanRepo(this.options.cwd, signal);\n\t\tif (scan.totalBytes < this.options.thresholdBytes) {\n\t\t\tthis.setState({\n\t\t\t\tphase: \"skipped\",\n\t\t\t\treason: `repo under threshold (${scan.totalBytes} < ${this.options.thresholdBytes} bytes)`,\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\tconst binaryVersion = this.probeBinaryVersion(binary);\n\t\tthis.lexicalRetriever = binaryVersion !== undefined && atLeast(binaryVersion, MIN_LEXICAL_RETRIEVER_VERSION);\n\t\tthis.crossEncoder = binaryVersion !== undefined && atLeast(binaryVersion, MIN_RERANK_VERSION);\n\n\t\tconst storeDir = this.options.storeDir ?? getEmbsearchStoreDir(this.options.cwd);\n\t\tthis.client = new EmbSearchClient({\n\t\t\tbinaryPath: binary,\n\t\t\tstorePath: getVectorStoreDir(storeDir),\n\t\t\thybrid: this.options.hybridStore,\n\t\t});\n\t\tawait this.client.ready();\n\n\t\tconst info = await this.client.info();\n\t\tif (info.modelId === MOCK_MODEL_ID) {\n\t\t\tthrow new Error(\"embsearch binary uses the mock embedder (not semantic); install an onnx build\");\n\t\t}\n\n\t\t// Missing/stale sidecar (format, chunker, or model changed) → clean rebuild.\n\t\tthis.meta = loadIndexMeta(storeDir, info.modelId) ?? emptyIndexMeta(this.options.cwd, info.modelId);\n\t\tthis.meta.lastUsedMs = Date.now();\n\n\t\tawait this.indexChangedFiles(scan.files, storeDir, signal);\n\t}\n\n\tprivate async indexChangedFiles(files: RepoScanFile[], storeDir: string, signal?: AbortSignal): Promise<void> {\n\t\tconst meta = this.meta!;\n\t\tconst client = this.client!;\n\n\t\t// Diff scan vs sidecar: cheap mtime+size check first, hash only on delta.\n\t\tconst toIndex: Array<{ file: RepoScanFile; content: string; hash: string }> = [];\n\t\tconst seen = new Set<string>();\n\t\tfor (const file of files) {\n\t\t\tseen.add(file.rel);\n\t\t\tconst known = meta.files[file.rel];\n\t\t\tif (known && known.mtimeMs === file.mtimeMs && known.size === file.size) continue;\n\t\t\tlet content: string;\n\t\t\ttry {\n\t\t\t\tcontent = readFileSync(file.abs, \"utf-8\");\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst hash = hashContent(content);\n\t\t\tif (known && known.hash === hash) {\n\t\t\t\t// Touched but unchanged — refresh stat info only.\n\t\t\t\tknown.mtimeMs = file.mtimeMs;\n\t\t\t\tknown.size = file.size;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\ttoIndex.push({ file, content, hash });\n\t\t}\n\t\tconst toRemove = Object.keys(meta.files).filter((rel) => !seen.has(rel));\n\n\t\tif (toIndex.length === 0 && toRemove.length === 0) {\n\t\t\tsaveIndexMeta(storeDir, meta);\n\t\t\tthis.setState({ phase: \"ready\", chunkCount: this.countChunks(meta) });\n\t\t\treturn;\n\t\t}\n\n\t\t// Chunk changed files; count total upserts for exact progress.\n\t\tconst work: Array<{ rel: string; fileMeta: FileMeta; chunks: Array<{ id: string; text: string }> }> = [];\n\t\tlet totalChunks = 0;\n\t\tfor (const { file, content, hash } of toIndex) {\n\t\t\tconst chunks = chunkFile(file.rel, content);\n\t\t\twork.push({\n\t\t\t\trel: file.rel,\n\t\t\t\tfileMeta: {\n\t\t\t\t\tmtimeMs: file.mtimeMs,\n\t\t\t\t\tsize: file.size,\n\t\t\t\t\thash,\n\t\t\t\t\tchunks: chunks.map((c) => [c.startLine, c.endLine]),\n\t\t\t\t},\n\t\t\t\tchunks: chunks.map((c) => ({ id: c.id, text: c.text })),\n\t\t\t});\n\t\t\ttotalChunks += chunks.length;\n\t\t}\n\n\t\tthis.setState({ phase: \"indexing\", done: 0, total: totalChunks });\n\n\t\t// Drop vectors of deleted files and superseded chunk tails.\n\t\tfor (const rel of toRemove) {\n\t\t\tfor (let i = 0; i < meta.files[rel].chunks.length; i++) await client.remove(`${rel}#${i}`);\n\t\t\tdelete meta.files[rel];\n\t\t}\n\n\t\tlet done = 0;\n\t\tfor (const item of work) {\n\t\t\tif (signal?.aborted || this.disposed) return;\n\t\t\tconst oldChunkCount = meta.files[item.rel]?.chunks.length ?? 0;\n\t\t\t// Remove old chunks beyond the new count (upsert covers the rest).\n\t\t\tfor (let i = item.chunks.length; i < oldChunkCount; i++) await client.remove(`${item.rel}#${i}`);\n\n\t\t\tfor (let offset = 0; offset < item.chunks.length; offset += BULK_BATCH_SIZE) {\n\t\t\t\tif (signal?.aborted || this.disposed) return;\n\t\t\t\tconst batch = item.chunks.slice(offset, offset + BULK_BATCH_SIZE);\n\t\t\t\tawait client.bulk(batch);\n\t\t\t\tdone += batch.length;\n\t\t\t\tthis.setState({ phase: \"indexing\", done, total: totalChunks });\n\t\t\t\t// Yield so queries and the event loop stay responsive.\n\t\t\t\tawait new Promise((resolve) => setTimeout(resolve, BATCH_YIELD_MS));\n\t\t\t}\n\t\t\tmeta.files[item.rel] = item.fileMeta;\n\t\t}\n\n\t\tawait client.compact();\n\t\tawait client.save();\n\t\tsaveIndexMeta(storeDir, meta);\n\t\tthis.setState({ phase: \"ready\", chunkCount: this.countChunks(meta) });\n\t}\n\n\tprivate countChunks(meta: IndexMeta): number {\n\t\tlet n = 0;\n\t\tfor (const rel of Object.keys(meta.files)) n += meta.files[rel].chunks.length;\n\t\treturn n;\n\t}\n\n\t/** Top-`k` semantic hits as `path` + line range + score. */\n\tasync search(query: string, k = 10): Promise<SemanticHit[]> {\n\t\treturn await this.searchChunks(query, k);\n\t}\n\n\t/** Top-`k` semantic hits including their chunk ids, for rank fusion. */\n\tasync searchChunks(\n\t\tquery: string,\n\t\tk = 10,\n\t\tglob?: string,\n\t\tretriever: DaemonRetriever = \"dense\",\n\t): Promise<SemanticChunkHit[]> {\n\t\tif (!this.client || this.client.isClosed || !this.meta) {\n\t\t\tthrow new Error(\"semantic index is not available\");\n\t\t}\n\t\tif (retriever === \"lexical\" && !this.lexicalRetriever) {\n\t\t\tthrow new Error(\n\t\t\t\t`embsearch is too old for retriever \"lexical\" (needs >= ${MIN_LEXICAL_RETRIEVER_VERSION.join(\".\")}); ` +\n\t\t\t\t\t\"an older daemon ignores the field and answers with dense results\",\n\t\t\t);\n\t\t}\n\t\tconst results = await this.client.query(query, k, retriever);\n\t\tconst hits: SemanticChunkHit[] = [];\n\t\tconst matchGlob = (rel: string): boolean => {\n\t\t\tif (!glob) return true;\n\t\t\treturn minimatch(rel, glob, { dot: true, matchBase: !glob.includes(\"/\") });\n\t\t};\n\t\tfor (const result of results) {\n\t\t\tconst sep = result.id.lastIndexOf(\"#\");\n\t\t\tif (sep === -1) continue;\n\t\t\tconst rel = result.id.slice(0, sep);\n\t\t\tif (!matchGlob(rel)) continue;\n\t\t\tconst chunkIndex = Number.parseInt(result.id.slice(sep + 1), 10);\n\t\t\tconst range = this.meta.files[rel]?.chunks[chunkIndex];\n\t\t\tif (!range) continue;\n\t\t\thits.push({ id: result.id, path: rel, startLine: range[0], endLine: range[1], score: result.score });\n\t\t}\n\t\treturn hits;\n\t}\n\n\t/**\n\t * Resolve a repo-relative path + line to its enclosing indexed chunk, or\n\t * undefined when the file/line is not covered by the index. Chunks overlap\n\t * by a few lines; the first (lowest-index) containing chunk wins so the\n\t * mapping is deterministic.\n\t */\n\tfindEnclosingChunk(\n\t\trel: string,\n\t\tline: number,\n\t): { id: string; path: string; startLine: number; endLine: number } | undefined {\n\t\tconst file = this.meta?.files[rel];\n\t\tif (!file) return undefined;\n\t\tfor (let i = 0; i < file.chunks.length; i++) {\n\t\t\tconst [startLine, endLine] = file.chunks[i];\n\t\t\tif (line >= startLine && line <= endLine) {\n\t\t\t\treturn { id: `${rel}#${i}`, path: rel, startLine, endLine };\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\tprivate async closeClient(): Promise<void> {\n\t\tconst client = this.client;\n\t\tthis.client = undefined;\n\t\tif (client && !client.isClosed) {\n\t\t\ttry {\n\t\t\t\tawait client.close();\n\t\t\t} catch {\n\t\t\t\t// already dead\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Persist state and shut the daemon down. Safe to call twice. */\n\tasync dispose(): Promise<void> {\n\t\tif (this.disposed) return;\n\t\tthis.disposed = true;\n\t\tif (this.client && !this.client.isClosed) {\n\t\t\ttry {\n\t\t\t\tawait this.client.save();\n\t\t\t} catch {\n\t\t\t\t// daemon may have exited; nothing to save\n\t\t\t}\n\t\t}\n\t\tawait this.closeClient();\n\t}\n}\n\n// --- Per-cwd service registry ---\n//\n// The search tool is constructed by the generic tool factory table and\n// only receives `cwd`; the service is created later during session init (flag\n// gated). This registry connects the two without threading a service instance\n// through every layer between main.ts and the tool factories.\n\nconst services = new Map<string, EmbsearchService>();\n\nexport function registerEmbsearchService(cwd: string, service: EmbsearchService): void {\n\tconst old = services.get(cwd);\n\tif (old && old !== service) {\n\t\told.dispose().catch(() => {});\n\t}\n\tservices.set(cwd, service);\n}\n\nexport function getEmbsearchService(cwd: string): EmbsearchService | undefined {\n\treturn services.get(cwd);\n}\n\nexport function unregisterEmbsearchService(cwd: string): void {\n\tservices.delete(cwd);\n}\n"]}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Cross-encoder reranking via the embsearch daemon.
3
+ *
4
+ * The deterministic reranker in `rerank.ts` scores candidates with lexical
5
+ * evidence — term coverage, path affinity, whether the window declares a query
6
+ * term. That took Recall@1 from 0.10 to 0.24 and then stopped paying: a
7
+ * term-proximity signal aimed at the classes it still handles worst moved
8
+ * nothing (p = 1.00 on every metric).
9
+ *
10
+ * What is left is a ranking problem the lexical view cannot see. Across the
11
+ * 62-query gold set the right span reaches the fused top-50 far more often
12
+ * than the top-10, so the candidates are in hand and merely ordered badly. A
13
+ * cross-encoder reads the query and the candidate *together*, which is exactly
14
+ * the evidence term counting lacks.
15
+ *
16
+ * It is also far more expensive — one model pass per candidate, with no
17
+ * precomputation possible — so it runs over a shortlist and its depth is
18
+ * capped separately from the fused window.
19
+ */
20
+ import type { EmbsearchService } from "../embsearch/embsearch-service.js";
21
+ import type { FusedCandidate } from "./types.js";
22
+ /**
23
+ * Candidates sent to the cross-encoder per query.
24
+ *
25
+ * Each one is a model pass, so this is a latency dial, not a quality dial:
26
+ * every candidate past here keeps its fused order rather than being scored.
27
+ */
28
+ export declare const CROSS_ENCODER_DEPTH = 30;
29
+ export interface CrossRerankResult {
30
+ candidates: FusedCandidate[];
31
+ latencyMs: number;
32
+ /** How many candidates the model actually scored. */
33
+ scored: number;
34
+ }
35
+ /**
36
+ * Reorder `candidates` by cross-encoder relevance.
37
+ *
38
+ * Only the first {@link CROSS_ENCODER_DEPTH} are scored; the remainder keep
39
+ * their incoming order and follow. Candidates whose window cannot be read are
40
+ * left unscored for the same reason — there is no text to give the model, and
41
+ * inventing one would score a fiction.
42
+ */
43
+ export declare function crossEncoderRerank(query: string, candidates: readonly FusedCandidate[], cwd: string, service: EmbsearchService): Promise<CrossRerankResult>;
44
+ //# sourceMappingURL=cross-rerank.d.ts.map
@@ -0,0 +1 @@
1
+ {"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"]}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Cross-encoder reranking via the embsearch daemon.
3
+ *
4
+ * The deterministic reranker in `rerank.ts` scores candidates with lexical
5
+ * evidence — term coverage, path affinity, whether the window declares a query
6
+ * term. That took Recall@1 from 0.10 to 0.24 and then stopped paying: a
7
+ * term-proximity signal aimed at the classes it still handles worst moved
8
+ * nothing (p = 1.00 on every metric).
9
+ *
10
+ * What is left is a ranking problem the lexical view cannot see. Across the
11
+ * 62-query gold set the right span reaches the fused top-50 far more often
12
+ * than the top-10, so the candidates are in hand and merely ordered badly. A
13
+ * cross-encoder reads the query and the candidate *together*, which is exactly
14
+ * the evidence term counting lacks.
15
+ *
16
+ * It is also far more expensive — one model pass per candidate, with no
17
+ * precomputation possible — so it runs over a shortlist and its depth is
18
+ * capped separately from the fused window.
19
+ */
20
+ import { readCandidateWindows } from "./rerank.js";
21
+ /**
22
+ * Candidates sent to the cross-encoder per query.
23
+ *
24
+ * Each one is a model pass, so this is a latency dial, not a quality dial:
25
+ * every candidate past here keeps its fused order rather than being scored.
26
+ */
27
+ export const CROSS_ENCODER_DEPTH = 30;
28
+ /**
29
+ * Reorder `candidates` by cross-encoder relevance.
30
+ *
31
+ * Only the first {@link CROSS_ENCODER_DEPTH} are scored; the remainder keep
32
+ * their incoming order and follow. Candidates whose window cannot be read are
33
+ * left unscored for the same reason — there is no text to give the model, and
34
+ * inventing one would score a fiction.
35
+ */
36
+ export async function crossEncoderRerank(query, candidates, cwd, service) {
37
+ const startedMs = Date.now();
38
+ if (candidates.length < 2) {
39
+ return { candidates: [...candidates], latencyMs: Date.now() - startedMs, scored: 0 };
40
+ }
41
+ const head = candidates.slice(0, CROSS_ENCODER_DEPTH);
42
+ const tail = candidates.slice(CROSS_ENCODER_DEPTH);
43
+ const windows = readCandidateWindows(head, cwd);
44
+ const passages = [];
45
+ const byId = new Map();
46
+ head.forEach((candidate, i) => {
47
+ const text = windows[i];
48
+ if (text === undefined)
49
+ return;
50
+ // Fused ids are unique per query, so they round-trip as passage ids.
51
+ passages.push({ id: candidate.id, text });
52
+ byId.set(candidate.id, candidate);
53
+ });
54
+ if (passages.length === 0) {
55
+ return { candidates: [...candidates], latencyMs: Date.now() - startedMs, scored: 0 };
56
+ }
57
+ const scored = await service.rerank(query, passages, passages.length);
58
+ // Reassemble: scored candidates in model order, then anything the model did
59
+ // not see, in fused order. An id the daemon did not return would otherwise
60
+ // vanish from the results entirely.
61
+ const ordered = [];
62
+ const seen = new Set();
63
+ for (const result of scored) {
64
+ const candidate = byId.get(result.id);
65
+ if (candidate && !seen.has(result.id)) {
66
+ ordered.push(candidate);
67
+ seen.add(result.id);
68
+ }
69
+ }
70
+ for (const candidate of head) {
71
+ if (!seen.has(candidate.id))
72
+ ordered.push(candidate);
73
+ }
74
+ ordered.push(...tail);
75
+ return { candidates: ordered, latencyMs: Date.now() - startedMs, scored: passages.length };
76
+ }
77
+ //# sourceMappingURL=cross-rerank.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cross-rerank.js","sourceRoot":"","sources":["../../../src/core/search/cross-rerank.ts"],"names":[],"mappings":"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","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"]}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Paired comparison between two configs in one run record.
3
+ *
4
+ * Aggregate means are the wrong instrument for "should we change this
5
+ * default". Both configs score the *same* queries, so the informative
6
+ * question is per-query: how many did B improve, how many did it hurt, and
7
+ * could that split have come from chance? A 3pp mean gap can be two queries
8
+ * out of sixty moving, which is not a reason to change anything.
9
+ *
10
+ * Reports a two-sided sign test over the queries where the two configs
11
+ * differ. Ties carry no information about direction and are excluded, which
12
+ * is what makes the test paired.
13
+ */
14
+ import type { EvalQueryResult } from "./eval.js";
15
+ /** Per-query metrics that can be compared. */
16
+ export type ComparableMetric = "recallAt1" | "recallAt5" | "recallAt10" | "recallAt50" | "mrr";
17
+ export interface PairedQueryDelta {
18
+ id: string;
19
+ class: string;
20
+ before: number;
21
+ after: number;
22
+ }
23
+ export interface PairedComparison {
24
+ metric: ComparableMetric;
25
+ before: string;
26
+ after: string;
27
+ better: number;
28
+ worse: number;
29
+ tied: number;
30
+ meanBefore: number;
31
+ meanAfter: number;
32
+ /** Two-sided sign-test p-value over non-tied queries. 1 when all tie. */
33
+ pValue: number;
34
+ deltas: PairedQueryDelta[];
35
+ }
36
+ export interface ComparableRun {
37
+ perQuery: Array<{
38
+ id: string;
39
+ class: string;
40
+ results: EvalQueryResult[];
41
+ }>;
42
+ }
43
+ /** Two-sided sign test: probability of a split at least this lopsided under
44
+ * a fair coin. */
45
+ export declare function signTestPValue(better: number, worse: number): number;
46
+ /**
47
+ * Pair the *same* config across two run records, so "did my change help?" is
48
+ * answerable at all. `comparePaired` compares two configs inside one run;
49
+ * this compares one config before and after a retrieval change.
50
+ *
51
+ * Only sound when both runs scored the same corpus and the same gold set —
52
+ * otherwise the delta includes corpus drift. The caller checks provenance.
53
+ */
54
+ export declare function mergeRunsForComparison(before: ComparableRun, after: ComparableRun, label: string): ComparableRun;
55
+ export declare function comparePaired(run: ComparableRun, before: string, after: string, metric: ComparableMetric): PairedComparison;
56
+ export declare function formatComparison(comparison: PairedComparison): string;
57
+ //# sourceMappingURL=eval-compare.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eval-compare.d.ts","sourceRoot":"","sources":["../../../src/core/search/eval-compare.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAEjD,8CAA8C;AAC9C,MAAM,MAAM,gBAAgB,GAAG,WAAW,GAAG,WAAW,GAAG,YAAY,GAAG,YAAY,GAAG,KAAK,CAAC;AAE/F,MAAM,WAAW,gBAAgB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,gBAAgB;IAChC,MAAM,EAAE,gBAAgB,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,gBAAgB,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC7B,QAAQ,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,eAAe,EAAE,CAAA;KAAE,CAAC,CAAC;CAC3E;AAQD;mBACmB;AACnB,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAOpE;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,GAAG,aAAa,CAkBhH;AAED,wBAAgB,aAAa,CAC5B,GAAG,EAAE,aAAa,EAClB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,gBAAgB,GACtB,gBAAgB,CA0ClB;AAED,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,gBAAgB,GAAG,MAAM,CAcrE","sourcesContent":["/**\n * Paired comparison between two configs in one run record.\n *\n * Aggregate means are the wrong instrument for \"should we change this\n * default\". Both configs score the *same* queries, so the informative\n * question is per-query: how many did B improve, how many did it hurt, and\n * could that split have come from chance? A 3pp mean gap can be two queries\n * out of sixty moving, which is not a reason to change anything.\n *\n * Reports a two-sided sign test over the queries where the two configs\n * differ. Ties carry no information about direction and are excluded, which\n * is what makes the test paired.\n */\n\nimport type { EvalQueryResult } from \"./eval.js\";\n\n/** Per-query metrics that can be compared. */\nexport type ComparableMetric = \"recallAt1\" | \"recallAt5\" | \"recallAt10\" | \"recallAt50\" | \"mrr\";\n\nexport interface PairedQueryDelta {\n\tid: string;\n\tclass: string;\n\tbefore: number;\n\tafter: number;\n}\n\nexport interface PairedComparison {\n\tmetric: ComparableMetric;\n\tbefore: string;\n\tafter: string;\n\tbetter: number;\n\tworse: number;\n\ttied: number;\n\tmeanBefore: number;\n\tmeanAfter: number;\n\t/** Two-sided sign-test p-value over non-tied queries. 1 when all tie. */\n\tpValue: number;\n\tdeltas: PairedQueryDelta[];\n}\n\nexport interface ComparableRun {\n\tperQuery: Array<{ id: string; class: string; results: EvalQueryResult[] }>;\n}\n\nfunction binomial(n: number, k: number): number {\n\tlet result = 1;\n\tfor (let i = 0; i < k; i++) result = (result * (n - i)) / (i + 1);\n\treturn result;\n}\n\n/** Two-sided sign test: probability of a split at least this lopsided under\n * a fair coin. */\nexport function signTestPValue(better: number, worse: number): number {\n\tconst n = better + worse;\n\tif (n === 0) return 1;\n\tconst tail = Math.min(better, worse);\n\tlet cumulative = 0;\n\tfor (let i = 0; i <= tail; i++) cumulative += binomial(n, i);\n\treturn Math.min(1, (cumulative / 2 ** n) * 2);\n}\n\n/**\n * Pair the *same* config across two run records, so \"did my change help?\" is\n * answerable at all. `comparePaired` compares two configs inside one run;\n * this compares one config before and after a retrieval change.\n *\n * Only sound when both runs scored the same corpus and the same gold set —\n * otherwise the delta includes corpus drift. The caller checks provenance.\n */\nexport function mergeRunsForComparison(before: ComparableRun, after: ComparableRun, label: string): ComparableRun {\n\tconst afterById = new Map(after.perQuery.map((q) => [q.id, q]));\n\tconst perQuery: ComparableRun[\"perQuery\"] = [];\n\tfor (const q of before.perQuery) {\n\t\tconst other = afterById.get(q.id);\n\t\tconst a = q.results.find((r) => r.label === label);\n\t\tconst b = other?.results.find((r) => r.label === label);\n\t\tif (!a || !b) continue;\n\t\tperQuery.push({\n\t\t\tid: q.id,\n\t\t\tclass: q.class,\n\t\t\tresults: [\n\t\t\t\t{ ...a, label: \"before\" },\n\t\t\t\t{ ...b, label: \"after\" },\n\t\t\t],\n\t\t});\n\t}\n\treturn { perQuery };\n}\n\nexport function comparePaired(\n\trun: ComparableRun,\n\tbefore: string,\n\tafter: string,\n\tmetric: ComparableMetric,\n): PairedComparison {\n\tconst pick = (results: readonly EvalQueryResult[], label: string): EvalQueryResult | undefined =>\n\t\tresults.find((r) => r.label === label);\n\n\tlet better = 0;\n\tlet worse = 0;\n\tlet tied = 0;\n\tlet sumBefore = 0;\n\tlet sumAfter = 0;\n\tlet n = 0;\n\tconst deltas: PairedQueryDelta[] = [];\n\n\tfor (const query of run.perQuery) {\n\t\tconst a = pick(query.results, before);\n\t\tconst b = pick(query.results, after);\n\t\tif (!a || !b) continue;\n\t\tn++;\n\t\tsumBefore += a[metric];\n\t\tsumAfter += b[metric];\n\t\tif (b[metric] > a[metric]) {\n\t\t\tbetter++;\n\t\t\tdeltas.push({ id: query.id, class: query.class, before: a[metric], after: b[metric] });\n\t\t} else if (b[metric] < a[metric]) {\n\t\t\tworse++;\n\t\t\tdeltas.push({ id: query.id, class: query.class, before: a[metric], after: b[metric] });\n\t\t} else {\n\t\t\ttied++;\n\t\t}\n\t}\n\n\treturn {\n\t\tmetric,\n\t\tbefore,\n\t\tafter,\n\t\tbetter,\n\t\tworse,\n\t\ttied,\n\t\tmeanBefore: n > 0 ? sumBefore / n : 0,\n\t\tmeanAfter: n > 0 ? sumAfter / n : 0,\n\t\tpValue: signTestPValue(better, worse),\n\t\tdeltas,\n\t};\n}\n\nexport function formatComparison(comparison: PairedComparison): string {\n\tconst { metric, before, after, better, worse, tied, pValue } = comparison;\n\tconst delta = comparison.meanAfter - comparison.meanBefore;\n\tconst verdict =\n\t\tpValue <= 0.05\n\t\t\t? \"significant at p<=0.05\"\n\t\t\t: `NOT significant (p=${pValue.toFixed(3)}) — mean gap is not evidence of a real difference`;\n\treturn (\n\t\t`${metric}: \"${after}\" vs \"${before}\"\\n` +\n\t\t` mean ${comparison.meanBefore.toFixed(3)} -> ${comparison.meanAfter.toFixed(3)} ` +\n\t\t`(${delta >= 0 ? \"+\" : \"\"}${delta.toFixed(3)})\\n` +\n\t\t` ${better} better, ${worse} worse, ${tied} tied\\n` +\n\t\t` ${verdict}`\n\t);\n}\n"]}
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Paired comparison between two configs in one run record.
3
+ *
4
+ * Aggregate means are the wrong instrument for "should we change this
5
+ * default". Both configs score the *same* queries, so the informative
6
+ * question is per-query: how many did B improve, how many did it hurt, and
7
+ * could that split have come from chance? A 3pp mean gap can be two queries
8
+ * out of sixty moving, which is not a reason to change anything.
9
+ *
10
+ * Reports a two-sided sign test over the queries where the two configs
11
+ * differ. Ties carry no information about direction and are excluded, which
12
+ * is what makes the test paired.
13
+ */
14
+ function binomial(n, k) {
15
+ let result = 1;
16
+ for (let i = 0; i < k; i++)
17
+ result = (result * (n - i)) / (i + 1);
18
+ return result;
19
+ }
20
+ /** Two-sided sign test: probability of a split at least this lopsided under
21
+ * a fair coin. */
22
+ export function signTestPValue(better, worse) {
23
+ const n = better + worse;
24
+ if (n === 0)
25
+ return 1;
26
+ const tail = Math.min(better, worse);
27
+ let cumulative = 0;
28
+ for (let i = 0; i <= tail; i++)
29
+ cumulative += binomial(n, i);
30
+ return Math.min(1, (cumulative / 2 ** n) * 2);
31
+ }
32
+ /**
33
+ * Pair the *same* config across two run records, so "did my change help?" is
34
+ * answerable at all. `comparePaired` compares two configs inside one run;
35
+ * this compares one config before and after a retrieval change.
36
+ *
37
+ * Only sound when both runs scored the same corpus and the same gold set —
38
+ * otherwise the delta includes corpus drift. The caller checks provenance.
39
+ */
40
+ export function mergeRunsForComparison(before, after, label) {
41
+ const afterById = new Map(after.perQuery.map((q) => [q.id, q]));
42
+ const perQuery = [];
43
+ for (const q of before.perQuery) {
44
+ const other = afterById.get(q.id);
45
+ const a = q.results.find((r) => r.label === label);
46
+ const b = other?.results.find((r) => r.label === label);
47
+ if (!a || !b)
48
+ continue;
49
+ perQuery.push({
50
+ id: q.id,
51
+ class: q.class,
52
+ results: [
53
+ { ...a, label: "before" },
54
+ { ...b, label: "after" },
55
+ ],
56
+ });
57
+ }
58
+ return { perQuery };
59
+ }
60
+ export function comparePaired(run, before, after, metric) {
61
+ const pick = (results, label) => results.find((r) => r.label === label);
62
+ let better = 0;
63
+ let worse = 0;
64
+ let tied = 0;
65
+ let sumBefore = 0;
66
+ let sumAfter = 0;
67
+ let n = 0;
68
+ const deltas = [];
69
+ for (const query of run.perQuery) {
70
+ const a = pick(query.results, before);
71
+ const b = pick(query.results, after);
72
+ if (!a || !b)
73
+ continue;
74
+ n++;
75
+ sumBefore += a[metric];
76
+ sumAfter += b[metric];
77
+ if (b[metric] > a[metric]) {
78
+ better++;
79
+ deltas.push({ id: query.id, class: query.class, before: a[metric], after: b[metric] });
80
+ }
81
+ else if (b[metric] < a[metric]) {
82
+ worse++;
83
+ deltas.push({ id: query.id, class: query.class, before: a[metric], after: b[metric] });
84
+ }
85
+ else {
86
+ tied++;
87
+ }
88
+ }
89
+ return {
90
+ metric,
91
+ before,
92
+ after,
93
+ better,
94
+ worse,
95
+ tied,
96
+ meanBefore: n > 0 ? sumBefore / n : 0,
97
+ meanAfter: n > 0 ? sumAfter / n : 0,
98
+ pValue: signTestPValue(better, worse),
99
+ deltas,
100
+ };
101
+ }
102
+ export function formatComparison(comparison) {
103
+ const { metric, before, after, better, worse, tied, pValue } = comparison;
104
+ const delta = comparison.meanAfter - comparison.meanBefore;
105
+ const verdict = pValue <= 0.05
106
+ ? "significant at p<=0.05"
107
+ : `NOT significant (p=${pValue.toFixed(3)}) — mean gap is not evidence of a real difference`;
108
+ return (`${metric}: "${after}" vs "${before}"\n` +
109
+ ` mean ${comparison.meanBefore.toFixed(3)} -> ${comparison.meanAfter.toFixed(3)} ` +
110
+ `(${delta >= 0 ? "+" : ""}${delta.toFixed(3)})\n` +
111
+ ` ${better} better, ${worse} worse, ${tied} tied\n` +
112
+ ` ${verdict}`);
113
+ }
114
+ //# sourceMappingURL=eval-compare.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eval-compare.js","sourceRoot":"","sources":["../../../src/core/search/eval-compare.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Paired comparison between two configs in one run record.\n *\n * Aggregate means are the wrong instrument for \"should we change this\n * default\". Both configs score the *same* queries, so the informative\n * question is per-query: how many did B improve, how many did it hurt, and\n * could that split have come from chance? A 3pp mean gap can be two queries\n * out of sixty moving, which is not a reason to change anything.\n *\n * Reports a two-sided sign test over the queries where the two configs\n * differ. Ties carry no information about direction and are excluded, which\n * is what makes the test paired.\n */\n\nimport type { EvalQueryResult } from \"./eval.js\";\n\n/** Per-query metrics that can be compared. */\nexport type ComparableMetric = \"recallAt1\" | \"recallAt5\" | \"recallAt10\" | \"recallAt50\" | \"mrr\";\n\nexport interface PairedQueryDelta {\n\tid: string;\n\tclass: string;\n\tbefore: number;\n\tafter: number;\n}\n\nexport interface PairedComparison {\n\tmetric: ComparableMetric;\n\tbefore: string;\n\tafter: string;\n\tbetter: number;\n\tworse: number;\n\ttied: number;\n\tmeanBefore: number;\n\tmeanAfter: number;\n\t/** Two-sided sign-test p-value over non-tied queries. 1 when all tie. */\n\tpValue: number;\n\tdeltas: PairedQueryDelta[];\n}\n\nexport interface ComparableRun {\n\tperQuery: Array<{ id: string; class: string; results: EvalQueryResult[] }>;\n}\n\nfunction binomial(n: number, k: number): number {\n\tlet result = 1;\n\tfor (let i = 0; i < k; i++) result = (result * (n - i)) / (i + 1);\n\treturn result;\n}\n\n/** Two-sided sign test: probability of a split at least this lopsided under\n * a fair coin. */\nexport function signTestPValue(better: number, worse: number): number {\n\tconst n = better + worse;\n\tif (n === 0) return 1;\n\tconst tail = Math.min(better, worse);\n\tlet cumulative = 0;\n\tfor (let i = 0; i <= tail; i++) cumulative += binomial(n, i);\n\treturn Math.min(1, (cumulative / 2 ** n) * 2);\n}\n\n/**\n * Pair the *same* config across two run records, so \"did my change help?\" is\n * answerable at all. `comparePaired` compares two configs inside one run;\n * this compares one config before and after a retrieval change.\n *\n * Only sound when both runs scored the same corpus and the same gold set —\n * otherwise the delta includes corpus drift. The caller checks provenance.\n */\nexport function mergeRunsForComparison(before: ComparableRun, after: ComparableRun, label: string): ComparableRun {\n\tconst afterById = new Map(after.perQuery.map((q) => [q.id, q]));\n\tconst perQuery: ComparableRun[\"perQuery\"] = [];\n\tfor (const q of before.perQuery) {\n\t\tconst other = afterById.get(q.id);\n\t\tconst a = q.results.find((r) => r.label === label);\n\t\tconst b = other?.results.find((r) => r.label === label);\n\t\tif (!a || !b) continue;\n\t\tperQuery.push({\n\t\t\tid: q.id,\n\t\t\tclass: q.class,\n\t\t\tresults: [\n\t\t\t\t{ ...a, label: \"before\" },\n\t\t\t\t{ ...b, label: \"after\" },\n\t\t\t],\n\t\t});\n\t}\n\treturn { perQuery };\n}\n\nexport function comparePaired(\n\trun: ComparableRun,\n\tbefore: string,\n\tafter: string,\n\tmetric: ComparableMetric,\n): PairedComparison {\n\tconst pick = (results: readonly EvalQueryResult[], label: string): EvalQueryResult | undefined =>\n\t\tresults.find((r) => r.label === label);\n\n\tlet better = 0;\n\tlet worse = 0;\n\tlet tied = 0;\n\tlet sumBefore = 0;\n\tlet sumAfter = 0;\n\tlet n = 0;\n\tconst deltas: PairedQueryDelta[] = [];\n\n\tfor (const query of run.perQuery) {\n\t\tconst a = pick(query.results, before);\n\t\tconst b = pick(query.results, after);\n\t\tif (!a || !b) continue;\n\t\tn++;\n\t\tsumBefore += a[metric];\n\t\tsumAfter += b[metric];\n\t\tif (b[metric] > a[metric]) {\n\t\t\tbetter++;\n\t\t\tdeltas.push({ id: query.id, class: query.class, before: a[metric], after: b[metric] });\n\t\t} else if (b[metric] < a[metric]) {\n\t\t\tworse++;\n\t\t\tdeltas.push({ id: query.id, class: query.class, before: a[metric], after: b[metric] });\n\t\t} else {\n\t\t\ttied++;\n\t\t}\n\t}\n\n\treturn {\n\t\tmetric,\n\t\tbefore,\n\t\tafter,\n\t\tbetter,\n\t\tworse,\n\t\ttied,\n\t\tmeanBefore: n > 0 ? sumBefore / n : 0,\n\t\tmeanAfter: n > 0 ? sumAfter / n : 0,\n\t\tpValue: signTestPValue(better, worse),\n\t\tdeltas,\n\t};\n}\n\nexport function formatComparison(comparison: PairedComparison): string {\n\tconst { metric, before, after, better, worse, tied, pValue } = comparison;\n\tconst delta = comparison.meanAfter - comparison.meanBefore;\n\tconst verdict =\n\t\tpValue <= 0.05\n\t\t\t? \"significant at p<=0.05\"\n\t\t\t: `NOT significant (p=${pValue.toFixed(3)}) — mean gap is not evidence of a real difference`;\n\treturn (\n\t\t`${metric}: \"${after}\" vs \"${before}\"\\n` +\n\t\t` mean ${comparison.meanBefore.toFixed(3)} -> ${comparison.meanAfter.toFixed(3)} ` +\n\t\t`(${delta >= 0 ? \"+\" : \"\"}${delta.toFixed(3)})\\n` +\n\t\t` ${better} better, ${worse} worse, ${tied} tied\\n` +\n\t\t` ${verdict}`\n\t);\n}\n"]}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Gold-set loading and anchor resolution.
3
+ *
4
+ * A gold span recorded as bare line numbers rots on the next refactor, and a
5
+ * rotted gold set fails *silently* — it just scores lower. So each span also
6
+ * carries an `anchor`: a literal snippet that must sit inside the range. The
7
+ * anchor is the source of truth; the line numbers are a cache of where it was
8
+ * when the fixture was last regenerated.
9
+ *
10
+ * That gives three properties the first gold set lacked:
11
+ * - `resolveGoldSet` recomputes ranges from anchors, so `--fix` re-pins the
12
+ * whole set after a refactor instead of someone hand-editing 60 numbers;
13
+ * - `validateGoldSet` fails loudly when an anchor has moved out of its
14
+ * recorded range, drifted, or become ambiguous;
15
+ * - anchors are never used for scoring, so this cannot flatter retrieval —
16
+ * `spanMatchesGold` still sees only a path and a line range.
17
+ */
18
+ import type { EvalGoldSpan, EvalQuery } from "./eval.js";
19
+ export interface GoldValidationIssue {
20
+ queryId: string;
21
+ path: string;
22
+ problem: string;
23
+ }
24
+ /** Re-pin one gold span's line range from its anchor. Returns the span
25
+ * unchanged when it is file-scoped or has no anchor to resolve. */
26
+ export declare function resolveGoldSpan(corpusRoot: string, span: EvalGoldSpan): {
27
+ span: EvalGoldSpan;
28
+ problem?: string;
29
+ };
30
+ /** Re-pin every gold span in the set. Problems are collected, not thrown, so
31
+ * a regeneration run reports all drift at once. */
32
+ export declare function resolveGoldSet(corpusRoot: string, dataset: readonly EvalQuery[]): {
33
+ dataset: EvalQuery[];
34
+ issues: GoldValidationIssue[];
35
+ };
36
+ /**
37
+ * Check the committed fixture against the corpus without rewriting it.
38
+ *
39
+ * Catches the two ways a gold set goes wrong: the anchor no longer exists (the
40
+ * code was deleted or renamed), or it exists but has moved outside the
41
+ * recorded range (the fixture is stale and every score computed from it is
42
+ * wrong).
43
+ */
44
+ export declare function validateGoldSet(corpusRoot: string, dataset: readonly EvalQuery[]): GoldValidationIssue[];
45
+ /** Load the gold set from a fixture file. */
46
+ export declare function loadGoldSet(fixturePath: string): EvalQuery[];
47
+ //# sourceMappingURL=eval-gold.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eval-gold.d.ts","sourceRoot":"","sources":["../../../src/core/search/eval-gold.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAYzD,MAAM,WAAW,mBAAmB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CAChB;AA2CD;oEACoE;AACpE,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAiBhH;AAED;oDACoD;AACpD,wBAAgB,cAAc,CAC7B,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,SAAS,SAAS,EAAE,GAC3B;IAAE,OAAO,EAAE,SAAS,EAAE,CAAC;IAAC,MAAM,EAAE,mBAAmB,EAAE,CAAA;CAAE,CAWzD;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,SAAS,EAAE,GAAG,mBAAmB,EAAE,CAmDxG;AAED,6CAA6C;AAC7C,wBAAgB,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,EAAE,CAE5D","sourcesContent":["/**\n * Gold-set loading and anchor resolution.\n *\n * A gold span recorded as bare line numbers rots on the next refactor, and a\n * rotted gold set fails *silently* — it just scores lower. So each span also\n * carries an `anchor`: a literal snippet that must sit inside the range. The\n * anchor is the source of truth; the line numbers are a cache of where it was\n * when the fixture was last regenerated.\n *\n * That gives three properties the first gold set lacked:\n * - `resolveGoldSet` recomputes ranges from anchors, so `--fix` re-pins the\n * whole set after a refactor instead of someone hand-editing 60 numbers;\n * - `validateGoldSet` fails loudly when an anchor has moved out of its\n * recorded range, drifted, or become ambiguous;\n * - anchors are never used for scoring, so this cannot flatter retrieval —\n * `spanMatchesGold` still sees only a path and a line range.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport type { EvalGoldSpan, EvalQuery } from \"./eval.js\";\n\n/** Lines of context kept either side of a non-block anchor (an error literal,\n * a single statement) — enough to be a real target, tight enough that a\n * 60-line chunk elsewhere in the file does not count as a hit. */\nconst LINE_ANCHOR_PAD = 2;\n/** Upper bound on a resolved block, so an anchor that fails to find its\n * closing brace cannot silently swallow an entire file. */\nconst MAX_BLOCK_LINES = 120;\n/** Fallback extent when a block anchor never finds its closing brace. */\nconst UNCLOSED_BLOCK_LINES = 15;\n\nexport interface GoldValidationIssue {\n\tqueryId: string;\n\tpath: string;\n\tproblem: string;\n}\n\nfunction readLines(corpusRoot: string, rel: string): string[] | undefined {\n\ttry {\n\t\treturn readFileSync(path.resolve(corpusRoot, rel), \"utf-8\").split(\"\\n\");\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** Indices (0-based) of every line containing `anchor`. */\nfunction findAnchorLines(lines: readonly string[], anchor: string): number[] {\n\tconst found: number[] = [];\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tif (lines[i].includes(anchor)) found.push(i);\n\t}\n\treturn found;\n}\n\n/**\n * Extent of the declaration an anchor names.\n *\n * A line ending in an opener (`{`, `(`, `[`) starts a block, which runs to the\n * first line closing at column 0 — the shape every top-level declaration in\n * this codebase has. Anything else is a statement, scored with a small pad.\n */\nfunction resolveExtent(lines: readonly string[], anchorIndex: number): { startLine: number; endLine: number } {\n\tconst head = lines[anchorIndex];\n\tconst opensBlock = /[{([]\\s*$/.test(head);\n\tif (!opensBlock) {\n\t\treturn {\n\t\t\tstartLine: Math.max(1, anchorIndex + 1 - LINE_ANCHOR_PAD),\n\t\t\tendLine: Math.min(lines.length, anchorIndex + 1 + LINE_ANCHOR_PAD),\n\t\t};\n\t}\n\tconst limit = Math.min(lines.length, anchorIndex + 1 + MAX_BLOCK_LINES);\n\tfor (let i = anchorIndex + 1; i < limit; i++) {\n\t\t// Column-0 close: `}`, `};`, `});`, `];` — the end of a top-level block.\n\t\tif (/^[}\\])]/.test(lines[i])) return { startLine: anchorIndex + 1, endLine: i + 1 };\n\t}\n\treturn { startLine: anchorIndex + 1, endLine: Math.min(lines.length, anchorIndex + 1 + UNCLOSED_BLOCK_LINES) };\n}\n\n/** Re-pin one gold span's line range from its anchor. Returns the span\n * unchanged when it is file-scoped or has no anchor to resolve. */\nexport function resolveGoldSpan(corpusRoot: string, span: EvalGoldSpan): { span: EvalGoldSpan; problem?: string } {\n\tconst lines = readLines(corpusRoot, span.path);\n\tif (!lines) return { span, problem: `file not found: ${span.path}` };\n\n\tif (span.scope === \"file\") {\n\t\treturn { span: { ...span, startLine: 1, endLine: lines.length } };\n\t}\n\tif (!span.anchor) return { span, problem: `span-scoped gold needs an anchor: ${span.path}` };\n\n\tconst matches = findAnchorLines(lines, span.anchor);\n\tif (matches.length === 0) return { span, problem: `anchor not found in ${span.path}: ${span.anchor}` };\n\tif (matches.length > 1) {\n\t\treturn { span, problem: `anchor is ambiguous (${matches.length} matches) in ${span.path}: ${span.anchor}` };\n\t}\n\n\tconst { startLine, endLine } = resolveExtent(lines, matches[0]);\n\treturn { span: { ...span, startLine, endLine } };\n}\n\n/** Re-pin every gold span in the set. Problems are collected, not thrown, so\n * a regeneration run reports all drift at once. */\nexport function resolveGoldSet(\n\tcorpusRoot: string,\n\tdataset: readonly EvalQuery[],\n): { dataset: EvalQuery[]; issues: GoldValidationIssue[] } {\n\tconst issues: GoldValidationIssue[] = [];\n\tconst resolved = dataset.map((query) => ({\n\t\t...query,\n\t\tgold: query.gold.map((span) => {\n\t\t\tconst result = resolveGoldSpan(corpusRoot, span);\n\t\t\tif (result.problem) issues.push({ queryId: query.id, path: span.path, problem: result.problem });\n\t\t\treturn result.span;\n\t\t}),\n\t}));\n\treturn { dataset: resolved, issues };\n}\n\n/**\n * Check the committed fixture against the corpus without rewriting it.\n *\n * Catches the two ways a gold set goes wrong: the anchor no longer exists (the\n * code was deleted or renamed), or it exists but has moved outside the\n * recorded range (the fixture is stale and every score computed from it is\n * wrong).\n */\nexport function validateGoldSet(corpusRoot: string, dataset: readonly EvalQuery[]): GoldValidationIssue[] {\n\tconst issues: GoldValidationIssue[] = [];\n\tconst seenIds = new Set<string>();\n\n\tfor (const query of dataset) {\n\t\tif (seenIds.has(query.id)) issues.push({ queryId: query.id, path: \"\", problem: \"duplicate query id\" });\n\t\tseenIds.add(query.id);\n\t\tif (query.gold.length === 0) issues.push({ queryId: query.id, path: \"\", problem: \"query has no gold spans\" });\n\n\t\tfor (const span of query.gold) {\n\t\t\tconst lines = readLines(corpusRoot, span.path);\n\t\t\tif (!lines) {\n\t\t\t\tissues.push({ queryId: query.id, path: span.path, problem: \"file not found\" });\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (span.startLine === undefined || span.endLine === undefined) {\n\t\t\t\tissues.push({ queryId: query.id, path: span.path, problem: \"gold span is missing line numbers\" });\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (span.startLine < 1 || span.endLine < span.startLine || span.endLine > lines.length) {\n\t\t\t\tissues.push({\n\t\t\t\t\tqueryId: query.id,\n\t\t\t\t\tpath: span.path,\n\t\t\t\t\tproblem: `line range ${span.startLine}-${span.endLine} out of bounds (file has ${lines.length} lines)`,\n\t\t\t\t});\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (span.scope === \"file\") continue;\n\t\t\tif (!span.anchor) {\n\t\t\t\tissues.push({ queryId: query.id, path: span.path, problem: \"span-scoped gold needs an anchor\" });\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst matches = findAnchorLines(lines, span.anchor);\n\t\t\tif (matches.length === 0) {\n\t\t\t\tissues.push({ queryId: query.id, path: span.path, problem: `anchor not found: ${span.anchor}` });\n\t\t\t} else if (matches.length > 1) {\n\t\t\t\tissues.push({\n\t\t\t\t\tqueryId: query.id,\n\t\t\t\t\tpath: span.path,\n\t\t\t\t\tproblem: `anchor is ambiguous (${matches.length} matches): ${span.anchor}`,\n\t\t\t\t});\n\t\t\t} else if (matches[0] + 1 < span.startLine || matches[0] + 1 > span.endLine) {\n\t\t\t\tissues.push({\n\t\t\t\t\tqueryId: query.id,\n\t\t\t\t\tpath: span.path,\n\t\t\t\t\tproblem: `anchor moved to line ${matches[0] + 1}, outside recorded range ${span.startLine}-${span.endLine}`,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\treturn issues;\n}\n\n/** Load the gold set from a fixture file. */\nexport function loadGoldSet(fixturePath: string): EvalQuery[] {\n\treturn JSON.parse(readFileSync(fixturePath, \"utf-8\")) as EvalQuery[];\n}\n"]}