codevira 1.6.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 (58) hide show
  1. codevira-1.6.0.dist-info/LICENSE +21 -0
  2. codevira-1.6.0.dist-info/METADATA +477 -0
  3. codevira-1.6.0.dist-info/RECORD +58 -0
  4. codevira-1.6.0.dist-info/WHEEL +5 -0
  5. codevira-1.6.0.dist-info/entry_points.txt +2 -0
  6. codevira-1.6.0.dist-info/top_level.txt +2 -0
  7. indexer/__init__.py +1 -0
  8. indexer/chunker.py +428 -0
  9. indexer/global_db.py +197 -0
  10. indexer/graph_generator.py +380 -0
  11. indexer/index_codebase.py +588 -0
  12. indexer/outcome_tracker.py +172 -0
  13. indexer/rule_learner.py +186 -0
  14. indexer/sqlite_graph.py +640 -0
  15. indexer/treesitter_parser.py +423 -0
  16. mcp_server/__init__.py +1 -0
  17. mcp_server/__main__.py +20 -0
  18. mcp_server/auto_init.py +257 -0
  19. mcp_server/cli.py +622 -0
  20. mcp_server/crash_logger.py +236 -0
  21. mcp_server/data/__init__.py +1 -0
  22. mcp_server/data/agents/builder.md +84 -0
  23. mcp_server/data/agents/developer.md +111 -0
  24. mcp_server/data/agents/documenter.md +138 -0
  25. mcp_server/data/agents/orchestrator.md +96 -0
  26. mcp_server/data/agents/planner.md +106 -0
  27. mcp_server/data/agents/reviewer.md +82 -0
  28. mcp_server/data/agents/tester.md +83 -0
  29. mcp_server/data/config.example.yaml +33 -0
  30. mcp_server/data/rules/coding-standards.md +48 -0
  31. mcp_server/data/rules/engineering-excellence.md +28 -0
  32. mcp_server/data/rules/git-cicd-governance.md +32 -0
  33. mcp_server/data/rules/git_commits.md +130 -0
  34. mcp_server/data/rules/incremental-updates.md +5 -0
  35. mcp_server/data/rules/master_rule.md +187 -0
  36. mcp_server/data/rules/multi-language.md +19 -0
  37. mcp_server/data/rules/persistence.md +21 -0
  38. mcp_server/data/rules/resilience-observability.md +17 -0
  39. mcp_server/data/rules/smoke-testing.md +48 -0
  40. mcp_server/data/rules/testing-standards.md +23 -0
  41. mcp_server/detect.py +284 -0
  42. mcp_server/gitignore.py +284 -0
  43. mcp_server/global_sync.py +187 -0
  44. mcp_server/http_server.py +341 -0
  45. mcp_server/ide_inject.py +444 -0
  46. mcp_server/launchd.py +156 -0
  47. mcp_server/migrate.py +215 -0
  48. mcp_server/paths.py +256 -0
  49. mcp_server/prompts.py +136 -0
  50. mcp_server/server.py +1049 -0
  51. mcp_server/tools/__init__.py +0 -0
  52. mcp_server/tools/changesets.py +223 -0
  53. mcp_server/tools/code_reader.py +335 -0
  54. mcp_server/tools/graph.py +637 -0
  55. mcp_server/tools/learning.py +238 -0
  56. mcp_server/tools/playbook.py +89 -0
  57. mcp_server/tools/roadmap.py +599 -0
  58. mcp_server/tools/search.py +145 -0
