How it works
- Resolve. Every lockfile in the project directory, and in monorepo roots above it for ecosystems not found yet, becomes a list of exact
(ecosystem, name, version)triples. Direct dependencies are marked. - Locate. Each package's files are found on disk (see Ecosystems). If the installed version differs from the lockfile, the answer says so and shows the installed one; with fetching enabled it reads the pinned version instead.
- Extract. Markdown and reStructuredText are split into heading-scoped sections (long ones at paragraph boundaries, never inside code fences). HTML headings in MDX (
<h3 id="…">@import</h3>) count as headings, andexport const titlenames the page. Docs pages written as React components (Tailwind's installation guides) become sections too: step titles are headings, JSX text is prose, andcode:strings are code blocks. Source files are parsed with tree-sitter (TypeScript, Python, Rust, Go) into symbols with a signature (the declaration without its body), a cleaned doc comment, a qualified path and a line number. - Index. Each entry becomes weighted terms: names and headings count most, doc prose next, signatures and code examples least. The tokenizer keeps identifiers whole and also splits camelCase, snake_case and digits; a light stemmer (plurals, -ing, -ed, -ation/-ate, -ability/-able) and a short programming synonym table (dict/dump, parse/validate, parameter/param, JavaScript/JS, ...) bridge wording. The index is cached on disk per package, version and source location, so a version is indexed once.
- Rank. BM25 over the full text, BM25 over just the heading (or name) and first sentence, so a long body does not bury what an entry says it is, each normalized to its best hit. Then signals that matter for docs: entries whose name or heading contains a query word, identifiers written in the question (
model_dump,z.object) and plain question words that name a documented top-level API of the package ("run code after the response" in Next.js, which exportsafter), changelog sections only when the question is about changes, and lower weight for private modules, bundled tooling, legacy copies, upgrade guides to an older major than yours ("Migrating to v6" in ESLint 9) and pages titled "(Deprecated)". The result is the keyword ranking. When one entry clearly leads (at least twice the runner-up) it is the answer and the embedding is not consulted. Otherwise (package files only) an embedding ranking (a static model2vec model: a text's vector is the mean of its WordPiece token vectors) is fused in by weighted reciprocal rank, 90% keyword and 10% embedding: the embedding breaks near-ties and rescues paraphrases, but cannot outvote an exact API-name or heading match. With upstream docs indexed, answers are prose pages and the normalized scores are mixed instead (45% embedding). A query is embedded by reading only its own tokens' rows from the model file, so the 32 MB weight table never enters memory. Generic headings such as Parameters, Returns or Examples take their topic from the heading above them. Deprecation notes among the top results ("usemodel_validateinstead") lift the API they point to. - Pack. The top result also quotes the first paragraph of its page and parent section (with the short list or code block that follows), when they add something: a deprecation notice at the top of the page, or the setup a subsection builds on. Results are added in rank order until the token budget is spent (the last one trimmed at a line boundary), each with a
package@version path:linecitation.
api is exact rather than ranked: it finds entries whose name matches the last segment of the symbol, scores qualifiers (Router in axum::Router::route), follows re-exports, and adds overloads, members and other matches.
Shared mechanics (lexical tokenization, model download/loading, quantization, cache-root selection and the one-time CLI star hint) come from sylphx-mcp-kit 0.3. lockdocs keeps its model choice, identifier input policy, ranking and retention. Existing model files and persisted embedding indexes remain readable in the same LOCKDOCS_CACHE directory; no rebuild or migration is needed.