superlocalmemory 2.7.6 → 2.8.0

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 (170) hide show
  1. package/CHANGELOG.md +120 -155
  2. package/README.md +115 -89
  3. package/api_server.py +2 -12
  4. package/docs/PATTERN-LEARNING.md +64 -199
  5. package/docs/example_graph_usage.py +4 -6
  6. package/install.sh +59 -0
  7. package/mcp_server.py +83 -7
  8. package/package.json +1 -8
  9. package/scripts/generate-thumbnails.py +3 -5
  10. package/skills/slm-build-graph/SKILL.md +1 -1
  11. package/skills/slm-list-recent/SKILL.md +1 -1
  12. package/skills/slm-recall/SKILL.md +1 -1
  13. package/skills/slm-remember/SKILL.md +1 -1
  14. package/skills/slm-show-patterns/SKILL.md +1 -1
  15. package/skills/slm-status/SKILL.md +1 -1
  16. package/skills/slm-switch-profile/SKILL.md +1 -1
  17. package/src/agent_registry.py +7 -18
  18. package/src/auth_middleware.py +3 -5
  19. package/src/auto_backup.py +3 -7
  20. package/src/behavioral/__init__.py +49 -0
  21. package/src/behavioral/behavioral_listener.py +203 -0
  22. package/src/behavioral/behavioral_patterns.py +275 -0
  23. package/src/behavioral/cross_project_transfer.py +206 -0
  24. package/src/behavioral/outcome_inference.py +194 -0
  25. package/src/behavioral/outcome_tracker.py +193 -0
  26. package/src/behavioral/tests/__init__.py +4 -0
  27. package/src/behavioral/tests/test_behavioral_integration.py +108 -0
  28. package/src/behavioral/tests/test_behavioral_patterns.py +150 -0
  29. package/src/behavioral/tests/test_cross_project_transfer.py +142 -0
  30. package/src/behavioral/tests/test_mcp_behavioral.py +139 -0
  31. package/src/behavioral/tests/test_mcp_report_outcome.py +117 -0
  32. package/src/behavioral/tests/test_outcome_inference.py +107 -0
  33. package/src/behavioral/tests/test_outcome_tracker.py +96 -0
  34. package/src/cache_manager.py +4 -6
  35. package/src/compliance/__init__.py +48 -0
  36. package/src/compliance/abac_engine.py +149 -0
  37. package/src/compliance/abac_middleware.py +116 -0
  38. package/src/compliance/audit_db.py +215 -0
  39. package/src/compliance/audit_logger.py +148 -0
  40. package/src/compliance/retention_manager.py +289 -0
  41. package/src/compliance/retention_scheduler.py +186 -0
  42. package/src/compliance/tests/__init__.py +4 -0
  43. package/src/compliance/tests/test_abac_enforcement.py +95 -0
  44. package/src/compliance/tests/test_abac_engine.py +124 -0
  45. package/src/compliance/tests/test_abac_mcp_integration.py +118 -0
  46. package/src/compliance/tests/test_audit_db.py +123 -0
  47. package/src/compliance/tests/test_audit_logger.py +98 -0
  48. package/src/compliance/tests/test_mcp_audit.py +128 -0
  49. package/src/compliance/tests/test_mcp_retention_policy.py +125 -0
  50. package/src/compliance/tests/test_retention_manager.py +131 -0
  51. package/src/compliance/tests/test_retention_scheduler.py +99 -0
  52. package/src/db_connection_manager.py +2 -12
  53. package/src/embedding_engine.py +61 -669
  54. package/src/embeddings/__init__.py +47 -0
  55. package/src/embeddings/cache.py +70 -0
  56. package/src/embeddings/cli.py +113 -0
  57. package/src/embeddings/constants.py +47 -0
  58. package/src/embeddings/database.py +91 -0
  59. package/src/embeddings/engine.py +247 -0
  60. package/src/embeddings/model_loader.py +145 -0
  61. package/src/event_bus.py +3 -13
  62. package/src/graph/__init__.py +36 -0
  63. package/src/graph/build_helpers.py +74 -0
  64. package/src/graph/cli.py +87 -0
  65. package/src/graph/cluster_builder.py +188 -0
  66. package/src/graph/cluster_summary.py +148 -0
  67. package/src/graph/constants.py +47 -0
  68. package/src/graph/edge_builder.py +162 -0
  69. package/src/graph/entity_extractor.py +95 -0
  70. package/src/graph/graph_core.py +226 -0
  71. package/src/graph/graph_search.py +231 -0
  72. package/src/graph/hierarchical.py +207 -0
  73. package/src/graph/schema.py +99 -0
  74. package/src/graph_engine.py +45 -1451
  75. package/src/hnsw_index.py +3 -7
  76. package/src/hybrid_search.py +36 -683
  77. package/src/learning/__init__.py +27 -12
  78. package/src/learning/adaptive_ranker.py +50 -12
  79. package/src/learning/cross_project_aggregator.py +2 -12
  80. package/src/learning/engagement_tracker.py +2 -12
  81. package/src/learning/feature_extractor.py +175 -43
  82. package/src/learning/feedback_collector.py +7 -12
  83. package/src/learning/learning_db.py +180 -12
  84. package/src/learning/project_context_manager.py +2 -12
  85. package/src/learning/source_quality_scorer.py +2 -12
  86. package/src/learning/synthetic_bootstrap.py +2 -12
  87. package/src/learning/tests/__init__.py +2 -0
  88. package/src/learning/tests/test_adaptive_ranker.py +2 -6
  89. package/src/learning/tests/test_adaptive_ranker_v28.py +60 -0
  90. package/src/learning/tests/test_aggregator.py +2 -6
  91. package/src/learning/tests/test_auto_retrain_v28.py +35 -0
  92. package/src/learning/tests/test_e2e_ranking_v28.py +82 -0
  93. package/src/learning/tests/test_feature_extractor_v28.py +93 -0
  94. package/src/learning/tests/test_feedback_collector.py +2 -6
  95. package/src/learning/tests/test_learning_db.py +2 -6
  96. package/src/learning/tests/test_learning_db_v28.py +110 -0
  97. package/src/learning/tests/test_learning_init_v28.py +48 -0
  98. package/src/learning/tests/test_outcome_signals.py +48 -0
  99. package/src/learning/tests/test_project_context.py +2 -6
  100. package/src/learning/tests/test_schema_migration.py +319 -0
  101. package/src/learning/tests/test_signal_inference.py +11 -13
  102. package/src/learning/tests/test_source_quality.py +2 -6
  103. package/src/learning/tests/test_synthetic_bootstrap.py +3 -7
  104. package/src/learning/tests/test_workflow_miner.py +2 -6
  105. package/src/learning/workflow_pattern_miner.py +2 -12
  106. package/src/lifecycle/__init__.py +54 -0
  107. package/src/lifecycle/bounded_growth.py +239 -0
  108. package/src/lifecycle/compaction_engine.py +226 -0
  109. package/src/lifecycle/lifecycle_engine.py +302 -0
  110. package/src/lifecycle/lifecycle_evaluator.py +225 -0
  111. package/src/lifecycle/lifecycle_scheduler.py +130 -0
  112. package/src/lifecycle/retention_policy.py +285 -0
  113. package/src/lifecycle/tests/__init__.py +4 -0
  114. package/src/lifecycle/tests/test_bounded_growth.py +193 -0
  115. package/src/lifecycle/tests/test_compaction.py +179 -0
  116. package/src/lifecycle/tests/test_lifecycle_engine.py +137 -0
  117. package/src/lifecycle/tests/test_lifecycle_evaluation.py +177 -0
  118. package/src/lifecycle/tests/test_lifecycle_scheduler.py +127 -0
  119. package/src/lifecycle/tests/test_lifecycle_search.py +109 -0
  120. package/src/lifecycle/tests/test_mcp_compact.py +149 -0
  121. package/src/lifecycle/tests/test_mcp_lifecycle_status.py +114 -0
  122. package/src/lifecycle/tests/test_retention_policy.py +162 -0
  123. package/src/mcp_tools_v28.py +280 -0
  124. package/src/memory-profiles.py +2 -12
  125. package/src/memory-reset.py +2 -12
  126. package/src/memory_compression.py +2 -12
  127. package/src/memory_store_v2.py +76 -20
  128. package/src/migrate_v1_to_v2.py +2 -12
  129. package/src/pattern_learner.py +29 -975
  130. package/src/patterns/__init__.py +24 -0
  131. package/src/patterns/analyzers.py +247 -0
  132. package/src/patterns/learner.py +267 -0
  133. package/src/patterns/scoring.py +167 -0
  134. package/src/patterns/store.py +223 -0
  135. package/src/patterns/terminology.py +138 -0
  136. package/src/provenance_tracker.py +4 -14
  137. package/src/query_optimizer.py +4 -6
  138. package/src/rate_limiter.py +2 -6
  139. package/src/search/__init__.py +20 -0
  140. package/src/search/cli.py +77 -0
  141. package/src/search/constants.py +26 -0
  142. package/src/search/engine.py +239 -0
  143. package/src/search/fusion.py +122 -0
  144. package/src/search/index_loader.py +112 -0
  145. package/src/search/methods.py +162 -0
  146. package/src/search_engine_v2.py +4 -6
  147. package/src/setup_validator.py +7 -13
  148. package/src/subscription_manager.py +2 -12
  149. package/src/tree/__init__.py +59 -0
  150. package/src/tree/builder.py +183 -0
  151. package/src/tree/nodes.py +196 -0
  152. package/src/tree/queries.py +252 -0
  153. package/src/tree/schema.py +76 -0
  154. package/src/tree_manager.py +10 -711
  155. package/src/trust/__init__.py +45 -0
  156. package/src/trust/constants.py +66 -0
  157. package/src/trust/queries.py +157 -0
  158. package/src/trust/schema.py +95 -0
  159. package/src/trust/scorer.py +299 -0
  160. package/src/trust/signals.py +95 -0
  161. package/src/trust_scorer.py +39 -697
  162. package/src/webhook_dispatcher.py +2 -12
  163. package/ui/app.js +1 -1
  164. package/ui/js/agents.js +1 -1
  165. package/ui_server.py +2 -14
  166. package/ATTRIBUTION.md +0 -140
  167. package/docs/ARCHITECTURE-V2.5.md +0 -190
  168. package/docs/GRAPH-ENGINE.md +0 -503
  169. package/docs/architecture-diagram.drawio +0 -405
  170. package/docs/plans/2026-02-13-benchmark-suite.md +0 -1349
