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,101 @@
|
|
|
1
|
+
"""CLI command: languages — List supported languages and their parsing status."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from semantic_code_intelligence.parsing.parser import (
|
|
8
|
+
_LANGUAGE_MODULES,
|
|
9
|
+
EXTENSION_TO_LANGUAGE,
|
|
10
|
+
FUNCTION_NODE_TYPES,
|
|
11
|
+
CLASS_NODE_TYPES,
|
|
12
|
+
IMPORT_NODE_TYPES,
|
|
13
|
+
get_language,
|
|
14
|
+
)
|
|
15
|
+
from semantic_code_intelligence.utils.logging import get_logger, console
|
|
16
|
+
|
|
17
|
+
logger = get_logger("cli.languages")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@click.command("languages")
|
|
21
|
+
@click.option(
|
|
22
|
+
"--json-output",
|
|
23
|
+
"--json",
|
|
24
|
+
"json_mode",
|
|
25
|
+
is_flag=True,
|
|
26
|
+
default=False,
|
|
27
|
+
help="Output as JSON.",
|
|
28
|
+
)
|
|
29
|
+
@click.option(
|
|
30
|
+
"--check",
|
|
31
|
+
is_flag=True,
|
|
32
|
+
default=False,
|
|
33
|
+
help="Verify each grammar can be loaded (slower).",
|
|
34
|
+
)
|
|
35
|
+
def languages_cmd(json_mode: bool, check: bool) -> None:
|
|
36
|
+
"""List all supported programming languages and their tree-sitter grammar status.
|
|
37
|
+
|
|
38
|
+
\b
|
|
39
|
+
Examples:
|
|
40
|
+
codexa languages
|
|
41
|
+
codexa languages --check
|
|
42
|
+
codexa languages --json
|
|
43
|
+
"""
|
|
44
|
+
import json as json_mod
|
|
45
|
+
|
|
46
|
+
# Build extension map (language -> list of extensions)
|
|
47
|
+
ext_map: dict[str, list[str]] = {}
|
|
48
|
+
for ext, lang in EXTENSION_TO_LANGUAGE.items():
|
|
49
|
+
ext_map.setdefault(lang, []).append(ext)
|
|
50
|
+
|
|
51
|
+
rows: list[dict] = []
|
|
52
|
+
for lang_name, module_name in sorted(_LANGUAGE_MODULES.items()):
|
|
53
|
+
extensions = ext_map.get(lang_name, [])
|
|
54
|
+
has_functions = lang_name in FUNCTION_NODE_TYPES
|
|
55
|
+
has_classes = lang_name in CLASS_NODE_TYPES
|
|
56
|
+
has_imports = lang_name in IMPORT_NODE_TYPES
|
|
57
|
+
|
|
58
|
+
status = "available"
|
|
59
|
+
if check:
|
|
60
|
+
loaded = get_language(lang_name)
|
|
61
|
+
status = "loaded" if loaded else "missing"
|
|
62
|
+
|
|
63
|
+
rows.append({
|
|
64
|
+
"language": lang_name,
|
|
65
|
+
"module": module_name,
|
|
66
|
+
"extensions": sorted(extensions),
|
|
67
|
+
"functions": has_functions,
|
|
68
|
+
"classes": has_classes,
|
|
69
|
+
"imports": has_imports,
|
|
70
|
+
"status": status,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
if json_mode:
|
|
74
|
+
click.echo(json_mod.dumps(rows, indent=2))
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
from rich.table import Table
|
|
78
|
+
|
|
79
|
+
table = Table(title="Supported Languages", show_lines=False)
|
|
80
|
+
table.add_column("Language", style="cyan bold")
|
|
81
|
+
table.add_column("Extensions", style="green")
|
|
82
|
+
table.add_column("Functions", justify="center")
|
|
83
|
+
table.add_column("Classes", justify="center")
|
|
84
|
+
table.add_column("Imports", justify="center")
|
|
85
|
+
if check:
|
|
86
|
+
table.add_column("Status", justify="center")
|
|
87
|
+
|
|
88
|
+
for row in rows:
|
|
89
|
+
exts = ", ".join(row["extensions"])
|
|
90
|
+
fn_icon = "[green]✓[/green]" if row["functions"] else "[red]✗[/red]"
|
|
91
|
+
cls_icon = "[green]✓[/green]" if row["classes"] else "[red]✗[/red]"
|
|
92
|
+
imp_icon = "[green]✓[/green]" if row["imports"] else "[red]✗[/red]"
|
|
93
|
+
cols = [row["language"], exts, fn_icon, cls_icon, imp_icon]
|
|
94
|
+
if check:
|
|
95
|
+
st = row["status"]
|
|
96
|
+
st_icon = "[green]loaded[/green]" if st == "loaded" else "[red]missing[/red]"
|
|
97
|
+
cols.append(st_icon)
|
|
98
|
+
table.add_row(*cols)
|
|
99
|
+
|
|
100
|
+
console.print(table)
|
|
101
|
+
console.print(f"\n[dim]{len(rows)} languages supported[/dim]")
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""CLI command: lsp — start the CodexA LSP server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from semantic_code_intelligence.config.settings import AppConfig
|
|
10
|
+
from semantic_code_intelligence.utils.logging import get_logger, print_error
|
|
11
|
+
|
|
12
|
+
logger = get_logger("cli.lsp")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.command("lsp")
|
|
16
|
+
@click.option(
|
|
17
|
+
"--path",
|
|
18
|
+
"-p",
|
|
19
|
+
default=".",
|
|
20
|
+
type=click.Path(exists=True, file_okay=False, resolve_path=True),
|
|
21
|
+
help="Project root path.",
|
|
22
|
+
)
|
|
23
|
+
@click.pass_context
|
|
24
|
+
def lsp_cmd(ctx: click.Context, path: str) -> None:
|
|
25
|
+
"""Start the CodexA Language Server Protocol server.
|
|
26
|
+
|
|
27
|
+
Runs over stdio using standard LSP Content-Length framing.
|
|
28
|
+
Compatible with any LSP client: VS Code, Neovim, Sublime, JetBrains.
|
|
29
|
+
|
|
30
|
+
\b
|
|
31
|
+
VS Code settings.json:
|
|
32
|
+
"codexa.lsp.path": "/path/to/your/project"
|
|
33
|
+
|
|
34
|
+
\b
|
|
35
|
+
Neovim (nvim-lspconfig):
|
|
36
|
+
require('lspconfig').codexa.setup {
|
|
37
|
+
cmd = { "codexa", "lsp", "--path", "/your/project" },
|
|
38
|
+
}
|
|
39
|
+
"""
|
|
40
|
+
root = Path(path).resolve()
|
|
41
|
+
config_dir = AppConfig.config_dir(root)
|
|
42
|
+
|
|
43
|
+
if not config_dir.exists():
|
|
44
|
+
print_error(f"Project not initialized at {root}. Run 'codexa init' first.")
|
|
45
|
+
ctx.exit(1)
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
from semantic_code_intelligence.lsp import run_lsp_server
|
|
49
|
+
run_lsp_server(root)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""CLI command: mcp - Start the MCP (Model Context Protocol) server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from semantic_code_intelligence.config.settings import AppConfig
|
|
10
|
+
from semantic_code_intelligence.utils.logging import get_logger, print_error, print_info
|
|
11
|
+
|
|
12
|
+
logger = get_logger("cli.mcp")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.command("mcp")
|
|
16
|
+
@click.option(
|
|
17
|
+
"--path",
|
|
18
|
+
"-p",
|
|
19
|
+
default=".",
|
|
20
|
+
type=click.Path(exists=True, file_okay=False, resolve_path=True),
|
|
21
|
+
help="Project root path.",
|
|
22
|
+
)
|
|
23
|
+
@click.pass_context
|
|
24
|
+
def mcp_cmd(ctx: click.Context, path: str) -> None:
|
|
25
|
+
"""Start the MCP server for AI agent integration.
|
|
26
|
+
|
|
27
|
+
Runs a JSON-RPC server over stdio, compatible with Claude Desktop,
|
|
28
|
+
Cursor, and other MCP-compatible AI tools.
|
|
29
|
+
|
|
30
|
+
\b
|
|
31
|
+
Configuration for Claude Desktop (claude_desktop_config.json):
|
|
32
|
+
{
|
|
33
|
+
"mcpServers": {
|
|
34
|
+
"codexa": {
|
|
35
|
+
"command": "codexa",
|
|
36
|
+
"args": ["mcp", "--path", "/your/project"]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
"""
|
|
41
|
+
root = Path(path).resolve()
|
|
42
|
+
config_dir = AppConfig.config_dir(root)
|
|
43
|
+
|
|
44
|
+
if not config_dir.exists():
|
|
45
|
+
print_error(f"Project not initialized at {root}. Run 'codexa init' first.")
|
|
46
|
+
ctx.exit(1)
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
from semantic_code_intelligence.mcp import run_mcp_server
|
|
50
|
+
run_mcp_server(root)
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""CLI command: metrics — code quality metrics, snapshots, and trends."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json as json_mod
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
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
|
+
print_success,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from semantic_code_intelligence.ci.metrics import MetricSnapshot, TrendResult, ProjectMetrics
|
|
21
|
+
|
|
22
|
+
logger = get_logger("cli.metrics")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ------------------------------------------------------------------
|
|
26
|
+
# Output helpers
|
|
27
|
+
# ------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _output_history(
|
|
31
|
+
snaps: list["MetricSnapshot"],
|
|
32
|
+
limit: int,
|
|
33
|
+
*,
|
|
34
|
+
json_mode: bool,
|
|
35
|
+
pipe: bool,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Emit snapshot history in the requested format."""
|
|
38
|
+
if json_mode:
|
|
39
|
+
click.echo(json_mod.dumps(
|
|
40
|
+
{"snapshots": [s.to_dict() for s in snaps]},
|
|
41
|
+
indent=2,
|
|
42
|
+
))
|
|
43
|
+
elif pipe:
|
|
44
|
+
for s in snaps:
|
|
45
|
+
click.echo(
|
|
46
|
+
f" {s.timestamp:.0f} MI={s.maintainability_index:.1f} "
|
|
47
|
+
f"LOC={s.total_loc} issues={s.issue_count}"
|
|
48
|
+
)
|
|
49
|
+
else:
|
|
50
|
+
if not snaps:
|
|
51
|
+
print_info("No snapshots found — run with --snapshot to save one.")
|
|
52
|
+
return
|
|
53
|
+
console.print(f"\n[bold cyan]Quality Snapshots[/bold cyan] (last {len(snaps)})\n")
|
|
54
|
+
for s in snaps:
|
|
55
|
+
import datetime
|
|
56
|
+
ts = datetime.datetime.fromtimestamp(s.timestamp).strftime("%Y-%m-%d %H:%M")
|
|
57
|
+
console.print(
|
|
58
|
+
f" {ts} MI=[bold]{s.maintainability_index:.1f}[/bold] "
|
|
59
|
+
f"LOC={s.total_loc} issues={s.issue_count}"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _output_trend(
|
|
64
|
+
results: list["TrendResult"],
|
|
65
|
+
snap_count: int,
|
|
66
|
+
*,
|
|
67
|
+
json_mode: bool,
|
|
68
|
+
pipe: bool,
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Emit trend analysis in the requested format."""
|
|
71
|
+
if json_mode:
|
|
72
|
+
click.echo(json_mod.dumps(
|
|
73
|
+
{"trends": [t.to_dict() for t in results]},
|
|
74
|
+
indent=2,
|
|
75
|
+
))
|
|
76
|
+
elif pipe:
|
|
77
|
+
for t in results:
|
|
78
|
+
click.echo(
|
|
79
|
+
f" TREND {t.metric_name} {t.direction} "
|
|
80
|
+
f"oldest={t.oldest_value:.2f} newest={t.newest_value:.2f} "
|
|
81
|
+
f"delta={t.delta:+.2f}"
|
|
82
|
+
)
|
|
83
|
+
else:
|
|
84
|
+
console.print(f"\n[bold cyan]Quality Trends[/bold cyan] ({snap_count} snapshots)\n")
|
|
85
|
+
for t in results:
|
|
86
|
+
color = {"improving": "green", "degrading": "red", "stable": "yellow"}.get(t.direction, "white")
|
|
87
|
+
console.print(
|
|
88
|
+
f" {t.metric_name:<30} [{color}]{t.direction:>10}[/{color}] "
|
|
89
|
+
f"{t.oldest_value:.1f} -> {t.newest_value:.1f} ({t.delta:+.1f})"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _output_current_metrics(
|
|
94
|
+
pm: "ProjectMetrics",
|
|
95
|
+
root: Path,
|
|
96
|
+
saved: "MetricSnapshot | None",
|
|
97
|
+
*,
|
|
98
|
+
json_mode: bool,
|
|
99
|
+
pipe: bool,
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Emit current metrics in the requested format."""
|
|
102
|
+
if json_mode:
|
|
103
|
+
payload = pm.to_dict()
|
|
104
|
+
if saved:
|
|
105
|
+
payload["snapshot"] = saved.to_dict()
|
|
106
|
+
click.echo(json_mod.dumps(payload, indent=2))
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
if pipe:
|
|
110
|
+
click.echo(
|
|
111
|
+
f"Files: {pm.files_analyzed} LOC: {pm.total_loc} "
|
|
112
|
+
f"MI: {pm.maintainability_index:.1f} "
|
|
113
|
+
f"AvgCC: {pm.avg_complexity:.1f} MaxCC: {pm.max_complexity}"
|
|
114
|
+
)
|
|
115
|
+
if saved:
|
|
116
|
+
click.echo(f"Snapshot saved at {saved.timestamp:.0f}")
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
# Rich output
|
|
120
|
+
console.print(f"\n[bold cyan]Quality Metrics[/bold cyan] — {root}\n")
|
|
121
|
+
console.print(f" Files analyzed: {pm.files_analyzed}")
|
|
122
|
+
console.print(f" Lines of code: {pm.total_loc}")
|
|
123
|
+
console.print(f" Comment lines: {pm.total_comment_lines}")
|
|
124
|
+
console.print(f" Comment ratio: {pm.comment_ratio:.1%}")
|
|
125
|
+
console.print(f" Symbols: {pm.total_symbols}")
|
|
126
|
+
console.print(f" Avg complexity: {pm.avg_complexity:.1f}")
|
|
127
|
+
console.print(f" Max complexity: {pm.max_complexity}")
|
|
128
|
+
console.print(f" Maintainability index: [bold]{pm.maintainability_index:.1f}[/bold]")
|
|
129
|
+
|
|
130
|
+
if saved:
|
|
131
|
+
print_success("Snapshot saved")
|
|
132
|
+
|
|
133
|
+
if pm.file_metrics:
|
|
134
|
+
console.print(f"\n[bold]Per-File Maintainability:[/bold]")
|
|
135
|
+
ranked = sorted(pm.file_metrics, key=lambda f: f.maintainability_index)
|
|
136
|
+
for fm in ranked[:10]:
|
|
137
|
+
mi = fm.maintainability_index
|
|
138
|
+
color = "green" if mi >= 65 else ("yellow" if mi >= 40 else "red")
|
|
139
|
+
name = Path(fm.file_path).name
|
|
140
|
+
console.print(f" [{color}]{mi:5.1f}[/{color}] {name} (LOC={fm.lines_of_code}, CC={fm.avg_complexity:.1f})")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@click.command("metrics")
|
|
144
|
+
@click.option(
|
|
145
|
+
"--path",
|
|
146
|
+
"-p",
|
|
147
|
+
default=".",
|
|
148
|
+
type=click.Path(exists=True, file_okay=False, resolve_path=True),
|
|
149
|
+
help="Project root path.",
|
|
150
|
+
)
|
|
151
|
+
@click.option(
|
|
152
|
+
"--json-output",
|
|
153
|
+
"--json",
|
|
154
|
+
"json_mode",
|
|
155
|
+
is_flag=True,
|
|
156
|
+
default=False,
|
|
157
|
+
help="Output in JSON format.",
|
|
158
|
+
)
|
|
159
|
+
@click.option(
|
|
160
|
+
"--pipe",
|
|
161
|
+
is_flag=True,
|
|
162
|
+
default=False,
|
|
163
|
+
help="Plain text output for piping / CI.",
|
|
164
|
+
)
|
|
165
|
+
@click.option(
|
|
166
|
+
"--snapshot",
|
|
167
|
+
is_flag=True,
|
|
168
|
+
default=False,
|
|
169
|
+
help="Save a quality snapshot after computing metrics.",
|
|
170
|
+
)
|
|
171
|
+
@click.option(
|
|
172
|
+
"--history",
|
|
173
|
+
type=int,
|
|
174
|
+
default=0,
|
|
175
|
+
help="Show last N snapshots (0 = skip history).",
|
|
176
|
+
)
|
|
177
|
+
@click.option(
|
|
178
|
+
"--trend",
|
|
179
|
+
is_flag=True,
|
|
180
|
+
default=False,
|
|
181
|
+
help="Show trend analysis from historical snapshots.",
|
|
182
|
+
)
|
|
183
|
+
@click.pass_context
|
|
184
|
+
def metrics_cmd(
|
|
185
|
+
ctx: click.Context,
|
|
186
|
+
path: str,
|
|
187
|
+
json_mode: bool,
|
|
188
|
+
pipe: bool,
|
|
189
|
+
snapshot: bool,
|
|
190
|
+
history: int,
|
|
191
|
+
trend: bool,
|
|
192
|
+
) -> None:
|
|
193
|
+
"""Compute code quality metrics, save snapshots, and track trends.
|
|
194
|
+
|
|
195
|
+
Calculates maintainability index, LOC, complexity, and comment ratios.
|
|
196
|
+
Supports saving metric snapshots for historical trend analysis.
|
|
197
|
+
|
|
198
|
+
Examples:
|
|
199
|
+
|
|
200
|
+
codexa metrics
|
|
201
|
+
|
|
202
|
+
codexa metrics --snapshot --json
|
|
203
|
+
|
|
204
|
+
codexa metrics --history 10
|
|
205
|
+
|
|
206
|
+
codexa metrics --trend
|
|
207
|
+
"""
|
|
208
|
+
from semantic_code_intelligence.ci.metrics import (
|
|
209
|
+
compute_project_metrics,
|
|
210
|
+
compute_trend,
|
|
211
|
+
load_snapshots,
|
|
212
|
+
save_snapshot,
|
|
213
|
+
)
|
|
214
|
+
from semantic_code_intelligence.ci.quality import analyze_project
|
|
215
|
+
|
|
216
|
+
root = Path(path).resolve()
|
|
217
|
+
|
|
218
|
+
# ── History-only mode ────────────────────────────────────────
|
|
219
|
+
if history > 0 and not trend:
|
|
220
|
+
snaps = load_snapshots(root, limit=history)
|
|
221
|
+
_output_history(snaps, history, json_mode=json_mode, pipe=pipe)
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
# ── Trend-only mode ──────────────────────────────────────────
|
|
225
|
+
if trend:
|
|
226
|
+
limit = history if history > 0 else 50
|
|
227
|
+
snaps = load_snapshots(root, limit=limit)
|
|
228
|
+
if len(snaps) < 2:
|
|
229
|
+
print_info("Need at least 2 snapshots for trend — run with --snapshot first.")
|
|
230
|
+
return
|
|
231
|
+
|
|
232
|
+
metrics_to_track = [
|
|
233
|
+
("maintainability_index", True),
|
|
234
|
+
("avg_complexity", False),
|
|
235
|
+
("issue_count", False),
|
|
236
|
+
("total_loc", True),
|
|
237
|
+
]
|
|
238
|
+
results = []
|
|
239
|
+
for metric, higher in metrics_to_track:
|
|
240
|
+
t = compute_trend(snaps, metric, higher_is_better=higher)
|
|
241
|
+
results.append(t)
|
|
242
|
+
|
|
243
|
+
_output_trend(results, len(snaps), json_mode=json_mode, pipe=pipe)
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
# ── Compute current metrics ──────────────────────────────────
|
|
247
|
+
try:
|
|
248
|
+
pm = compute_project_metrics(root)
|
|
249
|
+
except Exception as exc:
|
|
250
|
+
logger.debug("Metrics computation failed", exc_info=True)
|
|
251
|
+
print_error(f"Failed to compute metrics: {exc}")
|
|
252
|
+
ctx.exit(1)
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
saved = None
|
|
256
|
+
if snapshot:
|
|
257
|
+
try:
|
|
258
|
+
report = analyze_project(root)
|
|
259
|
+
saved = save_snapshot(root, pm, report)
|
|
260
|
+
except Exception as exc:
|
|
261
|
+
logger.debug("Snapshot save failed", exc_info=True)
|
|
262
|
+
print_error(f"Failed to save snapshot: {exc}")
|
|
263
|
+
|
|
264
|
+
_output_current_metrics(pm, root, saved, json_mode=json_mode, pipe=pipe)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""CLI command: models — download, list, switch, and inspect embedding models."""
|
|
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 AppConfig, load_config, save_config
|
|
11
|
+
from semantic_code_intelligence.embeddings.model_registry import (
|
|
12
|
+
AVAILABLE_MODELS,
|
|
13
|
+
DEFAULT_MODEL,
|
|
14
|
+
MODEL_ALIASES,
|
|
15
|
+
get_model_info,
|
|
16
|
+
list_models,
|
|
17
|
+
resolve_model_name,
|
|
18
|
+
)
|
|
19
|
+
from semantic_code_intelligence.utils.logging import (
|
|
20
|
+
console,
|
|
21
|
+
get_logger,
|
|
22
|
+
print_error,
|
|
23
|
+
print_info,
|
|
24
|
+
print_success,
|
|
25
|
+
print_warning,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
logger = get_logger("cli.models")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@click.group("models")
|
|
32
|
+
def models_cmd() -> None:
|
|
33
|
+
"""Manage embedding models — download, list, switch, info."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@models_cmd.command("list")
|
|
37
|
+
@click.option("--json-output", "--json", "json_mode", is_flag=True, help="Output as JSON.")
|
|
38
|
+
def models_list(json_mode: bool) -> None:
|
|
39
|
+
"""List all available embedding models and their properties."""
|
|
40
|
+
all_models = list_models()
|
|
41
|
+
|
|
42
|
+
if json_mode:
|
|
43
|
+
out = [
|
|
44
|
+
{
|
|
45
|
+
"name": m.name,
|
|
46
|
+
"display_name": m.display_name,
|
|
47
|
+
"dimension": m.dimension,
|
|
48
|
+
"description": m.description,
|
|
49
|
+
"recommended_for": m.recommended_for,
|
|
50
|
+
"backend": m.backend,
|
|
51
|
+
"is_default": m.name == DEFAULT_MODEL,
|
|
52
|
+
}
|
|
53
|
+
for m in all_models
|
|
54
|
+
]
|
|
55
|
+
click.echo(json.dumps(out, indent=2, ensure_ascii=False))
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
from rich.table import Table
|
|
59
|
+
|
|
60
|
+
table = Table(title="Available Embedding Models", show_lines=True)
|
|
61
|
+
table.add_column("Name", style="bold cyan", no_wrap=True)
|
|
62
|
+
table.add_column("Alias", style="dim")
|
|
63
|
+
table.add_column("Dim", justify="right")
|
|
64
|
+
table.add_column("Description")
|
|
65
|
+
table.add_column("Default", justify="center")
|
|
66
|
+
|
|
67
|
+
alias_reverse: dict[str, str] = {}
|
|
68
|
+
for alias, full in MODEL_ALIASES.items():
|
|
69
|
+
alias_reverse.setdefault(full, alias)
|
|
70
|
+
|
|
71
|
+
for m in all_models:
|
|
72
|
+
alias = alias_reverse.get(m.name, "—")
|
|
73
|
+
is_default = "✓" if m.name == DEFAULT_MODEL else ""
|
|
74
|
+
table.add_row(m.name, alias, str(m.dimension), m.description, is_default)
|
|
75
|
+
|
|
76
|
+
console.print(table)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@models_cmd.command("info")
|
|
80
|
+
@click.argument("model_name")
|
|
81
|
+
def models_info(model_name: str) -> None:
|
|
82
|
+
"""Show detailed information about a specific model."""
|
|
83
|
+
info = get_model_info(model_name)
|
|
84
|
+
if info is None:
|
|
85
|
+
print_error(f"Unknown model: {model_name}")
|
|
86
|
+
raise SystemExit(1)
|
|
87
|
+
|
|
88
|
+
from rich.panel import Panel
|
|
89
|
+
from rich.text import Text
|
|
90
|
+
|
|
91
|
+
body = Text()
|
|
92
|
+
body.append(f"Name: ", style="bold")
|
|
93
|
+
body.append(f"{info.name}\n")
|
|
94
|
+
body.append(f"Display name: ", style="bold")
|
|
95
|
+
body.append(f"{info.display_name}\n")
|
|
96
|
+
body.append(f"Dimension: ", style="bold")
|
|
97
|
+
body.append(f"{info.dimension}\n")
|
|
98
|
+
body.append(f"Backend: ", style="bold")
|
|
99
|
+
body.append(f"{info.backend}\n")
|
|
100
|
+
body.append(f"Recommended for: ", style="bold")
|
|
101
|
+
body.append(f"{info.recommended_for}\n")
|
|
102
|
+
body.append(f"Description: ", style="bold")
|
|
103
|
+
body.append(info.description)
|
|
104
|
+
|
|
105
|
+
console.print(Panel(body, title=f"[bold]{info.display_name}[/bold]", border_style="cyan"))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@models_cmd.command("download")
|
|
109
|
+
@click.argument("model_name")
|
|
110
|
+
@click.option("--backend", type=click.Choice(["auto", "onnx", "torch"]), default="auto")
|
|
111
|
+
def models_download(model_name: str, backend: str) -> None:
|
|
112
|
+
"""Pre-download a model so it is cached locally for offline use."""
|
|
113
|
+
resolved = resolve_model_name(model_name)
|
|
114
|
+
print_info(f"Downloading model: {resolved} (backend={backend}) ...")
|
|
115
|
+
|
|
116
|
+
from semantic_code_intelligence.embeddings.generator import get_model
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
model = get_model(resolved, backend=backend)
|
|
120
|
+
dim = model.get_sentence_embedding_dimension()
|
|
121
|
+
print_success(f"Model '{resolved}' ready — dimension={dim}")
|
|
122
|
+
except Exception as exc:
|
|
123
|
+
print_error(f"Failed to download model: {exc}")
|
|
124
|
+
raise SystemExit(1) from exc
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@models_cmd.command("switch")
|
|
128
|
+
@click.argument("model_name")
|
|
129
|
+
@click.option(
|
|
130
|
+
"--path",
|
|
131
|
+
"-p",
|
|
132
|
+
default=".",
|
|
133
|
+
type=click.Path(exists=True, file_okay=False, resolve_path=True),
|
|
134
|
+
help="Project root path.",
|
|
135
|
+
)
|
|
136
|
+
def models_switch(model_name: str, path: str) -> None:
|
|
137
|
+
"""Switch the active embedding model for a project.
|
|
138
|
+
|
|
139
|
+
Note: after switching models you must re-index (codexa index --reindex).
|
|
140
|
+
"""
|
|
141
|
+
resolved = resolve_model_name(model_name)
|
|
142
|
+
info = get_model_info(resolved)
|
|
143
|
+
if info is None:
|
|
144
|
+
print_warning(f"Model '{resolved}' is not in the built-in catalogue — using as custom HF model.")
|
|
145
|
+
|
|
146
|
+
root = Path(path).resolve()
|
|
147
|
+
config_dir = AppConfig.config_dir(root)
|
|
148
|
+
if not config_dir.exists():
|
|
149
|
+
print_error(f"Project not initialized at {root}. Run 'codexa init' first.")
|
|
150
|
+
raise SystemExit(1)
|
|
151
|
+
|
|
152
|
+
config = load_config(root)
|
|
153
|
+
old_model = config.embedding.model_name
|
|
154
|
+
config.embedding.model_name = resolved
|
|
155
|
+
save_config(config, root)
|
|
156
|
+
print_success(f"Switched model: {old_model} → {resolved}")
|
|
157
|
+
print_info("Run 'codexa index --reindex' to rebuild the index with the new model.")
|