# rq development conventions
`rq` (Reference Query) is a **code navigation engine** — it gets you to the
definition you most likely want, fast. Read [README.md](README.md) for the
product vision, [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the design, and
[docs/ROADMAP.md](docs/ROADMAP.md) for what ships when.
> **Shipping, maintained tool.** The design docs are the contract — keep them
> in sync with the code, changing them in the same commit when the design
> changes.
## First principles (do not drift from these)
- **Navigation, not search.** Fewer, better, ranked results beat exhaustive
ones. When a change trades relevance for completeness, it's probably wrong.
- **The core is language-agnostic.** No Ruby-specific (or any-language)
assumption leaks out of `src/lang/` into `index`/`search`/scoring. Languages
plug in via `LanguagePlugin`. The shared *model* may grow to fit a language —
e.g. Rust added `struct`/`enum`/`trait` to `core::Kind` — but that's
generalizing the vocabulary all languages share, not a one-off. Prefer
generalizing over a special case; change `core/` when it genuinely earns it.
- **Results stream.** The API is incremental from the start — sub-50 ms first
answer, then progressive improvement. Don't add synchronous "collect
everything" paths.
- **Ranking is explainable.** Scoring is an additive sum of named features;
`--explain` must always be able to show why a result ranked where it did.
- **Partial is normal.** Never assume a complete index. Code must work at 0%,
5%, and 100% coverage.
- **Every command is agent/script-friendly.** Any command that prints output
honors `--json`/`--ndjson`, not just search — `--status`, `--index`, `--drop`,
and anything new. `--json` is a pretty object (single-result commands) or array
(multi-row); `--ndjson` is one compact object per line. Keep field names stable
and consistent across commands (a repo identity is always `repo`). Route
single-object commands through the `emit_json` helper. Exit codes stay
meaningful (0 = something happened/matched, non-zero = nothing). When you add a
command, add its structured output and an e2e assertion in the same change.
## Language and toolchain
Rust, single static binary. Tree-sitter for symbol extraction, `rusqlite` for
storage (SQLite, WAL mode).
This machine's Rust came via Homebrew's keg-only `rustup`, so `cargo` may not be
on `PATH`. Either add it once —
```sh
echo 'export PATH="/opt/homebrew/opt/rustup/bin:$PATH"' >> ~/.bash_profile
```
— or invoke directly: `/opt/homebrew/opt/rustup/bin/cargo`.
## Repo layout
Single binary crate; modules mirror the architecture. Language specifics are
quarantined under `src/lang/`.
```text
rq/
Cargo.toml
src/
main.rs ← CLI entry
cli/ ← `rq <query>` default command, arg parsing, output
core/ ← symbol model, repo identity, scoring — NO language specifics
store/ ← SQLite schema, migrations, queries (WAL)
index/ ← walker, incremental indexer, coverage
search/ ← staged pipeline, scorer, --explain
lang/ ← Tree-sitter plugins (ruby, rust, go, python, typescript)
ruby/ ← first plugin
rust/ ← rq dogfoods on its own source
docs/ ← ARCHITECTURE.md, ROADMAP.md
tests/ ← integration tests + fixtures
```
Keep it a single crate until there's a concrete reason to split into a
workspace (e.g. a reusable library extracted for editor adapters). Simpler
wins.
## Building, testing, linting
```sh
cargo build # dev build → target/debug/rq
cargo build --release # optimized → target/release/rq
cargo run -- refund # run the CLI from source
cargo test # unit + integration tests
cargo clippy --all-targets # lint — keep it clean
cargo fmt # format — run before committing
```
Before committing: `cargo fmt && cargo clippy --all-targets && cargo test`.
## Testing conventions
- Write tests for new code, but keep them focused on quality, not quantity —
edge cases and error handling over restating the happy path.
- Ranking is the heart of the tool: test it with **fixture repos** under
`tests/fixtures/` and assert on *ordering* (the right result ranks first),
not just membership.
- A new language plugin ships with a fixture file of source + expected symbols.
- **Use generic, non-identifying test data** — neutral placeholders (`Widget`,
`Foo`, `HandlerA`, `Account`) over real class names, company/product terms, or
anything tied to a specific employer or codebase. This is a public repo; keep
fixtures and assertions domain-neutral.
- Spec descriptions stay simple and resilient ("ranks the exact match first",
not a brittle exact-string assertion).
- **Verify through `cargo test`, not by hand-running the binary.** CLI behavior
is covered by `tests/cli_e2e.rs`, which drives the built binary
(`CARGO_BIN_EXE_rq`) with an isolated `RQ_DB` and a temp repo — reproducible,
CI-checked, and no permission prompts. Extend that test rather than running
ad-hoc `rq …` invocations to confirm a change. Logic that would otherwise
need a manual run (e.g. git-log parsing) is factored into a pure function with
its own unit test.
## Adding a language plugin
1. Add the Tree-sitter grammar dependency.
2. Implement `LanguagePlugin` in `src/lang/<lang>/`: `extensions()` +
`extract(source) -> Vec<Symbol>`.
3. Register it in the extension→plugin registry.
4. Add a fixture (source + expected `Symbol`s) under `tests/fixtures/<lang>/`,
and assert on *ordering* end-to-end (see `tests/rust_fixture.rs`).
`index/` and `search/` should not need to change — if they do, a language
specific leaked and the design doc needs revisiting. The exception is the shared
`core::Kind` vocabulary: a language may add a kind it genuinely needs (Rust
added `struct`/`enum`/`trait`), which also touches the kind-keyed spots in
`search/score.rs` (weight + the path-only "primary definition" gate) and the
`--kind` canonicalizer in `cli/`. That's generalizing the model, not a leak —
prefer it over a one-off, and keep the new kind language-neutral.
**Dogfooding.** Rust is the dogfood language: `make dogfood Q=<query>` fully
indexes this repo into a throwaway DB and runs a query, so you can feel the
ranking on real Rust. Use it to catch quality regressions a unit test wouldn't.
## Schema changes
`store/` owns the schema and migrations. A schema change is a migration plus an
update to the schema block in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — keep
them in sync in the same PR.
## Landing changes
No pull requests for this repo — commit or merge directly to `main` and push.
It's a solo project; the PR ceremony is overhead we skip here.
Keep changes small, focused, and logically connected; change behavior or
structure, not both at once. Make sure CI is green
(`cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo test`)
before pushing.
## Versioning / releasing
Bump the version when a change reaches users — i.e. it alters the **built
binary** (behavior, a flag, ranking, even `--help`/output wording). Stay below
1.0 for now — **only minor or patch bumps**, never a major:
- **patch** (`0.1.x`) — fixes, output/`--help` wording, internal cleanups
- **minor** (`0.x.0`) — new user-facing capability (a flag, a ranking signal, a
language plugin)
Repo-only docs (README, CLAUDE.md, `docs/`) **don't** bump — they don't change
what `brew` builds, so a bump would only force an identical rebuild.
A release is a tagged tarball the Homebrew formula pins by URL + `sha256`, so
`brew upgrade` rebuilds from that exact commit. The steps, in order:
1. `Cargo.toml` `version`.
2. `Cargo.lock` — run `cargo build` so the `rq` entry updates.
3. Commit both (with the change) and push `main`. Green CI first.
4. Tag the release commit and push the tag:
`git tag -a vX.Y.Z -m "…" && git push origin vX.Y.Z`.
5. Compute the tarball hash — GitHub's tag archive is stable:
`curl -sL https://github.com/dpep/rq/archive/refs/tags/vX.Y.Z.tar.gz | shasum -a 256`.
6. In `~/code/lib/homebrew-tools/Formula/rq.rb`, update the `url` tag and the
`sha256` to the new values, then commit and push the tap.
The tag must exist before you can hash its tarball, so the tag push (4) always
precedes the formula edit (6). Skip the formula bump and installs serve a stale
cached build.