diffctx 1.18.4__tar.gz → 1.19.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 (175) hide show
  1. {diffctx-1.18.4 → diffctx-1.19.0}/CHANGELOG.md +263 -1
  2. {diffctx-1.18.4 → diffctx-1.19.0}/Cargo.lock +14 -3
  3. {diffctx-1.18.4 → diffctx-1.19.0}/PKG-INFO +2 -2
  4. {diffctx-1.18.4 → diffctx-1.19.0}/README.md +1 -1
  5. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/Cargo.toml +10 -1
  6. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/README.md +1 -1
  7. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/bindings.rs +17 -18
  8. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/candidate_files.rs +15 -7
  9. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/discovery.rs +7 -0
  10. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/base.rs +10 -1
  11. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/config_edges/helm.rs +7 -1
  12. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/mod.rs +7 -3
  13. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/jvm.rs +123 -2
  14. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/structural/testing.rs +27 -0
  15. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/effective_config.rs +1 -0
  16. diffctx-1.19.0/crates/diffctx-native/src/facts/kotlin.rs +1285 -0
  17. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/facts/mod.rs +1 -0
  18. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/git.rs +158 -94
  19. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/graph.rs +6 -0
  20. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/hook.rs +607 -653
  21. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/impact.rs +1246 -492
  22. diffctx-1.19.0/crates/diffctx-native/src/kotlin_bindings.rs +1054 -0
  23. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/lib.rs +1 -0
  24. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/locate.rs +10 -16
  25. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/main.rs +26 -16
  26. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/parsers/mod.rs +41 -0
  27. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/parsers/tree_sitter_strategy.rs +524 -19
  28. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/pipeline.rs +151 -43
  29. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/pybridge.rs +13 -7
  30. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/resource.rs +80 -0
  31. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/source.rs +79 -28
  32. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/token_corpus.rs +3 -1
  33. {diffctx-1.18.4 → diffctx-1.19.0}/pyproject.toml +1 -1
  34. {diffctx-1.18.4 → diffctx-1.19.0}/src/diffctx/_app.py +6 -1
  35. {diffctx-1.18.4 → diffctx-1.19.0}/src/diffctx/_diffctx.pyi +1 -0
  36. {diffctx-1.18.4 → diffctx-1.19.0}/src/diffctx/_native/pipeline.py +2 -0
  37. {diffctx-1.18.4 → diffctx-1.19.0}/src/diffctx/cli.py +2 -1
  38. {diffctx-1.18.4 → diffctx-1.19.0}/src/diffctx/mcp/security.py +11 -0
  39. {diffctx-1.18.4 → diffctx-1.19.0}/src/diffctx/mcp/server.py +17 -7
  40. diffctx-1.19.0/src/diffctx/version.py +1 -0
  41. diffctx-1.18.4/src/diffctx/version.py +0 -1
  42. {diffctx-1.18.4 → diffctx-1.19.0}/Cargo.toml +0 -0
  43. {diffctx-1.18.4 → diffctx-1.19.0}/LICENSE +0 -0
  44. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/analytics.rs +0 -0
  45. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/change_class.rs +0 -0
  46. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/config/analytics.rs +0 -0
  47. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/config/bm25.rs +0 -0
  48. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/config/budget.rs +0 -0
  49. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/config/category_weights.rs +0 -0
  50. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/config/edge_weights.rs +0 -0
  51. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/config/env_overrides.rs +0 -0
  52. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/config/extensions.rs +0 -0
  53. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/config/filtering.rs +0 -0
  54. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/config/fragmentation.rs +0 -0
  55. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/config/git.rs +0 -0
  56. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/config/graph_filtering.rs +0 -0
  57. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/config/importance.rs +0 -0
  58. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/config/limits.rs +0 -0
  59. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/config/mod.rs +0 -0
  60. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/config/mode.rs +0 -0
  61. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/config/needs.rs +0 -0
  62. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/config/parsers.rs +0 -0
  63. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/config/render.rs +0 -0
  64. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/config/scoring.rs +0 -0
  65. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/config/selection.rs +0 -0
  66. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/config/tokenization.rs +0 -0
  67. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/config/weights.rs +0 -0
  68. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/container.rs +0 -0
  69. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/core.rs +0 -0
  70. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/config_edges/build_system.rs +0 -0
  71. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/config_edges/cicd.rs +0 -0
  72. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/config_edges/docker.rs +0 -0
  73. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/config_edges/generic.rs +0 -0
  74. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/config_edges/kubernetes.rs +0 -0
  75. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/config_edges/mod.rs +0 -0
  76. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/document/mod.rs +0 -0
  77. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/history/cochange.rs +0 -0
  78. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/history/mod.rs +0 -0
  79. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/ansible.rs +0 -0
  80. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/bazel.rs +0 -0
  81. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/c_family.rs +0 -0
  82. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/cargo_edges.rs +0 -0
  83. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/clojure.rs +0 -0
  84. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/css.rs +0 -0
  85. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/dart.rs +0 -0
  86. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/dbt.rs +0 -0
  87. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/dotnet.rs +0 -0
  88. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/elixir.rs +0 -0
  89. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/erlang.rs +0 -0
  90. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/go.rs +0 -0
  91. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/graphql.rs +0 -0
  92. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/haskell.rs +0 -0
  93. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/javascript.rs +0 -0
  94. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/julia.rs +0 -0
  95. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/latex.rs +0 -0
  96. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/lua.rs +0 -0
  97. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/mod.rs +0 -0
  98. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/nim.rs +0 -0
  99. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/nix.rs +0 -0
  100. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/ocaml.rs +0 -0
  101. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/openapi.rs +0 -0
  102. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/perl.rs +0 -0
  103. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/php.rs +0 -0
  104. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/prisma.rs +0 -0
  105. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/protobuf.rs +0 -0
  106. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/python.rs +0 -0
  107. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/r_lang.rs +0 -0
  108. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/ruby.rs +0 -0
  109. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/rust_lang.rs +0 -0
  110. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/shell.rs +0 -0
  111. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/sql.rs +0 -0
  112. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/swift.rs +0 -0
  113. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/tags.rs +0 -0
  114. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/terraform.rs +0 -0
  115. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/semantic/zig.rs +0 -0
  116. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/similarity/lexical.rs +0 -0
  117. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/similarity/mod.rs +0 -0
  118. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/structural/containment.rs +0 -0
  119. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/structural/mod.rs +0 -0
  120. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/edges/structural/sibling.rs +0 -0
  121. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/excerpt.rs +0 -0
  122. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/facts/scala.rs +0 -0
  123. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/filtering.rs +0 -0
  124. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/fragmentation.rs +0 -0
  125. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/graph_export.rs +0 -0
  126. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/in_memory_harness.rs +0 -0
  127. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/interval.rs +0 -0
  128. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/languages.rs +0 -0
  129. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/mode.rs +0 -0
  130. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/parsers/config_parser.rs +0 -0
  131. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/parsers/generic.rs +0 -0
  132. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/parsers/markdown.rs +0 -0
  133. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/paths.rs +0 -0
  134. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/peak_rss.rs +0 -0
  135. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/postpass.rs +0 -0
  136. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/ppr.rs +0 -0
  137. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/project_graph.rs +0 -0
  138. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/provenance.rs +0 -0
  139. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/render.rs +0 -0
  140. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/run_provenance.rs +0 -0
  141. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/sanitize.rs +0 -0
  142. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/scoring.rs +0 -0
  143. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/select.rs +0 -0
  144. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/signatures.rs +0 -0
  145. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/stopwords/code.txt +0 -0
  146. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/stopwords/docs.txt +0 -0
  147. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/stopwords.rs +0 -0
  148. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/test_rng.rs +0 -0
  149. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/testfiles.rs +0 -0
  150. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/tokenizer.rs +0 -0
  151. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/types.rs +0 -0
  152. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/utility/boltzmann.rs +0 -0
  153. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/utility/importance.rs +0 -0
  154. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/utility/mod.rs +0 -0
  155. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/utility/needs.rs +0 -0
  156. {diffctx-1.18.4 → diffctx-1.19.0}/crates/diffctx-native/src/utility/objective.rs +0 -0
  157. {diffctx-1.18.4 → diffctx-1.19.0}/rust-toolchain.toml +0 -0
  158. {diffctx-1.18.4 → diffctx-1.19.0}/src/diffctx/__init__.py +0 -0
  159. {diffctx-1.18.4 → diffctx-1.19.0}/src/diffctx/__main__.py +0 -0
  160. {diffctx-1.18.4 → diffctx-1.19.0}/src/diffctx/_native/__init__.py +0 -0
  161. {diffctx-1.18.4 → diffctx-1.19.0}/src/diffctx/_native/graph_analytics.py +0 -0
  162. {diffctx-1.18.4 → diffctx-1.19.0}/src/diffctx/_native/graph_export.py +0 -0
  163. {diffctx-1.18.4 → diffctx-1.19.0}/src/diffctx/_native/project_graph.py +0 -0
  164. {diffctx-1.18.4 → diffctx-1.19.0}/src/diffctx/clipboard.py +0 -0
  165. {diffctx-1.18.4 → diffctx-1.19.0}/src/diffctx/ignore.py +0 -0
  166. {diffctx-1.18.4 → diffctx-1.19.0}/src/diffctx/logger.py +0 -0
  167. {diffctx-1.18.4 → diffctx-1.19.0}/src/diffctx/mcp/__init__.py +0 -0
  168. {diffctx-1.18.4 → diffctx-1.19.0}/src/diffctx/mcp/__main__.py +0 -0
  169. {diffctx-1.18.4 → diffctx-1.19.0}/src/diffctx/mcp/fetch.py +0 -0
  170. {diffctx-1.18.4 → diffctx-1.19.0}/src/diffctx/mcp/launch.py +0 -0
  171. {diffctx-1.18.4 → diffctx-1.19.0}/src/diffctx/mcp/legacy.py +0 -0
  172. {diffctx-1.18.4 → diffctx-1.19.0}/src/diffctx/py.typed +0 -0
  173. {diffctx-1.18.4 → diffctx-1.19.0}/src/diffctx/tokens.py +0 -0
  174. {diffctx-1.18.4 → diffctx-1.19.0}/src/diffctx/tree.py +0 -0
  175. {diffctx-1.18.4 → diffctx-1.19.0}/src/diffctx/writer.py +0 -0
