diffctx 1.12.3__tar.gz → 1.14.0__tar.gz

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