codegraphcontext-rust 0.5.5__cp311-cp311-win_amd64.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 (101) hide show
  1. codegraphcontext/__init__.py +2 -0
  2. codegraphcontext/__main__.py +5 -0
  3. codegraphcontext/_cgc_rust.cp311-win_amd64.pyd +0 -0
  4. codegraphcontext/cli/__init__.py +1 -0
  5. codegraphcontext/cli/cli_helpers.py +774 -0
  6. codegraphcontext/cli/config_manager.py +1051 -0
  7. codegraphcontext/cli/main.py +2386 -0
  8. codegraphcontext/cli/registry_commands.py +404 -0
  9. codegraphcontext/cli/setup_macos.py +93 -0
  10. codegraphcontext/cli/setup_wizard.py +991 -0
  11. codegraphcontext/cli/visualizer.py +50 -0
  12. codegraphcontext/core/__init__.py +165 -0
  13. codegraphcontext/core/bundle_registry.py +181 -0
  14. codegraphcontext/core/cgc_bundle.py +858 -0
  15. codegraphcontext/core/cgcignore.py +118 -0
  16. codegraphcontext/core/database.py +273 -0
  17. codegraphcontext/core/database_falkordb.py +480 -0
  18. codegraphcontext/core/database_falkordb_remote.py +199 -0
  19. codegraphcontext/core/database_kuzu.py +636 -0
  20. codegraphcontext/core/falkor_worker.py +132 -0
  21. codegraphcontext/core/jobs.py +132 -0
  22. codegraphcontext/core/watcher.py +260 -0
  23. codegraphcontext/prompts.py +125 -0
  24. codegraphcontext/server.py +471 -0
  25. codegraphcontext/tool_definitions.py +220 -0
  26. codegraphcontext/tools/__init__.py +0 -0
  27. codegraphcontext/tools/advanced_language_query_tool.py +103 -0
  28. codegraphcontext/tools/code_finder.py +1160 -0
  29. codegraphcontext/tools/graph_builder.py +337 -0
  30. codegraphcontext/tools/handlers/analysis_handlers.py +114 -0
  31. codegraphcontext/tools/handlers/indexing_handlers.py +116 -0
  32. codegraphcontext/tools/handlers/management_handlers.py +339 -0
  33. codegraphcontext/tools/handlers/query_handlers.py +83 -0
  34. codegraphcontext/tools/handlers/watcher_handlers.py +83 -0
  35. codegraphcontext/tools/indexing/__init__.py +1 -0
  36. codegraphcontext/tools/indexing/constants.py +25 -0
  37. codegraphcontext/tools/indexing/discovery.py +64 -0
  38. codegraphcontext/tools/indexing/engine.py +109 -0
  39. codegraphcontext/tools/indexing/persistence/__init__.py +3 -0
  40. codegraphcontext/tools/indexing/persistence/parquet_loader.py +643 -0
  41. codegraphcontext/tools/indexing/persistence/writer.py +1114 -0
  42. codegraphcontext/tools/indexing/pipeline.py +171 -0
  43. codegraphcontext/tools/indexing/pre_scan.py +105 -0
  44. codegraphcontext/tools/indexing/resolution/__init__.py +8 -0
  45. codegraphcontext/tools/indexing/resolution/calls.py +253 -0
  46. codegraphcontext/tools/indexing/resolution/inheritance.py +91 -0
  47. codegraphcontext/tools/indexing/sanitize.py +41 -0
  48. codegraphcontext/tools/indexing/schema.py +79 -0
  49. codegraphcontext/tools/indexing/schema_contract.py +44 -0
  50. codegraphcontext/tools/indexing/scip_pipeline.py +140 -0
  51. codegraphcontext/tools/languages/c.py +563 -0
  52. codegraphcontext/tools/languages/cpp.py +616 -0
  53. codegraphcontext/tools/languages/csharp.py +551 -0
  54. codegraphcontext/tools/languages/dart.py +378 -0
  55. codegraphcontext/tools/languages/elixir.py +460 -0
  56. codegraphcontext/tools/languages/go.py +499 -0
  57. codegraphcontext/tools/languages/haskell.py +426 -0
  58. codegraphcontext/tools/languages/java.py +500 -0
  59. codegraphcontext/tools/languages/javascript.py +589 -0
  60. codegraphcontext/tools/languages/kotlin.py +640 -0
  61. codegraphcontext/tools/languages/perl.py +260 -0
  62. codegraphcontext/tools/languages/php.py +519 -0
  63. codegraphcontext/tools/languages/python.py +576 -0
  64. codegraphcontext/tools/languages/ruby.py +537 -0
  65. codegraphcontext/tools/languages/rust.py +295 -0
  66. codegraphcontext/tools/languages/scala.py +519 -0
  67. codegraphcontext/tools/languages/swift.py +491 -0
  68. codegraphcontext/tools/languages/typescript.py +575 -0
  69. codegraphcontext/tools/languages/typescriptjsx.py +151 -0
  70. codegraphcontext/tools/package_resolver.py +472 -0
  71. codegraphcontext/tools/query_tool_languages/c_toolkit.py +5 -0
  72. codegraphcontext/tools/query_tool_languages/cpp_toolkit.py +87 -0
  73. codegraphcontext/tools/query_tool_languages/csharp_toolkit.py +5 -0
  74. codegraphcontext/tools/query_tool_languages/dart_toolkit.py +5 -0
  75. codegraphcontext/tools/query_tool_languages/go_toolkit.py +5 -0
  76. codegraphcontext/tools/query_tool_languages/haskell_toolkit.py +5 -0
  77. codegraphcontext/tools/query_tool_languages/java_toolkit.py +5 -0
  78. codegraphcontext/tools/query_tool_languages/javascript_toolkit.py +5 -0
  79. codegraphcontext/tools/query_tool_languages/perl_toolkit.py +5 -0
  80. codegraphcontext/tools/query_tool_languages/python_toolkit.py +5 -0
  81. codegraphcontext/tools/query_tool_languages/ruby_toolkit.py +5 -0
  82. codegraphcontext/tools/query_tool_languages/rust_toolkit.py +5 -0
  83. codegraphcontext/tools/query_tool_languages/scala_toolkit.py +5 -0
  84. codegraphcontext/tools/query_tool_languages/swift_toolkit.py +5 -0
  85. codegraphcontext/tools/query_tool_languages/typescript_toolkit.py +5 -0
  86. codegraphcontext/tools/scip_indexer.py +467 -0
  87. codegraphcontext/tools/scip_pb2.py +2455 -0
  88. codegraphcontext/tools/system.py +133 -0
  89. codegraphcontext/tools/tree_sitter_parser.py +103 -0
  90. codegraphcontext/utils/debug_log.py +90 -0
  91. codegraphcontext/utils/path_ignore.py +55 -0
  92. codegraphcontext/utils/repo_path.py +27 -0
  93. codegraphcontext/utils/tree_sitter_manager.py +285 -0
  94. codegraphcontext/utils/visualize_graph.py +0 -0
  95. codegraphcontext/viz/server.py +282 -0
  96. codegraphcontext_rust-0.5.5.dist-info/METADATA +537 -0
  97. codegraphcontext_rust-0.5.5.dist-info/RECORD +101 -0
  98. codegraphcontext_rust-0.5.5.dist-info/WHEEL +4 -0
  99. codegraphcontext_rust-0.5.5.dist-info/entry_points.txt +3 -0
  100. codegraphcontext_rust-0.5.5.dist-info/licenses/LICENSE +21 -0
  101. codegraphcontext_rust-0.5.5.dist-info/sboms/cgc-python.cyclonedx.json +2333 -0
