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,644 @@
1
+ """Shared primitive types, constants, and helpers for the Seam indexer graph layer.
2
+
3
+ LAYER: leaf — imports only stdlib and tree_sitter. This module must never import
4
+ from any other seam.indexer module.
5
+
6
+ LAYERING (import direction):
7
+ graph_common (this file — leaf, no seam deps)
8
+ ↑ ↑
9
+ graph.py graph_go_rust.py
10
+
11
+ Both graph.py and graph_go_rust.py import from here at their top levels.
12
+ Neither imports from the other, so no circular dependency exists.
13
+
14
+ WHY this split was necessary: before this module existed, graph_go_rust.py imported
15
+ helpers (Symbol, Edge, _text, etc.) from graph.py at its top level, and graph.py in
16
+ turn tried to import the Go/Rust extractors from graph_go_rust.py — a circular init.
17
+ The only escape at the time was deferred in-function imports, which violated the
18
+ project rule that all imports must be at the top of every file. Moving the shared
19
+ primitives here (a leaf with no seam deps) cuts the cycle completely, restores
20
+ top-level-only imports in all three files, and is the authoritative home for the
21
+ public TypedDicts so callers can do:
22
+ from seam.indexer.graph import Symbol, Edge # still works — graph re-exports these
23
+
24
+ Contents:
25
+ - TypedDicts: Confidence, Symbol, Edge, Comment
26
+ - Constants: SEMANTIC_MARKERS, _MARKER_RE
27
+ - Helpers: _text, _node_name, _make_symbol, _match_marker,
28
+ _block_comment_lines, _arrow_function_name, _find_enclosing_function
29
+ - Go/Rust receiver helpers: _go_recv_type_name, _rust_impl_type_name
30
+ (kept here so _find_enclosing_function can call them without importing
31
+ from graph_go_rust — this is what keeps the leaf property intact)
32
+ """
33
+
34
+ import logging
35
+ import re
36
+ from typing import Literal, NotRequired, TypedDict
37
+
38
+ from tree_sitter import Node
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+ # ── Semantic comment markers (WHY-extraction feature, Phase 1b) ───────────────
43
+
44
+ # Fixed set of marker keywords matched case-insensitively at the START of the
45
+ # comment body (after stripping the delimiter and leading whitespace).
46
+ SEMANTIC_MARKERS: frozenset[str] = frozenset({"WHY", "HACK", "NOTE", "TODO", "FIXME"})
47
+
48
+ # Pre-compiled regex: group 1=marker keyword, group 3=remainder text.
49
+ # Marker must be followed by ':', whitespace, or end-of-string — blocks prefix
50
+ # matches like 'whyever'.
51
+ _MARKER_RE = re.compile(
52
+ r"^(WHY|HACK|NOTE|TODO|FIXME)(?::|((?=\s)|$))(.*)",
53
+ re.IGNORECASE | re.DOTALL,
54
+ )
55
+
56
+ # Confidence level for an edge — persisted in the DB, exposed via MCP.
57
+ Confidence = Literal["EXTRACTED", "INFERRED", "AMBIGUOUS"]
58
+
59
+
60
+ class Symbol(TypedDict):
61
+ name: str
62
+ kind: str # 'function' | 'class' | 'method' | 'interface' | 'type'
63
+ file: str # str(path) — resolved at call time
64
+ start_line: int
65
+ end_line: int
66
+ docstring: str | None
67
+ # Phase 4 — Node-field enrichment. All nullable: None means "not yet extracted"
68
+ # (e.g. pre-v5 rows) rather than "has no value". Callers must not treat None as
69
+ # equivalent to empty string or False.
70
+ signature: str | None # declaration header, single line, truncated at SEAM_MAX_SIGNATURE_LEN
71
+ decorators: list[str] # verbatim decorator text (Python/TS only); [] for Go/Rust
72
+ is_exported: bool | None # export status; None when language has no uniform export concept
73
+ visibility: str | None # "public"|"private"|"protected"|"crate"; Python uses heuristic
74
+ qualified_name: str | None # "ClassName.method" for methods; None for anonymous/top-level unknown
75
+
76
+
77
+ class Edge(TypedDict):
78
+ source: str # Symbol name of caller / importer
79
+ target: str # Symbol name of callee / importee
80
+ # Edge kind vocabulary:
81
+ # 'import' — module/symbol import statement
82
+ # 'call' — function/method call edge
83
+ # 'extends' — class inheritance (base class)
84
+ # 'implements' — interface implementation
85
+ # 'instantiates' — object construction (new Foo(), Foo(), Foo::new(), Foo{}) [Tier B B6]
86
+ # 'holds' — composition: a class stores a plain user type as a typed field/property
87
+ # OR receives one as a typed constructor/init parameter [Slice #77]
88
+ # 'uses' — a function/method references a plain user type as a PARAMETER in its
89
+ # signature (e.g. f(x: T) → f uses T). Complements 'holds' (stored
90
+ # composition) with signature-level coupling. Gated by SEAM_PARAM_EDGES.
91
+ kind: str
92
+ file: str
93
+ line: int
94
+ confidence: Confidence # EXTRACTED | INFERRED | AMBIGUOUS
95
+ # Tier B B1 (v10): raw receiver expression text for attribute calls (e.g. 'self', 'obj').
96
+ # None for import edges, bare-identifier call edges, and pre-v10 rows.
97
+ #
98
+ # WHY NotRequired (not a plain required field): the Edge TypedDict is constructed in
99
+ # many call sites across 12 language extractors. Making receiver Required would force
100
+ # all existing Edge() instantiations to be updated simultaneously. NotRequired lets
101
+ # us add the field incrementally — old callers remain valid, new callers opt in.
102
+ # upsert_file uses edge.get("receiver") which defaults to None, so absent ≡ None
103
+ # at the DB layer. This is the same null-contract as Phase 4/5 enrichment fields.
104
+ #
105
+ # WHY stored even when target is already qualified (e.g. 'Client.method'):
106
+ # Preserves the raw receiver text for debugging, future re-inference passes, and
107
+ # any tooling that needs to re-derive qualification without a full re-index.
108
+ # Edges remain string-name-keyed (source/target are names, not node IDs) as required
109
+ # for independent re-indexing.
110
+ receiver: NotRequired[str | None]
111
+ # v12: synthesis channel that produced this edge. None for parser-extracted edges;
112
+ # a channel name string (e.g. 'interface-override') for edges emitted by the
113
+ # post-pass synthesis engine (seam/analysis/synthesis.py).
114
+ #
115
+ # WHY NotRequired: same rationale as receiver — all 12 extractors construct Edge()
116
+ # dicts without this field, and synthesis adds it only on its own output.
117
+ # upsert_file uses edge.get("synthesized_by") defaulting to None, so absent ≡ NULL.
118
+ # Provenance is DERIVED: synthesized_by IS NOT NULL ⟹ heuristic edge. This avoids
119
+ # a separate boolean column and keeps the schema additive.
120
+ synthesized_by: NotRequired[str | None]
121
+
122
+
123
+ class Comment(TypedDict):
124
+ """A semantic comment extracted from source code during indexing.
125
+
126
+ Only WHY/HACK/NOTE/TODO/FIXME-tagged comments are stored — plain
127
+ comments are ignored. Marker is normalized to UPPERCASE.
128
+ """
129
+ marker: str # Normalized: WHY | HACK | NOTE | TODO | FIXME
130
+ text: str # Body after the marker (and optional colon), stripped
131
+ line: int # 1-based line number in the source file
132
+
133
+
134
+ # ── Low-level AST helpers ──────────────────────────────────────────────────────
135
+
136
+
137
+ def _text(node: Node) -> str:
138
+ """Safely decode a tree-sitter node's text bytes to str.
139
+
140
+ node.text is typed as bytes | None in the stubs; guard against None.
141
+ """
142
+ raw = node.text
143
+ if raw is None:
144
+ return ""
145
+ return raw.decode("utf-8", errors="replace")
146
+
147
+
148
+ def _node_name(node: Node) -> str | None:
149
+ """Return the text of the 'name' field child, or None if absent."""
150
+ name_node = node.child_by_field_name("name")
151
+ if name_node is None:
152
+ return None
153
+ return _text(name_node)
154
+
155
+
156
+ def _make_symbol(
157
+ name: str,
158
+ kind: str,
159
+ file: str,
160
+ node: Node,
161
+ docstring: str | None,
162
+ signature: str | None = None,
163
+ decorators: list[str] | None = None,
164
+ is_exported: bool | None = None,
165
+ visibility: str | None = None,
166
+ qualified_name: str | None = None,
167
+ ) -> Symbol:
168
+ """Construct a Symbol TypedDict from a tree-sitter node.
169
+
170
+ Phase 4 enrichment fields default to None/[] so callers that don't yet call
171
+ extract_node_fields continue to work without changes — backward-compatible extension
172
+ rather than requiring every call site to be updated simultaneously.
173
+ """
174
+ return Symbol(
175
+ name=name,
176
+ kind=kind,
177
+ file=file,
178
+ start_line=node.start_point[0] + 1, # tree-sitter rows are 0-based
179
+ end_line=node.end_point[0] + 1,
180
+ docstring=docstring,
181
+ signature=signature,
182
+ decorators=decorators if decorators is not None else [],
183
+ is_exported=is_exported,
184
+ visibility=visibility,
185
+ qualified_name=qualified_name,
186
+ )
187
+
188
+
189
+ def _match_marker(body: str) -> tuple[str, str] | None:
190
+ """Try to match a semantic marker at the start of a stripped comment body.
191
+
192
+ Returns (marker_upper, text) if matched, or None.
193
+ The regex requires the marker to be followed by ':', whitespace, or end-of-string.
194
+ """
195
+ m = _MARKER_RE.match(body)
196
+ if not m:
197
+ return None
198
+ marker = m.group(1).upper()
199
+ text = (m.group(3) or "").strip()
200
+ return marker, text
201
+
202
+
203
+ def _block_comment_lines(raw: str) -> list[tuple[int, str]]:
204
+ """Return (line_offset, cleaned_body) for each non-empty line of a /* */ block.
205
+
206
+ line_offset is the 0-based line index from the block's first line.
207
+ Each line has /*, */ and leading '*' decorations stripped. Empty lines omitted.
208
+ """
209
+ out: list[tuple[int, str]] = []
210
+ for i, line in enumerate(raw.splitlines()):
211
+ s = line.strip()
212
+ if s.startswith("/*"):
213
+ s = s[2:]
214
+ if s.endswith("*/"):
215
+ s = s[:-2]
216
+ s = s.strip().lstrip("*").strip()
217
+ if s:
218
+ out.append((i, s))
219
+ return out
220
+
221
+
222
+ # ── Go/Rust receiver helpers (leaf — no circular dep) ─────────────────────────
223
+
224
+
225
+ def _go_recv_type_name(method_node: Node) -> str | None:
226
+ """Extract the receiver type name from a Go method_declaration.
227
+
228
+ Go method receivers come in four forms; all are normalized to a plain type name:
229
+ - value receiver: func (r Foo) M() → 'Foo' (type_identifier)
230
+ - pointer receiver: func (r *Foo) M() → 'Foo' (pointer_type → type_identifier)
231
+ - generic value receiver: func (r Foo[T]) M() → 'Foo' (generic_type → type_identifier)
232
+ - generic pointer recv: func (r *Repo[T]) M() → 'Repo' (pointer_type → generic_type → type_identifier)
233
+
234
+ Normalization rule: always extract the base type_identifier, discarding `*` and `[T]`.
235
+ Returns None if the receiver list is absent or the type node has an unexpected shape.
236
+ """
237
+ recv = method_node.child_by_field_name("receiver")
238
+ if recv is None:
239
+ return None
240
+ for pd in recv.named_children:
241
+ if pd.type == "parameter_declaration":
242
+ typ = pd.child_by_field_name("type")
243
+ if typ is None:
244
+ continue
245
+ if typ.type == "pointer_type":
246
+ # *T or *T[K]: inspect the single named child of pointer_type.
247
+ inner = next(iter(typ.named_children), None)
248
+ if inner is None:
249
+ continue
250
+ if inner.type == "type_identifier":
251
+ # Simple pointer receiver: *Foo → 'Foo'
252
+ return _text(inner)
253
+ if inner.type == "generic_type":
254
+ # Generic pointer receiver: *Repo[T] → extract 'Repo' from generic_type
255
+ base = next(
256
+ (c for c in inner.named_children if c.type == "type_identifier"),
257
+ None,
258
+ )
259
+ if base is not None:
260
+ return _text(base)
261
+ elif typ.type == "type_identifier":
262
+ # Plain value receiver: Foo → 'Foo'
263
+ return _text(typ)
264
+ elif typ.type == "generic_type":
265
+ # Generic value receiver: Repo[T] → extract 'Repo' from generic_type
266
+ base = next(
267
+ (c for c in typ.named_children if c.type == "type_identifier"),
268
+ None,
269
+ )
270
+ if base is not None:
271
+ return _text(base)
272
+ return None
273
+
274
+
275
+ def _rust_impl_type_name(impl_node: Node) -> str | None:
276
+ """Extract the base type name from a Rust impl_item node.
277
+
278
+ Handles:
279
+ - Plain impl: impl Foo { ... } → 'Foo' (type field is type_identifier)
280
+ - Generic impl: impl<T> Foo<T> { ... } → 'Foo' (type field is generic_type;
281
+ extract the first type_identifier child)
282
+
283
+ Returns the base type_identifier string (e.g. 'Foo', not 'Foo<T>'), or None if
284
+ the type field is absent or has an unexpected shape.
285
+ """
286
+ type_node = impl_node.child_by_field_name("type")
287
+ if type_node is None:
288
+ return None
289
+ if type_node.type == "type_identifier":
290
+ return _text(type_node)
291
+ if type_node.type == "generic_type":
292
+ base = next(
293
+ (c for c in type_node.named_children if c.type == "type_identifier"),
294
+ None,
295
+ )
296
+ if base is not None:
297
+ return _text(base)
298
+ return None
299
+
300
+
301
+ # ── Inheritance base-name normalizer (P6a) ───────────────────────────────────
302
+
303
+
304
+ def _base_type_name(node: Node) -> str | None:
305
+ """Normalize a base-class / interface type node to its bare type NAME.
306
+
307
+ The edge graph is name-keyed and homonym-collapsed, so generic arguments and
308
+ namespace/package qualifiers are stripped to the rightmost simple type name —
309
+ matching how call/import targets are stored. Handles the shapes that appear in
310
+ base-class / interface clauses across Python, TypeScript, Java, and C#:
311
+
312
+ - identifier / type_identifier → the text directly (Base)
313
+ - generic_type (Java/TS: Base<T>) → first type_identifier child (Base)
314
+ - generic_name (C#: IFace<T>) → first identifier child (IFace)
315
+ - qualified_name (C#: Ns.Base) → 'name' field, else last identifier child
316
+ - scoped_type_identifier (Java: a.B) → 'name' field, else last type_identifier
317
+ - member_expression (TS: ns.Base) → 'property' field, else last child
318
+
319
+ Returns the bare name string, or None when the node shape is unrecognized
320
+ (the caller then skips emitting an edge — never raises).
321
+ """
322
+ t = node.type
323
+ if t in ("identifier", "type_identifier"):
324
+ return _text(node)
325
+ if t in ("generic_type", "generic_name"):
326
+ for c in node.named_children:
327
+ if c.type in ("type_identifier", "identifier"):
328
+ return _text(c)
329
+ return None
330
+ if t in ("qualified_name", "scoped_type_identifier"):
331
+ name_node = node.child_by_field_name("name")
332
+ if name_node is not None:
333
+ return _text(name_node)
334
+ # Fallback: rightmost identifier-like child.
335
+ for c in reversed(node.named_children):
336
+ if c.type in ("identifier", "type_identifier"):
337
+ return _text(c)
338
+ return None
339
+ if t == "member_expression":
340
+ prop = node.child_by_field_name("property")
341
+ if prop is not None:
342
+ return _text(prop)
343
+ return None
344
+
345
+
346
+ # ── Arrow-function name resolver (TypeScript/JS only) ─────────────────────────
347
+
348
+
349
+ def _arrow_function_name(arrow_node: Node) -> str | None:
350
+ """Resolve the name of an arrow function from its assignment context.
351
+
352
+ Returns the variable name if `const X = () => {...}`, else None.
353
+ """
354
+ parent = arrow_node.parent
355
+ if parent is None:
356
+ return None
357
+ if parent.type == "variable_declarator":
358
+ name_node = parent.child_by_field_name("name")
359
+ if name_node is not None and name_node.type == "identifier":
360
+ return _text(name_node)
361
+ return None
362
+
363
+
364
+ # ── Enclosing-scope resolver ───────────────────────────────────────────────────
365
+
366
+
367
+ def _c_cpp_function_name_from_def(func_def: Node) -> str | None:
368
+ """Extract the function name from a C or C++ function_definition node.
369
+
370
+ C/C++ function_definition has NO 'name' field directly — the name is nested:
371
+ function_definition → declarator (function_declarator) → declarator (identifier
372
+ | field_identifier | qualified_identifier).
373
+
374
+ Returns:
375
+ - Plain name (str) for free functions and in-class method declarations.
376
+ - 'Class.method' qualified name for out-of-line definitions (Class::method).
377
+ - None if the name cannot be resolved.
378
+
379
+ WHY this helper is in graph_common (leaf): _find_enclosing_function needs it to
380
+ resolve enclosing scope names for C/C++ call edges. Putting it in graph_c_cpp
381
+ would create a cycle (graph_c_cpp → graph_common → graph_c_cpp). Keeping it here
382
+ (alongside the other language helpers like _go_recv_type_name) preserves the leaf.
383
+ """
384
+ try:
385
+ declarator = func_def.child_by_field_name("declarator")
386
+ if declarator is None or declarator.type != "function_declarator":
387
+ return None
388
+ inner = declarator.child_by_field_name("declarator")
389
+ if inner is None:
390
+ return None
391
+ if inner.type in ("identifier", "field_identifier"):
392
+ return _text(inner)
393
+ if inner.type == "qualified_identifier":
394
+ # Out-of-line C++ method: Class::method → 'Class.method'
395
+ scope = inner.child_by_field_name("scope")
396
+ name_node = inner.child_by_field_name("name")
397
+ if scope and name_node:
398
+ return f"{_text(scope)}.{_text(name_node)}"
399
+ except Exception: # noqa: BLE001
400
+ pass
401
+ return None
402
+
403
+
404
+ def _find_enclosing_function(node: Node, language: str) -> str | None:
405
+ """Walk up the parent chain to find the nearest enclosing function/method name.
406
+
407
+ Returns 'ClassName.methodName' for methods, plain name for functions, or None
408
+ when no enclosing function exists (e.g. top-level module code). None causes the
409
+ caller to drop the edge — only named scopes produce edge sources.
410
+
411
+ Supports all 12 Seam languages: python, typescript, javascript, go, rust,
412
+ java, csharp, ruby, c, cpp, php, swift. Per-language function/method node types
413
+ and class-context types are defined in the body below.
414
+
415
+ WHY in this leaf module: both graph.py and the family modules (graph_go_rust,
416
+ graph_java_csharp, graph_c_cpp, graph_ruby_php) call this for call-edge source
417
+ resolution. Keeping it here avoids circular imports between family modules.
418
+ """
419
+ func_types_py = {"function_definition"}
420
+ func_types_ts = {"function_declaration", "method_definition", "arrow_function"}
421
+ func_types_go = {"function_declaration", "method_declaration"}
422
+ func_types_rust = {"function_item"}
423
+ # Phase 9 function/method node types per language
424
+ func_types_java = {"method_declaration", "constructor_declaration"}
425
+ func_types_csharp = {
426
+ "method_declaration",
427
+ "constructor_declaration",
428
+ "local_function_statement",
429
+ }
430
+ func_types_ruby = {"method", "singleton_method"}
431
+ func_types_c = {"function_definition"}
432
+ func_types_cpp = {"function_definition"}
433
+ func_types_php = {"method_declaration", "function_definition"}
434
+ # Phase 10 Swift: function_declaration is the sole function/method node type.
435
+ func_types_swift = {"function_declaration", "protocol_function_declaration"}
436
+
437
+ # Phase 9 class-context node types for qualified 'Class.method' names
438
+ class_types_java = {
439
+ "class_declaration",
440
+ "interface_declaration",
441
+ "enum_declaration",
442
+ "record_declaration",
443
+ }
444
+ class_types_csharp = {
445
+ "class_declaration",
446
+ "struct_declaration",
447
+ "record_declaration",
448
+ "interface_declaration",
449
+ }
450
+ class_types_ruby = {"class", "module"}
451
+ class_types_cpp = {"class_specifier", "struct_specifier"}
452
+ class_types_php = {
453
+ "class_declaration",
454
+ "interface_declaration",
455
+ "trait_declaration",
456
+ }
457
+ # Phase 10 Swift: class_declaration covers class/struct/actor/extension;
458
+ # protocol_declaration covers protocol methods.
459
+ class_types_swift = {"class_declaration", "protocol_declaration"}
460
+
461
+ if language == "go":
462
+ func_types = func_types_go
463
+ elif language == "rust":
464
+ func_types = func_types_rust
465
+ elif language == "java":
466
+ func_types = func_types_java
467
+ elif language == "csharp":
468
+ func_types = func_types_csharp
469
+ elif language == "ruby":
470
+ func_types = func_types_ruby
471
+ elif language == "c":
472
+ func_types = func_types_c
473
+ elif language == "cpp":
474
+ func_types = func_types_cpp
475
+ elif language == "php":
476
+ func_types = func_types_php
477
+ elif language == "swift":
478
+ func_types = func_types_swift
479
+ else:
480
+ func_types = func_types_py if language == "python" else func_types_ts
481
+
482
+ # Fallback: name of the innermost const-assigned arrow found while walking up.
483
+ fallback_arrow_name: str | None = None
484
+
485
+ current = node.parent
486
+ while current is not None:
487
+ if current.type in func_types:
488
+ if current.type == "arrow_function":
489
+ if fallback_arrow_name is None:
490
+ fallback_arrow_name = _arrow_function_name(current)
491
+ current = current.parent
492
+ continue
493
+
494
+ # Go method_declaration: build qualified 'Recv.Name' using shared helper.
495
+ if language == "go" and current.type == "method_declaration":
496
+ # FIX 3: if the method has no name, skip — do NOT fall back to full node text.
497
+ func_name_node = current.child_by_field_name("name")
498
+ if func_name_node is None:
499
+ current = current.parent
500
+ continue
501
+ func_name = _text(func_name_node)
502
+ recv_name = _go_recv_type_name(current)
503
+ if recv_name:
504
+ return f"{recv_name}.{func_name}"
505
+ return func_name
506
+
507
+ # Rust function_item inside impl_item: build 'Type.fn' qualified name.
508
+ if language == "rust" and current.type == "function_item":
509
+ func_name_node = current.child_by_field_name("name")
510
+ if func_name_node is None:
511
+ current = current.parent
512
+ continue
513
+ func_name = _text(func_name_node)
514
+ parent = current.parent
515
+ if parent is not None and parent.type == "declaration_list":
516
+ grandparent = parent.parent
517
+ if grandparent is not None and grandparent.type == "impl_item":
518
+ type_name = _rust_impl_type_name(grandparent)
519
+ if type_name is not None:
520
+ return f"{type_name}.{func_name}"
521
+ return func_name
522
+
523
+ # C/C++ function_definition: name lives in declarator chain, not a 'name' field.
524
+ # function_definition → function_declarator ('declarator' field) →
525
+ # identifier or field_identifier or qualified_identifier ('declarator' field).
526
+ if language in ("c", "cpp") and current.type == "function_definition":
527
+ c_func_name = _c_cpp_function_name_from_def(current)
528
+ if c_func_name is None:
529
+ current = current.parent
530
+ continue
531
+ # C++ out-of-line method (e.g. 'Circle.area') is already qualified
532
+ if "." in c_func_name:
533
+ return c_func_name
534
+ # C++ in-class: check for enclosing class_specifier/struct_specifier
535
+ if language == "cpp":
536
+ parent = current.parent
537
+ while parent is not None:
538
+ if parent.type in class_types_cpp:
539
+ cls_name_node = parent.child_by_field_name("name")
540
+ if cls_name_node is not None:
541
+ return f"{_text(cls_name_node)}.{c_func_name}"
542
+ parent = parent.parent
543
+ return c_func_name
544
+
545
+ # Swift function_declaration has no 'name' field — name is a simple_identifier child.
546
+ # Also handle protocol_function_declaration the same way.
547
+ if language == "swift" and current.type in (
548
+ "function_declaration",
549
+ "protocol_function_declaration",
550
+ ):
551
+ swift_func_name: str | None = None
552
+ for sc in current.children:
553
+ if sc.type == "simple_identifier":
554
+ swift_func_name = _text(sc)
555
+ break
556
+ if swift_func_name is None:
557
+ current = current.parent
558
+ continue
559
+ func_name = swift_func_name
560
+ # Walk up for enclosing class_declaration or protocol_declaration
561
+ parent = current.parent
562
+ while parent is not None:
563
+ if parent.type in class_types_swift:
564
+ # Swift class_declaration has type_identifier child (not 'name' field)
565
+ # extension also uses type_identifier inside user_type
566
+ cls_name: str | None = None
567
+ kw_seen = False
568
+ for pc in parent.children:
569
+ if pc.type in ("class", "struct", "actor", "extension", "enum"):
570
+ kw_seen = True
571
+ continue
572
+ if kw_seen and pc.type == "type_identifier":
573
+ cls_name = _text(pc)
574
+ break
575
+ if kw_seen and pc.type == "user_type":
576
+ # extension node: name in user_type → type_identifier
577
+ for uc in pc.children:
578
+ if uc.type == "type_identifier":
579
+ cls_name = _text(uc)
580
+ break
581
+ if cls_name:
582
+ break
583
+ if kw_seen and pc.type in ("class_body", "protocol_body"):
584
+ break
585
+ if cls_name:
586
+ return f"{cls_name}.{func_name}"
587
+ # Protocol declaration: name is type_identifier directly
588
+ if parent.type == "protocol_declaration":
589
+ for pc in parent.children:
590
+ if pc.type == "type_identifier":
591
+ cls_name = _text(pc)
592
+ break
593
+ if cls_name:
594
+ return f"{cls_name}.{func_name}"
595
+ parent = parent.parent
596
+ return func_name
597
+
598
+ # Named scope (function_declaration, method_definition, function_definition).
599
+ name_node = current.child_by_field_name("name")
600
+ if name_node is None:
601
+ current = current.parent
602
+ continue
603
+ func_name = _text(name_node)
604
+
605
+ # Determine the set of class-context types for this language.
606
+ if language == "python":
607
+ class_types: set[str] = {"class_definition"}
608
+ elif language == "java":
609
+ class_types = class_types_java
610
+ elif language == "csharp":
611
+ class_types = class_types_csharp
612
+ elif language == "ruby":
613
+ class_types = class_types_ruby
614
+ elif language == "cpp":
615
+ class_types = class_types_cpp
616
+ elif language == "php":
617
+ class_types = class_types_php
618
+ elif language == "swift":
619
+ class_types = class_types_swift
620
+ elif language == "c":
621
+ # C has no classes — never qualify
622
+ class_types = set()
623
+ else:
624
+ # TypeScript/JavaScript
625
+ class_types = {"class_declaration", "class_body"}
626
+
627
+ parent = current.parent
628
+ while parent is not None:
629
+ if parent.type in class_types:
630
+ class_name_node = parent.child_by_field_name("name")
631
+ if class_name_node is not None:
632
+ cls_name = _text(class_name_node)
633
+ return f"{cls_name}.{func_name}"
634
+ parent = parent.parent
635
+ return func_name
636
+ current = current.parent
637
+
638
+ # No named function/method found; return arrow fallback or None.
639
+ if fallback_arrow_name is None:
640
+ logger.debug(
641
+ "_find_enclosing_function: no named scope found — edge source "
642
+ "cannot be resolved; edge will be dropped"
643
+ )
644
+ return fallback_arrow_name