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.
- ref_agents/__init__.py +9 -0
- ref_agents/api_keys.json.example +8 -0
- ref_agents/auth.py +129 -0
- ref_agents/codemap/..md +62 -0
- ref_agents/codemap/CODE_MAP.md +37 -0
- ref_agents/codemap/core.md +43 -0
- ref_agents/codemap/models.md +43 -0
- ref_agents/codemap/prompts.md +40 -0
- ref_agents/codemap/security.md +45 -0
- ref_agents/codemap/tools.md +94 -0
- ref_agents/codemap/tools_browser.md +44 -0
- ref_agents/codemap/utils.md +42 -0
- ref_agents/codemap/workflow.md +42 -0
- ref_agents/config/ai_patterns.yaml +101 -0
- ref_agents/config/frameworks/angular.yaml +104 -0
- ref_agents/config/frameworks/aspnet.yaml +84 -0
- ref_agents/config/frameworks/ef_core.yaml +81 -0
- ref_agents/config/frameworks/react.yaml +111 -0
- ref_agents/config/frameworks/spring_boot.yaml +117 -0
- ref_agents/config/languages/csharp.yaml +153 -0
- ref_agents/config/languages/java.yaml +188 -0
- ref_agents/config/languages/javascript.yaml +172 -0
- ref_agents/config/languages/python.yaml +153 -0
- ref_agents/config/languages/typescript.yaml +193 -0
- ref_agents/constants.py +553 -0
- ref_agents/core/__init__.py +15 -0
- ref_agents/core/config_loader.py +160 -0
- ref_agents/core/config_models.py +167 -0
- ref_agents/core/config_parsing.py +84 -0
- ref_agents/core/language_detector.py +388 -0
- ref_agents/core/validation_models.py +66 -0
- ref_agents/core/validation_primitives.py +176 -0
- ref_agents/errors.py +34 -0
- ref_agents/license_client.py +247 -0
- ref_agents/models/__init__.py +22 -0
- ref_agents/models/gherkin.py +45 -0
- ref_agents/models/hierarchy.py +80 -0
- ref_agents/models/invest.py +59 -0
- ref_agents/models/version.py +49 -0
- ref_agents/prompts/__init__.py +9 -0
- ref_agents/prompts/start_agent.py +772 -0
- ref_agents/rules/architecture/backend_patterns.md +43 -0
- ref_agents/rules/architecture/diagramming.md +100 -0
- ref_agents/rules/architecture/frontend_patterns.md +40 -0
- ref_agents/rules/architecture/impact_analysis.md +129 -0
- ref_agents/rules/architecture/migration_strategy.md +208 -0
- ref_agents/rules/architecture/regression_protocol.md +77 -0
- ref_agents/rules/architecture/system_design.md +97 -0
- ref_agents/rules/common/codemap_standard.md +97 -0
- ref_agents/rules/common/core_protocol.md +59 -0
- ref_agents/rules/common/prompt_engineering.md +294 -0
- ref_agents/rules/development/debugging.md +32 -0
- ref_agents/rules/development/implementation.md +205 -0
- ref_agents/rules/operations/completion.md +119 -0
- ref_agents/rules/operations/cutover_protocol.md +218 -0
- ref_agents/rules/operations/discovery.md +179 -0
- ref_agents/rules/operations/fix_workflow.md +87 -0
- ref_agents/rules/operations/forensics.md +278 -0
- ref_agents/rules/operations/platform.md +263 -0
- ref_agents/rules/operations/synchronous_flow.md +25 -0
- ref_agents/rules/product/ac_validation.md +25 -0
- ref_agents/rules/product/brainstorming.md +27 -0
- ref_agents/rules/product/ref_flow.md +101 -0
- ref_agents/rules/product/requirements_std.md +114 -0
- ref_agents/rules/product/spec_writing.md +235 -0
- ref_agents/rules/product/strategy.md +96 -0
- ref_agents/rules/quality/documentation_standards.md +46 -0
- ref_agents/rules/quality/parity_testing.md +234 -0
- ref_agents/rules/quality/project_documentation.md +56 -0
- ref_agents/rules/quality/qa_lead.md +111 -0
- ref_agents/rules/quality/test_design.md +146 -0
- ref_agents/rules/quality/testing_standards.md +293 -0
- ref_agents/rules/review/pr_review.md +116 -0
- ref_agents/rules/security/security_audit.md +83 -0
- ref_agents/security/__init__.py +22 -0
- ref_agents/security/dependency_audit.py +188 -0
- ref_agents/security/file_audit.py +208 -0
- ref_agents/security/network_scan.py +179 -0
- ref_agents/security/report_generator.py +313 -0
- ref_agents/security/secret_scan.py +252 -0
- ref_agents/security/url_scan.py +240 -0
- ref_agents/security_scan.py +236 -0
- ref_agents/server.py +1586 -0
- ref_agents/session.py +100 -0
- ref_agents/tool_names.py +55 -0
- ref_agents/tools/__init__.py +8 -0
- ref_agents/tools/agents_generator.py +315 -0
- ref_agents/tools/ai_pattern_detector.py +815 -0
- ref_agents/tools/brownfield_populator.py +529 -0
- ref_agents/tools/browser/__init__.py +50 -0
- ref_agents/tools/browser/evidence_verifier.py +302 -0
- ref_agents/tools/browser/execution_logger.py +249 -0
- ref_agents/tools/browser/playwright_mcp_client.py +259 -0
- ref_agents/tools/browser/screenshot_utils.py +184 -0
- ref_agents/tools/browser/test_executor.py +537 -0
- ref_agents/tools/code_quality_scanner.py +629 -0
- ref_agents/tools/codemap/..md +93 -0
- ref_agents/tools/codemap/CODE_MAP.md +30 -0
- ref_agents/tools/codemap/browser.md +44 -0
- ref_agents/tools/codemap.py +403 -0
- ref_agents/tools/codemap_freshness.py +234 -0
- ref_agents/tools/comment_smell_scanner.py +346 -0
- ref_agents/tools/complexity.py +436 -0
- ref_agents/tools/complexity_ast.py +333 -0
- ref_agents/tools/compliance.py +246 -0
- ref_agents/tools/compliance_remediation.py +846 -0
- ref_agents/tools/context_graph.py +839 -0
- ref_agents/tools/context_manager.py +550 -0
- ref_agents/tools/context_tools.py +121 -0
- ref_agents/tools/cross_repo_linker.py +393 -0
- ref_agents/tools/dead_code_scanner.py +637 -0
- ref_agents/tools/debt_scanner.py +1092 -0
- ref_agents/tools/dependency_graph.py +272 -0
- ref_agents/tools/discovery_audit.py +372 -0
- ref_agents/tools/docs_scanner.py +600 -0
- ref_agents/tools/evaluate_gate.py +119 -0
- ref_agents/tools/external_detector.py +524 -0
- ref_agents/tools/features_generator.py +282 -0
- ref_agents/tools/flow_gap_detector.py +373 -0
- ref_agents/tools/flow_mapper.py +327 -0
- ref_agents/tools/full_suite_runner.py +740 -0
- ref_agents/tools/gherkin_parser.py +227 -0
- ref_agents/tools/guard_tools.py +139 -0
- ref_agents/tools/handoff_tools.py +282 -0
- ref_agents/tools/health_scanner.py +1211 -0
- ref_agents/tools/hierarchy_manager.py +289 -0
- ref_agents/tools/invest_scorer.py +249 -0
- ref_agents/tools/jira_confluence_export.py +306 -0
- ref_agents/tools/json_output.py +76 -0
- ref_agents/tools/migration_mapper.py +946 -0
- ref_agents/tools/migration_readiness_scanner.py +209 -0
- ref_agents/tools/pattern_learner.py +522 -0
- ref_agents/tools/report_utils.py +155 -0
- ref_agents/tools/requirements_serializer.py +225 -0
- ref_agents/tools/security_audit_tool.py +106 -0
- ref_agents/tools/sequencing_engine.py +288 -0
- ref_agents/tools/summary_generator.py +275 -0
- ref_agents/tools/symbol_resolver.py +306 -0
- ref_agents/tools/symbol_smoke_runner.py +336 -0
- ref_agents/tools/test_plan_validator.py +189 -0
- ref_agents/tools/test_smell_walker.py +902 -0
- ref_agents/tools/tier1_fixer.py +502 -0
- ref_agents/tools/validators/__init__.py +419 -0
- ref_agents/tools/validators/architect.py +268 -0
- ref_agents/tools/validators/cutover_engineer.py +167 -0
- ref_agents/tools/validators/developer.py +180 -0
- ref_agents/tools/validators/discovery.py +150 -0
- ref_agents/tools/validators/forensic_engineer.py +191 -0
- ref_agents/tools/validators/impact_architect.py +181 -0
- ref_agents/tools/validators/migration_planner.py +166 -0
- ref_agents/tools/validators/parity_tester.py +155 -0
- ref_agents/tools/validators/platform_engineer.py +134 -0
- ref_agents/tools/validators/pr_reviewer.py +129 -0
- ref_agents/tools/validators/product_manager.py +291 -0
- ref_agents/tools/validators/qa_lead.py +172 -0
- ref_agents/tools/validators/scrum_master.py +212 -0
- ref_agents/tools/validators/security_owner.py +162 -0
- ref_agents/tools/validators/specifier.py +134 -0
- ref_agents/tools/validators/strategist.py +149 -0
- ref_agents/tools/validators/tester.py +121 -0
- ref_agents/tools/version_manager.py +202 -0
- ref_agents/tools/workflow_tools.py +1549 -0
- ref_agents/utils/__init__.py +21 -0
- ref_agents/utils/git_utils.py +351 -0
- ref_agents/utils/handoff_logger.py +368 -0
- ref_agents/utils/ignore_matcher.py +270 -0
- ref_agents/workflow/__init__.py +19 -0
- ref_agents/workflow/capabilities.py +328 -0
- ref_agents/workflow/state_machine.py +708 -0
- ref_agents/workflow/transitions.py +658 -0
- ref_agents-1.0.0.dist-info/METADATA +365 -0
- ref_agents-1.0.0.dist-info/RECORD +175 -0
- ref_agents-1.0.0.dist-info/WHEEL +4 -0
- ref_agents-1.0.0.dist-info/entry_points.txt +2 -0
- 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
|
+
]
|