# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Build, test, lint

```bash
cargo build                                  # debug
cargo build --release                        # release (use this for benches)
cargo test                                   # full suite (~431 tests)
cargo test diff_accuracy::                   # run a module
cargo test modified_file_returns_unified     # run a single test by name
cargo fmt --all -- --check                   # CI enforces
cargo clippy --all-targets --locked -- -D warnings   # CI enforces
```

`rusqlite` uses the `bundled` feature — no system SQLite needed. CI matrix runs on Ubuntu / macOS / Windows; OS-specific paths exist for FSEvents (macOS), inotify (Linux), and `ReadDirectoryChangesW` (Windows) in `drip watch`.

### Trying a local build against a real read

```bash
cargo build --release
DRIP_DATA_DIR=/tmp/drip-dev DRIP_SESSION_ID=dev \
  ./target/release/drip read README.md
```

### Benchmarks (release binary only)

```bash
bash scripts/bench_reddit.sh                 # 4-language realistic workload
bash scripts/bench_multilang.sh              # 8-language compression matrix
cargo test --release diff_perf::             # 50 KB / 99 KB diff perf assertions
cargo test concurrency::                     # 15-process SQLite contention
```

Numbers from these scripts feed `BENCHMARKS.md` — re-run when changing the diff path or session storage.

## Repository conventions

- **Branch from `develop`**, not `main`. PRs target `develop`. `main` is release-only (driven by `release-please`).
- **Conventional Commits are enforced by CI** (`commitlint` runs on PRs). Subject ≤ 100 chars, imperative, no trailing period. Sloppy subjects break the auto-generated CHANGELOG.
- Every behavioural change needs a test. Refactors that don't change behaviour and don't unlock a future change won't be merged.
- New dependencies need a clear perf or correctness justification — the binary is intentionally < 5 MB.
- `CHANGELOG.md` is generated by `release-please`; never edit by hand.

## Architecture (the big picture)

DRIP is a single Rust binary that sits between an LLM coding agent (Claude Code, Codex, Gemini) and the filesystem. It records a baseline on the first read; MCP/Bash/manual first reads can be semantically compressed for code, while Claude Code's native `Read` first pass stays native so Claude's read-before-edit tracker is populated. On re-read it returns a unified diff or `[unchanged]`. Same agent surface, far fewer tokens.

`ARCHITECTURE.md` is the design-rationale document — read it before changing anything load-bearing.

### Core invariant

> For a given `(session_id, file_path)`, `reads.content` always reflects the most recent version DRIP has *returned to the agent*.

Anything that updates `reads.content` without actually emitting that content to the agent (or vice versa) will silently produce wrong baselines. `tests/integration/diff_accuracy.rs` exists to catch this.

### Module layout

- `src/main.rs` — clap CLI surface; every subcommand routes to `src/commands/`.
- `src/commands/` — one file per subcommand (`read`, `init`, `meter`, `doctor`, `replay`, `watch`, `cache`, `registry`, `sessions`, `update`, …). `hook.rs` dispatches per-agent hook invocations.
- `src/core/` — the engine:
  - `session.rs` — SQLite schema, migrations, the 4-strategy session-id ladder (`env` → `git` → `pid` → `cwd`), heartbeat-based TTL with `expired_sessions` tombstone, `file_registry` for cross-session orientation. **Schema migrations are additive** (`ALTER TABLE … ADD COLUMN`); a `meta(schema_version)` row guards against running an older binary against a newer DB.
  - `tracker.rs` — the `process_read` orchestrator shared by every entry point (Claude hook, MCP). Computes hash, picks one of {full, diff, unchanged, large-file fallback, complexity-gate fallback, deleted}, writes the new baseline, returns the rendered string.
  - `compress.rs` — semantic compression for 13 languages. Two scanners (indent-based for Python; brace-balancing for the C-family) plus a Javadoc/KDoc/JSDoc collapser. Bodies under `DRIP_COMPRESS_MIN_BODY` (default 15) lines stay inline. **No real parser** — the line scanner is good enough on real code (>95% on test corpus) and degrades gracefully (false negatives mean "uncompressed", never mangled).
  - `differ.rs` — wraps `similar` for unified diffs (3 lines context). `analyze_complexity` enforces the diff-complexity gate (`DRIP_MAX_HUNKS`, `DRIP_MAX_CHANGED_PCT`, span heuristic) — if a diff would be sprawling DRIP ships a clean full re-read instead.
  - `cache.rs` — hybrid inline / file-cache storage. Files ≤ `DRIP_INLINE_MAX_BYTES` (default 32 KB) live in `reads.content`; larger blobs go to `<DRIP_DATA_DIR>/cache/<sha256>.bin` (atomic tmp + rename, 0700/0600). Hash-addressed naming gives automatic dedup across sessions. GC walks both `reads` and `file_registry` when computing the live set.
  - `git.rs` — pure-Rust `.git/HEAD` parsing (handles real `.git` dirs and worktree gitlinks); used by the `git` session strategy. Bails silently to `pid` on any ambiguity — confidently-wrong session ids would silently misroute reads.
  - `ignore.rs` — `.dripignore` (gitignore-style) with built-in defaults. Lookup order: `$DRIP_IGNORE_FILE` → `./.dripignore` → `~/.dripignore` → built-ins.
