Skip to main content

horon_engine/
store.rs

1//! store.rs - Simple, ergonomic wrapper around HTTStorage
2//!
3//! Provides a dead-simple API that hides all internal types (FixedPoint,
4//! IntegrationError, geometric signatures, etc.) behind standard Rust types.
5//!
6//! # Quick Start
7//!
8//! ```
9//! use horon_engine::Store;
10//!
11//! let store = Store::new();
12//! store.put("/greeting", b"Hello, world!").unwrap();
13//! let data = store.get("/greeting").unwrap();
14//! assert_eq!(data, b"Hello, world!");
15//! ```
16
17use std::collections::HashMap;
18use std::fmt;
19use std::ops::Range;
20
21use g_math::fixed_point::FixedPoint;
22
23use super::config::HTTStorageConfig;
24use super::constants::{OUTLIER_KNN, OUTLIER_MIN_POPULATION};
25use super::metric_tree::{EuclideanMetric, MetricVpTree};
26use super::storage::HTTStorage;
27use super::tensor_network::HyperbolicTensorNetwork;
28use super::tree_tensor::IntegrationError;
29
30// ---------------------------------------------------------------------------
31// StoreError
32// ---------------------------------------------------------------------------
33
34/// Simplified error type for Store operations.
35#[derive(Debug)]
36pub enum StoreError {
37    /// The requested key was not found.
38    NotFound(String),
39    /// A key already exists (when an exclusive insert was expected).
40    AlreadyExists(String),
41    /// The operation was invalid (bad key, configuration error, etc.).
42    InvalidOperation(String),
43    /// An internal error occurred (lock poisoned, deserialization, etc.).
44    Internal(String),
45}
46
47impl fmt::Display for StoreError {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        match self {
50            StoreError::NotFound(msg) => write!(f, "not found: {}", msg),
51            StoreError::AlreadyExists(msg) => write!(f, "already exists: {}", msg),
52            StoreError::InvalidOperation(msg) => write!(f, "invalid operation: {}", msg),
53            StoreError::Internal(msg) => write!(f, "internal error: {}", msg),
54        }
55    }
56}
57
58impl std::error::Error for StoreError {}
59
60/// A semantic outlier found by [`Store::find_outliers`]: a node whose
61/// average distance to its nearest peers is anomalously large relative to
62/// the population under the queried prefix.
63#[derive(Debug, Clone, PartialEq)]
64pub struct SemanticOutlier {
65    /// The outlying node's key.
66    pub key: String,
67    /// Its average distance to its k nearest peers in the population.
68    pub avg_knn_distance: FixedPoint,
69    /// How many standard deviations that average sits above the population
70    /// mean (always > the requested threshold).
71    pub z_score: FixedPoint,
72    /// The closest peer — "even its best match is this far away".
73    pub nearest_peer: String,
74    /// Distance to that closest peer.
75    pub nearest_distance: FixedPoint,
76}
77
78impl From<IntegrationError> for StoreError {
79    fn from(e: IntegrationError) -> Self {
80        match e {
81            IntegrationError::NotFound(msg) => StoreError::NotFound(msg),
82            IntegrationError::AlreadyExists(msg) => StoreError::AlreadyExists(msg),
83            IntegrationError::ValidationFailed(msg) | IntegrationError::ConfigurationError(msg) => {
84                StoreError::InvalidOperation(msg)
85            }
86            IntegrationError::OperationFailed(msg)
87            | IntegrationError::DeserializationError(msg)
88            | IntegrationError::LockError(msg) => StoreError::Internal(msg),
89        }
90    }
91}
92
93// ---------------------------------------------------------------------------
94// StoreConfig
95// ---------------------------------------------------------------------------
96
97/// Minimal configuration for a Store.
98///
99/// Power users who need control over dimension, grid resolution, or other
100/// internals should use [`HTTStorage`] directly.
101pub struct StoreConfig {
102    capacity: usize,
103    tau: FixedPoint,
104}
105
106impl StoreConfig {
107    /// Create a new config with default capacity (10,000 nodes).
108    pub fn new() -> Self {
109        Self { capacity: 10_000, tau: FixedPoint::from_int(0) }
110    }
111
112    /// Set the expected number of in-memory nodes.
113    ///
114    /// **Advisory only**: this sizes internal caches; it does not enforce a
115    /// limit. Inserts beyond `capacity` succeed and the store grows unbounded.
116    pub fn capacity(mut self, n: usize) -> Self {
117        self.capacity = n;
118        self
119    }
120
121    /// Set the Sarkar embedding scale factor τ.
122    ///
123    /// Controls the hyperbolic distance between parent and child nodes.
124    /// Default is 1.0. Smaller values allow deeper trees within the same
125    /// Q64.64 precision budget; larger values give better angular separation
126    /// between siblings.
127    pub fn tau(mut self, t: FixedPoint) -> Self {
128        self.tau = t;
129        self
130    }
131
132    fn to_htt_config(&self) -> HTTStorageConfig {
133        HTTStorageConfig {
134            dimension: 4,
135            max_memory_nodes: self.capacity,
136            cache_size: std::cmp::max(self.capacity / 10, 10),
137            storage_path: None,
138            flush_interval: 60,
139            optimize_on_shutdown: true,
140            grid_resolution: 0,
141            tau: self.tau,
142        }
143    }
144}
145
146impl Default for StoreConfig {
147    fn default() -> Self {
148        Self::new()
149    }
150}
151
152// ---------------------------------------------------------------------------
153// QueryAdapter
154// ---------------------------------------------------------------------------
155
156/// Result from a query adapter.
157#[derive(Debug, Clone)]
158pub enum QueryResult {
159    /// A single entry with key, data, and metadata.
160    Entry {
161        /// The entry's path key.
162        key: String,
163        /// The entry's raw data payload.
164        data: Vec<u8>,
165        /// The entry's key-value metadata.
166        meta: HashMap<String, String>,
167    },
168    /// A count of matching entries.
169    Count(usize),
170    /// A list of matching keys.
171    Keys(Vec<String>),
172}
173
174/// Trait for pluggable query adapters.
175///
176/// Adapters encapsulate query logic (e.g. search by metadata, semantic
177/// similarity, path patterns) and call `Store` methods internally.
178/// This is object-safe: `dyn QueryAdapter` works.
179pub trait QueryAdapter: Send + Sync {
180    /// Execute a query against the store.
181    fn execute(&self, store: &Store, query: &str) -> Result<Vec<QueryResult>, StoreError>;
182}
183
184// ---------------------------------------------------------------------------
185// Store
186// ---------------------------------------------------------------------------
187
188/// A simple, ergonomic hierarchical data store backed by Hyperbolic Tree Tensors.
189///
190/// All keys are path-like strings (e.g. `"/users/alice"`). Parent directories
191/// are created automatically. The root node `"/"` always exists.
192///
193/// # Examples
194///
195/// ```
196/// use horon_engine::Store;
197/// use g_math::fixed_point::FixedPoint;
198///
199/// let store = Store::new();
200///
201/// // Store and retrieve data
202/// store.put("/config/db", b"postgres://localhost").unwrap();
203/// assert_eq!(store.get("/config/db").unwrap(), b"postgres://localhost");
204///
205/// // Coordinates and distances are Q64.64 fixed point, never floats: the
206/// // same query returns bit-identical results on any platform. Converting
207/// // from a decimal literal is explicit, so the lossy step is visible at
208/// // the call site rather than hidden inside the API.
209/// let origin: Vec<FixedPoint> = (0..4).map(|_| FixedPoint::from_int(0)).collect();
210/// let (path, distance) = store.nearest(&origin).unwrap();
211/// ```
212pub struct Store {
213    inner: HTTStorage,
214}
215
216impl Store {
217    /// Create a new Store with default settings (capacity: 10,000).
218    pub fn new() -> Self {
219        Self::with_config(StoreConfig::new())
220    }
221
222    /// Create a new Store with custom configuration.
223    pub fn with_config(config: StoreConfig) -> Self {
224        Self {
225            inner: HTTStorage::new(config.to_htt_config()),
226        }
227    }
228
229    /// Store data at a key (upsert — inserts or updates).
230    ///
231    /// Parent directories are created automatically.
232    pub fn put(&self, key: &str, data: &[u8]) -> Result<(), StoreError> {
233        self.inner.store(key, data, None)?;
234        Ok(())
235    }
236
237    /// Store data without geometric embedding (data + semantic only).
238    ///
239    /// Much faster than `put()` for bulk loading — skips Sarkar embedding,
240    /// VP-tree, and power diagram construction. Semantic queries
241    /// (`nearest_semantic`, `neighbors_semantic`, `get_semantic`) work
242    /// normally. Spatial queries (`nearest`, `neighbors`) will not find
243    /// nodes loaded this way.
244    pub fn put_data_only(&self, key: &str, data: &[u8]) -> Result<(), StoreError> {
245        self.inner.store_data_only(key, data, None)?;
246        Ok(())
247    }
248
249    /// Store data with an explicit child_index for deterministic Sarkar reconstruction.
250    ///
251    /// Used during snapshot replay: the stored child_index ensures the node gets
252    /// the same geometric position regardless of replay order.
253    pub fn put_positioned(&self, key: &str, data: &[u8], child_index: u32) -> Result<(), StoreError> {
254        self.inner.store_positioned(key, data, None, child_index)?;
255        Ok(())
256    }
257
258    /// Retrieve data by key.
259    pub fn get(&self, key: &str) -> Result<Vec<u8>, StoreError> {
260        Ok(self.inner.retrieve(key)?)
261    }
262
263    /// Remove a key and its data.
264    pub fn remove(&self, key: &str) -> Result<(), StoreError> {
265        self.inner.delete(key)?;
266        Ok(())
267    }
268
269    /// Check if a key exists.
270    pub fn exists(&self, key: &str) -> bool {
271        self.inner.exists(key)
272    }
273
274    /// List immediate children of a path.
275    ///
276    /// Returns only direct children, not the full subtree.
277    pub fn children(&self, path: &str) -> Result<Vec<String>, StoreError> {
278        let htt = self.inner.shared_htt();
279        let nodes = htt.list_children(path)?;
280        Ok(nodes.into_iter().map(|n| n.metadata().key.clone()).collect())
281    }
282
283    /// List all keys under a prefix (full subtree).
284    pub fn list(&self, prefix: &str) -> Result<Vec<String>, StoreError> {
285        Ok(self.inner.list(prefix)?)
286    }
287
288    /// Set a metadata field on a key.
289    pub fn set_meta(&self, key: &str, name: &str, value: &str) -> Result<(), StoreError> {
290        self.inner.set_metadata(key, name, value)?;
291        Ok(())
292    }
293
294    /// Get all metadata for a key.
295    pub fn get_meta(&self, key: &str) -> Result<HashMap<String, String>, StoreError> {
296        Ok(self.inner.get_metadata(key)?)
297    }
298
299    /// Set semantic coordinates on a key (raw Q64.64 bytes, 16 bytes per dimension).
300    ///
301    /// Each coordinate is 16 bytes (i128 LE). For example, 2 semantic dimensions
302    /// requires 32 bytes. Use `FixedPoint::from_f64(value).raw().to_le_bytes()`
303    /// to encode each coordinate.
304    ///
305    /// Returns `InvalidOperation` if `coords` is not a multiple of 16 bytes —
306    /// a misaligned vector would otherwise have its trailing partial dimension
307    /// silently ignored by distance computations.
308    pub fn set_semantic(&self, key: &str, coords: Vec<u8>) -> Result<(), StoreError> {
309        if coords.len() % 16 != 0 {
310            return Err(StoreError::InvalidOperation(format!(
311                "semantic coordinates must be a multiple of 16 bytes (one Q64.64 value per dimension); got {} bytes",
312                coords.len()
313            )));
314        }
315        self.inner.set_semantic(key, coords)?;
316        Ok(())
317    }
318
319    /// Get semantic coordinates for a key (raw Q64.64 bytes).
320    ///
321    /// Returns empty Vec if no semantic coordinates have been set.
322    pub fn get_semantic(&self, key: &str) -> Result<Vec<u8>, StoreError> {
323        Ok(self.inner.get_semantic(key)?)
324    }
325
326    /// Find the nearest stored node to an arbitrary point in hyperbolic space.
327    ///
328    /// Coordinates are in the Poincare disk model (each component in `(-1, 1)`).
329    /// Uses the Nielsen power diagram for O(1) point location.
330    ///
331    /// Returns `(key, hyperbolic_distance)`.
332    pub fn nearest(&self, coords: &[FixedPoint]) -> Result<(String, FixedPoint), StoreError> {
333        Ok(self.inner.nearest_neighbor_point(coords)?)
334    }
335
336    /// Find the k nearest stored nodes to an arbitrary point in hyperbolic space.
337    ///
338    /// Like `nearest()` but returns multiple candidates, enabling the caller
339    /// to post-filter and still get results.
340    ///
341    /// **Complexity**: unlike `nearest()`, this always takes the bucketed
342    /// VP-tree path — O(B + log(n/B)) with B ≈ 61 fixed buckets — not the
343    /// O(1) power-diagram fast path.
344    ///
345    /// Returns `(key, hyperbolic_distance)` sorted by ascending distance.
346    pub fn nearest_k(&self, coords: &[FixedPoint], k: usize) -> Result<Vec<(String, FixedPoint)>, StoreError> {
347        Ok(self.inner.nearest_neighbor_point_k(coords, k)?)
348    }
349
350    /// Find the k nearest neighbors of an existing node.
351    ///
352    /// Returns keys sorted by ascending hyperbolic distance.
353    /// The queried key itself is excluded from results.
354    ///
355    /// **Complexity**: bucketed VP-tree search — O(B + log(n/B)) with
356    /// B ≈ 61 fixed buckets — not the O(1) power-diagram fast path.
357    pub fn neighbors(&self, path: &str, k: usize) -> Result<Vec<String>, StoreError> {
358        Ok(self.inner.find_nearest(path, k)?)
359    }
360
361    // -----------------------------------------------------------------------
362    // Semantic dimensional distance queries
363    // -----------------------------------------------------------------------
364
365    /// Find the k nearest nodes by Euclidean distance across a dimensional slice.
366    ///
367    /// A dimensional slice selects which semantic dimensions to compare.
368    /// For example, `16..33` compares only category preference axes,
369    /// ignoring operational dimensions. Different slices answer different
370    /// questions from the same data.
371    ///
372    /// `query_coords`: raw Q64.64 bytes (16 bytes per dimension).
373    /// `k`: number of nearest neighbors to return.
374    /// `dim_range`: which dimensions to include in the distance calculation.
375    ///
376    /// Returns `(key, distance)` sorted ascending by `(distance, key)` —
377    /// ties break deterministically.
378    /// Returns `InvalidOperation` if `query_coords` is not a multiple of 16 bytes.
379    ///
380    /// **Complexity** (`docs/SEMANTIC_INDEX.md`): stores below
381    /// `SEMANTIC_INDEX_MIN_NODES` use a brute-force O(n × d) scan. Larger
382    /// stores use a lazily built per-`dim_range` VP-tree: O(log n) expected
383    /// per warm query on low-dimensional slices; the first query for a slice
384    /// after any semantic write pays an O(n log n) rebuild. Results are
385    /// identical on both paths.
386    pub fn nearest_semantic(
387        &self,
388        query_coords: &[u8],
389        k: usize,
390        dim_range: Range<usize>,
391    ) -> Result<Vec<(String, FixedPoint)>, StoreError> {
392        if query_coords.len() % 16 != 0 {
393            return Err(StoreError::InvalidOperation(format!(
394                "semantic query coordinates must be a multiple of 16 bytes; got {} bytes",
395                query_coords.len()
396            )));
397        }
398        let results = self.inner.nearest_semantic(query_coords, k, &dim_range)?;
399        Ok(results)
400    }
401
402    /// Find the k nearest nodes to an existing node by semantic dimensional distance.
403    ///
404    /// Reads the node's semantic coordinates and finds the closest other nodes
405    /// in the specified dimensional slice. The queried node is excluded.
406    ///
407    /// **Complexity**: same routing as [`Store::nearest_semantic`] (indexed
408    /// above the node floor, brute-force below).
409    ///
410    /// Returns keys sorted by ascending semantic distance.
411    pub fn neighbors_semantic(
412        &self,
413        path: &str,
414        k: usize,
415        dim_range: Range<usize>,
416    ) -> Result<Vec<(String, FixedPoint)>, StoreError> {
417        let results = self.inner.neighbors_semantic(path, k, &dim_range)?;
418        Ok(results)
419    }
420
421    /// Find the k stored nodes most similar to an existing node across a
422    /// dimensional slice — "what's like this one?".
423    ///
424    /// This is [`Store::neighbors_semantic`] under a task-shaped name: it
425    /// reads the node's semantic coordinates and returns the k nearest other
426    /// nodes by Euclidean distance over `dim_range`, sorted ascending by
427    /// `(distance, key)`. Same routing and cost as `nearest_semantic`.
428    pub fn find_similar(
429        &self,
430        key: &str,
431        k: usize,
432        dim_range: Range<usize>,
433    ) -> Result<Vec<(String, FixedPoint)>, StoreError> {
434        self.neighbors_semantic(key, k, dim_range)
435    }
436
437    /// Find semantic outliers among the nodes under a key prefix:
438    /// nodes whose average distance to their nearest peers is anomalously
439    /// large relative to the population.
440    ///
441    /// For each node under `prefix` that has semantic coordinates, computes
442    /// the average distance to its `OUTLIER_KNN` (10, capped at
443    /// population−1) nearest peers **within the same population** over
444    /// `dim_range`, then flags nodes whose average exceeds the population
445    /// mean by more than `z_threshold` standard deviations. Returns outliers
446    /// sorted by descending z-score (ties by key). This is the
447    /// "room 403 rates unlike its floor-mates" / "course far from every
448    /// peer" query as one call.
449    ///
450    /// Statistics are computed strictly within the prefix population — nodes
451    /// outside `prefix` (or without coordinates) neither appear nor skew the
452    /// baseline. Populations below `OUTLIER_MIN_POPULATION` (5) return no
453    /// outliers: z-scores over a handful of nodes are noise, not findings.
454    ///
455    /// **Complexity**: builds a dedicated VP-tree over the population
456    /// (O(m log m) distance evaluations) plus one k-NN query per node —
457    /// ~seconds at 10k nodes, versus the O(m²) pairwise scan this replaces.
458    /// Deterministic: identical stores produce identical results.
459    ///
460    /// Returns `InvalidOperation` if `z_threshold` is not a finite positive
461    /// number.
462    pub fn find_outliers(
463        &self,
464        prefix: &str,
465        z_threshold: FixedPoint,
466        dim_range: Range<usize>,
467    ) -> Result<Vec<SemanticOutlier>, StoreError> {
468        if z_threshold <= FixedPoint::from_int(0) {
469            return Err(StoreError::InvalidOperation(format!(
470                "z_threshold must be a positive number; got {}",
471                z_threshold.to_f64()
472            )));
473        }
474
475        // Population: prefix members with semantic coordinates, key-sorted
476        // (deterministic accumulation order for the statistics below).
477        let mut keys = self.list(prefix)?;
478        keys.sort();
479        let entries: Vec<(String, Vec<FixedPoint>)> = keys
480            .into_iter()
481            .filter_map(|key| {
482                let coords = self.inner.get_semantic(&key).ok()?;
483                if coords.is_empty() {
484                    return None;
485                }
486                Some((
487                    key,
488                    HyperbolicTensorNetwork::decode_semantic_slice(&coords, &dim_range),
489                ))
490            })
491            .collect();
492
493        if entries.len() < OUTLIER_MIN_POPULATION {
494            return Ok(Vec::new());
495        }
496
497        // Population-local index: outlier statistics must not be skewed by
498        // nodes outside the prefix, so the store-wide slice cache is not
499        // reusable here.
500        let tree = MetricVpTree::build(entries.clone(), &EuclideanMetric);
501        let k = OUTLIER_KNN.min(entries.len() - 1);
502
503        // Per-node average k-NN distance (querying k+1 to skip self).
504        let zero = FixedPoint::from_int(0);
505        let mut scored: Vec<(String, FixedPoint, String, FixedPoint)> = entries
506            .iter()
507            .map(|(key, point)| {
508                let peers: Vec<(String, FixedPoint)> = tree
509                    .knn(point, k + 1, &EuclideanMetric)
510                    .into_iter()
511                    .filter(|(id, _)| id != key)
512                    .take(k)
513                    .collect();
514                let sum = peers.iter().fold(zero, |acc, (_, d)| acc + *d);
515                let avg = sum / FixedPoint::from_int(k as i32);
516                let (nearest_peer, nearest_distance) = peers[0].clone();
517                (key.clone(), avg, nearest_peer, nearest_distance)
518            })
519            .collect();
520
521        // Population statistics in fixed point: these feed the z-score that
522        // decides which nodes are reported, so float arithmetic here would
523        // put a nondeterministic step inside a query result.
524        let n = FixedPoint::from_int(scored.len() as i32);
525        let mean = scored.iter().fold(zero, |acc, (_, avg, _, _)| acc + *avg) / n;
526        let variance = scored
527            .iter()
528            .fold(zero, |acc, (_, avg, _, _)| {
529                let d = *avg - mean;
530                acc + d * d
531            })
532            / n;
533        let stdev = variance.sqrt();
534        if stdev <= FixedPoint::from_raw(1) {
535            return Ok(Vec::new()); // uniform population — no outliers
536        }
537
538        scored.sort_by(|a, b| {
539            b.1.partial_cmp(&a.1)
540                .unwrap_or(std::cmp::Ordering::Equal)
541                .then_with(|| a.0.cmp(&b.0))
542        });
543
544        Ok(scored
545            .into_iter()
546            .filter_map(|(key, avg, nearest_peer, nearest_distance)| {
547                let z_score = (avg - mean) / stdev;
548                (z_score > z_threshold).then_some(SemanticOutlier {
549                    key,
550                    avg_knn_distance: avg,
551                    z_score,
552                    nearest_peer,
553                    nearest_distance,
554                })
555            })
556            .collect())
557    }
558
559    /// Upgrade a data-only key (inserted via [`Store::put_data_only`]) to a
560    /// full geometric embedding, in place.
561    ///
562    /// Missing ancestors are embedded first; the node's key, value,
563    /// metadata, and semantic coordinates are preserved. After this call the
564    /// key participates in spatial queries (`nearest`, `neighbors`,
565    /// `find_within`) and has a [`Store::position`].
566    ///
567    /// Returns whether this call performed the upgrade (`false` = the key
568    /// was already embedded; idempotent). Positions are derived state, not
569    /// persisted: deterministic for a fixed operation sequence, but a
570    /// lazily-loaded store must re-embed after reopening.
571    pub fn embed_existing(&self, key: &str) -> Result<bool, StoreError> {
572        Ok(self.inner.embed_existing(key)?)
573    }
574
575    /// Embed the prefix node (when it exists) and every data-only key under
576    /// it (convenience). Parents embed before children (sorted order +
577    /// ancestor recursion). Returns how many keys this call upgraded.
578    pub fn embed_all(&self, prefix: &str) -> Result<usize, StoreError> {
579        let mut upgraded = 0;
580        if self.exists(prefix) && self.embed_existing(prefix)? {
581            upgraded += 1;
582        }
583        let mut keys = self.list(prefix)?;
584        keys.sort();
585        for key in keys {
586            if self.embed_existing(&key)? {
587                upgraded += 1;
588            }
589        }
590        Ok(upgraded)
591    }
592
593    /// The hyperbolic (Poincaré) position of a stored key.
594    ///
595    /// Errors for unknown keys and for data-only nodes (no embedding).
596    pub fn position(&self, key: &str) -> Result<Vec<FixedPoint>, StoreError> {
597        let point = self.inner.position(key)?;
598        Ok(point.coords().iter().copied().collect())
599    }
600
601    /// The exact fixed-point position — crate-internal (semantic disk
602    /// derives barycenters from it without an f64 round-trip).
603    pub(crate) fn position_fixed(
604        &self,
605        key: &str,
606    ) -> Result<crate::hyperbolic_geometry::HyperbolicPoint, StoreError> {
607        Ok(self.inner.position(key)?)
608    }
609
610    /// Monotone counter of semantic-relevant mutations (coordinate writes,
611    /// inserts, deletes). External caches over semantic state — e.g. the semantic disk
612    /// [`crate::semantic_disk::SemanticDisk`] — tag their builds with it and
613    /// rebuild when it has advanced, exactly like the internal index cache.
614    pub fn semantic_epoch(&self) -> u64 {
615        self.inner.semantic_epoch()
616    }
617
618    /// Compute the Euclidean distance between two raw semantic coordinate vectors
619    /// across a dimensional slice.
620    ///
621    /// Utility method for computing distances without querying the store.
622    pub fn semantic_distance(
623        coords_a: &[u8],
624        coords_b: &[u8],
625        dim_range: Range<usize>,
626    ) -> FixedPoint {
627        HyperbolicTensorNetwork::semantic_distance(coords_a, coords_b, &dim_range)
628    }
629
630    /// Find all nodes within a hyperbolic distance of an existing node.
631    ///
632    /// **Complexity**: bucketed VP-tree range search; cost grows with the
633    /// number of buckets intersecting the radius and the result size.
634    pub fn find_within(&self, path: &str, radius: FixedPoint) -> Result<Vec<String>, StoreError> {
635        Ok(self.inner.find_in_radius(path, radius)?)
636    }
637
638    /// Execute a query using a pluggable adapter.
639    ///
640    /// The adapter receives a reference to this store and the query string,
641    /// and returns results by calling `get`, `list`, `neighbors`, etc.
642    pub fn query(&self, adapter: &dyn QueryAdapter, query: &str) -> Result<Vec<QueryResult>, StoreError> {
643        adapter.execute(self, query)
644    }
645
646    /// Number of stored entries (excludes the root node).
647    pub fn len(&self) -> usize {
648        self.inner.node_count().saturating_sub(1) // exclude root
649    }
650
651    /// Returns `true` if the store contains no user data.
652    pub fn is_empty(&self) -> bool {
653        self.len() == 0
654    }
655
656    /// Access the underlying `HTTStorage` for advanced operations.
657    pub fn inner(&self) -> &HTTStorage {
658        &self.inner
659    }
660
661    /// Mutably access the underlying `HTTStorage` for advanced operations.
662    #[deprecated(note = "All HTTStorage methods now take &self; use inner() instead")]
663    pub fn inner_mut(&mut self) -> &mut HTTStorage {
664        &mut self.inner
665    }
666}
667
668impl Default for Store {
669    fn default() -> Self {
670        Self::new()
671    }
672}
673
674// ---------------------------------------------------------------------------
675// Tests
676// ---------------------------------------------------------------------------
677
678#[cfg(test)]
679mod tests {
680
681/// Exact fixed-point coordinates from decimal literals.
682fn fp(vals: &[f64]) -> Vec<g_math::fixed_point::FixedPoint> {
683    vals.iter().map(|&v| g_math::fixed_point::FixedPoint::from_f64(v)).collect()
684}
685
686    use super::*;
687
688    #[test]
689    fn test_new_store_is_empty() {
690        let store = Store::new();
691        assert!(store.is_empty());
692        assert_eq!(store.len(), 0);
693    }
694
695    #[test]
696    fn test_put_get_roundtrip() {
697        let store = Store::new();
698        store.put("/hello", b"world").unwrap();
699        assert_eq!(store.get("/hello").unwrap(), b"world");
700    }
701
702    #[test]
703    fn test_upsert() {
704        let store = Store::new();
705        store.put("/key", b"v1").unwrap();
706        store.put("/key", b"v2").unwrap();
707        assert_eq!(store.get("/key").unwrap(), b"v2");
708    }
709
710    #[test]
711    fn test_remove() {
712        let store = Store::new();
713        store.put("/tmp", b"data").unwrap();
714        assert!(store.exists("/tmp"));
715        store.remove("/tmp").unwrap();
716        assert!(!store.exists("/tmp"));
717    }
718
719    #[test]
720    fn test_exists() {
721        let store = Store::new();
722        assert!(!store.exists("/nope"));
723        store.put("/yes", b"").unwrap();
724        assert!(store.exists("/yes"));
725    }
726
727    #[test]
728    fn test_children() {
729        let store = Store::new();
730        store.put("/a/b", b"1").unwrap();
731        store.put("/a/c", b"2").unwrap();
732        store.put("/a/c/d", b"3").unwrap();
733
734        let kids = store.children("/a").unwrap();
735        assert!(kids.contains(&"/a/b".to_string()));
736        assert!(kids.contains(&"/a/c".to_string()));
737        // /a/c/d is a grandchild, not a direct child
738        assert!(!kids.contains(&"/a/c/d".to_string()));
739    }
740
741    #[test]
742    fn test_list() {
743        let store = Store::new();
744        store.put("/x/y", b"1").unwrap();
745        store.put("/x/z", b"2").unwrap();
746
747        let all = store.list("/x").unwrap();
748        assert!(all.contains(&"/x/y".to_string()));
749        assert!(all.contains(&"/x/z".to_string()));
750    }
751
752    #[test]
753    fn test_metadata() {
754        let store = Store::new();
755        store.put("/doc", b"content").unwrap();
756        store.set_meta("/doc", "author", "alice").unwrap();
757
758        let meta = store.get_meta("/doc").unwrap();
759        assert_eq!(meta.get("author"), Some(&"alice".to_string()));
760    }
761
762    #[test]
763    fn test_nearest() {
764        let store = Store::new();
765        store.put("/a", b"a").unwrap();
766        store.put("/b", b"b").unwrap();
767
768        let (path, dist) = store.nearest(&fp(&[0.0, 0.0, 0.0, 0.0])).unwrap();
769        // Origin query should find root "/"
770        assert_eq!(path, "/");
771        assert!(dist.to_f64() < 0.1);
772    }
773
774    #[test]
775    fn test_neighbors() {
776        let store = Store::new();
777        store.put("/a", b"a").unwrap();
778        store.put("/b", b"b").unwrap();
779        store.put("/c", b"c").unwrap();
780
781        let nbrs = store.neighbors("/a", 2).unwrap();
782        assert!(!nbrs.is_empty());
783        assert!(nbrs.len() <= 2);
784        assert!(!nbrs.contains(&"/a".to_string()));
785    }
786
787    #[test]
788    fn test_find_within() {
789        let store = Store::new();
790        store.put("/x", b"x").unwrap();
791        store.put("/y", b"y").unwrap();
792
793        let results = store.find_within("/x", g_math::fixed_point::FixedPoint::from_f64(10.0)).unwrap();
794        assert!(!results.is_empty());
795    }
796
797    #[test]
798    fn test_error_not_found() {
799        let store = Store::new();
800        let err = store.get("/missing").unwrap_err();
801        assert!(matches!(err, StoreError::NotFound(_)));
802    }
803
804    #[test]
805    fn test_len_tracking() {
806        let store = Store::new();
807        assert_eq!(store.len(), 0);
808
809        store.put("/one", b"1").unwrap();
810        assert_eq!(store.len(), 1);
811
812        store.put("/two", b"2").unwrap();
813        assert_eq!(store.len(), 2);
814
815        store.remove("/one").unwrap();
816        assert_eq!(store.len(), 1);
817    }
818
819    #[test]
820    fn test_inner_escape_hatch() {
821        let store = Store::new();
822        store.put("/test", b"data").unwrap();
823
824        // Read access via inner()
825        assert!(store.inner().exists("/test"));
826
827        // Write access via inner() — all methods are now &self
828        store.inner().store("/via_inner", b"inner", None).unwrap();
829        assert!(store.exists("/via_inner"));
830    }
831
832    #[test]
833    fn test_with_config() {
834        let config = StoreConfig::new().capacity(500);
835        let store = Store::with_config(config);
836        assert!(store.is_empty());
837    }
838
839    #[test]
840    fn test_tau_config() {
841        let store = Store::with_config(StoreConfig::new().capacity(1000).tau(FixedPoint::from_f64(0.8)));
842        store.put("/a", b"a").unwrap();
843        store.put("/b", b"b").unwrap();
844        store.put("/a/child", b"c").unwrap();
845        assert_eq!(store.len(), 3);
846
847        // NN should still work
848        let (path, dist) = store.nearest(&fp(&[0.0, 0.0, 0.0, 0.0])).unwrap();
849        assert_eq!(path, "/");
850        assert!(dist.to_f64() < 0.1);
851    }
852
853    #[test]
854    fn test_tau_deep_tree() {
855        // tau=0.8 allows deeper trees within Q64.64 precision
856        let store = Store::with_config(StoreConfig::new().tau(FixedPoint::from_f64(0.8)));
857        let mut path = String::new();
858        for i in 0..40 {
859            path = format!("{}/n{}", path, i);
860            store.put(&path, b"x").unwrap();
861        }
862        assert!(store.exists(&path));
863
864        let (nn, _) = store.nearest(&fp(&[0.0, 0.0, 0.0, 0.0])).unwrap();
865        assert!(store.exists(&nn));
866    }
867
868    #[test]
869    fn test_query_adapter() {
870        // Simple adapter that lists children of a path
871        struct ChildrenAdapter;
872        impl QueryAdapter for ChildrenAdapter {
873            fn execute(&self, store: &Store, query: &str) -> Result<Vec<QueryResult>, StoreError> {
874                let children = store.children(query)?;
875                Ok(vec![QueryResult::Keys(children)])
876            }
877        }
878
879        let store = Store::new();
880        store.put("/a/b", b"1").unwrap();
881        store.put("/a/c", b"2").unwrap();
882
883        let results = store.query(&ChildrenAdapter, "/a").unwrap();
884        assert_eq!(results.len(), 1);
885        match &results[0] {
886            QueryResult::Keys(keys) => {
887                assert!(keys.contains(&"/a/b".to_string()));
888                assert!(keys.contains(&"/a/c".to_string()));
889            }
890            _ => panic!("Expected Keys result"),
891        }
892    }
893
894    #[test]
895    fn test_query_adapter_object_safe() {
896        // Verify QueryAdapter is object-safe (dyn QueryAdapter works)
897        struct CountAdapter;
898        impl QueryAdapter for CountAdapter {
899            fn execute(&self, store: &Store, query: &str) -> Result<Vec<QueryResult>, StoreError> {
900                let keys = store.list(query)?;
901                Ok(vec![QueryResult::Count(keys.len())])
902            }
903        }
904
905        let adapter: Box<dyn QueryAdapter> = Box::new(CountAdapter);
906        let store = Store::new();
907        store.put("/x", b"x").unwrap();
908        store.put("/y", b"y").unwrap();
909
910        let results = store.query(&*adapter, "/").unwrap();
911        match &results[0] {
912            QueryResult::Count(n) => assert_eq!(*n, 2),
913            _ => panic!("Expected Count result"),
914        }
915    }
916
917    #[test]
918    fn test_nearest_semantic() {
919        use g_math::fixed_point::FixedPoint;
920
921        let store = Store::new();
922        store.put("/courses/trauma/emdr", b"EMDR").unwrap();
923        store.put("/courses/trauma/ptss", b"PTSS").unwrap();
924        store.put("/courses/cgt/basis", b"CGT").unwrap();
925
926        // Encode helper: 2 dims (dim 0 = trauma, dim 1 = cgt)
927        let coords = |d0: f64, d1: f64| -> Vec<u8> {
928            let mut v = vec![0u8; 2 * 16];
929            v[0..16].copy_from_slice(&FixedPoint::from_f64(d0).raw().to_le_bytes());
930            v[16..32].copy_from_slice(&FixedPoint::from_f64(d1).raw().to_le_bytes());
931            v
932        };
933
934        store.set_semantic("/courses/trauma/emdr", coords(0.9, 0.1)).unwrap();
935        store.set_semantic("/courses/trauma/ptss", coords(0.8, 0.2)).unwrap();
936        store.set_semantic("/courses/cgt/basis", coords(0.1, 0.9)).unwrap();
937
938        // Query: student with strong trauma preference
939        let query = coords(0.85, 0.15);
940        let results = store.nearest_semantic(&query, 3, 0..2).unwrap();
941
942        assert_eq!(results.len(), 3);
943        // EMDR and PTSS should be closer than CGT
944        let paths: Vec<&str> = results.iter().map(|(p, _)| p.as_str()).collect();
945        assert!(paths[0].contains("trauma"), "Nearest should be a trauma course, got {}", paths[0]);
946        assert!(paths[2].contains("cgt"), "Farthest should be CGT, got {}", paths[2]);
947    }
948
949    #[test]
950    fn test_neighbors_semantic() {
951        use g_math::fixed_point::FixedPoint;
952
953        let store = Store::new();
954        store.put("/a", b"a").unwrap();
955        store.put("/b", b"b").unwrap();
956        store.put("/c", b"c").unwrap();
957
958        let coords = |v: f64| -> Vec<u8> {
959            let mut buf = vec![0u8; 16];
960            buf[0..16].copy_from_slice(&FixedPoint::from_f64(v).raw().to_le_bytes());
961            buf
962        };
963
964        store.set_semantic("/a", coords(0.1)).unwrap();
965        store.set_semantic("/b", coords(0.2)).unwrap();
966        store.set_semantic("/c", coords(0.9)).unwrap();
967
968        // Neighbors of /a: /b should be closest, /c farthest
969        let results = store.neighbors_semantic("/a", 2, 0..1).unwrap();
970        assert_eq!(results.len(), 2);
971        assert_eq!(results[0].0, "/b", "Nearest semantic neighbor of /a should be /b");
972        assert_eq!(results[1].0, "/c", "Second neighbor of /a should be /c");
973
974        // Self (/a) should not appear in results
975        let paths: Vec<&str> = results.iter().map(|(p, _)| p.as_str()).collect();
976        assert!(!paths.contains(&"/a"), "Self should be excluded from neighbors_semantic");
977    }
978
979    #[test]
980    fn test_semantic_distance_utility() {
981        use g_math::fixed_point::FixedPoint;
982
983        let coords = |d0: f64, d1: f64| -> Vec<u8> {
984            let mut v = vec![0u8; 2 * 16];
985            v[0..16].copy_from_slice(&FixedPoint::from_f64(d0).raw().to_le_bytes());
986            v[16..32].copy_from_slice(&FixedPoint::from_f64(d1).raw().to_le_bytes());
987            v
988        };
989
990        let a = coords(0.0, 0.0);
991        let b = coords(0.3, 0.4);
992
993        // Euclidean distance should be 0.5 (3-4-5 triangle)
994        let dist = Store::semantic_distance(&a, &b, 0..2);
995        assert!((dist.to_f64() - 0.5).abs() < 0.01,
996            "Distance (0,0)→(0.3,0.4) should be 0.5, got {}", dist.to_f64());
997    }
998}