qmd ("Query Markup Documents") and Wax look like twins — local‑first, SQLite‑backed, BM25+vector hybrid, MCP servers. They're not. They solve adjacent but different problems, and that difference is exactly why one can sit on top of the other.
../qmd
Wax · Swift
qmd claims carry file:line citations into its source; Wax claims keep the amber cites from Part 1.
Almost every trade‑off below follows from two structural facts.
qmd's SQLite (~/.cache/qmd/index.sqlite) is a disposable index over files‑on‑disk. Collections are dirs+globs; update --pull does a git pull then re‑indexes; content dedups by hash. The files are the truth, the DB is rebuildable.
Wax's .wax is the truth — a transactional, crash‑safe store (WAL, dual headers, Merkle).
qmd's value‑add is ~2 GB of GGUF models driven from TS via node-llama-cpp: embeddinggemma‑300M, qwen3‑reranker‑0.6B, a finetuned 1.7B query‑expander. llm.ts:252–255
Wax stays CoreML‑light (MiniLM‑384) with no LLM in the loop.
keep qmd's brain (chunking, expansion, rerank) and swap its storage/retrieval for Wax. Whether that's wise depends entirely on which feature and which build strategy — the rest of this page.
Grounded in qmd's source, not its README.
| Dimension | qmd | Wax |
|---|---|---|
| Language / runtime | TypeScript · Node ≥22 / Bun | Swift 6 · Apple‑Silicon‑native |
| Storage | Plain SQLite + FTS5 + sqlite-vec db.ts | Custom .wax container; SQLite embedded in a frame |
| Source of truth | The files on disk (index is a cache) | The .wax file itself |
| Text search | FTS5 / BM25 (+ CJK normalization) store.ts:763 | FTS5 / BM25 |
| Vector search | vec0 cosine, embeddinggemma‑300M GGUF store.ts:1169 | Metal brute‑force cosine, MiniLM‑384 |
| Fusion | RRF k=60 + 2× original + rank bonuses + position‑aware blend store.ts:3807,3841 | RRF k=60 + query‑type adaptive weights |
| LLM in loop | Yes — query expansion + reranker | No |
| Structured / EAV | None — flat documents+chunks | Bitemporal EAV graph |
| Sync model | Sync the files, rebuild cache (git‑native) | Sync binary (risky) or the Markdown projection |
| Transparency | sqlite3‑openable; --explain score traces | Opaque (DB inside a frame); stats + query tools |
Shared DNA: local‑first, single‑SQLite‑backed, BM25+vector hybrid with the same RRF constant (k=60), and an MCP server with stdio+HTTP. The divergence is everything in the bottom four rows.
The real hybridQuery store.ts:4496. Click a stage.
Wax natively does only stages 3–4 (hybrid + RRF). Stages 2 (expand) and 5–7 (rerank+blend) are pure pre/post‑retrieval — which is precisely why they port onto any backend.
Tap a feature → how cleanly it sits on Wax‑as‑storage, and what you inherit free vs. build yourself.
✅ clean (no Wax change) · 🔧 workable (you own glue) · ⚠️ redundant/partial · ❌ fights Wax
The embedder swap is the fork in the road.
Wax's daemon/MCP search uses Wax's own MiniLM embedder — there's no exposed "search with my precomputed vector." So if you want qmd's multilingual embeddinggemma, you must go native Swift (plug a custom EmbeddingProvider + putEmbeddingBatch) — which means rewriting qmd's ~15k‑line pipeline. Stay in TS to reuse qmd's code and you inherit MiniLM‑384‑en. There's no cheap middle.
setCustomSQLite() to Homebrew's libsqlite3 because Apple's SQLite is built OMIT_LOAD_EXTENSION, else "vector search won't work." db.ts:25–100 Wax bundles its engines — that class of failure vanishes.--explain raw scores, llm_cache as a table."qmd, but native" is a poor trade — qmd's architecture already fits its job; you'd rewrite a lot for marginal gain and re‑add weight. The one combination worth building is the inverse: not "qmd on Wax storage" but qmd's retrieval feeding Wax's structured memory. Use qmd‑style smart chunking + rerank to find the right passages, then distill them into Wax EAV facts (with sm_evidence pointing back at the source span) and durable, bitemporal memory. The backend‑agnostic pieces — rerank and query expansion — drop in cleanly and are the layer to build first.