|
| 1 | +//! Unified CogRecord Schema — the canonical 2×8192-bit record layout. |
| 2 | +//! |
| 3 | +//! This module is the **single source of truth** for the CogRecord schema |
| 4 | +//! shared across ladybug-rs, crewai-rust, and n8n-rs when compiled together. |
| 5 | +//! |
| 6 | +//! ```text |
| 7 | +//! CogRecord = [Container; 2] = 2 KB = 16,384 bits |
| 8 | +//! ┌─────────────────────────────────────────────────────────────┐ |
| 9 | +//! │ meta (8,192 bits = 1 KB) │ |
| 10 | +//! │ ├── identity: words[0..8] dn_addr, container_count │ |
| 11 | +//! │ ├── nars: words[8..16] frequency, confidence │ |
| 12 | +//! │ ├── edges: words[16..48] adjacency bitfield │ |
| 13 | +//! │ ├── rung/rl: words[48..64] consciousness rung, RL │ |
| 14 | +//! │ ├── qualia: words[64..72] 8 affect channels │ |
| 15 | +//! │ └── repr: words[72..128] geometry-specific fields │ |
| 16 | +//! ├─────────────────────────────────────────────────────────────┤ |
| 17 | +//! │ content (8,192 bits = 1 KB) │ |
| 18 | +//! │ └── searchable fingerprint (Hamming / SIMD / XOR-bind) │ |
| 19 | +//! └─────────────────────────────────────────────────────────────┘ |
| 20 | +//! = 2 KB = 1 DN tree node = 1 Redis value = 1 Fingerprint |
| 21 | +//! ``` |
| 22 | +//! |
| 23 | +//! # Usage from Other Runtimes |
| 24 | +//! |
| 25 | +//! ```rust,no_run |
| 26 | +//! // In crewai-rust or n8n-rs (with `ladybug` feature): |
| 27 | +//! use ladybug_contract::schema; |
| 28 | +//! |
| 29 | +//! assert_eq!(schema::RECORD_BYTES, 2048); |
| 30 | +//! assert_eq!(schema::RECORD_CONTAINERS, 2); |
| 31 | +//! assert_eq!(schema::CONTAINER_BITS_EACH, 8192); |
| 32 | +//! ``` |
| 33 | +
|
| 34 | +use crate::container::{CONTAINER_BITS, CONTAINER_BYTES, CONTAINER_WORDS}; |
| 35 | + |
| 36 | +// ============================================================================= |
| 37 | +// RECORD SCHEMA CONSTANTS |
| 38 | +// ============================================================================= |
| 39 | + |
| 40 | +/// Total bits in a CogRecord: 2 × 8,192 = 16,384. |
| 41 | +pub const RECORD_BITS: usize = 2 * CONTAINER_BITS; |
| 42 | + |
| 43 | +/// Total bytes in a CogRecord: 2 × 1,024 = 2,048. |
| 44 | +pub const RECORD_BYTES: usize = 2 * CONTAINER_BYTES; |
| 45 | + |
| 46 | +/// Total u64 words in a CogRecord: 2 × 128 = 256. |
| 47 | +pub const RECORD_WORDS: usize = 2 * CONTAINER_WORDS; |
| 48 | + |
| 49 | +/// Number of containers in a CogRecord (always 2: meta + content). |
| 50 | +pub const RECORD_CONTAINERS: usize = 2; |
| 51 | + |
| 52 | +/// Bits per container: 8,192. |
| 53 | +pub const CONTAINER_BITS_EACH: usize = CONTAINER_BITS; |
| 54 | + |
| 55 | +/// Bytes per container: 1,024. |
| 56 | +pub const CONTAINER_BYTES_EACH: usize = CONTAINER_BYTES; |
| 57 | + |
| 58 | +/// Words per container: 128. |
| 59 | +pub const CONTAINER_WORDS_EACH: usize = CONTAINER_WORDS; |
| 60 | + |
| 61 | +// ============================================================================= |
| 62 | +// METADATA FIELD LAYOUT (word offsets within meta container) |
| 63 | +// ============================================================================= |
| 64 | + |
| 65 | +/// Word range: identity (dn_addr, container_count, geometry). |
| 66 | +pub const META_IDENTITY: (usize, usize) = (0, 8); |
| 67 | + |
| 68 | +/// Word range: NARS truth values (frequency, confidence, expectation). |
| 69 | +pub const META_NARS: (usize, usize) = (8, 16); |
| 70 | + |
| 71 | +/// Word range: edge adjacency bitfield (2048-bit neighborhood). |
| 72 | +pub const META_EDGES: (usize, usize) = (16, 48); |
| 73 | + |
| 74 | +/// Word range: consciousness rung + RL state. |
| 75 | +pub const META_RUNG_RL: (usize, usize) = (48, 64); |
| 76 | + |
| 77 | +/// Word range: qualia channels (8 affect dimensions × 8 bytes). |
| 78 | +pub const META_QUALIA: (usize, usize) = (64, 72); |
| 79 | + |
| 80 | +/// Word range: representation / geometry-specific fields. |
| 81 | +pub const META_REPR: (usize, usize) = (72, 128); |
| 82 | + |
| 83 | +// ============================================================================= |
| 84 | +// SCHEMA FIELD DESCRIPTORS |
| 85 | +// ============================================================================= |
| 86 | + |
| 87 | +/// A descriptor for a field in the CogRecord schema. |
| 88 | +#[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 89 | +pub struct FieldDescriptor { |
| 90 | + /// Human-readable field name. |
| 91 | + pub name: &'static str, |
| 92 | + /// Container index (0 = meta, 1 = content). |
| 93 | + pub container: u8, |
| 94 | + /// Start word offset within the container. |
| 95 | + pub word_start: usize, |
| 96 | + /// End word offset (exclusive). |
| 97 | + pub word_end: usize, |
| 98 | + /// Bit width of the field (word_count × 64). |
| 99 | + pub bits: usize, |
| 100 | +} |
| 101 | + |
| 102 | +impl FieldDescriptor { |
| 103 | + /// Number of u64 words this field occupies. |
| 104 | + pub const fn word_count(&self) -> usize { |
| 105 | + self.word_end - self.word_start |
| 106 | + } |
| 107 | + |
| 108 | + /// Byte offset from the start of the CogRecord. |
| 109 | + pub const fn byte_offset(&self) -> usize { |
| 110 | + (self.container as usize) * CONTAINER_BYTES + self.word_start * 8 |
| 111 | + } |
| 112 | + |
| 113 | + /// Byte length of this field. |
| 114 | + pub const fn byte_length(&self) -> usize { |
| 115 | + self.word_count() * 8 |
| 116 | + } |
| 117 | +} |
| 118 | + |
| 119 | +/// The complete CogRecord field layout. |
| 120 | +pub const FIELDS: &[FieldDescriptor] = &[ |
| 121 | + FieldDescriptor { |
| 122 | + name: "identity", |
| 123 | + container: 0, |
| 124 | + word_start: META_IDENTITY.0, |
| 125 | + word_end: META_IDENTITY.1, |
| 126 | + bits: (META_IDENTITY.1 - META_IDENTITY.0) * 64, |
| 127 | + }, |
| 128 | + FieldDescriptor { |
| 129 | + name: "nars", |
| 130 | + container: 0, |
| 131 | + word_start: META_NARS.0, |
| 132 | + word_end: META_NARS.1, |
| 133 | + bits: (META_NARS.1 - META_NARS.0) * 64, |
| 134 | + }, |
| 135 | + FieldDescriptor { |
| 136 | + name: "edges", |
| 137 | + container: 0, |
| 138 | + word_start: META_EDGES.0, |
| 139 | + word_end: META_EDGES.1, |
| 140 | + bits: (META_EDGES.1 - META_EDGES.0) * 64, |
| 141 | + }, |
| 142 | + FieldDescriptor { |
| 143 | + name: "rung_rl", |
| 144 | + container: 0, |
| 145 | + word_start: META_RUNG_RL.0, |
| 146 | + word_end: META_RUNG_RL.1, |
| 147 | + bits: (META_RUNG_RL.1 - META_RUNG_RL.0) * 64, |
| 148 | + }, |
| 149 | + FieldDescriptor { |
| 150 | + name: "qualia", |
| 151 | + container: 0, |
| 152 | + word_start: META_QUALIA.0, |
| 153 | + word_end: META_QUALIA.1, |
| 154 | + bits: (META_QUALIA.1 - META_QUALIA.0) * 64, |
| 155 | + }, |
| 156 | + FieldDescriptor { |
| 157 | + name: "repr", |
| 158 | + container: 0, |
| 159 | + word_start: META_REPR.0, |
| 160 | + word_end: META_REPR.1, |
| 161 | + bits: (META_REPR.1 - META_REPR.0) * 64, |
| 162 | + }, |
| 163 | + FieldDescriptor { |
| 164 | + name: "content", |
| 165 | + container: 1, |
| 166 | + word_start: 0, |
| 167 | + word_end: CONTAINER_WORDS, |
| 168 | + bits: CONTAINER_BITS, |
| 169 | + }, |
| 170 | +]; |
| 171 | + |
| 172 | +/// Look up a field descriptor by name. |
| 173 | +pub const fn field(name: &str) -> Option<&'static FieldDescriptor> { |
| 174 | + let mut i = 0; |
| 175 | + while i < FIELDS.len() { |
| 176 | + if const_str_eq(FIELDS[i].name, name) { |
| 177 | + return Some(&FIELDS[i]); |
| 178 | + } |
| 179 | + i += 1; |
| 180 | + } |
| 181 | + None |
| 182 | +} |
| 183 | + |
| 184 | +/// Const-compatible string equality. |
| 185 | +const fn const_str_eq(a: &str, b: &str) -> bool { |
| 186 | + let a = a.as_bytes(); |
| 187 | + let b = b.as_bytes(); |
| 188 | + if a.len() != b.len() { |
| 189 | + return false; |
| 190 | + } |
| 191 | + let mut i = 0; |
| 192 | + while i < a.len() { |
| 193 | + if a[i] != b[i] { |
| 194 | + return false; |
| 195 | + } |
| 196 | + i += 1; |
| 197 | + } |
| 198 | + true |
| 199 | +} |
| 200 | + |
| 201 | +// ============================================================================= |
| 202 | +// TESTS |
| 203 | +// ============================================================================= |
| 204 | + |
| 205 | +#[cfg(test)] |
| 206 | +mod tests { |
| 207 | + use super::*; |
| 208 | + |
| 209 | + #[test] |
| 210 | + fn test_record_constants() { |
| 211 | + assert_eq!(RECORD_BITS, 16_384); |
| 212 | + assert_eq!(RECORD_BYTES, 2048); |
| 213 | + assert_eq!(RECORD_WORDS, 256); |
| 214 | + assert_eq!(RECORD_CONTAINERS, 2); |
| 215 | + assert_eq!(CONTAINER_BITS_EACH, 8192); |
| 216 | + assert_eq!(CONTAINER_BYTES_EACH, 1024); |
| 217 | + assert_eq!(CONTAINER_WORDS_EACH, 128); |
| 218 | + } |
| 219 | + |
| 220 | + #[test] |
| 221 | + fn test_meta_fields_cover_all_words() { |
| 222 | + // Meta container is words 0..128. Verify fields are contiguous. |
| 223 | + assert_eq!(META_IDENTITY.0, 0); |
| 224 | + assert_eq!(META_IDENTITY.1, META_NARS.0); |
| 225 | + assert_eq!(META_NARS.1, META_EDGES.0); |
| 226 | + assert_eq!(META_EDGES.1, META_RUNG_RL.0); |
| 227 | + assert_eq!(META_RUNG_RL.1, META_QUALIA.0); |
| 228 | + assert_eq!(META_QUALIA.1, META_REPR.0); |
| 229 | + assert_eq!(META_REPR.1, CONTAINER_WORDS); |
| 230 | + } |
| 231 | + |
| 232 | + #[test] |
| 233 | + fn test_field_descriptors() { |
| 234 | + assert_eq!(FIELDS.len(), 7); // 6 meta fields + 1 content |
| 235 | + assert_eq!(FIELDS[0].name, "identity"); |
| 236 | + assert_eq!(FIELDS[6].name, "content"); |
| 237 | + assert_eq!(FIELDS[6].container, 1); |
| 238 | + assert_eq!(FIELDS[6].bits, 8192); |
| 239 | + } |
| 240 | + |
| 241 | + #[test] |
| 242 | + fn test_field_byte_offsets() { |
| 243 | + let identity = &FIELDS[0]; |
| 244 | + assert_eq!(identity.byte_offset(), 0); |
| 245 | + assert_eq!(identity.byte_length(), 64); // 8 words × 8 bytes |
| 246 | + |
| 247 | + let content = &FIELDS[6]; |
| 248 | + assert_eq!(content.byte_offset(), 1024); // starts at container 1 |
| 249 | + assert_eq!(content.byte_length(), 1024); // full container |
| 250 | + } |
| 251 | + |
| 252 | + #[test] |
| 253 | + fn test_field_lookup() { |
| 254 | + let nars = field("nars").unwrap(); |
| 255 | + assert_eq!(nars.word_start, 8); |
| 256 | + assert_eq!(nars.word_end, 16); |
| 257 | + |
| 258 | + let content = field("content").unwrap(); |
| 259 | + assert_eq!(content.container, 1); |
| 260 | + assert_eq!(content.bits, 8192); |
| 261 | + |
| 262 | + assert!(field("nonexistent").is_none()); |
| 263 | + } |
| 264 | + |
| 265 | + #[test] |
| 266 | + fn test_total_field_bits() { |
| 267 | + let total: usize = FIELDS.iter().map(|f| f.bits).sum(); |
| 268 | + assert_eq!(total, RECORD_BITS); |
| 269 | + } |
| 270 | +} |
0 commit comments