superlocalmemory 2.6.0 → 2.7.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 (46) hide show
  1. package/CHANGELOG.md +167 -1803
  2. package/README.md +212 -397
  3. package/bin/slm +179 -3
  4. package/bin/superlocalmemoryv2:learning +4 -0
  5. package/bin/superlocalmemoryv2:patterns +4 -0
  6. package/docs/ACCESSIBILITY.md +291 -0
  7. package/docs/ARCHITECTURE.md +12 -6
  8. package/docs/FRAMEWORK-INTEGRATIONS.md +300 -0
  9. package/docs/MCP-MANUAL-SETUP.md +14 -4
  10. package/install.sh +99 -3
  11. package/mcp_server.py +291 -1
  12. package/package.json +2 -1
  13. package/requirements-learning.txt +12 -0
  14. package/scripts/verify-v27.sh +233 -0
  15. package/skills/slm-show-patterns/SKILL.md +224 -0
  16. package/src/learning/__init__.py +201 -0
  17. package/src/learning/adaptive_ranker.py +826 -0
  18. package/src/learning/cross_project_aggregator.py +866 -0
  19. package/src/learning/engagement_tracker.py +638 -0
  20. package/src/learning/feature_extractor.py +461 -0
  21. package/src/learning/feedback_collector.py +690 -0
  22. package/src/learning/learning_db.py +842 -0
  23. package/src/learning/project_context_manager.py +582 -0
  24. package/src/learning/source_quality_scorer.py +685 -0
  25. package/src/learning/synthetic_bootstrap.py +1047 -0
  26. package/src/learning/tests/__init__.py +0 -0
  27. package/src/learning/tests/test_adaptive_ranker.py +328 -0
  28. package/src/learning/tests/test_aggregator.py +309 -0
  29. package/src/learning/tests/test_feedback_collector.py +295 -0
  30. package/src/learning/tests/test_learning_db.py +606 -0
  31. package/src/learning/tests/test_project_context.py +296 -0
  32. package/src/learning/tests/test_source_quality.py +355 -0
  33. package/src/learning/tests/test_synthetic_bootstrap.py +433 -0
  34. package/src/learning/tests/test_workflow_miner.py +322 -0
  35. package/src/learning/workflow_pattern_miner.py +665 -0
  36. package/ui/index.html +346 -13
  37. package/ui/js/clusters.js +90 -1
  38. package/ui/js/graph-core.js +445 -0
  39. package/ui/js/graph-cytoscape-monolithic-backup.js +1168 -0
  40. package/ui/js/graph-cytoscape.js +1168 -0
  41. package/ui/js/graph-d3-backup.js +32 -0
  42. package/ui/js/graph-filters.js +220 -0
  43. package/ui/js/graph-interactions.js +354 -0
  44. package/ui/js/graph-ui.js +214 -0
  45. package/ui/js/memories.js +52 -0
  46. package/ui/js/modal.js +104 -1
