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,445 @@
1
+ // SuperLocalMemory V2.6.5 - Interactive Knowledge Graph - Core Rendering Module
2
+ // Copyright (c) 2026 Varun Pratap Bhardwaj — MIT License
3
+ // Part of modular graph visualization system (split from monolithic graph-cytoscape.js)
4
+
5
+ // ============================================================================
6
+ // GLOBAL STATE
7
+ // ============================================================================
8
+
9
+ var cy = null; // Cytoscape.js instance (global)
10
+ var graphData = { nodes: [], links: [] }; // Raw data from API
11
+ var originalGraphData = { nodes: [], links: [] }; // Unfiltered data (for reset)
12
+ var currentLayout = 'fcose'; // Default layout
13
+ var filterState = { cluster_id: null, entity: null }; // Current filters
14
+ var isInitialLoad = true; // Track if this is the first graph load
15
+ var focusedNodeIndex = 0; // Keyboard navigation: currently focused node
16
+ var keyboardNavigationEnabled = false; // Track if keyboard nav is active
17
+ var lastFocusedElement = null; // Store last focused element for modal return
18
+
19
+ // ============================================================================
20
+ // CLUSTER COLORS
21
+ // ============================================================================
22
+
23
+ const CLUSTER_COLORS = [
24
+ '#667eea', '#764ba2', '#43e97b', '#38f9d7',
25
+ '#4facfe', '#00f2fe', '#f093fb', '#f5576c',
26
+ '#fa709a', '#fee140', '#30cfd0', '#330867'
27
+ ];
28
+
29
+ function getClusterColor(cluster_id) {
30
+ if (!cluster_id) return '#999';
31
+ return CLUSTER_COLORS[cluster_id % CLUSTER_COLORS.length];
32
+ }
33
+
34
+ // ============================================================================
35
+ // HTML ESCAPE UTILITY
36
+ // ============================================================================
37
+
38
+ function escapeHtml(text) {
39
+ const div = document.createElement('div');
40
+ div.textContent = text;
41
+ return div.innerHTML;
42
+ }
43
+
44
+ // ============================================================================
45
+ // GRAPH LOADING
46
+ // ============================================================================
47
+
48
+ async function loadGraph() {
49
+ var maxNodes = document.getElementById('graph-max-nodes').value;
50
+ var minImportance = document.getElementById('graph-min-importance')?.value || 1;
51
+
52
+ // Get cluster filter from URL params ONLY on initial load
53
+ // After that, use filterState (which tab event handler controls)
54
+ if (isInitialLoad) {
55
+ const urlParams = new URLSearchParams(window.location.search);
56
+ const clusterIdParam = urlParams.get('cluster_id');
57
+ if (clusterIdParam) {
58
+ filterState.cluster_id = parseInt(clusterIdParam);
59
+ console.log('[loadGraph] Initial load with cluster filter from URL:', filterState.cluster_id);
60
+ }
61
+ isInitialLoad = false;
62
+ }
63
+
64
+ // CRITICAL FIX: When filtering by cluster, fetch MORE nodes to ensure all cluster members are included
65
+ // Otherwise only top N memories are fetched and cluster filter fails
66
+ const fetchLimit = filterState.cluster_id ? 200 : maxNodes;
67
+ console.log('[loadGraph] Fetching with limit:', fetchLimit, 'Cluster filter:', filterState.cluster_id);
68
+
69
+ try {
70
+ showLoadingSpinner();
71
+ const response = await fetch(`/api/graph?max_nodes=${fetchLimit}&min_importance=${minImportance}`);
72
+ graphData = await response.json();
73
+ originalGraphData = JSON.parse(JSON.stringify(graphData)); // Deep copy
74
+
75
+ // Apply filters if set
76
+ if (filterState.cluster_id) {
77
+ graphData = filterByCluster(originalGraphData, filterState.cluster_id);
78
+ }
79
+ if (filterState.entity) {
80
+ graphData = filterByEntity(originalGraphData, filterState.entity);
81
+ }
82
+
83
+ renderGraph(graphData);
84
+ hideLoadingSpinner();
85
+ } catch (error) {
86
+ console.error('Error loading graph:', error);
87
+ showError('Failed to load graph. Please try again.');
88
+ hideLoadingSpinner();
89
+ }
90
+ }
91
+
92
+ // ============================================================================
93
+ // DATA TRANSFORMATION
94
+ // ============================================================================
95
+
96
+ function transformDataForCytoscape(data) {
97
+ const elements = [];
98
+
99
+ // Add nodes
100
+ data.nodes.forEach(node => {
101
+ const label = node.category || node.project_name || `Memory #${node.id}`;
102
+ const contentPreview = node.content_preview || node.summary || node.content || '';
103
+ const preview = contentPreview.substring(0, 50) + (contentPreview.length > 50 ? '...' : '');
104
+
105
+ elements.push({
106
+ group: 'nodes',
107
+ data: {
108
+ id: String(node.id),
109
+ label: label,
110
+ content: node.content || '',
111
+ summary: node.summary || '',
112
+ content_preview: preview,
113
+ category: node.category || '',
114
+ project_name: node.project_name || '',
115
+ cluster_id: node.cluster_id || 0,
116
+ importance: node.importance || 5,
117
+ tags: node.tags || '',
118
+ entities: node.entities || [],
119
+ created_at: node.created_at || '',
120
+ // For rendering
121
+ weight: (node.importance || 5) * 5 // Size multiplier
122
+ }
123
+ });
124
+ });
125
+
126
+ // Add edges
127
+ data.links.forEach(link => {
128
+ const sourceId = String(typeof link.source === 'object' ? link.source.id : link.source);
129
+ const targetId = String(typeof link.target === 'object' ? link.target.id : link.target);
130
+
131
+ elements.push({
132
+ group: 'edges',
133
+ data: {
134
+ id: `${sourceId}-${targetId}`,
135
+ source: sourceId,
136
+ target: targetId,
137
+ weight: link.weight || 0.5,
138
+ relationship_type: link.relationship_type || '',
139
+ shared_entities: link.shared_entities || []
140
+ }
141
+ });
142
+ });
143
+
144
+ return elements;
145
+ }
146
+
147
+ // ============================================================================
148
+ // GRAPH RENDERING
149
+ // ============================================================================
150
+
151
+ function renderGraph(data) {
152
+ const container = document.getElementById('graph-container');
153
+ if (!container) {
154
+ console.error('Graph container not found');
155
+ return;
156
+ }
157
+
158
+ // Clear existing graph
159
+ if (cy) {
160
+ cy.destroy();
161
+ }
162
+
163
+ // Check node count for performance tier
164
+ const nodeCount = data.nodes.length;
165
+ let layout = determineBestLayout(nodeCount);
166
+
167
+ // Transform data
168
+ const elements = transformDataForCytoscape(data);
169
+
170
+ if (elements.length === 0) {
171
+ const emptyMsg = document.createElement('div');
172
+ emptyMsg.style.cssText = 'text-align:center; padding:50px; color:#666;';
173
+ emptyMsg.textContent = 'No memories found. Try adjusting filters.';
174
+ container.textContent = ''; // Clear safely
175
+ container.appendChild(emptyMsg);
176
+ return;
177
+ }
178
+
179
+ // Clear container (remove spinner/old content) - safe approach
180
+ container.textContent = '';
181
+ console.log('[renderGraph] Container cleared, initializing Cytoscape with', elements.length, 'elements');
182
+
183
+ // Initialize Cytoscape WITHOUT running layout yet
184
+ try {
185
+ cy = cytoscape({
186
+ container: container,
187
+ elements: elements,
188
+ style: getCytoscapeStyles(),
189
+ layout: { name: 'preset' }, // Don't run layout yet
190
+ minZoom: 0.2,
191
+ maxZoom: 3,
192
+ wheelSensitivity: 0.05, // Reduced from 0.2 - less sensitive zoom
193
+ autoungrabify: false,
194
+ autounselectify: false,
195
+ // Mobile touch support
196
+ touchTapThreshold: 8, // Pixels of movement allowed for tap (vs drag)
197
+ desktopTapThreshold: 4, // Desktop click threshold
198
+ pixelRatio: 'auto' // Retina/HiDPI support
199
+ });
200
+ console.log('[renderGraph] Cytoscape initialized successfully, nodes:', cy.nodes().length);
201
+ } catch (error) {
202
+ console.error('[renderGraph] Cytoscape initialization failed:', error);
203
+ showError('Failed to render graph: ' + error.message);
204
+ return;
205
+ }
206
+
207
+ // Add interactions
208
+ addCytoscapeInteractions();
209
+
210
+ // Add navigator (mini-map) if available
211
+ if (cy.navigator && nodeCount > 50) {
212
+ cy.navigator({
213
+ container: false, // Will be added to DOM separately if needed
214
+ viewLiveFramerate: 0,
215
+ dblClickDelay: 200,
216
+ removeCustomContainer: false,
217
+ rerenderDelay: 100
218
+ });
219
+ }
220
+
221
+ // Update UI
222
+ updateFilterBadge();
223
+ updateGraphStats(data);
224
+
225
+ // Announce graph load to screen readers
226
+ updateScreenReaderStatus(`Graph loaded with ${data.nodes.length} memories and ${data.links.length} connections`);
227
+
228
+ // Check if we should restore saved layout or run fresh layout
229
+ const hasSavedLayout = localStorage.getItem('slm_graph_layout');
230
+
231
+ if (hasSavedLayout) {
232
+ // Restore saved positions, then fit
233
+ restoreSavedLayout();
234
+ console.log('[renderGraph] Restored saved layout positions');
235
+ cy.fit(null, 80);
236
+ } else {
237
+ // No saved layout - run layout algorithm with fit
238
+ console.log('[renderGraph] Running fresh layout:', layout);
239
+ const layoutConfig = getLayoutConfig(layout);
240
+ const graphLayout = cy.layout(layoutConfig);
241
+
242
+ // CRITICAL: Wait for layout to finish, THEN force fit
243
+ graphLayout.on('layoutstop', function() {
244
+ console.log('[renderGraph] Layout completed, forcing fit to viewport');
245
+ cy.fit(null, 80); // Force center with 80px padding
246
+ });
247
+
248
+ graphLayout.run();
249
+ }
250
+ }
251
+
252
+ // ============================================================================
253
+ // LAYOUT ALGORITHMS
254
+ // ============================================================================
255
+
256
+ function determineBestLayout(nodeCount) {
257
+ if (nodeCount <= 500) {
258
+ return 'fcose'; // Full interactive graph
259
+ } else if (nodeCount <= 2000) {
260
+ return 'cose'; // Faster force-directed
261
+ } else {
262
+ return 'circle'; // Focus mode (circular for large graphs)
263
+ }
264
+ }
265
+
266
+ function getLayoutConfig(layoutName) {
267
+ const configs = {
268
+ 'fcose': {
269
+ name: 'fcose',
270
+ quality: 'default',
271
+ randomize: false,
272
+ animate: false, // Disabled for stability
273
+ fit: true,
274
+ padding: 80, // Increased padding to keep within bounds
275
+ nodeSeparation: 100,
276
+ idealEdgeLength: 100,
277
+ edgeElasticity: 0.45,
278
+ nestingFactor: 0.1,
279
+ gravity: 0.25,
280
+ numIter: 2500,
281
+ tile: true,
282
+ tilingPaddingVertical: 10,
283
+ tilingPaddingHorizontal: 10,
284
+ gravityRangeCompound: 1.5,
285
+ gravityCompound: 1.0,
286
+ gravityRange: 3.8
287
+ },
288
+ 'cose': {
289
+ name: 'cose',
290
+ animate: false, // Disabled for stability
291
+ fit: true,
292
+ padding: 80, // Increased padding
293
+ nodeRepulsion: 8000,
294
+ idealEdgeLength: 100,
295
+ edgeElasticity: 100,
296
+ nestingFactor: 5,
297
+ gravity: 80,
298
+ numIter: 1000,
299
+ randomize: false
300
+ },
301
+ 'circle': {
302
+ name: 'circle',
303
+ animate: true,
304
+ animationDuration: 1000,
305
+ fit: true,
306
+ padding: 50,
307
+ sort: function(a, b) {
308
+ return b.data('importance') - a.data('importance');
309
+ }
310
+ },
311
+ 'grid': {
312
+ name: 'grid',
313
+ animate: true,
314
+ animationDuration: 1000,
315
+ fit: true,
316
+ padding: 50,
317
+ sort: function(a, b) {
318
+ return b.data('importance') - a.data('importance');
319
+ }
320
+ },
321
+ 'breadthfirst': {
322
+ name: 'breadthfirst',
323
+ animate: true,
324
+ animationDuration: 1000,
325
+ fit: true,
326
+ padding: 50,
327
+ directed: false,
328
+ circle: false,
329
+ spacingFactor: 1.5,
330
+ sort: function(a, b) {
331
+ return b.data('importance') - a.data('importance');
332
+ }
333
+ },
334
+ 'concentric': {
335
+ name: 'concentric',
336
+ animate: true,
337
+ animationDuration: 1000,
338
+ fit: true,
339
+ padding: 50,
340
+ concentric: function(node) {
341
+ return node.data('importance');
342
+ },
343
+ levelWidth: function() {
344
+ return 2;
345
+ }
346
+ }
347
+ };
348
+
349
+ return configs[layoutName] || configs['fcose'];
350
+ }
351
+
352
+ // ============================================================================
353
+ // CYTOSCAPE STYLES
354
+ // ============================================================================
355
+
356
+ function getCytoscapeStyles() {
357
+ return [
358
+ {
359
+ selector: 'node',
360
+ style: {
361
+ 'background-color': function(ele) {
362
+ return getClusterColor(ele.data('cluster_id'));
363
+ },
364
+ 'width': function(ele) {
365
+ return Math.max(20, ele.data('weight'));
366
+ },
367
+ 'height': function(ele) {
368
+ return Math.max(20, ele.data('weight'));
369
+ },
370
+ 'label': 'data(label)',
371
+ 'font-size': '10px',
372
+ 'text-valign': 'center',
373
+ 'text-halign': 'center',
374
+ 'color': '#333',
375
+ 'text-outline-width': 2,
376
+ 'text-outline-color': '#fff',
377
+ 'border-width': function(ele) {
378
+ // Trust score → border thickness (if available)
379
+ return 2;
380
+ },
381
+ 'border-color': '#555'
382
+ }
383
+ },
384
+ {
385
+ selector: 'node:selected',
386
+ style: {
387
+ 'border-width': 4,
388
+ 'border-color': '#667eea',
389
+ 'background-color': '#667eea'
390
+ }
391
+ },
392
+ {
393
+ selector: 'node.highlighted',
394
+ style: {
395
+ 'border-width': 4,
396
+ 'border-color': '#ff6b6b',
397
+ 'box-shadow': '0 0 20px #ff6b6b'
398
+ }
399
+ },
400
+ {
401
+ selector: 'node.dimmed',
402
+ style: {
403
+ 'opacity': 0.3
404
+ }
405
+ },
406
+ {
407
+ selector: 'node.keyboard-focused',
408
+ style: {
409
+ 'border-width': 5,
410
+ 'border-color': '#0066ff',
411
+ 'border-style': 'solid',
412
+ 'box-shadow': '0 0 15px #0066ff'
413
+ }
414
+ },
415
+ {
416
+ selector: 'edge',
417
+ style: {
418
+ 'width': function(ele) {
419
+ return Math.max(1, ele.data('weight') * 3);
420
+ },
421
+ 'line-color': '#ccc',
422
+ 'line-style': function(ele) {
423
+ return ele.data('weight') > 0.3 ? 'solid' : 'dashed';
424
+ },
425
+ 'curve-style': 'bezier',
426
+ 'target-arrow-shape': 'none',
427
+ 'opacity': 0.6
428
+ }
429
+ },
430
+ {
431
+ selector: 'edge.highlighted',
432
+ style: {
433
+ 'line-color': '#667eea',
434
+ 'width': 3,
435
+ 'opacity': 1
436
+ }
437
+ },
438
+ {
439
+ selector: 'edge.dimmed',
440
+ style: {
441
+ 'opacity': 0.1
442
+ }
443
+ }
444
+ ];
445
+ }