codegraph-network 0.2.0__tar.gz → 0.3.0__tar.gz
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_network-0.2.0 → codegraph_network-0.3.0}/PKG-INFO +27 -6
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/README.md +26 -5
- codegraph_network-0.3.0/src/codegraph/__init__.py +1 -0
- codegraph_network-0.3.0/src/codegraph/graphalgo.py +272 -0
- codegraph_network-0.3.0/src/codegraph/ui.html +531 -0
- codegraph_network-0.3.0/src/codegraph/ui.py +532 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/src/codegraph_network.egg-info/PKG-INFO +27 -6
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/src/codegraph_network.egg-info/SOURCES.txt +2 -0
- codegraph_network-0.3.0/tests/test_graphalgo.py +83 -0
- codegraph_network-0.3.0/tests/test_ui.py +104 -0
- codegraph_network-0.2.0/src/codegraph/__init__.py +0 -1
- codegraph_network-0.2.0/src/codegraph/ui.html +0 -262
- codegraph_network-0.2.0/src/codegraph/ui.py +0 -196
- codegraph_network-0.2.0/tests/test_ui.py +0 -60
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/LICENSE +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/pyproject.toml +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/setup.cfg +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/src/codegraph/bench.py +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/src/codegraph/cli.py +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/src/codegraph/community.py +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/src/codegraph/docs.py +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/src/codegraph/embed.py +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/src/codegraph/indexer.py +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/src/codegraph/install.py +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/src/codegraph/keys.py +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/src/codegraph/links.py +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/src/codegraph/parser.py +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/src/codegraph/py.typed +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/src/codegraph/queries.py +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/src/codegraph/rank.py +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/src/codegraph/report.py +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/src/codegraph/review.py +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/src/codegraph/server.py +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/src/codegraph/store.py +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/src/codegraph/tools.py +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/src/codegraph_network.egg-info/dependency_links.txt +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/src/codegraph_network.egg-info/entry_points.txt +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/src/codegraph_network.egg-info/requires.txt +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/src/codegraph_network.egg-info/top_level.txt +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/tests/test_edges.py +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/tests/test_imports.py +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/tests/test_incremental.py +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/tests/test_keys.py +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/tests/test_links.py +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/tests/test_mcp_e2e.py +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/tests/test_parity.py +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/tests/test_parser.py +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/tests/test_queries.py +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/tests/test_rank.py +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/tests/test_semantic.py +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/tests/test_server_cli.py +0 -0
- {codegraph_network-0.2.0 → codegraph_network-0.3.0}/tests/test_store.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codegraph-network
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Graph-based code understanding for LLM agents: fewer tokens, better answers
|
|
5
5
|
Author: Oviing
|
|
6
6
|
License-Expression: MIT
|
|
@@ -68,11 +68,32 @@ The index lives in `<repo>/.codegraph/graph.db` and updates incrementally on eve
|
|
|
68
68
|
codegraph -r /path/to/repo ui
|
|
69
69
|
|
|
70
70
|
Opens an interactive map of the repo in your browser (served from `127.0.0.1` only, nothing leaves your machine).
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
Everything is computed from the existing index with the standard library; no extra dependencies.
|
|
72
|
+
|
|
73
|
+
**Start from communities, zoom to code.** The first view is the community map: groups of symbols that are tightly
|
|
74
|
+
linked to each other, found with Louvain modularity clustering (the *clusters* slider merges or splits them). Double-click
|
|
75
|
+
a community to open it into files, a file into its symbols, a symbol to pull in what it calls and what calls it.
|
|
76
|
+
Drag nodes to rearrange and pin them; scroll to zoom. Click a community for cohesion, key symbols and its most
|
|
77
|
+
linked neighbours. The legend isolates a community with one click.
|
|
78
|
+
|
|
79
|
+
**Lenses** colour the graph by a graph-theory metric:
|
|
80
|
+
|
|
81
|
+
| Lens | Algorithm | Shows |
|
|
82
|
+
|---|---|---|
|
|
83
|
+
| importance | PageRank | what many others lean on |
|
|
84
|
+
| bottlenecks | betweenness centrality (sampled on big graphs) | symbols most paths run through |
|
|
85
|
+
| cycles | strongly connected components | circular dependencies between symbols and files |
|
|
86
|
+
| layers | condensation + longest path | callers on top, callees below |
|
|
87
|
+
| coupling / misplaced | cross-community link ratio | symbols that belong with another community |
|
|
88
|
+
| core vs periphery | k-core decomposition | the dense kernel vs leaf code |
|
|
89
|
+
| dead code | zero in-degree + no unresolved references | candidates nothing calls |
|
|
90
|
+
| hotspots | git co-change x importance | central code that also changes a lot |
|
|
91
|
+
|
|
92
|
+
**Layouts:** force, grouped by community, layered (callers on top), radial around the selection.
|
|
93
|
+
**Filters:** minimum link weight, certain-only links, hops from the selection, tests, kind, folder.
|
|
94
|
+
**Tools:** *Show impact* (transitive callers), *Path* between two symbols (shortest call chain), *Surprises*
|
|
95
|
+
(certain links between different communities and folders), *Save / Compare snapshot* (which symbols changed
|
|
96
|
+
community since you saved), and export to PNG, SVG or JSON. Press `/` to search.
|
|
76
97
|
|
|
77
98
|
## Tools
|
|
78
99
|
|
|
@@ -31,11 +31,32 @@ The index lives in `<repo>/.codegraph/graph.db` and updates incrementally on eve
|
|
|
31
31
|
codegraph -r /path/to/repo ui
|
|
32
32
|
|
|
33
33
|
Opens an interactive map of the repo in your browser (served from `127.0.0.1` only, nothing leaves your machine).
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
Everything is computed from the existing index with the standard library; no extra dependencies.
|
|
35
|
+
|
|
36
|
+
**Start from communities, zoom to code.** The first view is the community map: groups of symbols that are tightly
|
|
37
|
+
linked to each other, found with Louvain modularity clustering (the *clusters* slider merges or splits them). Double-click
|
|
38
|
+
a community to open it into files, a file into its symbols, a symbol to pull in what it calls and what calls it.
|
|
39
|
+
Drag nodes to rearrange and pin them; scroll to zoom. Click a community for cohesion, key symbols and its most
|
|
40
|
+
linked neighbours. The legend isolates a community with one click.
|
|
41
|
+
|
|
42
|
+
**Lenses** colour the graph by a graph-theory metric:
|
|
43
|
+
|
|
44
|
+
| Lens | Algorithm | Shows |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| importance | PageRank | what many others lean on |
|
|
47
|
+
| bottlenecks | betweenness centrality (sampled on big graphs) | symbols most paths run through |
|
|
48
|
+
| cycles | strongly connected components | circular dependencies between symbols and files |
|
|
49
|
+
| layers | condensation + longest path | callers on top, callees below |
|
|
50
|
+
| coupling / misplaced | cross-community link ratio | symbols that belong with another community |
|
|
51
|
+
| core vs periphery | k-core decomposition | the dense kernel vs leaf code |
|
|
52
|
+
| dead code | zero in-degree + no unresolved references | candidates nothing calls |
|
|
53
|
+
| hotspots | git co-change x importance | central code that also changes a lot |
|
|
54
|
+
|
|
55
|
+
**Layouts:** force, grouped by community, layered (callers on top), radial around the selection.
|
|
56
|
+
**Filters:** minimum link weight, certain-only links, hops from the selection, tests, kind, folder.
|
|
57
|
+
**Tools:** *Show impact* (transitive callers), *Path* between two symbols (shortest call chain), *Surprises*
|
|
58
|
+
(certain links between different communities and folders), *Save / Compare snapshot* (which symbols changed
|
|
59
|
+
community since you saved), and export to PNG, SVG or JSON. Press `/` to search.
|
|
39
60
|
|
|
40
61
|
## Tools
|
|
41
62
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""Graph-theory algorithms behind the UI lenses. Pure stdlib, deterministic, no I/O.
|
|
2
|
+
|
|
3
|
+
Nodes are any sortable hashables (symbol ids). Graphs are plain dicts so callers can build them from SQL
|
|
4
|
+
rows without an adapter: `edges` is a list of (a, b, weight) for the undirected algorithms and `out`
|
|
5
|
+
is {node: iterable of successors} for the directed ones.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import heapq
|
|
10
|
+
from collections import defaultdict, deque
|
|
11
|
+
|
|
12
|
+
EPS = 1e-12
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _renumber(label: dict, by_size: bool = True) -> dict:
|
|
16
|
+
"""Relabel communities 0..k-1, biggest first, so ids are stable and the largest is always 0."""
|
|
17
|
+
size: dict = defaultdict(int)
|
|
18
|
+
for lb in label.values():
|
|
19
|
+
size[lb] += 1
|
|
20
|
+
order = sorted(size, key=lambda lb: (-size[lb], lb) if by_size else (lb,))
|
|
21
|
+
ix = {lb: i for i, lb in enumerate(order)}
|
|
22
|
+
return {n: ix[lb] for n, lb in label.items()}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def louvain(nodes, edges, resolution: float = 1.0, max_levels: int = 10) -> list[dict]:
|
|
26
|
+
"""Louvain modularity optimisation (Blondel et al. 2008) with a resolution parameter.
|
|
27
|
+
|
|
28
|
+
Returns one partition per level, finest first: [{node: community}, ...]. Every level maps the
|
|
29
|
+
*original* nodes, so levels[-1] is the coarsest. Nodes without edges are left out. Resolution > 1
|
|
30
|
+
favours smaller communities, < 1 larger ones. Visiting order is sorted, so the result is reproducible.
|
|
31
|
+
"""
|
|
32
|
+
adj: dict = defaultdict(lambda: defaultdict(float))
|
|
33
|
+
loops: dict = defaultdict(float)
|
|
34
|
+
for a, b, w in edges:
|
|
35
|
+
if a == b:
|
|
36
|
+
loops[a] += w
|
|
37
|
+
else:
|
|
38
|
+
adj[a][b] += w
|
|
39
|
+
adj[b][a] += w
|
|
40
|
+
cur = sorted(n for n in set(nodes) if adj.get(n) or loops.get(n))
|
|
41
|
+
member = {n: n for n in cur} # original node -> node of the current (aggregated) graph
|
|
42
|
+
levels: list[dict] = []
|
|
43
|
+
for _ in range(max_levels):
|
|
44
|
+
deg = {i: sum(adj[i].values()) + 2 * loops.get(i, 0.0) for i in cur}
|
|
45
|
+
m2 = sum(deg.values())
|
|
46
|
+
if m2 <= 0:
|
|
47
|
+
break
|
|
48
|
+
comm = {i: i for i in cur}
|
|
49
|
+
tot = dict(deg)
|
|
50
|
+
improved = False
|
|
51
|
+
for _sweep in range(50):
|
|
52
|
+
moved = 0
|
|
53
|
+
for i in cur:
|
|
54
|
+
ci, ki = comm[i], deg[i]
|
|
55
|
+
wc: dict = defaultdict(float)
|
|
56
|
+
for j, w in adj[i].items():
|
|
57
|
+
wc[comm[j]] += w
|
|
58
|
+
tot[ci] -= ki
|
|
59
|
+
best = ci
|
|
60
|
+
best_gain = wc.get(ci, 0.0) - resolution * tot[ci] * ki / m2
|
|
61
|
+
for c in sorted(wc):
|
|
62
|
+
g = wc[c] - resolution * tot[c] * ki / m2
|
|
63
|
+
if g > best_gain + EPS:
|
|
64
|
+
best, best_gain = c, g
|
|
65
|
+
tot[best] += ki
|
|
66
|
+
if best != ci:
|
|
67
|
+
comm[i] = best
|
|
68
|
+
moved += 1
|
|
69
|
+
improved = True
|
|
70
|
+
if not moved:
|
|
71
|
+
break
|
|
72
|
+
if not improved:
|
|
73
|
+
break
|
|
74
|
+
comm = _renumber(comm)
|
|
75
|
+
member = {o: comm[member[o]] for o in member}
|
|
76
|
+
levels.append(dict(member))
|
|
77
|
+
k = len(set(comm.values()))
|
|
78
|
+
if k == len(cur):
|
|
79
|
+
break
|
|
80
|
+
nadj: dict = defaultdict(lambda: defaultdict(float))
|
|
81
|
+
nloops: dict = defaultdict(float)
|
|
82
|
+
for i in cur:
|
|
83
|
+
ci = comm[i]
|
|
84
|
+
nloops[ci] += loops.get(i, 0.0)
|
|
85
|
+
for j, w in adj[i].items():
|
|
86
|
+
cj = comm[j]
|
|
87
|
+
if ci == cj:
|
|
88
|
+
nloops[ci] += w / 2 # an internal edge is seen from both ends
|
|
89
|
+
else:
|
|
90
|
+
nadj[ci][cj] += w
|
|
91
|
+
adj, loops, cur = nadj, nloops, list(range(k))
|
|
92
|
+
return levels
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def modularity(part: dict, edges, resolution: float = 1.0) -> float:
|
|
96
|
+
"""Newman modularity of a partition; used to compare runs and in tests."""
|
|
97
|
+
deg: dict = defaultdict(float)
|
|
98
|
+
inside = 0.0
|
|
99
|
+
total = 0.0
|
|
100
|
+
for a, b, w in edges:
|
|
101
|
+
total += w
|
|
102
|
+
deg[a] += w
|
|
103
|
+
deg[b] += w
|
|
104
|
+
if part.get(a) is not None and part.get(a) == part.get(b):
|
|
105
|
+
inside += w
|
|
106
|
+
if total <= 0:
|
|
107
|
+
return 0.0
|
|
108
|
+
tot: dict = defaultdict(float)
|
|
109
|
+
for n, d in deg.items():
|
|
110
|
+
if n in part:
|
|
111
|
+
tot[part[n]] += d
|
|
112
|
+
return inside / total - resolution * sum(t * t for t in tot.values()) / (4 * total * total)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def fold_small(part: dict, edges, min_size: int = 3) -> dict:
|
|
116
|
+
"""Merge communities smaller than `min_size` into the neighbour they are most connected to."""
|
|
117
|
+
adj: dict = defaultdict(lambda: defaultdict(float))
|
|
118
|
+
for a, b, w in edges:
|
|
119
|
+
if a != b:
|
|
120
|
+
adj[a][b] += w
|
|
121
|
+
adj[b][a] += w
|
|
122
|
+
part = dict(part)
|
|
123
|
+
size: dict = defaultdict(int)
|
|
124
|
+
for c in part.values():
|
|
125
|
+
size[c] += 1
|
|
126
|
+
for n in sorted(part):
|
|
127
|
+
if size[part[n]] >= min_size:
|
|
128
|
+
continue
|
|
129
|
+
votes: dict = defaultdict(float)
|
|
130
|
+
for m, w in adj.get(n, {}).items():
|
|
131
|
+
if m in part and size[part[m]] >= min_size:
|
|
132
|
+
votes[part[m]] += w
|
|
133
|
+
if votes:
|
|
134
|
+
size[part[n]] -= 1
|
|
135
|
+
part[n] = min(votes, key=lambda c: (-votes[c], c))
|
|
136
|
+
size[part[n]] += 1
|
|
137
|
+
return _renumber(part)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def sccs(nodes, out) -> list[list]:
|
|
141
|
+
"""Strongly connected components (iterative Tarjan). `out` maps node -> iterable of successors."""
|
|
142
|
+
index: dict = {}
|
|
143
|
+
low: dict = {}
|
|
144
|
+
on: set = set()
|
|
145
|
+
stack: list = []
|
|
146
|
+
comps: list = []
|
|
147
|
+
n = 0
|
|
148
|
+
for root in sorted(nodes):
|
|
149
|
+
if root in index:
|
|
150
|
+
continue
|
|
151
|
+
index[root] = low[root] = n
|
|
152
|
+
n += 1
|
|
153
|
+
stack.append(root)
|
|
154
|
+
on.add(root)
|
|
155
|
+
work = [(root, iter(sorted(out.get(root, ()))))]
|
|
156
|
+
while work:
|
|
157
|
+
v, it = work[-1]
|
|
158
|
+
for w in it:
|
|
159
|
+
if w not in index:
|
|
160
|
+
index[w] = low[w] = n
|
|
161
|
+
n += 1
|
|
162
|
+
stack.append(w)
|
|
163
|
+
on.add(w)
|
|
164
|
+
work.append((w, iter(sorted(out.get(w, ())))))
|
|
165
|
+
break
|
|
166
|
+
if w in on:
|
|
167
|
+
low[v] = min(low[v], index[w])
|
|
168
|
+
else:
|
|
169
|
+
work.pop()
|
|
170
|
+
if work:
|
|
171
|
+
u = work[-1][0]
|
|
172
|
+
low[u] = min(low[u], low[v])
|
|
173
|
+
if low[v] == index[v]:
|
|
174
|
+
comp = []
|
|
175
|
+
while True:
|
|
176
|
+
w = stack.pop()
|
|
177
|
+
on.discard(w)
|
|
178
|
+
comp.append(w)
|
|
179
|
+
if w == v:
|
|
180
|
+
break
|
|
181
|
+
comps.append(comp)
|
|
182
|
+
return comps
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def betweenness(adj, pivots: int | None = None) -> dict:
|
|
186
|
+
"""Betweenness centrality (Brandes) on an unweighted undirected graph {node: iterable of neighbours}.
|
|
187
|
+
|
|
188
|
+
With `pivots` set, only that many evenly spaced sources are used and the sum is scaled up: the
|
|
189
|
+
standard sampling estimate, which keeps big graphs affordable while ranking hubs the same way.
|
|
190
|
+
"""
|
|
191
|
+
nodes = sorted(adj)
|
|
192
|
+
nbr = {v: sorted(set(adj[v]) - {v}) for v in nodes}
|
|
193
|
+
sources = nodes
|
|
194
|
+
if pivots and pivots < len(nodes):
|
|
195
|
+
step = len(nodes) / pivots
|
|
196
|
+
sources = [nodes[int(i * step)] for i in range(pivots)]
|
|
197
|
+
bc = dict.fromkeys(nodes, 0.0)
|
|
198
|
+
for s in sources:
|
|
199
|
+
order = []
|
|
200
|
+
preds: dict = defaultdict(list)
|
|
201
|
+
sigma = dict.fromkeys(nodes, 0)
|
|
202
|
+
dist = dict.fromkeys(nodes, -1)
|
|
203
|
+
sigma[s], dist[s] = 1, 0
|
|
204
|
+
q = deque([s])
|
|
205
|
+
while q:
|
|
206
|
+
v = q.popleft()
|
|
207
|
+
order.append(v)
|
|
208
|
+
for w in nbr[v]:
|
|
209
|
+
if dist[w] < 0:
|
|
210
|
+
dist[w] = dist[v] + 1
|
|
211
|
+
q.append(w)
|
|
212
|
+
if dist[w] == dist[v] + 1:
|
|
213
|
+
sigma[w] += sigma[v]
|
|
214
|
+
preds[w].append(v)
|
|
215
|
+
delta = dict.fromkeys(nodes, 0.0)
|
|
216
|
+
for w in reversed(order):
|
|
217
|
+
for v in preds[w]:
|
|
218
|
+
delta[v] += sigma[v] / sigma[w] * (1 + delta[w])
|
|
219
|
+
if w != s:
|
|
220
|
+
bc[w] += delta[w]
|
|
221
|
+
scale = len(nodes) / len(sources) if sources else 1.0
|
|
222
|
+
return {v: b * scale / 2 for v, b in bc.items()} # undirected: each pair was counted twice
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def kcore(adj) -> dict:
|
|
226
|
+
"""Core number of every node (Batagelj-Zaversnik, heap variant): the largest k such that the node
|
|
227
|
+
survives repeatedly peeling off nodes with fewer than k neighbours."""
|
|
228
|
+
nbr = {v: set(ns) - {v} for v, ns in adj.items()}
|
|
229
|
+
deg = {v: len(ns) for v, ns in nbr.items()}
|
|
230
|
+
heap = [(d, v) for v, d in deg.items()]
|
|
231
|
+
heapq.heapify(heap)
|
|
232
|
+
core: dict = {}
|
|
233
|
+
k = 0
|
|
234
|
+
while heap:
|
|
235
|
+
d, v = heapq.heappop(heap)
|
|
236
|
+
if v in core or d != deg[v]:
|
|
237
|
+
continue
|
|
238
|
+
k = max(k, d)
|
|
239
|
+
core[v] = k
|
|
240
|
+
for u in nbr[v]:
|
|
241
|
+
if u not in core:
|
|
242
|
+
deg[u] -= 1
|
|
243
|
+
heapq.heappush(heap, (deg[u], u))
|
|
244
|
+
return core
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def shortest_path(out, inn, a, b, directed_first: bool = True) -> list:
|
|
248
|
+
"""Fewest-hop path a -> b. Follows edge direction first (a calls ... calls b) and only if that fails
|
|
249
|
+
treats the graph as undirected. Returns [] when b is unreachable."""
|
|
250
|
+
def bfs(step):
|
|
251
|
+
prev = {a: None}
|
|
252
|
+
q = deque([a])
|
|
253
|
+
while q:
|
|
254
|
+
v = q.popleft()
|
|
255
|
+
if v == b:
|
|
256
|
+
path = []
|
|
257
|
+
while v is not None:
|
|
258
|
+
path.append(v)
|
|
259
|
+
v = prev[v]
|
|
260
|
+
return path[::-1]
|
|
261
|
+
for w in step(v):
|
|
262
|
+
if w not in prev:
|
|
263
|
+
prev[w] = v
|
|
264
|
+
q.append(w)
|
|
265
|
+
return []
|
|
266
|
+
if a == b:
|
|
267
|
+
return [a]
|
|
268
|
+
if directed_first:
|
|
269
|
+
p = bfs(lambda v: sorted(out.get(v, ())))
|
|
270
|
+
if p:
|
|
271
|
+
return p
|
|
272
|
+
return bfs(lambda v: sorted(set(out.get(v, ())) | set(inn.get(v, ()))))
|