diffctx 1.12.2__tar.gz → 1.13.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 (181) hide show
  1. diffctx-1.13.0/CHANGELOG.md +930 -0
  2. {diffctx-1.12.2 → diffctx-1.13.0}/Cargo.lock +21 -20
  3. diffctx-1.13.0/Cargo.toml +20 -0
  4. {diffctx-1.12.2 → diffctx-1.13.0}/PKG-INFO +92 -76
  5. diffctx-1.13.0/README.md +257 -0
  6. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/Cargo.toml +6 -5
  7. diffctx-1.13.0/crates/diffctx-native/README.md +257 -0
  8. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/analytics.rs +53 -14
  9. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/candidate_files.rs +26 -2
  10. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/config/analytics.rs +0 -2
  11. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/config/edge_weights.rs +12 -1
  12. diffctx-1.13.0/crates/diffctx-native/src/config/env_overrides.rs +273 -0
  13. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/config/filtering.rs +0 -6
  14. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/config/git.rs +2 -5
  15. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/config/graph_filtering.rs +0 -2
  16. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/config/limits.rs +6 -0
  17. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/config/mode.rs +6 -0
  18. diffctx-1.13.0/crates/diffctx-native/src/config/scoring.rs +69 -0
  19. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/config/weights.rs +2 -0
  20. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/core.rs +103 -3
  21. diffctx-1.13.0/crates/diffctx-native/src/deadline.rs +64 -0
  22. diffctx-1.13.0/crates/diffctx-native/src/discovery.rs +760 -0
  23. diffctx-1.13.0/crates/diffctx-native/src/edges/base.rs +501 -0
  24. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/config_edges/generic.rs +63 -12
  25. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/mod.rs +111 -3
  26. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/c_family.rs +102 -25
  27. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/dotnet.rs +179 -17
  28. diffctx-1.13.0/crates/diffctx-native/src/edges/semantic/jvm.rs +878 -0
  29. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/tags.rs +4 -0
  30. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/similarity/lexical.rs +45 -6
  31. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/structural/containment.rs +19 -1
  32. diffctx-1.13.0/crates/diffctx-native/src/edges/structural/sibling.rs +145 -0
  33. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/structural/testing.rs +49 -45
  34. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/excerpt.rs +51 -24
  35. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/filtering.rs +126 -16
  36. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/fragmentation.rs +65 -2
  37. diffctx-1.13.0/crates/diffctx-native/src/git.rs +2048 -0
  38. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/graph.rs +340 -30
  39. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/graph_export.rs +57 -0
  40. diffctx-1.13.0/crates/diffctx-native/src/interval.rs +210 -0
  41. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/languages.rs +69 -0
  42. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/lib.rs +5 -0
  43. diffctx-1.13.0/crates/diffctx-native/src/locate.rs +587 -0
  44. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/main.rs +117 -7
  45. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/memory_pipeline.rs +23 -12
  46. diffctx-1.13.0/crates/diffctx-native/src/mode.rs +159 -0
  47. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/parsers/generic.rs +55 -0
  48. diffctx-1.13.0/crates/diffctx-native/src/parsers/mod.rs +285 -0
  49. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/parsers/tree_sitter_strategy.rs +737 -1
  50. diffctx-1.13.0/crates/diffctx-native/src/paths.rs +64 -0
  51. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/pipeline.rs +511 -61
  52. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/postpass.rs +199 -10
  53. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/ppr.rs +231 -0
  54. diffctx-1.13.0/crates/diffctx-native/src/provenance.rs +190 -0
  55. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/pybridge.rs +140 -282
  56. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/render.rs +216 -9
  57. diffctx-1.13.0/crates/diffctx-native/src/scoring.rs +1133 -0
  58. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/select.rs +402 -22
  59. diffctx-1.13.0/crates/diffctx-native/src/signatures.rs +345 -0
  60. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/test_harness.rs +24 -2
  61. diffctx-1.13.0/crates/diffctx-native/src/testfiles.rs +169 -0
  62. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/token_corpus.rs +125 -5
  63. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/types.rs +55 -0
  64. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/utility/needs.rs +82 -41
  65. {diffctx-1.12.2 → diffctx-1.13.0}/pyproject.toml +10 -3
  66. {diffctx-1.12.2 → diffctx-1.13.0}/src/diffctx/_app.py +106 -13
  67. diffctx-1.13.0/src/diffctx/_diffctx.pyi +104 -0
  68. diffctx-1.13.0/src/diffctx/_native/__init__.py +7 -0
  69. {diffctx-1.12.2 → diffctx-1.13.0}/src/diffctx/_native/graph_analytics.py +6 -14
  70. {diffctx-1.12.2 → diffctx-1.13.0}/src/diffctx/_native/graph_export.py +6 -12
  71. diffctx-1.13.0/src/diffctx/_native/pipeline.py +194 -0
  72. {diffctx-1.12.2 → diffctx-1.13.0}/src/diffctx/cli.py +124 -12
  73. diffctx-1.13.0/src/diffctx/mcp/README.md +127 -0
  74. diffctx-1.13.0/src/diffctx/mcp/fetch.py +211 -0
  75. diffctx-1.13.0/src/diffctx/mcp/security.py +64 -0
  76. diffctx-1.13.0/src/diffctx/mcp/server.py +533 -0
  77. {diffctx-1.12.2 → diffctx-1.13.0}/src/diffctx/tree.py +6 -2
  78. diffctx-1.13.0/src/diffctx/version.py +1 -0
  79. {diffctx-1.12.2 → diffctx-1.13.0}/src/diffctx/writer.py +53 -21
  80. diffctx-1.12.2/CHANGELOG.md +0 -394
  81. diffctx-1.12.2/Cargo.toml +0 -12
  82. diffctx-1.12.2/README.md +0 -241
  83. diffctx-1.12.2/crates/diffctx-native/README.md +0 -241
  84. diffctx-1.12.2/crates/diffctx-native/src/config/env_overrides.rs +0 -125
  85. diffctx-1.12.2/crates/diffctx-native/src/config/scoring.rs +0 -21
  86. diffctx-1.12.2/crates/diffctx-native/src/discovery.rs +0 -293
  87. diffctx-1.12.2/crates/diffctx-native/src/edges/base.rs +0 -294
  88. diffctx-1.12.2/crates/diffctx-native/src/edges/semantic/jvm.rs +0 -420
  89. diffctx-1.12.2/crates/diffctx-native/src/edges/structural/sibling.rs +0 -89
  90. diffctx-1.12.2/crates/diffctx-native/src/git.rs +0 -842
  91. diffctx-1.12.2/crates/diffctx-native/src/interval.rs +0 -78
  92. diffctx-1.12.2/crates/diffctx-native/src/mode.rs +0 -96
  93. diffctx-1.12.2/crates/diffctx-native/src/parsers/mod.rs +0 -142
  94. diffctx-1.12.2/crates/diffctx-native/src/scoring.rs +0 -284
  95. diffctx-1.12.2/crates/diffctx-native/src/signatures.rs +0 -144
  96. diffctx-1.12.2/src/diffctx/_native/__init__.py +0 -7
  97. diffctx-1.12.2/src/diffctx/_native/pipeline.py +0 -114
  98. diffctx-1.12.2/src/diffctx/mcp/README.md +0 -152
  99. diffctx-1.12.2/src/diffctx/mcp/security.py +0 -35
  100. diffctx-1.12.2/src/diffctx/mcp/server.py +0 -312
  101. diffctx-1.12.2/src/diffctx/version.py +0 -1
  102. {diffctx-1.12.2 → diffctx-1.13.0}/LICENSE +0 -0
  103. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/config/bm25.rs +0 -0
  104. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/config/budget.rs +0 -0
  105. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/config/category_weights.rs +0 -0
  106. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/config/extensions.rs +0 -0
  107. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/config/fragmentation.rs +0 -0
  108. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/config/importance.rs +0 -0
  109. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/config/mod.rs +0 -0
  110. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/config/needs.rs +0 -0
  111. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/config/parsers.rs +0 -0
  112. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/config/render.rs +0 -0
  113. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/config/selection.rs +0 -0
  114. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/config/tokenization.rs +0 -0
  115. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/config_edges/build_system.rs +0 -0
  116. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/config_edges/cicd.rs +0 -0
  117. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/config_edges/docker.rs +0 -0
  118. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/config_edges/helm.rs +0 -0
  119. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/config_edges/kubernetes.rs +0 -0
  120. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/config_edges/mod.rs +0 -0
  121. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/document/mod.rs +0 -0
  122. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/history/cochange.rs +0 -0
  123. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/history/mod.rs +0 -0
  124. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/ansible.rs +0 -0
  125. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/bazel.rs +0 -0
  126. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/cargo_edges.rs +0 -0
  127. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/clojure.rs +0 -0
  128. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/css.rs +0 -0
  129. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/dart.rs +0 -0
  130. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/dbt.rs +0 -0
  131. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/elixir.rs +0 -0
  132. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/erlang.rs +0 -0
  133. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/go.rs +0 -0
  134. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/graphql.rs +0 -0
  135. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/haskell.rs +0 -0
  136. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/javascript.rs +0 -0
  137. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/julia.rs +0 -0
  138. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/latex.rs +0 -0
  139. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/lua.rs +0 -0
  140. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/mod.rs +0 -0
  141. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/nim.rs +0 -0
  142. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/nix.rs +0 -0
  143. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/ocaml.rs +0 -0
  144. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/openapi.rs +0 -0
  145. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/perl.rs +0 -0
  146. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/php.rs +0 -0
  147. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/prisma.rs +0 -0
  148. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/protobuf.rs +0 -0
  149. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/python.rs +0 -0
  150. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/r_lang.rs +0 -0
  151. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/ruby.rs +0 -0
  152. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/rust_lang.rs +0 -0
  153. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/shell.rs +0 -0
  154. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/sql.rs +0 -0
  155. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/swift.rs +0 -0
  156. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/terraform.rs +0 -0
  157. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/semantic/zig.rs +0 -0
  158. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/similarity/mod.rs +0 -0
  159. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/edges/structural/mod.rs +0 -0
  160. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/parsers/config_parser.rs +0 -0
  161. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/parsers/markdown.rs +0 -0
  162. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/peak_rss.rs +0 -0
  163. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/project_graph.rs +0 -0
  164. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/stopwords.rs +0 -0
  165. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/tokenizer.rs +0 -0
  166. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/utility/boltzmann.rs +0 -0
  167. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/utility/importance.rs +0 -0
  168. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/utility/mod.rs +0 -0
  169. {diffctx-1.12.2 → diffctx-1.13.0}/crates/diffctx-native/src/utility/scoring.rs +0 -0
  170. {diffctx-1.12.2 → diffctx-1.13.0}/rust-toolchain.toml +0 -0
  171. {diffctx-1.12.2 → diffctx-1.13.0}/src/diffctx/__init__.py +0 -0
  172. {diffctx-1.12.2 → diffctx-1.13.0}/src/diffctx/__main__.py +0 -0
  173. {diffctx-1.12.2 → diffctx-1.13.0}/src/diffctx/_native/project_graph.py +0 -0
  174. {diffctx-1.12.2 → diffctx-1.13.0}/src/diffctx/clipboard.py +0 -0
  175. {diffctx-1.12.2 → diffctx-1.13.0}/src/diffctx/ignore.py +0 -0
  176. {diffctx-1.12.2 → diffctx-1.13.0}/src/diffctx/logger.py +0 -0
  177. {diffctx-1.12.2 → diffctx-1.13.0}/src/diffctx/mcp/__init__.py +0 -0
  178. {diffctx-1.12.2 → diffctx-1.13.0}/src/diffctx/mcp/__main__.py +0 -0
  179. {diffctx-1.12.2 → diffctx-1.13.0}/src/diffctx/mcp/formatting.py +0 -0
  180. {diffctx-1.12.2 → diffctx-1.13.0}/src/diffctx/py.typed +0 -0
  181. {diffctx-1.12.2 → diffctx-1.13.0}/src/diffctx/tokens.py +0 -0
