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,75 @@
1
+ """
2
+ SuperQode Workspace Module.
3
+
4
+ Provides ephemeral-edit workspace with immutable repo guarantee.
5
+ Agents can freely modify code for QA without touching the repo permanently.
6
+
7
+ Features:
8
+ - Git worktree-based isolation
9
+ - QE session coordination with locking
10
+ - Diff tracking for patch generation
11
+ - Artifact management
12
+ - Git-based snapshots for robust state tracking
13
+ - Real-time file system watching
14
+ """
15
+
16
+ from .manager import WorkspaceManager, WorkspaceState
17
+ from .artifacts import ArtifactManager, ArtifactType
18
+ from .git_guard import GitGuard, GitOperationBlocked
19
+ from .snapshot import SnapshotManager
20
+ from .worktree import GitWorktreeManager, WorktreeInfo, prepare_qe_worktree
21
+ from .coordinator import QECoordinator, QELock, notify_file_change
22
+ from .diff_tracker import DiffTracker, ChangeType, generate_patch_file
23
+
24
+ # New advanced features
25
+ from .git_snapshot import (
26
+ GitSnapshotManager,
27
+ Snapshot,
28
+ FileChange as SnapshotFileChange,
29
+ FileStatus,
30
+ create_git_snapshot_manager,
31
+ )
32
+ from .watcher import (
33
+ DirectoryWatcher,
34
+ PollingWatcher,
35
+ WatcherConfig,
36
+ FileChange as WatcherFileChange,
37
+ ChangeType as WatcherChangeType,
38
+ create_watcher,
39
+ )
40
+
41
+ __all__ = [
42
+ # Core managers
43
+ "WorkspaceManager",
44
+ "WorkspaceState",
45
+ "ArtifactManager",
46
+ "ArtifactType",
47
+ "GitGuard",
48
+ "GitOperationBlocked",
49
+ "SnapshotManager",
50
+ # Git worktree
51
+ "GitWorktreeManager",
52
+ "WorktreeInfo",
53
+ "prepare_qe_worktree",
54
+ # Coordination
55
+ "QECoordinator",
56
+ "QELock",
57
+ "notify_file_change",
58
+ # Diff tracking
59
+ "DiffTracker",
60
+ "ChangeType",
61
+ "generate_patch_file",
62
+ # Git-based snapshots
63
+ "GitSnapshotManager",
64
+ "Snapshot",
65
+ "SnapshotFileChange",
66
+ "FileStatus",
67
+ "create_git_snapshot_manager",
68
+ # Directory watching
69
+ "DirectoryWatcher",
70
+ "PollingWatcher",
71
+ "WatcherConfig",
72
+ "WatcherFileChange",
73
+ "WatcherChangeType",
74
+ "create_watcher",
75
+ ]
@@ -0,0 +1,472 @@
1
+ """
2
+ Artifact Manager for SuperQode QE Sessions.
3
+
4
+ Manages preservation of QE artifacts that survive the ephemeral workspace reset:
5
+ - Candidate fixes / patch files
6
+ - Generated tests (unit, integration, fuzz, API, load, regression)
7
+ - QIRs (Quality Investigation Reports)
8
+ - Test results and evidence
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import difflib
14
+ import json
15
+ import shutil
16
+ from dataclasses import dataclass, field
17
+ from datetime import datetime
18
+ from enum import Enum
19
+ from pathlib import Path
20
+ from typing import Any, Dict, List, Optional
21
+ import hashlib
22
+
23
+
24
+ class ArtifactType(Enum):
25
+ """Types of artifacts generated during QE."""
26
+
27
+ # Patches and fixes
28
+ PATCH = "patch" # Unified diff patch file
29
+ SUGGESTED_FIX = "fix" # Suggested code fix
30
+
31
+ # Generated tests
32
+ TEST_UNIT = "test_unit"
33
+ TEST_INTEGRATION = "test_integration"
34
+ TEST_API = "test_api"
35
+ TEST_CONTRACT = "test_contract"
36
+ TEST_FUZZ = "test_fuzz"
37
+ TEST_LOAD = "test_load"
38
+ TEST_REGRESSION = "test_regression"
39
+ TEST_E2E = "test_e2e"
40
+ TEST_SECURITY = "test_security"
41
+
42
+ # Reports
43
+ QR = "qr" # Quality Report
44
+ COVERAGE = "coverage" # Coverage report
45
+ SUMMARY = "summary" # Session summary
46
+
47
+ # Evidence
48
+ LOG = "log" # Execution logs
49
+ SCREENSHOT = "screenshot" # Visual evidence
50
+ TRACE = "trace" # Execution trace
51
+ ERROR = "error" # Error capture
52
+
53
+
54
+ @dataclass
55
+ class Artifact:
56
+ """A single artifact from QE session."""
57
+
58
+ id: str
59
+ type: ArtifactType
60
+ name: str
61
+ path: Path # Relative to artifacts dir
62
+ description: str = ""
63
+ created_at: datetime = field(default_factory=datetime.now)
64
+ metadata: Dict[str, Any] = field(default_factory=dict)
65
+
66
+ # For patches/fixes
67
+ original_file: Optional[str] = None
68
+
69
+ def to_dict(self) -> Dict[str, Any]:
70
+ """Serialize to dictionary."""
71
+ return {
72
+ "id": self.id,
73
+ "type": self.type.value,
74
+ "name": self.name,
75
+ "path": str(self.path),
76
+ "description": self.description,
77
+ "created_at": self.created_at.isoformat(),
78
+ "metadata": self.metadata,
79
+ "original_file": self.original_file,
80
+ }
81
+
82
+ @classmethod
83
+ def from_dict(cls, data: Dict[str, Any]) -> "Artifact":
84
+ """Deserialize from dictionary."""
85
+ return cls(
86
+ id=data["id"],
87
+ type=ArtifactType(data["type"]),
88
+ name=data["name"],
89
+ path=Path(data["path"]),
90
+ description=data.get("description", ""),
91
+ created_at=datetime.fromisoformat(data["created_at"]),
92
+ metadata=data.get("metadata", {}),
93
+ original_file=data.get("original_file"),
94
+ )
95
+
96
+
97
+ class ArtifactManager:
98
+ """
99
+ Manages QE artifacts that persist after ephemeral workspace reset.
100
+
101
+ Directory structure:
102
+ .superqode/
103
+ └── qe-artifacts/
104
+ ├── manifest.json # Index of all artifacts
105
+ ├── patches/ # Suggested fixes as patches
106
+ │ └── fix-001-user-service.patch
107
+ ├── generated-tests/ # Tests generated by QE
108
+ │ ├── unit/
109
+ │ ├── integration/
110
+ │ ├── api/
111
+ │ ├── fuzz/
112
+ │ └── ...
113
+ ├── qr/ # Quality Investigation Reports
114
+ │ └── qr-2024-01-08-001.md
115
+ ├── coverage/ # Coverage reports
116
+ ├── logs/ # Execution logs
117
+ └── evidence/ # Screenshots, traces, etc.
118
+ """
119
+
120
+ ARTIFACTS_DIR = "qe-artifacts"
121
+ MANIFEST_FILE = "manifest.json"
122
+
123
+ # Subdirectory mapping
124
+ TYPE_DIRS = {
125
+ ArtifactType.PATCH: "patches",
126
+ ArtifactType.SUGGESTED_FIX: "patches",
127
+ ArtifactType.TEST_UNIT: "generated-tests/unit",
128
+ ArtifactType.TEST_INTEGRATION: "generated-tests/integration",
129
+ ArtifactType.TEST_API: "generated-tests/api",
130
+ ArtifactType.TEST_CONTRACT: "generated-tests/contract",
131
+ ArtifactType.TEST_FUZZ: "generated-tests/fuzz",
132
+ ArtifactType.TEST_LOAD: "generated-tests/load",
133
+ ArtifactType.TEST_REGRESSION: "generated-tests/regression",
134
+ ArtifactType.TEST_E2E: "generated-tests/e2e",
135
+ ArtifactType.TEST_SECURITY: "generated-tests/security",
136
+ ArtifactType.QR: "qr",
137
+ ArtifactType.COVERAGE: "coverage",
138
+ ArtifactType.SUMMARY: ".",
139
+ ArtifactType.LOG: "logs",
140
+ ArtifactType.SCREENSHOT: "evidence",
141
+ ArtifactType.TRACE: "evidence",
142
+ ArtifactType.ERROR: "logs",
143
+ }
144
+
145
+ def __init__(self, project_root: Path):
146
+ self.project_root = project_root.resolve()
147
+ self.superqode_dir = self.project_root / ".superqode"
148
+ self.artifacts_dir = self.superqode_dir / self.ARTIFACTS_DIR
149
+ self.manifest_path = self.artifacts_dir / self.MANIFEST_FILE
150
+
151
+ self._artifacts: Dict[str, Artifact] = {}
152
+ self._artifact_counter = 0
153
+ self._session_id: Optional[str] = None
154
+
155
+ def initialize(self, session_id: str) -> None:
156
+ """Initialize the artifacts directory for a new session."""
157
+ self._session_id = session_id
158
+
159
+ # Create directory structure
160
+ self.artifacts_dir.mkdir(parents=True, exist_ok=True)
161
+
162
+ for subdir in set(self.TYPE_DIRS.values()):
163
+ if subdir != ".":
164
+ (self.artifacts_dir / subdir).mkdir(parents=True, exist_ok=True)
165
+
166
+ # Load existing manifest if present
167
+ self._load_manifest()
168
+
169
+ def _load_manifest(self) -> None:
170
+ """Load existing manifest file."""
171
+ if self.manifest_path.exists():
172
+ try:
173
+ data = json.loads(self.manifest_path.read_text())
174
+ for artifact_data in data.get("artifacts", []):
175
+ artifact = Artifact.from_dict(artifact_data)
176
+ self._artifacts[artifact.id] = artifact
177
+ self._artifact_counter = data.get("counter", 0)
178
+ except (json.JSONDecodeError, KeyError):
179
+ pass
180
+
181
+ def _save_manifest(self) -> None:
182
+ """Save manifest file."""
183
+ data = {
184
+ "session_id": self._session_id,
185
+ "updated_at": datetime.now().isoformat(),
186
+ "counter": self._artifact_counter,
187
+ "artifacts": [a.to_dict() for a in self._artifacts.values()],
188
+ }
189
+ self.manifest_path.write_text(json.dumps(data, indent=2))
190
+
191
+ def _generate_id(self, artifact_type: ArtifactType) -> str:
192
+ """Generate a unique artifact ID."""
193
+ self._artifact_counter += 1
194
+ type_prefix = artifact_type.value.replace("_", "-")
195
+ return f"{type_prefix}-{self._artifact_counter:03d}"
196
+
197
+ def _get_artifact_path(self, artifact_type: ArtifactType, filename: str) -> Path:
198
+ """Get the full path for an artifact file."""
199
+ subdir = self.TYPE_DIRS.get(artifact_type, "misc")
200
+ return self.artifacts_dir / subdir / filename
201
+
202
+ def save_patch(
203
+ self,
204
+ original_file: str,
205
+ original_content: str,
206
+ modified_content: str,
207
+ description: str = "",
208
+ ) -> Artifact:
209
+ """
210
+ Save a patch file showing changes to a source file.
211
+
212
+ Creates a unified diff patch that can be applied with `patch` or `git apply`.
213
+ """
214
+ artifact_id = self._generate_id(ArtifactType.PATCH)
215
+
216
+ # Generate unified diff
217
+ original_lines = original_content.splitlines(keepends=True)
218
+ modified_lines = modified_content.splitlines(keepends=True)
219
+
220
+ diff = difflib.unified_diff(
221
+ original_lines,
222
+ modified_lines,
223
+ fromfile=f"a/{original_file}",
224
+ tofile=f"b/{original_file}",
225
+ )
226
+ patch_content = "".join(diff)
227
+
228
+ # Save patch file
229
+ safe_filename = original_file.replace("/", "-").replace("\\", "-")
230
+ filename = f"{artifact_id}-{safe_filename}.patch"
231
+ artifact_path = self._get_artifact_path(ArtifactType.PATCH, filename)
232
+ artifact_path.write_text(patch_content)
233
+
234
+ artifact = Artifact(
235
+ id=artifact_id,
236
+ type=ArtifactType.PATCH,
237
+ name=filename,
238
+ path=artifact_path.relative_to(self.artifacts_dir),
239
+ description=description or f"Suggested fix for {original_file}",
240
+ original_file=original_file,
241
+ metadata={
242
+ "lines_added": sum(
243
+ 1 for line in patch_content.splitlines() if line.startswith("+")
244
+ ),
245
+ "lines_removed": sum(
246
+ 1 for line in patch_content.splitlines() if line.startswith("-")
247
+ ),
248
+ },
249
+ )
250
+
251
+ self._artifacts[artifact_id] = artifact
252
+ self._save_manifest()
253
+
254
+ return artifact
255
+
256
+ def save_generated_test(
257
+ self,
258
+ test_type: ArtifactType,
259
+ filename: str,
260
+ content: str,
261
+ description: str = "",
262
+ target_file: Optional[str] = None,
263
+ metadata: Optional[Dict[str, Any]] = None,
264
+ ) -> Artifact:
265
+ """
266
+ Save a generated test file.
267
+
268
+ Args:
269
+ test_type: Type of test (TEST_UNIT, TEST_INTEGRATION, etc.)
270
+ filename: Name for the test file
271
+ content: Test file content
272
+ description: Description of what the test covers
273
+ target_file: Source file the test is for
274
+ metadata: Additional metadata
275
+ """
276
+ if not test_type.value.startswith("test_"):
277
+ raise ValueError(f"Invalid test type: {test_type}")
278
+
279
+ artifact_id = self._generate_id(test_type)
280
+
281
+ # Save test file
282
+ artifact_path = self._get_artifact_path(test_type, filename)
283
+ artifact_path.write_text(content)
284
+
285
+ artifact = Artifact(
286
+ id=artifact_id,
287
+ type=test_type,
288
+ name=filename,
289
+ path=artifact_path.relative_to(self.artifacts_dir),
290
+ description=description,
291
+ original_file=target_file,
292
+ metadata=metadata or {},
293
+ )
294
+
295
+ self._artifacts[artifact_id] = artifact
296
+ self._save_manifest()
297
+
298
+ return artifact
299
+
300
+ def save_qir(
301
+ self,
302
+ content: str,
303
+ session_id: str,
304
+ metadata: Optional[Dict[str, Any]] = None,
305
+ ) -> Artifact:
306
+ """Save a Quality Investigation Report."""
307
+ artifact_id = self._generate_id(ArtifactType.QR)
308
+ timestamp = datetime.now().strftime("%Y-%m-%d")
309
+ filename = f"qr-{timestamp}-{session_id[:8]}.md"
310
+
311
+ artifact_path = self._get_artifact_path(ArtifactType.QR, filename)
312
+ artifact_path.write_text(content)
313
+
314
+ # Also save JSON version for CI
315
+ json_filename = filename.replace(".md", ".json")
316
+ json_path = self._get_artifact_path(ArtifactType.QR, json_filename)
317
+ json_path.write_text(json.dumps(metadata or {}, indent=2))
318
+
319
+ artifact = Artifact(
320
+ id=artifact_id,
321
+ type=ArtifactType.QR,
322
+ name=filename,
323
+ path=artifact_path.relative_to(self.artifacts_dir),
324
+ description="Quality Investigation Report",
325
+ metadata=metadata or {},
326
+ )
327
+
328
+ self._artifacts[artifact_id] = artifact
329
+ self._save_manifest()
330
+
331
+ return artifact
332
+
333
+ def save_log(
334
+ self,
335
+ name: str,
336
+ content: str,
337
+ log_type: str = "execution",
338
+ ) -> Artifact:
339
+ """Save an execution log."""
340
+ artifact_id = self._generate_id(ArtifactType.LOG)
341
+ timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
342
+ filename = f"{log_type}-{timestamp}.log"
343
+
344
+ artifact_path = self._get_artifact_path(ArtifactType.LOG, filename)
345
+ artifact_path.write_text(content)
346
+
347
+ artifact = Artifact(
348
+ id=artifact_id,
349
+ type=ArtifactType.LOG,
350
+ name=filename,
351
+ path=artifact_path.relative_to(self.artifacts_dir),
352
+ description=name,
353
+ metadata={"log_type": log_type},
354
+ )
355
+
356
+ self._artifacts[artifact_id] = artifact
357
+ self._save_manifest()
358
+
359
+ return artifact
360
+
361
+ def save_file(
362
+ self,
363
+ artifact_type: ArtifactType,
364
+ filename: str,
365
+ content: str | bytes,
366
+ description: str = "",
367
+ metadata: Optional[Dict[str, Any]] = None,
368
+ ) -> Artifact:
369
+ """Save a generic artifact file."""
370
+ artifact_id = self._generate_id(artifact_type)
371
+ artifact_path = self._get_artifact_path(artifact_type, filename)
372
+
373
+ if isinstance(content, bytes):
374
+ artifact_path.write_bytes(content)
375
+ else:
376
+ artifact_path.write_text(content)
377
+
378
+ artifact = Artifact(
379
+ id=artifact_id,
380
+ type=artifact_type,
381
+ name=filename,
382
+ path=artifact_path.relative_to(self.artifacts_dir),
383
+ description=description,
384
+ metadata=metadata or {},
385
+ )
386
+
387
+ self._artifacts[artifact_id] = artifact
388
+ self._save_manifest()
389
+
390
+ return artifact
391
+
392
+ def get_artifact(self, artifact_id: str) -> Optional[Artifact]:
393
+ """Get an artifact by ID."""
394
+ return self._artifacts.get(artifact_id)
395
+
396
+ def get_artifacts_by_type(self, artifact_type: ArtifactType) -> List[Artifact]:
397
+ """Get all artifacts of a specific type."""
398
+ return [a for a in self._artifacts.values() if a.type == artifact_type]
399
+
400
+ def get_all_artifacts(self) -> List[Artifact]:
401
+ """Get all artifacts."""
402
+ return list(self._artifacts.values())
403
+
404
+ def get_artifact_content(self, artifact_id: str) -> Optional[str]:
405
+ """Get the content of an artifact."""
406
+ artifact = self._artifacts.get(artifact_id)
407
+ if not artifact:
408
+ return None
409
+
410
+ full_path = self.artifacts_dir / artifact.path
411
+ if full_path.exists():
412
+ return full_path.read_text()
413
+ return None
414
+
415
+ def list_patches(self) -> List[Artifact]:
416
+ """List all patch artifacts."""
417
+ return self.get_artifacts_by_type(ArtifactType.PATCH)
418
+
419
+ def list_generated_tests(self) -> List[Artifact]:
420
+ """List all generated test artifacts."""
421
+ test_types = [t for t in ArtifactType if t.value.startswith("test_")]
422
+ result = []
423
+ for t in test_types:
424
+ result.extend(self.get_artifacts_by_type(t))
425
+ return result
426
+
427
+ def list_qirs(self) -> List[Artifact]:
428
+ """List all QIR artifacts."""
429
+ return self.get_artifacts_by_type(ArtifactType.QR)
430
+
431
+ def get_summary(self) -> Dict[str, Any]:
432
+ """Get a summary of all artifacts."""
433
+ summary = {
434
+ "total_artifacts": len(self._artifacts),
435
+ "by_type": {},
436
+ "patches": len(self.list_patches()),
437
+ "generated_tests": len(self.list_generated_tests()),
438
+ "qirs": len(self.list_qirs()),
439
+ }
440
+
441
+ for artifact_type in ArtifactType:
442
+ count = len(self.get_artifacts_by_type(artifact_type))
443
+ if count > 0:
444
+ summary["by_type"][artifact_type.value] = count
445
+
446
+ return summary
447
+
448
+ def cleanup(self, keep_qirs: bool = True) -> int:
449
+ """
450
+ Clean up artifact directory.
451
+
452
+ Args:
453
+ keep_qirs: If True, keep QIR files
454
+
455
+ Returns:
456
+ Number of files removed
457
+ """
458
+ removed = 0
459
+
460
+ for artifact_id, artifact in list(self._artifacts.items()):
461
+ if keep_qirs and artifact.type == ArtifactType.QR:
462
+ continue
463
+
464
+ full_path = self.artifacts_dir / artifact.path
465
+ if full_path.exists():
466
+ full_path.unlink()
467
+ removed += 1
468
+
469
+ del self._artifacts[artifact_id]
470
+
471
+ self._save_manifest()
472
+ return removed