@@ -7,6 +7,267 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.19.0] - 2026-10-14
11
+
12
+ ### Added
13
+
14
+ - Kotlin (`.kt`, `.kts`) parses with tree-sitter, through
15
+ `brokk-tree-sitter-kotlin` 0.4.6 — the fwcd grammar as its maintained fork
16
+ publishes it, since fwcd's own crate still pins tree-sitter < 0.23. On
17
+ 1,036 real files it lost no declaration and broke on none;
18
+ `tree-sitter-kotlin-sg` 0.4.1 silently dropped seven classes (a bodyless
19
+ annotated class swallowed the next one) and `tree-sitter-kotlin-ng` broke
20
+ whole classes on a `get(...)` call. Classes, interfaces, objects,
21
+ companions (`Companion` when unnamed), functions, extension functions,
22
+ secondary constructors (named after their class), properties (a property
23
+ spans its getter and setter; a local `val` is not a member) and type
24
+ aliases become fragments (#303). This reverses the 1.7.0 removal, which
25
+ dropped `.kt` read through the Java grammar, not a Kotlin one.
26
+ - Kotlin has a binding model, read off shared syntax facts (one parse per
27
+ blob, shared by the fragmenter, `--symbol` and impact): a use binds through
28
+ its import or alias, the declared package (not the directory), the
29
+ receiver's declared type — a local, a parameter, a property of the class
30
+ or of a source-visible base class, a constructor or a function with a
31
+ written return type, called plainly or on a typed name
32
+ (`val x = repo.find(id)`) — an object or companion qualifier, or the
33
+ implicit `this`, which in an `inner class` reaches the outer instance too.
34
+ What the syntax cannot settle stays a candidate with its reason
35
+ (`receiver_unresolved`, `overloaded`, `wildcard_import`, `dispatch`,
36
+ `project_dependency_unknown`); `internal` stops at the project, which its
37
+ test source set shares. Callable references (`Obj::f`) are references,
38
+ implementations of a changed abstract member are their own relation, a
39
+ removed declaration is found by comparing the file's declarations at the
40
+ base and the head, and a public contract changes when the header does —
41
+ parameter defaults and all. In the context graph a Kotlin member use links
42
+ only to the definition it resolves to: a candidate suggests, it does not
43
+ link.
44
+
45
+ ### Security
46
+
47
+ - Impact and `--symbol` no longer name a withheld file that refers to the
48
+ change. 1.18.4's fix covered the changed side only: a file under
49
+ `.diffctx/ignore` or with a secret name that called a removed definition or
50
+ named a deleted path still appeared as a caller or a stale reference. The
51
+ withhold policy now applies inside the `git grep` readers, so every surface
52
+ gets only admissible files, and the files a read withheld count toward
53
+ `withheld_files`. The header says "N file(s) withheld by ignore rules"
54
+ rather than "N more", since they need not be changed files.
55
+
56
+ ### Fixed
57
+
58
+ - **`A..B` is what `B` changed since it forked from `A`**, in every mode and
59
+ at every entry point (CLI, MCP, the hook, the Action): the range resolves
60
+ once to the merge base and `B`'s commit. Read as two trees, a base that
61
+ moved on after the fork put its own later commits into the change as
62
+ reverts — a PR's target branch in the Action (#485), a remote-tracking
63
+ ref a `pull` on the same line had just moved in the hook (#462, which now
64
+ also measures a push from where the push itself found the ref). The
65
+ Action deepens a shallow checkout until the fork point is known, and says
66
+ so when it is not. Histories with no common commit keep git's two-tree
67
+ reading.
68
+ - A range is answered from its snapshot alone. The Helm chart root was
69
+ probed on the disk, so a working tree without the range's `Chart.yaml`
70
+ dropped the chart's templates from pack and locate (#408). A snapshot
71
+ read that failed — out of time, cancelled, a broken stream — read as an
72
+ absent file and now names its limit; a snapshot listing that failed no
73
+ longer hands discovery or the token cache the working tree's files in its
74
+ place (#443).
75
+ - The changed-file count of uncommitted work is what git would commit.
76
+ Untracked files are analysed as before but named apart (`untracked_files`,
77
+ "2 changed file(s), 1 untracked"), and an untracked binary — a local
78
+ database, a build artefact — is no source change at all; two `.db` files
79
+ and a notes file read as "5 changed file(s)" for a 2-file edit (#418,
80
+ #423). A search for a name defined only in an untracked file is answered
81
+ with its callers, as the inspection that follows already was (#448).
82
+ - `--full A..B` listed a file the range deleted, or renamed away, as
83
+ changed and showed its old body read from `A`. One rule now names what a
84
+ range's head no longer holds, for every mode; staged pack, `--full` and
85
+ locate are pinned to read the index alone, renames and mode changes
86
+ included (#409).
87
+ - A one-second impact run ends near its second: the loops over input-sized
88
+ lists (stale-reference lines, unanalysed and namer files, test edges)
89
+ iterate through the deadline instead of polling it by hand, and the
90
+ stale-reference loop, which had no poll, ran such a run to 4.5 s (now
91
+ 1.04 s, #510). A deadline that loop hit is named in `limits`; it was read
92
+ before the loop ran, so a run that had dropped its stale references
93
+ looked complete. `DIFFCTX_TEST_DEADLINE_AFTER_POLLS=n` stops a run at its
94
+ `n`th poll, so tests stop it at every point without racing the clock,
95
+ and a corpus case its deadline stopped says so instead of failing on a
96
+ score (#390).
97
+ - A call inside an f-string replacement field (`f"{fence(text)}"`) or a
98
+ template-literal substitution (`` `${formatBytes(n)}` ``) is a caller. The
99
+ hand lexer that impact and the binding resolver read code through
100
+ blanked every quoted byte, interpolations included, so both read
101
+ "0 callers" (#460, #475). Comments and string literals now come off the
102
+ file's grammar, interpolations kept, in every language with one; the
103
+ lexer and its special cases (`//` as floor division, a Rust lifetime,
104
+ `*` block-comment lines) are gone (#506).
105
+ - A removed definition is one the file declared at the base and does not
106
+ declare at the head, both read off the grammar. Read off removed lines by
107
+ regex, a Rust tail expression `Err("zero")` became a removed member named
108
+ `Err` with every `Err(` in the crate as its callers, a bare `int(text)` in
109
+ a Python method a removed `int`, and a private function next to an
110
+ `impl` a removed API (#515, #458). A member is as public as the class or
111
+ interface it sits in, by nesting, not by the nearest class above it.
112
+ - A caller of a removed definition is code that can still reach it:
113
+ through an import the binding model resolves to the file it left (a
114
+ removed member through its imported class), else from the same runtime.
115
+ By name alone, `subprocess.run`, another file's own `run`, a shell `die`
116
+ and a TypeScript `test.describe` were callers of a deleted Python script's
117
+ functions, and the import line was a second caller (#470, #471, #458,
118
+ #450). Files the range deleted resolve as files an import still names.
119
+ - A dependency bump is a change: impact names the lock files a range
120
+ changes (`lockfile_changes`) and never answers "No changes to analyse" for
121
+ them (#495).
122
+ - The hook's search pre-filter in the plugin script read the session's
123
+ cwd, not the Grep's own `path`, and ignored untracked files, so a search
124
+ over a pending change elsewhere never reached the binary. `bash -lc
125
+ "git commit …"` and `$(git -C r commit)` are named as unread rather than
126
+ logged as no trigger; reminders and gate denials are logged as such. A
127
+ cache directory the hook cannot write falls back to the temporary
128
+ directory instead of silently rerunning every preview (#407).
129
+ - Before a commit or push the hook says what it did when it has no impact
130
+ to show: one line when the change reaches nothing outside the diff, and
131
+ one when it could not read the command (a git inside a subshell, a `cd`
132
+ it cannot resolve), with the command to run instead. Silence then means
133
+ the hook did not run, not a clean review; the hook log keeps tens of
134
+ thousands of runs so an audit reads a whole session (#464).
135
+ - The MCP tool timed out at 300 s on a monorepo range the CLI answered in
136
+ four seconds (#457), for two reasons. It widened a subdirectory
137
+ `repo_path` to the whole repository; it now analyses the directory asked
138
+ about, as the CLI does with the same path. And impact grepped every
139
+ removed name as a fixed string in every file, so a deleted `call` matched
140
+ each `callback` in the repository's data and the run never stopped
141
+ (#413): the grep now reads whole words in code files only, at most 20
142
+ lines a file, and a name on more than 500 lines is answered "too common to
143
+ trace by name" instead of listed.
144
+ - Impact is fitted to its cap on the text the reader receives: markdown was
145
+ shed against the size of its JSON, so a 148-token answer cited a
146
+ 2000-token cap and lost its contract block; an omitted count printed
147
+ under the previous list's heading (#451). `--budget` (CLI) and
148
+ `budget_tokens` (MCP) set the cap, which stayed 2000 whatever was asked,
149
+ and a shed answer says which flag lists the rest (#466).
150
+ - The MCP tool's refusal of `symbol` together with `diff_ref` or
151
+ `fragment_ids` names the call that answers: drop them and ask in
152
+ `mode="impact"`, which looks the name up in the working tree (#499).
153
+ - Impact no longer reads "not examined" as "nothing there". A failed `git
154
+ grep` or file listing marks the run partial (`read_failed`) instead of
155
+ dropping the removed definition's callers and the stale references with
156
+ exit 0; a failed snapshot listing no longer falls back to the working
157
+ tree. A changed code file over the size cap or unreadable is named in
158
+ `unanalysed_files` (it answered `"empty": true` while pack said
159
+ `file_too_large`). Limits follow pack's rule, so the contribution and
160
+ total-byte caps, which drop callers, are named; the per-node edge cap
161
+ every repository trips still is not. A caller whose edge that cap cut
162
+ faces the same tests an edge would have and stays a caller (`--symbol`
163
+ answered "0 callers" for a function its route handler calls). A
164
+ definition changed only in comments, formatting or position is listed
165
+ in `layout_only` and the answer says its callers were not traced, where
166
+ it said "No resolved static callers"; the hook's search answer says the
167
+ same instead of claiming none. Under a limit, "nothing calls them" reads
168
+ "no caller found in this partial run". Unparsed files that name a
169
+ camelCase definition are disclosed: identifiers are stored lowercased,
170
+ and the lookup compared them as written.
171
+ - Impact no longer calls a definition whose only callers are inside the diff
172
+ one that "nothing calls": a helper extracted from a function the same
173
+ commit changes read as dead code, on 62–75% of that list across three
174
+ ranges of this repository. Such definitions are listed under "Changed,
175
+ called only from inside the diff, no static test link" and in the new
176
+ `untested_called_in_diff` field; an in-diff source counts when it passes
177
+ the tests any caller passes. 1.18.3 never printed the clause, so 1.18.4
178
+ added the false statement (#441).
179
+ - A control statement removed from a method body is no removed member:
180
+ `match (a, b) {` in a Rust `impl` read as a removed API `match`, and
181
+ every file spelling the keyword became its caller. `when`, `try`,
182
+ `synchronized`, `using`, `lock`, `fixed`, `foreach` and `elseif` are
183
+ filtered with it (#492).
184
+ - Impact looks a fragment id up as the fragment, not as the signature
185
+ variant that shares its id when the signature spans the whole fragment: a
186
+ Kotlin class whose header ends in `) {` answered `--symbol` with "0
187
+ definition(s)".
188
+ - The JVM builder links an owner-qualified short member (`UI.DP`) to its
189
+ declaration. Names under three characters never linked by name, so a
190
+ constant read through its object was missed.
191
+ - A JVM framework annotation registers what it annotates and nothing else:
192
+ a class-level `@Component` or `@Service` no longer marks every method a
193
+ registered entry point (which hid its missing test link); method-level
194
+ entry points (`@GetMapping`, `@Scheduled`, `@Bean`, …) count only when the
195
+ file imports them from a framework package.
196
+ - Impact names a caller by its whole definition, not by the signature
197
+ variant the graph linked: a use in the body — a constructor call after a
198
+ long parameter list — was dropped from the caller's sites.
199
+ - A name shared across runtimes no runtime crosses is neither a caller nor
200
+ a test link: a Kotlin test no longer reads as guarding a Swift function of
201
+ the same name, and a Kotlin or Swift spelling of a Python or TypeScript
202
+ name is not its caller. JVM languages still call each other, JavaScript
203
+ its HTML and components, Swift and Objective-C their C (#487, #458).
204
+ - A changed symbol whose name is spelled in a file in view that did not
205
+ parse cleanly lists that file (`missed_in`, "callers may be missing")
206
+ instead of reading as one nothing calls (#454).
207
+ - JVM tests pair with their subject the way JVM projects name them:
208
+ `FooTest`, `FooTests`, `FooSpec` and `FooIT` beside `Foo` are discovered
209
+ and linked in both directions, and `src/<set>/<java|kotlin|scala|groovy>/`
210
+ is a source root, so a test importing `com.acme.Foo` reaches
211
+ `src/main/kotlin/com/acme/Foo.kt`. A related test file is not a guard: a
212
+ method its tests never call keeps "no static test link".
213
+ - `--symbol Owner.name` reads Kotlin's owners and receivers from the syntax:
214
+ `String.pad` names the extension, `Worker.Companion.create` the companion
215
+ member; and the declaration search that keeps a capped query's definition
216
+ file knows `fun` and `typealias`.
217
+ - A Kotlin type the text declares at the start of a line but the parse did
218
+ not read — a grammar's silent misreading, with no error node — marks the
219
+ file analysed only in part (0 of yay-tsa's 1,036 files with the shipped
220
+ grammar; local classes count as read).
221
+ - The parse outcome is recorded when the fragmenter parses a file, not
222
+ inferred from the fragments that came out: a file that parsed with errors
223
+ is reported as analysed only in part even when a definition survived, and
224
+ a timed-out parse is named as such.
225
+
226
+ - Impact names the changed code files it could not read for definitions —
227
+ no grammar for the language in this build, or a parse that broke — in its
228
+ header and in `unanalysed_files`, and never calls such a change empty: a
229
+ Kotlin-only commit read "0 callers outside the diff" and the commit hook
230
+ stayed silent (#454). A script that parses cleanly and defines nothing is
231
+ not listed.
232
+ - `--symbol` no longer answers only "no definition found" when the files
233
+ naming the symbol were read as text: it names them and the reason, so an
234
+ unparsed definition is not mistaken for a missing one (#454).
235
+ - Locate's `unparsed_files` uses the same test: code with no grammar in this
236
+ build (Kotlin, F#, Fortran) or a broken parse is listed and lowers
237
+ confidence; a script that parses cleanly and defines nothing (a
238
+ `build.gradle.kts`, a Python script of top-level statements) no longer is.
239
+ Data files (`.sql`, `.toml`, `.txt`) stay unlisted (#330).
240
+
241
+ ### Changed
242
+
243
+ - **Plugin hook: a commit, merge, cherry-pick, revert or push is reviewed
244
+ from what git recorded, once the line ran** — read off the reflog (`HEAD`'s
245
+ last entry, a remote-tracking ref's "update by push"), never predicted
246
+ from the command text. The prediction took fifteen parser commits in
247
+ nine days and still reviewed another session's staged file for
248
+ `git commit --pathspec-from-file` instead of the paths the commit took
249
+ (#508). A loop, `xargs`, `git apply --cached`, `git --git-dir … commit` or
250
+ an escaped quote now stage what they stage, and the review says what the
251
+ commit took (#405, #436). A commit of a change already shown gets a
252
+ one-line pointer instead of silence (#501); a push of several refs
253
+ reviews the first and counts the rest; a `--squash` merge is reviewed by
254
+ the commit that records it. `L=/abs; cd $L && git commit` follows the
255
+ variable the line sets (#422). The opt-in strict gate still reviews
256
+ before git runs, and only what needs no prediction: the index a plain
257
+ `git commit` records; a commit line that stages for itself is denied with
258
+ the instruction to stage as its own command. Without the gate the impact
259
+ now arrives with the commit's result, which is when Claude Code delivered
260
+ the old preview too (#429).
261
+ - Claude plugin: the MCP server and the hooks launch `uvx diffctx[mcp]==X`
262
+ with no other option, and `plugin/uv.lock` pins the release's whole
263
+ dependency tree with PyPI hashes, the shape the plugin directory reads as a
264
+ locked launch ("Launcher lock invalid" before). `constraints.txt` is gone:
265
+ uv resolves diffctx's dependencies within their declared ranges, as every
266
+ other uvx install already did.
267
+ - CI and the pre-commit hook run the `impact`, `hook`, `bindings`, `staged`,
268
+ `git_faults`, `in_memory` and `context_schema` test targets, which no gate
269
+ ran before.
270
+
10
271
  ## [1.18.4] - 2026-10-07
11
272
 
12
273
  ### Security
@@ -2038,7 +2299,8 @@ Earlier releases shipped as `treemapper`; see
2038
2299
  <https://github.com/nikolay-e/diffctx/releases> for the corresponding GitHub
2039
2300
  release notes (`1.0.0` through `1.6.1`).
2040
2301
 
2041
- [Unreleased]: https://github.com/nikolay-e/diffctx/compare/v1.18.4...HEAD
2302
+ [Unreleased]: https://github.com/nikolay-e/diffctx/compare/v1.19.0...HEAD
2303
+ [1.19.0]: https://github.com/nikolay-e/diffctx/compare/v1.18.4...v1.19.0
2042
2304
  [1.18.4]: https://github.com/nikolay-e/diffctx/compare/v1.18.3...v1.18.4
2043
2305
  [1.18.3]: https://github.com/nikolay-e/diffctx/compare/v1.18.2...v1.18.3
2044
2306
  [1.18.2]: https://github.com/nikolay-e/diffctx/compare/v1.18.1...v1.18.2
@@ -94,6 +94,16 @@ version = "2.11.1"
94
94
  source = "registry+https://github.com/rust-lang/crates.io-index"
95
95
  checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
96
96
 
97
+ [[package]]
98
+ name = "brokk-tree-sitter-kotlin"
99
+ version = "0.4.6"
100
+ source = "registry+https://github.com/rust-lang/crates.io-index"
101
+ checksum = "11536d4605247354ad8992b7ae605e85508c07d64ed0710ff8494558660e40d0"
102
+ dependencies = [
103
+ "cc",
104
+ "tree-sitter-language",
105
+ ]
106
+
97
107
  [[package]]
98
108
  name = "bstr"
99
109
  version = "1.13.1"
@@ -107,9 +117,9 @@ dependencies = [
107
117
 
108
118
  [[package]]
109
119
  name = "cc"
110
- version = "1.2.60"
120
+ version = "1.2.56"
111
121
  source = "registry+https://github.com/rust-lang/crates.io-index"
112
- checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
122
+ checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
113
123
  dependencies = [
114
124
  "find-msvc-tools",
115
125
  "shlex",
@@ -194,10 +204,11 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
194
204
 
195
205
  [[package]]
196
206
  name = "diffctx"
197
- version = "1.18.4"
207
+ version = "1.19.0"
198
208
  dependencies = [
199
209
  "aho-corasick",
200
210
  "anyhow",
211
+ "brokk-tree-sitter-kotlin",
201
212
  "clap",
202
213
  "dunce",
203
214
  "glob",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diffctx
3
- Version: 1.18.4
3
+ Version: 1.19.0
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -77,7 +77,7 @@ zero.
77
77
  ## Install
78
78
 
79
79
  ```bash
80
- # Claude Code: the MCP server, /diffctx:impact, and hooks that run it before commit and push
80
+ # Claude Code: the MCP server, /diffctx:impact, and hooks that run it on each commit and push
81
81
  claude plugin marketplace add nikolay-e/diffctx
82
82
  claude plugin install diffctx@diffctx
83
83
 
@@ -18,7 +18,7 @@ zero.
18
18
  ## Install
19
19
 
20
20
  ```bash
21
- # Claude Code: the MCP server, /diffctx:impact, and hooks that run it before commit and push
21
+ # Claude Code: the MCP server, /diffctx:impact, and hooks that run it on each commit and push
22
22
  claude plugin marketplace add nikolay-e/diffctx
23
23
  claude plugin install diffctx@diffctx
24
24
 
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "diffctx"
3
- version = "1.18.4"
3
+ version = "1.19.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"
@@ -66,6 +66,7 @@ lang-extra = [
66
66
  "tree-sitter-svelte-ng",
67
67
  "tree-sitter-scala",
68
68
  "tree-sitter-swift",
69
+ "brokk-tree-sitter-kotlin",
69
70
  ]
70
71
 
71
72
  [dependencies]
@@ -86,6 +87,14 @@ tree-sitter-ruby = { version = "0.23", optional = true }
86
87
  tree-sitter-php = { version = "0.24", optional = true }
87
88
  tree-sitter-scala = { version = "0.26", optional = true }
88
89
  tree-sitter-swift = { version = "0.7", optional = true }
90
+ # The fwcd grammar (fwcd's own crate stops at 0.3.8, pinned to tree-sitter
91
+ # < 0.23, fwcd#242), as BrokkAi publishes its maintained fork with symbol-
92
+ # prefixed C exports. Exact pin: a fork's next release is reviewed, not taken.
93
+ # On yay-tsa's 1,036 files it lost no type declaration and broke on none;
94
+ # tree-sitter-kotlin-sg 0.4.1 silently dropped 7 classes (a bodyless
95
+ # annotated class swallowed the next one) and broke 5 Compose files, and
96
+ # tree-sitter-kotlin-ng dropped 8 and broke whole classes on `get(...)`.
97
+ brokk-tree-sitter-kotlin = { version = "=0.4.6", optional = true }
89
98
  tree-sitter-html = { version = "0.23", optional = true }
90
99
  tree-sitter-bash = { version = "0.25", optional = true }
91
100
  tree-sitter-css = { version = "0.25", optional = true }
@@ -18,7 +18,7 @@ zero.
18
18
  ## Install
19
19
 
20
20
  ```bash
21
- # Claude Code: the MCP server, /diffctx:impact, and hooks that run it before commit and push
21
+ # Claude Code: the MCP server, /diffctx:impact, and hooks that run it on each commit and push
22
22
  claude plugin marketplace add nikolay-e/diffctx
23
23
  claude plugin install diffctx@diffctx
24
24
 
@@ -747,6 +747,8 @@ impl Evidence {
747
747
  pub enum Relation {
748
748
  Call,
749
749
  Reference,
750
+ /// A class that implements the changed abstract member.
751
+ Implementation,
750
752
  }
751
753
 
752
754
  /// The changed definition: `name` in `path`, a method of `class` when it
@@ -1056,9 +1058,8 @@ impl Resolver<'_> {
1056
1058
  return direct;
1057
1059
  }
1058
1060
  }
1059
- let python = lang_of(site.path) == Some(Lang::Python);
1060
- if let Some(ctor) =
1061
- constructed_as(site.content, head, python).or_else(|| annotated_as(site.content, head))
1061
+ if let Some(ctor) = constructed_as(site.path, site.content, head)
1062
+ .or_else(|| annotated_as(site.content, head))
1062
1063
  {
1063
1064
  return self.constructor_is(site, &ctor, def, class);
1064
1065
  }
@@ -1097,12 +1098,10 @@ impl Resolver<'_> {
1097
1098
  pub fn classify(&self, site: &Site, def: &Definition) -> Option<Finding> {
1098
1099
  let words = self.names_for(site, def);
1099
1100
  let params = params_of(site.content);
1100
- let python = lang_of(site.path) == Some(Lang::Python);
1101
- let mut triple = None;
1102
1101
  let mut best: Option<Finding> = None;
1103
- for (i, line) in site.content.lines().enumerate() {
1104
- let code = crate::impact::mask_code(line, python, &mut triple);
1105
- if is_import_line(&code) {
1102
+ let masked = crate::impact::code_text(site.path, site.content);
1103
+ for (i, code) in masked.lines().enumerate() {
1104
+ if is_import_line(code) {
1106
1105
  continue;
1107
1106
  }
1108
1107
  let line_no = site.start_line + i as u32;
@@ -1198,7 +1197,7 @@ impl Resolver<'_> {
1198
1197
  }
1199
1198
  }
1200
1199
 
1201
- fn is_import_line(code: &str) -> bool {
1200
+ pub(crate) fn is_import_line(code: &str) -> bool {
1202
1201
  let head = code.trim_start();
1203
1202
  head.starts_with("import ")
1204
1203
  || head.starts_with("from ")
@@ -1260,21 +1259,21 @@ fn bases_of(header: &str) -> Vec<String> {
1260
1259
  /// constructor `x` holds. A binding is read within one line: across a
1261
1260
  /// newline, `as srv:` and the next line's `result = srv.call_tool(...)`
1262
1261
  /// read as an annotated assignment from `srv.call_tool` (#399).
1263
- fn constructed_as(content: &str, var: &str, python: bool) -> Option<String> {
1262
+ fn constructed_as(path: &str, content: &str, var: &str) -> Option<String> {
1264
1263
  let re = Regex::new(&format!(
1265
1264
  r"(?:^|[^\w$.])({})\s*(?::\s*[\w$.\[\]<>]+\s*)?=\s*(?:new\s+|await\s+)?([\w$.]+)\s*\(",
1266
1265
  regex::escape(var)
1267
1266
  ))
1268
1267
  .ok()?;
1269
- let mut triple = None;
1270
- content.lines().find_map(|line| {
1271
- let code = crate::impact::mask_code(line, python, &mut triple);
1272
- entered_as(&code, var).or_else(|| {
1273
- re.captures_iter(&code)
1274
- .find(|c| c.get(1).is_some_and(|v| !after_as(&code, v.start())))
1275
- .map(|c| c[2].to_string())
1268
+ crate::impact::code_text(path, content)
1269
+ .lines()
1270
+ .find_map(|code| {
1271
+ entered_as(code, var).or_else(|| {
1272
+ re.captures_iter(code)
1273
+ .find(|c| c.get(1).is_some_and(|v| !after_as(code, v.start())))
1274
+ .map(|c| c[2].to_string())
1275
+ })
1276
1276
  })
1277
- })
1278
1277
  }
1279
1278
 
1280
1279
  /// Whether the name at `at` is the target of an `as` (`with … as x`,
@@ -109,13 +109,21 @@ pub fn collect_candidate_files(
109
109
  ctx: &crate::resource::RunContext,
110
110
  ) -> Vec<PathBuf> {
111
111
  let source = crate::source::current();
112
- if let Some(blobs) = source.list_blobs(scope) {
113
- let admitted = blobs
114
- .into_iter()
115
- .filter(|b| is_snapshot_candidate(b, root_dir, included_set, &source, ctx))
116
- .map(|b| b.path)
117
- .collect();
118
- return filter_ignored_and_secret(root_dir, admitted);
112
+ match source.list_blobs(scope) {
113
+ Some(Ok(blobs)) => {
114
+ let admitted = blobs
115
+ .into_iter()
116
+ .filter(|b| is_snapshot_candidate(b, root_dir, included_set, &source, ctx))
117
+ .map(|b| b.path)
118
+ .collect();
119
+ return filter_ignored_and_secret(root_dir, admitted);
120
+ }
121
+ Some(Err(e)) => {
122
+ tracing::warn!("discovery: the snapshot's listing failed ({e})");
123
+ ctx.note(crate::resource::reason_for(&e));
124
+ return Vec::new();
125
+ }
126
+ None => {}
119
127
  }
120
128
  match tracked_candidates(root_dir, included_set, scope, ctx) {
121
129
  Some(files) => filter_ignored_and_secret(root_dir, files),
@@ -316,6 +316,13 @@ impl DiscoveryStrategy for TestFileDiscovery {
316
316
  for suffix in TEST_SUFFIXES {
317
317
  test_stems.insert(format!("{stem}{suffix}"));
318
318
  }
319
+ let ext = crate::edges::base::file_ext(f);
320
+ if crate::edges::structural::testing::JVM_TEST_SUFFIX_EXTENSIONS.contains(&ext.as_str())
321
+ {
322
+ for suffix in crate::edges::structural::testing::JVM_TEST_SUFFIXES {
323
+ test_stems.insert(format!("{stem}{suffix}"));
324
+ }
325
+ }
319
326
  }
320
327
 
321
328
  let mut discovered: Vec<PathBuf> = Vec::new();
@@ -47,10 +47,19 @@ pub trait EdgeBuilder: Send + Sync {
47
47
  static INDEX_FILE_STEMS: Lazy<FxHashSet<&str>> =
48
48
  Lazy::new(|| ["__init__", "index", "mod"].iter().copied().collect());
49
49
 
50
+ /// JVM source roots: `src/<source set>/<language>/` precedes the package
51
+ /// path in Gradle and Maven layouts, so `src/test/kotlin/com/acme/FooTest`
52
+ /// is the module `com.acme.FooTest`, the name its imports spell.
53
+ const JVM_SOURCE_LANGUAGES: &[&str] = &["java", "kotlin", "scala", "groovy"];
54
+
50
55
  fn strip_source_prefix(parts: &[&str]) -> Vec<String> {
51
56
  for (i, part) in parts.iter().enumerate() {
52
57
  if *part == "src" || *part == "lib" || *part == "packages" {
53
- return parts[i + 1..].iter().map(|s| s.to_string()).collect();
58
+ let mut rest = &parts[i + 1..];
59
+ if *part == "src" && rest.len() > 2 && JVM_SOURCE_LANGUAGES.contains(&rest[1]) {
60
+ rest = &rest[2..];
61
+ }
62
+ return rest.iter().map(|s| s.to_string()).collect();
54
63
  }
55
64
  }
56
65
  parts.iter().map(|s| s.to_string()).collect()
@@ -79,10 +79,16 @@ fn extract_yaml_keys(content: &str, max_depth: usize) -> FxHashSet<String> {
79
79
  keys
80
80
  }
81
81
 
82
+ /// The chart directory above `path`, in the analysed source: a snapshot's
83
+ /// chart is its own, whatever the disk holds (#408).
82
84
  fn get_chart_root(path: &Path) -> Option<PathBuf> {
85
+ let source = crate::source::current();
83
86
  let mut current = path.parent()?.to_path_buf();
84
87
  for _ in 0..5 {
85
- if current.join("Chart.yaml").exists() || current.join("chart.yaml").exists() {
88
+ if ["Chart.yaml", "chart.yaml"]
89
+ .iter()
90
+ .any(|name| source.read_bytes(&current.join(name)).is_some())
91
+ {
86
92
  return Some(current);
87
93
  }
88
94
  let parent = current.parent()?;
@@ -418,10 +418,13 @@ fn collect_capped_edges_from(
418
418
  cap_out_edges_per_source(&mut edges, max_per_node);
419
419
  ctx.record_usage(|u| u.final_edges = edges.len() as u64);
420
420
 
421
- let nodes_capped = out_degree
421
+ let capped_sources: FxHashSet<FragmentId> = out_degree
422
422
  .iter()
423
- .filter(|&&d| d as usize > max_per_node)
424
- .count();
423
+ .enumerate()
424
+ .filter(|&(_, &d)| d as usize > max_per_node)
425
+ .map(|(i, _)| idx_to_node[i].clone())
426
+ .collect();
427
+ let nodes_capped = capped_sources.len();
425
428
  let cap_stats = EdgeCapStats {
426
429
  edges_before_cap: deduped_edge_count,
427
430
  edges_after_cap: edges.len(),
@@ -429,6 +432,7 @@ fn collect_capped_edges_from(
429
432
  nodes_capped,
430
433
  max_out_edges_per_node: max_per_node,
431
434
  emissions_by_category,
435
+ capped_sources,
432
436
  };
433
437
 
434
438
  CappedEdges {