diffctx 1.17.0__tar.gz → 1.18.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.17.0 → diffctx-1.18.0}/CHANGELOG.md +48 -1
  2. {diffctx-1.17.0 → diffctx-1.18.0}/Cargo.lock +1 -1
  3. {diffctx-1.17.0 → diffctx-1.18.0}/PKG-INFO +9 -6
  4. {diffctx-1.17.0 → diffctx-1.18.0}/README.md +5 -2
  5. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/Cargo.toml +2 -2
  6. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/README.md +5 -2
  7. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/discovery.rs +126 -0
  8. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/effective_config.rs +2 -0
  9. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/graph.rs +33 -0
  10. diffctx-1.18.0/crates/diffctx-native/src/hook.rs +644 -0
  11. diffctx-1.18.0/crates/diffctx-native/src/impact.rs +707 -0
  12. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/in_memory_harness.rs +2 -1
  13. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/lib.rs +2 -0
  14. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/locate.rs +18 -1
  15. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/main.rs +101 -4
  16. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/pipeline.rs +99 -11
  17. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/pybridge.rs +58 -0
  18. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/render.rs +9 -1
  19. {diffctx-1.17.0 → diffctx-1.18.0}/pyproject.toml +3 -3
  20. {diffctx-1.17.0 → diffctx-1.18.0}/src/diffctx/_app.py +38 -4
  21. {diffctx-1.17.0 → diffctx-1.18.0}/src/diffctx/_diffctx.pyi +9 -0
  22. diffctx-1.18.0/src/diffctx/_native/__init__.py +7 -0
  23. {diffctx-1.17.0 → diffctx-1.18.0}/src/diffctx/_native/pipeline.py +24 -0
  24. {diffctx-1.17.0 → diffctx-1.18.0}/src/diffctx/cli.py +12 -2
  25. {diffctx-1.17.0 → diffctx-1.18.0}/src/diffctx/mcp/legacy.py +8 -10
  26. {diffctx-1.17.0 → diffctx-1.18.0}/src/diffctx/mcp/server.py +122 -35
  27. diffctx-1.18.0/src/diffctx/version.py +1 -0
  28. {diffctx-1.17.0 → diffctx-1.18.0}/src/diffctx/writer.py +29 -8
  29. diffctx-1.17.0/src/diffctx/_native/__init__.py +0 -7
  30. diffctx-1.17.0/src/diffctx/version.py +0 -1
  31. {diffctx-1.17.0 → diffctx-1.18.0}/Cargo.toml +0 -0
  32. {diffctx-1.17.0 → diffctx-1.18.0}/LICENSE +0 -0
  33. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/analytics.rs +0 -0
  34. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/candidate_files.rs +0 -0
  35. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/change_class.rs +0 -0
  36. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/analytics.rs +0 -0
  37. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/bm25.rs +0 -0
  38. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/budget.rs +0 -0
  39. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/category_weights.rs +0 -0
  40. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/edge_weights.rs +0 -0
  41. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/env_overrides.rs +0 -0
  42. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/extensions.rs +0 -0
  43. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/filtering.rs +0 -0
  44. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/fragmentation.rs +0 -0
  45. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/git.rs +0 -0
  46. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/graph_filtering.rs +0 -0
  47. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/importance.rs +0 -0
  48. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/limits.rs +0 -0
  49. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/mod.rs +0 -0
  50. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/mode.rs +0 -0
  51. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/needs.rs +0 -0
  52. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/parsers.rs +0 -0
  53. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/render.rs +0 -0
  54. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/scoring.rs +0 -0
  55. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/selection.rs +0 -0
  56. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/tokenization.rs +0 -0
  57. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/config/weights.rs +0 -0
  58. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/core.rs +0 -0
  59. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/base.rs +0 -0
  60. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/config_edges/build_system.rs +0 -0
  61. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/config_edges/cicd.rs +0 -0
  62. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/config_edges/docker.rs +0 -0
  63. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/config_edges/generic.rs +0 -0
  64. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/config_edges/helm.rs +0 -0
  65. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/config_edges/kubernetes.rs +0 -0
  66. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/config_edges/mod.rs +0 -0
  67. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/document/mod.rs +0 -0
  68. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/history/cochange.rs +0 -0
  69. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/history/mod.rs +0 -0
  70. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/mod.rs +0 -0
  71. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/ansible.rs +0 -0
  72. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/bazel.rs +0 -0
  73. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/c_family.rs +0 -0
  74. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/cargo_edges.rs +0 -0
  75. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/clojure.rs +0 -0
  76. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/css.rs +0 -0
  77. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/dart.rs +0 -0
  78. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/dbt.rs +0 -0
  79. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/dotnet.rs +0 -0
  80. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/elixir.rs +0 -0
  81. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/erlang.rs +0 -0
  82. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/go.rs +0 -0
  83. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/graphql.rs +0 -0
  84. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/haskell.rs +0 -0
  85. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/javascript.rs +0 -0
  86. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/julia.rs +0 -0
  87. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/jvm.rs +0 -0
  88. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/latex.rs +0 -0
  89. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/lua.rs +0 -0
  90. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/mod.rs +0 -0
  91. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/nim.rs +0 -0
  92. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/nix.rs +0 -0
  93. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/ocaml.rs +0 -0
  94. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/openapi.rs +0 -0
  95. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/perl.rs +0 -0
  96. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/php.rs +0 -0
  97. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/prisma.rs +0 -0
  98. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/protobuf.rs +0 -0
  99. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/python.rs +0 -0
  100. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/r_lang.rs +0 -0
  101. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/ruby.rs +0 -0
  102. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/rust_lang.rs +0 -0
  103. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/shell.rs +0 -0
  104. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/sql.rs +0 -0
  105. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/swift.rs +0 -0
  106. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/tags.rs +0 -0
  107. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/terraform.rs +0 -0
  108. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/semantic/zig.rs +0 -0
  109. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/similarity/lexical.rs +0 -0
  110. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/similarity/mod.rs +0 -0
  111. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/structural/containment.rs +0 -0
  112. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/structural/mod.rs +0 -0
  113. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/structural/sibling.rs +0 -0
  114. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/edges/structural/testing.rs +0 -0
  115. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/excerpt.rs +0 -0
  116. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/facts/mod.rs +0 -0
  117. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/facts/scala.rs +0 -0
  118. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/filtering.rs +0 -0
  119. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/fragmentation.rs +0 -0
  120. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/git.rs +0 -0
  121. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/graph_export.rs +0 -0
  122. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/interval.rs +0 -0
  123. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/languages.rs +0 -0
  124. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/mode.rs +0 -0
  125. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/parsers/config_parser.rs +0 -0
  126. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/parsers/generic.rs +0 -0
  127. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/parsers/markdown.rs +0 -0
  128. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/parsers/mod.rs +0 -0
  129. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/parsers/tree_sitter_strategy.rs +0 -0
  130. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/paths.rs +0 -0
  131. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/peak_rss.rs +0 -0
  132. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/postpass.rs +0 -0
  133. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/ppr.rs +0 -0
  134. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/project_graph.rs +0 -0
  135. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/provenance.rs +0 -0
  136. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/resource.rs +0 -0
  137. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/run_provenance.rs +0 -0
  138. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/sanitize.rs +0 -0
  139. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/scoring.rs +0 -0
  140. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/select.rs +0 -0
  141. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/signatures.rs +0 -0
  142. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/stopwords/code.txt +0 -0
  143. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/stopwords/docs.txt +0 -0
  144. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/stopwords.rs +0 -0
  145. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/test_rng.rs +0 -0
  146. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/testfiles.rs +0 -0
  147. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/token_corpus.rs +0 -0
  148. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/tokenizer.rs +0 -0
  149. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/types.rs +0 -0
  150. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/utility/boltzmann.rs +0 -0
  151. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/utility/importance.rs +0 -0
  152. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/utility/mod.rs +0 -0
  153. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/utility/needs.rs +0 -0
  154. {diffctx-1.17.0 → diffctx-1.18.0}/crates/diffctx-native/src/utility/objective.rs +0 -0
  155. {diffctx-1.17.0 → diffctx-1.18.0}/rust-toolchain.toml +0 -0
  156. {diffctx-1.17.0 → diffctx-1.18.0}/src/diffctx/__init__.py +0 -0
  157. {diffctx-1.17.0 → diffctx-1.18.0}/src/diffctx/__main__.py +0 -0
  158. {diffctx-1.17.0 → diffctx-1.18.0}/src/diffctx/_native/graph_analytics.py +0 -0
  159. {diffctx-1.17.0 → diffctx-1.18.0}/src/diffctx/_native/graph_export.py +0 -0
  160. {diffctx-1.17.0 → diffctx-1.18.0}/src/diffctx/_native/project_graph.py +0 -0
  161. {diffctx-1.17.0 → diffctx-1.18.0}/src/diffctx/clipboard.py +0 -0
  162. {diffctx-1.17.0 → diffctx-1.18.0}/src/diffctx/ignore.py +0 -0
  163. {diffctx-1.17.0 → diffctx-1.18.0}/src/diffctx/logger.py +0 -0
  164. {diffctx-1.17.0 → diffctx-1.18.0}/src/diffctx/mcp/__init__.py +0 -0
  165. {diffctx-1.17.0 → diffctx-1.18.0}/src/diffctx/mcp/__main__.py +0 -0
  166. {diffctx-1.17.0 → diffctx-1.18.0}/src/diffctx/mcp/fetch.py +0 -0
  167. {diffctx-1.17.0 → diffctx-1.18.0}/src/diffctx/mcp/security.py +0 -0
  168. {diffctx-1.17.0 → diffctx-1.18.0}/src/diffctx/py.typed +0 -0
  169. {diffctx-1.17.0 → diffctx-1.18.0}/src/diffctx/tokens.py +0 -0
  170. {diffctx-1.17.0 → diffctx-1.18.0}/src/diffctx/tree.py +0 -0
