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.
- codexa-0.4.0.dist-info/METADATA +650 -0
- codexa-0.4.0.dist-info/RECORD +189 -0
- codexa-0.4.0.dist-info/WHEEL +5 -0
- codexa-0.4.0.dist-info/entry_points.txt +2 -0
- codexa-0.4.0.dist-info/licenses/LICENSE +21 -0
- codexa-0.4.0.dist-info/top_level.txt +1 -0
- semantic_code_intelligence/__init__.py +5 -0
- semantic_code_intelligence/analysis/__init__.py +21 -0
- semantic_code_intelligence/analysis/ai_features.py +351 -0
- semantic_code_intelligence/bridge/__init__.py +28 -0
- semantic_code_intelligence/bridge/context_provider.py +245 -0
- semantic_code_intelligence/bridge/protocol.py +167 -0
- semantic_code_intelligence/bridge/server.py +348 -0
- semantic_code_intelligence/bridge/vscode.py +271 -0
- semantic_code_intelligence/ci/__init__.py +13 -0
- semantic_code_intelligence/ci/hooks.py +98 -0
- semantic_code_intelligence/ci/hotspots.py +272 -0
- semantic_code_intelligence/ci/impact.py +246 -0
- semantic_code_intelligence/ci/metrics.py +591 -0
- semantic_code_intelligence/ci/pr.py +412 -0
- semantic_code_intelligence/ci/quality.py +557 -0
- semantic_code_intelligence/ci/templates.py +164 -0
- semantic_code_intelligence/ci/trace.py +224 -0
- semantic_code_intelligence/cli/__init__.py +0 -0
- semantic_code_intelligence/cli/commands/__init__.py +0 -0
- semantic_code_intelligence/cli/commands/ask_cmd.py +153 -0
- semantic_code_intelligence/cli/commands/benchmark_cmd.py +303 -0
- semantic_code_intelligence/cli/commands/chat_cmd.py +252 -0
- semantic_code_intelligence/cli/commands/ci_gen_cmd.py +74 -0
- semantic_code_intelligence/cli/commands/context_cmd.py +120 -0
- semantic_code_intelligence/cli/commands/cross_refactor_cmd.py +113 -0
- semantic_code_intelligence/cli/commands/deps_cmd.py +91 -0
- semantic_code_intelligence/cli/commands/docs_cmd.py +101 -0
- semantic_code_intelligence/cli/commands/doctor_cmd.py +147 -0
- semantic_code_intelligence/cli/commands/evolve_cmd.py +171 -0
- semantic_code_intelligence/cli/commands/explain_cmd.py +112 -0
- semantic_code_intelligence/cli/commands/gate_cmd.py +135 -0
- semantic_code_intelligence/cli/commands/grep_cmd.py +234 -0
- semantic_code_intelligence/cli/commands/hotspots_cmd.py +119 -0
- semantic_code_intelligence/cli/commands/impact_cmd.py +131 -0
- semantic_code_intelligence/cli/commands/index_cmd.py +138 -0
- semantic_code_intelligence/cli/commands/init_cmd.py +152 -0
- semantic_code_intelligence/cli/commands/investigate_cmd.py +163 -0
- semantic_code_intelligence/cli/commands/languages_cmd.py +101 -0
- semantic_code_intelligence/cli/commands/lsp_cmd.py +49 -0
- semantic_code_intelligence/cli/commands/mcp_cmd.py +50 -0
- semantic_code_intelligence/cli/commands/metrics_cmd.py +264 -0
- semantic_code_intelligence/cli/commands/models_cmd.py +157 -0
- semantic_code_intelligence/cli/commands/plugin_cmd.py +275 -0
- semantic_code_intelligence/cli/commands/pr_summary_cmd.py +178 -0
- semantic_code_intelligence/cli/commands/quality_cmd.py +208 -0
- semantic_code_intelligence/cli/commands/refactor_cmd.py +103 -0
- semantic_code_intelligence/cli/commands/review_cmd.py +88 -0
- semantic_code_intelligence/cli/commands/search_cmd.py +236 -0
- semantic_code_intelligence/cli/commands/serve_cmd.py +117 -0
- semantic_code_intelligence/cli/commands/suggest_cmd.py +100 -0
- semantic_code_intelligence/cli/commands/summary_cmd.py +78 -0
- semantic_code_intelligence/cli/commands/tool_cmd.py +282 -0
- semantic_code_intelligence/cli/commands/trace_cmd.py +123 -0
- semantic_code_intelligence/cli/commands/tui_cmd.py +58 -0
- semantic_code_intelligence/cli/commands/viz_cmd.py +127 -0
- semantic_code_intelligence/cli/commands/watch_cmd.py +72 -0
- semantic_code_intelligence/cli/commands/web_cmd.py +61 -0
- semantic_code_intelligence/cli/commands/workspace_cmd.py +250 -0
- semantic_code_intelligence/cli/main.py +65 -0
- semantic_code_intelligence/cli/router.py +92 -0
- semantic_code_intelligence/config/__init__.py +0 -0
- semantic_code_intelligence/config/settings.py +260 -0
- semantic_code_intelligence/context/__init__.py +19 -0
- semantic_code_intelligence/context/engine.py +429 -0
- semantic_code_intelligence/context/memory.py +253 -0
- semantic_code_intelligence/daemon/__init__.py +1 -0
- semantic_code_intelligence/daemon/watcher.py +515 -0
- semantic_code_intelligence/docs/__init__.py +1080 -0
- semantic_code_intelligence/embeddings/__init__.py +0 -0
- semantic_code_intelligence/embeddings/enhanced.py +131 -0
- semantic_code_intelligence/embeddings/generator.py +149 -0
- semantic_code_intelligence/embeddings/model_registry.py +100 -0
- semantic_code_intelligence/evolution/__init__.py +1 -0
- semantic_code_intelligence/evolution/budget_guard.py +111 -0
- semantic_code_intelligence/evolution/commit_manager.py +88 -0
- semantic_code_intelligence/evolution/context_builder.py +131 -0
- semantic_code_intelligence/evolution/engine.py +249 -0
- semantic_code_intelligence/evolution/patch_generator.py +229 -0
- semantic_code_intelligence/evolution/task_selector.py +214 -0
- semantic_code_intelligence/evolution/test_runner.py +111 -0
- semantic_code_intelligence/indexing/__init__.py +0 -0
- semantic_code_intelligence/indexing/chunker.py +174 -0
- semantic_code_intelligence/indexing/parallel.py +86 -0
- semantic_code_intelligence/indexing/scanner.py +146 -0
- semantic_code_intelligence/indexing/semantic_chunker.py +337 -0
- semantic_code_intelligence/llm/__init__.py +62 -0
- semantic_code_intelligence/llm/cache.py +219 -0
- semantic_code_intelligence/llm/cached_provider.py +145 -0
- semantic_code_intelligence/llm/conversation.py +190 -0
- semantic_code_intelligence/llm/cross_refactor.py +272 -0
- semantic_code_intelligence/llm/investigation.py +274 -0
- semantic_code_intelligence/llm/mock_provider.py +77 -0
- semantic_code_intelligence/llm/ollama_provider.py +122 -0
- semantic_code_intelligence/llm/openai_provider.py +100 -0
- semantic_code_intelligence/llm/provider.py +92 -0
- semantic_code_intelligence/llm/rate_limiter.py +164 -0
- semantic_code_intelligence/llm/reasoning.py +438 -0
- semantic_code_intelligence/llm/safety.py +110 -0
- semantic_code_intelligence/llm/streaming.py +251 -0
- semantic_code_intelligence/lsp/__init__.py +609 -0
- semantic_code_intelligence/mcp/__init__.py +393 -0
- semantic_code_intelligence/parsing/__init__.py +19 -0
- semantic_code_intelligence/parsing/parser.py +375 -0
- semantic_code_intelligence/plugins/__init__.py +255 -0
- semantic_code_intelligence/plugins/examples/__init__.py +1 -0
- semantic_code_intelligence/plugins/examples/code_quality.py +73 -0
- semantic_code_intelligence/plugins/examples/search_annotator.py +56 -0
- semantic_code_intelligence/scalability/__init__.py +205 -0
- semantic_code_intelligence/search/__init__.py +0 -0
- semantic_code_intelligence/search/formatter.py +123 -0
- semantic_code_intelligence/search/grep.py +361 -0
- semantic_code_intelligence/search/hybrid_search.py +170 -0
- semantic_code_intelligence/search/keyword_search.py +311 -0
- semantic_code_intelligence/search/section_expander.py +103 -0
- semantic_code_intelligence/services/__init__.py +0 -0
- semantic_code_intelligence/services/indexing_service.py +630 -0
- semantic_code_intelligence/services/search_service.py +269 -0
- semantic_code_intelligence/storage/__init__.py +0 -0
- semantic_code_intelligence/storage/chunk_hash_store.py +86 -0
- semantic_code_intelligence/storage/hash_store.py +66 -0
- semantic_code_intelligence/storage/index_manifest.py +85 -0
- semantic_code_intelligence/storage/index_stats.py +138 -0
- semantic_code_intelligence/storage/query_history.py +160 -0
- semantic_code_intelligence/storage/symbol_registry.py +209 -0
- semantic_code_intelligence/storage/vector_store.py +297 -0
- semantic_code_intelligence/tests/__init__.py +0 -0
- semantic_code_intelligence/tests/test_ai_features.py +351 -0
- semantic_code_intelligence/tests/test_chunker.py +119 -0
- semantic_code_intelligence/tests/test_cli.py +188 -0
- semantic_code_intelligence/tests/test_config.py +154 -0
- semantic_code_intelligence/tests/test_context.py +381 -0
- semantic_code_intelligence/tests/test_embeddings.py +73 -0
- semantic_code_intelligence/tests/test_endtoend.py +1142 -0
- semantic_code_intelligence/tests/test_enhanced_embeddings.py +92 -0
- semantic_code_intelligence/tests/test_hash_store.py +79 -0
- semantic_code_intelligence/tests/test_logging.py +55 -0
- semantic_code_intelligence/tests/test_new_cli.py +138 -0
- semantic_code_intelligence/tests/test_parser.py +495 -0
- semantic_code_intelligence/tests/test_phase10.py +355 -0
- semantic_code_intelligence/tests/test_phase11.py +593 -0
- semantic_code_intelligence/tests/test_phase12.py +375 -0
- semantic_code_intelligence/tests/test_phase13.py +663 -0
- semantic_code_intelligence/tests/test_phase14.py +568 -0
- semantic_code_intelligence/tests/test_phase15.py +814 -0
- semantic_code_intelligence/tests/test_phase16.py +792 -0
- semantic_code_intelligence/tests/test_phase17.py +815 -0
- semantic_code_intelligence/tests/test_phase18.py +934 -0
- semantic_code_intelligence/tests/test_phase19.py +986 -0
- semantic_code_intelligence/tests/test_phase20.py +2753 -0
- semantic_code_intelligence/tests/test_phase20b.py +2058 -0
- semantic_code_intelligence/tests/test_phase20c.py +962 -0
- semantic_code_intelligence/tests/test_phase21.py +428 -0
- semantic_code_intelligence/tests/test_phase22.py +799 -0
- semantic_code_intelligence/tests/test_phase23.py +783 -0
- semantic_code_intelligence/tests/test_phase24.py +715 -0
- semantic_code_intelligence/tests/test_phase25.py +496 -0
- semantic_code_intelligence/tests/test_phase26.py +251 -0
- semantic_code_intelligence/tests/test_phase27.py +531 -0
- semantic_code_intelligence/tests/test_phase8.py +592 -0
- semantic_code_intelligence/tests/test_phase9.py +643 -0
- semantic_code_intelligence/tests/test_plugins.py +293 -0
- semantic_code_intelligence/tests/test_priority_features.py +727 -0
- semantic_code_intelligence/tests/test_router.py +41 -0
- semantic_code_intelligence/tests/test_scalability.py +138 -0
- semantic_code_intelligence/tests/test_scanner.py +125 -0
- semantic_code_intelligence/tests/test_search.py +160 -0
- semantic_code_intelligence/tests/test_semantic_chunker.py +255 -0
- semantic_code_intelligence/tests/test_tools.py +182 -0
- semantic_code_intelligence/tests/test_vector_store.py +151 -0
- semantic_code_intelligence/tests/test_watcher.py +211 -0
- semantic_code_intelligence/tools/__init__.py +442 -0
- semantic_code_intelligence/tools/executor.py +232 -0
- semantic_code_intelligence/tools/protocol.py +200 -0
- semantic_code_intelligence/tui/__init__.py +454 -0
- semantic_code_intelligence/utils/__init__.py +0 -0
- semantic_code_intelligence/utils/logging.py +112 -0
- semantic_code_intelligence/version.py +3 -0
- semantic_code_intelligence/web/__init__.py +11 -0
- semantic_code_intelligence/web/api.py +289 -0
- semantic_code_intelligence/web/server.py +397 -0
- semantic_code_intelligence/web/ui.py +659 -0
- semantic_code_intelligence/web/visualize.py +226 -0
- 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]")
|