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.
Files changed (44) hide show
  1. codegraph/__init__.py +12 -0
  2. codegraph/__main__.py +6 -0
  3. codegraph/cli.py +911 -0
  4. codegraph/codegraph.py +618 -0
  5. codegraph/context/__init__.py +216 -0
  6. codegraph/db/__init__.py +11 -0
  7. codegraph/db/connection.py +163 -0
  8. codegraph/db/queries.py +752 -0
  9. codegraph/db/schema.sql +151 -0
  10. codegraph/directory.py +160 -0
  11. codegraph/errors.py +101 -0
  12. codegraph/extraction/__init__.py +956 -0
  13. codegraph/extraction/languages/__init__.py +64 -0
  14. codegraph/extraction/languages/base.py +132 -0
  15. codegraph/extraction/languages/c_cfg.py +53 -0
  16. codegraph/extraction/languages/cpp_cfg.py +60 -0
  17. codegraph/extraction/languages/dart_cfg.py +53 -0
  18. codegraph/extraction/languages/go_cfg.py +70 -0
  19. codegraph/extraction/languages/java_cfg.py +62 -0
  20. codegraph/extraction/languages/javascript_cfg.py +84 -0
  21. codegraph/extraction/languages/kotlin_cfg.py +52 -0
  22. codegraph/extraction/languages/lua_cfg.py +65 -0
  23. codegraph/extraction/languages/python_cfg.py +75 -0
  24. codegraph/extraction/languages/ruby_cfg.py +48 -0
  25. codegraph/extraction/languages/rust_cfg.py +68 -0
  26. codegraph/extraction/languages/scala_cfg.py +59 -0
  27. codegraph/extraction/languages/swift_cfg.py +64 -0
  28. codegraph/extraction/languages/typescript_cfg.py +89 -0
  29. codegraph/extraction/tree_sitter_extractor.py +689 -0
  30. codegraph/graph/__init__.py +685 -0
  31. codegraph/installer/__init__.py +6 -0
  32. codegraph/mcp/__init__.py +668 -0
  33. codegraph/project_config.py +191 -0
  34. codegraph/resolution/__init__.py +337 -0
  35. codegraph/search/__init__.py +653 -0
  36. codegraph/sync/__init__.py +204 -0
  37. codegraph/types.py +334 -0
  38. codegraph/ui/__init__.py +11 -0
  39. codegraph/utils.py +218 -0
  40. codegraph_py-1.0.0.dist-info/METADATA +238 -0
  41. codegraph_py-1.0.0.dist-info/RECORD +44 -0
  42. codegraph_py-1.0.0.dist-info/WHEEL +5 -0
  43. codegraph_py-1.0.0.dist-info/entry_points.txt +2 -0
  44. 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)
@@ -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))