diffctx 1.16.0__tar.gz → 1.17.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 (168) hide show
  1. {diffctx-1.16.0 → diffctx-1.17.0}/CHANGELOG.md +77 -1
  2. {diffctx-1.16.0 → diffctx-1.17.0}/Cargo.lock +14 -7
  3. {diffctx-1.16.0 → diffctx-1.17.0}/PKG-INFO +15 -7
  4. {diffctx-1.16.0 → diffctx-1.17.0}/README.md +14 -6
  5. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/Cargo.toml +2 -1
  6. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/README.md +14 -6
  7. diffctx-1.17.0/crates/diffctx-native/src/candidate_files.rs +201 -0
  8. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/weights.rs +58 -18
  9. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/base.rs +9 -1
  10. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/mod.rs +45 -2
  11. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/bazel.rs +11 -4
  12. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/cargo_edges.rs +3 -3
  13. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/go.rs +1 -1
  14. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/javascript.rs +9 -1
  15. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/perl.rs +1 -1
  16. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/shell.rs +17 -3
  17. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/terraform.rs +4 -4
  18. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/structural/testing.rs +1 -1
  19. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/effective_config.rs +3 -0
  20. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/fragmentation.rs +217 -97
  21. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/git.rs +311 -73
  22. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/in_memory_harness.rs +38 -3
  23. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/languages.rs +116 -1
  24. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/lib.rs +1 -1
  25. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/locate.rs +45 -10
  26. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/main.rs +135 -27
  27. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/parsers/config_parser.rs +6 -15
  28. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/parsers/tree_sitter_strategy.rs +75 -14
  29. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/paths.rs +25 -3
  30. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/pipeline.rs +255 -86
  31. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/ppr.rs +4 -0
  32. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/project_graph.rs +7 -3
  33. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/pybridge.rs +22 -10
  34. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/render.rs +175 -3
  35. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/resource.rs +45 -5
  36. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/sanitize.rs +2 -2
  37. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/select.rs +39 -0
  38. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/token_corpus.rs +5 -2
  39. {diffctx-1.16.0 → diffctx-1.17.0}/pyproject.toml +1 -1
  40. {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/_app.py +56 -11
  41. {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/_diffctx.pyi +5 -1
  42. {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/_native/graph_analytics.py +1 -0
  43. {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/_native/pipeline.py +31 -33
  44. {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/cli.py +89 -36
  45. {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/ignore.py +15 -6
  46. {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/mcp/fetch.py +51 -39
  47. {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/mcp/legacy.py +14 -11
  48. {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/mcp/security.py +8 -3
  49. {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/mcp/server.py +11 -4
  50. {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/tree.py +41 -14
  51. diffctx-1.17.0/src/diffctx/version.py +1 -0
  52. {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/writer.py +78 -21
  53. diffctx-1.16.0/crates/diffctx-native/src/candidate_files.rs +0 -134
  54. diffctx-1.16.0/src/diffctx/version.py +0 -1
  55. {diffctx-1.16.0 → diffctx-1.17.0}/Cargo.toml +0 -0
  56. {diffctx-1.16.0 → diffctx-1.17.0}/LICENSE +0 -0
  57. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/analytics.rs +0 -0
  58. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/change_class.rs +0 -0
  59. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/analytics.rs +0 -0
  60. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/bm25.rs +0 -0
  61. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/budget.rs +0 -0
  62. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/category_weights.rs +0 -0
  63. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/edge_weights.rs +0 -0
  64. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/env_overrides.rs +0 -0
  65. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/extensions.rs +0 -0
  66. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/filtering.rs +0 -0
  67. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/fragmentation.rs +0 -0
  68. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/git.rs +0 -0
  69. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/graph_filtering.rs +0 -0
  70. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/importance.rs +0 -0
  71. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/limits.rs +0 -0
  72. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/mod.rs +0 -0
  73. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/mode.rs +0 -0
  74. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/needs.rs +0 -0
  75. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/parsers.rs +0 -0
  76. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/render.rs +0 -0
  77. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/scoring.rs +0 -0
  78. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/selection.rs +0 -0
  79. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/config/tokenization.rs +0 -0
  80. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/core.rs +0 -0
  81. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/discovery.rs +0 -0
  82. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/config_edges/build_system.rs +0 -0
  83. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/config_edges/cicd.rs +0 -0
  84. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/config_edges/docker.rs +0 -0
  85. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/config_edges/generic.rs +0 -0
  86. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/config_edges/helm.rs +0 -0
  87. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/config_edges/kubernetes.rs +0 -0
  88. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/config_edges/mod.rs +0 -0
  89. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/document/mod.rs +0 -0
  90. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/history/cochange.rs +0 -0
  91. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/history/mod.rs +0 -0
  92. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/ansible.rs +0 -0
  93. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/c_family.rs +0 -0
  94. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/clojure.rs +0 -0
  95. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/css.rs +0 -0
  96. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/dart.rs +0 -0
  97. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/dbt.rs +0 -0
  98. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/dotnet.rs +0 -0
  99. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/elixir.rs +0 -0
  100. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/erlang.rs +0 -0
  101. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/graphql.rs +0 -0
  102. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/haskell.rs +0 -0
  103. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/julia.rs +0 -0
  104. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/jvm.rs +0 -0
  105. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/latex.rs +0 -0
  106. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/lua.rs +0 -0
  107. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/mod.rs +0 -0
  108. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/nim.rs +0 -0
  109. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/nix.rs +0 -0
  110. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/ocaml.rs +0 -0
  111. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/openapi.rs +0 -0
  112. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/php.rs +0 -0
  113. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/prisma.rs +0 -0
  114. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/protobuf.rs +0 -0
  115. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/python.rs +0 -0
  116. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/r_lang.rs +0 -0
  117. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/ruby.rs +0 -0
  118. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/rust_lang.rs +0 -0
  119. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/sql.rs +0 -0
  120. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/swift.rs +0 -0
  121. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/tags.rs +0 -0
  122. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/semantic/zig.rs +0 -0
  123. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/similarity/lexical.rs +0 -0
  124. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/similarity/mod.rs +0 -0
  125. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/structural/containment.rs +0 -0
  126. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/structural/mod.rs +0 -0
  127. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/edges/structural/sibling.rs +0 -0
  128. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/excerpt.rs +0 -0
  129. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/facts/mod.rs +0 -0
  130. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/facts/scala.rs +0 -0
  131. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/filtering.rs +0 -0
  132. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/graph.rs +0 -0
  133. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/graph_export.rs +0 -0
  134. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/interval.rs +0 -0
  135. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/mode.rs +0 -0
  136. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/parsers/generic.rs +0 -0
  137. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/parsers/markdown.rs +0 -0
  138. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/parsers/mod.rs +0 -0
  139. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/peak_rss.rs +0 -0
  140. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/postpass.rs +0 -0
  141. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/provenance.rs +0 -0
  142. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/run_provenance.rs +0 -0
  143. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/scoring.rs +0 -0
  144. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/signatures.rs +0 -0
  145. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/stopwords/code.txt +0 -0
  146. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/stopwords/docs.txt +0 -0
  147. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/stopwords.rs +0 -0
  148. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/test_rng.rs +0 -0
  149. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/testfiles.rs +0 -0
  150. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/tokenizer.rs +0 -0
  151. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/types.rs +0 -0
  152. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/utility/boltzmann.rs +0 -0
  153. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/utility/importance.rs +0 -0
  154. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/utility/mod.rs +0 -0
  155. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/utility/needs.rs +0 -0
  156. {diffctx-1.16.0 → diffctx-1.17.0}/crates/diffctx-native/src/utility/objective.rs +0 -0
  157. {diffctx-1.16.0 → diffctx-1.17.0}/rust-toolchain.toml +0 -0
  158. {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/__init__.py +0 -0
  159. {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/__main__.py +0 -0
  160. {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/_native/__init__.py +0 -0
  161. {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/_native/graph_export.py +0 -0
  162. {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/_native/project_graph.py +0 -0
  163. {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/clipboard.py +0 -0
  164. {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/logger.py +0 -0
  165. {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/mcp/__init__.py +0 -0
  166. {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/mcp/__main__.py +0 -0
  167. {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/py.typed +0 -0
  168. {diffctx-1.16.0 → diffctx-1.17.0}/src/diffctx/tokens.py +0 -0
@@ -7,6 +7,81 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.17.0] - 2026-09-23
11
+
12
+ ### Added
13
+
14
+ - **Windows and Intel-Mac are release targets.** Every test matrix runs on
15
+ Linux, macOS and Windows on x64 and ARM; wheels and binaries ship for
16
+ `x86_64-apple-darwin` and `aarch64-pc-windows-msvc` as well, and `npm
17
+ install diffctx` and the Scoop bucket select them. What that exposed is
18
+ fixed: `--output-file` on Windows, backslashes in MCP output and in the
19
+ Rust path matching (discovery, C#/Go/Cargo edges), datasets checked out
20
+ byte-exact under a CRLF `autocrlf`, and a `--timeout 0` that now always
21
+ reports the timeout instead of racing the subprocess.
22
+ - **The landing page installs and opens offline** — a web manifest, icons
23
+ and a service worker that keeps every visit network-first.
24
+ - **Free-threaded CPython gets a wheel.** Each platform ships a `cp314t`
25
+ wheel beside the `abi3` one; `uvx` on 3.14t no longer compiles the sdist
26
+ (which timed MCP clients out), and the extension keeps the GIL off.
27
+ - **The GitHub Action works on a default shallow checkout**: with no
28
+ `diff-range` it fetches the pull request's base and head (or `HEAD~1`)
29
+ itself, so `fetch-depth: 0` is no longer required.
30
+ - **`provenance.engine.build` names the release commit** in wheels, binaries
31
+ and the image.
32
+ - **A glossary line under the scoring modes** on the landing page: three
33
+ independent first-visit probes stalled on `ego-network`, `PPR`, `rrf`,
34
+ `pit`, `τ` and `o200k_base` with nothing on the page defining them.
35
+
36
+ ### Removed
37
+
38
+ - **`build_diff_context(ignore_file=…, whitelist_file=…, no_default_ignores=…)`.**
39
+ The engine never applied a custom path-spec layer in diff mode and the three
40
+ arguments existed only to raise `NotImplementedError`; passing one is now a
41
+ `TypeError`. `.gitignore` and `.diffctx/ignore` still apply.
42
+
43
+ ### Fixed
44
+
45
+ - **The MCP server no longer hangs on Windows.** Every git child inherited the
46
+ server's stdin, which under an MCP client is the JSON-RPC pipe the client
47
+ holds open, and on Windows such a git never exited: any tool call that reached
48
+ git hung. Child processes now get a null stdin in both the engine and the
49
+ Python layer.
50
+ - **A long range says how many commits it held.** The commit list stops at
51
+ the newest twenty; the artifact read "20 commits" over a 42-commit range. It
52
+ now carries `commit_count` and reads "newest 20 of 42 commits" when the list
53
+ is cut, in both CLIs, pack and locate.
54
+ - **`--budget` fitting never passes context off as the change.** The witness
55
+ clip shortened any file's last fragment, so a context file shrank line by line
56
+ under a "more lines of this change" marker before it dropped; only a changed
57
+ fragment is shortened now.
58
+ - **MCP `fragment_ids` on a bare `diff_ref` read the code the change
59
+ replaced.** `HEAD` (or `HEAD~3`, or a duration window) is git's
60
+ base-versus-working-tree, so the locate ranking describes the files on
61
+ disk; the fetch read the base blob instead and returned the old body under
62
+ the ranking's own id. Bodies for a `..`-less ref now come from the working
63
+ tree, and a file the named revision lacks (or a revision git cannot
64
+ resolve) is reported as not found instead of being substituted with the
65
+ working-tree file under a historical label.
66
+ - **The native binary fits the written artifact to `--budget`** (#277).
67
+ npm, Scoop, Docker and the release archives ship the Rust binary, which
68
+ documented `--budget` as a cap on the whole artifact and enforced nothing on
69
+ the document it wrote: 9,121 tokens at `--budget 8000` on a real range. It
70
+ now runs the same render-count-drop loop as the Python CLI (context from the
71
+ tail first, a changed file's second fragment next, mechanical witnesses
72
+ before hand-written ones), records `selection_budget_exceeded` in
73
+ `coverage`, and exits 4 when nothing fits — the two CLIs deliver one set.
74
+ - **Eval harness: gold paths git had quoted** (`"b/caf\303\251.py"`) were
75
+ taken literally, so every non-ASCII gold file scored as a miss; the patch
76
+ header parser unquotes them. A checkpoint whose last line a kill left
77
+ unterminated no longer swallows the next appended row on resume.
78
+
79
+ ### Changed
80
+
81
+ - The GitHub Action's `python-version` defaults to `3.13` (`--output-file`
82
+ needs `os.fchmod`, which Windows Python gained in 3.13); the action
83
+ reference page says so. `--budget --help` names what `auto` resolves to.
84
+
10
85
  ## [1.16.0] - 2026-09-16
11
86
 
12
87
  ### Added
@@ -1551,7 +1626,8 @@ Earlier releases shipped as `treemapper`; see
1551
1626
  <https://github.com/nikolay-e/diffctx/releases> for the corresponding GitHub
1552
1627
  release notes (`1.0.0` through `1.6.1`).
1553
1628
 
1554
- [Unreleased]: https://github.com/nikolay-e/diffctx/compare/v1.16.0...HEAD
1629
+ [Unreleased]: https://github.com/nikolay-e/diffctx/compare/v1.17.0...HEAD
1630
+ [1.17.0]: https://github.com/nikolay-e/diffctx/compare/v1.16.0...v1.17.0
1555
1631
  [1.16.0]: https://github.com/nikolay-e/diffctx/compare/v1.15.0...v1.16.0
1556
1632
  [1.15.0]: https://github.com/nikolay-e/diffctx/compare/v1.14.0...v1.15.0
1557
1633
  [1.14.0]: https://github.com/nikolay-e/diffctx/compare/v1.13.0...v1.14.0
@@ -123,9 +123,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
123
123
 
124
124
  [[package]]
125
125
  name = "clap"
126
- version = "4.6.6"
126
+ version = "4.6.7"
127
127
  source = "registry+https://github.com/rust-lang/crates.io-index"
128
- checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
128
+ checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946"
129
129
  dependencies = [
130
130
  "clap_builder",
131
131
  "clap_derive",
@@ -133,9 +133,9 @@ dependencies = [
133
133
 
134
134
  [[package]]
135
135
  name = "clap_builder"
136
- version = "4.6.6"
136
+ version = "4.6.7"
137
137
  source = "registry+https://github.com/rust-lang/crates.io-index"
138
- checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
138
+ checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d"
139
139
  dependencies = [
140
140
  "anstream",
141
141
  "anstyle",
@@ -145,9 +145,9 @@ dependencies = [
145
145
 
146
146
  [[package]]
147
147
  name = "clap_derive"
148
- version = "4.6.4"
148
+ version = "4.6.7"
149
149
  source = "registry+https://github.com/rust-lang/crates.io-index"
150
- checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
150
+ checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0"
151
151
  dependencies = [
152
152
  "heck",
153
153
  "proc-macro2",
@@ -194,11 +194,12 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
194
194
 
195
195
  [[package]]
196
196
  name = "diffctx"
197
- version = "1.16.0"
197
+ version = "1.17.0"
198
198
  dependencies = [
199
199
  "aho-corasick",
200
200
  "anyhow",
201
201
  "clap",
202
+ "dunce",
202
203
  "glob",
203
204
  "libc",
204
205
  "libtest-mimic",
@@ -260,6 +261,12 @@ dependencies = [
260
261
  "walkdir",
261
262
  ]
262
263
 
264
+ [[package]]
265
+ name = "dunce"
266
+ version = "1.0.5"
267
+ source = "registry+https://github.com/rust-lang/crates.io-index"
268
+ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
269
+
263
270
  [[package]]
264
271
  name = "dyn-clone"
265
272
  version = "1.0.20"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diffctx
3
- Version: 1.16.0
3
+ Version: 1.17.0
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -104,8 +104,15 @@ scoop bucket add diffctx https://github.com/nikolay-e/diffctx
104
104
  scoop install diffctx/diffctx
105
105
  ```
106
106
 
107
- Prebuilt binaries for linux (x86_64/aarch64), macOS (arm64) and Windows (x64)
108
- are attached to every [release](https://github.com/nikolay-e/diffctx/releases/latest).
107
+ Every [release](https://github.com/nikolay-e/diffctx/releases/latest) carries
108
+ prebuilt binaries; the current target set is Linux (x86_64/aarch64), macOS
109
+ (arm64/x86_64) and Windows (x64/arm64), and a release older than a target's
110
+ first build lacks it (v1.16.0 predates the macOS x86_64 and Windows arm64
111
+ archives). On Windows on ARM, `diffctx[mcp]` builds `cryptography` from source,
112
+ which needs OpenSSL: that project publishes no wheel for the platform.
113
+ Free-threaded CPython (`3.14t`) cannot load the `abi3` wheel, so each release
114
+ after 1.16.0 also ships a `cp314t` wheel per platform (the extension runs with
115
+ the GIL off); on 1.16.0 itself `pip` and `uv` fall back to compiling the sdist.
109
116
  The native binary and Docker image cover diff mode with YAML/JSON output and
110
117
  write to stdout (redirect to capture); tree mode, Markdown output, the `graph`
111
118
  subcommand and the MCP server live in the Python package.
@@ -293,8 +300,9 @@ map a directory named `mcp` on older releases.
293
300
 
294
301
  Respects `.gitignore` and `.diffctx/ignore` automatically — hierarchically at
295
302
  every directory level, with full gitignore semantics (negation `!important.log`,
296
- anchored `/root_only.txt`), and the output file is always auto-ignored. Three
297
- controls are tree mode only and are refused with `--diff`: `.diffctx/whitelist`
303
+ anchored `/root_only.txt`), and in tree mode the output file is always
304
+ auto-ignored (`--diff` reports a saved `tree.md` like any other change). Three
305
+ controls are tree mode only and are refused with `--diff` and `graph`: `.diffctx/whitelist`
298
306
  (`-w`) as an include-only filter, `-i` for an extra ignore file, and
299
307
  `--no-default-ignores` / `--no-ignores` to drop the built-in patterns or every
300
308
  ignore rule.
@@ -318,8 +326,8 @@ its size (default 512 MB, `0` disables eviction).
318
326
  |------|---------|
319
327
  | `0` | Success — output contains content |
320
328
  | `1` | Runtime error (bad path, permission denied, etc.) |
321
- | `2` | Usage error (invalid flags/arguments) |
322
- | `3` | Environment error (`--diff` outside a git repo, git not installed, no commits yet) |
329
+ | `2` | Usage error (invalid flags/arguments, an empty `--diff ""`, a malformed duration such as `1.5h`, an out-of-range `--alpha`/`--tau`/`--budget`/`--timeout`, or a tree-mode flag — `-i`/`-w`/`--no-default-ignores`/`--no-ignores` — given with `--diff` or `graph`) |
330
+ | `3` | Environment error (`--diff` outside a git repo, in a bare one, or on a missing path, git not installed or refusing to start — its `fatal:` line is printed, no commits yet, a revision missing from a shallow clone) |
323
331
  | `4` | `--diff` produced no semantic context (clean tree, binary-only, everything filtered); output is still emitted. Deletion/rename/lockfile-only diffs list `deleted_files`/`renamed_files`/`lockfile_changes` and exit `0` |
324
332
  | `124`| `--diff` ran 30 s past the `--timeout` deadline without stopping cooperatively (the deadline itself yields a partial artifact and exit 0) |
325
333
  | `130`| Interrupted (Ctrl-C) |
@@ -45,8 +45,15 @@ scoop bucket add diffctx https://github.com/nikolay-e/diffctx
45
45
  scoop install diffctx/diffctx
46
46
  ```
47
47
 
48
- Prebuilt binaries for linux (x86_64/aarch64), macOS (arm64) and Windows (x64)
49
- are attached to every [release](https://github.com/nikolay-e/diffctx/releases/latest).
48
+ Every [release](https://github.com/nikolay-e/diffctx/releases/latest) carries
49
+ prebuilt binaries; the current target set is Linux (x86_64/aarch64), macOS
50
+ (arm64/x86_64) and Windows (x64/arm64), and a release older than a target's
51
+ first build lacks it (v1.16.0 predates the macOS x86_64 and Windows arm64
52
+ archives). On Windows on ARM, `diffctx[mcp]` builds `cryptography` from source,
53
+ which needs OpenSSL: that project publishes no wheel for the platform.
54
+ Free-threaded CPython (`3.14t`) cannot load the `abi3` wheel, so each release
55
+ after 1.16.0 also ships a `cp314t` wheel per platform (the extension runs with
56
+ the GIL off); on 1.16.0 itself `pip` and `uv` fall back to compiling the sdist.
50
57
  The native binary and Docker image cover diff mode with YAML/JSON output and
51
58
  write to stdout (redirect to capture); tree mode, Markdown output, the `graph`
52
59
  subcommand and the MCP server live in the Python package.
@@ -234,8 +241,9 @@ map a directory named `mcp` on older releases.
234
241
 
235
242
  Respects `.gitignore` and `.diffctx/ignore` automatically — hierarchically at
236
243
  every directory level, with full gitignore semantics (negation `!important.log`,
237
- anchored `/root_only.txt`), and the output file is always auto-ignored. Three
238
- controls are tree mode only and are refused with `--diff`: `.diffctx/whitelist`
244
+ anchored `/root_only.txt`), and in tree mode the output file is always
245
+ auto-ignored (`--diff` reports a saved `tree.md` like any other change). Three
246
+ controls are tree mode only and are refused with `--diff` and `graph`: `.diffctx/whitelist`
239
247
  (`-w`) as an include-only filter, `-i` for an extra ignore file, and
240
248
  `--no-default-ignores` / `--no-ignores` to drop the built-in patterns or every
241
249
  ignore rule.
@@ -259,8 +267,8 @@ its size (default 512 MB, `0` disables eviction).
259
267
  |------|---------|
260
268
  | `0` | Success — output contains content |
261
269
  | `1` | Runtime error (bad path, permission denied, etc.) |
262
- | `2` | Usage error (invalid flags/arguments) |
263
- | `3` | Environment error (`--diff` outside a git repo, git not installed, no commits yet) |
270
+ | `2` | Usage error (invalid flags/arguments, an empty `--diff ""`, a malformed duration such as `1.5h`, an out-of-range `--alpha`/`--tau`/`--budget`/`--timeout`, or a tree-mode flag — `-i`/`-w`/`--no-default-ignores`/`--no-ignores` — given with `--diff` or `graph`) |
271
+ | `3` | Environment error (`--diff` outside a git repo, in a bare one, or on a missing path, git not installed or refusing to start — its `fatal:` line is printed, no commits yet, a revision missing from a shallow clone) |
264
272
  | `4` | `--diff` produced no semantic context (clean tree, binary-only, everything filtered); output is still emitted. Deletion/rename/lockfile-only diffs list `deleted_files`/`renamed_files`/`lockfile_changes` and exit `0` |
265
273
  | `124`| `--diff` ran 30 s past the `--timeout` deadline without stopping cooperatively (the deadline itself yields a partial artifact and exit 0) |
266
274
  | `130`| Interrupted (Ctrl-C) |
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "diffctx"
3
- version = "1.16.0"
3
+ version = "1.17.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"
@@ -71,6 +71,7 @@ lang-extra = [
71
71
  [dependencies]
72
72
  mimalloc = { version = "0.1", default-features = false }
73
73
  clap = { version = "4", features = ["derive"] }
74
+ dunce = "1"
74
75
  tree-sitter = "0.25"
75
76
  tree-sitter-python = { version = "0.25", optional = true }
76
77
  tree-sitter-javascript = { version = "0.25", optional = true }
@@ -45,8 +45,15 @@ scoop bucket add diffctx https://github.com/nikolay-e/diffctx
45
45
  scoop install diffctx/diffctx
46
46
  ```
47
47
 
48
- Prebuilt binaries for linux (x86_64/aarch64), macOS (arm64) and Windows (x64)
49
- are attached to every [release](https://github.com/nikolay-e/diffctx/releases/latest).
48
+ Every [release](https://github.com/nikolay-e/diffctx/releases/latest) carries
49
+ prebuilt binaries; the current target set is Linux (x86_64/aarch64), macOS
50
+ (arm64/x86_64) and Windows (x64/arm64), and a release older than a target's
51
+ first build lacks it (v1.16.0 predates the macOS x86_64 and Windows arm64
52
+ archives). On Windows on ARM, `diffctx[mcp]` builds `cryptography` from source,
53
+ which needs OpenSSL: that project publishes no wheel for the platform.
54
+ Free-threaded CPython (`3.14t`) cannot load the `abi3` wheel, so each release
55
+ after 1.16.0 also ships a `cp314t` wheel per platform (the extension runs with
56
+ the GIL off); on 1.16.0 itself `pip` and `uv` fall back to compiling the sdist.
50
57
  The native binary and Docker image cover diff mode with YAML/JSON output and
51
58
  write to stdout (redirect to capture); tree mode, Markdown output, the `graph`
52
59
  subcommand and the MCP server live in the Python package.
@@ -234,8 +241,9 @@ map a directory named `mcp` on older releases.
234
241
 
235
242
  Respects `.gitignore` and `.diffctx/ignore` automatically — hierarchically at
236
243
  every directory level, with full gitignore semantics (negation `!important.log`,
237
- anchored `/root_only.txt`), and the output file is always auto-ignored. Three
238
- controls are tree mode only and are refused with `--diff`: `.diffctx/whitelist`
244
+ anchored `/root_only.txt`), and in tree mode the output file is always
245
+ auto-ignored (`--diff` reports a saved `tree.md` like any other change). Three
246
+ controls are tree mode only and are refused with `--diff` and `graph`: `.diffctx/whitelist`
239
247
  (`-w`) as an include-only filter, `-i` for an extra ignore file, and
240
248
  `--no-default-ignores` / `--no-ignores` to drop the built-in patterns or every
241
249
  ignore rule.
@@ -259,8 +267,8 @@ its size (default 512 MB, `0` disables eviction).
259
267
  |------|---------|
260
268
  | `0` | Success — output contains content |
261
269
  | `1` | Runtime error (bad path, permission denied, etc.) |
262
- | `2` | Usage error (invalid flags/arguments) |
263
- | `3` | Environment error (`--diff` outside a git repo, git not installed, no commits yet) |
270
+ | `2` | Usage error (invalid flags/arguments, an empty `--diff ""`, a malformed duration such as `1.5h`, an out-of-range `--alpha`/`--tau`/`--budget`/`--timeout`, or a tree-mode flag — `-i`/`-w`/`--no-default-ignores`/`--no-ignores` — given with `--diff` or `graph`) |
271
+ | `3` | Environment error (`--diff` outside a git repo, in a bare one, or on a missing path, git not installed or refusing to start — its `fatal:` line is printed, no commits yet, a revision missing from a shallow clone) |
264
272
  | `4` | `--diff` produced no semantic context (clean tree, binary-only, everything filtered); output is still emitted. Deletion/rename/lockfile-only diffs list `deleted_files`/`renamed_files`/`lockfile_changes` and exit `0` |
265
273
  | `124`| `--diff` ran 30 s past the `--timeout` deadline without stopping cooperatively (the deadline itself yields a partial artifact and exit 0) |
266
274
  | `130`| Interrupted (Ctrl-C) |
@@ -0,0 +1,201 @@
1
+ use std::path::{Path, PathBuf};
2
+
3
+ use rayon::prelude::*;
4
+ use rustc_hash::{FxHashMap, FxHashSet};
5
+ use walkdir::WalkDir;
6
+
7
+ use crate::config::graph_filtering::GRAPH_FILTERING;
8
+ use crate::config::limits::LIMITS;
9
+ use crate::git;
10
+ use crate::languages::get_language_for_file;
11
+
12
+ const SHEBANG_SNIFF_BYTES: usize = 128;
13
+
14
+ fn is_allowed_file(path: &Path) -> bool {
15
+ let name = path.to_string_lossy();
16
+ get_language_for_file(&name).is_some()
17
+ || (path.extension().is_none() && has_known_shebang(path, &name))
18
+ }
19
+
20
+ fn has_known_shebang(path: &Path, name: &str) -> bool {
21
+ use std::io::Read;
22
+ let mut head = [0u8; SHEBANG_SNIFF_BYTES];
23
+ let Ok(read) = std::fs::File::open(path).and_then(|mut f| f.read(&mut head)) else {
24
+ return false;
25
+ };
26
+ crate::languages::sniff_language(name, &String::from_utf8_lossy(&head[..read])).is_some()
27
+ }
28
+
29
+ fn is_candidate_file(
30
+ file_path: &Path,
31
+ root_dir: &Path,
32
+ included_set: &FxHashSet<PathBuf>,
33
+ ctx: &crate::resource::RunContext,
34
+ ) -> bool {
35
+ // `is_file()` and `metadata()` both follow symlinks, and every path here is
36
+ // only LEXICALLY under the root — `git ls-files` reports the link, not its
37
+ // target. Without this the universe admits `repo/evil -> /etc/shadow`,
38
+ // discovery reads it with a bare `read_to_string`, and the content is a
39
+ // context fragment. The boundary is checked once, here, so the readers
40
+ // downstream stay simple; a link pointing INSIDE the repo still resolves.
41
+ if crate::paths::resolve_within(root_dir, file_path).is_none() {
42
+ return false;
43
+ }
44
+ if !file_path.is_file() {
45
+ return false;
46
+ }
47
+ if !is_allowed_file(file_path) {
48
+ return false;
49
+ }
50
+ if included_set.contains(file_path) {
51
+ return false;
52
+ }
53
+ match file_path.metadata() {
54
+ Ok(meta) if meta.len() as usize > LIMITS.max_file_size => {
55
+ // A source file the universe will not read is a hole in the
56
+ // context, and the artifact says so rather than rendering as
57
+ // complete (#T4.2: a 237 KB helper importing the changed file
58
+ // vanished with no coverage block).
59
+ ctx.note(crate::resource::LimitReason::FileTooLarge);
60
+ return false;
61
+ }
62
+ Err(_) => return false,
63
+ _ => {}
64
+ }
65
+ true
66
+ }
67
+
68
+ /// The discovery universe under `scope` (repo-relative pathspecs; empty is
69
+ /// the whole repository), with every size exclusion recorded on `ctx`.
70
+ pub fn collect_candidate_files(
71
+ root_dir: &Path,
72
+ included_set: &FxHashSet<PathBuf>,
73
+ scope: &[String],
74
+ ctx: &crate::resource::RunContext,
75
+ ) -> Vec<PathBuf> {
76
+ match tracked_candidates(root_dir, included_set, scope, ctx) {
77
+ Some(files) => filter_ignored_and_secret(root_dir, files),
78
+ // The walk sees untracked paths, so ancestor-inherited rules must count
79
+ // (`.venv/x.py` is ignored BY `.venv/`); the attribution variant drops them.
80
+ None => filter_ignored_and_secret_walked(
81
+ root_dir,
82
+ walked_candidates(root_dir, included_set, scope, ctx),
83
+ ),
84
+ }
85
+ }
86
+
87
+ fn tracked_candidates(
88
+ root_dir: &Path,
89
+ included_set: &FxHashSet<PathBuf>,
90
+ scope: &[String],
91
+ ctx: &crate::resource::RunContext,
92
+ ) -> Option<Vec<PathBuf>> {
93
+ let mut args: Vec<&str> = vec!["ls-files", "-z"];
94
+ if !scope.is_empty() {
95
+ args.push("--");
96
+ args.extend(scope.iter().map(String::as_str));
97
+ }
98
+ let parts = git::run_git_z(root_dir, &args).ok()?;
99
+ Some(
100
+ parts
101
+ .into_iter()
102
+ .map(|f| crate::paths::repo_join(root_dir, &f))
103
+ .collect::<Vec<PathBuf>>()
104
+ .into_par_iter()
105
+ .filter(|f| is_candidate_file(f, root_dir, included_set, ctx))
106
+ .collect(),
107
+ )
108
+ }
109
+
110
+ fn is_walk_skipped_dir(entry: &walkdir::DirEntry) -> bool {
111
+ entry.depth() > 0
112
+ && entry.file_type().is_dir()
113
+ && entry.file_name().to_str().is_some_and(|name| {
114
+ name.starts_with('.') || name == "node_modules" || name == "__pycache__"
115
+ })
116
+ }
117
+
118
+ fn walked_candidates(
119
+ root_dir: &Path,
120
+ included_set: &FxHashSet<PathBuf>,
121
+ scope: &[String],
122
+ ctx: &crate::resource::RunContext,
123
+ ) -> Vec<PathBuf> {
124
+ let walk_roots: Vec<PathBuf> = if scope.is_empty() {
125
+ vec![root_dir.to_path_buf()]
126
+ } else {
127
+ scope
128
+ .iter()
129
+ .map(|rel| crate::paths::repo_join(root_dir, rel))
130
+ .collect()
131
+ };
132
+ let mut admitted: Vec<PathBuf> = Vec::new();
133
+ for walk_root in walk_roots {
134
+ let files = WalkDir::new(walk_root)
135
+ .sort_by_file_name()
136
+ .into_iter()
137
+ .filter_entry(|e| !is_walk_skipped_dir(e))
138
+ .filter_map(Result::ok)
139
+ .filter(|e| e.file_type().is_file());
140
+ for entry in files {
141
+ let path = entry.into_path();
142
+ if is_candidate_file(&path, root_dir, included_set, ctx) {
143
+ admitted.push(path);
144
+ }
145
+ // Counted AFTER admission: capping raw entries meant a repository
146
+ // whose first N walked files are ignored (a vendored tree, a build
147
+ // directory) produced a nearly empty universe while the useful files
148
+ // sat just past the cut.
149
+ if admitted.len() >= GRAPH_FILTERING.fallback_max_files {
150
+ return admitted;
151
+ }
152
+ }
153
+ }
154
+ admitted
155
+ }
156
+
157
+ fn filter_ignored_and_secret_walked(root_dir: &Path, files: Vec<PathBuf>) -> Vec<PathBuf> {
158
+ let rel_paths: Vec<String> = files
159
+ .iter()
160
+ .filter_map(|f| crate::pipeline::rel_path_string(root_dir, f))
161
+ .collect();
162
+ let ignored: FxHashMap<String, git::IgnoreSource> =
163
+ git::find_ignored_paths_any(root_dir, &rel_paths)
164
+ .into_iter()
165
+ .map(|p| (p, git::IgnoreSource::Gitignore))
166
+ .collect();
167
+ files
168
+ .into_par_iter()
169
+ .filter(|f| !crate::pipeline::is_withheld(root_dir, f, &ignored))
170
+ .collect()
171
+ }
172
+
173
+ /// Discovery's candidate universe otherwise skips straight from a language
174
+ /// check to the graph: an unchanged file a changed file imports would render
175
+ /// as neighbour context even when `.diffctx/ignore` explicitly excludes it,
176
+ /// or when it is secret-like (`id_rsa`, `*.pem`, ...) — `changed_files` is
177
+ /// filtered this way already (`pipeline::compute_scored_state`), the
178
+ /// discovery universe was not. One batched `git check-ignore` call covers
179
+ /// every survivor of the language filter, so cost stays O(1) subprocess
180
+ /// invocations regardless of repo size, and the final filter still runs in
181
+ /// parallel via rayon.
182
+ fn filter_ignored_and_secret(root_dir: &Path, files: Vec<PathBuf>) -> Vec<PathBuf> {
183
+ let rel_paths: Vec<String> = files
184
+ .iter()
185
+ .filter_map(|f| crate::pipeline::rel_path_string(root_dir, f))
186
+ .collect();
187
+ let ignored_rel_paths = git::find_ignored_paths_with_source(root_dir, &rel_paths);
188
+ files
189
+ .into_par_iter()
190
+ .filter(|f| !crate::pipeline::is_withheld(root_dir, f, &ignored_rel_paths))
191
+ .collect()
192
+ }
193
+
194
+ pub fn normalize_path(path: &Path, root_dir: &Path) -> PathBuf {
195
+ if path.is_absolute() {
196
+ dunce::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
197
+ } else {
198
+ let joined = crate::paths::repo_join(root_dir, &path.to_string_lossy());
199
+ dunce::canonicalize(&joined).unwrap_or(joined)
200
+ }
201
+ }
@@ -23,7 +23,6 @@ impl EdgeWeightConfig {
23
23
  pub static EDGE_WEIGHTS: Lazy<FxHashMap<&'static str, EdgeWeightConfig>> = Lazy::new(|| {
24
24
  let entries: &[(&str, EdgeWeightConfig)] = &[
25
25
  ("containment", EdgeWeightConfig::new(0.50, 0.70)),
26
- ("import", EdgeWeightConfig::new(0.50, 0.70)),
27
26
  ("test_direct", EdgeWeightConfig::new(0.60, 0.50)),
28
27
  ("test_naming", EdgeWeightConfig::new(0.50, 0.50)),
29
28
  ("test_reverse", EdgeWeightConfig::new(0.30, 1.0)),
@@ -33,12 +32,6 @@ pub static EDGE_WEIGHTS: Lazy<FxHashMap<&'static str, EdgeWeightConfig>> = Lazy:
33
32
  ("doc_structure", EdgeWeightConfig::new(0.30, 0.83)),
34
33
  ("anchor_link", EdgeWeightConfig::new(0.55, 0.64)),
35
34
  ("citation", EdgeWeightConfig::new(0.25, 1.0)),
36
- ("python_call", EdgeWeightConfig::new(0.65, 0.70)),
37
- ("python_symbol", EdgeWeightConfig::new(0.70, 0.70)),
38
- ("python_type", EdgeWeightConfig::new(0.50, 0.70)),
39
- ("javascript_call", EdgeWeightConfig::new(0.70, 0.50)),
40
- ("javascript_symbol", EdgeWeightConfig::new(0.75, 0.50)),
41
- ("javascript_type", EdgeWeightConfig::new(0.65, 0.50)),
42
35
  ("go_import", EdgeWeightConfig::new(0.70, 0.40)),
43
36
  ("go_type", EdgeWeightConfig::new(0.65, 0.40)),
44
37
  ("go_func", EdgeWeightConfig::new(0.60, 0.40)),
@@ -68,20 +61,16 @@ pub static EDGE_WEIGHTS: Lazy<FxHashMap<&'static str, EdgeWeightConfig>> = Lazy:
68
61
  ("ruby_require", EdgeWeightConfig::new(0.65, 0.40)),
69
62
  ("ruby_include", EdgeWeightConfig::new(0.60, 0.40)),
70
63
  ("ruby_const", EdgeWeightConfig::new(0.55, 0.40)),
71
- ("ruby_same_dir", EdgeWeightConfig::new(0.05, 0.40)),
72
64
  ("php_use", EdgeWeightConfig::new(0.65, 0.40)),
73
65
  ("php_require", EdgeWeightConfig::new(0.60, 0.40)),
74
66
  ("php_inheritance", EdgeWeightConfig::new(0.75, 0.40)),
75
67
  ("php_type", EdgeWeightConfig::new(0.55, 0.40)),
76
- ("php_same_namespace", EdgeWeightConfig::new(0.05, 0.40)),
77
68
  ("shell_source", EdgeWeightConfig::new(0.60, 0.35)),
78
69
  ("shell_script", EdgeWeightConfig::new(0.50, 0.35)),
79
70
  ("swift_import", EdgeWeightConfig::new(0.65, 0.40)),
80
71
  ("swift_conformance", EdgeWeightConfig::new(0.70, 0.40)),
81
72
  ("swift_extension", EdgeWeightConfig::new(0.65, 0.40)),
82
73
  ("swift_type", EdgeWeightConfig::new(0.60, 0.40)),
83
- ("swift_func", EdgeWeightConfig::new(0.55, 0.40)),
84
- ("swift_same_module", EdgeWeightConfig::new(0.05, 0.40)),
85
74
  ("zig_import", EdgeWeightConfig::new(0.65, 0.40)),
86
75
  ("zig_type", EdgeWeightConfig::new(0.60, 0.40)),
87
76
  ("zig_fn", EdgeWeightConfig::new(0.55, 0.40)),
@@ -95,20 +84,16 @@ pub static EDGE_WEIGHTS: Lazy<FxHashMap<&'static str, EdgeWeightConfig>> = Lazy:
95
84
  ("proto_import", EdgeWeightConfig::new(0.65, 0.40)),
96
85
  ("proto_message_ref", EdgeWeightConfig::new(0.60, 0.40)),
97
86
  ("proto_service_rpc", EdgeWeightConfig::new(0.55, 0.40)),
98
- ("graphql_import", EdgeWeightConfig::new(0.60, 0.40)),
99
87
  ("graphql_type_ref", EdgeWeightConfig::new(0.65, 0.40)),
100
88
  ("graphql_extend", EdgeWeightConfig::new(0.55, 0.40)),
101
89
  ("sql_fk", EdgeWeightConfig::new(0.70, 0.40)),
102
90
  ("sql_table_ref", EdgeWeightConfig::new(0.60, 0.40)),
103
- ("sql_view_source", EdgeWeightConfig::new(0.65, 0.40)),
104
- ("sql_migration", EdgeWeightConfig::new(0.55, 0.40)),
105
91
  ("css_import", EdgeWeightConfig::new(0.55, 0.40)),
106
92
  ("lua_require", EdgeWeightConfig::new(0.65, 0.40)),
107
93
  ("lua_fn", EdgeWeightConfig::new(0.55, 0.40)),
108
94
  ("lua_method", EdgeWeightConfig::new(0.50, 0.40)),
109
95
  ("openapi_internal_ref", EdgeWeightConfig::new(0.65, 0.40)),
110
96
  ("openapi_external_ref", EdgeWeightConfig::new(0.60, 0.40)),
111
- ("openapi_schema_ref", EdgeWeightConfig::new(0.55, 0.40)),
112
97
  ("erlang_include", EdgeWeightConfig::new(0.65, 0.40)),
113
98
  ("erlang_behaviour", EdgeWeightConfig::new(0.60, 0.40)),
114
99
  ("erlang_call", EdgeWeightConfig::new(0.55, 0.40)),
@@ -117,7 +102,6 @@ pub static EDGE_WEIGHTS: Lazy<FxHashMap<&'static str, EdgeWeightConfig>> = Lazy:
117
102
  ("elixir_behaviour", EdgeWeightConfig::new(0.55, 0.40)),
118
103
  ("elixir_fn", EdgeWeightConfig::new(0.50, 0.40)),
119
104
  ("dart_import", EdgeWeightConfig::new(0.65, 0.40)),
120
- ("dart_export", EdgeWeightConfig::new(0.60, 0.40)),
121
105
  ("dart_type", EdgeWeightConfig::new(0.60, 0.40)),
122
106
  ("dart_fn", EdgeWeightConfig::new(0.55, 0.40)),
123
107
  ("dart_inheritance", EdgeWeightConfig::new(0.70, 0.40)),
@@ -129,11 +113,9 @@ pub static EDGE_WEIGHTS: Lazy<FxHashMap<&'static str, EdgeWeightConfig>> = Lazy:
129
113
  ("ocaml_fn", EdgeWeightConfig::new(0.55, 0.40)),
130
114
  ("ocaml_module_ref", EdgeWeightConfig::new(0.60, 0.40)),
131
115
  ("r_source", EdgeWeightConfig::new(0.60, 0.40)),
132
- ("r_library", EdgeWeightConfig::new(0.55, 0.40)),
133
116
  ("r_fn", EdgeWeightConfig::new(0.50, 0.40)),
134
117
  ("r_s4", EdgeWeightConfig::new(0.55, 0.40)),
135
118
  ("perl_use", EdgeWeightConfig::new(0.65, 0.40)),
136
- ("perl_require", EdgeWeightConfig::new(0.60, 0.40)),
137
119
  ("perl_fn", EdgeWeightConfig::new(0.55, 0.40)),
138
120
  ("perl_method", EdgeWeightConfig::new(0.50, 0.40)),
139
121
  ("perl_inheritance", EdgeWeightConfig::new(0.65, 0.40)),
@@ -220,3 +202,61 @@ pub static LANG_WEIGHTS: Lazy<FxHashMap<&'static str, LangWeights>> = Lazy::new(
220
202
  ];
221
203
  entries.iter().copied().collect()
222
204
  });
205
+
206
+ #[cfg(test)]
207
+ mod tests {
208
+ use super::*;
209
+ use std::collections::BTreeSet;
210
+ use std::path::{Path, PathBuf};
211
+
212
+ fn collect_rs_files(dir: &Path, out: &mut Vec<PathBuf>) {
213
+ for entry in std::fs::read_dir(dir).unwrap().flatten() {
214
+ let path = entry.path();
215
+ if path.is_dir() {
216
+ collect_rs_files(&path, out);
217
+ } else if path.extension().is_some_and(|e| e == "rs") {
218
+ out.push(path);
219
+ }
220
+ }
221
+ }
222
+
223
+ /// A weight nobody reads is a model parameter that does not exist: cases
224
+ /// named after such a channel pass without it, and tuning it changes
225
+ /// nothing. Eighteen keys sat in this table unread until 2026-09-22.
226
+ #[test]
227
+ fn every_edge_weight_key_is_read_somewhere_outside_this_table() {
228
+ let re = regex::Regex::new(r#"EDGE_WEIGHTS\["([a-z0-9_]+)"\]"#).unwrap();
229
+ let mut files = Vec::new();
230
+ collect_rs_files(
231
+ &PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src"),
232
+ &mut files,
233
+ );
234
+ let mut read: BTreeSet<String> = BTreeSet::new();
235
+ for file in files {
236
+ if file.ends_with("weights.rs") {
237
+ continue;
238
+ }
239
+ let text = std::fs::read_to_string(&file).unwrap();
240
+ read.extend(re.captures_iter(&text).map(|c| c[1].to_string()));
241
+ }
242
+ let unread: Vec<&str> = EDGE_WEIGHTS
243
+ .keys()
244
+ .copied()
245
+ .filter(|k| !read.contains(*k))
246
+ .collect::<BTreeSet<_>>()
247
+ .into_iter()
248
+ .collect();
249
+ assert!(
250
+ unread.is_empty(),
251
+ "EDGE_WEIGHTS keys no builder reads: {unread:?}"
252
+ );
253
+ let unknown: Vec<&String> = read
254
+ .iter()
255
+ .filter(|k| !EDGE_WEIGHTS.contains_key(k.as_str()))
256
+ .collect();
257
+ assert!(
258
+ unknown.is_empty(),
259
+ "EDGE_WEIGHTS[...] lookups with no entry (would panic at runtime): {unknown:?}"
260
+ );
261
+ }
262
+ }