holosphere 1.1.20 โ†’ 2.0.0-alpha1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/.env.example +36 -0
  2. package/.eslintrc.json +16 -0
  3. package/.prettierrc.json +7 -0
  4. package/LICENSE +162 -38
  5. package/README.md +483 -367
  6. package/bin/holosphere-activitypub.js +158 -0
  7. package/cleanup-test-data.js +204 -0
  8. package/examples/demo.html +1333 -0
  9. package/examples/example-bot.js +197 -0
  10. package/package.json +47 -87
  11. package/scripts/check-bundle-size.js +54 -0
  12. package/scripts/check-quest-ids.js +77 -0
  13. package/scripts/import-holons.js +578 -0
  14. package/scripts/publish-to-relay.js +101 -0
  15. package/scripts/read-example.js +186 -0
  16. package/scripts/relay-diagnostic.js +59 -0
  17. package/scripts/relay-example.js +179 -0
  18. package/scripts/resync-to-relay.js +245 -0
  19. package/scripts/revert-import.js +196 -0
  20. package/scripts/test-hybrid-mode.js +108 -0
  21. package/scripts/test-local-storage.js +63 -0
  22. package/scripts/test-nostr-direct.js +55 -0
  23. package/scripts/test-read-data.js +45 -0
  24. package/scripts/test-write-read.js +63 -0
  25. package/scripts/verify-import.js +95 -0
  26. package/scripts/verify-relay-data.js +139 -0
  27. package/src/ai/aggregation.js +319 -0
  28. package/src/ai/breakdown.js +511 -0
  29. package/src/ai/classifier.js +217 -0
  30. package/src/ai/council.js +228 -0
  31. package/src/ai/embeddings.js +279 -0
  32. package/src/ai/federation-ai.js +324 -0
  33. package/src/ai/h3-ai.js +955 -0
  34. package/src/ai/index.js +112 -0
  35. package/src/ai/json-ops.js +225 -0
  36. package/src/ai/llm-service.js +205 -0
  37. package/src/ai/nl-query.js +223 -0
  38. package/src/ai/relationships.js +353 -0
  39. package/src/ai/schema-extractor.js +218 -0
  40. package/src/ai/spatial.js +293 -0
  41. package/src/ai/tts.js +194 -0
  42. package/src/content/social-protocols.js +168 -0
  43. package/src/core/holosphere.js +273 -0
  44. package/src/crypto/secp256k1.js +259 -0
  45. package/src/federation/discovery.js +334 -0
  46. package/src/federation/hologram.js +1042 -0
  47. package/src/federation/registry.js +386 -0
  48. package/src/hierarchical/upcast.js +110 -0
  49. package/src/index.js +2669 -0
  50. package/src/schema/validator.js +91 -0
  51. package/src/spatial/h3-operations.js +110 -0
  52. package/src/storage/backend-factory.js +125 -0
  53. package/src/storage/backend-interface.js +142 -0
  54. package/src/storage/backends/activitypub/server.js +653 -0
  55. package/src/storage/backends/activitypub-backend.js +272 -0
  56. package/src/storage/backends/gundb-backend.js +233 -0
  57. package/src/storage/backends/nostr-backend.js +136 -0
  58. package/src/storage/filesystem-storage-browser.js +41 -0
  59. package/src/storage/filesystem-storage.js +138 -0
  60. package/src/storage/global-tables.js +81 -0
  61. package/src/storage/gun-async.js +281 -0
  62. package/src/storage/gun-wrapper.js +221 -0
  63. package/src/storage/indexeddb-storage.js +122 -0
  64. package/src/storage/key-storage-simple.js +76 -0
  65. package/src/storage/key-storage.js +136 -0
  66. package/src/storage/memory-storage.js +59 -0
  67. package/src/storage/migration.js +338 -0
  68. package/src/storage/nostr-async.js +811 -0
  69. package/src/storage/nostr-client.js +939 -0
  70. package/src/storage/nostr-wrapper.js +211 -0
  71. package/src/storage/outbox-queue.js +208 -0
  72. package/src/storage/persistent-storage.js +109 -0
  73. package/src/storage/sync-service.js +164 -0
  74. package/src/subscriptions/manager.js +142 -0
  75. package/test-ai-real-api.js +202 -0
  76. package/tests/unit/ai/aggregation.test.js +295 -0
  77. package/tests/unit/ai/breakdown.test.js +446 -0
  78. package/tests/unit/ai/classifier.test.js +294 -0
  79. package/tests/unit/ai/council.test.js +262 -0
  80. package/tests/unit/ai/embeddings.test.js +384 -0
  81. package/tests/unit/ai/federation-ai.test.js +344 -0
  82. package/tests/unit/ai/h3-ai.test.js +458 -0
  83. package/tests/unit/ai/index.test.js +304 -0
  84. package/tests/unit/ai/json-ops.test.js +307 -0
  85. package/tests/unit/ai/llm-service.test.js +390 -0
  86. package/tests/unit/ai/nl-query.test.js +383 -0
  87. package/tests/unit/ai/relationships.test.js +311 -0
  88. package/tests/unit/ai/schema-extractor.test.js +384 -0
  89. package/tests/unit/ai/spatial.test.js +279 -0
  90. package/tests/unit/ai/tts.test.js +279 -0
  91. package/tests/unit/content.test.js +332 -0
  92. package/tests/unit/contract/core.test.js +88 -0
  93. package/tests/unit/contract/crypto.test.js +198 -0
  94. package/tests/unit/contract/data.test.js +223 -0
  95. package/tests/unit/contract/federation.test.js +181 -0
  96. package/tests/unit/contract/hierarchical.test.js +113 -0
  97. package/tests/unit/contract/schema.test.js +114 -0
  98. package/tests/unit/contract/social.test.js +217 -0
  99. package/tests/unit/contract/spatial.test.js +110 -0
  100. package/tests/unit/contract/subscriptions.test.js +128 -0
  101. package/tests/unit/contract/utils.test.js +159 -0
  102. package/tests/unit/core.test.js +152 -0
  103. package/tests/unit/crypto.test.js +328 -0
  104. package/tests/unit/federation.test.js +234 -0
  105. package/tests/unit/gun-async.test.js +252 -0
  106. package/tests/unit/hierarchical.test.js +399 -0
  107. package/tests/unit/integration/scenario-01-geographic-storage.test.js +74 -0
  108. package/tests/unit/integration/scenario-02-federation.test.js +76 -0
  109. package/tests/unit/integration/scenario-03-subscriptions.test.js +102 -0
  110. package/tests/unit/integration/scenario-04-validation.test.js +129 -0
  111. package/tests/unit/integration/scenario-05-hierarchy.test.js +125 -0
  112. package/tests/unit/integration/scenario-06-social.test.js +135 -0
  113. package/tests/unit/integration/scenario-07-persistence.test.js +130 -0
  114. package/tests/unit/integration/scenario-08-authorization.test.js +161 -0
  115. package/tests/unit/integration/scenario-09-cross-dimensional.test.js +139 -0
  116. package/tests/unit/integration/scenario-10-cross-holosphere-capabilities.test.js +357 -0
  117. package/tests/unit/integration/scenario-11-cross-holosphere-federation.test.js +410 -0
  118. package/tests/unit/integration/scenario-12-capability-federated-read.test.js +719 -0
  119. package/tests/unit/performance/benchmark.test.js +85 -0
  120. package/tests/unit/schema.test.js +213 -0
  121. package/tests/unit/spatial.test.js +158 -0
  122. package/tests/unit/storage.test.js +195 -0
  123. package/tests/unit/subscriptions.test.js +328 -0
  124. package/tests/unit/test-data-permanence-debug.js +197 -0
  125. package/tests/unit/test-data-permanence.js +340 -0
  126. package/tests/unit/test-key-persistence-fixed.js +148 -0
  127. package/tests/unit/test-key-persistence.js +172 -0
  128. package/tests/unit/test-relay-permanence.js +376 -0
  129. package/tests/unit/test-second-node.js +95 -0
  130. package/tests/unit/test-simple-write.js +89 -0
  131. package/vite.config.js +49 -0
  132. package/vitest.config.js +20 -0
  133. package/FEDERATION.md +0 -213
  134. package/compute.js +0 -298
  135. package/content.js +0 -980
  136. package/federation.js +0 -1234
  137. package/global.js +0 -736
  138. package/hexlib.js +0 -335
  139. package/hologram.js +0 -183
  140. package/holosphere-bundle.esm.js +0 -33256
  141. package/holosphere-bundle.js +0 -33287
  142. package/holosphere-bundle.min.js +0 -39
  143. package/holosphere.d.ts +0 -601
  144. package/holosphere.js +0 -719
  145. package/node.js +0 -246
  146. package/schema.js +0 -139
  147. package/utils.js +0 -302
