diffctx 1.13.0__tar.gz → 1.14.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.
Files changed (161) hide show
  1. {diffctx-1.13.0 → diffctx-1.14.0}/CHANGELOG.md +69 -1
  2. {diffctx-1.13.0 → diffctx-1.14.0}/Cargo.lock +25 -25
  3. {diffctx-1.13.0 → diffctx-1.14.0}/PKG-INFO +3 -3
  4. {diffctx-1.13.0 → diffctx-1.14.0}/README.md +2 -2
  5. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/Cargo.toml +1 -1
  6. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/README.md +2 -2
  7. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/config/limits.rs +9 -5
  8. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/config/selection.rs +25 -8
  9. diffctx-1.14.0/crates/diffctx-native/src/deadline.rs +142 -0
  10. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/discovery.rs +0 -25
  11. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/config_edges/generic.rs +70 -6
  12. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/mod.rs +81 -4
  13. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/c_family.rs +30 -18
  14. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/go.rs +113 -45
  15. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/javascript.rs +19 -0
  16. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/jvm.rs +14 -1
  17. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/python.rs +27 -1
  18. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/rust_lang.rs +63 -32
  19. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/structural/testing.rs +9 -0
  20. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/git.rs +35 -30
  21. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/graph.rs +30 -2
  22. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/locate.rs +3 -7
  23. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/memory_pipeline.rs +4 -0
  24. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/pipeline.rs +30 -9
  25. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/project_graph.rs +1 -0
  26. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/pybridge.rs +4 -0
  27. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/render.rs +3 -7
  28. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/scoring.rs +57 -4
  29. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/select.rs +221 -51
  30. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/token_corpus.rs +17 -9
  31. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/utility/scoring.rs +2 -0
  32. {diffctx-1.13.0 → diffctx-1.14.0}/pyproject.toml +1 -1
  33. {diffctx-1.13.0 → diffctx-1.14.0}/src/diffctx/_app.py +6 -0
  34. {diffctx-1.13.0 → diffctx-1.14.0}/src/diffctx/_diffctx.pyi +1 -0
  35. {diffctx-1.13.0 → diffctx-1.14.0}/src/diffctx/cli.py +2 -2
  36. diffctx-1.14.0/src/diffctx/version.py +1 -0
  37. {diffctx-1.13.0 → diffctx-1.14.0}/src/diffctx/writer.py +17 -6
  38. diffctx-1.13.0/crates/diffctx-native/src/deadline.rs +0 -64
  39. diffctx-1.13.0/src/diffctx/version.py +0 -1
  40. {diffctx-1.13.0 → diffctx-1.14.0}/Cargo.toml +0 -0
  41. {diffctx-1.13.0 → diffctx-1.14.0}/LICENSE +0 -0
  42. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/analytics.rs +0 -0
  43. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/candidate_files.rs +0 -0
  44. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/config/analytics.rs +0 -0
  45. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/config/bm25.rs +0 -0
  46. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/config/budget.rs +0 -0
  47. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/config/category_weights.rs +0 -0
  48. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/config/edge_weights.rs +0 -0
  49. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/config/env_overrides.rs +0 -0
  50. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/config/extensions.rs +0 -0
  51. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/config/filtering.rs +0 -0
  52. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/config/fragmentation.rs +0 -0
  53. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/config/git.rs +0 -0
  54. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/config/graph_filtering.rs +0 -0
  55. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/config/importance.rs +0 -0
  56. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/config/mod.rs +0 -0
  57. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/config/mode.rs +0 -0
  58. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/config/needs.rs +0 -0
  59. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/config/parsers.rs +0 -0
  60. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/config/render.rs +0 -0
  61. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/config/scoring.rs +0 -0
  62. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/config/tokenization.rs +0 -0
  63. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/config/weights.rs +0 -0
  64. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/core.rs +0 -0
  65. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/base.rs +0 -0
  66. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/config_edges/build_system.rs +0 -0
  67. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/config_edges/cicd.rs +0 -0
  68. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/config_edges/docker.rs +0 -0
  69. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/config_edges/helm.rs +0 -0
  70. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/config_edges/kubernetes.rs +0 -0
  71. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/config_edges/mod.rs +0 -0
  72. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/document/mod.rs +0 -0
  73. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/history/cochange.rs +0 -0
  74. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/history/mod.rs +0 -0
  75. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/ansible.rs +0 -0
  76. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/bazel.rs +0 -0
  77. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/cargo_edges.rs +0 -0
  78. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/clojure.rs +0 -0
  79. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/css.rs +0 -0
  80. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/dart.rs +0 -0
  81. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/dbt.rs +0 -0
  82. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/dotnet.rs +0 -0
  83. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/elixir.rs +0 -0
  84. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/erlang.rs +0 -0
  85. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/graphql.rs +0 -0
  86. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/haskell.rs +0 -0
  87. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/julia.rs +0 -0
  88. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/latex.rs +0 -0
  89. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/lua.rs +0 -0
  90. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/mod.rs +0 -0
  91. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/nim.rs +0 -0
  92. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/nix.rs +0 -0
  93. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/ocaml.rs +0 -0
  94. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/openapi.rs +0 -0
  95. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/perl.rs +0 -0
  96. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/php.rs +0 -0
  97. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/prisma.rs +0 -0
  98. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/protobuf.rs +0 -0
  99. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/r_lang.rs +0 -0
  100. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/ruby.rs +0 -0
  101. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/shell.rs +0 -0
  102. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/sql.rs +0 -0
  103. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/swift.rs +0 -0
  104. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/tags.rs +0 -0
  105. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/terraform.rs +0 -0
  106. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/semantic/zig.rs +0 -0
  107. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/similarity/lexical.rs +0 -0
  108. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/similarity/mod.rs +0 -0
  109. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/structural/containment.rs +0 -0
  110. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/structural/mod.rs +0 -0
  111. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/edges/structural/sibling.rs +0 -0
  112. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/excerpt.rs +0 -0
  113. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/filtering.rs +0 -0
  114. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/fragmentation.rs +0 -0
  115. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/graph_export.rs +0 -0
  116. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/interval.rs +0 -0
  117. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/languages.rs +0 -0
  118. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/lib.rs +0 -0
  119. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/main.rs +0 -0
  120. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/mode.rs +0 -0
  121. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/parsers/config_parser.rs +0 -0
  122. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/parsers/generic.rs +0 -0
  123. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/parsers/markdown.rs +0 -0
  124. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/parsers/mod.rs +0 -0
  125. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/parsers/tree_sitter_strategy.rs +0 -0
  126. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/paths.rs +0 -0
  127. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/peak_rss.rs +0 -0
  128. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/postpass.rs +0 -0
  129. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/ppr.rs +0 -0
  130. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/provenance.rs +0 -0
  131. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/signatures.rs +0 -0
  132. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/stopwords.rs +0 -0
  133. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/test_harness.rs +0 -0
  134. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/testfiles.rs +0 -0
  135. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/tokenizer.rs +0 -0
  136. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/types.rs +0 -0
  137. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/utility/boltzmann.rs +0 -0
  138. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/utility/importance.rs +0 -0
  139. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/utility/mod.rs +0 -0
  140. {diffctx-1.13.0 → diffctx-1.14.0}/crates/diffctx-native/src/utility/needs.rs +0 -0
  141. {diffctx-1.13.0 → diffctx-1.14.0}/rust-toolchain.toml +0 -0
  142. {diffctx-1.13.0 → diffctx-1.14.0}/src/diffctx/__init__.py +0 -0
  143. {diffctx-1.13.0 → diffctx-1.14.0}/src/diffctx/__main__.py +0 -0
  144. {diffctx-1.13.0 → diffctx-1.14.0}/src/diffctx/_native/__init__.py +0 -0
  145. {diffctx-1.13.0 → diffctx-1.14.0}/src/diffctx/_native/graph_analytics.py +0 -0
  146. {diffctx-1.13.0 → diffctx-1.14.0}/src/diffctx/_native/graph_export.py +0 -0
  147. {diffctx-1.13.0 → diffctx-1.14.0}/src/diffctx/_native/pipeline.py +0 -0
  148. {diffctx-1.13.0 → diffctx-1.14.0}/src/diffctx/_native/project_graph.py +0 -0
  149. {diffctx-1.13.0 → diffctx-1.14.0}/src/diffctx/clipboard.py +0 -0
  150. {diffctx-1.13.0 → diffctx-1.14.0}/src/diffctx/ignore.py +0 -0
  151. {diffctx-1.13.0 → diffctx-1.14.0}/src/diffctx/logger.py +0 -0
  152. {diffctx-1.13.0 → diffctx-1.14.0}/src/diffctx/mcp/README.md +0 -0
  153. {diffctx-1.13.0 → diffctx-1.14.0}/src/diffctx/mcp/__init__.py +0 -0
  154. {diffctx-1.13.0 → diffctx-1.14.0}/src/diffctx/mcp/__main__.py +0 -0
  155. {diffctx-1.13.0 → diffctx-1.14.0}/src/diffctx/mcp/fetch.py +0 -0
  156. {diffctx-1.13.0 → diffctx-1.14.0}/src/diffctx/mcp/formatting.py +0 -0
  157. {diffctx-1.13.0 → diffctx-1.14.0}/src/diffctx/mcp/security.py +0 -0
  158. {diffctx-1.13.0 → diffctx-1.14.0}/src/diffctx/mcp/server.py +0 -0
  159. {diffctx-1.13.0 → diffctx-1.14.0}/src/diffctx/py.typed +0 -0
  160. {diffctx-1.13.0 → diffctx-1.14.0}/src/diffctx/tokens.py +0 -0
  161. {diffctx-1.13.0 → diffctx-1.14.0}/src/diffctx/tree.py +0 -0
