codegraph-py 1.0.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.
- codegraph/__init__.py +12 -0
- codegraph/__main__.py +6 -0
- codegraph/cli.py +911 -0
- codegraph/codegraph.py +618 -0
- codegraph/context/__init__.py +216 -0
- codegraph/db/__init__.py +11 -0
- codegraph/db/connection.py +163 -0
- codegraph/db/queries.py +752 -0
- codegraph/db/schema.sql +151 -0
- codegraph/directory.py +160 -0
- codegraph/errors.py +101 -0
- codegraph/extraction/__init__.py +956 -0
- codegraph/extraction/languages/__init__.py +64 -0
- codegraph/extraction/languages/base.py +132 -0
- codegraph/extraction/languages/c_cfg.py +53 -0
- codegraph/extraction/languages/cpp_cfg.py +60 -0
- codegraph/extraction/languages/dart_cfg.py +53 -0
- codegraph/extraction/languages/go_cfg.py +70 -0
- codegraph/extraction/languages/java_cfg.py +62 -0
- codegraph/extraction/languages/javascript_cfg.py +84 -0
- codegraph/extraction/languages/kotlin_cfg.py +52 -0
- codegraph/extraction/languages/lua_cfg.py +65 -0
- codegraph/extraction/languages/python_cfg.py +75 -0
- codegraph/extraction/languages/ruby_cfg.py +48 -0
- codegraph/extraction/languages/rust_cfg.py +68 -0
- codegraph/extraction/languages/scala_cfg.py +59 -0
- codegraph/extraction/languages/swift_cfg.py +64 -0
- codegraph/extraction/languages/typescript_cfg.py +89 -0
- codegraph/extraction/tree_sitter_extractor.py +689 -0
- codegraph/graph/__init__.py +685 -0
- codegraph/installer/__init__.py +6 -0
- codegraph/mcp/__init__.py +668 -0
- codegraph/project_config.py +191 -0
- codegraph/resolution/__init__.py +337 -0
- codegraph/search/__init__.py +653 -0
- codegraph/sync/__init__.py +204 -0
- codegraph/types.py +334 -0
- codegraph/ui/__init__.py +11 -0
- codegraph/utils.py +218 -0
- codegraph_py-1.0.0.dist-info/METADATA +238 -0
- codegraph_py-1.0.0.dist-info/RECORD +44 -0
- codegraph_py-1.0.0.dist-info/WHEEL +5 -0
- codegraph_py-1.0.0.dist-info/entry_points.txt +2 -0
- codegraph_py-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CodeGraph Context Builder
|
|
3
|
+
|
|
4
|
+
Builds rich context for AI agents to understand code.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Dict, List, Optional, Set, Any, Tuple
|
|
10
|
+
|
|
11
|
+
from codegraph.types import (
|
|
12
|
+
Node, Edge, Subgraph, Context, CodeBlock, TaskContext,
|
|
13
|
+
BuildContextOptions, SearchOptions,
|
|
14
|
+
)
|
|
15
|
+
from codegraph.db.queries import QueryBuilder
|
|
16
|
+
from codegraph.graph import GraphTraverser
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ContextBuilder:
|
|
20
|
+
"""Builds rich code context for AI agents."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, project_root: str, queries: QueryBuilder,
|
|
23
|
+
traverser: GraphTraverser):
|
|
24
|
+
self._project_root = project_root
|
|
25
|
+
self._queries = queries
|
|
26
|
+
self._traverser = traverser
|
|
27
|
+
|
|
28
|
+
def build_context(self, node_id: str, depth: int = 2) -> Context:
|
|
29
|
+
"""Build full context around a focal node."""
|
|
30
|
+
focal = self._queries.get_node_by_id(node_id)
|
|
31
|
+
if not focal:
|
|
32
|
+
return Context()
|
|
33
|
+
|
|
34
|
+
ctx = Context(focal=focal)
|
|
35
|
+
|
|
36
|
+
# Get ancestors
|
|
37
|
+
ancestors = self._traverser.get_ancestors(node_id)
|
|
38
|
+
ctx.ancestors = ancestors
|
|
39
|
+
|
|
40
|
+
# Get children
|
|
41
|
+
children = self._traverser.get_children(node_id)
|
|
42
|
+
ctx.children = children
|
|
43
|
+
|
|
44
|
+
# Get incoming references
|
|
45
|
+
incoming_edges = self._queries.get_incoming_edges(node_id)
|
|
46
|
+
ctx.incoming_refs = []
|
|
47
|
+
for edge in incoming_edges:
|
|
48
|
+
source = self._queries.get_node_by_id(edge.source)
|
|
49
|
+
if source:
|
|
50
|
+
ctx.incoming_refs.append({'node': source, 'edge': edge})
|
|
51
|
+
|
|
52
|
+
# Get outgoing references
|
|
53
|
+
outgoing_edges = self._queries.get_outgoing_edges(node_id)
|
|
54
|
+
ctx.outgoing_refs = []
|
|
55
|
+
for edge in outgoing_edges:
|
|
56
|
+
target = self._queries.get_node_by_id(edge.target)
|
|
57
|
+
if target:
|
|
58
|
+
ctx.outgoing_refs.append({'node': target, 'edge': edge})
|
|
59
|
+
|
|
60
|
+
# Collect types (extends/implements targets)
|
|
61
|
+
for edge in outgoing_edges:
|
|
62
|
+
if edge.kind in ('extends', 'implements', 'type_of', 'returns'):
|
|
63
|
+
target = self._queries.get_node_by_id(edge.target)
|
|
64
|
+
if target and target not in ctx.types:
|
|
65
|
+
ctx.types.append(target)
|
|
66
|
+
|
|
67
|
+
# Collect imports
|
|
68
|
+
for edge in outgoing_edges:
|
|
69
|
+
if edge.kind == 'imports':
|
|
70
|
+
target = self._queries.get_node_by_id(edge.target)
|
|
71
|
+
if target:
|
|
72
|
+
ctx.imports.append(target)
|
|
73
|
+
|
|
74
|
+
return ctx
|
|
75
|
+
|
|
76
|
+
def build_task_context(self, query: str,
|
|
77
|
+
options: Optional[BuildContextOptions] = None) -> TaskContext:
|
|
78
|
+
"""Build context for a task query."""
|
|
79
|
+
opts = options or BuildContextOptions()
|
|
80
|
+
task_ctx = TaskContext(query=query)
|
|
81
|
+
|
|
82
|
+
# Search for relevant nodes
|
|
83
|
+
results = self._queries.search_nodes(query, SearchOptions(limit=opts.search_limit))
|
|
84
|
+
entry_points = [r.node for r in results]
|
|
85
|
+
task_ctx.entry_points = entry_points
|
|
86
|
+
|
|
87
|
+
if not entry_points:
|
|
88
|
+
return task_ctx
|
|
89
|
+
|
|
90
|
+
# Build subgraph by traversing from entry points
|
|
91
|
+
subgraph = Subgraph()
|
|
92
|
+
visited_nodes: Set[str] = set()
|
|
93
|
+
all_edges: List[Edge] = []
|
|
94
|
+
|
|
95
|
+
for ep in entry_points:
|
|
96
|
+
if ep.id not in visited_nodes:
|
|
97
|
+
subgraph.nodes[ep.id] = ep
|
|
98
|
+
visited_nodes.add(ep.id)
|
|
99
|
+
|
|
100
|
+
# Traverse outgoing edges
|
|
101
|
+
if opts.traversal_depth > 0:
|
|
102
|
+
edges = self._queries.get_outgoing_edges(ep.id)
|
|
103
|
+
for edge in edges:
|
|
104
|
+
all_edges.append(edge)
|
|
105
|
+
if edge.target not in visited_nodes:
|
|
106
|
+
target = self._queries.get_node_by_id(edge.target)
|
|
107
|
+
if target:
|
|
108
|
+
subgraph.nodes[edge.target] = target
|
|
109
|
+
visited_nodes.add(edge.target)
|
|
110
|
+
|
|
111
|
+
# Traverse incoming edges
|
|
112
|
+
edges_in = self._queries.get_incoming_edges(ep.id)
|
|
113
|
+
for edge in edges_in:
|
|
114
|
+
all_edges.append(edge)
|
|
115
|
+
|
|
116
|
+
# Deduplicate edges
|
|
117
|
+
seen_edges: Set[Tuple[str, str, str]] = set()
|
|
118
|
+
for edge in all_edges:
|
|
119
|
+
key = (edge.source, edge.target, edge.kind)
|
|
120
|
+
if key not in seen_edges:
|
|
121
|
+
seen_edges.add(key)
|
|
122
|
+
subgraph.edges.append(edge)
|
|
123
|
+
|
|
124
|
+
subgraph.roots = [ep.id for ep in entry_points]
|
|
125
|
+
subgraph.confidence = 'high' if entry_points else 'low'
|
|
126
|
+
task_ctx.subgraph = subgraph
|
|
127
|
+
|
|
128
|
+
# Generate code blocks from nodes
|
|
129
|
+
if opts.include_code:
|
|
130
|
+
code_blocks = []
|
|
131
|
+
nodes_to_include = list(subgraph.nodes.values())[:opts.max_code_blocks]
|
|
132
|
+
for node in nodes_to_include:
|
|
133
|
+
block = self._read_code_block(node, opts.max_code_block_size)
|
|
134
|
+
if block:
|
|
135
|
+
code_blocks.append(block)
|
|
136
|
+
task_ctx.code_blocks = code_blocks
|
|
137
|
+
|
|
138
|
+
# Collect related files
|
|
139
|
+
files: Set[str] = set()
|
|
140
|
+
for node in subgraph.nodes.values():
|
|
141
|
+
files.add(node.file_path)
|
|
142
|
+
task_ctx.related_files = list(files)[:50]
|
|
143
|
+
|
|
144
|
+
# Generate summary
|
|
145
|
+
task_ctx.summary = self._generate_summary(query, entry_points, subgraph)
|
|
146
|
+
|
|
147
|
+
# Stats
|
|
148
|
+
task_ctx.stats = {
|
|
149
|
+
'nodeCount': len(subgraph.nodes),
|
|
150
|
+
'edgeCount': len(subgraph.edges),
|
|
151
|
+
'fileCount': len(task_ctx.related_files),
|
|
152
|
+
'codeBlockCount': len(task_ctx.code_blocks),
|
|
153
|
+
'totalCodeSize': sum(len(b.content) for b in task_ctx.code_blocks),
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return task_ctx
|
|
157
|
+
|
|
158
|
+
def _read_code_block(self, node: Node, max_size: int) -> Optional[CodeBlock]:
|
|
159
|
+
"""Read the source code for a node."""
|
|
160
|
+
try:
|
|
161
|
+
filepath = os.path.join(self._project_root, node.file_path)
|
|
162
|
+
if not os.path.isfile(filepath):
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
with open(filepath, 'r', encoding='utf-8', errors='replace') as f:
|
|
166
|
+
lines = f.readlines()
|
|
167
|
+
|
|
168
|
+
# Extract the range
|
|
169
|
+
start = max(0, node.start_line - 1)
|
|
170
|
+
end = min(len(lines), node.end_line)
|
|
171
|
+
code_lines = lines[start:end]
|
|
172
|
+
|
|
173
|
+
content = ''.join(code_lines).strip()
|
|
174
|
+
if not content:
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
# Truncate if too long
|
|
178
|
+
if len(content) > max_size:
|
|
179
|
+
content = content[:max_size] + '\n... (truncated)'
|
|
180
|
+
|
|
181
|
+
return CodeBlock(
|
|
182
|
+
content=content,
|
|
183
|
+
file_path=node.file_path,
|
|
184
|
+
start_line=node.start_line,
|
|
185
|
+
end_line=node.end_line,
|
|
186
|
+
language=node.language,
|
|
187
|
+
node=node,
|
|
188
|
+
)
|
|
189
|
+
except Exception:
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
def _generate_summary(self, query: str, entry_points: List[Node],
|
|
193
|
+
subgraph: Subgraph) -> str:
|
|
194
|
+
"""Generate a text summary of the context."""
|
|
195
|
+
if not entry_points:
|
|
196
|
+
return f'No relevant symbols found for: {query}'
|
|
197
|
+
|
|
198
|
+
ep_names = ', '.join(
|
|
199
|
+
f'{n.name} ({n.kind})' for n in entry_points[:5]
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
files = set(n.file_path for n in subgraph.nodes.values())
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
f'Found {len(subgraph.nodes)} symbols across {len(files)} files '
|
|
206
|
+
f'related to "{query}". '
|
|
207
|
+
f'Entry points: {ep_names}.'
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
import os
|
|
212
|
+
|
|
213
|
+
def create_context_builder(project_root: str, queries: QueryBuilder,
|
|
214
|
+
traverser: GraphTraverser) -> ContextBuilder:
|
|
215
|
+
"""Factory function to create a ContextBuilder."""
|
|
216
|
+
return ContextBuilder(project_root, queries, traverser)
|
codegraph/db/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""CodeGraph Database Layer."""
|
|
2
|
+
|
|
3
|
+
from codegraph.db.connection import DatabaseConnection, get_database_path, remove_database_files
|
|
4
|
+
from codegraph.db.queries import QueryBuilder
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
'DatabaseConnection',
|
|
8
|
+
'get_database_path',
|
|
9
|
+
'remove_database_files',
|
|
10
|
+
'QueryBuilder',
|
|
11
|
+
]
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CodeGraph Database Connection
|
|
3
|
+
|
|
4
|
+
Manages SQLite database connections and lifecycle.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sqlite3
|
|
11
|
+
import json
|
|
12
|
+
import time
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
15
|
+
|
|
16
|
+
from codegraph.errors import DatabaseError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_database_path(project_root: str) -> str:
|
|
20
|
+
"""Get the path to the SQLite database."""
|
|
21
|
+
return os.path.join(project_root, '.codegraph', 'codegraph.db')
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def remove_database_files(db_path: str) -> None:
|
|
25
|
+
"""Remove the database file and its WAL/SHM sidecars."""
|
|
26
|
+
for suffix in ('', '-wal', '-shm'):
|
|
27
|
+
p = db_path + suffix
|
|
28
|
+
if os.path.exists(p):
|
|
29
|
+
os.remove(p)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DatabaseConnection:
|
|
33
|
+
"""Manages a SQLite database connection."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, db_path: str, db: sqlite3.Connection):
|
|
36
|
+
self._path = db_path
|
|
37
|
+
self._db = db
|
|
38
|
+
self._db.row_factory = sqlite3.Row
|
|
39
|
+
|
|
40
|
+
# ── Connection PRAGMAs (order matters) ──
|
|
41
|
+
# busy_timeout MUST be first — if another process holds a write lock,
|
|
42
|
+
# subsequent pragmas wait out the lock instead of throwing immediately.
|
|
43
|
+
self._db.execute("PRAGMA busy_timeout = 5000")
|
|
44
|
+
self._db.execute("PRAGMA foreign_keys = ON")
|
|
45
|
+
self._db.execute("PRAGMA journal_mode = WAL") # Write-Ahead Logging
|
|
46
|
+
self._db.execute("PRAGMA synchronous = NORMAL") # safe with WAL mode
|
|
47
|
+
self._db.execute("PRAGMA cache_size = -64000") # 64 MB page cache
|
|
48
|
+
self._db.execute("PRAGMA temp_store = MEMORY") # temp tables in memory
|
|
49
|
+
self._db.execute("PRAGMA mmap_size = 268435456") # 256 MB memory-mapped I/O
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def initialize(db_path: str) -> 'DatabaseConnection':
|
|
53
|
+
"""Create a new database with schema."""
|
|
54
|
+
from codegraph import directory as dir_mod
|
|
55
|
+
cg_dir = os.path.dirname(db_path)
|
|
56
|
+
os.makedirs(cg_dir, exist_ok=True)
|
|
57
|
+
|
|
58
|
+
# Create .gitignore inside .codegraph/ to prevent index from being tracked
|
|
59
|
+
gitignore_path = os.path.join(cg_dir, '.gitignore')
|
|
60
|
+
if not os.path.isfile(gitignore_path):
|
|
61
|
+
with open(gitignore_path, 'w') as f:
|
|
62
|
+
f.write('# CodeGraph index — auto-generated, not for version control\n*\n')
|
|
63
|
+
|
|
64
|
+
db = sqlite3.connect(db_path, check_same_thread=False)
|
|
65
|
+
conn = DatabaseConnection(db_path, db)
|
|
66
|
+
|
|
67
|
+
# Run schema
|
|
68
|
+
schema_path = os.path.join(os.path.dirname(__file__), 'schema.sql')
|
|
69
|
+
if os.path.isfile(schema_path):
|
|
70
|
+
with open(schema_path, 'r') as f:
|
|
71
|
+
schema = f.read()
|
|
72
|
+
db.executescript(schema)
|
|
73
|
+
|
|
74
|
+
# Set initial schema version
|
|
75
|
+
conn.set_metadata('schema_version', '1')
|
|
76
|
+
|
|
77
|
+
return conn
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def open(db_path: str) -> 'DatabaseConnection':
|
|
81
|
+
"""Open an existing database."""
|
|
82
|
+
if not os.path.isfile(db_path):
|
|
83
|
+
raise DatabaseError(f'Database not found: {db_path}')
|
|
84
|
+
db = sqlite3.connect(db_path, check_same_thread=False)
|
|
85
|
+
return DatabaseConnection(db_path, db)
|
|
86
|
+
|
|
87
|
+
def get_db(self) -> sqlite3.Connection:
|
|
88
|
+
"""Get the raw SQLite connection."""
|
|
89
|
+
return self._db
|
|
90
|
+
|
|
91
|
+
def get_path(self) -> str:
|
|
92
|
+
"""Get the database file path."""
|
|
93
|
+
return self._path
|
|
94
|
+
|
|
95
|
+
def close(self) -> None:
|
|
96
|
+
"""Close the database connection."""
|
|
97
|
+
try:
|
|
98
|
+
self._db.close()
|
|
99
|
+
except Exception:
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
def run_maintenance(self) -> None:
|
|
103
|
+
"""Run maintenance operations after bulk writes."""
|
|
104
|
+
try:
|
|
105
|
+
self._db.execute("PRAGMA optimize")
|
|
106
|
+
except Exception:
|
|
107
|
+
pass
|
|
108
|
+
try:
|
|
109
|
+
# Fold pending WAL pages back into the main DB file
|
|
110
|
+
self._db.execute("PRAGMA wal_checkpoint(PASSIVE)")
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
# =========================================================================
|
|
115
|
+
# Metadata
|
|
116
|
+
# =========================================================================
|
|
117
|
+
|
|
118
|
+
def get_metadata(self, key: str) -> Optional[str]:
|
|
119
|
+
"""Get a metadata value."""
|
|
120
|
+
try:
|
|
121
|
+
cur = self._db.execute(
|
|
122
|
+
'SELECT value FROM project_metadata WHERE key = ?', (key,)
|
|
123
|
+
)
|
|
124
|
+
row = cur.fetchone()
|
|
125
|
+
return row['value'] if row else None
|
|
126
|
+
except Exception:
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
def set_metadata(self, key: str, value: str) -> None:
|
|
130
|
+
"""Set a metadata value."""
|
|
131
|
+
try:
|
|
132
|
+
self._db.execute(
|
|
133
|
+
'''INSERT OR REPLACE INTO project_metadata (key, value, updated_at)
|
|
134
|
+
VALUES (?, ?, ?)''',
|
|
135
|
+
(key, value, int(time.time() * 1000))
|
|
136
|
+
)
|
|
137
|
+
except Exception:
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
def get_journal_mode(self) -> Optional[str]:
|
|
141
|
+
"""Get the effective journal mode."""
|
|
142
|
+
try:
|
|
143
|
+
cur = self._db.execute("PRAGMA journal_mode")
|
|
144
|
+
row = cur.fetchone()
|
|
145
|
+
return row[0] if row else None
|
|
146
|
+
except Exception:
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
def run_migrations(self) -> None:
|
|
150
|
+
"""Run any pending database migrations."""
|
|
151
|
+
# Check current version
|
|
152
|
+
current = self.get_metadata('schema_version')
|
|
153
|
+
if current is None:
|
|
154
|
+
current = '1'
|
|
155
|
+
|
|
156
|
+
version = int(current)
|
|
157
|
+
|
|
158
|
+
# Apply migrations as needed
|
|
159
|
+
# Version 1 is the initial schema
|
|
160
|
+
# Future migrations will be added here
|
|
161
|
+
|
|
162
|
+
if version > 1:
|
|
163
|
+
self.set_metadata('schema_version', str(version))
|