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
indexer/sqlite_graph.py
ADDED
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sqlite3
|
|
5
|
+
import os
|
|
6
|
+
import logging
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
class SQLiteGraph:
|
|
13
|
+
def __init__(self, db_path: str | Path):
|
|
14
|
+
self.db_path = Path(db_path)
|
|
15
|
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
16
|
+
self.conn = sqlite3.connect(self.db_path, check_same_thread=False)
|
|
17
|
+
self.conn.row_factory = sqlite3.Row
|
|
18
|
+
self._init_db()
|
|
19
|
+
|
|
20
|
+
@contextmanager
|
|
21
|
+
def transaction(self):
|
|
22
|
+
with self.conn:
|
|
23
|
+
yield self.conn
|
|
24
|
+
|
|
25
|
+
def _init_db(self):
|
|
26
|
+
with self.transaction() as conn:
|
|
27
|
+
conn.executescript('''
|
|
28
|
+
CREATE TABLE IF NOT EXISTS nodes (
|
|
29
|
+
id TEXT PRIMARY KEY,
|
|
30
|
+
kind TEXT NOT NULL,
|
|
31
|
+
name TEXT NOT NULL,
|
|
32
|
+
file_path TEXT NOT NULL,
|
|
33
|
+
line_start INTEGER,
|
|
34
|
+
line_end INTEGER,
|
|
35
|
+
docstring TEXT,
|
|
36
|
+
is_public BOOLEAN,
|
|
37
|
+
|
|
38
|
+
-- User/Agent Metadata
|
|
39
|
+
role TEXT,
|
|
40
|
+
type TEXT,
|
|
41
|
+
rules TEXT,
|
|
42
|
+
key_functions TEXT,
|
|
43
|
+
dependencies TEXT,
|
|
44
|
+
stability TEXT DEFAULT 'medium',
|
|
45
|
+
do_not_revert BOOLEAN DEFAULT 0,
|
|
46
|
+
layer TEXT
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_file_path ON nodes(file_path);
|
|
50
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes(name);
|
|
51
|
+
|
|
52
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
53
|
+
source_id TEXT,
|
|
54
|
+
target_id TEXT,
|
|
55
|
+
kind TEXT NOT NULL,
|
|
56
|
+
line INTEGER,
|
|
57
|
+
PRIMARY KEY (source_id, target_id, kind),
|
|
58
|
+
FOREIGN KEY (source_id) REFERENCES nodes(id) ON DELETE CASCADE,
|
|
59
|
+
FOREIGN KEY (target_id) REFERENCES nodes(id) ON DELETE CASCADE
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_id);
|
|
63
|
+
CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source_id);
|
|
64
|
+
|
|
65
|
+
-- ----------------------------------------------------
|
|
66
|
+
-- Hashing for Incremental Indexing
|
|
67
|
+
-- ----------------------------------------------------
|
|
68
|
+
CREATE TABLE IF NOT EXISTS file_hashes (
|
|
69
|
+
file_path TEXT PRIMARY KEY,
|
|
70
|
+
sha256 TEXT NOT NULL,
|
|
71
|
+
last_indexed_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
-- ----------------------------------------------------
|
|
75
|
+
-- Memory & Session Logs
|
|
76
|
+
-- ----------------------------------------------------
|
|
77
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
78
|
+
session_id TEXT PRIMARY KEY,
|
|
79
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
80
|
+
summary TEXT,
|
|
81
|
+
phase TEXT
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
CREATE TABLE IF NOT EXISTS decisions (
|
|
85
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
86
|
+
session_id TEXT NOT NULL,
|
|
87
|
+
file_path TEXT,
|
|
88
|
+
decision TEXT NOT NULL,
|
|
89
|
+
context TEXT,
|
|
90
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
91
|
+
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
CREATE INDEX IF NOT EXISTS idx_decisions_session ON decisions(session_id);
|
|
95
|
+
CREATE INDEX IF NOT EXISTS idx_decisions_file ON decisions(file_path);
|
|
96
|
+
|
|
97
|
+
-- ----------------------------------------------------
|
|
98
|
+
-- v1.4: Outcome Tracking (feedback loop)
|
|
99
|
+
-- ----------------------------------------------------
|
|
100
|
+
CREATE TABLE IF NOT EXISTS outcomes (
|
|
101
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
102
|
+
session_id TEXT NOT NULL,
|
|
103
|
+
file_path TEXT NOT NULL,
|
|
104
|
+
decision_id INTEGER,
|
|
105
|
+
outcome_type TEXT NOT NULL, -- 'kept' | 'modified' | 'reverted'
|
|
106
|
+
delta_summary TEXT,
|
|
107
|
+
detected_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
108
|
+
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE,
|
|
109
|
+
FOREIGN KEY (decision_id) REFERENCES decisions(id) ON DELETE SET NULL
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
CREATE INDEX IF NOT EXISTS idx_outcomes_session ON outcomes(session_id);
|
|
113
|
+
CREATE INDEX IF NOT EXISTS idx_outcomes_file ON outcomes(file_path);
|
|
114
|
+
|
|
115
|
+
-- ----------------------------------------------------
|
|
116
|
+
-- v1.4: Developer Preferences (learned from corrections)
|
|
117
|
+
-- ----------------------------------------------------
|
|
118
|
+
CREATE TABLE IF NOT EXISTS preferences (
|
|
119
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
120
|
+
category TEXT NOT NULL, -- 'naming' | 'structure' | 'patterns' | 'formatting'
|
|
121
|
+
signal TEXT NOT NULL,
|
|
122
|
+
example TEXT,
|
|
123
|
+
frequency INTEGER DEFAULT 1,
|
|
124
|
+
last_seen DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
CREATE INDEX IF NOT EXISTS idx_preferences_category ON preferences(category);
|
|
128
|
+
|
|
129
|
+
-- ----------------------------------------------------
|
|
130
|
+
-- v1.4: Learned Rules (auto-generated from patterns)
|
|
131
|
+
-- ----------------------------------------------------
|
|
132
|
+
CREATE TABLE IF NOT EXISTS learned_rules (
|
|
133
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
134
|
+
rule_text TEXT NOT NULL,
|
|
135
|
+
confidence REAL DEFAULT 0.5,
|
|
136
|
+
source_sessions TEXT, -- JSON array of session IDs
|
|
137
|
+
category TEXT, -- 'testing' | 'imports' | 'structure' | 'naming'
|
|
138
|
+
file_pattern TEXT, -- glob pattern this rule applies to
|
|
139
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
140
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
CREATE INDEX IF NOT EXISTS idx_learned_rules_category ON learned_rules(category);
|
|
144
|
+
|
|
145
|
+
-- ----------------------------------------------------
|
|
146
|
+
-- v1.5: Function-level symbols and call graph
|
|
147
|
+
-- ----------------------------------------------------
|
|
148
|
+
CREATE TABLE IF NOT EXISTS symbols (
|
|
149
|
+
id TEXT PRIMARY KEY, -- 'file:path.py::func_name'
|
|
150
|
+
file_node_id TEXT, -- FK to nodes.id
|
|
151
|
+
name TEXT NOT NULL,
|
|
152
|
+
kind TEXT, -- 'function' | 'class' | 'method' | 'interface' | 'struct'
|
|
153
|
+
signature TEXT,
|
|
154
|
+
parameters TEXT, -- JSON: [{name, type}]
|
|
155
|
+
return_type TEXT,
|
|
156
|
+
start_line INTEGER,
|
|
157
|
+
end_line INTEGER,
|
|
158
|
+
docstring TEXT,
|
|
159
|
+
is_public BOOLEAN DEFAULT 1,
|
|
160
|
+
calls TEXT, -- JSON: ['func_a', 'func_b']
|
|
161
|
+
FOREIGN KEY (file_node_id) REFERENCES nodes(id) ON DELETE CASCADE
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file_node_id);
|
|
165
|
+
CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name);
|
|
166
|
+
|
|
167
|
+
CREATE TABLE IF NOT EXISTS call_edges (
|
|
168
|
+
caller_id TEXT, -- symbols.id
|
|
169
|
+
callee_id TEXT, -- symbols.id
|
|
170
|
+
line INTEGER,
|
|
171
|
+
PRIMARY KEY (caller_id, callee_id),
|
|
172
|
+
FOREIGN KEY (caller_id) REFERENCES symbols(id) ON DELETE CASCADE,
|
|
173
|
+
FOREIGN KEY (callee_id) REFERENCES symbols(id) ON DELETE CASCADE
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
CREATE INDEX IF NOT EXISTS idx_call_edges_callee ON call_edges(callee_id);
|
|
177
|
+
CREATE INDEX IF NOT EXISTS idx_call_edges_caller ON call_edges(caller_id);
|
|
178
|
+
''')
|
|
179
|
+
conn.execute("PRAGMA foreign_keys = ON;")
|
|
180
|
+
|
|
181
|
+
# v1.5 migrations: add source column for global sync tracking
|
|
182
|
+
try:
|
|
183
|
+
conn.execute("ALTER TABLE preferences ADD COLUMN source TEXT DEFAULT 'local'")
|
|
184
|
+
except sqlite3.OperationalError:
|
|
185
|
+
pass # Column already exists
|
|
186
|
+
try:
|
|
187
|
+
conn.execute("ALTER TABLE learned_rules ADD COLUMN source TEXT DEFAULT 'local'")
|
|
188
|
+
except sqlite3.OperationalError:
|
|
189
|
+
pass # Column already exists
|
|
190
|
+
|
|
191
|
+
def add_node(self, node_id: str, kind: str, name: str, file_path: str, **kwargs):
|
|
192
|
+
existing = self.get_node(node_id)
|
|
193
|
+
fields = ["id", "kind", "name", "file_path"]
|
|
194
|
+
values = [node_id, kind, name, file_path]
|
|
195
|
+
|
|
196
|
+
metadata_fields = ["line_start", "line_end", "docstring", "is_public",
|
|
197
|
+
"role", "type", "rules", "key_functions", "dependencies",
|
|
198
|
+
"stability", "do_not_revert", "layer"]
|
|
199
|
+
|
|
200
|
+
for k in metadata_fields:
|
|
201
|
+
if k in kwargs:
|
|
202
|
+
fields.append(k)
|
|
203
|
+
values.append(kwargs[k])
|
|
204
|
+
elif existing and existing.get(k) is not None:
|
|
205
|
+
fields.append(k)
|
|
206
|
+
values.append(existing[k])
|
|
207
|
+
|
|
208
|
+
placeholders = ",".join(["?"] * len(fields))
|
|
209
|
+
query = f"INSERT OR REPLACE INTO nodes ({','.join(fields)}) VALUES ({placeholders})"
|
|
210
|
+
|
|
211
|
+
with self.transaction() as conn:
|
|
212
|
+
conn.execute(query, values)
|
|
213
|
+
|
|
214
|
+
def update_node_metadata(self, node_id: str, **kwargs):
|
|
215
|
+
valid_fields = ["role", "type", "rules", "key_functions", "dependencies",
|
|
216
|
+
"stability", "do_not_revert", "layer"]
|
|
217
|
+
updates, values = [], []
|
|
218
|
+
for k, v in kwargs.items():
|
|
219
|
+
if k in valid_fields:
|
|
220
|
+
updates.append(f"{k} = ?")
|
|
221
|
+
values.append(v)
|
|
222
|
+
if not updates: return
|
|
223
|
+
|
|
224
|
+
values.append(node_id)
|
|
225
|
+
query = f"UPDATE nodes SET {', '.join(updates)} WHERE id = ?"
|
|
226
|
+
with self.transaction() as conn:
|
|
227
|
+
conn.execute(query, values)
|
|
228
|
+
|
|
229
|
+
def get_node(self, node_id: str) -> dict | None:
|
|
230
|
+
cur = self.conn.execute('SELECT * FROM nodes WHERE id = ?', (node_id,))
|
|
231
|
+
row = cur.fetchone()
|
|
232
|
+
return dict(row) if row else None
|
|
233
|
+
|
|
234
|
+
def get_node_by_path(self, file_path: str) -> dict | None:
|
|
235
|
+
cur = self.conn.execute('SELECT * FROM nodes WHERE file_path = ? AND kind = "file" LIMIT 1', (file_path,))
|
|
236
|
+
row = cur.fetchone()
|
|
237
|
+
return dict(row) if row else None
|
|
238
|
+
|
|
239
|
+
def list_file_nodes(self, layer: str | None = None, stability: str | None = None, do_not_revert: bool | None = None) -> list[dict]:
|
|
240
|
+
query = 'SELECT * FROM nodes WHERE kind = "file"'
|
|
241
|
+
params = []
|
|
242
|
+
if layer:
|
|
243
|
+
query += ' AND layer = ?'
|
|
244
|
+
params.append(layer)
|
|
245
|
+
if stability:
|
|
246
|
+
query += ' AND stability = ?'
|
|
247
|
+
params.append(stability)
|
|
248
|
+
if do_not_revert is not None:
|
|
249
|
+
query += ' AND do_not_revert = ?'
|
|
250
|
+
params.append(1 if do_not_revert else 0)
|
|
251
|
+
|
|
252
|
+
cur = self.conn.execute(query, params)
|
|
253
|
+
return [dict(r) for r in cur.fetchall()]
|
|
254
|
+
|
|
255
|
+
def get_blast_radius(self, node_id: str, max_depth: int = 3) -> list[dict]:
|
|
256
|
+
query = '''
|
|
257
|
+
WITH RECURSIVE
|
|
258
|
+
dependents(id, path, depth) AS (
|
|
259
|
+
SELECT id, id, 0 FROM nodes WHERE id = ?
|
|
260
|
+
UNION ALL
|
|
261
|
+
SELECT e.source_id, d.path || '->' || e.source_id, d.depth + 1
|
|
262
|
+
FROM edges e
|
|
263
|
+
JOIN dependents d ON e.target_id = d.id
|
|
264
|
+
WHERE d.depth < ? AND instr(d.path, e.source_id) = 0
|
|
265
|
+
)
|
|
266
|
+
SELECT DISTINCT n.*, d.depth
|
|
267
|
+
FROM dependents d
|
|
268
|
+
JOIN nodes n ON d.id = n.id
|
|
269
|
+
WHERE d.id != ?
|
|
270
|
+
ORDER BY d.depth ASC;
|
|
271
|
+
'''
|
|
272
|
+
with self.transaction() as conn:
|
|
273
|
+
cur = conn.execute(query, (node_id, max_depth, node_id))
|
|
274
|
+
return [dict(r) for r in cur.fetchall()]
|
|
275
|
+
|
|
276
|
+
# ------------------------------------------------------------------
|
|
277
|
+
# Edge management
|
|
278
|
+
# ------------------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
def add_edge(self, source_id: str, target_id: str, kind: str = "imports", line: int | None = None):
|
|
281
|
+
with self.transaction() as conn:
|
|
282
|
+
conn.execute(
|
|
283
|
+
'INSERT OR REPLACE INTO edges (source_id, target_id, kind, line) VALUES (?, ?, ?, ?)',
|
|
284
|
+
(source_id, target_id, kind, line),
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
def remove_edges_for_node(self, node_id: str):
|
|
288
|
+
with self.transaction() as conn:
|
|
289
|
+
conn.execute('DELETE FROM edges WHERE source_id = ?', (node_id,))
|
|
290
|
+
|
|
291
|
+
def get_edges_from(self, node_id: str) -> list[dict]:
|
|
292
|
+
cur = self.conn.execute('SELECT * FROM edges WHERE source_id = ?', (node_id,))
|
|
293
|
+
return [dict(r) for r in cur.fetchall()]
|
|
294
|
+
|
|
295
|
+
def get_edges_to(self, node_id: str) -> list[dict]:
|
|
296
|
+
cur = self.conn.execute('SELECT * FROM edges WHERE target_id = ?', (node_id,))
|
|
297
|
+
return [dict(r) for r in cur.fetchall()]
|
|
298
|
+
|
|
299
|
+
def get_all_edges(self) -> list[dict]:
|
|
300
|
+
cur = self.conn.execute('SELECT * FROM edges')
|
|
301
|
+
return [dict(r) for r in cur.fetchall()]
|
|
302
|
+
|
|
303
|
+
# ------------------------------------------------------------------
|
|
304
|
+
# Outcome tracking (feedback loop)
|
|
305
|
+
# ------------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
def record_outcome(self, session_id: str, file_path: str, outcome_type: str,
|
|
308
|
+
decision_id: int | None = None, delta_summary: str | None = None):
|
|
309
|
+
with self.transaction() as conn:
|
|
310
|
+
conn.execute('''
|
|
311
|
+
INSERT INTO outcomes (session_id, file_path, decision_id, outcome_type, delta_summary)
|
|
312
|
+
VALUES (?, ?, ?, ?, ?)
|
|
313
|
+
''', (session_id, file_path, decision_id, outcome_type, delta_summary))
|
|
314
|
+
|
|
315
|
+
def get_outcomes_for_file(self, file_path: str, limit: int = 20) -> list[dict]:
|
|
316
|
+
cur = self.conn.execute('''
|
|
317
|
+
SELECT o.*, d.decision FROM outcomes o
|
|
318
|
+
LEFT JOIN decisions d ON o.decision_id = d.id
|
|
319
|
+
WHERE o.file_path = ?
|
|
320
|
+
ORDER BY o.detected_at DESC LIMIT ?
|
|
321
|
+
''', (file_path, limit))
|
|
322
|
+
return [dict(r) for r in cur.fetchall()]
|
|
323
|
+
|
|
324
|
+
def get_decision_confidence(self, file_path: str | None = None, pattern: str | None = None) -> dict:
|
|
325
|
+
"""Calculate confidence scores based on outcome history."""
|
|
326
|
+
if file_path:
|
|
327
|
+
cur = self.conn.execute('''
|
|
328
|
+
SELECT outcome_type, COUNT(*) as cnt FROM outcomes
|
|
329
|
+
WHERE file_path = ? GROUP BY outcome_type
|
|
330
|
+
''', (file_path,))
|
|
331
|
+
elif pattern:
|
|
332
|
+
cur = self.conn.execute('''
|
|
333
|
+
SELECT outcome_type, COUNT(*) as cnt FROM outcomes
|
|
334
|
+
WHERE file_path LIKE ? GROUP BY outcome_type
|
|
335
|
+
''', (f'%{pattern}%',))
|
|
336
|
+
else:
|
|
337
|
+
cur = self.conn.execute(
|
|
338
|
+
'SELECT outcome_type, COUNT(*) as cnt FROM outcomes GROUP BY outcome_type'
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
counts = {row['outcome_type']: row['cnt'] for row in cur.fetchall()}
|
|
342
|
+
total = sum(counts.values())
|
|
343
|
+
kept = counts.get('kept', 0)
|
|
344
|
+
modified = counts.get('modified', 0)
|
|
345
|
+
reverted = counts.get('reverted', 0)
|
|
346
|
+
|
|
347
|
+
confidence = (kept + modified * 0.5) / total if total > 0 else 0.0
|
|
348
|
+
return {
|
|
349
|
+
"total_decisions": total,
|
|
350
|
+
"kept": kept,
|
|
351
|
+
"modified": modified,
|
|
352
|
+
"reverted": reverted,
|
|
353
|
+
"confidence": round(confidence, 3),
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
# ------------------------------------------------------------------
|
|
357
|
+
# Developer preferences
|
|
358
|
+
# ------------------------------------------------------------------
|
|
359
|
+
|
|
360
|
+
def record_preference(self, category: str, signal: str, example: str | None = None,
|
|
361
|
+
source: str = "local"):
|
|
362
|
+
existing = self.conn.execute(
|
|
363
|
+
'SELECT id, frequency FROM preferences WHERE category = ? AND signal = ?',
|
|
364
|
+
(category, signal),
|
|
365
|
+
).fetchone()
|
|
366
|
+
|
|
367
|
+
if existing:
|
|
368
|
+
with self.transaction() as conn:
|
|
369
|
+
conn.execute('''
|
|
370
|
+
UPDATE preferences SET frequency = frequency + 1, last_seen = CURRENT_TIMESTAMP, example = COALESCE(?, example)
|
|
371
|
+
WHERE id = ?
|
|
372
|
+
''', (example, existing['id']))
|
|
373
|
+
else:
|
|
374
|
+
with self.transaction() as conn:
|
|
375
|
+
conn.execute('''
|
|
376
|
+
INSERT INTO preferences (category, signal, example, source) VALUES (?, ?, ?, ?)
|
|
377
|
+
''', (category, signal, example, source))
|
|
378
|
+
|
|
379
|
+
def get_preferences(self, category: str | None = None, min_frequency: int = 1) -> list[dict]:
|
|
380
|
+
if category:
|
|
381
|
+
cur = self.conn.execute('''
|
|
382
|
+
SELECT * FROM preferences WHERE category = ? AND frequency >= ?
|
|
383
|
+
ORDER BY frequency DESC
|
|
384
|
+
''', (category, min_frequency))
|
|
385
|
+
else:
|
|
386
|
+
cur = self.conn.execute('''
|
|
387
|
+
SELECT * FROM preferences WHERE frequency >= ? ORDER BY frequency DESC
|
|
388
|
+
''', (min_frequency,))
|
|
389
|
+
return [dict(r) for r in cur.fetchall()]
|
|
390
|
+
|
|
391
|
+
# ------------------------------------------------------------------
|
|
392
|
+
# Learned rules
|
|
393
|
+
# ------------------------------------------------------------------
|
|
394
|
+
|
|
395
|
+
def add_learned_rule(self, rule_text: str, confidence: float, source_sessions: list[str],
|
|
396
|
+
category: str | None = None, file_pattern: str | None = None):
|
|
397
|
+
with self.transaction() as conn:
|
|
398
|
+
conn.execute('''
|
|
399
|
+
INSERT INTO learned_rules (rule_text, confidence, source_sessions, category, file_pattern)
|
|
400
|
+
VALUES (?, ?, ?, ?, ?)
|
|
401
|
+
''', (rule_text, confidence, json.dumps(source_sessions), category, file_pattern))
|
|
402
|
+
|
|
403
|
+
def update_learned_rule(self, rule_id: int, confidence: float | None = None,
|
|
404
|
+
source_sessions: list[str] | None = None):
|
|
405
|
+
updates, values = [], []
|
|
406
|
+
if confidence is not None:
|
|
407
|
+
updates.append("confidence = ?")
|
|
408
|
+
values.append(confidence)
|
|
409
|
+
if source_sessions is not None:
|
|
410
|
+
updates.append("source_sessions = ?")
|
|
411
|
+
values.append(json.dumps(source_sessions))
|
|
412
|
+
if updates:
|
|
413
|
+
updates.append("updated_at = CURRENT_TIMESTAMP")
|
|
414
|
+
values.append(rule_id)
|
|
415
|
+
with self.transaction() as conn:
|
|
416
|
+
conn.execute(f'UPDATE learned_rules SET {", ".join(updates)} WHERE id = ?', values)
|
|
417
|
+
|
|
418
|
+
def get_learned_rules(self, category: str | None = None, file_pattern: str | None = None,
|
|
419
|
+
min_confidence: float = 0.0) -> list[dict]:
|
|
420
|
+
query = 'SELECT * FROM learned_rules WHERE confidence >= ?'
|
|
421
|
+
params: list = [min_confidence]
|
|
422
|
+
if category:
|
|
423
|
+
query += ' AND category = ?'
|
|
424
|
+
params.append(category)
|
|
425
|
+
if file_pattern:
|
|
426
|
+
query += ' AND (file_pattern IS NULL OR ? LIKE file_pattern)'
|
|
427
|
+
params.append(file_pattern)
|
|
428
|
+
query += ' ORDER BY confidence DESC'
|
|
429
|
+
cur = self.conn.execute(query, params)
|
|
430
|
+
return [dict(r) for r in cur.fetchall()]
|
|
431
|
+
|
|
432
|
+
# ------------------------------------------------------------------
|
|
433
|
+
# Project maturity metrics
|
|
434
|
+
# ------------------------------------------------------------------
|
|
435
|
+
|
|
436
|
+
def get_project_maturity(self) -> dict:
|
|
437
|
+
"""Compute overall project maturity based on outcomes and coverage."""
|
|
438
|
+
# Overall confidence
|
|
439
|
+
confidence = self.get_decision_confidence()
|
|
440
|
+
|
|
441
|
+
# File coverage: files with at least one session decision
|
|
442
|
+
total_files = self.conn.execute('SELECT COUNT(*) as c FROM nodes WHERE kind = "file"').fetchone()['c']
|
|
443
|
+
covered_files = self.conn.execute(
|
|
444
|
+
'SELECT COUNT(DISTINCT file_path) as c FROM decisions WHERE file_path IS NOT NULL'
|
|
445
|
+
).fetchone()['c']
|
|
446
|
+
|
|
447
|
+
# Learned rules count
|
|
448
|
+
rule_count = self.conn.execute('SELECT COUNT(*) as c FROM learned_rules WHERE confidence >= 0.5').fetchone()['c']
|
|
449
|
+
|
|
450
|
+
# Preference signals count
|
|
451
|
+
pref_count = self.conn.execute('SELECT COUNT(*) as c FROM preferences WHERE frequency >= 2').fetchone()['c']
|
|
452
|
+
|
|
453
|
+
# Session count
|
|
454
|
+
session_count = self.conn.execute('SELECT COUNT(*) as c FROM sessions').fetchone()['c']
|
|
455
|
+
|
|
456
|
+
coverage = round(covered_files / total_files, 3) if total_files > 0 else 0.0
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
"session_count": session_count,
|
|
460
|
+
"total_files": total_files,
|
|
461
|
+
"covered_files": covered_files,
|
|
462
|
+
"coverage": coverage,
|
|
463
|
+
"overall_confidence": confidence["confidence"],
|
|
464
|
+
"outcome_breakdown": confidence,
|
|
465
|
+
"learned_rules": rule_count,
|
|
466
|
+
"preference_signals": pref_count,
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
# ------------------------------------------------------------------
|
|
470
|
+
# Session helpers
|
|
471
|
+
# ------------------------------------------------------------------
|
|
472
|
+
|
|
473
|
+
def get_recent_sessions(self, limit: int = 5) -> list[dict]:
|
|
474
|
+
cur = self.conn.execute('''
|
|
475
|
+
SELECT * FROM sessions ORDER BY created_at DESC LIMIT ?
|
|
476
|
+
''', (limit,))
|
|
477
|
+
return [dict(r) for r in cur.fetchall()]
|
|
478
|
+
|
|
479
|
+
def get_recent_decisions(self, limit: int = 10) -> list[dict]:
|
|
480
|
+
cur = self.conn.execute('''
|
|
481
|
+
SELECT d.*, s.summary, s.phase FROM decisions d
|
|
482
|
+
JOIN sessions s ON d.session_id = s.session_id
|
|
483
|
+
ORDER BY d.created_at DESC LIMIT ?
|
|
484
|
+
''', (limit,))
|
|
485
|
+
return [dict(r) for r in cur.fetchall()]
|
|
486
|
+
|
|
487
|
+
def get_file_hash(self, file_path: str) -> str | None:
|
|
488
|
+
cur = self.conn.execute('SELECT sha256 FROM file_hashes WHERE file_path = ?', (file_path,))
|
|
489
|
+
row = cur.fetchone()
|
|
490
|
+
return row['sha256'] if row else None
|
|
491
|
+
|
|
492
|
+
def update_file_hash(self, file_path: str, sha256: str):
|
|
493
|
+
with self.transaction() as conn:
|
|
494
|
+
conn.execute('''
|
|
495
|
+
INSERT OR REPLACE INTO file_hashes (file_path, sha256, last_indexed_at)
|
|
496
|
+
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
497
|
+
''', (file_path, sha256))
|
|
498
|
+
|
|
499
|
+
def log_session(self, session_id: str, summary: str, phase: str, decisions: list[dict]):
|
|
500
|
+
with self.transaction() as conn:
|
|
501
|
+
conn.execute('''
|
|
502
|
+
INSERT OR REPLACE INTO sessions (session_id, summary, phase)
|
|
503
|
+
VALUES (?, ?, ?)
|
|
504
|
+
''', (session_id, summary, phase))
|
|
505
|
+
|
|
506
|
+
for d in decisions:
|
|
507
|
+
conn.execute('''
|
|
508
|
+
INSERT INTO decisions (session_id, file_path, decision, context)
|
|
509
|
+
VALUES (?, ?, ?, ?)
|
|
510
|
+
''', (session_id, d.get("file_path"), d.get("decision"), d.get("context")))
|
|
511
|
+
|
|
512
|
+
def search_decisions(self, query: str, limit: int = 10, session_id: str | None = None) -> list[dict]:
|
|
513
|
+
sql = '''
|
|
514
|
+
SELECT d.decision, d.context, d.file_path, s.summary, s.phase, d.created_at
|
|
515
|
+
FROM decisions d
|
|
516
|
+
JOIN sessions s ON d.session_id = s.session_id
|
|
517
|
+
WHERE (d.decision LIKE ? OR d.context LIKE ? OR s.summary LIKE ?)
|
|
518
|
+
'''
|
|
519
|
+
params = [f'%{query}%', f'%{query}%', f'%{query}%']
|
|
520
|
+
|
|
521
|
+
if session_id:
|
|
522
|
+
sql += ' AND d.session_id = ?'
|
|
523
|
+
params.append(session_id)
|
|
524
|
+
|
|
525
|
+
sql += ' ORDER BY d.created_at DESC LIMIT ?'
|
|
526
|
+
params.append(limit)
|
|
527
|
+
|
|
528
|
+
cur = self.conn.execute(sql, params)
|
|
529
|
+
return [dict(r) for r in cur.fetchall()]
|
|
530
|
+
|
|
531
|
+
# ------------------------------------------------------------------
|
|
532
|
+
# v1.5: Function-level symbols and call graph
|
|
533
|
+
# ------------------------------------------------------------------
|
|
534
|
+
|
|
535
|
+
def add_symbol(self, symbol_id: str, file_node_id: str, name: str, kind: str,
|
|
536
|
+
signature: str | None = None, parameters: str | None = None,
|
|
537
|
+
return_type: str | None = None, start_line: int | None = None,
|
|
538
|
+
end_line: int | None = None, docstring: str | None = None,
|
|
539
|
+
is_public: bool = True, calls: str | None = None):
|
|
540
|
+
"""Insert or replace a function/class symbol."""
|
|
541
|
+
with self.transaction() as conn:
|
|
542
|
+
conn.execute('''
|
|
543
|
+
INSERT OR REPLACE INTO symbols
|
|
544
|
+
(id, file_node_id, name, kind, signature, parameters, return_type,
|
|
545
|
+
start_line, end_line, docstring, is_public, calls)
|
|
546
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
547
|
+
''', (symbol_id, file_node_id, name, kind, signature, parameters,
|
|
548
|
+
return_type, start_line, end_line, docstring, is_public, calls))
|
|
549
|
+
|
|
550
|
+
def remove_symbols_for_file(self, file_node_id: str):
|
|
551
|
+
"""Remove all symbols (and their call_edges) for a file node."""
|
|
552
|
+
with self.transaction() as conn:
|
|
553
|
+
# call_edges cascade from symbols via FK
|
|
554
|
+
conn.execute("DELETE FROM symbols WHERE file_node_id = ?", (file_node_id,))
|
|
555
|
+
|
|
556
|
+
def add_call_edge(self, caller_id: str, callee_id: str, line: int | None = None):
|
|
557
|
+
"""Record a function call relationship."""
|
|
558
|
+
with self.transaction() as conn:
|
|
559
|
+
conn.execute('''
|
|
560
|
+
INSERT OR REPLACE INTO call_edges (caller_id, callee_id, line)
|
|
561
|
+
VALUES (?, ?, ?)
|
|
562
|
+
''', (caller_id, callee_id, line))
|
|
563
|
+
|
|
564
|
+
def get_callers(self, symbol_id: str) -> list[dict]:
|
|
565
|
+
"""Get all functions that call this symbol."""
|
|
566
|
+
cur = self.conn.execute('''
|
|
567
|
+
SELECT s.id, s.name, s.kind, s.file_node_id, ce.line
|
|
568
|
+
FROM call_edges ce
|
|
569
|
+
JOIN symbols s ON ce.caller_id = s.id
|
|
570
|
+
WHERE ce.callee_id = ?
|
|
571
|
+
ORDER BY s.name
|
|
572
|
+
''', (symbol_id,))
|
|
573
|
+
return [dict(r) for r in cur.fetchall()]
|
|
574
|
+
|
|
575
|
+
def get_callees(self, symbol_id: str) -> list[dict]:
|
|
576
|
+
"""Get all functions called by this symbol."""
|
|
577
|
+
cur = self.conn.execute('''
|
|
578
|
+
SELECT s.id, s.name, s.kind, s.file_node_id, ce.line
|
|
579
|
+
FROM call_edges ce
|
|
580
|
+
JOIN symbols s ON ce.callee_id = s.id
|
|
581
|
+
WHERE ce.caller_id = ?
|
|
582
|
+
ORDER BY s.name
|
|
583
|
+
''', (symbol_id,))
|
|
584
|
+
return [dict(r) for r in cur.fetchall()]
|
|
585
|
+
|
|
586
|
+
def get_symbols_for_file(self, file_node_id: str) -> list[dict]:
|
|
587
|
+
"""Get all symbols in a file."""
|
|
588
|
+
cur = self.conn.execute('''
|
|
589
|
+
SELECT * FROM symbols WHERE file_node_id = ? ORDER BY start_line
|
|
590
|
+
''', (file_node_id,))
|
|
591
|
+
return [dict(r) for r in cur.fetchall()]
|
|
592
|
+
|
|
593
|
+
def find_symbol(self, name: str, file_path: str | None = None) -> dict | None:
|
|
594
|
+
"""Find a symbol by name, optionally scoped to a file."""
|
|
595
|
+
if file_path:
|
|
596
|
+
node_id = f"file:{file_path}"
|
|
597
|
+
cur = self.conn.execute(
|
|
598
|
+
"SELECT * FROM symbols WHERE name = ? AND file_node_id = ?",
|
|
599
|
+
(name, node_id),
|
|
600
|
+
)
|
|
601
|
+
else:
|
|
602
|
+
cur = self.conn.execute(
|
|
603
|
+
"SELECT * FROM symbols WHERE name = ? LIMIT 1", (name,),
|
|
604
|
+
)
|
|
605
|
+
row = cur.fetchone()
|
|
606
|
+
return dict(row) if row else None
|
|
607
|
+
|
|
608
|
+
def get_symbol_count(self) -> int:
|
|
609
|
+
return self.conn.execute("SELECT COUNT(*) FROM symbols").fetchone()[0]
|
|
610
|
+
|
|
611
|
+
def get_call_edge_count(self) -> int:
|
|
612
|
+
return self.conn.execute("SELECT COUNT(*) FROM call_edges").fetchone()[0]
|
|
613
|
+
|
|
614
|
+
def find_hotspot_functions(self, min_lines: int = 50) -> list[dict]:
|
|
615
|
+
"""Find large functions exceeding line threshold."""
|
|
616
|
+
cur = self.conn.execute('''
|
|
617
|
+
SELECT s.*, (s.end_line - s.start_line) as line_count,
|
|
618
|
+
n.file_path as full_path
|
|
619
|
+
FROM symbols s
|
|
620
|
+
JOIN nodes n ON s.file_node_id = n.id
|
|
621
|
+
WHERE (s.end_line - s.start_line) >= ?
|
|
622
|
+
ORDER BY (s.end_line - s.start_line) DESC
|
|
623
|
+
''', (min_lines,))
|
|
624
|
+
return [dict(r) for r in cur.fetchall()]
|
|
625
|
+
|
|
626
|
+
def find_high_fan_in(self, min_callers: int = 5) -> list[dict]:
|
|
627
|
+
"""Find symbols with many callers (high fan-in = high risk)."""
|
|
628
|
+
cur = self.conn.execute('''
|
|
629
|
+
SELECT s.id, s.name, s.kind, s.file_node_id, COUNT(ce.caller_id) as caller_count
|
|
630
|
+
FROM symbols s
|
|
631
|
+
JOIN call_edges ce ON ce.callee_id = s.id
|
|
632
|
+
GROUP BY s.id
|
|
633
|
+
HAVING caller_count >= ?
|
|
634
|
+
ORDER BY caller_count DESC
|
|
635
|
+
''', (min_callers,))
|
|
636
|
+
return [dict(r) for r in cur.fetchall()]
|
|
637
|
+
|
|
638
|
+
def close(self):
|
|
639
|
+
self.conn.close()
|
|
640
|
+
|