diffctx 1.14.0__tar.gz → 1.15.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 (160) hide show
  1. {diffctx-1.14.0 → diffctx-1.15.0}/CHANGELOG.md +44 -1
  2. {diffctx-1.14.0 → diffctx-1.15.0}/Cargo.lock +1 -1
  3. {diffctx-1.14.0 → diffctx-1.15.0}/PKG-INFO +1 -1
  4. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/Cargo.toml +1 -1
  5. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/base.rs +64 -11
  6. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/config_edges/generic.rs +45 -1
  7. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/c_family.rs +5 -4
  8. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/jvm.rs +73 -4
  9. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/rust_lang.rs +59 -12
  10. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/structural/containment.rs +16 -8
  11. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/structural/testing.rs +2 -2
  12. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/locate.rs +19 -3
  13. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/parsers/tree_sitter_strategy.rs +51 -7
  14. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/render.rs +50 -8
  15. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/select.rs +72 -1
  16. {diffctx-1.14.0 → diffctx-1.15.0}/pyproject.toml +1 -1
  17. diffctx-1.15.0/src/diffctx/version.py +1 -0
  18. {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/writer.py +20 -0
  19. diffctx-1.14.0/src/diffctx/version.py +0 -1
  20. {diffctx-1.14.0 → diffctx-1.15.0}/Cargo.toml +0 -0
  21. {diffctx-1.14.0 → diffctx-1.15.0}/LICENSE +0 -0
  22. {diffctx-1.14.0 → diffctx-1.15.0}/README.md +0 -0
  23. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/README.md +0 -0
  24. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/analytics.rs +0 -0
  25. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/candidate_files.rs +0 -0
  26. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/analytics.rs +0 -0
  27. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/bm25.rs +0 -0
  28. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/budget.rs +0 -0
  29. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/category_weights.rs +0 -0
  30. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/edge_weights.rs +0 -0
  31. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/env_overrides.rs +0 -0
  32. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/extensions.rs +0 -0
  33. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/filtering.rs +0 -0
  34. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/fragmentation.rs +0 -0
  35. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/git.rs +0 -0
  36. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/graph_filtering.rs +0 -0
  37. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/importance.rs +0 -0
  38. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/limits.rs +0 -0
  39. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/mod.rs +0 -0
  40. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/mode.rs +0 -0
  41. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/needs.rs +0 -0
  42. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/parsers.rs +0 -0
  43. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/render.rs +0 -0
  44. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/scoring.rs +0 -0
  45. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/selection.rs +0 -0
  46. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/tokenization.rs +0 -0
  47. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/config/weights.rs +0 -0
  48. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/core.rs +0 -0
  49. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/deadline.rs +0 -0
  50. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/discovery.rs +0 -0
  51. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/config_edges/build_system.rs +0 -0
  52. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/config_edges/cicd.rs +0 -0
  53. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/config_edges/docker.rs +0 -0
  54. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/config_edges/helm.rs +0 -0
  55. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/config_edges/kubernetes.rs +0 -0
  56. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/config_edges/mod.rs +0 -0
  57. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/document/mod.rs +0 -0
  58. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/history/cochange.rs +0 -0
  59. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/history/mod.rs +0 -0
  60. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/mod.rs +0 -0
  61. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/ansible.rs +0 -0
  62. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/bazel.rs +0 -0
  63. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/cargo_edges.rs +0 -0
  64. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/clojure.rs +0 -0
  65. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/css.rs +0 -0
  66. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/dart.rs +0 -0
  67. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/dbt.rs +0 -0
  68. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/dotnet.rs +0 -0
  69. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/elixir.rs +0 -0
  70. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/erlang.rs +0 -0
  71. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/go.rs +0 -0
  72. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/graphql.rs +0 -0
  73. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/haskell.rs +0 -0
  74. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/javascript.rs +0 -0
  75. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/julia.rs +0 -0
  76. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/latex.rs +0 -0
  77. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/lua.rs +0 -0
  78. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/mod.rs +0 -0
  79. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/nim.rs +0 -0
  80. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/nix.rs +0 -0
  81. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/ocaml.rs +0 -0
  82. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/openapi.rs +0 -0
  83. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/perl.rs +0 -0
  84. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/php.rs +0 -0
  85. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/prisma.rs +0 -0
  86. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/protobuf.rs +0 -0
  87. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/python.rs +0 -0
  88. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/r_lang.rs +0 -0
  89. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/ruby.rs +0 -0
  90. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/shell.rs +0 -0
  91. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/sql.rs +0 -0
  92. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/swift.rs +0 -0
  93. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/tags.rs +0 -0
  94. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/terraform.rs +0 -0
  95. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/semantic/zig.rs +0 -0
  96. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/similarity/lexical.rs +0 -0
  97. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/similarity/mod.rs +0 -0
  98. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/structural/mod.rs +0 -0
  99. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/edges/structural/sibling.rs +0 -0
  100. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/excerpt.rs +0 -0
  101. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/filtering.rs +0 -0
  102. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/fragmentation.rs +0 -0
  103. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/git.rs +0 -0
  104. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/graph.rs +0 -0
  105. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/graph_export.rs +0 -0
  106. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/interval.rs +0 -0
  107. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/languages.rs +0 -0
  108. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/lib.rs +0 -0
  109. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/main.rs +0 -0
  110. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/memory_pipeline.rs +0 -0
  111. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/mode.rs +0 -0
  112. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/parsers/config_parser.rs +0 -0
  113. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/parsers/generic.rs +0 -0
  114. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/parsers/markdown.rs +0 -0
  115. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/parsers/mod.rs +0 -0
  116. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/paths.rs +0 -0
  117. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/peak_rss.rs +0 -0
  118. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/pipeline.rs +0 -0
  119. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/postpass.rs +0 -0
  120. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/ppr.rs +0 -0
  121. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/project_graph.rs +0 -0
  122. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/provenance.rs +0 -0
  123. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/pybridge.rs +0 -0
  124. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/scoring.rs +0 -0
  125. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/signatures.rs +0 -0
  126. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/stopwords.rs +0 -0
  127. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/test_harness.rs +0 -0
  128. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/testfiles.rs +0 -0
  129. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/token_corpus.rs +0 -0
  130. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/tokenizer.rs +0 -0
  131. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/types.rs +0 -0
  132. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/utility/boltzmann.rs +0 -0
  133. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/utility/importance.rs +0 -0
  134. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/utility/mod.rs +0 -0
  135. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/utility/needs.rs +0 -0
  136. {diffctx-1.14.0 → diffctx-1.15.0}/crates/diffctx-native/src/utility/scoring.rs +0 -0
  137. {diffctx-1.14.0 → diffctx-1.15.0}/rust-toolchain.toml +0 -0
  138. {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/__init__.py +0 -0
  139. {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/__main__.py +0 -0
  140. {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/_app.py +0 -0
  141. {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/_diffctx.pyi +0 -0
  142. {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/_native/__init__.py +0 -0
  143. {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/_native/graph_analytics.py +0 -0
  144. {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/_native/graph_export.py +0 -0
  145. {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/_native/pipeline.py +0 -0
  146. {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/_native/project_graph.py +0 -0
  147. {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/cli.py +0 -0
  148. {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/clipboard.py +0 -0
  149. {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/ignore.py +0 -0
  150. {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/logger.py +0 -0
  151. {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/mcp/README.md +0 -0
  152. {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/mcp/__init__.py +0 -0
  153. {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/mcp/__main__.py +0 -0
  154. {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/mcp/fetch.py +0 -0
  155. {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/mcp/formatting.py +0 -0
  156. {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/mcp/security.py +0 -0
  157. {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/mcp/server.py +0 -0
  158. {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/py.typed +0 -0
  159. {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/tokens.py +0 -0
  160. {diffctx-1.14.0 → diffctx-1.15.0}/src/diffctx/tree.py +0 -0
@@ -7,6 +7,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Added
11
+
12
+ - **Changed files that produced no fragments are disclosed in the md/text
13
+ output** ("Changed files not represented in the output"): a range whose
14
+ selection dropped some changed files no longer hides the gap — the footer
15
+ names exactly the changed files absent from the fragment list. Derived
16
+ from the already policy-clean `changed_files`, so withheld paths can never
17
+ appear in it.
18
+
19
+ ### Fixed
20
+
21
+ - **Path-reference ambiguity cap counted duplicate index keys** (#207): every
22
+ file was indexed under its absolute AND repo-relative path, so
23
+ `MAX_FILES_PER_PATH_REF` (8) effectively halved, and host directory
24
+ components (`/home/runner/work/...`) matched references they had nothing
25
+ to do with. The index now carries one repo-relative posix entry per file.
26
+ - **`<script>` type detection** (#213): `data-type="json"` no longer opts a
27
+ real JavaScript block out of parsing, and a legal MIME parameter
28
+ (`type="text/javascript; charset=utf-8"`) no longer degrades the block to
29
+ raw line windows.
30
+ - **Scala import/package extraction gaps** (#215): Scala 3 renames
31
+ (`import a.b.C as D`, `a.{B as C}`) keep the original name, multi-line
32
+ brace imports keep their selectors, and nested package blocks no longer
33
+ concatenate sibling scopes into a nonexistent package.
34
+ - **Rust `pub use` re-export registration no longer depends on file
35
+ iteration order** (#199): registration now runs after the definition maps
36
+ are complete, so a re-export defined in a later-iterated file still
37
+ resolves (`rust_027_arc_mutex` left the corpus baseline on this fix).
38
+ - **The per-file budget ceiling now also gates signature substitution**
39
+ (#212): a file at its ceiling can no longer keep placing signature stubs
40
+ ahead of other files' cores.
41
+ - **A signature stub substituted for a changed core renders as `changed`**
42
+ (#209): the stub shares the core's location, and both surfaces (pack render
43
+ and locate) now agree it carries the change, same as an excerpt. Ordinary
44
+ context signatures stay context.
45
+ - **Config-key word boundaries treat non-ASCII bytes as word bytes** (#216):
46
+ a config key ending exactly where a non-ASCII letter begins (e.g. inside
47
+ `tokenización`) is no longer a match, restoring the Unicode-`\b` rejection
48
+ the Aho-Corasick rewrite lost.
49
+
50
+ ## [1.14.0] - 2026-08-16
51
+
10
52
  ### Fixed
11
53
 
12
54
  - **Changed secret-classified files (`id_rsa`, `*.key`, `*.pem`, …) are now
@@ -991,7 +1033,8 @@ Earlier releases shipped as `treemapper`; see
991
1033
  <https://github.com/nikolay-e/diffctx/releases> for the corresponding GitHub
992
1034
  release notes (`1.0.0` through `1.6.1`).
993
1035
 
994
- [Unreleased]: https://github.com/nikolay-e/diffctx/compare/v1.13.0...HEAD
1036
+ [Unreleased]: https://github.com/nikolay-e/diffctx/compare/v1.14.0...HEAD
1037
+ [1.14.0]: https://github.com/nikolay-e/diffctx/compare/v1.13.0...v1.14.0
995
1038
  [1.13.0]: https://github.com/nikolay-e/diffctx/compare/v1.12.3...v1.13.0
996
1039
  [1.12.3]: https://github.com/nikolay-e/diffctx/compare/v1.12.2...v1.12.3
997
1040
  [1.12.2]: https://github.com/nikolay-e/diffctx/compare/v1.12.1...v1.12.2
@@ -194,7 +194,7 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
194
194
 
195
195
  [[package]]
196
196
  name = "diffctx"
197
- version = "1.14.0"
197
+ version = "1.15.0"
198
198
  dependencies = [
199
199
  "aho-corasick",
200
200
  "anyhow",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diffctx
3
- Version: 1.14.0
3
+ Version: 1.15.0
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "diffctx"
3
- version = "1.14.0"
3
+ version = "1.15.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"
@@ -97,8 +97,8 @@ pub struct FragmentIndex {
97
97
  /// interior) requires each reference component to appear as a whole path
98
98
  /// component, so the last component is a complete candidate filter. Each
99
99
  /// entry carries the file's representative fragment: a path reference is a
100
- /// file-level relation, and the containment star spreads its mass inside
101
- /// the file.
100
+ /// file-level relation and endorses the file, not each sibling (see
101
+ /// `file_representatives` on the accepted reachability trade, #208).
102
102
  lower_paths: Vec<(String, FragmentId)>,
103
103
  component_to_paths: FxHashMap<String, Vec<u32>>,
104
104
  }
@@ -136,18 +136,29 @@ impl FragmentIndex {
136
136
  }
137
137
  }
138
138
 
139
+ // One entry per FILE, keyed on the repo-relative posix path (the form
140
+ // path references are written in). Indexing every `by_path` variant put
141
+ // the same file under 2-3 keys, so `MAX_FILES_PER_PATH_REF` counted
142
+ // duplicate keys and silently halved (#207) — and the absolute keys'
143
+ // host directory components (`/home/runner/work/...`) matched
144
+ // references they had nothing to do with.
139
145
  let reps = file_representatives(fragments);
140
- let mut lower_paths: Vec<(String, FragmentId)> = Vec::with_capacity(by_path.len());
146
+ let mut lower_paths: Vec<(String, FragmentId)> = Vec::with_capacity(reps.len());
141
147
  let mut component_to_paths: FxHashMap<String, Vec<u32>> = FxHashMap::default();
142
- for (path_str, ids) in &by_path {
143
- // Every id under one path key belongs to one file; its
144
- // representative is looked up by the id's own (canonical) path,
145
- // which also covers the relative-variant keys.
146
- let Some(rep) = ids.first().and_then(|id| reps.get(id.path.as_ref())) else {
148
+ let mut indexed_files: FxHashSet<&str> = FxHashSet::default();
149
+ for f in fragments {
150
+ if !indexed_files.insert(f.path()) {
151
+ continue;
152
+ }
153
+ let Some(rep) = reps.get(f.path()) else {
147
154
  continue;
148
155
  };
156
+ let matchable = repo_root
157
+ .and_then(|root| Path::new(f.path()).strip_prefix(root).ok())
158
+ .map(|rel| rel.to_string_lossy().replace('\\', "/"))
159
+ .unwrap_or_else(|| f.path().replace('\\', "/"));
149
160
  let idx = lower_paths.len() as u32;
150
- let lower = path_str.to_lowercase();
161
+ let lower = matchable.to_lowercase();
151
162
  for comp in lower.split('/').filter(|c| !c.is_empty()) {
152
163
  let posting = component_to_paths.entry(comp.to_string()).or_default();
153
164
  if posting.last() != Some(&idx) {
@@ -174,8 +185,11 @@ impl FragmentIndex {
174
185
  /// change endorsing a file diffusely must not outweigh a call edge naming one
175
186
  /// symbol — the fragment-pair encoding gave both the same weight, which is
176
187
  /// simultaneously the quadratic edge blow-up (envoy: 22M c-family edges) and
177
- /// the file-level noise mechanism of #65. Reachability of the file's other
178
- /// fragments is the containment star's job.
188
+ /// the file-level noise mechanism of #65. The file's other fragments are
189
+ /// reachable only through nesting and naming edges — the intra-file star is
190
+ /// OFF by default and measured net-negative on the corpus (#208) — an
191
+ /// accepted trade: a file-level relation endorses the file, not each of its
192
+ /// siblings.
179
193
  pub fn file_representatives(fragments: &[Fragment]) -> FxHashMap<String, FragmentId> {
180
194
  let mut file_to_rep: FxHashMap<String, FragmentId> = FxHashMap::default();
181
195
  let mut file_to_token_count: FxHashMap<String, u32> = FxHashMap::default();
@@ -498,4 +512,43 @@ mod tests {
498
512
  let hit = linked_paths("pkg/api", &paths);
499
513
  assert_eq!(hit, vec!["pkg/api/server.go".to_string()]);
500
514
  }
515
+
516
+ fn linked_paths_rooted(reference: &str, root: &str, paths: &[&str]) -> Vec<String> {
517
+ let frags: Vec<Fragment> = paths.iter().map(|p| frag(p)).collect();
518
+ let idx = FragmentIndex::new(&frags, Some(Path::new(root)));
519
+ let src = frag("/ci/work/repo/src/origin.yml");
520
+ let mut edges: EdgeDict = FxHashMap::default();
521
+ link_by_path_match(&src.id, reference, &idx, &mut edges, 0.5, 0.5);
522
+ let mut out: Vec<String> = edges
523
+ .keys()
524
+ .map(|(_, dst)| dst.path.to_string())
525
+ .filter(|p| p != src.id.path.as_ref())
526
+ .collect();
527
+ out.sort();
528
+ out.dedup();
529
+ out
530
+ }
531
+
532
+ #[test]
533
+ fn the_ambiguity_cap_counts_distinct_files_not_index_keys() {
534
+ // 5 distinct files under a rooted index: the pre-#207 code indexed
535
+ // each under its absolute AND relative key, so matched.len() was 10
536
+ // and the cap (8) dropped a reference naming only 5 files.
537
+ let paths: Vec<String> = (0..5)
538
+ .map(|i| format!("/ci/work/repo/lib/util/mod_{i}.py"))
539
+ .collect();
540
+ let refs: Vec<&str> = paths.iter().map(String::as_str).collect();
541
+ let hit = linked_paths_rooted("lib/util", "/ci/work/repo", &refs);
542
+ assert_eq!(hit.len(), 5, "cap must count files, not duplicate keys");
543
+ }
544
+
545
+ #[test]
546
+ fn host_directory_components_are_not_matchable() {
547
+ let paths = ["/ci/work/repo/src/app.rs", "/ci/work/repo/src/db.rs"];
548
+ let hit = linked_paths_rooted("work", "/ci/work/repo", &paths);
549
+ assert!(
550
+ hit.is_empty(),
551
+ "components outside the repo root must not resolve a reference"
552
+ );
553
+ }
501
554
  }
@@ -170,7 +170,13 @@ fn extract_config_keys(suffix: &str, content: &str) -> FxHashSet<String> {
170
170
  }
171
171
 
172
172
  fn is_word_byte(b: u8) -> bool {
173
- b.is_ascii_alphanumeric() || b == b'_'
173
+ // Non-ASCII bytes count as word bytes: the regex `\b` this check replaced
174
+ // treats a Unicode letter as a word character, so `token` inside
175
+ // `tokenización` was NOT a match (#216). Treating every non-ASCII byte as
176
+ // a word byte is the conservative direction — it can only reject matches
177
+ // adjacent to non-ASCII punctuation that `\b` would have accepted, never
178
+ // admit ones it rejected.
179
+ b.is_ascii_alphanumeric() || b == b'_' || !b.is_ascii()
174
180
  }
175
181
 
176
182
  fn build_key_patterns(keys: &FxHashSet<String>) -> Vec<Regex> {
@@ -403,3 +409,41 @@ impl EdgeBuilder for ConfigToCodeEdgeBuilder {
403
409
  discovered
404
410
  }
405
411
  }
412
+
413
+ #[cfg(test)]
414
+ mod tests {
415
+ use super::*;
416
+ use crate::types::{FragmentId, FragmentKind};
417
+ use std::sync::Arc;
418
+
419
+ fn frag(path: &str, content: &str) -> Fragment {
420
+ Fragment {
421
+ id: FragmentId::new(Arc::from(path), 1, 5),
422
+ kind: FragmentKind::Chunk,
423
+ content: Arc::from(content),
424
+ identifiers: FxHashSet::default(),
425
+ token_count: 10,
426
+ symbol_name: None,
427
+ }
428
+ }
429
+
430
+ /// #216: the manual boundary check replaced the regex crate's Unicode
431
+ /// `\b`, which treats a non-ASCII letter as a word character. A key ending
432
+ /// exactly where a non-ASCII letter begins is NOT a whole-word match.
433
+ #[test]
434
+ fn a_key_inside_a_non_ascii_word_is_not_a_match() {
435
+ let cfg = frag("app.yaml", "sesion_timeout: 30\n");
436
+ let hit = frag("b.py", "sesion = abrir()\n");
437
+ let miss = frag("a.py", "valor = sesion\u{00e9}s\n");
438
+ let frags = vec![cfg.clone(), hit.clone(), miss.clone()];
439
+ let edges = ConfigToCodeEdgeBuilder.build(&frags, None);
440
+ assert!(
441
+ edges.contains_key(&(cfg.id.clone(), hit.id.clone())),
442
+ "a standalone key occurrence must still link"
443
+ );
444
+ assert!(
445
+ !edges.contains_key(&(cfg.id.clone(), miss.id.clone())),
446
+ "`sesion` inside `sesion\u{00e9}s` is not a word match"
447
+ );
448
+ }
449
+ }
@@ -172,8 +172,8 @@ impl EdgeBuilder for CFamilyEdgeBuilder {
172
172
 
173
173
  // An include names a file, so its edge lands on the file's
174
174
  // representative fragment (base::file_representatives — the sibling
175
- // builder's long-standing semantics); the containment star carries the
176
- // mass to the rest of the file. Buckets therefore hold file paths, and
175
+ // builder's long-standing semantics; see its doc on the accepted
176
+ // reachability trade, #208). Buckets therefore hold file paths, and
177
177
  // ambiguity is measured in files: a basename repeated across the tree
178
178
  // is what makes an include unresolvable.
179
179
  // Representatives keyed off the full slice: is_c_family is per-path, so
@@ -362,8 +362,9 @@ impl EdgeBuilder for CFamilyEdgeBuilder {
362
362
  }
363
363
 
364
364
  // A header/impl pair is a relation between two files; representatives
365
- // carry it, the containment star spreads it. Pairing every fragment
366
- // with every fragment restated the same fact quadratically.
365
+ // carry it (see base::file_representatives on the accepted
366
+ // reachability trade, #208). Pairing every fragment with every
367
+ // fragment restated the same fact quadratically.
367
368
  for (i, (_key, (files, _))) in by_stem.iter().enumerate() {
368
369
  crate::deadline::check_current_every(i, 256, "edge construction (c_family pairing)");
369
370
  if files.len() < 2 {
@@ -187,10 +187,15 @@ fn parse_scala_import_clause(clause: &str) -> Option<ScalaImport> {
187
187
  let mut selectors = Vec::new();
188
188
  let mut wildcard = false;
189
189
  for sel in inner.split(',') {
190
+ // A rename keeps the ORIGINAL name — that is what the target file
191
+ // defines. Scala 2 spells it `C => D`, Scala 3 `C as D`.
190
192
  let name = sel
191
193
  .split("=>")
192
194
  .next()
193
195
  .unwrap_or("")
196
+ .split(" as ")
197
+ .next()
198
+ .unwrap_or("")
194
199
  .trim()
195
200
  .trim_matches('`');
196
201
  match name {
@@ -211,6 +216,8 @@ fn parse_scala_import_clause(clause: &str) -> Option<ScalaImport> {
211
216
  wildcard,
212
217
  });
213
218
  }
219
+ // Scala 3 top-level rename: `import a.b.c as d` imports `a.b.c`.
220
+ let clause = clause.split(" as ").next().unwrap_or(clause).trim();
214
221
  if let Some(p) = clause
215
222
  .strip_suffix("._")
216
223
  .or_else(|| clause.strip_suffix(".*"))
@@ -248,9 +255,30 @@ fn parse_scala_import_clause(clause: &str) -> Option<ScalaImport> {
248
255
 
249
256
  fn parse_scala_imports(content: &str) -> Vec<ScalaImport> {
250
257
  let mut out = Vec::new();
251
- for cap in SCALA_IMPORT_LINE_RE.captures_iter(content) {
252
- let rest = cap[1].split("//").next().unwrap_or("").trim();
253
- for clause in split_top_level_commas(rest) {
258
+ let mut lines = content.lines();
259
+ while let Some(line) = lines.next() {
260
+ let Some(cap) = SCALA_IMPORT_LINE_RE.captures(line) else {
261
+ continue;
262
+ };
263
+ let mut clause_text = cap[1].split("//").next().unwrap_or("").trim().to_string();
264
+ // A brace group may span lines; join until the braces balance so
265
+ // `import scala.collection.{\n mutable,\n immutable\n}` keeps its
266
+ // selectors instead of yielding an empty list.
267
+ let mut open = clause_text.matches('{').count();
268
+ let mut close = clause_text.matches('}').count();
269
+ let mut joined = 0;
270
+ // The join is bounded so an unbalanced brace (a file mid-edit) cannot
271
+ // swallow the rest of the file into one clause.
272
+ while open > close && joined < 32 {
273
+ joined += 1;
274
+ let Some(next) = lines.next() else { break };
275
+ let next = next.split("//").next().unwrap_or("").trim();
276
+ open += next.matches('{').count();
277
+ close += next.matches('}').count();
278
+ clause_text.push(' ');
279
+ clause_text.push_str(next);
280
+ }
281
+ for clause in split_top_level_commas(&clause_text) {
254
282
  if let Some(imp) = parse_scala_import_clause(clause) {
255
283
  out.push(imp);
256
284
  }
@@ -315,7 +343,10 @@ fn extract_package(content: &str, path: &Path) -> Option<String> {
315
343
  if is_scala(path) {
316
344
  // Scala chains package clauses (`package com.acme` then `package svc`
317
345
  // means com.acme.svc); `package object x` is a member of the enclosing
318
- // package, not a chain segment.
346
+ // package, not a chain segment. A block-form clause (`package util {`)
347
+ // opens a scope whose SIBLINGS are not chain segments — concatenating
348
+ // past it produced nonexistent packages (`com.example.util.data`), so
349
+ // the chain stops at the first block.
319
350
  let mut parts: Vec<String> = Vec::new();
320
351
  for cap in SCALA_PACKAGE_RE.captures_iter(content) {
321
352
  let seg = &cap[1];
@@ -323,6 +354,10 @@ fn extract_package(content: &str, path: &Path) -> Option<String> {
323
354
  continue;
324
355
  }
325
356
  parts.push(seg.to_string());
357
+ let after = content[cap.get(0).map_or(0, |m| m.end())..].trim_start();
358
+ if after.starts_with('{') {
359
+ break;
360
+ }
326
361
  }
327
362
  if parts.is_empty() {
328
363
  None
@@ -875,6 +910,40 @@ mod tests {
875
910
  assert!(object_rooted.wildcard);
876
911
  }
877
912
 
913
+ #[test]
914
+ fn scala3_renames_keep_the_original_name() {
915
+ let top = parse_one("a.b.Conf as Config");
916
+ assert_eq!(top.prefix, "a.b");
917
+ assert_eq!(top.selectors, vec!["Conf".to_string()]);
918
+
919
+ let braced = parse_one("a.{B as C, D}");
920
+ assert_eq!(braced.prefix, "a");
921
+ assert_eq!(braced.selectors, vec!["B".to_string(), "D".to_string()]);
922
+ }
923
+
924
+ #[test]
925
+ fn scala_multiline_brace_import_keeps_its_selectors() {
926
+ let content = "import scala.collection.{\n mutable,\n immutable\n}\nimport a.B\n";
927
+ let imports = parse_scala_imports(content);
928
+ assert_eq!(imports.len(), 2);
929
+ assert_eq!(imports[0].prefix, "scala.collection");
930
+ assert_eq!(
931
+ imports[0].selectors,
932
+ vec!["mutable".to_string(), "immutable".to_string()]
933
+ );
934
+ assert_eq!(imports[1].selectors, vec!["B".to_string()]);
935
+ }
936
+
937
+ #[test]
938
+ fn scala_nested_package_blocks_do_not_concatenate_siblings() {
939
+ let content =
940
+ "package com.example\npackage util {\n class A\n}\npackage data {\n class B\n}\n";
941
+ assert_eq!(
942
+ extract_package(content, Path::new("a.scala")),
943
+ Some("com.example.util".to_string())
944
+ );
945
+ }
946
+
878
947
  #[test]
879
948
  fn scala_chained_packages_join_and_package_object_is_skipped() {
880
949
  let content = "package com.acme\npackage service\n\nclass X {}\n";
@@ -259,25 +259,34 @@ impl EdgeBuilder for RustEdgeBuilder {
259
259
  if !impls.is_empty() {
260
260
  trait_impls.insert(f.id.clone(), impls);
261
261
  }
262
+ }
262
263
 
264
+ // Re-export registration runs over the COMPLETE definition maps:
265
+ // inline in the loop above it saw only files iterated before the
266
+ // re-exporting one, so which `pub use` leaves resolved depended on
267
+ // file iteration order (#199). Registrations are collected before
268
+ // insertion so re-exporters do not shadow each other either.
269
+ let mut pub_use_regs: Vec<(String, FragmentId)> = Vec::new();
270
+ for f in &rust_frags {
263
271
  for pub_use_path in extract_pub_uses(&f.content) {
264
272
  let leaf_lower = pub_use_path.split("::").last().unwrap_or("").to_lowercase();
265
- if !leaf_lower.is_empty() && !name_to_frags.contains_key(&leaf_lower) {
266
- let has_target = type_defs
273
+ if leaf_lower.is_empty() || name_to_frags.contains_key(&leaf_lower) {
274
+ continue;
275
+ }
276
+ let has_target = type_defs
277
+ .get(&leaf_lower)
278
+ .map_or(false, |v| v.iter().any(|fid| fid != &f.id))
279
+ || fn_defs
267
280
  .get(&leaf_lower)
268
- .map_or(false, |v| v.iter().any(|fid| fid != &f.id))
269
- || fn_defs
270
- .get(&leaf_lower)
271
- .map_or(false, |v| v.iter().any(|fid| fid != &f.id));
272
- if has_target {
273
- name_to_frags
274
- .entry(leaf_lower)
275
- .or_default()
276
- .push(f.id.clone());
277
- }
281
+ .map_or(false, |v| v.iter().any(|fid| fid != &f.id));
282
+ if has_target {
283
+ pub_use_regs.push((leaf_lower, f.id.clone()));
278
284
  }
279
285
  }
280
286
  }
287
+ for (leaf_lower, fid) in pub_use_regs {
288
+ name_to_frags.entry(leaf_lower).or_default().push(fid);
289
+ }
281
290
 
282
291
  let mut edges: EdgeDict = FxHashMap::default();
283
292
  // Same ambiguity bar as CFamilySemanticWeights::max_files_per_name: a
@@ -520,3 +529,41 @@ impl EdgeBuilder for RustEdgeBuilder {
520
529
  result
521
530
  }
522
531
  }
532
+
533
+ #[cfg(test)]
534
+ mod tests {
535
+ use super::*;
536
+ use crate::types::FragmentKind;
537
+
538
+ fn frag(path: &str, content: &str) -> Fragment {
539
+ Fragment {
540
+ id: FragmentId::new(std::sync::Arc::from(path), 1, 20),
541
+ kind: FragmentKind::Function,
542
+ content: std::sync::Arc::from(content),
543
+ identifiers: FxHashSet::default(),
544
+ token_count: 20,
545
+ symbol_name: None,
546
+ }
547
+ }
548
+
549
+ #[test]
550
+ fn pub_use_registration_does_not_depend_on_file_iteration_order() {
551
+ // The re-exporting file iterates FIRST, the defining file after it —
552
+ // exactly the order that dropped the registration before #199.
553
+ let facade = frag("src/facade.rs", "pub use detail::WidgetFactory;\n");
554
+ let detail = frag(
555
+ "src/z_detail.rs",
556
+ "pub struct WidgetFactory {\n size: u32,\n}\n",
557
+ );
558
+ let consumer = frag(
559
+ "src/consumer.rs",
560
+ "use crate::api::WidgetFactory;\n\nfn run() {}\n",
561
+ );
562
+ let frags = vec![facade.clone(), detail, consumer.clone()];
563
+ let edges = RustEdgeBuilder.build(&frags, None);
564
+ assert!(
565
+ edges.contains_key(&(consumer.id.clone(), facade.id.clone())),
566
+ "a use of the re-exported leaf must resolve to the re-exporting file"
567
+ );
568
+ }
569
+ }
@@ -22,7 +22,15 @@ impl EdgeBuilder for ContainmentEdgeBuilder {
22
22
 
23
23
  let mut edges: EdgeDict = FxHashMap::default();
24
24
 
25
- let reps = super::super::base::file_representatives(fragments);
25
+ // Computed only for the experimental star below — OFF by default, and
26
+ // measured net-negative when ON (#208): 21 corpus regressions vs 14
27
+ // improvements, 2026-08-16 A/B at shipped defaults.
28
+ let star_on = std::env::var_os("DIFFCTX_FILE_STAR").is_some();
29
+ let reps = if star_on {
30
+ super::super::base::file_representatives(fragments)
31
+ } else {
32
+ FxHashMap::default()
33
+ };
26
34
 
27
35
  for (path, frags) in &by_path {
28
36
  if frags.len() < 2 {
@@ -58,13 +66,13 @@ impl EdgeBuilder for ContainmentEdgeBuilder {
58
66
  stack.push(f);
59
67
  }
60
68
 
61
- // The nesting pass only connects a fragment to what encloses it,
62
- // so a file's top-level fragments were mutually unreachable. The
63
- // star through the file representative gives every file an
64
- // intra-file spinelinear in fragment count which is what
65
- // lets file-level relations (includes, path references, pairing)
66
- // link representatives instead of full fragment cross products.
67
- if std::env::var_os("DIFFCTX_FILE_STAR").is_some() {
69
+ // Experimental intra-file spine: connects every fragment to the
70
+ // file representative so top-level siblings become mutually
71
+ // reachable. Ships OFF — the 2026-08-16 corpus A/B measured it
72
+ // net-negative (#208)so by default a flat file's siblings are
73
+ // reachable only through nesting and naming edges, and the
74
+ // representative-only builders accept that.
75
+ if star_on {
68
76
  if let Some(rep) = reps.get(*path) {
69
77
  for f in &sorted {
70
78
  if f.id != *rep {
@@ -120,8 +120,8 @@ impl EdgeBuilder for TestEdgeBuilder {
120
120
  let mut edges: EdgeDict = FxHashMap::default();
121
121
 
122
122
  // The naming convention names a FILE, so the relation is carried by
123
- // the two files' representatives (base::file_representatives) and the
124
- // containment star spreads it within each file. One pair per
123
+ // the two files' representatives (base::file_representatives; see its
124
+ // doc on the accepted reachability trade, #208). One pair per
125
125
  // (test file, source file), not per fragment pair.
126
126
  let mut linked_file_pairs: FxHashSet<(&str, &str)> = FxHashSet::default();
127
127
 
@@ -226,13 +226,28 @@ fn rel_path(state: &ScoredState, path: &str) -> String {
226
226
  .unwrap_or_else(|| crate::paths::to_posix_display(std::borrow::Cow::Borrowed(path)))
227
227
  }
228
228
 
229
+ /// Same predicate on both surfaces (pack render agrees via
230
+ /// `render::carries_changed_role`): the fragment is a core, an excerpt
231
+ /// stand-in, or a signature stub substituted at a core's location (#209).
232
+ fn carries_change(
233
+ state: &ScoredState,
234
+ frag: &Fragment,
235
+ core_locs: &rustc_hash::FxHashSet<(std::sync::Arc<str>, u32)>,
236
+ ) -> bool {
237
+ state.core_ids.contains(&frag.id)
238
+ || frag.kind == crate::types::FragmentKind::Excerpt
239
+ || (frag.kind.is_signature()
240
+ && core_locs.contains(&(frag.id.path.clone(), frag.id.start_line)))
241
+ }
242
+
229
243
  fn reasons_for(
230
244
  state: &ScoredState,
231
245
  frag: &Fragment,
246
+ core_locs: &rustc_hash::FxHashSet<(std::sync::Arc<str>, u32)>,
232
247
  hops: Option<u32>,
233
248
  attribution: Option<&Vec<(String, String, f64)>>,
234
249
  ) -> Vec<Reason> {
235
- if state.core_ids.contains(&frag.id) || frag.kind == crate::types::FragmentKind::Excerpt {
250
+ if carries_change(state, frag, core_locs) {
236
251
  return vec![Reason::Changed];
237
252
  }
238
253
  let mut reasons: Vec<Reason> = Vec::new();
@@ -456,12 +471,12 @@ pub fn build_locate(state: &ScoredState, outcome: &SelectionOutcome) -> LocateOu
456
471
  let attribution = incoming_attribution(state);
457
472
  let (overflow, overflow_count, next_up) = build_overflow(state, outcome, &hops, &attribution);
458
473
 
474
+ let core_locs = crate::render::core_substitute_locs(&state.core_ids);
459
475
  let items: Vec<LocateItem> = outcome
460
476
  .selected
461
477
  .iter()
462
478
  .map(|frag| {
463
- let is_changed = state.core_ids.contains(&frag.id)
464
- || frag.kind == crate::types::FragmentKind::Excerpt;
479
+ let is_changed = carries_change(state, frag, &core_locs);
465
480
  let path = rel_path(state, frag.id.path.as_ref());
466
481
  let group = group_of(&path, frag.kind);
467
482
  LocateItem {
@@ -479,6 +494,7 @@ pub fn build_locate(state: &ScoredState, outcome: &SelectionOutcome) -> LocateOu
479
494
  reasons: reasons_for(
480
495
  state,
481
496
  frag,
497
+ &core_locs,
482
498
  hops.get(&frag.id).copied(),
483
499
  attribution.get(&frag.id),
484
500
  ),