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.
Files changed (161) hide show
  1. {diffctx-1.13.0 → diffctx-1.15.0}/CHANGELOG.md +112 -1
  2. {diffctx-1.13.0 → diffctx-1.15.0}/Cargo.lock +25 -25
  3. {diffctx-1.13.0 → diffctx-1.15.0}/PKG-INFO +3 -3
  4. {diffctx-1.13.0 → diffctx-1.15.0}/README.md +2 -2
  5. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/Cargo.toml +1 -1
  6. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/README.md +2 -2
  7. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/limits.rs +9 -5
  8. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/selection.rs +25 -8
  9. diffctx-1.15.0/crates/diffctx-native/src/deadline.rs +142 -0
  10. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/discovery.rs +0 -25
  11. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/base.rs +64 -11
  12. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/config_edges/generic.rs +115 -7
  13. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/mod.rs +81 -4
  14. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/c_family.rs +35 -22
  15. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/go.rs +113 -45
  16. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/javascript.rs +19 -0
  17. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/jvm.rs +87 -5
  18. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/python.rs +27 -1
  19. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/rust_lang.rs +122 -44
  20. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/structural/containment.rs +16 -8
  21. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/structural/testing.rs +11 -2
  22. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/git.rs +35 -30
  23. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/graph.rs +30 -2
  24. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/locate.rs +22 -10
  25. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/memory_pipeline.rs +4 -0
  26. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/parsers/tree_sitter_strategy.rs +51 -7
  27. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/pipeline.rs +30 -9
  28. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/project_graph.rs +1 -0
  29. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/pybridge.rs +4 -0
  30. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/render.rs +53 -15
  31. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/scoring.rs +57 -4
  32. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/select.rs +292 -51
  33. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/token_corpus.rs +17 -9
  34. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/utility/scoring.rs +2 -0
  35. {diffctx-1.13.0 → diffctx-1.15.0}/pyproject.toml +1 -1
  36. {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/_app.py +6 -0
  37. {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/_diffctx.pyi +1 -0
  38. {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/cli.py +2 -2
  39. diffctx-1.15.0/src/diffctx/version.py +1 -0
  40. {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/writer.py +37 -6
  41. diffctx-1.13.0/crates/diffctx-native/src/deadline.rs +0 -64
  42. diffctx-1.13.0/src/diffctx/version.py +0 -1
  43. {diffctx-1.13.0 → diffctx-1.15.0}/Cargo.toml +0 -0
  44. {diffctx-1.13.0 → diffctx-1.15.0}/LICENSE +0 -0
  45. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/analytics.rs +0 -0
  46. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/candidate_files.rs +0 -0
  47. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/analytics.rs +0 -0
  48. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/bm25.rs +0 -0
  49. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/budget.rs +0 -0
  50. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/category_weights.rs +0 -0
  51. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/edge_weights.rs +0 -0
  52. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/env_overrides.rs +0 -0
  53. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/extensions.rs +0 -0
  54. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/filtering.rs +0 -0
  55. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/fragmentation.rs +0 -0
  56. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/git.rs +0 -0
  57. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/graph_filtering.rs +0 -0
  58. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/importance.rs +0 -0
  59. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/mod.rs +0 -0
  60. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/mode.rs +0 -0
  61. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/needs.rs +0 -0
  62. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/parsers.rs +0 -0
  63. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/render.rs +0 -0
  64. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/scoring.rs +0 -0
  65. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/tokenization.rs +0 -0
  66. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/weights.rs +0 -0
  67. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/core.rs +0 -0
  68. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/config_edges/build_system.rs +0 -0
  69. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/config_edges/cicd.rs +0 -0
  70. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/config_edges/docker.rs +0 -0
  71. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/config_edges/helm.rs +0 -0
  72. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/config_edges/kubernetes.rs +0 -0
  73. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/config_edges/mod.rs +0 -0
  74. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/document/mod.rs +0 -0
  75. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/history/cochange.rs +0 -0
  76. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/history/mod.rs +0 -0
  77. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/ansible.rs +0 -0
  78. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/bazel.rs +0 -0
  79. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/cargo_edges.rs +0 -0
  80. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/clojure.rs +0 -0
  81. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/css.rs +0 -0
  82. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/dart.rs +0 -0
  83. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/dbt.rs +0 -0
  84. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/dotnet.rs +0 -0
  85. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/elixir.rs +0 -0
  86. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/erlang.rs +0 -0
  87. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/graphql.rs +0 -0
  88. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/haskell.rs +0 -0
  89. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/julia.rs +0 -0
  90. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/latex.rs +0 -0
  91. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/lua.rs +0 -0
  92. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/mod.rs +0 -0
  93. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/nim.rs +0 -0
  94. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/nix.rs +0 -0
  95. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/ocaml.rs +0 -0
  96. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/openapi.rs +0 -0
  97. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/perl.rs +0 -0
  98. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/php.rs +0 -0
  99. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/prisma.rs +0 -0
  100. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/protobuf.rs +0 -0
  101. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/r_lang.rs +0 -0
  102. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/ruby.rs +0 -0
  103. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/shell.rs +0 -0
  104. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/sql.rs +0 -0
  105. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/swift.rs +0 -0
  106. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/tags.rs +0 -0
  107. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/terraform.rs +0 -0
  108. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/zig.rs +0 -0
  109. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/similarity/lexical.rs +0 -0
  110. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/similarity/mod.rs +0 -0
  111. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/structural/mod.rs +0 -0
  112. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/structural/sibling.rs +0 -0
  113. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/excerpt.rs +0 -0
  114. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/filtering.rs +0 -0
  115. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/fragmentation.rs +0 -0
  116. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/graph_export.rs +0 -0
  117. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/interval.rs +0 -0
  118. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/languages.rs +0 -0
  119. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/lib.rs +0 -0
  120. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/main.rs +0 -0
  121. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/mode.rs +0 -0
  122. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/parsers/config_parser.rs +0 -0
  123. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/parsers/generic.rs +0 -0
  124. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/parsers/markdown.rs +0 -0
  125. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/parsers/mod.rs +0 -0
  126. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/paths.rs +0 -0
  127. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/peak_rss.rs +0 -0
  128. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/postpass.rs +0 -0
  129. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/ppr.rs +0 -0
  130. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/provenance.rs +0 -0
  131. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/signatures.rs +0 -0
  132. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/stopwords.rs +0 -0
  133. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/test_harness.rs +0 -0
  134. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/testfiles.rs +0 -0
  135. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/tokenizer.rs +0 -0
  136. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/types.rs +0 -0
  137. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/utility/boltzmann.rs +0 -0
  138. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/utility/importance.rs +0 -0
  139. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/utility/mod.rs +0 -0
  140. {diffctx-1.13.0 → diffctx-1.15.0}/crates/diffctx-native/src/utility/needs.rs +0 -0
  141. {diffctx-1.13.0 → diffctx-1.15.0}/rust-toolchain.toml +0 -0
  142. {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/__init__.py +0 -0
  143. {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/__main__.py +0 -0
  144. {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/_native/__init__.py +0 -0
  145. {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/_native/graph_analytics.py +0 -0
  146. {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/_native/graph_export.py +0 -0
  147. {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/_native/pipeline.py +0 -0
  148. {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/_native/project_graph.py +0 -0
  149. {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/clipboard.py +0 -0
  150. {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/ignore.py +0 -0
  151. {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/logger.py +0 -0
  152. {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/mcp/README.md +0 -0
  153. {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/mcp/__init__.py +0 -0
  154. {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/mcp/__main__.py +0 -0
  155. {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/mcp/fetch.py +0 -0
  156. {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/mcp/formatting.py +0 -0
  157. {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/mcp/security.py +0 -0
  158. {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/mcp/server.py +0 -0
  159. {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/py.typed +0 -0
  160. {diffctx-1.13.0 → diffctx-1.15.0}/src/diffctx/tokens.py +0 -0
  161. {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.12.3...HEAD
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.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.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.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.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.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.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.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
  }