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
seam/server/mcp.py ADDED
@@ -0,0 +1,556 @@
1
+ """MCP server setup — FastMCP stdio transport, twelve tools registered.
2
+
3
+ Creates and configures the MCP server instance.
4
+ Tool handlers in tools.py are thin adapters; this module wires them to FastMCP.
5
+
6
+ Usage (from cli/main.py):
7
+ server = create_server(conn, root)
8
+ server.run(transport="stdio")
9
+
10
+ Tools registered (Phase 0 + Phase 1 + Phase 1b + Phase 2 + Phase 3 + Phase 6 + Tier D11):
11
+ seam_query — FTS5 + 1-hop graph expansion search
12
+ seam_context — 360-degree symbol view (callers, callees, location, cluster)
13
+ seam_search — full-text search (FTS5 BM25)
14
+ seam_impact — blast-radius analysis by risk tier (Phase 1)
15
+ seam_trace — shortest call/dependency path between two symbols (Phase 1)
16
+ seam_changes — git diff → changed symbols → risk level (Phase 1)
17
+ seam_why — semantic comments (WHY/HACK/NOTE/TODO/FIXME) near a location (Phase 1b)
18
+ seam_clusters — list clusters or members of a cluster (Phase 2)
19
+ seam_affected — changed files → impacted test files via reverse-dependency BFS (Phase 3)
20
+ seam_context_pack — enriched context bundle: target + neighbors + WHY + peers (Phase 6)
21
+ seam_flows — execution flows: entry points + forward call-chain expansion
22
+ seam_structure — whole-repo directory/file/container structure tree (Tier D11)
23
+
24
+ Design:
25
+ - One FastMCP instance per process; connection is injected at creation time.
26
+ - Tools are closures capturing conn + root so FastMCP's decorator pattern
27
+ (which does not pass state through the call signature) stays clean.
28
+ - Return types are Any to avoid FastMCP structured-output mode, which wraps
29
+ results in a Pydantic model we don't need.
30
+ """
31
+
32
+ import sqlite3
33
+ from pathlib import Path
34
+ from typing import Any
35
+
36
+ from mcp.server.fastmcp import FastMCP
37
+ from mcp.server.fastmcp.exceptions import ToolError
38
+
39
+ import seam.config as config
40
+ from seam.analysis.changes import DEFAULT_BASE_REF
41
+ from seam.server.tools import (
42
+ handle_seam_affected,
43
+ handle_seam_changes,
44
+ handle_seam_clusters,
45
+ handle_seam_context,
46
+ handle_seam_context_pack,
47
+ handle_seam_flows,
48
+ handle_seam_impact,
49
+ handle_seam_query,
50
+ handle_seam_search,
51
+ handle_seam_structure,
52
+ handle_seam_trace,
53
+ handle_seam_why,
54
+ )
55
+
56
+ # Limit defaults/bounds (mirrors tools.py constants — kept local to avoid circular import)
57
+ _QUERY_LIMIT_DEFAULT = 10
58
+ _SEARCH_LIMIT_DEFAULT = 20
59
+ _IMPACT_DEPTH_DEFAULT = 3
60
+ _IMPACT_DIRECTION_DEFAULT = "upstream"
61
+ _TRACE_DEPTH_DEFAULT = 10
62
+ _CHANGES_SCOPE_DEFAULT = "working"
63
+ # Import DEFAULT_BASE_REF from analysis.changes instead of redefining it
64
+ # to avoid drift when the canonical default changes.
65
+ _CHANGES_BASE_REF_DEFAULT = DEFAULT_BASE_REF
66
+
67
+ _AFFECTED_DEPTH_DEFAULT = config.SEAM_AFFECTED_DEPTH
68
+ _IMPACT_LIMIT_DEFAULT = config.SEAM_IMPACT_MAX_RESULTS
69
+ _IMPACT_MAX_BYTES_DEFAULT = config.SEAM_IMPACT_MAX_BYTES
70
+
71
+
72
+ def _finalize(result: Any) -> Any:
73
+ """Normalize a handler result to the MCP transport's native failure contract.
74
+
75
+ WHY: FastMCP only sets isError=True when a tool *raises* — returning an error
76
+ dict leaves isError=False, so a protocol-compliant agent (which checks isError)
77
+ reads a rejection as success. The handlers return {"error": CODE, "message": ...}
78
+ sentinels (kept for the CLI's {ok:false,error:{code,message}} envelope), so here —
79
+ at the MCP boundary only — we raise on that sentinel and let FastMCP flip isError.
80
+ A None ("handler found nothing") becomes a structured {"found": false} so agents
81
+ never receive empty content for a valid no-result answer. Every other value
82
+ (success dict, list) passes through byte-identical.
83
+
84
+ The CLI and MCP thus expose the SAME code+message via each transport's native
85
+ error signal — not byte-identical JSON (clean JSON cannot survive FastMCP's
86
+ "Error executing tool <name>: " content prefix on a raise).
87
+ """
88
+ if result is None:
89
+ return {"found": False}
90
+ if isinstance(result, dict) and "error" in result and "message" in result:
91
+ raise ToolError(f"{result['error']}: {result['message']}")
92
+ return result
93
+
94
+
95
+ def create_server(conn: sqlite3.Connection, root: Path) -> FastMCP:
96
+ """Configure and return a FastMCP server with all twelve Seam tools registered.
97
+
98
+ Phase 0: seam_query, seam_context, seam_search
99
+ Phase 1: seam_impact, seam_trace, seam_changes
100
+ Phase 1b: seam_why
101
+ Phase 2: seam_clusters
102
+ Phase 3: seam_affected
103
+ Phase 6: seam_context_pack
104
+ Flows: seam_flows
105
+ Tier D11: seam_structure
106
+
107
+ Args:
108
+ conn: Open SQLite connection to the Seam index DB.
109
+ root: Project root Path — used to relativize file paths in results
110
+ and as the git repo root for seam_changes.
111
+
112
+ Returns:
113
+ A FastMCP instance ready for server.run(transport="stdio").
114
+ """
115
+ mcp: FastMCP = FastMCP(name="seam")
116
+
117
+ @mcp.tool()
118
+ def seam_query(concept: str, limit: int = _QUERY_LIMIT_DEFAULT) -> Any:
119
+ """Find all code related to a concept using hybrid search (FTS5 + 1-hop graph expansion).
120
+
121
+ Use this when you need to find where a concept lives across the codebase.
122
+
123
+ No `verbose` flag: query results carry no Phase 4/5 enrichment fields, so lean
124
+ mode would be a no-op — query is enrichment-free, like seam_search.
125
+ """
126
+ return _finalize(handle_seam_query(conn, concept, root, limit=limit))
127
+
128
+ @mcp.tool()
129
+ def seam_context(symbol: str = "", verbose: bool = True, uid: str | None = None) -> Any:
130
+ """Get a 360-degree view of a symbol: its callers, callees, file location, and docstring.
131
+
132
+ Use before touching any existing function or class.
133
+
134
+ Pass uid (a stable handle from a seam_search/seam_query result) instead of
135
+ symbol to pin the EXACT (file, line) symbol — bypassing homonym ambiguity and
136
+ saving a disambiguation round-trip. When uid is given, symbol is ignored.
137
+
138
+ Set verbose=false to omit heavy enrichment fields (decorators, is_exported,
139
+ visibility, qualified_name) and receive a compact response. signature and all
140
+ core identity fields are always kept.
141
+ verbose=true (default) is byte-identical to the pre-Phase-8 output.
142
+
143
+ Returns {found: false} when the symbol/uid is not in the index.
144
+ """
145
+ return _finalize(handle_seam_context(conn, symbol, root, verbose=verbose, uid=uid))
146
+
147
+ @mcp.tool()
148
+ def seam_search(text: str, limit: int = _SEARCH_LIMIT_DEFAULT) -> Any:
149
+ """Full-text search across all indexed symbol names and docstrings (FTS5 BM25).
150
+
151
+ Use when you know a keyword but not the exact symbol name.
152
+ Supports FTS5 operators: AND, OR, NOT, phrase search in quotes.
153
+ """
154
+ return _finalize(handle_seam_search(conn, text, root, limit=limit))
155
+
156
+ @mcp.tool()
157
+ def seam_impact(
158
+ target: str = "",
159
+ direction: str = _IMPACT_DIRECTION_DEFAULT,
160
+ max_depth: int = _IMPACT_DEPTH_DEFAULT,
161
+ include_tests: bool = False,
162
+ verbose: bool = True,
163
+ limit: int = _IMPACT_LIMIT_DEFAULT,
164
+ max_bytes: int = _IMPACT_MAX_BYTES_DEFAULT,
165
+ uid: str | None = None,
166
+ ) -> Any:
167
+ """Blast-radius analysis — what breaks if I change this symbol?
168
+
169
+ Returns all symbols that depend on the target (upstream), that the target
170
+ depends on (downstream), or both — grouped into risk tiers by distance:
171
+ WILL_BREAK (distance 1) — direct dependents, definitely affected.
172
+ LIKELY_AFFECTED (distance 2) — indirect dependents, probably affected.
173
+ MAY_NEED_TESTING (distance 3+) — transitive dependents, test to be sure.
174
+
175
+ Each entry carries the aggregated path confidence (EXTRACTED | INFERRED | AMBIGUOUS)
176
+ so you know which conclusions to lean on and which to verify by reading.
177
+ Each entry also carries is_test (bool) so you can distinguish production dependents
178
+ from test-only callers.
179
+
180
+ E4 — Edge provenance (SEAM_EDGE_PROVENANCE=on, default):
181
+ kind — the edge kind via which this dependent was reached. Full
182
+ vocabulary: call | import | extends | implements | instantiates
183
+ | holds | reads | writes | uses. Lets you distinguish a hard
184
+ call-edge dependent from a data-coupling (reads/holds) or
185
+ signature-coupling (uses) dependent. Always present in both
186
+ verbose and lean modes (core field, never stripped).
187
+ synthesized_by — synthesis channel name when the edge is heuristic (e.g.
188
+ "interface-override", "closure-collection", "event-emitter"),
189
+ null when statically extracted. Lets you weight entries that
190
+ rest on over-approximated synthesized edges differently.
191
+ Null is RETAINED (null = "static edge", the informative common
192
+ case — unlike best_candidate which is E1-omitted when null).
193
+ Stripped in lean mode (verbose=false), like resolved_by.
194
+ AMBIGUITY (important): null does NOT, on its own, prove this
195
+ edge is static — an index that was never synthesis-rebuilt
196
+ (pre-v12, or SEAM_EDGE_SYNTHESIS=off at index time) carries
197
+ null for EVERY edge. So all-null across a result can mean
198
+ "no synthesized edges traversed" OR "synthesis never ran" — it
199
+ does not distinguish them; run `seam init` to populate. A
200
+ MISSING key (not null) means SEAM_EDGE_PROVENANCE=off or lean
201
+ mode stripped it — three distinct "no value" states.
202
+ Set SEAM_EDGE_PROVENANCE=off for byte-identical pre-E4 output.
203
+
204
+ E4 — Truncation steer (SEAM_IMPACT_STEER=on, default):
205
+ next_actions — top-level list[str] of ready-to-act prose hints, PRESENT only
206
+ when ≥1 entry was trimmed (by count cap or byte ceiling).
207
+ ABSENT when nothing was trimmed (so its presence is an
208
+ unambiguous "there is more" signal). Hints name the specific
209
+ direction+tier+count trimmed and the exact remedy (raise limit,
210
+ zero max_bytes, etc.) — so you know what to do, not just that
211
+ something was dropped. An all-trimmed response (empty entries
212
+ but non-empty risk_summary) includes a warning that the blast
213
+ radius was trimmed to nothing, NOT that no dependents exist.
214
+ Set SEAM_IMPACT_STEER=off to suppress next_actions entirely.
215
+
216
+ By default (include_tests=false) the result is the PRODUCTION blast radius:
217
+ test-file dependents are filtered out and their count is reported as hidden_tests.
218
+ This keeps "what breaks?" focused on production code (test callers otherwise
219
+ dominate the tiers and trip the per-tier cap). Set include_tests=true to include
220
+ test dependents too; or use seam_affected to get the impacted test files directly.
221
+
222
+ Set verbose=false to omit heavy enrichment fields (resolved_by, best_candidate,
223
+ synthesized_by) from every tier entry and get a compact response. kind is always
224
+ kept even in lean mode. verbose=true (default) returns all enrichment fields.
225
+ NOTE: risk_summary, truncated, and the per-tier cap apply regardless of verbose.
226
+ Use limit=0 for the full, uncapped transitive set.
227
+
228
+ limit controls the per-tier entry cap (default: SEAM_IMPACT_MAX_RESULTS=25).
229
+ Set limit=0 to disable the cap and receive all transitive entries.
230
+ The response always includes risk_summary: {direction: {tier: count}} computed
231
+ from the full pre-cap result, so the blast radius size is always visible.
232
+ When entries were truncated, truncated: {direction: {tier: omitted}} is included.
233
+
234
+ max_bytes controls the per-call character budget for the serialized output
235
+ (characters of compact JSON). Default: SEAM_IMPACT_MAX_BYTES (0 = unlimited).
236
+ When > 0, runs after the per-tier count cap and E2/E3 relevance ordering, trimming
237
+ entries from the least-valuable end (downstream before upstream, MAY_NEED_TESTING
238
+ before WILL_BREAK, tail before front) until the output fits. Byte-dropped counts
239
+ are merged into truncated additively. When the ceiling fires, byte_capped is added:
240
+ {"limit": <budget>, "omitted": <total entries dropped by the byte pass>}.
241
+ byte_capped is absent when max_bytes=0 or when everything fit (no trimming).
242
+ risk_summary remains the honest full pre-cap total regardless of byte trimming.
243
+
244
+ Pass uid (a stable handle from a seam_search/seam_query result) instead of
245
+ target to pin the exact symbol and skip homonym re-disambiguation.
246
+
247
+ Use before editing any symbol to understand the blast radius.
248
+ """
249
+ return _finalize(
250
+ handle_seam_impact(
251
+ conn,
252
+ target,
253
+ root,
254
+ direction=direction,
255
+ max_depth=max_depth,
256
+ include_tests=include_tests,
257
+ verbose=verbose,
258
+ limit=limit,
259
+ max_bytes=max_bytes,
260
+ uid=uid,
261
+ )
262
+ )
263
+
264
+ @mcp.tool()
265
+ def seam_trace(
266
+ source: str = "",
267
+ target: str = "",
268
+ max_depth: int = _TRACE_DEPTH_DEFAULT,
269
+ verbose: bool = True,
270
+ uid: str | None = None,
271
+ target_uid: str | None = None,
272
+ ) -> Any:
273
+ """Trace the call/dependency path between two symbols.
274
+
275
+ Returns the shortest path from source to target as an ordered list of hops,
276
+ where each hop carries the edge kind and per-edge confidence
277
+ (EXTRACTED | INFERRED | AMBIGUOUS).
278
+
279
+ Full edge kind vocabulary (E4 corrected from stale 'call | import'):
280
+ call | import | extends | implements | instantiates | holds | reads | writes | uses
281
+ The hop kind reflects the actual relationship traversed — e.g. a 'holds' hop
282
+ means one class stores the other as a typed field, while a 'reads' hop means
283
+ a field-access read edge was traversed.
284
+
285
+ E4 — synthesized_by on each hop (SEAM_EDGE_PROVENANCE=on, default):
286
+ synthesized_by — synthesis channel name when the hop is a heuristic synthesized
287
+ edge (e.g. "interface-override"), null when statically extracted.
288
+ Lets you see which hops in a path rest on over-approximations.
289
+ In lean mode (verbose=false), synthesized_by is stripped
290
+ (like resolved_by) — kind is always kept.
291
+ AMBIGUITY (important): null on a hop does NOT prove the hop is
292
+ static — an index never synthesis-rebuilt (pre-v12, or
293
+ SEAM_EDGE_SYNTHESIS=off at index time) carries null for EVERY
294
+ hop. all-null can mean "no synthesized hops" OR "synthesis never
295
+ ran"; run `seam init` to populate. A MISSING key means lean mode
296
+ or SEAM_EDGE_PROVENANCE=off.
297
+
298
+ Also returns one-hop callers and callees for both symbols so you can see
299
+ the immediate neighborhood alongside the path.
300
+
301
+ Use this when you need to understand how control flows from one symbol to
302
+ another, or to answer "how does X reach Y?" without manual grep.
303
+
304
+ Returns found=false (paths=[]) when no path exists — this is a real,
305
+ distinguishable "not connected" answer, not an error.
306
+
307
+ Per-hop confidence lets you flag any hop that rests on an AMBIGUOUS edge
308
+ (name collision at extraction time) so you know which conclusions are certain
309
+ and which need manual verification.
310
+
311
+ Set verbose=false to omit heavy fields (resolved_by, best_candidate,
312
+ synthesized_by) from every hop and edge hop. kind is always kept.
313
+ verbose=true (default) is byte-identical to pre-Phase-8 (plus E4 fields).
314
+
315
+ Pass uid / target_uid (stable handles from seam_search/seam_query results)
316
+ instead of source / target to pin exact symbols and skip re-disambiguation.
317
+ """
318
+ return _finalize(
319
+ handle_seam_trace(
320
+ conn,
321
+ source,
322
+ target,
323
+ root,
324
+ max_depth=max_depth,
325
+ verbose=verbose,
326
+ uid=uid,
327
+ target_uid=target_uid,
328
+ )
329
+ )
330
+
331
+ @mcp.tool()
332
+ def seam_changes(
333
+ scope: str = _CHANGES_SCOPE_DEFAULT,
334
+ base_ref: str = _CHANGES_BASE_REF_DEFAULT,
335
+ ) -> Any:
336
+ """Pre-commit risk check — map git diff to affected symbols and risk level.
337
+
338
+ Diffs the working tree / staged set / branch against a git ref, maps each
339
+ changed line range to the symbols it touched, runs impact analysis, and
340
+ returns an overall risk level:
341
+ low — no downstream dependents found
342
+ medium — transitive dependents (MAY_NEED_TESTING)
343
+ high — indirect dependents (LIKELY_AFFECTED)
344
+ critical — direct dependents (WILL_BREAK)
345
+
346
+ scope values:
347
+ working — git diff (unstaged working tree vs index)
348
+ staged — git diff --cached (staged changes only)
349
+ branch — git diff <base_ref>...HEAD (entire branch vs base ref)
350
+
351
+ Use before committing to understand what your changes break.
352
+ Fails (isError) with NOT_A_GIT_REPO when run outside a git repository.
353
+ """
354
+ return _finalize(handle_seam_changes(conn, root, base_ref=base_ref, scope=scope))
355
+
356
+ @mcp.tool()
357
+ def seam_why(
358
+ file: str | None = None,
359
+ line: int | None = None,
360
+ symbol: str | None = None,
361
+ ) -> Any:
362
+ """Return semantic comments (WHY/HACK/NOTE/TODO/FIXME) near a location or symbol.
363
+
364
+ Lookup modes (at least one of file or symbol is required):
365
+ file only — all semantic comments in that file.
366
+ file + line — comments within ±15 lines of that line number.
367
+ symbol — comments inside the symbol's body and just above its definition.
368
+
369
+ Result fields per comment:
370
+ file — path relative to project root.
371
+ line — 1-based line number.
372
+ marker — WHY | HACK | NOTE | TODO | FIXME.
373
+ text — comment body after the marker, stripped.
374
+
375
+ Returns an empty list (not an error) when a file/symbol has no semantic comments.
376
+ Use before editing a function to read the documented intent and known caveats.
377
+ """
378
+ return _finalize(handle_seam_why(conn, root, file=file, line=line, symbol=symbol))
379
+
380
+ @mcp.tool()
381
+ def seam_clusters(cluster_id: int | None = None) -> Any:
382
+ """List all code clusters (functional areas) or the members of one cluster.
383
+
384
+ With no argument: returns [{id, label, size}] — an overview of all detected
385
+ functional areas in the codebase, sorted by id.
386
+
387
+ With cluster_id: returns [{name, file, line, kind}] — all symbols that belong
388
+ to that cluster, with file paths relative to the project root.
389
+
390
+ Clusters are computed during `seam init` using Louvain community detection over
391
+ the call/import graph. Each cluster represents a cohesive group of symbols that
392
+ frequently call or import each other.
393
+
394
+ Returns an empty list (not an error) when the index has no cluster data —
395
+ either the repo has no symbols, or `seam init` hasn't been run yet.
396
+
397
+ Use this to understand the codebase's functional areas before diving into a
398
+ specific subsystem. Then use seam_context to see a symbol's cluster peers.
399
+ """
400
+ return _finalize(handle_seam_clusters(conn, root, cluster_id=cluster_id))
401
+
402
+ @mcp.tool()
403
+ def seam_affected(
404
+ changed_files: list[str],
405
+ depth: int = _AFFECTED_DEPTH_DEFAULT,
406
+ ) -> Any:
407
+ """Find which test files are impacted by a set of changed source files.
408
+
409
+ Given a list of changed file paths, traverses the reverse-dependency graph
410
+ (upstream impact) to find all test files that depend on symbols in those files.
411
+
412
+ Result shape:
413
+ changed_files — the input files (relativized to project root)
414
+ affected_tests — sorted unique test files that must be re-run
415
+ total_dependents_traversed — count of all dependency entries examined
416
+
417
+ Usage pattern (agent workflow):
418
+ 1. Get changed files from git: `git diff --name-only`
419
+ 2. Pass them to seam_affected to get the impacted test files
420
+ 3. Run only those tests: `pytest <affected_tests>`
421
+
422
+ A changed file that is itself a test file is always included in affected_tests.
423
+ Files not in the index are silently skipped (they have no graph dependents).
424
+ Fails (isError) with INVALID_INPUT when changed_files is empty.
425
+
426
+ Use this to determine the minimal set of tests to run before committing.
427
+ """
428
+ return _finalize(handle_seam_affected(conn, changed_files, root, depth=depth))
429
+
430
+ @mcp.tool()
431
+ def seam_context_pack(symbol: str, verbose: bool = True) -> Any:
432
+ """Get a ready-to-paste context bundle for a symbol.
433
+
434
+ Returns a single payload containing:
435
+ target — the symbol's full 360-degree view (file, kind, docstring,
436
+ signature, decorators, is_exported, visibility, ambiguous flag,
437
+ cluster info, raw callers/callees name lists)
438
+ callers — 1-hop callers, each enriched with {name, file, line, kind,
439
+ signature} — not just names. Capped at SEAM_PACK_NEIGHBOR_LIMIT.
440
+ callees — 1-hop callees, similarly enriched and capped.
441
+ why — WHY/HACK/NOTE/TODO/FIXME comments attached to the symbol.
442
+ Capped at SEAM_PACK_MAX_COMMENTS.
443
+ cluster_peers — the symbol's functional-area peers (from seam_clusters).
444
+ truncated — {callers, callees, comments} counts of entries dropped by caps.
445
+
446
+ When a neighbor name has no indexed declaration (external/unindexed symbol),
447
+ it is silently skipped in callers/callees — not an error.
448
+
449
+ Use this before modifying a symbol to get everything you need in one call:
450
+ location, enriched neighbors, rationale comments, and functional area — without
451
+ the five separate seam_context / seam_why / seam_context-on-each-neighbor calls.
452
+
453
+ Set verbose=false to omit heavy enrichment fields (decorators, is_exported,
454
+ visibility, qualified_name) from target and every neighbor. signature and core
455
+ fields are always kept. verbose=true (default) is byte-identical to pre-Phase-8.
456
+
457
+ Returns {found: false} when the symbol is not in the index (same contract as
458
+ seam_context). Fails (isError) with INVALID_INPUT when symbol is blank/whitespace.
459
+ """
460
+ return _finalize(handle_seam_context_pack(conn, symbol, root, verbose=verbose))
461
+
462
+ @mcp.tool()
463
+ def seam_flows(entry: str | None = None) -> Any:
464
+ """Discover execution flows — how the program actually runs, end to end.
465
+
466
+ With no argument: returns {"entry_points": [{name, kind, file, reach}]} —
467
+ the codebase's top execution starting points (call-graph roots ranked by
468
+ how many symbols they reach downstream). On a real repo these are the CLI
469
+ commands, web routes, MCP handlers, and main() — derived structurally, no AI.
470
+
471
+ With an entry name: returns that entry point's flow tree — a depth/breadth-
472
+ capped, cycle-safe expansion of what it calls, transitively:
473
+ entry, kind, file
474
+ steps — nested [{name, kind, file, line, confidence, children,
475
+ truncated}] following the call chain forward
476
+ total_steps — number of symbols in the flow
477
+ truncated — True if depth/breadth caps cut part of the tree
478
+
479
+ Use this to answer "how does feature X work?" in ONE call instead of reading
480
+ the entry file and chasing every callee by hand. Start with no argument to
481
+ see the entry points, then drill into the one you care about.
482
+
483
+ Each symbol appears once (first reach wins — cycle-safe). Step confidence
484
+ uses the fast name-count resolver; use seam_impact/seam_trace for import-
485
+ promoted confidence. Returns {found: false} when the entry name is unknown.
486
+ """
487
+ return _finalize(handle_seam_flows(conn, root, entry=entry))
488
+
489
+ @mcp.tool()
490
+ def seam_structure(
491
+ path: str | None = None,
492
+ depth: int | None = None,
493
+ nodes: int | None = None,
494
+ symbols: bool = False,
495
+ ) -> Any:
496
+ """Get the whole-repository directory/file/container structure tree.
497
+
498
+ By default this is a MODULE/AREA OVERVIEW — a nested tree of:
499
+ dir nodes — directories in the repository hierarchy
500
+ file nodes — indexed source files under each directory
501
+ container nodes — class/interface/type symbols within each file
502
+
503
+ Standalone module-level functions and method/member symbols are rolled up into
504
+ counts rather than listed as nodes — this keeps the tree a compact "what are the
505
+ main modules?" skeleton instead of an exhaustive symbol dump. Pass symbols=True to
506
+ also list standalone functions under each file (for a detailed structural view).
507
+
508
+ Each node carries:
509
+ kind: 'dir' | 'file' | 'container' | 'function'
510
+ name: display name (dir basename, file name, symbol name)
511
+ path: repo-root-relative path; null for container and function nodes
512
+ symbol_count: total symbol rows in this subtree
513
+ area: functional-area label from cluster data (null if no clustering)
514
+ children: child nodes
515
+ members: count of method/member rows rolled into this container (0 for non-containers)
516
+ truncated: count of nodes omitted by depth/node caps (0 = nothing trimmed)
517
+
518
+ Optional scoping and bounds (Slice 3):
519
+ path: Scope the tree to a subdirectory. A relative path resolves against the
520
+ repo root (NOT the server cwd); an absolute path is honoured as-is.
521
+ An unknown or out-of-tree path degrades to {found: false} — never an error.
522
+ depth: Maximum nesting depth (root=0). Nodes beyond this depth are dropped
523
+ and counted in `truncated`. Defaults to SEAM_STRUCTURE_MAX_DEPTH (8).
524
+ nodes: Maximum total non-root nodes. Excess nodes are dropped BFS-order (closest
525
+ to root survive) and counted in `truncated`. 0 = unlimited.
526
+ Defaults to SEAM_STRUCTURE_MAX_NODES (2000).
527
+ symbols: When true, also list standalone module-level functions as nodes under
528
+ each file. Default false = compact module/area overview (dirs, files,
529
+ classes only). Turn on for a detailed per-symbol structural view.
530
+
531
+ Use this to get a structural overview before diving into a specific file or
532
+ symbol, or to understand how files and containers are organized across the repo.
533
+
534
+ Returns {found: false} when the (scoped) tree has no symbols — an empty/not-yet
535
+ indexed repo, or a scope path that matches no indexed files.
536
+ """
537
+ # Pass the scope path string straight through: handle_seam_structure /
538
+ # build_structure resolve a relative path against `root`, not the server cwd.
539
+ scope_path = Path(path) if path else None
540
+ result = handle_seam_structure(
541
+ conn,
542
+ root,
543
+ path=scope_path,
544
+ max_depth=depth,
545
+ max_nodes=nodes,
546
+ include_functions=symbols,
547
+ )
548
+ # Normalize a genuinely-empty (scoped) tree to the not-found sentinel so
549
+ # _finalize emits {found: false} — matching every sibling read tool and the
550
+ # docstring contract. The CLI keeps rendering the (possibly empty) tree itself.
551
+ tree = result["tree"]
552
+ if not tree["children"] and tree["symbol_count"] == 0:
553
+ return _finalize(None)
554
+ return _finalize(result)
555
+
556
+ return mcp