diffctx 1.9.1__tar.gz → 1.10.1__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 (162) hide show
  1. {diffctx-1.9.1 → diffctx-1.10.1}/CHANGELOG.md +75 -1
  2. {diffctx-1.9.1 → diffctx-1.10.1}/PKG-INFO +7 -4
  3. {diffctx-1.9.1 → diffctx-1.10.1}/README.md +6 -3
  4. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/Cargo.lock +23 -57
  5. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/Cargo.toml +4 -4
  6. diffctx-1.10.1/diffctx/src/config/budget.rs +30 -0
  7. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/config/limits.rs +8 -0
  8. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/python.rs +7 -5
  9. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/structural/testing.rs +1 -1
  10. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/filtering.rs +2 -1
  11. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/fragmentation.rs +20 -5
  12. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/git.rs +22 -0
  13. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/graph.rs +5 -1
  14. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/main.rs +47 -11
  15. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/memory_pipeline.rs +1 -0
  16. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/parsers/tree_sitter_strategy.rs +19 -1
  17. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/pipeline.rs +30 -3
  18. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/postpass.rs +105 -2
  19. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/project_graph.rs +2 -0
  20. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/select.rs +4 -1
  21. {diffctx-1.9.1 → diffctx-1.10.1}/pyproject.toml +3 -3
  22. {diffctx-1.9.1 → diffctx-1.10.1}/src/diffctx/cli.py +3 -3
  23. {diffctx-1.9.1 → diffctx-1.10.1}/src/diffctx/diffctx/pipeline.py +2 -2
  24. {diffctx-1.9.1 → diffctx-1.10.1}/src/diffctx/ignore.py +14 -0
  25. {diffctx-1.9.1 → diffctx-1.10.1}/src/diffctx/main.py +2 -2
  26. diffctx-1.10.1/src/diffctx/mcp/README.md +142 -0
  27. {diffctx-1.9.1 → diffctx-1.10.1}/src/diffctx/mcp/__main__.py +1 -1
  28. {diffctx-1.9.1 → diffctx-1.10.1}/src/diffctx/mcp/server.py +15 -8
  29. diffctx-1.10.1/src/diffctx/version.py +1 -0
  30. {diffctx-1.9.1 → diffctx-1.10.1}/src/diffctx/writer.py +39 -25
  31. diffctx-1.9.1/diffctx/src/config/budget.rs +0 -21
  32. diffctx-1.9.1/src/diffctx/mcp/README.md +0 -104
  33. diffctx-1.9.1/src/diffctx/version.py +0 -1
  34. {diffctx-1.9.1 → diffctx-1.10.1}/LICENSE +0 -0
  35. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/analytics.rs +0 -0
  36. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/candidate_files.rs +0 -0
  37. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/config/analytics.rs +0 -0
  38. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/config/bm25.rs +0 -0
  39. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/config/category_weights.rs +0 -0
  40. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/config/edge_weights.rs +0 -0
  41. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/config/env_overrides.rs +0 -0
  42. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/config/extensions.rs +0 -0
  43. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/config/filtering.rs +0 -0
  44. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/config/fragmentation.rs +0 -0
  45. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/config/git.rs +0 -0
  46. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/config/graph_filtering.rs +0 -0
  47. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/config/importance.rs +0 -0
  48. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/config/mod.rs +0 -0
  49. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/config/mode.rs +0 -0
  50. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/config/needs.rs +0 -0
  51. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/config/parsers.rs +0 -0
  52. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/config/render.rs +0 -0
  53. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/config/scoring.rs +0 -0
  54. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/config/selection.rs +0 -0
  55. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/config/tokenization.rs +0 -0
  56. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/config/weights.rs +0 -0
  57. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/core.rs +0 -0
  58. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/discovery.rs +0 -0
  59. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/base.rs +0 -0
  60. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/config_edges/build_system.rs +0 -0
  61. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/config_edges/cicd.rs +0 -0
  62. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/config_edges/docker.rs +0 -0
  63. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/config_edges/generic.rs +0 -0
  64. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/config_edges/helm.rs +0 -0
  65. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/config_edges/kubernetes.rs +0 -0
  66. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/config_edges/mod.rs +0 -0
  67. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/document/mod.rs +0 -0
  68. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/history/cochange.rs +0 -0
  69. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/history/mod.rs +0 -0
  70. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/mod.rs +0 -0
  71. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/ansible.rs +0 -0
  72. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/bazel.rs +0 -0
  73. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/c_family.rs +0 -0
  74. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/cargo_edges.rs +0 -0
  75. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/clojure.rs +0 -0
  76. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/css.rs +0 -0
  77. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/dart.rs +0 -0
  78. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/dbt.rs +0 -0
  79. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/dotnet.rs +0 -0
  80. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/elixir.rs +0 -0
  81. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/erlang.rs +0 -0
  82. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/go.rs +0 -0
  83. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/graphql.rs +0 -0
  84. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/haskell.rs +0 -0
  85. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/javascript.rs +0 -0
  86. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/julia.rs +0 -0
  87. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/jvm.rs +0 -0
  88. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/latex.rs +0 -0
  89. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/lua.rs +0 -0
  90. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/mod.rs +0 -0
  91. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/nim.rs +0 -0
  92. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/nix.rs +0 -0
  93. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/ocaml.rs +0 -0
  94. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/openapi.rs +0 -0
  95. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/perl.rs +0 -0
  96. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/php.rs +0 -0
  97. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/prisma.rs +0 -0
  98. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/protobuf.rs +0 -0
  99. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/r_lang.rs +0 -0
  100. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/ruby.rs +0 -0
  101. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/rust_lang.rs +0 -0
  102. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/shell.rs +0 -0
  103. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/sql.rs +0 -0
  104. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/swift.rs +0 -0
  105. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/tags.rs +0 -0
  106. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/terraform.rs +0 -0
  107. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/semantic/zig.rs +0 -0
  108. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/similarity/lexical.rs +0 -0
  109. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/similarity/mod.rs +0 -0
  110. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/structural/containment.rs +0 -0
  111. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/structural/mod.rs +0 -0
  112. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/edges/structural/sibling.rs +0 -0
  113. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/graph_export.rs +0 -0
  114. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/interval.rs +0 -0
  115. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/languages.rs +0 -0
  116. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/lib.rs +0 -0
  117. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/mode.rs +0 -0
  118. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/parsers/config_parser.rs +0 -0
  119. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/parsers/generic.rs +0 -0
  120. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/parsers/markdown.rs +0 -0
  121. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/parsers/mod.rs +0 -0
  122. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/ppr.rs +0 -0
  123. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/pybridge.rs +0 -0
  124. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/render.rs +0 -0
  125. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/scoring.rs +0 -0
  126. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/signatures.rs +0 -0
  127. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/stopwords.rs +0 -0
  128. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/test_harness.rs +0 -0
  129. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/tokenizer.rs +0 -0
  130. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/types.rs +0 -0
  131. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/utility/boltzmann.rs +0 -0
  132. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/utility/importance.rs +0 -0
  133. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/utility/mod.rs +0 -0
  134. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/utility/needs.rs +0 -0
  135. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/src/utility/scoring.rs +0 -0
  136. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/tests/common/mod.rs +0 -0
  137. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/tests/fixtures/garbage/garbage_api.py +0 -0
  138. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/tests/fixtures/garbage/garbage_constants.py +0 -0
  139. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/tests/fixtures/garbage/garbage_handlers.py +0 -0
  140. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/tests/fixtures/garbage/garbage_models.py +0 -0
  141. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/tests/fixtures/garbage/garbage_module.js +0 -0
  142. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/tests/fixtures/garbage/garbage_services.py +0 -0
  143. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/tests/fixtures/garbage/garbage_types.py +0 -0
  144. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/tests/fixtures/garbage/garbage_unrelated.yaml +0 -0
  145. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/tests/fixtures/garbage/garbage_utils.py +0 -0
  146. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/tests/fixtures/garbage/garbage_validators.py +0 -0
  147. {diffctx-1.9.1 → diffctx-1.10.1}/diffctx/tests/yaml_cases.rs +0 -0
  148. {diffctx-1.9.1 → diffctx-1.10.1}/rust-toolchain.toml +0 -0
  149. {diffctx-1.9.1 → diffctx-1.10.1}/src/diffctx/__init__.py +0 -0
  150. {diffctx-1.9.1 → diffctx-1.10.1}/src/diffctx/__main__.py +0 -0
  151. {diffctx-1.9.1 → diffctx-1.10.1}/src/diffctx/clipboard.py +0 -0
  152. {diffctx-1.9.1 → diffctx-1.10.1}/src/diffctx/diffctx/__init__.py +0 -0
  153. {diffctx-1.9.1 → diffctx-1.10.1}/src/diffctx/diffctx/graph_analytics.py +0 -0
  154. {diffctx-1.9.1 → diffctx-1.10.1}/src/diffctx/diffctx/graph_export.py +0 -0
  155. {diffctx-1.9.1 → diffctx-1.10.1}/src/diffctx/diffctx/project_graph.py +0 -0
  156. {diffctx-1.9.1 → diffctx-1.10.1}/src/diffctx/logger.py +0 -0
  157. {diffctx-1.9.1 → diffctx-1.10.1}/src/diffctx/mcp/__init__.py +0 -0
  158. {diffctx-1.9.1 → diffctx-1.10.1}/src/diffctx/mcp/formatting.py +0 -0
  159. {diffctx-1.9.1 → diffctx-1.10.1}/src/diffctx/mcp/security.py +0 -0
  160. {diffctx-1.9.1 → diffctx-1.10.1}/src/diffctx/py.typed +0 -0
  161. {diffctx-1.9.1 → diffctx-1.10.1}/src/diffctx/tokens.py +0 -0
  162. {diffctx-1.9.1 → diffctx-1.10.1}/src/diffctx/tree.py +0 -0
