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/context.py ADDED
@@ -0,0 +1,293 @@
1
+ """Context-building helpers extracted from engine.py (Tier A refactor).
2
+
3
+ LEAF-ADJACENT MODULE — imported by engine.py and tests only. Imports:
4
+ - stdlib (sqlite3, json, logging)
5
+ - seam.config
6
+ - seam.query.names (edge bridging helpers)
7
+ - seam.query.clusters (cluster peers)
8
+ - seam.query.engine.decode_enrichment_fields (shared decoder)
9
+
10
+ WHY extracted from engine.py:
11
+ engine.py exceeded the 1000-line hard limit after Tier A Slice 2 added
12
+ _collect_edges_for_names, _build_merged_context_result, and _build_context_result.
13
+ Moving these three helpers here keeps engine.py as the thin orchestrator and
14
+ gives context() a dedicated home. No behavior change — pure refactor.
15
+
16
+ Context:
17
+ Seam stores method symbol names as "Class.method" (qualified) but stores call-edge
18
+ target_name as bare "method". These helpers bridge that asymmetry so callers/callees
19
+ are correctly merged even when the edge key and the symbol key don't join directly.
20
+
21
+ A3 addition: build_context_result and build_merged_context_result now include
22
+ field_readers / field_writers in the returned dict. These are populated by
23
+ collect_field_access_for_names(), which queries edges WHERE kind IN ('reads','writes')
24
+ targeting the symbol (for field seeds) or its member fields (for class seeds).
25
+ For non-field, non-class seeds both lists are [].
26
+ """
27
+
28
+ import logging
29
+ import sqlite3
30
+
31
+ from seam import config
32
+ from seam.query.clusters import cluster_peers as _cluster_peers
33
+ from seam.query.names import edge_match_names as _edge_match_names
34
+ from seam.query.names import is_container_symbol as _is_container_symbol
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+
39
+ def collect_field_access_for_names(
40
+ conn: sqlite3.Connection,
41
+ symbol_name: str,
42
+ kind: str,
43
+ ) -> list[sqlite3.Row] | list:
44
+ """Return all edges of kind='reads' or 'writes' targeting the given symbol name.
45
+
46
+ For a field seed 'Type.field': queries edges.target_name = 'Type.field' OR bare 'field'.
47
+ Returns source_name values (the methods that read/write this field).
48
+ Returns [] on error.
49
+
50
+ WHY: field access edges store target_name as the qualified 'Type.field' (when the
51
+ receiver type was inferred) OR as the bare 'field' (when the receiver was unresolvable).
52
+ We query both forms to be consistent with how callers/callees edges are looked up.
53
+
54
+ WHY kind is passed as a parameter instead of querying both 'reads' and 'writes':
55
+ the caller (collect_field_access_split) always needs the two lists separately for
56
+ seam_context to surface field_readers vs field_writers as distinct fields. A single
57
+ combined query would lose the distinction and require a second pass to re-split.
58
+ """
59
+ if not symbol_name:
60
+ return []
61
+ match_names = _edge_match_names(conn, symbol_name)
62
+ if not match_names:
63
+ return []
64
+ try:
65
+ ph = ",".join("?" * len(match_names))
66
+ rows = conn.execute(
67
+ f"SELECT DISTINCT source_name FROM edges WHERE kind=? AND target_name IN ({ph})",
68
+ [kind] + match_names,
69
+ ).fetchall()
70
+ return rows
71
+ except Exception: # noqa: BLE001
72
+ logger.debug(
73
+ "collect_field_access_for_names: DB error for %r kind=%r", symbol_name, kind,
74
+ exc_info=True,
75
+ )
76
+ return []
77
+
78
+
79
+ def collect_field_access_split(
80
+ conn: sqlite3.Connection,
81
+ symbol_name: str,
82
+ symbol_kind: str,
83
+ ) -> tuple[list[str], list[str]]:
84
+ """Return (field_readers, field_writers) for a symbol.
85
+
86
+ For kind='field': returns functions that read/write this specific field.
87
+ For kind='class'/'interface'/'type' (container): aggregates readers/writers across
88
+ all member fields bounded by SEAM_NAME_EXPANSION_CAP.
89
+ For all other kinds (function, method): returns ([], []).
90
+
91
+ Never raises.
92
+ """
93
+ try:
94
+ if symbol_kind == "field":
95
+ # Direct field seed: query edges targeting this field.
96
+ readers = sorted({
97
+ r["source_name"]
98
+ for r in collect_field_access_for_names(conn, symbol_name, "reads")
99
+ })
100
+ writers = sorted({
101
+ r["source_name"]
102
+ for r in collect_field_access_for_names(conn, symbol_name, "writes")
103
+ })
104
+ return readers, writers
105
+
106
+ if _is_container_symbol(conn, symbol_name):
107
+ # Class/interface/struct seed: aggregate readers/writers across ALL the
108
+ # class's field symbols.
109
+ #
110
+ # WHY query symbols (kind='field') directly instead of get_member_names:
111
+ # get_member_names is derived from the CALL graph (edge targets), so a field
112
+ # that is only written in __init__ and never appears as a call-edge target
113
+ # would be silently dropped from the class-level field_readers/field_writers.
114
+ # Field symbols are stored as 'Class.field', so a LIKE 'Class.%' scan over
115
+ # kind='field' rows returns every field of the class — including call-graph-
116
+ # invisible ones. Bounded by SEAM_NAME_EXPANSION_CAP (same cap as the read-path
117
+ # class fan-out) so a class with hundreds of fields can't blow up the query.
118
+ field_rows = conn.execute(
119
+ "SELECT name FROM symbols WHERE kind='field' AND name LIKE ? LIMIT ?",
120
+ (f"{symbol_name}.%", config.SEAM_NAME_EXPANSION_CAP),
121
+ ).fetchall()
122
+ qualified_members = [r["name"] for r in field_rows]
123
+
124
+ readers_set: set[str] = set()
125
+ writers_set: set[str] = set()
126
+ for qname in qualified_members:
127
+ for r in collect_field_access_for_names(conn, qname, "reads"):
128
+ readers_set.add(r["source_name"])
129
+ for r in collect_field_access_for_names(conn, qname, "writes"):
130
+ writers_set.add(r["source_name"])
131
+ return sorted(readers_set), sorted(writers_set)
132
+
133
+ # Non-field, non-class seeds: no readers/writers
134
+ return [], []
135
+
136
+ except Exception as exc: # noqa: BLE001
137
+ logger.debug(
138
+ "collect_field_access_split: failed for %r kind=%r: %r",
139
+ symbol_name,
140
+ symbol_kind,
141
+ exc,
142
+ )
143
+ return [], []
144
+
145
+
146
+ def collect_edges_for_names(
147
+ conn: sqlite3.Connection,
148
+ match_names: list[str],
149
+ ) -> tuple[set[str], set[str]]:
150
+ """Return (callers_set, callees_set) for match_names via DISTINCT edge lookups.
151
+
152
+ Used by both single-def (_build_context_result) and multi-def aggregation paths.
153
+ Per-edge confidence from the DB is preserved — the union never invents confidence.
154
+
155
+ Returns (set(), set()) for empty match_names or on DB error (defensive).
156
+ """
157
+ if not match_names:
158
+ return set(), set()
159
+ ph = ",".join("?" * len(match_names))
160
+ try:
161
+ # DISTINCT prevents duplicate caller/callee names when match_names contains
162
+ # both the qualified form ("Class.method") and the bare form ("method") — both
163
+ # can match the same edge row, so without DISTINCT the same caller appears twice.
164
+ # The set comprehension dedups at Python level too, but DISTINCT reduces fetch size.
165
+ callers = {
166
+ r["source_name"]
167
+ for r in conn.execute(
168
+ f"SELECT DISTINCT source_name FROM edges WHERE target_name IN ({ph})",
169
+ match_names,
170
+ ).fetchall()
171
+ }
172
+ callees = {
173
+ r["target_name"]
174
+ for r in conn.execute(
175
+ f"SELECT DISTINCT target_name FROM edges WHERE source_name IN ({ph})",
176
+ match_names,
177
+ ).fetchall()
178
+ }
179
+ except Exception: # noqa: BLE001
180
+ # Degrade gracefully — read path must never crash.
181
+ logger.debug(
182
+ "collect_edges_for_names: DB error for match_names=%r", match_names, exc_info=True
183
+ )
184
+ return set(), set()
185
+ return callers, callees
186
+
187
+
188
+ def build_merged_context_result(
189
+ conn: sqlite3.Connection,
190
+ def_rows: list[sqlite3.Row],
191
+ decode_enrichment_fields_fn, # type: ignore[type-arg]
192
+ ) -> dict:
193
+ """Merge callers/callees across multiple defs (Slice 2 multi-def path).
194
+
195
+ Primary def (lowest id) supplies location/kind/enrichment. ambiguous=True when >1
196
+ def or exact-name collision; False for unique bare-name resolution (1 qualified def).
197
+
198
+ decode_enrichment_fields_fn is passed in to avoid a circular import with engine.py
199
+ (engine.py defines decode_enrichment_fields and is itself the caller here).
200
+ """
201
+ primary = def_rows[0]
202
+ all_callers: set[str] = set()
203
+ all_callees: set[str] = set()
204
+ for row in def_rows:
205
+ match_names = _edge_match_names(conn, row["name"])
206
+ callers, callees = collect_edges_for_names(conn, match_names)
207
+ all_callers |= callers
208
+ all_callees |= callees
209
+
210
+ # Cluster lookup uses the primary (lowest-id) def's name: for a bare-name resolution
211
+ # like "parse" → "Parser.parse", the original bare name has no cluster row, whereas
212
+ # the qualified name does. Using primary["name"] gives the right cluster for the
213
+ # canonical representation in the index.
214
+ cluster_info = _cluster_peers(conn, primary["name"])
215
+ c_id, c_label, c_peers = cluster_info if cluster_info is not None else (None, None, [])
216
+ decoded_decorators, is_exported = decode_enrichment_fields_fn(primary)
217
+
218
+ # A3: field_readers / field_writers — populated for field and class seeds.
219
+ field_readers, field_writers = collect_field_access_split(
220
+ conn, primary["name"], primary["kind"]
221
+ )
222
+
223
+ return dict(
224
+ symbol=primary["name"],
225
+ file=primary["file"],
226
+ line=primary["start_line"],
227
+ end_line=primary["end_line"],
228
+ kind=primary["kind"],
229
+ docstring=primary["docstring"],
230
+ # Sorted so MCP consumers get a deterministic list regardless of edge insertion order.
231
+ callers=sorted(all_callers),
232
+ callees=sorted(all_callees),
233
+ ambiguous=len(def_rows) > 1,
234
+ cluster_id=c_id,
235
+ cluster_label=c_label,
236
+ cluster_peers=c_peers,
237
+ signature=primary["signature"],
238
+ decorators=decoded_decorators,
239
+ is_exported=is_exported,
240
+ visibility=primary["visibility"],
241
+ qualified_name=primary["qualified_name"],
242
+ field_readers=field_readers,
243
+ field_writers=field_writers,
244
+ )
245
+
246
+
247
+ def build_context_result(
248
+ conn: sqlite3.Connection,
249
+ row: sqlite3.Row,
250
+ *,
251
+ dup_count: int,
252
+ decode_enrichment_fields_fn, # type: ignore[type-arg]
253
+ ) -> dict:
254
+ """Single symbol row → ContextResult dict. Used by context_at and the single-def fast path.
255
+
256
+ dup_count: explicit collision count (required — no fallback to row['dup_count'] since
257
+ resolve_query_to_defs rows do not include that window column).
258
+ """
259
+ symbol_name = row["name"]
260
+ # A method stored as "Class.method" has call edges with target_name="method" (bare).
261
+ # edge_match_names expands to [qualified, bare] so both storage forms are matched —
262
+ # without this, callers of a qualified method always show up as an empty list.
263
+ callers, callees = collect_edges_for_names(conn, _edge_match_names(conn, symbol_name))
264
+ cluster_info = _cluster_peers(conn, symbol_name)
265
+ c_id, c_label, c_peers = cluster_info if cluster_info is not None else (None, None, [])
266
+ decoded_decorators, is_exported = decode_enrichment_fields_fn(row)
267
+
268
+ # A3: field_readers / field_writers — populated for field and class seeds.
269
+ field_readers, field_writers = collect_field_access_split(
270
+ conn, symbol_name, row["kind"]
271
+ )
272
+
273
+ return dict(
274
+ symbol=row["name"],
275
+ file=row["file"],
276
+ line=row["start_line"],
277
+ end_line=row["end_line"],
278
+ kind=row["kind"],
279
+ docstring=row["docstring"],
280
+ callers=sorted(callers),
281
+ callees=sorted(callees),
282
+ ambiguous=dup_count > 1,
283
+ cluster_id=c_id,
284
+ cluster_label=c_label,
285
+ cluster_peers=c_peers,
286
+ signature=row["signature"],
287
+ decorators=decoded_decorators,
288
+ is_exported=is_exported,
289
+ visibility=row["visibility"],
290
+ qualified_name=row["qualified_name"],
291
+ field_readers=field_readers,
292
+ field_writers=field_writers,
293
+ )