@@ -0,0 +1,322 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ SuperLocalMemory V2 - Tests for WorkflowPatternMiner (v2.7)
4
+ Copyright (c) 2026 Varun Pratap Bhardwaj
5
+ Licensed under MIT License
6
+ """
7
+
8
+ import pytest
9
+
10
+
11
+ # ---------------------------------------------------------------------------
12
+ # Fixtures
13
+ # ---------------------------------------------------------------------------
14
+
15
+ @pytest.fixture(autouse=True)
16
+ def reset_singleton():
17
+ from src.learning.learning_db import LearningDB
18
+ LearningDB.reset_instance()
19
+ yield
20
+ LearningDB.reset_instance()
21
+
22
+
23
+ @pytest.fixture
24
+ def learning_db(tmp_path):
25
+ from src.learning.learning_db import LearningDB
26
+ db_path = tmp_path / "learning.db"
27
+ return LearningDB(db_path=db_path)
28
+
29
+
30
+ @pytest.fixture
31
+ def miner(learning_db):
32
+ from src.learning.workflow_pattern_miner import WorkflowPatternMiner
33
+ return WorkflowPatternMiner(learning_db=learning_db)
34
+
35
+
36
+ @pytest.fixture
37
+ def miner_no_db():
38
+ from src.learning.workflow_pattern_miner import WorkflowPatternMiner
39
+ return WorkflowPatternMiner(learning_db=None)
40
+
41
+
42
+ # ---------------------------------------------------------------------------
43
+ # Activity Classification
44
+ # ---------------------------------------------------------------------------
45
+
46
+ class TestClassifyActivity:
47
+ """Test _classify_activity for all 7 types + unknown."""
48
+
49
+ def test_docs(self, miner):
50
+ assert miner._classify_activity("Updated the documentation for API") == "docs"
51
+ assert miner._classify_activity("Added README and wiki pages") == "docs"
52
+
53
+ def test_architecture(self, miner):
54
+ assert miner._classify_activity("Designed the system architecture") == "architecture"
55
+ assert miner._classify_activity("Created ERD for data model") == "architecture"
56
+
57
+ def test_code(self, miner):
58
+ assert miner._classify_activity("Implemented the new function for parsing") == "code"
59
+ assert miner._classify_activity("Refactored the module structure") == "code"
60
+
61
+ def test_test(self, miner):
62
+ assert miner._classify_activity("Added pytest fixtures and assertions") == "test"
63
+ assert miner._classify_activity("Wrote unit test for the parser") == "test"
64
+
65
+ def test_debug(self, miner):
66
+ assert miner._classify_activity("Fixed the bug in authentication") == "debug"
67
+ assert miner._classify_activity("Analyzed error traceback in production") == "debug"
68
+
69
+ def test_deploy(self, miner):
70
+ assert miner._classify_activity("Deployed via Docker to production") == "deploy"
71
+ assert miner._classify_activity("Updated the CI/CD pipeline") == "deploy"
72
+
73
+ def test_config(self, miner):
74
+ assert miner._classify_activity("Updated config settings and env variables") == "config"
75
+ assert miner._classify_activity("Added package dependency to requirements") == "config"
76
+
77
+ def test_unknown(self, miner):
78
+ assert miner._classify_activity("Had a meeting about the roadmap") == "unknown"
79
+ assert miner._classify_activity("") == "unknown"
80
+ assert miner._classify_activity("random unrelated text") == "unknown"
81
+
82
+ def test_word_boundary_matching(self, miner):
83
+ """'test' should match 'test' but not 'latest'."""
84
+ assert miner._classify_activity("ran the test suite") == "test"
85
+ # "latest" contains "test" but word boundary prevents match
86
+ # However, 'release' is a deploy keyword
87
+ result = miner._classify_activity("checked the latest version of release")
88
+ assert result == "deploy" # 'release' is deploy keyword
89
+
90
+
91
+ # ---------------------------------------------------------------------------
92
+ # Hour to Bucket Mapping
93
+ # ---------------------------------------------------------------------------
94
+
95
+ class TestHourToBucket:
96
+ def test_night_hours(self, miner):
97
+ for hour in [0, 1, 2, 3, 4, 5]:
98
+ assert miner._hour_to_bucket(hour) == "night", f"hour={hour}"
99
+
100
+ def test_morning_hours(self, miner):
101
+ for hour in [6, 7, 8, 9, 10, 11]:
102
+ assert miner._hour_to_bucket(hour) == "morning", f"hour={hour}"
103
+
104
+ def test_afternoon_hours(self, miner):
105
+ for hour in [12, 13, 14, 15, 16, 17]:
106
+ assert miner._hour_to_bucket(hour) == "afternoon", f"hour={hour}"
107
+
108
+ def test_evening_hours(self, miner):
109
+ for hour in [18, 19, 20, 21, 22, 23]:
110
+ assert miner._hour_to_bucket(hour) == "evening", f"hour={hour}"
111
+
112
+ def test_boundary_cases(self, miner):
113
+ assert miner._hour_to_bucket(0) == "night"
114
+ assert miner._hour_to_bucket(5) == "night"
115
+ assert miner._hour_to_bucket(6) == "morning"
116
+ assert miner._hour_to_bucket(11) == "morning"
117
+ assert miner._hour_to_bucket(12) == "afternoon"
118
+ assert miner._hour_to_bucket(17) == "afternoon"
119
+ assert miner._hour_to_bucket(18) == "evening"
120
+ assert miner._hour_to_bucket(23) == "evening"
121
+
122
+
123
+ # ---------------------------------------------------------------------------
124
+ # Sequence Mining
125
+ # ---------------------------------------------------------------------------
126
+
127
+ class TestMineSequences:
128
+ def test_basic_sequence(self, miner):
129
+ """A repeating pattern should be detected."""
130
+ memories = []
131
+ # Repeat: docs -> code -> test, 5 times
132
+ activity_words = {
133
+ "docs": "Updated the documentation",
134
+ "code": "Implemented new feature function",
135
+ "test": "Added pytest coverage",
136
+ }
137
+ for _ in range(5):
138
+ for act in ["docs", "code", "test"]:
139
+ memories.append({
140
+ "content": activity_words[act],
141
+ "created_at": "2026-02-16 10:00:00",
142
+ })
143
+
144
+ results = miner.mine_sequences(memories=memories, min_support=0.1)
145
+ assert len(results) > 0
146
+ # The docs->code->test pattern should appear
147
+ sequences_as_tuples = [tuple(r["sequence"]) for r in results]
148
+ assert ("docs", "code") in sequences_as_tuples or \
149
+ ("code", "test") in sequences_as_tuples or \
150
+ ("docs", "code", "test") in sequences_as_tuples
151
+
152
+ def test_min_support_filter(self, miner):
153
+ """High min_support should filter out rare 2-gram patterns.
154
+
155
+ With 4 distinct activities (docs, code, deploy, debug), each 2-gram
156
+ has support 1/3 ~ 0.33. Setting min_support=0.5 should exclude them.
157
+ Note: longer n-grams (4-gram) may have support=1.0 because there is
158
+ only 1 window of that length, so we specifically check 2-grams.
159
+ """
160
+ memories = [
161
+ {"content": "Updated documentation", "created_at": "2026-02-16 10:00:00"},
162
+ {"content": "Implemented function code", "created_at": "2026-02-16 11:00:00"},
163
+ {"content": "Deployed to production", "created_at": "2026-02-16 12:00:00"},
164
+ {"content": "Fixed the error bug", "created_at": "2026-02-16 13:00:00"},
165
+ ]
166
+ results = miner.mine_sequences(memories=memories, min_support=0.5)
167
+ # Filter results to only 2-grams (which cannot reach 0.5 support)
168
+ bigram_results = [r for r in results if r["length"] == 2]
169
+ assert len(bigram_results) == 0
170
+
171
+ def test_empty_memories(self, miner):
172
+ results = miner.mine_sequences(memories=[], min_support=0.1)
173
+ assert results == []
174
+
175
+ def test_single_memory(self, miner):
176
+ """Need at least 2 classified activities for sequence mining."""
177
+ memories = [{"content": "one code function", "created_at": "2026-02-16 10:00:00"}]
178
+ results = miner.mine_sequences(memories=memories, min_support=0.1)
179
+ assert results == []
180
+
181
+ def test_consecutive_identical_filtered(self, miner):
182
+ """N-grams with consecutive identical activities are filtered as noise."""
183
+ memories = [
184
+ {"content": "code function", "created_at": "2026-02-16 10:00:00"},
185
+ {"content": "code function", "created_at": "2026-02-16 11:00:00"},
186
+ {"content": "code function", "created_at": "2026-02-16 12:00:00"},
187
+ ]
188
+ results = miner.mine_sequences(memories=memories, min_support=0.1)
189
+ # All n-grams would be (code, code) or (code, code, code) -> filtered
190
+ assert len(results) == 0
191
+
192
+ def test_top_20_limit(self, miner):
193
+ """Results should be capped at 20."""
194
+ memories = []
195
+ types = ["docs", "code", "test", "debug", "deploy", "config"]
196
+ for i in range(100):
197
+ memories.append({
198
+ "content": f"Working on {types[i % len(types)]} implementation",
199
+ "created_at": f"2026-02-16 {i % 24:02d}:00:00",
200
+ })
201
+ results = miner.mine_sequences(memories=memories, min_support=0.01)
202
+ assert len(results) <= 20
203
+
204
+
205
+ # ---------------------------------------------------------------------------
206
+ # Temporal Pattern Mining
207
+ # ---------------------------------------------------------------------------
208
+
209
+ class TestMineTemporalPatterns:
210
+ def test_morning_coding(self, miner):
211
+ """Memories at morning hours with code content -> morning=code."""
212
+ memories = []
213
+ for hour in range(6, 12):
214
+ memories.append({
215
+ "content": "Implemented new function in the module",
216
+ "created_at": f"2026-02-16 {hour:02d}:30:00",
217
+ })
218
+ result = miner.mine_temporal_patterns(memories=memories)
219
+ assert "morning" in result
220
+ assert result["morning"]["dominant_activity"] == "code"
221
+ assert result["morning"]["evidence_count"] >= 5
222
+
223
+ def test_minimum_evidence_threshold(self, miner):
224
+ """Buckets with fewer than 5 evidence memories are omitted."""
225
+ memories = [
226
+ {"content": "Wrote code function", "created_at": "2026-02-16 09:00:00"},
227
+ {"content": "Wrote code class", "created_at": "2026-02-16 10:00:00"},
228
+ ]
229
+ result = miner.mine_temporal_patterns(memories=memories)
230
+ assert "morning" not in result # Only 2 morning memories, need 5+
231
+
232
+ def test_time_bucketing(self, miner):
233
+ """Verify that memories are assigned to the correct time bucket."""
234
+ memories = []
235
+ # 6 evening test activities (18-23)
236
+ for i in range(6):
237
+ memories.append({
238
+ "content": f"Running pytest assertions {i}",
239
+ "created_at": f"2026-02-16 {20 + (i % 4):02d}:00:00",
240
+ })
241
+ result = miner.mine_temporal_patterns(memories=memories)
242
+ if "evening" in result:
243
+ assert result["evening"]["dominant_activity"] == "test"
244
+
245
+ def test_empty_memories(self, miner):
246
+ result = miner.mine_temporal_patterns(memories=[])
247
+ assert result == {}
248
+
249
+ def test_only_unknown_activities(self, miner):
250
+ """All unknown activities should produce empty result."""
251
+ memories = [
252
+ {"content": "random chat", "created_at": f"2026-02-16 {h:02d}:00:00"}
253
+ for h in range(6, 12)
254
+ ]
255
+ result = miner.mine_temporal_patterns(memories=memories)
256
+ assert "morning" not in result
257
+
258
+
259
+ # ---------------------------------------------------------------------------
260
+ # mine_all + persistence
261
+ # ---------------------------------------------------------------------------
262
+
263
+ class TestMineAll:
264
+ def test_mine_all_persists(self, miner, learning_db):
265
+ """mine_all should store patterns in learning_db."""
266
+ memories = []
267
+ for _ in range(3):
268
+ memories.extend([
269
+ {"content": "docs documentation wiki", "created_at": "2026-02-16 09:00:00"},
270
+ {"content": "code implement function", "created_at": "2026-02-16 10:00:00"},
271
+ {"content": "test pytest assertion", "created_at": "2026-02-16 11:00:00"},
272
+ ])
273
+
274
+ results = miner.mine_all(memories=memories)
275
+ assert "sequences" in results
276
+ assert "temporal" in results
277
+
278
+ # Check that patterns were stored
279
+ stored_seq = learning_db.get_workflow_patterns(pattern_type="sequence")
280
+ stored_temp = learning_db.get_workflow_patterns(pattern_type="temporal")
281
+ total_stored = len(stored_seq) + len(stored_temp)
282
+ assert total_stored >= 0 # At least attempted storage
283
+
284
+ def test_mine_all_no_db(self, miner_no_db):
285
+ """mine_all without DB should still return results, just not persist."""
286
+ memories = [
287
+ {"content": "docs documentation", "created_at": "2026-02-16 09:00:00"},
288
+ {"content": "code function", "created_at": "2026-02-16 10:00:00"},
289
+ ]
290
+ results = miner_no_db.mine_all(memories=memories)
291
+ assert "sequences" in results
292
+ assert "temporal" in results
293
+
294
+
295
+ # ---------------------------------------------------------------------------
296
+ # Parse Hour
297
+ # ---------------------------------------------------------------------------
298
+
299
+ class TestParseHour:
300
+ def test_iso_format(self, miner):
301
+ assert miner._parse_hour("2026-02-16T09:30:00") == 9
302
+
303
+ def test_sqlite_format(self, miner):
304
+ assert miner._parse_hour("2026-02-16 14:45:00") == 14
305
+
306
+ def test_with_microseconds(self, miner):
307
+ assert miner._parse_hour("2026-02-16 23:59:59.123456") == 23
308
+
309
+ def test_date_only_returns_none(self, miner):
310
+ # fromisoformat may or may not parse date-only; if it does, hour=0
311
+ result = miner._parse_hour("2026-02-16")
312
+ # Either None or 0 is acceptable
313
+ assert result is None or result == 0
314
+
315
+ def test_none_input(self, miner):
316
+ assert miner._parse_hour(None) is None
317
+
318
+ def test_empty_string(self, miner):
319
+ assert miner._parse_hour("") is None
320
+
321
+ def test_invalid_string(self, miner):
322
+ assert miner._parse_hour("not-a-timestamp") is None