codexa 0.4.0__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 (189) hide show
  1. codexa-0.4.0.dist-info/METADATA +650 -0
  2. codexa-0.4.0.dist-info/RECORD +189 -0
  3. codexa-0.4.0.dist-info/WHEEL +5 -0
  4. codexa-0.4.0.dist-info/entry_points.txt +2 -0
  5. codexa-0.4.0.dist-info/licenses/LICENSE +21 -0
  6. codexa-0.4.0.dist-info/top_level.txt +1 -0
  7. semantic_code_intelligence/__init__.py +5 -0
  8. semantic_code_intelligence/analysis/__init__.py +21 -0
  9. semantic_code_intelligence/analysis/ai_features.py +351 -0
  10. semantic_code_intelligence/bridge/__init__.py +28 -0
  11. semantic_code_intelligence/bridge/context_provider.py +245 -0
  12. semantic_code_intelligence/bridge/protocol.py +167 -0
  13. semantic_code_intelligence/bridge/server.py +348 -0
  14. semantic_code_intelligence/bridge/vscode.py +271 -0
  15. semantic_code_intelligence/ci/__init__.py +13 -0
  16. semantic_code_intelligence/ci/hooks.py +98 -0
  17. semantic_code_intelligence/ci/hotspots.py +272 -0
  18. semantic_code_intelligence/ci/impact.py +246 -0
  19. semantic_code_intelligence/ci/metrics.py +591 -0
  20. semantic_code_intelligence/ci/pr.py +412 -0
  21. semantic_code_intelligence/ci/quality.py +557 -0
  22. semantic_code_intelligence/ci/templates.py +164 -0
  23. semantic_code_intelligence/ci/trace.py +224 -0
  24. semantic_code_intelligence/cli/__init__.py +0 -0
  25. semantic_code_intelligence/cli/commands/__init__.py +0 -0
  26. semantic_code_intelligence/cli/commands/ask_cmd.py +153 -0
  27. semantic_code_intelligence/cli/commands/benchmark_cmd.py +303 -0
  28. semantic_code_intelligence/cli/commands/chat_cmd.py +252 -0
  29. semantic_code_intelligence/cli/commands/ci_gen_cmd.py +74 -0
  30. semantic_code_intelligence/cli/commands/context_cmd.py +120 -0
  31. semantic_code_intelligence/cli/commands/cross_refactor_cmd.py +113 -0
  32. semantic_code_intelligence/cli/commands/deps_cmd.py +91 -0
  33. semantic_code_intelligence/cli/commands/docs_cmd.py +101 -0
  34. semantic_code_intelligence/cli/commands/doctor_cmd.py +147 -0
  35. semantic_code_intelligence/cli/commands/evolve_cmd.py +171 -0
  36. semantic_code_intelligence/cli/commands/explain_cmd.py +112 -0
  37. semantic_code_intelligence/cli/commands/gate_cmd.py +135 -0
  38. semantic_code_intelligence/cli/commands/grep_cmd.py +234 -0
  39. semantic_code_intelligence/cli/commands/hotspots_cmd.py +119 -0
  40. semantic_code_intelligence/cli/commands/impact_cmd.py +131 -0
  41. semantic_code_intelligence/cli/commands/index_cmd.py +138 -0
  42. semantic_code_intelligence/cli/commands/init_cmd.py +152 -0
  43. semantic_code_intelligence/cli/commands/investigate_cmd.py +163 -0
  44. semantic_code_intelligence/cli/commands/languages_cmd.py +101 -0
  45. semantic_code_intelligence/cli/commands/lsp_cmd.py +49 -0
  46. semantic_code_intelligence/cli/commands/mcp_cmd.py +50 -0
  47. semantic_code_intelligence/cli/commands/metrics_cmd.py +264 -0
  48. semantic_code_intelligence/cli/commands/models_cmd.py +157 -0
  49. semantic_code_intelligence/cli/commands/plugin_cmd.py +275 -0
  50. semantic_code_intelligence/cli/commands/pr_summary_cmd.py +178 -0
  51. semantic_code_intelligence/cli/commands/quality_cmd.py +208 -0
  52. semantic_code_intelligence/cli/commands/refactor_cmd.py +103 -0
  53. semantic_code_intelligence/cli/commands/review_cmd.py +88 -0
  54. semantic_code_intelligence/cli/commands/search_cmd.py +236 -0
  55. semantic_code_intelligence/cli/commands/serve_cmd.py +117 -0
  56. semantic_code_intelligence/cli/commands/suggest_cmd.py +100 -0
  57. semantic_code_intelligence/cli/commands/summary_cmd.py +78 -0
  58. semantic_code_intelligence/cli/commands/tool_cmd.py +282 -0
  59. semantic_code_intelligence/cli/commands/trace_cmd.py +123 -0
  60. semantic_code_intelligence/cli/commands/tui_cmd.py +58 -0
  61. semantic_code_intelligence/cli/commands/viz_cmd.py +127 -0
  62. semantic_code_intelligence/cli/commands/watch_cmd.py +72 -0
  63. semantic_code_intelligence/cli/commands/web_cmd.py +61 -0
  64. semantic_code_intelligence/cli/commands/workspace_cmd.py +250 -0
  65. semantic_code_intelligence/cli/main.py +65 -0
  66. semantic_code_intelligence/cli/router.py +92 -0
  67. semantic_code_intelligence/config/__init__.py +0 -0
  68. semantic_code_intelligence/config/settings.py +260 -0
  69. semantic_code_intelligence/context/__init__.py +19 -0
  70. semantic_code_intelligence/context/engine.py +429 -0
  71. semantic_code_intelligence/context/memory.py +253 -0
  72. semantic_code_intelligence/daemon/__init__.py +1 -0
  73. semantic_code_intelligence/daemon/watcher.py +515 -0
  74. semantic_code_intelligence/docs/__init__.py +1080 -0
  75. semantic_code_intelligence/embeddings/__init__.py +0 -0
  76. semantic_code_intelligence/embeddings/enhanced.py +131 -0
  77. semantic_code_intelligence/embeddings/generator.py +149 -0
  78. semantic_code_intelligence/embeddings/model_registry.py +100 -0
  79. semantic_code_intelligence/evolution/__init__.py +1 -0
  80. semantic_code_intelligence/evolution/budget_guard.py +111 -0
  81. semantic_code_intelligence/evolution/commit_manager.py +88 -0
  82. semantic_code_intelligence/evolution/context_builder.py +131 -0
  83. semantic_code_intelligence/evolution/engine.py +249 -0
  84. semantic_code_intelligence/evolution/patch_generator.py +229 -0
  85. semantic_code_intelligence/evolution/task_selector.py +214 -0
  86. semantic_code_intelligence/evolution/test_runner.py +111 -0
  87. semantic_code_intelligence/indexing/__init__.py +0 -0
  88. semantic_code_intelligence/indexing/chunker.py +174 -0
  89. semantic_code_intelligence/indexing/parallel.py +86 -0
  90. semantic_code_intelligence/indexing/scanner.py +146 -0
  91. semantic_code_intelligence/indexing/semantic_chunker.py +337 -0
  92. semantic_code_intelligence/llm/__init__.py +62 -0
  93. semantic_code_intelligence/llm/cache.py +219 -0
  94. semantic_code_intelligence/llm/cached_provider.py +145 -0
  95. semantic_code_intelligence/llm/conversation.py +190 -0
  96. semantic_code_intelligence/llm/cross_refactor.py +272 -0
  97. semantic_code_intelligence/llm/investigation.py +274 -0
  98. semantic_code_intelligence/llm/mock_provider.py +77 -0
  99. semantic_code_intelligence/llm/ollama_provider.py +122 -0
  100. semantic_code_intelligence/llm/openai_provider.py +100 -0
  101. semantic_code_intelligence/llm/provider.py +92 -0
  102. semantic_code_intelligence/llm/rate_limiter.py +164 -0
  103. semantic_code_intelligence/llm/reasoning.py +438 -0
  104. semantic_code_intelligence/llm/safety.py +110 -0
  105. semantic_code_intelligence/llm/streaming.py +251 -0
  106. semantic_code_intelligence/lsp/__init__.py +609 -0
  107. semantic_code_intelligence/mcp/__init__.py +393 -0
  108. semantic_code_intelligence/parsing/__init__.py +19 -0
  109. semantic_code_intelligence/parsing/parser.py +375 -0
  110. semantic_code_intelligence/plugins/__init__.py +255 -0
  111. semantic_code_intelligence/plugins/examples/__init__.py +1 -0
  112. semantic_code_intelligence/plugins/examples/code_quality.py +73 -0
  113. semantic_code_intelligence/plugins/examples/search_annotator.py +56 -0
  114. semantic_code_intelligence/scalability/__init__.py +205 -0
  115. semantic_code_intelligence/search/__init__.py +0 -0
  116. semantic_code_intelligence/search/formatter.py +123 -0
  117. semantic_code_intelligence/search/grep.py +361 -0
  118. semantic_code_intelligence/search/hybrid_search.py +170 -0
  119. semantic_code_intelligence/search/keyword_search.py +311 -0
  120. semantic_code_intelligence/search/section_expander.py +103 -0
  121. semantic_code_intelligence/services/__init__.py +0 -0
  122. semantic_code_intelligence/services/indexing_service.py +630 -0
  123. semantic_code_intelligence/services/search_service.py +269 -0
  124. semantic_code_intelligence/storage/__init__.py +0 -0
  125. semantic_code_intelligence/storage/chunk_hash_store.py +86 -0
  126. semantic_code_intelligence/storage/hash_store.py +66 -0
  127. semantic_code_intelligence/storage/index_manifest.py +85 -0
  128. semantic_code_intelligence/storage/index_stats.py +138 -0
  129. semantic_code_intelligence/storage/query_history.py +160 -0
  130. semantic_code_intelligence/storage/symbol_registry.py +209 -0
  131. semantic_code_intelligence/storage/vector_store.py +297 -0
  132. semantic_code_intelligence/tests/__init__.py +0 -0
  133. semantic_code_intelligence/tests/test_ai_features.py +351 -0
  134. semantic_code_intelligence/tests/test_chunker.py +119 -0
  135. semantic_code_intelligence/tests/test_cli.py +188 -0
  136. semantic_code_intelligence/tests/test_config.py +154 -0
  137. semantic_code_intelligence/tests/test_context.py +381 -0
  138. semantic_code_intelligence/tests/test_embeddings.py +73 -0
  139. semantic_code_intelligence/tests/test_endtoend.py +1142 -0
  140. semantic_code_intelligence/tests/test_enhanced_embeddings.py +92 -0
  141. semantic_code_intelligence/tests/test_hash_store.py +79 -0
  142. semantic_code_intelligence/tests/test_logging.py +55 -0
  143. semantic_code_intelligence/tests/test_new_cli.py +138 -0
  144. semantic_code_intelligence/tests/test_parser.py +495 -0
  145. semantic_code_intelligence/tests/test_phase10.py +355 -0
  146. semantic_code_intelligence/tests/test_phase11.py +593 -0
  147. semantic_code_intelligence/tests/test_phase12.py +375 -0
  148. semantic_code_intelligence/tests/test_phase13.py +663 -0
  149. semantic_code_intelligence/tests/test_phase14.py +568 -0
  150. semantic_code_intelligence/tests/test_phase15.py +814 -0
  151. semantic_code_intelligence/tests/test_phase16.py +792 -0
  152. semantic_code_intelligence/tests/test_phase17.py +815 -0
  153. semantic_code_intelligence/tests/test_phase18.py +934 -0
  154. semantic_code_intelligence/tests/test_phase19.py +986 -0
  155. semantic_code_intelligence/tests/test_phase20.py +2753 -0
  156. semantic_code_intelligence/tests/test_phase20b.py +2058 -0
  157. semantic_code_intelligence/tests/test_phase20c.py +962 -0
  158. semantic_code_intelligence/tests/test_phase21.py +428 -0
  159. semantic_code_intelligence/tests/test_phase22.py +799 -0
  160. semantic_code_intelligence/tests/test_phase23.py +783 -0
  161. semantic_code_intelligence/tests/test_phase24.py +715 -0
  162. semantic_code_intelligence/tests/test_phase25.py +496 -0
  163. semantic_code_intelligence/tests/test_phase26.py +251 -0
  164. semantic_code_intelligence/tests/test_phase27.py +531 -0
  165. semantic_code_intelligence/tests/test_phase8.py +592 -0
  166. semantic_code_intelligence/tests/test_phase9.py +643 -0
  167. semantic_code_intelligence/tests/test_plugins.py +293 -0
  168. semantic_code_intelligence/tests/test_priority_features.py +727 -0
  169. semantic_code_intelligence/tests/test_router.py +41 -0
  170. semantic_code_intelligence/tests/test_scalability.py +138 -0
  171. semantic_code_intelligence/tests/test_scanner.py +125 -0
  172. semantic_code_intelligence/tests/test_search.py +160 -0
  173. semantic_code_intelligence/tests/test_semantic_chunker.py +255 -0
  174. semantic_code_intelligence/tests/test_tools.py +182 -0
  175. semantic_code_intelligence/tests/test_vector_store.py +151 -0
  176. semantic_code_intelligence/tests/test_watcher.py +211 -0
  177. semantic_code_intelligence/tools/__init__.py +442 -0
  178. semantic_code_intelligence/tools/executor.py +232 -0
  179. semantic_code_intelligence/tools/protocol.py +200 -0
  180. semantic_code_intelligence/tui/__init__.py +454 -0
  181. semantic_code_intelligence/utils/__init__.py +0 -0
  182. semantic_code_intelligence/utils/logging.py +112 -0
  183. semantic_code_intelligence/version.py +3 -0
  184. semantic_code_intelligence/web/__init__.py +11 -0
  185. semantic_code_intelligence/web/api.py +289 -0
  186. semantic_code_intelligence/web/server.py +397 -0
  187. semantic_code_intelligence/web/ui.py +659 -0
  188. semantic_code_intelligence/web/visualize.py +226 -0
  189. semantic_code_intelligence/workspace/__init__.py +427 -0