@@ -7,10 +7,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.18.0] - 2026-09-29
11
+
12
+ ### Added
13
+
14
+ - **`--mode impact`: what a change reaches outside its own diff, under 2k
15
+ tokens.** For each changed symbol, the callers in files the diff does not
16
+ touch, whether a test guards each one, and which symbols more than one
17
+ commit of the range edited; public API and schema changes are listed as
18
+ facts. It walks the graph the other modes already build, with a discovery
19
+ pass that follows references *into* the changed files (the other modes
20
+ only follow what the changed files import), and filters name-only links
21
+ the way a reader would: the caller must name the symbol in code and
22
+ reference its module. An empty impact is an answer, not an error. CLI
23
+ (`-f md` is the text form), MCP `mode="impact"`, the GitHub Action's
24
+ `mode` input, and `schemas/diffctx.impact.v1.json`.
25
+ - **`diffctx hook pretooluse` / `posttooluse`: the Claude Code plugin hands
26
+ the agent a change's impact without asking it to call anything.** Eight
27
+ agent sessions with the tool installed made zero unprompted calls (#289).
28
+ Before a commit, merge, cherry-pick, push or pull request the impact of
29
+ what that command records is injected as context — a plain `git commit`
30
+ reviews the index, `-a` the working tree, a merge the branch since its
31
+ merge base; after a `git diff` or `git status` the agent ran itself, the
32
+ pending change's impact rides on the answer it asked for. Once per change
33
+ (content-keyed with `git patch-id`, so a manual `--mode impact` run
34
+ counts), silent when nothing outside the diff depends on the change, and
35
+ never blocking: a range that runs out of the 30 s deadline is marked
36
+ reviewed rather than retried on every push. `impact_gate` (off by default)
37
+ denies a commit until its impact was shown once. At session start the
38
+ plugin fetches the release binary for the platform, verified against
39
+ `checksums.json`, into its data directory, backs off for six hours after a
40
+ failed download, and puts the one-line recipe into the session's context;
41
+ `impact_hook` turns all of it off (#310).
42
+ - **`/diffctx:commit`**: the impact first, then the commit.
43
+ - **`diffctx_context` is always loaded** under Claude Code's tool search
44
+ (`_meta.anthropic/alwaysLoad`), and its description names `impact` as the
45
+ pre-commit call; both skills default to the server's own range choice.
46
+ - **The PR review comment opens with the impact view.**
47
+ - **Plugin eval `breaking-caller`** measures the number under the goal: with
48
+ a contract change uncommitted and two callers about to break, does the
49
+ agent handle or name them before committing.
50
+
10
51
  ## [1.17.0] - 2026-09-23
11
52
 
12
53
  ### Added
13
54
 
55
+ - **The site lives at <https://diffctx.com>.** It was a 301 to
56
+ `nikolay-e.github.io/diffctx/`, so search engines indexed the github.io
57
+ URL and the product domain had no presence of its own. It is now the
58
+ GitHub Pages custom domain (github.io redirects to it), with a
59
+ `robots.txt` and `sitemap.xml`; package metadata points at it.
14
60
  - **Windows and Intel-Mac are release targets.** Every test matrix runs on
15
61
  Linux, macOS and Windows on x64 and ARM; wheels and binaries ship for
16
62
  `x86_64-apple-darwin` and `aarch64-pc-windows-msvc` as well, and `npm
@@ -1626,7 +1672,8 @@ Earlier releases shipped as `treemapper`; see
1626
1672
  <https://github.com/nikolay-e/diffctx/releases> for the corresponding GitHub
1627
1673
  release notes (`1.0.0` through `1.6.1`).
1628
1674
 
1629
- [Unreleased]: https://github.com/nikolay-e/diffctx/compare/v1.17.0...HEAD
1675
+ [Unreleased]: https://github.com/nikolay-e/diffctx/compare/v1.18.0...HEAD
1676
+ [1.18.0]: https://github.com/nikolay-e/diffctx/compare/v1.17.0...v1.18.0
1630
1677
  [1.17.0]: https://github.com/nikolay-e/diffctx/compare/v1.16.0...v1.17.0
1631
1678
  [1.16.0]: https://github.com/nikolay-e/diffctx/compare/v1.15.0...v1.16.0
1632
1679
  [1.15.0]: https://github.com/nikolay-e/diffctx/compare/v1.14.0...v1.15.0
@@ -194,7 +194,7 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
194
194
 
195
195
  [[package]]
196
196
  name = "diffctx"
197
- version = "1.17.0"
197
+ version = "1.18.0"
198
198
  dependencies = [
199
199
  "aho-corasick",
200
200
  "anyhow",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diffctx
3
- Version: 1.17.0
3
+ Version: 1.18.0
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -44,15 +44,15 @@ Provides-Extra: mcp
44
44
  License-File: LICENSE
45
45
  Summary: Selects the minimum code an LLM needs to review a git diff
46
46
  Keywords: ai,chatgpt,claude,code-analysis,code-context,code-review,code-to-prompt,codebase,context,context-selection,diff-context,directory-tree,export,git-diff,json,llm,llm-context,mcp,model-context-protocol,tree,yaml
47
- Home-Page: https://github.com/nikolay-e/diffctx
47
+ Home-Page: https://diffctx.com/
48
48
  Author-email: Nikolay Eremeev <nikolay.eremeev@outlook.com>
49
49
  License-Expression: Apache-2.0
50
50
  Requires-Python: >=3.10
51
51
  Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
52
52
  Project-URL: Author, https://nikolay-eremeev.com
53
53
  Project-URL: Changelog, https://github.com/nikolay-e/diffctx/blob/main/CHANGELOG.md
54
- Project-URL: Documentation, https://nikolay-e.github.io/diffctx/
55
- Project-URL: Homepage, https://nikolay-e.github.io/diffctx/
54
+ Project-URL: Documentation, https://diffctx.com/
55
+ Project-URL: Homepage, https://diffctx.com/
56
56
  Project-URL: Issues, https://github.com/nikolay-e/diffctx/issues
57
57
  Project-URL: Paper, https://doi.org/10.5281/zenodo.18824579
58
58
  Project-URL: Repository, https://github.com/nikolay-e/diffctx
@@ -248,7 +248,10 @@ as `io.github.nikolay-e/diffctx`. One-line setup (zero-install via
248
248
  [uv](https://docs.astral.sh/uv/)):
249
249
 
250
250
  ```bash
251
- # Claude Code
251
+ # Claude Code plugin: the server plus /diffctx:diffctx and /diffctx:impact
252
+ claude plugin marketplace add nikolay-e/diffctx
253
+ claude plugin install diffctx@diffctx
254
+ # Claude Code, server only
252
255
  claude mcp add diffctx -- uvx --from 'diffctx[mcp]' diffctx-mcp
253
256
  # Codex CLI
254
257
  codex mcp add diffctx -- uvx --from 'diffctx[mcp]' diffctx-mcp
@@ -343,7 +346,7 @@ Apache 2.0
343
346
 
344
347
  ---
345
348
 
346
- - [Documentation site](https://nikolay-e.github.io/diffctx/) — the pipeline
349
+ - [Documentation site](https://diffctx.com/) — the pipeline
347
350
  end to end: diff → fragments → graph → relevance → selection
348
351
  - [Command-line reference](docs/product/cli.md) — every flag with its default
349
352
  and meaning, rendered from `diffctx --help`
@@ -189,7 +189,10 @@ as `io.github.nikolay-e/diffctx`. One-line setup (zero-install via
189
189
  [uv](https://docs.astral.sh/uv/)):
190
190
 
191
191
  ```bash
192
- # Claude Code
192
+ # Claude Code plugin: the server plus /diffctx:diffctx and /diffctx:impact
193
+ claude plugin marketplace add nikolay-e/diffctx
194
+ claude plugin install diffctx@diffctx
195
+ # Claude Code, server only
193
196
  claude mcp add diffctx -- uvx --from 'diffctx[mcp]' diffctx-mcp
194
197
  # Codex CLI
195
198
  codex mcp add diffctx -- uvx --from 'diffctx[mcp]' diffctx-mcp
@@ -284,7 +287,7 @@ Apache 2.0
284
287
 
285
288
  ---
286
289
 
287
- - [Documentation site](https://nikolay-e.github.io/diffctx/) — the pipeline
290
+ - [Documentation site](https://diffctx.com/) — the pipeline
288
291
  end to end: diff → fragments → graph → relevance → selection
289
292
  - [Command-line reference](docs/product/cli.md) — every flag with its default
290
293
  and meaning, rendered from `diffctx --help`
@@ -1,12 +1,12 @@
1
1
  [package]
2
2
  name = "diffctx"
3
- version = "1.17.0"
3
+ version = "1.18.0"
4
4
  edition = "2024"
5
5
  rust-version = "1.85"
6
6
  description = "Selects the minimum code an LLM needs to review a git diff: walks the dependency graph outward from changed lines and stops when extra context stops paying for itself"
7
7
  license = "Apache-2.0"
8
8
  repository = "https://github.com/nikolay-e/diffctx"
9
- homepage = "https://github.com/nikolay-e/diffctx"
9
+ homepage = "https://diffctx.com/"
10
10
  documentation = "https://docs.rs/diffctx"
11
11
  readme = "README.md"
12
12
  keywords = ["git", "diff", "llm", "context", "tree-sitter"]
@@ -189,7 +189,10 @@ as `io.github.nikolay-e/diffctx`. One-line setup (zero-install via
189
189
  [uv](https://docs.astral.sh/uv/)):
190
190
 
191
191
  ```bash
192
- # Claude Code
192
+ # Claude Code plugin: the server plus /diffctx:diffctx and /diffctx:impact
193
+ claude plugin marketplace add nikolay-e/diffctx
194
+ claude plugin install diffctx@diffctx
195
+ # Claude Code, server only
193
196
  claude mcp add diffctx -- uvx --from 'diffctx[mcp]' diffctx-mcp
194
197
  # Codex CLI
195
198
  codex mcp add diffctx -- uvx --from 'diffctx[mcp]' diffctx-mcp
@@ -284,7 +287,7 @@ Apache 2.0
284
287
 
285
288
  ---
286
289
 
287
- - [Documentation site](https://nikolay-e.github.io/diffctx/) — the pipeline
290
+ - [Documentation site](https://diffctx.com/) — the pipeline
288
291
  end to end: diff → fragments → graph → relevance → selection
289
292
  - [Command-line reference](docs/product/cli.md) — every flag with its default
290
293
  and meaning, rendered from `diffctx --help`
@@ -117,6 +117,132 @@ fn expand_by_rare_identifiers(ctx: &DiscoveryContext) -> Vec<PathBuf> {
117
117
  result
118
118
  }
119
119
 
120
+ /// Files that name what the change defines: the callers a change reaches
121
+ /// (#310). Forward discovery follows what the changed file imports; nothing
122
+ /// followed the other way, so a function with a hundred importers surfaced
123
+ /// one of them and the impact view called the rest silence. Only the
124
+ /// impact mode asks for this: widening the universe moves selection, and
125
+ /// the corpus operating point is calibrated without it.
126
+ pub struct ReverseReferenceDiscovery;
127
+
128
+ /// A name held by more candidate files than this is vocabulary, not a
129
+ /// reference, and following it would admit the repository.
130
+ const MAX_REVERSE_FILES_PER_NAME: usize = 64;
131
+ const MAX_REVERSE_FILES: usize = 256;
132
+ const MIN_REVERSE_NAME_LEN: usize = 4;
133
+
134
+ static DEFINITION_RE: OnceLock<regex::Regex> = OnceLock::new();
135
+
136
+ fn definition_names(content: &str) -> Vec<String> {
137
+ let re = DEFINITION_RE.get_or_init(|| {
138
+ regex::Regex::new(
139
+ r"(?m)^\s*(?:export\s+(?:default\s+)?|pub(?:\([^)]*\))?\s+|public\s+|static\s+|async\s+|abstract\s+|final\s+|data\s+|sealed\s+|open\s+)*(?:fn|def|class|struct|enum|interface|trait|type|function|const|let|var|val|object|impl|protocol|module|record)\s+([A-Za-z_][A-Za-z0-9_]*)",
140
+ )
141
+ .expect("definition regex")
142
+ });
143
+ re.captures_iter(content)
144
+ .filter_map(|c| c.get(1).map(|m| m.as_str().to_string()))
145
+ .filter(|n| n.len() >= MIN_REVERSE_NAME_LEN)
146
+ .collect()
147
+ }
148
+
149
+ impl DiscoveryStrategy for ReverseReferenceDiscovery {
150
+ fn name(&self) -> &'static str {
151
+ "reverse_reference"
152
+ }
153
+
154
+ fn discover(&self, ctx: &DiscoveryContext) -> Vec<PathBuf> {
155
+ let changed_set: FxHashSet<&Path> = ctx.changed_files.iter().map(|p| p.as_path()).collect();
156
+ // Per changed file: the module name a reference would import it by,
157
+ // and the names it defines. A file that carries both is a reference
158
+ // however common the name; one that carries the name alone counts
159
+ // only while the name is rare enough to mean this definition.
160
+ let mut per_file: Vec<(Option<String>, FxHashSet<String>)> = Vec::new();
161
+ for path in &ctx.changed_files {
162
+ let stem = path
163
+ .file_stem()
164
+ .and_then(|s| s.to_str())
165
+ .filter(|s| s.len() >= MIN_REVERSE_NAME_LEN)
166
+ .map(|s| s.to_lowercase());
167
+ let names: FxHashSet<String> = ctx
168
+ .read_file(path)
169
+ .map(|c| {
170
+ definition_names(&c)
171
+ .into_iter()
172
+ .map(|n| n.to_lowercase())
173
+ .collect()
174
+ })
175
+ .unwrap_or_default();
176
+ if stem.is_some() || !names.is_empty() {
177
+ per_file.push((stem, names));
178
+ }
179
+ }
180
+ if per_file.is_empty() {
181
+ return Vec::new();
182
+ }
183
+ let mut name_holders: FxHashMap<&str, usize> = FxHashMap::default();
184
+ let mut strong: Vec<&PathBuf> = Vec::new();
185
+ let mut weak: Vec<(&PathBuf, Vec<&str>)> = Vec::new();
186
+ for (path, doc) in &ctx.shared_corpus().docs {
187
+ if changed_set.contains(path.as_path()) {
188
+ continue;
189
+ }
190
+ let mut is_strong = false;
191
+ let mut named: Vec<&str> = Vec::new();
192
+ for (stem, names) in &per_file {
193
+ let has_stem = stem
194
+ .as_deref()
195
+ .is_some_and(|s| doc.term_counts.contains_key(s));
196
+ for name in names {
197
+ if doc.term_counts.contains_key(name) {
198
+ *name_holders.entry(name.as_str()).or_default() += 1;
199
+ named.push(name.as_str());
200
+ if has_stem {
201
+ is_strong = true;
202
+ }
203
+ }
204
+ }
205
+ }
206
+ if is_strong {
207
+ strong.push(path);
208
+ } else if !named.is_empty() {
209
+ weak.push((path, named));
210
+ }
211
+ }
212
+ strong.sort();
213
+ tracing::debug!(
214
+ "reverse references: {} definition sets, {} strong, {} weak, corpus {}",
215
+ per_file.len(),
216
+ strong.len(),
217
+ weak.len(),
218
+ ctx.shared_corpus().docs.len()
219
+ );
220
+ let mut result: Vec<PathBuf> = Vec::new();
221
+ let mut seen: FxHashSet<&PathBuf> = FxHashSet::default();
222
+ for f in strong {
223
+ if result.len() >= MAX_REVERSE_FILES {
224
+ return result;
225
+ }
226
+ if seen.insert(f) {
227
+ result.push(f.clone());
228
+ }
229
+ }
230
+ weak.sort_by(|a, b| a.0.cmp(b.0));
231
+ for (f, named) in weak {
232
+ if result.len() >= MAX_REVERSE_FILES {
233
+ break;
234
+ }
235
+ let rare = named
236
+ .iter()
237
+ .any(|n| name_holders.get(n).copied().unwrap_or(0) <= MAX_REVERSE_FILES_PER_NAME);
238
+ if rare && seen.insert(f) {
239
+ result.push(f.clone());
240
+ }
241
+ }
242
+ result
243
+ }
244
+ }
245
+
120
246
  pub struct TestFileDiscovery;
121
247
 
122
248
  const TEST_PREFIXES: &[&str] = &["test_", "spec_"];
@@ -196,6 +196,7 @@ pub const NON_SEMANTIC_ENV: &[&str] = &[
196
196
  "DIFFCTX_ALLOWED_PATHS",
197
197
  "DIFFCTX_BENCH_TIMEOUT_SEC",
198
198
  "DIFFCTX_BUILD_SHA",
199
+ "DIFFCTX_CACHE_DIR",
199
200
  "DIFFCTX_CONFIG_DIR",
200
201
  "DIFFCTX_DIR_IGNORE",
201
202
  "DIFFCTX_DIR_WHITELIST",
@@ -204,6 +205,7 @@ pub const NON_SEMANTIC_ENV: &[&str] = &[
204
205
  "DIFFCTX_EVAL_STRICT",
205
206
  "DIFFCTX_IGNORE_RELPATH",
206
207
  "DIFFCTX_MCP_LEGACY_TOOLS",
208
+ "DIFFCTX_NO_MARKER",
207
209
  "DIFFCTX_PROBE_MODE",
208
210
  "DIFFCTX_PROBE_TAU",
209
211
  "DIFFCTX_PROVENANCE",
@@ -426,6 +426,39 @@ impl Graph {
426
426
  }
427
427
  }
428
428
 
429
+ /// Invoke `f(source_id, weight)` for each node with an edge INTO `node`,
430
+ /// with the weight of that incoming edge.
431
+ pub fn for_each_reverse_neighbor<F: FnMut(&FragmentId, f64)>(
432
+ &self,
433
+ node: &FragmentId,
434
+ mut f: F,
435
+ ) {
436
+ let rev = match self.rev_csr() {
437
+ Some(c) => c,
438
+ None => return,
439
+ };
440
+ let idx = match rev.node_to_idx.get(node) {
441
+ Some(&i) => i as usize,
442
+ None => return,
443
+ };
444
+ let s = rev.indptr[idx] as usize;
445
+ let e = rev.indptr[idx + 1] as usize;
446
+ for k in s..e {
447
+ let src_idx = rev.indices[k] as usize;
448
+ f(&rev.idx_to_node[src_idx], rev.weights[k]);
449
+ }
450
+ }
451
+
452
+ /// Whether `a -> b` is the stronger direction of the pair. Every builder
453
+ /// writes the reverse of an edge at `weight * reverse_factor` with the
454
+ /// factor below one, so the comparison recovers the direction the builder
455
+ /// meant (caller -> callee, importer -> imported) without a second table.
456
+ pub fn is_forward(&self, a: &FragmentId, b: &FragmentId) -> bool {
457
+ let ab = self.forward_edge_weight(a, b).unwrap_or(0.0);
458
+ let ba = self.forward_edge_weight(b, a).unwrap_or(0.0);
459
+ ab > ba
460
+ }
461
+
429
462
  pub fn ego_graph(
430
463
  &self,
431
464
  seeds: &FxHashSet<FragmentId>,