Skip to content

docs: restructure README as qmd-ja native documentation#6

Merged
koz-oda merged 34 commits into
mainfrom
upstream-2.6.3-sync
Jun 29, 2026
Merged

docs: restructure README as qmd-ja native documentation#6
koz-oda merged 34 commits into
mainfrom
upstream-2.6.3-sync

Conversation

@koz-oda

@koz-oda koz-oda commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Summary

  • upstream タイトル(# QMD - Query Markup Documents)を ## qmd-ja — On-Device Search Engine に置き換え
  • アーキテクチャ図に qmd-ja note を追加(Vaporetto・日本語モデルへの参照)
  • What's different テーブルにモデル差分行を追加(3モデルすべて)
  • ## Japanese Optimization セクションを新設(Quick Start の前):Vaporetto WASM 説明 + 推奨モデル設定
  • generate モデルを adsholoko/qmd-query-expansion-ja に更新(Japanese ONNX section・Model Configuration)
  • 冒頭の upstream 参照を1行に整理

Test plan

  • npm run test:unit — 913 pass / 77 skip (vitest)
  • bun test test/store.test.ts — CJK テスト含め pass
  • フルスイートの CJK フレーキーは変更前コミットでも再現する既存問題

🤖 Generated with Claude Code

djalmajr and others added 30 commits May 24, 2026 11:54
The MCP HTTP server hardcoded listen(port, "localhost"), so it is
unreachable from any non-loopback address. In a container, a liveness
probe connects from the pod IP rather than 127.0.0.1, and the bind
refuses that connection.

Add a --host flag with a QMD_HOST environment-variable fallback. The
default stays "localhost", so out-of-the-box behavior is unchanged.
Set --host 0.0.0.0 to accept off-host connections (e.g. k8s probes).

- server.ts: startMcpHttpServer binds options.host ?? QMD_HOST ?? "localhost"
- cli: --host flag, forwarded to the daemon spawn and shown in startup logs
- README: document --host / QMD_HOST
…ing SQLITE_BUSY

Running multiple `qmd query` invocations against the same index in parallel (e.g. an agent fanning out searches) caused N-1 of N processes to fail immediately with `SQLiteError: database is locked` from `initializeDatabase`. The first DDL statement (`DROP TRIGGER IF EXISTS documents_ai`) hits the write lock; with `busy_timeout = 0` (the default for both `bun:sqlite` and `better-sqlite3`), the loser throws on contact instead of waiting.

WAL improves read-while-write concurrency but does not serialise concurrent writers. Only the timeout does that. Setting `PRAGMA busy_timeout = 5000` in `openDatabase` makes any connection wait up to 5s for the write lock before failing. Initialization runs in <100ms, so the worst-case wait for typical agent fan-out (5-10 processes) is ~1s and every process eventually succeeds.

Three tests in test/db.test.ts cover the PRAGMA round-trip on a single and multiple connections, plus a behavioural check that SQLite honours the configured timeout under real lock contention.
index.yml — qmd's declarative collection config — had no README documentation at
all, despite being the single source of truth for every collection's path, glob,
ignore rules, contexts, and update command. This documents it in full, with the
per-collection `update` hook the maintainer publicly called under-documented as the
centerpiece, and overhauls example-index.yml so the repo's starter template
actually teaches the schema.

- README: new "Configuring index.yml" section — annotated YAML example plus a
  key-reference table for global_context, editor_uri, models.*, and per-collection
  path/pattern/ignore/update/includeByDefault/context, with file-location rules
  (XDG_CONFIG_HOME, QMD_CONFIG_DIR, named {name}.yml, project-local .qmd/index.yml).
- README: "Automatic update commands" subsection — execution via bash -c in the
  collection's directory, run-then-reindex order, non-zero-exit abort, and the
  qmd collection update-cmd shortcut, with a sample qmd update trace.
- README: ignore documented as YAML-only and additive with the un-overridable
  built-in exclusions; XDG_CONFIG_HOME/QMD_CONFIG_DIR added to the env-var table;
  Model Configuration cross-references the models:/QMD_EMBED_MODEL override path.
- example-index.yml: rewritten into a fully-commented starter template where each
  collection demonstrates a distinct feature; models: left commented so it can't
  drift from the live defaults.

Every key verified against src/collections.ts and its consumers (src/cli/qmd.ts,
src/store.ts). example-index.yml parses as valid YAML. Companion to #715.
CLAUDE.md's command list had drifted from the CLI. Most notably it documented
`qmd update --pull` as "git pull first," but `--pull` is parsed and never consumed,
so it's a no-op — the same dead-code claim #715 removed from the README. This brings
CLAUDE.md in line and refreshes the rest of the cheat sheet.

- Fix --pull: point to the per-collection `update` hook (qmd collection update-cmd),
  the real mechanism that runs automatically on `qmd update`.
- Add commands that now exist: qmd init, qmd doctor, qmd bench.
- Add collection subcommands: show, update-cmd, include, exclude.
- Lead output options with --format <kind> (cli|json|csv|md|xml|files); keep the bare
  --json/--csv/--md/--xml/--files booleans noted as legacy aliases.
- Document `qmd get <file>[:from[:count]]` and add --intent, --no-rerank,
  --full-path, --no-line-numbers.

Cheat-sheet scope only: Bun guidance, the Do-NOT sections, Architecture, and
Releasing are untouched. Every command/flag verified against src/cli/qmd.ts.
`qmd doctor` could report a downloaded, working model as "invalid GGUF":

    ⚠ model cache: invalid 1: generation: hf:tobil/qmd-query-expansion-1.7B-gguf/...
      (.../qmd-query-expansion-1.7B-q4_k_m.gguf.etag: not valid GGUF
       (expected magic "GGUF", got "\"c38", 0 KB)). Next: run `qmd pull --refresh`

Root cause: `findCachedModelInspection()` scans the model-cache dir for every
entry whose name `includes(filename)` and inspects each as a GGUF. `qmd pull`
writes a `<filename>.etag` HTTP sidecar next to each blob, and that sidecar's
name also contains `filename`, so it gets inspected — and a ~67-byte etag has
no `GGUF` magic. The loop returns on the first *valid* GGUF it finds, so the
false positive only surfaces when `readdir` happens to yield the sidecar before
the blob; that's why it hits some models/platforms and not others, and why
`qmd pull --refresh` doesn't clear it. (It's also invisible to users who let
models download lazily, since only `qmd pull` writes the sidecar.)

