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.
- package/.env.example +36 -0
- package/.eslintrc.json +16 -0
- package/.prettierrc.json +7 -0
- package/LICENSE +162 -38
- package/README.md +483 -367
- package/bin/holosphere-activitypub.js +158 -0
- package/cleanup-test-data.js +204 -0
- package/examples/demo.html +1333 -0
- package/examples/example-bot.js +197 -0
- package/package.json +47 -87
- package/scripts/check-bundle-size.js +54 -0
- package/scripts/check-quest-ids.js +77 -0
- package/scripts/import-holons.js +578 -0
- package/scripts/publish-to-relay.js +101 -0
- package/scripts/read-example.js +186 -0
- package/scripts/relay-diagnostic.js +59 -0
- package/scripts/relay-example.js +179 -0
- package/scripts/resync-to-relay.js +245 -0
- package/scripts/revert-import.js +196 -0
- package/scripts/test-hybrid-mode.js +108 -0
- package/scripts/test-local-storage.js +63 -0
- package/scripts/test-nostr-direct.js +55 -0
- package/scripts/test-read-data.js +45 -0
- package/scripts/test-write-read.js +63 -0
- package/scripts/verify-import.js +95 -0
- package/scripts/verify-relay-data.js +139 -0
- package/src/ai/aggregation.js +319 -0
- package/src/ai/breakdown.js +511 -0
- package/src/ai/classifier.js +217 -0
- package/src/ai/council.js +228 -0
- package/src/ai/embeddings.js +279 -0
- package/src/ai/federation-ai.js +324 -0
- package/src/ai/h3-ai.js +955 -0
- package/src/ai/index.js +112 -0
- package/src/ai/json-ops.js +225 -0
- package/src/ai/llm-service.js +205 -0
- package/src/ai/nl-query.js +223 -0
- package/src/ai/relationships.js +353 -0
- package/src/ai/schema-extractor.js +218 -0
- package/src/ai/spatial.js +293 -0
- package/src/ai/tts.js +194 -0
- package/src/content/social-protocols.js +168 -0
- package/src/core/holosphere.js +273 -0
- package/src/crypto/secp256k1.js +259 -0
- package/src/federation/discovery.js +334 -0
- package/src/federation/hologram.js +1042 -0
- package/src/federation/registry.js +386 -0
- package/src/hierarchical/upcast.js +110 -0
- package/src/index.js +2669 -0
- package/src/schema/validator.js +91 -0
- package/src/spatial/h3-operations.js +110 -0
- package/src/storage/backend-factory.js +125 -0
- package/src/storage/backend-interface.js +142 -0
- package/src/storage/backends/activitypub/server.js +653 -0
- package/src/storage/backends/activitypub-backend.js +272 -0
- package/src/storage/backends/gundb-backend.js +233 -0
- package/src/storage/backends/nostr-backend.js +136 -0
- package/src/storage/filesystem-storage-browser.js +41 -0
- package/src/storage/filesystem-storage.js +138 -0
- package/src/storage/global-tables.js +81 -0
- package/src/storage/gun-async.js +281 -0
- package/src/storage/gun-wrapper.js +221 -0
- package/src/storage/indexeddb-storage.js +122 -0
- package/src/storage/key-storage-simple.js +76 -0
- package/src/storage/key-storage.js +136 -0
- package/src/storage/memory-storage.js +59 -0
- package/src/storage/migration.js +338 -0
- package/src/storage/nostr-async.js +811 -0
- package/src/storage/nostr-client.js +939 -0
- package/src/storage/nostr-wrapper.js +211 -0
- package/src/storage/outbox-queue.js +208 -0
- package/src/storage/persistent-storage.js +109 -0
- package/src/storage/sync-service.js +164 -0
- package/src/subscriptions/manager.js +142 -0
- package/test-ai-real-api.js +202 -0
- package/tests/unit/ai/aggregation.test.js +295 -0
- package/tests/unit/ai/breakdown.test.js +446 -0
- package/tests/unit/ai/classifier.test.js +294 -0
- package/tests/unit/ai/council.test.js +262 -0
- package/tests/unit/ai/embeddings.test.js +384 -0
- package/tests/unit/ai/federation-ai.test.js +344 -0
- package/tests/unit/ai/h3-ai.test.js +458 -0
- package/tests/unit/ai/index.test.js +304 -0
- package/tests/unit/ai/json-ops.test.js +307 -0
- package/tests/unit/ai/llm-service.test.js +390 -0
- package/tests/unit/ai/nl-query.test.js +383 -0
- package/tests/unit/ai/relationships.test.js +311 -0
- package/tests/unit/ai/schema-extractor.test.js +384 -0
- package/tests/unit/ai/spatial.test.js +279 -0
- package/tests/unit/ai/tts.test.js +279 -0
- package/tests/unit/content.test.js +332 -0
- package/tests/unit/contract/core.test.js +88 -0
- package/tests/unit/contract/crypto.test.js +198 -0
- package/tests/unit/contract/data.test.js +223 -0
- package/tests/unit/contract/federation.test.js +181 -0
- package/tests/unit/contract/hierarchical.test.js +113 -0
- package/tests/unit/contract/schema.test.js +114 -0
- package/tests/unit/contract/social.test.js +217 -0
- package/tests/unit/contract/spatial.test.js +110 -0
- package/tests/unit/contract/subscriptions.test.js +128 -0
- package/tests/unit/contract/utils.test.js +159 -0
- package/tests/unit/core.test.js +152 -0
- package/tests/unit/crypto.test.js +328 -0
- package/tests/unit/federation.test.js +234 -0
- package/tests/unit/gun-async.test.js +252 -0
- package/tests/unit/hierarchical.test.js +399 -0
- package/tests/unit/integration/scenario-01-geographic-storage.test.js +74 -0
- package/tests/unit/integration/scenario-02-federation.test.js +76 -0
- package/tests/unit/integration/scenario-03-subscriptions.test.js +102 -0
- package/tests/unit/integration/scenario-04-validation.test.js +129 -0
- package/tests/unit/integration/scenario-05-hierarchy.test.js +125 -0
- package/tests/unit/integration/scenario-06-social.test.js +135 -0
- package/tests/unit/integration/scenario-07-persistence.test.js +130 -0
- package/tests/unit/integration/scenario-08-authorization.test.js +161 -0
- package/tests/unit/integration/scenario-09-cross-dimensional.test.js +139 -0
- package/tests/unit/integration/scenario-10-cross-holosphere-capabilities.test.js +357 -0
- package/tests/unit/integration/scenario-11-cross-holosphere-federation.test.js +410 -0
- package/tests/unit/integration/scenario-12-capability-federated-read.test.js +719 -0
- package/tests/unit/performance/benchmark.test.js +85 -0
- package/tests/unit/schema.test.js +213 -0
- package/tests/unit/spatial.test.js +158 -0
- package/tests/unit/storage.test.js +195 -0
- package/tests/unit/subscriptions.test.js +328 -0
- package/tests/unit/test-data-permanence-debug.js +197 -0
- package/tests/unit/test-data-permanence.js +340 -0
- package/tests/unit/test-key-persistence-fixed.js +148 -0
- package/tests/unit/test-key-persistence.js +172 -0
- package/tests/unit/test-relay-permanence.js +376 -0
- package/tests/unit/test-second-node.js +95 -0
- package/tests/unit/test-simple-write.js +89 -0
- package/vite.config.js +49 -0
- package/vitest.config.js +20 -0
- package/FEDERATION.md +0 -213
- package/compute.js +0 -298
- package/content.js +0 -980
- package/federation.js +0 -1234
- package/global.js +0 -736
- package/hexlib.js +0 -335
- package/hologram.js +0 -183
- package/holosphere-bundle.esm.js +0 -33256
- package/holosphere-bundle.js +0 -33287
- package/holosphere-bundle.min.js +0 -39
- package/holosphere.d.ts +0 -601
- package/holosphere.js +0 -719
- package/node.js +0 -246
- package/schema.js +0 -139
- 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;
|