Architecture
Rift works as a toolkit for agents such as Claude Code and Codex - practically any agentic appliance that supports MCP.
When an agent has Rift configured as one of its MCP servers, the rift mcp process starts and
checks whether a rift server is already running for that particular folder. We'll call this
folder a workspace throughout this documentation.
Each rift mcp process is short-lived and isolated, while development operations depend on
state shared across requests and agents. To bridge that gap, rift mcp acts as a small proxy: it
forwards every request to one rift server running locally for the workspace. The two processes
communicate over Streamable
HTTP.
A foreground repository server can hold several workspaces from the same Git repository.
rift mcp adopts it when the workspace accepts its settings; automatic startup continues to
start a workspace server. Each workspace retains its own index, configuration, watcher, and databases.
The rift server coordinates source discovery and providers. Source
resolvers identify project files, dependencies, and generated code. Providers analyze that source
and produce facts such as symbols, syntax nodes, relationships, and diagnostics.
The server stores searchable provider output in an embedded index under .rift. The
search tool always retrieves lexical matches; when
rift.toml selects an embedding model, a local model adds vector ranking.
Server lifecycle
Since v0.0.11When the server starts, it writes .rift/server.json following the
ServerLock model. The file gives each rift mcp
process the port and token it needs to reach that server. For example:
{
"port": 12345,
"token": "Zk3mQ8xW1vY5uT9rN2bH7cJ4dL6fP0sA8gE5iK2oM7q",
"pid": 4242,
"identity": {
"version": "0.0.45+b006b8433ba06679f06a3c7f0743d65634d32c34",
"schema_digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}
}When no live server stands behind the file, rift mcp starts one. An exclusive file lock
elects one starter if several processes race; the winner binds a port and publishes the file,
and the rest connect to it. The winner holds the lock from its election onward, while its
index prepares source in the background.
The proxy adopts that server only when its identity matches the proxy's own: the package version with the git commit the binary was built from, and the served MCP schema's SHA-256. Requests therefore never cross between binaries or tool schemas that interpret them differently.
When the identities differ, the version decides:
- A server of an older version is asked to stop, and the proxy starts one from its own binary once the election releases. Every release bumps the version, so the first request after an update moves the workspace to the new binary.
- A server of the same version from another build, or of a newer version, refuses each request, so two development builds never displace each other and an older binary never displaces a newer server. Stopping the running server lets the next request start one that matches.
A reader that cannot reach the recorded port treats the file as stale. The election lock releases with its holder, so a stale file never has a live server behind it, and the next starter replaces the file. A server on its way out closes its port before it releases the lock, and the next starter waits for the release before electing a fresh one.
To enforce a cross-process boundary between OS users, the server persists a token in
.rift/server.json, which only the file's owner can read. Every request carries this token,
and the server rejects one without it. Processes running as the same OS user can read the same
token and are not separated by this boundary.
A browser introduces another route to a localhost port: a remote page can issue requests through
DNS rebinding or a forged origin. The HTTP server therefore accepts only the literal loopback
Host for its bound port. It also rejects a present Origin unless that origin is loopback;
non-browser MCP clients omit Origin and still authenticate with the token.
You might be asking why there's a server at all. Assume your agent launches a couple of
subagents. Each of them might also want to read the codebase through Rift, and each gets
its own rift mcp, because MCP's stdio transport has the client launch its server as a
subprocess.
None of those short-lived processes can hold the things that are expensive and shared - the
index - so a long-lived server holds them and every rift mcp
reaches the same one.
And why HTTP rather than a socket? Either way rift mcp has to find the running server, and
HTTP lets an HTTP-capable agent connect to the port without rift mcp.
On startup the server reads rift.toml from the workspace root and accepts the workspace. The
configuration allows you to describe:
- which embedding model, if any, ranks search
- which exact language entries select source files
A change to the file takes effect for later requests once the server accepts it again. An invalid file keeps the workspace unusable until you fix it.
A rebuild that fails - the tree crossed a configured bound, a file could not be read - leaves the
last published snapshot served. A read answers from that snapshot and carries a stale_index
warning naming the failure. The next filesystem
event or background validation retries the rebuild. A successful rebuild clears the warning,
including recovery after the tree returns to the publication's exact bytes.
The server stops itself once no request remains active and the
idle_timeout span passes after the last response completes.
rift server stop ends it sooner. The server removes its lock file on the way out, and the next
rift mcp starts a fresh one. The databases under .rift survive the exit.
A served workspace keeps three databases under .rift:
- the index database,
.rift/index, holds the lexical indexsearchranks from - the vectors database,
.rift/vectors, holds the vector ranking's rows. The server creates it at the first vector operation, and never while vector ranking is disabled - the metrics database,
.rift/metrics, holds the server's own log records; under repository serving, each workspace's metrics database holds the records that name that workspace, and a record that names none reaches stderr alone
Each database has its own connections and its own writes, so a long index write delays no log
record and no other database's write. rift server logs reads the metrics database alone,
with or without a running server, and answers while the index database is locked, missing, or
refused.
rift server status reports which of these states the workspace is in - a serving server with
its port, pid, and version, a server still indexing, a stale .rift/server.json, or no server
- and changes nothing. After taking its election and opening storage, a server that fails to
start, or panics, records why under
rift server logs --level errorbefore it exits. A start refused by the election reports its error on standard error. A serverrift server startspawned writes its standard error to.rift/server.stderr, truncated at each start.
Source discovery
Before Rift analyzes code, source resolvers discover what belongs to the project and which packages it depends on. The built-in project resolver catalogs workspace files.
Dependency resolvers read what the workspace's package managers resolved. Each resolver claims one manifest file name, and every visible manifest with that name reaches it:
- the Cargo resolver claims
Cargo.tomland reads theCargo.lockbeside it - the uv resolver claims
pyproject.tomland reads theuv.lockbeside it - the npm resolver claims
package.jsonand reads thepackage-lock.jsonbeside it - the Bun resolver claims
package.jsontoo and readsbun.lockthe same way, so a manifest reaches whichever of the two has a lockfile beside it
A resolver reads manifests and lockfiles, never a package's source, and it never downloads a package or writes to the tree. What the resolvers read becomes the dependency context, and a read that needs a package's declarations asks the global index for them.
Each resolver reads at most 256 manifests per workspace, taken in path order from the visible paths carrying its manifest file name. Selecting the top-level manifests compares every manifest pair, so that work grows with the square of the manifest count.
A workspace holding more manifests names the packages those 256 declare, and the resolver
reports the rest as a degradation. A read whose scope reaches packages carries it as a
package_context_degraded warning: the next.js repository holds 876 package.json files, so
the warning reads 620 of 876 package.json manifests were not read: at most 256 are read per workspace.
Each readable project file becomes a source unit in the source catalog, named by a
SourceUnitId such as
rift://source/project/src/lib.rs; a package file the global index answers from takes the same
form. Its
location says whether it belongs to the project, a dependency, the standard library, or an
external source. Its source_kind separately says whether a human authored it or a tool
generated it.
Generated project code and generated dependency code therefore keep both facts. Declarations
invented by a language tool have source_kind: "synthetic" and no source unit.
The project resolver publishes a new catalog revision when workspace files or generated-source metadata change. Fact providers consume that catalog. A parser such as tree-sitter can describe syntax in a supplied file, but cannot infer which package owns it or which generator produced it.
Dependency context
Since v0.0.39 · Rust, Python, TypeScriptThe dependency context lists the packages the workspace depends on. The resolvers reach the workspace through a reader that offers a file read and a directory listing and nothing else, so they run no toolchain and read no environment value.
Each entry carries the package manager, the name, and exactly one selector: the exact
version a lockfile pins, or the version requirement a manifest declares. A manifest value
is reported as a requirement unless its form pins one exact version - Cargo's 1.2.3 is a
caret requirement and =1.2.3 a pin - so a manifest-only project never invents a resolved
version. Entries merge by manager, name, and selector, and a requirement is dropped where some
input pins that package's version. The
[dependencies] packages list merges with
what the files state, under the same rules.
rift://map lists this context. The server reads it when it builds the index, and again when a
change touches a manifest, lockfile, or version pin it read, or when a language with a standard
library gains its first visible file or loses its last. Every other file change leaves it
untouched.
A uv sync that installs from an unchanged uv.lock changes no visible file, since .venv
holds its own .gitignore. The context therefore keeps the site-packages listing of each
project environment it read beside a uv.lock, and each request lists it again: a listing that
differs reads the context again before the request is answered.
An entry's availability says whether a public registry serves the package. An entry
crates.io, npm, or the Python Package Index serves is canonical, as is every standard library
entry, and the global index answers for it. Every other source has a value of its own, and no
global package index answers for it:
path- a path outside the workspace, or an archive at a path, such ashelper = { path = "../helper" }git- a git repositoryprivate_registry- a registry other than the public one its package manager readsurl- a URL no registry serves, such as a uv wheel URL or an npm tarball URL
A path dependency inside the workspace, such as a Cargo workspace member or an npm workspaces
member, is project source. The local index already covers it, so the context names no entry for
it.
Standard libraries
Since v0.0.46 · Rust, JavaScript, TypeScript, PythonCode in every language calls its standard library, and no manifest names it. The context
therefore adds one entry for each standard library a language the workspace uses relies on, under
the manager stdlib. A language counts when the
[languages] policy selects it for at least one
visible file, so a Rust repository holding one tool.py names stdlib/python too:
| Language | Entries | Version, in the order it is read |
|---|---|---|
| Rust | stdlib/rust | rustc --version, then the channel rust-toolchain.toml or rust-toolchain names |
| JavaScript, TypeScript | stdlib/node, npm typescript | .nvmrc, .node-version, volta.node in package.json, .tool-versions, mise.toml, then node --version, then engines.node |
| Python | stdlib/python | the project environment's .venv/pyvenv.cfg, then .python-version, then requires-python in pyproject.toml |
The npm typescript package ships the lib.*.d.ts files that declare the ECMAScript built-ins,
such as Array and Promise. It goes out at the version the lockfile pins, and otherwise as the
requirement >=0, which the global index answers from its newest collected release.
A whole version goes out exact and a partial one as a requirement: 1.98 in
rust-toolchain.toml goes out as ~1.98, and 3.12 in .python-version as ==3.12.*. A named
channel or alias, such as stable or lts/iron, goes out as >=0, and so does a library no
source names.
rustc --version and node --version run under
[dependencies] resolution = "auto" alone, each
bounded by command_timeout, and neither installs a toolchain a pin names but the machine lacks.
A probe that answers nothing leaves the static reading and a package_context_degraded warning
naming the entry. When rust-toolchain.toml pins 1.80.0 and the machine lacks that
toolchain, the entry goes out as stdlib/rust@1.80.0, and the warning carries
rustc --version: error: toolchain '1.80.0-aarch64-apple-darwin' is not installed.
The [languages.<name>] stdlib key turns a
language's entries off. The entries join the request to the global index alone: nothing is
installed, downloaded, or written.
Global index
Since v0.0.43 · Rust, Python, TypeScriptA package's declarations come from the global index, which holds facts for
collected OSS package versions. A global search reaches collected package and runtime facts selected by the dependency
context. Explicit packages replaces versions of named packages or adds packages the context
lacks; an omitted list keeps the snapshot context unchanged. An all search merges global results with the
primary project and accepted extra projects. A package declaration is a
Symbol like any other, and its returned id
selects one exact get_symbol read.
The local server analyzes no package source itself. Package analysis describes how one package's files become the declarations the global index answers from.
When global access is disabled or the global API cannot answer, the read answers the project hits
it has, and one typed warning names why package facts are missing. A package the global
publication holds no release of answers package_absent or package_requirement_absent, a
version it lacks answers from the nearest collected release under package_substituted, and an
entry no public registry serves answers package_unavailable. The
MCP reference states each warning.
Providers
Providers analyze cataloged source at different depths and publish immutable Contribution
records in provider-owned symbol spaces. Rift normalizes applicable Contributions into
workspace-local SymbolRecord values, then assembles readable Symbol values for one captured
index revision.
A condition the caller must weigh before relying on the answer - a stale index, an absent
capability - arrives as a typed entry in the result's warnings.
Syntax
Rift ships syntax providers based on tree-sitter by default. They parse readable project, dependency, and generated units into declarations, syntax structure, and parse diagnostics. Project declarations also carry their syntax nodes and byte ranges.
The server invalidates a changed path's syntax facts and search-index entries.
Language engines
A language engine resolves facts the grammar alone cannot provide, such as references to a declaration in another file. Rift's LSP client requests references, call hierarchy, and diagnostics over LSP.
Each [languages.<identity>] table selects
one named or inline process for an exact language identity: rust-analyzer for rust, or
typescript-language-server for typescript and typescript:tsx. Several identities may
select the same named process. LSP profiles register
directories as separate projects, each with its own configuration and language processes.
search uses the engine for relationship traversal: the
engine is the one source of the references an incoming walk follows and of the calls an outgoing
walk follows. Source and configuration must match the captured index before and after the engine
request.
Before a running engine's next request, the server sends it each publication's changed files
that match a file watcher the engine registered, as one workspace/didChangeWatchedFiles
notification. An engine that learns of file changes through its client, as rust-analyzer does,
then answers from the published source, including files the walk never opens.
The embedded ty engine serves Python over an in-memory transport through the same LSP session contract. Its version ships with the Rift binary.
History
The history source resolver reads the workspace's git objects in place, with no checkout or git
subprocess. That access serves revision-addressed reads: get_symbol, search, and nodes accept
a rev, and the server indexes that commit's tree - the same syntax providers, over committed
bytes - without touching the working tree.
A change comparison reads the same objects: the server lists the paths two sides hold differently, parses those paths alone on each side, and matches their declarations. Since v0.0.46 Against the working tree, the head side is the current publication, and the base side reads each changed file in the form the working tree holds it.
Since v0.0.46Walking git on every read costs more the further back it reaches, so the server keeps a history
store: tables derived from git and filled before any read asks. For each commit the
[providers.history] strategy selects, the store holds
the commit's author, time, and message, the paths it changed, the files it renamed without a
changed byte, and the declarations whose shape changed. Git stays the source of truth, and a
stored commit id names the git object it came from.
One store serves every worktree of a repository. It lives in the .rift/ folder of the common git
directory, .git/.rift/ in a plain checkout, so worktrees analyze the commits they share once; when
that directory refuses writes, the store sits in the worktree's own .rift/ folder instead.
A history task fills the store. The server spawns it at start, and it analyzes newest first, so
recent history answers soon, writing bounded batches at the share of one core cpu_share sets.
When HEAD moves, the task analyzes only the commits the store lacks and deletes the ones
outside every worktree's newest max_revisions commits.
No request waits for the task. A symbol timeline and a
commit search answer from what the store holds: a timeline whose
served revision the store has not analyzed yet answers complete: false, and a commit search
answered before the fill lands carries a history_store_filling warning naming how far it has got.
The task follows a declaration across files as it analyzes each commit. A deletion and an addition carrying one blob id are a rename, and a declaration an added file holds with the exact bytes a deleted file held is a move; a blob id or a declaration with more than one candidate pairs nothing, since no evidence says which addition answers which deletion.
Custom
Custom integrations implement a source resolver, a
fact provider, or both. The contracts stay separate even
when one integration implements both capabilities. A family with no serving provider surfaces
as a typed entry in warnings, which keeps an empty answer distinct from a complete answer with
no facts.
One integration can connect several cooperating pieces through a provider composition. Like a scikit-learn pipeline, the composition gives each step a stable name and feeds its output into compatible inputs.
The composition API can change the flow before Rift starts:
- insert, replace, or remove a named step
- transform one step's input or output
- branch one output into several providers, then publish each provider's Contributions
- nest one composition inside another
Provider compositions support several flow shapes:
- syntax and semantics consume the same source catalog, then publish separate Contributions
- history maps syntax analysis over source catalogs keyed by Git revision, then aggregates revision-qualified facts
- a custom external step can produce generated source or facts for another provider to analyze
A transform that changes source units or facts becomes a named, revisioned step and publishes new Contributions, so later answers still identify its inputs.
For example, a CSS integration can send templates and configuration to a registered Tailwind CLI, parse its generated CSS with a CSS syntax provider, and publish those facts beside authored CSS. The server runs the command with declared inputs, environment, timeout, and output limits; the composition step decodes its output.
Rift validates the composition's inputs, outputs, merge rules, and cycles before it runs. The server
then owns scheduling, caching, and invalidation; MCP requests do not assemble or modify the
composition. Composition controls data flow and provider selection. It cannot establish
Contribution equivalence or SymbolRecord identity.
The workspace resource assigns each provider a stable
ProviderId and reports its lifecycle state. A
provider derives an immutable Contribution collection from one tree revision, then publishes the
collection atomically.
While a provider catches up after a change, requests may serve its previous revision, and the
result carries a stale_index entry in warnings naming the two tree revision digests. A
provider with no usable revision contributes nothing, and the result's warnings say so.
A read captures one tree revision, one index revision where it searches, and the provider
revisions it uses. Later writes create new revisions; they do not alter the captured result. A
request addresses one page with page_index, and a page past the end returns an empty page
with the true total_pages.
Normalization validates equivalence evidence without using provider order, candidate names, or
provider composition as identity evidence. It retains unresolved and conflicting records instead
of forcing one SymbolId. When Rift assembles a readable Symbol, provider order selects scalar
presentation fields and combines list facts without discarding conflicting Contributions.
Index
Since v0.0.8 · Rust, JavaScript, TypeScript, Markdown, JSON, YAML, TOML, PythonThe server keeps an immutable index over the project's source. Tree-sitter extracts declarations and syntax nodes from the files the workspace's source policy includes.
Since v0.0.47The server starts with an empty publication, then publishes discovered paths and prepared files
as indexing advances. Reads answer from prepared source and carry
local_index_preparing while their selected local scope is
incomplete. A nodes request can read its supplied file ahead of background preparation.
Every publication owns its preparation counts and map together, so progress describes the answer the caller received. A tree change can reduce those counts; completion applies to the current selection of files.
The server watches the workspace and validates the source each current-tree tool read uses, so an answer never combines state from two index revisions. The implementation page describes the watcher, the rebuild lane, and the swap that keep this true.
When the published index lags the tree a read captured, the result says so: a stale_index
entry in warnings carries the digests of the tree revision the index covers and the revision
the read captured, so the caller knows the answer may miss the newest writes.
Each rebuild derives lexical units for search.
The rebuild derives one unit per indexed symbol, and chunks from every bounded UTF-8 file without
a NUL byte whose effective language entry or
[search.text].include selects it. Visible Markdown, MDX,
reStructuredText, and notebook documentation also enter collection. Syntax providers enrich files
their effective language entry selects. The units persist in the index database at
.rift/index, stamped with the tree revision that published them.
The rebuilt index becomes current as soon as its syntax index is built, and the units are written behind that publication. The store records which bytes each file's rows were derived from, so a write, startup's included, touches only the files that moved; the first load of a large workspace still runs for longer than a read is willing to wait, so the publication never waits for it.
A search that captures the publication before its transaction lands is ranked by identifier
matching alone and carries a lexical_ranking_unavailable warning naming the tree revision the
store is still committing; the store's ranking joins the answer once the transaction ends. A
transaction that refuses leaves the previous rows and stamp intact and is recorded in
rift server logs, and the next publication compares every file with
what the store recorded rather than applying its change on top of rows the store missed.
The vector ranking's vectors live in the vectors database and are not part of that transaction.
Embedding one declaration can cost
more than a read is willing to wait, so vectors are refreshed after the index publishes, and a
search whose vectors are still being written ranks lexically and reports the tier's readiness in
warnings.
Both tiers answer under one tree revision: the full-text store reads its stamp and its rows in
one transaction, and the vectors a pass published carry the tree they were described for. A store
that has moved past the tree a request captured returns nothing, and the request captures the
publication the store already answers for. What lexical_ranking_unavailable tells a caller
about is a store that has not yet committed the captured tree, one that missed a commit, one
whose every database connection stayed in use past [search] busy_timeout, or one that will not
answer at all because it refused to load or holds no indexed tree.
A file past the per-file byte bound, one its syntax provider refuses under a node or depth
bound, one holding a declaration the Contribution contract refuses, or one standing past the
[source] declarations capacity, is left out of the
index: it answers no search or lookup, counts nowhere in
rift://map, and is named in
rift server logs with the bound it crossed or the field the
contract refused. One minified bundle therefore never blocks the workspace, and neither does
a workspace declaring more than the index holds: the files that fit are served and the rest
are reported. The [source]
exclusion remains the way to keep such files out of the walk altogether.
Under the default [search.text]
large_files = "split", a file past [providers.syntax]
max_file stays in the index as text the syntax provider does not parse: search reads it in
chunks, its declarations are absent, and every answer names it in a large_file_unparsed
warning. Under skip the per-file byte bound is max_file, so such a file is left out as above,
and a file past max_chunk keeps no text in search, which a search whose paths reach it counts
in a large_file_skipped warning.
Search
search is for reads where the exact name isn't known.
It ranks declaration names, signatures, documentation, and file content from the index, and it
answers a regex pattern from the same stored
text.
The document shape
Every index publishes the same document shape, whether it holds the served project or a package version the global index collected. A document carries a stable identity, the project path or source unit it lives at, its language, its package when it came from one, a content digest, and the searchable fields below:
| Field | What it holds | Weight |
|---|---|---|
name | A declaration's name, or a file's final path segment with its extension | 10 |
qualified_name | The declaration's qualified name | 6 |
identifier_terms | The names split on case, acronym, and separator boundaries | 4 |
signature | The rendered signature, when a provider published one | 2 |
documentation | The attached doc comment, when a provider published one | 1.5 |
file_content | An indexed file's text, parsed source and plain text alike | 1 |
A fact no provider published stays absent. Nothing substitutes a declaration's own text into the signature or documentation field, because the two are weighed apart and filling one with the other would count the same bytes twice. The weights are what make a name hit outrank a hit buried in a comment, and the answer reports which field placed each hit, so a documentation match and a name match are distinguishable rather than both reported as "the ranking found it".
Since v0.0.46The index holds each file's text once, in the file's own documents, and a declaration's document carries the derived fields alone. Keeping one copy is what lets the regex search below read every match from one set of rows: a copy split between declarations and the rest of the file would put a match that crosses a declaration's edge in neither.
A word only a function's body holds therefore ranks the function's file. The read path then finds the query's terms in the first file documents of the ranked list, maps each position to the smallest declaration containing it, and answers those declarations in place of the first file document, scored together by BM25 over the terms each one holds.
The project path, the source unit, the package identity, the language, and the kind stay metadata. None of them enters the ranked text: indexing one tree below two different host roots must produce one order, and a path in the ranked text would make the clone directory part of the score.
The index records a corpus revision covering the field shape, the derivation the identifier terms were split with, the tokenizer, and the weights. A store stamped with another revision holds rows this build cannot read, so it answers as if nothing had been published and the next write clears it before it writes.
Documentation
search with target: "documentation" returns blocks from selected documentation sources.
Workspace selection includes .md, .markdown, .mdx, .rst, and .ipynb, plus .txt
selected by [search.text].include. Source visibility applies before collection.
The selection leaves out change logs, licenses, codes of conduct, and archived, deprecated, or
translated copies by default. The [documentation]
table overrides that default for the workspace.
Each regular source keeps its existing text chunks. Notebook markdown and code cells receive separate content owners; outputs and raw notebook JSON stay outside ranking. Collection records block ranges, headings, links, source digests, and exact-symbol references beside those owners. It neither executes code nor follows links to acquire more sources.
Before fusion, the read maps existing ranked documents to block identities and keeps the best
position once per ranking input. A heading and its text chunk therefore return one documentation
hit. Under target: "all", an attached comment keeps its symbol result when that symbol answered;
a file match outside the comment remains a file result.
For example, a guide containing the inline code Compass can contribute context to
get_symbol with its established id and include: ["documentation"]. The reference resolves only
when the selected declaration set establishes one exact target. Ambiguous names remain unresolved.
The read returns at most 32 references per symbol and 16 KiB of documentation excerpts across the
page, with typed warnings for missing bytes or reached bounds.
A package's documentation comes from the global index. Global documentation requests use the package REST client only after the service advertises the corresponding capability. Workspace paths and source bytes remain local.
Query phases
A caller's text is parsed once, into quoted phrases and unquoted terms, and every member is rendered as a quoted string literal, so an FTS operator a caller typed cannot become an operator the store executes. The parse is bounded: 4,096 bytes of query, 32 members, 256 bytes per member. A question carrying more terms than the bound narrows rather than failing - every quoted phrase survives and the longest remaining terms fill what is left - because a long question deserves an answer, not a refusal. More quoted phrases than the bound is still refused: dropping one would answer a question nobody asked.
Two expressions come out of one parse. The precise phase requires every member, which is
what answers a multi-term question exactly. The broad phase keeps every phrase required and
widens only the unquoted terms with OR, so "impact radius" graph search renders as
"impact radius" AND ("graph"* OR "search"*).
Every selected index runs the precise phase first and the results fuse together. Only when that merged answer leaves the candidate pool short does the broad phase join, and its identities append after every precise one. One index's widened hit therefore cannot displace another index's exact hit.
Ranking inputs and fusion
The rankings below answer, and their answers fuse together:
- identifier matching over the declaration names, for the identifiers the query carried;
- full-text matching over the document's searchable fields, by BM25 under the weights above;
- vector similarity over the embedded declarations, when a model is configured and its vectors are in.
The identifier input reads the query the way a reader would. The whole query counts when it is
one identifier-shaped token, a dotted name buried in prose counts along with its final segment,
and any token that splits into more than one word counts: where does encode_path get its escape set reaches encode_path without the caller quoting it. A plain prose word contributes
nothing here, because it reaches the full-text ranking through its own input.
Each ranking scores in units nothing converts between: a match class, a BM25 rank, and a
cosine. What they agree on is the position they put a candidate in, so fusion reads positions
alone and combines them by weighted reciprocal rank.
[search.ranking] carries that decision: fusion_k
sets how sharply a top rank counts, and identifier_weight, lexical_weight, and
vector_weight set each input's share. Only the inputs that answered take part, so an
unavailable vector ranking contributes nothing and leaves the order the other two state.
Several selected indexes may each contribute one kind; that kind's share then splits among
them, so reaching for the global index does not double what full-text matching is worth.
Fusion holds no project path and resolves no symbol. It answers ordered identities, and the
read path resolves them afterwards, which is what lets package hits from the global index rank
through the same code. A hit's score is derived from its place in the final order: it is
comparable inside one answer and nowhere else.
Regex patterns
Since v0.0.46 · Rust, JavaScript, TypeScript, Markdown, JSON, YAML, TOML, PythonA regex pattern answers from a trigram index
over the same stored file text, so the text still lives in one place. A trigram index records
which rows hold each run of three characters.
The pattern's query plan derives the trigrams any match must contain - load_config\( requires
loa, oad, and every other run of three characters in load_config( - and the index answers
the rows holding them. The pattern's matcher then verifies those rows over the text the
published index holds, line by line as ripgrep reads it.
The trigram index is filled in the background after the rows it covers are written, so no search
waits for it. A row it doesn't hold yet is verified beside the rows it selects, and when those rows
pass the search's bounds, the answer covers the rows the index holds and warns
pattern_index_preparing.
The plan never rules out a row that could hold a match: a class too large to expand, a
repetition that can match nothing, and a literal shorter than three characters each require
nothing. A pattern that requires nothing, such as [0-9a-f]{40}, verifies every file its
paths select. Each match maps to its smallest containing declaration through the same lookup
body matches use, which is how a symbol hit names the declaration holding a match.
Vector ranking
Since v0.0.16 · Rust, JavaScript, TypeScript, Markdown, JSON, YAML, TOML, PythonFull-text matching answers a query that shares a token with the code. A query that shares none - a description of what a declaration does, in the words a bug report would use - reaches nothing that way, and no amount of ranking repairs that, because the words are not there.
An embedding puts the query and the declaration in one space, so a paraphrase finds code it has no word in common with. It replaces nothing. A caller who quotes a real name - a symbol, a configuration key, a line from a traceback - has handed the full-text ranking an exact token to score, while the embedding maps that name into a space where its near neighbours crowd it.
A declaration is embedded as its own source under its qualified name. The name carries the module the source doesn't state; the source carries the identifiers and documentation an encoder trained on code reads best. The path is left out, because the full-text ranking already covers it.
Every model runs through one contract, whether it is an encoder this process loaded or a
service answering the OpenAI embedding shape. Rift keeps what that contract does not cover:
acquiring the weights, cutting a pass into batches under
batch_inputs and max_in_flight, and narrowing the
coordinates to the stored format once at the boundary.
Vectors are filed under the identity of the space they were embedded into: the source kind, the endpoint origin for a remote service, the model and its resolved revision, the width, and the document and query transformations. Any change there mints another identity and the held vectors are dropped rather than scored against, because two spaces are two coordinate systems. Credentials never enter it.
A remote endpoint sends declaration text outside the project while the vectors are built, and query text while a caller searches. The configuration page states both transfers. The local default sends nothing: it starts without a key and makes no request once the weights are acquired.
Preparation runs behind the answers. A search issued before the vectors are in is answered by the other two inputs and says so, rather than waiting for a model to load.
Retrieve symbol
get_symbol reads one logical symbol by its canonical id.
The answer separates the symbol from its declaration bindings and carries requested source
inline. An immutable view keeps a later read and its declaration continuation on the same
selection.
A Symbol carries id when the index established one identity for the declaration; a symbol
the providers could not settle answers without an id, and a retained provider disagreement
lands as a symbol_disagreement entry in the result's warnings.
Asking for history attaches the symbol's committed timeline to the same answer.