diffctx 1.8.1__tar.gz → 1.10.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 (163) hide show
  1. diffctx-1.10.0/CHANGELOG.md +200 -0
  2. {diffctx-1.8.1 → diffctx-1.10.0}/PKG-INFO +7 -4
  3. {diffctx-1.8.1 → diffctx-1.10.0}/README.md +6 -3
  4. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/Cargo.lock +1 -1
  5. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/Cargo.toml +1 -1
  6. diffctx-1.10.0/diffctx/src/config/budget.rs +30 -0
  7. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/limits.rs +8 -0
  8. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/python.rs +7 -5
  9. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/structural/testing.rs +1 -1
  10. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/filtering.rs +4 -4
  11. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/fragmentation.rs +20 -5
  12. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/git.rs +40 -2
  13. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/graph.rs +5 -1
  14. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/interval.rs +6 -4
  15. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/memory_pipeline.rs +16 -1
  16. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/parsers/tree_sitter_strategy.rs +19 -1
  17. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/pipeline.rs +92 -5
  18. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/postpass.rs +7 -2
  19. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/project_graph.rs +2 -0
  20. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/pybridge.rs +23 -0
  21. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/render.rs +106 -11
  22. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/select.rs +4 -1
  23. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/signatures.rs +5 -1
  24. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/tokenizer.rs +1 -0
  25. {diffctx-1.8.1 → diffctx-1.10.0}/pyproject.toml +3 -3
  26. {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/cli.py +3 -3
  27. {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/diffctx/pipeline.py +2 -2
  28. {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/ignore.py +14 -0
  29. diffctx-1.10.0/src/diffctx/mcp/README.md +142 -0
  30. {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/mcp/server.py +15 -8
  31. diffctx-1.10.0/src/diffctx/version.py +1 -0
  32. {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/writer.py +23 -0
  33. diffctx-1.8.1/CHANGELOG.md +0 -97
  34. diffctx-1.8.1/diffctx/src/config/budget.rs +0 -21
  35. diffctx-1.8.1/src/diffctx/mcp/README.md +0 -104
  36. diffctx-1.8.1/src/diffctx/version.py +0 -1
  37. {diffctx-1.8.1 → diffctx-1.10.0}/LICENSE +0 -0
  38. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/analytics.rs +0 -0
  39. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/candidate_files.rs +0 -0
  40. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/analytics.rs +0 -0
  41. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/bm25.rs +0 -0
  42. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/category_weights.rs +0 -0
  43. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/edge_weights.rs +0 -0
  44. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/env_overrides.rs +0 -0
  45. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/extensions.rs +0 -0
  46. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/filtering.rs +0 -0
  47. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/fragmentation.rs +0 -0
  48. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/git.rs +0 -0
  49. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/graph_filtering.rs +0 -0
  50. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/importance.rs +0 -0
  51. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/mod.rs +0 -0
  52. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/mode.rs +0 -0
  53. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/needs.rs +0 -0
  54. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/parsers.rs +0 -0
  55. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/render.rs +0 -0
  56. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/scoring.rs +0 -0
  57. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/selection.rs +0 -0
  58. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/tokenization.rs +0 -0
  59. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/weights.rs +0 -0
  60. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/core.rs +0 -0
  61. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/discovery.rs +0 -0
  62. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/base.rs +0 -0
  63. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/config_edges/build_system.rs +0 -0
  64. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/config_edges/cicd.rs +0 -0
  65. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/config_edges/docker.rs +0 -0
  66. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/config_edges/generic.rs +0 -0
  67. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/config_edges/helm.rs +0 -0
  68. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/config_edges/kubernetes.rs +0 -0
  69. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/config_edges/mod.rs +0 -0
  70. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/document/mod.rs +0 -0
  71. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/history/cochange.rs +0 -0
  72. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/history/mod.rs +0 -0
  73. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/mod.rs +0 -0
  74. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/ansible.rs +0 -0
  75. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/bazel.rs +0 -0
  76. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/c_family.rs +0 -0
  77. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/cargo_edges.rs +0 -0
  78. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/clojure.rs +0 -0
  79. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/css.rs +0 -0
  80. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/dart.rs +0 -0
  81. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/dbt.rs +0 -0
  82. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/dotnet.rs +0 -0
  83. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/elixir.rs +0 -0
  84. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/erlang.rs +0 -0
  85. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/go.rs +0 -0
  86. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/graphql.rs +0 -0
  87. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/haskell.rs +0 -0
  88. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/javascript.rs +0 -0
  89. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/julia.rs +0 -0
  90. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/jvm.rs +0 -0
  91. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/latex.rs +0 -0
  92. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/lua.rs +0 -0
  93. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/mod.rs +0 -0
  94. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/nim.rs +0 -0
  95. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/nix.rs +0 -0
  96. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/ocaml.rs +0 -0
  97. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/openapi.rs +0 -0
  98. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/perl.rs +0 -0
  99. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/php.rs +0 -0
  100. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/prisma.rs +0 -0
  101. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/protobuf.rs +0 -0
  102. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/r_lang.rs +0 -0
  103. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/ruby.rs +0 -0
  104. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/rust_lang.rs +0 -0
  105. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/shell.rs +0 -0
  106. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/sql.rs +0 -0
  107. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/swift.rs +0 -0
  108. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/tags.rs +0 -0
  109. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/terraform.rs +0 -0
  110. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/zig.rs +0 -0
  111. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/similarity/lexical.rs +0 -0
  112. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/similarity/mod.rs +0 -0
  113. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/structural/containment.rs +0 -0
  114. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/structural/mod.rs +0 -0
  115. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/structural/sibling.rs +0 -0
  116. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/graph_export.rs +0 -0
  117. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/languages.rs +0 -0
  118. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/lib.rs +0 -0
  119. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/main.rs +0 -0
  120. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/mode.rs +0 -0
  121. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/parsers/config_parser.rs +0 -0
  122. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/parsers/generic.rs +0 -0
  123. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/parsers/markdown.rs +0 -0
  124. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/parsers/mod.rs +0 -0
  125. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/ppr.rs +0 -0
  126. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/scoring.rs +0 -0
  127. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/stopwords.rs +0 -0
  128. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/test_harness.rs +0 -0
  129. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/types.rs +0 -0
  130. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/utility/boltzmann.rs +0 -0
  131. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/utility/importance.rs +0 -0
  132. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/utility/mod.rs +0 -0
  133. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/utility/needs.rs +0 -0
  134. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/utility/scoring.rs +0 -0
  135. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/tests/common/mod.rs +0 -0
  136. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/tests/fixtures/garbage/garbage_api.py +0 -0
  137. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/tests/fixtures/garbage/garbage_constants.py +0 -0
  138. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/tests/fixtures/garbage/garbage_handlers.py +0 -0
  139. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/tests/fixtures/garbage/garbage_models.py +0 -0
  140. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/tests/fixtures/garbage/garbage_module.js +0 -0
  141. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/tests/fixtures/garbage/garbage_services.py +0 -0
  142. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/tests/fixtures/garbage/garbage_types.py +0 -0
  143. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/tests/fixtures/garbage/garbage_unrelated.yaml +0 -0
  144. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/tests/fixtures/garbage/garbage_utils.py +0 -0
  145. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/tests/fixtures/garbage/garbage_validators.py +0 -0
  146. {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/tests/yaml_cases.rs +0 -0
  147. {diffctx-1.8.1 → diffctx-1.10.0}/rust-toolchain.toml +0 -0
  148. {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/__init__.py +0 -0
  149. {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/__main__.py +0 -0
  150. {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/clipboard.py +0 -0
  151. {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/diffctx/__init__.py +0 -0
  152. {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/diffctx/graph_analytics.py +0 -0
  153. {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/diffctx/graph_export.py +0 -0
  154. {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/diffctx/project_graph.py +0 -0
  155. {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/logger.py +0 -0
  156. {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/main.py +0 -0
  157. {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/mcp/__init__.py +0 -0
  158. {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/mcp/__main__.py +0 -0
  159. {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/mcp/formatting.py +0 -0
  160. {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/mcp/security.py +0 -0
  161. {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/py.typed +0 -0
  162. {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/tokens.py +0 -0
  163. {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/tree.py +0 -0
@@ -0,0 +1,200 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [1.10.0] - 2026-06-20
11
+
12
+ ### Changed
13
+
14
+ - The default `--tau` (stopping threshold) is now **0.12** across the CLI, MCP,
15
+ and Python API — the calibrated grid optimum — bringing the Python side in
16
+ line with the standalone Rust binary (was 0.08). Diff-context selections are
17
+ slightly tighter by default.
18
+ - The MCP `get_tree_map` / `get_file_context` default `max_file_bytes` is now
19
+ 256 KB, matching the documented CLI default (was 100 KB).
20
+
21
+ ### Fixed
22
+
23
+ - Document/citation edges can no longer create self-edges (a fragment linking to
24
+ itself); `Graph::add_edge` now rejects `src == dst`.
25
+ - Test→source import edges no longer match `import` statements that appear inside
26
+ comments or string literals (the import regex is now line-anchored).
27
+ - The MCP `get_file_context` clipboard confirmation now reports the number of
28
+ files actually copied, excluding files skipped for exceeding the size cap.
29
+
30
+ ### Docs
31
+
32
+ - Corrected the AST-parsing language count (was "12 languages", now "30+"),
33
+ aligned the Rust crate version with the wheel, completed the MCP server README
34
+ (all three tools documented), and fixed the documented `EGO.per_hop_decay`
35
+ default (0.5). Documented the `scoring_mode` / `timeout` Python API params,
36
+ clarified `--budget` / `--tau` help text, and marked the `DIFFCTX_OP_*` /
37
+ `DIFFCTX_*` tuning env vars as an experimental, non-public interface.
38
+
39
+ ## [1.9.2] - 2026-06-14
40
+
41
+ ### Added
42
+
43
+ - Private-key and keystore files are now excluded from output in **both** tree
44
+ mapping and `--diff` context, since such material is never legitimate LLM
45
+ context: `*.pem`, `*.key`, `*.pfx`, `*.p12`, `*.keystore`, `*.jks`, and SSH
46
+ private keys `id_rsa`/`id_dsa`/`id_ecdsa`/`id_ed25519` (public `.pub` keys stay
47
+ visible). The `--diff` path previously applied no ignore filtering at all, so a
48
+ changed key file would have leaked into context. Use `--no-default-ignores` to
49
+ opt out of tree-mode default ignores. (`.env` files are intentionally still
50
+ included — a changed `.env` is legitimate change context; redacting secret
51
+ *values* is a separate planned content-scan feature.)
52
+
53
+ ### Fixed
54
+
55
+ - **Diffs touching files larger than 100 KB no longer produce empty output.** A
56
+ changed file (e.g. a 142 KB `Math.h`) was subject to the same 100 KB cap as
57
+ context-discovery candidates, so the whole diff yielded "no semantic context".
58
+ Changed files — the subject of the diff — are now parsed up to 5 MB.
59
+ - **C/C++ symbol names** for variable declarations now report the variable, not
60
+ the type: a changed `const unsigned blane = …` is labeled `blane`, not
61
+ `unsigned`. Covers `init`/`array`/pointer/function declarators and C++
62
+ `reference`/`parenthesized` declarators (`int &r`, `int (*f)()`).
63
+ - Changed-file fragments are no longer dropped by the generated-file reduction
64
+ (cap of 5 + 30-line content truncation), which could discard the small
65
+ fragment covering the edited hunk and mislocate the change.
66
+ - Binary detection no longer misclassifies changed text files that embed ANSI
67
+ escape / control bytes (snapshot and terminal-recording fixtures); it now uses
68
+ git's NUL-byte heuristic, so such files are no longer silently dropped.
69
+ - Diff-context selection is now deterministic across processes: ego-graph score
70
+ accumulation, context-fragment capping, and the greedy selection heap use
71
+ stable tie-breaks, so identical inputs always yield byte-identical output.
72
+ - Python module-import edges are bidirectional (matching every other language
73
+ and Python's own symbol references), so a changed imported module now
74
+ propagates relevance back to its importers.
75
+
76
+ ### Hardened
77
+
78
+ - `git cat-file` blob reads are bounded (drain-and-reject above 16 MB) instead of
79
+ allocating an arbitrarily large buffer up front.
80
+
81
+ ## [1.9.1] - 2026-06-14
82
+
83
+ > Supersedes 1.9.0, which was tagged but never published to PyPI (release-process
84
+ > hiccup); 1.9.1 ships the same changes. There is no 1.9.0 on PyPI.
85
+
86
+ ### Added
87
+
88
+ - Diff-context output now leads with an orientation header — `commit_message`
89
+ and `changed_files` — in every format (YAML/JSON/Markdown/text), so a reader
90
+ sees *what* changed before reading any fragment.
91
+ - Each fragment carries a `role` of `changed` when it overlaps the diff hunks
92
+ (omitted for supporting context). Changed code is emitted first; context
93
+ follows, ordered by descending per-file relevance instead of alphabetically.
94
+
95
+ ### Changed
96
+
97
+ - Line-contiguous fragments of the same role within a file are merged into a
98
+ single entry, cutting the per-fragment scaffolding that dominated output made
99
+ up of one-line snippets (lossless on line coverage).
100
+
101
+ ### Fixed
102
+
103
+ - `get_changed_files` and `get_untracked_files` canonicalize paths consistently
104
+ with deleted/discovered files, preventing duplicate fragments when a tracked
105
+ path traverses a symlinked directory.
106
+ - Signature extraction no longer terminates at braces inside parameter defaults
107
+ or annotations (e.g. Python `def f(x={}):`), which previously truncated the
108
+ signature mid-parameter-list.
109
+ - The post-pass that rescues unrepresented changed files reuses the open
110
+ `git cat-file --batch` reader instead of spawning a `git show` per file.
111
+ - Degraded token-count fallback returns 0 for the empty string (was 1).
112
+
113
+ ## [1.8.0]
114
+
115
+ ### Added
116
+
117
+ - Public `diffctx.run(argv=None, *, prog=None, version=None)` engine entry. It
118
+ is the same execution path as the `diffctx` console script but accepts an
119
+ injected program name and version string, so a downstream wrapper (e.g. the
120
+ `treemapper` distribution) can present its own branding in `--help`,
121
+ `--version`, and error prefixes without duplicating the CLI. `main()` now
122
+ delegates to `run()` with the default `diffctx` identity — no behavior change
123
+ for existing callers.
124
+ - `diffctx.mcp.__main__.main(prog=..., extra=...)` accepts an injected program
125
+ name and install-extra string for the missing-dependency hint, so wrappers
126
+ can re-expose the MCP entry point with their own name.
127
+
128
+ ## [1.7.0] - 2026-05-22
129
+
130
+ ### Added
131
+
132
+ - README documents the `graph` subcommand and the `--scoring {ppr,ego,bm25}`
133
+ flag (default: `ego`); the absolutist "Uses Personalized PageRank" language
134
+ has been replaced with a table covering all three scoring modes.
135
+ - `SECURITY.md` now ships a "Threat model" section that scopes diffctx as a
136
+ local CLI (filesystem + git subprocess, no network) and documents that the
137
+ optional `diffctx-mcp` server confines its filesystem reach via
138
+ `DIFFCTX_ALLOWED_PATHS`.
139
+ - Footer of `README.md` links `CHANGELOG.md`, `SECURITY.md`, and
140
+ `docs/parameter-strategy.md` so they are no longer orphaned.
141
+
142
+ ### Changed
143
+
144
+ - **Package renamed from `treemapper` to `diffctx`.** PyPI distribution, CLI
145
+ binary, MCP server binary, and Python import path all use `diffctx`. The
146
+ `treemapper` PyPI package remains at 1.6.1 (frozen); install
147
+ `diffctx` for all new development. The `TREEMAPPER_ALLOWED_PATHS`
148
+ environment variable is now `DIFFCTX_ALLOWED_PATHS`.
149
+ - Single source of truth for the package version: the Python wheel reads its
150
+ version from `Cargo.toml` (via maturin) instead of duplicating it in
151
+ `pyproject.toml`, eliminating the Rust-crate-vs-Python-package version
152
+ desync that previously shipped to PyPI.
153
+ - `--diff` now defaults to `HEAD` when no range is supplied, matching the
154
+ most common invocation (`diffctx . --diff` → diff of the working tree
155
+ against `HEAD`) and saving an argument in the 30-second demo path.
156
+ - `numpy` moved from a required runtime dependency into the `[tree-sitter]`
157
+ extra, so default installs no longer pull a ~20 MB scientific stack the
158
+ core tree-mapping mode does not use.
159
+ - CLI error messages are now actionable: instead of a raw Python traceback,
160
+ invalid `--diff` ranges, missing git repositories, and unreadable paths
161
+ print a one-line `Error: <what> — try: <next step>` and exit with code `2`
162
+ for user-input errors (`1` is reserved for runtime failures).
163
+ - `automerge.yml` GitHub Actions workflow hardened: explicit minimal
164
+ `permissions:` block, pinned action SHAs, and a guard that refuses to
165
+ auto-merge anything touching `.github/`, `pyproject.toml`, or `Cargo.toml`.
166
+
167
+ ### Fixed
168
+
169
+ - Replaced every user-reachable `unwrap()`/`expect()` in the Rust core
170
+ (`tokenizer.rs`, `git.rs`, `scoring.rs`, `pybridge.rs`) with proper
171
+ `PyRuntimeError` / `GitError` propagation. A malformed diff, a missing
172
+ BPE table, or an oversized hunk-header integer no longer aborts the
173
+ Python interpreter via `panic = "abort"`.
174
+ - `diffctx-mcp` entry point now guards against being launched without the
175
+ `[mcp]` extra installed and prints an install hint instead of an
176
+ `ImportError` traceback.
177
+
178
+ ### Removed
179
+
180
+ - Dropped Kotlin and F# from the language matrix: tree-sitter grammars for
181
+ both were silently misaligned with the project's import-resolution rules
182
+ and produced misleading edge weights. They will return once the grammars
183
+ are vetted.
184
+
185
+ ### Security
186
+
187
+ - MCP server (`diffctx-mcp`) now refuses to traverse outside the
188
+ directories listed in `DIFFCTX_ALLOWED_PATHS` (OS-pathsep-separated)
189
+ and refuses to start if the envvar is unset when run as a network-facing
190
+ process. See [`SECURITY.md`](SECURITY.md) for the threat model.
191
+
192
+ ## [1.6.1 and earlier]
193
+
194
+ Earlier releases shipped as `treemapper`; see
195
+ <https://pypi.org/project/treemapper/#history> for legacy versions and
196
+ <https://github.com/nikolay-e/diffctx/releases> for the corresponding GitHub
197
+ release notes (`1.0.0` through `1.6.1`).
198
+
199
+ [Unreleased]: https://github.com/nikolay-e/diffctx/compare/v1.7.0...HEAD
200
+ [1.7.0]: https://github.com/nikolay-e/diffctx/compare/v1.6.1...v1.7.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diffctx
3
- Version: 1.8.1
3
+ Version: 1.10.0
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -127,7 +127,7 @@ commit, instead of dumping every changed file in full.*
127
127
  [releases page](https://github.com/nikolay-e/diffctx/releases/latest).
128
128
 
129
129
  > Diff context mode works out of the box. Adding `[tree-sitter]` enables AST-level
130
- > parsing for more accurate context selection across 12 languages.
130
+ > parsing for more accurate context selection across 30+ languages.
131
131
 
132
132
  ## Diff Context Mode
133
133
 
@@ -170,7 +170,7 @@ emitted, whichever comes first.
170
170
  | `--scoring` | `ego` | Scoring mode: `ego`, `ppr`, or `bm25` |
171
171
  | `--budget` | auto | Token cap. `auto` lets selection converge; `-1` disables the cap; `N` enforces a fixed cap |
172
172
  | `--alpha` | 0.60 | How tightly context clusters around changes (PPR damping; 0–1, higher = more focused) |
173
- | `--tau` | 0.08 | Minimum relevance required to include a fragment (lower = more context) |
173
+ | `--tau` | 0.12 | Minimum relevance required to include a fragment (lower = more context) |
174
174
  | `--full` | false | Include every changed fragment; skip the smart-selection step entirely |
175
175
 
176
176
  Calibration of `--alpha`, `--tau`, and the edge-weight priors is documented
@@ -209,6 +209,7 @@ diffctx . --max-depth 3 # limit depth
209
209
  diffctx . -i custom.ignore # custom ignore patterns
210
210
 
211
211
  # diff context mode (requires git repo):
212
+ diffctx . --diff # uncommitted changes (working tree vs HEAD)
212
213
  diffctx . --diff HEAD~1 # context for last commit
213
214
  diffctx . --diff main..feature # context for feature branch
214
215
  diffctx . --diff HEAD~1 --budget 30000 # limit to ~30k tokens
@@ -302,8 +303,10 @@ ctx = build_diff_context(
302
303
  # <0 = unlimited (10M-token soft ceiling)
303
304
  # >0 = explicit token cap
304
305
  alpha=0.6, # PPR damping factor
305
- tau=0.08, # stopping threshold
306
+ tau=0.12, # stopping threshold
306
307
  full=False, # skip smart selection
308
+ scoring_mode="ego", # "ego" (default), "ppr", or "bm25"
309
+ timeout=300, # seconds before the pipeline aborts
307
310
  )
308
311
  yaml_str = to_yaml(ctx)
309
312
  ```
@@ -50,7 +50,7 @@ commit, instead of dumping every changed file in full.*
50
50
  [releases page](https://github.com/nikolay-e/diffctx/releases/latest).
51
51
 
52
52
  > Diff context mode works out of the box. Adding `[tree-sitter]` enables AST-level
53
- > parsing for more accurate context selection across 12 languages.
53
+ > parsing for more accurate context selection across 30+ languages.
54
54
 
55
55
  ## Diff Context Mode
56
56
 
@@ -93,7 +93,7 @@ emitted, whichever comes first.
93
93
  | `--scoring` | `ego` | Scoring mode: `ego`, `ppr`, or `bm25` |
94
94
  | `--budget` | auto | Token cap. `auto` lets selection converge; `-1` disables the cap; `N` enforces a fixed cap |
95
95
  | `--alpha` | 0.60 | How tightly context clusters around changes (PPR damping; 0–1, higher = more focused) |
96
- | `--tau` | 0.08 | Minimum relevance required to include a fragment (lower = more context) |
96
+ | `--tau` | 0.12 | Minimum relevance required to include a fragment (lower = more context) |
97
97
  | `--full` | false | Include every changed fragment; skip the smart-selection step entirely |
98
98
 
99
99
  Calibration of `--alpha`, `--tau`, and the edge-weight priors is documented
@@ -132,6 +132,7 @@ diffctx . --max-depth 3 # limit depth
132
132
  diffctx . -i custom.ignore # custom ignore patterns
133
133
 
134
134
  # diff context mode (requires git repo):
135
+ diffctx . --diff # uncommitted changes (working tree vs HEAD)
135
136
  diffctx . --diff HEAD~1 # context for last commit
136
137
  diffctx . --diff main..feature # context for feature branch
137
138
  diffctx . --diff HEAD~1 --budget 30000 # limit to ~30k tokens
@@ -225,8 +226,10 @@ ctx = build_diff_context(
225
226
  # <0 = unlimited (10M-token soft ceiling)
226
227
  # >0 = explicit token cap
227
228
  alpha=0.6, # PPR damping factor
228
- tau=0.08, # stopping threshold
229
+ tau=0.12, # stopping threshold
229
230
  full=False, # skip smart selection
231
+ scoring_mode="ego", # "ego" (default), "ppr", or "bm25"
232
+ timeout=300, # seconds before the pipeline aborts
230
233
  )
231
234
  yaml_str = to_yaml(ctx)
232
235
  ```
@@ -194,7 +194,7 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
194
194
 
195
195
  [[package]]
196
196
  name = "diffctx"
197
- version = "1.8.0"
197
+ version = "1.10.0"
198
198
  dependencies = [
199
199
  "anyhow",
200
200
  "clap",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "diffctx"
3
- version = "1.8.0"
3
+ version = "1.10.0"
4
4
  edition = "2024"
5
5
  rust-version = "1.85"
6
6
 
@@ -0,0 +1,30 @@
1
+ use once_cell::sync::Lazy;
2
+
3
+ pub struct BudgetConfig {
4
+ pub unlimited: u32,
5
+ pub auto_multiplier: f64,
6
+ pub auto_min: u32,
7
+ pub auto_max: u32,
8
+ /// Per-core-fragment ceiling when sizing the auto budget. A core fragment
9
+ /// that is a whole-file `Chunk` (e.g. a 2000-line unparsed template touched
10
+ /// by one hunk) would otherwise inflate the budget by its entire byte
11
+ /// weight, dragging in the whole repo as "context". Capping each core
12
+ /// fragment's budget contribution decouples "how big are the files I edited"
13
+ /// from "how much of the repo do I pull in". Does not affect whether the
14
+ /// changed file is included (it always is) — only the context budget it earns.
15
+ pub core_token_cap_per_fragment: u32,
16
+ }
17
+
18
+ impl Default for BudgetConfig {
19
+ fn default() -> Self {
20
+ Self {
21
+ unlimited: 10_000_000,
22
+ auto_multiplier: 3.0,
23
+ auto_min: 8_000,
24
+ auto_max: 48_000,
25
+ core_token_cap_per_fragment: 1_500,
26
+ }
27
+ }
28
+ }
29
+
30
+ pub static BUDGET: Lazy<BudgetConfig> = Lazy::new(BudgetConfig::default);
@@ -4,6 +4,7 @@ use crate::config::env_overrides::{read_env_f64, read_env_fraction, read_env_ope
4
4
 
5
5
  pub struct AlgorithmLimits {
6
6
  pub max_file_size: usize,
7
+ pub max_changed_file_size: usize,
7
8
  pub max_fragments: usize,
8
9
  pub max_generated_fragments: usize,
9
10
  pub max_generated_lines: usize,
@@ -21,6 +22,7 @@ impl Default for AlgorithmLimits {
21
22
  .unwrap_or(200);
22
23
  Self {
23
24
  max_file_size: 100_000,
25
+ max_changed_file_size: 5_000_000,
24
26
  max_fragments,
25
27
  max_generated_fragments: 5,
26
28
  max_generated_lines: 30,
@@ -31,6 +33,12 @@ impl Default for AlgorithmLimits {
31
33
  }
32
34
  }
33
35
 
36
+ /// Hard ceiling for a single `git cat-file` blob read. Sized with headroom
37
+ /// above `max_changed_file_size` (5 MB) so legitimate large changed files still
38
+ /// load, while a pathological multi-hundred-MB blob is drained and rejected
39
+ /// instead of allocated up front.
40
+ pub const MAX_BLOB_READ_BYTES: usize = 16_000_000;
41
+
34
42
  pub const DEFAULT_PPR_ALPHA: f64 = 0.60;
35
43
  /// Calibrated on v1 manifest (2119 instances, 4 benchmarks). Winner
36
44
  /// of (tau, cbf) grid = (0.12, 0.5) at min(per_benchmark file_recall)
@@ -256,11 +256,13 @@ impl EdgeBuilder for PythonEdgeBuilder {
256
256
  if tgt == &f.id {
257
257
  continue;
258
258
  }
259
- let key = (f.id.clone(), tgt.clone());
260
- let existing = edges.get(&key).copied().unwrap_or(0.0);
261
- if PYTHON_SEMANTIC.import_weight > existing {
262
- edges.insert(key, PYTHON_SEMANTIC.import_weight);
263
- }
259
+ base::add_edge(
260
+ &mut edges,
261
+ &f.id,
262
+ tgt,
263
+ PYTHON_SEMANTIC.import_weight,
264
+ PYTHON_SEMANTIC.reverse_factor,
265
+ );
264
266
  }
265
267
  }
266
268
  }
@@ -11,7 +11,7 @@ use super::super::EdgeDict;
11
11
  use super::super::base::{self, EdgeBuilder, add_edge_unidirectional, path_to_module};
12
12
 
13
13
  static IMPORT_RE: Lazy<Regex> =
14
- Lazy::new(|| Regex::new(r"(?:from\s+([\w.]+)\s+import|import\s+([\w.]+))").unwrap());
14
+ Lazy::new(|| Regex::new(r"(?m)^\s*(?:from\s+([\w.]+)\s+import|import\s+([\w.]+))").unwrap());
15
15
 
16
16
  fn is_python_test(name: &str) -> bool {
17
17
  name.starts_with("test_") || name.ends_with("_test.py")
@@ -143,9 +143,8 @@ fn find_hub_noise_paths(graph: &Graph, changed_paths: &FxHashSet<Arc<str>>) -> F
143
143
 
144
144
  noise_counts
145
145
  .into_iter()
146
- .filter(|(p, count)| {
147
- *count >= 1
148
- && !direct_edge_paths.contains(p)
146
+ .filter(|(p, _count)| {
147
+ !direct_edge_paths.contains(p)
149
148
  && !changed_dirs.contains(
150
149
  &Path::new(p.as_ref())
151
150
  .parent()
@@ -290,7 +289,7 @@ pub fn cap_context_fragments(
290
289
  file_frags.sort_by(|a, b| {
291
290
  let sa = rel.get(&a.id).copied().unwrap_or(0.0);
292
291
  let sb = rel.get(&b.id).copied().unwrap_or(0.0);
293
- sb.partial_cmp(&sa).unwrap_or(std::cmp::Ordering::Equal)
292
+ sb.total_cmp(&sa).then_with(|| a.id.cmp(&b.id))
294
293
  });
295
294
  result.extend(
296
295
  file_frags
@@ -300,5 +299,6 @@ pub fn cap_context_fragments(
300
299
  }
301
300
  }
302
301
 
302
+ result.sort_by(|a, b| a.id.cmp(&b.id));
303
303
  result
304
304
  }
@@ -14,7 +14,11 @@ use crate::parsers::fragment_file;
14
14
  use crate::tokenizer::count_tokens;
15
15
  use crate::types::{Fragment, FragmentId, FragmentKind, extract_identifiers};
16
16
 
17
- static BINARY_CTRL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"[\x00-\x08\x0e-\x1f]").unwrap());
17
+ // Content reaching here is already-decoded UTF-8 text, so the only reliable
18
+ // binary signal is an embedded NUL (matching git's own heuristic). The old
19
+ // range flagged ESC/BS/etc., wrongly dropping changed text fixtures that embed
20
+ // ANSI escape codes (snapshot/terminal-recording files).
21
+ static BINARY_CTRL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\x00").unwrap());
18
22
 
19
23
  static GENERATED_FILENAME_PATTERNS: Lazy<FxHashSet<&'static str>> = Lazy::new(|| {
20
24
  [
@@ -193,6 +197,7 @@ fn read_file_content(
193
197
  root_dir: &Path,
194
198
  preferred_revs: &[String],
195
199
  mut batch_reader: Option<&mut CatFileBatch>,
200
+ is_changed: bool,
196
201
  ) -> Option<String> {
197
202
  let ext = file_path
198
203
  .extension()
@@ -208,7 +213,11 @@ fn read_file_content(
208
213
  .unwrap_or_else(|_| root_dir.to_path_buf());
209
214
  let rel = abs_path.strip_prefix(&resolved_root).ok()?;
210
215
 
211
- let max_size = LIMITS.max_file_size;
216
+ let max_size = if is_changed {
217
+ LIMITS.max_changed_file_size
218
+ } else {
219
+ LIMITS.max_file_size
220
+ };
212
221
  for rev in preferred_revs {
213
222
  if let Some(reader) = batch_reader.as_deref_mut() {
214
223
  match reader.get(rev, rel) {
@@ -229,7 +238,7 @@ fn read_file_content(
229
238
 
230
239
  if abs_path.exists() && abs_path.is_file() {
231
240
  if let Ok(meta) = std::fs::metadata(&abs_path) {
232
- if meta.len() as usize > LIMITS.max_file_size {
241
+ if meta.len() as usize > max_size {
233
242
  return None;
234
243
  }
235
244
  }
@@ -249,6 +258,7 @@ pub fn process_files_for_fragments(
249
258
  preferred_revs: &[String],
250
259
  seen_frag_ids: &mut FxHashSet<FragmentId>,
251
260
  mut batch_reader: Option<&mut CatFileBatch>,
261
+ is_changed: bool,
252
262
  ) -> Vec<Fragment> {
253
263
  let max_frags = LIMITS.max_fragments;
254
264
  let max_generated = LIMITS.max_generated_fragments;
@@ -267,6 +277,7 @@ pub fn process_files_for_fragments(
267
277
  root_dir,
268
278
  preferred_revs,
269
279
  batch_reader.as_deref_mut(),
280
+ is_changed,
270
281
  )?;
271
282
  Some((file_path.clone(), content))
272
283
  })
@@ -277,7 +288,11 @@ pub fn process_files_for_fragments(
277
288
  .map(|(file_path, content)| {
278
289
  let path_arc: Arc<str> = Arc::from(file_path.to_string_lossy().as_ref());
279
290
  let mut raw_frags = fragment_file(path_arc, content);
280
- let generated = is_generated_file(file_path, content);
291
+ // Changed files are the subject of the diff: never apply the
292
+ // aggressive generated-file reduction (cap=5 + 30-line content
293
+ // truncation), which can drop the small fragment covering the
294
+ // edited hunk before core identification runs.
295
+ let generated = !is_changed && is_generated_file(file_path, content);
281
296
  let cap = if generated { max_generated } else { max_frags };
282
297
  if raw_frags.len() > cap {
283
298
  raw_frags.sort_by(|a, b| b.line_count().cmp(&a.line_count()));
@@ -310,7 +325,7 @@ pub fn create_whole_file_fragment(
310
325
  preferred_revs: &[String],
311
326
  batch_reader: Option<&mut CatFileBatch>,
312
327
  ) -> Option<Fragment> {
313
- let content = read_file_content(path, root_dir, preferred_revs, batch_reader)?;
328
+ let content = read_file_content(path, root_dir, preferred_revs, batch_reader, true)?;
314
329
  let trimmed = content.trim();
315
330
  if trimmed.is_empty() {
316
331
  return None;
@@ -376,7 +376,15 @@ pub fn get_changed_files(repo_root: &Path, diff_range: Option<&str>) -> Result<V
376
376
  args.push(range);
377
377
  }
378
378
  let parts = run_git_z(repo_root, &args)?;
379
- Ok(parts.iter().map(|p| repo_root.join(p)).collect())
379
+ Ok(parts
380
+ .iter()
381
+ .map(|p| {
382
+ repo_root
383
+ .join(p)
384
+ .canonicalize()
385
+ .unwrap_or_else(|_| repo_root.join(p))
386
+ })
387
+ .collect())
380
388
  }
381
389
 
382
390
  pub fn get_deleted_files(repo_root: &Path, diff_range: Option<&str>) -> Result<FxHashSet<PathBuf>> {
@@ -481,7 +489,15 @@ pub fn get_untracked_files(repo_root: &Path) -> Result<Vec<PathBuf>> {
481
489
  repo_root,
482
490
  &["ls-files", "--others", "--exclude-standard", "-z"],
483
491
  )?;
484
- Ok(parts.iter().map(|p| repo_root.join(p)).collect())
492
+ Ok(parts
493
+ .iter()
494
+ .map(|p| {
495
+ repo_root
496
+ .join(p)
497
+ .canonicalize()
498
+ .unwrap_or_else(|_| repo_root.join(p))
499
+ })
500
+ .collect())
485
501
  }
486
502
 
487
503
  pub struct CatFileBatch {
@@ -578,6 +594,28 @@ impl CatFileBatch {
578
594
  GitError::CommandFailed(format!("cat-file: invalid size in header: {}", header_str))
579
595
  })?;
580
596
 
597
+ // Guard against allocating an unbounded blob. Anything larger than the
598
+ // biggest size we will ever parse is drained from the stream in bounded
599
+ // chunks (to keep the cat-file pipe in sync for the next request) and
600
+ // rejected, instead of allocating `size` bytes up front (OOM on a
601
+ // pathological multi-hundred-MB blob).
602
+ if size > crate::config::limits::MAX_BLOB_READ_BYTES {
603
+ let mut remaining = size;
604
+ let mut scratch = [0u8; 65536];
605
+ while remaining > 0 {
606
+ let want = remaining.min(scratch.len());
607
+ reader.read_exact(&mut scratch[..want])?;
608
+ remaining -= want;
609
+ }
610
+ let mut trailing = [0u8; 1];
611
+ let _ = reader.read_exact(&mut trailing);
612
+ return Err(GitError::CommandFailed(format!(
613
+ "cat-file: blob too large ({} bytes): {}",
614
+ size,
615
+ spec.trim()
616
+ )));
617
+ }
618
+
581
619
  let mut content = vec![0u8; size];
582
620
  reader.read_exact(&mut content)?;
583
621
 
@@ -113,6 +113,9 @@ impl Graph {
113
113
  if weight.is_nan() || weight.is_infinite() || weight <= 0.0 {
114
114
  return;
115
115
  }
116
+ if src == dst {
117
+ return;
118
+ }
116
119
  debug_assert!(
117
120
  self.csr_cache.is_none(),
118
121
  "add_edge called after Graph was frozen"
@@ -230,10 +233,11 @@ impl Graph {
230
233
  return FxHashMap::default();
231
234
  }
232
235
 
233
- let valid_seed_idxs: Vec<u32> = seeds
236
+ let mut valid_seed_idxs: Vec<u32> = seeds
234
237
  .iter()
235
238
  .filter_map(|s| fwd.node_to_idx.get(s).copied())
236
239
  .collect();
240
+ valid_seed_idxs.sort_unstable();
237
241
 
238
242
  let per_seed: Vec<Vec<(u32, u32, f64)>> = valid_seed_idxs
239
243
  .par_iter()
@@ -45,10 +45,12 @@ impl IntervalIndex {
45
45
  continue;
46
46
  }
47
47
  // Strict `>`: a fragment starting on the very last line of an
48
- // already-selected fragment is adjacent, not overlapping. Compact
49
- // languages (Rust/Go/Scala one-liners, Lisp `}{` chains) routinely
50
- // produce back-to-back fragments sharing exactly that boundary
51
- // line; treating it as overlap silently drops the next fragment.
48
+ // already-selected fragment shares exactly one boundary line. We
49
+ // deliberately tolerate that one-line overlap rather than drop the
50
+ // candidate, because compact languages (Rust/Go/Scala one-liners,
51
+ // Lisp `}{` chains) routinely produce back-to-back fragments sharing
52
+ // that boundary line; rejecting them would silently discard the
53
+ // next fragment's unique content for the sake of one duplicated line.
52
54
  if end > frag.start_line() {
53
55
  return true;
54
56
  }
@@ -165,7 +165,20 @@ pub fn build_diff_context_in_memory(
165
165
  );
166
166
 
167
167
  let dummy_root = Path::new(".");
168
- build_diff_context_output(dummy_root, &selected, no_content)
168
+ let mut changed_list: Vec<String> = changed_paths.iter().cloned().collect();
169
+ changed_list.sort();
170
+ let change = crate::render::ChangeSummary {
171
+ commit_message: None,
172
+ changed_files: changed_list,
173
+ };
174
+ build_diff_context_output(
175
+ dummy_root,
176
+ &selected,
177
+ no_content,
178
+ &core_ids,
179
+ &scoring_result.rel_scores,
180
+ change,
181
+ )
169
182
  }
170
183
 
171
184
  fn compute_memory_hunks(
@@ -323,6 +336,8 @@ fn empty_output(name: &str) -> DiffContextOutput {
323
336
  DiffContextOutput {
324
337
  name: name.to_string(),
325
338
  output_type: "diff_context".to_string(),
339
+ commit_message: None,
340
+ changed_files: Vec::new(),
326
341
  fragment_count: 0,
327
342
  fragments: Vec::new(),
328
343
  latency: None,