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
@@ -0,0 +1,816 @@
1
+ """Schema migration functions for the Seam SQLite index.
2
+
3
+ Each function is a guarded, idempotent migration that advances the DB schema
4
+ by one version. All take an open sqlite3.Connection and commit their own work.
5
+
6
+ Design rules (shared with db.py):
7
+ - Each migration is version-guarded: reads schema_version and short-circuits
8
+ if already at or beyond the target version.
9
+ - Each migration commits its own transaction (BEGIN IMMEDIATE / COMMIT).
10
+ - Each migration raises RuntimeError on failure so the caller knows the DB is
11
+ in a bad state rather than silently continuing.
12
+ - No external dependencies: stdlib only (sqlite3, logging). Leaf module.
13
+ """
14
+
15
+ import logging
16
+ import sqlite3
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def _run_migration_v1_to_v2(conn: sqlite3.Connection) -> None:
22
+ """Guarded migration: add edges.confidence column if this is a v1 database.
23
+
24
+ Idempotent: if the column already exists (v2+ db or fresh db), PRAGMA table_info
25
+ detects it and the migration is skipped silently.
26
+
27
+ When the column is absent (v1 db), the ALTER TABLE runs exactly once, the
28
+ schema_version is bumped to '2', and ONE info log is emitted recommending a
29
+ re-index (old edges carry DEFAULT 'INFERRED' — conservative, not high-trust).
30
+
31
+ Raises RuntimeError on failure so the caller knows the DB is in a bad state
32
+ rather than silently continuing with a broken schema.
33
+ """
34
+ try:
35
+ col_names = {row["name"] for row in conn.execute("PRAGMA table_info(edges)").fetchall()}
36
+ if "confidence" not in col_names:
37
+ # v1 db: add column. Legacy edges are conservatively INFERRED (not high-trust);
38
+ # extract under the new resolver via a re-index for accurate tags.
39
+ conn.execute("ALTER TABLE edges ADD COLUMN confidence TEXT NOT NULL DEFAULT 'INFERRED'")
40
+ conn.execute("UPDATE metadata SET value = '2' WHERE key = 'schema_version'")
41
+ # Commit this migration's own work so the chain is order-independent and
42
+ # the ALTER does not rely on a later migration's commit to persist.
43
+ conn.commit()
44
+ logger.info(
45
+ "Migrated Seam index v1->v2 (added edges.confidence). Existing edges marked "
46
+ "INFERRED; run 'seam init' to re-index for accurate confidence tags."
47
+ )
48
+ except Exception as exc: # noqa: BLE001
49
+ raise RuntimeError(
50
+ "Seam DB migration v1->v2 failed; run 'seam init' to rebuild the index"
51
+ ) from exc
52
+
53
+
54
+ def _run_migration_v2_to_v3(conn: sqlite3.Connection) -> None:
55
+ """Guarded migration: bump schema_version from 2 to 3 (adds comments table).
56
+
57
+ The comments table itself is created by the schema script's CREATE TABLE IF NOT EXISTS,
58
+ which runs on every init_db call before this function is reached. This guard only
59
+ bumps schema_version exactly once, and logs an info message telling the user to
60
+ re-index to populate the new table.
61
+
62
+ Idempotent: if schema_version >= 3 already, the migration is skipped silently.
63
+ Fail-loud: raises RuntimeError on any error so the caller knows the DB is bad.
64
+ """
65
+ try:
66
+ row = conn.execute("SELECT value FROM metadata WHERE key = 'schema_version'").fetchone()
67
+ version = int(row["value"]) if row else 0
68
+
69
+ if version < 3:
70
+ # The comments table was already created by the schema script above.
71
+ # Just bump the version and inform the operator.
72
+ conn.execute("UPDATE metadata SET value = '3' WHERE key = 'schema_version'")
73
+ conn.commit()
74
+ logger.info(
75
+ "Migrated Seam index v%d->v3 (added comments table). Existing indexes "
76
+ "have no comments; run 'seam init' to populate semantic comment data.",
77
+ version,
78
+ )
79
+ except Exception as exc: # noqa: BLE001
80
+ raise RuntimeError(
81
+ "Seam DB migration v2->v3 failed; run 'seam init' to rebuild the index"
82
+ ) from exc
83
+
84
+
85
+ def _run_migration_v3_to_v4(conn: sqlite3.Connection) -> None:
86
+ """Guarded migration: add clusters table + symbols.cluster_id column (v3 → v4).
87
+
88
+ The clusters table is created by the schema script's CREATE TABLE IF NOT EXISTS,
89
+ which runs on every init_db call before this function is reached.
90
+ This guard handles the additive ALTER TABLE for cluster_id on existing symbols
91
+ tables, and bumps schema_version to '4' exactly once.
92
+
93
+ WHY: symbols tables from v1-v3 were created WITHOUT cluster_id. Since
94
+ CREATE TABLE IF NOT EXISTS is idempotent (it skips if the table exists),
95
+ we must ALTER the existing table to add the column. The schema script's
96
+ CREATE TABLE includes cluster_id for new databases, but existing databases
97
+ need the ALTER TABLE path here.
98
+
99
+ Idempotent: if schema_version >= 4 already, skipped silently.
100
+ Fail-loud: raises RuntimeError on any error so the caller knows the DB is bad.
101
+ """
102
+ try:
103
+ row = conn.execute("SELECT value FROM metadata WHERE key = 'schema_version'").fetchone()
104
+ version = int(row["value"]) if row else 0
105
+
106
+ if version < 4:
107
+ # Add cluster_id column to symbols if absent (existing DBs won't have it).
108
+ col_names = {r["name"] for r in conn.execute("PRAGMA table_info(symbols)").fetchall()}
109
+ if "cluster_id" not in col_names:
110
+ conn.execute("ALTER TABLE symbols ADD COLUMN cluster_id INTEGER")
111
+
112
+ # The clusters table is already created by the schema script above;
113
+ # just bump the version and inform the operator.
114
+ conn.execute("UPDATE metadata SET value = '4' WHERE key = 'schema_version'")
115
+ conn.commit()
116
+ logger.info(
117
+ "Migrated Seam index v%d->v4 (added clusters table + symbols.cluster_id). "
118
+ "Run 'seam init' to compute cluster assignments.",
119
+ version,
120
+ )
121
+ except Exception as exc: # noqa: BLE001
122
+ raise RuntimeError(
123
+ "Seam DB migration v3->v4 failed; run 'seam init' to rebuild the index"
124
+ ) from exc
125
+
126
+
127
+ def _run_migration_v4_to_v5(conn: sqlite3.Connection) -> None:
128
+ """Guarded migration: add Phase 4 node-enrichment columns + rebuild FTS (v4 → v5).
129
+
130
+ Steps (all guarded — idempotent):
131
+ 1. ALTER TABLE symbols ADD COLUMN for the 5 new fields (signature, decorators,
132
+ is_exported, visibility, qualified_name). All nullable; existing rows default NULL.
133
+ 2. DROP symbols_fts virtual table and its 3 sync triggers. FTS5 columns cannot
134
+ be altered, so the only way to add "signature" is to drop and recreate.
135
+ 3. CREATE the new symbols_fts virtual table with columns (name, docstring, signature).
136
+ 4. Recreate the 3 sync triggers (symbols_ai, symbols_ad, symbols_au) for the new
137
+ FTS column set.
138
+ 5. Repopulate FTS from the symbols content table (INSERT INTO symbols_fts).
139
+ 6. Parity check: assert count(symbols_fts) == count(symbols) after repopulation.
140
+ 7. Bump schema_version to '5'.
141
+
142
+ ATOMICITY: all structural changes (ALTERs, DROP, CREATE, INSERT) and the version bump
143
+ run inside a SINGLE explicit transaction using BEGIN IMMEDIATE / COMMIT / ROLLBACK.
144
+ Every DDL statement uses conn.execute(), NOT conn.executescript — the batch method
145
+ issues an implicit COMMIT before running, which would make each DDL non-transactional.
146
+
147
+ WHY explicit BEGIN IMMEDIATE:
148
+ Python's sqlite3 module with the default isolation_level wraps DML in implicit
149
+ transactions, but DDL statements (DROP TABLE, CREATE TABLE) cause implicit commits
150
+ of the current Python-managed transaction. To make DDL transactional we must issue
151
+ an explicit BEGIN before any DDL and ROLLBACK on failure. This guarantees that if
152
+ the process dies or raises mid-migration, the DROP TABLE is rolled back and the
153
+ DB is left at v4 with the old FTS intact — not in a half-migrated state.
154
+
155
+ Guarded: runs only when the stored version is < 5.
156
+ Idempotent: each ALTER TABLE is guarded by a PRAGMA table_info check; DROP IF EXISTS
157
+ prevents double-drop; re-running is safe (no-op when already at v5).
158
+
159
+ Raises RuntimeError on any failure so the caller knows the DB is in a bad state.
160
+ """
161
+ row = conn.execute("SELECT value FROM metadata WHERE key = 'schema_version'").fetchone()
162
+ version = int(row["value"]) if row else 0
163
+
164
+ if version >= 5:
165
+ return # Already at v5 or newer — skip migration entirely.
166
+
167
+ # Capture symbol count BEFORE starting the transaction for observability log.
168
+ symbol_count = conn.execute("SELECT COUNT(*) FROM symbols").fetchone()[0]
169
+ logger.info(
170
+ "Starting v%d->v5 migration — rebuilding FTS5 for %d symbols "
171
+ "(adding signature column). DO NOT interrupt.",
172
+ version,
173
+ symbol_count,
174
+ )
175
+
176
+ # Start an explicit transaction BEFORE any DDL (DROP/CREATE TABLE/triggers).
177
+ # Python sqlite3's implicit transaction management commits pending state on DDL;
178
+ # an explicit BEGIN makes the DDL itself transactional so ROLLBACK reverts it.
179
+ conn.execute("BEGIN IMMEDIATE")
180
+ try:
181
+ # Step 1: Add the 5 new columns if they don't already exist.
182
+ # Each ALTER is individually guarded so repeated calls don't fail on "duplicate column name".
183
+ col_names = {r["name"] for r in conn.execute("PRAGMA table_info(symbols)").fetchall()}
184
+
185
+ new_cols = [
186
+ ("signature", "TEXT"),
187
+ ("decorators", "TEXT"), # JSON-encoded list of strings
188
+ ("is_exported", "INTEGER"), # 0/1/NULL — SQLite stores bool as integer
189
+ ("visibility", "TEXT"),
190
+ ("qualified_name", "TEXT"),
191
+ ]
192
+ for col, col_type in new_cols:
193
+ if col not in col_names:
194
+ conn.execute(f"ALTER TABLE symbols ADD COLUMN {col} {col_type}")
195
+
196
+ # Step 2: Drop the old FTS5 table and its triggers.
197
+ # FTS5 virtual tables cannot be altered (no ALTER VIRTUAL TABLE support in SQLite),
198
+ # so adding the 'signature' column requires a full DROP + CREATE cycle. Each DDL
199
+ # uses conn.execute() individually — the batch alternative would issue an implicit
200
+ # COMMIT, breaking the surrounding BEGIN IMMEDIATE transaction.
201
+ conn.execute("DROP TRIGGER IF EXISTS symbols_ai")
202
+ conn.execute("DROP TRIGGER IF EXISTS symbols_ad")
203
+ conn.execute("DROP TRIGGER IF EXISTS symbols_au")
204
+ conn.execute("DROP TABLE IF EXISTS symbols_fts")
205
+
206
+ # Step 3: Recreate symbols_fts with the new column set (name, docstring, signature).
207
+ conn.execute("""
208
+ CREATE VIRTUAL TABLE symbols_fts USING fts5(
209
+ name,
210
+ docstring,
211
+ signature,
212
+ content='symbols',
213
+ content_rowid='id'
214
+ )
215
+ """)
216
+
217
+ # Step 4: Recreate the 3 sync triggers (INSERT/DELETE/UPDATE) for the new column set.
218
+ # Individual execute() calls for the same reason as step 2: the batch alternative
219
+ # commits implicitly, breaking the surrounding BEGIN IMMEDIATE transaction.
220
+ conn.execute("""
221
+ CREATE TRIGGER symbols_ai AFTER INSERT ON symbols BEGIN
222
+ INSERT INTO symbols_fts(rowid, name, docstring, signature)
223
+ VALUES (new.id, new.name, new.docstring, new.signature);
224
+ END
225
+ """)
226
+ conn.execute("""
227
+ CREATE TRIGGER symbols_ad AFTER DELETE ON symbols BEGIN
228
+ INSERT INTO symbols_fts(symbols_fts, rowid, name, docstring, signature)
229
+ VALUES ('delete', old.id, old.name, old.docstring, old.signature);
230
+ END
231
+ """)
232
+ conn.execute("""
233
+ CREATE TRIGGER symbols_au AFTER UPDATE ON symbols BEGIN
234
+ INSERT INTO symbols_fts(symbols_fts, rowid, name, docstring, signature)
235
+ VALUES ('delete', old.id, old.name, old.docstring, old.signature);
236
+ INSERT INTO symbols_fts(rowid, name, docstring, signature)
237
+ VALUES (new.id, new.name, new.docstring, new.signature);
238
+ END
239
+ """)
240
+
241
+ # Step 5: Repopulate FTS from the symbols content table.
242
+ # Pre-migration rows have signature=NULL — FTS5 treats NULL as an empty token
243
+ # list for that column, so existing symbols are still searchable by name/docstring.
244
+ conn.execute("""
245
+ INSERT INTO symbols_fts(rowid, name, docstring, signature)
246
+ SELECT id, name, docstring, signature FROM symbols
247
+ """)
248
+
249
+ # Step 6: Parity check — abort and roll back if FTS wasn't fully populated.
250
+ # A partial repopulation (e.g. interrupted INSERT) silently drops search hits:
251
+ # FTS would return results but miss any symbol whose row was never inserted.
252
+ # Failing here ensures the ROLLBACK restores the old FTS rather than leaving
253
+ # the index in a silently degraded state.
254
+ fts_count = conn.execute("SELECT COUNT(*) FROM symbols_fts").fetchone()[0]
255
+ if fts_count != symbol_count:
256
+ raise RuntimeError(
257
+ f"FTS repopulation parity mismatch: symbols={symbol_count}, "
258
+ f"symbols_fts={fts_count}. Run 'seam init' to rebuild the index."
259
+ )
260
+ logger.info(
261
+ "v4->v5 migration: FTS repopulated with %d rows (parity OK).",
262
+ fts_count,
263
+ )
264
+
265
+ # Step 7: Bump schema_version to '5' as the very last step before commit.
266
+ # Placing this last ensures the version is only advanced when all structural
267
+ # changes have succeeded — a process crash between step 6 and here is safe
268
+ # because the ROLLBACK in the except block reverts everything, leaving the DB
269
+ # at v4 so this migration reruns cleanly on the next connect().
270
+ conn.execute("UPDATE metadata SET value = '5' WHERE key = 'schema_version'")
271
+ conn.execute("COMMIT")
272
+
273
+ logger.info(
274
+ "Migrated Seam index v%d->v5 (added Phase 4 node enrichment fields: "
275
+ "signature, decorators, is_exported, visibility, qualified_name; "
276
+ "FTS5 rebuilt to index signature). Run 'seam init' to populate new fields.",
277
+ version,
278
+ )
279
+ except Exception as exc: # noqa: BLE001
280
+ # Roll back all structural changes (DROP, CREATE, ALTER, INSERT) so the DB
281
+ # is left at v4 with the old FTS intact — not in a half-migrated state.
282
+ try:
283
+ conn.execute("ROLLBACK")
284
+ except Exception: # noqa: BLE001
285
+ pass # Best-effort rollback; if this fails the DB may need a full reinit.
286
+ raise RuntimeError(
287
+ "Seam DB migration v4->v5 failed; run 'seam init' to rebuild the index"
288
+ ) from exc
289
+
290
+
291
+ def _run_migration_v5_to_v6(conn: sqlite3.Connection) -> None:
292
+ """Guarded migration: add import_mappings table (v5 → v6).
293
+
294
+ Additive-only: creates import_mappings table and two indexes if absent.
295
+ Does NOT backfill existing rows — only `seam init` / watcher re-index
296
+ populates mappings. Until then, resolution silently falls back to the
297
+ name-count rule (documented gotcha, mirrors the Phase 4 backfill caveat).
298
+
299
+ Steps:
300
+ 1. CREATE TABLE IF NOT EXISTS import_mappings (idempotent).
301
+ 2. CREATE INDEX IF NOT EXISTS for file_id and local_name (idempotent).
302
+ 3. Bump schema_version to '6'.
303
+
304
+ Guarded: runs only when stored version < 6.
305
+ Idempotent: CREATE TABLE IF NOT EXISTS / CREATE INDEX IF NOT EXISTS are safe
306
+ on repeated calls. The version guard prevents double-bumping.
307
+ Fresh-DB-safe: a brand-new DB seeded with schema_version='6' returns early.
308
+
309
+ Uses BEGIN IMMEDIATE / COMMIT for atomicity (consistent with v4→v5 pattern).
310
+ Raises RuntimeError on failure so caller knows the DB is in a bad state.
311
+ """
312
+ try:
313
+ row = conn.execute("SELECT value FROM metadata WHERE key='schema_version'").fetchone()
314
+ version = int(row["value"]) if row else 0
315
+
316
+ if version >= 6:
317
+ return # Already at v6 or newer — no-op.
318
+
319
+ conn.execute("BEGIN IMMEDIATE")
320
+ try:
321
+ # Step 1: Create import_mappings table (CREATE TABLE IF NOT EXISTS is idempotent).
322
+ conn.execute("""
323
+ CREATE TABLE IF NOT EXISTS import_mappings (
324
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
325
+ file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
326
+ local_name TEXT NOT NULL,
327
+ exported_name TEXT NOT NULL,
328
+ source_module TEXT NOT NULL,
329
+ is_default INTEGER NOT NULL DEFAULT 0,
330
+ is_namespace INTEGER NOT NULL DEFAULT 0,
331
+ is_wildcard INTEGER NOT NULL DEFAULT 0,
332
+ line INTEGER NOT NULL
333
+ )
334
+ """)
335
+
336
+ # Step 2: Create indexes (CREATE INDEX IF NOT EXISTS is idempotent).
337
+ conn.execute("""
338
+ CREATE INDEX IF NOT EXISTS idx_import_mappings_file_id
339
+ ON import_mappings(file_id)
340
+ """)
341
+ conn.execute("""
342
+ CREATE INDEX IF NOT EXISTS idx_import_mappings_local_name
343
+ ON import_mappings(local_name)
344
+ """)
345
+
346
+ # Step 3: Bump schema_version to '6' as the last step before commit.
347
+ conn.execute("UPDATE metadata SET value = '6' WHERE key = 'schema_version'")
348
+ conn.execute("COMMIT")
349
+
350
+ logger.info(
351
+ "Migrated Seam index v%d->v6 (added import_mappings table). "
352
+ "Run 'seam init' to populate import mappings for existing files.",
353
+ version,
354
+ )
355
+ except Exception as exc: # noqa: BLE001
356
+ try:
357
+ conn.execute("ROLLBACK")
358
+ except Exception: # noqa: BLE001
359
+ pass
360
+ raise RuntimeError(
361
+ "Seam DB migration v5->v6 failed; run 'seam init' to rebuild the index"
362
+ ) from exc
363
+ except RuntimeError:
364
+ raise
365
+ except Exception as exc: # noqa: BLE001
366
+ raise RuntimeError(
367
+ "Seam DB migration v5->v6 failed; run 'seam init' to rebuild the index"
368
+ ) from exc
369
+
370
+
371
+ def _run_migration_v6_to_v7(conn: sqlite3.Connection) -> None:
372
+ """Guarded migration: add embeddings table (v6 → v7).
373
+
374
+ Additive-only: creates the embeddings table if absent.
375
+ Does NOT backfill — the table is populated ONLY by `seam init --semantic`.
376
+ Until then, read paths that check for embeddings will find an empty table
377
+ and degrade gracefully to FTS5-only (documented gotcha).
378
+
379
+ Steps:
380
+ 1. CREATE TABLE IF NOT EXISTS embeddings (idempotent).
381
+ 2. Bump schema_version to '7'.
382
+
383
+ Guarded: runs only when stored version < 7.
384
+ Idempotent: CREATE TABLE IF NOT EXISTS is safe on repeated calls.
385
+ The version guard prevents double-bumping.
386
+ Fresh-DB-safe: a brand-new DB seeded with schema_version='7' returns early.
387
+
388
+ Uses BEGIN IMMEDIATE / COMMIT for atomicity (consistent with v5→v6 pattern).
389
+ Raises RuntimeError on failure so caller knows the DB is in a bad state.
390
+ """
391
+ try:
392
+ row = conn.execute("SELECT value FROM metadata WHERE key='schema_version'").fetchone()
393
+ version = int(row["value"]) if row else 0
394
+
395
+ if version >= 7:
396
+ return # Already at v7 or newer — no-op.
397
+
398
+ conn.execute("BEGIN IMMEDIATE")
399
+ try:
400
+ # Step 1: Create embeddings table (CREATE TABLE IF NOT EXISTS is idempotent).
401
+ # symbol_id is PRIMARY KEY (one embedding per symbol row) and a FK to
402
+ # symbols(id) with ON DELETE CASCADE so re-indexing a file automatically
403
+ # removes stale embeddings for deleted/replaced symbol rows.
404
+ # Vector stored as float32 bytes (numpy.array(..., dtype=np.float32).tobytes()).
405
+ conn.execute("""
406
+ CREATE TABLE IF NOT EXISTS embeddings (
407
+ symbol_id INTEGER PRIMARY KEY REFERENCES symbols(id) ON DELETE CASCADE,
408
+ model TEXT NOT NULL,
409
+ dim INTEGER NOT NULL,
410
+ vector BLOB NOT NULL
411
+ )
412
+ """)
413
+
414
+ # Step 2: Bump schema_version to '7' as the last step before commit.
415
+ conn.execute("UPDATE metadata SET value = '7' WHERE key = 'schema_version'")
416
+ conn.execute("COMMIT")
417
+
418
+ logger.info(
419
+ "Migrated Seam index v%d->v7 (added embeddings table for semantic search). "
420
+ "Run 'seam init --semantic' to populate embeddings.",
421
+ version,
422
+ )
423
+ except Exception as exc: # noqa: BLE001
424
+ try:
425
+ conn.execute("ROLLBACK")
426
+ except Exception: # noqa: BLE001
427
+ pass
428
+ raise RuntimeError(
429
+ "Seam DB migration v6->v7 failed; run 'seam init' to rebuild the index"
430
+ ) from exc
431
+ except RuntimeError:
432
+ raise
433
+ except Exception as exc: # noqa: BLE001
434
+ raise RuntimeError(
435
+ "Seam DB migration v6->v7 failed; run 'seam init' to rebuild the index"
436
+ ) from exc
437
+
438
+
439
+ def _run_migration_v7_to_v8(conn: sqlite3.Connection) -> None:
440
+ """Guarded migration: add clusters.cohesion column (v7 → v8).
441
+
442
+ Additive-only: adds a nullable REAL column to the clusters table if absent.
443
+ Does NOT backfill — cohesion stays NULL on existing cluster rows until the
444
+ next `seam init` recomputes clusters (which writes the value). Until then,
445
+ the search rescore path treats NULL cohesion as "no bonus" (byte-identical
446
+ ranking), so the additive migration changes nothing for existing indexes.
447
+
448
+ Steps:
449
+ 1. ALTER TABLE clusters ADD COLUMN cohesion REAL (guarded by table_info).
450
+ 2. Bump schema_version to '8'.
451
+
452
+ Guarded: runs only when stored version < 8.
453
+ Idempotent: the PRAGMA table_info check skips the ALTER when the column
454
+ already exists; the version guard prevents double-bumping.
455
+ Fresh-DB-safe: a brand-new DB seeded with schema_version='8' returns early.
456
+
457
+ Uses BEGIN IMMEDIATE / COMMIT for atomicity (consistent with v6→v7 pattern).
458
+ Raises RuntimeError on failure so caller knows the DB is in a bad state.
459
+ """
460
+ try:
461
+ row = conn.execute("SELECT value FROM metadata WHERE key='schema_version'").fetchone()
462
+ version = int(row["value"]) if row else 0
463
+
464
+ if version >= 8:
465
+ return # Already at v8 or newer — no-op.
466
+
467
+ conn.execute("BEGIN IMMEDIATE")
468
+ try:
469
+ # Step 1: add the nullable cohesion column if it does not already exist.
470
+ col_names = {
471
+ r["name"] for r in conn.execute("PRAGMA table_info(clusters)").fetchall()
472
+ }
473
+ if "cohesion" not in col_names:
474
+ conn.execute("ALTER TABLE clusters ADD COLUMN cohesion REAL")
475
+
476
+ # Step 2: bump schema_version to '8' as the last step before commit.
477
+ conn.execute("UPDATE metadata SET value = '8' WHERE key = 'schema_version'")
478
+ conn.execute("COMMIT")
479
+
480
+ logger.info(
481
+ "Migrated Seam index v%d->v8 (added clusters.cohesion column). "
482
+ "Run 'seam init' to populate cohesion for existing clusters.",
483
+ version,
484
+ )
485
+ except Exception as exc: # noqa: BLE001
486
+ try:
487
+ conn.execute("ROLLBACK")
488
+ except Exception: # noqa: BLE001
489
+ pass
490
+ raise RuntimeError(
491
+ "Seam DB migration v7->v8 failed; run 'seam init' to rebuild the index"
492
+ ) from exc
493
+ except RuntimeError:
494
+ raise
495
+ except Exception as exc: # noqa: BLE001
496
+ raise RuntimeError(
497
+ "Seam DB migration v7->v8 failed; run 'seam init' to rebuild the index"
498
+ ) from exc
499
+
500
+
501
+ def _run_migration_v8_to_v9(conn: sqlite3.Connection) -> None:
502
+ """Guarded migration: add symbols.entry_score column (v8 → v9).
503
+
504
+ Additive-only: adds a nullable REAL column to the symbols table if absent.
505
+ Does NOT backfill — entry_score stays NULL on existing symbol rows until the
506
+ next `seam init` re-indexes (upsert_file computes the score from the file
507
+ path pattern + decorator text). Until then, list_entry_points() treats NULL
508
+ entry_score as the neutral baseline (1.0), so the additive migration changes
509
+ nothing for existing indexes (byte-identical ranking).
510
+
511
+ Steps:
512
+ 1. ALTER TABLE symbols ADD COLUMN entry_score REAL (guarded by table_info).
513
+ 2. Bump schema_version to '9'.
514
+
515
+ Guarded: runs only when stored version < 9.
516
+ Idempotent: the PRAGMA table_info check skips the ALTER when the column
517
+ already exists; the version guard prevents double-bumping.
518
+ Fresh-DB-safe: a brand-new DB seeded with schema_version='9' returns early.
519
+
520
+ Uses BEGIN IMMEDIATE / COMMIT for atomicity (consistent with v7→v8 pattern).
521
+ Raises RuntimeError on failure so caller knows the DB is in a bad state.
522
+ """
523
+ try:
524
+ row = conn.execute("SELECT value FROM metadata WHERE key='schema_version'").fetchone()
525
+ version = int(row["value"]) if row else 0
526
+
527
+ if version >= 9:
528
+ return # Already at v9 or newer — no-op.
529
+
530
+ conn.execute("BEGIN IMMEDIATE")
531
+ try:
532
+ # Step 1: add the nullable entry_score column if it does not already exist.
533
+ col_names = {
534
+ r["name"] for r in conn.execute("PRAGMA table_info(symbols)").fetchall()
535
+ }
536
+ if "entry_score" not in col_names:
537
+ conn.execute("ALTER TABLE symbols ADD COLUMN entry_score REAL")
538
+
539
+ # Step 2: bump schema_version to '9' as the last step before commit.
540
+ conn.execute("UPDATE metadata SET value = '9' WHERE key = 'schema_version'")
541
+ conn.execute("COMMIT")
542
+
543
+ logger.info(
544
+ "Migrated Seam index v%d->v9 (added symbols.entry_score column). "
545
+ "Run 'seam init' to populate entry_score for framework entry-point ranking.",
546
+ version,
547
+ )
548
+ except Exception as exc: # noqa: BLE001
549
+ try:
550
+ conn.execute("ROLLBACK")
551
+ except Exception: # noqa: BLE001
552
+ pass
553
+ raise RuntimeError(
554
+ "Seam DB migration v8->v9 failed; run 'seam init' to rebuild the index"
555
+ ) from exc
556
+ except RuntimeError:
557
+ raise
558
+ except Exception as exc: # noqa: BLE001
559
+ raise RuntimeError(
560
+ "Seam DB migration v8->v9 failed; run 'seam init' to rebuild the index"
561
+ ) from exc
562
+
563
+
564
+ def _run_migration_v9_to_v10(conn: sqlite3.Connection) -> None:
565
+ """Guarded migration: add edges.receiver column (v9 → v10).
566
+
567
+ Tier B B1 addition: captures the raw receiver expression text from attribute-call
568
+ edges (e.g., `recv.method()` → receiver='recv'). This enables later receiver-type
569
+ inference (Tier B slices B2+) without requiring another schema change.
570
+
571
+ Additive-only: adds a nullable TEXT column to the edges table if absent.
572
+ Does NOT backfill — receiver stays NULL on existing rows until re-index.
573
+ The null-contract mirrors Phase 4/5 fields: NULL means "not yet captured" or
574
+ "not applicable" (import edges, bare calls), not "has no receiver".
575
+
576
+ Steps:
577
+ 1. ALTER TABLE edges ADD COLUMN receiver TEXT (guarded by table_info).
578
+ 2. Bump schema_version to '10'.
579
+
580
+ Guarded: runs only when stored version < 10.
581
+ Idempotent: the PRAGMA table_info check skips the ALTER when the column
582
+ already exists; the version guard prevents double-bumping.
583
+ Fresh-DB-safe: a brand-new DB seeded with schema_version='10' returns early.
584
+ Uses BEGIN IMMEDIATE / COMMIT for atomicity (consistent with v8→v9 pattern).
585
+ Raises RuntimeError on failure so caller knows the DB is in a bad state.
586
+ """
587
+ try:
588
+ row = conn.execute("SELECT value FROM metadata WHERE key='schema_version'").fetchone()
589
+ version = int(row["value"]) if row else 0
590
+
591
+ if version >= 10:
592
+ return # Already at v10 or newer — no-op.
593
+
594
+ conn.execute("BEGIN IMMEDIATE")
595
+ try:
596
+ # Step 1: add the nullable receiver column if it does not already exist.
597
+ # Guarded by table_info to make repeated calls safe (idempotent).
598
+ col_names = {
599
+ r["name"] for r in conn.execute("PRAGMA table_info(edges)").fetchall()
600
+ }
601
+ if "receiver" not in col_names:
602
+ conn.execute("ALTER TABLE edges ADD COLUMN receiver TEXT")
603
+
604
+ # Step 2: bump schema_version to '10' as the last step before commit.
605
+ # Placing this last guarantees the version is only advanced when the
606
+ # structural change has succeeded.
607
+ conn.execute("UPDATE metadata SET value = '10' WHERE key = 'schema_version'")
608
+ conn.execute("COMMIT")
609
+
610
+ logger.info(
611
+ "Migrated Seam index v%d->v10 (added edges.receiver column for Tier B "
612
+ "receiver-type inference). Run 'seam init' to populate receiver for "
613
+ "existing Python attribute calls.",
614
+ version,
615
+ )
616
+ except Exception as exc: # noqa: BLE001
617
+ try:
618
+ conn.execute("ROLLBACK")
619
+ except Exception: # noqa: BLE001
620
+ pass
621
+ raise RuntimeError(
622
+ "Seam DB migration v9->v10 failed; run 'seam init' to rebuild the index"
623
+ ) from exc
624
+ except RuntimeError:
625
+ raise
626
+ except Exception as exc: # noqa: BLE001
627
+ raise RuntimeError(
628
+ "Seam DB migration v9->v10 failed; run 'seam init' to rebuild the index"
629
+ ) from exc
630
+
631
+
632
+ def _run_migration_v11_to_v12(conn: sqlite3.Connection) -> None:
633
+ """Guarded migration: add edges.synthesized_by column (v11 → v12).
634
+
635
+ Edge-synthesis post-pass addition: captures which synthesis channel produced an
636
+ edge (e.g. 'interface-override'). NULL = statically extracted by a parser;
637
+ a channel name string = synthesized by the post-pass. Provenance is derived:
638
+ synthesized_by IS NOT NULL ⟹ heuristic. No separate provenance column needed.
639
+
640
+ Additive-only: adds a nullable TEXT column to the edges table if absent.
641
+ Does NOT backfill — synthesized_by stays NULL on existing rows. Synthesized
642
+ edges appear only after the next full `seam init` (explicit backfill, same
643
+ null-contract as prior enrichment columns). Toggling SEAM_EDGE_SYNTHESIS
644
+ requires a re-index to take effect.
645
+
646
+ Steps:
647
+ 1. ALTER TABLE edges ADD COLUMN synthesized_by TEXT (guarded by table_info).
648
+ 2. Bump schema_version to '12'.
649
+
650
+ Guarded: runs only when stored version < 12.
651
+ Idempotent: the PRAGMA table_info check skips the ALTER when the column
652
+ already exists; the version guard prevents double-bumping.
653
+ Fresh-DB-safe: a brand-new DB seeded with schema_version='12' returns early.
654
+ Uses BEGIN IMMEDIATE / COMMIT for atomicity (consistent with v9→v10 pattern).
655
+ Raises RuntimeError on failure so caller knows the DB is in a bad state.
656
+ """
657
+ try:
658
+ row = conn.execute("SELECT value FROM metadata WHERE key='schema_version'").fetchone()
659
+ version = int(row["value"]) if row else 0
660
+
661
+ if version >= 12:
662
+ return # Already at v12 or newer — no-op.
663
+
664
+ conn.execute("BEGIN IMMEDIATE")
665
+ try:
666
+ # Step 1: add the nullable synthesized_by column if it does not already exist.
667
+ # Guarded by table_info to make repeated calls safe (idempotent).
668
+ col_names = {
669
+ r["name"] for r in conn.execute("PRAGMA table_info(edges)").fetchall()
670
+ }
671
+ if "synthesized_by" not in col_names:
672
+ conn.execute("ALTER TABLE edges ADD COLUMN synthesized_by TEXT")
673
+
674
+ # Step 2: bump schema_version to '12' as the last step before commit.
675
+ # Placing this last guarantees the version is only advanced when the
676
+ # structural change has succeeded.
677
+ conn.execute("UPDATE metadata SET value = '12' WHERE key = 'schema_version'")
678
+ conn.execute("COMMIT")
679
+
680
+ logger.info(
681
+ "Migrated Seam index v%d->v12 (added edges.synthesized_by column for "
682
+ "edge-synthesis post-pass provenance). Run 'seam init' to populate "
683
+ "synthesized_by for synthesized edges.",
684
+ version,
685
+ )
686
+ except Exception as exc: # noqa: BLE001
687
+ try:
688
+ conn.execute("ROLLBACK")
689
+ except Exception: # noqa: BLE001
690
+ pass
691
+ raise RuntimeError(
692
+ "Seam DB migration v11->v12 failed; run 'seam init' to rebuild the index"
693
+ ) from exc
694
+ except RuntimeError:
695
+ raise
696
+ except Exception as exc: # noqa: BLE001
697
+ raise RuntimeError(
698
+ "Seam DB migration v11->v12 failed; run 'seam init' to rebuild the index"
699
+ ) from exc
700
+
701
+
702
+ def _run_migration_v10_to_v11(conn: sqlite3.Connection) -> None:
703
+ """Guarded migration: add symbols.search_text + a 4th symbols_fts column (v10 → v11).
704
+
705
+ Tier D #12: identifier compound-split tokenization. Adds a nullable search_text column
706
+ holding camelCase/snake_case-split tokens, surfaced as a 4th FTS5 column so that a
707
+ natural-language query matches a camelCase identifier (the unicode61 tokenizer does not
708
+ split camelCase). Changing the FTS column set requires DROPping and recreating the
709
+ symbols_fts virtual table + its 3 sync triggers, then a 'rebuild'.
710
+
711
+ Additive-only / null-contract: search_text is NULL on existing rows after the rebuild —
712
+ full split-token recall arrives only after a `seam init` re-index. Until then, search
713
+ behaves exactly as before (the name/docstring/signature columns are unchanged).
714
+
715
+ Steps:
716
+ 1. ALTER TABLE symbols ADD COLUMN search_text TEXT (guarded by table_info).
717
+ 2. DROP the 3 sync triggers + symbols_fts; recreate symbols_fts with 4 columns and
718
+ the 3 triggers; INSERT ... VALUES('rebuild') to repopulate from the content table.
719
+ 3. Bump schema_version to '11'.
720
+
721
+ Guarded (version < 11), idempotent (table_info guard + version guard), fresh-DB-safe
722
+ (a new DB seeded with '11' returns early). BEGIN IMMEDIATE / COMMIT for atomicity.
723
+ Raises RuntimeError on failure.
724
+ """
725
+ try:
726
+ row = conn.execute("SELECT value FROM metadata WHERE key='schema_version'").fetchone()
727
+ version = int(row["value"]) if row else 0
728
+
729
+ if version >= 11:
730
+ return # Already at v11 or newer — no-op.
731
+
732
+ conn.execute("BEGIN IMMEDIATE")
733
+ try:
734
+ # Step 1: add the nullable search_text column if absent (idempotent).
735
+ col_names = {
736
+ r["name"] for r in conn.execute("PRAGMA table_info(symbols)").fetchall()
737
+ }
738
+ if "search_text" not in col_names:
739
+ conn.execute("ALTER TABLE symbols ADD COLUMN search_text TEXT")
740
+
741
+ # Step 2: rebuild the FTS table with the new column set. The FTS5 column list
742
+ # is fixed at creation, so a column addition requires a full drop+recreate.
743
+ # Triggers must be dropped first (they reference the old column list).
744
+ # The 'rebuild' below re-tokenizes every symbol; on a large index this is a
745
+ # multi-second write under BEGIN IMMEDIATE, and it fires on the FIRST process to
746
+ # open the DB after upgrade (often a read command). Log up front so a momentarily
747
+ # slow `seam query`/`start` right after upgrading is explainable, not mysterious.
748
+ logger.info(
749
+ "Migrating Seam index v%d->v11: rebuilding FTS index for camelCase search "
750
+ "(one-time; may take a moment on a large index)...",
751
+ version,
752
+ )
753
+ conn.execute("DROP TRIGGER IF EXISTS symbols_ai")
754
+ conn.execute("DROP TRIGGER IF EXISTS symbols_ad")
755
+ conn.execute("DROP TRIGGER IF EXISTS symbols_au")
756
+ conn.execute("DROP TABLE IF EXISTS symbols_fts")
757
+ conn.execute(
758
+ """
759
+ CREATE VIRTUAL TABLE symbols_fts USING fts5(
760
+ name, docstring, signature, search_text,
761
+ content='symbols', content_rowid='id'
762
+ )
763
+ """
764
+ )
765
+ conn.execute(
766
+ """
767
+ CREATE TRIGGER symbols_ai AFTER INSERT ON symbols BEGIN
768
+ INSERT INTO symbols_fts(rowid, name, docstring, signature, search_text)
769
+ VALUES (new.id, new.name, new.docstring, new.signature, new.search_text);
770
+ END
771
+ """
772
+ )
773
+ conn.execute(
774
+ """
775
+ CREATE TRIGGER symbols_ad AFTER DELETE ON symbols BEGIN
776
+ INSERT INTO symbols_fts(symbols_fts, rowid, name, docstring, signature, search_text)
777
+ VALUES ('delete', old.id, old.name, old.docstring, old.signature, old.search_text);
778
+ END
779
+ """
780
+ )
781
+ conn.execute(
782
+ """
783
+ CREATE TRIGGER symbols_au AFTER UPDATE ON symbols BEGIN
784
+ INSERT INTO symbols_fts(symbols_fts, rowid, name, docstring, signature, search_text)
785
+ VALUES ('delete', old.id, old.name, old.docstring, old.signature, old.search_text);
786
+ INSERT INTO symbols_fts(rowid, name, docstring, signature, search_text)
787
+ VALUES (new.id, new.name, new.docstring, new.signature, new.search_text);
788
+ END
789
+ """
790
+ )
791
+ # Repopulate the FTS index from the content table (search_text = NULL on old rows).
792
+ conn.execute("INSERT INTO symbols_fts(symbols_fts) VALUES('rebuild')")
793
+
794
+ # Step 3: bump version last — only advance once the rebuild succeeded.
795
+ conn.execute("UPDATE metadata SET value = '11' WHERE key = 'schema_version'")
796
+ conn.execute("COMMIT")
797
+
798
+ logger.info(
799
+ "Migrated Seam index v%d->v11 (added symbols.search_text + 4th symbols_fts "
800
+ "column for camelCase search recall). Run 'seam init' to populate search_text.",
801
+ version,
802
+ )
803
+ except Exception as exc: # noqa: BLE001
804
+ try:
805
+ conn.execute("ROLLBACK")
806
+ except Exception: # noqa: BLE001
807
+ pass
808
+ raise RuntimeError(
809
+ "Seam DB migration v10->v11 failed; run 'seam init' to rebuild the index"
810
+ ) from exc
811
+ except RuntimeError:
812
+ raise
813
+ except Exception as exc: # noqa: BLE001
814
+ raise RuntimeError(
815
+ "Seam DB migration v10->v11 failed; run 'seam init' to rebuild the index"
816
+ ) from exc