diffctx 1.10.2__tar.gz → 1.11.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.2 → diffctx-1.11.0}/CHANGELOG.md +12 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/PKG-INFO +16 -4
- {diffctx-1.10.2 → diffctx-1.11.0}/README.md +15 -3
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/Cargo.lock +3 -3
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/Cargo.toml +1 -1
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/analytics.rs +6 -8
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/config/limits.rs +4 -1
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/config/needs.rs +4 -2
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/config/scoring.rs +2 -2
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/mod.rs +49 -31
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/filtering.rs +8 -8
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/fragmentation.rs +12 -1
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/git.rs +50 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/graph.rs +375 -92
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/graph_export.rs +9 -12
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/memory_pipeline.rs +4 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/pipeline.rs +108 -8
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/postpass.rs +2 -4
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/project_graph.rs +4 -7
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/pybridge.rs +33 -13
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/render.rs +54 -7
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/scoring.rs +4 -6
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/select.rs +15 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/utility/boltzmann.rs +2 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/pyproject.toml +3 -3
- {diffctx-1.10.2 → diffctx-1.11.0}/src/diffctx/cli.py +8 -3
- {diffctx-1.10.2 → diffctx-1.11.0}/src/diffctx/diffctx/pipeline.py +21 -2
- {diffctx-1.10.2 → diffctx-1.11.0}/src/diffctx/main.py +8 -1
- {diffctx-1.10.2 → diffctx-1.11.0}/src/diffctx/mcp/README.md +6 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/src/diffctx/mcp/server.py +35 -0
- diffctx-1.11.0/src/diffctx/version.py +1 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/src/diffctx/writer.py +54 -17
- diffctx-1.10.2/src/diffctx/version.py +0 -1
- {diffctx-1.10.2 → diffctx-1.11.0}/LICENSE +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/candidate_files.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/config/analytics.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/config/bm25.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/config/budget.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/config/category_weights.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/config/edge_weights.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/config/env_overrides.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/config/extensions.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/config/filtering.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/config/fragmentation.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/config/git.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/config/graph_filtering.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/config/importance.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/config/mod.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/config/mode.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/config/parsers.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/config/render.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/config/selection.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/config/tokenization.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/config/weights.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/core.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/discovery.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/base.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/config_edges/build_system.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/config_edges/cicd.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/config_edges/docker.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/config_edges/generic.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/config_edges/helm.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/config_edges/kubernetes.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/config_edges/mod.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/document/mod.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/history/cochange.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/history/mod.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/ansible.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/bazel.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/c_family.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/cargo_edges.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/clojure.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/css.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/dart.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/dbt.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/dotnet.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/elixir.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/erlang.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/go.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/graphql.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/haskell.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/javascript.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/julia.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/jvm.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/latex.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/lua.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/mod.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/nim.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/nix.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/ocaml.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/openapi.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/perl.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/php.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/prisma.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/protobuf.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/python.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/r_lang.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/ruby.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/rust_lang.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/shell.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/sql.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/swift.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/tags.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/terraform.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/semantic/zig.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/similarity/lexical.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/similarity/mod.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/structural/containment.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/structural/mod.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/structural/sibling.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/edges/structural/testing.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/interval.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/languages.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/lib.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/main.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/mode.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/parsers/config_parser.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/parsers/generic.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/parsers/markdown.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/parsers/mod.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/parsers/tree_sitter_strategy.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/ppr.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/signatures.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/stopwords.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/test_harness.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/tokenizer.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/types.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/utility/importance.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/utility/mod.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/utility/needs.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/src/utility/scoring.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/tests/common/mod.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/tests/fixtures/garbage/garbage_api.py +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/tests/fixtures/garbage/garbage_constants.py +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/tests/fixtures/garbage/garbage_handlers.py +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/tests/fixtures/garbage/garbage_models.py +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/tests/fixtures/garbage/garbage_module.js +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/tests/fixtures/garbage/garbage_services.py +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/tests/fixtures/garbage/garbage_types.py +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/tests/fixtures/garbage/garbage_unrelated.yaml +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/tests/fixtures/garbage/garbage_utils.py +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/tests/fixtures/garbage/garbage_validators.py +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/diffctx/tests/yaml_cases.rs +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/rust-toolchain.toml +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/src/diffctx/__init__.py +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/src/diffctx/__main__.py +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/src/diffctx/clipboard.py +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/src/diffctx/diffctx/__init__.py +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/src/diffctx/diffctx/graph_analytics.py +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/src/diffctx/diffctx/graph_export.py +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/src/diffctx/diffctx/project_graph.py +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/src/diffctx/ignore.py +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/src/diffctx/logger.py +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/src/diffctx/mcp/__init__.py +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/src/diffctx/mcp/__main__.py +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/src/diffctx/mcp/formatting.py +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/src/diffctx/mcp/security.py +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/src/diffctx/py.typed +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/src/diffctx/tokens.py +0 -0
- {diffctx-1.10.2 → diffctx-1.11.0}/src/diffctx/tree.py +0 -0
|
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.11.0] - 2026-07-07
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **The default `--format` is now `md` (Markdown), previously `yaml`.** Markdown
|
|
15
|
+
is ~7% more token-efficient than YAML on real diffs and is preferred by
|
|
16
|
+
reviewers for code-fragment-heavy output (evidence:
|
|
17
|
+
`benchmarks/real_world_diff_bench/`). YAML remains available via `-f yaml`.
|
|
18
|
+
This also changes the default stdout format of `treemapper` (a passthrough
|
|
19
|
+
wrapper over this engine); downstream scripts that parse the default output
|
|
20
|
+
must now pass `-f yaml` explicitly. (#104)
|
|
21
|
+
|
|
10
22
|
## [1.10.0] - 2026-06-20
|
|
11
23
|
|
|
12
24
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: diffctx
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.11.0
|
|
4
4
|
Classifier: Development Status :: 5 - Production/Stable
|
|
5
5
|
Classifier: Environment :: Console
|
|
6
6
|
Classifier: Intended Audience :: Developers
|
|
@@ -101,9 +101,9 @@ lines outward and stops as soon as additional context stops paying for itself.
|
|
|
101
101
|
## Install (30 seconds)
|
|
102
102
|
|
|
103
103
|
```bash
|
|
104
|
-
|
|
104
|
+
uvx diffctx . --diff HEAD~1 # zero-install, run once via uv
|
|
105
|
+
pipx install diffctx # recommended for repeat use — isolated CLI, no venv needed
|
|
105
106
|
pip install diffctx # or: into an active environment
|
|
106
|
-
pipx install 'diffctx[tree-sitter]' # + AST parsing for smarter diff context
|
|
107
107
|
pipx install 'diffctx[mcp]' # + MCP server for AI assistants
|
|
108
108
|
```
|
|
109
109
|
|
|
@@ -201,7 +201,7 @@ diffctx graph . --level file -f graphml -o g.xml # file-level graph as GraphML
|
|
|
201
201
|
<!-- BEGIN USAGE -->
|
|
202
202
|
```bash
|
|
203
203
|
# full codebase export:
|
|
204
|
-
diffctx . #
|
|
204
|
+
diffctx . # Markdown to stdout + token count
|
|
205
205
|
diffctx . -f md -c # Markdown → clipboard
|
|
206
206
|
diffctx . -f json -o tree.json # JSON → file
|
|
207
207
|
diffctx . --no-content # structure only, no file contents
|
|
@@ -356,6 +356,18 @@ Auto-discovered files:
|
|
|
356
356
|
- `.diffctx/ignore` — diffctx-specific ignore patterns
|
|
357
357
|
- `.diffctx/whitelist` — Include-only filter (only matched files included)
|
|
358
358
|
|
|
359
|
+
## Exit Codes
|
|
360
|
+
|
|
361
|
+
| Code | Meaning |
|
|
362
|
+
|------|---------|
|
|
363
|
+
| `0` | Success — output contains content |
|
|
364
|
+
| `1` | Runtime error (bad path, permission denied, etc.) |
|
|
365
|
+
| `2` | Usage error (invalid flags/arguments) |
|
|
366
|
+
| `3` | Environment error (`--diff` outside a git repo, git not installed, no commits yet) |
|
|
367
|
+
| `4` | `--diff` produced no semantic context (clean working tree, binary-only, everything filtered out) — output is still emitted, but scripts can detect the empty case. Deletion/rename-only diffs are NOT empty: they list `deleted_files`/`renamed_files` and exit `0` |
|
|
368
|
+
| `130`| Interrupted (Ctrl-C) |
|
|
369
|
+
| `141`| Broken pipe (e.g. piping into `head`) |
|
|
370
|
+
|
|
359
371
|
## Content Placeholders
|
|
360
372
|
|
|
361
373
|
- `<file too large: N bytes>` — exceeds `--max-file-bytes`
|
|
@@ -24,9 +24,9 @@ lines outward and stops as soon as additional context stops paying for itself.
|
|
|
24
24
|
## Install (30 seconds)
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
|
-
|
|
27
|
+
uvx diffctx . --diff HEAD~1 # zero-install, run once via uv
|
|
28
|
+
pipx install diffctx # recommended for repeat use — isolated CLI, no venv needed
|
|
28
29
|
pip install diffctx # or: into an active environment
|
|
29
|
-
pipx install 'diffctx[tree-sitter]' # + AST parsing for smarter diff context
|
|
30
30
|
pipx install 'diffctx[mcp]' # + MCP server for AI assistants
|
|
31
31
|
```
|
|
32
32
|
|
|
@@ -124,7 +124,7 @@ diffctx graph . --level file -f graphml -o g.xml # file-level graph as GraphML
|
|
|
124
124
|
<!-- BEGIN USAGE -->
|
|
125
125
|
```bash
|
|
126
126
|
# full codebase export:
|
|
127
|
-
diffctx . #
|
|
127
|
+
diffctx . # Markdown to stdout + token count
|
|
128
128
|
diffctx . -f md -c # Markdown → clipboard
|
|
129
129
|
diffctx . -f json -o tree.json # JSON → file
|
|
130
130
|
diffctx . --no-content # structure only, no file contents
|
|
@@ -279,6 +279,18 @@ Auto-discovered files:
|
|
|
279
279
|
- `.diffctx/ignore` — diffctx-specific ignore patterns
|
|
280
280
|
- `.diffctx/whitelist` — Include-only filter (only matched files included)
|
|
281
281
|
|
|
282
|
+
## Exit Codes
|
|
283
|
+
|
|
284
|
+
| Code | Meaning |
|
|
285
|
+
|------|---------|
|
|
286
|
+
| `0` | Success — output contains content |
|
|
287
|
+
| `1` | Runtime error (bad path, permission denied, etc.) |
|
|
288
|
+
| `2` | Usage error (invalid flags/arguments) |
|
|
289
|
+
| `3` | Environment error (`--diff` outside a git repo, git not installed, no commits yet) |
|
|
290
|
+
| `4` | `--diff` produced no semantic context (clean working tree, binary-only, everything filtered out) — output is still emitted, but scripts can detect the empty case. Deletion/rename-only diffs are NOT empty: they list `deleted_files`/`renamed_files` and exit `0` |
|
|
291
|
+
| `130`| Interrupted (Ctrl-C) |
|
|
292
|
+
| `141`| Broken pipe (e.g. piping into `head`) |
|
|
293
|
+
|
|
282
294
|
## Content Placeholders
|
|
283
295
|
|
|
284
296
|
- `<file too large: N bytes>` — exceeds `--max-file-bytes`
|
|
@@ -63,9 +63,9 @@ dependencies = [
|
|
|
63
63
|
|
|
64
64
|
[[package]]
|
|
65
65
|
name = "anyhow"
|
|
66
|
-
version = "1.0.
|
|
66
|
+
version = "1.0.103"
|
|
67
67
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
68
|
-
checksum = "
|
|
68
|
+
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
|
|
69
69
|
|
|
70
70
|
[[package]]
|
|
71
71
|
name = "base64"
|
|
@@ -194,7 +194,7 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
|
|
194
194
|
|
|
195
195
|
[[package]]
|
|
196
196
|
name = "diffctx"
|
|
197
|
-
version = "1.
|
|
197
|
+
version = "1.11.0"
|
|
198
198
|
dependencies = [
|
|
199
199
|
"anyhow",
|
|
200
200
|
"clap",
|
|
@@ -190,9 +190,7 @@ pub fn quotient_graph(
|
|
|
190
190
|
None => return,
|
|
191
191
|
};
|
|
192
192
|
let cat = graph
|
|
193
|
-
.
|
|
194
|
-
.get(&(src.clone(), dst.clone()))
|
|
195
|
-
.copied()
|
|
193
|
+
.edge_category(src, dst)
|
|
196
194
|
.unwrap_or(EdgeCategory::Generic);
|
|
197
195
|
|
|
198
196
|
if src_key == dst_key {
|
|
@@ -415,15 +413,15 @@ pub fn hotspots(
|
|
|
415
413
|
}
|
|
416
414
|
|
|
417
415
|
let mut out_deg: FxHashMap<Arc<str>, u32> = FxHashMap::default();
|
|
418
|
-
|
|
416
|
+
graph.for_each_categorized_edge(|src, _dst, cat| {
|
|
419
417
|
if let Some(filter) = edge_types
|
|
420
|
-
&& !filter.contains(cat)
|
|
418
|
+
&& !filter.contains(&cat)
|
|
421
419
|
{
|
|
422
|
-
|
|
420
|
+
return;
|
|
423
421
|
}
|
|
424
422
|
let rel: Arc<str> = Arc::from(relative_path(src.path.as_ref(), root));
|
|
425
423
|
*out_deg.entry(rel).or_insert(0) += 1;
|
|
426
|
-
}
|
|
424
|
+
});
|
|
427
425
|
|
|
428
426
|
let max_deg = out_deg.values().copied().max().unwrap_or(0).max(1);
|
|
429
427
|
let max_churn = churn
|
|
@@ -601,7 +599,7 @@ mod tests {
|
|
|
601
599
|
}
|
|
602
600
|
for (s, d, w, c) in edges {
|
|
603
601
|
g.add_edge(s.clone(), d.clone(), *w);
|
|
604
|
-
g.
|
|
602
|
+
g.insert_edge_category(s.clone(), d.clone(), *c);
|
|
605
603
|
}
|
|
606
604
|
g.freeze();
|
|
607
605
|
g
|
|
@@ -28,7 +28,10 @@ impl Default for AlgorithmLimits {
|
|
|
28
28
|
max_generated_lines: 30,
|
|
29
29
|
skip_expensive_threshold: 2000,
|
|
30
30
|
rare_identifier_threshold: 3,
|
|
31
|
-
|
|
31
|
+
// Measured against real YAML serialization (path/lines/kind/symbol
|
|
32
|
+
// keys + quoting): actual scaffold runs ~40-45 tokens/fragment,
|
|
33
|
+
// not 18 - the old estimate let --budget overshoot by ~18%.
|
|
34
|
+
overhead_per_fragment: 40,
|
|
32
35
|
}
|
|
33
36
|
}
|
|
34
37
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
use once_cell::sync::Lazy;
|
|
2
2
|
|
|
3
|
+
use super::env_overrides::read_env_fraction;
|
|
4
|
+
|
|
3
5
|
pub struct NeedsConfig {
|
|
4
6
|
pub min_symbol_length: usize,
|
|
5
7
|
pub background_min_ident_length: usize,
|
|
@@ -54,8 +56,8 @@ impl Default for NeedsConfig {
|
|
|
54
56
|
test_mentions: 0.6,
|
|
55
57
|
mentions_fallback: 0.3,
|
|
56
58
|
|
|
57
|
-
min_rel_for_bonus: 0.03,
|
|
58
|
-
relatedness_bonus: 0.25,
|
|
59
|
+
min_rel_for_bonus: read_env_fraction("DIFFCTX_MIN_REL_FOR_BONUS", 0.03),
|
|
60
|
+
relatedness_bonus: read_env_fraction("DIFFCTX_RELATEDNESS_BONUS", 0.25),
|
|
59
61
|
}
|
|
60
62
|
}
|
|
61
63
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
use once_cell::sync::Lazy;
|
|
2
2
|
|
|
3
|
-
use super::env_overrides::read_env_open_fraction;
|
|
3
|
+
use super::env_overrides::{read_env_fraction, read_env_open_fraction};
|
|
4
4
|
|
|
5
5
|
pub struct EgoScoringConfig {
|
|
6
6
|
pub identifier_overlap_epsilon: f64,
|
|
@@ -11,7 +11,7 @@ pub struct EgoScoringConfig {
|
|
|
11
11
|
impl Default for EgoScoringConfig {
|
|
12
12
|
fn default() -> Self {
|
|
13
13
|
Self {
|
|
14
|
-
identifier_overlap_epsilon: 0.1,
|
|
14
|
+
identifier_overlap_epsilon: read_env_fraction("DIFFCTX_EGO_LEXICAL_EPS", 0.1),
|
|
15
15
|
identifier_overlap_cap: 10,
|
|
16
16
|
per_hop_decay: read_env_open_fraction("DIFFCTX_EGO_PER_HOP_DECAY", 0.5),
|
|
17
17
|
}
|
|
@@ -12,7 +12,9 @@ use rayon::prelude::*;
|
|
|
12
12
|
use rustc_hash::FxHashMap;
|
|
13
13
|
use tracing::debug;
|
|
14
14
|
|
|
15
|
-
use crate::graph::
|
|
15
|
+
use crate::graph::{
|
|
16
|
+
CompactEdge, CompactEdges, EdgeCategory, dedup_compact_edges, intern_fragment_nodes,
|
|
17
|
+
};
|
|
16
18
|
use crate::types::FragmentId;
|
|
17
19
|
|
|
18
20
|
pub type EdgeDict = FxHashMap<(FragmentId, FragmentId), f64>;
|
|
@@ -66,11 +68,18 @@ pub fn get_all_builders() -> Vec<Box<dyn EdgeBuilder>> {
|
|
|
66
68
|
all
|
|
67
69
|
}
|
|
68
70
|
|
|
71
|
+
/// Collect edges from all builders directly into the interned compact
|
|
72
|
+
/// representation. Each builder's FragmentId-keyed map is converted and
|
|
73
|
+
/// dropped inside the parallel closure, so at no point does the full
|
|
74
|
+
/// edge universe exist as string-keyed hashmaps — the peak that used to
|
|
75
|
+
/// OOM-kill multi-million-edge instances. Merge semantics match the
|
|
76
|
+
/// historical maps exactly: max weight across builders, category from
|
|
77
|
+
/// the first builder (in registration order) that produced the edge.
|
|
69
78
|
pub fn collect_all_edges(
|
|
70
79
|
fragments: &[Fragment],
|
|
71
80
|
repo_root: Option<&Path>,
|
|
72
81
|
skip_expensive: bool,
|
|
73
|
-
) ->
|
|
82
|
+
) -> CompactEdges {
|
|
74
83
|
let mut all_builders: Vec<(&str, Box<dyn EdgeBuilder>)> = Vec::new();
|
|
75
84
|
for cat in builder_categories() {
|
|
76
85
|
if skip_expensive && EXPENSIVE_CATEGORIES.contains(&cat.name) {
|
|
@@ -82,39 +91,48 @@ pub fn collect_all_edges(
|
|
|
82
91
|
}
|
|
83
92
|
}
|
|
84
93
|
|
|
94
|
+
let (node_to_idx, idx_to_node) = intern_fragment_nodes(fragments);
|
|
85
95
|
let category_weights = *crate::config::category_weights::CATEGORY_WEIGHTS;
|
|
86
|
-
let per_builder_edges: Vec<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
let
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
all_edges.insert((src.clone(), dst.clone()), weight);
|
|
96
|
+
let per_builder_edges: Vec<Vec<CompactEdge>> = all_builders
|
|
97
|
+
.par_iter()
|
|
98
|
+
.map(|(cat_name, builder)| {
|
|
99
|
+
let cat_label = builder.category_label().unwrap_or(cat_name);
|
|
100
|
+
let category = EdgeCategory::from_str(cat_label);
|
|
101
|
+
let multiplier = category_weights.multiplier(category);
|
|
102
|
+
let edges = builder.build(fragments, repo_root);
|
|
103
|
+
let mut compact = Vec::with_capacity(edges.len());
|
|
104
|
+
for ((src, dst), weight) in edges {
|
|
105
|
+
let s = match node_to_idx.get(&src) {
|
|
106
|
+
Some(&i) => i,
|
|
107
|
+
None => continue,
|
|
108
|
+
};
|
|
109
|
+
let d = match node_to_idx.get(&dst) {
|
|
110
|
+
Some(&i) => i,
|
|
111
|
+
None => continue,
|
|
112
|
+
};
|
|
113
|
+
compact.push(CompactEdge {
|
|
114
|
+
src: s,
|
|
115
|
+
dst: d,
|
|
116
|
+
weight: weight * multiplier,
|
|
117
|
+
category,
|
|
118
|
+
});
|
|
110
119
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
120
|
+
compact
|
|
121
|
+
})
|
|
122
|
+
.collect();
|
|
123
|
+
|
|
124
|
+
let total: usize = per_builder_edges.iter().map(|v| v.len()).sum();
|
|
125
|
+
let mut edges: Vec<CompactEdge> = Vec::with_capacity(total);
|
|
126
|
+
for v in per_builder_edges {
|
|
127
|
+
edges.extend(v);
|
|
115
128
|
}
|
|
129
|
+
dedup_compact_edges(&mut edges);
|
|
116
130
|
|
|
117
|
-
|
|
131
|
+
CompactEdges {
|
|
132
|
+
node_to_idx,
|
|
133
|
+
idx_to_node,
|
|
134
|
+
edges,
|
|
135
|
+
}
|
|
118
136
|
}
|
|
119
137
|
|
|
120
138
|
pub fn discover_all_related_files(
|
|
@@ -85,14 +85,14 @@ fn classify_semantic_edges(
|
|
|
85
85
|
let mut reverse_deps: FxHashMap<Arc<str>, FxHashSet<Arc<str>>> = FxHashMap::default();
|
|
86
86
|
let mut direct_edge_paths: FxHashSet<Arc<str>> = FxHashSet::default();
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
if
|
|
90
|
-
|
|
88
|
+
graph.for_each_categorized_edge(|src, dst, category| {
|
|
89
|
+
if category != EdgeCategory::Semantic {
|
|
90
|
+
return;
|
|
91
91
|
}
|
|
92
92
|
let src_changed = changed_paths.contains(&src.path);
|
|
93
93
|
let dst_changed = changed_paths.contains(&dst.path);
|
|
94
94
|
if !(src_changed ^ dst_changed) {
|
|
95
|
-
|
|
95
|
+
return;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
let (changed_frag, other_frag) = if src_changed { (src, dst) } else { (dst, src) };
|
|
@@ -112,7 +112,7 @@ fn classify_semantic_edges(
|
|
|
112
112
|
} else {
|
|
113
113
|
direct_edge_paths.insert(other_frag.path.clone());
|
|
114
114
|
}
|
|
115
|
-
}
|
|
115
|
+
});
|
|
116
116
|
|
|
117
117
|
(reverse_deps, direct_edge_paths)
|
|
118
118
|
}
|
|
@@ -172,11 +172,11 @@ fn find_config_generic_code_files(
|
|
|
172
172
|
})
|
|
173
173
|
.collect();
|
|
174
174
|
|
|
175
|
-
|
|
175
|
+
graph.for_each_categorized_edge(|src, dst, category| {
|
|
176
176
|
let src_changed = changed_paths.contains(&src.path);
|
|
177
177
|
let dst_changed = changed_paths.contains(&dst.path);
|
|
178
178
|
if !(src_changed ^ dst_changed) {
|
|
179
|
-
|
|
179
|
+
return;
|
|
180
180
|
}
|
|
181
181
|
let other_path = if src_changed { &dst.path } else { &src.path };
|
|
182
182
|
match category {
|
|
@@ -189,7 +189,7 @@ fn find_config_generic_code_files(
|
|
|
189
189
|
}
|
|
190
190
|
_ => {}
|
|
191
191
|
}
|
|
192
|
-
}
|
|
192
|
+
});
|
|
193
193
|
|
|
194
194
|
let generic_only: FxHashSet<Arc<str>> = has_generic_config
|
|
195
195
|
.difference(&has_real_edge)
|
|
@@ -293,7 +293,18 @@ pub fn process_files_for_fragments(
|
|
|
293
293
|
// truncation), which can drop the small fragment covering the
|
|
294
294
|
// edited hunk before core identification runs.
|
|
295
295
|
let generated = !is_changed && is_generated_file(file_path, content);
|
|
296
|
-
|
|
296
|
+
// Changed files also get 10x cap headroom: the biggest-N
|
|
297
|
+
// truncation below keeps the LONGEST fragments, and the
|
|
298
|
+
// edited hunk is typically a small leaf that would be
|
|
299
|
+
// dropped first (hunks are only known later, in core
|
|
300
|
+
// identification). max_changed_file_size still bounds cost.
|
|
301
|
+
let cap = if generated {
|
|
302
|
+
max_generated
|
|
303
|
+
} else if is_changed {
|
|
304
|
+
max_frags.saturating_mul(10)
|
|
305
|
+
} else {
|
|
306
|
+
max_frags
|
|
307
|
+
};
|
|
297
308
|
if raw_frags.len() > cap {
|
|
298
309
|
raw_frags.sort_by(|a, b| b.line_count().cmp(&a.line_count()));
|
|
299
310
|
raw_frags.truncate(cap);
|
|
@@ -143,6 +143,21 @@ pub fn is_git_repo(path: &Path) -> bool {
|
|
|
143
143
|
run_git(path, &["rev-parse", "--git-dir"]).is_ok()
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
/// Resolves the actual working-tree root for `path`, which may be a
|
|
147
|
+
/// subdirectory of the repository. `git diff`/`git cat-file` paths are
|
|
148
|
+
/// always reported relative to this root, not to an arbitrary `-C` cwd -
|
|
149
|
+
/// running the pipeline with `path` still set to a subdirectory silently
|
|
150
|
+
/// produces zero fragments because file lookups get double-prefixed
|
|
151
|
+
/// (e.g. `src/src/app.py`).
|
|
152
|
+
pub fn find_toplevel(path: &Path) -> Option<PathBuf> {
|
|
153
|
+
let out = run_git(path, &["rev-parse", "--show-toplevel"]).ok()?;
|
|
154
|
+
let trimmed = out.trim();
|
|
155
|
+
if trimmed.is_empty() {
|
|
156
|
+
return None;
|
|
157
|
+
}
|
|
158
|
+
Some(PathBuf::from(trimmed))
|
|
159
|
+
}
|
|
160
|
+
|
|
146
161
|
pub fn get_diff_text(repo_root: &Path, diff_range: Option<&str>) -> Result<String> {
|
|
147
162
|
let mut args: Vec<&str> = vec!["diff"];
|
|
148
163
|
args.extend_from_slice(SAFE_DIFF_FLAGS);
|
|
@@ -455,6 +470,41 @@ pub fn get_renamed_paths(
|
|
|
455
470
|
Ok((old_paths, pure_new_paths))
|
|
456
471
|
}
|
|
457
472
|
|
|
473
|
+
/// Rename pairs as repo-relative display paths (`old -> new`), for the output
|
|
474
|
+
/// header. Unlike `get_renamed_paths` this preserves the pairing and does not
|
|
475
|
+
/// canonicalize (the old path no longer exists on disk).
|
|
476
|
+
pub fn get_rename_pairs(
|
|
477
|
+
repo_root: &Path,
|
|
478
|
+
diff_range: Option<&str>,
|
|
479
|
+
) -> Result<Vec<(String, String)>> {
|
|
480
|
+
let mut args: Vec<&str> = vec!["diff"];
|
|
481
|
+
args.extend_from_slice(SAFE_DIFF_FLAGS);
|
|
482
|
+
args.extend_from_slice(&["--diff-filter=R", "--name-status", "-M", "-z"]);
|
|
483
|
+
if let Some(range) = diff_range {
|
|
484
|
+
validate_diff_range(range)?;
|
|
485
|
+
args.push(range);
|
|
486
|
+
}
|
|
487
|
+
let output = run_git(repo_root, &args)?;
|
|
488
|
+
let parts: Vec<&str> = output.split('\0').collect();
|
|
489
|
+
|
|
490
|
+
let mut pairs = Vec::new();
|
|
491
|
+
let mut i = 0;
|
|
492
|
+
while i < parts.len() {
|
|
493
|
+
if parts[i].starts_with('R') {
|
|
494
|
+
if i + 2 < parts.len() && !parts[i + 1].is_empty() && !parts[i + 2].is_empty() {
|
|
495
|
+
pairs.push((
|
|
496
|
+
parts[i + 1].replace('\\', "/"),
|
|
497
|
+
parts[i + 2].replace('\\', "/"),
|
|
498
|
+
));
|
|
499
|
+
}
|
|
500
|
+
i += 3;
|
|
501
|
+
} else {
|
|
502
|
+
i += 1;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
Ok(pairs)
|
|
506
|
+
}
|
|
507
|
+
|
|
458
508
|
pub fn split_diff_range(range: &str) -> (Option<String>, Option<String>) {
|
|
459
509
|
match RANGE_RE.captures(range) {
|
|
460
510
|
None => (None, None),
|