@@ -0,0 +1,930 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+
12
+ - **Inline `<script>` and `<style>` in HTML are parsed with their own
13
+ grammars** (#181). A changed function inside a single-file web app's script
14
+ now surfaces as a named definition fragment at its real file lines instead
15
+ of degrading to a raw line window with no symbol boundaries. JS and CSS
16
+ fragments overlay the `script_element`/`style_element` containers the way
17
+ methods overlay a class; `type=` attributes naming non-JS payloads (JSON,
18
+ templates) opt out.
19
+
20
+ - **`--diff <duration>`** — a time window instead of a revision: `24h`, `8d`,
21
+ `90min`, `1h30m`, `2w` (units `s`, `m`/`min`, `h`, `d`, `w`, composable). The
22
+ window resolves to the last commit before it and diffs the working tree
23
+ against that, so one flag covers the commits made inside the window plus the
24
+ uncommitted and untracked work on top — the "what have I touched today"
25
+ question, which previously meant counting `HEAD~n` by hand. A window older
26
+ than the repository falls back to the empty tree, so nothing is silently
27
+ dropped. Both CLIs and the MCP `diff_ref` share the resolver; a ref that
28
+ happens to look like a duration (a branch `24h`, an abbreviated sha) is
29
+ probed first and keeps its git meaning, so no existing invocation changes.
30
+
31
+ - **`locate` discloses its own blind spots** — a `coverage` block and an
32
+ `overflow` ranking, both omitted when there is nothing to report so a clean
33
+ run costs no tokens. Trust drives repeat calls: an agent told honestly where
34
+ the selection is thin can grep the gap itself, while one told nothing has to
35
+ distrust the whole answer.
36
+
37
+ `coverage.unparsed_files` names changed files whose language diffctx claims to
38
+ parse but which yielded no symbol-level structure — nothing else in the output
39
+ says the parser came back empty there. `zero_edge_files` names changed files
40
+ with no graph edge in either direction, where relevance had no path to travel.
41
+ `ppr_truncated` reports a diffusion cut short by its push cap.
42
+ `overflow` ranks the admitted candidates that did not fit, with a one-line
43
+ `why` and no bodies, capped at 50 entries against a full `overflow_count`.
44
+
45
+ `coverage.next_up` counts how many of those a 25% larger budget would admit,
46
+ and is zero when the budget was not what stopped selection — at `--budget -1`
47
+ nothing is crowded out by tokens, so there is nothing to report. It feeds a
48
+ documented `confidence` heuristic in [0, 1]
49
+ (`parsed_share * linked_share * fit_share`, less 0.1 when PPR truncated),
50
+ which says how much of the changed surface the run could see and fit and
51
+ deliberately does **not** claim the selection is correct. Pack output is
52
+ byte-unchanged — verified against the full 2902-case corpus (#136).
53
+ - **`--scoring pit`** — percentile fusion, the successor to `rrf`. Both blend
54
+ the structural (`ego`) and lexical (`bm25`) signals over the same candidate
55
+ universe; the difference is what is blended. `rrf` reduces each component to a
56
+ rank, which discards the magnitude that says "this scored near zero", and on
57
+ the oracle corpus that costs 79 cases against plain `ego` — every loss on
58
+ precision, because BM25 gives any generic-token match a small positive score
59
+ and `1/(k + rank)` promotes it to real fused mass. `pit` keeps the position
60
+ instead, via each component's empirical CDF: a fragment in the 5th percentile
61
+ of a signal contributes 0.05 from it, so two weak opinions cannot manufacture a
62
+ strong candidate. `score = blend·PIT(ego) + (1-blend)·PIT(bm25) + bonus·[both
63
+ in top-k]`, with `DIFFCTX_PIT_BLEND=0.65`, `DIFFCTX_PIT_AGREEMENT_BONUS=0.10`,
64
+ `DIFFCTX_PIT_AGREEMENT_TOP_K=20`. Measured on the full corpus it recovers 40 of
65
+ those 79 cases (`rrf` 450 below-threshold, `pit` 410) and still trails `ego`
66
+ (371), so **`ego` remains the default** and fusion has yet to earn it (#125).
67
+
68
+ Ablations settled *why*, and the answer is not calibration. At `blend=1.0` the
69
+ lexical arm contributes nothing to the score, and fusion still scores 388
70
+ against ego's 371 — so no mixing weight can recover the gap. Substituting the
71
+ identity for the percentile at that blend reproduces ego **exactly** (371),
72
+ which rules out every other difference between the fusion path and ego: the
73
+ restricted score map, the max-normalisation with cores pinned to 1.0, and
74
+ `finish_scoring` over the pre-filtered union all cost zero. The entire
75
+ structural gap belongs to the transform.
76
+
77
+ `DIFFCTX_PIT_TRANSFORM=maxnorm` fuses the components on their rescaled scores
78
+ instead of their distributional position, and dominates the percentile at
79
+ every blend measured (371 / 371 / 380 / 396 at blend 1.00 / 0.95 / 0.85 / 0.65
80
+ against 388 and 412 for the percentile). It still does not beat ego: both
81
+ curves rise monotonically with lexical weight, so in both families the optimum
82
+ sits at zero lexical weight.
83
+
84
+ One number invites a wrong reading: `--scoring bm25` alone scores 124, far
85
+ ahead of ego's 371. It is not better — it emits a median of 6 fragments to
86
+ ego's 10, half of them the change itself, and 71% of its failures are lost
87
+ recall against ego's 19%. The corpus metric `recall * (1 - forbidden_rate)`
88
+ has no over-selection term and a saturating forbidden term, so it rewards
89
+ conservatism; the comparison is a metric artefact, not a ranking result.
90
+ - **`--scoring rrf`** — reciprocal-rank fusion of the structural (`ego`) and
91
+ lexical (`bm25`) signals: each ranks the same candidate universe, and a
92
+ fragment scores `Σ 1/(k + rank_i)` with `k=60` (`DIFFCTX_RRF_K`). Fusion on
93
+ ranks alone removes the score-scale calibration between the two signals
94
+ that made the deployed mixture rank below its own lexical component on
95
+ genuine retrieval, and the candidate set becomes their union rather than
96
+ either one alone. EGO's structural guards (hub-noise and
97
+ generic-config-only suppression) and the per-file cap are re-applied to
98
+ the union against the fused scores, so the wider candidate set does not
99
+ leak the noise EGO already rejects. `ego` remains the default (#125).
100
+ - **`--mode locate`** emits the ranked selection as compact
101
+ `diffctx.locate.v1` JSON — path, line range, kind, symbol, relevance score
102
+ and machine-readable provenance reasons (`changed`, `edge` with category /
103
+ strongest source / mass, `proximity` seed-hops) with **no source bodies**:
104
+ a navigation prior for agents at a fraction of pack-mode tokens. Available
105
+ on the Python CLI, the native binary, and the MCP `diffctx_context` tool
106
+ (`mode="locate"`); pack output is byte-unchanged (#126). Each item carries
107
+ a coarse impact `group` (`test` / `type` / `config`) and the header a
108
+ blast-radius `summary` (files, changed, context, tests) — with
109
+ `--diff --mode locate` this answers "what does my uncommitted change
110
+ touch, and which tests cover it" in one call (#135).
111
+ - `DIFFCTX_PROVENANCE_DUMP=<path>` writes one JSONL record per scored
112
+ candidate — relevance, core/selected verdicts, seed-hop distance, and
113
+ per-edge-category incoming mass — for offline analysis of why the selector
114
+ included or skipped a fragment. Experimental telemetry, not a stable
115
+ interface; the rendered output is byte-identical with the variable set
116
+ (#93).
117
+ - The YAML test-case schema can express **deletions and renames**
118
+ (`repo.deleted_files`, `repo.renamed_files`), so rename regression cases
119
+ exercise git's real rename path instead of silently degrading to adds
120
+ (#176).
121
+
122
+ ### Security
123
+
124
+ - **A symlinked directory inside a repository was a way out of it.** The
125
+ `fragment_ids` reader checked containment lexically — rejecting `..` and
126
+ absolute paths — which admits `escape/secret.txt` where `escape/` is a symlink
127
+ pointing above the repository root. A property fuzz over hostile path shapes
128
+ found it reading a planted file outside the jail. Containment is now decided
129
+ after resolution, the fuzz runs in CI, and the case is pinned separately so it
130
+ holds where hypothesis is unavailable.
131
+ - **Releases are verifiable.** Every artifact attached to a release — wheels,
132
+ sdist, standalone binaries and the SBOM itself — now carries signed SLSA build
133
+ provenance from the release workflow's OIDC identity
134
+ (`gh attestation verify <file> --repo nikolay-e/diffctx`), PyPI uploads state
135
+ `attestations: true` explicitly rather than relying on an action default that
136
+ has changed between versions, and a CycloneDX SBOM ships as a release asset.
137
+ The binaries are what this is for: unlike the wheels they had no package index
138
+ vouching for them. SECURITY.md documents the verification commands.
139
+ - **Error payloads no longer disclose resolved paths.** A refusal used to name
140
+ the path the filesystem resolved to, which told a caller that had just been
141
+ denied exactly what lay outside its jail; and an `OSError` rendered with its
142
+ absolute filename into returned content. Messages now echo only the argument
143
+ the caller supplied, and carry no traceback (#147).
144
+
145
+ ### Changed
146
+
147
+ - **MCP: one tool instead of three.** `get_diff_context` becomes
148
+ `diffctx_context` and its `diff_range` parameter becomes `diff_ref`;
149
+ `get_tree_map` and `get_file_context` are off by default and return with
150
+ `DIFFCTX_MCP_LEGACY_TOOLS=1`. Tool definitions are sent on every request of
151
+ every session before any work happens, and the three-tool surface cost 1063
152
+ tokens of every context window it was installed in against 267 for the single
153
+ tool (-75%, `o200k_base` over each serialized `{name, description,
154
+ inputSchema}`).
155
+
156
+ The default `mode` is now `"locate"`, and the new `fragment_ids` parameter
157
+ reads the bodies of ids from a ranking — `"<path>:<lines>"`, taken straight
158
+ from locate's own fields — so a diff question resolves in two calls that pay
159
+ only for the fragments actually chosen. `mode="pack"` restores the previous
160
+ one-call behaviour. Bodies are read at the diff range's end revision rather
161
+ than from the working tree, so spans from a historical range are not
162
+ mis-sliced, and `.diffctx/ignore` is enforced on `fragment_ids` as it is
163
+ everywhere else (#127).
164
+
165
+ ### Fixed
166
+
167
+ - **The `.diffctx/ignore` withheld-files count counts files, not hunks.** A
168
+ withheld file with several separated edits inflated the "N changed file(s)
169
+ withheld" notice — the one number a reviewer uses to judge how much of the
170
+ change the output actually shows.
171
+
172
+ - **C# and Scala regained real cross-file reach after the tags gate** (#131,
173
+ #179). The tags fallback had been these languages' only working linkage:
174
+ Scala's import regex captured a truncated prefix (`com.foo.` instead of
175
+ `com.foo.Bar`), so production Scala produced literally zero import edges;
176
+ chained `package` clauses and `package object` parsed wrong; C# `using`
177
+ resolution required the namespace declaration to sit in the same fragment
178
+ as the lookup key, which per-method fragmentation almost never satisfies.
179
+ Scala imports are now parsed structurally (brace selectors `{A, B => C}`,
180
+ wildcards `._`/`.*`/`given`, object-rooted prefixes like `Tables._`), the
181
+ C# `using` regex accepts aliases, and both builders gained a member-use
182
+ channel: a `receiver.member(...)` call site links to the fragments defining
183
+ that member, but only between file pairs already related through an import,
184
+ a named type, or inheritance — a name match alone stays tags-grade evidence
185
+ and is dropped. Type-name lookups now carry the same ≤8-file ambiguity bar
186
+ as the C-family builder instead of fanning out unbounded. Corpus: 19 cases
187
+ fixed / 1 broken (281 → 263 known-below-threshold), including 13 of the 14
188
+ C#/Scala one-hop cases the tags gate broke.
189
+
190
+ - **Graph construction no longer hangs on large repositories** (#116, #121).
191
+ Four independent fan-out defects each produced tens of millions of edges on
192
+ envoy-class repos (6k C-family files, 520 sharing the stem `config`):
193
+ `TestEdgeBuilder` paired every test fragment with every fragment of every
194
+ same-stem file (180M edges alone); the C-family builder linked includes by
195
+ bare basename and cross-multiplied same-stem headers×impls at fragment
196
+ level; `link_by_path_match` substring-matched every reference against every
197
+ indexed path; and the config-to-code builder ran every key regex over every
198
+ code fragment (replaced by one Aho-Corasick pass with exact `\b` semantics —
199
+ byte-identical output, 42s → 1.2s measured). Together: an envoy commit that
200
+ never finished under 240s now completes in ~60s; on the 372-instance dcbench
201
+ corpus the produced count rose from 185 to 207 at an unchanged 60s cap.
202
+ Resolution rules that made it possible: a name carried by more than 8 files
203
+ cannot identify a target (ambiguity cap), test/header pairing is scoped to
204
+ the co-located directory first, and file-level relations (an include, a
205
+ path reference, a header/impl pair) now link one representative fragment
206
+ per file — the same representative the sibling builder always used — rather
207
+ than every-fragment-to-every-fragment.
208
+ - **The compute deadline now covers the library path** (#121). `timeout` only
209
+ ever bounded git subprocesses; pyo3/MCP callers could sit through an
210
+ unbounded edge build (observed: 8+ minutes past a 420s deadline). The edge
211
+ phase now checks a wall-clock deadline between builders and surfaces expiry
212
+ as an error instead of a hang.
213
+ - **Token cache eviction actually reaches its cap** (#122). Evicting one
214
+ random shard of 256 per run is the coupon collector's problem (~1570 runs
215
+ to touch every shard); the cache was measured at 6.3GB against its 512MB
216
+ cap. Each run now sweeps a 16-shard window, reaching full coverage in ~50
217
+ runs.
218
+ - **A tiny edit no longer ships the enclosing body as context** (#184). The
219
+ excerpt downshift compresses a changed oversized function to its hunk
220
+ window, but the body's gap chunks re-entered the output as *context* — a
221
+ 2-line edit in a 100-line function shipped 81 lines of the body it had just
222
+ compressed. Context candidates that are slices of a core fragment's span
223
+ are now dropped; signature stubs stay.
224
+ - **Tags edges are now a fallback, not dual coverage** (#131). The
225
+ identifier-pair builder ran unconditionally, duplicating every dedicated
226
+ semantic edge with same-weight noise; its emissions now survive only where
227
+ at least one endpoint file has no dedicated semantic edge at all. Largest
228
+ single corpus improvement on record: 118 cases fixed, 29 broken (net −89;
229
+ baseline 371 → 281, one stale Swift `xfail` retired). On real commits the
230
+ weakest measured languages double their nontrivial recall (Swift 0.049 →
231
+ 0.104, Elixir 0.059 → 0.137) with Tier-1 languages unchanged; the two
232
+ concentrated losses (C#, Scala one-hop reach that only tags provided) are
233
+ the named target of the dedicated-builder work in #179.
234
+ - **A gitignore negation no longer excludes the file it un-ignores** (#193).
235
+ `check-ignore -v` prints a record when the last matching pattern is a
236
+ negation — the path is then explicitly NOT ignored — and the record parser
237
+ treated any record as an exclusion, silently dropping files like an
238
+ un-ignored `SECURITY.md` from the diff context. Negation records are now
239
+ skipped for gitignore and `.diffctx/ignore` alike: a policy negation is the
240
+ user re-including a path in the policy's own terms.
241
+ - **Withheld changed files are visible as withheld** (#188). A changed file
242
+ excluded by ignore rules vanished silently — a reviewer reading the output
243
+ filed "no tests" against a change whose tests the tool had dropped.
244
+ gitignore exclusions are now listed by path under `ignored_changes`;
245
+ `.diffctx/ignore` exclusions surface as `policy_excluded_count` only, since
246
+ re-publishing the paths a declared confidentiality policy withholds would
247
+ undo the policy (#85).
248
+
249
+ - **`--scoring pit` was rejected by both CLIs.** The engine, the MCP server and
250
+ the eval harness all accepted the mode, while `diffctx --scoring pit` and the
251
+ standalone binary refused it: each CLI enumerated the accepted values in its
252
+ own literal, and neither was updated when the mode landed. Both now read
253
+ `SCORING_MODES` from the engine, and a test pins the list in both directions —
254
+ a mode the engine parses but does not advertise is as much a defect as the
255
+ reverse (#125).
256
+
257
+ - **`.diffctx/ignore` no longer stops applying when `git check-ignore` fails or
258
+ when a path contains a newline.** Two holes, both silent. The batched check
259
+ passed every diff path *plus every ancestor directory* as argv, so a
260
+ monorepo-sized range hit the platform limit, git failed to spawn, and the
261
+ result was read as "nothing is ignored" — the one answer that publishes what
262
+ the user asked to withhold. Paths now go over `--stdin` (fed from a private
263
+ temp file, not a pipe we write ourselves — writing the payload before reading
264
+ stdout deadlocks once it outgrows the pipe buffer), and the exchange is
265
+ NUL-delimited in both directions: line-delimited input split
266
+ `secret\nname.py` into two phantom queries, git answered about the stem, and
267
+ the real path never got a verdict. On failure the check now fails **closed**
268
+ when `.diffctx/ignore` declares patterns — excluding everything queried rather
269
+ than risk publishing it — and stays open when it declares none, so a bare
270
+ clone (where `check-ignore` cannot run at all) still produces output.
271
+ - **An untracked file is no longer loaded into memory to count its lines.** The
272
+ untracked scan runs before any size filter (`max_changed_file_size` is
273
+ enforced later, in fragmentation), so a dirty tree holding one multi-GB log
274
+ allocated all of it up front. Counting is now chunked over bytes with
275
+ streaming UTF-8 validation; a minified bundle that is one line hundreds of
276
+ megabytes long is bounded too, which a line-based reader would not have been.
277
+ Line counts are unchanged.
278
+ - **Acronym-prefixed test files are recognised** — `XMLTest`, `HTTPTest`,
279
+ `DBTest`, `UITest`, `IOTest`, `JSONSpec` all read as ordinary source because
280
+ the CamelCase rule demanded a lowercase character before the marker. A capital
281
+ `T` is itself the word boundary, and the markers are matched case-sensitively,
282
+ so `latest`/`contest`/`attest` were never at risk from dropping that guard
283
+ (#182). `PodSpec`/`JobSpec` and `ABTest` are accepted over-classifications:
284
+ no name-based rule separates them from `AuthSpec` and `FooTest`.
285
+ - **`locate` counted tests with its own weaker classifier**, so `summary.tests`
286
+ in the blast-radius block undercounted: it lowercased the path first, which
287
+ hides `FooTest.java`, `AuthSpec.scala`, `widget-spec.js` and `src/tests.rs`.
288
+ It now shares `crate::testfiles` with the edge builder and the needs matcher.
289
+ A `testing/` directory no longer counts as a test tree (it is Go's stdlib
290
+ package name and such directories hold helpers). Affects
291
+ `diffctx.locate.v1` output only; pack selection is unchanged.
292
+ - **An inverted line span can no longer reach a `FragmentId`.** `line_count()`
293
+ is `end - start + 1` on unsigned integers, so it panicked in debug and wrapped
294
+ to ~4 billion in release, always somewhere downstream of whoever built the
295
+ span; it had been fixed twice at call sites. The constructor now asserts in
296
+ debug and clamps to a one-line span in release.
297
+ - **Rename records with a missing destination are no longer treated as
298
+ renames.** The two `-z` walkers disagreed on validation; they are now one.
299
+
300
+ - **The lexical similarity builder no longer holds an unbounded pairwise
301
+ accumulator** (#116). Its output is bounded by `top_k_neighbors`, but the
302
+ intermediate `FxHashMap<(u32, u32), f32>` was not: it held every distinct
303
+ fragment pair co-occurring in any posting list under `max_postings`, i.e. up to
304
+ `terms x C(max_postings, 2)` entries. On one large instance that reached 199M
305
+ pairs and tens of GB resident. Contributions now accumulate into a flat vector
306
+ and reduce by pair — 12 bytes per contribution against hashbrown's ~16 plus a
307
+ transient copy of the whole table on every doubling rehash, so peak drops
308
+ several-fold and the rehash spikes disappear. The sort is deliberately
309
+ **stable**, which preserves each pair's original contribution order and keeps
310
+ the f32 sums bit-identical; `sort_unstable` would reorder within a run and f32
311
+ addition is not associative, which could flip a pair across `min_similarity`.
312
+ Pass 6's top-k cut now breaks weight ties by neighbour index, since candidate
313
+ order derives from a sorted pair list rather than hashmap iteration. Verified
314
+ byte-identical output across four scoring modes x three diff ranges, and
315
+ corpus-neutral (2902/2902).
316
+ - **One answer to "is this a test file"** (#182). Two implementations disagreed:
317
+ a per-language dispatch gating `TestEdge` emission and a flat suffix list
318
+ gating test-need match strength, so a `.kts` file was a test to one and not the
319
+ other and the two halves of "this is a test for the changed code" could hold
320
+ independently. Both also accepted any stem merely *ending* in the letters
321
+ `test` — they lowercased the name before comparing, which destroys the
322
+ CamelCase boundary the JVM/Scala convention relies on, so `latest`, `greatest`,
323
+ `contest` and `attest` were all classified as tests. Now one
324
+ `testfiles::is_test_path`, where a `test`/`spec` marker counts only at a word
325
+ boundary: its own `_`/`-`/`.`-delimited segment, or a capitalised `Test`/`Spec`
326
+ in the original name. `conftest.py` keeps its previous non-test classification,
327
+ which corpus cases depend on. Corpus-neutral (2902/2902).
328
+ - **Latency accounting reconciles with the wall clock** (#183). The phases
329
+ reported only the heavy stage, so a 182s run showed 5.8s of instrumented work
330
+ with nothing to say where the rest went — which is what made a slow range look
331
+ like an unexplained hang. The pre-heavy stage (hunk parse, untracked scan,
332
+ ignore resolution, the `git diff` calls) is now timed as `pre_phase_ms` and
333
+ included in `total_ms`, and the debug log emits `selection` (which has always
334
+ covered the three post-passes) alongside the heavy line. On the range from #121
335
+ that immediately localises the cost: `selection 216.5s, total 222.4s`. The
336
+ reported phases now sum to the total within a render-sized residual, and a
337
+ Python-level test asserts that so a future stage added outside the
338
+ instrumentation fails loudly instead of vanishing — it caught `pre_phase_ms`
339
+ missing from both pybridge call sites while being written.
340
+ - **Flat files finally get sub-file granularity** (#105, #107). An uncovered
341
+ region became one fragment however long it was, so a file the grammar extracts
342
+ nothing from — a flat bash script, a `CMakeLists.txt`, any language without a
343
+ grammar — collapsed into a single whole-file chunk and nothing narrower could
344
+ ever be selected. Long gaps are now split into bounded chunks, cutting at a
345
+ blank line where one is within reach, reusing the thresholds that already
346
+ govern sub-fragmenting large definitions rather than adding a second size
347
+ policy. Two consequences: a one-line diff no longer renders the whole file, and
348
+ the file's *unchanged* remainder becomes available as context at a useful
349
+ granularity instead of being all-or-nothing. On the corpus this moved
350
+ `gap_160_shell_terraform_vault_init_single_import` from below-threshold to a
351
+ full 100%, so its baseline entry is gone.
352
+ - **`xfail:` in a YAML case was an unconditional silent skip.** The runner
353
+ returned success before building the repo, so a marked case proved nothing and
354
+ an XPASS was unobservable — the day its bug got fixed it still reported a pass.
355
+ `known_below_threshold.txt` is enforced bidirectionally for exactly this
356
+ reason; the two suppression mechanisms now agree. Marked cases run, and one
357
+ that passes fails with instructions to drop the marker. Twelve of the 44
358
+ markers turned out to be stale and were removed (ansible jinja templates,
359
+ HTML `script src`, Rust `build.rs`, an abstract-class case, six C#/PHP one-hop
360
+ cases, Haskell record syntax, a shell/terraform same-file case); 32 remain
361
+ legitimately failing.
362
+ - **Excerpt downshift for mostly-unchanged cores** (#149, closing #105, #107 and
363
+ the scoping half of #114). A core fragment is now rendered as its hunk-window
364
+ excerpt whenever that window covers no more than half of it, instead of only
365
+ when the budget forces the substitution. The old rule made granularity a
366
+ function of leftover budget rather than of how much actually changed: the same
367
+ 402-line `CMakeLists.txt` shipped whole at `--budget 8000` and tightly
368
+ excerpted at a small budget.
369
+
370
+ Two changes make it work. Excerpts are now generated for kinds that *have* a
371
+ signature variant as well — a signature is not a substitute for a
372
+ mostly-unchanged function, because it drops the changed lines, which is the
373
+ one thing the fragment was selected to carry. And `select_core_fragments` now
374
+ reports which cores it satisfied rather than letting the caller infer it from
375
+ id membership: a core represented by a substitute has a different id, so it
376
+ looked "skipped" and the full fragment was handed straight back to the greedy,
377
+ which re-emitted it alongside the excerpt.
378
+
379
+ `render` also had to agree with `locate` that an `Excerpt` carries the
380
+ `changed` role. Its id is a synthetic span cut out of the core and is not in
381
+ `core_ids`, so without that the downshift would have stripped the change
382
+ marker from the output — worse than the over-dump it replaces.
383
+
384
+ Measured on the reported shapes: a 122-line bash script with one changed line
385
+ goes from the whole file to a 7-line window; a 402-line `CMakeLists.txt` from
386
+ the whole file to 7 lines; a 264-line JavaScript function with two changed
387
+ lines from the whole function to an 8-line window plus the enclosing
388
+ signature as context.
389
+ - **Diff-header paths escaping the repository root were accepted on Linux.**
390
+ `Path::starts_with` compares components, not locations, and `canonicalize`
391
+ fails for a path that does not exist — the normal case for the old side of a
392
+ deletion. The guard fell back to the lexically joined path, and
393
+ `<root>/../../escape.py` starts with `<root>` component-wise. It only failed
394
+ on macOS, where the temp root canonicalizes through `/var → /private/var`, so
395
+ the protection rested on an accident of filesystem layout; on Linux the
396
+ accepted path reached `read_file_content`, whose `exists()` check the OS
397
+ resolves through `..`, so content outside the repository could be read into
398
+ the output. A `..` or absolute component is now rejected before any
399
+ resolution. `--with-raw-diff`'s section filter was a second copy of the same
400
+ guard with the same hole; both now share one resolver (#147).
401
+ - **`core.excludesFile` was written to a guessable name with `fs::write`**,
402
+ which follows a symlink and truncates its target — so a pre-planted name in
403
+ the shared temp directory could redirect the write. Now `O_CREAT | O_EXCL`
404
+ with mode 0600 and a bounded retry (#147).
405
+ - **A rejected or unrecognized diff header charged its hunks to the previous
406
+ file.** The hunk loop could not tell "not a path line" from "path refused",
407
+ so the previous file's paths stayed live. Reset now happens on `diff --git`,
408
+ the per-file boundary git always emits.
409
+ - **`TestFileDiscovery` matched the changed file's bare stem repo-wide**, so one
410
+ changed `mod.rs` pulled in every other `mod.rs` in the tree. Bare-stem pairing
411
+ is now scoped to the changed file's own directory, which is what makes such a
412
+ pair meaningful; test prefixes and suffixes still match anywhere (#65).
413
+ - **Externally imported types became unanswerable information needs.** The
414
+ external-symbol filter gated only the call-reference loop, so
415
+ `from typing import Optional` plus `: Optional[int]` demanded a definition of
416
+ `optional` from the repository — permanently unsatisfied, which inflates the
417
+ diversity bonus for every candidate and lends match strength to any file
418
+ mentioning the same stdlib type (#65).
419
+ - **The nontrivial-context rescue could spend its whole allowance on one file.**
420
+ Its "file not yet represented" filter was a snapshot taken before the loop.
421
+ The metric it serves is file-level, so a second fragment of an already-reached
422
+ file buys nothing and costs the next file its chance.
423
+ - **Core seed selection depended on fragment order.** The tie-breaks compared
424
+ kind class and span length only, so equally-ranked candidates were resolved by
425
+ parser emission order — reversing it moved which code the output marks as
426
+ changed. All three selectors now end on the fragment id.
427
+ - **Two panics reachable from repository content**: generated-fragment
428
+ truncation checked the cap against the id's line span but sliced the actual
429
+ text, and signature generation produced an id whose end preceded its start
430
+ when a fragment's span was wider than its content. Both are now clamped.
431
+ - MCP `get_file_context` reported the raw glob match while checking containment
432
+ on the resolved path, so a file accepted through a symlink raised an
433
+ uncaught `ValueError` instead of returning its contents (#147).
434
+ - `build_locate` rejected an empty `diff_range` that `build_diff_context` treats
435
+ as the working tree, so two entry points into one pipeline disagreed.
436
+ - Sibling grouping deduped with `Vec::contains` against the bucket, making it
437
+ quadratic in exactly the shape it exists for — thousands of files in one
438
+ directory (#116).
439
+
440
+ ### Removed
441
+
442
+ - The dead `low_relevance_threshold` gate.
443
+ `PipelineConfig.low_relevance_filter` was `false` for every scoring mode, so
444
+ `filter_low_relevance` — and with it `FILTERING.low_relevance_threshold`
445
+ (0.015) and its `size_penalty_*` scaling — could never execute;
446
+ `filter_positive_relevance` was always the gate that ran. ContextBench result
447
+ rows nevertheless stamped `low_relevance_threshold: 0.015` into their `config`
448
+ block, so every recorded run attributed its selections to a threshold that
449
+ never fired. Removed on both sides. Bit-equivalent: identical output across
450
+ four scoring modes × three diff ranges, plus the full 2725-case corpus.
451
+ - Phantom knobs that only ever fed a discarded value:
452
+ `GRAPH_FILTERING.git_rename_similarity_threshold` (its return value was
453
+ dropped at the single call site) and `GitConfig`'s `poll_interval_ms` /
454
+ `default_timeout_seconds` fields, which nothing read. Also a
455
+ `_check_allowed(path.resolve())` in the MCP path validator that ran under a
456
+ comment claiming it closed a symlink-swap race, while comparing the same
457
+ already-resolved value against the same allowlist.
458
+
459
+ ## [1.12.3] - 2026-07-27
460
+
461
+ ### Added
462
+
463
+ - **Scoop install on Windows.** This repository is now itself the bucket:
464
+ `scoop bucket add diffctx https://github.com/nikolay-e/diffctx` then
465
+ `scoop install diffctx/diffctx`. The manifest moved from `packaging/scoop/`
466
+ to `bucket/diffctx.json`, where `scoop bucket add` reads it, so CD pushing
467
+ the regenerated manifest to `main` is the publication step. Previously the
468
+ manifest was correct but unreachable — no bucket existed, so
469
+ `scoop search diffctx` found nothing.
470
+
471
+ - `--with-raw-diff` bundles git's raw unified diff ahead of the selected
472
+ context in every Python-CLI format (md/yaml/json/txt) and in the Python API
473
+ (`build_diff_context(..., with_raw_diff=True)`). Additive only — selection is
474
+ byte-identical with and without it — and not charged to `--budget`; the
475
+ stderr token summary reports the real output size and breaks out the patch's
476
+ share. Lock-file (#112), ignored and secret-like sections stay omitted. Not
477
+ in the native binary or the MCP server yet (#150).
478
+ - Token counting is documented explicitly: every count and `--budget` use
479
+ tiktoken `o200k_base` (GPT-4o family) and are approximate for other model
480
+ families — `docs/product/token-budget.md`, plus a `Token counting` section in
481
+ `--help` (#150).
482
+ - A GitHub Action (`action.yml`) runs diffctx as a CI step and exposes the
483
+ context file, its exact token count and an `empty` flag as step outputs, so a
484
+ downstream job can feed an LLM without re-tokenizing
485
+ (`docs/product/github-action.md`, #145).
486
+ - The Claude Code plugin declares its MCP server self-bootstrapping:
487
+ `uvx --from 'diffctx[mcp]' diffctx-mcp`. Installing the plugin no longer
488
+ assumes `pip install 'diffctx[mcp]'` has already happened — `uv` fetches it
489
+ on first use. (The entry point rather than the `diffctx mcp` subcommand: the
490
+ subcommand only exists from this release on, so on any older version the
491
+ bare command would try to map a directory named `mcp`.)
492
+ - `diffctx mcp` starts the MCP server, alongside the existing `diffctx-mcp`
493
+ console script. MCP registries publish a *package* name, and clients that
494
+ derive the executable from it run `diffctx` — which started a tree-mapping
495
+ run and wrote 31 MB of the working directory into the protocol transport
496
+ instead of speaking MCP. Both spellings now reach the same server.
497
+
498
+ ### Security
499
+
500
+ - **A diff range could re-enable repository-configured diff commands.** The
501
+ range validator allowed a leading `-`, so `--ext-diff` or `--textconv` passed
502
+ as a "range" landed in argv *after* the `--no-ext-diff --no-textconv` that
503
+ every diff invocation sets — undoing them and letting `.gitattributes` run
504
+ external commands. Neither side of a range may now begin with a dash, and
505
+ single revisions are additionally rejected for whitespace or control
506
+ characters (a newline split one `cat-file --batch` request into two). Refs
507
+ starting with a dash are unaddressable on a git command line, so nothing
508
+ legitimate is lost.
509
+ - MCP tool calls had no wall-clock deadline — the CLI's watchdog covered only
510
+ the CLI, leaving parse, fragmentation and scoring unbounded on the MCP path.
511
+ All three tools now share the CLI's 300 s default.
512
+ - `SECURITY.md` documents prompt injection via repository content honestly: it
513
+ is inherent to moving repository text into a model's context, diffctx does
514
+ not detect or neutralize it, and output must be treated as untrusted.
515
+
516
+ ### Fixed
517
+
518
+ - **A changed file could vanish from the diff entirely when `.gitignore`
519
+ excluded its parent directory.** `check-ignore --no-index` — required for
520
+ `.diffctx/ignore` to apply to tracked files — also revives git's rule that a
521
+ file cannot be re-included once a parent directory is excluded. pandoc
522
+ excludes every dotted root entry with `/*.*` and re-includes `!.github/**`;
523
+ git keeps the tracked workflow file, diffctx dropped it, and a one-file
524
+ commit rendered as an empty selection with exit 4. An exclusion inherited
525
+ from an excluded ancestor no longer counts; a pattern matching the path
526
+ itself still excludes it, so `.diffctx/ignore` and per-directory
527
+ `.gitignore` rules are unaffected (#153).
528
+ - A user-level git config (`diff.noprefix`, `diff.external`, `color.ui=always`,
529
+ custom src/dst prefixes) could empty the selection: diffctx now pins the
530
+ diff invocation flags instead of inheriting the caller's configuration.
531
+ - `cd.yml` now dispatches `publish-extras.yml`, so npm and the Docker Hub
532
+ mirror track every release instead of silently serving the previous one.
533
+
534
+ ### Removed
535
+
536
+ - **AUR support.** `diffctx-bin` had never been submitted (the AUR RPC reported
537
+ `resultcount: 0`), the publishing job gated on a credential this repository
538
+ does not hold, and every release regenerated a PKGBUILD nobody could install.
539
+ Arch users are served by `pipx install diffctx` and `cargo install diffctx`.
540
+
541
+ ## [1.12.2] - 2026-07-26
542
+
543
+ ### Fixed
544
+
545
+ - **The container images ran as root.** The published 1.12.1 image had no
546
+ `USER` directive, so every `docker run` executed as uid 0 while the README
547
+ promised an unprivileged user. The image now runs as uid 10001.
548
+ - **The MCP server advertised the SDK's version as its own.** `FastMCP` takes
549
+ no version argument, so `initialize` reported the installed `mcp` package
550
+ version (e.g. `1.28.1`) as the diffctx server version — a number that drifts
551
+ with every SDK bump and never matched the shipped package. Clients now see
552
+ the real version.
553
+ - **The native binary hard-capped every run at 4096 tokens.** `--budget`
554
+ carried a fixed clap default instead of leaving the budget unset, so the
555
+ auto-sizing the Python CLI has always used never ran on the binary shipped
556
+ via crates.io, npm, the container images and the release archives — the
557
+ same command returned a truncated selection there (14-file
558
+ self-eat range: 15 fragments across 10 files, against 172 across 44). The
559
+ binary now defaults to auto sizing, and `--budget -1` means unlimited as it
560
+ does in Python.
561
+ - The token cache grew without bound (a machine that had analyzed a few dozen
562
+ repositories reached 817k files / 4.0 GB). It is now capped at 512 MB,
563
+ overridable with `DIFFCTX_TOKEN_CACHE_MAX_BYTES` (`0` disables eviction);
564
+ each run trims one of the 256 shards back under its share of the cap,
565
+ oldest entries first (#122).
566
+ - The native binary exited `0` on a diff that produced no semantic context
567
+ (clean tree, binary-only, everything over the size cap), so callers on the
568
+ binary channels could not tell an empty selection from a successful one. It
569
+ now mirrors the Python CLI: the `no semantic context` warning plus its hint
570
+ on stderr and exit code `4`.
571
+ - The native binary printed no token summary, although the README documents
572
+ one for every run. It now writes `N tokens (o200k_base), SIZE` to stderr,
573
+ silenced by the new `-q/--quiet`.
574
+ - `diffctx -v` was a usage error on the native binary while it printed the
575
+ version on the Python CLI. Both short forms (`-v`, `-V`) now work.
576
+ - **A diff could lose its change signal entirely.** When the only fragment
577
+ covering a hunk was a whole-file chunk — flat data files, languages without a
578
+ tree-sitter parser, parse degradation — that chunk had no cheap variant to
579
+ fall back on, so an oversized core was skipped and the output carried *zero*
580
+ `role: "changed"` fragments while rendering the unchanged rest of the file as
581
+ context. Such cores now fall back to an excerpt: the changed lines plus three
582
+ lines of context, cut from the chunk itself and rendered as `kind: excerpt`,
583
+ `role: "changed"`. On the elasticsearch `muted-tests.yml` repro the output
584
+ goes from 121 fragments and no change signal to 118 fragments carrying the
585
+ appended entry, 5,337 tokens instead of 5,970 (#103). The whole-file
586
+ fragmentation of flat lists that the same repro shows is the separate
587
+ granularity issue (#105) and is unchanged.
588
+ - A diff whose selection came back empty rendered as a bare `name`/`type` stub
589
+ in every format: the writer gated *all* diff metadata on there being
590
+ fragments, so the commit message and the list of changed files — the only
591
+ actionable facts about such a run — were dropped. They are now always
592
+ written.
593
+
594
+ ### Changed
595
+
596
+ - **Lock files no longer render their hunks in diff mode.** A dependency bump
597
+ is signal, the checksum churn carrying it is not: `diffctx . --diff` used to
598
+ spend 12 KB of a 48 KB output on a `Cargo.lock` chunk. The touched lock files
599
+ are now listed under `lockfile_changes:` (paths only, like `deleted_files:`),
600
+ mirroring the tree-mode ignore policy while keeping the fact that they
601
+ changed — on the reported range the output drops from 12,947 to 7,888 tokens
602
+ (#112). Recognized: `Cargo.lock`, `package-lock.json`, `npm-shrinkwrap.json`,
603
+ `yarn.lock`, `pnpm-lock.yaml`, `bun.lock(b)`, `deno.lock`, `Pipfile.lock`,
604
+ `poetry.lock`, `uv.lock`, `pdm.lock`, `composer.lock`, `Gemfile.lock`,
605
+ `flake.lock`, `go.sum`, `mix.lock`, `packages.lock.json`, `gradle.lockfile`,
606
+ `Package.resolved`, `cabal.project.freeze`. `--full` still renders their
607
+ content — it is the escape hatch that promises every fragment of the changed
608
+ files.
609
+ - Native binary CLI parity: `-f` is accepted as the short form of `--format`,
610
+ bare `--diff` means the working tree against `HEAD`, `--scoring` advertises
611
+ its accepted values, and every option carries help text.
612
+ - The standalone binary is covered by its own integration suite
613
+ (`crates/diffctx-native/tests/native_cli.rs`, run in CI): exit codes, both
614
+ version flags,
615
+ the token summary, `--quiet`, format validation and budget handling are now
616
+ contract-tested against real git repositories. Every parity defect above
617
+ shipped because nothing exercised the clap parser.
618
+
619
+ ## [1.12.1] - 2026-07-23
620
+
621
+ ### Added
622
+
623
+ - Standalone native binaries (linux x86_64/aarch64, macOS arm64, Windows
624
+ x64) are built and attached to every GitHub release. The README had
625
+ promised them since 1.9.x; releases only ever carried wheels and an sdist.
626
+ - The Rust engine is published to crates.io as the `diffctx` crate
627
+ (`cargo install diffctx` for the native CLI, `cargo add diffctx` to embed the
628
+ selection pipeline). Previously the name held only a reservation stub; the
629
+ crate now carries the released engine, starting at 1.12.0.
630
+ - Container image `ghcr.io/nikolay-e/diffctx` (linux amd64/arm64), built from
631
+ the release tag and smoke-tested against a real repository before the tag
632
+ moves: `docker run --rm -v "$PWD:/repo" ghcr.io/nikolay-e/diffctx . --diff HEAD~1`.
633
+ Mirrored to Docker Hub as `nikolajer/diffctx`.
634
+ - npm wrapper (`packaging/npm/`) that downloads the platform binary and
635
+ verifies its SHA-256 against the published checksum. Scoop
636
+ (`packaging/scoop/diffctx.json`) and AUR (`packaging/aur/`) manifests are
637
+ generated from the same checksums but are not published to any bucket or
638
+ to the AUR.
639
+
640
+ ### Fixed
641
+
642
+ - The native binary silently emitted YAML for every unrecognized `--format`,
643
+ including `md` — the documented default of the Python CLI. It now accepts
644
+ only `yaml`/`json` and exits 2 on anything else.
645
+
646
+ ## [1.12.0] - 2026-07-23
647
+
648
+ ### Added
649
+
650
+ - `--timeout SECONDS` — wall-clock deadline for `--diff` analysis (default
651
+ 300); exceeding it exits `124` instead of hanging indefinitely (#70).
652
+ - `--no-ignores` — turns off every ignore rule (built-in patterns, project
653
+ `.gitignore`, `.diffctx/ignore`). `--no-default-ignores` only disables the
654
+ built-in list; its help now says so. Not supported with `--diff`.
655
+ - Output format is inferred from the `-o` extension when `-f` is omitted, so
656
+ `-o out.json` no longer writes Markdown into a `.json` file; a mismatch
657
+ between `-f` and the extension warns.
658
+
659
+ ### Fixed
660
+
661
+ - **All error logging was dead.** An import-time `NullHandler` made
662
+ `setup_logging` skip attaching a real handler, so `--log-level` was a no-op
663
+ and all 19 `logger.error/warning/exception` sites were silent — `diffctx . -o
664
+ /bad/path.md` exited 1 with no message at all.
665
+ - **diffctx invoked from inside a git hook silently analyzed the wrong
666
+ repository.** Git exports repo-locating env vars (`GIT_DIR`,
667
+ `GIT_INDEX_FILE`, `GIT_WORK_TREE`, ...) to hook subprocesses; inherited,
668
+ they overrode `-C` on every internal git call. All git spawns now scrub
669
+ these variables (`git_command()` in `git.rs`).
670
+ - YAML output preserved file content byte-exactly except for trailing
671
+ newlines; the block chomping indicator is now chosen per content.
672
+ - Arrow-function fragments bound to variables were never stub-eligible (#106).
673
+ - Decorated definitions rendered as a bare `@decorator` line without the
674
+ `class X:` / `def x():` header.
675
+ - `--max-depth`-pruned directories were labelled `_(empty directory)_` — a
676
+ factual lie to the reader; they now read
677
+ `_(children omitted: --max-depth reached)_` (`truncated: true` in
678
+ YAML/JSON).
679
+ - Mixed directory + glob arguments dropped the glob files' parent path from
680
+ node names.
681
+ - Double Ctrl-C printed a ~60-line traceback.
682
+ - Lock files `uv.lock`, `pdm.lock`, `bun.lock`, `bun.lockb`, `deno.lock` and
683
+ `flake.lock` leaked into output; they now join the other lock files in the
684
+ default ignore patterns.
685
+ - Large-repo hangs/OOM on trivial diffs (#70, #95): discovery no longer
686
+ re-reads and re-tokenizes the whole candidate universe per ensemble
687
+ strategy (one shared pass + a persistent per-blob token cache keyed by
688
+ `(blob OID, tokenizer epoch)`), and edge construction is two-pass with a
689
+ bounded per-source top-K instead of materializing up to tens of millions
690
+ of raw edges before the cap; pass 2 replays a compact 16-byte-per-emission
691
+ log instead of re-running the builders, so generation cost stays 1x.
692
+ Verified: gitpod 8000s-hang -> 35.7s,
693
+ pytorch 1848s-SIGKILL -> 7.4s, mui/material-ui OOM class recovered.
694
+ Outputs are bit-identical (gated by `eval/analysis/equivalence_gate.py`).
695
+
696
+ ### Known limitations
697
+
698
+ - Near-dense edge emission on huge same-directory trees (observed: 199M
699
+ raw edges, 37GB peak on one mui/material-ui instance) remains expensive
700
+ even with bounded construction; tracked in #116.
701
+
702
+ ### Changed
703
+
704
+ - **The token budget is now a hard cap.** The changed-files post-pass no
705
+ longer exceeds the budget to guarantee representation: a changed file
706
+ whose cheapest representative does not fit stays unrepresented (visible
707
+ as changed-file retention < 1). `--budget 0` therefore yields an empty
708
+ selection (use `--full` for changed files only); CLI help updated.
709
+ - Latency telemetry: new `graph_build_ms` phase (graph construction was
710
+ previously misattributed to `scoring_ms`, which now measures pure
711
+ ranking) and `peak_rss_bytes` (in-process peak memory). Release builds
712
+ carry line tables (`debug = "line-tables-only"`) for profiling at no
713
+ runtime cost.
714
+ - CLI diagnostics are honest end to end: an exit-code table in `--help`
715
+ (2 usage, 3 environment, 4 empty diff, 124 timeout), flag-value validation
716
+ exits 2 instead of 1, git failures report a single line plus a
717
+ `git log --oneline` hint on unknown revisions, conflicting flags warn
718
+ (`-q`+`--log-level`, `--full`+selection flags, ...), a failed clipboard
719
+ copy warns before falling back to stdout, and `--tau` / `--scoring` /
720
+ `--alpha` / `--budget 0` help text describes what actually happens.
721
+ - `graph --summary` reports category shares as percentages, suppresses
722
+ degenerate top-referenced lists, detects cycles over dominant-direction
723
+ edges only, derives churn from `git log --since`, and disambiguates
724
+ duplicate mermaid labels to relative paths.
725
+
726
+ ## [1.11.0] - 2026-07-07
727
+
728
+ ### Changed
729
+
730
+ - **The default `--format` is now `md` (Markdown), previously `yaml`.** Markdown
731
+ is ~7% more token-efficient than YAML on real diffs and is preferred by
732
+ reviewers for code-fragment-heavy output (evidence:
733
+ `datasets/real-world-diff/v1/`). YAML remains available via `-f yaml`.
734
+ This also changes the default stdout format of `treemapper` (a passthrough
735
+ wrapper over this engine); downstream scripts that parse the default output
736
+ must now pass `-f yaml` explicitly. (#104)
737
+
738
+ ## [1.10.0] - 2026-06-20
739
+
740
+ ### Changed
741
+
742
+ - The default `--tau` (stopping threshold) is now **0.12** across the CLI, MCP,
743
+ and Python API — the calibrated grid optimum — bringing the Python side in
744
+ line with the standalone Rust binary (was 0.08). Diff-context selections are
745
+ slightly tighter by default.
746
+ - The MCP `get_tree_map` / `get_file_context` default `max_file_bytes` is now
747
+ 256 KB, matching the documented CLI default (was 100 KB).
748
+
749
+ ### Fixed
750
+
751
+ - Document/citation edges can no longer create self-edges (a fragment linking to
752
+ itself); `Graph::add_edge` now rejects `src == dst`.
753
+ - Test→source import edges no longer match `import` statements that appear inside
754
+ comments or string literals (the import regex is now line-anchored).
755
+ - The MCP `get_file_context` clipboard confirmation now reports the number of
756
+ files actually copied, excluding files skipped for exceeding the size cap.
757
+
758
+ ### Docs
759
+
760
+ - Corrected the AST-parsing language count (was "12 languages", now "30+"),
761
+ aligned the Rust crate version with the wheel, completed the MCP server README
762
+ (all three tools documented), and fixed the documented `EGO.per_hop_decay`
763
+ default (0.5). Documented the `scoring_mode` / `timeout` Python API params,
764
+ clarified `--budget` / `--tau` help text, and marked the `DIFFCTX_OP_*` /
765
+ `DIFFCTX_*` tuning env vars as an experimental, non-public interface.
766
+
767
+ ## [1.9.2] - 2026-06-14
768
+
769
+ ### Added
770
+
771
+ - Private-key and keystore files are now excluded from output in **both** tree
772
+ mapping and `--diff` context, since such material is never legitimate LLM
773
+ context: `*.pem`, `*.key`, `*.pfx`, `*.p12`, `*.keystore`, `*.jks`, and SSH
774
+ private keys `id_rsa`/`id_dsa`/`id_ecdsa`/`id_ed25519` (public `.pub` keys stay
775
+ visible). The `--diff` path previously applied no ignore filtering at all, so a
776
+ changed key file would have leaked into context. Use `--no-default-ignores` to
777
+ opt out of tree-mode default ignores. (`.env` files are intentionally still
778
+ included — a changed `.env` is legitimate change context; redacting secret
779
+ *values* is a separate planned content-scan feature.)
780
+
781
+ ### Fixed
782
+
783
+ - **Diffs touching files larger than 100 KB no longer produce empty output.** A
784
+ changed file (e.g. a 142 KB `Math.h`) was subject to the same 100 KB cap as
785
+ context-discovery candidates, so the whole diff yielded "no semantic context".
786
+ Changed files — the subject of the diff — are now parsed up to 5 MB.
787
+ - **C/C++ symbol names** for variable declarations now report the variable, not
788
+ the type: a changed `const unsigned blane = …` is labeled `blane`, not
789
+ `unsigned`. Covers `init`/`array`/pointer/function declarators and C++
790
+ `reference`/`parenthesized` declarators (`int &r`, `int (*f)()`).
791
+ - Changed-file fragments are no longer dropped by the generated-file reduction
792
+ (cap of 5 + 30-line content truncation), which could discard the small
793
+ fragment covering the edited hunk and mislocate the change.
794
+ - Binary detection no longer misclassifies changed text files that embed ANSI
795
+ escape / control bytes (snapshot and terminal-recording fixtures); it now uses
796
+ git's NUL-byte heuristic, so such files are no longer silently dropped.
797
+ - Diff-context selection is now deterministic across processes: ego-graph score
798
+ accumulation, context-fragment capping, and the greedy selection heap use
799
+ stable tie-breaks, so identical inputs always yield byte-identical output.
800
+ - Python module-import edges are bidirectional (matching every other language
801
+ and Python's own symbol references), so a changed imported module now
802
+ propagates relevance back to its importers.
803
+
804
+ ### Hardened
805
+
806
+ - `git cat-file` blob reads are bounded (drain-and-reject above 16 MB) instead of
807
+ allocating an arbitrarily large buffer up front.
808
+
809
+ ## [1.9.1] - 2026-06-14
810
+
811
+ > Supersedes 1.9.0, which was tagged but never published to PyPI (release-process
812
+ > hiccup); 1.9.1 ships the same changes. There is no 1.9.0 on PyPI.
813
+
814
+ ### Added
815
+
816
+ - Diff-context output now leads with an orientation header — `commit_message`
817
+ and `changed_files` — in every format (YAML/JSON/Markdown/text), so a reader
818
+ sees *what* changed before reading any fragment.
819
+ - Each fragment carries a `role` of `changed` when it overlaps the diff hunks
820
+ (omitted for supporting context). Changed code is emitted first; context
821
+ follows, ordered by descending per-file relevance instead of alphabetically.
822
+
823
+ ### Changed
824
+
825
+ - Line-contiguous fragments of the same role within a file are merged into a
826
+ single entry, cutting the per-fragment scaffolding that dominated output made
827
+ up of one-line snippets (lossless on line coverage).
828
+
829
+ ### Fixed
830
+
831
+ - `get_changed_files` and `get_untracked_files` canonicalize paths consistently
832
+ with deleted/discovered files, preventing duplicate fragments when a tracked
833
+ path traverses a symlinked directory.
834
+ - Signature extraction no longer terminates at braces inside parameter defaults
835
+ or annotations (e.g. Python `def f(x={}):`), which previously truncated the
836
+ signature mid-parameter-list.
837
+ - The post-pass that rescues unrepresented changed files reuses the open
838
+ `git cat-file --batch` reader instead of spawning a `git show` per file.
839
+ - Degraded token-count fallback returns 0 for the empty string (was 1).
840
+
841
+ ## [1.8.0]
842
+
843
+ ### Added
844
+
845
+ - Public `diffctx.run(argv=None, *, prog=None, version=None)` engine entry. It
846
+ is the same execution path as the `diffctx` console script but accepts an
847
+ injected program name and version string, so a downstream wrapper (e.g. the
848
+ `treemapper` distribution) can present its own branding in `--help`,
849
+ `--version`, and error prefixes without duplicating the CLI. `main()` now
850
+ delegates to `run()` with the default `diffctx` identity — no behavior change
851
+ for existing callers.
852
+ - `diffctx.mcp.__main__.main(prog=..., extra=...)` accepts an injected program
853
+ name and install-extra string for the missing-dependency hint, so wrappers
854
+ can re-expose the MCP entry point with their own name.
855
+
856
+ ## [1.7.0] - 2026-05-22
857
+
858
+ ### Added
859
+
860
+ - README documents the `graph` subcommand and the `--scoring {ppr,ego,bm25}`
861
+ flag (default: `ego`); the absolutist "Uses Personalized PageRank" language
862
+ has been replaced with a table covering all three scoring modes.
863
+ - `SECURITY.md` now ships a "Threat model" section that scopes diffctx as a
864
+ local CLI (filesystem + git subprocess, no network) and documents that the
865
+ optional `diffctx-mcp` server confines its filesystem reach via
866
+ `DIFFCTX_ALLOWED_PATHS`.
867
+ - Footer of `README.md` links `CHANGELOG.md`, `SECURITY.md`, and
868
+ `docs/engineering/parameter-strategy.md` so they are no longer orphaned.
869
+
870
+ ### Changed
871
+
872
+ - **Package renamed from `treemapper` to `diffctx`.** PyPI distribution, CLI
873
+ binary, MCP server binary, and Python import path all use `diffctx`. The
874
+ `treemapper` PyPI package remains at 1.6.1 (frozen); install
875
+ `diffctx` for all new development. The `TREEMAPPER_ALLOWED_PATHS`
876
+ environment variable is now `DIFFCTX_ALLOWED_PATHS`.
877
+ - Single source of truth for the package version: the Python wheel reads its
878
+ version from `Cargo.toml` (via maturin) instead of duplicating it in
879
+ `pyproject.toml`, eliminating the Rust-crate-vs-Python-package version
880
+ desync that previously shipped to PyPI.
881
+ - `--diff` now defaults to `HEAD` when no range is supplied, matching the
882
+ most common invocation (`diffctx . --diff` → diff of the working tree
883
+ against `HEAD`) and saving an argument in the 30-second demo path.
884
+ - `numpy` moved from a required runtime dependency into the `[tree-sitter]`
885
+ extra, so default installs no longer pull a ~20 MB scientific stack the
886
+ core tree-mapping mode does not use.
887
+ - CLI error messages are now actionable: instead of a raw Python traceback,
888
+ invalid `--diff` ranges, missing git repositories, and unreadable paths
889
+ print a one-line `Error: <what> — try: <next step>` and exit with code `2`
890
+ for user-input errors (`1` is reserved for runtime failures).
891
+ - `automerge.yml` GitHub Actions workflow hardened: explicit minimal
892
+ `permissions:` block, pinned action SHAs, and a guard that refuses to
893
+ auto-merge anything touching `.github/`, `pyproject.toml`, or `Cargo.toml`.
894
+
895
+ ### Fixed
896
+
897
+ - Replaced every user-reachable `unwrap()`/`expect()` in the Rust core
898
+ (`tokenizer.rs`, `git.rs`, `scoring.rs`, `pybridge.rs`) with proper
899
+ `PyRuntimeError` / `GitError` propagation. A malformed diff, a missing
900
+ BPE table, or an oversized hunk-header integer no longer aborts the
901
+ Python interpreter via `panic = "abort"`.
902
+ - `diffctx-mcp` entry point now guards against being launched without the
903
+ `[mcp]` extra installed and prints an install hint instead of an
904
+ `ImportError` traceback.
905
+
906
+ ### Removed
907
+
908
+ - Dropped Kotlin and F# from the language matrix: tree-sitter grammars for
909
+ both were silently misaligned with the project's import-resolution rules
910
+ and produced misleading edge weights. They will return once the grammars
911
+ are vetted.
912
+
913
+ ### Security
914
+
915
+ - MCP server (`diffctx-mcp`) now refuses to traverse outside the
916
+ directories listed in `DIFFCTX_ALLOWED_PATHS` (OS-pathsep-separated)
917
+ and refuses to start if the envvar is unset when run as a network-facing
918
+ process. See [`SECURITY.md`](SECURITY.md) for the threat model.
919
+
920
+ ## [1.6.1 and earlier]
921
+
922
+ Earlier releases shipped as `treemapper`; see
923
+ <https://pypi.org/project/treemapper/#history> for legacy versions and
924
+ <https://github.com/nikolay-e/diffctx/releases> for the corresponding GitHub
925
+ release notes (`1.0.0` through `1.6.1`).
926
+
927
+ [Unreleased]: https://github.com/nikolay-e/diffctx/compare/v1.12.3...HEAD
928
+ [1.12.3]: https://github.com/nikolay-e/diffctx/compare/v1.12.2...v1.12.3
929
+ [1.12.2]: https://github.com/nikolay-e/diffctx/compare/v1.12.1...v1.12.2
930
+ [1.7.0]: https://github.com/nikolay-e/diffctx/compare/v1.6.1...v1.7.0