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,119 @@
|
|
|
1
|
+
"""CLI command: hotspots — identify high-risk code hotspots."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json as json_mod
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from semantic_code_intelligence.utils.logging import (
|
|
11
|
+
console,
|
|
12
|
+
get_logger,
|
|
13
|
+
print_error,
|
|
14
|
+
print_success,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
logger = get_logger("cli.hotspots")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@click.command("hotspots")
|
|
21
|
+
@click.option(
|
|
22
|
+
"--path", "-p",
|
|
23
|
+
default=".",
|
|
24
|
+
type=click.Path(exists=True, file_okay=False, resolve_path=True),
|
|
25
|
+
help="Project root path.",
|
|
26
|
+
)
|
|
27
|
+
@click.option(
|
|
28
|
+
"--json-output", "--json", "json_mode",
|
|
29
|
+
is_flag=True, default=False,
|
|
30
|
+
help="Output in JSON format.",
|
|
31
|
+
)
|
|
32
|
+
@click.option(
|
|
33
|
+
"--pipe",
|
|
34
|
+
is_flag=True, default=False,
|
|
35
|
+
help="Plain text output for piping / CI.",
|
|
36
|
+
)
|
|
37
|
+
@click.option(
|
|
38
|
+
"--top-n", "-n",
|
|
39
|
+
type=int, default=20,
|
|
40
|
+
help="Number of hotspots to report (default: 20).",
|
|
41
|
+
)
|
|
42
|
+
@click.option(
|
|
43
|
+
"--include-git/--no-git",
|
|
44
|
+
default=True,
|
|
45
|
+
help="Include git churn data (default: enabled).",
|
|
46
|
+
)
|
|
47
|
+
@click.pass_context
|
|
48
|
+
def hotspots_cmd(
|
|
49
|
+
ctx: click.Context,
|
|
50
|
+
path: str,
|
|
51
|
+
json_mode: bool,
|
|
52
|
+
pipe: bool,
|
|
53
|
+
top_n: int,
|
|
54
|
+
include_git: bool,
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Identify high-risk code hotspots via multi-factor analysis.
|
|
57
|
+
|
|
58
|
+
Combines complexity, duplication, fan-in/out, and git churn to
|
|
59
|
+
score symbols by maintenance risk.
|
|
60
|
+
|
|
61
|
+
Examples:
|
|
62
|
+
|
|
63
|
+
codexa hotspots
|
|
64
|
+
|
|
65
|
+
codexa hotspots --top-n 10 --json
|
|
66
|
+
|
|
67
|
+
codexa hotspots --no-git --pipe
|
|
68
|
+
"""
|
|
69
|
+
from semantic_code_intelligence.ci.hotspots import analyze_hotspots
|
|
70
|
+
from semantic_code_intelligence.context.engine import CallGraph, ContextBuilder, DependencyMap
|
|
71
|
+
|
|
72
|
+
root = Path(path).resolve()
|
|
73
|
+
builder = ContextBuilder()
|
|
74
|
+
dep_map = DependencyMap()
|
|
75
|
+
|
|
76
|
+
py_files = sorted(root.rglob("*.py"))
|
|
77
|
+
py_files = [f for f in py_files if ".venv" not in f.parts and "__pycache__" not in f.parts]
|
|
78
|
+
|
|
79
|
+
for fp in py_files:
|
|
80
|
+
try:
|
|
81
|
+
content = fp.read_text(encoding="utf-8", errors="replace")
|
|
82
|
+
builder.index_file(str(fp), content)
|
|
83
|
+
dep_map.add_file(str(fp), content)
|
|
84
|
+
except Exception:
|
|
85
|
+
logger.debug("Failed to index %s", fp)
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
symbols = builder.get_all_symbols()
|
|
89
|
+
call_graph = CallGraph()
|
|
90
|
+
call_graph.build(symbols)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
report = analyze_hotspots(
|
|
94
|
+
symbols, call_graph, dep_map, root,
|
|
95
|
+
top_n=top_n, include_git=include_git,
|
|
96
|
+
)
|
|
97
|
+
except Exception as exc:
|
|
98
|
+
logger.debug("Hotspot analysis failed", exc_info=True)
|
|
99
|
+
print_error(f"Hotspot analysis failed: {exc}")
|
|
100
|
+
ctx.exit(1)
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
if json_mode:
|
|
104
|
+
click.echo(json_mod.dumps(report.to_dict(), indent=2))
|
|
105
|
+
elif pipe:
|
|
106
|
+
click.echo(f"files={report.files_analyzed} symbols={report.symbols_analyzed} hotspots={len(report.hotspots)}")
|
|
107
|
+
for h in report.hotspots:
|
|
108
|
+
click.echo(f" {h.risk_score:.3f} {h.kind:<10} {h.file_path}:{h.name}")
|
|
109
|
+
else:
|
|
110
|
+
console.print(f"\n[bold]Hotspot Analysis[/bold] — {report.files_analyzed} files, {report.symbols_analyzed} symbols\n")
|
|
111
|
+
if not report.hotspots:
|
|
112
|
+
print_success("No significant hotspots detected.")
|
|
113
|
+
return
|
|
114
|
+
for i, h in enumerate(report.hotspots, 1):
|
|
115
|
+
colour = "red" if h.risk_score >= 0.7 else "yellow" if h.risk_score >= 0.4 else "green"
|
|
116
|
+
console.print(f" [{colour}]{i:>3}. {h.risk_score:.3f}[/{colour}] {h.kind:<10} [cyan]{h.file_path}[/cyan]:[bold]{h.name}[/bold]")
|
|
117
|
+
for f in h.factors:
|
|
118
|
+
console.print(f" {f.name}: {f.raw_value:.2f} (norm={f.normalized:.2f}, w={f.weight:.2f})")
|
|
119
|
+
console.print()
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""CLI command: impact — analyse blast radius of code changes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json as json_mod
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from semantic_code_intelligence.utils.logging import (
|
|
11
|
+
console,
|
|
12
|
+
get_logger,
|
|
13
|
+
print_error,
|
|
14
|
+
print_success,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
logger = get_logger("cli.impact")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@click.command("impact")
|
|
21
|
+
@click.argument("target")
|
|
22
|
+
@click.option(
|
|
23
|
+
"--path", "-p",
|
|
24
|
+
default=".",
|
|
25
|
+
type=click.Path(exists=True, file_okay=False, resolve_path=True),
|
|
26
|
+
help="Project root path.",
|
|
27
|
+
)
|
|
28
|
+
@click.option(
|
|
29
|
+
"--json-output", "--json", "json_mode",
|
|
30
|
+
is_flag=True, default=False,
|
|
31
|
+
help="Output in JSON format.",
|
|
32
|
+
)
|
|
33
|
+
@click.option(
|
|
34
|
+
"--pipe",
|
|
35
|
+
is_flag=True, default=False,
|
|
36
|
+
help="Plain text output for piping / CI.",
|
|
37
|
+
)
|
|
38
|
+
@click.option(
|
|
39
|
+
"--max-depth", "-d",
|
|
40
|
+
type=int, default=5,
|
|
41
|
+
help="Maximum traversal depth (default: 5).",
|
|
42
|
+
)
|
|
43
|
+
@click.pass_context
|
|
44
|
+
def impact_cmd(
|
|
45
|
+
ctx: click.Context,
|
|
46
|
+
target: str,
|
|
47
|
+
path: str,
|
|
48
|
+
json_mode: bool,
|
|
49
|
+
pipe: bool,
|
|
50
|
+
max_depth: int,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Analyse the blast radius of a change to TARGET.
|
|
53
|
+
|
|
54
|
+
TARGET can be a symbol name (function/class) or a file path relative
|
|
55
|
+
to the project root.
|
|
56
|
+
|
|
57
|
+
Examples:
|
|
58
|
+
|
|
59
|
+
codexa impact parse_file
|
|
60
|
+
|
|
61
|
+
codexa impact src/parser.py --json
|
|
62
|
+
|
|
63
|
+
codexa impact MyClass --max-depth 3 --pipe
|
|
64
|
+
"""
|
|
65
|
+
from semantic_code_intelligence.ci.impact import analyze_impact
|
|
66
|
+
from semantic_code_intelligence.context.engine import CallGraph, ContextBuilder, DependencyMap
|
|
67
|
+
|
|
68
|
+
root = Path(path).resolve()
|
|
69
|
+
builder = ContextBuilder()
|
|
70
|
+
dep_map = DependencyMap()
|
|
71
|
+
|
|
72
|
+
py_files = sorted(root.rglob("*.py"))
|
|
73
|
+
py_files = [f for f in py_files if ".venv" not in f.parts and "__pycache__" not in f.parts]
|
|
74
|
+
|
|
75
|
+
for fp in py_files:
|
|
76
|
+
try:
|
|
77
|
+
content = fp.read_text(encoding="utf-8", errors="replace")
|
|
78
|
+
builder.index_file(str(fp), content)
|
|
79
|
+
dep_map.add_file(str(fp), content)
|
|
80
|
+
except Exception:
|
|
81
|
+
logger.debug("Failed to index %s", fp)
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
symbols = builder.get_all_symbols()
|
|
85
|
+
call_graph = CallGraph()
|
|
86
|
+
call_graph.build(symbols)
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
report = analyze_impact(
|
|
90
|
+
target, symbols, call_graph, dep_map, root,
|
|
91
|
+
max_depth=max_depth,
|
|
92
|
+
)
|
|
93
|
+
except Exception as exc:
|
|
94
|
+
logger.debug("Impact analysis failed", exc_info=True)
|
|
95
|
+
print_error(f"Impact analysis failed: {exc}")
|
|
96
|
+
ctx.exit(1)
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
if json_mode:
|
|
100
|
+
click.echo(json_mod.dumps(report.to_dict(), indent=2))
|
|
101
|
+
elif pipe:
|
|
102
|
+
click.echo(f"target={report.target} kind={report.target_kind} affected={report.total_affected}")
|
|
103
|
+
for s in report.direct_symbols:
|
|
104
|
+
click.echo(f" DIRECT {s.relationship:<20} {s.file_path}:{s.name}")
|
|
105
|
+
for s in report.transitive_symbols:
|
|
106
|
+
click.echo(f" TRANS {s.relationship:<20} {s.file_path}:{s.name}")
|
|
107
|
+
for m in report.affected_modules:
|
|
108
|
+
click.echo(f" MODULE {m.relationship:<20} {m.file_path}")
|
|
109
|
+
else:
|
|
110
|
+
console.print(f"\n[bold]Impact Analysis[/bold] — target: [cyan]{report.target}[/cyan] ({report.target_kind})\n")
|
|
111
|
+
if report.total_affected == 0:
|
|
112
|
+
print_success("No downstream impact detected.")
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
if report.direct_symbols:
|
|
116
|
+
console.print("[bold]Direct callers:[/bold]")
|
|
117
|
+
for s in report.direct_symbols:
|
|
118
|
+
console.print(f" [yellow]{s.name}[/yellow] ({s.kind}) [dim]{s.file_path}[/dim]")
|
|
119
|
+
|
|
120
|
+
if report.transitive_symbols:
|
|
121
|
+
console.print("\n[bold]Transitive callers:[/bold]")
|
|
122
|
+
for s in report.transitive_symbols:
|
|
123
|
+
console.print(f" [yellow]{s.name}[/yellow] depth={s.depth} [dim]{s.file_path}[/dim]")
|
|
124
|
+
|
|
125
|
+
if report.affected_modules:
|
|
126
|
+
console.print("\n[bold]Affected modules:[/bold]")
|
|
127
|
+
for m in report.affected_modules:
|
|
128
|
+
console.print(f" [cyan]{m.file_path}[/cyan] ({m.relationship}, depth={m.depth})")
|
|
129
|
+
|
|
130
|
+
console.print(f"\n[bold]Total affected:[/bold] {report.total_affected}")
|
|
131
|
+
console.print()
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""CLI command: index - Index a codebase for semantic search."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from semantic_code_intelligence.config.settings import AppConfig
|
|
11
|
+
from semantic_code_intelligence.services.indexing_service import run_indexing
|
|
12
|
+
from semantic_code_intelligence.utils.logging import (
|
|
13
|
+
get_logger,
|
|
14
|
+
print_error,
|
|
15
|
+
print_info,
|
|
16
|
+
print_success,
|
|
17
|
+
print_warning,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
logger = get_logger("cli.index")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _run_watch_mode(root: Path, force: bool) -> None:
|
|
24
|
+
"""Run continuous watch-mode indexing with live incremental updates."""
|
|
25
|
+
from semantic_code_intelligence.daemon.watcher import NativeFileWatcher
|
|
26
|
+
from semantic_code_intelligence.services.indexing_service import run_incremental_indexing
|
|
27
|
+
|
|
28
|
+
# Initial index
|
|
29
|
+
print_info("Watch mode: performing initial index...")
|
|
30
|
+
result = run_indexing(project_root=root, force=force)
|
|
31
|
+
print_success(
|
|
32
|
+
f"Initial index: {result.files_indexed} files, "
|
|
33
|
+
f"{result.chunks_created} chunks, {result.total_vectors} vectors."
|
|
34
|
+
)
|
|
35
|
+
print_info("Watching for changes... (press Ctrl+C to stop)")
|
|
36
|
+
|
|
37
|
+
update_count = 0
|
|
38
|
+
|
|
39
|
+
def _on_changes(events: list) -> None:
|
|
40
|
+
nonlocal update_count
|
|
41
|
+
changed = [str(e.path) for e in events if e.change_type in ("created", "modified")]
|
|
42
|
+
deleted = [str(e.path) for e in events if e.change_type == "deleted"]
|
|
43
|
+
if not changed and not deleted:
|
|
44
|
+
return
|
|
45
|
+
try:
|
|
46
|
+
inc = run_incremental_indexing(root, changed_files=changed, deleted_files=deleted)
|
|
47
|
+
update_count += 1
|
|
48
|
+
print_success(
|
|
49
|
+
f"[update #{update_count}] Re-indexed {inc.files_indexed} files "
|
|
50
|
+
f"({inc.chunks_created} chunks). {len(deleted)} deleted."
|
|
51
|
+
)
|
|
52
|
+
except Exception as exc:
|
|
53
|
+
logger.debug("Incremental indexing error", exc_info=True)
|
|
54
|
+
print_error(f"Incremental indexing failed: {exc}")
|
|
55
|
+
|
|
56
|
+
watcher = NativeFileWatcher(root)
|
|
57
|
+
watcher.on_change(_on_changes)
|
|
58
|
+
watcher.start()
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
while True:
|
|
62
|
+
time.sleep(1)
|
|
63
|
+
except KeyboardInterrupt:
|
|
64
|
+
pass
|
|
65
|
+
finally:
|
|
66
|
+
watcher.stop()
|
|
67
|
+
print_success(f"Watch mode stopped. {update_count} incremental updates applied.")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@click.command("index")
|
|
71
|
+
@click.argument(
|
|
72
|
+
"path",
|
|
73
|
+
default=".",
|
|
74
|
+
type=click.Path(exists=True, file_okay=False, resolve_path=True),
|
|
75
|
+
)
|
|
76
|
+
@click.option(
|
|
77
|
+
"--force",
|
|
78
|
+
is_flag=True,
|
|
79
|
+
default=False,
|
|
80
|
+
help="Force full re-index, ignoring cache.",
|
|
81
|
+
)
|
|
82
|
+
@click.option(
|
|
83
|
+
"--watch",
|
|
84
|
+
"-w",
|
|
85
|
+
is_flag=True,
|
|
86
|
+
default=False,
|
|
87
|
+
help="Watch for file changes and re-index incrementally.",
|
|
88
|
+
)
|
|
89
|
+
@click.pass_context
|
|
90
|
+
def index_cmd(ctx: click.Context, path: str, force: bool, watch: bool) -> None:
|
|
91
|
+
"""Index a codebase for semantic search.
|
|
92
|
+
|
|
93
|
+
Scans the target directory, extracts code chunks, generates embeddings,
|
|
94
|
+
and stores them in the vector index.
|
|
95
|
+
|
|
96
|
+
Use --watch to enable live incremental re-indexing on file changes.
|
|
97
|
+
|
|
98
|
+
\b
|
|
99
|
+
Examples:
|
|
100
|
+
codexa index
|
|
101
|
+
codexa index --force
|
|
102
|
+
codexa index --watch
|
|
103
|
+
"""
|
|
104
|
+
root = Path(path).resolve()
|
|
105
|
+
config_dir = AppConfig.config_dir(root)
|
|
106
|
+
|
|
107
|
+
if not config_dir.exists():
|
|
108
|
+
print_error(
|
|
109
|
+
f"Project not initialized at {root}. Run 'codexa init' first."
|
|
110
|
+
)
|
|
111
|
+
ctx.exit(1)
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
if watch:
|
|
115
|
+
_run_watch_mode(root, force)
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
print_info(f"Indexing codebase at: {root}")
|
|
119
|
+
|
|
120
|
+
if force:
|
|
121
|
+
print_info("Force mode: full re-index will be performed.")
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
result = run_indexing(project_root=root, force=force)
|
|
125
|
+
except Exception as e:
|
|
126
|
+
print_error(f"Indexing failed: {e}")
|
|
127
|
+
logger.debug("Indexing error details:", exc_info=True)
|
|
128
|
+
ctx.exit(1)
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
if result.files_scanned == 0:
|
|
132
|
+
print_warning("No indexable files found.")
|
|
133
|
+
else:
|
|
134
|
+
print_success(
|
|
135
|
+
f"Indexed {result.files_indexed} files "
|
|
136
|
+
f"({result.chunks_created} chunks, {result.total_vectors} vectors). "
|
|
137
|
+
f"Skipped {result.files_skipped} unchanged files."
|
|
138
|
+
)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""CLI command: init - Initialize a new project for semantic code intelligence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from semantic_code_intelligence.config.settings import (
|
|
11
|
+
AppConfig,
|
|
12
|
+
init_project,
|
|
13
|
+
load_config,
|
|
14
|
+
)
|
|
15
|
+
from semantic_code_intelligence.utils.logging import (
|
|
16
|
+
get_logger,
|
|
17
|
+
print_error,
|
|
18
|
+
print_info,
|
|
19
|
+
print_success,
|
|
20
|
+
print_warning,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
logger = get_logger("cli.init")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _generate_vscode_mcp_config(root: Path) -> bool:
|
|
27
|
+
"""Create .vscode/settings.json with MCP server config if not present."""
|
|
28
|
+
vscode_dir = root / ".vscode"
|
|
29
|
+
settings_path = vscode_dir / "settings.json"
|
|
30
|
+
|
|
31
|
+
mcp_block = {
|
|
32
|
+
"mcp": {
|
|
33
|
+
"servers": {
|
|
34
|
+
"codexa": {
|
|
35
|
+
"command": "codexa",
|
|
36
|
+
"args": ["mcp", "--path", str(root)],
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if settings_path.exists():
|
|
43
|
+
try:
|
|
44
|
+
existing = json.loads(settings_path.read_text(encoding="utf-8"))
|
|
45
|
+
except (json.JSONDecodeError, OSError):
|
|
46
|
+
existing = {}
|
|
47
|
+
|
|
48
|
+
if "mcp" in existing:
|
|
49
|
+
return False # already configured
|
|
50
|
+
|
|
51
|
+
existing.update(mcp_block)
|
|
52
|
+
settings_path.write_text(
|
|
53
|
+
json.dumps(existing, indent=4) + "\n", encoding="utf-8"
|
|
54
|
+
)
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
vscode_dir.mkdir(exist_ok=True)
|
|
58
|
+
settings_path.write_text(
|
|
59
|
+
json.dumps(mcp_block, indent=4) + "\n", encoding="utf-8"
|
|
60
|
+
)
|
|
61
|
+
return True
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@click.command("init")
|
|
65
|
+
@click.argument(
|
|
66
|
+
"path",
|
|
67
|
+
default=".",
|
|
68
|
+
type=click.Path(exists=True, file_okay=False, resolve_path=True),
|
|
69
|
+
)
|
|
70
|
+
@click.option(
|
|
71
|
+
"--index",
|
|
72
|
+
"auto_index",
|
|
73
|
+
is_flag=True,
|
|
74
|
+
default=False,
|
|
75
|
+
help="Automatically index the project after initialization.",
|
|
76
|
+
)
|
|
77
|
+
@click.option(
|
|
78
|
+
"--vscode",
|
|
79
|
+
"setup_vscode",
|
|
80
|
+
is_flag=True,
|
|
81
|
+
default=False,
|
|
82
|
+
help="Generate .vscode/settings.json with MCP server config.",
|
|
83
|
+
)
|
|
84
|
+
@click.pass_context
|
|
85
|
+
def init_cmd(ctx: click.Context, path: str, auto_index: bool, setup_vscode: bool) -> None:
|
|
86
|
+
"""Initialize a project for semantic code indexing.
|
|
87
|
+
|
|
88
|
+
Creates a .codexa/ directory with default configuration and an empty index.
|
|
89
|
+
|
|
90
|
+
\b
|
|
91
|
+
Quick start:
|
|
92
|
+
codexa init # basic setup
|
|
93
|
+
codexa init --index # setup + build index immediately
|
|
94
|
+
codexa init --vscode # setup + configure VS Code MCP
|
|
95
|
+
codexa init --index --vscode # full setup in one command
|
|
96
|
+
"""
|
|
97
|
+
root = Path(path).resolve()
|
|
98
|
+
|
|
99
|
+
# Check if already initialized
|
|
100
|
+
config_dir = AppConfig.config_dir(root)
|
|
101
|
+
if config_dir.exists():
|
|
102
|
+
print_info(f"Project already initialized at {root}")
|
|
103
|
+
print_info(f"Config directory: {config_dir}")
|
|
104
|
+
# Still allow --vscode and --index on existing projects
|
|
105
|
+
if setup_vscode:
|
|
106
|
+
if _generate_vscode_mcp_config(root):
|
|
107
|
+
print_success("VS Code MCP config written to .vscode/settings.json")
|
|
108
|
+
else:
|
|
109
|
+
print_info("VS Code MCP config already exists")
|
|
110
|
+
if auto_index:
|
|
111
|
+
_run_index(root)
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
config, config_path = init_project(root)
|
|
116
|
+
print_success(f"Initialized project at {root}")
|
|
117
|
+
print_info(f"Config file: {config_path}")
|
|
118
|
+
print_info(f"Index directory: {AppConfig.index_dir(root)}")
|
|
119
|
+
logger.debug("Default config: %s", config.model_dump())
|
|
120
|
+
except OSError as e:
|
|
121
|
+
print_error(f"Failed to initialize project: {e}")
|
|
122
|
+
ctx.exit(1)
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
if setup_vscode:
|
|
126
|
+
if _generate_vscode_mcp_config(root):
|
|
127
|
+
print_success("VS Code MCP config written to .vscode/settings.json")
|
|
128
|
+
|
|
129
|
+
if auto_index:
|
|
130
|
+
_run_index(root)
|
|
131
|
+
else:
|
|
132
|
+
print_info("")
|
|
133
|
+
print_info("Next steps:")
|
|
134
|
+
print_info(" codexa index — Build the search index")
|
|
135
|
+
print_info(" codexa search — Search your code")
|
|
136
|
+
print_info(" codexa grep — Raw file search (no index needed)")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _run_index(root: Path) -> None:
|
|
140
|
+
"""Run indexing as part of init."""
|
|
141
|
+
from semantic_code_intelligence.services.indexing_service import index_project
|
|
142
|
+
|
|
143
|
+
print_info("Building search index...")
|
|
144
|
+
try:
|
|
145
|
+
result = index_project(root)
|
|
146
|
+
print_success(
|
|
147
|
+
f"Indexed {result.chunks_stored} chunks from "
|
|
148
|
+
f"{result.files_scanned} files"
|
|
149
|
+
)
|
|
150
|
+
except Exception as e:
|
|
151
|
+
print_warning(f"Indexing failed: {e}")
|
|
152
|
+
print_info("Run 'codexa index' manually to build the index.")
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""CLI command: investigate — autonomous multi-step code investigation."""
|
|
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.utils.logging import (
|
|
12
|
+
console,
|
|
13
|
+
get_logger,
|
|
14
|
+
print_error,
|
|
15
|
+
print_info,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from semantic_code_intelligence.llm.provider import LLMProvider
|
|
20
|
+
|
|
21
|
+
logger = get_logger("cli.investigate")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _wrap_provider(provider: LLMProvider, llm: Any, config: Any) -> LLMProvider:
|
|
25
|
+
"""Wrap a provider with caching and rate limiting based on config."""
|
|
26
|
+
from semantic_code_intelligence.llm.cache import LLMCache
|
|
27
|
+
from semantic_code_intelligence.llm.cached_provider import CachedProvider
|
|
28
|
+
from semantic_code_intelligence.llm.rate_limiter import RateLimiter
|
|
29
|
+
|
|
30
|
+
cache = None
|
|
31
|
+
if getattr(llm, "cache_enabled", False):
|
|
32
|
+
cache_dir = str(config.config_dir(config.project_root)) if hasattr(config, "config_dir") else None
|
|
33
|
+
cache = LLMCache(
|
|
34
|
+
cache_dir=cache_dir,
|
|
35
|
+
ttl_hours=getattr(llm, "cache_ttl_hours", 24),
|
|
36
|
+
max_entries=getattr(llm, "cache_max_entries", 1000),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
rate_limiter = None
|
|
40
|
+
rpm = getattr(llm, "rate_limit_rpm", 0)
|
|
41
|
+
tpm = getattr(llm, "rate_limit_tpm", 0)
|
|
42
|
+
if rpm > 0 or tpm > 0:
|
|
43
|
+
rate_limiter = RateLimiter(rpm=rpm, tpm=tpm)
|
|
44
|
+
|
|
45
|
+
if cache is not None or rate_limiter is not None:
|
|
46
|
+
return CachedProvider(provider, cache=cache, rate_limiter=rate_limiter)
|
|
47
|
+
return provider
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _get_provider(config: Any) -> LLMProvider:
|
|
51
|
+
"""Build an LLM provider from the app configuration."""
|
|
52
|
+
from semantic_code_intelligence.config.settings import LLMConfig
|
|
53
|
+
|
|
54
|
+
llm: LLMConfig = config.llm
|
|
55
|
+
if llm.provider == "openai":
|
|
56
|
+
from semantic_code_intelligence.llm.openai_provider import OpenAIProvider
|
|
57
|
+
|
|
58
|
+
provider: LLMProvider = OpenAIProvider(
|
|
59
|
+
api_key=llm.api_key,
|
|
60
|
+
model=llm.model,
|
|
61
|
+
base_url=llm.base_url or None,
|
|
62
|
+
temperature=llm.temperature,
|
|
63
|
+
max_tokens=llm.max_tokens,
|
|
64
|
+
)
|
|
65
|
+
elif llm.provider == "ollama":
|
|
66
|
+
from semantic_code_intelligence.llm.ollama_provider import OllamaProvider
|
|
67
|
+
|
|
68
|
+
provider = OllamaProvider(
|
|
69
|
+
model=llm.model,
|
|
70
|
+
base_url=llm.base_url or "http://localhost:11434",
|
|
71
|
+
temperature=llm.temperature,
|
|
72
|
+
max_tokens=llm.max_tokens,
|
|
73
|
+
)
|
|
74
|
+
else:
|
|
75
|
+
from semantic_code_intelligence.llm.mock_provider import MockProvider
|
|
76
|
+
|
|
77
|
+
provider = MockProvider()
|
|
78
|
+
|
|
79
|
+
return _wrap_provider(provider, llm, config)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@click.command("investigate")
|
|
83
|
+
@click.argument("question", type=str)
|
|
84
|
+
@click.option(
|
|
85
|
+
"--max-steps", "-n",
|
|
86
|
+
default=6,
|
|
87
|
+
type=int,
|
|
88
|
+
help="Maximum investigation steps before forcing a conclusion.",
|
|
89
|
+
)
|
|
90
|
+
@click.option(
|
|
91
|
+
"--json-output", "--json", "json_mode",
|
|
92
|
+
is_flag=True,
|
|
93
|
+
default=False,
|
|
94
|
+
help="Output in JSON format.",
|
|
95
|
+
)
|
|
96
|
+
@click.option(
|
|
97
|
+
"--path", "-p",
|
|
98
|
+
default=".",
|
|
99
|
+
type=click.Path(exists=True, file_okay=False, resolve_path=True),
|
|
100
|
+
help="Project root path.",
|
|
101
|
+
)
|
|
102
|
+
@click.option(
|
|
103
|
+
"--stream",
|
|
104
|
+
is_flag=True,
|
|
105
|
+
default=False,
|
|
106
|
+
help="Stream the conclusion tokens incrementally.",
|
|
107
|
+
)
|
|
108
|
+
@click.option("--pipe", is_flag=True, default=False, hidden=True)
|
|
109
|
+
@click.pass_context
|
|
110
|
+
def investigate_cmd(
|
|
111
|
+
ctx: click.Context,
|
|
112
|
+
question: str,
|
|
113
|
+
max_steps: int,
|
|
114
|
+
json_mode: bool,
|
|
115
|
+
path: str,
|
|
116
|
+
stream: bool,
|
|
117
|
+
pipe: bool,
|
|
118
|
+
) -> None:
|
|
119
|
+
"""Run an autonomous multi-step investigation to answer a question.
|
|
120
|
+
|
|
121
|
+
CodexA iteratively searches, analyses symbols, and examines dependencies
|
|
122
|
+
until it can confidently answer your question. Each step is visible
|
|
123
|
+
so you can follow the reasoning chain.
|
|
124
|
+
"""
|
|
125
|
+
from semantic_code_intelligence.config.settings import load_config
|
|
126
|
+
from semantic_code_intelligence.llm.investigation import InvestigationChain
|
|
127
|
+
|
|
128
|
+
root = Path(path).resolve()
|
|
129
|
+
pipe = pipe or ctx.obj.get("pipe", False)
|
|
130
|
+
|
|
131
|
+
config = load_config(root)
|
|
132
|
+
provider = _get_provider(config)
|
|
133
|
+
|
|
134
|
+
chain = InvestigationChain(provider, root, max_steps=max_steps)
|
|
135
|
+
result = chain.investigate(question, stream_conclusion=stream and not json_mode)
|
|
136
|
+
|
|
137
|
+
if json_mode:
|
|
138
|
+
click.echo(json_mod.dumps(result.to_dict(), indent=2))
|
|
139
|
+
elif pipe:
|
|
140
|
+
for step in result.steps:
|
|
141
|
+
click.echo(f"[{step['step']}] {step['action']}: {step.get('action_input', '')}")
|
|
142
|
+
click.echo(f"\nConclusion: {result.conclusion}")
|
|
143
|
+
else:
|
|
144
|
+
from rich.panel import Panel
|
|
145
|
+
from rich.markdown import Markdown
|
|
146
|
+
|
|
147
|
+
for step in result.steps:
|
|
148
|
+
action = step["action"]
|
|
149
|
+
thought = step.get("thought", "")
|
|
150
|
+
output = step.get("output", "")[:300]
|
|
151
|
+
console.print(
|
|
152
|
+
f" [bold cyan]Step {step['step']}[/] [{action}] "
|
|
153
|
+
f"[dim]{thought}[/]"
|
|
154
|
+
)
|
|
155
|
+
if output and action != "conclude":
|
|
156
|
+
console.print(f" [dim]{output}[/dim]")
|
|
157
|
+
|
|
158
|
+
console.print()
|
|
159
|
+
console.print(Panel(
|
|
160
|
+
Markdown(result.conclusion),
|
|
161
|
+
title=f"Investigation ({result.total_steps} steps)",
|
|
162
|
+
border_style="green",
|
|
163
|
+
))
|