@@ -7,7 +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.9.0] - 2026-06-14
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.
11
85
 
12
86
  ### Added
13
87
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diffctx
3
- Version: 1.9.1
3
+ Version: 1.10.1
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",
@@ -355,12 +355,6 @@ version = "0.5.0"
355
355
  source = "registry+https://github.com/rust-lang/crates.io-index"
356
356
  checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
357
357
 
358
- [[package]]
359
- name = "hermit-abi"
360
- version = "0.5.2"
361
- source = "registry+https://github.com/rust-lang/crates.io-index"
362
- checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
363
-
364
358
  [[package]]
365
359
  name = "id-arena"
366
360
  version = "2.3.0"
@@ -411,23 +405,23 @@ checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
411
405
 
412
406
  [[package]]
413
407
  name = "libmimalloc-sys"
414
- version = "0.1.47"
408
+ version = "0.1.49"
415
409
  source = "registry+https://github.com/rust-lang/crates.io-index"
416
- checksum = "2d1eacfa31c33ec25e873c136ba5669f00f9866d0688bea7be4d3f7e43067df6"
410
+ checksum = "6a45a52f43e1c16f667ccfe4dd8c85b7f7c204fd5e3bf46c5b0db9a5c3c0b8e9"
417
411
  dependencies = [
418
412
  "cc",
419
413
  ]
