seam-code 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. seam/__init__.py +3 -0
  2. seam/_data/schema.sql +225 -0
  3. seam/_web/assets/index-BL_tqprR.js +216 -0
  4. seam/_web/assets/index-GTKUhVyD.css +1 -0
  5. seam/_web/index.html +13 -0
  6. seam/analysis/__init__.py +14 -0
  7. seam/analysis/affected.py +254 -0
  8. seam/analysis/builtins.py +966 -0
  9. seam/analysis/byte_budget.py +217 -0
  10. seam/analysis/changes.py +709 -0
  11. seam/analysis/cluster_naming.py +260 -0
  12. seam/analysis/clustering.py +216 -0
  13. seam/analysis/confidence.py +699 -0
  14. seam/analysis/embeddings.py +195 -0
  15. seam/analysis/flows.py +708 -0
  16. seam/analysis/impact.py +444 -0
  17. seam/analysis/imports.py +994 -0
  18. seam/analysis/imports_ext.py +780 -0
  19. seam/analysis/imports_resolve.py +176 -0
  20. seam/analysis/processes.py +453 -0
  21. seam/analysis/relevance.py +155 -0
  22. seam/analysis/rwr.py +129 -0
  23. seam/analysis/staleness.py +328 -0
  24. seam/analysis/steer.py +282 -0
  25. seam/analysis/synthesis.py +253 -0
  26. seam/analysis/synthesis_channels.py +433 -0
  27. seam/analysis/testpaths.py +103 -0
  28. seam/analysis/traversal.py +470 -0
  29. seam/cli/__init__.py +0 -0
  30. seam/cli/install.py +232 -0
  31. seam/cli/main.py +2602 -0
  32. seam/cli/output.py +137 -0
  33. seam/cli/read.py +244 -0
  34. seam/cli/serve.py +145 -0
  35. seam/config.py +551 -0
  36. seam/indexer/__init__.py +0 -0
  37. seam/indexer/cluster_index.py +425 -0
  38. seam/indexer/db.py +496 -0
  39. seam/indexer/embedding_index.py +183 -0
  40. seam/indexer/field_access.py +536 -0
  41. seam/indexer/field_access_c_cpp.py +643 -0
  42. seam/indexer/field_access_ext.py +708 -0
  43. seam/indexer/field_access_ext2.py +408 -0
  44. seam/indexer/field_access_go_rust.py +737 -0
  45. seam/indexer/field_access_php_swift.py +888 -0
  46. seam/indexer/field_access_ts.py +626 -0
  47. seam/indexer/graph.py +321 -0
  48. seam/indexer/graph_c.py +562 -0
  49. seam/indexer/graph_c_cpp.py +39 -0
  50. seam/indexer/graph_common.py +644 -0
  51. seam/indexer/graph_cpp.py +615 -0
  52. seam/indexer/graph_csharp.py +651 -0
  53. seam/indexer/graph_go.py +723 -0
  54. seam/indexer/graph_go_rust.py +39 -0
  55. seam/indexer/graph_java.py +689 -0
  56. seam/indexer/graph_java_csharp.py +38 -0
  57. seam/indexer/graph_php.py +914 -0
  58. seam/indexer/graph_python.py +628 -0
  59. seam/indexer/graph_ruby.py +748 -0
  60. seam/indexer/graph_rust.py +653 -0
  61. seam/indexer/graph_scope_infer.py +902 -0
  62. seam/indexer/graph_scope_infer_ext.py +723 -0
  63. seam/indexer/graph_scope_infer_ext2.py +992 -0
  64. seam/indexer/graph_swift.py +1014 -0
  65. seam/indexer/graph_swift_infer.py +515 -0
  66. seam/indexer/graph_typescript.py +663 -0
  67. seam/indexer/migrations.py +816 -0
  68. seam/indexer/parser.py +204 -0
  69. seam/indexer/pipeline.py +197 -0
  70. seam/indexer/signatures.py +634 -0
  71. seam/indexer/signatures_ext.py +780 -0
  72. seam/indexer/sync.py +287 -0
  73. seam/indexer/synthesis_index.py +291 -0
  74. seam/indexer/tokenize.py +79 -0
  75. seam/installer/__init__.py +67 -0
  76. seam/installer/claude.py +97 -0
  77. seam/installer/codex.py +94 -0
  78. seam/installer/core.py +127 -0
  79. seam/installer/cursor.py +61 -0
  80. seam/installer/guide.py +110 -0
  81. seam/installer/jsonfile.py +85 -0
  82. seam/installer/markdownfile.py +146 -0
  83. seam/installer/tomlfile.py +72 -0
  84. seam/query/__init__.py +0 -0
  85. seam/query/clusters.py +206 -0
  86. seam/query/comments.py +217 -0
  87. seam/query/context.py +293 -0
  88. seam/query/engine.py +940 -0
  89. seam/query/fts.py +328 -0
  90. seam/query/names.py +470 -0
  91. seam/query/pack.py +433 -0
  92. seam/query/semantic.py +339 -0
  93. seam/query/structure.py +727 -0
  94. seam/server/__init__.py +0 -0
  95. seam/server/graph_api.py +437 -0
  96. seam/server/handler_common.py +323 -0
  97. seam/server/impact_handler.py +615 -0
  98. seam/server/mcp.py +556 -0
  99. seam/server/tools.py +697 -0
  100. seam/server/trace_handler.py +184 -0
  101. seam/server/web.py +922 -0
  102. seam/watcher/__init__.py +0 -0
  103. seam/watcher/__main__.py +56 -0
  104. seam/watcher/daemon.py +237 -0
  105. seam_code-0.3.0.dist-info/METADATA +318 -0
  106. seam_code-0.3.0.dist-info/RECORD +109 -0
  107. seam_code-0.3.0.dist-info/WHEEL +4 -0
  108. seam_code-0.3.0.dist-info/entry_points.txt +2 -0
  109. seam_code-0.3.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,780 @@
