@wei840222/qmd 2026.8.23

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 (94) hide show
  1. package/CHANGELOG.md +1373 -0
  2. package/LICENSE +45 -0
  3. package/README.md +1439 -0
  4. package/THIRD_PARTY_NOTICES.md +31 -0
  5. package/bin/qmd +192 -0
  6. package/dist/ast.d.ts +65 -0
  7. package/dist/ast.js +334 -0
  8. package/dist/bench/bench.d.ts +35 -0
  9. package/dist/bench/bench.js +338 -0
  10. package/dist/bench/cjk-baseline.d.ts +36 -0
  11. package/dist/bench/cjk-baseline.js +111 -0
  12. package/dist/bench/fixture.d.ts +2 -0
  13. package/dist/bench/fixture.js +84 -0
  14. package/dist/bench/score.d.ts +38 -0
  15. package/dist/bench/score.js +107 -0
  16. package/dist/bench/types.d.ts +110 -0
  17. package/dist/bench/types.js +8 -0
  18. package/dist/cli/build-info.json +4 -0
  19. package/dist/cli/embed-lock.d.ts +24 -0
  20. package/dist/cli/embed-lock.js +94 -0
  21. package/dist/cli/embedding-owner.d.ts +10 -0
  22. package/dist/cli/embedding-owner.js +20 -0
  23. package/dist/cli/formatter.d.ts +120 -0
  24. package/dist/cli/formatter.js +355 -0
  25. package/dist/cli/mcp-pid.d.ts +25 -0
  26. package/dist/cli/mcp-pid.js +86 -0
  27. package/dist/cli/qmd.d.ts +72 -0
  28. package/dist/cli/qmd.js +4806 -0
  29. package/dist/cli/version.d.ts +42 -0
  30. package/dist/cli/version.js +80 -0
  31. package/dist/collections.d.ts +200 -0
  32. package/dist/collections.js +433 -0
  33. package/dist/db.d.ts +65 -0
  34. package/dist/db.js +143 -0
  35. package/dist/diagnostics.d.ts +62 -0
  36. package/dist/diagnostics.js +260 -0
  37. package/dist/embedding/config.d.ts +52 -0
  38. package/dist/embedding/config.js +229 -0
  39. package/dist/embedding/identity.d.ts +58 -0
  40. package/dist/embedding/identity.js +321 -0
  41. package/dist/embedding/local-identity.d.ts +1 -0
  42. package/dist/embedding/local-identity.js +15 -0
  43. package/dist/embedding/local.d.ts +34 -0
  44. package/dist/embedding/local.js +290 -0
  45. package/dist/embedding/openai.d.ts +79 -0
  46. package/dist/embedding/openai.js +477 -0
  47. package/dist/embedding/owner.d.ts +13 -0
  48. package/dist/embedding/owner.js +36 -0
  49. package/dist/embedding/provider.d.ts +68 -0
  50. package/dist/embedding/provider.js +16 -0
  51. package/dist/embedding/remote-chunking.d.ts +22 -0
  52. package/dist/embedding/remote-chunking.js +83 -0
  53. package/dist/embedding/remote-embedding.d.ts +15 -0
  54. package/dist/embedding/remote-embedding.js +77 -0
  55. package/dist/hybrid-llm.d.ts +18 -0
  56. package/dist/hybrid-llm.js +53 -0
  57. package/dist/index.d.ts +244 -0
  58. package/dist/index.js +418 -0
  59. package/dist/llm.d.ts +566 -0
  60. package/dist/llm.js +1847 -0
  61. package/dist/maintenance.d.ts +33 -0
  62. package/dist/maintenance.js +52 -0
  63. package/dist/mcp/origin-guard.d.ts +67 -0
  64. package/dist/mcp/origin-guard.js +137 -0
  65. package/dist/mcp/server.d.ts +116 -0
  66. package/dist/mcp/server.js +919 -0
  67. package/dist/paths.d.ts +1 -0
  68. package/dist/paths.js +4 -0
  69. package/dist/remote-llm.d.ts +52 -0
  70. package/dist/remote-llm.js +464 -0
  71. package/dist/search/cjk-analyzer.d.ts +33 -0
  72. package/dist/search/cjk-analyzer.js +158 -0
  73. package/dist/search/cjk-index.d.ts +104 -0
  74. package/dist/search/cjk-index.js +1031 -0
  75. package/dist/search/jieba-loader.d.ts +23 -0
  76. package/dist/search/jieba-loader.js +79 -0
  77. package/dist/search/query-expansion.d.ts +23 -0
  78. package/dist/search/query-expansion.js +43 -0
  79. package/dist/search/zh-dict.txt +624013 -0
  80. package/dist/store.d.ts +1218 -0
  81. package/dist/store.js +6076 -0
  82. package/dist/trust.d.ts +152 -0
  83. package/dist/trust.js +249 -0
  84. package/package.json +139 -0
  85. package/scripts/build.mjs +83 -0
  86. package/scripts/check-package-grammars.mjs +29 -0
  87. package/scripts/package-smoke.mjs +205 -0
  88. package/scripts/sync-zh-dict.mjs +187 -0
  89. package/scripts/test-all.mjs +45 -0
  90. package/skills/qmd/SKILL.md +324 -0
  91. package/skills/qmd/references/mcp-setup.md +119 -0
  92. package/skills/release/SKILL.md +141 -0
  93. package/skills/release/scripts/install-hooks.sh +38 -0
  94. package/skills/release/scripts/release-context.sh +129 -0
