alma-memory 0.5.0__py3-none-any.whl → 0.7.0__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.
- alma/__init__.py +296 -194
- alma/compression/__init__.py +33 -0
- alma/compression/pipeline.py +980 -0
- alma/confidence/__init__.py +47 -47
- alma/confidence/engine.py +540 -540
- alma/confidence/types.py +351 -351
- alma/config/loader.py +157 -157
- alma/consolidation/__init__.py +23 -23
- alma/consolidation/engine.py +678 -678
- alma/consolidation/prompts.py +84 -84
- alma/core.py +1189 -322
- alma/domains/__init__.py +30 -30
- alma/domains/factory.py +359 -359
- alma/domains/schemas.py +448 -448
- alma/domains/types.py +272 -272
- alma/events/__init__.py +75 -75
- alma/events/emitter.py +285 -284
- alma/events/storage_mixin.py +246 -246
- alma/events/types.py +126 -126
- alma/events/webhook.py +425 -425
- alma/exceptions.py +49 -49
- alma/extraction/__init__.py +31 -31
- alma/extraction/auto_learner.py +265 -264
- alma/extraction/extractor.py +420 -420
- alma/graph/__init__.py +106 -81
- alma/graph/backends/__init__.py +32 -18
- alma/graph/backends/kuzu.py +624 -0
- alma/graph/backends/memgraph.py +432 -0
- alma/graph/backends/memory.py +236 -236
- alma/graph/backends/neo4j.py +417 -417
- alma/graph/base.py +159 -159
- alma/graph/extraction.py +198 -198
- alma/graph/store.py +860 -860
- alma/harness/__init__.py +35 -35
- alma/harness/base.py +386 -386
- alma/harness/domains.py +705 -705
- alma/initializer/__init__.py +37 -37
- alma/initializer/initializer.py +418 -418
- alma/initializer/types.py +250 -250
- alma/integration/__init__.py +62 -62
- alma/integration/claude_agents.py +444 -432
- alma/integration/helena.py +423 -423
- alma/integration/victor.py +471 -471
- alma/learning/__init__.py +101 -86
- alma/learning/decay.py +878 -0
- alma/learning/forgetting.py +1446 -1446
- alma/learning/heuristic_extractor.py +390 -390
- alma/learning/protocols.py +374 -374
- alma/learning/validation.py +346 -346
- alma/mcp/__init__.py +123 -45
- alma/mcp/__main__.py +156 -156
- alma/mcp/resources.py +122 -122
- alma/mcp/server.py +955 -591
- alma/mcp/tools.py +3254 -511
- alma/observability/__init__.py +91 -0
- alma/observability/config.py +302 -0
- alma/observability/guidelines.py +170 -0
- alma/observability/logging.py +424 -0
- alma/observability/metrics.py +583 -0
- alma/observability/tracing.py +440 -0
- alma/progress/__init__.py +21 -21
- alma/progress/tracker.py +607 -607
- alma/progress/types.py +250 -250
- alma/retrieval/__init__.py +134 -53
- alma/retrieval/budget.py +525 -0
- alma/retrieval/cache.py +1304 -1061
- alma/retrieval/embeddings.py +202 -202
- alma/retrieval/engine.py +850 -366
- alma/retrieval/modes.py +365 -0
- alma/retrieval/progressive.py +560 -0
- alma/retrieval/scoring.py +344 -344
- alma/retrieval/trust_scoring.py +637 -0
- alma/retrieval/verification.py +797 -0
- alma/session/__init__.py +19 -19
- alma/session/manager.py +442 -399
- alma/session/types.py +288 -288
- alma/storage/__init__.py +101 -61
- alma/storage/archive.py +233 -0
- alma/storage/azure_cosmos.py +1259 -1048
- alma/storage/base.py +1083 -525
- alma/storage/chroma.py +1443 -1443
- alma/storage/constants.py +103 -0
- alma/storage/file_based.py +614 -619
- alma/storage/migrations/__init__.py +21 -0
- alma/storage/migrations/base.py +321 -0
- alma/storage/migrations/runner.py +323 -0
- alma/storage/migrations/version_stores.py +337 -0
- alma/storage/migrations/versions/__init__.py +11 -0
- alma/storage/migrations/versions/v1_0_0.py +373 -0
- alma/storage/migrations/versions/v1_1_0_workflow_context.py +551 -0
- alma/storage/pinecone.py +1080 -1080
- alma/storage/postgresql.py +1948 -1452
- alma/storage/qdrant.py +1306 -1306
- alma/storage/sqlite_local.py +3041 -1358
- alma/testing/__init__.py +46 -0
- alma/testing/factories.py +301 -0
- alma/testing/mocks.py +389 -0
- alma/types.py +292 -264
- alma/utils/__init__.py +19 -0
- alma/utils/tokenizer.py +521 -0
- alma/workflow/__init__.py +83 -0
- alma/workflow/artifacts.py +170 -0
- alma/workflow/checkpoint.py +311 -0
- alma/workflow/context.py +228 -0
- alma/workflow/outcomes.py +189 -0
- alma/workflow/reducers.py +393 -0
- {alma_memory-0.5.0.dist-info → alma_memory-0.7.0.dist-info}/METADATA +244 -72
- alma_memory-0.7.0.dist-info/RECORD +112 -0
- alma_memory-0.5.0.dist-info/RECORD +0 -76
- {alma_memory-0.5.0.dist-info → alma_memory-0.7.0.dist-info}/WHEEL +0 -0
- {alma_memory-0.5.0.dist-info → alma_memory-0.7.0.dist-info}/top_level.txt +0 -0
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)
|