- `src/hooks/` — one module per agent integration. Each parses the agent's stdin payload, calls `tracker::process_read`, and writes the agent's accept/reject envelope on stdout:
  - `claude.rs` (PreToolUse:Read), `claude_glob.rs`, `claude_grep.rs`, `claude_post_edit.rs` (PostToolUse:Edit/Write/MultiEdit/NotebookEdit — refreshes baseline + emits elided-function warning), `claude_session_start.rs`, `gemini.rs`.
  - **Claude substitution mechanism**: `permissionDecision: "deny"` + `permissionDecisionReason` carrying the rendered output. This is currently the only stable Claude Code hook contract that can *substitute* what the model sees in place of a tool result.
- `src/mcp.rs` — stdio JSON-RPC 2.0 server (`initialize`, `tools/list`, `tools/call`, `ping`) advertising a single `read_file` tool. ~150 lines, no MCP SDK dependency. Same `read::run()` powers hook + MCP paths so behaviour is identical.

### State on disk

- SQLite store at `~/.local/share/drip/sessions.db` (Linux) / `~/Library/Application Support/drip/` (macOS) / `%APPDATA%\drip\` (Windows). Override with `DRIP_DATA_DIR`. WAL mode + `busy_timeout = 500 ms` handles concurrent hooks.
- `cache/<sha256>.bin` for blobs > inline threshold.
- Schema is intentionally narrow — see `src/core/session.rs::SCHEMA`.

### Hook contract additions to know about

- **Partial reads** (`Read(file, offset=N, limit=M)`): if a baseline exists, the diff/unchanged logic is window-scoped (`[DRIP: unchanged (lines X-Y)]` / `[DRIP: delta only (lines X-Y)]`). On a file DRIP has never seen, partial reads pass through to native — DRIP has nothing to compare against. **Partial reads never mutate the baseline.**
- **Edit certificates**: a Read immediately following a Write/Edit/MultiEdit returns `[DRIP: edit verified | hash: …]` (~390 B) instead of the full file. Disable with `DRIP_CERT_DISABLE=1`. Out-of-band edits (non-hooked tools, `git pull`, manual): run `drip refresh <path>` to drop the stale baseline.

### Adding a new agent

Three contracts, in priority order:

1. **Pre-tool hooks (Claude-style)** — add `src/hooks/<agent>.rs`, route via a new `HookAgent` variant in `src/commands/hook.rs`, wire `drip init --agent <name>` in `src/commands/init.rs`, integration test under `tests/integration/<agent>_hook.rs`.
2. **MCP** — already covered by `drip mcp`. Just teach `drip init --agent <name>` to register the server in the agent's config and append a usage instruction.
3. **Neither** — pipe `{"file_path": "..."}` into a sibling of `drip hook gemini`.

Bias toward zero typing for the user. If `drip init --agent <name>` doesn't make the agent transparent, the integration isn't done.

## Tests

- Live in `tests/integration/` with shared helpers in `tests/common/mod.rs`.
- Drive the compiled binary against fake hook payloads — they exercise the same code path the agent does.
- Notable suites: `diff_accuracy`, `diff_perf`, `concurrency`, `session_keying`, `compression`, `regressions`, `post_edit_hook`, `read_offset_limit`, `mcp_server`.

## Things that look odd but are intentional

- **No tree-sitter / `syn`** for compression — the line scanner degrades gracefully where a parser would mangle. See `core/compress.rs`.
- **No daemon.** Prototyped, saved ~3–4 ms, not worth the failure modes (stale daemon, port collisions, restart-on-upgrade). Cold path is bounded by `clap` + WAL setup.
- **Token estimate is `bytes / 4`** — pulling `tiktoken-rs` would add ~3 MB for what is purely a reporting number. The savings *ratio* is stable under any monotonic estimator.
- **`similar` (not bsdiff/vcdiff)** — consumer is an LLM that only meaningfully reads text. A unified diff in the model's training distribution beats a smaller-but-opaque binary patch.
- **Cursor/IDE agents with built-in `read_file` are deliberately not supported** — their native tool wins by default and DRIP only sees a fraction of reads. We won't ship a half-working integration.
