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        let uid = self
539            .path_map
540            .get(path)
541            .map(|r| r.value().unique_id())
542            .ok_or_else(|| IntegrationError::NotFound(format!("Node at path {} not found", path)))?;
543
544        if self.tensor_network.update_node_value(&uid, value) {
545            Ok(())
546        } else {
547            Err(IntegrationError::NotFound(format!("Node with uid {} not found in network", uid)))
548        }
549    }
550
551    /// Set a metadata key-value pair on a node.
552    pub fn set_node_metadata(&self, path: &str, key: &str, value: &str) -> IntegrationResult<()> {
553        let uid = self
554            .path_map
555            .get(path)
556            .map(|r| r.value().unique_id())
557            .ok_or_else(|| IntegrationError::NotFound(format!("Node at path {} not found", path)))?;
558
559        if self.tensor_network.set_node_metadata_entry(&uid, key, value) {
560            Ok(())
561        } else {
562            Err(IntegrationError::NotFound(format!("Node with uid {} not found in network", uid)))
563        }
564    }
565
566    /// Set semantic coordinates on a node (raw Q64.64 bytes, 16 bytes per dimension).
567    pub fn set_semantic(&self, path: &str, coords: Vec<u8>) -> IntegrationResult<()> {
568        let uid = self
569            .path_map
570            .get(path)
571            .map(|r| r.value().unique_id())
572            .ok_or_else(|| IntegrationError::NotFound(format!("Node at path {} not found", path)))?;
573
574        if self.tensor_network.set_node_semantic(&uid, coords) {
575            Ok(())
576        } else {
577            Err(IntegrationError::NotFound(format!("Node with uid {} not found in network", uid)))
578        }
579    }
580
581    /// Get semantic coordinates for a node (raw Q64.64 bytes).
582    pub fn get_semantic(&self, path: &str) -> IntegrationResult<Vec<u8>> {
583        let uid = self
584            .path_map
585            .get(path)
586            .map(|r| r.value().unique_id())
587            .ok_or_else(|| IntegrationError::NotFound(format!("Node at path {} not found", path)))?;
588
589        self.tensor_network
590            .get_node_semantic(&uid)
591            .ok_or_else(|| IntegrationError::NotFound(format!("Node with uid {} not found in network", uid)))
592    }
593
594    /// Delete a node at the specified path.
595    ///
596    /// A node with live children cannot be deleted (that would orphan them).
597    /// The check and the removal run under two stripe locks — the node's own
598    /// (which blocks a concurrent insert of a child *under* it, closing the
599    /// has-no-children TOCTOU) and its parent's (which serializes the parent's
600    /// child-list mutation against sibling inserts/deletes). The two stripes
601    /// are taken in canonical order, so this never deadlocks against a
602    /// concurrent delete.
603    pub fn delete(&self, path: &str) -> IntegrationResult<()> {
604        // Resolve the node and its parent before locking.
605        let node_uid = match self.path_map.get(path) {
606            Some(r) => r.value().unique_id(),
607            None => {
608                return Err(IntegrationError::NotFound(format!(
609                    "Node at path {} not found",
610                    path
611                )))
612            }
613        };
614        let parent_uid = self
615            .path_ops
616            .parent_path(path)
617            .and_then(|pp| self.path_map.get(&pp).map(|s| s.unique_id()));
618
619        // Hold the node's stripe (and the parent's, if any) for the whole
620        // check-then-remove. `_guards` keeps both alive to the end of scope.
621        let _guards = match &parent_uid {
622            Some(puid) => {
623                let (g1, g2) = self.parent_locks.lock_two(&node_uid, puid);
624                (Some(g1), g2)
625            }
626            None => (Some(self.parent_locks.lock(&node_uid)), None),
627        };
628
629        // Re-check under the lock: the node may have been removed, or gained a
630        // child, since the pre-lock read.
631        if !self.path_map.contains_key(path) {
632            return Err(IntegrationError::NotFound(format!(
633                "Node at path {} not found",
634                path
635            )));
636        }
637        let children = self.list_children(path)?;
638        if !children.is_empty() {
639            return Err(IntegrationError::ValidationFailed(format!(
640                "Cannot delete node at {} because it has {} children",
641                path,
642                children.len()
643            )));
644        }
645
646        // Remove from path map, reverse map, and spatial index. The parent's
647        // unique id is passed so the parent's child list drops this node even
648        // when the node has no power cell (data-only nodes).
649        if let Some((_, sig)) = self.path_map.remove(path) {
650            let unique_id = sig.unique_id();
651            self.id_to_path.remove(&unique_id);
652            self.tensor_network
653                .unregister_node_with_parent(&unique_id, parent_uid.as_deref());
654        }
655
656        Ok(())
657    }
658
659    /// List children of a node at the specified path (cloned).
660    pub fn list_children(&self, path: &str) -> IntegrationResult<Vec<CompressedNode>> {
661        let signature = self
662            .path_map
663            .get(path)
664            .ok_or_else(|| IntegrationError::NotFound(format!("Node at path {} not found", path)))?;
665
666        let children = self.tensor_network.children_of(signature.value());
667
668        // Filter out deleted nodes (removed from path_map but still in tensor network)
669        let result = children
670            .into_iter()
671            .filter(|child| self.path_map.contains_key(&child.metadata().key))
672            .collect();
673
674        Ok(result)
675    }
676
677    /// List all node paths under a specified path.
678    pub fn list_subtree(&self, path: &str) -> IntegrationResult<Vec<String>> {
679        let prefix = if path.ends_with('/') {
680            path.to_string()
681        } else {
682            format!("{}/", path)
683        };
684
685        let mut result = Vec::new();
686        for entry in self.path_map.iter() {
687            let node_path = entry.key();
688            if node_path.as_str() != path && (path == "/" || node_path.starts_with(&prefix)) {
689                result.push(node_path.clone());
690            }
691        }
692
693        Ok(result)
694    }
695
696    /// Get the path depth (number of components).
697    fn path_depth(&self, path: &str) -> u32 {
698        if path == "/" {
699            return 0;
700        }
701        self.path_ops.split_path(path).len() as u32
702    }
703
704    /// Check if a path exists.
705    pub fn exists(&self, path: &str) -> bool {
706        self.path_map.contains_key(path)
707    }
708
709    /// Get the number of nodes in the tree.
710    pub fn node_count(&self) -> usize {
711        self.path_map.len()
712    }
713
714    /// Get tree statistics.
715    pub fn stats(&self) -> HashMap<String, String> {
716        let mut stats = HashMap::new();
717        stats.insert("node_count".to_string(), self.node_count().to_string());
718        stats.insert(
719            "dimension".to_string(),
720            self.config.dimension().to_string(),
721        );
722        stats
723    }
724
725    /// Validate the tree structure.
726    ///
727    /// Checks all structural invariants: parent-child consistency,
728    /// path_map ↔ id_to_path bidirectionality, and tensor network integrity.
729    pub fn validate(&self) -> bool {
730        // Empty tree is valid
731        if self.path_map.is_empty() {
732            return true;
733        }
734
735        // Check that tensor network is valid
736        if !self.tensor_network.validate_network() {
737            return false;
738        }
739
740        // All non-root paths have valid parents
741        for entry in self.path_map.iter() {
742            let path = entry.key();
743            if path == "/" {
744                continue;
745            }
746            if let Some(parent_path) = self.path_ops.parent_path(path) {
747                if !self.path_map.contains_key(&parent_path) {
748                    return false;
749                }
750            }
751        }
752
753        // path_map ↔ id_to_path bidirectional consistency
754        for entry in self.path_map.iter() {
755            let path = entry.key();
756            let sig = entry.value();
757            let uid = sig.unique_id();
758            match self.id_to_path.get(&uid) {
759                Some(reverse_path) if reverse_path.value() == path => {},
760                _ => return false,
761            }
762        }
763        for entry in self.id_to_path.iter() {
764            let uid = entry.key();
765            let path = entry.value();
766            match self.path_map.get(path.as_str()) {
767                Some(sig) if sig.value().unique_id() == *uid => {},
768                _ => return false,
769            }
770        }
771
772        // path_map and id_to_path must have the same size
773        if self.path_map.len() != self.id_to_path.len() {
774            return false;
775        }
776
777        true
778    }
779
780    /// Get the underlying tensor network (for spatial queries).
781    pub fn tensor_network(&self) -> &HyperbolicTensorNetwork {
782        &self.tensor_network
783    }
784
785    /// Resolve a unique_id to a path.
786    pub fn path_for_id(&self, unique_id: &str) -> Option<String> {
787        self.id_to_path.get(unique_id).map(|r| r.value().clone())
788    }
789
790    /// The hyperbolic (Poincaré) position of a stored node.
791    ///
792    /// Errors with `NotFound` for unknown paths and `OperationFailed` for
793    /// data-only nodes, which have no geometric embedding.
794    pub fn position(&self, path: &str) -> IntegrationResult<super::hyperbolic_geometry::HyperbolicPoint> {
795        let sig = self
796            .path_map
797            .get(path)
798            .ok_or_else(|| IntegrationError::NotFound(format!("Node at path {} not found", path)))?;
799        let unique_id = sig.value().unique_id();
800        drop(sig);
801        self.tensor_network.get_point(&unique_id).ok_or_else(|| {
802            IntegrationError::OperationFailed(format!(
803                "node {} has no geometric embedding (data-only)",
804                path
805            ))
806        })
807    }
808
809    /// Get the path operations.
810    pub fn path_ops(&self) -> &dyn PathOperations {
811        &*self.path_ops
812    }
813
814    /// Find the nearest stored node to an arbitrary Poincaré disk point.
815    ///
816    /// Answered exactly by the cell index. Returns (path, hyperbolic_distance).
817    /// Errors when the index holds nothing.
818    pub fn nearest_neighbor_point(&self, query: &super::hyperbolic_geometry::HyperbolicPoint) -> IntegrationResult<(String, g_math::fixed_point::FixedPoint)> {
819        let (uid, dist) = self.tensor_network
820            .nearest_neighbor_point(query)
821            .ok_or_else(|| IntegrationError::OperationFailed(
822                "No nodes in tree for nearest neighbor query".to_string()
823            ))?;
824
825        let path = self.id_to_path.get(&uid)
826            .ok_or_else(|| IntegrationError::NotFound(
827                format!("No path for unique_id {}", uid)
828            ))?;
829
830        Ok((path.value().clone(), dist))
831    }
832
833    /// Find the k nearest stored nodes to an arbitrary Poincaré disk point.
834    ///
835    /// Returns `(path, hyperbolic_distance)` sorted by ascending distance.
836    ///
837    /// `k == 0` is a request for nothing and is answered with nothing — the
838    /// same as [`Self::find_nearest`]. Only an index that holds no nodes is an
839    /// error. Conflating the two used to make `nearest_k(q, 0)` report "no
840    /// nodes in tree" against a fully populated store, which is simply untrue.
841    pub fn nearest_neighbor_point_k(&self, query: &super::hyperbolic_geometry::HyperbolicPoint, k: usize) -> IntegrationResult<Vec<(String, g_math::fixed_point::FixedPoint)>> {
842        if k == 0 {
843            return Ok(Vec::new());
844        }
845        let results = self.tensor_network.nearest_neighbor_point_k(query, k);
846        if results.is_empty() {
847            return Err(IntegrationError::OperationFailed(
848                "No nodes in tree for nearest neighbor query".to_string()
849            ));
850        }
851
852        let mut paths = Vec::with_capacity(results.len());
853        for (uid, dist) in results {
854            if let Some(p) = self.id_to_path.get(&uid) {
855                paths.push((p.value().clone(), dist));
856            }
857        }
858        Ok(paths)
859    }
860
861    /// Find the k nearest stored nodes to the given path's position in hyperbolic space.
862    /// Returns paths sorted by ascending hyperbolic distance.
863    pub fn find_nearest(&self, path: &str, k: usize) -> IntegrationResult<Vec<(String, g_math::fixed_point::FixedPoint)>> {
864        let sig = self
865            .path_map
866            .get(path)
867            .ok_or_else(|| IntegrationError::NotFound(format!("Node at path {} not found", path)))?;
868
869        let unique_id = sig.value().unique_id();
870        // Drop the DashMap guard before calling tensor_network
871        drop(sig);
872
873        let point = self
874            .tensor_network
875            .get_point(&unique_id)
876            .ok_or_else(|| IntegrationError::OperationFailed("Point not found in spatial index".to_string()))?;
877
878        let results = self
879            .tensor_network
880            .nearest_neighbor_point_k(&point, k + 1); // +1 to exclude self
881
882        let mut paths = Vec::new();
883        for (uid, dist) in results {
884            if uid == unique_id {
885                continue; // Skip self
886            }
887            if let Some(p) = self.id_to_path.get(&uid) {
888                paths.push((p.value().clone(), dist));
889            }
890        }
891        paths.truncate(k);
892        Ok(paths)
893    }
894
895    // -----------------------------------------------------------------------
896    // Semantic dimensional distance queries
897    // -----------------------------------------------------------------------
898
899    /// Find the k nearest nodes by Euclidean distance across a dimensional slice
900    /// of the semantic coordinate space.
901    ///
902    /// `query_coords`: raw Q64.64 bytes representing the query point.
903    /// `k`: number of results.
904    /// `dim_range`: which dimensions to compare (e.g., `16..33` for category axes).
905    ///
906    /// Returns `(path, distance)` sorted by distance ascending.
907    pub fn nearest_semantic(
908        &self,
909        query_coords: &[u8],
910        k: usize,
911        dim_range: &Range<usize>,
912    ) -> IntegrationResult<Vec<(String, g_math::fixed_point::FixedPoint)>> {
913        // The network returns node keys (== the paths registered at insert),
914        // already sorted ascending by (distance, key).
915        Ok(self.tensor_network.nearest_semantic(query_coords, k, dim_range))
916    }
917
918    /// Find the k nearest nodes to an existing node by semantic dimensional distance.
919    ///
920    /// Convenience wrapper: reads the node's semantic coordinates, then calls
921    /// `nearest_semantic`. The queried node is excluded from results.
922    pub fn neighbors_semantic(
923        &self,
924        path: &str,
925        k: usize,
926        dim_range: &Range<usize>,
927    ) -> IntegrationResult<Vec<(String, g_math::fixed_point::FixedPoint)>> {
928        let coords = self.get_semantic(path)?;
929
930        // Request k+1 to account for self, then filter.
931        // The network returns node keys directly; resolve the queried
932        // node's canonical key through path_map so a non-normalized input
933        // still matches its own entry.
934        let results = self.tensor_network.nearest_semantic(&coords, k + 1, dim_range);
935
936        let self_key = self
937            .path_map
938            .get(path)
939            .and_then(|r| self.id_to_path.get(&r.value().unique_id()).map(|p| p.value().clone()));
940
941        let mut paths = Vec::with_capacity(k);
942        for (key, dist) in results {
943            if Some(&key) == self_key.as_ref() {
944                continue; // Skip self
945            }
946            paths.push((key, dist));
947            if paths.len() >= k {
948                break;
949            }
950        }
951        Ok(paths)
952    }
953
954    /// Find all stored nodes within hyperbolic radius of the given path.
955    /// Returns paths and their distances.
956    pub fn find_in_radius(&self, path: &str, radius: g_math::fixed_point::FixedPoint) -> IntegrationResult<Vec<(String, g_math::fixed_point::FixedPoint)>> {
957        let sig = self
958            .path_map
959            .get(path)
960            .ok_or_else(|| IntegrationError::NotFound(format!("Node at path {} not found", path)))?;
961
962        let unique_id = sig.value().unique_id();
963        drop(sig);
964
965        let point = self
966            .tensor_network
967            .get_point(&unique_id)
968            .ok_or_else(|| IntegrationError::OperationFailed("Point not found in spatial index".to_string()))?;
969
970        let results = self
971            .tensor_network
972            .nodes_in_radius(&point, radius);
973
974        let mut paths = Vec::new();
975        for (uid, dist) in results {
976            if uid == unique_id {
977                continue; // Skip self
978            }
979            if let Some(p) = self.id_to_path.get(&uid) {
980                paths.push((p.value().clone(), dist));
981            }
982        }
983        Ok(paths)
984    }
985}
986
987impl Debug for HyperbolicTreeTensor {
988    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
989        write!(f, "HyperbolicTreeTensor(nodes={})", self.node_count())
990    }
991}
992
993/// Thread-safe wrapper for the HyperbolicTreeTensor.
994///
995/// No outer RwLock needed: all interior state uses DashMap, Mutex, or RwLock
996/// for fine-grained concurrency. Methods take `&self`.
997pub type SharedHTT = Arc<HyperbolicTreeTensor>;
998
999#[cfg(test)]
1000mod tests {
1001    use super::*;
1002
1003    #[test]
1004    fn test_path_operations() {
1005        let path_ops = DefaultPathOps;
1006
1007        let components = path_ops.split_path("/a/b/c");
1008        assert_eq!(
1009            components,
1010            vec!["a".to_string(), "b".to_string(), "c".to_string()]
1011        );
1012
1013        let path = path_ops.join_path(&["a".to_string(), "b".to_string(), "c".to_string()]);
1014        assert_eq!(path, "/a/b/c");
1015
1016        assert_eq!(path_ops.parent_path("/a/b/c"), Some("/a/b".to_string()));
1017        assert_eq!(path_ops.parent_path("/a"), Some("/".to_string()));
1018        assert_eq!(path_ops.parent_path("/"), None);
1019
1020        assert_eq!(path_ops.last_component("/a/b/c"), Some("c".to_string()));
1021        assert_eq!(path_ops.last_component("/a"), Some("a".to_string()));
1022        assert_eq!(path_ops.last_component("/"), None);
1023    }
1024
1025    #[test]
1026    fn test_tree_tensor_creation() {
1027        let config = HTTConfig::default();
1028        let tree = HyperbolicTreeTensor::new(config);
1029        assert_eq!(tree.node_count(), 0);
1030    }
1031
1032    #[test]
1033    fn test_tree_tensor_operations() {
1034        let config = HTTConfig::default();
1035        let tree = HyperbolicTreeTensor::new(config);
1036
1037        // Insert root node
1038        tree.insert("/", vec![], None).unwrap();
1039        assert_eq!(tree.node_count(), 1);
1040        assert!(tree.exists("/"));
1041
1042        // Get root node
1043        let root = tree.get("/").unwrap();
1044        assert_eq!(root.metadata().key, "/");
1045
1046        // Insert child nodes
1047        tree.insert("/child1", b"child1 data".to_vec(), None).unwrap();
1048        tree.insert("/child2", b"child2 data".to_vec(), None).unwrap();
1049        assert_eq!(tree.node_count(), 3);
1050
1051        // Insert grandchild
1052        tree.insert("/child1/grandchild", b"grandchild data".to_vec(), None).unwrap();
1053        assert_eq!(tree.node_count(), 4);
1054
1055        // List children
1056        let children = tree.list_children("/").unwrap();
1057        assert_eq!(children.len(), 2);
1058
1059        let child_keys: Vec<&str> = children.iter().map(|c| c.metadata().key.as_str()).collect();
1060        assert!(child_keys.contains(&"/child1"));
1061        assert!(child_keys.contains(&"/child2"));
1062
1063        // Update a node
1064        tree.update_value("/child1", b"updated data".to_vec()).unwrap();
1065        let updated = tree.get("/child1").unwrap();
1066        assert_eq!(updated.value(), b"updated data");
1067
1068        // List subtree
1069        let subtree = tree.list_subtree("/").unwrap();
1070        assert_eq!(subtree.len(), 3); // child1, child2, child1/grandchild
1071
1072        // Try to delete node with children (should fail)
1073        let result = tree.delete("/child1");
1074        assert!(result.is_err());
1075
1076        // Delete leaf node
1077        tree.delete("/child1/grandchild").unwrap();
1078        assert_eq!(tree.node_count(), 3);
1079
1080        // Now delete former parent
1081        tree.delete("/child1").unwrap();
1082        assert_eq!(tree.node_count(), 2);
1083    }
1084
1085    #[test]
1086    fn test_tree_validation() {
1087        let config = HTTConfig::default();
1088        let tree = HyperbolicTreeTensor::new(config);
1089
1090        // Empty tree should be valid
1091        assert!(tree.validate());
1092
1093        // Add some nodes
1094        tree.insert("/", vec![], None).unwrap();
1095        tree.insert("/child", b"child data".to_vec(), None).unwrap();
1096
1097        // Tree with nodes should still be valid
1098        assert!(tree.validate());
1099    }
1100
1101    #[test]
1102    fn test_id_to_path_mapping() {
1103        let config = HTTConfig::default();
1104        let tree = HyperbolicTreeTensor::new(config);
1105
1106        tree.insert("/", vec![], None).unwrap();
1107        tree.insert("/test", b"test".to_vec(), None).unwrap();
1108
1109        // Verify id_to_path works
1110        let uid = tree.path_map.get("/test").unwrap().value().unique_id();
1111        let resolved_path = tree.path_for_id(&uid);
1112        assert_eq!(resolved_path, Some("/test".to_string()));
1113    }
1114
1115    #[test]
1116    fn test_find_nearest() {
1117        let config = HTTConfig::default();
1118        let tree = HyperbolicTreeTensor::new(config);
1119
1120        tree.insert("/", vec![], None).unwrap();
1121        tree.insert("/a", b"a".to_vec(), None).unwrap();
1122        tree.insert("/b", b"b".to_vec(), None).unwrap();
1123        tree.insert("/c", b"c".to_vec(), None).unwrap();
1124        tree.insert("/a/child", b"ac".to_vec(), None).unwrap();
1125
1126        // Find nearest to /a — should return other nodes sorted by distance
1127        let nearest = tree.find_nearest("/a", 3).unwrap();
1128        assert!(!nearest.is_empty());
1129        assert!(nearest.len() <= 3);
1130
1131        // Results should not include /a itself
1132        let paths: Vec<&str> = nearest.iter().map(|(p, _)| p.as_str()).collect();
1133        assert!(!paths.contains(&"/a"));
1134
1135        // Distances should be in ascending order
1136        for i in 1..nearest.len() {
1137            assert!(nearest[i].1 >= nearest[i - 1].1);
1138        }
1139    }
1140
1141    #[test]
1142    fn test_find_in_radius() {
1143        use g_math::fixed_point::FixedPoint;
1144
1145        let config = HTTConfig::default();
1146        let tree = HyperbolicTreeTensor::new(config);
1147
1148        tree.insert("/", vec![], None).unwrap();
1149        tree.insert("/a", b"a".to_vec(), None).unwrap();
1150        tree.insert("/b", b"b".to_vec(), None).unwrap();
1151
1152        // With a very large radius, should find other nodes
1153        let large_radius = FixedPoint::from_int(10);
1154        let results = tree.find_in_radius("/", large_radius).unwrap();
1155        assert!(results.len() >= 2, "Expected at least 2 nodes within large radius, got {}", results.len());
1156
1157        // With a tiny radius, should find few or no nodes
1158        let tiny_radius = FixedPoint::from_int(1) / FixedPoint::from_int(10000);
1159        let results = tree.find_in_radius("/", tiny_radius).unwrap();
1160        // The root is at origin, children are close but not at zero distance
1161        // So with a very tiny radius, we might find 0 or a few
1162        assert!(results.len() <= 2);
1163    }
1164
1165    #[test]
1166    fn test_delete_unregisters_spatial() {
1167        let config = HTTConfig::default();
1168        let tree = HyperbolicTreeTensor::new(config);
1169
1170        tree.insert("/", vec![], None).unwrap();
1171        tree.insert("/leaf", b"leaf".to_vec(), None).unwrap();
1172
1173        // Get the unique_id before deletion
1174        let unique_id = tree.path_map.get("/leaf").unwrap().value().unique_id();
1175
1176        // Verify point exists in tensor network
1177        assert!(tree.tensor_network().get_point(&unique_id).is_some());
1178
1179        // Delete the leaf
1180        tree.delete("/leaf").unwrap();
1181
1182        // Point should be removed from spatial index
1183        assert!(tree.tensor_network().get_point(&unique_id).is_none());
1184        assert!(tree.path_for_id(&unique_id).is_none());
1185    }
1186}