holosphere 1.1.20 → 2.0.0-alpha1

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 (147) hide show
  1. package/.env.example +36 -0
  2. package/.eslintrc.json +16 -0
  3. package/.prettierrc.json +7 -0
  4. package/LICENSE +162 -38
  5. package/README.md +483 -367
  6. package/bin/holosphere-activitypub.js +158 -0
  7. package/cleanup-test-data.js +204 -0
  8. package/examples/demo.html +1333 -0
  9. package/examples/example-bot.js +197 -0
  10. package/package.json +47 -87
  11. package/scripts/check-bundle-size.js +54 -0
  12. package/scripts/check-quest-ids.js +77 -0
  13. package/scripts/import-holons.js +578 -0
  14. package/scripts/publish-to-relay.js +101 -0
  15. package/scripts/read-example.js +186 -0
  16. package/scripts/relay-diagnostic.js +59 -0
  17. package/scripts/relay-example.js +179 -0
  18. package/scripts/resync-to-relay.js +245 -0
  19. package/scripts/revert-import.js +196 -0
  20. package/scripts/test-hybrid-mode.js +108 -0
  21. package/scripts/test-local-storage.js +63 -0
  22. package/scripts/test-nostr-direct.js +55 -0
  23. package/scripts/test-read-data.js +45 -0
  24. package/scripts/test-write-read.js +63 -0
  25. package/scripts/verify-import.js +95 -0
  26. package/scripts/verify-relay-data.js +139 -0
  27. package/src/ai/aggregation.js +319 -0
  28. package/src/ai/breakdown.js +511 -0
  29. package/src/ai/classifier.js +217 -0
  30. package/src/ai/council.js +228 -0
  31. package/src/ai/embeddings.js +279 -0
  32. package/src/ai/federation-ai.js +324 -0
  33. package/src/ai/h3-ai.js +955 -0
  34. package/src/ai/index.js +112 -0
  35. package/src/ai/json-ops.js +225 -0
  36. package/src/ai/llm-service.js +205 -0
  37. package/src/ai/nl-query.js +223 -0
  38. package/src/ai/relationships.js +353 -0
  39. package/src/ai/schema-extractor.js +218 -0
  40. package/src/ai/spatial.js +293 -0
  41. package/src/ai/tts.js +194 -0
  42. package/src/content/social-protocols.js +168 -0
  43. package/src/core/holosphere.js +273 -0
  44. package/src/crypto/secp256k1.js +259 -0
  45. package/src/federation/discovery.js +334 -0
  46. package/src/federation/hologram.js +1042 -0
  47. package/src/federation/registry.js +386 -0
  48. package/src/hierarchical/upcast.js +110 -0
  49. package/src/index.js +2669 -0
  50. package/src/schema/validator.js +91 -0
  51. package/src/spatial/h3-operations.js +110 -0
  52. package/src/storage/backend-factory.js +125 -0
  53. package/src/storage/backend-interface.js +142 -0
  54. package/src/storage/backends/activitypub/server.js +653 -0
  55. package/src/storage/backends/activitypub-backend.js +272 -0
  56. package/src/storage/backends/gundb-backend.js +233 -0
  57. package/src/storage/backends/nostr-backend.js +136 -0
  58. package/src/storage/filesystem-storage-browser.js +41 -0
  59. package/src/storage/filesystem-storage.js +138 -0
  60. package/src/storage/global-tables.js +81 -0
  61. package/src/storage/gun-async.js +281 -0
  62. package/src/storage/gun-wrapper.js +221 -0
  63. package/src/storage/indexeddb-storage.js +122 -0
  64. package/src/storage/key-storage-simple.js +76 -0
  65. package/src/storage/key-storage.js +136 -0
  66. package/src/storage/memory-storage.js +59 -0
  67. package/src/storage/migration.js +338 -0
  68. package/src/storage/nostr-async.js +811 -0
  69. package/src/storage/nostr-client.js +939 -0
  70. package/src/storage/nostr-wrapper.js +211 -0
  71. package/src/storage/outbox-queue.js +208 -0
  72. package/src/storage/persistent-storage.js +109 -0
  73. package/src/storage/sync-service.js +164 -0
  74. package/src/subscriptions/manager.js +142 -0
  75. package/test-ai-real-api.js +202 -0
  76. package/tests/unit/ai/aggregation.test.js +295 -0
  77. package/tests/unit/ai/breakdown.test.js +446 -0
  78. package/tests/unit/ai/classifier.test.js +294 -0
  79. package/tests/unit/ai/council.test.js +262 -0
  80. package/tests/unit/ai/embeddings.test.js +384 -0
  81. package/tests/unit/ai/federation-ai.test.js +344 -0
  82. package/tests/unit/ai/h3-ai.test.js +458 -0
  83. package/tests/unit/ai/index.test.js +304 -0
  84. package/tests/unit/ai/json-ops.test.js +307 -0
  85. package/tests/unit/ai/llm-service.test.js +390 -0
  86. package/tests/unit/ai/nl-query.test.js +383 -0
  87. package/tests/unit/ai/relationships.test.js +311 -0
  88. package/tests/unit/ai/schema-extractor.test.js +384 -0
  89. package/tests/unit/ai/spatial.test.js +279 -0
  90. package/tests/unit/ai/tts.test.js +279 -0
  91. package/tests/unit/content.test.js +332 -0
  92. package/tests/unit/contract/core.test.js +88 -0
  93. package/tests/unit/contract/crypto.test.js +198 -0
  94. package/tests/unit/contract/data.test.js +223 -0
  95. package/tests/unit/contract/federation.test.js +181 -0
  96. package/tests/unit/contract/hierarchical.test.js +113 -0
  97. package/tests/unit/contract/schema.test.js +114 -0
  98. package/tests/unit/contract/social.test.js +217 -0
  99. package/tests/unit/contract/spatial.test.js +110 -0
  100. package/tests/unit/contract/subscriptions.test.js +128 -0
  101. package/tests/unit/contract/utils.test.js +159 -0
  102. package/tests/unit/core.test.js +152 -0
  103. package/tests/unit/crypto.test.js +328 -0
  104. package/tests/unit/federation.test.js +234 -0
  105. package/tests/unit/gun-async.test.js +252 -0
  106. package/tests/unit/hierarchical.test.js +399 -0
  107. package/tests/unit/integration/scenario-01-geographic-storage.test.js +74 -0
  108. package/tests/unit/integration/scenario-02-federation.test.js +76 -0
  109. package/tests/unit/integration/scenario-03-subscriptions.test.js +102 -0
  110. package/tests/unit/integration/scenario-04-validation.test.js +129 -0
  111. package/tests/unit/integration/scenario-05-hierarchy.test.js +125 -0
  112. package/tests/unit/integration/scenario-06-social.test.js +135 -0
  113. package/tests/unit/integration/scenario-07-persistence.test.js +130 -0
  114. package/tests/unit/integration/scenario-08-authorization.test.js +161 -0
  115. package/tests/unit/integration/scenario-09-cross-dimensional.test.js +139 -0
  116. package/tests/unit/integration/scenario-10-cross-holosphere-capabilities.test.js +357 -0
  117. package/tests/unit/integration/scenario-11-cross-holosphere-federation.test.js +410 -0
  118. package/tests/unit/integration/scenario-12-capability-federated-read.test.js +719 -0
  119. package/tests/unit/performance/benchmark.test.js +85 -0
  120. package/tests/unit/schema.test.js +213 -0
  121. package/tests/unit/spatial.test.js +158 -0
  122. package/tests/unit/storage.test.js +195 -0
  123. package/tests/unit/subscriptions.test.js +328 -0
  124. package/tests/unit/test-data-permanence-debug.js +197 -0
  125. package/tests/unit/test-data-permanence.js +340 -0
  126. package/tests/unit/test-key-persistence-fixed.js +148 -0
  127. package/tests/unit/test-key-persistence.js +172 -0
  128. package/tests/unit/test-relay-permanence.js +376 -0
  129. package/tests/unit/test-second-node.js +95 -0
  130. package/tests/unit/test-simple-write.js +89 -0
  131. package/vite.config.js +49 -0
  132. package/vitest.config.js +20 -0
  133. package/FEDERATION.md +0 -213
  134. package/compute.js +0 -298
  135. package/content.js +0 -980
  136. package/federation.js +0 -1234
  137. package/global.js +0 -736
  138. package/hexlib.js +0 -335
  139. package/hologram.js +0 -183
  140. package/holosphere-bundle.esm.js +0 -33256
  141. package/holosphere-bundle.js +0 -33287
  142. package/holosphere-bundle.min.js +0 -39
  143. package/holosphere.d.ts +0 -601
  144. package/holosphere.js +0 -719
  145. package/node.js +0 -246
  146. package/schema.js +0 -139
  147. package/utils.js +0 -302
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Real-time Subscription Management
3
+ */
4
+
5
+ import { subscribe } from '../storage/nostr-wrapper.js';
6
+ import { resolveHologram } from '../federation/hologram.js';
7
+
8
+ /**
9
+ * Create subscription with throttle and filter
10
+ * @param {Object} client - Nostr client instance
11
+ * @param {string} path - Path to subscribe to
12
+ * @param {Function} callback - Callback function
13
+ * @param {Object} options - Subscription options
14
+ * @param {number} options.throttle - Throttle interval in ms
15
+ * @param {Function} options.filter - Filter predicate
16
+ * @param {boolean} options.includeFederated - Include federated data
17
+ * @param {boolean} options.triggerInitial - Trigger callback with initial data (default: false)
18
+ * @param {boolean} options.resolveHolograms - Resolve holograms before callback (default: true)
19
+ * @returns {Object} Subscription object with unsubscribe method
20
+ */
21
+ export async function createSubscription(client, path, callback, options = {}) {
22
+ const { throttle = 0, filter = null, includeFederated = false, triggerInitial = false, realtimeOnly = true, resolveHolograms = true } = options;
23
+
24
+ let lastInvoke = 0;
25
+ let timeoutId = null;
26
+ const seenKeys = new Set();
27
+ let isActive = true; // Track active state
28
+
29
+ const wrappedCallback = async (data, key) => {
30
+ // Check if subscription is still active
31
+ if (!isActive) {
32
+ return;
33
+ }
34
+
35
+ // Skip if already seen (prevent duplicates)
36
+ // Include timestamp in unique key to allow updates to same item
37
+ const uniqueKey = `${key}-${data?.id || ''}-${data?._meta?.timestamp || Date.now()}`;
38
+ if (seenKeys.has(uniqueKey)) {
39
+ return;
40
+ }
41
+ seenKeys.add(uniqueKey);
42
+
43
+ // Resolve holograms if enabled and data is a hologram
44
+ let resolvedData = data;
45
+ if (resolveHolograms && data && data.hologram === true) {
46
+ try {
47
+ resolvedData = await resolveHologram(client, data);
48
+ // If resolution failed, skip this item (source data may not exist yet)
49
+ if (!resolvedData) {
50
+ return;
51
+ }
52
+ } catch (err) {
53
+ console.warn('Failed to resolve hologram in subscription:', err);
54
+ return;
55
+ }
56
+ }
57
+
58
+ // Apply filter (on resolved data)
59
+ if (filter && !filter(resolvedData, key)) {
60
+ return;
61
+ }
62
+
63
+ // Apply throttle
64
+ const now = Date.now();
65
+ if (throttle > 0) {
66
+ if (now - lastInvoke < throttle) {
67
+ // Clear existing timeout
68
+ if (timeoutId) clearTimeout(timeoutId);
69
+
70
+ // Schedule callback
71
+ timeoutId = setTimeout(() => {
72
+ lastInvoke = Date.now();
73
+ callback(resolvedData, key);
74
+ }, throttle - (now - lastInvoke));
75
+ return;
76
+ }
77
+ }
78
+
79
+ lastInvoke = now;
80
+ callback(resolvedData, key);
81
+ };
82
+
83
+ // Subscribe using Nostr wrapper
84
+ const subscription = await subscribe(client, path, wrappedCallback, { realtimeOnly });
85
+
86
+ // Return subscription object
87
+ return {
88
+ path,
89
+ unsubscribe: () => {
90
+ isActive = false; // Mark as inactive immediately
91
+ if (timeoutId) clearTimeout(timeoutId);
92
+ if (subscription && subscription.unsubscribe) {
93
+ subscription.unsubscribe();
94
+ }
95
+ seenKeys.clear();
96
+ },
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Subscription registry for tracking active subscriptions
102
+ */
103
+ export class SubscriptionRegistry {
104
+ constructor() {
105
+ this.subscriptions = new Map();
106
+ }
107
+
108
+ /**
109
+ * Register a subscription
110
+ */
111
+ register(id, subscription) {
112
+ this.subscriptions.set(id, subscription);
113
+ }
114
+
115
+ /**
116
+ * Unregister a subscription
117
+ */
118
+ unregister(id) {
119
+ const subscription = this.subscriptions.get(id);
120
+ if (subscription) {
121
+ subscription.unsubscribe();
122
+ this.subscriptions.delete(id);
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Unsubscribe all
128
+ */
129
+ unsubscribeAll() {
130
+ for (const [id, subscription] of this.subscriptions) {
131
+ subscription.unsubscribe();
132
+ }
133
+ this.subscriptions.clear();
134
+ }
135
+
136
+ /**
137
+ * Get active subscription count
138
+ */
139
+ count() {
140
+ return this.subscriptions.size;
141
+ }
142
+ }
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Test AI Module with Real OpenAI API Calls
3
+ *
4
+ * Run with: node test-ai-real-api.js
5
+ *
6
+ * Requires OPENAI_API_KEY environment variable to be set
7
+ */
8
+
9
+ import 'dotenv/config';
10
+ import { LLMService } from './src/ai/llm-service.js';
11
+ import { Embeddings } from './src/ai/embeddings.js';
12
+ import { SchemaExtractor } from './src/ai/schema-extractor.js';
13
+ import { Classifier } from './src/ai/classifier.js';
14
+ import { NLQuery } from './src/ai/nl-query.js';
15
+ import { createAIServices } from './src/ai/index.js';
16
+ import OpenAI from 'openai';
17
+
18
+ const apiKey = process.env.OPENAI_API_KEY;
19
+
20
+ if (!apiKey) {
21
+ console.error('ERROR: OPENAI_API_KEY environment variable is not set');
22
+ console.error('Please set it in your .env file or export it:');
23
+ console.error(' export OPENAI_API_KEY=sk-...');
24
+ process.exit(1);
25
+ }
26
+
27
+ console.log('='.repeat(60));
28
+ console.log(' HoloSphere AI Module - Real API Test');
29
+ console.log('='.repeat(60));
30
+ console.log(`API Key: ${apiKey.substring(0, 10)}...${apiKey.substring(apiKey.length - 4)}`);
31
+ console.log('');
32
+
33
+ let passed = 0;
34
+ let failed = 0;
35
+
36
+ async function test(name, fn) {
37
+ process.stdout.write(`Testing ${name}... `);
38
+ try {
39
+ const result = await fn();
40
+ console.log('PASSED');
41
+ if (result) console.log(` Result: ${JSON.stringify(result).substring(0, 100)}...`);
42
+ passed++;
43
+ return result;
44
+ } catch (err) {
45
+ console.log('FAILED');
46
+ console.log(` Error: ${err.message}`);
47
+ failed++;
48
+ return null;
49
+ }
50
+ }
51
+
52
+ // Initialize services
53
+ const llm = new LLMService(apiKey);
54
+ const openai = new OpenAI({ apiKey });
55
+ const embeddings = new Embeddings(openai);
56
+
57
+ console.log('\n--- LLM Service Tests ---\n');
58
+
59
+ // Test 1: Basic chat completion
60
+ await test('LLM sendMessage', async () => {
61
+ const response = await llm.sendMessage(
62
+ 'You are a helpful assistant. Respond in one sentence.',
63
+ 'What is H3 hexagonal indexing?'
64
+ );
65
+ return response.substring(0, 80);
66
+ });
67
+
68
+ // Test 2: Summarization
69
+ await test('LLM summarize', async () => {
70
+ const text = `
71
+ HoloSphere is a local-first, geospatially-aware database built on Nostr.
72
+ It uses H3 hexagonal cells to partition geographic space into holons.
73
+ Each holon can store data in different lenses for domain separation.
74
+ Federation allows holons to share data through lightweight references called holograms.
75
+ `;
76
+ const summary = await llm.summarize(text, { maxLength: 30 });
77
+ return summary;
78
+ });
79
+
80
+ // Test 3: Keyword extraction
81
+ await test('LLM extractKeywords', async () => {
82
+ const keywords = await llm.extractKeywords(
83
+ 'HoloSphere uses H3 hexagonal cells and Nostr protocol for geospatial data storage',
84
+ 5
85
+ );
86
+ return keywords;
87
+ });
88
+
89
+ // Test 4: JSON extraction
90
+ await test('LLM getJSON', async () => {
91
+ const json = await llm.getJSON(
92
+ 'Extract the person info as JSON with fields: name, age, city',
93
+ 'John Smith is 32 years old and lives in San Francisco'
94
+ );
95
+ return json;
96
+ });
97
+
98
+ console.log('\n--- Embeddings Tests ---\n');
99
+
100
+ // Test 5: Single embedding
101
+ await test('Embeddings embed', async () => {
102
+ const embedding = await embeddings.embed('HoloSphere geospatial database');
103
+ return { dimensions: embedding.length, sample: embedding.slice(0, 3) };
104
+ });
105
+
106
+ // Test 6: Batch embeddings
107
+ await test('Embeddings embedBatch', async () => {
108
+ const batch = await embeddings.embedBatch([
109
+ 'Geographic data',
110
+ 'Hexagonal cells',
111
+ 'Federation'
112
+ ]);
113
+ return { count: batch.length, dimensions: batch[0]?.length };
114
+ });
115
+
116
+ // Test 7: Similarity calculation
117
+ await test('Embeddings similarity', async () => {
118
+ const emb1 = await embeddings.embed('San Francisco California');
119
+ const emb2 = await embeddings.embed('Los Angeles California');
120
+ const emb3 = await embeddings.embed('Programming code software');
121
+
122
+ const sim12 = embeddings.cosineSimilarity(emb1, emb2);
123
+ const sim13 = embeddings.cosineSimilarity(emb1, emb3);
124
+
125
+ return {
126
+ 'SF vs LA': sim12.toFixed(3),
127
+ 'SF vs Code': sim13.toFixed(3),
128
+ 'Cities more similar': sim12 > sim13
129
+ };
130
+ });
131
+
132
+ console.log('\n--- Schema Extractor Tests ---\n');
133
+
134
+ // Test 8: Schema extraction
135
+ await test('SchemaExtractor extractToSchema', async () => {
136
+ const extractor = new SchemaExtractor(llm);
137
+ const schema = {
138
+ type: 'object',
139
+ properties: {
140
+ location: { type: 'string', description: 'Location name' },
141
+ latitude: { type: 'number', description: 'Latitude coordinate' },
142
+ longitude: { type: 'number', description: 'Longitude coordinate' }
143
+ },
144
+ required: ['location', 'latitude', 'longitude']
145
+ };
146
+
147
+ const data = await extractor.extractToSchema(
148
+ 'The Eiffel Tower is located in Paris at coordinates 48.8584 N, 2.2945 E',
149
+ schema
150
+ );
151
+ return data;
152
+ });
153
+
154
+ console.log('\n--- Classifier Tests ---\n');
155
+
156
+ // Test 9: Content classification
157
+ await test('Classifier classifyToLens', async () => {
158
+ const classifier = new Classifier(llm);
159
+ classifier.registerLenses({
160
+ events: { description: 'Events, meetings, gatherings' },
161
+ projects: { description: 'Projects, initiatives, work items' },
162
+ resources: { description: 'Resources, documents, materials' },
163
+ places: { description: 'Locations, venues, geographic points' }
164
+ });
165
+
166
+ const result = await classifier.classifyToLens({
167
+ title: 'Community Garden Project',
168
+ description: 'A local initiative to create a shared vegetable garden in the park'
169
+ });
170
+ return result;
171
+ });
172
+
173
+ console.log('\n--- NL Query Tests ---\n');
174
+
175
+ // Test 10: Natural language to query
176
+ await test('NLQuery parse', async () => {
177
+ const nlQuery = new NLQuery(llm);
178
+ const query = await nlQuery.parse(
179
+ 'Find all coffee shops within 500 meters of downtown San Francisco'
180
+ );
181
+ return query;
182
+ });
183
+
184
+ console.log('\n--- Full Service Creation ---\n');
185
+
186
+ // Test 11: Create all services
187
+ await test('createAIServices', async () => {
188
+ const services = createAIServices(apiKey, null, {}, openai);
189
+ return {
190
+ servicesCreated: Object.keys(services).length,
191
+ services: Object.keys(services)
192
+ };
193
+ });
194
+
195
+ // Summary
196
+ console.log('\n' + '='.repeat(60));
197
+ console.log(` Results: ${passed} passed, ${failed} failed`);
198
+ console.log('='.repeat(60));
199
+
200
+ if (failed > 0) {
201
+ process.exit(1);
202
+ }
@@ -0,0 +1,295 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { SmartAggregation } from '../../../src/ai/aggregation.js';
3
+
4
+ // Mock h3-js
5
+ vi.mock('h3-js', () => ({
6
+ getResolution: vi.fn().mockReturnValue(5),
7
+ cellToParent: vi.fn().mockReturnValue('parent_holon'),
8
+ cellToChildren: vi.fn().mockReturnValue(['child1', 'child2', 'child3'])
9
+ }));
10
+
11
+ describe('Unit: SmartAggregation', () => {
12
+ let aggregation;
13
+ let mockLLM;
14
+ let mockHolosphere;
15
+
16
+ beforeEach(() => {
17
+ vi.clearAllMocks();
18
+
19
+ mockLLM = {
20
+ getJSON: vi.fn()
21
+ };
22
+
23
+ mockHolosphere = {
24
+ getAll: vi.fn().mockResolvedValue([]),
25
+ put: vi.fn().mockResolvedValue({ success: true })
26
+ };
27
+
28
+ aggregation = new SmartAggregation(mockLLM, mockHolosphere);
29
+ });
30
+
31
+ describe('Constructor', () => {
32
+ it('should initialize with LLM service', () => {
33
+ const a = new SmartAggregation(mockLLM);
34
+ expect(a.llm).toBe(mockLLM);
35
+ expect(a.holosphere).toBeNull();
36
+ });
37
+
38
+ it('should accept optional HoloSphere instance', () => {
39
+ expect(aggregation.holosphere).toBe(mockHolosphere);
40
+ });
41
+ });
42
+
43
+ describe('setHoloSphere', () => {
44
+ it('should set HoloSphere instance', () => {
45
+ const a = new SmartAggregation(mockLLM);
46
+ a.setHoloSphere(mockHolosphere);
47
+ expect(a.holosphere).toBe(mockHolosphere);
48
+ });
49
+ });
50
+
51
+ describe('smartUpcast', () => {
52
+ it('should generate summary and upcast to parent', async () => {
53
+ mockHolosphere.getAll.mockResolvedValue([
54
+ { id: 1, title: 'Item 1' },
55
+ { id: 2, title: 'Item 2' }
56
+ ]);
57
+
58
+ mockLLM.getJSON.mockResolvedValue({
59
+ text: 'Two items in region',
60
+ stats: { count: 2 },
61
+ notable: ['Item 1'],
62
+ themes: ['development']
63
+ });
64
+
65
+ const result = await aggregation.smartUpcast('holon1', 'projects');
66
+
67
+ expect(result.holon).toBe('holon1');
68
+ expect(result.lens).toBe('projects');
69
+ expect(result.summary).toBeDefined();
70
+ expect(result.dataCount).toBe(2);
71
+ expect(result.parent).toBeDefined();
72
+ });
73
+
74
+ it('should throw error if HoloSphere not available', async () => {
75
+ const a = new SmartAggregation(mockLLM);
76
+
77
+ await expect(a.smartUpcast('holon', 'lens'))
78
+ .rejects.toThrow('HoloSphere instance required');
79
+ });
80
+
81
+ it('should store summary at parent level when storeResults is true', async () => {
82
+ mockHolosphere.getAll.mockResolvedValue([{ id: 1 }]);
83
+ mockLLM.getJSON.mockResolvedValue({ text: 'Summary' });
84
+
85
+ await aggregation.smartUpcast('holon1', 'projects', { storeResults: true });
86
+
87
+ expect(mockHolosphere.put).toHaveBeenCalledWith(
88
+ 'parent_holon',
89
+ 'projects_summaries',
90
+ expect.objectContaining({
91
+ childHolon: 'holon1',
92
+ summary: expect.any(Object)
93
+ })
94
+ );
95
+ });
96
+
97
+ it('should respect maxLevels option', async () => {
98
+ mockHolosphere.getAll.mockResolvedValue([{ id: 1 }]);
99
+ mockLLM.getJSON.mockResolvedValue({ text: 'Summary' });
100
+
101
+ const result = await aggregation.smartUpcast('holon1', 'projects', { maxLevels: 1 });
102
+
103
+ // Should only go up one level
104
+ expect(result.parent).toBeDefined();
105
+ });
106
+ });
107
+
108
+ describe('_generateSummary', () => {
109
+ it('should generate summary for data', async () => {
110
+ mockLLM.getJSON.mockResolvedValue({
111
+ text: 'Active region',
112
+ stats: { count: 5 },
113
+ notable: ['Project A'],
114
+ themes: ['sustainability']
115
+ });
116
+
117
+ const data = [{ id: 1 }, { id: 2 }];
118
+ const result = await aggregation._generateSummary(data, 'holon1', 'projects');
119
+
120
+ expect(result.text).toBe('Active region');
121
+ expect(result.stats.count).toBe(5);
122
+ });
123
+
124
+ it('should return empty summary for empty data', async () => {
125
+ const result = await aggregation._generateSummary([], 'holon1', 'lens');
126
+
127
+ expect(result.text).toBe('No data in this region');
128
+ expect(result.stats.count).toBe(0);
129
+ });
130
+ });
131
+
132
+ describe('generateHolonSummary', () => {
133
+ it('should generate comprehensive holon summary', async () => {
134
+ mockHolosphere.getAll
135
+ .mockResolvedValueOnce([{ id: 1 }]) // projects
136
+ .mockResolvedValueOnce([{ id: 2 }]) // quests
137
+ .mockResolvedValueOnce([]) // events
138
+ .mockResolvedValueOnce([]) // resources
139
+ .mockResolvedValueOnce([{ id: 3 }]); // default
140
+
141
+ mockLLM.getJSON.mockResolvedValue({
142
+ title: 'Region Summary',
143
+ executive_summary: 'Active region',
144
+ highlights: ['Growth'],
145
+ activities: { count: 2, summary: 'Various activities' },
146
+ community: { engagement_level: 'high', notes: 'Active community' },
147
+ resources: ['Local resources'],
148
+ challenges: ['Coordination'],
149
+ opportunities: ['Expansion'],
150
+ health_score: 0.8,
151
+ recommendations: ['Focus on coordination']
152
+ });
153
+
154
+ const result = await aggregation.generateHolonSummary('holon1');
155
+
156
+ expect(result.holon).toBe('holon1');
157
+ expect(result.summary.title).toBe('Region Summary');
158
+ expect(result.lensesAnalyzed).toBeDefined();
159
+ expect(result.totalItems).toBeGreaterThan(0);
160
+ });
161
+
162
+ it('should throw error if HoloSphere not available', async () => {
163
+ const a = new SmartAggregation(mockLLM);
164
+
165
+ await expect(a.generateHolonSummary('holon'))
166
+ .rejects.toThrow('HoloSphere instance required');
167
+ });
168
+
169
+ it('should skip unavailable lenses', async () => {
170
+ mockHolosphere.getAll
171
+ .mockResolvedValueOnce([{ id: 1 }])
172
+ .mockRejectedValueOnce(new Error('Lens not found'))
173
+ .mockResolvedValueOnce([])
174
+ .mockResolvedValueOnce([])
175
+ .mockResolvedValueOnce([]);
176
+
177
+ mockLLM.getJSON.mockResolvedValue({ title: 'Summary' });
178
+
179
+ const result = await aggregation.generateHolonSummary('holon1');
180
+
181
+ expect(result).toBeDefined();
182
+ });
183
+ });
184
+
185
+ describe('aggregateChildren', () => {
186
+ it('should aggregate summaries from child holons', async () => {
187
+ mockHolosphere.getAll
188
+ .mockResolvedValueOnce([{ id: 1 }])
189
+ .mockResolvedValueOnce([{ id: 2 }])
190
+ .mockResolvedValueOnce([{ id: 3 }]);
191
+
192
+ mockLLM.getJSON.mockResolvedValue({
193
+ text: 'Summary',
194
+ stats: { count: 1 }
195
+ });
196
+
197
+ const result = await aggregation.aggregateChildren('parent_holon', 'projects');
198
+
199
+ expect(result.holon).toBe('parent_holon');
200
+ expect(result.lens).toBe('projects');
201
+ expect(result.childCount).toBeGreaterThan(0);
202
+ });
203
+
204
+ it('should throw error if HoloSphere not available', async () => {
205
+ const a = new SmartAggregation(mockLLM);
206
+
207
+ await expect(a.aggregateChildren('parent', 'lens'))
208
+ .rejects.toThrow('HoloSphere instance required');
209
+ });
210
+
211
+ it('should return message when no child data found', async () => {
212
+ mockHolosphere.getAll.mockResolvedValue([]);
213
+
214
+ const result = await aggregation.aggregateChildren('parent_holon', 'projects');
215
+
216
+ expect(result.message).toBe('No child data found');
217
+ expect(result.summary).toBeNull();
218
+ });
219
+ });
220
+
221
+ describe('comparePeriods', () => {
222
+ it('should compare data across time periods', async () => {
223
+ mockHolosphere.getAll.mockResolvedValue([
224
+ { id: 1, timestamp: '2024-01-15' },
225
+ { id: 2, timestamp: '2024-06-15' },
226
+ { id: 3, timestamp: '2024-08-15' }
227
+ ]);
228
+
229
+ mockLLM.getJSON.mockResolvedValue({
230
+ growth_rate: 50,
231
+ direction: 'growth',
232
+ new_themes: ['sustainability'],
233
+ disappeared: ['legacy'],
234
+ key_changes: ['Focus shift'],
235
+ summary: 'Growing activity'
236
+ });
237
+
238
+ const result = await aggregation.comparePeriods(
239
+ 'holon1',
240
+ 'projects',
241
+ { start: '2024-01-01', end: '2024-03-31' },
242
+ { start: '2024-07-01', end: '2024-09-30' }
243
+ );
244
+
245
+ expect(result.growth_rate).toBe(50);
246
+ expect(result.direction).toBe('growth');
247
+ });
248
+
249
+ it('should throw error if HoloSphere not available', async () => {
250
+ const a = new SmartAggregation(mockLLM);
251
+
252
+ await expect(a.comparePeriods('h', 'l', {}, {}))
253
+ .rejects.toThrow('HoloSphere instance required');
254
+ });
255
+
256
+ it('should filter items by period', async () => {
257
+ mockHolosphere.getAll.mockResolvedValue([
258
+ { id: 1, timestamp: '2024-01-15' },
259
+ { id: 2, timestamp: '2024-02-15' },
260
+ { id: 3, timestamp: '2024-06-15' }
261
+ ]);
262
+
263
+ mockLLM.getJSON.mockResolvedValue({ growth_rate: 0 });
264
+
265
+ await aggregation.comparePeriods(
266
+ 'holon1',
267
+ 'lens',
268
+ { start: '2024-01-01', end: '2024-03-31' },
269
+ { start: '2024-06-01', end: '2024-06-30' }
270
+ );
271
+
272
+ const call = mockLLM.getJSON.mock.calls[0];
273
+ expect(call[1]).toContain('Period 1 data');
274
+ expect(call[1]).toContain('Period 2 data');
275
+ });
276
+
277
+ it('should handle items with created_at field', async () => {
278
+ mockHolosphere.getAll.mockResolvedValue([
279
+ { id: 1, created_at: '2024-01-15' },
280
+ { id: 2, created_at: '2024-06-15' }
281
+ ]);
282
+
283
+ mockLLM.getJSON.mockResolvedValue({ growth_rate: 100 });
284
+
285
+ await aggregation.comparePeriods(
286
+ 'holon1',
287
+ 'lens',
288
+ { start: '2024-01-01', end: '2024-03-31' },
289
+ { start: '2024-06-01', end: '2024-06-30' }
290
+ );
291
+
292
+ expect(mockLLM.getJSON).toHaveBeenCalled();
293
+ });
294
+ });
295
+ });