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,780 @@
1
+ """Phase 9 import mapping extraction and resolution — Java, C#, Ruby, C, C++, PHP.
2
+
3
+ LAYER: pure leaf module — imports only stdlib + tree_sitter types.
4
+ Must NOT import from seam.query, seam.server, confidence.py, or any other
5
+ seam module (same contract as imports.py).
6
+
7
+ LAYERING:
8
+ imports_ext (this file — leaf, no seam deps)
9
+
10
+ imports.py (dispatch entry point — imports this at top level)
11
+
12
+ Entry points (called from imports.extract_import_mappings and
13
+ imports.resolve_import_source):
14
+ _extract_<lang>(root, filepath) -> list[ImportMapping]
15
+ _resolve_<lang>(source, ref, root) -> list[str]
16
+
17
+ All functions NEVER raise. On failure they return [] (empty list),
18
+ degrading cleanly to the name-count resolution rule in confidence.py.
19
+ """
20
+
21
+ import logging
22
+ from pathlib import Path
23
+ from typing import Any, TypedDict
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ # WHY this type alias: imports_ext is a leaf that must not import from imports.py
28
+ # (imports.py imports imports_ext at its top level). We use TypedDict locally to
29
+ # construct ImportMapping-compatible dicts without the circular import.
30
+ _ImportMappingList = list[Any]
31
+
32
+
33
+ class _ImportMapping(TypedDict):
34
+ """Local copy of ImportMapping TypedDict to avoid circular import from imports.py."""
35
+
36
+ local_name: str
37
+ exported_name: str
38
+ source_module: str
39
+ is_default: bool
40
+ is_namespace: bool
41
+ is_wildcard: bool
42
+ line: int
43
+
44
+
45
+ def _text_node(node: Any) -> str:
46
+ """Safely decode a tree-sitter node's text bytes to str."""
47
+ try:
48
+ raw = node.text
49
+ if raw is None:
50
+ return ""
51
+ return raw.decode("utf-8", errors="replace")
52
+ except Exception: # noqa: BLE001
53
+ return ""
54
+
55
+
56
+ def _make_mapping(
57
+ local_name: str,
58
+ exported_name: str,
59
+ source_module: str,
60
+ line: int,
61
+ *,
62
+ is_default: bool = False,
63
+ is_namespace: bool = False,
64
+ is_wildcard: bool = False,
65
+ ) -> _ImportMapping:
66
+ """Construct an ImportMapping dict with safe defaults."""
67
+ return _ImportMapping(
68
+ local_name=local_name,
69
+ exported_name=exported_name,
70
+ source_module=source_module,
71
+ is_default=is_default,
72
+ is_namespace=is_namespace,
73
+ is_wildcard=is_wildcard,
74
+ line=line,
75
+ )
76
+
77
+
78
+ # ── Java ──────────────────────────────────────────────────────────────────────
79
+
80
+
81
+ def _java_scoped_identifier_last_segment(node: Any) -> str | None:
82
+ """Extract the rightmost identifier from a Java scoped_identifier or identifier.
83
+
84
+ scoped_identifier.name → rightmost segment (e.g. 'List' from java.util.List)
85
+ identifier → the text itself
86
+ """
87
+ try:
88
+ if node.type == "identifier":
89
+ return _text_node(node)
90
+ if node.type == "scoped_identifier":
91
+ name_node = node.child_by_field_name("name")
92
+ if name_node is not None:
93
+ return _text_node(name_node)
94
+ except Exception: # noqa: BLE001
95
+ pass
96
+ return None
97
+
98
+
99
+ def _java_scoped_identifier_full(node: Any) -> str:
100
+ """Return the full dotted name of a scoped_identifier or identifier.
101
+
102
+ Used for the source_module field — e.g. 'java.util.List'.
103
+ """
104
+ try:
105
+ return _text_node(node).strip()
106
+ except Exception: # noqa: BLE001
107
+ return ""
108
+
109
+
110
+ def _extract_java(root: object, filepath: Path) -> _ImportMappingList:
111
+ """Extract ImportMapping records from a Java AST root node.
112
+
113
+ Handles:
114
+ import java.util.List; → local_name='List', source_module='java.util.List'
115
+ import java.util.*; → is_wildcard=True (skipped — no single target)
116
+ import static java.lang.Math.abs; → local_name='abs', source_module='java.lang.Math.abs'
117
+
118
+ Resolution: Java package-to-directory resolution is out of scope per the spec;
119
+ resolve always returns [] (degrades to name-count rule in confidence.py).
120
+ Never raises. Returns [] on any failure.
121
+ """
122
+ result: _ImportMappingList = []
123
+ try:
124
+ from tree_sitter import Node
125
+
126
+ if not isinstance(root, Node):
127
+ return result
128
+
129
+ def _walk(node: Any) -> None:
130
+ if node.type == "import_declaration":
131
+ line = node.start_point[0] + 1
132
+ # WHY pre-scan: in `import java.util.*;` the tree is:
133
+ # import_declaration → [scoped_identifier('java.util'), '.', asterisk, ';']
134
+ # Iterating named_children left-to-right processes 'java.util' BEFORE the
135
+ # asterisk, which would emit 'util' as a spurious mapping. Pre-scan prevents.
136
+ if any(child.type == "asterisk" for child in node.children):
137
+ # Wildcard import (`import java.util.*;`) — no single binding target.
138
+ # Skip entirely: emitting 'util' (the scope before the *) would be a
139
+ # spurious mapping that does not correspond to any importable symbol.
140
+ # This matches the convention in imports.py where wildcard imports that
141
+ # cannot be meaningfully resolved are omitted from the mapping list.
142
+ return # No need to recurse.
143
+
144
+ for child in node.named_children:
145
+ last = _java_scoped_identifier_last_segment(child)
146
+ full = _java_scoped_identifier_full(child)
147
+ if last:
148
+ result.append(
149
+ _make_mapping(
150
+ local_name=last,
151
+ exported_name=last,
152
+ source_module=full,
153
+ line=line,
154
+ is_default=True,
155
+ )
156
+ )
157
+ return # No need to recurse into import_declaration children.
158
+ for child in node.named_children:
159
+ _walk(child)
160
+
161
+ _walk(root)
162
+ except Exception as exc: # noqa: BLE001
163
+ logger.debug("_extract_java: extraction failed for %s: %r", filepath, exc)
164
+ return result
165
+
166
+
167
+ def _resolve_java(
168
+ source_module: str,
169
+ referencing_file: Path,
170
+ repo_root: Path,
171
+ ) -> list[str]:
172
+ """Resolve a Java import path to source file paths.
173
+
174
+ Package-to-directory resolution is out of scope for the MVP (see PRD §implementation).
175
+ Returns [] — degrading cleanly to the name-count resolution rule.
176
+ """
177
+ return [] # Out of scope per spec — Java package resolution not implemented
178
+
179
+
180
+ # ── C# ────────────────────────────────────────────────────────────────────────
181
+
182
+
183
+ def _csharp_using_last_segment(node: Any) -> str | None:
184
+ """Extract the rightmost identifier from a C# qualified_name or identifier.
185
+
186
+ qualified_name.name → rightmost segment (e.g. 'Generic' from System.Collections.Generic)
187
+ identifier → the text itself
188
+ """
189
+ try:
190
+ if node.type == "identifier":
191
+ return _text_node(node)
192
+ if node.type == "qualified_name":
193
+ name_node = node.child_by_field_name("name")
194
+ if name_node is not None:
195
+ return _text_node(name_node)
196
+ except Exception: # noqa: BLE001
197
+ pass
198
+ return None
199
+
200
+
201
+ def _extract_csharp(root: object, filepath: Path) -> _ImportMappingList:
202
+ """Extract ImportMapping records from a C# AST root node.
203
+
204
+ Handles:
205
+ using System; → local_name='System'
206
+ using System.Collections.Generic; → local_name='Generic'
207
+
208
+ Resolution: C# namespace-to-directory resolution is out of scope per the spec.
209
+ Never raises. Returns [] on any failure.
210
+ """
211
+ result: _ImportMappingList = []
212
+ try:
213
+ from tree_sitter import Node
214
+
215
+ if not isinstance(root, Node):
216
+ return result
217
+
218
+ def _walk(node: Any) -> None:
219
+ if node.type == "using_directive":
220
+ line = node.start_point[0] + 1
221
+ named = node.named_children
222
+ # WHY alias detection: `using Foo = System.Collections.Generic;` produces:
223
+ # using_directive → [using, identifier('Foo'), '=', qualified_name(...), ';']
224
+ # Without this check, the first named child (identifier 'Foo') is emitted as
225
+ # the import target — a spurious alias edge. Detect the alias form by checking
226
+ # for BOTH an identifier AND a qualified_name sibling; if present, use only
227
+ # the qualified_name's last segment; the alias identifier is the local_name.
228
+ has_identifier = any(c.type == "identifier" for c in named)
229
+ has_qualified = any(c.type == "qualified_name" for c in named)
230
+ if has_identifier and has_qualified:
231
+ # Alias form: record alias as local_name, real namespace as exported_name.
232
+ alias = next((_text_node(c) for c in named if c.type == "identifier"), None)
233
+ for child in named:
234
+ if child.type == "qualified_name":
235
+ last = _csharp_using_last_segment(child)
236
+ full = _text_node(child).strip()
237
+ if last and alias:
238
+ result.append(
239
+ _make_mapping(
240
+ local_name=alias,
241
+ exported_name=last,
242
+ source_module=full,
243
+ line=line,
244
+ is_default=True,
245
+ )
246
+ )
247
+ return # No further children needed.
248
+
249
+ # Non-alias form: emit the last segment of whichever name is present.
250
+ for child in named:
251
+ last = _csharp_using_last_segment(child)
252
+ full = _text_node(child).strip()
253
+ if last:
254
+ result.append(
255
+ _make_mapping(
256
+ local_name=last,
257
+ exported_name=last,
258
+ source_module=full,
259
+ line=line,
260
+ is_default=True,
261
+ )
262
+ )
263
+ return # No need to recurse into using_directive children.
264
+ for child in node.named_children:
265
+ _walk(child)
266
+
267
+ _walk(root)
268
+ except Exception as exc: # noqa: BLE001
269
+ logger.debug("_extract_csharp: extraction failed for %s: %r", filepath, exc)
270
+ return result
271
+
272
+
273
+ def _resolve_csharp(
274
+ source_module: str,
275
+ referencing_file: Path,
276
+ repo_root: Path,
277
+ ) -> list[str]:
278
+ """Resolve a C# using directive to source file paths.
279
+
280
+ Namespace-to-directory resolution is out of scope for the MVP.
281
+ Returns [] — degrading cleanly to the name-count resolution rule.
282
+ """
283
+ return [] # Out of scope per spec — C# namespace resolution not implemented
284
+
285
+
286
+ # ── Ruby ──────────────────────────────────────────────────────────────────────
287
+
288
+
289
+ def _ruby_require_string_content(string_node: Any) -> str | None:
290
+ """Extract the content from a Ruby string node.
291
+
292
+ Ruby string: string → [" string_content "]
293
+ Falls back to stripping quotes from full text if string_content not found.
294
+ """
295
+ try:
296
+ for child in string_node.named_children:
297
+ if child.type == "string_content":
298
+ return _text_node(child)
299
+ # Fallback: strip surrounding single/double quotes.
300
+ raw = _text_node(string_node)
301
+ return raw.strip("'\"")
302
+ except Exception: # noqa: BLE001
303
+ return None
304
+
305
+
306
+ def _extract_ruby(root: object, filepath: Path) -> _ImportMappingList:
307
+ """Extract ImportMapping records from a Ruby AST root node.
308
+
309
+ Handles:
310
+ require 'json' → local_name='json', source_module='json'
311
+ require_relative './x' → local_name='x', source_module='./x'
312
+ require 'active_record' → local_name='active_record', source_module='active_record'
313
+
314
+ Resolution: require_relative './x' → relative .rb file probe (via _resolve_ruby).
315
+ require 'x' → [] (gem/load path out of scope per spec).
316
+ Never raises. Returns [] on any failure.
317
+ """
318
+ result: _ImportMappingList = []
319
+ try:
320
+ from tree_sitter import Node
321
+
322
+ if not isinstance(root, Node):
323
+ return result
324
+
325
+ def _walk(node: Any) -> None:
326
+ # Ruby require/require_relative appear as top-level 'call' nodes.
327
+ if node.type == "call":
328
+ method_node = node.child_by_field_name("method")
329
+ if method_node is None or method_node.type != "identifier":
330
+ # Not a plain identifier call — skip, still recurse.
331
+ for child in node.named_children:
332
+ _walk(child)
333
+ return
334
+
335
+ method_name = _text_node(method_node)
336
+ line = node.start_point[0] + 1
337
+
338
+ if method_name in ("require", "require_relative"):
339
+ arg_list = node.child_by_field_name("arguments")
340
+ if arg_list is not None:
341
+ for child in arg_list.named_children:
342
+ if child.type == "string":
343
+ content = _ruby_require_string_content(child)
344
+ if content:
345
+ from pathlib import Path as _Path
346
+
347
+ local = _Path(content).stem
348
+ result.append(
349
+ _make_mapping(
350
+ local_name=local,
351
+ exported_name=local,
352
+ source_module=content,
353
+ line=line,
354
+ is_default=True,
355
+ )
356
+ )
357
+ return # don't recurse into require call
358
+
359
+ # Recurse into other calls' children
360
+ for child in node.named_children:
361
+ _walk(child)
362
+ return
363
+
364
+ for child in node.named_children:
365
+ _walk(child)
366
+
367
+ _walk(root)
368
+ except Exception as exc: # noqa: BLE001
369
+ logger.debug("_extract_ruby: extraction failed for %s: %r", filepath, exc)
370
+ return result
371
+
372
+
373
+ def _resolve_ruby(
374
+ source_module: str,
375
+ referencing_file: Path,
376
+ repo_root: Path,
377
+ ) -> list[str]:
378
+ """Resolve a Ruby require source to file paths.
379
+
380
+ require_relative './x' → probe same directory as referencing_file for x.rb.
381
+ require 'x' → [] (gem / load path resolution out of scope per spec).
382
+
383
+ Never raises.
384
+ """
385
+ try:
386
+ # Only resolve relative paths (those starting with ./ or ../).
387
+ if not (source_module.startswith("./") or source_module.startswith("../")):
388
+ return [] # absolute gem name — out of scope
389
+
390
+ ref_dir = referencing_file.parent
391
+ # The source_module may or may not include .rb extension.
392
+ if not source_module.endswith(".rb"):
393
+ candidate = ref_dir / (source_module + ".rb")
394
+ else:
395
+ candidate = ref_dir / source_module
396
+
397
+ if candidate.exists():
398
+ return [str(candidate)]
399
+
400
+ # Fallback: try without extension adjustment.
401
+ candidate2 = ref_dir / source_module
402
+ if candidate2.exists():
403
+ return [str(candidate2)]
404
+ except Exception: # noqa: BLE001
405
+ pass
406
+ return []
407
+
408
+
409
+ # ── C ─────────────────────────────────────────────────────────────────────────
410
+
411
+
412
+ def _c_extract_include(node: Any, filepath: Path) -> _ImportMapping | None:
413
+ """Extract an ImportMapping from a C/C++ preproc_include node.
414
+
415
+ Handles:
416
+ #include "utils.h" → local_name='utils', source_module='utils.h'
417
+ #include <stdio.h> → local_name='stdio', source_module='<stdio.h>'
418
+
419
+ Returns None if the include path cannot be resolved.
420
+ """
421
+ try:
422
+ path_node = node.child_by_field_name("path")
423
+ if path_node is None:
424
+ return None
425
+
426
+ line = node.start_point[0] + 1
427
+
428
+ if path_node.type == "string_literal":
429
+ # Local include: #include "utils.h"
430
+ content = None
431
+ for child in path_node.children:
432
+ if child.type == "string_content":
433
+ content = _text_node(child)
434
+ break
435
+ if content is None:
436
+ content = _text_node(path_node).strip('"')
437
+ if not content:
438
+ return None
439
+ stem = Path(content).stem
440
+ return _make_mapping(
441
+ local_name=stem,
442
+ exported_name=stem,
443
+ source_module=content,
444
+ line=line,
445
+ is_default=True,
446
+ )
447
+
448
+ if path_node.type == "system_lib_string":
449
+ # System include: #include <stdio.h>
450
+ raw = _text_node(path_node).strip("<>")
451
+ if not raw:
452
+ return None
453
+ stem = Path(raw).stem
454
+ return _make_mapping(
455
+ local_name=stem,
456
+ exported_name=stem,
457
+ source_module=f"<{raw}>",
458
+ line=line,
459
+ is_default=True,
460
+ )
461
+ except Exception: # noqa: BLE001
462
+ pass
463
+ return None
464
+
465
+
466
+ def _extract_c(root: object, filepath: Path) -> _ImportMappingList:
467
+ """Extract ImportMapping records from a C AST root node.
468
+
469
+ Handles:
470
+ #include "utils.h" → local_name='utils', source_module='utils.h'
471
+ #include <stdio.h> → local_name='stdio', source_module='<stdio.h>'
472
+
473
+ Resolution: relative #include "x.h" → best-effort file path probe
474
+ (same dir, then repo_root). System #include <x> → [].
475
+ Never raises. Returns [] on any failure.
476
+ """
477
+ result: _ImportMappingList = []
478
+ try:
479
+ from tree_sitter import Node
480
+
481
+ if not isinstance(root, Node):
482
+ return result
483
+
484
+ def _walk(node: Any) -> None:
485
+ if node.type == "preproc_include":
486
+ mapping = _c_extract_include(node, filepath)
487
+ if mapping is not None:
488
+ result.append(mapping)
489
+ return # no need to recurse into include node
490
+ for child in node.named_children:
491
+ _walk(child)
492
+
493
+ _walk(root)
494
+ except Exception as exc: # noqa: BLE001
495
+ logger.debug("_extract_c: extraction failed for %s: %r", filepath, exc)
496
+ return result
497
+
498
+
499
+ def _resolve_c(
500
+ source_module: str,
501
+ referencing_file: Path,
502
+ repo_root: Path,
503
+ ) -> list[str]:
504
+ """Resolve a C #include path to source file paths.
505
+
506
+ #include "x.h" → probe same directory as referencing_file, then repo_root.
507
+ #include <x.h> → [] (system header — out of scope per spec).
508
+
509
+ Returns a list of matching file paths (as str), or [] if not found.
510
+ Never raises.
511
+ """
512
+ try:
513
+ # System includes start with '<' — out of scope per spec
514
+ if source_module.startswith("<"):
515
+ return []
516
+
517
+ # Local include: probe relative to the referencing file's directory
518
+ ref_dir = referencing_file.parent
519
+ candidate = ref_dir / source_module
520
+ if candidate.exists():
521
+ return [str(candidate)]
522
+
523
+ # Fallback: probe relative to repo root
524
+ candidate2 = repo_root / source_module
525
+ if candidate2.exists():
526
+ return [str(candidate2)]
527
+ except Exception: # noqa: BLE001
528
+ pass
529
+ return []
530
+
531
+
532
+ # ── C++ ───────────────────────────────────────────────────────────────────────
533
+
534
+
535
+ def _extract_cpp(root: object, filepath: Path) -> _ImportMappingList:
536
+ """Extract ImportMapping records from a C++ AST root node.
537
+
538
+ C++ uses the same preproc_include mechanism as C. Both local and system
539
+ includes are extracted; system includes resolve to [] at read time.
540
+ Never raises. Returns [] on any failure.
541
+ """
542
+ # C++ and C share the same preproc_include grammar node — reuse _extract_c.
543
+ return _extract_c(root, filepath)
544
+
545
+
546
+ def _resolve_cpp(
547
+ source_module: str,
548
+ referencing_file: Path,
549
+ repo_root: Path,
550
+ ) -> list[str]:
551
+ """Resolve a C++ #include path to source file paths.
552
+
553
+ #include "x.h" → probe same dir, then repo_root (same as C).
554
+ #include <x> → [] (system/STL header — out of scope per spec).
555
+
556
+ Never raises.
557
+ """
558
+ return _resolve_c(source_module, referencing_file, repo_root)
559
+
560
+
561
+ # ── PHP ───────────────────────────────────────────────────────────────────────
562
+
563
+
564
+ def _php_qualified_name_last_segment(node: Any) -> str | None:
565
+ """Extract the last 'name' segment from a PHP qualified_name node.
566
+
567
+ qualified_name: [namespace_name, "\\", name]
568
+ Returns the final 'name' child text (e.g. 'User' from 'App\\Models\\User').
569
+ Also handles bare 'name' nodes directly.
570
+ """
571
+ try:
572
+ if node.type == "name":
573
+ return _text_node(node)
574
+ if node.type == "qualified_name":
575
+ # Find the last 'name' child (after all backslashes).
576
+ last_name = None
577
+ for child in node.children:
578
+ if child.type == "name":
579
+ last_name = _text_node(child)
580
+ return last_name
581
+ except Exception: # noqa: BLE001
582
+ pass
583
+ return None
584
+
585
+
586
+ def _php_clause_mapping(clause: Any, line: int) -> "_ImportMapping | None":
587
+ """Extract an ImportMapping from a single PHP namespace_use_clause node.
588
+
589
+ Handles two forms:
590
+ Plain: namespace_use_clause → qualified_name (→ name segments)
591
+ local_name = exported_name = last segment (e.g. 'User')
592
+ Aliased: namespace_use_clause → qualified_name + 'as' + name (alias)
593
+ local_name = alias (e.g. 'Col'), exported_name = last segment (e.g. 'Collection')
594
+ WHY: consistent with TS/Rust alias convention — local_name is the
595
+ binding used in this file; exported_name is the real symbol name.
596
+
597
+ Returns None if the clause cannot be parsed.
598
+ """
599
+ named = clause.named_children
600
+ if not named:
601
+ return None
602
+
603
+ # Detect alias form: named_children = [qualified_name, name(alias)]
604
+ # The alias 'name' follows the qualified_name (and the 'as' keyword child).
605
+ qual_node = None
606
+ alias_node = None
607
+ for child in named:
608
+ if child.type == "qualified_name":
609
+ qual_node = child
610
+ elif child.type == "name":
611
+ # This is either a bare clause (use Foo) or the alias after 'as'.
612
+ alias_node = child
613
+
614
+ if qual_node is not None:
615
+ exported = _php_qualified_name_last_segment(qual_node)
616
+ full = _text_node(qual_node).strip()
617
+ if not exported:
618
+ return None
619
+ if alias_node is not None:
620
+ # Aliased: use App\Foo as Bar → local_name='Bar', exported_name='Foo'
621
+ local = _text_node(alias_node).strip()
622
+ else:
623
+ # Plain: use App\Foo → local_name='Foo', exported_name='Foo'
624
+ local = exported
625
+ return _make_mapping(
626
+ local_name=local,
627
+ exported_name=exported,
628
+ source_module=full,
629
+ line=line,
630
+ is_default=True,
631
+ )
632
+
633
+ if alias_node is not None:
634
+ # Bare name clause (e.g. inside a group: use App\{Foo} → bare 'Foo')
635
+ name = _text_node(alias_node).strip()
636
+ if name:
637
+ return _make_mapping(
638
+ local_name=name,
639
+ exported_name=name,
640
+ source_module=name,
641
+ line=line,
642
+ is_default=True,
643
+ )
644
+
645
+ return None
646
+
647
+
648
+ def _extract_php(root: object, filepath: Path) -> _ImportMappingList:
649
+ """Extract ImportMapping records from a PHP AST root node.
650
+
651
+ Handles:
652
+ use App\\Models\\User; → local_name='User', source='App\\Models\\User'
653
+ use App\\Support\\Collection as Col → local_name='Col', exported_name='Collection'
654
+ use App\\{Foo, Bar}; → local_name='Foo'; local_name='Bar'
655
+
656
+ Resolution: PSR-4 autoload mapping is out of scope per the spec → returns [].
657
+ Never raises. Returns [] on any failure.
658
+ """
659
+ result: _ImportMappingList = []
660
+ try:
661
+ from tree_sitter import Node
662
+
663
+ if not isinstance(root, Node):
664
+ return result
665
+
666
+ def _walk(node: Any) -> None:
667
+ if node.type == "namespace_use_declaration":
668
+ line = node.start_point[0] + 1
669
+ for child in node.named_children:
670
+ if child.type == "namespace_use_clause":
671
+ mapping = _php_clause_mapping(child, line)
672
+ if mapping is not None:
673
+ result.append(mapping)
674
+ elif child.type == "namespace_use_group":
675
+ # Grouped use: use App\{Foo, Bar} — descend into the group.
676
+ for clause in child.named_children:
677
+ if clause.type == "namespace_use_clause":
678
+ mapping = _php_clause_mapping(clause, line)
679
+ if mapping is not None:
680
+ result.append(mapping)
681
+ return # no recursion needed into use declaration
682
+
683
+ for child in node.named_children:
684
+ _walk(child)
685
+
686
+ _walk(root)
687
+ except Exception as exc: # noqa: BLE001
688
+ logger.debug("_extract_php: extraction failed for %s: %r", filepath, exc)
689
+ return result
690
+
691
+
692
+ def _resolve_php(
693
+ source_module: str,
694
+ referencing_file: Path,
695
+ repo_root: Path,
696
+ ) -> list[str]:
697
+ """Resolve a PHP use statement to source file paths.
698
+
699
+ PSR-4 autoload mapping is out of scope per the spec.
700
+ Returns [] — degrading cleanly to the name-count resolution rule.
701
+
702
+ WHY: PHP PSR-4 requires a composer.json namespace→directory mapping which
703
+ is not available at extraction time. Returning [] causes confidence.py to
704
+ fall back to the name-count rule (AMBIGUOUS if multiple declarations exist).
705
+ """
706
+ return [] # Out of scope per spec — PHP PSR-4 autoload not implemented
707
+
708
+
709
+ # ── Swift ─────────────────────────────────────────────────────────────────────
710
+
711
+
712
+ def _extract_swift(root: object, filepath: Path) -> _ImportMappingList:
713
+ """Extract ImportMapping records from a Swift AST root node.
714
+
715
+ Handles:
716
+ import Foundation → local_name='Foundation', source_module='Foundation'
717
+ import UIKit.UIView → local_name='UIView', source_module='UIKit.UIView'
718
+ (last segment of dotted path per spec)
719
+
720
+ Resolution: Swift modules are not file-path-resolvable in-repo without a build
721
+ graph (frameworks live outside the project tree). _resolve_swift always returns [].
722
+ Never raises. Returns [] on any failure.
723
+ """
724
+ result: _ImportMappingList = []
725
+ try:
726
+ from tree_sitter import Node
727
+
728
+ if not isinstance(root, Node):
729
+ return result
730
+
731
+ def _walk(node: Any) -> None:
732
+ if node.type == "import_declaration":
733
+ line = node.start_point[0] + 1
734
+ # Find the 'identifier' child → collect simple_identifier segments → last one.
735
+ for child in node.children:
736
+ if child.type == "identifier":
737
+ segments = [
738
+ _text_node(gc)
739
+ for gc in child.children
740
+ if gc.type == "simple_identifier"
741
+ ]
742
+ if segments:
743
+ target = segments[-1]
744
+ full = ".".join(s for s in segments if s)
745
+ result.append(
746
+ _make_mapping(
747
+ local_name=target,
748
+ exported_name=target,
749
+ source_module=full,
750
+ line=line,
751
+ is_default=True,
752
+ )
753
+ )
754
+ break
755
+ return # No recursion into import_declaration needed.
756
+ for child in node.named_children:
757
+ _walk(child)
758
+
759
+ _walk(root)
760
+ except Exception as exc: # noqa: BLE001
761
+ logger.debug("_extract_swift: extraction failed for %s: %r", filepath, exc)
762
+ return result
763
+
764
+
765
+ def _resolve_swift(
766
+ source_module: str,
767
+ referencing_file: Path,
768
+ repo_root: Path,
769
+ ) -> list[str]:
770
+ """Resolve a Swift import path to source file paths.
771
+
772
+ Swift modules (Foundation, UIKit, SwiftUI, etc.) are Apple SDK frameworks
773
+ and are NOT file-path-resolvable within the repo without a full build graph.
774
+ Returns [] — degrading cleanly to the name-count resolution rule in confidence.py.
775
+
776
+ WHY: Swift module resolution requires Xcode project / SwiftPM manifest parsing
777
+ which is out of scope per the spec. The degraded name-count rule still provides
778
+ EXTRACTED/AMBIGUOUS distinction for in-repo symbols.
779
+ """
780
+ return [] # Out of scope per spec — Swift module resolution not implemented