420
414
 
421
415
  [[package]]
422
416
  name = "libtest-mimic"
423
- version = "0.7.3"
417
+ version = "0.8.2"
424
418
  source = "registry+https://github.com/rust-lang/crates.io-index"
425
- checksum = "cc0bda45ed5b3a2904262c1bb91e526127aa70e7ef3758aba2ef93cf896b9b58"
419
+ checksum = "14e6ba06f0ade6e504aff834d7c34298e5155c6baca353cc6a4aaff2f9fd7f33"
426
420
  dependencies = [
421
+ "anstream",
422
+ "anstyle",
427
423
  "clap",
428
424
  "escape8259",
429
- "termcolor",
430
- "threadpool",
431
425
  ]
432
426
 
433
427
  [[package]]
@@ -459,9 +453,9 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
459
453
 
460
454
  [[package]]
461
455
  name = "mimalloc"
462
- version = "0.1.50"
456
+ version = "0.1.52"
463
457
  source = "registry+https://github.com/rust-lang/crates.io-index"
464
- checksum = "b3627c4272df786b9260cabaa46aec1d59c93ede723d4c3ef646c503816b0640"
458
+ checksum = "2d4139bb28d14ad1facf21d5eb8825051b326e172d216b39f6d31df53cc97862"
465
459
  dependencies = [
466
460
  "libmimalloc-sys",
467
461
  ]
