superqode 0.1.5__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 (288) hide show
  1. superqode/__init__.py +33 -0
  2. superqode/acp/__init__.py +23 -0
  3. superqode/acp/client.py +913 -0
  4. superqode/acp/permission_screen.py +457 -0
  5. superqode/acp/types.py +480 -0
  6. superqode/acp_discovery.py +856 -0
  7. superqode/agent/__init__.py +22 -0
  8. superqode/agent/edit_strategies.py +334 -0
  9. superqode/agent/loop.py +892 -0
  10. superqode/agent/qe_report_templates.py +39 -0
  11. superqode/agent/system_prompts.py +353 -0
  12. superqode/agent_output.py +721 -0
  13. superqode/agent_stream.py +953 -0
  14. superqode/agents/__init__.py +59 -0
  15. superqode/agents/acp_registry.py +305 -0
  16. superqode/agents/client.py +249 -0
  17. superqode/agents/data/augmentcode.com.toml +51 -0
  18. superqode/agents/data/cagent.dev.toml +51 -0
  19. superqode/agents/data/claude.com.toml +60 -0
  20. superqode/agents/data/codeassistant.dev.toml +51 -0
  21. superqode/agents/data/codex.openai.com.toml +57 -0
  22. superqode/agents/data/fastagent.ai.toml +66 -0
  23. superqode/agents/data/geminicli.com.toml +77 -0
  24. superqode/agents/data/goose.block.xyz.toml +54 -0
  25. superqode/agents/data/junie.jetbrains.com.toml +56 -0
  26. superqode/agents/data/kimi.moonshot.cn.toml +57 -0
  27. superqode/agents/data/llmlingagent.dev.toml +51 -0
  28. superqode/agents/data/molt.bot.toml +49 -0
  29. superqode/agents/data/opencode.ai.toml +60 -0
  30. superqode/agents/data/stakpak.dev.toml +51 -0
  31. superqode/agents/data/vtcode.dev.toml +51 -0
  32. superqode/agents/discovery.py +266 -0
  33. superqode/agents/messaging.py +160 -0
  34. superqode/agents/persona.py +166 -0
  35. superqode/agents/registry.py +421 -0
  36. superqode/agents/schema.py +72 -0
  37. superqode/agents/unified.py +367 -0
  38. superqode/app/__init__.py +111 -0
  39. superqode/app/constants.py +314 -0
  40. superqode/app/css.py +366 -0
  41. superqode/app/models.py +118 -0
  42. superqode/app/suggester.py +125 -0
  43. superqode/app/widgets.py +1591 -0
  44. superqode/app_enhanced.py +399 -0
  45. superqode/app_main.py +17187 -0
  46. superqode/approval.py +312 -0
  47. superqode/atomic.py +296 -0
  48. superqode/commands/__init__.py +1 -0
  49. superqode/commands/acp.py +965 -0
  50. superqode/commands/agents.py +180 -0
  51. superqode/commands/auth.py +278 -0
  52. superqode/commands/config.py +374 -0
  53. superqode/commands/init.py +826 -0
  54. superqode/commands/providers.py +819 -0
  55. superqode/commands/qe.py +1145 -0
  56. superqode/commands/roles.py +380 -0
  57. superqode/commands/serve.py +172 -0
  58. superqode/commands/suggestions.py +127 -0
  59. superqode/commands/superqe.py +460 -0
  60. superqode/config/__init__.py +51 -0
  61. superqode/config/loader.py +812 -0
  62. superqode/config/schema.py +498 -0
  63. superqode/core/__init__.py +111 -0
  64. superqode/core/roles.py +281 -0
  65. superqode/danger.py +386 -0
  66. superqode/data/superqode-template.yaml +1522 -0
  67. superqode/design_system.py +1080 -0
  68. superqode/dialogs/__init__.py +6 -0
  69. superqode/dialogs/base.py +39 -0
  70. superqode/dialogs/model.py +130 -0
  71. superqode/dialogs/provider.py +870 -0
  72. superqode/diff_view.py +919 -0
  73. superqode/enterprise.py +21 -0
  74. superqode/evaluation/__init__.py +25 -0
  75. superqode/evaluation/adapters.py +93 -0
  76. superqode/evaluation/behaviors.py +89 -0
  77. superqode/evaluation/engine.py +209 -0
  78. superqode/evaluation/scenarios.py +96 -0
  79. superqode/execution/__init__.py +36 -0
  80. superqode/execution/linter.py +538 -0
  81. superqode/execution/modes.py +347 -0
  82. superqode/execution/resolver.py +283 -0
  83. superqode/execution/runner.py +642 -0
  84. superqode/file_explorer.py +811 -0
  85. superqode/file_viewer.py +471 -0
  86. superqode/flash.py +183 -0
  87. superqode/guidance/__init__.py +58 -0
  88. superqode/guidance/config.py +203 -0
  89. superqode/guidance/prompts.py +71 -0
  90. superqode/harness/__init__.py +54 -0
  91. superqode/harness/accelerator.py +291 -0
  92. superqode/harness/config.py +319 -0
  93. superqode/harness/validator.py +147 -0
  94. superqode/history.py +279 -0
  95. superqode/integrations/superopt_runner.py +124 -0
  96. superqode/logging/__init__.py +49 -0
  97. superqode/logging/adapters.py +219 -0
  98. superqode/logging/formatter.py +923 -0
  99. superqode/logging/integration.py +341 -0
  100. superqode/logging/sinks.py +170 -0
  101. superqode/logging/unified_log.py +417 -0
  102. superqode/lsp/__init__.py +26 -0
  103. superqode/lsp/client.py +544 -0
  104. superqode/main.py +1069 -0
  105. superqode/mcp/__init__.py +89 -0
  106. superqode/mcp/auth_storage.py +380 -0
  107. superqode/mcp/client.py +1236 -0
  108. superqode/mcp/config.py +319 -0
  109. superqode/mcp/integration.py +337 -0
  110. superqode/mcp/oauth.py +436 -0
  111. superqode/mcp/oauth_callback.py +385 -0
  112. superqode/mcp/types.py +290 -0
  113. superqode/memory/__init__.py +31 -0
  114. superqode/memory/feedback.py +342 -0
  115. superqode/memory/store.py +522 -0
  116. superqode/notifications.py +369 -0
  117. superqode/optimization/__init__.py +5 -0
  118. superqode/optimization/config.py +33 -0
  119. superqode/permissions/__init__.py +25 -0
  120. superqode/permissions/rules.py +488 -0
  121. superqode/plan.py +323 -0
  122. superqode/providers/__init__.py +33 -0
  123. superqode/providers/gateway/__init__.py +165 -0
  124. superqode/providers/gateway/base.py +228 -0
  125. superqode/providers/gateway/litellm_gateway.py +1170 -0
  126. superqode/providers/gateway/openresponses_gateway.py +436 -0
  127. superqode/providers/health.py +297 -0
  128. superqode/providers/huggingface/__init__.py +74 -0
  129. superqode/providers/huggingface/downloader.py +472 -0
  130. superqode/providers/huggingface/endpoints.py +442 -0
  131. superqode/providers/huggingface/hub.py +531 -0
  132. superqode/providers/huggingface/inference.py +394 -0
  133. superqode/providers/huggingface/transformers_runner.py +516 -0
  134. superqode/providers/local/__init__.py +100 -0
  135. superqode/providers/local/base.py +438 -0
  136. superqode/providers/local/discovery.py +418 -0
  137. superqode/providers/local/lmstudio.py +256 -0
  138. superqode/providers/local/mlx.py +457 -0
  139. superqode/providers/local/ollama.py +486 -0
  140. superqode/providers/local/sglang.py +268 -0
  141. superqode/providers/local/tgi.py +260 -0
  142. superqode/providers/local/tool_support.py +477 -0
  143. superqode/providers/local/vllm.py +258 -0
  144. superqode/providers/manager.py +1338 -0
  145. superqode/providers/models.py +1016 -0
  146. superqode/providers/models_dev.py +578 -0
  147. superqode/providers/openresponses/__init__.py +87 -0
  148. superqode/providers/openresponses/converters/__init__.py +17 -0
  149. superqode/providers/openresponses/converters/messages.py +343 -0
  150. superqode/providers/openresponses/converters/tools.py +268 -0
  151. superqode/providers/openresponses/schema/__init__.py +56 -0
  152. superqode/providers/openresponses/schema/models.py +585 -0
  153. superqode/providers/openresponses/streaming/__init__.py +5 -0
  154. superqode/providers/openresponses/streaming/parser.py +338 -0
  155. superqode/providers/openresponses/tools/__init__.py +21 -0
  156. superqode/providers/openresponses/tools/apply_patch.py +352 -0
  157. superqode/providers/openresponses/tools/code_interpreter.py +290 -0
  158. superqode/providers/openresponses/tools/file_search.py +333 -0
  159. superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
  160. superqode/providers/registry.py +716 -0
  161. superqode/providers/usage.py +332 -0
  162. superqode/pure_mode.py +384 -0
  163. superqode/qr/__init__.py +23 -0
  164. superqode/qr/dashboard.py +781 -0
  165. superqode/qr/generator.py +1018 -0
  166. superqode/qr/templates.py +135 -0
  167. superqode/safety/__init__.py +41 -0
  168. superqode/safety/sandbox.py +413 -0
  169. superqode/safety/warnings.py +256 -0
  170. superqode/server/__init__.py +33 -0
  171. superqode/server/lsp_server.py +775 -0
  172. superqode/server/web.py +250 -0
  173. superqode/session/__init__.py +25 -0
  174. superqode/session/persistence.py +580 -0
  175. superqode/session/sharing.py +477 -0
  176. superqode/session.py +475 -0
  177. superqode/sidebar.py +2991 -0
  178. superqode/stream_view.py +648 -0
  179. superqode/styles/__init__.py +3 -0
  180. superqode/superqe/__init__.py +184 -0
  181. superqode/superqe/acp_runner.py +1064 -0
  182. superqode/superqe/constitution/__init__.py +62 -0
  183. superqode/superqe/constitution/evaluator.py +308 -0
  184. superqode/superqe/constitution/loader.py +432 -0
  185. superqode/superqe/constitution/schema.py +250 -0
  186. superqode/superqe/events.py +591 -0
  187. superqode/superqe/frameworks/__init__.py +65 -0
  188. superqode/superqe/frameworks/base.py +234 -0
  189. superqode/superqe/frameworks/e2e.py +263 -0
  190. superqode/superqe/frameworks/executor.py +237 -0
  191. superqode/superqe/frameworks/javascript.py +409 -0
  192. superqode/superqe/frameworks/python.py +373 -0
  193. superqode/superqe/frameworks/registry.py +92 -0
  194. superqode/superqe/mcp_tools/__init__.py +47 -0
  195. superqode/superqe/mcp_tools/core_tools.py +418 -0
  196. superqode/superqe/mcp_tools/registry.py +230 -0
  197. superqode/superqe/mcp_tools/testing_tools.py +167 -0
  198. superqode/superqe/noise.py +89 -0
  199. superqode/superqe/orchestrator.py +778 -0
  200. superqode/superqe/roles.py +609 -0
  201. superqode/superqe/session.py +713 -0
  202. superqode/superqe/skills/__init__.py +57 -0
  203. superqode/superqe/skills/base.py +106 -0
  204. superqode/superqe/skills/core_skills.py +899 -0
  205. superqode/superqe/skills/registry.py +90 -0
  206. superqode/superqe/verifier.py +101 -0
  207. superqode/superqe_cli.py +76 -0
  208. superqode/tool_call.py +358 -0
  209. superqode/tools/__init__.py +93 -0
  210. superqode/tools/agent_tools.py +496 -0
  211. superqode/tools/base.py +324 -0
  212. superqode/tools/batch_tool.py +133 -0
  213. superqode/tools/diagnostics.py +311 -0
  214. superqode/tools/edit_tools.py +653 -0
  215. superqode/tools/enhanced_base.py +515 -0
  216. superqode/tools/file_tools.py +269 -0
  217. superqode/tools/file_tracking.py +45 -0
  218. superqode/tools/lsp_tools.py +610 -0
  219. superqode/tools/network_tools.py +350 -0
  220. superqode/tools/permissions.py +400 -0
  221. superqode/tools/question_tool.py +324 -0
  222. superqode/tools/search_tools.py +598 -0
  223. superqode/tools/shell_tools.py +259 -0
  224. superqode/tools/todo_tools.py +121 -0
  225. superqode/tools/validation.py +80 -0
  226. superqode/tools/web_tools.py +639 -0
  227. superqode/tui.py +1152 -0
  228. superqode/tui_integration.py +875 -0
  229. superqode/tui_widgets/__init__.py +27 -0
  230. superqode/tui_widgets/widgets/__init__.py +18 -0
  231. superqode/tui_widgets/widgets/progress.py +185 -0
  232. superqode/tui_widgets/widgets/tool_display.py +188 -0
  233. superqode/undo_manager.py +574 -0
  234. superqode/utils/__init__.py +5 -0
  235. superqode/utils/error_handling.py +323 -0
  236. superqode/utils/fuzzy.py +257 -0
  237. superqode/widgets/__init__.py +477 -0
  238. superqode/widgets/agent_collab.py +390 -0
  239. superqode/widgets/agent_store.py +936 -0
  240. superqode/widgets/agent_switcher.py +395 -0
  241. superqode/widgets/animation_manager.py +284 -0
  242. superqode/widgets/code_context.py +356 -0
  243. superqode/widgets/command_palette.py +412 -0
  244. superqode/widgets/connection_status.py +537 -0
  245. superqode/widgets/conversation_history.py +470 -0
  246. superqode/widgets/diff_indicator.py +155 -0
  247. superqode/widgets/enhanced_status_bar.py +385 -0
  248. superqode/widgets/enhanced_toast.py +476 -0
  249. superqode/widgets/file_browser.py +809 -0
  250. superqode/widgets/file_reference.py +585 -0
  251. superqode/widgets/issue_timeline.py +340 -0
  252. superqode/widgets/leader_key.py +264 -0
  253. superqode/widgets/mode_switcher.py +445 -0
  254. superqode/widgets/model_picker.py +234 -0
  255. superqode/widgets/permission_preview.py +1205 -0
  256. superqode/widgets/prompt.py +358 -0
  257. superqode/widgets/provider_connect.py +725 -0
  258. superqode/widgets/pty_shell.py +587 -0
  259. superqode/widgets/qe_dashboard.py +321 -0
  260. superqode/widgets/resizable_sidebar.py +377 -0
  261. superqode/widgets/response_changes.py +218 -0
  262. superqode/widgets/response_display.py +528 -0
  263. superqode/widgets/rich_tool_display.py +613 -0
  264. superqode/widgets/sidebar_panels.py +1180 -0
  265. superqode/widgets/slash_complete.py +356 -0
  266. superqode/widgets/split_view.py +612 -0
  267. superqode/widgets/status_bar.py +273 -0
  268. superqode/widgets/superqode_display.py +786 -0
  269. superqode/widgets/thinking_display.py +815 -0
  270. superqode/widgets/throbber.py +87 -0
  271. superqode/widgets/toast.py +206 -0
  272. superqode/widgets/unified_output.py +1073 -0
  273. superqode/workspace/__init__.py +75 -0
  274. superqode/workspace/artifacts.py +472 -0
  275. superqode/workspace/coordinator.py +353 -0
  276. superqode/workspace/diff_tracker.py +429 -0
  277. superqode/workspace/git_guard.py +373 -0
  278. superqode/workspace/git_snapshot.py +526 -0
  279. superqode/workspace/manager.py +750 -0
  280. superqode/workspace/snapshot.py +357 -0
  281. superqode/workspace/watcher.py +535 -0
  282. superqode/workspace/worktree.py +440 -0
  283. superqode-0.1.5.dist-info/METADATA +204 -0
  284. superqode-0.1.5.dist-info/RECORD +288 -0
  285. superqode-0.1.5.dist-info/WHEEL +5 -0
  286. superqode-0.1.5.dist-info/entry_points.txt +3 -0
  287. superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
  288. superqode-0.1.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1018 @@
