diffcontext 0.5.1__tar.gz → 0.5.2__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 (72) hide show
  1. {diffcontext-0.5.1 → diffcontext-0.5.2}/PKG-INFO +41 -25
  2. {diffcontext-0.5.1 → diffcontext-0.5.2}/README.md +38 -24
  3. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext/__init__.py +1 -1
  4. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext/cli/__init__.py +11 -1
  5. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext/context/compiler.py +138 -104
  6. diffcontext-0.5.2/diffcontext/mcp_server.py +226 -0
  7. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext/pipeline.py +10 -0
  8. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext.egg-info/PKG-INFO +41 -25
  9. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext.egg-info/SOURCES.txt +2 -0
  10. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext.egg-info/entry_points.txt +1 -0
  11. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext.egg-info/requires.txt +3 -0
  12. {diffcontext-0.5.1 → diffcontext-0.5.2}/pyproject.toml +6 -0
  13. {diffcontext-0.5.1 → diffcontext-0.5.2}/tests/test_compiler_meta.py +69 -0
  14. diffcontext-0.5.2/tests/test_mcp_server.py +111 -0
  15. {diffcontext-0.5.1 → diffcontext-0.5.2}/LICENSE +0 -0
  16. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext/_warn_once.py +0 -0
  17. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext/cache.py +0 -0
  18. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext/context/__init__.py +0 -0
  19. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext/context/selector.py +0 -0
  20. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext/diff/__init__.py +0 -0
  21. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext/diff/git_diff.py +0 -0
  22. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext/diff/state_manager.py +0 -0
  23. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext/graph_builder.py +0 -0
  24. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext/history.py +0 -0
  25. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext/impact/__init__.py +0 -0
  26. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext/impact/blast_radius.py +0 -0
  27. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext/impact/scoring.py +0 -0
  28. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext/impact/traversal.py +0 -0
  29. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext/impact/visualizer.py +0 -0
  30. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext/languages/__init__.py +0 -0
  31. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext/languages/typescript.py +0 -0
  32. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext/lexical.py +0 -0
  33. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext/models.py +0 -0
  34. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext/parser.py +0 -0
  35. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext/py.typed +0 -0
  36. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext/rerank/__init__.py +0 -0
  37. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext/rerank/features.py +0 -0
  38. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext/rerank/model.py +0 -0
  39. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext/resolver.py +0 -0
  40. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext/scanner.py +0 -0
  41. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext/symbols.py +0 -0
  42. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext/verify/__init__.py +0 -0
  43. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext/verify/cases.py +0 -0
  44. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext/verify/history.py +0 -0
  45. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext/verify/sufficiency.py +0 -0
  46. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext.egg-info/dependency_links.txt +0 -0
  47. {diffcontext-0.5.1 → diffcontext-0.5.2}/diffcontext.egg-info/top_level.txt +0 -0
  48. {diffcontext-0.5.1 → diffcontext-0.5.2}/setup.cfg +0 -0
  49. {diffcontext-0.5.1 → diffcontext-0.5.2}/tests/test_cache.py +0 -0
  50. {diffcontext-0.5.1 → diffcontext-0.5.2}/tests/test_core.py +0 -0
  51. {diffcontext-0.5.1 → diffcontext-0.5.2}/tests/test_cutoff.py +0 -0
  52. {diffcontext-0.5.1 → diffcontext-0.5.2}/tests/test_determinism.py +0 -0
  53. {diffcontext-0.5.1 → diffcontext-0.5.2}/tests/test_downstream_eval.py +0 -0
  54. {diffcontext-0.5.1 → diffcontext-0.5.2}/tests/test_git_diff.py +0 -0
  55. {diffcontext-0.5.1 → diffcontext-0.5.2}/tests/test_harness_api.py +0 -0
  56. {diffcontext-0.5.1 → diffcontext-0.5.2}/tests/test_history.py +0 -0
  57. {diffcontext-0.5.1 → diffcontext-0.5.2}/tests/test_incremental.py +0 -0
  58. {diffcontext-0.5.1 → diffcontext-0.5.2}/tests/test_index_scoping.py +0 -0
  59. {diffcontext-0.5.1 → diffcontext-0.5.2}/tests/test_lexical.py +0 -0
  60. {diffcontext-0.5.1 → diffcontext-0.5.2}/tests/test_rerank.py +0 -0
  61. {diffcontext-0.5.1 → diffcontext-0.5.2}/tests/test_semantic_ablation.py +0 -0
  62. {diffcontext-0.5.1 → diffcontext-0.5.2}/tests/test_semantic_audit.py +0 -0
  63. {diffcontext-0.5.1 → diffcontext-0.5.2}/tests/test_semantic_gap.py +0 -0
  64. {diffcontext-0.5.1 → diffcontext-0.5.2}/tests/test_semantic_metrics.py +0 -0
  65. {diffcontext-0.5.1 → diffcontext-0.5.2}/tests/test_semantic_mine.py +0 -0
  66. {diffcontext-0.5.1 → diffcontext-0.5.2}/tests/test_service_clone.py +0 -0
  67. {diffcontext-0.5.1 → diffcontext-0.5.2}/tests/test_src_layout.py +0 -0
  68. {diffcontext-0.5.1 → diffcontext-0.5.2}/tests/test_stats.py +0 -0
  69. {diffcontext-0.5.1 → diffcontext-0.5.2}/tests/test_token_budget.py +0 -0
  70. {diffcontext-0.5.1 → diffcontext-0.5.2}/tests/test_typescript_adapter.py +0 -0
  71. {diffcontext-0.5.1 → diffcontext-0.5.2}/tests/test_verify.py +0 -0
  72. {diffcontext-0.5.1 → diffcontext-0.5.2}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diffcontext
