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,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]