@wei840222/qmd 2026.8.28 → 2026.9.6

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ### Changed
6
+
7
+ - Replace `better-sqlite3` with Node.js built-in `node:sqlite`, preventing SQLite runtime symbol collisions when QMD is embedded alongside other `node:sqlite` consumers. The minimum supported Node.js version is now 22.16.0.
8
+
5
9
  ### Added
6
10
 
7
11
  - **Disable HyDE Expansion Control**: Added `--no-hyde` CLI option for `qmd query` and `qmd vsearch`, `includeHyde` parameter to SDK (`store.search`, `store.expandQuery`) and MCP `query` tool, allowing users to disable generating hypothetical document embeddings during query expansion.
package/README.md CHANGED
@@ -982,7 +982,6 @@ and `deep-search` (→ `query`).
982
982
  --index <name> # Use named index
983
983
  --intent "<text>" # Legacy CLI alias for rerank context (e.g. "web page load times")
984
984
  --no-rerank # Skip LLM reranking (RRF scores only; faster on CPU)
985
- --no-hyde # Disable HyDE in query expansion (only lex and vec expansions)
986
985
  -C, --candidate-limit <n> # Max candidates to rerank (default: 40)
987
986
  --full-path # Emit on-disk filesystem paths instead of qmd:// URIs
988
987
  # (a result whose file has moved or been deleted since
@@ -1,4 +1,4 @@
1
1
  {
2
- "commit": "7cbc570",
3
- "builtAt": "2026-08-28T11:16:06.495Z"
2
+ "commit": "672da2e",
3
+ "builtAt": "2026-09-06T06:45:32.394Z"
4
4
  }
package/dist/cli/qmd.js CHANGED
@@ -137,12 +137,16 @@ function getStore() {
137
137
  }
138
138
  return store;
139
139
  }
140
- function getDoctorStore() {
140
+ function getDoctorStore(options = {}) {
141
141
  if (!store) {
142
142
  const dbPath = getDbPath();
143
+ const shouldReconcile = existsSync(dbPath) && options.reconcileConfig !== undefined;
143
144
  store = existsSync(dbPath)
144
- ? createStore(dbPath, { readOnly: true })
145
+ ? createStore(dbPath, { readOnly: !shouldReconcile })
145
146
  : createStore(":memory:");
147
+ if (shouldReconcile) {
148
+ syncConfigToDb(store.db, options.reconcileConfig);
149
+ }
146
150
  }
147
151
  return store;
148
152
  }
@@ -3916,10 +3920,6 @@ async function runDoctorDeviceChecks(nextSteps) {
3916
3920
  }
3917
3921
  }
