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.
- diffcontext-0.5.1/LICENSE +21 -0
- diffcontext-0.5.1/PKG-INFO +219 -0
- diffcontext-0.5.1/README.md +185 -0
- diffcontext-0.5.1/diffcontext/__init__.py +233 -0
- diffcontext-0.5.1/diffcontext/_warn_once.py +112 -0
- diffcontext-0.5.1/diffcontext/cache.py +216 -0
- diffcontext-0.5.1/diffcontext/cli/__init__.py +655 -0
- diffcontext-0.5.1/diffcontext/context/__init__.py +1 -0
- diffcontext-0.5.1/diffcontext/context/compiler.py +643 -0
- diffcontext-0.5.1/diffcontext/context/selector.py +258 -0
- diffcontext-0.5.1/diffcontext/diff/__init__.py +1 -0
- diffcontext-0.5.1/diffcontext/diff/git_diff.py +298 -0
- diffcontext-0.5.1/diffcontext/diff/state_manager.py +75 -0
- diffcontext-0.5.1/diffcontext/graph_builder.py +1026 -0
- diffcontext-0.5.1/diffcontext/history.py +154 -0
- diffcontext-0.5.1/diffcontext/impact/__init__.py +1 -0
- diffcontext-0.5.1/diffcontext/impact/blast_radius.py +58 -0
- diffcontext-0.5.1/diffcontext/impact/scoring.py +223 -0
- diffcontext-0.5.1/diffcontext/impact/traversal.py +58 -0
- diffcontext-0.5.1/diffcontext/impact/visualizer.py +338 -0
- diffcontext-0.5.1/diffcontext/languages/__init__.py +80 -0
- diffcontext-0.5.1/diffcontext/languages/typescript.py +960 -0
- diffcontext-0.5.1/diffcontext/lexical.py +108 -0
- diffcontext-0.5.1/diffcontext/models.py +180 -0
- diffcontext-0.5.1/diffcontext/parser.py +183 -0
- diffcontext-0.5.1/diffcontext/pipeline.py +887 -0
- diffcontext-0.5.1/diffcontext/py.typed +0 -0
- diffcontext-0.5.1/diffcontext/rerank/__init__.py +17 -0
- diffcontext-0.5.1/diffcontext/rerank/features.py +356 -0
- diffcontext-0.5.1/diffcontext/rerank/model.py +175 -0
- diffcontext-0.5.1/diffcontext/resolver.py +288 -0
- diffcontext-0.5.1/diffcontext/scanner.py +153 -0
- diffcontext-0.5.1/diffcontext/symbols.py +254 -0
- diffcontext-0.5.1/diffcontext/verify/__init__.py +68 -0
- diffcontext-0.5.1/diffcontext/verify/cases.py +631 -0
- diffcontext-0.5.1/diffcontext/verify/history.py +396 -0
- diffcontext-0.5.1/diffcontext/verify/sufficiency.py +324 -0
- diffcontext-0.5.1/diffcontext.egg-info/PKG-INFO +219 -0
- diffcontext-0.5.1/diffcontext.egg-info/SOURCES.txt +68 -0
- diffcontext-0.5.1/diffcontext.egg-info/dependency_links.txt +1 -0
- diffcontext-0.5.1/diffcontext.egg-info/entry_points.txt +2 -0
- diffcontext-0.5.1/diffcontext.egg-info/requires.txt +10 -0
- diffcontext-0.5.1/diffcontext.egg-info/top_level.txt +1 -0
- diffcontext-0.5.1/pyproject.toml +104 -0
- diffcontext-0.5.1/setup.cfg +4 -0
- diffcontext-0.5.1/tests/test_cache.py +145 -0
- diffcontext-0.5.1/tests/test_compiler_meta.py +208 -0
- diffcontext-0.5.1/tests/test_core.py +309 -0
- diffcontext-0.5.1/tests/test_cutoff.py +149 -0
- diffcontext-0.5.1/tests/test_determinism.py +55 -0
- diffcontext-0.5.1/tests/test_downstream_eval.py +336 -0
- diffcontext-0.5.1/tests/test_git_diff.py +144 -0
- diffcontext-0.5.1/tests/test_harness_api.py +176 -0
- diffcontext-0.5.1/tests/test_history.py +122 -0
- diffcontext-0.5.1/tests/test_incremental.py +213 -0
- diffcontext-0.5.1/tests/test_index_scoping.py +137 -0
- diffcontext-0.5.1/tests/test_lexical.py +186 -0
- diffcontext-0.5.1/tests/test_rerank.py +316 -0
- diffcontext-0.5.1/tests/test_semantic_ablation.py +78 -0
- diffcontext-0.5.1/tests/test_semantic_audit.py +118 -0
- diffcontext-0.5.1/tests/test_semantic_gap.py +88 -0
- diffcontext-0.5.1/tests/test_semantic_metrics.py +36 -0
- diffcontext-0.5.1/tests/test_semantic_mine.py +69 -0
- diffcontext-0.5.1/tests/test_service_clone.py +165 -0
- diffcontext-0.5.1/tests/test_src_layout.py +149 -0
- diffcontext-0.5.1/tests/test_stats.py +139 -0
- diffcontext-0.5.1/tests/test_token_budget.py +153 -0
- diffcontext-0.5.1/tests/test_typescript_adapter.py +373 -0
- diffcontext-0.5.1/tests/test_verify.py +368 -0
- 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
|
+
[](https://www.python.org)
|
|
41
|
+
[](https://github.com/trakshan-mishra/Diffcontext/actions)
|
|
42
|
+
[](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
|
+
[](https://www.python.org)
|
|
7
|
+
[](https://github.com/trakshan-mishra/Diffcontext/actions)
|
|
8
|
+
[](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
|
+
)
|