Fix: skip `*.etag` entries in the scan. One line in `findCachedModelInspection`.

Adds a regression test that stages a valid blob plus a garbage `.etag` sidecar
and asserts `qmd doctor` reports the model valid. The test fails on the prior
code (reproduces `invalid 1: ... .etag: not valid GGUF`) and passes with the fix.
…Y_TIMEOUT

The initial 5s ceiling was sized for init-time DDL contention (sub-100ms work). On
multi-GB indexes, a single `embed` batch commit can outlast 5s, so an `update` or
`query` that races a long-running `embed` still hits `SQLITE_BUSY` on the first
write. Raise the default to 120000 ms, which outlasts the worst-case batch commit
observed on multi-GB / multi-tens-of-thousands-of-doc indexes.

Add `QMD_SQLITE_BUSY_TIMEOUT` (milliseconds) as an operator escape hatch. Unset,
empty, or unparseable values fall back to the default; `0` restores upstream
fail-fast behaviour for environments that prefer surfacing contention as an
error.

Tests expand from 3 to 6 cases: default value, multi-connection default,
env-override honored, env=0 fail-fast, garbage value falls back to default, and
the existing real-lock-contention timing check (unchanged).

Verified under both `bun:sqlite` and `better-sqlite3` runtimes; `tsc` clean.
Runtime detection only looked for lockfiles inside the package
directory. Package-manager installs never place one there: a bun global
install writes $BUN_INSTALL/install/global/bun.lock while the package
lands in .../install/global/node_modules/@tobilu/qmd/, so bun-installed
qmd always ran under whatever node was first on PATH. The native modules
were built by the installing runtime, so the next Node major upgrade
breaks every store-touching command with ERR_DLOPEN_FAILED
(NODE_MODULE_VERSION mismatch).

When the package directory has no lockfile, apply the same npm-priority
rule one level up, at the install root above node_modules/. npm and
Homebrew globals keep no lockfile at either level and still default to
node.

Adds regression tests covering the bun-global install-root layout and
install-root npm priority.
Add an optional plain-text "query" parameter to the MCP query tool,
mutually exclusive with the existing "searches" array. When "query" is
provided it is handed to the SDK auto-expansion path (expand -> RRF fuse
-> rerank) instead of requiring the caller to hand-author typed
lex/vec/hyde sub-queries.

"searches" becomes optional; the handler validates that exactly one of
query/searches is supplied and returns a clear error otherwise. The typed
"searches" path is unchanged, and snippet extraction uses the plain query
when present, else the first lex/vec sub-query as before.

This makes natural-language search the recommended default for MCP clients
while keeping precise typed control available.
rebuildFTSForCjkNormalization() loaded every active document body into
the JS heap via Statement.all() before inserting, inside a single
transaction that first cleared documents_fts. On a large index this
OOMs the process at store open, and a crash mid-rebuild leaves FTS
empty with the version marker unset, so every subsequent open retries
the same failing migration.

Stream rows with Statement.iterate() and commit in 500-row batches into
a documents_fts_rebuild shadow table, then atomically swap it into place
(legacy_alter_table=ON so the documents_ai/au/ad triggers re-resolve
against the renamed table). The live index keeps serving until the swap
succeeds; a crashed prior run leaves only the shadow table, which the
next open drops and rebuilds.

