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.
- Agent/__init__.py +0 -0
- Agent/agent.py +891 -0
- Agent/chat_history_db.py +1500 -0
- Agent/command.py +49 -0
- Agent/config.py +46 -0
- Agent/effort_levels.py +33 -0
- Agent/git_manager.py +727 -0
- Agent/tools.py +35 -0
- Commands/__init__.py +18 -0
- Commands/effort.py +42 -0
- Commands/global_todo.py +23 -0
- Commands/help.py +22 -0
- Commands/reasoning.py +24 -0
- Commands/redo.py +11 -0
- Commands/reindex.py +27 -0
- Commands/shell.py +28 -0
- Commands/stream.py +24 -0
- Commands/undo.py +13 -0
- Commands/unlimited_effort.py +8 -0
- Commands/window_size.py +29 -0
- RAG/__init__.py +0 -0
- RAG/document.py +119 -0
- RAG/find.py +408 -0
- RAG/graph.py +231 -0
- Tools/GetFileCodeStructure.py +43 -0
- Tools/GetSymbolSourceCode.py +27 -0
- Tools/__init__.py +39 -0
- Tools/ask_user.py +102 -0
- Tools/dispatch_subagent.py +215 -0
- Tools/document.py +35 -0
- Tools/edit_symbol.py +250 -0
- Tools/fuzzy_search.py +119 -0
- Tools/list_dir.py +51 -0
- Tools/read.py +49 -0
- Tools/read_image.py +75 -0
- Tools/remove.py +75 -0
- Tools/replace.py +305 -0
- Tools/search.py +41 -0
- Tools/shell.py +149 -0
- Tools/shell_kill.py +87 -0
- Tools/temp_background_service.py +113 -0
- Tools/todo_list.py +481 -0
- Tools/utils.py +116 -0
- Tools/view_changes.py +179 -0
- Tools/walk_call_tree.py +30 -0
- Tools/web_fetch.py +175 -0
- Tools/web_search.py +69 -0
- Tools/write.py +48 -0
- cli.py +111 -0
- config/__init__.py +0 -0
- config/coder_system_prompt.md +119 -0
- config/roles.json +43 -0
- config/tools.json +709 -0
- indexing/__init__.py +0 -0
- indexing/cli.py +128 -0
- indexing/code_index_sdk.py +832 -0
- indexing/code_indexer.py +1763 -0
- indexing/db_schema.py +396 -0
- indexing/export_to_json.py +346 -0
- indexing/extractors.py +189 -0
- indexing/file_utils.py +97 -0
- indexing/frontend/__init__.py +0 -0
- indexing/frontend/css_extractor.py +195 -0
- indexing/frontend/css_parser.py +387 -0
- indexing/frontend/css_selector_utils.py +226 -0
- indexing/frontend/edit_safety.py +573 -0
- indexing/frontend/graph.py +838 -0
- indexing/frontend/html_extractor.py +496 -0
- indexing/frontend/html_parser.py +314 -0
- indexing/frontend/jsx_extractor.py +1204 -0
- indexing/frontend/location_lookup.py +247 -0
- indexing/frontend/resolver.py +485 -0
- indexing/frontend/runtime_resolver.py +862 -0
- indexing/frontend/semantic_output.py +705 -0
- indexing/frontend/source_location.py +69 -0
- indexing/frontend_config.py +72 -0
- indexing/frontend_models.py +347 -0
- indexing/language_config.py +360 -0
- indexing/models.py +284 -0
- indexing/node_utils.py +1112 -0
- indexing/parse_worker.py +1082 -0
- indexing/queries.py +1542 -0
- indexing/sdk_examples.py +426 -0
- interactive.py +248 -0
- raggie.py +673 -0
- raggiecode-0.2.1.dist-info/METADATA +944 -0
- raggiecode-0.2.1.dist-info/RECORD +93 -0
- raggiecode-0.2.1.dist-info/WHEEL +5 -0
- raggiecode-0.2.1.dist-info/entry_points.txt +2 -0
- raggiecode-0.2.1.dist-info/top_level.txt +10 -0
- skills/__init__.py +3 -0
- skills/manager.py +114 -0
- 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
|
+
}
|