diffctx 1.12.3__tar.gz → 1.13.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- diffctx-1.13.0/CHANGELOG.md +930 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/Cargo.lock +11 -10
- {diffctx-1.12.3 → diffctx-1.13.0}/PKG-INFO +59 -73
- {diffctx-1.12.3/crates/diffctx-native → diffctx-1.13.0}/README.md +60 -74
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/Cargo.toml +3 -2
- {diffctx-1.12.3 → diffctx-1.13.0/crates/diffctx-native}/README.md +60 -74
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/candidate_files.rs +1 -1
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/config/edge_weights.rs +12 -1
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/config/filtering.rs +0 -6
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/config/git.rs +2 -5
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/config/graph_filtering.rs +0 -2
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/config/limits.rs +6 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/config/mode.rs +6 -0
- diffctx-1.13.0/crates/diffctx-native/src/config/scoring.rs +69 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/config/weights.rs +2 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/core.rs +103 -3
- diffctx-1.13.0/crates/diffctx-native/src/deadline.rs +64 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/discovery.rs +204 -23
- diffctx-1.13.0/crates/diffctx-native/src/edges/base.rs +501 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/config_edges/generic.rs +63 -12
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/mod.rs +106 -1
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/c_family.rs +102 -25
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/dotnet.rs +179 -17
- diffctx-1.13.0/crates/diffctx-native/src/edges/semantic/jvm.rs +878 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/tags.rs +4 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/similarity/lexical.rs +45 -6
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/structural/containment.rs +19 -1
- diffctx-1.13.0/crates/diffctx-native/src/edges/structural/sibling.rs +145 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/structural/testing.rs +49 -45
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/excerpt.rs +51 -24
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/filtering.rs +33 -16
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/fragmentation.rs +65 -2
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/git.rs +803 -155
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/lib.rs +5 -0
- diffctx-1.13.0/crates/diffctx-native/src/locate.rs +587 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/main.rs +117 -7
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/memory_pipeline.rs +23 -12
- diffctx-1.13.0/crates/diffctx-native/src/mode.rs +159 -0
- diffctx-1.13.0/crates/diffctx-native/src/parsers/mod.rs +285 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/parsers/tree_sitter_strategy.rs +223 -3
- diffctx-1.13.0/crates/diffctx-native/src/paths.rs +64 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/pipeline.rs +387 -56
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/postpass.rs +101 -10
- diffctx-1.13.0/crates/diffctx-native/src/provenance.rs +190 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/pybridge.rs +112 -90
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/render.rs +115 -16
- diffctx-1.13.0/crates/diffctx-native/src/scoring.rs +1133 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/select.rs +155 -28
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/signatures.rs +35 -2
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/test_harness.rs +24 -2
- diffctx-1.13.0/crates/diffctx-native/src/testfiles.rs +169 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/token_corpus.rs +14 -5
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/types.rs +55 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/utility/needs.rs +82 -41
- {diffctx-1.12.3 → diffctx-1.13.0}/pyproject.toml +7 -2
- {diffctx-1.12.3 → diffctx-1.13.0}/src/diffctx/_app.py +61 -2
- diffctx-1.13.0/src/diffctx/_diffctx.pyi +104 -0
- diffctx-1.13.0/src/diffctx/_native/__init__.py +7 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/src/diffctx/_native/graph_analytics.py +6 -14
- {diffctx-1.12.3 → diffctx-1.13.0}/src/diffctx/_native/graph_export.py +6 -12
- {diffctx-1.12.3 → diffctx-1.13.0}/src/diffctx/_native/pipeline.py +42 -5
- {diffctx-1.12.3 → diffctx-1.13.0}/src/diffctx/cli.py +59 -10
- diffctx-1.13.0/src/diffctx/mcp/README.md +127 -0
- diffctx-1.13.0/src/diffctx/mcp/fetch.py +211 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/src/diffctx/mcp/security.py +22 -11
- {diffctx-1.12.3 → diffctx-1.13.0}/src/diffctx/mcp/server.py +175 -61
- {diffctx-1.12.3 → diffctx-1.13.0}/src/diffctx/tree.py +6 -2
- diffctx-1.13.0/src/diffctx/version.py +1 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/src/diffctx/writer.py +24 -15
- diffctx-1.12.3/CHANGELOG.md +0 -468
- diffctx-1.12.3/crates/diffctx-native/src/config/scoring.rs +0 -21
- diffctx-1.12.3/crates/diffctx-native/src/edges/base.rs +0 -294
- diffctx-1.12.3/crates/diffctx-native/src/edges/semantic/jvm.rs +0 -420
- diffctx-1.12.3/crates/diffctx-native/src/edges/structural/sibling.rs +0 -89
- diffctx-1.12.3/crates/diffctx-native/src/mode.rs +0 -96
- diffctx-1.12.3/crates/diffctx-native/src/parsers/mod.rs +0 -142
- diffctx-1.12.3/crates/diffctx-native/src/scoring.rs +0 -284
- diffctx-1.12.3/src/diffctx/_native/__init__.py +0 -7
- diffctx-1.12.3/src/diffctx/mcp/README.md +0 -161
- diffctx-1.12.3/src/diffctx/version.py +0 -1
- {diffctx-1.12.3 → diffctx-1.13.0}/Cargo.toml +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/LICENSE +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/analytics.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/config/analytics.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/config/bm25.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/config/budget.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/config/category_weights.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/config/env_overrides.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/config/extensions.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/config/fragmentation.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/config/importance.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/config/mod.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/config/needs.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/config/parsers.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/config/render.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/config/selection.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/config/tokenization.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/config_edges/build_system.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/config_edges/cicd.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/config_edges/docker.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/config_edges/helm.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/config_edges/kubernetes.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/config_edges/mod.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/document/mod.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/history/cochange.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/history/mod.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/ansible.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/bazel.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/cargo_edges.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/clojure.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/css.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/dart.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/dbt.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/elixir.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/erlang.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/go.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/graphql.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/haskell.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/javascript.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/julia.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/latex.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/lua.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/mod.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/nim.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/nix.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/ocaml.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/openapi.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/perl.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/php.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/prisma.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/protobuf.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/python.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/r_lang.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/ruby.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/rust_lang.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/shell.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/sql.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/swift.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/terraform.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/zig.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/similarity/mod.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/edges/structural/mod.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/graph.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/graph_export.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/interval.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/languages.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/parsers/config_parser.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/parsers/generic.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/parsers/markdown.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/peak_rss.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/ppr.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/project_graph.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/stopwords.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/tokenizer.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/utility/boltzmann.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/utility/importance.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/utility/mod.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/crates/diffctx-native/src/utility/scoring.rs +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/rust-toolchain.toml +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/src/diffctx/__init__.py +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/src/diffctx/__main__.py +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/src/diffctx/_native/project_graph.py +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/src/diffctx/clipboard.py +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/src/diffctx/ignore.py +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/src/diffctx/logger.py +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/src/diffctx/mcp/__init__.py +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/src/diffctx/mcp/__main__.py +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/src/diffctx/mcp/formatting.py +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/src/diffctx/py.typed +0 -0
- {diffctx-1.12.3 → diffctx-1.13.0}/src/diffctx/tokens.py +0 -0
|
@@ -0,0 +1,930 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Inline `<script>` and `<style>` in HTML are parsed with their own
|
|
13
|
+
grammars** (#181). A changed function inside a single-file web app's script
|
|
14
|
+
now surfaces as a named definition fragment at its real file lines instead
|
|
15
|
+
of degrading to a raw line window with no symbol boundaries. JS and CSS
|
|
16
|
+
fragments overlay the `script_element`/`style_element` containers the way
|
|
17
|
+
methods overlay a class; `type=` attributes naming non-JS payloads (JSON,
|
|
18
|
+
templates) opt out.
|
|
19
|
+
|
|
20
|
+
- **`--diff <duration>`** — a time window instead of a revision: `24h`, `8d`,
|
|
21
|
+
`90min`, `1h30m`, `2w` (units `s`, `m`/`min`, `h`, `d`, `w`, composable). The
|
|
22
|
+
window resolves to the last commit before it and diffs the working tree
|
|
23
|
+
against that, so one flag covers the commits made inside the window plus the
|
|
24
|
+
uncommitted and untracked work on top — the "what have I touched today"
|
|
25
|
+
question, which previously meant counting `HEAD~n` by hand. A window older
|
|
26
|
+
than the repository falls back to the empty tree, so nothing is silently
|
|
27
|
+
dropped. Both CLIs and the MCP `diff_ref` share the resolver; a ref that
|
|
28
|
+
happens to look like a duration (a branch `24h`, an abbreviated sha) is
|
|
29
|
+
probed first and keeps its git meaning, so no existing invocation changes.
|
|
30
|
+
|
|
31
|
+
- **`locate` discloses its own blind spots** — a `coverage` block and an
|
|
32
|
+
`overflow` ranking, both omitted when there is nothing to report so a clean
|
|
33
|
+
run costs no tokens. Trust drives repeat calls: an agent told honestly where
|
|
34
|
+
the selection is thin can grep the gap itself, while one told nothing has to
|
|
35
|
+
distrust the whole answer.
|
|
36
|
+
|
|
37
|
+
`coverage.unparsed_files` names changed files whose language diffctx claims to
|
|
38
|
+
parse but which yielded no symbol-level structure — nothing else in the output
|
|
39
|
+
says the parser came back empty there. `zero_edge_files` names changed files
|
|
40
|
+
with no graph edge in either direction, where relevance had no path to travel.
|
|
41
|
+
`ppr_truncated` reports a diffusion cut short by its push cap.
|
|
42
|
+
`overflow` ranks the admitted candidates that did not fit, with a one-line
|
|
43
|
+
`why` and no bodies, capped at 50 entries against a full `overflow_count`.
|
|
44
|
+
|
|
45
|
+
`coverage.next_up` counts how many of those a 25% larger budget would admit,
|
|
46
|
+
and is zero when the budget was not what stopped selection — at `--budget -1`
|
|
47
|
+
nothing is crowded out by tokens, so there is nothing to report. It feeds a
|
|
48
|
+
documented `confidence` heuristic in [0, 1]
|
|
49
|
+
(`parsed_share * linked_share * fit_share`, less 0.1 when PPR truncated),
|
|
50
|
+
which says how much of the changed surface the run could see and fit and
|
|
51
|
+
deliberately does **not** claim the selection is correct. Pack output is
|
|
52
|
+
byte-unchanged — verified against the full 2902-case corpus (#136).
|
|
53
|
+
- **`--scoring pit`** — percentile fusion, the successor to `rrf`. Both blend
|
|
54
|
+
the structural (`ego`) and lexical (`bm25`) signals over the same candidate
|
|
55
|
+
universe; the difference is what is blended. `rrf` reduces each component to a
|
|
56
|
+
rank, which discards the magnitude that says "this scored near zero", and on
|
|
57
|
+
the oracle corpus that costs 79 cases against plain `ego` — every loss on
|
|
58
|
+
precision, because BM25 gives any generic-token match a small positive score
|
|
59
|
+
and `1/(k + rank)` promotes it to real fused mass. `pit` keeps the position
|
|
60
|
+
instead, via each component's empirical CDF: a fragment in the 5th percentile
|
|
61
|
+
of a signal contributes 0.05 from it, so two weak opinions cannot manufacture a
|
|
62
|
+
strong candidate. `score = blend·PIT(ego) + (1-blend)·PIT(bm25) + bonus·[both
|
|
63
|
+
in top-k]`, with `DIFFCTX_PIT_BLEND=0.65`, `DIFFCTX_PIT_AGREEMENT_BONUS=0.10`,
|
|
64
|
+
`DIFFCTX_PIT_AGREEMENT_TOP_K=20`. Measured on the full corpus it recovers 40 of
|
|
65
|
+
those 79 cases (`rrf` 450 below-threshold, `pit` 410) and still trails `ego`
|
|
66
|
+
(371), so **`ego` remains the default** and fusion has yet to earn it (#125).
|
|
67
|
+
|
|
68
|
+
Ablations settled *why*, and the answer is not calibration. At `blend=1.0` the
|
|
69
|
+
lexical arm contributes nothing to the score, and fusion still scores 388
|
|
70
|
+
against ego's 371 — so no mixing weight can recover the gap. Substituting the
|
|
71
|
+
identity for the percentile at that blend reproduces ego **exactly** (371),
|
|
72
|
+
which rules out every other difference between the fusion path and ego: the
|
|
73
|
+
restricted score map, the max-normalisation with cores pinned to 1.0, and
|
|
74
|
+
`finish_scoring` over the pre-filtered union all cost zero. The entire
|
|
75
|
+
structural gap belongs to the transform.
|
|
76
|
+
|
|
77
|
+
`DIFFCTX_PIT_TRANSFORM=maxnorm` fuses the components on their rescaled scores
|
|
78
|
+
instead of their distributional position, and dominates the percentile at
|
|
79
|
+
every blend measured (371 / 371 / 380 / 396 at blend 1.00 / 0.95 / 0.85 / 0.65
|
|
80
|
+
against 388 and 412 for the percentile). It still does not beat ego: both
|
|
81
|
+
curves rise monotonically with lexical weight, so in both families the optimum
|
|
82
|
+
sits at zero lexical weight.
|
|
83
|
+
|
|
84
|
+
One number invites a wrong reading: `--scoring bm25` alone scores 124, far
|
|
85
|
+
ahead of ego's 371. It is not better — it emits a median of 6 fragments to
|
|
86
|
+
ego's 10, half of them the change itself, and 71% of its failures are lost
|
|
87
|
+
recall against ego's 19%. The corpus metric `recall * (1 - forbidden_rate)`
|
|
88
|
+
has no over-selection term and a saturating forbidden term, so it rewards
|
|
89
|
+
conservatism; the comparison is a metric artefact, not a ranking result.
|
|
90
|
+
- **`--scoring rrf`** — reciprocal-rank fusion of the structural (`ego`) and
|
|
91
|
+
lexical (`bm25`) signals: each ranks the same candidate universe, and a
|
|
92
|
+
fragment scores `Σ 1/(k + rank_i)` with `k=60` (`DIFFCTX_RRF_K`). Fusion on
|
|
93
|
+
ranks alone removes the score-scale calibration between the two signals
|
|
94
|
+
that made the deployed mixture rank below its own lexical component on
|
|
95
|
+
genuine retrieval, and the candidate set becomes their union rather than
|
|
96
|
+
either one alone. EGO's structural guards (hub-noise and
|
|
97
|
+
generic-config-only suppression) and the per-file cap are re-applied to
|
|
98
|
+
the union against the fused scores, so the wider candidate set does not
|
|
99
|
+
leak the noise EGO already rejects. `ego` remains the default (#125).
|
|
100
|
+
- **`--mode locate`** emits the ranked selection as compact
|
|
101
|
+
`diffctx.locate.v1` JSON — path, line range, kind, symbol, relevance score
|
|
102
|
+
and machine-readable provenance reasons (`changed`, `edge` with category /
|
|
103
|
+
strongest source / mass, `proximity` seed-hops) with **no source bodies**:
|
|
104
|
+
a navigation prior for agents at a fraction of pack-mode tokens. Available
|
|
105
|
+
on the Python CLI, the native binary, and the MCP `diffctx_context` tool
|
|
106
|
+
(`mode="locate"`); pack output is byte-unchanged (#126). Each item carries
|
|
107
|
+
a coarse impact `group` (`test` / `type` / `config`) and the header a
|
|
108
|
+
blast-radius `summary` (files, changed, context, tests) — with
|
|
109
|
+
`--diff --mode locate` this answers "what does my uncommitted change
|
|
110
|
+
touch, and which tests cover it" in one call (#135).
|
|
111
|
+
- `DIFFCTX_PROVENANCE_DUMP=<path>` writes one JSONL record per scored
|
|
112
|
+
candidate — relevance, core/selected verdicts, seed-hop distance, and
|
|
113
|
+
per-edge-category incoming mass — for offline analysis of why the selector
|
|
114
|
+
included or skipped a fragment. Experimental telemetry, not a stable
|
|
115
|
+
interface; the rendered output is byte-identical with the variable set
|
|
116
|
+
(#93).
|
|
117
|
+
- The YAML test-case schema can express **deletions and renames**
|
|
118
|
+
(`repo.deleted_files`, `repo.renamed_files`), so rename regression cases
|
|
119
|
+
exercise git's real rename path instead of silently degrading to adds
|
|
120
|
+
(#176).
|
|
121
|
+
|
|
122
|
+
### Security
|
|
123
|
+
|
|
124
|
+
- **A symlinked directory inside a repository was a way out of it.** The
|
|
125
|
+
`fragment_ids` reader checked containment lexically — rejecting `..` and
|
|
126
|
+
absolute paths — which admits `escape/secret.txt` where `escape/` is a symlink
|
|
127
|
+
pointing above the repository root. A property fuzz over hostile path shapes
|
|
128
|
+
found it reading a planted file outside the jail. Containment is now decided
|
|
129
|
+
after resolution, the fuzz runs in CI, and the case is pinned separately so it
|
|
130
|
+
holds where hypothesis is unavailable.
|
|
131
|
+
- **Releases are verifiable.** Every artifact attached to a release — wheels,
|
|
132
|
+
sdist, standalone binaries and the SBOM itself — now carries signed SLSA build
|
|
133
|
+
provenance from the release workflow's OIDC identity
|
|
134
|
+
(`gh attestation verify <file> --repo nikolay-e/diffctx`), PyPI uploads state
|
|
135
|
+
`attestations: true` explicitly rather than relying on an action default that
|
|
136
|
+
has changed between versions, and a CycloneDX SBOM ships as a release asset.
|
|
137
|
+
The binaries are what this is for: unlike the wheels they had no package index
|
|
138
|
+
vouching for them. SECURITY.md documents the verification commands.
|
|
139
|
+
- **Error payloads no longer disclose resolved paths.** A refusal used to name
|
|
140
|
+
the path the filesystem resolved to, which told a caller that had just been
|
|
141
|
+
denied exactly what lay outside its jail; and an `OSError` rendered with its
|
|
142
|
+
absolute filename into returned content. Messages now echo only the argument
|
|
143
|
+
the caller supplied, and carry no traceback (#147).
|
|
144
|
+
|
|
145
|
+
### Changed
|
|
146
|
+
|
|
147
|
+
- **MCP: one tool instead of three.** `get_diff_context` becomes
|
|
148
|
+
`diffctx_context` and its `diff_range` parameter becomes `diff_ref`;
|
|
149
|
+
`get_tree_map` and `get_file_context` are off by default and return with
|
|
150
|
+
`DIFFCTX_MCP_LEGACY_TOOLS=1`. Tool definitions are sent on every request of
|
|
151
|
+
every session before any work happens, and the three-tool surface cost 1063
|
|
152
|
+
tokens of every context window it was installed in against 267 for the single
|
|
153
|
+
tool (-75%, `o200k_base` over each serialized `{name, description,
|
|
154
|
+
inputSchema}`).
|
|
155
|
+
|
|
156
|
+
The default `mode` is now `"locate"`, and the new `fragment_ids` parameter
|
|
157
|
+
reads the bodies of ids from a ranking — `"<path>:<lines>"`, taken straight
|
|
158
|
+
from locate's own fields — so a diff question resolves in two calls that pay
|
|
159
|
+
only for the fragments actually chosen. `mode="pack"` restores the previous
|
|
160
|
+
one-call behaviour. Bodies are read at the diff range's end revision rather
|
|
161
|
+
than from the working tree, so spans from a historical range are not
|
|
162
|
+
mis-sliced, and `.diffctx/ignore` is enforced on `fragment_ids` as it is
|
|
163
|
+
everywhere else (#127).
|
|
164
|
+
|
|
165
|
+
### Fixed
|
|
166
|
+
|
|
167
|
+
- **The `.diffctx/ignore` withheld-files count counts files, not hunks.** A
|
|
168
|
+
withheld file with several separated edits inflated the "N changed file(s)
|
|
169
|
+
withheld" notice — the one number a reviewer uses to judge how much of the
|
|
170
|
+
change the output actually shows.
|
|
171
|
+
|
|
172
|
+
- **C# and Scala regained real cross-file reach after the tags gate** (#131,
|
|
173
|
+
#179). The tags fallback had been these languages' only working linkage:
|
|
174
|
+
Scala's import regex captured a truncated prefix (`com.foo.` instead of
|
|
175
|
+
`com.foo.Bar`), so production Scala produced literally zero import edges;
|
|
176
|
+
chained `package` clauses and `package object` parsed wrong; C# `using`
|
|
177
|
+
resolution required the namespace declaration to sit in the same fragment
|
|
178
|
+
as the lookup key, which per-method fragmentation almost never satisfies.
|
|
179
|
+
Scala imports are now parsed structurally (brace selectors `{A, B => C}`,
|
|
180
|
+
wildcards `._`/`.*`/`given`, object-rooted prefixes like `Tables._`), the
|
|
181
|
+
C# `using` regex accepts aliases, and both builders gained a member-use
|
|
182
|
+
channel: a `receiver.member(...)` call site links to the fragments defining
|
|
183
|
+
that member, but only between file pairs already related through an import,
|
|
184
|
+
a named type, or inheritance — a name match alone stays tags-grade evidence
|
|
185
|
+
and is dropped. Type-name lookups now carry the same ≤8-file ambiguity bar
|
|
186
|
+
as the C-family builder instead of fanning out unbounded. Corpus: 19 cases
|
|
187
|
+
fixed / 1 broken (281 → 263 known-below-threshold), including 13 of the 14
|
|
188
|
+
C#/Scala one-hop cases the tags gate broke.
|
|
189
|
+
|
|
190
|
+
- **Graph construction no longer hangs on large repositories** (#116, #121).
|
|
191
|
+
Four independent fan-out defects each produced tens of millions of edges on
|
|
192
|
+
envoy-class repos (6k C-family files, 520 sharing the stem `config`):
|
|
193
|
+
`TestEdgeBuilder` paired every test fragment with every fragment of every
|
|
194
|
+
same-stem file (180M edges alone); the C-family builder linked includes by
|
|
195
|
+
bare basename and cross-multiplied same-stem headers×impls at fragment
|
|
196
|
+
level; `link_by_path_match` substring-matched every reference against every
|
|
197
|
+
indexed path; and the config-to-code builder ran every key regex over every
|
|
198
|
+
code fragment (replaced by one Aho-Corasick pass with exact `\b` semantics —
|
|
199
|
+
byte-identical output, 42s → 1.2s measured). Together: an envoy commit that
|
|
200
|
+
never finished under 240s now completes in ~60s; on the 372-instance dcbench
|
|
201
|
+
corpus the produced count rose from 185 to 207 at an unchanged 60s cap.
|
|
202
|
+
Resolution rules that made it possible: a name carried by more than 8 files
|
|
203
|
+
cannot identify a target (ambiguity cap), test/header pairing is scoped to
|
|
204
|
+
the co-located directory first, and file-level relations (an include, a
|
|
205
|
+
path reference, a header/impl pair) now link one representative fragment
|
|
206
|
+
per file — the same representative the sibling builder always used — rather
|
|
207
|
+
than every-fragment-to-every-fragment.
|
|
208
|
+
- **The compute deadline now covers the library path** (#121). `timeout` only
|
|
209
|
+
ever bounded git subprocesses; pyo3/MCP callers could sit through an
|
|
210
|
+
unbounded edge build (observed: 8+ minutes past a 420s deadline). The edge
|
|
211
|
+
phase now checks a wall-clock deadline between builders and surfaces expiry
|
|
212
|
+
as an error instead of a hang.
|
|
213
|
+
- **Token cache eviction actually reaches its cap** (#122). Evicting one
|
|
214
|
+
random shard of 256 per run is the coupon collector's problem (~1570 runs
|
|
215
|
+
to touch every shard); the cache was measured at 6.3GB against its 512MB
|
|
216
|
+
cap. Each run now sweeps a 16-shard window, reaching full coverage in ~50
|
|
217
|
+
runs.
|
|
218
|
+
- **A tiny edit no longer ships the enclosing body as context** (#184). The
|
|
219
|
+
excerpt downshift compresses a changed oversized function to its hunk
|
|
220
|
+
window, but the body's gap chunks re-entered the output as *context* — a
|
|
221
|
+
2-line edit in a 100-line function shipped 81 lines of the body it had just
|
|
222
|
+
compressed. Context candidates that are slices of a core fragment's span
|
|
223
|
+
are now dropped; signature stubs stay.
|
|
224
|
+
- **Tags edges are now a fallback, not dual coverage** (#131). The
|
|
225
|
+
identifier-pair builder ran unconditionally, duplicating every dedicated
|
|
226
|
+
semantic edge with same-weight noise; its emissions now survive only where
|
|
227
|
+
at least one endpoint file has no dedicated semantic edge at all. Largest
|
|
228
|
+
single corpus improvement on record: 118 cases fixed, 29 broken (net −89;
|
|
229
|
+
baseline 371 → 281, one stale Swift `xfail` retired). On real commits the
|
|
230
|
+
weakest measured languages double their nontrivial recall (Swift 0.049 →
|
|
231
|
+
0.104, Elixir 0.059 → 0.137) with Tier-1 languages unchanged; the two
|
|
232
|
+
concentrated losses (C#, Scala one-hop reach that only tags provided) are
|
|
233
|
+
the named target of the dedicated-builder work in #179.
|
|
234
|
+
- **A gitignore negation no longer excludes the file it un-ignores** (#193).
|
|
235
|
+
`check-ignore -v` prints a record when the last matching pattern is a
|
|
236
|
+
negation — the path is then explicitly NOT ignored — and the record parser
|
|
237
|
+
treated any record as an exclusion, silently dropping files like an
|
|
238
|
+
un-ignored `SECURITY.md` from the diff context. Negation records are now
|
|
239
|
+
skipped for gitignore and `.diffctx/ignore` alike: a policy negation is the
|
|
240
|
+
user re-including a path in the policy's own terms.
|
|
241
|
+
- **Withheld changed files are visible as withheld** (#188). A changed file
|
|
242
|
+
excluded by ignore rules vanished silently — a reviewer reading the output
|
|
243
|
+
filed "no tests" against a change whose tests the tool had dropped.
|
|
244
|
+
gitignore exclusions are now listed by path under `ignored_changes`;
|
|
245
|
+
`.diffctx/ignore` exclusions surface as `policy_excluded_count` only, since
|
|
246
|
+
re-publishing the paths a declared confidentiality policy withholds would
|
|
247
|
+
undo the policy (#85).
|
|
248
|
+
|
|
249
|
+
- **`--scoring pit` was rejected by both CLIs.** The engine, the MCP server and
|
|
250
|
+
the eval harness all accepted the mode, while `diffctx --scoring pit` and the
|
|
251
|
+
standalone binary refused it: each CLI enumerated the accepted values in its
|
|
252
|
+
own literal, and neither was updated when the mode landed. Both now read
|
|
253
|
+
`SCORING_MODES` from the engine, and a test pins the list in both directions —
|
|
254
|
+
a mode the engine parses but does not advertise is as much a defect as the
|
|
255
|
+
reverse (#125).
|
|
256
|
+
|
|
257
|
+
- **`.diffctx/ignore` no longer stops applying when `git check-ignore` fails or
|
|
258
|
+
when a path contains a newline.** Two holes, both silent. The batched check
|
|
259
|
+
passed every diff path *plus every ancestor directory* as argv, so a
|
|
260
|
+
monorepo-sized range hit the platform limit, git failed to spawn, and the
|
|
261
|
+
result was read as "nothing is ignored" — the one answer that publishes what
|
|
262
|
+
the user asked to withhold. Paths now go over `--stdin` (fed from a private
|
|
263
|
+
temp file, not a pipe we write ourselves — writing the payload before reading
|
|
264
|
+
stdout deadlocks once it outgrows the pipe buffer), and the exchange is
|
|
265
|
+
NUL-delimited in both directions: line-delimited input split
|
|
266
|
+
`secret\nname.py` into two phantom queries, git answered about the stem, and
|
|
267
|
+
the real path never got a verdict. On failure the check now fails **closed**
|
|
268
|
+
when `.diffctx/ignore` declares patterns — excluding everything queried rather
|
|
269
|
+
than risk publishing it — and stays open when it declares none, so a bare
|
|
270
|
+
clone (where `check-ignore` cannot run at all) still produces output.
|
|
271
|
+
- **An untracked file is no longer loaded into memory to count its lines.** The
|
|
272
|
+
untracked scan runs before any size filter (`max_changed_file_size` is
|
|
273
|
+
enforced later, in fragmentation), so a dirty tree holding one multi-GB log
|
|
274
|
+
allocated all of it up front. Counting is now chunked over bytes with
|
|
275
|
+
streaming UTF-8 validation; a minified bundle that is one line hundreds of
|
|
276
|
+
megabytes long is bounded too, which a line-based reader would not have been.
|
|
277
|
+
Line counts are unchanged.
|
|
278
|
+
- **Acronym-prefixed test files are recognised** — `XMLTest`, `HTTPTest`,
|
|
279
|
+
`DBTest`, `UITest`, `IOTest`, `JSONSpec` all read as ordinary source because
|
|
280
|
+
the CamelCase rule demanded a lowercase character before the marker. A capital
|
|
281
|
+
`T` is itself the word boundary, and the markers are matched case-sensitively,
|
|
282
|
+
so `latest`/`contest`/`attest` were never at risk from dropping that guard
|
|
283
|
+
(#182). `PodSpec`/`JobSpec` and `ABTest` are accepted over-classifications:
|
|
284
|
+
no name-based rule separates them from `AuthSpec` and `FooTest`.
|
|
285
|
+
- **`locate` counted tests with its own weaker classifier**, so `summary.tests`
|
|
286
|
+
in the blast-radius block undercounted: it lowercased the path first, which
|
|
287
|
+
hides `FooTest.java`, `AuthSpec.scala`, `widget-spec.js` and `src/tests.rs`.
|
|
288
|
+
It now shares `crate::testfiles` with the edge builder and the needs matcher.
|
|
289
|
+
A `testing/` directory no longer counts as a test tree (it is Go's stdlib
|
|
290
|
+
package name and such directories hold helpers). Affects
|
|
291
|
+
`diffctx.locate.v1` output only; pack selection is unchanged.
|
|
292
|
+
- **An inverted line span can no longer reach a `FragmentId`.** `line_count()`
|
|
293
|
+
is `end - start + 1` on unsigned integers, so it panicked in debug and wrapped
|
|
294
|
+
to ~4 billion in release, always somewhere downstream of whoever built the
|
|
295
|
+
span; it had been fixed twice at call sites. The constructor now asserts in
|
|
296
|
+
debug and clamps to a one-line span in release.
|
|
297
|
+
- **Rename records with a missing destination are no longer treated as
|
|
298
|
+
renames.** The two `-z` walkers disagreed on validation; they are now one.
|
|
299
|
+
|
|
300
|
+
- **The lexical similarity builder no longer holds an unbounded pairwise
|
|
301
|
+
accumulator** (#116). Its output is bounded by `top_k_neighbors`, but the
|
|
302
|
+
intermediate `FxHashMap<(u32, u32), f32>` was not: it held every distinct
|
|
303
|
+
fragment pair co-occurring in any posting list under `max_postings`, i.e. up to
|
|
304
|
+
`terms x C(max_postings, 2)` entries. On one large instance that reached 199M
|
|
305
|
+
pairs and tens of GB resident. Contributions now accumulate into a flat vector
|
|
306
|
+
and reduce by pair — 12 bytes per contribution against hashbrown's ~16 plus a
|
|
307
|
+
transient copy of the whole table on every doubling rehash, so peak drops
|
|
308
|
+
several-fold and the rehash spikes disappear. The sort is deliberately
|
|
309
|
+
**stable**, which preserves each pair's original contribution order and keeps
|
|
310
|
+
the f32 sums bit-identical; `sort_unstable` would reorder within a run and f32
|
|
311
|
+
addition is not associative, which could flip a pair across `min_similarity`.
|
|
312
|
+
Pass 6's top-k cut now breaks weight ties by neighbour index, since candidate
|
|
313
|
+
order derives from a sorted pair list rather than hashmap iteration. Verified
|
|
314
|
+
byte-identical output across four scoring modes x three diff ranges, and
|
|
315
|
+
corpus-neutral (2902/2902).
|
|
316
|
+
- **One answer to "is this a test file"** (#182). Two implementations disagreed:
|
|
317
|
+
a per-language dispatch gating `TestEdge` emission and a flat suffix list
|
|
318
|
+
gating test-need match strength, so a `.kts` file was a test to one and not the
|
|
319
|
+
other and the two halves of "this is a test for the changed code" could hold
|
|
320
|
+
independently. Both also accepted any stem merely *ending* in the letters
|
|
321
|
+
`test` — they lowercased the name before comparing, which destroys the
|
|
322
|
+
CamelCase boundary the JVM/Scala convention relies on, so `latest`, `greatest`,
|
|
323
|
+
`contest` and `attest` were all classified as tests. Now one
|
|
324
|
+
`testfiles::is_test_path`, where a `test`/`spec` marker counts only at a word
|
|
325
|
+
boundary: its own `_`/`-`/`.`-delimited segment, or a capitalised `Test`/`Spec`
|
|
326
|
+
in the original name. `conftest.py` keeps its previous non-test classification,
|
|
327
|
+
which corpus cases depend on. Corpus-neutral (2902/2902).
|
|
328
|
+
- **Latency accounting reconciles with the wall clock** (#183). The phases
|
|
329
|
+
reported only the heavy stage, so a 182s run showed 5.8s of instrumented work
|
|
330
|
+
with nothing to say where the rest went — which is what made a slow range look
|
|
331
|
+
like an unexplained hang. The pre-heavy stage (hunk parse, untracked scan,
|
|
332
|
+
ignore resolution, the `git diff` calls) is now timed as `pre_phase_ms` and
|
|
333
|
+
included in `total_ms`, and the debug log emits `selection` (which has always
|
|
334
|
+
covered the three post-passes) alongside the heavy line. On the range from #121
|
|
335
|
+
that immediately localises the cost: `selection 216.5s, total 222.4s`. The
|
|
336
|
+
reported phases now sum to the total within a render-sized residual, and a
|
|
337
|
+
Python-level test asserts that so a future stage added outside the
|
|
338
|
+
instrumentation fails loudly instead of vanishing — it caught `pre_phase_ms`
|
|
339
|
+
missing from both pybridge call sites while being written.
|
|
340
|
+
- **Flat files finally get sub-file granularity** (#105, #107). An uncovered
|
|
341
|
+
region became one fragment however long it was, so a file the grammar extracts
|
|
342
|
+
nothing from — a flat bash script, a `CMakeLists.txt`, any language without a
|
|
343
|
+
grammar — collapsed into a single whole-file chunk and nothing narrower could
|
|
344
|
+
ever be selected. Long gaps are now split into bounded chunks, cutting at a
|
|
345
|
+
blank line where one is within reach, reusing the thresholds that already
|
|
346
|
+
govern sub-fragmenting large definitions rather than adding a second size
|
|
347
|
+
policy. Two consequences: a one-line diff no longer renders the whole file, and
|
|
348
|
+
the file's *unchanged* remainder becomes available as context at a useful
|
|
349
|
+
granularity instead of being all-or-nothing. On the corpus this moved
|
|
350
|
+
`gap_160_shell_terraform_vault_init_single_import` from below-threshold to a
|
|
351
|
+
full 100%, so its baseline entry is gone.
|
|
352
|
+
- **`xfail:` in a YAML case was an unconditional silent skip.** The runner
|
|
353
|
+
returned success before building the repo, so a marked case proved nothing and
|
|
354
|
+
an XPASS was unobservable — the day its bug got fixed it still reported a pass.
|
|
355
|
+
`known_below_threshold.txt` is enforced bidirectionally for exactly this
|
|
356
|
+
reason; the two suppression mechanisms now agree. Marked cases run, and one
|
|
357
|
+
that passes fails with instructions to drop the marker. Twelve of the 44
|
|
358
|
+
markers turned out to be stale and were removed (ansible jinja templates,
|
|
359
|
+
HTML `script src`, Rust `build.rs`, an abstract-class case, six C#/PHP one-hop
|
|
360
|
+
cases, Haskell record syntax, a shell/terraform same-file case); 32 remain
|
|
361
|
+
legitimately failing.
|
|
362
|
+
- **Excerpt downshift for mostly-unchanged cores** (#149, closing #105, #107 and
|
|
363
|
+
the scoping half of #114). A core fragment is now rendered as its hunk-window
|
|
364
|
+
excerpt whenever that window covers no more than half of it, instead of only
|
|
365
|
+
when the budget forces the substitution. The old rule made granularity a
|
|
366
|
+
function of leftover budget rather than of how much actually changed: the same
|
|
367
|
+
402-line `CMakeLists.txt` shipped whole at `--budget 8000` and tightly
|
|
368
|
+
excerpted at a small budget.
|
|
369
|
+
|
|
370
|
+
Two changes make it work. Excerpts are now generated for kinds that *have* a
|
|
371
|
+
signature variant as well — a signature is not a substitute for a
|
|
372
|
+
mostly-unchanged function, because it drops the changed lines, which is the
|
|
373
|
+
one thing the fragment was selected to carry. And `select_core_fragments` now
|
|
374
|
+
reports which cores it satisfied rather than letting the caller infer it from
|
|
375
|
+
id membership: a core represented by a substitute has a different id, so it
|
|
376
|
+
looked "skipped" and the full fragment was handed straight back to the greedy,
|
|
377
|
+
which re-emitted it alongside the excerpt.
|
|
378
|
+
|
|
379
|
+
`render` also had to agree with `locate` that an `Excerpt` carries the
|
|
380
|
+
`changed` role. Its id is a synthetic span cut out of the core and is not in
|
|
381
|
+
`core_ids`, so without that the downshift would have stripped the change
|
|
382
|
+
marker from the output — worse than the over-dump it replaces.
|
|
383
|
+
|
|
384
|
+
Measured on the reported shapes: a 122-line bash script with one changed line
|
|
385
|
+
goes from the whole file to a 7-line window; a 402-line `CMakeLists.txt` from
|
|
386
|
+
the whole file to 7 lines; a 264-line JavaScript function with two changed
|
|
387
|
+
lines from the whole function to an 8-line window plus the enclosing
|
|
388
|
+
signature as context.
|
|
389
|
+
- **Diff-header paths escaping the repository root were accepted on Linux.**
|
|
390
|
+
`Path::starts_with` compares components, not locations, and `canonicalize`
|
|
391
|
+
fails for a path that does not exist — the normal case for the old side of a
|
|
392
|
+
deletion. The guard fell back to the lexically joined path, and
|
|
393
|
+
`<root>/../../escape.py` starts with `<root>` component-wise. It only failed
|
|
394
|
+
on macOS, where the temp root canonicalizes through `/var → /private/var`, so
|
|
395
|
+
the protection rested on an accident of filesystem layout; on Linux the
|
|
396
|
+
accepted path reached `read_file_content`, whose `exists()` check the OS
|
|
397
|
+
resolves through `..`, so content outside the repository could be read into
|
|
398
|
+
the output. A `..` or absolute component is now rejected before any
|
|
399
|
+
resolution. `--with-raw-diff`'s section filter was a second copy of the same
|
|
400
|
+
guard with the same hole; both now share one resolver (#147).
|
|
401
|
+
- **`core.excludesFile` was written to a guessable name with `fs::write`**,
|
|
402
|
+
which follows a symlink and truncates its target — so a pre-planted name in
|
|
403
|
+
the shared temp directory could redirect the write. Now `O_CREAT | O_EXCL`
|
|
404
|
+
with mode 0600 and a bounded retry (#147).
|
|
405
|
+
- **A rejected or unrecognized diff header charged its hunks to the previous
|
|
406
|
+
file.** The hunk loop could not tell "not a path line" from "path refused",
|
|
407
|
+
so the previous file's paths stayed live. Reset now happens on `diff --git`,
|
|
408
|
+
the per-file boundary git always emits.
|
|
409
|
+
- **`TestFileDiscovery` matched the changed file's bare stem repo-wide**, so one
|
|
410
|
+
changed `mod.rs` pulled in every other `mod.rs` in the tree. Bare-stem pairing
|
|
411
|
+
is now scoped to the changed file's own directory, which is what makes such a
|
|
412
|
+
pair meaningful; test prefixes and suffixes still match anywhere (#65).
|
|
413
|
+
- **Externally imported types became unanswerable information needs.** The
|
|
414
|
+
external-symbol filter gated only the call-reference loop, so
|
|
415
|
+
`from typing import Optional` plus `: Optional[int]` demanded a definition of
|
|
416
|
+
`optional` from the repository — permanently unsatisfied, which inflates the
|
|
417
|
+
diversity bonus for every candidate and lends match strength to any file
|
|
418
|
+
mentioning the same stdlib type (#65).
|
|
419
|
+
- **The nontrivial-context rescue could spend its whole allowance on one file.**
|
|
420
|
+
Its "file not yet represented" filter was a snapshot taken before the loop.
|
|
421
|
+
The metric it serves is file-level, so a second fragment of an already-reached
|
|
422
|
+
file buys nothing and costs the next file its chance.
|
|
423
|
+
- **Core seed selection depended on fragment order.** The tie-breaks compared
|
|
424
|
+
kind class and span length only, so equally-ranked candidates were resolved by
|
|
425
|
+
parser emission order — reversing it moved which code the output marks as
|
|
426
|
+
changed. All three selectors now end on the fragment id.
|
|
427
|
+
- **Two panics reachable from repository content**: generated-fragment
|
|
428
|
+
truncation checked the cap against the id's line span but sliced the actual
|
|
429
|
+
text, and signature generation produced an id whose end preceded its start
|
|
430
|
+
when a fragment's span was wider than its content. Both are now clamped.
|
|
431
|
+
- MCP `get_file_context` reported the raw glob match while checking containment
|
|
432
|
+
on the resolved path, so a file accepted through a symlink raised an
|
|
433
|
+
uncaught `ValueError` instead of returning its contents (#147).
|
|
434
|
+
- `build_locate` rejected an empty `diff_range` that `build_diff_context` treats
|
|
435
|
+
as the working tree, so two entry points into one pipeline disagreed.
|
|
436
|
+
- Sibling grouping deduped with `Vec::contains` against the bucket, making it
|
|
437
|
+
quadratic in exactly the shape it exists for — thousands of files in one
|
|
438
|
+
directory (#116).
|
|
439
|
+
|
|
440
|
+
### Removed
|
|
441
|
+
|
|
442
|
+
- The dead `low_relevance_threshold` gate.
|
|
443
|
+
`PipelineConfig.low_relevance_filter` was `false` for every scoring mode, so
|
|
444
|
+
`filter_low_relevance` — and with it `FILTERING.low_relevance_threshold`
|
|
445
|
+
(0.015) and its `size_penalty_*` scaling — could never execute;
|
|
446
|
+
`filter_positive_relevance` was always the gate that ran. ContextBench result
|
|
447
|
+
rows nevertheless stamped `low_relevance_threshold: 0.015` into their `config`
|
|
448
|
+
block, so every recorded run attributed its selections to a threshold that
|
|
449
|
+
never fired. Removed on both sides. Bit-equivalent: identical output across
|
|
450
|
+
four scoring modes × three diff ranges, plus the full 2725-case corpus.
|
|
451
|
+
- Phantom knobs that only ever fed a discarded value:
|
|
452
|
+
`GRAPH_FILTERING.git_rename_similarity_threshold` (its return value was
|
|
453
|
+
dropped at the single call site) and `GitConfig`'s `poll_interval_ms` /
|
|
454
|
+
`default_timeout_seconds` fields, which nothing read. Also a
|
|
455
|
+
`_check_allowed(path.resolve())` in the MCP path validator that ran under a
|
|
456
|
+
comment claiming it closed a symlink-swap race, while comparing the same
|
|
457
|
+
already-resolved value against the same allowlist.
|
|
458
|
+
|
|
459
|
+
## [1.12.3] - 2026-07-27
|
|
460
|
+
|
|
461
|
+
### Added
|
|
462
|
+
|
|
463
|
+
- **Scoop install on Windows.** This repository is now itself the bucket:
|
|
464
|
+
`scoop bucket add diffctx https://github.com/nikolay-e/diffctx` then
|
|
465
|
+
`scoop install diffctx/diffctx`. The manifest moved from `packaging/scoop/`
|
|
466
|
+
to `bucket/diffctx.json`, where `scoop bucket add` reads it, so CD pushing
|
|
467
|
+
the regenerated manifest to `main` is the publication step. Previously the
|
|
468
|
+
manifest was correct but unreachable — no bucket existed, so
|
|
469
|
+
`scoop search diffctx` found nothing.
|
|
470
|
+
|
|
471
|
+
- `--with-raw-diff` bundles git's raw unified diff ahead of the selected
|
|
472
|
+
context in every Python-CLI format (md/yaml/json/txt) and in the Python API
|
|
473
|
+
(`build_diff_context(..., with_raw_diff=True)`). Additive only — selection is
|
|
474
|
+
byte-identical with and without it — and not charged to `--budget`; the
|
|
475
|
+
stderr token summary reports the real output size and breaks out the patch's
|
|
476
|
+
share. Lock-file (#112), ignored and secret-like sections stay omitted. Not
|
|
477
|
+
in the native binary or the MCP server yet (#150).
|
|
478
|
+
- Token counting is documented explicitly: every count and `--budget` use
|
|
479
|
+
tiktoken `o200k_base` (GPT-4o family) and are approximate for other model
|
|
480
|
+
families — `docs/product/token-budget.md`, plus a `Token counting` section in
|
|
481
|
+
`--help` (#150).
|
|
482
|
+
- A GitHub Action (`action.yml`) runs diffctx as a CI step and exposes the
|
|
483
|
+
context file, its exact token count and an `empty` flag as step outputs, so a
|
|
484
|
+
downstream job can feed an LLM without re-tokenizing
|
|
485
|
+
(`docs/product/github-action.md`, #145).
|
|
486
|
+
- The Claude Code plugin declares its MCP server self-bootstrapping:
|
|
487
|
+
`uvx --from 'diffctx[mcp]' diffctx-mcp`. Installing the plugin no longer
|
|
488
|
+
assumes `pip install 'diffctx[mcp]'` has already happened — `uv` fetches it
|
|
489
|
+
on first use. (The entry point rather than the `diffctx mcp` subcommand: the
|
|
490
|
+
subcommand only exists from this release on, so on any older version the
|
|
491
|
+
bare command would try to map a directory named `mcp`.)
|
|
492
|
+
- `diffctx mcp` starts the MCP server, alongside the existing `diffctx-mcp`
|
|
493
|
+
console script. MCP registries publish a *package* name, and clients that
|
|
494
|
+
derive the executable from it run `diffctx` — which started a tree-mapping
|
|
495
|
+
run and wrote 31 MB of the working directory into the protocol transport
|
|
496
|
+
instead of speaking MCP. Both spellings now reach the same server.
|
|
497
|
+
|
|
498
|
+
### Security
|
|
499
|
+
|
|
500
|
+
- **A diff range could re-enable repository-configured diff commands.** The
|
|
501
|
+
range validator allowed a leading `-`, so `--ext-diff` or `--textconv` passed
|
|
502
|
+
as a "range" landed in argv *after* the `--no-ext-diff --no-textconv` that
|
|
503
|
+
every diff invocation sets — undoing them and letting `.gitattributes` run
|
|
504
|
+
external commands. Neither side of a range may now begin with a dash, and
|
|
505
|
+
single revisions are additionally rejected for whitespace or control
|
|
506
|
+
characters (a newline split one `cat-file --batch` request into two). Refs
|
|
507
|
+
starting with a dash are unaddressable on a git command line, so nothing
|
|
508
|
+
legitimate is lost.
|
|
509
|
+
- MCP tool calls had no wall-clock deadline — the CLI's watchdog covered only
|
|
510
|
+
the CLI, leaving parse, fragmentation and scoring unbounded on the MCP path.
|
|
511
|
+
All three tools now share the CLI's 300 s default.
|
|
512
|
+
- `SECURITY.md` documents prompt injection via repository content honestly: it
|
|
513
|
+
is inherent to moving repository text into a model's context, diffctx does
|
|
514
|
+
not detect or neutralize it, and output must be treated as untrusted.
|
|
515
|
+
|
|
516
|
+
### Fixed
|
|
517
|
+
|
|
518
|
+
- **A changed file could vanish from the diff entirely when `.gitignore`
|
|
519
|
+
excluded its parent directory.** `check-ignore --no-index` — required for
|
|
520
|
+
`.diffctx/ignore` to apply to tracked files — also revives git's rule that a
|
|
521
|
+
file cannot be re-included once a parent directory is excluded. pandoc
|
|
522
|
+
excludes every dotted root entry with `/*.*` and re-includes `!.github/**`;
|
|
523
|
+
git keeps the tracked workflow file, diffctx dropped it, and a one-file
|
|
524
|
+
commit rendered as an empty selection with exit 4. An exclusion inherited
|
|
525
|
+
from an excluded ancestor no longer counts; a pattern matching the path
|
|
526
|
+
itself still excludes it, so `.diffctx/ignore` and per-directory
|
|
527
|
+
`.gitignore` rules are unaffected (#153).
|
|
528
|
+
- A user-level git config (`diff.noprefix`, `diff.external`, `color.ui=always`,
|
|
529
|
+
custom src/dst prefixes) could empty the selection: diffctx now pins the
|
|
530
|
+
diff invocation flags instead of inheriting the caller's configuration.
|
|
531
|
+
- `cd.yml` now dispatches `publish-extras.yml`, so npm and the Docker Hub
|
|
532
|
+
mirror track every release instead of silently serving the previous one.
|
|
533
|
+
|
|
534
|
+
### Removed
|
|
535
|
+
|
|
536
|
+
- **AUR support.** `diffctx-bin` had never been submitted (the AUR RPC reported
|
|
537
|
+
`resultcount: 0`), the publishing job gated on a credential this repository
|
|
538
|
+
does not hold, and every release regenerated a PKGBUILD nobody could install.
|
|
539
|
+
Arch users are served by `pipx install diffctx` and `cargo install diffctx`.
|
|
540
|
+
|
|
541
|
+
## [1.12.2] - 2026-07-26
|
|
542
|
+
|
|
543
|
+
### Fixed
|
|
544
|
+
|
|
545
|
+
- **The container images ran as root.** The published 1.12.1 image had no
|
|
546
|
+
`USER` directive, so every `docker run` executed as uid 0 while the README
|
|
547
|
+
promised an unprivileged user. The image now runs as uid 10001.
|
|
548
|
+
- **The MCP server advertised the SDK's version as its own.** `FastMCP` takes
|
|
549
|
+
no version argument, so `initialize` reported the installed `mcp` package
|
|
550
|
+
version (e.g. `1.28.1`) as the diffctx server version — a number that drifts
|
|
551
|
+
with every SDK bump and never matched the shipped package. Clients now see
|
|
552
|
+
the real version.
|
|
553
|
+
- **The native binary hard-capped every run at 4096 tokens.** `--budget`
|
|
554
|
+
carried a fixed clap default instead of leaving the budget unset, so the
|
|
555
|
+
auto-sizing the Python CLI has always used never ran on the binary shipped
|
|
556
|
+
via crates.io, npm, the container images and the release archives — the
|
|
557
|
+
same command returned a truncated selection there (14-file
|
|
558
|
+
self-eat range: 15 fragments across 10 files, against 172 across 44). The
|
|
559
|
+
binary now defaults to auto sizing, and `--budget -1` means unlimited as it
|
|
560
|
+
does in Python.
|
|
561
|
+
- The token cache grew without bound (a machine that had analyzed a few dozen
|
|
562
|
+
repositories reached 817k files / 4.0 GB). It is now capped at 512 MB,
|
|
563
|
+
overridable with `DIFFCTX_TOKEN_CACHE_MAX_BYTES` (`0` disables eviction);
|
|
564
|
+
each run trims one of the 256 shards back under its share of the cap,
|
|
565
|
+
oldest entries first (#122).
|
|
566
|
+
- The native binary exited `0` on a diff that produced no semantic context
|
|
567
|
+
(clean tree, binary-only, everything over the size cap), so callers on the
|
|
568
|
+
binary channels could not tell an empty selection from a successful one. It
|
|
569
|
+
now mirrors the Python CLI: the `no semantic context` warning plus its hint
|
|
570
|
+
on stderr and exit code `4`.
|
|
571
|
+
- The native binary printed no token summary, although the README documents
|
|
572
|
+
one for every run. It now writes `N tokens (o200k_base), SIZE` to stderr,
|
|
573
|
+
silenced by the new `-q/--quiet`.
|
|
574
|
+
- `diffctx -v` was a usage error on the native binary while it printed the
|
|
575
|
+
version on the Python CLI. Both short forms (`-v`, `-V`) now work.
|
|
576
|
+
- **A diff could lose its change signal entirely.** When the only fragment
|
|
577
|
+
covering a hunk was a whole-file chunk — flat data files, languages without a
|
|
578
|
+
tree-sitter parser, parse degradation — that chunk had no cheap variant to
|
|
579
|
+
fall back on, so an oversized core was skipped and the output carried *zero*
|
|
580
|
+
`role: "changed"` fragments while rendering the unchanged rest of the file as
|
|
581
|
+
context. Such cores now fall back to an excerpt: the changed lines plus three
|
|
582
|
+
lines of context, cut from the chunk itself and rendered as `kind: excerpt`,
|
|
583
|
+
`role: "changed"`. On the elasticsearch `muted-tests.yml` repro the output
|
|
584
|
+
goes from 121 fragments and no change signal to 118 fragments carrying the
|
|
585
|
+
appended entry, 5,337 tokens instead of 5,970 (#103). The whole-file
|
|
586
|
+
fragmentation of flat lists that the same repro shows is the separate
|
|
587
|
+
granularity issue (#105) and is unchanged.
|
|
588
|
+
- A diff whose selection came back empty rendered as a bare `name`/`type` stub
|
|
589
|
+
in every format: the writer gated *all* diff metadata on there being
|
|
590
|
+
fragments, so the commit message and the list of changed files — the only
|
|
591
|
+
actionable facts about such a run — were dropped. They are now always
|
|
592
|
+
written.
|
|
593
|
+
|
|
594
|
+
### Changed
|
|
595
|
+
|
|
596
|
+
- **Lock files no longer render their hunks in diff mode.** A dependency bump
|
|
597
|
+
is signal, the checksum churn carrying it is not: `diffctx . --diff` used to
|
|
598
|
+
spend 12 KB of a 48 KB output on a `Cargo.lock` chunk. The touched lock files
|
|
599
|
+
are now listed under `lockfile_changes:` (paths only, like `deleted_files:`),
|
|
600
|
+
mirroring the tree-mode ignore policy while keeping the fact that they
|
|
601
|
+
changed — on the reported range the output drops from 12,947 to 7,888 tokens
|
|
602
|
+
(#112). Recognized: `Cargo.lock`, `package-lock.json`, `npm-shrinkwrap.json`,
|
|
603
|
+
`yarn.lock`, `pnpm-lock.yaml`, `bun.lock(b)`, `deno.lock`, `Pipfile.lock`,
|
|
604
|
+
`poetry.lock`, `uv.lock`, `pdm.lock`, `composer.lock`, `Gemfile.lock`,
|
|
605
|
+
`flake.lock`, `go.sum`, `mix.lock`, `packages.lock.json`, `gradle.lockfile`,
|
|
606
|
+
`Package.resolved`, `cabal.project.freeze`. `--full` still renders their
|
|
607
|
+
content — it is the escape hatch that promises every fragment of the changed
|
|
608
|
+
files.
|
|
609
|
+
- Native binary CLI parity: `-f` is accepted as the short form of `--format`,
|
|
610
|
+
bare `--diff` means the working tree against `HEAD`, `--scoring` advertises
|
|
611
|
+
its accepted values, and every option carries help text.
|
|
612
|
+
- The standalone binary is covered by its own integration suite
|
|
613
|
+
(`crates/diffctx-native/tests/native_cli.rs`, run in CI): exit codes, both
|
|
614
|
+
version flags,
|
|
615
|
+
the token summary, `--quiet`, format validation and budget handling are now
|
|
616
|
+
contract-tested against real git repositories. Every parity defect above
|
|
617
|
+
shipped because nothing exercised the clap parser.
|
|
618
|
+
|
|
619
|
+
## [1.12.1] - 2026-07-23
|
|
620
|
+
|
|
621
|
+
### Added
|
|
622
|
+
|
|
623
|
+
- Standalone native binaries (linux x86_64/aarch64, macOS arm64, Windows
|
|
624
|
+
x64) are built and attached to every GitHub release. The README had
|
|
625
|
+
promised them since 1.9.x; releases only ever carried wheels and an sdist.
|
|
626
|
+
- The Rust engine is published to crates.io as the `diffctx` crate
|
|
627
|
+
(`cargo install diffctx` for the native CLI, `cargo add diffctx` to embed the
|
|
628
|
+
selection pipeline). Previously the name held only a reservation stub; the
|
|
629
|
+
crate now carries the released engine, starting at 1.12.0.
|
|
630
|
+
- Container image `ghcr.io/nikolay-e/diffctx` (linux amd64/arm64), built from
|
|
631
|
+
the release tag and smoke-tested against a real repository before the tag
|
|
632
|
+
moves: `docker run --rm -v "$PWD:/repo" ghcr.io/nikolay-e/diffctx . --diff HEAD~1`.
|
|
633
|
+
Mirrored to Docker Hub as `nikolajer/diffctx`.
|
|
634
|
+
- npm wrapper (`packaging/npm/`) that downloads the platform binary and
|
|
635
|
+
verifies its SHA-256 against the published checksum. Scoop
|
|
636
|
+
(`packaging/scoop/diffctx.json`) and AUR (`packaging/aur/`) manifests are
|
|
637
|
+
generated from the same checksums but are not published to any bucket or
|
|
638
|
+
to the AUR.
|
|
639
|
+
|
|
640
|
+
### Fixed
|
|
641
|
+
|
|
642
|
+
- The native binary silently emitted YAML for every unrecognized `--format`,
|
|
643
|
+
including `md` — the documented default of the Python CLI. It now accepts
|
|
644
|
+
only `yaml`/`json` and exits 2 on anything else.
|
|
645
|
+
|
|
646
|
+
## [1.12.0] - 2026-07-23
|
|
647
|
+
|
|
648
|
+
### Added
|
|
649
|
+
|
|
650
|
+
- `--timeout SECONDS` — wall-clock deadline for `--diff` analysis (default
|
|
651
|
+
300); exceeding it exits `124` instead of hanging indefinitely (#70).
|
|
652
|
+
- `--no-ignores` — turns off every ignore rule (built-in patterns, project
|
|
653
|
+
`.gitignore`, `.diffctx/ignore`). `--no-default-ignores` only disables the
|
|
654
|
+
built-in list; its help now says so. Not supported with `--diff`.
|
|
655
|
+
- Output format is inferred from the `-o` extension when `-f` is omitted, so
|
|
656
|
+
`-o out.json` no longer writes Markdown into a `.json` file; a mismatch
|
|
657
|
+
between `-f` and the extension warns.
|
|
658
|
+
|
|
659
|
+
### Fixed
|
|
660
|
+
|
|
661
|
+
- **All error logging was dead.** An import-time `NullHandler` made
|
|
662
|
+
`setup_logging` skip attaching a real handler, so `--log-level` was a no-op
|
|
663
|
+
and all 19 `logger.error/warning/exception` sites were silent — `diffctx . -o
|
|
664
|
+
/bad/path.md` exited 1 with no message at all.
|
|
665
|
+
- **diffctx invoked from inside a git hook silently analyzed the wrong
|
|
666
|
+
repository.** Git exports repo-locating env vars (`GIT_DIR`,
|
|
667
|
+
`GIT_INDEX_FILE`, `GIT_WORK_TREE`, ...) to hook subprocesses; inherited,
|
|
668
|
+
they overrode `-C` on every internal git call. All git spawns now scrub
|
|
669
|
+
these variables (`git_command()` in `git.rs`).
|
|
670
|
+
- YAML output preserved file content byte-exactly except for trailing
|
|
671
|
+
newlines; the block chomping indicator is now chosen per content.
|
|
672
|
+
- Arrow-function fragments bound to variables were never stub-eligible (#106).
|
|
673
|
+
- Decorated definitions rendered as a bare `@decorator` line without the
|
|
674
|
+
`class X:` / `def x():` header.
|
|
675
|
+
- `--max-depth`-pruned directories were labelled `_(empty directory)_` — a
|
|
676
|
+
factual lie to the reader; they now read
|
|
677
|
+
`_(children omitted: --max-depth reached)_` (`truncated: true` in
|
|
678
|
+
YAML/JSON).
|
|
679
|
+
- Mixed directory + glob arguments dropped the glob files' parent path from
|
|
680
|
+
node names.
|
|
681
|
+
- Double Ctrl-C printed a ~60-line traceback.
|
|
682
|
+
- Lock files `uv.lock`, `pdm.lock`, `bun.lock`, `bun.lockb`, `deno.lock` and
|
|
683
|
+
`flake.lock` leaked into output; they now join the other lock files in the
|
|
684
|
+
default ignore patterns.
|
|
685
|
+
- Large-repo hangs/OOM on trivial diffs (#70, #95): discovery no longer
|
|
686
|
+
re-reads and re-tokenizes the whole candidate universe per ensemble
|
|
687
|
+
strategy (one shared pass + a persistent per-blob token cache keyed by
|
|
688
|
+
`(blob OID, tokenizer epoch)`), and edge construction is two-pass with a
|
|
689
|
+
bounded per-source top-K instead of materializing up to tens of millions
|
|
690
|
+
of raw edges before the cap; pass 2 replays a compact 16-byte-per-emission
|
|
691
|
+
log instead of re-running the builders, so generation cost stays 1x.
|
|
692
|
+
Verified: gitpod 8000s-hang -> 35.7s,
|
|
693
|
+
pytorch 1848s-SIGKILL -> 7.4s, mui/material-ui OOM class recovered.
|
|
694
|
+
Outputs are bit-identical (gated by `eval/analysis/equivalence_gate.py`).
|
|
695
|
+
|
|
696
|
+
### Known limitations
|
|
697
|
+
|
|
698
|
+
- Near-dense edge emission on huge same-directory trees (observed: 199M
|
|
699
|
+
raw edges, 37GB peak on one mui/material-ui instance) remains expensive
|
|
700
|
+
even with bounded construction; tracked in #116.
|
|
701
|
+
|
|
702
|
+
### Changed
|
|
703
|
+
|
|
704
|
+
- **The token budget is now a hard cap.** The changed-files post-pass no
|
|
705
|
+
longer exceeds the budget to guarantee representation: a changed file
|
|
706
|
+
whose cheapest representative does not fit stays unrepresented (visible
|
|
707
|
+
as changed-file retention < 1). `--budget 0` therefore yields an empty
|
|
708
|
+
selection (use `--full` for changed files only); CLI help updated.
|
|
709
|
+
- Latency telemetry: new `graph_build_ms` phase (graph construction was
|
|
710
|
+
previously misattributed to `scoring_ms`, which now measures pure
|
|
711
|
+
ranking) and `peak_rss_bytes` (in-process peak memory). Release builds
|
|
712
|
+
carry line tables (`debug = "line-tables-only"`) for profiling at no
|
|
713
|
+
runtime cost.
|
|
714
|
+
- CLI diagnostics are honest end to end: an exit-code table in `--help`
|
|
715
|
+
(2 usage, 3 environment, 4 empty diff, 124 timeout), flag-value validation
|
|
716
|
+
exits 2 instead of 1, git failures report a single line plus a
|
|
717
|
+
`git log --oneline` hint on unknown revisions, conflicting flags warn
|
|
718
|
+
(`-q`+`--log-level`, `--full`+selection flags, ...), a failed clipboard
|
|
719
|
+
copy warns before falling back to stdout, and `--tau` / `--scoring` /
|
|
720
|
+
`--alpha` / `--budget 0` help text describes what actually happens.
|
|
721
|
+
- `graph --summary` reports category shares as percentages, suppresses
|
|
722
|
+
degenerate top-referenced lists, detects cycles over dominant-direction
|
|
723
|
+
edges only, derives churn from `git log --since`, and disambiguates
|
|
724
|
+
duplicate mermaid labels to relative paths.
|
|
725
|
+
|
|
726
|
+
## [1.11.0] - 2026-07-07
|
|
727
|
+
|
|
728
|
+
### Changed
|
|
729
|
+
|
|
730
|
+
- **The default `--format` is now `md` (Markdown), previously `yaml`.** Markdown
|
|
731
|
+
is ~7% more token-efficient than YAML on real diffs and is preferred by
|
|
732
|
+
reviewers for code-fragment-heavy output (evidence:
|
|
733
|
+
`datasets/real-world-diff/v1/`). YAML remains available via `-f yaml`.
|
|
734
|
+
This also changes the default stdout format of `treemapper` (a passthrough
|
|
735
|
+
wrapper over this engine); downstream scripts that parse the default output
|
|
736
|
+
must now pass `-f yaml` explicitly. (#104)
|
|
737
|
+
|
|
738
|
+
## [1.10.0] - 2026-06-20
|
|
739
|
+
|
|
740
|
+
### Changed
|
|
741
|
+
|
|
742
|
+
- The default `--tau` (stopping threshold) is now **0.12** across the CLI, MCP,
|
|
743
|
+
and Python API — the calibrated grid optimum — bringing the Python side in
|
|
744
|
+
line with the standalone Rust binary (was 0.08). Diff-context selections are
|
|
745
|
+
slightly tighter by default.
|
|
746
|
+
- The MCP `get_tree_map` / `get_file_context` default `max_file_bytes` is now
|
|
747
|
+
256 KB, matching the documented CLI default (was 100 KB).
|
|
748
|
+
|
|
749
|
+
### Fixed
|
|
750
|
+
|
|
751
|
+
- Document/citation edges can no longer create self-edges (a fragment linking to
|
|
752
|
+
itself); `Graph::add_edge` now rejects `src == dst`.
|
|
753
|
+
- Test→source import edges no longer match `import` statements that appear inside
|
|
754
|
+
comments or string literals (the import regex is now line-anchored).
|
|
755
|
+
- The MCP `get_file_context` clipboard confirmation now reports the number of
|
|
756
|
+
files actually copied, excluding files skipped for exceeding the size cap.
|
|
757
|
+
|
|
758
|
+
### Docs
|
|
759
|
+
|
|
760
|
+
- Corrected the AST-parsing language count (was "12 languages", now "30+"),
|
|
761
|
+
aligned the Rust crate version with the wheel, completed the MCP server README
|
|
762
|
+
(all three tools documented), and fixed the documented `EGO.per_hop_decay`
|
|
763
|
+
default (0.5). Documented the `scoring_mode` / `timeout` Python API params,
|
|
764
|
+
clarified `--budget` / `--tau` help text, and marked the `DIFFCTX_OP_*` /
|
|
765
|
+
`DIFFCTX_*` tuning env vars as an experimental, non-public interface.
|
|
766
|
+
|
|
767
|
+
## [1.9.2] - 2026-06-14
|
|
768
|
+
|
|
769
|
+
### Added
|
|
770
|
+
|
|
771
|
+
- Private-key and keystore files are now excluded from output in **both** tree
|
|
772
|
+
mapping and `--diff` context, since such material is never legitimate LLM
|
|
773
|
+
context: `*.pem`, `*.key`, `*.pfx`, `*.p12`, `*.keystore`, `*.jks`, and SSH
|
|
774
|
+
private keys `id_rsa`/`id_dsa`/`id_ecdsa`/`id_ed25519` (public `.pub` keys stay
|
|
775
|
+
visible). The `--diff` path previously applied no ignore filtering at all, so a
|
|
776
|
+
changed key file would have leaked into context. Use `--no-default-ignores` to
|
|
777
|
+
opt out of tree-mode default ignores. (`.env` files are intentionally still
|
|
778
|
+
included — a changed `.env` is legitimate change context; redacting secret
|
|
779
|
+
*values* is a separate planned content-scan feature.)
|
|
780
|
+
|
|
781
|
+
### Fixed
|
|
782
|
+
|
|
783
|
+
- **Diffs touching files larger than 100 KB no longer produce empty output.** A
|
|
784
|
+
changed file (e.g. a 142 KB `Math.h`) was subject to the same 100 KB cap as
|
|
785
|
+
context-discovery candidates, so the whole diff yielded "no semantic context".
|
|
786
|
+
Changed files — the subject of the diff — are now parsed up to 5 MB.
|
|
787
|
+
- **C/C++ symbol names** for variable declarations now report the variable, not
|
|
788
|
+
the type: a changed `const unsigned blane = …` is labeled `blane`, not
|
|
789
|
+
`unsigned`. Covers `init`/`array`/pointer/function declarators and C++
|
|
790
|
+
`reference`/`parenthesized` declarators (`int &r`, `int (*f)()`).
|
|
791
|
+
- Changed-file fragments are no longer dropped by the generated-file reduction
|
|
792
|
+
(cap of 5 + 30-line content truncation), which could discard the small
|
|
793
|
+
fragment covering the edited hunk and mislocate the change.
|
|
794
|
+
- Binary detection no longer misclassifies changed text files that embed ANSI
|
|
795
|
+
escape / control bytes (snapshot and terminal-recording fixtures); it now uses
|
|
796
|
+
git's NUL-byte heuristic, so such files are no longer silently dropped.
|
|
797
|
+
- Diff-context selection is now deterministic across processes: ego-graph score
|
|
798
|
+
accumulation, context-fragment capping, and the greedy selection heap use
|
|
799
|
+
stable tie-breaks, so identical inputs always yield byte-identical output.
|
|
800
|
+
- Python module-import edges are bidirectional (matching every other language
|
|
801
|
+
and Python's own symbol references), so a changed imported module now
|
|
802
|
+
propagates relevance back to its importers.
|
|
803
|
+
|
|
804
|
+
### Hardened
|
|
805
|
+
|
|
806
|
+
- `git cat-file` blob reads are bounded (drain-and-reject above 16 MB) instead of
|
|
807
|
+
allocating an arbitrarily large buffer up front.
|
|
808
|
+
|
|
809
|
+
## [1.9.1] - 2026-06-14
|
|
810
|
+
|
|
811
|
+
> Supersedes 1.9.0, which was tagged but never published to PyPI (release-process
|
|
812
|
+
> hiccup); 1.9.1 ships the same changes. There is no 1.9.0 on PyPI.
|
|
813
|
+
|
|
814
|
+
### Added
|
|
815
|
+
|
|
816
|
+
- Diff-context output now leads with an orientation header — `commit_message`
|
|
817
|
+
and `changed_files` — in every format (YAML/JSON/Markdown/text), so a reader
|
|
818
|
+
sees *what* changed before reading any fragment.
|
|
819
|
+
- Each fragment carries a `role` of `changed` when it overlaps the diff hunks
|
|
820
|
+
(omitted for supporting context). Changed code is emitted first; context
|
|
821
|
+
follows, ordered by descending per-file relevance instead of alphabetically.
|
|
822
|
+
|
|
823
|
+
### Changed
|
|
824
|
+
|
|
825
|
+
- Line-contiguous fragments of the same role within a file are merged into a
|
|
826
|
+
single entry, cutting the per-fragment scaffolding that dominated output made
|
|
827
|
+
up of one-line snippets (lossless on line coverage).
|
|
828
|
+
|
|
829
|
+
### Fixed
|
|
830
|
+
|
|
831
|
+
- `get_changed_files` and `get_untracked_files` canonicalize paths consistently
|
|
832
|
+
with deleted/discovered files, preventing duplicate fragments when a tracked
|
|
833
|
+
path traverses a symlinked directory.
|
|
834
|
+
- Signature extraction no longer terminates at braces inside parameter defaults
|
|
835
|
+
or annotations (e.g. Python `def f(x={}):`), which previously truncated the
|
|
836
|
+
signature mid-parameter-list.
|
|
837
|
+
- The post-pass that rescues unrepresented changed files reuses the open
|
|
838
|
+
`git cat-file --batch` reader instead of spawning a `git show` per file.
|
|
839
|
+
- Degraded token-count fallback returns 0 for the empty string (was 1).
|
|
840
|
+
|
|
841
|
+
## [1.8.0]
|
|
842
|
+
|
|
843
|
+
### Added
|
|
844
|
+
|
|
845
|
+
- Public `diffctx.run(argv=None, *, prog=None, version=None)` engine entry. It
|
|
846
|
+
is the same execution path as the `diffctx` console script but accepts an
|
|
847
|
+
injected program name and version string, so a downstream wrapper (e.g. the
|
|
848
|
+
`treemapper` distribution) can present its own branding in `--help`,
|
|
849
|
+
`--version`, and error prefixes without duplicating the CLI. `main()` now
|
|
850
|
+
delegates to `run()` with the default `diffctx` identity — no behavior change
|
|
851
|
+
for existing callers.
|
|
852
|
+
- `diffctx.mcp.__main__.main(prog=..., extra=...)` accepts an injected program
|
|
853
|
+
name and install-extra string for the missing-dependency hint, so wrappers
|
|
854
|
+
can re-expose the MCP entry point with their own name.
|
|
855
|
+
|
|
856
|
+
## [1.7.0] - 2026-05-22
|
|
857
|
+
|
|
858
|
+
### Added
|
|
859
|
+
|
|
860
|
+
- README documents the `graph` subcommand and the `--scoring {ppr,ego,bm25}`
|
|
861
|
+
flag (default: `ego`); the absolutist "Uses Personalized PageRank" language
|
|
862
|
+
has been replaced with a table covering all three scoring modes.
|
|
863
|
+
- `SECURITY.md` now ships a "Threat model" section that scopes diffctx as a
|
|
864
|
+
local CLI (filesystem + git subprocess, no network) and documents that the
|
|
865
|
+
optional `diffctx-mcp` server confines its filesystem reach via
|
|
866
|
+
`DIFFCTX_ALLOWED_PATHS`.
|
|
867
|
+
- Footer of `README.md` links `CHANGELOG.md`, `SECURITY.md`, and
|
|
868
|
+
`docs/engineering/parameter-strategy.md` so they are no longer orphaned.
|
|
869
|
+
|
|
870
|
+
### Changed
|
|
871
|
+
|
|
872
|
+
- **Package renamed from `treemapper` to `diffctx`.** PyPI distribution, CLI
|
|
873
|
+
binary, MCP server binary, and Python import path all use `diffctx`. The
|
|
874
|
+
`treemapper` PyPI package remains at 1.6.1 (frozen); install
|
|
875
|
+
`diffctx` for all new development. The `TREEMAPPER_ALLOWED_PATHS`
|
|
876
|
+
environment variable is now `DIFFCTX_ALLOWED_PATHS`.
|
|
877
|
+
- Single source of truth for the package version: the Python wheel reads its
|
|
878
|
+
version from `Cargo.toml` (via maturin) instead of duplicating it in
|
|
879
|
+
`pyproject.toml`, eliminating the Rust-crate-vs-Python-package version
|
|
880
|
+
desync that previously shipped to PyPI.
|
|
881
|
+
- `--diff` now defaults to `HEAD` when no range is supplied, matching the
|
|
882
|
+
most common invocation (`diffctx . --diff` → diff of the working tree
|
|
883
|
+
against `HEAD`) and saving an argument in the 30-second demo path.
|
|
884
|
+
- `numpy` moved from a required runtime dependency into the `[tree-sitter]`
|
|
885
|
+
extra, so default installs no longer pull a ~20 MB scientific stack the
|
|
886
|
+
core tree-mapping mode does not use.
|
|
887
|
+
- CLI error messages are now actionable: instead of a raw Python traceback,
|
|
888
|
+
invalid `--diff` ranges, missing git repositories, and unreadable paths
|
|
889
|
+
print a one-line `Error: <what> — try: <next step>` and exit with code `2`
|
|
890
|
+
for user-input errors (`1` is reserved for runtime failures).
|
|
891
|
+
- `automerge.yml` GitHub Actions workflow hardened: explicit minimal
|
|
892
|
+
`permissions:` block, pinned action SHAs, and a guard that refuses to
|
|
893
|
+
auto-merge anything touching `.github/`, `pyproject.toml`, or `Cargo.toml`.
|
|
894
|
+
|
|
895
|
+
### Fixed
|
|
896
|
+
|
|
897
|
+
- Replaced every user-reachable `unwrap()`/`expect()` in the Rust core
|
|
898
|
+
(`tokenizer.rs`, `git.rs`, `scoring.rs`, `pybridge.rs`) with proper
|
|
899
|
+
`PyRuntimeError` / `GitError` propagation. A malformed diff, a missing
|
|
900
|
+
BPE table, or an oversized hunk-header integer no longer aborts the
|
|
901
|
+
Python interpreter via `panic = "abort"`.
|
|
902
|
+
- `diffctx-mcp` entry point now guards against being launched without the
|
|
903
|
+
`[mcp]` extra installed and prints an install hint instead of an
|
|
904
|
+
`ImportError` traceback.
|
|
905
|
+
|
|
906
|
+
### Removed
|
|
907
|
+
|
|
908
|
+
- Dropped Kotlin and F# from the language matrix: tree-sitter grammars for
|
|
909
|
+
both were silently misaligned with the project's import-resolution rules
|
|
910
|
+
and produced misleading edge weights. They will return once the grammars
|
|
911
|
+
are vetted.
|
|
912
|
+
|
|
913
|
+
### Security
|
|
914
|
+
|
|
915
|
+
- MCP server (`diffctx-mcp`) now refuses to traverse outside the
|
|
916
|
+
directories listed in `DIFFCTX_ALLOWED_PATHS` (OS-pathsep-separated)
|
|
917
|
+
and refuses to start if the envvar is unset when run as a network-facing
|
|
918
|
+
process. See [`SECURITY.md`](SECURITY.md) for the threat model.
|
|
919
|
+
|
|
920
|
+
## [1.6.1 and earlier]
|
|
921
|
+
|
|
922
|
+
Earlier releases shipped as `treemapper`; see
|
|
923
|
+
<https://pypi.org/project/treemapper/#history> for legacy versions and
|
|
924
|
+
<https://github.com/nikolay-e/diffctx/releases> for the corresponding GitHub
|
|
925
|
+
release notes (`1.0.0` through `1.6.1`).
|
|
926
|
+
|
|
927
|
+
[Unreleased]: https://github.com/nikolay-e/diffctx/compare/v1.12.3...HEAD
|
|
928
|
+
[1.12.3]: https://github.com/nikolay-e/diffctx/compare/v1.12.2...v1.12.3
|
|
929
|
+
[1.12.2]: https://github.com/nikolay-e/diffctx/compare/v1.12.1...v1.12.2
|
|
930
|
+
[1.7.0]: https://github.com/nikolay-e/diffctx/compare/v1.6.1...v1.7.0
|