rf 0.0.4

rf — an agent-friendly search envelope that fuses ripgrep and fd, with cross-tool stage attribution for false-negative forensics. Early development; see the repository for status.
# rf

A naive `rg pattern` silently skips gitignored files, hidden files, and binary
files. On a working tree it can miss most of the matches and still exit 0, so a
caller thinks the search succeeded. `rf` (**r**ipgrep + **f**d) finds those
matches and tells you which filter hid each one.

**Status: early development.** The command surface and the JSON envelope below
work today; expect breaking changes before 1.0.

## Install

```sh
cargo install rf
```

## Example

A repo with the search term in three places — a tracked file, a hidden file,
and a gitignored file:

```
config.py          timeout = 30
.env.local         timeout = 5      # hidden
cache/build.py     timeout = 999    # gitignored
```

Plain ripgrep finds one of the three. `rf` finds all three and names the filter
that hid each:

```
$ rf content timeout .
content 'timeout' in .: 3 file(s), 1 by default, 2 hidden by filters
  hidden       .env.local
  vcs_ignore   cache/build.py
  default      config.py
  ! IGNORE_VCS: 1 match(es) hidden by default; add -u (ignore .gitignore/.ignore rules)
  ! HIDDEN_SKIPPED: 1 match(es) hidden by default; add -uu (also search hidden/dotfiles)
  $ rg -u 'timeout' .
  $ rg -uu 'timeout' .
```

Each miss carries a paste-ready correction. Run at a terminal, `rf` prints the
summary above; piped or with `--json`, it prints one structured envelope:

```sh
rf content timeout . --json
```

```json
{
  "ok": true,
  "data": [
    { "file": ".env.local",     "surfaced_by": "hidden" },
    { "file": "cache/build.py", "surfaced_by": "vcs_ignore" },
    { "file": "config.py",      "surfaced_by": "default" }
  ],
  "meta": { "matched_files": 3, "default_matched_files": 1, "hidden_by_filters": 2 }
}
```

The `warnings` array (elided above) carries one entry per miss, each with a
warning code and the paste-ready `rg` command that surfaces it.

## Verbs

Every verb emits the same envelope. Run `rf capabilities` for the full machine
contract (verbs, flags, exit codes, warning codes).

- **`rf content <pattern> <path>`** — content search that peels ripgrep's
  default filters as layers (vcs-ignore, hidden, binary, case) plus an encoding
  probe (utf-16), and attributes every recovered file to the filter that hid it.
- **`rf find <pattern> <path> --name <ext>`** — staged discovery across four
  independent sources. The fd name filters (`fd_name`, `fd_hidden`, `fd_ignore`)
  and ripgrep's binary skip (`rg_binary`) run in-process; git history
  (`git_deleted`) recovers matches scrubbed from the tree; with
  `--structural PAT --lang LANG`, ast-grep (`ast_structural`) finds matches that
  have no fixed literal form. Each miss is attributed to the one stage that hid
  it.
- **`rf doctor <path>`** — reports the linked engine and whether `.gitignore` is
  active for the path. (`.gitignore` applies only inside a git work tree, so the
  same search can answer differently in a scratch dir and a real repo.)
- **`rf capabilities`** — the machine contract as JSON.

## How it works

`rf` links the [`ignore`](https://crates.io/crates/ignore) walker and the
[`grep`](https://crates.io/crates/grep) searcher — ripgrep's and fd's own crates
— directly, rather than shelling out to the `rg` and `fd` binaries. Each file's
membership in a filter is read natively as the walk runs, so a match keeps its
stage provenance instead of losing it when a shell pipe erases fd's exit code
and conflates ripgrep's "no files" with "no match." git history and ast-grep are
external oracles with no Rust binding, so `rf` runs them as subprocesses; each
contributes nothing (rather than failing) when its tool is absent.

## License

Licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE).