ctxgraph-code 0.1.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.
- ctxgraph_code/__init__.py +0 -0
- ctxgraph_code/__main__.py +3 -0
- ctxgraph_code/analyzers/__init__.py +0 -0
- ctxgraph_code/analyzers/python/__init__.py +0 -0
- ctxgraph_code/analyzers/python/importer.py +140 -0
- ctxgraph_code/analyzers/python/semantic.py +75 -0
- ctxgraph_code/analyzers/python/symbols.py +221 -0
- ctxgraph_code/cli.py +337 -0
- ctxgraph_code/config/__init__.py +0 -0
- ctxgraph_code/config/init.py +14 -0
- ctxgraph_code/config/settings.py +121 -0
- ctxgraph_code/exclude/__init__.py +0 -0
- ctxgraph_code/exclude/patterns.py +75 -0
- ctxgraph_code/graph/__init__.py +0 -0
- ctxgraph_code/graph/builder.py +76 -0
- ctxgraph_code/graph/models.py +83 -0
- ctxgraph_code/graph/query.py +115 -0
- ctxgraph_code/graph/storage.py +224 -0
- ctxgraph_code/render.py +244 -0
- ctxgraph_code-0.1.0.dist-info/METADATA +279 -0
- ctxgraph_code-0.1.0.dist-info/RECORD +24 -0
- ctxgraph_code-0.1.0.dist-info/WHEEL +5 -0
- ctxgraph_code-0.1.0.dist-info/entry_points.txt +2 -0
- ctxgraph_code-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Node:
|
|
9
|
+
id: str
|
|
10
|
+
type: str
|
|
11
|
+
name: str
|
|
12
|
+
path: Optional[str] = None
|
|
13
|
+
parent_id: Optional[str] = None
|
|
14
|
+
summary: Optional[str] = None
|
|
15
|
+
importance: float = 0.5
|
|
16
|
+
size_bytes: int = 0
|
|
17
|
+
lineno: int = 0
|
|
18
|
+
|
|
19
|
+
def __hash__(self):
|
|
20
|
+
return hash(self.id)
|
|
21
|
+
|
|
22
|
+
def __eq__(self, other):
|
|
23
|
+
return isinstance(other, Node) and self.id == other.id
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class Edge:
|
|
28
|
+
source_id: str
|
|
29
|
+
target_id: str
|
|
30
|
+
relation: str
|
|
31
|
+
weight: float = 1.0
|
|
32
|
+
|
|
33
|
+
def __hash__(self):
|
|
34
|
+
return hash((self.source_id, self.target_id, self.relation))
|
|
35
|
+
|
|
36
|
+
def __eq__(self, other):
|
|
37
|
+
return (
|
|
38
|
+
isinstance(other, Edge)
|
|
39
|
+
and self.source_id == other.source_id
|
|
40
|
+
and self.target_id == other.target_id
|
|
41
|
+
and self.relation == other.relation
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class Graph:
|
|
47
|
+
nodes: dict[str, Node] = field(default_factory=dict)
|
|
48
|
+
edges: list[Edge] = field(default_factory=list)
|
|
49
|
+
|
|
50
|
+
def add_node(self, node: Node):
|
|
51
|
+
self.nodes[node.id] = node
|
|
52
|
+
|
|
53
|
+
def add_edge(self, edge: Edge):
|
|
54
|
+
self.edges.append(edge)
|
|
55
|
+
|
|
56
|
+
def get_node(self, node_id: str) -> Optional[Node]:
|
|
57
|
+
return self.nodes.get(node_id)
|
|
58
|
+
|
|
59
|
+
def get_edges_from(self, source_id: str) -> list[Edge]:
|
|
60
|
+
return [e for e in self.edges if e.source_id == source_id]
|
|
61
|
+
|
|
62
|
+
def get_edges_to(self, target_id: str) -> list[Edge]:
|
|
63
|
+
return [e for e in self.edges if e.target_id == target_id]
|
|
64
|
+
|
|
65
|
+
def get_neighbors(self, node_id: str) -> list[str]:
|
|
66
|
+
result = set()
|
|
67
|
+
for e in self.edges:
|
|
68
|
+
if e.source_id == node_id:
|
|
69
|
+
result.add(e.target_id)
|
|
70
|
+
if e.target_id == node_id:
|
|
71
|
+
result.add(e.source_id)
|
|
72
|
+
return list(result)
|
|
73
|
+
|
|
74
|
+
def merge(self, other: Graph):
|
|
75
|
+
for node_id, node in other.nodes.items():
|
|
76
|
+
if node_id not in self.nodes:
|
|
77
|
+
self.nodes[node_id] = node
|
|
78
|
+
existing = {(e.source_id, e.target_id, e.relation) for e in self.edges}
|
|
79
|
+
for e in other.edges:
|
|
80
|
+
key = (e.source_id, e.target_id, e.relation)
|
|
81
|
+
if key not in existing:
|
|
82
|
+
self.edges.append(e)
|
|
83
|
+
existing.add(key)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from ctxgraph_code.graph.models import Node
|
|
7
|
+
from ctxgraph_code.graph.storage import Storage
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def search_relevant_nodes(
|
|
11
|
+
storage: Storage,
|
|
12
|
+
query: str,
|
|
13
|
+
max_nodes: int = 15,
|
|
14
|
+
max_depth: int = 2,
|
|
15
|
+
) -> list[tuple[Node, float]]:
|
|
16
|
+
tokens = _tokenize(query)
|
|
17
|
+
if not tokens:
|
|
18
|
+
return []
|
|
19
|
+
|
|
20
|
+
scored: dict[str, float] = {}
|
|
21
|
+
seen_ids: set[str] = set()
|
|
22
|
+
|
|
23
|
+
matched_nodes = storage.search_nodes(query)
|
|
24
|
+
for node in matched_nodes:
|
|
25
|
+
score = _compute_relevance(node, tokens)
|
|
26
|
+
if score > 0:
|
|
27
|
+
scored[node.id] = score
|
|
28
|
+
seen_ids.add(node.id)
|
|
29
|
+
|
|
30
|
+
if not scored:
|
|
31
|
+
for token in tokens:
|
|
32
|
+
token_nodes = storage.search_nodes(token)
|
|
33
|
+
for node in token_nodes:
|
|
34
|
+
if node.id not in seen_ids:
|
|
35
|
+
seen_ids.add(node.id)
|
|
36
|
+
score = _compute_relevance(node, tokens)
|
|
37
|
+
if score > 0:
|
|
38
|
+
scored[node.id] = score
|
|
39
|
+
|
|
40
|
+
if not scored:
|
|
41
|
+
return []
|
|
42
|
+
|
|
43
|
+
seed_ids = set(scored.keys())
|
|
44
|
+
edge_ids = set()
|
|
45
|
+
|
|
46
|
+
for _ in range(max_depth):
|
|
47
|
+
edges = storage.get_edges_for_nodes(seed_ids | edge_ids)
|
|
48
|
+
new_ids = set()
|
|
49
|
+
for e in edges:
|
|
50
|
+
if e.source_id in (seed_ids | edge_ids):
|
|
51
|
+
new_ids.add(e.target_id)
|
|
52
|
+
if e.target_id in (seed_ids | edge_ids):
|
|
53
|
+
new_ids.add(e.source_id)
|
|
54
|
+
edge_ids |= new_ids
|
|
55
|
+
|
|
56
|
+
all_ids = seed_ids | edge_ids
|
|
57
|
+
for nid in edge_ids:
|
|
58
|
+
if nid not in scored:
|
|
59
|
+
node = storage.get_node(nid)
|
|
60
|
+
if node:
|
|
61
|
+
neighbors = _count_matched_neighbors(nid, storage, seed_ids)
|
|
62
|
+
scored[nid] = 0.1 * neighbors
|
|
63
|
+
|
|
64
|
+
ranked = sorted(scored.items(), key=lambda x: x[1], reverse=True)
|
|
65
|
+
ranked = ranked[:max_nodes]
|
|
66
|
+
|
|
67
|
+
result = []
|
|
68
|
+
for nid, score in ranked:
|
|
69
|
+
node = storage.get_node(nid)
|
|
70
|
+
if node:
|
|
71
|
+
result.append((node, round(score, 3)))
|
|
72
|
+
|
|
73
|
+
return result
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _tokenize(text: str) -> list[str]:
|
|
77
|
+
text = text.lower()
|
|
78
|
+
tokens = re.findall(r"[a-zA-Z_][a-zA-Z0-9_]*", text)
|
|
79
|
+
stopwords = {
|
|
80
|
+
"the", "a", "an", "in", "on", "at", "to", "for", "of", "is",
|
|
81
|
+
"fix", "bug", "implement", "add", "change", "update", "remove",
|
|
82
|
+
"need", "want", "please", "can", "how", "what", "where", "why",
|
|
83
|
+
"this", "that", "with", "from", "by", "be", "has", "have", "do",
|
|
84
|
+
"does", "did", "will", "would", "could", "should", "may", "might",
|
|
85
|
+
"file", "function", "class", "code", "issue", "problem", "error",
|
|
86
|
+
"work", "make", "get", "set",
|
|
87
|
+
}
|
|
88
|
+
return [t for t in tokens if t not in stopwords and len(t) > 1]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _compute_relevance(node: Node, tokens: list[str]) -> float:
|
|
92
|
+
score = 0.0
|
|
93
|
+
text = f"{node.name} {node.summary or ''} {node.path or ''}".lower()
|
|
94
|
+
|
|
95
|
+
for token in tokens:
|
|
96
|
+
if token in node.name.lower():
|
|
97
|
+
score += 2.0
|
|
98
|
+
count = text.count(token)
|
|
99
|
+
score += count * 0.5
|
|
100
|
+
|
|
101
|
+
if node.importance:
|
|
102
|
+
score *= (0.5 + node.importance)
|
|
103
|
+
|
|
104
|
+
return score
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _count_matched_neighbors(
|
|
108
|
+
node_id: str, storage: Storage, matched_ids: set[str]
|
|
109
|
+
) -> int:
|
|
110
|
+
edges = storage.get_edges_for_nodes({node_id})
|
|
111
|
+
count = 0
|
|
112
|
+
for e in edges:
|
|
113
|
+
if e.source_id in matched_ids or e.target_id in matched_ids:
|
|
114
|
+
count += 1
|
|
115
|
+
return count
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sqlite3
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from ctxgraph_code.graph.models import Edge, Graph, Node
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _table_schema() -> str:
|
|
11
|
+
return """
|
|
12
|
+
CREATE TABLE IF NOT EXISTS nodes (
|
|
13
|
+
id TEXT PRIMARY KEY,
|
|
14
|
+
type TEXT NOT NULL,
|
|
15
|
+
name TEXT NOT NULL,
|
|
16
|
+
path TEXT,
|
|
17
|
+
parent_id TEXT,
|
|
18
|
+
summary TEXT,
|
|
19
|
+
importance REAL DEFAULT 0.5,
|
|
20
|
+
size_bytes INTEGER DEFAULT 0,
|
|
21
|
+
lineno INTEGER DEFAULT 0
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
25
|
+
source_id TEXT NOT NULL,
|
|
26
|
+
target_id TEXT NOT NULL,
|
|
27
|
+
relation TEXT NOT NULL,
|
|
28
|
+
weight REAL DEFAULT 1.0,
|
|
29
|
+
PRIMARY KEY (source_id, target_id, relation)
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source_id);
|
|
33
|
+
CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_id);
|
|
34
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_path ON nodes(path);
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_type ON nodes(type);
|
|
36
|
+
|
|
37
|
+
CREATE TABLE IF NOT EXISTS metadata (
|
|
38
|
+
key TEXT PRIMARY KEY,
|
|
39
|
+
value TEXT
|
|
40
|
+
);
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Storage:
|
|
45
|
+
def __init__(self, db_path: str | Path):
|
|
46
|
+
self.db_path = Path(db_path)
|
|
47
|
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
self._conn: Optional[sqlite3.Connection] = None
|
|
49
|
+
|
|
50
|
+
def connect(self):
|
|
51
|
+
self._conn = sqlite3.connect(str(self.db_path))
|
|
52
|
+
self._conn.execute("PRAGMA journal_mode=WAL")
|
|
53
|
+
self._conn.execute("PRAGMA synchronous=NORMAL")
|
|
54
|
+
self._conn.row_factory = sqlite3.Row
|
|
55
|
+
self._init_schema()
|
|
56
|
+
|
|
57
|
+
def _init_schema(self):
|
|
58
|
+
self._conn.executescript(_table_schema())
|
|
59
|
+
self._conn.commit()
|
|
60
|
+
|
|
61
|
+
def close(self):
|
|
62
|
+
if self._conn:
|
|
63
|
+
self._conn.close()
|
|
64
|
+
self._conn = None
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def conn(self) -> sqlite3.Connection:
|
|
68
|
+
if self._conn is None:
|
|
69
|
+
raise RuntimeError("Storage not connected. Call connect() first.")
|
|
70
|
+
return self._conn
|
|
71
|
+
|
|
72
|
+
def update_node_summary(self, node_id: str, summary: str):
|
|
73
|
+
self.conn.execute(
|
|
74
|
+
"UPDATE nodes SET summary = ? WHERE id = ?", (summary, node_id)
|
|
75
|
+
)
|
|
76
|
+
self.conn.commit()
|
|
77
|
+
|
|
78
|
+
def save_node(self, node: Node):
|
|
79
|
+
self.conn.execute(
|
|
80
|
+
"""INSERT OR REPLACE INTO nodes
|
|
81
|
+
(id, type, name, path, parent_id, summary, importance, size_bytes, lineno)
|
|
82
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
83
|
+
(
|
|
84
|
+
node.id,
|
|
85
|
+
node.type,
|
|
86
|
+
node.name,
|
|
87
|
+
node.path,
|
|
88
|
+
node.parent_id,
|
|
89
|
+
node.summary,
|
|
90
|
+
node.importance,
|
|
91
|
+
node.size_bytes,
|
|
92
|
+
node.lineno,
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def save_edge(self, edge: Edge):
|
|
97
|
+
self.conn.execute(
|
|
98
|
+
"""INSERT OR REPLACE INTO edges
|
|
99
|
+
(source_id, target_id, relation, weight)
|
|
100
|
+
VALUES (?, ?, ?, ?)""",
|
|
101
|
+
(edge.source_id, edge.target_id, edge.relation, edge.weight),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def save_graph(self, graph: Graph):
|
|
105
|
+
for node in graph.nodes.values():
|
|
106
|
+
self.save_node(node)
|
|
107
|
+
for edge in graph.edges:
|
|
108
|
+
self.save_edge(edge)
|
|
109
|
+
self.conn.commit()
|
|
110
|
+
|
|
111
|
+
def get_node(self, node_id: str) -> Optional[Node]:
|
|
112
|
+
row = self.conn.execute(
|
|
113
|
+
"SELECT * FROM nodes WHERE id = ?", (node_id,)
|
|
114
|
+
).fetchone()
|
|
115
|
+
if row is None:
|
|
116
|
+
return None
|
|
117
|
+
return Node(
|
|
118
|
+
id=row["id"],
|
|
119
|
+
type=row["type"],
|
|
120
|
+
name=row["name"],
|
|
121
|
+
path=row["path"],
|
|
122
|
+
parent_id=row["parent_id"],
|
|
123
|
+
summary=row["summary"],
|
|
124
|
+
importance=row["importance"],
|
|
125
|
+
size_bytes=row["size_bytes"],
|
|
126
|
+
lineno=row["lineno"],
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def search_nodes(self, text: str) -> list[Node]:
|
|
130
|
+
query = f"%{text}%"
|
|
131
|
+
rows = self.conn.execute(
|
|
132
|
+
"""SELECT * FROM nodes WHERE
|
|
133
|
+
name LIKE ? OR summary LIKE ? OR path LIKE ?
|
|
134
|
+
ORDER BY importance DESC
|
|
135
|
+
LIMIT 50""",
|
|
136
|
+
(query, query, query),
|
|
137
|
+
).fetchall()
|
|
138
|
+
return [
|
|
139
|
+
Node(
|
|
140
|
+
id=r["id"],
|
|
141
|
+
type=r["type"],
|
|
142
|
+
name=r["name"],
|
|
143
|
+
path=r["path"],
|
|
144
|
+
parent_id=r["parent_id"],
|
|
145
|
+
summary=r["summary"],
|
|
146
|
+
importance=r["importance"],
|
|
147
|
+
size_bytes=r["size_bytes"],
|
|
148
|
+
lineno=r["lineno"],
|
|
149
|
+
)
|
|
150
|
+
for r in rows
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
def get_edges_for_nodes(self, node_ids: set[str]) -> list[Edge]:
|
|
154
|
+
if not node_ids:
|
|
155
|
+
return []
|
|
156
|
+
placeholders = ",".join("?" for _ in node_ids)
|
|
157
|
+
rows = self.conn.execute(
|
|
158
|
+
f"""SELECT * FROM edges WHERE
|
|
159
|
+
source_id IN ({placeholders}) OR target_id IN ({placeholders})""",
|
|
160
|
+
list(node_ids) + list(node_ids),
|
|
161
|
+
).fetchall()
|
|
162
|
+
return [
|
|
163
|
+
Edge(
|
|
164
|
+
source_id=r["source_id"],
|
|
165
|
+
target_id=r["target_id"],
|
|
166
|
+
relation=r["relation"],
|
|
167
|
+
weight=r["weight"],
|
|
168
|
+
)
|
|
169
|
+
for r in rows
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
def get_all_nodes(self) -> list[Node]:
|
|
173
|
+
rows = self.conn.execute("SELECT * FROM nodes").fetchall()
|
|
174
|
+
return [
|
|
175
|
+
Node(
|
|
176
|
+
id=r["id"],
|
|
177
|
+
type=r["type"],
|
|
178
|
+
name=r["name"],
|
|
179
|
+
path=r["path"],
|
|
180
|
+
parent_id=r["parent_id"],
|
|
181
|
+
summary=r["summary"],
|
|
182
|
+
importance=r["importance"],
|
|
183
|
+
size_bytes=r["size_bytes"],
|
|
184
|
+
lineno=r["lineno"],
|
|
185
|
+
)
|
|
186
|
+
for r in rows
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
def get_all_edges(self) -> list[Edge]:
|
|
190
|
+
rows = self.conn.execute("SELECT * FROM edges").fetchall()
|
|
191
|
+
return [
|
|
192
|
+
Edge(
|
|
193
|
+
source_id=r["source_id"],
|
|
194
|
+
target_id=r["target_id"],
|
|
195
|
+
relation=r["relation"],
|
|
196
|
+
weight=r["weight"],
|
|
197
|
+
)
|
|
198
|
+
for r in rows
|
|
199
|
+
]
|
|
200
|
+
|
|
201
|
+
def stats(self) -> dict:
|
|
202
|
+
node_count = self.conn.execute("SELECT COUNT(*) FROM nodes").fetchone()[0]
|
|
203
|
+
edge_count = self.conn.execute("SELECT COUNT(*) FROM edges").fetchone()[0]
|
|
204
|
+
type_counts = self.conn.execute(
|
|
205
|
+
"SELECT type, COUNT(*) as cnt FROM nodes GROUP BY type"
|
|
206
|
+
).fetchall()
|
|
207
|
+
return {
|
|
208
|
+
"nodes": node_count,
|
|
209
|
+
"edges": edge_count,
|
|
210
|
+
"types": {r["type"]: r["cnt"] for r in type_counts},
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
def save_metadata(self, key: str, value: str):
|
|
214
|
+
self.conn.execute(
|
|
215
|
+
"INSERT OR REPLACE INTO metadata (key, value) VALUES (?, ?)",
|
|
216
|
+
(key, value),
|
|
217
|
+
)
|
|
218
|
+
self.conn.commit()
|
|
219
|
+
|
|
220
|
+
def get_metadata(self, key: str) -> Optional[str]:
|
|
221
|
+
row = self.conn.execute(
|
|
222
|
+
"SELECT value FROM metadata WHERE key = ?", (key,)
|
|
223
|
+
).fetchone()
|
|
224
|
+
return row["value"] if row else None
|
ctxgraph_code/render.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from ctxgraph_code.graph.models import Node
|
|
7
|
+
from ctxgraph_code.graph.storage import Storage
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def render_overview(storage: Storage, max_files: int = 30) -> str:
|
|
11
|
+
all_nodes = storage.get_all_nodes()
|
|
12
|
+
file_nodes = [n for n in all_nodes if n.type == "file"][:max_files]
|
|
13
|
+
|
|
14
|
+
lines = ["Project Overview", ""]
|
|
15
|
+
for node in file_nodes:
|
|
16
|
+
summary = node.summary or ""
|
|
17
|
+
lines.append(f" [F] {node.path or node.name}")
|
|
18
|
+
if summary:
|
|
19
|
+
lines.append(f" {summary}")
|
|
20
|
+
|
|
21
|
+
children = [
|
|
22
|
+
n for n in all_nodes
|
|
23
|
+
if n.parent_id == node.id and n.type in ("class", "function")
|
|
24
|
+
]
|
|
25
|
+
if children:
|
|
26
|
+
names = [c.name for c in children[:8]]
|
|
27
|
+
lines.append(f" Symbols: {', '.join(names)}")
|
|
28
|
+
|
|
29
|
+
lines.append("")
|
|
30
|
+
return "\n".join(lines)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def render_deps(storage: Storage, file_path: str) -> str:
|
|
34
|
+
all_nodes = storage.get_all_nodes()
|
|
35
|
+
all_edges = storage.get_all_edges()
|
|
36
|
+
node_id = f"file:{file_path}"
|
|
37
|
+
|
|
38
|
+
node = storage.get_node(node_id)
|
|
39
|
+
if not node:
|
|
40
|
+
return f"File not found in graph: {file_path}"
|
|
41
|
+
|
|
42
|
+
imports = []
|
|
43
|
+
imported_by = []
|
|
44
|
+
for e in all_edges:
|
|
45
|
+
if e.source_id == node_id and e.relation == "imports":
|
|
46
|
+
target = storage.get_node(e.target_id)
|
|
47
|
+
if target:
|
|
48
|
+
imports.append(target.path or target.name)
|
|
49
|
+
if e.target_id == node_id and e.relation == "imports":
|
|
50
|
+
source = storage.get_node(e.source_id)
|
|
51
|
+
if source:
|
|
52
|
+
imported_by.append(source.path or source.name)
|
|
53
|
+
|
|
54
|
+
lines = [f"Dependencies for: {file_path}", ""]
|
|
55
|
+
|
|
56
|
+
symbols = [n for n in all_nodes if n.parent_id == node_id]
|
|
57
|
+
if symbols:
|
|
58
|
+
class_names = [n.name for n in symbols if n.type == "class"]
|
|
59
|
+
func_names = [n.name for n in symbols if n.type == "function"]
|
|
60
|
+
if class_names:
|
|
61
|
+
lines.append(f" Classes: {', '.join(class_names)}")
|
|
62
|
+
if func_names:
|
|
63
|
+
lines.append(f" Functions: {', '.join(func_names)}")
|
|
64
|
+
lines.append("")
|
|
65
|
+
|
|
66
|
+
if imports:
|
|
67
|
+
lines.append(" Imports:")
|
|
68
|
+
for imp in sorted(imports):
|
|
69
|
+
lines.append(f" -> {imp}")
|
|
70
|
+
else:
|
|
71
|
+
lines.append(" Imports: (none)")
|
|
72
|
+
|
|
73
|
+
if imported_by:
|
|
74
|
+
lines.append("")
|
|
75
|
+
lines.append(" Imported by:")
|
|
76
|
+
for imp in sorted(imported_by):
|
|
77
|
+
lines.append(f" <- {imp}")
|
|
78
|
+
|
|
79
|
+
calls_made = []
|
|
80
|
+
called_by = []
|
|
81
|
+
for e in all_edges:
|
|
82
|
+
if e.source_id == node_id and e.relation == "calls":
|
|
83
|
+
target = storage.get_node(e.target_id)
|
|
84
|
+
if target:
|
|
85
|
+
calls_made.append(f"{target.name} ({target.path})")
|
|
86
|
+
if e.target_id == node_id and e.relation == "calls":
|
|
87
|
+
source = storage.get_node(e.source_id)
|
|
88
|
+
if source:
|
|
89
|
+
called_by.append(f"{source.name} ({source.path})")
|
|
90
|
+
|
|
91
|
+
if calls_made:
|
|
92
|
+
lines.append("")
|
|
93
|
+
lines.append(" Calls:")
|
|
94
|
+
for c in sorted(calls_made):
|
|
95
|
+
lines.append(f" -> {c}")
|
|
96
|
+
|
|
97
|
+
if called_by:
|
|
98
|
+
lines.append("")
|
|
99
|
+
lines.append(" Called by:")
|
|
100
|
+
for c in sorted(called_by):
|
|
101
|
+
lines.append(f" <- {c}")
|
|
102
|
+
|
|
103
|
+
return "\n".join(lines)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def render_usedby(storage: Storage, file_path: str) -> str:
|
|
107
|
+
node_id = f"file:{file_path}"
|
|
108
|
+
node = storage.get_node(node_id)
|
|
109
|
+
if not node:
|
|
110
|
+
return f"File not found in graph: {file_path}"
|
|
111
|
+
|
|
112
|
+
all_edges = storage.get_all_edges()
|
|
113
|
+
|
|
114
|
+
imported_by = []
|
|
115
|
+
called_by = []
|
|
116
|
+
for e in all_edges:
|
|
117
|
+
if e.target_id == node_id and e.relation == "imports":
|
|
118
|
+
source = storage.get_node(e.source_id)
|
|
119
|
+
if source:
|
|
120
|
+
imported_by.append(source.path or source.name)
|
|
121
|
+
if e.target_id == node_id and e.relation == "calls":
|
|
122
|
+
source = storage.get_node(e.source_id)
|
|
123
|
+
if source:
|
|
124
|
+
called_by.append(f"{source.name} ({source.path})")
|
|
125
|
+
|
|
126
|
+
lines = [f"References to: {file_path}", ""]
|
|
127
|
+
if imported_by:
|
|
128
|
+
lines.append(f" Imported by ({len(imported_by)}):")
|
|
129
|
+
for ref in sorted(imported_by):
|
|
130
|
+
lines.append(f" {ref}")
|
|
131
|
+
else:
|
|
132
|
+
lines.append(" Imported by: (none)")
|
|
133
|
+
|
|
134
|
+
if called_by:
|
|
135
|
+
lines.append("")
|
|
136
|
+
lines.append(f" Called by ({len(called_by)}):")
|
|
137
|
+
for ref in sorted(called_by):
|
|
138
|
+
lines.append(f" {ref}")
|
|
139
|
+
|
|
140
|
+
return "\n".join(lines)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def render_symbols(storage: Storage, file_path: str) -> str:
|
|
144
|
+
node_id = f"file:{file_path}"
|
|
145
|
+
node = storage.get_node(node_id)
|
|
146
|
+
if not node:
|
|
147
|
+
return f"File not found in graph: {file_path}"
|
|
148
|
+
|
|
149
|
+
all_nodes = storage.get_all_nodes()
|
|
150
|
+
symbols = [n for n in all_nodes if n.parent_id == node_id]
|
|
151
|
+
|
|
152
|
+
if not symbols:
|
|
153
|
+
return f"No symbols found in: {file_path}"
|
|
154
|
+
|
|
155
|
+
lines = [f"Symbols in: {file_path}", ""]
|
|
156
|
+
for s in symbols:
|
|
157
|
+
tag = "[C]" if s.type == "class" else "[M]"
|
|
158
|
+
summary = f" - {s.summary}" if s.summary else ""
|
|
159
|
+
lines.append(f" {tag} {s.name} (line {s.lineno}){summary}")
|
|
160
|
+
if s.type == "class":
|
|
161
|
+
methods = [n for n in all_nodes if n.parent_id == s.id]
|
|
162
|
+
if methods:
|
|
163
|
+
for m in methods:
|
|
164
|
+
ms = f" - {m.summary}" if m.summary else ""
|
|
165
|
+
lines.append(f" [M] {m.name} (line {m.lineno}){ms}")
|
|
166
|
+
|
|
167
|
+
return "\n".join(lines)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def render_context(storage: Storage, query: str, max_nodes: int = 15) -> str:
|
|
171
|
+
from ctxgraph_code.graph.query import search_relevant_nodes
|
|
172
|
+
|
|
173
|
+
ranked = search_relevant_nodes(storage, query, max_nodes)
|
|
174
|
+
if not ranked:
|
|
175
|
+
return f"No context found for: {query}"
|
|
176
|
+
|
|
177
|
+
all_nodes = storage.get_all_nodes()
|
|
178
|
+
all_edges = storage.get_all_edges()
|
|
179
|
+
|
|
180
|
+
node_ids = {n.id for n, _ in ranked}
|
|
181
|
+
relevant_edges = [
|
|
182
|
+
e for e in all_edges
|
|
183
|
+
if e.source_id in node_ids and e.target_id in node_ids
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
lines = [f"Context: {query}", ""]
|
|
187
|
+
|
|
188
|
+
file_nodes = [n for n, _ in ranked if n.type == "file"]
|
|
189
|
+
symbol_nodes = [n for n, _ in ranked if n.type != "file"]
|
|
190
|
+
|
|
191
|
+
for node in file_nodes:
|
|
192
|
+
lines.append(f" [F] {node.path or node.name}")
|
|
193
|
+
if node.summary:
|
|
194
|
+
lines.append(f" {node.summary}")
|
|
195
|
+
children = [
|
|
196
|
+
n for n in all_nodes
|
|
197
|
+
if n.parent_id == node.id and n.type in ("class", "function")
|
|
198
|
+
]
|
|
199
|
+
if children:
|
|
200
|
+
child_names = [c.name for c in children[:10]]
|
|
201
|
+
lines.append(f" Symbols: {', '.join(child_names)}")
|
|
202
|
+
|
|
203
|
+
if symbol_nodes:
|
|
204
|
+
lines.append("")
|
|
205
|
+
for node in symbol_nodes:
|
|
206
|
+
tag = "[C]" if node.type == "class" else "[M]"
|
|
207
|
+
name = node.name
|
|
208
|
+
if node.parent_id and "::" not in node.parent_id:
|
|
209
|
+
parent_short = node.parent_id.split(":")[-1] if ":" in node.parent_id else node.parent_id
|
|
210
|
+
name = f"{parent_short}.{node.name}"
|
|
211
|
+
lines.append(f" {tag} {name}")
|
|
212
|
+
if node.summary:
|
|
213
|
+
lines.append(f" {node.summary}")
|
|
214
|
+
|
|
215
|
+
import_edges = [(s, t) for s, t, r in [(e.source_id, e.target_id, e.relation) for e in relevant_edges] if r == "imports"]
|
|
216
|
+
call_edges = [(s, t) for s, t, r in [(e.source_id, e.target_id, e.relation) for e in relevant_edges] if r == "calls"]
|
|
217
|
+
|
|
218
|
+
if import_edges or call_edges:
|
|
219
|
+
lines.append("")
|
|
220
|
+
if import_edges:
|
|
221
|
+
lines.append(" Dependencies:")
|
|
222
|
+
for src, tgt in import_edges[:10]:
|
|
223
|
+
src_name = _short_name(src, {n.id: n for n in all_nodes})
|
|
224
|
+
tgt_name = _short_name(tgt, {n.id: n for n in all_nodes})
|
|
225
|
+
if src_name and tgt_name:
|
|
226
|
+
lines.append(f" {src_name} -> {tgt_name}")
|
|
227
|
+
if call_edges:
|
|
228
|
+
lines.append(" Calls:")
|
|
229
|
+
for src, tgt in call_edges[:10]:
|
|
230
|
+
src_name = _short_name(src, {n.id: n for n in all_nodes})
|
|
231
|
+
tgt_name = _short_name(tgt, {n.id: n for n in all_nodes})
|
|
232
|
+
if src_name and tgt_name:
|
|
233
|
+
lines.append(f" {src_name} -> {tgt_name}")
|
|
234
|
+
|
|
235
|
+
return "\n".join(lines)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _short_name(node_id: str, node_map: dict[str, Node]) -> Optional[str]:
|
|
239
|
+
if node_id in node_map:
|
|
240
|
+
n = node_map[node_id]
|
|
241
|
+
if n.type == "file":
|
|
242
|
+
return n.path or n.name
|
|
243
|
+
return f"{n.path}:{n.name}" if n.path else n.name
|
|
244
|
+
return node_id.split(":")[-1] if ":" in node_id else node_id
|