|
2 | 2 |
|
3 | 3 | ## [0.9.0] — 08-12-2026 |
4 | 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 | + |
5 | 21 | ### Security |
6 | 22 |
|
7 | 23 | - **Model weights are now pinned and verified.** Downloads used HuggingFace's |
|
22 | 38 | Existing downloads are verified against the new pins on next use; the current |
23 | 39 | files match, so no re-download is expected. |
24 | 40 |
|
| 41 | +### Added |
| 42 | + |
| 43 | +- **Code graph.** A tree-sitter symbol graph for 13 languages — Rust, |
| 44 | + TypeScript, TSX, JavaScript, Python, Go, Java, C#, Ruby, PHP, C, C++ and |
| 45 | + Scala — stored in `.diffmind/graph.db` and updated incrementally |
| 46 | + by mtime. Replaces the regex symbol index, which could only see `pub`/`export` |
| 47 | + declarations and had no concept of a reference at all. |
| 48 | + Review context now includes **the callers of every changed symbol** — a |
| 49 | + changed signature is judged against the code that depends on it — plus the |
| 50 | + enclosing definition, referenced definitions, and the file's tests. Everything |
| 51 | + stays inside a byte budget, so context does not grow with the repository. |
| 52 | + Bodies are read from the working tree rather than stored, so a snippet can |
| 53 | + never disagree with the file being reviewed. The graph refreshes itself |
| 54 | + incrementally before every review (~0.1s on 647 unchanged files) and builds on |
| 55 | + first use, so it can never fall behind the code; `--no-index` opts out. Adding a language is one entry in |
| 56 | + a table; contributions welcome. |
| 57 | + |
| 58 | +- **Cross-file review units.** When a symbol and code that calls it both change |
| 59 | + in one diff, they are reviewed together as a single unit instead of separately. |
| 60 | + Reviewed apart, the model judges an interaction while seeing only one side of |
| 61 | + it as background. One call replaces two. |
| 62 | +- **Reviewer's cockpit** (`diffmind --tui`). Analyses on launch. Each finding |
| 63 | + shows the actual hunk the model reviewed and the context it was given. |
| 64 | + `a` accepts (and copies a review comment via OSC 52, which works over SSH), |
| 65 | + `d` dismisses, `w` marks wrong. Verdicts are written through immediately. |
| 66 | +- **Review standards as markdown** — `.diffmind/rules/*.md`, scoped by path |
| 67 | + glob, committed to the repo. `diffmind rules init` / `rules list`. A finding |
| 68 | + the model attributes to a rule set gets the ID `rulebook.<id>` and suppresses |
| 69 | + like any other; an attribution naming a rule set that does not govern that |
| 70 | + file is discarded. |
| 71 | +- **Reportable pre-filter.** Lockfiles, `linguist-generated` paths, `@generated` |
| 72 | + banners, minified bundles, assets, snapshots and formatting-only hunks are |
| 73 | + dropped before the model sees them — and the run says what it skipped: |
| 74 | + `312 hunks → 74 reviewable (238 filtered: lockfiles, generated, formatting)`. |
| 75 | + Whitespace inside a string literal counts as content; indentation is never |
| 76 | + dismissed in Python or YAML. |
| 77 | +- **`diffmind stats`** — findings, cost and the accept-to-wrong ratio over |
| 78 | + recorded runs. Every review is filed to `.diffmind/runs/<sha>/`. |
| 79 | +- **Cost reporting** — wall-clock and token counts in the footer, in JSON, and |
| 80 | + in the run record. Estimated counts are marked `~` rather than passed off as |
| 81 | + exact. |
| 82 | +- **Revision ranges** — `diffmind v1.2.0..HEAD`, or `--range`. Paths after a |
| 83 | + range narrow it. |
| 84 | +- **`review.ignore`** globs in `.diffmind/config.toml`. |
| 85 | +- diffmind writes its own `.diffmind/.gitignore`, keeping generated state out of |
| 86 | + git while leaving `rules/`, `rules.toml`, `config.toml` and `baseline.json` |
| 87 | + committable. Your repository's `.gitignore` is not touched. |
| 88 | + |
| 89 | +### Changed |
| 90 | + |
| 91 | +- `Graph::definitions_of` is replaced by `definitions_of_names` (batched) and |
| 92 | + `declarations_overlapping` (span-based). The "prefer a definition in this |
| 93 | + file" rule moved to `rag`, where the file being reviewed is known. |
| 94 | +- `ReviewAnalyzer::analyze`'s progress callback takes the unit being reviewed: |
| 95 | + `Fn(usize, usize)` → `Fn(usize, usize, &ReviewUnit)`. Callers that need to |
| 96 | + show a reader the hunk behind a finding should record it here rather than |
| 97 | + predicting the unit list with `plan_units`. |
| 98 | + |
25 | 99 | ### Fixed |
26 | 100 |
|
27 | 101 | - **A non-ASCII path in a diff header could abort the process.** The |
|
34 | 108 | which matters for binary files and mode changes, where no `+++ b/…` follows to |
35 | 109 | correct it. |
36 | 110 |
|
| 111 | +- **A large rule set could fail the whole review.** Two faults compounding. |
| 112 | + Splitting an oversized prompt halves the *diff*, but the context and rule |
| 113 | + sections were sized from the window alone — identical at every recursion |
| 114 | + level — so a prompt that overran because of its rules failed the same way four |
| 115 | + times and then gave up. And giving up returned an error that propagated out of |
| 116 | + `analyze`, exiting 2 and discarding the findings every *other* unit had |
| 117 | + produced, including deterministic ones that never needed a model. |
| 118 | + |
| 119 | + Both sections now shrink with each split (dropping whole rule sets rather than |
| 120 | + truncating one mid-sentence), and are held to at most half of what the window |
| 121 | + has left, so the diff under review cannot be crowded out. A unit that still |
| 122 | + will not fit is counted in the new `units_too_large` stat and skipped, and the |
| 123 | + footer says so. Unchanged for an ordinary review: at depth 0 on the shipped |
| 124 | + 32K window the budgets are exactly what they were. |
| 125 | + |
37 | 126 | - **A non-ASCII identifier could abort the detector.** `references_identifier` |
38 | 127 | advanced by one *byte* past a match, so a name whose first character is |
39 | 128 | multi-byte — `const élan = 1`, which `extract_declared_name` collects because |
|
97 | 186 | Both are now per-request. The daemon also re-asserts that caching is off above |
98 | 187 | temperature 0 rather than trusting the client to have done so. |
99 | 188 |
|
100 | | -### Internal |
101 | | - |
102 | | -- **End-to-end coverage for `--stdin`.** `apps/tui-cli/tests/stdin_pipeline.rs` |
103 | | - runs the real binary with a diff on stdin and asserts on stdout and the exit |
104 | | - code, with inference supplied by a stub `openai-compatible` endpoint rather |
105 | | - than the bundled model — so no 1.1 GB download is a test dependency, and the |
106 | | - path exercised is a shipped one rather than a test-only seam. |
107 | | - The stub answers each request about whichever file it can see in the prompt, |
108 | | - and records the prompts, so the tests can assert what the model was *shown* and |
109 | | - not merely what came back. |
110 | | - |
111 | | - This is the coverage whose absence let one defect live in four places at once: |
112 | | - each copy had unit tests, and none of them tested the four together. Reverting |
113 | | - all four fixes now fails four of the six new tests. |
| 189 | +- **The result cache never worked across files.** Context was assembled once |
| 190 | + from the whole diff and folded into every chunk's cache key, so editing one |
| 191 | + file invalidated every other file's cached result — a re-review after a |
| 192 | + force-push re-inferred the entire diff. Context is now assembled per unit. |
| 193 | +- **Each chunk was given the wrong context.** The six enclosing functions were |
| 194 | + taken from whichever files sorted first, so most chunks paid for context about |
| 195 | + files they did not contain. |
| 196 | +- **Editing one region of a file re-reviewed the whole file.** Units are grouped |
| 197 | + by region, so a change in one function only invalidates that function. |
| 198 | +- **Glob matching could not express a directory prefix.** `src/api/**` matched |
| 199 | + nothing, and `**/gen/**` was a substring test that also matched |
| 200 | + `src/gen-legacy/`. Affects `rules.toml` `files` patterns as well. |
| 201 | +- **Conflicting input selectors were silently resolved by order.** Passing both |
| 202 | + `--staged` and `--last` reviewed whichever the code checked first; it now |
| 203 | + errors. |
| 204 | +- A diff dominated by a lockfile is no longer refused for size before the |
| 205 | + lockfile is filtered out. |
| 206 | +- The TUI now honours the pre-filter and files a run record, which it did not. |
114 | 207 |
|
115 | 208 | ### Performance |
116 | 209 |
|
@@ -138,101 +231,17 @@ every review — including one that turned out to be fully cached. |
138 | 231 | collision would hand one unit another's context, and confidently wrong context |
139 | 232 | is the failure the whole module exists to prevent. |
140 | 233 |
|
141 | | -### Changed |
142 | | - |
143 | | -- `Graph::definitions_of` is replaced by `definitions_of_names` (batched) and |
144 | | - `declarations_overlapping` (span-based). The "prefer a definition in this |
145 | | - file" rule moved to `rag`, where the file being reviewed is known. |
146 | | -- `ReviewAnalyzer::analyze`'s progress callback takes the unit being reviewed: |
147 | | - `Fn(usize, usize)` → `Fn(usize, usize, &ReviewUnit)`. Callers that need to |
148 | | - show a reader the hunk behind a finding should record it here rather than |
149 | | - predicting the unit list with `plan_units`. |
150 | | -- **Daemon protocol changed** — `ReviewRequest` gains `temperature` and `seed` |
151 | | - and drops the unused `languages` field. The version gate already refuses a |
152 | | - daemon from another build; run `diffmind serve --stop` after upgrading. |
153 | | - |
154 | | - |
155 | | -diffmind was built as a gate: run it, get a verdict, pass or fail. This release |
156 | | -adds the other half — a place to *sit* while deciding what to say about someone |
157 | | -else's branch. Both surfaces share one engine; neither replaces the other. |
158 | | - |
159 | | -### Breaking |
160 | | - |
161 | | -- **`--format json`: `stats.chunks` → `stats.units`**, plus `units_cached` and |
162 | | - `units_unparseable`. The unit of review is now a region of a file rather than |
163 | | - an arbitrary slice of lines, and the field names say so. Exit codes, SARIF and |
164 | | - Markdown output are unchanged, so CI gates are unaffected. |
165 | | -- **Daemon protocol changed.** A running 0.8 daemon is ignored rather than |
166 | | - misused — the version gate already handled this — but you will want |
167 | | - `diffmind serve --stop` after upgrading. |
168 | | -- **`chunk_diff` removed** from the `core-engine` public API, superseded by |
169 | | - `build_units`. |
170 | | - |
171 | | -### Added |
172 | | - |
173 | | -- **Code graph.** A tree-sitter symbol graph for 13 languages — Rust, |
174 | | - TypeScript, TSX, JavaScript, Python, Go, Java, C#, Ruby, PHP, C, C++ and |
175 | | - Scala — stored in `.diffmind/graph.db` and updated incrementally |
176 | | - by mtime. Replaces the regex symbol index, which could only see `pub`/`export` |
177 | | - declarations and had no concept of a reference at all. |
178 | | - Review context now includes **the callers of every changed symbol** — a |
179 | | - changed signature is judged against the code that depends on it — plus the |
180 | | - enclosing definition, referenced definitions, and the file's tests. Everything |
181 | | - stays inside a byte budget, so context does not grow with the repository. |
182 | | - Bodies are read from the working tree rather than stored, so a snippet can |
183 | | - never disagree with the file being reviewed. The graph refreshes itself |
184 | | - incrementally before every review (~0.1s on 647 unchanged files) and builds on |
185 | | - first use, so it can never fall behind the code; `--no-index` opts out. Adding a language is one entry in |
186 | | - a table; contributions welcome. |
187 | | - |
188 | | -- **Cross-file review units.** When a symbol and code that calls it both change |
189 | | - in one diff, they are reviewed together as a single unit instead of separately. |
190 | | - Reviewed apart, the model judges an interaction while seeing only one side of |
191 | | - it as background. One call replaces two. |
192 | | -- **Reviewer's cockpit** (`diffmind --tui`). Analyses on launch. Each finding |
193 | | - shows the actual hunk the model reviewed and the context it was given. |
194 | | - `a` accepts (and copies a review comment via OSC 52, which works over SSH), |
195 | | - `d` dismisses, `w` marks wrong. Verdicts are written through immediately. |
196 | | -- **Review standards as markdown** — `.diffmind/rules/*.md`, scoped by path |
197 | | - glob, committed to the repo. `diffmind rules init` / `rules list`. A finding |
198 | | - the model attributes to a rule set gets the ID `rulebook.<id>` and suppresses |
199 | | - like any other; an attribution naming a rule set that does not govern that |
200 | | - file is discarded. |
201 | | -- **Reportable pre-filter.** Lockfiles, `linguist-generated` paths, `@generated` |
202 | | - banners, minified bundles, assets, snapshots and formatting-only hunks are |
203 | | - dropped before the model sees them — and the run says what it skipped: |
204 | | - `312 hunks → 74 reviewable (238 filtered: lockfiles, generated, formatting)`. |
205 | | - Whitespace inside a string literal counts as content; indentation is never |
206 | | - dismissed in Python or YAML. |
207 | | -- **`diffmind stats`** — findings, cost and the accept-to-wrong ratio over |
208 | | - recorded runs. Every review is filed to `.diffmind/runs/<sha>/`. |
209 | | -- **Cost reporting** — wall-clock and token counts in the footer, in JSON, and |
210 | | - in the run record. Estimated counts are marked `~` rather than passed off as |
211 | | - exact. |
212 | | -- **Revision ranges** — `diffmind v1.2.0..HEAD`, or `--range`. Paths after a |
213 | | - range narrow it. |
214 | | -- **`review.ignore`** globs in `.diffmind/config.toml`. |
215 | | -- diffmind writes its own `.diffmind/.gitignore`, keeping generated state out of |
216 | | - git while leaving `rules/`, `rules.toml`, `config.toml` and `baseline.json` |
217 | | - committable. Your repository's `.gitignore` is not touched. |
| 234 | +### Internal |
218 | 235 |
|
219 | | -### Fixed |
| 236 | +- **End-to-end coverage for `--stdin`.** `apps/tui-cli/tests/stdin_pipeline.rs` |
| 237 | + runs the real binary with a diff on stdin and asserts on stdout and the exit |
| 238 | + code, with inference supplied by a stub `openai-compatible` endpoint rather |
| 239 | + than the bundled model — so no 1.1 GB download is a test dependency, and the |
| 240 | + path exercised is a shipped one rather than a test-only seam. |
| 241 | + The stub answers each request about whichever file it can see in the prompt, |
| 242 | + and records the prompts, so the tests can assert what the model was *shown* and |
| 243 | + not merely what came back. |
220 | 244 |
|
221 | | -- **The result cache never worked across files.** Context was assembled once |
222 | | - from the whole diff and folded into every chunk's cache key, so editing one |
223 | | - file invalidated every other file's cached result — a re-review after a |
224 | | - force-push re-inferred the entire diff. Context is now assembled per unit. |
225 | | -- **Each chunk was given the wrong context.** The six enclosing functions were |
226 | | - taken from whichever files sorted first, so most chunks paid for context about |
227 | | - files they did not contain. |
228 | | -- **Editing one region of a file re-reviewed the whole file.** Units are grouped |
229 | | - by region, so a change in one function only invalidates that function. |
230 | | -- **Glob matching could not express a directory prefix.** `src/api/**` matched |
231 | | - nothing, and `**/gen/**` was a substring test that also matched |
232 | | - `src/gen-legacy/`. Affects `rules.toml` `files` patterns as well. |
233 | | -- **Conflicting input selectors were silently resolved by order.** Passing both |
234 | | - `--staged` and `--last` reviewed whichever the code checked first; it now |
235 | | - errors. |
236 | | -- A diff dominated by a lockfile is no longer refused for size before the |
237 | | - lockfile is filtered out. |
238 | | -- The TUI now honours the pre-filter and files a run record, which it did not. |
| 245 | + This is the coverage whose absence let one defect live in four places at once: |
| 246 | + each copy had unit tests, and none of them tested the four together. Reverting |
| 247 | + all four fixes now fails four of the six new tests. |
0 commit comments