diffctx 1.15.0__tar.gz → 1.17.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. {diffctx-1.15.0 → diffctx-1.17.0}/CHANGELOG.md +602 -6
  2. {diffctx-1.15.0 → diffctx-1.17.0}/Cargo.lock +83 -13
  3. diffctx-1.17.0/Cargo.toml +25 -0
  4. {diffctx-1.15.0 → diffctx-1.17.0}/PKG-INFO +68 -38
  5. {diffctx-1.15.0 → diffctx-1.17.0}/README.md +65 -16
  6. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/Cargo.toml +13 -5
  7. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/README.md +65 -16
  8. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/analytics.rs +209 -226
  9. diffctx-1.17.0/crates/diffctx-native/src/candidate_files.rs +201 -0
  10. diffctx-1.17.0/crates/diffctx-native/src/change_class.rs +169 -0
  11. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/budget.rs +6 -0
  12. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/category_weights.rs +4 -5
  13. diffctx-1.17.0/crates/diffctx-native/src/config/edge_weights.rs +165 -0
  14. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/limits.rs +5 -0
  15. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/scoring.rs +2 -6
  16. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/weights.rs +94 -198
  17. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/base.rs +230 -48
  18. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/config_edges/build_system.rs +8 -16
  19. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/config_edges/cicd.rs +98 -38
  20. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/config_edges/docker.rs +99 -36
  21. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/config_edges/generic.rs +8 -3
  22. diffctx-1.17.0/crates/diffctx-native/src/edges/config_edges/kubernetes.rs +951 -0
  23. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/history/cochange.rs +2 -6
  24. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/mod.rs +232 -14
  25. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/ansible.rs +25 -19
  26. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/bazel.rs +16 -18
  27. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/c_family.rs +52 -91
  28. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/cargo_edges.rs +21 -35
  29. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/clojure.rs +22 -51
  30. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/css.rs +13 -24
  31. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/dart.rs +24 -109
  32. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/dbt.rs +33 -56
  33. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/dotnet.rs +34 -128
  34. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/elixir.rs +19 -45
  35. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/erlang.rs +21 -52
  36. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/go.rs +237 -65
  37. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/graphql.rs +18 -43
  38. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/haskell.rs +27 -123
  39. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/javascript.rs +26 -88
  40. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/julia.rs +20 -42
  41. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/jvm.rs +176 -145
  42. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/latex.rs +14 -21
  43. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/lua.rs +18 -44
  44. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/nim.rs +20 -89
  45. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/nix.rs +13 -20
  46. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/ocaml.rs +22 -46
  47. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/openapi.rs +20 -34
  48. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/perl.rs +21 -63
  49. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/php.rs +17 -41
  50. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/prisma.rs +25 -45
  51. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/protobuf.rs +19 -43
  52. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/python.rs +197 -120
  53. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/r_lang.rs +23 -82
  54. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/ruby.rs +17 -39
  55. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/rust_lang.rs +169 -173
  56. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/shell.rs +25 -21
  57. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/sql.rs +18 -34
  58. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/swift.rs +19 -43
  59. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/terraform.rs +15 -57
  60. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/zig.rs +23 -80
  61. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/similarity/lexical.rs +0 -4
  62. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/structural/containment.rs +1 -27
  63. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/structural/sibling.rs +2 -9
  64. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/structural/testing.rs +21 -30
  65. diffctx-1.17.0/crates/diffctx-native/src/effective_config.rs +451 -0
  66. diffctx-1.17.0/crates/diffctx-native/src/facts/mod.rs +28 -0
  67. diffctx-1.17.0/crates/diffctx-native/src/facts/scala.rs +235 -0
  68. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/fragmentation.rs +260 -91
  69. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/git.rs +656 -154
  70. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/graph.rs +49 -15
  71. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/graph_export.rs +6 -16
  72. diffctx-1.17.0/crates/diffctx-native/src/in_memory_harness.rs +501 -0
  73. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/languages.rs +138 -6
  74. diffctx-1.17.0/crates/diffctx-native/src/lib.rs +70 -0
  75. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/locate.rs +91 -48
  76. diffctx-1.17.0/crates/diffctx-native/src/main.rs +476 -0
  77. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/mode.rs +26 -65
  78. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/parsers/config_parser.rs +6 -87
  79. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/parsers/markdown.rs +1 -9
  80. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/parsers/mod.rs +8 -0
  81. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/parsers/tree_sitter_strategy.rs +389 -901
  82. diffctx-1.17.0/crates/diffctx-native/src/paths.rs +209 -0
  83. diffctx-1.17.0/crates/diffctx-native/src/pipeline.rs +2240 -0
  84. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/postpass.rs +118 -18
  85. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/ppr.rs +6 -7
  86. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/project_graph.rs +25 -78
  87. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/pybridge.rs +231 -235
  88. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/render.rs +366 -61
  89. diffctx-1.17.0/crates/diffctx-native/src/resource.rs +465 -0
  90. diffctx-1.17.0/crates/diffctx-native/src/run_provenance.rs +126 -0
  91. diffctx-1.17.0/crates/diffctx-native/src/sanitize.rs +183 -0
  92. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/scoring.rs +188 -144
  93. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/select.rs +548 -61
  94. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/signatures.rs +34 -7
  95. diffctx-1.17.0/crates/diffctx-native/src/stopwords/code.txt +79 -0
  96. diffctx-1.17.0/crates/diffctx-native/src/stopwords/docs.txt +8 -0
  97. diffctx-1.17.0/crates/diffctx-native/src/stopwords.rs +112 -0
  98. diffctx-1.17.0/crates/diffctx-native/src/test_rng.rs +15 -0
  99. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/token_corpus.rs +5 -2
  100. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/tokenizer.rs +83 -2
  101. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/types.rs +20 -5
  102. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/utility/boltzmann.rs +25 -34
  103. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/utility/mod.rs +1 -2
  104. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/utility/needs.rs +44 -62
  105. diffctx-1.15.0/crates/diffctx-native/src/utility/scoring.rs → diffctx-1.17.0/crates/diffctx-native/src/utility/objective.rs +6 -28
  106. {diffctx-1.15.0 → diffctx-1.17.0}/pyproject.toml +24 -66
  107. diffctx-1.17.0/rust-toolchain.toml +12 -0
  108. {diffctx-1.15.0 → diffctx-1.17.0}/src/diffctx/__init__.py +5 -14
  109. {diffctx-1.15.0 → diffctx-1.17.0}/src/diffctx/_app.py +97 -30
  110. {diffctx-1.15.0 → diffctx-1.17.0}/src/diffctx/_diffctx.pyi +21 -17
  111. {diffctx-1.15.0 → diffctx-1.17.0}/src/diffctx/_native/graph_analytics.py +7 -3
  112. {diffctx-1.15.0 → diffctx-1.17.0}/src/diffctx/_native/pipeline.py +45 -50
  113. diffctx-1.17.0/src/diffctx/_native/project_graph.py +39 -0
  114. {diffctx-1.15.0 → diffctx-1.17.0}/src/diffctx/cli.py +136 -38
  115. {diffctx-1.15.0 → diffctx-1.17.0}/src/diffctx/ignore.py +23 -21
  116. diffctx-1.17.0/src/diffctx/mcp/fetch.py +270 -0
  117. diffctx-1.17.0/src/diffctx/mcp/legacy.py +272 -0
  118. {diffctx-1.15.0 → diffctx-1.17.0}/src/diffctx/mcp/security.py +15 -4
  119. diffctx-1.17.0/src/diffctx/mcp/server.py +304 -0
  120. {diffctx-1.15.0 → diffctx-1.17.0}/src/diffctx/tree.py +54 -15
  121. diffctx-1.17.0/src/diffctx/version.py +1 -0
  122. {diffctx-1.15.0 → diffctx-1.17.0}/src/diffctx/writer.py +332 -50
  123. diffctx-1.15.0/Cargo.toml +0 -20
  124. diffctx-1.15.0/crates/diffctx-native/src/candidate_files.rs +0 -110
  125. diffctx-1.15.0/crates/diffctx-native/src/config/edge_weights.rs +0 -257
  126. diffctx-1.15.0/crates/diffctx-native/src/deadline.rs +0 -142
  127. diffctx-1.15.0/crates/diffctx-native/src/edges/config_edges/kubernetes.rs +0 -552
  128. diffctx-1.15.0/crates/diffctx-native/src/lib.rs +0 -39
  129. diffctx-1.15.0/crates/diffctx-native/src/main.rs +0 -354
  130. diffctx-1.15.0/crates/diffctx-native/src/memory_pipeline.rs +0 -369
  131. diffctx-1.15.0/crates/diffctx-native/src/paths.rs +0 -64
  132. diffctx-1.15.0/crates/diffctx-native/src/pipeline.rs +0 -1406
  133. diffctx-1.15.0/crates/diffctx-native/src/stopwords.rs +0 -990
  134. diffctx-1.15.0/crates/diffctx-native/src/test_harness.rs +0 -406
  135. diffctx-1.15.0/rust-toolchain.toml +0 -4
  136. diffctx-1.15.0/src/diffctx/_native/project_graph.py +0 -36
  137. diffctx-1.15.0/src/diffctx/mcp/README.md +0 -127
  138. diffctx-1.15.0/src/diffctx/mcp/fetch.py +0 -211
  139. diffctx-1.15.0/src/diffctx/mcp/formatting.py +0 -9
  140. diffctx-1.15.0/src/diffctx/mcp/server.py +0 -533
  141. diffctx-1.15.0/src/diffctx/version.py +0 -1
  142. {diffctx-1.15.0 → diffctx-1.17.0}/LICENSE +0 -0
  143. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/analytics.rs +0 -0
  144. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/bm25.rs +0 -0
  145. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/env_overrides.rs +0 -0
  146. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/extensions.rs +0 -0
  147. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/filtering.rs +0 -0
  148. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/fragmentation.rs +0 -0
  149. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/git.rs +0 -0
  150. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/graph_filtering.rs +0 -0
  151. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/importance.rs +0 -0
  152. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/mod.rs +0 -0
  153. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/mode.rs +0 -0
  154. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/needs.rs +0 -0
  155. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/parsers.rs +0 -0
  156. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/render.rs +0 -0
  157. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/selection.rs +0 -0
  158. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/tokenization.rs +0 -0
  159. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/core.rs +0 -0
  160. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/discovery.rs +0 -0
  161. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/config_edges/helm.rs +0 -0
  162. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/config_edges/mod.rs +0 -0
  163. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/document/mod.rs +0 -0
  164. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/history/mod.rs +0 -0
  165. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/mod.rs +0 -0
  166. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/tags.rs +0 -0
  167. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/similarity/mod.rs +0 -0
  168. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/structural/mod.rs +0 -0
  169. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/excerpt.rs +0 -0
  170. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/filtering.rs +0 -0
  171. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/interval.rs +0 -0
  172. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/parsers/generic.rs +0 -0
  173. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/peak_rss.rs +0 -0
  174. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/provenance.rs +0 -0
  175. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/testfiles.rs +0 -0
  176. {diffctx-1.15.0 → diffctx-1.17.0}/crates/diffctx-native/src/utility/importance.rs +0 -0
  177. {diffctx-1.15.0 → diffctx-1.17.0}/src/diffctx/__main__.py +0 -0
  178. {diffctx-1.15.0 → diffctx-1.17.0}/src/diffctx/_native/__init__.py +0 -0
  179. {diffctx-1.15.0 → diffctx-1.17.0}/src/diffctx/_native/graph_export.py +0 -0
  180. {diffctx-1.15.0 → diffctx-1.17.0}/src/diffctx/clipboard.py +0 -0
  181. {diffctx-1.15.0 → diffctx-1.17.0}/src/diffctx/logger.py +0 -0
  182. {diffctx-1.15.0 → diffctx-1.17.0}/src/diffctx/mcp/__init__.py +0 -0
  183. {diffctx-1.15.0 → diffctx-1.17.0}/src/diffctx/mcp/__main__.py +0 -0
  184. {diffctx-1.15.0 → diffctx-1.17.0}/src/diffctx/py.typed +0 -0
  185. {diffctx-1.15.0 → diffctx-1.17.0}/src/diffctx/tokens.py +0 -0
