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,615 @@
1
+ """C++ symbol, edge, and comment extraction from tree-sitter ASTs.
2
+
3
+ LAYER: imports from graph_common (leaf), graph_scope_infer_ext[2] (leaf), graph_c (leaf) — never from graph.py.
4
+
5
+ LAYERING:
6
+ graph_common (leaf — no seam deps)
7
+ graph_c (leaf — C extractors + shared C/C++ helpers)
8
+
9
+ graph_cpp (this file)
10
+
11
+ graph_c_cpp (thin re-exporter; graph.py imports from there)
12
+
13
+ WHY split from graph_c_cpp.py: graph_c_cpp.py exceeded 1000 lines after Tier B additions.
14
+ C and C++ are now each large enough to stand alone. graph_cpp.py imports shared helpers
15
+ (_c_doc_comment, _handle_c_include) from graph_c to avoid duplication.
16
+
17
+ GRAMMAR FACTS (verified by dumping AST from real fixtures):
18
+ namespace_definition: traversed, NOT emitted as a symbol.
19
+ class_specifier / struct_specifier: field 'name' → kind='class'.
20
+ field_declaration_list body may contain:
21
+ access_specifier nodes (public/private/protected), field_declaration,
22
+ function_definition (in-class method).
23
+ In-class function_definition: declarator field is function_declarator whose
24
+ 'declarator' field is a field_identifier (method name, NOT identifier).
25
+ Out-of-line method: function_declarator's 'declarator' field is a
26
+ qualified_identifier with 'scope' (class name) and 'name' (method name).
27
+ enum_specifier: field 'name' → kind='type'.
28
+ template_declaration: the inner class_specifier or function_definition is
29
+ extracted; the template itself is NOT emitted as a symbol.
30
+ call_expression: field 'function' = identifier → bare call target.
31
+ comment: same as C — covers // and /* */ comments.
32
+ """
33
+
34
+ import logging
35
+ from pathlib import Path
36
+
37
+ from tree_sitter import Node
38
+
39
+ import seam.config as config
40
+ from seam.indexer.field_access_ext import (
41
+ collect_field_symbols_cpp,
42
+ extract_field_accesses_cpp,
43
+ )
44
+ from seam.indexer.graph_c import (
45
+ _c_doc_comment,
46
+ _extract_comments_c,
47
+ _handle_c_include,
48
+ )
49
+ from seam.indexer.graph_common import (
50
+ Comment,
51
+ Edge,
52
+ Symbol,
53
+ _find_enclosing_function,
54
+ _make_symbol,
55
+ _text,
56
+ )
57
+ from seam.indexer.graph_scope_infer_ext import param_types_via_recorder, resolve_receiver_type_ext
58
+ from seam.indexer.graph_scope_infer_ext2 import (
59
+ _CPP_SELF_NAMES,
60
+ collect_composition_types_cpp,
61
+ record_cpp_local_types,
62
+ record_cpp_param_types,
63
+ scan_class_fields_cpp,
64
+ )
65
+ from seam.indexer.signatures import extract_node_fields
66
+
67
+ logger = logging.getLogger(__name__)
68
+
69
+
70
+ # ── C++ helpers ────────────────────────────────────────────────────────────────
71
+
72
+
73
+ def _cpp_function_name(func_def: Node) -> str | None:
74
+ """Extract the function name from a C++ function_definition node.
75
+
76
+ Handles three patterns observed in the real grammar:
77
+ 1. Free function: declarator → function_declarator → declarator=identifier
78
+ 2. In-class method: declarator → function_declarator → declarator=field_identifier
79
+ 3. Out-of-line method: declarator → function_declarator → declarator=qualified_identifier
80
+
81
+ Returns a plain name (for free functions and in-class methods) or a
82
+ 'Class.method' qualified name (for out-of-line Class::method definitions).
83
+ """
84
+ try:
85
+ declarator = func_def.child_by_field_name("declarator")
86
+ if declarator is None:
87
+ return None
88
+ if declarator.type != "function_declarator":
89
+ return None
90
+ inner = declarator.child_by_field_name("declarator")
91
+ if inner is None:
92
+ return None
93
+ if inner.type == "identifier":
94
+ return _text(inner)
95
+ if inner.type == "field_identifier":
96
+ return _text(inner)
97
+ if inner.type == "qualified_identifier":
98
+ scope = inner.child_by_field_name("scope")
99
+ name_node = inner.child_by_field_name("name")
100
+ if scope is not None and name_node is not None:
101
+ return f"{_text(scope)}.{_text(name_node)}"
102
+ except Exception: # noqa: BLE001
103
+ pass
104
+ return None
105
+
106
+
107
+ def _cpp_enclosing_class_name(node: Node) -> str | None:
108
+ """Walk up the parent chain to find the nearest enclosing C++ class/struct name."""
109
+ try:
110
+ current = node.parent
111
+ while current is not None:
112
+ if current.type in ("class_specifier", "struct_specifier"):
113
+ name_node = current.child_by_field_name("name")
114
+ if name_node is not None:
115
+ return _text(name_node)
116
+ current = current.parent
117
+ except Exception: # noqa: BLE001
118
+ pass
119
+ return None
120
+
121
+
122
+ def _dedup_cpp_symbols(symbols: list[Symbol]) -> list[Symbol]:
123
+ """De-duplicate C++ symbols by (name, kind), keeping one per (name, kind) pair.
124
+
125
+ WHY: a class method declared in-class AND defined out-of-line both produce a
126
+ symbol with the same qualified name and kind. The de-duplication rule: prefer
127
+ the entry with the larger line span (out-of-line definitions typically have a full body).
128
+ """
129
+ seen: dict[tuple[str, str], Symbol] = {}
130
+ for sym in symbols:
131
+ key = (sym["name"], sym["kind"])
132
+ if key not in seen:
133
+ seen[key] = sym
134
+ else:
135
+ existing = seen[key]
136
+ existing_span = existing["end_line"] - existing["start_line"]
137
+ current_span = sym["end_line"] - sym["start_line"]
138
+ if current_span > existing_span:
139
+ seen[key] = sym
140
+ result: list[Symbol] = []
141
+ added: set[tuple[str, str]] = set()
142
+ for sym in symbols:
143
+ key = (sym["name"], sym["kind"])
144
+ if seen[key] is sym and key not in added:
145
+ result.append(sym)
146
+ added.add(key)
147
+ return result
148
+
149
+
150
+ # ── C++ extraction ─────────────────────────────────────────────────────────────
151
+
152
+
153
+ def _extract_symbols_cpp(root: Node, filepath: Path) -> list[Symbol]:
154
+ """Walk a C++ AST and extract class, struct, union, enum, and function symbols.
155
+
156
+ Kind mapping (per Phase 9 spec):
157
+ namespace_definition → traversed, NOT emitted
158
+ class_specifier / struct_specifier → class
159
+ union_specifier → class
160
+ enum_specifier → type
161
+ function_definition (free) → function
162
+ function_definition (in-class) → method (Class.method)
163
+ function_definition (out-of-line) → method (Class.method via qualified_identifier)
164
+ template_declaration → inner class/function extracted
165
+
166
+ De-duplication is applied at the end: in-class + out-of-line definitions of the
167
+ same method both produce the same (name, kind) — only one is retained.
168
+ """
169
+ try:
170
+ symbols: list[Symbol] = []
171
+ file_str = str(filepath)
172
+
173
+ def _walk(node: Node, class_name: str | None = None) -> None:
174
+ if node.type == "namespace_definition":
175
+ body = node.child_by_field_name("body")
176
+ if body is not None:
177
+ for child in body.children:
178
+ _walk(child, class_name)
179
+
180
+ elif node.type in ("class_specifier", "struct_specifier"):
181
+ _handle_cpp_class(node, file_str, symbols, _walk)
182
+
183
+ elif node.type == "union_specifier":
184
+ _handle_cpp_union(node, file_str, symbols, _walk)
185
+
186
+ elif node.type == "enum_specifier":
187
+ _handle_cpp_enum(node, file_str, symbols)
188
+
189
+ elif node.type == "function_definition":
190
+ _handle_cpp_function(node, file_str, symbols, class_name)
191
+
192
+ elif node.type == "template_declaration":
193
+ for child in node.children:
194
+ if child.type in (
195
+ "class_specifier",
196
+ "struct_specifier",
197
+ "function_definition",
198
+ ):
199
+ _walk(child, class_name)
200
+
201
+ else:
202
+ for child in node.children:
203
+ _walk(child, class_name)
204
+
205
+ for child in root.children:
206
+ _walk(child, class_name=None)
207
+
208
+ return _dedup_cpp_symbols(symbols)
209
+ except Exception: # noqa: BLE001
210
+ logger.debug(
211
+ "_extract_symbols_cpp: unhandled exception for file=%s",
212
+ filepath,
213
+ exc_info=True,
214
+ )
215
+ return []
216
+
217
+
218
+ def _handle_cpp_class(
219
+ node: Node,
220
+ file_str: str,
221
+ symbols: list[Symbol],
222
+ walk_fn: object,
223
+ ) -> None:
224
+ """Emit a C++ class/struct symbol and recurse into its body for methods."""
225
+ try:
226
+ name_node = node.child_by_field_name("name")
227
+ if name_node is None:
228
+ return
229
+ cls_name = _text(name_node)
230
+ if not cls_name:
231
+ return
232
+ doc = _c_doc_comment(node)
233
+ fields = extract_node_fields(
234
+ node,
235
+ "cpp",
236
+ qualified_name=cls_name,
237
+ max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
238
+ )
239
+ symbols.append(
240
+ _make_symbol(
241
+ cls_name,
242
+ "class",
243
+ file_str,
244
+ node,
245
+ doc,
246
+ signature=fields["signature"],
247
+ decorators=fields["decorators"],
248
+ is_exported=fields["is_exported"],
249
+ visibility=fields["visibility"],
250
+ qualified_name=cls_name,
251
+ )
252
+ )
253
+ # A3 Slice 4: emit field symbols for C++ class/struct fields.
254
+ if config.SEAM_FIELD_ACCESS_EDGES == "on":
255
+ for qual_name, field_line in collect_field_symbols_cpp(node, cls_name):
256
+ symbols.append(Symbol(
257
+ name=qual_name,
258
+ kind="field",
259
+ file=file_str,
260
+ start_line=field_line,
261
+ end_line=field_line,
262
+ docstring=None,
263
+ signature=None,
264
+ decorators=[],
265
+ is_exported=None,
266
+ visibility=None,
267
+ qualified_name=qual_name,
268
+ ))
269
+ body = node.child_by_field_name("body")
270
+ if body is not None:
271
+ for child in body.children:
272
+ if child.type == "function_definition":
273
+ _handle_cpp_function(child, file_str, symbols, cls_name)
274
+ except Exception: # noqa: BLE001
275
+ logger.debug("_handle_cpp_class: failed for node at row %d", node.start_point[0])
276
+
277
+
278
+ def _handle_cpp_union(
279
+ node: Node,
280
+ file_str: str,
281
+ symbols: list[Symbol],
282
+ walk_fn: object,
283
+ ) -> None:
284
+ """Emit a C++ union symbol from a union_specifier node."""
285
+ try:
286
+ name_node = node.child_by_field_name("name")
287
+ if name_node is None:
288
+ return
289
+ name = _text(name_node)
290
+ if not name:
291
+ return
292
+ doc = _c_doc_comment(node)
293
+ fields = extract_node_fields(
294
+ node,
295
+ "cpp",
296
+ qualified_name=name,
297
+ max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
298
+ )
299
+ symbols.append(
300
+ _make_symbol(
301
+ name,
302
+ "class",
303
+ file_str,
304
+ node,
305
+ doc,
306
+ signature=fields["signature"],
307
+ decorators=fields["decorators"],
308
+ is_exported=fields["is_exported"],
309
+ visibility=fields["visibility"],
310
+ qualified_name=name,
311
+ )
312
+ )
313
+ except Exception: # noqa: BLE001
314
+ logger.debug("_handle_cpp_union: failed for node at row %d", node.start_point[0])
315
+
316
+
317
+ def _handle_cpp_enum(node: Node, file_str: str, symbols: list[Symbol]) -> None:
318
+ """Emit a C++ enum symbol from an enum_specifier node."""
319
+ try:
320
+ name_node = node.child_by_field_name("name")
321
+ if name_node is None:
322
+ return
323
+ name = _text(name_node)
324
+ if not name:
325
+ return
326
+ doc = _c_doc_comment(node)
327
+ fields = extract_node_fields(
328
+ node,
329
+ "cpp",
330
+ qualified_name=name,
331
+ max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
332
+ )
333
+ symbols.append(
334
+ _make_symbol(
335
+ name,
336
+ "type",
337
+ file_str,
338
+ node,
339
+ doc,
340
+ signature=fields["signature"],
341
+ decorators=fields["decorators"],
342
+ is_exported=fields["is_exported"],
343
+ visibility=fields["visibility"],
344
+ qualified_name=name,
345
+ )
346
+ )
347
+ except Exception: # noqa: BLE001
348
+ logger.debug("_handle_cpp_enum: failed for node at row %d", node.start_point[0])
349
+
350
+
351
+ def _handle_cpp_function(
352
+ node: Node,
353
+ file_str: str,
354
+ symbols: list[Symbol],
355
+ enclosing_class: str | None,
356
+ ) -> None:
357
+ """Emit a C++ function or method symbol from a function_definition node.
358
+
359
+ Qualification rules:
360
+ - Out-of-line method (qualified_identifier declarator): name = 'Class.method'
361
+ - In-class method (field_identifier declarator with enclosing class known):
362
+ name = 'EnclosingClass.method', kind='method'.
363
+ - Free function (identifier declarator, no enclosing class): name = plain name, kind='function'.
364
+ """
365
+ try:
366
+ name = _cpp_function_name(node)
367
+ if not name:
368
+ return
369
+
370
+ if "." in name:
371
+ kind = "method"
372
+ qualified = name
373
+ elif enclosing_class:
374
+ kind = "method"
375
+ qualified = f"{enclosing_class}.{name}"
376
+ else:
377
+ kind = "function"
378
+ qualified = name
379
+
380
+ doc = _c_doc_comment(node)
381
+ fields = extract_node_fields(
382
+ node,
383
+ "cpp",
384
+ qualified_name=qualified,
385
+ max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
386
+ )
387
+ symbols.append(
388
+ _make_symbol(
389
+ qualified,
390
+ kind,
391
+ file_str,
392
+ node,
393
+ doc,
394
+ signature=fields["signature"],
395
+ decorators=fields["decorators"],
396
+ is_exported=fields["is_exported"],
397
+ visibility=fields["visibility"],
398
+ qualified_name=qualified,
399
+ )
400
+ )
401
+ except Exception: # noqa: BLE001
402
+ logger.debug("_handle_cpp_function: failed for node at row %d", node.start_point[0])
403
+
404
+
405
+ def _extract_edges_cpp(root: Node, filepath: Path) -> list[Edge]:
406
+ """Extract import and call edges from a C++ AST.
407
+
408
+ Import heuristic:
409
+ #include "x.h" → target = stem ('x')
410
+ #include <x> → target = stem ('x') [system/STL header]
411
+
412
+ Call heuristic:
413
+ call_expression where 'function' is an identifier → bare call.
414
+ call_expression where 'function' is field_expression → obj.m() or obj->m().
415
+
416
+ Tier B B5: when SEAM_TYPE_INFERENCE is on, field_expression calls are resolved to
417
+ 'Type.method' qualified targets using per-function scope.
418
+ """
419
+ try:
420
+ edges: list[Edge] = []
421
+ file_str = str(filepath)
422
+ file_stem = filepath.stem
423
+ infer = config.SEAM_TYPE_INFERENCE == "on"
424
+ composition_on = config.SEAM_COMPOSITION_EDGES == "on"
425
+ field_access_on = config.SEAM_FIELD_ACCESS_EDGES == "on"
426
+ param_edges_on = config.SEAM_PARAM_EDGES == "on"
427
+
428
+ def _walk(
429
+ node: Node,
430
+ class_name: str | None,
431
+ class_fields: dict[str, str],
432
+ var_types: dict[str, str],
433
+ ) -> None:
434
+ ntype = node.type
435
+ if ntype == "preproc_include":
436
+ _handle_c_include(node, file_str, file_stem, edges)
437
+ return
438
+
439
+ if ntype in ("class_specifier", "struct_specifier"):
440
+ new_class: str | None = None
441
+ cn = node.child_by_field_name("name")
442
+ if cn is not None:
443
+ new_class = _text(cn).strip() or None
444
+ # Slice #79: emit holds edges for C++ class/struct fields.
445
+ if composition_on and new_class:
446
+ _handle_cpp_class_holds(node, new_class, file_str, edges)
447
+ new_fields = scan_class_fields_cpp(node) if infer else {}
448
+ for child in node.children:
449
+ _walk(child, new_class, new_fields, dict(new_fields))
450
+ return
451
+
452
+ if ntype == "function_definition":
453
+ new_types: dict[str, str] = dict(class_fields)
454
+ if infer:
455
+ record_cpp_param_types(node, new_types)
456
+ # 'uses' edges: function/method references plain user types as params.
457
+ if param_edges_on:
458
+ _fn = _cpp_function_name(node)
459
+ if _fn:
460
+ _src = (
461
+ f"{class_name}.{_fn}" if ("." not in _fn and class_name) else _fn
462
+ )
463
+ for ptype, pline in param_types_via_recorder(
464
+ node, record_cpp_param_types, "cpp"
465
+ ):
466
+ edges.append(Edge(
467
+ source=_src, target=ptype, kind="uses",
468
+ file=file_str, line=pline, confidence="INFERRED", receiver=None,
469
+ ))
470
+ body = node.child_by_field_name("body")
471
+ if body is not None:
472
+ # A3 Slice 4: emit reads/writes field-access edges for C++ methods.
473
+ if field_access_on:
474
+ func_name = _cpp_function_name(node)
475
+ if func_name:
476
+ # For in-class methods (field_identifier declarator),
477
+ # qualify with enclosing class.
478
+ if "." not in func_name and class_name:
479
+ source_fn = f"{class_name}.{func_name}"
480
+ else:
481
+ source_fn = func_name
482
+ # Determine class context: use dotted prefix if out-of-line.
483
+ ctx_class = class_name
484
+ if "." in source_fn:
485
+ ctx_class = source_fn.split(".")[0]
486
+ for src, tgt, mode, ln in extract_field_accesses_cpp(
487
+ body, source_fn, ctx_class, new_types
488
+ ):
489
+ edges.append(Edge(
490
+ source=src,
491
+ target=tgt,
492
+ kind=mode,
493
+ file=file_str,
494
+ line=ln,
495
+ confidence="INFERRED",
496
+ receiver=None,
497
+ ))
498
+ for child in body.children:
499
+ _walk(child, class_name, class_fields, new_types)
500
+ return
501
+
502
+ if infer and ntype == "declaration":
503
+ record_cpp_local_types(node, var_types)
504
+
505
+ # Tier B B6: new_expression (new Foo(...)) → instantiates edge.
506
+ elif ntype == "new_expression":
507
+ type_node = next(
508
+ (c for c in node.children if c.type == "type_identifier"), None
509
+ )
510
+ if type_node is not None:
511
+ type_name = _text(type_node)
512
+ if type_name:
513
+ source = _find_enclosing_function(node, "cpp")
514
+ if source is not None:
515
+ edges.append(
516
+ Edge(
517
+ source=source,
518
+ target=type_name,
519
+ kind="instantiates",
520
+ file=file_str,
521
+ line=node.start_point[0] + 1,
522
+ confidence="INFERRED",
523
+ receiver=None,
524
+ )
525
+ )
526
+ for child in node.children:
527
+ _walk(child, class_name, class_fields, var_types)
528
+ return
529
+
530
+ elif ntype == "call_expression":
531
+ func_child = node.child_by_field_name("function")
532
+ cpp_callee: str | None = None
533
+ cpp_recv: str | None = None
534
+
535
+ if func_child and func_child.type == "identifier":
536
+ cpp_callee = _text(func_child)
537
+ elif func_child and func_child.type == "field_expression":
538
+ arg_node = func_child.child_by_field_name("argument")
539
+ field_node = func_child.child_by_field_name("field")
540
+ if field_node is not None and field_node.type == "field_identifier":
541
+ cpp_callee = _text(field_node)
542
+ if arg_node is not None:
543
+ cpp_recv = _text(arg_node)
544
+
545
+ if cpp_callee:
546
+ final_target = cpp_callee
547
+ if infer and cpp_recv is not None:
548
+ recv_lookup = cpp_recv.lstrip("*").strip()
549
+ resolved_type = resolve_receiver_type_ext(
550
+ recv_lookup, class_name, var_types, _CPP_SELF_NAMES
551
+ )
552
+ if resolved_type:
553
+ final_target = f"{resolved_type}.{cpp_callee}"
554
+
555
+ source = _find_enclosing_function(node, "cpp")
556
+ if source is not None:
557
+ edges.append(
558
+ Edge(
559
+ source=source,
560
+ target=final_target,
561
+ kind="call",
562
+ file=file_str,
563
+ line=node.start_point[0] + 1,
564
+ confidence="INFERRED",
565
+ receiver=cpp_recv,
566
+ )
567
+ )
568
+
569
+ for child in node.children:
570
+ _walk(child, class_name, class_fields, var_types)
571
+
572
+ for child in root.children:
573
+ _walk(child, None, {}, {})
574
+
575
+ return edges
576
+ except Exception: # noqa: BLE001
577
+ logger.debug(
578
+ "_extract_edges_cpp: unhandled exception for file=%s",
579
+ filepath,
580
+ exc_info=True,
581
+ )
582
+ return []
583
+
584
+
585
+ def _handle_cpp_class_holds(
586
+ class_node: Node, class_name: str, file_str: str, edges: list[Edge]
587
+ ) -> None:
588
+ """Emit holds edges for each plain user-type field in a C++ class/struct.
589
+
590
+ Delegates to collect_composition_types_cpp for (held_type, line) pairs and
591
+ emits one Edge per unique pair. Never raises (backstop try/except).
592
+ """
593
+ try:
594
+ for held_type, held_line in collect_composition_types_cpp(class_node):
595
+ edges.append(
596
+ Edge(
597
+ source=class_name,
598
+ target=held_type,
599
+ kind="holds",
600
+ file=file_str,
601
+ line=held_line,
602
+ confidence="INFERRED",
603
+ receiver=None,
604
+ )
605
+ )
606
+ except Exception as exc: # noqa: BLE001
607
+ logger.debug("_handle_cpp_class_holds: failed: %r", exc)
608
+
609
+
610
+ def _extract_comments_cpp(root: Node, filepath: Path) -> list[Comment]:
611
+ """Walk a C++ AST and extract semantic comment markers (WHY/HACK/NOTE/TODO/FIXME).
612
+
613
+ C++ shares the same 'comment' node type as C — delegate to the C implementation.
614
+ """
615
+ return _extract_comments_c(root, filepath)