code-review-graph-codeblackwell 2.3.6.post1__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.
- code_review_graph/__init__.py +20 -0
- code_review_graph/__main__.py +4 -0
- code_review_graph/analysis.py +410 -0
- code_review_graph/changes.py +409 -0
- code_review_graph/cli.py +1255 -0
- code_review_graph/communities.py +874 -0
- code_review_graph/constants.py +23 -0
- code_review_graph/context_savings.py +317 -0
- code_review_graph/custom_languages.py +322 -0
- code_review_graph/daemon.py +1009 -0
- code_review_graph/daemon_cli.py +320 -0
- code_review_graph/docs/LLM-OPTIMIZED-REFERENCE.md +71 -0
- code_review_graph/embeddings.py +1006 -0
- code_review_graph/enrich.py +303 -0
- code_review_graph/eval/__init__.py +33 -0
- code_review_graph/eval/benchmarks/__init__.py +1 -0
- code_review_graph/eval/benchmarks/agent_baseline.py +193 -0
- code_review_graph/eval/benchmarks/build_performance.py +60 -0
- code_review_graph/eval/benchmarks/flow_completeness.py +36 -0
- code_review_graph/eval/benchmarks/impact_accuracy.py +220 -0
- code_review_graph/eval/benchmarks/multi_hop_retrieval.py +125 -0
- code_review_graph/eval/benchmarks/search_quality.py +59 -0
- code_review_graph/eval/benchmarks/token_efficiency.py +143 -0
- code_review_graph/eval/configs/code-review-graph.yaml +50 -0
- code_review_graph/eval/configs/express.yaml +45 -0
- code_review_graph/eval/configs/fastapi.yaml +48 -0
- code_review_graph/eval/configs/flask.yaml +50 -0
- code_review_graph/eval/configs/gin.yaml +51 -0
- code_review_graph/eval/configs/httpx.yaml +48 -0
- code_review_graph/eval/reporter.py +301 -0
- code_review_graph/eval/runner.py +211 -0
- code_review_graph/eval/scorer.py +85 -0
- code_review_graph/eval/token_benchmark.py +182 -0
- code_review_graph/exports.py +409 -0
- code_review_graph/flows.py +698 -0
- code_review_graph/graph.py +1427 -0
- code_review_graph/graph_diff.py +122 -0
- code_review_graph/hints.py +384 -0
- code_review_graph/incremental.py +1245 -0
- code_review_graph/jedi_resolver.py +303 -0
- code_review_graph/main.py +1079 -0
- code_review_graph/memory.py +142 -0
- code_review_graph/migrations.py +284 -0
- code_review_graph/parser.py +6957 -0
- code_review_graph/postprocessing.py +134 -0
- code_review_graph/prompts.py +159 -0
- code_review_graph/refactor.py +852 -0
- code_review_graph/registry.py +319 -0
- code_review_graph/rescript_resolver.py +206 -0
- code_review_graph/search.py +447 -0
- code_review_graph/skills.py +1481 -0
- code_review_graph/spring_resolver.py +200 -0
- code_review_graph/temporal_resolver.py +199 -0
- code_review_graph/token_benchmark.py +125 -0
- code_review_graph/tools/__init__.py +156 -0
- code_review_graph/tools/_common.py +176 -0
- code_review_graph/tools/analysis_tools.py +184 -0
- code_review_graph/tools/build.py +541 -0
- code_review_graph/tools/community_tools.py +246 -0
- code_review_graph/tools/context.py +152 -0
- code_review_graph/tools/docs.py +274 -0
- code_review_graph/tools/flows_tools.py +176 -0
- code_review_graph/tools/query.py +692 -0
- code_review_graph/tools/refactor_tools.py +168 -0
- code_review_graph/tools/registry_tools.py +125 -0
- code_review_graph/tools/review.py +477 -0
- code_review_graph/tsconfig_resolver.py +257 -0
- code_review_graph/visualization.py +2184 -0
- code_review_graph/wiki.py +305 -0
- code_review_graph_codeblackwell-2.3.6.post1.dist-info/METADATA +718 -0
- code_review_graph_codeblackwell-2.3.6.post1.dist-info/RECORD +74 -0
- code_review_graph_codeblackwell-2.3.6.post1.dist-info/WHEEL +4 -0
- code_review_graph_codeblackwell-2.3.6.post1.dist-info/entry_points.txt +3 -0
- code_review_graph_codeblackwell-2.3.6.post1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Memory/feedback loop -- persist Q&A results for graph enrichment."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import re
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def save_result(
|
|
15
|
+
question: str,
|
|
16
|
+
answer: str,
|
|
17
|
+
nodes: list[str] | None = None,
|
|
18
|
+
result_type: str = "query",
|
|
19
|
+
memory_dir: Path | None = None,
|
|
20
|
+
repo_root: Path | None = None,
|
|
21
|
+
) -> Path:
|
|
22
|
+
"""Save a Q&A result as markdown for re-ingestion.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
question: The question that was asked.
|
|
26
|
+
answer: The answer/result.
|
|
27
|
+
nodes: Related node qualified names.
|
|
28
|
+
result_type: Type of result (query, review, debug).
|
|
29
|
+
memory_dir: Directory to save to. Defaults to
|
|
30
|
+
<repo>/.code-review-graph/memory/
|
|
31
|
+
repo_root: Repository root for default memory_dir.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Path to the saved file.
|
|
35
|
+
"""
|
|
36
|
+
if memory_dir is None:
|
|
37
|
+
if repo_root is None:
|
|
38
|
+
raise ValueError(
|
|
39
|
+
"Either memory_dir or repo_root required"
|
|
40
|
+
)
|
|
41
|
+
memory_dir = (
|
|
42
|
+
repo_root / ".code-review-graph" / "memory"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
memory_dir.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
|
|
47
|
+
# Generate filename from question
|
|
48
|
+
slug = re.sub(r"[^\w\s-]", "", question.lower())
|
|
49
|
+
slug = re.sub(r"[\s_]+", "-", slug).strip("-")[:60]
|
|
50
|
+
timestamp = int(time.time())
|
|
51
|
+
filename = f"{slug}-{timestamp}.md"
|
|
52
|
+
|
|
53
|
+
# Build markdown with YAML frontmatter
|
|
54
|
+
lines = [
|
|
55
|
+
"---",
|
|
56
|
+
f"type: {result_type}",
|
|
57
|
+
f"timestamp: {timestamp}",
|
|
58
|
+
]
|
|
59
|
+
if nodes:
|
|
60
|
+
lines.append("nodes:")
|
|
61
|
+
for n in nodes[:20]:
|
|
62
|
+
lines.append(f" - {n}")
|
|
63
|
+
lines.extend([
|
|
64
|
+
"---",
|
|
65
|
+
"",
|
|
66
|
+
f"# {question}",
|
|
67
|
+
"",
|
|
68
|
+
answer,
|
|
69
|
+
])
|
|
70
|
+
|
|
71
|
+
path = memory_dir / filename
|
|
72
|
+
path.write_text("\n".join(lines), encoding="utf-8")
|
|
73
|
+
logger.info("Saved result to %s", path)
|
|
74
|
+
return path
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def list_memories(
|
|
78
|
+
memory_dir: Path | None = None,
|
|
79
|
+
repo_root: Path | None = None,
|
|
80
|
+
) -> list[dict[str, Any]]:
|
|
81
|
+
"""List all saved memory files.
|
|
82
|
+
|
|
83
|
+
Returns list of dicts with: path, question, type, timestamp.
|
|
84
|
+
"""
|
|
85
|
+
if memory_dir is None:
|
|
86
|
+
if repo_root is None:
|
|
87
|
+
return []
|
|
88
|
+
memory_dir = (
|
|
89
|
+
repo_root / ".code-review-graph" / "memory"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if not memory_dir.exists():
|
|
93
|
+
return []
|
|
94
|
+
|
|
95
|
+
results = []
|
|
96
|
+
for f in sorted(memory_dir.glob("*.md")):
|
|
97
|
+
try:
|
|
98
|
+
text = f.read_text(encoding="utf-8")
|
|
99
|
+
# Parse frontmatter
|
|
100
|
+
meta: dict[str, Any] = {"path": str(f)}
|
|
101
|
+
if text.startswith("---"):
|
|
102
|
+
parts = text.split("---", 2)
|
|
103
|
+
if len(parts) >= 3:
|
|
104
|
+
fm_lines = parts[1].strip().split("\n")
|
|
105
|
+
for line in fm_lines:
|
|
106
|
+
if ": " in line and not line.startswith(" "):
|
|
107
|
+
k, v = line.split(": ", 1)
|
|
108
|
+
meta[k.strip()] = v.strip()
|
|
109
|
+
# Extract question from first heading
|
|
110
|
+
for line in text.split("\n"):
|
|
111
|
+
if line.startswith("# "):
|
|
112
|
+
meta["question"] = line[2:].strip()
|
|
113
|
+
break
|
|
114
|
+
results.append(meta)
|
|
115
|
+
except OSError:
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
return results
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def clear_memories(
|
|
122
|
+
memory_dir: Path | None = None,
|
|
123
|
+
repo_root: Path | None = None,
|
|
124
|
+
) -> int:
|
|
125
|
+
"""Delete all memory files. Returns count deleted."""
|
|
126
|
+
if memory_dir is None:
|
|
127
|
+
if repo_root is None:
|
|
128
|
+
return 0
|
|
129
|
+
memory_dir = (
|
|
130
|
+
repo_root / ".code-review-graph" / "memory"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if not memory_dir.exists():
|
|
134
|
+
return 0
|
|
135
|
+
|
|
136
|
+
count = 0
|
|
137
|
+
for f in memory_dir.glob("*.md"):
|
|
138
|
+
f.unlink()
|
|
139
|
+
count += 1
|
|
140
|
+
|
|
141
|
+
logger.info("Cleared %d memory files", count)
|
|
142
|
+
return count
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Schema migration framework for the code-review-graph SQLite database.
|
|
2
|
+
|
|
3
|
+
Manages incremental schema changes via versioned migration functions.
|
|
4
|
+
Each migration is idempotent (uses IF NOT EXISTS / column existence checks).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import sqlite3
|
|
11
|
+
from typing import Callable
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_schema_version(conn: sqlite3.Connection) -> int:
|
|
17
|
+
"""Read the current schema version from the metadata table.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
int: The schema version (0 if metadata table doesn't exist, 1 if not set).
|
|
21
|
+
"""
|
|
22
|
+
try:
|
|
23
|
+
row = conn.execute(
|
|
24
|
+
"SELECT value FROM metadata WHERE key = 'schema_version'"
|
|
25
|
+
).fetchone()
|
|
26
|
+
if row is None:
|
|
27
|
+
return 1
|
|
28
|
+
return int(row[0] if isinstance(row, (tuple, list)) else row["value"])
|
|
29
|
+
except sqlite3.OperationalError:
|
|
30
|
+
# metadata table doesn't exist
|
|
31
|
+
return 0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _set_schema_version(conn: sqlite3.Connection, version: int) -> None:
|
|
35
|
+
"""Set the schema version in the metadata table."""
|
|
36
|
+
conn.execute(
|
|
37
|
+
"INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', ?)",
|
|
38
|
+
(str(version),),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
_KNOWN_TABLES = frozenset({
|
|
43
|
+
"nodes", "edges", "metadata", "communities", "flows", "flow_memberships", "nodes_fts",
|
|
44
|
+
"community_summaries", "flow_snapshots", "risk_index",
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _has_column(conn: sqlite3.Connection, table: str, column: str) -> bool:
|
|
49
|
+
"""Check if a column exists in a table."""
|
|
50
|
+
if table not in _KNOWN_TABLES:
|
|
51
|
+
raise ValueError(f"Unknown table: {table}")
|
|
52
|
+
cursor = conn.execute(f"PRAGMA table_info({table})") # noqa: S608
|
|
53
|
+
columns = [row[1] if isinstance(row, tuple) else row["name"] for row in cursor]
|
|
54
|
+
return column in columns
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _table_exists(conn: sqlite3.Connection, table: str) -> bool:
|
|
58
|
+
"""Check if a table exists."""
|
|
59
|
+
if table not in _KNOWN_TABLES:
|
|
60
|
+
raise ValueError(f"Unknown table: {table}")
|
|
61
|
+
row = conn.execute(
|
|
62
|
+
"SELECT count(*) FROM sqlite_master WHERE type IN ('table', 'view') "
|
|
63
|
+
"AND name = ?",
|
|
64
|
+
(table,),
|
|
65
|
+
).fetchone()
|
|
66
|
+
return row[0] > 0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# Migration functions
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _migrate_v2(conn: sqlite3.Connection) -> None:
|
|
75
|
+
"""v2: Add signature column to nodes table."""
|
|
76
|
+
if not _has_column(conn, "nodes", "signature"):
|
|
77
|
+
conn.execute("ALTER TABLE nodes ADD COLUMN signature TEXT")
|
|
78
|
+
logger.info("Migration v2: added 'signature' column to nodes")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _migrate_v3(conn: sqlite3.Connection) -> None:
|
|
82
|
+
"""v3: Create flows and flow_memberships tables."""
|
|
83
|
+
conn.execute("""
|
|
84
|
+
CREATE TABLE IF NOT EXISTS flows (
|
|
85
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
86
|
+
name TEXT NOT NULL,
|
|
87
|
+
entry_point_id INTEGER NOT NULL,
|
|
88
|
+
depth INTEGER NOT NULL,
|
|
89
|
+
node_count INTEGER NOT NULL,
|
|
90
|
+
file_count INTEGER NOT NULL,
|
|
91
|
+
criticality REAL NOT NULL DEFAULT 0.0,
|
|
92
|
+
path_json TEXT NOT NULL,
|
|
93
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
94
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
95
|
+
)
|
|
96
|
+
""")
|
|
97
|
+
conn.execute("""
|
|
98
|
+
CREATE TABLE IF NOT EXISTS flow_memberships (
|
|
99
|
+
flow_id INTEGER NOT NULL,
|
|
100
|
+
node_id INTEGER NOT NULL,
|
|
101
|
+
position INTEGER NOT NULL,
|
|
102
|
+
PRIMARY KEY (flow_id, node_id)
|
|
103
|
+
)
|
|
104
|
+
""")
|
|
105
|
+
conn.execute(
|
|
106
|
+
"CREATE INDEX IF NOT EXISTS idx_flows_criticality ON flows(criticality DESC)"
|
|
107
|
+
)
|
|
108
|
+
conn.execute(
|
|
109
|
+
"CREATE INDEX IF NOT EXISTS idx_flows_entry ON flows(entry_point_id)"
|
|
110
|
+
)
|
|
111
|
+
conn.execute(
|
|
112
|
+
"CREATE INDEX IF NOT EXISTS idx_flow_memberships_node ON flow_memberships(node_id)"
|
|
113
|
+
)
|
|
114
|
+
logger.info("Migration v3: created flows and flow_memberships tables")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _migrate_v4(conn: sqlite3.Connection) -> None:
|
|
118
|
+
"""v4: Create communities table, add community_id to nodes."""
|
|
119
|
+
conn.execute("""
|
|
120
|
+
CREATE TABLE IF NOT EXISTS communities (
|
|
121
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
122
|
+
name TEXT NOT NULL,
|
|
123
|
+
level INTEGER NOT NULL DEFAULT 0,
|
|
124
|
+
parent_id INTEGER,
|
|
125
|
+
cohesion REAL NOT NULL DEFAULT 0.0,
|
|
126
|
+
size INTEGER NOT NULL DEFAULT 0,
|
|
127
|
+
dominant_language TEXT,
|
|
128
|
+
description TEXT,
|
|
129
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
130
|
+
)
|
|
131
|
+
""")
|
|
132
|
+
if not _has_column(conn, "nodes", "community_id"):
|
|
133
|
+
conn.execute("ALTER TABLE nodes ADD COLUMN community_id INTEGER")
|
|
134
|
+
logger.info("Migration v4: added 'community_id' column to nodes")
|
|
135
|
+
conn.execute(
|
|
136
|
+
"CREATE INDEX IF NOT EXISTS idx_nodes_community ON nodes(community_id)"
|
|
137
|
+
)
|
|
138
|
+
conn.execute(
|
|
139
|
+
"CREATE INDEX IF NOT EXISTS idx_communities_parent ON communities(parent_id)"
|
|
140
|
+
)
|
|
141
|
+
conn.execute(
|
|
142
|
+
"CREATE INDEX IF NOT EXISTS idx_communities_cohesion ON communities(cohesion DESC)"
|
|
143
|
+
)
|
|
144
|
+
logger.info("Migration v4: created communities table")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _migrate_v5(conn: sqlite3.Connection) -> None:
|
|
148
|
+
"""v5: Create FTS5 virtual table for nodes."""
|
|
149
|
+
if not _table_exists(conn, "nodes_fts"):
|
|
150
|
+
conn.execute("""
|
|
151
|
+
CREATE VIRTUAL TABLE nodes_fts USING fts5(
|
|
152
|
+
name, qualified_name, file_path, signature,
|
|
153
|
+
content='nodes', content_rowid='rowid',
|
|
154
|
+
tokenize='porter unicode61'
|
|
155
|
+
)
|
|
156
|
+
""")
|
|
157
|
+
logger.info("Migration v5: created nodes_fts FTS5 virtual table")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _migrate_v6(conn: sqlite3.Connection) -> None:
|
|
161
|
+
"""v6: Add pre-computed summary tables for token-efficient queries."""
|
|
162
|
+
conn.execute("""
|
|
163
|
+
CREATE TABLE IF NOT EXISTS community_summaries (
|
|
164
|
+
community_id INTEGER PRIMARY KEY,
|
|
165
|
+
name TEXT NOT NULL,
|
|
166
|
+
purpose TEXT DEFAULT '',
|
|
167
|
+
key_symbols TEXT DEFAULT '[]',
|
|
168
|
+
risk TEXT DEFAULT 'unknown',
|
|
169
|
+
size INTEGER DEFAULT 0,
|
|
170
|
+
dominant_language TEXT DEFAULT '',
|
|
171
|
+
FOREIGN KEY (community_id) REFERENCES communities(id)
|
|
172
|
+
)
|
|
173
|
+
""")
|
|
174
|
+
conn.execute("""
|
|
175
|
+
CREATE TABLE IF NOT EXISTS flow_snapshots (
|
|
176
|
+
flow_id INTEGER PRIMARY KEY,
|
|
177
|
+
name TEXT NOT NULL,
|
|
178
|
+
entry_point TEXT NOT NULL,
|
|
179
|
+
critical_path TEXT DEFAULT '[]',
|
|
180
|
+
criticality REAL DEFAULT 0.0,
|
|
181
|
+
node_count INTEGER DEFAULT 0,
|
|
182
|
+
file_count INTEGER DEFAULT 0,
|
|
183
|
+
FOREIGN KEY (flow_id) REFERENCES flows(id)
|
|
184
|
+
)
|
|
185
|
+
""")
|
|
186
|
+
conn.execute("""
|
|
187
|
+
CREATE TABLE IF NOT EXISTS risk_index (
|
|
188
|
+
node_id INTEGER PRIMARY KEY,
|
|
189
|
+
qualified_name TEXT NOT NULL,
|
|
190
|
+
risk_score REAL DEFAULT 0.0,
|
|
191
|
+
caller_count INTEGER DEFAULT 0,
|
|
192
|
+
test_coverage TEXT DEFAULT 'unknown',
|
|
193
|
+
security_relevant INTEGER DEFAULT 0,
|
|
194
|
+
last_computed TEXT DEFAULT '',
|
|
195
|
+
FOREIGN KEY (node_id) REFERENCES nodes(id)
|
|
196
|
+
)
|
|
197
|
+
""")
|
|
198
|
+
conn.execute(
|
|
199
|
+
"CREATE INDEX IF NOT EXISTS idx_risk_index_score "
|
|
200
|
+
"ON risk_index(risk_score DESC)"
|
|
201
|
+
)
|
|
202
|
+
logger.info("Migration v6: created summary tables "
|
|
203
|
+
"(community_summaries, flow_snapshots, risk_index)")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _migrate_v7(conn: sqlite3.Connection) -> None:
|
|
207
|
+
"""v7: Add compound edge indexes for summary and risk queries."""
|
|
208
|
+
conn.execute(
|
|
209
|
+
"CREATE INDEX IF NOT EXISTS idx_edges_target_kind "
|
|
210
|
+
"ON edges(target_qualified, kind)"
|
|
211
|
+
)
|
|
212
|
+
conn.execute(
|
|
213
|
+
"CREATE INDEX IF NOT EXISTS idx_edges_source_kind "
|
|
214
|
+
"ON edges(source_qualified, kind)"
|
|
215
|
+
)
|
|
216
|
+
logger.info("Migration v7: added compound edge indexes")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _migrate_v8(conn: sqlite3.Connection) -> None:
|
|
220
|
+
"""v8: Add composite index on edges for upsert_edge performance."""
|
|
221
|
+
conn.execute("""
|
|
222
|
+
CREATE INDEX IF NOT EXISTS idx_edges_composite
|
|
223
|
+
ON edges(kind, source_qualified, target_qualified, file_path, line)
|
|
224
|
+
""")
|
|
225
|
+
logger.info("Migration v8: created composite edge index")
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _migrate_v9(conn: sqlite3.Connection) -> None:
|
|
229
|
+
"""v9: Add confidence scoring to edges."""
|
|
230
|
+
if not _has_column(conn, "edges", "confidence"):
|
|
231
|
+
conn.execute(
|
|
232
|
+
"ALTER TABLE edges ADD COLUMN confidence REAL DEFAULT 1.0"
|
|
233
|
+
)
|
|
234
|
+
if not _has_column(conn, "edges", "confidence_tier"):
|
|
235
|
+
conn.execute(
|
|
236
|
+
"ALTER TABLE edges ADD COLUMN confidence_tier TEXT DEFAULT 'EXTRACTED'"
|
|
237
|
+
)
|
|
238
|
+
logger.info("Migration v9: added edge confidence columns")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# ---------------------------------------------------------------------------
|
|
242
|
+
# Migration registry
|
|
243
|
+
# ---------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
MIGRATIONS: dict[int, Callable[[sqlite3.Connection], None]] = {
|
|
246
|
+
2: _migrate_v2,
|
|
247
|
+
3: _migrate_v3,
|
|
248
|
+
4: _migrate_v4,
|
|
249
|
+
5: _migrate_v5,
|
|
250
|
+
6: _migrate_v6,
|
|
251
|
+
7: _migrate_v7,
|
|
252
|
+
8: _migrate_v8,
|
|
253
|
+
9: _migrate_v9,
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
LATEST_VERSION = max(MIGRATIONS.keys())
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def run_migrations(conn: sqlite3.Connection) -> None:
|
|
260
|
+
"""Run all pending migrations in order.
|
|
261
|
+
|
|
262
|
+
Each migration runs in its own transaction. The schema_version metadata
|
|
263
|
+
entry is updated after each successful migration.
|
|
264
|
+
"""
|
|
265
|
+
current = get_schema_version(conn)
|
|
266
|
+
if current >= LATEST_VERSION:
|
|
267
|
+
return
|
|
268
|
+
|
|
269
|
+
logger.info("Schema version %d -> %d: running migrations", current, LATEST_VERSION)
|
|
270
|
+
|
|
271
|
+
for version in sorted(MIGRATIONS.keys()):
|
|
272
|
+
if version <= current:
|
|
273
|
+
continue
|
|
274
|
+
logger.info("Running migration v%d", version)
|
|
275
|
+
try:
|
|
276
|
+
MIGRATIONS[version](conn)
|
|
277
|
+
_set_schema_version(conn, version)
|
|
278
|
+
conn.commit()
|
|
279
|
+
except sqlite3.Error:
|
|
280
|
+
conn.rollback()
|
|
281
|
+
logger.error("Migration v%d failed, rolling back", version, exc_info=True)
|
|
282
|
+
raise
|
|
283
|
+
|
|
284
|
+
logger.info("Migrations complete, now at schema version %d", LATEST_VERSION)
|