seam-code 0.3.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.
- seam/__init__.py +3 -0
- seam/_data/schema.sql +225 -0
- seam/_web/assets/index-BL_tqprR.js +216 -0
- seam/_web/assets/index-GTKUhVyD.css +1 -0
- seam/_web/index.html +13 -0
- seam/analysis/__init__.py +14 -0
- seam/analysis/affected.py +254 -0
- seam/analysis/builtins.py +966 -0
- seam/analysis/byte_budget.py +217 -0
- seam/analysis/changes.py +709 -0
- seam/analysis/cluster_naming.py +260 -0
- seam/analysis/clustering.py +216 -0
- seam/analysis/confidence.py +699 -0
- seam/analysis/embeddings.py +195 -0
- seam/analysis/flows.py +708 -0
- seam/analysis/impact.py +444 -0
- seam/analysis/imports.py +994 -0
- seam/analysis/imports_ext.py +780 -0
- seam/analysis/imports_resolve.py +176 -0
- seam/analysis/processes.py +453 -0
- seam/analysis/relevance.py +155 -0
- seam/analysis/rwr.py +129 -0
- seam/analysis/staleness.py +328 -0
- seam/analysis/steer.py +282 -0
- seam/analysis/synthesis.py +253 -0
- seam/analysis/synthesis_channels.py +433 -0
- seam/analysis/testpaths.py +103 -0
- seam/analysis/traversal.py +470 -0
- seam/cli/__init__.py +0 -0
- seam/cli/install.py +232 -0
- seam/cli/main.py +2602 -0
- seam/cli/output.py +137 -0
- seam/cli/read.py +244 -0
- seam/cli/serve.py +145 -0
- seam/config.py +551 -0
- seam/indexer/__init__.py +0 -0
- seam/indexer/cluster_index.py +425 -0
- seam/indexer/db.py +496 -0
- seam/indexer/embedding_index.py +183 -0
- seam/indexer/field_access.py +536 -0
- seam/indexer/field_access_c_cpp.py +643 -0
- seam/indexer/field_access_ext.py +708 -0
- seam/indexer/field_access_ext2.py +408 -0
- seam/indexer/field_access_go_rust.py +737 -0
- seam/indexer/field_access_php_swift.py +888 -0
- seam/indexer/field_access_ts.py +626 -0
- seam/indexer/graph.py +321 -0
- seam/indexer/graph_c.py +562 -0
- seam/indexer/graph_c_cpp.py +39 -0
- seam/indexer/graph_common.py +644 -0
- seam/indexer/graph_cpp.py +615 -0
- seam/indexer/graph_csharp.py +651 -0
- seam/indexer/graph_go.py +723 -0
- seam/indexer/graph_go_rust.py +39 -0
- seam/indexer/graph_java.py +689 -0
- seam/indexer/graph_java_csharp.py +38 -0
- seam/indexer/graph_php.py +914 -0
- seam/indexer/graph_python.py +628 -0
- seam/indexer/graph_ruby.py +748 -0
- seam/indexer/graph_rust.py +653 -0
- seam/indexer/graph_scope_infer.py +902 -0
- seam/indexer/graph_scope_infer_ext.py +723 -0
- seam/indexer/graph_scope_infer_ext2.py +992 -0
- seam/indexer/graph_swift.py +1014 -0
- seam/indexer/graph_swift_infer.py +515 -0
- seam/indexer/graph_typescript.py +663 -0
- seam/indexer/migrations.py +816 -0
- seam/indexer/parser.py +204 -0
- seam/indexer/pipeline.py +197 -0
- seam/indexer/signatures.py +634 -0
- seam/indexer/signatures_ext.py +780 -0
- seam/indexer/sync.py +287 -0
- seam/indexer/synthesis_index.py +291 -0
- seam/indexer/tokenize.py +79 -0
- seam/installer/__init__.py +67 -0
- seam/installer/claude.py +97 -0
- seam/installer/codex.py +94 -0
- seam/installer/core.py +127 -0
- seam/installer/cursor.py +61 -0
- seam/installer/guide.py +110 -0
- seam/installer/jsonfile.py +85 -0
- seam/installer/markdownfile.py +146 -0
- seam/installer/tomlfile.py +72 -0
- seam/query/__init__.py +0 -0
- seam/query/clusters.py +206 -0
- seam/query/comments.py +217 -0
- seam/query/context.py +293 -0
- seam/query/engine.py +940 -0
- seam/query/fts.py +328 -0
- seam/query/names.py +470 -0
- seam/query/pack.py +433 -0
- seam/query/semantic.py +339 -0
- seam/query/structure.py +727 -0
- seam/server/__init__.py +0 -0
- seam/server/graph_api.py +437 -0
- seam/server/handler_common.py +323 -0
- seam/server/impact_handler.py +615 -0
- seam/server/mcp.py +556 -0
- seam/server/tools.py +697 -0
- seam/server/trace_handler.py +184 -0
- seam/server/web.py +922 -0
- seam/watcher/__init__.py +0 -0
- seam/watcher/__main__.py +56 -0
- seam/watcher/daemon.py +237 -0
- seam_code-0.3.0.dist-info/METADATA +318 -0
- seam_code-0.3.0.dist-info/RECORD +109 -0
- seam_code-0.3.0.dist-info/WHEEL +4 -0
- seam_code-0.3.0.dist-info/entry_points.txt +2 -0
- seam_code-0.3.0.dist-info/licenses/LICENSE +21 -0
seam/server/__init__.py
ADDED
|
File without changes
|
seam/server/graph_api.py
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
"""Neighborhood graph builder for the Seam Explorer visual API.
|
|
2
|
+
|
|
3
|
+
Exposed as: build_neighborhood(conn, symbol_name, direction) -> dict
|
|
4
|
+
|
|
5
|
+
The API contract (from .claude/tasks/seam-explorer-frontend.md):
|
|
6
|
+
|
|
7
|
+
GET /api/graph/neighborhood?symbol=<name>&direction=both
|
|
8
|
+
→ { center, nodes: GraphNode[], edges: GraphEdge[] }
|
|
9
|
+
|
|
10
|
+
GraphNode = {
|
|
11
|
+
id=name, name, kind, signature, visibility, is_exported,
|
|
12
|
+
cluster_id, cluster_label, definition_count
|
|
13
|
+
}
|
|
14
|
+
GraphEdge = {
|
|
15
|
+
id, source=name, target=name,
|
|
16
|
+
kind: "call"|"import",
|
|
17
|
+
confidence: "EXTRACTED"|"AMBIGUOUS"|"INFERRED"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
Design notes:
|
|
21
|
+
- Node = symbol NAME (not file+name). Two files defining "helper" collapse to one
|
|
22
|
+
node — consistent with the edges table which is name-keyed. This is the same
|
|
23
|
+
homonym-collapse semantics used by seam_impact and seam_trace. Callers who need
|
|
24
|
+
the per-definition detail use the /api/symbol/<name> endpoint (detail panel).
|
|
25
|
+
- Depth-1 only: we pull direct callers/callees from the edges table and stop.
|
|
26
|
+
The client does lazy expansion (double-click a node) to explore further.
|
|
27
|
+
- Confidence is read directly from edges.confidence — no re-computation.
|
|
28
|
+
The engine's confidence resolution (Phase 5 import-promotion) lives at query/engine
|
|
29
|
+
time; the graph API trusts what was stored at index time, which is fine for
|
|
30
|
+
the explorer's needs (visualisation, not blast-radius analysis).
|
|
31
|
+
- NEVER raises. Unknown symbol returns a safe empty envelope.
|
|
32
|
+
|
|
33
|
+
LAYER: seam.server (adapter layer) — may import from seam.query.* and seam.indexer.*
|
|
34
|
+
but not from seam.cli.* or seam.server.mcp.*.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
import sqlite3
|
|
38
|
+
from typing import Any
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _fetch_center_node(
|
|
42
|
+
conn: sqlite3.Connection,
|
|
43
|
+
symbol_name: str,
|
|
44
|
+
) -> dict[str, Any] | None:
|
|
45
|
+
"""Fetch the center node's enrichment data from the symbols table.
|
|
46
|
+
|
|
47
|
+
Returns a GraphNode dict if the symbol exists, else None.
|
|
48
|
+
Aggregates across all definitions (homonym-collapse):
|
|
49
|
+
- kind, signature, visibility, is_exported from the lowest-id row (consistent
|
|
50
|
+
with engine.context() which uses ORDER BY s.id LIMIT 1)
|
|
51
|
+
- definition_count from COUNT(*) over all rows with that name
|
|
52
|
+
- cluster_id, cluster_label: from the first clustered row (or None)
|
|
53
|
+
"""
|
|
54
|
+
row = conn.execute(
|
|
55
|
+
"""
|
|
56
|
+
SELECT
|
|
57
|
+
s.name,
|
|
58
|
+
s.kind,
|
|
59
|
+
s.signature,
|
|
60
|
+
s.visibility,
|
|
61
|
+
s.is_exported,
|
|
62
|
+
s.cluster_id,
|
|
63
|
+
c.label AS cluster_label,
|
|
64
|
+
COUNT(*) OVER () AS definition_count
|
|
65
|
+
FROM symbols s
|
|
66
|
+
LEFT JOIN clusters c ON c.id = s.cluster_id
|
|
67
|
+
WHERE s.name = ?
|
|
68
|
+
ORDER BY s.id
|
|
69
|
+
LIMIT 1
|
|
70
|
+
""",
|
|
71
|
+
(symbol_name,),
|
|
72
|
+
).fetchone()
|
|
73
|
+
|
|
74
|
+
if row is None:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
# is_exported is stored as 0/1/NULL (SQLite has no native bool).
|
|
78
|
+
raw_exp = row["is_exported"]
|
|
79
|
+
is_exported: bool | None = None if raw_exp is None else bool(raw_exp)
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
"id": row["name"],
|
|
83
|
+
"name": row["name"],
|
|
84
|
+
"kind": row["kind"],
|
|
85
|
+
"signature": row["signature"],
|
|
86
|
+
"visibility": row["visibility"],
|
|
87
|
+
"is_exported": is_exported,
|
|
88
|
+
"cluster_id": row["cluster_id"],
|
|
89
|
+
"cluster_label": row["cluster_label"],
|
|
90
|
+
"definition_count": row["definition_count"],
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _fetch_neighbor_nodes(
|
|
95
|
+
conn: sqlite3.Connection,
|
|
96
|
+
symbol_names: set[str],
|
|
97
|
+
) -> dict[str, dict[str, Any]]:
|
|
98
|
+
"""Fetch enrichment data for a set of symbol names.
|
|
99
|
+
|
|
100
|
+
Returns a dict keyed by name. Names not found in the symbols table are omitted
|
|
101
|
+
(dangling edge targets — they won't appear as nodes). Each name collapses to
|
|
102
|
+
one node exactly (homonym-collapse: definition_count reflects how many rows
|
|
103
|
+
share that name, but all other fields come from the lowest-id row).
|
|
104
|
+
|
|
105
|
+
WHY LEFT JOIN clusters: cluster data may not exist (pre-v4 index). The LEFT JOIN
|
|
106
|
+
degrades cleanly — cluster_label becomes NULL, which maps to None in the output.
|
|
107
|
+
"""
|
|
108
|
+
if not symbol_names:
|
|
109
|
+
return {}
|
|
110
|
+
|
|
111
|
+
placeholders = ",".join("?" * len(symbol_names))
|
|
112
|
+
rows = conn.execute(
|
|
113
|
+
f"""
|
|
114
|
+
SELECT
|
|
115
|
+
s.name,
|
|
116
|
+
s.kind,
|
|
117
|
+
s.signature,
|
|
118
|
+
s.visibility,
|
|
119
|
+
s.is_exported,
|
|
120
|
+
s.cluster_id,
|
|
121
|
+
c.label AS cluster_label,
|
|
122
|
+
COUNT(*) OVER (PARTITION BY s.name) AS definition_count
|
|
123
|
+
FROM symbols s
|
|
124
|
+
LEFT JOIN clusters c ON c.id = s.cluster_id
|
|
125
|
+
WHERE s.name IN ({placeholders})
|
|
126
|
+
ORDER BY s.name, s.id
|
|
127
|
+
""",
|
|
128
|
+
list(symbol_names),
|
|
129
|
+
).fetchall()
|
|
130
|
+
|
|
131
|
+
# Keep only the first (lowest-id) row per name — homonym-collapse.
|
|
132
|
+
seen: set[str] = set()
|
|
133
|
+
nodes: dict[str, dict[str, Any]] = {}
|
|
134
|
+
for row in rows:
|
|
135
|
+
name = row["name"]
|
|
136
|
+
if name in seen:
|
|
137
|
+
continue
|
|
138
|
+
seen.add(name)
|
|
139
|
+
raw_exp = row["is_exported"]
|
|
140
|
+
is_exported: bool | None = None if raw_exp is None else bool(raw_exp)
|
|
141
|
+
nodes[name] = {
|
|
142
|
+
"id": name,
|
|
143
|
+
"name": name,
|
|
144
|
+
"kind": row["kind"],
|
|
145
|
+
"signature": row["signature"],
|
|
146
|
+
"visibility": row["visibility"],
|
|
147
|
+
"is_exported": is_exported,
|
|
148
|
+
"cluster_id": row["cluster_id"],
|
|
149
|
+
"cluster_label": row["cluster_label"],
|
|
150
|
+
"definition_count": row["definition_count"],
|
|
151
|
+
}
|
|
152
|
+
return nodes
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _fetch_edges(
|
|
156
|
+
conn: sqlite3.Connection,
|
|
157
|
+
symbol_name: str,
|
|
158
|
+
direction: str,
|
|
159
|
+
) -> list[dict[str, Any]]:
|
|
160
|
+
"""Fetch depth-1 edges for a symbol from the edges table.
|
|
161
|
+
|
|
162
|
+
direction:
|
|
163
|
+
"callees" -> edges where source_name = symbol_name (symbol calls others)
|
|
164
|
+
"callers" -> edges where target_name = symbol_name (others call symbol)
|
|
165
|
+
"both" -> union of the above (deduped by DB rowid)
|
|
166
|
+
|
|
167
|
+
WHY read edges.confidence directly: the stored confidence was written at index
|
|
168
|
+
time. For the explorer's visualization (solid/dashed/dotted lines), the stored
|
|
169
|
+
value is the right signal. The Phase 5 import-promotion resolver adds provenance
|
|
170
|
+
at query time but does not change the stored confidence — and this endpoint is
|
|
171
|
+
purely visual, not blast-radius analysis.
|
|
172
|
+
"""
|
|
173
|
+
if direction == "callees":
|
|
174
|
+
sql = """
|
|
175
|
+
SELECT id, source_name AS source, target_name AS target, kind, confidence
|
|
176
|
+
FROM edges
|
|
177
|
+
WHERE source_name = ?
|
|
178
|
+
"""
|
|
179
|
+
params = [symbol_name]
|
|
180
|
+
elif direction == "callers":
|
|
181
|
+
sql = """
|
|
182
|
+
SELECT id, source_name AS source, target_name AS target, kind, confidence
|
|
183
|
+
FROM edges
|
|
184
|
+
WHERE target_name = ?
|
|
185
|
+
"""
|
|
186
|
+
params = [symbol_name]
|
|
187
|
+
else: # "both"
|
|
188
|
+
# UNION deduplicates by (source, target, kind) at the SQL level; rowid
|
|
189
|
+
# is taken from whichever branch first emits the row. This is fine for
|
|
190
|
+
# the explorer — duplicate display edges would be confusing.
|
|
191
|
+
sql = """
|
|
192
|
+
SELECT id, source_name AS source, target_name AS target, kind, confidence
|
|
193
|
+
FROM edges
|
|
194
|
+
WHERE source_name = ?
|
|
195
|
+
UNION
|
|
196
|
+
SELECT id, source_name AS source, target_name AS target, kind, confidence
|
|
197
|
+
FROM edges
|
|
198
|
+
WHERE target_name = ?
|
|
199
|
+
"""
|
|
200
|
+
params = [symbol_name, symbol_name]
|
|
201
|
+
|
|
202
|
+
rows = conn.execute(sql, params).fetchall()
|
|
203
|
+
return [
|
|
204
|
+
{
|
|
205
|
+
"id": row["id"],
|
|
206
|
+
"source": row["source"],
|
|
207
|
+
"target": row["target"],
|
|
208
|
+
"kind": row["kind"],
|
|
209
|
+
"confidence": row["confidence"],
|
|
210
|
+
}
|
|
211
|
+
for row in rows
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def build_neighborhood(
|
|
216
|
+
conn: sqlite3.Connection,
|
|
217
|
+
symbol_name: str,
|
|
218
|
+
direction: str = "both",
|
|
219
|
+
) -> dict[str, Any]:
|
|
220
|
+
"""Build a depth-1 neighborhood graph for a symbol.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
{
|
|
224
|
+
"center": symbol_name,
|
|
225
|
+
"nodes": list[GraphNode], # one entry per unique NAME (homonym-collapse)
|
|
226
|
+
"edges": list[GraphEdge], # depth-1 only
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
GraphNode shape:
|
|
230
|
+
id, name, kind, signature, visibility, is_exported,
|
|
231
|
+
cluster_id, cluster_label, definition_count
|
|
232
|
+
|
|
233
|
+
GraphEdge shape:
|
|
234
|
+
id, source (name), target (name), kind ("call"|"import"),
|
|
235
|
+
confidence ("EXTRACTED"|"AMBIGUOUS"|"INFERRED")
|
|
236
|
+
|
|
237
|
+
Unknown symbol (not in DB) -> {center: name, nodes: [], edges: []}.
|
|
238
|
+
Symbol with no edges -> {center: name, nodes: [center_node], edges: []}.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
conn: Open SQLite connection (read-only access patterns).
|
|
242
|
+
symbol_name: The symbol name to center the graph on.
|
|
243
|
+
direction: "both" | "callers" | "callees". Defaults to "both".
|
|
244
|
+
|
|
245
|
+
NEVER raises — bad input returns the safe empty envelope.
|
|
246
|
+
"""
|
|
247
|
+
# Fetch center node enrichment.
|
|
248
|
+
center_node = _fetch_center_node(conn, symbol_name)
|
|
249
|
+
|
|
250
|
+
# Fetch depth-1 edges respecting direction filter.
|
|
251
|
+
edges = _fetch_edges(conn, symbol_name, direction)
|
|
252
|
+
|
|
253
|
+
if not edges and center_node is None:
|
|
254
|
+
# Symbol unknown entirely — empty result.
|
|
255
|
+
return {"center": symbol_name, "nodes": [], "edges": []}
|
|
256
|
+
|
|
257
|
+
# Collect unique neighbor names from edges (excluding the center itself).
|
|
258
|
+
neighbor_names: set[str] = set()
|
|
259
|
+
for edge in edges:
|
|
260
|
+
src, tgt = edge["source"], edge["target"]
|
|
261
|
+
if src != symbol_name:
|
|
262
|
+
neighbor_names.add(src)
|
|
263
|
+
if tgt != symbol_name:
|
|
264
|
+
neighbor_names.add(tgt)
|
|
265
|
+
|
|
266
|
+
# Fetch enrichment data for neighbors.
|
|
267
|
+
neighbor_nodes = _fetch_neighbor_nodes(conn, neighbor_names)
|
|
268
|
+
|
|
269
|
+
# Build the nodes list: center first, then neighbors in deterministic order.
|
|
270
|
+
nodes: list[dict[str, Any]] = []
|
|
271
|
+
if center_node is not None:
|
|
272
|
+
nodes.append(center_node)
|
|
273
|
+
nodes.extend(neighbor_nodes[name] for name in sorted(neighbor_nodes))
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
"center": symbol_name,
|
|
277
|
+
"nodes": nodes,
|
|
278
|
+
"edges": edges,
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def build_constellation(conn: sqlite3.Connection) -> dict[str, Any]:
|
|
283
|
+
"""Build the whole-repo cluster overview: clusters + inter-cluster links.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
{
|
|
287
|
+
"clusters": [ {cluster_id, label, size} ], # all clusters, largest first
|
|
288
|
+
"links": [ {source, target, weight} ], # cross-cluster edge counts
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
`links` aggregates the `edges` table: every call/import whose source and target
|
|
292
|
+
symbols live in DIFFERENT clusters contributes 1 to the (source_cid → target_cid)
|
|
293
|
+
weight. Intra-cluster edges and edges touching an unclustered symbol are skipped.
|
|
294
|
+
Direction is preserved (source cluster → target cluster).
|
|
295
|
+
|
|
296
|
+
name→cluster mapping uses the lowest-id row per name (homonym-collapse, matching
|
|
297
|
+
the name-keyed edges table): a name maps to exactly one cluster.
|
|
298
|
+
|
|
299
|
+
Pre-v4 index (no clusters table) or empty index → {clusters: [], links: []}.
|
|
300
|
+
NEVER raises — any DB error degrades to a safe (possibly partial) envelope.
|
|
301
|
+
"""
|
|
302
|
+
# 1. Clusters. Guard the table itself (pre-v4 indexes have no clusters table).
|
|
303
|
+
try:
|
|
304
|
+
cluster_rows = conn.execute(
|
|
305
|
+
"SELECT id, label, size FROM clusters ORDER BY size DESC, id"
|
|
306
|
+
).fetchall()
|
|
307
|
+
except sqlite3.Error:
|
|
308
|
+
return {"clusters": [], "links": []}
|
|
309
|
+
|
|
310
|
+
clusters = [
|
|
311
|
+
{"cluster_id": r["id"], "label": r["label"], "size": r["size"]}
|
|
312
|
+
for r in cluster_rows
|
|
313
|
+
]
|
|
314
|
+
if not clusters:
|
|
315
|
+
return {"clusters": [], "links": []}
|
|
316
|
+
|
|
317
|
+
# 2. name → cluster_id (lowest-id row per name). SQLite's bare-column rule binds
|
|
318
|
+
# cluster_id to the MIN(id) row, so each name resolves to one cluster.
|
|
319
|
+
try:
|
|
320
|
+
name_rows = conn.execute(
|
|
321
|
+
"SELECT name, cluster_id, MIN(id) FROM symbols "
|
|
322
|
+
"WHERE cluster_id IS NOT NULL GROUP BY name"
|
|
323
|
+
).fetchall()
|
|
324
|
+
except sqlite3.Error:
|
|
325
|
+
return {"clusters": clusters, "links": []}
|
|
326
|
+
name_to_cid: dict[str, int] = {r["name"]: r["cluster_id"] for r in name_rows}
|
|
327
|
+
|
|
328
|
+
# 3. Aggregate cross-cluster edge weights.
|
|
329
|
+
weights: dict[tuple[int, int], int] = {}
|
|
330
|
+
try:
|
|
331
|
+
edge_rows = conn.execute("SELECT source_name, target_name FROM edges").fetchall()
|
|
332
|
+
except sqlite3.Error:
|
|
333
|
+
edge_rows = []
|
|
334
|
+
for r in edge_rows:
|
|
335
|
+
src_cid = name_to_cid.get(r["source_name"])
|
|
336
|
+
tgt_cid = name_to_cid.get(r["target_name"])
|
|
337
|
+
# Skip unclustered endpoints and intra-cluster edges (no inter-cluster signal).
|
|
338
|
+
if src_cid is None or tgt_cid is None or src_cid == tgt_cid:
|
|
339
|
+
continue
|
|
340
|
+
key = (src_cid, tgt_cid)
|
|
341
|
+
weights[key] = weights.get(key, 0) + 1
|
|
342
|
+
|
|
343
|
+
# Heaviest links first (deterministic tie-break on the cluster id pair).
|
|
344
|
+
links = [
|
|
345
|
+
{"source": src, "target": tgt, "weight": weight}
|
|
346
|
+
for (src, tgt), weight in sorted(weights.items(), key=lambda kv: (-kv[1], kv[0]))
|
|
347
|
+
]
|
|
348
|
+
|
|
349
|
+
return {"clusters": clusters, "links": links}
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def top_hub_symbols(conn: sqlite3.Connection, limit: int = 8) -> list[dict[str, Any]]:
|
|
353
|
+
"""Return the most-connected (highest-degree) symbols — the repo's hubs.
|
|
354
|
+
|
|
355
|
+
These are the natural entry points for a landing page: the symbols everything
|
|
356
|
+
else touches. Degree = number of edges where the symbol is the source OR the
|
|
357
|
+
target (name-keyed, matching the edges table).
|
|
358
|
+
|
|
359
|
+
Only names that are actually DEFINED in the symbols table are returned, so
|
|
360
|
+
builtins/stdlib (e.g. `print`, `len`) that appear only as edge targets are
|
|
361
|
+
excluded — they have no row in `symbols`.
|
|
362
|
+
|
|
363
|
+
Returns [{name, kind, degree, path}] sorted by degree desc, then name. `path`
|
|
364
|
+
is a representative declaring file (lowest-id row for the name — homonym-safe);
|
|
365
|
+
the explorer uses it to bucket hubs into functional areas. Empty list on any
|
|
366
|
+
DB error or empty index. NEVER raises.
|
|
367
|
+
"""
|
|
368
|
+
try:
|
|
369
|
+
rows = conn.execute(
|
|
370
|
+
"""
|
|
371
|
+
SELECT
|
|
372
|
+
d.name AS name,
|
|
373
|
+
d.degree AS degree,
|
|
374
|
+
(SELECT s.kind FROM symbols s WHERE s.name = d.name ORDER BY s.id LIMIT 1) AS kind,
|
|
375
|
+
(SELECT f.path FROM symbols s JOIN files f ON f.id = s.file_id
|
|
376
|
+
WHERE s.name = d.name ORDER BY s.id LIMIT 1) AS path
|
|
377
|
+
FROM (
|
|
378
|
+
SELECT name, COUNT(*) AS degree
|
|
379
|
+
FROM (
|
|
380
|
+
SELECT source_name AS name FROM edges
|
|
381
|
+
UNION ALL
|
|
382
|
+
SELECT target_name AS name FROM edges
|
|
383
|
+
)
|
|
384
|
+
GROUP BY name
|
|
385
|
+
) d
|
|
386
|
+
WHERE EXISTS (SELECT 1 FROM symbols s WHERE s.name = d.name)
|
|
387
|
+
ORDER BY d.degree DESC, d.name
|
|
388
|
+
LIMIT ?
|
|
389
|
+
""",
|
|
390
|
+
(limit,),
|
|
391
|
+
).fetchall()
|
|
392
|
+
except sqlite3.Error:
|
|
393
|
+
return []
|
|
394
|
+
|
|
395
|
+
return [
|
|
396
|
+
{"name": r["name"], "kind": r["kind"], "degree": r["degree"], "path": r["path"]}
|
|
397
|
+
for r in rows
|
|
398
|
+
]
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def list_structure(conn: sqlite3.Connection) -> list[dict[str, Any]]:
|
|
402
|
+
"""Return every symbol with its file path + nesting info, for the structure map.
|
|
403
|
+
|
|
404
|
+
Flat by design: the frontend builds the folder → file → class → method tree and
|
|
405
|
+
the treemap from this list (one cheap query here; all hierarchy logic client-side).
|
|
406
|
+
|
|
407
|
+
Returns [{path, name, kind, line, qualified_name}] (absolute path — the web route
|
|
408
|
+
relativizes it). Ordered by path then symbol id for stable tree construction.
|
|
409
|
+
Empty list on any DB error. NEVER raises.
|
|
410
|
+
"""
|
|
411
|
+
try:
|
|
412
|
+
rows = conn.execute(
|
|
413
|
+
"""
|
|
414
|
+
SELECT
|
|
415
|
+
f.path AS path,
|
|
416
|
+
s.name AS name,
|
|
417
|
+
s.kind AS kind,
|
|
418
|
+
s.start_line AS line,
|
|
419
|
+
s.qualified_name AS qualified_name
|
|
420
|
+
FROM symbols s
|
|
421
|
+
JOIN files f ON f.id = s.file_id
|
|
422
|
+
ORDER BY f.path, s.id
|
|
423
|
+
"""
|
|
424
|
+
).fetchall()
|
|
425
|
+
except sqlite3.Error:
|
|
426
|
+
return []
|
|
427
|
+
|
|
428
|
+
return [
|
|
429
|
+
{
|
|
430
|
+
"path": r["path"],
|
|
431
|
+
"name": r["name"],
|
|
432
|
+
"kind": r["kind"],
|
|
433
|
+
"line": r["line"],
|
|
434
|
+
"qualified_name": r["qualified_name"],
|
|
435
|
+
}
|
|
436
|
+
for r in rows
|
|
437
|
+
]
|