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.
Files changed (72) hide show
  1. {diffcontext-0.5.2 → diffcontext-0.5.4}/PKG-INFO +1 -1
  2. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/__init__.py +1 -1
  3. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/cli/__init__.py +18 -3
  4. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/context/compiler.py +5 -8
  5. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/mcp_server.py +35 -9
  6. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/pipeline.py +42 -3
  7. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext.egg-info/PKG-INFO +1 -1
  8. {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_compiler_meta.py +1 -2
  9. {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_core.py +80 -0
  10. {diffcontext-0.5.2 → diffcontext-0.5.4}/LICENSE +0 -0
  11. {diffcontext-0.5.2 → diffcontext-0.5.4}/README.md +0 -0
  12. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/_warn_once.py +0 -0
  13. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/cache.py +0 -0
  14. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/context/__init__.py +0 -0
  15. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/context/selector.py +0 -0
  16. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/diff/__init__.py +0 -0
  17. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/diff/git_diff.py +0 -0
  18. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/diff/state_manager.py +0 -0
  19. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/graph_builder.py +0 -0
  20. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/history.py +0 -0
  21. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/impact/__init__.py +0 -0
  22. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/impact/blast_radius.py +0 -0
  23. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/impact/scoring.py +0 -0
  24. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/impact/traversal.py +0 -0
  25. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/impact/visualizer.py +0 -0
  26. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/languages/__init__.py +0 -0
  27. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/languages/typescript.py +0 -0
  28. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/lexical.py +0 -0
  29. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/models.py +0 -0
  30. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/parser.py +0 -0
  31. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/py.typed +0 -0
  32. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/rerank/__init__.py +0 -0
  33. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/rerank/features.py +0 -0
  34. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/rerank/model.py +0 -0
  35. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/resolver.py +0 -0
  36. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/scanner.py +0 -0
  37. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/symbols.py +0 -0
  38. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/verify/__init__.py +0 -0
  39. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/verify/cases.py +0 -0
  40. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/verify/history.py +0 -0
  41. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext/verify/sufficiency.py +0 -0
  42. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext.egg-info/SOURCES.txt +0 -0
  43. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext.egg-info/dependency_links.txt +0 -0
  44. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext.egg-info/entry_points.txt +0 -0
  45. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext.egg-info/requires.txt +0 -0
  46. {diffcontext-0.5.2 → diffcontext-0.5.4}/diffcontext.egg-info/top_level.txt +0 -0
  47. {diffcontext-0.5.2 → diffcontext-0.5.4}/pyproject.toml +0 -0
  48. {diffcontext-0.5.2 → diffcontext-0.5.4}/setup.cfg +0 -0
  49. {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_cache.py +0 -0
  50. {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_cutoff.py +0 -0
  51. {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_determinism.py +0 -0
  52. {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_downstream_eval.py +0 -0
  53. {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_git_diff.py +0 -0
  54. {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_harness_api.py +0 -0
  55. {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_history.py +0 -0
  56. {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_incremental.py +0 -0
  57. {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_index_scoping.py +0 -0
  58. {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_lexical.py +0 -0
  59. {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_mcp_server.py +0 -0
  60. {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_rerank.py +0 -0
  61. {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_semantic_ablation.py +0 -0
  62. {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_semantic_audit.py +0 -0
  63. {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_semantic_gap.py +0 -0
  64. {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_semantic_metrics.py +0 -0
  65. {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_semantic_mine.py +0 -0
  66. {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_service_clone.py +0 -0
  67. {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_src_layout.py +0 -0
  68. {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_stats.py +0 -0
  69. {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_token_budget.py +0 -0
  70. {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_typescript_adapter.py +0 -0
  71. {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_verify.py +0 -0
  72. {diffcontext-0.5.2 → diffcontext-0.5.4}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diffcontext
3
- Version: 0.5.2
3
+ Version: 0.5.4
4
4
  Summary: Static-analysis-powered repository context compiler for LLMs
5
5
  Author-email: Trakshan Mishra <trakshanmishra477@gmail.com>
6
6
  License: MIT
@@ -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.2"
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 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."
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 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.
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), and the A/B test showed meta
425
- # displaces code under tight budgets. Compact keeps counts + dropped
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="0.5.1",
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 = "compact",
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" (default), or "off".
106
- Compact saves ~60% of header tokens for code.
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
- changed = _resolve_changed(idx, changed_symbols, git_ref, repo)
113
- if not changed:
114
- return "No changed symbols found. Pass changed_symbols or git_ref."
115
- impact = analyze_impact(idx, changed)
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
- A/B test showed full meta costs ~10pp pass@1 at 4000
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diffcontext
3
- Version: 0.5.2
3
+ Version: 0.5.4
4
4
  Summary: Static-analysis-powered repository context compiler for LLMs
5
5
  Author-email: Trakshan Mishra <trakshanmishra477@gmail.com>
6
6
  License: MIT
@@ -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 A/B test showed full meta costs ~10pp pass@1 at 4000
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