ref-agents 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. ref_agents/__init__.py +9 -0
  2. ref_agents/api_keys.json.example +8 -0
  3. ref_agents/auth.py +129 -0
  4. ref_agents/codemap/..md +62 -0
  5. ref_agents/codemap/CODE_MAP.md +37 -0
  6. ref_agents/codemap/core.md +43 -0
  7. ref_agents/codemap/models.md +43 -0
  8. ref_agents/codemap/prompts.md +40 -0
  9. ref_agents/codemap/security.md +45 -0
  10. ref_agents/codemap/tools.md +94 -0
  11. ref_agents/codemap/tools_browser.md +44 -0
  12. ref_agents/codemap/utils.md +42 -0
  13. ref_agents/codemap/workflow.md +42 -0
  14. ref_agents/config/ai_patterns.yaml +101 -0
  15. ref_agents/config/frameworks/angular.yaml +104 -0
  16. ref_agents/config/frameworks/aspnet.yaml +84 -0
  17. ref_agents/config/frameworks/ef_core.yaml +81 -0
  18. ref_agents/config/frameworks/react.yaml +111 -0
  19. ref_agents/config/frameworks/spring_boot.yaml +117 -0
  20. ref_agents/config/languages/csharp.yaml +153 -0
  21. ref_agents/config/languages/java.yaml +188 -0
  22. ref_agents/config/languages/javascript.yaml +172 -0
  23. ref_agents/config/languages/python.yaml +153 -0
  24. ref_agents/config/languages/typescript.yaml +193 -0
  25. ref_agents/constants.py +553 -0
  26. ref_agents/core/__init__.py +15 -0
  27. ref_agents/core/config_loader.py +160 -0
  28. ref_agents/core/config_models.py +167 -0
  29. ref_agents/core/config_parsing.py +84 -0
  30. ref_agents/core/language_detector.py +388 -0
  31. ref_agents/core/validation_models.py +66 -0
  32. ref_agents/core/validation_primitives.py +176 -0
  33. ref_agents/errors.py +34 -0
  34. ref_agents/license_client.py +247 -0
  35. ref_agents/models/__init__.py +22 -0
  36. ref_agents/models/gherkin.py +45 -0
  37. ref_agents/models/hierarchy.py +80 -0
  38. ref_agents/models/invest.py +59 -0
  39. ref_agents/models/version.py +49 -0
  40. ref_agents/prompts/__init__.py +9 -0
  41. ref_agents/prompts/start_agent.py +772 -0
  42. ref_agents/rules/architecture/backend_patterns.md +43 -0
  43. ref_agents/rules/architecture/diagramming.md +100 -0
  44. ref_agents/rules/architecture/frontend_patterns.md +40 -0
  45. ref_agents/rules/architecture/impact_analysis.md +129 -0
  46. ref_agents/rules/architecture/migration_strategy.md +208 -0
  47. ref_agents/rules/architecture/regression_protocol.md +77 -0
  48. ref_agents/rules/architecture/system_design.md +97 -0
  49. ref_agents/rules/common/codemap_standard.md +97 -0
  50. ref_agents/rules/common/core_protocol.md +59 -0
  51. ref_agents/rules/common/prompt_engineering.md +294 -0
  52. ref_agents/rules/development/debugging.md +32 -0
  53. ref_agents/rules/development/implementation.md +205 -0
  54. ref_agents/rules/operations/completion.md +119 -0
  55. ref_agents/rules/operations/cutover_protocol.md +218 -0
  56. ref_agents/rules/operations/discovery.md +179 -0
  57. ref_agents/rules/operations/fix_workflow.md +87 -0
  58. ref_agents/rules/operations/forensics.md +278 -0
  59. ref_agents/rules/operations/platform.md +263 -0
  60. ref_agents/rules/operations/synchronous_flow.md +25 -0
  61. ref_agents/rules/product/ac_validation.md +25 -0
  62. ref_agents/rules/product/brainstorming.md +27 -0
  63. ref_agents/rules/product/ref_flow.md +101 -0
  64. ref_agents/rules/product/requirements_std.md +114 -0
  65. ref_agents/rules/product/spec_writing.md +235 -0
  66. ref_agents/rules/product/strategy.md +96 -0
  67. ref_agents/rules/quality/documentation_standards.md +46 -0
  68. ref_agents/rules/quality/parity_testing.md +234 -0
  69. ref_agents/rules/quality/project_documentation.md +56 -0
  70. ref_agents/rules/quality/qa_lead.md +111 -0
  71. ref_agents/rules/quality/test_design.md +146 -0
  72. ref_agents/rules/quality/testing_standards.md +293 -0
  73. ref_agents/rules/review/pr_review.md +116 -0
  74. ref_agents/rules/security/security_audit.md +83 -0
  75. ref_agents/security/__init__.py +22 -0
  76. ref_agents/security/dependency_audit.py +188 -0
  77. ref_agents/security/file_audit.py +208 -0
  78. ref_agents/security/network_scan.py +179 -0
  79. ref_agents/security/report_generator.py +313 -0
  80. ref_agents/security/secret_scan.py +252 -0
  81. ref_agents/security/url_scan.py +240 -0
  82. ref_agents/security_scan.py +236 -0
  83. ref_agents/server.py +1586 -0
  84. ref_agents/session.py +100 -0
  85. ref_agents/tool_names.py +55 -0
  86. ref_agents/tools/__init__.py +8 -0
  87. ref_agents/tools/agents_generator.py +315 -0
  88. ref_agents/tools/ai_pattern_detector.py +815 -0
  89. ref_agents/tools/brownfield_populator.py +529 -0
  90. ref_agents/tools/browser/__init__.py +50 -0
  91. ref_agents/tools/browser/evidence_verifier.py +302 -0
  92. ref_agents/tools/browser/execution_logger.py +249 -0
  93. ref_agents/tools/browser/playwright_mcp_client.py +259 -0
  94. ref_agents/tools/browser/screenshot_utils.py +184 -0
  95. ref_agents/tools/browser/test_executor.py +537 -0
  96. ref_agents/tools/code_quality_scanner.py +629 -0
  97. ref_agents/tools/codemap/..md +93 -0
  98. ref_agents/tools/codemap/CODE_MAP.md +30 -0
  99. ref_agents/tools/codemap/browser.md +44 -0
  100. ref_agents/tools/codemap.py +403 -0
  101. ref_agents/tools/codemap_freshness.py +234 -0
  102. ref_agents/tools/comment_smell_scanner.py +346 -0
  103. ref_agents/tools/complexity.py +436 -0
  104. ref_agents/tools/complexity_ast.py +333 -0
  105. ref_agents/tools/compliance.py +246 -0
  106. ref_agents/tools/compliance_remediation.py +846 -0
  107. ref_agents/tools/context_graph.py +839 -0
  108. ref_agents/tools/context_manager.py +550 -0
  109. ref_agents/tools/context_tools.py +121 -0
  110. ref_agents/tools/cross_repo_linker.py +393 -0
  111. ref_agents/tools/dead_code_scanner.py +637 -0
  112. ref_agents/tools/debt_scanner.py +1092 -0
  113. ref_agents/tools/dependency_graph.py +272 -0
  114. ref_agents/tools/discovery_audit.py +372 -0
  115. ref_agents/tools/docs_scanner.py +600 -0
  116. ref_agents/tools/evaluate_gate.py +119 -0
  117. ref_agents/tools/external_detector.py +524 -0
  118. ref_agents/tools/features_generator.py +282 -0
  119. ref_agents/tools/flow_gap_detector.py +373 -0
  120. ref_agents/tools/flow_mapper.py +327 -0
  121. ref_agents/tools/full_suite_runner.py +740 -0
  122. ref_agents/tools/gherkin_parser.py +227 -0
  123. ref_agents/tools/guard_tools.py +139 -0
  124. ref_agents/tools/handoff_tools.py +282 -0
  125. ref_agents/tools/health_scanner.py +1211 -0
  126. ref_agents/tools/hierarchy_manager.py +289 -0
  127. ref_agents/tools/invest_scorer.py +249 -0
  128. ref_agents/tools/jira_confluence_export.py +306 -0
  129. ref_agents/tools/json_output.py +76 -0
  130. ref_agents/tools/migration_mapper.py +946 -0
  131. ref_agents/tools/migration_readiness_scanner.py +209 -0
  132. ref_agents/tools/pattern_learner.py +522 -0
  133. ref_agents/tools/report_utils.py +155 -0
  134. ref_agents/tools/requirements_serializer.py +225 -0
  135. ref_agents/tools/security_audit_tool.py +106 -0
  136. ref_agents/tools/sequencing_engine.py +288 -0
  137. ref_agents/tools/summary_generator.py +275 -0
  138. ref_agents/tools/symbol_resolver.py +306 -0
  139. ref_agents/tools/symbol_smoke_runner.py +336 -0
  140. ref_agents/tools/test_plan_validator.py +189 -0
  141. ref_agents/tools/test_smell_walker.py +902 -0
  142. ref_agents/tools/tier1_fixer.py +502 -0
  143. ref_agents/tools/validators/__init__.py +419 -0
  144. ref_agents/tools/validators/architect.py +268 -0
  145. ref_agents/tools/validators/cutover_engineer.py +167 -0
  146. ref_agents/tools/validators/developer.py +180 -0
  147. ref_agents/tools/validators/discovery.py +150 -0
  148. ref_agents/tools/validators/forensic_engineer.py +191 -0
  149. ref_agents/tools/validators/impact_architect.py +181 -0
  150. ref_agents/tools/validators/migration_planner.py +166 -0
  151. ref_agents/tools/validators/parity_tester.py +155 -0
  152. ref_agents/tools/validators/platform_engineer.py +134 -0
  153. ref_agents/tools/validators/pr_reviewer.py +129 -0
  154. ref_agents/tools/validators/product_manager.py +291 -0
  155. ref_agents/tools/validators/qa_lead.py +172 -0
  156. ref_agents/tools/validators/scrum_master.py +212 -0
  157. ref_agents/tools/validators/security_owner.py +162 -0
  158. ref_agents/tools/validators/specifier.py +134 -0
  159. ref_agents/tools/validators/strategist.py +149 -0
  160. ref_agents/tools/validators/tester.py +121 -0
  161. ref_agents/tools/version_manager.py +202 -0
  162. ref_agents/tools/workflow_tools.py +1549 -0
  163. ref_agents/utils/__init__.py +21 -0
  164. ref_agents/utils/git_utils.py +351 -0
  165. ref_agents/utils/handoff_logger.py +368 -0
  166. ref_agents/utils/ignore_matcher.py +270 -0
  167. ref_agents/workflow/__init__.py +19 -0
  168. ref_agents/workflow/capabilities.py +328 -0
  169. ref_agents/workflow/state_machine.py +708 -0
  170. ref_agents/workflow/transitions.py +658 -0
  171. ref_agents-1.0.0.dist-info/METADATA +365 -0
  172. ref_agents-1.0.0.dist-info/RECORD +175 -0
  173. ref_agents-1.0.0.dist-info/WHEEL +4 -0
  174. ref_agents-1.0.0.dist-info/entry_points.txt +2 -0
  175. ref_agents-1.0.0.dist-info/licenses/LICENSE +115 -0
