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.
- diffctx-1.10.0/CHANGELOG.md +200 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/PKG-INFO +7 -4
- {diffctx-1.8.1 → diffctx-1.10.0}/README.md +6 -3
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/Cargo.lock +1 -1
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/Cargo.toml +1 -1
- diffctx-1.10.0/diffctx/src/config/budget.rs +30 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/limits.rs +8 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/python.rs +7 -5
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/structural/testing.rs +1 -1
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/filtering.rs +4 -4
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/fragmentation.rs +20 -5
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/git.rs +40 -2
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/graph.rs +5 -1
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/interval.rs +6 -4
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/memory_pipeline.rs +16 -1
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/parsers/tree_sitter_strategy.rs +19 -1
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/pipeline.rs +92 -5
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/postpass.rs +7 -2
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/project_graph.rs +2 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/pybridge.rs +23 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/render.rs +106 -11
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/select.rs +4 -1
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/signatures.rs +5 -1
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/tokenizer.rs +1 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/pyproject.toml +3 -3
- {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/cli.py +3 -3
- {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/diffctx/pipeline.py +2 -2
- {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/ignore.py +14 -0
- diffctx-1.10.0/src/diffctx/mcp/README.md +142 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/mcp/server.py +15 -8
- diffctx-1.10.0/src/diffctx/version.py +1 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/writer.py +23 -0
- diffctx-1.8.1/CHANGELOG.md +0 -97
- diffctx-1.8.1/diffctx/src/config/budget.rs +0 -21
- diffctx-1.8.1/src/diffctx/mcp/README.md +0 -104
- diffctx-1.8.1/src/diffctx/version.py +0 -1
- {diffctx-1.8.1 → diffctx-1.10.0}/LICENSE +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/analytics.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/candidate_files.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/analytics.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/bm25.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/category_weights.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/edge_weights.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/env_overrides.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/extensions.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/filtering.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/fragmentation.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/git.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/graph_filtering.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/importance.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/mod.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/mode.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/needs.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/parsers.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/render.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/scoring.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/selection.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/tokenization.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/config/weights.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/core.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/discovery.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/base.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/config_edges/build_system.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/config_edges/cicd.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/config_edges/docker.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/config_edges/generic.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/config_edges/helm.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/config_edges/kubernetes.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/config_edges/mod.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/document/mod.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/history/cochange.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/history/mod.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/mod.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/ansible.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/bazel.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/c_family.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/cargo_edges.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/clojure.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/css.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/dart.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/dbt.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/dotnet.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/elixir.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/erlang.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/go.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/graphql.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/haskell.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/javascript.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/julia.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/jvm.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/latex.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/lua.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/mod.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/nim.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/nix.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/ocaml.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/openapi.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/perl.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/php.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/prisma.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/protobuf.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/r_lang.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/ruby.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/rust_lang.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/shell.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/sql.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/swift.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/tags.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/terraform.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/semantic/zig.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/similarity/lexical.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/similarity/mod.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/structural/containment.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/structural/mod.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/edges/structural/sibling.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/graph_export.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/languages.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/lib.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/main.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/mode.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/parsers/config_parser.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/parsers/generic.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/parsers/markdown.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/parsers/mod.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/ppr.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/scoring.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/stopwords.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/test_harness.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/types.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/utility/boltzmann.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/utility/importance.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/utility/mod.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/utility/needs.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/src/utility/scoring.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/tests/common/mod.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/tests/fixtures/garbage/garbage_api.py +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/tests/fixtures/garbage/garbage_constants.py +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/tests/fixtures/garbage/garbage_handlers.py +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/tests/fixtures/garbage/garbage_models.py +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/tests/fixtures/garbage/garbage_module.js +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/tests/fixtures/garbage/garbage_services.py +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/tests/fixtures/garbage/garbage_types.py +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/tests/fixtures/garbage/garbage_unrelated.yaml +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/tests/fixtures/garbage/garbage_utils.py +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/tests/fixtures/garbage/garbage_validators.py +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/diffctx/tests/yaml_cases.rs +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/rust-toolchain.toml +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/__init__.py +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/__main__.py +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/clipboard.py +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/diffctx/__init__.py +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/diffctx/graph_analytics.py +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/diffctx/graph_export.py +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/diffctx/project_graph.py +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/logger.py +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/main.py +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/mcp/__init__.py +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/mcp/__main__.py +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/mcp/formatting.py +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/mcp/security.py +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/py.typed +0 -0
- {diffctx-1.8.1 → diffctx-1.10.0}/src/diffctx/tokens.py +0 -0
- {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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
```
|
|
@@ -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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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,
|
|
147
|
-
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
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 >
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
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
|
-
|
|
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,
|