1
+ """Phase 9 node-field extractors — Java, C#, Ruby, C, C++, PHP.
2
+
3
+ LAYER: pure leaf module — imports only stdlib + tree_sitter.
4
+ Must NOT import from any other seam.indexer module (same contract as signatures.py).
5
+
6
+ LAYERING:
7
+ signatures_ext (this file — leaf, no seam deps)
8
+
9
+ signatures.py (dispatch entry point — imports this at top level)
10
+
11
+ Entry points (called from signatures.extract_node_fields):
12
+ _extract_java / _extract_csharp / _extract_ruby
13
+ _extract_c / _extract_cpp / _extract_php
14
+
15
+ Each function signature: (node, qualified_name, max_sig_len) -> NodeFields
16
+ NEVER raises. On any failure, returns _safe_defaults(qualified_name).
17
+ """
18
+
19
+ import logging
20
+ from typing import TypedDict
21
+
22
+ from tree_sitter import Node
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class NodeFields(TypedDict):
28
+ """Five enrichment fields extracted per symbol node (all nullable).
29
+
30
+ WHY re-declared here rather than imported from signatures.py:
31
+ signatures_ext must stay a leaf (no seam deps). Importing NodeFields from
32
+ signatures.py would create a cycle (signatures.py imports signatures_ext).
33
+ The TypedDict shape is identical — this is intentional duplication to
34
+ preserve the leaf property of both modules.
35
+ """
36
+
37
+ signature: str | None
38
+ decorators: list[str]
39
+ is_exported: bool | None
40
+ visibility: str | None
41
+ qualified_name: str | None
42
+
43
+
44
+ def _safe_defaults(qualified_name: str | None = None) -> NodeFields:
45
+ """Return a safe NodeFields with all nulls/empty. Used on any extraction failure."""
46
+ return NodeFields(
47
+ signature=None,
48
+ decorators=[],
49
+ is_exported=None,
50
+ visibility=None,
51
+ qualified_name=qualified_name,
52
+ )
53
+
54
+
55
+ def _text(node: Node | None) -> str:
56
+ """Safely decode a tree-sitter node's text bytes to str."""
57
+ if node is None:
58
+ return ""
59
+ raw = node.text
60
+ if raw is None:
61
+ return ""
62
+ return raw.decode("utf-8", errors="replace")
63
+
64
+
65
+ def _normalize_to_one_line(text: str) -> str:
66
+ """Collapse embedded newlines and multiple spaces into a single space."""
67
+ return " ".join(text.split())
68
+
69
+
70
+ def _truncate(text: str, max_len: int) -> str:
71
+ """Truncate text to max_len characters, appending '...' if needed."""
72
+ if len(text) <= max_len:
73
+ return text
74
+ return text[: max_len - 3] + "..."
75
+
76
+
77
+ # ── Java ──────────────────────────────────────────────────────────────────────
78
+
79
+
80
+ _JAVA_VISIBILITY_KEYWORDS: frozenset[str] = frozenset({"public", "private", "protected"})
81
+
82
+
83
+ def _java_modifiers(node: Node) -> Node | None:
84
+ """Find the 'modifiers' child of a Java declaration node, or None."""
85
+ for child in node.children:
86
+ if child.type == "modifiers":
87
+ return child
88
+ return None
89
+
90
+
91
+ def _java_visibility(node: Node) -> str | None:
92
+ """Extract visibility from a Java declaration's modifiers child.
93
+
94
+ Scans modifiers children for public/private/protected keyword nodes.
95
+ Returns None for package-private (no access modifier present).
96
+ """
97
+ mods = _java_modifiers(node)
98
+ if mods is None:
99
+ return None
100
+ try:
101
+ for child in mods.children:
102
+ if child.type in _JAVA_VISIBILITY_KEYWORDS:
103
+ return child.type
104
+ except Exception: # noqa: BLE001
105
+ pass
106
+ return None
107
+
108
+
109
+ def _java_annotations(node: Node) -> list[str]:
110
+ """Extract Java annotation texts (@Service, @Override, etc.) from a declaration node.
111
+
112
+ Looks inside the modifiers child for marker_annotation and annotation nodes.
113
+ Returns verbatim text (e.g. '@Service', '@SuppressWarnings("all")').
114
+ """
115
+ result: list[str] = []
116
+ mods = _java_modifiers(node)
117
+ if mods is None:
118
+ return result
119
+ try:
120
+ for child in mods.children:
121
+ if child.type in ("marker_annotation", "annotation"):
122
+ text = _text(child).strip()
123
+ if text:
124
+ result.append(text)
125
+ except Exception: # noqa: BLE001
126
+ pass
127
+ return result
128
+
129
+
130
+ def _java_signature(node: Node) -> str | None:
131
+ """Build a one-line Java signature from a declaration node.
132
+
133
+ Covers: class_declaration, interface_declaration, enum_declaration,
134
+ record_declaration, method_declaration, constructor_declaration.
135
+
136
+ Strategy: collect text from all children BEFORE the body (class_body, block,
137
+ constructor_body, enum_body, interface_body), join, and normalize to one line.
138
+ """
139
+ try:
140
+ body_types = frozenset(
141
+ {
142
+ "class_body",
143
+ "block",
144
+ "constructor_body",
145
+ "enum_body",
146
+ "interface_body",
147
+ }
148
+ )
149
+ parts: list[str] = []
150
+ for child in node.children:
151
+ if child.type in body_types:
152
+ break
153
+ text = _text(child).strip()
154
+ if text:
155
+ parts.append(text)
156
+ if not parts:
157
+ return None
158
+ return _normalize_to_one_line(" ".join(parts))
159
+ except Exception: # noqa: BLE001
160
+ pass
161
+ return None
162
+
163
+
164
+ def _extract_java(
165
+ node: Node,
166
+ qualified_name: str | None,
167
+ max_sig_len: int,
168
+ ) -> NodeFields:
169
+ """Extract all five enrichment fields for a Java symbol node.
170
+
171
+ signature : declaration header up to (but not including) the body, one line.
172
+ decorators : Java annotations verbatim (@Override, @Service, etc.).
173
+ is_exported : True when 'public' modifier is present.
174
+ visibility : 'public' | 'private' | 'protected' | None (package-private).
175
+ qualified_name : passed through from the caller.
176
+ """
177
+ try:
178
+ vis = _java_visibility(node)
179
+ is_exported = vis == "public"
180
+ annotations = _java_annotations(node)
181
+
182
+ sig = _java_signature(node)
183
+ if sig is not None:
184
+ sig = _truncate(sig, max_sig_len)
185
+
186
+ return NodeFields(
187
+ signature=sig,
188
+ decorators=annotations,
189
+ is_exported=is_exported,
190
+ visibility=vis,
191
+ qualified_name=qualified_name,
192
+ )
193
+ except Exception: # noqa: BLE001
194
+ return _safe_defaults(qualified_name)
195
+
196
+
197
+ # ── C# ────────────────────────────────────────────────────────────────────────
198
+
199
+
200
+ _CSHARP_VISIBILITY_KEYWORDS: frozenset[str] = frozenset(
201
+ {
202
+ "public",
203
+ "private",
204
+ "protected",
205
+ "internal",
206
+ }
207
+ )
208
+
209
+
210
+ def _csharp_modifier_text(node: Node) -> str | None:
211
+ """Find the first access modifier from a C# declaration's 'modifier' children.
212
+
213
+ Returns 'public', 'private', 'protected', or 'internal' if found, else None.
214
+ """
215
+ try:
216
+ for child in node.children:
217
+ if child.type == "modifier":
218
+ text = _text(child).strip()
219
+ if text in _CSHARP_VISIBILITY_KEYWORDS:
220
+ return text
221
+ except Exception: # noqa: BLE001
222
+ pass
223
+ return None
224
+
225
+
226
+ def _csharp_attributes_texts(node: Node) -> list[str]:
227
+ """Extract C# attribute lists ([Serializable], [HttpGet], etc.) verbatim.
228
+
229
+ Attribute lists appear as 'attribute_list' children before the modifier.
230
+ Returns each attribute_list as a verbatim string.
231
+ """
232
+ result: list[str] = []
233
+ try:
234
+ for child in node.children:
235
+ if child.type == "attribute_list":
236
+ text = _text(child).strip()
237
+ if text:
238
+ result.append(text)
239
+ except Exception: # noqa: BLE001
240
+ pass
241
+ return result
242
+
243
+
244
+ def _csharp_signature(node: Node) -> str | None:
245
+ """Build a one-line C# signature from a declaration node.
246
+
247
+ Covers: class_declaration, struct_declaration, record_declaration,
248
+ interface_declaration, enum_declaration, delegate_declaration,
249
+ method_declaration, constructor_declaration.
250
+
251
+ Strategy: collect text from all children BEFORE the body (block, declaration_list,
252
+ enum_member_declaration_list), join, normalize. Skip attribute_list children
253
+ (those are decorators, not part of the signature header).
254
+ """
255
+ try:
256
+ body_types = frozenset(
257
+ {
258
+ "block",
259
+ "declaration_list",
260
+ "enum_member_declaration_list",
261
+ }
262
+ )
263
+ skip_types = frozenset({"attribute_list"})
264
+ parts: list[str] = []
265
+ for child in node.children:
266
+ if child.type in body_types:
267
+ break
268
+ if child.type in skip_types:
269
+ continue
270
+ text = _text(child).strip()
271
+ if text:
272
+ parts.append(text)
273
+ if not parts:
274
+ return None
275
+ return _normalize_to_one_line(" ".join(parts))
276
+ except Exception: # noqa: BLE001
277
+ pass
278
+ return None
279
+
280
+
281
+ def _extract_csharp(
282
+ node: Node,
283
+ qualified_name: str | None,
284
+ max_sig_len: int,
285
+ ) -> NodeFields:
286
+ """Extract all five enrichment fields for a C# symbol node.
287
+
288
+ signature : declaration header up to (but not including) the body, one line.
289
+ decorators : C# attributes verbatim ([Serializable], [HttpGet], etc.).
290
+ is_exported : True when 'public' modifier is present.
291
+ visibility : 'public' | 'private' | 'protected' | 'internal' | None.
292
+ qualified_name : passed through from the caller.
293
+ """
294
+ try:
295
+ vis = _csharp_modifier_text(node)
296
+ is_exported = vis == "public"
297
+ attributes = _csharp_attributes_texts(node)
298
+
299
+ sig = _csharp_signature(node)
300
+ if sig is not None:
301
+ sig = _truncate(sig, max_sig_len)
302
+
303
+ return NodeFields(
304
+ signature=sig,
305
+ decorators=attributes,
306
+ is_exported=is_exported,
307
+ visibility=vis,
308
+ qualified_name=qualified_name,
309
+ )
310
+ except Exception: # noqa: BLE001
311
+ return _safe_defaults(qualified_name)
312
+
313
+
314
+ # ── Ruby ──────────────────────────────────────────────────────────────────────
315
+
316
+
317
+ def _ruby_signature(node: Node) -> str | None:
318
+ """Build a one-line Ruby signature from a method, class, or module node.
319
+
320
+ For method nodes: capture everything from 'def' up to (but not including)
321
+ the body_statement. For class/module nodes: capture 'class Name' / 'module Name'.
322
+
323
+ Covers: method, singleton_method, class, module nodes.
324
+ Strategy: collect text from all children BEFORE body_statement, join, normalize.
325
+ SKIP 'comment' child nodes — inline comments like `module Utils # NOTE: ...`
326
+ can appear between the name (constant) and body_statement and must not be
327
+ included in the signature text.
328
+ """
329
+ try:
330
+ body_stop_types = frozenset({"body_statement", "end"})
331
+ # WHY skip comment: class/module nodes may have inline comment children
332
+ # between the class/module name and the body_statement. Including them
333
+ # would corrupt the signature (e.g. 'module Utils # NOTE: ...').
334
+ skip_types = frozenset({"comment"})
335
+ parts: list[str] = []
336
+ for child in node.children:
337
+ if child.type in body_stop_types:
338
+ break
339
+ if child.type in skip_types:
340
+ continue # skip inline comments
341
+ text = _text(child).strip()
342
+ if text:
343
+ parts.append(text)
344
+ if not parts:
345
+ return None
346
+ return _normalize_to_one_line(" ".join(parts))
347
+ except Exception: # noqa: BLE001
348
+ pass
349
+ return None
350
+
351
+
352
+ def _extract_ruby(
353
+ node: Node,
354
+ qualified_name: str | None,
355
+ max_sig_len: int,
356
+ ) -> NodeFields:
357
+ """Extract enrichment fields for a Ruby symbol node.
358
+
359
+ signature : method/class declaration header (best-effort, one line).
360
+ decorators : [] (Ruby has no decorator/annotation syntax).
361
+ is_exported : None (Ruby visibility is dynamic — private/protected not tracked at
362
+ extraction time; tracking would require stateful AST traversal).
363
+ visibility : None (same reason — deferred to a future enhancement).
364
+ qualified_name : passed through from the caller.
365
+
366
+ WHY None for visibility: Ruby's visibility model (private/protected/public) is
367
+ set by method calls (private :foo) rather than keywords on the declaration itself.
368
+ Statically extracting it correctly would require tracking call context — out of
369
+ scope for this MVP (same limitation noted in the spec).
370
+ """
371
+ try:
372
+ sig = _ruby_signature(node)
373
+ if sig is not None:
374
+ sig = _truncate(sig, max_sig_len)
375
+
376
+ return NodeFields(
377
+ signature=sig,
378
+ decorators=[], # Ruby has no annotation/decorator syntax
379
+ is_exported=None, # Dynamic visibility — not tracked statically
380
+ visibility=None, # Dynamic visibility — not tracked statically
381
+ qualified_name=qualified_name,
382
+ )
383
+ except Exception: # noqa: BLE001
384
+ return _safe_defaults(qualified_name)
385
+
386
+
387
+ # ── C ─────────────────────────────────────────────────────────────────────────
388
+
389
+
390
+ def _c_is_static(node: Node) -> bool:
391
+ """Return True if a C function_definition has a 'static' storage_class_specifier."""
392
+ try:
393
+ for child in node.children:
394
+ if child.type == "storage_class_specifier":
395
+ if _text(child).strip() == "static":
396
+ return True
397
+ except Exception: # noqa: BLE001
398
+ pass
399
+ return False
400
+
401
+
402
+ def _c_signature(node: Node) -> str | None:
403
+ """Build a one-line C signature from a declaration node.
404
+
405
+ Strategy: collect text from all children BEFORE the body (compound_statement,
406
+ field_declaration_list, enumerator_list), join, and normalize to one line.
407
+
408
+ Covers: function_definition, struct_specifier, union_specifier, enum_specifier,
409
+ type_definition.
410
+ """
411
+ try:
412
+ body_types = frozenset(
413
+ {
414
+ "compound_statement",
415
+ "field_declaration_list",
416
+ "enumerator_list",
417
+ }
418
+ )
419
+ parts: list[str] = []
420
+ for child in node.children:
421
+ if child.type in body_types:
422
+ break
423
+ text = _text(child).strip()
424
+ if text:
425
+ parts.append(text)
426
+ if not parts:
427
+ return None
428
+ return _normalize_to_one_line(" ".join(parts))
429
+ except Exception: # noqa: BLE001
430
+ pass
431
+ return None
432
+
433
+
434
+ def _extract_c(
435
+ node: Node,
436
+ qualified_name: str | None,
437
+ max_sig_len: int,
438
+ ) -> NodeFields:
439
+ """Extract all five enrichment fields for a C symbol node.
440
+
441
+ signature : declaration header up to (but not including) the body, one line.
442
+ decorators : [] (C has no decorator syntax).
443
+ is_exported : False when 'static' storage class is present (file-local); True otherwise.
444
+ visibility : 'private' for static functions (file-local); 'public' otherwise.
445
+ qualified_name : passed through from the caller.
446
+
447
+ WHY static→private: C 'static' at file scope means the function/variable is not
448
+ visible outside the translation unit — the closest equivalent to 'private'.
449
+ """
450
+ try:
451
+ is_static = _c_is_static(node)
452
+ vis = "private" if is_static else "public"
453
+ is_exported = not is_static
454
+
455
+ sig = _c_signature(node)
456
+ if sig is not None:
457
+ sig = _truncate(sig, max_sig_len)
458
+
459
+ return NodeFields(
460
+ signature=sig,
461
+ decorators=[],
462
+ is_exported=is_exported,
463
+ visibility=vis,
464
+ qualified_name=qualified_name,
465
+ )
466
+ except Exception: # noqa: BLE001
467
+ return _safe_defaults(qualified_name)
468
+
469
+
470
+ # ── C++ ───────────────────────────────────────────────────────────────────────
471
+
472
+
473
+ def _cpp_signature(node: Node) -> str | None:
474
+ """Build a one-line C++ signature from a declaration node.
475
+
476
+ Strategy: collect text from all children BEFORE the body (compound_statement,
477
+ field_declaration_list, enumerator_list, declaration_list), join, normalize.
478
+
479
+ Covers: class_specifier, struct_specifier, union_specifier, enum_specifier,
480
+ function_definition.
481
+ """
482
+ try:
483
+ body_types = frozenset(
484
+ {
485
+ "compound_statement",
486
+ "field_declaration_list",
487
+ "enumerator_list",
488
+ "declaration_list",
489
+ }
490
+ )
491
+ parts: list[str] = []
492
+ for child in node.children:
493
+ if child.type in body_types:
494
+ break
495
+ # Skip field_initializer_list (constructor initializer : x(v)) from signature
496
+ if child.type == "field_initializer_list":
497
+ break
498
+ text = _text(child).strip()
499
+ if text:
500
+ parts.append(text)
501
+ if not parts:
502
+ return None
503
+ return _normalize_to_one_line(" ".join(parts))
504
+ except Exception: # noqa: BLE001
505
+ pass
506
+ return None
507
+
508
+
509
+ def _extract_cpp(
510
+ node: Node,
511
+ qualified_name: str | None,
512
+ max_sig_len: int,
513
+ ) -> NodeFields:
514
+ """Extract all five enrichment fields for a C++ symbol node.
515
+
516
+ signature : declaration header up to (but not including) the body, one line.
517
+ decorators : [] (C++ [[...]] attributes are out of scope per PRD).
518
+ is_exported : True for public class members; True for free functions; None for
519
+ non-class context where visibility is undecidable at extraction time.
520
+ visibility : Not tracked per-member in this MVP (would require tracking
521
+ access_specifier state across the class body); returns None.
522
+ qualified_name : passed through from the caller.
523
+
524
+ WHY None for visibility: C++ visibility depends on the current access_specifier
525
+ (public/private/protected) within the class body, which would require stateful
526
+ tracking during AST traversal. This is deferred to a future enhancement.
527
+ """
528
+ try:
529
+ sig = _cpp_signature(node)
530
+ if sig is not None:
531
+ sig = _truncate(sig, max_sig_len)
532
+
533
+ return NodeFields(
534
+ signature=sig,
535
+ decorators=[],
536
+ is_exported=True, # Conservative: assume exportable at extraction time
537
+ visibility=None, # Stateful tracking required; out of scope for MVP
538
+ qualified_name=qualified_name,
539
+ )
540
+ except Exception: # noqa: BLE001
541
+ return _safe_defaults(qualified_name)
542
+
543
+
544
+ # ── PHP ───────────────────────────────────────────────────────────────────────
545
+
546
+
547
+ _PHP_VISIBILITY_KEYWORDS: frozenset[str] = frozenset({"public", "private", "protected"})
548
+
549
+
550
+ def _php_visibility(node: Node) -> str | None:
551
+ """Extract visibility from a PHP method_declaration or property_declaration node.
552
+
553
+ Looks for a visibility_modifier child and reads its keyword child.
554
+ Returns 'public' | 'private' | 'protected' | None.
555
+ """
556
+ try:
557
+ for child in node.children:
558
+ if child.type == "visibility_modifier":
559
+ for kw in child.children:
560
+ if kw.type in _PHP_VISIBILITY_KEYWORDS:
561
+ return kw.type
562
+ except Exception: # noqa: BLE001
563
+ pass
564
+ return None
565
+
566
+
567
+ def _php_attributes(node: Node) -> list[str]:
568
+ """Extract PHP 8 attribute lists (#[...]) verbatim from a declaration node.
569
+
570
+ PHP attributes appear as attribute_list children before visibility_modifier.
571
+ Each attribute_list (which may contain one or more attribute_group entries)
572
+ is returned verbatim (e.g. "#[Route('/users')]").
573
+ """
574
+ result: list[str] = []
575
+ try:
576
+ for child in node.children:
577
+ if child.type == "attribute_list":
578
+ text = _text(child).strip()
579
+ if text:
580
+ result.append(text)
581
+ except Exception: # noqa: BLE001
582
+ pass
583
+ return result
584
+
585
+
586
+ def _php_signature(node: Node) -> str | None:
587
+ """Build a one-line PHP signature from a declaration node.
588
+
589
+ Covers: class_declaration, interface_declaration, trait_declaration,
590
+ enum_declaration, function_definition, method_declaration.
591
+
592
+ Strategy: collect text from all children BEFORE the body (compound_statement,
593
+ declaration_list, enum_declaration_list), join, normalize. Skip attribute_list
594
+ children (those are decorators, not signature header).
595
+ """
596
+ try:
597
+ body_stop_types = frozenset(
598
+ {
599
+ "compound_statement",
600
+ "declaration_list",
601
+ "enum_declaration_list",
602
+ }
603
+ )
604
+ skip_types = frozenset({"attribute_list"})
605
+ parts: list[str] = []
606
+ for child in node.children:
607
+ if child.type in body_stop_types:
608
+ break
609
+ if child.type in skip_types:
610
+ continue # skip attributes — those are decorators
611
+ text = _text(child).strip()
612
+ if text:
613
+ parts.append(text)
614
+ if not parts:
615
+ return None
616
+ return _normalize_to_one_line(" ".join(parts))
617
+ except Exception: # noqa: BLE001
618
+ pass
619
+ return None
620
+
621
+
622
+ def _extract_php(
623
+ node: Node,
624
+ qualified_name: str | None,
625
+ max_sig_len: int,
626
+ ) -> NodeFields:
627
+ """Extract all five enrichment fields for a PHP symbol node.
628
+
629
+ signature : declaration header up to (but not including) the body, one line.
630
+ decorators : PHP 8 attributes (#[Route(...)]) verbatim from attribute_list children.
631
+ is_exported : True when 'public' modifier is present.
632
+ visibility : 'public' | 'private' | 'protected' | None (no modifier).
633
+ qualified_name : passed through from the caller.
634
+ """
635
+ try:
636
+ vis = _php_visibility(node)
637
+ is_exported = vis == "public"
638
+ attributes = _php_attributes(node)
639
+
640
+ sig = _php_signature(node)
641
+ if sig is not None:
642
+ sig = _truncate(sig, max_sig_len)
643
+
644
+ return NodeFields(
645
+ signature=sig,
646
+ decorators=attributes,
647
+ is_exported=is_exported,
648
+ visibility=vis,
649
+ qualified_name=qualified_name,
650
+ )
651
+ except Exception: # noqa: BLE001
652
+ return _safe_defaults(qualified_name)
653
+
654
+
655
+ # ── Swift ─────────────────────────────────────────────────────────────────────
656
+
657
+
658
+ def _swift_ext_modifiers(node: Node) -> Node | None:
659
+ """Find the 'modifiers' child of a Swift declaration node, or None."""
660
+ for child in node.children:
661
+ if child.type == "modifiers":
662
+ return child
663
+ return None
664
+
665
+
666
+ def _swift_ext_visibility(node: Node) -> tuple[str, bool]:
667
+ """Extract (visibility_str, is_exported) from a Swift declaration modifiers.
668
+
669
+ public / open → ('public', True)
670
+ private / fileprivate → ('private', False)
671
+ internal (explicit or absent) → ('internal', False)
672
+ """
673
+ mods = _swift_ext_modifiers(node)
674
+ if mods is None:
675
+ return ("internal", False)
676
+ try:
677
+ for child in mods.children:
678
+ if child.type == "visibility_modifier":
679
+ vis_text = _text(child).strip()
680
+ if vis_text in ("public", "open"):
681
+ return ("public", True)
682
+ if vis_text in ("private", "fileprivate"):
683
+ return ("private", False)
684
+ return ("internal", False)
685
+ except Exception: # noqa: BLE001
686
+ pass
687
+ return ("internal", False)
688
+
689
+
690
+ def _swift_ext_attributes(node: Node) -> list[str]:
691
+ """Extract Swift @attribute decorator texts from a declaration node's modifiers.
692
+
693
+ Returns each attribute verbatim (e.g. '@objc', '@available(iOS 13.0, *)').
694
+ """
695
+ result: list[str] = []
696
+ mods = _swift_ext_modifiers(node)
697
+ if mods is None:
698
+ return result
699
+ try:
700
+ for child in mods.children:
701
+ if child.type == "attribute":
702
+ text = _text(child).strip()
703
+ if text:
704
+ result.append(text)
705
+ except Exception: # noqa: BLE001
706
+ pass
707
+ return result
708
+
709
+
710
+ def _swift_ext_signature(node: Node) -> str | None:
711
+ """Build a one-line Swift signature from a declaration node.
712
+
713
+ Collects children before the body (function_body / class_body / enum_class_body /
714
+ protocol_body). Includes visibility_modifier from modifiers but skips @attributes.
715
+ """
716
+ try:
717
+ body_stop_types = frozenset({
718
+ "function_body",
719
+ "class_body",
720
+ "enum_class_body",
721
+ "protocol_body",
722
+ })
723
+ parts: list[str] = []
724
+ for child in node.children:
725
+ if child.type in body_stop_types:
726
+ break
727
+ if child.type == "modifiers":
728
+ # Include only visibility_modifier in signature text.
729
+ for mc in child.children:
730
+ if mc.type == "visibility_modifier":
731
+ vis = _text(mc).strip()
732
+ if vis:
733
+ parts.append(vis)
734
+ continue
735
+ text = _text(child).strip()
736
+ if text:
737
+ parts.append(text)
738
+ if not parts:
739
+ return None
740
+ return " ".join(" ".join(parts).split())
741
+ except Exception: # noqa: BLE001
742
+ pass
743
+ return None
744
+
745
+
746
+ def _extract_swift(
747
+ node: Node,
748
+ qualified_name: str | None,
749
+ max_sig_len: int,
750
+ ) -> NodeFields:
751
+ """Extract all five enrichment fields for a Swift symbol node.
752
+
753
+ signature : declaration header up to (but not including) the body, one line.
754
+ decorators : Swift @attributes verbatim (@objc, @available(...), etc.).
755
+ is_exported : True when 'public' or 'open' modifier is present.
756
+ visibility : 'public' (public/open) | 'private' (private/fileprivate) |
757
+ 'internal' (explicit internal or absent — Swift default).
758
+ qualified_name : passed through from the caller.
759
+
760
+ WHY internal for absent modifier: Swift's default access level is 'internal'
761
+ (visible within the module but not exported). Only public/open cross the module
762
+ boundary (is_exported=True). This mirrors the PRD spec decision.
763
+ """
764
+ try:
765
+ vis, is_exported = _swift_ext_visibility(node)
766
+ attributes = _swift_ext_attributes(node)
767
+
768
+ sig = _swift_ext_signature(node)
769
+ if sig is not None:
770
+ sig = _truncate(sig, max_sig_len)
771
+
772
+ return NodeFields(
773
+ signature=sig,
774
+ decorators=attributes,
775
+ is_exported=is_exported,
776
+ visibility=vis,
777
+ qualified_name=qualified_name,
778
+ )
779
+ except Exception: # noqa: BLE001
780
+ return _safe_defaults(qualified_name)