@@ -0,0 +1,529 @@
1
+ """Brownfield Context Graph Populator.
2
+
3
+ Orchestrates discovery-time population of the context graph from existing codebase.
4
+ Layers:
5
+ 1. Structural: File nodes + AST extraction via tree-sitter (Universal)
6
+ 2. Archaeological: Git history -> Decision nodes (L1/L2 only)
7
+ 3. Synthesized: LLM -> Enriched Decision nodes (L1/L2 only)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from pathlib import Path
13
+ from typing import Any, Literal
14
+
15
+ import structlog
16
+
17
+ from ref_agents.tools.context_graph import (
18
+ ContextNode,
19
+ get_context_graph,
20
+ )
21
+
22
+ logger = structlog.get_logger(__name__)
23
+
24
+ # Mapping file extensions → tree-sitter language module name
25
+ _LANG_LOADERS: dict[str, str] = {
26
+ ".py": "tree_sitter_python",
27
+ ".ts": "tree_sitter_typescript",
28
+ ".tsx": "tree_sitter_typescript",
29
+ }
30
+
31
+ # Node types to extract per language (tree-sitter type name → our node category)
32
+ _CLASS_TYPES: dict[str, set[str]] = {
33
+ "python": {"class_definition"},
34
+ "typescript": {"class_declaration"},
35
+ }
36
+ _FUNC_TYPES: dict[str, set[str]] = {
37
+ "python": {"function_definition"},
38
+ "typescript": {"function_declaration", "method_definition"},
39
+ }
40
+ _IMPORT_TYPES: dict[str, set[str]] = {
41
+ "python": {"import_statement", "import_from_statement"},
42
+ "typescript": {"import_statement"},
43
+ }
44
+ _EXT_TO_LANG: dict[str, str] = {
45
+ ".py": "python",
46
+ ".ts": "typescript",
47
+ ".tsx": "typescript",
48
+ }
49
+
50
+
51
+ def _get_language(ext: str) -> Any | None:
52
+ """Load tree-sitter Language for a file extension.
53
+
54
+ Args:
55
+ ext: File extension including dot (e.g. '.py').
56
+
57
+ Returns:
58
+ tree_sitter.Language or None if unsupported.
59
+ """
60
+ loader = _LANG_LOADERS.get(ext)
61
+ if not loader:
62
+ return None
63
+ try:
64
+ import importlib
65
+
66
+ from tree_sitter import Language
67
+
68
+ mod = importlib.import_module(loader)
69
+ return Language(mod.language())
70
+ except (ImportError, AttributeError) as e:
71
+ logger.debug("tree_sitter_lang_load_failed", ext=ext, error=str(e))
72
+ return None
73
+
74
+
75
+ def _walk(node: Any, target_types: set[str]) -> list[Any]:
76
+ """Walk tree-sitter node tree, collecting nodes matching target_types.
77
+
78
+ Args:
79
+ node: tree_sitter.Node root to walk.
80
+ target_types: Set of node type strings to collect.
81
+
82
+ Returns:
83
+ List of matching nodes.
84
+ """
85
+ results = []
86
+ cursor = node.walk()
87
+ visited_children = False
88
+ while True:
89
+ if not visited_children:
90
+ if cursor.node.type in target_types:
91
+ results.append(cursor.node)
92
+ if not cursor.goto_first_child():
93
+ visited_children = True
94
+ elif cursor.goto_next_sibling():
95
+ visited_children = False
96
+ elif not cursor.goto_parent():
97
+ break
98
+ return results
99
+
100
+
101
+ def _node_name(node: Any) -> str | None:
102
+ """Extract identifier name from a class/function definition node.
103
+
104
+ Args:
105
+ node: tree_sitter.Node (class_definition, function_definition, etc.)
106
+
107
+ Returns:
108
+ Name string or None if not found.
109
+ """
110
+ name_node = node.child_by_field_name("name")
111
+ if name_node is not None and name_node.text:
112
+ return name_node.text.decode("utf-8", errors="replace")
113
+ return None
114
+
115
+
116
+ def _import_modules(node: Any, lang: str) -> list[str]:
117
+ """Extract module names from an import node.
118
+
119
+ Args:
120
+ node: tree_sitter.Node for an import statement.
121
+ lang: Language name ('python' or 'typescript').
122
+
123
+ Returns:
124
+ List of module name strings.
125
+ """
126
+ names: list[str] = []
127
+ if lang == "python":
128
+ if node.type == "import_statement":
129
+ for child in node.children:
130
+ if child.type in ("dotted_name", "aliased_import"):
131
+ n = child.child_by_field_name("name") or child
132
+ if n.text:
133
+ names.append(n.text.decode("utf-8", errors="replace"))
134
+ elif node.type == "import_from_statement":
135
+ mod = node.child_by_field_name("module_name")
136
+ if mod and mod.text:
137
+ names.append(mod.text.decode("utf-8", errors="replace"))
138
+ elif lang == "typescript":
139
+ src = node.child_by_field_name("source")
140
+ if src and src.text:
141
+ raw = src.text.decode("utf-8", errors="replace").strip("'\"")
142
+ names.append(raw)
143
+ return names
144
+
145
+
146
+ def _get_decorators(node: Any) -> list[str]:
147
+ """Collect decorator strings from siblings preceding a class/function node.
148
+
149
+ tree-sitter represents decorators as preceding siblings of the decorated
150
+ node (not children), so we walk backwards through prev_sibling chain.
151
+ Works for both Python and TypeScript — both use node type "decorator".
152
+
153
+ Args:
154
+ node: tree_sitter.Node for a class or function definition.
155
+
156
+ Returns:
157
+ List of decorator strings in declaration order (outermost first).
158
+ """
159
+ # TODO: TypeScript method decorators (@Injectable, @Get) may use a different
160
+ # node type path depending on TS grammar version — verify with tree-sitter-typescript.
161
+ decorator_type = "decorator"
162
+ decorators: list[str] = []
163
+ sibling = node.prev_sibling
164
+ while sibling is not None and sibling.type == decorator_type:
165
+ text = sibling.text
166
+ if text:
167
+ decorators.append(text.decode("utf-8", errors="replace").strip())
168
+ sibling = sibling.prev_sibling
169
+ decorators.reverse()
170
+ return decorators
171
+
172
+
173
+ CriticalityLevel = Literal["L1", "L2", "L3"]
174
+
175
+
176
+ class BrownfieldPopulator:
177
+ """Populates context graph during discovery."""
178
+
179
+ def __init__(self, directory: Path | None = None):
180
+ self.root = directory or Path.cwd()
181
+ self.graph = get_context_graph(self.root)
182
+
183
+ def populate(
184
+ self,
185
+ levels: list[CriticalityLevel] | None = None,
186
+ include_git: bool = True,
187
+ include_llm: bool = True,
188
+ ) -> dict:
189
+ """Run full brownfield population.
190
+
191
+ Args:
192
+ levels: List of criticality levels to process for deep analysis (git/llm).
193
+ Defaults to ["L1", "L2"].
194
+ include_git: Whether to mine git history.
195
+ include_llm: Whether to use LLM synthesis.
196
+
197
+ Returns:
198
+ Dict with counts of nodes/edges added.
199
+ """
200
+ if levels is None:
201
+ levels = ["L1", "L2"]
202
+
203
+ logger.info("brownfield_population_start", root=str(self.root), levels=levels)
204
+
205
+ # Clear stale AST nodes so re-populate is idempotent
206
+ self._clear_ast_nodes()
207
+
208
+ # 1. Structural Scan (All files)
209
+ file_nodes = self._scan_structural()
210
+
211
+ # 2. Classify Criticality
212
+ classification = self._classify_criticality(file_nodes)
213
+
214
+ # Filter files for deep analysis based on levels
215
+ deep_analysis_files = []
216
+ for level in levels:
217
+ deep_analysis_files.extend(classification.get(level, []))
218
+
219
+ # 3. Archaeological Mining (Git)
220
+ if include_git:
221
+ self._mine_git_decisions(deep_analysis_files)
222
+
223
+ # 4. Synthesized Decisions (LLM)
224
+ if include_llm:
225
+ self._synthesize_decisions(deep_analysis_files)
226
+
227
+ # 5. Export for Dashboard (handled by save)
228
+ self.graph.save()
229
+
230
+ return {
231
+ "files_scanned": len(file_nodes),
232
+ "deep_analysis_files": len(deep_analysis_files),
233
+ "total_nodes": self.graph.node_count,
234
+ "total_edges": self.graph.edge_count,
235
+ }
236
+
237
+ def _clear_ast_nodes(self) -> None:
238
+ """Remove all AST-origin nodes from graph before repopulation.
239
+
240
+ Clears by node_type (not origin) so old-format nodes without origin field
241
+ are also removed. file/module/class/function are always AST-derived.
242
+ Context nodes (decision/pattern/issue/story/agent_session) preserved.
243
+ """
244
+ from ref_agents.tools.context_graph import AST_NODE_TYPES
245
+
246
+ ast_node_ids = [
247
+ n
248
+ for n in list(self.graph._graph.nodes)
249
+ if self.graph._graph.nodes[n].get("node_type") in AST_NODE_TYPES
250
+ ]
251
+ self.graph._graph.remove_nodes_from(ast_node_ids)
252
+ # Also nuke stale ast_graph.json cache to avoid re-merging on next load
253
+ ast_cache = self.root / ".ref_cache" / "ast_graph.json"
254
+ if ast_cache.exists():
255
+ ast_cache.unlink()
256
+ logger.info("ast_nodes_cleared", count=len(ast_node_ids))
257
+
258
+ def _scan_structural(self) -> list[str]:
259
+ """Layer 1: Create file + AST nodes via tree-sitter extraction."""
260
+ from tree_sitter import Parser
261
+
262
+ added_files = []
263
+ _skip_dirs = {
264
+ ".git",
265
+ "__pycache__",
266
+ ".venv",
267
+ "venv",
268
+ ".ref_cache",
269
+ "node_modules",
270
+ ".pytest_cache",
271
+ }
272
+
273
+ for path in self.root.rglob("*"):
274
+ if not path.is_file():
275
+ continue
276
+
277
+ rel_path = str(path.relative_to(self.root))
278
+ # Check skip dirs against RELATIVE path parts only
279
+ if any(p in _skip_dirs or p.startswith(".") for p in Path(rel_path).parts):
280
+ continue
281
+ file_node_id = f"file-{rel_path.replace('/', '-').replace('.', '_')}"
282
+
283
+ file_node = ContextNode(
284
+ id=file_node_id,
285
+ node_type="file",
286
+ content=rel_path,
287
+ origin="ast",
288
+ metadata={"path": rel_path, "ext": path.suffix},
289
+ )
290
+ self.graph.add_node(file_node)
291
+ added_files.append(rel_path)
292
+
293
+ lang_name = _EXT_TO_LANG.get(path.suffix)
294
+ lang = _get_language(path.suffix)
295
+ if lang is None or lang_name is None:
296
+ continue
297
+
298
+ try:
299
+ source = path.read_bytes()
300
+ except OSError:
301
+ continue
302
+
303
+ try:
304
+ parser = Parser(lang)
305
+ tree = parser.parse(source)
306
+ except (ValueError, RuntimeError) as e:
307
+ logger.debug("tree_sitter_parse_failed", file=rel_path, error=str(e))
308
+ continue
309
+
310
+ self._emit_ast_nodes(tree.root_node, file_node_id, rel_path, lang_name)
311
+
312
+ return added_files
313
+
314
+ def _emit_ast_nodes(
315
+ self,
316
+ root_node: Any,
317
+ file_node_id: str,
318
+ rel_path: str,
319
+ lang_name: str,
320
+ ) -> None:
321
+ """Create module/class/function nodes and edges via tree walker.
322
+
323
+ Functions nested inside a class are parented to that class node
324
+ (contains edge), not to the module. Decorators are captured in
325
+ node metadata.
326
+
327
+ Args:
328
+ root_node: tree_sitter.Node root of parsed tree.
329
+ file_node_id: Parent file node ID.
330
+ rel_path: Relative path for node ID generation.
331
+ lang_name: Language name ('python' or 'typescript').
332
+ """
333
+ base = rel_path.replace("/", "-").replace(".", "_")
334
+
335
+ # Module node — one per file
336
+ mod_id = f"module-{base}"
337
+ if mod_id not in self.graph._graph:
338
+ mod_node = ContextNode(
339
+ id=mod_id,
340
+ node_type="module",
341
+ content=rel_path,
342
+ origin="ast",
343
+ metadata={"path": rel_path},
344
+ )
345
+ self.graph.add_node(mod_node)
346
+ try:
347
+ self.graph.add_edge(mod_id, file_node_id, "contains", confidence=1.0)
348
+ except ValueError:
349
+ pass
350
+
351
+ class_types = _CLASS_TYPES.get(lang_name, set())
352
+ func_types = _FUNC_TYPES.get(lang_name, set())
353
+ import_types = _IMPORT_TYPES.get(lang_name, set())
354
+
355
+ all_target_types = class_types | func_types | import_types
356
+ matched_nodes = _walk(root_node, all_target_types)
357
+
358
+ # Two-pass: first pass builds class_id lookup keyed by ts_node id,
359
+ # so second pass can resolve enclosing class without re-walking.
360
+ class_counter: dict[str, int] = {}
361
+ func_counter: dict[str, int] = {}
362
+
363
+ # Map tree-sitter node id → graph class_id (for method parent resolution)
364
+ ts_node_to_class_id: dict[int, str] = {}
365
+
366
+ # Pass 1: emit class nodes
367
+ for ts_node in matched_nodes:
368
+ if ts_node.type not in class_types:
369
+ continue
370
+ name = _node_name(ts_node)
371
+ if not name:
372
+ continue
373
+ i = class_counter.get(name, 0)
374
+ class_counter[name] = i + 1
375
+ class_id = f"class-{base}-{name}-{i}"
376
+ ts_node_to_class_id[ts_node.id] = class_id
377
+ if class_id not in self.graph._graph:
378
+ decorators = _get_decorators(ts_node)
379
+ cls_node = ContextNode(
380
+ id=class_id,
381
+ node_type="class",
382
+ content=f"{rel_path}::{name}",
383
+ origin="ast",
384
+ metadata={"name": name, "path": rel_path, "decorators": decorators},
385
+ )
386
+ self.graph.add_node(cls_node)
387
+ try:
388
+ self.graph.add_edge(mod_id, class_id, "contains", confidence=1.0)
389
+ except ValueError:
390
+ pass
391
+
392
+ # Pass 2: emit function nodes, resolve parent (class or module)
393
+ for ts_node in matched_nodes:
394
+ ntype = ts_node.type
395
+ if ntype in func_types:
396
+ name = _node_name(ts_node)
397
+ if not name:
398
+ continue
399
+ i = func_counter.get(name, 0)
400
+ func_counter[name] = i + 1
401
+ func_id = f"func-{base}-{name}-{i}"
402
+ if func_id not in self.graph._graph:
403
+ decorators = _get_decorators(ts_node)
404
+ func_node = ContextNode(
405
+ id=func_id,
406
+ node_type="function",
407
+ content=f"{rel_path}::{name}",
408
+ origin="ast",
409
+ metadata={
410
+ "name": name,
411
+ "path": rel_path,
412
+ "decorators": decorators,
413
+ },
414
+ )
415
+ self.graph.add_node(func_node)
416
+ # Find enclosing class by walking ancestor chain
417
+ parent_id = mod_id
418
+ ancestor = ts_node.parent
419
+ while ancestor is not None:
420
+ if ancestor.id in ts_node_to_class_id:
421
+ parent_id = ts_node_to_class_id[ancestor.id]
422
+ break
423
+ ancestor = ancestor.parent
424
+ try:
425
+ self.graph.add_edge(
426
+ parent_id, func_id, "contains", confidence=1.0
427
+ )
428
+ except ValueError:
429
+ pass
430
+
431
+ elif ntype in import_types:
432
+ for imp in _import_modules(ts_node, lang_name):
433
+ candidate_id = (
434
+ "file-" + imp.replace(".", "-").replace("/", "-") + "_py"
435
+ )
436
+ if candidate_id in self.graph._graph:
437
+ try:
438
+ self.graph.add_edge(
439
+ mod_id, candidate_id, "imports", confidence=0.8
440
+ )
441
+ except ValueError:
442
+ pass
443
+
444
+ def _scan_structural_single(self, path: Path, rel_path: str) -> None:
445
+ """Incremental re-extraction for a single changed file (used by file watcher).
446
+
447
+ Args:
448
+ path: Absolute path to the changed file.
449
+ rel_path: Path relative to project root.
450
+ """
451
+ from tree_sitter import Parser
452
+
453
+ file_node_id = f"file-{rel_path.replace('/', '-').replace('.', '_')}"
454
+
455
+ # Remove stale AST nodes for this file before re-adding
456
+ stale = [
457
+ n
458
+ for n in list(self.graph._graph.nodes)
459
+ if self.graph._graph.nodes[n].get("metadata", {}).get("path") == rel_path
460
+ and self.graph._graph.nodes[n].get("origin") == "ast"
461
+ ]
462
+ for s in stale:
463
+ self.graph._graph.remove_node(s)
464
+
465
+ file_node = ContextNode(
466
+ id=file_node_id,
467
+ node_type="file",
468
+ content=rel_path,
469
+ origin="ast",
470
+ metadata={"path": rel_path, "ext": path.suffix},
471
+ )
472
+ self.graph.add_node(file_node)
473
+
474
+ lang_name = _EXT_TO_LANG.get(path.suffix)
475
+ lang = _get_language(path.suffix)
476
+ if lang is None or lang_name is None:
477
+ return
478
+
479
+ try:
480
+ source = path.read_bytes()
481
+ except OSError:
482
+ return
483
+
484
+ try:
485
+ from tree_sitter import Parser
486
+
487
+ parser = Parser(lang)
488
+ tree = parser.parse(source)
489
+ except (ValueError, RuntimeError) as e:
490
+ logger.debug("tree_sitter_parse_failed", file=rel_path, error=str(e))
491
+ return
492
+
493
+ self._emit_ast_nodes(tree.root_node, file_node_id, rel_path, lang_name)
494
+
495
+ def _classify_criticality(
496
+ self, files: list[str]
497
+ ) -> dict[CriticalityLevel, list[str]]:
498
+ """Classify files into L1/L2/L3."""
499
+ classified: dict[CriticalityLevel, list[str]] = {"L1": [], "L2": [], "L3": []}
500
+
501
+ for rel_path in files:
502
+ # Heuristics
503
+ level: CriticalityLevel = "L2" # Default
504
+
505
+ parts = Path(rel_path).parts
506
+ name = Path(rel_path).name
507
+
508
+ if name in ["main.py", "app.py", "server.py", "manage.py"]:
509
+ level = "L1"
510
+ elif "tests" in parts or name.startswith("test_"):
511
+ level = "L3"
512
+ elif name.endswith((".md", ".json", ".yaml", ".toml", ".txt")):
513
+ level = "L3"
514
+ elif "core" in parts or "api" in parts:
515
+ level = "L1"
516
+
517
+ classified[level].append(rel_path)
518
+
519
+ return classified
520
+
521
+ def _mine_git_decisions(self, files: list[str]) -> None:
522
+ """Layer 2: Create decision nodes from git history."""
523
+ # Placeholder for git miner integration
524
+ pass
525
+
526
+ def _synthesize_decisions(self, files: list[str]) -> None:
527
+ """Layer 3: Create decision nodes from LLM analysis."""
528
+ # Placeholder for LLM synthesizer integration
529
+ pass
@@ -0,0 +1,50 @@
1
+ """Browser Tools for REF Agents.
2
+
3
+ This package provides browser automation capabilities for frontend E2E testing
4
+ using Playwright MCP. It includes:
5
+
6
+ - playwright_mcp_client: Client wrapper for Playwright MCP tools
7
+ - execution_logger: Audit trail logging for test execution
8
+ - evidence_verifier: Verify screenshots, logs, and timestamps
9
+ - screenshot_utils: Screenshot capture and management
10
+ - test_executor: Execute conceptual tests via browser tools
11
+ - report_generator: Generate test reports with evidence
12
+ """
13
+
14
+ from ref_agents.tools.browser.evidence_verifier import (
15
+ EvidenceVerifier,
16
+ VerificationResult,
17
+ )
18
+ from ref_agents.tools.browser.execution_logger import (
19
+ ExecutionLogEntry,
20
+ ExecutionLogger,
21
+ )
22
+ from ref_agents.tools.browser.screenshot_utils import (
23
+ get_screenshot_dir,
24
+ get_screenshot_path,
25
+ save_screenshot,
26
+ verify_screenshot_content,
27
+ )
28
+ from ref_agents.tools.browser.test_executor import (
29
+ BrowserTestExecutor,
30
+ ExecutionResult,
31
+ StepResult,
32
+ )
33
+
34
+ __all__ = [
35
+ # Evidence verification
36
+ "EvidenceVerifier",
37
+ "VerificationResult",
38
+ # Execution logging
39
+ "ExecutionLogEntry",
40
+ "ExecutionLogger",
41
+ # Screenshot utilities
42
+ "get_screenshot_dir",
43
+ "get_screenshot_path",
44
+ "save_screenshot",
45
+ "verify_screenshot_content",
46
+ # Test execution
47
+ "BrowserTestExecutor",
48
+ "ExecutionResult",
49
+ "StepResult",
50
+ ]