@@ -0,0 +1,380 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import json
5
+ import os
6
+ import re
7
+ import subprocess
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import yaml
12
+
13
+ from indexer.treesitter_parser import (
14
+ parse_file as ts_parse_file,
15
+ get_language as ts_get_language,
16
+ EXTENSION_MAP as TS_EXTENSION_MAP,
17
+ )
18
+ from indexer.sqlite_graph import SQLiteGraph
19
+ from indexer.chunker import extract_imports
20
+
21
+ def _infer_layer(file_path: str) -> str:
22
+ path = file_path.lower()
23
+ if any(x in path for x in ["/api/", "/controllers/", "/routers/", "/routes/"]):
24
+ return "api"
25
+ if any(x in path for x in ["/models/", "/db/", "/schemas/", "/orm/"]):
26
+ return "database"
27
+ if any(x in path for x in ["/services/", "/core/", "/logic/", "/usecases/"]):
28
+ return "service"
29
+ if any(x in path for x in ["/utils/", "/helpers/", "/common/"]):
30
+ return "utility"
31
+ if any(x in path for x in ["/frontend/", "/ui/", "/components/", "/views/"]):
32
+ return "frontend"
33
+ if "test" in path:
34
+ return "test"
35
+ return "core"
36
+
37
+ def _get_python_docstring(file_path: str) -> str | None:
38
+ try:
39
+ with open(file_path, "r", encoding="utf-8") as f:
40
+ tree = ast.parse(f.read())
41
+ doc = ast.get_docstring(tree)
42
+ if doc:
43
+ return doc.splitlines()[0]
44
+ except Exception:
45
+ pass
46
+ return None
47
+
48
+ def _get_python_public_symbols(file_path: str) -> list[str]:
49
+ symbols = []
50
+ try:
51
+ with open(file_path, "r", encoding="utf-8") as f:
52
+ tree = ast.parse(f.read())
53
+ for node in tree.body:
54
+ if isinstance(node, ast.FunctionDef) and not node.name.startswith("_"):
55
+ symbols.append(node.name)
56
+ elif isinstance(node, ast.ClassDef) and not node.name.startswith("_"):
57
+ symbols.append(node.name)
58
+ except Exception:
59
+ pass
60
+ return symbols
61
+
62
+ def _get_python_symbols_detailed(file_path: str) -> list:
63
+ """Extract Python symbols with call information for the call graph."""
64
+ from indexer.treesitter_parser import ParsedSymbol
65
+ symbols = []
66
+ try:
67
+ with open(file_path, "r", encoding="utf-8") as f:
68
+ source = f.read()
69
+ tree = ast.parse(source)
70
+ source_lines = source.splitlines()
71
+
72
+ for node in ast.walk(tree):
73
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
74
+ if node.name.startswith("_"):
75
+ continue
76
+
77
+ # Extract function calls within the body
78
+ calls = []
79
+ for child in ast.walk(node):
80
+ if isinstance(child, ast.Call):
81
+ if isinstance(child.func, ast.Name):
82
+ calls.append(child.func.id)
83
+ elif isinstance(child.func, ast.Attribute):
84
+ calls.append(child.func.attr)
85
+
86
+ # Extract parameters
87
+ params = []
88
+ for arg in node.args.args:
89
+ param = {"name": arg.arg}
90
+ if arg.annotation:
91
+ try:
92
+ param["type"] = ast.unparse(arg.annotation)
93
+ except Exception:
94
+ pass
95
+ params.append(param)
96
+
97
+ # Extract return type
98
+ ret_type = None
99
+ if node.returns:
100
+ try:
101
+ ret_type = ast.unparse(node.returns)
102
+ except Exception:
103
+ pass
104
+
105
+ sig_line = source_lines[node.lineno - 1].strip() if node.lineno <= len(source_lines) else ""
106
+ doc = ast.get_docstring(node)
107
+
108
+ sym = ParsedSymbol(
109
+ name=node.name,
110
+ kind="function",
111
+ signature_line=sig_line,
112
+ start_line=node.lineno,
113
+ end_line=node.end_lineno or node.lineno,
114
+ docstring=doc.splitlines()[0] if doc else None,
115
+ is_public=not node.name.startswith("_"),
116
+ )
117
+ # Attach extra fields
118
+ sym.calls = calls # type: ignore[attr-defined]
119
+ sym.parameters = params # type: ignore[attr-defined]
120
+ sym.return_type = ret_type # type: ignore[attr-defined]
121
+ symbols.append(sym)
122
+
123
+ elif isinstance(node, ast.ClassDef):
124
+ if node.name.startswith("_"):
125
+ continue
126
+ sig_line = source_lines[node.lineno - 1].strip() if node.lineno <= len(source_lines) else ""
127
+ doc = ast.get_docstring(node)
128
+ methods = [n.name for n in node.body if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef)) and not n.name.startswith("_")]
129
+ sym = ParsedSymbol(
130
+ name=node.name,
131
+ kind="class",
132
+ signature_line=sig_line,
133
+ start_line=node.lineno,
134
+ end_line=node.end_lineno or node.lineno,
135
+ docstring=doc.splitlines()[0] if doc else None,
136
+ is_public=not node.name.startswith("_"),
137
+ methods=methods,
138
+ )
139
+ sym.calls = [] # type: ignore[attr-defined]
140
+ sym.parameters = [] # type: ignore[attr-defined]
141
+ sym.return_type = None # type: ignore[attr-defined]
142
+ symbols.append(sym)
143
+ except Exception:
144
+ pass
145
+ return symbols
146
+
147
+
148
+ def generate_graph_node(file_path: str, project_root: str) -> dict[str, Any]:
149
+ abs_path = os.path.join(project_root, file_path)
150
+ if not os.path.exists(abs_path):
151
+ return {}
152
+
153
+ layer = _infer_layer(file_path)
154
+ role = f"Handles {layer} logic."
155
+ key_funcs = []
156
+
157
+ ext = os.path.splitext(abs_path)[1].lower()
158
+
159
+ lang = ts_get_language(ext)
160
+ if lang:
161
+ try:
162
+ parsed = ts_parse_file(abs_path, lang)
163
+ if parsed.module_docstring:
164
+ role = parsed.module_docstring
165
+ key_funcs = [s.name for s in parsed.symbols if s.is_public]
166
+ except Exception:
167
+ pass
168
+ elif ext == ".py":
169
+ doc = _get_python_docstring(abs_path)
170
+ if doc:
171
+ role = doc
172
+ key_funcs = _get_python_public_symbols(abs_path)
173
+
174
+ if not role.endswith("."):
175
+ role += "."
176
+
177
+ return {
178
+ "file_path": file_path,
179
+ "role": role,
180
+ "type": "component" if layer != "utility" else "utility",
181
+ "layer": layer,
182
+ "stability": "high" if layer == "database" else "medium",
183
+ "key_functions": key_funcs,
184
+ "connects_to": [],
185
+ "rules": [],
186
+ "tests": [],
187
+ "do_not_revert": False,
188
+ "auto_generated": True,
189
+ }
190
+
191
+ def generate_graph_sqlite(project_root: str, db_path: str | None = None) -> dict[str, Any]:
192
+ if not db_path:
193
+ from mcp_server.paths import get_data_dir
194
+ db_path = str(get_data_dir() / "graph" / "graph.db")
195
+
196
+ db = SQLiteGraph(db_path)
197
+
198
+ file_paths = []
199
+ for ext in TS_EXTENSION_MAP.keys():
200
+ file_paths.extend([str(p.relative_to(project_root)) for p in Path(project_root).rglob(f"*{ext}")])
201
+ file_paths.extend([str(p.relative_to(project_root)) for p in Path(project_root).rglob("*.py")])
202
+
203
+ added = 0
204
+ skipped = 0
205
+ files_added = []
206
+
207
+ for fp in file_paths:
208
+ if "node_modules" in fp or ".venv" in fp:
209
+ continue
210
+
211
+ node_id = f"file:{fp}"
212
+ existing = db.get_node(node_id)
213
+ if existing:
214
+ skipped += 1
215
+ continue
216
+
217
+ node_data = generate_graph_node(fp, project_root)
218
+ if not node_data:
219
+ continue
220
+
221
+ db.add_node(
222
+ node_id=node_id,
223
+ kind="file",
224
+ name=Path(fp).name,
225
+ file_path=fp,
226
+ role=node_data["role"],
227
+ layer=node_data["layer"],
228
+ stability=node_data["stability"],
229
+ type=node_data["type"],
230
+ key_functions=json.dumps(node_data["key_functions"]),
231
+ dependencies="[]",
232
+ rules="[]",
233
+ do_not_revert=node_data.get("do_not_revert", False)
234
+ )
235
+ added += 1
236
+ files_added.append(fp)
237
+
238
+ # Build the full set of project file paths (used by Phases 2 and 4)
239
+ all_node_paths = {fp for fp in file_paths if "node_modules" not in fp and ".venv" not in fp}
240
+
241
+ # ---- Phase 2: Populate function-level symbols ----
242
+ symbols_added = 0
243
+ for fp in all_node_paths:
244
+ file_node_id = f"file:{fp}"
245
+ abs_path = os.path.join(project_root, fp)
246
+ if not os.path.exists(abs_path):
247
+ continue
248
+
249
+ # Clear old symbols for this file (call_edges cascade via FK)
250
+ db.remove_symbols_for_file(file_node_id)
251
+
252
+ ext = os.path.splitext(abs_path)[1].lower()
253
+ symbols_for_file = []
254
+
255
+ lang = ts_get_language(ext)
256
+ if lang:
257
+ try:
258
+ parsed = ts_parse_file(abs_path, lang)
259
+ symbols_for_file = parsed.symbols
260
+ except Exception:
261
+ continue
262
+ elif ext == ".py":
263
+ symbols_for_file = _get_python_symbols_detailed(abs_path)
264
+
265
+ for sym in symbols_for_file:
266
+ sym_id = f"file:{fp}::{sym.name}"
267
+ calls_json = json.dumps(getattr(sym, 'calls', []) if hasattr(sym, 'calls') else [])
268
+ params_json = json.dumps(getattr(sym, 'parameters', []) if hasattr(sym, 'parameters') else [])
269
+ ret_type = getattr(sym, 'return_type', None) if hasattr(sym, 'return_type') else None
270
+
271
+ db.add_symbol(
272
+ symbol_id=sym_id,
273
+ file_node_id=file_node_id,
274
+ name=sym.name,
275
+ kind=sym.kind,
276
+ signature=sym.signature_line,
277
+ parameters=params_json,
278
+ return_type=ret_type,
279
+ start_line=sym.start_line,
280
+ end_line=sym.end_line,
281
+ docstring=sym.docstring,
282
+ is_public=sym.is_public,
283
+ calls=calls_json,
284
+ )
285
+ symbols_added += 1
286
+
287
+ # ---- Phase 3: Resolve call edges between symbols ----
288
+ call_edges_added = 0
289
+ # Build a name→symbol_id lookup for the whole project
290
+ all_symbols = {}
291
+ for row in db.conn.execute("SELECT id, name FROM symbols").fetchall():
292
+ name = row["name"]
293
+ if name not in all_symbols:
294
+ all_symbols[name] = row["id"]
295
+
296
+ for row in db.conn.execute("SELECT id, calls FROM symbols WHERE calls IS NOT NULL AND calls != '[]'").fetchall():
297
+ caller_id = row["id"]
298
+ try:
299
+ calls = json.loads(row["calls"])
300
+ except (json.JSONDecodeError, TypeError):
301
+ continue
302
+ for callee_name in calls:
303
+ if callee_name in all_symbols:
304
+ callee_id = all_symbols[callee_name]
305
+ if caller_id != callee_id: # avoid self-edges
306
+ db.add_call_edge(caller_id, callee_id)
307
+ call_edges_added += 1
308
+
309
+ # ---- Phase 4: Populate dependency edges from imports ----
310
+ edges_added = 0
311
+ for fp in all_node_paths:
312
+ source_id = f"file:{fp}"
313
+ abs_path = os.path.join(project_root, fp)
314
+ if not os.path.exists(abs_path):
315
+ continue
316
+
317
+ # Clear old edges and re-derive from current imports
318
+ db.remove_edges_for_node(source_id)
319
+
320
+ try:
321
+ imported_paths = extract_imports(abs_path, project_root)
322
+ except Exception:
323
+ continue
324
+
325
+ for imp_path in imported_paths:
326
+ target_id = f"file:{imp_path}"
327
+ # Only create edges to files that exist in the graph
328
+ if imp_path in all_node_paths or db.get_node(target_id):
329
+ db.add_edge(source_id, target_id, kind="imports")
330
+ edges_added += 1
331
+
332
+ db.close()
333
+ return {
334
+ "files_processed": added + skipped,
335
+ "nodes_added": added,
336
+ "nodes_skipped": skipped,
337
+ "edges_added": edges_added,
338
+ "symbols_added": symbols_added,
339
+ "call_edges_added": call_edges_added,
340
+ "files_added": files_added,
341
+ }
342
+
343
+ def generate_roadmap_stub(project_root: str, output_path: str):
344
+ if os.path.exists(output_path):
345
+ return
346
+
347
+ phase_name = "Phase 1: Initial Development"
348
+ desc = "Bootstrap project and core architecture."
349
+
350
+ try:
351
+ out = subprocess.check_output(
352
+ ["git", "-C", project_root, "log", "-1", "--pretty=format:%s"],
353
+ stderr=subprocess.DEVNULL
354
+ ).decode("utf-8").strip()
355
+ if out:
356
+ desc = f"Latest context: {out}"
357
+ except Exception:
358
+ pass
359
+
360
+ stub = {
361
+ "project": Path(project_root).name,
362
+ "version": "1.0",
363
+ "current_phase": {
364
+ "number": 1,
365
+ "name": phase_name,
366
+ "status": "in_progress",
367
+ "next_action": "Review architecture and implement core components.",
368
+ "open_changesets": [],
369
+ "description": desc,
370
+ "goal": desc,
371
+ },
372
+ "upcoming_phases": [],
373
+ "deferred": [],
374
+ "completed_phases": [],
375
+ }
376
+
377
+ Path(output_path).parent.mkdir(parents=True, exist_ok=True)
378
+ with open(output_path, "w", encoding="utf-8") as f:
379
+ yaml.safe_dump(stub, f, default_flow_style=False, sort_keys=False, allow_unicode=True)
380
+ print(f"Created initial roadmap: {output_path}")