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
mcp_server/gitignore.py
ADDED
|
@@ -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
|