diffctx 1.11.0__tar.gz → 1.12.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. {diffctx-1.11.0 → diffctx-1.12.1}/CHANGELOG.md +103 -0
  2. diffctx-1.12.1/PKG-INFO +287 -0
  3. diffctx-1.12.1/README.md +210 -0
  4. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/Cargo.lock +16 -16
  5. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/Cargo.toml +13 -11
  6. diffctx-1.12.1/diffctx/README.md +210 -0
  7. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/config/limits.rs +4 -3
  8. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/config/selection.rs +6 -6
  9. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/discovery.rs +25 -52
  10. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/history/cochange.rs +1 -4
  11. diffctx-1.12.1/diffctx/src/edges/mod.rs +282 -0
  12. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/git.rs +31 -15
  13. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/graph.rs +199 -50
  14. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/lib.rs +2 -0
  15. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/main.rs +5 -2
  16. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/parsers/tree_sitter_strategy.rs +6 -1
  17. diffctx-1.12.1/diffctx/src/peak_rss.rs +46 -0
  18. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/pipeline.rs +21 -4
  19. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/postpass.rs +21 -8
  20. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/project_graph.rs +16 -28
  21. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/pybridge.rs +20 -0
  22. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/render.rs +19 -5
  23. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/scoring.rs +17 -4
  24. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/signatures.rs +22 -1
  25. diffctx-1.12.1/diffctx/src/token_corpus.rs +228 -0
  26. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/types.rs +14 -1
  27. {diffctx-1.11.0 → diffctx-1.12.1}/pyproject.toml +1 -17
  28. {diffctx-1.11.0 → diffctx-1.12.1}/src/diffctx/cli.py +210 -56
  29. diffctx-1.12.1/src/diffctx/diffctx/graph_analytics.py +293 -0
  30. {diffctx-1.11.0 → diffctx-1.12.1}/src/diffctx/diffctx/graph_export.py +16 -8
  31. {diffctx-1.11.0 → diffctx-1.12.1}/src/diffctx/diffctx/pipeline.py +1 -14
  32. diffctx-1.12.1/src/diffctx/diffctx/project_graph.py +36 -0
  33. {diffctx-1.11.0 → diffctx-1.12.1}/src/diffctx/ignore.py +11 -3
  34. {diffctx-1.11.0 → diffctx-1.12.1}/src/diffctx/logger.py +3 -2
  35. {diffctx-1.11.0 → diffctx-1.12.1}/src/diffctx/main.py +109 -33
  36. {diffctx-1.11.0 → diffctx-1.12.1}/src/diffctx/tokens.py +1 -4
  37. {diffctx-1.11.0 → diffctx-1.12.1}/src/diffctx/tree.py +11 -0
  38. diffctx-1.12.1/src/diffctx/version.py +1 -0
  39. {diffctx-1.11.0 → diffctx-1.12.1}/src/diffctx/writer.py +28 -6
  40. diffctx-1.11.0/PKG-INFO +0 -388
  41. diffctx-1.11.0/README.md +0 -310
  42. diffctx-1.11.0/diffctx/src/edges/mod.rs +0 -155
  43. diffctx-1.11.0/diffctx/tests/common/mod.rs +0 -274
  44. diffctx-1.11.0/diffctx/tests/fixtures/garbage/garbage_api.py +0 -31
  45. diffctx-1.11.0/diffctx/tests/fixtures/garbage/garbage_constants.py +0 -11
  46. diffctx-1.11.0/diffctx/tests/fixtures/garbage/garbage_handlers.py +0 -27
  47. diffctx-1.11.0/diffctx/tests/fixtures/garbage/garbage_models.py +0 -25
  48. diffctx-1.11.0/diffctx/tests/fixtures/garbage/garbage_module.js +0 -23
  49. diffctx-1.11.0/diffctx/tests/fixtures/garbage/garbage_services.py +0 -37
  50. diffctx-1.11.0/diffctx/tests/fixtures/garbage/garbage_types.py +0 -31
  51. diffctx-1.11.0/diffctx/tests/fixtures/garbage/garbage_unrelated.yaml +0 -8
  52. diffctx-1.11.0/diffctx/tests/fixtures/garbage/garbage_utils.py +0 -31
  53. diffctx-1.11.0/diffctx/tests/fixtures/garbage/garbage_validators.py +0 -23
  54. diffctx-1.11.0/diffctx/tests/yaml_cases.rs +0 -279
  55. diffctx-1.11.0/src/diffctx/diffctx/graph_analytics.py +0 -59
  56. diffctx-1.11.0/src/diffctx/diffctx/project_graph.py +0 -27
  57. diffctx-1.11.0/src/diffctx/version.py +0 -1
  58. {diffctx-1.11.0 → diffctx-1.12.1}/LICENSE +0 -0
  59. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/analytics.rs +0 -0
  60. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/candidate_files.rs +0 -0
  61. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/config/analytics.rs +0 -0
  62. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/config/bm25.rs +0 -0
  63. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/config/budget.rs +0 -0
  64. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/config/category_weights.rs +0 -0
  65. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/config/edge_weights.rs +0 -0
  66. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/config/env_overrides.rs +0 -0
  67. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/config/extensions.rs +0 -0
  68. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/config/filtering.rs +0 -0
  69. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/config/fragmentation.rs +0 -0
  70. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/config/git.rs +0 -0
  71. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/config/graph_filtering.rs +0 -0
  72. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/config/importance.rs +0 -0
  73. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/config/mod.rs +0 -0
  74. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/config/mode.rs +0 -0
  75. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/config/needs.rs +0 -0
  76. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/config/parsers.rs +0 -0
  77. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/config/render.rs +0 -0
  78. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/config/scoring.rs +0 -0
  79. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/config/tokenization.rs +0 -0
  80. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/config/weights.rs +0 -0
  81. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/core.rs +0 -0
  82. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/base.rs +0 -0
  83. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/config_edges/build_system.rs +0 -0
  84. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/config_edges/cicd.rs +0 -0
  85. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/config_edges/docker.rs +0 -0
  86. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/config_edges/generic.rs +0 -0
  87. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/config_edges/helm.rs +0 -0
  88. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/config_edges/kubernetes.rs +0 -0
  89. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/config_edges/mod.rs +0 -0
  90. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/document/mod.rs +0 -0
  91. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/history/mod.rs +0 -0
  92. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/ansible.rs +0 -0
  93. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/bazel.rs +0 -0
  94. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/c_family.rs +0 -0
  95. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/cargo_edges.rs +0 -0
  96. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/clojure.rs +0 -0
  97. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/css.rs +0 -0
  98. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/dart.rs +0 -0
  99. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/dbt.rs +0 -0
  100. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/dotnet.rs +0 -0
  101. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/elixir.rs +0 -0
  102. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/erlang.rs +0 -0
  103. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/go.rs +0 -0
  104. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/graphql.rs +0 -0
  105. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/haskell.rs +0 -0
  106. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/javascript.rs +0 -0
  107. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/julia.rs +0 -0
  108. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/jvm.rs +0 -0
  109. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/latex.rs +0 -0
  110. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/lua.rs +0 -0
  111. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/mod.rs +0 -0
  112. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/nim.rs +0 -0
  113. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/nix.rs +0 -0
  114. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/ocaml.rs +0 -0
  115. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/openapi.rs +0 -0
  116. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/perl.rs +0 -0
  117. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/php.rs +0 -0
  118. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/prisma.rs +0 -0
  119. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/protobuf.rs +0 -0
  120. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/python.rs +0 -0
  121. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/r_lang.rs +0 -0
  122. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/ruby.rs +0 -0
  123. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/rust_lang.rs +0 -0
  124. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/shell.rs +0 -0
  125. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/sql.rs +0 -0
  126. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/swift.rs +0 -0
  127. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/tags.rs +0 -0
  128. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/terraform.rs +0 -0
  129. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/semantic/zig.rs +0 -0
  130. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/similarity/lexical.rs +0 -0
  131. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/similarity/mod.rs +0 -0
  132. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/structural/containment.rs +0 -0
  133. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/structural/mod.rs +0 -0
  134. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/structural/sibling.rs +0 -0
  135. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/edges/structural/testing.rs +0 -0
  136. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/filtering.rs +0 -0
  137. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/fragmentation.rs +0 -0
  138. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/graph_export.rs +0 -0
  139. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/interval.rs +0 -0
  140. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/languages.rs +0 -0
  141. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/memory_pipeline.rs +0 -0
  142. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/mode.rs +0 -0
  143. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/parsers/config_parser.rs +0 -0
  144. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/parsers/generic.rs +0 -0
  145. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/parsers/markdown.rs +0 -0
  146. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/parsers/mod.rs +0 -0
  147. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/ppr.rs +0 -0
  148. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/select.rs +0 -0
  149. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/stopwords.rs +0 -0
  150. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/test_harness.rs +0 -0
  151. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/tokenizer.rs +0 -0
  152. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/utility/boltzmann.rs +0 -0
  153. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/utility/importance.rs +0 -0
  154. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/utility/mod.rs +0 -0
  155. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/utility/needs.rs +0 -0
  156. {diffctx-1.11.0 → diffctx-1.12.1}/diffctx/src/utility/scoring.rs +0 -0
  157. {diffctx-1.11.0 → diffctx-1.12.1}/rust-toolchain.toml +0 -0
  158. {diffctx-1.11.0 → diffctx-1.12.1}/src/diffctx/__init__.py +0 -0
  159. {diffctx-1.11.0 → diffctx-1.12.1}/src/diffctx/__main__.py +0 -0
  160. {diffctx-1.11.0 → diffctx-1.12.1}/src/diffctx/clipboard.py +0 -0
  161. {diffctx-1.11.0 → diffctx-1.12.1}/src/diffctx/diffctx/__init__.py +0 -0
  162. {diffctx-1.11.0 → diffctx-1.12.1}/src/diffctx/mcp/README.md +0 -0
  163. {diffctx-1.11.0 → diffctx-1.12.1}/src/diffctx/mcp/__init__.py +0 -0
  164. {diffctx-1.11.0 → diffctx-1.12.1}/src/diffctx/mcp/__main__.py +0 -0
  165. {diffctx-1.11.0 → diffctx-1.12.1}/src/diffctx/mcp/formatting.py +0 -0
  166. {diffctx-1.11.0 → diffctx-1.12.1}/src/diffctx/mcp/security.py +0 -0
  167. {diffctx-1.11.0 → diffctx-1.12.1}/src/diffctx/mcp/server.py +0 -0
  168. {diffctx-1.11.0 → diffctx-1.12.1}/src/diffctx/py.typed +0 -0
