import type { Embedder } from '../embedders/embedder.js'; import type { Chunk, HealthCheckResult, SearchOptions, SearchResult, SourceContext } from '../types.js'; /** * Escape a string for use in a LanceDB SQL WHERE clause (single-quoted literal). * * LanceDB uses DataFusion under the hood, which follows the SQL standard: * single quotes are escaped by doubling ('' → ') and backslash is treated * as a literal character (NOT an escape character). Therefore only quote * doubling is required — escaping backslashes would break path matching. */ export declare function escapeSqlString(input: string): string; export declare class LanceStore { private db; private table; private dbPath; private embedder; private hasFtsIndex; private onWarn; /** * Instrumentation: number of times the read path has reopened the handle * via `refreshReadHandle()`. Exposed via `readRefreshCount` so tests * (mmnto/totem#1418) can assert the reopen fires on every search. */ private readRefreshes; /** * Source repo context injected at construction time. Primary stores use * `{ absolutePathRoot: projectRoot }` with no `sourceRepo`; linked stores * (mmnto/totem#1294 Cross-Repo Context Mesh) use * `{ sourceRepo: '', absolutePathRoot: '' }`. * Passed down to every `search` / `searchFts` call so every SearchResult * gets stamped with the owning store's identity via `rowToSearchResult`. * * **Required** since mmnto/totem#1295 — CR flagged that an optional * context with a silent `filePath` fallback for `absoluteFilePath` sent * legacy callers down the wrong repo root instead of failing fast. * Making the parameter required closes that class of bug at the type * level and forces every call site to state its source explicitly. */ private sourceContext; constructor(dbPath: string, embedder: Embedder, sourceContext: SourceContext, onWarn?: (msg: string) => void); /** * Connect to LanceDB. Auto-heals on version mismatch, corruption, * or embedder dimension change by deleting the index and signaling * that a full rebuild is needed. */ connect(): Promise; /** * Connect for FTS-only use — skips embedder dimension validation. * Use when the embedder is unavailable (offline, no API key) and * only FTS search will be performed. */ connectFtsOnly(): Promise; /** Check if the stored vector dimensions differ from the current embedder. */ private hasDimensionMismatch; /** Detect errors that warrant auto-healing (nuke + rebuild). */ private isHealableError; /** Delete the entire .lancedb/ directory and reset state. */ private nukeAndReset; /** Insert chunks into the store. Embeds them first. */ insert(chunks: Chunk[]): Promise; /** * Create (or rebuild) the FTS index on the `content` column. * Must be called after table creation or after incremental adds, * because LanceDB FTS indexes do not auto-update on `table.add()`. * Uses `replace: true` (the LanceDB default) to overwrite any stale index. */ createFtsIndex(): Promise; /** Check whether an FTS index exists on the table. */ private detectFtsIndex; /** Whether the FTS index is available for hybrid search. */ get ftsIndexReady(): boolean; /** * Search with optional hybrid mode (vector + FTS with RRF reranking). * Falls back to vector-only if no FTS index exists. * * Opens a fresh LanceDB snapshot for this call (mmnto/totem#1418) so an * external `totem sync` cannot leave this store reading a stale view, * and so concurrent searches never invalidate each other's handle. */ search(options: SearchOptions): Promise; /** * FTS-only search. No embedder required. * Use when embedding is unavailable (offline, no API key, cold-start fallback). * Requires an FTS index to exist; returns empty if none available. * * Opens a fresh LanceDB snapshot for this call (mmnto/totem#1418). */ searchFts(options: SearchOptions): Promise; /** Delete all chunks from a specific file (for incremental re-index). */ deleteByFile(filePath: string): Promise; /** Drop the entire table. Used for full re-index. */ reset(): Promise; /** Re-open the LanceDB connection, picking up rebuilt files after a full sync. */ reconnect(): Promise; /** * Open a fresh read snapshot (connection + table + fts flag) for a single * query. Scoped to the caller, NOT written onto `this.db` / `this.table`, * so concurrent searches don't race each other's handle lifetime * (mmnto/totem#1418 Shield CRITICAL follow-up). * * **Why unconditional reopen.** The MCP server holds LanceStore for the * life of the process. When `totem sync` (a separate process) rewrites * `.lancedb/` files underneath us, LanceDB's in-memory manifest keeps * pointing at the pre-sync snapshot. Vector search against that stale * view returns empty, which falls through to FTS-only via the hybrid * path, producing uniform ~0.016 RRF scores instead of real similarity * ranks. The corrupt path is silent because no exception fires. * * **Why not mtime-check.** A benchmark (see scripts/bench-lance-open.ts) * measured connect+openTable at ~0.5-1.1ms per call against real indexes. * That's well under the 10ms threshold where mtime gating starts to pay * for its own complexity. Reopening every time is strictly simpler and * eliminates a whole category of cache-invalidation bugs. * * **Why per-call snapshots.** Assigning the fresh connection to `this.db` * and then closing the old one from a second concurrent caller would * invalidate the first caller's in-flight query. Instead each call holds * its own connection + table references through the query lifetime, and * closes the connection in a finally block after the results return. * The instance-level `this.db` / `this.table` fields are still maintained * so write paths and existing consumers (healthCheck, stats, count) see * an up-to-date view on the next non-read call via `connect()`. * * **Why not also refresh write paths.** `totem sync` owns the writer * connection exclusively inside a single process. Write operations * (insert, deleteByFile, reset) already hold a consistent view for * their own workflow and a mid-sequence reopen would drop the in-flight * table reference that `insert()` sets during first-table creation. */ private openReadSnapshot; /** * Test-seam instrumentation (mmnto/totem#1418): total count of read-path * handle refreshes since this store was constructed. Asserted by the * stale-handle regression test to confirm every search() call reopens. */ get readRefreshCount(): number; /** Return true if the table doesn't exist or has zero rows. */ isEmpty(): Promise; /** Return the total number of rows in the store. */ count(): Promise; /** Return stats about the current index. */ stats(): Promise<{ totalChunks: number; byType: Record; }>; /** Run a health check against the index, verifying dimensions, search, and FTS. */ healthCheck(): Promise; /** * Returns one entry per distinct `filePath` in the store, with row counts * and a derived `origin` (`@scope/pkg` or `pkg` when the path lives under * `node_modules/`, otherwise `local`). * * `lastSynced` on every returned entry is set to the supplied `writtenAt` * timestamp (or `new Date()` if omitted). LanceDB rows do not carry per-row * sync timestamps in the current schema, so every document in a single * manifest necessarily carries the same `lastSynced` value — callers * building an `IndexManifest` should pass their `writtenAt` here so that * `documents[].lastSynced === manifest.writtenAt` for that run. * * Output is sorted by `sourceFile` ascending so the manifest payload is * deterministic across runs. */ manifestDocuments(writtenAt?: Date): Promise<{ sourceFile: string; origin: string; rowCount: number; lastSynced: string; }[]>; /** * Distinct raw `filePath` values currently in the store — the set of source * files with at least one chunk. Returns paths AS STORED (NOT normalized), * because callers purge via `deleteByFile`, which matches the stored literal; * separator normalization for comparison is the caller's job * (mmnto-ai/totem#2151 W1). Bounded `select(['filePath'])` projection — never * pulls vectors or chunk bodies into memory, but reads O(chunks) rows and * dedups in JS (same shape as `manifestDocuments`); a DB-level DISTINCT is * tracked in mmnto-ai/totem#2175 for large stores. */ getDistinctPaths(): Promise; } //# sourceMappingURL=lance-store.d.ts.map