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