alma-memory 0.5.0__py3-none-any.whl → 0.5.1__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 (36) hide show
  1. alma/__init__.py +33 -1
  2. alma/core.py +124 -16
  3. alma/extraction/auto_learner.py +4 -3
  4. alma/graph/__init__.py +26 -1
  5. alma/graph/backends/__init__.py +14 -0
  6. alma/graph/backends/kuzu.py +624 -0
  7. alma/graph/backends/memgraph.py +432 -0
  8. alma/integration/claude_agents.py +22 -10
  9. alma/learning/protocols.py +3 -3
  10. alma/mcp/tools.py +9 -11
  11. alma/observability/__init__.py +84 -0
  12. alma/observability/config.py +302 -0
  13. alma/observability/logging.py +424 -0
  14. alma/observability/metrics.py +583 -0
  15. alma/observability/tracing.py +440 -0
  16. alma/retrieval/engine.py +65 -4
  17. alma/storage/__init__.py +29 -0
  18. alma/storage/azure_cosmos.py +343 -132
  19. alma/storage/base.py +58 -0
  20. alma/storage/constants.py +103 -0
  21. alma/storage/file_based.py +3 -8
  22. alma/storage/migrations/__init__.py +21 -0
  23. alma/storage/migrations/base.py +321 -0
  24. alma/storage/migrations/runner.py +323 -0
  25. alma/storage/migrations/version_stores.py +337 -0
  26. alma/storage/migrations/versions/__init__.py +11 -0
  27. alma/storage/migrations/versions/v1_0_0.py +373 -0
  28. alma/storage/postgresql.py +185 -78
  29. alma/storage/sqlite_local.py +149 -50
  30. alma/testing/__init__.py +46 -0
  31. alma/testing/factories.py +301 -0
  32. alma/testing/mocks.py +389 -0
  33. {alma_memory-0.5.0.dist-info → alma_memory-0.5.1.dist-info}/METADATA +42 -8
  34. {alma_memory-0.5.0.dist-info → alma_memory-0.5.1.dist-info}/RECORD +36 -19
  35. {alma_memory-0.5.0.dist-info → alma_memory-0.5.1.dist-info}/WHEEL +0 -0
  36. {alma_memory-0.5.0.dist-info → alma_memory-0.5.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,301 @@
1
+ """
2
+ ALMA Test Factories.
3
+
4
+ Provides factory functions for creating test data with sensible defaults.
5
+ All factory functions accept keyword arguments to override any field.
6
+ """
7
+
8
+ import uuid
9
+ from datetime import datetime, timedelta, timezone
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ from alma.types import (
13
+ AntiPattern,
14
+ DomainKnowledge,
15
+ Heuristic,
16
+ Outcome,
17
+ UserPreference,
18
+ )
19
+
20
+ __all__ = [
21
+ "create_test_heuristic",
22
+ "create_test_outcome",
23
+ "create_test_preference",
24
+ "create_test_knowledge",
25
+ "create_test_anti_pattern",
26
+ ]
27
+
28
+
29
+ def create_test_heuristic(
30
+ id: Optional[str] = None,
31
+ agent: str = "test-agent",
32
+ project_id: str = "test-project",
33
+ condition: str = "test condition",
34
+ strategy: str = "test strategy",
35
+ confidence: float = 0.85,
36
+ occurrence_count: int = 10,
37
+ success_count: int = 8,
38
+ last_validated: Optional[datetime] = None,
39
+ created_at: Optional[datetime] = None,
40
+ embedding: Optional[List[float]] = None,
41
+ metadata: Optional[Dict[str, Any]] = None,
42
+ ) -> Heuristic:
43
+ """
44
+ Create a test Heuristic with sensible defaults.
45
+
46
+ All parameters can be overridden to customize the test data.
47
+
48
+ Example:
49
+ >>> heuristic = create_test_heuristic(agent="helena", confidence=0.95)
50
+ >>> assert heuristic.agent == "helena"
51
+ >>> assert heuristic.confidence == 0.95
52
+
53
+ Args:
54
+ id: Unique identifier (auto-generated if not provided)
55
+ agent: Agent name that owns this heuristic
56
+ project_id: Project identifier
57
+ condition: When this heuristic applies
58
+ strategy: What strategy to use
59
+ confidence: Confidence score (0.0-1.0)
60
+ occurrence_count: Number of times this heuristic has been observed
61
+ success_count: Number of successful applications
62
+ last_validated: Last validation timestamp
63
+ created_at: Creation timestamp
64
+ embedding: Optional embedding vector
65
+ metadata: Additional metadata
66
+
67
+ Returns:
68
+ A fully populated Heuristic instance
69
+ """
70
+ now = datetime.now(timezone.utc)
71
+ return Heuristic(
72
+ id=id or str(uuid.uuid4()),
73
+ agent=agent,
74
+ project_id=project_id,
75
+ condition=condition,
76
+ strategy=strategy,
77
+ confidence=confidence,
78
+ occurrence_count=occurrence_count,
79
+ success_count=success_count,
80
+ last_validated=last_validated or now,
81
+ created_at=created_at or now - timedelta(days=7),
82
+ embedding=embedding,
83
+ metadata=metadata or {},
84
+ )
85
+
86
+
87
+ def create_test_outcome(
88
+ id: Optional[str] = None,
89
+ agent: str = "test-agent",
90
+ project_id: str = "test-project",
91
+ task_type: str = "test_task",
92
+ task_description: str = "Test task description",
93
+ success: bool = True,
94
+ strategy_used: str = "test strategy",
95
+ duration_ms: Optional[int] = 500,
96
+ error_message: Optional[str] = None,
97
+ user_feedback: Optional[str] = None,
98
+ timestamp: Optional[datetime] = None,
99
+ embedding: Optional[List[float]] = None,
100
+ metadata: Optional[Dict[str, Any]] = None,
101
+ ) -> Outcome:
102
+ """
103
+ Create a test Outcome with sensible defaults.
104
+
105
+ All parameters can be overridden to customize the test data.
106
+
107
+ Example:
108
+ >>> outcome = create_test_outcome(success=False, error_message="Failed")
109
+ >>> assert outcome.success is False
110
+ >>> assert outcome.error_message == "Failed"
111
+
112
+ Args:
113
+ id: Unique identifier (auto-generated if not provided)
114
+ agent: Agent name that produced this outcome
115
+ project_id: Project identifier
116
+ task_type: Type of task (e.g., "api_validation")
117
+ task_description: Description of the task
118
+ success: Whether the task succeeded
119
+ strategy_used: Strategy that was applied
120
+ duration_ms: Task duration in milliseconds
121
+ error_message: Error message if failed
122
+ user_feedback: Optional user feedback
123
+ timestamp: When the outcome occurred
124
+ embedding: Optional embedding vector
125
+ metadata: Additional metadata
126
+
127
+ Returns:
128
+ A fully populated Outcome instance
129
+ """
130
+ return Outcome(
131
+ id=id or str(uuid.uuid4()),
132
+ agent=agent,
133
+ project_id=project_id,
134
+ task_type=task_type,
135
+ task_description=task_description,
136
+ success=success,
137
+ strategy_used=strategy_used,
138
+ duration_ms=duration_ms,
139
+ error_message=error_message,
140
+ user_feedback=user_feedback,
141
+ timestamp=timestamp or datetime.now(timezone.utc),
142
+ embedding=embedding,
143
+ metadata=metadata or {},
144
+ )
145
+
146
+
147
+ def create_test_preference(
148
+ id: Optional[str] = None,
149
+ user_id: str = "test-user",
150
+ category: str = "code_style",
151
+ preference: str = "Test preference value",
152
+ source: str = "explicit_instruction",
153
+ confidence: float = 1.0,
154
+ timestamp: Optional[datetime] = None,
155
+ metadata: Optional[Dict[str, Any]] = None,
156
+ ) -> UserPreference:
157
+ """
158
+ Create a test UserPreference with sensible defaults.
159
+
160
+ All parameters can be overridden to customize the test data.
161
+
162
+ Example:
163
+ >>> pref = create_test_preference(
164
+ ... category="communication",
165
+ ... preference="No emojis"
166
+ ... )
167
+ >>> assert pref.category == "communication"
168
+
169
+ Args:
170
+ id: Unique identifier (auto-generated if not provided)
171
+ user_id: User identifier
172
+ category: Preference category (e.g., "code_style", "communication")
173
+ preference: The actual preference text
174
+ source: How this preference was learned
175
+ confidence: Confidence in this preference (0.0-1.0)
176
+ timestamp: When the preference was recorded
177
+ metadata: Additional metadata
178
+
179
+ Returns:
180
+ A fully populated UserPreference instance
181
+ """
182
+ return UserPreference(
183
+ id=id or str(uuid.uuid4()),
184
+ user_id=user_id,
185
+ category=category,
186
+ preference=preference,
187
+ source=source,
188
+ confidence=confidence,
189
+ timestamp=timestamp or datetime.now(timezone.utc),
190
+ metadata=metadata or {},
191
+ )
192
+
193
+
194
+ def create_test_knowledge(
195
+ id: Optional[str] = None,
196
+ agent: str = "test-agent",
197
+ project_id: str = "test-project",
198
+ domain: str = "test_domain",
199
+ fact: str = "Test domain fact",
200
+ source: str = "test_source",
201
+ confidence: float = 1.0,
202
+ last_verified: Optional[datetime] = None,
203
+ embedding: Optional[List[float]] = None,
204
+ metadata: Optional[Dict[str, Any]] = None,
205
+ ) -> DomainKnowledge:
206
+ """
207
+ Create a test DomainKnowledge with sensible defaults.
208
+
209
+ All parameters can be overridden to customize the test data.
210
+
211
+ Example:
212
+ >>> knowledge = create_test_knowledge(
213
+ ... domain="authentication",
214
+ ... fact="JWT tokens expire in 24h"
215
+ ... )
216
+ >>> assert knowledge.domain == "authentication"
217
+
218
+ Args:
219
+ id: Unique identifier (auto-generated if not provided)
220
+ agent: Agent name that owns this knowledge
221
+ project_id: Project identifier
222
+ domain: Knowledge domain (e.g., "authentication", "database")
223
+ fact: The factual information
224
+ source: How this knowledge was acquired
225
+ confidence: Confidence in this knowledge (0.0-1.0)
226
+ last_verified: Last verification timestamp
227
+ embedding: Optional embedding vector
228
+ metadata: Additional metadata
229
+
230
+ Returns:
231
+ A fully populated DomainKnowledge instance
232
+ """
233
+ return DomainKnowledge(
234
+ id=id or str(uuid.uuid4()),
235
+ agent=agent,
236
+ project_id=project_id,
237
+ domain=domain,
238
+ fact=fact,
239
+ source=source,
240
+ confidence=confidence,
241
+ last_verified=last_verified or datetime.now(timezone.utc),
242
+ embedding=embedding,
243
+ metadata=metadata or {},
244
+ )
245
+
246
+
247
+ def create_test_anti_pattern(
248
+ id: Optional[str] = None,
249
+ agent: str = "test-agent",
250
+ project_id: str = "test-project",
251
+ pattern: str = "Test anti-pattern",
252
+ why_bad: str = "This is why it's bad",
253
+ better_alternative: str = "Do this instead",
254
+ occurrence_count: int = 3,
255
+ last_seen: Optional[datetime] = None,
256
+ created_at: Optional[datetime] = None,
257
+ embedding: Optional[List[float]] = None,
258
+ metadata: Optional[Dict[str, Any]] = None,
259
+ ) -> AntiPattern:
260
+ """
261
+ Create a test AntiPattern with sensible defaults.
262
+
263
+ All parameters can be overridden to customize the test data.
264
+
265
+ Example:
266
+ >>> anti_pattern = create_test_anti_pattern(
267
+ ... pattern="Using sleep() for waits",
268
+ ... better_alternative="Use explicit waits"
269
+ ... )
270
+ >>> assert "sleep" in anti_pattern.pattern
271
+
272
+ Args:
273
+ id: Unique identifier (auto-generated if not provided)
274
+ agent: Agent name that identified this anti-pattern
275
+ project_id: Project identifier
276
+ pattern: Description of the anti-pattern
277
+ why_bad: Explanation of why this pattern is problematic
278
+ better_alternative: Recommended alternative approach
279
+ occurrence_count: How often this pattern has been observed
280
+ last_seen: Last time this pattern was observed
281
+ created_at: When this anti-pattern was first identified
282
+ embedding: Optional embedding vector
283
+ metadata: Additional metadata
284
+
285
+ Returns:
286
+ A fully populated AntiPattern instance
287
+ """
288
+ now = datetime.now(timezone.utc)
289
+ return AntiPattern(
290
+ id=id or str(uuid.uuid4()),
291
+ agent=agent,
292
+ project_id=project_id,
293
+ pattern=pattern,
294
+ why_bad=why_bad,
295
+ better_alternative=better_alternative,
296
+ occurrence_count=occurrence_count,
297
+ last_seen=last_seen or now,
298
+ created_at=created_at or now - timedelta(days=3),
299
+ embedding=embedding,
300
+ metadata=metadata or {},
301
+ )
alma/testing/mocks.py ADDED
@@ -0,0 +1,389 @@
1
+ """
2
+ ALMA Testing Mocks.
3
+
4
+ Provides mock implementations of ALMA interfaces for testing.
5
+ """
6
+
7
+ from datetime import datetime, timezone
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ from alma.retrieval.embeddings import MockEmbedder
11
+ from alma.storage.base import StorageBackend
12
+ from alma.types import (
13
+ AntiPattern,
14
+ DomainKnowledge,
15
+ Heuristic,
16
+ Outcome,
17
+ UserPreference,
18
+ )
19
+
20
+ # Re-export MockEmbedder for convenience
21
+ __all__ = ["MockStorage", "MockEmbedder"]
22
+
23
+
24
+ class MockStorage(StorageBackend):
25
+ """
26
+ In-memory mock storage backend for testing.
27
+
28
+ Stores all memory types in dictionaries for fast, isolated testing.
29
+ Does not persist data between test runs.
30
+
31
+ Example:
32
+ >>> from alma.testing import MockStorage, create_test_heuristic
33
+ >>> storage = MockStorage()
34
+ >>> heuristic = create_test_heuristic(agent="test-agent")
35
+ >>> storage.save_heuristic(heuristic)
36
+ >>> found = storage.get_heuristics("test-project", agent="test-agent")
37
+ >>> assert len(found) == 1
38
+ """
39
+
40
+ def __init__(self):
41
+ """Initialize empty in-memory storage."""
42
+ self._heuristics: Dict[str, Heuristic] = {}
43
+ self._outcomes: Dict[str, Outcome] = {}
44
+ self._preferences: Dict[str, UserPreference] = {}
45
+ self._domain_knowledge: Dict[str, DomainKnowledge] = {}
46
+ self._anti_patterns: Dict[str, AntiPattern] = {}
47
+
48
+ # ==================== WRITE OPERATIONS ====================
49
+
50
+ def save_heuristic(self, heuristic: Heuristic) -> str:
51
+ """Save a heuristic, return its ID."""
52
+ self._heuristics[heuristic.id] = heuristic
53
+ return heuristic.id
54
+
55
+ def save_outcome(self, outcome: Outcome) -> str:
56
+ """Save an outcome, return its ID."""
57
+ self._outcomes[outcome.id] = outcome
58
+ return outcome.id
59
+
60
+ def save_user_preference(self, preference: UserPreference) -> str:
61
+ """Save a user preference, return its ID."""
62
+ self._preferences[preference.id] = preference
63
+ return preference.id
64
+
65
+ def save_domain_knowledge(self, knowledge: DomainKnowledge) -> str:
66
+ """Save domain knowledge, return its ID."""
67
+ self._domain_knowledge[knowledge.id] = knowledge
68
+ return knowledge.id
69
+
70
+ def save_anti_pattern(self, anti_pattern: AntiPattern) -> str:
71
+ """Save an anti-pattern, return its ID."""
72
+ self._anti_patterns[anti_pattern.id] = anti_pattern
73
+ return anti_pattern.id
74
+
75
+ # ==================== READ OPERATIONS ====================
76
+
77
+ def get_heuristics(
78
+ self,
79
+ project_id: str,
80
+ agent: Optional[str] = None,
81
+ embedding: Optional[List[float]] = None,
82
+ top_k: int = 5,
83
+ min_confidence: float = 0.0,
84
+ ) -> List[Heuristic]:
85
+ """Get heuristics, optionally filtered by agent."""
86
+ results = []
87
+ for h in self._heuristics.values():
88
+ if h.project_id != project_id:
89
+ continue
90
+ if agent and h.agent != agent:
91
+ continue
92
+ if h.confidence < min_confidence:
93
+ continue
94
+ results.append(h)
95
+
96
+ # Sort by confidence descending
97
+ results.sort(key=lambda x: x.confidence, reverse=True)
98
+ return results[:top_k]
99
+
100
+ def get_outcomes(
101
+ self,
102
+ project_id: str,
103
+ agent: Optional[str] = None,
104
+ task_type: Optional[str] = None,
105
+ embedding: Optional[List[float]] = None,
106
+ top_k: int = 5,
107
+ success_only: bool = False,
108
+ ) -> List[Outcome]:
109
+ """Get outcomes, optionally filtered."""
110
+ results = []
111
+ for o in self._outcomes.values():
112
+ if o.project_id != project_id:
113
+ continue
114
+ if agent and o.agent != agent:
115
+ continue
116
+ if task_type and o.task_type != task_type:
117
+ continue
118
+ if success_only and not o.success:
119
+ continue
120
+ results.append(o)
121
+
122
+ # Sort by timestamp descending (most recent first)
123
+ results.sort(key=lambda x: x.timestamp, reverse=True)
124
+ return results[:top_k]
125
+
126
+ def get_user_preferences(
127
+ self,
128
+ user_id: str,
129
+ category: Optional[str] = None,
130
+ ) -> List[UserPreference]:
131
+ """Get user preferences."""
132
+ results = []
133
+ for p in self._preferences.values():
134
+ if p.user_id != user_id:
135
+ continue
136
+ if category and p.category != category:
137
+ continue
138
+ results.append(p)
139
+ return results
140
+
141
+ def get_domain_knowledge(
142
+ self,
143
+ project_id: str,
144
+ agent: Optional[str] = None,
145
+ domain: Optional[str] = None,
146
+ embedding: Optional[List[float]] = None,
147
+ top_k: int = 5,
148
+ ) -> List[DomainKnowledge]:
149
+ """Get domain knowledge."""
150
+ results = []
151
+ for dk in self._domain_knowledge.values():
152
+ if dk.project_id != project_id:
153
+ continue
154
+ if agent and dk.agent != agent:
155
+ continue
156
+ if domain and dk.domain != domain:
157
+ continue
158
+ results.append(dk)
159
+
160
+ # Sort by confidence descending
161
+ results.sort(key=lambda x: x.confidence, reverse=True)
162
+ return results[:top_k]
163
+
164
+ def get_anti_patterns(
165
+ self,
166
+ project_id: str,
167
+ agent: Optional[str] = None,
168
+ embedding: Optional[List[float]] = None,
169
+ top_k: int = 5,
170
+ ) -> List[AntiPattern]:
171
+ """Get anti-patterns."""
172
+ results = []
173
+ for ap in self._anti_patterns.values():
174
+ if ap.project_id != project_id:
175
+ continue
176
+ if agent and ap.agent != agent:
177
+ continue
178
+ results.append(ap)
179
+
180
+ # Sort by occurrence_count descending
181
+ results.sort(key=lambda x: x.occurrence_count, reverse=True)
182
+ return results[:top_k]
183
+
184
+ # ==================== UPDATE OPERATIONS ====================
185
+
186
+ def update_heuristic(
187
+ self,
188
+ heuristic_id: str,
189
+ updates: Dict[str, Any],
190
+ ) -> bool:
191
+ """Update a heuristic's fields."""
192
+ if heuristic_id not in self._heuristics:
193
+ return False
194
+
195
+ h = self._heuristics[heuristic_id]
196
+ for key, value in updates.items():
197
+ if hasattr(h, key):
198
+ setattr(h, key, value)
199
+ return True
200
+
201
+ def increment_heuristic_occurrence(
202
+ self,
203
+ heuristic_id: str,
204
+ success: bool,
205
+ ) -> bool:
206
+ """Increment heuristic occurrence count."""
207
+ if heuristic_id not in self._heuristics:
208
+ return False
209
+
210
+ h = self._heuristics[heuristic_id]
211
+ h.occurrence_count += 1
212
+ if success:
213
+ h.success_count += 1
214
+ h.last_validated = datetime.now(timezone.utc)
215
+ return True
216
+
217
+ def update_heuristic_confidence(
218
+ self,
219
+ heuristic_id: str,
220
+ new_confidence: float,
221
+ ) -> bool:
222
+ """Update a heuristic's confidence value."""
223
+ if heuristic_id not in self._heuristics:
224
+ return False
225
+
226
+ self._heuristics[heuristic_id].confidence = new_confidence
227
+ return True
228
+
229
+ def update_knowledge_confidence(
230
+ self,
231
+ knowledge_id: str,
232
+ new_confidence: float,
233
+ ) -> bool:
234
+ """Update domain knowledge confidence value."""
235
+ if knowledge_id not in self._domain_knowledge:
236
+ return False
237
+
238
+ self._domain_knowledge[knowledge_id].confidence = new_confidence
239
+ return True
240
+
241
+ # ==================== DELETE OPERATIONS ====================
242
+
243
+ def delete_heuristic(self, heuristic_id: str) -> bool:
244
+ """Delete a heuristic by ID."""
245
+ if heuristic_id not in self._heuristics:
246
+ return False
247
+ del self._heuristics[heuristic_id]
248
+ return True
249
+
250
+ def delete_outcome(self, outcome_id: str) -> bool:
251
+ """Delete an outcome by ID."""
252
+ if outcome_id not in self._outcomes:
253
+ return False
254
+ del self._outcomes[outcome_id]
255
+ return True
256
+
257
+ def delete_domain_knowledge(self, knowledge_id: str) -> bool:
258
+ """Delete domain knowledge by ID."""
259
+ if knowledge_id not in self._domain_knowledge:
260
+ return False
261
+ del self._domain_knowledge[knowledge_id]
262
+ return True
263
+
264
+ def delete_anti_pattern(self, anti_pattern_id: str) -> bool:
265
+ """Delete an anti-pattern by ID."""
266
+ if anti_pattern_id not in self._anti_patterns:
267
+ return False
268
+ del self._anti_patterns[anti_pattern_id]
269
+ return True
270
+
271
+ def delete_outcomes_older_than(
272
+ self,
273
+ project_id: str,
274
+ older_than: datetime,
275
+ agent: Optional[str] = None,
276
+ ) -> int:
277
+ """Delete old outcomes."""
278
+ to_delete = []
279
+ for oid, o in self._outcomes.items():
280
+ if o.project_id != project_id:
281
+ continue
282
+ if agent and o.agent != agent:
283
+ continue
284
+ if o.timestamp < older_than:
285
+ to_delete.append(oid)
286
+
287
+ for oid in to_delete:
288
+ del self._outcomes[oid]
289
+ return len(to_delete)
290
+
291
+ def delete_low_confidence_heuristics(
292
+ self,
293
+ project_id: str,
294
+ below_confidence: float,
295
+ agent: Optional[str] = None,
296
+ ) -> int:
297
+ """Delete low-confidence heuristics."""
298
+ to_delete = []
299
+ for hid, h in self._heuristics.items():
300
+ if h.project_id != project_id:
301
+ continue
302
+ if agent and h.agent != agent:
303
+ continue
304
+ if h.confidence < below_confidence:
305
+ to_delete.append(hid)
306
+
307
+ for hid in to_delete:
308
+ del self._heuristics[hid]
309
+ return len(to_delete)
310
+
311
+ # ==================== STATS ====================
312
+
313
+ def get_stats(
314
+ self,
315
+ project_id: str,
316
+ agent: Optional[str] = None,
317
+ ) -> Dict[str, Any]:
318
+ """Get memory statistics."""
319
+ stats = {
320
+ "heuristics": 0,
321
+ "outcomes": 0,
322
+ "preferences": 0,
323
+ "domain_knowledge": 0,
324
+ "anti_patterns": 0,
325
+ }
326
+
327
+ for h in self._heuristics.values():
328
+ if h.project_id == project_id and (not agent or h.agent == agent):
329
+ stats["heuristics"] += 1
330
+
331
+ for o in self._outcomes.values():
332
+ if o.project_id == project_id and (not agent or o.agent == agent):
333
+ stats["outcomes"] += 1
334
+
335
+ for dk in self._domain_knowledge.values():
336
+ if dk.project_id == project_id and (not agent or dk.agent == agent):
337
+ stats["domain_knowledge"] += 1
338
+
339
+ for ap in self._anti_patterns.values():
340
+ if ap.project_id == project_id and (not agent or ap.agent == agent):
341
+ stats["anti_patterns"] += 1
342
+
343
+ # Note: preferences are user-scoped, not project-scoped
344
+ stats["preferences"] = len(self._preferences)
345
+
346
+ stats["total_count"] = sum(stats.values())
347
+ return stats
348
+
349
+ # ==================== UTILITY ====================
350
+
351
+ @classmethod
352
+ def from_config(cls, config: Dict[str, Any]) -> "MockStorage":
353
+ """Create instance from configuration dict (ignores config for mock)."""
354
+ return cls()
355
+
356
+ # ==================== MOCK-SPECIFIC METHODS ====================
357
+
358
+ def clear(self) -> None:
359
+ """Clear all stored data. Useful for test cleanup."""
360
+ self._heuristics.clear()
361
+ self._outcomes.clear()
362
+ self._preferences.clear()
363
+ self._domain_knowledge.clear()
364
+ self._anti_patterns.clear()
365
+
366
+ @property
367
+ def heuristic_count(self) -> int:
368
+ """Get total number of stored heuristics."""
369
+ return len(self._heuristics)
370
+
371
+ @property
372
+ def outcome_count(self) -> int:
373
+ """Get total number of stored outcomes."""
374
+ return len(self._outcomes)
375
+
376
+ @property
377
+ def preference_count(self) -> int:
378
+ """Get total number of stored preferences."""
379
+ return len(self._preferences)
380
+
381
+ @property
382
+ def knowledge_count(self) -> int:
383
+ """Get total number of stored domain knowledge items."""
384
+ return len(self._domain_knowledge)
385
+
386
+ @property
387
+ def anti_pattern_count(self) -> int:
388
+ """Get total number of stored anti-patterns."""
389
+ return len(self._anti_patterns)