@@ -7,6 +7,73 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Fixed
11
+
12
+ - **Changed secret-classified files (`id_rsa`, `*.key`, `*.pem`, …) are now
13
+ disclosed in `policy_excluded_count`** (#214): they were silently dropped
14
+ before the ignore-exclusion disclosure ran, so a change touching only
15
+ secret paths printed a bare stub and a mixed change under-counted its
16
+ withheld files. Count only — the paths themselves stay out of the output,
17
+ same stance as `.diffctx/ignore`. The renderers' note now reads "withheld
18
+ by exclusion policy" to cover both families.
19
+ - **The compute deadline and the git subprocess timeout are per-run, not
20
+ process-global** (#210): concurrent MCP requests overwrote each other's
21
+ ceiling (a short-timeout request could abort a long-timeout one mid-flight,
22
+ and vice versa disable it), an expired ceiling was never cleared and leaked
23
+ into later in-process runs, and a single flooding edge builder could outrun
24
+ the whole timeout between checks. The deadline is now a value threaded
25
+ through the run, the git timeout is thread-scoped, and the known flooding
26
+ loops (config-key scan, C-family pairing, Python identifier emission) poll
27
+ the deadline inside the loop.
28
+ - A failed Aho-Corasick automaton build in the config-edge builder now logs a
29
+ warning instead of silently disabling the entire keyed channel (#216).
30
+
31
+ - **A commit touching only excluded paths now renders its disclosure** (#188
32
+ completion): the YAML/Markdown/text renderers and the CLI's empty-diff exit
33
+ treated `ignored_changes`/`policy_excluded_count` as "no output", so the one
34
+ case the disclosure exists for — every hunk withheld — printed a bare stub
35
+ and exited 4. All three renderers, `locate`, and the exit path now count the
36
+ disclosure as content.
37
+ - **Scala import with `}` before `{` no longer panics** the edge build
38
+ (`import a}.{B` in any `.scala`/`.sbt` line aborted the whole run).
39
+ - The corpus harness (`build_diff_context_in_memory`) now passes the per-file
40
+ admission gate to selection, matching the shipped pipeline; the full corpus
41
+ is bit-stable under the fix (2902 passed, baseline unchanged).
42
+ - An absurd `timeout` value can no longer wrap the compute deadline into the
43
+ past (saturating arithmetic); the token-cache eviction sweep no longer
44
+ deletes another process's in-flight temp file.
45
+ - Text renderer writes one path per line (a comma or newline inside a path
46
+ made the joined line unparseable).
47
+
48
+ ### Changed
49
+
50
+ - **New default operating point: per-file admission ON, `tau` 0.12 -> 0.05,
51
+ `core_budget_fraction` 0.5 -> 0.4** — the v5 evaluation cycle's validated
52
+ winner. A file whose only connection to the change is proximity (sibling
53
+ directory, shared rare token, lexical similarity) is no longer selectable as
54
+ context; it must be naming-reachable (an import/call/type-ref path from the
55
+ changed set, up to two hops, either direction). The weak stop then spends
56
+ the freed budget inside that admissible set. Evidence chain: 12-cell
57
+ screening grid (corpus known-failures 227 -> 196 with three xfails retiring;
58
+ the strict-tau admission variant was killed by a significant real-commit
59
+ recall loss), tau x cbf calibration + held-out validation (0.6815 vs 0.6684
60
+ for the old point), and a six-arm confirmation sweep on the three 500-case
61
+ test splits: file_recall parity with the old default, contextbench
62
+ file_precision +0.041 (bootstrap CI excludes zero), dcbench-372 paired
63
+ nontrivial recall within noise at -4% tokens. Opt out with
64
+ `DIFFCTX_FILE_ADMISSION=0` and `--tau 0.12` to reproduce the old behaviour
65
+ (#65).
66
+
67
+ - **Selection enforces a per-file budget ceiling** (#194): while other files
68
+ still have unplaced candidates, one file may not consume more than
69
+ `SELECTION.per_file_budget_fraction` (default 0.25) of the budget — the
70
+ reporter's +9k-line generated JSON no longer crowds 34 source files out of
71
+ the output. Blocked candidates are deferred, not dropped: once every other
72
+ file has had its chance the ceiling lifts, so a single-file change still
73
+ fills the whole budget.
74
+
75
+ ## [1.13.0] - 2026-08-04
76
+
10
77
  ### Added
11
78
 
12
79
  - **Inline `<script>` and `<style>` in HTML are parsed with their own
@@ -924,7 +991,8 @@ Earlier releases shipped as `treemapper`; see
924
991
  <https://github.com/nikolay-e/diffctx/releases> for the corresponding GitHub
925
992
  release notes (`1.0.0` through `1.6.1`).
926
993
 
927
- [Unreleased]: https://github.com/nikolay-e/diffctx/compare/v1.12.3...HEAD
994
+ [Unreleased]: https://github.com/nikolay-e/diffctx/compare/v1.13.0...HEAD
995
+ [1.13.0]: https://github.com/nikolay-e/diffctx/compare/v1.12.3...v1.13.0
928
996
  [1.12.3]: https://github.com/nikolay-e/diffctx/compare/v1.12.2...v1.12.3
929
997
  [1.12.2]: https://github.com/nikolay-e/diffctx/compare/v1.12.1...v1.12.2
930
998
  [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.4"
7
+ version = "1.1.5"
8
8
  source = "registry+https://github.com/rust-lang/crates.io-index"
9
- checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
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.4"
126
+ version = "4.6.6"
127
127
  source = "registry+https://github.com/rust-lang/crates.io-index"
128
- checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7"
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.2"
136
+ version = "4.6.6"
137
137
  source = "registry+https://github.com/rust-lang/crates.io-index"
138
- checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b"
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.13.0"
197
+ version = "1.14.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.0"
518
+ version = "0.29.2"
519
519
  source = "registry+https://github.com/rust-lang/crates.io-index"
520
- checksum = "cd274650b21d4bfc26a0a47587962c1edb425f69287324355cd040c3ea66071c"
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.0"
532
+ version = "0.29.2"
533
533
  source = "registry+https://github.com/rust-lang/crates.io-index"
534
- checksum = "c5e2a7d2f0d013342f295c048ad19237add5154a55b1c5a254c0ec93d4109078"
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.0"
541
+ version = "0.29.2"
542
542
  source = "registry+https://github.com/rust-lang/crates.io-index"
543
- checksum = "ca85c467da1bbc8d866eea5deff9cf29ea5f7785054a17da36e65bda9c05845b"
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.0"
551
+ version = "0.29.2"
552
552
  source = "registry+https://github.com/rust-lang/crates.io-index"
553
- checksum = "9ac53762fd065daa3194dd09337a38bd793a188100fd1a9304c4ab312d901771"
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.0"
563
+ version = "0.29.2"
564
564
  source = "registry+https://github.com/rust-lang/crates.io-index"
565
- checksum = "4ca3a1557399783172dc5bf39cfca835157732532cba56b71d2292161e53b362"
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.1"
751
+ version = "3.1.2"
752
752
  source = "registry+https://github.com/rust-lang/crates.io-index"
753
- checksum = "e6505efef05804732ed8a3f2d4f279429eb485bd69d5b0cc6b19cc02005cda16"
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.19"
819
+ version = "2.0.20"
820
820
  source = "registry+https://github.com/rust-lang/crates.io-index"
821
- checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9"
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.19"
828
+ version = "2.0.20"
829
829
  source = "registry+https://github.com/rust-lang/crates.io-index"
830
- checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd"
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.0"
1255
+ version = "0.26.2"
1256
1256
  source = "registry+https://github.com/rust-lang/crates.io-index"
1257
- checksum = "de5a4a7ff23a55474ce6a741d52aaeca7a82fe9421bb982b86e98c6ac8629397"
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.13.0
3
+ Version: 1.14.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.12 | Relevance threshold for full fragment content; lower-scoring fragments are stubbed or dropped (lower = more context) |
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.12,
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.12 | Relevance threshold for full fragment content; lower-scoring fragments are stubbed or dropped (lower = more context) |
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.12,
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.13.0"
3
+ version = "1.14.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.12 | Relevance threshold for full fragment content; lower-scoring fragments are stubbed or dropped (lower = more context) |
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.12,
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
- /// Confirmed by the v4 re-calibration on the EGO pipeline (5x3 grid,
53
- /// v1 calibration manifest): validated winner (tau, cbf) = (0.12, 0.5)
54
- /// at min(per_benchmark file_recall) = 0.6532, validation 0.6604.
55
- /// Surface flat and monotone (top-3 within 0.004) choice is robust.
56
- pub const DEFAULT_STOPPING_THRESHOLD: f64 = 0.12;
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
- // Confirmed by the v4 re-calibration on the EGO pipeline
13
- // (5x3 grid over the v1 calibration manifest): validated
14
- // winner (tau, cbf) = (0.12, 0.5) at min(per_benchmark
15
- // file_recall) = 0.6532. Surface is flat and monotone
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("DIFFCTX_OP_SELECTION_CORE_BUDGET_FRACTION", 0.5),
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
  }