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,388 @@
|
|
|
1
|
+
"""Language Detection for REF Agents.
|
|
2
|
+
|
|
3
|
+
Detects programming language and framework from file extensions and content.
|
|
4
|
+
Used to load appropriate configuration for debt scanning and pattern detection.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
import json
|
|
12
|
+
|
|
13
|
+
import structlog
|
|
14
|
+
|
|
15
|
+
logger = structlog.get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class LanguageInfo:
|
|
20
|
+
"""Information about detected language and framework.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
language: Primary language (python, typescript, javascript, java).
|
|
24
|
+
framework: Detected framework (react, angular, spring_boot) or None.
|
|
25
|
+
extensions: File extensions for this language.
|
|
26
|
+
confidence: Detection confidence (0.0-1.0).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
language: str
|
|
30
|
+
framework: str | None = None
|
|
31
|
+
extensions: list[str] = field(default_factory=list)
|
|
32
|
+
confidence: float = 1.0
|
|
33
|
+
|
|
34
|
+
def __str__(self) -> str:
|
|
35
|
+
"""Return string representation."""
|
|
36
|
+
if self.framework:
|
|
37
|
+
return f"{self.language}/{self.framework}"
|
|
38
|
+
return self.language
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Language definitions
|
|
42
|
+
LANGUAGE_EXTENSIONS: dict[str, list[str]] = {
|
|
43
|
+
"python": [".py", ".pyw", ".pyi"],
|
|
44
|
+
"typescript": [".ts", ".tsx", ".mts", ".cts"],
|
|
45
|
+
"javascript": [".js", ".jsx", ".mjs", ".cjs"],
|
|
46
|
+
"java": [".java"],
|
|
47
|
+
"csharp": [".cs", ".csx"],
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Framework detection patterns (file content or project structure)
|
|
51
|
+
FRAMEWORK_PATTERNS: dict[str, dict[str, Any]] = {
|
|
52
|
+
"react": {
|
|
53
|
+
"languages": ["typescript", "javascript"],
|
|
54
|
+
"imports": [
|
|
55
|
+
r"from\s+['\"]react['\"]",
|
|
56
|
+
r"import\s+.*\s+from\s+['\"]react['\"]",
|
|
57
|
+
r"require\(['\"]react['\"]\)",
|
|
58
|
+
],
|
|
59
|
+
"files": ["package.json"],
|
|
60
|
+
"package_deps": ["react", "react-dom"],
|
|
61
|
+
},
|
|
62
|
+
"angular": {
|
|
63
|
+
"languages": ["typescript"],
|
|
64
|
+
"imports": [
|
|
65
|
+
r"from\s+['\"]@angular/",
|
|
66
|
+
r"import\s+.*\s+from\s+['\"]@angular/",
|
|
67
|
+
],
|
|
68
|
+
"files": ["angular.json", "package.json"],
|
|
69
|
+
"package_deps": ["@angular/core"],
|
|
70
|
+
"decorators": ["@Component", "@NgModule", "@Injectable"],
|
|
71
|
+
},
|
|
72
|
+
"spring_boot": {
|
|
73
|
+
"languages": ["java"],
|
|
74
|
+
"imports": [
|
|
75
|
+
r"import\s+org\.springframework\.",
|
|
76
|
+
r"import\s+org\.springframework\.boot\.",
|
|
77
|
+
],
|
|
78
|
+
"files": ["pom.xml", "build.gradle"],
|
|
79
|
+
"annotations": [
|
|
80
|
+
"@SpringBootApplication",
|
|
81
|
+
"@RestController",
|
|
82
|
+
"@Service",
|
|
83
|
+
"@Repository",
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
"aspnet": {
|
|
87
|
+
"languages": ["csharp"],
|
|
88
|
+
# .csproj substring signals (rglob-based, not root-only)
|
|
89
|
+
"csproj_contains": [
|
|
90
|
+
"Microsoft.AspNetCore",
|
|
91
|
+
'Sdk="Microsoft.NET.Sdk.Web"',
|
|
92
|
+
],
|
|
93
|
+
"imports": [
|
|
94
|
+
r"using\s+Microsoft\.AspNetCore\.",
|
|
95
|
+
r"using\s+Microsoft\.Extensions\.",
|
|
96
|
+
],
|
|
97
|
+
"annotations": ["[ApiController]", "[HttpGet", "[HttpPost", "[Authorize]"],
|
|
98
|
+
},
|
|
99
|
+
"ef_core": {
|
|
100
|
+
"languages": ["csharp"],
|
|
101
|
+
"csproj_contains": ["Microsoft.EntityFrameworkCore"],
|
|
102
|
+
"imports": [r"using\s+Microsoft\.EntityFrameworkCore"],
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class LanguageDetector:
|
|
108
|
+
"""Detects programming language and framework from files.
|
|
109
|
+
|
|
110
|
+
Attributes:
|
|
111
|
+
project_root: Root directory of the project.
|
|
112
|
+
_framework_cache: Cached framework detection results.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
def __init__(self, project_root: Path | None = None) -> None:
|
|
116
|
+
"""Initialize language detector.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
project_root: Project root for framework detection. Defaults to cwd.
|
|
120
|
+
"""
|
|
121
|
+
self.project_root = project_root or Path.cwd()
|
|
122
|
+
self._framework_cache: dict[str, str | None] = {}
|
|
123
|
+
|
|
124
|
+
def detect_from_extension(self, file_path: Path | str) -> str | None:
|
|
125
|
+
"""Detect language from file extension.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
file_path: Path to file.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Language name or None if unknown.
|
|
132
|
+
"""
|
|
133
|
+
path = Path(file_path)
|
|
134
|
+
ext = path.suffix.lower()
|
|
135
|
+
|
|
136
|
+
for language, extensions in LANGUAGE_EXTENSIONS.items():
|
|
137
|
+
if ext in extensions:
|
|
138
|
+
return language
|
|
139
|
+
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
def detect_framework_from_content(
|
|
143
|
+
self,
|
|
144
|
+
content: str,
|
|
145
|
+
language: str,
|
|
146
|
+
) -> str | None:
|
|
147
|
+
"""Detect framework from file content.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
content: File content.
|
|
151
|
+
language: Detected language.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Framework name or None.
|
|
155
|
+
"""
|
|
156
|
+
for framework, patterns in FRAMEWORK_PATTERNS.items():
|
|
157
|
+
if language not in patterns.get("languages", []):
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
# Check import patterns
|
|
161
|
+
for pattern in patterns.get("imports", []):
|
|
162
|
+
if re.search(pattern, content):
|
|
163
|
+
logger.debug(
|
|
164
|
+
"framework_detected_from_import",
|
|
165
|
+
framework=framework,
|
|
166
|
+
pattern=pattern,
|
|
167
|
+
)
|
|
168
|
+
return framework
|
|
169
|
+
|
|
170
|
+
# Check decorators/annotations
|
|
171
|
+
for decorator in patterns.get("decorators", []) + patterns.get(
|
|
172
|
+
"annotations", []
|
|
173
|
+
):
|
|
174
|
+
if decorator in content:
|
|
175
|
+
logger.debug(
|
|
176
|
+
"framework_detected_from_decorator",
|
|
177
|
+
framework=framework,
|
|
178
|
+
decorator=decorator,
|
|
179
|
+
)
|
|
180
|
+
return framework
|
|
181
|
+
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
def _check_package_json_deps(
|
|
185
|
+
self, file_path: Path, deps_to_check: list[str]
|
|
186
|
+
) -> bool:
|
|
187
|
+
"""Check if package.json contains specific dependencies."""
|
|
188
|
+
try:
|
|
189
|
+
pkg = json.loads(file_path.read_text())
|
|
190
|
+
deps = {
|
|
191
|
+
**pkg.get("dependencies", {}),
|
|
192
|
+
**pkg.get("devDependencies", {}),
|
|
193
|
+
}
|
|
194
|
+
for dep in deps_to_check:
|
|
195
|
+
if dep in deps:
|
|
196
|
+
return True
|
|
197
|
+
except (json.JSONDecodeError, OSError):
|
|
198
|
+
pass
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
def _check_csproj_framework(self, substrings: list[str]) -> bool:
|
|
202
|
+
"""Check if any .csproj under project root contains all given substrings.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
substrings: Strings that must appear in at least one .csproj file.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
True if any .csproj contains at least one of the substrings.
|
|
209
|
+
"""
|
|
210
|
+
for csproj in self.project_root.rglob("*.csproj"):
|
|
211
|
+
try:
|
|
212
|
+
content = csproj.read_text(encoding="utf-8")
|
|
213
|
+
except OSError:
|
|
214
|
+
continue
|
|
215
|
+
if any(sub in content for sub in substrings):
|
|
216
|
+
logger.debug(
|
|
217
|
+
"framework_detected_from_csproj",
|
|
218
|
+
file=str(csproj),
|
|
219
|
+
)
|
|
220
|
+
return True
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
def _check_framework_match(self, framework: str, patterns: dict[str, Any]) -> bool:
|
|
224
|
+
"""Check if project matches framework patterns."""
|
|
225
|
+
# .NET frameworks: use rglob on .csproj (files are in subdirectories)
|
|
226
|
+
csproj_signals: list[str] = patterns.get("csproj_contains", [])
|
|
227
|
+
if csproj_signals and self._check_csproj_framework(csproj_signals):
|
|
228
|
+
return True
|
|
229
|
+
|
|
230
|
+
for filename in patterns.get("files", []):
|
|
231
|
+
file_path = self.project_root / filename
|
|
232
|
+
if not file_path.exists():
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
# For package.json, check dependencies
|
|
236
|
+
if filename == "package.json":
|
|
237
|
+
if self._check_package_json_deps(
|
|
238
|
+
file_path, patterns.get("package_deps", [])
|
|
239
|
+
):
|
|
240
|
+
return True
|
|
241
|
+
# For pom.xml/build.gradle, presence is enough
|
|
242
|
+
elif filename in ("pom.xml", "build.gradle", "angular.json"):
|
|
243
|
+
if filename == "angular.json":
|
|
244
|
+
return True
|
|
245
|
+
elif framework == "spring_boot":
|
|
246
|
+
try:
|
|
247
|
+
build_content = file_path.read_text()
|
|
248
|
+
if "spring-boot" in build_content.lower():
|
|
249
|
+
return True
|
|
250
|
+
except OSError:
|
|
251
|
+
pass
|
|
252
|
+
return False
|
|
253
|
+
|
|
254
|
+
def detect_framework_from_project(self) -> str | None:
|
|
255
|
+
"""Detect framework from project structure.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Framework name or None.
|
|
259
|
+
"""
|
|
260
|
+
# Check cache
|
|
261
|
+
cache_key = str(self.project_root)
|
|
262
|
+
if cache_key in self._framework_cache:
|
|
263
|
+
return self._framework_cache[cache_key]
|
|
264
|
+
|
|
265
|
+
detected: str | None = None
|
|
266
|
+
|
|
267
|
+
for framework, patterns in FRAMEWORK_PATTERNS.items():
|
|
268
|
+
if self._check_framework_match(framework, patterns):
|
|
269
|
+
detected = framework
|
|
270
|
+
break
|
|
271
|
+
|
|
272
|
+
self._framework_cache[cache_key] = detected
|
|
273
|
+
return detected
|
|
274
|
+
|
|
275
|
+
def detect(self, file_path: Path | str) -> LanguageInfo:
|
|
276
|
+
"""Detect language and framework for a file.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
file_path: Path to file.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
LanguageInfo with detected language and framework.
|
|
283
|
+
"""
|
|
284
|
+
path = Path(file_path)
|
|
285
|
+
|
|
286
|
+
# Detect language from extension
|
|
287
|
+
language = self.detect_from_extension(path)
|
|
288
|
+
|
|
289
|
+
if not language:
|
|
290
|
+
logger.debug("unknown_language", file=str(path))
|
|
291
|
+
return LanguageInfo(
|
|
292
|
+
language="unknown",
|
|
293
|
+
confidence=0.0,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
# Detect framework from project structure first (cached)
|
|
297
|
+
framework = self.detect_framework_from_project()
|
|
298
|
+
|
|
299
|
+
# If no framework from project, try from file content
|
|
300
|
+
if not framework and path.exists():
|
|
301
|
+
try:
|
|
302
|
+
content = path.read_text(encoding="utf-8")
|
|
303
|
+
framework = self.detect_framework_from_content(content, language)
|
|
304
|
+
except (OSError, UnicodeDecodeError):
|
|
305
|
+
pass
|
|
306
|
+
|
|
307
|
+
# Validate framework matches language
|
|
308
|
+
if framework:
|
|
309
|
+
expected_langs = FRAMEWORK_PATTERNS.get(framework, {}).get("languages", [])
|
|
310
|
+
if language not in expected_langs:
|
|
311
|
+
framework = None
|
|
312
|
+
|
|
313
|
+
return LanguageInfo(
|
|
314
|
+
language=language,
|
|
315
|
+
framework=framework,
|
|
316
|
+
extensions=LANGUAGE_EXTENSIONS.get(language, []),
|
|
317
|
+
confidence=1.0 if language != "unknown" else 0.0,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
def detect_directory(self, directory: Path | str) -> dict[str, int]:
|
|
321
|
+
"""Detect language distribution in a directory.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
directory: Directory to scan.
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
Dict mapping language to file count.
|
|
328
|
+
"""
|
|
329
|
+
dir_path = Path(directory)
|
|
330
|
+
counts: dict[str, int] = {}
|
|
331
|
+
|
|
332
|
+
if not dir_path.exists():
|
|
333
|
+
return counts
|
|
334
|
+
|
|
335
|
+
exclude_dirs = {
|
|
336
|
+
"__pycache__",
|
|
337
|
+
"node_modules",
|
|
338
|
+
".git",
|
|
339
|
+
".venv",
|
|
340
|
+
"venv",
|
|
341
|
+
"dist",
|
|
342
|
+
"build",
|
|
343
|
+
"target",
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
for ext_list in LANGUAGE_EXTENSIONS.values():
|
|
347
|
+
for ext in ext_list:
|
|
348
|
+
for file_path in dir_path.rglob(f"*{ext}"):
|
|
349
|
+
if any(excluded in file_path.parts for excluded in exclude_dirs):
|
|
350
|
+
continue
|
|
351
|
+
|
|
352
|
+
info = self.detect(file_path)
|
|
353
|
+
lang_key = str(info)
|
|
354
|
+
counts[lang_key] = counts.get(lang_key, 0) + 1
|
|
355
|
+
|
|
356
|
+
return counts
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def detect_language(file_path: str) -> str:
|
|
360
|
+
"""Detect language for a file (convenience function).
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
file_path: Path to file.
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
Language string (e.g., "python", "typescript/react").
|
|
367
|
+
"""
|
|
368
|
+
detector = LanguageDetector()
|
|
369
|
+
info = detector.detect(file_path)
|
|
370
|
+
return str(info)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def get_supported_languages() -> list[str]:
|
|
374
|
+
"""Get list of supported languages.
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
List of language names.
|
|
378
|
+
"""
|
|
379
|
+
return list(LANGUAGE_EXTENSIONS.keys())
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def get_supported_frameworks() -> list[str]:
|
|
383
|
+
"""Get list of supported frameworks.
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
List of framework names.
|
|
387
|
+
"""
|
|
388
|
+
return list(FRAMEWORK_PATTERNS.keys())
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Pydantic models for role-output validation reports.
|
|
2
|
+
|
|
3
|
+
Used by the `validate` MCP dispatcher and per-role validator modules in
|
|
4
|
+
`ref_agents.tools.validators.*`. See SPEC-STORY-021 for full contract.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Verdict(str, Enum):
|
|
13
|
+
"""Outcome of a role-output validation pass."""
|
|
14
|
+
|
|
15
|
+
PASS = "PASS"
|
|
16
|
+
FAIL_AUTO = "FAIL_AUTO"
|
|
17
|
+
NEEDS_USER = "NEEDS_USER"
|
|
18
|
+
HALT = "HALT"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Gap(BaseModel):
|
|
22
|
+
"""One reason a validator did not return PASS.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
code: Machine-readable identifier (e.g., "ac_count_lt_3").
|
|
26
|
+
detail: Human-readable explanation.
|
|
27
|
+
location: Optional path / line / artifact pointer.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
code: str
|
|
31
|
+
detail: str
|
|
32
|
+
location: str | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class UserQuestion(BaseModel):
|
|
36
|
+
"""Question surfaced to the user when verdict is NEEDS_USER.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
text: Question prompt.
|
|
40
|
+
options: Optional enumerated answer choices.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
text: str
|
|
44
|
+
options: list[str] | None = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ValidationReport(BaseModel):
|
|
48
|
+
"""Outcome of a single `validate(role=...)` call.
|
|
49
|
+
|
|
50
|
+
Attributes:
|
|
51
|
+
verdict: Overall outcome.
|
|
52
|
+
score: Optional 0-5 quality score (PASS-style validators only).
|
|
53
|
+
gaps: List of failures preventing PASS.
|
|
54
|
+
halt_reason: Set when verdict == HALT.
|
|
55
|
+
user_questions: Set when verdict == NEEDS_USER.
|
|
56
|
+
validator_version: Identifier of the validator that produced this
|
|
57
|
+
report. Defaults to project version at evaluation time. Used
|
|
58
|
+
for audit bisection across validator revisions.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
verdict: Verdict
|
|
62
|
+
score: float | None = None
|
|
63
|
+
gaps: list[Gap] = Field(default_factory=list)
|
|
64
|
+
halt_reason: str | None = None
|
|
65
|
+
user_questions: list[UserQuestion] = Field(default_factory=list)
|
|
66
|
+
validator_version: str = "3.13.2"
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Shared primitives for role-output validators.
|
|
2
|
+
|
|
3
|
+
Used by per-role validator modules in `ref_agents.tools.validators.*`.
|
|
4
|
+
Each primitive is pure and deterministic — given identical inputs it
|
|
5
|
+
returns identical outputs (no time, no randomness, no I/O beyond reading
|
|
6
|
+
the artifact file passed in).
|
|
7
|
+
|
|
8
|
+
See SPEC-STORY-021 / DESIGN-STORY-021 for the full contract.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
from collections.abc import Callable, Iterable
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from pydantic import BaseModel, ValidationError
|
|
18
|
+
|
|
19
|
+
from ref_agents.errors import CycleDetectedError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ValidationOutcome(BaseModel):
|
|
23
|
+
"""Result of `pydantic_schema_validator`."""
|
|
24
|
+
|
|
25
|
+
ok: bool
|
|
26
|
+
error: str | None = None
|
|
27
|
+
data: dict[str, Any] | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BijectionResult(BaseModel):
|
|
31
|
+
"""Result of `bijection_checker`."""
|
|
32
|
+
|
|
33
|
+
missing_in_a: list[str]
|
|
34
|
+
missing_in_b: list[str]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def markdown_section_checker(path: Path, required_sections: list[str]) -> list[str]:
|
|
38
|
+
"""Return required Markdown sections missing from a file.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
path: Path to a Markdown file.
|
|
42
|
+
required_sections: Section titles (without leading "#" markers).
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Required sections not found in the file, in input order.
|
|
46
|
+
Empty list = all sections present.
|
|
47
|
+
"""
|
|
48
|
+
if not required_sections:
|
|
49
|
+
return []
|
|
50
|
+
|
|
51
|
+
content = path.read_text(encoding="utf-8")
|
|
52
|
+
headers = set(re.findall(r"^#{1,6}\s+(.+?)\s*$", content, flags=re.MULTILINE))
|
|
53
|
+
return [s for s in required_sections if s not in headers]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def regex_forbidden_scanner(path: Path, patterns: list[str]) -> list[tuple[str, int]]:
|
|
57
|
+
"""Scan a file for forbidden regex patterns.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
path: Path to the file to scan.
|
|
61
|
+
patterns: List of regex patterns; any match counts as forbidden.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
List of (pattern, line_number) tuples for each match.
|
|
65
|
+
Empty list = no forbidden tokens present.
|
|
66
|
+
"""
|
|
67
|
+
if not patterns:
|
|
68
|
+
return []
|
|
69
|
+
|
|
70
|
+
compiled = [(p, re.compile(p)) for p in patterns]
|
|
71
|
+
matches: list[tuple[str, int]] = []
|
|
72
|
+
|
|
73
|
+
with path.open("r", encoding="utf-8") as f:
|
|
74
|
+
for lineno, line in enumerate(f, start=1):
|
|
75
|
+
for pattern, regex in compiled:
|
|
76
|
+
if regex.search(line):
|
|
77
|
+
matches.append((pattern, lineno))
|
|
78
|
+
return matches
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def pydantic_schema_validator(path: Path, model: type[BaseModel]) -> ValidationOutcome:
|
|
82
|
+
"""Validate a JSON file against a pydantic model.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
path: Path to a JSON file.
|
|
86
|
+
model: Pydantic model class to validate against.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
ValidationOutcome with `ok=True` + parsed data on success,
|
|
90
|
+
or `ok=False` + structured error on failure. Never raises.
|
|
91
|
+
"""
|
|
92
|
+
try:
|
|
93
|
+
raw = path.read_text(encoding="utf-8")
|
|
94
|
+
except OSError as exc:
|
|
95
|
+
return ValidationOutcome(ok=False, error=f"file_read_failed: {exc}")
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
data = json.loads(raw)
|
|
99
|
+
except json.JSONDecodeError as exc:
|
|
100
|
+
return ValidationOutcome(ok=False, error=f"json_decode_failed: {exc}")
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
parsed = model.model_validate(data)
|
|
104
|
+
except ValidationError as exc:
|
|
105
|
+
return ValidationOutcome(ok=False, error=exc.json())
|
|
106
|
+
|
|
107
|
+
return ValidationOutcome(ok=True, data=parsed.model_dump())
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def bijection_checker(
|
|
111
|
+
set_a: Iterable[Any],
|
|
112
|
+
set_b: Iterable[Any],
|
|
113
|
+
key_fn_a: Callable[[Any], str],
|
|
114
|
+
key_fn_b: Callable[[Any], str],
|
|
115
|
+
) -> BijectionResult:
|
|
116
|
+
"""Compute bidirectional difference between two iterables.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
set_a: First iterable.
|
|
120
|
+
set_b: Second iterable.
|
|
121
|
+
key_fn_a: Function deriving comparison key from each set_a element.
|
|
122
|
+
key_fn_b: Function deriving comparison key from each set_b element.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
BijectionResult with `missing_in_a` (keys present in b but not a)
|
|
126
|
+
and `missing_in_b` (keys present in a but not b). Sorted for
|
|
127
|
+
deterministic output.
|
|
128
|
+
"""
|
|
129
|
+
keys_a = {key_fn_a(x) for x in set_a}
|
|
130
|
+
keys_b = {key_fn_b(x) for x in set_b}
|
|
131
|
+
return BijectionResult(
|
|
132
|
+
missing_in_a=sorted(keys_b - keys_a),
|
|
133
|
+
missing_in_b=sorted(keys_a - keys_b),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def dag_validator(nodes: list[str], edges: list[tuple[str, str]]) -> None:
|
|
138
|
+
"""Verify a directed graph is acyclic.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
nodes: All node identifiers.
|
|
142
|
+
edges: Directed edges as (from, to) tuples.
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
CycleDetectedError: If any cycle exists. `path` attribute holds
|
|
146
|
+
the ordered cycle node list.
|
|
147
|
+
"""
|
|
148
|
+
if not nodes:
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
adjacency: dict[str, list[str]] = {n: [] for n in nodes}
|
|
152
|
+
for src, dst in edges:
|
|
153
|
+
if src in adjacency:
|
|
154
|
+
adjacency[src].append(dst)
|
|
155
|
+
|
|
156
|
+
WHITE, GRAY, BLACK = 0, 1, 2
|
|
157
|
+
color: dict[str, int] = {n: WHITE for n in nodes}
|
|
158
|
+
stack: list[str] = []
|
|
159
|
+
|
|
160
|
+
def visit(node: str) -> None:
|
|
161
|
+
color[node] = GRAY
|
|
162
|
+
stack.append(node)
|
|
163
|
+
for neighbor in adjacency.get(node, []):
|
|
164
|
+
if neighbor not in color:
|
|
165
|
+
continue
|
|
166
|
+
if color[neighbor] == GRAY:
|
|
167
|
+
cycle_start = stack.index(neighbor)
|
|
168
|
+
raise CycleDetectedError(path=stack[cycle_start:] + [neighbor])
|
|
169
|
+
if color[neighbor] == WHITE:
|
|
170
|
+
visit(neighbor)
|
|
171
|
+
stack.pop()
|
|
172
|
+
color[node] = BLACK
|
|
173
|
+
|
|
174
|
+
for node in nodes:
|
|
175
|
+
if color[node] == WHITE:
|
|
176
|
+
visit(node)
|
ref_agents/errors.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Common exception hierarchy for REF Agents.
|
|
2
|
+
|
|
3
|
+
All REF-specific exceptions derive from `AppError`. Use specific subclasses
|
|
4
|
+
rather than bare `Exception`. Add new subclasses here when a new error
|
|
5
|
+
category appears (avoid one-off ad-hoc classes scattered across modules).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AppError(Exception):
|
|
10
|
+
"""Base class for all REF Agents application errors."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CycleDetectedError(AppError):
|
|
14
|
+
"""Raised when a DAG validator finds a cycle.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
path: Ordered list of node IDs forming the cycle.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, path: list[str], message: str | None = None) -> None:
|
|
21
|
+
self.path = path
|
|
22
|
+
super().__init__(message or f"Cycle detected: {' -> '.join(path)}")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ValidatorLoadError(AppError):
|
|
26
|
+
"""Raised when a registered role validator fails to import.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
role: Role name whose validator failed to load.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, role: str, message: str | None = None) -> None:
|
|
33
|
+
self.role = role
|
|
34
|
+
super().__init__(message or f"Failed to load validator for role: {role}")
|