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,902 @@
1
+ """Scope-inference module for receiver-type resolution in Python and TypeScript/JS.
2
+
3
+ LAYER: leaf — imports from graph_common (leaf) and stdlib only. Never imports from
4
+ graph.py, graph_swift, or any other seam module with side effects.
5
+
6
+ LAYERING:
7
+ graph_common (leaf — no seam deps)
8
+
9
+ graph_scope_infer (this file — pure receiver-type inference, no Edge construction)
10
+
11
+ graph.py (_extract_edges_python / _extract_edges_typescript use these helpers)
12
+
13
+ WHY a separate module:
14
+ 1. graph.py would exceed the 1000-line limit if this logic were inlined.
15
+ 2. The scope-inference cluster (pre-scan + per-function lookup + resolve) is a coherent
16
+ leaf unit — no Edge construction, no AST walker orchestration — so it belongs in a
17
+ dedicated leaf, mirroring the graph_swift_infer.py precedent from Phase 10.
18
+
19
+ CONSERVATISM CONTRACT (identical to graph_swift_infer):
20
+ NEVER emit a wrong edge. resolve_receiver_type returns None on ANY uncertainty:
21
+ - Optionals: Foo | None, Foo?, Optional[Foo] → None
22
+ - Containers: list[T], List[T], dict[K,V], Tuple[...] → None
23
+ - Generics: Array<T>, Set<T>, Map<K,V> → None
24
+ - Unknown: identifiers not found in the field/param/local scope → None
25
+ - Chained: a.b.c() where the 'b' field has no known type → None
26
+ Callers that receive None keep the raw bare target (never fabricate a type).
27
+
28
+ Two-layer scope model (order-independent):
29
+ Layer 1 — class-level pre-scan: field/property type bindings gathered for the whole
30
+ class body BEFORE walking methods. This is essential for DI'd stored properties
31
+ that are declared after the method that uses them.
32
+ Layer 2 — per-function scope: parameter and local-variable type bindings accumulated
33
+ during the function-body walk. Function scope is fresh for each function.
34
+
35
+ Self/this/cls normalization:
36
+ self / cls → enclosing class name (Python)
37
+ this / super → enclosing class name (TypeScript/JS) for 'super': same class because
38
+ we only know the type at declaration, not the base class; refuse if enclosing unknown.
39
+
40
+ All public functions never raise (backstop try/except in each body).
41
+ """
42
+
43
+ import logging
44
+
45
+ from tree_sitter import Node
46
+
47
+ from seam.indexer.graph_common import _text
48
+
49
+ logger = logging.getLogger(__name__)
50
+
51
+ # ── Self/this normalized receivers ────────────────────────────────────────────
52
+
53
+ # Python: 'self' and 'cls' are conventional but not enforced — treat any of these
54
+ # as "enclosing type" without caring about exact identifier.
55
+ _PY_SELF_NAMES: frozenset[str] = frozenset({"self", "cls"})
56
+
57
+ # TypeScript/JS: 'this' always refers to the enclosing class instance.
58
+ # 'super' is deliberately excluded — we can't statically know the base class.
59
+ _TS_SELF_NAMES: frozenset[str] = frozenset({"this"})
60
+
61
+
62
+ # ── Shared: resolve a receiver expression to a type name ─────────────────────
63
+
64
+
65
+ def resolve_receiver_type(
66
+ receiver_text: str,
67
+ class_name: str | None,
68
+ var_types: dict[str, str],
69
+ self_names: frozenset[str],
70
+ ) -> str | None:
71
+ """Resolve a receiver expression to its declared type name.
72
+
73
+ This is the core lookup that maps a receiver string (as captured by the edge
74
+ extractor) to a type name using the current scope state (class fields + params +
75
+ locals) — exactly mirroring _resolve_navigation_target in graph_swift_infer.
76
+
77
+ Args:
78
+ receiver_text: Raw receiver text captured from the AST (e.g. 'self', 'client',
79
+ 'this', 'a.b'). Multi-part receivers (containing '.') resolve
80
+ only if they start with a self/this alias followed by a known
81
+ class field.
82
+ class_name: Name of the enclosing class (or None for module-level functions).
83
+ var_types: scope map — name → type for all in-scope bindings (class fields
84
+ merged with per-function params and locals).
85
+ self_names: Set of receiver strings that alias 'self' (e.g. {'self','cls'}
86
+ for Python, {'this'} for TypeScript).
87
+
88
+ Returns:
89
+ The resolved type name string (e.g. 'Client'), or None when the type cannot
90
+ be determined with confidence. None → caller keeps the bare target.
91
+ """
92
+ try:
93
+ if not receiver_text:
94
+ return None
95
+
96
+ # Multi-part receiver: only handle 'self.field' / 'this.field' patterns.
97
+ # a.b.method() where 'a' is not a self-alias → refuse (cross-class chain).
98
+ if "." in receiver_text:
99
+ return _resolve_chained(receiver_text, class_name, var_types, self_names)
100
+
101
+ # Plain identifier — check self/this alias first.
102
+ if receiver_text in self_names:
103
+ return class_name # May be None (module-level scope)
104
+
105
+ # Look up in the scope map (class fields + params + locals).
106
+ return var_types.get(receiver_text) # None if not in scope
107
+
108
+ except Exception as exc: # noqa: BLE001
109
+ logger.debug("resolve_receiver_type: failed for %r: %r", receiver_text, exc)
110
+ return None
111
+
112
+
113
+ def _resolve_chained(
114
+ receiver_text: str,
115
+ class_name: str | None,
116
+ var_types: dict[str, str],
117
+ self_names: frozenset[str],
118
+ ) -> str | None:
119
+ """Resolve a dotted receiver like 'self.field' or 'this.repo'.
120
+
121
+ Only ONE level of chaining is supported:
122
+ self.field / this.field → look up field's type from var_types
123
+ Everything else (self.a.b, unknown.field, chained member expressions) → None.
124
+
125
+ This mirrors _resolve_navigation_target's self.prop.method handling in Swift:
126
+ only the self.<prop> form is resolvable since we only track class-level field types.
127
+ """
128
+ parts = receiver_text.split(".", 1)
129
+ if len(parts) != 2:
130
+ return None
131
+ head, field = parts
132
+ if head not in self_names:
133
+ # Foreign chain (a.b, not self.b) → refuse — would need cross-class field typing.
134
+ return None
135
+ return var_types.get(field) # None if the field has no known type
136
+
137
+
138
+ # ── Python: plain-type extraction ────────────────────────────────────────────
139
+
140
+
141
+ def _py_plain_type_from_annotation(ann_node: Node) -> str | None:
142
+ """Extract a plain type name from a Python type annotation node.
143
+
144
+ Conservative: returns the type name ONLY for bare identifiers. Refuses:
145
+ - Union types (X | Y, Optional[X]) → node type contains '|' or subscript
146
+ - Generic subscripts (list[T], List[T], dict[K,V])
147
+ - String-quoted annotations (deferred — we'd need to eval the string)
148
+ - Any complex expression
149
+
150
+ WHY refuse optionals and generics: `foo: Optional[Client]` means `foo` could be
151
+ None; emitting `Client.method()` on `foo.method()` would be wrong when foo=None.
152
+ `foo: list[Client]` means `foo` is a container — `foo.method()` resolves on list,
153
+ not Client. The conservatism contract requires NEVER emitting a wrong edge, so
154
+ any type that could be None or that wraps another type is refused entirely.
155
+
156
+ The node passed here is whatever tree-sitter calls the 'type' field, or
157
+ an 'annotation' node child. We inspect its grammar type:
158
+ identifier → plain bare name → accept
159
+ type → nested type node → recurse one level
160
+ subscript → generic subscript (list[Foo]) → refuse
161
+ binary_operator → X | Y union → refuse
162
+ attribute → dotted type (pkg.Foo) → refuse
163
+ string → deferred annotation → refuse (conservative)
164
+ """
165
+ if ann_node is None:
166
+ return None
167
+ t = ann_node.type
168
+ if t == "identifier":
169
+ name = _text(ann_node)
170
+ # Reject built-in type names that are not user classes.
171
+ if name in _PY_BUILTIN_TYPES:
172
+ return None
173
+ return name or None
174
+ if t == "type":
175
+ # Nested type wrapper — recurse into its first child.
176
+ for child in ann_node.children:
177
+ if child.type not in (":", "->"):
178
+ return _py_plain_type_from_annotation(child)
179
+ return None
180
+ # All other shapes (subscript, binary_operator, attribute, string, etc.) → refuse.
181
+ return None
182
+
183
+
184
+ # Python built-in type names that are NOT user classes. Seeing one of these as
185
+ # the annotation means we must NOT try to resolve .method() calls on it.
186
+ _PY_BUILTIN_TYPES: frozenset[str] = frozenset({
187
+ "int", "float", "str", "bytes", "bool", "None", "object",
188
+ "list", "dict", "tuple", "set", "frozenset",
189
+ "List", "Dict", "Tuple", "Set", "FrozenSet", "Optional", "Union",
190
+ "Any", "Type", "Callable", "Generator", "Iterator", "Iterable",
191
+ "Sequence", "Mapping", "MutableMapping", "MutableSequence",
192
+ "ClassVar", "Final", "Literal", "TypeVar",
193
+ })
194
+
195
+
196
+ def _py_constructor_class(value_node: Node) -> str | None:
197
+ """If value_node is a 'ClassName(...)' constructor call, return 'ClassName', else None.
198
+
199
+ A Python constructor call is a call node whose function child is a bare identifier.
200
+ Conservative: refuses dotted constructors (pkg.Foo()), subscripts (Foo[T]()), etc.
201
+ """
202
+ if value_node.type != "call":
203
+ return None
204
+ func = value_node.child_by_field_name("function")
205
+ if func is None:
206
+ return None
207
+ if func.type == "identifier":
208
+ name = _text(func)
209
+ if name and name not in _PY_BUILTIN_TYPES:
210
+ return name
211
+ return None
212
+
213
+
214
+ # ── Python: field pre-scan ────────────────────────────────────────────────────
215
+
216
+
217
+ def scan_class_fields_python(class_node: Node) -> dict[str, str]:
218
+ """Pre-scan a Python class_definition body for field-level type bindings.
219
+
220
+ WHY a pre-scan: methods may reference a field before its declaration in source
221
+ order. Pre-scanning the whole class body gives an order-independent type map.
222
+
223
+ Captures:
224
+ field: Type (annotated class variable, no value)
225
+ field: Type = ... (annotated class variable with value)
226
+ field = ClassName() (plain assignment with constructor; conservative)
227
+
228
+ Returns name → type dict for direct class body members only (not nested classes).
229
+ Never raises.
230
+ """
231
+ out: dict[str, str] = {}
232
+ try:
233
+ body = class_node.child_by_field_name("body")
234
+ if body is None:
235
+ return out
236
+ for stmt in body.children:
237
+ _py_scan_field_stmt(stmt, out)
238
+ except Exception as exc: # noqa: BLE001
239
+ logger.debug("scan_class_fields_python: failed: %r", exc)
240
+ return out
241
+
242
+
243
+ def _py_scan_field_stmt(stmt: Node, out: dict[str, str]) -> None:
244
+ """Record a single class body statement's field binding if it has a plain type.
245
+
246
+ Handles:
247
+ expression_statement → assignment with 'type' field (field: Type or field: Type = val)
248
+ expression_statement → assignment without 'type' field (field = ClassName())
249
+
250
+ NOTE: Python tree-sitter represents `x: Type` and `x: Type = val` as `assignment`
251
+ nodes (not `annotated_assignment`), with the annotation stored in the 'type' field.
252
+ The `annotated_assignment` node type exists in older grammars but in practice the
253
+ current Python grammar uses `assignment` with an optional `type` field.
254
+ We handle both for robustness.
255
+ """
256
+ try:
257
+ if stmt.type == "expression_statement" and stmt.children:
258
+ inner = stmt.children[0]
259
+ if inner.type in ("assignment", "annotated_assignment"):
260
+ # Check for type annotation field first (covers x: Type and x: Type = val)
261
+ ann = inner.child_by_field_name("type")
262
+ if ann is not None:
263
+ _py_record_annotated_assignment(inner, out)
264
+ else:
265
+ # Plain assignment — check if RHS is a constructor call.
266
+ _py_record_constructor_assignment(inner, out)
267
+ except Exception as exc: # noqa: BLE001
268
+ logger.debug("_py_scan_field_stmt: failed: %r", exc)
269
+
270
+
271
+ def _py_record_annotated_assignment(node: Node, out: dict[str, str]) -> None:
272
+ """Record binding from `name: Type` or `name: Type = value`.
273
+
274
+ Handles both `annotated_assignment` and `assignment` nodes with a `type` field.
275
+ tree-sitter Python uses `assignment` nodes (with optional `type` field) for:
276
+ field: Type → assignment{left=identifier, type=type}
277
+ field: Type = val → assignment{left=identifier, type=type, right=...}
278
+ """
279
+ try:
280
+ left = node.child_by_field_name("left")
281
+ ann = node.child_by_field_name("type")
282
+ if left is None or ann is None:
283
+ return
284
+ # Only bare identifier targets (no 'self.x: T' — that's an attribute)
285
+ if left.type != "identifier":
286
+ return
287
+ name = _text(left)
288
+ if not name:
289
+ return
290
+ type_name = _py_plain_type_from_annotation(ann)
291
+ if type_name:
292
+ out[name] = type_name
293
+ except Exception as exc: # noqa: BLE001
294
+ logger.debug("_py_record_annotated_assignment: failed: %r", exc)
295
+
296
+
297
+ def _py_record_constructor_assignment(node: Node, out: dict[str, str]) -> None:
298
+ """Record binding from `name = ClassName()`.
299
+
300
+ tree-sitter Python: assignment has left + right children.
301
+ """
302
+ try:
303
+ left = node.child_by_field_name("left")
304
+ right = node.child_by_field_name("right")
305
+ if left is None or right is None:
306
+ return
307
+ if left.type != "identifier":
308
+ return
309
+ name = _text(left)
310
+ if not name:
311
+ return
312
+ cls = _py_constructor_class(right)
313
+ if cls:
314
+ out[name] = cls
315
+ except Exception as exc: # noqa: BLE001
316
+ logger.debug("_py_record_constructor_assignment: failed: %r", exc)
317
+
318
+
319
+ # ── Python: per-function scope ────────────────────────────────────────────────
320
+
321
+
322
+ def record_py_param_types(func_node: Node, var_types: dict[str, str]) -> None:
323
+ """Bind each function parameter's name → declared type into var_types.
324
+
325
+ Handles:
326
+ def f(self, x: Foo, y: Bar) → binds x→Foo, y→Bar
327
+ def f(cls, x: Foo) → binds x→Foo (cls itself is handled by _PY_SELF_NAMES)
328
+
329
+ Conservative: only plain type annotations bind. Optional / generic / union → skip.
330
+ Never raises.
331
+ """
332
+ try:
333
+ params_node = func_node.child_by_field_name("parameters")
334
+ if params_node is None:
335
+ return
336
+ for param in params_node.children:
337
+ _py_record_single_param(param, var_types)
338
+ except Exception as exc: # noqa: BLE001
339
+ logger.debug("record_py_param_types: failed: %r", exc)
340
+
341
+
342
+ def _py_record_single_param(param: Node, var_types: dict[str, str]) -> None:
343
+ """Record name → type for a single parameter node.
344
+
345
+ tree-sitter Python parameter shapes:
346
+ identifier (positional, no annotation)
347
+ typed_parameter (positional with annotation: x: Foo)
348
+ default_parameter (positional with default: x=val — no annotation)
349
+ typed_default_parameter (positional with annotation+default: x: Foo = val)
350
+ list_splat_pattern / dict_splat_pattern (*args/**kwargs — skip)
351
+ """
352
+ try:
353
+ t = param.type
354
+ if t == "typed_parameter":
355
+ # children: identifier, ':', type
356
+ names = [c for c in param.children if c.type == "identifier"]
357
+ ann = param.child_by_field_name("type")
358
+ if names and ann:
359
+ pname = _text(names[0])
360
+ if pname and pname not in _PY_SELF_NAMES:
361
+ type_name = _py_plain_type_from_annotation(ann)
362
+ if type_name:
363
+ var_types[pname] = type_name
364
+ elif t == "typed_default_parameter":
365
+ name_node = param.child_by_field_name("name")
366
+ ann = param.child_by_field_name("type")
367
+ if name_node and ann:
368
+ pname = _text(name_node)
369
+ if pname and pname not in _PY_SELF_NAMES:
370
+ type_name = _py_plain_type_from_annotation(ann)
371
+ if type_name:
372
+ var_types[pname] = type_name
373
+ except Exception as exc: # noqa: BLE001
374
+ logger.debug("_py_record_single_param: failed: %r", exc)
375
+
376
+
377
+ def record_py_local_types(stmt_node: Node, var_types: dict[str, str]) -> None:
378
+ """Record type bindings from a local statement (annotated assignment or constructor).
379
+
380
+ Handles two patterns inside function bodies:
381
+ x: Foo = ... (assignment with type field — Python grammar for annotated vars)
382
+ x = Foo() (assignment whose RHS is a constructor call)
383
+
384
+ NOTE: Python tree-sitter uses `assignment` nodes with a `type` field for `x: Foo`
385
+ patterns (not the `annotated_assignment` node type). We check for `type` field first.
386
+
387
+ Called incrementally during the function-body walk so later code in the same
388
+ function can resolve vars defined earlier. Never raises.
389
+ """
390
+ try:
391
+ if stmt_node.type == "expression_statement" and stmt_node.children:
392
+ inner = stmt_node.children[0]
393
+ if inner.type in ("assignment", "annotated_assignment"):
394
+ ann = inner.child_by_field_name("type")
395
+ if ann is not None:
396
+ _py_record_annotated_assignment(inner, var_types)
397
+ else:
398
+ _py_record_constructor_assignment(inner, var_types)
399
+ except Exception as exc: # noqa: BLE001
400
+ logger.debug("record_py_local_types: failed: %r", exc)
401
+
402
+
403
+ # ── TypeScript/JS: plain-type extraction ──────────────────────────────────────
404
+
405
+
406
+ def _ts_plain_type_from_annotation(type_node: Node) -> str | None:
407
+ """Extract a plain user type name from a TypeScript type annotation.
408
+
409
+ Conservative: only accepts a bare type_identifier. Refuses:
410
+ - Union types (string | null, Foo | Bar) → union_type node
411
+ - Generic types (Array<T>, Map<K,V>) → generic_type node
412
+ - Array types (T[]) → array_type node (or predefined_type)
413
+ - Intersection, tuple, function types → other nodes
414
+ - Predefined/primitive types → predefined_type (string, number, etc.)
415
+
416
+ WHY refuse union types: `foo: Client | null` means `foo` may be null; binding
417
+ Client and emitting `Client.method()` would be incorrect half the time at runtime.
418
+ WHY refuse generics: `Array<Client>` or `Map<K,V>` are container types — calling
419
+ `.method()` on them resolves on the container, not the element type.
420
+ Both refusals are required by the conservatism contract: prefer no edge over a
421
+ wrong edge.
422
+
423
+ The node passed here is a type_annotation node or a direct type child.
424
+ """
425
+ if type_node is None:
426
+ return None
427
+ t = type_node.type
428
+
429
+ # type_annotation wraps the actual type — skip the ':' and recurse.
430
+ if t == "type_annotation":
431
+ for child in type_node.children:
432
+ if child.type not in (":", "=>"):
433
+ result = _ts_plain_type_from_annotation(child)
434
+ if result:
435
+ return result
436
+ return None
437
+
438
+ # A bare type identifier (e.g. `Client`, `Parser`, `Engine`)
439
+ if t == "type_identifier":
440
+ name = _text(type_node)
441
+ if name and name not in _TS_BUILTIN_TYPES:
442
+ return name
443
+ return None
444
+
445
+ # All other node types (union_type, generic_type, array_type, predefined_type,
446
+ # intersection_type, tuple_type, function_type, undefined_type, literal_type) → refuse.
447
+ return None
448
+
449
+
450
+ # TypeScript built-in / primitive type names that are not user classes.
451
+ _TS_BUILTIN_TYPES: frozenset[str] = frozenset({
452
+ "string", "number", "boolean", "void", "any", "unknown", "never", "object",
453
+ "symbol", "bigint", "null", "undefined",
454
+ "Array", "Map", "Set", "WeakMap", "WeakSet", "WeakRef",
455
+ "Promise", "Function", "Object", "Error",
456
+ "String", "Number", "Boolean", "Symbol", "BigInt",
457
+ "Readonly", "Record", "Partial", "Required", "Pick", "Omit",
458
+ "Exclude", "Extract", "NonNullable", "ReturnType", "InstanceType",
459
+ "Parameters", "ConstructorParameters",
460
+ "ReadonlyArray", "ReadonlyMap", "ReadonlySet",
461
+ })
462
+
463
+
464
+ def _ts_constructor_class(value_node: Node) -> str | None:
465
+ """If value_node is `new ClassName(...)`, return 'ClassName', else None.
466
+
467
+ TypeScript new_expression: `new Foo(...)` has a 'constructor' field that is a
468
+ type_identifier (plain class name) or a member_expression (namespace.Foo — refuse).
469
+ Conservative: only accepts plain type_identifier.
470
+ """
471
+ if value_node.type != "new_expression":
472
+ return None
473
+ constructor = value_node.child_by_field_name("constructor")
474
+ if constructor is None:
475
+ return None
476
+ if constructor.type == "identifier":
477
+ name = _text(constructor)
478
+ if name and name not in _TS_BUILTIN_TYPES:
479
+ return name
480
+ return None
481
+
482
+
483
+ # ── TypeScript: class field pre-scan ─────────────────────────────────────────
484
+
485
+
486
+ def scan_class_fields_typescript(class_node: Node) -> dict[str, str]:
487
+ """Pre-scan a TS class_declaration body for field-level type bindings.
488
+
489
+ WHY pre-scan: same reason as Python — methods may reference a field declared
490
+ below them. Pre-scanning the class body gives an order-independent field map.
491
+
492
+ Captures:
493
+ field: Type; (public_field_definition or field_definition)
494
+ field: Type = new Foo(); (field with initializer — take annotation type)
495
+
496
+ Returns name → type dict for direct class members only (not nested classes).
497
+ Never raises.
498
+ """
499
+ out: dict[str, str] = {}
500
+ try:
501
+ body = class_node.child_by_field_name("body")
502
+ if body is None:
503
+ return out
504
+ for child in body.children:
505
+ _ts_scan_field_member(child, out)
506
+ except Exception as exc: # noqa: BLE001
507
+ logger.debug("scan_class_fields_typescript: failed: %r", exc)
508
+ return out
509
+
510
+
511
+ def _ts_scan_field_member(member: Node, out: dict[str, str]) -> None:
512
+ """Record a single class body member if it is a field with a plain type annotation.
513
+
514
+ TypeScript grammar field shapes:
515
+ public_field_definition → name field (property_identifier) + type (type_annotation)
516
+ field_definition → same structure (used in some grammars)
517
+ """
518
+ try:
519
+ if member.type not in ("public_field_definition", "field_definition"):
520
+ return
521
+ name_node = member.child_by_field_name("name")
522
+ type_node = member.child_by_field_name("type")
523
+ if name_node is None or type_node is None:
524
+ return
525
+ field_name = _text(name_node)
526
+ if not field_name:
527
+ return
528
+ type_name = _ts_plain_type_from_annotation(type_node)
529
+ if type_name:
530
+ out[field_name] = type_name
531
+ except Exception as exc: # noqa: BLE001
532
+ logger.debug("_ts_scan_field_member: failed: %r", exc)
533
+
534
+
535
+ # ── TypeScript: per-function scope ────────────────────────────────────────────
536
+
537
+
538
+ def record_ts_param_types(func_node: Node, var_types: dict[str, str]) -> None:
539
+ """Bind each TS/JS function parameter's name → declared type into var_types.
540
+
541
+ Handles:
542
+ required_parameter (x: Foo)
543
+ optional_parameter (x?: Foo)
544
+
545
+ Conservative: only plain type_identifier annotations bind. Never raises.
546
+ """
547
+ try:
548
+ params_node = func_node.child_by_field_name("parameters")
549
+ if params_node is None:
550
+ return
551
+ for param in params_node.children:
552
+ _ts_record_single_param(param, var_types)
553
+ except Exception as exc: # noqa: BLE001
554
+ logger.debug("record_ts_param_types: failed: %r", exc)
555
+
556
+
557
+ def _ts_record_single_param(param: Node, var_types: dict[str, str]) -> None:
558
+ """Record name → type for a single TS/JS parameter node."""
559
+ try:
560
+ if param.type not in ("required_parameter", "optional_parameter"):
561
+ return
562
+ name_node = param.child_by_field_name("pattern")
563
+ type_node = param.child_by_field_name("type")
564
+ if name_node is None or type_node is None:
565
+ return
566
+ if name_node.type not in ("identifier", "shorthand_property_identifier_pattern"):
567
+ return
568
+ pname = _text(name_node)
569
+ if not pname or pname in _TS_SELF_NAMES:
570
+ return
571
+ type_name = _ts_plain_type_from_annotation(type_node)
572
+ if type_name:
573
+ var_types[pname] = type_name
574
+ except Exception as exc: # noqa: BLE001
575
+ logger.debug("_ts_record_single_param: failed: %r", exc)
576
+
577
+
578
+ def record_ts_local_types(stmt_node: Node, var_types: dict[str, str]) -> None:
579
+ """Record type bindings from a local TS/JS statement.
580
+
581
+ Handles:
582
+ const x: Foo = ... (lexical_declaration with type_annotation)
583
+ const x = new Foo() (lexical_declaration with new_expression initializer)
584
+ let x: Foo = ... (same — let)
585
+
586
+ Called incrementally during the function-body walk. Never raises.
587
+ """
588
+ try:
589
+ if stmt_node.type not in ("lexical_declaration", "variable_declaration"):
590
+ return
591
+ for child in stmt_node.children:
592
+ if child.type == "variable_declarator":
593
+ _ts_record_single_declarator(child, var_types)
594
+ except Exception as exc: # noqa: BLE001
595
+ logger.debug("record_ts_local_types: failed: %r", exc)
596
+
597
+
598
+ def _ts_record_single_declarator(decl: Node, var_types: dict[str, str]) -> None:
599
+ """Record one variable_declarator's name → type binding.
600
+
601
+ Two forms:
602
+ const x: Type = ... → 'type' field is type_annotation → use annotation
603
+ const x = new Foo() → 'value' field is new_expression → use constructor name
604
+ Annotation wins over constructor when both present.
605
+ """
606
+ try:
607
+ name_node = decl.child_by_field_name("name")
608
+ if name_node is None or name_node.type not in ("identifier",):
609
+ return
610
+ var_name = _text(name_node)
611
+ if not var_name:
612
+ return
613
+
614
+ # Prefer explicit type annotation.
615
+ type_node = decl.child_by_field_name("type")
616
+ if type_node is not None:
617
+ type_name = _ts_plain_type_from_annotation(type_node)
618
+ if type_name:
619
+ var_types[var_name] = type_name
620
+ return
621
+
622
+ # Fall back to constructor-call inference.
623
+ value_node = decl.child_by_field_name("value")
624
+ if value_node is not None:
625
+ cls = _ts_constructor_class(value_node)
626
+ if cls:
627
+ var_types[var_name] = cls
628
+ except Exception as exc: # noqa: BLE001
629
+ logger.debug("_ts_record_single_declarator: failed: %r", exc)
630
+
631
+
632
+ # ── Slice #77: Composition (holds) collectors ────────────────────────────────
633
+ #
634
+ # These functions return deduped (held_type_name, line) pairs for a class node.
635
+ # They REUSE the existing scan_class_fields_* helpers (Layer 1 field pre-scan) for
636
+ # the stored-field half, and add a constructor/init-parameter pass using the SAME
637
+ # plain-type extractors (_py_plain_type_from_annotation, _ts_plain_type_from_annotation).
638
+ #
639
+ # CONSERVATISM CONTRACT (same as receiver-type inference):
640
+ # NEVER emit a wrong type. Only plain user-type identifiers are accepted.
641
+ # Optionals, containers, generics, primitives, and dotted types are all refused.
642
+ # The existing scan_class_fields_* helpers already apply this filter for fields.
643
+ # Constructor/init params add the same filter on top.
644
+ #
645
+ # WHY no is_builtin() post-filter here (unlike graph_scope_infer_ext*.py):
646
+ # This file is a LEAF module — it may only import from graph_common and stdlib.
647
+ # Importing seam.analysis.builtins would break the layering constraint. The
648
+ # _PY_BUILTIN_TYPES and _TS_BUILTIN_TYPES frozensets (already used by the
649
+ # plain-type helpers) serve the same authoritative-filter role in a leaf-safe way.
650
+ #
651
+ # WHY constructor/__init__ params count as composition but other method params do not:
652
+ # A constructor/init parameter that is stored as a field represents an OWNED
653
+ # dependency — the object holds a reference for its lifetime. Parameters of
654
+ # other methods are transient (passed per-call) and express USE, not composition.
655
+ # Emitting holds edges for non-init params would conflate dependency injection with
656
+ # ordinary function arguments, producing false composition claims.
657
+ #
658
+ # DEDUPE: a type name that appears as BOTH a field and a ctor/init param is emitted
659
+ # only ONCE. Dedupe is per (source, target) — the set returned here is deduplicated.
660
+ #
661
+ # NEVER RAISES: all public functions have a backstop try/except and return [] on error.
662
+
663
+
664
+ def collect_composition_types_python(class_node: Node) -> list[tuple[str, int]]:
665
+ """Collect (held_type_name, line) pairs from a Python class_definition node.
666
+
667
+ Two passes:
668
+ 1. Field types: reuses scan_class_fields_python (annotated class attrs).
669
+ 2. __init__ parameter types: scans parameters of __init__ with plain-type annotations.
670
+
671
+ Deduped: if the same type appears in both field and __init__ param, only one entry
672
+ is returned (first source wins — typically the field declaration wins since the class
673
+ body is scanned first, but dedup uses a set so order doesn't matter for correctness).
674
+
675
+ Returns [] on any error. Never raises.
676
+ """
677
+ try:
678
+ seen: set[str] = set()
679
+ result: list[tuple[str, int]] = []
680
+
681
+ # Pass 1: class-level field annotations.
682
+ # scan_class_fields_python returns name→type, but we need the LINE too.
683
+ # Walk the body directly to capture line numbers alongside types.
684
+ body = class_node.child_by_field_name("body")
685
+ if body is not None:
686
+ for stmt in body.children:
687
+ try:
688
+ if stmt.type == "expression_statement" and stmt.children:
689
+ inner = stmt.children[0]
690
+ if inner.type in ("assignment", "annotated_assignment"):
691
+ ann = inner.child_by_field_name("type")
692
+ left = inner.child_by_field_name("left")
693
+ if ann is not None and left is not None and left.type == "identifier":
694
+ type_name = _py_plain_type_from_annotation(ann)
695
+ if type_name and type_name not in seen:
696
+ seen.add(type_name)
697
+ result.append((type_name, stmt.start_point[0] + 1))
698
+ except Exception: # noqa: BLE001
699
+ pass
700
+
701
+ # Pass 2: __init__ parameter types.
702
+ if body is not None:
703
+ for stmt in body.children:
704
+ try:
705
+ fn = _py_get_init_function(stmt)
706
+ if fn is None:
707
+ continue
708
+ params = fn.child_by_field_name("parameters")
709
+ if params is None:
710
+ continue
711
+ for param in params.children:
712
+ if param.type not in ("typed_parameter", "typed_default_parameter"):
713
+ continue
714
+ ann = param.child_by_field_name("type")
715
+ if ann is None:
716
+ continue
717
+ type_name = _py_plain_type_from_annotation(ann)
718
+ if type_name and type_name not in seen:
719
+ seen.add(type_name)
720
+ result.append((type_name, param.start_point[0] + 1))
721
+ except Exception: # noqa: BLE001
722
+ pass
723
+
724
+ return result
725
+ except Exception as exc: # noqa: BLE001
726
+ logger.debug("collect_composition_types_python: failed: %r", exc)
727
+ return []
728
+
729
+
730
+ def _py_get_init_function(stmt: Node) -> Node | None:
731
+ """Return the function_definition node if stmt is a Python __init__ method.
732
+
733
+ Handles plain `def __init__(...)` and `@decorator def __init__(...)`.
734
+ Returns None for all other nodes.
735
+ """
736
+ if stmt.type == "function_definition":
737
+ name = _text(stmt.child_by_field_name("name") or stmt)
738
+ if name == "__init__":
739
+ return stmt
740
+ elif stmt.type == "decorated_definition":
741
+ inner = stmt.child_by_field_name("definition")
742
+ if inner is not None and inner.type == "function_definition":
743
+ name = _text(inner.child_by_field_name("name") or inner)
744
+ if name == "__init__":
745
+ return inner
746
+ return None
747
+
748
+
749
+ def collect_composition_types_typescript(class_node: Node) -> list[tuple[str, int]]:
750
+ """Collect (held_type_name, line) pairs from a TypeScript class_declaration node.
751
+
752
+ Two passes:
753
+ 1. Field types: scan public_field_definition/field_definition nodes with a plain
754
+ type_annotation — reuses _ts_plain_type_from_annotation (same as scan_class_fields_ts).
755
+ 2. Constructor parameter types: scan required_parameter and optional_parameter nodes
756
+ in the constructor method, including parameter-property forms
757
+ (e.g. constructor(private svc: Service)).
758
+
759
+ Deduped: same type from both field and ctor param → one entry.
760
+ Returns [] on any error. Never raises.
761
+ """
762
+ try:
763
+ seen: set[str] = set()
764
+ result: list[tuple[str, int]] = []
765
+
766
+ body = class_node.child_by_field_name("body")
767
+ if body is None:
768
+ return result
769
+
770
+ # Pass 1: field declarations.
771
+ for child in body.children:
772
+ try:
773
+ if child.type not in ("public_field_definition", "field_definition"):
774
+ continue
775
+ type_node = child.child_by_field_name("type")
776
+ if type_node is None:
777
+ continue
778
+ type_name = _ts_plain_type_from_annotation(type_node)
779
+ if type_name and type_name not in seen:
780
+ seen.add(type_name)
781
+ result.append((type_name, child.start_point[0] + 1))
782
+ except Exception: # noqa: BLE001
783
+ pass
784
+
785
+ # Pass 2: constructor parameters (including parameter-property DI).
786
+ for child in body.children:
787
+ try:
788
+ if child.type != "method_definition":
789
+ continue
790
+ name_node = child.child_by_field_name("name")
791
+ if name_node is None or _text(name_node) != "constructor":
792
+ continue
793
+ # Found the constructor — scan its parameters.
794
+ params = child.child_by_field_name("parameters")
795
+ if params is None:
796
+ continue
797
+ for param in params.children:
798
+ type_name = _ts_param_plain_type(param)
799
+ if type_name and type_name not in seen:
800
+ seen.add(type_name)
801
+ result.append((type_name, param.start_point[0] + 1))
802
+ except Exception: # noqa: BLE001
803
+ pass
804
+
805
+ return result
806
+ except Exception as exc: # noqa: BLE001
807
+ logger.debug("collect_composition_types_typescript: failed: %r", exc)
808
+ return []
809
+
810
+
811
+ def _ts_param_plain_type(param: Node) -> str | None:
812
+ """Extract a plain user type from a TS constructor parameter node.
813
+
814
+ Handles:
815
+ required_parameter(x: Type) — normal param
816
+ optional_parameter(x?: Type) — optional param name (still has a type)
817
+ required_parameter with access_modifier — parameter property (private/public/protected)
818
+ e.g. constructor(private svc: Service)
819
+ public_field_definition used as param — some grammars model param-props as fields
820
+
821
+ Conservative: only plain type_identifier annotations bind. Never raises.
822
+ """
823
+ try:
824
+ # Standard required/optional parameter.
825
+ if param.type in ("required_parameter", "optional_parameter"):
826
+ type_node = param.child_by_field_name("type")
827
+ if type_node is not None:
828
+ return _ts_plain_type_from_annotation(type_node)
829
+
830
+ # Parameter property: tree-sitter models `constructor(private svc: Service)` as
831
+ # a required_parameter whose FIRST child is an accessibility_modifier ("private",
832
+ # "public", "protected"), followed by the identifier and type_annotation.
833
+ # Some grammar versions emit `public_field_definition` inside the parameter list.
834
+ if param.type == "public_field_definition":
835
+ type_node = param.child_by_field_name("type")
836
+ if type_node is not None:
837
+ return _ts_plain_type_from_annotation(type_node)
838
+
839
+ return None
840
+ except Exception: # noqa: BLE001
841
+ return None
842
+
843
+
844
+ def collect_param_types_python(func_node: Node) -> list[tuple[str, int]]:
845
+ """Collect (param_type_name, line) pairs from a Python function_definition node.
846
+
847
+ Walks the function's `parameters` and applies _py_plain_type_from_annotation — the
848
+ SAME plain-type extraction used for __init__ holds param collection — so a param
849
+ annotated with a plain user type (x: Service) is captured while Optional[T]/list[T]/
850
+ dict[K,V]/generics/builtins are refused (builtins via _PY_BUILTIN_TYPES inside the
851
+ helper). Deduped within the signature.
852
+
853
+ Returns [] on any error. Never raises.
854
+ """
855
+ try:
856
+ seen: set[str] = set()
857
+ result: list[tuple[str, int]] = []
858
+ params = func_node.child_by_field_name("parameters")
859
+ if params is None:
860
+ return result
861
+ for param in params.children:
862
+ if param.type not in ("typed_parameter", "typed_default_parameter"):
863
+ continue
864
+ ann = param.child_by_field_name("type")
865
+ if ann is None:
866
+ continue
867
+ type_name = _py_plain_type_from_annotation(ann)
868
+ if type_name and type_name not in seen:
869
+ seen.add(type_name)
870
+ result.append((type_name, param.start_point[0] + 1))
871
+ return result
872
+ except Exception as exc: # noqa: BLE001
873
+ logger.debug("collect_param_types_python: failed: %r", exc)
874
+ return []
875
+
876
+
877
+ def collect_param_types_typescript(func_node: Node) -> list[tuple[str, int]]:
878
+ """Collect (param_type_name, line) pairs from a TS/JS function-like node.
879
+
880
+ Works for function_declaration, method_definition, arrow_function, and
881
+ function_expression — all carry a `parameters` (formal_parameters) field. Applies
882
+ _ts_param_plain_type per parameter (same helper as constructor holds), so plain
883
+ user-typed params bind while optionals/generics/builtins are refused. Plain JS has
884
+ no type annotations → returns [] naturally. Deduped within the signature.
885
+
886
+ Returns [] on any error. Never raises.
887
+ """
888
+ try:
889
+ seen: set[str] = set()
890
+ result: list[tuple[str, int]] = []
891
+ params = func_node.child_by_field_name("parameters")
892
+ if params is None:
893
+ return result
894
+ for param in params.children:
895
+ type_name = _ts_param_plain_type(param)
896
+ if type_name and type_name not in seen:
897
+ seen.add(type_name)
898
+ result.append((type_name, param.start_point[0] + 1))
899
+ return result
900
+ except Exception as exc: # noqa: BLE001
901
+ logger.debug("collect_param_types_typescript: failed: %r", exc)
902
+ return []