reference-query 0.44.0

Reference Query — find the code you're looking for.
Documentation
# 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.** `make dogfood Q=<query>` fully indexes a repo into a throwaway
DB and runs a query from inside it, so you can feel the ranking on real code.
Use it to catch quality regressions a unit test wouldn't.

`REPO=` picks the target; it defaults to this repo, which makes Rust the
default dogfood language. Reach for someone else's code whenever the question
is about *ranking* rather than extraction: rq's own source is ~600 symbols, too
few for same-name collisions and ambiguity to appear at all, and it can only
ever exercise the Rust plugin. `~/code/lib/ruby/rails` is a good large Ruby
corpus (~3k files, indexes in seconds):

```sh
make dogfood REPO=~/code/lib/ruby/rails Q=Middleware
```

## 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.

Releasing is one command — **don't do these steps by hand**:

```sh
release 0.41.0 --steps         # the checklist: what to do, in order, with ✓ on what's done
release minor --summary "…"    # or run the whole thing
release 0.41.0 --dry-run       # what an automated run would do and skip
release --audit                # is anything out of sync, across every tool in the tap?
```

`--steps` is often the right one. The script's value is knowing the ordering,
the derived values, and the steps that get forgotten — not executing them. Use
it to drive a release yourself and keep judgment at each stop; it works on a
dirty tree and part-way through.

`release` lives at `~/.claude/bin/release` and is shared by every tool in the
`dpep/tools` tap. It bumps `Cargo.toml`/`Cargo.lock`, runs `script/check.sh`,
pushes `main`, **waits for CI**, tags, publishes `reference-query` to crates.io,
hashes the tag tarball into `~/code/lib/homebrew-tools/Formula/rq.rb`, builds +
tests + audits the formula, pushes the tap, opens the GitHub release from the
changelog section, and syncs the skill with a plugin-version bump. Every step is
idempotent, so a run that dies partway is just re-run.

Ordering that the script enforces and a hand-run forgets: CI has to be green
before the tag exists, and the tag has to be on GitHub before its tarball can be
hashed. Skip the formula bump and installs serve a stale cached build.

rq ships through **four** channels — tag, crates.io, tap, plugin skill — each
forgettable on its own. `release --audit` compares all four across every tool in
the tap and is the way to catch one that was missed weeks ago.

`CHANGELOG.md` keeps a rolling `## Unreleased` heading: log a change under it in
the commit that earns it, and `release` retitles that heading as the version and
date. Four commits once shipped with no entries at all, and reconstructing them a
week later is worse than writing them cold.

## The skill has three copies; keep them one

`claude/rq-skill.md` is the source. It is copied verbatim — no edits, no
stripping — to:

- `~/code/lib/claude/plugins/code/skills/rq/SKILL.md`, the public marketplace
- whatever a user installed, which updates only when the **code plugin's
  version** moves in `plugins/code/.claude-plugin/plugin.json`

`release` does the copy and the plugin-version bump for you, and `release
--audit` reports a skill copy that has drifted. If you change the skill *without*
releasing, do both by hand in the same change — or it reaches nobody: `claude
plugin update` compares versions, not content, and reports a plugin current
while serving the old file. That has already happened once, to gqls — four
skill-touching commits under one plugin version.

Install guidance for humans lives in `claude/INSTALL.md`, deliberately outside
the skill so the copy stays a copy.