@@ -7,6 +7,109 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Added
11
+
12
+ - Standalone native binaries (linux x86_64/aarch64, macOS arm64, Windows
13
+ x64) are built and attached to every GitHub release. The README had
14
+ promised them since 1.9.x; releases only ever carried wheels and an sdist.
15
+ - The Rust engine is published to crates.io as the `diffctx` crate
16
+ (`cargo install diffctx` for the native CLI, `cargo add diffctx` to embed the
17
+ selection pipeline). Previously the name held only a reservation stub; the
18
+ crate now carries the released engine, starting at 1.12.0.
19
+ - Container image `ghcr.io/nikolay-e/diffctx` (linux amd64/arm64), built from
20
+ the release tag and smoke-tested against a real repository before the tag
21
+ moves: `docker run --rm -v "$PWD:/repo" ghcr.io/nikolay-e/diffctx . --diff HEAD~1`.
22
+ - Packaging manifests generated from the release checksums: Scoop
23
+ (`packaging/scoop/diffctx.json`), AUR (`packaging/aur/`) and an npm wrapper
24
+ (`packaging/npm/`) that downloads the platform binary and verifies its
25
+ SHA-256 against the published checksum.
26
+
27
+ ### Fixed
28
+
29
+ - The native binary silently emitted YAML for every unrecognized `--format`,
30
+ including `md` — the documented default of the Python CLI. It now accepts
31
+ only `yaml`/`json` and exits 2 on anything else.
32
+
33
+ ## [1.12.0] - 2026-07-23
34
+
35
+ ### Added
36
+
37
+ - `--timeout SECONDS` — wall-clock deadline for `--diff` analysis (default
38
+ 300); exceeding it exits `124` instead of hanging indefinitely (#70).
39
+ - `--no-ignores` — turns off every ignore rule (built-in patterns, project
40
+ `.gitignore`, `.diffctx/ignore`). `--no-default-ignores` only disables the
41
+ built-in list; its help now says so. Not supported with `--diff`.
42
+ - Output format is inferred from the `-o` extension when `-f` is omitted, so
43
+ `-o out.json` no longer writes Markdown into a `.json` file; a mismatch
44
+ between `-f` and the extension warns.
45
+
46
+ ### Fixed
47
+
48
+ - **All error logging was dead.** An import-time `NullHandler` made
49
+ `setup_logging` skip attaching a real handler, so `--log-level` was a no-op
50
+ and all 19 `logger.error/warning/exception` sites were silent — `diffctx . -o
51
+ /bad/path.md` exited 1 with no message at all.
52
+ - **diffctx invoked from inside a git hook silently analyzed the wrong
53
+ repository.** Git exports repo-locating env vars (`GIT_DIR`,
54
+ `GIT_INDEX_FILE`, `GIT_WORK_TREE`, ...) to hook subprocesses; inherited,
55
+ they overrode `-C` on every internal git call. All git spawns now scrub
56
+ these variables (`git_command()` in `git.rs`).
57
+ - YAML output preserved file content byte-exactly except for trailing
58
+ newlines; the block chomping indicator is now chosen per content.
59
+ - Arrow-function fragments bound to variables were never stub-eligible (#106).
60
+ - Decorated definitions rendered as a bare `@decorator` line without the
61
+ `class X:` / `def x():` header.
62
+ - `--max-depth`-pruned directories were labelled `_(empty directory)_` — a
63
+ factual lie to the reader; they now read
64
+ `_(children omitted: --max-depth reached)_` (`truncated: true` in
65
+ YAML/JSON).
66
+ - Mixed directory + glob arguments dropped the glob files' parent path from
67
+ node names.
68
+ - Double Ctrl-C printed a ~60-line traceback.
69
+ - Lock files `uv.lock`, `pdm.lock`, `bun.lock`, `bun.lockb`, `deno.lock` and
70
+ `flake.lock` leaked into output; they now join the other lock files in the
71
+ default ignore patterns.
72
+ - Large-repo hangs/OOM on trivial diffs (#70, #95): discovery no longer
73
+ re-reads and re-tokenizes the whole candidate universe per ensemble
74
+ strategy (one shared pass + a persistent per-blob token cache keyed by
75
+ `(blob OID, tokenizer epoch)`), and edge construction is two-pass with a
76
+ bounded per-source top-K instead of materializing up to tens of millions
77
+ of raw edges before the cap; pass 2 replays a compact 16-byte-per-emission
78
+ log instead of re-running the builders, so generation cost stays 1x.
79
+ Verified: gitpod 8000s-hang -> 35.7s,
80
+ pytorch 1848s-SIGKILL -> 7.4s, mui/material-ui OOM class recovered.
81
+ Outputs are bit-identical (gated by `benchmarks/equivalence_gate.py`).
82
+
83
+ ### Known limitations
84
+
85
+ - Near-dense edge emission on huge same-directory trees (observed: 199M
86
+ raw edges, 37GB peak on one mui/material-ui instance) remains expensive
87
+ even with bounded construction; tracked in #116.
88
+
89
+ ### Changed
90
+
91
+ - **The token budget is now a hard cap.** The changed-files post-pass no
92
+ longer exceeds the budget to guarantee representation: a changed file
93
+ whose cheapest representative does not fit stays unrepresented (visible
94
+ as changed-file retention < 1). `--budget 0` therefore yields an empty
95
+ selection (use `--full` for changed files only); CLI help updated.
96
+ - Latency telemetry: new `graph_build_ms` phase (graph construction was
97
+ previously misattributed to `scoring_ms`, which now measures pure
98
+ ranking) and `peak_rss_bytes` (in-process peak memory). Release builds
99
+ carry line tables (`debug = "line-tables-only"`) for profiling at no
100
+ runtime cost.
101
+ - CLI diagnostics are honest end to end: an exit-code table in `--help`
102
+ (2 usage, 3 environment, 4 empty diff, 124 timeout), flag-value validation
103
+ exits 2 instead of 1, git failures report a single line plus a
104
+ `git log --oneline` hint on unknown revisions, conflicting flags warn
105
+ (`-q`+`--log-level`, `--full`+selection flags, ...), a failed clipboard
106
+ copy warns before falling back to stdout, and `--tau` / `--scoring` /
107
+ `--alpha` / `--budget 0` help text describes what actually happens.
108
+ - `graph --summary` reports category shares as percentages, suppresses
109
+ degenerate top-referenced lists, detects cycles over dominant-direction
110
+ edges only, derives churn from `git log --since`, and disambiguates
111
+ duplicate mermaid labels to relative paths.
112
+
10
113
  ## [1.11.0] - 2026-07-07
11
114
 
12
115
  ### Changed
@@ -0,0 +1,287 @@
1
+ Metadata-Version: 2.4
2
+ Name: diffctx
3
+ Version: 1.12.1
4
+ Classifier: Development Status :: 5 - Production/Stable
5
+ Classifier: Environment :: Console
6
+ Classifier: Intended Audience :: Developers
7
+ Classifier: Operating System :: OS Independent
8
+ Classifier: Programming Language :: Python :: 3 :: Only
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Classifier: Topic :: Software Development
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Classifier: Topic :: Software Development :: Version Control :: Git
17
+ Classifier: Topic :: Utilities
18
+ Classifier: Typing :: Typed
19
+ Requires-Dist: pathspec>=0.11,<2.0
20
+ Requires-Dist: black>=23.0.0,<27.0 ; extra == 'dev'
21
+ Requires-Dist: charset-normalizer>=3.0,<4.0 ; extra == 'dev'
22
+ Requires-Dist: coverage>=7.0,<8.0 ; extra == 'dev'
23
+ Requires-Dist: diffctx[tree-sitter] ; extra == 'dev'
24
+ Requires-Dist: hypothesis>=6.0,<7.0 ; extra == 'dev'
25
+ Requires-Dist: import-linter>=2.0,<3.0 ; extra == 'dev'
26
+ Requires-Dist: mypy>=1.0,<3.0 ; extra == 'dev'
27
+ Requires-Dist: pre-commit>=3.0,<5.0 ; extra == 'dev'
28
+ Requires-Dist: pygit2>=1.12,<2.0 ; extra == 'dev'
29
+ Requires-Dist: pytest>=7.0,<10.0 ; extra == 'dev'
30
+ Requires-Dist: pytest-asyncio>=0.23,<2.0 ; extra == 'dev'
31
+ Requires-Dist: pytest-cov>=3.0,<8.0 ; extra == 'dev'
32
+ Requires-Dist: pytest-timeout>=2.1,<3.0 ; extra == 'dev'
33
+ Requires-Dist: pytest-xdist>=3.0,<4.0 ; extra == 'dev'
34
+ Requires-Dist: pyyaml>=6.0.2,<8.0 ; extra == 'dev'
35
+ Requires-Dist: radon>=6.0,<7.0 ; extra == 'dev'
36
+ Requires-Dist: ruff>=0.4,<1.0 ; extra == 'dev'
37
+ Requires-Dist: tiktoken>=0.9,<1.0 ; extra == 'dev'
38
+ Requires-Dist: types-pyyaml>=6.0,<7.0 ; extra == 'dev'
39
+ Requires-Dist: charset-normalizer>=3.0,<4.0 ; extra == 'full'
40
+ Requires-Dist: diffctx[tree-sitter] ; extra == 'full'
41
+ Requires-Dist: anyio>=4.5,<5.0 ; extra == 'mcp'
42
+ Requires-Dist: mcp>=1.27,<2.0 ; extra == 'mcp'
43
+ Requires-Dist: tree-sitter>=0.21,<1.0 ; extra == 'tree-sitter'
44
+ Requires-Dist: tree-sitter-c>=0.21,<1.0 ; extra == 'tree-sitter'
45
+ Requires-Dist: tree-sitter-c-sharp>=0.21,<1.0 ; extra == 'tree-sitter'
46
+ Requires-Dist: tree-sitter-cpp>=0.22,<1.0 ; extra == 'tree-sitter'
47
+ Requires-Dist: tree-sitter-elixir>=0.3,<1.0 ; extra == 'tree-sitter'
48
+ Requires-Dist: tree-sitter-go>=0.21,<1.0 ; extra == 'tree-sitter'
49
+ Requires-Dist: tree-sitter-html>=0.23,<1.0 ; extra == 'tree-sitter'
50
+ Requires-Dist: tree-sitter-java>=0.21,<1.0 ; extra == 'tree-sitter'
51
+ Requires-Dist: tree-sitter-javascript>=0.21,<1.0 ; extra == 'tree-sitter'
52
+ Requires-Dist: tree-sitter-lua>=0.5,<1.0 ; extra == 'tree-sitter'
53
+ Requires-Dist: tree-sitter-php>=0.24,<1.0 ; extra == 'tree-sitter'
54
+ Requires-Dist: tree-sitter-python>=0.21,<1.0 ; extra == 'tree-sitter'
55
+ Requires-Dist: tree-sitter-ruby>=0.21,<1.0 ; extra == 'tree-sitter'
56
+ Requires-Dist: tree-sitter-rust>=0.21,<1.0 ; extra == 'tree-sitter'
57
+ Requires-Dist: tree-sitter-scala>=0.24,<1.0 ; extra == 'tree-sitter'
58
+ Requires-Dist: tree-sitter-swift>=0.0.1,<1.0 ; extra == 'tree-sitter'
59
+ Requires-Dist: tree-sitter-typescript>=0.21,<1.0 ; extra == 'tree-sitter'
60
+ Provides-Extra: dev
61
+ Provides-Extra: full
62
+ Provides-Extra: mcp
63
+ Provides-Extra: tree-sitter
64
+ License-File: LICENSE
65
+ Summary: Export codebase structure and contents for AI/LLM context
66
+ Keywords: ai,chatgpt,claude,code-analysis,code-context,code-review,code-to-prompt,codebase,context,context-selection,diff-context,directory-tree,export,git-diff,json,llm,llm-context,mcp,model-context-protocol,tree,yaml
67
+ Home-Page: https://github.com/nikolay-e/diffctx
68
+ Author-email: Nikolay Eremeev <nikolay.eremeev@outlook.com>
69
+ License-Expression: Apache-2.0
70
+ Requires-Python: >=3.10
71
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
72
+ Project-URL: Changelog, https://github.com/nikolay-e/diffctx/releases
73
+ Project-URL: Homepage, https://github.com/nikolay-e/diffctx
74
+ Project-URL: Issues, https://github.com/nikolay-e/diffctx/issues
75
+ Project-URL: Repository, https://github.com/nikolay-e/diffctx
76
+
77
+ # diffctx — smart diff context for LLM code review
78
+
79
+ [![CI](https://github.com/nikolay-e/diffctx/actions/workflows/ci.yml/badge.svg)](https://github.com/nikolay-e/diffctx/actions/workflows/ci.yml)
80
+ [![PyPI](https://img.shields.io/pypi/v/diffctx)](https://pypi.org/project/diffctx/)
81
+ [![crates.io](https://img.shields.io/crates/v/diffctx)](https://crates.io/crates/diffctx)
82
+ [![License](https://img.shields.io/pypi/l/diffctx)](https://pypi.org/project/diffctx/)
83
+
84
+ **diffctx selects the minimum code an LLM needs to review a git diff.**
85
+ Instead of pasting whole files, it walks the dependency graph from the changed
86
+ lines outward and stops as soon as additional context stops paying for itself.
87
+
88
+ > Coming from [`treemapper`](https://pypi.org/project/treemapper/)? That name is
89
+ > deprecated — it was a thin wrapper around diffctx. Every command, flag, and
90
+ > API call works unchanged: `treemapper` → `diffctx`, `treemapper-mcp` → `diffctx-mcp`.
91
+
92
+ ## Why not just use `tree` or repomix?
93
+
94
+ | | `tree` | repomix | Claude Code Review | **diffctx** |
95
+ |---|:---:|:---:|:---:|:---:|
96
+ | **Primary use case** | directory listing | full repo export | automated PR review | **diff context for code review** |
97
+ | Smart diff context | ✗ | ✗ | ✓ | ✓ |
98
+ | Works with any LLM | ✓ | ✓ | Claude only | ✓ |
99
+ | Free / local / offline | ✓ | ✓ | $15–25/review | ✓ |
100
+ | GitHub required | ✗ | ✗ | ✓ | ✗ |
101
+ | Multiple output formats | ✗ | limited | — | YAML/JSON/MD/txt |
102
+ | Python API | ✗ | ✗ | ✗ | ✓ |
103
+ | MCP server | ✗ | ✗ | ✗ | ✓ |
104
+
105
+ ## Install
106
+
107
+ ```bash
108
+ uvx diffctx . --diff HEAD~1 # zero-install, run once via uv
109
+ pipx install diffctx # recommended: isolated CLI, no venv needed
110
+ pip install diffctx # or: into an active environment
111
+ pipx install 'diffctx[mcp]' # + MCP server for AI assistants
112
+ ```
113
+
114
+ The `[tree-sitter]` extra adds AST-level parsing for more accurate context
115
+ selection across 30+ languages.
116
+
117
+ Without Python:
118
+
119
+ ```bash
120
+ cargo install diffctx # native CLI from crates.io
121
+ docker run --rm -v "$PWD:/repo" ghcr.io/nikolay-e/diffctx . --diff HEAD~1
122
+ ```
123
+
124
+ Prebuilt binaries for linux (x86_64/aarch64), macOS (arm64) and Windows (x64)
125
+ are attached to every [release](https://github.com/nikolay-e/diffctx/releases/latest).
126
+ The native binary covers diff mode with YAML/JSON output; tree mode, Markdown
127
+ output, the `graph` subcommand and the MCP server live in the Python package.
128
+ `cargo add diffctx` embeds the selection pipeline in a Rust project
129
+ ([docs.rs](https://docs.rs/diffctx)).
130
+
131
+ ## Quick start
132
+
133
+ ```bash
134
+ diffctx . --diff HEAD~1 # smart context for last commit → paste into Claude/ChatGPT
135
+ diffctx . -f md -c # full codebase export → clipboard in Markdown
136
+ ```
137
+
138
+ ![diffctx demo](https://raw.githubusercontent.com/nikolay-e/diffctx/main/docs/demo.gif)
139
+
140
+ *`diffctx . --diff HEAD~1` selects only the fragments — functions, imports,
141
+ type definitions — that an LLM actually needs to review the last commit,
142
+ instead of dumping every changed file in full.*
143
+
144
+ ## Diff context mode
145
+
146
+ Finds the minimal set of code fragments needed to understand a change —
147
+ imports, callers, type definitions, config dependencies — across 50+ file
148
+ types. It builds a code graph (imports, co-changes, type refs), propagates
149
+ relevance from the changed lines outward, and stops when relevance drops below
150
+ `--tau` or the `--budget` token cap is reached.
151
+
152
+ | Flag | Default | Description |
153
+ |-------------|---------|--------------------------------------------------------------------------|
154
+ | `--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) |
155
+ | `--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) |
156
+ | `--alpha` | 0.60 | PPR damping; higher = context clusters tighter around changes (`--scoring ppr` only) |
157
+ | `--tau` | 0.12 | Relevance threshold for full fragment content; lower-scoring fragments are stubbed or dropped (lower = more context) |
158
+ | `--full` | false | Only the changed files, every fragment, no related-code context |
159
+ | `--timeout` | 300 | Wall-clock deadline in seconds; on expiry diffctx exits 124 instead of hanging |
160
+
161
+ Calibration of `--alpha`, `--tau`, and the edge-weight priors:
162
+ [`docs/parameter-strategy.md`](docs/parameter-strategy.md). Theory:
163
+ [Context-Selection for Git Diff (Zenodo, 2026)](https://doi.org/10.5281/zenodo.18824580).
164
+
165
+ ### `graph` subcommand
166
+
167
+ Explore the underlying dependency graph directly, without a diff:
168
+
169
+ ```bash
170
+ diffctx graph . # Mermaid graph of directory deps (default)
171
+ diffctx graph . --summary # cycles, hotspots, coupling metrics
172
+ diffctx graph . --level fragment -f json # fragment-level graph as JSON
173
+ diffctx graph . --level file -f graphml -o g.xml # file-level graph as GraphML
174
+ ```
175
+
176
+ ## Usage
177
+
178
+ <!-- BEGIN USAGE -->
179
+ ```bash
180
+ # full codebase export:
181
+ diffctx . # Markdown to stdout + token count
182
+ diffctx . -f md -c # Markdown → clipboard
183
+ diffctx . -f json -o tree.json # JSON → file
184
+ diffctx . --no-content # structure only, no file contents
185
+ diffctx . --max-depth 3 # limit depth
186
+ diffctx . -i custom.ignore # custom ignore patterns
187
+
188
+ # diff context mode (requires git repo):
189
+ diffctx . --diff # uncommitted changes (working tree vs HEAD)
190
+ diffctx . --diff HEAD~1 # context for last commit
191
+ diffctx . --diff main..feature # context for feature branch
192
+ diffctx . --diff HEAD~1 --budget 30000 # limit to ~30k tokens
193
+ diffctx . --diff HEAD~1 -c # diff context to clipboard
194
+ ```
195
+ <!-- END USAGE -->
196
+
197
+ Every run reports token count and size on stderr — `12,847 tokens
198
+ (o200k_base), 52.3 KB` (tiktoken, the GPT-4o tokenizer; `~`-prefixed
199
+ approximation above 1 MB). `-c/--copy` sends output to the clipboard via
200
+ `pbcopy` (macOS), `clip` (Windows), or `wl-copy`/`xclip`/`xsel` (Linux).
201
+ Unreadable files are replaced by placeholders such as `<binary file: N bytes>`,
202
+ `<file too large: N bytes>`, or `<unreadable content: not utf-8>`.
203
+
204
+ ## Python API
205
+
206
+ ```python
207
+ from pathlib import Path
208
+ from diffctx import build_diff_context, map_directory, to_json, to_markdown, to_text, to_yaml
209
+
210
+ ctx = build_diff_context(
211
+ Path("."),
212
+ "HEAD~1..HEAD",
213
+ budget_tokens=None, # None = auto; 0 = strict-zero floor (empty); -1 = uncapped; N = hard cap
214
+ alpha=0.6,
215
+ tau=0.12,
216
+ full=False,
217
+ scoring_mode="ego",
218
+ timeout=300,
219
+ )
220
+ print(to_markdown(ctx))
221
+
222
+ tree = map_directory(
223
+ ".",
224
+ max_depth=None,
225
+ no_content=False,
226
+ max_file_bytes=None,
227
+ ignore_file=None,
228
+ no_default_ignores=False,
229
+ whitelist_file=None,
230
+ )
231
+ print(to_yaml(tree))
232
+ ```
233
+
234
+ ## MCP server
235
+
236
+ diffctx includes an [MCP](https://modelcontextprotocol.io) server that lets AI
237
+ assistants (Claude Code, Cursor, Windsurf, etc.) call diff context analysis
238
+ automatically during code review. Install with `pip install 'diffctx[mcp]'`
239
+ and add to your MCP client config (e.g. `~/.claude/mcp.json` for Claude Code):
240
+
241
+ ```json
242
+ {
243
+ "mcpServers": {
244
+ "diffctx": {
245
+ "command": "diffctx-mcp"
246
+ }
247
+ }
248
+ }
249
+ ```
250
+
251
+ The server exposes a `get_diff_context` tool that assistants call when
252
+ reviewing PRs, explaining changes, or investigating broken tests. Configs for
253
+ Cursor, Continue, Windsurf, and Zed:
254
+ [`src/diffctx/mcp/README.md`](src/diffctx/mcp/README.md).
255
+
256
+ ## Ignore patterns
257
+
258
+ Respects `.gitignore` and `.diffctx/ignore` automatically — hierarchically at
259
+ every directory level, with gitignore semantics (negation `!important.log`,
260
+ anchored `/root_only.txt`). `.diffctx/whitelist` acts as an include-only
261
+ filter, and the output file is always auto-ignored. `--no-default-ignores`
262
+ disables the built-in patterns; `--no-ignores` disables all ignore rules
263
+ (tree mode only).
264
+
265
+ ## Exit codes
266
+
267
+ | Code | Meaning |
268
+ |------|---------|
269
+ | `0` | Success — output contains content |
270
+ | `1` | Runtime error (bad path, permission denied, etc.) |
271
+ | `2` | Usage error (invalid flags/arguments) |
272
+ | `3` | Environment error (`--diff` outside a git repo, git not installed, no commits yet) |
273
+ | `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` |
274
+ | `130`| Interrupted (Ctrl-C) |
275
+ | `141`| Broken pipe (e.g. piping into `head`) |
276
+
277
+ ## License
278
+
279
+ Apache 2.0
280
+
281
+ ---
282
+
283
+ - [Changelog](CHANGELOG.md)
284
+ - [Security policy](SECURITY.md) — threat model and vulnerability reporting
285
+ - [Parameter strategy](docs/parameter-strategy.md) — how `--alpha`,
286
+ `--tau`, and edge weights are calibrated
287
+
@@ -0,0 +1,210 @@
1
+ # diffctx — smart diff context for LLM code review
2
+
3
+ [![CI](https://github.com/nikolay-e/diffctx/actions/workflows/ci.yml/badge.svg)](https://github.com/nikolay-e/diffctx/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/diffctx)](https://pypi.org/project/diffctx/)
5
+ [![crates.io](https://img.shields.io/crates/v/diffctx)](https://crates.io/crates/diffctx)
6
+ [![License](https://img.shields.io/pypi/l/diffctx)](https://pypi.org/project/diffctx/)
7
+
8
+ **diffctx selects the minimum code an LLM needs to review a git diff.**
9
+ Instead of pasting whole files, it walks the dependency graph from the changed
10
+ lines outward and stops as soon as additional context stops paying for itself.
11
+
12
+ > Coming from [`treemapper`](https://pypi.org/project/treemapper/)? That name is
13
+ > deprecated — it was a thin wrapper around diffctx. Every command, flag, and
14
+ > API call works unchanged: `treemapper` → `diffctx`, `treemapper-mcp` → `diffctx-mcp`.
15
+
16
+ ## Why not just use `tree` or repomix?
17
+
18
+ | | `tree` | repomix | Claude Code Review | **diffctx** |
19
+ |---|:---:|:---:|:---:|:---:|
20
+ | **Primary use case** | directory listing | full repo export | automated PR review | **diff context for code review** |
21
+ | Smart diff context | ✗ | ✗ | ✓ | ✓ |
22
+ | Works with any LLM | ✓ | ✓ | Claude only | ✓ |
23
+ | Free / local / offline | ✓ | ✓ | $15–25/review | ✓ |
24
+ | GitHub required | ✗ | ✗ | ✓ | ✗ |
25
+ | Multiple output formats | ✗ | limited | — | YAML/JSON/MD/txt |
26
+ | Python API | ✗ | ✗ | ✗ | ✓ |
27
+ | MCP server | ✗ | ✗ | ✗ | ✓ |
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ uvx diffctx . --diff HEAD~1 # zero-install, run once via uv
33
+ pipx install diffctx # recommended: isolated CLI, no venv needed
34
+ pip install diffctx # or: into an active environment
35
+ pipx install 'diffctx[mcp]' # + MCP server for AI assistants
36
+ ```
37
+
38
+ The `[tree-sitter]` extra adds AST-level parsing for more accurate context
39
+ selection across 30+ languages.
40
+
41
+ Without Python:
42
+
43
+ ```bash
44
+ cargo install diffctx # native CLI from crates.io
45
+ docker run --rm -v "$PWD:/repo" ghcr.io/nikolay-e/diffctx . --diff HEAD~1
46
+ ```
47
+
48
+ Prebuilt binaries for linux (x86_64/aarch64), macOS (arm64) and Windows (x64)
49
+ are attached to every [release](https://github.com/nikolay-e/diffctx/releases/latest).
50
+ The native binary covers diff mode with YAML/JSON output; tree mode, Markdown
51
+ output, the `graph` subcommand and the MCP server live in the Python package.
52
+ `cargo add diffctx` embeds the selection pipeline in a Rust project
53
+ ([docs.rs](https://docs.rs/diffctx)).
54
+
55
+ ## Quick start
56
+
57
+ ```bash
58
+ diffctx . --diff HEAD~1 # smart context for last commit → paste into Claude/ChatGPT
59
+ diffctx . -f md -c # full codebase export → clipboard in Markdown
60
+ ```
61
+
62
+ ![diffctx demo](https://raw.githubusercontent.com/nikolay-e/diffctx/main/docs/demo.gif)
63
+
64
+ *`diffctx . --diff HEAD~1` selects only the fragments — functions, imports,
65
+ type definitions — that an LLM actually needs to review the last commit,
66
+ instead of dumping every changed file in full.*
67
+
68
+ ## Diff context mode
69
+
70
+ Finds the minimal set of code fragments needed to understand a change —
71
+ imports, callers, type definitions, config dependencies — across 50+ file
72
+ types. It builds a code graph (imports, co-changes, type refs), propagates
73
+ relevance from the changed lines outward, and stops when relevance drops below
74
+ `--tau` or the `--budget` token cap is reached.
75
+
76
+ | Flag | Default | Description |
77
+ |-------------|---------|--------------------------------------------------------------------------|
78
+ | `--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) |
79
+ | `--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) |
80
+ | `--alpha` | 0.60 | PPR damping; higher = context clusters tighter around changes (`--scoring ppr` only) |
81
+ | `--tau` | 0.12 | Relevance threshold for full fragment content; lower-scoring fragments are stubbed or dropped (lower = more context) |
82
+ | `--full` | false | Only the changed files, every fragment, no related-code context |
83
+ | `--timeout` | 300 | Wall-clock deadline in seconds; on expiry diffctx exits 124 instead of hanging |
84
+
85
+ Calibration of `--alpha`, `--tau`, and the edge-weight priors:
86
+ [`docs/parameter-strategy.md`](docs/parameter-strategy.md). Theory:
87
+ [Context-Selection for Git Diff (Zenodo, 2026)](https://doi.org/10.5281/zenodo.18824580).
88
+
89
+ ### `graph` subcommand
90
+
91
+ Explore the underlying dependency graph directly, without a diff:
92
+
93
+ ```bash
94
+ diffctx graph . # Mermaid graph of directory deps (default)
95
+ diffctx graph . --summary # cycles, hotspots, coupling metrics
96
+ diffctx graph . --level fragment -f json # fragment-level graph as JSON
97
+ diffctx graph . --level file -f graphml -o g.xml # file-level graph as GraphML
98
+ ```
99
+
100
+ ## Usage
101
+
102
+ <!-- BEGIN USAGE -->
103
+ ```bash
104
+ # full codebase export:
105
+ diffctx . # Markdown to stdout + token count
106
+ diffctx . -f md -c # Markdown → clipboard
107
+ diffctx . -f json -o tree.json # JSON → file
108
+ diffctx . --no-content # structure only, no file contents
109
+ diffctx . --max-depth 3 # limit depth
110
+ diffctx . -i custom.ignore # custom ignore patterns
111
+
112
+ # diff context mode (requires git repo):
113
+ diffctx . --diff # uncommitted changes (working tree vs HEAD)
114
+ diffctx . --diff HEAD~1 # context for last commit
115
+ diffctx . --diff main..feature # context for feature branch
116
+ diffctx . --diff HEAD~1 --budget 30000 # limit to ~30k tokens
117
+ diffctx . --diff HEAD~1 -c # diff context to clipboard
118
+ ```
119
+ <!-- END USAGE -->
120
+
121
+ Every run reports token count and size on stderr — `12,847 tokens
122
+ (o200k_base), 52.3 KB` (tiktoken, the GPT-4o tokenizer; `~`-prefixed
123
+ approximation above 1 MB). `-c/--copy` sends output to the clipboard via
124
+ `pbcopy` (macOS), `clip` (Windows), or `wl-copy`/`xclip`/`xsel` (Linux).
125
+ Unreadable files are replaced by placeholders such as `<binary file: N bytes>`,
126
+ `<file too large: N bytes>`, or `<unreadable content: not utf-8>`.
127
+
128
+ ## Python API
129
+
130
+ ```python
131
+ from pathlib import Path
132
+ from diffctx import build_diff_context, map_directory, to_json, to_markdown, to_text, to_yaml
133
+
134
+ ctx = build_diff_context(
135
+ Path("."),
136
+ "HEAD~1..HEAD",
137
+ budget_tokens=None, # None = auto; 0 = strict-zero floor (empty); -1 = uncapped; N = hard cap
138
+ alpha=0.6,
139
+ tau=0.12,
140
+ full=False,
141
+ scoring_mode="ego",
142
+ timeout=300,
143
+ )
144
+ print(to_markdown(ctx))
145
+
146
+ tree = map_directory(
147
+ ".",
148
+ max_depth=None,
149
+ no_content=False,
150
+ max_file_bytes=None,
151
+ ignore_file=None,
152
+ no_default_ignores=False,
153
+ whitelist_file=None,
154
+ )
155
+ print(to_yaml(tree))
156
+ ```
157
+
158
+ ## MCP server
159
+
160
+ diffctx includes an [MCP](https://modelcontextprotocol.io) server that lets AI
161
+ assistants (Claude Code, Cursor, Windsurf, etc.) call diff context analysis
162
+ automatically during code review. Install with `pip install 'diffctx[mcp]'`
163
+ and add to your MCP client config (e.g. `~/.claude/mcp.json` for Claude Code):
164
+
165
+ ```json
166
+ {
167
+ "mcpServers": {
168
+ "diffctx": {
169
+ "command": "diffctx-mcp"
170
+ }
171
+ }
172
+ }
173
+ ```
174
+
175
+ The server exposes a `get_diff_context` tool that assistants call when
176
+ reviewing PRs, explaining changes, or investigating broken tests. Configs for
177
+ Cursor, Continue, Windsurf, and Zed:
178
+ [`src/diffctx/mcp/README.md`](src/diffctx/mcp/README.md).
179
+
180
+ ## Ignore patterns
181
+
182
+ Respects `.gitignore` and `.diffctx/ignore` automatically — hierarchically at
183
+ every directory level, with gitignore semantics (negation `!important.log`,
184
+ anchored `/root_only.txt`). `.diffctx/whitelist` acts as an include-only
185
+ filter, and the output file is always auto-ignored. `--no-default-ignores`
186
+ disables the built-in patterns; `--no-ignores` disables all ignore rules
187
+ (tree mode only).
188
+
189
+ ## Exit codes
190
+
191
+ | Code | Meaning |
192
+ |------|---------|
193
+ | `0` | Success — output contains content |
194
+ | `1` | Runtime error (bad path, permission denied, etc.) |
195
+ | `2` | Usage error (invalid flags/arguments) |
196
+ | `3` | Environment error (`--diff` outside a git repo, git not installed, no commits yet) |
197
+ | `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` |
198
+ | `130`| Interrupted (Ctrl-C) |
199
+ | `141`| Broken pipe (e.g. piping into `head`) |
200
+
201
+ ## License
202
+
203
+ Apache 2.0
204
+
205
+ ---
206
+
207
+ - [Changelog](CHANGELOG.md)
208
+ - [Security policy](SECURITY.md) — threat model and vulnerability reporting
209
+ - [Parameter strategy](docs/parameter-strategy.md) — how `--alpha`,
210
+ `--tau`, and edge weights are calibrated