/** * VectorIndex * * Builds and queries a LanceDB vector index over the call graph functions. * Each function is represented by a document combining its signature, docstring, * file path, language, and topological metadata (fanIn/fanOut, hub, entry point). * * Storage: /vector-index/ (LanceDB database folder) * Table name: "functions" * * Usage: * // Build (after openlore analyze --embed) * await VectorIndex.build(outputDir, nodes, signatures, hubIds, entryPointIds, embedSvc); * * // Search * const results = await VectorIndex.search(outputDir, "authenticate user with JWT", embedSvc); */ import type { CallEdge, FunctionNode } from './call-graph.js'; import type { FileSignatureMap } from './signature-extractor.js'; import type { Embedder } from './embedding-service.js'; import { type MatchEvidence, type SearchableFields } from './retrieval-evidence.js'; export type { MatchEvidence, MatchField, RetrievalTier } from './retrieval-evidence.js'; export { TOKENIZER_VERSION, tokenize } from './bm25-tokenizer.js'; export interface FunctionRecord { id: string; name: string; filePath: string; className: string; language: string; signature: string; docstring: string; fanIn: number; fanOut: number; isHub: boolean; isEntryPoint: boolean; /** Concatenated text used for embedding */ text: string; /** Embedding vector */ vector: number[]; } export interface SearchResult { record: Omit; /** * Relevance score. For hybrid search (default): RRF score, higher = more relevant. * For dense-only search: cosine distance from LanceDB, lower = more similar. */ score: number; /** Optional for legacy test doubles; every production search path emits it. */ scoreKind?: 'rrf' | 'bm25' | 'cosine_distance'; /** Optional for legacy test doubles; every production search path emits it. */ matchEvidence?: MatchEvidence; /** Vocabulary terms that contributed a non-zero score to this result. */ expansionTerms?: string[]; } export interface KeywordMissDiagnostics { missedTokens: string[]; nearTokens: Array<{ queryToken: string; indexedTokens: string[]; }>; } export interface VectorIndexMeta { hasEmbeddings: boolean; dim: number; model: string | null; builtAt: string; /** Changes only after a complete rebuild; incremental mutations preserve it. */ fullBuildAt?: string; schemaVersion: number; /** * Version of the BM25 tokenizer that produced this index's corpus. A mismatch * against the running `TOKENIZER_VERSION` means an incremental patch would mix * token sets, so `updateFiles` defers (`deferred: 'tokenizer-changed'`) and a * full rebuild re-stamps. A legacy meta without this field is treated as v1. */ tokenizerVersion?: number; /** Corpus identity of the verified repository-vocabulary sidecar, when present. */ vocabularyContentStamp?: string; /** Present only when an incremental update could neither add nor restore rows. */ degraded?: { reason: 'incremental-update-restore-failed'; recordedAt: string; }; } declare function dbPathFor(outputDir: string): string; export interface Bm25Corpus { docs: Array<{ id: string; tfMap: Map; length: number; }>; /** term → number of documents containing it */ df: Map; avgLength: number; N: number; } export declare function buildBm25Corpus(records: Array<{ id: string; text: string; }>): Bm25Corpus; /** * The per-corpus lookup structures a keyword query needs, built once per corpus OBJECT * and carried across an incremental patch rather than rebuilt. * * `byTerm` maps a term to the ids of the documents containing it; `indexById` maps a * document id back to its position in `corpus.docs`. Postings are keyed by ID rather * than by position precisely so a patch can maintain them in O(edit): a patch drops * removed documents from the middle of `docs`, which renumbers every position after * them, but leaves every surviving document's id alone. * * Neither structure is part of {@link Bm25Corpus} and neither reaches the persisted * sidecar — both are derivable from `docs`. * (change: optimize-serving-hot-path-caches) */ interface Bm25Postings { byTerm: Map>; indexById: Map; /** False when two documents share an id, which would make `indexById` lossy. */ idsAreUnique: boolean; } export declare function _bm25WorkCountersForTesting(): { postingsBuilds: number; docsScored: number; }; export declare function _resetBm25WorkCountersForTesting(): void; /** * The doc indices that can score above zero for any of `tokenSets`, ascending. * * Exactly the set the caller's `score > 0 || expansionScore > 0` filter would keep: * `bm25Score` contributes only for a term with `df > 0` AND `tf > 0`, and both the idf * and the tf-norm factors are strictly positive whenever those hold — so "appears in * some posting list" and "scores above zero" are the same predicate. Scoring the * candidates instead of every document is what keeps a keyword query from walking the * whole corpus. * * If two documents ever share an id the position map is lossy, so this returns every * index rather than silently dropping one — a slow answer, never a wrong one. * (change: optimize-serving-hot-path-caches) */ export declare function bm25CandidateDocs(corpus: Bm25Corpus, ...tokenSets: ReadonlyArray): number[]; export declare function bm25Score(corpus: Bm25Corpus, queryTokens: string[], docIdx: number): number; /** Attribute the ranker's exact aggregate term contributions across one bounded candidate's fields. */ export declare function bm25MatchEvidence(corpus: Bm25Corpus, queryTokens: string[], docIdx: number, fields: SearchableFields, tier?: 1 | 2): MatchEvidence; declare const _cacheStats: { tableHits: number; tableMisses: number; bm25Hits: number; bm25Misses: number; }; /** Clear every process-lifetime cache for one on-disk vector index. */ export declare function invalidateVectorIndexCaches(outputDir: string): void; /** Test-only: expose the canonical cache identity used for an index path. */ export declare const _vectorIndexCacheIdentityForTesting: typeof dbPathFor; /** Test-only: clear in-memory BM25 + LanceDB caches to force cold path. */ export declare function _resetVectorIndexCachesForTesting(): void; /** Test-only proof that a cold request populated, and a warm request reused, each search cache. */ export declare function _vectorIndexCacheStatsForTesting(): Readonly; interface MutableTable { delete(predicate: string): Promise; add(rows: Record[]): Promise; } declare function replaceRowsWithRestore(table: MutableTable, predicate: string | null, replacementRows: Record[], previousRows: Record[], onRestoreFailure: () => Promise): Promise; /** Test-only: exercise the transactional delete/add helper without LanceDB. */ export declare const _replaceRowsWithRestoreForTesting: typeof replaceRowsWithRestore; /** * Absorb one incremental update into an existing corpus, returning the new corpus and rows. * * Pure — no cache, no disk — so it can be checked directly against {@link buildBm25Corpus}, which * is the only assurance that matters here (see `bm25-incremental-patch.test.ts`). */ declare function patchBm25Corpus(previous: Bm25Corpus, previousRows: Record[], changedFilePaths: Set, newRows: Record[], previousPostings?: Bm25Postings): { corpus: Bm25Corpus; rows: Record[]; postings?: Bm25Postings; }; /** Test-only: drive {@link patchBm25Corpus} directly, to diff it against a full rebuild. */ export declare const _patchBm25CorpusForTesting: typeof patchBm25Corpus; /** * Test-only: patch a corpus the way {@link patchBm25Cache} does — carrying the memoized * postings index across the new corpus object instead of leaving it to be rebuilt. * The cache-side half of the pure {@link patchBm25Corpus}, isolated from LanceDB. */ export declare function _patchBm25CorpusCarryingPostingsForTesting(previous: Bm25Corpus, previousRows: Record[], changedFilePaths: Set, newRows: Record[]): Bm25Corpus; export declare function searchableFieldsForFunctionRow(row: Record): SearchableFields; export declare class VectorIndex { /** User-facing disclosure for an index whose incremental rollback also failed. */ static degradationNotice(outputDir: string): string | null; /** * Build (or rebuild) the vector index from call graph nodes + signatures. * * When `incremental` is true and an existing index is found, only functions * whose text has changed since the last build are re-embedded. Unchanged * functions reuse their cached vectors. Pass `incremental: false` (or omit * when no index exists) to do a full rebuild. * * Returns a summary of how many functions were embedded vs reused. * * When `embedSvc` is null, builds a **keyword-only (BM25)** index: the corpus * rows are written without a `vector` column and the meta sidecar records * `hasEmbeddings: false`. Search then serves BM25 results and never attempts * ANN. Re-building a previously-embedded index with `embedSvc=null` downgrades * it to BM25-only (overwrite + meta update), and vice-versa upgrades it. */ static build(outputDir: string, nodes: FunctionNode[], signatures: FileSignatureMap[], hubIds: Set, entryPointIds: Set, embedSvc: Embedder | null, /** Optional map of filePath → source content for skeleton-based body indexing */ fileContents?: Map, /** When true, reuse cached vectors for unchanged functions */ incremental?: boolean, vocabularyExpansion?: boolean, callEdges?: readonly Pick[]): Promise<{ embedded: number; reused: number; total: number; hasEmbeddings: boolean; productionFunctions: number; testFunctions: number; signatureOnlySymbols: number; }>; private static buildUnlocked; /** * Watch-mode incremental update (Spec 13.1). Replace only the rows for the * changed files with freshly-built records — a row-level delete+add instead of * the full-corpus read+overwrite that build() performs. The cold build() path * is untouched, protecting the `analyze --embed` contract (G7). * * - Embedded index: reuse existing vectors for rows whose embed-text is * unchanged (queried for the changed files only, not the whole corpus), * embed just the new/changed texts, then delete the changed files' old rows * and add the rebuilt ones. The LanceDB table handle in _tableCache stays * valid across row ops, so search() does not pay a reconnect. * - BM25-only index: delete+add the changed files' documents and patch the * cached BM25 corpus in place rather than dropping the whole corpus cache. */ static updateFiles(outputDir: string, nodes: FunctionNode[], changedFilePaths: Set, signatures: FileSignatureMap[], hubIds: Set, entryPointIds: Set, embedSvc: Embedder | null | undefined, fileContents?: Map): Promise<{ embedded: number; reused: number; total: number; hasEmbeddings: boolean; deferred?: 'model-changed' | 'tokenizer-changed'; }>; private static updateFilesUnlocked; /** * Hybrid search over the index: dense (ANN) + sparse (BM25) merged via RRF. * * Dense recall fetches top `limit*5` candidates from the vector index. * Sparse recall scores the full corpus with BM25 (cached per session). * Reciprocal Rank Fusion (RRF) combines both rankings into a single list. * * Set `hybrid: false` to use dense-only search (original behaviour). * Returns up to `limit` results sorted by relevance (highest first). */ static search(outputDir: string, query: string, embedSvc: Embedder | null | undefined, opts?: { limit?: number; language?: string; minFanIn?: number; /** Enable hybrid dense+sparse retrieval via RRF (default: true when embedSvc available) */ hybrid?: boolean; /** Internal diagnostic: return the ordinary bounded candidate window before result cutoff. */ traceCandidates?: boolean; /** Disable repository-vocabulary query expansion without rebuilding. */ vocabularyExpansion?: boolean; /** Reports the retrieval mode that actually produced this result set. */ onRetrievalMode?: (mode: 'keyword' | 'keyword+vocabulary' | 'semantic') => void; }): Promise; /** * BM25-only search: used when no embedding service is available. * Scores the full corpus with BM25 and returns the top `limit` results. */ private static _bm25Only; /** * Explain an empty keyword result using the corpus already loaded by search(). * The lookup is bounded and deterministic: at most 12 distinct query tokens, * 3 near identifier tokens per miss, and no model or secondary index. */ static keywordMissDiagnostics(outputDir: string, query: string): Promise; /** * Returns true if a vector index has been built for this output directory. */ static exists(outputDir: string): boolean; } //# sourceMappingURL=vector-index.d.ts.map