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
File without changes
@@ -0,0 +1,223 @@
1
+ """
2
+ MCP tools for managing multi-file change sets.
3
+ Tracks in-progress fixes that span multiple files across sessions.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ from datetime import date
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import yaml
12
+
13
+ from mcp_server.paths import get_data_dir
14
+
15
+
16
+ def _changesets_dir() -> Path:
17
+ return get_data_dir() / "graph" / "changesets"
18
+
19
+
20
+ def _changeset_path(changeset_id: str) -> Path:
21
+ return _changesets_dir() / f"{changeset_id}.yaml"
22
+
23
+
24
+ def start_changeset(
25
+ changeset_id: str,
26
+ description: str,
27
+ files: list[str],
28
+ trigger: str = "medium_change",
29
+ ) -> dict[str, Any]:
30
+ """
31
+ Begin tracking a multi-file fix. Call BEFORE touching any files.
32
+
33
+ Args:
34
+ changeset_id: Short slug (e.g. "synonym-pipeline-fix")
35
+ description: What this changeset does
36
+ files: All files that will be modified (include ones not yet modified)
37
+ trigger: small_fix | medium_change | large_change
38
+
39
+ Creates .agents/graph/changesets/{id}.yaml
40
+ """
41
+ _changesets_dir().mkdir(parents=True, exist_ok=True)
42
+ path = _changeset_path(changeset_id)
43
+
44
+ if path.exists():
45
+ return {
46
+ "success": False,
47
+ "message": f"Changeset '{changeset_id}' already exists. Use a different ID or complete the existing one.",
48
+ }
49
+
50
+ data = {
51
+ "id": changeset_id,
52
+ "status": "in_progress",
53
+ "created": date.today().isoformat(),
54
+ "trigger": trigger,
55
+ "description": description,
56
+ "files_modified": [],
57
+ "files_pending": files,
58
+ "blocker": None,
59
+ "decisions": [],
60
+ }
61
+
62
+ with open(path, "w") as f:
63
+ yaml.dump(data, f, default_flow_style=False, sort_keys=False)
64
+
65
+ return {"success": True, "changeset_id": changeset_id, "tracking": files}
66
+
67
+
68
+ def update_changeset_progress(
69
+ changeset_id: str,
70
+ file_done: str,
71
+ blocker: str | None = None,
72
+ ) -> dict[str, Any]:
73
+ """
74
+ Move a file from files_pending to files_modified.
75
+ Call after each file is completed within the changeset.
76
+ """
77
+ path = _changeset_path(changeset_id)
78
+ if not path.exists():
79
+ return {"success": False, "message": f"Changeset '{changeset_id}' not found."}
80
+
81
+ with open(path) as f:
82
+ data = yaml.safe_load(f)
83
+
84
+ pending = data.get("files_pending", [])
85
+ modified = data.get("files_modified", [])
86
+
87
+ if file_done in pending:
88
+ pending.remove(file_done)
89
+ if file_done not in modified:
90
+ modified.append(file_done)
91
+
92
+ data["files_pending"] = pending
93
+ data["files_modified"] = modified
94
+ if blocker is not None:
95
+ data["blocker"] = blocker
96
+
97
+ with open(path, "w") as f:
98
+ yaml.dump(data, f, default_flow_style=False, sort_keys=False)
99
+
100
+ return {
101
+ "success": True,
102
+ "files_done": len(modified),
103
+ "files_remaining": len(pending),
104
+ "blocker": blocker,
105
+ }
106
+
107
+
108
+ def complete_changeset(changeset_id: str, decisions: list[str]) -> dict[str, Any]:
109
+ """
110
+ Mark a changeset as complete. Call at session end after all files are done.
111
+
112
+ Args:
113
+ changeset_id: The changeset to complete
114
+ decisions: Key decisions made (e.g. "synonyms in text only, not metadata")
115
+ These are written to the changeset record for future agents.
116
+ """
117
+ path = _changeset_path(changeset_id)
118
+ if not path.exists():
119
+ return {"success": False, "message": f"Changeset '{changeset_id}' not found."}
120
+
121
+ with open(path) as f:
122
+ data = yaml.safe_load(f)
123
+
124
+ if data.get("files_pending"):
125
+ return {
126
+ "success": False,
127
+ "message": f"Cannot complete — {len(data['files_pending'])} files still pending: {data['files_pending']}",
128
+ "hint": "Either finish the pending files or document the blocker with update_changeset_progress.",
129
+ }
130
+
131
+ data["status"] = "complete"
132
+ data["completed"] = date.today().isoformat()
133
+ data["decisions"] = decisions
134
+ data["blocker"] = None
135
+
136
+ with open(path, "w") as f:
137
+ yaml.dump(data, f, default_flow_style=False, sort_keys=False)
138
+
139
+ return {"success": True, "changeset_id": changeset_id, "decisions_recorded": len(decisions)}
140
+
141
+
142
+ def get_changeset(changeset_id: str) -> dict[str, Any]:
143
+ """Get the current state of a changeset."""
144
+ path = _changeset_path(changeset_id)
145
+ if not path.exists():
146
+ return {"found": False, "message": f"Changeset '{changeset_id}' not found."}
147
+
148
+ with open(path) as f:
149
+ data = yaml.safe_load(f)
150
+ return {"found": True, "changeset": data}
151
+
152
+
153
+ def list_open_changesets() -> dict[str, Any]:
154
+ """List all in-progress changesets. Call at session start to check for unfinished work."""
155
+ _changesets_dir().mkdir(parents=True, exist_ok=True)
156
+ open_cs = []
157
+ for yaml_file in _changesets_dir().glob("*.yaml"):
158
+ try:
159
+ with open(yaml_file) as f:
160
+ data = yaml.safe_load(f)
161
+ if data and data.get("status") == "in_progress":
162
+ open_cs.append({
163
+ "id": data["id"],
164
+ "description": data.get("description", ""),
165
+ "created": data.get("created", ""),
166
+ "files_pending": data.get("files_pending", []),
167
+ "blocker": data.get("blocker"),
168
+ })
169
+ except Exception as e:
170
+ try:
171
+ from mcp_server.crash_logger import log_crash
172
+ log_crash(e, context=f"list_open_changesets: reading {yaml_file.name}")
173
+ except Exception:
174
+ pass
175
+
176
+ return {
177
+ "open_changesets": open_cs,
178
+ "count": len(open_cs),
179
+ "warning": "Complete or document blockers before starting new work." if open_cs else None,
180
+ }
181
+
182
+
183
+ def update_node_after_change(file_path: str, changes: dict[str, Any]) -> dict[str, Any]:
184
+ """
185
+ Update a graph node's metadata after making changes to that file.
186
+ Called by the documenter agent at session end.
187
+
188
+ Args:
189
+ file_path: The file that was changed
190
+ changes: Dict with any of: last_changed_by, new_rules (list), new_connections (list)
191
+ """
192
+ from mcp_server.tools.graph import update_node
193
+
194
+ translated_changes: dict[str, Any] = {}
195
+ passthrough_fields = {"key_functions", "stability", "do_not_revert"}
196
+
197
+ for field in passthrough_fields:
198
+ if field in changes:
199
+ translated_changes[field] = changes[field]
200
+
201
+ if "new_rules" in changes:
202
+ translated_changes["rules"] = changes["new_rules"]
203
+ if "new_connections" in changes:
204
+ translated_changes["connects_to"] = changes["new_connections"]
205
+
206
+ result = update_node(file_path, translated_changes)
207
+
208
+ if "error" in result:
209
+ return {
210
+ "success": False,
211
+ "message": result["error"],
212
+ }
213
+
214
+ response = {
215
+ "success": True,
216
+ "updated_node": file_path,
217
+ }
218
+ if "last_changed_by" in changes:
219
+ response["note"] = (
220
+ "Node metadata updated. 'last_changed_by' is not persisted in the SQLite graph schema."
221
+ )
222
+
223
+ return response
@@ -0,0 +1,335 @@
1
+ """
2
+ code_reader.py — MCP tools for reading source files
3
+
4
+ get_signature(file_path) → skeleton: all public symbols, signatures, docstrings, line ranges
5
+ get_code(file_path, symbol) → full source of one named function or class from disk
6
+
7
+ Always reads from disk. No index, no ChromaDB, no staleness risk.
8
+
9
+ Language support:
10
+ - Python: stdlib ast module (full support)
11
+ - TypeScript, Go, Rust: tree-sitter grammars via treesitter_parser
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import ast
16
+ from pathlib import Path
17
+
18
+ from mcp_server.paths import get_project_root
19
+ from indexer.treesitter_parser import (
20
+ parse_file as ts_parse_file,
21
+ get_symbol_source as ts_get_symbol_source,
22
+ get_language as ts_get_language,
23
+ EXTENSION_MAP as TS_EXTENSION_MAP,
24
+ )
25
+
26
+
27
+ def _resolve(file_path: str) -> Path:
28
+ """Resolve relative file path to absolute using the project root."""
29
+ p = Path(file_path)
30
+ if p.is_absolute():
31
+ return p
32
+ return get_project_root() / p
33
+
34
+
35
+ def _is_private(name: str) -> bool:
36
+ return name.startswith("_")
37
+
38
+
39
+ def _node_kind(node: ast.AST) -> str:
40
+ if isinstance(node, ast.ClassDef):
41
+ return "class"
42
+ return "function"
43
+
44
+
45
+ def _signature_line(source_lines: list[str], node: ast.AST) -> str:
46
+ """Return the def/class line (first line of the node), stripped."""
47
+ return source_lines[node.lineno - 1].strip()
48
+
49
+
50
+ def get_signature(file_path: str) -> dict:
51
+ """
52
+ Get the skeleton of a source file: all public function and class names,
53
+ their signatures, docstrings, and line ranges.
54
+
55
+ Supports Python, TypeScript, Go, and Rust.
56
+
57
+ Args:
58
+ file_path: Relative path from project root (e.g. 'src/services/generator.py')
59
+
60
+ Returns:
61
+ dict with module_docstring, symbols list, and file metadata
62
+ """
63
+ abs_path = _resolve(file_path)
64
+
65
+ if not abs_path.exists():
66
+ return {
67
+ "found": False,
68
+ "file_path": file_path,
69
+ "error": f"File not found: {abs_path}",
70
+ }
71
+
72
+ ext = abs_path.suffix.lower()
73
+
74
+ # Non-Python: dispatch to tree-sitter
75
+ if ext in TS_EXTENSION_MAP:
76
+ return _get_signature_treesitter(file_path, abs_path)
77
+
78
+ # Python: existing ast-based extraction
79
+ if ext == ".py":
80
+ return _get_signature_python(file_path, abs_path)
81
+
82
+ return {
83
+ "found": False,
84
+ "file_path": file_path,
85
+ "error": f"Unsupported file type: {ext}. Supported: .py, .ts, .tsx, .go, .rs",
86
+ }
87
+
88
+
89
+ def _get_signature_treesitter(file_path: str, abs_path: Path) -> dict:
90
+ """Get file skeleton using tree-sitter for TS/Go/Rust files."""
91
+ try:
92
+ parsed = ts_parse_file(str(abs_path))
93
+ except (ValueError, FileNotFoundError) as e:
94
+ return {
95
+ "found": False,
96
+ "file_path": file_path,
97
+ "error": str(e),
98
+ }
99
+
100
+ symbols = []
101
+ for sym in parsed.symbols:
102
+ if not sym.is_public:
103
+ continue
104
+
105
+ entry = {
106
+ "name": sym.name,
107
+ "kind": sym.kind,
108
+ "signature_line": sym.signature_line,
109
+ "start_line": sym.start_line,
110
+ "end_line": sym.end_line,
111
+ }
112
+ if sym.docstring:
113
+ first_line = sym.docstring.strip().splitlines()[0]
114
+ entry["docstring"] = first_line
115
+
116
+ if sym.methods:
117
+ entry["public_methods"] = sym.methods
118
+
119
+ symbols.append(entry)
120
+
121
+ return {
122
+ "found": True,
123
+ "file_path": file_path,
124
+ "language": parsed.language,
125
+ "module_docstring": parsed.module_docstring,
126
+ "symbol_count": len(symbols),
127
+ "symbols": symbols,
128
+ "hint": "Use get_code(file_path, symbol) to read the full body of any symbol listed above.",
129
+ }
130
+
131
+
132
+ def _get_signature_python(file_path: str, abs_path: Path) -> dict:
133
+ """Get file skeleton using Python ast module."""
134
+ source = abs_path.read_text(encoding="utf-8")
135
+ source_lines = source.splitlines()
136
+
137
+ try:
138
+ tree = ast.parse(source, filename=str(abs_path))
139
+ except SyntaxError as e:
140
+ return {
141
+ "found": False,
142
+ "file_path": file_path,
143
+ "error": f"Syntax error: {e}",
144
+ }
145
+
146
+ module_docstring = ast.get_docstring(tree) or None
147
+
148
+ symbols = []
149
+ for node in tree.body:
150
+ if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
151
+ continue
152
+ if _is_private(node.name):
153
+ continue
154
+
155
+ kind = _node_kind(node)
156
+ sig_line = _signature_line(source_lines, node)
157
+ docstring = ast.get_docstring(node) or None
158
+
159
+ entry = {
160
+ "name": node.name,
161
+ "kind": kind,
162
+ "signature_line": sig_line,
163
+ "start_line": node.lineno,
164
+ "end_line": node.end_lineno,
165
+ }
166
+ if docstring:
167
+ first_line = docstring.strip().splitlines()[0]
168
+ entry["docstring"] = first_line
169
+
170
+ if kind == "class":
171
+ methods = []
172
+ for child in ast.walk(node):
173
+ if child is node:
174
+ continue
175
+ if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
176
+ if not _is_private(child.name) and child.name != "__init__":
177
+ methods.append(child.name)
178
+ if methods:
179
+ entry["public_methods"] = methods
180
+
181
+ symbols.append(entry)
182
+
183
+ return {
184
+ "found": True,
185
+ "file_path": file_path,
186
+ "module_docstring": module_docstring,
187
+ "symbol_count": len(symbols),
188
+ "symbols": symbols,
189
+ "hint": "Use get_code(file_path, symbol) to read the full body of any symbol listed above.",
190
+ }
191
+
192
+
193
+ def get_code(file_path: str, symbol: str | None = None) -> dict:
194
+ """
195
+ Get the full source of a single function or class by name.
196
+ Always reads from disk — always current.
197
+
198
+ Supports Python, TypeScript, Go, and Rust.
199
+
200
+ Args:
201
+ file_path: Relative path from project root
202
+ symbol: Function or class name. Omit to get module-level constants/assignments only.
203
+
204
+ Returns:
205
+ dict with source, start_line, end_line, kind, docstring
206
+ """
207
+ abs_path = _resolve(file_path)
208
+
209
+ if not abs_path.exists():
210
+ return {
211
+ "found": False,
212
+ "file_path": file_path,
213
+ "symbol": symbol,
214
+ "error": f"File not found: {abs_path}",
215
+ }
216
+
217
+ ext = abs_path.suffix.lower()
218
+
219
+ # Non-Python: dispatch to tree-sitter
220
+ if ext in TS_EXTENSION_MAP:
221
+ return _get_code_treesitter(file_path, abs_path, symbol)
222
+
223
+ # Python: existing ast-based extraction
224
+ if ext == ".py":
225
+ return _get_code_python(file_path, abs_path, symbol)
226
+
227
+ return {
228
+ "found": False,
229
+ "file_path": file_path,
230
+ "symbol": symbol,
231
+ "error": f"Unsupported file type: {ext}. Supported: .py, .ts, .tsx, .go, .rs",
232
+ }
233
+
234
+
235
+ def _get_code_treesitter(file_path: str, abs_path: Path, symbol: str | None) -> dict:
236
+ """Get symbol source using tree-sitter for TS/Go/Rust files."""
237
+ # symbol=None → return module-level info
238
+ if symbol is None:
239
+ try:
240
+ parsed = ts_parse_file(str(abs_path))
241
+ except (ValueError, FileNotFoundError) as e:
242
+ return {
243
+ "found": False,
244
+ "file_path": file_path,
245
+ "symbol": None,
246
+ "error": str(e),
247
+ }
248
+ return {
249
+ "found": True,
250
+ "file_path": file_path,
251
+ "symbol": None,
252
+ "kind": "module_info",
253
+ "language": parsed.language,
254
+ "module_docstring": parsed.module_docstring,
255
+ "imports": [imp.module for imp in parsed.imports],
256
+ }
257
+
258
+ # Named symbol lookup
259
+ result = ts_get_symbol_source(str(abs_path), symbol)
260
+ result["file_path"] = file_path
261
+ return result
262
+
263
+
264
+ def _get_code_python(file_path: str, abs_path: Path, symbol: str | None) -> dict:
265
+ """Get symbol source using Python ast module."""
266
+ source = abs_path.read_text(encoding="utf-8")
267
+ source_lines = source.splitlines()
268
+
269
+ try:
270
+ tree = ast.parse(source, filename=str(abs_path))
271
+ except SyntaxError as e:
272
+ return {
273
+ "found": False,
274
+ "file_path": file_path,
275
+ "symbol": symbol,
276
+ "error": f"Syntax error: {e}",
277
+ }
278
+
279
+ # symbol=None → return module-level non-function, non-class content
280
+ if symbol is None:
281
+ module_docstring = ast.get_docstring(tree) or None
282
+ assignments = []
283
+ for node in tree.body:
284
+ if isinstance(node, (ast.Assign, ast.AnnAssign, ast.AugAssign)):
285
+ lines = source_lines[node.lineno - 1 : node.end_lineno]
286
+ assignments.append({
287
+ "start_line": node.lineno,
288
+ "end_line": node.end_lineno,
289
+ "source": "\n".join(lines),
290
+ })
291
+ return {
292
+ "found": True,
293
+ "file_path": file_path,
294
+ "symbol": None,
295
+ "kind": "module_constants",
296
+ "module_docstring": module_docstring,
297
+ "assignments": assignments,
298
+ }
299
+
300
+ # Walk all nodes to find functions inside classes too
301
+ for node in ast.walk(tree):
302
+ if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
303
+ continue
304
+ if node.name != symbol:
305
+ continue
306
+
307
+ kind = _node_kind(node)
308
+ docstring = ast.get_docstring(node) or None
309
+ lines = source_lines[node.lineno - 1 : node.end_lineno]
310
+
311
+ return {
312
+ "found": True,
313
+ "file_path": file_path,
314
+ "symbol": symbol,
315
+ "kind": kind,
316
+ "start_line": node.lineno,
317
+ "end_line": node.end_lineno,
318
+ "docstring": docstring,
319
+ "source": "\n".join(lines),
320
+ }
321
+
322
+ # Symbol not found — provide available symbol names as a hint
323
+ available = []
324
+ for node in ast.walk(tree):
325
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
326
+ available.append(node.name)
327
+
328
+ return {
329
+ "found": False,
330
+ "file_path": file_path,
331
+ "symbol": symbol,
332
+ "error": f"Symbol '{symbol}' not found in {file_path}",
333
+ "available_symbols": sorted(set(available)),
334
+ "hint": "Call get_signature(file_path) to see public symbols with line ranges.",
335
+ }