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,284 @@
1
+ """
2
+ gitignore.py — .gitignore-aware file discovery for Codevira v1.6.
3
+
4
+ Replaces the old "scan fixed watched_dirs" approach with a model that:
5
+ 1. Walks the entire project tree
6
+ 2. Respects .gitignore + nested .gitignore files (via pathspec)
7
+ 3. Always skips a safety-net of well-known noise directories
8
+ 4. Optionally filters by config.yaml watched_dirs / file_extensions overrides
9
+
10
+ Usage:
11
+ from mcp_server.gitignore import discover_source_files, infer_language_from_files
12
+
13
+ files = discover_source_files(project_root)
14
+ lang = infer_language_from_files(files)
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import os
19
+ from collections import Counter
20
+ from pathlib import Path
21
+
22
+ try:
23
+ import pathspec
24
+ _PATHSPEC_AVAILABLE = True
25
+ except ImportError:
26
+ _PATHSPEC_AVAILABLE = False
27
+
28
+
29
+ # Directories that are ALWAYS skipped regardless of .gitignore contents.
30
+ # These are well-known noise directories that developers almost never want
31
+ # to index: build artifacts, dependency caches, IDE state, etc.
32
+ _SAFETY_NET_DIRS: frozenset[str] = frozenset({
33
+ ".git",
34
+ "node_modules",
35
+ ".venv",
36
+ "venv",
37
+ "__pycache__",
38
+ ".tox",
39
+ ".mypy_cache",
40
+ ".pytest_cache",
41
+ ".ruff_cache",
42
+ ".next",
43
+ ".nuxt",
44
+ ".turbo",
45
+ ".cache",
46
+ "dist",
47
+ "build",
48
+ "out",
49
+ ".build",
50
+ ".svelte-kit",
51
+ ".parcel-cache",
52
+ "coverage",
53
+ ".nyc_output",
54
+ "target", # Rust / Maven build output
55
+ "vendor", # Go / PHP vendor dirs
56
+ ".codevira", # Our own data dir
57
+ ".codevira.migrated",
58
+ })
59
+
60
+ # File name suffixes (extensions) that are never indexed.
61
+ _SKIP_EXTENSIONS: frozenset[str] = frozenset({
62
+ ".pyc", ".pyo", ".pyd",
63
+ ".so", ".dylib", ".dll", ".exe",
64
+ ".o", ".a", ".lib",
65
+ ".jpg", ".jpeg", ".png", ".gif", ".svg", ".ico", ".webp",
66
+ ".mp3", ".mp4", ".wav", ".avi", ".mov",
67
+ ".zip", ".tar", ".gz", ".bz2", ".xz", ".7z",
68
+ ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx",
69
+ ".bin", ".dat", ".db", ".sqlite", ".sqlite3",
70
+ ".lock", # package-lock.json, Cargo.lock, etc. — not useful for search
71
+ })
72
+
73
+ # Extension → language mapping for language inference
74
+ _EXTENSION_LANGUAGE: dict[str, str] = {
75
+ ".py": "python",
76
+ ".ts": "typescript", ".tsx": "typescript",
77
+ ".js": "javascript", ".jsx": "javascript", ".mjs": "javascript", ".cjs": "javascript",
78
+ ".go": "go",
79
+ ".rs": "rust",
80
+ ".java": "java",
81
+ ".kt": "kotlin", ".kts": "kotlin",
82
+ ".cs": "csharp",
83
+ ".rb": "ruby",
84
+ ".php": "php",
85
+ ".c": "c", ".h": "c",
86
+ ".cpp": "cpp", ".cc": "cpp", ".cxx": "cpp", ".hpp": "cpp",
87
+ ".swift": "swift",
88
+ ".sol": "solidity",
89
+ ".vue": "vue",
90
+ ".svelte": "svelte",
91
+ ".scala": "scala",
92
+ ".ex": "elixir", ".exs": "elixir",
93
+ ".hs": "haskell",
94
+ ".ml": "ocaml", ".mli": "ocaml",
95
+ ".clj": "clojure", ".cljs": "clojure",
96
+ ".dart": "dart",
97
+ ".r": "r", ".R": "r",
98
+ ".lua": "lua",
99
+ ".sh": "shell", ".bash": "shell", ".zsh": "shell",
100
+ ".tf": "terraform", ".tfvars": "terraform",
101
+ ".graphql": "graphql", ".gql": "graphql",
102
+ ".proto": "protobuf",
103
+ ".sql": "sql",
104
+ ".prisma": "prisma",
105
+ ".yaml": "yaml", ".yml": "yaml",
106
+ ".toml": "toml",
107
+ ".json": "json",
108
+ ".md": "markdown", ".mdx": "markdown",
109
+ ".html": "html", ".htm": "html",
110
+ ".css": "css", ".scss": "css", ".sass": "css", ".less": "css",
111
+ }
112
+
113
+
114
+ def load_gitignore_spec(project_root: Path) -> "pathspec.PathSpec | None":
115
+ """Load and merge all .gitignore files from the project tree.
116
+
117
+ Recursively finds all .gitignore files (root and nested) and builds a
118
+ combined pathspec. Patterns in nested .gitignore files are prefixed with
119
+ their relative directory so they apply only under that subtree.
120
+
121
+ Returns None if pathspec is not installed (fail-open — no exclusions).
122
+ """
123
+ if not _PATHSPEC_AVAILABLE:
124
+ return None
125
+
126
+ lines: list[str] = []
127
+
128
+ for root, dirs, files in os.walk(str(project_root)):
129
+ # Prune safety-net dirs so we don't descend into them
130
+ dirs[:] = [d for d in dirs if d not in _SAFETY_NET_DIRS]
131
+
132
+ if ".gitignore" in files:
133
+ rel_dir = Path(root).relative_to(project_root)
134
+ try:
135
+ content = (Path(root) / ".gitignore").read_text(errors="replace")
136
+ except OSError:
137
+ continue
138
+
139
+ for line in content.splitlines():
140
+ stripped = line.strip()
141
+ if not stripped or stripped.startswith("#"):
142
+ continue
143
+ # Prefix nested .gitignore rules with the subdirectory
144
+ if rel_dir != Path("."):
145
+ # Pathspec supports dir-prefixed patterns like "src/*.pyc"
146
+ prefix = str(rel_dir).replace("\\", "/")
147
+ if stripped.startswith("!"):
148
+ lines.append(f"!{prefix}/{stripped[1:]}")
149
+ elif stripped.startswith("/"):
150
+ lines.append(f"{prefix}{stripped}")
151
+ else:
152
+ lines.append(f"{prefix}/{stripped}")
153
+ else:
154
+ lines.append(stripped)
155
+
156
+ if not lines:
157
+ return None
158
+
159
+ return pathspec.PathSpec.from_lines("gitignore", lines)
160
+
161
+
162
+ def discover_source_files(
163
+ project_root: Path,
164
+ config_overrides: dict | None = None,
165
+ ) -> list[Path]:
166
+ """Walk the project tree and return all source files to index.
167
+
168
+ Exclusion order (highest priority first):
169
+ 1. Safety-net directories (always skipped)
170
+ 2. .gitignore patterns (skipped if pathspec available)
171
+ 3. Skip extensions (_SKIP_EXTENSIONS — binaries, compiled outputs, etc.)
172
+
173
+ If config_overrides provides watched_dirs and/or file_extensions, those
174
+ act as an allowlist filter on top of the above exclusions (backward compat
175
+ with projects that have explicit config.yaml settings).
176
+
177
+ Args:
178
+ project_root: Absolute path to the project root.
179
+ config_overrides: Dict with optional keys:
180
+ - watched_dirs: list[str] — restrict to these subdirs
181
+ - file_extensions: list[str] — restrict to these extensions
182
+ - skip_dirs: list[str] — additional dirs to skip
183
+
184
+ Returns:
185
+ Sorted list of absolute Path objects for all indexable source files.
186
+ """
187
+ spec = load_gitignore_spec(project_root)
188
+
189
+ extra_skip_dirs: set[str] = set()
190
+ allowed_extensions: set[str] | None = None
191
+ allowed_dirs: list[Path] | None = None
192
+
193
+ if config_overrides:
194
+ if "skip_dirs" in config_overrides:
195
+ extra_skip_dirs = set(config_overrides["skip_dirs"])
196
+ if "file_extensions" in config_overrides:
197
+ allowed_extensions = set(config_overrides["file_extensions"])
198
+ if "watched_dirs" in config_overrides:
199
+ allowed_dirs = [
200
+ project_root / d for d in config_overrides["watched_dirs"]
201
+ if (project_root / d).exists()
202
+ ]
203
+
204
+ skip_dirs_all = _SAFETY_NET_DIRS | extra_skip_dirs
205
+ result: list[Path] = []
206
+
207
+ for root, dirs, files in os.walk(str(project_root)):
208
+ root_path = Path(root)
209
+
210
+ # Prune skipped directories in-place so os.walk doesn't descend
211
+ dirs[:] = [
212
+ d for d in dirs
213
+ if d not in skip_dirs_all
214
+ ]
215
+
216
+ # If watched_dirs override is set, skip dirs not under any allowed dir
217
+ if allowed_dirs is not None:
218
+ dirs[:] = [
219
+ d for d in dirs
220
+ if any(
221
+ str(root_path / d).startswith(str(a)) or a == root_path / d
222
+ for a in allowed_dirs
223
+ )
224
+ ]
225
+ # Also check the current root itself
226
+ if not any(
227
+ str(root_path).startswith(str(a)) or root_path == project_root
228
+ for a in allowed_dirs
229
+ ):
230
+ if root_path != project_root:
231
+ dirs.clear()
232
+ continue
233
+
234
+ for fname in files:
235
+ fpath = root_path / fname
236
+ suffix = fpath.suffix.lower()
237
+
238
+ # Skip known non-source extensions
239
+ if suffix in _SKIP_EXTENSIONS:
240
+ continue
241
+
242
+ # Apply watched_dirs filter
243
+ if allowed_dirs is not None:
244
+ if not any(
245
+ str(fpath).startswith(str(a))
246
+ for a in allowed_dirs
247
+ ):
248
+ continue
249
+
250
+ # Apply file_extensions filter from config
251
+ if allowed_extensions is not None and suffix not in allowed_extensions:
252
+ continue
253
+
254
+ # Apply .gitignore spec
255
+ if spec is not None:
256
+ try:
257
+ rel = str(fpath.relative_to(project_root)).replace("\\", "/")
258
+ if spec.match_file(rel):
259
+ continue
260
+ except ValueError:
261
+ pass
262
+
263
+ result.append(fpath)
264
+
265
+ result.sort()
266
+ return result
267
+
268
+
269
+ def infer_language_from_files(files: list[Path]) -> str:
270
+ """Infer the dominant programming language from a list of source files.
271
+
272
+ Counts extensions, maps to languages, and returns the most frequent one.
273
+ Falls back to "unknown" if no recognizable extensions are found.
274
+ """
275
+ counts: Counter[str] = Counter()
276
+ for f in files:
277
+ lang = _EXTENSION_LANGUAGE.get(f.suffix.lower())
278
+ if lang:
279
+ counts[lang] += 1
280
+
281
+ if not counts:
282
+ return "unknown"
283
+
284
+ return counts.most_common(1)[0][0]
@@ -0,0 +1,187 @@
1
+ """
2
+ global_sync.py — Sync intelligence between project and global databases.
3
+
4
+ - import_global_to_project(): Called on server startup. Imports qualifying
5
+ global preferences and rules into the project's local database.
6
+ - export_project_to_global(): Called on session end. Pushes qualifying local
7
+ preferences and rules to the global database.
8
+
9
+ All operations are best-effort: if global DB is locked, missing, or corrupt,
10
+ the server operates normally with local memory only.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import logging
16
+ from pathlib import Path
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def import_global_to_project() -> dict:
22
+ """
23
+ Import qualifying global intelligence into the current project's database.
24
+ Called on MCP server startup. Returns summary of what was imported.
25
+ """
26
+ from mcp_server.paths import get_global_db_path, get_data_dir, get_project_root
27
+ from indexer.global_db import GlobalDB
28
+ from indexer.sqlite_graph import SQLiteGraph
29
+
30
+ stats = {"preferences_imported": 0, "rules_imported": 0}
31
+
32
+ global_db_path = get_global_db_path()
33
+ if not global_db_path.exists():
34
+ return stats
35
+
36
+ project_db_path = get_data_dir() / "graph" / "graph.db"
37
+ if not project_db_path.exists():
38
+ return stats
39
+
40
+ # Read project language from config
41
+ language = _get_project_language()
42
+
43
+ global_db = GlobalDB(global_db_path)
44
+ project_db = SQLiteGraph(project_db_path)
45
+
46
+ try:
47
+ # Import preferences with frequency >= 3
48
+ global_prefs = global_db.get_preferences(min_frequency=3)
49
+ for pref in global_prefs:
50
+ # Check if already exists locally
51
+ existing = project_db.conn.execute(
52
+ "SELECT id FROM preferences WHERE category = ? AND signal = ?",
53
+ (pref["category"], pref["signal"]),
54
+ ).fetchone()
55
+ if not existing:
56
+ project_db.record_preference(
57
+ pref["category"], pref["signal"],
58
+ example=pref.get("example"), source="global",
59
+ )
60
+ stats["preferences_imported"] += 1
61
+
62
+ # Import rules with confidence >= 0.6, matching language
63
+ global_rules = global_db.get_rules(min_confidence=0.6, language=language)
64
+ for rule in global_rules:
65
+ existing = project_db.conn.execute(
66
+ "SELECT id FROM learned_rules WHERE rule_text = ?",
67
+ (rule["rule_text"],),
68
+ ).fetchone()
69
+ if not existing:
70
+ # Apply 0.8x confidence decay on import
71
+ decayed_confidence = rule["confidence"] * 0.8
72
+ project_db.add_learned_rule(
73
+ rule["rule_text"], decayed_confidence,
74
+ source_sessions=[], category=rule.get("category"),
75
+ file_pattern=None,
76
+ )
77
+ # Mark as globally sourced
78
+ project_db.conn.execute(
79
+ "UPDATE learned_rules SET source = 'global' WHERE rule_text = ?",
80
+ (rule["rule_text"],),
81
+ )
82
+ project_db.conn.commit()
83
+ stats["rules_imported"] += 1
84
+
85
+ # Register this project in global DB
86
+ project_root = get_project_root()
87
+ project_name = project_root.name
88
+ global_db.register_project(str(project_root), project_name, language or "unknown")
89
+
90
+ finally:
91
+ global_db.close()
92
+ project_db.close()
93
+
94
+ if stats["preferences_imported"] or stats["rules_imported"]:
95
+ logger.info("Global sync imported: %d preferences, %d rules",
96
+ stats["preferences_imported"], stats["rules_imported"])
97
+
98
+ return stats
99
+
100
+
101
+ def export_project_to_global() -> dict:
102
+ """
103
+ Export qualifying local intelligence to the global database.
104
+ Called on session end (write_session_log). Returns summary of what was exported.
105
+ """
106
+ from mcp_server.paths import get_global_db_path, get_data_dir, get_project_root
107
+ from indexer.global_db import GlobalDB
108
+ from indexer.sqlite_graph import SQLiteGraph
109
+
110
+ stats = {"preferences_exported": 0, "rules_exported": 0}
111
+
112
+ project_db_path = get_data_dir() / "graph" / "graph.db"
113
+ if not project_db_path.exists():
114
+ return stats
115
+
116
+ language = _get_project_language()
117
+ project_root = str(get_project_root())
118
+
119
+ global_db = GlobalDB(get_global_db_path())
120
+ project_db = SQLiteGraph(project_db_path)
121
+
122
+ try:
123
+ # Export preferences with frequency >= 2
124
+ local_prefs = project_db.get_preferences(min_frequency=2)
125
+ for pref in local_prefs:
126
+ global_db.upsert_preference(
127
+ pref["category"], pref["signal"],
128
+ example=pref.get("example"),
129
+ source_project=project_root,
130
+ frequency=pref.get("frequency", 1),
131
+ )
132
+ stats["preferences_exported"] += 1
133
+
134
+ # Export rules with confidence >= 0.5
135
+ local_rules = project_db.get_learned_rules(min_confidence=0.5)
136
+ for rule in local_rules:
137
+ global_db.upsert_rule(
138
+ rule["rule_text"], rule["confidence"],
139
+ source_project=project_root,
140
+ category=rule.get("category"),
141
+ language=language,
142
+ )
143
+ stats["rules_exported"] += 1
144
+
145
+ # Update project sync timestamp
146
+ global_db.register_project(project_root, Path(project_root).name, language or "unknown")
147
+
148
+ finally:
149
+ global_db.close()
150
+ project_db.close()
151
+
152
+ return stats
153
+
154
+
155
+ def get_global_stats() -> dict | None:
156
+ """Return global database stats for display in get_session_context()."""
157
+ from mcp_server.paths import get_global_db_path
158
+ from indexer.global_db import GlobalDB
159
+
160
+ global_db_path = get_global_db_path()
161
+ if not global_db_path.exists():
162
+ return None
163
+
164
+ global_db = GlobalDB(global_db_path)
165
+ try:
166
+ return global_db.get_stats()
167
+ finally:
168
+ global_db.close()
169
+
170
+
171
+ def _get_project_language() -> str | None:
172
+ """Read the project language from .codevira/config.yaml."""
173
+ from mcp_server.paths import get_data_dir
174
+ import yaml
175
+
176
+ config_path = get_data_dir() / "config.yaml"
177
+ if not config_path.exists():
178
+ return None
179
+ try:
180
+ with open(config_path) as f:
181
+ config = yaml.safe_load(f) or {}
182
+ project = config.get("project", config)
183
+ return project.get("language")
184
+ except Exception as e:
185
+ import logging
186
+ logging.getLogger("codevira.global_sync").warning("Could not read project language: %s", e)
187
+ return None