@@ -7,6 +7,599 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.17.0] - 2026-09-23
11
+
12
+ ### Added
13
+
14
+ - **Windows and Intel-Mac are release targets.** Every test matrix runs on
15
+ Linux, macOS and Windows on x64 and ARM; wheels and binaries ship for
16
+ `x86_64-apple-darwin` and `aarch64-pc-windows-msvc` as well, and `npm
17
+ install diffctx` and the Scoop bucket select them. What that exposed is
18
+ fixed: `--output-file` on Windows, backslashes in MCP output and in the
19
+ Rust path matching (discovery, C#/Go/Cargo edges), datasets checked out
20
+ byte-exact under a CRLF `autocrlf`, and a `--timeout 0` that now always
21
+ reports the timeout instead of racing the subprocess.
22
+ - **The landing page installs and opens offline** — a web manifest, icons
23
+ and a service worker that keeps every visit network-first.
24
+ - **Free-threaded CPython gets a wheel.** Each platform ships a `cp314t`
25
+ wheel beside the `abi3` one; `uvx` on 3.14t no longer compiles the sdist
26
+ (which timed MCP clients out), and the extension keeps the GIL off.
27
+ - **The GitHub Action works on a default shallow checkout**: with no
28
+ `diff-range` it fetches the pull request's base and head (or `HEAD~1`)
29
+ itself, so `fetch-depth: 0` is no longer required.
30
+ - **`provenance.engine.build` names the release commit** in wheels, binaries
31
+ and the image.
32
+ - **A glossary line under the scoring modes** on the landing page: three
33
+ independent first-visit probes stalled on `ego-network`, `PPR`, `rrf`,
34
+ `pit`, `τ` and `o200k_base` with nothing on the page defining them.
35
+
36
+ ### Removed
37
+
38
+ - **`build_diff_context(ignore_file=…, whitelist_file=…, no_default_ignores=…)`.**
39
+ The engine never applied a custom path-spec layer in diff mode and the three
40
+ arguments existed only to raise `NotImplementedError`; passing one is now a
41
+ `TypeError`. `.gitignore` and `.diffctx/ignore` still apply.
42
+
43
+ ### Fixed
44
+
45
+ - **The MCP server no longer hangs on Windows.** Every git child inherited the
46
+ server's stdin, which under an MCP client is the JSON-RPC pipe the client
47
+ holds open, and on Windows such a git never exited: any tool call that reached
48
+ git hung. Child processes now get a null stdin in both the engine and the
49
+ Python layer.
50
+ - **A long range says how many commits it held.** The commit list stops at
51
+ the newest twenty; the artifact read "20 commits" over a 42-commit range. It
52
+ now carries `commit_count` and reads "newest 20 of 42 commits" when the list
53
+ is cut, in both CLIs, pack and locate.
54
+ - **`--budget` fitting never passes context off as the change.** The witness
55
+ clip shortened any file's last fragment, so a context file shrank line by line
56
+ under a "more lines of this change" marker before it dropped; only a changed
57
+ fragment is shortened now.
58
+ - **MCP `fragment_ids` on a bare `diff_ref` read the code the change
59
+ replaced.** `HEAD` (or `HEAD~3`, or a duration window) is git's
60
+ base-versus-working-tree, so the locate ranking describes the files on
61
+ disk; the fetch read the base blob instead and returned the old body under
62
+ the ranking's own id. Bodies for a `..`-less ref now come from the working
63
+ tree, and a file the named revision lacks (or a revision git cannot
64
+ resolve) is reported as not found instead of being substituted with the
65
+ working-tree file under a historical label.
66
+ - **The native binary fits the written artifact to `--budget`** (#277).
67
+ npm, Scoop, Docker and the release archives ship the Rust binary, which
68
+ documented `--budget` as a cap on the whole artifact and enforced nothing on
69
+ the document it wrote: 9,121 tokens at `--budget 8000` on a real range. It
70
+ now runs the same render-count-drop loop as the Python CLI (context from the
71
+ tail first, a changed file's second fragment next, mechanical witnesses
72
+ before hand-written ones), records `selection_budget_exceeded` in
73
+ `coverage`, and exits 4 when nothing fits — the two CLIs deliver one set.
74
+ - **Eval harness: gold paths git had quoted** (`"b/caf\303\251.py"`) were
75
+ taken literally, so every non-ASCII gold file scored as a miss; the patch
76
+ header parser unquotes them. A checkpoint whose last line a kill left
77
+ unterminated no longer swallows the next appended row on resume.
78
+
79
+ ### Changed
80
+
81
+ - The GitHub Action's `python-version` defaults to `3.13` (`--output-file`
82
+ needs `os.fchmod`, which Windows Python gained in 3.13); the action
83
+ reference page says so. `--budget --help` names what `auto` resolves to.
84
+
85
+ ## [1.16.0] - 2026-09-16
86
+
87
+ ### Added
88
+
89
+ - **A FAQ and a benchmarks page** (#151): `docs/product/faq.md` answers the
90
+ recurring critiques (heuristic or oracle, the `tree-sitter` extra that no
91
+ longer exists, whose tokens `--budget` counts, MCP versus a prompt builder,
92
+ monorepo limits, the raw diff, secrets) and `BENCHMARKS.md` carries every
93
+ published number with what it was measured on and how to reproduce it.
94
+ - **A command-line reference page** (#240): `docs/product/cli.md` lists every
95
+ flag with its default and meaning, rendered from the parsers by
96
+ `scripts/update_cli_reference.py` and pinned by a test so it cannot drift
97
+ from `diffctx --help`. Linked from the site's nav, `llms.txt` and the
98
+ README.
99
+ - **Every changed file gets a witness before any file gets a second one**
100
+ (#263). Selection now opens with an evidence floor: one representation per
101
+ changed file — the same fragment the relevance pass would place, its
102
+ stand-in when that does not fit, a clipped head of the change when nothing
103
+ does — ordered by change class (hand-written content before mechanical
104
+ version/tag bumps) and then by path. Relevance and density compete only
105
+ for what is left. A range of nine one-line image-tag bumps plus four
106
+ rewritten manifests at `--budget 4000` now keeps all thirteen; before, the
107
+ four manifests were dropped in favour of the bumps. A file that still gets
108
+ nothing marks the artifact `coverage.status: degraded` with
109
+ `evidence_budget_exceeded`.
110
+ - **The rendered document is what `--budget` bounds** (#259). Every
111
+ renderer — JSON, YAML, Markdown, text, the CLI and the Python `to_*`
112
+ functions — counts its own output and drops context fragments from the
113
+ tail (a changed fragment only when no context is left) until it fits,
114
+ noting `selection_budget_exceeded` in coverage. The engine's envelope
115
+ estimate still shapes what selection admits, but it no longer decides
116
+ whether the artifact honours the cap.
117
+ - **The artifact inventories every changed file and says which ones it
118
+ left out** (#263). `changes` carries one row per changed file — its
119
+ `class` (`content`, `unknown`, `mechanical`: one-line version/tag/digest
120
+ edits of the kind an image updater writes, `generated`), the `reason`, and
121
+ `represented`, false when no fragment of the file is in the output. JSON,
122
+ YAML, the Python dict, Markdown and text all say it; `locate`'s coverage
123
+ lists `unrepresented_changed_files`. An incomplete context is no longer
124
+ indistinguishable from a complete one on any surface.
125
+ - **The artifact carries the commit messages of the range, whole.**
126
+ `commit_messages` lists every message — subject and body, up to 20
127
+ commits, 2 000 characters each — newest first; Markdown and text render
128
+ them instead of the subject of whichever commit happened to be last, and
129
+ every message feeds the query expansion, not only the head's subject.
130
+ `commit_message` stays the head's subject for existing readers. The list is
131
+ bounded by the budget — every subject, bodies newest-first while they fit a
132
+ tenth of it — and, like the inventory rows, charged before selection.
133
+ - **One artifact, `diffctx.context.v1`, on every surface.** JSON and YAML
134
+ output open with `schema: diffctx.context.v1`; the document is generated
135
+ from the engine's type and pinned as `schemas/diffctx.context.v1.json`
136
+ (JSON Schema 2020-12, a test regenerates and compares it). The Python dict
137
+ is that same serialization — the hand-written `set_item` copy of the
138
+ shape, the one that never ran on the CLI path, is gone (#229).
139
+ - **Every run records what produced it.** JSON and YAML output (and the
140
+ Python dict) carry a `provenance` block — `diffctx.provenance.v1`: engine
141
+ version, the input revisions as object ids, the resolved effective
142
+ configuration (`diffctx.effective_config.v1`: every parameter and every
143
+ `DIFFCTX_*` override that shaped the selection) and its 64-bit hash, the
144
+ selection parameters actually used (budget, tau, gate). The full record —
145
+ every parameter, the tokenizer, the resource caps, ~500 tokens — is
146
+ opt-in with `DIFFCTX_PROVENANCE=full`. Two runs that differ can now say
147
+ what differed between them.
148
+ - **`DIFFCTX_EVAL_STRICT=1`** refuses to start when the environment carries a
149
+ `DIFFCTX_*` name the effective configuration does not know — an evaluation
150
+ row shaped by an undeclared knob is a number nobody can reproduce.
151
+ - **`DIFFCTX_TOKEN_SAFETY_FACTOR`** (≥ 1.0, default 1.0) scales every token
152
+ count for consumers whose model tokenizes denser than `o200k_base`;
153
+ `--budget N` stays "N o200k_base accounting tokens" and the factor is in
154
+ provenance. No model-specific default is shipped.
155
+ - **A run that hits a limit says so instead of failing.** `--timeout` is
156
+ cooperative now: the phase it interrupts stops at its next bounded unit (a
157
+ file, an edge builder, a diffusion batch) and the run renders what it has,
158
+ with a `coverage` block — `status: partial`, `limit_reasons`, and the
159
+ resources consumed — on JSON, YAML, Markdown, text, the Python dict and
160
+ `locate`'s coverage. The same vocabulary covers the new memory caps:
161
+ `DIFFCTX_MAX_EDGE_CONTRIBUTIONS` (20M, counted before deduplication — the
162
+ number that owned 11 GB on a 10k-module monorepo, #196),
163
+ `DIFFCTX_MAX_SOURCE_BYTES` (256 MB of parsed source, never the changed
164
+ files themselves), `DIFFCTX_MAX_CANDIDATE_FILES` (200k) and
165
+ `DIFFCTX_MAX_NEEDS` (4k: every candidate is scored against every
166
+ information need mined from the diff, and a repository-sized diff produced
167
+ a need set that made selection take 413 s on this repository's own
168
+ history — 6 s under the cap). The caps are part of the effective
169
+ configuration and its hash.
170
+
171
+ ### Security
172
+
173
+ - **Credential-shaped strings are redacted on every read surface** (#147).
174
+ A last pass over the diff artifact (fragments, commit messages), the
175
+ `--with-raw-diff` bundle, the MCP fetch and glob readers and tree mode
176
+ replaces strings matching a small set of high-confidence shapes — AWS
177
+ access key ids, GitHub, Slack, Stripe, Google and OpenAI keys, JWTs, PEM
178
+ private-key blocks — with `[REDACTED:<category>]`; the artifact carries a
179
+ `redactions` block (count, categories) and `sanitization_redaction` among
180
+ its coverage limit reasons. Defence in depth, not a guarantee: only shapes
181
+ that cannot be anything else are matched, and SECURITY.md says what is not
182
+ caught. The withhold policy (secret-by-name files, ignore rules) is
183
+ unchanged and remains the mechanism to rely on.
184
+ - **A `diff_ref` could turn the read-only MCP fetch into a file write.** With
185
+ `fragment_ids` set the engine never sees `diff_ref`, and `fetch_fragments`
186
+ passed whatever followed `..` straight to `git show <rev>:<path>` — so
187
+ `HEAD..--output=/tmp/x` wrote a file. A rev starting with `-` is refused and
188
+ `git show` runs with `--end-of-options`.
189
+ - **An explicit file argument bypassed the secret-path floor.** The tree walk
190
+ applies `is_secret_path` to every entry, but `diffctx id_rsa` and
191
+ `diffctx '**/*'` fed the expanded paths straight to the reader; the same
192
+ policy now applies to them, with a one-line notice of how many were
193
+ withheld.
194
+ - **The MCP allow-list is checked before the filesystem is.** `validate_repo_path`
195
+ probed `is_dir()` and walked up to `.git` before consulting
196
+ `DIFFCTX_ALLOWED_PATHS`, so a refused caller learned whether a path existed;
197
+ the order now matches what SECURITY.md promised.
198
+ - **The scratch git dir for non-repository reads is created exclusively.** Its
199
+ name was `temp_dir/diffctx-scratch-git-<pid>-<n>`, guessable, and `git init`
200
+ adopts an existing `.git` — a pre-planted one with its own `excludesFile`
201
+ decided what the reader called ignored. Created with `create_dir` (0700), a
202
+ collision moves to the next name.
203
+ - **A `.diffctx/ignore` policy that could not be read failed open.** When
204
+ `ls-files` or a pattern file read failed, the lookup reported "no patterns
205
+ declared" and a subsequent `check-ignore` failure landed on the fail-open
206
+ branch. Discovery failure now fails closed; a bare repository, which cannot
207
+ carry a policy, is the one exception.
208
+
209
+ - **An ignored directory's contents were readable through the MCP glob**
210
+ (regression introduced with #228, caught the same day). `withheld_paths`
211
+ reused the engine's *attribution* lookup, which deliberately drops
212
+ ancestor-inherited exclusions — a tracked file under a directory that
213
+ `--no-index` reports excluded is not really ignored (#153). For the diff,
214
+ whose paths are tracked by construction, that is right; for a reader walking
215
+ the working tree it is exactly wrong, because `.venv/x.py` is ignored *by*
216
+ the `.venv/` rule on its parent. `git check-ignore` called those paths
217
+ ignored while the engine called them servable, so `get_file_context` and
218
+ `diffctx_context` would return the contents of `.venv/`, `dist/`, `target/`
219
+ and any repository's `secrets/`. Fixed with a lookup that keeps
220
+ ancestor-inherited matches; `.git/` is withheld outright — it is not
221
+ gitignored at all, and `.git/config` carries the remote URL.
222
+ - **The glob reader applies the tree-mode noise filter again**: unifying it
223
+ onto the engine alone (#228) also dropped `node_modules/`, `target/` and
224
+ lock files from `get_file_context`, contradicting the tool's own
225
+ description. Both filters now apply there — the noise spec `get_tree_map`
226
+ uses, and the engine as the security floor. `fetch_fragments` keeps the
227
+ engine alone, which is what makes a ranked `build/gen.py` fetchable.
228
+
229
+ - **One secret-path policy for every surface** (#227): tree mode kept its own,
230
+ shorter list, so `.netrc`, `credentials`, `_netrc`, `.npmrc`, `.pypirc`,
231
+ `id_*_sk`, `*.ppk`, `*.p8` and `*.asc` — all withheld by diff mode — were
232
+ printed in full by `diffctx .`. Tree mode now asks the engine
233
+ (`_diffctx.is_secret_path`), unconditionally: `--no-default-ignores` drops
234
+ the noise filters, never the secret policy, the same contract diff mode has.
235
+ - **The MCP fetch refuses exactly what the engine withholds** (#228). It
236
+ re-derived admissibility with pathspec, so it served a `.netrc` the
237
+ selection had refused and refused a `build/gen.py` the selection had
238
+ ranked. `fragment_ids` and the legacy glob reader now ask the engine
239
+ (`_diffctx.withheld_paths`: secret by name, or ignored as `git check-ignore`
240
+ resolves `.gitignore` / `.diffctx/ignore`) in one batched call. Outside a
241
+ git work tree — the legacy reader accepts any directory — git runs with a
242
+ scratch `--git-dir`, so `.diffctx/ignore` binds there too.
243
+
244
+ ### Fixed
245
+
246
+ - **A `---` bundle is every resource it holds, not its first** (#258). The
247
+ Kubernetes edge builder now reads one view per YAML document, so a
248
+ Service in the third document is indexed as a Service, its selector meets
249
+ the Deployment in the second, and two Deployments' labels no longer
250
+ collapse into one last-wins map. A generated manifest keeps its
251
+ `apiVersion:`/`kind:`/`metadata:` pairs through the generated-file cut,
252
+ which used to drop them as the shortest fragments and with them the
253
+ detection that made the file a manifest at all.
254
+ - **Scala imports, packages and inheritance come off the parse tree**
255
+ (#243). `tree-sitter-scala` already parsed every Scala file for
256
+ fragmentation; the edge layer re-read the text with a regex on its fifth
257
+ patch. `facts::scala` now walks `import_declaration` (braces, `=>` and
258
+ `as` renames, `_`/`*`/`given` wildcards, multi-line groups),
259
+ `package_clause` chains and `extends_clause` types; the regex reader
260
+ stays as the fallback for text that does not parse cleanly, and the two
261
+ are held to agree on every shape the tests name. An import inside a
262
+ block comment is no longer an import.
263
+ - **The compose `context:` and CI→`package.json` channels revived on
264
+ 2026-09-02 were dead on arrival.** Both built their path reference from the
265
+ fragment's absolute path, while the fragment index is keyed repo-relative,
266
+ so nothing matched — and the CI walk, climbing to `/`, degraded to the bare
267
+ name and linked every `package.json` again. Both resolve in the index's own
268
+ key space now, the CI walk matches one exact path per level, and each
269
+ builder has a test with absolute paths and a repo root, the shape
270
+ production uses. Q-class: one overflow candidate moves on the bitcheck
271
+ fixture (its workflows run `npm`), nothing in the selection.
272
+ - **A plain directory whose scratch `git init` failed withheld every file**:
273
+ "no git dir" was read as "policy unreadable" and failed closed. It has no
274
+ policy to read; gitignore filtering is skipped there, secret-by-name still
275
+ applies.
276
+ - **A blank line after `selector:` let sibling keys back into the label
277
+ block**; the indentation bar is taken from the first line carrying a key.
278
+ - **`envelope_tokens` was computed before the lockfile/ignored lists it
279
+ summarises were set** on the empty-selection path.
280
+ - **`TestSetReport.n` reports instances attempted**, not survivors, so a cell
281
+ that failed on every instance no longer reads as "ran fine, recall 0".
282
+ - **The dev-loop harness honours `DIFFCTX_YAML_MIN_SCORE`** like the CI gate,
283
+ and `compare` / `aggregate-seeds` keep their exit code when run as scripts.
284
+
285
+ - **`stratified-analysis` returned no rows at all**: the 2026-09-01 fix that
286
+ taught it the multi-budget checkpoint layout left the `rows.append` under a
287
+ `continue` and the `return` inside the loop, so the first cell's rows were
288
+ never appended and every later cell was never visited. Sonar's
289
+ unreachable-code rule caught it; a test now feeds it two cells in both
290
+ layouts and expects every row back — it fails on the previous code.
291
+ - **The demo names what it left out.** Its output panel showed the selection
292
+ and nothing else; the real artifact lists omitted changed files and locate
293
+ mode reports overflow. The toy now prints which fragments did not fit the
294
+ budget and how many fell below τ, and a direct click on stage 5 renders
295
+ the selection instead of "0 tokens".
296
+
297
+ - **Edge builders that linked the wrong thing, or everything.** A compose
298
+ file's `context: .` stripped to `.` and `path.contains(".")` matched every
299
+ file with an extension — the compose file linked to the whole universe at
300
+ naming weight; `.` now means the compose file's own directory and a named
301
+ context takes the path channel's ambiguity bar. A Service's selector regex
302
+ kept consuming less-indented sibling keys (`type: ClusterIP`) and made each
303
+ a required label, so kubectl-ordered Services never matched their workload;
304
+ label blocks now end where their indentation does. `envFrom:
305
+ configMapRef/secretRef` — the standard way to inject a whole map — was not
306
+ a recognised reference. A shared base image linked every workload pairwise
307
+ at naming weight; the image channel takes the same eight-file bar as every
308
+ other reference channel and never links a manifest to itself. A CI file
309
+ mentioning `npm` linked to every `package.json` in the repository; it links
310
+ to the nearest one above it. A bare-name reference (`config.yaml`,
311
+ `main.go`) linked to the first fragment of the first same-named file; it
312
+ links each such file's representative, abstaining above the bar, and a
313
+ module directory (`import Database`) still resolves as a path. Cargo
314
+ entry-point edges landed on the last fragment of the target file instead
315
+ of its representative, and Ansible role siblings were emitted
316
+ fragment × fragment (22k emissions for two task files) — now one edge per
317
+ file pair through representatives, capped like directory siblings. All
318
+ Q-class; corpus 2902/2902, no baseline movement.
319
+ - **The eval harness's multi-budget checkpoints were invisible to
320
+ `stratified-analysis` and `backfill-checkpoints`**: both globbed only the
321
+ flat `<set>.checkpoint.jsonl`, so every diffctx cell of a full sweep
322
+ contributed zero rows. One discovery helper serves all three readers.
323
+ `--repos-dir` now relocates the bare-clone cache too (it read an
324
+ import-time constant and sent clones to the home directory while the disk
325
+ probe measured the flag's volume); `run-final` names its checkpoint after
326
+ the ablation parameters so cells sharing an `--out` stop resuming each
327
+ other's rows; `clone_fail`/`error` rows are re-evaluated on resume instead
328
+ of being checkpointed as verdicts; infrastructure failures are excluded
329
+ from the headline recall as `apply_gold_patch` always documented; per-depth
330
+ reports no longer pool under one name; `compare` and `aggregate-seeds`
331
+ exit non-zero on an unusable input.
332
+
333
+ - **A filename containing a newline desynchronised the cat-file batch**: the
334
+ request stream is line-delimited, so one such path became two requests and
335
+ every later file was served the previous request's leftover body. Paths with
336
+ control characters go through `git show` on argv instead.
337
+ - **The native binary without `--diff` ran a bare `git diff`** (index vs
338
+ worktree, staged edits invisible) while the pipeline read the same absence
339
+ as "vs HEAD"; both now mean HEAD, and git/environment failures exit 3 as the
340
+ README and the Python CLI already promised (was 1).
341
+ - **`diffctx . | head` exits 141 in tree and pack mode**, as documented; the
342
+ writer swallowed the broken pipe and the run exited 0.
343
+ - **`-o FILE` no longer downgrades the output to mode 0600** — the temp file
344
+ takes the mode the umask gives a new file before it replaces the target.
345
+ - **A fragment id past the end of its file is refused**, not silently clamped
346
+ to the last line.
347
+ - **The discovery walk fallback used the attribution ignore lookup**, so an
348
+ ignored directory's contents entered the universe when `ls-files` failed.
349
+ - **`--alpha` was documented backwards**: it is PPR's continuation
350
+ probability, so a higher value sends relevance further from the change.
351
+ README, CLI help and the Action docs said "tighter".
352
+ - **`--budget 0` was described four incompatible ways**; every surface now
353
+ says what happens — no fragments, changed files listed as omitted.
354
+ - **Three CI gates could not fail**: the sdist "contains Rust sources" check
355
+ returned `head`'s exit status, the results-branch push loop exited 0 after
356
+ three failed pushes, and the radon complexity step returned 0 whatever it
357
+ found (deleted; the pre-commit xenon hook is the gate). The sensitivity
358
+ workflow gets pipefail, the Hetzner cleanup no longer cancels itself, the
359
+ dead v1-artifact heartbeat is gone, and the release bump now patches
360
+ `CITATION.cff` (the test that asserts it equal to `__version__` would have
361
+ turned the next release red).
362
+ - **`bitcheck.sh`'s pinned commits are anchored by `bitcheck/<sha>` tags** on
363
+ both remotes — the history rewrite had left them reachable from nothing.
364
+ - **The corpus harness verifies `auto_garbage`** as SCHEMA.md promised; the
365
+ injection half existed alone. The pre-commit corpus sample covers every
366
+ stratum and runs on case edits.
367
+
368
+ - **`--budget` now covers the whole artifact** (#241). It bounded only the
369
+ fragments: the commit message and the changed/deleted/renamed/lockfile/
370
+ ignored path lists rendered for free, and the changed-file list was printed
371
+ twice — once in full, once again as a "not represented" footer. On an
372
+ 83-file range `--budget 1000` produced 3 150 tokens and `--budget 0`
373
+ produced 2 464, with the selection dutifully under budget the whole time.
374
+ The summary is charged before selection starts and printed once, with
375
+ omitted entries marked in place. A budget too small to hold the summary now
376
+ yields the summary and no fragments — a changed path is never dropped to fit.
377
+ - **One generated file can no longer take the budget from every other changed
378
+ file** (#238). Cores that miss the core-budget reservation are swept
379
+ cheapest-first, and that sweep applied no per-file ceiling — so a changed
380
+ `records.json`, whose record-sized cores are the cheapest candidates in the
381
+ run, refilled the whole budget while the files it was deferred behind got
382
+ nothing. The sweep now runs in two rounds: the ceiling holds in the first,
383
+ and only what it blocked is replayed in the second, so unclaimed budget
384
+ still flows back.
385
+ - **The Kubernetes edge builder emitted nothing at all** (#226). Detection ran
386
+ per fragment and required `apiVersion:` and `kind:` in the same one, but the
387
+ YAML parser splits a manifest by top-level key — so no fragment of any
388
+ manifest ever qualified and all six channels (selector→workload, configmap,
389
+ secret, service, volume, image) were dead. Detection runs on the file, whose
390
+ text is rebuilt from its fragments by line number, and each edge is anchored
391
+ on the smallest fragment covering the construct it is about.
392
+ - **The deadline test measured the repository's history, not the deadline.**
393
+ It asked for `HEAD~20`, which stopped exceeding the ceiling when the history
394
+ was squashed into epochs; it now runs the root commit to HEAD, the largest
395
+ diff the repo can produce.
396
+ - **`python -m eval equivalence` could not fail** (#233): the CLI dispatcher
397
+ discarded each subcommand's return value, so the gate printed
398
+ "EQUIVALENCE FAILED" and exited 0. Any CI job wired to the documented
399
+ invocation was decorative.
400
+
401
+ - **A POSIX filename containing a backslash keeps its name and its own
402
+ content** (#239). Every path written into output — and the `rev:path` spec
403
+ handed to `git show` — rewrote `\` to `/`, so with `src\utils.py`
404
+ (changed) and `src/utils.py` (untouched) both present, the changed file was
405
+ listed under the other's name and rendered with the other's body. All
406
+ eight sites now go through `paths.rs`, which rewrites only on Windows.
407
+ - **The corpus harness anchors a pure deletion where git does.** The
408
+ in-memory diff started a deletion-only hunk one line below git's
409
+ `@@ -5 +4,0 @@`, so `core_selection_range` — and therefore every
410
+ deletion-carrying corpus case — scored a selection production never
411
+ produces. Pinned by a test that compares both sides on the same deletion.
412
+ - **A truncated generated fragment costs what it weighs**: it carried
413
+ `token_count: 0`, free for the budget while still rendering in full.
414
+ - **`cat-file --batch` stays in sync**: a swallowed record terminator used to
415
+ shift the stream one byte for every later read, and a child that could not
416
+ be queried counted as alive and was never restarted.
417
+ - **Naming only files roots the tree at their common parent**, not the
418
+ current directory, so `.diffctx/ignore` and relative paths resolve where
419
+ the files live; the wrapper node is named after that root instead of ".".
420
+ - **A failed native import no longer buries the error being handled**: the
421
+ CLI's git-error handler imported the extension inside the `except` clause.
422
+ - **The quotient-graph registry holds the graphs it keys by `id()`**; a
423
+ collected graph's recycled id could label the next graph with the wrong
424
+ node keys.
425
+ - Boltzmann objective: the selection reason is decided after the loop, cores
426
+ count toward the reported utility, and overlap goes through the shared
427
+ `IntervalIndex` instead of a fourth hand-written check.
428
+
429
+ ### Changed
430
+
431
+ - **File-level relations land on one fragment per file, and a change
432
+ lifts to its file** (#196). An import, a directory sibling, a shared
433
+ package or crate root, a covering test are relations between files: the
434
+ Python, Rust and Go builders link the file's representative fragment (the
435
+ same one every builder names, ties now broken by position rather than by
436
+ the order a builder happened to hold the fragments in), and `from m import
437
+ x` / `pkg.Sym` / `Mod::sym` link the fragment that defines the named
438
+ symbol. In return the ego walk seeds each changed fragment's file
439
+ representative at the containment discount, so a function added to an
440
+ existing module still meets the module's importers and its tests. A name
441
+ used in more than 64 files without importing its definer (`hass`,
442
+ `config`, `entry`) is vocabulary and no longer fans out to its
443
+ definitions; qualified calls (`Type::new(`, `pkg.New(`), method calls
444
+ and definition lines no longer feed the unqualified call channel, which
445
+ linked every `::new(` to every `fn new` in reach; Go imports come from
446
+ the import block, not from any quoted line; Python relative imports
447
+ resolve to the sibling module they name. Same machine, same instances as
448
+ the issue's table: home-assistant 38.1M → 2.8M python edges, 100 s → 24 s,
449
+ 10.9 → 2.5 GB; polars 35.4M → 0.8M rust edges, 54 s → 4 s, 5.4 → 0.8 GB;
450
+ kubernetes 14.3M → 4.9M go edges (the rest of that run is parsing).
451
+ Q-class: 16 corpus cases lift above the threshold, one `gap` case is
452
+ baselined (`gap_132`: the file lift admits a consumer of a sibling type).
453
+ Edge-weight profile `v2-2026-09-16`.
454
+ - **One heavy phase for the product and the corpus harness.** From the
455
+ fragments onward — token counts, cores and their stand-ins, signature
456
+ variants, seed weights, scoring, information needs — both paths call
457
+ `pipeline::score_from_fragments`; the harness's own spelling of those
458
+ steps is gone, and the `DIFFCTX_OBJECTIVE` override, which only the
459
+ product used to see, is resolved with the mode for every caller (#232).
460
+ - **The compute deadline is no longer a panic, and never an exception.**
461
+ `ComputeTimeoutError` stays importable but the engine does not raise it;
462
+ a git subprocess that overruns its share of the timeout is still a
463
+ `GitError`. The native CLI exits 0 with a partial artifact at the deadline;
464
+ 124 is reserved for the watchdog that fires 30 s later when a phase could
465
+ not stop. `--timeout 0` is therefore a git failure (exit 3), not an abort.
466
+
467
+ - **The hero comparison names both baselines.** It measured whole changed
468
+ files pasted (17.6×) while the headline invites comparison with a raw
469
+ `git diff` — the question three first-visit probes actually asked. The raw
470
+ diff of the same commit is 169,809 tokens, so that ratio is 3.9×, and the
471
+ caption says so next to the number.
472
+
473
+ - **The `tree-sitter` extra is gone** (`full` = charset-normalizer alone):
474
+ it shipped seventeen grammar wheels nothing in the package imports — parsing
475
+ lives in the Rust crate. `uv.lock` shrinks by ~370 lines.
476
+ - **The landing page's copy now matches the tool**: five scoring modes, the
477
+ τ dial at the shipped 0.05, YAML labelled as the shown (not default)
478
+ format, α attributed to PPR, the toy's numbers reconciled with the hero
479
+ measurement and its denominator's generated snapshot named, `llms.txt`
480
+ naming the one tool that exists.
481
+
482
+ - **The landing page's headline comparison names its source.** It shipped
483
+ `48,210 → 6,930 tokens, 7×` with no provenance on the page and none in the
484
+ repository either; three independent first-visit probes each reached the
485
+ same verdict — the one number meant to justify adoption could not be checked.
486
+ It now reads 768,268 → 43,769 (17.6×), measured on
487
+ home-assistant/core@1d885bd0 (66 files changed) with the command printed
488
+ beside it, and says plainly that the ratio tracks how much of the changed
489
+ files is unrelated to the change. A test gates the provenance, not the value.
490
+
491
+ - **The GIL is released for the last three heavy extension calls** (#245):
492
+ `build_project_graph` (a full repository walk plus a tree-sitter parse of
493
+ every file), `resolve_diff_range` (spawns `git`) and `count_tokens`. Held
494
+ under the GIL these blocked every other thread in the host interpreter for
495
+ their whole duration, which is precisely the wall-clock a latency column is
496
+ supposed to attribute to diffctx.
497
+ - **The eval sweep no longer runs 45 duplicate cells** (#233): `depth` reaches
498
+ the runner only when it is non-negative, but `internal-bm25`, `rrf` and
499
+ `pit` had no depth excludes, so each ran five byte-identical copies of its
500
+ cell per test set — half the matrix, about a third of the machine hours. The
501
+ smoke cell also asserts its own result now instead of printing it, and is
502
+ handed `HF_TOKEN` so dataset reads are not rate-limited.
503
+
504
+ - **The eval harness's Python dependencies are a `uv` dependency group**
505
+ (`uv sync --group eval`), locked in `uv.lock` like everything else.
506
+ `requirements-eval.txt` + `.lock` — a second resolver with its own bot —
507
+ are gone; `Dockerfile.eval` exports the group with hashes.
508
+ - `paper/v1` left the tree (it lives at tag `paper-v1-grid`); five
509
+ never-called statistics helpers and `reset_to_commit` left `eval/`.
510
+ - Test hygiene: locate and pack are pinned to each other's `changed` set
511
+ (pack used to be compared with itself); the path-jail refusal property
512
+ runs on every example and registers the legacy tools once; the deadline
513
+ test requires the ceiling to fire; the panic-profile test reads the
514
+ release table only; the Rust and Python `.diffctx/ignore` anchorers share
515
+ one fixture table instead of a "mirrors" comment.
516
+ - **A git subprocess whose output could not be read no longer reports an
517
+ empty diff.** The pipe reader's failure collapsed into an empty buffer
518
+ returned as success, so an unreadable `git diff` was indistinguishable from
519
+ a range with no changes. It is an error now. `is_git_repo` likewise stops
520
+ answering "not a git repository" when git did not run at all (missing from
521
+ PATH, permission denied): only git's own "no" is a "no".
522
+ - **The weak stopping threshold never ships without the admission gate.**
523
+ `tau = 0.05` was calibrated with the per-file naming gate on (#65);
524
+ `--scoring bm25` has no graph to build a gate from and rode the weak stop
525
+ anyway, re-admitting exactly the diffuse tail the gate blocks. A scorer that
526
+ produces no gate — BM25, or `DIFFCTX_FILE_ADMISSION=0` — now uses the
527
+ pre-gate threshold (0.12) whenever `--tau` is left at its default value
528
+ (an explicit `--tau 0.05` is indistinguishable from the default and gets the
529
+ same substitution; any other explicit value is used as given).
530
+ - **A range whose changed-file list is empty still discloses what it
531
+ withheld**: the third early exit in `resolve_change_set` returned zeroed
532
+ lock-file, ignored and policy counts it had already computed.
533
+ - **Rust lifetimes no longer break signature extraction.** `'` was a string
534
+ opener in every language; `fn f<'a>(x: &'a T)` has no closing quote, the
535
+ scanner swallowed the rest of the line and lost the parameter list, and the
536
+ stub fell back to two lines cut mid-signature.
537
+ - **`.env` and `.editorconfig` are recognized.** They were registered as
538
+ extensions, and a dotfile has none, so neither ever entered the candidate
539
+ universe. A bare lowercase `build` or `workspace` is no longer classified as
540
+ Bazel — only the capitalized names Bazel actually uses.
541
+ - **Scala imports inside an unbalanced brace group are kept.** The
542
+ multi-line join consumed up to 32 lines and never re-scanned them, so an
543
+ `import` in that window vanished; it now stops at the next `import` line.
544
+ Scala 3 `as` renames split on any whitespace, not the literal single space.
545
+ - **The C-family pairing loop polls the deadline per pair, not per stem
546
+ bucket** (#210): envoy's 520 files sharing one stem are a single bucket, so
547
+ the bucket-level poll never fired inside the case it was added for.
548
+ - `scripts/bitcheck.sh` builds the binary it measures. It read whatever
549
+ `target/release/diffctx` the last `cargo test --release` had left, so a
550
+ `record`/`check` pair around an unbuilt edit compared one stale binary with
551
+ itself.
552
+ - The MCP server warns at startup when `DIFFCTX_ALLOWED_PATHS` is unset, so
553
+ running unconfined is a choice the log records rather than a default nobody
554
+ sees.
555
+ - CI: `sensitivity-check` installs the pinned Rust toolchain before syncing
556
+ (the sync compiles the extension); a release run that skipped PyPI attaches
557
+ its wheels and sdist to the GitHub release instead of shipping none.
558
+ - **An expired compute deadline no longer kills the process.** The release
559
+ profile set `panic = "abort"`, so the deadline — which fires as a panic on
560
+ purpose, because the phases it guards return no `Result` — reached a
561
+ published wheel as SIGABRT instead of an error (measured: signal 6 from a
562
+ `pip install`ed 1.15.0 wheel). For the MCP server, which abandons a
563
+ timed-out worker thread and keeps serving, that took every concurrent
564
+ request down with it. The profile now unwinds, which is what lets PyO3
565
+ convert a panic into a Python exception at all, and the deadline arrives as
566
+ `TimeoutError` (`_diffctx.ComputeTimeoutError`) that a caller can catch. The
567
+ test profile is now the shipped profile: the old `release-unwind` clone is
568
+ exactly what let this mechanism pass its own test while aborting in
569
+ production.
570
+ - **A timeout too small for git to answer no longer accuses the repository.**
571
+ `is_git_repo` collapsed every failure into "false", so `timeout=0` reported
572
+ a valid repo as "not a git repository" instead of saying it timed out.
573
+ - **The `changed` role is recorded where the substitution happens** (#209),
574
+ not re-derived from a shared start line in three places (pack render,
575
+ `locate`, and the post-pass coverage rank, the last scanning every core id
576
+ per candidate). `SelectionResult::stand_in_ids` carries the fact and
577
+ `types::carries_change` is the single predicate.
578
+ - **`<script type=>` is read from the AST** rather than scanned out of the
579
+ start tag's text (#213), so `type = "..."` with spaces around the `=` and
580
+ single-quoted values are handled by the grammar instead of by hand.
581
+
582
+ - **The per-file admission gate (#65) now covers every selection path**
583
+ (#211): both post-selection passes and both singleton comparators used to
584
+ admit files the greedy was forbidden to open. The rescue pass keeps a
585
+ measured escape — when the selection holds no context at all, an admissible
586
+ answer beats an empty one. Singletons admit via a wider declared-relation
587
+ reachability (config keys, latex/terraform references) so weak-but-declared
588
+ channels are not starved. Net on the 2725-case oracle corpus: 133 cases
589
+ fixed, 6 explained regressions (`known_below_threshold.txt` 195 → 68).
590
+ - **`--alpha` now actually reaches PPR scoring** (it was validated and then
591
+ silently discarded; every run used the default damping regardless).
592
+ - **A fragment deferred by the per-file ceiling still sets the stopping
593
+ peak** (#197): the tau bar was being calibrated on whatever foreign-file
594
+ fragment came after the deferred argmax.
595
+ - **Test-file imports are read from every fragment of the file** (#217):
596
+ module-level imports land in a fragment appended after the definitions, so
597
+ the old first-fragment read never saw them and the `test_direct` weight
598
+ never fired for tree-sitter languages. Test-naming edges also join the
599
+ naming class, so a `widget_test.go` with no import is admissible context.
600
+
601
+ ## [1.15.0] - 2026-08-19
602
+
10
603
  ### Added
11
604
 
12
605
  - **Changed files that produced no fragments are disclosed in the md/text
@@ -167,8 +760,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
167
760
  precision, because BM25 gives any generic-token match a small positive score
168
761
  and `1/(k + rank)` promotes it to real fused mass. `pit` keeps the position
169
762
  instead, via each component's empirical CDF: a fragment in the 5th percentile
170
- of a signal contributes 0.05 from it, so two weak opinions cannot manufacture a
171
- strong candidate. `score = blend·PIT(ego) + (1-blend)·PIT(bm25) + bonus·[both
763
+ of a signal contributes 0.05 from it, so two weak opinions cannot manufacture
764
+ a strong candidate. `score = blend·PIT(ego) + (1-blend)·PIT(bm25) + bonus·[both
172
765
  in top-k]`, with `DIFFCTX_PIT_BLEND=0.65`, `DIFFCTX_PIT_AGREEMENT_BONUS=0.10`,
173
766
  `DIFFCTX_PIT_AGREEMENT_TOP_K=20`. Measured on the full corpus it recovers 40 of
174
767
  those 79 cases (`rrf` 450 below-threshold, `pit` 410) and still trails `ego`
@@ -881,9 +1474,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
881
1474
  mapping and `--diff` context, since such material is never legitimate LLM
882
1475
  context: `*.pem`, `*.key`, `*.pfx`, `*.p12`, `*.keystore`, `*.jks`, and SSH
883
1476
  private keys `id_rsa`/`id_dsa`/`id_ecdsa`/`id_ed25519` (public `.pub` keys stay
884
- visible). The `--diff` path previously applied no ignore filtering at all, so a
885
- changed key file would have leaked into context. Use `--no-default-ignores` to
886
- opt out of tree-mode default ignores. (`.env` files are intentionally still
1477
+ visible). The `--diff` path previously applied no ignore filtering at all, so
1478
+ a changed key file would have leaked into context. Use `--no-default-ignores`
1479
+ to opt out of tree-mode default ignores. (`.env` files are intentionally still
887
1480
  included — a changed `.env` is legitimate change context; redacting secret
888
1481
  *values* is a separate planned content-scan feature.)
889
1482
 
@@ -1033,7 +1626,10 @@ Earlier releases shipped as `treemapper`; see
1033
1626
  <https://github.com/nikolay-e/diffctx/releases> for the corresponding GitHub
1034
1627
  release notes (`1.0.0` through `1.6.1`).
1035
1628
 
1036
- [Unreleased]: https://github.com/nikolay-e/diffctx/compare/v1.14.0...HEAD
1629
+ [Unreleased]: https://github.com/nikolay-e/diffctx/compare/v1.17.0...HEAD
1630
+ [1.17.0]: https://github.com/nikolay-e/diffctx/compare/v1.16.0...v1.17.0
1631
+ [1.16.0]: https://github.com/nikolay-e/diffctx/compare/v1.15.0...v1.16.0
1632
+ [1.15.0]: https://github.com/nikolay-e/diffctx/compare/v1.14.0...v1.15.0
1037
1633
  [1.14.0]: https://github.com/nikolay-e/diffctx/compare/v1.13.0...v1.14.0
1038
1634
  [1.13.0]: https://github.com/nikolay-e/diffctx/compare/v1.12.3...v1.13.0
1039
1635
  [1.12.3]: https://github.com/nikolay-e/diffctx/compare/v1.12.2...v1.12.3