diffcontext 0.5.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 (70) hide show
  1. diffcontext-0.5.1/LICENSE +21 -0
  2. diffcontext-0.5.1/PKG-INFO +219 -0
  3. diffcontext-0.5.1/README.md +185 -0
  4. diffcontext-0.5.1/diffcontext/__init__.py +233 -0
  5. diffcontext-0.5.1/diffcontext/_warn_once.py +112 -0
  6. diffcontext-0.5.1/diffcontext/cache.py +216 -0
  7. diffcontext-0.5.1/diffcontext/cli/__init__.py +655 -0
  8. diffcontext-0.5.1/diffcontext/context/__init__.py +1 -0
  9. diffcontext-0.5.1/diffcontext/context/compiler.py +643 -0
  10. diffcontext-0.5.1/diffcontext/context/selector.py +258 -0
  11. diffcontext-0.5.1/diffcontext/diff/__init__.py +1 -0
  12. diffcontext-0.5.1/diffcontext/diff/git_diff.py +298 -0
  13. diffcontext-0.5.1/diffcontext/diff/state_manager.py +75 -0
  14. diffcontext-0.5.1/diffcontext/graph_builder.py +1026 -0
  15. diffcontext-0.5.1/diffcontext/history.py +154 -0
  16. diffcontext-0.5.1/diffcontext/impact/__init__.py +1 -0
  17. diffcontext-0.5.1/diffcontext/impact/blast_radius.py +58 -0
  18. diffcontext-0.5.1/diffcontext/impact/scoring.py +223 -0
  19. diffcontext-0.5.1/diffcontext/impact/traversal.py +58 -0
  20. diffcontext-0.5.1/diffcontext/impact/visualizer.py +338 -0
  21. diffcontext-0.5.1/diffcontext/languages/__init__.py +80 -0
  22. diffcontext-0.5.1/diffcontext/languages/typescript.py +960 -0
  23. diffcontext-0.5.1/diffcontext/lexical.py +108 -0
  24. diffcontext-0.5.1/diffcontext/models.py +180 -0
  25. diffcontext-0.5.1/diffcontext/parser.py +183 -0
  26. diffcontext-0.5.1/diffcontext/pipeline.py +887 -0
  27. diffcontext-0.5.1/diffcontext/py.typed +0 -0
  28. diffcontext-0.5.1/diffcontext/rerank/__init__.py +17 -0
  29. diffcontext-0.5.1/diffcontext/rerank/features.py +356 -0
  30. diffcontext-0.5.1/diffcontext/rerank/model.py +175 -0
  31. diffcontext-0.5.1/diffcontext/resolver.py +288 -0
  32. diffcontext-0.5.1/diffcontext/scanner.py +153 -0
  33. diffcontext-0.5.1/diffcontext/symbols.py +254 -0
  34. diffcontext-0.5.1/diffcontext/verify/__init__.py +68 -0
  35. diffcontext-0.5.1/diffcontext/verify/cases.py +631 -0
  36. diffcontext-0.5.1/diffcontext/verify/history.py +396 -0
  37. diffcontext-0.5.1/diffcontext/verify/sufficiency.py +324 -0
  38. diffcontext-0.5.1/diffcontext.egg-info/PKG-INFO +219 -0
  39. diffcontext-0.5.1/diffcontext.egg-info/SOURCES.txt +68 -0
  40. diffcontext-0.5.1/diffcontext.egg-info/dependency_links.txt +1 -0
  41. diffcontext-0.5.1/diffcontext.egg-info/entry_points.txt +2 -0
  42. diffcontext-0.5.1/diffcontext.egg-info/requires.txt +10 -0
  43. diffcontext-0.5.1/diffcontext.egg-info/top_level.txt +1 -0
  44. diffcontext-0.5.1/pyproject.toml +104 -0
  45. diffcontext-0.5.1/setup.cfg +4 -0
  46. diffcontext-0.5.1/tests/test_cache.py +145 -0
  47. diffcontext-0.5.1/tests/test_compiler_meta.py +208 -0
  48. diffcontext-0.5.1/tests/test_core.py +309 -0
  49. diffcontext-0.5.1/tests/test_cutoff.py +149 -0
  50. diffcontext-0.5.1/tests/test_determinism.py +55 -0
  51. diffcontext-0.5.1/tests/test_downstream_eval.py +336 -0
  52. diffcontext-0.5.1/tests/test_git_diff.py +144 -0
  53. diffcontext-0.5.1/tests/test_harness_api.py +176 -0
  54. diffcontext-0.5.1/tests/test_history.py +122 -0
  55. diffcontext-0.5.1/tests/test_incremental.py +213 -0
  56. diffcontext-0.5.1/tests/test_index_scoping.py +137 -0
  57. diffcontext-0.5.1/tests/test_lexical.py +186 -0
  58. diffcontext-0.5.1/tests/test_rerank.py +316 -0
  59. diffcontext-0.5.1/tests/test_semantic_ablation.py +78 -0
  60. diffcontext-0.5.1/tests/test_semantic_audit.py +118 -0
  61. diffcontext-0.5.1/tests/test_semantic_gap.py +88 -0
  62. diffcontext-0.5.1/tests/test_semantic_metrics.py +36 -0
  63. diffcontext-0.5.1/tests/test_semantic_mine.py +69 -0
  64. diffcontext-0.5.1/tests/test_service_clone.py +165 -0
  65. diffcontext-0.5.1/tests/test_src_layout.py +149 -0
  66. diffcontext-0.5.1/tests/test_stats.py +139 -0
  67. diffcontext-0.5.1/tests/test_token_budget.py +153 -0
  68. diffcontext-0.5.1/tests/test_typescript_adapter.py +373 -0
  69. diffcontext-0.5.1/tests/test_verify.py +368 -0
  70. diffcontext-0.5.1/tests/test_version.py +41 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Trakshan Mishra
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,219 @@
1
+ Metadata-Version: 2.4
2
+ Name: diffcontext
3
+ Version: 0.5.1
4
+ Summary: Static-analysis-powered repository context compiler for LLMs
5
+ Author-email: Trakshan Mishra <trakshanmishra477@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/trakshan-mishra/diffcontext
8
+ Project-URL: Changelog, https://github.com/trakshan-mishra/diffcontext/blob/main/CHANGELOG.md
9
+ Keywords: llm,context,static-analysis,call-graph,retrieval,code-search
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: License :: OSI Approved :: MIT License
20
+ Classifier: Operating System :: OS Independent
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7.0; extra == "dev"
27
+ Requires-Dist: ruff>=0.4; extra == "dev"
28
+ Requires-Dist: mypy>=1.8; extra == "dev"
29
+ Provides-Extra: typescript
30
+ Requires-Dist: tree-sitter>=0.21; extra == "typescript"
31
+ Requires-Dist: tree-sitter-typescript>=0.21; extra == "typescript"
32
+ Requires-Dist: tree-sitter-javascript>=0.21; extra == "typescript"
33
+ Dynamic: license-file
34
+
35
+ # DiffContext
36
+
37
+ **Show an AI coding assistant only the code that matters for the change it is
38
+ making.**
39
+
40
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue)](https://www.python.org)
41
+ [![CI](https://img.shields.io/github/actions/workflow/status/trakshan-mishra/Diffcontext/test.yml?branch=main)](https://github.com/trakshan-mishra/Diffcontext/actions)
42
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
43
+
44
+ DiffContext is a **context compiler for LLM coding agents**. Give it a Python
45
+ repository and a change — a git diff, a branch, or a single function name —
46
+ and it returns the small set of functions the model actually needs to make
47
+ that change safely: the callers that will break, the subclasses that override
48
+ it, the tests that cover it. It fits them to whatever token budget you have,
49
+ and it tells the model what it had to leave out.
50
+
51
+ It is built for people wiring LLMs into real codebases — agent loops, PR
52
+ review bots, CI checks — anywhere you have to decide what goes in the prompt
53
+ and the repository is far too large to send.
54
+
55
+ ## The problem
56
+
57
+ Ask an assistant to change one function in a 50,000-line project and you have
58
+ three bad options: paste the whole repository (it does not fit, and models get
59
+ worse in very large contexts), paste just that one function (the model breaks
60
+ three callers it never saw), or grep for the name (grep cannot find the
61
+ subclass that overrides it, or the handler that receives it through
62
+ `functools.partial` — we measured grep's recall *plateauing* no matter how
63
+ much budget you give it).
64
+
65
+ DiffContext is the fourth option. Parse the repository once into a real
66
+ dependency graph, then for any change select the few functions that actually
67
+ matter and pack them into the smallest useful prompt.
68
+
69
+ ```
70
+ git change ──► changed functions ──► hybrid retrieval ──► token budget ──► LLM-ready context
71
+ graph ∪ BM25 ∪ file top-k + tokens
72
+ ```
73
+
74
+ ## Does it make the model better?
75
+
76
+ Yes — measured end to end, not by proxy. On 128 ContextBench Python tasks
77
+ judged by each repository's own test suite (no LLM-as-judge), **context
78
+ roughly quadruples pass@1: 5.5% → 25.8%**, exact McNemar p < 0.0001.
79
+
80
+ Two qualifiers, both in [`benchmarks/contextbench/RESULTS.md`](benchmarks/contextbench/RESULTS.md)
81
+ §6: **(a)** the seed functions given to every arm are **oracle** — extracted
82
+ from the gold patch — so this measures *"given correct localization, does
83
+ context quality matter?"*, not end-to-end issue solving (localization is
84
+ handed to every arm for free); **(b)** 121 of the 128 effective tasks are
85
+ django, so this is largely a django result.
86
+
87
+ The honest companion: the three context variants (default / gap / depboost)
88
+ are statistically **indistinguishable** from each other, p = 0.36–0.81. The
89
+ win is context versus no context — not this selector versus that one. Full
90
+ results: [`benchmarks/contextbench/RESULTS.md`](benchmarks/contextbench/RESULTS.md).
91
+
92
+ ## Install
93
+
94
+ ```bash
95
+ pip install diffcontext
96
+ ```
97
+
98
+ Zero runtime dependencies, Python 3.9+.
99
+
100
+ From source for development:
101
+
102
+ ```bash
103
+ git clone https://github.com/trakshan-mishra/Diffcontext.git
104
+ cd Diffcontext && pip install -e .
105
+ ```
106
+
107
+ ## Quick start
108
+
109
+ ```bash
110
+ diffcontext index /path/to/project # cold: seconds; warm: ~0.02s
111
+ diffcontext compile --ref HEAD~1 --max-tokens 8000
112
+ diffcontext verify --from-history 20 --calibrate
113
+ ```
114
+
115
+ More commands: [USAGE.md](USAGE.md). Production recipes: [docs/USE_CASES.md](docs/USE_CASES.md).
116
+
117
+ ## What this is not
118
+
119
+ - **Not a code generator.** It selects and packs context; the model writes
120
+ the code.
121
+ - **Not precision-first.** It casts a wide net — mean precision is under 0.1
122
+ at the default top-k. Use `--cutoff gap` if you pay per token.
123
+ - **Not multi-language yet.** Python is fully supported. TypeScript/JS (ESM)
124
+ is a working prototype; CommonJS is a measured failure mode.
125
+ - **Not a replacement for reading the code.** Static analysis has blind spots,
126
+ itemized below and in [docs/BENCHMARKS.md](docs/BENCHMARKS.md).
127
+
128
+ ## Retrieval quality (measured, not claimed)
129
+
130
+ Ground truth is mined from git history — *a developer changed these functions
131
+ together in one commit; shown one, does the tool find the others?* Measured on
132
+ **701 real commits across 9 Python repositories**, and re-run as a CI gate on
133
+ every push so quality cannot silently regress.
134
+
135
+ Per-commit hit / recall of real co-change partners, hybrid retrieval:
136
+
137
+ | | django | click | flask | httpx | pydantic | black* | requests* |
138
+ |---|---|---|---|---|---|---|---|
139
+ | Hit | 0.894 | 0.889 | 0.863 | 0.935 | 0.758 | 0.897 | 0.953 |
140
+ | Recall | 0.774 | 0.750 | 0.694 | 0.772 | 0.536 | 0.712 | 0.762 |
141
+
142
+ \* validation repos, never used for tuning. Full table across all 9 repos:
143
+ [benchmarks/README.md](benchmarks/README.md).
144
+
145
+ Head-to-head vs grep at identical token budgets, grep **plateaus** at
146
+ 0.215 recall past 4k tokens while DiffContext reaches 0.576 at 8k
147
+ (2.7×). The honest flip side: mean precision is under 0.1 at the default
148
+ top-k — most retrieved symbols are supporting context, not the exact
149
+ co-change set. `--cutoff gap` cuts at the largest score drop for ~4×
150
+ precision at ~30% recall cost (co-change benchmark; 2.2× / ~14% on
151
+ ContextBench).
152
+
153
+ ## I audited my own benchmark, and three of my claims lost
154
+
155
+ A 2026-07 pass attacked the *evaluation* instead of the tool. Three published
156
+ numbers did not survive:
157
+
158
+ - **Calibration** — the only citable number (r=0.274, n≈25) was measured on a
159
+ polluted index. Re-measured clean at n=1,080 the legacy score gets
160
+ **r=0.016 (p=0.60)**: no relationship at all. Fixed by shrinking toward
161
+ "don't know" → **r=0.287 (p=0.0001)** — a ranking signal, not a probability.
162
+ - **Blend weights** — the shipped [0.5, 0.35, 0.15] failed leave-one-repo-out;
163
+ every fold picked a less graph-heavy blend. Now [0.3, 0.5, 0.2].
164
+ - **Dense baseline** — a TF-IDF stand-in had overstated dense retrieval (0.664,
165
+ beating BM25 5/5). The real MiniLM encoder scores 0.597 and beats BM25 only
166
+ 2/5. Two prior conclusions corrected on the record.
167
+
168
+ Full write-up: [docs/auditing-my-own-benchmark.md](docs/auditing-my-own-benchmark.md)
169
+ · raw pass: [benchmarks/RIGOR_REPORT_2026-07.md](benchmarks/RIGOR_REPORT_2026-07.md).
170
+
171
+ **Don't trust our benchmarks — run yours (2 minutes):**
172
+ `diffcontext verify --from-history 20 --calibrate` mines test cases from
173
+ *your* repo's git history and grades retrieval against them — and prints
174
+ **NULL RESULT** rather than a decorative number when the tool doesn't fit
175
+ your repo. Finding that out *is* the feature.
176
+
177
+ ## Use as a library
178
+
179
+ ```python
180
+ from diffcontext.pipeline import index_repository, analyze_impact, compile
181
+
182
+ idx = index_repository("/path/to/repo")
183
+ impact = analyze_impact(idx, ["./src/auth.py:validate_jwt"])
184
+ ctx = compile(idx, impact, max_tokens=8000, top_k=20)
185
+ print(ctx.text) # paste-ready, meta-header discloses what was dropped
186
+ ```
187
+
188
+ Incremental API (`idx.update([...])`), structured output, pluggable tokenizer:
189
+ [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
190
+
191
+ ## Language support
192
+
193
+ | Language | Status | Retrieval quality |
194
+ |---|---|---|
195
+ | Python | **Full** | Benchmarked: 701 commits, 5 repos + 4 validation repos |
196
+ | TypeScript / JS (ESM) | **Prototype** | Mean recall **0–68% depending on code style** |
197
+ | JavaScript (CommonJS) | **Unsupported** | Measured **0.0%** on express — do not use |
198
+
199
+ ## Known limitations (measured, not guessed)
200
+
201
+ Static analysis has a ceiling: thematic siblings with no call between them,
202
+ cross-subsystem conceptual links (all methods score **0/20**), and dynamic
203
+ dispatch are measured blind spots — itemized in
204
+ [docs/BENCHMARKS.md](docs/BENCHMARKS.md). When in doubt:
205
+ `grep -rn "function_name(" --include="*.py" .` before fully trusting
206
+ "no callers found."
207
+
208
+ ## More
209
+
210
+ - [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — pipeline, module map, agent API
211
+ - [docs/BENCHMARKS.md](docs/BENCHMARKS.md) — all numbers, downstream pass@1, limitations
212
+ - [docs/ROADMAP.md](docs/ROADMAP.md) — prioritized plan with measured motivations
213
+ - [diffcontext-service/](diffcontext-service/) — FastAPI service + web UI
214
+ - [observability/](observability/) — retrieval pipeline tracing
215
+ - [CONTRIBUTING.md](CONTRIBUTING.md) — setup, CI gates, adapter development
216
+
217
+ ## License
218
+
219
+ MIT
@@ -0,0 +1,185 @@
1
+ # DiffContext
2
+
3
+ **Show an AI coding assistant only the code that matters for the change it is
4
+ making.**
5
+
6
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue)](https://www.python.org)
7
+ [![CI](https://img.shields.io/github/actions/workflow/status/trakshan-mishra/Diffcontext/test.yml?branch=main)](https://github.com/trakshan-mishra/Diffcontext/actions)
8
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
9
+
10
+ DiffContext is a **context compiler for LLM coding agents**. Give it a Python
11
+ repository and a change — a git diff, a branch, or a single function name —
12
+ and it returns the small set of functions the model actually needs to make
13
+ that change safely: the callers that will break, the subclasses that override
14
+ it, the tests that cover it. It fits them to whatever token budget you have,
15
+ and it tells the model what it had to leave out.
16
+
17
+ It is built for people wiring LLMs into real codebases — agent loops, PR
18
+ review bots, CI checks — anywhere you have to decide what goes in the prompt
19
+ and the repository is far too large to send.
20
+
21
+ ## The problem
22
+
23
+ Ask an assistant to change one function in a 50,000-line project and you have
24
+ three bad options: paste the whole repository (it does not fit, and models get
25
+ worse in very large contexts), paste just that one function (the model breaks
26
+ three callers it never saw), or grep for the name (grep cannot find the
27
+ subclass that overrides it, or the handler that receives it through
28
+ `functools.partial` — we measured grep's recall *plateauing* no matter how
29
+ much budget you give it).
30
+
31
+ DiffContext is the fourth option. Parse the repository once into a real
32
+ dependency graph, then for any change select the few functions that actually
33
+ matter and pack them into the smallest useful prompt.
34
+
35
+ ```
36
+ git change ──► changed functions ──► hybrid retrieval ──► token budget ──► LLM-ready context
37
+ graph ∪ BM25 ∪ file top-k + tokens
38
+ ```
39
+
40
+ ## Does it make the model better?
41
+
42
+ Yes — measured end to end, not by proxy. On 128 ContextBench Python tasks
43
+ judged by each repository's own test suite (no LLM-as-judge), **context
44
+ roughly quadruples pass@1: 5.5% → 25.8%**, exact McNemar p < 0.0001.
45
+
46
+ Two qualifiers, both in [`benchmarks/contextbench/RESULTS.md`](benchmarks/contextbench/RESULTS.md)
47
+ §6: **(a)** the seed functions given to every arm are **oracle** — extracted
48
+ from the gold patch — so this measures *"given correct localization, does
49
+ context quality matter?"*, not end-to-end issue solving (localization is
50
+ handed to every arm for free); **(b)** 121 of the 128 effective tasks are
51
+ django, so this is largely a django result.
52
+
53
+ The honest companion: the three context variants (default / gap / depboost)
54
+ are statistically **indistinguishable** from each other, p = 0.36–0.81. The
55
+ win is context versus no context — not this selector versus that one. Full
56
+ results: [`benchmarks/contextbench/RESULTS.md`](benchmarks/contextbench/RESULTS.md).
57
+
58
+ ## Install
59
+
60
+ ```bash
61
+ pip install diffcontext
62
+ ```
63
+
64
+ Zero runtime dependencies, Python 3.9+.
65
+
66
+ From source for development:
67
+
68
+ ```bash
69
+ git clone https://github.com/trakshan-mishra/Diffcontext.git
70
+ cd Diffcontext && pip install -e .
71
+ ```
72
+
73
+ ## Quick start
74
+
75
+ ```bash
76
+ diffcontext index /path/to/project # cold: seconds; warm: ~0.02s
77
+ diffcontext compile --ref HEAD~1 --max-tokens 8000
78
+ diffcontext verify --from-history 20 --calibrate
79
+ ```
80
+
81
+ More commands: [USAGE.md](USAGE.md). Production recipes: [docs/USE_CASES.md](docs/USE_CASES.md).
82
+
83
+ ## What this is not
84
+
85
+ - **Not a code generator.** It selects and packs context; the model writes
86
+ the code.
87
+ - **Not precision-first.** It casts a wide net — mean precision is under 0.1
88
+ at the default top-k. Use `--cutoff gap` if you pay per token.
89
+ - **Not multi-language yet.** Python is fully supported. TypeScript/JS (ESM)
90
+ is a working prototype; CommonJS is a measured failure mode.
91
+ - **Not a replacement for reading the code.** Static analysis has blind spots,
92
+ itemized below and in [docs/BENCHMARKS.md](docs/BENCHMARKS.md).
93
+
94
+ ## Retrieval quality (measured, not claimed)
95
+
96
+ Ground truth is mined from git history — *a developer changed these functions
97
+ together in one commit; shown one, does the tool find the others?* Measured on
98
+ **701 real commits across 9 Python repositories**, and re-run as a CI gate on
99
+ every push so quality cannot silently regress.
100
+
101
+ Per-commit hit / recall of real co-change partners, hybrid retrieval:
102
+
103
+ | | django | click | flask | httpx | pydantic | black* | requests* |
104
+ |---|---|---|---|---|---|---|---|
105
+ | Hit | 0.894 | 0.889 | 0.863 | 0.935 | 0.758 | 0.897 | 0.953 |
106
+ | Recall | 0.774 | 0.750 | 0.694 | 0.772 | 0.536 | 0.712 | 0.762 |
107
+
108
+ \* validation repos, never used for tuning. Full table across all 9 repos:
109
+ [benchmarks/README.md](benchmarks/README.md).
110
+
111
+ Head-to-head vs grep at identical token budgets, grep **plateaus** at
112
+ 0.215 recall past 4k tokens while DiffContext reaches 0.576 at 8k
113
+ (2.7×). The honest flip side: mean precision is under 0.1 at the default
114
+ top-k — most retrieved symbols are supporting context, not the exact
115
+ co-change set. `--cutoff gap` cuts at the largest score drop for ~4×
116
+ precision at ~30% recall cost (co-change benchmark; 2.2× / ~14% on
117
+ ContextBench).
118
+
119
+ ## I audited my own benchmark, and three of my claims lost
120
+
121
+ A 2026-07 pass attacked the *evaluation* instead of the tool. Three published
122
+ numbers did not survive:
123
+
124
+ - **Calibration** — the only citable number (r=0.274, n≈25) was measured on a
125
+ polluted index. Re-measured clean at n=1,080 the legacy score gets
126
+ **r=0.016 (p=0.60)**: no relationship at all. Fixed by shrinking toward
127
+ "don't know" → **r=0.287 (p=0.0001)** — a ranking signal, not a probability.
128
+ - **Blend weights** — the shipped [0.5, 0.35, 0.15] failed leave-one-repo-out;
129
+ every fold picked a less graph-heavy blend. Now [0.3, 0.5, 0.2].
130
+ - **Dense baseline** — a TF-IDF stand-in had overstated dense retrieval (0.664,
131
+ beating BM25 5/5). The real MiniLM encoder scores 0.597 and beats BM25 only
132
+ 2/5. Two prior conclusions corrected on the record.
133
+
134
+ Full write-up: [docs/auditing-my-own-benchmark.md](docs/auditing-my-own-benchmark.md)
135
+ · raw pass: [benchmarks/RIGOR_REPORT_2026-07.md](benchmarks/RIGOR_REPORT_2026-07.md).
136
+
137
+ **Don't trust our benchmarks — run yours (2 minutes):**
138
+ `diffcontext verify --from-history 20 --calibrate` mines test cases from
139
+ *your* repo's git history and grades retrieval against them — and prints
140
+ **NULL RESULT** rather than a decorative number when the tool doesn't fit
141
+ your repo. Finding that out *is* the feature.
142
+
143
+ ## Use as a library
144
+
145
+ ```python
146
+ from diffcontext.pipeline import index_repository, analyze_impact, compile
147
+
148
+ idx = index_repository("/path/to/repo")
149
+ impact = analyze_impact(idx, ["./src/auth.py:validate_jwt"])
150
+ ctx = compile(idx, impact, max_tokens=8000, top_k=20)
151
+ print(ctx.text) # paste-ready, meta-header discloses what was dropped
152
+ ```
153
+
154
+ Incremental API (`idx.update([...])`), structured output, pluggable tokenizer:
155
+ [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
156
+
157
+ ## Language support
158
+
159
+ | Language | Status | Retrieval quality |
160
+ |---|---|---|
161
+ | Python | **Full** | Benchmarked: 701 commits, 5 repos + 4 validation repos |
162
+ | TypeScript / JS (ESM) | **Prototype** | Mean recall **0–68% depending on code style** |
163
+ | JavaScript (CommonJS) | **Unsupported** | Measured **0.0%** on express — do not use |
164
+
165
+ ## Known limitations (measured, not guessed)
166
+
167
+ Static analysis has a ceiling: thematic siblings with no call between them,
168
+ cross-subsystem conceptual links (all methods score **0/20**), and dynamic
169
+ dispatch are measured blind spots — itemized in
170
+ [docs/BENCHMARKS.md](docs/BENCHMARKS.md). When in doubt:
171
+ `grep -rn "function_name(" --include="*.py" .` before fully trusting
172
+ "no callers found."
173
+
174
+ ## More
175
+
176
+ - [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — pipeline, module map, agent API
177
+ - [docs/BENCHMARKS.md](docs/BENCHMARKS.md) — all numbers, downstream pass@1, limitations
178
+ - [docs/ROADMAP.md](docs/ROADMAP.md) — prioritized plan with measured motivations
179
+ - [diffcontext-service/](diffcontext-service/) — FastAPI service + web UI
180
+ - [observability/](observability/) — retrieval pipeline tracing
181
+ - [CONTRIBUTING.md](CONTRIBUTING.md) — setup, CI gates, adapter development
182
+
183
+ ## License
184
+
185
+ MIT
@@ -0,0 +1,233 @@
1
+ """
2
+ DiffContext — static-analysis-powered repository context compiler for LLMs.
3
+
4
+ Converts code changes into dependency-aware, blast-radius-aware context
5
+ packages, enabling far more accurate code understanding than keyword search
6
+ or traditional RAG.
7
+
8
+ Usage as a library:
9
+
10
+ from diffcontext import blast_radius, index, diff, compile_context
11
+
12
+ # Get blast radius for a symbol
13
+ result = blast_radius("./auth.py:validate_jwt", repo="/path/to/project")
14
+ print(result.callers) # who calls this?
15
+ print(result.dependencies) # what does this call?
16
+ print(result.total_affected) # total transitive impact
17
+
18
+ # Auto-detect changes and get blast radius
19
+ result = blast_radius(ref="HEAD~1", repo="/path/to/project")
20
+
21
+ # Index a repository
22
+ idx = index("/path/to/project")
23
+ print(idx.symbols) # all parsed symbols
24
+ print(idx.graph) # call graph
25
+
26
+ # Find changed symbols from git diff
27
+ changed = diff(repo="/path/to/project", ref="HEAD~1")
28
+
29
+ # Full context compilation for LLMs
30
+ ctx = compile_context(ref="HEAD~1", repo="/path/to/project")
31
+ print(ctx.text) # LLM-ready context
32
+ print(ctx.reduction_pct) # how much code was filtered out
33
+ """
34
+
35
+ __version__ = "0.5.1"
36
+
37
+ # Public, semver-covered API. Everything not listed here (graph_builder,
38
+ # resolver, symbols, scanner, parser internals) is importable but carries no
39
+ # stability guarantee across releases.
40
+ __all__ = [
41
+ "__version__",
42
+ "BlastResult",
43
+ "CoChangeIndex",
44
+ "ContextItem",
45
+ "ScoringConfig",
46
+ "blast_radius",
47
+ "index",
48
+ "diff",
49
+ "compile_context",
50
+ ]
51
+
52
+ from dataclasses import dataclass, field
53
+ from typing import Callable, Dict, List, Optional
54
+
55
+ from .pipeline import index_repository, analyze_impact
56
+ from .pipeline import compile as _compile_pipeline
57
+ from .diff.git_diff import find_changed_symbols
58
+ # Redundant alias: intentional re-export (used by library callers even
59
+ # though nothing in this module calls it).
60
+ from .impact.blast_radius import get_blast_radius as get_blast_radius
61
+ from .history import CoChangeIndex
62
+ from .impact.scoring import ScoringConfig
63
+ from .models import ContextItem
64
+
65
+
66
+ # ---------------------------------------------------------------------------
67
+ # Public data classes
68
+ # ---------------------------------------------------------------------------
69
+
70
+ @dataclass
71
+ class BlastResult:
72
+ """Result of a blast radius analysis — the public API return type."""
73
+ changed: List[str]
74
+ callers: List[str]
75
+ dependencies: List[str]
76
+ total_affected: int
77
+ scores: Dict[str, float] = field(default_factory=dict)
78
+ graph: Dict[str, List[str]] = field(default_factory=dict)
79
+
80
+ @property
81
+ def affected_files(self) -> List[str]:
82
+ """Unique files in the blast radius."""
83
+ files = set()
84
+ for sym in self.callers:
85
+ parts = sym.split(":", 1)
86
+ if len(parts) == 2:
87
+ files.add(parts[0])
88
+ return sorted(files)
89
+
90
+ def __repr__(self):
91
+ return (
92
+ f"BlastResult(changed={len(self.changed)}, "
93
+ f"callers={len(self.callers)}, "
94
+ f"dependencies={len(self.dependencies)}, "
95
+ f"total_affected={self.total_affected})"
96
+ )
97
+
98
+
99
+ # ---------------------------------------------------------------------------
100
+ # Public API
101
+ # ---------------------------------------------------------------------------
102
+
103
+ def blast_radius(
104
+ symbol: Optional[str] = None,
105
+ *,
106
+ ref: Optional[str] = None,
107
+ repo: str = ".",
108
+ depth: int = 3,
109
+ ) -> BlastResult:
110
+ """
111
+ Compute the blast radius of a change.
112
+
113
+ Args:
114
+ symbol: Symbol ID like "./auth.py:validate_jwt". If None, uses `ref`.
115
+ ref: Git ref to auto-detect changes (e.g. "HEAD~1").
116
+ repo: Path to the repository root.
117
+ depth: Max traversal depth.
118
+
119
+ Returns:
120
+ BlastResult with callers, dependencies, scores, etc.
121
+
122
+ Examples:
123
+ >>> from diffcontext import blast_radius
124
+ >>> r = blast_radius("./auth.py:validate_jwt", repo="/path/to/project")
125
+ >>> r = blast_radius(ref="HEAD~1", repo="/path/to/project")
126
+ """
127
+ idx = index_repository(repo)
128
+
129
+ # Determine changed symbols
130
+ if symbol:
131
+ changed = [symbol]
132
+ elif ref:
133
+ changed = find_changed_symbols(repo, idx.symbols, ref=ref)
134
+ else:
135
+ changed = find_changed_symbols(repo, idx.symbols, ref="HEAD~1")
136
+
137
+ if not changed:
138
+ return BlastResult(
139
+ changed=[], callers=[], dependencies=[],
140
+ total_affected=0, scores={}, graph=idx.graph,
141
+ )
142
+
143
+ impact = analyze_impact(idx, changed, max_depth=depth)
144
+
145
+ return BlastResult(
146
+ changed=impact.changed,
147
+ callers=impact.blast_radius,
148
+ dependencies=impact.dependencies,
149
+ total_affected=len(impact.all_relevant),
150
+ scores=impact.scores,
151
+ graph=idx.graph,
152
+ )
153
+
154
+
155
+ def index(repo: str = "."):
156
+ """
157
+ Index a repository: parse all Python files and build the call graph.
158
+
159
+ Returns a RepositoryIndex with .symbols and .graph attributes.
160
+
161
+ Example:
162
+ >>> from diffcontext import index
163
+ >>> idx = index("/path/to/project")
164
+ >>> len(idx.symbols)
165
+ 354
166
+ """
167
+ return index_repository(repo)
168
+
169
+
170
+ def diff(repo: str = ".", ref: str = "HEAD~1") -> List[str]:
171
+ """
172
+ Find changed symbol IDs from git diff.
173
+
174
+ Returns list of symbol IDs that were modified.
175
+
176
+ Example:
177
+ >>> from diffcontext import diff
178
+ >>> diff(repo="/path/to/project", ref="HEAD~1")
179
+ ['./auth.py:validate_jwt', './models.py:User.__init__']
180
+ """
181
+ idx = index_repository(repo)
182
+ return find_changed_symbols(repo, idx.symbols, ref=ref)
183
+
184
+
185
+ def compile_context(
186
+ symbol: Optional[str] = None,
187
+ *,
188
+ ref: Optional[str] = None,
189
+ repo: str = ".",
190
+ depth: int = 2,
191
+ max_tokens: int = 10000,
192
+ token_counter: Optional[Callable[[str], int]] = None,
193
+ scoring_config: Optional[ScoringConfig] = None,
194
+ ):
195
+ """
196
+ Full pipeline: detect changes → blast radius → compile LLM context.
197
+
198
+ Returns a ContextPackage with .text (rendered), .items (structured
199
+ ContextItem list a harness can re-budget itself), .token_estimate,
200
+ and .reduction_pct.
201
+
202
+ Args:
203
+ token_counter: text -> token count callable. Pass your model's
204
+ real tokenizer to enforce hard window limits;
205
+ defaults to a ~4-chars/token heuristic.
206
+ scoring_config: Custom impact-scoring weights (ScoringConfig);
207
+ tuned defaults when None.
208
+
209
+ Example:
210
+ >>> from diffcontext import compile_context
211
+ >>> ctx = compile_context(ref="HEAD~1", repo="/path/to/project")
212
+ >>> print(ctx.text) # LLM-ready context
213
+ >>> ctx.items[0].symbol_id # structured form for harnesses
214
+ >>> print(ctx.reduction_pct) # e.g. 99.2
215
+ """
216
+ idx = index_repository(repo)
217
+
218
+ if symbol:
219
+ changed = [symbol]
220
+ elif ref:
221
+ changed = find_changed_symbols(repo, idx.symbols, ref=ref)
222
+ else:
223
+ changed = find_changed_symbols(repo, idx.symbols, ref="HEAD~1")
224
+
225
+ if not changed:
226
+ from .models import ContextPackage
227
+ return ContextPackage(text="", symbol_count=0, token_estimate=0, total_repo_tokens=0)
228
+
229
+ impact = analyze_impact(idx, changed, max_depth=depth, scoring_config=scoring_config)
230
+ return _compile_pipeline(
231
+ idx, impact, max_tokens=max_tokens,
232
+ token_counter=token_counter, scoring_config=scoring_config,
233
+ )