terminals-core 0.1.0

Core runtime primitives for Terminals OS: phase dynamics, AXON wire protocol, substrate engine, and sematonic types
Documentation
//! GraphProjection — HNSW neighbor topology + p-adic address.
//!
//! Stores the atom's position in the approximate nearest neighbor graph
//! and its hierarchical locality label (p-adic address).

use super::projection::{Projection, ProjectionId};

/// Max neighbor slots per atom in the HNSW graph.
pub const MAX_NEIGHBORS: usize = 8;

/// Bytes: 8 neighbors × 2 (u16) + 1 layer (u8) + 3 padding + 12 address bytes = 32 bytes.
/// Simplified: neighbors(16) + layer(4 as f32) + address base(4) + address coeffs(8) = 32.
const GRAPH_BYTES: usize = MAX_NEIGHBORS * 2 + 4 + 4 + 8;

/// P-adic address: hierarchical locality label.
/// Format: "kindIndex.depth.counter" encoded as (base: u32, coeff0: u16, coeff1: u16).
#[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct PadicAddr {
    pub base: u32,
    pub coeff0: u16,
    pub coeff1: u16,
}

impl PadicAddr {
    /// Format as "base.coeff0.coeff1" string.
    pub fn to_string(&self) -> String {
        format!("{}.{}.{}", self.base, self.coeff0, self.coeff1)
    }
}

#[derive(Debug, Clone)]
pub struct GraphProjection {
    /// Neighbor indices in the HNSW graph (u16::MAX = empty slot).
    pub neighbors: [u16; MAX_NEIGHBORS],
    /// HNSW layer level.
    pub layer: u8,
    /// P-adic hierarchical address.
    pub address: PadicAddr,
}

impl Default for GraphProjection {
    fn default() -> Self {
        Self {
            neighbors: [u16::MAX; MAX_NEIGHBORS],
            layer: 0,
            address: PadicAddr::default(),
        }
    }
}

impl Projection for GraphProjection {
    fn byte_size() -> usize {
        GRAPH_BYTES
    }

    fn id() -> ProjectionId {
        ProjectionId::Graph
    }

    fn read(buf: &[u8]) -> Self {
        assert!(buf.len() >= GRAPH_BYTES);
        let mut neighbors = [u16::MAX; MAX_NEIGHBORS];
        for i in 0..MAX_NEIGHBORS {
            let off = i * 2;
            neighbors[i] = u16::from_le_bytes([buf[off], buf[off + 1]]);
        }
        let layer_off = MAX_NEIGHBORS * 2;
        let layer = buf[layer_off];

        let addr_off = layer_off + 4; // 3 bytes padding after layer
        let base = u32::from_le_bytes([
            buf[addr_off],
            buf[addr_off + 1],
            buf[addr_off + 2],
            buf[addr_off + 3],
        ]);
        let coeff0 = u16::from_le_bytes([buf[addr_off + 4], buf[addr_off + 5]]);
        let coeff1 = u16::from_le_bytes([buf[addr_off + 6], buf[addr_off + 7]]);

        Self {
            neighbors,
            layer,
            address: PadicAddr {
                base,
                coeff0,
                coeff1,
            },
        }
    }

    fn write(&self, buf: &mut [u8]) {
        assert!(buf.len() >= GRAPH_BYTES);
        for i in 0..MAX_NEIGHBORS {
            let off = i * 2;
            buf[off..off + 2].copy_from_slice(&self.neighbors[i].to_le_bytes());
        }
        let layer_off = MAX_NEIGHBORS * 2;
        buf[layer_off] = self.layer;
        buf[layer_off + 1] = 0; // padding
        buf[layer_off + 2] = 0;
        buf[layer_off + 3] = 0;

        let addr_off = layer_off + 4;
        buf[addr_off..addr_off + 4].copy_from_slice(&self.address.base.to_le_bytes());
        buf[addr_off + 4..addr_off + 6].copy_from_slice(&self.address.coeff0.to_le_bytes());
        buf[addr_off + 6..addr_off + 8].copy_from_slice(&self.address.coeff1.to_le_bytes());
    }

    fn shape_hash_contribution(&self) -> u32 {
        let mut hash = 0x811c_9dc5u32;
        // Hash the neighbor topology (which connections exist)
        for &n in &self.neighbors {
            for byte in n.to_le_bytes() {
                hash ^= byte as u32;
                hash = hash.wrapping_mul(0x0100_0193);
            }
        }
        hash
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_graph_byte_size() {
        assert_eq!(GraphProjection::byte_size(), 32);
    }

    #[test]
    fn test_graph_roundtrip() {
        let mut proj = GraphProjection::default();
        proj.neighbors[0] = 42;
        proj.neighbors[3] = 100;
        proj.layer = 2;
        proj.address = PadicAddr {
            base: 5,
            coeff0: 1,
            coeff1: 77,
        };

        let mut buf = vec![0u8; GraphProjection::byte_size()];
        proj.write(&mut buf);
        let restored = GraphProjection::read(&buf);

        assert_eq!(restored.neighbors[0], 42);
        assert_eq!(restored.neighbors[3], 100);
        assert_eq!(restored.neighbors[7], u16::MAX); // empty slot
        assert_eq!(restored.layer, 2);
        assert_eq!(restored.address.base, 5);
        assert_eq!(restored.address.coeff0, 1);
        assert_eq!(restored.address.coeff1, 77);
    }

    #[test]
    fn test_padic_to_string() {
        let addr = PadicAddr {
            base: 3,
            coeff0: 0,
            coeff1: 42,
        };
        assert_eq!(addr.to_string(), "3.0.42");
    }
}