diffctx 1.11.0__tar.gz → 1.12.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.11.0 → diffctx-1.12.0}/CHANGELOG.md +80 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/PKG-INFO +79 -194
- diffctx-1.12.0/README.md +197 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/Cargo.lock +16 -16
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/Cargo.toml +4 -3
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/limits.rs +4 -3
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/selection.rs +6 -6
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/discovery.rs +25 -52
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/history/cochange.rs +1 -4
- diffctx-1.12.0/diffctx/src/edges/mod.rs +282 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/git.rs +31 -15
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/graph.rs +199 -50
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/lib.rs +2 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/parsers/tree_sitter_strategy.rs +6 -1
- diffctx-1.12.0/diffctx/src/peak_rss.rs +46 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/pipeline.rs +21 -4
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/postpass.rs +21 -8
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/project_graph.rs +16 -28
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/pybridge.rs +20 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/render.rs +19 -5
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/scoring.rs +17 -4
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/signatures.rs +22 -1
- diffctx-1.12.0/diffctx/src/token_corpus.rs +228 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/types.rs +14 -1
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/tests/yaml_cases.rs +2 -5
- {diffctx-1.11.0 → diffctx-1.12.0}/pyproject.toml +1 -17
- {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/cli.py +210 -56
- diffctx-1.12.0/src/diffctx/diffctx/graph_analytics.py +293 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/diffctx/graph_export.py +16 -8
- {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/diffctx/pipeline.py +1 -14
- diffctx-1.12.0/src/diffctx/diffctx/project_graph.py +36 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/ignore.py +11 -3
- {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/logger.py +3 -2
- {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/main.py +109 -33
- {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/tokens.py +1 -4
- {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/tree.py +11 -0
- diffctx-1.12.0/src/diffctx/version.py +1 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/writer.py +28 -6
- diffctx-1.11.0/README.md +0 -310
- diffctx-1.11.0/diffctx/src/edges/mod.rs +0 -155
- diffctx-1.11.0/src/diffctx/diffctx/graph_analytics.py +0 -59
- diffctx-1.11.0/src/diffctx/diffctx/project_graph.py +0 -27
- diffctx-1.11.0/src/diffctx/version.py +0 -1
- {diffctx-1.11.0 → diffctx-1.12.0}/LICENSE +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/analytics.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/candidate_files.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/analytics.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/bm25.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/budget.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/category_weights.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/edge_weights.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/env_overrides.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/extensions.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/filtering.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/fragmentation.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/git.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/graph_filtering.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/importance.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/mod.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/mode.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/needs.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/parsers.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/render.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/scoring.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/tokenization.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/weights.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/core.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/base.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/config_edges/build_system.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/config_edges/cicd.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/config_edges/docker.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/config_edges/generic.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/config_edges/helm.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/config_edges/kubernetes.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/config_edges/mod.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/document/mod.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/history/mod.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/ansible.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/bazel.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/c_family.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/cargo_edges.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/clojure.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/css.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/dart.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/dbt.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/dotnet.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/elixir.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/erlang.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/go.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/graphql.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/haskell.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/javascript.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/julia.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/jvm.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/latex.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/lua.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/mod.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/nim.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/nix.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/ocaml.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/openapi.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/perl.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/php.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/prisma.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/protobuf.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/python.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/r_lang.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/ruby.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/rust_lang.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/shell.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/sql.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/swift.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/tags.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/terraform.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/zig.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/similarity/lexical.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/similarity/mod.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/structural/containment.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/structural/mod.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/structural/sibling.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/structural/testing.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/filtering.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/fragmentation.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/graph_export.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/interval.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/languages.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/main.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/memory_pipeline.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/mode.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/parsers/config_parser.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/parsers/generic.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/parsers/markdown.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/parsers/mod.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/ppr.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/select.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/stopwords.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/test_harness.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/tokenizer.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/utility/boltzmann.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/utility/importance.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/utility/mod.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/utility/needs.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/utility/scoring.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/tests/common/mod.rs +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/tests/fixtures/garbage/garbage_api.py +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/tests/fixtures/garbage/garbage_constants.py +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/tests/fixtures/garbage/garbage_handlers.py +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/tests/fixtures/garbage/garbage_models.py +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/tests/fixtures/garbage/garbage_module.js +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/tests/fixtures/garbage/garbage_services.py +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/tests/fixtures/garbage/garbage_types.py +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/tests/fixtures/garbage/garbage_unrelated.yaml +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/tests/fixtures/garbage/garbage_utils.py +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/tests/fixtures/garbage/garbage_validators.py +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/rust-toolchain.toml +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/__init__.py +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/__main__.py +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/clipboard.py +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/diffctx/__init__.py +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/mcp/README.md +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/mcp/__init__.py +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/mcp/__main__.py +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/mcp/formatting.py +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/mcp/security.py +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/mcp/server.py +0 -0
- {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/py.typed +0 -0
|
@@ -7,6 +7,86 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.12.0] - 2026-07-23
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `--timeout SECONDS` — wall-clock deadline for `--diff` analysis (default
|
|
15
|
+
300); exceeding it exits `124` instead of hanging indefinitely (#70).
|
|
16
|
+
- `--no-ignores` — turns off every ignore rule (built-in patterns, project
|
|
17
|
+
`.gitignore`, `.diffctx/ignore`). `--no-default-ignores` only disables the
|
|
18
|
+
built-in list; its help now says so. Not supported with `--diff`.
|
|
19
|
+
- Output format is inferred from the `-o` extension when `-f` is omitted, so
|
|
20
|
+
`-o out.json` no longer writes Markdown into a `.json` file; a mismatch
|
|
21
|
+
between `-f` and the extension warns.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- **All error logging was dead.** An import-time `NullHandler` made
|
|
26
|
+
`setup_logging` skip attaching a real handler, so `--log-level` was a no-op
|
|
27
|
+
and all 19 `logger.error/warning/exception` sites were silent — `diffctx . -o
|
|
28
|
+
/bad/path.md` exited 1 with no message at all.
|
|
29
|
+
- **diffctx invoked from inside a git hook silently analyzed the wrong
|
|
30
|
+
repository.** Git exports repo-locating env vars (`GIT_DIR`,
|
|
31
|
+
`GIT_INDEX_FILE`, `GIT_WORK_TREE`, ...) to hook subprocesses; inherited,
|
|
32
|
+
they overrode `-C` on every internal git call. All git spawns now scrub
|
|
33
|
+
these variables (`git_command()` in `git.rs`).
|
|
34
|
+
- YAML output preserved file content byte-exactly except for trailing
|
|
35
|
+
newlines; the block chomping indicator is now chosen per content.
|
|
36
|
+
- Arrow-function fragments bound to variables were never stub-eligible (#106).
|
|
37
|
+
- Decorated definitions rendered as a bare `@decorator` line without the
|
|
38
|
+
`class X:` / `def x():` header.
|
|
39
|
+
- `--max-depth`-pruned directories were labelled `_(empty directory)_` — a
|
|
40
|
+
factual lie to the reader; they now read
|
|
41
|
+
`_(children omitted: --max-depth reached)_` (`truncated: true` in
|
|
42
|
+
YAML/JSON).
|
|
43
|
+
- Mixed directory + glob arguments dropped the glob files' parent path from
|
|
44
|
+
node names.
|
|
45
|
+
- Double Ctrl-C printed a ~60-line traceback.
|
|
46
|
+
- Lock files `uv.lock`, `pdm.lock`, `bun.lock`, `bun.lockb`, `deno.lock` and
|
|
47
|
+
`flake.lock` leaked into output; they now join the other lock files in the
|
|
48
|
+
default ignore patterns.
|
|
49
|
+
- Large-repo hangs/OOM on trivial diffs (#70, #95): discovery no longer
|
|
50
|
+
re-reads and re-tokenizes the whole candidate universe per ensemble
|
|
51
|
+
strategy (one shared pass + a persistent per-blob token cache keyed by
|
|
52
|
+
`(blob OID, tokenizer epoch)`), and edge construction is two-pass with a
|
|
53
|
+
bounded per-source top-K instead of materializing up to tens of millions
|
|
54
|
+
of raw edges before the cap; pass 2 replays a compact 16-byte-per-emission
|
|
55
|
+
log instead of re-running the builders, so generation cost stays 1x.
|
|
56
|
+
Verified: gitpod 8000s-hang -> 35.7s,
|
|
57
|
+
pytorch 1848s-SIGKILL -> 7.4s, mui/material-ui OOM class recovered.
|
|
58
|
+
Outputs are bit-identical (gated by `benchmarks/equivalence_gate.py`).
|
|
59
|
+
|
|
60
|
+
### Known limitations
|
|
61
|
+
|
|
62
|
+
- Near-dense edge emission on huge same-directory trees (observed: 199M
|
|
63
|
+
raw edges, 37GB peak on one mui/material-ui instance) remains expensive
|
|
64
|
+
even with bounded construction; tracked in #116.
|
|
65
|
+
|
|
66
|
+
### Changed
|
|
67
|
+
|
|
68
|
+
- **The token budget is now a hard cap.** The changed-files post-pass no
|
|
69
|
+
longer exceeds the budget to guarantee representation: a changed file
|
|
70
|
+
whose cheapest representative does not fit stays unrepresented (visible
|
|
71
|
+
as changed-file retention < 1). `--budget 0` therefore yields an empty
|
|
72
|
+
selection (use `--full` for changed files only); CLI help updated.
|
|
73
|
+
- Latency telemetry: new `graph_build_ms` phase (graph construction was
|
|
74
|
+
previously misattributed to `scoring_ms`, which now measures pure
|
|
75
|
+
ranking) and `peak_rss_bytes` (in-process peak memory). Release builds
|
|
76
|
+
carry line tables (`debug = "line-tables-only"`) for profiling at no
|
|
77
|
+
runtime cost.
|
|
78
|
+
- CLI diagnostics are honest end to end: an exit-code table in `--help`
|
|
79
|
+
(2 usage, 3 environment, 4 empty diff, 124 timeout), flag-value validation
|
|
80
|
+
exits 2 instead of 1, git failures report a single line plus a
|
|
81
|
+
`git log --oneline` hint on unknown revisions, conflicting flags warn
|
|
82
|
+
(`-q`+`--log-level`, `--full`+selection flags, ...), a failed clipboard
|
|
83
|
+
copy warns before falling back to stdout, and `--tau` / `--scoring` /
|
|
84
|
+
`--alpha` / `--budget 0` help text describes what actually happens.
|
|
85
|
+
- `graph --summary` reports category shares as percentages, suppresses
|
|
86
|
+
degenerate top-referenced lists, detects cycles over dominant-direction
|
|
87
|
+
edges only, derives churn from `git log --since`, and disambiguates
|
|
88
|
+
duplicate mermaid labels to relative paths.
|
|
89
|
+
|
|
10
90
|
## [1.11.0] - 2026-07-07
|
|
11
91
|
|
|
12
92
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: diffctx
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.12.0
|
|
4
4
|
Classifier: Development Status :: 5 - Production/Stable
|
|
5
5
|
Classifier: Environment :: Console
|
|
6
6
|
Classifier: Intended Audience :: Developers
|
|
@@ -36,7 +36,6 @@ Requires-Dist: radon>=6.0,<7.0 ; extra == 'dev'
|
|
|
36
36
|
Requires-Dist: ruff>=0.4,<1.0 ; extra == 'dev'
|
|
37
37
|
Requires-Dist: tiktoken>=0.9,<1.0 ; extra == 'dev'
|
|
38
38
|
Requires-Dist: types-pyyaml>=6.0,<7.0 ; extra == 'dev'
|
|
39
|
-
Requires-Dist: numpy>=1.24,<3.0 ; extra == 'diffctx'
|
|
40
39
|
Requires-Dist: charset-normalizer>=3.0,<4.0 ; extra == 'full'
|
|
41
40
|
Requires-Dist: diffctx[tree-sitter] ; extra == 'full'
|
|
42
41
|
Requires-Dist: anyio>=4.5,<5.0 ; extra == 'mcp'
|
|
@@ -59,7 +58,6 @@ Requires-Dist: tree-sitter-scala>=0.24,<1.0 ; extra == 'tree-sitter'
|
|
|
59
58
|
Requires-Dist: tree-sitter-swift>=0.0.1,<1.0 ; extra == 'tree-sitter'
|
|
60
59
|
Requires-Dist: tree-sitter-typescript>=0.21,<1.0 ; extra == 'tree-sitter'
|
|
61
60
|
Provides-Extra: dev
|
|
62
|
-
Provides-Extra: diffctx
|
|
63
61
|
Provides-Extra: full
|
|
64
62
|
Provides-Extra: mcp
|
|
65
63
|
Provides-Extra: tree-sitter
|
|
@@ -85,6 +83,10 @@ Project-URL: Repository, https://github.com/nikolay-e/diffctx
|
|
|
85
83
|
Instead of pasting whole files, it walks the dependency graph from the changed
|
|
86
84
|
lines outward and stops as soon as additional context stops paying for itself.
|
|
87
85
|
|
|
86
|
+
> Coming from [`treemapper`](https://pypi.org/project/treemapper/)? That name is
|
|
87
|
+
> deprecated — it was a thin wrapper around diffctx. Every command, flag, and
|
|
88
|
+
> API call works unchanged: `treemapper` → `diffctx`, `treemapper-mcp` → `diffctx-mcp`.
|
|
89
|
+
|
|
88
90
|
## Why not just use `tree` or repomix?
|
|
89
91
|
|
|
90
92
|
| | `tree` | repomix | Claude Code Review | **diffctx** |
|
|
@@ -98,90 +100,57 @@ lines outward and stops as soon as additional context stops paying for itself.
|
|
|
98
100
|
| Python API | ✗ | ✗ | ✗ | ✓ |
|
|
99
101
|
| MCP server | ✗ | ✗ | ✗ | ✓ |
|
|
100
102
|
|
|
101
|
-
## Install
|
|
103
|
+
## Install
|
|
102
104
|
|
|
103
105
|
```bash
|
|
104
106
|
uvx diffctx . --diff HEAD~1 # zero-install, run once via uv
|
|
105
|
-
pipx install diffctx # recommended
|
|
107
|
+
pipx install diffctx # recommended: isolated CLI, no venv needed
|
|
106
108
|
pip install diffctx # or: into an active environment
|
|
107
109
|
pipx install 'diffctx[mcp]' # + MCP server for AI assistants
|
|
108
110
|
```
|
|
109
111
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
A standalone binary (no Python required) is on the
|
|
113
|
+
[releases page](https://github.com/nikolay-e/diffctx/releases/latest).
|
|
114
|
+
The `[tree-sitter]` extra adds AST-level parsing for more accurate context
|
|
115
|
+
selection across 30+ languages.
|
|
116
|
+
|
|
117
|
+
## Quick start
|
|
114
118
|
|
|
115
119
|
```bash
|
|
116
120
|
diffctx . --diff HEAD~1 # smart context for last commit → paste into Claude/ChatGPT
|
|
117
|
-
diffctx . -f md -c # full export → clipboard in Markdown
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-

|
|
121
|
-
|
|
122
|
-
*Demo: `diffctx . --diff HEAD~1` selects only the fragments — functions,
|
|
123
|
-
imports, type definitions — that an LLM actually needs to review the last
|
|
124
|
-
commit, instead of dumping every changed file in full.*
|
|
125
|
-
|
|
126
|
-
**Standalone binary** (no Python required): download from the
|
|
127
|
-
[releases page](https://github.com/nikolay-e/diffctx/releases/latest).
|
|
128
|
-
|
|
129
|
-
> Diff context mode works out of the box. Adding `[tree-sitter]` enables AST-level
|
|
130
|
-
> parsing for more accurate context selection across 30+ languages.
|
|
131
|
-
|
|
132
|
-
## Diff Context Mode
|
|
133
|
-
|
|
134
|
-
Automatically finds the minimal set of code fragments needed to understand
|
|
135
|
-
a change — imports, callers, type definitions, config dependencies — without
|
|
136
|
-
dumping entire files. Understands 50+ file types.
|
|
137
|
-
|
|
138
|
-
```yaml
|
|
139
|
-
name: myproject
|
|
140
|
-
type: diff_context
|
|
141
|
-
fragment_count: 5
|
|
142
|
-
fragments:
|
|
143
|
-
- path: src/main.py
|
|
144
|
-
lines: "10-25"
|
|
145
|
-
kind: function
|
|
146
|
-
symbol: process_data
|
|
147
|
-
content: |
|
|
148
|
-
def process_data(items):
|
|
149
|
-
...
|
|
121
|
+
diffctx . -f md -c # full codebase export → clipboard in Markdown
|
|
150
122
|
```
|
|
151
123
|
|
|
152
|
-
|
|
124
|
+

|
|
153
125
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
126
|
+
*`diffctx . --diff HEAD~1` selects only the fragments — functions, imports,
|
|
127
|
+
type definitions — that an LLM actually needs to review the last commit,
|
|
128
|
+
instead of dumping every changed file in full.*
|
|
157
129
|
|
|
158
|
-
|
|
159
|
-
|-------------|-----------------------------------------------------------|
|
|
160
|
-
| `ego` (default) | Bounded ego-network expansion around changed nodes — fast, predictable radius, the current default |
|
|
161
|
-
| `ppr` | Personalized PageRank with damping `--alpha` — global, smoother decay, slower |
|
|
162
|
-
| `bm25` | Lexical fragment retrieval against the diff hunks — useful as a baseline / fallback when the graph is sparse |
|
|
130
|
+
## Diff context mode
|
|
163
131
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
132
|
+
Finds the minimal set of code fragments needed to understand a change —
|
|
133
|
+
imports, callers, type definitions, config dependencies — across 50+ file
|
|
134
|
+
types. It builds a code graph (imports, co-changes, type refs), propagates
|
|
135
|
+
relevance from the changed lines outward, and stops when relevance drops below
|
|
136
|
+
`--tau` or the `--budget` token cap is reached.
|
|
167
137
|
|
|
168
138
|
| Flag | Default | Description |
|
|
169
139
|
|-------------|---------|--------------------------------------------------------------------------|
|
|
170
|
-
| `--scoring` | `ego` |
|
|
171
|
-
| `--budget` | auto |
|
|
172
|
-
| `--alpha` | 0.60 |
|
|
173
|
-
| `--tau` | 0.12 |
|
|
174
|
-
| `--full` | false |
|
|
140
|
+
| `--scoring` | `ego` | `ego` = bounded expansion around changed nodes (fast, predictable radius); `ppr` = Personalized PageRank (global, smoother decay, slower); `bm25` = lexical retrieval against the diff hunks (baseline for sparse graphs) |
|
|
141
|
+
| `--budget` | auto | Hard token cap: `N` enforces a fixed cap, `-1` disables it, `0` is a strict-zero floor (empty selection; use `--full` for changed files only) |
|
|
142
|
+
| `--alpha` | 0.60 | PPR damping; higher = context clusters tighter around changes (`--scoring ppr` only) |
|
|
143
|
+
| `--tau` | 0.12 | Relevance threshold for full fragment content; lower-scoring fragments are stubbed or dropped (lower = more context) |
|
|
144
|
+
| `--full` | false | Only the changed files, every fragment, no related-code context |
|
|
145
|
+
| `--timeout` | 300 | Wall-clock deadline in seconds; on expiry diffctx exits 124 instead of hanging |
|
|
175
146
|
|
|
176
|
-
Calibration of `--alpha`, `--tau`, and the edge-weight priors
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
*Theory: [Context-Selection for Git Diff (Zenodo, 2026)](https://doi.org/10.5281/zenodo.18824580).*
|
|
147
|
+
Calibration of `--alpha`, `--tau`, and the edge-weight priors:
|
|
148
|
+
[`docs/parameter-strategy.md`](docs/parameter-strategy.md). Theory:
|
|
149
|
+
[Context-Selection for Git Diff (Zenodo, 2026)](https://doi.org/10.5281/zenodo.18824580).
|
|
180
150
|
|
|
181
151
|
### `graph` subcommand
|
|
182
152
|
|
|
183
|
-
|
|
184
|
-
use the `graph` subcommand:
|
|
153
|
+
Explore the underlying dependency graph directly, without a diff:
|
|
185
154
|
|
|
186
155
|
```bash
|
|
187
156
|
diffctx graph . # Mermaid graph of directory deps (default)
|
|
@@ -190,12 +159,6 @@ diffctx graph . --level fragment -f json # fragment-level graph as JSON
|
|
|
190
159
|
diffctx graph . --level file -f graphml -o g.xml # file-level graph as GraphML
|
|
191
160
|
```
|
|
192
161
|
|
|
193
|
-
| Flag | Default | Description |
|
|
194
|
-
|-------------|--------------|----------------------------------------------------------|
|
|
195
|
-
| `-f/--format` | `mermaid` | Output format: `mermaid`, `json`, or `graphml` |
|
|
196
|
-
| `--level` | `directory` | Granularity: `fragment`, `file`, or `directory` |
|
|
197
|
-
| `--summary` | false | Print graph statistics (cycles, hotspots, coupling) |
|
|
198
|
-
|
|
199
162
|
## Usage
|
|
200
163
|
|
|
201
164
|
<!-- BEGIN USAGE -->
|
|
@@ -217,111 +180,49 @@ diffctx . --diff HEAD~1 -c # diff context to clipboard
|
|
|
217
180
|
```
|
|
218
181
|
<!-- END USAGE -->
|
|
219
182
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
- name: main.py
|
|
227
|
-
type: file
|
|
228
|
-
content: |
|
|
229
|
-
def hello():
|
|
230
|
-
print("Hello, World!")
|
|
231
|
-
- name: utils/
|
|
232
|
-
type: directory
|
|
233
|
-
children:
|
|
234
|
-
- name: helpers.py
|
|
235
|
-
type: file
|
|
236
|
-
content: |
|
|
237
|
-
def add(a, b):
|
|
238
|
-
return a + b
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
## Token Counting
|
|
242
|
-
|
|
243
|
-
Token count and size are always displayed on stderr:
|
|
244
|
-
|
|
245
|
-
```text
|
|
246
|
-
12,847 tokens (o200k_base), 52.3 KB
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
For large outputs (>1MB), approximate counts with `~` prefix:
|
|
250
|
-
|
|
251
|
-
```text
|
|
252
|
-
~125,000 tokens (o200k_base), 5.2 MB
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
Uses tiktoken with `o200k_base` encoding (GPT-4o tokenizer).
|
|
256
|
-
|
|
257
|
-
## Clipboard Support
|
|
258
|
-
|
|
259
|
-
Copy output directly to clipboard with `-c` or `--copy`:
|
|
260
|
-
|
|
261
|
-
```bash
|
|
262
|
-
diffctx . -c # copy (stdout suppressed, stderr: token count)
|
|
263
|
-
diffctx . -c -o tree.yaml # copy + save to file
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
**System Requirements:**
|
|
267
|
-
|
|
268
|
-
- **macOS:** `pbcopy` (pre-installed)
|
|
269
|
-
- **Windows:** `clip` (pre-installed)
|
|
270
|
-
- **Linux (Wayland):** `wl-copy`
|
|
271
|
-
- **Linux (X11):** `xclip` or `xsel`
|
|
183
|
+
Every run reports token count and size on stderr — `12,847 tokens
|
|
184
|
+
(o200k_base), 52.3 KB` (tiktoken, the GPT-4o tokenizer; `~`-prefixed
|
|
185
|
+
approximation above 1 MB). `-c/--copy` sends output to the clipboard via
|
|
186
|
+
`pbcopy` (macOS), `clip` (Windows), or `wl-copy`/`xclip`/`xsel` (Linux).
|
|
187
|
+
Unreadable files are replaced by placeholders such as `<binary file: N bytes>`,
|
|
188
|
+
`<file too large: N bytes>`, or `<unreadable content: not utf-8>`.
|
|
272
189
|
|
|
273
190
|
## Python API
|
|
274
191
|
|
|
275
192
|
```python
|
|
276
|
-
from diffctx import map_directory
|
|
277
|
-
from diffctx import to_yaml, to_json, to_text, to_markdown
|
|
278
|
-
|
|
279
|
-
tree = map_directory(
|
|
280
|
-
path, # directory path
|
|
281
|
-
max_depth=None, # limit traversal depth
|
|
282
|
-
no_content=False, # exclude file contents
|
|
283
|
-
max_file_bytes=None, # skip large files
|
|
284
|
-
ignore_file=None, # custom ignore file
|
|
285
|
-
no_default_ignores=False, # disable default ignores
|
|
286
|
-
whitelist_file=None, # include-only filter
|
|
287
|
-
)
|
|
288
|
-
|
|
289
|
-
yaml_str = to_yaml(tree)
|
|
290
|
-
json_str = to_json(tree)
|
|
291
|
-
text_str = to_text(tree)
|
|
292
|
-
md_str = to_markdown(tree)
|
|
293
|
-
|
|
294
|
-
# Diff context mode
|
|
295
193
|
from pathlib import Path
|
|
296
|
-
from diffctx import build_diff_context, to_yaml
|
|
194
|
+
from diffctx import build_diff_context, map_directory, to_json, to_markdown, to_text, to_yaml
|
|
297
195
|
|
|
298
196
|
ctx = build_diff_context(
|
|
299
|
-
Path("."),
|
|
300
|
-
"HEAD~1..HEAD",
|
|
301
|
-
budget_tokens=None, # None =
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
full=False, # skip smart selection
|
|
308
|
-
scoring_mode="ego", # "ego" (default), "ppr", or "bm25"
|
|
309
|
-
timeout=300, # seconds before the pipeline aborts
|
|
197
|
+
Path("."),
|
|
198
|
+
"HEAD~1..HEAD",
|
|
199
|
+
budget_tokens=None, # None = auto; 0 = strict-zero floor (empty); -1 = uncapped; N = hard cap
|
|
200
|
+
alpha=0.6,
|
|
201
|
+
tau=0.12,
|
|
202
|
+
full=False,
|
|
203
|
+
scoring_mode="ego",
|
|
204
|
+
timeout=300,
|
|
310
205
|
)
|
|
311
|
-
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
## MCP Server
|
|
206
|
+
print(to_markdown(ctx))
|
|
315
207
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
208
|
+
tree = map_directory(
|
|
209
|
+
".",
|
|
210
|
+
max_depth=None,
|
|
211
|
+
no_content=False,
|
|
212
|
+
max_file_bytes=None,
|
|
213
|
+
ignore_file=None,
|
|
214
|
+
no_default_ignores=False,
|
|
215
|
+
whitelist_file=None,
|
|
216
|
+
)
|
|
217
|
+
print(to_yaml(tree))
|
|
322
218
|
```
|
|
323
219
|
|
|
324
|
-
|
|
220
|
+
## MCP server
|
|
221
|
+
|
|
222
|
+
diffctx includes an [MCP](https://modelcontextprotocol.io) server that lets AI
|
|
223
|
+
assistants (Claude Code, Cursor, Windsurf, etc.) call diff context analysis
|
|
224
|
+
automatically during code review. Install with `pip install 'diffctx[mcp]'`
|
|
225
|
+
and add to your MCP client config (e.g. `~/.claude/mcp.json` for Claude Code):
|
|
325
226
|
|
|
326
227
|
```json
|
|
327
228
|
{
|
|
@@ -333,30 +234,21 @@ Add to your MCP client config (e.g. `~/.claude/mcp.json` for Claude Code):
|
|
|
333
234
|
}
|
|
334
235
|
```
|
|
335
236
|
|
|
336
|
-
The server exposes a `get_diff_context` tool
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
See [`src/diffctx/mcp/README.md`](src/diffctx/mcp/README.md) for configs
|
|
341
|
-
for Cursor, Continue, Windsurf, and Zed.
|
|
237
|
+
The server exposes a `get_diff_context` tool that assistants call when
|
|
238
|
+
reviewing PRs, explaining changes, or investigating broken tests. Configs for
|
|
239
|
+
Cursor, Continue, Windsurf, and Zed:
|
|
240
|
+
[`src/diffctx/mcp/README.md`](src/diffctx/mcp/README.md).
|
|
342
241
|
|
|
343
|
-
## Ignore
|
|
242
|
+
## Ignore patterns
|
|
344
243
|
|
|
345
|
-
Respects `.gitignore` and `.diffctx/ignore` automatically
|
|
346
|
-
|
|
347
|
-
|
|
244
|
+
Respects `.gitignore` and `.diffctx/ignore` automatically — hierarchically at
|
|
245
|
+
every directory level, with gitignore semantics (negation `!important.log`,
|
|
246
|
+
anchored `/root_only.txt`). `.diffctx/whitelist` acts as an include-only
|
|
247
|
+
filter, and the output file is always auto-ignored. `--no-default-ignores`
|
|
248
|
+
disables the built-in patterns; `--no-ignores` disables all ignore rules
|
|
249
|
+
(tree mode only).
|
|
348
250
|
|
|
349
|
-
|
|
350
|
-
- Negation patterns: `!important.log` un-ignores a file
|
|
351
|
-
- Anchored patterns: `/root_only.txt` matches only in root
|
|
352
|
-
- Output file is always auto-ignored
|
|
353
|
-
|
|
354
|
-
Auto-discovered files:
|
|
355
|
-
|
|
356
|
-
- `.diffctx/ignore` — diffctx-specific ignore patterns
|
|
357
|
-
- `.diffctx/whitelist` — Include-only filter (only matched files included)
|
|
358
|
-
|
|
359
|
-
## Exit Codes
|
|
251
|
+
## Exit codes
|
|
360
252
|
|
|
361
253
|
| Code | Meaning |
|
|
362
254
|
|------|---------|
|
|
@@ -364,17 +256,10 @@ Auto-discovered files:
|
|
|
364
256
|
| `1` | Runtime error (bad path, permission denied, etc.) |
|
|
365
257
|
| `2` | Usage error (invalid flags/arguments) |
|
|
366
258
|
| `3` | Environment error (`--diff` outside a git repo, git not installed, no commits yet) |
|
|
367
|
-
| `4` | `--diff` produced no semantic context (clean
|
|
259
|
+
| `4` | `--diff` produced no semantic context (clean tree, binary-only, everything filtered); output is still emitted. Deletion/rename-only diffs list `deleted_files`/`renamed_files` and exit `0` |
|
|
368
260
|
| `130`| Interrupted (Ctrl-C) |
|
|
369
261
|
| `141`| Broken pipe (e.g. piping into `head`) |
|
|
370
262
|
|
|
371
|
-
## Content Placeholders
|
|
372
|
-
|
|
373
|
-
- `<file too large: N bytes>` — exceeds `--max-file-bytes`
|
|
374
|
-
- `<binary file: N bytes>` — binary file detected
|
|
375
|
-
- `<unreadable content: not utf-8>` — not valid UTF-8
|
|
376
|
-
- `<unreadable content>` — permission denied or I/O error
|
|
377
|
-
|
|
378
263
|
## License
|
|
379
264
|
|
|
380
265
|
Apache 2.0
|
diffctx-1.12.0/README.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# diffctx — smart diff context for LLM code review
|
|
2
|
+
|
|
3
|
+
[](https://github.com/nikolay-e/diffctx/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/diffctx/)
|
|
5
|
+
[](https://pypi.org/project/diffctx/)
|
|
6
|
+
|
|
7
|
+
**diffctx selects the minimum code an LLM needs to review a git diff.**
|
|
8
|
+
Instead of pasting whole files, it walks the dependency graph from the changed
|
|
9
|
+
lines outward and stops as soon as additional context stops paying for itself.
|
|
10
|
+
|
|
11
|
+
> Coming from [`treemapper`](https://pypi.org/project/treemapper/)? That name is
|
|
12
|
+
> deprecated — it was a thin wrapper around diffctx. Every command, flag, and
|
|
13
|
+
> API call works unchanged: `treemapper` → `diffctx`, `treemapper-mcp` → `diffctx-mcp`.
|
|
14
|
+
|
|
15
|
+
## Why not just use `tree` or repomix?
|
|
16
|
+
|
|
17
|
+
| | `tree` | repomix | Claude Code Review | **diffctx** |
|
|
18
|
+
|---|:---:|:---:|:---:|:---:|
|
|
19
|
+
| **Primary use case** | directory listing | full repo export | automated PR review | **diff context for code review** |
|
|
20
|
+
| Smart diff context | ✗ | ✗ | ✓ | ✓ |
|
|
21
|
+
| Works with any LLM | ✓ | ✓ | Claude only | ✓ |
|
|
22
|
+
| Free / local / offline | ✓ | ✓ | $15–25/review | ✓ |
|
|
23
|
+
| GitHub required | ✗ | ✗ | ✓ | ✗ |
|
|
24
|
+
| Multiple output formats | ✗ | limited | — | YAML/JSON/MD/txt |
|
|
25
|
+
| Python API | ✗ | ✗ | ✗ | ✓ |
|
|
26
|
+
| MCP server | ✗ | ✗ | ✗ | ✓ |
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
uvx diffctx . --diff HEAD~1 # zero-install, run once via uv
|
|
32
|
+
pipx install diffctx # recommended: isolated CLI, no venv needed
|
|
33
|
+
pip install diffctx # or: into an active environment
|
|
34
|
+
pipx install 'diffctx[mcp]' # + MCP server for AI assistants
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
A standalone binary (no Python required) is on the
|
|
38
|
+
[releases page](https://github.com/nikolay-e/diffctx/releases/latest).
|
|
39
|
+
The `[tree-sitter]` extra adds AST-level parsing for more accurate context
|
|
40
|
+
selection across 30+ languages.
|
|
41
|
+
|
|
42
|
+
## Quick start
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
diffctx . --diff HEAD~1 # smart context for last commit → paste into Claude/ChatGPT
|
|
46
|
+
diffctx . -f md -c # full codebase export → clipboard in Markdown
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+

|
|
50
|
+
|
|
51
|
+
*`diffctx . --diff HEAD~1` selects only the fragments — functions, imports,
|
|
52
|
+
type definitions — that an LLM actually needs to review the last commit,
|
|
53
|
+
instead of dumping every changed file in full.*
|
|
54
|
+
|
|
55
|
+
## Diff context mode
|
|
56
|
+
|
|
57
|
+
Finds the minimal set of code fragments needed to understand a change —
|
|
58
|
+
imports, callers, type definitions, config dependencies — across 50+ file
|
|
59
|
+
types. It builds a code graph (imports, co-changes, type refs), propagates
|
|
60
|
+
relevance from the changed lines outward, and stops when relevance drops below
|
|
61
|
+
`--tau` or the `--budget` token cap is reached.
|
|
62
|
+
|
|
63
|
+
| Flag | Default | Description |
|
|
64
|
+
|-------------|---------|--------------------------------------------------------------------------|
|
|
65
|
+
| `--scoring` | `ego` | `ego` = bounded expansion around changed nodes (fast, predictable radius); `ppr` = Personalized PageRank (global, smoother decay, slower); `bm25` = lexical retrieval against the diff hunks (baseline for sparse graphs) |
|
|
66
|
+
| `--budget` | auto | Hard token cap: `N` enforces a fixed cap, `-1` disables it, `0` is a strict-zero floor (empty selection; use `--full` for changed files only) |
|
|
67
|
+
| `--alpha` | 0.60 | PPR damping; higher = context clusters tighter around changes (`--scoring ppr` only) |
|
|
68
|
+
| `--tau` | 0.12 | Relevance threshold for full fragment content; lower-scoring fragments are stubbed or dropped (lower = more context) |
|
|
69
|
+
| `--full` | false | Only the changed files, every fragment, no related-code context |
|
|
70
|
+
| `--timeout` | 300 | Wall-clock deadline in seconds; on expiry diffctx exits 124 instead of hanging |
|
|
71
|
+
|
|
72
|
+
Calibration of `--alpha`, `--tau`, and the edge-weight priors:
|
|
73
|
+
[`docs/parameter-strategy.md`](docs/parameter-strategy.md). Theory:
|
|
74
|
+
[Context-Selection for Git Diff (Zenodo, 2026)](https://doi.org/10.5281/zenodo.18824580).
|
|
75
|
+
|
|
76
|
+
### `graph` subcommand
|
|
77
|
+
|
|
78
|
+
Explore the underlying dependency graph directly, without a diff:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
diffctx graph . # Mermaid graph of directory deps (default)
|
|
82
|
+
diffctx graph . --summary # cycles, hotspots, coupling metrics
|
|
83
|
+
diffctx graph . --level fragment -f json # fragment-level graph as JSON
|
|
84
|
+
diffctx graph . --level file -f graphml -o g.xml # file-level graph as GraphML
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Usage
|
|
88
|
+
|
|
89
|
+
<!-- BEGIN USAGE -->
|
|
90
|
+
```bash
|
|
91
|
+
# full codebase export:
|
|
92
|
+
diffctx . # Markdown to stdout + token count
|
|
93
|
+
diffctx . -f md -c # Markdown → clipboard
|
|
94
|
+
diffctx . -f json -o tree.json # JSON → file
|
|
95
|
+
diffctx . --no-content # structure only, no file contents
|
|
96
|
+
diffctx . --max-depth 3 # limit depth
|
|
97
|
+
diffctx . -i custom.ignore # custom ignore patterns
|
|
98
|
+
|
|
99
|
+
# diff context mode (requires git repo):
|
|
100
|
+
diffctx . --diff # uncommitted changes (working tree vs HEAD)
|
|
101
|
+
diffctx . --diff HEAD~1 # context for last commit
|
|
102
|
+
diffctx . --diff main..feature # context for feature branch
|
|
103
|
+
diffctx . --diff HEAD~1 --budget 30000 # limit to ~30k tokens
|
|
104
|
+
diffctx . --diff HEAD~1 -c # diff context to clipboard
|
|
105
|
+
```
|
|
106
|
+
<!-- END USAGE -->
|
|
107
|
+
|
|
108
|
+
Every run reports token count and size on stderr — `12,847 tokens
|
|
109
|
+
(o200k_base), 52.3 KB` (tiktoken, the GPT-4o tokenizer; `~`-prefixed
|
|
110
|
+
approximation above 1 MB). `-c/--copy` sends output to the clipboard via
|
|
111
|
+
`pbcopy` (macOS), `clip` (Windows), or `wl-copy`/`xclip`/`xsel` (Linux).
|
|
112
|
+
Unreadable files are replaced by placeholders such as `<binary file: N bytes>`,
|
|
113
|
+
`<file too large: N bytes>`, or `<unreadable content: not utf-8>`.
|
|
114
|
+
|
|
115
|
+
## Python API
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from pathlib import Path
|
|
119
|
+
from diffctx import build_diff_context, map_directory, to_json, to_markdown, to_text, to_yaml
|
|
120
|
+
|
|
121
|
+
ctx = build_diff_context(
|
|
122
|
+
Path("."),
|
|
123
|
+
"HEAD~1..HEAD",
|
|
124
|
+
budget_tokens=None, # None = auto; 0 = strict-zero floor (empty); -1 = uncapped; N = hard cap
|
|
125
|
+
alpha=0.6,
|
|
126
|
+
tau=0.12,
|
|
127
|
+
full=False,
|
|
128
|
+
scoring_mode="ego",
|
|
129
|
+
timeout=300,
|
|
130
|
+
)
|
|
131
|
+
print(to_markdown(ctx))
|
|
132
|
+
|
|
133
|
+
tree = map_directory(
|
|
134
|
+
".",
|
|
135
|
+
max_depth=None,
|
|
136
|
+
no_content=False,
|
|
137
|
+
max_file_bytes=None,
|
|
138
|
+
ignore_file=None,
|
|
139
|
+
no_default_ignores=False,
|
|
140
|
+
whitelist_file=None,
|
|
141
|
+
)
|
|
142
|
+
print(to_yaml(tree))
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## MCP server
|
|
146
|
+
|
|
147
|
+
diffctx includes an [MCP](https://modelcontextprotocol.io) server that lets AI
|
|
148
|
+
assistants (Claude Code, Cursor, Windsurf, etc.) call diff context analysis
|
|
149
|
+
automatically during code review. Install with `pip install 'diffctx[mcp]'`
|
|
150
|
+
and add to your MCP client config (e.g. `~/.claude/mcp.json` for Claude Code):
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
{
|
|
154
|
+
"mcpServers": {
|
|
155
|
+
"diffctx": {
|
|
156
|
+
"command": "diffctx-mcp"
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The server exposes a `get_diff_context` tool that assistants call when
|
|
163
|
+
reviewing PRs, explaining changes, or investigating broken tests. Configs for
|
|
164
|
+
Cursor, Continue, Windsurf, and Zed:
|
|
165
|
+
[`src/diffctx/mcp/README.md`](src/diffctx/mcp/README.md).
|
|
166
|
+
|
|
167
|
+
## Ignore patterns
|
|
168
|
+
|
|
169
|
+
Respects `.gitignore` and `.diffctx/ignore` automatically — hierarchically at
|
|
170
|
+
every directory level, with gitignore semantics (negation `!important.log`,
|
|
171
|
+
anchored `/root_only.txt`). `.diffctx/whitelist` acts as an include-only
|
|
172
|
+
filter, and the output file is always auto-ignored. `--no-default-ignores`
|
|
173
|
+
disables the built-in patterns; `--no-ignores` disables all ignore rules
|
|
174
|
+
(tree mode only).
|
|
175
|
+
|
|
176
|
+
## Exit codes
|
|
177
|
+
|
|
178
|
+
| Code | Meaning |
|
|
179
|
+
|------|---------|
|
|
180
|
+
| `0` | Success — output contains content |
|
|
181
|
+
| `1` | Runtime error (bad path, permission denied, etc.) |
|
|
182
|
+
| `2` | Usage error (invalid flags/arguments) |
|
|
183
|
+
| `3` | Environment error (`--diff` outside a git repo, git not installed, no commits yet) |
|
|
184
|
+
| `4` | `--diff` produced no semantic context (clean tree, binary-only, everything filtered); output is still emitted. Deletion/rename-only diffs list `deleted_files`/`renamed_files` and exit `0` |
|
|
185
|
+
| `130`| Interrupted (Ctrl-C) |
|
|
186
|
+
| `141`| Broken pipe (e.g. piping into `head`) |
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
Apache 2.0
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
- [Changelog](CHANGELOG.md)
|
|
195
|
+
- [Security policy](SECURITY.md) — threat model and vulnerability reporting
|
|
196
|
+
- [Parameter strategy](docs/parameter-strategy.md) — how `--alpha`,
|
|
197
|
+
`--tau`, and edge weights are calibrated
|