diffctx 1.17.0__tar.gz → 1.18.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.17.0 → diffctx-1.18.1}/CHANGELOG.md +93 -1
- {diffctx-1.17.0 → diffctx-1.18.1}/Cargo.lock +1 -1
- {diffctx-1.17.0 → diffctx-1.18.1}/PKG-INFO +10 -7
- {diffctx-1.17.0 → diffctx-1.18.1}/README.md +6 -3
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/Cargo.toml +2 -2
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/README.md +6 -3
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/discovery.rs +126 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/rust_lang.rs +13 -4
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/effective_config.rs +2 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/graph.rs +33 -0
- diffctx-1.18.1/crates/diffctx-native/src/hook.rs +803 -0
- diffctx-1.18.1/crates/diffctx-native/src/impact.rs +937 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/in_memory_harness.rs +2 -1
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/lib.rs +2 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/locate.rs +36 -21
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/main.rs +101 -4
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/pipeline.rs +99 -11
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/pybridge.rs +65 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/render.rs +9 -1
- {diffctx-1.17.0 → diffctx-1.18.1}/pyproject.toml +3 -3
- {diffctx-1.17.0 → diffctx-1.18.1}/src/diffctx/_app.py +38 -4
- {diffctx-1.17.0 → diffctx-1.18.1}/src/diffctx/_diffctx.pyi +9 -0
- diffctx-1.18.1/src/diffctx/_native/__init__.py +7 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/src/diffctx/_native/pipeline.py +24 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/src/diffctx/cli.py +13 -2
- {diffctx-1.17.0 → diffctx-1.18.1}/src/diffctx/mcp/legacy.py +8 -10
- {diffctx-1.17.0 → diffctx-1.18.1}/src/diffctx/mcp/server.py +137 -35
- diffctx-1.18.1/src/diffctx/version.py +1 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/src/diffctx/writer.py +29 -8
- diffctx-1.17.0/src/diffctx/_native/__init__.py +0 -7
- diffctx-1.17.0/src/diffctx/version.py +0 -1
- {diffctx-1.17.0 → diffctx-1.18.1}/Cargo.toml +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/LICENSE +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/analytics.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/candidate_files.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/change_class.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/analytics.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/bm25.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/budget.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/category_weights.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/edge_weights.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/env_overrides.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/extensions.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/filtering.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/fragmentation.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/git.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/graph_filtering.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/importance.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/limits.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/mod.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/mode.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/needs.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/parsers.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/render.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/scoring.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/selection.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/tokenization.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/config/weights.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/core.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/base.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/config_edges/build_system.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/config_edges/cicd.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/config_edges/docker.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/config_edges/generic.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/config_edges/helm.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/config_edges/kubernetes.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/config_edges/mod.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/document/mod.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/history/cochange.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/history/mod.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/mod.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/ansible.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/bazel.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/c_family.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/cargo_edges.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/clojure.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/css.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/dart.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/dbt.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/dotnet.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/elixir.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/erlang.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/go.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/graphql.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/haskell.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/javascript.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/julia.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/jvm.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/latex.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/lua.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/mod.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/nim.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/nix.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/ocaml.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/openapi.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/perl.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/php.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/prisma.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/protobuf.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/python.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/r_lang.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/ruby.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/shell.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/sql.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/swift.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/tags.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/terraform.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/semantic/zig.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/similarity/lexical.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/similarity/mod.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/structural/containment.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/structural/mod.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/structural/sibling.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/edges/structural/testing.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/excerpt.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/facts/mod.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/facts/scala.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/filtering.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/fragmentation.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/git.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/graph_export.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/interval.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/languages.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/mode.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/parsers/config_parser.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/parsers/generic.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/parsers/markdown.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/parsers/mod.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/parsers/tree_sitter_strategy.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/paths.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/peak_rss.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/postpass.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/ppr.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/project_graph.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/provenance.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/resource.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/run_provenance.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/sanitize.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/scoring.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/select.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/signatures.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/stopwords/code.txt +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/stopwords/docs.txt +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/stopwords.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/test_rng.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/testfiles.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/token_corpus.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/tokenizer.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/types.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/utility/boltzmann.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/utility/importance.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/utility/mod.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/utility/needs.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/crates/diffctx-native/src/utility/objective.rs +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/rust-toolchain.toml +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/src/diffctx/__init__.py +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/src/diffctx/__main__.py +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/src/diffctx/_native/graph_analytics.py +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/src/diffctx/_native/graph_export.py +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/src/diffctx/_native/project_graph.py +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/src/diffctx/clipboard.py +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/src/diffctx/ignore.py +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/src/diffctx/logger.py +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/src/diffctx/mcp/__init__.py +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/src/diffctx/mcp/__main__.py +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/src/diffctx/mcp/fetch.py +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/src/diffctx/mcp/security.py +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/src/diffctx/py.typed +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/src/diffctx/tokens.py +0 -0
- {diffctx-1.17.0 → diffctx-1.18.1}/src/diffctx/tree.py +0 -0
|
@@ -7,10 +7,100 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.18.1] - 2026-09-30
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **The plugin's impact hook never ran under macOS `/bin/bash` 3.2**: an empty
|
|
15
|
+
`gate=()` expanded under `set -u` is an unbound variable there, the pipeline
|
|
16
|
+
died right of the pipe and `|| true` hid it. One never-empty argument array.
|
|
17
|
+
- Hook command parsing: `git add … && git commit` reviews the working tree
|
|
18
|
+
(the add has not run when the hook sees the line — it reviewed the stale
|
|
19
|
+
index and was silent on the commonest agent pattern); `-m a -m b`, `-F file`
|
|
20
|
+
and other value flags no longer count as pathspecs; `git diff A B` and
|
|
21
|
+
`git diff main` are history, not the pending change; `cd dir && git …` and
|
|
22
|
+
a relative `-C` resolve against the agent's cwd.
|
|
23
|
+
- Impact: a test guards a caller only when it names the symbol *and* reaches
|
|
24
|
+
the caller's module (a Rust test mentioning `render` was the guard of a
|
|
25
|
+
Python `render()`, #312); a chunk caller named `main[88]` is looked up as
|
|
26
|
+
`main`; two-letter modules (`db.py`) keep their callers; `*out = f()` and
|
|
27
|
+
`#[derive]` lines are code, not comments; a wide changed-file list folds
|
|
28
|
+
into a count before a single caller is dropped, and a symbol keeps its
|
|
29
|
+
strongest caller; a run that hit a limit says `(partial: …)` and is never
|
|
30
|
+
`empty`; schema contracts are listed before public symbols with a `+N more`
|
|
31
|
+
count; SHA-256 repositories count commits.
|
|
32
|
+
- `diffctx.impact.v1` and `diffctx.locate.v1` required every field the
|
|
33
|
+
serializer may omit (`truncated`, `commit_count`, `deleted_files`, …), so no
|
|
34
|
+
real document validated; the schemas now require what is always present,
|
|
35
|
+
and a test validates real impact and locate output against them.
|
|
36
|
+
- MCP: the default `diff_ref` on a clean single-commit repository is the
|
|
37
|
+
working tree, not an error naming a range the caller never passed; the
|
|
38
|
+
`git status` guess runs off the event loop with a 10 s cap and never leaks
|
|
39
|
+
the command line into the response.
|
|
40
|
+
- CLI: `--mode impact -o out.yaml` is refused like `-f yaml` instead of
|
|
41
|
+
writing JSON silently; `--mode` help names the text default.
|
|
42
|
+
- Plugin installer: the download is unpacked beside its target so the final
|
|
43
|
+
`mv` is a rename, never a copy another session could execute half-written.
|
|
44
|
+
- MCP `mode=impact` with `paths` no longer marks the whole change reviewed.
|
|
45
|
+
- Landing page: the four-part iOS viewport contract (`viewport-fit=cover`,
|
|
46
|
+
status-bar style, safe-area insets, taller `html`).
|
|
47
|
+
- Privacy page names the release-binary download the plugin makes.
|
|
48
|
+
- `bitcheck.sh` ranges re-pinned on the rewritten history; the old anchors
|
|
49
|
+
kept the pre-rewrite tree, with third-party tokens in retired benchmark
|
|
50
|
+
diffs, reachable on both forges.
|
|
51
|
+
- Runtime image: one package layer (Sonar `docker:S7031`/`S6595`); PyJWT
|
|
52
|
+
2.15.1 in `uv.lock` (GHSA-w6j9-cwv2-h6wq, transitive).
|
|
53
|
+
|
|
54
|
+
## [1.18.0] - 2026-09-29
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
|
|
58
|
+
- **`--mode impact`: what a change reaches outside its own diff, under 2k
|
|
59
|
+
tokens.** For each changed symbol, the callers in files the diff does not
|
|
60
|
+
touch, whether a test guards each one, and which symbols more than one
|
|
61
|
+
commit of the range edited; public API and schema changes are listed as
|
|
62
|
+
facts. It walks the graph the other modes already build, with a discovery
|
|
63
|
+
pass that follows references *into* the changed files (the other modes
|
|
64
|
+
only follow what the changed files import), and filters name-only links
|
|
65
|
+
the way a reader would: the caller must name the symbol in code and
|
|
66
|
+
reference its module. An empty impact is an answer, not an error. CLI
|
|
67
|
+
(`-f md` is the text form), MCP `mode="impact"`, the GitHub Action's
|
|
68
|
+
`mode` input, and `schemas/diffctx.impact.v1.json`.
|
|
69
|
+
- **`diffctx hook pretooluse` / `posttooluse`: the Claude Code plugin hands
|
|
70
|
+
the agent a change's impact without asking it to call anything.** Eight
|
|
71
|
+
agent sessions with the tool installed made zero unprompted calls (#289).
|
|
72
|
+
Before a commit, merge, cherry-pick, push or pull request the impact of
|
|
73
|
+
what that command records is injected as context — a plain `git commit`
|
|
74
|
+
reviews the index, `-a` the working tree, a merge the branch since its
|
|
75
|
+
merge base; after a `git diff` or `git status` the agent ran itself, the
|
|
76
|
+
pending change's impact rides on the answer it asked for. Once per change
|
|
77
|
+
(content-keyed with `git patch-id`, so a manual `--mode impact` run
|
|
78
|
+
counts), silent when nothing outside the diff depends on the change, and
|
|
79
|
+
never blocking: a range that runs out of the 30 s deadline is marked
|
|
80
|
+
reviewed rather than retried on every push. `impact_gate` (off by default)
|
|
81
|
+
denies a commit until its impact was shown once. At session start the
|
|
82
|
+
plugin fetches the release binary for the platform, verified against
|
|
83
|
+
`checksums.json`, into its data directory, backs off for six hours after a
|
|
84
|
+
failed download, and puts the one-line recipe into the session's context;
|
|
85
|
+
`impact_hook` turns all of it off (#310).
|
|
86
|
+
- **`/diffctx:commit`**: the impact first, then the commit.
|
|
87
|
+
- **`diffctx_context` is always loaded** under Claude Code's tool search
|
|
88
|
+
(`_meta.anthropic/alwaysLoad`), and its description names `impact` as the
|
|
89
|
+
pre-commit call; both skills default to the server's own range choice.
|
|
90
|
+
- **The PR review comment opens with the impact view.**
|
|
91
|
+
- **Plugin eval `breaking-caller`** measures the number under the goal: with
|
|
92
|
+
a contract change uncommitted and two callers about to break, does the
|
|
93
|
+
agent handle or name them before committing.
|
|
94
|
+
|
|
10
95
|
## [1.17.0] - 2026-09-23
|
|
11
96
|
|
|
12
97
|
### Added
|
|
13
98
|
|
|
99
|
+
- **The site lives at <https://diffctx.com>.** It was a 301 to
|
|
100
|
+
`nikolay-e.github.io/diffctx/`, so search engines indexed the github.io
|
|
101
|
+
URL and the product domain had no presence of its own. It is now the
|
|
102
|
+
GitHub Pages custom domain (github.io redirects to it), with a
|
|
103
|
+
`robots.txt` and `sitemap.xml`; package metadata points at it.
|
|
14
104
|
- **Windows and Intel-Mac are release targets.** Every test matrix runs on
|
|
15
105
|
Linux, macOS and Windows on x64 and ARM; wheels and binaries ship for
|
|
16
106
|
`x86_64-apple-darwin` and `aarch64-pc-windows-msvc` as well, and `npm
|
|
@@ -1626,7 +1716,9 @@ Earlier releases shipped as `treemapper`; see
|
|
|
1626
1716
|
<https://github.com/nikolay-e/diffctx/releases> for the corresponding GitHub
|
|
1627
1717
|
release notes (`1.0.0` through `1.6.1`).
|
|
1628
1718
|
|
|
1629
|
-
[Unreleased]: https://github.com/nikolay-e/diffctx/compare/v1.
|
|
1719
|
+
[Unreleased]: https://github.com/nikolay-e/diffctx/compare/v1.18.1...HEAD
|
|
1720
|
+
[1.18.1]: https://github.com/nikolay-e/diffctx/compare/v1.18.0...v1.18.1
|
|
1721
|
+
[1.18.0]: https://github.com/nikolay-e/diffctx/compare/v1.17.0...v1.18.0
|
|
1630
1722
|
[1.17.0]: https://github.com/nikolay-e/diffctx/compare/v1.16.0...v1.17.0
|
|
1631
1723
|
[1.16.0]: https://github.com/nikolay-e/diffctx/compare/v1.15.0...v1.16.0
|
|
1632
1724
|
[1.15.0]: https://github.com/nikolay-e/diffctx/compare/v1.14.0...v1.15.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: diffctx
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.18.1
|
|
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
|
|
@@ -154,7 +154,7 @@ keeps its git meaning.
|
|
|
154
154
|
| `--full` | false | Only the changed files, every fragment, no related-code context |
|
|
155
155
|
| `--timeout` | 300 | Wall-clock deadline in seconds; on expiry the run stops cooperatively and emits a partial artifact whose `coverage` block names the limit (exit 0). 124 is the watchdog behind it, 30 s later, for a phase that could not stop |
|
|
156
156
|
| `--with-raw-diff` | false | Also embed git's raw unified diff ahead of the selected fragments — additive (selection unchanged), not charged to `--budget`, lock/ignored/secret-like sections omitted. Python CLI only |
|
|
157
|
-
| `--mode` | `pack` | `locate` emits the same ranked selection as compact `diffctx.locate.v1` JSON — path, lines, score, provenance reasons, a blast-radius `summary` and per-item impact `group` (`test`/`type`/`config`), NO source bodies. Adds a `coverage` block naming what the run could not see (`unparsed_files`, `zero_edge_files`, `ppr_truncated`, `next_up`, a heuristic `confidence`) and an `overflow` ranking of what the budget left behind — omitted entirely when there is nothing to disclose. `
|
|
157
|
+
| `--mode` | `pack` | `locate` emits the same ranked selection as compact `diffctx.locate.v1` JSON — path, lines, score, provenance reasons, a blast-radius `summary` and per-item impact `group` (`test`/`type`/`config`), NO source bodies. Adds a `coverage` block naming what the run could not see (`unparsed_files`, `zero_edge_files`, `ppr_truncated`, `next_up`, a heuristic `confidence`) and an `overflow` ranking of what the budget left behind — omitted entirely when there is nothing to disclose. `impact` answers, under 2k tokens, what the change reaches outside its diff: callers in files the diff does not touch, whether a test guards each, cross-commit overlap, and the public API and schema changes (text by default, `diffctx.impact.v1` JSON with `-f json`). The MCP tool takes both as `mode` |
|
|
158
158
|
|
|
159
159
|
Every JSON/YAML artifact opens with `schema: diffctx.context.v1` and validates
|
|
160
160
|
against [`schemas/diffctx.context.v1.json`](schemas/diffctx.context.v1.json),
|
|
@@ -248,7 +248,10 @@ as `io.github.nikolay-e/diffctx`. One-line setup (zero-install via
|
|
|
248
248
|
[uv](https://docs.astral.sh/uv/)):
|
|
249
249
|
|
|
250
250
|
```bash
|
|
251
|
-
# 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
|
|
252
255
|
claude mcp add diffctx -- uvx --from 'diffctx[mcp]' diffctx-mcp
|
|
253
256
|
# Codex CLI
|
|
254
257
|
codex mcp add diffctx -- uvx --from 'diffctx[mcp]' diffctx-mcp
|
|
@@ -343,7 +346,7 @@ Apache 2.0
|
|
|
343
346
|
|
|
344
347
|
---
|
|
345
348
|
|
|
346
|
-
- [Documentation site](https://
|
|
349
|
+
- [Documentation site](https://diffctx.com/) — the pipeline
|
|
347
350
|
end to end: diff → fragments → graph → relevance → selection
|
|
348
351
|
- [Command-line reference](docs/product/cli.md) — every flag with its default
|
|
349
352
|
and meaning, rendered from `diffctx --help`
|
|
@@ -95,7 +95,7 @@ keeps its git meaning.
|
|
|
95
95
|
| `--full` | false | Only the changed files, every fragment, no related-code context |
|
|
96
96
|
| `--timeout` | 300 | Wall-clock deadline in seconds; on expiry the run stops cooperatively and emits a partial artifact whose `coverage` block names the limit (exit 0). 124 is the watchdog behind it, 30 s later, for a phase that could not stop |
|
|
97
97
|
| `--with-raw-diff` | false | Also embed git's raw unified diff ahead of the selected fragments — additive (selection unchanged), not charged to `--budget`, lock/ignored/secret-like sections omitted. Python CLI only |
|
|
98
|
-
| `--mode` | `pack` | `locate` emits the same ranked selection as compact `diffctx.locate.v1` JSON — path, lines, score, provenance reasons, a blast-radius `summary` and per-item impact `group` (`test`/`type`/`config`), NO source bodies. Adds a `coverage` block naming what the run could not see (`unparsed_files`, `zero_edge_files`, `ppr_truncated`, `next_up`, a heuristic `confidence`) and an `overflow` ranking of what the budget left behind — omitted entirely when there is nothing to disclose. `
|
|
98
|
+
| `--mode` | `pack` | `locate` emits the same ranked selection as compact `diffctx.locate.v1` JSON — path, lines, score, provenance reasons, a blast-radius `summary` and per-item impact `group` (`test`/`type`/`config`), NO source bodies. Adds a `coverage` block naming what the run could not see (`unparsed_files`, `zero_edge_files`, `ppr_truncated`, `next_up`, a heuristic `confidence`) and an `overflow` ranking of what the budget left behind — omitted entirely when there is nothing to disclose. `impact` answers, under 2k tokens, what the change reaches outside its diff: callers in files the diff does not touch, whether a test guards each, cross-commit overlap, and the public API and schema changes (text by default, `diffctx.impact.v1` JSON with `-f json`). The MCP tool takes both as `mode` |
|
|
99
99
|
|
|
100
100
|
Every JSON/YAML artifact opens with `schema: diffctx.context.v1` and validates
|
|
101
101
|
against [`schemas/diffctx.context.v1.json`](schemas/diffctx.context.v1.json),
|
|
@@ -189,7 +189,10 @@ as `io.github.nikolay-e/diffctx`. One-line setup (zero-install via
|
|
|
189
189
|
[uv](https://docs.astral.sh/uv/)):
|
|
190
190
|
|
|
191
191
|
```bash
|
|
192
|
-
# 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
|
|
193
196
|
claude mcp add diffctx -- uvx --from 'diffctx[mcp]' diffctx-mcp
|
|
194
197
|
# Codex CLI
|
|
195
198
|
codex mcp add diffctx -- uvx --from 'diffctx[mcp]' diffctx-mcp
|
|
@@ -284,7 +287,7 @@ Apache 2.0
|
|
|
284
287
|
|
|
285
288
|
---
|
|
286
289
|
|
|
287
|
-
- [Documentation site](https://
|
|
290
|
+
- [Documentation site](https://diffctx.com/) — the pipeline
|
|
288
291
|
end to end: diff → fragments → graph → relevance → selection
|
|
289
292
|
- [Command-line reference](docs/product/cli.md) — every flag with its default
|
|
290
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.1"
|
|
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"]
|
|
@@ -95,7 +95,7 @@ keeps its git meaning.
|
|
|
95
95
|
| `--full` | false | Only the changed files, every fragment, no related-code context |
|
|
96
96
|
| `--timeout` | 300 | Wall-clock deadline in seconds; on expiry the run stops cooperatively and emits a partial artifact whose `coverage` block names the limit (exit 0). 124 is the watchdog behind it, 30 s later, for a phase that could not stop |
|
|
97
97
|
| `--with-raw-diff` | false | Also embed git's raw unified diff ahead of the selected fragments — additive (selection unchanged), not charged to `--budget`, lock/ignored/secret-like sections omitted. Python CLI only |
|
|
98
|
-
| `--mode` | `pack` | `locate` emits the same ranked selection as compact `diffctx.locate.v1` JSON — path, lines, score, provenance reasons, a blast-radius `summary` and per-item impact `group` (`test`/`type`/`config`), NO source bodies. Adds a `coverage` block naming what the run could not see (`unparsed_files`, `zero_edge_files`, `ppr_truncated`, `next_up`, a heuristic `confidence`) and an `overflow` ranking of what the budget left behind — omitted entirely when there is nothing to disclose. `
|
|
98
|
+
| `--mode` | `pack` | `locate` emits the same ranked selection as compact `diffctx.locate.v1` JSON — path, lines, score, provenance reasons, a blast-radius `summary` and per-item impact `group` (`test`/`type`/`config`), NO source bodies. Adds a `coverage` block naming what the run could not see (`unparsed_files`, `zero_edge_files`, `ppr_truncated`, `next_up`, a heuristic `confidence`) and an `overflow` ranking of what the budget left behind — omitted entirely when there is nothing to disclose. `impact` answers, under 2k tokens, what the change reaches outside its diff: callers in files the diff does not touch, whether a test guards each, cross-commit overlap, and the public API and schema changes (text by default, `diffctx.impact.v1` JSON with `-f json`). The MCP tool takes both as `mode` |
|
|
99
99
|
|
|
100
100
|
Every JSON/YAML artifact opens with `schema: diffctx.context.v1` and validates
|
|
101
101
|
against [`schemas/diffctx.context.v1.json`](schemas/diffctx.context.v1.json),
|
|
@@ -189,7 +189,10 @@ as `io.github.nikolay-e/diffctx`. One-line setup (zero-install via
|
|
|
189
189
|
[uv](https://docs.astral.sh/uv/)):
|
|
190
190
|
|
|
191
191
|
```bash
|
|
192
|
-
# 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
|
|
193
196
|
claude mcp add diffctx -- uvx --from 'diffctx[mcp]' diffctx-mcp
|
|
194
197
|
# Codex CLI
|
|
195
198
|
codex mcp add diffctx -- uvx --from 'diffctx[mcp]' diffctx-mcp
|
|
@@ -284,7 +287,7 @@ Apache 2.0
|
|
|
284
287
|
|
|
285
288
|
---
|
|
286
289
|
|
|
287
|
-
- [Documentation site](https://
|
|
290
|
+
- [Documentation site](https://diffctx.com/) — the pipeline
|
|
288
291
|
end to end: diff → fragments → graph → relevance → selection
|
|
289
292
|
- [Command-line reference](docs/product/cli.md) — every flag with its default
|
|
290
293
|
and meaning, rendered from `diffctx --help`
|
|
@@ -117,6 +117,132 @@ fn expand_by_rare_identifiers(ctx: &DiscoveryContext) -> Vec<PathBuf> {
|
|
|
117
117
|
result
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
/// Files that name what the change defines: the callers a change reaches
|
|
121
|
+
/// (#310). Forward discovery follows what the changed file imports; nothing
|
|
122
|
+
/// followed the other way, so a function with a hundred importers surfaced
|
|
123
|
+
/// one of them and the impact view called the rest silence. Only the
|
|
124
|
+
/// impact mode asks for this: widening the universe moves selection, and
|
|
125
|
+
/// the corpus operating point is calibrated without it.
|
|
126
|
+
pub struct ReverseReferenceDiscovery;
|
|
127
|
+
|
|
128
|
+
/// A name held by more candidate files than this is vocabulary, not a
|
|
129
|
+
/// reference, and following it would admit the repository.
|
|
130
|
+
const MAX_REVERSE_FILES_PER_NAME: usize = 64;
|
|
131
|
+
const MAX_REVERSE_FILES: usize = 256;
|
|
132
|
+
const MIN_REVERSE_NAME_LEN: usize = 4;
|
|
133
|
+
|
|
134
|
+
static DEFINITION_RE: OnceLock<regex::Regex> = OnceLock::new();
|
|
135
|
+
|
|
136
|
+
fn definition_names(content: &str) -> Vec<String> {
|
|
137
|
+
let re = DEFINITION_RE.get_or_init(|| {
|
|
138
|
+
regex::Regex::new(
|
|
139
|
+
r"(?m)^\s*(?:export\s+(?:default\s+)?|pub(?:\([^)]*\))?\s+|public\s+|static\s+|async\s+|abstract\s+|final\s+|data\s+|sealed\s+|open\s+)*(?:fn|def|class|struct|enum|interface|trait|type|function|const|let|var|val|object|impl|protocol|module|record)\s+([A-Za-z_][A-Za-z0-9_]*)",
|
|
140
|
+
)
|
|
141
|
+
.expect("definition regex")
|
|
142
|
+
});
|
|
143
|
+
re.captures_iter(content)
|
|
144
|
+
.filter_map(|c| c.get(1).map(|m| m.as_str().to_string()))
|
|
145
|
+
.filter(|n| n.len() >= MIN_REVERSE_NAME_LEN)
|
|
146
|
+
.collect()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
impl DiscoveryStrategy for ReverseReferenceDiscovery {
|
|
150
|
+
fn name(&self) -> &'static str {
|
|
151
|
+
"reverse_reference"
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
fn discover(&self, ctx: &DiscoveryContext) -> Vec<PathBuf> {
|
|
155
|
+
let changed_set: FxHashSet<&Path> = ctx.changed_files.iter().map(|p| p.as_path()).collect();
|
|
156
|
+
// Per changed file: the module name a reference would import it by,
|
|
157
|
+
// and the names it defines. A file that carries both is a reference
|
|
158
|
+
// however common the name; one that carries the name alone counts
|
|
159
|
+
// only while the name is rare enough to mean this definition.
|
|
160
|
+
let mut per_file: Vec<(Option<String>, FxHashSet<String>)> = Vec::new();
|
|
161
|
+
for path in &ctx.changed_files {
|
|
162
|
+
let stem = path
|
|
163
|
+
.file_stem()
|
|
164
|
+
.and_then(|s| s.to_str())
|
|
165
|
+
.filter(|s| s.len() >= MIN_REVERSE_NAME_LEN)
|
|
166
|
+
.map(|s| s.to_lowercase());
|
|
167
|
+
let names: FxHashSet<String> = ctx
|
|
168
|
+
.read_file(path)
|
|
169
|
+
.map(|c| {
|
|
170
|
+
definition_names(&c)
|
|
171
|
+
.into_iter()
|
|
172
|
+
.map(|n| n.to_lowercase())
|
|
173
|
+
.collect()
|
|
174
|
+
})
|
|
175
|
+
.unwrap_or_default();
|
|
176
|
+
if stem.is_some() || !names.is_empty() {
|
|
177
|
+
per_file.push((stem, names));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if per_file.is_empty() {
|
|
181
|
+
return Vec::new();
|
|
182
|
+
}
|
|
183
|
+
let mut name_holders: FxHashMap<&str, usize> = FxHashMap::default();
|
|
184
|
+
let mut strong: Vec<&PathBuf> = Vec::new();
|
|
185
|
+
let mut weak: Vec<(&PathBuf, Vec<&str>)> = Vec::new();
|
|
186
|
+
for (path, doc) in &ctx.shared_corpus().docs {
|
|
187
|
+
if changed_set.contains(path.as_path()) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
let mut is_strong = false;
|
|
191
|
+
let mut named: Vec<&str> = Vec::new();
|
|
192
|
+
for (stem, names) in &per_file {
|
|
193
|
+
let has_stem = stem
|
|
194
|
+
.as_deref()
|
|
195
|
+
.is_some_and(|s| doc.term_counts.contains_key(s));
|
|
196
|
+
for name in names {
|
|
197
|
+
if doc.term_counts.contains_key(name) {
|
|
198
|
+
*name_holders.entry(name.as_str()).or_default() += 1;
|
|
199
|
+
named.push(name.as_str());
|
|
200
|
+
if has_stem {
|
|
201
|
+
is_strong = true;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if is_strong {
|
|
207
|
+
strong.push(path);
|
|
208
|
+
} else if !named.is_empty() {
|
|
209
|
+
weak.push((path, named));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
strong.sort();
|
|
213
|
+
tracing::debug!(
|
|
214
|
+
"reverse references: {} definition sets, {} strong, {} weak, corpus {}",
|
|
215
|
+
per_file.len(),
|
|
216
|
+
strong.len(),
|
|
217
|
+
weak.len(),
|
|
218
|
+
ctx.shared_corpus().docs.len()
|
|
219
|
+
);
|
|
220
|
+
let mut result: Vec<PathBuf> = Vec::new();
|
|
221
|
+
let mut seen: FxHashSet<&PathBuf> = FxHashSet::default();
|
|
222
|
+
for f in strong {
|
|
223
|
+
if result.len() >= MAX_REVERSE_FILES {
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
if seen.insert(f) {
|
|
227
|
+
result.push(f.clone());
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
weak.sort_by(|a, b| a.0.cmp(b.0));
|
|
231
|
+
for (f, named) in weak {
|
|
232
|
+
if result.len() >= MAX_REVERSE_FILES {
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
let rare = named
|
|
236
|
+
.iter()
|
|
237
|
+
.any(|n| name_holders.get(n).copied().unwrap_or(0) <= MAX_REVERSE_FILES_PER_NAME);
|
|
238
|
+
if rare && seen.insert(f) {
|
|
239
|
+
result.push(f.clone());
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
result
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
120
246
|
pub struct TestFileDiscovery;
|
|
121
247
|
|
|
122
248
|
const TEST_PREFIXES: &[&str] = &["test_", "spec_"];
|
|
@@ -32,7 +32,10 @@ static FN_DEF_RE: Lazy<Regex> = Lazy::new(|| {
|
|
|
32
32
|
});
|
|
33
33
|
static TYPE_REF_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b([A-Z]\w*)\b").unwrap());
|
|
34
34
|
static FN_CALL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b([a-z_]\w+)\s*[(<]").unwrap());
|
|
35
|
-
|
|
35
|
+
/// Whole `a::b::c` paths; every adjacent pair is a (module, symbol) call so
|
|
36
|
+
/// `crate::pricing::total(..)` names `pricing::total` and not only
|
|
37
|
+
/// `crate::pricing`, which a pairwise regex stopped at.
|
|
38
|
+
static PATH_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b\w+(?:::\w+)+").unwrap());
|
|
36
39
|
static IMPL_RE: Lazy<Regex> =
|
|
37
40
|
Lazy::new(|| Regex::new(r"(?m)^\s*impl(?:<[^>]*>)?\s+(\w+)\s+for\s+(\w+)").unwrap());
|
|
38
41
|
static PUB_USE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^\s*pub\s+use\s+([\w:]+)").unwrap());
|
|
@@ -107,9 +110,15 @@ fn extract_references(content: &str) -> References {
|
|
|
107
110
|
fn_calls.insert(name.to_string());
|
|
108
111
|
}
|
|
109
112
|
}
|
|
110
|
-
let path_calls: FxHashSet<(String, String)> =
|
|
111
|
-
.
|
|
112
|
-
.
|
|
113
|
+
let path_calls: FxHashSet<(String, String)> = PATH_RE
|
|
114
|
+
.find_iter(content)
|
|
115
|
+
.flat_map(|m| {
|
|
116
|
+
let segments: Vec<&str> = m.as_str().split("::").collect();
|
|
117
|
+
segments
|
|
118
|
+
.windows(2)
|
|
119
|
+
.map(|w| (w[0].to_string(), w[1].to_string()))
|
|
120
|
+
.collect::<Vec<_>>()
|
|
121
|
+
})
|
|
113
122
|
.collect();
|
|
114
123
|
References {
|
|
115
124
|
type_refs,
|
|
@@ -196,6 +196,7 @@ pub const NON_SEMANTIC_ENV: &[&str] = &[
|
|
|
196
196
|
"DIFFCTX_ALLOWED_PATHS",
|
|
197
197
|
"DIFFCTX_BENCH_TIMEOUT_SEC",
|
|
198
198
|
"DIFFCTX_BUILD_SHA",
|
|
199
|
+
"DIFFCTX_CACHE_DIR",
|
|
199
200
|
"DIFFCTX_CONFIG_DIR",
|
|
200
201
|
"DIFFCTX_DIR_IGNORE",
|
|
201
202
|
"DIFFCTX_DIR_WHITELIST",
|
|
@@ -204,6 +205,7 @@ pub const NON_SEMANTIC_ENV: &[&str] = &[
|
|
|
204
205
|
"DIFFCTX_EVAL_STRICT",
|
|
205
206
|
"DIFFCTX_IGNORE_RELPATH",
|
|
206
207
|
"DIFFCTX_MCP_LEGACY_TOOLS",
|
|
208
|
+
"DIFFCTX_NO_MARKER",
|
|
207
209
|
"DIFFCTX_PROBE_MODE",
|
|
208
210
|
"DIFFCTX_PROBE_TAU",
|
|
209
211
|
"DIFFCTX_PROVENANCE",
|
|
@@ -426,6 +426,39 @@ impl Graph {
|
|
|
426
426
|
}
|
|
427
427
|
}
|
|
428
428
|
|
|
429
|
+
/// Invoke `f(source_id, weight)` for each node with an edge INTO `node`,
|
|
430
|
+
/// with the weight of that incoming edge.
|
|
431
|
+
pub fn for_each_reverse_neighbor<F: FnMut(&FragmentId, f64)>(
|
|
432
|
+
&self,
|
|
433
|
+
node: &FragmentId,
|
|
434
|
+
mut f: F,
|
|
435
|
+
) {
|
|
436
|
+
let rev = match self.rev_csr() {
|
|
437
|
+
Some(c) => c,
|
|
438
|
+
None => return,
|
|
439
|
+
};
|
|
440
|
+
let idx = match rev.node_to_idx.get(node) {
|
|
441
|
+
Some(&i) => i as usize,
|
|
442
|
+
None => return,
|
|
443
|
+
};
|
|
444
|
+
let s = rev.indptr[idx] as usize;
|
|
445
|
+
let e = rev.indptr[idx + 1] as usize;
|
|
446
|
+
for k in s..e {
|
|
447
|
+
let src_idx = rev.indices[k] as usize;
|
|
448
|
+
f(&rev.idx_to_node[src_idx], rev.weights[k]);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/// Whether `a -> b` is the stronger direction of the pair. Every builder
|
|
453
|
+
/// writes the reverse of an edge at `weight * reverse_factor` with the
|
|
454
|
+
/// factor below one, so the comparison recovers the direction the builder
|
|
455
|
+
/// meant (caller -> callee, importer -> imported) without a second table.
|
|
456
|
+
pub fn is_forward(&self, a: &FragmentId, b: &FragmentId) -> bool {
|
|
457
|
+
let ab = self.forward_edge_weight(a, b).unwrap_or(0.0);
|
|
458
|
+
let ba = self.forward_edge_weight(b, a).unwrap_or(0.0);
|
|
459
|
+
ab > ba
|
|
460
|
+
}
|
|
461
|
+
|
|
429
462
|
pub fn ego_graph(
|
|
430
463
|
&self,
|
|
431
464
|
seeds: &FxHashSet<FragmentId>,
|