diffcontext 0.5.2__tar.gz → 0.5.4__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.2 → diffcontext-0.5.4}/PKG-INFO +1 -1
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/__init__.py +1 -1
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/cli/__init__.py +18 -3
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/context/compiler.py +5 -8
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/mcp_server.py +35 -9
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/pipeline.py +42 -3
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext.egg-info/PKG-INFO +1 -1
- {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_compiler_meta.py +1 -2
- {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_core.py +80 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/LICENSE +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/README.md +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/_warn_once.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/cache.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/context/__init__.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/context/selector.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/diff/__init__.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/diff/git_diff.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/diff/state_manager.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/graph_builder.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/history.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/impact/__init__.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/impact/blast_radius.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/impact/scoring.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/impact/traversal.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/impact/visualizer.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/languages/__init__.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/languages/typescript.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/lexical.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/models.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/parser.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/py.typed +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/rerank/__init__.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/rerank/features.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/rerank/model.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/resolver.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/scanner.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/symbols.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/verify/__init__.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/verify/cases.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/verify/history.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/verify/sufficiency.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext.egg-info/SOURCES.txt +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext.egg-info/dependency_links.txt +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext.egg-info/entry_points.txt +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext.egg-info/requires.txt +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext.egg-info/top_level.txt +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/pyproject.toml +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/setup.cfg +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_cache.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_cutoff.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_determinism.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_downstream_eval.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_git_diff.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_harness_api.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_history.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_incremental.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_index_scoping.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_lexical.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_mcp_server.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_rerank.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_semantic_ablation.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_semantic_audit.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_semantic_gap.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_semantic_metrics.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_semantic_mine.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_service_clone.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_src_layout.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_stats.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_token_budget.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_typescript_adapter.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_verify.py +0 -0
- {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_version.py +0 -0
|
@@ -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.
|
|
35
|
+
__version__ = "0.5.4"
|
|
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
|
|
@@ -23,6 +23,7 @@ import time
|
|
|
23
23
|
from ..pipeline import index_repository, analyze_impact, compile, warn_unknown_symbols
|
|
24
24
|
from ..diff.git_diff import find_changed_symbols
|
|
25
25
|
from ..impact.visualizer import render_blast_radius, render_verification
|
|
26
|
+
from .. import __version__ as DC_VERSION
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
def main():
|
|
@@ -37,6 +38,7 @@ def main():
|
|
|
37
38
|
prog="diffcontext",
|
|
38
39
|
description="Static-analysis-powered repository context compiler for LLMs",
|
|
39
40
|
)
|
|
41
|
+
parser.add_argument("--version", action="version", version=f"diffcontext {DC_VERSION}")
|
|
40
42
|
sub = parser.add_subparsers(dest="command", help="Available commands")
|
|
41
43
|
|
|
42
44
|
# --- index ---
|
|
@@ -113,14 +115,20 @@ def main():
|
|
|
113
115
|
"reaches related files with no call or lexical connection)",
|
|
114
116
|
)
|
|
115
117
|
p_compile.add_argument("--notes", type=str, default=None, help="Developer notes to prepend to the context output")
|
|
118
|
+
p_compile.add_argument(
|
|
119
|
+
"--query-text", type=str, default=None,
|
|
120
|
+
help="Bug report or issue text. Biases retrieval toward symbols "
|
|
121
|
+
"semantically related to the described problem — the one "
|
|
122
|
+
"signal the graph alone can't provide.",
|
|
123
|
+
)
|
|
116
124
|
p_compile.add_argument(
|
|
117
125
|
"--meta", choices=["full", "compact", "off"], default="full",
|
|
118
126
|
help=(
|
|
119
127
|
"Disclosure-header level. 'full' (default): counts, architecture "
|
|
120
128
|
"snapshot, dropped manifest, graph confidence, warnings. 'compact': "
|
|
121
|
-
"counts + dropped top-3 + warnings only (~60%% smaller
|
|
122
|
-
"
|
|
123
|
-
"
|
|
129
|
+
"counts + dropped top-3 + warnings only (~60%% smaller). 'off': no "
|
|
130
|
+
"meta-header, just code sections. The pass@1 effect of meta level "
|
|
131
|
+
"is UNMEASURED."
|
|
124
132
|
),
|
|
125
133
|
)
|
|
126
134
|
p_compile.add_argument("--json", action="store_true", help="Output as JSON")
|
|
@@ -384,9 +392,16 @@ def _cmd_compile(args):
|
|
|
384
392
|
from ..history import CoChangeIndex
|
|
385
393
|
history = CoChangeIndex(args.repo)
|
|
386
394
|
|
|
395
|
+
# Untuned default; not derived from any sweep. The query_weight controls
|
|
396
|
+
# how much the problem-description signal moves candidates relative to
|
|
397
|
+
# the graph + BM25 + same-file blend. 0.3 is a guess that "matters but
|
|
398
|
+
# doesn't dominate" — it has NOT been benchmarked or optimized. Do not
|
|
399
|
+
# cite this as a measured value; tune it with evidence when data exists.
|
|
400
|
+
query_weight = 0.3 if args.query_text else 0.0
|
|
387
401
|
impact = analyze_impact(
|
|
388
402
|
idx, changed, max_depth=args.depth, hybrid=not args.graph_only,
|
|
389
403
|
history=history,
|
|
404
|
+
query_text=args.query_text, query_weight=query_weight,
|
|
390
405
|
)
|
|
391
406
|
max_tokens = args.max_tokens if args.max_tokens > 0 else None
|
|
392
407
|
top_k = args.top_k * len(changed) if args.top_k > 0 else None
|
|
@@ -167,11 +167,9 @@ def compile_context(
|
|
|
167
167
|
compacted so meta can't dwarf the code it annotates.
|
|
168
168
|
meta: Disclosure level: "full" (default — counts, architecture
|
|
169
169
|
snapshot, dropped manifest, graph confidence, warnings),
|
|
170
|
-
"compact" (counts + dropped top-3 only — ~60% smaller
|
|
171
|
-
"off" (no meta-header
|
|
172
|
-
|
|
173
|
-
tokens by displacing code; "compact" is the measured
|
|
174
|
-
middle ground.
|
|
170
|
+
"compact" (counts + dropped top-3 only — ~60% smaller,
|
|
171
|
+
keeps warnings), "off" (no meta-header, just code). The
|
|
172
|
+
pass@1 effect of meta level is UNMEASURED.
|
|
175
173
|
"""
|
|
176
174
|
dropped_ids = dropped_ids or []
|
|
177
175
|
skipped_files = skipped_files or []
|
|
@@ -421,9 +419,8 @@ def _build_meta_header(
|
|
|
421
419
|
|
|
422
420
|
# --- Repository Architecture Snapshot ---
|
|
423
421
|
# Skipped entirely in compact mode — it's the largest meta component
|
|
424
|
-
# (per-module listing + docstrings)
|
|
425
|
-
#
|
|
426
|
-
# top-3 only.
|
|
422
|
+
# (per-module listing + docstrings). Compact keeps counts + dropped
|
|
423
|
+
# top-3 only. The pass@1 effect of meta level is UNMEASURED.
|
|
427
424
|
if not compact:
|
|
428
425
|
# Build rel_file -> absolute_path mapping from symbol table.
|
|
429
426
|
# sym_id gives us relative path; sym.file gives us the absolute path we
|
|
@@ -68,6 +68,8 @@ def main():
|
|
|
68
68
|
args = parser.parse_args()
|
|
69
69
|
default_repo = os.path.abspath(args.repo)
|
|
70
70
|
|
|
71
|
+
from diffcontext import __version__ as dc_version
|
|
72
|
+
|
|
71
73
|
server = MCPServer(
|
|
72
74
|
name="diffcontext",
|
|
73
75
|
description=(
|
|
@@ -75,7 +77,7 @@ def main():
|
|
|
75
77
|
"the code that matters for a change and packs it into a token "
|
|
76
78
|
"budget. Measures whether it works on your repo."
|
|
77
79
|
),
|
|
78
|
-
version=
|
|
80
|
+
version=dc_version,
|
|
79
81
|
)
|
|
80
82
|
|
|
81
83
|
@server.tool()
|
|
@@ -83,8 +85,9 @@ def main():
|
|
|
83
85
|
repo_path: str = "",
|
|
84
86
|
changed_symbols: Optional[List[str]] = None,
|
|
85
87
|
git_ref: Optional[str] = None,
|
|
88
|
+
task_description: Optional[str] = None,
|
|
86
89
|
max_tokens: int = 8000,
|
|
87
|
-
meta: str = "
|
|
90
|
+
meta: str = "full",
|
|
88
91
|
) -> str:
|
|
89
92
|
"""Compile LLM-ready context for a change.
|
|
90
93
|
|
|
@@ -93,6 +96,10 @@ def main():
|
|
|
93
96
|
related functions the model needs to make the change safely — packed
|
|
94
97
|
into max_tokens with a disclosure header showing what was dropped.
|
|
95
98
|
|
|
99
|
+
Optionally pass task_description (the bug report or issue text) to
|
|
100
|
+
bias retrieval toward symbols relevant to the described problem —
|
|
101
|
+
the one signal the graph alone can't provide.
|
|
102
|
+
|
|
96
103
|
Args:
|
|
97
104
|
repo_path: Absolute path to the repository. If omitted, uses
|
|
98
105
|
the --repo from server startup.
|
|
@@ -100,19 +107,38 @@ def main():
|
|
|
100
107
|
["./src/auth.py:validate_jwt"]). Mutually exclusive with
|
|
101
108
|
git_ref.
|
|
102
109
|
git_ref: Git ref to detect changes from (e.g. "HEAD~1").
|
|
103
|
-
Mutually exclusive with changed_symbols.
|
|
110
|
+
Mutually exclusive with changed_symbols. When only
|
|
111
|
+
task_description is given (no changed_symbols or git_ref),
|
|
112
|
+
defaults to "HEAD".
|
|
113
|
+
task_description: The bug report or issue text. Biases retrieval
|
|
114
|
+
toward symbols semantically related to the described problem,
|
|
115
|
+
not just structurally near the changed symbols.
|
|
104
116
|
max_tokens: Token budget for the context (default 8000).
|
|
105
|
-
meta: Disclosure header level: "full", "compact"
|
|
106
|
-
|
|
117
|
+
meta: Disclosure header level: "full" (default), "compact", or "off".
|
|
118
|
+
The pass@1 effect of meta level is UNMEASURED.
|
|
107
119
|
"""
|
|
108
120
|
from diffcontext.pipeline import analyze_impact, compile
|
|
109
121
|
|
|
110
122
|
repo = repo_path or default_repo
|
|
111
123
|
idx = _get_index(repo)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
124
|
+
|
|
125
|
+
# When only task_description is given, auto-detect changes from HEAD.
|
|
126
|
+
effective_ref = git_ref
|
|
127
|
+
if not changed_symbols and not git_ref and task_description:
|
|
128
|
+
effective_ref = "HEAD"
|
|
129
|
+
|
|
130
|
+
changed = _resolve_changed(idx, changed_symbols, effective_ref, repo)
|
|
131
|
+
if not changed and not task_description:
|
|
132
|
+
return "No changed symbols found. Pass changed_symbols, git_ref, or task_description."
|
|
133
|
+
|
|
134
|
+
# Untuned default; not derived from any sweep. The query_weight controls
|
|
135
|
+
# how much the problem-description signal moves candidates relative to
|
|
136
|
+
# the graph + BM25 + same-file blend. 0.3 is a guess that "matters but
|
|
137
|
+
# doesn't dominate" — it has NOT been benchmarked or optimized.
|
|
138
|
+
query_weight = 0.3 if task_description else 0.0
|
|
139
|
+
impact = analyze_impact(
|
|
140
|
+
idx, changed, query_text=task_description, query_weight=query_weight,
|
|
141
|
+
)
|
|
116
142
|
ctx = compile(idx, impact, max_tokens=max_tokens, meta=meta)
|
|
117
143
|
return ctx.text
|
|
118
144
|
|
|
@@ -585,6 +585,8 @@ def _blend_hybrid(
|
|
|
585
585
|
adaptive: bool = True,
|
|
586
586
|
history_scores: Optional[Dict[str, float]] = None,
|
|
587
587
|
history_weight: float = 0.15,
|
|
588
|
+
query_text: Optional[str] = None,
|
|
589
|
+
query_weight: float = 0.0,
|
|
588
590
|
) -> Dict[str, float]:
|
|
589
591
|
"""
|
|
590
592
|
Blend graph impact scores with BM25 and same-file signals.
|
|
@@ -605,6 +607,16 @@ def _blend_hybrid(
|
|
|
605
607
|
file that historically co-changed with the changed files gets
|
|
606
608
|
`history_weight * association` added — the only signal that can reach
|
|
607
609
|
co-change partners with no structural or lexical connection at all.
|
|
610
|
+
|
|
611
|
+
`query_text` (the bug report / issue text / task description) is an
|
|
612
|
+
optional fifth signal: BM25 scores the query text against every
|
|
613
|
+
symbol's source code, and `query_weight * normalized_score` is added
|
|
614
|
+
to each candidate. This is the one signal that ranks by relevance to
|
|
615
|
+
the *described problem* rather than structural nearness to the changed
|
|
616
|
+
symbols — the "thematic siblings" blind spot the graph alone cannot
|
|
617
|
+
address. Default query_weight=0.0 = no effect (backwards compatible).
|
|
618
|
+
When query_weight > 0, symbols matching the problem description get a
|
|
619
|
+
boost even with no call-graph or same-file connection.
|
|
608
620
|
"""
|
|
609
621
|
from .lexical import get_lexical_index
|
|
610
622
|
|
|
@@ -630,6 +642,18 @@ def _blend_hybrid(
|
|
|
630
642
|
lex_raw[sid] = sc
|
|
631
643
|
lex_norm = _normalize_scores(lex_raw)
|
|
632
644
|
|
|
645
|
+
# Query-text BM25: rank symbols by relevance to the described problem.
|
|
646
|
+
# This is the one signal that doesn't derive from the changed symbols
|
|
647
|
+
# at all — it reaches symbols semantically related to the bug report
|
|
648
|
+
# even with no structural or lexical connection to the changed code.
|
|
649
|
+
query_norm: Dict[str, float] = {}
|
|
650
|
+
if query_text and query_weight > 0:
|
|
651
|
+
query_raw: Dict[str, float] = {}
|
|
652
|
+
for sid, sc in lexical_index.scores_for(query_text).items():
|
|
653
|
+
if sid not in changed_set and sc > query_raw.get(sid, 0.0):
|
|
654
|
+
query_raw[sid] = sc
|
|
655
|
+
query_norm = _normalize_scores(query_raw)
|
|
656
|
+
|
|
633
657
|
changed_files = {s.split(":")[0] for s in changed_in_index}
|
|
634
658
|
|
|
635
659
|
history_files = {
|
|
@@ -647,6 +671,8 @@ def _blend_hybrid(
|
|
|
647
671
|
sid for sid in index.symbols
|
|
648
672
|
if sid.split(":")[0] in history_files and sid not in changed_set
|
|
649
673
|
)
|
|
674
|
+
if query_norm:
|
|
675
|
+
candidates.update(query_norm.keys())
|
|
650
676
|
for sid in candidates:
|
|
651
677
|
score = w_graph * graph_norm.get(sid, 0.0) + w_lex * lex_norm.get(sid, 0.0)
|
|
652
678
|
sid_file = sid.split(":")[0]
|
|
@@ -654,6 +680,8 @@ def _blend_hybrid(
|
|
|
654
680
|
score += w_file
|
|
655
681
|
if history_scores:
|
|
656
682
|
score += history_weight * history_scores.get(sid_file, 0.0)
|
|
683
|
+
if query_norm:
|
|
684
|
+
score += query_weight * query_norm.get(sid, 0.0)
|
|
657
685
|
blended[sid] = 100.0 * score
|
|
658
686
|
|
|
659
687
|
# Changed symbols keep their unblended score so they stay ranked on top.
|
|
@@ -712,6 +740,8 @@ def analyze_impact(
|
|
|
712
740
|
hybrid: bool = True,
|
|
713
741
|
adaptive: bool = True,
|
|
714
742
|
history: Optional[object] = None,
|
|
743
|
+
query_text: Optional[str] = None,
|
|
744
|
+
query_weight: float = 0.0,
|
|
715
745
|
) -> ImpactResult:
|
|
716
746
|
"""
|
|
717
747
|
Phase 2: Given changed symbols, compute blast radius and impact scores.
|
|
@@ -729,6 +759,16 @@ def analyze_impact(
|
|
|
729
759
|
co-change association is blended as a fourth signal — the only
|
|
730
760
|
signal that can reach co-change partners with no structural or
|
|
731
761
|
lexical connection (the measured cross-subsystem ceiling).
|
|
762
|
+
query_text: the bug report / issue text / task description. When given
|
|
763
|
+
with query_weight > 0, BM25 scores the query text against every
|
|
764
|
+
symbol's source code and adds query_weight * normalized_score to
|
|
765
|
+
each candidate. This is the one signal that ranks by relevance to
|
|
766
|
+
the *described problem* rather than structural nearness — the
|
|
767
|
+
"thematic siblings" blind spot. Default query_weight=0.0 = no
|
|
768
|
+
effect (backwards compatible).
|
|
769
|
+
query_weight: weight for the query_text signal (default 0.0 = off).
|
|
770
|
+
When > 0, symbols matching the problem description get a boost
|
|
771
|
+
even with no call-graph or same-file connection.
|
|
732
772
|
|
|
733
773
|
Fix: expanded_deps is now passed into compute_impact_scores so those
|
|
734
774
|
nodes are actually scored. Previously they were computed and discarded.
|
|
@@ -774,6 +814,7 @@ def analyze_impact(
|
|
|
774
814
|
scores = _blend_hybrid(
|
|
775
815
|
index, changed_symbols, scores,
|
|
776
816
|
adaptive=adaptive, history_scores=history_scores,
|
|
817
|
+
query_text=query_text, query_weight=query_weight,
|
|
777
818
|
)
|
|
778
819
|
|
|
779
820
|
return ImpactResult(
|
|
@@ -836,9 +877,7 @@ def compile(
|
|
|
836
877
|
manifest, graph confidence, warnings), "compact"
|
|
837
878
|
(counts + dropped top-3 only — ~60% smaller, keeps
|
|
838
879
|
warnings), "off" (no meta-header, just code). The
|
|
839
|
-
|
|
840
|
-
tokens by displacing code; compact is the measured
|
|
841
|
-
middle ground.
|
|
880
|
+
pass@1 effect of meta level is UNMEASURED.
|
|
842
881
|
"""
|
|
843
882
|
# Apply dependency-type boost BEFORE selection (the key experiment).
|
|
844
883
|
# Boosts direct callees, callers, and siblings of changed symbols so
|
|
@@ -210,8 +210,7 @@ class TestDocstringCache:
|
|
|
210
210
|
|
|
211
211
|
class TestMetaLevels:
|
|
212
212
|
"""The --meta full|compact|off flag controls the disclosure header's
|
|
213
|
-
budget cost. The
|
|
214
|
-
tokens by displacing code; compact is the measured middle ground."""
|
|
213
|
+
budget cost. The pass@1 effect of meta level is UNMEASURED."""
|
|
215
214
|
|
|
216
215
|
def _make_pkg(self, meta=None):
|
|
217
216
|
symbols = _make_symbols()
|
|
@@ -305,5 +305,85 @@ class TestPipeline:
|
|
|
305
305
|
assert ctx.token_estimate > 0
|
|
306
306
|
|
|
307
307
|
|
|
308
|
+
class TestQueryText:
|
|
309
|
+
"""The query_text signal: BM25 scores the bug report / issue text
|
|
310
|
+
against every symbol's source code, adding a relevance-to-the-problem
|
|
311
|
+
signal the graph alone can't provide.
|
|
312
|
+
|
|
313
|
+
Two requirements the user asked for:
|
|
314
|
+
1. query_text=None reproduces current ranking byte-identically
|
|
315
|
+
(regression guard — no behavior change when the feature is off).
|
|
316
|
+
2. A non-None value measurably reorders results (the signal actually
|
|
317
|
+
changes the ranking, not just the scores).
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
def test_none_reproduces_baseline_byte_identically(self):
|
|
321
|
+
"""The regression guard: query_text=None must not change ANY score,
|
|
322
|
+
not even by a floating-point epsilon. This is what keeps the feature
|
|
323
|
+
truly opt-in — existing users see zero behavior change."""
|
|
324
|
+
_require_fixture(MEDIUM, "medium_repo")
|
|
325
|
+
idx = index_repository(MEDIUM)
|
|
326
|
+
seed = "./service.py:onboard_user"
|
|
327
|
+
|
|
328
|
+
baseline = analyze_impact(idx, [seed])
|
|
329
|
+
with_query_kwarg = analyze_impact(
|
|
330
|
+
idx, [seed], query_text=None, query_weight=0.0,
|
|
331
|
+
)
|
|
332
|
+
# Every score must be identical — not just the set of candidates,
|
|
333
|
+
# but the actual float values. query_text=None is a true no-op.
|
|
334
|
+
assert baseline.scores == with_query_kwarg.scores, (
|
|
335
|
+
"query_text=None must reproduce baseline scores exactly; "
|
|
336
|
+
"the feature must be a true no-op when disabled."
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
def test_query_text_reorders_results(self):
|
|
340
|
+
"""A non-None query_text must measurably change the ranking — not
|
|
341
|
+
just add candidates, but move at least one existing symbol to a
|
|
342
|
+
different rank position. This proves the signal is doing work."""
|
|
343
|
+
_require_fixture(MEDIUM, "medium_repo")
|
|
344
|
+
idx = index_repository(MEDIUM)
|
|
345
|
+
seed = "./service.py:onboard_user"
|
|
346
|
+
|
|
347
|
+
baseline = analyze_impact(idx, [seed])
|
|
348
|
+
base_ranked = sorted(baseline.scores.items(), key=lambda x: -x[1])
|
|
349
|
+
base_order = [sid for sid, _ in base_ranked]
|
|
350
|
+
|
|
351
|
+
# "email validation check" should boost is_valid_email (whose code
|
|
352
|
+
# contains "email" and "valid") above User.__init__ (which doesn't).
|
|
353
|
+
impacted = analyze_impact(
|
|
354
|
+
idx, [seed], query_text="email validation check", query_weight=0.3,
|
|
355
|
+
)
|
|
356
|
+
q_ranked = sorted(impacted.scores.items(), key=lambda x: -x[1])
|
|
357
|
+
q_order = [sid for sid, _ in q_ranked]
|
|
358
|
+
|
|
359
|
+
assert base_order != q_order, (
|
|
360
|
+
"query_text must reorder at least one symbol; if the ranking "
|
|
361
|
+
"is identical the signal has no effect."
|
|
362
|
+
)
|
|
363
|
+
# Specifically: is_valid_email should rank higher with the query
|
|
364
|
+
# "email validation check" than without it.
|
|
365
|
+
base_email_rank = base_order.index("./validators.py:is_valid_email")
|
|
366
|
+
q_email_rank = q_order.index("./validators.py:is_valid_email")
|
|
367
|
+
assert q_email_rank < base_email_rank, (
|
|
368
|
+
f"is_valid_email should rank HIGHER with query_text='email "
|
|
369
|
+
f"validation check' (baseline rank {base_email_rank} -> query "
|
|
370
|
+
f"rank {q_email_rank}), not lower or unchanged."
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
def test_query_weight_zero_is_noop(self):
|
|
374
|
+
"""query_weight=0.0 must be a no-op even with non-None query_text."""
|
|
375
|
+
_require_fixture(MEDIUM, "medium_repo")
|
|
376
|
+
idx = index_repository(MEDIUM)
|
|
377
|
+
seed = "./service.py:onboard_user"
|
|
378
|
+
|
|
379
|
+
baseline = analyze_impact(idx, [seed])
|
|
380
|
+
with_weight_zero = analyze_impact(
|
|
381
|
+
idx, [seed], query_text="email validation", query_weight=0.0,
|
|
382
|
+
)
|
|
383
|
+
assert baseline.scores == with_weight_zero.scores, (
|
|
384
|
+
"query_weight=0.0 must be a no-op even with non-None query_text."
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
|
|
308
388
|
if __name__ == "__main__":
|
|
309
389
|
pytest.main([__file__, "-v"])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|