ontosight-codegraph 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.
- ontosight_codegraph/__init__.py +38 -0
- ontosight_codegraph/cli.py +96 -0
- ontosight_codegraph/query.py +59 -0
- ontosight_codegraph/store.py +589 -0
- ontosight_codegraph/topology.py +262 -0
- ontosight_codegraph/topology_display.py +102 -0
- ontosight_codegraph/view.py +267 -0
- ontosight_codegraph-0.1.0.dist-info/METADATA +71 -0
- ontosight_codegraph-0.1.0.dist-info/RECORD +11 -0
- ontosight_codegraph-0.1.0.dist-info/WHEEL +4 -0
- ontosight_codegraph-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""Graph topology analysis — rank nodes by connectivity and structural importance."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict, dataclass
|
|
6
|
+
from typing import Any, Callable, List, Optional, Sequence, Tuple, TypeVar
|
|
7
|
+
|
|
8
|
+
NodeSchema = TypeVar("NodeSchema")
|
|
9
|
+
EdgeSchema = TypeVar("EdgeSchema")
|
|
10
|
+
|
|
11
|
+
TIER_CRITICAL = "critical"
|
|
12
|
+
TIER_HIGH = "high"
|
|
13
|
+
TIER_MEDIUM = "medium"
|
|
14
|
+
TIER_LOW = "low"
|
|
15
|
+
|
|
16
|
+
VALID_METRICS = frozenset({"degree", "betweenness", "composite"})
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _ensure_networkx():
|
|
20
|
+
"""Lazy import networkx; optional for betweenness and articulation."""
|
|
21
|
+
try:
|
|
22
|
+
import networkx as nx
|
|
23
|
+
|
|
24
|
+
return nx
|
|
25
|
+
except ImportError:
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _normalize(values: dict[str, float]) -> dict[str, float]:
|
|
30
|
+
if not values:
|
|
31
|
+
return {}
|
|
32
|
+
lo = min(values.values())
|
|
33
|
+
hi = max(values.values())
|
|
34
|
+
if hi == lo:
|
|
35
|
+
return {k: 1.0 if hi > 0 else 0.0 for k in values}
|
|
36
|
+
return {k: (v - lo) / (hi - lo) for k, v in values.items()}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _assign_tiers(
|
|
40
|
+
node_ids: list[str],
|
|
41
|
+
importance: dict[str, float],
|
|
42
|
+
articulation: set[str],
|
|
43
|
+
) -> dict[str, str]:
|
|
44
|
+
if not node_ids:
|
|
45
|
+
return {}
|
|
46
|
+
|
|
47
|
+
sorted_ids = sorted(node_ids, key=lambda nid: importance.get(nid, 0.0), reverse=True)
|
|
48
|
+
n = len(sorted_ids)
|
|
49
|
+
critical_cutoff = max(1, int(n * 0.1))
|
|
50
|
+
high_cutoff = max(critical_cutoff + 1, int(n * 0.25))
|
|
51
|
+
|
|
52
|
+
tiers: dict[str, str] = {}
|
|
53
|
+
for i, nid in enumerate(sorted_ids):
|
|
54
|
+
if nid in articulation or i < critical_cutoff:
|
|
55
|
+
tiers[nid] = TIER_CRITICAL
|
|
56
|
+
elif i < high_cutoff:
|
|
57
|
+
tiers[nid] = TIER_HIGH
|
|
58
|
+
elif i < max(high_cutoff + 1, int(n * 0.6)):
|
|
59
|
+
tiers[nid] = TIER_MEDIUM
|
|
60
|
+
else:
|
|
61
|
+
tiers[nid] = TIER_LOW
|
|
62
|
+
return tiers
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class NodeRanking:
|
|
67
|
+
"""Ranked node with topology metrics."""
|
|
68
|
+
|
|
69
|
+
node_id: str
|
|
70
|
+
label: str
|
|
71
|
+
degree: int
|
|
72
|
+
betweenness: float
|
|
73
|
+
is_articulation: bool
|
|
74
|
+
importance: float
|
|
75
|
+
tier: str
|
|
76
|
+
|
|
77
|
+
def model_dump(self) -> dict[str, Any]:
|
|
78
|
+
return asdict(self)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _build_undirected_graph(
|
|
82
|
+
node_ids: set[str],
|
|
83
|
+
edges: list[tuple[str, str]],
|
|
84
|
+
) -> Any:
|
|
85
|
+
nx = _ensure_networkx()
|
|
86
|
+
if nx is None:
|
|
87
|
+
return None
|
|
88
|
+
g = nx.Graph()
|
|
89
|
+
g.add_nodes_from(node_ids)
|
|
90
|
+
for a, b in edges:
|
|
91
|
+
if a in node_ids and b in node_ids and a != b:
|
|
92
|
+
g.add_edge(a, b)
|
|
93
|
+
return g
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _compute_degrees(
|
|
97
|
+
node_ids: set[str],
|
|
98
|
+
edges: list[tuple[str, str]],
|
|
99
|
+
) -> dict[str, int]:
|
|
100
|
+
degree: dict[str, int] = {nid: 0 for nid in node_ids}
|
|
101
|
+
for a, b in edges:
|
|
102
|
+
if a not in node_ids or b not in node_ids:
|
|
103
|
+
continue
|
|
104
|
+
if a == b:
|
|
105
|
+
continue
|
|
106
|
+
degree[a] += 1
|
|
107
|
+
degree[b] += 1
|
|
108
|
+
return degree
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _rank_nodes(
|
|
112
|
+
node_list: Sequence[NodeSchema],
|
|
113
|
+
edge_pairs: list[tuple[str, str]],
|
|
114
|
+
*,
|
|
115
|
+
node_id_extractor: Callable[[NodeSchema], str],
|
|
116
|
+
node_label_extractor: Optional[Callable[[NodeSchema], str]],
|
|
117
|
+
top_k: int = 10,
|
|
118
|
+
metric: str = "composite",
|
|
119
|
+
include_betweenness: bool = True,
|
|
120
|
+
) -> list[NodeRanking]:
|
|
121
|
+
if metric not in VALID_METRICS:
|
|
122
|
+
raise ValueError(f"metric must be one of {sorted(VALID_METRICS)}, got {metric!r}")
|
|
123
|
+
|
|
124
|
+
label_fn = node_label_extractor or (lambda n: str(node_id_extractor(n)))
|
|
125
|
+
|
|
126
|
+
node_ids: set[str] = set()
|
|
127
|
+
labels: dict[str, str] = {}
|
|
128
|
+
for node in node_list:
|
|
129
|
+
nid = str(node_id_extractor(node))
|
|
130
|
+
node_ids.add(nid)
|
|
131
|
+
labels[nid] = str(label_fn(node))
|
|
132
|
+
|
|
133
|
+
if not node_ids:
|
|
134
|
+
return []
|
|
135
|
+
|
|
136
|
+
degree = _compute_degrees(node_ids, edge_pairs)
|
|
137
|
+
nx_graph = _build_undirected_graph(node_ids, edge_pairs) if include_betweenness else None
|
|
138
|
+
|
|
139
|
+
betweenness: dict[str, float] = {nid: 0.0 for nid in node_ids}
|
|
140
|
+
articulation: set[str] = set()
|
|
141
|
+
|
|
142
|
+
if nx_graph is not None and nx_graph.number_of_edges() > 0:
|
|
143
|
+
nx = _ensure_networkx()
|
|
144
|
+
assert nx is not None
|
|
145
|
+
raw_betweenness = nx.betweenness_centrality(nx_graph, normalized=True)
|
|
146
|
+
betweenness = {nid: float(raw_betweenness.get(nid, 0.0)) for nid in node_ids}
|
|
147
|
+
articulation = set(nx.articulation_points(nx_graph))
|
|
148
|
+
|
|
149
|
+
norm_degree = _normalize({nid: float(degree[nid]) for nid in node_ids})
|
|
150
|
+
norm_betweenness = _normalize(betweenness)
|
|
151
|
+
|
|
152
|
+
importance: dict[str, float] = {}
|
|
153
|
+
for nid in node_ids:
|
|
154
|
+
if metric == "degree":
|
|
155
|
+
importance[nid] = norm_degree[nid]
|
|
156
|
+
elif metric == "betweenness":
|
|
157
|
+
importance[nid] = norm_betweenness[nid]
|
|
158
|
+
else:
|
|
159
|
+
if nx_graph is not None and nx_graph.number_of_edges() > 0:
|
|
160
|
+
importance[nid] = 0.6 * norm_degree[nid] + 0.4 * norm_betweenness[nid]
|
|
161
|
+
else:
|
|
162
|
+
importance[nid] = norm_degree[nid]
|
|
163
|
+
|
|
164
|
+
tiers = _assign_tiers(list(node_ids), importance, articulation)
|
|
165
|
+
|
|
166
|
+
rankings = [
|
|
167
|
+
NodeRanking(
|
|
168
|
+
node_id=nid,
|
|
169
|
+
label=labels[nid],
|
|
170
|
+
degree=degree[nid],
|
|
171
|
+
betweenness=round(betweenness[nid], 6),
|
|
172
|
+
is_articulation=nid in articulation,
|
|
173
|
+
importance=round(importance[nid], 6),
|
|
174
|
+
tier=tiers[nid],
|
|
175
|
+
)
|
|
176
|
+
for nid in node_ids
|
|
177
|
+
]
|
|
178
|
+
rankings.sort(
|
|
179
|
+
key=lambda r: (r.importance, r.degree, r.betweenness, r.label),
|
|
180
|
+
reverse=True,
|
|
181
|
+
)
|
|
182
|
+
if top_k > 0:
|
|
183
|
+
rankings = rankings[:top_k]
|
|
184
|
+
return rankings
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def rank_graph_nodes(
|
|
188
|
+
node_list: Sequence[NodeSchema],
|
|
189
|
+
edge_list: Sequence[EdgeSchema],
|
|
190
|
+
*,
|
|
191
|
+
node_id_extractor: Callable[[NodeSchema], str],
|
|
192
|
+
node_ids_in_edge_extractor: Callable[[EdgeSchema], Tuple[str, str]],
|
|
193
|
+
node_label_extractor: Optional[Callable[[NodeSchema], str]] = None,
|
|
194
|
+
top_k: int = 10,
|
|
195
|
+
metric: str = "composite",
|
|
196
|
+
include_betweenness: bool = True,
|
|
197
|
+
) -> list[NodeRanking]:
|
|
198
|
+
"""Rank nodes in a pairwise graph by topology metrics."""
|
|
199
|
+
edge_pairs: list[tuple[str, str]] = []
|
|
200
|
+
for edge in edge_list:
|
|
201
|
+
source_id, target_id = node_ids_in_edge_extractor(edge)
|
|
202
|
+
edge_pairs.append((str(source_id), str(target_id)))
|
|
203
|
+
|
|
204
|
+
return _rank_nodes(
|
|
205
|
+
node_list,
|
|
206
|
+
edge_pairs,
|
|
207
|
+
node_id_extractor=node_id_extractor,
|
|
208
|
+
node_label_extractor=node_label_extractor,
|
|
209
|
+
top_k=top_k,
|
|
210
|
+
metric=metric,
|
|
211
|
+
include_betweenness=include_betweenness,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def rank_hypergraph_nodes(
|
|
216
|
+
node_list: Sequence[NodeSchema],
|
|
217
|
+
edge_list: Sequence[EdgeSchema],
|
|
218
|
+
*,
|
|
219
|
+
node_id_extractor: Callable[[NodeSchema], str],
|
|
220
|
+
nodes_in_edge_extractor: Callable[[EdgeSchema], Sequence[str]],
|
|
221
|
+
node_label_extractor: Optional[Callable[[NodeSchema], str]] = None,
|
|
222
|
+
top_k: int = 10,
|
|
223
|
+
metric: str = "composite",
|
|
224
|
+
include_betweenness: bool = True,
|
|
225
|
+
) -> list[NodeRanking]:
|
|
226
|
+
"""Rank nodes in a hypergraph by hyperedge participation (clique-expanded for betweenness)."""
|
|
227
|
+
edge_pairs: list[tuple[str, str]] = []
|
|
228
|
+
for edge in edge_list:
|
|
229
|
+
members = [str(m) for m in nodes_in_edge_extractor(edge)]
|
|
230
|
+
unique_members = list(dict.fromkeys(members))
|
|
231
|
+
for i, a in enumerate(unique_members):
|
|
232
|
+
for b in unique_members[i + 1 :]:
|
|
233
|
+
edge_pairs.append((a, b))
|
|
234
|
+
|
|
235
|
+
return _rank_nodes(
|
|
236
|
+
node_list,
|
|
237
|
+
edge_pairs,
|
|
238
|
+
node_id_extractor=node_id_extractor,
|
|
239
|
+
node_label_extractor=node_label_extractor,
|
|
240
|
+
top_k=top_k,
|
|
241
|
+
metric=metric,
|
|
242
|
+
include_betweenness=include_betweenness,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def topology_summary(rankings: list[NodeRanking], total_nodes: int) -> dict[str, Any]:
|
|
247
|
+
"""Aggregate KPIs for CLI / MCP display."""
|
|
248
|
+
if total_nodes == 0:
|
|
249
|
+
return {
|
|
250
|
+
"total_nodes": 0,
|
|
251
|
+
"avg_degree": 0.0,
|
|
252
|
+
"articulation_count": 0,
|
|
253
|
+
"critical_count": 0,
|
|
254
|
+
}
|
|
255
|
+
all_degrees = [r.degree for r in rankings]
|
|
256
|
+
avg_degree = sum(all_degrees) / total_nodes if total_nodes else 0.0
|
|
257
|
+
return {
|
|
258
|
+
"total_nodes": total_nodes,
|
|
259
|
+
"avg_degree": round(avg_degree, 2),
|
|
260
|
+
"articulation_count": sum(1 for r in rankings if r.is_articulation),
|
|
261
|
+
"critical_count": sum(1 for r in rankings if r.tier == TIER_CRITICAL),
|
|
262
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Rich terminal display for graph topology rankings."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
_TIER_STYLES = {
|
|
11
|
+
"critical": "bold #F59E0B",
|
|
12
|
+
"high": "#1E40AF",
|
|
13
|
+
"medium": "dim",
|
|
14
|
+
"low": "dim italic",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _summary_from_rows(rankings: list[dict[str, Any]], total_nodes: int) -> dict[str, Any]:
|
|
19
|
+
if total_nodes == 0:
|
|
20
|
+
return {
|
|
21
|
+
"total_nodes": 0,
|
|
22
|
+
"avg_degree": 0.0,
|
|
23
|
+
"articulation_count": 0,
|
|
24
|
+
"critical_count": 0,
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
"total_nodes": total_nodes,
|
|
28
|
+
"avg_degree": round(
|
|
29
|
+
sum(r.get("degree", 0) for r in rankings) / total_nodes,
|
|
30
|
+
2,
|
|
31
|
+
),
|
|
32
|
+
"articulation_count": sum(1 for r in rankings if r.get("is_articulation")),
|
|
33
|
+
"critical_count": sum(1 for r in rankings if r.get("tier") == "critical"),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def print_topology_table(
|
|
38
|
+
rankings: list[dict[str, Any]],
|
|
39
|
+
*,
|
|
40
|
+
total_nodes: int,
|
|
41
|
+
metric: str = "composite",
|
|
42
|
+
console: Optional[Console] = None,
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Print a data-dense critical-nodes table to the terminal."""
|
|
45
|
+
if not rankings:
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
out = console or Console()
|
|
49
|
+
summary = _summary_from_rows(rankings, total_nodes)
|
|
50
|
+
|
|
51
|
+
out.print(
|
|
52
|
+
f"[dim]Topology[/dim] nodes={summary['total_nodes']} "
|
|
53
|
+
f"avg_degree={summary['avg_degree']} "
|
|
54
|
+
f"articulation={summary['articulation_count']} "
|
|
55
|
+
f"critical={summary['critical_count']} "
|
|
56
|
+
f"metric={metric}"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
table = Table(title="Critical / Hub Nodes", show_header=True, header_style="bold")
|
|
60
|
+
table.add_column("#", justify="right", style="dim", width=4)
|
|
61
|
+
table.add_column("Node", style="cyan", max_width=40, overflow="ellipsis")
|
|
62
|
+
table.add_column("Degree", justify="right")
|
|
63
|
+
table.add_column("Between.", justify="right")
|
|
64
|
+
table.add_column("Importance", justify="right")
|
|
65
|
+
table.add_column("Tier")
|
|
66
|
+
|
|
67
|
+
for i, row in enumerate(rankings, start=1):
|
|
68
|
+
tier = row.get("tier", "low")
|
|
69
|
+
style = _TIER_STYLES.get(tier, "")
|
|
70
|
+
articulation = " [dim]cut[/dim]" if row.get("is_articulation") else ""
|
|
71
|
+
table.add_row(
|
|
72
|
+
str(i),
|
|
73
|
+
str(row.get("label", row.get("node_id", ""))) + articulation,
|
|
74
|
+
str(row.get("degree", 0)),
|
|
75
|
+
f"{row.get('betweenness', 0):.3f}",
|
|
76
|
+
f"{row.get('importance', 0):.3f}",
|
|
77
|
+
f"[{style}]{tier}[/{style}]" if style else tier,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
out.print(table)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def rankings_to_json_payload(
|
|
84
|
+
rankings: list[dict[str, Any]],
|
|
85
|
+
*,
|
|
86
|
+
total_nodes: int,
|
|
87
|
+
metric: str = "composite",
|
|
88
|
+
all_rankings: Optional[list[dict[str, Any]]] = None,
|
|
89
|
+
) -> dict[str, Any]:
|
|
90
|
+
"""Serialize rankings for ``he analyze --json`` and MCP."""
|
|
91
|
+
full = all_rankings if all_rankings is not None else rankings
|
|
92
|
+
return {
|
|
93
|
+
"metric": metric,
|
|
94
|
+
"total_nodes": total_nodes,
|
|
95
|
+
"summary": _summary_from_rows(full, total_nodes),
|
|
96
|
+
"rankings": rankings,
|
|
97
|
+
"critical_node_ids": [
|
|
98
|
+
r["node_id"]
|
|
99
|
+
for r in full
|
|
100
|
+
if r.get("tier") in ("critical", "high")
|
|
101
|
+
],
|
|
102
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Launch OntoSight visualization for a CodeGraph call subgraph."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from ontosight import view_graph
|
|
10
|
+
from ontosight.core.storage import GraphStorage
|
|
11
|
+
from ontosight.server.state import global_state
|
|
12
|
+
|
|
13
|
+
from ontosight_codegraph.store import (
|
|
14
|
+
CodeCallEdge,
|
|
15
|
+
CodeSymbolNode,
|
|
16
|
+
SubgraphResult,
|
|
17
|
+
load_call_subgraph,
|
|
18
|
+
make_search_callback,
|
|
19
|
+
)
|
|
20
|
+
from ontosight_codegraph.topology import (
|
|
21
|
+
TIER_CRITICAL,
|
|
22
|
+
TIER_HIGH,
|
|
23
|
+
rank_graph_nodes,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _node_label(node: CodeSymbolNode) -> str:
|
|
30
|
+
return f"{node.name} ({node.kind})"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _topology_view_context(
|
|
34
|
+
nodes: List[CodeSymbolNode],
|
|
35
|
+
edges: List[CodeCallEdge],
|
|
36
|
+
*,
|
|
37
|
+
top_k: int = 10,
|
|
38
|
+
highlight_critical: bool = True,
|
|
39
|
+
) -> dict:
|
|
40
|
+
all_rankings = rank_graph_nodes(
|
|
41
|
+
nodes,
|
|
42
|
+
edges,
|
|
43
|
+
node_id_extractor=lambda n: n.id,
|
|
44
|
+
node_ids_in_edge_extractor=lambda e: (e.source_id, e.target_id),
|
|
45
|
+
node_label_extractor=_node_label,
|
|
46
|
+
top_k=0,
|
|
47
|
+
metric="composite",
|
|
48
|
+
)
|
|
49
|
+
display_rankings = all_rankings[:top_k] if top_k > 0 else all_rankings
|
|
50
|
+
critical_ids = [
|
|
51
|
+
r.node_id for r in all_rankings if r.tier in (TIER_CRITICAL, TIER_HIGH)
|
|
52
|
+
]
|
|
53
|
+
if not highlight_critical:
|
|
54
|
+
critical_ids = []
|
|
55
|
+
return {
|
|
56
|
+
"node_rankings": [r.model_dump() for r in display_rankings],
|
|
57
|
+
"critical_node_ids": critical_ids,
|
|
58
|
+
"topology_metric": "composite",
|
|
59
|
+
"topology_total_nodes": len(nodes),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _build_view_payload(
|
|
64
|
+
graph_export: Dict[str, Any],
|
|
65
|
+
*,
|
|
66
|
+
rankings: List[dict],
|
|
67
|
+
meta: Dict[str, Any],
|
|
68
|
+
truncated: bool,
|
|
69
|
+
filter_summary: str,
|
|
70
|
+
languages: List[str],
|
|
71
|
+
) -> Dict[str, Any]:
|
|
72
|
+
return {
|
|
73
|
+
"nodes": graph_export.get("nodes", []),
|
|
74
|
+
"edges": graph_export.get("edges", []),
|
|
75
|
+
"rankings": rankings,
|
|
76
|
+
"meta": meta,
|
|
77
|
+
"truncated": truncated,
|
|
78
|
+
"filter_summary": filter_summary,
|
|
79
|
+
"languages": languages,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def apply_subgraph_to_view(
|
|
84
|
+
result: SubgraphResult,
|
|
85
|
+
*,
|
|
86
|
+
max_nodes: int,
|
|
87
|
+
top_k_critical: int = 10,
|
|
88
|
+
highlight_critical: bool = True,
|
|
89
|
+
) -> Dict[str, Any]:
|
|
90
|
+
"""Rebuild GraphStorage and visualization metadata from a subgraph result."""
|
|
91
|
+
if not result.nodes:
|
|
92
|
+
raise ValueError(
|
|
93
|
+
"No symbols found for the given filters. "
|
|
94
|
+
"Try a broader path, symbol, or task."
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
topology_context = _topology_view_context(
|
|
98
|
+
result.nodes,
|
|
99
|
+
result.edges,
|
|
100
|
+
top_k=top_k_critical,
|
|
101
|
+
highlight_critical=highlight_critical,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
languages = ", ".join(result.languages) if result.languages else "unknown"
|
|
105
|
+
meta_stats = {
|
|
106
|
+
"Nodes": len(result.nodes),
|
|
107
|
+
"Edges": len(result.edges),
|
|
108
|
+
"Source": "CodeGraph",
|
|
109
|
+
"Filter": result.filter_summary,
|
|
110
|
+
"Languages": languages,
|
|
111
|
+
}
|
|
112
|
+
if result.truncated:
|
|
113
|
+
meta_stats["Note"] = f"Truncated at {max_nodes} nodes"
|
|
114
|
+
|
|
115
|
+
storage = GraphStorage(
|
|
116
|
+
node_list=result.nodes,
|
|
117
|
+
edge_list=result.edges,
|
|
118
|
+
node_id_extractor=lambda n: n.id,
|
|
119
|
+
node_ids_in_edge_extractor=lambda e: (e.source_id, e.target_id),
|
|
120
|
+
edge_label_extractor=lambda _: "calls",
|
|
121
|
+
node_label_extractor=_node_label,
|
|
122
|
+
node_rankings=topology_context.get("node_rankings"),
|
|
123
|
+
critical_node_ids=topology_context.get("critical_node_ids"),
|
|
124
|
+
)
|
|
125
|
+
global_state.set_storage(storage)
|
|
126
|
+
|
|
127
|
+
stats = storage.get_stats()
|
|
128
|
+
meta_data = {
|
|
129
|
+
"Nodes": stats["total_nodes"],
|
|
130
|
+
"Edges": stats["total_edges"],
|
|
131
|
+
"Average Node Degree": stats["avg_degree"],
|
|
132
|
+
"Average Edge Degree": 2,
|
|
133
|
+
**meta_stats,
|
|
134
|
+
}
|
|
135
|
+
global_state.set_visualization_data("meta_data", meta_data)
|
|
136
|
+
global_state.set_visualization_data("source", "codegraph")
|
|
137
|
+
global_state.set_visualization_data("filter_summary", result.filter_summary)
|
|
138
|
+
global_state.set_visualization_data("codegraph_languages", result.languages)
|
|
139
|
+
global_state.set_visualization_data("codegraph_meta_stats", meta_stats)
|
|
140
|
+
global_state.set_visualization_data(
|
|
141
|
+
"node_rankings", topology_context.get("node_rankings", [])
|
|
142
|
+
)
|
|
143
|
+
global_state.set_visualization_data(
|
|
144
|
+
"critical_node_ids", topology_context.get("critical_node_ids", [])
|
|
145
|
+
)
|
|
146
|
+
global_state.set_visualization_data(
|
|
147
|
+
"topology_metric", topology_context.get("topology_metric", "composite")
|
|
148
|
+
)
|
|
149
|
+
global_state.set_visualization_data(
|
|
150
|
+
"topology_total_nodes",
|
|
151
|
+
topology_context.get("topology_total_nodes", len(result.nodes)),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
graph_export = storage.export_full_graph()
|
|
155
|
+
return _build_view_payload(
|
|
156
|
+
graph_export,
|
|
157
|
+
rankings=topology_context.get("node_rankings", []),
|
|
158
|
+
meta=meta_data,
|
|
159
|
+
truncated=result.truncated,
|
|
160
|
+
filter_summary=result.filter_summary,
|
|
161
|
+
languages=result.languages,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def show_codegraph(
|
|
166
|
+
project_path: Path,
|
|
167
|
+
*,
|
|
168
|
+
path_filter: Optional[str] = None,
|
|
169
|
+
symbol: Optional[str] = None,
|
|
170
|
+
task: Optional[str] = None,
|
|
171
|
+
hops: int = 2,
|
|
172
|
+
max_nodes: int = 200,
|
|
173
|
+
top_k_critical: int = 10,
|
|
174
|
+
highlight_critical: bool = True,
|
|
175
|
+
print_topology_summary: bool = True,
|
|
176
|
+
) -> SubgraphResult:
|
|
177
|
+
"""Visualize a CodeGraph call subgraph in OntoSight."""
|
|
178
|
+
result = load_call_subgraph(
|
|
179
|
+
project_path,
|
|
180
|
+
path_filter=path_filter,
|
|
181
|
+
symbol=symbol,
|
|
182
|
+
task=task,
|
|
183
|
+
hops=hops,
|
|
184
|
+
max_nodes=max_nodes,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if not result.nodes:
|
|
188
|
+
raise ValueError(
|
|
189
|
+
"No symbols found for the given filters. "
|
|
190
|
+
"Try a broader --path, --symbol, or --task."
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
topology_context = _topology_view_context(
|
|
194
|
+
result.nodes,
|
|
195
|
+
result.edges,
|
|
196
|
+
top_k=top_k_critical,
|
|
197
|
+
highlight_critical=highlight_critical,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
if print_topology_summary and topology_context.get("node_rankings"):
|
|
201
|
+
from ontosight_codegraph.topology_display import print_topology_table
|
|
202
|
+
|
|
203
|
+
print_topology_table(
|
|
204
|
+
topology_context["node_rankings"],
|
|
205
|
+
total_nodes=topology_context.get("topology_total_nodes", len(result.nodes)),
|
|
206
|
+
metric=topology_context.get("topology_metric", "composite"),
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
languages = ", ".join(result.languages) if result.languages else "unknown"
|
|
210
|
+
meta_stats = {
|
|
211
|
+
"Nodes": len(result.nodes),
|
|
212
|
+
"Edges": len(result.edges),
|
|
213
|
+
"Source": "CodeGraph",
|
|
214
|
+
"Filter": result.filter_summary,
|
|
215
|
+
"Languages": languages,
|
|
216
|
+
}
|
|
217
|
+
if result.truncated:
|
|
218
|
+
meta_stats["Note"] = f"Truncated at {max_nodes} nodes"
|
|
219
|
+
|
|
220
|
+
context = {
|
|
221
|
+
**topology_context,
|
|
222
|
+
"source": "codegraph",
|
|
223
|
+
"filter_summary": result.filter_summary,
|
|
224
|
+
"codegraph_languages": result.languages,
|
|
225
|
+
"codegraph_meta_stats": meta_stats,
|
|
226
|
+
"codegraph_truncated": result.truncated,
|
|
227
|
+
"codegraph_project_path": str(project_path.resolve()),
|
|
228
|
+
"codegraph_default_hops": hops,
|
|
229
|
+
"codegraph_default_max_nodes": max_nodes,
|
|
230
|
+
"codegraph_default_path_filter": path_filter,
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
search_callback = make_search_callback(project_path, path_filter)
|
|
234
|
+
from ontosight_codegraph.query import make_query_callback
|
|
235
|
+
|
|
236
|
+
query_callback = make_query_callback(
|
|
237
|
+
project_path,
|
|
238
|
+
default_path_filter=path_filter,
|
|
239
|
+
default_hops=hops,
|
|
240
|
+
default_max_nodes=max_nodes,
|
|
241
|
+
top_k_critical=top_k_critical,
|
|
242
|
+
highlight_critical=highlight_critical,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
logger.info(
|
|
246
|
+
"Opening CodeGraph viewer: nodes=%d edges=%d filter=%s",
|
|
247
|
+
len(result.nodes),
|
|
248
|
+
len(result.edges),
|
|
249
|
+
result.filter_summary,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
view_graph(
|
|
253
|
+
node_list=result.nodes,
|
|
254
|
+
edge_list=result.edges,
|
|
255
|
+
node_schema=CodeSymbolNode,
|
|
256
|
+
edge_schema=CodeCallEdge,
|
|
257
|
+
node_id_extractor=lambda n: n.id,
|
|
258
|
+
node_ids_in_edge_extractor=lambda e: (e.source_id, e.target_id),
|
|
259
|
+
node_label_extractor=_node_label,
|
|
260
|
+
edge_label_extractor=lambda _: "calls",
|
|
261
|
+
on_search=search_callback,
|
|
262
|
+
on_chat=None,
|
|
263
|
+
context=context,
|
|
264
|
+
callbacks={"codegraph_query": query_callback},
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
return result
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ontosight-codegraph
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Visualize CodeGraph call subgraphs in OntoSight
|
|
5
|
+
Project-URL: Homepage, https://github.com/yifanfeng97/hyper-extract
|
|
6
|
+
Project-URL: Repository, https://github.com/yifanfeng97/hyper-extract
|
|
7
|
+
Author-email: Yifan Feng <evanfeng97@gmail.com>
|
|
8
|
+
License: Apache-2.0
|
|
9
|
+
Keywords: call-graph,codegraph,ontosight,visualization
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
Requires-Dist: networkx>=3.0
|
|
18
|
+
Requires-Dist: ontosight>=0.2.0
|
|
19
|
+
Requires-Dist: pydantic>=2.0
|
|
20
|
+
Requires-Dist: rich>=13.7.0
|
|
21
|
+
Requires-Dist: typer>=0.13.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=9.0.0; extra == 'dev'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# OntoSight CodeGraph
|
|
27
|
+
|
|
28
|
+
Read a local [CodeGraph](https://github.com/colbymchenry/codegraph) index (`.codegraph/codegraph.db`) and visualize call subgraphs in [OntoSight](https://pypi.org/project/ontosight/).
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install ontosight-codegraph
|
|
34
|
+
# or
|
|
35
|
+
uvx ontosight-codegraph .
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Ensure index exists
|
|
42
|
+
npx @colbymchenry/codegraph init -i
|
|
43
|
+
|
|
44
|
+
# Auto-seed from highest fan-in symbols
|
|
45
|
+
ontosight-codegraph .
|
|
46
|
+
|
|
47
|
+
# Seed around a symbol
|
|
48
|
+
ontosight-codegraph . --symbol view_graph --path vendor/ontosight/
|
|
49
|
+
|
|
50
|
+
# Task-scoped subgraph
|
|
51
|
+
ontosight-codegraph . --task "auth flow" --hops 2 --max-nodes 200
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## npm wrapper
|
|
55
|
+
|
|
56
|
+
For a zero-Python-install workflow (uses `uvx` under the hood; auto-runs CodeGraph init when the index is missing):
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npx @royalsolution/ontosight .
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
See [`packages/ontosight/`](../ontosight/) and [`packages/ontosight/AGENTS.md`](../ontosight/AGENTS.md) for npm package details and **AI agent usage**.
|
|
63
|
+
|
|
64
|
+
## Publish (maintainers)
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
cd packages/ontosight-codegraph
|
|
68
|
+
python -m build
|
|
69
|
+
twine check dist/*
|
|
70
|
+
twine upload dist/*
|
|
71
|
+
```
|