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,815 @@
1
+ """Tests for Phase 17 — Code Quality Metrics & Trends.
2
+
3
+ Covers: file metrics, project metrics, maintainability index, quality snapshots,
4
+ trend analysis, quality policies, gate enforcement, CLI commands,
5
+ router, version, docs, and module structure.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import time
12
+ from pathlib import Path
13
+ from unittest.mock import patch
14
+
15
+ import pytest
16
+ from click.testing import CliRunner
17
+
18
+
19
+ # =========================================================================
20
+ # Helper: create sample Python files
21
+ # =========================================================================
22
+
23
+
24
+ def _write_sample_project(root: Path) -> None:
25
+ """Write a small multi-file project for metrics testing."""
26
+ src = root / "src"
27
+ src.mkdir(parents=True, exist_ok=True)
28
+
29
+ (src / "simple.py").write_text(
30
+ '# Simple module\n'
31
+ 'def greet(name: str) -> str:\n'
32
+ ' """Return a greeting."""\n'
33
+ ' return f"Hello, {name}!"\n'
34
+ '\n'
35
+ 'def add(a: int, b: int) -> int:\n'
36
+ ' return a + b\n',
37
+ encoding="utf-8",
38
+ )
39
+
40
+ (src / "complex.py").write_text(
41
+ 'def process(data):\n'
42
+ ' if not data:\n'
43
+ ' return None\n'
44
+ ' result = []\n'
45
+ ' for item in data:\n'
46
+ ' if isinstance(item, dict):\n'
47
+ ' if "key" in item:\n'
48
+ ' if item["key"] > 0:\n'
49
+ ' result.append(item)\n'
50
+ ' else:\n'
51
+ ' result.append(None)\n'
52
+ ' elif "alt" in item:\n'
53
+ ' result.append(item["alt"])\n'
54
+ ' elif isinstance(item, list):\n'
55
+ ' result.extend(item)\n'
56
+ ' else:\n'
57
+ ' result.append(item)\n'
58
+ ' return result\n',
59
+ encoding="utf-8",
60
+ )
61
+
62
+ (src / "empty.py").write_text("", encoding="utf-8")
63
+
64
+
65
+ # =========================================================================
66
+ # FileMetrics tests
67
+ # =========================================================================
68
+
69
+
70
+ class TestFileMetrics:
71
+ """Tests for per-file metric computation."""
72
+
73
+ def test_basic_metrics(self, tmp_path):
74
+ from semantic_code_intelligence.ci.metrics import compute_file_metrics
75
+
76
+ _write_sample_project(tmp_path)
77
+ fm = compute_file_metrics(tmp_path / "src" / "simple.py")
78
+
79
+ assert fm.file_path.endswith("simple.py")
80
+ assert fm.lines_of_code > 0
81
+ assert fm.comment_lines >= 1 # '# Simple module'
82
+ assert fm.symbol_count >= 2 # greet, add
83
+ assert 0 <= fm.maintainability_index <= 100
84
+
85
+ def test_empty_file(self, tmp_path):
86
+ from semantic_code_intelligence.ci.metrics import compute_file_metrics
87
+
88
+ _write_sample_project(tmp_path)
89
+ fm = compute_file_metrics(tmp_path / "src" / "empty.py")
90
+
91
+ assert fm.lines_of_code == 0
92
+ assert fm.maintainability_index >= 0
93
+
94
+ def test_nonexistent_file(self, tmp_path):
95
+ from semantic_code_intelligence.ci.metrics import compute_file_metrics
96
+
97
+ fm = compute_file_metrics(tmp_path / "nope.py")
98
+ assert fm.lines_of_code == 0
99
+
100
+ def test_comment_ratio_property(self, tmp_path):
101
+ from semantic_code_intelligence.ci.metrics import compute_file_metrics
102
+
103
+ _write_sample_project(tmp_path)
104
+ fm = compute_file_metrics(tmp_path / "src" / "simple.py")
105
+ assert 0.0 <= fm.comment_ratio <= 1.0
106
+
107
+ def test_to_dict(self, tmp_path):
108
+ from semantic_code_intelligence.ci.metrics import compute_file_metrics
109
+
110
+ _write_sample_project(tmp_path)
111
+ fm = compute_file_metrics(tmp_path / "src" / "simple.py")
112
+ d = fm.to_dict()
113
+
114
+ assert "file_path" in d
115
+ assert "lines_of_code" in d
116
+ assert "maintainability_index" in d
117
+ assert "comment_ratio" in d
118
+ assert isinstance(d["maintainability_index"], float)
119
+
120
+
121
+ # =========================================================================
122
+ # ProjectMetrics tests
123
+ # =========================================================================
124
+
125
+
126
+ class TestProjectMetrics:
127
+ """Tests for project-wide metric aggregation."""
128
+
129
+ def test_project_metrics(self, tmp_path):
130
+ from semantic_code_intelligence.ci.metrics import compute_project_metrics
131
+
132
+ _write_sample_project(tmp_path)
133
+ pm = compute_project_metrics(tmp_path)
134
+
135
+ assert pm.files_analyzed >= 2 # simple.py, complex.py (empty.py may be counted too)
136
+ assert pm.total_loc > 0
137
+ assert pm.total_symbols > 0
138
+ assert 0 <= pm.maintainability_index <= 100
139
+
140
+ def test_project_metrics_with_file_paths(self, tmp_path):
141
+ from semantic_code_intelligence.ci.metrics import compute_project_metrics
142
+
143
+ _write_sample_project(tmp_path)
144
+ files = [str(tmp_path / "src" / "simple.py")]
145
+ pm = compute_project_metrics(tmp_path, file_paths=files)
146
+
147
+ assert pm.files_analyzed == 1
148
+
149
+ def test_empty_project(self, tmp_path):
150
+ from semantic_code_intelligence.ci.metrics import compute_project_metrics
151
+
152
+ pm = compute_project_metrics(tmp_path)
153
+ assert pm.files_analyzed == 0
154
+ assert pm.maintainability_index >= 0
155
+
156
+ def test_to_dict(self, tmp_path):
157
+ from semantic_code_intelligence.ci.metrics import compute_project_metrics
158
+
159
+ _write_sample_project(tmp_path)
160
+ pm = compute_project_metrics(tmp_path)
161
+ d = pm.to_dict()
162
+
163
+ assert "files_analyzed" in d
164
+ assert "total_loc" in d
165
+ assert "maintainability_index" in d
166
+ assert "file_metrics" in d
167
+ assert isinstance(d["file_metrics"], list)
168
+
169
+ def test_comment_ratio(self, tmp_path):
170
+ from semantic_code_intelligence.ci.metrics import compute_project_metrics
171
+
172
+ _write_sample_project(tmp_path)
173
+ pm = compute_project_metrics(tmp_path)
174
+ assert 0.0 <= pm.comment_ratio <= 1.0
175
+
176
+
177
+ # =========================================================================
178
+ # Maintainability index computation
179
+ # =========================================================================
180
+
181
+
182
+ class TestMaintainabilityIndex:
183
+ """Tests for the MI formula."""
184
+
185
+ def test_mi_range(self):
186
+ from semantic_code_intelligence.ci.metrics import _compute_mi
187
+
188
+ # Simple code should have high MI
189
+ mi_simple = _compute_mi(10.0, 1.0, 0.3)
190
+ assert 0 <= mi_simple <= 100
191
+
192
+ # Complex code should have lower MI
193
+ mi_complex = _compute_mi(500.0, 20.0, 0.0)
194
+ assert 0 <= mi_complex <= 100
195
+ assert mi_complex < mi_simple
196
+
197
+ def test_mi_zero_loc(self):
198
+ from semantic_code_intelligence.ci.metrics import _compute_mi
199
+
200
+ mi = _compute_mi(0.0, 0.0, 0.0)
201
+ assert 0 <= mi <= 100
202
+
203
+ def test_mi_high_comments_helps(self):
204
+ from semantic_code_intelligence.ci.metrics import _compute_mi
205
+
206
+ mi_no_cm = _compute_mi(100.0, 5.0, 0.0)
207
+ mi_with_cm = _compute_mi(100.0, 5.0, 0.3)
208
+ assert mi_with_cm >= mi_no_cm
209
+
210
+
211
+ # =========================================================================
212
+ # Line counting helpers
213
+ # =========================================================================
214
+
215
+
216
+ class TestLineCounting:
217
+ """Tests for _count_lines helper."""
218
+
219
+ def test_count_python_lines(self):
220
+ from semantic_code_intelligence.ci.metrics import _count_lines
221
+
222
+ code = "# comment\ndef foo():\n pass\n\n"
223
+ loc, comments, blanks = _count_lines(code)
224
+ assert comments >= 1
225
+ assert loc >= 2 # def foo, pass
226
+ assert blanks >= 1
227
+
228
+ def test_count_js_comments(self):
229
+ from semantic_code_intelligence.ci.metrics import _count_lines
230
+
231
+ code = "// comment\nfunction foo() {\n}\n"
232
+ loc, comments, blanks = _count_lines(code)
233
+ assert comments >= 1
234
+
235
+ def test_empty_content(self):
236
+ from semantic_code_intelligence.ci.metrics import _count_lines
237
+
238
+ loc, comments, blanks = _count_lines("")
239
+ assert loc == 0
240
+ assert comments == 0
241
+ assert blanks == 0
242
+
243
+
244
+ # =========================================================================
245
+ # Quality snapshots
246
+ # =========================================================================
247
+
248
+
249
+ class TestQualitySnapshots:
250
+ """Tests for snapshot save/load via WorkspaceMemory."""
251
+
252
+ def test_save_snapshot(self, tmp_path):
253
+ from semantic_code_intelligence.ci.metrics import (
254
+ ProjectMetrics,
255
+ save_snapshot,
256
+ )
257
+ from semantic_code_intelligence.ci.quality import QualityReport
258
+
259
+ (tmp_path / ".codexa").mkdir(exist_ok=True)
260
+ pm = ProjectMetrics(
261
+ files_analyzed=5,
262
+ total_loc=200,
263
+ avg_complexity=3.5,
264
+ max_complexity=8,
265
+ total_symbols=20,
266
+ maintainability_index=72.5,
267
+ )
268
+ qr = QualityReport(files_analyzed=5, symbol_count=20)
269
+
270
+ snap = save_snapshot(tmp_path, pm, qr, metadata={"branch": "main"})
271
+ assert snap.timestamp > 0
272
+ assert snap.maintainability_index == 72.5
273
+ assert snap.metadata["branch"] == "main"
274
+
275
+ def test_save_and_load(self, tmp_path):
276
+ from semantic_code_intelligence.ci.metrics import (
277
+ ProjectMetrics,
278
+ load_snapshots,
279
+ save_snapshot,
280
+ )
281
+ from semantic_code_intelligence.ci.quality import QualityReport
282
+
283
+ (tmp_path / ".codexa").mkdir(exist_ok=True)
284
+ pm = ProjectMetrics(
285
+ files_analyzed=3,
286
+ total_loc=100,
287
+ avg_complexity=2.0,
288
+ max_complexity=5,
289
+ total_symbols=10,
290
+ maintainability_index=80.0,
291
+ )
292
+ qr = QualityReport(files_analyzed=3, symbol_count=10)
293
+
294
+ save_snapshot(tmp_path, pm, qr)
295
+ time.sleep(0.01)
296
+ save_snapshot(tmp_path, pm, qr)
297
+
298
+ snaps = load_snapshots(tmp_path, limit=10)
299
+ assert len(snaps) >= 2
300
+ # Newest first
301
+ assert snaps[0].timestamp >= snaps[1].timestamp
302
+
303
+ def test_load_empty(self, tmp_path):
304
+ from semantic_code_intelligence.ci.metrics import load_snapshots
305
+
306
+ (tmp_path / ".codexa").mkdir(exist_ok=True)
307
+ snaps = load_snapshots(tmp_path)
308
+ assert snaps == []
309
+
310
+ def test_snapshot_to_dict_roundtrip(self):
311
+ from semantic_code_intelligence.ci.metrics import QualitySnapshot
312
+
313
+ snap = QualitySnapshot(
314
+ timestamp=1234567890.0,
315
+ maintainability_index=65.0,
316
+ total_loc=500,
317
+ total_symbols=50,
318
+ issue_count=3,
319
+ files_analyzed=10,
320
+ avg_complexity=4.2,
321
+ comment_ratio=0.15,
322
+ metadata={"test": True},
323
+ )
324
+ d = snap.to_dict()
325
+ restored = QualitySnapshot.from_dict(d)
326
+ assert restored.timestamp == snap.timestamp
327
+ assert restored.maintainability_index == snap.maintainability_index
328
+ assert restored.metadata["test"] is True
329
+
330
+
331
+ # =========================================================================
332
+ # Trend analysis
333
+ # =========================================================================
334
+
335
+
336
+ class TestTrendAnalysis:
337
+ """Tests for trend computation over snapshots."""
338
+
339
+ def _make_snapshot(self, ts, mi, issues=0, cc=1.0, loc=100):
340
+ from semantic_code_intelligence.ci.metrics import QualitySnapshot
341
+
342
+ return QualitySnapshot(
343
+ timestamp=ts,
344
+ maintainability_index=mi,
345
+ total_loc=loc,
346
+ total_symbols=10,
347
+ issue_count=issues,
348
+ files_analyzed=5,
349
+ avg_complexity=cc,
350
+ comment_ratio=0.1,
351
+ )
352
+
353
+ def test_improving_trend(self):
354
+ from semantic_code_intelligence.ci.metrics import compute_trend
355
+
356
+ # MI getting better over time (newest first)
357
+ snaps = [
358
+ self._make_snapshot(3.0, 80.0),
359
+ self._make_snapshot(2.0, 60.0),
360
+ self._make_snapshot(1.0, 40.0),
361
+ ]
362
+ t = compute_trend(snaps, "maintainability_index", higher_is_better=True)
363
+ assert t.direction == "improving"
364
+ assert t.delta > 0
365
+
366
+ def test_degrading_trend(self):
367
+ from semantic_code_intelligence.ci.metrics import compute_trend
368
+
369
+ # MI getting worse
370
+ snaps = [
371
+ self._make_snapshot(3.0, 30.0),
372
+ self._make_snapshot(2.0, 50.0),
373
+ self._make_snapshot(1.0, 80.0),
374
+ ]
375
+ t = compute_trend(snaps, "maintainability_index", higher_is_better=True)
376
+ assert t.direction == "degrading"
377
+ assert t.delta < 0
378
+
379
+ def test_stable_trend(self):
380
+ from semantic_code_intelligence.ci.metrics import compute_trend
381
+
382
+ snaps = [
383
+ self._make_snapshot(3.0, 50.0),
384
+ self._make_snapshot(2.0, 50.0),
385
+ self._make_snapshot(1.0, 50.0),
386
+ ]
387
+ t = compute_trend(snaps, "maintainability_index")
388
+ assert t.direction == "stable"
389
+
390
+ def test_empty_snapshots(self):
391
+ from semantic_code_intelligence.ci.metrics import compute_trend
392
+
393
+ t = compute_trend([], "maintainability_index")
394
+ assert t.snapshot_count == 0
395
+ assert t.direction == "stable"
396
+
397
+ def test_single_snapshot(self):
398
+ from semantic_code_intelligence.ci.metrics import compute_trend
399
+
400
+ snaps = [self._make_snapshot(1.0, 50.0)]
401
+ t = compute_trend(snaps, "maintainability_index")
402
+ assert t.direction == "stable"
403
+ assert t.snapshot_count == 1
404
+
405
+ def test_trend_to_dict(self):
406
+ from semantic_code_intelligence.ci.metrics import compute_trend
407
+
408
+ snaps = [
409
+ self._make_snapshot(2.0, 70.0),
410
+ self._make_snapshot(1.0, 50.0),
411
+ ]
412
+ t = compute_trend(snaps, "maintainability_index")
413
+ d = t.to_dict()
414
+ assert "metric_name" in d
415
+ assert "direction" in d
416
+ assert "delta" in d
417
+
418
+
419
+ # =========================================================================
420
+ # Quality policy & gate enforcement
421
+ # =========================================================================
422
+
423
+
424
+ class TestQualityPolicy:
425
+ """Tests for QualityPolicy dataclass."""
426
+
427
+ def test_defaults(self):
428
+ from semantic_code_intelligence.ci.metrics import QualityPolicy
429
+
430
+ p = QualityPolicy()
431
+ assert p.min_maintainability == 40.0
432
+ assert p.max_complexity == 25
433
+ assert p.max_issues == 20
434
+
435
+ def test_to_dict_roundtrip(self):
436
+ from semantic_code_intelligence.ci.metrics import QualityPolicy
437
+
438
+ p = QualityPolicy(min_maintainability=50.0, max_complexity=15)
439
+ d = p.to_dict()
440
+ restored = QualityPolicy.from_dict(d)
441
+ assert restored.min_maintainability == 50.0
442
+ assert restored.max_complexity == 15
443
+
444
+
445
+ class TestGateEnforcement:
446
+ """Tests for enforce_quality_gate."""
447
+
448
+ def test_gate_pass(self):
449
+ from semantic_code_intelligence.ci.metrics import (
450
+ ProjectMetrics,
451
+ QualityPolicy,
452
+ enforce_quality_gate,
453
+ )
454
+ from semantic_code_intelligence.ci.quality import QualityReport
455
+
456
+ pm = ProjectMetrics(
457
+ maintainability_index=80.0,
458
+ max_complexity=5,
459
+ )
460
+ qr = QualityReport(files_analyzed=5, symbol_count=20)
461
+
462
+ result = enforce_quality_gate(pm, qr, QualityPolicy())
463
+ assert result.passed is True
464
+ assert len(result.violations) == 0
465
+
466
+ def test_gate_fail_maintainability(self):
467
+ from semantic_code_intelligence.ci.metrics import (
468
+ ProjectMetrics,
469
+ QualityPolicy,
470
+ enforce_quality_gate,
471
+ )
472
+ from semantic_code_intelligence.ci.quality import QualityReport
473
+
474
+ pm = ProjectMetrics(maintainability_index=20.0, max_complexity=5)
475
+ qr = QualityReport(files_analyzed=5, symbol_count=20)
476
+
477
+ result = enforce_quality_gate(pm, qr, QualityPolicy(min_maintainability=40.0))
478
+ assert result.passed is False
479
+ assert any(v.rule == "min_maintainability" for v in result.violations)
480
+
481
+ def test_gate_fail_complexity(self):
482
+ from semantic_code_intelligence.ci.metrics import (
483
+ ProjectMetrics,
484
+ QualityPolicy,
485
+ enforce_quality_gate,
486
+ )
487
+ from semantic_code_intelligence.ci.quality import QualityReport
488
+
489
+ pm = ProjectMetrics(maintainability_index=80.0, max_complexity=30)
490
+ qr = QualityReport(files_analyzed=5, symbol_count=20)
491
+
492
+ result = enforce_quality_gate(pm, qr, QualityPolicy(max_complexity=25))
493
+ assert result.passed is False
494
+ assert any(v.rule == "max_complexity" for v in result.violations)
495
+
496
+ def test_gate_multiple_violations(self):
497
+ from semantic_code_intelligence.ci.metrics import (
498
+ ProjectMetrics,
499
+ QualityPolicy,
500
+ enforce_quality_gate,
501
+ )
502
+ from semantic_code_intelligence.ci.quality import QualityReport
503
+
504
+ pm = ProjectMetrics(maintainability_index=10.0, max_complexity=50)
505
+ qr = QualityReport(files_analyzed=5, symbol_count=20)
506
+
507
+ result = enforce_quality_gate(pm, qr, QualityPolicy())
508
+ assert result.passed is False
509
+ assert len(result.violations) >= 2
510
+
511
+ def test_gate_result_to_dict(self):
512
+ from semantic_code_intelligence.ci.metrics import (
513
+ ProjectMetrics,
514
+ enforce_quality_gate,
515
+ )
516
+ from semantic_code_intelligence.ci.quality import QualityReport
517
+
518
+ pm = ProjectMetrics(maintainability_index=80.0, max_complexity=5)
519
+ qr = QualityReport(files_analyzed=5, symbol_count=20)
520
+
521
+ result = enforce_quality_gate(pm, qr)
522
+ d = result.to_dict()
523
+ assert "passed" in d
524
+ assert "violations" in d
525
+ assert "policy" in d
526
+
527
+ def test_gate_default_policy(self):
528
+ from semantic_code_intelligence.ci.metrics import (
529
+ ProjectMetrics,
530
+ enforce_quality_gate,
531
+ )
532
+ from semantic_code_intelligence.ci.quality import QualityReport
533
+
534
+ pm = ProjectMetrics(maintainability_index=80.0, max_complexity=5)
535
+ qr = QualityReport(files_analyzed=5, symbol_count=20)
536
+
537
+ # Should use default policy
538
+ result = enforce_quality_gate(pm, qr)
539
+ assert result.passed is True
540
+
541
+
542
+ # =========================================================================
543
+ # CLI: metrics command
544
+ # =========================================================================
545
+
546
+
547
+ class TestMetricsCLI:
548
+ """Tests for the `codexa metrics` CLI command."""
549
+
550
+ @pytest.fixture
551
+ def runner(self):
552
+ return CliRunner()
553
+
554
+ def test_basic_metrics_json(self, runner, tmp_path):
555
+ from semantic_code_intelligence.cli.commands.metrics_cmd import metrics_cmd
556
+
557
+ _write_sample_project(tmp_path)
558
+ result = runner.invoke(metrics_cmd, [
559
+ "--path", str(tmp_path), "--json",
560
+ ], obj={"pipe": False})
561
+ assert result.exit_code == 0
562
+ data = json.loads(result.output)
563
+ assert "files_analyzed" in data
564
+ assert "maintainability_index" in data
565
+
566
+ def test_pipe_mode(self, runner, tmp_path):
567
+ from semantic_code_intelligence.cli.commands.metrics_cmd import metrics_cmd
568
+
569
+ _write_sample_project(tmp_path)
570
+ result = runner.invoke(metrics_cmd, [
571
+ "--path", str(tmp_path), "--pipe",
572
+ ], obj={"pipe": False})
573
+ assert result.exit_code == 0
574
+ assert "MI:" in result.output
575
+
576
+ def test_rich_output(self, runner, tmp_path):
577
+ from semantic_code_intelligence.cli.commands.metrics_cmd import metrics_cmd
578
+
579
+ _write_sample_project(tmp_path)
580
+ result = runner.invoke(metrics_cmd, [
581
+ "--path", str(tmp_path),
582
+ ], obj={"pipe": False})
583
+ assert result.exit_code == 0
584
+ assert "Quality Metrics" in result.output
585
+
586
+ def test_snapshot_json(self, runner, tmp_path):
587
+ from semantic_code_intelligence.cli.commands.metrics_cmd import metrics_cmd
588
+
589
+ _write_sample_project(tmp_path)
590
+ (tmp_path / ".codexa").mkdir(exist_ok=True)
591
+ result = runner.invoke(metrics_cmd, [
592
+ "--path", str(tmp_path), "--json", "--snapshot",
593
+ ], obj={"pipe": False})
594
+ assert result.exit_code == 0
595
+ data = json.loads(result.output)
596
+ assert "snapshot" in data
597
+
598
+ def test_history_empty(self, runner, tmp_path):
599
+ from semantic_code_intelligence.cli.commands.metrics_cmd import metrics_cmd
600
+
601
+ (tmp_path / ".codexa").mkdir(exist_ok=True)
602
+ result = runner.invoke(metrics_cmd, [
603
+ "--path", str(tmp_path), "--history", "5",
604
+ ], obj={"pipe": False})
605
+ assert result.exit_code == 0
606
+
607
+ def test_trend_needs_data(self, runner, tmp_path):
608
+ from semantic_code_intelligence.cli.commands.metrics_cmd import metrics_cmd
609
+
610
+ (tmp_path / ".codexa").mkdir(exist_ok=True)
611
+ result = runner.invoke(metrics_cmd, [
612
+ "--path", str(tmp_path), "--trend",
613
+ ], obj={"pipe": False})
614
+ assert result.exit_code == 0
615
+ # Should tell user they need more snapshots
616
+ assert "2 snapshots" in result.output.lower() or "need" in result.output.lower()
617
+
618
+
619
+ # =========================================================================
620
+ # CLI: gate command
621
+ # =========================================================================
622
+
623
+
624
+ class TestGateCLI:
625
+ """Tests for the `codexa gate` CLI command."""
626
+
627
+ @pytest.fixture
628
+ def runner(self):
629
+ return CliRunner()
630
+
631
+ def test_gate_json(self, runner, tmp_path):
632
+ from semantic_code_intelligence.cli.commands.gate_cmd import gate_cmd
633
+
634
+ _write_sample_project(tmp_path)
635
+ result = runner.invoke(gate_cmd, [
636
+ "--path", str(tmp_path), "--json",
637
+ ], obj={"pipe": False})
638
+ assert result.exit_code == 0
639
+ data = json.loads(result.output)
640
+ assert "passed" in data
641
+ assert "violations" in data
642
+
643
+ def test_gate_pipe_pass(self, runner, tmp_path):
644
+ from semantic_code_intelligence.cli.commands.gate_cmd import gate_cmd
645
+
646
+ _write_sample_project(tmp_path)
647
+ result = runner.invoke(gate_cmd, [
648
+ "--path", str(tmp_path), "--pipe",
649
+ ], obj={"pipe": False})
650
+ assert result.exit_code == 0
651
+ assert "MI=" in result.output
652
+
653
+ def test_gate_rich_output(self, runner, tmp_path):
654
+ from semantic_code_intelligence.cli.commands.gate_cmd import gate_cmd
655
+
656
+ _write_sample_project(tmp_path)
657
+ result = runner.invoke(gate_cmd, [
658
+ "--path", str(tmp_path),
659
+ ], obj={"pipe": False})
660
+ assert result.exit_code == 0
661
+
662
+ def test_gate_custom_thresholds(self, runner, tmp_path):
663
+ from semantic_code_intelligence.cli.commands.gate_cmd import gate_cmd
664
+
665
+ _write_sample_project(tmp_path)
666
+ result = runner.invoke(gate_cmd, [
667
+ "--path", str(tmp_path), "--json",
668
+ "--min-maintainability", "99",
669
+ ], obj={"pipe": False})
670
+ assert result.exit_code == 0
671
+ data = json.loads(result.output)
672
+ # With MI threshold of 99, should likely fail
673
+ assert "passed" in data
674
+
675
+
676
+ # =========================================================================
677
+ # Configuration extension
678
+ # =========================================================================
679
+
680
+
681
+ class TestQualityConfigExtension:
682
+ """Tests for QualityConfig in AppConfig."""
683
+
684
+ def test_default_quality_config(self):
685
+ from semantic_code_intelligence.config.settings import AppConfig
686
+
687
+ cfg = AppConfig()
688
+ assert hasattr(cfg, "quality")
689
+ assert cfg.quality.complexity_threshold == 10
690
+ assert cfg.quality.min_maintainability == 40.0
691
+ assert cfg.quality.snapshot_on_index is False
692
+
693
+ def test_quality_config_in_json(self):
694
+ from semantic_code_intelligence.config.settings import AppConfig
695
+
696
+ cfg = AppConfig()
697
+ d = json.loads(cfg.model_dump_json())
698
+ assert "quality" in d
699
+ assert d["quality"]["complexity_threshold"] == 10
700
+
701
+ def test_load_config_with_quality(self, tmp_path):
702
+ from semantic_code_intelligence.config.settings import (
703
+ AppConfig,
704
+ save_config,
705
+ load_config,
706
+ )
707
+
708
+ cfg = AppConfig(project_root=str(tmp_path))
709
+ cfg.quality.min_maintainability = 60.0
710
+ save_config(cfg, tmp_path)
711
+
712
+ loaded = load_config(tmp_path)
713
+ assert loaded.quality.min_maintainability == 60.0
714
+
715
+
716
+ # =========================================================================
717
+ # Documentation generation
718
+ # =========================================================================
719
+
720
+
721
+ class TestDocsPhase17:
722
+ """Tests for QUALITY_METRICS.md documentation generation."""
723
+
724
+ def test_quality_metrics_reference(self):
725
+ from semantic_code_intelligence.docs import generate_quality_metrics_reference
726
+
727
+ md = generate_quality_metrics_reference()
728
+ assert "Maintainability Index" in md
729
+ assert "Quality Gates" in md
730
+ assert "codexa gate" in md
731
+ assert "codexa metrics" in md
732
+
733
+ def test_generate_all_docs_includes_quality(self, tmp_path):
734
+ from semantic_code_intelligence.docs import generate_all_docs
735
+
736
+ generated = generate_all_docs(tmp_path)
737
+ assert "QUALITY_METRICS.md" in generated
738
+
739
+ def test_ci_reference_updated(self):
740
+ from semantic_code_intelligence.docs import generate_ci_reference
741
+
742
+ md = generate_ci_reference()
743
+ assert "codexa metrics" in md
744
+ assert "codexa gate" in md
745
+
746
+
747
+ # =========================================================================
748
+ # Router, version, and module structure tests
749
+ # =========================================================================
750
+
751
+
752
+ class TestRouterPhase17:
753
+ """Tests for CLI router registration."""
754
+
755
+ def test_register_commands_count(self):
756
+ import click
757
+ from semantic_code_intelligence.cli.router import register_commands
758
+
759
+ group = click.Group("test")
760
+ register_commands(group)
761
+ assert len(group.commands) == 39
762
+
763
+ def test_metrics_command_registered(self):
764
+ from semantic_code_intelligence.cli.main import cli
765
+
766
+ assert "metrics" in cli.commands
767
+
768
+ def test_gate_command_registered(self):
769
+ from semantic_code_intelligence.cli.main import cli
770
+
771
+ assert "gate" in cli.commands
772
+
773
+
774
+ class TestVersionBump17:
775
+ """Test version is 0.19.0."""
776
+
777
+ def test_version_is_017(self):
778
+ from semantic_code_intelligence import __version__
779
+
780
+ assert __version__ == "0.4.0"
781
+
782
+
783
+ class TestPhase17ModuleStructure:
784
+ """Tests for module import structure."""
785
+
786
+ def test_import_metrics(self):
787
+ from semantic_code_intelligence.ci.metrics import (
788
+ FileMetrics,
789
+ ProjectMetrics,
790
+ QualitySnapshot,
791
+ TrendResult,
792
+ QualityPolicy,
793
+ GateResult,
794
+ GateViolation,
795
+ compute_file_metrics,
796
+ compute_project_metrics,
797
+ save_snapshot,
798
+ load_snapshots,
799
+ compute_trend,
800
+ enforce_quality_gate,
801
+ )
802
+
803
+ def test_import_quality_config(self):
804
+ from semantic_code_intelligence.config.settings import QualityConfig
805
+
806
+ def test_import_metrics_cmd(self):
807
+ from semantic_code_intelligence.cli.commands.metrics_cmd import metrics_cmd
808
+
809
+ def test_import_gate_cmd(self):
810
+ from semantic_code_intelligence.cli.commands.gate_cmd import gate_cmd
811
+
812
+ def test_ci_module_docstring_updated(self):
813
+ import semantic_code_intelligence.ci as ci_mod
814
+
815
+ assert "metrics" in ci_mod.__doc__.lower()