moflo 4.0.2 → 4.0.4

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 (90) hide show
  1. package/package.json +114 -110
  2. package/v3/@claude-flow/cli/dist/src/commands/hooks.js +4 -1
  3. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +61 -5
  4. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +1892 -1841
  5. package/v3/@claude-flow/memory/README.md +587 -0
  6. package/v3/@claude-flow/memory/dist/agent-memory-scope.d.ts +131 -0
  7. package/v3/@claude-flow/memory/dist/agent-memory-scope.js +223 -0
  8. package/v3/@claude-flow/memory/dist/agent-memory-scope.test.d.ts +8 -0
  9. package/v3/@claude-flow/memory/dist/agent-memory-scope.test.js +463 -0
  10. package/v3/@claude-flow/memory/dist/agentdb-adapter.d.ts +165 -0
  11. package/v3/@claude-flow/memory/dist/agentdb-adapter.js +806 -0
  12. package/v3/@claude-flow/memory/dist/agentdb-backend.d.ts +214 -0
  13. package/v3/@claude-flow/memory/dist/agentdb-backend.js +844 -0
  14. package/v3/@claude-flow/memory/dist/agentdb-backend.test.d.ts +7 -0
  15. package/v3/@claude-flow/memory/dist/agentdb-backend.test.js +258 -0
  16. package/v3/@claude-flow/memory/dist/application/commands/delete-memory.command.d.ts +65 -0
  17. package/v3/@claude-flow/memory/dist/application/commands/delete-memory.command.js +129 -0
  18. package/v3/@claude-flow/memory/dist/application/commands/store-memory.command.d.ts +48 -0
  19. package/v3/@claude-flow/memory/dist/application/commands/store-memory.command.js +72 -0
  20. package/v3/@claude-flow/memory/dist/application/index.d.ts +12 -0
  21. package/v3/@claude-flow/memory/dist/application/index.js +15 -0
  22. package/v3/@claude-flow/memory/dist/application/queries/search-memory.query.d.ts +72 -0
  23. package/v3/@claude-flow/memory/dist/application/queries/search-memory.query.js +143 -0
  24. package/v3/@claude-flow/memory/dist/application/services/memory-application-service.d.ts +121 -0
  25. package/v3/@claude-flow/memory/dist/application/services/memory-application-service.js +190 -0
  26. package/v3/@claude-flow/memory/dist/auto-memory-bridge.d.ts +226 -0
  27. package/v3/@claude-flow/memory/dist/auto-memory-bridge.js +709 -0
  28. package/v3/@claude-flow/memory/dist/auto-memory-bridge.test.d.ts +8 -0
  29. package/v3/@claude-flow/memory/dist/auto-memory-bridge.test.js +754 -0
  30. package/v3/@claude-flow/memory/dist/benchmark.test.d.ts +2 -0
  31. package/v3/@claude-flow/memory/dist/benchmark.test.js +277 -0
  32. package/v3/@claude-flow/memory/dist/cache-manager.d.ts +134 -0
  33. package/v3/@claude-flow/memory/dist/cache-manager.js +407 -0
  34. package/v3/@claude-flow/memory/dist/controller-registry.d.ts +216 -0
  35. package/v3/@claude-flow/memory/dist/controller-registry.js +893 -0
  36. package/v3/@claude-flow/memory/dist/controller-registry.test.d.ts +14 -0
  37. package/v3/@claude-flow/memory/dist/controller-registry.test.js +636 -0
  38. package/v3/@claude-flow/memory/dist/database-provider.d.ts +87 -0
  39. package/v3/@claude-flow/memory/dist/database-provider.js +375 -0
  40. package/v3/@claude-flow/memory/dist/database-provider.test.d.ts +7 -0
  41. package/v3/@claude-flow/memory/dist/database-provider.test.js +285 -0
  42. package/v3/@claude-flow/memory/dist/domain/entities/memory-entry.d.ts +143 -0
  43. package/v3/@claude-flow/memory/dist/domain/entities/memory-entry.js +226 -0
  44. package/v3/@claude-flow/memory/dist/domain/index.d.ts +11 -0
  45. package/v3/@claude-flow/memory/dist/domain/index.js +12 -0
  46. package/v3/@claude-flow/memory/dist/domain/repositories/memory-repository.interface.d.ts +102 -0
  47. package/v3/@claude-flow/memory/dist/domain/repositories/memory-repository.interface.js +11 -0
  48. package/v3/@claude-flow/memory/dist/domain/services/memory-domain-service.d.ts +105 -0
  49. package/v3/@claude-flow/memory/dist/domain/services/memory-domain-service.js +297 -0
  50. package/v3/@claude-flow/memory/dist/hnsw-index.d.ts +111 -0
  51. package/v3/@claude-flow/memory/dist/hnsw-index.js +781 -0
  52. package/v3/@claude-flow/memory/dist/hnsw-lite.d.ts +23 -0
  53. package/v3/@claude-flow/memory/dist/hnsw-lite.js +168 -0
  54. package/v3/@claude-flow/memory/dist/hybrid-backend.d.ts +245 -0
  55. package/v3/@claude-flow/memory/dist/hybrid-backend.js +569 -0
  56. package/v3/@claude-flow/memory/dist/hybrid-backend.test.d.ts +8 -0
  57. package/v3/@claude-flow/memory/dist/hybrid-backend.test.js +320 -0
  58. package/v3/@claude-flow/memory/dist/index.d.ts +207 -0
  59. package/v3/@claude-flow/memory/dist/index.js +361 -0
  60. package/v3/@claude-flow/memory/dist/infrastructure/index.d.ts +17 -0
  61. package/v3/@claude-flow/memory/dist/infrastructure/index.js +16 -0
  62. package/v3/@claude-flow/memory/dist/infrastructure/repositories/hybrid-memory-repository.d.ts +66 -0
  63. package/v3/@claude-flow/memory/dist/infrastructure/repositories/hybrid-memory-repository.js +409 -0
  64. package/v3/@claude-flow/memory/dist/learning-bridge.d.ts +137 -0
  65. package/v3/@claude-flow/memory/dist/learning-bridge.js +335 -0
  66. package/v3/@claude-flow/memory/dist/learning-bridge.test.d.ts +8 -0
  67. package/v3/@claude-flow/memory/dist/learning-bridge.test.js +578 -0
  68. package/v3/@claude-flow/memory/dist/memory-graph.d.ts +100 -0
  69. package/v3/@claude-flow/memory/dist/memory-graph.js +333 -0
  70. package/v3/@claude-flow/memory/dist/memory-graph.test.d.ts +8 -0
  71. package/v3/@claude-flow/memory/dist/memory-graph.test.js +609 -0
  72. package/v3/@claude-flow/memory/dist/migration.d.ts +68 -0
  73. package/v3/@claude-flow/memory/dist/migration.js +513 -0
  74. package/v3/@claude-flow/memory/dist/persistent-sona.d.ts +144 -0
  75. package/v3/@claude-flow/memory/dist/persistent-sona.js +332 -0
  76. package/v3/@claude-flow/memory/dist/query-builder.d.ts +211 -0
  77. package/v3/@claude-flow/memory/dist/query-builder.js +438 -0
  78. package/v3/@claude-flow/memory/dist/rvf-backend.d.ts +51 -0
  79. package/v3/@claude-flow/memory/dist/rvf-backend.js +481 -0
  80. package/v3/@claude-flow/memory/dist/rvf-learning-store.d.ts +139 -0
  81. package/v3/@claude-flow/memory/dist/rvf-learning-store.js +295 -0
  82. package/v3/@claude-flow/memory/dist/rvf-migration.d.ts +45 -0
  83. package/v3/@claude-flow/memory/dist/rvf-migration.js +234 -0
  84. package/v3/@claude-flow/memory/dist/sqlite-backend.d.ts +121 -0
  85. package/v3/@claude-flow/memory/dist/sqlite-backend.js +572 -0
  86. package/v3/@claude-flow/memory/dist/sqljs-backend.d.ts +128 -0
  87. package/v3/@claude-flow/memory/dist/sqljs-backend.js +601 -0
  88. package/v3/@claude-flow/memory/dist/types.d.ts +484 -0
  89. package/v3/@claude-flow/memory/dist/types.js +58 -0
  90. package/v3/@claude-flow/memory/package.json +42 -0