@@ -0,0 +1,1218 @@
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 type { EmbeddingProvider } from "./embedding/provider.js";
15
+ import { type EmbeddingBuildLease, type EmbeddingIdentity } from "./embedding/identity.js";
16
+ import { type ExpansionDecision, type ExpansionMode } from "./search/query-expansion.js";
17
+ import type { LLM } from "./llm.js";
18
+ import { LlamaCpp, formatQueryForEmbedding, formatDocForEmbedding, type ILLMSession } from "./llm.js";
19
+ import type { NamedCollection, Collection, CollectionConfig } from "./collections.js";
20
+ import type { IndexDiagnostics } from "./diagnostics.js";
21
+ export declare const DEFAULT_EMBED_MODEL = "hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf";
22
+ export declare const DEFAULT_RERANK_MODEL = "hf:ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF/qwen3-reranker-0.6b-q8_0.gguf";
23
+ export declare const DEFAULT_QUERY_MODEL = "hf:tobil/qmd-query-expansion-1.7B-gguf/qmd-query-expansion-1.7B-q4_k_m.gguf";
24
+ export declare const DEFAULT_GLOB = "**/*.md";
25
+ /**
26
+ * Split a collection glob mask into fast-glob patterns.
27
+ *
28
+ * `--mask "a.md,*.txt"` is a comma-separated union (issue #557), but
29
+ * fast-glob treats a comma outside `{...}` as a literal character, so
30
+ * the joined string matches nothing. Brace form `{a.md,*.txt}` is
31
+ * already valid glob syntax and is left intact.
32
+ *
33
+ * Commas inside `{...}` or `[...]` are not separators. Empty segments
34
+ * after the split are dropped.
35
+ */
36
+ export declare function splitGlobMask(mask: string): string[];
37
+ export declare const DEFAULT_MULTI_GET_MAX_BYTES: number;
38
+ export declare const DEFAULT_EMBED_MAX_DOCS_PER_BATCH = 64;
39
+ export declare const DEFAULT_EMBED_MAX_BATCH_BYTES: number;
40
+ export declare const DEFAULT_EMBED_MAX_DURATION_MS: number;
41
+ export declare const CHUNK_SIZE_TOKENS = 900;
42
+ export declare const CHUNK_OVERLAP_TOKENS: number;
43
+ export declare const CHUNK_SIZE_CHARS: number;
44
+ export declare const CHUNK_OVERLAP_CHARS: number;
45
+ export declare const CHUNK_WINDOW_TOKENS = 200;
46
+ export declare const CHUNK_WINDOW_CHARS: number;
47
+ export declare function canonicalEmbeddingBuildMaterial(providerIdentity: string, strategy: EmbedOptions["chunkStrategy"]): string;
48
+ export declare function getEmbeddingFingerprint(model?: string): string;
49
+ /**
50
+ * A potential break point in the document with a base score indicating quality.
51
+ */
52
+ export interface BreakPoint {
53
+ pos: number;
54
+ score: number;
55
+ type: string;
56
+ }
57
+ /**
58
+ * A region where a code fence exists (between ``` markers).
59
+ * We should never split inside a code fence.
60
+ */
61
+ export interface CodeFenceRegion {
62
+ start: number;
63
+ end: number;
64
+ }
65
+ /**
66
+ * Patterns for detecting break points in markdown documents.
67
+ * Higher scores indicate better places to split.
68
+ * Scores are spread wide so headings decisively beat lower-quality breaks.
69
+ * Order matters for scoring - more specific patterns first.
70
+ */
71
+ export declare const BREAK_PATTERNS: [RegExp, number, string][];
72
+ /**
73
+ * Scan text for all potential break points.
74
+ * Returns sorted array of break points with higher-scoring patterns taking precedence
75
+ * when multiple patterns match the same position.
76
+ */
77
+ export declare function scanBreakPoints(text: string): BreakPoint[];
78
+ /**
79
+ * Find all code fence regions in the text.
80
+ * Code fences are delimited by ``` and we should never split inside them.
81
+ */
82
+ export declare function findCodeFences(text: string): CodeFenceRegion[];
83
+ /**
84
+ * Check if a position is inside a code fence region.
85
+ */
86
+ export declare function isInsideCodeFence(pos: number, fences: CodeFenceRegion[]): boolean;
87
+ /**
88
+ * Find the best cut position using scored break points with distance decay.
89
+ *
90
+ * Uses squared distance for gentler early decay - headings far back still win
91
+ * over low-quality breaks near the target.
92
+ *
93
+ * @param breakPoints - Pre-scanned break points from scanBreakPoints()
94
+ * @param targetCharPos - The ideal cut position (e.g., maxChars boundary)
95
+ * @param windowChars - How far back to search for break points (default ~200 tokens)
96
+ * @param decayFactor - How much to penalize distance (0.7 = 30% score at window edge)
97
+ * @param codeFences - Code fence regions to avoid splitting inside
98
+ * @returns The best position to cut at
99
+ */
100
+ export declare function findBestCutoff(breakPoints: BreakPoint[], targetCharPos: number, windowChars?: number, decayFactor?: number, codeFences?: CodeFenceRegion[]): number;
101
+ export type ChunkStrategy = "auto" | "regex";
102
+ /**
103
+ * Merge two sets of break points (e.g. regex + AST), keeping the highest
104
+ * score at each position. Result is sorted by position.
105
+ */
106
+ export declare function mergeBreakPoints(a: BreakPoint[], b: BreakPoint[]): BreakPoint[];
107
+ /**
108
+ * Core chunk algorithm that operates on precomputed break points and code fences.
109
+ * This is the shared implementation used by both regex-only and AST-aware chunking.
110
+ */
111
+ export declare function chunkDocumentWithBreakPoints(content: string, breakPoints: BreakPoint[], codeFences: CodeFenceRegion[], maxChars?: number, overlapChars?: number, windowChars?: number): {
112
+ text: string;
113
+ pos: number;
114
+ }[];
115
+ export declare const STRONG_SIGNAL_MIN_SCORE = 0.85;
116
+ export declare const STRONG_SIGNAL_MIN_GAP = 0.15;
117
+ export declare const STRONG_SIGNAL_POLICY_VERSION = "backend-strength-v1";
118
+ export declare const RERANK_CANDIDATE_LIMIT = 40;
119
+ /**
120
+ * A typed query expansion result. Decoupled from llm.ts internal Queryable —
121
+ * same shape, but store.ts owns its own public API type.
122
+ *
123
+ * - lex: keyword variant → routes to FTS only
124
+ * - vec: semantic variant → routes to vector only
125
+ * - hyde: hypothetical document → routes to vector only
126
+ */
127
+ export type ExpandedQuery = {
128
+ type: 'lex' | 'vec' | 'hyde';
129
+ query: string;
130
+ /** Optional line number for error reporting (CLI parser) */
131
+ line?: number;
132
+ };
133
+ export type QueryExpansionOptions = {
134
+ requireResult?: boolean;
135
+ };
136
+ export declare function homedir(): string;
137
+ /**
138
+ * Check if a path is absolute.
139
+ * Supports:
140
+ * - Unix paths: /path/to/file
141
+ * - Windows native: C:\path or C:/path
142
+ * - Git Bash: /c/path or /C/path (C-Z drives, excluding A/B floppy drives)
143
+ *
144
+ * Note: /c without trailing slash is treated as Unix path (directory named "c"),
145
+ * while /c/ or /c/path are treated as Git Bash paths (C: drive).
146
+ */
147
+ export declare function isAbsolutePath(path: string): boolean;
148
+ /**
149
+ * Normalize path separators to forward slashes.
150
+ * Converts Windows backslashes to forward slashes.
151
+ */
152
+ export declare function normalizePathSeparators(path: string): string;
153
+ /**
154
+ * Get the relative path from a prefix.
155
+ * Returns null if path is not under prefix.
156
+ * Returns empty string if path equals prefix.
157
+ */
158
+ export declare function getRelativePathFromPrefix(path: string, prefix: string): string | null;
159
+ export declare function resolve(...paths: string[]): string;
160
+ export declare function enableProductionMode(): void;
161
+ /** Reset production mode flag — only for testing. */
162
+ export declare function _resetProductionModeForTesting(): void;
163
+ export declare function getDefaultDbPath(indexName?: string): string;
164
+ export declare function getPwd(): string;
165
+ export declare function getRealPath(path: string): string;
166
+ /**
167
+ * True if `target` is `dir` or a descendant, after resolving symlinks.
168
+ * Used to keep indexing and qmd:// filesystem resolution inside a collection.
169
+ */
170
+ export declare function isPathInsideDir(dir: string, target: string): boolean;
171
+ export type VirtualPath = {
172
+ collectionName: string;
173
+ path: string;
174
+ indexName?: string;
175
+ };
176
+ /**
177
+ * Normalize explicit virtual path formats to standard qmd:// format.
178
+ * Only handles paths that are already explicitly virtual:
179
+ * - qmd://collection/path.md (already normalized)
180
+ * - qmd:////collection/path.md (extra slashes - normalize)
181
+ * - //collection/path.md (missing qmd: prefix - add it)
182
+ *
183
+ * Does NOT handle:
184
+ * - collection/path.md (bare paths - could be filesystem relative)
185
+ * - :linenum suffix (should be parsed separately before calling this)
186
+ */
187
+ export declare function normalizeVirtualPath(input: string): string;
188
+ /**
189
+ * Parse a virtual path like "qmd://collection-name/path/to/file.md"
190
+ * into its components.
191
+ * Also supports collection root: "qmd://collection-name/" or "qmd://collection-name"
192
+ */
193
+ export declare function parseVirtualPath(virtualPath: string): VirtualPath | null;
194
+ /**
195
+ * Build a virtual path from collection name and relative path.
196
+ */
197
+ export declare function buildVirtualPath(collectionName: string, path: string, indexName?: string): string;
198
+ /**
199
+ * Check if a path is explicitly a virtual path.
200
+ * Only recognizes explicit virtual path formats:
201
+ * - qmd://collection/path.md
202
+ * - //collection/path.md
203
+ *
204
+ * Does NOT consider bare collection/path.md as virtual - that should be
205
+ * handled separately by checking if the first component is a collection name.
206
+ */
207
+ export declare function isVirtualPath(path: string): boolean;
208
+ /**
209
+ * Resolve a virtual path to absolute filesystem path.
210
+ */
211
+ export declare function resolveVirtualPath(db: Database, virtualPath: string): string | null;
212
+ /**
213
+ * Convert an absolute filesystem path to a virtual path.
214
+ * Returns null if the file is not in any indexed collection.
215
+ */
216
+ export declare function toVirtualPath(db: Database, absolutePath: string): string | null;
217
+ export declare function verifySqliteVecLoaded(db: Database): void;
218
+ /**
219
+ * FTS5's unicode61 tokenizer does not segment CJK text into searchable words.
220
+ * Normalize CJK runs by spacing every character so exact CJK queries can be
221
+ * translated into phrase queries while Latin text keeps the default tokenizer.
222
+ */
223
+ export declare function normalizeCjkForFTS(text: string): string;
224
+ export type LegacyFtsMigrationStage = "source-row" | "live-renamed";
225
+ /** @internal Test-only interleaving and rollback injection for the legacy FTS migration. */
226
+ export declare function setLegacyFtsMigrationHookForTests(hook?: (stage: LegacyFtsMigrationStage) => void): void;
227
+ export declare function getStoreCollections(db: Database): NamedCollection[];
228
+ export declare function getStoreCollection(db: Database, name: string): NamedCollection | null;
229
+ export declare function getStoreGlobalContext(db: Database): string | undefined;
230
+ export declare function getStoreContexts(db: Database): Array<{
231
+ collection: string;
232
+ path: string;
233
+ context: string;
234
+ }>;
235
+ export declare function upsertStoreCollection(db: Database, name: string, collection: Omit<Collection, 'pattern'> & {
236
+ pattern?: string;
237
+ }): void;
238
+ export declare function deleteStoreCollection(db: Database, name: string): boolean;
239
+ export declare function renameStoreCollection(db: Database, oldName: string, newName: string): boolean;
240
+ export declare function updateStoreContext(db: Database, collectionName: string, path: string, text: string): boolean;
241
+ export declare function removeStoreContext(db: Database, collectionName: string, path: string): boolean;
242
+ export declare function setStoreGlobalContext(db: Database, value: string | undefined): void;
243
+ /**
244
+ * Sync external config (YAML/inline) into SQLite store_collections.
245
+ * External config always wins. The stored config hash is diagnostic only:
246
+ * SDK or direct SQLite mutations may have changed the materialized rows even
247
+ * when the external config bytes are unchanged, so every sync reconciles the
248
+ * actual rows.
249
+ */
250
+ export type ConfigSyncDiagnostic = {
251
+ configHashChanged: boolean;
252
+ reconciled: boolean;
253
+ collections: {
254
+ added: string[];
255
+ updated: string[];
256
+ removed: string[];
257
+ };
258
+ globalContextUpdated: boolean;
259
+ };
260
+ export declare function syncConfigToDb(db: Database, config: CollectionConfig): ConfigSyncDiagnostic;
261
+ export declare function isSqliteVecAvailable(): boolean;
262
+ export type RemoteRequestAuthorizationContext = {
263
+ identity?: EmbeddingIdentity;
264
+ lease?: EmbeddingBuildLease;
265
+ };
266
+ export type Store = {
267
+ db: Database;
268
+ dbPath: string;
269
+ /** Borrowed embedding provider. Store.close() never disposes it. */
270
+ embeddingProvider?: EmbeddingProvider;
271
+ /** Composition-root policy hook invoked immediately before every remote provider request. */
272
+ authorizeRemoteRequest?: (purpose: "index-build" | "query-embedding", context: RemoteRequestAuthorizationContext) => void;
273
+ /** Policy guard executed under the embedding build write lock before any reset. */
274
+ authorizeRemoteBuildStart?: (identity: EmbeddingIdentity) => void;
275
+ /** Optional local LlamaCpp instance */
276
+ localLlm?: LlamaCpp;
277
+ /** Optional LLM instance for this store (overrides the global singleton) */
278
+ llm?: LLM;
279
+ close: () => void;
280
+ ensureVecTable: (dimensions: number) => void;
281
+ getHashesNeedingEmbedding: (model?: string) => number;
282
+ getIndexHealth: (model?: string) => IndexHealthInfo;
283
+ getStatus: (model?: string) => IndexStatus;
284
+ getCacheKey: typeof getCacheKey;
285
+ getCachedResult: (cacheKey: string) => string | null;
286
+ setCachedResult: (cacheKey: string, result: string) => void;
287
+ clearCache: () => void;
288
+ deleteLLMCache: () => number;
289
+ deleteInactiveDocuments: () => number;
290
+ cleanupOrphanedContent: () => number;
291
+ cleanupOrphanedVectors: () => number;
292
+ vacuumDatabase: () => void;
293
+ getContextForFile: (filepath: string) => string | null;
294
+ getContextForPath: (collectionName: string, path: string) => string | null;
295
+ getCollectionByName: (name: string) => {
296
+ name: string;
297
+ pwd: string;
298
+ glob_pattern: string;
299
+ } | null;
300
+ getCollectionsWithoutContext: () => {
301
+ name: string;
302
+ pwd: string;
303
+ doc_count: number;
304
+ }[];
305
+ getTopLevelPathsWithoutContext: (collectionName: string) => string[];
306
+ parseVirtualPath: typeof parseVirtualPath;
307
+ buildVirtualPath: typeof buildVirtualPath;
308
+ isVirtualPath: typeof isVirtualPath;
309
+ resolveVirtualPath: (virtualPath: string) => string | null;
310
+ toVirtualPath: (absolutePath: string) => string | null;
311
+ searchCharFTS: (query: string, limit?: number, collectionFilter?: CollectionFilter) => SearchResult[];
312
+ searchFTS: (query: string, limit?: number, collectionFilter?: CollectionFilter) => SearchResult[];
313
+ searchVec: (query: string, model: string, limit?: number, collectionFilter?: CollectionFilter, session?: ILLMSession, precomputedEmbedding?: number[]) => Promise<SearchResult[]>;
314
+ expandQuery: (query: string, model?: string, expansionContext?: string, options?: QueryExpansionOptions) => Promise<ExpandedQuery[]>;
315
+ /** Drop the cached expansion for a query so the next call regenerates. */
316
+ invalidateExpansionCache: (query: string, expansionContext?: string) => void;
317
+ rerank: (query: string, documents: {
318
+ file: string;
319
+ text: string;
320
+ }[], model?: string, rerankContext?: string) => Promise<{
321
+ file: string;
322
+ score: number;
323
+ }[]>;
324
+ findDocument: (filename: string, options?: {
325
+ includeBody?: boolean;
326
+ }) => DocumentResult | DocumentLookupError;
327
+ getDocumentBody: (doc: DocumentResult | {
328
+ filepath: string;
329
+ }, fromLine?: number, maxLines?: number) => string | null;
330
+ findDocuments: (pattern: string, options?: {
331
+ includeBody?: boolean;
332
+ maxBytes?: number;
333
+ }) => {
334
+ docs: MultiGetResult[];
335
+ errors: string[];
336
+ };
337
+ findSimilarFiles: (query: string, maxDistance?: number, limit?: number) => string[];
338
+ matchFilesByGlob: (pattern: string) => {
339
+ filepath: string;
340
+ displayPath: string;
341
+ bodyLength: number;
342
+ }[];
343
+ findDocumentByDocid: (docid: string) => {
344
+ filepath: string;
345
+ hash: string;
346
+ } | null;
347
+ insertContent: (hash: string, content: string, createdAt: string) => void;
348
+ insertDocument: (collectionName: string, path: string, title: string, hash: string, createdAt: string, modifiedAt: string) => void;
349
+ findActiveDocument: (collectionName: string, path: string) => {
350
+ id: number;
351
+ hash: string;
352
+ title: string;
353
+ } | null;
354
+ findOrMigrateLegacyDocument: (collectionName: string, path: string) => {
355
+ id: number;
356
+ hash: string;
357
+ title: string;
358
+ } | null;
359
+ updateDocumentTitle: (documentId: number, title: string, modifiedAt: string) => void;
360
+ updateDocument: (documentId: number, title: string, hash: string, modifiedAt: string) => void;
361
+ deactivateDocument: (collectionName: string, path: string) => void;
362
+ getActiveDocumentPaths: (collectionName: string) => string[];
363
+ getHashesForEmbedding: () => {
364
+ hash: string;
365
+ body: string;
366
+ path: string;
367
+ }[];
368
+ };
369
+ export type ReindexProgress = {
370
+ file: string;
371
+ current: number;
372
+ total: number;
373
+ };
374
+ export type ReindexSkippedFile = {
375
+ file: string;
376
+ code: string;
377
+ };
378
+ export type ReindexResult = {
379
+ indexed: number;
380
+ updated: number;
381
+ unchanged: number;
382
+ removed: number;
383
+ orphanedCleaned: number;
384
+ skipped: number;
385
+ skippedFiles: ReindexSkippedFile[];
386
+ };
387
+ /**
388
+ * Re-index a single collection by scanning the filesystem and updating the database.
389
+ * Pure function — no console output, no db lifecycle management.
390
+ */
391
+ export declare function reindexCollection(store: Store, collectionPath: string, globPattern: string, collectionName: string, options?: {
392
+ ignorePatterns?: string[];
393
+ onProgress?: (info: ReindexProgress) => void;
394
+ }): Promise<ReindexResult>;
395
+ export type EmbedFailure = {
396
+ path: string;
397
+ hash: string;
398
+ seq: number;
399
+ attempts: number;
400
+ reason: string;
401
+ };
402
+ export type EmbedProgress = {
403
+ chunksEmbedded: number;
404
+ totalChunks: number;
405
+ bytesProcessed: number;
406
+ totalBytes: number;
407
+ /** Active failed chunks still awaiting a successful retry. */
408
+ errors: number;
409
+ failures?: EmbedFailure[];
410
+ };
411
+ export type EmbedResult = {
412
+ docsProcessed: number;
413
+ chunksEmbedded: number;
414
+ /** Active failed chunks that did not recover after retries. */
415
+ errors: number;
416
+ failures?: EmbedFailure[];
417
+ durationMs: number;
418
+ };
419
+ export type EmbedOptions = {
420
+ force?: boolean;
421
+ /** Explicit authorization for a destructive remote identity/dimension rebuild. */
422
+ allowDestructiveRebuild?: boolean;
423
+ model?: string;
424
+ /**
425
+ * Restrict embedding to documents in a single collection.
426
+ * When omitted, all pending documents across every collection are embedded.
427
+ */
428
+ collection?: string;
429
+ maxDocsPerBatch?: number;
430
+ maxBatchBytes?: number;
431
+ chunkStrategy?: ChunkStrategy;
432
+ /**
433
+ * Max wall-clock duration for the whole embed session, in milliseconds. When the
434
+ * cap is reached, remaining document batches are skipped (re-run `qmd embed` to
435
+ * continue). A value <= 0 disables the cap. Defaults to
436
+ * {@link DEFAULT_EMBED_MAX_DURATION_MS} (30 minutes).
437
+ */
438
+ maxDurationMs?: number;
439
+ onProgress?: (info: EmbedProgress) => void;
440
+ };
441
+ export type PendingEmbeddingDoc = {
442
+ hash: string;
443
+ path: string;
444
+ bytes: number;
445
+ };
446
+ export declare function getPendingEmbeddingDocs(db: Database, collection?: string, model?: string, fingerprint?: string): PendingEmbeddingDoc[];
447
+ /**
448
+ * Conservative read-only variant for status/preflight surfaces.
449
+ * Legacy schemas are reported as fully pending instead of being migrated.
450
+ */
451
+ export declare function getPendingEmbeddingDocsReadOnly(db: Database, collection?: string, model?: string, fingerprint?: string): PendingEmbeddingDoc[];
452
+ export declare function finalizeEmbeddingBuild(db: Database, lease: EmbeddingBuildLease, model: string, fingerprint: string, options?: {
453
+ allowReady?: boolean;
454
+ now?: number;
455
+ afterHealthScan?: () => void;
456
+ }): boolean;
457
+ /**
458
+ * Generate vector embeddings for documents that need them.
459
+ * Pure function — no console output, no db lifecycle management.
460
+ * Uses the store's LlamaCpp instance if set, otherwise the global singleton.
461
+ */
462
+ export declare function generateEmbeddings(store: Store, options?: EmbedOptions): Promise<EmbedResult>;
463
+ /**
464
+ * Create a new store instance with the given database path.
465
+ * If no path is provided, uses the default path (~/.cache/qmd/index.sqlite).
466
+ *
467
+ * @param dbPath - Path to the SQLite database file
468
+ * @returns Store instance with all methods bound to the database
469
+ */
470
+ export type CreateStoreOptions = {
471
+ /** Borrowed embedding provider; its owner is responsible for disposal. */
472
+ embeddingProvider?: EmbeddingProvider;
473
+ /** Open an existing index without schema, journal, or data mutation. */
474
+ readOnly?: boolean;
475
+ };
476
+ export declare function createStore(dbPath?: string, options?: CreateStoreOptions): Store;
477
+ /**
478
+ * Unified document result type with all metadata.
479
+ * Body is optional - use getDocumentBody() to load it separately if needed.
480
+ */
481
+ export type DocumentResult = {
482
+ filepath: string;
483
+ displayPath: string;
484
+ title: string;
485
+ context: string | null;
486
+ hash: string;
487
+ docid: string;
488
+ collectionName: string;
489
+ modifiedAt: string;
490
+ bodyLength: number;
491
+ body?: string;
492
+ };
493
+ /**
494
+ * Extract short docid from a full hash (first 6 characters).
495
+ */
496
+ export declare function getDocid(hash: string): string;
497
+ export declare function handelize(path: string): string;
498
+ /**
499
+ * Search result extends DocumentResult with score and source info
500
+ */
501
+ export type SearchResult = DocumentResult & {
502
+ score: number;
503
+ source: "fts" | "vec";
504
+ chunkPos?: number;
505
+ lexicalTrace?: CjkLexicalTrace;
506
+ };
507
+ export type CjkLexicalChannel = "char" | "word" | "bigram";
508
+ export type CjkLexicalTrace = {
509
+ policyVersion: string;
510
+ channels: Array<{
511
+ channel: CjkLexicalChannel;
512
+ status: "used" | "omitted";
513
+ reason?: string;
514
+ }>;
515
+ contributions: Array<{
516
+ channel: CjkLexicalChannel;
517
+ rank: number;
518
+ backendScore: number;
519
+ weight: number;
520
+ rrfContribution: number;
521
+ }>;
522
+ fusionScore: number;
523
+ };
524
+ export type LexicalStrongSignal = {
525
+ policyVersion: string;
526
+ strong: boolean;
527
+ channel: CjkLexicalChannel | null;
528
+ topScore: number;
529
+ gap: number;
530
+ };
531
+ export declare function getLexicalStrongSignal(results: SearchResult[]): LexicalStrongSignal;
532
+ /**
533
+ * Ranked result for RRF fusion (simplified, used internally)
534
+ */
535
+ export type RankedResult = {
536
+ file: string;
537
+ displayPath: string;
538
+ title: string;
539
+ body: string;
540
+ score: number;
541
+ };
542
+ export type RRFContributionTrace = {
543
+ listIndex: number;
544
+ source: "fts" | "vec";
545
+ queryType: "original" | "lex" | "vec" | "hyde";
546
+ query: string;
547
+ rank: number;
548
+ weight: number;
549
+ backendScore: number;
550
+ rrfContribution: number;
551
+ };
552
+ export type RRFScoreTrace = {
553
+ contributions: RRFContributionTrace[];
554
+ baseScore: number;
555
+ topRank: number;
556
+ topRankBonus: number;
557
+ totalScore: number;
558
+ };
559
+ export type HybridQueryExplain = {
560
+ expansion?: ExpansionDecision;
561
+ ftsScores: number[];
562
+ vectorScores: number[];
563
+ rrf: {
564
+ rank: number;
565
+ positionScore: number;
566
+ weight: number;
567
+ baseScore: number;
568
+ topRankBonus: number;
569
+ totalScore: number;
570
+ contributions: RRFContributionTrace[];
571
+ };
572
+ rerankScore: number;
573
+ blendedScore: number;
574
+ };
575
+ /**
576
+ * Error result when document is not found
577
+ */
578
+ export type DocumentNotFound = {
579
+ error: "not_found";
580
+ query: string;
581
+ similarFiles: string[];
582
+ };
583
+ export type DocumentExcludedByIgnore = {
584
+ error: "excluded_by_ignore";
585
+ query: string;
586
+ collection: string;
587
+ path: string;
588
+ rule: string;
589
+ };
590
+ export type DocumentLookupError = DocumentNotFound | DocumentExcludedByIgnore;
591
+ /**
592
+ * Result from multi-get operations
593
+ */
594
+ export type MultiGetResult = {
595
+ doc: DocumentResult;
596
+ skipped: false;
597
+ } | {
598
+ doc: Pick<DocumentResult, "filepath" | "displayPath">;
599
+ skipped: true;
600
+ skipReason: string;
601
+ };
602
+ export type CollectionInfo = {
603
+ name: string;
604
+ path: string | null;
605
+ pattern: string | null;
606
+ documents: number;
607
+ lastUpdated: string;
608
+ };
609
+ export type IndexStatus = {
610
+ totalDocuments: number;
611
+ needsEmbedding: number;
612
+ hasVectorIndex: boolean;
613
+ collections: CollectionInfo[];
614
+ /** Additive diagnostics populated by high-level composition roots. */
615
+ diagnostics?: IndexDiagnostics;
616
+ };
617
+ export declare function getHashesNeedingEmbedding(db: Database, collection?: string, model?: string): number;
618
+ export type IndexHealthInfo = {
619
+ needsEmbedding: number;
620
+ totalDocs: number;
621
+ daysStale: number | null;
622
+ };
623
+ export type LegacyFingerprintAdoptionResult = {
624
+ checked: boolean;
625
+ adopted: number;
626
+ reason: string;
627
+ };
628
+ export declare function maybeAdoptLegacyEmbeddingFingerprint(store: Store, model?: string): Promise<LegacyFingerprintAdoptionResult>;
629
+ export declare function getIndexHealth(db: Database, model?: string): IndexHealthInfo;
630
+ export declare function getIndexHealthReadOnly(db: Database, needsEmbedding: number): IndexHealthInfo;
631
+ export type CacheKeyBody = {
632
+ query?: string;
633
+ model?: string;
634
+ chunk?: string;
635
+ file?: string;
636
+ expansionContext?: string;
637
+ };
638
+ export declare function getCacheKey(url: string, body: CacheKeyBody): string;
639
+ export declare function getCachedResult(db: Database, cacheKey: string): string | null;
640
+ export declare function setCachedResult(db: Database, cacheKey: string, result: string): void;
641
+ export declare function clearCache(db: Database): void;
642
+ /**
643
+ * Delete cached LLM API responses.
644
+ * Returns the number of cached responses deleted.
645
+ */
646
+ export declare function deleteLLMCache(db: Database): number;
647
+ /**
648
+ * Remove inactive document records (active = 0).
649
+ * Returns the number of inactive documents deleted.
650
+ */
651
+ export declare function deleteInactiveDocuments(db: Database): number;
652
+ /**
653
+ * Remove orphaned content hashes that are not referenced by any document.
654
+ * Inactive documents are soft-deleted tombstones, so their content rows must
655
+ * remain referenced until deleteInactiveDocuments() hard-deletes them.
656
+ * Returns the number of orphaned content hashes deleted.
657
+ */
658
+ export declare function cleanupOrphanedContent(db: Database): number;
659
+ /**
660
+ * Count content hashes that would be unreferenced after inactive documents
661
+ * are hard-deleted. Shared hashes still used by an active document are kept.
662
+ */
663
+ export declare function countOrphanedContent(db: Database): number;
664
+ /**
665
+ * Count embedding chunks whose hash is not referenced by any active document.
666
+ * Reads `content_vectors` only, so this works even when sqlite-vec is unavailable (#768).
667
+ */
668
+ export declare function countOrphanedVectors(db: Database): number;
669
+ /**
670
+ * Remove orphaned vector embeddings that are not referenced by any active document.
671
+ * Returns the number of orphaned embedding chunks deleted.
672
+ */
673
+ export declare function cleanupOrphanedVectors(db: Database): number;
674
+ /**
675
+ * Run VACUUM to reclaim unused space in the database.
676
+ * This operation rebuilds the database file to eliminate fragmentation.
677
+ */
678
+ export declare function vacuumDatabase(db: Database): void;
679
+ /**
680
+ * Merge FTS5 b-trees so deleted rows (deactivated / hard-deleted documents)
681
+ * leave `documents_fts_data`. VACUUM alone does not compact FTS5 (#550).
682
+ */
683
+ export declare function optimizeDocumentsFts(db: Database): void;
684
+ export type CleanupStats = {
685
+ cacheCount: number;
686
+ orphanedVectors: number;
687
+ inactiveDocs: number;
688
+ orphanedContent: number;
689
+ };
690
+ /** Counts what `runCleanup` would remove, including content only held by inactive docs. */
691
+ export declare function previewCleanup(db: Database): CleanupStats;
692
+ /**
693
+ * Full `qmd cleanup` sequence: drop cache, orphaned vectors, inactive document
694
+ * rows, then the content those rows were pinning, compact FTS5, vacuum.
695
+ */
696
+ export declare function runCleanup(db: Database): CleanupStats;
697
+ export declare function hashContent(content: string): Promise<string>;
698
+ export declare function extractTitle(content: string, filename: string): string;
699
+ /**
700
+ * Insert content into the content table (content-addressable storage).
701
+ * Uses INSERT OR IGNORE so duplicate hashes are skipped.
702
+ */
703
+ export declare function insertContent(db: Database, hash: string, content: string, createdAt: string): void;
704
+ /**
705
+ * Insert a new document into the documents table.
706
+ */
707
+ export declare function insertDocument(db: Database, collectionName: string, path: string, title: string, hash: string, createdAt: string, modifiedAt: string): void;
708
+ /** Insert immutable content and its document row in the same lexical transaction. */
709
+ export declare function insertDocumentWithContent(db: Database, hash: string, content: string, contentCreatedAt: string, collectionName: string, path: string, title: string, documentCreatedAt: string, modifiedAt: string): void;
710
+ /**
711
+ * Find an active document by collection name and path.
712
+ */
713
+ export declare function findActiveDocument(db: Database, collectionName: string, path: string): {
714
+ id: number;
715
+ hash: string;
716
+ title: string;
717
+ } | null;
718
+ /**
719
+ * Find an active document, falling back to a legacy handalized-path match.
720
+ * If found under the pre-2.6 slug, renames it in-place and rebuilds the
721
+ * FTS entry. Embeddings are keyed by content hash, so the rename is
722
+ * safe — no re-embedding required.
723
+ *
724
+ * `livePaths`, when given, is the set of literal paths of every file in the
725
+ * current scan of this collection. A legacy row whose path is in that set
726
+ * belongs to a *different* file that still exists on disk, so it must never be
727
+ * adopted — renaming it would evict that file from the index (#717).
728
+ *
729
+ * @internal Used by reindexCollection and indexFiles during qmd update.
730
+ * Returns null if the document does not exist under either path.
731
+ */
732
+ export declare function findOrMigrateLegacyDocument(db: Database, collectionName: string, path: string, livePaths?: ReadonlySet<string>): {
733
+ id: number;
734
+ hash: string;
735
+ title: string;
736
+ } | null;
737
+ /**
738
+ * Update the title and modified_at timestamp for a document.
739
+ */
740
+ export declare function updateDocumentTitle(db: Database, documentId: number, title: string, modifiedAt: string): void;
741
+ /**
742
+ * Update an existing document's hash, title, and modified_at timestamp.
743
+ * Used when content changes but the file path stays the same.
744
+ */
745
+ export declare function updateDocument(db: Database, documentId: number, title: string, hash: string, modifiedAt: string): void;
746
+ /** Insert immutable content and repoint its document row atomically. */
747
+ export declare function updateDocumentWithContent(db: Database, hash: string, content: string, contentCreatedAt: string, documentId: number, title: string, modifiedAt: string): void;
748
+ /**
749
+ * Deactivate a document (mark as inactive but don't delete).
750
+ */
751
+ export declare function deactivateDocument(db: Database, collectionName: string, path: string): void;
752
+ /**
753
+ * Get all active document paths for a collection.
754
+ */
755
+ export declare function getActiveDocumentPaths(db: Database, collectionName: string): string[];
756
+ export { formatQueryForEmbedding, formatDocForEmbedding };
757
+ /**
758
+ * Chunk a document using regex-only break point detection.
759
+ * This is the sync, backward-compatible API used by tests and legacy callers.
760
+ */
761
+ export declare function chunkDocument(content: string, maxChars?: number, overlapChars?: number, windowChars?: number): {
762
+ text: string;
763
+ pos: number;
764
+ }[];
765
+ /**
766
+ * Async AST-aware chunking. Detects language from filepath, computes AST
767
+ * break points for supported code files, merges with regex break points,
768
+ * and delegates to the shared chunk algorithm.
769
+ *
770
+ * Falls back to regex-only when strategy is "regex", filepath is absent,
771
+ * or language is unsupported.
772
+ */
773
+ export declare function chunkDocumentAsync(content: string, maxChars?: number, overlapChars?: number, windowChars?: number, filepath?: string, chunkStrategy?: ChunkStrategy): Promise<{
774
+ text: string;
775
+ pos: number;
776
+ }[]>;
777
+ /**
778
+ * Chunk a document by actual token count using the LLM tokenizer.
779
+ * More accurate than character-based chunking but requires async.
780
+ *
781
+ * When filepath and chunkStrategy are provided, uses AST-aware break points
782
+ * for supported code files.
783
+ */
784
+ export declare function chunkDocumentByTokens(content: string, maxTokens?: number, overlapTokens?: number, windowTokens?: number, filepath?: string, chunkStrategy?: ChunkStrategy, signal?: AbortSignal): Promise<{
785
+ text: string;
786
+ pos: number;
787
+ tokens: number;
788
+ }[]>;
789
+ /**
790
+ * Normalize a docid input by stripping surrounding quotes and leading #.
791
+ * Handles: "#abc123", 'abc123', "abc123", #abc123, abc123
792
+ * Returns the bare hex string.
793
+ */
794
+ export declare function normalizeDocid(docid: string): string;
795
+ /**
796
+ * Check if a string looks like a docid reference.
797
+ * Accepts: #abc123, abc123, "#abc123", "abc123", '#abc123', 'abc123'
798
+ * Returns true if the normalized form is a valid hex string of 6+ chars.
799
+ */
800
+ export declare function isDocid(input: string): boolean;
801
+ /**
802
+ * Find a document by its short docid (first 6 characters of hash).
803
+ * Returns the document's virtual path if found, null otherwise.
804
+ * If multiple documents match the same short hash (collision), returns the first one.
805
+ *
806
+ * Accepts lenient input: #abc123, abc123, "#abc123", "abc123"
807
+ */
808
+ export declare function findDocumentByDocid(db: Database, docid: string): {
809
+ filepath: string;
810
+ hash: string;
811
+ } | null;
812
+ export declare function findSimilarFiles(db: Database, query: string, maxDistance?: number, limit?: number): string[];
813
+ export declare function matchFilesByGlob(db: Database, pattern: string): {
814
+ filepath: string;
815
+ displayPath: string;
816
+ bodyLength: number;
817
+ }[];
818
+ /**
819
+ * Get context for a file path using hierarchical inheritance.
820
+ * Contexts are collection-scoped and inherit from parent directories.
821
+ * For example, context at "/talks" applies to "/talks/2024/keynote.md".
822
+ *
823
+ * @param db Database instance (unused - kept for compatibility)
824
+ * @param collectionName Collection name
825
+ * @param path Relative path within the collection
826
+ * @returns Context string or null if no context is defined
827
+ */
828
+ export declare function getContextForPath(db: Database, collectionName: string, path: string): string | null;
829
+ /**
830
+ * Get context for a file path (virtual or filesystem).
831
+ * Resolves the collection and relative path from the DB store_collections table.
832
+ */
833
+ export declare function getContextForFile(db: Database, filepath: string): string | null;
834
+ /**
835
+ * Get collection by name from DB store_collections table.
836
+ */
837
+ export declare function getCollectionByName(db: Database, name: string): {
838
+ name: string;
839
+ pwd: string;
840
+ glob_pattern: string;
841
+ } | null;
842
+ /**
843
+ * List all collections with document counts from database.
844
+ * Merges store_collections config with database statistics.
845
+ */
846
+ export declare function listCollections(db: Database): {
847
+ name: string;
848
+ pwd: string;
849
+ glob_pattern: string;
850
+ doc_count: number;
851
+ active_count: number;
852
+ last_modified: string | null;
853
+ includeByDefault: boolean;
854
+ }[];
855
+ /**
856
+ * Remove a collection and clean up its documents.
857
+ * Uses collections.ts to remove from YAML config and cleans up database.
858
+ */
859
+ export declare function removeCollection(db: Database, collectionName: string): {
860
+ deletedDocs: number;
861
+ cleanedHashes: number;
862
+ };
863
+ /**
864
+ * Rename a collection.
865
+ * Updates both YAML config and database documents table.
866
+ */
867
+ export declare function renameCollection(db: Database, oldName: string, newName: string): void;
868
+ /**
869
+ * Insert or update a context for a specific collection and path prefix.
870
+ *
871
+ * `store_collections` is keyed by name (`TEXT PRIMARY KEY`), not an integer id.
872
+ * #754 retargeted this query from the dropped `collections` table but left
873
+ * `WHERE id = ?`, which throws `no such column: id`.
874
+ */
875
+ export declare function insertContext(db: Database, collectionName: string, pathPrefix: string, context: string): void;
876
+ /**
877
+ * Delete a context for a specific collection and path prefix.
878
+ * Returns the number of contexts deleted.
879
+ */
880
+ export declare function deleteContext(db: Database, collectionName: string, pathPrefix: string): number;
881
+ /**
882
+ * Delete all global contexts (contexts with empty path_prefix).
883
+ * Returns the number of contexts deleted.
884
+ */
885
+ export declare function deleteGlobalContexts(db: Database): number;
886
+ /**
887
+ * List all contexts, grouped by collection.
888
+ * Returns contexts ordered by collection name, then by path prefix length (longest first).
889
+ */
890
+ export declare function listPathContexts(db: Database): {
891
+ collection_name: string;
892
+ path_prefix: string;
893
+ context: string;
894
+ }[];
895
+ /**
896
+ * Get all collections (name only - from YAML config).
897
+ */
898
+ export declare function getAllCollections(db: Database): {
899
+ name: string;
900
+ }[];
901
+ /**
902
+ * Check which collections don't have any context defined.
903
+ * Returns collections that have no context entries at all (not even root context).
904
+ */
905
+ export declare function getCollectionsWithoutContext(db: Database): {
906
+ name: string;
907
+ pwd: string;
908
+ doc_count: number;
909
+ }[];
910
+ /**
911
+ * Get top-level directories in a collection that don't have context.
912
+ * Useful for suggesting where context might be needed.
913
+ */
914
+ export declare function getTopLevelPathsWithoutContext(db: Database, collectionName: string): string[];
915
+ export declare function sanitizeFTS5Term(term: string): string;
916
+ /**
917
+ * Validate that a vec/hyde query doesn't use lex-only syntax.
918
+ * Returns error message if invalid, null if valid.
919
+ */
920
+ export declare function validateSemanticQuery(query: string): string | null;
921
+ export declare function validateLexQuery(query: string): string | null;
922
+ export declare const CJK_LEXICAL_RRF_POLICY_VERSION = "cjk-lexical-rrf-v1";
923
+ export declare const CJK_LEXICAL_RRF_K = 60;
924
+ export declare const CJK_LEXICAL_CANDIDATE_DEPTH = 60;
925
+ export declare const CJK_LEXICAL_RRF_WEIGHTS: Readonly<Record<CjkLexicalChannel, number>>;
926
+ export type CollectionFilter = string | readonly string[];
927
+ export type CollectionScope = string | readonly string[] | undefined;
928
+ export declare function searchCharFTS(db: Database, query: string, limit?: number, collectionFilter?: CollectionFilter): SearchResult[];
929
+ export declare function searchFTS(db: Database, query: string, limit?: number, collectionFilter?: CollectionFilter): SearchResult[];
930
+ export declare function searchVec(db: Database, query: string, model: string, limit?: number, collectionFilter?: CollectionFilter, session?: ILLMSession, precomputedEmbedding?: number[], provider?: EmbeddingProvider, authorizeRemoteRequest?: Store["authorizeRemoteRequest"], llmOverride?: LLM): Promise<SearchResult[]>;
931
+ /**
932
+ * Get all unique content hashes that need embeddings (from active documents).
933
+ * Returns hash, document body, and a sample path for display purposes.
934
+ */
935
+ export declare function getHashesForEmbedding(db: Database, model?: string): {
936
+ hash: string;
937
+ body: string;
938
+ path: string;
939
+ }[];
940
+ /**
941
+ * Clear embeddings for the whole index, or just for one collection.
942
+ *
943
+ * When `collection` is omitted the entire content_vectors table is emptied and
944
+ * the vectors_vec virtual table is dropped (it is recreated with the right
945
+ * dimensions on the next embed run).
946
+ *
947
+ * When `collection` is provided, only vectors whose hash is referenced
948
+ * exclusively by active documents in that collection are removed. Hashes
949
+ * shared with active documents in other collections are left in place so
950
+ * vector search keeps working there (content_vectors is keyed globally by
951
+ * content hash; identical document bodies across collections share a row).
952
+ * vectors_vec is preserved so other collections keep working unless the scoped
953
+ * clear empties content_vectors entirely, in which case it is dropped so the
954
+ * next embed can recreate the table with the current dimensions.
955
+ */
956
+ export declare function clearAllEmbeddings(db: Database, collection?: string, lease?: EmbeddingBuildLease): void;
957
+ /**
958
+ * Insert a single embedding into both content_vectors and vectors_vec tables.
959
+ * The hash_seq key is formatted as "hash_seq" for the vectors_vec table.
960
+ *
961
+ * vectors_vec uses DELETE + INSERT instead of INSERT OR REPLACE because sqlite-vec's
962
+ * vec0 virtual tables silently ignore the OR REPLACE conflict clause.
963
+ */
964
+ export declare function insertEmbedding(db: Database, hash: string, seq: number, pos: number, embedding: Float32Array, model: string, embeddedAt: string, totalChunks?: number, fingerprint?: string, lease?: EmbeddingBuildLease): void;
965
+ export declare function expandQuery(query: string, model: string | undefined, db: Database, expansionContext?: string, llmOverride?: LLM, options?: QueryExpansionOptions): Promise<ExpandedQuery[]>;
966
+ /**
967
+ * Delete the cached expansion for a query. hybridQuery() calls this when an
968
+ * expansion's sub-queries all came back empty — left in place, the dud entry
969
+ * would replay the same misses on every warm repeat of the query.
970
+ */
971
+ export declare function deleteExpansionCacheEntry(db: Database, query: string, model?: string, expansionContext?: string): void;
972
+ export declare function rerank(query: string, documents: {
973
+ file: string;
974
+ text: string;
975
+ }[], model: string | undefined, db: Database, rerankContext?: string, llmOverride?: LLM): Promise<{
976
+ file: string;
977
+ score: number;
978
+ }[]>;
979
+ export declare function reciprocalRankFusion(resultLists: RankedResult[][], weights?: number[], k?: number): RankedResult[];
980
+ /**
981
+ * Build per-document RRF contribution traces for explain/debug output.
982
+ */
983
+ export declare function buildRrfTrace(resultLists: RankedResult[][], weights?: number[], listMeta?: RankedListMeta[], k?: number): Map<string, RRFScoreTrace>;
984
+ /**
985
+ * Find a document by filename/path, docid (#hash), or with fuzzy matching.
986
+ * Returns document metadata without body by default.
987
+ *
988
+ * Supports:
989
+ * - Virtual paths: qmd://collection/path/to/file.md
990
+ * - Absolute paths: /path/to/file.md
991
+ * - Relative paths: path/to/file.md
992
+ * - Short docid: #abc123 (first 6 chars of hash)
993
+ */
994
+ export declare function findDocument(db: Database, filename: string, options?: {
995
+ includeBody?: boolean;
996
+ }): DocumentResult | DocumentLookupError;
997
+ /**
998
+ * Get the body content for a document
999
+ * Optionally slice by line range
1000
+ */
1001
+ export declare function getDocumentBody(db: Database, doc: DocumentResult | {
1002
+ filepath: string;
1003
+ }, fromLine?: number, maxLines?: number): string | null;
1004
+ /**
1005
+ * Escape a user-supplied string so it is matched literally by SQLite LIKE.
1006
+ * Uses '#' as the ESCAPE character (avoids quote-escaping pitfalls with '\\').
1007
+ */
1008
+ export declare function escapeLikePattern(value: string): string;
1009
+ export type CommaListMatch = {
1010
+ collection: string;
1011
+ path: string;
1012
+ virtualPath: string;
1013
+ bodyLength: number;
1014
+ };
1015
+ export type CommaListResolve = {
1016
+ ok: true;
1017
+ match: CommaListMatch;
1018
+ } | {
1019
+ ok: false;
1020
+ error: string;
1021
+ };
1022
+ /**
1023
+ * Resolve one comma-list name for multi-get (shared by CLI and SDK/MCP).
1024
+ *
1025
+ * Match order: docid / qmd:// URI (exact only), then exact collection-prefixed
1026
+ * path, then exact document path, then a path-boundary suffix (`.../name`).
1027
+ * Unanchored LIKE is never used, so a fragment like `NTAX.md` cannot silently
1028
+ * fetch `SYNTAX.md`. Multiple hits at the same tier error with the candidate
1029
+ * list instead of `LIMIT 1` (#759).
1030
+ */
1031
+ export declare function resolveCommaListName(db: Database, name: string): CommaListResolve;
1032
+ /**
1033
+ * Find multiple documents by glob pattern or comma-separated list
1034
+ * Returns documents without body by default (use getDocumentBody to load)
1035
+ */
1036
+ export declare function findDocuments(db: Database, pattern: string, options?: {
1037
+ includeBody?: boolean;
1038
+ maxBytes?: number;
1039
+ }): {
1040
+ docs: MultiGetResult[];
1041
+ errors: string[];
1042
+ };
1043
+ export declare function getStatus(db: Database, model?: string): IndexStatus;
1044
+ export declare function getStatusReadOnly(db: Database, needsEmbedding: number): IndexStatus;
1045
+ export type SnippetResult = {
1046
+ line: number;
1047
+ snippet: string;
1048
+ linesBefore: number;
1049
+ linesAfter: number;
1050
+ snippetLines: number;
1051
+ };
1052
+ /** Weight for intent terms relative to query terms (1.0) in snippet scoring */
1053
+ export declare const INTENT_WEIGHT_SNIPPET = 0.3;
1054
+ /** Weight for intent terms relative to query terms (1.0) in chunk selection */
1055
+ export declare const INTENT_WEIGHT_CHUNK = 0.5;
1056
+ /**
1057
+ * Extract meaningful terms from an intent string, filtering stop words and punctuation.
1058
+ * Uses Unicode-aware punctuation stripping so domain terms like "API" survive.
1059
+ * Returns lowercase terms suitable for text matching.
1060
+ */
1061
+ export declare function extractIntentTerms(intent: string): string[];
1062
+ export declare function extractSnippet(body: string, query: string, maxLen?: number, chunkPos?: number, chunkLen?: number, intent?: string): SnippetResult;
1063
+ /**
1064
+ * Add line numbers to text content.
1065
+ * Each line becomes: "{lineNum}: {content}"
1066
+ */
1067
+ export declare function addLineNumbers(text: string, startLine?: number): string;
1068
+ /**
1069
+ * Optional progress hooks for search orchestration.
1070
+ * CLI wires these to stderr for user feedback; MCP leaves them unset.
1071
+ */
1072
+ export interface SearchHooks {
1073
+ /** BM25 probe found strong signal — expansion will be skipped */
1074
+ onStrongSignal?: (topScore: number) => void;
1075
+ /** Shared expansion policy decision after prefix stripping and BM25 probe */
1076
+ onExpansionDecision?: (decision: ExpansionDecision) => void;
1077
+ /** Expansion policy or execution failed; event intentionally excludes native errors. */
1078
+ onExpansionError?: (event: ExpansionErrorEvent) => void;
1079
+ /** Query expansion starting */
1080
+ onExpandStart?: () => void;
1081
+ /** Query expansion complete. Empty array = strong signal skip. elapsedMs = time taken. */
1082
+ onExpand?: (original: string, expanded: ExpandedQuery[], elapsedMs: number) => void;
1083
+ /** Embedding starting (vec/hyde queries) */
1084
+ onEmbedStart?: (count: number) => void;
1085
+ /** Embedding complete */
1086
+ onEmbedDone?: (elapsedMs: number) => void;
1087
+ /** Reranking is about to start */
1088
+ onRerankStart?: (chunkCount: number) => void;
1089
+ /** Reranking finished */
1090
+ onRerankDone?: (elapsedMs: number) => void;
1091
+ }
1092
+ export type ExpansionErrorEvent = {
1093
+ reason: "conflicting-directives" | "no-result" | "provider-error";
1094
+ query: string;
1095
+ mode: ExpansionMode;
1096
+ };
1097
+ export interface HybridQueryOptions {
1098
+ collection?: string | readonly string[];
1099
+ collections?: readonly string[];
1100
+ limit?: number;
1101
+ minScore?: number;
1102
+ candidateLimit?: number;
1103
+ explain?: boolean;
1104
+ /** Additional context used only while generating query expansions. */
1105
+ expansionContext?: string;
1106
+ /** Additional context used for reranking and snippet/chunk selection. */
1107
+ rerankContext?: string;
1108
+ expansion?: ExpansionMode;
1109
+ skipRerank?: boolean;
1110
+ chunkStrategy?: ChunkStrategy;
1111
+ hooks?: SearchHooks;
1112
+ }
1113
+ export interface HybridQueryResult {
1114
+ file: string;
1115
+ displayPath: string;
1116
+ title: string;
1117
+ body: string;
1118
+ bestChunk: string;
1119
+ bestChunkPos: number;
1120
+ score: number;
1121
+ context: string | null;
1122
+ docid: string;
1123
+ explain?: HybridQueryExplain;
1124
+ }
1125
+ export type RankedListMeta = {
1126
+ source: "fts" | "vec";
1127
+ queryType: "original" | "lex" | "vec" | "hyde";
1128
+ query: string;
1129
+ };
1130
+ /**
1131
+ * RRF list weights for hybridQuery.
1132
+ *
1133
+ * Original-query retrieval paths are the primary evidence and get 2x weight:
1134
+ * - original FTS
1135
+ * - original vector search
1136
+ *
1137
+ * Expansion-derived lists (lex/vec/hyde) stay at 1x regardless of list order,
1138
+ * so a lex expansion inserted before original vector search cannot steal the
1139
+ * original vector boost.
1140
+ */
1141
+ export declare function getHybridRrfWeights(rankedListMeta: RankedListMeta[]): number[];
1142
+ /**
1143
+ * Hybrid search: BM25 + vector + query expansion + RRF + chunked reranking.
1144
+ *
1145
+ * Pipeline:
1146
+ * 1. BM25 probe → skip expansion if strong signal
1147
+ * 2. expandQuery() → typed query variants (lex/vec/hyde)
1148
+ * 3. Type-routed search: original→vector, lex→FTS, vec/hyde→vector
1149
+ * 4. RRF fusion → slice to candidateLimit
1150
+ * 5. chunkDocument() + keyword-best-chunk selection
1151
+ * 6. rerank on chunks (NOT full bodies — O(tokens) trap)
1152
+ * 7. Position-aware score blending (RRF rank × reranker score)
1153
+ * 8. Dedup by file, filter by minScore, slice to limit
1154
+ */
1155
+ export declare function hybridQuery(store: Store, query: string, options?: HybridQueryOptions): Promise<HybridQueryResult[]>;
1156
+ export interface VectorSearchOptions {
1157
+ collection?: CollectionFilter;
1158
+ limit?: number;
1159
+ minScore?: number;
1160
+ /** Additional context used only while generating query expansions. */
1161
+ expansionContext?: string;
1162
+ hooks?: Pick<SearchHooks, 'onExpand'>;
1163
+ }
1164
+ export interface VectorSearchResult {
1165
+ file: string;
1166
+ displayPath: string;
1167
+ title: string;
1168
+ body: string;
1169
+ score: number;
1170
+ context: string | null;
1171
+ docid: string;
1172
+ }
1173
+ /**
1174
+ * Vector-only semantic search with query expansion.
1175
+ *
1176
+ * Pipeline:
1177
+ * 1. expandQuery() → typed variants, filter to vec/hyde only (lex irrelevant here)
1178
+ * 2. searchVec() for original + vec/hyde variants (sequential — node-llama-cpp embed limitation)
1179
+ * 3. Dedup by filepath (keep max score)
1180
+ * 4. Sort by score descending, filter by minScore, slice to limit
1181
+ */
1182
+ export declare function vectorSearchQuery(store: Store, query: string, options?: VectorSearchOptions): Promise<VectorSearchResult[]>;
1183
+ /**
1184
+ * A single sub-search in a structured search request.
1185
+ * Matches the format used in QMD training data.
1186
+ */
1187
+ export interface StructuredSearchOptions {
1188
+ collections?: string[];
1189
+ limit?: number;
1190
+ minScore?: number;
1191
+ candidateLimit?: number;
1192
+ explain?: boolean;
1193
+ /** Additional context used for reranking and snippet/chunk selection. */
1194
+ rerankContext?: string;
1195
+ /** Skip LLM reranking, use only RRF scores */
1196
+ skipRerank?: boolean;
1197
+ chunkStrategy?: ChunkStrategy;
1198
+ hooks?: SearchHooks;
1199
+ }
1200
+ /**
1201
+ * Structured search: execute pre-expanded queries without LLM query expansion.
1202
+ *
1203
+ * Designed for LLM callers (MCP/HTTP) that generate their own query expansions.
1204
+ * Skips the internal expandQuery() step — goes directly to:
1205
+ *
1206
+ * Pipeline:
1207
+ * 1. Route searches: lex→FTS, vec/hyde→vector (batch embed)
1208
+ * 2. RRF fusion across all result lists
1209
+ * 3. Chunk documents + keyword-best-chunk selection
1210
+ * 4. Rerank on chunks
1211
+ * 5. Position-aware score blending
1212
+ * 6. Dedup, filter, slice
1213
+ *
1214
+ * This is the recommended endpoint for capable LLMs — they can generate
1215
+ * better query variations than our small local model, especially for
1216
+ * domain-specific or nuanced queries.
1217
+ */
1218
+ export declare function structuredSearch(store: Store, searches: ExpandedQuery[], options?: StructuredSearchOptions): Promise<HybridQueryResult[]>;