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/query/semantic.py ADDED
@@ -0,0 +1,339 @@
1
+ """Semantic search read path — T5.
2
+
3
+ Leaf module: imports stdlib + seam.config + seam.analysis.embeddings.
4
+ numpy is imported LAZILY inside _semantic_candidates_impl only (never at module scope).
5
+ NO server/cli imports; NO direct fastembed import.
6
+
7
+ Public API:
8
+ rrf_merge(fts_ranked, semantic_ranked, k=60) -> list[int]
9
+ Pure Reciprocal Rank Fusion of two ranked id lists.
10
+ Fully unit-testable without a model.
11
+
12
+ cosine_sim(a_bytes, b_bytes) -> float
13
+ Cosine similarity between two float32 blobs.
14
+ Returns 0.0 on zero-norm vectors or dimension mismatch (safe fallback).
15
+ Pure-Python (struct.unpack) — importable even when numpy is absent.
16
+
17
+ semantic_candidates(conn, query, *, model, limit) -> list[tuple[int, float]]
18
+ Embed query via analysis.embeddings.embed_query, compute cosine similarity
19
+ against stored vectors, return top-k (symbol_id, score) pairs.
20
+
21
+ Returns [] if:
22
+ - fastembed unavailable
23
+ - embed_query returns b'' (model error)
24
+ - stored model != configured model (never silently mix models)
25
+ - no embeddings in DB
26
+
27
+ Design decisions:
28
+ - WHY numpy is lazy here (not at module scope): this module must be importable when
29
+ only the base install (no [semantic] extra) is present — numpy may be absent. numpy
30
+ is imported inside _semantic_candidates_impl, gated by is_available() being True
31
+ (which implies fastembed and thus numpy are present). A try/except ImportError falls
32
+ back to pure-Python cosine_sim so the module degrades safely even in pathological
33
+ environments where numpy is somehow missing despite fastembed being importable.
34
+ - Model-mismatch guard: the DB stores the model name per row. If no row matches the
35
+ requested model, we treat the entire store as incompatible and return []. This
36
+ prevents silently mixing vectors from different embedding spaces (different spaces
37
+ produce meaningless cosine scores without any visible error). The check runs before
38
+ any vector load or cosine computation.
39
+ - Brute-force cosine: O(n * dim). For 1k–20k symbols × 384 dim this is
40
+ ~1–5ms with numpy — no ANN index needed at this scale. sqlite-vec is deferred.
41
+ - SEAM_SEMANTIC_SCAN_CAP bounds the SQL LIMIT so we never load more rows than
42
+ the configured cap (protects against unbounded memory use on very large indexes).
43
+ Logs at DEBUG when cap truncates the scan so operators can tune the knob.
44
+ """
45
+
46
+ import logging
47
+ import sqlite3
48
+ import struct
49
+
50
+ import seam.config as config
51
+ from seam.analysis.embeddings import embed_query, is_available
52
+
53
+ logger = logging.getLogger(__name__)
54
+
55
+
56
+ # ── rrf_merge ─────────────────────────────────────────────────────────────────
57
+
58
+
59
+ def rrf_merge(
60
+ fts_ranked: list[int],
61
+ semantic_ranked: list[int],
62
+ k: int = 60,
63
+ ) -> list[int]:
64
+ """Reciprocal Rank Fusion of two ranked id lists.
65
+
66
+ Combines a keyword-ranked list and a semantic-ranked list into a single
67
+ merged ranking using the RRF formula:
68
+
69
+ score(id) = Σ 1 / (k + rank_i(id))
70
+
71
+ where rank_i is the 1-based position of the id in list i (lists that do not
72
+ contain the id contribute 0). The merged list is sorted by score descending.
73
+
74
+ Args:
75
+ fts_ranked: Symbol ids ranked by FTS5 relevance (best first).
76
+ semantic_ranked: Symbol ids ranked by cosine similarity (best first).
77
+ k: RRF smoothing constant. Default 60 (standard value from
78
+ the original Cormack et al. paper). Higher k flattens
79
+ rank differences; lower k amplifies them.
80
+
81
+ Returns:
82
+ A deduplicated list of ids sorted by merged RRF score descending.
83
+ Ids that appear in both lists receive a higher score than ids in only one.
84
+ Stable: ties are broken by original list order (fts first, then semantic).
85
+
86
+ WHY RRF vs. score interpolation:
87
+ Score interpolation requires normalizing scores across heterogeneous systems
88
+ (BM25 vs cosine). RRF only needs rank positions, which are always comparable.
89
+ It consistently outperforms score interpolation in hybrid retrieval benchmarks
90
+ without any additional tuning. (Cormack, Clarke & Buettcher, SIGIR 2009.)
91
+ """
92
+ if not fts_ranked and not semantic_ranked:
93
+ return []
94
+
95
+ # Build a score map: id → accumulated RRF score
96
+ scores: dict[int, float] = {}
97
+
98
+ # Add contributions from the FTS list (1-based rank)
99
+ for rank, doc_id in enumerate(fts_ranked, start=1):
100
+ scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (k + rank)
101
+
102
+ # Add contributions from the semantic list
103
+ for rank, doc_id in enumerate(semantic_ranked, start=1):
104
+ scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (k + rank)
105
+
106
+ # Sort by score descending; stable sort preserves insertion order for ties,
107
+ # which effectively prioritises fts_ranked for tie-breaking (fts ids were inserted first).
108
+ return sorted(scores, key=lambda doc_id: scores[doc_id], reverse=True)
109
+
110
+
111
+ # ── cosine_sim ────────────────────────────────────────────────────────────────
112
+
113
+
114
+ def cosine_sim(a_bytes: bytes, b_bytes: bytes) -> float:
115
+ """Compute cosine similarity between two float32 byte blobs.
116
+
117
+ Uses struct.unpack (stdlib only — no numpy import here) to decode the vectors.
118
+ This keeps the module importable even when numpy is absent.
119
+
120
+ Returns:
121
+ float in [-1.0, 1.0]; 0.0 on any degenerate case (zero norm, dim mismatch,
122
+ empty input, corrupt bytes). Never raises.
123
+
124
+ WHY struct.unpack over numpy:
125
+ numpy is a fastembed transitive dep — when fastembed is absent, numpy may
126
+ also be absent. Using struct keeps this module dependency-free, ensuring
127
+ it is always importable without extras.
128
+ """
129
+ if not a_bytes or not b_bytes:
130
+ return 0.0
131
+
132
+ n_a = len(a_bytes) // 4
133
+ n_b = len(b_bytes) // 4
134
+
135
+ if n_a != n_b or n_a == 0:
136
+ return 0.0
137
+
138
+ try:
139
+ fmt = f"{n_a}f"
140
+ a = struct.unpack(fmt, a_bytes)
141
+ b = struct.unpack(fmt, b_bytes)
142
+ except struct.error:
143
+ # Corrupt bytes — degrade gracefully
144
+ return 0.0
145
+
146
+ dot = sum(ai * bi for ai, bi in zip(a, b))
147
+ norm_a = sum(ai * ai for ai in a) ** 0.5
148
+ norm_b = sum(bi * bi for bi in b) ** 0.5
149
+
150
+ if norm_a == 0.0 or norm_b == 0.0:
151
+ return 0.0
152
+
153
+ return dot / (norm_a * norm_b)
154
+
155
+
156
+ # ── semantic_candidates ───────────────────────────────────────────────────────
157
+
158
+
159
+ def semantic_candidates(
160
+ conn: sqlite3.Connection,
161
+ query: str,
162
+ *,
163
+ model: str,
164
+ limit: int,
165
+ ) -> list[tuple[int, float]]:
166
+ """Find the top-k symbol_ids by semantic similarity to the query.
167
+
168
+ Steps:
169
+ 1. Check fastembed availability — return [] if absent.
170
+ 2. Embed the query using analysis.embeddings.embed_query.
171
+ 3. Verify DB has embeddings for the configured model (model-mismatch guard).
172
+ 4. Load all stored vectors for the configured model.
173
+ 5. Compute cosine similarity for each stored vector vs. the query vector.
174
+ 6. Return the top-k (symbol_id, score) pairs sorted by score descending.
175
+
176
+ Returns [] if:
177
+ - fastembed is not available (graceful degradation)
178
+ - embed_query returns b'' (model load error)
179
+ - stored model != configured model (never silently mix model vectors)
180
+ - no embeddings present in the DB for the configured model
181
+ - any unexpected error occurs (logs a warning, never raises)
182
+
183
+ Args:
184
+ conn: Open SQLite connection with read access.
185
+ query: The search query string to embed.
186
+ model: FastEmbed model name (must match the model used at index time).
187
+ limit: Maximum number of (symbol_id, score) pairs to return.
188
+
189
+ Returns:
190
+ list[tuple[int, float]] — at most `limit` entries, sorted by score descending.
191
+ Each tuple is (symbol_id, cosine_score).
192
+ """
193
+ # Step 1: Fast-path when fastembed absent
194
+ if not is_available():
195
+ logger.debug("semantic_candidates: fastembed unavailable — returning []")
196
+ return []
197
+
198
+ try:
199
+ return _semantic_candidates_impl(conn, query, model=model, limit=limit)
200
+ except Exception as exc: # noqa: BLE001
201
+ logger.warning(
202
+ "semantic_candidates: unexpected error for query %r (model=%r): %s: %s — "
203
+ "falling back to FTS5-only path.",
204
+ query,
205
+ model,
206
+ type(exc).__name__,
207
+ exc,
208
+ )
209
+ return []
210
+
211
+
212
+ def _semantic_candidates_impl(
213
+ conn: sqlite3.Connection,
214
+ query: str,
215
+ *,
216
+ model: str,
217
+ limit: int,
218
+ ) -> list[tuple[int, float]]:
219
+ """Inner implementation — may raise; outer function catches everything.
220
+
221
+ WHY split: the outer guard converts any exception to [] and logs a warning,
222
+ keeping the read path non-crashing. Inner logic is cleaner without try/except.
223
+
224
+ Performance:
225
+ - SEAM_SEMANTIC_SCAN_CAP: SQL LIMIT caps how many rows are loaded.
226
+ - numpy fast path: when numpy is importable (it is a fastembed transitive dep,
227
+ so it is available whenever is_available() is True), all stored vectors are
228
+ decoded into a single (N, dim) float32 matrix and cosine is computed in one
229
+ matmul — ~1–5ms for 10k × 384-dim vs ~10–50ms for pure-Python per-row loop.
230
+ - Falls back to pure-Python cosine_sim if numpy import fails (defensive fallback).
231
+ """
232
+ # Step 2: Embed the query
233
+ query_vec = embed_query(query, model)
234
+ if not query_vec:
235
+ logger.debug(
236
+ "semantic_candidates: embed_query returned empty bytes for %r — returning []",
237
+ query,
238
+ )
239
+ return []
240
+
241
+ # Step 3: Model-mismatch guard.
242
+ # Check whether ANY embeddings row exists for the configured model.
243
+ # If only rows from a different model are present, return [] immediately — do not
244
+ # mix vectors from different embedding spaces (silently wrong scores).
245
+ model_check = conn.execute(
246
+ "SELECT COUNT(*) FROM embeddings WHERE model = ?", (model,)
247
+ ).fetchone()[0]
248
+
249
+ if model_check == 0:
250
+ # No rows for this model. Could be: (a) no embeddings at all, (b) model mismatch.
251
+ total_embeddings = conn.execute("SELECT COUNT(*) FROM embeddings").fetchone()[0]
252
+ if total_embeddings > 0:
253
+ # Model mismatch is a user misconfiguration — warn so it's visible.
254
+ logger.warning(
255
+ "semantic_candidates: model mismatch — DB has %d embedding(s) for a "
256
+ "different model; configured model=%r. "
257
+ "Run 'seam init --semantic' to rebuild embeddings with the current model.",
258
+ total_embeddings,
259
+ model,
260
+ )
261
+ else:
262
+ logger.debug(
263
+ "semantic_candidates: no embeddings in DB for model=%r — returning [].",
264
+ model,
265
+ )
266
+ return []
267
+
268
+ # Step 4: Load stored vectors for this model, bounded by SEAM_SEMANTIC_SCAN_CAP.
269
+ # The LIMIT prevents loading an unbounded number of rows on very large indexes.
270
+ scan_cap = config.SEAM_SEMANTIC_SCAN_CAP
271
+ rows = conn.execute(
272
+ "SELECT symbol_id, vector FROM embeddings WHERE model = ? LIMIT ?",
273
+ (model, scan_cap),
274
+ ).fetchall()
275
+
276
+ if not rows:
277
+ return []
278
+
279
+ # Log at DEBUG when the scan was capped (user might have more symbols than the cap).
280
+ actual_count = conn.execute(
281
+ "SELECT COUNT(*) FROM embeddings WHERE model = ?", (model,)
282
+ ).fetchone()[0]
283
+ if actual_count > scan_cap:
284
+ logger.debug(
285
+ "semantic_candidates: scan capped at %d rows (index has %d for model=%r); "
286
+ "raise SEAM_SEMANTIC_SCAN_CAP to scan all vectors.",
287
+ scan_cap,
288
+ actual_count,
289
+ model,
290
+ )
291
+
292
+ # Step 5: Compute cosine similarity — numpy fast path when available.
293
+ # numpy is a fastembed transitive dep: if is_available() is True, numpy is importable.
294
+ # The try/except ImportError is a defensive fallback for pathological environments.
295
+ try:
296
+ import numpy as np
297
+
298
+ # Decode query vector: (dim,) float32
299
+ q_arr = np.frombuffer(query_vec, dtype=np.float32) # shape (dim,)
300
+
301
+ # Build (N, dim) matrix from all stored blobs in one pass.
302
+ mat = np.stack(
303
+ [np.frombuffer(bytes(row["vector"]), dtype=np.float32) for row in rows]
304
+ ) # shape (N, dim)
305
+
306
+ sym_ids = [row["symbol_id"] for row in rows]
307
+
308
+ # Cosine similarity: (mat @ q) / (||mat|| * ||q||) per row.
309
+ # dot: shape (N,)
310
+ dots = mat @ q_arr
311
+ norms_mat = np.linalg.norm(mat, axis=1) # (N,)
312
+ norm_q = float(np.linalg.norm(q_arr))
313
+
314
+ if norm_q == 0.0:
315
+ return []
316
+
317
+ # Zero-norm rows get cosine=0 (safe: norms_mat == 0 → division → 0 via clip).
318
+ with np.errstate(invalid="ignore", divide="ignore"):
319
+ cosines = np.where(norms_mat == 0.0, 0.0, dots / (norms_mat * norm_q))
320
+
321
+ # Pair (symbol_id, score) and get top-k via argsort.
322
+ top_k_indices = np.argsort(-cosines)[:limit]
323
+ return [(int(sym_ids[i]), float(cosines[i])) for i in top_k_indices]
324
+
325
+ except ImportError:
326
+ # numpy absent (defensive fallback — should not happen when is_available() is True).
327
+ logger.debug(
328
+ "semantic_candidates: numpy not available — falling back to pure-Python cosine."
329
+ )
330
+
331
+ # Pure-Python fallback (slower but correct; keeps the function working without numpy).
332
+ scored: list[tuple[int, float]] = []
333
+ for row in rows:
334
+ score = cosine_sim(query_vec, bytes(row["vector"]))
335
+ scored.append((row["symbol_id"], score))
336
+
337
+ # Step 6: Sort by score descending, return top-k
338
+ scored.sort(key=lambda t: t[1], reverse=True)
339
+ return scored[:limit]