diffctx 1.9.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 (162) hide show
  1. {diffctx-1.9.1 → diffctx-1.10.0}/CHANGELOG.md +75 -1
  2. {diffctx-1.9.1 → diffctx-1.10.0}/PKG-INFO +7 -4
  3. {diffctx-1.9.1 → diffctx-1.10.0}/README.md +6 -3
  4. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/Cargo.lock +1 -1
  5. {diffctx-1.9.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.9.1 → diffctx-1.10.0}/diffctx/src/config/limits.rs +8 -0
  8. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/python.rs +7 -5
  9. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/structural/testing.rs +1 -1
  10. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/filtering.rs +2 -1
  11. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/fragmentation.rs +20 -5
  12. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/git.rs +22 -0
  13. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/graph.rs +5 -1
  14. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/parsers/tree_sitter_strategy.rs +19 -1
  15. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/pipeline.rs +29 -3
  16. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/project_graph.rs +2 -0
  17. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/select.rs +4 -1
  18. {diffctx-1.9.1 → diffctx-1.10.0}/pyproject.toml +3 -3
  19. {diffctx-1.9.1 → diffctx-1.10.0}/src/diffctx/cli.py +3 -3
  20. {diffctx-1.9.1 → diffctx-1.10.0}/src/diffctx/diffctx/pipeline.py +2 -2
  21. {diffctx-1.9.1 → diffctx-1.10.0}/src/diffctx/ignore.py +14 -0
  22. diffctx-1.10.0/src/diffctx/mcp/README.md +142 -0
  23. {diffctx-1.9.1 → diffctx-1.10.0}/src/diffctx/mcp/server.py +15 -8
  24. diffctx-1.10.0/src/diffctx/version.py +1 -0
  25. diffctx-1.9.1/diffctx/src/config/budget.rs +0 -21
  26. diffctx-1.9.1/src/diffctx/mcp/README.md +0 -104
  27. diffctx-1.9.1/src/diffctx/version.py +0 -1
  28. {diffctx-1.9.1 → diffctx-1.10.0}/LICENSE +0 -0
  29. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/analytics.rs +0 -0
  30. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/candidate_files.rs +0 -0
  31. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/config/analytics.rs +0 -0
  32. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/config/bm25.rs +0 -0
  33. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/config/category_weights.rs +0 -0
  34. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/config/edge_weights.rs +0 -0
  35. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/config/env_overrides.rs +0 -0
  36. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/config/extensions.rs +0 -0
  37. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/config/filtering.rs +0 -0
  38. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/config/fragmentation.rs +0 -0
  39. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/config/git.rs +0 -0
  40. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/config/graph_filtering.rs +0 -0
  41. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/config/importance.rs +0 -0
  42. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/config/mod.rs +0 -0
  43. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/config/mode.rs +0 -0
  44. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/config/needs.rs +0 -0
  45. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/config/parsers.rs +0 -0
  46. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/config/render.rs +0 -0
  47. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/config/scoring.rs +0 -0
  48. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/config/selection.rs +0 -0
  49. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/config/tokenization.rs +0 -0
  50. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/config/weights.rs +0 -0
  51. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/core.rs +0 -0
  52. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/discovery.rs +0 -0
  53. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/base.rs +0 -0
  54. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/config_edges/build_system.rs +0 -0
  55. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/config_edges/cicd.rs +0 -0
  56. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/config_edges/docker.rs +0 -0
  57. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/config_edges/generic.rs +0 -0
  58. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/config_edges/helm.rs +0 -0
  59. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/config_edges/kubernetes.rs +0 -0
  60. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/config_edges/mod.rs +0 -0
  61. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/document/mod.rs +0 -0
  62. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/history/cochange.rs +0 -0
  63. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/history/mod.rs +0 -0
  64. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/mod.rs +0 -0
  65. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/ansible.rs +0 -0
  66. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/bazel.rs +0 -0
  67. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/c_family.rs +0 -0
  68. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/cargo_edges.rs +0 -0
  69. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/clojure.rs +0 -0
  70. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/css.rs +0 -0
  71. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/dart.rs +0 -0
  72. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/dbt.rs +0 -0
  73. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/dotnet.rs +0 -0
  74. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/elixir.rs +0 -0
  75. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/erlang.rs +0 -0
  76. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/go.rs +0 -0
  77. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/graphql.rs +0 -0
  78. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/haskell.rs +0 -0
  79. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/javascript.rs +0 -0
  80. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/julia.rs +0 -0
  81. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/jvm.rs +0 -0
  82. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/latex.rs +0 -0
  83. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/lua.rs +0 -0
  84. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/mod.rs +0 -0
  85. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/nim.rs +0 -0
  86. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/nix.rs +0 -0
  87. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/ocaml.rs +0 -0
  88. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/openapi.rs +0 -0
  89. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/perl.rs +0 -0
  90. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/php.rs +0 -0
  91. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/prisma.rs +0 -0
  92. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/protobuf.rs +0 -0
  93. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/r_lang.rs +0 -0
  94. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/ruby.rs +0 -0
  95. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/rust_lang.rs +0 -0
  96. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/shell.rs +0 -0
  97. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/sql.rs +0 -0
  98. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/swift.rs +0 -0
  99. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/tags.rs +0 -0
  100. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/terraform.rs +0 -0
  101. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/zig.rs +0 -0
  102. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/similarity/lexical.rs +0 -0
  103. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/similarity/mod.rs +0 -0
  104. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/structural/containment.rs +0 -0
  105. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/structural/mod.rs +0 -0
  106. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/edges/structural/sibling.rs +0 -0
  107. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/graph_export.rs +0 -0
  108. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/interval.rs +0 -0
  109. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/languages.rs +0 -0
  110. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/lib.rs +0 -0
  111. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/main.rs +0 -0
  112. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/memory_pipeline.rs +0 -0
  113. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/mode.rs +0 -0
  114. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/parsers/config_parser.rs +0 -0
  115. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/parsers/generic.rs +0 -0
  116. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/parsers/markdown.rs +0 -0
  117. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/parsers/mod.rs +0 -0
  118. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/postpass.rs +0 -0
  119. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/ppr.rs +0 -0
  120. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/pybridge.rs +0 -0
  121. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/render.rs +0 -0
  122. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/scoring.rs +0 -0
  123. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/signatures.rs +0 -0
  124. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/stopwords.rs +0 -0
  125. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/test_harness.rs +0 -0
  126. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/tokenizer.rs +0 -0
  127. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/types.rs +0 -0
  128. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/utility/boltzmann.rs +0 -0
  129. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/utility/importance.rs +0 -0
  130. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/utility/mod.rs +0 -0
  131. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/utility/needs.rs +0 -0
  132. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/src/utility/scoring.rs +0 -0
  133. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/tests/common/mod.rs +0 -0
  134. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/tests/fixtures/garbage/garbage_api.py +0 -0
  135. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/tests/fixtures/garbage/garbage_constants.py +0 -0
  136. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/tests/fixtures/garbage/garbage_handlers.py +0 -0
  137. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/tests/fixtures/garbage/garbage_models.py +0 -0
  138. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/tests/fixtures/garbage/garbage_module.js +0 -0
  139. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/tests/fixtures/garbage/garbage_services.py +0 -0
  140. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/tests/fixtures/garbage/garbage_types.py +0 -0
  141. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/tests/fixtures/garbage/garbage_unrelated.yaml +0 -0
  142. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/tests/fixtures/garbage/garbage_utils.py +0 -0
  143. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/tests/fixtures/garbage/garbage_validators.py +0 -0
  144. {diffctx-1.9.1 → diffctx-1.10.0}/diffctx/tests/yaml_cases.rs +0 -0
  145. {diffctx-1.9.1 → diffctx-1.10.0}/rust-toolchain.toml +0 -0
  146. {diffctx-1.9.1 → diffctx-1.10.0}/src/diffctx/__init__.py +0 -0
  147. {diffctx-1.9.1 → diffctx-1.10.0}/src/diffctx/__main__.py +0 -0
  148. {diffctx-1.9.1 → diffctx-1.10.0}/src/diffctx/clipboard.py +0 -0
  149. {diffctx-1.9.1 → diffctx-1.10.0}/src/diffctx/diffctx/__init__.py +0 -0
  150. {diffctx-1.9.1 → diffctx-1.10.0}/src/diffctx/diffctx/graph_analytics.py +0 -0
  151. {diffctx-1.9.1 → diffctx-1.10.0}/src/diffctx/diffctx/graph_export.py +0 -0
  152. {diffctx-1.9.1 → diffctx-1.10.0}/src/diffctx/diffctx/project_graph.py +0 -0
  153. {diffctx-1.9.1 → diffctx-1.10.0}/src/diffctx/logger.py +0 -0
  154. {diffctx-1.9.1 → diffctx-1.10.0}/src/diffctx/main.py +0 -0
  155. {diffctx-1.9.1 → diffctx-1.10.0}/src/diffctx/mcp/__init__.py +0 -0
  156. {diffctx-1.9.1 → diffctx-1.10.0}/src/diffctx/mcp/__main__.py +0 -0
  157. {diffctx-1.9.1 → diffctx-1.10.0}/src/diffctx/mcp/formatting.py +0 -0
  158. {diffctx-1.9.1 → diffctx-1.10.0}/src/diffctx/mcp/security.py +0 -0
  159. {diffctx-1.9.1 → diffctx-1.10.0}/src/diffctx/py.typed +0 -0
  160. {diffctx-1.9.1 → diffctx-1.10.0}/src/diffctx/tokens.py +0 -0
  161. {diffctx-1.9.1 → diffctx-1.10.0}/src/diffctx/tree.py +0 -0
  162. {diffctx-1.9.1 → diffctx-1.10.0}/src/diffctx/writer.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.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")
