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.
- codevira-1.6.0.dist-info/LICENSE +21 -0
- codevira-1.6.0.dist-info/METADATA +477 -0
- codevira-1.6.0.dist-info/RECORD +58 -0
- codevira-1.6.0.dist-info/WHEEL +5 -0
- codevira-1.6.0.dist-info/entry_points.txt +2 -0
- codevira-1.6.0.dist-info/top_level.txt +2 -0
- indexer/__init__.py +1 -0
- indexer/chunker.py +428 -0
- indexer/global_db.py +197 -0
- indexer/graph_generator.py +380 -0
- indexer/index_codebase.py +588 -0
- indexer/outcome_tracker.py +172 -0
- indexer/rule_learner.py +186 -0
- indexer/sqlite_graph.py +640 -0
- indexer/treesitter_parser.py +423 -0
- mcp_server/__init__.py +1 -0
- mcp_server/__main__.py +20 -0
- mcp_server/auto_init.py +257 -0
- mcp_server/cli.py +622 -0
- mcp_server/crash_logger.py +236 -0
- mcp_server/data/__init__.py +1 -0
- mcp_server/data/agents/builder.md +84 -0
- mcp_server/data/agents/developer.md +111 -0
- mcp_server/data/agents/documenter.md +138 -0
- mcp_server/data/agents/orchestrator.md +96 -0
- mcp_server/data/agents/planner.md +106 -0
- mcp_server/data/agents/reviewer.md +82 -0
- mcp_server/data/agents/tester.md +83 -0
- mcp_server/data/config.example.yaml +33 -0
- mcp_server/data/rules/coding-standards.md +48 -0
- mcp_server/data/rules/engineering-excellence.md +28 -0
- mcp_server/data/rules/git-cicd-governance.md +32 -0
- mcp_server/data/rules/git_commits.md +130 -0
- mcp_server/data/rules/incremental-updates.md +5 -0
- mcp_server/data/rules/master_rule.md +187 -0
- mcp_server/data/rules/multi-language.md +19 -0
- mcp_server/data/rules/persistence.md +21 -0
- mcp_server/data/rules/resilience-observability.md +17 -0
- mcp_server/data/rules/smoke-testing.md +48 -0
- mcp_server/data/rules/testing-standards.md +23 -0
- mcp_server/detect.py +284 -0
- mcp_server/gitignore.py +284 -0
- mcp_server/global_sync.py +187 -0
- mcp_server/http_server.py +341 -0
- mcp_server/ide_inject.py +444 -0
- mcp_server/launchd.py +156 -0
- mcp_server/migrate.py +215 -0
- mcp_server/paths.py +256 -0
- mcp_server/prompts.py +136 -0
- mcp_server/server.py +1049 -0
- mcp_server/tools/__init__.py +0 -0
- mcp_server/tools/changesets.py +223 -0
- mcp_server/tools/code_reader.py +335 -0
- mcp_server/tools/graph.py +637 -0
- mcp_server/tools/learning.py +238 -0
- mcp_server/tools/playbook.py +89 -0
- mcp_server/tools/roadmap.py +599 -0
- 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}")
|