@@ -0,0 +1,133 @@
1
+ # src/codegraphcontext/tools/system.py
2
+ import logging
3
+ from dataclasses import asdict
4
+ from typing import Any, Dict
5
+ from datetime import datetime, timedelta
6
+
7
+ from neo4j.exceptions import CypherSyntaxError
8
+
9
+ from ..core.database import DatabaseManager
10
+ from ..core.jobs import JobManager, JobStatus
11
+ from ..utils.debug_log import debug_log
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class SystemTools:
17
+ """Handles system-level tools like job management and direct DB queries."""
18
+
19
+ def __init__(self, db_manager: DatabaseManager, job_manager: JobManager):
20
+ self.db_manager = db_manager
21
+ self.job_manager = job_manager
22
+
23
+ def check_job_status_tool(self, job_id: str) -> Dict[str, Any]:
24
+ """Tool to check job status"""
25
+ try:
26
+ job = self.job_manager.get_job(job_id)
27
+ if not job:
28
+ return {"error": f"Job {job_id} not found"}
29
+
30
+ job_dict = asdict(job)
31
+
32
+ if job.status == JobStatus.RUNNING:
33
+ if job.estimated_time_remaining:
34
+ remaining = job.estimated_time_remaining
35
+ job_dict["estimated_time_remaining_human"] = (
36
+ f"{int(remaining // 60)}m {int(remaining % 60)}s"
37
+ if remaining >= 60 else f"{int(remaining)}s"
38
+ )
39
+
40
+ if job.start_time:
41
+ elapsed = (datetime.now() - job.start_time).total_seconds()
42
+ job_dict["elapsed_time_human"] = (
43
+ f"{int(elapsed // 60)}m {int(elapsed % 60)}s"
44
+ if elapsed >= 60 else f"{int(elapsed)}s"
45
+ )
46
+
47
+ elif job.status == JobStatus.COMPLETED and job.start_time and job.end_time:
48
+ duration = (job.end_time - job.start_time).total_seconds()
49
+ job_dict["actual_duration_human"] = (
50
+ f"{int(duration // 60)}m {int(duration % 60)}s"
51
+ if duration >= 60 else f"{int(duration)}s"
52
+ )
53
+
54
+ job_dict["start_time"] = job.start_time.strftime("%Y-%m-%d %H:%M:%S")
55
+ if job.end_time:
56
+ job_dict["end_time"] = job.end_time.strftime("%Y-%m-%d %H:%M:%S")
57
+
58
+ job_dict["status"] = job.status.value
59
+ return {"success": True, "job": job_dict}
60
+ except Exception as e:
61
+ return {"error": f"Failed to check job status: {str(e)}"}
62
+
63
+ def list_jobs_tool(self) -> Dict[str, Any]:
64
+ """Tool to list all jobs"""
65
+ try:
66
+ jobs = self.job_manager.list_jobs()
67
+ jobs_data = []
68
+ for job in sorted(jobs, key=lambda j: j.start_time, reverse=True):
69
+ job_dict = asdict(job)
70
+ job_dict["status"] = job.status.value
71
+ job_dict["start_time"] = job.start_time.isoformat()
72
+ if job.end_time:
73
+ job_dict["end_time"] = job.end_time.isoformat()
74
+ jobs_data.append(job_dict)
75
+ return {"success": True, "jobs": jobs_data, "total_jobs": len(jobs_data)}
76
+ except Exception as e:
77
+ return {"error": f"Failed to list jobs: {str(e)}"}
78
+
79
+ def execute_cypher_query_tool(self, cypher_query: str) -> Dict[str, Any]:
80
+ """Tool to execute a read-only Cypher query."""
81
+ if not cypher_query:
82
+ return {"error": "Cypher query cannot be empty."}
83
+
84
+ import re as _re
85
+ forbidden_keywords = ['CREATE', 'MERGE', 'DELETE', 'SET', 'REMOVE', 'DROP', 'CALL apoc']
86
+ string_literal_pattern = r'"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\''
87
+ query_without_strings = _re.sub(string_literal_pattern, '', cypher_query)
88
+ for keyword in forbidden_keywords:
89
+ if _re.search(r'\b' + keyword + r'\b', query_without_strings, _re.IGNORECASE):
90
+ return {"error": "This tool only supports read-only queries. Prohibited keywords like CREATE, MERGE, DELETE, SET, etc., are not allowed."}
91
+
92
+ try:
93
+ with self.db_manager.get_driver().session() as session:
94
+ result = session.run(cypher_query)
95
+ records = [record.data() for record in result]
96
+ return {
97
+ "success": True,
98
+ "query": cypher_query,
99
+ "record_count": len(records),
100
+ "results": records
101
+ }
102
+ except CypherSyntaxError as e:
103
+ return {"error": "Cypher syntax error.", "details": str(e)}
104
+ except Exception as e:
105
+ return {"error": "An unexpected error occurred.", "details": str(e)}
106
+
107
+ def find_dead_code_tool(self) -> Dict[str, Any]:
108
+ """Finds potentially unused functions (dead code)."""
109
+ # This logic was moved from CodeFinder to be a system diagnostic tool
110
+ try:
111
+ with self.db_manager.get_driver().session() as session:
112
+ result = session.run("""
113
+ MATCH (func:Function)
114
+ WHERE func.is_dependency = false
115
+ AND NOT func.name STARTS WITH '_'
116
+ AND NOT func.name IN ['main', 'setup', 'run']
117
+ OPTIONAL MATCH (caller:Function)-[:CALLS]->(func)
118
+ WHERE caller.is_dependency = false
119
+ WITH func, count(caller) as caller_count
120
+ WHERE caller_count = 0
121
+ RETURN func.name as function_name, func.path as path, func.line_number as line_number
122
+ ORDER BY func.path, func.line_number
123
+ LIMIT 50
124
+ """)
125
+ return {
126
+ "success": True,
127
+ "results": {
128
+ "potentially_unused_functions": [dict(record) for record in result],
129
+ "note": "These functions might be entry points or called dynamically."
130
+ }
131
+ }
132
+ except Exception as e:
133
+ return {"error": f"Failed to find dead code: {str(e)}"}
@@ -0,0 +1,103 @@
1
+ """Tree-sitter parser dispatch by language name."""
2
+
3
+ from pathlib import Path
4
+ from typing import Dict, Optional
5
+
6
+ from tree_sitter import Language, Parser
7
+
8
+ from ..utils.tree_sitter_manager import get_tree_sitter_manager
9
+
10
+
11
+ class TreeSitterParser:
12
+ """A generic parser wrapper for a specific language using tree-sitter."""
13
+
14
+ def __init__(self, language_name: str):
15
+ self.language_name = language_name
16
+ self.ts_manager = get_tree_sitter_manager()
17
+
18
+ self.language: Language = self.ts_manager.get_language_safe(language_name)
19
+ self.parser = Parser(self.language)
20
+
21
+ self.language_specific_parser = None
22
+ if self.language_name == "python":
23
+ from .languages.python import PythonTreeSitterParser
24
+
25
+ self.language_specific_parser = PythonTreeSitterParser(self)
26
+ elif self.language_name == "javascript":
27
+ from .languages.javascript import JavascriptTreeSitterParser
28
+
29
+ self.language_specific_parser = JavascriptTreeSitterParser(self)
30
+ elif self.language_name == "go":
31
+ from .languages.go import GoTreeSitterParser
32
+
33
+ self.language_specific_parser = GoTreeSitterParser(self)
34
+ elif self.language_name == "typescript":
35
+ from .languages.typescript import TypescriptTreeSitterParser
36
+
37
+ self.language_specific_parser = TypescriptTreeSitterParser(self)
38
+ elif self.language_name == "tsx":
39
+ from .languages.typescriptjsx import TypescriptJSXTreeSitterParser
40
+
41
+ self.language_specific_parser = TypescriptJSXTreeSitterParser(self)
42
+ elif self.language_name == "cpp":
43
+ from .languages.cpp import CppTreeSitterParser
44
+
45
+ self.language_specific_parser = CppTreeSitterParser(self)
46
+ elif self.language_name == "rust":
47
+ from .languages.rust import RustTreeSitterParser
48
+
49
+ self.language_specific_parser = RustTreeSitterParser(self)
50
+ elif self.language_name == "c":
51
+ from .languages.c import CTreeSitterParser
52
+
53
+ self.language_specific_parser = CTreeSitterParser(self)
54
+ elif self.language_name == "java":
55
+ from .languages.java import JavaTreeSitterParser
56
+
57
+ self.language_specific_parser = JavaTreeSitterParser(self)
58
+ elif self.language_name == "ruby":
59
+ from .languages.ruby import RubyTreeSitterParser
60
+
61
+ self.language_specific_parser = RubyTreeSitterParser(self)
62
+ elif self.language_name == "c_sharp":
63
+ from .languages.csharp import CSharpTreeSitterParser
64
+
65
+ self.language_specific_parser = CSharpTreeSitterParser(self)
66
+ elif self.language_name == "php":
67
+ from .languages.php import PhpTreeSitterParser
68
+
69
+ self.language_specific_parser = PhpTreeSitterParser(self)
70
+ elif self.language_name == "kotlin":
71
+ from .languages.kotlin import KotlinTreeSitterParser
72
+
73
+ self.language_specific_parser = KotlinTreeSitterParser(self)
74
+ elif self.language_name == "scala":
75
+ from .languages.scala import ScalaTreeSitterParser
76
+
77
+ self.language_specific_parser = ScalaTreeSitterParser(self)
78
+ elif self.language_name == "swift":
79
+ from .languages.swift import SwiftTreeSitterParser
80
+
81
+ self.language_specific_parser = SwiftTreeSitterParser(self)
82
+ elif self.language_name == "haskell":
83
+ from .languages.haskell import HaskellTreeSitterParser
84
+
85
+ self.language_specific_parser = HaskellTreeSitterParser(self)
86
+ elif self.language_name == "dart":
87
+ from .languages.dart import DartTreeSitterParser
88
+
89
+ self.language_specific_parser = DartTreeSitterParser(self)
90
+ elif self.language_name == "perl":
91
+ from .languages.perl import PerlTreeSitterParser
92
+
93
+ self.language_specific_parser = PerlTreeSitterParser(self)
94
+ elif self.language_name == "elixir":
95
+ from .languages.elixir import ElixirTreeSitterParser
96
+
97
+ self.language_specific_parser = ElixirTreeSitterParser(self)
98
+
99
+ def parse(self, path: Path, is_dependency: bool = False, **kwargs) -> Dict:
100
+ """Dispatches parsing to the language-specific parser."""
101
+ if self.language_specific_parser:
102
+ return self.language_specific_parser.parse(path, is_dependency, **kwargs)
103
+ raise NotImplementedError(f"No language-specific parser implemented for {self.language_name}")
@@ -0,0 +1,90 @@
1
+ import os
2
+ from datetime import datetime
3
+ import logging
4
+ from pathlib import Path
5
+ logger = logging.getLogger(__name__)
6
+
7
+ # Log level mapping
8
+ LOG_LEVELS = {
9
+ 'DEBUG': logging.DEBUG,
10
+ 'INFO': logging.INFO,
11
+ 'WARNING': logging.WARNING,
12
+ 'ERROR': logging.ERROR,
13
+ 'CRITICAL': logging.CRITICAL,
14
+ 'DISABLED': logging.CRITICAL + 10 # Higher than CRITICAL to disable all
15
+ }
16
+
17
+ def _get_config_value(key, default):
18
+ """Helper to get config value with fallback"""
19
+ try:
20
+ from codegraphcontext.cli.config_manager import get_config_value
21
+ value = get_config_value(key)
22
+ if value is None:
23
+ return default
24
+ # Convert string boolean to actual boolean
25
+ if isinstance(value, str):
26
+ if value.lower() in ('true', 'false'):
27
+ return value.lower() == 'true'
28
+ return value
29
+ except Exception:
30
+ return default
31
+
32
+ def _should_log(level_name):
33
+ """Check if a message at the given level should be logged"""
34
+ configured_level = _get_config_value('ENABLE_APP_LOGS', 'INFO')
35
+
36
+ # Handle legacy boolean values
37
+ if isinstance(configured_level, bool):
38
+ return configured_level
39
+
40
+ # Convert to uppercase for comparison
41
+ configured_level = str(configured_level).upper()
42
+
43
+ # If disabled, don't log anything
44
+ if configured_level == 'DISABLED':
45
+ return False
46
+
47
+ # Get numeric levels
48
+ configured_numeric = LOG_LEVELS.get(configured_level, logging.INFO)
49
+ message_numeric = LOG_LEVELS.get(level_name.upper(), logging.INFO)
50
+
51
+ # Log if message level >= configured level
52
+ return message_numeric >= configured_numeric
53
+
54
+ def debug_log(message):
55
+ """Write debug message to a file if DEBUG_LOGS is enabled"""
56
+ # Check if debug logging is enabled via config
57
+ debug_mode = _get_config_value('DEBUG_LOGS', False)
58
+ if not debug_mode:
59
+ return
60
+
61
+ # Get debug log path from config
62
+ debug_file = _get_config_value('DEBUG_LOG_PATH', os.path.expanduser("~/mcp_debug.log"))
63
+
64
+ # Ensure parent directory exists
65
+ Path(debug_file).parent.mkdir(parents=True, exist_ok=True)
66
+
67
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
68
+ with open(debug_file, "a") as f:
69
+ f.write(f"[{timestamp}] {message}\n")
70
+ f.flush()
71
+
72
+ def info_logger(msg):
73
+ """Log info message if log level allows"""
74
+ if _should_log('INFO'):
75
+ return logger.info(msg)
76
+
77
+ def error_logger(msg):
78
+ """Log error message if log level allows"""
79
+ if _should_log('ERROR'):
80
+ return logger.error(msg)
81
+
82
+ def warning_logger(msg):
83
+ """Log warning message if log level allows"""
84
+ if _should_log('WARNING'):
85
+ return logger.warning(msg)
86
+
87
+ def debug_logger(msg):
88
+ """Log debug message if log level allows"""
89
+ if _should_log('DEBUG'):
90
+ return logger.debug(msg)
@@ -0,0 +1,55 @@
1
+ """
2
+ Path segment helpers aligned with IGNORE_DIRS indexing rules.
3
+
4
+ `is_dependency` on graph nodes marks separately indexed packages, not vendored
5
+ dirs inside a repo (e.g. node_modules). Analysis queries must also exclude
6
+ those paths when IGNORE_DIRS would skip them during indexing.
7
+ """
8
+
9
+ from pathlib import Path
10
+ from typing import List, Optional
11
+
12
+ from ..cli.config_manager import DEFAULT_CONFIG, get_config_value
13
+
14
+
15
+ def parse_ignore_dir_names() -> List[str]:
16
+ """Directory names from IGNORE_DIRS, falling back to defaults when unset or empty."""
17
+ raw = (get_config_value("IGNORE_DIRS") or "").strip()
18
+ if not raw:
19
+ raw = DEFAULT_CONFIG["IGNORE_DIRS"]
20
+ return [d.strip() for d in raw.split(",") if d.strip()]
21
+
22
+
23
+ def cypher_path_not_under_ignore_dirs(path_var: str, ignore_dir_names: Optional[List[str]] = None) -> str:
24
+ """
25
+ Returns a Cypher fragment that keeps only paths not under any IGNORE_DIRS segment
26
+ (Unix `/name/` or Windows `\\name\\`).
27
+ """
28
+ names = ignore_dir_names if ignore_dir_names is not None else parse_ignore_dir_names()
29
+ if not names:
30
+ return ""
31
+ parts: List[str] = []
32
+ for d in names:
33
+ esc = d.replace("\\", "\\\\").replace("'", "\\'")
34
+ parts.append(f"{path_var} CONTAINS '/{esc}/'")
35
+ parts.append(f"{path_var} CONTAINS '\\\\{esc}\\\\'")
36
+ return " AND NOT (" + " OR ".join(parts) + ")"
37
+
38
+
39
+ def file_path_has_ignore_dir_segment(file_path: Path, index_root: Path) -> bool:
40
+ """
41
+ True if any directory segment of file_path relative to index_root matches
42
+ IGNORE_DIRS (same logic as Tree-sitter indexing).
43
+ """
44
+ ignore_dirs_str = (get_config_value("IGNORE_DIRS") or "").strip()
45
+ if not ignore_dirs_str:
46
+ ignore_dirs_str = DEFAULT_CONFIG["IGNORE_DIRS"]
47
+ ignore_dirs = {d.strip().lower() for d in ignore_dirs_str.split(",") if d.strip()}
48
+ if not ignore_dirs:
49
+ return False
50
+ try:
51
+ rel = file_path.resolve().relative_to(index_root.resolve())
52
+ parts = {p.lower() for p in rel.parent.parts}
53
+ return bool(parts.intersection(ignore_dirs))
54
+ except ValueError:
55
+ return False
@@ -0,0 +1,27 @@
1
+ """Helpers for comparing indexed repository records to filesystem paths.
2
+
3
+ Rows with *no* path are skipped so the CLI never raises on ``Path(None)``. That is
4
+ intentional: corrupted graph data should not block indexing. When the graph is
5
+ queried, ``CodeFinder.list_indexed_repositories`` emits a ``logging`` warning if
6
+ any ``Repository`` row has a missing path, so the issue is visible in normal CLI
7
+ runs (see application logging configuration in ``cli.main``).
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from pathlib import Path
12
+ from typing import Any, Dict, Iterable
13
+
14
+
15
+ def repo_record_matches_path(repo: Dict[str, Any], path_obj: Path) -> bool:
16
+ """True if *repo* has a resolvable path equal to *path_obj* (resolved)."""
17
+ raw = repo.get("path")
18
+ if raw is None or raw == "":
19
+ return False
20
+ try:
21
+ return Path(raw).resolve() == path_obj
22
+ except (TypeError, OSError, ValueError):
23
+ return False
24
+
25
+
26
+ def any_repo_matches_path(repos: Iterable[Dict[str, Any]], path_obj: Path) -> bool:
27
+ return any(repo_record_matches_path(r, path_obj) for r in repos)