mcp-vector-search 0.12.6__py3-none-any.whl → 1.1.22__py3-none-any.whl

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 (92) hide show
  1. mcp_vector_search/__init__.py +3 -3
  2. mcp_vector_search/analysis/__init__.py +111 -0
  3. mcp_vector_search/analysis/baseline/__init__.py +68 -0
  4. mcp_vector_search/analysis/baseline/comparator.py +462 -0
  5. mcp_vector_search/analysis/baseline/manager.py +621 -0
  6. mcp_vector_search/analysis/collectors/__init__.py +74 -0
  7. mcp_vector_search/analysis/collectors/base.py +164 -0
  8. mcp_vector_search/analysis/collectors/cohesion.py +463 -0
  9. mcp_vector_search/analysis/collectors/complexity.py +743 -0
  10. mcp_vector_search/analysis/collectors/coupling.py +1162 -0
  11. mcp_vector_search/analysis/collectors/halstead.py +514 -0
  12. mcp_vector_search/analysis/collectors/smells.py +325 -0
  13. mcp_vector_search/analysis/debt.py +516 -0
  14. mcp_vector_search/analysis/interpretation.py +685 -0
  15. mcp_vector_search/analysis/metrics.py +414 -0
  16. mcp_vector_search/analysis/reporters/__init__.py +7 -0
  17. mcp_vector_search/analysis/reporters/console.py +646 -0
  18. mcp_vector_search/analysis/reporters/markdown.py +480 -0
  19. mcp_vector_search/analysis/reporters/sarif.py +377 -0
  20. mcp_vector_search/analysis/storage/__init__.py +93 -0
  21. mcp_vector_search/analysis/storage/metrics_store.py +762 -0
  22. mcp_vector_search/analysis/storage/schema.py +245 -0
  23. mcp_vector_search/analysis/storage/trend_tracker.py +560 -0
  24. mcp_vector_search/analysis/trends.py +308 -0
  25. mcp_vector_search/analysis/visualizer/__init__.py +90 -0
  26. mcp_vector_search/analysis/visualizer/d3_data.py +534 -0
  27. mcp_vector_search/analysis/visualizer/exporter.py +484 -0
  28. mcp_vector_search/analysis/visualizer/html_report.py +2895 -0
  29. mcp_vector_search/analysis/visualizer/schemas.py +525 -0
  30. mcp_vector_search/cli/commands/analyze.py +1062 -0
  31. mcp_vector_search/cli/commands/chat.py +1455 -0
  32. mcp_vector_search/cli/commands/index.py +621 -5
  33. mcp_vector_search/cli/commands/index_background.py +467 -0
  34. mcp_vector_search/cli/commands/init.py +13 -0
  35. mcp_vector_search/cli/commands/install.py +597 -335
  36. mcp_vector_search/cli/commands/install_old.py +8 -4
  37. mcp_vector_search/cli/commands/mcp.py +78 -6
  38. mcp_vector_search/cli/commands/reset.py +68 -26
  39. mcp_vector_search/cli/commands/search.py +224 -8
  40. mcp_vector_search/cli/commands/setup.py +1184 -0
  41. mcp_vector_search/cli/commands/status.py +339 -5
  42. mcp_vector_search/cli/commands/uninstall.py +276 -357
  43. mcp_vector_search/cli/commands/visualize/__init__.py +39 -0
  44. mcp_vector_search/cli/commands/visualize/cli.py +292 -0
  45. mcp_vector_search/cli/commands/visualize/exporters/__init__.py +12 -0
  46. mcp_vector_search/cli/commands/visualize/exporters/html_exporter.py +33 -0
  47. mcp_vector_search/cli/commands/visualize/exporters/json_exporter.py +33 -0
  48. mcp_vector_search/cli/commands/visualize/graph_builder.py +647 -0
  49. mcp_vector_search/cli/commands/visualize/layout_engine.py +469 -0
  50. mcp_vector_search/cli/commands/visualize/server.py +600 -0
  51. mcp_vector_search/cli/commands/visualize/state_manager.py +428 -0
  52. mcp_vector_search/cli/commands/visualize/templates/__init__.py +16 -0
  53. mcp_vector_search/cli/commands/visualize/templates/base.py +234 -0
  54. mcp_vector_search/cli/commands/visualize/templates/scripts.py +4542 -0
  55. mcp_vector_search/cli/commands/visualize/templates/styles.py +2522 -0
  56. mcp_vector_search/cli/didyoumean.py +27 -2
  57. mcp_vector_search/cli/main.py +127 -160
  58. mcp_vector_search/cli/output.py +158 -13
  59. mcp_vector_search/config/__init__.py +4 -0
  60. mcp_vector_search/config/default_thresholds.yaml +52 -0
  61. mcp_vector_search/config/settings.py +12 -0
  62. mcp_vector_search/config/thresholds.py +273 -0
  63. mcp_vector_search/core/__init__.py +16 -0
  64. mcp_vector_search/core/auto_indexer.py +3 -3
  65. mcp_vector_search/core/boilerplate.py +186 -0
  66. mcp_vector_search/core/config_utils.py +394 -0
  67. mcp_vector_search/core/database.py +406 -94
  68. mcp_vector_search/core/embeddings.py +24 -0
  69. mcp_vector_search/core/exceptions.py +11 -0
  70. mcp_vector_search/core/git.py +380 -0
  71. mcp_vector_search/core/git_hooks.py +4 -4
  72. mcp_vector_search/core/indexer.py +632 -54
  73. mcp_vector_search/core/llm_client.py +756 -0
  74. mcp_vector_search/core/models.py +91 -1
  75. mcp_vector_search/core/project.py +17 -0
  76. mcp_vector_search/core/relationships.py +473 -0
  77. mcp_vector_search/core/scheduler.py +11 -11
  78. mcp_vector_search/core/search.py +179 -29
  79. mcp_vector_search/mcp/server.py +819 -9
  80. mcp_vector_search/parsers/python.py +285 -5
  81. mcp_vector_search/utils/__init__.py +2 -0
  82. mcp_vector_search/utils/gitignore.py +0 -3
  83. mcp_vector_search/utils/gitignore_updater.py +212 -0
  84. mcp_vector_search/utils/monorepo.py +66 -4
  85. mcp_vector_search/utils/timing.py +10 -6
  86. {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/METADATA +184 -53
  87. mcp_vector_search-1.1.22.dist-info/RECORD +120 -0
  88. {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/WHEEL +1 -1
  89. {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/entry_points.txt +1 -0
  90. mcp_vector_search/cli/commands/visualize.py +0 -1467
  91. mcp_vector_search-0.12.6.dist-info/RECORD +0 -68
  92. {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/licenses/LICENSE +0 -0
@@ -578,7 +578,8 @@ def demo(
578
578
  demo_dir.mkdir()
579
579
 
580
580
  # Create sample files
581
- (demo_dir / "main.py").write_text("""
581
+ (demo_dir / "main.py").write_text(
582
+ """
582
583
  def main():
583
584
  '''Main entry point for the application.'''
584
585
  print("Hello, World!")
@@ -600,9 +601,11 @@ class UserService:
600
601
 
601
602
  if __name__ == "__main__":
602
603
  main()
603
- """)
604
+ """
605
+ )
604
606
 
605
- (demo_dir / "utils.py").write_text("""
607
+ (demo_dir / "utils.py").write_text(
608
+ """
606
609
  import json
607
610
  from typing import Dict, Any
608
611
 
@@ -619,7 +622,8 @@ def hash_password(password: str) -> str:
619
622
  '''Hash password for secure storage.'''
620
623
  import hashlib
621
624
  return hashlib.sha256(password.encode()).hexdigest()
622
- """)
625
+ """
626
+ )
623
627
 
624
628
  console.print(
625
629
  f"\n[bold blue]📁 Created demo project at:[/bold blue] {demo_dir}"
@@ -38,11 +38,50 @@ Each tool has its own configuration format and location.
38
38
  3. Test setup: [green]mcp-vector-search mcp test[/green]
39
39
 
40
40
  [dim]Use --force to overwrite existing configurations[/dim]
41
- """
41
+ """,
42
+ no_args_is_help=False, # Allow running without subcommand
43
+ invoke_without_command=True, # Call callback even without subcommand
42
44
  )
43
45
 
44
46
  console = Console()
45
47
 
48
+
49
+ @mcp_app.callback()
50
+ def mcp_callback(ctx: typer.Context):
51
+ """MCP server management.
52
+
53
+ When invoked without a subcommand, starts the MCP server over stdio.
54
+ Use subcommands to configure MCP integration for different AI tools.
55
+ """
56
+ # Store context for subcommands
57
+ if not ctx.obj:
58
+ ctx.obj = {}
59
+
60
+ # If a subcommand was invoked, let it handle things (check this FIRST)
61
+ if ctx.invoked_subcommand is not None:
62
+ return
63
+
64
+ # No subcommand - start the MCP server
65
+ import asyncio
66
+ from pathlib import Path
67
+
68
+ from ...mcp.server import run_mcp_server
69
+
70
+ project_root = ctx.obj.get("project_root") if ctx.obj else None
71
+ if project_root is None:
72
+ project_root = Path.cwd()
73
+
74
+ # Start the MCP server over stdio
75
+ try:
76
+ asyncio.run(run_mcp_server(project_root))
77
+ raise typer.Exit(0)
78
+ except KeyboardInterrupt:
79
+ raise typer.Exit(0)
80
+ except Exception as e:
81
+ print(f"MCP server error: {e}", file=sys.stderr)
82
+ raise typer.Exit(1)
83
+
84
+
46
85
  # Supported AI tools and their configuration details
47
86
  SUPPORTED_TOOLS = {
48
87
  "auggie": {
@@ -123,6 +162,29 @@ def get_mcp_server_command(
123
162
  return f"{python_exe} -m mcp_vector_search.mcp.server{watch_flag} {project_root}"
124
163
 
125
164
 
165
+ def detect_install_method() -> tuple[str, list[str]]:
166
+ """Detect how mcp-vector-search is installed and return appropriate command.
167
+
168
+ Returns:
169
+ Tuple of (command, args) for running mcp-vector-search mcp
170
+ """
171
+ # Check if we're in a uv-managed environment
172
+ # uv sets UV_PROJECT_ENVIRONMENT or has .venv structure
173
+ if os.environ.get("VIRTUAL_ENV") and ".venv" in os.environ.get("VIRTUAL_ENV", ""):
174
+ # Likely uv project environment
175
+ if shutil.which("uv"):
176
+ return ("uv", ["run", "mcp-vector-search", "mcp"])
177
+
178
+ # Check if mcp-vector-search is directly available in PATH
179
+ mcp_cmd = shutil.which("mcp-vector-search")
180
+ if mcp_cmd:
181
+ # Installed via pipx or pip - use direct command
182
+ return ("mcp-vector-search", ["mcp"])
183
+
184
+ # Fallback to uv run (development mode)
185
+ return ("uv", ["run", "mcp-vector-search", "mcp"])
186
+
187
+
126
188
  def get_mcp_server_config_for_tool(
127
189
  project_root: Path,
128
190
  tool_name: str,
@@ -130,9 +192,11 @@ def get_mcp_server_config_for_tool(
130
192
  enable_file_watching: bool = True,
131
193
  ) -> dict[str, Any]:
132
194
  """Generate MCP server configuration for a specific tool."""
195
+ command, args = detect_install_method()
196
+
133
197
  base_config = {
134
- "command": "uv",
135
- "args": ["run", "mcp-vector-search", "mcp"],
198
+ "command": command,
199
+ "args": args,
136
200
  "env": {
137
201
  "MCP_ENABLE_FILE_WATCHING": "true" if enable_file_watching else "false"
138
202
  },
@@ -183,11 +247,12 @@ def create_project_claude_config(
183
247
  if "mcpServers" not in config:
184
248
  config["mcpServers"] = {}
185
249
 
186
- # Use uv for better compatibility, with proper args structure
250
+ # Detect installation method and use appropriate command
251
+ command, args = detect_install_method()
187
252
  config["mcpServers"][server_name] = {
188
253
  "type": "stdio",
189
- "command": "uv",
190
- "args": ["run", "mcp-vector-search", "mcp"],
254
+ "command": command,
255
+ "args": args,
191
256
  "env": {
192
257
  "MCP_ENABLE_FILE_WATCHING": "true" if enable_file_watching else "false"
193
258
  },
@@ -198,6 +263,13 @@ def create_project_claude_config(
198
263
  json.dump(config, f, indent=2)
199
264
 
200
265
  print_success("Created project-level .mcp.json with MCP server configuration")
266
+
267
+ # Show which command will be used
268
+ if command == "uv":
269
+ print_info(f"Using uv: {command} {' '.join(args)}")
270
+ else:
271
+ print_info(f"Using direct command: {command} {' '.join(args)}")
272
+
201
273
  if enable_file_watching:
202
274
  print_info("File watching is enabled for automatic reindexing")
203
275
  else:
@@ -82,27 +82,58 @@ def reset_index(
82
82
  console.print("[yellow]Reset cancelled[/yellow]")
83
83
  raise typer.Exit(0)
84
84
 
85
- # Get the database directory
86
- project_manager.load_config()
87
- db_path = root / ".mcp_vector_search" / "db"
85
+ # Get the database directory from config
86
+ config = project_manager.load_config()
87
+ db_path = Path(config.index_path)
88
+
89
+ # Check if index exists (look for chroma.sqlite3 or collection directories)
90
+ has_index = (db_path / "chroma.sqlite3").exists()
88
91
 
89
- if not db_path.exists():
92
+ if not has_index:
90
93
  print_warning("No index found. Nothing to reset.")
91
94
  raise typer.Exit(0)
92
95
 
96
+ # Files/dirs to remove (index data)
97
+ index_files = [
98
+ "chroma.sqlite3",
99
+ "cache",
100
+ "indexing_errors.log",
101
+ "index_metadata.json",
102
+ "directory_index.json",
103
+ ]
104
+
105
+ # Also remove any UUID-named directories (ChromaDB collections)
106
+ if db_path.exists():
107
+ for item in db_path.iterdir():
108
+ if item.is_dir() and len(item.name) == 36 and "-" in item.name:
109
+ # Looks like a UUID directory
110
+ index_files.append(item.name)
111
+
93
112
  # Create backup if requested
94
113
  if backup:
95
- backup_dir = root / ".mcp_vector_search" / "backups"
114
+ backup_dir = db_path / "backups"
96
115
  backup_dir.mkdir(exist_ok=True)
97
116
 
98
117
  import time
99
118
 
100
119
  timestamp = int(time.time())
101
- backup_path = backup_dir / f"db_backup_{timestamp}"
120
+ backup_path = backup_dir / f"index_backup_{timestamp}"
121
+ backup_path.mkdir(exist_ok=True)
102
122
 
103
123
  try:
104
- shutil.copytree(db_path, backup_path)
105
- print_success(f"Created backup at: {backup_path.relative_to(root)}")
124
+ backed_up = []
125
+ for file in index_files:
126
+ src = db_path / file
127
+ if src.exists():
128
+ dest = backup_path / file
129
+ if src.is_dir():
130
+ shutil.copytree(src, dest)
131
+ else:
132
+ shutil.copy2(src, dest)
133
+ backed_up.append(file)
134
+
135
+ if backed_up:
136
+ print_success(f"Created backup at: {backup_path.relative_to(root)}")
106
137
  except Exception as e:
107
138
  print_warning(f"Could not create backup: {e}")
108
139
  if not force:
@@ -110,12 +141,25 @@ def reset_index(
110
141
  console.print("[yellow]Reset cancelled[/yellow]")
111
142
  raise typer.Exit(0)
112
143
 
113
- # Clear the index
144
+ # Clear the index files
114
145
  console.print("[cyan]Clearing index...[/cyan]")
146
+ removed_count = 0
115
147
  try:
116
- shutil.rmtree(db_path)
117
- db_path.mkdir(parents=True, exist_ok=True)
118
- print_success("Index cleared successfully!")
148
+ for file in index_files:
149
+ path = db_path / file
150
+ if path.exists():
151
+ if path.is_dir():
152
+ shutil.rmtree(path)
153
+ else:
154
+ path.unlink()
155
+ removed_count += 1
156
+
157
+ if removed_count > 0:
158
+ print_success(
159
+ f"Index cleared successfully! ({removed_count} items removed)"
160
+ )
161
+ else:
162
+ print_warning("No index files found to remove.")
119
163
  except Exception as e:
120
164
  print_error(f"Failed to clear index: {e}")
121
165
  raise typer.Exit(1)
@@ -215,19 +259,9 @@ def reset_all(
215
259
  raise typer.Exit(1)
216
260
 
217
261
 
218
- @reset_app.command("health")
219
262
  async def check_health(
220
- project_root: Path = typer.Option(
221
- None,
222
- "--project-root",
223
- "-p",
224
- help="Project root directory",
225
- ),
226
- fix: bool = typer.Option(
227
- False,
228
- "--fix",
229
- help="Attempt to fix issues if found",
230
- ),
263
+ project_root: Path,
264
+ fix: bool,
231
265
  ) -> None:
232
266
  """Check the health of the search index.
233
267
 
@@ -254,7 +288,7 @@ async def check_health(
254
288
  from ...core.embeddings import create_embedding_function
255
289
 
256
290
  config = project_manager.load_config()
257
- db_path = root / ".mcp_vector_search" / "db"
291
+ db_path = Path(config.index_path)
258
292
 
259
293
  # Setup embedding function and cache
260
294
  cache_dir = get_default_cache_path(root) if config.cache_embeddings else None
@@ -376,6 +410,7 @@ main = reset_main
376
410
 
377
411
 
378
412
  # Make health check synchronous for CLI
413
+ @reset_app.command("health")
379
414
  def health_main(
380
415
  project_root: Path = typer.Option(
381
416
  None,
@@ -389,5 +424,12 @@ def health_main(
389
424
  help="Attempt to fix issues if found",
390
425
  ),
391
426
  ) -> None:
392
- """Check the health of the search index (sync wrapper)."""
427
+ """Check the health of the search index.
428
+
429
+ This command will:
430
+ - Verify database connectivity
431
+ - Check for index corruption
432
+ - Validate collection integrity
433
+ - Optionally attempt repairs with --fix
434
+ """
393
435
  asyncio.run(check_health(project_root, fix))
@@ -1,6 +1,8 @@
1
1
  """Search command for MCP Vector Search CLI."""
2
2
 
3
3
  import asyncio
4
+ import os
5
+ from fnmatch import fnmatch
4
6
  from pathlib import Path
5
7
 
6
8
  import typer
@@ -62,7 +64,7 @@ def search_main(
62
64
  None,
63
65
  "--files",
64
66
  "-f",
65
- help="Filter by file patterns (e.g., '*.py' or 'src/*.js')",
67
+ help="Filter by file glob patterns (e.g., '*.py', 'src/*.js', 'tests/*.ts'). Matches basename or relative path.",
66
68
  rich_help_panel="🔍 Filters",
67
69
  ),
68
70
  language: str | None = typer.Option(
@@ -134,6 +136,41 @@ def search_main(
134
136
  help="Custom export file path",
135
137
  rich_help_panel="💾 Export Options",
136
138
  ),
139
+ max_complexity: int | None = typer.Option(
140
+ None,
141
+ "--max-complexity",
142
+ help="Filter results with cognitive complexity greater than N",
143
+ min=1,
144
+ rich_help_panel="🎯 Quality Filters",
145
+ ),
146
+ no_smells: bool = typer.Option(
147
+ False,
148
+ "--no-smells",
149
+ help="Exclude results with code smells",
150
+ rich_help_panel="🎯 Quality Filters",
151
+ ),
152
+ grade: str | None = typer.Option(
153
+ None,
154
+ "--grade",
155
+ help="Filter by complexity grade (e.g., 'A,B,C' or 'A-C')",
156
+ rich_help_panel="🎯 Quality Filters",
157
+ ),
158
+ min_quality: int | None = typer.Option(
159
+ None,
160
+ "--min-quality",
161
+ help="Filter by minimum quality score (0-100)",
162
+ min=0,
163
+ max=100,
164
+ rich_help_panel="🎯 Quality Filters",
165
+ ),
166
+ quality_weight: float = typer.Option(
167
+ 0.3,
168
+ "--quality-weight",
169
+ help="Weight for quality ranking (0.0=pure relevance, 1.0=pure quality, default=0.3)",
170
+ min=0.0,
171
+ max=1.0,
172
+ rich_help_panel="🎯 Quality Filters",
173
+ ),
137
174
  ) -> None:
138
175
  """🔍 Search your codebase semantically.
139
176
 
@@ -153,8 +190,10 @@ def search_main(
153
190
 
154
191
  [bold cyan]Advanced Search:[/bold cyan]
155
192
 
156
- [green]Filter by file pattern:[/green]
157
- $ mcp-vector-search search "validation" --files "src/*.py"
193
+ [green]Filter by file pattern (glob):[/green]
194
+ $ mcp-vector-search search "validation" --files "*.py"
195
+ $ mcp-vector-search search "component" --files "src/*.tsx"
196
+ $ mcp-vector-search search "test utils" --files "tests/*.ts"
158
197
 
159
198
  [green]Find similar code:[/green]
160
199
  $ mcp-vector-search search "src/auth.py" --similar
@@ -162,6 +201,25 @@ def search_main(
162
201
  [green]Context-based search:[/green]
163
202
  $ mcp-vector-search search "implement rate limiting" --context --focus security
164
203
 
204
+ [bold cyan]Quality Filters:[/bold cyan]
205
+
206
+ [green]Filter by complexity:[/green]
207
+ $ mcp-vector-search search "authentication" --max-complexity 15
208
+
209
+ [green]Exclude code smells:[/green]
210
+ $ mcp-vector-search search "login" --no-smells
211
+
212
+ [green]Filter by grade:[/green]
213
+ $ mcp-vector-search search "api" --grade A,B
214
+
215
+ [green]Minimum quality score:[/green]
216
+ $ mcp-vector-search search "handler" --min-quality 80
217
+
218
+ [green]Quality-aware ranking:[/green]
219
+ $ mcp-vector-search search "auth" --quality-weight 0.5 # Balance relevance and quality
220
+ $ mcp-vector-search search "api" --quality-weight 0.0 # Pure semantic search
221
+ $ mcp-vector-search search "util" --quality-weight 1.0 # Pure quality ranking
222
+
165
223
  [bold cyan]Export Results:[/bold cyan]
166
224
 
167
225
  [green]Export to JSON:[/green]
@@ -238,6 +296,11 @@ def search_main(
238
296
  json_output=json_output,
239
297
  export_format=export_format,
240
298
  export_path=export_path,
299
+ max_complexity=max_complexity,
300
+ no_smells=no_smells,
301
+ grade=grade,
302
+ min_quality=min_quality,
303
+ quality_weight=quality_weight,
241
304
  )
242
305
  )
243
306
 
@@ -247,6 +310,52 @@ def search_main(
247
310
  raise typer.Exit(1)
248
311
 
249
312
 
313
+ def _parse_grade_filter(grade_str: str) -> set[str]:
314
+ """Parse grade filter string into set of allowed grades.
315
+
316
+ Supports formats:
317
+ - Comma-separated: "A,B,C"
318
+ - Range: "A-C" (expands to A, B, C)
319
+ - Mixed: "A,C-D" (expands to A, C, D)
320
+
321
+ Args:
322
+ grade_str: Grade filter string
323
+
324
+ Returns:
325
+ Set of allowed grade letters
326
+ """
327
+ allowed_grades = set()
328
+ grade_order = ["A", "B", "C", "D", "F"]
329
+
330
+ # Split by comma
331
+ parts = [part.strip().upper() for part in grade_str.split(",")]
332
+
333
+ for part in parts:
334
+ if "-" in part:
335
+ # Range format (e.g., "A-C")
336
+ start, end = part.split("-", 1)
337
+ start = start.strip()
338
+ end = end.strip()
339
+
340
+ if start in grade_order and end in grade_order:
341
+ start_idx = grade_order.index(start)
342
+ end_idx = grade_order.index(end)
343
+
344
+ # Handle reverse ranges (C-A becomes A-C)
345
+ if start_idx > end_idx:
346
+ start_idx, end_idx = end_idx, start_idx
347
+
348
+ # Add all grades in range
349
+ for grade in grade_order[start_idx : end_idx + 1]:
350
+ allowed_grades.add(grade)
351
+ else:
352
+ # Single grade
353
+ if part in grade_order:
354
+ allowed_grades.add(part)
355
+
356
+ return allowed_grades
357
+
358
+
250
359
  async def run_search(
251
360
  project_root: Path,
252
361
  query: str,
@@ -260,8 +369,13 @@ async def run_search(
260
369
  json_output: bool = False,
261
370
  export_format: str | None = None,
262
371
  export_path: Path | None = None,
372
+ max_complexity: int | None = None,
373
+ no_smells: bool = False,
374
+ grade: str | None = None,
375
+ min_quality: int | None = None,
376
+ quality_weight: float = 0.3,
263
377
  ) -> None:
264
- """Run semantic search."""
378
+ """Run semantic search with optional quality filters and quality-aware ranking."""
265
379
  # Load project configuration
266
380
  project_manager = ProjectManager(project_root)
267
381
 
@@ -326,7 +440,7 @@ async def run_search(
326
440
  similarity_threshold=similarity_threshold or config.similarity_threshold,
327
441
  )
328
442
 
329
- # Build filters
443
+ # Build filters (exclude file_path - will be handled with post-filtering)
330
444
  filters = {}
331
445
  if language:
332
446
  filters["language"] = language
@@ -334,9 +448,6 @@ async def run_search(
334
448
  filters["function_name"] = function_name
335
449
  if class_name:
336
450
  filters["class_name"] = class_name
337
- if files:
338
- # Simple file pattern matching (could be enhanced)
339
- filters["file_path"] = files
340
451
 
341
452
  try:
342
453
  async with database:
@@ -348,6 +459,110 @@ async def run_search(
348
459
  include_context=show_content,
349
460
  )
350
461
 
462
+ # Post-filter results by file pattern if specified
463
+ if files and results:
464
+ filtered_results = []
465
+ for result in results:
466
+ # Get relative path from project root
467
+ try:
468
+ rel_path = str(result.file_path.relative_to(project_root))
469
+ except ValueError:
470
+ # If file is outside project root, use absolute path
471
+ rel_path = str(result.file_path)
472
+
473
+ # Match against glob pattern (both full path and basename)
474
+ if fnmatch(rel_path, files) or fnmatch(
475
+ os.path.basename(rel_path), files
476
+ ):
477
+ filtered_results.append(result)
478
+
479
+ results = filtered_results
480
+ logger.debug(
481
+ f"File pattern '{files}' filtered results to {len(results)} matches"
482
+ )
483
+
484
+ # Apply quality filters if specified
485
+ if any([max_complexity, no_smells, grade, min_quality]) and results:
486
+ filtered_results = []
487
+ for result in results:
488
+ # Parse quality metrics from result metadata
489
+ cognitive_complexity = getattr(result, "cognitive_complexity", None)
490
+ complexity_grade = getattr(result, "complexity_grade", None)
491
+ smell_count = getattr(result, "smell_count", None)
492
+ quality_score = getattr(result, "quality_score", None)
493
+
494
+ # Filter by max complexity
495
+ if max_complexity is not None and cognitive_complexity is not None:
496
+ if cognitive_complexity > max_complexity:
497
+ continue
498
+
499
+ # Filter by code smells
500
+ if no_smells and smell_count is not None:
501
+ if smell_count > 0:
502
+ continue
503
+
504
+ # Filter by grade
505
+ if grade and complexity_grade:
506
+ allowed_grades = _parse_grade_filter(grade)
507
+ if complexity_grade not in allowed_grades:
508
+ continue
509
+
510
+ # Filter by minimum quality score
511
+ if min_quality is not None and quality_score is not None:
512
+ if quality_score < min_quality:
513
+ continue
514
+
515
+ filtered_results.append(result)
516
+
517
+ initial_count = len(results)
518
+ results = filtered_results
519
+ logger.debug(
520
+ f"Quality filters reduced results from {initial_count} to {len(results)}"
521
+ )
522
+
523
+ # Apply quality-aware ranking if quality_weight > 0 and results have quality metrics
524
+ if quality_weight > 0.0 and results:
525
+ # Calculate quality scores for results that don't have them
526
+ for result in results:
527
+ if result.quality_score is None:
528
+ # Calculate quality score using the formula
529
+ calculated_score = result.calculate_quality_score()
530
+ if calculated_score is not None:
531
+ result.quality_score = calculated_score
532
+
533
+ # Re-rank results based on combined score
534
+ # Store original similarity score for display
535
+ for result in results:
536
+ # Store original relevance score
537
+ if not hasattr(result, "_original_similarity"):
538
+ result._original_similarity = result.similarity_score
539
+
540
+ # Calculate combined score
541
+ if result.quality_score is not None:
542
+ # Normalize quality score to 0-1 range (it's 0-100)
543
+ normalized_quality = result.quality_score / 100.0
544
+
545
+ # Combined score: (1-W) × relevance + W × quality
546
+ combined_score = (
547
+ (1.0 - quality_weight) * result.similarity_score
548
+ + quality_weight * normalized_quality
549
+ )
550
+
551
+ # Update similarity_score with combined score for sorting
552
+ result.similarity_score = combined_score
553
+ # If no quality score, keep original similarity_score
554
+
555
+ # Re-sort by combined score
556
+ results.sort(key=lambda r: r.similarity_score, reverse=True)
557
+
558
+ # Update ranks
559
+ for i, result in enumerate(results):
560
+ result.rank = i + 1
561
+
562
+ logger.debug(
563
+ f"Quality-aware ranking applied with weight {quality_weight:.2f}"
564
+ )
565
+
351
566
  # Handle export if requested
352
567
  if export_format:
353
568
  from ..export import SearchResultExporter, get_export_path
@@ -401,6 +616,7 @@ async def run_search(
401
616
  results=results,
402
617
  query=query,
403
618
  show_content=show_content,
619
+ quality_weight=quality_weight,
404
620
  )
405
621
 
406
622
  # Add contextual tips based on results