@@ -475,16 +469,6 @@ dependencies = [
475
469
  "windows-sys",
476
470
  ]
477
471
 
478
- [[package]]
479
- name = "num_cpus"
480
- version = "1.17.0"
481
- source = "registry+https://github.com/rust-lang/crates.io-index"
482
- checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
483
- dependencies = [
484
- "hermit-abi",
485
- "libc",
486
- ]
487
-
488
472
  [[package]]
489
473
  name = "once_cell"
490
474
  version = "1.21.4"
@@ -622,9 +606,9 @@ dependencies = [
622
606
 
623
607
  [[package]]
624
608
  name = "regex"
625
- version = "1.12.3"
609
+ version = "1.12.4"
626
610
  source = "registry+https://github.com/rust-lang/crates.io-index"
627
- checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
611
+ checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
628
612
  dependencies = [
629
613
  "aho-corasick",
630
614
  "memchr",
@@ -645,9 +629,9 @@ dependencies = [
645
629
 
646
630
  [[package]]
647
631
  name = "regex-syntax"
648
- version = "0.8.10"
632
+ version = "0.8.11"
649
633
  source = "registry+https://github.com/rust-lang/crates.io-index"
650
- checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
634
+ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
651
635
 
652
636
  [[package]]
653
637
  name = "rustc-hash"
@@ -721,9 +705,9 @@ dependencies = [
721
705
 
722
706
  [[package]]
723
707
  name = "serde_json"
724
- version = "1.0.149"
708
+ version = "1.0.150"
725
709
  source = "registry+https://github.com/rust-lang/crates.io-index"
726
- checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
710
+ checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
727
711
  dependencies = [
728
712
  "indexmap",
729
713
  "itoa",
@@ -772,9 +756,9 @@ dependencies = [
772
756
 
773
757
  [[package]]
774
758
  name = "smallvec"
775
- version = "1.15.1"
759
+ version = "1.15.2"
776
760
  source = "registry+https://github.com/rust-lang/crates.io-index"
777
- checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
761
+ checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
778
762
 
779
763
  [[package]]
780
764
  name = "streaming-iterator"
@@ -818,15 +802,6 @@ dependencies = [
818
802
  "windows-sys",
819
803
  ]
820
804
 
821
- [[package]]
822
- name = "termcolor"
823
- version = "1.4.1"
824
- source = "registry+https://github.com/rust-lang/crates.io-index"
825
- checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
826
- dependencies = [
827
- "winapi-util",
828
- ]
829
-
830
805
  [[package]]
831
806
  name = "thiserror"
832
807
  version = "2.0.18"
@@ -856,15 +831,6 @@ dependencies = [
856
831
  "cfg-if",
857
832
  ]
858
833
 
859
- [[package]]
860
- name = "threadpool"
861
- version = "1.8.1"
862
- source = "registry+https://github.com/rust-lang/crates.io-index"
863
- checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa"
864
- dependencies = [
865
- "num_cpus",
866
- ]
867
-
868
834
  [[package]]
869
835
  name = "tiktoken-rs"
870
836
  version = "0.12.0"
@@ -1058,9 +1024,9 @@ dependencies = [
1058
1024
 
1059
1025
  [[package]]
1060
1026
  name = "tree-sitter-go"
1061
- version = "0.23.4"
1027
+ version = "0.25.0"
1062
1028
  source = "registry+https://github.com/rust-lang/crates.io-index"
1063
- checksum = "b13d476345220dbe600147dd444165c5791bf85ef53e28acbedd46112ee18431"
1029
+ checksum = "c8560a4d2f835cc0d4d2c2e03cbd0dde2f6114b43bc491164238d333e28b16ea"
1064
1030
  dependencies = [
1065
1031
  "cc",
1066
1032
  "tree-sitter-language",
@@ -1244,9 +1210,9 @@ dependencies = [
1244
1210
 
1245
1211
  [[package]]
1246
1212
  name = "tree-sitter-r"
1247
- version = "1.2.0"
1213
+ version = "1.3.0"
1248
1214
  source = "registry+https://github.com/rust-lang/crates.io-index"
1249
- checksum = "429133cbda9f8a46e03ef3aae6abb6c3d22875f8585cad472138101bfd517255"
1215
+ checksum = "ffc9954ec870dcad6cffdd302b405306c68cf031ed79a78cd9746f6740d9fe20"
1250
1216
  dependencies = [
1251
1217
  "cc",
1252
1218
  "tree-sitter-language",
@@ -1264,9 +1230,9 @@ dependencies = [
1264
1230
 
1265
1231
  [[package]]
1266
1232
  name = "tree-sitter-rust"
1267
- version = "0.23.3"
1233
+ version = "0.24.2"
1268
1234
  source = "registry+https://github.com/rust-lang/crates.io-index"
1269
- checksum = "ca8ccb3e3a3495c8a943f6c3fd24c3804c471fd7f4f16087623c7fa4c0068e8a"
1235
+ checksum = "439e577dbe07423ec2582ac62c7531120dbfccfa6e5f92406f93dd271a120e45"
1270
1236
  dependencies = [
1271
1237
  "cc",
1272
1238
  "tree-sitter-language",
@@ -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
 
@@ -70,8 +70,8 @@ tree-sitter = "0.25"
70
70
  tree-sitter-python = { version = "0.23", optional = true }
71
71
  tree-sitter-javascript = { version = "0.23", optional = true }
72
72
  tree-sitter-typescript = { version = "0.23", optional = true }
73
- tree-sitter-go = { version = "0.23", optional = true }
74
- tree-sitter-rust = { version = "0.23", optional = true }
73
+ tree-sitter-go = { version = "0.25", optional = true }
74
+ tree-sitter-rust = { version = "0.24", optional = true }
75
75
  tree-sitter-java = { version = "0.23", optional = true }
76
76
  tree-sitter-c = { version = "0.24", optional = true }
77
77
  tree-sitter-cpp = { version = "0.23", optional = true }
@@ -126,7 +126,7 @@ pyo3 = { version = "0.29", features = ["extension-module", "abi3-py310"], option
126
126
  [dev-dependencies]
127
127
  tempfile = "3"
128
128
  serde_yaml = "0.9"
129
- libtest-mimic = "0.7"
129
+ libtest-mimic = "0.8"
130
130
 
131
131
  [[test]]
132
132
  name = "yaml_cases"
@@ -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")
@@ -289,7 +289,7 @@ pub fn cap_context_fragments(
289
289
  file_frags.sort_by(|a, b| {
290
290
  let sa = rel.get(&a.id).copied().unwrap_or(0.0);
291
291
  let sb = rel.get(&b.id).copied().unwrap_or(0.0);
292
- sb.partial_cmp(&sa).unwrap_or(std::cmp::Ordering::Equal)
292
+ sb.total_cmp(&sa).then_with(|| a.id.cmp(&b.id))
293
293
  });
294
294
  result.extend(
295
295
  file_frags
@@ -299,5 +299,6 @@ pub fn cap_context_fragments(
299
299
  }
300
300
  }
301
301
 
302
+ result.sort_by(|a, b| a.id.cmp(&b.id));
302
303
  result
303
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;
@@ -594,6 +594,28 @@ impl CatFileBatch {
594
594
  GitError::CommandFailed(format!("cat-file: invalid size in header: {}", header_str))
595
595
  })?;
596
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
+
597
619
  let mut content = vec![0u8; size];
598
620
  reader.read_exact(&mut content)?;
599
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()