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.
Files changed (109) hide show
  1. seam/__init__.py +3 -0
  2. seam/_data/schema.sql +225 -0
  3. seam/_web/assets/index-BL_tqprR.js +216 -0
  4. seam/_web/assets/index-GTKUhVyD.css +1 -0
  5. seam/_web/index.html +13 -0
  6. seam/analysis/__init__.py +14 -0
  7. seam/analysis/affected.py +254 -0
  8. seam/analysis/builtins.py +966 -0
  9. seam/analysis/byte_budget.py +217 -0
  10. seam/analysis/changes.py +709 -0
  11. seam/analysis/cluster_naming.py +260 -0
  12. seam/analysis/clustering.py +216 -0
  13. seam/analysis/confidence.py +699 -0
  14. seam/analysis/embeddings.py +195 -0
  15. seam/analysis/flows.py +708 -0
  16. seam/analysis/impact.py +444 -0
  17. seam/analysis/imports.py +994 -0
  18. seam/analysis/imports_ext.py +780 -0
  19. seam/analysis/imports_resolve.py +176 -0
  20. seam/analysis/processes.py +453 -0
  21. seam/analysis/relevance.py +155 -0
  22. seam/analysis/rwr.py +129 -0
  23. seam/analysis/staleness.py +328 -0
  24. seam/analysis/steer.py +282 -0
  25. seam/analysis/synthesis.py +253 -0
  26. seam/analysis/synthesis_channels.py +433 -0
  27. seam/analysis/testpaths.py +103 -0
  28. seam/analysis/traversal.py +470 -0
  29. seam/cli/__init__.py +0 -0
  30. seam/cli/install.py +232 -0
  31. seam/cli/main.py +2602 -0
  32. seam/cli/output.py +137 -0
  33. seam/cli/read.py +244 -0
  34. seam/cli/serve.py +145 -0
  35. seam/config.py +551 -0
  36. seam/indexer/__init__.py +0 -0
  37. seam/indexer/cluster_index.py +425 -0
  38. seam/indexer/db.py +496 -0
  39. seam/indexer/embedding_index.py +183 -0
  40. seam/indexer/field_access.py +536 -0
  41. seam/indexer/field_access_c_cpp.py +643 -0
  42. seam/indexer/field_access_ext.py +708 -0
  43. seam/indexer/field_access_ext2.py +408 -0
  44. seam/indexer/field_access_go_rust.py +737 -0
  45. seam/indexer/field_access_php_swift.py +888 -0
  46. seam/indexer/field_access_ts.py +626 -0
  47. seam/indexer/graph.py +321 -0
  48. seam/indexer/graph_c.py +562 -0
  49. seam/indexer/graph_c_cpp.py +39 -0
  50. seam/indexer/graph_common.py +644 -0
  51. seam/indexer/graph_cpp.py +615 -0
  52. seam/indexer/graph_csharp.py +651 -0
  53. seam/indexer/graph_go.py +723 -0
  54. seam/indexer/graph_go_rust.py +39 -0
  55. seam/indexer/graph_java.py +689 -0
  56. seam/indexer/graph_java_csharp.py +38 -0
  57. seam/indexer/graph_php.py +914 -0
  58. seam/indexer/graph_python.py +628 -0
  59. seam/indexer/graph_ruby.py +748 -0
  60. seam/indexer/graph_rust.py +653 -0
  61. seam/indexer/graph_scope_infer.py +902 -0
  62. seam/indexer/graph_scope_infer_ext.py +723 -0
  63. seam/indexer/graph_scope_infer_ext2.py +992 -0
  64. seam/indexer/graph_swift.py +1014 -0
  65. seam/indexer/graph_swift_infer.py +515 -0
  66. seam/indexer/graph_typescript.py +663 -0
  67. seam/indexer/migrations.py +816 -0
  68. seam/indexer/parser.py +204 -0
  69. seam/indexer/pipeline.py +197 -0
  70. seam/indexer/signatures.py +634 -0
  71. seam/indexer/signatures_ext.py +780 -0
  72. seam/indexer/sync.py +287 -0
  73. seam/indexer/synthesis_index.py +291 -0
  74. seam/indexer/tokenize.py +79 -0
  75. seam/installer/__init__.py +67 -0
  76. seam/installer/claude.py +97 -0
  77. seam/installer/codex.py +94 -0
  78. seam/installer/core.py +127 -0
  79. seam/installer/cursor.py +61 -0
  80. seam/installer/guide.py +110 -0
  81. seam/installer/jsonfile.py +85 -0
  82. seam/installer/markdownfile.py +146 -0
  83. seam/installer/tomlfile.py +72 -0
  84. seam/query/__init__.py +0 -0
  85. seam/query/clusters.py +206 -0
  86. seam/query/comments.py +217 -0
  87. seam/query/context.py +293 -0
  88. seam/query/engine.py +940 -0
  89. seam/query/fts.py +328 -0
  90. seam/query/names.py +470 -0
  91. seam/query/pack.py +433 -0
  92. seam/query/semantic.py +339 -0
  93. seam/query/structure.py +727 -0
  94. seam/server/__init__.py +0 -0
  95. seam/server/graph_api.py +437 -0
  96. seam/server/handler_common.py +323 -0
  97. seam/server/impact_handler.py +615 -0
  98. seam/server/mcp.py +556 -0
  99. seam/server/tools.py +697 -0
  100. seam/server/trace_handler.py +184 -0
  101. seam/server/web.py +922 -0
  102. seam/watcher/__init__.py +0 -0
  103. seam/watcher/__main__.py +56 -0
  104. seam/watcher/daemon.py +237 -0
  105. seam_code-0.3.0.dist-info/METADATA +318 -0
  106. seam_code-0.3.0.dist-info/RECORD +109 -0
  107. seam_code-0.3.0.dist-info/WHEEL +4 -0
  108. seam_code-0.3.0.dist-info/entry_points.txt +2 -0
  109. seam_code-0.3.0.dist-info/licenses/LICENSE +21 -0
File without changes
@@ -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
+ ]