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