3
- Version: 0.5.1
3
+ Version: 0.5.2
4
4
  Summary: Static-analysis-powered repository context compiler for LLMs
5
5
  Author-email: Trakshan Mishra <trakshanmishra477@gmail.com>
6
6
  License: MIT
@@ -30,6 +30,8 @@ Provides-Extra: typescript
30
30
  Requires-Dist: tree-sitter>=0.21; extra == "typescript"
31
31
  Requires-Dist: tree-sitter-typescript>=0.21; extra == "typescript"
32
32
  Requires-Dist: tree-sitter-javascript>=0.21; extra == "typescript"
33
+ Provides-Extra: mcp
34
+ Requires-Dist: mcp>=1.0; extra == "mcp"
33
35
  Dynamic: license-file
34
36
 
35
37
  # DiffContext
@@ -52,6 +54,10 @@ It is built for people wiring LLMs into real codebases — agent loops, PR
52
54
  review bots, CI checks — anywhere you have to decide what goes in the prompt
53
55
  and the repository is far too large to send.
54
56
 
57
+ And it grades itself: point it at your repo and it mines your git history,
58
+ runs retrieval against real co-change pairs, and prints **NULL RESULT** when
59
+ it doesn't fit — finding that out *is* the feature.
60
+
55
61
  ## The problem
56
62
 
57
63
  Ask an assistant to change one function in a 50,000-line project and you have
@@ -71,24 +77,6 @@ git change ──► changed functions ──► hybrid retrieval ──► toke
71
77
  graph ∪ BM25 ∪ file top-k + tokens
72
78
  ```
73
79
 
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
80
  ## Install
93
81
 
94
82
  ```bash
@@ -97,6 +85,14 @@ pip install diffcontext
97
85
 
98
86
  Zero runtime dependencies, Python 3.9+.
99
87
 
88
+ For MCP integration (Claude Code / Cursor / Windsurf):
89
+
90
+ ```bash
91
+ pip install "diffcontext[mcp]"
92
+ ```
93
+
94
+ See [docs/MCP.md](docs/MCP.md) for the server config.
95
+
100
96
  From source for development:
101
97
 
