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
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
"""Graph traversal engine — shared core for impact, trace, and detect_changes.
|
|
2
|
+
|
|
3
|
+
Contract
|
|
4
|
+
--------
|
|
5
|
+
``walk(conn, seeds, direction, max_depth, repo_root=None) -> list[Reached]``
|
|
6
|
+
|
|
7
|
+
Walk the edges table starting from ``seeds``, returning every reachable
|
|
8
|
+
symbol with its minimum distance from any seed and the aggregated path
|
|
9
|
+
confidence at that distance.
|
|
10
|
+
|
|
11
|
+
Direction semantics:
|
|
12
|
+
upstream — who depends on the seed: follow edges where target_name == seed
|
|
13
|
+
back to source_name (callers/importers).
|
|
14
|
+
downstream — what the seed depends on: follow edges where source_name == seed
|
|
15
|
+
forward to target_name.
|
|
16
|
+
|
|
17
|
+
Reached TypedDict:
|
|
18
|
+
name — symbol name (string, from edges table)
|
|
19
|
+
distance — hops from any seed (1-based; seeds themselves are NOT in output)
|
|
20
|
+
confidence — aggregated path confidence at this distance (see path-confidence rule)
|
|
21
|
+
resolved_by — Phase 5: how the final hop's confidence was decided
|
|
22
|
+
(from resolve_edge when repo_root is available, else from name-count).
|
|
23
|
+
|
|
24
|
+
Path-confidence rule (from PRD):
|
|
25
|
+
The confidence of a path is its WEAKEST hop.
|
|
26
|
+
Ordering (weakest first): AMBIGUOUS < INFERRED < EXTRACTED.
|
|
27
|
+
When multiple paths reach the same symbol at the same distance, we report
|
|
28
|
+
the STRONGEST confidence among them (best available path).
|
|
29
|
+
|
|
30
|
+
Confidence rank mapping:
|
|
31
|
+
AMBIGUOUS = 0 (weakest)
|
|
32
|
+
INFERRED = 1
|
|
33
|
+
EXTRACTED = 2 (strongest)
|
|
34
|
+
|
|
35
|
+
Cycle safety:
|
|
36
|
+
We use Python-side BFS with an explicit ``visited`` set.
|
|
37
|
+
A pure recursive CTE with UNION would theoretically terminate,
|
|
38
|
+
but is error-prone to bound precisely across SQLite versions when
|
|
39
|
+
self-edges and multi-hop cycles coexist. Python BFS is trivially
|
|
40
|
+
cycle-safe and easy to test. Max round-trips = max_depth (typically 3–10).
|
|
41
|
+
|
|
42
|
+
Self-edges:
|
|
43
|
+
If source_name == target_name, the edge is skipped to prevent a symbol
|
|
44
|
+
from being "reached" from itself in 1 hop.
|
|
45
|
+
|
|
46
|
+
Module imports:
|
|
47
|
+
Only sqlite3, typing, logging, pathlib (stdlib). No server/cli/query imports.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
import logging
|
|
51
|
+
import sqlite3
|
|
52
|
+
from pathlib import Path
|
|
53
|
+
from typing import TypedDict
|
|
54
|
+
|
|
55
|
+
import seam.config as config
|
|
56
|
+
from seam.analysis.confidence import (
|
|
57
|
+
CONFIDENCE_AMBIGUOUS,
|
|
58
|
+
CONFIDENCE_EXTRACTED,
|
|
59
|
+
CONFIDENCE_INFERRED,
|
|
60
|
+
Resolution,
|
|
61
|
+
load_import_mappings,
|
|
62
|
+
load_name_counts,
|
|
63
|
+
resolve,
|
|
64
|
+
resolve_edge,
|
|
65
|
+
)
|
|
66
|
+
from seam.analysis.imports import ImportMapping
|
|
67
|
+
|
|
68
|
+
logger = logging.getLogger(__name__)
|
|
69
|
+
|
|
70
|
+
# ── Confidence constants ───────────────────────────────────────────────────────
|
|
71
|
+
# Re-export the canonical strings from confidence.py so existing callers that
|
|
72
|
+
# import them from this module continue to work without changes.
|
|
73
|
+
# The definitions live in seam.analysis.confidence — that is the single source of truth.
|
|
74
|
+
__all__ = [
|
|
75
|
+
"CONFIDENCE_EXTRACTED",
|
|
76
|
+
"CONFIDENCE_INFERRED",
|
|
77
|
+
"CONFIDENCE_AMBIGUOUS",
|
|
78
|
+
"Reached",
|
|
79
|
+
"walk",
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
# Integer rank: higher = stronger. Used for min/max comparisons.
|
|
83
|
+
_CONFIDENCE_RANK: dict[str, int] = {
|
|
84
|
+
CONFIDENCE_AMBIGUOUS: 0,
|
|
85
|
+
CONFIDENCE_INFERRED: 1,
|
|
86
|
+
CONFIDENCE_EXTRACTED: 2,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# Reverse lookup: rank -> confidence string (for converting back after arithmetic).
|
|
90
|
+
_RANK_CONFIDENCE: dict[int, str] = {v: k for k, v in _CONFIDENCE_RANK.items()}
|
|
91
|
+
|
|
92
|
+
# Default rank for unknown/missing confidence values (conservative = INFERRED).
|
|
93
|
+
_DEFAULT_RANK = _CONFIDENCE_RANK[CONFIDENCE_INFERRED]
|
|
94
|
+
|
|
95
|
+
# Maximum number of SQL bind parameters per IN-clause batch.
|
|
96
|
+
# SQLite's default SQLITE_MAX_VARIABLE_NUMBER is 999 on Linux/CI.
|
|
97
|
+
# We use 900 to stay safely below that limit even if a query adds extra params.
|
|
98
|
+
_SQL_VAR_BATCH = 900
|
|
99
|
+
|
|
100
|
+
# ── Public types ───────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class Reached(TypedDict):
|
|
104
|
+
"""A symbol reachable from the walk seeds, with its distance and path confidence.
|
|
105
|
+
|
|
106
|
+
Fields:
|
|
107
|
+
name — symbol name (string, matches edges.source_name / target_name)
|
|
108
|
+
distance — number of hops from the nearest seed (1-based)
|
|
109
|
+
confidence — aggregated path confidence at this distance:
|
|
110
|
+
EXTRACTED if all hops are EXTRACTED;
|
|
111
|
+
INFERRED if any hop is INFERRED (no AMBIGUOUS);
|
|
112
|
+
AMBIGUOUS if any hop is AMBIGUOUS.
|
|
113
|
+
When multiple paths reach the same symbol at the same distance,
|
|
114
|
+
the STRONGEST confidence among those paths is reported.
|
|
115
|
+
resolved_by — Phase 5: how the final hop's confidence was decided.
|
|
116
|
+
None for fast-path walk (name-count only, no import context).
|
|
117
|
+
NOTE: resolved_by comes from the FINAL hop of the winning path,
|
|
118
|
+
while confidence is the WEAKEST hop — so resolved_by='import'
|
|
119
|
+
can accompany confidence='AMBIGUOUS' on multi-hop paths.
|
|
120
|
+
best_candidate — Phase 5: for AMBIGUOUS final-hop entries, the most
|
|
121
|
+
file-path-proximate declaring file (absolute path string).
|
|
122
|
+
None for non-AMBIGUOUS hops or when proximity data is unavailable.
|
|
123
|
+
kind — E4: edge kind of the FINAL hop of the winning (strongest-confidence)
|
|
124
|
+
path to this symbol. Full closed vocabulary:
|
|
125
|
+
call | import | extends | implements | instantiates | holds |
|
|
126
|
+
reads | writes | uses.
|
|
127
|
+
Same provenance source as resolved_by — all four fields describe
|
|
128
|
+
one coherent edge. Empty string for degenerate BFS cases.
|
|
129
|
+
synthesized_by — E4: synthesis channel name when the final hop is a heuristic
|
|
130
|
+
synthesized edge (e.g. 'interface-override', 'closure-collection',
|
|
131
|
+
'event-emitter'). None when the final hop is statically extracted.
|
|
132
|
+
Same null-contract as resolved_by/best_candidate: null ≡ static.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
name: str
|
|
136
|
+
distance: int
|
|
137
|
+
confidence: str
|
|
138
|
+
resolved_by: str | None
|
|
139
|
+
best_candidate: str | None
|
|
140
|
+
kind: str
|
|
141
|
+
synthesized_by: str | None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# ── Internal helpers ───────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _rank(confidence: str) -> int:
|
|
148
|
+
"""Return the integer rank for a confidence string (unknown -> INFERRED rank).
|
|
149
|
+
|
|
150
|
+
Logs a debug message for unknown values to aid debugging without spamming logs.
|
|
151
|
+
"""
|
|
152
|
+
if confidence not in _CONFIDENCE_RANK:
|
|
153
|
+
# Unknown confidence value — treat conservatively as INFERRED, log at debug.
|
|
154
|
+
logger.debug("unknown confidence value %r, treating as INFERRED", confidence)
|
|
155
|
+
return _CONFIDENCE_RANK.get(confidence, _DEFAULT_RANK)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _min_rank(a: int, b: int) -> int:
|
|
159
|
+
"""Propagate weakest-hop rule: return the weaker (lower) rank."""
|
|
160
|
+
return min(a, b)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _fetch_neighbors_with_parents(
|
|
164
|
+
conn: sqlite3.Connection,
|
|
165
|
+
names: set[str],
|
|
166
|
+
direction: str,
|
|
167
|
+
) -> list[tuple[str, str, str, str, str, str, str | None]]:
|
|
168
|
+
"""Fetch one-hop neighbors for a set of symbol names, with file context for Phase 5.
|
|
169
|
+
|
|
170
|
+
Returns a list of:
|
|
171
|
+
(neighbor_name, parent_name, edge_target_name, ref_file_path, language,
|
|
172
|
+
edge_kind, edge_synthesized_by)
|
|
173
|
+
|
|
174
|
+
The 3rd column — edge_target_name — is always the edge's target_name column,
|
|
175
|
+
regardless of traversal direction. This lets the caller resolve confidence
|
|
176
|
+
against the whole-index name map keyed on the callee/importee name.
|
|
177
|
+
|
|
178
|
+
4th column — ref_file_path — is the absolute path of the file the edge was extracted
|
|
179
|
+
from (edges.file_id → files.path). Used by resolve_edge() for import-promotion.
|
|
180
|
+
|
|
181
|
+
5th column — language — is files.language for the referencing file. Used by
|
|
182
|
+
resolve_edge() for builtin-check and import source resolution.
|
|
183
|
+
|
|
184
|
+
6th column — edge_kind — the edge.kind value ('call'|'import'|'holds'|...).
|
|
185
|
+
7th column — edge_synthesized_by — the edge.synthesized_by value (None for static edges;
|
|
186
|
+
channel name string for synthesized edges).
|
|
187
|
+
|
|
188
|
+
The stored edges.confidence column is intentionally NOT selected: hop
|
|
189
|
+
confidence is resolved whole-index from edge_target_name at read time
|
|
190
|
+
(see confidence.resolve_edge), so the stored same-file value never feeds traversal.
|
|
191
|
+
|
|
192
|
+
Splits the IN-clause into batches of _SQL_VAR_BATCH to avoid hitting
|
|
193
|
+
SQLite's SQLITE_MAX_VARIABLE_NUMBER limit (999 on Linux/CI).
|
|
194
|
+
|
|
195
|
+
direction='upstream' → edges where target_name IN names;
|
|
196
|
+
neighbor=source_name, parent=target_name, edge_target_name=target_name
|
|
197
|
+
direction='downstream' → edges where source_name IN names;
|
|
198
|
+
neighbor=target_name, parent=source_name, edge_target_name=target_name
|
|
199
|
+
"""
|
|
200
|
+
if not names:
|
|
201
|
+
return []
|
|
202
|
+
|
|
203
|
+
names_list = list(names)
|
|
204
|
+
all_rows: list[tuple[str, str, str, str, str, str, str | None]] = []
|
|
205
|
+
|
|
206
|
+
# Process in batches to avoid SQLITE_MAX_VARIABLE_NUMBER (999 on most builds).
|
|
207
|
+
for batch_start in range(0, len(names_list), _SQL_VAR_BATCH):
|
|
208
|
+
batch = names_list[batch_start : batch_start + _SQL_VAR_BATCH]
|
|
209
|
+
placeholders = ",".join("?" * len(batch))
|
|
210
|
+
|
|
211
|
+
if direction == "upstream":
|
|
212
|
+
# Who depends on us? → edges pointing at us → source_name is the caller.
|
|
213
|
+
# edge_target_name = target_name (the callee, i.e. the seed symbol).
|
|
214
|
+
# JOIN files to get the referencing file path and language for resolve_edge.
|
|
215
|
+
# E4: also select e.kind and e.synthesized_by for provenance threading.
|
|
216
|
+
sql = f"""
|
|
217
|
+
SELECT e.source_name AS neighbor,
|
|
218
|
+
e.target_name AS parent,
|
|
219
|
+
e.target_name AS edge_target_name,
|
|
220
|
+
f.path AS ref_file_path,
|
|
221
|
+
f.language AS language,
|
|
222
|
+
e.kind AS edge_kind,
|
|
223
|
+
e.synthesized_by AS edge_synthesized_by
|
|
224
|
+
FROM edges e
|
|
225
|
+
JOIN files f ON f.id = e.file_id
|
|
226
|
+
WHERE e.target_name IN ({placeholders})
|
|
227
|
+
AND e.source_name != e.target_name
|
|
228
|
+
"""
|
|
229
|
+
else:
|
|
230
|
+
# What do we depend on? → edges going out from us → target_name is dep.
|
|
231
|
+
# edge_target_name = target_name (the callee, i.e. the neighbor).
|
|
232
|
+
# E4: also select e.kind and e.synthesized_by for provenance threading.
|
|
233
|
+
sql = f"""
|
|
234
|
+
SELECT e.target_name AS neighbor,
|
|
235
|
+
e.source_name AS parent,
|
|
236
|
+
e.target_name AS edge_target_name,
|
|
237
|
+
f.path AS ref_file_path,
|
|
238
|
+
f.language AS language,
|
|
239
|
+
e.kind AS edge_kind,
|
|
240
|
+
e.synthesized_by AS edge_synthesized_by
|
|
241
|
+
FROM edges e
|
|
242
|
+
JOIN files f ON f.id = e.file_id
|
|
243
|
+
WHERE e.source_name IN ({placeholders})
|
|
244
|
+
AND e.source_name != e.target_name
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
rows = conn.execute(sql, batch).fetchall()
|
|
248
|
+
all_rows.extend(
|
|
249
|
+
(
|
|
250
|
+
row["neighbor"],
|
|
251
|
+
row["parent"],
|
|
252
|
+
row["edge_target_name"],
|
|
253
|
+
row["ref_file_path"],
|
|
254
|
+
row["language"],
|
|
255
|
+
row["edge_kind"],
|
|
256
|
+
row["edge_synthesized_by"],
|
|
257
|
+
)
|
|
258
|
+
for row in rows
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return all_rows
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# ── Public interface ───────────────────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def walk(
|
|
268
|
+
conn: sqlite3.Connection,
|
|
269
|
+
seeds: list[str],
|
|
270
|
+
direction: str,
|
|
271
|
+
max_depth: int,
|
|
272
|
+
repo_root: Path | None = None,
|
|
273
|
+
) -> list[Reached]:
|
|
274
|
+
"""Walk the edge graph from seed symbols, returning reachable symbols.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
conn: Open SQLite connection (read-only semantics; no writes).
|
|
278
|
+
seeds: Symbol names to start from. Empty list -> returns [].
|
|
279
|
+
direction: "upstream" (callers/importers) or "downstream" (callees/importees).
|
|
280
|
+
max_depth: Maximum number of hops from any seed (1-based). Must be >= 1.
|
|
281
|
+
Clamping to [1, 10] is the CALLER's responsibility (impact.py does this).
|
|
282
|
+
repo_root: Repository root. When provided (and SEAM_IMPORT_RESOLUTION="on"),
|
|
283
|
+
each hop's confidence is resolved via resolve_edge() with full
|
|
284
|
+
import-promotion context (Phase 5 homonym fix). When None or when
|
|
285
|
+
SEAM_IMPORT_RESOLUTION="off", falls back to name-count resolution.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
List of Reached dicts — one per reachable symbol (excluding the seeds themselves).
|
|
289
|
+
Ordered by distance ascending, then name alphabetically.
|
|
290
|
+
Empty list if seeds is empty, no edges, or all reachable are already seeds.
|
|
291
|
+
|
|
292
|
+
Phase 5 resolved_by:
|
|
293
|
+
When repo_root is provided and SEAM_IMPORT_RESOLUTION="on", each Reached entry
|
|
294
|
+
has resolved_by populated from the final-hop's Resolution["resolved_by"]:
|
|
295
|
+
"import" for import-promoted hops, "name-unique"/"name-collision"/"builtin"/
|
|
296
|
+
"unresolved" for name-count-resolved hops, or None on any failure (degrade).
|
|
297
|
+
|
|
298
|
+
Cycle safety:
|
|
299
|
+
Uses a ``visited`` set to avoid revisiting symbols. A symbol is visited
|
|
300
|
+
at the first (minimum-distance) encounter. If the same symbol appears via
|
|
301
|
+
multiple paths at the same distance, we keep the strongest confidence
|
|
302
|
+
among those paths (see path-confidence rule in module docstring).
|
|
303
|
+
|
|
304
|
+
SQLite variable limit:
|
|
305
|
+
The IN-clause is batched in groups of _SQL_VAR_BATCH (900) to avoid
|
|
306
|
+
hitting SQLite's SQLITE_MAX_VARIABLE_NUMBER limit (999 on Linux/CI).
|
|
307
|
+
The batching is transparent — results are identical to an unbatched query.
|
|
308
|
+
"""
|
|
309
|
+
if not seeds or max_depth < 1:
|
|
310
|
+
return []
|
|
311
|
+
|
|
312
|
+
# Load whole-index name-count map ONCE per walk call.
|
|
313
|
+
# This single GROUP BY query is the foundation of whole-index confidence resolution.
|
|
314
|
+
name_counts = load_name_counts(conn)
|
|
315
|
+
|
|
316
|
+
# Determine whether full import-promotion is enabled for this walk call.
|
|
317
|
+
# Both repo_root AND SEAM_IMPORT_RESOLUTION="on" are required.
|
|
318
|
+
use_import_promotion = repo_root is not None and config.SEAM_IMPORT_RESOLUTION == "on"
|
|
319
|
+
|
|
320
|
+
# Per-file import mapping cache: path -> list[ImportMapping].
|
|
321
|
+
# Populated lazily on first access per file within this walk call.
|
|
322
|
+
# This prevents N re-queries of load_import_mappings for every hop in large graphs.
|
|
323
|
+
_import_cache: dict[str, list[ImportMapping]] = {}
|
|
324
|
+
|
|
325
|
+
# Per-(file, target) resolution-result cache keyed on (ref_file_path, target_name).
|
|
326
|
+
# A hub symbol (many callers from the same file) produces many edge rows with the
|
|
327
|
+
# same pair, so without this cache each row would re-run the declaration-check and
|
|
328
|
+
# proximity SELECT in resolve_edge — O(edge rows) DB queries instead of O(distinct pairs).
|
|
329
|
+
_resolution_cache: dict[tuple[str, str], Resolution] = {}
|
|
330
|
+
|
|
331
|
+
def _get_import_mappings(file_path: str) -> list[ImportMapping]:
|
|
332
|
+
"""Lazily load and cache import mappings for a referencing file path."""
|
|
333
|
+
if file_path not in _import_cache:
|
|
334
|
+
_import_cache[file_path] = load_import_mappings(conn, file_path)
|
|
335
|
+
return _import_cache[file_path]
|
|
336
|
+
|
|
337
|
+
# BFS state:
|
|
338
|
+
# visited: symbols already processed (prevents revisiting and cycles)
|
|
339
|
+
# current_frontier: set of (name, path_rank) for the current BFS level
|
|
340
|
+
# results: name -> (distance, best_path_rank, resolved_by_for_best_path, best_candidate,
|
|
341
|
+
# kind_for_best_path, synthesized_by_for_best_path)
|
|
342
|
+
seed_set = set(seeds)
|
|
343
|
+
visited: set[str] = set(seeds) # seeds are "visited" — we don't return them
|
|
344
|
+
|
|
345
|
+
# Initial frontier: all seeds at EXTRACTED rank (perfect start — the seed IS the seed).
|
|
346
|
+
current_frontier: list[tuple[str, int]] = [(s, 2) for s in seeds]
|
|
347
|
+
|
|
348
|
+
# Results map: name -> (distance, best_path_rank, resolved_by, best_candidate, kind, synthesized_by)
|
|
349
|
+
# best_candidate, kind, and synthesized_by all come from the final hop of the winning
|
|
350
|
+
# (strongest-confidence) path — so all four provenance fields describe one coherent edge.
|
|
351
|
+
results: dict[str, tuple[int, int, str | None, str | None, str, str | None]] = {}
|
|
352
|
+
|
|
353
|
+
for depth in range(1, max_depth + 1):
|
|
354
|
+
if not current_frontier:
|
|
355
|
+
break
|
|
356
|
+
|
|
357
|
+
# Build a map: parent_name -> path_rank so we can propagate confidence.
|
|
358
|
+
parent_rank: dict[str, int] = {}
|
|
359
|
+
for name, pr in current_frontier:
|
|
360
|
+
if name not in parent_rank or pr > parent_rank[name]:
|
|
361
|
+
parent_rank[name] = pr
|
|
362
|
+
|
|
363
|
+
# Fetch one-hop neighbors for all symbols in the current frontier.
|
|
364
|
+
# Returns (neighbor, parent, edge_target_name, ref_file_path, language, edge_kind, edge_synthesized_by).
|
|
365
|
+
frontier_names = {name for name, _ in current_frontier}
|
|
366
|
+
rows = _fetch_neighbors_with_parents(conn, frontier_names, direction)
|
|
367
|
+
|
|
368
|
+
if not rows:
|
|
369
|
+
break
|
|
370
|
+
|
|
371
|
+
# next_frontier tracks: name -> (best_path_rank, resolved_by, best_candidate, kind, synthesized_by)
|
|
372
|
+
next_frontier: dict[str, tuple[int, str | None, str | None, str, str | None]] = {}
|
|
373
|
+
|
|
374
|
+
for neighbor, parent, edge_target_name, ref_file_path, language, edge_kind, edge_synth_by in rows:
|
|
375
|
+
# Skip seeds — they are never returned as reachable.
|
|
376
|
+
if neighbor in seed_set:
|
|
377
|
+
continue
|
|
378
|
+
|
|
379
|
+
# Resolve hop confidence using full Phase 5 resolver when available.
|
|
380
|
+
# Falls back to plain name-count when import promotion is disabled/unavailable.
|
|
381
|
+
# Resolution cache: (ref_file_path, edge_target_name) pairs recur
|
|
382
|
+
# on hub symbols — cache results to collapse O(edge rows) → O(distinct pairs).
|
|
383
|
+
hop_best_candidate: str | None = None
|
|
384
|
+
if use_import_promotion and ref_file_path and language:
|
|
385
|
+
cache_key = (ref_file_path, edge_target_name)
|
|
386
|
+
if cache_key not in _resolution_cache:
|
|
387
|
+
import_mappings = _get_import_mappings(ref_file_path)
|
|
388
|
+
_resolution_cache[cache_key] = resolve_edge(
|
|
389
|
+
target_name=edge_target_name,
|
|
390
|
+
name_counts=name_counts,
|
|
391
|
+
language=language,
|
|
392
|
+
import_mappings=import_mappings,
|
|
393
|
+
referencing_file=Path(ref_file_path),
|
|
394
|
+
repo_root=repo_root,
|
|
395
|
+
conn=conn,
|
|
396
|
+
max_import_candidates=config.SEAM_MAX_IMPORT_CANDIDATES,
|
|
397
|
+
max_proximity_candidates=config.SEAM_PROXIMITY_MAX_CANDIDATES,
|
|
398
|
+
)
|
|
399
|
+
resolution = _resolution_cache[cache_key]
|
|
400
|
+
hop_confidence = resolution["confidence"]
|
|
401
|
+
hop_resolved_by = resolution["resolved_by"]
|
|
402
|
+
# best_candidate is only present on AMBIGUOUS hops (proximity tie-break);
|
|
403
|
+
# captured here so it surfaces in the final Reached output.
|
|
404
|
+
hop_best_candidate = resolution.get("best_candidate")
|
|
405
|
+
else:
|
|
406
|
+
# Fast-path: name-count only (SEAM_IMPORT_RESOLUTION="off" or no context).
|
|
407
|
+
hop_confidence = resolve(edge_target_name, name_counts)
|
|
408
|
+
hop_resolved_by = None
|
|
409
|
+
|
|
410
|
+
# Propagate weakest-hop rule: path confidence = min(parent_rank, this_hop_rank).
|
|
411
|
+
parent_path_rank = parent_rank.get(parent, _DEFAULT_RANK)
|
|
412
|
+
hop_rank = _rank(hop_confidence)
|
|
413
|
+
path_rank = _min_rank(parent_path_rank, hop_rank)
|
|
414
|
+
|
|
415
|
+
if neighbor in visited:
|
|
416
|
+
# Already at a shorter distance (BFS guarantees minimum-distance-first).
|
|
417
|
+
continue
|
|
418
|
+
|
|
419
|
+
# Keep the strongest path rank for this neighbor at this BFS level.
|
|
420
|
+
# When replacing with a stronger path, update resolved_by, best_candidate,
|
|
421
|
+
# kind, and synthesized_by from the winning hop — all four provenance fields
|
|
422
|
+
# come from the same final hop of the strongest-confidence path.
|
|
423
|
+
existing = next_frontier.get(neighbor)
|
|
424
|
+
if existing is None or path_rank > existing[0]:
|
|
425
|
+
next_frontier[neighbor] = (
|
|
426
|
+
path_rank,
|
|
427
|
+
hop_resolved_by,
|
|
428
|
+
hop_best_candidate,
|
|
429
|
+
edge_kind,
|
|
430
|
+
edge_synth_by,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
# Commit next_frontier to results and advance visited set.
|
|
434
|
+
new_frontier: list[tuple[str, int]] = []
|
|
435
|
+
for name, (pr, resolved_by, best_candidate, kind, synth_by) in next_frontier.items():
|
|
436
|
+
visited.add(name)
|
|
437
|
+
results[name] = (depth, pr, resolved_by, best_candidate, kind, synth_by)
|
|
438
|
+
new_frontier.append((name, pr))
|
|
439
|
+
|
|
440
|
+
current_frontier = new_frontier
|
|
441
|
+
|
|
442
|
+
# Convert results to Reached list, sorted by distance then name.
|
|
443
|
+
# resolved_by, kind, and synthesized_by all reflect the FINAL hop of the winning
|
|
444
|
+
# (strongest-confidence) path. confidence is the WEAKEST hop — so resolved_by='import'
|
|
445
|
+
# can accompany confidence='AMBIGUOUS' on multi-hop paths where an earlier hop was
|
|
446
|
+
# ambiguous. best_candidate is from the final hop's Resolution (only set for AMBIGUOUS).
|
|
447
|
+
reached: list[Reached] = [
|
|
448
|
+
Reached(
|
|
449
|
+
name=name,
|
|
450
|
+
distance=distance,
|
|
451
|
+
confidence=_RANK_CONFIDENCE.get(best_rank, CONFIDENCE_INFERRED),
|
|
452
|
+
resolved_by=resolved_by,
|
|
453
|
+
best_candidate=best_candidate,
|
|
454
|
+
kind=kind,
|
|
455
|
+
synthesized_by=synth_by,
|
|
456
|
+
)
|
|
457
|
+
for name, (distance, best_rank, resolved_by, best_candidate, kind, synth_by) in results.items()
|
|
458
|
+
]
|
|
459
|
+
reached.sort(key=lambda r: (r["distance"], r["name"]))
|
|
460
|
+
|
|
461
|
+
logger.debug(
|
|
462
|
+
"walk(seeds=%s, direction=%s, max_depth=%d, import_promotion=%s) -> %d reached",
|
|
463
|
+
seeds,
|
|
464
|
+
direction,
|
|
465
|
+
max_depth,
|
|
466
|
+
use_import_promotion,
|
|
467
|
+
len(reached),
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
return reached
|
seam/cli/__init__.py
ADDED
|
File without changes
|