raggiecode 0.2.1__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 (93) hide show
  1. Agent/__init__.py +0 -0
  2. Agent/agent.py +891 -0
  3. Agent/chat_history_db.py +1500 -0
  4. Agent/command.py +49 -0
  5. Agent/config.py +46 -0
  6. Agent/effort_levels.py +33 -0
  7. Agent/git_manager.py +727 -0
  8. Agent/tools.py +35 -0
  9. Commands/__init__.py +18 -0
  10. Commands/effort.py +42 -0
  11. Commands/global_todo.py +23 -0
  12. Commands/help.py +22 -0
  13. Commands/reasoning.py +24 -0
  14. Commands/redo.py +11 -0
  15. Commands/reindex.py +27 -0
  16. Commands/shell.py +28 -0
  17. Commands/stream.py +24 -0
  18. Commands/undo.py +13 -0
  19. Commands/unlimited_effort.py +8 -0
  20. Commands/window_size.py +29 -0
  21. RAG/__init__.py +0 -0
  22. RAG/document.py +119 -0
  23. RAG/find.py +408 -0
  24. RAG/graph.py +231 -0
  25. Tools/GetFileCodeStructure.py +43 -0
  26. Tools/GetSymbolSourceCode.py +27 -0
  27. Tools/__init__.py +39 -0
  28. Tools/ask_user.py +102 -0
  29. Tools/dispatch_subagent.py +215 -0
  30. Tools/document.py +35 -0
  31. Tools/edit_symbol.py +250 -0
  32. Tools/fuzzy_search.py +119 -0
  33. Tools/list_dir.py +51 -0
  34. Tools/read.py +49 -0
  35. Tools/read_image.py +75 -0
  36. Tools/remove.py +75 -0
  37. Tools/replace.py +305 -0
  38. Tools/search.py +41 -0
  39. Tools/shell.py +149 -0
  40. Tools/shell_kill.py +87 -0
  41. Tools/temp_background_service.py +113 -0
  42. Tools/todo_list.py +481 -0
  43. Tools/utils.py +116 -0
  44. Tools/view_changes.py +179 -0
  45. Tools/walk_call_tree.py +30 -0
  46. Tools/web_fetch.py +175 -0
  47. Tools/web_search.py +69 -0
  48. Tools/write.py +48 -0
  49. cli.py +111 -0
  50. config/__init__.py +0 -0
  51. config/coder_system_prompt.md +119 -0
  52. config/roles.json +43 -0
  53. config/tools.json +709 -0
  54. indexing/__init__.py +0 -0
  55. indexing/cli.py +128 -0
  56. indexing/code_index_sdk.py +832 -0
  57. indexing/code_indexer.py +1763 -0
  58. indexing/db_schema.py +396 -0
  59. indexing/export_to_json.py +346 -0
  60. indexing/extractors.py +189 -0
  61. indexing/file_utils.py +97 -0
  62. indexing/frontend/__init__.py +0 -0
  63. indexing/frontend/css_extractor.py +195 -0
  64. indexing/frontend/css_parser.py +387 -0
  65. indexing/frontend/css_selector_utils.py +226 -0
  66. indexing/frontend/edit_safety.py +573 -0
  67. indexing/frontend/graph.py +838 -0
  68. indexing/frontend/html_extractor.py +496 -0
  69. indexing/frontend/html_parser.py +314 -0
  70. indexing/frontend/jsx_extractor.py +1204 -0
  71. indexing/frontend/location_lookup.py +247 -0
  72. indexing/frontend/resolver.py +485 -0
  73. indexing/frontend/runtime_resolver.py +862 -0
  74. indexing/frontend/semantic_output.py +705 -0
  75. indexing/frontend/source_location.py +69 -0
  76. indexing/frontend_config.py +72 -0
  77. indexing/frontend_models.py +347 -0
  78. indexing/language_config.py +360 -0
  79. indexing/models.py +284 -0
  80. indexing/node_utils.py +1112 -0
  81. indexing/parse_worker.py +1082 -0
  82. indexing/queries.py +1542 -0
  83. indexing/sdk_examples.py +426 -0
  84. interactive.py +248 -0
  85. raggie.py +673 -0
  86. raggiecode-0.2.1.dist-info/METADATA +944 -0
  87. raggiecode-0.2.1.dist-info/RECORD +93 -0
  88. raggiecode-0.2.1.dist-info/WHEEL +5 -0
  89. raggiecode-0.2.1.dist-info/entry_points.txt +2 -0
  90. raggiecode-0.2.1.dist-info/top_level.txt +10 -0
  91. skills/__init__.py +3 -0
  92. skills/manager.py +114 -0
  93. skills/tool.py +121 -0
