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.
- superqode/__init__.py +33 -0
- superqode/acp/__init__.py +23 -0
- superqode/acp/client.py +913 -0
- superqode/acp/permission_screen.py +457 -0
- superqode/acp/types.py +480 -0
- superqode/acp_discovery.py +856 -0
- superqode/agent/__init__.py +22 -0
- superqode/agent/edit_strategies.py +334 -0
- superqode/agent/loop.py +892 -0
- superqode/agent/qe_report_templates.py +39 -0
- superqode/agent/system_prompts.py +353 -0
- superqode/agent_output.py +721 -0
- superqode/agent_stream.py +953 -0
- superqode/agents/__init__.py +59 -0
- superqode/agents/acp_registry.py +305 -0
- superqode/agents/client.py +249 -0
- superqode/agents/data/augmentcode.com.toml +51 -0
- superqode/agents/data/cagent.dev.toml +51 -0
- superqode/agents/data/claude.com.toml +60 -0
- superqode/agents/data/codeassistant.dev.toml +51 -0
- superqode/agents/data/codex.openai.com.toml +57 -0
- superqode/agents/data/fastagent.ai.toml +66 -0
- superqode/agents/data/geminicli.com.toml +77 -0
- superqode/agents/data/goose.block.xyz.toml +54 -0
- superqode/agents/data/junie.jetbrains.com.toml +56 -0
- superqode/agents/data/kimi.moonshot.cn.toml +57 -0
- superqode/agents/data/llmlingagent.dev.toml +51 -0
- superqode/agents/data/molt.bot.toml +49 -0
- superqode/agents/data/opencode.ai.toml +60 -0
- superqode/agents/data/stakpak.dev.toml +51 -0
- superqode/agents/data/vtcode.dev.toml +51 -0
- superqode/agents/discovery.py +266 -0
- superqode/agents/messaging.py +160 -0
- superqode/agents/persona.py +166 -0
- superqode/agents/registry.py +421 -0
- superqode/agents/schema.py +72 -0
- superqode/agents/unified.py +367 -0
- superqode/app/__init__.py +111 -0
- superqode/app/constants.py +314 -0
- superqode/app/css.py +366 -0
- superqode/app/models.py +118 -0
- superqode/app/suggester.py +125 -0
- superqode/app/widgets.py +1591 -0
- superqode/app_enhanced.py +399 -0
- superqode/app_main.py +17187 -0
- superqode/approval.py +312 -0
- superqode/atomic.py +296 -0
- superqode/commands/__init__.py +1 -0
- superqode/commands/acp.py +965 -0
- superqode/commands/agents.py +180 -0
- superqode/commands/auth.py +278 -0
- superqode/commands/config.py +374 -0
- superqode/commands/init.py +826 -0
- superqode/commands/providers.py +819 -0
- superqode/commands/qe.py +1145 -0
- superqode/commands/roles.py +380 -0
- superqode/commands/serve.py +172 -0
- superqode/commands/suggestions.py +127 -0
- superqode/commands/superqe.py +460 -0
- superqode/config/__init__.py +51 -0
- superqode/config/loader.py +812 -0
- superqode/config/schema.py +498 -0
- superqode/core/__init__.py +111 -0
- superqode/core/roles.py +281 -0
- superqode/danger.py +386 -0
- superqode/data/superqode-template.yaml +1522 -0
- superqode/design_system.py +1080 -0
- superqode/dialogs/__init__.py +6 -0
- superqode/dialogs/base.py +39 -0
- superqode/dialogs/model.py +130 -0
- superqode/dialogs/provider.py +870 -0
- superqode/diff_view.py +919 -0
- superqode/enterprise.py +21 -0
- superqode/evaluation/__init__.py +25 -0
- superqode/evaluation/adapters.py +93 -0
- superqode/evaluation/behaviors.py +89 -0
- superqode/evaluation/engine.py +209 -0
- superqode/evaluation/scenarios.py +96 -0
- superqode/execution/__init__.py +36 -0
- superqode/execution/linter.py +538 -0
- superqode/execution/modes.py +347 -0
- superqode/execution/resolver.py +283 -0
- superqode/execution/runner.py +642 -0
- superqode/file_explorer.py +811 -0
- superqode/file_viewer.py +471 -0
- superqode/flash.py +183 -0
- superqode/guidance/__init__.py +58 -0
- superqode/guidance/config.py +203 -0
- superqode/guidance/prompts.py +71 -0
- superqode/harness/__init__.py +54 -0
- superqode/harness/accelerator.py +291 -0
- superqode/harness/config.py +319 -0
- superqode/harness/validator.py +147 -0
- superqode/history.py +279 -0
- superqode/integrations/superopt_runner.py +124 -0
- superqode/logging/__init__.py +49 -0
- superqode/logging/adapters.py +219 -0
- superqode/logging/formatter.py +923 -0
- superqode/logging/integration.py +341 -0
- superqode/logging/sinks.py +170 -0
- superqode/logging/unified_log.py +417 -0
- superqode/lsp/__init__.py +26 -0
- superqode/lsp/client.py +544 -0
- superqode/main.py +1069 -0
- superqode/mcp/__init__.py +89 -0
- superqode/mcp/auth_storage.py +380 -0
- superqode/mcp/client.py +1236 -0
- superqode/mcp/config.py +319 -0
- superqode/mcp/integration.py +337 -0
- superqode/mcp/oauth.py +436 -0
- superqode/mcp/oauth_callback.py +385 -0
- superqode/mcp/types.py +290 -0
- superqode/memory/__init__.py +31 -0
- superqode/memory/feedback.py +342 -0
- superqode/memory/store.py +522 -0
- superqode/notifications.py +369 -0
- superqode/optimization/__init__.py +5 -0
- superqode/optimization/config.py +33 -0
- superqode/permissions/__init__.py +25 -0
- superqode/permissions/rules.py +488 -0
- superqode/plan.py +323 -0
- superqode/providers/__init__.py +33 -0
- superqode/providers/gateway/__init__.py +165 -0
- superqode/providers/gateway/base.py +228 -0
- superqode/providers/gateway/litellm_gateway.py +1170 -0
- superqode/providers/gateway/openresponses_gateway.py +436 -0
- superqode/providers/health.py +297 -0
- superqode/providers/huggingface/__init__.py +74 -0
- superqode/providers/huggingface/downloader.py +472 -0
- superqode/providers/huggingface/endpoints.py +442 -0
- superqode/providers/huggingface/hub.py +531 -0
- superqode/providers/huggingface/inference.py +394 -0
- superqode/providers/huggingface/transformers_runner.py +516 -0
- superqode/providers/local/__init__.py +100 -0
- superqode/providers/local/base.py +438 -0
- superqode/providers/local/discovery.py +418 -0
- superqode/providers/local/lmstudio.py +256 -0
- superqode/providers/local/mlx.py +457 -0
- superqode/providers/local/ollama.py +486 -0
- superqode/providers/local/sglang.py +268 -0
- superqode/providers/local/tgi.py +260 -0
- superqode/providers/local/tool_support.py +477 -0
- superqode/providers/local/vllm.py +258 -0
- superqode/providers/manager.py +1338 -0
- superqode/providers/models.py +1016 -0
- superqode/providers/models_dev.py +578 -0
- superqode/providers/openresponses/__init__.py +87 -0
- superqode/providers/openresponses/converters/__init__.py +17 -0
- superqode/providers/openresponses/converters/messages.py +343 -0
- superqode/providers/openresponses/converters/tools.py +268 -0
- superqode/providers/openresponses/schema/__init__.py +56 -0
- superqode/providers/openresponses/schema/models.py +585 -0
- superqode/providers/openresponses/streaming/__init__.py +5 -0
- superqode/providers/openresponses/streaming/parser.py +338 -0
- superqode/providers/openresponses/tools/__init__.py +21 -0
- superqode/providers/openresponses/tools/apply_patch.py +352 -0
- superqode/providers/openresponses/tools/code_interpreter.py +290 -0
- superqode/providers/openresponses/tools/file_search.py +333 -0
- superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
- superqode/providers/registry.py +716 -0
- superqode/providers/usage.py +332 -0
- superqode/pure_mode.py +384 -0
- superqode/qr/__init__.py +23 -0
- superqode/qr/dashboard.py +781 -0
- superqode/qr/generator.py +1018 -0
- superqode/qr/templates.py +135 -0
- superqode/safety/__init__.py +41 -0
- superqode/safety/sandbox.py +413 -0
- superqode/safety/warnings.py +256 -0
- superqode/server/__init__.py +33 -0
- superqode/server/lsp_server.py +775 -0
- superqode/server/web.py +250 -0
- superqode/session/__init__.py +25 -0
- superqode/session/persistence.py +580 -0
- superqode/session/sharing.py +477 -0
- superqode/session.py +475 -0
- superqode/sidebar.py +2991 -0
- superqode/stream_view.py +648 -0
- superqode/styles/__init__.py +3 -0
- superqode/superqe/__init__.py +184 -0
- superqode/superqe/acp_runner.py +1064 -0
- superqode/superqe/constitution/__init__.py +62 -0
- superqode/superqe/constitution/evaluator.py +308 -0
- superqode/superqe/constitution/loader.py +432 -0
- superqode/superqe/constitution/schema.py +250 -0
- superqode/superqe/events.py +591 -0
- superqode/superqe/frameworks/__init__.py +65 -0
- superqode/superqe/frameworks/base.py +234 -0
- superqode/superqe/frameworks/e2e.py +263 -0
- superqode/superqe/frameworks/executor.py +237 -0
- superqode/superqe/frameworks/javascript.py +409 -0
- superqode/superqe/frameworks/python.py +373 -0
- superqode/superqe/frameworks/registry.py +92 -0
- superqode/superqe/mcp_tools/__init__.py +47 -0
- superqode/superqe/mcp_tools/core_tools.py +418 -0
- superqode/superqe/mcp_tools/registry.py +230 -0
- superqode/superqe/mcp_tools/testing_tools.py +167 -0
- superqode/superqe/noise.py +89 -0
- superqode/superqe/orchestrator.py +778 -0
- superqode/superqe/roles.py +609 -0
- superqode/superqe/session.py +713 -0
- superqode/superqe/skills/__init__.py +57 -0
- superqode/superqe/skills/base.py +106 -0
- superqode/superqe/skills/core_skills.py +899 -0
- superqode/superqe/skills/registry.py +90 -0
- superqode/superqe/verifier.py +101 -0
- superqode/superqe_cli.py +76 -0
- superqode/tool_call.py +358 -0
- superqode/tools/__init__.py +93 -0
- superqode/tools/agent_tools.py +496 -0
- superqode/tools/base.py +324 -0
- superqode/tools/batch_tool.py +133 -0
- superqode/tools/diagnostics.py +311 -0
- superqode/tools/edit_tools.py +653 -0
- superqode/tools/enhanced_base.py +515 -0
- superqode/tools/file_tools.py +269 -0
- superqode/tools/file_tracking.py +45 -0
- superqode/tools/lsp_tools.py +610 -0
- superqode/tools/network_tools.py +350 -0
- superqode/tools/permissions.py +400 -0
- superqode/tools/question_tool.py +324 -0
- superqode/tools/search_tools.py +598 -0
- superqode/tools/shell_tools.py +259 -0
- superqode/tools/todo_tools.py +121 -0
- superqode/tools/validation.py +80 -0
- superqode/tools/web_tools.py +639 -0
- superqode/tui.py +1152 -0
- superqode/tui_integration.py +875 -0
- superqode/tui_widgets/__init__.py +27 -0
- superqode/tui_widgets/widgets/__init__.py +18 -0
- superqode/tui_widgets/widgets/progress.py +185 -0
- superqode/tui_widgets/widgets/tool_display.py +188 -0
- superqode/undo_manager.py +574 -0
- superqode/utils/__init__.py +5 -0
- superqode/utils/error_handling.py +323 -0
- superqode/utils/fuzzy.py +257 -0
- superqode/widgets/__init__.py +477 -0
- superqode/widgets/agent_collab.py +390 -0
- superqode/widgets/agent_store.py +936 -0
- superqode/widgets/agent_switcher.py +395 -0
- superqode/widgets/animation_manager.py +284 -0
- superqode/widgets/code_context.py +356 -0
- superqode/widgets/command_palette.py +412 -0
- superqode/widgets/connection_status.py +537 -0
- superqode/widgets/conversation_history.py +470 -0
- superqode/widgets/diff_indicator.py +155 -0
- superqode/widgets/enhanced_status_bar.py +385 -0
- superqode/widgets/enhanced_toast.py +476 -0
- superqode/widgets/file_browser.py +809 -0
- superqode/widgets/file_reference.py +585 -0
- superqode/widgets/issue_timeline.py +340 -0
- superqode/widgets/leader_key.py +264 -0
- superqode/widgets/mode_switcher.py +445 -0
- superqode/widgets/model_picker.py +234 -0
- superqode/widgets/permission_preview.py +1205 -0
- superqode/widgets/prompt.py +358 -0
- superqode/widgets/provider_connect.py +725 -0
- superqode/widgets/pty_shell.py +587 -0
- superqode/widgets/qe_dashboard.py +321 -0
- superqode/widgets/resizable_sidebar.py +377 -0
- superqode/widgets/response_changes.py +218 -0
- superqode/widgets/response_display.py +528 -0
- superqode/widgets/rich_tool_display.py +613 -0
- superqode/widgets/sidebar_panels.py +1180 -0
- superqode/widgets/slash_complete.py +356 -0
- superqode/widgets/split_view.py +612 -0
- superqode/widgets/status_bar.py +273 -0
- superqode/widgets/superqode_display.py +786 -0
- superqode/widgets/thinking_display.py +815 -0
- superqode/widgets/throbber.py +87 -0
- superqode/widgets/toast.py +206 -0
- superqode/widgets/unified_output.py +1073 -0
- superqode/workspace/__init__.py +75 -0
- superqode/workspace/artifacts.py +472 -0
- superqode/workspace/coordinator.py +353 -0
- superqode/workspace/diff_tracker.py +429 -0
- superqode/workspace/git_guard.py +373 -0
- superqode/workspace/git_snapshot.py +526 -0
- superqode/workspace/manager.py +750 -0
- superqode/workspace/snapshot.py +357 -0
- superqode/workspace/watcher.py +535 -0
- superqode/workspace/worktree.py +440 -0
- superqode-0.1.5.dist-info/METADATA +204 -0
- superqode-0.1.5.dist-info/RECORD +288 -0
- superqode-0.1.5.dist-info/WHEEL +5 -0
- superqode-0.1.5.dist-info/entry_points.txt +3 -0
- superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
- 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
|