1
+ """
2
+ QR Generator - Quality Report Generator.
3
+
4
+ Produces research-grade QA reports that transform findings
5
+ from "bug reports" into "evidence-backed decisions."
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from datetime import datetime
12
+ from enum import Enum
13
+ from pathlib import Path
14
+ from typing import Any, Optional
15
+ import json
16
+
17
+
18
+ class QRVerdict(Enum):
19
+ """Overall QIR verdict."""
20
+
21
+ PASS = "pass" # No significant issues
22
+ CONDITIONAL_PASS = "conditional" # Warnings but acceptable
23
+ FAIL = "fail" # Critical issues found
24
+ BLOCKED = "blocked" # Could not complete analysis
25
+
26
+
27
+ class QRSection(Enum):
28
+ """Sections of a QIR."""
29
+
30
+ EXECUTIVE_SUMMARY = "executive_summary"
31
+ SCOPE = "scope"
32
+ METHODOLOGY = "methodology"
33
+ FINDINGS = "findings"
34
+ ROOT_CAUSE = "root_cause"
35
+ SUGGESTED_FIXES = "suggested_fixes"
36
+ GENERATED_TESTS = "generated_tests"
37
+ BENCHMARKS = "benchmarks"
38
+ RECOMMENDATIONS = "recommendations"
39
+ APPENDIX = "appendix"
40
+
41
+
42
+ class FindingPriority(Enum):
43
+ """
44
+ Priority levels for findings.
45
+
46
+ P0 - Drop everything. Blocking release/operations.
47
+ P1 - Urgent. Should be addressed in next cycle.
48
+ P2 - Normal. To be fixed eventually.
49
+ P3 - Low. Nice to have.
50
+ """
51
+
52
+ P0 = 0 # Drop everything
53
+ P1 = 1 # Urgent
54
+ P2 = 2 # Normal
55
+ P3 = 3 # Low/Nice to have
56
+
57
+
58
+ @dataclass
59
+ class Finding:
60
+ """
61
+ A single finding in the QIR.
62
+
63
+ Enhanced with priorities and confidence scores for CI filtering.
64
+ """
65
+
66
+ id: str
67
+ severity: str # "critical", "high", "medium", "low", "info"
68
+ category: str # "security", "performance", "reliability", "maintainability"
69
+ title: str
70
+ description: str
71
+
72
+ # Priority
73
+ priority: FindingPriority = FindingPriority.P2
74
+
75
+ # Confidence score (0.0-1.0) for filtering noise
76
+ confidence_score: float = 0.8
77
+
78
+ # Location
79
+ file_path: Optional[str] = None
80
+ line_start: Optional[int] = None
81
+ line_end: Optional[int] = None
82
+
83
+ # Evidence and context
84
+ evidence: Optional[str] = None
85
+ evidence_snippet: Optional[str] = None # Code snippet showing the issue
86
+ reproduction_steps: List[str] = field(default_factory=list)
87
+
88
+ # Fix information
89
+ suggested_fix: Optional[str] = None
90
+ suggested_fix_snippet: Optional[str] = None # Code showing the fix
91
+ patch_id: Optional[str] = None
92
+
93
+ # Metadata
94
+ found_by: Optional[str] = None # QE role that found this
95
+ references: List[str] = field(default_factory=list)
96
+ tags: List[str] = field(default_factory=list)
97
+
98
+ @property
99
+ def severity_icon(self) -> str:
100
+ """Get emoji icon for severity."""
101
+ icons = {
102
+ "critical": "🔴",
103
+ "high": "🟠",
104
+ "medium": "🟡",
105
+ "low": "🔵",
106
+ "info": "⚪",
107
+ }
108
+ return icons.get(self.severity, "⚪")
109
+
110
+ @property
111
+ def priority_label(self) -> str:
112
+ """Get priority label like [P0], [P1], etc."""
113
+ return f"[P{self.priority.value}]"
114
+
115
+ @property
116
+ def location(self) -> str:
117
+ """Get formatted location string."""
118
+ if not self.file_path:
119
+ return ""
120
+ loc = self.file_path
121
+ if self.line_start:
122
+ loc += f":{self.line_start}"
123
+ if self.line_end and self.line_end != self.line_start:
124
+ loc += f"-{self.line_end}"
125
+ return loc
126
+
127
+ @property
128
+ def full_title(self) -> str:
129
+ """Get title with priority prefix."""
130
+ return f"{self.priority_label} {self.title}"
131
+
132
+ def to_dict(self) -> Dict[str, Any]:
133
+ """Convert to dictionary for JSON serialization."""
134
+ return {
135
+ "id": self.id,
136
+ "severity": self.severity,
137
+ "priority": self.priority.value,
138
+ "confidence_score": self.confidence_score,
139
+ "category": self.category,
140
+ "title": self.title,
141
+ "description": self.description,
142
+ "file_path": self.file_path,
143
+ "line_range": {
144
+ "start": self.line_start,
145
+ "end": self.line_end,
146
+ }
147
+ if self.line_start
148
+ else None,
149
+ "location": self.location,
150
+ "evidence": self.evidence,
151
+ "suggested_fix": self.suggested_fix,
152
+ "patch_id": self.patch_id,
153
+ "found_by": self.found_by,
154
+ "tags": self.tags,
155
+ }
156
+
157
+
158
+ @dataclass
159
+ class TestArtifact:
160
+ """A generated test artifact."""
161
+
162
+ id: str
163
+ test_type: str # "unit", "integration", "api", "fuzz", etc.
164
+ filename: str
165
+ description: str
166
+ target_file: Optional[str] = None
167
+ coverage_added: Optional[float] = None
168
+
169
+
170
+ @dataclass
171
+ class PatchArtifact:
172
+ """A suggested fix patch."""
173
+
174
+ id: str
175
+ filename: str
176
+ description: str
177
+ target_file: str
178
+ lines_added: int = 0
179
+ lines_removed: int = 0
180
+
181
+
182
+ @dataclass
183
+ class BenchmarkResult:
184
+ """A benchmark or validation result."""
185
+
186
+ name: str
187
+ metric: str
188
+ value: float
189
+ unit: str
190
+ baseline: Optional[float] = None
191
+ threshold: Optional[float] = None
192
+ passed: bool = True
193
+
194
+
195
+ @dataclass
196
+ class VerifiedFix:
197
+ """A fix that has been verified through the suggestion workflow.
198
+
199
+ Records the full proof chain:
200
+ 1. Original issue found
201
+ 2. Fix applied in sandbox
202
+ 3. Tests run before/after
203
+ 4. Proof of improvement
204
+ 5. Code reverted (always)
205
+ """
206
+
207
+ finding_id: str
208
+ finding_title: str
209
+ patch_id: str
210
+ patch_file: str
211
+
212
+ # Verification status
213
+ fix_verified: bool = False
214
+ is_improvement: bool = False
215
+
216
+ # Test metrics
217
+ tests_before_passed: int = 0
218
+ tests_before_total: int = 0
219
+ tests_after_passed: int = 0
220
+ tests_after_total: int = 0
221
+
222
+ # Coverage
223
+ coverage_before: Optional[float] = None
224
+ coverage_after: Optional[float] = None
225
+
226
+ # Evidence
227
+ verification_evidence: List[str] = field(default_factory=list)
228
+ verification_duration_ms: int = 0
229
+
230
+ # Confidence in the fix
231
+ fix_confidence: float = 0.8
232
+
233
+ @property
234
+ def tests_fixed(self) -> int:
235
+ """Number of tests that now pass after the fix."""
236
+ return max(0, self.tests_after_passed - self.tests_before_passed)
237
+
238
+ @property
239
+ def tests_broken(self) -> int:
240
+ """Number of tests broken by the fix (regressions)."""
241
+ after_failed = self.tests_after_total - self.tests_after_passed
242
+ before_failed = self.tests_before_total - self.tests_before_passed
243
+ return max(0, after_failed - before_failed)
244
+
245
+ @property
246
+ def coverage_delta(self) -> Optional[float]:
247
+ """Change in coverage."""
248
+ if self.coverage_before is not None and self.coverage_after is not None:
249
+ return self.coverage_after - self.coverage_before
250
+ return None
251
+
252
+ def to_dict(self) -> Dict[str, Any]:
253
+ return {
254
+ "finding_id": self.finding_id,
255
+ "finding_title": self.finding_title,
256
+ "patch_id": self.patch_id,
257
+ "patch_file": self.patch_file,
258
+ "fix_verified": self.fix_verified,
259
+ "is_improvement": self.is_improvement,
260
+ "tests_fixed": self.tests_fixed,
261
+ "tests_broken": self.tests_broken,
262
+ "metrics": {
263
+ "before": {
264
+ "tests_passed": self.tests_before_passed,
265
+ "tests_total": self.tests_before_total,
266
+ "coverage": self.coverage_before,
267
+ },
268
+ "after": {
269
+ "tests_passed": self.tests_after_passed,
270
+ "tests_total": self.tests_after_total,
271
+ "coverage": self.coverage_after,
272
+ },
273
+ },
274
+ "coverage_delta": self.coverage_delta,
275
+ "fix_confidence": self.fix_confidence,
276
+ "verification_evidence": self.verification_evidence,
277
+ }
278
+
279
+
280
+ @dataclass
281
+ class QRData:
282
+ """All data for generating a QIR."""
283
+
284
+ session_id: str
285
+ mode: str
286
+ started_at: datetime
287
+ ended_at: Optional[datetime] = None
288
+
289
+ # Scope
290
+ target_description: str = ""
291
+ files_analyzed: List[str] = field(default_factory=list)
292
+ total_lines: int = 0
293
+
294
+ # Methodology
295
+ roles_used: List[str] = field(default_factory=list)
296
+ tools_used: List[str] = field(default_factory=list)
297
+ methodology_notes: List[str] = field(default_factory=list)
298
+
299
+ # Findings
300
+ findings: List[Finding] = field(default_factory=list)
301
+
302
+ # Artifacts
303
+ generated_tests: List[TestArtifact] = field(default_factory=list)
304
+ patches: List[PatchArtifact] = field(default_factory=list)
305
+
306
+ # Benchmarks
307
+ benchmarks: List[BenchmarkResult] = field(default_factory=list)
308
+ coverage_before: Optional[float] = None
309
+ coverage_after: Optional[float] = None
310
+
311
+ # Verified fixes (when allow_suggestions is enabled)
312
+ verified_fixes: List[VerifiedFix] = field(default_factory=list)
313
+ allow_suggestions_enabled: bool = False
314
+
315
+ # Meta
316
+ blocked_operations: List[str] = field(default_factory=list)
317
+ errors: List[str] = field(default_factory=list)
318
+
319
+ @property
320
+ def duration_seconds(self) -> float:
321
+ """Get session duration."""
322
+ if self.ended_at:
323
+ return (self.ended_at - self.started_at).total_seconds()
324
+ return (datetime.now() - self.started_at).total_seconds()
325
+
326
+ @property
327
+ def critical_count(self) -> int:
328
+ """Count of critical findings."""
329
+ return sum(1 for f in self.findings if f.severity == "critical")
330
+
331
+ @property
332
+ def high_count(self) -> int:
333
+ """Count of high severity findings."""
334
+ return sum(1 for f in self.findings if f.severity == "high")
335
+
336
+ @property
337
+ def verdict(self) -> QRVerdict:
338
+ """Determine overall verdict."""
339
+ if self.errors:
340
+ return QRVerdict.BLOCKED
341
+ if self.critical_count > 0:
342
+ return QRVerdict.FAIL
343
+ if self.high_count > 0 or sum(1 for f in self.findings if f.severity == "medium") > 3:
344
+ return QRVerdict.CONDITIONAL_PASS
345
+ return QRVerdict.PASS
346
+
347
+
348
+ class QRGenerator:
349
+ """
350
+ Generates Quality Investigation Reports.
351
+
352
+ Produces Markdown reports with optional JSON output for CI integration.
353
+ """
354
+
355
+ def __init__(self, data: QRData):
356
+ self.data = data
357
+
358
+ def generate_markdown(self) -> str:
359
+ """Generate the full QIR in Markdown format."""
360
+ sections = [
361
+ self._header(),
362
+ self._executive_summary(),
363
+ self._scope(),
364
+ self._methodology(),
365
+ self._findings(),
366
+ self._suggested_fixes(),
367
+ self._fix_verification(), # New: verification results
368
+ self._generated_tests(),
369
+ self._benchmarks(),
370
+ self._recommendations(),
371
+ self._appendix(),
372
+ self._footer(),
373
+ ]
374
+
375
+ return "\n".join(filter(None, sections))
376
+
377
+ def generate_json(self) -> Dict[str, Any]:
378
+ """Generate QIR data as JSON for CI integration."""
379
+ # Calculate confidence statistics
380
+ confidence_scores = [f.confidence_score for f in self.data.findings]
381
+ avg_confidence = (
382
+ sum(confidence_scores) / len(confidence_scores) if confidence_scores else 1.0
383
+ )
384
+
385
+ # Priority breakdown
386
+ priority_counts = {f"P{i}": 0 for i in range(4)}
387
+ for f in self.data.findings:
388
+ priority_counts[f"P{f.priority.value}"] += 1
389
+
390
+ return {
391
+ "version": "1.0",
392
+ "schema": "superqode-qr-v1",
393
+ "session_id": self.data.session_id,
394
+ "mode": self.data.mode,
395
+ "started_at": self.data.started_at.isoformat(),
396
+ "ended_at": self.data.ended_at.isoformat() if self.data.ended_at else None,
397
+ "duration_seconds": self.data.duration_seconds,
398
+ # Verdict with confidence
399
+ "verdict": self.data.verdict.value,
400
+ "overall_correctness": "correct"
401
+ if self.data.verdict == QRVerdict.PASS
402
+ else "incorrect",
403
+ "overall_confidence_score": avg_confidence,
404
+ "overall_explanation": self._generate_verdict_explanation(),
405
+ # Summary statistics
406
+ "summary": {
407
+ "total_findings": len(self.data.findings),
408
+ "by_severity": {
409
+ "critical": self.data.critical_count,
410
+ "high": self.data.high_count,
411
+ "medium": sum(1 for f in self.data.findings if f.severity == "medium"),
412
+ "low": sum(1 for f in self.data.findings if f.severity == "low"),
413
+ "info": sum(1 for f in self.data.findings if f.severity == "info"),
414
+ },
415
+ "by_priority": priority_counts,
416
+ "tests_generated": len(self.data.generated_tests),
417
+ "patches_generated": len(self.data.patches),
418
+ },
419
+ # Detailed findings (CI-friendly format)
420
+ "findings": [f.to_dict() for f in self.data.findings],
421
+ # Coverage information
422
+ "coverage": {
423
+ "before": self.data.coverage_before,
424
+ "after": self.data.coverage_after,
425
+ "change": (self.data.coverage_after - self.data.coverage_before)
426
+ if self.data.coverage_before and self.data.coverage_after
427
+ else None,
428
+ },
429
+ # Generated artifacts
430
+ "artifacts": {
431
+ "tests": [
432
+ {
433
+ "filename": t.filename,
434
+ "type": t.test_type,
435
+ "target": t.target_file,
436
+ }
437
+ for t in self.data.generated_tests
438
+ ],
439
+ "patches": [
440
+ {
441
+ "filename": p.filename,
442
+ "target": p.target_file,
443
+ "lines_added": p.lines_added,
444
+ "lines_removed": p.lines_removed,
445
+ }
446
+ for p in self.data.patches
447
+ ],
448
+ },
449
+ # Verified fixes (when allow_suggestions enabled)
450
+ "verified_fixes": {
451
+ "enabled": self.data.allow_suggestions_enabled,
452
+ "total": len(self.data.verified_fixes),
453
+ "verified": sum(1 for f in self.data.verified_fixes if f.fix_verified),
454
+ "improvements": sum(1 for f in self.data.verified_fixes if f.is_improvement),
455
+ "fixes": [vf.to_dict() for vf in self.data.verified_fixes],
456
+ }
457
+ if self.data.verified_fixes
458
+ else None,
459
+ # Metadata
460
+ "metadata": {
461
+ "roles_used": self.data.roles_used,
462
+ "files_analyzed": len(self.data.files_analyzed),
463
+ "total_lines": self.data.total_lines,
464
+ "blocked_operations": len(self.data.blocked_operations),
465
+ "errors": len(self.data.errors),
466
+ "allow_suggestions": self.data.allow_suggestions_enabled,
467
+ },
468
+ }
469
+
470
+ def _generate_verdict_explanation(self) -> str:
471
+ """Generate a brief explanation for the verdict."""
472
+ if self.data.verdict == QRVerdict.PASS:
473
+ return "No significant issues were found during the investigation."
474
+ elif self.data.verdict == QRVerdict.CONDITIONAL_PASS:
475
+ return f"Found {self.data.high_count} high-severity issues that should be reviewed."
476
+ elif self.data.verdict == QRVerdict.FAIL:
477
+ return f"Found {self.data.critical_count} critical issues that require immediate attention."
478
+ else:
479
+ return f"Analysis could not complete due to {len(self.data.errors)} errors."
480
+
481
+ def _header(self) -> str:
482
+ """Generate report header."""
483
+ return f"""# Quality Report (QR)
484
+
485
+ **Session ID**: `{self.data.session_id}`
486
+ **Mode**: {self.data.mode}
487
+ **Date**: {self.data.started_at.strftime("%Y-%m-%d %H:%M")}
488
+ **Duration**: {self.data.duration_seconds:.1f}s
489
+
490
+ ---
491
+ """
492
+
493
+ def _executive_summary(self) -> str:
494
+ """Generate executive summary section."""
495
+ verdict = self.data.verdict
496
+
497
+ verdict_display = {
498
+ QRVerdict.PASS: "🟢 **PASS** - No significant issues found",
499
+ QRVerdict.CONDITIONAL_PASS: "🟡 **CONDITIONAL PASS** - Issues found, review recommended",
500
+ QRVerdict.FAIL: "🔴 **FAIL** - Critical issues require attention",
501
+ QRVerdict.BLOCKED: "⚫ **BLOCKED** - Analysis could not complete",
502
+ }
503
+
504
+ lines = [
505
+ "## Executive Summary",
506
+ "",
507
+ f"**Verdict**: {verdict_display[verdict]}",
508
+ "",
509
+ "### Findings Overview",
510
+ "",
511
+ "| Severity | Count | Action |",
512
+ "|----------|-------|--------|",
513
+ f"| 🔴 Critical | {self.data.critical_count} | Must fix |",
514
+ f"| 🟠 High | {self.data.high_count} | Should fix |",
515
+ f"| 🟡 Medium | {sum(1 for f in self.data.findings if f.severity == 'medium')} | Consider |",
516
+ f"| 🔵 Low | {sum(1 for f in self.data.findings if f.severity == 'low')} | Optional |",
517
+ f"| ⚪ Info | {sum(1 for f in self.data.findings if f.severity == 'info')} | Note |",
518
+ "",
519
+ ]
520
+
521
+ # Coverage change
522
+ if self.data.coverage_before is not None and self.data.coverage_after is not None:
523
+ change = self.data.coverage_after - self.data.coverage_before
524
+ change_str = f"+{change:.1f}%" if change >= 0 else f"{change:.1f}%"
525
+ lines.extend(
526
+ [
527
+ "### Coverage Impact",
528
+ "",
529
+ f"- Before: {self.data.coverage_before:.1f}%",
530
+ f"- After: {self.data.coverage_after:.1f}%",
531
+ f"- Change: {change_str}",
532
+ "",
533
+ ]
534
+ )
535
+
536
+ # Artifacts generated
537
+ if self.data.generated_tests or self.data.patches:
538
+ lines.extend(
539
+ [
540
+ "### Generated Artifacts",
541
+ "",
542
+ f"- 📝 {len(self.data.patches)} suggested fixes",
543
+ f"- 🧪 {len(self.data.generated_tests)} new tests",
544
+ "",
545
+ ]
546
+ )
547
+
548
+ return "\n".join(lines)
549
+
550
+ def _scope(self) -> str:
551
+ """Generate scope section."""
552
+ lines = [
553
+ "## Investigation Scope",
554
+ "",
555
+ ]
556
+
557
+ if self.data.target_description:
558
+ lines.extend(
559
+ [
560
+ f"**Target**: {self.data.target_description}",
561
+ "",
562
+ ]
563
+ )
564
+
565
+ lines.extend(
566
+ [
567
+ f"- Files analyzed: {len(self.data.files_analyzed)}",
568
+ f"- Total lines: {self.data.total_lines:,}",
569
+ ]
570
+ )
571
+
572
+ if self.data.files_analyzed:
573
+ lines.extend(
574
+ [
575
+ "",
576
+ "<details>",
577
+ "<summary>Files analyzed</summary>",
578
+ "",
579
+ ]
580
+ )
581
+ for f in self.data.files_analyzed[:20]: # Limit to 20
582
+ lines.append(f"- `{f}`")
583
+ if len(self.data.files_analyzed) > 20:
584
+ lines.append(f"- ... and {len(self.data.files_analyzed) - 20} more")
585
+ lines.extend(
586
+ [
587
+ "",
588
+ "</details>",
589
+ ]
590
+ )
591
+
592
+ lines.append("")
593
+ return "\n".join(lines)
594
+
595
+ def _methodology(self) -> str:
596
+ """Generate methodology section."""
597
+ lines = [
598
+ "## Methodology",
599
+ "",
600
+ ]
601
+
602
+ if self.data.roles_used:
603
+ lines.append("**QE Roles Used**:")
604
+ for role in self.data.roles_used:
605
+ lines.append(f"- {role}")
606
+ lines.append("")
607
+
608
+ if self.data.tools_used:
609
+ lines.append("**Tools/Techniques**:")
610
+ for tool in self.data.tools_used:
611
+ lines.append(f"- {tool}")
612
+ lines.append("")
613
+
614
+ if self.data.methodology_notes:
615
+ lines.append("**Approach**:")
616
+ for note in self.data.methodology_notes:
617
+ lines.append(f"- {note}")
618
+ lines.append("")
619
+
620
+ return "\n".join(lines)
621
+
622
+ def _findings(self) -> str:
623
+ """Generate findings section."""
624
+ if not self.data.findings:
625
+ return """## Findings
626
+
627
+ ✅ No issues found during this investigation.
628
+ """
629
+
630
+ lines = [
631
+ "## Findings",
632
+ "",
633
+ ]
634
+
635
+ # Group by severity
636
+ for severity in ["critical", "high", "medium", "low", "info"]:
637
+ severity_findings = [f for f in self.data.findings if f.severity == severity]
638
+ if not severity_findings:
639
+ continue
640
+
641
+ severity_title = severity.title()
642
+ lines.append(f"### {severity_title} ({len(severity_findings)})")
643
+ lines.append("")
644
+
645
+ for finding in severity_findings:
646
+ lines.extend(self._render_finding(finding))
647
+ lines.append("")
648
+
649
+ return "\n".join(lines)
650
+
651
+ def _render_finding(self, finding: Finding) -> List[str]:
652
+ """Render a single finding."""
653
+ lines = [
654
+ f"#### {finding.severity_icon} {finding.title}",
655
+ "",
656
+ ]
657
+
658
+ if finding.location:
659
+ lines.append(f"**Location**: `{finding.location}`")
660
+ lines.append("")
661
+
662
+ lines.append(finding.description)
663
+ lines.append("")
664
+
665
+ if finding.evidence:
666
+ lines.extend(
667
+ [
668
+ "**Evidence**:",
669
+ "```",
670
+ finding.evidence,
671
+ "```",
672
+ "",
673
+ ]
674
+ )
675
+
676
+ if finding.reproduction_steps:
677
+ lines.append("**Reproduction Steps**:")
678
+ for i, step in enumerate(finding.reproduction_steps, 1):
679
+ lines.append(f"{i}. {step}")
680
+ lines.append("")
681
+
682
+ if finding.patch_id:
683
+ lines.append(f"💡 **Suggested Fix**: See patch `{finding.patch_id}`")
684
+ lines.append("")
685
+
686
+ if finding.references:
687
+ lines.append("**References**:")
688
+ for ref in finding.references:
689
+ lines.append(f"- {ref}")
690
+ lines.append("")
691
+
692
+ return lines
693
+
694
+ def _suggested_fixes(self) -> str:
695
+ """Generate suggested fixes section."""
696
+ if not self.data.patches:
697
+ return ""
698
+
699
+ lines = [
700
+ "## Suggested Fixes",
701
+ "",
702
+ "The following patches have been generated and are available in `.superqode/qe-artifacts/patches/`:",
703
+ "",
704
+ "| Patch | Target | Changes | Description |",
705
+ "|-------|--------|---------|-------------|",
706
+ ]
707
+
708
+ for patch in self.data.patches:
709
+ changes = f"+{patch.lines_added}/-{patch.lines_removed}"
710
+ lines.append(
711
+ f"| `{patch.filename}` | `{patch.target_file}` | {changes} | {patch.description} |"
712
+ )
713
+
714
+ lines.extend(
715
+ [
716
+ "",
717
+ "**To apply a patch**:",
718
+ "```bash",
719
+ "cd /path/to/project",
720
+ "patch -p1 < .superqode/qe-artifacts/patches/<patch-file>.patch",
721
+ "```",
722
+ "",
723
+ ]
724
+ )
725
+
726
+ return "\n".join(lines)
727
+
728
+ def _fix_verification(self) -> str:
729
+ """Generate fix verification section (when allow_suggestions is enabled)."""
730
+ if not self.data.verified_fixes:
731
+ return ""
732
+
733
+ lines = [
734
+ "## Fix Verification Results",
735
+ "",
736
+ "The following fixes were verified in sandbox environment.",
737
+ "All changes have been **reverted** - patches preserved for your review.",
738
+ "",
739
+ ]
740
+
741
+ # Summary table
742
+ verified_count = sum(1 for f in self.data.verified_fixes if f.fix_verified)
743
+ improvement_count = sum(1 for f in self.data.verified_fixes if f.is_improvement)
744
+
745
+ lines.extend(
746
+ [
747
+ "### Summary",
748
+ "",
749
+ f"- Total fixes attempted: {len(self.data.verified_fixes)}",
750
+ f"- Verified successful: {verified_count}",
751
+ f"- Confirmed improvements: {improvement_count}",
752
+ "",
753
+ "### Verification Details",
754
+ "",
755
+ "| Finding | Fix Status | Tests Before | Tests After | Improvement |",
756
+ "|---------|------------|--------------|-------------|-------------|",
757
+ ]
758
+ )
759
+
760
+ for vf in self.data.verified_fixes:
761
+ status = "✅ Verified" if vf.fix_verified else "❌ Failed"
762
+ before = f"{vf.tests_before_passed}/{vf.tests_before_total}"
763
+ after = f"{vf.tests_after_passed}/{vf.tests_after_total}"
764
+ improvement = "✅ Yes" if vf.is_improvement else "❌ No"
765
+
766
+ lines.append(
767
+ f"| {vf.finding_title[:30]}... | {status} | {before} | {after} | {improvement} |"
768
+ )
769
+
770
+ lines.append("")
771
+
772
+ # Detailed verification for improvements
773
+ improvements = [vf for vf in self.data.verified_fixes if vf.is_improvement]
774
+ if improvements:
775
+ lines.extend(
776
+ [
777
+ "### Recommended Fixes (Verified Improvements)",
778
+ "",
779
+ ]
780
+ )
781
+
782
+ for vf in improvements:
783
+ lines.extend(
784
+ [
785
+ f"#### {vf.finding_title}",
786
+ "",
787
+ f"**Patch**: `{vf.patch_file}`",
788
+ f"**Confidence**: {vf.fix_confidence:.0%}",
789
+ "",
790
+ "**Verification Evidence**:",
791
+ "",
792
+ ]
793
+ )
794
+
795
+ for evidence in vf.verification_evidence:
796
+ lines.append(f"- {evidence}")
797
+
798
+ lines.append("")
799
+
800
+ # Before/after comparison
801
+ if vf.tests_fixed > 0:
802
+ lines.append(f"✅ **Fixed {vf.tests_fixed} failing test(s)**")
803
+
804
+ if vf.coverage_delta and vf.coverage_delta > 0:
805
+ lines.append(f"✅ **Coverage improved by {vf.coverage_delta:.1f}%**")
806
+
807
+ lines.extend(
808
+ [
809
+ "",
810
+ "---",
811
+ "",
812
+ ]
813
+ )
814
+
815
+ # Note about revert
816
+ lines.extend(
817
+ [
818
+ "### Important",
819
+ "",
820
+ "🔄 **All changes have been reverted to preserve your original code.**",
821
+ "",
822
+ "The patches are available in `.superqode/qe-artifacts/patches/` for you to review and apply.",
823
+ "",
824
+ ]
825
+ )
826
+
827
+ return "\n".join(lines)
828
+
829
+ def _generated_tests(self) -> str:
830
+ """Generate tests section."""
831
+ if not self.data.generated_tests:
832
+ return ""
833
+
834
+ lines = [
835
+ "## Generated Tests",
836
+ "",
837
+ "The following tests have been generated and are available in `.superqode/qe-artifacts/generated-tests/`:",
838
+ "",
839
+ ]
840
+
841
+ # Group by type
842
+ by_type: Dict[str, List[TestArtifact]] = {}
843
+ for test in self.data.generated_tests:
844
+ by_type.setdefault(test.test_type, []).append(test)
845
+
846
+ for test_type, tests in by_type.items():
847
+ lines.append(f"### {test_type.title()} Tests ({len(tests)})")
848
+ lines.append("")
849
+
850
+ for test in tests:
851
+ target = f" (for `{test.target_file}`)" if test.target_file else ""
852
+ lines.append(f"- `{test.filename}`{target}: {test.description}")
853
+
854
+ lines.append("")
855
+
856
+ lines.extend(
857
+ [
858
+ "**To run generated tests**:",
859
+ "```bash",
860
+ "# Copy tests to your test directory",
861
+ "cp .superqode/qe-artifacts/generated-tests/unit/* tests/",
862
+ "",
863
+ "# Run tests",
864
+ "pytest tests/",
865
+ "```",
866
+ "",
867
+ ]
868
+ )
869
+
870
+ return "\n".join(lines)
871
+
872
+ def _benchmarks(self) -> str:
873
+ """Generate benchmarks section."""
874
+ if not self.data.benchmarks:
875
+ return ""
876
+
877
+ lines = [
878
+ "## Benchmark Results",
879
+ "",
880
+ "| Metric | Value | Baseline | Status |",
881
+ "|--------|-------|----------|--------|",
882
+ ]
883
+
884
+ for bench in self.data.benchmarks:
885
+ status = "✅" if bench.passed else "❌"
886
+ baseline = f"{bench.baseline}{bench.unit}" if bench.baseline else "-"
887
+ lines.append(f"| {bench.name} | {bench.value}{bench.unit} | {baseline} | {status} |")
888
+
889
+ lines.append("")
890
+ return "\n".join(lines)
891
+
892
+ def _recommendations(self) -> str:
893
+ """Generate recommendations section."""
894
+ lines = [
895
+ "## Recommendations",
896
+ "",
897
+ ]
898
+
899
+ # Priority actions based on findings
900
+ if self.data.critical_count > 0:
901
+ lines.extend(
902
+ [
903
+ "### 🚨 Immediate Actions Required",
904
+ "",
905
+ ]
906
+ )
907
+ for f in self.data.findings:
908
+ if f.severity == "critical":
909
+ action = f"Apply patch `{f.patch_id}`" if f.patch_id else "Manual fix required"
910
+ lines.append(f"1. **{f.title}** - {action}")
911
+ lines.append("")
912
+
913
+ if self.data.high_count > 0:
914
+ lines.extend(
915
+ [
916
+ "### ⚠️ Should Address",
917
+ "",
918
+ ]
919
+ )
920
+ for f in self.data.findings:
921
+ if f.severity == "high":
922
+ lines.append(f"- {f.title}")
923
+ lines.append("")
924
+
925
+ # General recommendations
926
+ lines.extend(
927
+ [
928
+ "### 📋 General",
929
+ "",
930
+ ]
931
+ )
932
+
933
+ if self.data.generated_tests:
934
+ lines.append("- Review and integrate generated tests to improve coverage")
935
+
936
+ if self.data.patches:
937
+ lines.append("- Review suggested patches before applying")
938
+
939
+ if self.data.coverage_after and self.data.coverage_after < 80:
940
+ lines.append(
941
+ f"- Consider increasing test coverage (currently {self.data.coverage_after:.1f}%)"
942
+ )
943
+
944
+ lines.append("")
945
+ return "\n".join(lines)
946
+
947
+ def _appendix(self) -> str:
948
+ """Generate appendix section."""
949
+ lines = []
950
+
951
+ if self.data.blocked_operations:
952
+ lines.extend(
953
+ [
954
+ "## Appendix A: Blocked Operations",
955
+ "",
956
+ "The following operations were blocked to maintain repo integrity:",
957
+ "",
958
+ ]
959
+ )
960
+ for op in self.data.blocked_operations:
961
+ lines.append(f"- `{op}`")
962
+ lines.append("")
963
+
964
+ if self.data.errors:
965
+ lines.extend(
966
+ [
967
+ "## Appendix B: Errors",
968
+ "",
969
+ "The following errors occurred during the investigation:",
970
+ "",
971
+ ]
972
+ )
973
+ for err in self.data.errors:
974
+ lines.append(f"- {err}")
975
+ lines.append("")
976
+
977
+ return "\n".join(lines) if lines else ""
978
+
979
+ def _footer(self) -> str:
980
+ """Generate report footer."""
981
+ return """---
982
+
983
+ *Generated by SuperQode - Agentic Quality Engineering*
984
+
985
+ All changes made during this investigation have been reverted.
986
+ Artifacts are preserved in `.superqode/qe-artifacts/` for review and integration.
987
+ """
988
+
989
+ def save(self, output_dir: Path, formats: List[str] = None) -> Dict[str, Path]:
990
+ """
991
+ Save QIR to files.
992
+
993
+ Args:
994
+ output_dir: Directory to save to
995
+ formats: List of formats ("md", "json"). Default: both
996
+
997
+ Returns:
998
+ Dict mapping format to file path
999
+ """
1000
+ formats = formats or ["md", "json"]
1001
+ output_dir.mkdir(parents=True, exist_ok=True)
1002
+
1003
+ timestamp = self.data.started_at.strftime("%Y-%m-%d")
1004
+ base_name = f"qr-{timestamp}-{self.data.session_id[:8]}"
1005
+
1006
+ saved = {}
1007
+
1008
+ if "md" in formats:
1009
+ md_path = output_dir / f"{base_name}.md"
1010
+ md_path.write_text(self.generate_markdown())
1011
+ saved["md"] = md_path
1012
+
1013
+ if "json" in formats:
1014
+ json_path = output_dir / f"{base_name}.json"
1015
+ json_path.write_text(json.dumps(self.generate_json(), indent=2))
1016
+ saved["json"] = json_path
1017
+
1018
+ return saved