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,862 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Runtime element resolution (Phase 9).
4
+
5
+ Accepts metadata from a browser extension (or any source) and resolves it
6
+ to the best matching source semantic entity in the index.
7
+
8
+ Resolution strategies (tried in priority order):
9
+ 1. Exact source location (file + line + column)
10
+ 2. Exact source file + component name
11
+ 3. Component ancestry → walk render graph
12
+ 4. Element ID → markup_elements.element_id_attr
13
+ 5. Tag + class combination
14
+ 6. Attributes
15
+ 7. Text content
16
+ 8. DOM ancestry → walk markup tree
17
+ 9. Heuristic selector matching
18
+
19
+ All applicable strategies are run. Results are merged, deduplicated by
20
+ entity ID, and sorted by confidence score (highest first). Ambiguity is
21
+ never hidden — all plausible candidates are returned.
22
+ """
23
+
24
+ import json
25
+ import sqlite3
26
+ from dataclasses import dataclass, field
27
+ from typing import Dict, List, Optional, Any, Set, Tuple
28
+
29
+ from indexing.frontend.location_lookup import lookup_entity_at_location
30
+ from indexing.frontend.graph import (
31
+ traverse_render_graph,
32
+ traverse_markup_tree,
33
+ traverse_event_graph,
34
+ traverse_binding_graph,
35
+ traverse_style_graph,
36
+ )
37
+ from indexing.frontend.css_selector_utils import selector_matches_element
38
+
39
+
40
+ # ──────────────────────────────────────────────────────────────
41
+ # Data structures
42
+ # ──────────────────────────────────────────────────────────────
43
+
44
+ @dataclass
45
+ class ResolutionCandidate:
46
+ """A single candidate from runtime resolution."""
47
+
48
+ entity_type: str # "markup_element" or "component"
49
+ entity_id: int
50
+ confidence: float
51
+ strategies_matched: List[str] = field(default_factory=list)
52
+ component: Optional[Dict[str, Any]] = None
53
+ source_range: Optional[Dict[str, int]] = None
54
+ file_path: Optional[str] = None
55
+ tag_name: Optional[str] = None
56
+ static_classes: List[str] = field(default_factory=list)
57
+ element_id_attr: Optional[str] = None
58
+ events: List[Dict[str, Any]] = field(default_factory=list)
59
+ bindings: List[Dict[str, Any]] = field(default_factory=list)
60
+ styles: List[Dict[str, Any]] = field(default_factory=list)
61
+ rendering_parents: List[Dict[str, Any]] = field(default_factory=list)
62
+
63
+ def to_dict(self) -> Dict[str, Any]:
64
+ return {
65
+ "entity_type": self.entity_type,
66
+ "entity_id": self.entity_id,
67
+ "confidence": self.confidence,
68
+ "strategies_matched": self.strategies_matched,
69
+ "component": self.component,
70
+ "source_range": self.source_range,
71
+ "file_path": self.file_path,
72
+ "tag_name": self.tag_name,
73
+ "static_classes": self.static_classes,
74
+ "element_id_attr": self.element_id_attr,
75
+ "events": self.events,
76
+ "bindings": self.bindings,
77
+ "styles": self.styles,
78
+ "rendering_parents": self.rendering_parents,
79
+ }
80
+
81
+
82
+ @dataclass
83
+ class ResolutionResult:
84
+ """Result of runtime element resolution."""
85
+
86
+ candidates: List[ResolutionCandidate] = field(default_factory=list)
87
+ ambiguity_explanation: Optional[str] = None
88
+ best_confidence: float = 0.0
89
+
90
+ def to_dict(self) -> Dict[str, Any]:
91
+ return {
92
+ "candidates": [c.to_dict() for c in self.candidates],
93
+ "ambiguity_explanation": self.ambiguity_explanation,
94
+ "best_confidence": self.best_confidence,
95
+ }
96
+
97
+
98
+ # ──────────────────────────────────────────────────────────────
99
+ # Confidence scores per strategy
100
+ # ──────────────────────────────────────────────────────────────
101
+
102
+ _CONFIDENCE = {
103
+ "exact_location": 1.0,
104
+ "file_and_component": 0.9,
105
+ "component_ancestry": 0.8,
106
+ "element_id_unique": 0.85,
107
+ "element_id_duplicate": 0.5,
108
+ "tag_and_classes_full": 0.7,
109
+ "tag_and_classes_partial": 0.5,
110
+ "attributes": 0.6,
111
+ "text_content": 0.5,
112
+ "dom_ancestry_full": 0.65,
113
+ "dom_ancestry_partial": 0.4,
114
+ "selector_matching": 0.55,
115
+ }
116
+
117
+
118
+ def _combine_confidence(a: float, b: float) -> float:
119
+ """Combine two confidence scores using probabilistic OR."""
120
+ return 1.0 - (1.0 - a) * (1.0 - b)
121
+
122
+
123
+ # ──────────────────────────────────────────────────────────────
124
+ # Helpers
125
+ # ──────────────────────────────────────────────────────────────
126
+
127
+ def _parse_json(raw: Optional[str]) -> Optional[Any]:
128
+ if not raw:
129
+ return None
130
+ try:
131
+ return json.loads(raw)
132
+ except (json.JSONDecodeError, TypeError):
133
+ return None
134
+
135
+
136
+ def _element_row_to_candidate(
137
+ row: sqlite3.Row,
138
+ confidence: float,
139
+ strategy: str,
140
+ ) -> ResolutionCandidate:
141
+ """Convert a markup_elements row to a ResolutionCandidate."""
142
+ source_range = _parse_json(row["source_range"]) if "source_range" in row.keys() else None
143
+ static_classes = _parse_json(row["static_classes"]) if "static_classes" in row.keys() else []
144
+ if static_classes is None:
145
+ static_classes = []
146
+
147
+ comp = None
148
+ if row["component_id"]:
149
+ comp_row = row.keys() and None # placeholder, enriched later
150
+ comp = {"id": row["component_id"]}
151
+
152
+ return ResolutionCandidate(
153
+ entity_type="markup_element",
154
+ entity_id=row["id"],
155
+ confidence=confidence,
156
+ strategies_matched=[strategy],
157
+ component=comp,
158
+ source_range=source_range,
159
+ file_path=row["file_path"] if "file_path" in row.keys() else None,
160
+ tag_name=row["tag_name"],
161
+ static_classes=static_classes,
162
+ element_id_attr=row["element_id_attr"] if "element_id_attr" in row.keys() else None,
163
+ )
164
+
165
+
166
+ def _component_row_to_candidate(
167
+ row: sqlite3.Row,
168
+ confidence: float,
169
+ strategy: str,
170
+ ) -> ResolutionCandidate:
171
+ """Convert a frontend_components row to a ResolutionCandidate."""
172
+ source_range = _parse_json(row["source_range"]) if "source_range" in row.keys() else None
173
+
174
+ return ResolutionCandidate(
175
+ entity_type="component",
176
+ entity_id=row["id"],
177
+ confidence=confidence,
178
+ strategies_matched=[strategy],
179
+ component={"id": row["id"], "name": row["name"]},
180
+ source_range=source_range,
181
+ file_path=row["file_path"] if "file_path" in row.keys() else None,
182
+ tag_name=None,
183
+ )
184
+
185
+
186
+ # ──────────────────────────────────────────────────────────────
187
+ # Strategy 1: Exact source location
188
+ # ──────────────────────────────────────────────────────────────
189
+
190
+ def _try_exact_location(conn, meta: dict) -> List[ResolutionCandidate]:
191
+ source_file = meta.get("source_file")
192
+ source_line = meta.get("source_line")
193
+ source_column = meta.get("source_column")
194
+
195
+ if not source_file or source_line is None or source_column is None:
196
+ return []
197
+
198
+ matches = lookup_entity_at_location(
199
+ conn, source_file, source_line, source_column, include_backend=False
200
+ )
201
+
202
+ candidates = []
203
+ for m in matches:
204
+ c = ResolutionCandidate(
205
+ entity_type=m.entity_type,
206
+ entity_id=m.entity_id,
207
+ confidence=_CONFIDENCE["exact_location"],
208
+ strategies_matched=["exact_location"],
209
+ component={"id": m.extra.get("component_id")} if m.extra.get("component_id") else None,
210
+ source_range=m.source_range,
211
+ file_path=m.file_path,
212
+ tag_name=m.extra.get("tag_name"),
213
+ static_classes=_parse_json(m.extra.get("static_classes")) or [],
214
+ element_id_attr=m.extra.get("element_id_attr"),
215
+ )
216
+ candidates.append(c)
217
+
218
+ return candidates
219
+
220
+
221
+ # ──────────────────────────────────────────────────────────────
222
+ # Strategy 2: Source file + component name
223
+ # ──────────────────────────────────────────────────────────────
224
+
225
+ def _try_file_and_component(conn, meta: dict) -> List[ResolutionCandidate]:
226
+ source_file = meta.get("source_file")
227
+ component_name = meta.get("component_name")
228
+
229
+ if not component_name:
230
+ return []
231
+
232
+ if source_file:
233
+ row = conn.execute(
234
+ """SELECT c.*, f.path as file_path
235
+ FROM frontend_components c
236
+ JOIN files f ON c.file_id = f.id
237
+ WHERE c.name = ? AND f.path = ?
238
+ LIMIT 1""",
239
+ (component_name, source_file)
240
+ ).fetchone()
241
+ else:
242
+ row = conn.execute(
243
+ """SELECT c.*, f.path as file_path
244
+ FROM frontend_components c
245
+ JOIN files f ON c.file_id = f.id
246
+ WHERE c.name = ?
247
+ LIMIT 1""",
248
+ (component_name,)
249
+ ).fetchone()
250
+
251
+ if not row:
252
+ return []
253
+
254
+ return [_component_row_to_candidate(row, _CONFIDENCE["file_and_component"], "file_and_component")]
255
+
256
+
257
+ # ──────────────────────────────────────────────────────────────
258
+ # Strategy 3: Component ancestry → render graph
259
+ # ──────────────────────────────────────────────────────────────
260
+
261
+ def _try_component_ancestry(conn, meta: dict) -> List[ResolutionCandidate]:
262
+ ancestry = meta.get("component_ancestry")
263
+ if not ancestry or not isinstance(ancestry, list) or len(ancestry) < 2:
264
+ return []
265
+
266
+ # ancestry is a list of component names from root to leaf, e.g. ["App", "Card", "Button"]
267
+ # We start from the root and walk the render graph to find the leaf
268
+ root_name = ancestry[0]
269
+ root_row = conn.execute(
270
+ """SELECT c.id FROM frontend_components c WHERE c.name = ? LIMIT 1""",
271
+ (root_name,)
272
+ ).fetchone()
273
+ if not root_row:
274
+ return []
275
+
276
+ # Walk render graph following ancestry names
277
+ current_id = root_row["id"]
278
+ for i in range(1, len(ancestry)):
279
+ name = ancestry[i]
280
+ children = conn.execute(
281
+ """SELECT rr.child_component_id, c.name
282
+ FROM render_relationships rr
283
+ JOIN frontend_components c ON rr.child_component_id = c.id
284
+ WHERE rr.parent_component_id = ? AND c.name = ?
285
+ LIMIT 1""",
286
+ (current_id, name)
287
+ ).fetchone()
288
+ if not children:
289
+ # Ancestry doesn't match — try without name match (fuzzy)
290
+ return []
291
+ current_id = children["child_component_id"]
292
+
293
+ # Found the leaf component
294
+ row = conn.execute(
295
+ """SELECT c.*, f.path as file_path
296
+ FROM frontend_components c
297
+ JOIN files f ON c.file_id = f.id
298
+ WHERE c.id = ?""",
299
+ (current_id,)
300
+ ).fetchone()
301
+ if not row:
302
+ return []
303
+
304
+ return [_component_row_to_candidate(row, _CONFIDENCE["component_ancestry"], "component_ancestry")]
305
+
306
+
307
+ # ──────────────────────────────────────────────────────────────
308
+ # Strategy 4: Element ID
309
+ # ──────────────────────────────────────────────────────────────
310
+
311
+ def _try_element_id(conn, meta: dict) -> List[ResolutionCandidate]:
312
+ element_id_attr = meta.get("element_id")
313
+ if not element_id_attr:
314
+ return []
315
+
316
+ rows = conn.execute(
317
+ """SELECT m.*, f.path as file_path
318
+ FROM markup_elements m
319
+ JOIN files f ON m.file_id = f.id
320
+ WHERE m.element_id_attr = ?
321
+ ORDER BY m.id""",
322
+ (element_id_attr,)
323
+ ).fetchall()
324
+
325
+ if not rows:
326
+ return []
327
+
328
+ if len(rows) == 1:
329
+ return [_element_row_to_candidate(rows[0], _CONFIDENCE["element_id_unique"], "element_id_unique")]
330
+ else:
331
+ return [
332
+ _element_row_to_candidate(r, _CONFIDENCE["element_id_duplicate"], "element_id_duplicate")
333
+ for r in rows
334
+ ]
335
+
336
+
337
+ # ──────────────────────────────────────────────────────────────
338
+ # Strategy 5: Tag + class combination
339
+ # ──────────────────────────────────────────────────────────────
340
+
341
+ def _try_tag_and_classes(conn, meta: dict) -> List[ResolutionCandidate]:
342
+ dom_tag = meta.get("dom_tag")
343
+ classes = meta.get("classes")
344
+
345
+ if not dom_tag or not classes or not isinstance(classes, list):
346
+ return []
347
+
348
+ # Query elements matching the tag
349
+ rows = conn.execute(
350
+ """SELECT m.*, f.path as file_path
351
+ FROM markup_elements m
352
+ JOIN files f ON m.file_id = f.id
353
+ WHERE m.tag_name = ?
354
+ ORDER BY m.id""",
355
+ (dom_tag,)
356
+ ).fetchall()
357
+
358
+ candidates = []
359
+ target_class_set = set(classes)
360
+
361
+ for row in rows:
362
+ elem_classes = _parse_json(row["static_classes"]) or []
363
+ elem_class_set = set(elem_classes)
364
+
365
+ if target_class_set.issubset(elem_class_set):
366
+ # All target classes present
367
+ c = _element_row_to_candidate(row, _CONFIDENCE["tag_and_classes_full"], "tag_and_classes_full")
368
+ candidates.append(c)
369
+ elif elem_class_set & target_class_set:
370
+ # Partial overlap
371
+ overlap = len(elem_class_set & target_class_set)
372
+ total = len(target_class_set)
373
+ score = _CONFIDENCE["tag_and_classes_partial"] * (overlap / total) if total else 0
374
+ c = _element_row_to_candidate(row, score, "tag_and_classes_partial")
375
+ candidates.append(c)
376
+
377
+ return candidates
378
+
379
+
380
+ # ──────────────────────────────────────────────────────────────
381
+ # Strategy 6: Attributes
382
+ # ──────────────────────────────────────────────────────────────
383
+
384
+ def _try_attributes(conn, meta: dict) -> List[ResolutionCandidate]:
385
+ attributes = meta.get("attributes")
386
+ dom_tag = meta.get("dom_tag")
387
+
388
+ if not attributes or not isinstance(attributes, dict):
389
+ return []
390
+
391
+ # Build a query that searches for elements with matching attributes
392
+ # attributes is a dict like {"data-testid": "submit", "role": "button"}
393
+ # We use JSON extraction to match
394
+ rows = conn.execute(
395
+ """SELECT m.*, f.path as file_path
396
+ FROM markup_elements m
397
+ JOIN files f ON m.file_id = f.id
398
+ WHERE m.attributes IS NOT NULL
399
+ ORDER BY m.id"""
400
+ ).fetchall()
401
+
402
+ candidates = []
403
+ for row in rows:
404
+ elem_attrs = _parse_json(row["attributes"]) or {}
405
+ if not isinstance(elem_attrs, dict):
406
+ continue
407
+
408
+ # Check how many attributes match
409
+ matched = 0
410
+ total = len(attributes)
411
+ for k, v in attributes.items():
412
+ if k in elem_attrs and str(elem_attrs[k]) == str(v):
413
+ matched += 1
414
+
415
+ if matched == 0:
416
+ continue
417
+
418
+ # If dom_tag is provided, also check tag
419
+ if dom_tag and row["tag_name"] != dom_tag:
420
+ continue
421
+
422
+ score = _CONFIDENCE["attributes"] * (matched / total) if total else 0
423
+ c = _element_row_to_candidate(row, score, "attributes")
424
+ candidates.append(c)
425
+
426
+ return candidates
427
+
428
+
429
+ # ──────────────────────────────────────────────────────────────
430
+ # Strategy 7: Text content
431
+ # ──────────────────────────────────────────────────────────────
432
+
433
+ def _try_text_content(conn, meta: dict, project_root: str = None) -> List[ResolutionCandidate]:
434
+ text = meta.get("text")
435
+ if not text:
436
+ return []
437
+
438
+ # Text nodes are stored as element_type='text' but the actual text content
439
+ # is not stored in the DB — we need to read it from the source file using
440
+ # the element's source_range.
441
+ rows = conn.execute(
442
+ """SELECT m.*, f.path as file_path
443
+ FROM markup_elements m
444
+ JOIN files f ON m.file_id = f.id
445
+ WHERE m.element_type = 'text' OR m.element_type = '#text'
446
+ ORDER BY m.id"""
447
+ ).fetchall()
448
+
449
+ candidates = []
450
+ for row in rows:
451
+ # Try to extract text from the source file using source_range
452
+ elem_text = ""
453
+ source_range = _parse_json(row["source_range"]) if "source_range" in row.keys() else None
454
+ file_path = row["file_path"] if "file_path" in row.keys() else None
455
+
456
+ if source_range and file_path:
457
+ try:
458
+ import os
459
+ # file_path is project-relative; try resolving with project_root
460
+ candidates_paths = [file_path]
461
+ if project_root:
462
+ candidates_paths.append(os.path.join(project_root, file_path))
463
+ resolved = None
464
+ for p in candidates_paths:
465
+ if os.path.exists(p):
466
+ resolved = p
467
+ break
468
+ if resolved:
469
+ with open(resolved, "r", encoding="utf-8", errors="replace") as f:
470
+ lines = f.readlines()
471
+ start_line = source_range.get("start_line", 1) - 1
472
+ end_line = source_range.get("end_line", 1)
473
+ if 0 <= start_line < len(lines):
474
+ elem_text = "".join(lines[start_line:end_line]).strip()
475
+ except Exception:
476
+ pass
477
+
478
+ # Also check attributes as a fallback
479
+ if not elem_text:
480
+ attrs = _parse_json(row["attributes"]) or {}
481
+ if isinstance(attrs, dict):
482
+ elem_text = attrs.get("text", "") or attrs.get("content", "")
483
+
484
+ if not elem_text:
485
+ continue
486
+
487
+ # Check if the text matches (exact or contains)
488
+ if text in elem_text or elem_text in text:
489
+ # Get the parent element (the real element, not the text node)
490
+ parent_id = row["parent_element_id"]
491
+ if parent_id:
492
+ parent_row = conn.execute(
493
+ """SELECT m.*, f.path as file_path
494
+ FROM markup_elements m
495
+ JOIN files f ON m.file_id = f.id
496
+ WHERE m.id = ?""",
497
+ (parent_id,)
498
+ ).fetchone()
499
+ if parent_row:
500
+ c = _element_row_to_candidate(parent_row, _CONFIDENCE["text_content"], "text_content")
501
+ candidates.append(c)
502
+ continue
503
+
504
+ # Fallback: match on the text node itself
505
+ c = _element_row_to_candidate(row, _CONFIDENCE["text_content"] * 0.8, "text_content")
506
+ candidates.append(c)
507
+
508
+ return candidates
509
+
510
+
511
+ # ──────────────────────────────────────────────────────────────
512
+ # Strategy 8: DOM ancestry → markup tree
513
+ # ──────────────────────────────────────────────────────────────
514
+
515
+ def _try_dom_ancestry(conn, meta: dict) -> List[ResolutionCandidate]:
516
+ dom_ancestry = meta.get("dom_ancestry")
517
+ if not dom_ancestry or not isinstance(dom_ancestry, list) or len(dom_ancestry) < 2:
518
+ return []
519
+
520
+ # dom_ancestry is a list of dicts: [{"tag": "body"}, {"tag": "div", "classes": ["container"]}, {"tag": "button", "classes": ["btn"]}]
521
+ # The last element is the target element
522
+ target = dom_ancestry[-1]
523
+ target_tag = target.get("tag", "")
524
+ target_classes = target.get("classes", [])
525
+
526
+ # Find candidate elements matching the target's tag
527
+ rows = conn.execute(
528
+ """SELECT m.*, f.path as file_path
529
+ FROM markup_elements m
530
+ JOIN files f ON m.file_id = f.id
531
+ WHERE m.tag_name = ?
532
+ ORDER BY m.id""",
533
+ (target_tag,)
534
+ ).fetchall()
535
+
536
+ candidates = []
537
+ for row in rows:
538
+ # Walk up the markup tree and check ancestry
539
+ ancestry_match = _check_ancestry(conn, row["id"], dom_ancestry[:-1])
540
+ if ancestry_match == "full":
541
+ c = _element_row_to_candidate(row, _CONFIDENCE["dom_ancestry_full"], "dom_ancestry_full")
542
+ candidates.append(c)
543
+ elif ancestry_match == "partial":
544
+ c = _element_row_to_candidate(row, _CONFIDENCE["dom_ancestry_partial"], "dom_ancestry_partial")
545
+ candidates.append(c)
546
+
547
+ return candidates
548
+
549
+
550
+ def _check_ancestry(conn, element_id: int, expected_ancestry: List[dict]) -> str:
551
+ """Walk up the markup tree from element_id, checking against expected ancestry.
552
+
553
+ Returns "full" if all ancestors match, "partial" if some match, "" if none.
554
+ """
555
+ if not expected_ancestry:
556
+ return "full"
557
+
558
+ current_id = element_id
559
+ matched = 0
560
+ expected_idx = len(expected_ancestry) - 1 # Start from the immediate parent
561
+
562
+ while current_id and expected_idx >= 0:
563
+ row = conn.execute(
564
+ "SELECT parent_element_id, tag_name, static_classes FROM markup_elements WHERE id = ?",
565
+ (current_id,)
566
+ ).fetchone()
567
+ if not row:
568
+ break
569
+
570
+ parent_id = row["parent_element_id"]
571
+ if parent_id is None:
572
+ break
573
+
574
+ parent_row = conn.execute(
575
+ "SELECT tag_name, static_classes FROM markup_elements WHERE id = ?",
576
+ (parent_id,)
577
+ ).fetchone()
578
+ if not parent_row:
579
+ break
580
+
581
+ expected = expected_ancestry[expected_idx]
582
+ expected_tag = expected.get("tag", "")
583
+
584
+ if parent_row["tag_name"] == expected_tag:
585
+ expected_classes = set(expected.get("classes", []))
586
+ if expected_classes:
587
+ parent_classes = set(_parse_json(parent_row["static_classes"]) or [])
588
+ if expected_classes.issubset(parent_classes):
589
+ matched += 1
590
+ else:
591
+ matched += 1
592
+
593
+ current_id = parent_id
594
+ expected_idx -= 1
595
+
596
+ if matched == len(expected_ancestry):
597
+ return "full"
598
+ elif matched > 0:
599
+ return "partial"
600
+ return ""
601
+
602
+
603
+ # ──────────────────────────────────────────────────────────────
604
+ # Strategy 9: Heuristic selector matching
605
+ # ──────────────────────────────────────────────────────────────
606
+
607
+ def _try_selector_matching(conn, meta: dict) -> List[ResolutionCandidate]:
608
+ dom_tag = meta.get("dom_tag")
609
+ classes = meta.get("classes", [])
610
+ element_id_attr = meta.get("element_id")
611
+
612
+ if not dom_tag and not classes and not element_id_attr:
613
+ return []
614
+
615
+ # First, check style_selector_matches for pre-computed matches
616
+ # If we have classes, find selectors that match those classes
617
+ rows = conn.execute(
618
+ """SELECT m.*, f.path as file_path
619
+ FROM markup_elements m
620
+ JOIN files f ON m.file_id = f.id
621
+ WHERE m.tag_name = ?
622
+ ORDER BY m.id""",
623
+ (dom_tag,)
624
+ ).fetchall() if dom_tag else []
625
+
626
+ candidates = []
627
+ for row in rows:
628
+ elem_classes = _parse_json(row["static_classes"]) or []
629
+ elem_id = row["element_id_attr"]
630
+
631
+ # Check if any selector in the index matches this element
632
+ # First check pre-computed matches
633
+ sm_rows = conn.execute(
634
+ "SELECT selector_id FROM style_selector_matches WHERE element_id = ? LIMIT 1",
635
+ (row["id"],)
636
+ ).fetchall()
637
+
638
+ if sm_rows:
639
+ c = _element_row_to_candidate(row, _CONFIDENCE["selector_matching"], "selector_matching")
640
+ candidates.append(c)
641
+ continue
642
+
643
+ # Fallback: runtime selector matching against all selectors
644
+ all_selectors = conn.execute(
645
+ "SELECT selector_text FROM style_selectors"
646
+ ).fetchall()
647
+
648
+ for sel_row in all_selectors:
649
+ if selector_matches_element(sel_row["selector_text"], dom_tag, elem_classes, elem_id):
650
+ c = _element_row_to_candidate(row, _CONFIDENCE["selector_matching"], "selector_matching")
651
+ candidates.append(c)
652
+ break
653
+
654
+ return candidates
655
+
656
+
657
+ # ──────────────────────────────────────────────────────────────
658
+ # Enrichment
659
+ # ──────────────────────────────────────────────────────────────
660
+
661
+ def _enrich_candidate(conn, candidate: ResolutionCandidate) -> None:
662
+ """Enrich a candidate with events, bindings, styles, and rendering parents."""
663
+ if candidate.entity_type == "markup_element":
664
+ eid = candidate.entity_id
665
+
666
+ # Events
667
+ events_result = traverse_event_graph(conn, eid)
668
+ candidate.events = events_result.get("events", [])
669
+
670
+ # Bindings
671
+ bindings_result = traverse_binding_graph(conn, eid)
672
+ candidate.bindings = bindings_result.get("bindings", [])
673
+
674
+ # Styles (selectors matching this element)
675
+ styles_result = traverse_style_graph(conn, eid, "to_definition")
676
+ candidate.styles = styles_result.get("candidate_selectors", [])
677
+
678
+ # Owning component
679
+ if candidate.component and "id" in candidate.component:
680
+ comp_id = candidate.component["id"]
681
+ comp_row = conn.execute(
682
+ """SELECT c.name, c.source_range, f.path as file_path
683
+ FROM frontend_components c
684
+ JOIN files f ON c.file_id = f.id
685
+ WHERE c.id = ?""",
686
+ (comp_id,)
687
+ ).fetchone()
688
+ if comp_row:
689
+ candidate.component = {
690
+ "id": comp_id,
691
+ "name": comp_row["name"],
692
+ "source_range": _parse_json(comp_row["source_range"]),
693
+ "file_path": comp_row["file_path"],
694
+ }
695
+
696
+ # Rendering parents
697
+ parents_result = traverse_render_graph(conn, comp_id, "parents", max_depth=5)
698
+ candidate.rendering_parents = parents_result.get("children", [])
699
+
700
+ elif candidate.entity_type == "component":
701
+ comp_id = candidate.entity_id
702
+
703
+ # Rendering parents
704
+ parents_result = traverse_render_graph(conn, comp_id, "parents", max_depth=5)
705
+ candidate.rendering_parents = parents_result.get("children", [])
706
+
707
+ # Enrich component info
708
+ comp_row = conn.execute(
709
+ """SELECT c.name, c.source_range, f.path as file_path
710
+ FROM frontend_components c
711
+ JOIN files f ON c.file_id = f.id
712
+ WHERE c.id = ?""",
713
+ (comp_id,)
714
+ ).fetchone()
715
+ if comp_row:
716
+ candidate.component = {
717
+ "id": comp_id,
718
+ "name": comp_row["name"],
719
+ "source_range": _parse_json(comp_row["source_range"]),
720
+ "file_path": comp_row["file_path"],
721
+ }
722
+
723
+
724
+ # ──────────────────────────────────────────────────────────────
725
+ # Merge and score
726
+ # ──────────────────────────────────────────────────────────────
727
+
728
+ def _merge_candidates(candidates: List[ResolutionCandidate]) -> List[ResolutionCandidate]:
729
+ """Merge candidates that refer to the same entity.
730
+
731
+ Combines confidence scores (probabilistic OR) and merges strategies_matched.
732
+ """
733
+ merged: Dict[Tuple[str, int], ResolutionCandidate] = {}
734
+
735
+ for c in candidates:
736
+ key = (c.entity_type, c.entity_id)
737
+ if key in merged:
738
+ existing = merged[key]
739
+ existing.confidence = _combine_confidence(existing.confidence, c.confidence)
740
+ for s in c.strategies_matched:
741
+ if s not in existing.strategies_matched:
742
+ existing.strategies_matched.append(s)
743
+ # Keep the higher-confidence fields
744
+ if c.confidence > existing.confidence:
745
+ existing.source_range = c.source_range or existing.source_range
746
+ existing.file_path = c.file_path or existing.file_path
747
+ existing.tag_name = c.tag_name or existing.tag_name
748
+ existing.static_classes = c.static_classes or existing.static_classes
749
+ existing.element_id_attr = c.element_id_attr or existing.element_id_attr
750
+ else:
751
+ merged[key] = c
752
+
753
+ return list(merged.values())
754
+
755
+
756
+ def _build_ambiguity_explanation(candidates: List[ResolutionCandidate]) -> Optional[str]:
757
+ """Build a human-readable explanation of ambiguity."""
758
+ if len(candidates) <= 1:
759
+ return None
760
+
761
+ # Group by strategy
762
+ strategies = set()
763
+ for c in candidates:
764
+ strategies.update(c.strategies_matched)
765
+
766
+ descriptions = []
767
+ for c in candidates[:5]: # Top 5
768
+ entity_desc = c.tag_name or c.entity_type
769
+ if c.static_classes:
770
+ entity_desc += "." + ".".join(c.static_classes[:3])
771
+ if c.element_id_attr:
772
+ entity_desc += f"#{c.element_id_attr}"
773
+ strategies_str = ", ".join(c.strategies_matched)
774
+ descriptions.append(
775
+ f"{entity_desc} (id={c.entity_id}, confidence={c.confidence:.2f}, strategies: {strategies_str})"
776
+ )
777
+
778
+ summary = f"{len(candidates)} candidates found. " + "; ".join(descriptions)
779
+ if len(candidates) > 5:
780
+ summary += f" ... and {len(candidates) - 5} more"
781
+ return summary
782
+
783
+
784
+ # ──────────────────────────────────────────────────────────────
785
+ # Main entry point
786
+ # ──────────────────────────────────────────────────────────────
787
+
788
+ def resolve_runtime_element(
789
+ conn: sqlite3.Connection,
790
+ metadata: Dict[str, Any],
791
+ project_root: str = None,
792
+ ) -> Dict[str, Any]:
793
+ """Resolve browser/runtime element metadata to source semantic entities.
794
+
795
+ Args:
796
+ conn: SQLite connection to the index database.
797
+ metadata: Dict with any combination of keys:
798
+ - source_file (str): Project-relative file path
799
+ - source_line (int): 1-indexed line number
800
+ - source_column (int): 0-indexed column number
801
+ - component_name (str): React/component name
802
+ - component_ancestry (list[str]): Component names from root to leaf
803
+ - dom_tag (str): HTML tag name
804
+ - element_id (str): Element's id attribute value
805
+ - classes (list[str]): CSS class names
806
+ - attributes (dict): Element attributes
807
+ - text (str): Text content
808
+ - dom_ancestry (list[dict]): DOM tree from root to target,
809
+ each dict has "tag" (str) and optionally "classes" (list[str])
810
+ project_root: Optional path to the project root directory, for resolving
811
+ project-relative file paths when reading source files.
812
+
813
+ Returns:
814
+ Dict with:
815
+ - candidates: list of candidate dicts sorted by confidence (highest first)
816
+ - ambiguity_explanation: str or None
817
+ - best_confidence: float
818
+ """
819
+ all_candidates: List[ResolutionCandidate] = []
820
+
821
+ # Run all applicable strategies
822
+ strategies = [
823
+ _try_exact_location,
824
+ _try_file_and_component,
825
+ _try_component_ancestry,
826
+ _try_element_id,
827
+ _try_tag_and_classes,
828
+ _try_attributes,
829
+ _try_text_content,
830
+ _try_dom_ancestry,
831
+ _try_selector_matching,
832
+ ]
833
+
834
+ for strategy_fn in strategies:
835
+ try:
836
+ if strategy_fn is _try_text_content:
837
+ results = strategy_fn(conn, metadata, project_root)
838
+ else:
839
+ results = strategy_fn(conn, metadata)
840
+ all_candidates.extend(results)
841
+ except Exception:
842
+ # A strategy failure should never prevent other strategies from running
843
+ continue
844
+
845
+ # Merge duplicates
846
+ merged = _merge_candidates(all_candidates)
847
+
848
+ # Enrich each candidate
849
+ for c in merged:
850
+ _enrich_candidate(conn, c)
851
+
852
+ # Sort by confidence (highest first)
853
+ merged.sort(key=lambda c: c.confidence, reverse=True)
854
+
855
+ # Build result
856
+ result = ResolutionResult(
857
+ candidates=merged,
858
+ ambiguity_explanation=_build_ambiguity_explanation(merged),
859
+ best_confidence=merged[0].confidence if merged else 0.0,
860
+ )
861
+
862
+ return result.to_dict()