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.
- seam/__init__.py +3 -0
- seam/_data/schema.sql +225 -0
- seam/_web/assets/index-BL_tqprR.js +216 -0
- seam/_web/assets/index-GTKUhVyD.css +1 -0
- seam/_web/index.html +13 -0
- seam/analysis/__init__.py +14 -0
- seam/analysis/affected.py +254 -0
- seam/analysis/builtins.py +966 -0
- seam/analysis/byte_budget.py +217 -0
- seam/analysis/changes.py +709 -0
- seam/analysis/cluster_naming.py +260 -0
- seam/analysis/clustering.py +216 -0
- seam/analysis/confidence.py +699 -0
- seam/analysis/embeddings.py +195 -0
- seam/analysis/flows.py +708 -0
- seam/analysis/impact.py +444 -0
- seam/analysis/imports.py +994 -0
- seam/analysis/imports_ext.py +780 -0
- seam/analysis/imports_resolve.py +176 -0
- seam/analysis/processes.py +453 -0
- seam/analysis/relevance.py +155 -0
- seam/analysis/rwr.py +129 -0
- seam/analysis/staleness.py +328 -0
- seam/analysis/steer.py +282 -0
- seam/analysis/synthesis.py +253 -0
- seam/analysis/synthesis_channels.py +433 -0
- seam/analysis/testpaths.py +103 -0
- seam/analysis/traversal.py +470 -0
- seam/cli/__init__.py +0 -0
- seam/cli/install.py +232 -0
- seam/cli/main.py +2602 -0
- seam/cli/output.py +137 -0
- seam/cli/read.py +244 -0
- seam/cli/serve.py +145 -0
- seam/config.py +551 -0
- seam/indexer/__init__.py +0 -0
- seam/indexer/cluster_index.py +425 -0
- seam/indexer/db.py +496 -0
- seam/indexer/embedding_index.py +183 -0
- seam/indexer/field_access.py +536 -0
- seam/indexer/field_access_c_cpp.py +643 -0
- seam/indexer/field_access_ext.py +708 -0
- seam/indexer/field_access_ext2.py +408 -0
- seam/indexer/field_access_go_rust.py +737 -0
- seam/indexer/field_access_php_swift.py +888 -0
- seam/indexer/field_access_ts.py +626 -0
- seam/indexer/graph.py +321 -0
- seam/indexer/graph_c.py +562 -0
- seam/indexer/graph_c_cpp.py +39 -0
- seam/indexer/graph_common.py +644 -0
- seam/indexer/graph_cpp.py +615 -0
- seam/indexer/graph_csharp.py +651 -0
- seam/indexer/graph_go.py +723 -0
- seam/indexer/graph_go_rust.py +39 -0
- seam/indexer/graph_java.py +689 -0
- seam/indexer/graph_java_csharp.py +38 -0
- seam/indexer/graph_php.py +914 -0
- seam/indexer/graph_python.py +628 -0
- seam/indexer/graph_ruby.py +748 -0
- seam/indexer/graph_rust.py +653 -0
- seam/indexer/graph_scope_infer.py +902 -0
- seam/indexer/graph_scope_infer_ext.py +723 -0
- seam/indexer/graph_scope_infer_ext2.py +992 -0
- seam/indexer/graph_swift.py +1014 -0
- seam/indexer/graph_swift_infer.py +515 -0
- seam/indexer/graph_typescript.py +663 -0
- seam/indexer/migrations.py +816 -0
- seam/indexer/parser.py +204 -0
- seam/indexer/pipeline.py +197 -0
- seam/indexer/signatures.py +634 -0
- seam/indexer/signatures_ext.py +780 -0
- seam/indexer/sync.py +287 -0
- seam/indexer/synthesis_index.py +291 -0
- seam/indexer/tokenize.py +79 -0
- seam/installer/__init__.py +67 -0
- seam/installer/claude.py +97 -0
- seam/installer/codex.py +94 -0
- seam/installer/core.py +127 -0
- seam/installer/cursor.py +61 -0
- seam/installer/guide.py +110 -0
- seam/installer/jsonfile.py +85 -0
- seam/installer/markdownfile.py +146 -0
- seam/installer/tomlfile.py +72 -0
- seam/query/__init__.py +0 -0
- seam/query/clusters.py +206 -0
- seam/query/comments.py +217 -0
- seam/query/context.py +293 -0
- seam/query/engine.py +940 -0
- seam/query/fts.py +328 -0
- seam/query/names.py +470 -0
- seam/query/pack.py +433 -0
- seam/query/semantic.py +339 -0
- seam/query/structure.py +727 -0
- seam/server/__init__.py +0 -0
- seam/server/graph_api.py +437 -0
- seam/server/handler_common.py +323 -0
- seam/server/impact_handler.py +615 -0
- seam/server/mcp.py +556 -0
- seam/server/tools.py +697 -0
- seam/server/trace_handler.py +184 -0
- seam/server/web.py +922 -0
- seam/watcher/__init__.py +0 -0
- seam/watcher/__main__.py +56 -0
- seam/watcher/daemon.py +237 -0
- seam_code-0.3.0.dist-info/METADATA +318 -0
- seam_code-0.3.0.dist-info/RECORD +109 -0
- seam_code-0.3.0.dist-info/WHEEL +4 -0
- seam_code-0.3.0.dist-info/entry_points.txt +2 -0
- 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
|
+
)
|