@@ -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()
@@ -1172,13 +1172,31 @@ fn unwrap_decorated<'a>(node: Node<'a>) -> Node<'a> {
1172
1172
  fn unwrap_declarator<'a>(mut name_node: Node<'a>) -> Node<'a> {
1173
1173
  loop {
1174
1174
  match name_node.kind() {
1175
- "pointer_declarator" | "function_declarator" => {
1175
+ "pointer_declarator"
1176
+ | "function_declarator"
1177
+ | "init_declarator"
1178
+ | "array_declarator" => {
1176
1179
  if let Some(inner) = name_node.child_by_field_name("declarator") {
1177
1180
  name_node = inner;
1178
1181
  } else {
1179
1182
  break;
1180
1183
  }
1181
1184
  }
1185
+ // C++ `reference_declarator` (`int &r`) and `parenthesized_declarator`
1186
+ // (`int (*f)()`) hold their inner declarator as a positional child,
1187
+ // NOT under a `declarator` field, so descend the first named
1188
+ // declarator/identifier child instead of querying the field.
1189
+ "reference_declarator" | "parenthesized_declarator" => {
1190
+ match (0..name_node.named_child_count())
1191
+ .filter_map(|i| name_node.named_child(i))
1192
+ .find(|c| {
1193
+ let k = c.kind();
1194
+ k.ends_with("declarator") || k.ends_with("identifier")
1195
+ }) {
1196
+ Some(inner) => name_node = inner,
1197
+ None => break,
1198
+ }
1199
+ }
1182
1200
  _ => break,
1183
1201
  }
1184
1202
  }
@@ -125,6 +125,8 @@ pub fn compute_scored_state(
125
125
  }
126
126
  }
