Skip to content

Commit 851742b

Browse files
authored
Merge pull request #23 from thinkgrid-labs/feat/reportable-prefilter
Feat/reportable prefilter
2 parents b46c8fa + fa4bda2 commit 851742b

35 files changed

Lines changed: 4873 additions & 428 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,5 @@ npm-debug.log*
6363
yarn-debug.log*
6464
yarn-error.log*
6565
lerna-debug.log*
66+
67+
PLAN.md

.pre-commit-hooks.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
#
44
# repos:
55
# - repo: https://github.com/thinkgrid-labs/diffmind
6-
# rev: v0.8.0
6+
# rev: v0.9.0
77
# hooks:
88
# - id: diffmind
99
#

CHANGELOG.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Changelog
2+
3+
## 0.9.0 — the reviewer's cockpit
4+
5+
diffmind was built as a gate: run it, get a verdict, pass or fail. This release
6+
adds the other half — a place to *sit* while deciding what to say about someone
7+
else's branch. Both surfaces share one engine; neither replaces the other.
8+
9+
### Breaking
10+
11+
- **`--format json`: `stats.chunks``stats.units`**, plus `units_cached` and
12+
`units_unparseable`. The unit of review is now a region of a file rather than
13+
an arbitrary slice of lines, and the field names say so. Exit codes, SARIF and
14+
Markdown output are unchanged, so CI gates are unaffected.
15+
- **Daemon protocol changed.** A running 0.8 daemon is ignored rather than
16+
misused — the version gate already handled this — but you will want
17+
`diffmind serve --stop` after upgrading.
18+
- **`chunk_diff` removed** from the `core-engine` public API, superseded by
19+
`build_units`.
20+
21+
### Added
22+
23+
- **Reviewer's cockpit** (`diffmind --tui`). Analyses on launch. Each finding
24+
shows the actual hunk the model reviewed and the context it was given.
25+
`a` accepts (and copies a review comment via OSC 52, which works over SSH),
26+
`d` dismisses, `w` marks wrong. Verdicts are written through immediately.
27+
- **Review standards as markdown**`.diffmind/rules/*.md`, scoped by path
28+
glob, committed to the repo. `diffmind rules init` / `rules list`. A finding
29+
the model attributes to a rule set gets the ID `rulebook.<id>` and suppresses
30+
like any other; an attribution naming a rule set that does not govern that
31+
file is discarded.
32+
- **Reportable pre-filter.** Lockfiles, `linguist-generated` paths, `@generated`
33+
banners, minified bundles, assets, snapshots and formatting-only hunks are
34+
dropped before the model sees them — and the run says what it skipped:
35+
`312 hunks → 74 reviewable (238 filtered: lockfiles, generated, formatting)`.
36+
Whitespace inside a string literal counts as content; indentation is never
37+
dismissed in Python or YAML.
38+
- **`diffmind stats`** — findings, cost and the accept-to-wrong ratio over
39+
recorded runs. Every review is filed to `.diffmind/runs/<sha>/`.
40+
- **Cost reporting** — wall-clock and token counts in the footer, in JSON, and
41+
in the run record. Estimated counts are marked `~` rather than passed off as
42+
exact.
43+
- **Revision ranges**`diffmind v1.2.0..HEAD`, or `--range`. Paths after a
44+
range narrow it.
45+
- **`review.ignore`** globs in `.diffmind/config.toml`.
46+
- diffmind writes its own `.diffmind/.gitignore`, keeping generated state out of
47+
git while leaving `rules/`, `rules.toml`, `config.toml` and `baseline.json`
48+
committable. Your repository's `.gitignore` is not touched.
49+
50+
### Fixed
51+
52+
- **The result cache never worked across files.** Context was assembled once
53+
from the whole diff and folded into every chunk's cache key, so editing one
54+
file invalidated every other file's cached result — a re-review after a
55+
force-push re-inferred the entire diff. Context is now assembled per unit.
56+
- **Each chunk was given the wrong context.** The six enclosing functions were
57+
taken from whichever files sorted first, so most chunks paid for context about
58+
files they did not contain.
59+
- **Editing one region of a file re-reviewed the whole file.** Units are grouped
60+
by region, so a change in one function only invalidates that function.
61+
- **Glob matching could not express a directory prefix.** `src/api/**` matched
62+
nothing, and `**/gen/**` was a substring test that also matched
63+
`src/gen-legacy/`. Affects `rules.toml` `files` patterns as well.
64+
- **Conflicting input selectors were silently resolved by order.** Passing both
65+
`--staged` and `--last` reviewed whichever the code checked first; it now
66+
errors.
67+
- A diff dominated by a lockfile is no longer refused for size before the
68+
lockfile is filtered out.
69+
- The TUI now honours the pre-filter and files a run record, which it did not.

