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,302 @@
1
+ """Evidence verifier for browser test results.
2
+
3
+ Verifies all test evidence (screenshots, logs, timestamps)
4
+ to ensure test results are not hallucinated.
5
+ """
6
+
7
+ from dataclasses import dataclass, field
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+
11
+ import structlog
12
+
13
+ from ref_agents.tools.browser.execution_logger import ExecutionLogger
14
+ from ref_agents.tools.browser.screenshot_utils import (
15
+ get_screenshot_dir,
16
+ get_screenshot_mtime,
17
+ verify_screenshot_content,
18
+ verify_screenshot_exists,
19
+ )
20
+
21
+ logger = structlog.get_logger(__name__)
22
+
23
+
24
+ @dataclass
25
+ class VerificationResult:
26
+ """Result of evidence verification.
27
+
28
+ Attributes:
29
+ valid: Whether all evidence is valid.
30
+ issues: List of issues found.
31
+ screenshot_count: Number of screenshots verified.
32
+ log_entry_count: Number of log entries verified.
33
+ warnings: Non-critical issues.
34
+ """
35
+
36
+ valid: bool
37
+ issues: list[str] = field(default_factory=list)
38
+ screenshot_count: int = 0
39
+ log_entry_count: int = 0
40
+ warnings: list[str] = field(default_factory=list)
41
+
42
+ def to_dict(self) -> dict:
43
+ """Convert to dictionary."""
44
+ return {
45
+ "valid": self.valid,
46
+ "issues": self.issues,
47
+ "screenshot_count": self.screenshot_count,
48
+ "log_entry_count": self.log_entry_count,
49
+ "warnings": self.warnings,
50
+ }
51
+
52
+
53
+ class EvidenceVerifier:
54
+ """Verify test execution evidence.
55
+
56
+ Ensures test results are backed by real evidence:
57
+ - Screenshots exist and are valid
58
+ - Execution log exists and is consistent
59
+ - Timestamps are sequential and realistic
60
+ """
61
+
62
+ def __init__(self, story_id: str, base_dir: Path | None = None):
63
+ """Initialize evidence verifier.
64
+
65
+ Args:
66
+ story_id: Story identifier.
67
+ base_dir: Base directory for evidence files.
68
+ """
69
+ self.story_id = story_id
70
+
71
+ if base_dir is None:
72
+ from ref_agents.session import SessionManager
73
+
74
+ session = SessionManager.get()
75
+ base_dir = session.project_root or Path.cwd()
76
+
77
+ self.base_dir = base_dir
78
+ self.execution_logger = ExecutionLogger(story_id, base_dir)
79
+ self.screenshot_dir = get_screenshot_dir(story_id, base_dir)
80
+
81
+ def verify_screenshot(self, path: Path) -> tuple[bool, str]:
82
+ """Verify a single screenshot file.
83
+
84
+ Args:
85
+ path: Path to screenshot file.
86
+
87
+ Returns:
88
+ Tuple of (valid, error_message).
89
+ """
90
+ # First check existence
91
+ valid, error = verify_screenshot_exists(path)
92
+ if not valid:
93
+ return False, error
94
+
95
+ # Then check content
96
+ valid, error = verify_screenshot_content(path)
97
+ if not valid:
98
+ return False, error
99
+
100
+ return True, ""
101
+
102
+ def verify_all_screenshots(self) -> tuple[bool, list[str], int]:
103
+ """Verify all screenshots in the screenshot directory.
104
+
105
+ Returns:
106
+ Tuple of (all_valid, issues, count).
107
+ """
108
+ issues = []
109
+ count = 0
110
+
111
+ if not self.screenshot_dir.exists():
112
+ return False, ["Screenshot directory does not exist"], 0
113
+
114
+ screenshots = list(self.screenshot_dir.glob("*.png"))
115
+
116
+ if not screenshots:
117
+ return False, ["No screenshots found"], 0
118
+
119
+ for screenshot in screenshots:
120
+ valid, error = self.verify_screenshot(screenshot)
121
+ if valid:
122
+ count += 1
123
+ else:
124
+ issues.append(error)
125
+
126
+ return len(issues) == 0, issues, count
127
+
128
+ def verify_log_integrity(self) -> tuple[bool, list[str]]:
129
+ """Verify execution log is valid.
130
+
131
+ Returns:
132
+ Tuple of (valid, issues).
133
+ """
134
+ return self.execution_logger.verify_log_integrity()
135
+
136
+ def verify_timestamp_consistency(self) -> tuple[bool, list[str]]:
137
+ """Verify timestamps are consistent.
138
+
139
+ Checks:
140
+ - Log timestamps are sequential
141
+ - Log timestamps are realistic (not too fast/slow)
142
+ - Screenshot mtimes are close to log timestamps
143
+
144
+ Returns:
145
+ Tuple of (valid, issues).
146
+ """
147
+ issues = []
148
+ entries = self.execution_logger.read_log()
149
+
150
+ if not entries:
151
+ return False, ["No log entries to verify"]
152
+
153
+ prev_timestamp = None
154
+
155
+ for entry in entries:
156
+ try:
157
+ timestamp = datetime.fromisoformat(entry.timestamp)
158
+
159
+ # Check sequential
160
+ if prev_timestamp:
161
+ if timestamp < prev_timestamp:
162
+ issues.append(
163
+ f"Timestamp not sequential: {entry.test_id} step {entry.step_number}"
164
+ )
165
+
166
+ # Check realistic duration (between 50ms and 5 min)
167
+ duration = (timestamp - prev_timestamp).total_seconds()
168
+ if duration < 0.05: # noqa: PLR2004 # TECH_DEBT: extract to named constant — G25
169
+ issues.append(
170
+ f"Suspiciously fast execution: {duration:.3f}s for {entry.test_id} step {entry.step_number}"
171
+ )
172
+ elif duration > 300: # noqa: PLR2004 # TECH_DEBT: extract to named constant — G25
173
+ issues.append(
174
+ f"Suspiciously slow execution: {duration:.1f}s for {entry.test_id} step {entry.step_number}"
175
+ )
176
+
177
+ # Check screenshot timestamp if available
178
+ if entry.screenshot_path:
179
+ screenshot_path = Path(entry.screenshot_path)
180
+ if screenshot_path.exists():
181
+ try:
182
+ mtime = get_screenshot_mtime(screenshot_path)
183
+ screenshot_time = datetime.fromtimestamp(mtime)
184
+ time_diff = abs(
185
+ (timestamp - screenshot_time).total_seconds()
186
+ )
187
+
188
+ if (
189
+ time_diff > 10 # noqa: PLR2004 # TECH_DEBT: extract to named constant — G25
190
+ ): # More than 10 seconds difference # noqa: PLR2004 # TECH_DEBT: extract to named constant — G25
191
+ issues.append(
192
+ f"Screenshot mtime mismatch: {time_diff:.1f}s diff for {entry.test_id} step {entry.step_number}"
193
+ )
194
+ except FileNotFoundError:
195
+ pass # Already caught by screenshot verification
196
+
197
+ prev_timestamp = timestamp
198
+
199
+ except ValueError as e:
200
+ issues.append(f"Invalid timestamp: {entry.timestamp} - {e}")
201
+
202
+ return len(issues) == 0, issues
203
+
204
+ def verify_log_screenshot_consistency(self) -> tuple[bool, list[str]]:
205
+ """Verify log entries have corresponding screenshots.
206
+
207
+ Returns:
208
+ Tuple of (valid, issues).
209
+ """
210
+ issues = []
211
+ entries = self.execution_logger.read_log()
212
+
213
+ for entry in entries:
214
+ if entry.screenshot_path:
215
+ screenshot = Path(entry.screenshot_path)
216
+ if not screenshot.exists():
217
+ issues.append(
218
+ f"Screenshot missing: {entry.screenshot_path} for {entry.test_id} step {entry.step_number}"
219
+ )
220
+ elif screenshot.stat().st_size != entry.screenshot_size:
221
+ issues.append(
222
+ f"Screenshot size mismatch: {entry.test_id} step {entry.step_number}"
223
+ )
224
+
225
+ return len(issues) == 0, issues
226
+
227
+ def cross_validate(self) -> VerificationResult:
228
+ """Cross-validate all evidence sources.
229
+
230
+ Performs comprehensive verification:
231
+ 1. Verify all screenshots exist and are valid
232
+ 2. Verify execution log integrity
233
+ 3. Verify timestamp consistency
234
+ 4. Verify log-screenshot consistency
235
+
236
+ Returns:
237
+ VerificationResult with all findings.
238
+ """
239
+ all_issues = []
240
+ warnings = []
241
+
242
+ # 1. Verify screenshots
243
+ screenshots_valid, screenshot_issues, screenshot_count = (
244
+ self.verify_all_screenshots()
245
+ )
246
+ if not screenshots_valid:
247
+ all_issues.extend(screenshot_issues)
248
+
249
+ # 2. Verify log integrity
250
+ log_valid, log_issues = self.verify_log_integrity()
251
+ if not log_valid:
252
+ all_issues.extend(log_issues)
253
+
254
+ log_entry_count = self.execution_logger.get_entry_count()
255
+
256
+ # 3. Verify timestamp consistency
257
+ timestamp_valid, timestamp_issues = self.verify_timestamp_consistency()
258
+ if not timestamp_valid:
259
+ # Timestamp issues are warnings, not critical
260
+ warnings.extend(timestamp_issues)
261
+
262
+ # 4. Verify log-screenshot consistency
263
+ consistency_valid, consistency_issues = self.verify_log_screenshot_consistency()
264
+ if not consistency_valid:
265
+ all_issues.extend(consistency_issues)
266
+
267
+ valid = len(all_issues) == 0
268
+
269
+ result = VerificationResult(
270
+ valid=valid,
271
+ issues=all_issues,
272
+ screenshot_count=screenshot_count,
273
+ log_entry_count=log_entry_count,
274
+ warnings=warnings,
275
+ )
276
+
277
+ logger.info(
278
+ "evidence_verification_complete",
279
+ valid=valid,
280
+ screenshot_count=screenshot_count,
281
+ log_entry_count=log_entry_count,
282
+ issue_count=len(all_issues),
283
+ warning_count=len(warnings),
284
+ )
285
+
286
+ return result
287
+
288
+
289
+ def verify_test_evidence(
290
+ story_id: str, base_dir: Path | None = None
291
+ ) -> VerificationResult:
292
+ """Verify all test evidence for a story.
293
+
294
+ Args:
295
+ story_id: Story identifier.
296
+ base_dir: Base directory for evidence files.
297
+
298
+ Returns:
299
+ VerificationResult with all findings.
300
+ """
301
+ verifier = EvidenceVerifier(story_id, base_dir)
302
+ return verifier.cross_validate()
@@ -0,0 +1,249 @@
1
+ """Execution logger for browser test audit trail.
2
+
3
+ Logs all MCP tool calls with timestamps, arguments, responses,
4
+ and screenshot paths for complete audit trail.
5
+ """
6
+
7
+ import json
8
+ from dataclasses import asdict, dataclass, field
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+
12
+ import structlog
13
+
14
+ logger = structlog.get_logger(__name__)
15
+
16
+
17
+ @dataclass
18
+ class ExecutionLogEntry:
19
+ """Single execution log entry.
20
+
21
+ Attributes:
22
+ timestamp: ISO8601 timestamp of execution.
23
+ test_id: Test case identifier.
24
+ step_number: Step number within test case.
25
+ tool_name: Name of MCP tool called.
26
+ tool_args: Arguments passed to tool.
27
+ response: Response from tool.
28
+ screenshot_path: Path to captured screenshot.
29
+ screenshot_size: Size of screenshot file in bytes.
30
+ duration_ms: Execution duration in milliseconds.
31
+ status: Step status (PASS/FAIL).
32
+ error: Error message if failed.
33
+ """
34
+
35
+ timestamp: str
36
+ test_id: str
37
+ step_number: int
38
+ tool_name: str
39
+ tool_args: dict = field(default_factory=dict)
40
+ response: dict = field(default_factory=dict)
41
+ screenshot_path: str | None = None
42
+ screenshot_size: int = 0
43
+ duration_ms: int = 0
44
+ status: str = "PASS"
45
+ error: str | None = None
46
+
47
+ def to_json(self) -> str:
48
+ """Convert to JSON string."""
49
+ return json.dumps(asdict(self), ensure_ascii=False)
50
+
51
+ @classmethod
52
+ def from_json(cls, json_str: str) -> "ExecutionLogEntry":
53
+ """Create from JSON string."""
54
+ data = json.loads(json_str)
55
+ return cls(**data)
56
+
57
+
58
+ def get_log_path(story_id: str, base_dir: Path | None = None) -> Path:
59
+ """Get execution log path for a story.
60
+
61
+ Args:
62
+ story_id: Story identifier.
63
+ base_dir: Base directory. Defaults to project root.
64
+
65
+ Returns:
66
+ Path to log file.
67
+ """
68
+ if base_dir is None:
69
+ from ref_agents.session import SessionManager
70
+
71
+ session = SessionManager.get()
72
+ base_dir = session.project_root or Path.cwd()
73
+
74
+ return base_dir / "ref-reports" / "qa_lead" / f"test_execution_log_{story_id}.jsonl"
75
+
76
+
77
+ class ExecutionLogger:
78
+ """Log all test execution for audit trail.
79
+
80
+ Writes JSONL format with one entry per line.
81
+ """
82
+
83
+ def __init__(self, story_id: str, base_dir: Path | None = None):
84
+ """Initialize execution logger.
85
+
86
+ Args:
87
+ story_id: Story identifier.
88
+ base_dir: Base directory for log files.
89
+ """
90
+ self.story_id = story_id
91
+ self.log_file = get_log_path(story_id, base_dir)
92
+ self._ensure_log_dir()
93
+
94
+ def _ensure_log_dir(self) -> None:
95
+ """Ensure log directory exists."""
96
+ self.log_file.parent.mkdir(parents=True, exist_ok=True)
97
+
98
+ def log_execution(self, entry: ExecutionLogEntry) -> None:
99
+ """Append execution entry to log file.
100
+
101
+ Args:
102
+ entry: Execution log entry to append.
103
+ """
104
+ with open(self.log_file, "a", encoding="utf-8") as f:
105
+ f.write(entry.to_json() + "\n")
106
+
107
+ logger.info(
108
+ "execution_logged",
109
+ test_id=entry.test_id,
110
+ step=entry.step_number,
111
+ tool=entry.tool_name,
112
+ status=entry.status,
113
+ )
114
+
115
+ def log_step( # noqa: PLR0913 # TECH_DEBT: refactor to TypedDict — STORY-046
116
+ self,
117
+ test_id: str,
118
+ step_number: int,
119
+ tool_name: str,
120
+ tool_args: dict,
121
+ response: dict,
122
+ screenshot_path: Path | None = None,
123
+ duration_ms: int = 0,
124
+ status: str = "PASS",
125
+ error: str | None = None,
126
+ ) -> ExecutionLogEntry:
127
+ """Log a test step execution.
128
+
129
+ Args:
130
+ test_id: Test case identifier.
131
+ step_number: Step number.
132
+ tool_name: MCP tool name.
133
+ tool_args: Tool arguments.
134
+ response: Tool response.
135
+ screenshot_path: Path to screenshot.
136
+ duration_ms: Execution duration.
137
+ status: Step status.
138
+ error: Error message if failed.
139
+
140
+ Returns:
141
+ The logged entry.
142
+ """
143
+ screenshot_size = 0
144
+ screenshot_str = None
145
+
146
+ if screenshot_path and screenshot_path.exists():
147
+ screenshot_size = screenshot_path.stat().st_size
148
+ screenshot_str = str(screenshot_path)
149
+
150
+ entry = ExecutionLogEntry(
151
+ timestamp=datetime.now().isoformat(),
152
+ test_id=test_id,
153
+ step_number=step_number,
154
+ tool_name=tool_name,
155
+ tool_args=tool_args,
156
+ response=response,
157
+ screenshot_path=screenshot_str,
158
+ screenshot_size=screenshot_size,
159
+ duration_ms=duration_ms,
160
+ status=status,
161
+ error=error,
162
+ )
163
+
164
+ self.log_execution(entry)
165
+ return entry
166
+
167
+ def read_log(self) -> list[ExecutionLogEntry]:
168
+ """Read all entries from log file.
169
+
170
+ Returns:
171
+ List of log entries.
172
+ """
173
+ if not self.log_file.exists():
174
+ return []
175
+
176
+ entries = []
177
+ with open(self.log_file, "r", encoding="utf-8") as f:
178
+ for line in f:
179
+ line = line.strip()
180
+ if line:
181
+ try:
182
+ entries.append(ExecutionLogEntry.from_json(line))
183
+ except json.JSONDecodeError as e:
184
+ logger.warning("invalid_log_entry", line=line, error=str(e))
185
+
186
+ return entries
187
+
188
+ def clear_log(self) -> None:
189
+ """Clear the log file."""
190
+ if self.log_file.exists():
191
+ self.log_file.unlink()
192
+ self._ensure_log_dir()
193
+
194
+ def verify_log_integrity(self) -> tuple[bool, list[str]]:
195
+ """Verify log file integrity.
196
+
197
+ Checks:
198
+ - Log file exists
199
+ - All entries are valid JSON
200
+ - Timestamps are sequential
201
+ - No gaps in step numbers per test
202
+
203
+ Returns:
204
+ Tuple of (valid, list of issues).
205
+ """
206
+ issues = []
207
+
208
+ if not self.log_file.exists():
209
+ return False, ["Log file does not exist"]
210
+
211
+ entries = self.read_log()
212
+
213
+ if not entries:
214
+ return False, ["Log file is empty"]
215
+
216
+ # Check timestamps are sequential
217
+ prev_timestamp = None
218
+ for entry in entries:
219
+ try:
220
+ timestamp = datetime.fromisoformat(entry.timestamp)
221
+ if prev_timestamp and timestamp < prev_timestamp:
222
+ issues.append(
223
+ f"Timestamp not sequential: {entry.timestamp} < {prev_timestamp.isoformat()}"
224
+ )
225
+ prev_timestamp = timestamp
226
+ except ValueError:
227
+ issues.append(f"Invalid timestamp format: {entry.timestamp}")
228
+
229
+ # Check step numbers per test
230
+ test_steps: dict[str, list[int]] = {}
231
+ for entry in entries:
232
+ if entry.test_id not in test_steps:
233
+ test_steps[entry.test_id] = []
234
+ test_steps[entry.test_id].append(entry.step_number)
235
+
236
+ for test_id, steps in test_steps.items():
237
+ expected = list(range(1, len(steps) + 1))
238
+ if sorted(steps) != expected:
239
+ issues.append(f"Step numbers not sequential for {test_id}: {steps}")
240
+
241
+ return len(issues) == 0, issues
242
+
243
+ def get_entry_count(self) -> int:
244
+ """Get number of entries in log.
245
+
246
+ Returns:
247
+ Number of log entries.
248
+ """
249
+ return len(self.read_log())