codexa 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. codexa-0.4.0.dist-info/METADATA +650 -0
  2. codexa-0.4.0.dist-info/RECORD +189 -0
  3. codexa-0.4.0.dist-info/WHEEL +5 -0
  4. codexa-0.4.0.dist-info/entry_points.txt +2 -0
  5. codexa-0.4.0.dist-info/licenses/LICENSE +21 -0
  6. codexa-0.4.0.dist-info/top_level.txt +1 -0
  7. semantic_code_intelligence/__init__.py +5 -0
  8. semantic_code_intelligence/analysis/__init__.py +21 -0
  9. semantic_code_intelligence/analysis/ai_features.py +351 -0
  10. semantic_code_intelligence/bridge/__init__.py +28 -0
  11. semantic_code_intelligence/bridge/context_provider.py +245 -0
  12. semantic_code_intelligence/bridge/protocol.py +167 -0
  13. semantic_code_intelligence/bridge/server.py +348 -0
  14. semantic_code_intelligence/bridge/vscode.py +271 -0
  15. semantic_code_intelligence/ci/__init__.py +13 -0
  16. semantic_code_intelligence/ci/hooks.py +98 -0
  17. semantic_code_intelligence/ci/hotspots.py +272 -0
  18. semantic_code_intelligence/ci/impact.py +246 -0
  19. semantic_code_intelligence/ci/metrics.py +591 -0
  20. semantic_code_intelligence/ci/pr.py +412 -0
  21. semantic_code_intelligence/ci/quality.py +557 -0
  22. semantic_code_intelligence/ci/templates.py +164 -0
  23. semantic_code_intelligence/ci/trace.py +224 -0
  24. semantic_code_intelligence/cli/__init__.py +0 -0
  25. semantic_code_intelligence/cli/commands/__init__.py +0 -0
  26. semantic_code_intelligence/cli/commands/ask_cmd.py +153 -0
  27. semantic_code_intelligence/cli/commands/benchmark_cmd.py +303 -0
  28. semantic_code_intelligence/cli/commands/chat_cmd.py +252 -0
  29. semantic_code_intelligence/cli/commands/ci_gen_cmd.py +74 -0
  30. semantic_code_intelligence/cli/commands/context_cmd.py +120 -0
  31. semantic_code_intelligence/cli/commands/cross_refactor_cmd.py +113 -0
  32. semantic_code_intelligence/cli/commands/deps_cmd.py +91 -0
  33. semantic_code_intelligence/cli/commands/docs_cmd.py +101 -0
  34. semantic_code_intelligence/cli/commands/doctor_cmd.py +147 -0
  35. semantic_code_intelligence/cli/commands/evolve_cmd.py +171 -0
  36. semantic_code_intelligence/cli/commands/explain_cmd.py +112 -0
  37. semantic_code_intelligence/cli/commands/gate_cmd.py +135 -0
  38. semantic_code_intelligence/cli/commands/grep_cmd.py +234 -0
  39. semantic_code_intelligence/cli/commands/hotspots_cmd.py +119 -0
  40. semantic_code_intelligence/cli/commands/impact_cmd.py +131 -0
  41. semantic_code_intelligence/cli/commands/index_cmd.py +138 -0
  42. semantic_code_intelligence/cli/commands/init_cmd.py +152 -0
  43. semantic_code_intelligence/cli/commands/investigate_cmd.py +163 -0
  44. semantic_code_intelligence/cli/commands/languages_cmd.py +101 -0
  45. semantic_code_intelligence/cli/commands/lsp_cmd.py +49 -0
  46. semantic_code_intelligence/cli/commands/mcp_cmd.py +50 -0
  47. semantic_code_intelligence/cli/commands/metrics_cmd.py +264 -0
  48. semantic_code_intelligence/cli/commands/models_cmd.py +157 -0
  49. semantic_code_intelligence/cli/commands/plugin_cmd.py +275 -0
  50. semantic_code_intelligence/cli/commands/pr_summary_cmd.py +178 -0
  51. semantic_code_intelligence/cli/commands/quality_cmd.py +208 -0
  52. semantic_code_intelligence/cli/commands/refactor_cmd.py +103 -0
  53. semantic_code_intelligence/cli/commands/review_cmd.py +88 -0
  54. semantic_code_intelligence/cli/commands/search_cmd.py +236 -0
  55. semantic_code_intelligence/cli/commands/serve_cmd.py +117 -0
  56. semantic_code_intelligence/cli/commands/suggest_cmd.py +100 -0
  57. semantic_code_intelligence/cli/commands/summary_cmd.py +78 -0
  58. semantic_code_intelligence/cli/commands/tool_cmd.py +282 -0
  59. semantic_code_intelligence/cli/commands/trace_cmd.py +123 -0
  60. semantic_code_intelligence/cli/commands/tui_cmd.py +58 -0
  61. semantic_code_intelligence/cli/commands/viz_cmd.py +127 -0
  62. semantic_code_intelligence/cli/commands/watch_cmd.py +72 -0
  63. semantic_code_intelligence/cli/commands/web_cmd.py +61 -0
  64. semantic_code_intelligence/cli/commands/workspace_cmd.py +250 -0
  65. semantic_code_intelligence/cli/main.py +65 -0
  66. semantic_code_intelligence/cli/router.py +92 -0
  67. semantic_code_intelligence/config/__init__.py +0 -0
  68. semantic_code_intelligence/config/settings.py +260 -0
  69. semantic_code_intelligence/context/__init__.py +19 -0
  70. semantic_code_intelligence/context/engine.py +429 -0
  71. semantic_code_intelligence/context/memory.py +253 -0
  72. semantic_code_intelligence/daemon/__init__.py +1 -0
  73. semantic_code_intelligence/daemon/watcher.py +515 -0
  74. semantic_code_intelligence/docs/__init__.py +1080 -0
  75. semantic_code_intelligence/embeddings/__init__.py +0 -0
  76. semantic_code_intelligence/embeddings/enhanced.py +131 -0
  77. semantic_code_intelligence/embeddings/generator.py +149 -0
  78. semantic_code_intelligence/embeddings/model_registry.py +100 -0
  79. semantic_code_intelligence/evolution/__init__.py +1 -0
  80. semantic_code_intelligence/evolution/budget_guard.py +111 -0
  81. semantic_code_intelligence/evolution/commit_manager.py +88 -0
  82. semantic_code_intelligence/evolution/context_builder.py +131 -0
  83. semantic_code_intelligence/evolution/engine.py +249 -0
  84. semantic_code_intelligence/evolution/patch_generator.py +229 -0
  85. semantic_code_intelligence/evolution/task_selector.py +214 -0
  86. semantic_code_intelligence/evolution/test_runner.py +111 -0
  87. semantic_code_intelligence/indexing/__init__.py +0 -0
  88. semantic_code_intelligence/indexing/chunker.py +174 -0
  89. semantic_code_intelligence/indexing/parallel.py +86 -0
  90. semantic_code_intelligence/indexing/scanner.py +146 -0
  91. semantic_code_intelligence/indexing/semantic_chunker.py +337 -0
  92. semantic_code_intelligence/llm/__init__.py +62 -0
  93. semantic_code_intelligence/llm/cache.py +219 -0
  94. semantic_code_intelligence/llm/cached_provider.py +145 -0
  95. semantic_code_intelligence/llm/conversation.py +190 -0
  96. semantic_code_intelligence/llm/cross_refactor.py +272 -0
  97. semantic_code_intelligence/llm/investigation.py +274 -0
  98. semantic_code_intelligence/llm/mock_provider.py +77 -0
  99. semantic_code_intelligence/llm/ollama_provider.py +122 -0
  100. semantic_code_intelligence/llm/openai_provider.py +100 -0
  101. semantic_code_intelligence/llm/provider.py +92 -0
  102. semantic_code_intelligence/llm/rate_limiter.py +164 -0
  103. semantic_code_intelligence/llm/reasoning.py +438 -0
  104. semantic_code_intelligence/llm/safety.py +110 -0
  105. semantic_code_intelligence/llm/streaming.py +251 -0
  106. semantic_code_intelligence/lsp/__init__.py +609 -0
  107. semantic_code_intelligence/mcp/__init__.py +393 -0
  108. semantic_code_intelligence/parsing/__init__.py +19 -0
  109. semantic_code_intelligence/parsing/parser.py +375 -0
  110. semantic_code_intelligence/plugins/__init__.py +255 -0
  111. semantic_code_intelligence/plugins/examples/__init__.py +1 -0
  112. semantic_code_intelligence/plugins/examples/code_quality.py +73 -0
  113. semantic_code_intelligence/plugins/examples/search_annotator.py +56 -0
  114. semantic_code_intelligence/scalability/__init__.py +205 -0
  115. semantic_code_intelligence/search/__init__.py +0 -0
  116. semantic_code_intelligence/search/formatter.py +123 -0
  117. semantic_code_intelligence/search/grep.py +361 -0
  118. semantic_code_intelligence/search/hybrid_search.py +170 -0
  119. semantic_code_intelligence/search/keyword_search.py +311 -0
  120. semantic_code_intelligence/search/section_expander.py +103 -0
  121. semantic_code_intelligence/services/__init__.py +0 -0
  122. semantic_code_intelligence/services/indexing_service.py +630 -0
  123. semantic_code_intelligence/services/search_service.py +269 -0
  124. semantic_code_intelligence/storage/__init__.py +0 -0
  125. semantic_code_intelligence/storage/chunk_hash_store.py +86 -0
  126. semantic_code_intelligence/storage/hash_store.py +66 -0
  127. semantic_code_intelligence/storage/index_manifest.py +85 -0
  128. semantic_code_intelligence/storage/index_stats.py +138 -0
  129. semantic_code_intelligence/storage/query_history.py +160 -0
  130. semantic_code_intelligence/storage/symbol_registry.py +209 -0
  131. semantic_code_intelligence/storage/vector_store.py +297 -0
  132. semantic_code_intelligence/tests/__init__.py +0 -0
  133. semantic_code_intelligence/tests/test_ai_features.py +351 -0
  134. semantic_code_intelligence/tests/test_chunker.py +119 -0
  135. semantic_code_intelligence/tests/test_cli.py +188 -0
  136. semantic_code_intelligence/tests/test_config.py +154 -0
  137. semantic_code_intelligence/tests/test_context.py +381 -0
  138. semantic_code_intelligence/tests/test_embeddings.py +73 -0
  139. semantic_code_intelligence/tests/test_endtoend.py +1142 -0
  140. semantic_code_intelligence/tests/test_enhanced_embeddings.py +92 -0
  141. semantic_code_intelligence/tests/test_hash_store.py +79 -0
  142. semantic_code_intelligence/tests/test_logging.py +55 -0
  143. semantic_code_intelligence/tests/test_new_cli.py +138 -0
  144. semantic_code_intelligence/tests/test_parser.py +495 -0
  145. semantic_code_intelligence/tests/test_phase10.py +355 -0
  146. semantic_code_intelligence/tests/test_phase11.py +593 -0
  147. semantic_code_intelligence/tests/test_phase12.py +375 -0
  148. semantic_code_intelligence/tests/test_phase13.py +663 -0
  149. semantic_code_intelligence/tests/test_phase14.py +568 -0
  150. semantic_code_intelligence/tests/test_phase15.py +814 -0
  151. semantic_code_intelligence/tests/test_phase16.py +792 -0
  152. semantic_code_intelligence/tests/test_phase17.py +815 -0
  153. semantic_code_intelligence/tests/test_phase18.py +934 -0
  154. semantic_code_intelligence/tests/test_phase19.py +986 -0
  155. semantic_code_intelligence/tests/test_phase20.py +2753 -0
  156. semantic_code_intelligence/tests/test_phase20b.py +2058 -0
  157. semantic_code_intelligence/tests/test_phase20c.py +962 -0
  158. semantic_code_intelligence/tests/test_phase21.py +428 -0
  159. semantic_code_intelligence/tests/test_phase22.py +799 -0
  160. semantic_code_intelligence/tests/test_phase23.py +783 -0
  161. semantic_code_intelligence/tests/test_phase24.py +715 -0
  162. semantic_code_intelligence/tests/test_phase25.py +496 -0
  163. semantic_code_intelligence/tests/test_phase26.py +251 -0
  164. semantic_code_intelligence/tests/test_phase27.py +531 -0
  165. semantic_code_intelligence/tests/test_phase8.py +592 -0
  166. semantic_code_intelligence/tests/test_phase9.py +643 -0
  167. semantic_code_intelligence/tests/test_plugins.py +293 -0
  168. semantic_code_intelligence/tests/test_priority_features.py +727 -0
  169. semantic_code_intelligence/tests/test_router.py +41 -0
  170. semantic_code_intelligence/tests/test_scalability.py +138 -0
  171. semantic_code_intelligence/tests/test_scanner.py +125 -0
  172. semantic_code_intelligence/tests/test_search.py +160 -0
  173. semantic_code_intelligence/tests/test_semantic_chunker.py +255 -0
  174. semantic_code_intelligence/tests/test_tools.py +182 -0
  175. semantic_code_intelligence/tests/test_vector_store.py +151 -0
  176. semantic_code_intelligence/tests/test_watcher.py +211 -0
  177. semantic_code_intelligence/tools/__init__.py +442 -0
  178. semantic_code_intelligence/tools/executor.py +232 -0
  179. semantic_code_intelligence/tools/protocol.py +200 -0
  180. semantic_code_intelligence/tui/__init__.py +454 -0
  181. semantic_code_intelligence/utils/__init__.py +0 -0
  182. semantic_code_intelligence/utils/logging.py +112 -0
  183. semantic_code_intelligence/version.py +3 -0
  184. semantic_code_intelligence/web/__init__.py +11 -0
  185. semantic_code_intelligence/web/api.py +289 -0
  186. semantic_code_intelligence/web/server.py +397 -0
  187. semantic_code_intelligence/web/ui.py +659 -0
  188. semantic_code_intelligence/web/visualize.py +226 -0
  189. semantic_code_intelligence/workspace/__init__.py +427 -0
@@ -0,0 +1,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.")