holosphere 2.0.0-alpha1 → 2.0.0-alpha2
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.
- package/dist/cjs/holosphere.cjs +2 -0
- package/dist/cjs/holosphere.cjs.map +1 -0
- package/dist/esm/holosphere.js +56 -0
- package/dist/esm/holosphere.js.map +1 -0
- package/dist/index-CDfIuXew.js +15974 -0
- package/dist/index-CDfIuXew.js.map +1 -0
- package/dist/index-ifOgtDvd.cjs +3 -0
- package/dist/index-ifOgtDvd.cjs.map +1 -0
- package/dist/indexeddb-storage-CMW4qRQS.js +96 -0
- package/dist/indexeddb-storage-CMW4qRQS.js.map +1 -0
- package/dist/indexeddb-storage-DLZOgetM.cjs +2 -0
- package/dist/indexeddb-storage-DLZOgetM.cjs.map +1 -0
- package/dist/memory-storage-DQzcAZlf.js +47 -0
- package/dist/memory-storage-DQzcAZlf.js.map +1 -0
- package/dist/memory-storage-DmePEP2q.cjs +2 -0
- package/dist/memory-storage-DmePEP2q.cjs.map +1 -0
- package/dist/secp256k1-CP0ZkpAx.cjs +13 -0
- package/dist/secp256k1-CP0ZkpAx.cjs.map +1 -0
- package/dist/secp256k1-vOXp40Fx.js +2281 -0
- package/dist/secp256k1-vOXp40Fx.js.map +1 -0
- package/docs/FOSDEM_PROPOSAL.md +388 -0
- package/docs/LOCALFIRST.md +266 -0
- package/docs/contracts/api-interface.md +793 -0
- package/docs/data-model.md +476 -0
- package/docs/gun-async-usage.md +338 -0
- package/docs/plan.md +349 -0
- package/docs/quickstart.md +674 -0
- package/docs/research.md +362 -0
- package/docs/spec.md +244 -0
- package/docs/storage-backends.md +326 -0
- package/docs/tasks.md +947 -0
- package/package.json +1 -1
- package/tests/unit/ai/aggregation.test.js +0 -295
- package/tests/unit/ai/breakdown.test.js +0 -446
- package/tests/unit/ai/classifier.test.js +0 -294
- package/tests/unit/ai/council.test.js +0 -262
- package/tests/unit/ai/embeddings.test.js +0 -384
- package/tests/unit/ai/federation-ai.test.js +0 -344
- package/tests/unit/ai/h3-ai.test.js +0 -458
- package/tests/unit/ai/index.test.js +0 -304
- package/tests/unit/ai/json-ops.test.js +0 -307
- package/tests/unit/ai/llm-service.test.js +0 -390
- package/tests/unit/ai/nl-query.test.js +0 -383
- package/tests/unit/ai/relationships.test.js +0 -311
- package/tests/unit/ai/schema-extractor.test.js +0 -384
- package/tests/unit/ai/spatial.test.js +0 -279
- package/tests/unit/ai/tts.test.js +0 -279
- package/tests/unit/content.test.js +0 -332
- package/tests/unit/contract/core.test.js +0 -88
- package/tests/unit/contract/crypto.test.js +0 -198
- package/tests/unit/contract/data.test.js +0 -223
- package/tests/unit/contract/federation.test.js +0 -181
- package/tests/unit/contract/hierarchical.test.js +0 -113
- package/tests/unit/contract/schema.test.js +0 -114
- package/tests/unit/contract/social.test.js +0 -217
- package/tests/unit/contract/spatial.test.js +0 -110
- package/tests/unit/contract/subscriptions.test.js +0 -128
- package/tests/unit/contract/utils.test.js +0 -159
- package/tests/unit/core.test.js +0 -152
- package/tests/unit/crypto.test.js +0 -328
- package/tests/unit/federation.test.js +0 -234
- package/tests/unit/gun-async.test.js +0 -252
- package/tests/unit/hierarchical.test.js +0 -399
- package/tests/unit/integration/scenario-01-geographic-storage.test.js +0 -74
- package/tests/unit/integration/scenario-02-federation.test.js +0 -76
- package/tests/unit/integration/scenario-03-subscriptions.test.js +0 -102
- package/tests/unit/integration/scenario-04-validation.test.js +0 -129
- package/tests/unit/integration/scenario-05-hierarchy.test.js +0 -125
- package/tests/unit/integration/scenario-06-social.test.js +0 -135
- package/tests/unit/integration/scenario-07-persistence.test.js +0 -130
- package/tests/unit/integration/scenario-08-authorization.test.js +0 -161
- package/tests/unit/integration/scenario-09-cross-dimensional.test.js +0 -139
- package/tests/unit/integration/scenario-10-cross-holosphere-capabilities.test.js +0 -357
- package/tests/unit/integration/scenario-11-cross-holosphere-federation.test.js +0 -410
- package/tests/unit/integration/scenario-12-capability-federated-read.test.js +0 -719
- package/tests/unit/performance/benchmark.test.js +0 -85
- package/tests/unit/schema.test.js +0 -213
- package/tests/unit/spatial.test.js +0 -158
- package/tests/unit/storage.test.js +0 -195
- package/tests/unit/subscriptions.test.js +0 -328
- package/tests/unit/test-data-permanence-debug.js +0 -197
- package/tests/unit/test-data-permanence.js +0 -340
- package/tests/unit/test-key-persistence-fixed.js +0 -148
- package/tests/unit/test-key-persistence.js +0 -172
- package/tests/unit/test-relay-permanence.js +0 -376
- package/tests/unit/test-second-node.js +0 -95
- package/tests/unit/test-simple-write.js +0 -89
|
@@ -1,383 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
-
import { NLQuery } from '../../../src/ai/nl-query.js';
|
|
3
|
-
|
|
4
|
-
describe('Unit: NLQuery', () => {
|
|
5
|
-
let nlQuery;
|
|
6
|
-
let mockLLM;
|
|
7
|
-
let mockHolosphere;
|
|
8
|
-
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
vi.clearAllMocks();
|
|
11
|
-
|
|
12
|
-
mockLLM = {
|
|
13
|
-
getJSON: vi.fn(),
|
|
14
|
-
sendMessage: vi.fn()
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
mockHolosphere = {
|
|
18
|
-
getAll: vi.fn().mockResolvedValue([])
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
nlQuery = new NLQuery(mockLLM, mockHolosphere);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
describe('Constructor', () => {
|
|
25
|
-
it('should initialize with LLM service', () => {
|
|
26
|
-
const nq = new NLQuery(mockLLM);
|
|
27
|
-
expect(nq.llm).toBe(mockLLM);
|
|
28
|
-
expect(nq.holosphere).toBeNull();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('should accept optional HoloSphere instance', () => {
|
|
32
|
-
expect(nlQuery.holosphere).toBe(mockHolosphere);
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
describe('setHoloSphere', () => {
|
|
37
|
-
it('should set HoloSphere instance', () => {
|
|
38
|
-
const nq = new NLQuery(mockLLM);
|
|
39
|
-
nq.setHoloSphere(mockHolosphere);
|
|
40
|
-
expect(nq.holosphere).toBe(mockHolosphere);
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
describe('parse', () => {
|
|
45
|
-
it('should parse natural language query into structured format', async () => {
|
|
46
|
-
mockLLM.getJSON.mockResolvedValue({
|
|
47
|
-
holon: null,
|
|
48
|
-
lens: 'projects',
|
|
49
|
-
filters: { status: { op: 'eq', value: 'active' } },
|
|
50
|
-
sort: { field: 'created_at', order: 'desc' },
|
|
51
|
-
limit: 10,
|
|
52
|
-
spatial: null,
|
|
53
|
-
temporal: null
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
const result = await nlQuery.parse('Show me the 10 most recent active projects');
|
|
57
|
-
|
|
58
|
-
expect(result.lens).toBe('projects');
|
|
59
|
-
expect(result.filters.status.op).toBe('eq');
|
|
60
|
-
expect(result.limit).toBe(10);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('should include context in parsing', async () => {
|
|
64
|
-
mockLLM.getJSON.mockResolvedValue({ holon: 'h1', lens: 'tasks' });
|
|
65
|
-
|
|
66
|
-
await nlQuery.parse('Find tasks', {
|
|
67
|
-
holons: ['h1', 'h2'],
|
|
68
|
-
lenses: ['projects', 'tasks']
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
const call = mockLLM.getJSON.mock.calls[0];
|
|
72
|
-
expect(call[0]).toContain('h1, h2');
|
|
73
|
-
expect(call[0]).toContain('projects, tasks');
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('should parse spatial queries', async () => {
|
|
77
|
-
mockLLM.getJSON.mockResolvedValue({
|
|
78
|
-
lens: 'projects',
|
|
79
|
-
spatial: { near: 'Rome', radius: '10km' }
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
const result = await nlQuery.parse('Show projects near Rome');
|
|
83
|
-
|
|
84
|
-
expect(result.spatial.near).toBe('Rome');
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
describe('execute', () => {
|
|
89
|
-
it('should execute query and return results', async () => {
|
|
90
|
-
mockLLM.getJSON.mockResolvedValue({
|
|
91
|
-
holon: 'holon1',
|
|
92
|
-
lens: 'projects',
|
|
93
|
-
filters: null,
|
|
94
|
-
sort: null,
|
|
95
|
-
limit: null
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
mockHolosphere.getAll.mockResolvedValue([
|
|
99
|
-
{ id: 1, title: 'Project 1' },
|
|
100
|
-
{ id: 2, title: 'Project 2' }
|
|
101
|
-
]);
|
|
102
|
-
|
|
103
|
-
const result = await nlQuery.execute('Show all projects');
|
|
104
|
-
|
|
105
|
-
expect(result.query).toBe('Show all projects');
|
|
106
|
-
expect(result.parsed.lens).toBe('projects');
|
|
107
|
-
expect(result.results).toHaveLength(2);
|
|
108
|
-
expect(result.count).toBe(2);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('should throw error if HoloSphere not available', async () => {
|
|
112
|
-
const nq = new NLQuery(mockLLM);
|
|
113
|
-
|
|
114
|
-
await expect(nq.execute('query'))
|
|
115
|
-
.rejects.toThrow('HoloSphere instance required for query execution');
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('should search across holons when only lens specified', async () => {
|
|
119
|
-
mockLLM.getJSON.mockResolvedValue({
|
|
120
|
-
holon: null,
|
|
121
|
-
lens: 'tasks'
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
mockHolosphere.getAll
|
|
125
|
-
.mockResolvedValueOnce([{ id: 1 }])
|
|
126
|
-
.mockResolvedValueOnce([{ id: 2 }]);
|
|
127
|
-
|
|
128
|
-
const result = await nlQuery.execute('Find all tasks', {
|
|
129
|
-
holons: ['h1', 'h2']
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
expect(result.results).toHaveLength(2);
|
|
133
|
-
expect(mockHolosphere.getAll).toHaveBeenCalledTimes(2);
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
describe('_applyFilters', () => {
|
|
138
|
-
it('should filter by equality', () => {
|
|
139
|
-
const items = [
|
|
140
|
-
{ status: 'active' },
|
|
141
|
-
{ status: 'inactive' },
|
|
142
|
-
{ status: 'active' }
|
|
143
|
-
];
|
|
144
|
-
|
|
145
|
-
const result = nlQuery._applyFilters(items, {
|
|
146
|
-
status: { op: 'eq', value: 'active' }
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
expect(result).toHaveLength(2);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('should filter by inequality', () => {
|
|
153
|
-
const items = [
|
|
154
|
-
{ status: 'active' },
|
|
155
|
-
{ status: 'inactive' }
|
|
156
|
-
];
|
|
157
|
-
|
|
158
|
-
const result = nlQuery._applyFilters(items, {
|
|
159
|
-
status: { op: 'ne', value: 'inactive' }
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
expect(result).toHaveLength(1);
|
|
163
|
-
expect(result[0].status).toBe('active');
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it('should filter by greater than', () => {
|
|
167
|
-
const items = [
|
|
168
|
-
{ priority: 1 },
|
|
169
|
-
{ priority: 5 },
|
|
170
|
-
{ priority: 10 }
|
|
171
|
-
];
|
|
172
|
-
|
|
173
|
-
const result = nlQuery._applyFilters(items, {
|
|
174
|
-
priority: { op: 'gt', value: 3 }
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
expect(result).toHaveLength(2);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('should filter by greater than or equal', () => {
|
|
181
|
-
const items = [
|
|
182
|
-
{ priority: 5 },
|
|
183
|
-
{ priority: 10 }
|
|
184
|
-
];
|
|
185
|
-
|
|
186
|
-
const result = nlQuery._applyFilters(items, {
|
|
187
|
-
priority: { op: 'gte', value: 5 }
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
expect(result).toHaveLength(2);
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it('should filter by less than', () => {
|
|
194
|
-
const items = [
|
|
195
|
-
{ priority: 1 },
|
|
196
|
-
{ priority: 5 }
|
|
197
|
-
];
|
|
198
|
-
|
|
199
|
-
const result = nlQuery._applyFilters(items, {
|
|
200
|
-
priority: { op: 'lt', value: 3 }
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
expect(result).toHaveLength(1);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it('should filter by less than or equal', () => {
|
|
207
|
-
const items = [
|
|
208
|
-
{ priority: 1 },
|
|
209
|
-
{ priority: 3 }
|
|
210
|
-
];
|
|
211
|
-
|
|
212
|
-
const result = nlQuery._applyFilters(items, {
|
|
213
|
-
priority: { op: 'lte', value: 3 }
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
expect(result).toHaveLength(2);
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it('should filter by contains (case insensitive)', () => {
|
|
220
|
-
const items = [
|
|
221
|
-
{ title: 'Build Website' },
|
|
222
|
-
{ title: 'Write Documentation' },
|
|
223
|
-
{ title: 'Website Maintenance' }
|
|
224
|
-
];
|
|
225
|
-
|
|
226
|
-
const result = nlQuery._applyFilters(items, {
|
|
227
|
-
title: { op: 'contains', value: 'website' }
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
expect(result).toHaveLength(2);
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it('should filter by in array', () => {
|
|
234
|
-
const items = [
|
|
235
|
-
{ status: 'active' },
|
|
236
|
-
{ status: 'pending' },
|
|
237
|
-
{ status: 'completed' }
|
|
238
|
-
];
|
|
239
|
-
|
|
240
|
-
const result = nlQuery._applyFilters(items, {
|
|
241
|
-
status: { op: 'in', value: ['active', 'pending'] }
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
expect(result).toHaveLength(2);
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it('should handle nested field paths', () => {
|
|
248
|
-
const items = [
|
|
249
|
-
{ meta: { priority: 1 } },
|
|
250
|
-
{ meta: { priority: 5 } }
|
|
251
|
-
];
|
|
252
|
-
|
|
253
|
-
const result = nlQuery._applyFilters(items, {
|
|
254
|
-
'meta.priority': { op: 'gt', value: 2 }
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
expect(result).toHaveLength(1);
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it('should handle unknown operators', () => {
|
|
261
|
-
const items = [{ value: 1 }];
|
|
262
|
-
|
|
263
|
-
const result = nlQuery._applyFilters(items, {
|
|
264
|
-
value: { op: 'unknown', value: 1 }
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
expect(result).toHaveLength(1);
|
|
268
|
-
});
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
describe('_getNestedValue', () => {
|
|
272
|
-
it('should get simple value', () => {
|
|
273
|
-
const obj = { name: 'Test' };
|
|
274
|
-
expect(nlQuery._getNestedValue(obj, 'name')).toBe('Test');
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
it('should get nested value', () => {
|
|
278
|
-
const obj = { user: { profile: { name: 'Test' } } };
|
|
279
|
-
expect(nlQuery._getNestedValue(obj, 'user.profile.name')).toBe('Test');
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
it('should return undefined for missing path', () => {
|
|
283
|
-
const obj = { user: {} };
|
|
284
|
-
expect(nlQuery._getNestedValue(obj, 'user.profile.name')).toBeUndefined();
|
|
285
|
-
});
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
describe('_applySort', () => {
|
|
289
|
-
it('should sort ascending', () => {
|
|
290
|
-
const items = [
|
|
291
|
-
{ priority: 3 },
|
|
292
|
-
{ priority: 1 },
|
|
293
|
-
{ priority: 2 }
|
|
294
|
-
];
|
|
295
|
-
|
|
296
|
-
const result = nlQuery._applySort(items, { field: 'priority', order: 'asc' });
|
|
297
|
-
|
|
298
|
-
expect(result[0].priority).toBe(1);
|
|
299
|
-
expect(result[2].priority).toBe(3);
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
it('should sort descending', () => {
|
|
303
|
-
const items = [
|
|
304
|
-
{ priority: 1 },
|
|
305
|
-
{ priority: 3 },
|
|
306
|
-
{ priority: 2 }
|
|
307
|
-
];
|
|
308
|
-
|
|
309
|
-
const result = nlQuery._applySort(items, { field: 'priority', order: 'desc' });
|
|
310
|
-
|
|
311
|
-
expect(result[0].priority).toBe(3);
|
|
312
|
-
expect(result[2].priority).toBe(1);
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
it('should handle string sorting', () => {
|
|
316
|
-
const items = [
|
|
317
|
-
{ name: 'Charlie' },
|
|
318
|
-
{ name: 'Alice' },
|
|
319
|
-
{ name: 'Bob' }
|
|
320
|
-
];
|
|
321
|
-
|
|
322
|
-
const result = nlQuery._applySort(items, { field: 'name', order: 'asc' });
|
|
323
|
-
|
|
324
|
-
expect(result[0].name).toBe('Alice');
|
|
325
|
-
expect(result[2].name).toBe('Charlie');
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
it('should not mutate original array', () => {
|
|
329
|
-
const items = [{ v: 2 }, { v: 1 }];
|
|
330
|
-
const original = [...items];
|
|
331
|
-
|
|
332
|
-
nlQuery._applySort(items, { field: 'v', order: 'asc' });
|
|
333
|
-
|
|
334
|
-
expect(items[0].v).toBe(original[0].v);
|
|
335
|
-
});
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
describe('suggest', () => {
|
|
339
|
-
it('should suggest queries based on context', async () => {
|
|
340
|
-
mockLLM.getJSON.mockResolvedValue([
|
|
341
|
-
'Show all active projects',
|
|
342
|
-
'Find tasks due this week',
|
|
343
|
-
'List recent events'
|
|
344
|
-
]);
|
|
345
|
-
|
|
346
|
-
const suggestions = await nlQuery.suggest({
|
|
347
|
-
holons: ['region1'],
|
|
348
|
-
lenses: ['projects', 'tasks', 'events']
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
expect(Array.isArray(suggestions)).toBe(true);
|
|
352
|
-
expect(suggestions).toHaveLength(3);
|
|
353
|
-
});
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
describe('explain', () => {
|
|
357
|
-
it('should explain query results in natural language', async () => {
|
|
358
|
-
mockLLM.sendMessage.mockResolvedValue('Found 5 active projects in the Rome region.');
|
|
359
|
-
|
|
360
|
-
const result = await nlQuery.explain('Show projects near Rome', [
|
|
361
|
-
{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }
|
|
362
|
-
]);
|
|
363
|
-
|
|
364
|
-
expect(result).toContain('5');
|
|
365
|
-
expect(mockLLM.sendMessage).toHaveBeenCalledWith(
|
|
366
|
-
expect.stringContaining('Show projects near Rome'),
|
|
367
|
-
expect.any(String),
|
|
368
|
-
expect.objectContaining({ temperature: 0.5 })
|
|
369
|
-
);
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
it('should limit sample results to 5', async () => {
|
|
373
|
-
mockLLM.sendMessage.mockResolvedValue('Summary');
|
|
374
|
-
|
|
375
|
-
const manyResults = Array(20).fill({ id: 1 });
|
|
376
|
-
await nlQuery.explain('Query', manyResults);
|
|
377
|
-
|
|
378
|
-
const call = mockLLM.sendMessage.mock.calls[0];
|
|
379
|
-
const resultSample = JSON.parse(call[1].replace('Results sample: ', ''));
|
|
380
|
-
expect(resultSample).toHaveLength(5);
|
|
381
|
-
});
|
|
382
|
-
});
|
|
383
|
-
});
|
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
-
import { RelationshipDiscovery } from '../../../src/ai/relationships.js';
|
|
3
|
-
|
|
4
|
-
describe('Unit: RelationshipDiscovery', () => {
|
|
5
|
-
let relationships;
|
|
6
|
-
let mockLLM;
|
|
7
|
-
let mockHolosphere;
|
|
8
|
-
let mockEmbeddings;
|
|
9
|
-
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
vi.clearAllMocks();
|
|
12
|
-
|
|
13
|
-
mockLLM = {
|
|
14
|
-
getJSON: vi.fn()
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
mockHolosphere = {
|
|
18
|
-
getAll: vi.fn().mockResolvedValue([])
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
mockEmbeddings = {
|
|
22
|
-
findSimilar: vi.fn().mockResolvedValue([])
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
relationships = new RelationshipDiscovery(mockLLM, mockHolosphere, mockEmbeddings);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
describe('Constructor', () => {
|
|
29
|
-
it('should initialize with LLM service', () => {
|
|
30
|
-
const r = new RelationshipDiscovery(mockLLM);
|
|
31
|
-
expect(r.llm).toBe(mockLLM);
|
|
32
|
-
expect(r.holosphere).toBeNull();
|
|
33
|
-
expect(r.embeddings).toBeNull();
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('should accept optional HoloSphere and Embeddings', () => {
|
|
37
|
-
expect(relationships.holosphere).toBe(mockHolosphere);
|
|
38
|
-
expect(relationships.embeddings).toBe(mockEmbeddings);
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
describe('setHoloSphere', () => {
|
|
43
|
-
it('should set HoloSphere instance', () => {
|
|
44
|
-
const r = new RelationshipDiscovery(mockLLM);
|
|
45
|
-
r.setHoloSphere(mockHolosphere);
|
|
46
|
-
expect(r.holosphere).toBe(mockHolosphere);
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
describe('setEmbeddings', () => {
|
|
51
|
-
it('should set Embeddings instance', () => {
|
|
52
|
-
const r = new RelationshipDiscovery(mockLLM);
|
|
53
|
-
r.setEmbeddings(mockEmbeddings);
|
|
54
|
-
expect(r.embeddings).toBe(mockEmbeddings);
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
describe('discoverRelationships', () => {
|
|
59
|
-
it('should discover relationships in holon data', async () => {
|
|
60
|
-
mockHolosphere.getAll.mockResolvedValue([
|
|
61
|
-
{ id: 1, title: 'Project A', topic: 'sustainability' },
|
|
62
|
-
{ id: 2, title: 'Project B', topic: 'sustainability' },
|
|
63
|
-
{ id: 3, title: 'Project C', topic: 'tech' }
|
|
64
|
-
]);
|
|
65
|
-
|
|
66
|
-
mockLLM.getJSON.mockResolvedValue({
|
|
67
|
-
relationships: [
|
|
68
|
-
{
|
|
69
|
-
item1: { id: 1, title: 'Project A' },
|
|
70
|
-
item2: { id: 2, title: 'Project B' },
|
|
71
|
-
type: 'thematic',
|
|
72
|
-
strength: 0.9,
|
|
73
|
-
description: 'Both focus on sustainability'
|
|
74
|
-
}
|
|
75
|
-
],
|
|
76
|
-
clusters: [
|
|
77
|
-
{ theme: 'sustainability', items: [1, 2] }
|
|
78
|
-
],
|
|
79
|
-
key_entities: ['sustainability', 'tech'],
|
|
80
|
-
summary: 'Two sustainability projects are related'
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
const result = await relationships.discoverRelationships('holon1', 'projects');
|
|
84
|
-
|
|
85
|
-
expect(result.holon).toBe('holon1');
|
|
86
|
-
expect(result.lens).toBe('projects');
|
|
87
|
-
expect(result.itemCount).toBe(3);
|
|
88
|
-
expect(result.relationships.relationships).toHaveLength(1);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('should throw error if HoloSphere not available', async () => {
|
|
92
|
-
const r = new RelationshipDiscovery(mockLLM);
|
|
93
|
-
|
|
94
|
-
await expect(r.discoverRelationships('holon', 'lens'))
|
|
95
|
-
.rejects.toThrow('HoloSphere instance required');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('should return message if not enough data', async () => {
|
|
99
|
-
mockHolosphere.getAll.mockResolvedValue([{ id: 1 }]);
|
|
100
|
-
|
|
101
|
-
const result = await relationships.discoverRelationships('holon1', 'lens');
|
|
102
|
-
|
|
103
|
-
expect(result.relationships).toEqual([]);
|
|
104
|
-
expect(result.message).toBe('Not enough data for relationship discovery');
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('should search multiple lenses when lens is null', async () => {
|
|
108
|
-
mockHolosphere.getAll.mockResolvedValue([{ id: 1 }, { id: 2 }]);
|
|
109
|
-
mockLLM.getJSON.mockResolvedValue({ relationships: [] });
|
|
110
|
-
|
|
111
|
-
await relationships.discoverRelationships('holon1', null);
|
|
112
|
-
|
|
113
|
-
expect(mockHolosphere.getAll.mock.calls.length).toBeGreaterThan(1);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('should add _lens field to items', async () => {
|
|
117
|
-
mockHolosphere.getAll.mockResolvedValue([{ id: 1 }, { id: 2 }]);
|
|
118
|
-
mockLLM.getJSON.mockResolvedValue({ relationships: [] });
|
|
119
|
-
|
|
120
|
-
await relationships.discoverRelationships('holon1', 'projects');
|
|
121
|
-
|
|
122
|
-
const call = mockLLM.getJSON.mock.calls[0];
|
|
123
|
-
expect(call[1]).toContain('projects');
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
describe('findSimilar', () => {
|
|
128
|
-
it('should use embeddings when available', async () => {
|
|
129
|
-
mockEmbeddings.findSimilar.mockResolvedValue([
|
|
130
|
-
{ item: { id: 1 }, similarity: 0.9 }
|
|
131
|
-
]);
|
|
132
|
-
|
|
133
|
-
const item = { title: 'Test' };
|
|
134
|
-
const result = await relationships.findSimilar(item, 'holon1', 'lens1');
|
|
135
|
-
|
|
136
|
-
expect(mockEmbeddings.findSimilar).toHaveBeenCalledWith(
|
|
137
|
-
item,
|
|
138
|
-
'holon1',
|
|
139
|
-
'lens1',
|
|
140
|
-
expect.objectContaining({ limit: 10, threshold: 0.5 })
|
|
141
|
-
);
|
|
142
|
-
expect(result).toHaveLength(1);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('should fall back to LLM when embeddings not available', async () => {
|
|
146
|
-
const r = new RelationshipDiscovery(mockLLM, mockHolosphere);
|
|
147
|
-
|
|
148
|
-
mockHolosphere.getAll.mockResolvedValue([
|
|
149
|
-
{ id: 1, title: 'Similar' },
|
|
150
|
-
{ id: 2, title: 'Different' }
|
|
151
|
-
]);
|
|
152
|
-
|
|
153
|
-
mockLLM.getJSON.mockResolvedValue([
|
|
154
|
-
{ item: { id: 1 }, similarity: 0.8, reasons: ['Same topic'] }
|
|
155
|
-
]);
|
|
156
|
-
|
|
157
|
-
const result = await r.findSimilar({ title: 'Test' }, 'holon1', 'lens1');
|
|
158
|
-
|
|
159
|
-
expect(result).toHaveLength(1);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it('should throw error if no holon for LLM-based similarity', async () => {
|
|
163
|
-
const r = new RelationshipDiscovery(mockLLM, mockHolosphere);
|
|
164
|
-
|
|
165
|
-
await expect(r.findSimilar({ title: 'Test' }, null))
|
|
166
|
-
.rejects.toThrow('Holon required for LLM-based similarity');
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it('should filter by threshold', async () => {
|
|
170
|
-
const r = new RelationshipDiscovery(mockLLM, mockHolosphere);
|
|
171
|
-
|
|
172
|
-
mockHolosphere.getAll.mockResolvedValue([{ id: 1 }]);
|
|
173
|
-
mockLLM.getJSON.mockResolvedValue([
|
|
174
|
-
{ item: { id: 1 }, similarity: 0.8 },
|
|
175
|
-
{ item: { id: 2 }, similarity: 0.4 }
|
|
176
|
-
]);
|
|
177
|
-
|
|
178
|
-
const result = await r.findSimilar({ title: 'Test' }, 'holon', 'lens', { threshold: 0.6 });
|
|
179
|
-
|
|
180
|
-
expect(result).toHaveLength(1);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it('should skip embeddings when useEmbeddings is false', async () => {
|
|
184
|
-
mockHolosphere.getAll.mockResolvedValue([{ id: 1 }]);
|
|
185
|
-
mockLLM.getJSON.mockResolvedValue([]);
|
|
186
|
-
|
|
187
|
-
await relationships.findSimilar({ title: 'Test' }, 'holon', 'lens', { useEmbeddings: false });
|
|
188
|
-
|
|
189
|
-
expect(mockEmbeddings.findSimilar).not.toHaveBeenCalled();
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
describe('buildGraph', () => {
|
|
194
|
-
it('should build relationship graph structure', async () => {
|
|
195
|
-
mockHolosphere.getAll.mockResolvedValue([{ id: 1 }, { id: 2 }]);
|
|
196
|
-
|
|
197
|
-
mockLLM.getJSON.mockResolvedValue({
|
|
198
|
-
relationships: [
|
|
199
|
-
{
|
|
200
|
-
item1: { id: 'a', title: 'A' },
|
|
201
|
-
item2: { id: 'b', title: 'B' },
|
|
202
|
-
type: 'thematic',
|
|
203
|
-
strength: 0.8
|
|
204
|
-
}
|
|
205
|
-
],
|
|
206
|
-
clusters: [{ theme: 'test', items: ['a', 'b'] }]
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
const result = await relationships.buildGraph('holon1', 'lens1');
|
|
210
|
-
|
|
211
|
-
expect(result.nodes).toHaveLength(2);
|
|
212
|
-
expect(result.edges).toHaveLength(1);
|
|
213
|
-
expect(result.edges[0].source).toBe('a');
|
|
214
|
-
expect(result.edges[0].target).toBe('b');
|
|
215
|
-
expect(result.clusters).toHaveLength(1);
|
|
216
|
-
});
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
describe('findCrossHolonRelationships', () => {
|
|
220
|
-
it('should find relationships across holons', async () => {
|
|
221
|
-
mockHolosphere.getAll
|
|
222
|
-
.mockResolvedValueOnce([{ id: 1, title: 'H1 Item' }])
|
|
223
|
-
.mockResolvedValueOnce([{ id: 2, title: 'H2 Item' }]);
|
|
224
|
-
|
|
225
|
-
mockLLM.getJSON.mockResolvedValue({
|
|
226
|
-
cross_relationships: [
|
|
227
|
-
{
|
|
228
|
-
holon1: 'h1',
|
|
229
|
-
item1: 1,
|
|
230
|
-
holon2: 'h2',
|
|
231
|
-
item2: 2,
|
|
232
|
-
type: 'thematic',
|
|
233
|
-
potential: 'collaboration'
|
|
234
|
-
}
|
|
235
|
-
],
|
|
236
|
-
shared_themes: [{ theme: 'innovation', holons: ['h1', 'h2'] }],
|
|
237
|
-
opportunities: ['Joint project']
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
const result = await relationships.findCrossHolonRelationships(['h1', 'h2'], 'lens');
|
|
241
|
-
|
|
242
|
-
expect(result.cross_relationships).toHaveLength(1);
|
|
243
|
-
expect(result.shared_themes).toHaveLength(1);
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it('should throw error if HoloSphere not available', async () => {
|
|
247
|
-
const r = new RelationshipDiscovery(mockLLM);
|
|
248
|
-
|
|
249
|
-
await expect(r.findCrossHolonRelationships(['h1', 'h2'], 'lens'))
|
|
250
|
-
.rejects.toThrow('HoloSphere instance required');
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it('should require at least 2 holons with data', async () => {
|
|
254
|
-
mockHolosphere.getAll.mockResolvedValue([{ id: 1 }]);
|
|
255
|
-
|
|
256
|
-
const result = await relationships.findCrossHolonRelationships(['h1'], 'lens');
|
|
257
|
-
|
|
258
|
-
expect(result.message).toBe('Need at least 2 holons with data');
|
|
259
|
-
});
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
describe('suggestConnections', () => {
|
|
263
|
-
it('should suggest connections for an item', async () => {
|
|
264
|
-
mockEmbeddings.findSimilar.mockResolvedValue([
|
|
265
|
-
{ item: { id: 2 }, similarity: 0.8 }
|
|
266
|
-
]);
|
|
267
|
-
|
|
268
|
-
mockLLM.getJSON.mockResolvedValue({
|
|
269
|
-
collaborations: [{ with: 2, action: 'Partner on project' }],
|
|
270
|
-
resource_sharing: [{ items: [1, 2], resource: 'Equipment' }],
|
|
271
|
-
initiatives: [{ description: 'Joint workshop', participants: [1, 2] }],
|
|
272
|
-
knowledge_exchange: [{ from: 1, to: 2, topic: 'Best practices' }]
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
const result = await relationships.suggestConnections({ id: 1 }, 'holon', 'lens');
|
|
276
|
-
|
|
277
|
-
expect(result.collaborations).toBeDefined();
|
|
278
|
-
expect(result.resource_sharing).toBeDefined();
|
|
279
|
-
});
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
describe('detectPatterns', () => {
|
|
283
|
-
it('should detect relationship patterns', async () => {
|
|
284
|
-
mockHolosphere.getAll.mockResolvedValue([{ id: 1 }, { id: 2 }, { id: 3 }]);
|
|
285
|
-
|
|
286
|
-
mockLLM.getJSON
|
|
287
|
-
.mockResolvedValueOnce({
|
|
288
|
-
relationships: [
|
|
289
|
-
{ item1: { id: 'a' }, item2: { id: 'b' }, type: 'thematic', strength: 0.8 }
|
|
290
|
-
]
|
|
291
|
-
})
|
|
292
|
-
.mockResolvedValueOnce({
|
|
293
|
-
patterns: [
|
|
294
|
-
{
|
|
295
|
-
type: 'hub',
|
|
296
|
-
description: 'Central node connects multiple items',
|
|
297
|
-
items: ['a'],
|
|
298
|
-
significance: 'Key connector'
|
|
299
|
-
}
|
|
300
|
-
],
|
|
301
|
-
network_structure: 'Hub-spoke pattern',
|
|
302
|
-
recommendations: ['Strengthen peripheral connections']
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
const result = await relationships.detectPatterns('holon', 'lens');
|
|
306
|
-
|
|
307
|
-
expect(result.patterns).toBeDefined();
|
|
308
|
-
expect(result.network_structure).toBeDefined();
|
|
309
|
-
});
|
|
310
|
-
});
|
|
311
|
-
});
|