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,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}")