SQLite Index Builder
Build or refresh .knowledge/knowledge.sqlite from portable knowledge artifacts. Markdown and JSONL remain canonical; SQLite is derived local state for FTS5, embeddings, and graph traversal.
Input Contract
Discover the knowledge root from the user-provided path, the current directory, or the conventional knowledge-system/, knowledge-base/, and knowledge-artifacts/ directories. The indexer consumes these files when present:
_knowledge/catalog.jsonl
_rag/chunks.jsonl # or rag/chunks.jsonl
_graph/nodes.jsonl # or graph/nodes.jsonl
_graph/edges.jsonl # or graph/edges.jsonl
Markdown is indexed through each catalog record's path; the indexer does not crawl arbitrary *.md files. If Markdown exists without _knowledge/catalog.jsonl, use knowledge:knowledge-base-builder to create the catalog before indexing. If graph or RAG JSONL must be produced or repaired, use knowledge:knowledge-graph-builder or knowledge:rag-corpus-builder first.
Build
Inspect the discovered inputs and state which artifact types are present or missing.
Choose the embedding provider:
hashis the dependency-free default. It provides deterministic lexical feature vectors, not semantic-model embeddings, and search ranks full-text matches ahead of its vector similarity. Report this when the user expects semantic recall; the build result carriesembedding_quality: lexical-baselineand a matching notice.ollamaprovides semantic embeddings when a local Ollama endpoint and model already exist. Do not silently install Ollama or pull a model.
The provider is the largest single retrieval lever, and it is usually the wrong one to leave for last. Measured on a 94-question vault with the engine and corpus held fixed, swapping
hashforembeddinggemmamoved hits 43 → 62, recall@10 0.631 → 0.803, and MRR 0.558 → 0.791 — more than the previous six engine versions combined. Paraphrased and operator-phrased questions are the oneshashcannot reach at all, so when eval misses cluster there, raise the provider before proposing vocabulary edits.Report
embedding_promptfrom the build result.embeddinggemmais trained for asymmetric retrieval and the indexer applies its query and document instructions; an index that reportsnoneunder that model predates the prompts and must be rebuilt to get them.Report
documents_windowedas well. It counts notes too long for the model's context window, which are embedded in overlapping windows and pooled rather than truncated. A high count is not a failure, but it means the corpus is chunked more coarsely than the model can read in one pass;knowledge:rag-corpus-builderis where that is fixed, not--embed-chars.When the
knowledge-localMCP server is available, callknowledge_statusand compare its reported root with the desired knowledge root. If they resolve to the same path, callknowledge_indexwith the selected provider and model; the MCP tool does not accept a per-call root. If the server is unavailable or points at a different root, run the bundled CLI with an explicit--rootrelative to the plugin root:
node --no-warnings "${CLAUDE_PLUGIN_ROOT}/scripts/sqlite-knowledge.mjs" index \
--root /path/to/vault \
--provider hash
For an existing local Ollama model:
node --no-warnings "${CLAUDE_PLUGIN_ROOT}/scripts/sqlite-knowledge.mjs" index \
--root /path/to/vault \
--provider ollama \
--model embeddinggemma
If CLAUDE_PLUGIN_ROOT is unavailable, resolve ../../scripts/sqlite-knowledge.mjs from this SKILL.md.
The indexer requires Node 24+, or Node 22.5-23 with --experimental-sqlite and an FTS5-enabled SQLite build. On No such built-in module: node:sqlite or no such module: fts5, switch to the Docker path in the local SQLite reference instead of reporting the vault as unindexable.
The index operation rebuilds the SQLite schema and contents from the current source artifacts; it is not a live file sync. It is, however, incremental where it costs: a document whose embedded text is unchanged reuses its stored vector, so a rebuild after editing three notes embeds three documents, not the corpus. Everything else is rebuilt from scratch, so a deleted or renamed note cannot leave a stale row behind — the failure mode a partial reindex has and this does not.
Report embeddings_reused and embeddings_computed. A rebuild that recomputes everything when little changed means the cache was rejected: the provider, model, or prompt template differs from the existing index, or the schema version moved. That is correct behavior, not a fault — but say which it was rather than reporting a slow build as normal. Pass --no-reuse-embeddings to force a cold rebuild when a measurement needs one.
Verify
After indexing:
- Run
knowledge_status, or the CLIstatus --root /path/to/vault. - Require the database to exist and report
stale: false. - Compare the returned note, chunk, node, and edge counts with the discovered inputs. Explain legitimately absent artifact types instead of treating every zero as success.
- When notes exist, run one bounded search smoke test using a term that appears verbatim in a known note title or alias, and confirm that note is returned with
lexical_match: true. A smoke test whose results are alllexical_match: falseindicates a ranking or tokenization problem, not a passing build. When graph nodes and edges exist, run one neighbor lookup for a known node. - When
_knowledge/questions.jsonlexists, runeval --root /path/to/vault --k 10and reportmrr,recall_at_k, andmean_distinct_notes. Amean_distinct_noteswell below k means chunks of one note are crowding the top slots; results are grouped per note by default, so this should only happen with--group none. Any competency question whose required notes are missing from the top k is a retrieval defect: name the question rather than reporting the build as clean, and follow the retrieval repair loop below instead of accepting the score. - Report the database path, source root, indexed counts, embedding provider/model/dimensions, fingerprint, and freshness.
Retrieval Repair Loop
Run this when eval reports missed questions. It is a measurement loop, not an editing loop: the danger is not a failed question, it is a fix that scores well because it was tuned against the same questions used to judge it.
- Split before touching anything.
eval --split holdoutscores the reserved third of the question set;--split devscores the rest. Buckets are derived from each question id, so they are stable across runs and cannot drift while vocabulary is being edited. Repair againstdevonly, and record the holdout number first — a holdout measured after the repair proves nothing. - Read
repair_targetsbefore proposing an edit. Each entry names an unretrieved required note, how many questions it blocks, and itsgap:missing-notemeans the note is absent from the catalog and this is an extraction job forknowledge:knowledge-base-builder;no-lookup-vocabularymeans the note carries no aliases, user terms, or source symbols;rankingmeans the vocabulary exists and something else is outranking it. Start with the note blocking the most questions, not the note that is easiest to edit. When a comparison question retrieves its relation note but none of the sides, check the relation record'sparticipantsbefore touching vocabulary: search pulls declared participants in on its own, so an undeclared side is a catalog defect, not a lookup one. - Ground each added term in the repo, not in the question set. See the vocabulary-bridge repair rules in answerability-contract.md.
- Re-index and re-score after each change, against the saved run. One edit per measurement; a batch of edits cannot be attributed or reverted.
- Sweep the fusion split before accepting a semantic provider's losses. With a real
embedding the default is
semantic 0.7 / lexical 0.3, which is a guess. Questions that quote an exact screen label back at the index are the ones it loses: meaning similarity dilutes an exact term match.eval --sweep 0.3,0.4,0.5scores every weight in one pass without re-indexing, comparing each against the first weight per question. Judge a sweep on the whole question set, not on the label questions that motivated it, and treatdecisive: falseas a tie — adopting a winner that cannot be separated from the reference is how a guess becomes a default.
node --no-warnings "${CLAUDE_PLUGIN_ROOT}/scripts/sqlite-knowledge.mjs" eval \
--root /path/to/vault --split dev --k 10 > /tmp/before.json
# edit vocabulary, then index again
node --no-warnings "${CLAUDE_PLUGIN_ROOT}/scripts/sqlite-knowledge.mjs" eval \
--root /path/to/vault --split dev --k 10 --baseline /tmp/before.json
# fusion sweep — one pass, no re-index, shared query embeddings
node --no-warnings "${CLAUDE_PLUGIN_ROOT}/scripts/sqlite-knowledge.mjs" eval \
--root /path/to/vault --split dev --k 10 --sweep 0.3,0.4,0.5
The baseline block reports improvements, regressions, and a verdict. Revert on any
regression, even when recall_at_k rose. Competency sets are small enough that one question
moves recall by several points, so an aggregate gain routinely hides a question that stopped
working; regressions names it. This rule scopes to vocabulary and catalog edits at a fixed engine and provider, where a
regression means the edit broke something. A provider or engine change is judged differently:
it moves every question at once, so weigh net movement and holdout together and name the
regressions rather than reverting on their existence. Only after dev is stable, score
--split holdout once and report both numbers. A holdout that did not move means the repair generalized to nothing —
report that plainly instead of citing the dev gain.
- Measure a reranker's ceiling before attaching one. A cross-encoder can only reorder what
retrieval already returned, so its maximum gain is
recall@50 − recall@10. Scoreeval --k 10andeval --k 50on the same index: required notes missing at both depths are a retrieval or catalog problem that no reranker can fix. When the headroom is real and the user has an endpoint, pass--reranker-urland re-score — and record in the report that the run had a reranker attached, because it is not comparable to one without.
Do not cite the SQLite file as source evidence and do not commit it merely to share knowledge. Commit or synchronize the canonical Markdown and JSONL instead. Do not modify source artifacts during an index-only request.
Read the local SQLite reference when Docker operation, MCP routing, Ollama configuration, or failure recovery is needed.
Related Skills
knowledge:knowledge-base-builder- create the Markdown catalog that controls note inclusion.knowledge:knowledge-graph-builder- create or repair graph node and edge JSONL.knowledge:rag-corpus-builder- create or repair retrieval chunk JSONL.knowledge:knowledge-query- query and cite evidence from an existing index after the build.