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,172 @@
1
+ """
2
+ Outcome Tracker — Git-based feedback loop for Codevira's adaptive memory.
3
+
4
+ After an agent session ends and changes are committed, this module analyzes
5
+ what happened to the agent's changes:
6
+ - 'kept': Code survived untouched in subsequent commits
7
+ - 'modified': Developer edited the agent's output (correction signal)
8
+ - 'reverted': Code was reverted within N commits (negative signal)
9
+
10
+ This feedback feeds into confidence scoring, preference learning, and
11
+ automatic rule generation.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import difflib
16
+ import logging
17
+ import subprocess
18
+ from pathlib import Path
19
+
20
+ from mcp_server.paths import get_data_dir, get_project_root
21
+ from indexer.sqlite_graph import SQLiteGraph
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ def _project_root():
27
+ return get_project_root()
28
+
29
+
30
+ def _git_cmd(*args: str) -> str | None:
31
+ try:
32
+ return subprocess.check_output(
33
+ ["git", "-C", str(_project_root())] + list(args),
34
+ stderr=subprocess.DEVNULL,
35
+ ).decode("utf-8", errors="replace").strip()
36
+ except (subprocess.CalledProcessError, FileNotFoundError):
37
+ return None
38
+
39
+
40
+ def analyze_session_outcomes(session_id: str | None = None):
41
+ """
42
+ Analyze git history to determine outcomes for recent sessions.
43
+ If session_id is provided, only analyzes that session.
44
+ Otherwise, analyzes all sessions that don't yet have outcomes.
45
+ """
46
+ db = SQLiteGraph(get_data_dir() / "graph" / "graph.db")
47
+
48
+ try:
49
+ if session_id:
50
+ sessions = [{"session_id": session_id}]
51
+ else:
52
+ # Find sessions that have decisions but no outcomes yet
53
+ cur = db.conn.execute('''
54
+ SELECT DISTINCT d.session_id FROM decisions d
55
+ LEFT JOIN outcomes o ON d.session_id = o.session_id
56
+ WHERE o.id IS NULL
57
+ ORDER BY d.created_at DESC LIMIT 20
58
+ ''')
59
+ sessions = [dict(r) for r in cur.fetchall()]
60
+
61
+ for sess in sessions:
62
+ sid = sess["session_id"]
63
+ _analyze_single_session(db, sid)
64
+ finally:
65
+ db.close()
66
+
67
+
68
+ def _analyze_single_session(db: SQLiteGraph, session_id: str):
69
+ """Analyze outcomes for a single session's decisions."""
70
+ decisions = db.conn.execute('''
71
+ SELECT id, file_path, decision, created_at FROM decisions
72
+ WHERE session_id = ? AND file_path IS NOT NULL
73
+ ''', (session_id,)).fetchall()
74
+
75
+ if not decisions:
76
+ return
77
+
78
+ for dec in decisions:
79
+ file_path = dec["file_path"]
80
+ decision_id = dec["id"]
81
+ created_at = dec["created_at"]
82
+
83
+ outcome = _determine_file_outcome(file_path, created_at)
84
+ if outcome:
85
+ db.record_outcome(
86
+ session_id=session_id,
87
+ file_path=file_path,
88
+ outcome_type=outcome["type"],
89
+ decision_id=decision_id,
90
+ delta_summary=outcome.get("delta"),
91
+ )
92
+
93
+ # If modified, try to learn preferences from the diff
94
+ if outcome["type"] == "modified" and outcome.get("delta"):
95
+ _learn_from_modification(db, file_path, outcome["delta"])
96
+
97
+
98
+ def _determine_file_outcome(file_path: str, session_date: str) -> dict | None:
99
+ """
100
+ Check git history to see what happened to a file after a session.
101
+ Returns {'type': 'kept'|'modified'|'reverted', 'delta': ...}
102
+ """
103
+ abs_path = _project_root() / file_path
104
+ if not abs_path.exists():
105
+ return {"type": "reverted", "delta": "File no longer exists"}
106
+
107
+ # Normalize date to ISO 8601 for git --since compatibility
108
+ try:
109
+ from datetime import datetime
110
+ dt = datetime.fromisoformat(session_date.replace(" ", "T"))
111
+ since_date = dt.isoformat()
112
+ except (ValueError, AttributeError):
113
+ since_date = session_date
114
+
115
+ # Get commits touching this file after the session date
116
+ log_output = _git_cmd(
117
+ "log", "--oneline", "--follow", f"--since={since_date}",
118
+ "--", file_path
119
+ )
120
+
121
+ if not log_output:
122
+ return {"type": "kept", "delta": None}
123
+
124
+ commits = log_output.split("\n")
125
+ if not commits or commits == [""]:
126
+ return {"type": "kept", "delta": None}
127
+
128
+ # Check if any commit message suggests a revert
129
+ for commit_line in commits:
130
+ lower = commit_line.lower()
131
+ if any(word in lower for word in ["revert", "undo", "rollback", "roll back"]):
132
+ return {"type": "reverted", "delta": commit_line}
133
+
134
+ # If there are subsequent commits but no revert, it was modified
135
+ if len(commits) >= 1:
136
+ # Get a summary of changes
137
+ diff_stat = _git_cmd("diff", "--stat", f"HEAD~{min(len(commits), 5)}", "--", file_path)
138
+ if not diff_stat:
139
+ logger.debug("Could not get diff stats for %s, using commit count", file_path)
140
+ return {"type": "modified", "delta": diff_stat or f"{len(commits)} subsequent commits"}
141
+
142
+ return {"type": "kept", "delta": None}
143
+
144
+
145
+ def _learn_from_modification(db: SQLiteGraph, file_path: str, delta: str):
146
+ """
147
+ When a developer modifies agent output, try to extract preference signals.
148
+ This is a lightweight heuristic — not perfect, but builds up over time.
149
+ """
150
+ # Detect naming convention changes
151
+ if "camelCase" in delta or "snake_case" in delta:
152
+ db.record_preference("naming", "Prefers consistent naming convention", example=file_path)
153
+
154
+ # Detect structural patterns from file extension
155
+ ext = Path(file_path).suffix
156
+ if ext in ('.py', '.ts', '.tsx', '.go', '.rs'):
157
+ db.record_preference("structure", f"Developer modifies AI output in {ext} files", example=file_path)
158
+
159
+
160
+ def get_file_outcome_summary(file_path: str) -> dict:
161
+ """Get a summary of all outcomes for a specific file."""
162
+ db = SQLiteGraph(get_data_dir() / "graph" / "graph.db")
163
+ try:
164
+ outcomes = db.get_outcomes_for_file(file_path)
165
+ confidence = db.get_decision_confidence(file_path=file_path)
166
+ return {
167
+ "file_path": file_path,
168
+ "outcomes": outcomes,
169
+ "confidence": confidence,
170
+ }
171
+ finally:
172
+ db.close()
@@ -0,0 +1,186 @@
1
+ """
2
+ Rule Learner — Automatic rule generation from observed patterns.
3
+
4
+ Analyzes session decisions and outcomes to infer recurring patterns
5
+ and generate rules that future agents can use. Rules are stored in
6
+ SQLite and served alongside static rules from rules/*.md.
7
+
8
+ This is the engine that makes Codevira's memory adaptive:
9
+ the more sessions that happen, the less ambiguous future decisions become.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import logging
15
+ import re
16
+ from collections import Counter, defaultdict
17
+ from pathlib import Path
18
+
19
+ from mcp_server.paths import get_data_dir
20
+ from indexer.sqlite_graph import SQLiteGraph
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ def run_rule_inference():
26
+ """
27
+ Main entry point: analyze all decisions and outcomes,
28
+ detect patterns, and create or update learned rules.
29
+ """
30
+ db = SQLiteGraph(get_data_dir() / "graph" / "graph.db")
31
+ try:
32
+ _infer_test_pairing_rules(db)
33
+ _infer_import_pattern_rules(db)
34
+ _infer_decision_pattern_rules(db)
35
+ _infer_file_co_change_rules(db)
36
+ finally:
37
+ db.close()
38
+
39
+
40
+ def _infer_test_pairing_rules(db: SQLiteGraph):
41
+ """Detect test file pairing patterns (e.g., src/foo.py always has tests/test_foo.py)."""
42
+ nodes = db.list_file_nodes()
43
+ test_files = [n for n in nodes if n.get("layer") == "test"]
44
+ source_files = [n for n in nodes if n.get("layer") != "test"]
45
+
46
+ pairings = Counter()
47
+ for tf in test_files:
48
+ test_path = tf["file_path"]
49
+ for sf in source_files:
50
+ src_path = sf["file_path"]
51
+ src_stem = Path(src_path).stem
52
+ if src_stem in test_path:
53
+ # Found a pairing pattern
54
+ src_dir = str(Path(src_path).parent)
55
+ test_dir = str(Path(test_path).parent)
56
+ pairings[(src_dir, test_dir)] += 1
57
+
58
+ for (src_dir, test_dir), count in pairings.items():
59
+ if count >= 2:
60
+ rule_text = f"Files in '{src_dir}/' should have corresponding tests in '{test_dir}/'."
61
+ confidence = min(count / 5.0, 1.0) # Max confidence at 5+ pairings
62
+ _upsert_rule(db, rule_text, confidence, category="testing", file_pattern=f"{src_dir}/*")
63
+
64
+
65
+ def _infer_import_pattern_rules(db: SQLiteGraph):
66
+ """Detect common import patterns from the dependency graph edges."""
67
+ edges = db.get_all_edges()
68
+ if not edges:
69
+ return
70
+
71
+ # Count how many files import each target
72
+ import_counts = Counter()
73
+ for edge in edges:
74
+ if edge["kind"] == "imports":
75
+ import_counts[edge["target_id"]] += 1
76
+
77
+ # Files imported by many others are "core" and should be stable
78
+ for target_id, count in import_counts.items():
79
+ if count >= 3:
80
+ file_path = target_id.replace("file:", "")
81
+ rule_text = f"'{file_path}' is imported by {count} files — changes here have wide blast radius. Review carefully."
82
+ confidence = min(count / 10.0, 0.95)
83
+ _upsert_rule(db, rule_text, confidence, category="imports", file_pattern=file_path)
84
+
85
+
86
+ def _infer_decision_pattern_rules(db: SQLiteGraph):
87
+ """Detect recurring decision patterns from session history."""
88
+ decisions = db.conn.execute('''
89
+ SELECT d.decision, d.file_path, o.outcome_type
90
+ FROM decisions d
91
+ LEFT JOIN outcomes o ON d.id = o.decision_id
92
+ WHERE d.decision IS NOT NULL
93
+ ORDER BY d.created_at DESC LIMIT 200
94
+ ''').fetchall()
95
+
96
+ if len(decisions) < 3:
97
+ return
98
+
99
+ # Group decisions by file directory to find area-specific patterns
100
+ dir_decisions = defaultdict(list)
101
+ for dec in decisions:
102
+ if dec["file_path"]:
103
+ dir_name = str(Path(dec["file_path"]).parent)
104
+ dir_decisions[dir_name].append({
105
+ "decision": dec["decision"],
106
+ "outcome": dec["outcome_type"],
107
+ })
108
+
109
+ # Look for repeated decision keywords per directory
110
+ for dir_name, decs in dir_decisions.items():
111
+ if len(decs) < 2:
112
+ continue
113
+
114
+ # Extract common phrases from successful decisions
115
+ successful = [d["decision"] for d in decs if d.get("outcome") in ("kept", None)]
116
+ if len(successful) >= 2:
117
+ common = _find_common_phrases(successful)
118
+ for phrase, count in common:
119
+ if count >= 2 and len(phrase) > 10:
120
+ rule_text = f"In '{dir_name}/': recurring pattern — {phrase}"
121
+ confidence = min(count / 5.0, 0.9)
122
+ _upsert_rule(db, rule_text, confidence, category="patterns", file_pattern=f"{dir_name}/*")
123
+
124
+
125
+ def _infer_file_co_change_rules(db: SQLiteGraph):
126
+ """Detect files that are frequently modified together across sessions."""
127
+ sessions = db.conn.execute('''
128
+ SELECT session_id, GROUP_CONCAT(DISTINCT file_path) as files
129
+ FROM decisions
130
+ WHERE file_path IS NOT NULL
131
+ GROUP BY session_id
132
+ HAVING COUNT(DISTINCT file_path) >= 2
133
+ ''').fetchall()
134
+
135
+ if len(sessions) < 2:
136
+ return
137
+
138
+ co_change = Counter()
139
+ for sess in sessions:
140
+ files = sorted(sess["files"].split(","))
141
+ for i, f1 in enumerate(files):
142
+ for f2 in files[i + 1:]:
143
+ co_change[(f1, f2)] += 1
144
+
145
+ for (f1, f2), count in co_change.items():
146
+ if count >= 2:
147
+ rule_text = f"'{Path(f1).name}' and '{Path(f2).name}' are frequently modified together. Changes to one likely require changes to the other."
148
+ confidence = min(count / 4.0, 0.9)
149
+ _upsert_rule(db, rule_text, confidence, category="structure")
150
+
151
+
152
+ def _find_common_phrases(texts: list[str], min_words: int = 3) -> list[tuple[str, int]]:
153
+ """Find common multi-word phrases across a list of texts."""
154
+ phrase_counts = Counter()
155
+ for text in texts:
156
+ words = re.findall(r'\b\w+\b', text.lower())
157
+ for length in range(min_words, min(len(words) + 1, 8)):
158
+ for i in range(len(words) - length + 1):
159
+ phrase = " ".join(words[i:i + length])
160
+ phrase_counts[phrase] += 1
161
+
162
+ # Return phrases that appear in multiple texts
163
+ return [(phrase, count) for phrase, count in phrase_counts.most_common(10) if count >= 2]
164
+
165
+
166
+ def _upsert_rule(db: SQLiteGraph, rule_text: str, confidence: float,
167
+ category: str, file_pattern: str | None = None):
168
+ """Insert a new learned rule or update confidence if a similar one exists."""
169
+ with db.transaction() as conn:
170
+ existing = conn.execute(
171
+ 'SELECT id, confidence FROM learned_rules WHERE rule_text = ?',
172
+ (rule_text,)
173
+ ).fetchone()
174
+
175
+ if existing:
176
+ # Update confidence (weighted average — new evidence matters)
177
+ new_confidence = (existing["confidence"] * 0.7) + (confidence * 0.3)
178
+ conn.execute(
179
+ 'UPDATE learned_rules SET confidence = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
180
+ (new_confidence, existing["id"]),
181
+ )
182
+ else:
183
+ conn.execute(
184
+ 'INSERT INTO learned_rules (rule_text, confidence, source_sessions, category, file_pattern) VALUES (?, ?, ?, ?, ?)',
185
+ (rule_text, confidence, json.dumps([]), category, file_pattern),
186
+ )