/** * Orchestrates semantic indexing and search for a repository. * * Lifecycle (all behind --enable-semantic-index): * 1. `start()` — resolve the embsearch binary, scan the repo (ignore-aware), * apply the byte threshold. Under threshold → dormant. Over → spawn the * daemon, verify the backend is not the mock embedder, then index changed * files in the background in small batches, reporting progress. * 2. `search()` — top-k semantic query, mapping chunk ids back to * `path:start-end` via the sidecar metadata. * 3. `dispose()` — save + close the daemon. * * Every failure degrades to `unavailable` with a reason; nothing here ever * blocks session startup or affects grep/find. */ import { type DaemonRetriever, type EmbSearchRerankPassage, type EmbSearchRerankResult } from "./client.js"; export type EmbsearchState = { phase: "idle"; } | { phase: "skipped"; reason: string; } | { phase: "downloading"; receivedBytes: number; totalBytes: number | null; } | { phase: "indexing"; done: number; total: number; } | { phase: "ready"; chunkCount: number; } | { phase: "unavailable"; reason: string; }; export interface EmbsearchServiceOptions { cwd: string; /** Explicit binary path (settings override). Default: "embsearch" from PATH. */ binaryPath?: string; /** * Model directory handed to the daemon as `--model`, overriding the model * bundled in the binary. * * Only the eval harness sets this, to score two embedding models from one * binary. Pair it with a distinct `storeDir`: vectors from different models * are incompatible, and the daemon refuses to open a store built by another * model rather than mixing them. */ modelDir?: string; /** * Override the chunker's character cap. * * Only the eval harness sets this, to sweep the chunk window. It changes * what every vector in the store *is*, and nothing stored records it, so it * must be paired with a distinct `storeDir` exactly as `modelDir` is — * otherwise a run silently scores an index built at another cap. */ chunkMaxChars?: number; /** Minimum indexable bytes before indexing kicks in. */ thresholdBytes: number; /** * Override the store location. Only the eval harness sets this, so a * second index (e.g. a BM25-hybrid store) can exist for the same repo * without colliding with the primary one. */ storeDir?: string; /** * Create the store with the daemon's BM25 lexical index. * * Defaults to whatever the daemon can serve. Fixed at store creation, so an * existing store that disagrees is rebuilt once; when overriding this to * hold two different stores for one repo, pair it with a distinct * `storeDir` so they do not fight over the same directory. */ hybridStore?: boolean; /** Progress callback for UI (footer / stderr lines). */ onProgress?: (state: EmbsearchState) => void; } export interface SemanticHit { path: string; startLine: number; endLine: number; score: number; } export interface SemanticChunkHit extends SemanticHit { /** Per-build chunk id (`relpath#index`) — the fusion identity for hybrid search. */ id: string; } export declare class EmbsearchService { private readonly options; private client; private meta; private state; private disposed; /** Whether the resolved binary serves `retriever: "lexical"`. */ private lexicalRetriever; /** Whether the store actually opened carries a BM25 index. */ private hybridStore; /** Whether the resolved binary serves the cross-encoder `rerank` op. */ private crossEncoder; constructor(options: EmbsearchServiceOptions); getState(): EmbsearchState; /** Semantic search is usable (index ready, or still building with partial data). */ /** * Model id reported by the running daemon, once it is up. * * This — not the binary's version — identifies which model produced the * vectors in the store, because `--model` decouples the two. */ modelId(): string | undefined; isAvailable(): boolean; private setState; /** * Whether a BM25-only query will work: the daemon has to understand the * `lexical` retriever *and* the open store has to carry a BM25 index. */ supportsLexicalRetriever(): boolean; /** * Repo files whose on-disk content the index does not have — unknown to it, * or changed since it last read them. * * This is the set BM25 is structurally blind to, and the only place the * grep leg still earns its keep once BM25 is available. An agent that edits * a file and immediately searches for what it wrote is asking about exactly * these files; the index cannot answer until the next pass. * * Compares mtime and size only, never hashing: the check runs per query, and * a false positive merely lets grep cover a file BM25 already covers, while * a false negative would lose the edit. * * Deliberately uncached. A cache here caches the *absence* of an edit, which * is the one thing this must never do — an agent writes a file and searches * for it in the same breath. A 1s TTL was tried and cost the live-edit set * 75% to 100% of its score depending on how the timing fell, which is worse * than wrong: it was non-deterministic. One scan is ~25ms over ~1k files and * happens once per search, against retrieval that already costs more. */ staleFiles(signal?: AbortSignal): string[]; /** Whether the running daemon can score with a cross-encoder. */ supportsCrossEncoder(): boolean; /** * Cross-encoder rerank of caller-supplied passages. * * Unlike the retrievers this does not consult the index at all — it scores * exactly the text passed in, which is why the caller sends its expanded * windows rather than chunk ids. */ rerank(query: string, passages: EmbSearchRerankPassage[], k: number): Promise; private probeBinaryVersion; /** * What the store on disk says about itself, read straight from its manifest. * * `store-info` exists precisely for the case where the daemon will not open * the store: a `serve` pairs a store with an embedder and refuses the pair * when their models disagree, so at that moment nothing else can tell us * what built it. Returns undefined when there is no readable store — which * includes a binary too old to have the subcommand, and so degrades to the * previous behaviour rather than guessing. */ private probeStore; private resolveBinary; /** * Scan, threshold-check, and (when needed) index in the background. * Resolves when indexing completes or the feature settles dormant. */ start(signal?: AbortSignal): Promise; private run; private indexChangedFiles; private countChunks; /** Top-`k` semantic hits as `path` + line range + score. */ search(query: string, k?: number): Promise; /** Top-`k` semantic hits including their chunk ids, for rank fusion. */ searchChunks(query: string, k?: number, glob?: string, retriever?: DaemonRetriever): Promise; /** * Resolve a repo-relative path + line to its enclosing indexed chunk, or * undefined when the file/line is not covered by the index. Chunks overlap * by a few lines; the first (lowest-index) containing chunk wins so the * mapping is deterministic. */ findEnclosingChunk(rel: string, line: number): { id: string; path: string; startLine: number; endLine: number; } | undefined; private closeClient; /** Persist state and shut the daemon down. Safe to call twice. */ dispose(): Promise; } export declare function registerEmbsearchService(cwd: string, service: EmbsearchService): void; export declare function getEmbsearchService(cwd: string): EmbsearchService | undefined; export declare function unregisterEmbsearchService(cwd: string): void; //# sourceMappingURL=embsearch-service.d.ts.map