102
98
  ```bash
@@ -114,6 +110,31 @@ diffcontext verify --from-history 20 --calibrate
114
110
 
115
111
  More commands: [USAGE.md](USAGE.md). Production recipes: [docs/USE_CASES.md](docs/USE_CASES.md).
116
112
 
113
+ ## Don't trust our benchmarks — run yours (2 minutes)
114
+
115
+ `diffcontext verify --from-history 20 --calibrate` mines test cases from
116
+ *your* repo's git history and grades retrieval against them — and prints
117
+ **NULL RESULT** rather than a decorative number when the tool doesn't fit
118
+ your repo. Finding that out *is* the feature.
119
+
120
+ ## Does it make the model better?
121
+
122
+ Yes — measured end to end, not by proxy. On 128 ContextBench Python tasks
123
+ judged by each repository's own test suite (no LLM-as-judge), **context
124
+ roughly quadruples pass@1: 5.5% → 25.8%**, exact McNemar p < 0.0001.
125
+
126
+ Two qualifiers, both in [`benchmarks/contextbench/RESULTS.md`](benchmarks/contextbench/RESULTS.md)
127
+ §6: **(a)** the seed functions given to every arm are **oracle** — extracted
128
+ from the gold patch — so this measures *"given correct localization, does
129
+ context quality matter?"*, not end-to-end issue solving (localization is
130
+ handed to every arm for free); **(b)** 121 of the 128 effective tasks are
131
+ django, so this is largely a django result.
132
+
133
+ The honest companion: the three context variants (default / gap / depboost)
134
+ are statistically **indistinguishable** from each other, p = 0.36–0.81. The
135
+ win is context versus no context — not this selector versus that one. Full
136
+ results: [`benchmarks/contextbench/RESULTS.md`](benchmarks/contextbench/RESULTS.md).
137
+
117
138
  ## What this is not
118
139
 
119
140
  - **Not a code generator.** It selects and packs context; the model writes
@@ -168,12 +189,6 @@ numbers did not survive:
168
189
  Full write-up: [docs/auditing-my-own-benchmark.md](docs/auditing-my-own-benchmark.md)
169
190
  · raw pass: [benchmarks/RIGOR_REPORT_2026-07.md](benchmarks/RIGOR_REPORT_2026-07.md).
170
191
 
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
192
  ## Use as a library
178
193
 
179
194
  ```python
@@ -209,6 +224,7 @@ dispatch are measured blind spots — itemized in
209
224
 
