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,56 @@
1
+ """Watcher subprocess entry point.
2
+
3
+ Launched by `seam start` as: python -m seam.watcher <db_path> <root_path>
4
+
5
+ Passing paths as argv (not interpolated into a `python -c` string) avoids
6
+ breakage on paths containing spaces, quotes, or backslashes. Logs go to
7
+ <db_path.parent>/watcher.log so failures are visible (the parent redirects
8
+ this process's stdio to DEVNULL).
9
+ """
10
+
11
+ import logging
12
+ import signal
13
+ import sys
14
+ import time
15
+ from pathlib import Path
16
+
17
+ import seam.config as config
18
+ from seam.watcher.daemon import SeamWatcher
19
+
20
+
21
+ def main(argv: list[str]) -> int:
22
+ if len(argv) != 2:
23
+ print("usage: python -m seam.watcher <db_path> <root_path>", file=sys.stderr)
24
+ return 2
25
+
26
+ db_path = Path(argv[0])
27
+ root_path = Path(argv[1])
28
+
29
+ logging.basicConfig(
30
+ level=getattr(logging, config.SEAM_LOG_LEVEL, logging.INFO),
31
+ filename=str(db_path.parent / "watcher.log"),
32
+ format="%(asctime)s %(levelname)s %(name)s: %(message)s",
33
+ )
34
+
35
+ watcher = SeamWatcher(db_path=db_path, root_path=root_path)
36
+ watcher.start()
37
+
38
+ # Stop cleanly on signals so the PID file and observer are torn down.
39
+ def _stop(signum: int, frame: object) -> None: # noqa: ARG001
40
+ watcher.stop()
41
+ sys.exit(0)
42
+
43
+ signal.signal(signal.SIGTERM, _stop)
44
+ signal.signal(signal.SIGINT, _stop)
45
+
46
+ # Block forever; events are handled on the observer's threads.
47
+ try:
48
+ while True:
49
+ time.sleep(3600)
50
+ finally:
51
+ watcher.stop()
52
+ return 0
53
+
54
+
55
+ if __name__ == "__main__":
56
+ raise SystemExit(main(sys.argv[1:]))
seam/watcher/daemon.py ADDED
@@ -0,0 +1,237 @@
1
+ """File watcher daemon — watchdog-based auto-sync for the Seam index.
2
+
3
+ SeamWatcher extends watchdog's FileSystemEventHandler.
4
+
5
+ Debounce strategy:
6
+ - Each source file that fires an event gets its own threading.Timer.
7
+ - If the same file fires again before the timer expires the old timer is
8
+ cancelled and a new one is started. This prevents hammering the indexer
9
+ on rapid editor saves (which often write the file several times per save).
10
+ - Debounce delay is read from config.SEAM_DEBOUNCE_MS (default 500 ms).
11
+
12
+ DB lifetime:
13
+ - One SQLite connection is opened at start() and closed at stop().
14
+ - All debounce callbacks run on the watchdog Observer thread pool, so the
15
+ connection is shared across callbacks. SQLite handles this safely because
16
+ watchdog's per-directory threads serialise event delivery.
17
+
18
+ PID file:
19
+ - start() writes os.getpid() to <db_path.parent>/watcher.pid
20
+ - stop() removes it; seam status reads it to show watcher state.
21
+ """
22
+
23
+ import logging
24
+ import os
25
+ import sqlite3
26
+ import threading
27
+ from pathlib import Path
28
+ from typing import Any
29
+
30
+ from watchdog.events import FileSystemEvent, FileSystemEventHandler
31
+ from watchdog.observers import Observer
32
+
33
+ import seam.config as config
34
+ from seam.indexer.db import connect, delete_file
35
+ from seam.indexer.pipeline import index_one_file
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ class SeamWatcher(FileSystemEventHandler):
41
+ """Watchdog event handler with per-file debounced re-indexing.
42
+
43
+ Usage:
44
+ watcher = SeamWatcher(db_path=..., root_path=...)
45
+ watcher.start()
46
+ # ... file system events are handled automatically ...
47
+ watcher.stop()
48
+ """
49
+
50
+ def __init__(self, db_path: Path, root_path: Path) -> None:
51
+ super().__init__()
52
+
53
+ self._db_path = db_path
54
+ # Resolve the root so watchdog event paths (watched-root + relative)
55
+ # match the resolved-absolute paths that `seam init` stores. Without
56
+ # this, macOS /var vs /private/var divergence produces duplicate
57
+ # `files` rows and orphaned symbols on the watcher path.
58
+ self._root_path = root_path.resolve()
59
+
60
+ # DB connection — opened by start(), closed by stop()
61
+ self._conn: sqlite3.Connection | None = None
62
+
63
+ # Watchdog observer — created at start(), joined at stop()
64
+ # Typed as Any because watchdog's Observer is a module-level variable
65
+ # resolved at runtime (not a stable class in stub), causing mypy error.
66
+ self._observer: Any = None
67
+
68
+ # Per-file debounce timers. key = str(absolute path), value = active Timer.
69
+ self._timers: dict[str, threading.Timer] = {}
70
+
71
+ # Lock to serialise access to _timers dict (safety for multi-threaded watchdog)
72
+ self._timer_lock = threading.Lock()
73
+
74
+ # Debounce delay in seconds (config is in ms)
75
+ self._debounce_s: float = config.SEAM_DEBOUNCE_MS / 1000.0
76
+
77
+ # PID file path sits next to the DB
78
+ self._pid_file: Path = db_path.parent / "watcher.pid"
79
+
80
+ # ── Lifecycle ─────────────────────────────────────────────────────────────
81
+
82
+ def start(self) -> None:
83
+ """Start the watchdog Observer in a background thread.
84
+
85
+ Opens the DB connection, writes the PID file, schedules the observer
86
+ to watch root_path recursively, then starts the observer thread.
87
+ """
88
+ logger.info("SeamWatcher starting — root=%s db=%s", self._root_path, self._db_path)
89
+
90
+ # Open via connect() (sets foreign_keys=ON + busy_timeout) with
91
+ # check_same_thread=False so the observer thread and timer callbacks
92
+ # can share it. foreign_keys MUST be on here — this is the only
93
+ # long-lived WRITER, so without it re-index/delete don't cascade.
94
+ self._conn = connect(self._db_path, check_same_thread=False)
95
+
96
+ # Write PID file so `seam status` can report watcher state
97
+ self._pid_file.write_text(str(os.getpid()))
98
+ logger.debug("PID file written: %s (pid=%d)", self._pid_file, os.getpid())
99
+
100
+ # Create and start the watchdog observer
101
+ self._observer = Observer()
102
+ self._observer.schedule(self, str(self._root_path), recursive=True)
103
+ self._observer.start()
104
+
105
+ logger.info("SeamWatcher started (debounce=%.0f ms)", config.SEAM_DEBOUNCE_MS)
106
+
107
+ def stop(self) -> None:
108
+ """Stop the watchdog Observer, cancel pending timers, and clean up.
109
+
110
+ Idempotent — safe to call multiple times.
111
+ """
112
+ logger.info("SeamWatcher stopping")
113
+
114
+ # Cancel any pending debounce timers
115
+ with self._timer_lock:
116
+ for timer in self._timers.values():
117
+ timer.cancel()
118
+ self._timers.clear()
119
+
120
+ # Stop the watchdog observer
121
+ if self._observer is not None:
122
+ self._observer.stop()
123
+ self._observer.join()
124
+ self._observer = None
125
+ logger.debug("Observer stopped and joined")
126
+
127
+ # Close DB connection
128
+ if self._conn is not None:
129
+ self._conn.close()
130
+ self._conn = None
131
+ logger.debug("DB connection closed")
132
+
133
+ # Remove PID file
134
+ try:
135
+ self._pid_file.unlink(missing_ok=True)
136
+ logger.debug("PID file removed: %s", self._pid_file)
137
+ except OSError as exc:
138
+ logger.warning("Could not remove PID file %s: %s", self._pid_file, exc)
139
+
140
+ logger.info("SeamWatcher stopped")
141
+
142
+ # ── watchdog event handlers ────────────────────────────────────────────────
143
+
144
+ def on_created(self, event: FileSystemEvent) -> None:
145
+ """Handle file creation — schedule a debounced index of the new file."""
146
+ if event.is_directory:
147
+ return
148
+ self._schedule_index(Path(str(event.src_path)))
149
+
150
+ def on_modified(self, event: FileSystemEvent) -> None:
151
+ """Handle file modification — schedule a debounced re-index."""
152
+ if event.is_directory:
153
+ return
154
+ self._schedule_index(Path(str(event.src_path)))
155
+
156
+ def on_deleted(self, event: FileSystemEvent) -> None:
157
+ """Handle file deletion — remove from the DB immediately (no debounce needed)."""
158
+ if event.is_directory:
159
+ return
160
+
161
+ path = Path(str(event.src_path))
162
+
163
+ # Cancel any pending index timer for this file (it no longer exists)
164
+ with self._timer_lock:
165
+ timer = self._timers.pop(str(path), None)
166
+ if timer is not None:
167
+ timer.cancel()
168
+
169
+ logger.debug("File deleted: %s — removing from DB", path)
170
+ self._do_delete(path)
171
+
172
+ # ── Internal helpers ───────────────────────────────────────────────────────
173
+
174
+ def _schedule_index(self, path: Path) -> None:
175
+ """Cancel any existing timer for path, start a fresh debounce timer.
176
+
177
+ Only schedules for extensions in config.SEAM_LANGUAGE_MAP.
178
+ """
179
+ if path.suffix.lower() not in config.SEAM_LANGUAGE_MAP:
180
+ return # not a file we care about
181
+
182
+ key = str(path)
183
+ with self._timer_lock:
184
+ # Cancel existing timer for this path if any
185
+ existing = self._timers.pop(key, None)
186
+ if existing is not None:
187
+ existing.cancel()
188
+
189
+ # Schedule new timer — callback runs after debounce delay
190
+ timer = threading.Timer(self._debounce_s, self._do_index, args=(path,))
191
+ self._timers[key] = timer
192
+ timer.start()
193
+ logger.debug("Debounce timer set for %s (%.0f ms)", path.name, config.SEAM_DEBOUNCE_MS)
194
+
195
+ def _do_index(self, path: Path) -> None:
196
+ """Debounce callback: index a single file into the DB.
197
+
198
+ Runs on a timer thread. Guards against missing conn (watcher was stopped).
199
+ """
200
+ # Remove the timer entry (it has already fired)
201
+ with self._timer_lock:
202
+ self._timers.pop(str(path), None)
203
+
204
+ conn = self._conn
205
+ if conn is None:
206
+ logger.debug("_do_index: watcher stopped, skipping %s", path)
207
+ return
208
+
209
+ # Runs on a Timer thread with no caller — any unhandled exception would
210
+ # die silently and leave the index stale without warning. Log it.
211
+ try:
212
+ logger.debug("Indexing file: %s", path)
213
+ result = index_one_file(conn, path)
214
+ if result is None:
215
+ logger.debug("Skipped %s (unsupported/binary/error)", path)
216
+ else:
217
+ logger.info("Indexed %s — %d symbols, %d edges", path.name, result[0], result[1])
218
+ except Exception: # noqa: BLE001 — never let a re-index failure crash the daemon silently
219
+ logger.exception("re-index failed for %s — index may be stale", path)
220
+
221
+ def _do_delete(self, path: Path) -> None:
222
+ """Remove a file's index entries from the DB.
223
+
224
+ Runs on the watchdog event thread. Guards against missing conn.
225
+ """
226
+ conn = self._conn
227
+ if conn is None:
228
+ logger.debug("_do_delete: watcher stopped, skipping %s", path)
229
+ return
230
+
231
+ # Runs on the watchdog event thread — guard so a delete failure is
232
+ # logged rather than killing the event dispatch thread silently.
233
+ try:
234
+ delete_file(conn, path)
235
+ logger.info("Removed %s from index", path.name)
236
+ except Exception: # noqa: BLE001
237
+ logger.exception("delete failed for %s", path)
@@ -0,0 +1,318 @@
1
+ Metadata-Version: 2.4
2
+ Name: seam-code
3
+ Version: 0.3.0
4
+ Summary: Local code intelligence MCP server for AI agents
5
+ Project-URL: Homepage, https://github.com/Catafal/seam
6
+ Project-URL: Repository, https://github.com/Catafal/seam
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Keywords: ai-agents,code-intelligence,mcp,sqlite,tree-sitter
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Topic :: Software Development :: Libraries
17
+ Requires-Python: >=3.12
18
+ Requires-Dist: tomlkit>=0.15.0
19
+ Requires-Dist: tree-sitter-c-sharp>=0.23.5
20
+ Requires-Dist: tree-sitter-c>=0.24.2
21
+ Requires-Dist: tree-sitter-cpp>=0.23.4
22
+ Requires-Dist: tree-sitter-go>=0.25.0
23
+ Requires-Dist: tree-sitter-java>=0.23.5
24
+ Requires-Dist: tree-sitter-javascript>=0.22.0
25
+ Requires-Dist: tree-sitter-php>=0.24.1
26
+ Requires-Dist: tree-sitter-python>=0.22.0
27
+ Requires-Dist: tree-sitter-ruby>=0.23.1
28
+ Requires-Dist: tree-sitter-rust>=0.24.2
29
+ Requires-Dist: tree-sitter-swift>=0.7.3
30
+ Requires-Dist: tree-sitter-typescript>=0.22.0
31
+ Requires-Dist: tree-sitter>=0.22.0
32
+ Requires-Dist: typer>=0.12.0
33
+ Requires-Dist: watchdog>=4.0.0
34
+ Provides-Extra: semantic
35
+ Requires-Dist: fastembed>=0.4; extra == 'semantic'
36
+ Provides-Extra: server
37
+ Requires-Dist: mcp>=1.0.0; extra == 'server'
38
+ Provides-Extra: web
39
+ Requires-Dist: fastapi>=0.110; extra == 'web'
40
+ Requires-Dist: uvicorn>=0.30; extra == 'web'
41
+ Description-Content-Type: text/markdown
42
+
43
+ # Seam
44
+
45
+ <p align="center">
46
+ <img src="docs/assets/seam-hero.png" alt="A codebase as a graph: hexagonal symbol nodes connected by typed edges around a central indexed core; a queried symbol (chartreuse) traces through the core to an impacted dependent (magenta)" width="100%">
47
+ </p>
48
+
49
+ **Local code intelligence for AI agents.** Index a codebase once; agents query its structure instead of re-discovering it with `grep` every session.
50
+
51
+ `v0.3.0` · 12 languages · 12 MCP tools · SQLite-backed · **zero network calls at query time** · gate-green (~3,055 tests)
52
+
53
+ ---
54
+
55
+ ## The problem
56
+
57
+ An AI coding agent starts every session blind. To answer "what breaks if I change `init_db`?" it greps for the name, opens each hit, reads the surrounding code, follows the imports, and reconstructs the call graph by hand — spending thousands of tokens rebuilding structural knowledge that was true last session and the session before.
58
+
59
+ That structure is *computable*. A parser already knows that `index_one_file` calls `init_db`, that `Server` holds a `Client`, that `UserView` implements `Renderable`. Seam computes it once, stores it in a local SQLite graph, and exposes it over a handful of MCP tools so the agent **asks instead of greps**.
60
+
61
+ ```text
62
+ Without Seam: "what calls init_db?" → grep → 14 files → read each → trace imports → ~30k tokens, often wrong
63
+ With Seam: seam_impact init_db → blast radius by risk tier → ~4.5k tokens, graph-accurate
64
+ ```
65
+
66
+ The win compounds: every structural question — callers, blast radius, call paths, functional areas, which tests to run — is one tool call against a graph that stays fresh automatically.
67
+
68
+ ## The mental model
69
+
70
+ Think of Seam as **a compiler's symbol table and call graph for your whole repository**, kept fresh in the background and exposed over MCP and a CLI.
71
+
72
+ ```text
73
+ source files tree-sitter .seam/seam.db
74
+ (12 languages) ─────────▶ structural ─────▶ SQLite + FTS5 ◀── file watcher
75
+ parsing (symbols + edges (debounced
76
+ + clusters + FTS) re-index)
77
+
78
+ ┌───────────────┴───────────────┐
79
+ ▼ ▼
80
+ MCP server (stdio) CLI read commands
81
+ 12 read-only tools query / impact / trace …
82
+ │ │
83
+ └──────────────┬────────────────┘
84
+
85
+ AI agent (Claude Code · Cursor · Codex)
86
+ ```
87
+
88
+ Three properties define it:
89
+
90
+ - **Indexed once, fresh forever.** `seam init` builds the graph; an optional `watchdog` daemon re-indexes edited files in the background. The agent never thinks about staleness — and graph-traversal tools surface a banner if the index *is* stale.
91
+ - **100% local.** The index is a per-project SQLite file (`.seam/seam.db`). The read path makes **no network calls** — no API keys, no cloud, no telemetry.
92
+ - **A graph, not a search box.** Symbols are nodes; calls, imports, inheritance, composition, and field access are typed edges. Every answer is graph traversal, not string matching.
93
+
94
+ ---
95
+
96
+ ## Quickstart
97
+
98
+ > **Not yet on PyPI.** The distribution name will be `seam-mcp` (the name `seam` belongs to an unrelated package); the import package and the `seam` command keep the short name. Install from source for now.
99
+
100
+ ```bash
101
+ git clone <repo-url> && cd seam
102
+ uv sync # CLI only — no MCP server, no semantic search
103
+ uv sync --extra server # + the MCP server (`seam start`) — adds the `mcp` package
104
+ uv sync --extra semantic # + semantic search (fastembed, ONNX/CPU, no torch, ~67 MB model on first run)
105
+ uv sync --extra web # + the Seam Explorer web UI (FastAPI)
106
+ # everything: uv sync --extra server --extra semantic --extra web
107
+ ```
108
+
109
+ ### Use it from the CLI (no server needed)
110
+
111
+ Every read command queries the SQLite index directly — the full feature set works with no MCP server running:
112
+
113
+ ```bash
114
+ cd /path/to/your/project
115
+ uv run seam init # index the project (writes .seam/seam.db)
116
+ uv run seam search "auth token" # full-text (hybrid semantic when enabled)
117
+ uv run seam query "verify user login" # concept search + 1-hop graph expansion
118
+ uv run seam context authenticate_user # 360° view: callers, callees, cluster, signature
119
+ uv run seam impact authenticate_user # blast radius by risk tier
120
+ uv run seam structure # whole-repo directory/container map
121
+ # also: trace · changes · why · clusters · affected · flows · pack · status · sync
122
+ ```
123
+
124
+ ### Wire it to an AI agent
125
+
126
+ `seam install` defaults to writing a **token-lean CLI playbook** into the repo so the agent queries via the `seam` CLI (cheaper than MCP — the CLI's `--quiet` mode is ~14× leaner than the leanest MCP call, and there's no ~6k-token standing tool-schema cost). It renders into each agent's cheapest native mechanism: a Claude Code skill, a Cursor agent-requested rule, and an `AGENTS.md` block for Codex.
127
+
128
+ ```bash
129
+ uv run seam install # CLI guidance for Claude Code (skill + CLAUDE.md hook)
130
+ uv run seam install --target all # guidance for Claude Code + Cursor + Codex
131
+ uv run seam install --with-mcp # ALSO wire the MCP server (needs the `server` extra)
132
+ uv run seam install --print-config # preview everything, write nothing
133
+ uv run seam uninstall # reverse it (removes guidance + MCP config)
134
+ ```
135
+
136
+ The guidance teaches the agent the escalation ladder (`--quiet` → `--json --lean` → full `--json`), how to keep the index fresh (`seam init` / `seam sync`), and when to reach for each command. It's idempotent (a marker-delimited block in `AGENTS.md`/`CLAUDE.md`, never duplicated, foreign content preserved) and reversible.
137
+
138
+ Prefer native tool-calling? Add `--with-mcp` (install the `server` extra first). To wire MCP by hand, add to `.mcp.json` at the repo root:
139
+
140
+ ```json
141
+ {
142
+ "mcpServers": {
143
+ "seam": { "type": "stdio", "command": "seam", "args": ["start", "/path/to/your/project"] }
144
+ }
145
+ }
146
+ ```
147
+
148
+ ---
149
+
150
+ ## The 12 MCP tools
151
+
152
+ Grouped by the question an agent is asking. Every tool is **read-only**; the server never writes the index.
153
+
154
+ ### Find code
155
+
156
+ | Tool | Answers | Key args |
157
+ |------|---------|----------|
158
+ | `seam_search` | "Where is text X mentioned?" — FTS5 over names + docstrings + signatures, with fuzzy fallback; hybrid keyword+semantic when enabled. | `text`, `limit`, `semantic` |
159
+ | `seam_query` | "Find all code related to concept X." — FTS5 match + 1-hop graph expansion, rescored by name/path/cluster signals. | `concept`, `limit`, `semantic` |
160
+
161
+ ### Understand a symbol
162
+
163
+ | Tool | Answers | Key args |
164
+ |------|---------|----------|
165
+ | `seam_context` | "Show me everything about symbol X." — callers, callees, signature, cluster, `field_readers`/`field_writers`. Resolves bare/qualified/class names and merges homonyms. | `symbol` or `uid` |
166
+ | `seam_context_pack` | "Give me a paste-ready bundle for X." — `seam_context` + WHY comments + enriched neighbors + cluster peers in one call; neighbors ranked by relevance to the seed. | `symbol` |
167
+ | `seam_why` | "Why is this code like this?" — the `WHY`/`HACK`/`NOTE`/`TODO`/`FIXME` comments. | `symbol`, `path` |
168
+
169
+ ### Assess change risk
170
+
171
+ | Tool | Answers | Key args |
172
+ |------|---------|----------|
173
+ | `seam_impact` | "What breaks if I change X?" — blast radius bucketed into risk tiers, with provenance, summary counts, and a hard byte/entry budget. | `target`, `direction`, `max_depth`, `limit`, `max_bytes` |
174
+ | `seam_changes` | "Is my current diff risky?" — git diff → changed symbols → overall risk level. | `scope`, `base_ref` |
175
+ | `seam_affected` | "Which tests should I run?" — changed files → impacted test files via reverse-dependency traversal. | `changed_files`, `depth` |
176
+
177
+ ### Navigate the graph
178
+
179
+ | Tool | Answers | Key args |
180
+ |------|---------|----------|
181
+ | `seam_trace` | "How does X reach Y?" — shortest call/dependency path, hop by hop, with edge kind + confidence. | `source`, `target`, `max_depth` |
182
+ | `seam_flows` | "Where does execution start, and where does it go?" — entry points ranked by reach, or one entry's forward call-chain tree. | `entry` (optional) |
183
+
184
+ ### Map the repository
185
+
186
+ | Tool | Answers | Key args |
187
+ |------|---------|----------|
188
+ | `seam_clusters` | "What are the functional areas?" — Louvain communities (semantic coupling), or the members of one. | `cluster_id` (optional) |
189
+ | `seam_structure` | "How is the repo laid out?" — the filesystem → directory → file → container tree with symbol counts and area labels. | `path`, `depth`, `nodes` |
190
+
191
+ > **Lean mode.** The enrichment-carrying tools (`seam_context`, `seam_trace`, `seam_impact`, `seam_context_pack`) accept `verbose=false` (CLI `--lean`) to drop heavy provenance fields and shrink the response for tight token budgets. `seam_impact` additionally supports a hard `max_bytes` ceiling and emits `next_actions` hints when results are trimmed.
192
+
193
+ ---
194
+
195
+ ## Supported languages
196
+
197
+ Twelve languages, parsed with [tree-sitter](https://tree-sitter.github.io/). All share the same tools, symbol kinds, edge kinds, and enrichment fields.
198
+
199
+ | Language | Extensions | | Language | Extensions |
200
+ |----------|-----------|-|----------|-----------|
201
+ | Python | `.py` | | C# | `.cs` |
202
+ | TypeScript | `.ts` `.tsx` | | Ruby | `.rb` |
203
+ | JavaScript | `.js` `.mjs` `.cjs` | | C | `.c` `.h` |
204
+ | Go | `.go` | | C++ | `.cpp` `.cc` `.cxx` `.c++` `.hpp` `.hh` `.hxx` |
205
+ | Rust | `.rs` | | PHP | `.php` |
206
+ | Java | `.java` | | Swift | `.swift` |
207
+
208
+ > **Kotlin is not yet supported** — the available tree-sitter-kotlin grammar mis-parses common constructs (interfaces, objects, classes with constructors). Tracked for a future release. See [`docs/adr/009-swift-support.md`](docs/adr/009-swift-support.md).
209
+ >
210
+ > Per-language extraction caveats (e.g. `.h` maps to C; C++ method visibility; Ruby dynamic visibility) are documented in [`docs/CONCEPTS.md`](docs/CONCEPTS.md#per-language-notes).
211
+
212
+ ---
213
+
214
+ ## Core concepts
215
+
216
+ A short tour — the full treatment is in [`docs/CONCEPTS.md`](docs/CONCEPTS.md).
217
+
218
+ **The graph.** Nodes are symbols (`function`, `class`, `method`, `interface`, `type`, `field`). Edges are **typed** and capture nine relationships:
219
+
220
+ | Edge kind | Captures |
221
+ |-----------|----------|
222
+ | `call` | one symbol invokes another |
223
+ | `import` | a module/symbol import |
224
+ | `extends` · `implements` | class inheritance / interface conformance |
225
+ | `instantiates` | `new Foo()` / struct-literal construction |
226
+ | `holds` | a class **stores** a typed field/property (composition / DI) |
227
+ | `uses` | a function **references** a user type as a parameter (signature coupling) |
228
+ | `reads` · `writes` | a field/property is read or written (data-flow) |
229
+
230
+ Edges are keyed by **symbol name**, not row id — this is what lets the watcher re-index one file independently without rewriting the whole graph. All traversal is kind-agnostic, so every tool picks up every edge kind automatically.
231
+
232
+ **Confidence tiers.** Each edge resolves to `EXTRACTED` (target is unambiguous), `AMBIGUOUS` (name collides — verify), or `INFERRED` (heuristic / cross-module). A multi-hop path is only as strong as its weakest hop. Each result carries `resolved_by` provenance explaining *how* the tier was decided.
233
+
234
+ **Risk tiers.** `seam_impact` and `seam_changes` bucket dependents by distance: `WILL_BREAK` (d=1, must update), `LIKELY_AFFECTED` (d=2, should test), `MAY_NEED_TESTING` (d≥3, test if critical). `seam_changes` rolls these up into `low` → `medium` → `high` → `critical`.
235
+
236
+ **Clusters.** A pure-Python Louvain pass groups symbols into functional areas by coupling. Labels are deterministic by default (`dir/file — top symbol`) or, opt-in, LLM-generated at index time only.
237
+
238
+ **Edge synthesis.** Static parsing can't see runtime polymorphism. A post-pass over the whole graph synthesizes the edges parsing structurally misses — interface→implementation fan-out, closure-collection dispatch, event-emitter handlers — tagged with their provenance channel.
239
+
240
+ **Semantic search.** Opt-in local embeddings (fastembed, ONNX on CPU) merge with FTS5 via Reciprocal Rank Fusion, so `"retry logic"` can surface `_backoff_with_jitter` even with no shared token. The model downloads once, then runs 100% locally.
241
+
242
+ **Staleness banner.** Graph-traversal tools attach an `index_status` banner when the index has drifted from disk — so an agent is never silently handed wrong blast-radius answers.
243
+
244
+ ---
245
+
246
+ ## Configuration
247
+
248
+ Everything is environment-variable driven with sensible defaults — see [`docs/CONFIGURATION.md`](docs/CONFIGURATION.md) for the full reference (~50 knobs). The few you might actually set:
249
+
250
+ | Variable | Default | Effect |
251
+ |----------|---------|--------|
252
+ | `SEAM_SEMANTIC` | `off` | Enable hybrid keyword + semantic search (needs the `semantic` extra + `seam init --semantic`). |
253
+ | `SEAM_CLUSTER_NAMING` | `deterministic` | `llm` opts into LLM cluster labels at index time (needs `SEAM_LLM_API_KEY`). Read path stays 100% local regardless. |
254
+ | `SEAM_IMPACT_MAX_BYTES` | `0` (off) | Hard character ceiling on `seam_impact` output for tight context budgets. |
255
+
256
+ Most knobs are gated `on` by default and have an `off` that restores byte-identical pre-feature behavior — a deliberate discipline so upgrades never silently change tool output.
257
+
258
+ ---
259
+
260
+ ## Seam Explorer — local visual graph UI
261
+
262
+ `seam serve` (the `[web]` extra) starts a read-only, `127.0.0.1`-only browser explorer for the index.
263
+
264
+ ```bash
265
+ uv sync --extra web
266
+ seam init # index first
267
+ seam serve # opens http://127.0.0.1:7420
268
+ seam serve --no-open --port 8000
269
+ ```
270
+
271
+ A React + TypeScript SPA (React Flow) served by FastAPI. Nothing leaves the machine. Features: command-palette search, a depth-1 caller/callee card-canvas with confidence-styled edges, lazy expand, a detail panel, an impact overlay that paints blast radius by risk tier, a trace-path highlighter, a git-changes drawer, and a whole-repo cluster constellation. All four analyses reuse the **same handlers** that power the CLI/MCP tools — a third transport, no query logic duplicated.
272
+
273
+ ---
274
+
275
+ ## Architecture
276
+
277
+ The import hierarchy is strictly layered — read flows down, never up:
278
+
279
+ ```text
280
+ cli / server / web → analysis → query → indexer / db
281
+ ```
282
+
283
+ `analysis` is built from **pure leaf modules** (no DB, no IO, never raise) — clustering, RWR neighbor ranking, byte budgeting, the truncation steer, staleness detection. This makes each piece testable in isolation and keeps the failure surface tiny.
284
+
285
+ - **Read it as a narrative:** [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — current system overview, then the full phase-by-phase history.
286
+ - **Read it as a diagram:** [`docs/architecture.html`](docs/architecture.html) — a standalone illustrated page (system pipeline, layered hierarchy, data-flow, edge-kind graph, schema).
287
+ - **Decision rationale:** [`docs/adr/`](docs/adr/) — architecture decision records.
288
+ - **Concepts in depth:** [`docs/CONCEPTS.md`](docs/CONCEPTS.md) — how and why each subsystem works.
289
+
290
+ ---
291
+
292
+ ## Design principles
293
+
294
+ These are the non-negotiables — and they are guarantees to the user, not just internal rules:
295
+
296
+ 1. **Zero external services at runtime.** The MCP read path makes no network calls. The only optional outbound call (LLM cluster naming) runs at index time, is off by default, and falls back to deterministic labels on any error.
297
+ 2. **SQLite only.** No graph DB, no vector DB to babysit, no ORM. One file per project, gitignored.
298
+ 3. **Parsers never raise.** A malformed file is skipped, not fatal. The indexer degrades gracefully; analysis leaves return empty rather than throw.
299
+ 4. **Edges are keyed by name.** This is what makes independent per-file re-indexing correct — the watcher can rewrite one file's symbols and edges without touching the rest of the graph.
300
+ 5. **Additive by default.** New features ship behind a defaulted-on switch with an `off` that is byte-identical to before. Schema changes are additive migrations that auto-run on open.
301
+
302
+ ---
303
+
304
+ ## Development
305
+
306
+ ```bash
307
+ uv sync --dev # install dev dependencies
308
+ make gate # lint (ruff) + typecheck (mypy) + tests — must be green before every commit
309
+ make fmt # format + autofix (not part of the gate)
310
+ make build-web # build the Explorer SPA into seam/_web/ (requires Node.js — build-time only)
311
+ make eval # run the recall-regression harness
312
+ ```
313
+
314
+ **Conventions:** max 200 lines/function, 1000 lines/file · all imports at top · config only from `seam/config.py` · type hints required (`X | None`, not `Optional[X]`) · tests in `tests/` mirroring the package.
315
+
316
+ ## License
317
+
318
+ See [LICENSE](LICENSE).