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
package/src/index.js ADDED
@@ -0,0 +1,2669 @@
1
+ /**
2
+ * HoloSphere - Holonic Geospatial Communication Infrastructure
3
+ * Public API
4
+ */
5
+
6
+ import { HoloSphere as HoloSphereCore } from './core/holosphere.js';
7
+ import * as spatial from './spatial/h3-operations.js';
8
+ import * as storage from './storage/nostr-wrapper.js';
9
+ import * as nostrAsync from './storage/nostr-async.js';
10
+ import * as globalTables from './storage/global-tables.js';
11
+ import * as schema from './schema/validator.js';
12
+ import { ValidationError } from './schema/validator.js';
13
+ import * as federation from './federation/hologram.js';
14
+ import * as crypto from './crypto/secp256k1.js';
15
+ import * as social from './content/social-protocols.js';
16
+ import * as subscriptions from './subscriptions/manager.js';
17
+ import * as hierarchical from './hierarchical/upcast.js';
18
+ import * as registry from './federation/registry.js';
19
+ import * as discovery from './federation/discovery.js';
20
+
21
+ // AI Module imports
22
+ import { LLMService } from './ai/llm-service.js';
23
+ import { SchemaExtractor } from './ai/schema-extractor.js';
24
+ import { JSONOps } from './ai/json-ops.js';
25
+ import { Embeddings } from './ai/embeddings.js';
26
+ import { Council } from './ai/council.js';
27
+ import { TTS, VOICES, MODELS } from './ai/tts.js';
28
+ import { NLQuery } from './ai/nl-query.js';
29
+ import { Classifier } from './ai/classifier.js';
30
+ import { SpatialAnalysis } from './ai/spatial.js';
31
+ import { SmartAggregation } from './ai/aggregation.js';
32
+ import { FederationAdvisor } from './ai/federation-ai.js';
33
+ import { RelationshipDiscovery } from './ai/relationships.js';
34
+ import { TaskBreakdown } from './ai/breakdown.js';
35
+ import { H3AI } from './ai/h3-ai.js';
36
+
37
+ /**
38
+ * Main HoloSphere class with all methods
39
+ */
40
+ export class HoloSphere extends HoloSphereCore {
41
+ constructor(config) {
42
+ super(config);
43
+ this.schemas = new Map();
44
+ this.subscriptionRegistry = new subscriptions.SubscriptionRegistry();
45
+
46
+ // Initialize AI services if openaiKey is provided or OPENAI_API_KEY env var is set
47
+ this._ai = null;
48
+ const openaiKey = config.openaiKey || this._getEnv('OPENAI_API_KEY');
49
+ if (openaiKey) {
50
+ const aiOptions = {
51
+ ...config.aiOptions,
52
+ model: config.aiOptions?.model || this._getEnv('HOLOSPHERE_AI_MODEL'),
53
+ temperature: config.aiOptions?.temperature ?? this._parseFloat(this._getEnv('HOLOSPHERE_AI_TEMPERATURE')),
54
+ };
55
+ this._initializeAI(openaiKey, aiOptions);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Get environment variable (works in Node.js)
61
+ * @private
62
+ */
63
+ _getEnv(name) {
64
+ if (typeof process !== 'undefined' && process.env) {
65
+ return process.env[name];
66
+ }
67
+ return undefined;
68
+ }
69
+
70
+ /**
71
+ * Parse float from string, return undefined if invalid
72
+ * @private
73
+ */
74
+ _parseFloat(value) {
75
+ if (value === undefined || value === null) return undefined;
76
+ const parsed = parseFloat(value);
77
+ return isNaN(parsed) ? undefined : parsed;
78
+ }
79
+
80
+ /**
81
+ * Initialize AI services
82
+ * @private
83
+ */
84
+ _initializeAI(apiKey, options = {}) {
85
+ // Build LLM options from config
86
+ const llmOptions = {
87
+ ...options.llm,
88
+ model: options.model || options.llm?.model,
89
+ temperature: options.temperature ?? options.llm?.temperature,
90
+ };
91
+
92
+ // Create shared LLM service
93
+ this._ai = {
94
+ llm: new LLMService(apiKey, llmOptions),
95
+ };
96
+
97
+ // Create OpenAI client for embeddings and TTS
98
+ const OpenAI = require('openai').default;
99
+ const openai = new OpenAI({ apiKey });
100
+ this._ai.openai = openai;
101
+
102
+ // Create all services
103
+ this._ai.embeddings = new Embeddings(openai, this);
104
+ this._ai.schemaExtractor = new SchemaExtractor(this._ai.llm);
105
+ this._ai.jsonOps = new JSONOps(this._ai.llm);
106
+ this._ai.council = new Council(this._ai.llm);
107
+ this._ai.tts = new TTS(openai);
108
+ this._ai.nlQuery = new NLQuery(this._ai.llm, this);
109
+ this._ai.classifier = new Classifier(this._ai.llm, this);
110
+ this._ai.spatial = new SpatialAnalysis(this._ai.llm, this);
111
+ this._ai.aggregation = new SmartAggregation(this._ai.llm, this);
112
+ this._ai.federationAdvisor = new FederationAdvisor(this._ai.llm, this, this._ai.embeddings);
113
+ this._ai.relationships = new RelationshipDiscovery(this._ai.llm, this, this._ai.embeddings);
114
+ this._ai.taskBreakdown = new TaskBreakdown(this._ai.llm, this);
115
+ this._ai.h3ai = new H3AI(this._ai.llm, this);
116
+ }
117
+
118
+ /**
119
+ * Check if AI services are initialized
120
+ * @returns {boolean}
121
+ */
122
+ hasAI() {
123
+ return this._ai !== null;
124
+ }
125
+
126
+ /**
127
+ * Get AI services object (for advanced usage)
128
+ * @returns {Object|null}
129
+ */
130
+ getAIServices() {
131
+ return this._ai;
132
+ }
133
+
134
+ // === Spatial Operations ===
135
+ async toHolon(lat, lng, resolution) {
136
+ return spatial.toHolon(lat, lng, resolution);
137
+ }
138
+
139
+ async getParents(holonId, maxResolution) {
140
+ return spatial.getParents(holonId, maxResolution);
141
+ }
142
+
143
+ async getChildren(holonId) {
144
+ return spatial.getChildren(holonId);
145
+ }
146
+
147
+ isValidH3(holonId) {
148
+ return spatial.isValidH3(holonId);
149
+ }
150
+
151
+ // === Data Operations ===
152
+ async write(holonId, lensName, data, options = {}) {
153
+ // Validate inputs
154
+ if (!holonId || typeof holonId !== 'string') {
155
+ throw new ValidationError('ValidationError: holonId must be a non-empty string');
156
+ }
157
+ if (!lensName || typeof lensName !== 'string') {
158
+ throw new ValidationError('ValidationError: lensName must be a non-empty string');
159
+ }
160
+ if (!data || typeof data !== 'object') {
161
+ throw new ValidationError('ValidationError: data must be an object');
162
+ }
163
+
164
+ // Check authorization if capability token provided
165
+ const capToken = options.capabilityToken || options.capability;
166
+ if (capToken) {
167
+ const authorized = await this.verifyCapability(
168
+ capToken,
169
+ 'write',
170
+ { holonId, lensName }
171
+ );
172
+ if (!authorized) {
173
+ throw new AuthorizationError('AuthorizationError: Invalid capability token for write operation', 'write');
174
+ }
175
+ }
176
+
177
+ // Auto-generate ID if not provided
178
+ if (!data.id) {
179
+ data.id = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
180
+ }
181
+
182
+ // Check if we're writing to an existing hologram location
183
+ const path = storage.buildPath(this.config.appName, holonId, lensName, data.id);
184
+ const existingData = await storage.read(this.client, path);
185
+
186
+ // If existing data is a hologram, split the write between local overrides and source
187
+ if (existingData && existingData.hologram === true && existingData.target) {
188
+ // Fields that define the hologram structure (never overwrite these)
189
+ const hologramStructureFields = ['hologram', 'soul', 'target', 'id', '_meta'];
190
+ // Fields that exist in the hologram as local overrides
191
+ const localOverrideFields = Object.keys(existingData).filter(k => !hologramStructureFields.includes(k));
192
+
193
+ // Split data: local fields stay local, everything else goes to source
194
+ const localData = {
195
+ ...existingData, // Keep hologram structure
196
+ };
197
+ const sourceUpdates = {};
198
+
199
+ for (const [key, value] of Object.entries(data)) {
200
+ if (hologramStructureFields.includes(key)) {
201
+ // Skip - don't overwrite hologram structure
202
+ continue;
203
+ } else if (key === '_hologram') {
204
+ // Skip - this is resolved metadata, not real data
205
+ continue;
206
+ } else if (localOverrideFields.includes(key)) {
207
+ // This field exists in the hologram - update locally
208
+ localData[key] = value;
209
+ } else {
210
+ // This field doesn't exist in hologram - update source
211
+ sourceUpdates[key] = value;
212
+ }
213
+ }
214
+
215
+ // Schema validation for source updates (if schema exists)
216
+ if (Object.keys(sourceUpdates).length > 0 && options.validate !== false && this.schemas.has(lensName)) {
217
+ // Read current source data to merge with updates for validation
218
+ const sourcePath = storage.buildPath(
219
+ existingData.target.appname || this.config.appName,
220
+ existingData.target.holonId,
221
+ existingData.target.lensName,
222
+ existingData.target.dataId
223
+ );
224
+ const currentSourceData = await storage.read(this.client, sourcePath);
225
+ const mergedSourceData = { ...currentSourceData, ...sourceUpdates };
226
+
227
+ const schemaObj = this.schemas.get(lensName);
228
+ const strict = options.strict !== undefined ? options.strict : schemaObj.strict;
229
+
230
+ if (schemaObj.schema && typeof schemaObj.schema === 'object' && !schemaObj.schema.$ref) {
231
+ schema.validate(mergedSourceData, schemaObj.schema, lensName, strict);
232
+ }
233
+ }
234
+
235
+ // Update local hologram with its override fields
236
+ const startTime = Date.now();
237
+ await storage.write(this.client, path, localData);
238
+
239
+ // Update source data with non-local fields (partial update, not replace)
240
+ if (Object.keys(sourceUpdates).length > 0) {
241
+ const sourceAppname = existingData.target.appname || this.config.appName;
242
+ const sourceHolonId = existingData.target.holonId;
243
+ const sourceLensName = existingData.target.lensName;
244
+ const sourceDataId = existingData.target.dataId;
245
+
246
+ const sourcePath = storage.buildPath(sourceAppname, sourceHolonId, sourceLensName, sourceDataId);
247
+ await storage.update(this.client, sourcePath, sourceUpdates);
248
+
249
+ // Refresh timestamps on all activeHolograms in source data's _meta
250
+ await federation.refreshActiveHolograms(
251
+ this.client,
252
+ sourceAppname,
253
+ sourceHolonId,
254
+ sourceLensName,
255
+ sourceDataId
256
+ );
257
+ }
258
+
259
+ const endTime = Date.now();
260
+ this._metrics.writes++;
261
+ if (!this._metrics.totalWriteTime) this._metrics.totalWriteTime = 0;
262
+ this._metrics.totalWriteTime += (endTime - startTime);
263
+
264
+ return true;
265
+ }
266
+
267
+ // Regular write (not a hologram) - proceed normally
268
+ // Add metadata
269
+ if (!data._meta) {
270
+ data._meta = {};
271
+ }
272
+ data._meta.timestamp = Date.now();
273
+
274
+ // Schema validation if exists
275
+ if (options.validate !== false && this.schemas.has(lensName)) {
276
+ const schemaObj = this.schemas.get(lensName);
277
+ const strict = options.strict !== undefined ? options.strict : schemaObj.strict;
278
+
279
+ // If schema is a URI reference
280
+ if (schemaObj.schema && typeof schemaObj.schema === 'object' && schemaObj.schema.$ref) {
281
+ // URI schemas cannot be validated - skip validation in both modes
282
+ // In a real implementation, URIs would be resolved first
283
+ // For now, we allow writes through (no validation possible)
284
+ } else if (schemaObj.schema && typeof schemaObj.schema === 'object') {
285
+ // Validate if schema is an actual object
286
+ schema.validate(data, schemaObj.schema, lensName, strict);
287
+ }
288
+ }
289
+
290
+ // Write to storage
291
+ const startTime = Date.now();
292
+ const success = await storage.write(this.client, path, data);
293
+ const endTime = Date.now();
294
+
295
+ if (success) {
296
+ this._metrics.writes++;
297
+ // Track average write time
298
+ if (!this._metrics.totalWriteTime) this._metrics.totalWriteTime = 0;
299
+ this._metrics.totalWriteTime += (endTime - startTime);
300
+
301
+ // Refresh timestamps on all activeHolograms in the source data's _meta
302
+ // This allows interfaces to detect updates automatically
303
+ if (data._meta && Array.isArray(data._meta.activeHolograms) && data._meta.activeHolograms.length > 0) {
304
+ const result = await federation.refreshActiveHolograms(
305
+ this.client,
306
+ this.config.appName,
307
+ holonId,
308
+ lensName,
309
+ data.id,
310
+ data // Pass the data we already have
311
+ );
312
+ // Update the written data with refreshed timestamps (already done in-place by refreshActiveHolograms)
313
+ if (result.refreshed > 0) {
314
+ // Re-write to persist the updated activeHolograms timestamps
315
+ await storage.write(this.client, path, result.sourceData);
316
+ }
317
+ }
318
+ }
319
+
320
+ return success;
321
+ }
322
+
323
+ /**
324
+ * Resolve holograms recursively
325
+ * @private
326
+ * @param {*} data - Data that may contain holograms (object, array, or primitive)
327
+ * @returns {Promise<*>} Resolved data
328
+ */
329
+ async _resolveHolograms(data) {
330
+ // Handle null/undefined
331
+ if (!data) return data;
332
+
333
+ // Handle arrays - resolve each item and filter out nulls
334
+ if (Array.isArray(data)) {
335
+ const resolved = [];
336
+ for (const item of data) {
337
+ const resolvedItem = await this._resolveHolograms(item);
338
+ // Only add non-null items to preserve array integrity
339
+ if (resolvedItem != null) {
340
+ resolved.push(resolvedItem);
341
+ }
342
+ }
343
+ return resolved;
344
+ }
345
+
346
+ // Handle objects
347
+ if (typeof data === 'object') {
348
+ // Check if this is a hologram - ONLY if explicitly marked with hologram:true
349
+ // Having a 'soul' property alone does NOT make it a hologram (soul is used for metadata/linking)
350
+ const isHologram = data.hologram === true;
351
+
352
+ if (isHologram) {
353
+ const resolved = await federation.resolveHologram(this.client, data);
354
+ // Recursively resolve the resolved data (in case it contains more holograms)
355
+ return resolved ? await this._resolveHolograms(resolved) : null;
356
+ }
357
+
358
+ // Not a hologram, but might contain holograms in nested properties
359
+ // For now, just return as-is to avoid deep traversal overhead
360
+ // If needed, we can add deep resolution later
361
+ return data;
362
+ }
363
+
364
+ // Primitive value
365
+ return data;
366
+ }
367
+
368
+ async read(holonId, lensName, dataId = null, options = {}) {
369
+ // Validate inputs
370
+ if (!holonId || typeof holonId !== 'string') {
371
+ throw new ValidationError('ValidationError: holonId must be a non-empty string');
372
+ }
373
+ if (!lensName || typeof lensName !== 'string') {
374
+ throw new ValidationError('ValidationError: lensName must be a non-empty string');
375
+ }
376
+
377
+ const startTime = Date.now();
378
+ this._metrics.reads++;
379
+
380
+ // Build the scope for capability lookup
381
+ const scope = { holonId, lensName };
382
+ if (dataId) scope.dataId = dataId;
383
+
384
+ // Get federated authors (partners who granted us read access)
385
+ // Default: federated=true unless explicitly disabled
386
+ const federated = options.federated !== false;
387
+ let authors = [this.client.publicKey]; // Always include our own data
388
+
389
+ if (federated) {
390
+ try {
391
+ const federatedAuthors = await registry.getFederatedAuthorsForScope(
392
+ this.client,
393
+ this.config.appName,
394
+ scope,
395
+ 'read'
396
+ );
397
+ // Add federated author pubKeys
398
+ for (const { pubKey } of federatedAuthors) {
399
+ if (!authors.includes(pubKey)) {
400
+ authors.push(pubKey);
401
+ }
402
+ }
403
+ } catch (error) {
404
+ // Silently continue with just our own data if federation lookup fails
405
+ console.debug('[read] Federation lookup failed, using local only:', error.message);
406
+ }
407
+ }
408
+
409
+ // Query options including federated authors
410
+ const queryOptions = {
411
+ authors,
412
+ includeAuthor: true, // Track which author each item came from
413
+ };
414
+
415
+ if (dataId) {
416
+ const path = storage.buildPath(this.config.appName, holonId, lensName, dataId);
417
+ const data = await storage.read(this.client, path, queryOptions);
418
+
419
+ // Filter out deleted items
420
+ if (data && data._deleted) {
421
+ const endTime = Date.now();
422
+ if (!this._metrics.totalReadTime) this._metrics.totalReadTime = 0;
423
+ this._metrics.totalReadTime += (endTime - startTime);
424
+ return null;
425
+ }
426
+
427
+ // Always resolve holograms
428
+ const resolved = await this._resolveHolograms(data);
429
+
430
+ const endTime = Date.now();
431
+ if (!this._metrics.totalReadTime) this._metrics.totalReadTime = 0;
432
+ this._metrics.totalReadTime += (endTime - startTime);
433
+ return resolved;
434
+ } else {
435
+ const path = storage.buildPath(this.config.appName, holonId, lensName);
436
+ const result = await storage.readAll(this.client, path, {
437
+ ...queryOptions,
438
+ hybrid: this.config.hybridMode
439
+ });
440
+
441
+ // Always resolve holograms in array results
442
+ const resolved = await this._resolveHolograms(result);
443
+
444
+ // Filter out deleted items
445
+ const filtered = Array.isArray(resolved)
446
+ ? resolved.filter(item => !item || !item._deleted)
447
+ : (resolved && resolved._deleted ? null : resolved);
448
+
449
+ const endTime = Date.now();
450
+ if (!this._metrics.totalReadTime) this._metrics.totalReadTime = 0;
451
+ this._metrics.totalReadTime += (endTime - startTime);
452
+ return filtered;
453
+ }
454
+ }
455
+
456
+ async update(holonId, lensName, dataId, updates, options = {}) {
457
+ // Check authorization if capability token provided
458
+ const capToken = options.capabilityToken || options.capability;
459
+ if (capToken) {
460
+ const authorized = await this.verifyCapability(
461
+ capToken,
462
+ 'write',
463
+ { holonId, lensName }
464
+ );
465
+ if (!authorized) {
466
+ throw new AuthorizationError('AuthorizationError: Invalid capability token for update operation', 'write');
467
+ }
468
+ }
469
+
470
+ // Read raw data (without resolving) to check if it's a hologram
471
+ const path = storage.buildPath(this.config.appName, holonId, lensName, dataId);
472
+ const rawData = await storage.read(this.client, path);
473
+
474
+ // If this is a hologram, split updates between local overrides and source
475
+ if (rawData && rawData.hologram === true && rawData.target) {
476
+ // Fields that are stored in the hologram itself (local overrides)
477
+ // These are fields that exist in the hologram beyond the core hologram structure
478
+ const hologramStructureFields = ['hologram', 'soul', 'target', 'id', '_meta'];
479
+ const localOverrideFields = Object.keys(rawData).filter(k => !hologramStructureFields.includes(k));
480
+
481
+ // Split updates: local fields stay local, everything else goes to source
482
+ const localUpdates = {};
483
+ const sourceUpdates = {};
484
+
485
+ for (const [key, value] of Object.entries(updates)) {
486
+ if (hologramStructureFields.includes(key)) {
487
+ // Skip hologram structure fields entirely
488
+ continue;
489
+ } else if (localOverrideFields.includes(key)) {
490
+ // This field exists in the hologram - update locally
491
+ localUpdates[key] = value;
492
+ } else {
493
+ // This field doesn't exist in hologram - update source
494
+ sourceUpdates[key] = value;
495
+ }
496
+ }
497
+
498
+ // Update local hologram overrides
499
+ if (Object.keys(localUpdates).length > 0) {
500
+ await storage.update(this.client, path, localUpdates);
501
+ }
502
+
503
+ // Update source data
504
+ if (Object.keys(sourceUpdates).length > 0) {
505
+ const sourcePath = storage.buildPath(
506
+ rawData.target.appname || this.config.appName,
507
+ rawData.target.holonId,
508
+ rawData.target.lensName,
509
+ rawData.target.dataId
510
+ );
511
+ await storage.update(this.client, sourcePath, sourceUpdates);
512
+ }
513
+
514
+ return true;
515
+ }
516
+
517
+ // Regular data (not a hologram) - proceed with normal update
518
+ // Schema validation if exists and updates data
519
+ if (options.validate !== false && this.schemas.has(lensName)) {
520
+ const existing = await this.read(holonId, lensName, dataId);
521
+ const merged = { ...existing, ...updates };
522
+ const schemaObj = this.schemas.get(lensName);
523
+ const strict = options.strict !== undefined ? options.strict : schemaObj.strict;
524
+
525
+ // If schema is a URI reference
526
+ if (schemaObj.schema && typeof schemaObj.schema === 'object' && schemaObj.schema.$ref) {
527
+ // URI schemas cannot be validated - skip validation in both modes
528
+ // In a real implementation, URIs would be resolved first
529
+ // For now, we allow updates through (no validation possible)
530
+ } else if (schemaObj.schema && typeof schemaObj.schema === 'object') {
531
+ schema.validate(merged, schemaObj.schema, lensName, strict);
532
+ }
533
+ }
534
+
535
+ return storage.update(this.client, path, updates);
536
+ }
537
+
538
+ async delete(holonId, lensName, dataId, options = {}) {
539
+ // Read existing data to check ownership and get activeHolograms
540
+ const existing = await this.read(holonId, lensName, dataId);
541
+
542
+ // If data doesn't exist, return false
543
+ if (!existing) {
544
+ return false;
545
+ }
546
+
547
+ // Check authorization
548
+ const capToken = options.capabilityToken || options.capability;
549
+ const creator = existing._creator || existing.owner;
550
+ const currentUser = this.client.publicKey;
551
+
552
+ // Authorization logic:
553
+ // 1. If capability token provided, verify it
554
+ // 2. Else if data has a creator/owner AND it's not the current user, deny
555
+ // 3. Else allow (no owner or current user is owner)
556
+
557
+ if (capToken) {
558
+ const authorized = await this.verifyCapability(
559
+ capToken,
560
+ 'delete',
561
+ { holonId, lensName }
562
+ );
563
+ if (!authorized) {
564
+ const error = new AuthorizationError('AuthorizationError: Invalid capability token for delete operation', 'delete');
565
+ throw error;
566
+ }
567
+ } else if (creator && creator !== currentUser) {
568
+ // Data has an owner and it's not the current user - require authorization
569
+ const error = new AuthorizationError('AuthorizationError: Delete operation requires capability token', 'delete');
570
+ throw error;
571
+ }
572
+ // else: no owner or current user is owner - allow deletion
573
+
574
+ // Delete all holograms that point to this data
575
+ if (existing._meta?.activeHolograms && Array.isArray(existing._meta.activeHolograms)) {
576
+ console.info(`🗑️ Deleting ${existing._meta.activeHolograms.length} holograms for ${holonId}/${lensName}/${dataId}`);
577
+ for (const hologramEntry of existing._meta.activeHolograms) {
578
+ try {
579
+ const hologramPath = storage.buildPath(
580
+ this.config.appName,
581
+ hologramEntry.targetHolon,
582
+ lensName,
583
+ dataId
584
+ );
585
+ await storage.deleteData(this.client, hologramPath);
586
+ console.info(` ✓ Deleted hologram in ${hologramEntry.targetHolon}`);
587
+ } catch (err) {
588
+ console.warn(` ⚠️ Failed to delete hologram in ${hologramEntry.targetHolon}:`, err.message);
589
+ }
590
+ }
591
+ }
592
+
593
+ const path = storage.buildPath(this.config.appName, holonId, lensName, dataId);
594
+ return storage.deleteData(this.client, path);
595
+ }
596
+
597
+ // === Global Data Operations ===
598
+ async writeGlobal(table, data) {
599
+ return globalTables.writeGlobal(this.client, this.config.appName, table, data);
600
+ }
601
+
602
+ async readGlobal(table, key = null) {
603
+ return globalTables.readGlobal(this.client, this.config.appName, table, key);
604
+ }
605
+
606
+ async updateGlobal(table, key, updates) {
607
+ return globalTables.updateGlobal(this.client, this.config.appName, table, key, updates);
608
+ }
609
+
610
+ async deleteGlobal(table, key) {
611
+ return globalTables.deleteGlobal(this.client, this.config.appName, table, key);
612
+ }
613
+
614
+ async getAllGlobal(table) {
615
+ return globalTables.readGlobal(this.client, this.config.appName, table);
616
+ }
617
+
618
+ async deleteAllGlobal(table) {
619
+ return globalTables.deleteAllGlobal(this.client, this.config.appName, table);
620
+ }
621
+
622
+ // === Holon Data Operations ===
623
+ async getAll(holonId, lensName) {
624
+ return this.read(holonId, lensName);
625
+ }
626
+
627
+ async deleteAll(holonId, lensName) {
628
+ const path = storage.buildPath(this.config.appName, holonId, lensName);
629
+ return storage.deleteAll(this.client, path);
630
+ }
631
+
632
+ // === Schema Operations ===
633
+ async setSchema(lensName, schemaObj, strict = false) {
634
+ let resolvedSchema = schemaObj;
635
+
636
+ // If schemaObj is a string (URI), store as reference
637
+ if (typeof schemaObj === 'string') {
638
+ // Store as URI reference - validation will happen when used
639
+ resolvedSchema = { $ref: schemaObj };
640
+ }
641
+
642
+ if (!resolvedSchema || (typeof resolvedSchema !== 'object' && typeof resolvedSchema !== 'boolean')) {
643
+ throw new ValidationError('Schema must be a valid JSON Schema object or boolean');
644
+ }
645
+
646
+ // Validate that it's a valid JSON Schema (basic check) - only for actual schemas, not refs
647
+ if (typeof resolvedSchema === 'object' && !resolvedSchema.$ref) {
648
+ if (!resolvedSchema.type && !resolvedSchema.properties && !resolvedSchema.items && resolvedSchema !== true && resolvedSchema !== false) {
649
+ throw new ValidationError('ValidationError: Invalid JSON Schema format - must have type, properties, or items');
650
+ }
651
+ }
652
+
653
+ this.schemas.set(lensName, { schema: resolvedSchema, strict });
654
+ }
655
+
656
+ async getSchema(lensName) {
657
+ const schemaObj = this.schemas.get(lensName);
658
+ return schemaObj ? schemaObj.schema : null;
659
+ }
660
+
661
+ async clearSchema(lensName) {
662
+ this.schemas.delete(lensName);
663
+ schema.clearSchemaCache(lensName);
664
+ }
665
+
666
+ // === Federation Operations ===
667
+ async federate(sourceHolon, targetHolon, lensName, options = {}) {
668
+ // Validate self-federation
669
+ if (sourceHolon === targetHolon) {
670
+ throw new Error('Cannot federate a holon with itself');
671
+ }
672
+
673
+ // Validate direction (if provided)
674
+ if (options.direction && !['inbound', 'outbound', 'bidirectional'].includes(options.direction)) {
675
+ throw new Error('Invalid federation direction - must be inbound, outbound, or bidirectional');
676
+ }
677
+
678
+ // Handle bidirectional as both inbound and outbound
679
+ if (options.direction === 'bidirectional') {
680
+ const { direction, ...restOptions } = options;
681
+ const outboundResult = await this.federate(sourceHolon, targetHolon, lensName, { ...restOptions, direction: 'outbound' });
682
+ const inboundResult = await this.federate(sourceHolon, targetHolon, lensName, { ...restOptions, direction: 'inbound' });
683
+ return outboundResult && inboundResult;
684
+ }
685
+
686
+ const result = await federation.setupFederation(
687
+ this.client,
688
+ this.config.appName,
689
+ sourceHolon,
690
+ targetHolon,
691
+ lensName,
692
+ options
693
+ );
694
+
695
+ if (result) {
696
+ if (!this._metrics.federations) this._metrics.federations = 0;
697
+ this._metrics.federations++;
698
+
699
+ // By default, propagate existing data when federation is set up
700
+ // Set propagateExisting: false to only federate future writes
701
+ const propagateExisting = options.propagateExisting !== false;
702
+
703
+ if (!propagateExisting) {
704
+ return result;
705
+ }
706
+
707
+ // Propagate existing data based on direction
708
+ // Default to 'outbound' (source -> target) if no direction specified
709
+ const direction = options.direction || 'outbound';
710
+ const mode = options.mode || 'reference';
711
+ const filter = options.filter;
712
+
713
+ if (direction === 'outbound') {
714
+ // Source -> Target: propagate data from source to target
715
+ // Check if receiver accepts this lens in their inbound
716
+ const targetFederation = await this.getFederation(targetHolon);
717
+ const receiverLensConfig = targetFederation?.lensConfig?.[sourceHolon];
718
+ if (receiverLensConfig && receiverLensConfig.inbound && !receiverLensConfig.inbound.includes(lensName)) {
719
+ console.log(`Skipping outbound propagation - ${targetHolon} doesn't accept lens '${lensName}' in inbound from ${sourceHolon}`);
720
+ } else {
721
+ // Read raw data to avoid resolving holograms
722
+ const path = storage.buildPath(this.config.appName, sourceHolon, lensName);
723
+ const sourceData = await storage.readAll(this.client, path);
724
+ const sourceArray = Array.isArray(sourceData) ? sourceData : (sourceData ? [sourceData] : []);
725
+
726
+ for (const item of sourceArray) {
727
+ if (item.id === '_federation') continue; // Skip federation config
728
+ // Skip items that are already holograms or from another source
729
+ if (item.hologram === true) continue;
730
+ if (item._meta && item._meta.source && item._meta.source !== sourceHolon) continue;
731
+ if (filter && !filter(item)) continue; // Apply filter if provided
732
+
733
+ await federation.propagateData(
734
+ this.client,
735
+ this.config.appName,
736
+ item,
737
+ sourceHolon,
738
+ targetHolon,
739
+ lensName,
740
+ mode
741
+ );
742
+ }
743
+ }
744
+ }
745
+
746
+ if (direction === 'inbound') {
747
+ // Target -> Source: propagate data from target to source
748
+ // Check if receiver (source) accepts this lens in their inbound
749
+ const sourceFederation = await this.getFederation(sourceHolon);
750
+ const receiverLensConfig = sourceFederation?.lensConfig?.[targetHolon];
751
+ if (receiverLensConfig && receiverLensConfig.inbound && !receiverLensConfig.inbound.includes(lensName)) {
752
+ console.log(`Skipping inbound propagation - ${sourceHolon} doesn't accept lens '${lensName}' in inbound from ${targetHolon}`);
753
+ } else {
754
+ // Read raw data to avoid resolving holograms
755
+ const path = storage.buildPath(this.config.appName, targetHolon, lensName);
756
+ const targetData = await storage.readAll(this.client, path);
757
+ const targetArray = Array.isArray(targetData) ? targetData : (targetData ? [targetData] : []);
758
+
759
+ for (const item of targetArray) {
760
+ if (item.id === '_federation') continue; // Skip federation config
761
+ // Skip items that are already holograms or from another source
762
+ if (item.hologram === true) continue;
763
+ if (item._meta && item._meta.source && item._meta.source !== targetHolon) continue;
764
+ if (filter && !filter(item)) continue; // Apply filter if provided
765
+
766
+ await federation.propagateData(
767
+ this.client,
768
+ this.config.appName,
769
+ item,
770
+ targetHolon,
771
+ sourceHolon,
772
+ lensName,
773
+ mode
774
+ );
775
+ }
776
+ }
777
+ }
778
+ }
779
+
780
+ return result;
781
+ }
782
+
783
+ async getFederatedData(holonId, lensName, options = {}) {
784
+ const { resolveHolograms = true } = options;
785
+
786
+ // For getFederatedData, we just return the local data
787
+ // The federation setup already propagates data (as holograms or copies)
788
+ // So all federated data should already be accessible locally
789
+
790
+ if (resolveHolograms) {
791
+ // Use normal read which auto-resolves holograms
792
+ const localData = await this.read(holonId, lensName);
793
+ const localArray = Array.isArray(localData) ? localData : (localData ? [localData] : []);
794
+
795
+ // Filter out federation config and nulls
796
+ return localArray.filter(item => item != null && item.id !== '_federation');
797
+ } else {
798
+ // Read without resolving holograms - need to read raw data
799
+ const path = storage.buildPath(this.config.appName, holonId, lensName);
800
+ const rawData = await storage.readAll(this.client, path);
801
+ const rawArray = Array.isArray(rawData) ? rawData : (rawData ? [rawData] : []);
802
+
803
+ // Filter out federation config
804
+ return rawArray.filter(item => item != null && item.id !== '_federation');
805
+ }
806
+ }
807
+
808
+ async unfederate(sourceHolon, targetHolon, lensName) {
809
+ // Delete federation config (idempotent - always returns true)
810
+ const path = storage.buildPath(this.config.appName, sourceHolon, lensName, '_federation');
811
+ await storage.deleteData(this.client, path);
812
+ return true;
813
+ }
814
+
815
+ /**
816
+ * Update local overrides on a hologram (e.g., position, pinned status)
817
+ * @param {string} holonId - Holon where the hologram lives
818
+ * @param {string} lensName - Lens name (e.g., 'quests')
819
+ * @param {string} dataId - Data ID of the hologram
820
+ * @param {Object} overrides - Local fields to update (e.g., { x: 100, y: 200 })
821
+ * @returns {Promise<boolean>} Success indicator
822
+ */
823
+ async updateHologramOverrides(holonId, lensName, dataId, overrides) {
824
+ return federation.updateHologramOverrides(
825
+ this.client,
826
+ this.config.appName,
827
+ holonId,
828
+ lensName,
829
+ dataId,
830
+ overrides
831
+ );
832
+ }
833
+
834
+ /**
835
+ * Create a hologram (lightweight reference) for federation
836
+ * @param {string} sourceHolon - Source holon ID where the original data lives
837
+ * @param {string} lensName - Lens name (e.g., 'quests')
838
+ * @param {Object} data - Data object (must have an 'id' property)
839
+ * @param {string} [targetHolon] - Optional target holon ID (defaults to sourceHolon)
840
+ * @returns {Object} Hologram object ready to be written to a holon
841
+ */
842
+ createHologram(sourceHolon, lensName, data, targetHolon = null) {
843
+ if (!data || !data.id) {
844
+ throw new Error('createHologram: data must have an id property');
845
+ }
846
+ return federation.createHologram(
847
+ sourceHolon,
848
+ targetHolon || sourceHolon,
849
+ lensName,
850
+ data.id,
851
+ this.config.appName
852
+ );
853
+ }
854
+
855
+ /**
856
+ * Get active holograms for a specific data item
857
+ * Returns the activeHolograms array from the source data's _meta
858
+ * @param {string} holonId - Source holon ID
859
+ * @param {string} lensName - Lens name (e.g., 'quests')
860
+ * @param {string} dataId - Data ID to get holograms for
861
+ * @returns {Promise<Object[]>} Array of hologram info objects with timestamps
862
+ */
863
+ async getActiveHolograms(holonId, lensName, dataId) {
864
+ const data = await this.read(holonId, lensName, dataId);
865
+ if (!data || !data._meta || !Array.isArray(data._meta.activeHolograms)) {
866
+ return [];
867
+ }
868
+ return data._meta.activeHolograms;
869
+ }
870
+
871
+ /**
872
+ * Remove a hologram reference from source data's _meta.activeHolograms
873
+ * Call this when a hologram is deleted
874
+ * @param {string} sourceHolon - Source holon ID
875
+ * @param {string} lensName - Lens name
876
+ * @param {string} dataId - Data ID
877
+ * @param {string} targetHolon - Target holon where hologram lives
878
+ * @returns {Promise<boolean>} Success indicator
879
+ */
880
+ async removeActiveHologram(sourceHolon, lensName, dataId, targetHolon) {
881
+ return federation.removeActiveHologram(
882
+ this.client,
883
+ this.config.appName,
884
+ sourceHolon,
885
+ lensName,
886
+ dataId,
887
+ targetHolon
888
+ );
889
+ }
890
+
891
+ /**
892
+ * Delete a hologram and clean up activeHolograms on the original source
893
+ * @param {string} holonId - Holon where the hologram lives
894
+ * @param {string} lensName - Lens name
895
+ * @param {string} dataId - Data ID of the hologram
896
+ * @param {Object} options - Optional settings
897
+ * @returns {Promise<Object>} Result with deletion info
898
+ */
899
+ async deleteHologram(holonId, lensName, dataId, options = {}) {
900
+ return federation.deleteHologram(
901
+ this.client,
902
+ this.config.appName,
903
+ holonId,
904
+ lensName,
905
+ dataId,
906
+ options
907
+ );
908
+ }
909
+
910
+ /**
911
+ * Propagate data to a specific target holon (creates hologram or copy)
912
+ * Use this for direct propagation; use propagate() for federation-based propagation
913
+ * @param {Object} data - Data to propagate
914
+ * @param {string} sourceHolon - Source holon ID
915
+ * @param {string} targetHolon - Target holon ID
916
+ * @param {string} lensName - Lens name
917
+ * @param {Object} options - Options
918
+ * @param {string} options.mode - 'reference' (creates hologram) or 'copy' (copies data)
919
+ * @returns {Promise<boolean>} Success indicator
920
+ */
921
+ async propagateData(data, sourceHolon, targetHolon, lensName, options = {}) {
922
+ const { mode = 'reference' } = options;
923
+ return federation.propagateData(
924
+ this.client,
925
+ this.config.appName,
926
+ data,
927
+ sourceHolon,
928
+ targetHolon,
929
+ lensName,
930
+ mode
931
+ );
932
+ }
933
+
934
+ /**
935
+ * Resolve a hologram to its actual data, merging local overrides
936
+ * @param {Object} hologram - Hologram object to resolve
937
+ * @returns {Promise<Object|null>} Resolved data or null
938
+ */
939
+ async resolveHologram(hologram) {
940
+ return federation.resolveHologram(this.client, hologram);
941
+ }
942
+
943
+ /**
944
+ * Check if data is a hologram (unresolved reference)
945
+ * @param {Object} data - Data to check
946
+ * @returns {boolean} True if data is a hologram
947
+ */
948
+ isHologram(data) {
949
+ return federation.isHologram(data);
950
+ }
951
+
952
+ /**
953
+ * Check if data was resolved from a hologram
954
+ * @param {Object} data - Data to check
955
+ * @returns {boolean} True if data was resolved from a hologram
956
+ */
957
+ isResolvedHologram(data) {
958
+ return federation.isResolvedHologram(data);
959
+ }
960
+
961
+ /**
962
+ * Get hologram source information from resolved data
963
+ * @param {Object} data - Resolved data
964
+ * @returns {Object|null} Source info { soul, sourceHolon, localOverrides } or null
965
+ */
966
+ getHologramSource(data) {
967
+ return federation.getHologramSource(data);
968
+ }
969
+
970
+ /**
971
+ * Cleanup circular holograms in a holon's lens
972
+ * Detects and removes holograms that create circular references (A→B→A)
973
+ * @param {string} holonId - Holon to clean up
974
+ * @param {string} lensName - Lens name to check
975
+ * @param {Object} options - Optional settings
976
+ * @param {boolean} options.dryRun - If true, only report what would be deleted
977
+ * @returns {Promise<Object>} Result with cleanup info
978
+ */
979
+ async cleanupCircularHolograms(holonId, lensName, options = {}) {
980
+ return federation.cleanupCircularHolograms(
981
+ this.client,
982
+ this.config.appName,
983
+ holonId,
984
+ lensName,
985
+ options
986
+ );
987
+ }
988
+
989
+ /**
990
+ * Cleanup circular holograms for a specific list of IDs
991
+ * Use this when you don't have an index but know which IDs to check
992
+ * @param {string} holonId - Holon to clean up
993
+ * @param {string} lensName - Lens name
994
+ * @param {string[]} dataIds - Array of data IDs to check
995
+ * @param {Object} options - Optional settings
996
+ * @param {boolean} options.dryRun - If true, only report what would be deleted
997
+ * @returns {Promise<Object>} Result with cleanup info
998
+ */
999
+ async cleanupCircularHologramsByIds(holonId, lensName, dataIds, options = {}) {
1000
+ return federation.cleanupCircularHologramsByIds(
1001
+ this.client,
1002
+ this.config.appName,
1003
+ holonId,
1004
+ lensName,
1005
+ dataIds,
1006
+ options
1007
+ );
1008
+ }
1009
+
1010
+ /**
1011
+ * Propagate data to federated holons and optionally to parent holons
1012
+ * @param {string} holonId - Source holon ID
1013
+ * @param {string} lensName - Lens name (e.g., 'quests')
1014
+ * @param {Object} data - Data or hologram to propagate
1015
+ * @param {Object} options - Propagation options
1016
+ * @param {boolean} options.useHolograms - Use holograms instead of copies (default: true)
1017
+ * @param {boolean} options.propagateToParents - Propagate to parent holons for H3 holons (default: false)
1018
+ * @param {number} options.maxParentLevels - Maximum parent levels to propagate (default: 3)
1019
+ * @returns {Promise<Object>} Result with success/failure counts
1020
+ */
1021
+ async propagate(holonId, lensName, data, options = {}) {
1022
+ const {
1023
+ useHolograms = true,
1024
+ propagateToParents = false,
1025
+ maxParentLevels = 3
1026
+ } = options;
1027
+
1028
+ const result = {
1029
+ success: 0,
1030
+ failed: 0,
1031
+ targets: [],
1032
+ parentPropagation: null
1033
+ };
1034
+
1035
+ // Get federation config for this holon
1036
+ const federationData = await this.getFederation(holonId);
1037
+
1038
+ // Propagate to federated holons (outbound)
1039
+ if (federationData && federationData.outbound) {
1040
+ for (const targetHolon of federationData.outbound) {
1041
+ // Check if sender has this lens configured for outbound to this target
1042
+ const senderLensConfig = federationData.lensConfig?.[targetHolon];
1043
+ if (senderLensConfig && senderLensConfig.outbound && !senderLensConfig.outbound.includes(lensName)) {
1044
+ continue; // Skip - sender hasn't configured this lens for outbound to this target
1045
+ }
1046
+
1047
+ // Check if receiver accepts this lens in their inbound from sender
1048
+ try {
1049
+ const targetFederation = await this.getFederation(targetHolon);
1050
+ const receiverLensConfig = targetFederation?.lensConfig?.[holonId];
1051
+ if (receiverLensConfig && receiverLensConfig.inbound && !receiverLensConfig.inbound.includes(lensName)) {
1052
+ console.log(`Skipping propagation to ${targetHolon} - lens '${lensName}' not in their inbound config for ${holonId}`);
1053
+ continue; // Skip - receiver doesn't accept this lens from sender
1054
+ }
1055
+ } catch (error) {
1056
+ console.warn(`Could not verify receiver's inbound config for ${targetHolon}, proceeding anyway:`, error);
1057
+ }
1058
+
1059
+ try {
1060
+ const mode = useHolograms ? 'reference' : 'copy';
1061
+ await federation.propagateData(
1062
+ this.client,
1063
+ this.config.appName,
1064
+ data,
1065
+ holonId,
1066
+ targetHolon,
1067
+ lensName,
1068
+ mode
1069
+ );
1070
+ result.success++;
1071
+ result.targets.push(targetHolon);
1072
+ } catch (error) {
1073
+ console.error(`Failed to propagate to ${targetHolon}:`, error);
1074
+ result.failed++;
1075
+ }
1076
+ }
1077
+ }
1078
+
1079
+ // Propagate to parent holons (for H3/geographic holons)
1080
+ if (propagateToParents && spatial.isValidH3(holonId)) {
1081
+ const parents = spatial.getParents(holonId);
1082
+ const targetParents = parents.slice(0, maxParentLevels);
1083
+
1084
+ result.parentPropagation = {
1085
+ success: 0,
1086
+ failed: 0,
1087
+ targets: []
1088
+ };
1089
+
1090
+ for (const parentHolon of targetParents) {
1091
+ try {
1092
+ const mode = useHolograms ? 'reference' : 'copy';
1093
+ await federation.propagateData(
1094
+ this.client,
1095
+ this.config.appName,
1096
+ data,
1097
+ holonId,
1098
+ parentHolon,
1099
+ lensName,
1100
+ mode
1101
+ );
1102
+ result.parentPropagation.success++;
1103
+ result.parentPropagation.targets.push(parentHolon);
1104
+ } catch (error) {
1105
+ console.error(`Failed to propagate to parent ${parentHolon}:`, error);
1106
+ result.parentPropagation.failed++;
1107
+ }
1108
+ }
1109
+ }
1110
+
1111
+ return result;
1112
+ }
1113
+
1114
+ async getFederation(holonId) {
1115
+ if (!holonId) {
1116
+ throw new Error('getFederation: Missing holon ID');
1117
+ }
1118
+ const data = await this.getGlobal('federation', holonId);
1119
+ if (!data) return null;
1120
+
1121
+ // Ensure arrays exist (in case data from storage is malformed)
1122
+ if (!Array.isArray(data.inbound)) {
1123
+ data.inbound = [];
1124
+ }
1125
+ if (!Array.isArray(data.outbound)) {
1126
+ data.outbound = [];
1127
+ }
1128
+ if (!data.lensConfig || typeof data.lensConfig !== 'object') {
1129
+ data.lensConfig = {};
1130
+ }
1131
+
1132
+ // Backwards compatibility: populate federated from inbound/outbound if it doesn't exist
1133
+ if (!Array.isArray(data.federated)) {
1134
+ // Combine inbound and outbound into a unique set
1135
+ const allFederated = new Set([...data.inbound, ...data.outbound]);
1136
+ data.federated = Array.from(allFederated);
1137
+ }
1138
+
1139
+ return data;
1140
+ }
1141
+
1142
+ /**
1143
+ * Federate two holons at the holon level (manages multiple lenses)
1144
+ * This is a higher-level method that manages federation metadata and lens-specific federations
1145
+ * @param {string} sourceHolon - Source holon ID
1146
+ * @param {string} targetHolon - Target holon ID
1147
+ * @param {Object} options - Federation options
1148
+ * @param {Object} options.lensConfig - Lens configuration {inbound: [], outbound: []}
1149
+ * @returns {Promise<boolean>} Success indicator
1150
+ */
1151
+ async federateHolon(sourceHolon, targetHolon, options = {}) {
1152
+ const { lensConfig = { inbound: [], outbound: [] } } = options;
1153
+
1154
+ // Validate self-federation
1155
+ if (sourceHolon === targetHolon) {
1156
+ throw new Error('Cannot federate a holon with itself');
1157
+ }
1158
+
1159
+ // Get or create federation record for source holon
1160
+ let federationData = await this.getGlobal('federation', sourceHolon) || {
1161
+ id: sourceHolon,
1162
+ name: sourceHolon,
1163
+ federated: [],
1164
+ inbound: [],
1165
+ outbound: [],
1166
+ lensConfig: {},
1167
+ timestamp: Date.now()
1168
+ };
1169
+
1170
+ // Ensure arrays exist (in case data from storage is malformed)
1171
+ if (!Array.isArray(federationData.federated)) {
1172
+ federationData.federated = [];
1173
+ }
1174
+ if (!Array.isArray(federationData.inbound)) {
1175
+ federationData.inbound = [];
1176
+ }
1177
+ if (!Array.isArray(federationData.outbound)) {
1178
+ federationData.outbound = [];
1179
+ }
1180
+ if (!federationData.lensConfig || typeof federationData.lensConfig !== 'object') {
1181
+ federationData.lensConfig = {};
1182
+ }
1183
+
1184
+ // Always add target to federated list (tracks all federation links)
1185
+ if (!federationData.federated.includes(targetHolon)) {
1186
+ federationData.federated.push(targetHolon);
1187
+ }
1188
+
1189
+ // Add target to inbound/outbound lists based on lensConfig
1190
+ // If there are outbound lenses, add to outbound list
1191
+ if (lensConfig.outbound && lensConfig.outbound.length > 0) {
1192
+ if (!federationData.outbound.includes(targetHolon)) {
1193
+ federationData.outbound.push(targetHolon);
1194
+ }
1195
+ } else {
1196
+ // Remove from outbound if no outbound lenses
1197
+ federationData.outbound = federationData.outbound.filter(id => id !== targetHolon);
1198
+ }
1199
+
1200
+ // If there are inbound lenses, add to inbound list
1201
+ if (lensConfig.inbound && lensConfig.inbound.length > 0) {
1202
+ if (!federationData.inbound.includes(targetHolon)) {
1203
+ federationData.inbound.push(targetHolon);
1204
+ }
1205
+ } else {
1206
+ // Remove from inbound if no inbound lenses
1207
+ federationData.inbound = federationData.inbound.filter(id => id !== targetHolon);
1208
+ }
1209
+
1210
+ // Store lens configuration for this target holon
1211
+ federationData.lensConfig[targetHolon] = {
1212
+ inbound: lensConfig.inbound || [],
1213
+ outbound: lensConfig.outbound || [],
1214
+ timestamp: Date.now()
1215
+ };
1216
+
1217
+ // Save federation metadata
1218
+ const success = await this.writeGlobal('federation', federationData);
1219
+
1220
+ // Clear cache so getFederation returns fresh data immediately
1221
+ this.clearCache('federation');
1222
+
1223
+ // Setup lens-specific federations
1224
+ if (success) {
1225
+ // Federate each lens specified in inbound array (receive from target)
1226
+ for (const lens of (lensConfig.inbound || [])) {
1227
+ await this.federate(targetHolon, sourceHolon, lens, { direction: 'outbound', mode: 'reference' });
1228
+ }
1229
+
1230
+ // Federate each lens specified in outbound array (send to target)
1231
+ for (const lens of (lensConfig.outbound || [])) {
1232
+ await this.federate(sourceHolon, targetHolon, lens, { direction: 'outbound', mode: 'reference' });
1233
+ }
1234
+ }
1235
+
1236
+ return success;
1237
+ }
1238
+
1239
+ /**
1240
+ * Unfederate two holons at the holon level
1241
+ * @param {string} sourceHolon - Source holon ID
1242
+ * @param {string} targetHolon - Target holon ID
1243
+ * @returns {Promise<boolean>} Success indicator
1244
+ */
1245
+ async unfederateHolon(sourceHolon, targetHolon) {
1246
+ // Get federation record
1247
+ const federationData = await this.getGlobal('federation', sourceHolon);
1248
+ if (!federationData) {
1249
+ return false;
1250
+ }
1251
+
1252
+ // Get lens config for this target before removing
1253
+ const lensConfig = federationData.lensConfig?.[targetHolon];
1254
+
1255
+ // Remove target from federation lists
1256
+ federationData.federated = (federationData.federated || []).filter(id => id !== targetHolon);
1257
+ federationData.inbound = (federationData.inbound || []).filter(id => id !== targetHolon);
1258
+ federationData.outbound = (federationData.outbound || []).filter(id => id !== targetHolon);
1259
+
1260
+ // Remove lens config for this target
1261
+ if (federationData.lensConfig) {
1262
+ delete federationData.lensConfig[targetHolon];
1263
+ }
1264
+
1265
+ // Update federation metadata
1266
+ const success = await this.writeGlobal('federation', federationData);
1267
+
1268
+ // Clear cache so getFederation returns fresh data immediately
1269
+ this.clearCache('federation');
1270
+
1271
+ // Unfederate lens-specific federations
1272
+ if (success && lensConfig) {
1273
+ // Unfederate each lens that was in inbound array
1274
+ for (const lens of (lensConfig.inbound || [])) {
1275
+ await this.unfederate(targetHolon, sourceHolon, lens);
1276
+ }
1277
+
1278
+ // Unfederate each lens that was in outbound array
1279
+ for (const lens of (lensConfig.outbound || [])) {
1280
+ await this.unfederate(sourceHolon, targetHolon, lens);
1281
+ }
1282
+ }
1283
+
1284
+ return success;
1285
+ }
1286
+
1287
+ /**
1288
+ * Get the lens configuration for a specific federated holon
1289
+ * @param {string} sourceHolon - Source holon ID
1290
+ * @param {string} targetHolon - Target holon ID
1291
+ * @returns {Promise<Object|null>} Lens config {inbound: [], outbound: []} or null
1292
+ */
1293
+ async getFederatedConfig(sourceHolon, targetHolon) {
1294
+ const federationData = await this.getGlobal('federation', sourceHolon);
1295
+ if (!federationData || !federationData.lensConfig) {
1296
+ return null;
1297
+ }
1298
+ return federationData.lensConfig[targetHolon] || null;
1299
+ }
1300
+
1301
+ // === Hierarchical Operations ===
1302
+ async upcast(holonId, lensName, dataId, options = {}) {
1303
+ return hierarchical.upcast(this.client, this.config.appName, holonId, lensName, dataId, options);
1304
+ }
1305
+
1306
+ // === Subscription Operations ===
1307
+ subscribe(holonId, lensName, callback, options = {}) {
1308
+ if (typeof callback !== 'function') {
1309
+ throw new TypeError('callback must be a function');
1310
+ }
1311
+
1312
+ const path = storage.buildPath(this.config.appName, holonId, lensName);
1313
+
1314
+ // Default to real-time only mode (only receive new events, not historical)
1315
+ const subscriptionOptions = {
1316
+ realtimeOnly: true, // Only get events from subscription time forward
1317
+ ...options
1318
+ };
1319
+
1320
+ const subscriptionId = `${holonId}-${lensName}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1321
+ let innerSubscription = null;
1322
+ let unsubscribeCalled = false;
1323
+
1324
+ // Start async subscription setup in background
1325
+ subscriptions.createSubscription(this.client, path, callback, subscriptionOptions)
1326
+ .then(subscription => {
1327
+ innerSubscription = subscription;
1328
+ // If unsubscribe was called before setup completed, clean up immediately
1329
+ if (unsubscribeCalled) {
1330
+ subscription.unsubscribe();
1331
+ } else {
1332
+ this.subscriptionRegistry.register(subscriptionId, subscription);
1333
+ }
1334
+ })
1335
+ .catch(err => {
1336
+ this._log('ERROR', 'Subscription setup failed', { path, error: err.message });
1337
+ });
1338
+
1339
+ this._metrics.subscriptions++;
1340
+
1341
+ // Return synchronous subscription object
1342
+ return {
1343
+ unsubscribe: () => {
1344
+ unsubscribeCalled = true;
1345
+ if (innerSubscription) {
1346
+ this.subscriptionRegistry.unregister(subscriptionId);
1347
+ }
1348
+ },
1349
+ };
1350
+ }
1351
+
1352
+ /**
1353
+ * Subscribe to global table changes
1354
+ * @param {string} table - The global table name (e.g., 'federation')
1355
+ * @param {string} key - The specific key within the table (e.g., holonId)
1356
+ * @param {Function} callback - Callback function called on data changes
1357
+ * @param {Object} options - Subscription options
1358
+ * @returns {Object} Subscription object with unsubscribe method
1359
+ */
1360
+ async subscribeGlobal(table, key, callback, options = {}) {
1361
+ if (typeof callback !== 'function') {
1362
+ throw new TypeError('callback must be a function');
1363
+ }
1364
+
1365
+ // Build global table path: appname/table/key
1366
+ const path = `${this.config.appName}/${table}/${key}`;
1367
+
1368
+ // Default to real-time only mode
1369
+ const subscriptionOptions = {
1370
+ realtimeOnly: true,
1371
+ ...options
1372
+ };
1373
+
1374
+ const subscription = await subscriptions.createSubscription(this.client, path, callback, subscriptionOptions);
1375
+
1376
+ const subscriptionId = `global-${table}-${key}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1377
+ this.subscriptionRegistry.register(subscriptionId, subscription);
1378
+ this._metrics.subscriptions++;
1379
+
1380
+ return {
1381
+ unsubscribe: () => {
1382
+ this.subscriptionRegistry.unregister(subscriptionId);
1383
+ },
1384
+ };
1385
+ }
1386
+
1387
+ // === Crypto Operations ===
1388
+ async getPublicKey(privateKey) {
1389
+ return crypto.getPublicKey(privateKey);
1390
+ }
1391
+
1392
+ async sign(content, privateKey) {
1393
+ return crypto.sign(content, privateKey);
1394
+ }
1395
+
1396
+ async verify(content, signature, publicKey) {
1397
+ return crypto.verify(content, signature, publicKey);
1398
+ }
1399
+
1400
+ async issueCapability(permissions, scope, recipient, options) {
1401
+ return crypto.issueCapability(permissions, scope, recipient, options);
1402
+ }
1403
+
1404
+ async verifyCapability(token, requiredPermission, scope) {
1405
+ return crypto.verifyCapability(token, requiredPermission, scope);
1406
+ }
1407
+
1408
+ // === Social Protocol Operations ===
1409
+ async publishNostr(event, holonId, lensName = 'social') {
1410
+ // Validate with throwing error (partial=true allows missing id/pubkey/sig)
1411
+ social.validateNostrEvent(event, true, true);
1412
+
1413
+ // Sign the event if not already signed
1414
+ if (!event.id || !event.pubkey || !event.sig) {
1415
+ // Use nostr-tools to properly sign the event
1416
+ const { finalizeEvent } = await import('nostr-tools');
1417
+ const signedEvent = finalizeEvent(event, this.client.privateKey);
1418
+
1419
+ // Update event with signature fields
1420
+ event.id = signedEvent.id;
1421
+ event.pubkey = signedEvent.pubkey;
1422
+ event.sig = signedEvent.sig;
1423
+ }
1424
+
1425
+ const transformed = social.transformNostrEvent(event);
1426
+ return this.write(holonId, lensName, transformed);
1427
+ }
1428
+
1429
+ async publishActivityPub(object, holonId, lensName = 'social') {
1430
+ // Validate with throwing error
1431
+ social.validateActivityPubObject(object, true);
1432
+
1433
+ const transformed = social.transformActivityPubObject(object);
1434
+ return this.write(holonId, lensName, transformed);
1435
+ }
1436
+
1437
+ async querySocial(holonId, options = {}) {
1438
+ const { protocol, accessLevel } = options;
1439
+ let data = await this.read(holonId, 'social');
1440
+
1441
+ if (protocol) {
1442
+ data = social.filterByProtocol(data, protocol);
1443
+ }
1444
+
1445
+ if (accessLevel) {
1446
+ data = social.filterByAccessLevel(data, accessLevel);
1447
+ }
1448
+
1449
+ // Parse tags back to array for Nostr events (stored as JSON string)
1450
+ if (Array.isArray(data)) {
1451
+ data = data.map(item => {
1452
+ if (item.protocol === 'nostr' && typeof item.tags === 'string') {
1453
+ return { ...item, tags: JSON.parse(item.tags) };
1454
+ }
1455
+ return item;
1456
+ });
1457
+ }
1458
+
1459
+ return data;
1460
+ }
1461
+
1462
+ async verifyNostrEvent(event) {
1463
+ // Verify a Nostr event signature
1464
+ const { verifyEvent } = await import('nostr-tools');
1465
+
1466
+ // Parse tags if they're stored as a string
1467
+ const eventToVerify = {
1468
+ ...event,
1469
+ tags: typeof event.tags === 'string' ? JSON.parse(event.tags) : event.tags
1470
+ };
1471
+
1472
+ return verifyEvent(eventToVerify);
1473
+ }
1474
+
1475
+ // === Metrics ===
1476
+ metrics() {
1477
+ const baseMetrics = super.metrics();
1478
+ return {
1479
+ ...baseMetrics,
1480
+ federations: this._metrics.federations || 0,
1481
+ avgWriteTime: this._metrics.writes > 0
1482
+ ? (this._metrics.totalWriteTime || 0) / this._metrics.writes
1483
+ : 0,
1484
+ avgReadTime: this._metrics.reads > 0
1485
+ ? (this._metrics.totalReadTime || 0) / this._metrics.reads
1486
+ : 0,
1487
+ };
1488
+ }
1489
+
1490
+ // === Functional Aliases ===
1491
+ // Common CRUD-style aliases for data operations
1492
+
1493
+ /**
1494
+ * Alias for write() - stores data in a holon/lens
1495
+ * @alias write
1496
+ */
1497
+ async put(holonId, lensName, data, options = {}) {
1498
+ return this.write(holonId, lensName, data, options);
1499
+ }
1500
+
1501
+ /**
1502
+ * Alias for read() - retrieves data from a holon/lens
1503
+ * @alias read
1504
+ */
1505
+ async get(holonId, lensName, dataId = null, options = {}) {
1506
+ return this.read(holonId, lensName, dataId, options);
1507
+ }
1508
+
1509
+ /**
1510
+ * Alias for delete() - removes data from a holon/lens
1511
+ * @alias delete
1512
+ */
1513
+ async remove(holonId, lensName, dataId, options = {}) {
1514
+ return this.delete(holonId, lensName, dataId, options);
1515
+ }
1516
+
1517
+ /**
1518
+ * Alias for writeGlobal() - stores data in global table
1519
+ * @alias writeGlobal
1520
+ */
1521
+ async putGlobal(table, data) {
1522
+ return this.writeGlobal(table, data);
1523
+ }
1524
+
1525
+ /**
1526
+ * Alias for readGlobal() - retrieves data from global table
1527
+ * @alias readGlobal
1528
+ */
1529
+ async getGlobal(table, key = null) {
1530
+ return this.readGlobal(table, key);
1531
+ }
1532
+
1533
+ /**
1534
+ * Alias for deleteGlobal() - removes data from global table
1535
+ * @alias deleteGlobal
1536
+ */
1537
+ async removeGlobal(table, key) {
1538
+ return this.deleteGlobal(table, key);
1539
+ }
1540
+
1541
+ /**
1542
+ * Alias for setSchema() - defines a schema for a lens
1543
+ * @alias setSchema
1544
+ */
1545
+ async defineSchema(lensName, schemaObj, strict = false) {
1546
+ return this.setSchema(lensName, schemaObj, strict);
1547
+ }
1548
+
1549
+ /**
1550
+ * Alias for getSchema() - retrieves schema for a lens
1551
+ * @alias getSchema
1552
+ */
1553
+ async fetchSchema(lensName) {
1554
+ return this.getSchema(lensName);
1555
+ }
1556
+
1557
+ /**
1558
+ * Alias for clearSchema() - removes schema for a lens
1559
+ * @alias clearSchema
1560
+ */
1561
+ async removeSchema(lensName) {
1562
+ return this.clearSchema(lensName);
1563
+ }
1564
+
1565
+ /**
1566
+ * Store data - more semantic alias for write
1567
+ * @alias write
1568
+ */
1569
+ async store(holonId, lensName, data, options = {}) {
1570
+ return this.write(holonId, lensName, data, options);
1571
+ }
1572
+
1573
+ /**
1574
+ * Fetch data - more semantic alias for read
1575
+ * @alias read
1576
+ */
1577
+ async fetch(holonId, lensName, dataId = null, options = {}) {
1578
+ return this.read(holonId, lensName, dataId, options);
1579
+ }
1580
+
1581
+ /**
1582
+ * Save data - alternative alias for write
1583
+ * @alias write
1584
+ */
1585
+ async save(holonId, lensName, data, options = {}) {
1586
+ return this.write(holonId, lensName, data, options);
1587
+ }
1588
+
1589
+ /**
1590
+ * Load data - alternative alias for read
1591
+ * @alias read
1592
+ */
1593
+ async load(holonId, lensName, dataId = null, options = {}) {
1594
+ return this.read(holonId, lensName, dataId, options);
1595
+ }
1596
+
1597
+ /**
1598
+ * Clear the internal cache (or specific entries matching a pattern)
1599
+ * Useful after write operations when you need to immediately read fresh data
1600
+ * @param {string} [pattern] - Optional pattern to match cache keys (clears all if not provided)
1601
+ */
1602
+ clearCache(pattern = null) {
1603
+ if (this.client && this.client.clearCache) {
1604
+ this.client.clearCache(pattern);
1605
+ }
1606
+ }
1607
+
1608
+ // === AI Operations ===
1609
+
1610
+ /**
1611
+ * Check if AI is available and throw if not
1612
+ * @private
1613
+ */
1614
+ _requireAI() {
1615
+ if (!this._ai) {
1616
+ throw new Error('AI services not initialized. Provide openaiKey in config.');
1617
+ }
1618
+ }
1619
+
1620
+ // --- Core LLM Methods ---
1621
+
1622
+ /**
1623
+ * Summarize text using AI
1624
+ * @param {string} text - Text to summarize
1625
+ * @param {Object} options - Options for summarization
1626
+ * @returns {Promise<string>} Summary
1627
+ */
1628
+ async summarize(text, options = {}) {
1629
+ this._requireAI();
1630
+ return this._ai.llm.summarize(text, options);
1631
+ }
1632
+
1633
+ /**
1634
+ * Analyze text from a specific perspective
1635
+ * @param {string} text - Text to analyze
1636
+ * @param {string} aspect - Perspective to analyze from
1637
+ * @param {Object} options - Options
1638
+ * @returns {Promise<string>} Analysis
1639
+ */
1640
+ async analyze(text, aspect, options = {}) {
1641
+ this._requireAI();
1642
+ return this._ai.llm.analyze(text, aspect, options);
1643
+ }
1644
+
1645
+ /**
1646
+ * Extract keywords from text
1647
+ * @param {string} text - Text to extract keywords from
1648
+ * @param {Object} options - Options
1649
+ * @returns {Promise<string[]>} Keywords
1650
+ */
1651
+ async extractKeywords(text, options = {}) {
1652
+ this._requireAI();
1653
+ return this._ai.llm.extractKeywords(text, options);
1654
+ }
1655
+
1656
+ /**
1657
+ * Categorize text into one of the provided categories
1658
+ * @param {string} text - Text to categorize
1659
+ * @param {string[]} categories - Available categories
1660
+ * @param {Object} options - Options
1661
+ * @returns {Promise<string>} Selected category
1662
+ */
1663
+ async categorize(text, categories, options = {}) {
1664
+ this._requireAI();
1665
+ return this._ai.llm.categorize(text, categories, options);
1666
+ }
1667
+
1668
+ /**
1669
+ * Translate text to a target language
1670
+ * @param {string} text - Text to translate
1671
+ * @param {string} targetLanguage - Target language
1672
+ * @param {Object} options - Options
1673
+ * @returns {Promise<string>} Translated text
1674
+ */
1675
+ async translate(text, targetLanguage, options = {}) {
1676
+ this._requireAI();
1677
+ return this._ai.llm.translate(text, targetLanguage, options);
1678
+ }
1679
+
1680
+ /**
1681
+ * Generate questions based on text
1682
+ * @param {string} text - Text to generate questions from
1683
+ * @param {Object} options - Options
1684
+ * @returns {Promise<string[]>} Generated questions
1685
+ */
1686
+ async generateQuestions(text, options = {}) {
1687
+ this._requireAI();
1688
+ return this._ai.llm.generateQuestions(text, options);
1689
+ }
1690
+
1691
+ // --- Schema Extraction ---
1692
+
1693
+ /**
1694
+ * Extract structured data from text based on a lens schema
1695
+ * @param {string} text - Natural language text
1696
+ * @param {string} lensName - Name of the lens to get schema from
1697
+ * @param {Object} options - Options
1698
+ * @returns {Promise<Object>} Extracted structured data
1699
+ */
1700
+ async extractToSchema(text, lensName, options = {}) {
1701
+ this._requireAI();
1702
+ const schemaObj = await this.getSchema(lensName);
1703
+ if (!schemaObj) {
1704
+ throw new Error(`No schema found for lens: ${lensName}`);
1705
+ }
1706
+ return this._ai.schemaExtractor.extractToSchema(text, schemaObj, options);
1707
+ }
1708
+
1709
+ // --- Fuzzy JSON Operations ---
1710
+
1711
+ /**
1712
+ * Semantically merge two JSON objects
1713
+ * @param {Object} obj1 - First object
1714
+ * @param {Object} obj2 - Second object
1715
+ * @param {Object} options - Options
1716
+ * @returns {Promise<Object>} Merged object
1717
+ */
1718
+ async jsonAdd(obj1, obj2, options = {}) {
1719
+ this._requireAI();
1720
+ return this._ai.jsonOps.add(obj1, obj2, options);
1721
+ }
1722
+
1723
+ /**
1724
+ * Remove concepts from obj1 based on obj2
1725
+ * @param {Object} obj1 - Base object
1726
+ * @param {Object} obj2 - Object with concepts to remove
1727
+ * @param {Object} options - Options
1728
+ * @returns {Promise<Object>} Result
1729
+ */
1730
+ async jsonSubtract(obj1, obj2, options = {}) {
1731
+ this._requireAI();
1732
+ return this._ai.jsonOps.subtract(obj1, obj2, options);
1733
+ }
1734
+
1735
+ /**
1736
+ * Union two objects with intelligent deduplication
1737
+ * @param {Object} obj1 - First object
1738
+ * @param {Object} obj2 - Second object
1739
+ * @param {Object} options - Options
1740
+ * @returns {Promise<Object>} United object
1741
+ */
1742
+ async jsonUnion(obj1, obj2, options = {}) {
1743
+ this._requireAI();
1744
+ return this._ai.jsonOps.union(obj1, obj2, options);
1745
+ }
1746
+
1747
+ /**
1748
+ * Find semantic differences between two objects
1749
+ * @param {Object} obj1 - First object
1750
+ * @param {Object} obj2 - Second object
1751
+ * @param {Object} options - Options
1752
+ * @returns {Promise<Object>} Differences
1753
+ */
1754
+ async jsonDifference(obj1, obj2, options = {}) {
1755
+ this._requireAI();
1756
+ return this._ai.jsonOps.difference(obj1, obj2, options);
1757
+ }
1758
+
1759
+ /**
1760
+ * Semantically concatenate objects/arrays
1761
+ * @param {Object} obj1 - First object
1762
+ * @param {Object} obj2 - Second object
1763
+ * @param {Object} options - Options
1764
+ * @returns {Promise<Object>} Concatenated result
1765
+ */
1766
+ async jsonConcatenate(obj1, obj2, options = {}) {
1767
+ this._requireAI();
1768
+ return this._ai.jsonOps.concatenate(obj1, obj2, options);
1769
+ }
1770
+
1771
+ // --- Embeddings & Semantic Search ---
1772
+
1773
+ /**
1774
+ * Generate embedding vector for text
1775
+ * @param {string} text - Text to embed
1776
+ * @returns {Promise<number[]>} Embedding vector
1777
+ */
1778
+ async embed(text) {
1779
+ this._requireAI();
1780
+ return this._ai.embeddings.embed(text);
1781
+ }
1782
+
1783
+ /**
1784
+ * Perform semantic search in a holon/lens
1785
+ * @param {string} query - Search query
1786
+ * @param {string} holonId - Holon to search in
1787
+ * @param {string} lensName - Lens to search in
1788
+ * @param {Object} options - Search options
1789
+ * @returns {Promise<Array>} Search results
1790
+ */
1791
+ async semanticSearch(query, holonId, lensName, options = {}) {
1792
+ this._requireAI();
1793
+ return this._ai.embeddings.semanticSearch(query, holonId, lensName, options);
1794
+ }
1795
+
1796
+ /**
1797
+ * Store data with embedding for later semantic search
1798
+ * @param {string} holonId - Holon ID
1799
+ * @param {string} lensName - Lens name
1800
+ * @param {Object} data - Data to store
1801
+ * @param {string} textField - Field to generate embedding from
1802
+ * @returns {Promise<boolean>} Success
1803
+ */
1804
+ async storeWithEmbedding(holonId, lensName, data, textField = 'description') {
1805
+ this._requireAI();
1806
+ return this._ai.embeddings.storeWithEmbedding(holonId, lensName, data, textField);
1807
+ }
1808
+
1809
+ // --- Multi-Perspective Council ---
1810
+
1811
+ /**
1812
+ * Ask the council of perspectives for wisdom
1813
+ * @param {string} question - Question to ask
1814
+ * @param {Object} options - Options
1815
+ * @returns {Promise<Object>} Council responses
1816
+ */
1817
+ async askCouncil(question, options = {}) {
1818
+ this._requireAI();
1819
+ return this._ai.council.ask(question, options);
1820
+ }
1821
+
1822
+ /**
1823
+ * Ask council with custom perspectives
1824
+ * @param {string} question - Question to ask
1825
+ * @param {string[]} perspectives - Custom perspectives
1826
+ * @param {Object} options - Options
1827
+ * @returns {Promise<Object>} Council responses
1828
+ */
1829
+ async askCouncilCustom(question, perspectives, options = {}) {
1830
+ this._requireAI();
1831
+ return this._ai.council.askCustom(question, perspectives, options);
1832
+ }
1833
+
1834
+ // --- Text-to-Speech ---
1835
+
1836
+ /**
1837
+ * Convert text to speech
1838
+ * @param {string} text - Text to speak
1839
+ * @param {string} voice - Voice to use
1840
+ * @param {Object} options - TTS options
1841
+ * @returns {Promise<Buffer>} Audio buffer
1842
+ */
1843
+ async textToSpeech(text, voice = 'nova', options = {}) {
1844
+ this._requireAI();
1845
+ return this._ai.tts.speak(text, voice, options);
1846
+ }
1847
+
1848
+ /**
1849
+ * Convert text to speech as base64
1850
+ * @param {string} text - Text to speak
1851
+ * @param {string} voice - Voice to use
1852
+ * @param {Object} options - TTS options
1853
+ * @returns {Promise<string>} Base64 audio
1854
+ */
1855
+ async textToSpeechBase64(text, voice = 'nova', options = {}) {
1856
+ this._requireAI();
1857
+ return this._ai.tts.speakBase64(text, voice, options);
1858
+ }
1859
+
1860
+ // --- Natural Language Queries ---
1861
+
1862
+ /**
1863
+ * Query data using natural language
1864
+ * @param {string} query - Natural language query
1865
+ * @param {string} holonId - Holon to query (optional)
1866
+ * @param {string} lensName - Lens to query (optional)
1867
+ * @param {Object} options - Options
1868
+ * @returns {Promise<Array>} Query results
1869
+ */
1870
+ async nlQuery(query, holonId = null, lensName = null, options = {}) {
1871
+ this._requireAI();
1872
+ return this._ai.nlQuery.execute(query, holonId, lensName, options);
1873
+ }
1874
+
1875
+ /**
1876
+ * Parse natural language query to structured query
1877
+ * @param {string} query - Natural language query
1878
+ * @returns {Promise<Object>} Parsed query structure
1879
+ */
1880
+ async parseNLQuery(query) {
1881
+ this._requireAI();
1882
+ return this._ai.nlQuery.parse(query);
1883
+ }
1884
+
1885
+ // --- Auto-Classification ---
1886
+
1887
+ /**
1888
+ * Classify content to suggest the best lens
1889
+ * @param {Object} content - Content to classify
1890
+ * @param {Object} options - Options
1891
+ * @returns {Promise<Object>} Classification result
1892
+ */
1893
+ async classifyToLens(content, options = {}) {
1894
+ this._requireAI();
1895
+ return this._ai.classifier.classifyToLens(content, options);
1896
+ }
1897
+
1898
+ /**
1899
+ * Automatically store content in the best lens
1900
+ * @param {string} holonId - Holon ID
1901
+ * @param {Object} content - Content to store
1902
+ * @param {Object} options - Options
1903
+ * @returns {Promise<Object>} Storage result
1904
+ */
1905
+ async autoStore(holonId, content, options = {}) {
1906
+ this._requireAI();
1907
+ return this._ai.classifier.autoStore(holonId, content, options);
1908
+ }
1909
+
1910
+ /**
1911
+ * Register a lens with the classifier
1912
+ * @param {string} lensName - Lens name
1913
+ * @param {string} description - Lens description
1914
+ * @param {string[]} keywords - Keywords for this lens
1915
+ */
1916
+ registerLensForClassification(lensName, description, keywords = []) {
1917
+ this._requireAI();
1918
+ this._ai.classifier.registerLens(lensName, description, keywords);
1919
+ }
1920
+
1921
+ // --- Spatial Analysis ---
1922
+
1923
+ /**
1924
+ * AI-powered analysis of a geographic region
1925
+ * @param {string} holonId - Holon to analyze
1926
+ * @param {string} lensName - Lens to analyze (optional)
1927
+ * @param {string} aspect - Aspect to focus on (optional)
1928
+ * @param {Object} options - Options
1929
+ * @returns {Promise<Object>} Analysis result
1930
+ */
1931
+ async analyzeRegion(holonId, lensName = null, aspect = null, options = {}) {
1932
+ this._requireAI();
1933
+ return this._ai.spatial.analyzeRegion(holonId, lensName, aspect, options);
1934
+ }
1935
+
1936
+ /**
1937
+ * Compare data between two regions
1938
+ * @param {string} holon1 - First holon
1939
+ * @param {string} holon2 - Second holon
1940
+ * @param {string} lensName - Lens to compare (optional)
1941
+ * @returns {Promise<Object>} Comparison result
1942
+ */
1943
+ async compareRegions(holon1, holon2, lensName = null) {
1944
+ this._requireAI();
1945
+ return this._ai.spatial.compareRegions(holon1, holon2, lensName);
1946
+ }
1947
+
1948
+ /**
1949
+ * Identify trends over time/space
1950
+ * @param {string} holonId - Holon to analyze
1951
+ * @param {string} lensName - Lens
1952
+ * @param {Object} timeRange - Time range
1953
+ * @returns {Promise<Object>} Trends
1954
+ */
1955
+ async spatialTrends(holonId, lensName, timeRange = null) {
1956
+ this._requireAI();
1957
+ return this._ai.spatial.spatialTrends(holonId, lensName, timeRange);
1958
+ }
1959
+
1960
+ // --- Smart Aggregation ---
1961
+
1962
+ /**
1963
+ * AI-summarized hierarchical aggregation up the holon tree
1964
+ * @param {string} holonId - Starting holon
1965
+ * @param {string} lensName - Lens to aggregate
1966
+ * @param {Object} options - Options
1967
+ * @returns {Promise<Object>} Aggregation result
1968
+ */
1969
+ async smartUpcast(holonId, lensName, options = {}) {
1970
+ this._requireAI();
1971
+ return this._ai.aggregation.smartUpcast(holonId, lensName, options);
1972
+ }
1973
+
1974
+ /**
1975
+ * Generate comprehensive holon summary
1976
+ * @param {string} holonId - Holon to summarize
1977
+ * @returns {Promise<Object>} Summary
1978
+ */
1979
+ async generateHolonSummary(holonId) {
1980
+ this._requireAI();
1981
+ return this._ai.aggregation.generateHolonSummary(holonId);
1982
+ }
1983
+
1984
+ // --- Federation Advisor ---
1985
+
1986
+ /**
1987
+ * Get AI suggestions for federation partners
1988
+ * @param {string} holonId - Holon to find partners for
1989
+ * @param {Object} options - Options including candidateHolons
1990
+ * @returns {Promise<Object>} Federation suggestions
1991
+ */
1992
+ async suggestFederations(holonId, options = {}) {
1993
+ this._requireAI();
1994
+ return this._ai.federationAdvisor.suggestFederations(holonId, options);
1995
+ }
1996
+
1997
+ /**
1998
+ * Analyze federation health
1999
+ * @param {string} holonId - Holon to analyze
2000
+ * @returns {Promise<Object>} Health analysis
2001
+ */
2002
+ async analyzeFederationHealth(holonId) {
2003
+ this._requireAI();
2004
+ return this._ai.federationAdvisor.analyzeFederationHealth(holonId);
2005
+ }
2006
+
2007
+ /**
2008
+ * Get federation optimization suggestions
2009
+ * @param {string} holonId - Holon to optimize
2010
+ * @returns {Promise<Object>} Optimization suggestions
2011
+ */
2012
+ async optimizeFederation(holonId) {
2013
+ this._requireAI();
2014
+ return this._ai.federationAdvisor.optimizeFederation(holonId);
2015
+ }
2016
+
2017
+ // --- Relationship Discovery ---
2018
+
2019
+ /**
2020
+ * Discover hidden relationships in data
2021
+ * @param {string} holonId - Holon to analyze
2022
+ * @param {string} lensName - Lens (optional)
2023
+ * @returns {Promise<Object>} Discovered relationships
2024
+ */
2025
+ async discoverRelationships(holonId, lensName = null) {
2026
+ this._requireAI();
2027
+ return this._ai.relationships.discoverRelationships(holonId, lensName);
2028
+ }
2029
+
2030
+ /**
2031
+ * Find semantically similar items
2032
+ * @param {Object} item - Item to find similar items for
2033
+ * @param {string} holonId - Holon to search in
2034
+ * @param {string} lensName - Lens to search in (optional)
2035
+ * @param {Object} options - Options
2036
+ * @returns {Promise<Array>} Similar items
2037
+ */
2038
+ async findSimilar(item, holonId, lensName = null, options = {}) {
2039
+ this._requireAI();
2040
+ return this._ai.relationships.findSimilar(item, holonId, lensName, options);
2041
+ }
2042
+
2043
+ /**
2044
+ * Build relationship graph
2045
+ * @param {string} holonId - Holon
2046
+ * @param {string} lensName - Lens
2047
+ * @returns {Promise<Object>} Graph structure
2048
+ */
2049
+ async buildRelationshipGraph(holonId, lensName) {
2050
+ this._requireAI();
2051
+ return this._ai.relationships.buildGraph(holonId, lensName);
2052
+ }
2053
+
2054
+ /**
2055
+ * Suggest connections for an item
2056
+ * @param {Object} item - Item to find connections for
2057
+ * @param {string} holonId - Holon
2058
+ * @param {string} lensName - Lens
2059
+ * @returns {Promise<Object>} Suggested connections
2060
+ */
2061
+ async suggestConnections(item, holonId, lensName) {
2062
+ this._requireAI();
2063
+ return this._ai.relationships.suggestConnections(item, holonId, lensName);
2064
+ }
2065
+
2066
+ // --- Task Breakdown ---
2067
+
2068
+ /**
2069
+ * Recursively break down a task/quest into subtasks
2070
+ * @param {Object} item - The item to break down (must have id)
2071
+ * @param {string} holonId - Holon where the item lives
2072
+ * @param {string} lensName - Lens name (e.g., 'quests', 'tasks')
2073
+ * @param {Object} options - Breakdown options
2074
+ * @param {number} options.depth - Maximum recursion depth (default: 2)
2075
+ * @param {number|Object} options.stepsPerLevel - Subtasks per level (default: {min:3, max:5})
2076
+ * @param {boolean} options.useContext - Look at other items for context (default: true)
2077
+ * @param {boolean} options.storeResults - Store generated subtasks (default: true)
2078
+ * @returns {Promise<Object>} Breakdown result with tree structure
2079
+ */
2080
+ async breakdown(item, holonId, lensName, options = {}) {
2081
+ this._requireAI();
2082
+ return this._ai.taskBreakdown.breakdown(item, holonId, lensName, options);
2083
+ }
2084
+
2085
+ /**
2086
+ * Suggest breakdown strategy for a task
2087
+ * @param {Object} item - Item to analyze
2088
+ * @returns {Promise<Object>} Suggested breakdown strategy
2089
+ */
2090
+ async suggestBreakdownStrategy(item) {
2091
+ this._requireAI();
2092
+ return this._ai.taskBreakdown.suggestStrategy(item);
2093
+ }
2094
+
2095
+ /**
2096
+ * Flatten a breakdown tree into a list
2097
+ * @param {Object} breakdownResult - Result from breakdown()
2098
+ * @returns {Object[]} Flat list of all items
2099
+ */
2100
+ flattenBreakdown(breakdownResult) {
2101
+ this._requireAI();
2102
+ return this._ai.taskBreakdown.flatten(breakdownResult);
2103
+ }
2104
+
2105
+ /**
2106
+ * Get items in dependency order for execution
2107
+ * @param {Object} breakdownResult - Result from breakdown()
2108
+ * @returns {Object[]} Items in dependency order
2109
+ */
2110
+ getBreakdownDependencyOrder(breakdownResult) {
2111
+ this._requireAI();
2112
+ return this._ai.taskBreakdown.getDependencyOrder(breakdownResult);
2113
+ }
2114
+
2115
+ /**
2116
+ * Get progress on a breakdown tree
2117
+ * @param {string} holonId - Holon ID
2118
+ * @param {string} lensName - Lens name
2119
+ * @param {string} itemId - Root item ID
2120
+ * @returns {Promise<Object>} Progress summary
2121
+ */
2122
+ async getBreakdownProgress(holonId, lensName, itemId) {
2123
+ this._requireAI();
2124
+ return this._ai.taskBreakdown.getProgress(holonId, lensName, itemId);
2125
+ }
2126
+
2127
+ // --- H3 Geospatial AI ---
2128
+
2129
+ /**
2130
+ * Suggest optimal H3 resolution for a task/project
2131
+ * @param {Object} item - Item to analyze
2132
+ * @param {Object} options - Options including currentResolution
2133
+ * @returns {Promise<Object>} Resolution recommendation
2134
+ */
2135
+ async suggestH3Resolution(item, options = {}) {
2136
+ this._requireAI();
2137
+ return this._ai.h3ai.suggestResolution(item, options);
2138
+ }
2139
+
2140
+ /**
2141
+ * Analyze geographic distribution of data in a region
2142
+ * @param {string} holonId - Parent holon to analyze
2143
+ * @param {string} lensName - Lens to analyze
2144
+ * @param {Object} options - Options
2145
+ * @returns {Promise<Object>} Distribution analysis
2146
+ */
2147
+ async analyzeH3Distribution(holonId, lensName, options = {}) {
2148
+ this._requireAI();
2149
+ return this._ai.h3ai.analyzeDistribution(holonId, lensName, options);
2150
+ }
2151
+
2152
+ /**
2153
+ * Find relevant data from neighboring H3 cells
2154
+ * @param {string} holonId - Center holon
2155
+ * @param {string} lensName - Lens to search
2156
+ * @param {Object} options - Options including ringSize
2157
+ * @returns {Promise<Object>} Neighbor analysis
2158
+ */
2159
+ async findH3NeighborRelevance(holonId, lensName, options = {}) {
2160
+ this._requireAI();
2161
+ return this._ai.h3ai.findNeighborRelevance(holonId, lensName, options);
2162
+ }
2163
+
2164
+ /**
2165
+ * Suggest geographic expansion or contraction for an item
2166
+ * @param {Object} item - Item to analyze
2167
+ * @param {string} holonId - Current holon
2168
+ * @param {string} lensName - Lens
2169
+ * @param {Object} options - Options
2170
+ * @returns {Promise<Object>} Scope suggestions
2171
+ */
2172
+ async suggestGeographicScope(item, holonId, lensName, options = {}) {
2173
+ this._requireAI();
2174
+ return this._ai.h3ai.suggestGeographicScope(item, holonId, lensName, options);
2175
+ }
2176
+
2177
+ /**
2178
+ * Analyze coverage gaps in a region
2179
+ * @param {string} holonId - Region to analyze
2180
+ * @param {string} lensName - Lens
2181
+ * @param {Object} options - Options including targetResolution
2182
+ * @returns {Promise<Object>} Coverage analysis
2183
+ */
2184
+ async analyzeH3Coverage(holonId, lensName, options = {}) {
2185
+ this._requireAI();
2186
+ return this._ai.h3ai.analyzeCoverage(holonId, lensName, options);
2187
+ }
2188
+
2189
+ /**
2190
+ * Find patterns across multiple H3 resolutions
2191
+ * @param {string} holonId - Starting holon
2192
+ * @param {string} lensName - Lens
2193
+ * @param {Object} options - Options including levels
2194
+ * @returns {Promise<Object>} Cross-resolution insights
2195
+ */
2196
+ async crossH3ResolutionInsights(holonId, lensName, options = {}) {
2197
+ this._requireAI();
2198
+ return this._ai.h3ai.crossResolutionInsights(holonId, lensName, options);
2199
+ }
2200
+
2201
+ /**
2202
+ * Suggest item migration between holons based on geographic fit
2203
+ * @param {Object} item - Item to analyze
2204
+ * @param {string} holonId - Current location
2205
+ * @param {string} lensName - Lens
2206
+ * @param {Object} options - Options including searchRadius
2207
+ * @returns {Promise<Object>} Migration suggestions
2208
+ */
2209
+ async suggestH3Migration(item, holonId, lensName, options = {}) {
2210
+ this._requireAI();
2211
+ return this._ai.h3ai.suggestMigration(item, holonId, lensName, options);
2212
+ }
2213
+
2214
+ /**
2215
+ * Generate a geographic activity report for a region
2216
+ * @param {string} holonId - Region to report on
2217
+ * @param {Object} options - Options including lenses array
2218
+ * @returns {Promise<Object>} Geographic report
2219
+ */
2220
+ async generateH3Report(holonId, options = {}) {
2221
+ this._requireAI();
2222
+ return this._ai.h3ai.generateGeographicReport(holonId, options);
2223
+ }
2224
+
2225
+ /**
2226
+ * Find geographic clusters of related items
2227
+ * @param {string} holonId - Region to analyze
2228
+ * @param {string} lensName - Lens
2229
+ * @param {Object} options - Options including clusterResolution
2230
+ * @returns {Promise<Object>} Geographic clusters
2231
+ */
2232
+ async findH3Clusters(holonId, lensName, options = {}) {
2233
+ this._requireAI();
2234
+ return this._ai.h3ai.findGeographicClusters(holonId, lensName, options);
2235
+ }
2236
+
2237
+ /**
2238
+ * Analyze geographic impact of an item
2239
+ * @param {Object} item - Item to analyze
2240
+ * @param {string} holonId - Item's holon
2241
+ * @param {string} lensName - Lens
2242
+ * @param {Object} options - Options
2243
+ * @returns {Promise<Object>} Impact analysis
2244
+ */
2245
+ async analyzeH3Impact(item, holonId, lensName, options = {}) {
2246
+ this._requireAI();
2247
+ return this._ai.h3ai.analyzeGeographicImpact(item, holonId, lensName, options);
2248
+ }
2249
+
2250
+ // === Cross-Holosphere Federation ===
2251
+
2252
+ /**
2253
+ * Add a federated holosphere partner (manual key exchange)
2254
+ * @param {string} pubKey - Partner's public key
2255
+ * @param {Object} options - Federation options
2256
+ * @param {string} options.alias - Human-readable name for this partner
2257
+ * @param {Object[]} options.inboundCapabilities - Capabilities they've granted us
2258
+ * @returns {Promise<boolean>} Success indicator
2259
+ */
2260
+ async addFederatedHolosphere(pubKey, options = {}) {
2261
+ if (!pubKey || typeof pubKey !== 'string') {
2262
+ throw new ValidationError('pubKey must be a valid public key string');
2263
+ }
2264
+
2265
+ return registry.addFederatedPartner(
2266
+ this.client,
2267
+ this.config.appName,
2268
+ pubKey,
2269
+ options
2270
+ );
2271
+ }
2272
+
2273
+ /**
2274
+ * Remove a federated holosphere partner
2275
+ * @param {string} pubKey - Partner's public key
2276
+ * @returns {Promise<boolean>} Success indicator
2277
+ */
2278
+ async removeFederatedHolosphere(pubKey) {
2279
+ return registry.removeFederatedPartner(
2280
+ this.client,
2281
+ this.config.appName,
2282
+ pubKey
2283
+ );
2284
+ }
2285
+
2286
+ /**
2287
+ * Get all federated holosphere public keys
2288
+ * @returns {Promise<string[]>} Array of federated public keys
2289
+ */
2290
+ async getFederatedHolospheres() {
2291
+ return registry.getFederatedAuthors(
2292
+ this.client,
2293
+ this.config.appName
2294
+ );
2295
+ }
2296
+
2297
+ /**
2298
+ * Get the full federation registry for this holosphere
2299
+ * @returns {Promise<Object>} Federation registry
2300
+ */
2301
+ async getFederationRegistry() {
2302
+ return registry.getFederationRegistry(
2303
+ this.client,
2304
+ this.config.appName
2305
+ );
2306
+ }
2307
+
2308
+ /**
2309
+ * Store a capability token received from another holosphere
2310
+ * @param {string} partnerPubKey - Partner's public key
2311
+ * @param {Object} capabilityInfo - Capability information
2312
+ * @param {string} capabilityInfo.token - The capability token
2313
+ * @param {Object} capabilityInfo.scope - Token scope
2314
+ * @param {string[]} capabilityInfo.permissions - Granted permissions
2315
+ * @param {number} capabilityInfo.expires - Expiration timestamp
2316
+ * @returns {Promise<boolean>} Success indicator
2317
+ */
2318
+ async storeInboundCapability(partnerPubKey, capabilityInfo) {
2319
+ return registry.storeInboundCapability(
2320
+ this.client,
2321
+ this.config.appName,
2322
+ partnerPubKey,
2323
+ capabilityInfo
2324
+ );
2325
+ }
2326
+
2327
+ /**
2328
+ * Read data from a federated source (cross-holosphere)
2329
+ * @param {string} sourcePubKey - Source holosphere's public key
2330
+ * @param {string} holonId - Holon ID
2331
+ * @param {string} lensName - Lens name
2332
+ * @param {string} dataId - Optional specific data ID
2333
+ * @returns {Promise<Object|Object[]|null>} Data from federated source
2334
+ */
2335
+ async readFromFederatedSource(sourcePubKey, holonId, lensName, dataId = null) {
2336
+ // Get capability for this source
2337
+ const capabilityEntry = await registry.getCapabilityForAuthor(
2338
+ this.client,
2339
+ this.config.appName,
2340
+ sourcePubKey,
2341
+ { holonId, lensName, dataId }
2342
+ );
2343
+
2344
+ if (!capabilityEntry) {
2345
+ throw new AuthorizationError('No valid capability for federated source', 'read');
2346
+ }
2347
+
2348
+ // Verify capability is still valid
2349
+ const isValid = await this.verifyCapability(
2350
+ capabilityEntry.token,
2351
+ 'read',
2352
+ { holonId, lensName, dataId }
2353
+ );
2354
+
2355
+ if (!isValid) {
2356
+ throw new AuthorizationError('Capability verification failed', 'read');
2357
+ }
2358
+
2359
+ // Read with specific author
2360
+ if (dataId) {
2361
+ const path = storage.buildPath(this.config.appName, holonId, lensName, dataId);
2362
+ return nostrAsync.nostrGet(this.client, path, 30000, {
2363
+ authors: [sourcePubKey],
2364
+ includeAuthor: true,
2365
+ });
2366
+ } else {
2367
+ const path = storage.buildPath(this.config.appName, holonId, lensName);
2368
+ return nostrAsync.nostrGetAll(this.client, path, 30000, {
2369
+ authors: [sourcePubKey],
2370
+ includeAuthor: true,
2371
+ });
2372
+ }
2373
+ }
2374
+
2375
+ /**
2376
+ * Create a cross-holosphere hologram with embedded capability
2377
+ * @param {string} sourcePubKey - Source holosphere's public key
2378
+ * @param {string} sourceHolon - Source holon ID
2379
+ * @param {string} lensName - Lens name
2380
+ * @param {string} dataId - Data ID
2381
+ * @param {string} targetHolon - Target holon ID (where hologram will live)
2382
+ * @param {Object} options - Creation options
2383
+ * @param {boolean} options.embedCapability - Whether to embed capability in hologram (default: true)
2384
+ * @returns {Promise<Object>} Hologram object
2385
+ */
2386
+ async createCrossHolosphereHologram(sourcePubKey, sourceHolon, lensName, dataId, targetHolon, options = {}) {
2387
+ const { embedCapability = true } = options;
2388
+
2389
+ // Get capability for this source
2390
+ const capabilityEntry = await registry.getCapabilityForAuthor(
2391
+ this.client,
2392
+ this.config.appName,
2393
+ sourcePubKey,
2394
+ { holonId: sourceHolon, lensName, dataId }
2395
+ );
2396
+
2397
+ if (!capabilityEntry) {
2398
+ throw new AuthorizationError('No valid capability for cross-holosphere hologram', 'read');
2399
+ }
2400
+
2401
+ // Create hologram with embedded capability
2402
+ const hologram = federation.createHologram(
2403
+ sourceHolon,
2404
+ targetHolon,
2405
+ lensName,
2406
+ dataId,
2407
+ this.config.appName,
2408
+ {
2409
+ authorPubKey: sourcePubKey,
2410
+ capability: embedCapability ? capabilityEntry.token : null,
2411
+ }
2412
+ );
2413
+
2414
+ // Write hologram to target holon
2415
+ const targetPath = storage.buildPath(this.config.appName, targetHolon, lensName, dataId);
2416
+ await storage.write(this.client, targetPath, hologram);
2417
+
2418
+ return hologram;
2419
+ }
2420
+
2421
+ /**
2422
+ * Issue a capability token for federation with another holosphere
2423
+ * @param {string} targetPubKey - Recipient's public key
2424
+ * @param {Object} scope - Capability scope { holonId, lensName, dataId? }
2425
+ * @param {string[]} permissions - Array of permissions ['read', 'write', 'delete']
2426
+ * @param {Object} options - Additional options
2427
+ * @param {number} options.expiresIn - Expiration in ms (default: 1 hour)
2428
+ * @param {boolean} options.trackInRegistry - Store in registry (default: true)
2429
+ * @returns {Promise<string>} Capability token
2430
+ */
2431
+ async issueCapabilityForFederation(targetPubKey, scope, permissions, options = {}) {
2432
+ const { expiresIn = 3600000, trackInRegistry = true } = options;
2433
+
2434
+ const token = await crypto.issueCapability(
2435
+ permissions,
2436
+ scope,
2437
+ targetPubKey,
2438
+ {
2439
+ expiresIn,
2440
+ issuerKey: this.client.privateKey,
2441
+ }
2442
+ );
2443
+
2444
+ // Track in registry
2445
+ if (trackInRegistry) {
2446
+ // Ensure partner exists in registry
2447
+ const partner = await registry.getFederatedPartner(
2448
+ this.client,
2449
+ this.config.appName,
2450
+ targetPubKey
2451
+ );
2452
+
2453
+ if (!partner) {
2454
+ await registry.addFederatedPartner(
2455
+ this.client,
2456
+ this.config.appName,
2457
+ targetPubKey,
2458
+ { addedVia: 'capability_issued' }
2459
+ );
2460
+ }
2461
+
2462
+ await registry.storeOutboundCapability(
2463
+ this.client,
2464
+ this.config.appName,
2465
+ targetPubKey,
2466
+ {
2467
+ tokenHash: await this._hashToken(token),
2468
+ scope,
2469
+ permissions,
2470
+ expires: Date.now() + expiresIn,
2471
+ }
2472
+ );
2473
+ }
2474
+
2475
+ return token;
2476
+ }
2477
+
2478
+ /**
2479
+ * Hash a token for storage (don't store full token)
2480
+ * @private
2481
+ */
2482
+ async _hashToken(token) {
2483
+ const { sha256 } = await import('@noble/hashes/sha256');
2484
+ const { bytesToHex } = await import('@noble/hashes/utils');
2485
+ const encoder = new TextEncoder();
2486
+ return bytesToHex(sha256(encoder.encode(token)));
2487
+ }
2488
+
2489
+ // === Nostr Discovery Protocol (Optional) ===
2490
+
2491
+ /**
2492
+ * Send a federation request via Nostr
2493
+ * @param {string} targetPubKey - Target holosphere's public key
2494
+ * @param {Object} options - Request options
2495
+ * @param {Object} options.scope - Requested scope { holonId, lensName }
2496
+ * @param {string[]} options.permissions - Requested permissions
2497
+ * @param {string} options.message - Optional message
2498
+ * @returns {Promise<Object>} Published event info
2499
+ */
2500
+ async sendFederationRequest(targetPubKey, options = {}) {
2501
+ return discovery.sendFederationRequest(
2502
+ this.client,
2503
+ this.config.appName,
2504
+ targetPubKey,
2505
+ options
2506
+ );
2507
+ }
2508
+
2509
+ /**
2510
+ * Subscribe to incoming federation requests
2511
+ * @param {Function} callback - Called with each request
2512
+ * @returns {Promise<Object>} Subscription with unsubscribe method
2513
+ */
2514
+ async subscribeFederationRequests(callback) {
2515
+ return discovery.subscribeFederationRequests(this.client, callback);
2516
+ }
2517
+
2518
+ /**
2519
+ * Accept a federation request
2520
+ * @param {Object} request - Request object from subscription
2521
+ * @param {Object} options - Acceptance options
2522
+ * @returns {Promise<Object>} Published acceptance event info
2523
+ */
2524
+ async acceptFederationRequest(request, options = {}) {
2525
+ return discovery.acceptFederationRequest(
2526
+ this.client,
2527
+ this.config.appName,
2528
+ request,
2529
+ options
2530
+ );
2531
+ }
2532
+
2533
+ /**
2534
+ * Decline a federation request
2535
+ * @param {Object} request - Request object from subscription
2536
+ * @param {string} reason - Optional decline reason
2537
+ * @returns {Promise<Object>} Published decline event info
2538
+ */
2539
+ async declineFederationRequest(request, reason = '') {
2540
+ return discovery.declineFederationRequest(
2541
+ this.client,
2542
+ this.config.appName,
2543
+ request,
2544
+ reason
2545
+ );
2546
+ }
2547
+
2548
+ /**
2549
+ * Subscribe to federation acceptances (responses to our requests)
2550
+ * @param {Function} callback - Called with each acceptance
2551
+ * @returns {Promise<Object>} Subscription with unsubscribe method
2552
+ */
2553
+ async subscribeFederationAcceptances(callback) {
2554
+ return discovery.subscribeFederationAcceptances(
2555
+ this.client,
2556
+ this.config.appName,
2557
+ callback
2558
+ );
2559
+ }
2560
+
2561
+ /**
2562
+ * Subscribe to federation declines
2563
+ * @param {Function} callback - Called with each decline
2564
+ * @returns {Promise<Object>} Subscription with unsubscribe method
2565
+ */
2566
+ async subscribeFederationDeclines(callback) {
2567
+ return discovery.subscribeFederationDeclines(this.client, callback);
2568
+ }
2569
+
2570
+ /**
2571
+ * Get pending federation requests (requests we've sent that haven't been answered)
2572
+ * @param {Object} options - Query options
2573
+ * @returns {Promise<Object[]>} Array of pending requests
2574
+ */
2575
+ async getPendingFederationRequests(options = {}) {
2576
+ return discovery.getPendingFederationRequests(this.client, options);
2577
+ }
2578
+ }
2579
+
2580
+ // Export error classes
2581
+ export class AuthorizationError extends Error {
2582
+ constructor(message, requiredPermission = null) {
2583
+ super(message);
2584
+ this.name = 'AuthorizationError';
2585
+ this.requiredPermission = requiredPermission;
2586
+ }
2587
+ }
2588
+
2589
+ export class HolosphereError extends Error {
2590
+ constructor(message) {
2591
+ super(message);
2592
+ this.name = 'HolosphereError';
2593
+ }
2594
+ }
2595
+
2596
+ // Re-export ValidationError from validator module
2597
+ export { ValidationError } from './schema/validator.js';
2598
+
2599
+ // Export utilities
2600
+ export { nostrAsync };
2601
+
2602
+ // Export hologram helper functions
2603
+ export {
2604
+ isHologram,
2605
+ isResolvedHologram,
2606
+ getHologramSource,
2607
+ updateHologramOverrides,
2608
+ addActiveHologram,
2609
+ removeActiveHologram,
2610
+ refreshActiveHolograms,
2611
+ deleteHologram,
2612
+ propagateData,
2613
+ createHologram,
2614
+ resolveHologram,
2615
+ cleanupCircularHolograms,
2616
+ cleanupCircularHologramsByIds
2617
+ } from './federation/hologram.js';
2618
+
2619
+ // Export federation registry functions
2620
+ export {
2621
+ getFederationRegistry,
2622
+ addFederatedPartner,
2623
+ removeFederatedPartner,
2624
+ getFederatedAuthors,
2625
+ getCapabilityForAuthor,
2626
+ storeInboundCapability,
2627
+ storeOutboundCapability,
2628
+ cleanupExpiredCapabilities
2629
+ } from './federation/registry.js';
2630
+
2631
+ // Export Nostr discovery protocol functions
2632
+ export {
2633
+ sendFederationRequest,
2634
+ subscribeFederationRequests,
2635
+ acceptFederationRequest,
2636
+ declineFederationRequest,
2637
+ subscribeFederationAcceptances,
2638
+ subscribeFederationDeclines,
2639
+ getPendingFederationRequests
2640
+ } from './federation/discovery.js';
2641
+
2642
+ // Export scope matching utility
2643
+ export { matchScope } from './crypto/secp256k1.js';
2644
+
2645
+ // Export AI modules for standalone use
2646
+ export {
2647
+ LLMService,
2648
+ SchemaExtractor,
2649
+ JSONOps,
2650
+ Embeddings,
2651
+ Council,
2652
+ TTS,
2653
+ VOICES,
2654
+ MODELS,
2655
+ NLQuery,
2656
+ Classifier,
2657
+ SpatialAnalysis,
2658
+ SmartAggregation,
2659
+ FederationAdvisor,
2660
+ RelationshipDiscovery,
2661
+ TaskBreakdown,
2662
+ H3AI
2663
+ };
2664
+
2665
+ // Export AI factory function
2666
+ export { createAIServices } from './ai/index.js';
2667
+
2668
+ // Default export for backward compatibility
2669
+ export default HoloSphere;