ko-qmd 2.8.3-ko.10

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 (76) hide show
  1. package/CHANGELOG.md +1937 -0
  2. package/LICENSE +21 -0
  3. package/README.en.md +184 -0
  4. package/README.md +292 -0
  5. package/bin/qmd +192 -0
  6. package/dist/ast.d.ts +65 -0
  7. package/dist/ast.js +51 -0
  8. package/dist/bench/bench.d.ts +41 -0
  9. package/dist/bench/bench.js +12 -0
  10. package/dist/bench/score.d.ts +33 -0
  11. package/dist/bench/score.js +1 -0
  12. package/dist/bench/types.d.ts +101 -0
  13. package/dist/bench/types.js +1 -0
  14. package/dist/cli/build-info.json +4 -0
  15. package/dist/cli/embed-lock.d.ts +24 -0
  16. package/dist/cli/embed-lock.js +2 -0
  17. package/dist/cli/formatter.d.ts +120 -0
  18. package/dist/cli/formatter.js +60 -0
  19. package/dist/cli/mcp-pid.d.ts +33 -0
  20. package/dist/cli/mcp-pid.js +1 -0
  21. package/dist/cli/qmd.d.ts +101 -0
  22. package/dist/cli/qmd.js +165 -0
  23. package/dist/cli/version.d.ts +42 -0
  24. package/dist/cli/version.js +1 -0
  25. package/dist/collections.d.ts +162 -0
  26. package/dist/collections.js +1 -0
  27. package/dist/db.d.ts +63 -0
  28. package/dist/db.js +2 -0
  29. package/dist/filters.d.ts +46 -0
  30. package/dist/filters.js +1 -0
  31. package/dist/grep.d.ts +85 -0
  32. package/dist/grep.js +9 -0
  33. package/dist/hangul.d.ts +102 -0
  34. package/dist/hangul.js +1 -0
  35. package/dist/index.d.ts +270 -0
  36. package/dist/index.js +1 -0
  37. package/dist/llm.d.ts +584 -0
  38. package/dist/llm.js +40 -0
  39. package/dist/maintenance.d.ts +33 -0
  40. package/dist/maintenance.js +1 -0
  41. package/dist/mcp/daemon-health.d.ts +17 -0
  42. package/dist/mcp/daemon-health.js +1 -0
  43. package/dist/mcp/origin-guard.d.ts +67 -0
  44. package/dist/mcp/origin-guard.js +1 -0
  45. package/dist/mcp/server.d.ts +131 -0
  46. package/dist/mcp/server.js +35 -0
  47. package/dist/memory.d.ts +74 -0
  48. package/dist/memory.js +13 -0
  49. package/dist/metadata-filter.d.ts +74 -0
  50. package/dist/metadata-filter.js +1 -0
  51. package/dist/metadata-store.d.ts +45 -0
  52. package/dist/metadata-store.js +68 -0
  53. package/dist/metadata.d.ts +61 -0
  54. package/dist/metadata.js +1 -0
  55. package/dist/paths.d.ts +1 -0
  56. package/dist/paths.js +1 -0
  57. package/dist/query-log.d.ts +156 -0
  58. package/dist/query-log.js +2 -0
  59. package/dist/refresh.d.ts +60 -0
  60. package/dist/refresh.js +1 -0
  61. package/dist/rerank-input.d.ts +46 -0
  62. package/dist/rerank-input.js +5 -0
  63. package/dist/store.d.ts +1267 -0
  64. package/dist/store.js +421 -0
  65. package/dist/trust.d.ts +152 -0
  66. package/dist/trust.js +2 -0
  67. package/package.json +124 -0
  68. package/scripts/build.mjs +130 -0
  69. package/scripts/check-package-grammars.mjs +29 -0
  70. package/scripts/package-smoke.mjs +65 -0
  71. package/scripts/test-all.mjs +42 -0
  72. package/skills/qmd/SKILL.md +333 -0
  73. package/skills/qmd/references/mcp-setup.md +122 -0
  74. package/skills/release/SKILL.md +141 -0
  75. package/skills/release/scripts/install-hooks.sh +38 -0
  76. package/skills/release/scripts/release-context.sh +129 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,1937 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ## [2.8.3-ko.10] - 2026-10-15
