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 /// The power-diagram grid supplies candidates in O(1); the answer is
330 /// then decided by hyperbolic distance against the VP-tree's candidate
331 /// as well, so the result is the true nearest node.
332 ///
333 /// **Complexity**: O(log n). The grid alone cannot decide the query —
334 /// it holds one owner per tile, and Sarkar placement drives power cells
335 /// below tile size within a few levels, so a grid hit may name a node
336 /// that is not nearest.
337 ///
338 /// Returns `(key, hyperbolic_distance)`.
339 pub fn nearest(&self, coords: &[FixedPoint]) -> Result<(String, FixedPoint), StoreError> {
340 Ok(self.inner.nearest_neighbor_point(coords)?)
341 }
342
343 /// Find the k nearest stored nodes to an arbitrary point in hyperbolic space.
344 ///
345 /// Like `nearest()` but returns multiple candidates, enabling the caller
346 /// to post-filter and still get results.
347 ///
348 /// **Complexity**: unlike `nearest()`, this always takes the bucketed
349 /// VP-tree path — O(B + log(n/B)) with B ≈ 61 fixed buckets — not the
350 /// O(1) power-diagram fast path.
351 ///
352 /// Returns `(key, hyperbolic_distance)` sorted by ascending distance.
353 pub fn nearest_k(&self, coords: &[FixedPoint], k: usize) -> Result<Vec<(String, FixedPoint)>, StoreError> {
354 Ok(self.inner.nearest_neighbor_point_k(coords, k)?)
355 }
356
357 /// Find the k nearest neighbors of an existing node.
358 ///
359 /// Returns keys sorted by ascending hyperbolic distance.
360 /// The queried key itself is excluded from results.
361 ///
362 /// **Complexity**: bucketed VP-tree search — O(B + log(n/B)) with
363 /// B ≈ 61 fixed buckets — not the O(1) power-diagram fast path.
364 pub fn neighbors(&self, path: &str, k: usize) -> Result<Vec<String>, StoreError> {
365 Ok(self.inner.find_nearest(path, k)?)
366 }
367
368 // -----------------------------------------------------------------------
369 // Semantic dimensional distance queries
370 // -----------------------------------------------------------------------
371
372 /// Find the k nearest nodes by Euclidean distance across a dimensional slice.
373 ///
374 /// A dimensional slice selects which semantic dimensions to compare.
375 /// For example, `16..33` compares only category preference axes,
376 /// ignoring operational dimensions. Different slices answer different
377 /// questions from the same data.
378 ///
379 /// `query_coords`: raw Q64.64 bytes (16 bytes per dimension).
380 /// `k`: number of nearest neighbors to return.
381 /// `dim_range`: which dimensions to include in the distance calculation.
382 ///
383 /// Returns `(key, distance)` sorted ascending by `(distance, key)` —
384 /// ties break deterministically.
385 /// Returns `InvalidOperation` if `query_coords` is not a multiple of 16 bytes.
386 ///
387 /// **Complexity** (`docs/SEMANTIC_INDEX.md`): stores below
388 /// `SEMANTIC_INDEX_MIN_NODES` use a brute-force O(n × d) scan. Larger
389 /// stores use a lazily built per-`dim_range` VP-tree: O(log n) expected
390 /// per warm query on low-dimensional slices; the first query for a slice
391 /// after any semantic write pays an O(n log n) rebuild. Results are
392 /// identical on both paths.
393 pub fn nearest_semantic(
394 &self,
395 query_coords: &[u8],
396 k: usize,
397 dim_range: Range<usize>,
398 ) -> Result<Vec<(String, FixedPoint)>, StoreError> {
399 if query_coords.len() % 16 != 0 {
400 return Err(StoreError::InvalidOperation(format!(
401 "semantic query coordinates must be a multiple of 16 bytes; got {} bytes",
402 query_coords.len()
403 )));
404 }
405 let results = self.inner.nearest_semantic(query_coords, k, &dim_range)?;
406 Ok(results)
407 }
408
409 /// Find the k nearest nodes to an existing node by semantic dimensional distance.
410 ///
411 /// Reads the node's semantic coordinates and finds the closest other nodes
412 /// in the specified dimensional slice. The queried node is excluded.
413 ///
414 /// **Complexity**: same routing as [`Store::nearest_semantic`] (indexed
415 /// above the node floor, brute-force below).
416 ///
417 /// Returns keys sorted by ascending semantic distance.
418 pub fn neighbors_semantic(
419 &self,
420 path: &str,
421 k: usize,
422 dim_range: Range<usize>,
423 ) -> Result<Vec<(String, FixedPoint)>, StoreError> {
424 let results = self.inner.neighbors_semantic(path, k, &dim_range)?;
425 Ok(results)
426 }
427
428 /// Find the k stored nodes most similar to an existing node across a
429 /// dimensional slice — "what's like this one?".
430 ///
431 /// This is [`Store::neighbors_semantic`] under a task-shaped name: it
432 /// reads the node's semantic coordinates and returns the k nearest other
433 /// nodes by Euclidean distance over `dim_range`, sorted ascending by
434 /// `(distance, key)`. Same routing and cost as `nearest_semantic`.
435 pub fn find_similar(
436 &self,
437 key: &str,
438 k: usize,
439 dim_range: Range<usize>,
440 ) -> Result<Vec<(String, FixedPoint)>, StoreError> {
441 self.neighbors_semantic(key, k, dim_range)
442 }
443
444 /// Find semantic outliers among the nodes under a key prefix:
445 /// nodes whose average distance to their nearest peers is anomalously
446 /// large relative to the population.
447 ///
448 /// For each node under `prefix` that has semantic coordinates, computes
449 /// the average distance to its `OUTLIER_KNN` (10, capped at
450 /// population−1) nearest peers **within the same population** over
451 /// `dim_range`, then flags nodes whose average exceeds the population
452 /// mean by more than `z_threshold` standard deviations. Returns outliers
453 /// sorted by descending z-score (ties by key). This is the
454 /// "room 403 rates unlike its floor-mates" / "course far from every
455 /// peer" query as one call.
456 ///
457 /// Statistics are computed strictly within the prefix population — nodes
458 /// outside `prefix` (or without coordinates) neither appear nor skew the
459 /// baseline. Populations below `OUTLIER_MIN_POPULATION` (5) return no
460 /// outliers: z-scores over a handful of nodes are noise, not findings.
461 ///
462 /// **Complexity**: builds a dedicated VP-tree over the population
463 /// (O(m log m) distance evaluations) plus one k-NN query per node —
464 /// ~seconds at 10k nodes, versus the O(m²) pairwise scan this replaces.
465 /// Deterministic: identical stores produce identical results.
466 ///
467 /// Returns `InvalidOperation` if `z_threshold` is not a finite positive
468 /// number.
469 pub fn find_outliers(
470 &self,
471 prefix: &str,
472 z_threshold: FixedPoint,
473 dim_range: Range<usize>,
474 ) -> Result<Vec<SemanticOutlier>, StoreError> {
475 if z_threshold <= FixedPoint::from_int(0) {
476 return Err(StoreError::InvalidOperation(format!(
477 "z_threshold must be a positive number; got {}",
478 z_threshold.to_f64()
479 )));
480 }
481
482 // Population: prefix members with semantic coordinates, key-sorted
483 // (deterministic accumulation order for the statistics below).
484 let mut keys = self.list(prefix)?;
485 keys.sort();
486 let entries: Vec<(String, Vec<FixedPoint>)> = keys
487 .into_iter()
488 .filter_map(|key| {
489 let coords = self.inner.get_semantic(&key).ok()?;
490 if coords.is_empty() {
491 return None;
492 }
493 Some((
494 key,
495 HyperbolicTensorNetwork::decode_semantic_slice(&coords, &dim_range),
496 ))
497 })
498 .collect();
499
500 if entries.len() < OUTLIER_MIN_POPULATION {
501 return Ok(Vec::new());
502 }
503
504 // Population-local index: outlier statistics must not be skewed by
505 // nodes outside the prefix, so the store-wide slice cache is not
506 // reusable here.
507 let tree = MetricVpTree::build(entries.clone(), &EuclideanMetric);
508 let k = OUTLIER_KNN.min(entries.len() - 1);
509
510 // Per-node average k-NN distance (querying k+1 to skip self).
511 let zero = FixedPoint::from_int(0);
512 let mut scored: Vec<(String, FixedPoint, String, FixedPoint)> = entries
513 .iter()
514 .map(|(key, point)| {
515 let peers: Vec<(String, FixedPoint)> = tree
516 .knn(point, k + 1, &EuclideanMetric)
517 .into_iter()
518 .filter(|(id, _)| id != key)
519 .take(k)
520 .collect();
521 let sum = peers.iter().fold(zero, |acc, (_, d)| acc + *d);
522 let avg = sum / FixedPoint::from_int(k as i32);
523 let (nearest_peer, nearest_distance) = peers[0].clone();
524 (key.clone(), avg, nearest_peer, nearest_distance)
525 })
526 .collect();
527
528 // Population statistics in fixed point: these feed the z-score that
529 // decides which nodes are reported, so float arithmetic here would
530 // put a nondeterministic step inside a query result.
531 let n = FixedPoint::from_int(scored.len() as i32);
532 let mean = scored.iter().fold(zero, |acc, (_, avg, _, _)| acc + *avg) / n;
533 let variance = scored
534 .iter()
535 .fold(zero, |acc, (_, avg, _, _)| {
536 let d = *avg - mean;
537 acc + d * d
538 })
539 / n;
540 let stdev = variance.sqrt();
541 if stdev <= FixedPoint::from_raw(1) {
542 return Ok(Vec::new()); // uniform population — no outliers
543 }
544
545 scored.sort_by(|a, b| {
546 b.1.partial_cmp(&a.1)
547 .unwrap_or(std::cmp::Ordering::Equal)
548 .then_with(|| a.0.cmp(&b.0))
549 });
550
551 Ok(scored
552 .into_iter()
553 .filter_map(|(key, avg, nearest_peer, nearest_distance)| {
554 let z_score = (avg - mean) / stdev;
555 (z_score > z_threshold).then_some(SemanticOutlier {
556 key,
557 avg_knn_distance: avg,
558 z_score,
559 nearest_peer,
560 nearest_distance,
561 })
562 })
563 .collect())
564 }
565
566 /// Upgrade a data-only key (inserted via [`Store::put_data_only`]) to a
567 /// full geometric embedding, in place.
568 ///
569 /// Missing ancestors are embedded first; the node's key, value,
570 /// metadata, and semantic coordinates are preserved. After this call the
571 /// key participates in spatial queries (`nearest`, `neighbors`,
572 /// `find_within`) and has a [`Store::position`].
573 ///
574 /// Returns whether this call performed the upgrade (`false` = the key
575 /// was already embedded; idempotent). Positions are derived state, not
576 /// persisted: deterministic for a fixed operation sequence, but a
577 /// lazily-loaded store must re-embed after reopening.
578 pub fn embed_existing(&self, key: &str) -> Result<bool, StoreError> {
579 Ok(self.inner.embed_existing(key)?)
580 }
581
582 /// Embed the prefix node (when it exists) and every data-only key under
583 /// it (convenience). Parents embed before children (sorted order +
584 /// ancestor recursion). Returns how many keys this call upgraded.
585 pub fn embed_all(&self, prefix: &str) -> Result<usize, StoreError> {
586 let mut upgraded = 0;
587 if self.exists(prefix) && self.embed_existing(prefix)? {
588 upgraded += 1;
589 }
590 let mut keys = self.list(prefix)?;
591 keys.sort();
592 for key in keys {
593 if self.embed_existing(&key)? {
594 upgraded += 1;
595 }
596 }
597 Ok(upgraded)
598 }
599
600 /// The hyperbolic (Poincaré) position of a stored key.
601 ///
602 /// Errors for unknown keys and for data-only nodes (no embedding).
603 pub fn position(&self, key: &str) -> Result<Vec<FixedPoint>, StoreError> {
604 let point = self.inner.position(key)?;
605 Ok(point.coords().iter().copied().collect())
606 }
607
608 /// The exact fixed-point position — crate-internal (semantic disk
609 /// derives barycenters from it without an f64 round-trip).
610 pub(crate) fn position_fixed(
611 &self,
612 key: &str,
613 ) -> Result<crate::hyperbolic_geometry::HyperbolicPoint, StoreError> {
614 Ok(self.inner.position(key)?)
615 }
616
617 /// Monotone counter of semantic-relevant mutations (coordinate writes,
618 /// inserts, deletes). External caches over semantic state — e.g. the semantic disk
619 /// [`crate::semantic_disk::SemanticDisk`] — tag their builds with it and
620 /// rebuild when it has advanced, exactly like the internal index cache.
621 pub fn semantic_epoch(&self) -> u64 {
622 self.inner.semantic_epoch()
623 }
624
625 /// Compute the Euclidean distance between two raw semantic coordinate vectors
626 /// across a dimensional slice.
627 ///
628 /// Utility method for computing distances without querying the store.
629 pub fn semantic_distance(
630 coords_a: &[u8],
631 coords_b: &[u8],
632 dim_range: Range<usize>,
633 ) -> FixedPoint {
634 HyperbolicTensorNetwork::semantic_distance(coords_a, coords_b, &dim_range)
635 }
636
637 /// Find all nodes within a hyperbolic distance of an existing node.
638 ///
639 /// **Complexity**: bucketed VP-tree range search; cost grows with the
640 /// number of buckets intersecting the radius and the result size.
641 pub fn find_within(&self, path: &str, radius: FixedPoint) -> Result<Vec<String>, StoreError> {
642 Ok(self.inner.find_in_radius(path, radius)?)
643 }
644
645 /// Execute a query using a pluggable adapter.
646 ///
647 /// The adapter receives a reference to this store and the query string,
648 /// and returns results by calling `get`, `list`, `neighbors`, etc.
649 pub fn query(&self, adapter: &dyn QueryAdapter, query: &str) -> Result<Vec<QueryResult>, StoreError> {
650 adapter.execute(self, query)
651 }
652
653 /// Number of stored entries (excludes the root node).
654 pub fn len(&self) -> usize {
655 self.inner.node_count().saturating_sub(1) // exclude root
656 }
657
658 /// Returns `true` if the store contains no user data.
659 pub fn is_empty(&self) -> bool {
660 self.len() == 0
661 }
662
663 /// Access the underlying `HTTStorage` for advanced operations.
664 pub fn inner(&self) -> &HTTStorage {
665 &self.inner
666 }
667
668 /// Mutably access the underlying `HTTStorage` for advanced operations.
669 #[deprecated(note = "All HTTStorage methods now take &self; use inner() instead")]
670 pub fn inner_mut(&mut self) -> &mut HTTStorage {
671 &mut self.inner
672 }
673}
674
675impl Default for Store {
676 fn default() -> Self {
677 Self::new()
678 }
679}
680
681// ---------------------------------------------------------------------------
682// Tests
683// ---------------------------------------------------------------------------
684
685#[cfg(test)]
686mod tests {
687
688/// Exact fixed-point coordinates from decimal literals.
689fn fp(vals: &[f64]) -> Vec<g_math::fixed_point::FixedPoint> {
690 vals.iter().map(|&v| g_math::fixed_point::FixedPoint::from_f64(v)).collect()
691}
692
693 use super::*;
694
695 #[test]
696 fn test_new_store_is_empty() {
697 let store = Store::new();
698 assert!(store.is_empty());
699 assert_eq!(store.len(), 0);
700 }
701
702 #[test]
703 fn test_put_get_roundtrip() {
704 let store = Store::new();
705 store.put("/hello", b"world").unwrap();
706 assert_eq!(store.get("/hello").unwrap(), b"world");
707 }
708
709 #[test]
710 fn test_upsert() {
711 let store = Store::new();
712 store.put("/key", b"v1").unwrap();
713 store.put("/key", b"v2").unwrap();
714 assert_eq!(store.get("/key").unwrap(), b"v2");
715 }
716
717 #[test]
718 fn test_remove() {
719 let store = Store::new();
720 store.put("/tmp", b"data").unwrap();
721 assert!(store.exists("/tmp"));
722 store.remove("/tmp").unwrap();
723 assert!(!store.exists("/tmp"));
724 }
725
726 #[test]
727 fn test_exists() {
728 let store = Store::new();
729 assert!(!store.exists("/nope"));
730 store.put("/yes", b"").unwrap();
731 assert!(store.exists("/yes"));
732 }
733
734 #[test]
735 fn test_children() {
736 let store = Store::new();
737 store.put("/a/b", b"1").unwrap();
738 store.put("/a/c", b"2").unwrap();
739 store.put("/a/c/d", b"3").unwrap();
740
741 let kids = store.children("/a").unwrap();
742 assert!(kids.contains(&"/a/b".to_string()));
743 assert!(kids.contains(&"/a/c".to_string()));
744 // /a/c/d is a grandchild, not a direct child
745 assert!(!kids.contains(&"/a/c/d".to_string()));
746 }
747
748 #[test]
749 fn test_list() {
750 let store = Store::new();
751 store.put("/x/y", b"1").unwrap();
752 store.put("/x/z", b"2").unwrap();
753
754 let all = store.list("/x").unwrap();
755 assert!(all.contains(&"/x/y".to_string()));
756 assert!(all.contains(&"/x/z".to_string()));
757 }
758
759 #[test]
760 fn test_metadata() {
761 let store = Store::new();
762 store.put("/doc", b"content").unwrap();
763 store.set_meta("/doc", "author", "alice").unwrap();
764
765 let meta = store.get_meta("/doc").unwrap();
766 assert_eq!(meta.get("author"), Some(&"alice".to_string()));
767 }
768
769 #[test]
770 fn test_nearest() {
771 let store = Store::new();
772 store.put("/a", b"a").unwrap();
773 store.put("/b", b"b").unwrap();
774
775 let (path, dist) = store.nearest(&fp(&[0.0, 0.0, 0.0, 0.0])).unwrap();
776 // Origin query should find root "/"
777 assert_eq!(path, "/");
778 assert!(dist.to_f64() < 0.1);
779 }
780
781 #[test]
782 fn test_neighbors() {
783 let store = Store::new();
784 store.put("/a", b"a").unwrap();
785 store.put("/b", b"b").unwrap();
786 store.put("/c", b"c").unwrap();
787
788 let nbrs = store.neighbors("/a", 2).unwrap();
789 assert!(!nbrs.is_empty());
790 assert!(nbrs.len() <= 2);
791 assert!(!nbrs.contains(&"/a".to_string()));
792 }
793
794 #[test]
795 fn test_find_within() {
796 let store = Store::new();
797 store.put("/x", b"x").unwrap();
798 store.put("/y", b"y").unwrap();
799
800 let results = store.find_within("/x", g_math::fixed_point::FixedPoint::from_f64(10.0)).unwrap();
801 assert!(!results.is_empty());
802 }
803
804 #[test]
805 fn test_error_not_found() {
806 let store = Store::new();
807 let err = store.get("/missing").unwrap_err();
808 assert!(matches!(err, StoreError::NotFound(_)));
809 }
810
811 #[test]
812 fn test_len_tracking() {
813 let store = Store::new();
814 assert_eq!(store.len(), 0);
815
816 store.put("/one", b"1").unwrap();
817 assert_eq!(store.len(), 1);
818
819 store.put("/two", b"2").unwrap();
820 assert_eq!(store.len(), 2);
821
822 store.remove("/one").unwrap();
823 assert_eq!(store.len(), 1);
824 }
825
826 #[test]
827 fn test_inner_escape_hatch() {
828 let store = Store::new();
829 store.put("/test", b"data").unwrap();
830
831 // Read access via inner()
832 assert!(store.inner().exists("/test"));
833
834 // Write access via inner() — all methods are now &self
835 store.inner().store("/via_inner", b"inner", None).unwrap();
836 assert!(store.exists("/via_inner"));
837 }
838
839 #[test]
840 fn test_with_config() {
841 let config = StoreConfig::new().capacity(500);
842 let store = Store::with_config(config);
843 assert!(store.is_empty());
844 }
845
846 #[test]
847 fn test_tau_config() {
848 let store = Store::with_config(StoreConfig::new().capacity(1000).tau(FixedPoint::from_f64(0.8)));
849 store.put("/a", b"a").unwrap();
850 store.put("/b", b"b").unwrap();
851 store.put("/a/child", b"c").unwrap();
852 assert_eq!(store.len(), 3);
853
854 // NN should still work
855 let (path, dist) = store.nearest(&fp(&[0.0, 0.0, 0.0, 0.0])).unwrap();
856 assert_eq!(path, "/");
857 assert!(dist.to_f64() < 0.1);
858 }
859
860 #[test]
861 fn test_tau_deep_tree() {
862 // tau=0.8 allows deeper trees within Q64.64 precision
863 let store = Store::with_config(StoreConfig::new().tau(FixedPoint::from_f64(0.8)));
864 let mut path = String::new();
865 for i in 0..40 {
866 path = format!("{}/n{}", path, i);
867 store.put(&path, b"x").unwrap();
868 }
869 assert!(store.exists(&path));
870
871 let (nn, _) = store.nearest(&fp(&[0.0, 0.0, 0.0, 0.0])).unwrap();
872 assert!(store.exists(&nn));
873 }
874
875 #[test]
876 fn test_query_adapter() {
877 // Simple adapter that lists children of a path
878 struct ChildrenAdapter;
879 impl QueryAdapter for ChildrenAdapter {
880 fn execute(&self, store: &Store, query: &str) -> Result<Vec<QueryResult>, StoreError> {
881 let children = store.children(query)?;
882 Ok(vec![QueryResult::Keys(children)])
883 }
884 }
885
886 let store = Store::new();
887 store.put("/a/b", b"1").unwrap();
888 store.put("/a/c", b"2").unwrap();
889
890 let results = store.query(&ChildrenAdapter, "/a").unwrap();
891 assert_eq!(results.len(), 1);
892 match &results[0] {
893 QueryResult::Keys(keys) => {
894 assert!(keys.contains(&"/a/b".to_string()));
895 assert!(keys.contains(&"/a/c".to_string()));
896 }
897 _ => panic!("Expected Keys result"),
898 }
899 }
900
901 #[test]
902 fn test_query_adapter_object_safe() {
903 // Verify QueryAdapter is object-safe (dyn QueryAdapter works)
904 struct CountAdapter;
905 impl QueryAdapter for CountAdapter {
906 fn execute(&self, store: &Store, query: &str) -> Result<Vec<QueryResult>, StoreError> {
907 let keys = store.list(query)?;
908 Ok(vec![QueryResult::Count(keys.len())])
909 }
910 }
911
912 let adapter: Box<dyn QueryAdapter> = Box::new(CountAdapter);
913 let store = Store::new();
914 store.put("/x", b"x").unwrap();
915 store.put("/y", b"y").unwrap();
916
917 let results = store.query(&*adapter, "/").unwrap();
918 match &results[0] {
919 QueryResult::Count(n) => assert_eq!(*n, 2),
920 _ => panic!("Expected Count result"),
921 }
922 }
923
924 #[test]
925 fn test_nearest_semantic() {
926 use g_math::fixed_point::FixedPoint;
927
928 let store = Store::new();
929 store.put("/courses/trauma/emdr", b"EMDR").unwrap();
930 store.put("/courses/trauma/ptss", b"PTSS").unwrap();
931 store.put("/courses/cgt/basis", b"CGT").unwrap();
932
933 // Encode helper: 2 dims (dim 0 = trauma, dim 1 = cgt)
934 let coords = |d0: f64, d1: f64| -> Vec<u8> {
935 let mut v = vec![0u8; 2 * 16];
936 v[0..16].copy_from_slice(&FixedPoint::from_f64(d0).raw().to_le_bytes());
937 v[16..32].copy_from_slice(&FixedPoint::from_f64(d1).raw().to_le_bytes());
938 v
939 };
940
941 store.set_semantic("/courses/trauma/emdr", coords(0.9, 0.1)).unwrap();
942 store.set_semantic("/courses/trauma/ptss", coords(0.8, 0.2)).unwrap();
943 store.set_semantic("/courses/cgt/basis", coords(0.1, 0.9)).unwrap();
944
945 // Query: student with strong trauma preference
946 let query = coords(0.85, 0.15);
947 let results = store.nearest_semantic(&query, 3, 0..2).unwrap();
948
949 assert_eq!(results.len(), 3);
950 // EMDR and PTSS should be closer than CGT
951 let paths: Vec<&str> = results.iter().map(|(p, _)| p.as_str()).collect();
952 assert!(paths[0].contains("trauma"), "Nearest should be a trauma course, got {}", paths[0]);
953 assert!(paths[2].contains("cgt"), "Farthest should be CGT, got {}", paths[2]);
954 }
955
956 #[test]
957 fn test_neighbors_semantic() {
958 use g_math::fixed_point::FixedPoint;
959
960 let store = Store::new();
961 store.put("/a", b"a").unwrap();
962 store.put("/b", b"b").unwrap();
963 store.put("/c", b"c").unwrap();
964
965 let coords = |v: f64| -> Vec<u8> {
966 let mut buf = vec![0u8; 16];
967 buf[0..16].copy_from_slice(&FixedPoint::from_f64(v).raw().to_le_bytes());
968 buf
969 };
970
971 store.set_semantic("/a", coords(0.1)).unwrap();
972 store.set_semantic("/b", coords(0.2)).unwrap();
973 store.set_semantic("/c", coords(0.9)).unwrap();
974
975 // Neighbors of /a: /b should be closest, /c farthest
976 let results = store.neighbors_semantic("/a", 2, 0..1).unwrap();
977 assert_eq!(results.len(), 2);
978 assert_eq!(results[0].0, "/b", "Nearest semantic neighbor of /a should be /b");
979 assert_eq!(results[1].0, "/c", "Second neighbor of /a should be /c");
980
981 // Self (/a) should not appear in results
982 let paths: Vec<&str> = results.iter().map(|(p, _)| p.as_str()).collect();
983 assert!(!paths.contains(&"/a"), "Self should be excluded from neighbors_semantic");
984 }
985
986 #[test]
987 fn test_semantic_distance_utility() {
988 use g_math::fixed_point::FixedPoint;
989
990 let coords = |d0: f64, d1: f64| -> Vec<u8> {
991 let mut v = vec![0u8; 2 * 16];
992 v[0..16].copy_from_slice(&FixedPoint::from_f64(d0).raw().to_le_bytes());
993 v[16..32].copy_from_slice(&FixedPoint::from_f64(d1).raw().to_le_bytes());
994 v
995 };
996
997 let a = coords(0.0, 0.0);
998 let b = coords(0.3, 0.4);
999
1000 // Euclidean distance should be 0.5 (3-4-5 triangle)
1001 let dist = Store::semantic_distance(&a, &b, 0..2);
1002 assert!((dist.to_f64() - 0.5).abs() < 0.01,
1003 "Distance (0,0)→(0.3,0.4) should be 0.5, got {}", dist.to_f64());
1004 }
1005}