README.md

Lines changed: 147 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,19 @@ Your source code never leaves your environment. Works offline. Ships as a **sing
2727

2828
## Features
2929

30+
- **Deterministic pre-filter** — lockfiles, generated files, minified bundles, assets and formatting-only hunks are dropped before the model sees them, and the run tells you exactly what it skipped: `312 hunks → 74 reviewable (238 filtered: lockfiles, generated, formatting)`.
3031
- **Deterministic detectors** — commented-out code, removed-but-still-used declarations, and your own regex rules. These run before the model, cost nothing, and are the findings you can trust unconditionally.
3132
- **Security, bug, performance and maintainability review** by a local model
33+
- **Review standards as markdown** — commit your team's own rules to `.diffmind/rules/`, scoped by path. Prose the model reads, versioned next to the code it governs.
3234
- **Ticket-aware review** — check the diff actually implements the acceptance criteria (`--ticket`)
3335
- **Suppressions** — inline `// diffmind-ignore` comments and a project baseline, so one false positive doesn't get the whole gate deleted
3436
- **SARIF output** — inline PR annotations via GitHub Code Scanning, no bot account or token
3537
- **Pluggable backends** — the bundled GGUF, or your own Ollama / vLLM / LM Studio endpoint
3638
- **Daemon mode** — keep the model resident so reviews are near-instant
3739
- **Local RAG** — feeds the model the *enclosing function* of each hunk, not just the diff
3840
- **Reproducible** — greedy decoding with a fixed seed: the same diff always reviews the same way
39-
- **Interactive TUI** (`--tui`), JSON / Markdown output, and a proper CI gate
41+
- **Reviewer's cockpit** (`--tui`) — analyses on launch, shows the hunk and context behind each finding, and records accept / dismiss / wrong so the signal-to-noise ratio is measured rather than guessed
42+
- JSON / Markdown output, and a proper CI gate
4043

4144
---
4245

@@ -53,7 +56,7 @@ Auto-detects your OS and CPU architecture, verifies the SHA-256 checksum, and in
5356
Pin a specific version — note that the variable goes on the **`bash`** side of the pipe, not the `curl` side:
5457

5558
```bash
56-
curl -fsSL https://github.com/thinkgrid-labs/diffmind/releases/latest/download/install.sh | VERSION=v0.8.0 bash
59+
curl -fsSL https://github.com/thinkgrid-labs/diffmind/releases/latest/download/install.sh | VERSION=v0.9.0 bash
5760
```
5861

5962
### Windows
@@ -89,11 +92,14 @@ diffmind # review this branch against the repo's default branc
8992

9093
`--branch` is no longer assumed to be `main`: diffmind reads the repository's default branch from `origin/HEAD` and falls back to whichever of `main`/`master`/`develop`/`trunk` actually exists.
9194

95+
A bare `a..b` argument is recognised as a revision range without needing `--range`. The detection is strict — both endpoints must resolve as revisions and the string must not name an existing path — so `diffmind ../lib` and a file genuinely called `a..b` are still treated as paths. Paths after a range narrow it: `diffmind v1.2.0..HEAD src/api/`.
96+
9297
```bash
9398
diffmind --last # just the last commit
9499
diffmind --staged # just what's staged
100+
diffmind v1.2.0..HEAD # an explicit revision range
95101
diffmind src/auth/ # just these paths
96-
diffmind --tui # interactive browser
102+
diffmind --tui # the reviewer's cockpit
97103
```
98104

99105
---
@@ -150,6 +156,7 @@ Findings carry a stable rule ID, shown in the output, so you can silence exactly
150156
| `DM002` | A declaration was removed but is still referenced |
151157
| `DM900.<category>` | A model-authored finding of that category |
152158
| `custom.<slug>` | One of your `.diffmind/rules.toml` rules |
159+
| `rulebook.<id>` | A violation of one of your `.diffmind/rules/*.md` |
153160

