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.
Files changed (166) hide show
  1. {diffctx-1.11.0 → diffctx-1.12.0}/CHANGELOG.md +80 -0
  2. {diffctx-1.11.0 → diffctx-1.12.0}/PKG-INFO +79 -194
  3. diffctx-1.12.0/README.md +197 -0
  4. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/Cargo.lock +16 -16
  5. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/Cargo.toml +4 -3
  6. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/limits.rs +4 -3
  7. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/selection.rs +6 -6
  8. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/discovery.rs +25 -52
  9. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/history/cochange.rs +1 -4
  10. diffctx-1.12.0/diffctx/src/edges/mod.rs +282 -0
  11. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/git.rs +31 -15
  12. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/graph.rs +199 -50
  13. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/lib.rs +2 -0
  14. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/parsers/tree_sitter_strategy.rs +6 -1
  15. diffctx-1.12.0/diffctx/src/peak_rss.rs +46 -0
  16. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/pipeline.rs +21 -4
  17. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/postpass.rs +21 -8
  18. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/project_graph.rs +16 -28
  19. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/pybridge.rs +20 -0
  20. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/render.rs +19 -5
  21. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/scoring.rs +17 -4
  22. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/signatures.rs +22 -1
  23. diffctx-1.12.0/diffctx/src/token_corpus.rs +228 -0
  24. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/types.rs +14 -1
  25. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/tests/yaml_cases.rs +2 -5
  26. {diffctx-1.11.0 → diffctx-1.12.0}/pyproject.toml +1 -17
  27. {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/cli.py +210 -56
  28. diffctx-1.12.0/src/diffctx/diffctx/graph_analytics.py +293 -0
  29. {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/diffctx/graph_export.py +16 -8
  30. {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/diffctx/pipeline.py +1 -14
  31. diffctx-1.12.0/src/diffctx/diffctx/project_graph.py +36 -0
  32. {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/ignore.py +11 -3
  33. {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/logger.py +3 -2
  34. {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/main.py +109 -33
  35. {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/tokens.py +1 -4
  36. {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/tree.py +11 -0
  37. diffctx-1.12.0/src/diffctx/version.py +1 -0
  38. {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/writer.py +28 -6
  39. diffctx-1.11.0/README.md +0 -310
  40. diffctx-1.11.0/diffctx/src/edges/mod.rs +0 -155
  41. diffctx-1.11.0/src/diffctx/diffctx/graph_analytics.py +0 -59
  42. diffctx-1.11.0/src/diffctx/diffctx/project_graph.py +0 -27
  43. diffctx-1.11.0/src/diffctx/version.py +0 -1
  44. {diffctx-1.11.0 → diffctx-1.12.0}/LICENSE +0 -0
  45. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/analytics.rs +0 -0
  46. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/candidate_files.rs +0 -0
  47. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/analytics.rs +0 -0
  48. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/bm25.rs +0 -0
  49. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/budget.rs +0 -0
  50. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/category_weights.rs +0 -0
  51. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/edge_weights.rs +0 -0
  52. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/env_overrides.rs +0 -0
  53. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/extensions.rs +0 -0
  54. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/filtering.rs +0 -0
  55. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/fragmentation.rs +0 -0
  56. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/git.rs +0 -0
  57. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/graph_filtering.rs +0 -0
  58. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/importance.rs +0 -0
  59. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/mod.rs +0 -0
  60. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/mode.rs +0 -0
  61. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/needs.rs +0 -0
  62. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/parsers.rs +0 -0
  63. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/render.rs +0 -0
  64. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/scoring.rs +0 -0
  65. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/tokenization.rs +0 -0
  66. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/config/weights.rs +0 -0
  67. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/core.rs +0 -0
  68. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/base.rs +0 -0
  69. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/config_edges/build_system.rs +0 -0
  70. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/config_edges/cicd.rs +0 -0
  71. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/config_edges/docker.rs +0 -0
  72. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/config_edges/generic.rs +0 -0
  73. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/config_edges/helm.rs +0 -0
  74. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/config_edges/kubernetes.rs +0 -0
  75. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/config_edges/mod.rs +0 -0
  76. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/document/mod.rs +0 -0
  77. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/history/mod.rs +0 -0
  78. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/ansible.rs +0 -0
  79. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/bazel.rs +0 -0
  80. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/c_family.rs +0 -0
  81. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/cargo_edges.rs +0 -0
  82. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/clojure.rs +0 -0
  83. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/css.rs +0 -0
  84. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/dart.rs +0 -0
  85. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/dbt.rs +0 -0
  86. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/dotnet.rs +0 -0
  87. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/elixir.rs +0 -0
  88. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/erlang.rs +0 -0
  89. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/go.rs +0 -0
  90. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/graphql.rs +0 -0
  91. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/haskell.rs +0 -0
  92. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/javascript.rs +0 -0
  93. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/julia.rs +0 -0
  94. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/jvm.rs +0 -0
  95. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/latex.rs +0 -0
  96. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/lua.rs +0 -0
  97. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/mod.rs +0 -0
  98. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/nim.rs +0 -0
  99. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/nix.rs +0 -0
  100. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/ocaml.rs +0 -0
  101. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/openapi.rs +0 -0
  102. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/perl.rs +0 -0
  103. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/php.rs +0 -0
  104. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/prisma.rs +0 -0
  105. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/protobuf.rs +0 -0
  106. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/python.rs +0 -0
  107. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/r_lang.rs +0 -0
  108. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/ruby.rs +0 -0
  109. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/rust_lang.rs +0 -0
  110. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/shell.rs +0 -0
  111. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/sql.rs +0 -0
  112. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/swift.rs +0 -0
  113. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/tags.rs +0 -0
  114. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/terraform.rs +0 -0
  115. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/semantic/zig.rs +0 -0
  116. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/similarity/lexical.rs +0 -0
  117. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/similarity/mod.rs +0 -0
  118. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/structural/containment.rs +0 -0
  119. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/structural/mod.rs +0 -0
  120. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/structural/sibling.rs +0 -0
  121. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/edges/structural/testing.rs +0 -0
  122. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/filtering.rs +0 -0
  123. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/fragmentation.rs +0 -0
  124. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/graph_export.rs +0 -0
  125. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/interval.rs +0 -0
  126. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/languages.rs +0 -0
  127. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/main.rs +0 -0
  128. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/memory_pipeline.rs +0 -0
  129. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/mode.rs +0 -0
  130. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/parsers/config_parser.rs +0 -0
  131. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/parsers/generic.rs +0 -0
  132. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/parsers/markdown.rs +0 -0
  133. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/parsers/mod.rs +0 -0
  134. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/ppr.rs +0 -0
  135. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/select.rs +0 -0
  136. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/stopwords.rs +0 -0
  137. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/test_harness.rs +0 -0
  138. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/tokenizer.rs +0 -0
  139. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/utility/boltzmann.rs +0 -0
  140. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/utility/importance.rs +0 -0
  141. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/utility/mod.rs +0 -0
  142. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/utility/needs.rs +0 -0
  143. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/src/utility/scoring.rs +0 -0
  144. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/tests/common/mod.rs +0 -0
  145. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/tests/fixtures/garbage/garbage_api.py +0 -0
  146. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/tests/fixtures/garbage/garbage_constants.py +0 -0
  147. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/tests/fixtures/garbage/garbage_handlers.py +0 -0
  148. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/tests/fixtures/garbage/garbage_models.py +0 -0
  149. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/tests/fixtures/garbage/garbage_module.js +0 -0
  150. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/tests/fixtures/garbage/garbage_services.py +0 -0
  151. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/tests/fixtures/garbage/garbage_types.py +0 -0
  152. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/tests/fixtures/garbage/garbage_unrelated.yaml +0 -0
  153. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/tests/fixtures/garbage/garbage_utils.py +0 -0
  154. {diffctx-1.11.0 → diffctx-1.12.0}/diffctx/tests/fixtures/garbage/garbage_validators.py +0 -0
  155. {diffctx-1.11.0 → diffctx-1.12.0}/rust-toolchain.toml +0 -0
  156. {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/__init__.py +0 -0
  157. {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/__main__.py +0 -0
  158. {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/clipboard.py +0 -0
  159. {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/diffctx/__init__.py +0 -0
  160. {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/mcp/README.md +0 -0
  161. {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/mcp/__init__.py +0 -0
  162. {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/mcp/__main__.py +0 -0
  163. {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/mcp/formatting.py +0 -0
  164. {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/mcp/security.py +0 -0
  165. {diffctx-1.11.0 → diffctx-1.12.0}/src/diffctx/mcp/server.py +0 -0
  166. {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.11.0
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 (30 seconds)
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 for repeat use — isolated CLI, no venv needed
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
- > For everyday use, install once with `pipx` and call `diffctx` from any
111
- > directory. Do **not** `source` the project's `.venv` to run `diffctx` from
112
- > another repo that runs a working-tree build and mutates the shell's
113
- > `PATH`/`PYTHONHOME` for every subsequent command.
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
- ![diffctx demo: running `diffctx . --diff HEAD~1` inside a git repo and copying the relevance-ranked YAML output to the clipboard for an LLM](https://raw.githubusercontent.com/nikolay-e/diffctx/main/docs/demo.gif)
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
- ### How it works
124
+ ![diffctx demo](https://raw.githubusercontent.com/nikolay-e/diffctx/main/docs/demo.gif)
153
125
 
154
- Builds a code graph (imports, co-changes, type refs) and propagates
155
- relevance from changed lines outward across it. Three scoring modes are
156
- available pick one with `--scoring`:
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
- | `--scoring` | What it does |
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
- Selection stops when relevance drops below `--tau` (the minimum score a
165
- fragment must beat to be kept), or once `--budget` tokens have been
166
- emitted, whichever comes first.
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` | Scoring mode: `ego`, `ppr`, or `bm25` |
171
- | `--budget` | auto | Token cap. `auto` lets selection converge; `-1` disables the cap; `N` enforces a fixed cap |
172
- | `--alpha` | 0.60 | How tightly context clusters around changes (PPR damping; 0–1, higher = more focused) |
173
- | `--tau` | 0.12 | Minimum relevance required to include a fragment (lower = more context) |
174
- | `--full` | false | Include every changed fragment; skip the smart-selection step entirely |
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 is documented
177
- in [`docs/parameter-strategy.md`](docs/parameter-strategy.md).
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
- For exploring the underlying dependency graph directly (without a diff),
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
- Full codebase export output format:
221
-
222
- ```yaml
223
- name: myproject
224
- type: directory
225
- children:
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("."), # repository root
300
- "HEAD~1..HEAD", # diff range; also accepts "main..feature"
301
- budget_tokens=None, # None = convergence-based (default)
302
- # 0 = diff only, no expansion (recall floor)
303
- # <0 = unlimited (10M-token soft ceiling)
304
- # >0 = explicit token cap
305
- alpha=0.6, # PPR damping factor
306
- tau=0.12, # stopping threshold
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
- yaml_str = to_yaml(ctx)
312
- ```
313
-
314
- ## MCP Server
206
+ print(to_markdown(ctx))
315
207
 
316
- diffctx includes an [MCP](https://modelcontextprotocol.io) server that lets
317
- AI assistants (Claude Code, Cursor, Windsurf, etc.) call diff context analysis
318
- automatically during code review.
319
-
320
- ```bash
321
- pip install 'diffctx[mcp]'
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
- Add to your MCP client config (e.g. `~/.claude/mcp.json` for Claude Code):
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. Your AI assistant will
337
- automatically call it when reviewing PRs, explaining changes, or investigating
338
- broken tests no manual invocation needed.
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 Patterns
242
+ ## Ignore patterns
344
243
 
345
- Respects `.gitignore` and `.diffctx/ignore` automatically.
346
- Use `--no-default-ignores` to disable built-in patterns
347
- (`.gitignore` and `.diffctx/ignore` still apply).
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
- - Hierarchical: nested ignore files at each directory level
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 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` |
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
@@ -0,0 +1,197 @@
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
+ [![License](https://img.shields.io/pypi/l/diffctx)](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
+ ![diffctx demo](https://raw.githubusercontent.com/nikolay-e/diffctx/main/docs/demo.gif)
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