127
127
 
128
+ hunks.retain(|h| !is_secret_path(Path::new(&*h.path)));
129
+
128
130
  if hunks.is_empty() {
129
131
  return Ok(empty_scored_state(root_dir));
130
132
  }
@@ -150,7 +152,7 @@ pub fn compute_scored_state(
150
152
  .into_iter()
151
153
  .filter(|f| {
152
154
  let resolved = f.canonicalize().unwrap_or_else(|_| f.clone());
153
- !excluded.contains(&resolved)
155
+ !excluded.contains(&resolved) && !is_secret_path(f)
154
156
  })
155
157
  .collect();
156
158
 
@@ -178,6 +180,7 @@ pub fn compute_scored_state(
178
180
  &preferred_revs,
179
181
  &mut seen_frag_ids,
180
182
  Some(&mut batch_reader),
183
+ true,
181
184
  );
182
185
 
183
186
  let t_parse_changed = Instant::now();
@@ -237,6 +240,7 @@ pub fn compute_scored_state(
237
240
  &preferred_revs,
238
241
  &mut seen_frag_ids,
239
242
  Some(&mut batch_reader),
243
+ false,
240
244
  ));
241
245
 
242
246
  let t_parse_discovered = Instant::now();
@@ -337,7 +341,7 @@ pub fn select_with_params(
337
341
  .all_fragments
338
342
  .iter()
339
343
  .filter(|f| state.core_ids.contains(&f.id))
340
- .map(|f| f.token_count)
344
+ .map(|f| f.token_count.min(BUDGET.core_token_cap_per_fragment))
341
345
  .sum();
342
346
  let auto = (core_tokens as f64 * BUDGET.auto_multiplier) as u32;
343
347
  auto.clamp(BUDGET.auto_min, BUDGET.auto_max)
@@ -470,6 +474,25 @@ pub fn select_with_params(
470
474
 
471
475
  /// Special path for `--full` mode: bypass scoring entirely, return all
472
476
  /// changed-file fragments. Doesn't share the `ScoredState` plumbing.
477
+ /// Private-key and keystore files must never reach LLM-bound diff context, even
478
+ /// when they appear in the diff hunks — such material is never legitimate change
479
+ /// context. Mirrors the Python tree-mode default ignores (`ignore.py`
480
+ /// DEFAULT_IGNORE_PATTERNS). Matches by file name only, so public keys (`*.pub`)
481
+ /// stay visible. `.env` files are intentionally NOT excluded here: a changed
482
+ /// `.env` is legitimate change context (see the `*_env_file_change` cases).
483
+ fn is_secret_path(path: &Path) -> bool {
484
+ let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
485
+ return false;
486
+ };
487
+ if matches!(name, "id_rsa" | "id_dsa" | "id_ecdsa" | "id_ed25519") {
488
+ return true;
489
+ }
490
+ matches!(
491
+ path.extension().and_then(|e| e.to_str()),
492
+ Some("pem" | "key" | "pfx" | "p12" | "keystore" | "jks")
493
+ )
494
+ }
495
+
473
496
  fn build_diff_context_full(
474
497
  root_dir: &Path,
475
498
  diff_range: Option<&str>,
@@ -484,11 +507,13 @@ fn build_diff_context_full(
484
507
  if !git::is_git_repo(&root_dir) {
485
508
  anyhow::bail!("'{}' is not a git repository", root_dir.display());
486
509
  }
487
- let hunks = git::parse_diff(&root_dir, diff_range)?;
510
+ let mut hunks = git::parse_diff(&root_dir, diff_range)?;
511
+ hunks.retain(|h| !is_secret_path(Path::new(&*h.path)));
488
512
  if hunks.is_empty() {
489
513
  return Ok(empty_output(&root_dir));
490
514
  }
491
515
  let mut changed_files = git::get_changed_files(&root_dir, diff_range)?;
516
+ changed_files.retain(|f| !is_secret_path(f));
492
517
  if changed_files.is_empty() {
493
518
  return Ok(empty_output(&root_dir));
494
519
  }
@@ -504,6 +529,7 @@ fn build_diff_context_full(
504
529
  &preferred_revs,
505
530
  &mut seen_frag_ids,
506
531
  Some(&mut batch_reader),
532
+ true,
507
533
  );
508
534
  assign_token_counts(&mut all_fragments);
509
535
  let mut sig_frags = generate_signature_variants(&all_fragments);
@@ -74,6 +74,7 @@ pub fn build_project_graph_with_options(
74
74
  &[],
75
75
  &mut seen_frag_ids,
76
76
  Some(&mut batch_reader),
77
+ false,
77
78
  );
78
79
  batch_reader.close();
79
80
  frags
@@ -84,6 +85,7 @@ pub fn build_project_graph_with_options(
84
85
  &[],
85
86
  &mut seen_frag_ids,
86
87
  None,
88
+ false,
87
89
  )
88
90
  };
89
91
 
@@ -72,7 +72,10 @@ impl PartialOrd for HeapEntry {
72
72
 
73
73
  impl Ord for HeapEntry {
74
74
  fn cmp(&self, other: &Self) -> Ordering {
75
- other.neg_density.total_cmp(&self.neg_density)
75
+ other
76
+ .neg_density
77
+ .total_cmp(&self.neg_density)
78
+ .then_with(|| other.frag_id.cmp(&self.frag_id))
76
79
  }
77
80
  }
78
81
 
@@ -1,11 +1,11 @@
1
1
  # pyproject.toml
2
2
  [build-system]
3
3
  build-backend = "maturin"
4
- requires = [ "maturin>=1.10,<1.14" ]
4
+ requires = [ "maturin>=1.10,<1.15" ]
5
5
 
6
6
  [project]
7
7
  name = "diffctx"
8
- version = "1.9.1"
8
+ version = "1.10.0"
9
9
  description = "Export codebase structure and contents for AI/LLM context"
10
10
  readme = "README.md"
11
11
  keywords = [
@@ -124,7 +124,7 @@ features = [ "pyo3/extension-module", "python" ]
124
124
  # contains "version" and the explicit `[project].version` line is removed
125
125
  # (PEP 621). On the next release, replace `version = "..."` in [project] with
126
126
  # `dynamic = ["version"]` to activate single-sourcing. Until then, keep the
127
- # two versions manually aligned (Cargo.toml = pyproject.toml = 1.7.0).
127
+ # two versions manually aligned (Cargo.toml = pyproject.toml = 1.9.2).
128
128
  include = [
129
129
  { path = "LICENSE", format = "sdist" },
130
130
  { path = "README.md", format = "sdist" },
@@ -13,7 +13,7 @@ logger = logging.getLogger(__name__)
13
13
 
14
14
  DEFAULT_MAX_FILE_BYTES = 256 * 1024 # 256 KB
15
15
  _DEFAULT_ALPHA = 0.60
16
- _DEFAULT_TAU = 0.08
16
+ _DEFAULT_TAU = 0.12
17
17
 
18
18
 
19
19
  class _Unset:
@@ -346,7 +346,7 @@ def _build_main_parser(prog: str = "diffctx", version: str = __version__) -> arg
346
346
  type=int,
347
347
  default=_UNSET,
348
348
  metavar="N",
349
- help="Token budget: 0=auto (default), -1=unlimited, N=fixed budget",
349
+ help="Token budget: omit or 0 = auto (default), -1 = unlimited, N = fixed cap",
350
350
  )
351
351
  diff_group.add_argument(
352
352
  "--alpha",
@@ -360,7 +360,7 @@ def _build_main_parser(prog: str = "diffctx", version: str = __version__) -> arg
360
360
  type=float,
361
361
  default=_UNSET,
362
362
  metavar="F",
363
- help="Minimum relevance to include a fragment (default: 0.08, lower = more context)",
363
+ help="Minimum relevance to include a fragment (default: 0.12, typical 0-1; lower = more context, >1 keeps only changed code)",
364
364
  )
365
365
  diff_group.add_argument(
366
366
  "--scoring",
@@ -46,7 +46,7 @@ def compute_scored_state(
46
46
  def select_with_params(
47
47
  state: Any,
48
48
  budget_tokens: int | None = None,
49
- tau: float = 0.08,
49
+ tau: float = 0.12,
50
50
  no_content: bool = False,
51
51
  ) -> dict[str, Any]:
52
52
  """Light-phase select+postpass+render against a precomputed state."""
@@ -65,7 +65,7 @@ def build_diff_context(
65
65
  diff_range: str,
66
66
  budget_tokens: int | None = None,
67
67
  alpha: float = 0.60,
68
- tau: float = 0.08,
68
+ tau: float = 0.12,
69
69
  no_content: bool = False,
70
70
  ignore_file: Path | None = None,
71
71
  no_default_ignores: bool = False,