210
225
  - [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — pipeline, module map, agent API
211
226
  - [docs/BENCHMARKS.md](docs/BENCHMARKS.md) — all numbers, downstream pass@1, limitations
227
+ - [docs/MCP.md](docs/MCP.md) — MCP server for Claude Code / Cursor / Windsurf
212
228
  - [docs/ROADMAP.md](docs/ROADMAP.md) — prioritized plan with measured motivations
213
229
  - [diffcontext-service/](diffcontext-service/) — FastAPI service + web UI
214
230
  - [observability/](observability/) — retrieval pipeline tracing
@@ -18,6 +18,10 @@ It is built for people wiring LLMs into real codebases — agent loops, PR
18
18
  review bots, CI checks — anywhere you have to decide what goes in the prompt
19
19
  and the repository is far too large to send.
20
20
 
21
+ And it grades itself: point it at your repo and it mines your git history,
22
+ runs retrieval against real co-change pairs, and prints **NULL RESULT** when
23
+ it doesn't fit — finding that out *is* the feature.
24
+
21
25
  ## The problem
22
26
 
23
27
  Ask an assistant to change one function in a 50,000-line project and you have
@@ -37,24 +41,6 @@ git change ──► changed functions ──► hybrid retrieval ──► toke
37
41
  graph ∪ BM25 ∪ file top-k + tokens
38
42
  ```
39
43
 
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
44
  ## Install
59
45
 
60
46
  ```bash
@@ -63,6 +49,14 @@ pip install diffcontext
63
49
 
64
50
  Zero runtime dependencies, Python 3.9+.
65
51
 
52
+ For MCP integration (Claude Code / Cursor / Windsurf):
53
+
54
+ ```bash
55
+ pip install "diffcontext[mcp]"
56
+ ```
57
+
58
+ See [docs/MCP.md](docs/MCP.md) for the server config.
59
+
66
60
  From source for development:
67
61
 
68
62
  ```bash
@@ -80,6 +74,31 @@ diffcontext verify --from-history 20 --calibrate
80
74
 
81
75
  More commands: [USAGE.md](USAGE.md). Production recipes: [docs/USE_CASES.md](docs/USE_CASES.md).
82
76
 
77
+ ## Don't trust our benchmarks — run yours (2 minutes)
78
+
79
+ `diffcontext verify --from-history 20 --calibrate` mines test cases from
80
+ *your* repo's git history and grades retrieval against them — and prints
81
+ **NULL RESULT** rather than a decorative number when the tool doesn't fit
82
+ your repo. Finding that out *is* the feature.
83
+
84
+ ## Does it make the model better?
85
+
86
+ Yes — measured end to end, not by proxy. On 128 ContextBench Python tasks
87
+ judged by each repository's own test suite (no LLM-as-judge), **context
88
+ roughly quadruples pass@1: 5.5% → 25.8%**, exact McNemar p < 0.0001.
89
+
90
+ Two qualifiers, both in [`benchmarks/contextbench/RESULTS.md`](benchmarks/contextbench/RESULTS.md)
91
+ §6: **(a)** the seed functions given to every arm are **oracle** — extracted
92
+ from the gold patch — so this measures *"given correct localization, does
93
+ context quality matter?"*, not end-to-end issue solving (localization is
94
+ handed to every arm for free); **(b)** 121 of the 128 effective tasks are
95
+ django, so this is largely a django result.
96
+
97
+ The honest companion: the three context variants (default / gap / depboost)
98
+ are statistically **indistinguishable** from each other, p = 0.36–0.81. The
99
+ win is context versus no context — not this selector versus that one. Full
100
+ results: [`benchmarks/contextbench/RESULTS.md`](benchmarks/contextbench/RESULTS.md).
101
+
83
102
  ## What this is not
84
103
 
85
104
  - **Not a code generator.** It selects and packs context; the model writes
@@ -134,12 +153,6 @@ numbers did not survive:
134
153
  Full write-up: [docs/auditing-my-own-benchmark.md](docs/auditing-my-own-benchmark.md)
135
154
  · raw pass: [benchmarks/RIGOR_REPORT_2026-07.md](benchmarks/RIGOR_REPORT_2026-07.md).
136
155
 
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
156
  ## Use as a library
144
157
 
145
158
  ```python
@@ -175,6 +188,7 @@ dispatch are measured blind spots — itemized in
175
188
 
176
189
  - [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — pipeline, module map, agent API
177
190
  - [docs/BENCHMARKS.md](docs/BENCHMARKS.md) — all numbers, downstream pass@1, limitations
191
+ - [docs/MCP.md](docs/MCP.md) — MCP server for Claude Code / Cursor / Windsurf
178
192
  - [docs/ROADMAP.md](docs/ROADMAP.md) — prioritized plan with measured motivations
179
193
  - [diffcontext-service/](diffcontext-service/) — FastAPI service + web UI
180
194
  - [observability/](observability/) — retrieval pipeline tracing
@@ -32,7 +32,7 @@ Usage as a library:
32
32
  print(ctx.reduction_pct) # how much code was filtered out
33
33
  """
34
34
 
35
- __version__ = "0.5.1"
35
+ __version__ = "0.5.2"
36
36
 
37
37
  # Public, semver-covered API. Everything not listed here (graph_builder,
38
38
  # resolver, symbols, scanner, parser internals) is importable but carries no
@@ -113,6 +113,16 @@ def main():
113
113
  "reaches related files with no call or lexical connection)",
114
114
  )
115
115
  p_compile.add_argument("--notes", type=str, default=None, help="Developer notes to prepend to the context output")
116
+ p_compile.add_argument(
117
+ "--meta", choices=["full", "compact", "off"], default="full",
118
+ help=(
119
+ "Disclosure-header level. 'full' (default): counts, architecture "
120
+ "snapshot, dropped manifest, graph confidence, warnings. 'compact': "
121
+ "counts + dropped top-3 + warnings only (~60%% smaller — the A/B "
122
+ "test showed full meta costs ~10pp pass@1 at 4000 tokens by "
123
+ "displacing code). 'off': no meta-header, just code sections."
124
+ ),
125
+ )
116
126
  p_compile.add_argument("--json", action="store_true", help="Output as JSON")
117
127
 
118
128
  # --- blast (NEW: visual blast radius) ---
@@ -382,7 +392,7 @@ def _cmd_compile(args):
382
392
  top_k = args.top_k * len(changed) if args.top_k > 0 else None
383
393
  cutoff = args.cutoff if args.cutoff != "topk" else None
384
394
  ctx = compile(idx, impact, max_tokens=max_tokens, notes=args.notes,
385
- top_k=top_k, cutoff=cutoff)
395
+ top_k=top_k, cutoff=cutoff, meta=args.meta)
386
396
 
387
397
  if args.json:
388
398
  # Existing keys are kept for backwards compatibility. Added for
@@ -136,6 +136,7 @@ def compile_context(
136
136
  token_counter: Optional[Callable[[str], int]] = None,
137
137
  scoring_config: Optional[ScoringConfig] = None,
138
138
  max_tokens: Optional[int] = None,
139
+ meta: str = "full",
139
140
  ) -> ContextPackage:
140
141
  """
141
142
  Build the final context package from selected symbols.
@@ -164,6 +165,13 @@ def compile_context(
164
165
  unlimited). Used to keep the meta-header proportionate:
165
166
  under tight budgets the architecture snapshot is
166
167
  compacted so meta can't dwarf the code it annotates.
168
+ meta: Disclosure level: "full" (default — counts, architecture
169
+ snapshot, dropped manifest, graph confidence, warnings),
170
+ "compact" (counts + dropped top-3 only — ~60% smaller),
171
+ "off" (no meta-header at all, just code sections). The
172
+ A/B test showed full meta costs ~10pp pass@1 at 4000
173
+ tokens by displacing code; "compact" is the measured
174
+ middle ground.
167
175
  """
168
176
  dropped_ids = dropped_ids or []
169
177
  skipped_files = skipped_files or []
@@ -230,23 +238,27 @@ def compile_context(
230
238
  code_text = "\n\n".join(parts)
231
239
  context_tokens = count(code_text)
232
240
 
233
- meta = _build_meta_header(
234
- symbols = symbols,
235
- selected_ids = sel_ids,
236
- dropped_ids = drop_ids,
237
- skipped_files = skipped_files,
238
- changed_ids = changed_ids,
239
- graph = graph,
240
- reverse = reverse,
241
- graph_confidence = graph_confidence,
242
- token_budget = total_repo_tokens, # not the budget cap; just total repo
243
- context_tokens = context_tokens,
244
- scores = scores,
245
- notes = notes,
246
- scoring_config = scoring_config,
247
- max_tokens = max_tokens,
248
- count = count,
249
- )
241
+ if meta == "off":
242
+ meta_text = ""
243
+ else:
244
+ meta_text = _build_meta_header(
245
+ symbols = symbols,
246
+ selected_ids = sel_ids,
247
+ dropped_ids = drop_ids,
248
+ skipped_files = skipped_files,
249
+ changed_ids = changed_ids,
250
+ graph = graph,
251
+ reverse = reverse,
252
+ graph_confidence = graph_confidence,
253
+ token_budget = total_repo_tokens, # not the budget cap; just total repo
254
+ context_tokens = context_tokens,
255
+ scores = scores,
256
+ notes = notes,
257
+ scoring_config = scoring_config,
258
+ max_tokens = max_tokens,
259
+ count = count,
260
+ meta_level = meta,
261
+ )
250
262
 
251
263
  suggestions = _build_suggestions(
252
264
  changed_ids = changed_ids,
@@ -258,7 +270,7 @@ def compile_context(
258
270
  scores = scores,
259
271
  )
260
272
 
261
- full_text = meta + "\n\n" + code_text
273
+ full_text = meta_text + "\n\n" + code_text if meta_text else code_text
262
274
  if suggestions:
263
275
  full_text += "\n\n" + suggestions
264
276
  return items, full_text
@@ -357,6 +369,7 @@ def _build_meta_header(
357
369
  scoring_config: Optional[ScoringConfig] = None,
358
370
  max_tokens: Optional[int] = None,
359
371
  count: Optional[Callable[[str], int]] = None,
372
+ meta_level: str = "full",
360
373
  ) -> str:
361
374
  count = count or (lambda text: max(1, len(text) // 4))
362
375
  total_syms = len(symbols)
@@ -374,108 +387,120 @@ def _build_meta_header(
374
387
  for s in changed_ids
375
388
  ) if graph else 0
376
389
 
390
+ compact = meta_level == "compact"
391
+
377
392
  lines = [
378
393
  "=== DIFFCONTEXT META ===",
379
394
  f"Repo symbols total : {total_syms}",
380
- f"Symbols scored : {scored_cnt}",
381
395
  f"Symbols IN context : {selected_cnt}",
382
396
  f"Symbols DROPPED : {dropped_cnt} ← you cannot see these",
383
- f"Graph edges total : {total_edges}",
384
- f"Graph confidence : {graph_confidence * 100:.0f}%"
385
- + (" ✓" if graph_confidence >= 0.9 else " ⚠ incomplete"),
386
- # Always-present disclosure, same category as the DROPPED manifest:
387
- # benchmarked cross-subsystem conceptual co-changes score 0% recall
388
- # for every static method (see EVAL_V2_REPORT.md failure taxonomy),
389
- # so a confident-looking 100% must not read as "nothing was missed".
390
- "Note: graph confidence = STRUCTURAL completeness only. Static "
391
- "analysis cannot see cross-subsystem conceptual coupling (e.g. a "
392
- "settings flag and the unrelated code that reads it) — such "
393
- "related code may exist and not be listed anywhere above.",
397
+ ]
398
+ if not compact:
399
+ lines.extend([
400
+ f"Symbols scored : {scored_cnt}",
401
+ f"Graph edges total : {total_edges}",
402
+ f"Graph confidence : {graph_confidence * 100:.0f}%"
403
+ + (" ✓" if graph_confidence >= 0.9 else " incomplete"),
404
+ # Always-present disclosure, same category as the DROPPED manifest:
405
+ # benchmarked cross-subsystem conceptual co-changes score 0% recall
406
+ # for every static method (see EVAL_V2_REPORT.md failure taxonomy),
407
+ # so a confident-looking 100% must not read as "nothing was missed".
408
+ "Note: graph confidence = STRUCTURAL completeness only. Static "
409
+ "analysis cannot see cross-subsystem conceptual coupling (e.g. a "
410
+ "settings flag and the unrelated code that reads it) — such "
411
+ "related code may exist and not be listed anywhere above.",
412
+ ])
413
+ lines.extend([
394
414
  f"Changed symbols : {len(changed_ids)}",
395
415
  f"Direct callers found : {direct_callers}",
396
416
  f"Direct callees found : {direct_callees}",
397
417
  f"Context tokens (code) : {context_tokens:,}",
398
418
  "Output tokens (full) : {FULL_OUTPUT_TOKENS}",
399
419
  f"Scoring basis : {describe_scoring_basis(scoring_config)}",
400
- ]
420
+ ])
401
421
 
402
422
  # --- Repository Architecture Snapshot ---
403
- # Build rel_file -> absolute_path mapping from symbol table.
404
- # sym_id gives us relative path; sym.file gives us the absolute path we
405
- # need to actually open the file for its docstring.
406
- rel_to_abs: Dict[str, str] = {}
407
- for sym_id, sym in symbols.items():
408
- rel_file = sym_id.split(":", 1)[0]
409
- if rel_file not in rel_to_abs:
410
- rel_to_abs[rel_file] = sym.file # sym.file is always absolute
411
-
412
- modules_total = {}
413
- modules_selected = {}
414
- for sym_id in symbols:
415
- file_name = sym_id.split(":", 1)[0]
416
- modules_total[file_name] = modules_total.get(file_name, 0) + 1
417
-
418
- for sym_id in selected_ids:
419
- if sym_id in symbols:
423
+ # Skipped entirely in compact mode it's the largest meta component
424
+ # (per-module listing + docstrings), and the A/B test showed meta
425
+ # displaces code under tight budgets. Compact keeps counts + dropped
426
+ # top-3 only.
427
+ if not compact:
428
+ # Build rel_file -> absolute_path mapping from symbol table.
429
+ # sym_id gives us relative path; sym.file gives us the absolute path we
430
+ # need to actually open the file for its docstring.
431
+ rel_to_abs: Dict[str, str] = {}
432
+ for sym_id, sym in symbols.items():
433
+ rel_file = sym_id.split(":", 1)[0]
434
+ if rel_file not in rel_to_abs:
435
+ rel_to_abs[rel_file] = sym.file # sym.file is always absolute
436
+
437
+ modules_total = {}
438
+ modules_selected = {}
439
+ for sym_id in symbols:
420
440
  file_name = sym_id.split(":", 1)[0]
421
- modules_selected[file_name] = modules_selected.get(file_name, 0) + 1
441
+ modules_total[file_name] = modules_total.get(file_name, 0) + 1
422
442
 
423
- lines.append("")
424
- lines.append("=== REPOSITORY ARCHITECTURE SNAPSHOT ===")
443
+ for sym_id in selected_ids:
444
+ if sym_id in symbols:
445
+ file_name = sym_id.split(":", 1)[0]
446
+ modules_selected[file_name] = modules_selected.get(file_name, 0) + 1
425
447
 
426
- loaded_files = []
427
- blind_files = []
448
+ lines.append("")
449
+ lines.append("=== REPOSITORY ARCHITECTURE SNAPSHOT ===")
428
450
 
429
- for file_name, total in sorted(modules_total.items()):
430
- selected = modules_selected.get(file_name, 0)
451
+ loaded_files = []
452
+ blind_files = []
431
453
 
432
- doc_snippet = ""
433
- if file_name.endswith(".py"):
434
- # FIX: use absolute path, not the relative file_name
435
- abs_path = rel_to_abs.get(file_name, "")
436
- doc_str = _get_module_docstring(abs_path) if abs_path else ""
437
- if doc_str:
438
- doc_snippet = f" — {doc_str}"
454
+ for file_name, total in sorted(modules_total.items()):
455
+ selected = modules_selected.get(file_name, 0)
439
456
 
440
- if selected > 0:
441
- loaded_files.append(f" - {file_name} ({selected}/{total} symbols loaded){doc_snippet}")
442
- else:
443
- blind_files.append(f" - {file_name} ({total} symbols){doc_snippet}")
444
-
445
- # Budget proportionality: the snapshot scales with repo size, not with
446
- # the requested budget. Under a tight budget an uncapped snapshot can
447
- # cost multiples of the code it annotates (measured: --max-tokens 500 on
448
- # black produced ~2,600 total tokens, 5x the request). Compact it when
449
- # it would exceed ~25% of the symbol budget.
450
- snapshot_cost = count("\n".join(loaded_files + blind_files))
451
- snapshot_budget = max(max_tokens // 4, 150) if max_tokens else None
452
- if snapshot_budget is not None and snapshot_cost > snapshot_budget:
453
- n_loaded = len(loaded_files)
454
- n_blind = len(blind_files)
455
- lines.append(
456
- f"MODULES: {len(modules_total)} files — {n_loaded} in context, "
457
- f"{n_blind} blind spots"
458
- )
459
- lines.append(
460
- " (per-module snapshot omitted under tight budget — raise "
461
- "--max-tokens to see it)"
462
- )
463
- else:
464
- lines.append("MODULES IN CONTEXT:")
465
- if loaded_files:
466
- lines.extend(loaded_files)
467
- else:
468
- lines.append(" (none)")
457
+ doc_snippet = ""
458
+ if file_name.endswith(".py"):
459
+ # FIX: use absolute path, not the relative file_name
460
+ abs_path = rel_to_abs.get(file_name, "")
461
+ doc_str = _get_module_docstring(abs_path) if abs_path else ""
462
+ if doc_str:
463
+ doc_snippet = f" {doc_str}"
469
464
 
470
- lines.append("")
471
- lines.append("KNOWN MODULES (NOT IN CONTEXT - BLIND SPOTS):")
472
- if blind_files:
473
- _BLIND_CAP = 25
474
- lines.extend(blind_files[:_BLIND_CAP])
475
- if len(blind_files) > _BLIND_CAP:
476
- lines.append(f" ... and {len(blind_files) - _BLIND_CAP} more modules")
465
+ if selected > 0:
466
+ loaded_files.append(f" - {file_name} ({selected}/{total} symbols loaded){doc_snippet}")
467
+ else:
468
+ blind_files.append(f" - {file_name} ({total} symbols){doc_snippet}")
469
+
470
+ # Budget proportionality: the snapshot scales with repo size, not with
471
+ # the requested budget. Under a tight budget an uncapped snapshot can
472
+ # cost multiples of the code it annotates (measured: --max-tokens 500 on
473
+ # black produced ~2,600 total tokens, 5x the request). Compact it when
474
+ # it would exceed ~25% of the symbol budget.
475
+ snapshot_cost = count("\n".join(loaded_files + blind_files))
476
+ snapshot_budget = max(max_tokens // 4, 150) if max_tokens else None
477
+ if snapshot_budget is not None and snapshot_cost > snapshot_budget:
478
+ n_loaded = len(loaded_files)
479
+ n_blind = len(blind_files)
480
+ lines.append(
481
+ f"MODULES: {len(modules_total)} files — {n_loaded} in context, "
482
+ f"{n_blind} blind spots"
483
+ )
484
+ lines.append(
485
+ " (per-module snapshot omitted under tight budget — raise "
486
+ "--max-tokens to see it)"
487
+ )
477
488
  else:
478
- lines.append(" (none)")
489
+ lines.append("MODULES IN CONTEXT:")
490
+ if loaded_files:
491
+ lines.extend(loaded_files)
492
+ else:
493
+ lines.append(" (none)")
494
+
495
+ lines.append("")
496
+ lines.append("KNOWN MODULES (NOT IN CONTEXT - BLIND SPOTS):")
497
+ if blind_files:
498
+ _BLIND_CAP = 25
499
+ lines.extend(blind_files[:_BLIND_CAP])
500
+ if len(blind_files) > _BLIND_CAP:
501
+ lines.append(f" ... and {len(blind_files) - _BLIND_CAP} more modules")
502
+ else:
503
+ lines.append(" (none)")
479
504
 
480
505
  if skipped_files:
481
506
  lines.append("")
@@ -484,17 +509,26 @@ def _build_meta_header(
484
509
  lines.append(f" ✗ {f}")
485
510
 
486
511
  if dropped_cnt > 0:
487
- # Under a tight budget, the top-15 manifest itself costs more than
488
- # some requested budgets show the top 5 and keep the count honest.
489
- drop_cap = 5 if (max_tokens and max_tokens < 2000) else 15
512
+ # Compact mode: top-3 only. Full mode: top 15 (top 5 under <2000
513
+ # budget) — the dropped manifest itself costs more than some budgets.
514
+ if compact:
515
+ drop_cap = 3
516
+ elif max_tokens and max_tokens < 2000:
517
+ drop_cap = 5
518
+ else:
519
+ drop_cap = 15
490
520
  lines.append("")
491
521
  lines.append(f"DROPPED SYMBOLS ({dropped_cnt}) — scored but cut by token budget:")
492
522
  for d in dropped_ids[:drop_cap]:
493
523
  lines.append(f" - {d} (score: {scores.get(d, 0):.0f})")
494
524
  if dropped_cnt > drop_cap:
495
525
  lines.append(f" ... and {dropped_cnt - drop_cap} more")
496
- lines.append(" → If any of these are critical, re-run with a higher --max-tokens.")
526
+ if not compact:
527
+ lines.append(" → If any of these are critical, re-run with a higher --max-tokens.")
497
528
 
529
+ # Warnings are kept in compact mode — they flag real graph holes and
530
+ # dropped references the model needs to know about. Only the
531
+ # architecture snapshot and the verbose counts are compacted away.
498
532
  warnings = []
499
533
  if skipped_files:
500
534
  warnings.append(
@@ -506,7 +540,7 @@ def _build_meta_header(
506
540
  f"⚠ {dropped_cnt} symbol(s) were dropped. "
507
541
  "References to them in the code below are NOT backed by visible implementations."
508
542
  )
509
- if graph_confidence < 0.8:
543
+ if graph_confidence < 0.8 and not compact:
510
544
  warnings.append(
511
545
  f"⚠ Graph confidence is {graph_confidence * 100:.0f}%. "
512
546
  "Many calls could not be resolved — likely external/stdlib deps or dynamic dispatch."