@@ -0,0 +1,1042 @@
1
+ /**
2
+ * Federation and Hologram (reference) Management
3
+ */
4
+
5
+ import { buildPath, write, read, update } from '../storage/nostr-wrapper.js';
6
+ import { verifyCapability } from '../crypto/secp256k1.js';
7
+ import { getCapabilityForAuthor } from './registry.js';
8
+
9
+ const MAX_RESOLUTION_DEPTH = 10;
10
+
11
+ /**
12
+ * Check if creating a hologram would result in a circular reference
13
+ * This follows the chain from the source to see if it eventually points back to the target
14
+ * @param {Object} client - Nostr client instance
15
+ * @param {string} appname - Application namespace
16
+ * @param {string} sourceHolon - Source holon ID (where original data lives)
17
+ * @param {string} targetHolon - Target holon ID (where hologram would be created)
18
+ * @param {string} lensName - Lens name
19
+ * @param {string} dataId - Data ID
20
+ * @returns {Promise<Object>} Result with { isCircular, chain }
21
+ */
22
+ export async function wouldCreateCircularHologram(client, appname, sourceHolon, targetHolon, lensName, dataId) {
23
+ const visited = new Set();
24
+ const chain = [];
25
+
26
+ let currentHolon = sourceHolon;
27
+ let currentLens = lensName;
28
+ let currentDataId = dataId;
29
+ let currentAppname = appname;
30
+
31
+ // Follow the chain to see if we eventually reach targetHolon
32
+ while (visited.size < MAX_RESOLUTION_DEPTH) {
33
+ const key = `${currentAppname}:${currentHolon}:${currentLens}:${currentDataId}`;
34
+
35
+ if (visited.has(key)) {
36
+ // Already visited this node - there's a cycle in the existing chain
37
+ return { isCircular: true, chain, reason: 'existing_cycle' };
38
+ }
39
+
40
+ visited.add(key);
41
+ chain.push({ holon: currentHolon, lens: currentLens, dataId: currentDataId });
42
+
43
+ // Check if current holon is our target - this would create a circular reference
44
+ if (currentHolon === targetHolon) {
45
+ return { isCircular: true, chain, reason: 'would_create_cycle' };
46
+ }
47
+
48
+ // Read the data at current location
49
+ const path = buildPath(currentAppname, currentHolon, currentLens, currentDataId);
50
+ const data = await read(client, path);
51
+
52
+ if (!data) {
53
+ // Data doesn't exist - no circular reference possible from this chain
54
+ return { isCircular: false, chain };
55
+ }
56
+
57
+ if (!data.hologram || !data.target) {
58
+ // Not a hologram - chain ends here, no circular reference
59
+ return { isCircular: false, chain };
60
+ }
61
+
62
+ // Follow the hologram to its target
63
+ currentAppname = data.target.appname || currentAppname;
64
+ currentHolon = data.target.holonId;
65
+ currentLens = data.target.lensName || currentLens;
66
+ currentDataId = data.target.dataId || currentDataId;
67
+ }
68
+
69
+ // Max depth reached - treat as potential circular to be safe
70
+ return { isCircular: true, chain, reason: 'max_depth_exceeded' };
71
+ }
72
+
73
+ /**
74
+ * Create a hologram (lightweight reference)
75
+ * @param {string} sourceHolon - Source holon ID
76
+ * @param {string} targetHolon - Target holon ID
77
+ * @param {string} lensName - Lens name
78
+ * @param {string} dataId - Data ID
79
+ * @param {string} appname - Application namespace
80
+ * @param {Object} options - Optional cross-holosphere settings
81
+ * @param {string} options.authorPubKey - Source author's public key (for cross-holosphere)
82
+ * @param {string} options.capability - Capability token (for cross-holosphere)
83
+ * @returns {Object} Hologram object
84
+ */
85
+ export function createHologram(sourceHolon, targetHolon, lensName, dataId, appname, options = {}) {
86
+ const { authorPubKey = null, capability = null } = options;
87
+
88
+ const soul = buildPath(appname, sourceHolon, lensName, dataId);
89
+
90
+ const hologram = {
91
+ id: dataId,
92
+ hologram: true,
93
+ soul,
94
+ target: {
95
+ appname,
96
+ holonId: sourceHolon,
97
+ lensName,
98
+ dataId,
99
+ },
100
+ _meta: {
101
+ created: Date.now(),
102
+ sourceHolon,
103
+ source: sourceHolon, // Alias for compatibility
104
+ },
105
+ };
106
+
107
+ // Cross-holosphere enhancements
108
+ if (authorPubKey) {
109
+ hologram.crossHolosphere = true;
110
+ hologram.target.authorPubKey = authorPubKey;
111
+ hologram._meta.sourcePubKey = authorPubKey;
112
+ }
113
+
114
+ if (capability) {
115
+ hologram.capability = capability;
116
+ hologram._meta.grantedAt = Date.now();
117
+ }
118
+
119
+ return hologram;
120
+ }
121
+
122
+ /**
123
+ * Resolve hologram to actual data, merging local overrides
124
+ * Supports cross-holosphere holograms with capability token verification
125
+ * @param {Object} client - Nostr client instance
126
+ * @param {Object} hologram - Hologram object
127
+ * @param {Set} visited - Visited souls (circular reference detection)
128
+ * @param {Array} chain - Chain of souls for debugging circular references
129
+ * @param {Object} options - Resolution options
130
+ * @param {string} options.appname - Application namespace (for registry lookup)
131
+ * @param {boolean} options.deleteCircular - If true, delete circular holograms when detected (default: true)
132
+ * @param {string} options.hologramPath - Path of the hologram being resolved (for deletion)
133
+ * @returns {Promise<Object|null>} Resolved data with _hologram metadata, or null
134
+ */
135
+ export async function resolveHologram(client, hologram, visited = new Set(), chain = [], options = {}) {
136
+ const { deleteCircular = true, hologramPath = null } = options;
137
+
138
+ if (!hologram || !hologram.hologram) {
139
+ return hologram; // Not a hologram, return as-is
140
+ }
141
+
142
+ const { soul } = hologram;
143
+ const target = hologram.target || {};
144
+
145
+ // Circular reference detection
146
+ if (visited.has(soul)) {
147
+ const chainStr = [...chain, soul].join(' โ†’ ');
148
+ console.warn(`๐Ÿ”„ Circular reference detected - removing hologram`);
149
+ console.warn(` Soul: ${soul}`);
150
+ console.warn(` Chain: ${chainStr}`);
151
+ console.warn(` Source holon: ${target.holonId || 'unknown'}`);
152
+ console.warn(` Lens: ${target.lensName || 'unknown'}`);
153
+ console.warn(` Data ID: ${target.dataId || hologram.id || 'unknown'}`);
154
+ console.warn(` Resolution depth: ${visited.size}`);
155
+
156
+ // Delete the circular hologram
157
+ if (deleteCircular && hologramPath) {
158
+ console.info(` ๐Ÿ—‘๏ธ Deleting circular hologram at: ${hologramPath}`);
159
+ await write(client, hologramPath, null);
160
+ }
161
+
162
+ return null;
163
+ }
164
+
165
+ if (visited.size >= MAX_RESOLUTION_DEPTH) {
166
+ const chainStr = [...chain, soul].join(' โ†’ ');
167
+ console.warn(`โš ๏ธ Max resolution depth (${MAX_RESOLUTION_DEPTH}) exceeded - removing hologram`);
168
+ console.warn(` Current soul: ${soul}`);
169
+ console.warn(` Chain: ${chainStr}`);
170
+ console.warn(` Source holon: ${target.holonId || 'unknown'}`);
171
+ console.warn(` Lens: ${target.lensName || 'unknown'}`);
172
+
173
+ // Delete the problematic hologram
174
+ if (deleteCircular && hologramPath) {
175
+ console.info(` ๐Ÿ—‘๏ธ Deleting deep chain hologram at: ${hologramPath}`);
176
+ await write(client, hologramPath, null);
177
+ }
178
+
179
+ return null;
180
+ }
181
+
182
+ visited.add(soul);
183
+ chain.push(soul);
184
+
185
+ let sourceData;
186
+
187
+ // Handle cross-holosphere holograms
188
+ if (hologram.crossHolosphere && target.authorPubKey) {
189
+ // Get capability - try embedded first, then registry fallback
190
+ let capability = hologram.capability;
191
+
192
+ if (!capability && options.appname) {
193
+ // Fallback to registry lookup
194
+ const capEntry = await getCapabilityForAuthor(
195
+ client,
196
+ options.appname,
197
+ target.authorPubKey,
198
+ {
199
+ holonId: target.holonId,
200
+ lensName: target.lensName,
201
+ dataId: target.dataId,
202
+ }
203
+ );
204
+ if (capEntry) {
205
+ capability = capEntry.token;
206
+ }
207
+ }
208
+
209
+ // Verify capability before resolving
210
+ if (!capability) {
211
+ console.warn(`โŒ Cross-holosphere hologram missing capability: ${soul}`);
212
+ return null;
213
+ }
214
+
215
+ const isValid = await verifyCapability(
216
+ capability,
217
+ 'read',
218
+ {
219
+ holonId: target.holonId,
220
+ lensName: target.lensName,
221
+ dataId: target.dataId,
222
+ }
223
+ );
224
+
225
+ if (!isValid) {
226
+ console.warn(`โŒ Capability verification failed for cross-holosphere hologram: ${soul}`);
227
+ return null;
228
+ }
229
+
230
+ // Read from federated source (different author)
231
+ sourceData = await read(client, soul, {
232
+ authors: [target.authorPubKey]
233
+ });
234
+ } else {
235
+ // Same-holosphere resolution (original behavior)
236
+ sourceData = await read(client, soul);
237
+ }
238
+
239
+ if (!sourceData) {
240
+ return null;
241
+ }
242
+
243
+ // If source data is also a hologram, recursively resolve
244
+ if (sourceData.hologram) {
245
+ // Pass the soul as the hologramPath so circular holograms can be deleted
246
+ return resolveHologram(client, sourceData, visited, chain, { ...options, hologramPath: soul });
247
+ }
248
+
249
+ // Merge hologram with source data
250
+ return mergeHologramWithSource(hologram, sourceData);
251
+ }
252
+
253
+ /**
254
+ * Merge hologram local overrides with source data
255
+ * @private
256
+ * @param {Object} hologram - Hologram object
257
+ * @param {Object} sourceData - Source data
258
+ * @returns {Object} Merged data with _hologram metadata
259
+ */
260
+ function mergeHologramWithSource(hologram, sourceData) {
261
+ // Extract local overrides from the hologram (exclude hologram-specific fields)
262
+ const hologramOnlyFields = ['hologram', 'soul', 'target', '_meta', 'id', 'capability', 'crossHolosphere'];
263
+ const localOverrides = {};
264
+ const localOverrideKeys = [];
265
+
266
+ for (const key of Object.keys(hologram)) {
267
+ if (!hologramOnlyFields.includes(key)) {
268
+ localOverrides[key] = hologram[key];
269
+ localOverrideKeys.push(key);
270
+ }
271
+ }
272
+
273
+ // Merge: source data + local overrides + hologram metadata
274
+ const sourceHolon = hologram._meta?.sourceHolon || hologram._meta?.source || hologram.target?.holonId;
275
+ const resolved = {
276
+ ...sourceData, // Original data from source
277
+ ...localOverrides, // Local overrides (x, y, pinned, etc.)
278
+ _hologram: {
279
+ isHologram: true,
280
+ soul: hologram.soul,
281
+ sourceHolon: sourceHolon,
282
+ localOverrides: localOverrideKeys,
283
+ crossHolosphere: hologram.crossHolosphere || false,
284
+ sourcePubKey: hologram._meta?.sourcePubKey || null,
285
+ }
286
+ };
287
+
288
+ // Preserve original _meta and add source from hologram
289
+ if (sourceData._meta) {
290
+ resolved._meta = { ...sourceData._meta, source: sourceHolon };
291
+ } else {
292
+ resolved._meta = { source: sourceHolon };
293
+ }
294
+
295
+ return resolved;
296
+ }
297
+
298
+ /**
299
+ * Setup federation between holons
300
+ * @param {Object} client - Nostr client instance
301
+ * @param {string} appname - Application namespace
302
+ * @param {string} sourceHolon - Source holon ID
303
+ * @param {string} targetHolon - Target holon ID
304
+ * @param {string} lensName - Lens name
305
+ * @param {Object} options - Federation options
306
+ * @param {string} options.direction - 'inbound' or 'outbound'
307
+ * @param {string} options.mode - 'reference' or 'copy'
308
+ * @returns {Promise<boolean>} Success indicator
309
+ */
310
+ export async function setupFederation(
311
+ client,
312
+ appname,
313
+ sourceHolon,
314
+ targetHolon,
315
+ lensName,
316
+ options = {}
317
+ ) {
318
+ const { direction = 'outbound', mode = 'reference' } = options;
319
+
320
+ const federationConfig = {
321
+ sourceHolon,
322
+ targetHolon,
323
+ lensName,
324
+ direction,
325
+ mode,
326
+ created: Date.now(),
327
+ };
328
+
329
+ // Store federation config
330
+ const configPath = buildPath(appname, sourceHolon, lensName, '_federation');
331
+ return write(client, configPath, federationConfig);
332
+ }
333
+
334
+ /**
335
+ * Propagate data to federated location
336
+ * @param {Object} client - Nostr client instance
337
+ * @param {string} appname - Application namespace
338
+ * @param {Object} data - Data to propagate
339
+ * @param {string} sourceHolon - Source holon ID
340
+ * @param {string} targetHolon - Target holon ID
341
+ * @param {string} lensName - Lens name
342
+ * @param {string} mode - 'reference' or 'copy'
343
+ * @returns {Promise<boolean>} Success indicator
344
+ */
345
+ export async function propagateData(
346
+ client,
347
+ appname,
348
+ data,
349
+ sourceHolon,
350
+ targetHolon,
351
+ lensName,
352
+ mode = 'reference'
353
+ ) {
354
+ const dataId = data.id;
355
+
356
+ // Don't create hologram to the same holon - it would point to itself
357
+ if (sourceHolon === targetHolon) {
358
+ console.info(`โญ๏ธ Skipping propagation - source and target are the same holon: ${sourceHolon}`);
359
+ return true;
360
+ }
361
+
362
+ // Don't propagate holograms back to avoid circular references
363
+ // If this data is already a hologram pointing to the target, skip it
364
+ if (data.hologram === true) {
365
+ // Check if this hologram would create a circular reference
366
+ if (data.target && data.target.holonId === targetHolon) {
367
+ console.info(`๐Ÿ”„ Skipping propagation - would create circular reference:`);
368
+ console.info(` Data ID: ${dataId}`);
369
+ console.info(` Source holon: ${sourceHolon}`);
370
+ console.info(` Target holon: ${targetHolon}`);
371
+ console.info(` Hologram points to: ${data.target.holonId}`);
372
+ console.info(` Lens: ${lensName}`);
373
+ console.info(` Original soul: ${data.soul || 'unknown'}`);
374
+ return true;
375
+ }
376
+
377
+ // Check if targetHolon is already in the original source's activeHolograms
378
+ // This prevents creating duplicate holograms when the target already has one
379
+ if (data.target) {
380
+ const originalSourcePath = buildPath(
381
+ data.target.appname || appname,
382
+ data.target.holonId,
383
+ data.target.lensName || lensName,
384
+ data.target.dataId || dataId
385
+ );
386
+ const originalSource = await read(client, originalSourcePath);
387
+
388
+ if (originalSource?._meta?.activeHolograms) {
389
+ const alreadyHasHologram = originalSource._meta.activeHolograms.some(
390
+ h => h.targetHolon === targetHolon
391
+ );
392
+ if (alreadyHasHologram) {
393
+ console.info(`โญ๏ธ Skipping propagation - target already in activeHolograms:`);
394
+ console.info(` Data ID: ${dataId}`);
395
+ console.info(` Original source: ${data.target.holonId}`);
396
+ console.info(` Target holon: ${targetHolon}`);
397
+ return true;
398
+ }
399
+ }
400
+ }
401
+
402
+ // Deep circular check for hologram copy - ensure the original source doesn't eventually point to target
403
+ const originalSourceHolon = data.target.holonId;
404
+ const originalDataId = data.target.dataId || dataId;
405
+ const originalLens = data.target.lensName || lensName;
406
+ const originalAppname = data.target.appname || appname;
407
+
408
+ const circularCheck = await wouldCreateCircularHologram(
409
+ client, originalAppname, originalSourceHolon, targetHolon, originalLens, originalDataId
410
+ );
411
+
412
+ if (circularCheck.isCircular) {
413
+ const chainStr = circularCheck.chain.map(c => c.holon).join(' โ†’ ');
414
+ console.warn(`๐Ÿ”„ Preventing circular hologram copy:`);
415
+ console.warn(` Data ID: ${dataId}`);
416
+ console.warn(` Original source: ${originalSourceHolon}`);
417
+ console.warn(` Would copy to: ${targetHolon}`);
418
+ console.warn(` Existing chain: ${chainStr}`);
419
+ console.warn(` Reason: ${circularCheck.reason}`);
420
+ return false;
421
+ }
422
+
423
+ // When propagating a hologram, copy it instead of creating a hologram of a hologram
424
+ // This maintains the reference to the original source
425
+ const targetPath = buildPath(appname, targetHolon, lensName, dataId);
426
+ const copiedHologram = {
427
+ ...data,
428
+ _meta: {
429
+ ...data._meta,
430
+ copiedFrom: sourceHolon,
431
+ copiedAt: Date.now()
432
+ }
433
+ };
434
+ const success = await write(client, targetPath, copiedHologram);
435
+
436
+ // Add this new location to the original source's activeHolograms
437
+ if (success && data.target) {
438
+ await addActiveHologram(
439
+ client,
440
+ data.target.appname || appname,
441
+ data.target.holonId,
442
+ data.target.lensName || lensName,
443
+ data.target.dataId || dataId,
444
+ targetHolon
445
+ );
446
+ console.info(`๐Ÿ“‹ Copied hologram to ${targetHolon}:`);
447
+ console.info(` Data ID: ${dataId}`);
448
+ console.info(` Original source: ${data.target.holonId}`);
449
+ console.info(` Copied from: ${sourceHolon}`);
450
+ }
451
+
452
+ return success;
453
+ }
454
+
455
+ // For non-hologram data, check if targetHolon is already in activeHolograms
456
+ if (mode === 'reference' && data._meta?.activeHolograms) {
457
+ const alreadyHasHologram = data._meta.activeHolograms.some(
458
+ h => h.targetHolon === targetHolon
459
+ );
460
+ if (alreadyHasHologram) {
461
+ console.info(`โญ๏ธ Skipping propagation - target already in activeHolograms:`);
462
+ console.info(` Data ID: ${dataId}`);
463
+ console.info(` Source holon: ${sourceHolon}`);
464
+ console.info(` Target holon: ${targetHolon}`);
465
+ return true;
466
+ }
467
+ }
468
+
469
+ if (mode === 'reference') {
470
+ // Deep circular reference check - follow the chain from source to see if it would loop back
471
+ const circularCheck = await wouldCreateCircularHologram(
472
+ client, appname, sourceHolon, targetHolon, lensName, dataId
473
+ );
474
+
475
+ if (circularCheck.isCircular) {
476
+ const chainStr = circularCheck.chain.map(c => c.holon).join(' โ†’ ');
477
+ console.warn(`๐Ÿ”„ Preventing circular hologram creation:`);
478
+ console.warn(` Data ID: ${dataId}`);
479
+ console.warn(` Would create: ${sourceHolon} โ†’ ${targetHolon}`);
480
+ console.warn(` Existing chain: ${chainStr}`);
481
+ console.warn(` Reason: ${circularCheck.reason}`);
482
+ return false;
483
+ }
484
+
485
+ // Create hologram in target
486
+ const hologram = createHologram(sourceHolon, targetHolon, lensName, dataId, appname);
487
+ const targetPath = buildPath(appname, targetHolon, lensName, dataId);
488
+ const success = await write(client, targetPath, hologram);
489
+
490
+ // Add hologram to source data's _meta.activeHolograms for tracking
491
+ if (success) {
492
+ await addActiveHologram(client, appname, sourceHolon, lensName, dataId, targetHolon);
493
+ }
494
+
495
+ return success;
496
+ } else {
497
+ // Copy data to target
498
+ const targetPath = buildPath(appname, targetHolon, lensName, dataId);
499
+ return write(client, targetPath, { ...data, _meta: { ...data._meta, source: sourceHolon } });
500
+ }
501
+ }
502
+
503
+ /**
504
+ * Update local overrides on an existing hologram
505
+ * This allows storing holon-specific data (position, pinned status, etc.) on a hologram
506
+ * @param {Object} client - Nostr client instance
507
+ * @param {string} appname - Application namespace
508
+ * @param {string} holonId - Holon where the hologram lives
509
+ * @param {string} lensName - Lens name
510
+ * @param {string} dataId - Data ID
511
+ * @param {Object} overrides - Local override fields to set (e.g., { x: 100, y: 200, pinned: true })
512
+ * @returns {Promise<boolean>} Success indicator
513
+ */
514
+ export async function updateHologramOverrides(client, appname, holonId, lensName, dataId, overrides) {
515
+ const path = buildPath(appname, holonId, lensName, dataId);
516
+
517
+ // Read existing hologram
518
+ const existing = await read(client, path);
519
+
520
+ if (!existing) {
521
+ console.warn(`Hologram not found at ${path}`);
522
+ return false;
523
+ }
524
+
525
+ // Only update if it's actually a hologram
526
+ if (!existing.hologram) {
527
+ console.warn(`Data at ${path} is not a hologram, cannot update overrides`);
528
+ return false;
529
+ }
530
+
531
+ // Merge overrides into hologram (but don't allow overriding hologram-specific fields)
532
+ const protectedFields = ['hologram', 'soul', 'target', '_meta', 'id'];
533
+ const safeOverrides = {};
534
+
535
+ for (const [key, value] of Object.entries(overrides)) {
536
+ if (!protectedFields.includes(key)) {
537
+ safeOverrides[key] = value;
538
+ }
539
+ }
540
+
541
+ const updated = {
542
+ ...existing,
543
+ ...safeOverrides
544
+ };
545
+
546
+ return write(client, path, updated);
547
+ }
548
+
549
+ /**
550
+ * Check if data is a hologram (unresolved reference)
551
+ * @param {Object} data - Data to check
552
+ * @returns {boolean} True if data is an unresolved hologram
553
+ */
554
+ export function isHologram(data) {
555
+ return data && data.hologram === true;
556
+ }
557
+
558
+ /**
559
+ * Check if data was resolved from a hologram
560
+ * @param {Object} data - Data to check
561
+ * @returns {boolean} True if data was resolved from a hologram
562
+ */
563
+ export function isResolvedHologram(data) {
564
+ return data && data._hologram && data._hologram.isHologram === true;
565
+ }
566
+
567
+ /**
568
+ * Get hologram source information from resolved data
569
+ * @param {Object} data - Resolved data
570
+ * @returns {Object|null} Source info { soul, sourceHolon, localOverrides } or null
571
+ */
572
+ export function getHologramSource(data) {
573
+ if (!isResolvedHologram(data)) {
574
+ return null;
575
+ }
576
+ return {
577
+ soul: data._hologram.soul,
578
+ sourceHolon: data._hologram.sourceHolon,
579
+ localOverrides: data._hologram.localOverrides || []
580
+ };
581
+ }
582
+
583
+ /**
584
+ * Add a hologram reference to the source data's _meta.activeHolograms
585
+ * This tracks all holograms pointing to this specific data item
586
+ * If the source is itself a hologram, follows the chain to find the real source
587
+ * @param {Object} client - Nostr client instance
588
+ * @param {string} appname - Application namespace
589
+ * @param {string} sourceHolon - Source holon ID (where original data lives)
590
+ * @param {string} lensName - Lens name
591
+ * @param {string} dataId - Data ID
592
+ * @param {string} targetHolon - Target holon ID (where hologram lives)
593
+ * @returns {Promise<boolean>} Success indicator
594
+ */
595
+ export async function addActiveHologram(client, appname, sourceHolon, lensName, dataId, targetHolon) {
596
+ let currentAppname = appname;
597
+ let currentHolon = sourceHolon;
598
+ let currentLens = lensName;
599
+ let currentDataId = dataId;
600
+ let sourcePath = buildPath(currentAppname, currentHolon, currentLens, currentDataId);
601
+
602
+ // Read existing source data
603
+ let sourceData = await read(client, sourcePath);
604
+ if (!sourceData) {
605
+ console.warn(`Source data not found at ${sourcePath}`);
606
+ return false;
607
+ }
608
+
609
+ // If source is a hologram, follow the chain to find the real source
610
+ // This ensures activeHolograms is always tracked on the actual data, not on holograms
611
+ let depth = 0;
612
+ const maxDepth = 10;
613
+ while (sourceData.hologram === true && sourceData.target && depth < maxDepth) {
614
+ depth++;
615
+ currentAppname = sourceData.target.appname || currentAppname;
616
+ currentHolon = sourceData.target.holonId;
617
+ currentLens = sourceData.target.lensName || currentLens;
618
+ currentDataId = sourceData.target.dataId || currentDataId;
619
+ sourcePath = buildPath(currentAppname, currentHolon, currentLens, currentDataId);
620
+
621
+ sourceData = await read(client, sourcePath);
622
+ if (!sourceData) {
623
+ console.warn(`Real source data not found at ${sourcePath}`);
624
+ return false;
625
+ }
626
+ }
627
+
628
+ if (depth > 0) {
629
+ console.info(`๐Ÿ“ Followed hologram chain (depth: ${depth}) to real source: ${currentHolon}/${currentLens}/${currentDataId}`);
630
+ }
631
+
632
+ // Initialize _meta and activeHolograms if needed
633
+ if (!sourceData._meta) {
634
+ sourceData._meta = {};
635
+ }
636
+ if (!Array.isArray(sourceData._meta.activeHolograms)) {
637
+ sourceData._meta.activeHolograms = [];
638
+ }
639
+
640
+ const now = Date.now();
641
+
642
+ // Check if this hologram already exists
643
+ const existingIndex = sourceData._meta.activeHolograms.findIndex(
644
+ h => h.targetHolon === targetHolon
645
+ );
646
+
647
+ if (existingIndex >= 0) {
648
+ // Update existing entry
649
+ sourceData._meta.activeHolograms[existingIndex].lastUpdated = now;
650
+ } else {
651
+ // Add new entry
652
+ sourceData._meta.activeHolograms.push({
653
+ targetHolon,
654
+ created: now,
655
+ lastUpdated: now
656
+ });
657
+ }
658
+
659
+ // Write to the real source path (not the original one if we followed a chain)
660
+ return write(client, sourcePath, sourceData);
661
+ }
662
+
663
+ /**
664
+ * Remove a hologram reference from the source data's _meta.activeHolograms
665
+ * If the source is itself a hologram, follows the chain to find the real source
666
+ * @param {Object} client - Nostr client instance
667
+ * @param {string} appname - Application namespace
668
+ * @param {string} sourceHolon - Source holon ID
669
+ * @param {string} lensName - Lens name
670
+ * @param {string} dataId - Data ID
671
+ * @param {string} targetHolon - Target holon ID
672
+ * @returns {Promise<boolean>} Success indicator
673
+ */
674
+ export async function removeActiveHologram(client, appname, sourceHolon, lensName, dataId, targetHolon) {
675
+ let currentAppname = appname;
676
+ let currentHolon = sourceHolon;
677
+ let currentLens = lensName;
678
+ let currentDataId = dataId;
679
+ let sourcePath = buildPath(currentAppname, currentHolon, currentLens, currentDataId);
680
+
681
+ // Read existing source data
682
+ let sourceData = await read(client, sourcePath);
683
+ if (!sourceData) {
684
+ return false;
685
+ }
686
+
687
+ // If source is a hologram, follow the chain to find the real source
688
+ let depth = 0;
689
+ const maxDepth = 10;
690
+ while (sourceData.hologram === true && sourceData.target && depth < maxDepth) {
691
+ depth++;
692
+ currentAppname = sourceData.target.appname || currentAppname;
693
+ currentHolon = sourceData.target.holonId;
694
+ currentLens = sourceData.target.lensName || currentLens;
695
+ currentDataId = sourceData.target.dataId || currentDataId;
696
+ sourcePath = buildPath(currentAppname, currentHolon, currentLens, currentDataId);
697
+
698
+ sourceData = await read(client, sourcePath);
699
+ if (!sourceData) {
700
+ return false;
701
+ }
702
+ }
703
+
704
+ if (!sourceData._meta || !Array.isArray(sourceData._meta.activeHolograms)) {
705
+ return false;
706
+ }
707
+
708
+ const initialLength = sourceData._meta.activeHolograms.length;
709
+ sourceData._meta.activeHolograms = sourceData._meta.activeHolograms.filter(
710
+ h => h.targetHolon !== targetHolon
711
+ );
712
+
713
+ // Only write if something changed
714
+ if (sourceData._meta.activeHolograms.length < initialLength) {
715
+ return write(client, sourcePath, sourceData);
716
+ }
717
+
718
+ return false;
719
+ }
720
+
721
+ /**
722
+ * Refresh lastUpdated timestamps on all activeHolograms in source data
723
+ * Also updates the hologram objects in target holons
724
+ * Called when source data is updated
725
+ * @param {Object} client - Nostr client instance
726
+ * @param {string} appname - Application namespace
727
+ * @param {string} sourceHolon - Source holon ID
728
+ * @param {string} lensName - Lens name
729
+ * @param {string} dataId - Data ID that was updated
730
+ * @param {Object} sourceData - The source data object (already read, to avoid re-reading)
731
+ * @returns {Promise<Object>} Result with refreshed count
732
+ */
733
+ export async function refreshActiveHolograms(client, appname, sourceHolon, lensName, dataId, sourceData = null) {
734
+ // Read source data if not provided
735
+ if (!sourceData) {
736
+ const sourcePath = buildPath(appname, sourceHolon, lensName, dataId);
737
+ sourceData = await read(client, sourcePath);
738
+ }
739
+
740
+ if (!sourceData || !sourceData._meta || !Array.isArray(sourceData._meta.activeHolograms)) {
741
+ return { refreshed: 0, holograms: [] };
742
+ }
743
+
744
+ const now = Date.now();
745
+ const refreshed = [];
746
+
747
+ // Update timestamps on all activeHolograms
748
+ for (const hologramEntry of sourceData._meta.activeHolograms) {
749
+ hologramEntry.lastUpdated = now;
750
+
751
+ // Also update the hologram itself in the target holon
752
+ const hologramPath = buildPath(appname, hologramEntry.targetHolon, lensName, dataId);
753
+ const hologram = await read(client, hologramPath);
754
+
755
+ if (hologram && hologram.hologram === true) {
756
+ // Update hologram's _meta.lastUpdated
757
+ if (!hologram._meta) hologram._meta = {};
758
+ hologram._meta.lastUpdated = now;
759
+ await write(client, hologramPath, hologram);
760
+ refreshed.push({
761
+ targetHolon: hologramEntry.targetHolon,
762
+ dataId,
763
+ timestamp: now
764
+ });
765
+ }
766
+ }
767
+
768
+ // Source data's activeHolograms timestamps are updated in-place
769
+ // The caller should save sourceData if needed
770
+
771
+ return { refreshed: refreshed.length, holograms: refreshed, sourceData };
772
+ }
773
+
774
+ /**
775
+ * Delete a hologram and clean up the activeHolograms list on the original source
776
+ * @param {Object} client - Nostr client instance
777
+ * @param {string} appname - Application namespace
778
+ * @param {string} holonId - Holon where the hologram lives (to be deleted from)
779
+ * @param {string} lensName - Lens name
780
+ * @param {string} dataId - Data ID of the hologram
781
+ * @param {Object} options - Optional settings
782
+ * @param {boolean} options.deleteData - If true, actually delete the data (default: true)
783
+ * @returns {Promise<Object>} Result with deletion info
784
+ */
785
+ export async function deleteHologram(client, appname, holonId, lensName, dataId, options = {}) {
786
+ const { deleteData = true } = options;
787
+ const path = buildPath(appname, holonId, lensName, dataId);
788
+ const result = {
789
+ success: false,
790
+ wasHologram: false,
791
+ sourceHolon: null,
792
+ activeHologramsUpdated: false,
793
+ error: null
794
+ };
795
+
796
+ try {
797
+ // Read the hologram to get source information
798
+ const hologram = await read(client, path);
799
+
800
+ if (!hologram) {
801
+ console.warn(`๐Ÿ—‘๏ธ Hologram not found at ${path}`);
802
+ result.error = 'Hologram not found';
803
+ return result;
804
+ }
805
+
806
+ // Check if it's actually a hologram
807
+ if (hologram.hologram === true && hologram.target) {
808
+ result.wasHologram = true;
809
+ result.sourceHolon = hologram.target.holonId;
810
+
811
+ console.info(`๐Ÿ—‘๏ธ Deleting hologram:`);
812
+ console.info(` Path: ${path}`);
813
+ console.info(` Data ID: ${dataId}`);
814
+ console.info(` From holon: ${holonId}`);
815
+ console.info(` Source holon: ${hologram.target.holonId}`);
816
+ console.info(` Lens: ${lensName}`);
817
+
818
+ // Remove from source's activeHolograms list
819
+ const removed = await removeActiveHologram(
820
+ client,
821
+ hologram.target.appname || appname,
822
+ hologram.target.holonId,
823
+ hologram.target.lensName || lensName,
824
+ hologram.target.dataId || dataId,
825
+ holonId
826
+ );
827
+
828
+ result.activeHologramsUpdated = removed;
829
+
830
+ if (removed) {
831
+ console.info(` โœ“ Removed from activeHolograms on source`);
832
+ } else {
833
+ console.warn(` โš ๏ธ Could not update activeHolograms on source (may already be removed)`);
834
+ }
835
+ } else {
836
+ console.info(`๐Ÿ—‘๏ธ Deleting non-hologram data at ${path}`);
837
+ }
838
+
839
+ // Delete the hologram data if requested
840
+ if (deleteData) {
841
+ // Write null to delete (or use a delete function if available)
842
+ const deleted = await write(client, path, null);
843
+ result.success = deleted;
844
+
845
+ if (deleted) {
846
+ console.info(` โœ“ Hologram deleted successfully`);
847
+ } else {
848
+ console.warn(` โš ๏ธ Failed to delete hologram data`);
849
+ result.error = 'Failed to delete hologram data';
850
+ }
851
+ } else {
852
+ result.success = true;
853
+ }
854
+
855
+ return result;
856
+ } catch (error) {
857
+ console.error(`โŒ Error deleting hologram at ${path}:`, error);
858
+ result.error = error.message || 'Unknown error';
859
+ return result;
860
+ }
861
+ }
862
+
863
+ /**
864
+ * Cleanup circular holograms in a holon's lens
865
+ * This detects and removes holograms that create circular references (Aโ†’Bโ†’A)
866
+ * Useful for cleaning up existing bad data created before circular reference prevention was added
867
+ * @param {Object} client - Nostr client instance
868
+ * @param {string} appname - Application namespace
869
+ * @param {string} holonId - Holon to clean up
870
+ * @param {string} lensName - Lens name to check
871
+ * @param {Object} options - Optional settings
872
+ * @param {boolean} options.dryRun - If true, only report what would be deleted (default: false)
873
+ * @returns {Promise<Object>} Result with cleanup info
874
+ */
875
+ export async function cleanupCircularHolograms(client, appname, holonId, lensName, options = {}) {
876
+ const { dryRun = false } = options;
877
+ const result = {
878
+ scanned: 0,
879
+ circularFound: 0,
880
+ deleted: 0,
881
+ errors: [],
882
+ details: []
883
+ };
884
+
885
+ console.info(`๐Ÿงน ${dryRun ? '[DRY RUN] ' : ''}Cleaning up circular holograms in ${holonId}/${lensName}...`);
886
+
887
+ try {
888
+ // Get all data in this lens
889
+ const basePath = buildPath(appname, holonId, lensName);
890
+ // We need to iterate through all items - this requires listing capability
891
+ // For now, we'll rely on the caller to provide a list of IDs to check
892
+ // or we can use a different approach
893
+
894
+ // Alternative: Read the lens index if it exists
895
+ const lensIndexPath = buildPath(appname, holonId, lensName, '_index');
896
+ const lensIndex = await read(client, lensIndexPath);
897
+
898
+ if (!lensIndex || !Array.isArray(lensIndex.items)) {
899
+ console.warn(` No index found for ${holonId}/${lensName}, cannot scan for circular references`);
900
+ console.info(` Tip: Provide specific IDs to check using cleanupCircularHologramsByIds()`);
901
+ return result;
902
+ }
903
+
904
+ for (const itemId of lensIndex.items) {
905
+ result.scanned++;
906
+ const itemPath = buildPath(appname, holonId, lensName, itemId);
907
+ const item = await read(client, itemPath);
908
+
909
+ if (!item || item.hologram !== true || !item.target) {
910
+ continue; // Not a hologram, skip
911
+ }
912
+
913
+ // This is a hologram - check if it creates a circular reference
914
+ const sourceHolon = item.target.holonId;
915
+ const sourceDataId = item.target.dataId || itemId;
916
+ const sourceLens = item.target.lensName || lensName;
917
+
918
+ // Read the source data
919
+ const sourcePath = buildPath(item.target.appname || appname, sourceHolon, sourceLens, sourceDataId);
920
+ const sourceData = await read(client, sourcePath);
921
+
922
+ if (sourceData && sourceData.hologram === true && sourceData.target) {
923
+ // Source is also a hologram - check if it points back
924
+ if (sourceData.target.holonId === holonId) {
925
+ result.circularFound++;
926
+ const detail = {
927
+ itemId,
928
+ path: itemPath,
929
+ pointsTo: `${sourceHolon}/${sourceLens}/${sourceDataId}`,
930
+ circularWith: `${sourceData.target.holonId}/${sourceData.target.lensName}/${sourceData.target.dataId}`
931
+ };
932
+ result.details.push(detail);
933
+
934
+ console.warn(` ๐Ÿ”„ Found circular hologram:`);
935
+ console.warn(` ${holonId}/${lensName}/${itemId} โ†’ ${sourceHolon}/${sourceLens}/${sourceDataId} โ†’ ${holonId}`);
936
+
937
+ if (!dryRun) {
938
+ // Delete this hologram
939
+ const deleteResult = await deleteHologram(client, appname, holonId, lensName, itemId);
940
+ if (deleteResult.success) {
941
+ result.deleted++;
942
+ console.info(` โœ“ Deleted circular hologram`);
943
+ } else {
944
+ result.errors.push({ itemId, error: deleteResult.error });
945
+ console.error(` โŒ Failed to delete: ${deleteResult.error}`);
946
+ }
947
+ }
948
+ }
949
+ }
950
+ }
951
+
952
+ console.info(`๐Ÿงน Cleanup complete for ${holonId}/${lensName}:`);
953
+ console.info(` Scanned: ${result.scanned}`);
954
+ console.info(` Circular found: ${result.circularFound}`);
955
+ console.info(` Deleted: ${result.deleted}`);
956
+ if (result.errors.length > 0) {
957
+ console.warn(` Errors: ${result.errors.length}`);
958
+ }
959
+
960
+ return result;
961
+ } catch (error) {
962
+ console.error(`โŒ Error during cleanup:`, error);
963
+ result.errors.push({ error: error.message });
964
+ return result;
965
+ }
966
+ }
967
+
968
+ /**
969
+ * Cleanup circular holograms for a specific list of IDs
970
+ * Use this when you don't have an index but know which IDs to check
971
+ * @param {Object} client - Nostr client instance
972
+ * @param {string} appname - Application namespace
973
+ * @param {string} holonId - Holon to clean up
974
+ * @param {string} lensName - Lens name
975
+ * @param {string[]} dataIds - Array of data IDs to check
976
+ * @param {Object} options - Optional settings
977
+ * @param {boolean} options.dryRun - If true, only report what would be deleted
978
+ * @returns {Promise<Object>} Result with cleanup info
979
+ */
980
+ export async function cleanupCircularHologramsByIds(client, appname, holonId, lensName, dataIds, options = {}) {
981
+ const { dryRun = false } = options;
982
+ const result = {
983
+ scanned: 0,
984
+ circularFound: 0,
985
+ deleted: 0,
986
+ errors: [],
987
+ details: []
988
+ };
989
+
990
+ console.info(`๐Ÿงน ${dryRun ? '[DRY RUN] ' : ''}Cleaning up circular holograms for ${dataIds.length} IDs in ${holonId}/${lensName}...`);
991
+
992
+ for (const itemId of dataIds) {
993
+ result.scanned++;
994
+ const itemPath = buildPath(appname, holonId, lensName, itemId);
995
+ const item = await read(client, itemPath);
996
+
997
+ if (!item || item.hologram !== true || !item.target) {
998
+ continue; // Not a hologram, skip
999
+ }
1000
+
1001
+ // This is a hologram - check if it creates a circular reference
1002
+ const sourceHolon = item.target.holonId;
1003
+ const sourceDataId = item.target.dataId || itemId;
1004
+ const sourceLens = item.target.lensName || lensName;
1005
+
1006
+ // Read the source data
1007
+ const sourcePath = buildPath(item.target.appname || appname, sourceHolon, sourceLens, sourceDataId);
1008
+ const sourceData = await read(client, sourcePath);
1009
+
1010
+ if (sourceData && sourceData.hologram === true && sourceData.target) {
1011
+ // Source is also a hologram - check if it points back
1012
+ if (sourceData.target.holonId === holonId) {
1013
+ result.circularFound++;
1014
+ const detail = {
1015
+ itemId,
1016
+ path: itemPath,
1017
+ pointsTo: `${sourceHolon}/${sourceLens}/${sourceDataId}`,
1018
+ circularWith: `${sourceData.target.holonId}/${sourceData.target.lensName}/${sourceData.target.dataId}`
1019
+ };
1020
+ result.details.push(detail);
1021
+
1022
+ console.warn(` ๐Ÿ”„ Found circular hologram: ${itemId}`);
1023
+ console.warn(` Chain: ${holonId} โ†’ ${sourceHolon} โ†’ ${holonId}`);
1024
+
1025
+ if (!dryRun) {
1026
+ // Delete this hologram
1027
+ const deleteResult = await deleteHologram(client, appname, holonId, lensName, itemId);
1028
+ if (deleteResult.success) {
1029
+ result.deleted++;
1030
+ console.info(` โœ“ Deleted`);
1031
+ } else {
1032
+ result.errors.push({ itemId, error: deleteResult.error });
1033
+ console.error(` โŒ Failed: ${deleteResult.error}`);
1034
+ }
1035
+ }
1036
+ }
1037
+ }
1038
+ }
1039
+
1040
+ console.info(`๐Ÿงน Cleanup complete: scanned=${result.scanned}, circular=${result.circularFound}, deleted=${result.deleted}`);
1041
+ return result;
1042
+ }