@@ -0,0 +1,164 @@
1
+ """GitHub Actions workflow template generation.
2
+
3
+ Generates YAML workflow files that integrate CodexA analysis into
4
+ CI pipelines. Templates are plain strings — no YAML library required.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any, Callable
10
+
11
+
12
+ def generate_analysis_workflow(
13
+ *,
14
+ python_version: str = "3.12",
15
+ trigger: str = "pull_request",
16
+ ) -> str:
17
+ """Generate a GitHub Actions workflow for CodexA analysis on PRs.
18
+
19
+ The workflow:
20
+ 1. Installs CodexA and dependencies.
21
+ 2. Runs ``codexa quality`` on the repository.
22
+ 3. Runs ``codexa pr-summary`` on changed files.
23
+ 4. Posts a summary comment (advisory only).
24
+ """
25
+ return f"""\
26
+ # CodexA Analysis Workflow
27
+ # Auto-generated by `codexa ci-gen`
28
+ # This workflow runs CodexA quality and PR analysis on pull requests.
29
+ # All outputs are advisory — CodexA never modifies repository code.
30
+
31
+ name: CodexA Analysis
32
+
33
+ on:
34
+ {trigger}:
35
+ branches: [main]
36
+
37
+ permissions:
38
+ contents: read
39
+ pull-requests: read
40
+
41
+ jobs:
42
+ analyze:
43
+ runs-on: ubuntu-latest
44
+ steps:
45
+ - uses: actions/checkout@v4
46
+ with:
47
+ fetch-depth: 0
48
+
49
+ - name: Set up Python {python_version}
50
+ uses: actions/setup-python@v5
51
+ with:
52
+ python-version: "{python_version}"
53
+
54
+ - name: Install dependencies
55
+ run: |
56
+ python -m pip install --upgrade pip setuptools wheel
57
+ pip install -r requirements.txt
58
+
59
+ - name: CodexA — Initialize
60
+ run: codexa init
61
+
62
+ - name: CodexA — Quality Report
63
+ run: codexa quality --json > quality-report.json
64
+ continue-on-error: true
65
+
66
+ - name: CodexA — PR Summary
67
+ run: codexa pr-summary --json > pr-report.json
68
+ continue-on-error: true
69
+
70
+ - name: Upload Reports
71
+ uses: actions/upload-artifact@v4
72
+ with:
73
+ name: codexa-reports
74
+ path: |
75
+ quality-report.json
76
+ pr-report.json
77
+ """
78
+
79
+
80
+ def generate_precommit_config() -> str:
81
+ """Generate a ``.pre-commit-config.yaml`` snippet for CodexA hooks.
82
+
83
+ Uses the ``local`` repo type so no external hook package is needed.
84
+ """
85
+ return """\
86
+ # CodexA pre-commit hooks
87
+ # Add this to your .pre-commit-config.yaml
88
+
89
+ repos:
90
+ - repo: local
91
+ hooks:
92
+ - id: codexa-safety
93
+ name: CodexA Safety Check
94
+ entry: codexa quality --safety-only --pipe
95
+ language: system
96
+ types: [python]
97
+ pass_filenames: false
98
+
99
+ - id: codexa-quality
100
+ name: CodexA Quality Check
101
+ entry: codexa quality --pipe
102
+ language: system
103
+ types: [python]
104
+ pass_filenames: false
105
+ """
106
+
107
+
108
+ def generate_safety_workflow(
109
+ *,
110
+ python_version: str = "3.12",
111
+ ) -> str:
112
+ """Generate a lightweight safety-only workflow for fast PR checks."""
113
+ return f"""\
114
+ # CodexA Safety Check
115
+ # Lightweight workflow — validates changed code for security patterns.
116
+
117
+ name: CodexA Safety
118
+
119
+ on:
120
+ pull_request:
121
+ branches: [main]
122
+
123
+ permissions:
124
+ contents: read
125
+
126
+ jobs:
127
+ safety:
128
+ runs-on: ubuntu-latest
129
+ steps:
130
+ - uses: actions/checkout@v4
131
+
132
+ - name: Set up Python {python_version}
133
+ uses: actions/setup-python@v5
134
+ with:
135
+ python-version: "{python_version}"
136
+
137
+ - name: Install dependencies
138
+ run: |
139
+ python -m pip install --upgrade pip setuptools wheel
140
+ pip install -r requirements.txt
141
+
142
+ - name: CodexA — Safety Validation
143
+ run: codexa quality --safety-only --pipe
144
+ """
145
+
146
+
147
+ TEMPLATES: dict[str, str] = {
148
+ "analysis": "Full analysis workflow (quality + PR summary)",
149
+ "safety": "Lightweight safety-only workflow",
150
+ "precommit": "Pre-commit hook configuration",
151
+ }
152
+
153
+
154
+ def get_template(name: str, **kwargs: Any) -> str:
155
+ """Get a named template. Raises ``KeyError`` for unknown names."""
156
+ generators: dict[str, Callable[..., str]] = {
157
+ "analysis": generate_analysis_workflow,
158
+ "safety": generate_safety_workflow,
159
+ "precommit": generate_precommit_config,
160
+ }
161
+ gen = generators.get(name)
162
+ if gen is None:
163
+ raise KeyError(f"Unknown template: {name!r}. Available: {', '.join(generators)}")
164
+ return gen(**kwargs)
@@ -0,0 +1,224 @@
1
+ """Symbol trace tool — traces execution relationships between symbols.
2
+
3
+ Builds upstream callers and downstream callees for a given symbol,
4
+ with transitive traversal and cross-file relationship tracking.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections import deque
10
+ from dataclasses import dataclass, field
11
+ from typing import Any
12
+
13
+ from semantic_code_intelligence.context.engine import CallGraph
14
+ from semantic_code_intelligence.parsing.parser import Symbol
15
+ from semantic_code_intelligence.utils.logging import get_logger
16
+
17
+ logger = get_logger("ci.trace")
18
+
19
+
20
+ @dataclass
21
+ class TraceNode:
22
+ """A node in a symbol trace — a caller or callee."""
23
+
24
+ name: str
25
+ file_path: str
26
+ kind: str # "function", "method", "class"
27
+ depth: int # hops from source; negative=upstream, positive=downstream
28
+
29
+ def to_dict(self) -> dict[str, Any]:
30
+ return {
31
+ "name": self.name,
32
+ "file_path": self.file_path,
33
+ "kind": self.kind,
34
+ "depth": self.depth,
35
+ }
36
+
37
+
38
+ @dataclass
39
+ class TraceEdge:
40
+ """A directed edge in the trace graph."""
41
+
42
+ caller: str
43
+ callee: str
44
+ file_path: str
45
+
46
+ def to_dict(self) -> dict[str, Any]:
47
+ return {
48
+ "caller": self.caller,
49
+ "callee": self.callee,
50
+ "file_path": self.file_path,
51
+ }
52
+
53
+
54
+ @dataclass
55
+ class TraceResult:
56
+ """Result of tracing a symbol's execution relationships."""
57
+
58
+ target: str
59
+ target_file: str
60
+ upstream: list[TraceNode] = field(default_factory=list)
61
+ downstream: list[TraceNode] = field(default_factory=list)
62
+ edges: list[TraceEdge] = field(default_factory=list)
63
+ max_upstream_depth: int = 0
64
+ max_downstream_depth: int = 0
65
+
66
+ @property
67
+ def total_nodes(self) -> int:
68
+ return len(self.upstream) + len(self.downstream)
69
+
70
+ def to_dict(self) -> dict[str, Any]:
71
+ return {
72
+ "target": self.target,
73
+ "target_file": self.target_file,
74
+ "upstream": [n.to_dict() for n in self.upstream],
75
+ "downstream": [n.to_dict() for n in self.downstream],
76
+ "edges": [e.to_dict() for e in self.edges],
77
+ "max_upstream_depth": self.max_upstream_depth,
78
+ "max_downstream_depth": self.max_downstream_depth,
79
+ "total_nodes": self.total_nodes,
80
+ }
81
+
82
+
83
+ def trace_symbol(
84
+ target: str,
85
+ symbols: list[Symbol],
86
+ call_graph: CallGraph,
87
+ *,
88
+ max_depth: int = 5,
89
+ ) -> TraceResult:
90
+ """Trace execution relationships for a symbol.
91
+
92
+ Walks both upstream (callers) and downstream (callees) in the call graph.
93
+
94
+ Args:
95
+ target: Symbol name to trace.
96
+ symbols: All parsed symbols.
97
+ call_graph: Pre-built call graph.
98
+ max_depth: Maximum BFS traversal depth in each direction.
99
+
100
+ Returns:
101
+ TraceResult with upstream/downstream nodes and edges.
102
+ """
103
+ # Resolve target to a known symbol
104
+ target_syms = [s for s in symbols if s.name == target and s.kind != "import"]
105
+ if not target_syms:
106
+ return TraceResult(target=target, target_file="")
107
+
108
+ target_sym = target_syms[0]
109
+ target_key = f"{target_sym.file_path}:{target_sym.name}"
110
+
111
+ # Build name → Symbol index
112
+ sym_by_name: dict[str, Symbol] = {}
113
+ for s in symbols:
114
+ if s.kind != "import":
115
+ sym_by_name.setdefault(s.name, s)
116
+
117
+ # Reverse caller_key → Symbol
118
+ sym_by_key: dict[str, Symbol] = {}
119
+ for s in symbols:
120
+ if s.kind in ("function", "method", "class"):
121
+ sym_by_key[f"{s.file_path}:{s.name}"] = s
122
+
123
+ upstream: list[TraceNode] = []
124
+ downstream: list[TraceNode] = []
125
+ edges: list[TraceEdge] = []
126
+ max_up = 0
127
+ max_down = 0
128
+
129
+ # ── Upstream: who calls target? ──────────────────────────────
130
+ visited_up: set[str] = {target}
131
+ queue_up: deque[tuple[str, int]] = deque() # (symbol_name, depth)
132
+
133
+ for edge in call_graph.callers_of(target):
134
+ parts = edge.caller.rsplit(":", 1)
135
+ caller_name = parts[-1] if len(parts) == 2 else edge.caller
136
+ if caller_name not in visited_up:
137
+ visited_up.add(caller_name)
138
+ queue_up.append((caller_name, 1))
139
+ edges.append(TraceEdge(
140
+ caller=caller_name,
141
+ callee=target,
142
+ file_path=edge.file_path,
143
+ ))
144
+
145
+ while queue_up:
146
+ sym_name, depth = queue_up.popleft()
147
+ sym = sym_by_name.get(sym_name)
148
+ if sym is None:
149
+ continue
150
+
151
+ upstream.append(TraceNode(
152
+ name=sym.name,
153
+ file_path=sym.file_path,
154
+ kind=sym.kind,
155
+ depth=-depth, # negative = upstream
156
+ ))
157
+ max_up = max(max_up, depth)
158
+
159
+ if depth < max_depth:
160
+ for edge in call_graph.callers_of(sym.name):
161
+ parts = edge.caller.rsplit(":", 1)
162
+ cname = parts[-1] if len(parts) == 2 else edge.caller
163
+ if cname not in visited_up:
164
+ visited_up.add(cname)
165
+ queue_up.append((cname, depth + 1))
166
+ edges.append(TraceEdge(
167
+ caller=cname,
168
+ callee=sym.name,
169
+ file_path=edge.file_path,
170
+ ))
171
+
172
+ # ── Downstream: what does target call? ────────────────────────
173
+ visited_down: set[str] = {target_key}
174
+ queue_down: deque[tuple[str, str, int]] = deque() # (caller_key, symbol_name, depth)
175
+
176
+ for edge in call_graph.callees_of(target_key):
177
+ callee_sym = sym_by_name.get(edge.callee)
178
+ callee_key = f"{callee_sym.file_path}:{callee_sym.name}" if callee_sym else edge.callee
179
+ if callee_key not in visited_down:
180
+ visited_down.add(callee_key)
181
+ queue_down.append((callee_key, edge.callee, 1))
182
+ edges.append(TraceEdge(
183
+ caller=target,
184
+ callee=edge.callee,
185
+ file_path=edge.file_path,
186
+ ))
187
+
188
+ while queue_down:
189
+ caller_key, sym_name, depth = queue_down.popleft()
190
+ sym = sym_by_name.get(sym_name)
191
+ if sym is None:
192
+ continue
193
+
194
+ downstream.append(TraceNode(
195
+ name=sym.name,
196
+ file_path=sym.file_path,
197
+ kind=sym.kind,
198
+ depth=depth,
199
+ ))
200
+ max_down = max(max_down, depth)
201
+
202
+ if depth < max_depth:
203
+ key = f"{sym.file_path}:{sym.name}"
204
+ for edge in call_graph.callees_of(key):
205
+ callee_sym = sym_by_name.get(edge.callee)
206
+ callee_key = f"{callee_sym.file_path}:{callee_sym.name}" if callee_sym else edge.callee
207
+ if callee_key not in visited_down:
208
+ visited_down.add(callee_key)
209
+ queue_down.append((callee_key, edge.callee, depth + 1))
210
+ edges.append(TraceEdge(
211
+ caller=sym.name,
212
+ callee=edge.callee,
213
+ file_path=edge.file_path,
214
+ ))
215
+
216
+ return TraceResult(
217
+ target=target,
218
+ target_file=target_sym.file_path,
219
+ upstream=upstream,
220
+ downstream=downstream,
221
+ edges=edges,
222
+ max_upstream_depth=max_up,
223
+ max_downstream_depth=max_down,
224
+ )
File without changes
File without changes
@@ -0,0 +1,153 @@
1
+ """CLI command: ask — ask a natural-language question about the codebase."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json as json_mod
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ import click
10
+
11
+ from semantic_code_intelligence.config.settings import AppConfig, load_config
12
+ from semantic_code_intelligence.utils.logging import (
13
+ console,
14
+ get_logger,
15
+ print_error,
16
+ print_info,
17
+ )
18
+
19
+ if TYPE_CHECKING:
20
+ from semantic_code_intelligence.llm.provider import LLMProvider
21
+
22
+ logger = get_logger("cli.ask")
23
+
24
+
25
+ def _wrap_provider(provider: LLMProvider, llm: Any, config: Any) -> LLMProvider:
26
+ """Wrap a provider with caching and rate limiting based on config."""
27
+ from semantic_code_intelligence.llm.cache import LLMCache
28
+ from semantic_code_intelligence.llm.cached_provider import CachedProvider
29
+ from semantic_code_intelligence.llm.rate_limiter import RateLimiter
30
+
31
+ cache = None
32
+ if getattr(llm, "cache_enabled", False):
33
+ cache_dir = str(config.config_dir(config.project_root)) if hasattr(config, "config_dir") else None
34
+ cache = LLMCache(
35
+ cache_dir=cache_dir,
36
+ ttl_hours=getattr(llm, "cache_ttl_hours", 24),
37
+ max_entries=getattr(llm, "cache_max_entries", 1000),
38
+ )
39
+
40
+ rate_limiter = None
41
+ rpm = getattr(llm, "rate_limit_rpm", 0)
42
+ tpm = getattr(llm, "rate_limit_tpm", 0)
43
+ if rpm > 0 or tpm > 0:
44
+ rate_limiter = RateLimiter(rpm=rpm, tpm=tpm)
45
+
46
+ if cache is not None or rate_limiter is not None:
47
+ return CachedProvider(provider, cache=cache, rate_limiter=rate_limiter)
48
+ return provider
49
+
50
+
51
+ def _get_provider(config: Any) -> LLMProvider:
52
+ """Build an LLM provider from the app configuration."""
53
+ from semantic_code_intelligence.config.settings import LLMConfig
54
+
55
+ llm: LLMConfig = config.llm
56
+ if llm.provider == "openai":
57
+ from semantic_code_intelligence.llm.openai_provider import OpenAIProvider
58
+
59
+ provider: LLMProvider = OpenAIProvider(
60
+ api_key=llm.api_key,
61
+ model=llm.model,
62
+ base_url=llm.base_url or None,
63
+ temperature=llm.temperature,
64
+ max_tokens=llm.max_tokens,
65
+ )
66
+ elif llm.provider == "ollama":
67
+ from semantic_code_intelligence.llm.ollama_provider import OllamaProvider
68
+
69
+ provider = OllamaProvider(
70
+ model=llm.model,
71
+ base_url=llm.base_url or "http://localhost:11434",
72
+ temperature=llm.temperature,
73
+ max_tokens=llm.max_tokens,
74
+ )
75
+ else:
76
+ from semantic_code_intelligence.llm.mock_provider import MockProvider
77
+
78
+ provider = MockProvider()
79
+
80
+ return _wrap_provider(provider, llm, config)
81
+
82
+
83
+ @click.command("ask")
84
+ @click.argument("question", type=str)
85
+ @click.option(
86
+ "--top-k",
87
+ "-k",
88
+ default=5,
89
+ type=int,
90
+ help="Number of context snippets to retrieve.",
91
+ )
92
+ @click.option(
93
+ "--json-output",
94
+ "--json",
95
+ "json_mode",
96
+ is_flag=True,
97
+ default=False,
98
+ help="Output in JSON format.",
99
+ )
100
+ @click.option(
101
+ "--path",
102
+ "-p",
103
+ default=".",
104
+ type=click.Path(exists=True, file_okay=False, resolve_path=True),
105
+ help="Project root path.",
106
+ )
107
+ @click.pass_context
108
+ def ask_cmd(
109
+ ctx: click.Context,
110
+ question: str,
111
+ top_k: int,
112
+ json_mode: bool,
113
+ path: str,
114
+ ) -> None:
115
+ """Ask a natural-language question about the codebase.
116
+
117
+ Uses semantic search + LLM to answer questions about your code.
118
+
119
+ Examples:
120
+
121
+ codexa ask "How does authentication work?"
122
+
123
+ codexa ask "What does the search_codebase function do?" --json
124
+ """
125
+ root = Path(path).resolve()
126
+ config_dir = AppConfig.config_dir(root)
127
+
128
+ if not config_dir.exists():
129
+ print_error(f"Project not initialized at {root}. Run 'codexa init' first.")
130
+ ctx.exit(1)
131
+ return
132
+
133
+ config = load_config(root)
134
+ provider = _get_provider(config)
135
+
136
+ from semantic_code_intelligence.llm.reasoning import ReasoningEngine
137
+
138
+ try:
139
+ engine = ReasoningEngine(provider, root)
140
+ result = engine.ask(question, top_k=top_k)
141
+ except Exception as exc:
142
+ logger.debug("Ask failed", exc_info=True)
143
+ print_error(f"Failed to answer question: {exc}")
144
+ ctx.exit(1)
145
+ return
146
+
147
+ if json_mode:
148
+ click.echo(json_mod.dumps(result.to_dict(), indent=2))
149
+ else:
150
+ console.print(f"\n[bold cyan]Question:[/bold cyan] {result.question}\n")
151
+ console.print(f"[bold green]Answer:[/bold green]\n{result.answer}\n")
152
+ if result.context_snippets:
153
+ console.print(f"[dim]({len(result.context_snippets)} context snippets used)[/dim]")