diffctx 1.12.0__tar.gz → 1.12.1__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.12.0 → diffctx-1.12.1}/CHANGELOG.md +23 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/PKG-INFO +17 -3
- {diffctx-1.12.0 → diffctx-1.12.1}/README.md +15 -2
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/Cargo.lock +1 -1
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/Cargo.toml +10 -9
- diffctx-1.12.1/diffctx/README.md +210 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/main.rs +5 -2
- {diffctx-1.12.0 → diffctx-1.12.1}/pyproject.toml +1 -1
- diffctx-1.12.1/src/diffctx/version.py +1 -0
- diffctx-1.12.0/diffctx/tests/common/mod.rs +0 -274
- diffctx-1.12.0/diffctx/tests/fixtures/garbage/garbage_api.py +0 -31
- diffctx-1.12.0/diffctx/tests/fixtures/garbage/garbage_constants.py +0 -11
- diffctx-1.12.0/diffctx/tests/fixtures/garbage/garbage_handlers.py +0 -27
- diffctx-1.12.0/diffctx/tests/fixtures/garbage/garbage_models.py +0 -25
- diffctx-1.12.0/diffctx/tests/fixtures/garbage/garbage_module.js +0 -23
- diffctx-1.12.0/diffctx/tests/fixtures/garbage/garbage_services.py +0 -37
- diffctx-1.12.0/diffctx/tests/fixtures/garbage/garbage_types.py +0 -31
- diffctx-1.12.0/diffctx/tests/fixtures/garbage/garbage_unrelated.yaml +0 -8
- diffctx-1.12.0/diffctx/tests/fixtures/garbage/garbage_utils.py +0 -31
- diffctx-1.12.0/diffctx/tests/fixtures/garbage/garbage_validators.py +0 -23
- diffctx-1.12.0/diffctx/tests/yaml_cases.rs +0 -276
- diffctx-1.12.0/src/diffctx/version.py +0 -1
- {diffctx-1.12.0 → diffctx-1.12.1}/LICENSE +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/analytics.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/candidate_files.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/config/analytics.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/config/bm25.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/config/budget.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/config/category_weights.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/config/edge_weights.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/config/env_overrides.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/config/extensions.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/config/filtering.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/config/fragmentation.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/config/git.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/config/graph_filtering.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/config/importance.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/config/limits.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/config/mod.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/config/mode.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/config/needs.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/config/parsers.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/config/render.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/config/scoring.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/config/selection.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/config/tokenization.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/config/weights.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/core.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/discovery.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/base.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/config_edges/build_system.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/config_edges/cicd.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/config_edges/docker.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/config_edges/generic.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/config_edges/helm.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/config_edges/kubernetes.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/config_edges/mod.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/document/mod.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/history/cochange.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/history/mod.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/mod.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/ansible.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/bazel.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/c_family.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/cargo_edges.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/clojure.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/css.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/dart.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/dbt.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/dotnet.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/elixir.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/erlang.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/go.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/graphql.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/haskell.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/javascript.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/julia.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/jvm.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/latex.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/lua.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/mod.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/nim.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/nix.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/ocaml.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/openapi.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/perl.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/php.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/prisma.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/protobuf.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/python.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/r_lang.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/ruby.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/rust_lang.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/shell.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/sql.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/swift.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/tags.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/terraform.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/zig.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/similarity/lexical.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/similarity/mod.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/structural/containment.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/structural/mod.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/structural/sibling.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/edges/structural/testing.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/filtering.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/fragmentation.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/git.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/graph.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/graph_export.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/interval.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/languages.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/lib.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/memory_pipeline.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/mode.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/parsers/config_parser.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/parsers/generic.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/parsers/markdown.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/parsers/mod.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/parsers/tree_sitter_strategy.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/peak_rss.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/pipeline.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/postpass.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/ppr.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/project_graph.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/pybridge.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/render.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/scoring.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/select.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/signatures.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/stopwords.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/test_harness.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/token_corpus.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/tokenizer.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/types.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/utility/boltzmann.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/utility/importance.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/utility/mod.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/utility/needs.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/diffctx/src/utility/scoring.rs +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/rust-toolchain.toml +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/src/diffctx/__init__.py +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/src/diffctx/__main__.py +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/src/diffctx/cli.py +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/src/diffctx/clipboard.py +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/src/diffctx/diffctx/__init__.py +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/src/diffctx/diffctx/graph_analytics.py +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/src/diffctx/diffctx/graph_export.py +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/src/diffctx/diffctx/pipeline.py +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/src/diffctx/diffctx/project_graph.py +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/src/diffctx/ignore.py +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/src/diffctx/logger.py +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/src/diffctx/main.py +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/src/diffctx/mcp/README.md +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/src/diffctx/mcp/__init__.py +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/src/diffctx/mcp/__main__.py +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/src/diffctx/mcp/formatting.py +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/src/diffctx/mcp/security.py +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/src/diffctx/mcp/server.py +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/src/diffctx/py.typed +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/src/diffctx/tokens.py +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/src/diffctx/tree.py +0 -0
- {diffctx-1.12.0 → diffctx-1.12.1}/src/diffctx/writer.py +0 -0
|
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Standalone native binaries (linux x86_64/aarch64, macOS arm64, Windows
|
|
13
|
+
x64) are built and attached to every GitHub release. The README had
|
|
14
|
+
promised them since 1.9.x; releases only ever carried wheels and an sdist.
|
|
15
|
+
- The Rust engine is published to crates.io as the `diffctx` crate
|
|
16
|
+
(`cargo install diffctx` for the native CLI, `cargo add diffctx` to embed the
|
|
17
|
+
selection pipeline). Previously the name held only a reservation stub; the
|
|
18
|
+
crate now carries the released engine, starting at 1.12.0.
|
|
19
|
+
- Container image `ghcr.io/nikolay-e/diffctx` (linux amd64/arm64), built from
|
|
20
|
+
the release tag and smoke-tested against a real repository before the tag
|
|
21
|
+
moves: `docker run --rm -v "$PWD:/repo" ghcr.io/nikolay-e/diffctx . --diff HEAD~1`.
|
|
22
|
+
- Packaging manifests generated from the release checksums: Scoop
|
|
23
|
+
(`packaging/scoop/diffctx.json`), AUR (`packaging/aur/`) and an npm wrapper
|
|
24
|
+
(`packaging/npm/`) that downloads the platform binary and verifies its
|
|
25
|
+
SHA-256 against the published checksum.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- The native binary silently emitted YAML for every unrecognized `--format`,
|
|
30
|
+
including `md` — the documented default of the Python CLI. It now accepts
|
|
31
|
+
only `yaml`/`json` and exits 2 on anything else.
|
|
32
|
+
|
|
10
33
|
## [1.12.0] - 2026-07-23
|
|
11
34
|
|
|
12
35
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: diffctx
|
|
3
|
-
Version: 1.12.
|
|
3
|
+
Version: 1.12.1
|
|
4
4
|
Classifier: Development Status :: 5 - Production/Stable
|
|
5
5
|
Classifier: Environment :: Console
|
|
6
6
|
Classifier: Intended Audience :: Developers
|
|
@@ -64,6 +64,7 @@ Provides-Extra: tree-sitter
|
|
|
64
64
|
License-File: LICENSE
|
|
65
65
|
Summary: Export codebase structure and contents for AI/LLM context
|
|
66
66
|
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
|
|
67
|
+
Home-Page: https://github.com/nikolay-e/diffctx
|
|
67
68
|
Author-email: Nikolay Eremeev <nikolay.eremeev@outlook.com>
|
|
68
69
|
License-Expression: Apache-2.0
|
|
69
70
|
Requires-Python: >=3.10
|
|
@@ -77,6 +78,7 @@ Project-URL: Repository, https://github.com/nikolay-e/diffctx
|
|
|
77
78
|
|
|
78
79
|
[](https://github.com/nikolay-e/diffctx/actions/workflows/ci.yml)
|
|
79
80
|
[](https://pypi.org/project/diffctx/)
|
|
81
|
+
[](https://crates.io/crates/diffctx)
|
|
80
82
|
[](https://pypi.org/project/diffctx/)
|
|
81
83
|
|
|
82
84
|
**diffctx selects the minimum code an LLM needs to review a git diff.**
|
|
@@ -109,11 +111,23 @@ pip install diffctx # or: into an active environment
|
|
|
109
111
|
pipx install 'diffctx[mcp]' # + MCP server for AI assistants
|
|
110
112
|
```
|
|
111
113
|
|
|
112
|
-
A standalone binary (no Python required) is on the
|
|
113
|
-
[releases page](https://github.com/nikolay-e/diffctx/releases/latest).
|
|
114
114
|
The `[tree-sitter]` extra adds AST-level parsing for more accurate context
|
|
115
115
|
selection across 30+ languages.
|
|
116
116
|
|
|
117
|
+
Without Python:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
cargo install diffctx # native CLI from crates.io
|
|
121
|
+
docker run --rm -v "$PWD:/repo" ghcr.io/nikolay-e/diffctx . --diff HEAD~1
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Prebuilt binaries for linux (x86_64/aarch64), macOS (arm64) and Windows (x64)
|
|
125
|
+
are attached to every [release](https://github.com/nikolay-e/diffctx/releases/latest).
|
|
126
|
+
The native binary covers diff mode with YAML/JSON output; tree mode, Markdown
|
|
127
|
+
output, the `graph` subcommand and the MCP server live in the Python package.
|
|
128
|
+
`cargo add diffctx` embeds the selection pipeline in a Rust project
|
|
129
|
+
([docs.rs](https://docs.rs/diffctx)).
|
|
130
|
+
|
|
117
131
|
## Quick start
|
|
118
132
|
|
|
119
133
|
```bash
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/nikolay-e/diffctx/actions/workflows/ci.yml)
|
|
4
4
|
[](https://pypi.org/project/diffctx/)
|
|
5
|
+
[](https://crates.io/crates/diffctx)
|
|
5
6
|
[](https://pypi.org/project/diffctx/)
|
|
6
7
|
|
|
7
8
|
**diffctx selects the minimum code an LLM needs to review a git diff.**
|
|
@@ -34,11 +35,23 @@ pip install diffctx # or: into an active environment
|
|
|
34
35
|
pipx install 'diffctx[mcp]' # + MCP server for AI assistants
|
|
35
36
|
```
|
|
36
37
|
|
|
37
|
-
A standalone binary (no Python required) is on the
|
|
38
|
-
[releases page](https://github.com/nikolay-e/diffctx/releases/latest).
|
|
39
38
|
The `[tree-sitter]` extra adds AST-level parsing for more accurate context
|
|
40
39
|
selection across 30+ languages.
|
|
41
40
|
|
|
41
|
+
Without Python:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
cargo install diffctx # native CLI from crates.io
|
|
45
|
+
docker run --rm -v "$PWD:/repo" ghcr.io/nikolay-e/diffctx . --diff HEAD~1
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Prebuilt binaries for linux (x86_64/aarch64), macOS (arm64) and Windows (x64)
|
|
49
|
+
are attached to every [release](https://github.com/nikolay-e/diffctx/releases/latest).
|
|
50
|
+
The native binary covers diff mode with YAML/JSON output; tree mode, Markdown
|
|
51
|
+
output, the `graph` subcommand and the MCP server live in the Python package.
|
|
52
|
+
`cargo add diffctx` embeds the selection pipeline in a Rust project
|
|
53
|
+
([docs.rs](https://docs.rs/diffctx)).
|
|
54
|
+
|
|
42
55
|
## Quick start
|
|
43
56
|
|
|
44
57
|
```bash
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "diffctx"
|
|
3
|
-
version = "1.12.
|
|
3
|
+
version = "1.12.1"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
rust-version = "1.85"
|
|
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
|
+
license = "Apache-2.0"
|
|
8
|
+
repository = "https://github.com/nikolay-e/diffctx"
|
|
9
|
+
homepage = "https://github.com/nikolay-e/diffctx"
|
|
10
|
+
documentation = "https://docs.rs/diffctx"
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
keywords = ["git", "diff", "llm", "context", "tree-sitter"]
|
|
13
|
+
categories = ["development-tools", "command-line-utilities"]
|
|
14
|
+
exclude = ["tests/**", "src/test_harness.rs"]
|
|
6
15
|
|
|
7
16
|
[lib]
|
|
8
17
|
name = "_diffctx"
|
|
@@ -12,10 +21,6 @@ crate-type = ["cdylib", "rlib"]
|
|
|
12
21
|
name = "diffctx"
|
|
13
22
|
path = "src/main.rs"
|
|
14
23
|
|
|
15
|
-
[[example]]
|
|
16
|
-
name = "diffctx-test"
|
|
17
|
-
path = "src/test_harness.rs"
|
|
18
|
-
|
|
19
24
|
[features]
|
|
20
25
|
default = ["lang-core", "lang-extra"]
|
|
21
26
|
python = ["pyo3"]
|
|
@@ -127,10 +132,6 @@ pyo3 = { version = "0.29", features = ["extension-module", "abi3-py310"], option
|
|
|
127
132
|
tempfile = "3"
|
|
128
133
|
libtest-mimic = "0.8"
|
|
129
134
|
|
|
130
|
-
[[test]]
|
|
131
|
-
name = "yaml_cases"
|
|
132
|
-
harness = false
|
|
133
|
-
|
|
134
135
|
[lints.clippy]
|
|
135
136
|
pedantic = { level = "warn", priority = -1 }
|
|
136
137
|
nursery = { level = "warn", priority = -1 }
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# diffctx — smart diff context for LLM code review
|
|
2
|
+
|
|
3
|
+
[](https://github.com/nikolay-e/diffctx/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/diffctx/)
|
|
5
|
+
[](https://crates.io/crates/diffctx)
|
|
6
|
+
[](https://pypi.org/project/diffctx/)
|
|
7
|
+
|
|
8
|
+
**diffctx selects the minimum code an LLM needs to review a git diff.**
|
|
9
|
+
Instead of pasting whole files, it walks the dependency graph from the changed
|
|
10
|
+
lines outward and stops as soon as additional context stops paying for itself.
|
|
11
|
+
|
|
12
|
+
> Coming from [`treemapper`](https://pypi.org/project/treemapper/)? That name is
|
|
13
|
+
> deprecated — it was a thin wrapper around diffctx. Every command, flag, and
|
|
14
|
+
> API call works unchanged: `treemapper` → `diffctx`, `treemapper-mcp` → `diffctx-mcp`.
|
|
15
|
+
|
|
16
|
+
## Why not just use `tree` or repomix?
|
|
17
|
+
|
|
18
|
+
| | `tree` | repomix | Claude Code Review | **diffctx** |
|
|
19
|
+
|---|:---:|:---:|:---:|:---:|
|
|
20
|
+
| **Primary use case** | directory listing | full repo export | automated PR review | **diff context for code review** |
|
|
21
|
+
| Smart diff context | ✗ | ✗ | ✓ | ✓ |
|
|
22
|
+
| Works with any LLM | ✓ | ✓ | Claude only | ✓ |
|
|
23
|
+
| Free / local / offline | ✓ | ✓ | $15–25/review | ✓ |
|
|
24
|
+
| GitHub required | ✗ | ✗ | ✓ | ✗ |
|
|
25
|
+
| Multiple output formats | ✗ | limited | — | YAML/JSON/MD/txt |
|
|
26
|
+
| Python API | ✗ | ✗ | ✗ | ✓ |
|
|
27
|
+
| MCP server | ✗ | ✗ | ✗ | ✓ |
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
uvx diffctx . --diff HEAD~1 # zero-install, run once via uv
|
|
33
|
+
pipx install diffctx # recommended: isolated CLI, no venv needed
|
|
34
|
+
pip install diffctx # or: into an active environment
|
|
35
|
+
pipx install 'diffctx[mcp]' # + MCP server for AI assistants
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The `[tree-sitter]` extra adds AST-level parsing for more accurate context
|
|
39
|
+
selection across 30+ languages.
|
|
40
|
+
|
|
41
|
+
Without Python:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
cargo install diffctx # native CLI from crates.io
|
|
45
|
+
docker run --rm -v "$PWD:/repo" ghcr.io/nikolay-e/diffctx . --diff HEAD~1
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Prebuilt binaries for linux (x86_64/aarch64), macOS (arm64) and Windows (x64)
|
|
49
|
+
are attached to every [release](https://github.com/nikolay-e/diffctx/releases/latest).
|
|
50
|
+
The native binary covers diff mode with YAML/JSON output; tree mode, Markdown
|
|
51
|
+
output, the `graph` subcommand and the MCP server live in the Python package.
|
|
52
|
+
`cargo add diffctx` embeds the selection pipeline in a Rust project
|
|
53
|
+
([docs.rs](https://docs.rs/diffctx)).
|
|
54
|
+
|
|
55
|
+
## Quick start
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
diffctx . --diff HEAD~1 # smart context for last commit → paste into Claude/ChatGPT
|
|
59
|
+
diffctx . -f md -c # full codebase export → clipboard in Markdown
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+

|
|
63
|
+
|
|
64
|
+
*`diffctx . --diff HEAD~1` selects only the fragments — functions, imports,
|
|
65
|
+
type definitions — that an LLM actually needs to review the last commit,
|
|
66
|
+
instead of dumping every changed file in full.*
|
|
67
|
+
|
|
68
|
+
## Diff context mode
|
|
69
|
+
|
|
70
|
+
Finds the minimal set of code fragments needed to understand a change —
|
|
71
|
+
imports, callers, type definitions, config dependencies — across 50+ file
|
|
72
|
+
types. It builds a code graph (imports, co-changes, type refs), propagates
|
|
73
|
+
relevance from the changed lines outward, and stops when relevance drops below
|
|
74
|
+
`--tau` or the `--budget` token cap is reached.
|
|
75
|
+
|
|
76
|
+
| Flag | Default | Description |
|
|
77
|
+
|-------------|---------|--------------------------------------------------------------------------|
|
|
78
|
+
| `--scoring` | `ego` | `ego` = bounded expansion around changed nodes (fast, predictable radius); `ppr` = Personalized PageRank (global, smoother decay, slower); `bm25` = lexical retrieval against the diff hunks (baseline for sparse graphs) |
|
|
79
|
+
| `--budget` | auto | Hard token cap: `N` enforces a fixed cap, `-1` disables it, `0` is a strict-zero floor (empty selection; use `--full` for changed files only) |
|
|
80
|
+
| `--alpha` | 0.60 | PPR damping; higher = context clusters tighter around changes (`--scoring ppr` only) |
|
|
81
|
+
| `--tau` | 0.12 | Relevance threshold for full fragment content; lower-scoring fragments are stubbed or dropped (lower = more context) |
|
|
82
|
+
| `--full` | false | Only the changed files, every fragment, no related-code context |
|
|
83
|
+
| `--timeout` | 300 | Wall-clock deadline in seconds; on expiry diffctx exits 124 instead of hanging |
|
|
84
|
+
|
|
85
|
+
Calibration of `--alpha`, `--tau`, and the edge-weight priors:
|
|
86
|
+
[`docs/parameter-strategy.md`](docs/parameter-strategy.md). Theory:
|
|
87
|
+
[Context-Selection for Git Diff (Zenodo, 2026)](https://doi.org/10.5281/zenodo.18824580).
|
|
88
|
+
|
|
89
|
+
### `graph` subcommand
|
|
90
|
+
|
|
91
|
+
Explore the underlying dependency graph directly, without a diff:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
diffctx graph . # Mermaid graph of directory deps (default)
|
|
95
|
+
diffctx graph . --summary # cycles, hotspots, coupling metrics
|
|
96
|
+
diffctx graph . --level fragment -f json # fragment-level graph as JSON
|
|
97
|
+
diffctx graph . --level file -f graphml -o g.xml # file-level graph as GraphML
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Usage
|
|
101
|
+
|
|
102
|
+
<!-- BEGIN USAGE -->
|
|
103
|
+
```bash
|
|
104
|
+
# full codebase export:
|
|
105
|
+
diffctx . # Markdown to stdout + token count
|
|
106
|
+
diffctx . -f md -c # Markdown → clipboard
|
|
107
|
+
diffctx . -f json -o tree.json # JSON → file
|
|
108
|
+
diffctx . --no-content # structure only, no file contents
|
|
109
|
+
diffctx . --max-depth 3 # limit depth
|
|
110
|
+
diffctx . -i custom.ignore # custom ignore patterns
|
|
111
|
+
|
|
112
|
+
# diff context mode (requires git repo):
|
|
113
|
+
diffctx . --diff # uncommitted changes (working tree vs HEAD)
|
|
114
|
+
diffctx . --diff HEAD~1 # context for last commit
|
|
115
|
+
diffctx . --diff main..feature # context for feature branch
|
|
116
|
+
diffctx . --diff HEAD~1 --budget 30000 # limit to ~30k tokens
|
|
117
|
+
diffctx . --diff HEAD~1 -c # diff context to clipboard
|
|
118
|
+
```
|
|
119
|
+
<!-- END USAGE -->
|
|
120
|
+
|
|
121
|
+
Every run reports token count and size on stderr — `12,847 tokens
|
|
122
|
+
(o200k_base), 52.3 KB` (tiktoken, the GPT-4o tokenizer; `~`-prefixed
|
|
123
|
+
approximation above 1 MB). `-c/--copy` sends output to the clipboard via
|
|
124
|
+
`pbcopy` (macOS), `clip` (Windows), or `wl-copy`/`xclip`/`xsel` (Linux).
|
|
125
|
+
Unreadable files are replaced by placeholders such as `<binary file: N bytes>`,
|
|
126
|
+
`<file too large: N bytes>`, or `<unreadable content: not utf-8>`.
|
|
127
|
+
|
|
128
|
+
## Python API
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from pathlib import Path
|
|
132
|
+
from diffctx import build_diff_context, map_directory, to_json, to_markdown, to_text, to_yaml
|
|
133
|
+
|
|
134
|
+
ctx = build_diff_context(
|
|
135
|
+
Path("."),
|
|
136
|
+
"HEAD~1..HEAD",
|
|
137
|
+
budget_tokens=None, # None = auto; 0 = strict-zero floor (empty); -1 = uncapped; N = hard cap
|
|
138
|
+
alpha=0.6,
|
|
139
|
+
tau=0.12,
|
|
140
|
+
full=False,
|
|
141
|
+
scoring_mode="ego",
|
|
142
|
+
timeout=300,
|
|
143
|
+
)
|
|
144
|
+
print(to_markdown(ctx))
|
|
145
|
+
|
|
146
|
+
tree = map_directory(
|
|
147
|
+
".",
|
|
148
|
+
max_depth=None,
|
|
149
|
+
no_content=False,
|
|
150
|
+
max_file_bytes=None,
|
|
151
|
+
ignore_file=None,
|
|
152
|
+
no_default_ignores=False,
|
|
153
|
+
whitelist_file=None,
|
|
154
|
+
)
|
|
155
|
+
print(to_yaml(tree))
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## MCP server
|
|
159
|
+
|
|
160
|
+
diffctx includes an [MCP](https://modelcontextprotocol.io) server that lets AI
|
|
161
|
+
assistants (Claude Code, Cursor, Windsurf, etc.) call diff context analysis
|
|
162
|
+
automatically during code review. Install with `pip install 'diffctx[mcp]'`
|
|
163
|
+
and add to your MCP client config (e.g. `~/.claude/mcp.json` for Claude Code):
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"mcpServers": {
|
|
168
|
+
"diffctx": {
|
|
169
|
+
"command": "diffctx-mcp"
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The server exposes a `get_diff_context` tool that assistants call when
|
|
176
|
+
reviewing PRs, explaining changes, or investigating broken tests. Configs for
|
|
177
|
+
Cursor, Continue, Windsurf, and Zed:
|
|
178
|
+
[`src/diffctx/mcp/README.md`](src/diffctx/mcp/README.md).
|
|
179
|
+
|
|
180
|
+
## Ignore patterns
|
|
181
|
+
|
|
182
|
+
Respects `.gitignore` and `.diffctx/ignore` automatically — hierarchically at
|
|
183
|
+
every directory level, with gitignore semantics (negation `!important.log`,
|
|
184
|
+
anchored `/root_only.txt`). `.diffctx/whitelist` acts as an include-only
|
|
185
|
+
filter, and the output file is always auto-ignored. `--no-default-ignores`
|
|
186
|
+
disables the built-in patterns; `--no-ignores` disables all ignore rules
|
|
187
|
+
(tree mode only).
|
|
188
|
+
|
|
189
|
+
## Exit codes
|
|
190
|
+
|
|
191
|
+
| Code | Meaning |
|
|
192
|
+
|------|---------|
|
|
193
|
+
| `0` | Success — output contains content |
|
|
194
|
+
| `1` | Runtime error (bad path, permission denied, etc.) |
|
|
195
|
+
| `2` | Usage error (invalid flags/arguments) |
|
|
196
|
+
| `3` | Environment error (`--diff` outside a git repo, git not installed, no commits yet) |
|
|
197
|
+
| `4` | `--diff` produced no semantic context (clean tree, binary-only, everything filtered); output is still emitted. Deletion/rename-only diffs list `deleted_files`/`renamed_files` and exit `0` |
|
|
198
|
+
| `130`| Interrupted (Ctrl-C) |
|
|
199
|
+
| `141`| Broken pipe (e.g. piping into `head`) |
|
|
200
|
+
|
|
201
|
+
## License
|
|
202
|
+
|
|
203
|
+
Apache 2.0
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
- [Changelog](CHANGELOG.md)
|
|
208
|
+
- [Security policy](SECURITY.md) — threat model and vulnerability reporting
|
|
209
|
+
- [Parameter strategy](docs/parameter-strategy.md) — how `--alpha`,
|
|
210
|
+
`--tau`, and edge weights are calibrated
|
|
@@ -27,7 +27,7 @@ struct Cli {
|
|
|
27
27
|
#[arg(long, default_value_t = DEFAULT_BUDGET_TOKENS)]
|
|
28
28
|
budget: u32,
|
|
29
29
|
|
|
30
|
-
#[arg(long, default_value = "yaml")]
|
|
30
|
+
#[arg(long, default_value = "yaml", value_parser = ["yaml", "json"])]
|
|
31
31
|
format: String,
|
|
32
32
|
|
|
33
33
|
#[arg(long = "diff")]
|
|
@@ -113,10 +113,13 @@ fn main() -> Result<()> {
|
|
|
113
113
|
let json = serde_json::to_string_pretty(&output)?;
|
|
114
114
|
println!("{}", json);
|
|
115
115
|
}
|
|
116
|
-
|
|
116
|
+
"yaml" => {
|
|
117
117
|
let yaml = serde_yaml::to_string(&output)?;
|
|
118
118
|
print!("{}", yaml);
|
|
119
119
|
}
|
|
120
|
+
other => {
|
|
121
|
+
anyhow::bail!("diffctx: unsupported --format '{other}' (native binary: yaml, json)")
|
|
122
|
+
}
|
|
120
123
|
}
|
|
121
124
|
|
|
122
125
|
Ok(())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.12.1"
|
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
use std::collections::BTreeMap;
|
|
2
|
-
use std::path::Path;
|
|
3
|
-
use std::sync::OnceLock;
|
|
4
|
-
|
|
5
|
-
use serde::Deserialize;
|
|
6
|
-
|
|
7
|
-
use _diffctx::render::{DiffContextOutput, FragmentEntry};
|
|
8
|
-
|
|
9
|
-
#[derive(Debug, Deserialize, Clone, Default)]
|
|
10
|
-
pub struct TestCase {
|
|
11
|
-
#[serde(default)]
|
|
12
|
-
pub name: String,
|
|
13
|
-
#[serde(default)]
|
|
14
|
-
pub tags: Vec<String>,
|
|
15
|
-
#[serde(default)]
|
|
16
|
-
pub repo: Repo,
|
|
17
|
-
#[serde(default)]
|
|
18
|
-
pub fixtures: Fixtures,
|
|
19
|
-
#[serde(default)]
|
|
20
|
-
pub fragments: Vec<DeclaredFragment>,
|
|
21
|
-
#[serde(default)]
|
|
22
|
-
pub oracle: Oracle,
|
|
23
|
-
#[serde(default)]
|
|
24
|
-
pub accept: Accept,
|
|
25
|
-
#[serde(default)]
|
|
26
|
-
pub xfail: Option<XFail>,
|
|
27
|
-
#[serde(default)]
|
|
28
|
-
pub min_score: Option<f64>,
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
#[derive(Debug, Deserialize, Clone, Default)]
|
|
32
|
-
pub struct Repo {
|
|
33
|
-
#[serde(default)]
|
|
34
|
-
pub initial_files: BTreeMap<String, String>,
|
|
35
|
-
#[serde(default)]
|
|
36
|
-
pub changed_files: BTreeMap<String, String>,
|
|
37
|
-
#[serde(default = "default_commit_message")]
|
|
38
|
-
pub commit_message: String,
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
#[derive(Debug, Deserialize, Clone, Default)]
|
|
42
|
-
pub struct Fixtures {
|
|
43
|
-
#[serde(default)]
|
|
44
|
-
pub auto_garbage: bool,
|
|
45
|
-
#[serde(default)]
|
|
46
|
-
pub distractors: BTreeMap<String, String>,
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
#[derive(Debug, Deserialize, Clone)]
|
|
50
|
-
pub struct DeclaredFragment {
|
|
51
|
-
pub id: String,
|
|
52
|
-
#[serde(default)]
|
|
53
|
-
pub selector: Selector,
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
#[derive(Debug, Deserialize, Clone, Default)]
|
|
57
|
-
pub struct Selector {
|
|
58
|
-
pub path: Option<String>,
|
|
59
|
-
pub symbol: Option<String>,
|
|
60
|
-
pub kind: Option<String>,
|
|
61
|
-
pub anchor: Option<String>,
|
|
62
|
-
pub any_of: Option<Vec<Selector>>,
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
#[derive(Debug, Deserialize, Clone, Default)]
|
|
66
|
-
pub struct Oracle {
|
|
67
|
-
#[serde(default)]
|
|
68
|
-
pub required: Vec<String>,
|
|
69
|
-
#[serde(default)]
|
|
70
|
-
pub allowed: Vec<String>,
|
|
71
|
-
#[serde(default)]
|
|
72
|
-
pub forbidden: Vec<String>,
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
#[derive(Debug, Deserialize, Clone)]
|
|
76
|
-
pub struct Accept {
|
|
77
|
-
#[serde(default = "default_symbol_match")]
|
|
78
|
-
pub symbol_match: String,
|
|
79
|
-
#[serde(default)]
|
|
80
|
-
pub kind_must_match: bool,
|
|
81
|
-
#[serde(default = "default_span_relation")]
|
|
82
|
-
pub span_relation: String,
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
impl Default for Accept {
|
|
86
|
-
fn default() -> Self {
|
|
87
|
-
Self {
|
|
88
|
-
symbol_match: default_symbol_match(),
|
|
89
|
-
kind_must_match: false,
|
|
90
|
-
span_relation: default_span_relation(),
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
#[derive(Debug, Deserialize, Clone)]
|
|
96
|
-
pub struct XFail {
|
|
97
|
-
#[serde(default)]
|
|
98
|
-
pub category: Option<String>,
|
|
99
|
-
#[serde(default)]
|
|
100
|
-
pub reason: Option<String>,
|
|
101
|
-
#[serde(default)]
|
|
102
|
-
pub issue: Option<String>,
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
impl XFail {
|
|
106
|
-
pub fn is_active(&self) -> bool {
|
|
107
|
-
self.reason.is_some() || self.category.is_some()
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
fn default_symbol_match() -> String {
|
|
112
|
-
"exact".into()
|
|
113
|
-
}
|
|
114
|
-
fn default_span_relation() -> String {
|
|
115
|
-
"exact_or_enclosing".into()
|
|
116
|
-
}
|
|
117
|
-
fn default_commit_message() -> String {
|
|
118
|
-
"Update files".into()
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
pub fn match_path(candidate: &str, target: &str) -> bool {
|
|
122
|
-
candidate == target
|
|
123
|
-
|| candidate.ends_with(&format!("/{target}"))
|
|
124
|
-
|| candidate
|
|
125
|
-
.replace('\\', "/")
|
|
126
|
-
.ends_with(&format!("/{target}"))
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
pub fn symbol_matches(frag_symbol: &str, expected: &str, mode: &str) -> bool {
|
|
130
|
-
match mode {
|
|
131
|
-
"prefix" => frag_symbol.starts_with(expected),
|
|
132
|
-
"substring" => frag_symbol.contains(expected),
|
|
133
|
-
_ => frag_symbol == expected,
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
pub fn matches_selector(fragment: &FragmentEntry, selector: &Selector, accept: &Accept) -> bool {
|
|
138
|
-
if let Some(any) = &selector.any_of {
|
|
139
|
-
return any.iter().any(|s| matches_selector(fragment, s, accept));
|
|
140
|
-
}
|
|
141
|
-
if let Some(path) = &selector.path {
|
|
142
|
-
if !match_path(&fragment.path, path) {
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
if let Some(symbol) = &selector.symbol {
|
|
147
|
-
let frag_symbol = fragment.symbol.as_deref().unwrap_or("");
|
|
148
|
-
if !symbol_matches(frag_symbol, symbol, &accept.symbol_match) {
|
|
149
|
-
return false;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
if let Some(kind) = &selector.kind {
|
|
153
|
-
if accept.kind_must_match && fragment.kind != *kind {
|
|
154
|
-
return false;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
if let Some(anchor) = &selector.anchor {
|
|
158
|
-
let content = fragment.content.as_deref().unwrap_or("");
|
|
159
|
-
if !content.contains(anchor) && !fragment.path.contains(anchor) {
|
|
160
|
-
return false;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
true
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
pub struct OracleResult {
|
|
167
|
-
pub passed: bool,
|
|
168
|
-
pub score: f64,
|
|
169
|
-
pub recall: f64,
|
|
170
|
-
pub forbidden_rate: f64,
|
|
171
|
-
pub missing_required: Vec<String>,
|
|
172
|
-
pub hit_forbidden: Vec<String>,
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
pub fn evaluate_oracle(case: &TestCase, output: &DiffContextOutput) -> OracleResult {
|
|
176
|
-
let mut matched_ids: std::collections::HashSet<&str> = std::collections::HashSet::new();
|
|
177
|
-
for entry in &output.fragments {
|
|
178
|
-
for decl in &case.fragments {
|
|
179
|
-
if matches_selector(entry, &decl.selector, &case.accept) {
|
|
180
|
-
matched_ids.insert(decl.id.as_str());
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
let required_total = case.oracle.required.len();
|
|
186
|
-
let missing_required: Vec<String> = case
|
|
187
|
-
.oracle
|
|
188
|
-
.required
|
|
189
|
-
.iter()
|
|
190
|
-
.filter(|id| !matched_ids.contains(id.as_str()))
|
|
191
|
-
.cloned()
|
|
192
|
-
.collect();
|
|
193
|
-
|
|
194
|
-
let forbidden_total = case.oracle.forbidden.len();
|
|
195
|
-
let hit_forbidden: Vec<String> = case
|
|
196
|
-
.oracle
|
|
197
|
-
.forbidden
|
|
198
|
-
.iter()
|
|
199
|
-
.filter(|id| matched_ids.contains(id.as_str()))
|
|
200
|
-
.cloned()
|
|
201
|
-
.collect();
|
|
202
|
-
|
|
203
|
-
let recall = if required_total == 0 {
|
|
204
|
-
1.0
|
|
205
|
-
} else {
|
|
206
|
-
(required_total - missing_required.len()) as f64 / required_total as f64
|
|
207
|
-
};
|
|
208
|
-
let forbidden_rate = if forbidden_total == 0 {
|
|
209
|
-
0.0
|
|
210
|
-
} else {
|
|
211
|
-
hit_forbidden.len() as f64 / forbidden_total as f64
|
|
212
|
-
};
|
|
213
|
-
let score = 100.0 * recall * (1.0 - forbidden_rate);
|
|
214
|
-
let passed = missing_required.is_empty() && hit_forbidden.is_empty();
|
|
215
|
-
|
|
216
|
-
OracleResult {
|
|
217
|
-
passed,
|
|
218
|
-
score,
|
|
219
|
-
recall,
|
|
220
|
-
forbidden_rate,
|
|
221
|
-
missing_required,
|
|
222
|
-
hit_forbidden,
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
pub fn calculate_budget(case: &TestCase) -> u32 {
|
|
227
|
-
let overhead = _diffctx::config::limits::LIMITS.overhead_per_fragment;
|
|
228
|
-
let mut all_files: BTreeMap<String, String> = case.repo.initial_files.clone();
|
|
229
|
-
for (k, v) in &case.repo.changed_files {
|
|
230
|
-
all_files.insert(k.clone(), v.clone());
|
|
231
|
-
}
|
|
232
|
-
for (k, v) in &case.fixtures.distractors {
|
|
233
|
-
all_files.insert(k.clone(), v.clone());
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
let content_tokens: u32 = all_files
|
|
237
|
-
.values()
|
|
238
|
-
.map(|c| _diffctx::tokenizer::count_tokens(c))
|
|
239
|
-
.sum();
|
|
240
|
-
let n_frags = all_files.len().max(2) as u32;
|
|
241
|
-
((content_tokens + n_frags * overhead) * 5 / 2).max(500)
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
static GARBAGE_CACHE: OnceLock<BTreeMap<String, String>> = OnceLock::new();
|
|
245
|
-
|
|
246
|
-
pub fn garbage_files() -> &'static BTreeMap<String, String> {
|
|
247
|
-
GARBAGE_CACHE.get_or_init(|| {
|
|
248
|
-
let mut m = BTreeMap::new();
|
|
249
|
-
let dir = Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
250
|
-
.join("tests")
|
|
251
|
-
.join("fixtures")
|
|
252
|
-
.join("garbage");
|
|
253
|
-
let entries = match std::fs::read_dir(&dir) {
|
|
254
|
-
Ok(e) => e,
|
|
255
|
-
Err(_) => return m,
|
|
256
|
-
};
|
|
257
|
-
for entry in entries.flatten() {
|
|
258
|
-
let path = entry.path();
|
|
259
|
-
if !path.is_file() {
|
|
260
|
-
continue;
|
|
261
|
-
}
|
|
262
|
-
let name = match path.file_name().and_then(|n| n.to_str()) {
|
|
263
|
-
Some(n) => n,
|
|
264
|
-
None => continue,
|
|
265
|
-
};
|
|
266
|
-
let content = match std::fs::read_to_string(&path) {
|
|
267
|
-
Ok(c) => c,
|
|
268
|
-
Err(_) => continue,
|
|
269
|
-
};
|
|
270
|
-
m.insert(format!("unrelated_dir/{name}"), content);
|
|
271
|
-
}
|
|
272
|
-
m
|
|
273
|
-
})
|
|
274
|
-
}
|