diffctx 1.16.0__tar.gz → 1.18.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.16.0 → diffctx-1.18.0}/CHANGELOG.md +124 -1
- {diffctx-1.16.0 → diffctx-1.18.0}/Cargo.lock +14 -7
- {diffctx-1.16.0 → diffctx-1.18.0}/PKG-INFO +23 -12
- {diffctx-1.16.0 → diffctx-1.18.0}/README.md +19 -8
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/Cargo.toml +3 -2
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/README.md +19 -8
- diffctx-1.18.0/crates/diffctx-native/src/candidate_files.rs +201 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/weights.rs +58 -18
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/discovery.rs +126 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/base.rs +9 -1
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/mod.rs +45 -2
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/bazel.rs +11 -4
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/cargo_edges.rs +3 -3
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/go.rs +1 -1
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/javascript.rs +9 -1
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/perl.rs +1 -1
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/shell.rs +17 -3
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/terraform.rs +4 -4
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/structural/testing.rs +1 -1
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/effective_config.rs +5 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/fragmentation.rs +217 -97
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/git.rs +311 -73
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/graph.rs +33 -0
- diffctx-1.18.0/crates/diffctx-native/src/hook.rs +644 -0
- diffctx-1.18.0/crates/diffctx-native/src/impact.rs +707 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/in_memory_harness.rs +40 -4
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/languages.rs +116 -1
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/lib.rs +3 -1
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/locate.rs +63 -11
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/main.rs +236 -31
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/parsers/config_parser.rs +6 -15
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/parsers/tree_sitter_strategy.rs +75 -14
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/paths.rs +25 -3
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/pipeline.rs +353 -96
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/ppr.rs +4 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/project_graph.rs +7 -3
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/pybridge.rs +80 -10
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/render.rs +183 -3
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/resource.rs +45 -5
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/sanitize.rs +2 -2
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/select.rs +39 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/token_corpus.rs +5 -2
- {diffctx-1.16.0 → diffctx-1.18.0}/pyproject.toml +3 -3
- {diffctx-1.16.0 → diffctx-1.18.0}/src/diffctx/_app.py +94 -15
- {diffctx-1.16.0 → diffctx-1.18.0}/src/diffctx/_diffctx.pyi +14 -1
- diffctx-1.18.0/src/diffctx/_native/__init__.py +7 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/src/diffctx/_native/graph_analytics.py +1 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/src/diffctx/_native/pipeline.py +55 -33
- {diffctx-1.16.0 → diffctx-1.18.0}/src/diffctx/cli.py +101 -38
- {diffctx-1.16.0 → diffctx-1.18.0}/src/diffctx/ignore.py +15 -6
- {diffctx-1.16.0 → diffctx-1.18.0}/src/diffctx/mcp/fetch.py +51 -39
- {diffctx-1.16.0 → diffctx-1.18.0}/src/diffctx/mcp/legacy.py +22 -21
- {diffctx-1.16.0 → diffctx-1.18.0}/src/diffctx/mcp/security.py +8 -3
- {diffctx-1.16.0 → diffctx-1.18.0}/src/diffctx/mcp/server.py +128 -34
- {diffctx-1.16.0 → diffctx-1.18.0}/src/diffctx/tree.py +41 -14
- diffctx-1.18.0/src/diffctx/version.py +1 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/src/diffctx/writer.py +107 -29
- diffctx-1.16.0/crates/diffctx-native/src/candidate_files.rs +0 -134
- diffctx-1.16.0/src/diffctx/_native/__init__.py +0 -7
- diffctx-1.16.0/src/diffctx/version.py +0 -1
- {diffctx-1.16.0 → diffctx-1.18.0}/Cargo.toml +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/LICENSE +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/analytics.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/change_class.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/analytics.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/bm25.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/budget.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/category_weights.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/edge_weights.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/env_overrides.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/extensions.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/filtering.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/fragmentation.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/git.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/graph_filtering.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/importance.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/limits.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/mod.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/mode.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/needs.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/parsers.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/render.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/scoring.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/selection.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/tokenization.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/core.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/config_edges/build_system.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/config_edges/cicd.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/config_edges/docker.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/config_edges/generic.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/config_edges/helm.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/config_edges/kubernetes.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/config_edges/mod.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/document/mod.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/history/cochange.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/history/mod.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/ansible.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/c_family.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/clojure.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/css.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/dart.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/dbt.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/dotnet.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/elixir.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/erlang.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/graphql.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/haskell.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/julia.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/jvm.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/latex.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/lua.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/mod.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/nim.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/nix.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/ocaml.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/openapi.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/php.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/prisma.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/protobuf.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/python.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/r_lang.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/ruby.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/rust_lang.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/sql.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/swift.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/tags.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/zig.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/similarity/lexical.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/similarity/mod.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/structural/containment.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/structural/mod.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/structural/sibling.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/excerpt.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/facts/mod.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/facts/scala.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/filtering.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/graph_export.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/interval.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/mode.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/parsers/generic.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/parsers/markdown.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/parsers/mod.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/peak_rss.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/postpass.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/provenance.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/run_provenance.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/scoring.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/signatures.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/stopwords/code.txt +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/stopwords/docs.txt +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/stopwords.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/test_rng.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/testfiles.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/tokenizer.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/types.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/utility/boltzmann.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/utility/importance.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/utility/mod.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/utility/needs.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/crates/diffctx-native/src/utility/objective.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/rust-toolchain.toml +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/src/diffctx/__init__.py +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/src/diffctx/__main__.py +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/src/diffctx/_native/graph_export.py +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/src/diffctx/_native/project_graph.py +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/src/diffctx/clipboard.py +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/src/diffctx/logger.py +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/src/diffctx/mcp/__init__.py +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/src/diffctx/mcp/__main__.py +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/src/diffctx/py.typed +0 -0
- {diffctx-1.16.0 → diffctx-1.18.0}/src/diffctx/tokens.py +0 -0
|
@@ -7,6 +7,127 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.18.0] - 2026-09-29
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`--mode impact`: what a change reaches outside its own diff, under 2k
|
|
15
|
+
tokens.** For each changed symbol, the callers in files the diff does not
|
|
16
|
+
touch, whether a test guards each one, and which symbols more than one
|
|
17
|
+
commit of the range edited; public API and schema changes are listed as
|
|
18
|
+
facts. It walks the graph the other modes already build, with a discovery
|
|
19
|
+
pass that follows references *into* the changed files (the other modes
|
|
20
|
+
only follow what the changed files import), and filters name-only links
|
|
21
|
+
the way a reader would: the caller must name the symbol in code and
|
|
22
|
+
reference its module. An empty impact is an answer, not an error. CLI
|
|
23
|
+
(`-f md` is the text form), MCP `mode="impact"`, the GitHub Action's
|
|
24
|
+
`mode` input, and `schemas/diffctx.impact.v1.json`.
|
|
25
|
+
- **`diffctx hook pretooluse` / `posttooluse`: the Claude Code plugin hands
|
|
26
|
+
the agent a change's impact without asking it to call anything.** Eight
|
|
27
|
+
agent sessions with the tool installed made zero unprompted calls (#289).
|
|
28
|
+
Before a commit, merge, cherry-pick, push or pull request the impact of
|
|
29
|
+
what that command records is injected as context — a plain `git commit`
|
|
30
|
+
reviews the index, `-a` the working tree, a merge the branch since its
|
|
31
|
+
merge base; after a `git diff` or `git status` the agent ran itself, the
|
|
32
|
+
pending change's impact rides on the answer it asked for. Once per change
|
|
33
|
+
(content-keyed with `git patch-id`, so a manual `--mode impact` run
|
|
34
|
+
counts), silent when nothing outside the diff depends on the change, and
|
|
35
|
+
never blocking: a range that runs out of the 30 s deadline is marked
|
|
36
|
+
reviewed rather than retried on every push. `impact_gate` (off by default)
|
|
37
|
+
denies a commit until its impact was shown once. At session start the
|
|
38
|
+
plugin fetches the release binary for the platform, verified against
|
|
39
|
+
`checksums.json`, into its data directory, backs off for six hours after a
|
|
40
|
+
failed download, and puts the one-line recipe into the session's context;
|
|
41
|
+
`impact_hook` turns all of it off (#310).
|
|
42
|
+
- **`/diffctx:commit`**: the impact first, then the commit.
|
|
43
|
+
- **`diffctx_context` is always loaded** under Claude Code's tool search
|
|
44
|
+
(`_meta.anthropic/alwaysLoad`), and its description names `impact` as the
|
|
45
|
+
pre-commit call; both skills default to the server's own range choice.
|
|
46
|
+
- **The PR review comment opens with the impact view.**
|
|
47
|
+
- **Plugin eval `breaking-caller`** measures the number under the goal: with
|
|
48
|
+
a contract change uncommitted and two callers about to break, does the
|
|
49
|
+
agent handle or name them before committing.
|
|
50
|
+
|
|
51
|
+
## [1.17.0] - 2026-09-23
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
|
|
55
|
+
- **The site lives at <https://diffctx.com>.** It was a 301 to
|
|
56
|
+
`nikolay-e.github.io/diffctx/`, so search engines indexed the github.io
|
|
57
|
+
URL and the product domain had no presence of its own. It is now the
|
|
58
|
+
GitHub Pages custom domain (github.io redirects to it), with a
|
|
59
|
+
`robots.txt` and `sitemap.xml`; package metadata points at it.
|
|
60
|
+
- **Windows and Intel-Mac are release targets.** Every test matrix runs on
|
|
61
|
+
Linux, macOS and Windows on x64 and ARM; wheels and binaries ship for
|
|
62
|
+
`x86_64-apple-darwin` and `aarch64-pc-windows-msvc` as well, and `npm
|
|
63
|
+
install diffctx` and the Scoop bucket select them. What that exposed is
|
|
64
|
+
fixed: `--output-file` on Windows, backslashes in MCP output and in the
|
|
65
|
+
Rust path matching (discovery, C#/Go/Cargo edges), datasets checked out
|
|
66
|
+
byte-exact under a CRLF `autocrlf`, and a `--timeout 0` that now always
|
|
67
|
+
reports the timeout instead of racing the subprocess.
|
|
68
|
+
- **The landing page installs and opens offline** — a web manifest, icons
|
|
69
|
+
and a service worker that keeps every visit network-first.
|
|
70
|
+
- **Free-threaded CPython gets a wheel.** Each platform ships a `cp314t`
|
|
71
|
+
wheel beside the `abi3` one; `uvx` on 3.14t no longer compiles the sdist
|
|
72
|
+
(which timed MCP clients out), and the extension keeps the GIL off.
|
|
73
|
+
- **The GitHub Action works on a default shallow checkout**: with no
|
|
74
|
+
`diff-range` it fetches the pull request's base and head (or `HEAD~1`)
|
|
75
|
+
itself, so `fetch-depth: 0` is no longer required.
|
|
76
|
+
- **`provenance.engine.build` names the release commit** in wheels, binaries
|
|
77
|
+
and the image.
|
|
78
|
+
- **A glossary line under the scoring modes** on the landing page: three
|
|
79
|
+
independent first-visit probes stalled on `ego-network`, `PPR`, `rrf`,
|
|
80
|
+
`pit`, `τ` and `o200k_base` with nothing on the page defining them.
|
|
81
|
+
|
|
82
|
+
### Removed
|
|
83
|
+
|
|
84
|
+
- **`build_diff_context(ignore_file=…, whitelist_file=…, no_default_ignores=…)`.**
|
|
85
|
+
The engine never applied a custom path-spec layer in diff mode and the three
|
|
86
|
+
arguments existed only to raise `NotImplementedError`; passing one is now a
|
|
87
|
+
`TypeError`. `.gitignore` and `.diffctx/ignore` still apply.
|
|
88
|
+
|
|
89
|
+
### Fixed
|
|
90
|
+
|
|
91
|
+
- **The MCP server no longer hangs on Windows.** Every git child inherited the
|
|
92
|
+
server's stdin, which under an MCP client is the JSON-RPC pipe the client
|
|
93
|
+
holds open, and on Windows such a git never exited: any tool call that reached
|
|
94
|
+
git hung. Child processes now get a null stdin in both the engine and the
|
|
95
|
+
Python layer.
|
|
96
|
+
- **A long range says how many commits it held.** The commit list stops at
|
|
97
|
+
the newest twenty; the artifact read "20 commits" over a 42-commit range. It
|
|
98
|
+
now carries `commit_count` and reads "newest 20 of 42 commits" when the list
|
|
99
|
+
is cut, in both CLIs, pack and locate.
|
|
100
|
+
- **`--budget` fitting never passes context off as the change.** The witness
|
|
101
|
+
clip shortened any file's last fragment, so a context file shrank line by line
|
|
102
|
+
under a "more lines of this change" marker before it dropped; only a changed
|
|
103
|
+
fragment is shortened now.
|
|
104
|
+
- **MCP `fragment_ids` on a bare `diff_ref` read the code the change
|
|
105
|
+
replaced.** `HEAD` (or `HEAD~3`, or a duration window) is git's
|
|
106
|
+
base-versus-working-tree, so the locate ranking describes the files on
|
|
107
|
+
disk; the fetch read the base blob instead and returned the old body under
|
|
108
|
+
the ranking's own id. Bodies for a `..`-less ref now come from the working
|
|
109
|
+
tree, and a file the named revision lacks (or a revision git cannot
|
|
110
|
+
resolve) is reported as not found instead of being substituted with the
|
|
111
|
+
working-tree file under a historical label.
|
|
112
|
+
- **The native binary fits the written artifact to `--budget`** (#277).
|
|
113
|
+
npm, Scoop, Docker and the release archives ship the Rust binary, which
|
|
114
|
+
documented `--budget` as a cap on the whole artifact and enforced nothing on
|
|
115
|
+
the document it wrote: 9,121 tokens at `--budget 8000` on a real range. It
|
|
116
|
+
now runs the same render-count-drop loop as the Python CLI (context from the
|
|
117
|
+
tail first, a changed file's second fragment next, mechanical witnesses
|
|
118
|
+
before hand-written ones), records `selection_budget_exceeded` in
|
|
119
|
+
`coverage`, and exits 4 when nothing fits — the two CLIs deliver one set.
|
|
120
|
+
- **Eval harness: gold paths git had quoted** (`"b/caf\303\251.py"`) were
|
|
121
|
+
taken literally, so every non-ASCII gold file scored as a miss; the patch
|
|
122
|
+
header parser unquotes them. A checkpoint whose last line a kill left
|
|
123
|
+
unterminated no longer swallows the next appended row on resume.
|
|
124
|
+
|
|
125
|
+
### Changed
|
|
126
|
+
|
|
127
|
+
- The GitHub Action's `python-version` defaults to `3.13` (`--output-file`
|
|
128
|
+
needs `os.fchmod`, which Windows Python gained in 3.13); the action
|
|
129
|
+
reference page says so. `--budget --help` names what `auto` resolves to.
|
|
130
|
+
|
|
10
131
|
## [1.16.0] - 2026-09-16
|
|
11
132
|
|
|
12
133
|
### Added
|
|
@@ -1551,7 +1672,9 @@ Earlier releases shipped as `treemapper`; see
|
|
|
1551
1672
|
<https://github.com/nikolay-e/diffctx/releases> for the corresponding GitHub
|
|
1552
1673
|
release notes (`1.0.0` through `1.6.1`).
|
|
1553
1674
|
|
|
1554
|
-
[Unreleased]: https://github.com/nikolay-e/diffctx/compare/v1.
|
|
1675
|
+
[Unreleased]: https://github.com/nikolay-e/diffctx/compare/v1.18.0...HEAD
|
|
1676
|
+
[1.18.0]: https://github.com/nikolay-e/diffctx/compare/v1.17.0...v1.18.0
|
|
1677
|
+
[1.17.0]: https://github.com/nikolay-e/diffctx/compare/v1.16.0...v1.17.0
|
|
1555
1678
|
[1.16.0]: https://github.com/nikolay-e/diffctx/compare/v1.15.0...v1.16.0
|
|
1556
1679
|
[1.15.0]: https://github.com/nikolay-e/diffctx/compare/v1.14.0...v1.15.0
|
|
1557
1680
|
[1.14.0]: https://github.com/nikolay-e/diffctx/compare/v1.13.0...v1.14.0
|
|
@@ -123,9 +123,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
|
|
123
123
|
|
|
124
124
|
[[package]]
|
|
125
125
|
name = "clap"
|
|
126
|
-
version = "4.6.
|
|
126
|
+
version = "4.6.7"
|
|
127
127
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
128
|
-
checksum = "
|
|
128
|
+
checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946"
|
|
129
129
|
dependencies = [
|
|
130
130
|
"clap_builder",
|
|
131
131
|
"clap_derive",
|
|
@@ -133,9 +133,9 @@ dependencies = [
|
|
|
133
133
|
|
|
134
134
|
[[package]]
|
|
135
135
|
name = "clap_builder"
|
|
136
|
-
version = "4.6.
|
|
136
|
+
version = "4.6.7"
|
|
137
137
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
138
|
-
checksum = "
|
|
138
|
+
checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d"
|
|
139
139
|
dependencies = [
|
|
140
140
|
"anstream",
|
|
141
141
|
"anstyle",
|
|
@@ -145,9 +145,9 @@ dependencies = [
|
|
|
145
145
|
|
|
146
146
|
[[package]]
|
|
147
147
|
name = "clap_derive"
|
|
148
|
-
version = "4.6.
|
|
148
|
+
version = "4.6.7"
|
|
149
149
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
150
|
-
checksum = "
|
|
150
|
+
checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0"
|
|
151
151
|
dependencies = [
|
|
152
152
|
"heck",
|
|
153
153
|
"proc-macro2",
|
|
@@ -194,11 +194,12 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
|
|
194
194
|
|
|
195
195
|
[[package]]
|
|
196
196
|
name = "diffctx"
|
|
197
|
-
version = "1.
|
|
197
|
+
version = "1.18.0"
|
|
198
198
|
dependencies = [
|
|
199
199
|
"aho-corasick",
|
|
200
200
|
"anyhow",
|
|
201
201
|
"clap",
|
|
202
|
+
"dunce",
|
|
202
203
|
"glob",
|
|
203
204
|
"libc",
|
|
204
205
|
"libtest-mimic",
|
|
@@ -260,6 +261,12 @@ dependencies = [
|
|
|
260
261
|
"walkdir",
|
|
261
262
|
]
|
|
262
263
|
|
|
264
|
+
[[package]]
|
|
265
|
+
name = "dunce"
|
|
266
|
+
version = "1.0.5"
|
|
267
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
268
|
+
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
|
269
|
+
|
|
263
270
|
[[package]]
|
|
264
271
|
name = "dyn-clone"
|
|
265
272
|
version = "1.0.20"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: diffctx
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.18.0
|
|
4
4
|
Classifier: Development Status :: 5 - Production/Stable
|
|
5
5
|
Classifier: Environment :: Console
|
|
6
6
|
Classifier: Intended Audience :: Developers
|
|
@@ -44,15 +44,15 @@ Provides-Extra: mcp
|
|
|
44
44
|
License-File: LICENSE
|
|
45
45
|
Summary: Selects the minimum code an LLM needs to review a git diff
|
|
46
46
|
Keywords: ai,chatgpt,claude,code-analysis,code-context,code-review,code-to-prompt,codebase,context,context-selection,diff-context,directory-tree,export,git-diff,json,llm,llm-context,mcp,model-context-protocol,tree,yaml
|
|
47
|
-
Home-Page: https://
|
|
47
|
+
Home-Page: https://diffctx.com/
|
|
48
48
|
Author-email: Nikolay Eremeev <nikolay.eremeev@outlook.com>
|
|
49
49
|
License-Expression: Apache-2.0
|
|
50
50
|
Requires-Python: >=3.10
|
|
51
51
|
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
52
52
|
Project-URL: Author, https://nikolay-eremeev.com
|
|
53
53
|
Project-URL: Changelog, https://github.com/nikolay-e/diffctx/blob/main/CHANGELOG.md
|
|
54
|
-
Project-URL: Documentation, https://
|
|
55
|
-
Project-URL: Homepage, https://
|
|
54
|
+
Project-URL: Documentation, https://diffctx.com/
|
|
55
|
+
Project-URL: Homepage, https://diffctx.com/
|
|
56
56
|
Project-URL: Issues, https://github.com/nikolay-e/diffctx/issues
|
|
57
57
|
Project-URL: Paper, https://doi.org/10.5281/zenodo.18824579
|
|
58
58
|
Project-URL: Repository, https://github.com/nikolay-e/diffctx
|
|
@@ -104,8 +104,15 @@ scoop bucket add diffctx https://github.com/nikolay-e/diffctx
|
|
|
104
104
|
scoop install diffctx/diffctx
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
Every [release](https://github.com/nikolay-e/diffctx/releases/latest) carries
|
|
108
|
+
prebuilt binaries; the current target set is Linux (x86_64/aarch64), macOS
|
|
109
|
+
(arm64/x86_64) and Windows (x64/arm64), and a release older than a target's
|
|
110
|
+
first build lacks it (v1.16.0 predates the macOS x86_64 and Windows arm64
|
|
111
|
+
archives). On Windows on ARM, `diffctx[mcp]` builds `cryptography` from source,
|
|
112
|
+
which needs OpenSSL: that project publishes no wheel for the platform.
|
|
113
|
+
Free-threaded CPython (`3.14t`) cannot load the `abi3` wheel, so each release
|
|
114
|
+
after 1.16.0 also ships a `cp314t` wheel per platform (the extension runs with
|
|
115
|
+
the GIL off); on 1.16.0 itself `pip` and `uv` fall back to compiling the sdist.
|
|
109
116
|
The native binary and Docker image cover diff mode with YAML/JSON output and
|
|
110
117
|
write to stdout (redirect to capture); tree mode, Markdown output, the `graph`
|
|
111
118
|
subcommand and the MCP server live in the Python package.
|
|
@@ -241,7 +248,10 @@ as `io.github.nikolay-e/diffctx`. One-line setup (zero-install via
|
|
|
241
248
|
[uv](https://docs.astral.sh/uv/)):
|
|
242
249
|
|
|
243
250
|
```bash
|
|
244
|
-
# Claude Code
|
|
251
|
+
# Claude Code plugin: the server plus /diffctx:diffctx and /diffctx:impact
|
|
252
|
+
claude plugin marketplace add nikolay-e/diffctx
|
|
253
|
+
claude plugin install diffctx@diffctx
|
|
254
|
+
# Claude Code, server only
|
|
245
255
|
claude mcp add diffctx -- uvx --from 'diffctx[mcp]' diffctx-mcp
|
|
246
256
|
# Codex CLI
|
|
247
257
|
codex mcp add diffctx -- uvx --from 'diffctx[mcp]' diffctx-mcp
|
|
@@ -293,8 +303,9 @@ map a directory named `mcp` on older releases.
|
|
|
293
303
|
|
|
294
304
|
Respects `.gitignore` and `.diffctx/ignore` automatically — hierarchically at
|
|
295
305
|
every directory level, with full gitignore semantics (negation `!important.log`,
|
|
296
|
-
anchored `/root_only.txt`), and the output file is always
|
|
297
|
-
|
|
306
|
+
anchored `/root_only.txt`), and in tree mode the output file is always
|
|
307
|
+
auto-ignored (`--diff` reports a saved `tree.md` like any other change). Three
|
|
308
|
+
controls are tree mode only and are refused with `--diff` and `graph`: `.diffctx/whitelist`
|
|
298
309
|
(`-w`) as an include-only filter, `-i` for an extra ignore file, and
|
|
299
310
|
`--no-default-ignores` / `--no-ignores` to drop the built-in patterns or every
|
|
300
311
|
ignore rule.
|
|
@@ -318,8 +329,8 @@ its size (default 512 MB, `0` disables eviction).
|
|
|
318
329
|
|------|---------|
|
|
319
330
|
| `0` | Success — output contains content |
|
|
320
331
|
| `1` | Runtime error (bad path, permission denied, etc.) |
|
|
321
|
-
| `2` | Usage error (invalid flags/arguments) |
|
|
322
|
-
| `3` | Environment error (`--diff` outside a git repo, git not installed, no commits yet) |
|
|
332
|
+
| `2` | Usage error (invalid flags/arguments, an empty `--diff ""`, a malformed duration such as `1.5h`, an out-of-range `--alpha`/`--tau`/`--budget`/`--timeout`, or a tree-mode flag — `-i`/`-w`/`--no-default-ignores`/`--no-ignores` — given with `--diff` or `graph`) |
|
|
333
|
+
| `3` | Environment error (`--diff` outside a git repo, in a bare one, or on a missing path, git not installed or refusing to start — its `fatal:` line is printed, no commits yet, a revision missing from a shallow clone) |
|
|
323
334
|
| `4` | `--diff` produced no semantic context (clean tree, binary-only, everything filtered); output is still emitted. Deletion/rename/lockfile-only diffs list `deleted_files`/`renamed_files`/`lockfile_changes` and exit `0` |
|
|
324
335
|
| `124`| `--diff` ran 30 s past the `--timeout` deadline without stopping cooperatively (the deadline itself yields a partial artifact and exit 0) |
|
|
325
336
|
| `130`| Interrupted (Ctrl-C) |
|
|
@@ -335,7 +346,7 @@ Apache 2.0
|
|
|
335
346
|
|
|
336
347
|
---
|
|
337
348
|
|
|
338
|
-
- [Documentation site](https://
|
|
349
|
+
- [Documentation site](https://diffctx.com/) — the pipeline
|
|
339
350
|
end to end: diff → fragments → graph → relevance → selection
|
|
340
351
|
- [Command-line reference](docs/product/cli.md) — every flag with its default
|
|
341
352
|
and meaning, rendered from `diffctx --help`
|
|
@@ -45,8 +45,15 @@ scoop bucket add diffctx https://github.com/nikolay-e/diffctx
|
|
|
45
45
|
scoop install diffctx/diffctx
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
Every [release](https://github.com/nikolay-e/diffctx/releases/latest) carries
|
|
49
|
+
prebuilt binaries; the current target set is Linux (x86_64/aarch64), macOS
|
|
50
|
+
(arm64/x86_64) and Windows (x64/arm64), and a release older than a target's
|
|
51
|
+
first build lacks it (v1.16.0 predates the macOS x86_64 and Windows arm64
|
|
52
|
+
archives). On Windows on ARM, `diffctx[mcp]` builds `cryptography` from source,
|
|
53
|
+
which needs OpenSSL: that project publishes no wheel for the platform.
|
|
54
|
+
Free-threaded CPython (`3.14t`) cannot load the `abi3` wheel, so each release
|
|
55
|
+
after 1.16.0 also ships a `cp314t` wheel per platform (the extension runs with
|
|
56
|
+
the GIL off); on 1.16.0 itself `pip` and `uv` fall back to compiling the sdist.
|
|
50
57
|
The native binary and Docker image cover diff mode with YAML/JSON output and
|
|
51
58
|
write to stdout (redirect to capture); tree mode, Markdown output, the `graph`
|
|
52
59
|
subcommand and the MCP server live in the Python package.
|
|
@@ -182,7 +189,10 @@ as `io.github.nikolay-e/diffctx`. One-line setup (zero-install via
|
|
|
182
189
|
[uv](https://docs.astral.sh/uv/)):
|
|
183
190
|
|
|
184
191
|
```bash
|
|
185
|
-
# Claude Code
|
|
192
|
+
# Claude Code plugin: the server plus /diffctx:diffctx and /diffctx:impact
|
|
193
|
+
claude plugin marketplace add nikolay-e/diffctx
|
|
194
|
+
claude plugin install diffctx@diffctx
|
|
195
|
+
# Claude Code, server only
|
|
186
196
|
claude mcp add diffctx -- uvx --from 'diffctx[mcp]' diffctx-mcp
|
|
187
197
|
# Codex CLI
|
|
188
198
|
codex mcp add diffctx -- uvx --from 'diffctx[mcp]' diffctx-mcp
|
|
@@ -234,8 +244,9 @@ map a directory named `mcp` on older releases.
|
|
|
234
244
|
|
|
235
245
|
Respects `.gitignore` and `.diffctx/ignore` automatically — hierarchically at
|
|
236
246
|
every directory level, with full gitignore semantics (negation `!important.log`,
|
|
237
|
-
anchored `/root_only.txt`), and the output file is always
|
|
238
|
-
|
|
247
|
+
anchored `/root_only.txt`), and in tree mode the output file is always
|
|
248
|
+
auto-ignored (`--diff` reports a saved `tree.md` like any other change). Three
|
|
249
|
+
controls are tree mode only and are refused with `--diff` and `graph`: `.diffctx/whitelist`
|
|
239
250
|
(`-w`) as an include-only filter, `-i` for an extra ignore file, and
|
|
240
251
|
`--no-default-ignores` / `--no-ignores` to drop the built-in patterns or every
|
|
241
252
|
ignore rule.
|
|
@@ -259,8 +270,8 @@ its size (default 512 MB, `0` disables eviction).
|
|
|
259
270
|
|------|---------|
|
|
260
271
|
| `0` | Success — output contains content |
|
|
261
272
|
| `1` | Runtime error (bad path, permission denied, etc.) |
|
|
262
|
-
| `2` | Usage error (invalid flags/arguments) |
|
|
263
|
-
| `3` | Environment error (`--diff` outside a git repo, git not installed, no commits yet) |
|
|
273
|
+
| `2` | Usage error (invalid flags/arguments, an empty `--diff ""`, a malformed duration such as `1.5h`, an out-of-range `--alpha`/`--tau`/`--budget`/`--timeout`, or a tree-mode flag — `-i`/`-w`/`--no-default-ignores`/`--no-ignores` — given with `--diff` or `graph`) |
|
|
274
|
+
| `3` | Environment error (`--diff` outside a git repo, in a bare one, or on a missing path, git not installed or refusing to start — its `fatal:` line is printed, no commits yet, a revision missing from a shallow clone) |
|
|
264
275
|
| `4` | `--diff` produced no semantic context (clean tree, binary-only, everything filtered); output is still emitted. Deletion/rename/lockfile-only diffs list `deleted_files`/`renamed_files`/`lockfile_changes` and exit `0` |
|
|
265
276
|
| `124`| `--diff` ran 30 s past the `--timeout` deadline without stopping cooperatively (the deadline itself yields a partial artifact and exit 0) |
|
|
266
277
|
| `130`| Interrupted (Ctrl-C) |
|
|
@@ -276,7 +287,7 @@ Apache 2.0
|
|
|
276
287
|
|
|
277
288
|
---
|
|
278
289
|
|
|
279
|
-
- [Documentation site](https://
|
|
290
|
+
- [Documentation site](https://diffctx.com/) — the pipeline
|
|
280
291
|
end to end: diff → fragments → graph → relevance → selection
|
|
281
292
|
- [Command-line reference](docs/product/cli.md) — every flag with its default
|
|
282
293
|
and meaning, rendered from `diffctx --help`
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "diffctx"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.18.0"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
rust-version = "1.85"
|
|
6
6
|
description = "Selects the minimum code an LLM needs to review a git diff: walks the dependency graph outward from changed lines and stops when extra context stops paying for itself"
|
|
7
7
|
license = "Apache-2.0"
|
|
8
8
|
repository = "https://github.com/nikolay-e/diffctx"
|
|
9
|
-
homepage = "https://
|
|
9
|
+
homepage = "https://diffctx.com/"
|
|
10
10
|
documentation = "https://docs.rs/diffctx"
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
keywords = ["git", "diff", "llm", "context", "tree-sitter"]
|
|
@@ -71,6 +71,7 @@ lang-extra = [
|
|
|
71
71
|
[dependencies]
|
|
72
72
|
mimalloc = { version = "0.1", default-features = false }
|
|
73
73
|
clap = { version = "4", features = ["derive"] }
|
|
74
|
+
dunce = "1"
|
|
74
75
|
tree-sitter = "0.25"
|
|
75
76
|
tree-sitter-python = { version = "0.25", optional = true }
|
|
76
77
|
tree-sitter-javascript = { version = "0.25", optional = true }
|
|
@@ -45,8 +45,15 @@ scoop bucket add diffctx https://github.com/nikolay-e/diffctx
|
|
|
45
45
|
scoop install diffctx/diffctx
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
Every [release](https://github.com/nikolay-e/diffctx/releases/latest) carries
|
|
49
|
+
prebuilt binaries; the current target set is Linux (x86_64/aarch64), macOS
|
|
50
|
+
(arm64/x86_64) and Windows (x64/arm64), and a release older than a target's
|
|
51
|
+
first build lacks it (v1.16.0 predates the macOS x86_64 and Windows arm64
|
|
52
|
+
archives). On Windows on ARM, `diffctx[mcp]` builds `cryptography` from source,
|
|
53
|
+
which needs OpenSSL: that project publishes no wheel for the platform.
|
|
54
|
+
Free-threaded CPython (`3.14t`) cannot load the `abi3` wheel, so each release
|
|
55
|
+
after 1.16.0 also ships a `cp314t` wheel per platform (the extension runs with
|
|
56
|
+
the GIL off); on 1.16.0 itself `pip` and `uv` fall back to compiling the sdist.
|
|
50
57
|
The native binary and Docker image cover diff mode with YAML/JSON output and
|
|
51
58
|
write to stdout (redirect to capture); tree mode, Markdown output, the `graph`
|
|
52
59
|
subcommand and the MCP server live in the Python package.
|
|
@@ -182,7 +189,10 @@ as `io.github.nikolay-e/diffctx`. One-line setup (zero-install via
|
|
|
182
189
|
[uv](https://docs.astral.sh/uv/)):
|
|
183
190
|
|
|
184
191
|
```bash
|
|
185
|
-
# Claude Code
|
|
192
|
+
# Claude Code plugin: the server plus /diffctx:diffctx and /diffctx:impact
|
|
193
|
+
claude plugin marketplace add nikolay-e/diffctx
|
|
194
|
+
claude plugin install diffctx@diffctx
|
|
195
|
+
# Claude Code, server only
|
|
186
196
|
claude mcp add diffctx -- uvx --from 'diffctx[mcp]' diffctx-mcp
|
|
187
197
|
# Codex CLI
|
|
188
198
|
codex mcp add diffctx -- uvx --from 'diffctx[mcp]' diffctx-mcp
|
|
@@ -234,8 +244,9 @@ map a directory named `mcp` on older releases.
|
|
|
234
244
|
|
|
235
245
|
Respects `.gitignore` and `.diffctx/ignore` automatically — hierarchically at
|
|
236
246
|
every directory level, with full gitignore semantics (negation `!important.log`,
|
|
237
|
-
anchored `/root_only.txt`), and the output file is always
|
|
238
|
-
|
|
247
|
+
anchored `/root_only.txt`), and in tree mode the output file is always
|
|
248
|
+
auto-ignored (`--diff` reports a saved `tree.md` like any other change). Three
|
|
249
|
+
controls are tree mode only and are refused with `--diff` and `graph`: `.diffctx/whitelist`
|
|
239
250
|
(`-w`) as an include-only filter, `-i` for an extra ignore file, and
|
|
240
251
|
`--no-default-ignores` / `--no-ignores` to drop the built-in patterns or every
|
|
241
252
|
ignore rule.
|
|
@@ -259,8 +270,8 @@ its size (default 512 MB, `0` disables eviction).
|
|
|
259
270
|
|------|---------|
|
|
260
271
|
| `0` | Success — output contains content |
|
|
261
272
|
| `1` | Runtime error (bad path, permission denied, etc.) |
|
|
262
|
-
| `2` | Usage error (invalid flags/arguments) |
|
|
263
|
-
| `3` | Environment error (`--diff` outside a git repo, git not installed, no commits yet) |
|
|
273
|
+
| `2` | Usage error (invalid flags/arguments, an empty `--diff ""`, a malformed duration such as `1.5h`, an out-of-range `--alpha`/`--tau`/`--budget`/`--timeout`, or a tree-mode flag — `-i`/`-w`/`--no-default-ignores`/`--no-ignores` — given with `--diff` or `graph`) |
|
|
274
|
+
| `3` | Environment error (`--diff` outside a git repo, in a bare one, or on a missing path, git not installed or refusing to start — its `fatal:` line is printed, no commits yet, a revision missing from a shallow clone) |
|
|
264
275
|
| `4` | `--diff` produced no semantic context (clean tree, binary-only, everything filtered); output is still emitted. Deletion/rename/lockfile-only diffs list `deleted_files`/`renamed_files`/`lockfile_changes` and exit `0` |
|
|
265
276
|
| `124`| `--diff` ran 30 s past the `--timeout` deadline without stopping cooperatively (the deadline itself yields a partial artifact and exit 0) |
|
|
266
277
|
| `130`| Interrupted (Ctrl-C) |
|
|
@@ -276,7 +287,7 @@ Apache 2.0
|
|
|
276
287
|
|
|
277
288
|
---
|
|
278
289
|
|
|
279
|
-
- [Documentation site](https://
|
|
290
|
+
- [Documentation site](https://diffctx.com/) — the pipeline
|
|
280
291
|
end to end: diff → fragments → graph → relevance → selection
|
|
281
292
|
- [Command-line reference](docs/product/cli.md) — every flag with its default
|
|
282
293
|
and meaning, rendered from `diffctx --help`
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
use std::path::{Path, PathBuf};
|
|
2
|
+
|
|
3
|
+
use rayon::prelude::*;
|
|
4
|
+
use rustc_hash::{FxHashMap, FxHashSet};
|
|
5
|
+
use walkdir::WalkDir;
|
|
6
|
+
|
|
7
|
+
use crate::config::graph_filtering::GRAPH_FILTERING;
|
|
8
|
+
use crate::config::limits::LIMITS;
|
|
9
|
+
use crate::git;
|
|
10
|
+
use crate::languages::get_language_for_file;
|
|
11
|
+
|
|
12
|
+
const SHEBANG_SNIFF_BYTES: usize = 128;
|
|
13
|
+
|
|
14
|
+
fn is_allowed_file(path: &Path) -> bool {
|
|
15
|
+
let name = path.to_string_lossy();
|
|
16
|
+
get_language_for_file(&name).is_some()
|
|
17
|
+
|| (path.extension().is_none() && has_known_shebang(path, &name))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
fn has_known_shebang(path: &Path, name: &str) -> bool {
|
|
21
|
+
use std::io::Read;
|
|
22
|
+
let mut head = [0u8; SHEBANG_SNIFF_BYTES];
|
|
23
|
+
let Ok(read) = std::fs::File::open(path).and_then(|mut f| f.read(&mut head)) else {
|
|
24
|
+
return false;
|
|
25
|
+
};
|
|
26
|
+
crate::languages::sniff_language(name, &String::from_utf8_lossy(&head[..read])).is_some()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fn is_candidate_file(
|
|
30
|
+
file_path: &Path,
|
|
31
|
+
root_dir: &Path,
|
|
32
|
+
included_set: &FxHashSet<PathBuf>,
|
|
33
|
+
ctx: &crate::resource::RunContext,
|
|
34
|
+
) -> bool {
|
|
35
|
+
// `is_file()` and `metadata()` both follow symlinks, and every path here is
|
|
36
|
+
// only LEXICALLY under the root — `git ls-files` reports the link, not its
|
|
37
|
+
// target. Without this the universe admits `repo/evil -> /etc/shadow`,
|
|
38
|
+
// discovery reads it with a bare `read_to_string`, and the content is a
|
|
39
|
+
// context fragment. The boundary is checked once, here, so the readers
|
|
40
|
+
// downstream stay simple; a link pointing INSIDE the repo still resolves.
|
|
41
|
+
if crate::paths::resolve_within(root_dir, file_path).is_none() {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
if !file_path.is_file() {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
if !is_allowed_file(file_path) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
if included_set.contains(file_path) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
match file_path.metadata() {
|
|
54
|
+
Ok(meta) if meta.len() as usize > LIMITS.max_file_size => {
|
|
55
|
+
// A source file the universe will not read is a hole in the
|
|
56
|
+
// context, and the artifact says so rather than rendering as
|
|
57
|
+
// complete (#T4.2: a 237 KB helper importing the changed file
|
|
58
|
+
// vanished with no coverage block).
|
|
59
|
+
ctx.note(crate::resource::LimitReason::FileTooLarge);
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
Err(_) => return false,
|
|
63
|
+
_ => {}
|
|
64
|
+
}
|
|
65
|
+
true
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// The discovery universe under `scope` (repo-relative pathspecs; empty is
|
|
69
|
+
/// the whole repository), with every size exclusion recorded on `ctx`.
|
|
70
|
+
pub fn collect_candidate_files(
|
|
71
|
+
root_dir: &Path,
|
|
72
|
+
included_set: &FxHashSet<PathBuf>,
|
|
73
|
+
scope: &[String],
|
|
74
|
+
ctx: &crate::resource::RunContext,
|
|
75
|
+
) -> Vec<PathBuf> {
|
|
76
|
+
match tracked_candidates(root_dir, included_set, scope, ctx) {
|
|
77
|
+
Some(files) => filter_ignored_and_secret(root_dir, files),
|
|
78
|
+
// The walk sees untracked paths, so ancestor-inherited rules must count
|
|
79
|
+
// (`.venv/x.py` is ignored BY `.venv/`); the attribution variant drops them.
|
|
80
|
+
None => filter_ignored_and_secret_walked(
|
|
81
|
+
root_dir,
|
|
82
|
+
walked_candidates(root_dir, included_set, scope, ctx),
|
|
83
|
+
),
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fn tracked_candidates(
|
|
88
|
+
root_dir: &Path,
|
|
89
|
+
included_set: &FxHashSet<PathBuf>,
|
|
90
|
+
scope: &[String],
|
|
91
|
+
ctx: &crate::resource::RunContext,
|
|
92
|
+
) -> Option<Vec<PathBuf>> {
|
|
93
|
+
let mut args: Vec<&str> = vec!["ls-files", "-z"];
|
|
94
|
+
if !scope.is_empty() {
|
|
95
|
+
args.push("--");
|
|
96
|
+
args.extend(scope.iter().map(String::as_str));
|
|
97
|
+
}
|
|
98
|
+
let parts = git::run_git_z(root_dir, &args).ok()?;
|
|
99
|
+
Some(
|
|
100
|
+
parts
|
|
101
|
+
.into_iter()
|
|
102
|
+
.map(|f| crate::paths::repo_join(root_dir, &f))
|
|
103
|
+
.collect::<Vec<PathBuf>>()
|
|
104
|
+
.into_par_iter()
|
|
105
|
+
.filter(|f| is_candidate_file(f, root_dir, included_set, ctx))
|
|
106
|
+
.collect(),
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
fn is_walk_skipped_dir(entry: &walkdir::DirEntry) -> bool {
|
|
111
|
+
entry.depth() > 0
|
|
112
|
+
&& entry.file_type().is_dir()
|
|
113
|
+
&& entry.file_name().to_str().is_some_and(|name| {
|
|
114
|
+
name.starts_with('.') || name == "node_modules" || name == "__pycache__"
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
fn walked_candidates(
|
|
119
|
+
root_dir: &Path,
|
|
120
|
+
included_set: &FxHashSet<PathBuf>,
|
|
121
|
+
scope: &[String],
|
|
122
|
+
ctx: &crate::resource::RunContext,
|
|
123
|
+
) -> Vec<PathBuf> {
|
|
124
|
+
let walk_roots: Vec<PathBuf> = if scope.is_empty() {
|
|
125
|
+
vec![root_dir.to_path_buf()]
|
|
126
|
+
} else {
|
|
127
|
+
scope
|
|
128
|
+
.iter()
|
|
129
|
+
.map(|rel| crate::paths::repo_join(root_dir, rel))
|
|
130
|
+
.collect()
|
|
131
|
+
};
|
|
132
|
+
let mut admitted: Vec<PathBuf> = Vec::new();
|
|
133
|
+
for walk_root in walk_roots {
|
|
134
|
+
let files = WalkDir::new(walk_root)
|
|
135
|
+
.sort_by_file_name()
|
|
136
|
+
.into_iter()
|
|
137
|
+
.filter_entry(|e| !is_walk_skipped_dir(e))
|
|
138
|
+
.filter_map(Result::ok)
|
|
139
|
+
.filter(|e| e.file_type().is_file());
|
|
140
|
+
for entry in files {
|
|
141
|
+
let path = entry.into_path();
|
|
142
|
+
if is_candidate_file(&path, root_dir, included_set, ctx) {
|
|
143
|
+
admitted.push(path);
|
|
144
|
+
}
|
|
145
|
+
// Counted AFTER admission: capping raw entries meant a repository
|
|
146
|
+
// whose first N walked files are ignored (a vendored tree, a build
|
|
147
|
+
// directory) produced a nearly empty universe while the useful files
|
|
148
|
+
// sat just past the cut.
|
|
149
|
+
if admitted.len() >= GRAPH_FILTERING.fallback_max_files {
|
|
150
|
+
return admitted;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
admitted
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
fn filter_ignored_and_secret_walked(root_dir: &Path, files: Vec<PathBuf>) -> Vec<PathBuf> {
|
|
158
|
+
let rel_paths: Vec<String> = files
|
|
159
|
+
.iter()
|
|
160
|
+
.filter_map(|f| crate::pipeline::rel_path_string(root_dir, f))
|
|
161
|
+
.collect();
|
|
162
|
+
let ignored: FxHashMap<String, git::IgnoreSource> =
|
|
163
|
+
git::find_ignored_paths_any(root_dir, &rel_paths)
|
|
164
|
+
.into_iter()
|
|
165
|
+
.map(|p| (p, git::IgnoreSource::Gitignore))
|
|
166
|
+
.collect();
|
|
167
|
+
files
|
|
168
|
+
.into_par_iter()
|
|
169
|
+
.filter(|f| !crate::pipeline::is_withheld(root_dir, f, &ignored))
|
|
170
|
+
.collect()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// Discovery's candidate universe otherwise skips straight from a language
|
|
174
|
+
/// check to the graph: an unchanged file a changed file imports would render
|
|
175
|
+
/// as neighbour context even when `.diffctx/ignore` explicitly excludes it,
|
|
176
|
+
/// or when it is secret-like (`id_rsa`, `*.pem`, ...) — `changed_files` is
|
|
177
|
+
/// filtered this way already (`pipeline::compute_scored_state`), the
|
|
178
|
+
/// discovery universe was not. One batched `git check-ignore` call covers
|
|
179
|
+
/// every survivor of the language filter, so cost stays O(1) subprocess
|
|
180
|
+
/// invocations regardless of repo size, and the final filter still runs in
|
|
181
|
+
/// parallel via rayon.
|
|
182
|
+
fn filter_ignored_and_secret(root_dir: &Path, files: Vec<PathBuf>) -> Vec<PathBuf> {
|
|
183
|
+
let rel_paths: Vec<String> = files
|
|
184
|
+
.iter()
|
|
185
|
+
.filter_map(|f| crate::pipeline::rel_path_string(root_dir, f))
|
|
186
|
+
.collect();
|
|
187
|
+
let ignored_rel_paths = git::find_ignored_paths_with_source(root_dir, &rel_paths);
|
|
188
|
+
files
|
|
189
|
+
.into_par_iter()
|
|
190
|
+
.filter(|f| !crate::pipeline::is_withheld(root_dir, f, &ignored_rel_paths))
|
|
191
|
+
.collect()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
pub fn normalize_path(path: &Path, root_dir: &Path) -> PathBuf {
|
|
195
|
+
if path.is_absolute() {
|
|
196
|
+
dunce::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
|
|
197
|
+
} else {
|
|
198
|
+
let joined = crate::paths::repo_join(root_dir, &path.to_string_lossy());
|
|
199
|
+
dunce::canonicalize(&joined).unwrap_or(joined)
|
|
200
|
+
}
|
|
201
|
+
}
|