@@ -0,0 +1,578 @@
1
+ /**
2
+ * Tests for LearningBridge
3
+ *
4
+ * TDD London School (mock-first) tests for the bridge that connects
5
+ * AutoMemoryBridge insights to the NeuralLearningSystem.
6
+ */
7
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
8
+ import { LearningBridge } from './learning-bridge.js';
9
+ // ===== Mock Neural System =====
10
+ function createMockNeuralSystem() {
11
+ return {
12
+ initialize: vi.fn().mockResolvedValue(undefined),
13
+ beginTask: vi.fn().mockReturnValue('traj-1'),
14
+ recordStep: vi.fn(),
15
+ completeTask: vi.fn().mockResolvedValue(undefined),
16
+ findPatterns: vi.fn().mockResolvedValue([]),
17
+ cleanup: vi.fn().mockResolvedValue(undefined),
18
+ };
19
+ }
20
+ function createNeuralLoader(neural) {
21
+ return async () => neural;
22
+ }
23
+ function createFailingNeuralLoader() {
24
+ return async () => { throw new Error('Module not found'); };
25
+ }
26
+ // ===== Mock Backend =====
27
+ function createMockBackend() {
28
+ const storedEntries = [];
29
+ return {
30
+ storedEntries,
31
+ initialize: vi.fn().mockResolvedValue(undefined),
32
+ shutdown: vi.fn().mockResolvedValue(undefined),
33
+ store: vi.fn().mockImplementation(async (entry) => {
34
+ storedEntries.push(entry);
35
+ }),
36
+ get: vi.fn().mockResolvedValue(null),
37
+ getByKey: vi.fn().mockResolvedValue(null),
38
+ update: vi.fn().mockImplementation(async (id, upd) => {
39
+ const entry = storedEntries.find(e => e.id === id);
40
+ if (!entry)
41
+ return null;
42
+ if (upd.metadata)
43
+ entry.metadata = { ...entry.metadata, ...upd.metadata };
44
+ return entry;
45
+ }),
46
+ delete: vi.fn().mockResolvedValue(true),
47
+ query: vi.fn().mockResolvedValue([]),
48
+ search: vi.fn().mockResolvedValue([]),
49
+ bulkInsert: vi.fn().mockResolvedValue(undefined),
50
+ bulkDelete: vi.fn().mockResolvedValue(0),
51
+ count: vi.fn().mockResolvedValue(0),
52
+ listNamespaces: vi.fn().mockResolvedValue([]),
53
+ clearNamespace: vi.fn().mockResolvedValue(0),
54
+ getStats: vi.fn().mockResolvedValue({
55
+ totalEntries: 0, entriesByNamespace: {}, entriesByType: {},
56
+ memoryUsage: 0, avgQueryTime: 0, avgSearchTime: 0,
57
+ }),
58
+ healthCheck: vi.fn().mockResolvedValue({
59
+ status: 'healthy',
60
+ components: {
61
+ storage: { status: 'healthy', latency: 0 },
62
+ index: { status: 'healthy', latency: 0 },
63
+ cache: { status: 'healthy', latency: 0 },
64
+ },
65
+ timestamp: Date.now(), issues: [], recommendations: [],
66
+ }),
67
+ };
68
+ }
69
+ // ===== Test Fixtures =====
70
+ function createTestInsight(overrides = {}) {
71
+ return {
72
+ category: 'debugging',
73
+ summary: 'HNSW index requires initialization before search',
74
+ source: 'agent:tester',
75
+ confidence: 0.95,
76
+ ...overrides,
77
+ };
78
+ }
79
+ function createTestEntry(overrides = {}) {
80
+ const now = Date.now();
81
+ return {
82
+ id: 'entry-1',
83
+ key: 'insight:debugging:12345:0',
84
+ content: 'HNSW index requires initialization',
85
+ type: 'semantic',
86
+ namespace: 'learnings',
87
+ tags: ['insight', 'debugging'],
88
+ metadata: { confidence: 0.8, category: 'debugging' },
89
+ accessLevel: 'private',
90
+ createdAt: now,
91
+ updatedAt: now,
92
+ version: 1,
93
+ references: [],
94
+ accessCount: 0,
95
+ lastAccessedAt: now,
96
+ ...overrides,
97
+ };
98
+ }
99
+ // ===== Tests =====
100
+ describe('LearningBridge', () => {
101
+ let bridge;
102
+ let backend;
103
+ let neural;
104
+ beforeEach(() => {
105
+ backend = createMockBackend();
106
+ neural = createMockNeuralSystem();
107
+ bridge = new LearningBridge(backend, { neuralLoader: createNeuralLoader(neural) });
108
+ });
109
+ afterEach(() => {
110
+ bridge.destroy();
111
+ });
112
+ // ===== constructor =====
113
+ describe('constructor', () => {
114
+ it('should create with default config', () => {
115
+ const b = new LearningBridge(backend);
116
+ const stats = b.getStats();
117
+ expect(stats.totalTrajectories).toBe(0);
118
+ expect(stats.neuralAvailable).toBe(false);
119
+ b.destroy();
120
+ });
121
+ it('should create with custom config', () => {
122
+ const custom = new LearningBridge(backend, {
123
+ sonaMode: 'research',
124
+ confidenceDecayRate: 0.01,
125
+ accessBoostAmount: 0.05,
126
+ maxConfidence: 0.9,
127
+ minConfidence: 0.2,
128
+ ewcLambda: 5000,
129
+ consolidationThreshold: 20,
130
+ });
131
+ expect(custom.getStats().totalTrajectories).toBe(0);
132
+ custom.destroy();
133
+ });
134
+ it('should respect enabled=false', async () => {
135
+ const disabled = new LearningBridge(backend, {
136
+ enabled: false,
137
+ neuralLoader: createNeuralLoader(neural),
138
+ });
139
+ await disabled.onInsightRecorded(createTestInsight(), 'entry-1');
140
+ expect(neural.beginTask).not.toHaveBeenCalled();
141
+ disabled.destroy();
142
+ });
143
+ });
144
+ // ===== onInsightRecorded =====
145
+ describe('onInsightRecorded', () => {
146
+ it('should store trajectory when neural available', async () => {
147
+ const insight = createTestInsight();
148
+ await bridge.onInsightRecorded(insight, 'entry-1');
149
+ expect(neural.beginTask).toHaveBeenCalledWith(insight.summary, 'general');
150
+ expect(neural.recordStep).toHaveBeenCalledWith('traj-1', expect.objectContaining({
151
+ action: 'record:debugging',
152
+ reward: 0.95,
153
+ }));
154
+ expect(bridge.getStats().totalTrajectories).toBe(1);
155
+ });
156
+ it('should emit insight:learning-started event', async () => {
157
+ const handler = vi.fn();
158
+ bridge.on('insight:learning-started', handler);
159
+ await bridge.onInsightRecorded(createTestInsight(), 'entry-1');
160
+ expect(handler).toHaveBeenCalledWith({ entryId: 'entry-1', category: 'debugging' });
161
+ });
162
+ it('should no-op when disabled', async () => {
163
+ const disabled = new LearningBridge(backend, {
164
+ enabled: false,
165
+ neuralLoader: createNeuralLoader(neural),
166
+ });
167
+ await disabled.onInsightRecorded(createTestInsight(), 'entry-1');
168
+ expect(neural.beginTask).not.toHaveBeenCalled();
169
+ disabled.destroy();
170
+ });
171
+ it('should no-op when destroyed', async () => {
172
+ bridge.destroy();
173
+ await bridge.onInsightRecorded(createTestInsight(), 'entry-1');
174
+ expect(neural.beginTask).not.toHaveBeenCalled();
175
+ });
176
+ it('should handle neural unavailable gracefully', async () => {
177
+ const safeBridge = new LearningBridge(backend, {
178
+ neuralLoader: createFailingNeuralLoader(),
179
+ });
180
+ const handler = vi.fn();
181
+ safeBridge.on('insight:learning-started', handler);
182
+ await safeBridge.onInsightRecorded(createTestInsight(), 'entry-1');
183
+ expect(handler).toHaveBeenCalled();
184
+ expect(safeBridge.getStats().neuralAvailable).toBe(false);
185
+ safeBridge.destroy();
186
+ });
187
+ it('should pass hash embedding as stateEmbedding', async () => {
188
+ await bridge.onInsightRecorded(createTestInsight(), 'entry-1');
189
+ const stepArg = neural.recordStep.mock.calls[0][1];
190
+ expect(stepArg.stateEmbedding).toBeInstanceOf(Float32Array);
191
+ expect(stepArg.stateEmbedding.length).toBe(768);
192
+ });
193
+ it('should create unique trajectory per entry', async () => {
194
+ neural.beginTask.mockReturnValueOnce('traj-1').mockReturnValueOnce('traj-2');
195
+ await bridge.onInsightRecorded(createTestInsight(), 'entry-1');
196
+ await bridge.onInsightRecorded(createTestInsight({ summary: 'Second insight' }), 'entry-2');
197
+ expect(bridge.getStats().totalTrajectories).toBe(2);
198
+ expect(bridge.getStats().activeTrajectories).toBe(2);
199
+ });
200
+ it('should survive beginTask throwing', async () => {
201
+ neural.beginTask.mockImplementationOnce(() => { throw new Error('fail'); });
202
+ const handler = vi.fn();
203
+ bridge.on('insight:learning-started', handler);
204
+ await bridge.onInsightRecorded(createTestInsight(), 'entry-1');
205
+ expect(handler).toHaveBeenCalled();
206
+ });
207
+ });
208
+ // ===== onInsightAccessed =====
209
+ describe('onInsightAccessed', () => {
210
+ it('should boost confidence by accessBoostAmount', async () => {
211
+ const entry = createTestEntry({ metadata: { confidence: 0.5 } });
212
+ backend.get.mockResolvedValueOnce(entry);
213
+ await bridge.onInsightAccessed('entry-1');
214
+ expect(backend.update).toHaveBeenCalledWith('entry-1', {
215
+ metadata: expect.objectContaining({ confidence: 0.53 }),
216
+ });
217
+ });
218
+ it('should cap confidence at maxConfidence', async () => {
219
+ const entry = createTestEntry({ metadata: { confidence: 0.99 } });
220
+ backend.get.mockResolvedValueOnce(entry);
221
+ await bridge.onInsightAccessed('entry-1');
222
+ const updateCall = backend.update.mock.calls[0];
223
+ expect(updateCall[1].metadata.confidence).toBeLessThanOrEqual(1.0);
224
+ });
225
+ it('should handle missing entry gracefully', async () => {
226
+ backend.get.mockResolvedValueOnce(null);
227
+ await bridge.onInsightAccessed('nonexistent');
228
+ expect(backend.update).not.toHaveBeenCalled();
229
+ });
230
+ it('should emit insight:accessed event', async () => {
231
+ const entry = createTestEntry({ metadata: { confidence: 0.7 } });
232
+ backend.get.mockResolvedValueOnce(entry);
233
+ const handler = vi.fn();
234
+ bridge.on('insight:accessed', handler);
235
+ await bridge.onInsightAccessed('entry-1');
236
+ expect(handler).toHaveBeenCalledWith({
237
+ entryId: 'entry-1',
238
+ newConfidence: expect.any(Number),
239
+ });
240
+ });
241
+ it('should update entry metadata in backend preserving existing fields', async () => {
242
+ const entry = createTestEntry({
243
+ metadata: { confidence: 0.6, category: 'debugging', extra: 'preserved' },
244
+ });
245
+ backend.get.mockResolvedValueOnce(entry);
246
+ await bridge.onInsightAccessed('entry-1');
247
+ const updateCall = backend.update.mock.calls[0][1];
248
+ expect(updateCall.metadata.category).toBe('debugging');
249
+ expect(updateCall.metadata.extra).toBe('preserved');
250
+ expect(updateCall.metadata.confidence).toBeCloseTo(0.63, 5);
251
+ });
252
+ it('should record neural step when trajectory exists', async () => {
253
+ await bridge.onInsightRecorded(createTestInsight(), 'entry-1');
254
+ vi.clearAllMocks();
255
+ const entry = createTestEntry();
256
+ backend.get.mockResolvedValueOnce(entry);
257
+ await bridge.onInsightAccessed('entry-1');
258
+ expect(neural.recordStep).toHaveBeenCalledWith('traj-1', {
259
+ action: 'access',
260
+ reward: 0.03,
261
+ });
262
+ });
263
+ it('should not record neural step without trajectory', async () => {
264
+ const entry = createTestEntry();
265
+ backend.get.mockResolvedValueOnce(entry);
266
+ await bridge.onInsightAccessed('entry-1');
267
+ expect(neural.recordStep).not.toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ action: 'access' }));
268
+ });
269
+ it('should use default 0.5 when metadata lacks confidence', async () => {
270
+ const entry = createTestEntry({ metadata: {} });
271
+ backend.get.mockResolvedValueOnce(entry);
272
+ await bridge.onInsightAccessed('entry-1');
273
+ const updateCall = backend.update.mock.calls[0][1];
274
+ expect(updateCall.metadata.confidence).toBeCloseTo(0.53, 5);
275
+ });
276
+ it('should no-op when disabled', async () => {
277
+ const disabled = new LearningBridge(backend, { enabled: false });
278
+ await disabled.onInsightAccessed('entry-1');
279
+ expect(backend.get).not.toHaveBeenCalled();
280
+ disabled.destroy();
281
+ });
282
+ it('should track boost stats', async () => {
283
+ const entry = createTestEntry({ metadata: { confidence: 0.5 } });
284
+ backend.get.mockResolvedValue(entry);
285
+ await bridge.onInsightAccessed('entry-1');
286
+ await bridge.onInsightAccessed('entry-1');
287
+ expect(bridge.getStats().avgConfidenceBoost).toBeCloseTo(0.03, 5);
288
+ });
289
+ });
290
+ // ===== consolidate =====
291
+ describe('consolidate', () => {
292
+ async function seedTrajectories(count) {
293
+ for (let i = 0; i < count; i++) {
294
+ neural.beginTask.mockReturnValueOnce(`traj-${i}`);
295
+ await bridge.onInsightRecorded(createTestInsight({ summary: `Insight ${i}` }), `entry-${i}`);
296
+ }
297
+ }
298
+ it('should complete active trajectories', async () => {
299
+ await seedTrajectories(10);
300
+ const result = await bridge.consolidate();
301
+ expect(result.trajectoriesCompleted).toBe(10);
302
+ expect(result.patternsLearned).toBe(10);
303
+ expect(result.durationMs).toBeGreaterThanOrEqual(0);
304
+ });
305
+ it('should return early when below threshold', async () => {
306
+ await seedTrajectories(2);
307
+ const result = await bridge.consolidate();
308
+ expect(result.trajectoriesCompleted).toBe(0);
309
+ expect(neural.completeTask).not.toHaveBeenCalled();
310
+ });
311
+ it('should return early when neural unavailable', async () => {
312
+ const safeBridge = new LearningBridge(backend, {
313
+ neuralLoader: createFailingNeuralLoader(),
314
+ });
315
+ const result = await safeBridge.consolidate();
316
+ expect(result.trajectoriesCompleted).toBe(0);
317
+ safeBridge.destroy();
318
+ });
319
+ it('should clear completed trajectories', async () => {
320
+ await seedTrajectories(10);
321
+ expect(bridge.getStats().activeTrajectories).toBe(10);
322
+ await bridge.consolidate();
323
+ expect(bridge.getStats().activeTrajectories).toBe(0);
324
+ });
325
+ it('should emit consolidation:completed event', async () => {
326
+ await seedTrajectories(10);
327
+ const handler = vi.fn();
328
+ bridge.on('consolidation:completed', handler);
329
+ await bridge.consolidate();
330
+ expect(handler).toHaveBeenCalledWith(expect.objectContaining({
331
+ trajectoriesCompleted: 10,
332
+ patternsLearned: 10,
333
+ }));
334
+ });
335
+ it('should track stats correctly', async () => {
336
+ await seedTrajectories(10);
337
+ await bridge.consolidate();
338
+ const stats = bridge.getStats();
339
+ expect(stats.completedTrajectories).toBe(10);
340
+ expect(stats.totalConsolidations).toBe(1);
341
+ });
342
+ it('should handle completeTask failure for individual trajectories', async () => {
343
+ await seedTrajectories(10);
344
+ let callCount = 0;
345
+ neural.completeTask.mockImplementation(async () => {
346
+ callCount++;
347
+ if (callCount === 3)
348
+ throw new Error('Neural failure');
349
+ });
350
+ const result = await bridge.consolidate();
351
+ expect(result.trajectoriesCompleted).toBe(9);
352
+ });
353
+ it('should respect custom consolidationThreshold', async () => {
354
+ const customBridge = new LearningBridge(backend, {
355
+ consolidationThreshold: 2,
356
+ neuralLoader: createNeuralLoader(neural),
357
+ });
358
+ neural.beginTask.mockReturnValueOnce('traj-1').mockReturnValueOnce('traj-2');
359
+ await customBridge.onInsightRecorded(createTestInsight(), 'e-1');
360
+ await customBridge.onInsightRecorded(createTestInsight({ summary: 'S2' }), 'e-2');
361
+ const result = await customBridge.consolidate();
362
+ expect(result.trajectoriesCompleted).toBe(2);
363
+ customBridge.destroy();
364
+ });
365
+ });
366
+ // ===== decayConfidences =====
367
+ describe('decayConfidences', () => {
368
+ it('should decay entries older than 1 hour', async () => {
369
+ const twoHoursAgo = Date.now() - 2 * 3_600_000;
370
+ const entry = createTestEntry({
371
+ id: 'old-entry', updatedAt: twoHoursAgo, metadata: { confidence: 0.9 },
372
+ });
373
+ backend.query.mockResolvedValueOnce([entry]);
374
+ const count = await bridge.decayConfidences('learnings');
375
+ expect(count).toBe(1);
376
+ const newConf = backend.update.mock.calls[0][1].metadata.confidence;
377
+ expect(newConf).toBeCloseTo(0.89, 2);
378
+ });
379
+ it('should respect minConfidence floor', async () => {
380
+ const longAgo = Date.now() - 200 * 3_600_000;
381
+ const entry = createTestEntry({
382
+ id: 'ancient', updatedAt: longAgo, metadata: { confidence: 0.5 },
383
+ });
384
+ backend.query.mockResolvedValueOnce([entry]);
385
+ await bridge.decayConfidences('learnings');
386
+ const newConf = backend.update.mock.calls[0][1].metadata.confidence;
387
+ expect(newConf).toBeGreaterThanOrEqual(0.1);
388
+ });
389
+ it('should skip recent entries', async () => {
390
+ const entry = createTestEntry({
391
+ id: 'recent', updatedAt: Date.now() - 30 * 60_000, metadata: { confidence: 0.8 },
392
+ });
393
+ backend.query.mockResolvedValueOnce([entry]);
394
+ const count = await bridge.decayConfidences('learnings');
395
+ expect(count).toBe(0);
396
+ expect(backend.update).not.toHaveBeenCalled();
397
+ });
398
+ it('should return count of decayed entries', async () => {
399
+ const old = Date.now() - 2 * 3_600_000;
400
+ const entries = [
401
+ createTestEntry({ id: 'e1', updatedAt: old, metadata: { confidence: 0.9 } }),
402
+ createTestEntry({ id: 'e2', updatedAt: old, metadata: { confidence: 0.7 } }),
403
+ createTestEntry({ id: 'e3', updatedAt: Date.now(), metadata: { confidence: 0.5 } }),
404
+ ];
405
+ backend.query.mockResolvedValueOnce(entries);
406
+ const count = await bridge.decayConfidences('learnings');
407
+ expect(count).toBe(2);
408
+ });
409
+ it('should handle empty namespace', async () => {
410
+ backend.query.mockResolvedValueOnce([]);
411
+ const count = await bridge.decayConfidences('empty-ns');
412
+ expect(count).toBe(0);
413
+ });
414
+ it('should handle query failure gracefully', async () => {
415
+ backend.query.mockRejectedValueOnce(new Error('DB error'));
416
+ const count = await bridge.decayConfidences('broken');
417
+ expect(count).toBe(0);
418
+ });
419
+ it('should track total decays in stats', async () => {
420
+ const old = Date.now() - 5 * 3_600_000;
421
+ backend.query.mockResolvedValueOnce([
422
+ createTestEntry({ id: 'e1', updatedAt: old, metadata: { confidence: 0.9 } }),
423
+ ]);
424
+ await bridge.decayConfidences('learnings');
425
+ expect(bridge.getStats().totalDecays).toBe(1);
426
+ });
427
+ });
428
+ // ===== findSimilarPatterns =====
429
+ describe('findSimilarPatterns', () => {
430
+ it('should return patterns when neural available', async () => {
431
+ neural.findPatterns.mockResolvedValueOnce([
432
+ { content: 'Pattern A', similarity: 0.9, category: 'debugging', confidence: 0.8 },
433
+ { content: 'Pattern B', similarity: 0.7, category: 'performance', confidence: 0.6 },
434
+ ]);
435
+ const patterns = await bridge.findSimilarPatterns('test query');
436
+ expect(patterns).toHaveLength(2);
437
+ expect(patterns[0].content).toBe('Pattern A');
438
+ expect(patterns[0].similarity).toBe(0.9);
439
+ expect(patterns[1].category).toBe('performance');
440
+ });
441
+ it('should return empty when neural unavailable', async () => {
442
+ const safeBridge = new LearningBridge(backend, {
443
+ neuralLoader: createFailingNeuralLoader(),
444
+ });
445
+ const patterns = await safeBridge.findSimilarPatterns('test');
446
+ expect(patterns).toHaveLength(0);
447
+ safeBridge.destroy();
448
+ });
449
+ it('should map results to PatternMatch format', async () => {
450
+ neural.findPatterns.mockResolvedValueOnce([
451
+ { data: 'Raw data', score: 0.85, reward: 0.7 },
452
+ ]);
453
+ const patterns = await bridge.findSimilarPatterns('test');
454
+ expect(patterns).toHaveLength(1);
455
+ expect(patterns[0].content).toBe('Raw data');
456
+ expect(patterns[0].similarity).toBe(0.85);
457
+ expect(patterns[0].confidence).toBe(0.7);
458
+ expect(patterns[0].category).toBe('unknown');
459
+ });
460
+ it('should pass k parameter to neural', async () => {
461
+ await bridge.findSimilarPatterns('test', 3);
462
+ expect(neural.findPatterns).toHaveBeenCalledWith(expect.any(Float32Array), 3);
463
+ });
464
+ it('should handle findPatterns throwing', async () => {
465
+ neural.findPatterns.mockRejectedValueOnce(new Error('Neural error'));
466
+ const patterns = await bridge.findSimilarPatterns('test');
467
+ expect(patterns).toHaveLength(0);
468
+ });
469
+ it('should handle non-array result from findPatterns', async () => {
470
+ neural.findPatterns.mockResolvedValueOnce(null);
471
+ const patterns = await bridge.findSimilarPatterns('test');
472
+ expect(patterns).toHaveLength(0);
473
+ });
474
+ });
475
+ // ===== getStats =====
476
+ describe('getStats', () => {
477
+ it('should return correct initial stats', () => {
478
+ const stats = bridge.getStats();
479
+ expect(stats.totalTrajectories).toBe(0);
480
+ expect(stats.completedTrajectories).toBe(0);
481
+ expect(stats.activeTrajectories).toBe(0);
482
+ expect(stats.totalConsolidations).toBe(0);
483
+ expect(stats.totalDecays).toBe(0);
484
+ expect(stats.avgConfidenceBoost).toBe(0);
485
+ });
486
+ it('should reflect operations in stats', async () => {
487
+ await bridge.onInsightRecorded(createTestInsight(), 'entry-1');
488
+ let stats = bridge.getStats();
489
+ expect(stats.totalTrajectories).toBe(1);
490
+ expect(stats.activeTrajectories).toBe(1);
491
+ expect(stats.neuralAvailable).toBe(true);
492
+ const entry = createTestEntry({ metadata: { confidence: 0.5 } });
493
+ backend.get.mockResolvedValueOnce(entry);
494
+ await bridge.onInsightAccessed('entry-1');
495
+ stats = bridge.getStats();
496
+ expect(stats.avgConfidenceBoost).toBeCloseTo(0.03, 5);
497
+ });
498
+ it('should show neuralAvailable=false before init', () => {
499
+ const fresh = new LearningBridge(backend);
500
+ expect(fresh.getStats().neuralAvailable).toBe(false);
501
+ fresh.destroy();
502
+ });
503
+ });
504
+ // ===== destroy =====
505
+ describe('destroy', () => {
506
+ it('should set destroyed state', async () => {
507
+ bridge.destroy();
508
+ await bridge.onInsightRecorded(createTestInsight(), 'entry-1');
509
+ expect(neural.beginTask).not.toHaveBeenCalled();
510
+ });
511
+ it('should clear trajectories', async () => {
512
+ await bridge.onInsightRecorded(createTestInsight(), 'entry-1');
513
+ expect(bridge.getStats().activeTrajectories).toBe(1);
514
+ bridge.destroy();
515
+ expect(bridge.getStats().activeTrajectories).toBe(0);
516
+ });
517
+ it('should make subsequent onInsightRecorded no-op', async () => {
518
+ bridge.destroy();
519
+ await bridge.onInsightRecorded(createTestInsight(), 'entry-1');
520
+ expect(bridge.getStats().totalTrajectories).toBe(0);
521
+ });
522
+ it('should make subsequent onInsightAccessed no-op', async () => {
523
+ bridge.destroy();
524
+ await bridge.onInsightAccessed('entry-1');
525
+ expect(backend.get).not.toHaveBeenCalled();
526
+ });
527
+ it('should make subsequent consolidate no-op', async () => {
528
+ bridge.destroy();
529
+ const result = await bridge.consolidate();
530
+ expect(result.trajectoriesCompleted).toBe(0);
531
+ });
532
+ it('should make subsequent decayConfidences no-op', async () => {
533
+ bridge.destroy();
534
+ const count = await bridge.decayConfidences('learnings');
535
+ expect(count).toBe(0);
536
+ });
537
+ it('should make subsequent findSimilarPatterns no-op', async () => {
538
+ bridge.destroy();
539
+ const patterns = await bridge.findSimilarPatterns('test');
540
+ expect(patterns).toHaveLength(0);
541
+ });
542
+ it('should call neural cleanup if available', async () => {
543
+ // Trigger neural init
544
+ await bridge.onInsightRecorded(createTestInsight(), 'entry-1');
545
+ vi.clearAllMocks();
546
+ bridge.destroy();
547
+ expect(neural.cleanup).toHaveBeenCalled();
548
+ });
549
+ it('should remove all event listeners', () => {
550
+ bridge.on('insight:learning-started', () => { });
551
+ bridge.on('consolidation:completed', () => { });
552
+ bridge.destroy();
553
+ expect(bridge.listenerCount('insight:learning-started')).toBe(0);
554
+ expect(bridge.listenerCount('consolidation:completed')).toBe(0);
555
+ });
556
+ });
557
+ // ===== Neural init caching =====
558
+ describe('neural initialization', () => {
559
+ it('should only attempt neural load once', async () => {
560
+ const loaderFn = vi.fn().mockResolvedValue(neural);
561
+ const b = new LearningBridge(backend, { neuralLoader: loaderFn });
562
+ await b.onInsightRecorded(createTestInsight(), 'entry-1');
563
+ await b.onInsightRecorded(createTestInsight({ summary: 'Second' }), 'entry-2');
564
+ expect(loaderFn).toHaveBeenCalledTimes(1);
565
+ b.destroy();
566
+ });
567
+ it('should cache failed init and not retry', async () => {
568
+ const loaderFn = vi.fn().mockRejectedValue(new Error('fail'));
569
+ const b = new LearningBridge(backend, { neuralLoader: loaderFn });
570
+ await b.onInsightRecorded(createTestInsight(), 'entry-1');
571
+ await b.onInsightRecorded(createTestInsight(), 'entry-2');
572
+ expect(loaderFn).toHaveBeenCalledTimes(1);
573
+ expect(b.getStats().neuralAvailable).toBe(false);
574
+ b.destroy();
575
+ });
576
+ });
577
+ });
578
+ //# sourceMappingURL=learning-bridge.test.js.map
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Knowledge Graph Module for @claude-flow/memory
3
+ *
4
+ * Builds a graph from MemoryEntry.references, computes PageRank,
5
+ * detects communities via label propagation, and provides
6
+ * graph-aware ranking for search results.
7
+ *
8
+ * Pure TypeScript - no external graph libraries.
9
+ * @module v3/memory/memory-graph
10
+ */
11
+ import { EventEmitter } from 'node:events';
12
+ import type { IMemoryBackend, MemoryEntry, SearchResult } from './types.js';
13
+ export type EdgeType = 'reference' | 'similar' | 'temporal' | 'co-accessed' | 'causal';
14
+ export interface MemoryGraphConfig {
15
+ similarityThreshold?: number;
16
+ pageRankDamping?: number;
17
+ pageRankIterations?: number;
18
+ pageRankConvergence?: number;
19
+ maxNodes?: number;
20
+ enableAutoEdges?: boolean;
21
+ communityAlgorithm?: 'louvain' | 'label-propagation';
22
+ }
23
+ export interface GraphNode {
24
+ id: string;
25
+ category: string;
26
+ confidence: number;
27
+ accessCount: number;
28
+ createdAt: number;
29
+ }
30
+ export interface GraphEdge {
31
+ targetId: string;
32
+ type: EdgeType;
33
+ weight: number;
34
+ }
35
+ export interface RankedResult {
36
+ entry: MemoryEntry;
37
+ score: number;
38
+ pageRank: number;
39
+ combinedScore: number;
40
+ community?: string;
41
+ }
42
+ export interface GraphStats {
43
+ nodeCount: number;
44
+ edgeCount: number;
45
+ avgDegree: number;
46
+ communityCount: number;
47
+ pageRankComputed: boolean;
48
+ maxPageRank: number;
49
+ minPageRank: number;
50
+ }
51
+ /**
52
+ * Knowledge graph built from memory entry references.
53
+ * Supports PageRank, community detection (label propagation),
54
+ * and graph-aware result ranking blending vector similarity with structural importance.
55
+ */
56
+ export declare class MemoryGraph extends EventEmitter {
57
+ private nodes;
58
+ private edges;
59
+ private reverseEdges;
60
+ private pageRanks;
61
+ private communities;
62
+ private config;
63
+ private dirty;
64
+ constructor(config?: MemoryGraphConfig);
65
+ /** Build graph from all entries in a backend. Creates nodes and reference edges. */
66
+ buildFromBackend(backend: IMemoryBackend, namespace?: string): Promise<void>;
67
+ /** Add a node from a MemoryEntry. Skips silently at maxNodes capacity. */
68
+ addNode(entry: MemoryEntry): void;
69
+ /** Add a directed edge. Skips if either node missing. Updates weight to max if exists. */
70
+ addEdge(sourceId: string, targetId: string, type: EdgeType, weight?: number): void;
71
+ /** Remove a node and all associated edges (both directions). */
72
+ removeNode(id: string): void;
73
+ /** Add similarity edges by searching backend. Returns count of edges added. */
74
+ addSimilarityEdges(backend: IMemoryBackend, entryId: string): Promise<number>;
75
+ /**
76
+ * Compute PageRank via power iteration with dangling node redistribution.
77
+ * Returns map of node ID to PageRank score.
78
+ */
79
+ computePageRank(): Map<string, number>;
80
+ /** Detect communities using label propagation. Returns map of nodeId to communityId. */
81
+ detectCommunities(): Map<string, string>;
82
+ /**
83
+ * Rank search results blending vector similarity and PageRank.
84
+ * @param alpha - Weight for vector score (default 0.7). PageRank weight is (1 - alpha).
85
+ */
86
+ rankWithGraph(searchResults: SearchResult[], alpha?: number): RankedResult[];
87
+ /** Get top N nodes by PageRank score. */
88
+ getTopNodes(n: number): Array<{
89
+ id: string;
90
+ pageRank: number;
91
+ community: string;
92
+ }>;
93
+ /** BFS neighbors from a node up to given depth. Excludes the start node. */
94
+ getNeighbors(id: string, depth?: number): Set<string>;
95
+ /** Get statistics about the current graph state. */
96
+ getStats(): GraphStats;
97
+ private hasEdge;
98
+ private shuffleArray;
99
+ }
100
+ //# sourceMappingURL=memory-graph.d.ts.map