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,562 @@
1
+ """C symbol, edge, and comment extraction from tree-sitter ASTs.
2
+
3
+ LAYER: imports from graph_common (leaf) — never from graph.py.
4
+
5
+ LAYERING:
6
+ graph_common (leaf — no seam deps)
7
+
8
+ graph_c (this file)
9
+
10
+ graph_c_cpp (thin re-exporter; graph.py imports from there)
11
+
12
+ WHY split from graph_c_cpp.py: graph_c_cpp.py exceeded 1000 lines after Tier B additions.
13
+ C and C++ are now each large enough to stand alone.
14
+
15
+ All extractor functions follow the same contract:
16
+ - Accept a tree-sitter Node + filepath.
17
+ - Return a list — never raise, never return None.
18
+ - Edges carry confidence='INFERRED' by default.
19
+
20
+ GRAMMAR FACTS (verified by dumping AST from real fixtures):
21
+ function_definition: fields 'type', 'declarator' (function_declarator whose
22
+ 'declarator' field is an identifier = function name), 'body'.
23
+ storage_class_specifier "static" → file-local → visibility='private', is_exported=False.
24
+ struct_specifier / union_specifier: field 'name' (type_identifier) → kind='class'.
25
+ enum_specifier: field 'name' → kind='type'.
26
+ type_definition: last type_identifier child is the typedef alias → kind='type'.
27
+ preproc_include: field 'path' — string_literal (local) or system_lib_string (system).
28
+ call_expression: field 'function' = identifier → bare call target.
29
+ comment: covers both // and /* */ comments.
30
+ """
31
+
32
+ import logging
33
+ from pathlib import Path
34
+
35
+ from tree_sitter import Node
36
+
37
+ import seam.config as config
38
+ from seam.analysis.builtins import is_builtin
39
+ from seam.indexer.field_access_ext import (
40
+ collect_field_symbols_c,
41
+ extract_field_accesses_c,
42
+ )
43
+ from seam.indexer.graph_common import (
44
+ Comment,
45
+ Edge,
46
+ Symbol,
47
+ _block_comment_lines,
48
+ _find_enclosing_function,
49
+ _make_symbol,
50
+ _match_marker,
51
+ _text,
52
+ )
53
+ from seam.indexer.signatures import extract_node_fields
54
+
55
+ logger = logging.getLogger(__name__)
56
+
57
+
58
+ # ── Shared C/C++ helpers (also imported by graph_cpp.py) ──────────────────────
59
+
60
+
61
+ def _c_doc_comment(decl_node: Node) -> str | None:
62
+ """Capture a C/C++ doc comment above a declaration.
63
+
64
+ Strategy:
65
+ 1. If the immediately preceding sibling is a /* */ block comment that
66
+ starts with '/**', return its cleaned content (Javadoc-style).
67
+ 2. Otherwise, collect contiguous // line comments immediately above the
68
+ declaration (row-adjacent, no blank lines).
69
+ """
70
+ try:
71
+ prev = decl_node.prev_sibling
72
+ if prev is None:
73
+ return None
74
+
75
+ if prev.type == "comment":
76
+ raw = _text(prev)
77
+ if raw.startswith("/**"):
78
+ block_lines: list[str] = []
79
+ for line in raw.splitlines():
80
+ s = line.strip()
81
+ if s.startswith("/*"):
82
+ s = s[2:]
83
+ if s.endswith("*/"):
84
+ s = s[:-2]
85
+ s = s.strip().lstrip("*").strip()
86
+ if s:
87
+ block_lines.append(s)
88
+ return "\n".join(block_lines) if block_lines else None
89
+
90
+ if raw.startswith("//"):
91
+ lines: list[str] = []
92
+ current: Node | None = prev
93
+ while current is not None and current.type == "comment":
94
+ c_raw = _text(current)
95
+ if not c_raw.startswith("//"):
96
+ break
97
+ next_node = current.next_sibling
98
+ if next_node is not None:
99
+ end_row = current.end_point[0]
100
+ next_start = next_node.start_point[0]
101
+ if end_row + 1 != next_start:
102
+ break
103
+ body = c_raw[2:].strip()
104
+ lines.append(body)
105
+ current = current.prev_sibling
106
+ if not lines:
107
+ return None
108
+ return "\n".join(reversed(lines))
109
+ except Exception: # noqa: BLE001
110
+ pass
111
+ return None
112
+
113
+
114
+ def _handle_c_include(node: Node, file_str: str, file_stem: str, edges: list[Edge]) -> None:
115
+ """Emit an import edge from a C/C++ preproc_include node.
116
+
117
+ Both local (#include "x.h") and system (#include <x.h>) includes produce edges.
118
+ The target is the header filename stem (e.g. "utils" from "utils.h" or <stdio.h>).
119
+ """
120
+ try:
121
+ line = node.start_point[0] + 1
122
+ path_node = node.child_by_field_name("path")
123
+ if path_node is None:
124
+ return
125
+
126
+ if path_node.type == "string_literal":
127
+ content = None
128
+ for child in path_node.children:
129
+ if child.type == "string_content":
130
+ content = _text(child)
131
+ break
132
+ if content is None:
133
+ content = _text(path_node).strip('"')
134
+ target = Path(content).stem if content else None
135
+
136
+ elif path_node.type == "system_lib_string":
137
+ raw = _text(path_node).strip("<>")
138
+ target = Path(raw).stem if raw else None
139
+
140
+ else:
141
+ return
142
+
143
+ if target:
144
+ edges.append(
145
+ Edge(
146
+ source=file_stem,
147
+ target=target,
148
+ kind="import",
149
+ file=file_str,
150
+ line=line,
151
+ confidence="INFERRED",
152
+ receiver=None,
153
+ )
154
+ )
155
+ except Exception: # noqa: BLE001
156
+ logger.debug("_handle_c_include: failed for node at row %d", node.start_point[0])
157
+
158
+
159
+ # ── C extraction ───────────────────────────────────────────────────────────────
160
+
161
+
162
+ def _c_function_name(func_def: Node) -> str | None:
163
+ """Extract the function name from a C function_definition node.
164
+
165
+ The name lives inside: function_definition → function_declarator → identifier.
166
+ """
167
+ try:
168
+ declarator = func_def.child_by_field_name("declarator")
169
+ if declarator is None:
170
+ return None
171
+ name_node = declarator.child_by_field_name("declarator")
172
+ if name_node is not None and name_node.type == "identifier":
173
+ return _text(name_node)
174
+ except Exception: # noqa: BLE001
175
+ pass
176
+ return None
177
+
178
+
179
+ def _c_param_types(func_def: Node) -> list[tuple[str, int]]:
180
+ """Return (plain_user_type, line) pairs from a C function_definition's parameters.
181
+
182
+ A C parameter_declaration's `type` field is a type_identifier (a typedef'd user type,
183
+ e.g. `Engine e`) or a struct_specifier (`struct Engine *e`). Primitive params
184
+ (int/char/…) parse as primitive_type and are skipped naturally. Pointer/array shapes
185
+ live in the declarator, so the type field is the pointed-to type regardless of `*`.
186
+ Builtins filtered via is_builtin. Deduped within the signature. Never raises.
187
+ """
188
+ try:
189
+ declarator = func_def.child_by_field_name("declarator")
190
+ if declarator is None:
191
+ return []
192
+ params = declarator.child_by_field_name("parameters")
193
+ if params is None:
194
+ return []
195
+ line = func_def.start_point[0] + 1
196
+ seen: set[str] = set()
197
+ out: list[tuple[str, int]] = []
198
+ for p in params.named_children:
199
+ if p.type != "parameter_declaration":
200
+ continue
201
+ tnode = p.child_by_field_name("type")
202
+ if tnode is None:
203
+ continue
204
+ name: str | None = None
205
+ if tnode.type == "type_identifier":
206
+ name = _text(tnode)
207
+ elif tnode.type == "struct_specifier":
208
+ nm = tnode.child_by_field_name("name")
209
+ if nm is not None:
210
+ name = _text(nm)
211
+ if name and not is_builtin(name, "c") and name not in seen:
212
+ seen.add(name)
213
+ out.append((name, line))
214
+ return out
215
+ except Exception as exc: # noqa: BLE001
216
+ logger.debug("_c_param_types: failed: %r", exc)
217
+ return []
218
+
219
+
220
+ def _c_is_static(func_def: Node) -> bool:
221
+ """Return True if a C function_definition has a storage_class_specifier 'static'."""
222
+ try:
223
+ for child in func_def.children:
224
+ if child.type == "storage_class_specifier":
225
+ if _text(child).strip() == "static":
226
+ return True
227
+ except Exception: # noqa: BLE001
228
+ pass
229
+ return False
230
+
231
+
232
+ def _extract_symbols_c(root: Node, filepath: Path) -> list[Symbol]:
233
+ """Walk a C AST and extract function, struct, union, enum, and typedef symbols.
234
+
235
+ Kind mapping (per Phase 9 spec):
236
+ function_definition → function
237
+ struct_specifier (named) → class
238
+ union_specifier (named) → class
239
+ enum_specifier (named) → type
240
+ type_definition (typedef) → type
241
+ """
242
+ try:
243
+ symbols: list[Symbol] = []
244
+ file_str = str(filepath)
245
+ field_access_on = config.SEAM_FIELD_ACCESS_EDGES == "on"
246
+
247
+ def _walk(node: Node) -> None:
248
+ if node.type == "function_definition":
249
+ _handle_c_function(node, file_str, symbols)
250
+
251
+ elif node.type == "struct_specifier":
252
+ _handle_c_aggregate(node, "class", file_str, symbols)
253
+ # A3 Slice 4: emit field symbols for C struct fields.
254
+ if field_access_on:
255
+ _c_emit_field_symbols(node, file_str, symbols)
256
+
257
+ elif node.type == "union_specifier":
258
+ _handle_c_aggregate(node, "class", file_str, symbols)
259
+
260
+ elif node.type == "enum_specifier":
261
+ _handle_c_aggregate(node, "type", file_str, symbols)
262
+
263
+ elif node.type == "type_definition":
264
+ _handle_c_typedef(node, file_str, symbols)
265
+
266
+ else:
267
+ for child in node.children:
268
+ _walk(child)
269
+
270
+ for child in root.children:
271
+ _walk(child)
272
+
273
+ return symbols
274
+ except Exception: # noqa: BLE001
275
+ logger.debug(
276
+ "_extract_symbols_c: unhandled exception for file=%s",
277
+ filepath,
278
+ exc_info=True,
279
+ )
280
+ return []
281
+
282
+
283
+ def _handle_c_function(node: Node, file_str: str, symbols: list[Symbol]) -> None:
284
+ """Emit a C function symbol from a function_definition node."""
285
+ try:
286
+ name = _c_function_name(node)
287
+ if not name:
288
+ return
289
+ doc = _c_doc_comment(node)
290
+ fields = extract_node_fields(
291
+ node,
292
+ "c",
293
+ qualified_name=name,
294
+ max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
295
+ )
296
+ symbols.append(
297
+ _make_symbol(
298
+ name,
299
+ "function",
300
+ file_str,
301
+ node,
302
+ doc,
303
+ signature=fields["signature"],
304
+ decorators=fields["decorators"],
305
+ is_exported=fields["is_exported"],
306
+ visibility=fields["visibility"],
307
+ qualified_name=name,
308
+ )
309
+ )
310
+ except Exception: # noqa: BLE001
311
+ logger.debug("_handle_c_function: failed for node at row %d", node.start_point[0])
312
+
313
+
314
+ def _handle_c_aggregate(node: Node, kind: str, file_str: str, symbols: list[Symbol]) -> None:
315
+ """Emit a C struct/union/enum symbol from a named specifier node."""
316
+ try:
317
+ name_node = node.child_by_field_name("name")
318
+ if name_node is None:
319
+ return
320
+ name = _text(name_node)
321
+ if not name:
322
+ return
323
+ doc = _c_doc_comment(node)
324
+ fields = extract_node_fields(
325
+ node,
326
+ "c",
327
+ qualified_name=name,
328
+ max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
329
+ )
330
+ symbols.append(
331
+ _make_symbol(
332
+ name,
333
+ kind,
334
+ file_str,
335
+ node,
336
+ doc,
337
+ signature=fields["signature"],
338
+ decorators=fields["decorators"],
339
+ is_exported=fields["is_exported"],
340
+ visibility=fields["visibility"],
341
+ qualified_name=name,
342
+ )
343
+ )
344
+ except Exception: # noqa: BLE001
345
+ logger.debug("_handle_c_aggregate: failed for node at row %d", node.start_point[0])
346
+
347
+
348
+ def _handle_c_typedef(node: Node, file_str: str, symbols: list[Symbol]) -> None:
349
+ """Emit a C typedef symbol from a type_definition node."""
350
+ try:
351
+ declarator = node.child_by_field_name("declarator")
352
+ if declarator is not None and declarator.type == "type_identifier":
353
+ name = _text(declarator)
354
+ else:
355
+ name = None
356
+ for child in reversed(node.children):
357
+ if child.type == "type_identifier":
358
+ name = _text(child)
359
+ break
360
+ if not name:
361
+ return
362
+ doc = _c_doc_comment(node)
363
+ fields = extract_node_fields(
364
+ node,
365
+ "c",
366
+ qualified_name=name,
367
+ max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
368
+ )
369
+ symbols.append(
370
+ _make_symbol(
371
+ name,
372
+ "type",
373
+ file_str,
374
+ node,
375
+ doc,
376
+ signature=fields["signature"],
377
+ decorators=fields["decorators"],
378
+ is_exported=fields["is_exported"],
379
+ visibility=fields["visibility"],
380
+ qualified_name=name,
381
+ )
382
+ )
383
+ except Exception: # noqa: BLE001
384
+ logger.debug("_handle_c_typedef: failed for node at row %d", node.start_point[0])
385
+
386
+
387
+ def _c_emit_field_symbols(
388
+ struct_node: Node,
389
+ file_str: str,
390
+ symbols: list[Symbol],
391
+ ) -> None:
392
+ """Emit kind='field' symbols for all fields in a C struct_specifier.
393
+
394
+ Called when SEAM_FIELD_ACCESS_EDGES='on'. Uses collect_field_symbols_c to get
395
+ (qualified_field, line) pairs and appends Symbol entries.
396
+
397
+ Never raises (backstop try/except).
398
+ """
399
+ try:
400
+ name_node = struct_node.child_by_field_name("name")
401
+ if name_node is None:
402
+ return
403
+ struct_name = _text(name_node).strip()
404
+ if not struct_name:
405
+ return
406
+
407
+ for qualified_field, field_line in collect_field_symbols_c(struct_node, struct_name):
408
+ symbols.append(Symbol(
409
+ name=qualified_field,
410
+ kind="field",
411
+ file=file_str,
412
+ start_line=field_line,
413
+ end_line=field_line,
414
+ docstring=None,
415
+ signature=None,
416
+ decorators=[],
417
+ is_exported=None,
418
+ visibility=None,
419
+ qualified_name=qualified_field,
420
+ ))
421
+ except Exception as exc: # noqa: BLE001
422
+ logger.debug("_c_emit_field_symbols: failed: %r", exc)
423
+
424
+
425
+ def _extract_edges_c(root: Node, filepath: Path) -> list[Edge]:
426
+ """Extract import and call edges from a C AST.
427
+
428
+ Import heuristic:
429
+ #include "x.h" → target = stem of "x.h" (e.g. 'x')
430
+ #include <x.h> → target = stem of 'x.h' (e.g. 'x') [system header]
431
+
432
+ Call heuristic (MVP — bare identifiers and field_expression):
433
+ call_expression where 'function' field is an identifier → kind="call".
434
+ call_expression where 'function' field is field_expression → obj->func_ptr() → capture receiver.
435
+ """
436
+ try:
437
+ edges: list[Edge] = []
438
+ file_str = str(filepath)
439
+ file_stem = filepath.stem
440
+ field_access_on = config.SEAM_FIELD_ACCESS_EDGES == "on"
441
+
442
+ def _walk(node: Node) -> None:
443
+ if node.type == "preproc_include":
444
+ _handle_c_include(node, file_str, file_stem, edges)
445
+ return
446
+
447
+ elif node.type == "function_definition":
448
+ # 'uses' edges: function references plain user types as params. C functions
449
+ # are top-level → source is the bare function name.
450
+ if config.SEAM_PARAM_EDGES == "on":
451
+ _fn = _c_function_name(node)
452
+ if _fn:
453
+ for ptype, pline in _c_param_types(node):
454
+ edges.append(Edge(
455
+ source=_fn, target=ptype, kind="uses",
456
+ file=file_str, line=pline, confidence="INFERRED", receiver=None,
457
+ ))
458
+ # A3 Slice 4: emit reads/writes field-access edges from C function bodies.
459
+ if field_access_on:
460
+ func_name = _c_function_name(node)
461
+ body = node.child_by_field_name("body")
462
+ if func_name and body is not None:
463
+ for src, tgt, mode, line in extract_field_accesses_c(
464
+ body, func_name, None, {}
465
+ ):
466
+ edges.append(Edge(
467
+ source=src,
468
+ target=tgt,
469
+ kind=mode,
470
+ file=file_str,
471
+ line=line,
472
+ confidence="INFERRED",
473
+ receiver=None,
474
+ ))
475
+
476
+ elif node.type == "call_expression":
477
+ func_child = node.child_by_field_name("function")
478
+ c_callee: str | None = None
479
+ c_recv: str | None = None
480
+
481
+ if func_child and func_child.type == "identifier":
482
+ c_callee = _text(func_child)
483
+ elif func_child and func_child.type == "field_expression":
484
+ arg_node = func_child.child_by_field_name("argument")
485
+ field_node = func_child.child_by_field_name("field")
486
+ if field_node is not None and field_node.type == "field_identifier":
487
+ c_callee = _text(field_node)
488
+ if arg_node is not None:
489
+ c_recv = _text(arg_node)
490
+
491
+ if c_callee:
492
+ source = _find_enclosing_function(node, "c")
493
+ if source is not None:
494
+ edges.append(
495
+ Edge(
496
+ source=source,
497
+ target=c_callee,
498
+ kind="call",
499
+ file=file_str,
500
+ line=node.start_point[0] + 1,
501
+ confidence="INFERRED",
502
+ receiver=c_recv,
503
+ )
504
+ )
505
+
506
+ for child in node.children:
507
+ _walk(child)
508
+
509
+ for child in root.children:
510
+ _walk(child)
511
+
512
+ return edges
513
+ except Exception: # noqa: BLE001
514
+ logger.debug(
515
+ "_extract_edges_c: unhandled exception for file=%s",
516
+ filepath,
517
+ exc_info=True,
518
+ )
519
+ return []
520
+
521
+
522
+ def _extract_comments_c(root: Node, filepath: Path) -> list[Comment]:
523
+ """Walk a C AST and extract semantic comment markers (WHY/HACK/NOTE/TODO/FIXME).
524
+
525
+ C uses a single 'comment' node type for both // line and /* */ block comments.
526
+ """
527
+ try:
528
+ comments: list[Comment] = []
529
+
530
+ def _walk(node: Node) -> None:
531
+ if node.type == "comment":
532
+ raw = _text(node)
533
+ base_row = node.start_point[0] + 1
534
+ if raw.startswith("/*"):
535
+ for offset, body in _block_comment_lines(raw):
536
+ result = _match_marker(body)
537
+ if result is not None:
538
+ marker, text = result
539
+ comments.append(
540
+ Comment(marker=marker, text=text, line=base_row + offset)
541
+ )
542
+ else:
543
+ body = raw.lstrip("/").strip()
544
+ result = _match_marker(body)
545
+ if result is not None:
546
+ marker, text = result
547
+ comments.append(Comment(marker=marker, text=text, line=base_row))
548
+
549
+ for child in node.children:
550
+ _walk(child)
551
+
552
+ for child in root.children:
553
+ _walk(child)
554
+
555
+ return comments
556
+ except Exception: # noqa: BLE001
557
+ logger.debug(
558
+ "_extract_comments_c: unhandled exception for file=%s",
559
+ filepath,
560
+ exc_info=True,
561
+ )
562
+ return []
@@ -0,0 +1,39 @@
1
+ """Thin re-exporter: C + C++ symbol/edge/comment extractors.
2
+
3
+ LAYER: imports from graph_c (leaf) and graph_cpp (leaf) — never from graph.py.
4
+
5
+ WHY this file exists: graph.py imports all family extractors through stable public names.
6
+ The C and C++ extractors were split into graph_c.py and graph_cpp.py when
7
+ graph_c_cpp.py exceeded 1000 lines (Tier B additions). This re-exporter keeps
8
+ graph.py's import stable.
9
+
10
+ graph.py continues to import from this module:
11
+ from seam.indexer.graph_c_cpp import (
12
+ _extract_comments_c, _extract_comments_cpp,
13
+ _extract_edges_c, _extract_edges_cpp,
14
+ _extract_symbols_c, _extract_symbols_cpp,
15
+ )
16
+ """
17
+
18
+ # Re-export C extractors — the actual code lives in graph_c.py.
19
+ from seam.indexer.graph_c import (
20
+ _extract_comments_c,
21
+ _extract_edges_c,
22
+ _extract_symbols_c,
23
+ )
24
+
25
+ # Re-export C++ extractors — the actual code lives in graph_cpp.py.
26
+ from seam.indexer.graph_cpp import (
27
+ _extract_comments_cpp,
28
+ _extract_edges_cpp,
29
+ _extract_symbols_cpp,
30
+ )
31
+
32
+ __all__ = [
33
+ "_extract_comments_c",
34
+ "_extract_edges_c",
35
+ "_extract_symbols_c",
36
+ "_extract_comments_cpp",
37
+ "_extract_edges_cpp",
38
+ "_extract_symbols_cpp",
39
+ ]