6
+
7
+ ko-qmd 2.8.3-ko.10 adds a shared working memory for agents: three MCP tools
8
+ write provenance-tagged claims into a `mem` collection and read them back, so
9
+ Claude Code, opencode and pi share one store through the same daemon.
10
+
11
+ - MCP: `memory_store`, `memory_recall` and `memory_correct` over a collection
12
+ named `mem`. Every claim carries `actor`, `type` (fact · opinion · decision),
13
+ optional `source` and `project` frontmatter. A claim-lint gate (R1–R6) rejects
14
+ missing fields, facts or decisions without a locatable source, oversize
15
+ fields, out-of-range confidence and anything that looks like a secret before
16
+ a file is written. Corrections are append-only: `memory_correct`
17
+ writes a new file that `supersedes` the old one and stamps the old file with a
18
+ `superseded_by` footer. `memory_recall` searches `mem` and `kb` together,
19
+ narrows by actor or project, and marks a replaced claim with `superseded_by`.
20
+ Writes are lex-searchable at once; vectors follow on the next
21
+ `qmd embed -c mem`.
22
+ - `memory_correct` path checks use the platform separator, so the
23
+ stay-inside-the-collection check no longer rejects every correction on
24
+ Windows.
25
+ - Build: minification calls the esbuild JS API instead of spawning
26
+ `node_modules/.bin/esbuild.exe`, which npm never creates on Windows
27
+ (only `.cmd`/`.ps1` shims), so `npm install` fails there no more.
28
+
29
+ - `qmd mcp --http --port` now validates the port: non-numeric or outside
30
+ 1–65535 exits 1 with an error instead of silently falling back to 8181
31
+ (`--port 0`/`abc`) or crashing downstream on bind (`--port -1`).
32
+ - SDK `addCollection` write-through no longer drops `ignore` (and preserves
33
+ `includeByDefault`/`update`/`context`): the YAML/inline entry is merged
34
+ instead of replaced, so `ignore` survives a close/reopen round-trip instead
35
+ of being nulled in the DB on resync.
36
+ - Idle-unload guard now sees `withLLMSessionForLlm` sessions: per-instance
37
+ session managers are shared through a WeakMap and `canUnloadLLM(llm)` scopes
38
+ the verdict to that instance, so a live store session vetoes its own unload
39
+ (no more idle-timer dispose of a working context) while an unrelated
40
+ instance's session no longer pins it. No-arg `canUnloadLLM()` behavior on the
41
+ default path is unchanged.
42
+ - `qmd update` no longer claims "✓ All collections updated." when trust-gated
43
+ collections were skipped: it prints `Updated N of M collection(s), skipped K`
44
+ and exits 1 so unattended runs can detect the partial update. No skips keeps
45
+ the success line and exit 0.
46
+
47
+ - Reindex no longer aborts the whole `qmd update` when a file vanishes
48
+ between read and stat (ENOENT mid-pass, e.g. an update hook mutating the
49
+ tree): the `statSync` pair is guarded exactly like the #460 read guard —
50
+ skip + report via `skippedFiles`, rest of the pass completes.
51
+ - `generateEmbeddings`: the 80%-error-rate abort now stops the whole run
52
+ instead of just the current 32-chunk window. Previously the outer
53
+ doc-batch loop kept going (re-warning per batch) and the post-batch
54
+ `retryFailedChunks(true)` immediately re-embedded the just-aborted
55
+ chunks, neutralizing the circuit breaker. The abort flag now breaks the
56
+ outer loop and skips the force-retry pass.
57
+ - Rerank cascade head batch 15 → 8 (`RERANK_CASCADE_FIRST_BATCH`; `QMD_RERANK_FIRST_BATCH` /
58
+ `QMD_RERANK_MARGIN` overrides added for tuning). Cold hard-44 probe at
59
+ `candidateLimit` 40: rerank p50 2492 → 1267 ms, p90 2570 → 1352 ms, top-1
60
+ agreement 44/44; ko-vault seam gate holds (hard `full_r1`=0.6630, easy
61
+ `full_r5` +1 query).
62
+ - Rerank input now strips a leading YAML frontmatter block over 350 chars
63
+ (`LONG_FRONTMATTER_CHARS` in `src/rerank-input.ts`) and no longer prepends
64
+ `# <title>` to the chunk. Third autoresearch loop (24 exps): train
65
+ `full_mrr` 0.6769 → 0.6880, held-out 0.7459 → 0.7572; ko-vault seam lines
66
+ byte-identical. ~90 notes carry up-to-2800-char `sources:` URL dumps that
67
+ filled the whole first chunk, so the reranker scored boilerplate.
68
+
69
+ - Dependencies: `node-llama-cpp` 3.20.0 → **3.22.1**. ko-vault seam bench lines
70
+ are byte-identical before and after; Nix flake `qmd-node-modules` FOD hashes
71
+ refreshed for the new tree.
72
+
73
+ ## [2.8.3-ko.9] - 2026-10-10
74
+
75
+ ko-qmd 2.8.3-ko.9 buys rank-1 accuracy and rerank latency at once. Per-document
76
+ coverage weighting on relaxed lex lists takes hard `hybrid_r1` from 0.4444 to
77
+ 0.4778, and head-first rerank cascade halves rerank p50 with identical top-1
78
+ answers. A load timeout (default 120 s) stops one stuck model load from hanging
79
+ the query, as the scoring deadlines never covered the pre-ready wait.
80
+
81
+ - `runWithDeadline` now bounds the model-load wait itself (`QMD_LLM_LOAD_TIMEOUT_MS`,
82
+ default 120 s, `0` = wait forever). The scoring deadlines never covered a load
83
+ that reports ready never: the pre-ready race hung with no timer, so one stuck
84
+ load hung the query. Past the load bound the stage is skipped exactly like a
85
+ scoring deadline (same `timeouts` entry, same RRF-order / original-query
86
+ fallback, same abandoned-run cache fill), and the bounded wait is reported as
87
+ the `load` stage. Listed in `qmd doctor`.
88
+ - `QMD_RERANK_CASCADE=0` (or `false`/`off`/`no`) restores single-batch rerank.
89
+ Kill switch for measuring the cascade and for operations; unset cascades.
90
+ Listed in `qmd doctor`.
91
+ - Rerank now cascades: the RRF head (15) is scored first, and the tail is
92
+ skipped with score 0 (RRF position order only) when the head's top-2 margin
93
+ reaches 0.08 (`RERANK_CASCADE_FIRST_BATCH`/`RERANK_CASCADE_MARGIN` in
94
+ `src/store.ts`). A contested top scores everything, as before; deadlines and
95
+ the rerank cache are unchanged (a skipped tail writes no cache entries).
96
+ - `structuredSearch` and `hybridQuery` now weight a relaxed (any-word) lex list per
97
+ document by query coverage (`relaxedDocCoverage` in `src/store.ts`: 0.5 + 0.5 ×
98
+ matched units, mirroring the rerank chunk scorer's Hangul bigrams). A global
99
+ constant could not split the mirrored tie of `hmu-05` vs `sem-08` (2026-10-09
100
+ probe); both lead one list and take the same top-rank bonus, so only a per-doc
101
+ signal separates them. List weights are unchanged.
102
+ ## [2.8.3-ko.8] - 2026-10-09
103
+
104
+ - `structuredSearch` (MCP `searches`, SDK `search({ queries })`) no longer lets a relaxed
105
+ (any-word) first lex list keep the 2x positional boost: it now counts 0.25 instead of 1.0.
106
+ Even halved, a relaxed rank 1 still tied a vector rank 1 against a lex rank 4 (`hmu-05`
107
+ probe), so the decoy sharing a few question words kept rank 1 on the `rerank:false` path.
108
+ On the ko-vault seam form, hard `hybrid_r1` goes 0.4111 → 0.4444 with `full_r1` unchanged at
109
+ 0.6111; one easy query (`sem-08`) trades hybrid rank 1 for 2 while its `full` rank 1 and all
110
+ recall@5 lines hold. Non-first relaxed lists still count half, as before.
111
+ - Hangul loanword table gains 11 entries mined from real query-log traffic (`지연` →
112
+ `latency`, `병렬` → `parallel`, `게이트` → `gate`, `가드` → `guard`, `패널` → `panel`,
113
+ `팔레트` → `palette`, `하네스` → `harness`, `옵시디언` → `obsidian`, `백링크` → `backlink`,
114
+ `포즈` → `pause`, `루프` → `loop`). Native words from the same logs (`색인`·`질의`·`요약`·`문서`
115
+ …) stay unbridged. Query-side only, no re-index; ko-vault bench lines are unchanged.
116
+ - Negation routing stays strip-only by measurement: demoting X with lex `-term`s was tried
117
+ and reverted — the expected note often discusses X itself (`bm25-ranking.md` mentions TF-IDF),
118
+ so NOT removed the answer with the decoy (`hybrid_r1` 0.4111 → 0.3778). `stripHangulNegation`
119
+ now shares its parsing through an internal split helper; behavior is unchanged.
120
+
121
+ ## [2.8.3-ko.7] - 2026-10-02
122
+
123
+ The query path is now observable and bounded. Every query-log row breaks its latency into
124
+ stages, the daemon logs its heap once an hour, and expansion and rerank stop waiting past a
125
+ deadline instead of holding a query for 85 s. The reranker also sees a note's first chunk by default.
126
+
127
+ - `qmd mcp` (stdio and HTTP) no longer waits on a slow LLM stage without bound. Past
128
+ `QMD_EXPAND_DEADLINE_MS` (default 5 s) a query searches its original text alone, and past
129
+ `QMD_RERANK_DEADLINE_MS` (default 10 s) it answers in RRF order; `0` restores waiting. The clock
130
+ starts once the model is loaded, so a cold start or a reload after the idle unload is not a timeout;
131
+ the query log records that wait as a `load` stage. One MCP `query` with expansion and rerank took
132
+ 85 s. The REST `/query` response and the MCP `query` tool say which stage timed out (`timeouts`),
133
+ the query log row lists it the same way, and a timed-out rerank reads `rerank: "timeout"` there.
134
+ A fallback does not apply `minScore` to its 1/rank scores. An abandoned expansion or rerank keeps
135
+ running and fills its cache; until it finishes, later queries skip that stage (logged once) instead
136
+ of queueing behind it. The CLI search commands and the SDK (`rerankDeadlineMs`, `expandDeadlineMs`
137
+ options) never set a deadline on their own.
138
+
139
+ - The daemon's query log row now carries `stages`, the milliseconds spent in each stage of the
140
+ search: `refresh` (the pre-query re-index check), `expand`, `fts`, `embed`, `vec`, `chunk`,
141
+ `rerank` and `serialize`; a stage that did not run is absent. REST `rerank:false` queries had a p95 of 2.9–5.3 s
142
+ against a p50 of 0.2–0.5 s with no rerank in the path, and the single `ms` could not say which
143
+ stage the tail came from. Daemon log lines now start with a dated local ISO timestamp instead of
144
+ a UTC `HH:mm:ss.SSS`, so a slow line can be matched to its row.
145
+
146
+ - The HTTP daemon logs a `health` line every hour: heap used and total, rss, and how many
147
+ `subscriptions/listen` and other `/mcp` requests have a handler still waiting on its response. A daemon that ran ten days died with
148
+ "Reached heap limit" after its heap crept to 3.8 GB under idle discover/cancelled traffic, and
149
+ nothing in the log showed the slope or what was holding memory. `QMD_HEALTH_LOG_INTERVAL_MS`
150
+ changes the interval; `0` turns it off.
151
+
152
+ - `scripts/autoresearch/prepare.sh` builds the goldset from more than the collection folder and over
153
+ several runs. `AR_EXTRA_DIRS="outputs docs"` copies sibling folders of the collection into the corpus
154
+ (names only, so no vault path lands in git), `AR_RUN=N` writes `generated-N.json` from documents no
155
+ earlier run answered (`bench-vault-paraphrase.ts --dirs`, `--skip`), and `split` merges every run,
156
+ drops the query ids listed in `dropped.txt`, and splits only each run's new documents, so an earlier
157
+ train/held-out assignment never moves.
158
+
159
+ - The reranker now gets a note's first chunk, its title and summary: the first chunk starts 16
160
+ points ahead in chunk selection (8 when an intent is given, so intent words can still pick a later
161
+ chunk). A later chunk needs 17 more points, about nine English words or seventeen Hangul syllable
162
+ pairs, so for a typical short query the first chunk is always sent. A second autoresearch loop found
163
+ this on a goldset of 320 train and 71 held-out queries drawn from four vault folders. Held-out full
164
+ MRR goes 0.7291 → 0.7459, ko-vault seam and hard lines are unchanged, and bm25, vector and hybrid
165
+ results are identical (`BASELINE.md` 2026-09-29).
166
+
167
+ ## [2.8.3-ko.6] - 2026-09-29
168
+
169
+ Korean queries are handled better before and during reranking. A query written
170
+ as "X 말고 Y" now searches for Y alone, and a Hangul query skips LLM expansion,
171
+ which steadies hard-query results and cuts hybrid latency from about 1s to tens
172
+ of ms per query. The reranker is now shown the chunk that best matches the
173
+ query's Hangul syllable bigrams, led by the note title: held-out full MRR goes
174
+ 0.6589 → 0.7260 on a goldset generated from a real vault. Scoped vector search on
175
+ small indexes runs one KNN (vector stage 250–1300ms → about 25ms). The README
176
+ gains an English edition and an FAQ.
177
+
178
+ - `QMD_RERANK_CACHE=<path>` keeps rerank scores in their own sqlite file instead of the index's
179
+ `llm_cache`, so a store over a rebuilt index reuses them. The key carries the query, the rerank
180
+ model, the doc-token cap and the chunk text, not the scoring code in `src/llm.ts`, so the file is
181
+ safe across index rebuilds with the same reranker code. It is not trimmed to 1000 rows. With it
182
+ set, a second `scripts/bench-ko.sh` run on ko-vault gives the same lines and its `full` backend
183
+ spends 3.7 s reranking instead of 300 s (the whole run drops from 5:17 to 0:56). Unset, nothing
184
+ changes. A fallback result (no rerank context could be created, every score 0.5) is no longer
185
+ cached in either place.
186
+
187
+ - The reranker now sees a better chunk of a long Korean note. The chunk is chosen by the
188
+ two-syllable pairs of the query's Hangul words of three or more syllables, instead of whole
189
+ words, so `임베딩은` still finds a chunk about `임베딩`. Generic two-syllable words such as 방법
190
+ and 경우 no longer steer the choice. A query word with no Hangul (an English term, say) counts double. When the chosen chunk
191
+ does not contain the note title, `# <title>` is put in front of it. An autonomous loop
192
+ (`scripts/autoresearch/`) found this over 28 experiments on a goldset generated from a private
193
+ vault. The goldset's held-out split, which the loop never saw, goes from full MRR 0.6589 to
194
+ 0.7260 (32 queries). ko-vault seam and hard lines are unchanged. The bm25, vector and hybrid
195
+ results do not read the rerank input and are identical on both sets (`BASELINE.md`
196
+ 2026-09-28). An intent word now counts 1 point in chunk selection (was 0.5), keeping it at half a
197
+ query word now that a non-Hangul query word counts 2; no goldset carries intent, so no bench
198
+ number moves.
199
+
200
+ - The reranker's input (the query string, which chunk of each candidate is sent and the document
201
+ text) is now built in `src/rerank-input.ts` for both `hybridQuery` and `structuredSearch`, which
202
+ had copied the chunk-scoring loop. Results are unchanged.
203
+
204
+ - A Korean query of the form "X 말고 Y" now searches for Y alone. `hybridQuery` (CLI `qmd query`,
205
+ plain MCP `query`) and the `lex`/`vec` lines of `structuredSearch` (MCP `searches`, typed lines)
206
+ drop everything up to the last whitespace-separated 말고, 빼고, 제외하고 or 제외한. The rerank
207
+ query follows. `hyde` lines, `intent` and `qmd search`/`vsearch` stay as written. Before this
208
+ change every list also searched for X, the concept the user ruled out, so a note about X often
209
+ took rank 1. `아닌` and bare `제외` are not markers, because they also state conditions or act as
210
+ nouns. On the ko-vault seam form, the `neg` queries' full MRR goes 0.4500 → 0.6944. Hard
211
+ `full_r1` goes 0.5111 → 0.6111 and hard `full_mrr` 0.7472 → 0.8287. No query outside `neg`
212
+ changed its results, and that run is the gate's new reference (`BASELINE.md` 2026-09-27,
213
+ "a Korean negation clause is stripped before search").
214
+
215
+ - `structuredSearch` (MCP `searches`, SDK `search({ queries })`, typed `lex:`/`vec:` lines) now
216
+ counts a relaxed lex list, one that matched only through the any-word retry, at half its slot's
217
+ weight, as `hybridQuery` already did. At a flat 2.0 for the first list, a lex line that matched
218
+ some of a Korean question's words outranked the vector list, and a decoy sharing those words took
219
+ rank 1. This one weight was the whole gap between the two paths on the Korean ko-vault queries:
220
+ re-fusing each path's lists with the other's weights reproduced its top 10 for 85 of 85 queries.
221
+ On the seam form, hard `hybrid_r1` goes 0.3111 → 0.3778 and `hybrid_r5` 0.8996 → 0.9211, with
222
+ `full_r1` unchanged at 0.5111, and that run is the gate's new reference (`BASELINE.md` 2026-09-27).
223
+
224
+ - A query with any Hangul is no longer expanded by the LLM on the hybrid path (`hybridQuery`). That
225
+ path serves CLI `qmd query`, including an explicit `expand:` line, as well as a plain MCP `query`
226
+ and SDK `search({ query })`; `qmd vsearch` still expands. On the ko-vault goldset the
227
+ expansion model wrapped Korean questions in English sentence templates and stray Chinese, and its
228
+ sampling alone moved hard rank 1 by up to five of 30 queries between runs. Without it, hard
229
+ `hybrid_r1` reads 0.3778 (0.2611–0.3611 with expansion), `full_r1` holds at 0.5111, the easy
230
+ queries lose nothing, and the median hybrid query drops from about 1 s to 20 ms. The Korean
231
+ queries now return the same results on every run. English-only queries are still expanded, and a
232
+ caller who wants variants for a Korean question can pass its own `searches`
233
+ (`BASELINE.md` 2026-09-26).
234
+
235
+ - `scripts/bench-ko.sh` benches the seam form by default. Every one-line goldset query reaches
236
+ `qmd bench` as `lex: <q>` + `vec: <q>`, the shape REST/MCP callers send, which skips LLM query
237
+ expansion. `KO_FORM=plain` keeps the old path, and the `RESULT-HARD` line now ends with
238
+ `form=<seam|plain>`. The `dogfood.sh` gate refuses a bench whose form differs from its baseline
239
+ line's form. On the seam form it gates hard `hybrid_r1` as well as `full_r1`, against a reference
240
+ line with `tol=0`, since the seam form gave the same lines in every run. The reason is the plain
241
+ form's hard wobble, which turned out to be the expansion sampler alone. Replaying recorded
242
+ expansions reproduces every query's top 10, and every query that moved between same-commit runs
243
+ had drawn a different expansion. The draw moved `hybrid_r1` by up to five of 30 queries and once
244
+ put `full_r1` 0.0007 above the old gate's floor (`BASELINE.md` 2026-09-26). The gate also runs
245
+ the bench with any exported `KO_BENCH`/`KO_CORPUS`/`KO_FORM` removed, and `bench-ko.sh` refuses a
246
+ `KO_BENCH` inside `tmp/bench-ko/`, where the run clears its outputs.
247
+
248
+ - `scripts/bench-ko.sh` takes `KO_CORPUS` (a corpus directory holding `wiki/` and
249
+ `distractors/`) and `KO_BENCH` (a goldset) so an A/B runs through the same isolated harness
250
+ and stdout lines. Overrides resolve against the caller's cwd and are checked before the
251
+ previous run's output is removed. The first use settled a question: three everyday-language
252
+ question lines per document, decoys included, did not move hard rank 1 on the REST-shaped
253
+ goldset (hybrid_r1 0.3111 → 0.2778, full_r1 flat at 0.5111; hard hit@5 up 2 of 30). Index-time
254
+ summaries are therefore not being built. The same runs point the `hybrid_r1` wobble at LLM
255
+ query expansion rather than re-embedding (`BASELINE.md` 2026-09-25).
256
+ - ko-vault goldset: 69 near-topic distractor documents and 30 hard queries (`sem-hard` /
257
+ `multi` / `neg`) join the fixture; `bench-ko.sh` indexes both directories (97 files) and
258
+ prints a `RESULT-HARD hybrid_r1=… full_r1=… n=30` line next to `RESULT`. The `dogfood.sh`
259
+ gate now also fails when the hard `full_r1` falls below the baseline minus the `tol=` recorded
260
+ on that baseline line, and fails closed when `RESULT-HARD` or its `tol=` goes missing.
261
+ `full_r1` is gated rather than `hybrid_r1` because it held one value across eight same-commit
262
+ runs, while `hybrid_r1` moved by about 3.5 queries of 30.
263
+ Fixture invariants (no title-term leakage, no excluded concept among expected files, no basename
264
+ collisions) are enforced by `test/ko-bench-fixture.test.ts`.
265
+
266
+ - Collection- or filter-scoped vector search is faster on indexes of up to
267
+ 4096 vectors. It now post-filters one sqlite-vec KNN over the whole table,
268
+ which is exact because vec0 KNN is a brute-force scan, instead of computing
269
+ `vec_distance_cosine` row by row. On the KB goldset (1047 of 3389 vectors,
270
+ M3 Max) the vector stage dropped from 250–1300ms to about 25ms, and a cold
271
+ capped query (cap 128, candidateLimit 15) went from p90 2000ms to 906ms with
272
+ identical rankings. Larger indexes keep the exact scan.
273
+
274
+ ## [2.8.3-ko.5] - 2026-09-24
275
+
276
+ Rerank gets an input cap. Truncating each document to 128 tokens keeps KB
277
+ goldset quality (Hit@5 0.97 / Recall@5 0.77) while cutting cold rerank p90 from
278
+ 3.6s to 0.9s on an M3 Max. The cap can be set daemon-wide
279
+ (`QMD_RERANK_MAX_DOC_TOKENS`) or per request (`rerankMaxDocTokens` on REST
280
+ `/query` and SDK `search()`). The README is now a Korean guide to ko-qmd itself,
281
+ and the English reference lives in `docs/REFERENCE.md`.
282
+
283
+ ### Changes
284
+
285
+ - README.md is rewritten in Korean around ko-qmd itself: what differs from
286
+ upstream, install, quick start, MCP/REST/SDK, environment variables and the
287
+ maintenance guide that used to live in `README.ko.md` (now removed). The
288
+ upstream-derived English reference (full CLI, MCP tool parameters, SDK API,
289
+ score fusion) moved to `docs/REFERENCE.md`, with install and import names
290
+ switched to `ko-qmd`.
291
+ - REST `POST /query` and SDK `search()` take `rerankMaxDocTokens`, a
292
+ per-request rerank doc token cap. For that request it overrides the daemon's
293
+ `QMD_RERANK_MAX_DOC_TOKENS`, and it never raises the context budget. A caller
294
+ like the KB seam can then turn a capped rerank on for one machine without
295
+ changing the daemon's env. The rerank cache key follows the effective cap
296
+ (request, else env). Invalid values (non-integer, ≤ 0, strings) are ignored.
297
+ - `QMD_RERANK_MAX_DOC_TOKENS` caps the tokens of each document sent to the
298
+ reranker, below the context budget. Unset keeps today's behaviour. On the KB
299
+ goldset (40 queries, candidateLimit 15), a cap of 128 keeps quality at
300
+ Hit@5 0.97 / Recall@5 0.77 against 0.95 / 0.76 uncapped. It cuts cold p90 from
301
+ 3623ms to 897ms on an M3 Max and from 4720ms to 2192ms on another Apple Silicon
302
+ Mac. The cap is part of the rerank cache key, so capped and uncapped scores
303
+ never mix. Listed in `qmd doctor`.
304
+ - `scripts/dogfood.sh` warms the embedding model with one vec query after
305
+ every daemon restart (deploy and `--restore`). A fresh daemon's first vec
306
+ query took 15.5s, so the first KB seam search after a restart hit its 20s
307
+ limit and fell back to rg. A failed warm-up is a warning, not a failed restart.
308
+ - `scripts/dogfood.sh` waits for the *restarted* daemon. `launchctl kickstart -k`
309
+ returns while the old process is still shutting down and answering `/health`,
310
+ so the restart looked done ~9s before the new process listened. A `/health`
311
+ whose `uptime` predates the kickstart is now treated as the old process.
312
+
313
+ ## [2.8.3-ko.4] - 2026-09-24
314
+
315
+ Korean documents are now chunked at their real token budget, and Hangul
316
+ spellings of technical loanwords (`서치`, `웹 코어`) reach notes written in Latin
317
+ script. This release also syncs tobi/qmd up to `04e4dbd` — metadata filtering,
318
+ the oxlint fence and the REST JSON body guard — and adds `qmd grep`, `path`/
319
+ `since`/`until` scoping on MCP and REST, and a daemon that keeps the text index
320
+ fresh on its own. Existing indexes need one `qmd embed` run to pick up the new
321
+ chunking, and SDK callers that passed `filter` for path/time scoping must rename
322
+ it to `scope`.
323
+
324
+ ### Breaking
325
+
326
+ - SDK: ko-qmd's path/time narrowing option is now `scope` (was `filter`) on
327
+ `search()`, `searchLex()` and `searchVector()`, and the trailing `scope`
328
+ parameter of `searchFTS`/`searchVec`. `filter` is upstream's metadata filter.
329
+ CLI `--path/--since/--until` and REST/MCP `path/since/until` are unchanged,
330
+ and a request may pass both — a result has to satisfy each.
331
+
332
+ ### Upstream sync
333
+
334
+ - Merged tobi/qmd `main` up to `04e4dbd` (24 commits past v2.8.3): oxlint
335
+ fence (`bun run lint` is oxlint; the tsc check is `bun run test:types`),
336
+ metadata filtering, separator-aware FTS terms (quoted `"PIO-1384"` now
337
+ matches), REST JSON body guard, Windows index names, unreadable-gguf
338
+ diagnostics and the store-local embedding tokenizer. ko-qmd behaviour is
339
+ kept: the Hangul query branch runs ahead of the separator split, a metadata
340
+ filter gets the same anti-starvation scan as `--path`, and bm25 on the
341
+ ko-vault bench is identical per query.
342
+
343
+ ### Added
344
+
345
+ - Added Oxlint lint fence.
346
+ - Document metadata and metadata filtering. Markdown documents can opt into typed metadata through a namespaced frontmatter block (`qmd.metadata` with strings, numbers, booleans, or flat homogeneous arrays), and every search surface — CLI `search`/`vsearch`/`query` via `--filter <json>`, the SDK's `filter` option on `search()`/`searchLex()`/`searchVector()`, the MCP `query` tool, and HTTP `POST /query` and `/search` — accepts one shared recursive filter AST discriminated by `operator`: `and`/`or`/`not` logical groups, `eq`/`ne`/`gt`/`gte`/`lt`/`lte` comparisons, `in`/`nin`/`all` membership, and `exists` presence. Every returned result satisfies the filter (applied before RRF fusion and reranking); like collection filtering, highly selective filters remain best-effort for top-K completeness. Frontmatter stays ordinary searchable content — no chunking, embedding, snippet, or line-number changes — and documents without `qmd.metadata` behave exactly as before. JSON/SDK/MCP/HTTP results now include each document's indexed metadata, and `qmd status` reports how many documents still need metadata extraction (a normal `qmd update` backfills existing indexes).
347
+
348
+ ### Fixed
349
+
350
+ - Embedding generation and legacy fingerprint adoption now tokenize documents
351
+ with the store-selected embedding model instead of the global default. This
352
+ keeps chunk boundaries aligned with the model that creates and verifies the
353
+ stored vectors without initializing an unrelated provider.
354
+
355
+ ### Changes
356
+
357
+ - Plain Hangul lex terms that spell a common technical loanword (`서치`, `웹`,
358
+ `코어`, `마이그레이션`, about 80 entries in a built-in table) also match the
359
+ Latin spelling, so `하이브리드 서치` finds a note that only says
360
+ `hybrid-search` and `리액트 웹 코어` reaches `react-web-core`. The lookup runs
361
+ on particle-stripped stems too (`서치를`), single-syllable entries (`웹`, `훅`)
362
+ are bridged when they stand alone (not `웹을`), and terms with no entry produce the same FTS5 query as before.
363
+ Query-side only; no re-index. On the ko-vault bench the eleven new loanword
364
+ queries go from bm25_r5 0.0000 to 1.0000 while the original 52 hold at
365
+ 0.9519 per query.
366
+ - REST `POST /query` and the MCP `query` tool attach `vec_score` (cosine
367
+ similarity) and `fts_score` (normalized BM25) to each result on the
368
+ `rerank:false` path, where `score` is the 1/rank fusion position and cannot
369
+ be thresholded. Each is the max over the sub-queries that returned the hit
370
+ and is omitted, never 0, when that backend did not return it. The daemon
371
+ query log (`QMD_QUERY_LOG`) records the same two fields per result; the row
372
+ schema stays `v: 1` and `rerank:true` responses are unchanged.
373
+ - Korean documents are chunked at their real token budget instead of the
374
+ English-tuned 3.0 chars/token: `estimateCharsPerToken` blends CJK and other
375
+ scripts as a harmonic mean (measured ko 1.64 / en 6.08 chars per token on
376
+ Qwen3-Embedding-0.6B-Q8_0, 2026-09-21), and indexing plus both search-path
377
+ best-chunk selections size chunks with it. A 20KB Korean document now
378
+ first-passes into ~900-token chunks instead of being split ~855 and Korean
379
+ rerank chunks shrink from ~3600 to ~1440 chars. Non-CJK text keeps the ratio
380
+ each call site already used — 3.0 when indexing, 4.0 on the search paths — so
381
+ English chunk boundaries do not move, and the ko-vault bench holds
382
+ (bm25_r5=0.9519).
383
+ - Re-embedding a document whose chunk count shrank now prunes its stale
384
+ higher-`seq` vectors before inserting, so one `qmd embed` run converges
385
+ instead of waiting for a second run. The embedding fingerprint gains a
386
+ `chunk_chars_estimator` term, so `qmd status` reports re-embedding as needed
387
+ for existing indexes; run `qmd embed` once to pick up the new chunking.
388
+
389
+ - `search`, `vsearch` and `query` take `--path`, `--since` and `--until`, so a
390
+ search can be narrowed to part of a collection or to what changed recently.
391
+ `--path` matches globs against `collection/path` and is repeatable, with a
392
+ `!` prefix to exclude; `--since` / `--until` take a span (`7d`) or a date.
393
+ The filter narrows the corpus rather than the result list — excluded
394
+ documents are gone before ranking, on the vector path as well as BM25, so a
395
+ narrow filter returns its own best matches instead of whatever survived the
396
+ global top-K. A span that does not parse is an error, and a filter that
397
+ leaves nothing to search says so instead of looking like "no match".
398
+
399
+ - `qmd query --explain` now lists every expansion sub-query that placed the
400
+ document, with that sub-query's own rank, contribution, backend score and the
401
+ text it ran. It previously showed the top three contributions collapsed onto
402
+ one line without the query text, which could not distinguish a document that
403
+ ranked first in one sub-query and nowhere else from one that ranked middling
404
+ everywhere — two situations that call for different fixes.
405
+
406
+ - `qmd grep <pattern>` matches document bodies exactly, by string or regular
407
+ expression, with no ranking and no LLM: every matching line, grouped by file,
408
+ with line numbers that `qmd get <file>:<n>:<count>` accepts. It is the escape
409
+ hatch for a query that finds nothing because tokenization never produced the
410
+ term the document contains — the failure this fork keeps hitting with Korean
411
+ text — and for finding an identifier or error string verbatim. It searches
412
+ exactly what the index holds and honours collection scoping, `--path`,
413
+ `--since` and `--until`. Smart case: an uppercase letter in the pattern makes
414
+ it case-sensitive, and Hangul has no case so Korean patterns are unaffected.
415
+ `-F` searches for a literal string. An MCP `grep` tool exposes the same thing
416
+ to agents, which is the reason it exists: a client whose only tools are
417
+ `query`, `get` and `multi_get` has no other way to match a string exactly.
418
+
419
+ - The MCP `query` tool and the REST `/query` endpoint take the same `path`,
420
+ `since` and `until` parameters, so an agent can scope a search to a folder or
421
+ to recent changes without shelling out. A span that does not parse comes back
422
+ as an error rather than an unfiltered search, and an empty result under a
423
+ filter reports how many documents the filter left — an agent reading a bare
424
+ "No results" would otherwise conclude the corpus has nothing on the topic.
425
+
426
+ - The HTTP and stdio MCP daemon keeps the text index current on its own. Before
427
+ each search it checks the collections that search covers — at most once every
428
+ 30 seconds each — and re-indexes the ones whose files changed, so a note
429
+ written while the daemon runs is findable on the next query without `qmd
430
+ update`. It updates keyword search only; vectors still wait for `qmd embed`.
431
+ It does not run a collection's `update:` hook, does not clear the LLM cache
432
+ the way `qmd update` does, and does not touch a collection whose folder has
433
+ vanished or emptied, since reindexing would deactivate every document in it.
434
+
435
+ - The daemon's query log now covers the MCP `query` tool, not just the REST
436
+ endpoints. A search an agent runs through MCP over HTTP lands in the same
437
+ `queries-YYYY-MM.jsonl` with `via: "mcp"`, the same result path spelling and
438
+ the same `X-QMD-*` header handling, so a dogfooding session's real traffic is
439
+ in one file. A plain `query` argument is recorded as one search of type
440
+ `auto`. stdio MCP calls stay unlogged — they carry no request to annotate.
441
+
442
+ ## [2.8.3-ko.3] - 2026-09-18
443
+
444
+ The reranker now decides which document ranks first. The blend's retrieval-position
445
+ term could outweigh the entire reranker score, so the top retrieval hit was
446
+ unreachable; making position a 10% linear decay took recall@1 from .713 to .874 on
447
+ 223 name-like queries and from .467 to .867 on 30 paraphrase queries. The HTTP daemon
448
+ gains two operational knobs: `QMD_QUERY_LOG=1` records every search it serves to a
449
+ monthly JSONL, and `QMD_LLM_IDLE_TIMEOUT_MS=0` keeps models resident, which takes the
450
+ first search after an idle spell from ~1.8s to ~0.07s.
451
+
452
+ ### Changes
453
+
454
+ - `qmd query`'s reranker can now change which document comes first. The score
455
+ that orders results blended the reranker against a retrieval-position term of
456
+ `1 / rrfRank` under rank-gated weights, and that term falls 0.375 between rank
457
+ 1 and rank 2 — more than the 0.25 the entire reranker term could contribute.
458
+ The top retrieval hit was therefore unreachable: a document the reranker scored
459
+ 0.03 stayed ahead of one it scored 1.00. Position is now a linear decay across
460
+ the candidate window worth 10% of the blend, so it separates documents the
461
+ reranker scores alike instead of overruling it. Measured over a fixed candidate
462
+ dump per goldset, so every formula saw identical retrieval: recall@1 went .713
463
+ to .874 on 223 alias queries and .467 to .867 on 30 paraphrase queries. On both
464
+ goldsets the old blend's recall@1 equalled retrieval-only recall@1 to three
465
+ decimals, which is what an inert reranker looks like as a number.
466
+ - New bench tooling: `scripts/bench-vault-summarize.ts` writes a plain-language
467
+ summary per document with a local model, and `bench-vault-summary-apply.mjs`
468
+ builds the summary-augmented copy of a vault that the bench then indexes. This
469
+ measures whether the vocabulary gap the paraphrase goldset is made of closes at
470
+ index time. On 16 such queries, giving each document three everyday-language
471
+ lines took `search` (BM25, no LLM) from .250 to .562 recall@5 and the unreranked
472
+ hybrid path from .625 to .812. The reranked path did not move (.875 both ways,
473
+ the same two queries missed): it was already covering the gap, so the summaries
474
+ buy the cheap backends most of what the expensive one had. The BM25 number
475
+ carries no confound — that backend never calls query expansion. Summary lines
476
+ are filtered by document frequency, because a model that emits "이 도구는 뭘 하는
477
+ 건데?" would otherwise give every vague query a weak match on an unrelated
478
+ document.
479
+ - New bench tooling: `scripts/bench-dump-candidates.ts` records the RRF rank and
480
+ reranker score of every candidate for each goldset query, and
481
+ `scripts/bench-blend-sweep.mjs` re-scores that dump under different blend
482
+ formulas. Comparing two formulas used to mean two bench runs, which vary in
483
+ their retrieval and take about an hour each; it now costs one dump and a
484
+ millisecond per formula, and the variants are compared against the same
485
+ candidates.
486
+
487
+ - `bench-vault-goldset.mjs` adds two buckets that cost no model call: `alias`,
488
+ the display text of a `[[target|alias]]` wikilink, and `fmalias`, an entry of
489
+ the target's own `aliases:` frontmatter list. Both are human-written other
490
+ names for a document. First measurement on a 232-document vault (223 queries):
491
+ `full` scored 0.924 r@5 against `hybrid` 0.978 and `vector` 0.982 — of the 17
492
+ queries `full` missed, `hybrid` had 14 in its top five. The reranker demotes
493
+ short name-like queries that retrieval already found.
494
+ - `bench-vault-paraphrase.ts` writes its output after every document, so a run
495
+ killed from outside keeps what it had done.
496
+
497
+ - `QMD_LLM_IDLE_TIMEOUT_MS` sets how long loaded models stay idle before they
498
+ are unloaded (`0` = never), overriding the 5 minutes `createStore()` passes.
499
+ On a long-running HTTP daemon the first search after a quiet spell no longer
500
+ pays for reloading the models.
501
+ - `QMD_QUERY_LOG=1` makes the HTTP daemon append every `POST /query` /
502
+ `/search` search to `queries-YYYY-MM.jsonl` in the qmd cache directory: the
503
+ qmd version and commit, an index fingerprint, the searches, and each result's
504
+ path, score and rank — never snippets. Clients tag rows with `X-QMD-Tag`,
505
+ `X-QMD-Qid` and `X-QMD-Role`, and skip them with `X-QMD-No-Log: 1`. Writes
506
+ never fail a search; `qmd status` shows the newest log file and the MCP
507
+ `status` tool shows the daemon's flag and last write error.
508
+ - `scripts/dogfood.sh` builds the checkout, stops if bench-ko `bm25_r5` falls
509
+ below the newest BASELINE.md line, installs the packed tarball globally,
510
+ restarts the launchd daemon and smoke-tests it; `--restore` reinstalls the
511
+ published package.
512
+
513
+ - `develop` is the working branch; `main` only receives merges from it and is
514
+ where `/release` tags. `publish.yml` authenticates with the `NPM_TOKEN`
515
+ repository secret and skips a version that is already on the registry, so a
516
+ manually published version or a re-pushed tag still gets its GitHub release.
517
+ CI runs for `develop` pushes and pull requests too.
518
+
519
+ ### Fixes
520
+
521
+ - `bench-vault-report.mjs` writes its miss list next to the input, never on top
522
+ of it. The path came from `replace('bench.json', 'misses.json')`, a no-op for
523
+ any other file name, so reporting on a copied result overwrote that result.
524
+ - Query expansion drops sub-queries that have nothing to do with the query again.
525
+ The drift filter kept an expansion only if it shared a word with the query, but
526
+ it read the query as `[a-z0-9]` words, so a Korean query produced an empty word
527
+ list and the filter passed everything. Measured over 151 expansions of 30
528
+ Korean queries it rejected none, including 22 that were generic English
529
+ boilerplate ("Proper implementation follows established patterns and best
530
+ practices.") with no relation to the query — each one an embedding call and a
531
+ vector search spent on noise. A shared Hangul syllable bigram now counts as the
532
+ same kind of anchor: it drops those 22 and keeps 127 of the 129 Korean
533
+ expansions. A query with no anchor of either kind still keeps everything.
534
+ - Query expansion strips thinking tags from a sub-query before searching for it.
535
+ The grammar forbids newlines inside a line, so a `</think>` arrives welded to
536
+ the sub-query text rather than on a line of its own that parsing would skip; 20
537
+ of 46 hyde expansions in the same measurement carried one, and the tag was
538
+ embedded along with the text that mattered.
539
+ - Query expansion no longer runs the same sub-query twice on the same backend.
540
+ - `limit` no longer decides how wide a search looks, only how many results come
541
+ back. Both searches over-fetch by a multiple of `limit` — the FTS index by 10x
542
+ because the collection filter runs after the match, vectors by 3x or 30x
543
+ because chunks of one document collapse into one result — so a small `limit`
544
+ left a pool that post-filtering could exhaust, and `-n 5` was not the first five
545
+ of `-n 20`. The CLI made it visible: its default limit is 20 for `--json` and
546
+ `--files` and 5 otherwise, so the output format changed which documents were
547
+ found. Retrieval now looks at 20 documents at minimum whatever `limit` says,
548
+ which is the breadth `qmd query` already used internally.
549
+ - `bench-vault.sh` refuses to run when the vault it was given sits inside the
550
+ work directory it is about to `rm -rf`. The work directory is named after the
551
+ vault, so a vault under `tmp/real-bench/` resolved to the same path and the
552
+ corpus was deleted before it could be indexed, which showed up as a bench over
553
+ zero documents rather than as an error.
554
+ - `bench-vault-report.mjs` prints every backend that ran under each bucket, and
555
+ takes its miss list from the best backend rather than from BM25. The bucket
556
+ lines were BM25-only under a bare bucket name, so a run's `sem: 9/16` read as
557
+ the result of the pipeline the run was measuring.
558
+
559
+ - The Windows git-install probe failed on every push with `Permission denied
560
+ (publickey)`: npm and pnpm resolve any github.com dependency over SSH first
561
+ and do not retry an auth failure over https, the runner has no key, and the
562
+ repository is private so anonymous https fails as well. The job now rewrites
563
+ every github.com URL to https with the job token via git `insteadOf` before
564
+ installing.
565
+
566
+ ## [2.8.3-ko.2] - 2026-09-17
567
+
568
+ ### Changes
569
+
570
+ - Published to npm as `ko-qmd`. Install with `npm i -g ko-qmd`
571
+ after `npm uninstall -g @tobilu/qmd` (both ship a `qmd` bin). `publish.yml` is
572
+ back: pushing a `v*` tag runs the tests, publishes with provenance and creates
573
+ the GitHub release. The GitHub-tag install path still works for unpublished
574
+ commits.
575
+ - New bench tooling: `scripts/bench-vault-paraphrase.ts` builds a paraphrase
576
+ goldset from a vault by asking a local model, through ollama, for the question
577
+ someone would type when they do not know the vocabulary a document is written
578
+ in. The existing `bench-vault-goldset.mjs` harvests the document's own words,
579
+ which measures lexical matching; this measures the case that fails it. Every
580
+ candidate passes four mechanical gates and a separate verification call that
581
+ rejects fluent questions about things the document does not discuss. Defaults
582
+ to `qwen3:4b`; takes `--minutes` to stop on a time budget, `--seed` to sample
583
+ the vault when it does, and `--max-df` to tune which heading words a question
584
+ may not reuse.
585
+ - `GenerateOptions` no longer has a `model` field. `LlamaCpp.generate` never read
586
+ it: the model is fixed per instance from `generateModel` at construction, so a
587
+ caller passing `model` got the instance's model back with no error and no
588
+ indication the request was dropped. Construct a second `LlamaCpp` to generate
589
+ with a different model. Nothing in the package passed the field.
590
+
591
+ ### Fixes
592
+
593
+ - The release pre-push hook reads CI from the remote being pushed to. It asked
594
+ `gh api repos/{owner}/{repo}`, which resolves through gh's own remote
595
+ heuristic: with an `upstream` remote present that is the repo this one was
596
+ forked from, so every commit that exists only here answered 422. The hook
597
+ reported "no runs found" and let the tag through unverified. An API error now
598
+ aborts instead of reading as "CI has not started".
599
+
600
+ ## [2.8.3-ko.1] - 2026-09-16
601
+
602
+ ### Changes
603
+
604
+ - ko-vault bench fixture pins the package's default embedding model
605
+ (Qwen3-Embedding-0.6B-Q8_0) instead of embeddinggemma. Measured on the same
606
+ tree: `vector_r5` 0.9861 → 1.0000, `hybrid_r5` 0.9722 → 0.9861, `bm25_r5`
607
+ unchanged at 0.6528. Numbers and caveats in
608
+ `test/fixtures/ko-vault/BASELINE.md`.
609
+ - README.md states what this distribution is and links README.ko.md.
610
+ - Hangul lex queries also strip a verbal or nominalizing ending (`토큰화하는` →
611
+ `토큰화`, `검색하기` → `검색`) and unwind a two-particle chain (`청킹에서의` →
612
+ `청킹`). ko-vault bench `bm25_r5` 0.6635 → 0.7212 on a 52-query goldset, with
613
+ the original 36 queries unchanged at 0.6528.
614
+ - Hangul stems also cover a bare `한`/`된` ending (`필요한` → `필요`) and the
615
+ 하↔해 contraction (`더하` ↔ `더해`), so a query stem meets the contracted spelling
616
+ documents actually use. `bm25_r5` 0.7212 → 0.7404, hybrid and full 1.0000.
617
+ - Long Korean queries no longer return nothing. `searchFTS` retries the same
618
+ terms ORed when the strict AND matches no document at all (three terms
619
+ minimum). A natural question is a sentence, and requiring every word of it
620
+ present matched nothing: on a 30-query Korean paraphrase goldset lex recall@5
621
+ was 0.0000 and is now 0.5667. The synthetic ko-vault fixture moves with it,
622
+ `bm25_r5` 0.7404 → 0.9519.
623
+ - Hybrid RRF weights rebalanced for Korean: vector lists count half, relaxed
624
+ (ORed) lex lists count half again and never count as a strong signal, and
625
+ expansion-derived lists move 1.0 → 0.75 so the original query's vector list
626
+ still outranks a lex expansion. Hybrid recall@5 on 240 exact-phrase queries
627
+ 0.9167 → 0.9583, on the paraphrase goldset 0.8333 → 0.8667. Sweep and caveats
628
+ in `test/fixtures/ko-vault/BASELINE.md`.
629
+ - Hangul lex queries split script-mixed terms into runs and AND them
630
+ (`SKILL.md계약의핵심` → `skill` AND `md` AND `계약의핵심`). Before this the glued
631
+ token was one phrase that matches nothing, and the hyphen/dot branches kept the
632
+ Hangul run glued too. Measured on a real 232-document Korean vault with a
633
+ 240-query goldset: `bm25_r5` 0.9042 → 0.9333, `bm25_mrr` 0.8666 → 0.8937.
634
+ - `scripts/bench-vault.sh` and `scripts/bench-vault-goldset.mjs` run the Korean
635
+ bench over any real vault, building the goldset from that vault's own headings
636
+ and body clauses. Nothing from the vault is copied into the repo.
637
+ - ko-vault goldset grew 36 → 52 with particle chains, verb endings and joined
638
+ compounds.
639
+ - `qmd bench` no longer needs `expected_in_top_k` on every fixture entry. It was
640
+ required in practice: omitting it made the result limit NaN and every backend
641
+ returned zero results with no error.
642
+
643
+ ## [2.8.3-ko.0] - 2026-09-16
644
+
645
+ ### Fixes
646
+
647
+ - pnpm consumers install this package from a git tag, where pnpm runs `prepare`
648
+ (tsc only). Two things broke that on Windows: a stale `pnpm-lock.yaml`
649
+ (`ERR_PNPM_OUTDATED_LOCKFILE`) and `pnpm.onlyBuiltDependencies`, which made
650
+ pnpm build better-sqlite3 from source in an environment with no Visual Studio.
651
+ The lockfile is regenerated and the built-dependency list is now empty — the
652
+ `dist` build needs no native module, and consumers build their own. Run
653
+ `pnpm approve-builds` when developing this repo with pnpm. npm installs were
654
+ unaffected.
655
+
656
+ ### Changes
657
+
658
+ - ko-qmd distribution: package renamed to `ko-qmd`
659
+ (`2.8.3-ko.0`), consumed from git tags instead of npm (`publish.yml`
660
+ removed). CI adds a `windows-latest` Node 22 job and an Electron 39 smoke job
661
+ that imports the SDK with dynamic `import()` and runs `searchLex`. See
662
+ `README.ko.md`.
663
+ - Hangul lex queries: an unquoted Korean word also matches its stem with one
664
+ trailing particle or `기` stripped (`검색을` → `("검 색" OR "검 색 을")`,
665
+ stem ≥2 syllables). Quoted phrases and Han/kana terms are unchanged. ko-vault
666
+ bench `bm25_r5` 0.6250 → 0.6528.
667
+ - Hangul syllable bigrams in the FTS index: each indexed field carries the
668
+ bigrams of its Hangul runs after the character tokens, and a plain Korean word
669
+ queries bigram OR character phrases of the word and its stem. Character-phrase
670
+ and quoted-phrase matching is unchanged. `FTS_CJK_NORMALIZED_VERSION` is now
671
+ `"2"`, so existing indexes rebuild FTS once on open. ko-vault bench
672
+ `bm25_r5` 0.6528 → 0.6528 (no measured gain on the fixture).
673
+ - Korean defaults: the default embedding model is now
674
+ `hf:Qwen/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf`
675
+ (1024 dimensions; upstream default is embeddinggemma-300M). Reranker and
676
+ query-expansion defaults are unchanged. Existing indexes need `qmd embed -f`;
677
+ set `models: embed:` or `QMD_EMBED_MODEL` to keep embeddinggemma. The qmd skill
678
+ gains a "Korean queries" section. Hangul handling and ko-vault bench numbers
679
+ are in `README.ko.md`.
680
+
681
+ ## [2.8.3] - 2026-08-16
682
+
683
+ ### Security
684
+
685
+ - `qmd update` no longer runs a project-local `.qmd/index.yml`'s `update:`
686
+ commands without approval (#886). That file arrives with a `git clone` and is
687
+ adopted automatically for any command run inside the tree, so cloning a
688
+ repository and running `qmd update` executed shell commands chosen by whoever
689
+ wrote it. On a terminal QMD now lists the commands and asks; with nobody to
690
+ ask it skips them and keeps indexing. Approvals are recorded per config file
691
+ and per command set in `<config dir>/trusted.json`, so editing a command — or
692
+ a `git pull` that rewrites one — asks again. New `qmd trust`,
693
+ `qmd trust list` and `qmd trust revoke` manage approvals, and
694
+ `QMD_TRUST_UPDATE_HOOKS=1` opts unattended runs back in. Commands in your own
695
+ `~/.config/qmd/*.yml`, including anything `qmd collection update-cmd` writes,
696
+ are unaffected.
697
+
698
+ - The same project-local trust gate now covers collection `path` values that
699
+ resolve outside the project and non-default `models.embed` / `models.rerank`
700
+ / `models.generate` URIs (#889). In-project paths still index unattended;
701
+ out-of-project directories are skipped until `qmd trust`, and custom model
702
+ URIs are not loaded or downloaded. `QMD_TRUST_LOCAL_CONFIG=1` opts unattended
703
+ runs back in (and `QMD_TRUST_UPDATE_HOOKS=1` still does). `qmd collection
704
+ add` records trust as it writes, the same way `update-cmd` does.
705
+
706
+ - Indexing no longer follows file symlinks or glob `../` / absolute patterns
707
+ out of the collection directory. `fast-glob` already skipped symlinked
708
+ directories, but a file symlink (or a mask like `../**/*.md`) still resolved
709
+ via `realpath` and ingested the target. `qmd://` filesystem resolution uses
710
+ the same containment check, so `qmd://collection/../../../etc/passwd` no
711
+ longer produces a path outside the collection.
712
+
713
+ - `qmd mcp --http` now validates the `Origin` and `Host` headers on every
714
+ request and answers `403` when they name anything but a loopback address
715
+ (#881). Binding to localhost is no defence against the user's own browser:
716
+ a page can re-point its hostname at `127.0.0.1` (DNS rebinding) and read
717
+ the indexed corpus through `POST /query` or `POST /mcp`. Requests with no
718
+ `Origin` (curl, MCP clients, editors) are unaffected. Extend the allowlists
719
+ with `QMD_ALLOWED_ORIGINS` / `QMD_ALLOWED_HOSTS`, or set
720
+ `QMD_ALLOWED_ORIGINS=*` behind your own authenticating proxy. A wildcard
721
+ bind (`--host 0.0.0.0`) skips the host check and warns at startup.
722
+
723
+ ### Changed
724
+
725
+ - Dependencies: `node-llama-cpp` 3.18.1 → **3.20.0** (llama.cpp b8390 → b10361, 2026-08-11). Also safe patch/minors: `picomatch` 4.0.4 → 4.0.5, `web-tree-sitter` 0.26.8 → 0.26.12, `tsx` 4.21.0 → 4.23.12, `vitest` 3.2.4 → 3.2.7. No zod/vitest major; `@modelcontextprotocol/server` stays 2.0.0 (no 2.x patch). `flake.nix` FOD hashes are not updated here.
726
+ - `generate` and query expansion now await `LlamaContextSequence.dispose()` before disposing the parent context. node-llama-cpp 3.20 made sequence dispose async; the library's context-onDispose path does not wait.
727
+
728
+ - MCP server now speaks protocol revision **2026-07-28** via the official
729
+ TypeScript SDK 2.x (`@modelcontextprotocol/server`). HTTP is sessionless
730
+ (no `Mcp-Session-Id`, no initialize handshake, no idle-session TTL / #816
731
+ reaper). Clients send version and capabilities in `_meta`; `server/discover`
732
+ is implemented; Streamable HTTP POST requires `Mcp-Method` / `Mcp-Name`
733
+ (mismatch → `-32020`); `tools/list` is deterministic and carries `ttlMs` /
734
+ `cacheScope`. 2025-era stdio clients still work (`serveStdio` dual-speak);
735
+ 2025-era HTTP `initialize` is answered per-request without minting a
736
+ session. Existing tools (`query` / `get` / `multi_get` / `status`), stdio
737
+ EOF shutdown, and named-index daemon PIDs are unchanged. No release.
738
+
739
+ - `qmd pull` (and implicit model downloads in `embed`/`query`) no longer print
740
+ node-llama-cpp's download progress bar. The bar redraws every few kilobytes
741
+ and flooded agent transcripts with thousands of tokens (#776). Pass
742
+ `qmd pull --progress` to show it on an interactive terminal.
743
+ - `--full-path` no longer degrades silently when a result cannot be resolved on
744
+ disk (#785). A fallback there means the file moved or was deleted since the
745
+ last index, so `search`, `query`, `get` and `multi-get` now print a notice to
746
+ stderr naming how many results fell back and suggesting `qmd update`; stdout
747
+ stays machine-readable.
748
+ - `search`/`query` now decide per result whether to show the docid under
749
+ `--full-path`, matching `multi-get` and `get`: a result that resolved shows
750
+ its on-disk path and no docid, one that did not keeps its `qmd://` URI *and*
751
+ its docid, so it is still addressable. Previously the docid was dropped for
752
+ every row whenever the flag was set, leaving unresolved rows with neither a
753
+ usable path nor an identifier.
754
+ - `search --format csv` always emits the `docid` column, empty for rows that
755
+ resolved to an on-disk path. Under `--full-path` the header previously
756
+ dropped the column entirely — which also disagreed with the empty-result
757
+ header, always printed with `docid`. Column positions are now stable across
758
+ runs and formats.
759
+
760
+ ### Fixed
761
+
762
+ - Concurrent first-open of a cold index no longer fails with
763
+ `table documents_fts already exists` on Bun/macOS. FTS5
764
+ `CREATE VIRTUAL TABLE IF NOT EXISTS` is not atomic across WAL
765
+ connections: two processes can both see a missing table on their
766
+ schema snapshot and the loser throws. Table create and legacy-schema
767
+ repair now use the same `BEGIN IMMEDIATE` + double-check as the FTS
768
+ sync triggers, and treat a concurrent "already exists" as success
769
+ when the table is present.
770
+
771
+ - Nix flake `qmd-node-modules` FOD hashes updated for x86_64-linux and
772
+ aarch64-darwin after the MCP SDK 2.0 bump. `nix build` / Nix GHA was
773
+ failing with a fixed-output hash mismatch.
774
+
775
+ - CJK FTS rebuild no longer skips leftover `fts5(name, body, content='documents')`
776
+ tables when `fts_cjk_normalized_version` is already stamped, and schema
777
+ repair now checks live FTS columns (`PRAGMA table_info`) as well as
778
+ `sqlite_master.sql`. The MCP HTTP test helper still seeds that legacy
779
+ table; `startMcpHttpServer` / `createStore` on it must not throw
780
+ `no such column: T.name` (#792 regression).
781
+
782
+ - `qmd collection add --glob` is no longer silently ignored. parseArgs ran
783
+ with `strict: false`, so OpenClaw's `--glob memory.md` (and any other
784
+ `--glob`) fell through, the default `**/*.md` was used, and a second
785
+ collection on the same path collided as a duplicate instead of indexing
786
+ the requested mask (#536). `--glob` is now an alias for `--mask`.
787
+
788
+ - The `bin/qmd` trampoline now execs `process.execPath` instead of
789
+ re-resolving `node` from PATH. Native addons (`better-sqlite3`) are
790
+ compiled for the Node that installed qmd; a version manager (nvm, fnm,
791
+ mise) selecting a different major in the working directory used to spawn
792
+ that other binary and fail with `NODE_MODULE_VERSION` / `ERR_DLOPEN_FAILED`
793
+ (#577 leftover; #319). `bun bin/qmd` still resolves `node` from PATH so
794
+ Node-ABI addons are not loaded into bun.
795
+
796
+ - `qmd update` / `qmd collection add` no longer swallow unreadable files
797
+ silently (#460). `readFileSync` failures (ETIMEDOUT on APFS compressed
798
+ files, EAGAIN, EACCES, …) still skip the file so the rest of the collection
799
+ indexes, but the CLI now warns with the path and error code and reports
800
+ the skip count. The SDK `update()` result includes `skipped`.
801
+
802
+ - The architecture diagram no longer draws Vec expansions into BM25 search
803
+ (#680). `lex` expansions are FTS-only; `vec` and `hyde` expansions are
804
+ vector-only. The original query still goes to both backends.
805
+
806
+ - `qmd bench` no longer runs to a wall of 0.00 when the fixture collection is
807
+ missing or empty (#716). It errors up front with the same "Collection not
808
+ found" / index hint as `qmd search -c`, and if every backend still scores
809
+ zero it warns on stderr to check `qmd ls`.
810
+
811
+ - `qmd cleanup` now reclaims the content and FTS space left behind after a
812
+ wrong-directory `qmd update`. Deactivating files (the next update in the
813
+ right directory) only tombstoned the `documents` rows; cleanup deleted those
814
+ rows and vacuumed, but never dropped the unreferenced `content` hashes and
815
+ never ran FTS5 `optimize`, so `documents_fts_data` kept the old bodies
816
+ (#550). Cleanup now deletes inactive docs, then orphaned content, then
817
+ compact FTS, then vacuum. `--dry-run` reports the content hashes too.
818
+
819
+ - Concurrent `query` calls with `rerank: true` on a cold MCP server no longer
820
+ race `ensureRerankContexts()`. Embed already serialized context creation;
821
+ rerank did not, so two overlapping first queries both saw an empty pool,
822
+ both created ranking contexts, and the inactivity timer disposed the loser
823
+ (`Object is disposed`, #682). Callers now await the in-flight create.
824
+
825
+ - Embedding-context pool size no longer assumes every GGUF costs 150 MB of
826
+ VRAM (the nomic-embed figure). Larger models such as Qwen3-Embedding-0.6B
827
+ are ~1190 MB per 2048-token context; opening 8 of those exhausted an 8 GB
828
+ card so `qmd query` failed with `Failed to create any rerank context` even
829
+ though the reranker itself was fine. The pool is now sized from the weight
830
+ file, and 1 GB is reserved for the reranker (#799). Default
831
+ embeddinggemma/nomic throughput is unchanged. `QMD_EMBED_PARALLELISM` still
832
+ overrides.
833
+
834
+ - Multi-collection `-c A -c B` (and SDK/MCP `collections: [A, B]`) no longer
835
+ searches globally then post-filters. A large unrelated collection could fill
836
+ the FTS/ANN top-k so the requested collections vanished, yielding false-empty
837
+ results even though each collection matched on its own. `searchFTS` /
838
+ `searchVec` now search each requested collection, then merge by score
839
+ (#775). Single-collection exact-scan (#791, #803) is unchanged.
840
+
841
+ - Query expansion no longer consumes caller `intent`, and a cached expansion
842
+ whose sub-queries all miss is dropped instead of replaying forever (#818).
843
+ Intent still steers reranking and snippet/chunk selection; it just no longer
844
+ enters the expansion prompt or cache key, where the model copied meta-language
845
+ ("so I can compare spend settings") into lex/vec terms that matched nothing.
846
+
847
+ - Files whose names differ only in the characters the legacy slug collapsed to
848
+ `-` (spaces, underscores) no longer evict each other from the index (#717).
849
+ The handalized-path migration now skips any row whose path is still owned by
850
+ a file in the current scan, so it only adopts genuinely stale pre-2.6 rows.
851
+ `qmd get` and `qmd ls <prefix>` also match `_` and `%` in paths literally
852
+ instead of as SQL `LIKE` wildcards, so `qmd get 2026_06_16.md` no longer
853
+ returns a sibling `2026-06-16.md`.
854
+
855
+ - CLI `multi-get` and SDK/MCP `multi_get` now share one comma-list resolver.
856
+ Collection-prefixed paths (`qmd/docs/SYNTAX.md`) work in both transports,
857
+ unanchored `LIKE '%name'` no longer silently fetches a different document
858
+ for a filename fragment (`NTAX.md` ≠ `SYNTAX.md`), and ambiguous names
859
+ across collections error with the candidate list instead of `LIMIT 1` (#759).
860
+
861
+ - The Nix flake wrapper now seeds the same pre-import env as `bin/qmd`.
862
+ Nix installs exec `bun src/cli/qmd.ts` directly, so they previously skipped
863
+ the launcher: `qmd mcp` could leak llama/ggml native logs onto JSON-RPC
864
+ stdio, and Darwin CLI exits dumped a ggml Metal residency-set stack trace
865
+ after an otherwise successful query. The wrapper now quiets those logs for
866
+ `mcp` and sets `GGML_METAL_NO_RESIDENCY=1` on Darwin unless
867
+ `QMD_METAL_KEEP_RESIDENCY=1` (#723).
868
+
869
+ - Rerank context creation no longer swallows the real failure. A VRAM OOM or
870
+ corrupt model previously produced only `Reranker unavailable — skipping
871
+ reranking` (and a dead identical retry whose comment claimed it disabled
872
+ flash attention, which ranking contexts never supported). The warning now
873
+ includes the underlying message so the two cases are distinguishable (#782).
874
+
875
+ - The Nix flake package now ships `skills/` next to `src/` in `$out/lib/qmd/`.
876
+ `findPackageRoot()` walks up from the wrapped `src/cli/qmd.ts` looking for a
877
+ sibling `skills/` directory; without it, `qmd skill show` and `qmd skills list`
878
+ always failed with "QMD skill not found" on Nix-installed binaries (#722).
879
+
880
+ - `/release` step 1 no longer points at a missing script. `skills/release/scripts/release-context.sh`
881
+ now exists: it silently installs git hooks and prints version info, working-tree
882
+ status, commits and files since the last tag, `[Unreleased]`, and the previous
883
+ changelog entry. The skill's process list also drops the duplicate step 7 and
884
+ checks dependency updates before cutting the release (#796).
885
+
886
+ - `store.searchVec()` (and SDK `searchVector()`) now embed the query with the
887
+ store's pinned embed model instead of the global `QMD_EMBED_MODEL`. A store
888
+ created with a non-default `models.embed` previously failed with
889
+ `Dimension mismatch ... Expected N ... received M` and loaded the wrong
890
+ (often much larger) model at query time. Hybrid/precomputed/session search
891
+ paths were unaffected and stay unchanged (#690).
892
+
893
+ - `qmd collection add --mask "a.md,*.txt"` now indexes the union of each
894
+ pattern. The comma-separated form was documented and commonly guessed, but
895
+ the joined string was passed to fast-glob as one literal glob, so it
896
+ matched zero files with no error. Brace form `{a.md,*.txt}` is unchanged.
897
+ The same split applies on `qmd update` for stored comma-list masks (#557).
898
+
899
+ - NixOS / immutable-root installs no longer crash `qmd embed` with EACCES
900
+ when node-llama-cpp tries to compile llama.cpp into a read-only
901
+ `node_modules`. The flake wrapper puts Nix's glibc and libstdc++ on
902
+ `LD_LIBRARY_PATH` so prebuilt binaries can `dlopen` them, and
903
+ `getLlama()` uses `build: "never"` when the llama directory is not
904
+ writable (#574).
905
+
906
+ - CJK FTS rebuild no longer raises "database is busy" when flushing insert
907
+ batches. The streaming `.iterate()` cursor stayed open across
908
+ `BEGIN` on the same connection; the scan now uses keyset-paginated
909
+ `LIMIT` batches so each SELECT finalizes before the insert transaction
910
+ starts (#797).
911
+
912
+ - Quoted FTS phrases containing dotted tokens (e.g. `"1.0.21"`) now match the
913
+ indexed document. The porter unicode61 tokenizer stores dotted strings as
914
+ adjacent parts, but phrase sanitization stripped the dots into a single
915
+ token (`1021`) that could never hit. Dotted tokens inside quotes are split
916
+ into adjacent phrase terms, matching the bare-term rewrite from #563 (#757).
917
+
918
+ - `scripts/build.mjs` no longer passes `shell: true` to `spawnSync` on Windows.
919
+ With the default Node install path (`C:\Program Files\nodejs\node.exe`),
920
+ `cmd.exe` split the unquoted `process.execPath` at the space, the `tsc`
921
+ spawn failed, and `prepare` could still report success with no `dist/` —
922
+ leaving `bin/qmd` at "not built". The helper always receives a real binary
923
+ path plus an args array, so no shell is needed. A spawn error now prints
924
+ the missing binary path instead of failing silently. (#681)
925
+
926
+ - Case-sensitive collections no longer collapse distinct document identities that
927
+ differ only by path casing. The implicit `COLLATE NOCASE` legacy migration was
928
+ unsafe for filesystems that contain both `README.md` and `readme.md`; case-only
929
+ legacy migrations must now be explicit and operator-reviewed (#801).
930
+
931
+ - `cleanupOrphanedVectors` now runs its orphan count and both DELETEs in a
932
+ single immediate transaction. An interruption between the two DELETEs
933
+ (crash, `SQLITE_BUSY`) could desync `vectors_vec` from `content_vectors`,
934
+ leaving stale metadata rows that make a later reactivation of the same
935
+ content hash look already-embedded — so `qmd embed` skips it and the
936
+ document becomes silently unsearchable by vector, with no orphan left to
937
+ clean up (#766).
938
+
939
+ - `qmd embed` no longer splits a UTF-16 surrogate pair (emoji, etc.) across a
940
+ chunk boundary. A chunk ending or starting mid-pair produced an unpaired
941
+ surrogate in the chunk text, which some remote embedding APIs reject as
942
+ invalid JSON — permanently failing that chunk on every retry, since the
943
+ boundary calculation is deterministic. This covers both the character-based
944
+ chunker and `chunkDocumentByTokens`'s recursive re-splitting for
945
+ astral-plane-dense content, which could previously drive the char budget
946
+ low enough to reproduce the same split (#777).
947
+
948
+ - `insertContext` looks up `store_collections` by name. #754 retargeted the
949
+ query from the dropped `collections` table but left `WHERE id = ?`, and
950
+ `store_collections` has `name TEXT PRIMARY KEY` with no `id` column — the
951
+ call threw `no such column: id`. Matches `deleteContext` /
952
+ `updateStoreContext` (#853).
953
+
954
+ - `qmd collection add` with no path argument now errors with usage instead of
955
+ silently indexing the current working directory (#684). Pass `.` to index
956
+ CWD, matching the documented examples.
957
+
958
+ - `qmd status` reports orphaned embedding chunks, `qmd update` hints when they
959
+ exceed 10% of vectors, and `qmd cleanup --dry-run` previews what would be
960
+ removed. Incremental update still does not auto-prune vectors (a transient
961
+ empty mount would otherwise force a full re-embed) (#768).
962
+
963
+ - `qmd --index <name> mcp --http --daemon` now scopes PID/log files per index
964
+ (`mcp-<name>.pid`) and passes the resolved database path to the child, so a
965
+ named-index daemon no longer collides with the default `mcp.pid` or opens
966
+ the default store (#772).
967
+
968
+ - Opening a store no longer throws `SQLiteError: no such column: T.name` when
969
+ `documents_fts` is still the legacy `fts5(name, body, content='documents')`
970
+ schema. `CREATE VIRTUAL TABLE IF NOT EXISTS` left that table in place, and
971
+ the CJK FTS rebuild's `DELETE FROM documents_fts` compiled against
972
+ `documents.name`, which does not exist (#792).
973
+
974
+ - Rerank cache keys now include the resolved `models.rerank` URI, so swapping
975
+ the configured reranker no longer serves the previous model's cached scores
976
+ (#764).
977
+
978
+ - `vsearch -c <collection>` no longer returns empty results for small
979
+ collections crowded out of the global ANN candidate pool. `searchVec` now
980
+ exact-scans the collection's vectors with `vec_distance_cosine` when the
981
+ set is within 20k rows (ANN + post-filter cannot see collections that never
982
+ enter global top-k, and sqlite-vec caps `k` at 4096 so a larger multiplier
983
+ alone is not enough). Larger collections still use capped ANN over-fetch
984
+ (#791, #803).
985
+
986
+ - `multi-get --format files` now emits the docid as its own CSV field
987
+ (`#docid,path,...`) instead of prepending it into the path field with a
988
+ space (`#docid path,...`), matching `search --format files` and keeping
989
+ naive comma-splitting usable (#760).
990
+
991
+ - `qmd embed` now takes an exclusive process lock (`.qmd-embed.lock` next to
992
+ the index DB) so concurrent invocations no longer race on `vectors_vec`
993
+ and fail with `UNIQUE constraint failed: vectors_vec.hash_seq`. A second
994
+ embed exits early with `Another embed process is already running. Skipping.`
995
+ Stale locks from crashed processes are recovered via PID identity checks
996
+ (#825).
997
+
998
+ - windows prepare fix #778 keeps dist build #824
999
+
1000
+ - `qmd mcp` (stdio) now shuts down gracefully when stdin reaches EOF instead
1001
+ of orphaning to PID 1 when the parent MCP client dies (#751): the server
1002
+ closes its transport, gives in-flight request handlers a bounded window to
1003
+ settle, closes the store (which disposes its llama.cpp instance), and lets
1004
+ the process drain via `process.exitCode` (no forced `process.exit()`, which
1005
+ has caused exit-time native crashes before).
1006
+
1007
+ - `qmd --version` no longer reports an unrelated repository's commit (#787).
1008
+ The commit was discovered at runtime with `git -C <installDir> rev-parse`,
1009
+ and `git -C` walks *up*, so any install nested inside another checkout
1010
+ reported that checkout's HEAD — a global npm install under a git-managed
1011
+ prefix such as Homebrew's `/opt/homebrew` claimed Homebrew's commit as
1012
+ qmd's. Identical tarballs reported different "commits" depending only on
1013
+ where they were installed, which is why one issue can collect three distinct
1014
+ hashes for the same published build. `scripts/build.mjs` now stamps the
1015
+ commit it built from into `dist/cli/build-info.json` (suffixed `-dirty` when
1016
+ built from a modified tree), and the runtime prefers that. Source checkouts
1017
+ still resolve their own HEAD, but only after confirming the enclosing
1018
+ repository is qmd's; anything else reports no commit rather than a
1019
+ misleading one. The lookup also no longer interpolates the install path into
1020
+ a shell string, so a path containing a space stops silently dropping the
1021
+ commit, and `--version` from a build runs no subprocess at all.
1022
+
1023
+ - `qmd doctor` no longer false-positives `.etag` HTTP sidecars (written by
1024
+ `qmd pull` next to each download) as invalid GGUF models. The model-cache
1025
+ check now only inspects real `.gguf` files, so a sidecar that happens to
1026
+ sort before the blob no longer poisons the report. #812
1027
+
1028
+ - `qmd mcp stop` and `qmd mcp --http --daemon` now verify that a pidfile PID still belongs to a qmd process before signalling it or refusing to start. Recycled PIDs (common after reboot) are treated as stale: the pidfile is unlinked instead of SIGTERM'ing an unrelated process or blocking daemon start with a false "Already running" error (#806).
1029
+
1030
+ - `qmd collection add` now rejects missing paths and regular files before
1031
+ creating collection configuration or index state. The error reports both the
1032
+ received and resolved path so malformed shell arguments can be corrected.
1033
+
1034
+ - Claude Code plugin: scope the plugin `source` to `./skills` so installs
1035
+ copy just the skills (~50 KB) instead of the entire repository. Previously a
1036
+ canonical install materialized ~230 MB / 9,000+ items into
1037
+ `~/.claude/plugins/cache/` — including a full npm dependency install
1038
+ triggered by the repo-root `package.json`. Both skills (`qmd` and `release`)
1039
+ still ship, unchanged. (#790)
1040
+
1041
+ - Claude Code plugin: releases now bump the plugin version in
1042
+ `.claude-plugin/marketplace.json` in lockstep with `package.json`. The
1043
+ plugin cache is keyed on this version, and it had been stuck at `0.1.0`
1044
+ since February — so installed plugins never received skill updates
1045
+ (users who installed in February are still being served that snapshot
1046
+ today, despite the qmd skill nearly tripling in size since). Also bumps
1047
+ the plugin to `2.6.3` as a one-time catch-up so existing installs pick
1048
+ up the current skill on their next `claude plugin update`. (#789)
1049
+
1050
+ ### Changed
1051
+
1052
+ - `--full-path` no longer degrades silently when a result cannot be resolved on
1053
+ disk (#785). A fallback there means the file moved or was deleted since the
1054
+ last index, so `search`, `query`, `get` and `multi-get` now print a notice to
1055
+ stderr naming how many results fell back and suggesting `qmd update`; stdout
1056
+ stays machine-readable.
1057
+ - `search`/`query` now decide per result whether to show the docid under
1058
+ `--full-path`, matching `multi-get` and `get`: a result that resolved shows
1059
+ its on-disk path and no docid, one that did not keeps its `qmd://` URI *and*
1060
+ its docid, so it is still addressable. Previously the docid was dropped for
1061
+ every row whenever the flag was set, leaving unresolved rows with neither a
1062
+ usable path nor an identifier.
1063
+ - `search --format csv` always emits the `docid` column, empty for rows that
1064
+ resolved to an on-disk path. Under `--full-path` the header previously
1065
+ dropped the column entirely — which also disagreed with the empty-result
1066
+ header, always printed with `docid`. Column positions are now stable across
1067
+ runs and formats.
1068
+
1069
+ ## [2.6.3] - 2026-06-24
1070
+
1071
+ ### Added
1072
+
1073
+ - `qmd embed --timeout <minutes>` overrides the embed session's max duration
1074
+ (previously hardcoded to 30 minutes). Use a larger value to let a big index
1075
+ finish in one run, or `--timeout 0` to remove the cap entirely. When the cap is
1076
+ reached, remaining document batches are skipped as before, so re-running
1077
+ `qmd embed` continues where it left off.
1078
+
1079
+ ### Documentation
1080
+
1081
+ - README: added a "Configuring `index.yml`" section documenting the full config
1082
+ schema — `global_context`, `editor_uri`, the `models.embed`/`rerank`/`generate`
1083
+ overrides, and per-collection `path`/`pattern`/`ignore`/`update`/
1084
+ `includeByDefault`/`context` — with file-location rules (`XDG_CONFIG_HOME`,
1085
+ `QMD_CONFIG_DIR`, named `{name}.yml`, project-local `.qmd/index.yml`). Every key
1086
+ is verified against `src/collections.ts` and its consumers. Documents behavior
1087
+ that previously existed only in code, where the absence of docs led contributors
1088
+ to repeatedly re-submit already-shipped model-resolution fixes (#502, #559, #564)
1089
+ and to request config that already works (#645, #678). Added
1090
+ `XDG_CONFIG_HOME`/`QMD_CONFIG_DIR` to the environment-variable table and noted
1091
+ the `index.yml` `models:` / `QMD_EMBED_MODEL` override path in the Model
1092
+ Configuration section.
1093
+ - README: expanded the per-collection `update` field into an "Automatic update
1094
+ commands" subsection — the feature the maintainer publicly called under-documented
1095
+ — covering execution via `bash -c` in the collection's directory, the run-then-
1096
+ reindex order, the non-zero-exit abort behavior, and the `qmd collection
1097
+ update-cmd` set/clear shortcut. The `ignore` key now states it is YAML-only (no
1098
+ CLI command) and additive with the un-overridable built-in exclusions.
1099
+ - `example-index.yml`: overhauled from three near-identical collections into a
1100
+ fully-commented starter template where each collection demonstrates a distinct
1101
+ feature (hierarchical context, auto-`update`, `ignore` patterns, non-markdown
1102
+ globs, `includeByDefault: false`, and an all-fields example), plus commented
1103
+ `editor_uri`/`models` stubs. README now links to it. Model URIs are intentionally
1104
+ left as placeholders so the template can't drift from the defaults.
1105
+ - README: documented collection filtering (`-c` semantics), the `collection
1106
+ show`/`include`/`exclude`/`update-cmd` subcommands, the `--intent`/`--no-rerank`/
1107
+ `-C`/`--full-path` search flags, the `--format <kind>` output selector (with the
1108
+ legacy `--json`/`--csv`/`--md`/`--xml`/`--files` booleans noted as aliases),
1109
+ `vector-search`/`deep-search` aliases, embed
1110
+ memory flags (`--max-docs-per-batch`/`--max-batch-mb`), a sample `--explain`
1111
+ score trace, the `qmd doctor`/`qmd init` commands, the `get` `:from:count`
1112
+ suffix and `--no-line-numbers`, an MCP tool parameter reference, and a
1113
+ Benchmarking section for `qmd bench`.
1114
+ - docs/SYNTAX.md: removed the non-existent `q` MCP parameter example (the `query`
1115
+ tool and REST endpoint accept only the `searches` array) and added a Scoping
1116
+ section.
1117
+ - README: removed the misleading `qmd update --pull` example. The `--pull` flag is
1118
+ parsed but never consumed (`updateCollections()` ignores it); the real mechanism
1119
+ for running `git pull` before re-indexing is a per-collection `update` command,
1120
+ set via `qmd collection update-cmd`.
1121
+
1122
+ ### Fixed
1123
+
1124
+ - MCP server instructions now tell agents to scope with the plural `collections`
1125
+ parameter (matching the schema). The previous singular `collection` hint led
1126
+ agents to pass a parameter that Zod silently strips, producing unscoped results.
1127
+ The `get` instruction line also now documents the full `file.md:from:count`
1128
+ range suffix instead of only the single-line `file.md:100` offset.
1129
+
1130
+ - Filesystem paths with special characters (`#`, `&`, spaces, `[]`, `()`, etc.)
1131
+ now round-trip correctly through index → search → get. Previously
1132
+ `reindexCollection` called `handelize()` on relative paths before storing
1133
+ them, turning `# Meeting - 234232 3432 __ 5.md` into
1134
+ `Meeting-234232-3432-5.md` and making `qmd get <actual-path>`,
1135
+ `qmd get --full-path`, and `qmd ls` return dead or garbled paths. Paths are
1136
+ now stored verbatim. Existing indexes auto-migrate on the next `qmd update`.
1137
+
1138
+ - FTS5 search now correctly matches dotted version strings like `2026.4.10`. The
1139
+ `porter unicode61` tokenizer splits on dots (storing `2026`, `4`, `10` as
1140
+ separate tokens), but the query sanitizer was stripping dots and producing
1141
+ `2026410` which never matched. Dotted terms are now split and ANDed together
1142
+ so version-string searches work as expected (#563).
1143
+ - HTTP REST endpoints `/query` and `/search` now return `qmd://collection/path`
1144
+ URIs in the `file` field, matching the output format used by the CLI and MCP
1145
+ resource URIs. Previously the raw `displayPath` (`collection/path`) was
1146
+ returned without the scheme prefix (#576).
1147
+ - The embed session `maxDuration` is now env-configurable via
1148
+ `QMD_EMBED_MAX_DURATION_MS` (default: 30 min). This prevents large-corpus
1149
+ embeddings from being aborted by the hardcoded 30-minute ceiling (#673).
1150
+ - `qmd query`, `qmd update`, and other commands no longer fail with
1151
+ `SQLiteError: database is locked` when multiple processes run against the
1152
+ same index in parallel (e.g. an `update` racing a long `embed`, the
1153
+ first-open schema migration racing a routine command, or an agent fanning
1154
+ out searches). `openDatabase` now sets `PRAGMA busy_timeout = 120000` on
1155
+ every connection, so a writer that loses the race queues at batch
1156
+ boundaries instead of throwing immediately. WAL handles read/write
1157
+ concurrency but does not serialise concurrent writers, and `bun:sqlite`
1158
+ and `better-sqlite3` both default the timeout to 0, so the loser
1159
+ previously failed on the first DDL statement in `initializeDatabase`.
1160
+ Override the default with `QMD_SQLITE_BUSY_TIMEOUT` (milliseconds; `0`
1161
+ restores fail-fast). Two more crashes on the same concurrent-open
1162
+ path are fixed: `trigger documents_ai already exists` (the FTS sync
1163
+ triggers were dropped and recreated as separate statements on every
1164
+ open, so two processes interleaved between the `DROP` and the
1165
+ `CREATE`; `busy_timeout` serialises individual statements but not the
1166
+ pair) and `database is locked` while migrating a cold database to WAL
1167
+ (the `journal_mode` switch needs a brief exclusive lock and does not
1168
+ invoke the busy handler). FTS trigger setup is now gated behind
1169
+ `PRAGMA user_version` inside one `IMMEDIATE` transaction, and the WAL
1170
+ migration retries within the busy-timeout budget.
1171
+
1172
+ ## [2.5.3] - 2026-05-28
1173
+
1174
+ ### Features
1175
+
1176
+ - `qmd get` now accepts a `:from:count` suffix on a path or docid (e.g.
1177
+ `qmd get "#abc123:120:40"` reads 40 lines starting at line 120). Explicit
1178
+ `--from`/`-l` flags still override the suffix. The MCP `get` tool accepts the
1179
+ same suffix.
1180
+ - `qmd get` and `qmd multi-get` are now **line-numbered by default** and print
1181
+ the document's `#docid` and `qmd://` path in the output header. Disable line
1182
+ numbers with `--no-line-numbers`. The MCP `get`/`multi_get` tools default
1183
+ `lineNumbers` to `true` to match.
1184
+ - `qmd multi-get` now includes the `#docid` in every output format
1185
+ (`--md`, `--json`, `--csv`, `--xml`, `--files`, and the default CLI view),
1186
+ consistent with `qmd search`.
1187
+ - `qmd get` and `qmd multi-get` accept `--full-path`, which replaces the
1188
+ `qmd://` path + `#docid` with the document's on-disk filesystem path (handy for
1189
+ piping into `Read`/`Edit`/an editor). Falls back to the canonical `qmd://` +
1190
+ docid header when the file no longer exists on disk.
1191
+ - `qmd search` / `qmd query` now show a clearer hit identifier: the default CLI
1192
+ view (and the new `**file:**` line in `--md` output) always prints the full
1193
+ `qmd://collection/path` URI so you can pipe it straight back into `qmd get`.
1194
+ - `qmd search` / `qmd query` accept `--full-path` with the same semantics as
1195
+ `qmd get`: the result label becomes the file's on-disk path — `./`-prefixed
1196
+ relative path when the file lives in a subfolder of `$PWD`, absolute realpath
1197
+ otherwise — and the per-result `#docid` is dropped because the path is the
1198
+ identifier. The leading `./` is intentional so the output is unambiguously a
1199
+ filesystem path. Applies to all output formats.
1200
+ - `qmd get` and `qmd multi-get` now also use the `./`-prefixed convention when
1201
+ `--full-path` renders a path under `$PWD`, matching `search`/`query`.
1202
+ - New `--format <kind>` flag selects the output format (`cli` | `json` | `csv` |
1203
+ `md` | `xml` | `files`) for `search`, `query`, and `multi-get`. The legacy
1204
+ boolean aliases (`--json`/`--csv`/`--md`/`--xml`/`--files`) still work but are
1205
+ no longer in `--help`; prefer `--format`.
1206
+
1207
+ ### Fixes
1208
+
1209
+ - Launcher: source-mode runner selection now prefers Node + tsx over Bun when
1210
+ both `package-lock.json` and `bun.lock` are present in the package root,
1211
+ mirroring the dist-mode "npm priority" rule. Fixes pnpm-global installs that
1212
+ copy the entire working tree (including `.git` and `bun.lock`) into the
1213
+ install dir and previously routed through Bun, causing ABI mismatches with
1214
+ the Node-built `better-sqlite3` / `sqlite-vec` native modules.
1215
+ - Darwin Metal: llama-using commands (`query`, `vsearch`, `embed`) no longer
1216
+ dump a multi-kB GGML/Metal backtrace at process exit even when output
1217
+ succeeded. The libggml-metal static `ggml_metal_device` destructor asserts
1218
+ `[rsets->data count] == 0` during `__cxa_finalize_ranges`, but the
1219
+ buffer-free path never calls the symmetric `ggml_metal_device_rsets_rm`
1220
+ to remove released rsets from the device collection (upstream
1221
+ ggml-org/llama.cpp#22593, one-line fix open as PR #22595). The assertion
1222
+ only fires when `process.exit()` skips Node's `beforeExit` hook, which is
1223
+ what node-llama-cpp uses to auto-dispose Metal contexts. Primary fix:
1224
+ `finishSuccessfulCliCommand` now sets `process.exitCode = 0` and returns
1225
+ instead of calling `process.exit(0)`, so `beforeExit` fires and the native
1226
+ binding cleans up before libc's static destructor runs. Defense-in-depth:
1227
+ the launcher (`bin/qmd`) and the npm test driver (`scripts/test-all.mjs`
1228
+ + the `test:bun` / `test:unit` package.json scripts) also set
1229
+ `GGML_METAL_NO_RESIDENCY=1` on darwin before spawning node/bun, covering
1230
+ error paths and tests that still terminate via `process.exit()`. The env
1231
+ var must be set before node/bun start — libggml-metal reads it via libc
1232
+ `getenv` at module-load time, and Bun does not propagate `process.env`
1233
+ mutations to libc `setenv` — so it lives in the launcher rather than in
1234
+ test-preload. Residency sets give no measurable speedup for QMD's
1235
+ short-lived CLI workflow (benchmarked on M3 Pro). Opt back in with
1236
+ `QMD_METAL_KEEP_RESIDENCY=1` for long-lived qmd processes (e.g. the MCP
1237
+ daemon may benefit on hot reload) or to triage the upstream fix.
1238
+ `qmd doctor` reports the mitigation state. Minimal reproduction:
1239
+ `scripts/repro-metal-rsets-crash.mjs`.
1240
+
1241
+ ### Docs
1242
+
1243
+ - qmd skill: emphasize reading line ranges with `get`'s built-in
1244
+ `:from:count` suffix / `--from`/`-l` flags instead of piping through
1245
+ `sed`/`head`/`tail`; cite the docid and line numbers now present in retrieval
1246
+ output; and author structured `intent:`/`lex:`/`vec:`/`hyde:` queries yourself
1247
+ rather than relying on built-in query expansion.
1248
+
1249
+ ## [2.5.2] - 2026-05-22
1250
+
1251
+ ### Fixes
1252
+
1253
+ - Launcher: Rewrite `bin/qmd` as a Node-based shebang polyglot to fix global npm installation execution failures on Windows (#668 / #452), while supporting seamless fallback to Bun in Node-less environments.
1254
+
1255
+ ## [2.5.1] - 2026-05-20
1256
+
1257
+ ### Changes
1258
+
1259
+ - Release: publish from GitHub Actions via npm Trusted Publishing/OIDC instead of a long-lived `NPM_TOKEN` secret.
1260
+
1261
+ ## [2.5.0] - 2026-05-19
1262
+
1263
+ ### Changes
1264
+
1265
+ - Dependencies: update core SQLite/config/chunking packages (`better-sqlite3`, `yaml`, `web-tree-sitter`, `tree-sitter-go`, and `tree-sitter-python`) while keeping incompatible `zod`, `tsx`, and `vitest` majors pinned.
1266
+ - Agent skills: add `qmd skills list|get|path` to serve version-matched runtime skill instructions from the installed CLI, and make `qmd skill install` write a stable discovery stub so installed agent skills do not go stale after QMD upgrades.
1267
+ - CLI: add `qmd doctor` for index/runtime diagnostics, including SQLite/sqlite-vec versions, embedding fingerprint freshness, mixed-fingerprint detection, safe legacy fingerprint adoption, and content-hash sampling.
1268
+
1269
+ ### Fixes
1270
+
1271
+ - Launcher: prefer runnable TypeScript source in git checkouts even when ignored `dist/` artifacts exist, while packaged installs continue to run `dist/`.
1272
+ - GPU: keep node-llama-cpp's documented `gpu: "auto"` initialization as the primary path, then perform no-build packaged CUDA/Vulkan/Metal probes only if auto falls back to CPU.
1273
+ - CLI: move GPU/CPU runtime diagnostics out of `qmd status`; use `qmd doctor` for device probing and related environment guidance.
1274
+ - CLI: point unexpected command/setup failures toward `qmd doctor` so diagnostics are the default next step when QMD behaves incorrectly.
1275
+ - Doctor: explicitly warn when `content_vectors` contains multiple non-empty embedding fingerprint names, with the per-fingerprint document/chunk breakdown.
1276
+ - Embed: make the TTY progress line label byte-based input progress explicitly, show embedded chunks as a count, and shorten the displayed model name.
1277
+ - Embed: retain per-chunk failure details, retry failed chunks after later successful embeds and again when no other chunks remain, clear recovered errors, and cap retries to avoid endless loops.
1278
+ - Tests: expand the container smoke harness to cover npm-global, npx-style, and Bun-global install scenarios, always checking auto and `QMD_FORCE_CPU=1` doctor modes, with opt-in tiny `qmd embed` and GPU probe runs for supported container runtimes.
1279
+ - Embedding: fingerprint vector metadata using the active embedding model and formatting/chunking parameters so stale vectors are treated as pending after search semantics change. Legacy `content_vectors` columns are migrated lazily on first vector-health/write use to preserve fast QMD startup.
1280
+
1281
+ - Skill: expand the packaged QMD skill with retrieval-first workflows, structured query examples, wiki/source collection guidance, and safe fallbacks when model-backed search is unavailable.
1282
+ - Tests: make `bun run test` execute the local unit suite under both Node/Vitest and Bun (`test:node` + `test:bun`) so runtime-specific regressions are caught before CI.
1283
+ - Model config: centralize embedding/rerank/generation model resolution so `qmd embed`, `status`, `query`, `vsearch`, `pull`, SDK vector search, and `bench` use the same active `.qmd/index.yaml` model hints and environment fallbacks.
1284
+ - GPU/status: `qmd status` now uses the same embedding model identity as `qmd embed` when computing pending embeddings, so URI-backed embeddings are not incorrectly reported as pending under the legacy `embeddinggemma` alias.
1285
+ - GPU status: `qmd status` now always shows GPU mode/configuration without unsafe native probing, and CPU-fallback warnings point to `QMD_STATUS_DEVICE_PROBE=1 qmd status` for an actual backend probe. The no-GPU warning is emitted once per process instead of once per LLM instance during benchmarks.
1286
+ - GPU: add `QMD_FORCE_CPU=1` / `--no-gpu` to bypass CUDA/Vulkan/Metal probing entirely, and route native llama.cpp stdout noise to stderr so JSON output stays parseable during search/query commands.
1287
+ - Snippet line numbers: `qmd_query` (MCP), HTTP `/query`, and `qmd query`
1288
+ (CLI JSON output and snippet headers) now return absolute source-file
1289
+ line numbers instead of chunk-local ones, so the `line` field can be
1290
+ passed back to `qmd_get` as `fromLine` without a separate lookup.
1291
+ Snippet selection remains scoped to the best matching chunk
1292
+ (preserves #149).
1293
+ - CLI: `qmd query --full` now emits the full document body in all output
1294
+ formats (json, csv, md, xml), restoring the documented behavior of the
1295
+ flag. Previously it returned only the best matching chunk (~3.6KB max
1296
+ per result). Output payload for `--full` queries is now proportional
1297
+ to total document size.
1298
+ - macOS Metal: `qmd query --json` now flushes successful JSON output and uses a safe immediate-exit path on Darwin to avoid ggml Metal finalizer aborts; other commands still dispose LLM contexts/models before the llama runtime. #368
1299
+ - Embedding: require complete chunk coverage before treating a document as
1300
+ embedded, remove partial vectors when chunk/session failures leave a
1301
+ document incomplete, and keep `qmd status` pending counts honest after
1302
+ interrupted long embed runs. #637 #378
1303
+ - Embedding: `qmd embed -c <collection>` now scopes pending-doc selection
1304
+ to the requested collection instead of embedding global pending work.
1305
+ Scoped `--force` clears only collection-owned vectors, preserves shared
1306
+ hashes referenced by sibling collections, and drops `vectors_vec` only
1307
+ when the scoped clear empties all vectors.
1308
+ - Hybrid search: weight RRF lists by query type so original FTS and original vector evidence get the intended 2x boost, instead of accidentally boosting the first lexical expansion. #591
1309
+ - MCP: seed llama.cpp/GGML quiet env vars before launching `qmd mcp` so native logs cannot pollute stdio JSON-RPC framing. #593
1310
+ - CLI: remove CommonJS `require()` calls from ESM index path normalization so `qmd --index <path>` no longer crashes with `ERR_AMBIGUOUS_MODULE_SYNTAX` on Node 22+. #634
1311
+ - Windows CUDA: serialize llama.cpp embedding/reranking contexts by default to avoid intermittent `ggml-cuda.cu:98` crashes in `qmd query`; set `QMD_EMBED_PARALLELISM` to opt back into parallel contexts if your driver is stable. #519
1312
+ - MCP: make `qmd mcp --index <name>` use the selected index for both foreground and daemon HTTP servers instead of falling back to the default store. #343
1313
+ - Embedding: respect `QMD_EMBED_MODEL` consistently for vector indexing and vector-backed search, with default-model fallback when unset.
1314
+ - Config: use one home-directory resolver for YAML config and the default SQLite cache path, avoiding Windows CLI/MCP split-brain when `HOME` is unset.
1315
+ - GPU: respect explicit `QMD_LLAMA_GPU=metal|vulkan|cuda` backend overrides instead of always using auto GPU selection. #529
1316
+ - Fix: preserve original filename case in `handelize()`. The previous
1317
+ `.toLowerCase()` call made indexed paths unreachable on case-sensitive
1318
+ filesystems (Linux). `qmd update` automatically migrates legacy
1319
+ lowercase paths without re-embedding.
1320
+ - CLI: make `qmd status` skip native `node-llama-cpp` device probing by
1321
+ default so status stays safe on machines with broken or unsupported GPU
1322
+ drivers. Set `QMD_STATUS_DEVICE_PROBE=1` to opt in.
1323
+ - CLI: lazy-load `node-llama-cpp` so lightweight commands such as
1324
+ `qmd status` do not import native ML dependencies or trigger llama.cpp
1325
+ builds on ARM/no-GPU machines. #491
1326
+ - Store: keep content rows referenced by inactive documents during orphan
1327
+ cleanup so `qmd update` preserves soft-deleted tombstones for removed
1328
+ files. #585
1329
+ - Packaging: install AST grammar WASM packages as required dependencies so
1330
+ Bun global installs include TypeScript/TSX/JavaScript grammars, and add a
1331
+ `smoke:package-grammars` verification command. #595
1332
+ - Launcher: add wrapper smoke coverage for scoped package, npm/npx,
1333
+ Homebrew/Linuxbrew, Bun global symlink layouts, and `$BUN_INSTALL`
1334
+ false-positive runtime selection regressions. #351 #353 #354 #356 #358 #359
1335
+
1336
+ ## [2.1.0] - 2026-04-05
1337
+
1338
+ Code files now chunk at function and class boundaries via tree-sitter,
1339
+ clickable editor links land you at the right line from search results,
1340
+ and per-collection model configuration means you can point different
1341
+ collections at different embedding models. 25+ community PRs fix
1342
+ embedding stability, BM25 accuracy, and cross-platform launcher issues.
1343
+
1344
+ ### Changes
1345
+
1346
+ - AST-aware chunking for code files via `web-tree-sitter`. Supported
1347
+ languages: TypeScript/JavaScript, Python, Go, and Rust. Code files
1348
+ are chunked at function, class, and import boundaries instead of
1349
+ arbitrary text positions. Markdown and unknown file types are unchanged.
1350
+ `--chunk-strategy <auto|regex>` flag on `qmd embed` and `qmd query`
1351
+ (default `regex`). SDK: `chunkStrategy` option on `embed()` and
1352
+ `search()`. `qmd status` shows grammar availability.
1353
+ - `qmd bench <fixture.json>` command for search quality benchmarks.
1354
+ Measures precision@k, recall, MRR, and F1 across BM25, vector, hybrid,
1355
+ and full pipeline backends. Ships with an example fixture against
1356
+ the eval-docs test collection. #470 (thanks @jmilinovich)
1357
+ - `models:` section in `index.yml` lets you configure `embed`, `rerank`,
1358
+ and `generate` model URIs per collection. Resolution order is
1359
+ config > env var (`QMD_EMBED_MODEL`, `QMD_RERANK_MODEL`,
1360
+ `QMD_GENERATE_MODEL`) > built-in default. #502
1361
+ (thanks @JohnRichardEnders)
1362
+ - CLI search output now emits clickable OSC 8 terminal hyperlinks when
1363
+ stdout is a TTY. Links resolve `qmd://` paths to absolute filesystem
1364
+ paths and open in editors via URI templates (default:
1365
+ `vscode://file/{path}:{line}:{col}`). Configure with `QMD_EDITOR_URI`
1366
+ or `editor_uri` in the YAML config. #508 (thanks @danmackinlay)
1367
+ - `--no-rerank` flag skips the reranking step in `qmd query` — useful
1368
+ when you want fast results or don't have a GPU. Also exposed as
1369
+ `rerank: false` on the MCP `query` tool. #370 (thanks @mvanhorn),
1370
+ #478 (thanks @zestyboy)
1371
+ - ONNX conversion script for deploying embedding models via
1372
+ Transformers.js. #399 (thanks @shreyaskarnik)
1373
+ - GitHub Actions workflow to build the Nix flake on Linux and macOS.
1374
+
1375
+ ### Fixes
1376
+
1377
+ - Embedding: prevent `qmd embed` from running indefinitely when the
1378
+ embedding loop stalls. #458 (thanks @ccc-fff)
1379
+ - Embedding: truncate oversized text before embedding to prevent GGML
1380
+ crash, and bound memory usage during batch embedding. #393
1381
+ (thanks @lskun), #395 (thanks @ProgramCaiCai)
1382
+ - Embedding: set explicit embed context size (default 2048, configurable
1383
+ via `QMD_EMBED_CONTEXT_SIZE`) instead of using the model's full
1384
+ window. #500
1385
+ - Embedding: error on dimension mismatch instead of silently rebuilding
1386
+ the vec0 table. #501
1387
+ - Embedding: handle vec0 `OR REPLACE` limitation in `insertEmbedding`.
1388
+ #456 (thanks @antonio-mello-ai)
1389
+ - Embedding: fix model selection when multiple models are configured.
1390
+ #494
1391
+ - BM25: correct field weights to include all 3 FTS columns — title,
1392
+ body, and path were not weighted correctly. #462 (thanks @goldsr09)
1393
+ - BM25: handle hyphenated tokens in FTS5 lex queries so terms like
1394
+ "real-time" match correctly. #463 (thanks @goldsr09)
1395
+ - BM25: preserve underscores in search terms instead of stripping them.
1396
+ #404
1397
+ - BM25: use CTE in `searchFTS` to prevent query planner regression with
1398
+ collection filter.
1399
+ - Reranker: increase default context size 2048→4096 and make
1400
+ configurable via `QMD_RERANK_CONTEXT_SIZE`. Fix template overhead
1401
+ underestimate 200→512. #453 (thanks @builderjarvis)
1402
+ - GPU: catch initialization failures and fall back to CPU instead of
1403
+ crashing.
1404
+ - MCP: read version from `package.json` instead of hardcoding. #431
1405
+ - MCP: include collection name in status output. #416
1406
+ - Multi-get: support brace expansion patterns in glob matching. #424
1407
+ - Launcher: prioritize `package-lock.json` to prevent Bun false
1408
+ positive. #385 (thanks @rymalia)
1409
+ - Launcher: remove `$BUN_INSTALL` check that caused false Bun detection.
1410
+ #362 (thanks @syedair)
1411
+ - Launcher: skip Git Bash path detection on WSL. #371
1412
+ (thanks @oysteinkrog)
1413
+ - Model cache: respect `XDG_CACHE_HOME` for model cache directory. #457
1414
+ (thanks @antonio-mello-ai)
1415
+ - SQLite: add macOS Homebrew SQLite support for Bun and restore
1416
+ actionable errors. #377 (thanks @serhii12)
1417
+ - Pin zod to exact 4.2.1 to fix `tsc` build failure. #382
1418
+ (thanks @rymalia)
1419
+ - Preserve dots and original case in `handelize()` — filenames like
1420
+ `MEMORY.md` no longer become `memory-md`. #475 (thanks @alexei-led)
1421
+ - Include `line` in `--json` search output so editor integrations can
1422
+ jump directly to `file:line`. #506 (thanks @danmackinlay)
1423
+ - Nix: fix paths in flake and make Bun dependency a fixed-output
1424
+ derivation so sandboxed Linux builds work offline. #479
1425
+ (thanks @surma-dump)
1426
+ - Sync stale `bun.lock` (`better-sqlite3` 11.x → 12.x). CI and release
1427
+ script now use `--frozen-lockfile` to prevent recurrence. #386
1428
+ (thanks @Mic92)
1429
+ - Approve native build scripts in pnpm so `better-sqlite3` and
1430
+ tree-sitter modules compile correctly. Update vitest ^3.0.0 → ^3.2.4.
1431
+
1432
+ ## [2.0.1] - 2026-03-10
1433
+
1434
+ ### Changes
1435
+
1436
+ - `qmd skill install` copies the packaged QMD skill into
1437
+ `~/.claude/commands/` for one-command setup. #355 (thanks @nibzard)
1438
+
1439
+ ### Fixes
1440
+
1441
+ - Fix Qwen3-Embedding GGUF filename case — HuggingFace filenames are
1442
+ case-sensitive, the lowercase variant returned 404. #349 (thanks @byheaven)
1443
+ - Resolve symlinked global launcher path so `qmd` works correctly when
1444
+ installed via `npm i -g`. #352 (thanks @nibzard)
1445
+
1446
+ ## [2.0.0] - 2026-03-10
1447
+
1448
+ QMD 2.0 declares a stable library API. The SDK is now the primary interface —
1449
+ the MCP server is a clean consumer of it, and the source is organized into
1450
+ `src/cli/` and `src/mcp/`. Also: Node 25 support and a runtime-aware bin wrapper
1451
+ for bun installs.
1452
+
1453
+ ### Changes
1454
+
1455
+ - Stable SDK API with `QMDStore` interface — search, retrieval, collection/context
1456
+ management, indexing, lifecycle
1457
+ - Unified `search()`: pass `query` for auto-expansion or `queries` for
1458
+ pre-expanded lex/vec/hyde — replaces the old query/search/structuredSearch split
1459
+ - New `getDocumentBody()`, `getDefaultCollectionNames()`, `Maintenance` class
1460
+ - MCP server rewritten as a clean SDK consumer — zero internal store access
1461
+ - CLI and MCP organized into `src/cli/` and `src/mcp/` subdirectories
1462
+ - Runtime-aware `bin/qmd` wrapper detects bun vs node to avoid ABI mismatches.
1463
+ Closes #319
1464
+ - `better-sqlite3` bumped to ^12.4.5 for Node 25 support. Closes #257
1465
+ - Utility exports: `extractSnippet`, `addLineNumbers`, `DEFAULT_MULTI_GET_MAX_BYTES`
1466
+
1467
+ ### Fixes
1468
+
1469
+ - Remove unused `import { resolve }` in store.ts that shadowed local export
1470
+
1471
+ ## [1.1.6] - 2026-03-09
1472
+
1473
+ QMD can now be used as a library. `import { createStore } from '@tobilu/qmd'`
1474
+ gives you the full search and indexing API — hybrid query, BM25, structured
1475
+ search, collection/context management — without shelling out to the CLI.
1476
+
1477
+ ### Changes
1478
+
1479
+ - **SDK / library mode**: `createStore({ dbPath, config })` returns a
1480
+ `QMDStore` with `query()`, `search()`, `structuredSearch()`, `get()`,
1481
+ `multiGet()`, and collection/context management methods. Supports inline
1482
+ config (no files needed) or a YAML config path.
1483
+ - **Package exports**: `package.json` now declares `main`, `types`, and
1484
+ `exports` so bundlers and TypeScript resolve `@tobilu/qmd` correctly.
1485
+
1486
+ ## [1.1.5] - 2026-03-07
1487
+
1488
+ Ambiguous queries like "performance" now produce dramatically better results
1489
+ when the caller knows what they mean. The new `intent` parameter steers all
1490
+ five pipeline stages — expansion, strong-signal bypass, chunk selection,
1491
+ reranking, and snippet extraction — without searching on its own. Design and
1492
+ original implementation by Ilya Grigorik (@vyalamar) in #180.
1493
+
1494
+ ### Changes
1495
+
1496
+ - **Intent parameter**: optional `intent` string disambiguates queries across
1497
+ the entire search pipeline. Available via CLI (`--intent` flag or `intent:`
1498
+ line in query documents), MCP (`intent` field on the query tool), and
1499
+ programmatic API. Adapted from PR #180 (thanks @vyalamar).
1500
+ - **Query expansion**: when intent is provided, the expansion LLM prompt
1501
+ includes `Query intent: {intent}`, matching the finetune training data
1502
+ format for better-aligned expansions.
1503
+ - **Reranking**: intent is prepended to the rerank query so Qwen3-Reranker
1504
+ scores with domain context.
1505
+ - **Chunk selection**: intent terms scored at 0.5× weight alongside query
1506
+ terms (1.0×) when selecting the best chunk per document for reranking.
1507
+ - **Snippet extraction**: intent terms scored at 0.3× weight to nudge
1508
+ snippets toward intent-relevant lines without overriding query anchoring.
1509
+ - **Strong-signal bypass disabled with intent**: when intent is provided, the
1510
+ BM25 strong-signal shortcut is skipped — the obvious keyword match may not
1511
+ be what the caller wants.
1512
+ - **MCP instructions**: callers are now guided to provide `intent` on every
1513
+ search call for disambiguation.
1514
+ - **Query document syntax**: `intent:` recognized as a line type. At most one
1515
+ per document, cannot appear alone. Grammar updated in `docs/SYNTAX.md`.
1516
+
1517
+ ## [1.1.2] - 2026-03-07
1518
+
1519
+ 13 community PRs merged. GPU initialization replaced with node-llama-cpp's
1520
+ built-in `autoAttempt` — deleting ~220 lines of manual fallback code and
1521
+ fixing GPU issues reported across 10+ PRs in one shot. Reranking is faster
1522
+ through chunk deduplication and a parallelism cap that prevents VRAM
1523
+ exhaustion.
1524
+
1525
+ ### Changes
1526
+
1527
+ - **GPU init**: use node-llama-cpp's `build: "autoAttempt"` instead of manual
1528
+ GPU backend detection. Automatically tries Metal/CUDA/Vulkan and falls back
1529
+ gracefully. #310 (thanks @giladgd — the node-llama-cpp author)
1530
+ - **Query `--explain`**: `qmd query --explain` exposes retrieval score traces
1531
+ — backend scores, per-list RRF contributions, top-rank bonus, reranker
1532
+ score, and final blended score. Works in JSON and CLI output. #242
1533
+ (thanks @vyalamar)
1534
+ - **Collection ignore patterns**: `ignore: ["Sessions/**", "*.tmp"]` in
1535
+ collection config to exclude files from indexing. #304 (thanks @sebkouba)
1536
+ - **Multilingual embeddings**: `QMD_EMBED_MODEL` env var lets you swap in
1537
+ models like Qwen3-Embedding for non-English collections. #273 (thanks
1538
+ @daocoding)
1539
+ - **Configurable expansion context**: `QMD_EXPAND_CONTEXT_SIZE` env var
1540
+ (default 2048) — previously used the model's full 40960-token window,
1541
+ wasting VRAM. #313 (thanks @0xble)
1542
+ - **`candidateLimit` exposed**: `-C` / `--candidate-limit` flag and MCP
1543
+ parameter to tune how many candidates reach the reranker. #255 (thanks
1544
+ @pandysp)
1545
+ - **MCP multi-session**: HTTP transport now supports multiple concurrent
1546
+ client sessions, each with its own server instance. #286 (thanks @joelev)
1547
+
1548
+ ### Fixes
1549
+
1550
+ - **Reranking performance**: cap parallel rerank contexts at 4 to prevent
1551
+ VRAM exhaustion on high-core machines. Deduplicate identical chunk texts
1552
+ before reranking — same content from different files now shares a single
1553
+ reranker call. Cache scores by content hash instead of file path.
1554
+ - Deactivate stale docs when all files are removed from a collection and
1555
+ `qmd update` is run. #312 (thanks @0xble)
1556
+ - Handle emoji-only filenames (`🐘.md` → `1f418.md`) instead of crashing.
1557
+ #308 (thanks @debugerman)
1558
+ - Skip unreadable files during indexing (e.g. iCloud-evicted files returning
1559
+ EAGAIN) instead of crashing. #253 (thanks @jimmynail)
1560
+ - Suppress progress bar escape sequences when stderr is not a TTY. #230
1561
+ (thanks @dgilperez)
1562
+ - Emit format-appropriate empty output (`[]` for JSON, CSV header for CSV,
1563
+ etc.) instead of plain text "No results." #228 (thanks @amsminn)
1564
+ - Correct Windows sqlite-vec package name (`sqlite-vec-windows-x64`) and add
1565
+ `sqlite-vec-linux-arm64`. #225 (thanks @ilepn)
1566
+ - Fix claude plugin setup CLI commands in README. #311 (thanks @gi11es)
1567
+
1568
+ ## [1.1.1] - 2026-03-06
1569
+
1570
+ ### Fixes
1571
+
1572
+ - Reranker: truncate documents exceeding the 2048-token context window
1573
+ instead of silently producing garbage scores. Long chunks (e.g. from
1574
+ PDF ingestion) now get a fair ranking.
1575
+ - Nix: add python3 and cctools to build dependencies. #214 (thanks
1576
+ @pcasaretto)
1577
+
1578
+ ## [1.1.0] - 2026-02-20
1579
+
1580
+ QMD now speaks in **query documents** — structured multi-line queries where every line is typed (`lex:`, `vec:`, `hyde:`), combining keyword precision with semantic recall. A single plain query still works exactly as before (it's treated as an implicit `expand:` and auto-expanded by the LLM). Lex now supports quoted phrases and negation (`"C++ performance" -sports -athlete`), making intent-aware disambiguation practical. The formal query grammar is documented in `docs/SYNTAX.md`.
1581
+
1582
+ The npm package now uses the standard `#!/usr/bin/env node` bin convention, replacing the custom bash wrapper. This fixes native module ABI mismatches when installed via bun and works on any platform with node >= 22 on PATH.
1583
+
1584
+ ### Changes
1585
+
1586
+ - **Query document format**: multi-line queries with typed sub-queries (`lex:`, `vec:`, `hyde:`). Plain queries remain the default (`expand:` implicit, but not written inside the document). First sub-query gets 2× fusion weight — put your strongest signal first. Formal grammar in `docs/SYNTAX.md`.
1587
+ - **Lex syntax**: full BM25 operator support. `"exact phrase"` for verbatim matching; `-term` and `-"phrase"` for exclusions. Essential for disambiguation when a term is overloaded across domains (e.g. `performance -sports -athlete`).
1588
+ - **`expand:` shortcut**: send a single plain query (or start the document with `expand:` on its only line) to auto-expand via the local LLM. Query documents themselves are limited to `lex`, `vec`, and `hyde` lines.
1589
+ - **MCP `query` tool** (renamed from `structured_search`): rewrote the tool description to fully teach AI agents the query document format, lex syntax, and combination strategy. Includes worked examples with intent-aware lex.
1590
+ - **HTTP `/query` endpoint** (renamed from `/search`; `/search` kept as silent alias).
1591
+ - **`collections` array filter**: filter by multiple collections in a single query (`collections: ["notes", "brain"]`). Removed the single `collection` string param — array only.
1592
+ - **Collection `include`/`exclude`**: `includeByDefault: false` hides a collection from all queries unless explicitly named via `collections`. CLI: `qmd collection exclude <name>` / `qmd collection include <name>`.
1593
+ - **Collection `update-cmd`**: attach a shell command that runs before every `qmd update` (e.g. `git stash && git pull --rebase --ff-only && git stash pop`). CLI: `qmd collection update-cmd <name> '<cmd>'`.
1594
+ - **`qmd status` tips**: shows actionable tips when collections lack context descriptions or update commands.
1595
+ - **`qmd collection` subcommands**: `show`, `update-cmd`, `include`, `exclude`. Bare `qmd collection` now prints help.
1596
+ - **Packaging**: replaced custom bash wrapper with standard `#!/usr/bin/env node` shebang on `dist/qmd.js`. Fixes native module ABI mismatches when installed via bun, and works on any platform where node >= 22 is on PATH.
1597
+ - **Removed MCP tools** `search`, `vector_search`, `deep_search` — all superseded by `query`.
1598
+ - **Removed** `qmd context check` command.
1599
+ - **CLI timing**: each LLM step (expand, embed, rerank) prints elapsed time inline (`Expanding query... (4.2s)`).
1600
+
1601
+ ### Fixes
1602
+
1603
+ - `qmd collection list` shows `[excluded]` tag for collections with `includeByDefault: false`.
1604
+ - Default searches now respect `includeByDefault` — excluded collections are skipped unless explicitly named.
1605
+ - Fix main module detection when installed globally via npm/bun (symlink resolution).
1606
+
1607
+ ## [1.0.7] - 2026-02-18
1608
+
1609
+ ### Changes
1610
+
1611
+ - LLM: add LiquidAI LFM2-1.2B as an alternative base model for query
1612
+ expansion fine-tuning. LFM2's hybrid architecture (convolutions + attention)
1613
+ is 2x faster at decode/prefill vs standard transformers — good fit for
1614
+ on-device inference.
1615
+ - CLI: support multiple `-c` flags to search across several collections at
1616
+ once (e.g. `qmd search -c notes -c journals "query"`). #191 (thanks
1617
+ @openclaw)
1618
+
1619
+ ### Fixes
1620
+
1621
+ - Return empty JSON array `[]` instead of no output when `--json` search
1622
+ finds no results.
1623
+ - Resolve relative paths passed to `--index` so they don't produce malformed
1624
+ config entries.
1625
+ - Respect `XDG_CONFIG_HOME` for collection config path instead of always
1626
+ using `~/.config`. #190 (thanks @openclaw)
1627
+ - CLI: empty-collection hint now shows the correct `collection add` command.
1628
+ #200 (thanks @vincentkoc)
1629
+
1630
+ ## [1.0.6] - 2026-02-16
1631
+
1632
+ ### Changes
1633
+
1634
+ - CLI: `qmd status` now shows models with full HuggingFace links instead of
1635
+ static names in `--help`. Model info is derived from the actual configured
1636
+ URIs so it stays accurate if models change.
1637
+ - Release tooling: pre-push hook handles non-interactive shells (CI, editors)
1638
+ gracefully — warnings auto-proceed instead of hanging on a tty prompt.
1639
+ Annotated tags now resolve correctly for CI checks.
1640
+
1641
+ ## [1.0.5] - 2026-02-16
1642
+
1643
+ The npm package now ships compiled JavaScript instead of raw TypeScript,
1644
+ removing the `tsx` runtime dependency. A new `/release` skill automates the
1645
+ full release workflow with changelog validation and git hook enforcement.
1646
+
1647
+ ### Changes
1648
+
1649
+ - Build: compile TypeScript to `dist/` via `tsc` so the npm package no longer
1650
+ requires `tsx` at runtime. The `qmd` shell wrapper now runs `dist/qmd.js`
1651
+ directly.
1652
+ - Release tooling: new `/release` skill that manages the full release
1653
+ lifecycle — validates changelog, installs git hooks, previews release notes,
1654
+ and cuts the release. Auto-populates `[Unreleased]` from git history when
1655
+ empty.
1656
+ - Release tooling: `scripts/extract-changelog.sh` extracts cumulative notes
1657
+ for the full minor series (e.g. 1.0.0 through 1.0.5) for GitHub releases.
1658
+ Includes `[Unreleased]` content in previews.
1659
+ - Release tooling: `scripts/release.sh` renames `[Unreleased]` to a versioned
1660
+ heading and inserts a fresh empty `[Unreleased]` section automatically.
1661
+ - Release tooling: pre-push git hook blocks `v*` tag pushes unless
1662
+ `package.json` version matches the tag, a changelog entry exists, and CI
1663
+ passed on GitHub.
1664
+ - Publish workflow: GitHub Actions now builds TypeScript, creates a GitHub
1665
+ release with cumulative notes extracted from the changelog, and publishes
1666
+ to npm with provenance.
1667
+
1668
+ ## [1.0.0] - 2026-02-15
1669
+
1670
+ QMD now runs on both Node.js and Bun, with up to 2.7x faster reranking
1671
+ through parallel GPU contexts. GPU auto-detection replaces the unreliable
1672
+ `gpu: "auto"` with explicit CUDA/Metal/Vulkan probing.
1673
+
1674
+ ### Changes
1675
+
1676
+ - Runtime: support Node.js (>=22) alongside Bun via a cross-runtime SQLite
1677
+ abstraction layer (`src/db.ts`). `bun:sqlite` on Bun, `better-sqlite3` on
1678
+ Node. The `qmd` wrapper auto-detects a suitable Node.js install via PATH,
1679
+ then falls back to mise, asdf, nvm, and Homebrew locations.
1680
+ - Performance: parallel embedding & reranking via multiple LlamaContext
1681
+ instances — up to 2.7x faster on multi-core machines.
1682
+ - Performance: flash attention for ~20% less VRAM per reranking context,
1683
+ enabling more parallel contexts on GPU.
1684
+ - Performance: right-sized reranker context (40960 → 2048 tokens, 17x less
1685
+ memory) since chunks are capped at ~900 tokens.
1686
+ - Performance: adaptive parallelism — context count computed from available
1687
+ VRAM (GPU) or CPU math cores rather than hardcoded.
1688
+ - GPU: probe for CUDA, Metal, Vulkan explicitly at startup instead of
1689
+ relying on node-llama-cpp's `gpu: "auto"`. `qmd status` shows device info.
1690
+ - Tests: reorganized into flat `test/` directory with vitest for Node.js and
1691
+ bun test for Bun. New `eval-bm25` and `store.helpers.unit` suites.
1692
+
1693
+ ### Fixes
1694
+
1695
+ - Prevent VRAM waste from duplicate context creation during concurrent
1696
+ `embedBatch` calls — initialization lock now covers the full path.
1697
+ - Collection-aware FTS filtering so scoped keyword search actually restricts
1698
+ results to the requested collection.
1699
+
1700
+ ## [0.9.0] - 2026-02-15
1701
+
1702
+ First published release on npm as `@tobilu/qmd`. MCP HTTP transport with
1703
+ daemon mode cuts warm query latency from ~16s to ~10s by keeping models
1704
+ loaded between requests.
1705
+
1706
+ ### Changes
1707
+
1708
+ - MCP: HTTP transport with daemon lifecycle — `qmd mcp --http --daemon`
1709
+ starts a background server, `qmd mcp stop` shuts it down. Models stay warm
1710
+ in VRAM between queries. #149 (thanks @igrigorik)
1711
+ - Search: type-routed query expansion preserves lex/vec/hyde type info and
1712
+ routes to the appropriate backend. Eliminates ~4 wasted backend calls per
1713
+ query (10.0 → 6.0 calls, 1278ms → 549ms). #149 (thanks @igrigorik)
1714
+ - Search: unified pipeline — extracted `hybridQuery()` and
1715
+ `vectorSearchQuery()` to `store.ts` so CLI and MCP share identical logic.
1716
+ Fixes a class of bugs where results differed between the two. #149 (thanks
1717
+ @igrigorik)
1718
+ - MCP: dynamic instructions generated at startup from actual index state —
1719
+ LLMs see collection names, doc counts, and content descriptions. #149
1720
+ (thanks @igrigorik)
1721
+ - MCP: tool renames (vsearch → vector_search, query → deep_search) with
1722
+ rewritten descriptions for better tool selection. #149 (thanks @igrigorik)
1723
+ - Integration: Claude Code plugin with inline status checks and MCP
1724
+ integration. #99 (thanks @galligan)
1725
+
1726
+ ### Fixes
1727
+
1728
+ - BM25 score normalization — formula was inverted (`1/(1+|x|)` instead of
1729
+ `|x|/(1+|x|)`), so strong matches scored *lowest*. Broke `--min-score`
1730
+ filtering and made the "strong signal" short-circuit dead code. #76 (thanks
1731
+ @dgilperez)
1732
+ - Normalize Unicode paths to NFC for macOS compatibility. #82 (thanks
1733
+ @c-stoeckl)
1734
+ - Handle dense content (code) that tokenizes beyond expected chunk size.
1735
+ - Proper cleanup of Metal GPU resources on process exit.
1736
+ - SQLite-vec readiness verification after extension load.
1737
+ - Reactivate deactivated documents on re-index instead of creating duplicates.
1738
+ - Bun UTF-8 path corruption workaround for non-ASCII filenames.
1739
+ - Disable following symlinks in glob.scan to avoid infinite loops.
1740
+
1741
+ ## [0.8.0] - 2026-01-28
1742
+
1743
+ Fine-tuned query expansion model trained with GRPO replaces the stock Qwen3
1744
+ 0.6B. The training pipeline scores expansions on named entity preservation,
1745
+ format compliance, and diversity — producing noticeably better lexical
1746
+ variations and HyDE documents.
1747
+
1748
+ ### Changes
1749
+
1750
+ - LLM: deploy GRPO-trained (Group Relative Policy Optimization) query
1751
+ expansion model, hosted on HuggingFace and auto-downloaded on first use.
1752
+ Better preservation of proper nouns and technical terms in expansions.
1753
+ - LLM: `/only:lex` mode for single-type expansions — useful when you know
1754
+ which search backend will help.
1755
+ - LLM: HyDE output moved to first position so vector search can start
1756
+ embedding while other expansions generate.
1757
+ - LLM: session lifecycle management via `withLLMSession()` pattern — ensures
1758
+ cleanup even on failure, similar to database transactions.
1759
+ - Integration: org-mode title extraction support. #50 (thanks @sh54)
1760
+ - Integration: SQLite extension loading in Nix devshell. #48 (thanks @sh54)
1761
+ - Integration: AI agent discovery via skills.sh. #64 (thanks @Algiras)
1762
+
1763
+ ### Fixes
1764
+
1765
+ - Use sequential embedding on CPU-only systems — parallel contexts caused a
1766
+ race condition where contexts competed for CPU cores, making things slower.
1767
+ #54 (thanks @freeman-jiang)
1768
+ - Fix `collectionName` column in vector search SQL (was still using old
1769
+ `collectionId` from before YAML migration). #61 (thanks @jdvmi00)
1770
+ - Fix Qwen3 sampling params to prevent repetition loops — stock
1771
+ temperature/top-p caused occasional infinite repeat patterns.
1772
+ - Add `--index` option to CLI argument parser (was documented but not wired
1773
+ up). #84 (thanks @Tritlo)
1774
+ - Fix DisposedError during slow batch embedding. #41 (thanks @wuhup)
1775
+
1776
+ ## [0.7.0] - 2026-01-09
1777
+
1778
+ First community contributions. The project gained external contributors,
1779
+ surfacing bugs that only appear in diverse environments — Homebrew sqlite-vec
1780
+ paths, case-sensitive model filenames, and sqlite-vec JOIN incompatibilities.
1781
+
1782
+ ### Changes
1783
+
1784
+ - Indexing: native `realpathSync()` replaces `readlink -f` subprocess spawn
1785
+ per file. On a 5000-file collection this eliminates 5000 shell spawns,
1786
+ ~15% faster. #8 (thanks @burke)
1787
+ - Indexing: single-pass tokenization — chunking algorithm tokenized each
1788
+ document twice (count then split); now tokenizes once and reuses. #9
1789
+ (thanks @burke)
1790
+
1791
+ ### Fixes
1792
+
1793
+ - Fix `vsearch` and `query` hanging — sqlite-vec's virtual table doesn't
1794
+ support the JOIN pattern used; rewrote to subquery. #23 (thanks @mbrendan)
1795
+ - Fix MCP server exiting immediately after startup — process had no active
1796
+ handles keeping the event loop alive. #29 (thanks @mostlydev)
1797
+ - Fix collection filter SQL to properly restrict vector search results.
1798
+ - Support non-ASCII filenames in collection filter.
1799
+ - Skip empty files during indexing instead of crashing on zero-length content.
1800
+ - Fix case sensitivity in Qwen3 model filename resolution. #15 (thanks
1801
+ @gavrix)
1802
+ - Fix sqlite-vec loading on macOS with Homebrew (`BREW_PREFIX` detection).
1803
+ #42 (thanks @komsit37)
1804
+ - Fix Nix flake to use correct `src/qmd.ts` path. #7 (thanks @burke)
1805
+ - Fix docid lookup with quotes support in get command. #36 (thanks
1806
+ @JoshuaLelon)
1807
+ - Fix query expansion model size in documentation. #38 (thanks @odysseus0)
1808
+
1809
+ ## [0.6.0] - 2025-12-28
1810
+
1811
+ Replaced Ollama HTTP API with node-llama-cpp for all LLM operations. Ollama
1812
+ adds convenience but also a running server dependency. node-llama-cpp loads
1813
+ GGUF models directly in-process — zero external dependencies. Models
1814
+ auto-download from HuggingFace on first use.
1815
+
1816
+ ### Changes
1817
+
1818
+ - LLM: structured query expansion via JSON schema grammar constraints.
1819
+ Model produces typed expansions — **lexical** (BM25 keywords), **vector**
1820
+ (semantic rephrasings), **HyDE** (hypothetical document excerpts) — so each
1821
+ routes to the right backend instead of sending everything everywhere.
1822
+ - LLM: lazy model loading with 2-minute inactivity auto-unload. Keeps memory
1823
+ low when idle while avoiding ~3s model load on every query.
1824
+ - Search: conditional query expansion — when BM25 returns strong results, the
1825
+ expensive LLM expansion is skipped entirely.
1826
+ - Search: multi-chunk reranking — documents with multiple relevant chunks
1827
+ scored by aggregating across all chunks rather than best single chunk.
1828
+ - Search: cosine distance for vector search (was L2).
1829
+ - Search: embeddinggemma nomic-style prompt formatting.
1830
+ - Testing: evaluation harness with synthetic test documents and Hit@K metrics
1831
+ for BM25, vector, and hybrid RRF.
1832
+
1833
+ ## [0.5.0] - 2025-12-13
1834
+
1835
+ Collections and contexts moved from SQLite tables to YAML at
1836
+ `~/.config/qmd/index.yml`. SQLite was overkill for config — you can't share
1837
+ it, and it's opaque. YAML is human-readable and version-controllable. The
1838
+ migration was extensive (35+ commits) because every part of the system that
1839
+ touched collections or contexts had to be updated.
1840
+
1841
+ ### Changes
1842
+
1843
+ - Config: YAML-based collections and contexts replace SQLite tables.
1844
+ `collections` and `path_contexts` tables dropped from schema. Collections
1845
+ support an optional `update:` command (e.g., `git pull`) before re-index.
1846
+ - CLI: `qmd collection add/list/remove/rename` commands with `--name` and
1847
+ `--mask` glob pattern support.
1848
+ - CLI: `qmd ls` virtual file tree — list collections, files in a collection,
1849
+ or files under a path prefix.
1850
+ - CLI: `qmd context add/list/check/rm` with hierarchical context inheritance.
1851
+ A query to `qmd://notes/2024/jan/` inherits context from `notes/`,
1852
+ `notes/2024/`, and `notes/2024/jan/`.
1853
+ - CLI: `qmd context add / "text"` for global context across all collections.
1854
+ - CLI: `qmd context check` audit command to find paths without context.
1855
+ - Paths: `qmd://` virtual URI scheme for portable document references.
1856
+ `qmd://notes/ideas.md` works regardless of where the collection lives on
1857
+ disk. Works in `get`, `multi-get`, `ls`, and context commands.
1858
+ - CLI: document IDs (docid) — first 6 chars of content hash for stable
1859
+ references. Shown as `#abc123` in search results, usable with `get` and
1860
+ `multi-get`.
1861
+ - CLI: `--line-numbers` flag for get command output.
1862
+
1863
+ ## [0.4.0] - 2025-12-10
1864
+
1865
+ MCP server for AI agent integration. Without it, agents had to shell out to
1866
+ `qmd search` and parse CLI output. The monolithic `qmd.ts` (1840 lines) was
1867
+ split into focused modules with the project's first test suite (215 tests).
1868
+
1869
+ ### Changes
1870
+
1871
+ - MCP: stdio server with tools for search, vector search, hybrid query,
1872
+ document retrieval, and status. Runs over stdio transport for Claude
1873
+ Desktop and MCP clients.
1874
+ - MCP: spec-compliant with June 2025 MCP specification — removed non-spec
1875
+ `mimeType`, added `isError: true` to errors, `structuredContent` for
1876
+ machine-readable results, proper URI encoding.
1877
+ - MCP: simplified tool naming (`qmd_search` → `search`) since MCP already
1878
+ namespaces by server.
1879
+ - Architecture: extract `store.ts` (1221 LOC), `llm.ts` (539 LOC),
1880
+ `formatter.ts` (359 LOC), `mcp.ts` (503 LOC) from monolithic `qmd.ts`.
1881
+ - Testing: 215 tests (store: 96, llm: 60, mcp: 59) with mocked Ollama for
1882
+ fast, deterministic runs. Before this: zero tests.
1883
+
1884
+ ## [0.3.0] - 2025-12-08
1885
+
1886
+ Document chunking for vector search. A 5000-word document about many topics
1887
+ gets a single embedding that averages everything together, matching poorly for
1888
+ specific queries. Chunking produces one embedding per ~900-token section with
1889
+ focused semantic signal.
1890
+
1891
+ ### Changes
1892
+
1893
+ - Search: markdown-aware chunking — prefers heading boundaries, then paragraph
1894
+ breaks, then sentence boundaries. 15% overlap between chunks ensures
1895
+ cross-boundary queries still match.
1896
+ - Search: multi-chunk scoring bonus (+0.02 per additional chunk, capped at
1897
+ +0.1 for 5+ chunks). Documents relevant in multiple sections rank higher.
1898
+ - CLI: display paths show collection-relative paths and extracted titles
1899
+ (from H1 headings or YAML frontmatter) instead of raw filesystem paths.
1900
+ - CLI: `--all` flag returns all matches (use with `--min-score` to filter).
1901
+ - CLI: byte-based progress bar with ETA for `embed` command.
1902
+ - CLI: human-readable time formatting ("15m 4s" instead of "904.2s").
1903
+ - CLI: documents >64KB truncated with warning during embedding.
1904
+
1905
+ ## [0.2.0] - 2025-12-08
1906
+
1907
+ ### Changes
1908
+
1909
+ - CLI: `--json`, `--csv`, `--files`, `--md`, `--xml` output format flags.
1910
+ `--json` for programmatic access, `--files` for piping, `--md`/`--xml` for
1911
+ LLM consumption, `--csv` for spreadsheets.
1912
+ - CLI: `qmd status` shows index health — document count, size, embedding
1913
+ coverage, time since last update.
1914
+ - Search: weighted RRF — original query gets 2x weight relative to expanded
1915
+ queries since the user's actual words are a more reliable signal.
1916
+
1917
+ ## [0.1.0] - 2025-12-07
1918
+
1919
+ Initial implementation. Built in a single day for searching personal markdown
1920
+ notes, journals, and meeting transcripts.
1921
+
1922
+ ### Changes
1923
+
1924
+ - Search: SQLite FTS5 with BM25 ranking. Chose SQLite over Elasticsearch
1925
+ because QMD is a personal tool — single binary, no server dependencies.
1926
+ - Search: sqlite-vec for vector similarity. Same rationale: in-process, no
1927
+ external vector database.
1928
+ - Search: Reciprocal Rank Fusion to combine BM25 and vector results. RRF is
1929
+ parameter-free and handles missing signals gracefully.
1930
+ - LLM: Ollama for embeddings, reranking, and query expansion. Later replaced
1931
+ with node-llama-cpp in 0.6.0.
1932
+ - CLI: `qmd add`, `qmd embed`, `qmd search`, `qmd vsearch`, `qmd query`,
1933
+ `qmd get`. ~1800 lines of TypeScript in a single `qmd.ts` file.
1934
+
1935
+ [Unreleased]: https://github.com/tobi/qmd/compare/v1.0.0...HEAD
1936
+ [1.0.0]: https://github.com/tobi/qmd/releases/tag/v1.0.0
1937
+ [0.9.0]: https://github.com/tobi/qmd/compare/v0.8.0...v0.9.0