Adds Statement.iterate<T>() to the db wrapper interface and 6 regression
tests (OOM-safe open, crash recovery, CJK + Latin search, inactive-doc
exclusion, no-.all() structural).
ALTER TABLE ... RENAME triggers SQLite 3.25+ re-validation of every
trigger/view body that references the renamed table. The documents_ai
and documents_au triggers reference documents_fts by name; renaming
documents_fts_rebuild after DROP documents_fts fails with
"no such table: main.documents_fts" before the rename can complete.

Replace the RENAME path with:
  DELETE FROM documents_fts
  INSERT INTO documents_fts(...) SELECT ... FROM documents_fts_rebuild
  DROP TABLE documents_fts_rebuild

The live table and its triggers stay untouched until the transaction
commits. No PRAGMA legacy_alter_table workaround needed.

Update the structural test assertion to match (was checking for
"RENAME TO documents_fts", now checks for "INSERT INTO documents_fts").

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
qmd embed runs inside a session capped at a hardcoded 30 minutes. When the cap is
reached the remaining document batches are skipped and you re-run embed to pick up
where it left off. On a large corpus that turns one index into several supervised
restarts.

--timeout <minutes> lets you set the cap yourself, or pass 0 to remove it, so a big
index can finish in a single run. The default stays at 30 minutes and the
skip-and-resume behavior is unchanged when the cap is hit. Wired through the embed
CLI option, vectorIndex, and generateEmbeddings (new EmbedOptions.maxDurationMs),
with input validation and a unit test covering early stop.
The tool was renamed from structured_search to query (per CHANGELOG, at/before
2.1.0) but skills/qmd/references/mcp-setup.md was never updated. The binary and
skills/qmd/SKILL.md already use 'query'; this aligns the setup reference.

Refs #741
Concurrent processes opening the same index could crash during store
initialization with `trigger documents_ai already exists` or
`database is locked`, even with busy_timeout set.

The FTS sync triggers were dropped and recreated on every open as separate
autocommit statements, so two connections interleaved between the DROP and the
CREATE (A drops, B drops, A creates, B creates -> "already exists");
busy_timeout serialises individual statements but not the DROP/CREATE pair.
Separately, `PRAGMA journal_mode = WAL` needs a brief exclusive lock to migrate
a cold database and does not invoke the busy handler, so concurrent first opens
threw SQLITE_BUSY.

Gate the trigger rebuild behind PRAGMA user_version inside one IMMEDIATE
transaction with a double-checked read, so the DROP/CREATE pair is atomic
across connections and runs once per schema version. Move WAL setup into
openDatabase with a bounded retry within the busy-timeout budget, alongside
busy_timeout. Add a multi-process regression test (cold and existing database)
that fails before and passes after.
docs: rename MCP search tool heading `structured_search` -> `query`
docs: refresh stale command reference in CLAUDE.md
docs: document the index.yml configuration schema
feat(mcp): add --host flag (and QMD_HOST env) to the HTTP server
…lse-positive

fix(doctor): skip `.etag` sidecars when validating the model cache
fix(launcher): detect bun global installs via the install-root lockfile
fix(store): stream-batch CJK FTS rebuild to prevent OOM on large indexes
feat(mcp): expose plain query parameter for auto-expand search
feat(embed): add --timeout to override the 30-minute session cap
fix(db): guard SQLite store init against concurrent opens
…eady

fix(db): make concurrent store initialization safe
tobi and others added 4 commits June 24, 2026 04:49
- Resolve merge conflicts in package.json, src/store.ts, README.md,
  CHANGELOG.md against upstream/main (v2.6.3).
- Bump version to 2.6.3-ja.1; preserve qmd-ja name and ONNX/Vaporetto
  configuration throughout.
- Adopt upstream embed --timeout support via options?.maxDurationMs in
  generateEmbeddings().
- Fix upstream test incompatibilities with qmd-ja environment:
  - store-paths: skip Git Bash path tests on non-Windows (WSL2 compat)
  - store-cjk-fts / store-concurrency: import FTS_CJK_NORMALIZED_VERSION
    from src/store.js instead of hardcoding "1" (qmd-ja uses "3")
  - cli: comment out GGUF-specific doctor assertion (qmd-ja uses ONNX)
- Add qmd-ja-specific tests (34 tests across 2 new files):
  - test/providers.test.ts: unit tests for ONNX provider routing logic
  - test/store-ja-vaporetto.test.ts: Vaporetto tokenizer and Japanese
    BM25 search end-to-end tests
- Replace upstream title with qmd-ja focused heading
- Add architecture diagram note: Vaporetto tokenizer and Japanese models
- Update What's different table to include model-level differences
- Add Japanese Optimization section (Vaporetto WASM + recommended models)
- Update generate model to adsholoko/qmd-query-expansion-ja in Japanese config examples
- Simplify installation note to a single upstream reference line
@koz-oda koz-oda merged commit 51fdd09 into main Jun 29, 2026
5 of 8 checks passed
@koz-oda koz-oda deleted the upstream-2.6.3-sync branch June 29, 2026 12:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.