@@ -0,0 +1,838 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Frontend graph traversal (Phase 8).
4
+
5
+ Provides depth-limited, cycle-safe traversal of frontend semantic
6
+ relationships stored in the index database:
7
+
8
+ - traverse_render_graph: component → rendered children / parents
9
+ - traverse_markup_tree: element → child / parent elements
10
+ - traverse_style_graph: selector ↔ matching elements
11
+ - traverse_event_graph: element → event handlers → handler symbols
12
+ - traverse_binding_graph: element → bindings → referenced expressions
13
+ - traverse_component_to_code: component → implementation function/class → call tree
14
+ - traverse_full_frontend: combined traversal of all the above
15
+ """
16
+
17
+ import json
18
+ import sqlite3
19
+ from collections import deque
20
+ from typing import Dict, List, Optional, Any, Set
21
+
22
+
23
+ def _row_to_dict(row: sqlite3.Row) -> Dict[str, Any]:
24
+ return {k: row[k] for k in row.keys()}
25
+
26
+
27
+ def _get_component(conn, component_id: int) -> Optional[Dict[str, Any]]:
28
+ row = conn.execute(
29
+ "SELECT c.*, f.path as file_path FROM frontend_components c "
30
+ "JOIN files f ON c.file_id = f.id WHERE c.id = ?",
31
+ (component_id,)
32
+ ).fetchone()
33
+ return _row_to_dict(row) if row else None
34
+
35
+
36
+ def _get_component_by_name(conn, name: str) -> Optional[Dict[str, Any]]:
37
+ row = conn.execute(
38
+ "SELECT c.*, f.path as file_path FROM frontend_components c "
39
+ "JOIN files f ON c.file_id = f.id WHERE c.name = ? LIMIT 1",
40
+ (name,)
41
+ ).fetchone()
42
+ return _row_to_dict(row) if row else None
43
+
44
+
45
+ def _get_element(conn, element_id: int) -> Optional[Dict[str, Any]]:
46
+ row = conn.execute(
47
+ "SELECT m.*, f.path as file_path FROM markup_elements m "
48
+ "JOIN files f ON m.file_id = f.id WHERE m.id = ?",
49
+ (element_id,)
50
+ ).fetchone()
51
+ return _row_to_dict(row) if row else None
52
+
53
+
54
+ def _get_selector(conn, selector_id: int) -> Optional[Dict[str, Any]]:
55
+ row = conn.execute(
56
+ "SELECT s.*, f.path as file_path FROM style_selectors s "
57
+ "JOIN files f ON s.file_id = f.id WHERE s.id = ?",
58
+ (selector_id,)
59
+ ).fetchone()
60
+ return _row_to_dict(row) if row else None
61
+
62
+
63
+ # ──────────────────────────────────────────────────────────────
64
+ # 1. Render graph traversal
65
+ # ──────────────────────────────────────────────────────────────
66
+
67
+ def traverse_render_graph(
68
+ conn: sqlite3.Connection,
69
+ component_id: int,
70
+ direction: str = "children",
71
+ max_depth: int = 10,
72
+ ) -> Dict[str, Any]:
73
+ """Traverse the component render graph.
74
+
75
+ Args:
76
+ conn: SQLite connection.
77
+ component_id: ID of the starting frontend_components row.
78
+ direction: "children" (component → rendered children) or
79
+ "parents" (component → rendering parents).
80
+ max_depth: Maximum traversal depth.
81
+
82
+ Returns:
83
+ Dict with component info and a "nodes" list (BFS order, depth-limited,
84
+ cycle-detected). Each node has: id, name, render_type, depth, cycle,
85
+ file_path, children.
86
+ """
87
+ comp = _get_component(conn, component_id)
88
+ if comp is None:
89
+ return {"error": f"Component not found: id={component_id}"}
90
+
91
+ root = {
92
+ "id": component_id,
93
+ "name": comp["name"],
94
+ "render_type": "root",
95
+ "source_range": json.loads(comp["source_range"]) if comp.get("source_range") else None,
96
+ "depth": 0,
97
+ "cycle": False,
98
+ "file_path": comp["file_path"],
99
+ "children": [],
100
+ }
101
+
102
+ nodes: Dict[int, Dict[str, Any]] = {component_id: root}
103
+ queue: deque = deque()
104
+ queue.append((component_id, root, 0, frozenset([component_id])))
105
+
106
+ while queue:
107
+ comp_id, parent_node, depth, ancestors = queue.popleft()
108
+ if depth >= max_depth:
109
+ continue
110
+
111
+ if direction == "children":
112
+ rows = conn.execute(
113
+ """SELECT rr.child_component_id, rr.child_component_name,
114
+ rr.child_element_id, rr.render_type, rr.controlling_expr,
115
+ c.name as resolved_name, c.source_range as child_source_range, f.path as file_path
116
+ FROM render_relationships rr
117
+ LEFT JOIN frontend_components c ON rr.child_component_id = c.id
118
+ LEFT JOIN files f ON c.file_id = f.id
119
+ WHERE rr.parent_component_id = ?
120
+ ORDER BY rr.id""",
121
+ (comp_id,)
122
+ ).fetchall()
123
+ else: # parents
124
+ rows = conn.execute(
125
+ """SELECT rr.parent_component_id as child_component_id,
126
+ c.name as child_component_name,
127
+ NULL as child_element_id,
128
+ rr.render_type, rr.controlling_expr,
129
+ c.name as resolved_name, c.source_range as child_source_range, f.path as file_path
130
+ FROM render_relationships rr
131
+ JOIN frontend_components c ON rr.parent_component_id = c.id
132
+ JOIN files f ON c.file_id = f.id
133
+ WHERE rr.child_component_id = ?
134
+ ORDER BY rr.id""",
135
+ (comp_id,)
136
+ ).fetchall()
137
+
138
+ for row in rows:
139
+ child_id = row["child_component_id"]
140
+ child_name = row["resolved_name"] or row["child_component_name"] or "unknown"
141
+ render_type = row["render_type"]
142
+
143
+ if child_id is None:
144
+ # Unresolved reference — add as a leaf
145
+ parent_node["children"].append({
146
+ "id": None,
147
+ "name": child_name,
148
+ "render_type": render_type,
149
+ "controlling_expr": row["controlling_expr"],
150
+ "source_range": None,
151
+ "depth": depth + 1,
152
+ "cycle": False,
153
+ "file_path": None,
154
+ "children": [],
155
+ "unresolved": True,
156
+ })
157
+ continue
158
+
159
+ is_cycle = child_id in ancestors
160
+ if child_id not in nodes:
161
+ node = {
162
+ "id": child_id,
163
+ "name": child_name,
164
+ "render_type": render_type,
165
+ "controlling_expr": row["controlling_expr"],
166
+ "source_range": json.loads(row["child_source_range"]) if row["child_source_range"] else None,
167
+ "depth": depth + 1,
168
+ "cycle": is_cycle,
169
+ "file_path": row["file_path"],
170
+ "children": [],
171
+ }
172
+ nodes[child_id] = node
173
+ parent_node["children"].append(node)
174
+ if not is_cycle:
175
+ queue.append((child_id, node, depth + 1, ancestors | {child_id}))
176
+ else:
177
+ # Already visited — add as reference
178
+ parent_node["children"].append({
179
+ "id": child_id,
180
+ "name": child_name,
181
+ "render_type": render_type,
182
+ "controlling_expr": row["controlling_expr"],
183
+ "source_range": json.loads(row["child_source_range"]) if row["child_source_range"] else None,
184
+ "depth": depth + 1,
185
+ "cycle": is_cycle,
186
+ "file_path": row["file_path"],
187
+ "children": [],
188
+ "already_visited": True,
189
+ })
190
+
191
+ return root
192
+
193
+
194
+ # ──────────────────────────────────────────────────────────────
195
+ # 2. Markup tree traversal
196
+ # ──────────────────────────────────────────────────────────────
197
+
198
+ def traverse_markup_tree(
199
+ conn: sqlite3.Connection,
200
+ element_id: int,
201
+ direction: str = "children",
202
+ max_depth: int = 10,
203
+ ) -> Dict[str, Any]:
204
+ """Traverse the markup element tree via parent_element_id.
205
+
206
+ Args:
207
+ conn: SQLite connection.
208
+ element_id: ID of the starting markup_elements row.
209
+ direction: "children" or "parents".
210
+ max_depth: Maximum traversal depth.
211
+
212
+ Returns:
213
+ Dict with element info and nested "children" or "parents" list.
214
+ """
215
+ elem = _get_element(conn, element_id)
216
+ if elem is None:
217
+ return {"error": f"Element not found: id={element_id}"}
218
+
219
+ root = {
220
+ "id": element_id,
221
+ "tag_name": elem["tag_name"],
222
+ "element_type": elem["element_type"],
223
+ "static_classes": json.loads(elem["static_classes"]) if elem.get("static_classes") else [],
224
+ "element_id_attr": elem.get("element_id_attr"),
225
+ "is_conditional": bool(elem["is_conditional"]) if elem.get("is_conditional") else False,
226
+ "is_repeated": bool(elem["is_repeated"]) if elem.get("is_repeated") else False,
227
+ "source_range": json.loads(elem["source_range"]) if elem.get("source_range") else None,
228
+ "depth": 0,
229
+ "cycle": False,
230
+ "file_path": elem["file_path"],
231
+ "children": [],
232
+ }
233
+
234
+ nodes: Dict[int, Dict[str, Any]] = {element_id: root}
235
+ queue: deque = deque()
236
+ queue.append((element_id, root, 0, frozenset([element_id])))
237
+
238
+ while queue:
239
+ eid, parent_node, depth, ancestors = queue.popleft()
240
+ if depth >= max_depth:
241
+ continue
242
+
243
+ if direction == "children":
244
+ rows = conn.execute(
245
+ """SELECT m.id, m.tag_name, m.element_type, m.static_classes,
246
+ m.element_id_attr, m.is_conditional, m.is_repeated,
247
+ f.path as file_path
248
+ FROM markup_elements m
249
+ JOIN files f ON m.file_id = f.id
250
+ WHERE m.parent_element_id = ?
251
+ ORDER BY m.id""",
252
+ (eid,)
253
+ ).fetchall()
254
+ else: # parents
255
+ rows = conn.execute(
256
+ """SELECT m.id, m.tag_name, m.element_type, m.static_classes,
257
+ m.element_id_attr, m.is_conditional, m.is_repeated,
258
+ f.path as file_path
259
+ FROM markup_elements m
260
+ JOIN files f ON m.file_id = f.id
261
+ WHERE m.id = (SELECT parent_element_id FROM markup_elements WHERE id = ?)
262
+ LIMIT 1""",
263
+ (eid,)
264
+ ).fetchall()
265
+
266
+ for row in rows:
267
+ child_eid = row["id"]
268
+ is_cycle = child_eid in ancestors
269
+
270
+ if child_eid not in nodes:
271
+ node = {
272
+ "id": child_eid,
273
+ "tag_name": row["tag_name"],
274
+ "element_type": row["element_type"],
275
+ "static_classes": json.loads(row["static_classes"]) if row["static_classes"] else [],
276
+ "element_id_attr": row["element_id_attr"],
277
+ "is_conditional": bool(row["is_conditional"]) if row["is_conditional"] else False,
278
+ "is_repeated": bool(row["is_repeated"]) if row["is_repeated"] else False,
279
+ "depth": depth + 1,
280
+ "cycle": is_cycle,
281
+ "file_path": row["file_path"],
282
+ "children": [],
283
+ }
284
+ nodes[child_eid] = node
285
+ parent_node["children"].append(node)
286
+ if not is_cycle:
287
+ queue.append((child_eid, node, depth + 1, ancestors | {child_eid}))
288
+ else:
289
+ parent_node["children"].append({
290
+ "id": child_eid,
291
+ "tag_name": row["tag_name"],
292
+ "element_type": row["element_type"],
293
+ "static_classes": json.loads(row["static_classes"]) if row["static_classes"] else [],
294
+ "element_id_attr": row["element_id_attr"],
295
+ "is_conditional": bool(row["is_conditional"]) if row["is_conditional"] else False,
296
+ "is_repeated": bool(row["is_repeated"]) if row["is_repeated"] else False,
297
+ "depth": depth + 1,
298
+ "cycle": is_cycle,
299
+ "file_path": row["file_path"],
300
+ "children": [],
301
+ "already_visited": True,
302
+ })
303
+
304
+ return root
305
+
306
+
307
+ # ──────────────────────────────────────────────────────────────
308
+ # 3. Style graph traversal
309
+ # ──────────────────────────────────────────────────────────────
310
+
311
+ def traverse_style_graph(
312
+ conn: sqlite3.Connection,
313
+ selector_id: int,
314
+ direction: str = "using_elements",
315
+ max_depth: int = 10,
316
+ ) -> Dict[str, Any]:
317
+ """Traverse the style selector ↔ element graph.
318
+
319
+ Args:
320
+ conn: SQLite connection.
321
+ selector_id: ID of the starting style_selectors row.
322
+ direction: "using_elements" (selector → matching elements) or
323
+ "to_definition" (element → candidate selectors).
324
+ max_depth: Maximum traversal depth (usually 1-2 levels).
325
+
326
+ Returns:
327
+ Dict with selector/element info and matched entities.
328
+ """
329
+ if direction == "using_elements":
330
+ sel = _get_selector(conn, selector_id)
331
+ if sel is None:
332
+ return {"error": f"Selector not found: id={selector_id}"}
333
+
334
+ matches = conn.execute(
335
+ """SELECT m.id, m.tag_name, m.element_type, m.static_classes,
336
+ m.element_id_attr, m.source_range, f.path as file_path,
337
+ sm.match_type, sm.confidence
338
+ FROM style_selector_matches sm
339
+ JOIN markup_elements m ON sm.element_id = m.id
340
+ JOIN files f ON m.file_id = f.id
341
+ WHERE sm.selector_id = ?
342
+ ORDER BY m.id""",
343
+ (selector_id,)
344
+ ).fetchall()
345
+
346
+ elements = []
347
+ for row in matches:
348
+ elements.append({
349
+ "id": row["id"],
350
+ "tag_name": row["tag_name"],
351
+ "element_type": row["element_type"],
352
+ "static_classes": json.loads(row["static_classes"]) if row["static_classes"] else [],
353
+ "element_id_attr": row["element_id_attr"],
354
+ "source_range": json.loads(row["source_range"]) if row["source_range"] else None,
355
+ "file_path": row["file_path"],
356
+ "match_type": row["match_type"],
357
+ "confidence": row["confidence"],
358
+ })
359
+
360
+ return {
361
+ "selector_id": selector_id,
362
+ "selector_text": sel["selector_text"],
363
+ "selector_type": sel["selector_type"],
364
+ "file_path": sel["file_path"],
365
+ "using_elements": elements,
366
+ }
367
+
368
+ else: # to_definition — given an element, find candidate selectors
369
+ # selector_id is actually element_id in this direction
370
+ element_id = selector_id
371
+ elem = _get_element(conn, element_id)
372
+ if elem is None:
373
+ return {"error": f"Element not found: id={element_id}"}
374
+
375
+ matches = conn.execute(
376
+ """SELECT s.id, s.selector_text, s.selector_type, s.normalized_selector,
377
+ s.is_scoped, f.path as file_path,
378
+ sm.match_type, sm.confidence
379
+ FROM style_selector_matches sm
380
+ JOIN style_selectors s ON sm.selector_id = s.id
381
+ JOIN files f ON s.file_id = f.id
382
+ WHERE sm.element_id = ?
383
+ ORDER BY s.id""",
384
+ (element_id,)
385
+ ).fetchall()
386
+
387
+ selectors = []
388
+ for row in matches:
389
+ selectors.append({
390
+ "id": row["id"],
391
+ "selector_text": row["selector_text"],
392
+ "selector_type": row["selector_type"],
393
+ "normalized_selector": row["normalized_selector"],
394
+ "is_scoped": bool(row["is_scoped"]),
395
+ "file_path": row["file_path"],
396
+ "match_type": row["match_type"],
397
+ "confidence": row["confidence"],
398
+ })
399
+
400
+ return {
401
+ "element_id": element_id,
402
+ "tag_name": elem["tag_name"],
403
+ "static_classes": json.loads(elem["static_classes"]) if elem["static_classes"] else [],
404
+ "source_range": json.loads(elem["source_range"]) if elem.get("source_range") else None,
405
+ "file_path": elem["file_path"],
406
+ "candidate_selectors": selectors,
407
+ }
408
+
409
+
410
+ # ──────────────────────────────────────────────────────────────
411
+ # 4. Event graph traversal
412
+ # ──────────────────────────────────────────────────────────────
413
+
414
+ def traverse_event_graph(
415
+ conn: sqlite3.Connection,
416
+ element_id: int,
417
+ ) -> Dict[str, Any]:
418
+ """Traverse element → event handlers → handler symbols.
419
+
420
+ Args:
421
+ conn: SQLite connection.
422
+ element_id: ID of the markup_elements row.
423
+
424
+ Returns:
425
+ Dict with element info and list of events with handler symbol details.
426
+ """
427
+ elem = _get_element(conn, element_id)
428
+ if elem is None:
429
+ return {"error": f"Element not found: id={element_id}"}
430
+
431
+ events = conn.execute(
432
+ """SELECT e.id, e.event_name, e.handler_type, e.handler_expression,
433
+ e.resolution_status, e.handler_symbol_id, e.source_range,
434
+ fn.name as handler_symbol_name,
435
+ fn.type as handler_symbol_type,
436
+ f.path as handler_symbol_file
437
+ FROM frontend_events e
438
+ LEFT JOIN functions fn ON e.handler_symbol_id = fn.id
439
+ LEFT JOIN files f ON fn.file_id = f.id
440
+ WHERE e.element_id = ?
441
+ ORDER BY e.id""",
442
+ (element_id,)
443
+ ).fetchall()
444
+
445
+ event_list = []
446
+ for row in events:
447
+ ev = {
448
+ "id": row["id"],
449
+ "event_name": row["event_name"],
450
+ "handler_type": row["handler_type"],
451
+ "handler_expression": row["handler_expression"],
452
+ "resolution_status": row["resolution_status"],
453
+ "source_range": json.loads(row["source_range"]) if row["source_range"] else None,
454
+ }
455
+ if row["handler_symbol_id"]:
456
+ ev["handler_symbol"] = {
457
+ "id": row["handler_symbol_id"],
458
+ "name": row["handler_symbol_name"],
459
+ "type": row["handler_symbol_type"],
460
+ "file_path": row["handler_symbol_file"],
461
+ }
462
+ else:
463
+ ev["handler_symbol"] = None
464
+ event_list.append(ev)
465
+
466
+ return {
467
+ "element_id": element_id,
468
+ "tag_name": elem["tag_name"],
469
+ "source_range": json.loads(elem["source_range"]) if elem.get("source_range") else None,
470
+ "file_path": elem["file_path"],
471
+ "events": event_list,
472
+ }
473
+
474
+
475
+ # ──────────────────────────────────────────────────────────────
476
+ # 5. Binding graph traversal
477
+ # ──────────────────────────────────────────────────────────────
478
+
479
+ def traverse_binding_graph(
480
+ conn: sqlite3.Connection,
481
+ element_id: int,
482
+ ) -> Dict[str, Any]:
483
+ """Traverse element → bindings → referenced state/expressions.
484
+
485
+ Args:
486
+ conn: SQLite connection.
487
+ element_id: ID of the markup_elements row.
488
+
489
+ Returns:
490
+ Dict with element info and list of bindings.
491
+ """
492
+ elem = _get_element(conn, element_id)
493
+ if elem is None:
494
+ return {"error": f"Element not found: id={element_id}"}
495
+
496
+ bindings = conn.execute(
497
+ """SELECT b.id, b.binding_type, b.binding_name, b.binding_expression,
498
+ b.resolution_status, b.source_range
499
+ FROM frontend_bindings b
500
+ WHERE b.element_id = ?
501
+ ORDER BY b.id""",
502
+ (element_id,)
503
+ ).fetchall()
504
+
505
+ binding_list = []
506
+ for row in bindings:
507
+ binding_list.append({
508
+ "id": row["id"],
509
+ "binding_type": row["binding_type"],
510
+ "binding_name": row["binding_name"],
511
+ "binding_expression": row["binding_expression"],
512
+ "resolution_status": row["resolution_status"],
513
+ "source_range": json.loads(row["source_range"]) if row["source_range"] else None,
514
+ })
515
+
516
+ return {
517
+ "element_id": element_id,
518
+ "tag_name": elem["tag_name"],
519
+ "source_range": json.loads(elem["source_range"]) if elem.get("source_range") else None,
520
+ "file_path": elem["file_path"],
521
+ "bindings": binding_list,
522
+ }
523
+
524
+
525
+ # ──────────────────────────────────────────────────────────────
526
+ # 6. Component-to-code traversal
527
+ # ──────────────────────────────────────────────────────────────
528
+
529
+ def traverse_component_to_code(
530
+ conn: sqlite3.Connection,
531
+ component_id: int,
532
+ max_depth: int = 5,
533
+ ) -> Dict[str, Any]:
534
+ """Link a frontend component to its executable implementation and call tree.
535
+
536
+ Args:
537
+ conn: SQLite connection.
538
+ component_id: ID of the frontend_components row.
539
+ max_depth: Maximum depth for the call tree traversal.
540
+
541
+ Returns:
542
+ Dict with component info, implementation function/class details,
543
+ and a call tree (BFS, cycle-detected).
544
+ """
545
+ comp = _get_component(conn, component_id)
546
+ if comp is None:
547
+ return {"error": f"Component not found: id={component_id}"}
548
+
549
+ result = {
550
+ "component_id": component_id,
551
+ "name": comp["name"],
552
+ "file_path": comp["file_path"],
553
+ "impl_function_id": comp["impl_function_id"],
554
+ "impl_class_id": comp["impl_class_id"],
555
+ "implementation": None,
556
+ "call_tree": None,
557
+ }
558
+
559
+ # Resolve implementation function
560
+ impl_func_id = comp["impl_function_id"]
561
+ impl_class_id = comp["impl_class_id"]
562
+
563
+ if impl_func_id:
564
+ func_row = conn.execute(
565
+ "SELECT fn.*, f.path as file_path FROM functions fn "
566
+ "JOIN files f ON fn.file_id = f.id WHERE fn.id = ?",
567
+ (impl_func_id,)
568
+ ).fetchone()
569
+ if func_row:
570
+ result["implementation"] = {
571
+ "type": "function",
572
+ "id": func_row["id"],
573
+ "name": func_row["name"],
574
+ "kind": func_row["type"],
575
+ "file_path": func_row["file_path"],
576
+ "location": func_row["location"],
577
+ "source_range": json.loads(func_row["location"]) if func_row["location"] else None,
578
+ }
579
+
580
+ # Build call tree from this function
581
+ result["call_tree"] = _build_call_tree(conn, impl_func_id, max_depth)
582
+
583
+ elif impl_class_id:
584
+ cls_row = conn.execute(
585
+ "SELECT c.*, f.path as file_path FROM classes c "
586
+ "JOIN files f ON c.file_id = f.id WHERE c.id = ?",
587
+ (impl_class_id,)
588
+ ).fetchone()
589
+ if cls_row:
590
+ result["implementation"] = {
591
+ "type": "class",
592
+ "id": cls_row["id"],
593
+ "name": cls_row["name"],
594
+ "file_path": cls_row["file_path"],
595
+ "location": cls_row["location"],
596
+ "source_range": json.loads(cls_row["location"]) if cls_row["location"] else None,
597
+ }
598
+ # List methods of the class
599
+ methods = conn.execute(
600
+ "SELECT fn.id, fn.name, fn.type, fn.location FROM functions fn "
601
+ "WHERE fn.parent_id = ? ORDER BY fn.id",
602
+ (impl_class_id,)
603
+ ).fetchall()
604
+ result["call_tree"] = {
605
+ "type": "class",
606
+ "name": cls_row["name"],
607
+ "methods": [
608
+ {
609
+ "id": m["id"],
610
+ "name": m["name"],
611
+ "type": m["type"],
612
+ "call_tree": _build_call_tree(conn, m["id"], max_depth),
613
+ }
614
+ for m in methods
615
+ ],
616
+ }
617
+
618
+ return result
619
+
620
+
621
+ def _build_call_tree(
622
+ conn: sqlite3.Connection,
623
+ func_id: int,
624
+ max_depth: int,
625
+ ) -> Dict[str, Any]:
626
+ """Build a BFS call tree from a function, with cycle detection."""
627
+ root = {
628
+ "id": f"f:{func_id}",
629
+ "name": None,
630
+ "depth": 0,
631
+ "cycle": False,
632
+ "callees": [],
633
+ }
634
+
635
+ # Get root function name
636
+ func_row = conn.execute("SELECT name FROM functions WHERE id = ?", (func_id,)).fetchone()
637
+ if func_row:
638
+ root["name"] = func_row["name"]
639
+
640
+ nodes: Dict[int, Dict[str, Any]] = {func_id: root}
641
+ queue: deque = deque()
642
+ queue.append((func_id, root, 0, frozenset([func_id])))
643
+
644
+ while queue:
645
+ fid, parent_node, depth, ancestors = queue.popleft()
646
+ if depth >= max_depth:
647
+ continue
648
+
649
+ deps = conn.execute(
650
+ """SELECT d.name, d.target_function_id, d.target_class_id, d.dependency_type
651
+ FROM dependencies d
652
+ WHERE d.source_function_id = ?
653
+ AND d.dependency_type IN ('function_call', 'method_call', 'class_reference')
654
+ ORDER BY d.name""",
655
+ (fid,)
656
+ ).fetchall()
657
+
658
+ seen = set()
659
+ for dep in deps:
660
+ callee_id = dep["target_function_id"]
661
+ callee_name = dep["name"]
662
+ dep_type = dep["dependency_type"]
663
+
664
+ # For class_reference, use target_class_id if target_function_id is NULL
665
+ if callee_id is None and dep["target_class_id"] is not None:
666
+ # Look up the class as a node
667
+ cls_id = dep["target_class_id"]
668
+ key = (callee_name, cls_id, dep_type)
669
+ if key in seen:
670
+ continue
671
+ seen.add(key)
672
+
673
+ cls_row = conn.execute(
674
+ "SELECT c.name, f.path as file_path FROM classes c "
675
+ "JOIN files f ON c.file_id = f.id WHERE c.id = ?",
676
+ (cls_id,)
677
+ ).fetchone()
678
+ node = {
679
+ "id": f"c:{cls_id}",
680
+ "name": cls_row["name"] if cls_row else callee_name,
681
+ "depth": depth + 1,
682
+ "cycle": cls_id in ancestors,
683
+ "file_path": cls_row["file_path"] if cls_row else None,
684
+ "callees": [],
685
+ "kind": "class",
686
+ }
687
+ # Class nodes don't have callees in this traversal
688
+ parent_node["callees"].append(node)
689
+ continue
690
+
691
+ key = (callee_name, callee_id, dep_type)
692
+ if key in seen:
693
+ continue
694
+ seen.add(key)
695
+
696
+ if callee_id is None:
697
+ parent_node["callees"].append({
698
+ "id": None,
699
+ "name": callee_name,
700
+ "depth": depth + 1,
701
+ "cycle": False,
702
+ "callees": [],
703
+ "unresolved": True,
704
+ })
705
+ continue
706
+
707
+ is_cycle = callee_id in ancestors
708
+ if callee_id not in nodes:
709
+ callee_row = conn.execute(
710
+ "SELECT fn.name, f.path as file_path FROM functions fn "
711
+ "JOIN files f ON fn.file_id = f.id WHERE fn.id = ?",
712
+ (callee_id,)
713
+ ).fetchone()
714
+ node = {
715
+ "id": f"f:{callee_id}",
716
+ "name": callee_row["name"] if callee_row else callee_name,
717
+ "depth": depth + 1,
718
+ "cycle": is_cycle,
719
+ "file_path": callee_row["file_path"] if callee_row else None,
720
+ "callees": [],
721
+ }
722
+ nodes[callee_id] = node
723
+ parent_node["callees"].append(node)
724
+ if not is_cycle:
725
+ queue.append((callee_id, node, depth + 1, ancestors | {callee_id}))
726
+ else:
727
+ parent_node["callees"].append({
728
+ "id": f"f:{callee_id}",
729
+ "name": nodes[callee_id]["name"],
730
+ "depth": depth + 1,
731
+ "cycle": is_cycle,
732
+ "callees": [],
733
+ "already_visited": True,
734
+ })
735
+
736
+ return root
737
+
738
+
739
+ # ──────────────────────────────────────────────────────────────
740
+ # 7. Full frontend traversal
741
+ # ──────────────────────────────────────────────────────────────
742
+
743
+ def traverse_full_frontend(
744
+ conn: sqlite3.Connection,
745
+ component_id: int,
746
+ max_depth: int = 10,
747
+ ) -> Dict[str, Any]:
748
+ """Combined traversal: render children + markup + events + bindings + styles.
749
+
750
+ Args:
751
+ conn: SQLite connection.
752
+ component_id: ID of the starting frontend_components row.
753
+ max_depth: Maximum traversal depth for render/markup trees.
754
+
755
+ Returns:
756
+ Dict combining render graph, markup trees, events, bindings, and
757
+ style information for the component and its rendered children.
758
+ """
759
+ comp = _get_component(conn, component_id)
760
+ if comp is None:
761
+ return {"error": f"Component not found: id={component_id}"}
762
+
763
+ # 1. Render graph (children)
764
+ render_tree = traverse_render_graph(conn, component_id, "children", max_depth)
765
+
766
+ # 2. Collect all markup elements for this component
767
+ elements = conn.execute(
768
+ """SELECT m.id, m.tag_name, m.element_type, m.parent_element_id,
769
+ m.static_classes, m.element_id_attr, m.is_conditional, m.is_repeated,
770
+ m.source_range, f.path as file_path
771
+ FROM markup_elements m
772
+ JOIN files f ON m.file_id = f.id
773
+ WHERE m.component_id = ?
774
+ ORDER BY m.id""",
775
+ (component_id,)
776
+ ).fetchall()
777
+
778
+ markup_trees = []
779
+ for elem in elements:
780
+ # Only build trees for root elements (no parent)
781
+ if elem["parent_element_id"] is None:
782
+ tree = traverse_markup_tree(conn, elem["id"], "children", max_depth)
783
+ markup_trees.append(tree)
784
+
785
+ # 3. Events and bindings for each element
786
+ element_details = []
787
+ for elem in elements:
788
+ eid = elem["id"]
789
+ events = traverse_event_graph(conn, eid)
790
+ bindings = traverse_binding_graph(conn, eid)
791
+ element_details.append({
792
+ "element_id": eid,
793
+ "tag_name": elem["tag_name"],
794
+ "element_type": elem["element_type"],
795
+ "static_classes": json.loads(elem["static_classes"]) if elem["static_classes"] else [],
796
+ "source_range": json.loads(elem["source_range"]) if elem["source_range"] else None,
797
+ "events": events.get("events", []),
798
+ "bindings": bindings.get("bindings", []),
799
+ })
800
+
801
+ # 4. Style selectors for this component
802
+ selectors = conn.execute(
803
+ """SELECT s.id, s.selector_text, s.selector_type, s.is_scoped,
804
+ f.path as file_path
805
+ FROM style_selectors s
806
+ JOIN files f ON s.file_id = f.id
807
+ WHERE s.component_id = ?
808
+ ORDER BY s.id""",
809
+ (component_id,)
810
+ ).fetchall()
811
+
812
+ style_info = []
813
+ for sel in selectors:
814
+ style_info.append({
815
+ "id": sel["id"],
816
+ "selector_text": sel["selector_text"],
817
+ "selector_type": sel["selector_type"],
818
+ "is_scoped": bool(sel["is_scoped"]),
819
+ "file_path": sel["file_path"],
820
+ })
821
+
822
+ # 5. Component-to-code link
823
+ code_link = traverse_component_to_code(conn, component_id, max_depth=max_depth)
824
+
825
+ return {
826
+ "component": {
827
+ "id": component_id,
828
+ "name": comp["name"],
829
+ "source_range": json.loads(comp["source_range"]) if comp.get("source_range") else None,
830
+ "file_path": comp["file_path"],
831
+ },
832
+ "render_tree": render_tree,
833
+ "markup_trees": markup_trees,
834
+ "element_details": element_details,
835
+ "styles": style_info,
836
+ "implementation": code_link.get("implementation"),
837
+ "call_tree": code_link.get("call_tree"),
838
+ }