holosphere 1.1.20 → 2.0.0-alpha0

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 (146) hide show
  1. package/.env.example +36 -0
  2. package/.eslintrc.json +16 -0
  3. package/.prettierrc.json +7 -0
  4. package/README.md +483 -367
  5. package/bin/holosphere-activitypub.js +158 -0
  6. package/cleanup-test-data.js +204 -0
  7. package/examples/demo.html +1333 -0
  8. package/examples/example-bot.js +197 -0
  9. package/package.json +47 -87
  10. package/scripts/check-bundle-size.js +54 -0
  11. package/scripts/check-quest-ids.js +77 -0
  12. package/scripts/import-holons.js +578 -0
  13. package/scripts/publish-to-relay.js +101 -0
  14. package/scripts/read-example.js +186 -0
  15. package/scripts/relay-diagnostic.js +59 -0
  16. package/scripts/relay-example.js +179 -0
  17. package/scripts/resync-to-relay.js +245 -0
  18. package/scripts/revert-import.js +196 -0
  19. package/scripts/test-hybrid-mode.js +108 -0
  20. package/scripts/test-local-storage.js +63 -0
  21. package/scripts/test-nostr-direct.js +55 -0
  22. package/scripts/test-read-data.js +45 -0
  23. package/scripts/test-write-read.js +63 -0
  24. package/scripts/verify-import.js +95 -0
  25. package/scripts/verify-relay-data.js +139 -0
  26. package/src/ai/aggregation.js +319 -0
  27. package/src/ai/breakdown.js +511 -0
  28. package/src/ai/classifier.js +217 -0
  29. package/src/ai/council.js +228 -0
  30. package/src/ai/embeddings.js +279 -0
  31. package/src/ai/federation-ai.js +324 -0
  32. package/src/ai/h3-ai.js +955 -0
  33. package/src/ai/index.js +112 -0
  34. package/src/ai/json-ops.js +225 -0
  35. package/src/ai/llm-service.js +205 -0
  36. package/src/ai/nl-query.js +223 -0
  37. package/src/ai/relationships.js +353 -0
  38. package/src/ai/schema-extractor.js +218 -0
  39. package/src/ai/spatial.js +293 -0
  40. package/src/ai/tts.js +194 -0
  41. package/src/content/social-protocols.js +168 -0
  42. package/src/core/holosphere.js +273 -0
  43. package/src/crypto/secp256k1.js +259 -0
  44. package/src/federation/discovery.js +334 -0
  45. package/src/federation/hologram.js +1042 -0
  46. package/src/federation/registry.js +386 -0
  47. package/src/hierarchical/upcast.js +110 -0
  48. package/src/index.js +2669 -0
  49. package/src/schema/validator.js +91 -0
  50. package/src/spatial/h3-operations.js +110 -0
  51. package/src/storage/backend-factory.js +125 -0
  52. package/src/storage/backend-interface.js +142 -0
  53. package/src/storage/backends/activitypub/server.js +653 -0
  54. package/src/storage/backends/activitypub-backend.js +272 -0
  55. package/src/storage/backends/gundb-backend.js +233 -0
  56. package/src/storage/backends/nostr-backend.js +136 -0
  57. package/src/storage/filesystem-storage-browser.js +41 -0
  58. package/src/storage/filesystem-storage.js +138 -0
  59. package/src/storage/global-tables.js +81 -0
  60. package/src/storage/gun-async.js +281 -0
  61. package/src/storage/gun-wrapper.js +221 -0
  62. package/src/storage/indexeddb-storage.js +122 -0
  63. package/src/storage/key-storage-simple.js +76 -0
  64. package/src/storage/key-storage.js +136 -0
  65. package/src/storage/memory-storage.js +59 -0
  66. package/src/storage/migration.js +338 -0
  67. package/src/storage/nostr-async.js +811 -0
  68. package/src/storage/nostr-client.js +939 -0
  69. package/src/storage/nostr-wrapper.js +211 -0
  70. package/src/storage/outbox-queue.js +208 -0
  71. package/src/storage/persistent-storage.js +109 -0
  72. package/src/storage/sync-service.js +164 -0
  73. package/src/subscriptions/manager.js +142 -0
  74. package/test-ai-real-api.js +202 -0
  75. package/tests/unit/ai/aggregation.test.js +295 -0
  76. package/tests/unit/ai/breakdown.test.js +446 -0
  77. package/tests/unit/ai/classifier.test.js +294 -0
  78. package/tests/unit/ai/council.test.js +262 -0
  79. package/tests/unit/ai/embeddings.test.js +384 -0
  80. package/tests/unit/ai/federation-ai.test.js +344 -0
  81. package/tests/unit/ai/h3-ai.test.js +458 -0
  82. package/tests/unit/ai/index.test.js +304 -0
  83. package/tests/unit/ai/json-ops.test.js +307 -0
  84. package/tests/unit/ai/llm-service.test.js +390 -0
  85. package/tests/unit/ai/nl-query.test.js +383 -0
  86. package/tests/unit/ai/relationships.test.js +311 -0
  87. package/tests/unit/ai/schema-extractor.test.js +384 -0
  88. package/tests/unit/ai/spatial.test.js +279 -0
  89. package/tests/unit/ai/tts.test.js +279 -0
  90. package/tests/unit/content.test.js +332 -0
  91. package/tests/unit/contract/core.test.js +88 -0
  92. package/tests/unit/contract/crypto.test.js +198 -0
  93. package/tests/unit/contract/data.test.js +223 -0
  94. package/tests/unit/contract/federation.test.js +181 -0
  95. package/tests/unit/contract/hierarchical.test.js +113 -0
  96. package/tests/unit/contract/schema.test.js +114 -0
  97. package/tests/unit/contract/social.test.js +217 -0
  98. package/tests/unit/contract/spatial.test.js +110 -0
  99. package/tests/unit/contract/subscriptions.test.js +128 -0
  100. package/tests/unit/contract/utils.test.js +159 -0
  101. package/tests/unit/core.test.js +152 -0
  102. package/tests/unit/crypto.test.js +328 -0
  103. package/tests/unit/federation.test.js +234 -0
  104. package/tests/unit/gun-async.test.js +252 -0
  105. package/tests/unit/hierarchical.test.js +399 -0
  106. package/tests/unit/integration/scenario-01-geographic-storage.test.js +74 -0
  107. package/tests/unit/integration/scenario-02-federation.test.js +76 -0
  108. package/tests/unit/integration/scenario-03-subscriptions.test.js +102 -0
  109. package/tests/unit/integration/scenario-04-validation.test.js +129 -0
  110. package/tests/unit/integration/scenario-05-hierarchy.test.js +125 -0
  111. package/tests/unit/integration/scenario-06-social.test.js +135 -0
  112. package/tests/unit/integration/scenario-07-persistence.test.js +130 -0
  113. package/tests/unit/integration/scenario-08-authorization.test.js +161 -0
  114. package/tests/unit/integration/scenario-09-cross-dimensional.test.js +139 -0
  115. package/tests/unit/integration/scenario-10-cross-holosphere-capabilities.test.js +357 -0
  116. package/tests/unit/integration/scenario-11-cross-holosphere-federation.test.js +410 -0
  117. package/tests/unit/integration/scenario-12-capability-federated-read.test.js +719 -0
  118. package/tests/unit/performance/benchmark.test.js +85 -0
  119. package/tests/unit/schema.test.js +213 -0
  120. package/tests/unit/spatial.test.js +158 -0
  121. package/tests/unit/storage.test.js +195 -0
  122. package/tests/unit/subscriptions.test.js +328 -0
  123. package/tests/unit/test-data-permanence-debug.js +197 -0
  124. package/tests/unit/test-data-permanence.js +340 -0
  125. package/tests/unit/test-key-persistence-fixed.js +148 -0
  126. package/tests/unit/test-key-persistence.js +172 -0
  127. package/tests/unit/test-relay-permanence.js +376 -0
  128. package/tests/unit/test-second-node.js +95 -0
  129. package/tests/unit/test-simple-write.js +89 -0
  130. package/vite.config.js +49 -0
  131. package/vitest.config.js +20 -0
  132. package/FEDERATION.md +0 -213
  133. package/compute.js +0 -298
  134. package/content.js +0 -980
  135. package/federation.js +0 -1234
  136. package/global.js +0 -736
  137. package/hexlib.js +0 -335
  138. package/hologram.js +0 -183
  139. package/holosphere-bundle.esm.js +0 -33256
  140. package/holosphere-bundle.js +0 -33287
  141. package/holosphere-bundle.min.js +0 -39
  142. package/holosphere.d.ts +0 -601
  143. package/holosphere.js +0 -719
  144. package/node.js +0 -246
  145. package/schema.js +0 -139
  146. package/utils.js +0 -302
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Verify what data is actually on the relay
5
+ */
6
+
7
+ import { HoloSphere } from '../src/index.js';
8
+ import dotenv from 'dotenv';
9
+
10
+ dotenv.config();
11
+
12
+ async function main() {
13
+ console.log('Verifying Relay Data');
14
+ console.log('====================\n');
15
+
16
+ const holonId = process.argv[2] || '235114395';
17
+ const relayUrl = process.env.HOLOSPHERE_RELAY || 'wss://relay.holons.io';
18
+
19
+ // First, check what we have locally
20
+ console.log('Step 1: Checking local storage...\n');
21
+ const hsLocal = new HoloSphere({
22
+ appName: 'Holons',
23
+ relays: [],
24
+ persistence: true,
25
+ logLevel: 'ERROR',
26
+ privateKey: process.env.HOLOSPHERE_PRIVATE_KEY
27
+ });
28
+
29
+ await new Promise(resolve => setTimeout(resolve, 2000));
30
+
31
+ const localQuests = await hsLocal.read(holonId, 'quests');
32
+ const localQuestIds = localQuests ? Object.keys(localQuests).sort() : [];
33
+ console.log(`Local storage: ${localQuestIds.length} quests`);
34
+
35
+ // Now check what's on the relay
36
+ console.log(`\nStep 2: Checking relay (${relayUrl})...\n`);
37
+ const hsRelay = new HoloSphere({
38
+ appName: 'Holons',
39
+ relays: [relayUrl],
40
+ persistence: false, // Don't load from local storage
41
+ logLevel: 'ERROR',
42
+ privateKey: process.env.HOLOSPHERE_PRIVATE_KEY
43
+ });
44
+
45
+ // Wait for relay connection
46
+ await new Promise(resolve => setTimeout(resolve, 2000));
47
+
48
+ const relayQuests = await hsRelay.read(holonId, 'quests');
49
+ const relayQuestIds = relayQuests ? Object.keys(relayQuests).sort() : [];
50
+ console.log(`Relay storage: ${relayQuestIds.length} quests`);
51
+
52
+ // Compare
53
+ console.log('\n' + '='.repeat(60));
54
+ console.log('Comparison');
55
+ console.log('='.repeat(60) + '\n');
56
+
57
+ console.log(`Local: ${localQuestIds.length} quests`);
58
+ console.log(`Relay: ${relayQuestIds.length} quests`);
59
+ console.log(`Missing from relay: ${localQuestIds.length - relayQuestIds.length} quests\n`);
60
+
61
+ // Find which quests are missing from relay
62
+ const missingFromRelay = localQuestIds.filter(id => !relayQuestIds.includes(id));
63
+
64
+ if (missingFromRelay.length > 0) {
65
+ console.log(`Missing quest IDs (first 20):`);
66
+ missingFromRelay.slice(0, 20).forEach(id => {
67
+ const quest = localQuests[id];
68
+ console.log(` - ${id}: ${quest.title || 'N/A'}`);
69
+ });
70
+
71
+ if (missingFromRelay.length > 20) {
72
+ console.log(` ... and ${missingFromRelay.length - 20} more`);
73
+ }
74
+ }
75
+
76
+ // Check if there are any on relay that aren't local
77
+ const onlyOnRelay = relayQuestIds.filter(id => !localQuestIds.includes(id));
78
+ if (onlyOnRelay.length > 0) {
79
+ console.log(`\nQuests on relay but not local: ${onlyOnRelay.length}`);
80
+ onlyOnRelay.forEach(id => {
81
+ console.log(` - ${id}`);
82
+ });
83
+ }
84
+
85
+ // Check other lenses
86
+ console.log('\n' + '='.repeat(60));
87
+ console.log('All Lenses Comparison');
88
+ console.log('='.repeat(60) + '\n');
89
+
90
+ const lenses = ['3891', 'announcements', 'checklists', 'expenses', 'linked_quests',
91
+ 'profile', 'quests', 'roles', 'settings', 'users'];
92
+
93
+ console.log('Lens'.padEnd(20) + 'Local'.padEnd(10) + 'Relay'.padEnd(10) + 'Missing');
94
+ console.log('-'.repeat(60));
95
+
96
+ for (const lens of lenses) {
97
+ const localData = await hsLocal.read(holonId, lens);
98
+ const relayData = await hsRelay.read(holonId, lens);
99
+
100
+ const localCount = localData ? Object.keys(localData).length : 0;
101
+ const relayCount = relayData ? Object.keys(relayData).length : 0;
102
+ const missing = localCount - relayCount;
103
+
104
+ console.log(
105
+ lens.padEnd(20) +
106
+ localCount.toString().padEnd(10) +
107
+ relayCount.toString().padEnd(10) +
108
+ missing.toString()
109
+ );
110
+ }
111
+
112
+ // Test a few specific quests that we know should exist
113
+ console.log('\n' + '='.repeat(60));
114
+ console.log('Testing Specific Quest Retrieval from Relay');
115
+ console.log('='.repeat(60) + '\n');
116
+
117
+ const testQuestIds = ['1002', '10026', '1073', '1005', '1011'];
118
+
119
+ for (const questId of testQuestIds) {
120
+ const inLocal = localQuests && localQuests[questId];
121
+ const inRelay = relayQuests && relayQuests[questId];
122
+
123
+ console.log(`Quest ${questId}:`);
124
+ console.log(` Local: ${inLocal ? '✓ Found' : '✗ Not found'}`);
125
+ console.log(` Relay: ${inRelay ? '✓ Found' : '✗ Not found'}`);
126
+ if (inLocal) {
127
+ console.log(` Title: ${inLocal.title || 'N/A'}`);
128
+ }
129
+ console.log('');
130
+ }
131
+
132
+ console.log('✓ Verification complete\n');
133
+ process.exit(0);
134
+ }
135
+
136
+ main().catch(error => {
137
+ console.error('Error:', error);
138
+ process.exit(1);
139
+ });
@@ -0,0 +1,319 @@
1
+ /**
2
+ * Smart Aggregation - AI-assisted hierarchical data aggregation up the holon tree
3
+ */
4
+
5
+ import * as h3 from 'h3-js';
6
+
7
+ /**
8
+ * Smart Aggregation class
9
+ */
10
+ export class SmartAggregation {
11
+ /**
12
+ * @param {LLMService} llmService - LLM service instance
13
+ * @param {Object} holosphere - HoloSphere instance
14
+ */
15
+ constructor(llmService, holosphere = null) {
16
+ this.llm = llmService;
17
+ this.holosphere = holosphere;
18
+ }
19
+
20
+ /**
21
+ * Set HoloSphere instance
22
+ * @param {Object} holosphere - HoloSphere instance
23
+ */
24
+ setHoloSphere(holosphere) {
25
+ this.holosphere = holosphere;
26
+ }
27
+
28
+ /**
29
+ * Smart upcast - AI-summarized hierarchical aggregation
30
+ * @param {string} holon - Starting holon
31
+ * @param {string} lens - Lens to aggregate
32
+ * @param {Object} options - Options
33
+ * @returns {Promise<Object>} Aggregation result
34
+ */
35
+ async smartUpcast(holon, lens, options = {}) {
36
+ if (!this.holosphere) {
37
+ throw new Error('HoloSphere instance required');
38
+ }
39
+
40
+ const { maxLevels = 5, storeResults = true } = options;
41
+
42
+ // Get current data
43
+ const data = await this.holosphere.getAll(holon, lens);
44
+
45
+ // Generate summary for current level
46
+ const summary = await this._generateSummary(data, holon, lens);
47
+
48
+ // Get parent holon
49
+ const resolution = h3.getResolution(holon);
50
+ if (resolution <= 0 || maxLevels <= 0) {
51
+ return { holon, lens, summary, level: resolution };
52
+ }
53
+
54
+ const parent = h3.cellToParent(holon, resolution - 1);
55
+
56
+ // Store summary at parent level
57
+ if (storeResults) {
58
+ await this.holosphere.put(parent, `${lens}_summaries`, {
59
+ id: holon,
60
+ childHolon: holon,
61
+ summary,
62
+ dataCount: data.length,
63
+ timestamp: Date.now(),
64
+ });
65
+ }
66
+
67
+ // Recursively upcast to parent
68
+ const parentResult = await this.smartUpcast(parent, lens, {
69
+ ...options,
70
+ maxLevels: maxLevels - 1,
71
+ });
72
+
73
+ return {
74
+ holon,
75
+ lens,
76
+ summary,
77
+ dataCount: data.length,
78
+ parent: parentResult,
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Generate AI summary for data
84
+ * @private
85
+ */
86
+ async _generateSummary(data, holon, lens) {
87
+ if (data.length === 0) {
88
+ return { text: 'No data in this region', stats: { count: 0 } };
89
+ }
90
+
91
+ const systemPrompt = `You are a data summarization expert. Create a concise summary of this geographic data.
92
+
93
+ Region: ${holon}
94
+ Category: ${lens}
95
+ Items: ${data.length}
96
+
97
+ Generate:
98
+ 1. A brief text summary (2-3 sentences)
99
+ 2. Key statistics
100
+ 3. Notable items
101
+ 4. Themes/patterns
102
+
103
+ Return JSON:
104
+ {
105
+ "text": "summary text",
106
+ "stats": {"count": n, "key_metric": value},
107
+ "notable": ["item1", "item2"],
108
+ "themes": ["theme1", "theme2"]
109
+ }`;
110
+
111
+ return this.llm.getJSON(
112
+ systemPrompt,
113
+ JSON.stringify(data.slice(0, 30), null, 2),
114
+ { temperature: 0.3 }
115
+ );
116
+ }
117
+
118
+ /**
119
+ * Generate comprehensive holon summary
120
+ * @param {string} holon - Holon identifier
121
+ * @returns {Promise<Object>} Comprehensive summary
122
+ */
123
+ async generateHolonSummary(holon) {
124
+ if (!this.holosphere) {
125
+ throw new Error('HoloSphere instance required');
126
+ }
127
+
128
+ // Get all available lenses (would need to discover)
129
+ const lenses = ['projects', 'quests', 'events', 'resources', 'default'];
130
+ const allData = {};
131
+
132
+ for (const lens of lenses) {
133
+ try {
134
+ const data = await this.holosphere.getAll(holon, lens);
135
+ if (data.length > 0) {
136
+ allData[lens] = data;
137
+ }
138
+ } catch {
139
+ // Skip unavailable lenses
140
+ }
141
+ }
142
+
143
+ const systemPrompt = `You are a regional analyst. Generate a comprehensive summary of this geographic area.
144
+
145
+ Region: ${holon}
146
+ Data available: ${Object.entries(allData).map(([l, d]) => `${l}: ${d.length}`).join(', ')}
147
+
148
+ Generate a complete overview including:
149
+ 1. Executive summary
150
+ 2. Key activities and projects
151
+ 3. Community engagement
152
+ 4. Resources and assets
153
+ 5. Challenges and opportunities
154
+ 6. Overall health assessment
155
+
156
+ Return JSON:
157
+ {
158
+ "title": "Region Title",
159
+ "executive_summary": "brief overview",
160
+ "highlights": ["highlight1", "highlight2"],
161
+ "activities": {"count": n, "summary": "text"},
162
+ "community": {"engagement_level": "high|medium|low", "notes": "text"},
163
+ "resources": ["resource1"],
164
+ "challenges": ["challenge1"],
165
+ "opportunities": ["opp1"],
166
+ "health_score": 0.0-1.0,
167
+ "recommendations": ["rec1"]
168
+ }`;
169
+
170
+ const dataSample = {};
171
+ for (const [lens, data] of Object.entries(allData)) {
172
+ dataSample[lens] = data.slice(0, 15);
173
+ }
174
+
175
+ const summary = await this.llm.getJSON(
176
+ systemPrompt,
177
+ JSON.stringify(dataSample, null, 2),
178
+ { temperature: 0.4, maxTokens: 2000 }
179
+ );
180
+
181
+ return {
182
+ holon,
183
+ summary,
184
+ lensesAnalyzed: Object.keys(allData),
185
+ totalItems: Object.values(allData).reduce((sum, d) => sum + d.length, 0),
186
+ timestamp: Date.now(),
187
+ };
188
+ }
189
+
190
+ /**
191
+ * Aggregate child summaries into parent
192
+ * @param {string} parentHolon - Parent holon
193
+ * @param {string} lens - Lens
194
+ * @returns {Promise<Object>} Aggregated summary
195
+ */
196
+ async aggregateChildren(parentHolon, lens) {
197
+ if (!this.holosphere) {
198
+ throw new Error('HoloSphere instance required');
199
+ }
200
+
201
+ // Get children holons
202
+ const resolution = h3.getResolution(parentHolon);
203
+ const children = h3.cellToChildren(parentHolon, resolution + 1);
204
+
205
+ // Collect summaries from children
206
+ const childSummaries = [];
207
+ for (const child of children.slice(0, 7)) { // Limit to 7 children for token limits
208
+ try {
209
+ const data = await this.holosphere.getAll(child, lens);
210
+ if (data.length > 0) {
211
+ const summary = await this._generateSummary(data, child, lens);
212
+ childSummaries.push({ holon: child, summary });
213
+ }
214
+ } catch {
215
+ // Skip
216
+ }
217
+ }
218
+
219
+ if (childSummaries.length === 0) {
220
+ return { holon: parentHolon, summary: null, message: 'No child data found' };
221
+ }
222
+
223
+ // Aggregate child summaries
224
+ const systemPrompt = `You are aggregating summaries from child regions into a parent region summary.
225
+
226
+ Parent region: ${parentHolon}
227
+ Child summaries: ${childSummaries.length}
228
+
229
+ Create an aggregated summary that:
230
+ 1. Synthesizes common themes
231
+ 2. Highlights regional diversity
232
+ 3. Identifies cross-cutting patterns
233
+ 4. Notes outliers
234
+
235
+ Return JSON:
236
+ {
237
+ "aggregated_summary": "text",
238
+ "common_themes": ["theme1"],
239
+ "regional_diversity": ["diff1"],
240
+ "patterns": ["pattern1"],
241
+ "total_activity": {"count": n, "trend": "growing|stable|declining"}
242
+ }`;
243
+
244
+ const summary = await this.llm.getJSON(
245
+ systemPrompt,
246
+ JSON.stringify(childSummaries, null, 2),
247
+ { temperature: 0.3 }
248
+ );
249
+
250
+ return {
251
+ holon: parentHolon,
252
+ lens,
253
+ childCount: childSummaries.length,
254
+ summary,
255
+ timestamp: Date.now(),
256
+ };
257
+ }
258
+
259
+ /**
260
+ * Compare aggregations across different time periods
261
+ * @param {string} holon - Holon
262
+ * @param {string} lens - Lens
263
+ * @param {Object} period1 - First period {start, end}
264
+ * @param {Object} period2 - Second period {start, end}
265
+ * @returns {Promise<Object>} Comparison
266
+ */
267
+ async comparePeriods(holon, lens, period1, period2) {
268
+ if (!this.holosphere) {
269
+ throw new Error('HoloSphere instance required');
270
+ }
271
+
272
+ const allData = await this.holosphere.getAll(holon, lens);
273
+
274
+ const filterByPeriod = (data, period) => {
275
+ return data.filter(item => {
276
+ const ts = item.timestamp || item.created_at;
277
+ if (!ts) return false;
278
+ const itemTime = new Date(ts).getTime();
279
+ return itemTime >= new Date(period.start).getTime() &&
280
+ itemTime <= new Date(period.end).getTime();
281
+ });
282
+ };
283
+
284
+ const data1 = filterByPeriod(allData, period1);
285
+ const data2 = filterByPeriod(allData, period2);
286
+
287
+ const systemPrompt = `Compare data between two time periods for a geographic region.
288
+
289
+ Region: ${holon}
290
+ Period 1: ${JSON.stringify(period1)} - ${data1.length} items
291
+ Period 2: ${JSON.stringify(period2)} - ${data2.length} items
292
+
293
+ Analyze:
294
+ 1. Growth/decline
295
+ 2. New themes in period 2
296
+ 3. Themes that disappeared
297
+ 4. Key changes
298
+
299
+ Return JSON:
300
+ {
301
+ "growth_rate": percentage,
302
+ "direction": "growth|decline|stable",
303
+ "new_themes": ["theme1"],
304
+ "disappeared": ["theme1"],
305
+ "key_changes": ["change1"],
306
+ "summary": "text"
307
+ }`;
308
+
309
+ const userMessage = `Period 1 data:
310
+ ${JSON.stringify(data1.slice(0, 20), null, 2)}
311
+
312
+ Period 2 data:
313
+ ${JSON.stringify(data2.slice(0, 20), null, 2)}`;
314
+
315
+ return this.llm.getJSON(systemPrompt, userMessage, { temperature: 0.3 });
316
+ }
317
+ }
318
+
319
+ export default SmartAggregation;