diffctx 1.13.0__tar.gz → 1.15.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {diffctx-1.13.0 → diffctx-1.15.0}/CHANGELOG.md +112 -1
- {diffctx-1.13.0 → diffctx-1.15.0}/Cargo.lock +25 -25
- {diffctx-1.13.0 → diffctx-1.15.0}/PKG-INFO +3 -3
- {diffctx-1.13.0 → diffctx-1.15.0}/README.md +2 -2
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/Cargo.toml +1 -1
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/README.md +2 -2
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/limits.rs +9 -5
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/selection.rs +25 -8
- diffctx-1.15.0/crates/diffctx-native/src/deadline.rs +142 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/discovery.rs +0 -25
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/base.rs +64 -11
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/config_edges/generic.rs +115 -7
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/mod.rs +81 -4
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/c_family.rs +35 -22
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/go.rs +113 -45
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/javascript.rs +19 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/jvm.rs +87 -5
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/python.rs +27 -1
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/rust_lang.rs +122 -44
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/structural/containment.rs +16 -8
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/structural/testing.rs +11 -2
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/git.rs +35 -30
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/graph.rs +30 -2
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/locate.rs +22 -10
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/memory_pipeline.rs +4 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/parsers/tree_sitter_strategy.rs +51 -7
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/pipeline.rs +30 -9
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/project_graph.rs +1 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/pybridge.rs +4 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/render.rs +53 -15
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/scoring.rs +57 -4
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/select.rs +292 -51
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/token_corpus.rs +17 -9
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/utility/scoring.rs +2 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/pyproject.toml +1 -1
- {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/_app.py +6 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/_diffctx.pyi +1 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/cli.py +2 -2
- diffctx-1.15.0/src/diffctx/version.py +1 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/writer.py +37 -6
- diffctx-1.13.0/crates/diffctx-native/src/deadline.rs +0 -64
- diffctx-1.13.0/src/diffctx/version.py +0 -1
- {diffctx-1.13.0 → diffctx-1.15.0}/Cargo.toml +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/LICENSE +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/analytics.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/candidate_files.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/analytics.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/bm25.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/budget.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/category_weights.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/edge_weights.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/env_overrides.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/extensions.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/filtering.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/fragmentation.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/git.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/graph_filtering.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/importance.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/mod.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/mode.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/needs.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/parsers.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/render.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/scoring.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/tokenization.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/weights.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/core.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/config_edges/build_system.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/config_edges/cicd.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/config_edges/docker.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/config_edges/helm.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/config_edges/kubernetes.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/config_edges/mod.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/document/mod.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/history/cochange.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/history/mod.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/ansible.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/bazel.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/cargo_edges.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/clojure.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/css.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/dart.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/dbt.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/dotnet.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/elixir.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/erlang.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/graphql.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/haskell.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/julia.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/latex.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/lua.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/mod.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/nim.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/nix.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/ocaml.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/openapi.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/perl.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/php.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/prisma.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/protobuf.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/r_lang.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/ruby.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/shell.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/sql.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/swift.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/tags.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/terraform.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/zig.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/similarity/lexical.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/similarity/mod.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/structural/mod.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/structural/sibling.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/excerpt.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/filtering.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/fragmentation.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/graph_export.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/interval.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/languages.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/lib.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/main.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/mode.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/parsers/config_parser.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/parsers/generic.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/parsers/markdown.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/parsers/mod.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/paths.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/peak_rss.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/postpass.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/ppr.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/provenance.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/signatures.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/stopwords.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/test_harness.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/testfiles.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/tokenizer.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/types.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/utility/boltzmann.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/utility/importance.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/utility/mod.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/utility/needs.rs +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/rust-toolchain.toml +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/__init__.py +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/__main__.py +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/_native/__init__.py +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/_native/graph_analytics.py +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/_native/graph_export.py +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/_native/pipeline.py +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/_native/project_graph.py +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/clipboard.py +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/ignore.py +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/logger.py +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/mcp/README.md +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/mcp/__init__.py +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/mcp/__main__.py +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/mcp/fetch.py +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/mcp/formatting.py +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/mcp/security.py +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/mcp/server.py +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/py.typed +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/tokens.py +0 -0
- {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/tree.py +0 -0
|
@@ -9,6 +9,115 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
11
|
|
|
12
|
+
- **Changed files that produced no fragments are disclosed in the md/text
|
|
13
|
+
output** ("Changed files not represented in the output"): a range whose
|
|
14
|
+
selection dropped some changed files no longer hides the gap — the footer
|
|
15
|
+
names exactly the changed files absent from the fragment list. Derived
|
|
16
|
+
from the already policy-clean `changed_files`, so withheld paths can never
|
|
17
|
+
appear in it.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **Path-reference ambiguity cap counted duplicate index keys** (#207): every
|
|
22
|
+
file was indexed under its absolute AND repo-relative path, so
|
|
23
|
+
`MAX_FILES_PER_PATH_REF` (8) effectively halved, and host directory
|
|
24
|
+
components (`/home/runner/work/...`) matched references they had nothing
|
|
25
|
+
to do with. The index now carries one repo-relative posix entry per file.
|
|
26
|
+
- **`<script>` type detection** (#213): `data-type="json"` no longer opts a
|
|
27
|
+
real JavaScript block out of parsing, and a legal MIME parameter
|
|
28
|
+
(`type="text/javascript; charset=utf-8"`) no longer degrades the block to
|
|
29
|
+
raw line windows.
|
|
30
|
+
- **Scala import/package extraction gaps** (#215): Scala 3 renames
|
|
31
|
+
(`import a.b.C as D`, `a.{B as C}`) keep the original name, multi-line
|
|
32
|
+
brace imports keep their selectors, and nested package blocks no longer
|
|
33
|
+
concatenate sibling scopes into a nonexistent package.
|
|
34
|
+
- **Rust `pub use` re-export registration no longer depends on file
|
|
35
|
+
iteration order** (#199): registration now runs after the definition maps
|
|
36
|
+
are complete, so a re-export defined in a later-iterated file still
|
|
37
|
+
resolves (`rust_027_arc_mutex` left the corpus baseline on this fix).
|
|
38
|
+
- **The per-file budget ceiling now also gates signature substitution**
|
|
39
|
+
(#212): a file at its ceiling can no longer keep placing signature stubs
|
|
40
|
+
ahead of other files' cores.
|
|
41
|
+
- **A signature stub substituted for a changed core renders as `changed`**
|
|
42
|
+
(#209): the stub shares the core's location, and both surfaces (pack render
|
|
43
|
+
and locate) now agree it carries the change, same as an excerpt. Ordinary
|
|
44
|
+
context signatures stay context.
|
|
45
|
+
- **Config-key word boundaries treat non-ASCII bytes as word bytes** (#216):
|
|
46
|
+
a config key ending exactly where a non-ASCII letter begins (e.g. inside
|
|
47
|
+
`tokenización`) is no longer a match, restoring the Unicode-`\b` rejection
|
|
48
|
+
the Aho-Corasick rewrite lost.
|
|
49
|
+
|
|
50
|
+
## [1.14.0] - 2026-08-16
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- **Changed secret-classified files (`id_rsa`, `*.key`, `*.pem`, …) are now
|
|
55
|
+
disclosed in `policy_excluded_count`** (#214): they were silently dropped
|
|
56
|
+
before the ignore-exclusion disclosure ran, so a change touching only
|
|
57
|
+
secret paths printed a bare stub and a mixed change under-counted its
|
|
58
|
+
withheld files. Count only — the paths themselves stay out of the output,
|
|
59
|
+
same stance as `.diffctx/ignore`. The renderers' note now reads "withheld
|
|
60
|
+
by exclusion policy" to cover both families.
|
|
61
|
+
- **The compute deadline and the git subprocess timeout are per-run, not
|
|
62
|
+
process-global** (#210): concurrent MCP requests overwrote each other's
|
|
63
|
+
ceiling (a short-timeout request could abort a long-timeout one mid-flight,
|
|
64
|
+
and vice versa disable it), an expired ceiling was never cleared and leaked
|
|
65
|
+
into later in-process runs, and a single flooding edge builder could outrun
|
|
66
|
+
the whole timeout between checks. The deadline is now a value threaded
|
|
67
|
+
through the run, the git timeout is thread-scoped, and the known flooding
|
|
68
|
+
loops (config-key scan, C-family pairing, Python identifier emission) poll
|
|
69
|
+
the deadline inside the loop.
|
|
70
|
+
- A failed Aho-Corasick automaton build in the config-edge builder now logs a
|
|
71
|
+
warning instead of silently disabling the entire keyed channel (#216).
|
|
72
|
+
|
|
73
|
+
- **A commit touching only excluded paths now renders its disclosure** (#188
|
|
74
|
+
completion): the YAML/Markdown/text renderers and the CLI's empty-diff exit
|
|
75
|
+
treated `ignored_changes`/`policy_excluded_count` as "no output", so the one
|
|
76
|
+
case the disclosure exists for — every hunk withheld — printed a bare stub
|
|
77
|
+
and exited 4. All three renderers, `locate`, and the exit path now count the
|
|
78
|
+
disclosure as content.
|
|
79
|
+
- **Scala import with `}` before `{` no longer panics** the edge build
|
|
80
|
+
(`import a}.{B` in any `.scala`/`.sbt` line aborted the whole run).
|
|
81
|
+
- The corpus harness (`build_diff_context_in_memory`) now passes the per-file
|
|
82
|
+
admission gate to selection, matching the shipped pipeline; the full corpus
|
|
83
|
+
is bit-stable under the fix (2902 passed, baseline unchanged).
|
|
84
|
+
- An absurd `timeout` value can no longer wrap the compute deadline into the
|
|
85
|
+
past (saturating arithmetic); the token-cache eviction sweep no longer
|
|
86
|
+
deletes another process's in-flight temp file.
|
|
87
|
+
- Text renderer writes one path per line (a comma or newline inside a path
|
|
88
|
+
made the joined line unparseable).
|
|
89
|
+
|
|
90
|
+
### Changed
|
|
91
|
+
|
|
92
|
+
- **New default operating point: per-file admission ON, `tau` 0.12 -> 0.05,
|
|
93
|
+
`core_budget_fraction` 0.5 -> 0.4** — the v5 evaluation cycle's validated
|
|
94
|
+
winner. A file whose only connection to the change is proximity (sibling
|
|
95
|
+
directory, shared rare token, lexical similarity) is no longer selectable as
|
|
96
|
+
context; it must be naming-reachable (an import/call/type-ref path from the
|
|
97
|
+
changed set, up to two hops, either direction). The weak stop then spends
|
|
98
|
+
the freed budget inside that admissible set. Evidence chain: 12-cell
|
|
99
|
+
screening grid (corpus known-failures 227 -> 196 with three xfails retiring;
|
|
100
|
+
the strict-tau admission variant was killed by a significant real-commit
|
|
101
|
+
recall loss), tau x cbf calibration + held-out validation (0.6815 vs 0.6684
|
|
102
|
+
for the old point), and a six-arm confirmation sweep on the three 500-case
|
|
103
|
+
test splits: file_recall parity with the old default, contextbench
|
|
104
|
+
file_precision +0.041 (bootstrap CI excludes zero), dcbench-372 paired
|
|
105
|
+
nontrivial recall within noise at -4% tokens. Opt out with
|
|
106
|
+
`DIFFCTX_FILE_ADMISSION=0` and `--tau 0.12` to reproduce the old behaviour
|
|
107
|
+
(#65).
|
|
108
|
+
|
|
109
|
+
- **Selection enforces a per-file budget ceiling** (#194): while other files
|
|
110
|
+
still have unplaced candidates, one file may not consume more than
|
|
111
|
+
`SELECTION.per_file_budget_fraction` (default 0.25) of the budget — the
|
|
112
|
+
reporter's +9k-line generated JSON no longer crowds 34 source files out of
|
|
113
|
+
the output. Blocked candidates are deferred, not dropped: once every other
|
|
114
|
+
file has had its chance the ceiling lifts, so a single-file change still
|
|
115
|
+
fills the whole budget.
|
|
116
|
+
|
|
117
|
+
## [1.13.0] - 2026-08-04
|
|
118
|
+
|
|
119
|
+
### Added
|
|
120
|
+
|
|
12
121
|
- **Inline `<script>` and `<style>` in HTML are parsed with their own
|
|
13
122
|
grammars** (#181). A changed function inside a single-file web app's script
|
|
14
123
|
now surfaces as a named definition fragment at its real file lines instead
|
|
@@ -924,7 +1033,9 @@ Earlier releases shipped as `treemapper`; see
|
|
|
924
1033
|
<https://github.com/nikolay-e/diffctx/releases> for the corresponding GitHub
|
|
925
1034
|
release notes (`1.0.0` through `1.6.1`).
|
|
926
1035
|
|
|
927
|
-
[Unreleased]: https://github.com/nikolay-e/diffctx/compare/v1.
|
|
1036
|
+
[Unreleased]: https://github.com/nikolay-e/diffctx/compare/v1.14.0...HEAD
|
|
1037
|
+
[1.14.0]: https://github.com/nikolay-e/diffctx/compare/v1.13.0...v1.14.0
|
|
1038
|
+
[1.13.0]: https://github.com/nikolay-e/diffctx/compare/v1.12.3...v1.13.0
|
|
928
1039
|
[1.12.3]: https://github.com/nikolay-e/diffctx/compare/v1.12.2...v1.12.3
|
|
929
1040
|
[1.12.2]: https://github.com/nikolay-e/diffctx/compare/v1.12.1...v1.12.2
|
|
930
1041
|
[1.7.0]: https://github.com/nikolay-e/diffctx/compare/v1.6.1...v1.7.0
|
|
@@ -4,9 +4,9 @@ version = 4
|
|
|
4
4
|
|
|
5
5
|
[[package]]
|
|
6
6
|
name = "aho-corasick"
|
|
7
|
-
version = "1.1.
|
|
7
|
+
version = "1.1.5"
|
|
8
8
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
9
|
-
checksum = "
|
|
9
|
+
checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
|
|
10
10
|
dependencies = [
|
|
11
11
|
"memchr",
|
|
12
12
|
]
|
|
@@ -123,9 +123,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
|
|
123
123
|
|
|
124
124
|
[[package]]
|
|
125
125
|
name = "clap"
|
|
126
|
-
version = "4.6.
|
|
126
|
+
version = "4.6.6"
|
|
127
127
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
128
|
-
checksum = "
|
|
128
|
+
checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
|
|
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.6"
|
|
137
137
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
138
|
-
checksum = "
|
|
138
|
+
checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
|
|
139
139
|
dependencies = [
|
|
140
140
|
"anstream",
|
|
141
141
|
"anstyle",
|
|
@@ -194,7 +194,7 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
|
|
194
194
|
|
|
195
195
|
[[package]]
|
|
196
196
|
name = "diffctx"
|
|
197
|
-
version = "1.
|
|
197
|
+
version = "1.15.0"
|
|
198
198
|
dependencies = [
|
|
199
199
|
"aho-corasick",
|
|
200
200
|
"anyhow",
|
|
@@ -515,9 +515,9 @@ dependencies = [
|
|
|
515
515
|
|
|
516
516
|
[[package]]
|
|
517
517
|
name = "pyo3"
|
|
518
|
-
version = "0.29.
|
|
518
|
+
version = "0.29.2"
|
|
519
519
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
520
|
-
checksum = "
|
|
520
|
+
checksum = "4688ddedf473e32662b9b067670129a8afb8c18e351482c70d62ba4a88171e8b"
|
|
521
521
|
dependencies = [
|
|
522
522
|
"libc",
|
|
523
523
|
"once_cell",
|
|
@@ -529,18 +529,18 @@ dependencies = [
|
|
|
529
529
|
|
|
530
530
|
[[package]]
|
|
531
531
|
name = "pyo3-build-config"
|
|
532
|
-
version = "0.29.
|
|
532
|
+
version = "0.29.2"
|
|
533
533
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
534
|
-
checksum = "
|
|
534
|
+
checksum = "f41027e41b4bd03f6e60f9f417fe24a6341a6bb744edd62b6f709f2a52ea30e9"
|
|
535
535
|
dependencies = [
|
|
536
536
|
"target-lexicon",
|
|
537
537
|
]
|
|
538
538
|
|
|
539
539
|
[[package]]
|
|
540
540
|
name = "pyo3-ffi"
|
|
541
|
-
version = "0.29.
|
|
541
|
+
version = "0.29.2"
|
|
542
542
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
543
|
-
checksum = "
|
|
543
|
+
checksum = "e591a95526fead067432c3b3a33fc74770b87b1e04e73671090d9c2055a2b327"
|
|
544
544
|
dependencies = [
|
|
545
545
|
"libc",
|
|
546
546
|
"pyo3-build-config",
|
|
@@ -548,9 +548,9 @@ dependencies = [
|
|
|
548
548
|
|
|
549
549
|
[[package]]
|
|
550
550
|
name = "pyo3-macros"
|
|
551
|
-
version = "0.29.
|
|
551
|
+
version = "0.29.2"
|
|
552
552
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
553
|
-
checksum = "
|
|
553
|
+
checksum = "73225868fc1cd84eef2c3c230ddb91273bf1de46aeb8a4248da76d32a0924a1c"
|
|
554
554
|
dependencies = [
|
|
555
555
|
"proc-macro2",
|
|
556
556
|
"pyo3-macros-backend",
|
|
@@ -560,9 +560,9 @@ dependencies = [
|
|
|
560
560
|
|
|
561
561
|
[[package]]
|
|
562
562
|
name = "pyo3-macros-backend"
|
|
563
|
-
version = "0.29.
|
|
563
|
+
version = "0.29.2"
|
|
564
564
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
565
|
-
checksum = "
|
|
565
|
+
checksum = "571575aa3749fa6216757dd47d2a3e7ef360f329a40f0666a9fbd14889024952"
|
|
566
566
|
dependencies = [
|
|
567
567
|
"heck",
|
|
568
568
|
"proc-macro2",
|
|
@@ -748,9 +748,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
|
|
748
748
|
|
|
749
749
|
[[package]]
|
|
750
750
|
name = "similar"
|
|
751
|
-
version = "3.1.
|
|
751
|
+
version = "3.1.2"
|
|
752
752
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
753
|
-
checksum = "
|
|
753
|
+
checksum = "85ee016af5d736b69fc89e19254540fa4b5f5492853fb5503920f084011c78b6"
|
|
754
754
|
dependencies = [
|
|
755
755
|
"bstr",
|
|
756
756
|
]
|
|
@@ -816,18 +816,18 @@ dependencies = [
|
|
|
816
816
|
|
|
817
817
|
[[package]]
|
|
818
818
|
name = "thiserror"
|
|
819
|
-
version = "2.0.
|
|
819
|
+
version = "2.0.20"
|
|
820
820
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
821
|
-
checksum = "
|
|
821
|
+
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
|
|
822
822
|
dependencies = [
|
|
823
823
|
"thiserror-impl",
|
|
824
824
|
]
|
|
825
825
|
|
|
826
826
|
[[package]]
|
|
827
827
|
name = "thiserror-impl"
|
|
828
|
-
version = "2.0.
|
|
828
|
+
version = "2.0.20"
|
|
829
829
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
830
|
-
checksum = "
|
|
830
|
+
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
|
|
831
831
|
dependencies = [
|
|
832
832
|
"proc-macro2",
|
|
833
833
|
"quote",
|
|
@@ -1252,9 +1252,9 @@ dependencies = [
|
|
|
1252
1252
|
|
|
1253
1253
|
[[package]]
|
|
1254
1254
|
name = "tree-sitter-scala"
|
|
1255
|
-
version = "0.26.
|
|
1255
|
+
version = "0.26.2"
|
|
1256
1256
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1257
|
-
checksum = "
|
|
1257
|
+
checksum = "24e0ab4505990bfe30051761d40a7bf4033ce5a81c9eda9e20e987a5cdc84826"
|
|
1258
1258
|
dependencies = [
|
|
1259
1259
|
"cc",
|
|
1260
1260
|
"tree-sitter-language",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: diffctx
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.15.0
|
|
4
4
|
Classifier: Development Status :: 5 - Production/Stable
|
|
5
5
|
Classifier: Environment :: Console
|
|
6
6
|
Classifier: Intended Audience :: Developers
|
|
@@ -162,7 +162,7 @@ keeps its git meaning.
|
|
|
162
162
|
| `--scoring` | `ego` | `ego` = bounded expansion around changed nodes (fast, predictable radius); `ppr` = Personalized PageRank (global, smoother decay, slower); `bm25` = lexical retrieval against the diff hunks (baseline for sparse graphs); `rrf` = reciprocal-rank fusion of `ego` and `bm25` (widest recall, no scale calibration between the two signals) |
|
|
163
163
|
| `--budget` | auto | Hard cap in o200k_base tokens (see [Token counting](docs/product/token-budget.md)): `N` enforces a fixed cap, `-1` disables it, `0` is a strict-zero floor (empty selection; use `--full` for changed files only) |
|
|
164
164
|
| `--alpha` | 0.60 | PPR damping; higher = context clusters tighter around changes (`--scoring ppr` only) |
|
|
165
|
-
| `--tau` | 0.
|
|
165
|
+
| `--tau` | 0.05 | Relevance threshold for full fragment content; lower-scoring fragments are stubbed or dropped (lower = more context) |
|
|
166
166
|
| `--full` | false | Only the changed files, every fragment, no related-code context |
|
|
167
167
|
| `--timeout` | 300 | Wall-clock deadline in seconds; on expiry diffctx exits 124 instead of hanging |
|
|
168
168
|
| `--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 |
|
|
@@ -221,7 +221,7 @@ ctx = build_diff_context(
|
|
|
221
221
|
"HEAD~1..HEAD",
|
|
222
222
|
budget_tokens=None, # None = auto; 0 = strict-zero floor (empty); -1 = uncapped; N = hard cap
|
|
223
223
|
alpha=0.6,
|
|
224
|
-
tau=0.
|
|
224
|
+
tau=0.05,
|
|
225
225
|
full=False,
|
|
226
226
|
scoring_mode="ego",
|
|
227
227
|
timeout=300,
|
|
@@ -84,7 +84,7 @@ keeps its git meaning.
|
|
|
84
84
|
| `--scoring` | `ego` | `ego` = bounded expansion around changed nodes (fast, predictable radius); `ppr` = Personalized PageRank (global, smoother decay, slower); `bm25` = lexical retrieval against the diff hunks (baseline for sparse graphs); `rrf` = reciprocal-rank fusion of `ego` and `bm25` (widest recall, no scale calibration between the two signals) |
|
|
85
85
|
| `--budget` | auto | Hard cap in o200k_base tokens (see [Token counting](docs/product/token-budget.md)): `N` enforces a fixed cap, `-1` disables it, `0` is a strict-zero floor (empty selection; use `--full` for changed files only) |
|
|
86
86
|
| `--alpha` | 0.60 | PPR damping; higher = context clusters tighter around changes (`--scoring ppr` only) |
|
|
87
|
-
| `--tau` | 0.
|
|
87
|
+
| `--tau` | 0.05 | Relevance threshold for full fragment content; lower-scoring fragments are stubbed or dropped (lower = more context) |
|
|
88
88
|
| `--full` | false | Only the changed files, every fragment, no related-code context |
|
|
89
89
|
| `--timeout` | 300 | Wall-clock deadline in seconds; on expiry diffctx exits 124 instead of hanging |
|
|
90
90
|
| `--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 |
|
|
@@ -143,7 +143,7 @@ ctx = build_diff_context(
|
|
|
143
143
|
"HEAD~1..HEAD",
|
|
144
144
|
budget_tokens=None, # None = auto; 0 = strict-zero floor (empty); -1 = uncapped; N = hard cap
|
|
145
145
|
alpha=0.6,
|
|
146
|
-
tau=0.
|
|
146
|
+
tau=0.05,
|
|
147
147
|
full=False,
|
|
148
148
|
scoring_mode="ego",
|
|
149
149
|
timeout=300,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "diffctx"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.15.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"
|
|
@@ -84,7 +84,7 @@ keeps its git meaning.
|
|
|
84
84
|
| `--scoring` | `ego` | `ego` = bounded expansion around changed nodes (fast, predictable radius); `ppr` = Personalized PageRank (global, smoother decay, slower); `bm25` = lexical retrieval against the diff hunks (baseline for sparse graphs); `rrf` = reciprocal-rank fusion of `ego` and `bm25` (widest recall, no scale calibration between the two signals) |
|
|
85
85
|
| `--budget` | auto | Hard cap in o200k_base tokens (see [Token counting](docs/product/token-budget.md)): `N` enforces a fixed cap, `-1` disables it, `0` is a strict-zero floor (empty selection; use `--full` for changed files only) |
|
|
86
86
|
| `--alpha` | 0.60 | PPR damping; higher = context clusters tighter around changes (`--scoring ppr` only) |
|
|
87
|
-
| `--tau` | 0.
|
|
87
|
+
| `--tau` | 0.05 | Relevance threshold for full fragment content; lower-scoring fragments are stubbed or dropped (lower = more context) |
|
|
88
88
|
| `--full` | false | Only the changed files, every fragment, no related-code context |
|
|
89
89
|
| `--timeout` | 300 | Wall-clock deadline in seconds; on expiry diffctx exits 124 instead of hanging |
|
|
90
90
|
| `--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 |
|
|
@@ -143,7 +143,7 @@ ctx = build_diff_context(
|
|
|
143
143
|
"HEAD~1..HEAD",
|
|
144
144
|
budget_tokens=None, # None = auto; 0 = strict-zero floor (empty); -1 = uncapped; N = hard cap
|
|
145
145
|
alpha=0.6,
|
|
146
|
-
tau=0.
|
|
146
|
+
tau=0.05,
|
|
147
147
|
full=False,
|
|
148
148
|
scoring_mode="ego",
|
|
149
149
|
timeout=300,
|
|
@@ -49,11 +49,15 @@ pub const MAX_BLOB_READ_BYTES: usize = 16_000_000;
|
|
|
49
49
|
pub const DEFAULT_SCORING: &str = "ego";
|
|
50
50
|
|
|
51
51
|
pub const DEFAULT_PPR_ALPHA: f64 = 0.60;
|
|
52
|
-
///
|
|
53
|
-
///
|
|
54
|
-
///
|
|
55
|
-
///
|
|
56
|
-
|
|
52
|
+
/// v5 re-calibration under per-file admission (4x3 grid, v1 calibration
|
|
53
|
+
/// manifest, held-out validated): winner (tau, cbf) = (0.05, 0.4) at
|
|
54
|
+
/// min(per_benchmark file_recall) = 0.6815 vs 0.6684 for the old point.
|
|
55
|
+
/// Confirmed on the test splits (500x3): file_recall parity with the old
|
|
56
|
+
/// default, contextbench file_precision +0.041 (CI excludes zero), and on
|
|
57
|
+
/// dcbench-372 paired nontrivial recall within noise at -4% tokens. The
|
|
58
|
+
/// weak stop only ships together with the admission gate (#65): without it,
|
|
59
|
+
/// tau=0.05 re-admits the diffuse tail the gate exists to block.
|
|
60
|
+
pub const DEFAULT_STOPPING_THRESHOLD: f64 = 0.05;
|
|
57
61
|
pub const DEFAULT_PIPELINE_TIMEOUT_SECONDS: u64 = 300;
|
|
58
62
|
|
|
59
63
|
pub struct PPRConfig {
|
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
use crate::config::env_overrides::{read_env_fraction, read_env_u32};
|
|
2
2
|
|
|
3
|
+
// v5 re-calibration under per-file admission: validated winner
|
|
4
|
+
// (tau, cbf) = (0.05, 0.4); the tau half lives in limits.rs. Named (and
|
|
5
|
+
// exported through pybridge) so the Python layers read it instead of
|
|
6
|
+
// restating it — the same drift class as #175's tau copies.
|
|
7
|
+
pub const DEFAULT_CORE_BUDGET_FRACTION: f64 = 0.4;
|
|
8
|
+
|
|
3
9
|
#[derive(Clone)]
|
|
4
10
|
pub struct SelectionConfig {
|
|
5
11
|
pub core_budget_fraction: f64,
|
|
6
12
|
pub r_cap_min: f64,
|
|
13
|
+
/// Share of the run budget one file may consume while other files still
|
|
14
|
+
/// have unplaced candidates (#194: a single +9k-line data blob monopolized
|
|
15
|
+
/// 364 of 370 output sections). The ceiling binds only in the first
|
|
16
|
+
/// placement phase — leftovers no other file claims flow back to whoever
|
|
17
|
+
/// wants them, so a single-file change is unaffected.
|
|
18
|
+
pub per_file_budget_fraction: f64,
|
|
7
19
|
}
|
|
8
20
|
|
|
9
21
|
impl Default for SelectionConfig {
|
|
10
22
|
fn default() -> Self {
|
|
11
23
|
Self {
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
// (top-3 within 0.004), so the choice is robust rather
|
|
17
|
-
// than tuned to a sharp optimum.
|
|
18
|
-
core_budget_fraction: 0.5,
|
|
24
|
+
// cbf margin over 0.5 is small but consistent in direction on
|
|
25
|
+
// both the calibration and held-out manifests. Surface flat and
|
|
26
|
+
// monotone, as in v4.
|
|
27
|
+
core_budget_fraction: DEFAULT_CORE_BUDGET_FRACTION,
|
|
19
28
|
r_cap_min: 0.01,
|
|
29
|
+
per_file_budget_fraction: 0.25,
|
|
20
30
|
}
|
|
21
31
|
}
|
|
22
32
|
}
|
|
@@ -57,8 +67,15 @@ impl Default for BoltzmannConfig {
|
|
|
57
67
|
|
|
58
68
|
pub fn selection() -> SelectionConfig {
|
|
59
69
|
SelectionConfig {
|
|
60
|
-
core_budget_fraction: read_env_fraction(
|
|
70
|
+
core_budget_fraction: read_env_fraction(
|
|
71
|
+
"DIFFCTX_OP_SELECTION_CORE_BUDGET_FRACTION",
|
|
72
|
+
DEFAULT_CORE_BUDGET_FRACTION,
|
|
73
|
+
),
|
|
61
74
|
r_cap_min: read_env_fraction("DIFFCTX_OP_SELECTION_R_CAP_MIN", 0.01),
|
|
75
|
+
per_file_budget_fraction: read_env_fraction(
|
|
76
|
+
"DIFFCTX_OP_SELECTION_PER_FILE_BUDGET_FRACTION",
|
|
77
|
+
0.25,
|
|
78
|
+
),
|
|
62
79
|
}
|
|
63
80
|
}
|
|
64
81
|
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
use std::cell::Cell;
|
|
2
|
+
use std::time::{Duration, Instant};
|
|
3
|
+
|
|
4
|
+
/// Per-run wall-clock ceiling for the compute phases.
|
|
5
|
+
///
|
|
6
|
+
/// The `timeout` parameter only ever reached git subprocesses
|
|
7
|
+
/// (`set_git_timeout`); the native CLI is protected by a process-level
|
|
8
|
+
/// watchdog that exits 124, but the library path — pyo3, and through it the
|
|
9
|
+
/// MCP server — had no ceiling at all: a 420s timeout was observed to sit
|
|
10
|
+
/// through an 8-minute edge build without firing.
|
|
11
|
+
///
|
|
12
|
+
/// A per-run value rather than a process global (#210): the MCP server runs
|
|
13
|
+
/// overlapping pipelines on worker threads, and a shared atomic meant the
|
|
14
|
+
/// last request to arrive overwrote every in-flight request's ceiling — and
|
|
15
|
+
/// an expired ceiling was never cleared, so unrelated later runs in the same
|
|
16
|
+
/// process (the yaml harness above all) inherited it.
|
|
17
|
+
///
|
|
18
|
+
/// Expiry panics with a recognizable message on purpose: the phase runs deep
|
|
19
|
+
/// inside call chains that do not return `Result`, rayon propagates the
|
|
20
|
+
/// unwind to the caller, and pyo3 surfaces it as a Python exception — an
|
|
21
|
+
/// error after `timeout` seconds, where before there was a hang.
|
|
22
|
+
#[derive(Clone, Copy, Debug)]
|
|
23
|
+
pub struct Deadline {
|
|
24
|
+
expires_at: Option<Instant>,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
impl Deadline {
|
|
28
|
+
/// Saturating: an absurd caller-supplied timeout must clamp to "no
|
|
29
|
+
/// ceiling", not wrap behind `now` and fire instantly.
|
|
30
|
+
pub fn from_timeout_secs(timeout_secs: u64) -> Self {
|
|
31
|
+
Deadline {
|
|
32
|
+
expires_at: Instant::now().checked_add(Duration::from_secs(timeout_secs)),
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
pub fn none() -> Self {
|
|
37
|
+
Deadline { expires_at: None }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
pub fn check(&self, phase: &str) {
|
|
41
|
+
if let Some(expires_at) = self.expires_at {
|
|
42
|
+
check_expired(Instant::now(), expires_at, phase);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Publishes this deadline to the current thread for the guard's
|
|
47
|
+
/// lifetime, so hot loops deep inside edge builders can poll it via
|
|
48
|
+
/// `check_current_every` without threading a parameter through 51
|
|
49
|
+
/// `EdgeBuilder::build` implementations. Each rayon worker publishes its
|
|
50
|
+
/// own copy, so concurrent runs never see each other's ceiling.
|
|
51
|
+
pub fn enter(&self) -> ScopedDeadline {
|
|
52
|
+
let prev = CURRENT.with(|c| c.replace(self.expires_at));
|
|
53
|
+
ScopedDeadline { prev }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
thread_local! {
|
|
58
|
+
static CURRENT: Cell<Option<Instant>> = const { Cell::new(None) };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
pub struct ScopedDeadline {
|
|
62
|
+
prev: Option<Instant>,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
impl Drop for ScopedDeadline {
|
|
66
|
+
fn drop(&mut self) {
|
|
67
|
+
let prev = self.prev;
|
|
68
|
+
CURRENT.with(|c| c.set(prev));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Cheap intra-loop poll: costs a branch on all but every `every`-th
|
|
73
|
+
/// iteration. For the builder loops whose single invocation can outrun the
|
|
74
|
+
/// whole timeout (the envoy 520-`config` cross product, the sentry-scale
|
|
75
|
+
/// config-key scan), where the between-builders check cannot help.
|
|
76
|
+
pub fn check_current_every(i: usize, every: usize, phase: &str) {
|
|
77
|
+
if i % every != 0 {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if let Some(expires_at) = CURRENT.with(|c| c.get()) {
|
|
81
|
+
check_expired(Instant::now(), expires_at, phase);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
fn check_expired(now: Instant, expires_at: Instant, phase: &str) {
|
|
86
|
+
if now > expires_at {
|
|
87
|
+
panic!("diffctx compute deadline exceeded during {phase}");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#[cfg(test)]
|
|
92
|
+
mod tests {
|
|
93
|
+
use super::*;
|
|
94
|
+
|
|
95
|
+
#[test]
|
|
96
|
+
fn an_expired_deadline_panics_with_the_phase_name() {
|
|
97
|
+
let deadline = Deadline::from_timeout_secs(0);
|
|
98
|
+
std::thread::sleep(Duration::from_millis(5));
|
|
99
|
+
let err = std::panic::catch_unwind(|| deadline.check("edge construction"))
|
|
100
|
+
.expect_err("deadline did not fire");
|
|
101
|
+
let msg = err.downcast_ref::<String>().cloned().unwrap_or_default();
|
|
102
|
+
assert!(msg.contains("edge construction"), "message was: {msg}");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#[test]
|
|
106
|
+
fn an_unexpired_deadline_does_not_fire() {
|
|
107
|
+
Deadline::from_timeout_secs(1000).check("edge construction");
|
|
108
|
+
Deadline::none().check("edge construction");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#[test]
|
|
112
|
+
fn concurrent_deadlines_do_not_affect_each_other() {
|
|
113
|
+
// The #210 defect: request B's short ceiling used to overwrite
|
|
114
|
+
// request A's. Per-run values make each check see only its own.
|
|
115
|
+
let short = Deadline::from_timeout_secs(0);
|
|
116
|
+
let long = Deadline::from_timeout_secs(1000);
|
|
117
|
+
std::thread::sleep(Duration::from_millis(5));
|
|
118
|
+
long.check("edge construction");
|
|
119
|
+
std::panic::catch_unwind(|| short.check("edge construction"))
|
|
120
|
+
.expect_err("short deadline did not fire");
|
|
121
|
+
long.check("edge construction");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#[test]
|
|
125
|
+
fn scoped_deadline_clears_on_drop_and_nests() {
|
|
126
|
+
let outer = Deadline::from_timeout_secs(1000);
|
|
127
|
+
let guard = outer.enter();
|
|
128
|
+
check_current_every(0, 1, "edge construction");
|
|
129
|
+
{
|
|
130
|
+
let expired = Deadline::from_timeout_secs(0);
|
|
131
|
+
let inner = expired.enter();
|
|
132
|
+
std::thread::sleep(Duration::from_millis(5));
|
|
133
|
+
std::panic::catch_unwind(|| check_current_every(0, 1, "edge construction"))
|
|
134
|
+
.expect_err("inner deadline did not fire");
|
|
135
|
+
drop(inner);
|
|
136
|
+
}
|
|
137
|
+
check_current_every(0, 1, "edge construction");
|
|
138
|
+
drop(guard);
|
|
139
|
+
// No deadline published: must never fire.
|
|
140
|
+
check_current_every(0, 1, "edge construction");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -309,24 +309,6 @@ impl EnsembleDiscovery {
|
|
|
309
309
|
pub fn new(strategies: Vec<Box<dyn DiscoveryStrategy>>) -> Self {
|
|
310
310
|
Self { strategies }
|
|
311
311
|
}
|
|
312
|
-
|
|
313
|
-
pub fn default_ensemble() -> Self {
|
|
314
|
-
// The lexical channel's breadth into the candidate universe. The
|
|
315
|
-
// shipped 1 means BM25 contributes a single file to discovery; measured
|
|
316
|
-
// on dcbench, 79% of nontrivial gold never enters the universe at all,
|
|
317
|
-
// and most of it shares 3+ path tokens with the diff — exactly what a
|
|
318
|
-
// wider lexical channel would surface. Env-gated for the universe
|
|
319
|
-
// ablation (#130); unset, behaviour is byte-identical to before.
|
|
320
|
-
let bm25_k =
|
|
321
|
-
crate::config::env_overrides::read_env_usize("DIFFCTX_BM25_DISCOVERY_TOP_K", 1);
|
|
322
|
-
Self {
|
|
323
|
-
strategies: vec![
|
|
324
|
-
Box::new(DefaultDiscovery),
|
|
325
|
-
Box::new(TestFileDiscovery),
|
|
326
|
-
Box::new(BM25Discovery::new(bm25_k)),
|
|
327
|
-
],
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
312
|
}
|
|
331
313
|
|
|
332
314
|
impl DiscoveryStrategy for EnsembleDiscovery {
|
|
@@ -750,11 +732,4 @@ mod tests {
|
|
|
750
732
|
assert_eq!(paths.len(), 1);
|
|
751
733
|
assert_eq!(attribution[0].1, "solo");
|
|
752
734
|
}
|
|
753
|
-
|
|
754
|
-
#[test]
|
|
755
|
-
fn default_ensemble_wires_three_channels() {
|
|
756
|
-
// A channel silently disappearing from the default wiring is exactly
|
|
757
|
-
// the regression the ensemble's own output cannot reveal.
|
|
758
|
-
assert_eq!(EnsembleDiscovery::default_ensemble().strategies.len(), 3);
|
|
759
|
-
}
|
|
760
735
|
}
|