diffctx 1.16.0__tar.gz → 1.17.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.17.0}/CHANGELOG.md +77 -1
- {diffctx-1.16.0 → diffctx-1.17.0}/Cargo.lock +14 -7
- {diffctx-1.16.0 → diffctx-1.17.0}/PKG-INFO +15 -7
- {diffctx-1.16.0 → diffctx-1.17.0}/README.md +14 -6
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/Cargo.toml +2 -1
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/README.md +14 -6
- diffctx-1.17.0/crates/diffctx-native/src/candidate_files.rs +201 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/weights.rs +58 -18
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/base.rs +9 -1
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/mod.rs +45 -2
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/bazel.rs +11 -4
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/cargo_edges.rs +3 -3
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/go.rs +1 -1
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/javascript.rs +9 -1
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/perl.rs +1 -1
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/shell.rs +17 -3
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/terraform.rs +4 -4
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/structural/testing.rs +1 -1
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/effective_config.rs +3 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/fragmentation.rs +217 -97
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/git.rs +311 -73
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/in_memory_harness.rs +38 -3
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/languages.rs +116 -1
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/lib.rs +1 -1
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/locate.rs +45 -10
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/main.rs +135 -27
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/parsers/config_parser.rs +6 -15
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/parsers/tree_sitter_strategy.rs +75 -14
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/paths.rs +25 -3
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/pipeline.rs +255 -86
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/ppr.rs +4 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/project_graph.rs +7 -3
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/pybridge.rs +22 -10
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/render.rs +175 -3
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/resource.rs +45 -5
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/sanitize.rs +2 -2
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/select.rs +39 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/token_corpus.rs +5 -2
- {diffctx-1.16.0 → diffctx-1.17.0}/pyproject.toml +1 -1
- {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/_app.py +56 -11
- {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/_diffctx.pyi +5 -1
- {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/_native/graph_analytics.py +1 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/_native/pipeline.py +31 -33
- {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/cli.py +89 -36
- {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/ignore.py +15 -6
- {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/mcp/fetch.py +51 -39
- {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/mcp/legacy.py +14 -11
- {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/mcp/security.py +8 -3
- {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/mcp/server.py +11 -4
- {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/tree.py +41 -14
- diffctx-1.17.0/src/diffctx/version.py +1 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/writer.py +78 -21
- diffctx-1.16.0/crates/diffctx-native/src/candidate_files.rs +0 -134
- diffctx-1.16.0/src/diffctx/version.py +0 -1
- {diffctx-1.16.0 → diffctx-1.17.0}/Cargo.toml +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/LICENSE +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/analytics.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/change_class.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/analytics.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/bm25.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/budget.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/category_weights.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/edge_weights.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/env_overrides.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/extensions.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/filtering.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/fragmentation.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/git.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/graph_filtering.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/importance.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/limits.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/mod.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/mode.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/needs.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/parsers.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/render.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/scoring.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/selection.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/tokenization.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/core.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/discovery.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/config_edges/build_system.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/config_edges/cicd.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/config_edges/docker.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/config_edges/generic.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/config_edges/helm.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/config_edges/kubernetes.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/config_edges/mod.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/document/mod.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/history/cochange.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/history/mod.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/ansible.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/c_family.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/clojure.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/css.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/dart.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/dbt.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/dotnet.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/elixir.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/erlang.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/graphql.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/haskell.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/julia.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/jvm.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/latex.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/lua.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/mod.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/nim.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/nix.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/ocaml.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/openapi.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/php.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/prisma.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/protobuf.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/python.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/r_lang.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/ruby.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/rust_lang.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/sql.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/swift.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/tags.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/zig.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/similarity/lexical.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/similarity/mod.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/structural/containment.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/structural/mod.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/structural/sibling.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/excerpt.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/facts/mod.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/facts/scala.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/filtering.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/graph.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/graph_export.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/interval.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/mode.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/parsers/generic.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/parsers/markdown.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/parsers/mod.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/peak_rss.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/postpass.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/provenance.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/run_provenance.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/scoring.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/signatures.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/stopwords/code.txt +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/stopwords/docs.txt +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/stopwords.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/test_rng.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/testfiles.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/tokenizer.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/types.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/utility/boltzmann.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/utility/importance.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/utility/mod.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/utility/needs.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/utility/objective.rs +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/rust-toolchain.toml +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/__init__.py +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/__main__.py +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/_native/__init__.py +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/_native/graph_export.py +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/_native/project_graph.py +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/clipboard.py +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/logger.py +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/mcp/__init__.py +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/mcp/__main__.py +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/py.typed +0 -0
- {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/tokens.py +0 -0
|
@@ -7,6 +7,81 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.17.0] - 2026-09-23
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Windows and Intel-Mac are release targets.** Every test matrix runs on
|
|
15
|
+
Linux, macOS and Windows on x64 and ARM; wheels and binaries ship for
|
|
16
|
+
`x86_64-apple-darwin` and `aarch64-pc-windows-msvc` as well, and `npm
|
|
17
|
+
install diffctx` and the Scoop bucket select them. What that exposed is
|
|
18
|
+
fixed: `--output-file` on Windows, backslashes in MCP output and in the
|
|
19
|
+
Rust path matching (discovery, C#/Go/Cargo edges), datasets checked out
|
|
20
|
+
byte-exact under a CRLF `autocrlf`, and a `--timeout 0` that now always
|
|
21
|
+
reports the timeout instead of racing the subprocess.
|
|
22
|
+
- **The landing page installs and opens offline** — a web manifest, icons
|
|
23
|
+
and a service worker that keeps every visit network-first.
|
|
24
|
+
- **Free-threaded CPython gets a wheel.** Each platform ships a `cp314t`
|
|
25
|
+
wheel beside the `abi3` one; `uvx` on 3.14t no longer compiles the sdist
|
|
26
|
+
(which timed MCP clients out), and the extension keeps the GIL off.
|
|
27
|
+
- **The GitHub Action works on a default shallow checkout**: with no
|
|
28
|
+
`diff-range` it fetches the pull request's base and head (or `HEAD~1`)
|
|
29
|
+
itself, so `fetch-depth: 0` is no longer required.
|
|
30
|
+
- **`provenance.engine.build` names the release commit** in wheels, binaries
|
|
31
|
+
and the image.
|
|
32
|
+
- **A glossary line under the scoring modes** on the landing page: three
|
|
33
|
+
independent first-visit probes stalled on `ego-network`, `PPR`, `rrf`,
|
|
34
|
+
`pit`, `τ` and `o200k_base` with nothing on the page defining them.
|
|
35
|
+
|
|
36
|
+
### Removed
|
|
37
|
+
|
|
38
|
+
- **`build_diff_context(ignore_file=…, whitelist_file=…, no_default_ignores=…)`.**
|
|
39
|
+
The engine never applied a custom path-spec layer in diff mode and the three
|
|
40
|
+
arguments existed only to raise `NotImplementedError`; passing one is now a
|
|
41
|
+
`TypeError`. `.gitignore` and `.diffctx/ignore` still apply.
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- **The MCP server no longer hangs on Windows.** Every git child inherited the
|
|
46
|
+
server's stdin, which under an MCP client is the JSON-RPC pipe the client
|
|
47
|
+
holds open, and on Windows such a git never exited: any tool call that reached
|
|
48
|
+
git hung. Child processes now get a null stdin in both the engine and the
|
|
49
|
+
Python layer.
|
|
50
|
+
- **A long range says how many commits it held.** The commit list stops at
|
|
51
|
+
the newest twenty; the artifact read "20 commits" over a 42-commit range. It
|
|
52
|
+
now carries `commit_count` and reads "newest 20 of 42 commits" when the list
|
|
53
|
+
is cut, in both CLIs, pack and locate.
|
|
54
|
+
- **`--budget` fitting never passes context off as the change.** The witness
|
|
55
|
+
clip shortened any file's last fragment, so a context file shrank line by line
|
|
56
|
+
under a "more lines of this change" marker before it dropped; only a changed
|
|
57
|
+
fragment is shortened now.
|
|
58
|
+
- **MCP `fragment_ids` on a bare `diff_ref` read the code the change
|
|
59
|
+
replaced.** `HEAD` (or `HEAD~3`, or a duration window) is git's
|
|
60
|
+
base-versus-working-tree, so the locate ranking describes the files on
|
|
61
|
+
disk; the fetch read the base blob instead and returned the old body under
|
|
62
|
+
the ranking's own id. Bodies for a `..`-less ref now come from the working
|
|
63
|
+
tree, and a file the named revision lacks (or a revision git cannot
|
|
64
|
+
resolve) is reported as not found instead of being substituted with the
|
|
65
|
+
working-tree file under a historical label.
|
|
66
|
+
- **The native binary fits the written artifact to `--budget`** (#277).
|
|
67
|
+
npm, Scoop, Docker and the release archives ship the Rust binary, which
|
|
68
|
+
documented `--budget` as a cap on the whole artifact and enforced nothing on
|
|
69
|
+
the document it wrote: 9,121 tokens at `--budget 8000` on a real range. It
|
|
70
|
+
now runs the same render-count-drop loop as the Python CLI (context from the
|
|
71
|
+
tail first, a changed file's second fragment next, mechanical witnesses
|
|
72
|
+
before hand-written ones), records `selection_budget_exceeded` in
|
|
73
|
+
`coverage`, and exits 4 when nothing fits — the two CLIs deliver one set.
|
|
74
|
+
- **Eval harness: gold paths git had quoted** (`"b/caf\303\251.py"`) were
|
|
75
|
+
taken literally, so every non-ASCII gold file scored as a miss; the patch
|
|
76
|
+
header parser unquotes them. A checkpoint whose last line a kill left
|
|
77
|
+
unterminated no longer swallows the next appended row on resume.
|
|
78
|
+
|
|
79
|
+
### Changed
|
|
80
|
+
|
|
81
|
+
- The GitHub Action's `python-version` defaults to `3.13` (`--output-file`
|
|
82
|
+
needs `os.fchmod`, which Windows Python gained in 3.13); the action
|
|
83
|
+
reference page says so. `--budget --help` names what `auto` resolves to.
|
|
84
|
+
|
|
10
85
|
## [1.16.0] - 2026-09-16
|
|
11
86
|
|
|
12
87
|
### Added
|
|
@@ -1551,7 +1626,8 @@ Earlier releases shipped as `treemapper`; see
|
|
|
1551
1626
|
<https://github.com/nikolay-e/diffctx/releases> for the corresponding GitHub
|
|
1552
1627
|
release notes (`1.0.0` through `1.6.1`).
|
|
1553
1628
|
|
|
1554
|
-
[Unreleased]: https://github.com/nikolay-e/diffctx/compare/v1.
|
|
1629
|
+
[Unreleased]: https://github.com/nikolay-e/diffctx/compare/v1.17.0...HEAD
|
|
1630
|
+
[1.17.0]: https://github.com/nikolay-e/diffctx/compare/v1.16.0...v1.17.0
|
|
1555
1631
|
[1.16.0]: https://github.com/nikolay-e/diffctx/compare/v1.15.0...v1.16.0
|
|
1556
1632
|
[1.15.0]: https://github.com/nikolay-e/diffctx/compare/v1.14.0...v1.15.0
|
|
1557
1633
|
[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.17.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.17.0
|
|
4
4
|
Classifier: Development Status :: 5 - Production/Stable
|
|
5
5
|
Classifier: Environment :: Console
|
|
6
6
|
Classifier: Intended Audience :: Developers
|
|
@@ -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.
|
|
@@ -293,8 +300,9 @@ map a directory named `mcp` on older releases.
|
|
|
293
300
|
|
|
294
301
|
Respects `.gitignore` and `.diffctx/ignore` automatically — hierarchically at
|
|
295
302
|
every directory level, with full gitignore semantics (negation `!important.log`,
|
|
296
|
-
anchored `/root_only.txt`), and the output file is always
|
|
297
|
-
|
|
303
|
+
anchored `/root_only.txt`), and in tree mode the output file is always
|
|
304
|
+
auto-ignored (`--diff` reports a saved `tree.md` like any other change). Three
|
|
305
|
+
controls are tree mode only and are refused with `--diff` and `graph`: `.diffctx/whitelist`
|
|
298
306
|
(`-w`) as an include-only filter, `-i` for an extra ignore file, and
|
|
299
307
|
`--no-default-ignores` / `--no-ignores` to drop the built-in patterns or every
|
|
300
308
|
ignore rule.
|
|
@@ -318,8 +326,8 @@ its size (default 512 MB, `0` disables eviction).
|
|
|
318
326
|
|------|---------|
|
|
319
327
|
| `0` | Success — output contains content |
|
|
320
328
|
| `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) |
|
|
329
|
+
| `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`) |
|
|
330
|
+
| `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
331
|
| `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
332
|
| `124`| `--diff` ran 30 s past the `--timeout` deadline without stopping cooperatively (the deadline itself yields a partial artifact and exit 0) |
|
|
325
333
|
| `130`| Interrupted (Ctrl-C) |
|
|
@@ -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.
|
|
@@ -234,8 +241,9 @@ map a directory named `mcp` on older releases.
|
|
|
234
241
|
|
|
235
242
|
Respects `.gitignore` and `.diffctx/ignore` automatically — hierarchically at
|
|
236
243
|
every directory level, with full gitignore semantics (negation `!important.log`,
|
|
237
|
-
anchored `/root_only.txt`), and the output file is always
|
|
238
|
-
|
|
244
|
+
anchored `/root_only.txt`), and in tree mode the output file is always
|
|
245
|
+
auto-ignored (`--diff` reports a saved `tree.md` like any other change). Three
|
|
246
|
+
controls are tree mode only and are refused with `--diff` and `graph`: `.diffctx/whitelist`
|
|
239
247
|
(`-w`) as an include-only filter, `-i` for an extra ignore file, and
|
|
240
248
|
`--no-default-ignores` / `--no-ignores` to drop the built-in patterns or every
|
|
241
249
|
ignore rule.
|
|
@@ -259,8 +267,8 @@ its size (default 512 MB, `0` disables eviction).
|
|
|
259
267
|
|------|---------|
|
|
260
268
|
| `0` | Success — output contains content |
|
|
261
269
|
| `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) |
|
|
270
|
+
| `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`) |
|
|
271
|
+
| `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
272
|
| `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
273
|
| `124`| `--diff` ran 30 s past the `--timeout` deadline without stopping cooperatively (the deadline itself yields a partial artifact and exit 0) |
|
|
266
274
|
| `130`| Interrupted (Ctrl-C) |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "diffctx"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.17.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"
|
|
@@ -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.
|
|
@@ -234,8 +241,9 @@ map a directory named `mcp` on older releases.
|
|
|
234
241
|
|
|
235
242
|
Respects `.gitignore` and `.diffctx/ignore` automatically — hierarchically at
|
|
236
243
|
every directory level, with full gitignore semantics (negation `!important.log`,
|
|
237
|
-
anchored `/root_only.txt`), and the output file is always
|
|
238
|
-
|
|
244
|
+
anchored `/root_only.txt`), and in tree mode the output file is always
|
|
245
|
+
auto-ignored (`--diff` reports a saved `tree.md` like any other change). Three
|
|
246
|
+
controls are tree mode only and are refused with `--diff` and `graph`: `.diffctx/whitelist`
|
|
239
247
|
(`-w`) as an include-only filter, `-i` for an extra ignore file, and
|
|
240
248
|
`--no-default-ignores` / `--no-ignores` to drop the built-in patterns or every
|
|
241
249
|
ignore rule.
|
|
@@ -259,8 +267,8 @@ its size (default 512 MB, `0` disables eviction).
|
|
|
259
267
|
|------|---------|
|
|
260
268
|
| `0` | Success — output contains content |
|
|
261
269
|
| `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) |
|
|
270
|
+
| `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`) |
|
|
271
|
+
| `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
272
|
| `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
273
|
| `124`| `--diff` ran 30 s past the `--timeout` deadline without stopping cooperatively (the deadline itself yields a partial artifact and exit 0) |
|
|
266
274
|
| `130`| Interrupted (Ctrl-C) |
|
|
@@ -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
|
+
}
|
|
@@ -23,7 +23,6 @@ impl EdgeWeightConfig {
|
|
|
23
23
|
pub static EDGE_WEIGHTS: Lazy<FxHashMap<&'static str, EdgeWeightConfig>> = Lazy::new(|| {
|
|
24
24
|
let entries: &[(&str, EdgeWeightConfig)] = &[
|
|
25
25
|
("containment", EdgeWeightConfig::new(0.50, 0.70)),
|
|
26
|
-
("import", EdgeWeightConfig::new(0.50, 0.70)),
|
|
27
26
|
("test_direct", EdgeWeightConfig::new(0.60, 0.50)),
|
|
28
27
|
("test_naming", EdgeWeightConfig::new(0.50, 0.50)),
|
|
29
28
|
("test_reverse", EdgeWeightConfig::new(0.30, 1.0)),
|
|
@@ -33,12 +32,6 @@ pub static EDGE_WEIGHTS: Lazy<FxHashMap<&'static str, EdgeWeightConfig>> = Lazy:
|
|
|
33
32
|
("doc_structure", EdgeWeightConfig::new(0.30, 0.83)),
|
|
34
33
|
("anchor_link", EdgeWeightConfig::new(0.55, 0.64)),
|
|
35
34
|
("citation", EdgeWeightConfig::new(0.25, 1.0)),
|
|
36
|
-
("python_call", EdgeWeightConfig::new(0.65, 0.70)),
|
|
37
|
-
("python_symbol", EdgeWeightConfig::new(0.70, 0.70)),
|
|
38
|
-
("python_type", EdgeWeightConfig::new(0.50, 0.70)),
|
|
39
|
-
("javascript_call", EdgeWeightConfig::new(0.70, 0.50)),
|
|
40
|
-
("javascript_symbol", EdgeWeightConfig::new(0.75, 0.50)),
|
|
41
|
-
("javascript_type", EdgeWeightConfig::new(0.65, 0.50)),
|
|
42
35
|
("go_import", EdgeWeightConfig::new(0.70, 0.40)),
|
|
43
36
|
("go_type", EdgeWeightConfig::new(0.65, 0.40)),
|
|
44
37
|
("go_func", EdgeWeightConfig::new(0.60, 0.40)),
|
|
@@ -68,20 +61,16 @@ pub static EDGE_WEIGHTS: Lazy<FxHashMap<&'static str, EdgeWeightConfig>> = Lazy:
|
|
|
68
61
|
("ruby_require", EdgeWeightConfig::new(0.65, 0.40)),
|
|
69
62
|
("ruby_include", EdgeWeightConfig::new(0.60, 0.40)),
|
|
70
63
|
("ruby_const", EdgeWeightConfig::new(0.55, 0.40)),
|
|
71
|
-
("ruby_same_dir", EdgeWeightConfig::new(0.05, 0.40)),
|
|
72
64
|
("php_use", EdgeWeightConfig::new(0.65, 0.40)),
|
|
73
65
|
("php_require", EdgeWeightConfig::new(0.60, 0.40)),
|
|
74
66
|
("php_inheritance", EdgeWeightConfig::new(0.75, 0.40)),
|
|
75
67
|
("php_type", EdgeWeightConfig::new(0.55, 0.40)),
|
|
76
|
-
("php_same_namespace", EdgeWeightConfig::new(0.05, 0.40)),
|
|
77
68
|
("shell_source", EdgeWeightConfig::new(0.60, 0.35)),
|
|
78
69
|
("shell_script", EdgeWeightConfig::new(0.50, 0.35)),
|
|
79
70
|
("swift_import", EdgeWeightConfig::new(0.65, 0.40)),
|
|
80
71
|
("swift_conformance", EdgeWeightConfig::new(0.70, 0.40)),
|
|
81
72
|
("swift_extension", EdgeWeightConfig::new(0.65, 0.40)),
|
|
82
73
|
("swift_type", EdgeWeightConfig::new(0.60, 0.40)),
|
|
83
|
-
("swift_func", EdgeWeightConfig::new(0.55, 0.40)),
|
|
84
|
-
("swift_same_module", EdgeWeightConfig::new(0.05, 0.40)),
|
|
85
74
|
("zig_import", EdgeWeightConfig::new(0.65, 0.40)),
|
|
86
75
|
("zig_type", EdgeWeightConfig::new(0.60, 0.40)),
|
|
87
76
|
("zig_fn", EdgeWeightConfig::new(0.55, 0.40)),
|
|
@@ -95,20 +84,16 @@ pub static EDGE_WEIGHTS: Lazy<FxHashMap<&'static str, EdgeWeightConfig>> = Lazy:
|
|
|
95
84
|
("proto_import", EdgeWeightConfig::new(0.65, 0.40)),
|
|
96
85
|
("proto_message_ref", EdgeWeightConfig::new(0.60, 0.40)),
|
|
97
86
|
("proto_service_rpc", EdgeWeightConfig::new(0.55, 0.40)),
|
|
98
|
-
("graphql_import", EdgeWeightConfig::new(0.60, 0.40)),
|
|
99
87
|
("graphql_type_ref", EdgeWeightConfig::new(0.65, 0.40)),
|
|
100
88
|
("graphql_extend", EdgeWeightConfig::new(0.55, 0.40)),
|
|
101
89
|
("sql_fk", EdgeWeightConfig::new(0.70, 0.40)),
|
|
102
90
|
("sql_table_ref", EdgeWeightConfig::new(0.60, 0.40)),
|
|
103
|
-
("sql_view_source", EdgeWeightConfig::new(0.65, 0.40)),
|
|
104
|
-
("sql_migration", EdgeWeightConfig::new(0.55, 0.40)),
|
|
105
91
|
("css_import", EdgeWeightConfig::new(0.55, 0.40)),
|
|
106
92
|
("lua_require", EdgeWeightConfig::new(0.65, 0.40)),
|
|
107
93
|
("lua_fn", EdgeWeightConfig::new(0.55, 0.40)),
|
|
108
94
|
("lua_method", EdgeWeightConfig::new(0.50, 0.40)),
|
|
109
95
|
("openapi_internal_ref", EdgeWeightConfig::new(0.65, 0.40)),
|
|
110
96
|
("openapi_external_ref", EdgeWeightConfig::new(0.60, 0.40)),
|
|
111
|
-
("openapi_schema_ref", EdgeWeightConfig::new(0.55, 0.40)),
|
|
112
97
|
("erlang_include", EdgeWeightConfig::new(0.65, 0.40)),
|
|
113
98
|
("erlang_behaviour", EdgeWeightConfig::new(0.60, 0.40)),
|
|
114
99
|
("erlang_call", EdgeWeightConfig::new(0.55, 0.40)),
|
|
@@ -117,7 +102,6 @@ pub static EDGE_WEIGHTS: Lazy<FxHashMap<&'static str, EdgeWeightConfig>> = Lazy:
|
|
|
117
102
|
("elixir_behaviour", EdgeWeightConfig::new(0.55, 0.40)),
|
|
118
103
|
("elixir_fn", EdgeWeightConfig::new(0.50, 0.40)),
|
|
119
104
|
("dart_import", EdgeWeightConfig::new(0.65, 0.40)),
|
|
120
|
-
("dart_export", EdgeWeightConfig::new(0.60, 0.40)),
|
|
121
105
|
("dart_type", EdgeWeightConfig::new(0.60, 0.40)),
|
|
122
106
|
("dart_fn", EdgeWeightConfig::new(0.55, 0.40)),
|
|
123
107
|
("dart_inheritance", EdgeWeightConfig::new(0.70, 0.40)),
|
|
@@ -129,11 +113,9 @@ pub static EDGE_WEIGHTS: Lazy<FxHashMap<&'static str, EdgeWeightConfig>> = Lazy:
|
|
|
129
113
|
("ocaml_fn", EdgeWeightConfig::new(0.55, 0.40)),
|
|
130
114
|
("ocaml_module_ref", EdgeWeightConfig::new(0.60, 0.40)),
|
|
131
115
|
("r_source", EdgeWeightConfig::new(0.60, 0.40)),
|
|
132
|
-
("r_library", EdgeWeightConfig::new(0.55, 0.40)),
|
|
133
116
|
("r_fn", EdgeWeightConfig::new(0.50, 0.40)),
|
|
134
117
|
("r_s4", EdgeWeightConfig::new(0.55, 0.40)),
|
|
135
118
|
("perl_use", EdgeWeightConfig::new(0.65, 0.40)),
|
|
136
|
-
("perl_require", EdgeWeightConfig::new(0.60, 0.40)),
|
|
137
119
|
("perl_fn", EdgeWeightConfig::new(0.55, 0.40)),
|
|
138
120
|
("perl_method", EdgeWeightConfig::new(0.50, 0.40)),
|
|
139
121
|
("perl_inheritance", EdgeWeightConfig::new(0.65, 0.40)),
|
|
@@ -220,3 +202,61 @@ pub static LANG_WEIGHTS: Lazy<FxHashMap<&'static str, LangWeights>> = Lazy::new(
|
|
|
220
202
|
];
|
|
221
203
|
entries.iter().copied().collect()
|
|
222
204
|
});
|
|
205
|
+
|
|
206
|
+
#[cfg(test)]
|
|
207
|
+
mod tests {
|
|
208
|
+
use super::*;
|
|
209
|
+
use std::collections::BTreeSet;
|
|
210
|
+
use std::path::{Path, PathBuf};
|
|
211
|
+
|
|
212
|
+
fn collect_rs_files(dir: &Path, out: &mut Vec<PathBuf>) {
|
|
213
|
+
for entry in std::fs::read_dir(dir).unwrap().flatten() {
|
|
214
|
+
let path = entry.path();
|
|
215
|
+
if path.is_dir() {
|
|
216
|
+
collect_rs_files(&path, out);
|
|
217
|
+
} else if path.extension().is_some_and(|e| e == "rs") {
|
|
218
|
+
out.push(path);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/// A weight nobody reads is a model parameter that does not exist: cases
|
|
224
|
+
/// named after such a channel pass without it, and tuning it changes
|
|
225
|
+
/// nothing. Eighteen keys sat in this table unread until 2026-09-22.
|
|
226
|
+
#[test]
|
|
227
|
+
fn every_edge_weight_key_is_read_somewhere_outside_this_table() {
|
|
228
|
+
let re = regex::Regex::new(r#"EDGE_WEIGHTS\["([a-z0-9_]+)"\]"#).unwrap();
|
|
229
|
+
let mut files = Vec::new();
|
|
230
|
+
collect_rs_files(
|
|
231
|
+
&PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src"),
|
|
232
|
+
&mut files,
|
|
233
|
+
);
|
|
234
|
+
let mut read: BTreeSet<String> = BTreeSet::new();
|
|
235
|
+
for file in files {
|
|
236
|
+
if file.ends_with("weights.rs") {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
let text = std::fs::read_to_string(&file).unwrap();
|
|
240
|
+
read.extend(re.captures_iter(&text).map(|c| c[1].to_string()));
|
|
241
|
+
}
|
|
242
|
+
let unread: Vec<&str> = EDGE_WEIGHTS
|
|
243
|
+
.keys()
|
|
244
|
+
.copied()
|
|
245
|
+
.filter(|k| !read.contains(*k))
|
|
246
|
+
.collect::<BTreeSet<_>>()
|
|
247
|
+
.into_iter()
|
|
248
|
+
.collect();
|
|
249
|
+
assert!(
|
|
250
|
+
unread.is_empty(),
|
|
251
|
+
"EDGE_WEIGHTS keys no builder reads: {unread:?}"
|
|
252
|
+
);
|
|
253
|
+
let unknown: Vec<&String> = read
|
|
254
|
+
.iter()
|
|
255
|
+
.filter(|k| !EDGE_WEIGHTS.contains_key(k.as_str()))
|
|
256
|
+
.collect();
|
|
257
|
+
assert!(
|
|
258
|
+
unknown.is_empty(),
|
|
259
|
+
"EDGE_WEIGHTS[...] lookups with no entry (would panic at runtime): {unknown:?}"
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}
|