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,522 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Memory Store - Persistent storage for QE learnings.
|
|
3
|
+
|
|
4
|
+
Stores project-specific memory that persists across QE sessions:
|
|
5
|
+
- Issue patterns (recurring issues)
|
|
6
|
+
- False positive suppressions
|
|
7
|
+
- Successful fix patterns
|
|
8
|
+
- File risk scores
|
|
9
|
+
- Role effectiveness metrics
|
|
10
|
+
|
|
11
|
+
Storage locations:
|
|
12
|
+
- ~/.superqode/memory/project-{hash}.json (per-project, user-local)
|
|
13
|
+
- .superqode/memory.json (team-shared, in repo)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import hashlib
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
from dataclasses import dataclass, field, asdict
|
|
22
|
+
from datetime import datetime
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, Dict, List, Optional
|
|
25
|
+
import logging
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class IssuePattern:
|
|
32
|
+
"""A recurring issue pattern detected across sessions."""
|
|
33
|
+
|
|
34
|
+
fingerprint: str # Hash of title + category
|
|
35
|
+
title: str
|
|
36
|
+
category: str
|
|
37
|
+
severity: str
|
|
38
|
+
occurrences: int = 1
|
|
39
|
+
first_seen: str = "" # ISO timestamp
|
|
40
|
+
last_seen: str = "" # ISO timestamp
|
|
41
|
+
files_affected: List[str] = field(default_factory=list)
|
|
42
|
+
avg_confidence: float = 0.8
|
|
43
|
+
|
|
44
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
45
|
+
return asdict(self)
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def from_dict(cls, data: Dict[str, Any]) -> "IssuePattern":
|
|
49
|
+
return cls(**data)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class Suppression:
|
|
54
|
+
"""A false positive suppression rule."""
|
|
55
|
+
|
|
56
|
+
id: str
|
|
57
|
+
pattern: str # What to match (title, rule_id, or fingerprint)
|
|
58
|
+
pattern_type: str # "title", "rule_id", "fingerprint", "file_pattern"
|
|
59
|
+
reason: str
|
|
60
|
+
created_at: str # ISO timestamp
|
|
61
|
+
created_by: str # User or "system"
|
|
62
|
+
expires_at: Optional[str] = None # Optional expiration
|
|
63
|
+
scope: str = "project" # "project" or "global"
|
|
64
|
+
|
|
65
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
66
|
+
return asdict(self)
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Suppression":
|
|
70
|
+
return cls(**data)
|
|
71
|
+
|
|
72
|
+
def is_expired(self) -> bool:
|
|
73
|
+
"""Check if suppression has expired."""
|
|
74
|
+
if not self.expires_at:
|
|
75
|
+
return False
|
|
76
|
+
try:
|
|
77
|
+
expires = datetime.fromisoformat(self.expires_at)
|
|
78
|
+
return datetime.now() > expires
|
|
79
|
+
except ValueError:
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class FixPattern:
|
|
85
|
+
"""A successful fix pattern that can be reused."""
|
|
86
|
+
|
|
87
|
+
id: str
|
|
88
|
+
issue_fingerprint: str # Links to IssuePattern
|
|
89
|
+
issue_title: str
|
|
90
|
+
fix_description: str
|
|
91
|
+
patch_template: Optional[str] = None
|
|
92
|
+
success_rate: float = 1.0
|
|
93
|
+
times_applied: int = 1
|
|
94
|
+
times_succeeded: int = 1
|
|
95
|
+
created_at: str = ""
|
|
96
|
+
|
|
97
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
98
|
+
return asdict(self)
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def from_dict(cls, data: Dict[str, Any]) -> "FixPattern":
|
|
102
|
+
return cls(**data)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class RoleMetrics:
|
|
107
|
+
"""Effectiveness metrics for a QE role."""
|
|
108
|
+
|
|
109
|
+
role_name: str
|
|
110
|
+
sessions_run: int = 0
|
|
111
|
+
total_findings: int = 0
|
|
112
|
+
confirmed_findings: int = 0 # User validated as true positives
|
|
113
|
+
false_positives: int = 0
|
|
114
|
+
avg_session_duration_seconds: float = 0.0
|
|
115
|
+
accuracy_rate: float = 1.0 # confirmed / (confirmed + false_positives)
|
|
116
|
+
|
|
117
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
118
|
+
return asdict(self)
|
|
119
|
+
|
|
120
|
+
@classmethod
|
|
121
|
+
def from_dict(cls, data: Dict[str, Any]) -> "RoleMetrics":
|
|
122
|
+
return cls(**data)
|
|
123
|
+
|
|
124
|
+
def update_accuracy(self) -> None:
|
|
125
|
+
"""Recalculate accuracy rate."""
|
|
126
|
+
total = self.confirmed_findings + self.false_positives
|
|
127
|
+
if total > 0:
|
|
128
|
+
self.accuracy_rate = self.confirmed_findings / total
|
|
129
|
+
else:
|
|
130
|
+
self.accuracy_rate = 1.0
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass
|
|
134
|
+
class QEMemory:
|
|
135
|
+
"""
|
|
136
|
+
Complete memory store for a project.
|
|
137
|
+
|
|
138
|
+
Contains all learnings from past QE sessions.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
project_id: str # Hash of project root path
|
|
142
|
+
project_name: str
|
|
143
|
+
created_at: str
|
|
144
|
+
updated_at: str
|
|
145
|
+
|
|
146
|
+
# Learnings
|
|
147
|
+
issue_patterns: List[IssuePattern] = field(default_factory=list)
|
|
148
|
+
suppressions: List[Suppression] = field(default_factory=list)
|
|
149
|
+
fix_patterns: List[FixPattern] = field(default_factory=list)
|
|
150
|
+
|
|
151
|
+
# Risk analysis
|
|
152
|
+
file_risk_map: Dict[str, float] = field(default_factory=dict)
|
|
153
|
+
|
|
154
|
+
# Role effectiveness
|
|
155
|
+
role_metrics: Dict[str, RoleMetrics] = field(default_factory=dict)
|
|
156
|
+
|
|
157
|
+
# Statistics
|
|
158
|
+
total_sessions: int = 0
|
|
159
|
+
total_findings: int = 0
|
|
160
|
+
total_suppressions_applied: int = 0
|
|
161
|
+
|
|
162
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
163
|
+
return {
|
|
164
|
+
"project_id": self.project_id,
|
|
165
|
+
"project_name": self.project_name,
|
|
166
|
+
"created_at": self.created_at,
|
|
167
|
+
"updated_at": self.updated_at,
|
|
168
|
+
"issue_patterns": [p.to_dict() for p in self.issue_patterns],
|
|
169
|
+
"suppressions": [s.to_dict() for s in self.suppressions],
|
|
170
|
+
"fix_patterns": [f.to_dict() for f in self.fix_patterns],
|
|
171
|
+
"file_risk_map": self.file_risk_map,
|
|
172
|
+
"role_metrics": {k: v.to_dict() for k, v in self.role_metrics.items()},
|
|
173
|
+
"total_sessions": self.total_sessions,
|
|
174
|
+
"total_findings": self.total_findings,
|
|
175
|
+
"total_suppressions_applied": self.total_suppressions_applied,
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
@classmethod
|
|
179
|
+
def from_dict(cls, data: Dict[str, Any]) -> "QEMemory":
|
|
180
|
+
return cls(
|
|
181
|
+
project_id=data.get("project_id", ""),
|
|
182
|
+
project_name=data.get("project_name", ""),
|
|
183
|
+
created_at=data.get("created_at", ""),
|
|
184
|
+
updated_at=data.get("updated_at", ""),
|
|
185
|
+
issue_patterns=[IssuePattern.from_dict(p) for p in data.get("issue_patterns", [])],
|
|
186
|
+
suppressions=[Suppression.from_dict(s) for s in data.get("suppressions", [])],
|
|
187
|
+
fix_patterns=[FixPattern.from_dict(f) for f in data.get("fix_patterns", [])],
|
|
188
|
+
file_risk_map=data.get("file_risk_map", {}),
|
|
189
|
+
role_metrics={
|
|
190
|
+
k: RoleMetrics.from_dict(v) for k, v in data.get("role_metrics", {}).items()
|
|
191
|
+
},
|
|
192
|
+
total_sessions=data.get("total_sessions", 0),
|
|
193
|
+
total_findings=data.get("total_findings", 0),
|
|
194
|
+
total_suppressions_applied=data.get("total_suppressions_applied", 0),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
def get_active_suppressions(self) -> List[Suppression]:
|
|
198
|
+
"""Get non-expired suppressions."""
|
|
199
|
+
return [s for s in self.suppressions if not s.is_expired()]
|
|
200
|
+
|
|
201
|
+
def get_file_risk(self, file_path: str) -> float:
|
|
202
|
+
"""Get risk score for a file (0.0 to 1.0)."""
|
|
203
|
+
return self.file_risk_map.get(file_path, 0.5)
|
|
204
|
+
|
|
205
|
+
def update_file_risk(self, file_path: str, finding_severity: str) -> None:
|
|
206
|
+
"""Update risk score based on a new finding."""
|
|
207
|
+
severity_weights = {
|
|
208
|
+
"critical": 0.3,
|
|
209
|
+
"high": 0.2,
|
|
210
|
+
"medium": 0.1,
|
|
211
|
+
"low": 0.05,
|
|
212
|
+
"info": 0.02,
|
|
213
|
+
}
|
|
214
|
+
delta = severity_weights.get(finding_severity, 0.05)
|
|
215
|
+
current = self.file_risk_map.get(file_path, 0.5)
|
|
216
|
+
# Increase risk, cap at 1.0
|
|
217
|
+
self.file_risk_map[file_path] = min(1.0, current + delta)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class MemoryStore:
|
|
221
|
+
"""
|
|
222
|
+
Manages persistence and retrieval of QE memory.
|
|
223
|
+
|
|
224
|
+
Storage strategy:
|
|
225
|
+
1. User-local: ~/.superqode/memory/project-{hash}.json
|
|
226
|
+
2. Team-shared: .superqode/memory.json (committed to repo)
|
|
227
|
+
|
|
228
|
+
The two are merged, with user-local taking precedence for conflicts.
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
def __init__(self, project_root: Path):
|
|
232
|
+
self.project_root = project_root.resolve()
|
|
233
|
+
self.project_id = self._compute_project_id()
|
|
234
|
+
self.project_name = project_root.name
|
|
235
|
+
|
|
236
|
+
# Storage paths
|
|
237
|
+
self._user_dir = Path.home() / ".superqode" / "memory"
|
|
238
|
+
self._user_file = self._user_dir / f"project-{self.project_id}.json"
|
|
239
|
+
self._team_file = project_root / ".superqode" / "memory.json"
|
|
240
|
+
|
|
241
|
+
self._memory: Optional[QEMemory] = None
|
|
242
|
+
|
|
243
|
+
def _compute_project_id(self) -> str:
|
|
244
|
+
"""Compute a stable ID for the project."""
|
|
245
|
+
return hashlib.sha256(str(self.project_root).encode()).hexdigest()[:16]
|
|
246
|
+
|
|
247
|
+
def load(self) -> QEMemory:
|
|
248
|
+
"""Load memory from storage, merging user and team files."""
|
|
249
|
+
if self._memory is not None:
|
|
250
|
+
return self._memory
|
|
251
|
+
|
|
252
|
+
user_memory = self._load_file(self._user_file)
|
|
253
|
+
team_memory = self._load_file(self._team_file)
|
|
254
|
+
|
|
255
|
+
if user_memory and team_memory:
|
|
256
|
+
# Merge: user takes precedence
|
|
257
|
+
self._memory = self._merge_memories(user_memory, team_memory)
|
|
258
|
+
elif user_memory:
|
|
259
|
+
self._memory = user_memory
|
|
260
|
+
elif team_memory:
|
|
261
|
+
self._memory = team_memory
|
|
262
|
+
else:
|
|
263
|
+
# Create new memory
|
|
264
|
+
now = datetime.now().isoformat()
|
|
265
|
+
self._memory = QEMemory(
|
|
266
|
+
project_id=self.project_id,
|
|
267
|
+
project_name=self.project_name,
|
|
268
|
+
created_at=now,
|
|
269
|
+
updated_at=now,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
return self._memory
|
|
273
|
+
|
|
274
|
+
def _load_file(self, path: Path) -> Optional[QEMemory]:
|
|
275
|
+
"""Load memory from a single file."""
|
|
276
|
+
if not path.exists():
|
|
277
|
+
return None
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
data = json.loads(path.read_text())
|
|
281
|
+
return QEMemory.from_dict(data)
|
|
282
|
+
except Exception as e:
|
|
283
|
+
logger.warning(f"Failed to load memory from {path}: {e}")
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
def _merge_memories(self, user: QEMemory, team: QEMemory) -> QEMemory:
|
|
287
|
+
"""Merge user and team memories."""
|
|
288
|
+
# Start with team as base
|
|
289
|
+
merged = QEMemory.from_dict(team.to_dict())
|
|
290
|
+
|
|
291
|
+
# Add user suppressions (these are personal)
|
|
292
|
+
user_supp_ids = {s.id for s in user.suppressions}
|
|
293
|
+
team_supp_ids = {s.id for s in team.suppressions}
|
|
294
|
+
for supp in user.suppressions:
|
|
295
|
+
if supp.id not in team_supp_ids:
|
|
296
|
+
merged.suppressions.append(supp)
|
|
297
|
+
|
|
298
|
+
# Merge issue patterns (combine occurrences)
|
|
299
|
+
user_patterns = {p.fingerprint: p for p in user.issue_patterns}
|
|
300
|
+
for pattern in merged.issue_patterns:
|
|
301
|
+
if pattern.fingerprint in user_patterns:
|
|
302
|
+
up = user_patterns[pattern.fingerprint]
|
|
303
|
+
pattern.occurrences = max(pattern.occurrences, up.occurrences)
|
|
304
|
+
pattern.last_seen = max(pattern.last_seen, up.last_seen)
|
|
305
|
+
|
|
306
|
+
# Use user's role metrics (more recent)
|
|
307
|
+
merged.role_metrics.update(user.role_metrics)
|
|
308
|
+
|
|
309
|
+
# Use user's file risk map
|
|
310
|
+
merged.file_risk_map.update(user.file_risk_map)
|
|
311
|
+
|
|
312
|
+
return merged
|
|
313
|
+
|
|
314
|
+
def save(self, to_team: bool = False) -> None:
|
|
315
|
+
"""
|
|
316
|
+
Save memory to storage.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
to_team: If True, also save to team file (.superqode/memory.json)
|
|
320
|
+
"""
|
|
321
|
+
if self._memory is None:
|
|
322
|
+
return
|
|
323
|
+
|
|
324
|
+
self._memory.updated_at = datetime.now().isoformat()
|
|
325
|
+
data = json.dumps(self._memory.to_dict(), indent=2)
|
|
326
|
+
|
|
327
|
+
# Always save to user file
|
|
328
|
+
self._user_dir.mkdir(parents=True, exist_ok=True)
|
|
329
|
+
self._user_file.write_text(data)
|
|
330
|
+
logger.debug(f"Saved memory to {self._user_file}")
|
|
331
|
+
|
|
332
|
+
# Optionally save to team file
|
|
333
|
+
if to_team:
|
|
334
|
+
self._team_file.parent.mkdir(parents=True, exist_ok=True)
|
|
335
|
+
# Filter out user-specific data for team file
|
|
336
|
+
team_data = self._prepare_team_data()
|
|
337
|
+
self._team_file.write_text(json.dumps(team_data, indent=2))
|
|
338
|
+
logger.debug(f"Saved team memory to {self._team_file}")
|
|
339
|
+
|
|
340
|
+
def _prepare_team_data(self) -> Dict[str, Any]:
|
|
341
|
+
"""Prepare memory data for team sharing (remove personal data)."""
|
|
342
|
+
if self._memory is None:
|
|
343
|
+
return {}
|
|
344
|
+
|
|
345
|
+
data = self._memory.to_dict()
|
|
346
|
+
# Keep only team-scope suppressions
|
|
347
|
+
data["suppressions"] = [s for s in data["suppressions"] if s.get("scope") == "team"]
|
|
348
|
+
return data
|
|
349
|
+
|
|
350
|
+
def add_suppression(
|
|
351
|
+
self,
|
|
352
|
+
pattern: str,
|
|
353
|
+
pattern_type: str,
|
|
354
|
+
reason: str,
|
|
355
|
+
scope: str = "project",
|
|
356
|
+
expires_in_days: Optional[int] = None,
|
|
357
|
+
) -> Suppression:
|
|
358
|
+
"""Add a new suppression rule."""
|
|
359
|
+
memory = self.load()
|
|
360
|
+
|
|
361
|
+
now = datetime.now()
|
|
362
|
+
supp_id = hashlib.sha256(
|
|
363
|
+
f"{pattern}:{pattern_type}:{now.isoformat()}".encode()
|
|
364
|
+
).hexdigest()[:12]
|
|
365
|
+
|
|
366
|
+
expires_at = None
|
|
367
|
+
if expires_in_days:
|
|
368
|
+
from datetime import timedelta
|
|
369
|
+
|
|
370
|
+
expires_at = (now + timedelta(days=expires_in_days)).isoformat()
|
|
371
|
+
|
|
372
|
+
suppression = Suppression(
|
|
373
|
+
id=supp_id,
|
|
374
|
+
pattern=pattern,
|
|
375
|
+
pattern_type=pattern_type,
|
|
376
|
+
reason=reason,
|
|
377
|
+
created_at=now.isoformat(),
|
|
378
|
+
created_by=os.environ.get("USER", "unknown"),
|
|
379
|
+
expires_at=expires_at,
|
|
380
|
+
scope=scope,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
memory.suppressions.append(suppression)
|
|
384
|
+
self.save(to_team=(scope == "team"))
|
|
385
|
+
|
|
386
|
+
return suppression
|
|
387
|
+
|
|
388
|
+
def remove_suppression(self, suppression_id: str) -> bool:
|
|
389
|
+
"""Remove a suppression by ID."""
|
|
390
|
+
memory = self.load()
|
|
391
|
+
original_count = len(memory.suppressions)
|
|
392
|
+
memory.suppressions = [s for s in memory.suppressions if s.id != suppression_id]
|
|
393
|
+
if len(memory.suppressions) < original_count:
|
|
394
|
+
self.save()
|
|
395
|
+
return True
|
|
396
|
+
return False
|
|
397
|
+
|
|
398
|
+
def record_finding(
|
|
399
|
+
self,
|
|
400
|
+
title: str,
|
|
401
|
+
category: str,
|
|
402
|
+
severity: str,
|
|
403
|
+
file_path: Optional[str],
|
|
404
|
+
confidence: float,
|
|
405
|
+
) -> None:
|
|
406
|
+
"""Record a finding to update patterns and risk scores."""
|
|
407
|
+
memory = self.load()
|
|
408
|
+
|
|
409
|
+
# Compute fingerprint
|
|
410
|
+
fingerprint = hashlib.sha256(f"{title}:{category}".encode()).hexdigest()[:16]
|
|
411
|
+
|
|
412
|
+
now = datetime.now().isoformat()
|
|
413
|
+
|
|
414
|
+
# Update or create pattern
|
|
415
|
+
existing = next(
|
|
416
|
+
(p for p in memory.issue_patterns if p.fingerprint == fingerprint),
|
|
417
|
+
None,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
if existing:
|
|
421
|
+
existing.occurrences += 1
|
|
422
|
+
existing.last_seen = now
|
|
423
|
+
existing.avg_confidence = (
|
|
424
|
+
existing.avg_confidence * (existing.occurrences - 1) + confidence
|
|
425
|
+
) / existing.occurrences
|
|
426
|
+
if file_path and file_path not in existing.files_affected:
|
|
427
|
+
existing.files_affected.append(file_path)
|
|
428
|
+
else:
|
|
429
|
+
pattern = IssuePattern(
|
|
430
|
+
fingerprint=fingerprint,
|
|
431
|
+
title=title,
|
|
432
|
+
category=category,
|
|
433
|
+
severity=severity,
|
|
434
|
+
occurrences=1,
|
|
435
|
+
first_seen=now,
|
|
436
|
+
last_seen=now,
|
|
437
|
+
files_affected=[file_path] if file_path else [],
|
|
438
|
+
avg_confidence=confidence,
|
|
439
|
+
)
|
|
440
|
+
memory.issue_patterns.append(pattern)
|
|
441
|
+
|
|
442
|
+
# Update file risk
|
|
443
|
+
if file_path:
|
|
444
|
+
memory.update_file_risk(file_path, severity)
|
|
445
|
+
|
|
446
|
+
memory.total_findings += 1
|
|
447
|
+
|
|
448
|
+
def record_session(
|
|
449
|
+
self,
|
|
450
|
+
roles_used: List[str],
|
|
451
|
+
findings_count: int,
|
|
452
|
+
duration_seconds: float,
|
|
453
|
+
) -> None:
|
|
454
|
+
"""Record a completed QE session."""
|
|
455
|
+
memory = self.load()
|
|
456
|
+
memory.total_sessions += 1
|
|
457
|
+
|
|
458
|
+
# Update role metrics
|
|
459
|
+
for role in roles_used:
|
|
460
|
+
if role not in memory.role_metrics:
|
|
461
|
+
memory.role_metrics[role] = RoleMetrics(role_name=role)
|
|
462
|
+
|
|
463
|
+
metrics = memory.role_metrics[role]
|
|
464
|
+
metrics.sessions_run += 1
|
|
465
|
+
# Rolling average of duration
|
|
466
|
+
metrics.avg_session_duration_seconds = (
|
|
467
|
+
metrics.avg_session_duration_seconds * (metrics.sessions_run - 1) + duration_seconds
|
|
468
|
+
) / metrics.sessions_run
|
|
469
|
+
|
|
470
|
+
def get_suppressions_for_finding(
|
|
471
|
+
self,
|
|
472
|
+
title: str,
|
|
473
|
+
rule_id: Optional[str],
|
|
474
|
+
fingerprint: Optional[str],
|
|
475
|
+
file_path: Optional[str],
|
|
476
|
+
) -> List[Suppression]:
|
|
477
|
+
"""Get suppressions that match a finding."""
|
|
478
|
+
memory = self.load()
|
|
479
|
+
matches = []
|
|
480
|
+
|
|
481
|
+
for supp in memory.get_active_suppressions():
|
|
482
|
+
if supp.pattern_type == "title" and supp.pattern.lower() in title.lower():
|
|
483
|
+
matches.append(supp)
|
|
484
|
+
elif supp.pattern_type == "rule_id" and rule_id == supp.pattern:
|
|
485
|
+
matches.append(supp)
|
|
486
|
+
elif supp.pattern_type == "fingerprint" and fingerprint == supp.pattern:
|
|
487
|
+
matches.append(supp)
|
|
488
|
+
elif supp.pattern_type == "file_pattern" and file_path:
|
|
489
|
+
import fnmatch
|
|
490
|
+
|
|
491
|
+
if fnmatch.fnmatch(file_path, supp.pattern):
|
|
492
|
+
matches.append(supp)
|
|
493
|
+
|
|
494
|
+
return matches
|
|
495
|
+
|
|
496
|
+
def should_suppress(
|
|
497
|
+
self,
|
|
498
|
+
title: str,
|
|
499
|
+
rule_id: Optional[str] = None,
|
|
500
|
+
fingerprint: Optional[str] = None,
|
|
501
|
+
file_path: Optional[str] = None,
|
|
502
|
+
) -> bool:
|
|
503
|
+
"""Check if a finding should be suppressed."""
|
|
504
|
+
matches = self.get_suppressions_for_finding(title, rule_id, fingerprint, file_path)
|
|
505
|
+
if matches:
|
|
506
|
+
memory = self.load()
|
|
507
|
+
memory.total_suppressions_applied += 1
|
|
508
|
+
return True
|
|
509
|
+
return False
|
|
510
|
+
|
|
511
|
+
def get_high_risk_files(self, threshold: float = 0.7) -> List[tuple]:
|
|
512
|
+
"""Get files with risk score above threshold."""
|
|
513
|
+
memory = self.load()
|
|
514
|
+
high_risk = [
|
|
515
|
+
(path, score) for path, score in memory.file_risk_map.items() if score >= threshold
|
|
516
|
+
]
|
|
517
|
+
return sorted(high_risk, key=lambda x: x[1], reverse=True)
|
|
518
|
+
|
|
519
|
+
def get_recurring_issues(self, min_occurrences: int = 2) -> List[IssuePattern]:
|
|
520
|
+
"""Get issues that have occurred multiple times."""
|
|
521
|
+
memory = self.load()
|
|
522
|
+
return [p for p in memory.issue_patterns if p.occurrences >= min_occurrences]
|