3918
3922
  async function showDoctor() {
3919
- const storeInstance = getDoctorStore();
3920
- const db = storeInstance.db;
3921
- const pkg = readPackageJson();
3922
- const activeModels = resolveModelsForCli();
3923
3923
  let doctorConfig;
3924
3924
  try {
3925
3925
  doctorConfig = loadConfig();
@@ -3928,6 +3928,9 @@ async function showDoctor() {
3928
3928
  // The dedicated index-config check below reports parse errors. Keep the
3929
3929
  // remaining diagnostics available by falling back to DB/default config.
3930
3930
  }
3931
+ const storeInstance = getDoctorStore({ reconcileConfig: doctorConfig });
3932
+ const db = storeInstance.db;
3933
+ const activeModels = resolveModelsForCli();
3931
3934
  const doctorEmbedding = resolveEmbeddingConfig({
3932
3935
  config: doctorConfig,
3933
3936
  dbConfig: readCanonicalEmbeddingConfig(db),
@@ -3938,7 +3941,7 @@ async function showDoctor() {
3938
3941
  const nextSteps = [];
3939
3942
  console.log(`${c.bold}QMD Doctor${c.reset}\n`);
3940
3943
  console.log(`Index: ${getDbPath()}`);
3941
- console.log(`Runtime: better-sqlite3`);
3944
+ console.log(`Runtime: node:sqlite`);
3942
3945
  try {
3943
3946
  const row = db.prepare(`SELECT sqlite_version() AS version`).get();
3944
3947
  doctorCheck("SQLite runtime", true, row.version);
@@ -3946,8 +3949,7 @@ async function showDoctor() {
3946
3949
  catch (error) {
3947
3950
  doctorCheck("SQLite runtime", false, error instanceof Error ? error.message : String(error));
3948
3951
  }
3949
- const betterSqliteVersion = pkg.dependencies?.["better-sqlite3"] ?? pkg.devDependencies?.["better-sqlite3"] ?? "not declared";
3950
- doctorCheck("better-sqlite3 package", true, String(betterSqliteVersion));
3952
+ doctorCheck("node:sqlite", true, process.versions.node);
3951
3953
  try {
3952
3954
  loadSqliteVec(db);
3953
3955
  const row = db.prepare(`SELECT vec_version() AS version`).get();
package/dist/db.d.ts CHANGED
@@ -1,39 +1,26 @@
1
1
  /**
2
2
  * db.ts - SQLite database connection and extension management
3
3
  *
4
- * Provides Database export and connection management using better-sqlite3
5
- * and sqlite-vec.
4
+ * Provides a synchronous node:sqlite connection with QMD's transaction helper
5
+ * and sqlite-vec extension loading.
6
6
  */
7
- import BetterSqlite3 from "better-sqlite3";
7
+ import { DatabaseSync, type StatementSync } from "node:sqlite";
8
8
  export type SQLiteValue = string | number | bigint | Buffer | Uint8Array | Float32Array | null;
9
9
  export type SQLiteParams = readonly SQLiteValue[];
10
- /**
11
- * Open a SQLite database using better-sqlite3.
12
- *
13
- * `better-sqlite3` defaults `busy_timeout` to 0, so concurrent writers throw
14
- * `SQLITE_BUSY` instead of waiting. WAL improves read-while-write concurrency
15
- * but does not serialise writers. Setting the timeout at connection open makes
16
- * parallel processes queue at batch boundaries instead of failing on contact.
17
- *
18
- * WAL is enabled here too (with a bounded retry) so connection-level pragmas
19
- * live in one place and the cold-database journal migration survives concurrent
20
- * opens.
21
- *
22
- * Default 120_000 ms outlasts the worst-case batch commit on a multi-GB
23
- * index. Override with `QMD_SQLITE_BUSY_TIMEOUT` (value in milliseconds; `0`
24
- * restores the upstream fail-fast behaviour).
25
- */
10
+ export type Transaction<TArgs extends unknown[], TResult> = ((...args: TArgs) => TResult) & {
11
+ deferred: (...args: TArgs) => TResult;
12
+ immediate: (...args: TArgs) => TResult;
13
+ exclusive: (...args: TArgs) => TResult;
14
+ };
15
+ /** Synchronous SQLite connection with QMD-compatible transactions. */
16
+ export declare class Database extends DatabaseSync {
17
+ transaction<TArgs extends unknown[], TResult>(operation: (...args: TArgs) => TResult): Transaction<TArgs, TResult>;
18
+ }
19
+ /** Statement type used throughout QMD. */
20
+ export type Statement<T extends SQLiteParams = SQLiteParams> = StatementSync;
21
+ /** Open a writable QMD database using Node's built-in SQLite runtime. */
26
22
  export declare function openDatabase(path: string): Database;
27
23
  /** Open an existing database without changing journal mode, schema, or user data. */
28
24
  export declare function openReadOnlyDatabase(path: string): Database;
29
- /**
30
- * Database and Statement types used throughout QMD.
31
- */
32
- export type Database = BetterSqlite3.Database;
33
- export type Statement<T extends SQLiteParams = SQLiteParams> = BetterSqlite3.Statement<T>;
34
- /**
35
- * Load the sqlite-vec extension into a database.
36
- *
37
- * Throws with fix instructions when the extension is unavailable.
38
- */
25
+ /** Load the sqlite-vec extension into a database. */
39
26
  export declare function loadSqliteVec(db: Database): void;
package/dist/db.js CHANGED
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * db.ts - SQLite database connection and extension management
3
3
  *
4
- * Provides Database export and connection management using better-sqlite3
5
- * and sqlite-vec.
4
+ * Provides a synchronous node:sqlite connection with QMD's transaction helper
5
+ * and sqlite-vec extension loading.
6
6
  */
7
- import BetterSqlite3 from "better-sqlite3";
7
+ import { DatabaseSync } from "node:sqlite";
8
8
  import * as sqliteVec from "sqlite-vec";
9
+ let savepointSequence = 0;
9
10
  function isBusyError(err) {
10
11
  if (typeof err !== "object" || err === null)
11
12
  return false;
@@ -18,12 +19,57 @@ function isBusyError(err) {
18
19
  function sleepSync(ms) {
19
20
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
20
21
  }
22
+ /** Synchronous SQLite connection with QMD-compatible transactions. */
23
+ export class Database extends DatabaseSync {
24
+ transaction(operation) {
25
+ const execute = (mode, args) => {
26
+ if (!this.isTransaction) {
27
+ this.exec(`BEGIN ${mode}`);
28
+ try {
29
+ const result = operation(...args);
30
+ this.exec("COMMIT");
31
+ return result;
32
+ }
33
+ catch (error) {
34
+ try {
35
+ this.exec("ROLLBACK");
36
+ }
37
+ catch { }
38
+ throw error;
39
+ }
40
+ }
41
+ const savepoint = `qmd_${++savepointSequence}`;
42
+ this.exec(`SAVEPOINT ${savepoint}`);
43
+ try {
44
+ const result = operation(...args);
45
+ this.exec(`RELEASE ${savepoint}`);
46
+ return result;
47
+ }
48
+ catch (error) {
49
+ try {
50
+ this.exec(`ROLLBACK TO ${savepoint}`);
51
+ this.exec(`RELEASE ${savepoint}`);
52
+ }
53
+ catch { }
54
+ throw error;
55
+ }
56
+ };
57
+ const transaction = ((...args) => execute("DEFERRED", args));
58
+ transaction.deferred = (...args) => execute("DEFERRED", args);
59
+ transaction.immediate = (...args) => execute("IMMEDIATE", args);
60
+ transaction.exclusive = (...args) => execute("EXCLUSIVE", args);
61
+ return transaction;
62
+ }
63
+ }
64
+ function resolveBusyTimeout() {
65
+ const raw = process.env.QMD_SQLITE_BUSY_TIMEOUT;
66
+ const parsed = raw !== undefined && raw !== "" ? Number(raw) : Number.NaN;
67
+ return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : 120_000;
68
+ }
21
69
  /**
22
70
  * Switch a connection to WAL, retrying on `SQLITE_BUSY` within the busy-timeout
23
- * budget. Unlike ordinary writes, migrating the journal needs a brief exclusive
24
- * lock and does NOT invoke the busy handler, so concurrent first-time opens of a
25
- * cold database throw "database is locked" even with `busy_timeout` set. Once the
26
- * database is already WAL the pragma is a cheap no-op that does not contend.
71
+ * budget. Migrating the journal needs a brief exclusive lock and does not invoke
72
+ * SQLite's busy handler on every supported runtime.
27
73
  */
28
74
  function enableWal(db, budgetMs) {
29
75
  const deadline = Date.now() + Math.max(budgetMs, 0);
@@ -39,46 +85,23 @@ function enableWal(db, budgetMs) {
39
85
  }
40
86
  }
41
87
  }
42
- /**
43
- * Open a SQLite database using better-sqlite3.
44
- *
45
- * `better-sqlite3` defaults `busy_timeout` to 0, so concurrent writers throw
46
- * `SQLITE_BUSY` instead of waiting. WAL improves read-while-write concurrency
47
- * but does not serialise writers. Setting the timeout at connection open makes
48
- * parallel processes queue at batch boundaries instead of failing on contact.
49
- *
50
- * WAL is enabled here too (with a bounded retry) so connection-level pragmas
51
- * live in one place and the cold-database journal migration survives concurrent
52
- * opens.
53
- *
54
- * Default 120_000 ms outlasts the worst-case batch commit on a multi-GB
55
- * index. Override with `QMD_SQLITE_BUSY_TIMEOUT` (value in milliseconds; `0`
56
- * restores the upstream fail-fast behaviour).
57
- */
88
+ /** Open a writable QMD database using Node's built-in SQLite runtime. */
58
89
  export function openDatabase(path) {
59
- const db = new BetterSqlite3(path);
60
- const raw = process.env.QMD_SQLITE_BUSY_TIMEOUT;
61
- const parsed = raw !== undefined && raw !== "" ? Number(raw) : Number.NaN;
62
- const busyTimeoutMs = Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : 120_000;
63
- db.exec(`PRAGMA busy_timeout = ${busyTimeoutMs}`);
90
+ const busyTimeoutMs = resolveBusyTimeout();
91
+ const db = new Database(path, { allowExtension: true, timeout: busyTimeoutMs });
64
92
  enableWal(db, busyTimeoutMs);
65
93
  return db;
66
94
  }
67
95
  /** Open an existing database without changing journal mode, schema, or user data. */
68
96
  export function openReadOnlyDatabase(path) {
69
- const options = { readonly: true, fileMustExist: true };
70
- const db = new BetterSqlite3(path, options);
71
- const raw = process.env.QMD_SQLITE_BUSY_TIMEOUT;
72
- const parsed = raw !== undefined && raw !== "" ? Number(raw) : Number.NaN;
73
- const busyTimeoutMs = Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : 120_000;
74
- db.exec(`PRAGMA busy_timeout = ${busyTimeoutMs}`);
75
- return db;
97
+ const busyTimeoutMs = resolveBusyTimeout();
98
+ return new Database(path, {
99
+ readOnly: true,
100
+ allowExtension: true,
101
+ timeout: busyTimeoutMs,
102
+ });
76
103
  }
77
- /**
78
- * Load the sqlite-vec extension into a database.
79
- *
80
- * Throws with fix instructions when the extension is unavailable.
81
- */
104
+ /** Load the sqlite-vec extension into a database. */
82
105
  export function loadSqliteVec(db) {
83
106
  try {
84
107
  sqliteVec.load(db);
package/dist/store.js CHANGED
@@ -1208,7 +1208,7 @@ function rebuildFTSForCjkNormalization(db) {
1208
1208
  }
1209
1209
  });
1210
1210
  // Pull bounded batches and finalize each SELECT before starting the insert
1211
- // transaction. Holding a better-sqlite3 iterator open while beginning a
1211
+ // transaction. Holding an SQLite iterator open while beginning a
1212
1212
  // transaction on the same connection raises "database is busy".
1213
1213
  const selectBatch = db.prepare(`
1214
1214
  SELECT d.id, d.collection, d.path, d.title, content.doc as body
@@ -1532,6 +1532,12 @@ export function setStoreGlobalContext(db, value) {
1532
1532
  db.prepare(`INSERT INTO store_config (key, value) VALUES ('global_context', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value`).run(value);
1533
1533
  }
1534
1534
  }
1535
+ function writeConfigSyncDiagnostic(db, diagnostic) {
1536
+ db.prepare(`
1537
+ INSERT INTO store_config (key, value) VALUES ('config_sync_diagnostic', ?)
1538
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value
1539
+ `).run(JSON.stringify(diagnostic));
1540
+ }
1535
1541
  function storeCollectionMatchesConfig(row, collection) {
1536
1542
  return row.path === collection.path
1537
1543
  && row.pattern === (collection.pattern || '**/*.md')
@@ -1556,12 +1562,14 @@ export function syncConfigToDb(db, config) {
1556
1562
  });
1557
1563
  const currentGlobalContext = getStoreGlobalContext(db);
1558
1564
  if (allMatch && currentGlobalContext === config.global_context) {
1559
- return {
1565
+ const diagnostic = {
1560
1566
  configHashChanged: false,
1561
1567
  reconciled: false,
1562
1568
  collections: { added: [], updated: [], removed: [] },
1563
1569
  globalContextUpdated: false,
1564
1570
  };
1571
+ writeConfigSyncDiagnostic(db, diagnostic);
1572
+ return diagnostic;
1565
1573
  }
1566
1574
  }
1567
1575
  }
@@ -2905,7 +2913,7 @@ export async function maybeAdoptLegacyEmbeddingFingerprint(store, model = DEFAUL
2905
2913
  return { checked: true, adopted: 0, reason: `legacy sample differs from current fingerprint (nearest ${nearest.hash_seq}, distance ${nearest.distance.toFixed(6)})` };
2906
2914
  }
2907
2915
  const update = withLazyContentVectorMigration(db, () => db.prepare(`UPDATE content_vectors SET embed_fingerprint = ? WHERE model = ? AND embed_fingerprint = ''`).run(fingerprint, model));
2908
- return { checked: true, adopted: update.changes, reason: `sample ${expectedHashSeq} matched current fingerprint at distance ${nearest.distance.toFixed(6)}` };
2916
+ return { checked: true, adopted: Number(update.changes), reason: `sample ${expectedHashSeq} matched current fingerprint at distance ${nearest.distance.toFixed(6)}` };
2909
2917
  });
2910
2918
  }
2911
2919
  export function getIndexHealth(db, model = DEFAULT_EMBED_MODEL) {
@@ -2952,7 +2960,7 @@ export function clearCache(db) {
2952
2960
  */
2953
2961
  export function deleteLLMCache(db) {
2954
2962
  const result = db.prepare(`DELETE FROM llm_cache`).run();
2955
- return result.changes;
2963
+ return Number(result.changes);
2956
2964
  }
2957
2965
  /**
2958
2966
  * Remove inactive document records (active = 0).
@@ -2976,7 +2984,7 @@ export function cleanupOrphanedContent(db) {
2976
2984
  DELETE FROM content
2977
2985
  WHERE hash NOT IN (SELECT DISTINCT hash FROM documents)
2978
2986
  `).run();
2979
- return result.changes;
2987
+ return Number(result.changes);
2980
2988
  }
2981
2989
  /**
2982
2990
  * Count content hashes that would be unreferenced after inactive documents
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wei840222/qmd",
3
- "version": "2026.8.28",
3
+ "version": "2026.9.6",
4
4
  "packageManager": "pnpm@11.15.1",
5
5
  "description": "Query Markup Documents - On-device hybrid search for markdown files with BM25, vector search, and LLM reranking",
6
6
  "type": "module",
@@ -66,7 +66,6 @@
66
66
  "dependencies": {
67
67
  "@modelcontextprotocol/server": "2.0.0",
68
68
  "@node-rs/jieba": "2.0.2",
69
- "better-sqlite3": "^13.0.3",
70
69
  "fast-glob": "3.3.3",
71
70
  "node-llama-cpp": "3.20.0",
72
71
  "picomatch": "4.0.5",
@@ -88,7 +87,6 @@
88
87
  },
89
88
  "devDependencies": {
90
89
  "@oxlint/plugins": "1.78.0",
91
- "@types/better-sqlite3": "7.6.13",
92
90
  "oxlint": "1.78.0",
93
91
  "tsx": "4.23.12",
94
92
  "vitest": "3.2.7"
@@ -113,7 +111,7 @@
113
111
  "typescript": "^5.9.3 || ^6.0.0-0"
114
112
  },
115
113
  "engines": {
116
- "node": ">=22.0.0"
114
+ "node": ">=22.16.0"
117
115
  },
118
116
  "keywords": [
119
117
  "markdown",
@@ -230,8 +230,6 @@ Query types:
230
230
  - `vec` — vector semantic search. Best for natural-language concepts.
231
231
  - `hyde` — vector search using a hypothetical answer/document passage.
232
232
 
233
- When invoking `query` with a plain `query` string instead of explicit `searches`, you can set `includeHyde: false` (to omit HyDE passage generation) and `expansion: "auto" | "force" | "skip"`.
234
-
235
233
  ## Query craft
236
234
 
237
235
  Good QMD searches mix three things:
@@ -30,7 +30,6 @@ newline = "\n" ;
30
30
 
31
31
  A query is either a single policy query or a multi-line query document:
32
32
  - **`auto` (Default)**: CJK queries and strong lexical matches automatically bypass model expansion. Other plain queries expand into `lex`, `vec`, and `hyde` variants.
33
- - **`--no-hyde`**: Disables HyDE (hypothetical document) in query expansion, generating only `lex` and `vec` variants (faster, avoids hallucinated passage drift).
34
33
  - **`expand:` / `--expand` (`force`)**: Explicitly forces expansion even if bypass heuristics apply.
35
34
  - **`lex:` (`skip`)**: Explicitly disables expansion and performs direct BM25 search.
36
35
 
@@ -38,9 +37,6 @@ A query is either a single policy query or a multi-line query document:
38
37
  # Automatic policy:
39
38
  qmd query "how does authentication work"
40
39
 
41
- # Disable HyDE during expansion:
42
- qmd query --no-hyde "how does authentication work"
43
-
44
40
  # Force expansion:
45
41
  qmd query "expand: how does authentication work"
46
42
  # or: qmd query --expand "資料庫同步"
@@ -85,8 +81,6 @@ A 50–100 word hypothetical answer passage representing what the target documen
85
81
  hyde: The rate limiter uses a sliding window counter algorithm with a 60-second window. When a client exceeds 100 requests per minute, subsequent requests return 429 Too Many Requests.
86
82
  ```
87
83
 
88
- When relying on query expansion, HyDE generation can be excluded using `--no-hyde` (CLI) or `"includeHyde": false` (MCP/SDK).
89
-
90
84
  ## Multi-Line Structured Queries
91
85
 
92
86
  Combine multiple sub-query types for optimal retrieval. The first sub-query receives **2x weight** during Reciprocal Rank Fusion:
@@ -156,9 +150,8 @@ When calling the `qmd` MCP server's `query` tool, provide a structured `searches
156
150
 
157
151
  ```json
158
152
  {
159
- "query": "authentication flow",
153
+ "query": "CAP theorem consistency",
160
154
  "expansion": "auto",
161
- "includeHyde": false,
162
155
  "explain": true,
163
156
  "collections": ["docs"]
164
157
  }