Skip to main content

horon_engine/
tree_tensor.rs

1//! tree_tensor.rs - Hyperbolic Tree Tensor Core Implementation
2//!
3//! Efficient hierarchical data representation: path lookups are hash-map
4//! access, spatial queries go through the cell index (see per-method docs on
5//! `Store` for costs).
6
7use std::collections::HashMap;
8use std::fmt::{self, Debug, Formatter};
9use std::ops::Range;
10use std::sync::Arc;
11use dashmap::DashMap;
12use super::concurrency::StripedLock;
13use super::hash_table::GeometricSignature;
14use super::tensor_network::{HyperbolicTensorNetwork, CompressedNode, NodeMetadata};
15
16/// Error type for tree tensor integration operations.
17#[derive(Debug)]
18pub enum IntegrationError {
19    /// A node already exists at the target path.
20    AlreadyExists(String),
21    /// No node exists at the target path.
22    NotFound(String),
23    /// The operation could not be completed.
24    OperationFailed(String),
25    /// Input failed validation (dimensions, structure, or constraints).
26    ValidationFailed(String),
27    /// Stored bytes could not be deserialized.
28    DeserializationError(String),
29    /// A lock could not be acquired.
30    LockError(String),
31    /// The supplied configuration is invalid.
32    ConfigurationError(String),
33}
34
35impl IntegrationError {
36    /// Create a configuration error.
37    pub fn configuration_error(msg: String) -> Self {
38        Self::ConfigurationError(msg)
39    }
40}
41
42impl std::fmt::Display for IntegrationError {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        match self {
45            Self::AlreadyExists(s) => write!(f, "already exists: {s}"),
46            Self::NotFound(s) => write!(f, "not found: {s}"),
47            Self::OperationFailed(s) => write!(f, "operation failed: {s}"),
48            Self::ValidationFailed(s) => write!(f, "validation failed: {s}"),
49            Self::DeserializationError(s) => write!(f, "deserialization error: {s}"),
50            Self::LockError(s) => write!(f, "lock error: {s}"),
51            Self::ConfigurationError(s) => write!(f, "configuration error: {s}"),
52        }
53    }
54}
55
56impl std::error::Error for IntegrationError {}
57
58/// Result type alias for tree-tensor integration operations.
59pub type IntegrationResult<T> = Result<T, IntegrationError>;
60
61/// Path operations for hierarchical data.
62pub trait PathOperations {
63    /// Split a path into components.
64    fn split_path(&self, path: &str) -> Vec<String>;
65
66    /// Join path components into a full path.
67    fn join_path(&self, components: &[String]) -> String;
68
69    /// Get parent path from a path.
70    fn parent_path(&self, path: &str) -> Option<String>;
71
72    /// Get the last component of a path.
73    fn last_component(&self, path: &str) -> Option<String>;
74}
75
76/// Default path operations implementation using '/' separator.
77pub struct DefaultPathOps;
78
79impl PathOperations for DefaultPathOps {
80    fn split_path(&self, path: &str) -> Vec<String> {
81        path.split('/')
82            .filter(|s| !s.is_empty())
83            .map(String::from)
84            .collect()
85    }
86
87    fn join_path(&self, components: &[String]) -> String {
88        let mut path = String::new();
89        for component in components {
90            path.push('/');
91            path.push_str(component);
92        }
93        if path.is_empty() {
94            path.push('/');
95        }
96        path
97    }
98
99    fn parent_path(&self, path: &str) -> Option<String> {
100        if path == "/" {
101            return None;
102        }
103
104        let components = self.split_path(path);
105        if components.is_empty() {
106            return Some("/".to_string());
107        }
108
109        let parent_components = &components[0..components.len() - 1];
110        Some(self.join_path(parent_components))
111    }
112
113    fn last_component(&self, path: &str) -> Option<String> {
114        let components = self.split_path(path);
115        components.last().cloned()
116    }
117}
118
119/// Configuration for the Hyperbolic Tree Tensor.
120#[derive(Clone, Debug)]
121pub struct HTTConfig {
122    /// Dimension of the hyperbolic space
123    dimension: usize,
124    /// Maximum in-memory nodes before flushing to storage
125    max_memory_nodes: usize,
126    /// Cache size for frequently accessed nodes
127    cache_size: usize,
128    /// Sarkar embedding scale factor τ: parent-child hyperbolic distance
129    tau: g_math::fixed_point::FixedPoint,
130}
131
132impl HTTConfig {
133    /// Create a new HTT configuration (uses default τ = 1.0).
134    pub fn new(dimension: usize, max_memory_nodes: usize, cache_size: usize) -> Self {
135        Self {
136            dimension,
137            max_memory_nodes,
138            cache_size,
139            tau: crate::constants::default_tau(),
140        }
141    }
142
143    /// Create a default configuration.
144    pub fn default_config() -> Self {
145        Self {
146            dimension: 4,
147            max_memory_nodes: 1000,
148            cache_size: 100,
149            tau: crate::constants::default_tau(),
150        }
151    }
152
153    /// Set the Sarkar embedding scale factor τ.
154    pub fn with_tau(mut self, tau: g_math::fixed_point::FixedPoint) -> Self {
155        self.tau = tau;
156        self
157    }
158
159    /// Get the dimension.
160    pub fn dimension(&self) -> usize {
161        self.dimension
162    }
163
164    /// Get the maximum memory nodes.
165    pub fn max_memory_nodes(&self) -> usize {
166        self.max_memory_nodes
167    }
168
169    /// Get the cache size.
170    pub fn cache_size(&self) -> usize {
171        self.cache_size
172    }
173
174    /// Get the Sarkar embedding scale factor τ.
175    pub fn tau(&self) -> g_math::fixed_point::FixedPoint {
176        self.tau
177    }
178}
179
180impl Default for HTTConfig {
181    fn default() -> Self {
182        Self::default_config()
183    }
184}
185
186/// Hyperbolic Tree Tensor - core data structure.
187///
188/// Path access is hash-map cost; spatial queries go through the cell index.
189/// Nodes are embedded in the Poincare disk, and their positions are derived
190/// from tree shape rather than stored.
191///
192/// Path maps use DashMap for lock-free concurrent reads.
193pub struct HyperbolicTreeTensor {
194    /// Tensor network for spatial embedding
195    tensor_network: HyperbolicTensorNetwork,
196    /// Path operations
197    path_ops: Box<dyn PathOperations + Send + Sync>,
198    /// Signature map for O(1) path lookups: path -> signature
199    path_map: DashMap<String, GeometricSignature>,
200    /// Reverse map: unique_id -> path (for spatial query results)
201    id_to_path: DashMap<String, String>,
202    /// Striped parent locks: serialize writes to the same parent,
203    /// allow parallel writes to different parents (64 stripes)
204    parent_locks: StripedLock<64>,
205    /// Configuration
206    config: HTTConfig,
207}
208
209impl HyperbolicTreeTensor {
210    /// Create a new Hyperbolic Tree Tensor.
211    pub fn new(config: HTTConfig) -> Self {
212        let tensor_network = HyperbolicTensorNetwork::new(config.dimension(), config.tau());
213
214        Self {
215            tensor_network,
216            path_ops: Box::new(DefaultPathOps),
217            path_map: DashMap::new(),
218            id_to_path: DashMap::new(),
219            parent_locks: StripedLock::new(),
220            config,
221        }
222    }
223
224    /// Resolve a node's parent signature, enforcing the tree invariant.
225    ///
226    /// Returns `Ok(None)` for a root insert (parent path is `None`, i.e. the
227    /// path is `/`), `Ok(Some(sig))` when the parent exists, and
228    /// `Err(NotFound)` when a non-root node's parent is absent. The last case
229    /// is the important one: `add_node` places a `None`-parent node at the
230    /// disk origin as a root, so silently passing `None` for a missing parent
231    /// would stack unrelated subtrees on top of each other at the centre.
232    fn resolve_parent(
233        &self,
234        path: &str,
235        parent_path: &Option<String>,
236    ) -> IntegrationResult<Option<GeometricSignature>> {
237        match parent_path {
238            Some(p) if p.as_str() != path => match self.path_map.get(p.as_str()) {
239                Some(r) => Ok(Some(r.value().clone())),
240                None => Err(IntegrationError::NotFound(format!(
241                    "cannot insert '{}': parent '{}' does not exist",
242                    path, p
243                ))),
244            },
245            _ => Ok(None), // root, or a degenerate self-parent
246        }
247    }
248
249    /// Insert a node without geometric embedding (data + semantic only).
250    ///
251    /// Much faster than `insert()` — skips Sarkar embedding, VP-tree, power
252    /// diagram, and point location grid. Use for bulk loading when spatial
253    /// queries are not needed (semantic queries still work).
254    pub fn insert_data_only(&self, path: &str, value: Vec<u8>, content_type: Option<String>) -> IntegrationResult<()> {
255        // Fast path: lock-free duplicate check.
256        if self.path_map.contains_key(path) {
257            return Err(IntegrationError::AlreadyExists(
258                format!("Node at path {} already exists", path),
259            ));
260        }
261
262        // Enforce the tree invariant: a non-root node's parent must exist.
263        // (HTTStorage creates ancestors first; a direct caller inserting an
264        // orphan would otherwise leave a dangling path.)
265        let parent_path = self.path_ops.parent_path(path);
266        if let Some(p) = &parent_path {
267            if p.as_str() != path && !self.path_map.contains_key(p.as_str()) {
268                return Err(IntegrationError::NotFound(format!(
269                    "cannot insert '{}': parent '{}' does not exist",
270                    path, p
271                )));
272            }
273        }
274
275        // Serialize sibling creation under the same parent (data-only nodes
276        // don't touch the geometric child list, but the stripe lock closes
277        // the TOCTOU between the duplicate check and the insert). Root inserts
278        // ("/") have no parent → no lock needed.
279        let _stripe_guard = parent_path.as_deref().map(|p| self.parent_locks.lock(p));
280
281        // Re-check under the lock.
282        if self.path_map.contains_key(path) {
283            return Err(IntegrationError::AlreadyExists(
284                format!("Node at path {} already exists", path),
285            ));
286        }
287
288        let metadata = NodeMetadata::new(path.to_string(), content_type);
289
290        let unique_id = self.tensor_network.add_node_data_only(metadata, value, 0);
291
292        // Create a stub signature for path_map (no geometric meaning)
293        let stub_sig = GeometricSignature::stub(&unique_id);
294        self.id_to_path.insert(unique_id, path.to_string());
295        self.path_map.insert(path.to_string(), stub_sig);
296
297        Ok(())
298    }
299
300    /// Insert a node at the specified path.
301    ///
302    /// Acquires a striped parent lock to serialize writes to the same parent
303    /// while allowing parallel writes to different parents.
304    pub fn insert(&self, path: &str, value: Vec<u8>, content_type: Option<String>) -> IntegrationResult<()> {
305        // Fast path: lock-free DashMap read
306        if self.path_map.contains_key(path) {
307            return Err(IntegrationError::AlreadyExists(
308                format!("Node at path {} already exists", path),
309            ));
310        }
311
312        let parent_path = self.path_ops.parent_path(path);
313        let level = self.path_depth(path);
314        let metadata = NodeMetadata::new(path.to_string(), content_type);
315
316        // Look up the parent signature (lock-free DashMap read). A non-root
317        // node whose parent is absent is rejected — silently treating it as a
318        // second root would place it at the origin, on top of the real root.
319        let parent_signature = self.resolve_parent(path, &parent_path)?;
320
321        // Acquire stripe lock on parent to serialize sibling creation.
322        // This ensures child_counts consistency and prevents duplicate path creation
323        // (TOCTOU between contains_key above and path_map.insert below).
324        // Different parents hit different stripes → parallel writes to independent subtrees.
325        let parent_uid = parent_signature.as_ref().map(|s| s.unique_id());
326        let _stripe_guard = parent_uid.as_deref().map(|uid| self.parent_locks.lock(uid));
327
328        // Re-check under the stripe lock (double-checked locking)
329        if self.path_map.contains_key(path) {
330            return Err(IntegrationError::AlreadyExists(
331                format!("Node at path {} already exists", path),
332            ));
333        }
334
335        let signature = self
336            .tensor_network
337            .add_node(metadata, value, parent_signature.as_ref(), level)
338            .ok_or_else(|| {
339                IntegrationError::OperationFailed(
340                    "Failed to add node to tensor network".to_string(),
341                )
342            })?;
343
344        let unique_id = signature.unique_id();
345        self.id_to_path.insert(unique_id, path.to_string());
346        self.path_map.insert(path.to_string(), signature);
347
348        // _stripe_guard dropped here, releasing the parent stripe lock
349        Ok(())
350    }
351
352    /// Insert a node with an explicit child_index for deterministic Sarkar reconstruction.
353    ///
354    /// Same as `insert()` but passes the child_index through to the tensor network
355    /// so the node gets the same geometric position regardless of insertion order.
356    pub fn insert_positioned(&self, path: &str, value: Vec<u8>, content_type: Option<String>, child_index: u32) -> IntegrationResult<()> {
357        if self.path_map.contains_key(path) {
358            return Err(IntegrationError::AlreadyExists(
359                format!("Node at path {} already exists", path),
360            ));
361        }
362
363        let parent_path = self.path_ops.parent_path(path);
364        let level = self.path_depth(path);
365        let metadata = NodeMetadata::new(path.to_string(), content_type);
366
367        let parent_signature = self.resolve_parent(path, &parent_path)?;
368
369        let parent_uid = parent_signature.as_ref().map(|s| s.unique_id());
370        let _stripe_guard = parent_uid.as_deref().map(|uid| self.parent_locks.lock(uid));
371
372        if self.path_map.contains_key(path) {
373            return Err(IntegrationError::AlreadyExists(
374                format!("Node at path {} already exists", path),
375            ));
376        }
377
378        let signature = self
379            .tensor_network
380            .add_node_positioned(metadata, value, parent_signature.as_ref(), level, child_index)
381            .ok_or_else(|| {
382                IntegrationError::OperationFailed(
383                    "Failed to add node to tensor network".to_string(),
384                )
385            })?;
386
387        let unique_id = signature.unique_id();
388        self.id_to_path.insert(unique_id, path.to_string());
389        self.path_map.insert(path.to_string(), signature);
390
391        Ok(())
392    }
393
394    /// Upgrade a data-only node to a full geometric embedding, in place.
395    ///
396    /// Data-only nodes (`insert_data_only`) live under a stub signature with
397    /// no Poincaré position — semantic queries see them, spatial queries do
398    /// not. This computes their Sarkar placement on demand: missing ancestors
399    /// are embedded first (placement needs an embedded parent; recursion is
400    /// bounded by the ~44/τ depth budget), then the node is re-registered
401    /// under its position-derived signature with its identity preserved —
402    /// key, value, user metadata, timestamps, and semantic coordinates all
403    /// carry over.
404    ///
405    /// Returns `Ok(true)` when this call performed the upgrade, `Ok(false)`
406    /// when the node was already embedded (idempotent), `NotFound` for
407    /// missing paths.
408    ///
409    /// Concurrency: takes the same parent stripe lock as `insert`, so embeds
410    /// serialize with sibling inserts and deletes; racing embeds of the same
411    /// node resolve to one upgrade (double-checked under the lock). Readers
412    /// never observe the node missing: the embedded replacement (with
413    /// coordinates already copied) goes live in `path_map` before the
414    /// data-only entry is retired — a concurrent semantic query may
415    /// transiently see the key twice (same key, same coordinates) inside
416    /// that window.
417    ///
418    /// Geometric positions are derived state and are NOT persisted: the
419    /// position depends on the sibling order at embed time, so it is
420    /// deterministic for a fixed operation sequence but not stable across
421    /// sessions. Callers re-embed after reopening a lazily-loaded store.
422    pub fn embed_existing(&self, path: &str) -> IntegrationResult<bool> {
423        // Lock-free fast path.
424        let sig = match self.path_map.get(path) {
425            Some(r) => r.value().clone(),
426            None => {
427                return Err(IntegrationError::NotFound(format!(
428                    "Node at path {} not found",
429                    path
430                )))
431            }
432        };
433        if !sig.is_stub() {
434            return Ok(false);
435        }
436
437        // Ancestors first. The recursive call acquires and releases its own
438        // parent stripe before this frame takes any lock — no nested guards,
439        // no ordering hazard.
440        let parent_path = self.path_ops.parent_path(path);
441        if let Some(p) = &parent_path {
442            if p.as_str() != path {
443                self.embed_existing(p)?;
444            }
445        }
446
447        let parent_signature = self.resolve_parent(path, &parent_path)?;
448        if let Some(ps) = &parent_signature {
449            if ps.is_stub() {
450                // A concurrent delete+reinsert put a fresh data-only parent
451                // back between our recursion and this lookup. Surface it
452                // rather than embedding against a positionless parent.
453                return Err(IntegrationError::OperationFailed(format!(
454                    "cannot embed '{}': parent lost its embedding concurrently",
455                    path
456                )));
457            }
458        }
459        let parent_uid = parent_signature.as_ref().map(|s| s.unique_id());
460        let _stripe_guard = parent_uid.as_deref().map(|uid| self.parent_locks.lock(uid));
461
462        // Re-check under the lock: a racing embed may have won, or a racing
463        // delete may have removed the node.
464        let old_sig = match self.path_map.get(path) {
465            Some(r) => r.value().clone(),
466            None => {
467                return Err(IntegrationError::NotFound(format!(
468                    "Node at path {} not found",
469                    path
470                )))
471            }
472        };
473        if !old_sig.is_stub() {
474            return Ok(false);
475        }
476        let old_uid = old_sig.unique_id();
477
478        // Preserve the node's identity.
479        let node = self
480            .tensor_network
481            .get_node_by_signature(&old_sig)
482            .ok_or_else(|| {
483                IntegrationError::OperationFailed(format!(
484                    "data-only node '{}' missing from the node map",
485                    path
486                ))
487            })?;
488        let metadata = node.metadata().clone();
489        let value = node.value().to_vec();
490        let coords = node.semantic_coords().to_vec();
491
492        let level = self.path_depth(path);
493        let signature = self
494            .tensor_network
495            .add_node(metadata, value, parent_signature.as_ref(), level)
496            .ok_or_else(|| {
497                IntegrationError::OperationFailed(format!(
498                    "failed to embed node '{}'",
499                    path
500                ))
501            })?;
502        let new_uid = signature.unique_id();
503
504        // Coordinates before the visibility swap: a concurrent semantic
505        // query never misses the node.
506        if !coords.is_empty() {
507            self.tensor_network.set_node_semantic(&new_uid, coords);
508        }
509
510        // Swap visibility to the embedded node, then retire the stub entry.
511        self.id_to_path.insert(new_uid, path.to_string());
512        self.path_map.insert(path.to_string(), signature);
513        self.id_to_path.remove(&old_uid);
514        self.tensor_network.remove_detached_node(&old_uid);
515
516        Ok(true)
517    }
518
519    /// Get a node by path (returns cloned value).
520    pub fn get(&self, path: &str) -> IntegrationResult<CompressedNode> {
521        let signature = self
522            .path_map
523            .get(path)
524            .ok_or_else(|| IntegrationError::NotFound(format!("Node at path {} not found", path)))?;
525
526        self.tensor_network
527            .get_node_by_signature(signature.value())
528            .ok_or_else(|| {
529                IntegrationError::NotFound(format!(
530                    "Node with signature {} not found",
531                    signature.value().hash()
532                ))
533            })
534    }
535
536    /// Update the value of a node at the specified path.
537    pub fn update_value(&self, path: &str, value: Vec<u8>) -> IntegrationResult<()> {
538        // The path_map guard is held across the network read on purpose.
539        // `embed_existing` swaps a stub for an embedded node by inserting the
540        // new signature into `path_map` and THEN retiring the old node. A
541        // reader that resolves the uid and drops the guard first can be
542        // descheduled in that window and then look up a uid the network has
543        // already released — a spurious NotFound for a key that never went
544        // away. Holding the shard guard makes the swap's `path_map.insert`
545        // wait. This is what `get()` has always done; these four did not.
546        // No inversion: the writer releases every node lock inside `add_node`
547        // before it touches `path_map`, so the orders never interleave.
548        let guard = self
549            .path_map
550            .get(path)
551            .ok_or_else(|| IntegrationError::NotFound(format!("Node at path {} not found", path)))?;
552        let uid = guard.value().unique_id();
553        let ok = self.tensor_network.update_node_value(&uid, value);
554        drop(guard);
555
556        if ok {
557            Ok(())
558        } else {
559            Err(IntegrationError::NotFound(format!("Node with uid {} not found in network", uid)))
560        }
561    }
562
563    /// Set a metadata key-value pair on a node.
564    pub fn set_node_metadata(&self, path: &str, key: &str, value: &str) -> IntegrationResult<()> {
565        // The path_map guard is held across the network read on purpose.
566        // `embed_existing` swaps a stub for an embedded node by inserting the
567        // new signature into `path_map` and THEN retiring the old node. A
568        // reader that resolves the uid and drops the guard first can be
569        // descheduled in that window and then look up a uid the network has
570        // already released — a spurious NotFound for a key that never went
571        // away. Holding the shard guard makes the swap's `path_map.insert`
572        // wait. This is what `get()` has always done; these four did not.
573        // No inversion: the writer releases every node lock inside `add_node`
574        // before it touches `path_map`, so the orders never interleave.
575        let guard = self
576            .path_map
577            .get(path)
578            .ok_or_else(|| IntegrationError::NotFound(format!("Node at path {} not found", path)))?;
579        let uid = guard.value().unique_id();
580        let ok = self.tensor_network.set_node_metadata_entry(&uid, key, value);
581        drop(guard);
582
583        if ok {
584            Ok(())
585        } else {
586            Err(IntegrationError::NotFound(format!("Node with uid {} not found in network", uid)))
587        }
588    }
589
590    /// Set semantic coordinates on a node (raw Q64.64 bytes, 16 bytes per dimension).
591    pub fn set_semantic(&self, path: &str, coords: Vec<u8>) -> IntegrationResult<()> {
592        // The path_map guard is held across the network read on purpose.
593        // `embed_existing` swaps a stub for an embedded node by inserting the
594        // new signature into `path_map` and THEN retiring the old node. A
595        // reader that resolves the uid and drops the guard first can be
596        // descheduled in that window and then look up a uid the network has
597        // already released — a spurious NotFound for a key that never went
598        // away. Holding the shard guard makes the swap's `path_map.insert`
599        // wait. This is what `get()` has always done; these four did not.
600        // No inversion: the writer releases every node lock inside `add_node`
601        // before it touches `path_map`, so the orders never interleave.
602        let guard = self
603            .path_map
604            .get(path)
605            .ok_or_else(|| IntegrationError::NotFound(format!("Node at path {} not found", path)))?;
606        let uid = guard.value().unique_id();
607        let ok = self.tensor_network.set_node_semantic(&uid, coords);
608        drop(guard);
609
610        if ok {
611            Ok(())
612        } else {
613            Err(IntegrationError::NotFound(format!("Node with uid {} not found in network", uid)))
614        }
615    }
616
617    /// Get semantic coordinates for a node (raw Q64.64 bytes).
618    pub fn get_semantic(&self, path: &str) -> IntegrationResult<Vec<u8>> {
619        // The path_map guard is held across the network read on purpose.
620        // `embed_existing` swaps a stub for an embedded node by inserting the
621        // new signature into `path_map` and THEN retiring the old node. A
622        // reader that resolves the uid and drops the guard first can be
623        // descheduled in that window and then look up a uid the network has
624        // already released — a spurious NotFound for a key that never went
625        // away. Holding the shard guard makes the swap's `path_map.insert`
626        // wait. This is what `get()` has always done; these four did not.
627        // No inversion: the writer releases every node lock inside `add_node`
628        // before it touches `path_map`, so the orders never interleave.
629        let guard = self
630            .path_map
631            .get(path)
632            .ok_or_else(|| IntegrationError::NotFound(format!("Node at path {} not found", path)))?;
633        let uid = guard.value().unique_id();
634        let found = self.tensor_network.get_node_semantic(&uid);
635        drop(guard);
636
637        found
638            .ok_or_else(|| IntegrationError::NotFound(format!("Node with uid {} not found in network", uid)))
639    }
640
641    /// Delete a node at the specified path.
642    ///
643    /// A node with live children cannot be deleted (that would orphan them).
644    /// The check and the removal run under two stripe locks — the node's own
645    /// (which blocks a concurrent insert of a child *under* it, closing the
646    /// has-no-children TOCTOU) and its parent's (which serializes the parent's
647    /// child-list mutation against sibling inserts/deletes). The two stripes
648    /// are taken in canonical order, so this never deadlocks against a
649    /// concurrent delete.
650    pub fn delete(&self, path: &str) -> IntegrationResult<()> {
651        // Resolve the node and its parent before locking.
652        let node_uid = match self.path_map.get(path) {
653            Some(r) => r.value().unique_id(),
654            None => {
655                return Err(IntegrationError::NotFound(format!(
656                    "Node at path {} not found",
657                    path
658                )))
659            }
660        };
661        let parent_uid = self
662            .path_ops
663            .parent_path(path)
664            .and_then(|pp| self.path_map.get(&pp).map(|s| s.unique_id()));
665
666        // Hold the node's stripe (and the parent's, if any) for the whole
667        // check-then-remove. `_guards` keeps both alive to the end of scope.
668        let _guards = match &parent_uid {
669            Some(puid) => {
670                let (g1, g2) = self.parent_locks.lock_two(&node_uid, puid);
671                (Some(g1), g2)
672            }
673            None => (Some(self.parent_locks.lock(&node_uid)), None),
674        };
675
676        // Re-check under the lock: the node may have been removed, or gained a
677        // child, since the pre-lock read.
678        if !self.path_map.contains_key(path) {
679            return Err(IntegrationError::NotFound(format!(
680                "Node at path {} not found",
681                path
682            )));
683        }
684        let children = self.list_children(path)?;
685        if !children.is_empty() {
686            return Err(IntegrationError::ValidationFailed(format!(
687                "Cannot delete node at {} because it has {} children",
688                path,
689                children.len()
690            )));
691        }
692
693        // Remove from path map, reverse map, and spatial index. The parent's
694        // unique id is passed so the parent's child list drops this node even
695        // when the node has no power cell (data-only nodes).
696        if let Some((_, sig)) = self.path_map.remove(path) {
697            let unique_id = sig.unique_id();
698            self.id_to_path.remove(&unique_id);
699            self.tensor_network
700                .unregister_node_with_parent(&unique_id, parent_uid.as_deref());
701        }
702
703        Ok(())
704    }
705
706    /// List children of a node at the specified path (cloned).
707    pub fn list_children(&self, path: &str) -> IntegrationResult<Vec<CompressedNode>> {
708        let signature = self
709            .path_map
710            .get(path)
711            .ok_or_else(|| IntegrationError::NotFound(format!("Node at path {} not found", path)))?;
712
713        let children = self.tensor_network.children_of(signature.value());
714
715        // Filter out deleted nodes (removed from path_map but still in tensor network)
716        let result = children
717            .into_iter()
718            .filter(|child| self.path_map.contains_key(&child.metadata().key))
719            .collect();
720
721        Ok(result)
722    }
723
724    /// List all node paths under a specified path.
725    pub fn list_subtree(&self, path: &str) -> IntegrationResult<Vec<String>> {
726        let prefix = if path.ends_with('/') {
727            path.to_string()
728        } else {
729            format!("{}/", path)
730        };
731
732        let mut result = Vec::new();
733        for entry in self.path_map.iter() {
734            let node_path = entry.key();
735            if node_path.as_str() != path && (path == "/" || node_path.starts_with(&prefix)) {
736                result.push(node_path.clone());
737            }
738        }
739
740        Ok(result)
741    }
742
743    /// Get the path depth (number of components).
744    fn path_depth(&self, path: &str) -> u32 {
745        if path == "/" {
746            return 0;
747        }
748        self.path_ops.split_path(path).len() as u32
749    }
750
751    /// Check if a path exists.
752    pub fn exists(&self, path: &str) -> bool {
753        self.path_map.contains_key(path)
754    }
755
756    /// Get the number of nodes in the tree.
757    pub fn node_count(&self) -> usize {
758        self.path_map.len()
759    }
760
761    /// Get tree statistics.
762    pub fn stats(&self) -> HashMap<String, String> {
763        let mut stats = HashMap::new();
764        stats.insert("node_count".to_string(), self.node_count().to_string());
765        stats.insert(
766            "dimension".to_string(),
767            self.config.dimension().to_string(),
768        );
769        stats
770    }
771
772    /// Validate the tree structure.
773    ///
774    /// Checks all structural invariants: parent-child consistency,
775    /// path_map ↔ id_to_path bidirectionality, and tensor network integrity.
776    pub fn validate(&self) -> bool {
777        // Empty tree is valid
778        if self.path_map.is_empty() {
779            return true;
780        }
781
782        // Check that tensor network is valid
783        if !self.tensor_network.validate_network() {
784            return false;
785        }
786
787        // All non-root paths have valid parents
788        for entry in self.path_map.iter() {
789            let path = entry.key();
790            if path == "/" {
791                continue;
792            }
793            if let Some(parent_path) = self.path_ops.parent_path(path) {
794                if !self.path_map.contains_key(&parent_path) {
795                    return false;
796                }
797            }
798        }
799
800        // path_map ↔ id_to_path bidirectional consistency
801        for entry in self.path_map.iter() {
802            let path = entry.key();
803            let sig = entry.value();
804            let uid = sig.unique_id();
805            match self.id_to_path.get(&uid) {
806                Some(reverse_path) if reverse_path.value() == path => {},
807                _ => return false,
808            }
809        }
810        for entry in self.id_to_path.iter() {
811            let uid = entry.key();
812            let path = entry.value();
813            match self.path_map.get(path.as_str()) {
814                Some(sig) if sig.value().unique_id() == *uid => {},
815                _ => return false,
816            }
817        }
818
819        // path_map and id_to_path must have the same size
820        if self.path_map.len() != self.id_to_path.len() {
821            return false;
822        }
823
824        true
825    }
826
827    /// Get the underlying tensor network (for spatial queries).
828    pub fn tensor_network(&self) -> &HyperbolicTensorNetwork {
829        &self.tensor_network
830    }
831
832    /// Resolve a unique_id to a path.
833    pub fn path_for_id(&self, unique_id: &str) -> Option<String> {
834        self.id_to_path.get(unique_id).map(|r| r.value().clone())
835    }
836
837    /// The hyperbolic (Poincaré) position of a stored node.
838    ///
839    /// Errors with `NotFound` for unknown paths and `OperationFailed` for
840    /// data-only nodes, which have no geometric embedding.
841    pub fn position(&self, path: &str) -> IntegrationResult<super::hyperbolic_geometry::HyperbolicPoint> {
842        let sig = self
843            .path_map
844            .get(path)
845            .ok_or_else(|| IntegrationError::NotFound(format!("Node at path {} not found", path)))?;
846        let unique_id = sig.value().unique_id();
847        drop(sig);
848        self.tensor_network.get_point(&unique_id).ok_or_else(|| {
849            IntegrationError::OperationFailed(format!(
850                "node {} has no geometric embedding (data-only)",
851                path
852            ))
853        })
854    }
855
856    /// Get the path operations.
857    pub fn path_ops(&self) -> &dyn PathOperations {
858        &*self.path_ops
859    }
860
861    /// Find the nearest stored node to an arbitrary Poincaré disk point.
862    ///
863    /// Answered exactly by the cell index. Returns (path, hyperbolic_distance).
864    /// Errors when the index holds nothing.
865    pub fn nearest_neighbor_point(&self, query: &super::hyperbolic_geometry::HyperbolicPoint) -> IntegrationResult<(String, g_math::fixed_point::FixedPoint)> {
866        let (uid, dist) = self.tensor_network
867            .nearest_neighbor_point(query)
868            .ok_or_else(|| IntegrationError::OperationFailed(
869                "No nodes in tree for nearest neighbor query".to_string()
870            ))?;
871
872        let path = self.id_to_path.get(&uid)
873            .ok_or_else(|| IntegrationError::NotFound(
874                format!("No path for unique_id {}", uid)
875            ))?;
876
877        Ok((path.value().clone(), dist))
878    }
879
880    /// Find the k nearest stored nodes to an arbitrary Poincaré disk point.
881    ///
882    /// Returns `(path, hyperbolic_distance)` sorted by ascending distance.
883    ///
884    /// `k == 0` is a request for nothing and is answered with nothing — the
885    /// same as [`Self::find_nearest`]. Only an index that holds no nodes is an
886    /// error. Conflating the two used to make `nearest_k(q, 0)` report "no
887    /// nodes in tree" against a fully populated store, which is simply untrue.
888    pub fn nearest_neighbor_point_k(&self, query: &super::hyperbolic_geometry::HyperbolicPoint, k: usize) -> IntegrationResult<Vec<(String, g_math::fixed_point::FixedPoint)>> {
889        if k == 0 {
890            return Ok(Vec::new());
891        }
892        let results = self.tensor_network.nearest_neighbor_point_k(query, k);
893        if results.is_empty() {
894            return Err(IntegrationError::OperationFailed(
895                "No nodes in tree for nearest neighbor query".to_string()
896            ));
897        }
898
899        let found = results.len();
900        let mut paths = Vec::with_capacity(found);
901        for (uid, dist) in results {
902            if let Some(p) = self.id_to_path.get(&uid) {
903                paths.push((p.value().clone(), dist));
904            }
905        }
906        Self::note_unmapped(found, paths.len(), "nearest_neighbor_point_k");
907        Ok(paths)
908    }
909
910    /// Report results the index found but that had no path.
911    ///
912    /// The spatial index is keyed by `unique_id`; `id_to_path` is what turns
913    /// one back into a key. A delete removes the path first and the index
914    /// entry after, so a concurrent query can briefly see an id with no path.
915    /// That is benign and self-correcting.
916    ///
917    /// It is logged rather than swallowed because the alternative is a silent
918    /// short result: the caller asked for `k` and got fewer, with nothing
919    /// anywhere saying why. A *persistent* count here is not a race, it is
920    /// `id_to_path` drifting from the index, which no other check would catch.
921    fn note_unmapped(found: usize, kept: usize, op: &str) {
922        if kept < found {
923            log::debug!(
924                "{}: {} of {} index hits had no path and were dropped, so the result \
925                 is short by that many. Transient during a concurrent delete; \
926                 persistent means id_to_path has drifted from the spatial index.",
927                op,
928                found - kept,
929                found,
930            );
931        }
932    }
933
934    /// Find the k nearest stored nodes to the given path's position in hyperbolic space.
935    /// Returns paths sorted by ascending hyperbolic distance.
936    pub fn find_nearest(&self, path: &str, k: usize) -> IntegrationResult<Vec<(String, g_math::fixed_point::FixedPoint)>> {
937        let sig = self
938            .path_map
939            .get(path)
940            .ok_or_else(|| IntegrationError::NotFound(format!("Node at path {} not found", path)))?;
941
942        let unique_id = sig.value().unique_id();
943        // Drop the DashMap guard before calling tensor_network
944        drop(sig);
945
946        let point = self
947            .tensor_network
948            .get_point(&unique_id)
949            .ok_or_else(|| IntegrationError::OperationFailed("Point not found in spatial index".to_string()))?;
950
951        let results = self
952            .tensor_network
953            .nearest_neighbor_point_k(&point, k + 1); // +1 to exclude self
954
955        // Self-exclusion is intended and is not a dropped result, so it is
956        // discounted before counting what genuinely had no path.
957        let mut considered = 0usize;
958        let mut paths = Vec::new();
959        for (uid, dist) in results {
960            if uid == unique_id {
961                continue; // Skip self
962            }
963            considered += 1;
964            if let Some(p) = self.id_to_path.get(&uid) {
965                paths.push((p.value().clone(), dist));
966            }
967        }
968        Self::note_unmapped(considered, paths.len(), "find_nearest");
969        paths.truncate(k);
970        Ok(paths)
971    }
972
973    // -----------------------------------------------------------------------
974    // Semantic dimensional distance queries
975    // -----------------------------------------------------------------------
976
977    /// Find the k nearest nodes by Euclidean distance across a dimensional slice
978    /// of the semantic coordinate space.
979    ///
980    /// `query_coords`: raw Q64.64 bytes representing the query point.
981    /// `k`: number of results.
982    /// `dim_range`: which dimensions to compare (e.g., `16..33` for category axes).
983    ///
984    /// Returns `(path, distance)` sorted by distance ascending.
985    pub fn nearest_semantic(
986        &self,
987        query_coords: &[u8],
988        k: usize,
989        dim_range: &Range<usize>,
990    ) -> IntegrationResult<Vec<(String, g_math::fixed_point::FixedPoint)>> {
991        // The network returns node keys (== the paths registered at insert),
992        // already sorted ascending by (distance, key).
993        Ok(self.tensor_network.nearest_semantic(query_coords, k, dim_range))
994    }
995
996    /// Find the k nearest nodes to an existing node by semantic dimensional distance.
997    ///
998    /// Convenience wrapper: reads the node's semantic coordinates, then calls
999    /// `nearest_semantic`. The queried node is excluded from results.
1000    pub fn neighbors_semantic(
1001        &self,
1002        path: &str,
1003        k: usize,
1004        dim_range: &Range<usize>,
1005    ) -> IntegrationResult<Vec<(String, g_math::fixed_point::FixedPoint)>> {
1006        let coords = self.get_semantic(path)?;
1007
1008        // Request k+1 to account for self, then filter.
1009        // The network returns node keys directly; resolve the queried
1010        // node's canonical key through path_map so a non-normalized input
1011        // still matches its own entry.
1012        let results = self.tensor_network.nearest_semantic(&coords, k + 1, dim_range);
1013
1014        let self_key = self
1015            .path_map
1016            .get(path)
1017            .and_then(|r| self.id_to_path.get(&r.value().unique_id()).map(|p| p.value().clone()));
1018
1019        let mut paths = Vec::with_capacity(k);
1020        for (key, dist) in results {
1021            if Some(&key) == self_key.as_ref() {
1022                continue; // Skip self
1023            }
1024            paths.push((key, dist));
1025            if paths.len() >= k {
1026                break;
1027            }
1028        }
1029        Ok(paths)
1030    }
1031
1032    /// Find all stored nodes within hyperbolic radius of the given path.
1033    /// Returns paths and their distances.
1034    pub fn find_in_radius(&self, path: &str, radius: g_math::fixed_point::FixedPoint) -> IntegrationResult<Vec<(String, g_math::fixed_point::FixedPoint)>> {
1035        let sig = self
1036            .path_map
1037            .get(path)
1038            .ok_or_else(|| IntegrationError::NotFound(format!("Node at path {} not found", path)))?;
1039
1040        let unique_id = sig.value().unique_id();
1041        drop(sig);
1042
1043        let point = self
1044            .tensor_network
1045            .get_point(&unique_id)
1046            .ok_or_else(|| IntegrationError::OperationFailed("Point not found in spatial index".to_string()))?;
1047
1048        let results = self
1049            .tensor_network
1050            .nodes_in_radius(&point, radius);
1051
1052        let mut paths = Vec::new();
1053        for (uid, dist) in results {
1054            if uid == unique_id {
1055                continue; // Skip self
1056            }
1057            if let Some(p) = self.id_to_path.get(&uid) {
1058                paths.push((p.value().clone(), dist));
1059            }
1060        }
1061        Ok(paths)
1062    }
1063}
1064
1065impl Debug for HyperbolicTreeTensor {
1066    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1067        write!(f, "HyperbolicTreeTensor(nodes={})", self.node_count())
1068    }
1069}
1070
1071/// Thread-safe wrapper for the HyperbolicTreeTensor.
1072///
1073/// No outer RwLock needed: all interior state uses DashMap, Mutex, or RwLock
1074/// for fine-grained concurrency. Methods take `&self`.
1075pub type SharedHTT = Arc<HyperbolicTreeTensor>;
1076
1077#[cfg(test)]
1078mod tests {
1079    use super::*;
1080
1081    #[test]
1082    fn test_path_operations() {
1083        let path_ops = DefaultPathOps;
1084
1085        let components = path_ops.split_path("/a/b/c");
1086        assert_eq!(
1087            components,
1088            vec!["a".to_string(), "b".to_string(), "c".to_string()]
1089        );
1090
1091        let path = path_ops.join_path(&["a".to_string(), "b".to_string(), "c".to_string()]);
1092        assert_eq!(path, "/a/b/c");
1093
1094        assert_eq!(path_ops.parent_path("/a/b/c"), Some("/a/b".to_string()));
1095        assert_eq!(path_ops.parent_path("/a"), Some("/".to_string()));
1096        assert_eq!(path_ops.parent_path("/"), None);
1097
1098        assert_eq!(path_ops.last_component("/a/b/c"), Some("c".to_string()));
1099        assert_eq!(path_ops.last_component("/a"), Some("a".to_string()));
1100        assert_eq!(path_ops.last_component("/"), None);
1101    }
1102
1103    #[test]
1104    fn test_tree_tensor_creation() {
1105        let config = HTTConfig::default();
1106        let tree = HyperbolicTreeTensor::new(config);
1107        assert_eq!(tree.node_count(), 0);
1108    }
1109
1110    #[test]
1111    fn test_tree_tensor_operations() {
1112        let config = HTTConfig::default();
1113        let tree = HyperbolicTreeTensor::new(config);
1114
1115        // Insert root node
1116        tree.insert("/", vec![], None).unwrap();
1117        assert_eq!(tree.node_count(), 1);
1118        assert!(tree.exists("/"));
1119
1120        // Get root node
1121        let root = tree.get("/").unwrap();
1122        assert_eq!(root.metadata().key, "/");
1123
1124        // Insert child nodes
1125        tree.insert("/child1", b"child1 data".to_vec(), None).unwrap();
1126        tree.insert("/child2", b"child2 data".to_vec(), None).unwrap();
1127        assert_eq!(tree.node_count(), 3);
1128
1129        // Insert grandchild
1130        tree.insert("/child1/grandchild", b"grandchild data".to_vec(), None).unwrap();
1131        assert_eq!(tree.node_count(), 4);
1132
1133        // List children
1134        let children = tree.list_children("/").unwrap();
1135        assert_eq!(children.len(), 2);
1136
1137        let child_keys: Vec<&str> = children.iter().map(|c| c.metadata().key.as_str()).collect();
1138        assert!(child_keys.contains(&"/child1"));
1139        assert!(child_keys.contains(&"/child2"));
1140
1141        // Update a node
1142        tree.update_value("/child1", b"updated data".to_vec()).unwrap();
1143        let updated = tree.get("/child1").unwrap();
1144        assert_eq!(updated.value(), b"updated data");
1145
1146        // List subtree
1147        let subtree = tree.list_subtree("/").unwrap();
1148        assert_eq!(subtree.len(), 3); // child1, child2, child1/grandchild
1149
1150        // Try to delete node with children (should fail)
1151        let result = tree.delete("/child1");
1152        assert!(result.is_err());
1153
1154        // Delete leaf node
1155        tree.delete("/child1/grandchild").unwrap();
1156        assert_eq!(tree.node_count(), 3);
1157
1158        // Now delete former parent
1159        tree.delete("/child1").unwrap();
1160        assert_eq!(tree.node_count(), 2);
1161    }
1162
1163    #[test]
1164    fn test_tree_validation() {
1165        let config = HTTConfig::default();
1166        let tree = HyperbolicTreeTensor::new(config);
1167
1168        // Empty tree should be valid
1169        assert!(tree.validate());
1170
1171        // Add some nodes
1172        tree.insert("/", vec![], None).unwrap();
1173        tree.insert("/child", b"child data".to_vec(), None).unwrap();
1174
1175        // Tree with nodes should still be valid
1176        assert!(tree.validate());
1177    }
1178
1179    #[test]
1180    fn test_id_to_path_mapping() {
1181        let config = HTTConfig::default();
1182        let tree = HyperbolicTreeTensor::new(config);
1183
1184        tree.insert("/", vec![], None).unwrap();
1185        tree.insert("/test", b"test".to_vec(), None).unwrap();
1186
1187        // Verify id_to_path works
1188        let uid = tree.path_map.get("/test").unwrap().value().unique_id();
1189        let resolved_path = tree.path_for_id(&uid);
1190        assert_eq!(resolved_path, Some("/test".to_string()));
1191    }
1192
1193    #[test]
1194    fn test_find_nearest() {
1195        let config = HTTConfig::default();
1196        let tree = HyperbolicTreeTensor::new(config);
1197
1198        tree.insert("/", vec![], None).unwrap();
1199        tree.insert("/a", b"a".to_vec(), None).unwrap();
1200        tree.insert("/b", b"b".to_vec(), None).unwrap();
1201        tree.insert("/c", b"c".to_vec(), None).unwrap();
1202        tree.insert("/a/child", b"ac".to_vec(), None).unwrap();
1203
1204        // Find nearest to /a — should return other nodes sorted by distance
1205        let nearest = tree.find_nearest("/a", 3).unwrap();
1206        assert!(!nearest.is_empty());
1207        assert!(nearest.len() <= 3);
1208
1209        // Results should not include /a itself
1210        let paths: Vec<&str> = nearest.iter().map(|(p, _)| p.as_str()).collect();
1211        assert!(!paths.contains(&"/a"));
1212
1213        // Distances should be in ascending order
1214        for i in 1..nearest.len() {
1215            assert!(nearest[i].1 >= nearest[i - 1].1);
1216        }
1217    }
1218
1219    #[test]
1220    fn test_find_in_radius() {
1221        use g_math::fixed_point::FixedPoint;
1222
1223        let config = HTTConfig::default();
1224        let tree = HyperbolicTreeTensor::new(config);
1225
1226        tree.insert("/", vec![], None).unwrap();
1227        tree.insert("/a", b"a".to_vec(), None).unwrap();
1228        tree.insert("/b", b"b".to_vec(), None).unwrap();
1229
1230        // With a very large radius, should find other nodes
1231        let large_radius = FixedPoint::from_int(10);
1232        let results = tree.find_in_radius("/", large_radius).unwrap();
1233        assert!(results.len() >= 2, "Expected at least 2 nodes within large radius, got {}", results.len());
1234
1235        // With a tiny radius, should find few or no nodes
1236        let tiny_radius = FixedPoint::from_int(1) / FixedPoint::from_int(10000);
1237        let results = tree.find_in_radius("/", tiny_radius).unwrap();
1238        // The root is at origin, children are close but not at zero distance
1239        // So with a very tiny radius, we might find 0 or a few
1240        assert!(results.len() <= 2);
1241    }
1242
1243    #[test]
1244    fn test_delete_unregisters_spatial() {
1245        let config = HTTConfig::default();
1246        let tree = HyperbolicTreeTensor::new(config);
1247
1248        tree.insert("/", vec![], None).unwrap();
1249        tree.insert("/leaf", b"leaf".to_vec(), None).unwrap();
1250
1251        // Get the unique_id before deletion
1252        let unique_id = tree.path_map.get("/leaf").unwrap().value().unique_id();
1253
1254        // Verify point exists in tensor network
1255        assert!(tree.tensor_network().get_point(&unique_id).is_some());
1256
1257        // Delete the leaf
1258        tree.delete("/leaf").unwrap();
1259
1260        // Point should be removed from spatial index
1261        assert!(tree.tensor_network().get_point(&unique_id).is_none());
1262        assert!(tree.path_for_id(&unique_id).is_none());
1263    }
1264}