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.
@@ -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
+ ```