@@ -0,0 +1,98 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
3
+ """Tests for audit logger EventBus listener.
4
+ """
5
+ import tempfile, os, sys, sqlite3
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
9
+
10
+ class TestAuditLogger:
11
+ def setup_method(self):
12
+ self.tmp_dir = tempfile.mkdtemp()
13
+ self.audit_db_path = os.path.join(self.tmp_dir, "audit.db")
14
+
15
+ def teardown_method(self):
16
+ import shutil
17
+ shutil.rmtree(self.tmp_dir, ignore_errors=True)
18
+
19
+ def test_creation(self):
20
+ from compliance.audit_logger import AuditLogger
21
+ logger = AuditLogger(self.audit_db_path)
22
+ assert logger is not None
23
+
24
+ def test_logs_memory_created(self):
25
+ from compliance.audit_logger import AuditLogger
26
+ logger = AuditLogger(self.audit_db_path)
27
+ logger.handle_event({"event_type": "memory.created", "memory_id": 1, "payload": {}, "timestamp": datetime.now().isoformat(), "source_agent": "user"})
28
+ conn = sqlite3.connect(self.audit_db_path)
29
+ rows = conn.execute("SELECT * FROM audit_events WHERE event_type='memory.created'").fetchall()
30
+ conn.close()
31
+ assert len(rows) == 1
32
+
33
+ def test_logs_memory_recalled(self):
34
+ from compliance.audit_logger import AuditLogger
35
+ logger = AuditLogger(self.audit_db_path)
36
+ logger.handle_event({"event_type": "memory.recalled", "memory_id": 2, "payload": {"query": "test"}, "timestamp": datetime.now().isoformat(), "source_agent": "agent_a"})
37
+ conn = sqlite3.connect(self.audit_db_path)
38
+ rows = conn.execute("SELECT * FROM audit_events WHERE event_type='memory.recalled'").fetchall()
39
+ conn.close()
40
+ assert len(rows) == 1
41
+
42
+ def test_logs_memory_deleted(self):
43
+ from compliance.audit_logger import AuditLogger
44
+ logger = AuditLogger(self.audit_db_path)
45
+ logger.handle_event({"event_type": "memory.deleted", "memory_id": 3, "payload": {}, "timestamp": datetime.now().isoformat(), "source_agent": "user"})
46
+ conn = sqlite3.connect(self.audit_db_path)
47
+ rows = conn.execute("SELECT * FROM audit_events WHERE event_type='memory.deleted'").fetchall()
48
+ conn.close()
49
+ assert len(rows) == 1
50
+
51
+ def test_hash_chain_maintained(self):
52
+ """Multiple events maintain hash chain integrity."""
53
+ from compliance.audit_logger import AuditLogger
54
+ logger = AuditLogger(self.audit_db_path)
55
+ for i in range(5):
56
+ logger.handle_event({"event_type": "memory.created", "memory_id": i, "payload": {}, "timestamp": datetime.now().isoformat(), "source_agent": "user"})
57
+ from compliance.audit_db import AuditDB
58
+ db = AuditDB(self.audit_db_path)
59
+ result = db.verify_chain()
60
+ assert result["valid"] is True
61
+ assert result["entries_checked"] == 5
62
+
63
+ def test_logs_lifecycle_transitions(self):
64
+ from compliance.audit_logger import AuditLogger
65
+ logger = AuditLogger(self.audit_db_path)
66
+ logger.handle_event({"event_type": "lifecycle.transitioned", "memory_id": 1, "payload": {"from_state": "active", "to_state": "warm"}, "timestamp": datetime.now().isoformat(), "source_agent": "scheduler"})
67
+ conn = sqlite3.connect(self.audit_db_path)
68
+ rows = conn.execute("SELECT * FROM audit_events").fetchall()
69
+ conn.close()
70
+ assert len(rows) == 1
71
+
72
+ def test_ignores_unknown_gracefully(self):
73
+ """Unknown event types logged without error."""
74
+ from compliance.audit_logger import AuditLogger
75
+ logger = AuditLogger(self.audit_db_path)
76
+ logger.handle_event({"event_type": "unknown.event", "payload": {}, "timestamp": datetime.now().isoformat(), "source_agent": "test"})
77
+ assert logger.events_logged >= 1
78
+
79
+ def test_graceful_on_malformed_event(self):
80
+ """Malformed events don't crash the logger."""
81
+ from compliance.audit_logger import AuditLogger
82
+ logger = AuditLogger(self.audit_db_path)
83
+ logger.handle_event({}) # Empty event
84
+ logger.handle_event({"event_type": "test"}) # Missing fields
85
+ # Should not crash
86
+
87
+ def test_register_with_eventbus(self):
88
+ from compliance.audit_logger import AuditLogger
89
+ logger = AuditLogger(self.audit_db_path)
90
+ result = logger.register_with_eventbus()
91
+ assert isinstance(result, bool)
92
+
93
+ def test_get_status(self):
94
+ from compliance.audit_logger import AuditLogger
95
+ logger = AuditLogger(self.audit_db_path)
96
+ status = logger.get_status()
97
+ assert "events_logged" in status
98
+ assert "registered" in status
@@ -0,0 +1,128 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
3
+ """Tests for audit_trail MCP tool handler.
4
+
5
+ Validates the MCP wrapper around AuditDB — tests empty trail, event logging,
6
+ event_type and actor filtering, and hash chain verification.
7
+ """
8
+ import asyncio
9
+ import os
10
+ import shutil
11
+ import sys
12
+ import tempfile
13
+ from pathlib import Path
14
+
15
+ import pytest
16
+
17
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
18
+
19
+
20
+ class TestMCPAuditTrail:
21
+ """Tests for the audit_trail tool handler."""
22
+
23
+ def setup_method(self):
24
+ self.tmp_dir = tempfile.mkdtemp()
25
+ self.db_path = os.path.join(self.tmp_dir, "audit.db")
26
+
27
+ def teardown_method(self):
28
+ shutil.rmtree(self.tmp_dir, ignore_errors=True)
29
+
30
+ def _run(self, coro):
31
+ return asyncio.get_event_loop().run_until_complete(coro)
32
+
33
+ def test_empty_trail(self):
34
+ """Fresh audit DB should return count=0."""
35
+ import mcp_tools_v28 as tools
36
+ tools.DEFAULT_AUDIT_DB = self.db_path
37
+
38
+ result = self._run(tools.audit_trail())
39
+ assert result["success"] is True
40
+ assert result["count"] == 0
41
+ assert result["events"] == []
42
+
43
+ def test_verify_empty_chain(self):
44
+ """Hash chain verification on empty DB should be valid."""
45
+ import mcp_tools_v28 as tools
46
+ tools.DEFAULT_AUDIT_DB = self.db_path
47
+
48
+ result = self._run(tools.audit_trail(verify_chain=True))
49
+ assert result["success"] is True
50
+ assert result["chain_valid"] is True
51
+ assert result["chain_entries"] == 0
52
+
53
+ def test_query_with_events(self):
54
+ """After logging events, query should return them."""
55
+ from compliance.audit_db import AuditDB
56
+
57
+ db = AuditDB(self.db_path)
58
+ db.log_event("memory.created", actor="user", resource_id=1)
59
+ db.log_event("memory.recalled", actor="agent_a", resource_id=1)
60
+ db.log_event("memory.created", actor="user", resource_id=2)
61
+
62
+ import mcp_tools_v28 as tools
63
+ tools.DEFAULT_AUDIT_DB = self.db_path
64
+
65
+ result = self._run(tools.audit_trail())
66
+ assert result["success"] is True
67
+ assert result["count"] == 3
68
+
69
+ def test_filter_by_event_type(self):
70
+ """Filtering by event_type should narrow results."""
71
+ from compliance.audit_db import AuditDB
72
+
73
+ db = AuditDB(self.db_path)
74
+ db.log_event("memory.created", actor="user", resource_id=1)
75
+ db.log_event("memory.recalled", actor="agent_a", resource_id=1)
76
+
77
+ import mcp_tools_v28 as tools
78
+ tools.DEFAULT_AUDIT_DB = self.db_path
79
+
80
+ result = self._run(tools.audit_trail(event_type="memory.created"))
81
+ assert result["count"] == 1
82
+ assert result["events"][0]["event_type"] == "memory.created"
83
+
84
+ def test_filter_by_actor(self):
85
+ """Filtering by actor should narrow results."""
86
+ from compliance.audit_db import AuditDB
87
+
88
+ db = AuditDB(self.db_path)
89
+ db.log_event("memory.created", actor="user", resource_id=1)
90
+ db.log_event("memory.recalled", actor="agent_a", resource_id=1)
91
+
92
+ import mcp_tools_v28 as tools
93
+ tools.DEFAULT_AUDIT_DB = self.db_path
94
+
95
+ result = self._run(tools.audit_trail(actor="agent_a"))
96
+ assert result["count"] == 1
97
+ assert result["events"][0]["actor"] == "agent_a"
98
+
99
+ def test_verify_chain_with_events(self):
100
+ """Hash chain with events should verify successfully."""
101
+ from compliance.audit_db import AuditDB
102
+
103
+ db = AuditDB(self.db_path)
104
+ db.log_event("memory.created", actor="user", resource_id=1)
105
+ db.log_event("memory.recalled", actor="user", resource_id=1)
106
+ db.log_event("memory.updated", actor="user", resource_id=1)
107
+
108
+ import mcp_tools_v28 as tools
109
+ tools.DEFAULT_AUDIT_DB = self.db_path
110
+
111
+ result = self._run(tools.audit_trail(verify_chain=True))
112
+ assert result["success"] is True
113
+ assert result["chain_valid"] is True
114
+ assert result["chain_entries"] == 3
115
+
116
+ def test_limit_parameter(self):
117
+ """Limit parameter should cap returned events."""
118
+ from compliance.audit_db import AuditDB
119
+
120
+ db = AuditDB(self.db_path)
121
+ for i in range(10):
122
+ db.log_event("memory.created", actor="user", resource_id=i)
123
+
124
+ import mcp_tools_v28 as tools
125
+ tools.DEFAULT_AUDIT_DB = self.db_path
126
+
127
+ result = self._run(tools.audit_trail(limit=3))
128
+ assert result["count"] == 3
@@ -0,0 +1,125 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
3
+ """Tests for set_retention_policy MCP tool handler.
4
+
5
+ Validates the MCP wrapper around RetentionPolicyManager — tests policy
6
+ creation with tags, project scope, and various framework types.
7
+ """
8
+ import asyncio
9
+ import os
10
+ import shutil
11
+ import sqlite3
12
+ import sys
13
+ import tempfile
14
+ from pathlib import Path
15
+
16
+ import pytest
17
+
18
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
19
+
20
+
21
+ def _create_memory_db(db_path: str) -> None:
22
+ """Create a minimal memory.db for RetentionPolicyManager."""
23
+ conn = sqlite3.connect(db_path)
24
+ conn.execute(
25
+ """CREATE TABLE memories (
26
+ id INTEGER PRIMARY KEY,
27
+ content TEXT,
28
+ tags TEXT DEFAULT '[]',
29
+ project_name TEXT,
30
+ lifecycle_state TEXT DEFAULT 'active',
31
+ profile TEXT DEFAULT 'default'
32
+ )"""
33
+ )
34
+ conn.commit()
35
+ conn.close()
36
+
37
+
38
+ class TestMCPRetentionPolicy:
39
+ """Tests for the set_retention_policy tool handler."""
40
+
41
+ def setup_method(self):
42
+ self.tmp_dir = tempfile.mkdtemp()
43
+ self.db_path = os.path.join(self.tmp_dir, "memory.db")
44
+ _create_memory_db(self.db_path)
45
+
46
+ def teardown_method(self):
47
+ shutil.rmtree(self.tmp_dir, ignore_errors=True)
48
+
49
+ def _run(self, coro):
50
+ return asyncio.get_event_loop().run_until_complete(coro)
51
+
52
+ def test_create_gdpr_policy(self):
53
+ """Creating a GDPR tombstone policy should return success with policy_id."""
54
+ import mcp_tools_v28 as tools
55
+ tools.DEFAULT_MEMORY_DB = self.db_path
56
+
57
+ result = self._run(
58
+ tools.set_retention_policy(
59
+ "GDPR Erasure", "gdpr", 0, "tombstone", ["gdpr"]
60
+ )
61
+ )
62
+ assert result["success"] is True
63
+ assert isinstance(result["policy_id"], int)
64
+ assert result["policy_id"] > 0
65
+ assert result["name"] == "GDPR Erasure"
66
+ assert result["framework"] == "gdpr"
67
+
68
+ def test_create_hipaa_policy(self):
69
+ """Creating a HIPAA retention policy should succeed."""
70
+ import mcp_tools_v28 as tools
71
+ tools.DEFAULT_MEMORY_DB = self.db_path
72
+
73
+ result = self._run(
74
+ tools.set_retention_policy(
75
+ "HIPAA Retention", "hipaa", 2190, "retain", ["medical"]
76
+ )
77
+ )
78
+ assert result["success"] is True
79
+ assert result["framework"] == "hipaa"
80
+
81
+ def test_create_policy_with_project(self):
82
+ """Policy scoped to a project should succeed."""
83
+ import mcp_tools_v28 as tools
84
+ tools.DEFAULT_MEMORY_DB = self.db_path
85
+
86
+ result = self._run(
87
+ tools.set_retention_policy(
88
+ "Internal Retention",
89
+ "internal",
90
+ 365,
91
+ "archive",
92
+ applies_to_project="myproject",
93
+ )
94
+ )
95
+ assert result["success"] is True
96
+
97
+ def test_create_policy_with_tags_and_project(self):
98
+ """Policy with both tags and project scope should succeed."""
99
+ import mcp_tools_v28 as tools
100
+ tools.DEFAULT_MEMORY_DB = self.db_path
101
+
102
+ result = self._run(
103
+ tools.set_retention_policy(
104
+ "EU AI Act",
105
+ "eu_ai_act",
106
+ 1825,
107
+ "retain",
108
+ applies_to_tags=["ai-decision"],
109
+ applies_to_project="ml-pipeline",
110
+ )
111
+ )
112
+ assert result["success"] is True
113
+
114
+ def test_multiple_policies_unique_ids(self):
115
+ """Consecutive policies should get distinct IDs."""
116
+ import mcp_tools_v28 as tools
117
+ tools.DEFAULT_MEMORY_DB = self.db_path
118
+
119
+ r1 = self._run(
120
+ tools.set_retention_policy("Policy A", "gdpr", 0, "tombstone", ["a"])
121
+ )
122
+ r2 = self._run(
123
+ tools.set_retention_policy("Policy B", "hipaa", 365, "retain", ["b"])
124
+ )
125
+ assert r1["policy_id"] != r2["policy_id"]
@@ -0,0 +1,131 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
3
+ """Tests for compliance retention manager.
4
+ """
5
+ import sqlite3
6
+ import tempfile
7
+ import os
8
+ import sys
9
+ import json
10
+ from datetime import datetime, timedelta
11
+ from pathlib import Path
12
+
13
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
14
+
15
+
16
+ class TestComplianceRetentionManager:
17
+ def setup_method(self):
18
+ self.tmp_dir = tempfile.mkdtemp()
19
+ self.memory_db_path = os.path.join(self.tmp_dir, "memory.db")
20
+ self.audit_db_path = os.path.join(self.tmp_dir, "audit.db")
21
+
22
+ # Create memory.db with test data
23
+ conn = sqlite3.connect(self.memory_db_path)
24
+ conn.execute("""
25
+ CREATE TABLE memories (
26
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
27
+ content TEXT NOT NULL,
28
+ importance INTEGER DEFAULT 5,
29
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
30
+ last_accessed TIMESTAMP,
31
+ access_count INTEGER DEFAULT 0,
32
+ lifecycle_state TEXT DEFAULT 'active',
33
+ lifecycle_updated_at TIMESTAMP,
34
+ lifecycle_history TEXT DEFAULT '[]',
35
+ access_level TEXT DEFAULT 'public',
36
+ profile TEXT DEFAULT 'default',
37
+ tags TEXT DEFAULT '[]',
38
+ project_name TEXT
39
+ )
40
+ """)
41
+ conn.execute("INSERT INTO memories (content, tags, project_name) VALUES ('user PII data', '[\"gdpr\",\"pii\"]', 'eu-app')")
42
+ conn.execute("INSERT INTO memories (content, tags, project_name) VALUES ('medical record', '[\"hipaa\"]', 'healthcare')")
43
+ conn.execute("INSERT INTO memories (content, tags) VALUES ('general note', '[]')")
44
+ conn.commit()
45
+ conn.close()
46
+
47
+ def teardown_method(self):
48
+ import shutil
49
+ shutil.rmtree(self.tmp_dir, ignore_errors=True)
50
+
51
+ def test_creation(self):
52
+ from compliance.retention_manager import ComplianceRetentionManager
53
+ mgr = ComplianceRetentionManager(self.memory_db_path, self.audit_db_path)
54
+ assert mgr is not None
55
+
56
+ def test_create_gdpr_policy(self):
57
+ from compliance.retention_manager import ComplianceRetentionManager
58
+ mgr = ComplianceRetentionManager(self.memory_db_path, self.audit_db_path)
59
+ pid = mgr.create_retention_rule(
60
+ name="GDPR Right to Erasure",
61
+ framework="gdpr",
62
+ retention_days=0,
63
+ action="tombstone",
64
+ applies_to={"tags": ["gdpr"]},
65
+ )
66
+ assert isinstance(pid, int)
67
+
68
+ def test_create_eu_ai_act_policy(self):
69
+ from compliance.retention_manager import ComplianceRetentionManager
70
+ mgr = ComplianceRetentionManager(self.memory_db_path, self.audit_db_path)
71
+ pid = mgr.create_retention_rule(
72
+ name="EU AI Act Audit Retention",
73
+ framework="eu_ai_act",
74
+ retention_days=3650,
75
+ action="retain_audit",
76
+ applies_to={"tags": ["gdpr"]},
77
+ )
78
+ assert pid > 0
79
+
80
+ def test_gdpr_erasure_tombstones_memory(self):
81
+ """GDPR erasure request tombstones the memory."""
82
+ from compliance.retention_manager import ComplianceRetentionManager
83
+ mgr = ComplianceRetentionManager(self.memory_db_path, self.audit_db_path)
84
+ result = mgr.execute_erasure_request(memory_id=1, framework="gdpr", requested_by="data_subject")
85
+ assert result["success"] is True
86
+ assert result["action"] == "tombstoned"
87
+ # Verify in DB
88
+ conn = sqlite3.connect(self.memory_db_path)
89
+ row = conn.execute("SELECT lifecycle_state FROM memories WHERE id=1").fetchone()
90
+ conn.close()
91
+ assert row[0] == "tombstoned"
92
+
93
+ def test_gdpr_erasure_preserves_audit(self):
94
+ """GDPR erasure logs the action to audit.db."""
95
+ from compliance.retention_manager import ComplianceRetentionManager
96
+ mgr = ComplianceRetentionManager(self.memory_db_path, self.audit_db_path)
97
+ mgr.execute_erasure_request(memory_id=1, framework="gdpr", requested_by="data_subject")
98
+ conn = sqlite3.connect(self.audit_db_path)
99
+ rows = conn.execute("SELECT * FROM audit_events WHERE event_type='retention.erasure'").fetchall()
100
+ conn.close()
101
+ assert len(rows) >= 1
102
+
103
+ def test_list_rules(self):
104
+ from compliance.retention_manager import ComplianceRetentionManager
105
+ mgr = ComplianceRetentionManager(self.memory_db_path, self.audit_db_path)
106
+ mgr.create_retention_rule("GDPR", "gdpr", 0, "tombstone", {"tags": ["gdpr"]})
107
+ mgr.create_retention_rule("HIPAA", "hipaa", 2555, "retain", {"tags": ["hipaa"]})
108
+ rules = mgr.list_rules()
109
+ assert len(rules) == 2
110
+
111
+ def test_evaluate_memory_against_rules(self):
112
+ from compliance.retention_manager import ComplianceRetentionManager
113
+ mgr = ComplianceRetentionManager(self.memory_db_path, self.audit_db_path)
114
+ mgr.create_retention_rule("HIPAA Retention", "hipaa", 2555, "retain", {"tags": ["hipaa"]})
115
+ result = mgr.evaluate_memory(2) # Memory 2 has hipaa tag
116
+ assert result is not None
117
+ assert result["rule_name"] == "HIPAA Retention"
118
+
119
+ def test_no_rule_match_returns_none(self):
120
+ from compliance.retention_manager import ComplianceRetentionManager
121
+ mgr = ComplianceRetentionManager(self.memory_db_path, self.audit_db_path)
122
+ mgr.create_retention_rule("HIPAA", "hipaa", 2555, "retain", {"tags": ["hipaa"]})
123
+ result = mgr.evaluate_memory(3) # Memory 3 has no hipaa tag
124
+ assert result is None
125
+
126
+ def test_get_compliance_status(self):
127
+ from compliance.retention_manager import ComplianceRetentionManager
128
+ mgr = ComplianceRetentionManager(self.memory_db_path, self.audit_db_path)
129
+ status = mgr.get_compliance_status()
130
+ assert "rules_count" in status
131
+ assert "frameworks" in status
@@ -0,0 +1,99 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
3
+ """Tests for retention policy background scheduler.
4
+ """
5
+ import sqlite3, tempfile, os, sys, json, threading
6
+ from datetime import datetime, timedelta
7
+ from pathlib import Path
8
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
9
+
10
+ class TestRetentionScheduler:
11
+ def setup_method(self):
12
+ self.tmp_dir = tempfile.mkdtemp()
13
+ self.memory_db = os.path.join(self.tmp_dir, "memory.db")
14
+ self.audit_db = os.path.join(self.tmp_dir, "audit.db")
15
+ conn = sqlite3.connect(self.memory_db)
16
+ conn.execute("""
17
+ CREATE TABLE memories (
18
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
19
+ content TEXT NOT NULL,
20
+ importance INTEGER DEFAULT 5,
21
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
22
+ last_accessed TIMESTAMP,
23
+ access_count INTEGER DEFAULT 0,
24
+ lifecycle_state TEXT DEFAULT 'active',
25
+ lifecycle_updated_at TIMESTAMP,
26
+ lifecycle_history TEXT DEFAULT '[]',
27
+ access_level TEXT DEFAULT 'public',
28
+ profile TEXT DEFAULT 'default',
29
+ tags TEXT DEFAULT '[]',
30
+ project_name TEXT
31
+ )
32
+ """)
33
+ now = datetime.now()
34
+ # Memory 1: old GDPR data (created 400 days ago)
35
+ conn.execute("INSERT INTO memories (content, tags, created_at, lifecycle_state) VALUES (?, ?, ?, ?)",
36
+ ("old PII data", '["gdpr"]', (now - timedelta(days=400)).isoformat(), "active"))
37
+ # Memory 2: recent data
38
+ conn.execute("INSERT INTO memories (content, tags) VALUES (?, ?)",
39
+ ("fresh data", '[]'))
40
+ # Memory 3: tombstoned (should be checked for final deletion)
41
+ conn.execute("INSERT INTO memories (content, lifecycle_state, created_at) VALUES (?, ?, ?)",
42
+ ("tombstoned data", "tombstoned", (now - timedelta(days=100)).isoformat()))
43
+ conn.commit()
44
+ conn.close()
45
+
46
+ def teardown_method(self):
47
+ import shutil
48
+ shutil.rmtree(self.tmp_dir, ignore_errors=True)
49
+
50
+ def test_creation(self):
51
+ from compliance.retention_scheduler import RetentionScheduler
52
+ sched = RetentionScheduler(self.memory_db, self.audit_db)
53
+ assert sched is not None
54
+
55
+ def test_default_interval(self):
56
+ from compliance.retention_scheduler import RetentionScheduler
57
+ sched = RetentionScheduler(self.memory_db, self.audit_db)
58
+ assert sched.interval_seconds == 86400 # 24 hours
59
+
60
+ def test_run_now(self):
61
+ from compliance.retention_scheduler import RetentionScheduler
62
+ sched = RetentionScheduler(self.memory_db, self.audit_db)
63
+ result = sched.run_now()
64
+ assert "timestamp" in result
65
+ assert "actions" in result
66
+
67
+ def test_start_and_stop(self):
68
+ from compliance.retention_scheduler import RetentionScheduler
69
+ sched = RetentionScheduler(self.memory_db, self.audit_db, interval_seconds=3600)
70
+ sched.start()
71
+ assert sched.is_running is True
72
+ sched.stop()
73
+ assert sched.is_running is False
74
+
75
+ def test_thread_is_daemon(self):
76
+ from compliance.retention_scheduler import RetentionScheduler
77
+ sched = RetentionScheduler(self.memory_db, self.audit_db, interval_seconds=3600)
78
+ sched.start()
79
+ assert sched._timer.daemon is True
80
+ sched.stop()
81
+
82
+ def test_manual_trigger_works(self):
83
+ from compliance.retention_scheduler import RetentionScheduler
84
+ sched = RetentionScheduler(self.memory_db, self.audit_db)
85
+ result = sched.run_now()
86
+ assert isinstance(result["actions"], list)
87
+
88
+ def test_configurable_interval(self):
89
+ from compliance.retention_scheduler import RetentionScheduler
90
+ sched = RetentionScheduler(self.memory_db, self.audit_db, interval_seconds=7200)
91
+ assert sched.interval_seconds == 7200
92
+
93
+ def test_result_structure(self):
94
+ from compliance.retention_scheduler import RetentionScheduler
95
+ sched = RetentionScheduler(self.memory_db, self.audit_db)
96
+ result = sched.run_now()
97
+ assert "timestamp" in result
98
+ assert "actions" in result
99
+ assert "rules_evaluated" in result
@@ -1,16 +1,6 @@
1
1
  #!/usr/bin/env python3
2
- """
3
- SuperLocalMemory V2 - Database Connection Manager
4
- Copyright (c) 2026 Varun Pratap Bhardwaj
5
- Licensed under MIT License
6
-
7
- Repository: https://github.com/varun369/SuperLocalMemoryV2
8
- Author: Varun Pratap Bhardwaj (Solution Architect)
9
-
10
- NOTICE: This software is protected by MIT License.
11
- Attribution must be preserved in all copies or derivatives.
12
- """
13
-
2
+ # SPDX-License-Identifier: MIT
3
+ # Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
14
4
  """
15
5
  DbConnectionManager — Thread-safe SQLite connection management with WAL mode.
16
6