store.d.ts 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002
  1. /**
  2. * QMD Store - Core data access and retrieval functions
  3. *
  4. * This module provides all database operations, search functions, and document
  5. * retrieval for QMD. It returns raw data structures that can be formatted by
  6. * CLI or MCP consumers.
  7. *
  8. * Usage:
  9. * const store = createStore("/path/to/db.sqlite");
  10. * // or use default path:
  11. * const store = createStore();
  12. */
  13. import type { Database } from "./db.js";
  14. import { LlamaCpp, formatQueryForEmbedding, formatDocForEmbedding, type ILLMSession } from "./llm.js";
  15. import type { NamedCollection, Collection, CollectionConfig } from "./collections.js";
  16. import { type EmbeddingProvider } from "./embedding/provider.js";
  17. export declare const DEFAULT_EMBED_MODEL = "embeddinggemma";
  18. export declare const DEFAULT_RERANK_MODEL = "ExpedientFalcon/qwen3-reranker:0.6b-q8_0";
  19. export declare const DEFAULT_QUERY_MODEL = "Qwen/Qwen3-1.7B";
  20. export declare const DEFAULT_GLOB = "**/*.md";
  21. export declare const DEFAULT_MULTI_GET_MAX_BYTES: number;
  22. export declare const DEFAULT_EMBED_MAX_DOCS_PER_BATCH = 64;
  23. export declare const DEFAULT_EMBED_MAX_BATCH_BYTES: number;
  24. export declare const CHUNK_SIZE_TOKENS = 900;
  25. export declare const CHUNK_OVERLAP_TOKENS: number;
  26. export declare const CHUNK_SIZE_CHARS: number;
  27. export declare const CHUNK_OVERLAP_CHARS: number;
  28. export declare const CHUNK_WINDOW_TOKENS = 200;
  29. export declare const CHUNK_WINDOW_CHARS: number;
  30. /**
  31. * A potential break point in the document with a base score indicating quality.
  32. */
  33. export interface BreakPoint {
  34. pos: number;
  35. score: number;
  36. type: string;
  37. }
  38. /**
  39. * A region where a code fence exists (between ``` markers).
  40. * We should never split inside a code fence.
  41. */
  42. export interface CodeFenceRegion {
  43. start: number;
  44. end: number;
  45. }
  46. /**
  47. * Patterns for detecting break points in markdown documents.
  48. * Higher scores indicate better places to split.
  49. * Scores are spread wide so headings decisively beat lower-quality breaks.
  50. * Order matters for scoring - more specific patterns first.
  51. */
  52. export declare const BREAK_PATTERNS: [RegExp, number, string][];
  53. /**
  54. * Scan text for all potential break points.
  55. * Returns sorted array of break points with higher-scoring patterns taking precedence
  56. * when multiple patterns match the same position.
  57. */
  58. export declare function scanBreakPoints(text: string): BreakPoint[];
  59. /**
  60. * Find all code fence regions in the text.
  61. * Code fences are delimited by ``` and we should never split inside them.
  62. */
  63. export declare function findCodeFences(text: string): CodeFenceRegion[];
  64. /**
  65. * Check if a position is inside a code fence region.
  66. */
  67. export declare function isInsideCodeFence(pos: number, fences: CodeFenceRegion[]): boolean;
  68. /**
  69. * Find the best cut position using scored break points with distance decay.
  70. *
  71. * Uses squared distance for gentler early decay - headings far back still win
  72. * over low-quality breaks near the target.
  73. *
  74. * @param breakPoints - Pre-scanned break points from scanBreakPoints()
  75. * @param targetCharPos - The ideal cut position (e.g., maxChars boundary)
  76. * @param windowChars - How far back to search for break points (default ~200 tokens)
  77. * @param decayFactor - How much to penalize distance (0.7 = 30% score at window edge)
  78. * @param codeFences - Code fence regions to avoid splitting inside
  79. * @returns The best position to cut at
  80. */
  81. export declare function findBestCutoff(breakPoints: BreakPoint[], targetCharPos: number, windowChars?: number, decayFactor?: number, codeFences?: CodeFenceRegion[]): number;
  82. export type ChunkStrategy = "auto" | "regex" | "function";
  83. /**
  84. * Merge two sets of break points (e.g. regex + AST), keeping the highest
  85. * score at each position. Result is sorted by position.
  86. */
  87. export declare function mergeBreakPoints(a: BreakPoint[], b: BreakPoint[]): BreakPoint[];
  88. /**
  89. * Core chunk algorithm that operates on precomputed break points and code fences.
  90. * This is the shared implementation used by both regex-only and AST-aware chunking.
  91. */
  92. export declare function chunkDocumentWithBreakPoints(content: string, breakPoints: BreakPoint[], codeFences: CodeFenceRegion[], maxChars?: number, overlapChars?: number, windowChars?: number): {
  93. text: string;
  94. pos: number;
  95. }[];
  96. export declare const STRONG_SIGNAL_MIN_SCORE = 0.85;
  97. export declare const STRONG_SIGNAL_MIN_GAP = 0.15;
  98. export declare const RERANK_CANDIDATE_LIMIT = 40;
  99. /**
  100. * A typed query expansion result. Decoupled from llm.ts internal Queryable —
  101. * same shape, but store.ts owns its own public API type.
  102. *
  103. * - lex: keyword variant → routes to FTS only
  104. * - vec: semantic variant → routes to vector only
  105. * - hyde: hypothetical document → routes to vector only
  106. */
  107. export type ExpandedQuery = {
  108. type: 'lex' | 'vec' | 'hyde';
  109. query: string;
  110. /** Optional line number for error reporting (CLI parser) */
  111. line?: number;
  112. };
  113. export declare function homedir(): string;
  114. /**
  115. * Check if a path is absolute.
  116. * Supports:
  117. * - Unix paths: /path/to/file
  118. * - Windows native: C:\path or C:/path
  119. * - Git Bash: /c/path or /C/path (C-Z drives, excluding A/B floppy drives)
  120. *
  121. * Note: /c without trailing slash is treated as Unix path (directory named "c"),
  122. * while /c/ or /c/path are treated as Git Bash paths (C: drive).
  123. */
  124. export declare function isAbsolutePath(path: string): boolean;
  125. /**
  126. * Normalize path separators to forward slashes.
  127. * Converts Windows backslashes to forward slashes.
  128. */
  129. export declare function normalizePathSeparators(path: string): string;
  130. /**
  131. * Get the relative path from a prefix.
  132. * Returns null if path is not under prefix.
  133. * Returns empty string if path equals prefix.
  134. */
  135. export declare function getRelativePathFromPrefix(path: string, prefix: string): string | null;
  136. export declare function resolve(...paths: string[]): string;
  137. export declare function enableProductionMode(): void;
  138. /** Reset production mode flag — only for testing. */
  139. export declare function _resetProductionModeForTesting(): void;
  140. export declare function getDefaultDbPath(indexName?: string): string;
  141. export declare function getPwd(): string;
  142. export declare function getRealPath(path: string): string;
  143. export type VirtualPath = {
  144. collectionName: string;
  145. path: string;
  146. };
  147. /**
  148. * Normalize explicit virtual path formats to standard qmd:// format.
  149. * Only handles paths that are already explicitly virtual:
  150. * - qmd://collection/path.md (already normalized)
  151. * - qmd:////collection/path.md (extra slashes - normalize)
  152. * - //collection/path.md (missing qmd: prefix - add it)
  153. *
  154. * Does NOT handle:
  155. * - collection/path.md (bare paths - could be filesystem relative)
  156. * - :linenum suffix (should be parsed separately before calling this)
  157. */
  158. export declare function normalizeVirtualPath(input: string): string;
  159. /**
  160. * Parse a virtual path like "qmd://collection-name/path/to/file.md"
  161. * into its components.
  162. * Also supports collection root: "qmd://collection-name/" or "qmd://collection-name"
  163. */
  164. export declare function parseVirtualPath(virtualPath: string): VirtualPath | null;
  165. /**
  166. * Build a virtual path from collection name and relative path.
  167. */
  168. export declare function buildVirtualPath(collectionName: string, path: string): string;
  169. /**
  170. * Check if a path is explicitly a virtual path.
  171. * Only recognizes explicit virtual path formats:
  172. * - qmd://collection/path.md
  173. * - //collection/path.md
  174. *
  175. * Does NOT consider bare collection/path.md as virtual - that should be
  176. * handled separately by checking if the first component is a collection name.
  177. */
  178. export declare function isVirtualPath(path: string): boolean;
  179. /**
  180. * Resolve a virtual path to absolute filesystem path.
  181. */
  182. export declare function resolveVirtualPath(db: Database, virtualPath: string): string | null;
  183. /**
  184. * Convert an absolute filesystem path to a virtual path.
  185. * Returns null if the file is not in any indexed collection.
  186. */
  187. export declare function toVirtualPath(db: Database, absolutePath: string): string | null;
  188. export declare function verifySqliteVecLoaded(db: Database): void;
  189. /**
  190. * Apply concurrency pragmas with env-var override support. Exported for
  191. * unit tests; consumers should rely on `initializeDatabase` instead.
  192. */
  193. export declare function applyConcurrencyPragmas(db: Database): void;
  194. export declare function getStoreCollections(db: Database): NamedCollection[];
  195. export declare function getStoreCollection(db: Database, name: string): NamedCollection | null;
  196. export declare function getStoreGlobalContext(db: Database): string | undefined;
  197. export declare function getStoreContexts(db: Database): Array<{
  198. collection: string;
  199. path: string;
  200. context: string;
  201. }>;
  202. export declare function upsertStoreCollection(db: Database, name: string, collection: Omit<Collection, 'pattern'> & {
  203. pattern?: string;
  204. }): void;
  205. export declare function deleteStoreCollection(db: Database, name: string): boolean;
  206. export declare function renameStoreCollection(db: Database, oldName: string, newName: string): boolean;
  207. export declare function updateStoreContext(db: Database, collectionName: string, path: string, text: string): boolean;
  208. export declare function removeStoreContext(db: Database, collectionName: string, path: string): boolean;
  209. export declare function setStoreGlobalContext(db: Database, value: string | undefined): void;
  210. /**
  211. * Sync external config (YAML/inline) into SQLite store_collections.
  212. * External config always wins. Skips sync if config hash hasn't changed.
  213. */
  214. export declare function syncConfigToDb(db: Database, config: CollectionConfig): void;
  215. export declare function isSqliteVecAvailable(): boolean;
  216. export type Store = {
  217. db: Database;
  218. dbPath: string;
  219. /** Optional LlamaCpp instance for this store (overrides the global singleton) */
  220. llm?: LlamaCpp;
  221. close: () => void;
  222. ensureVecTable: (dimensions: number) => void;
  223. getHashesNeedingEmbedding: () => number;
  224. getIndexHealth: () => IndexHealthInfo;
  225. getStatus: () => IndexStatus;
  226. getCacheKey: typeof getCacheKey;
  227. getCachedResult: (cacheKey: string) => string | null;
  228. setCachedResult: (cacheKey: string, result: string) => void;
  229. clearCache: () => void;
  230. deleteLLMCache: () => number;
  231. deleteInactiveDocuments: () => number;
  232. cleanupOrphanedContent: () => number;
  233. cleanupOrphanedVectors: () => number;
  234. vacuumDatabase: () => void;
  235. getContextForFile: (filepath: string) => string | null;
  236. getContextForPath: (collectionName: string, path: string) => string | null;
  237. getCollectionByName: (name: string) => {
  238. name: string;
  239. pwd: string;
  240. glob_pattern: string;
  241. } | null;
  242. getCollectionsWithoutContext: () => {
  243. name: string;
  244. pwd: string;
  245. doc_count: number;
  246. }[];
  247. getTopLevelPathsWithoutContext: (collectionName: string) => string[];
  248. parseVirtualPath: typeof parseVirtualPath;
  249. buildVirtualPath: typeof buildVirtualPath;
  250. isVirtualPath: typeof isVirtualPath;
  251. resolveVirtualPath: (virtualPath: string) => string | null;
  252. toVirtualPath: (absolutePath: string) => string | null;
  253. searchFTS: (query: string, limit?: number, collectionName?: string) => SearchResult[];
  254. searchVec: (query: string, model: string, limit?: number, collectionName?: string, session?: ILLMSession, precomputedEmbedding?: number[], embedProvider?: EmbeddingProvider) => Promise<SearchResult[]>;
  255. expandQuery: (query: string, model?: string, intent?: string) => Promise<ExpandedQuery[]>;
  256. rerank: (query: string, documents: {
  257. file: string;
  258. text: string;
  259. }[], model?: string, intent?: string) => Promise<{
  260. file: string;
  261. score: number;
  262. }[]>;
  263. findDocument: (filename: string, options?: {
  264. includeBody?: boolean;
  265. }) => DocumentResult | DocumentNotFound;
  266. getDocumentBody: (doc: DocumentResult | {
  267. filepath: string;
  268. }, fromLine?: number, maxLines?: number) => string | null;
  269. findDocuments: (pattern: string, options?: {
  270. includeBody?: boolean;
  271. maxBytes?: number;
  272. }) => {
  273. docs: MultiGetResult[];
  274. errors: string[];
  275. };
  276. findSimilarFiles: (query: string, maxDistance?: number, limit?: number) => string[];
  277. matchFilesByGlob: (pattern: string) => {
  278. filepath: string;
  279. displayPath: string;
  280. bodyLength: number;
  281. }[];
  282. findDocumentByDocid: (docid: string) => {
  283. filepath: string;
  284. hash: string;
  285. } | null;
  286. insertContent: (hash: string, content: string, createdAt: string) => void;
  287. insertDocument: (collectionName: string, path: string, title: string, hash: string, createdAt: string, modifiedAt: string) => void;
  288. findActiveDocument: (collectionName: string, path: string) => {
  289. id: number;
  290. hash: string;
  291. title: string;
  292. } | null;
  293. updateDocumentTitle: (documentId: number, title: string, modifiedAt: string) => void;
  294. updateDocument: (documentId: number, title: string, hash: string, modifiedAt: string) => void;
  295. deactivateDocument: (collectionName: string, path: string) => void;
  296. getActiveDocumentPaths: (collectionName: string) => string[];
  297. getHashesForEmbedding: () => {
  298. hash: string;
  299. body: string;
  300. path: string;
  301. }[];
  302. clearAllEmbeddings: () => void;
  303. insertEmbedding: (hash: string, seq: number, pos: number, embedding: Float32Array, model: string, embeddedAt: string) => void;
  304. };
  305. export type ReindexProgress = {
  306. file: string;
  307. current: number;
  308. total: number;
  309. };
  310. export type ReindexResult = {
  311. indexed: number;
  312. updated: number;
  313. unchanged: number;
  314. removed: number;
  315. orphanedCleaned: number;
  316. };
  317. /**
  318. * Re-index a single collection by scanning the filesystem and updating the database.
  319. * Pure function — no console output, no db lifecycle management.
  320. */
  321. export declare function reindexCollection(store: Store, collectionPath: string, globPattern: string, collectionName: string, options?: {
  322. ignorePatterns?: string[];
  323. onProgress?: (info: ReindexProgress) => void;
  324. }): Promise<ReindexResult>;
  325. export type EmbedProgress = {
  326. chunksEmbedded: number;
  327. totalChunks: number;
  328. bytesProcessed: number;
  329. totalBytes: number;
  330. /** Chunks that were sent to the provider and came back without an embedding. */
  331. errors: number;
  332. /**
  333. * Chunks the run gave up on WITHOUT sending them (abort / expired session).
  334. * Kept separate from `errors` so "we stopped early" can never be reported as
  335. * "N chunks failed" — see `generateEmbeddings` (i-yghj098h).
  336. */
  337. skipped?: number;
  338. };
  339. export type EmbedResult = {
  340. docsProcessed: number;
  341. chunksEmbedded: number;
  342. /** Attempted-and-failed chunks only. Never includes un-attempted ones. */
  343. errors: number;
  344. /** Un-attempted chunks left behind by an early abort. */
  345. skipped?: number;
  346. durationMs: number;
  347. };
  348. export type EmbedOptions = {
  349. force?: boolean;
  350. model?: string;
  351. maxDocsPerBatch?: number;
  352. maxBatchBytes?: number;
  353. chunkStrategy?: ChunkStrategy;
  354. onProgress?: (info: EmbedProgress) => void;
  355. /**
  356. * Required provider for embedding work. Embeddings are routed through the
  357. * approved commercial HTTPS API. The provider's `getModelId()` is verified against existing
  358. * `content_vectors.model` rows; mismatch throws unless `force` is set.
  359. *
  360. * When omitted, learned work reaches the fail-closed compatibility adapter
  361. * and returns typed HOLD.
  362. */
  363. embedProvider?: EmbeddingProvider;
  364. /**
  365. * Optional collection name filter (i-ofojj7dy). When set, only content
  366. * hashes that have at least one document in this collection are embedded.
  367. * `getPendingEmbeddingDocs` filters at the SQL level. Callers are expected
  368. * to validate the name against `listCollections(db)` first; passing an
  369. * unknown name yields zero pending docs (no work, no error).
  370. */
  371. collection?: string;
  372. };
  373. /**
  374. * Record how many chunks a document was split into. Called at chunk time, before
  375. * the embeddings are attempted — see {@link PENDING_EMBEDDING_PREDICATE}.
  376. * `INSERT OR REPLACE` because a chunkStrategy change legitimately changes the
  377. * count, and the newest chunking is the one the vectors will match.
  378. */
  379. export declare function recordDocumentChunkCount(db: Database, hash: string, chunks: number, chunkedAt: string): void;
  380. /**
  381. * Generate vector embeddings for documents that need them.
  382. * Pure function — no console output, no db lifecycle management.
  383. * Uses the store's LlamaCpp instance if set, otherwise the global singleton.
  384. */
  385. export declare function generateEmbeddings(store: Store, options?: EmbedOptions): Promise<EmbedResult>;
  386. /**
  387. * Create a new store instance with the given database path.
  388. * If no path is provided, uses the default path (~/.cache/qmd/index.sqlite).
  389. *
  390. * @param dbPath - Path to the SQLite database file
  391. * @returns Store instance with all methods bound to the database
  392. */
  393. export declare function createStore(dbPath?: string): Store;
  394. /**
  395. * Unified document result type with all metadata.
  396. * Body is optional - use getDocumentBody() to load it separately if needed.
  397. */
  398. export type DocumentResult = {
  399. filepath: string;
  400. displayPath: string;
  401. title: string;
  402. context: string | null;
  403. hash: string;
  404. docid: string;
  405. collectionName: string;
  406. modifiedAt: string;
  407. bodyLength: number;
  408. body?: string;
  409. };
  410. /**
  411. * Extract short docid from a full hash (first 6 characters).
  412. */
  413. export declare function getDocid(hash: string): string;
  414. export declare function handelize(path: string): string;
  415. /**
  416. * Search result extends DocumentResult with score and source info
  417. */
  418. export type SearchResult = DocumentResult & {
  419. score: number;
  420. source: "fts" | "vec";
  421. chunkPos?: number;
  422. };
  423. /**
  424. * Ranked result for RRF fusion (simplified, used internally)
  425. */
  426. export type RankedResult = {
  427. file: string;
  428. displayPath: string;
  429. title: string;
  430. body: string;
  431. score: number;
  432. };
  433. export type RRFContributionTrace = {
  434. listIndex: number;
  435. source: "fts" | "vec";
  436. queryType: "original" | "lex" | "vec" | "hyde";
  437. query: string;
  438. rank: number;
  439. weight: number;
  440. backendScore: number;
  441. rrfContribution: number;
  442. };
  443. export type RRFScoreTrace = {
  444. contributions: RRFContributionTrace[];
  445. baseScore: number;
  446. topRank: number;
  447. topRankBonus: number;
  448. totalScore: number;
  449. };
  450. export type HybridQueryExplain = {
  451. ftsScores: number[];
  452. vectorScores: number[];
  453. rrf: {
  454. rank: number;
  455. positionScore: number;
  456. weight: number;
  457. baseScore: number;
  458. topRankBonus: number;
  459. totalScore: number;
  460. contributions: RRFContributionTrace[];
  461. };
  462. rerankScore: number;
  463. blendedScore: number;
  464. };
  465. /**
  466. * Error result when document is not found
  467. */
  468. export type DocumentNotFound = {
  469. error: "not_found";
  470. query: string;
  471. similarFiles: string[];
  472. };
  473. /**
  474. * Result from multi-get operations
  475. */
  476. export type MultiGetResult = {
  477. doc: DocumentResult;
  478. skipped: false;
  479. } | {
  480. doc: Pick<DocumentResult, "filepath" | "displayPath">;
  481. skipped: true;
  482. skipReason: string;
  483. };
  484. export type CollectionInfo = {
  485. name: string;
  486. path: string | null;
  487. pattern: string | null;
  488. documents: number;
  489. lastUpdated: string;
  490. };
  491. export type IndexStatus = {
  492. totalDocuments: number;
  493. needsEmbedding: number;
  494. hasVectorIndex: boolean;
  495. collections: CollectionInfo[];
  496. };
  497. export declare function getHashesNeedingEmbedding(db: Database, collection?: string): number;
  498. export type IndexHealthInfo = {
  499. needsEmbedding: number;
  500. totalDocs: number;
  501. daysStale: number | null;
  502. };
  503. export declare function getIndexHealth(db: Database): IndexHealthInfo;
  504. export declare function getCacheKey(url: string, body: object): string;
  505. export declare function getCachedResult(db: Database, cacheKey: string): string | null;
  506. export declare function setCachedResult(db: Database, cacheKey: string, result: string): void;
  507. export declare function clearCache(db: Database): void;
  508. /**
  509. * Delete cached LLM API responses.
  510. * Returns the number of cached responses deleted.
  511. */
  512. export declare function deleteLLMCache(db: Database): number;
  513. /**
  514. * Remove inactive document records (active = 0).
  515. * Returns the number of inactive documents deleted.
  516. */
  517. export declare function deleteInactiveDocuments(db: Database): number;
  518. /**
  519. * Remove orphaned content hashes that are not referenced by any active document.
  520. * Returns the number of orphaned content hashes deleted.
  521. */
  522. export declare function cleanupOrphanedContent(db: Database): number;
  523. /**
  524. * Remove orphaned vector embeddings that are not referenced by any active document.
  525. * Returns the number of orphaned embedding chunks deleted.
  526. */
  527. export declare function cleanupOrphanedVectors(db: Database): number;
  528. /**
  529. * Run VACUUM to reclaim unused space in the database.
  530. * This operation rebuilds the database file to eliminate fragmentation.
  531. */
  532. export declare function vacuumDatabase(db: Database): void;
  533. export declare function hashContent(content: string): Promise<string>;
  534. export declare function extractTitle(content: string, filename: string): string;
  535. /**
  536. * Insert content into the content table (content-addressable storage).
  537. * Uses INSERT OR IGNORE so duplicate hashes are skipped.
  538. */
  539. export declare function insertContent(db: Database, hash: string, content: string, createdAt: string): void;
  540. /**
  541. * Insert a new document into the documents table.
  542. */
  543. export declare function insertDocument(db: Database, collectionName: string, path: string, title: string, hash: string, createdAt: string, modifiedAt: string): void;
  544. /**
  545. * Find an active document by collection name and path.
  546. */
  547. export declare function findActiveDocument(db: Database, collectionName: string, path: string): {
  548. id: number;
  549. hash: string;
  550. title: string;
  551. } | null;
  552. /**
  553. * Update the title and modified_at timestamp for a document.
  554. */
  555. export declare function updateDocumentTitle(db: Database, documentId: number, title: string, modifiedAt: string): void;
  556. /**
  557. * Update an existing document's hash, title, and modified_at timestamp.
  558. * Used when content changes but the file path stays the same.
  559. */
  560. export declare function updateDocument(db: Database, documentId: number, title: string, hash: string, modifiedAt: string): void;
  561. /**
  562. * Deactivate a document (mark as inactive but don't delete).
  563. */
  564. export declare function deactivateDocument(db: Database, collectionName: string, path: string): void;
  565. /**
  566. * Get all active document paths for a collection.
  567. */
  568. export declare function getActiveDocumentPaths(db: Database, collectionName: string): string[];
  569. export { formatQueryForEmbedding, formatDocForEmbedding };
  570. /**
  571. * Chunk a document using regex-only break point detection.
  572. * This is the sync, backward-compatible API used by tests and legacy callers.
  573. */
  574. export declare function chunkDocument(content: string, maxChars?: number, overlapChars?: number, windowChars?: number): {
  575. text: string;
  576. pos: number;
  577. }[];
  578. /**
  579. * Async AST-aware chunking. Detects language from filepath, computes AST
  580. * break points for supported code files, merges with regex break points,
  581. * and delegates to the shared chunk algorithm.
  582. *
  583. * Strategies:
  584. * - "regex" (default) — char-based chunking with regex break points only.
  585. * - "auto" — regex break points merged with AST break points (soft hints).
  586. * - "function" — one chunk per AST function range (Phase 2); inter-range
  587. * gaps (imports, top-level code) are char-chunked with AST
  588. * hints. Falls back to "auto" when zero ranges are detected.
  589. */
  590. export declare function chunkDocumentAsync(content: string, maxChars?: number, overlapChars?: number, windowChars?: number, filepath?: string, chunkStrategy?: ChunkStrategy): Promise<{
  591. text: string;
  592. pos: number;
  593. }[]>;
  594. /**
  595. * Counts the tokens in `text`. Used by `chunkDocumentByTokens` for the
  596. * safety re-split that splits chunks exceeding `maxTokens`.
  597. *
  598. * When `chunkDocumentByTokens` is called without a tokenizer, the disabled
  599. * compatibility adapter returns typed HOLD.
  600. *
  601. * Commercial-provider callers pass a deterministic JS-only approximator. A char-based estimate like
  602. * `Math.ceil(text.length / 3)` is a reasonable default — it matches the
  603. * `avgCharsPerToken=3` heuristic used for the initial char-space chunk
  604. * step, so the safety re-split stays a near no-op while populating the
  605. * `tokens` field with a stable estimate.
  606. */
  607. export type TokenCounter = (text: string) => number | Promise<number>;
  608. /**
  609. * Chunk a document with an injected token counter.
  610. *
  611. * When `tokenizer` is supplied, no compatibility learned adapter is invoked.
  612. *
  613. * When `filepath` and `chunkStrategy` are provided, uses AST-aware break
  614. * points for supported code files.
  615. */
  616. export declare function chunkDocumentByTokens(content: string, maxTokens?: number, overlapTokens?: number, windowTokens?: number, filepath?: string, chunkStrategy?: ChunkStrategy, signal?: AbortSignal, tokenizer?: TokenCounter): Promise<{
  617. text: string;
  618. pos: number;
  619. tokens: number;
  620. }[]>;
  621. /**
  622. * Normalize a docid input by stripping surrounding quotes and leading #.
  623. * Handles: "#abc123", 'abc123', "abc123", #abc123, abc123
  624. * Returns the bare hex string.
  625. */
  626. export declare function normalizeDocid(docid: string): string;
  627. /**
  628. * Check if a string looks like a docid reference.
  629. * Accepts: #abc123, abc123, "#abc123", "abc123", '#abc123', 'abc123'
  630. * Returns true if the normalized form is a valid hex string of 6+ chars.
  631. */
  632. export declare function isDocid(input: string): boolean;
  633. /**
  634. * Find a document by its short docid (first 6 characters of hash).
  635. * Returns the document's virtual path if found, null otherwise.
  636. * If multiple documents match the same short hash (collision), returns the first one.
  637. *
  638. * Accepts lenient input: #abc123, abc123, "#abc123", "abc123"
  639. */
  640. export declare function findDocumentByDocid(db: Database, docid: string): {
  641. filepath: string;
  642. hash: string;
  643. } | null;
  644. export declare function findSimilarFiles(db: Database, query: string, maxDistance?: number, limit?: number): string[];
  645. export declare function matchFilesByGlob(db: Database, pattern: string): {
  646. filepath: string;
  647. displayPath: string;
  648. bodyLength: number;
  649. }[];
  650. /**
  651. * Get context for a file path using hierarchical inheritance.
  652. * Contexts are collection-scoped and inherit from parent directories.
  653. * For example, context at "/talks" applies to "/talks/2024/keynote.md".
  654. *
  655. * @param db Database instance (unused - kept for compatibility)
  656. * @param collectionName Collection name
  657. * @param path Relative path within the collection
  658. * @returns Context string or null if no context is defined
  659. */
  660. export declare function getContextForPath(db: Database, collectionName: string, path: string): string | null;
  661. /**
  662. * Get context for a file path (virtual or filesystem).
  663. * Resolves the collection and relative path from the DB store_collections table.
  664. */
  665. export declare function getContextForFile(db: Database, filepath: string): string | null;
  666. /**
  667. * Get collection by name from DB store_collections table.
  668. */
  669. export declare function getCollectionByName(db: Database, name: string): {
  670. name: string;
  671. pwd: string;
  672. glob_pattern: string;
  673. } | null;
  674. /**
  675. * List all collections with document counts from database.
  676. * Merges store_collections config with database statistics.
  677. */
  678. export declare function listCollections(db: Database): {
  679. name: string;
  680. pwd: string;
  681. glob_pattern: string;
  682. doc_count: number;
  683. active_count: number;
  684. last_modified: string | null;
  685. includeByDefault: boolean;
  686. }[];
  687. /**
  688. * Remove a collection and clean up its documents.
  689. * Uses collections.ts to remove from YAML config and cleans up database.
  690. */
  691. export declare function removeCollection(db: Database, collectionName: string): {
  692. deletedDocs: number;
  693. cleanedHashes: number;
  694. };
  695. /**
  696. * Rename a collection.
  697. * Updates both YAML config and database documents table.
  698. */
  699. export declare function renameCollection(db: Database, oldName: string, newName: string): void;
  700. /**
  701. * Insert or update a context for a specific collection and path prefix.
  702. */
  703. export declare function insertContext(db: Database, collectionId: number, pathPrefix: string, context: string): void;
  704. /**
  705. * Delete a context for a specific collection and path prefix.
  706. * Returns the number of contexts deleted.
  707. */
  708. export declare function deleteContext(db: Database, collectionName: string, pathPrefix: string): number;
  709. /**
  710. * Delete all global contexts (contexts with empty path_prefix).
  711. * Returns the number of contexts deleted.
  712. */
  713. export declare function deleteGlobalContexts(db: Database): number;
  714. /**
  715. * List all contexts, grouped by collection.
  716. * Returns contexts ordered by collection name, then by path prefix length (longest first).
  717. */
  718. export declare function listPathContexts(db: Database): {
  719. collection_name: string;
  720. path_prefix: string;
  721. context: string;
  722. }[];
  723. /**
  724. * Get all collections (name only - from YAML config).
  725. */
  726. export declare function getAllCollections(db: Database): {
  727. name: string;
  728. }[];
  729. /**
  730. * Check which collections don't have any context defined.
  731. * Returns collections that have no context entries at all (not even root context).
  732. */
  733. export declare function getCollectionsWithoutContext(db: Database): {
  734. name: string;
  735. pwd: string;
  736. doc_count: number;
  737. }[];
  738. /**
  739. * Get top-level directories in a collection that don't have context.
  740. * Useful for suggesting where context might be needed.
  741. */
  742. export declare function getTopLevelPathsWithoutContext(db: Database, collectionName: string): string[];
  743. export declare function sanitizeFTS5Term(term: string): string;
  744. /**
  745. * Validate that a vec/hyde query doesn't use lex-only syntax.
  746. * Returns error message if invalid, null if valid.
  747. *
  748. * Negation is detected ONLY when `-` is preceded by whitespace or sits at
  749. * the start of the query. Hyphens inside words (e.g. `auto-archived`,
  750. * `pre-commit`, `multi-session`, `state-of-the-art`) carry no negation
  751. * semantics in natural English and must pass through unchanged.
  752. */
  753. export declare function validateSemanticQuery(query: string): string | null;
  754. export declare function validateLexQuery(query: string): string | null;
  755. export declare function searchFTS(db: Database, query: string, limit?: number, collectionName?: string): SearchResult[];
  756. export declare function searchVec(db: Database, query: string, model: string, limit?: number, collectionName?: string, session?: ILLMSession, precomputedEmbedding?: number[], embedProvider?: EmbeddingProvider): Promise<SearchResult[]>;
  757. /**
  758. * Get all unique content hashes that need embeddings (from active documents).
  759. * Returns hash, document body, and a sample path for display purposes.
  760. */
  761. export declare function getHashesForEmbedding(db: Database): {
  762. hash: string;
  763. body: string;
  764. path: string;
  765. }[];
  766. /**
  767. * Clear all embeddings from the database (force re-index).
  768. * Deletes all rows from content_vectors and drops the vectors_vec table.
  769. */
  770. export declare function clearAllEmbeddings(db: Database): void;
  771. /**
  772. * Get the distinct set of model identifiers present in `content_vectors`.
  773. *
  774. * Used by the embedding migration-safety guard: if a configured provider's
  775. * `getModelId()` does not appear in this list (and the table is non-empty),
  776. * we refuse to embed and ask the user to run `qmd embed -f` to rebuild.
  777. *
  778. * Returns `[]` when the table is empty (fresh DB) — in which case any
  779. * provider is allowed.
  780. */
  781. export declare function getDistinctEmbeddingModels(db: Database): string[];
  782. /**
  783. * Insert a single embedding into both content_vectors and vectors_vec tables.
  784. * The hash_seq key is formatted as "hash_seq" for the vectors_vec table.
  785. *
  786. * content_vectors is inserted first so that getHashesForEmbedding (which checks
  787. * only content_vectors) won't re-select the hash on a crash between the two inserts.
  788. *
  789. * vectors_vec uses DELETE + INSERT instead of INSERT OR REPLACE because sqlite-vec's
  790. * vec0 virtual tables silently ignore the OR REPLACE conflict clause.
  791. */
  792. export declare function insertEmbedding(db: Database, hash: string, seq: number, pos: number, embedding: Float32Array, model: string, embeddedAt: string): void;
  793. export declare function expandQuery(query: string, model: string | undefined, db: Database, intent?: string, llmOverride?: LlamaCpp): Promise<ExpandedQuery[]>;
  794. export declare function rerank(query: string, documents: {
  795. file: string;
  796. text: string;
  797. }[], model: string | undefined, db: Database, intent?: string, llmOverride?: LlamaCpp): Promise<{
  798. file: string;
  799. score: number;
  800. }[]>;
  801. export declare function reciprocalRankFusion(resultLists: RankedResult[][], weights?: number[], k?: number): RankedResult[];
  802. /**
  803. * Build per-document RRF contribution traces for explain/debug output.
  804. */
  805. export declare function buildRrfTrace(resultLists: RankedResult[][], weights?: number[], listMeta?: RankedListMeta[], k?: number): Map<string, RRFScoreTrace>;
  806. /**
  807. * Find a document by filename/path, docid (#hash), or with fuzzy matching.
  808. * Returns document metadata without body by default.
  809. *
  810. * Supports:
  811. * - Virtual paths: qmd://collection/path/to/file.md
  812. * - Absolute paths: /path/to/file.md
  813. * - Relative paths: path/to/file.md
  814. * - Short docid: #abc123 (first 6 chars of hash)
  815. */
  816. export declare function findDocument(db: Database, filename: string, options?: {
  817. includeBody?: boolean;
  818. }): DocumentResult | DocumentNotFound;
  819. /**
  820. * Get the body content for a document
  821. * Optionally slice by line range
  822. */
  823. export declare function getDocumentBody(db: Database, doc: DocumentResult | {
  824. filepath: string;
  825. }, fromLine?: number, maxLines?: number): string | null;
  826. /**
  827. * Find multiple documents by glob pattern or comma-separated list
  828. * Returns documents without body by default (use getDocumentBody to load)
  829. */
  830. export declare function findDocuments(db: Database, pattern: string, options?: {
  831. includeBody?: boolean;
  832. maxBytes?: number;
  833. }): {
  834. docs: MultiGetResult[];
  835. errors: string[];
  836. };
  837. export declare function getStatus(db: Database): IndexStatus;
  838. export type SnippetResult = {
  839. line: number;
  840. snippet: string;
  841. linesBefore: number;
  842. linesAfter: number;
  843. snippetLines: number;
  844. };
  845. /** Weight for intent terms relative to query terms (1.0) in snippet scoring */
  846. export declare const INTENT_WEIGHT_SNIPPET = 0.3;
  847. /** Weight for intent terms relative to query terms (1.0) in chunk selection */
  848. export declare const INTENT_WEIGHT_CHUNK = 0.5;
  849. /**
  850. * Extract meaningful terms from an intent string, filtering stop words and punctuation.
  851. * Uses Unicode-aware punctuation stripping so domain terms like "API" survive.
  852. * Returns lowercase terms suitable for text matching.
  853. */
  854. export declare function extractIntentTerms(intent: string): string[];
  855. export declare function extractSnippet(body: string, query: string, maxLen?: number, chunkPos?: number, chunkLen?: number, intent?: string): SnippetResult;
  856. /**
  857. * Add line numbers to text content.
  858. * Each line becomes: "{lineNum}: {content}"
  859. */
  860. export declare function addLineNumbers(text: string, startLine?: number): string;
  861. /**
  862. * Optional progress hooks for search orchestration.
  863. * CLI wires these to stderr for user feedback; MCP leaves them unset.
  864. */
  865. export interface SearchHooks {
  866. /** BM25 probe found strong signal — expansion will be skipped */
  867. onStrongSignal?: (topScore: number) => void;
  868. /** Query expansion starting */
  869. onExpandStart?: () => void;
  870. /** Query expansion complete. Empty array = strong signal skip. elapsedMs = time taken. */
  871. onExpand?: (original: string, expanded: ExpandedQuery[], elapsedMs: number) => void;
  872. /** Embedding starting (vec/hyde queries) */
  873. onEmbedStart?: (count: number) => void;
  874. /** Embedding complete */
  875. onEmbedDone?: (elapsedMs: number) => void;
  876. /** Reranking is about to start */
  877. onRerankStart?: (chunkCount: number) => void;
  878. /** Reranking finished */
  879. onRerankDone?: (elapsedMs: number) => void;
  880. }
  881. export interface HybridQueryOptions {
  882. collection?: string;
  883. limit?: number;
  884. minScore?: number;
  885. candidateLimit?: number;
  886. explain?: boolean;
  887. intent?: string;
  888. skipRerank?: boolean;
  889. chunkStrategy?: ChunkStrategy;
  890. hooks?: SearchHooks;
  891. /**
  892. * Optional embedding provider for query-side encoding (i-loazq6ze).
  893. * When supplied, the original-query vector AND any vec/hyde expansion
  894. * variants are encoded through this commercial provider. Without one,
  895. * learned work returns typed HOLD.
  896. */
  897. embedProvider?: EmbeddingProvider;
  898. }
  899. export interface HybridQueryResult {
  900. file: string;
  901. displayPath: string;
  902. title: string;
  903. body: string;
  904. bestChunk: string;
  905. bestChunkPos: number;
  906. score: number;
  907. context: string | null;
  908. docid: string;
  909. explain?: HybridQueryExplain;
  910. }
  911. export type RankedListMeta = {
  912. source: "fts" | "vec";
  913. queryType: "original" | "lex" | "vec" | "hyde";
  914. query: string;
  915. };
  916. /**
  917. * Hybrid search: BM25 + vector + query expansion + RRF + chunked reranking.
  918. *
  919. * Pipeline:
  920. * 1. BM25 probe → skip expansion if strong signal
  921. * 2. expandQuery() → typed query variants (lex/vec/hyde)
  922. * 3. Type-routed search: original→vector, lex→FTS, vec/hyde→vector
  923. * 4. RRF fusion → slice to candidateLimit
  924. * 5. chunkDocument() + keyword-best-chunk selection
  925. * 6. rerank on chunks (NOT full bodies — O(tokens) trap)
  926. * 7. Position-aware score blending (RRF rank × reranker score)
  927. * 8. Dedup by file, filter by minScore, slice to limit
  928. */
  929. export declare function hybridQuery(store: Store, query: string, options?: HybridQueryOptions): Promise<HybridQueryResult[]>;
  930. export interface VectorSearchOptions {
  931. collection?: string;
  932. limit?: number;
  933. minScore?: number;
  934. intent?: string;
  935. hooks?: Pick<SearchHooks, 'onExpand'>;
  936. /**
  937. * Optional embedding provider for query-side encoding (i-loazq6ze).
  938. * When supplied, query vectors are encoded through the commercial API.
  939. * Without one, learned work returns typed HOLD.
  940. */
  941. embedProvider?: EmbeddingProvider;
  942. }
  943. export interface VectorSearchResult {
  944. file: string;
  945. displayPath: string;
  946. title: string;
  947. body: string;
  948. score: number;
  949. context: string | null;
  950. docid: string;
  951. }
  952. /**
  953. * Vector-only semantic search with query expansion.
  954. *
  955. * Pipeline:
  956. * 1. expandQuery() → typed variants, filter to vec/hyde only (lex irrelevant here)
  957. * 2. searchVec() for original + vec/hyde variants through the commercial provider
  958. * 3. Dedup by filepath (keep max score)
  959. * 4. Sort by score descending, filter by minScore, slice to limit
  960. */
  961. export declare function vectorSearchQuery(store: Store, query: string, options?: VectorSearchOptions): Promise<VectorSearchResult[]>;
  962. /**
  963. * A single sub-search in a structured search request.
  964. * Matches the format used in QMD training data.
  965. */
  966. export interface StructuredSearchOptions {
  967. collections?: string[];
  968. limit?: number;
  969. minScore?: number;
  970. candidateLimit?: number;
  971. explain?: boolean;
  972. /** Domain intent hint for disambiguation — steers reranking and chunk selection */
  973. intent?: string;
  974. /** Skip LLM reranking, use only RRF scores */
  975. skipRerank?: boolean;
  976. chunkStrategy?: ChunkStrategy;
  977. hooks?: SearchHooks;
  978. /**
  979. * Optional embedding provider for query-side encoding (i-loazq6ze).
  980. * When supplied, vec/hyde sub-queries are batch-encoded via the provider
  981. * (HTTP / GPU worker / fallback chain) instead of `getLlm(store).embedBatch`.
  982. */
  983. embedProvider?: EmbeddingProvider;
  984. }
  985. /**
  986. * Structured search: execute pre-expanded queries without LLM query expansion.
  987. *
  988. * Designed for LLM callers (MCP/HTTP) that generate their own query expansions.
  989. * Skips the internal expandQuery() step — goes directly to:
  990. *
  991. * Pipeline:
  992. * 1. Route searches: lex→FTS, vec/hyde→vector (batch embed)
  993. * 2. RRF fusion across all result lists
  994. * 3. Chunk documents + keyword-best-chunk selection
  995. * 4. Rerank on chunks
  996. * 5. Position-aware score blending
  997. * 6. Dedup, filter, slice
  998. *
  999. * This is the recommended endpoint when the caller supplies domain-specific
  1000. * query variants and a commercial provider contract is active.
  1001. */
  1002. export declare function structuredSearch(store: Store, searches: ExpandedQuery[], options?: StructuredSearchOptions): Promise<HybridQueryResult[]>;