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,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