154161
### Inline
155162

@@ -183,7 +190,7 @@ Commit `.diffmind/baseline.json`. Future runs report only new issues. The baseli
183190
### GitHub Action
184191

185192
```yaml
186-
- uses: thinkgrid-labs/diffmind@v0.8.0
193+
- uses: thinkgrid-labs/diffmind@v0.9.0
187194
with:
188195
model: 1.5b
189196
fail-on: high
@@ -192,7 +199,7 @@ Commit `.diffmind/baseline.json`. Future runs report only new issues. The baseli
192199
That caches the model, installs the binary, reviews the PR diff, and uploads SARIF so findings appear inline on the diff. For a PR comment instead:
193200
194201
```yaml
195-
- uses: thinkgrid-labs/diffmind@v0.8.0
202+
- uses: thinkgrid-labs/diffmind@v0.9.0
196203
with:
197204
format: markdown
198205
comment: true
@@ -231,13 +238,83 @@ Or via [pre-commit](https://pre-commit.com):
231238
```yaml
232239
repos:
233240
- repo: https://github.com/thinkgrid-labs/diffmind
234-
rev: v0.8.0
241+
rev: v0.9.0
235242
hooks:
236243
- id: diffmind
237244
```
238245

239246
---
240247

248+
## The cockpit — `diffmind --tui`
249+
250+
```bash
251+
diffmind --tui
252+
```
253+
254+
Analysis starts on launch. Each finding shows the **actual hunk the model
255+
reviewed** and the **context it was given** — a finding you cannot check is one
256+
you will eventually stop reading.
257+
258+
| Key | Action |
259+
| --------- | ------------------------------------------------------------ |
260+
| `j` / `k` | Move through findings |
261+
| `a` | Accept — records the verdict and copies a review comment |
262+
| `d` | Dismiss — read, not worth raising |
263+
| `w` | Wrong — the finding was incorrect |
264+
| `PgUp` / `PgDn` | Scroll the detail pane |
265+
| `r` | Re-run |
266+
| `q` | Quit |
267+
268+
Verdicts are written through immediately, so closing the terminal mid-triage
269+
loses nothing. They feed `diffmind stats`.
270+
271+
Accept copies via **OSC 52**, the terminal's own clipboard escape — no
272+
dependency, and it works over SSH. tmux and screen need clipboard passthrough
273+
enabled; if the copy fails you are told, so you never believe you have copied
274+
something you have not.
275+
276+
Because the output is private to you, the tool can afford to be wrong
277+
occasionally: a bad finding costs one keystroke, not an author's afternoon.
278+
279+
---
280+
281+
## Run history
282+
283+
Every review is filed to `.diffmind/runs/<sha>/` — the findings as `run.json`
284+
and `review.md`, plus what the run cost. `diffmind stats` reads them back:
285+
286+
```
287+
Runs 34
288+
Median findings 3
289+
Median time 6.2s
290+
Median tokens 18420
291+
Cache hits 61%
292+
293+
Verdicts 71 accepted · 44 dismissed · 12 wrong
294+
Accept : wrong 5.9:1 (on target)
295+
296+
Most often wrong
297+
7 DM900.maintainability
298+
3 rulebook.house-style
299+
```
300+
301+
The **accept-to-wrong ratio** is the number that decides whether the tool is
302+
earning its keep — noise is this category's known failure mode, and a reviewer
303+
who cannot measure it will just quietly stop running the reviewer. Verdicts come
304+
from the TUI. Dismissals are excluded from the ratio: choosing not to raise a
305+
correct observation is not the tool being wrong.
306+
307+
Run snapshots are overwritten when a sha is reviewed again; verdicts are
308+
append-only and survive `diffmind stats --clear`, because the ratio is only
309+
meaningful over months.
310+
311+
diffmind writes `.diffmind/.gitignore` covering `runs/`, `cache/`, `models/`,
312+
`symbols.json` and `daemon.json` — your review notes stay private, while
313+
`rules/`, `rules.toml`, `config.toml` and `baseline.json` remain committable.
314+
Your repository's own `.gitignore` is never touched.
315+
316+
---
317+
241318
## Daemon mode
242319

243320
Every invocation otherwise pays the model-load cost — seconds, every time.
@@ -267,6 +344,8 @@ triage = "auto" # two-pass triage on large diffs
267344
cache = true
268345
temperature = 0.0 # 0 = greedy and reproducible
269346
max_tokens = 1024
347+
# Extra paths to drop, on top of the built-in noise rules.
348+
ignore = ["**/legacy/**", "*.generated.ts"]
270349
271350
[backend]
272351
kind = "local" # or "ollama" / "openai-compatible"
@@ -277,7 +356,50 @@ api_key_env = "DIFFMIND_API_KEY"
277356

278357
Unknown keys are reported rather than silently ignored.
279358

280-
### Team rules — `.diffmind/rules.toml`
359+
### Review standards — `.diffmind/rules/*.md`
360+
361+
Rules that need judgement rather than a pattern. Written as prose, committed to
362+
the repo, and read by the model on every review — so a team's review culture
363+
becomes a reviewed artifact instead of tacit knowledge.
364+
365+
```bash
366+
diffmind rules init # scaffold .diffmind/rules/default.md
367+
diffmind rules list # what would load, and what each governs
368+
```
369+
370+
```markdown
371+
---
372+
scope: ["src/api/**/*.ts"]
373+
severity: high
374+
---
375+
376+
# API conventions
377+
378+
- Public handlers must return `ApiError`, never a bare string.
379+
- Any new endpoint needs a corresponding entry in `openapi.yaml`.
380+
- Reject changes that widen a response struct without a version bump.
381+
```
382+
383+
| Key | Description |
384+
| ---------- | ----------------------------------------------------------------- |
385+
| `scope` | Globs this rule set governs. Omit to cover the whole repository. |
386+
| `severity` | **Ceiling** for findings attributed to it — never a promotion. |
387+
| `id` | Name used to attribute and suppress. Defaults to the file stem. |
388+
389+
A finding the model attributes to a rule set gets the rule ID
390+
`rulebook.<id>`, so it suppresses like any other:
391+
`// diffmind-ignore-next-line rulebook.api-conventions`. An attribution naming a
392+
rule set that does not govern that file is discarded — a 1.5B will invent a
393+
plausible name, and an invented one could never be suppressed.
394+
395+
Rule bodies go in the *stable* half of the prompt and units are grouped by which
396+
rule sets govern them, so every unit in a group sends a byte-identical prefix.
397+
That is what keeps prompt-prefix caching possible; scoping rules per file would
398+
otherwise make every prompt unique.
399+
400+
A rule set that fails to parse is reported and skipped, never silently ignored.
401+
402+
### Pattern rules — `.diffmind/rules.toml`
281403

282404
Regex rules run before the model: instant, deterministic, zero inference cost.
283405

@@ -317,7 +439,11 @@ category = "security"
317439
diffmind describe # PR title, summary, and test plan
318440
diffmind commit # conventional commit message for staged changes
319441
diffmind commit --apply
442+
diffmind rules init # scaffold .diffmind/rules/default.md
443+
diffmind rules list # show which rule sets load
320444
diffmind index # build the symbol index
445+
diffmind stats # cost and signal over recorded runs
446+
diffmind stats --clear # drop run snapshots (verdicts are kept)
321447
diffmind cache show # cache location and size
322448
diffmind cache clear
323449
```
@@ -335,6 +461,8 @@ Commands:
335461
describe Generate a PR title and description
336462
commit Suggest a conventional commit message
337463
baseline Record current findings as accepted
464+
rules Manage the prose rule sets in .diffmind/rules/
465+
stats Findings, cost and accept/wrong ratio over recorded runs
338466
install-hooks Install git hooks
339467
serve Keep the model resident between runs
340468
cache Inspect or clear the review cache
@@ -345,6 +473,7 @@ Options:
345473
-l, --last Review the last commit only
346474
--staged Review staged changes only
347475
--stdin Read the diff from stdin
476+
--range <RANGE> Review an explicit revision range, e.g. v1.2.0..HEAD
348477
-t, --tui Launch the interactive TUI
349478
--ticket <FILE_OR_TEXT> Acceptance criteria to check against
350479
--min-severity <LEVEL> Minimum severity to report [default: low]
@@ -371,13 +500,14 @@ Options:
371500
## How It Works
372501
373502
1. **Parse** — the diff is parsed once into typed per-file hunks with real pre/post-image line numbers.
374-
2. **Deterministic detectors** — commented-out code, removed-but-used declarations, and your regex rules. No model involved.
375-
3. **Context** — the enclosing function of each hunk (plus definitions of referenced symbols) is pulled from `.diffmind/symbols.json`.
376-
4. **Triage** — on large diffs, a cheap first pass decides which files carry real risk.
377-
5. **Chunked inference** — chunks are sized to the backend's *actual* context window, read from the GGUF metadata.
378-
6. **Constrained decoding** — the sampler consults a JSON state machine before committing each token, so the model cannot emit a preamble, an unbalanced brace, or a truncated string. Output that hits the token cap is repaired rather than discarded.
379-
7. **Anchoring** — findings pointing at a file not in the diff are dropped; off-by-N line numbers snap to the nearest changed line.
380-
8. **Suppression** — inline directives, the baseline, and `--min-confidence` are applied, then results are deduplicated and sorted.
503+
2. **Pre-filter** — lockfiles, `linguist-generated` paths, files carrying a `@generated` banner, minified bundles, assets, snapshots, your `ignore` globs, and hunks that only change whitespace are dropped. Costs nothing, typically removes most of a real branch, and the counts are reported rather than silently applied. Whitespace inside a string literal counts as content, and indentation is never dismissed in Python or YAML.
504+
3. **Deterministic detectors** — commented-out code, removed-but-used declarations, and your regex rules. No model involved.
505+
4. **Context** — the enclosing function of each hunk (plus definitions of referenced symbols) is pulled from `.diffmind/symbols.json`, assembled per chunk so one file's edit does not invalidate another's cached result.
506+
5. **Triage** — on large diffs, a cheap first pass decides which files carry real risk.
507+
6. **Review units** — hunks are grouped into regions of a file rather than cut wherever a line budget ran out, so related hunks are read together and an edit in one function only re-reviews that function. Units are sized to the backend's *actual* context window, read from the GGUF metadata.
508+
7. **Constrained decoding** — the sampler consults a JSON state machine before committing each token, so the model cannot emit a preamble, an unbalanced brace, or a truncated string. Output that hits the token cap is repaired rather than discarded.
509+
8. **Anchoring** — findings pointing at a file not in the diff are dropped; off-by-N line numbers snap to the nearest changed line.
510+
9. **Suppression** — inline directives, the baseline, and `--min-confidence` are applied, then results are deduplicated and sorted.
381511
382512
---
383513
@@ -394,7 +524,10 @@ diffmind/
394524
│ ├── detectors.rs # deterministic rules
395525
│ ├── diff.rs # unified-diff parser, chunking, finding anchoring
396526
│ ├── json_guard.rs # constrained-decoding state machine
527+
│ ├── prefilter.rs # deterministic noise removal, with counts
397528
│ ├── prompt.rs # prompt construction
529+
│ ├── rulebook.rs # prose rule sets (.diffmind/rules/*.md)
530+
│ ├── unit.rs # hunks → review units (the cached, reviewed thing)
398531
│ ├── sarif.rs # SARIF 2.1.0 output
399532
│ └── suppression.rs # inline directives and baselines
400533
└── apps/tui-cli/src/
@@ -412,7 +545,6 @@ diffmind/
412545
413546
- [ ] Auto-fix patches (`diffmind fix`) — gated on remote backends landing, since a 1.5B's patches are not trustworthy enough to apply
414547
- [ ] Cross-file impact analysis — find callers of deleted or renamed functions
415-
- [ ] `diffmind stats` — findings over time
416548
- [ ] VS Code / JetBrains extensions, talking to the daemon
417549
- [ ] Homebrew tap and scoop manifest
418550
- [ ] Fine-tuned review model

apps/tui-cli/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "diffmind"
3-
version = "0.8.0"
3+
version = "0.9.0"
44
edition = "2024"
55
description = "Local-first AI code review agent — powered by on-device inference"
66

0 commit comments

Comments
 (0)