1use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
67use memmap2::{MmapMut, MmapOptions};
68use parking_lot::{Mutex, MutexGuard};
69use std::fs::OpenOptions;
70use std::io::Cursor;
71use std::path::{Path, PathBuf};
72use std::sync::atomic::{AtomicU64, Ordering};
73use std::sync::Arc;
74
75pub const PAGE_SIZE: usize = 16 * 1024;
76pub const HEADER_SIZE: usize = 64;
77pub const MAGIC: u64 = 0x4D31_4353_5031_4E45;
78pub const N_LAYERS: usize = 12;
79pub const ATTENTION_LAYERS: usize = 7;
80
81pub const SUPERBLOCK_VERSION: u32 = 2;
91pub const SUPERBLOCK_BYTES: usize = 56;
97pub const SEQ_SLOT: usize = 56;
101const _: () = assert!(
102 SEQ_SLOT.is_multiple_of(8),
103 "the counter slot must stay 8-byte aligned"
104);
105const _: () = assert!(
106 SEQ_SLOT >= SUPERBLOCK_BYTES,
107 "the payload must end before the counter"
108);
109pub const SNAPSHOT_PREFIX_BYTES: usize = 128;
113pub const SNAPSHOT_MAX_RETRIES: u32 = 65_536;
123pub const SNAPSHOT_MAX_WAIT_MS: u64 = 50;
130pub const PUBLISH_LOCK_FILE_NAME: &str = "spine.publish";
132pub const WRITER_LOCK_FILE_NAME: &str = "spine.lock";
134
135const _: () = assert!(SEQ_SLOT + 8 <= PAGE_SIZE);
136const _: () = assert!(SNAPSHOT_PREFIX_BYTES >= StreamHeader::HEADER_BYTES);
137const _: () = assert!(SNAPSHOT_PREFIX_BYTES >= HebbianHeader::HEADER_BYTES);
138const _: () = assert!(SNAPSHOT_PREFIX_BYTES >= AttentionHeader::HEADER_BYTES);
139const _: () = assert!(SNAPSHOT_PREFIX_BYTES >= EmotionHeader::HEADER_BYTES);
140const _: () = assert!(SNAPSHOT_PREFIX_BYTES >= ResonanceHeader::HEADER_BYTES);
141const _: () = assert!(SNAPSHOT_PREFIX_BYTES >= GraphHeader::HEADER_BYTES);
142const _: () = assert!(SNAPSHOT_PREFIX_BYTES >= CacheHeader::HEADER_BYTES);
143const _: () = assert!(SNAPSHOT_PREFIX_BYTES >= ArchetypeHeader::HEADER_BYTES);
144const _: () = assert!(SNAPSHOT_PREFIX_BYTES >= MirrorHeader::HEADER_BYTES);
145const _: () = assert!(SNAPSHOT_PREFIX_BYTES >= MerkleHeader::HEADER_BYTES);
146const _: () = assert!(SNAPSHOT_PREFIX_BYTES >= MetaHeader::HEADER_BYTES);
147const _: () = assert!(SNAPSHOT_PREFIX_BYTES <= PAGE_SIZE);
148const _: () = assert!(SUPERBLOCK_BYTES + 8 <= PAGE_SIZE);
149const _: () = assert!(HEADER_SIZE == 64);
150const _: () = assert!(11 * PAGE_SIZE + SNAPSHOT_PREFIX_BYTES <= 12 * PAGE_SIZE);
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153#[repr(u32)]
154pub enum Slot {
155 Superblock = 0,
156 Stream = 1,
157 Hebbian = 2,
158 Attention = 3,
159 Emotion = 4,
160 Resonance = 5,
161 Graph = 6,
162 Cache = 7,
163 Archetype = 8,
164 Mirror = 9,
165 Merkle = 10,
166 Meta = 11,
167 WalStart = 12,
168}
169impl Slot {
170 pub fn offset(&self) -> usize {
171 *self as usize * PAGE_SIZE
172 }
173 pub fn from_u32(v: u32) -> Option<Slot> {
174 Some(match v {
175 0 => Slot::Superblock,
176 1 => Slot::Stream,
177 2 => Slot::Hebbian,
178 3 => Slot::Attention,
179 4 => Slot::Emotion,
180 5 => Slot::Resonance,
181 6 => Slot::Graph,
182 7 => Slot::Cache,
183 8 => Slot::Archetype,
184 9 => Slot::Mirror,
185 10 => Slot::Merkle,
186 11 => Slot::Meta,
187 12 => Slot::WalStart,
188 _ => return None,
189 })
190 }
191}
192
193#[derive(Debug, Clone, Copy)]
196#[repr(C, align(64))]
197pub struct StreamHeader {
198 pub cycle: u64,
199 pub surprise_level: f32,
200 pub curiosity_level: f32,
201 pub predicted_query_hash: u64,
202 pub predicted_confidence: f32,
203 pub last_query_hash: u64,
204 pub last_query_ms: u64,
205 pub hebbian_total_energy: f64,
206 pub updated_ms: u64,
207 pub _pad: [u8; PAGE_SIZE - 60],
208}
209impl StreamHeader {
210 pub const HEADER_BYTES: usize = 60;
211 pub fn to_bytes(&self) -> [u8; PAGE_SIZE] {
212 let mut buf = [0u8; PAGE_SIZE];
213 let mut c = Cursor::new(&mut buf[..Self::HEADER_BYTES]);
214 c.write_u64::<LittleEndian>(self.cycle).unwrap();
215 c.write_f32::<LittleEndian>(self.surprise_level).unwrap();
216 c.write_f32::<LittleEndian>(self.curiosity_level).unwrap();
217 c.write_u64::<LittleEndian>(self.predicted_query_hash)
218 .unwrap();
219 c.write_f32::<LittleEndian>(self.predicted_confidence)
220 .unwrap();
221 c.write_u64::<LittleEndian>(self.last_query_hash).unwrap();
222 c.write_u64::<LittleEndian>(self.last_query_ms).unwrap();
223 c.write_f64::<LittleEndian>(self.hebbian_total_energy)
224 .unwrap();
225 c.write_u64::<LittleEndian>(self.updated_ms).unwrap();
226 buf
227 }
228 pub fn from_bytes(buf: &[u8]) -> Self {
229 let mut c = Cursor::new(buf);
230 Self {
231 cycle: c.read_u64::<LittleEndian>().unwrap_or(0),
232 surprise_level: c.read_f32::<LittleEndian>().unwrap_or(0.0),
233 curiosity_level: c.read_f32::<LittleEndian>().unwrap_or(0.0),
234 predicted_query_hash: c.read_u64::<LittleEndian>().unwrap_or(0),
235 predicted_confidence: c.read_f32::<LittleEndian>().unwrap_or(0.0),
236 last_query_hash: c.read_u64::<LittleEndian>().unwrap_or(0),
237 last_query_ms: c.read_u64::<LittleEndian>().unwrap_or(0),
238 hebbian_total_energy: c.read_f64::<LittleEndian>().unwrap_or(0.0),
239 updated_ms: c.read_u64::<LittleEndian>().unwrap_or(0),
240 _pad: [0; PAGE_SIZE - Self::HEADER_BYTES],
241 }
242 }
243}
244impl Default for StreamHeader {
245 fn default() -> Self {
246 Self {
247 cycle: 0,
248 surprise_level: 0.0,
249 curiosity_level: 0.0,
250 predicted_query_hash: 0,
251 predicted_confidence: 0.0,
252 last_query_hash: 0,
253 last_query_ms: 0,
254 hebbian_total_energy: 0.0,
255 updated_ms: 0,
256 _pad: [0; PAGE_SIZE - Self::HEADER_BYTES],
257 }
258 }
259}
260
261#[derive(Debug, Clone, Copy)]
262#[repr(C, align(64))]
263pub struct HebbianHeader {
264 pub block_count: u32,
265 pub activation_count: u32,
266 pub coactivation_count: u32,
267 pub fingerprint_count: u32,
268 pub delta_tail_offset: u64,
269 pub decay_event_count: u64,
278 pub last_activation_ms: u64,
279 pub _pad: [u8; PAGE_SIZE - 40],
280}
281impl HebbianHeader {
282 pub const HEADER_BYTES: usize = 40;
283 pub fn to_bytes(&self) -> [u8; PAGE_SIZE] {
284 let mut buf = [0u8; PAGE_SIZE];
285 let mut c = Cursor::new(&mut buf[..Self::HEADER_BYTES]);
286 c.write_u32::<LittleEndian>(self.block_count).unwrap();
287 c.write_u32::<LittleEndian>(self.activation_count).unwrap();
288 c.write_u32::<LittleEndian>(self.coactivation_count)
289 .unwrap();
290 c.write_u32::<LittleEndian>(self.fingerprint_count).unwrap();
291 c.write_u64::<LittleEndian>(self.delta_tail_offset).unwrap();
292 c.write_u64::<LittleEndian>(self.decay_event_count).unwrap();
293 c.write_u64::<LittleEndian>(self.last_activation_ms)
294 .unwrap();
295 buf
296 }
297 pub fn from_bytes(buf: &[u8]) -> Self {
298 let mut c = Cursor::new(buf);
299 Self {
300 block_count: c.read_u32::<LittleEndian>().unwrap_or(0),
301 activation_count: c.read_u32::<LittleEndian>().unwrap_or(0),
302 coactivation_count: c.read_u32::<LittleEndian>().unwrap_or(0),
303 fingerprint_count: c.read_u32::<LittleEndian>().unwrap_or(0),
304 delta_tail_offset: c.read_u64::<LittleEndian>().unwrap_or(0),
305 decay_event_count: c.read_u64::<LittleEndian>().unwrap_or(0),
306 last_activation_ms: c.read_u64::<LittleEndian>().unwrap_or(0),
307 _pad: [0; PAGE_SIZE - Self::HEADER_BYTES],
308 }
309 }
310}
311impl Default for HebbianHeader {
312 fn default() -> Self {
313 Self {
314 block_count: 0,
315 activation_count: 0,
316 coactivation_count: 0,
317 fingerprint_count: 0,
318 delta_tail_offset: 0,
319 decay_event_count: 0,
320 last_activation_ms: 0,
321 _pad: [0; PAGE_SIZE - Self::HEADER_BYTES],
322 }
323 }
324}
325
326#[derive(Debug, Clone, Copy)]
327#[repr(C, align(64))]
328pub struct AttentionHeader {
329 pub learned_weights: [f32; ATTENTION_LAYERS],
330 pub last_recall_ms: u64,
331 pub total_recalls: u32,
332 pub quality_signal: f32,
333 pub _pad: [u8; PAGE_SIZE - (4 * ATTENTION_LAYERS + 16)],
334}
335impl AttentionHeader {
336 pub const HEADER_BYTES: usize = 4 * ATTENTION_LAYERS + 16;
337 pub fn to_bytes(&self) -> [u8; PAGE_SIZE] {
338 let mut buf = [0u8; PAGE_SIZE];
339 let mut c = Cursor::new(&mut buf[..Self::HEADER_BYTES]);
340 for w in &self.learned_weights {
341 c.write_f32::<LittleEndian>(*w).unwrap();
342 }
343 c.write_u64::<LittleEndian>(self.last_recall_ms).unwrap();
344 c.write_u32::<LittleEndian>(self.total_recalls).unwrap();
345 c.write_f32::<LittleEndian>(self.quality_signal).unwrap();
346 buf
347 }
348 pub fn from_bytes(buf: &[u8]) -> Self {
349 let mut c = Cursor::new(buf);
350 let mut lw = [0.0f32; ATTENTION_LAYERS];
351 for w in lw.iter_mut() {
352 *w = c.read_f32::<LittleEndian>().unwrap_or(0.0);
353 }
354 Self {
355 learned_weights: lw,
356 last_recall_ms: c.read_u64::<LittleEndian>().unwrap_or(0),
357 total_recalls: c.read_u32::<LittleEndian>().unwrap_or(0),
358 quality_signal: c.read_f32::<LittleEndian>().unwrap_or(0.0),
359 _pad: [0; PAGE_SIZE - Self::HEADER_BYTES],
360 }
361 }
362}
363impl Default for AttentionHeader {
364 fn default() -> Self {
365 Self {
366 learned_weights: [1.0 / ATTENTION_LAYERS as f32; ATTENTION_LAYERS],
367 last_recall_ms: 0,
368 total_recalls: 0,
369 quality_signal: 0.0,
370 _pad: [0; PAGE_SIZE - Self::HEADER_BYTES],
371 }
372 }
373}
374
375#[derive(Debug, Clone, Copy)]
376#[repr(C, align(64))]
377pub struct EmotionHeader {
378 pub current: [f32; 21],
379 pub head: u32,
380 pub count: u32,
381 pub last_update: u64,
382 pub total_updates: u64,
383 pub _pad: [u8; PAGE_SIZE - 108],
384}
385impl EmotionHeader {
386 pub const HEADER_BYTES: usize = 4 * 21 + 24;
387 pub fn to_bytes(&self) -> [u8; PAGE_SIZE] {
388 let mut buf = [0u8; PAGE_SIZE];
389 let mut c = Cursor::new(&mut buf[..Self::HEADER_BYTES]);
390 for v in &self.current {
391 c.write_f32::<LittleEndian>(*v).unwrap();
392 }
393 c.write_u32::<LittleEndian>(self.head).unwrap();
394 c.write_u32::<LittleEndian>(self.count).unwrap();
395 c.write_u64::<LittleEndian>(self.last_update).unwrap();
396 c.write_u64::<LittleEndian>(self.total_updates).unwrap();
397 buf
398 }
399 pub fn from_bytes(buf: &[u8]) -> Self {
400 let mut c = Cursor::new(buf);
401 let mut cur = [0.0f32; 21];
402 for v in cur.iter_mut() {
403 *v = c.read_f32::<LittleEndian>().unwrap_or(0.0);
404 }
405 Self {
406 current: cur,
407 head: c.read_u32::<LittleEndian>().unwrap_or(0),
408 count: c.read_u32::<LittleEndian>().unwrap_or(0),
409 last_update: c.read_u64::<LittleEndian>().unwrap_or(0),
410 total_updates: c.read_u64::<LittleEndian>().unwrap_or(0),
411 _pad: [0; PAGE_SIZE - Self::HEADER_BYTES],
412 }
413 }
414}
415impl Default for EmotionHeader {
416 fn default() -> Self {
417 Self {
418 current: [0.0; 21],
419 head: 0,
420 count: 0,
421 last_update: 0,
422 total_updates: 0,
423 _pad: [0; PAGE_SIZE - Self::HEADER_BYTES],
424 }
425 }
426}
427
428#[derive(Debug, Clone, Copy)]
429#[repr(C, align(64))]
430pub struct ResonanceHeader {
431 pub instance_id: u64,
432 pub field_cells: u32,
433 pub outgoing_count: u32,
434 pub incoming_count: u32,
435 pub pulses_emitted: u64,
436 pub pulses_received: u64,
437 pub last_decay_ms: u64,
438 pub _pad: [u8; PAGE_SIZE - 44],
439}
440impl ResonanceHeader {
441 pub const HEADER_BYTES: usize = 44;
442 pub fn to_bytes(&self) -> [u8; PAGE_SIZE] {
443 let mut buf = [0u8; PAGE_SIZE];
444 let mut c = Cursor::new(&mut buf[..Self::HEADER_BYTES]);
445 c.write_u64::<LittleEndian>(self.instance_id).unwrap();
446 c.write_u32::<LittleEndian>(self.field_cells).unwrap();
447 c.write_u32::<LittleEndian>(self.outgoing_count).unwrap();
448 c.write_u32::<LittleEndian>(self.incoming_count).unwrap();
449 c.write_u64::<LittleEndian>(self.pulses_emitted).unwrap();
450 c.write_u64::<LittleEndian>(self.pulses_received).unwrap();
451 c.write_u64::<LittleEndian>(self.last_decay_ms).unwrap();
452 buf
453 }
454 pub fn from_bytes(buf: &[u8]) -> Self {
455 let mut c = Cursor::new(buf);
456 Self {
457 instance_id: c.read_u64::<LittleEndian>().unwrap_or(0),
458 field_cells: c.read_u32::<LittleEndian>().unwrap_or(0),
459 outgoing_count: c.read_u32::<LittleEndian>().unwrap_or(0),
460 incoming_count: c.read_u32::<LittleEndian>().unwrap_or(0),
461 pulses_emitted: c.read_u64::<LittleEndian>().unwrap_or(0),
462 pulses_received: c.read_u64::<LittleEndian>().unwrap_or(0),
463 last_decay_ms: c.read_u64::<LittleEndian>().unwrap_or(0),
464 _pad: [0; PAGE_SIZE - Self::HEADER_BYTES],
465 }
466 }
467}
468impl Default for ResonanceHeader {
469 fn default() -> Self {
470 Self {
471 instance_id: 0,
472 field_cells: 0,
473 outgoing_count: 0,
474 incoming_count: 0,
475 pulses_emitted: 0,
476 pulses_received: 0,
477 last_decay_ms: 0,
478 _pad: [0; PAGE_SIZE - Self::HEADER_BYTES],
479 }
480 }
481}
482
483#[derive(Debug, Clone, Copy)]
484#[repr(C, align(64))]
485pub struct GraphHeader {
486 pub node_count: u32,
487 pub edge_count: u32,
488 pub pattern_count: u32,
489 pub current_session_id: u32,
490 pub next_pattern_id: u32,
491 pub last_node_ts: u64,
492 pub _pad: [u8; PAGE_SIZE - 28],
493}
494impl GraphHeader {
495 pub const HEADER_BYTES: usize = 28;
496 pub fn to_bytes(&self) -> [u8; PAGE_SIZE] {
497 let mut buf = [0u8; PAGE_SIZE];
498 let mut c = Cursor::new(&mut buf[..Self::HEADER_BYTES]);
499 c.write_u32::<LittleEndian>(self.node_count).unwrap();
500 c.write_u32::<LittleEndian>(self.edge_count).unwrap();
501 c.write_u32::<LittleEndian>(self.pattern_count).unwrap();
502 c.write_u32::<LittleEndian>(self.current_session_id)
503 .unwrap();
504 c.write_u32::<LittleEndian>(self.next_pattern_id).unwrap();
505 c.write_u64::<LittleEndian>(self.last_node_ts).unwrap();
506 buf
507 }
508 pub fn from_bytes(buf: &[u8]) -> Self {
509 let mut c = Cursor::new(buf);
510 Self {
511 node_count: c.read_u32::<LittleEndian>().unwrap_or(0),
512 edge_count: c.read_u32::<LittleEndian>().unwrap_or(0),
513 pattern_count: c.read_u32::<LittleEndian>().unwrap_or(0),
514 current_session_id: c.read_u32::<LittleEndian>().unwrap_or(0),
515 next_pattern_id: c.read_u32::<LittleEndian>().unwrap_or(1),
516 last_node_ts: c.read_u64::<LittleEndian>().unwrap_or(0),
517 _pad: [0; PAGE_SIZE - Self::HEADER_BYTES],
518 }
519 }
520}
521impl Default for GraphHeader {
522 fn default() -> Self {
523 Self {
524 node_count: 0,
525 edge_count: 0,
526 pattern_count: 0,
527 current_session_id: 0,
528 next_pattern_id: 1,
529 last_node_ts: 0,
530 _pad: [0; PAGE_SIZE - Self::HEADER_BYTES],
531 }
532 }
533}
534
535#[derive(Debug, Clone, Copy)]
536#[repr(C, align(64))]
537pub struct CacheHeader {
538 pub total_predictions: u32,
539 pub total_hits: u32,
540 pub total_misses: u32,
541 pub total_partial_hits: u32,
542 pub current_predictions: u32,
543 pub avg_confidence: f32,
544 pub last_predict_ms: u64,
545 pub _pad: [u8; PAGE_SIZE - 32],
546}
547impl CacheHeader {
548 pub const HEADER_BYTES: usize = 32;
549 pub fn to_bytes(&self) -> [u8; PAGE_SIZE] {
550 let mut buf = [0u8; PAGE_SIZE];
551 let mut c = Cursor::new(&mut buf[..Self::HEADER_BYTES]);
552 c.write_u32::<LittleEndian>(self.total_predictions).unwrap();
553 c.write_u32::<LittleEndian>(self.total_hits).unwrap();
554 c.write_u32::<LittleEndian>(self.total_misses).unwrap();
555 c.write_u32::<LittleEndian>(self.total_partial_hits)
556 .unwrap();
557 c.write_u32::<LittleEndian>(self.current_predictions)
558 .unwrap();
559 c.write_f32::<LittleEndian>(self.avg_confidence).unwrap();
560 c.write_u64::<LittleEndian>(self.last_predict_ms).unwrap();
561 buf
562 }
563 pub fn from_bytes(buf: &[u8]) -> Self {
564 let mut c = Cursor::new(buf);
565 Self {
566 total_predictions: c.read_u32::<LittleEndian>().unwrap_or(0),
567 total_hits: c.read_u32::<LittleEndian>().unwrap_or(0),
568 total_misses: c.read_u32::<LittleEndian>().unwrap_or(0),
569 total_partial_hits: c.read_u32::<LittleEndian>().unwrap_or(0),
570 current_predictions: c.read_u32::<LittleEndian>().unwrap_or(0),
571 avg_confidence: c.read_f32::<LittleEndian>().unwrap_or(0.0),
572 last_predict_ms: c.read_u64::<LittleEndian>().unwrap_or(0),
573 _pad: [0; PAGE_SIZE - Self::HEADER_BYTES],
574 }
575 }
576 pub fn hit_rate(&self) -> f32 {
577 let total = self.total_hits + self.total_misses + self.total_partial_hits;
578 if total == 0 {
579 return 0.0;
580 }
581 self.total_hits as f32 / total as f32
582 }
583}
584impl Default for CacheHeader {
585 fn default() -> Self {
586 Self {
587 total_predictions: 0,
588 total_hits: 0,
589 total_misses: 0,
590 total_partial_hits: 0,
591 current_predictions: 0,
592 avg_confidence: 0.0,
593 last_predict_ms: 0,
594 _pad: [0; PAGE_SIZE - Self::HEADER_BYTES],
595 }
596 }
597}
598
599#[derive(Debug, Clone, Copy)]
600#[repr(C, align(64))]
601pub struct ArchetypeHeader {
602 pub active_count: u32,
603 pub next_id: u32,
604 pub emergence_events: u64,
605 pub decay_events: u64,
609 pub last_emergence_ms: u64,
610 pub _pad: [u8; PAGE_SIZE - 32],
611}
612impl ArchetypeHeader {
613 pub const HEADER_BYTES: usize = 32;
614 pub fn to_bytes(&self) -> [u8; PAGE_SIZE] {
615 let mut buf = [0u8; PAGE_SIZE];
616 let mut c = Cursor::new(&mut buf[..Self::HEADER_BYTES]);
617 c.write_u32::<LittleEndian>(self.active_count).unwrap();
618 c.write_u32::<LittleEndian>(self.next_id).unwrap();
619 c.write_u64::<LittleEndian>(self.emergence_events).unwrap();
620 c.write_u64::<LittleEndian>(self.decay_events).unwrap();
621 c.write_u64::<LittleEndian>(self.last_emergence_ms).unwrap();
622 buf
623 }
624 pub fn from_bytes(buf: &[u8]) -> Self {
625 let mut c = Cursor::new(buf);
626 Self {
627 active_count: c.read_u32::<LittleEndian>().unwrap_or(0),
628 next_id: c.read_u32::<LittleEndian>().unwrap_or(1),
629 emergence_events: c.read_u64::<LittleEndian>().unwrap_or(0),
630 decay_events: c.read_u64::<LittleEndian>().unwrap_or(0),
631 last_emergence_ms: c.read_u64::<LittleEndian>().unwrap_or(0),
632 _pad: [0; PAGE_SIZE - Self::HEADER_BYTES],
633 }
634 }
635}
636impl Default for ArchetypeHeader {
637 fn default() -> Self {
638 Self {
639 active_count: 0,
640 next_id: 1,
641 emergence_events: 0,
642 decay_events: 0,
643 last_emergence_ms: 0,
644 _pad: [0; PAGE_SIZE - Self::HEADER_BYTES],
645 }
646 }
647}
648
649#[derive(Debug, Clone, Copy)]
650#[repr(C, align(64))]
651pub struct MirrorHeader {
652 pub echo_count: u32,
653 pub block_resonance_count: u32,
654 pub strongest_similarity: f32,
655 pub total_echoes: u64,
656 pub last_mirror_ms: u64,
657 pub _pad: [u8; PAGE_SIZE - 28],
658}
659impl MirrorHeader {
660 pub const HEADER_BYTES: usize = 28;
661 pub fn to_bytes(&self) -> [u8; PAGE_SIZE] {
662 let mut buf = [0u8; PAGE_SIZE];
663 let mut c = Cursor::new(&mut buf[..Self::HEADER_BYTES]);
664 c.write_u32::<LittleEndian>(self.echo_count).unwrap();
665 c.write_u32::<LittleEndian>(self.block_resonance_count)
666 .unwrap();
667 c.write_f32::<LittleEndian>(self.strongest_similarity)
668 .unwrap();
669 c.write_u64::<LittleEndian>(self.total_echoes).unwrap();
670 c.write_u64::<LittleEndian>(self.last_mirror_ms).unwrap();
671 buf
672 }
673 pub fn from_bytes(buf: &[u8]) -> Self {
674 let mut c = Cursor::new(buf);
675 Self {
676 echo_count: c.read_u32::<LittleEndian>().unwrap_or(0),
677 block_resonance_count: c.read_u32::<LittleEndian>().unwrap_or(0),
678 strongest_similarity: c.read_f32::<LittleEndian>().unwrap_or(0.0),
679 total_echoes: c.read_u64::<LittleEndian>().unwrap_or(0),
680 last_mirror_ms: c.read_u64::<LittleEndian>().unwrap_or(0),
681 _pad: [0; PAGE_SIZE - Self::HEADER_BYTES],
682 }
683 }
684}
685impl Default for MirrorHeader {
686 fn default() -> Self {
687 Self {
688 echo_count: 0,
689 block_resonance_count: 0,
690 strongest_similarity: 0.0,
691 total_echoes: 0,
692 last_mirror_ms: 0,
693 _pad: [0; PAGE_SIZE - Self::HEADER_BYTES],
694 }
695 }
696}
697
698#[derive(Debug, Clone, Copy)]
699#[repr(C, align(64))]
700pub struct MerkleHeader {
701 pub root_hash: u64,
702 pub chain_height: u64,
703 pub block_count: u32,
704 pub evidence_audit_entries: u32,
705 pub last_anchor_ms: u64,
706 pub _pad: [u8; PAGE_SIZE - 32],
707}
708impl MerkleHeader {
709 pub const HEADER_BYTES: usize = 32;
710 pub fn to_bytes(&self) -> [u8; PAGE_SIZE] {
711 let mut buf = [0u8; PAGE_SIZE];
712 let mut c = Cursor::new(&mut buf[..Self::HEADER_BYTES]);
713 c.write_u64::<LittleEndian>(self.root_hash).unwrap();
714 c.write_u64::<LittleEndian>(self.chain_height).unwrap();
715 c.write_u32::<LittleEndian>(self.block_count).unwrap();
716 c.write_u32::<LittleEndian>(self.evidence_audit_entries)
717 .unwrap();
718 c.write_u64::<LittleEndian>(self.last_anchor_ms).unwrap();
719 buf
720 }
721 pub fn from_bytes(buf: &[u8]) -> Self {
722 let mut c = Cursor::new(buf);
723 Self {
724 root_hash: c.read_u64::<LittleEndian>().unwrap_or(0),
725 chain_height: c.read_u64::<LittleEndian>().unwrap_or(0),
726 block_count: c.read_u32::<LittleEndian>().unwrap_or(0),
727 evidence_audit_entries: c.read_u32::<LittleEndian>().unwrap_or(0),
728 last_anchor_ms: c.read_u64::<LittleEndian>().unwrap_or(0),
729 _pad: [0; PAGE_SIZE - Self::HEADER_BYTES],
730 }
731 }
732}
733impl Default for MerkleHeader {
734 fn default() -> Self {
735 Self {
736 root_hash: 0,
737 chain_height: 0,
738 block_count: 0,
739 evidence_audit_entries: 0,
740 last_anchor_ms: 0,
741 _pad: [0; PAGE_SIZE - Self::HEADER_BYTES],
742 }
743 }
744}
745
746#[derive(Debug, Clone, Copy)]
747#[repr(C, align(64))]
748pub struct MetaHeader {
749 pub block_count: u32,
750 pub append_log_len: u32,
751 pub dream_cycle_count: u32,
752 pub reps_served_count: u32,
753 pub last_dream_ms: u64,
754 pub promotion_count: u64,
755 pub _pad: [u8; PAGE_SIZE - 32],
756}
757impl MetaHeader {
758 pub const HEADER_BYTES: usize = 32;
759 pub fn to_bytes(&self) -> [u8; PAGE_SIZE] {
760 let mut buf = [0u8; PAGE_SIZE];
761 let mut c = Cursor::new(&mut buf[..Self::HEADER_BYTES]);
762 c.write_u32::<LittleEndian>(self.block_count).unwrap();
763 c.write_u32::<LittleEndian>(self.append_log_len).unwrap();
764 c.write_u32::<LittleEndian>(self.dream_cycle_count).unwrap();
765 c.write_u32::<LittleEndian>(self.reps_served_count).unwrap();
766 c.write_u64::<LittleEndian>(self.last_dream_ms).unwrap();
767 c.write_u64::<LittleEndian>(self.promotion_count).unwrap();
768 buf
769 }
770 pub fn from_bytes(buf: &[u8]) -> Self {
771 let mut c = Cursor::new(buf);
772 Self {
773 block_count: c.read_u32::<LittleEndian>().unwrap_or(0),
774 append_log_len: c.read_u32::<LittleEndian>().unwrap_or(0),
775 dream_cycle_count: c.read_u32::<LittleEndian>().unwrap_or(0),
776 reps_served_count: c.read_u32::<LittleEndian>().unwrap_or(0),
777 last_dream_ms: c.read_u64::<LittleEndian>().unwrap_or(0),
778 promotion_count: c.read_u64::<LittleEndian>().unwrap_or(0),
779 _pad: [0; PAGE_SIZE - Self::HEADER_BYTES],
780 }
781 }
782}
783impl Default for MetaHeader {
784 fn default() -> Self {
785 Self {
786 block_count: 0,
787 append_log_len: 0,
788 dream_cycle_count: 0,
789 reps_served_count: 0,
790 last_dream_ms: 0,
791 promotion_count: 0,
792 _pad: [0; PAGE_SIZE - Self::HEADER_BYTES],
793 }
794 }
795}
796
797#[derive(Debug, Clone, Copy)]
804pub struct Superblock {
805 pub magic: u64,
806 pub version: u32,
807 pub page_size: u32,
808 pub wal_start_page: u32,
809 pub wal_head_page: u32,
810 pub tick_seq: u64,
811 pub epoch_ms: i64,
812}
813impl Default for Superblock {
814 fn default() -> Self {
815 Self::ZEROED
816 }
817}
818impl Superblock {
819 const ZEROED: Self = Self {
820 magic: 0,
821 version: SUPERBLOCK_VERSION,
822 page_size: 0,
823 wal_start_page: 0,
824 wal_head_page: 0,
825 tick_seq: 0,
826 epoch_ms: 0,
827 };
828 pub fn new() -> Self {
829 Self {
830 magic: MAGIC,
831 version: SUPERBLOCK_VERSION,
832 page_size: PAGE_SIZE as u32,
833 wal_start_page: Slot::WalStart as u32,
834 wal_head_page: Slot::WalStart as u32,
835 tick_seq: 0,
836 epoch_ms: now_ms(),
837 }
838 }
839 pub fn to_payload(&self) -> [u8; SUPERBLOCK_BYTES] {
843 let mut buf = [0u8; SUPERBLOCK_BYTES];
844 let mut c = Cursor::new(&mut buf[..]);
845 c.write_u64::<LittleEndian>(self.magic).unwrap();
846 c.write_u32::<LittleEndian>(self.version).unwrap();
847 c.write_u32::<LittleEndian>(self.page_size).unwrap();
848 c.write_u32::<LittleEndian>(self.wal_start_page).unwrap();
849 c.write_u32::<LittleEndian>(self.wal_head_page).unwrap();
850 c.write_u64::<LittleEndian>(self.tick_seq).unwrap();
851 c.write_i64::<LittleEndian>(self.epoch_ms).unwrap();
852 buf
853 }
854 pub fn from_payload(buf: &[u8]) -> Option<Self> {
858 let mut c = Cursor::new(buf);
859 let magic = c.read_u64::<LittleEndian>().ok()?;
860 if magic != MAGIC {
861 return None;
862 }
863 let version = c.read_u32::<LittleEndian>().ok()?;
864 if version != SUPERBLOCK_VERSION {
865 return None;
866 }
867 Some(Self {
868 magic,
869 version,
870 page_size: c.read_u32::<LittleEndian>().ok()?,
871 wal_start_page: c.read_u32::<LittleEndian>().ok()?,
872 wal_head_page: c.read_u32::<LittleEndian>().ok()?,
873 tick_seq: c.read_u64::<LittleEndian>().ok()?,
874 epoch_ms: c.read_i64::<LittleEndian>().ok()?,
875 })
876 }
877 fn from_v1_payload(buf: &[u8]) -> Option<Self> {
882 let mut c = Cursor::new(buf);
883 let magic = c.read_u64::<LittleEndian>().ok()?;
884 if magic != MAGIC {
885 return None;
886 }
887 let version = c.read_u32::<LittleEndian>().ok()?;
888 if version != 1 {
889 return None;
890 }
891 Some(Self {
892 magic,
893 version,
894 page_size: c.read_u32::<LittleEndian>().ok()?,
895 wal_start_page: c.read_u32::<LittleEndian>().ok()?,
896 wal_head_page: c.read_u32::<LittleEndian>().ok()?,
897 tick_seq: 0,
898 epoch_ms: c.read_i64::<LittleEndian>().ok()?,
899 })
900 }
901}
902
903fn now_ms() -> i64 {
904 std::time::SystemTime::now()
905 .duration_since(std::time::UNIX_EPOCH)
906 .map(|d| d.as_millis() as i64)
907 .unwrap_or(0)
908}
909
910#[derive(Debug, Clone, Copy)]
914pub struct PublishSnapshot {
915 pub stream: StreamHeader,
916 pub tick_seq: u64,
920 pub epoch_ms: i64,
922 pub sequence: u64,
924}
925
926#[derive(Debug, Clone, Copy)]
930pub struct StoreSnapshot {
931 pub stream: StreamHeader,
932 pub hebbian: HebbianHeader,
933 pub attention: AttentionHeader,
934 pub emotion: EmotionHeader,
935 pub resonance: ResonanceHeader,
936 pub graph: GraphHeader,
937 pub cache: CacheHeader,
938 pub archetype: ArchetypeHeader,
939 pub mirror: MirrorHeader,
940 pub merkle: MerkleHeader,
941 pub meta: MetaHeader,
942 pub tick_seq: u64,
943 pub epoch_ms: i64,
944 pub sequence: u64,
945}
946
947struct SnapshotBytes<const N: usize> {
948 headers: [[u8; SNAPSHOT_PREFIX_BYTES]; N],
949 payload: [u8; SUPERBLOCK_BYTES],
950 sequence: u64,
951}
952
953#[must_use = "hold the batch while updating headers, then commit or drop it"]
961pub struct Publish<'a> {
962 _advisory: PublishGuard<'a>,
964 mmap: MutexGuard<'a, MmapMut>,
965 window: Option<u64>,
966}
967
968impl Drop for Publish<'_> {
969 fn drop(&mut self) {
970 if let Some(n) = self.window.take() {
971 seq_end(&mut self.mmap, n);
972 }
973 }
974}
975
976impl Publish<'_> {
977 fn begin_write(&mut self) -> u64 {
978 if let Some(n) = self.window {
979 return n;
980 }
981 let n = seq_begin(&mut self.mmap);
982 self.window = Some(n);
983 n
984 }
985
986 fn read_prefix(&self, slot: Slot) -> [u8; SNAPSHOT_PREFIX_BYTES] {
987 let mut bytes = [0; SNAPSHOT_PREFIX_BYTES];
988 let off = slot.offset();
989 let end = (off + SNAPSHOT_PREFIX_BYTES).min(self.mmap.len());
990 if off < end {
991 bytes[..end - off].copy_from_slice(&self.mmap[off..end]);
992 }
993 bytes
994 }
995
996 fn write_slot(&mut self, slot: Slot, bytes: &[u8; PAGE_SIZE]) {
997 let off = slot.offset();
998 let len = if slot == Slot::Superblock {
999 SUPERBLOCK_BYTES
1000 } else {
1001 PAGE_SIZE
1002 };
1003 if off + len > self.mmap.len() {
1004 return;
1005 }
1006 self.begin_write();
1007 self.mmap[off..off + len].copy_from_slice(&bytes[..len]);
1008 }
1009
1010 pub fn is_advisory(&self) -> bool {
1012 self._advisory.is_advisory()
1013 }
1014
1015 pub fn commit(self) {}
1017
1018 pub fn read_superblock(&self) -> Option<Superblock> {
1019 Superblock::from_payload(&self.mmap[..SUPERBLOCK_BYTES])
1020 }
1021
1022 pub fn write_superblock(&mut self, h: &Superblock) {
1024 self.begin_write();
1025 self.mmap[..SUPERBLOCK_BYTES].copy_from_slice(&h.to_payload());
1026 }
1027
1028 pub fn read_stream(&self) -> StreamHeader {
1029 StreamHeader::from_bytes(&self.read_prefix(Slot::Stream))
1030 }
1031
1032 pub fn write_stream(&mut self, h: &StreamHeader) {
1033 self.write_slot(Slot::Stream, &h.to_bytes());
1034 }
1035
1036 pub fn read_hebbian(&self) -> HebbianHeader {
1037 HebbianHeader::from_bytes(&self.read_prefix(Slot::Hebbian))
1038 }
1039
1040 pub fn write_hebbian(&mut self, h: &HebbianHeader) {
1041 self.write_slot(Slot::Hebbian, &h.to_bytes());
1042 }
1043
1044 pub fn read_attention(&self) -> AttentionHeader {
1045 AttentionHeader::from_bytes(&self.read_prefix(Slot::Attention))
1046 }
1047
1048 pub fn write_attention(&mut self, h: &AttentionHeader) {
1049 self.write_slot(Slot::Attention, &h.to_bytes());
1050 }
1051
1052 pub fn read_emotion(&self) -> EmotionHeader {
1053 EmotionHeader::from_bytes(&self.read_prefix(Slot::Emotion))
1054 }
1055
1056 pub fn write_emotion(&mut self, h: &EmotionHeader) {
1057 self.write_slot(Slot::Emotion, &h.to_bytes());
1058 }
1059
1060 pub fn read_resonance(&self) -> ResonanceHeader {
1061 ResonanceHeader::from_bytes(&self.read_prefix(Slot::Resonance))
1062 }
1063
1064 pub fn write_resonance(&mut self, h: &ResonanceHeader) {
1065 self.write_slot(Slot::Resonance, &h.to_bytes());
1066 }
1067
1068 pub fn read_graph(&self) -> GraphHeader {
1069 GraphHeader::from_bytes(&self.read_prefix(Slot::Graph))
1070 }
1071
1072 pub fn write_graph(&mut self, h: &GraphHeader) {
1073 self.write_slot(Slot::Graph, &h.to_bytes());
1074 }
1075
1076 pub fn read_cache(&self) -> CacheHeader {
1077 CacheHeader::from_bytes(&self.read_prefix(Slot::Cache))
1078 }
1079
1080 pub fn write_cache(&mut self, h: &CacheHeader) {
1081 self.write_slot(Slot::Cache, &h.to_bytes());
1082 }
1083
1084 pub fn read_archetype(&self) -> ArchetypeHeader {
1085 ArchetypeHeader::from_bytes(&self.read_prefix(Slot::Archetype))
1086 }
1087
1088 pub fn write_archetype(&mut self, h: &ArchetypeHeader) {
1089 self.write_slot(Slot::Archetype, &h.to_bytes());
1090 }
1091
1092 pub fn read_mirror(&self) -> MirrorHeader {
1093 MirrorHeader::from_bytes(&self.read_prefix(Slot::Mirror))
1094 }
1095
1096 pub fn write_mirror(&mut self, h: &MirrorHeader) {
1097 self.write_slot(Slot::Mirror, &h.to_bytes());
1098 }
1099
1100 pub fn read_merkle(&self) -> MerkleHeader {
1101 MerkleHeader::from_bytes(&self.read_prefix(Slot::Merkle))
1102 }
1103
1104 pub fn write_merkle(&mut self, h: &MerkleHeader) {
1105 self.write_slot(Slot::Merkle, &h.to_bytes());
1106 }
1107
1108 pub fn read_meta(&self) -> MetaHeader {
1109 MetaHeader::from_bytes(&self.read_prefix(Slot::Meta))
1110 }
1111
1112 pub fn write_meta(&mut self, h: &MetaHeader) {
1113 self.write_slot(Slot::Meta, &h.to_bytes());
1114 }
1115
1116 #[allow(clippy::too_many_arguments)]
1119 pub fn tick_stream(
1120 &mut self,
1121 cycle: u64,
1122 surprise: f32,
1123 curiosity: f32,
1124 predicted_hash: u64,
1125 predicted_confidence: f32,
1126 last_query_hash: u64,
1127 last_query_ms: u64,
1128 hebbian_total_energy: f64,
1129 epoch_ms: i64,
1130 ) {
1131 if self.mmap.len() < Slot::Stream.offset() + PAGE_SIZE {
1132 return;
1133 }
1134 let h = StreamHeader {
1135 cycle,
1136 surprise_level: surprise,
1137 curiosity_level: curiosity,
1138 predicted_query_hash: predicted_hash,
1139 predicted_confidence,
1140 last_query_hash,
1141 last_query_ms,
1142 hebbian_total_energy,
1143 updated_ms: epoch_ms as u64,
1144 _pad: [0; PAGE_SIZE - StreamHeader::HEADER_BYTES],
1145 };
1146 let n = self.begin_write();
1147 let payload = Superblock {
1148 tick_seq: n.wrapping_add(1) >> 1,
1149 epoch_ms,
1150 ..current_superblock(&self.mmap)
1151 }
1152 .to_payload();
1153 self.mmap[..SUPERBLOCK_BYTES].copy_from_slice(&payload);
1154 self.write_stream(&h);
1155 }
1156}
1157
1158pub struct PublishGuard<'a> {
1161 file: Option<&'a std::fs::File>,
1162 held: bool,
1163}
1164
1165impl Drop for PublishGuard<'_> {
1166 fn drop(&mut self) {
1167 #[cfg(unix)]
1168 if let (true, Some(file)) = (self.held, self.file) {
1169 use std::os::fd::AsRawFd;
1170 let _ = writer_lock::try_op(file.as_raw_fd(), writer_lock::UN);
1173 }
1174 }
1175}
1176
1177impl PublishGuard<'_> {
1178 pub fn is_advisory(&self) -> bool {
1182 self.held
1183 }
1184}
1185
1186pub struct Spine {
1190 output_dir: PathBuf,
1191 mmap: Arc<Mutex<MmapMut>>,
1192 lock_file: Option<std::fs::File>,
1196 publish_lock_file: Option<std::fs::File>,
1200 publish_lock_advisory: bool,
1203}
1204
1205unsafe fn seq_atomic(mmap: &mut MmapMut) -> &AtomicU64 {
1220 assert!(
1221 mmap.len() >= SEQ_SLOT + 8,
1222 "spine mapping is {} bytes, too short to carry a counter at {SEQ_SLOT}",
1223 mmap.len()
1224 );
1225 let base = mmap.as_mut_ptr();
1226 debug_assert_eq!(
1231 (base as usize + SEQ_SLOT) % std::mem::align_of::<AtomicU64>(),
1232 0,
1233 "publish counter is not AtomicU64-aligned"
1234 );
1235 unsafe { AtomicU64::from_ptr(base.add(SEQ_SLOT).cast::<u64>()) }
1238}
1239
1240fn seq_load(mmap: &mut MmapMut) -> u64 {
1242 unsafe { seq_atomic(mmap).load(Ordering::Acquire) }
1244}
1245
1246fn snapshot_backoff(attempt: u32) {
1249 if attempt < 64 {
1250 std::hint::spin_loop();
1251 } else {
1252 std::thread::yield_now();
1253 }
1254}
1255
1256fn seq_begin(mmap: &mut MmapMut) -> u64 {
1260 unsafe { seq_atomic(mmap).fetch_or(1, Ordering::AcqRel) | 1 }
1264}
1265
1266fn seq_end(mmap: &mut MmapMut, n: u64) {
1270 unsafe { seq_atomic(mmap).store(n.wrapping_add(1), Ordering::Release) }
1272}
1273
1274#[cfg(unix)]
1277mod writer_lock {
1278 use std::io;
1279
1280 pub const SH: i32 = 1;
1281 pub const EX: i32 = 2;
1282 pub const NB: i32 = 4;
1283 pub const UN: i32 = 8;
1284
1285 unsafe extern "C" {
1286 fn flock(fd: i32, operation: i32) -> i32;
1287 }
1288
1289 pub fn try_op(fd: i32, operation: i32) -> Result<(), (bool, io::Error)> {
1292 if unsafe { flock(fd, operation) } == 0 {
1294 return Ok(());
1295 }
1296 let err = io::Error::last_os_error();
1297 let contended = operation & NB != 0 && matches!(err.raw_os_error(), Some(11) | Some(35));
1299 Err((contended, err))
1300 }
1301}
1302
1303#[cfg(not(unix))]
1307mod writer_lock {
1308 use std::io;
1309
1310 #[allow(dead_code)]
1312 pub const SH: i32 = 1;
1313 #[allow(dead_code)]
1314 pub const EX: i32 = 2;
1315 #[allow(dead_code)]
1316 pub const NB: i32 = 4;
1317 #[allow(dead_code)]
1318 pub const UN: i32 = 8;
1319
1320 pub fn try_op(_fd: i32, _operation: i32) -> Result<(), (bool, io::Error)> {
1321 Err((
1322 false,
1323 io::Error::other("advisory writer locks are unavailable on this target"),
1324 ))
1325 }
1326}
1327
1328impl Spine {
1329 fn open_publish_lock(
1338 dir: &Path,
1339 create: bool,
1340 ) -> std::io::Result<(Option<std::fs::File>, bool)> {
1341 let path = dir.join(PUBLISH_LOCK_FILE_NAME);
1342 let opened = if create {
1343 OpenOptions::new()
1344 .read(true)
1345 .write(true)
1346 .truncate(false)
1347 .create(true)
1348 .open(&path)
1349 } else {
1350 OpenOptions::new().read(true).write(true).open(&path)
1351 };
1352 match opened {
1353 Ok(file) => Ok((Some(file), cfg!(unix))),
1354 Err(_) if !create => Ok((None, false)),
1355 Err(e) => Err(e),
1356 }
1357 }
1358
1359 pub fn begin_publish(&self) -> Publish<'_> {
1362 let mmap = self.mmap.lock();
1363 let advisory = self.publish_guard();
1364 Publish {
1365 _advisory: advisory,
1366 mmap,
1367 window: None,
1368 }
1369 }
1370
1371 fn publish_guard(&self) -> PublishGuard<'_> {
1374 #[cfg(unix)]
1375 if let Some(file) = self.publish_lock_file.as_ref() {
1376 use std::os::fd::AsRawFd;
1377 if writer_lock::try_op(file.as_raw_fd(), writer_lock::EX).is_ok() {
1381 return PublishGuard {
1382 file: Some(file),
1383 held: true,
1384 };
1385 }
1386 }
1387 PublishGuard {
1388 file: None,
1389 held: false,
1390 }
1391 }
1392
1393 pub fn publish_lock_is_advisory(&self) -> bool {
1397 self.publish_lock_advisory
1398 }
1399
1400 pub fn open(output_dir: impl AsRef<Path>) -> std::io::Result<Self> {
1403 let dir = output_dir.as_ref().to_path_buf();
1404 std::fs::create_dir_all(&dir)?;
1405 let path = dir.join("spine.bin");
1406 let file = OpenOptions::new()
1407 .read(true)
1408 .write(true)
1409 .truncate(false)
1410 .create(true)
1411 .open(&path)?;
1412 let min_len = (Slot::WalStart as usize + 8) * PAGE_SIZE;
1413 if file.metadata()?.len() < min_len as u64 {
1414 file.set_len(min_len as u64)?;
1415 }
1416
1417 let lock_file = OpenOptions::new()
1418 .read(true)
1419 .write(true)
1420 .truncate(false)
1421 .create(true)
1422 .open(dir.join(WRITER_LOCK_FILE_NAME))?;
1423 Self::mark_shared(&lock_file)?;
1424 let (publish_lock_file, publish_lock_advisory) = Self::open_publish_lock(&dir, true)?;
1425
1426 let mmap = unsafe {
1431 MmapOptions::new()
1432 .map_mut(&file)
1433 .map_err(std::io::Error::other)?
1434 };
1435 let spine = Self {
1436 output_dir: dir,
1437 mmap: Arc::new(Mutex::new(mmap)),
1438 lock_file: Some(lock_file),
1439 publish_lock_file,
1440 publish_lock_advisory,
1441 };
1442 spine.init_superblock()?;
1443 Ok(spine)
1444 }
1445
1446 pub fn open_ro(output_dir: impl AsRef<Path>) -> std::io::Result<Self> {
1450 let dir = output_dir.as_ref().to_path_buf();
1451 let file = OpenOptions::new()
1452 .read(true)
1453 .write(true)
1454 .truncate(false)
1455 .open(dir.join("spine.bin"))?;
1456 let mmap = unsafe {
1458 MmapOptions::new()
1459 .map_mut(&file)
1460 .map_err(std::io::Error::other)?
1461 };
1462 if mmap.len() < SEQ_SLOT + 8 {
1463 return Err(std::io::Error::new(
1464 std::io::ErrorKind::InvalidInput,
1465 "spine.bin is shorter than the publish header",
1466 ));
1467 }
1468 let (publish_lock_file, publish_lock_advisory) = Self::open_publish_lock(&dir, false)?;
1469 Ok(Self {
1470 output_dir: dir,
1471 mmap: Arc::new(Mutex::new(mmap)),
1472 lock_file: None,
1473 publish_lock_file,
1474 publish_lock_advisory,
1475 })
1476 }
1477
1478 #[cfg(not(unix))]
1481 fn mark_shared(_lock_file: &std::fs::File) -> std::io::Result<()> {
1482 Ok(())
1483 }
1484
1485 #[cfg(unix)]
1486 fn mark_shared(lock_file: &std::fs::File) -> std::io::Result<()> {
1487 use std::os::fd::AsRawFd;
1488 let deadline = std::time::Instant::now() + std::time::Duration::from_millis(500);
1489 loop {
1490 match writer_lock::try_op(lock_file.as_raw_fd(), writer_lock::SH | writer_lock::NB) {
1491 Ok(()) => return Ok(()),
1492 Err((true, _)) if std::time::Instant::now() < deadline => {
1493 std::thread::sleep(std::time::Duration::from_millis(2));
1494 }
1495 Err((true, _)) => {
1496 return Err(std::io::Error::new(
1497 std::io::ErrorKind::WouldBlock,
1498 "another process holds the spine lock exclusively",
1499 ))
1500 }
1501 Err((false, e)) => return Err(e),
1502 }
1503 }
1504 }
1505
1506 #[cfg(unix)]
1510 fn acquire_exclusive_until(lock: &std::fs::File) -> std::io::Result<()> {
1511 use std::os::fd::AsRawFd;
1512 let deadline = std::time::Instant::now() + std::time::Duration::from_millis(500);
1513 loop {
1514 match writer_lock::try_op(lock.as_raw_fd(), writer_lock::EX | writer_lock::NB) {
1515 Ok(()) => return Ok(()),
1516 Err((true, _)) if std::time::Instant::now() < deadline => {
1517 std::thread::sleep(std::time::Duration::from_millis(2));
1518 }
1519 Err((true, _)) => {
1520 return Err(std::io::Error::new(
1521 std::io::ErrorKind::WouldBlock,
1522 "another process holds the spine lock; resize would remap its pages",
1523 ))
1524 }
1525 Err((false, e)) => return Err(e),
1526 }
1527 }
1528 }
1529
1530 fn init_superblock(&self) -> std::io::Result<()> {
1535 let mut m = self.mmap.lock();
1536 let publish = self.publish_guard();
1537 if Superblock::from_payload(&m[0..SUPERBLOCK_BYTES]).is_some() {
1538 return Ok(()); }
1540 let fresh = match Superblock::from_v1_payload(&m[0..SUPERBLOCK_BYTES]) {
1544 Some(old) => Superblock {
1545 version: SUPERBLOCK_VERSION,
1546 ..old
1547 },
1548 None => Superblock::new(),
1552 };
1553 let n = seq_begin(&mut m); m[0..SUPERBLOCK_BYTES].copy_from_slice(&fresh.to_payload());
1555 seq_end(&mut m, n); drop(publish);
1557 drop(m);
1558 self.sync()?; Ok(())
1560 }
1561
1562 fn read_slot(&self, slot: Slot) -> [u8; SNAPSHOT_PREFIX_BYTES] {
1565 let off = slot.offset();
1566 let m = self.mmap.lock();
1567 let mut buf = [0u8; SNAPSHOT_PREFIX_BYTES];
1568 let end = (off + SNAPSHOT_PREFIX_BYTES).min(m.len());
1569 if off < end {
1570 buf[..end - off].copy_from_slice(&m[off..end]);
1571 }
1572 buf
1573 }
1574
1575 pub fn write_slot(&self, slot: Slot, bytes: &[u8; PAGE_SIZE]) {
1578 self.begin_publish().write_slot(slot, bytes);
1579 }
1580
1581 pub fn read_stream(&self) -> StreamHeader {
1582 StreamHeader::from_bytes(&self.read_slot(Slot::Stream))
1583 }
1584 pub fn write_stream(&self, h: &StreamHeader) {
1585 self.write_slot(Slot::Stream, &h.to_bytes());
1586 }
1587
1588 #[allow(clippy::too_many_arguments)]
1591 pub fn tick(
1592 &self,
1593 cycle: u64,
1594 surprise: f32,
1595 curiosity: f32,
1596 predicted_hash: u64,
1597 predicted_confidence: f32,
1598 last_query_hash: u64,
1599 last_query_ms: u64,
1600 hebbian_total_energy: f64,
1601 ) {
1602 let epoch_ms = now_ms();
1603 let mut p = self.begin_publish();
1604 p.tick_stream(
1605 cycle,
1606 surprise,
1607 curiosity,
1608 predicted_hash,
1609 predicted_confidence,
1610 last_query_hash,
1611 last_query_ms,
1612 hebbian_total_energy,
1613 epoch_ms,
1614 );
1615 p.commit();
1616 }
1617
1618 pub fn read_snapshot(&self) -> Option<PublishSnapshot> {
1621 let bytes = self.read_prefixes([Slot::Stream])?;
1622 let sb = Superblock::from_payload(&bytes.payload)?;
1623 Some(PublishSnapshot {
1624 stream: StreamHeader::from_bytes(&bytes.headers[0]),
1625 tick_seq: sb.tick_seq,
1626 epoch_ms: sb.epoch_ms,
1627 sequence: bytes.sequence,
1628 })
1629 }
1630
1631 pub fn read_store_snapshot(&self) -> Option<StoreSnapshot> {
1634 let bytes = self.read_prefixes([
1635 Slot::Stream,
1636 Slot::Hebbian,
1637 Slot::Attention,
1638 Slot::Emotion,
1639 Slot::Resonance,
1640 Slot::Graph,
1641 Slot::Cache,
1642 Slot::Archetype,
1643 Slot::Mirror,
1644 Slot::Merkle,
1645 Slot::Meta,
1646 ])?;
1647 let sb = Superblock::from_payload(&bytes.payload)?;
1648 Some(StoreSnapshot {
1649 stream: StreamHeader::from_bytes(&bytes.headers[0]),
1650 hebbian: HebbianHeader::from_bytes(&bytes.headers[1]),
1651 attention: AttentionHeader::from_bytes(&bytes.headers[2]),
1652 emotion: EmotionHeader::from_bytes(&bytes.headers[3]),
1653 resonance: ResonanceHeader::from_bytes(&bytes.headers[4]),
1654 graph: GraphHeader::from_bytes(&bytes.headers[5]),
1655 cache: CacheHeader::from_bytes(&bytes.headers[6]),
1656 archetype: ArchetypeHeader::from_bytes(&bytes.headers[7]),
1657 mirror: MirrorHeader::from_bytes(&bytes.headers[8]),
1658 merkle: MerkleHeader::from_bytes(&bytes.headers[9]),
1659 meta: MetaHeader::from_bytes(&bytes.headers[10]),
1660 tick_seq: sb.tick_seq,
1661 epoch_ms: sb.epoch_ms,
1662 sequence: bytes.sequence,
1663 })
1664 }
1665
1666 fn read_prefixes<const N: usize>(&self, slots: [Slot; N]) -> Option<SnapshotBytes<N>> {
1667 let required_len = slots
1668 .iter()
1669 .map(|s| s.offset() + SNAPSHOT_PREFIX_BYTES)
1670 .max()
1671 .unwrap_or(SEQ_SLOT + 8)
1672 .max(SEQ_SLOT + 8);
1673 if self.mmap.lock().len() < required_len {
1674 return None;
1675 }
1676 let mut started = None;
1677 let mut attempt = 0u32;
1678 loop {
1679 if let Some(bytes) = self.read_prefixes_once(&slots) {
1680 return Some(bytes);
1681 }
1682 if self.snapshot_patience(&mut started, attempt) {
1683 break;
1684 }
1685 snapshot_backoff(attempt);
1686 attempt = attempt.saturating_add(1);
1687 }
1688 if self.recover_stalled_publish() {
1689 return self.read_prefixes_once(&slots);
1690 }
1691 None
1692 }
1693
1694 fn read_prefixes_once<const N: usize>(&self, slots: &[Slot; N]) -> Option<SnapshotBytes<N>> {
1695 let mut m = self.mmap.lock();
1696 if m.len() < SEQ_SLOT + 8
1697 || slots
1698 .iter()
1699 .any(|s| s.offset() + SNAPSHOT_PREFIX_BYTES > m.len())
1700 {
1701 return None;
1702 }
1703 let s1 = seq_load(&mut m);
1704 if s1 & 1 == 1 {
1705 return None;
1706 }
1707 let mut bytes = SnapshotBytes {
1708 headers: [[0; SNAPSHOT_PREFIX_BYTES]; N],
1709 payload: [0; SUPERBLOCK_BYTES],
1710 sequence: s1,
1711 };
1712 for (prefix, slot) in bytes.headers.iter_mut().zip(slots) {
1713 let off = slot.offset();
1714 prefix.copy_from_slice(&m[off..off + SNAPSHOT_PREFIX_BYTES]);
1715 }
1716 bytes.payload.copy_from_slice(&m[..SUPERBLOCK_BYTES]);
1717 std::sync::atomic::fence(Ordering::Acquire);
1719 if seq_load(&mut m) != s1 {
1720 return None;
1721 }
1722 Some(bytes)
1723 }
1724
1725 fn snapshot_patience(&self, started: &mut Option<std::time::Instant>, attempt: u32) -> bool {
1729 if attempt < SNAPSHOT_MAX_RETRIES {
1730 return false;
1731 }
1732 let t0 = *started.get_or_insert_with(std::time::Instant::now);
1733 t0.elapsed() >= std::time::Duration::from_millis(SNAPSHOT_MAX_WAIT_MS)
1734 }
1735
1736 pub fn publish_window_open(&self) -> bool {
1741 self.publish_in_flight()
1742 }
1743
1744 pub fn recover_stalled_publish(&self) -> bool {
1758 let mut m = self.mmap.lock();
1759 if m.len() < SEQ_SLOT + 8 {
1760 return false;
1761 }
1762 if seq_load(&mut m) & 1 == 0 {
1765 return false;
1766 }
1767 let _publish = self.publish_guard();
1768 let n = seq_load(&mut m);
1769 if n & 1 == 0 {
1770 return false; }
1772 if !_publish.is_advisory() {
1773 return false;
1777 }
1778 seq_end(&mut m, n);
1779 true
1780 }
1781
1782 pub fn publish_sequence(&self) -> u64 {
1784 let mut m = self.mmap.lock();
1785 if m.len() < SEQ_SLOT + 8 {
1786 return 0;
1787 }
1788 seq_load(&mut m)
1789 }
1790
1791 pub fn publish_in_flight(&self) -> bool {
1795 let mut m = self.mmap.lock();
1796 if m.len() < SEQ_SLOT + 8 {
1797 return false;
1798 }
1799 seq_load(&mut m) & 1 == 1
1800 }
1801
1802 pub fn read_hebbian(&self) -> HebbianHeader {
1803 HebbianHeader::from_bytes(&self.read_slot(Slot::Hebbian))
1804 }
1805 pub fn write_hebbian(&self, h: &HebbianHeader) {
1806 self.write_slot(Slot::Hebbian, &h.to_bytes());
1807 }
1808 pub fn read_attention(&self) -> AttentionHeader {
1809 AttentionHeader::from_bytes(&self.read_slot(Slot::Attention))
1810 }
1811 pub fn write_attention(&self, h: &AttentionHeader) {
1812 self.write_slot(Slot::Attention, &h.to_bytes());
1813 }
1814 pub fn read_emotion(&self) -> EmotionHeader {
1815 EmotionHeader::from_bytes(&self.read_slot(Slot::Emotion))
1816 }
1817 pub fn write_emotion(&self, h: &EmotionHeader) {
1818 self.write_slot(Slot::Emotion, &h.to_bytes());
1819 }
1820 pub fn read_resonance(&self) -> ResonanceHeader {
1821 ResonanceHeader::from_bytes(&self.read_slot(Slot::Resonance))
1822 }
1823 pub fn write_resonance(&self, h: &ResonanceHeader) {
1824 self.write_slot(Slot::Resonance, &h.to_bytes());
1825 }
1826 pub fn read_graph(&self) -> GraphHeader {
1827 GraphHeader::from_bytes(&self.read_slot(Slot::Graph))
1828 }
1829 pub fn write_graph(&self, h: &GraphHeader) {
1830 self.write_slot(Slot::Graph, &h.to_bytes());
1831 }
1832 pub fn read_cache(&self) -> CacheHeader {
1833 CacheHeader::from_bytes(&self.read_slot(Slot::Cache))
1834 }
1835 pub fn write_cache(&self, h: &CacheHeader) {
1836 self.write_slot(Slot::Cache, &h.to_bytes());
1837 }
1838 pub fn read_archetype(&self) -> ArchetypeHeader {
1839 ArchetypeHeader::from_bytes(&self.read_slot(Slot::Archetype))
1840 }
1841 pub fn write_archetype(&self, h: &ArchetypeHeader) {
1842 self.write_slot(Slot::Archetype, &h.to_bytes());
1843 }
1844 pub fn read_mirror(&self) -> MirrorHeader {
1845 MirrorHeader::from_bytes(&self.read_slot(Slot::Mirror))
1846 }
1847 pub fn write_mirror(&self, h: &MirrorHeader) {
1848 self.write_slot(Slot::Mirror, &h.to_bytes());
1849 }
1850 pub fn read_merkle(&self) -> MerkleHeader {
1851 MerkleHeader::from_bytes(&self.read_slot(Slot::Merkle))
1852 }
1853 pub fn write_merkle(&self, h: &MerkleHeader) {
1854 self.write_slot(Slot::Merkle, &h.to_bytes());
1855 }
1856 pub fn read_meta(&self) -> MetaHeader {
1857 MetaHeader::from_bytes(&self.read_slot(Slot::Meta))
1858 }
1859 pub fn write_meta(&self, h: &MetaHeader) {
1860 self.write_slot(Slot::Meta, &h.to_bytes());
1861 }
1862
1863 pub fn read_superblock(&self) -> Option<Superblock> {
1866 let m = self.mmap.lock();
1867 if m.len() < SUPERBLOCK_BYTES {
1868 return None;
1869 }
1870 Superblock::from_payload(&m[0..SUPERBLOCK_BYTES])
1871 }
1872
1873 pub fn sync(&self) -> std::io::Result<()> {
1874 self.mmap.lock().flush()
1875 }
1876 pub fn size_bytes(&self) -> usize {
1877 self.mmap.lock().len()
1878 }
1879 pub fn output_dir(&self) -> &Path {
1880 &self.output_dir
1881 }
1882 pub fn path(&self) -> PathBuf {
1883 self.output_dir.join("spine.bin")
1884 }
1885
1886 pub fn resize(&self, new_len: usize) -> std::io::Result<()> {
1890 #[cfg(unix)]
1891 use std::os::fd::AsRawFd;
1892 let Some(lock) = self.lock_file.as_ref() else {
1893 return Err(std::io::Error::new(
1894 std::io::ErrorKind::PermissionDenied,
1895 "read-only spine handle cannot resize",
1896 ));
1897 };
1898 #[cfg(unix)]
1899 {
1900 let _ = writer_lock::try_op(lock.as_raw_fd(), writer_lock::UN);
1902 if let Err(e) = Self::acquire_exclusive_until(lock) {
1903 let _ = Self::mark_shared(lock);
1907 return Err(e);
1908 }
1909 }
1910 #[cfg(not(unix))]
1911 {
1912 let _ = lock;
1913 return Err(std::io::Error::other(
1914 "resize needs advisory file locks, unavailable on this target",
1915 ));
1916 }
1917
1918 let result = (|| -> std::io::Result<()> {
1919 {
1920 let f = OpenOptions::new()
1921 .read(true)
1922 .write(true)
1923 .truncate(false)
1924 .open(self.path())?;
1925 f.set_len(new_len as u64)?;
1926 }
1927 let file = OpenOptions::new()
1928 .read(true)
1929 .write(true)
1930 .truncate(false)
1931 .open(self.path())?;
1932 let new_mmap = unsafe {
1934 MmapOptions::new()
1935 .map_mut(&file)
1936 .map_err(std::io::Error::other)?
1937 };
1938 *self.mmap.lock() = new_mmap;
1939 Ok(())
1940 })();
1941
1942 #[cfg(unix)]
1943 {
1944 let _ = writer_lock::try_op(lock.as_raw_fd(), writer_lock::UN);
1945 let _ = Self::mark_shared(lock);
1946 }
1947 result
1948 }
1949}
1950
1951fn current_superblock(mmap: &MmapMut) -> Superblock {
1954 match Superblock::from_payload(&mmap[0..SUPERBLOCK_BYTES]) {
1955 Some(sb) => sb,
1956 None => Superblock::new(), }
1958}
1959
1960pub fn format_summary(spine: &Spine) -> String {
1965 let Some(snap) = spine.read_store_snapshot() else {
1966 return "Spine: snapshot unavailable (busy, short or invalid mapping)".to_owned();
1967 };
1968 let s = snap.stream;
1969 let tick_seq = snap.tick_seq;
1970 let epoch_ms = snap.epoch_ms;
1971 let h = snap.hebbian;
1972 let a = snap.attention;
1973 let e = snap.emotion;
1974 let r = snap.resonance;
1975 let g = snap.graph;
1976 let c = snap.cache;
1977 let ar = snap.archetype;
1978 let mi = snap.mirror;
1979 let me = snap.merkle;
1980 let meta = snap.meta;
1981 format!(
1982 "Spine: {} pages mapped ({} KB)\n\
1983 Publish: seq={} epoch_ms={} in_flight={}\n\
1984 Stream: cycle={} surprise={:.3} curiosity={:.3} predict_conf={:.3}\n\
1985 Hebbian: {} activations, {} coactivations, delta_tail={}\n\
1986 Attention: {} recalls, quality={:.2}\n\
1987 Emotion: {} updates, dominant_idx={}\n\
1988 Resonance: {} cells, {} emitted, {} received\n\
1989 Graph: {} nodes, {} edges, {} patterns\n\
1990 Cache: {} predictions, hit_rate={:.1}%\n\
1991 Archetype: {} active, {} emerged\n\
1992 Mirror: {} echoes, strongest={:.3}\n\
1993 Merkle: height={}, {} blocks\n\
1994 Meta: {} blocks, {} dream cycles",
1995 spine.size_bytes() / PAGE_SIZE,
1996 spine.size_bytes() / 1024,
1997 tick_seq,
1998 epoch_ms,
1999 spine.publish_in_flight(),
2000 s.cycle,
2001 s.surprise_level,
2002 s.curiosity_level,
2003 s.predicted_confidence,
2004 h.activation_count,
2005 h.coactivation_count,
2006 h.delta_tail_offset,
2007 a.total_recalls,
2008 a.quality_signal,
2009 e.total_updates,
2010 e.current[0],
2011 r.field_cells,
2012 r.pulses_emitted,
2013 r.pulses_received,
2014 g.node_count,
2015 g.edge_count,
2016 g.pattern_count,
2017 c.total_predictions,
2018 c.hit_rate() * 100.0,
2019 ar.active_count,
2020 ar.emergence_events,
2021 mi.echo_count,
2022 mi.strongest_similarity,
2023 me.chain_height,
2024 me.block_count,
2025 meta.block_count,
2026 meta.dream_cycle_count
2027 )
2028}
2029
2030#[cfg(test)]
2031mod tests {
2032 use super::*;
2033 fn temp_spine(tag: &str) -> Spine {
2034 let dir = std::env::temp_dir().join(format!("m1c_spine_{}_{}", tag, std::process::id()));
2035 let _ = std::fs::remove_dir_all(&dir);
2036 Spine::open(&dir).unwrap()
2037 }
2038 #[test]
2039 fn test_read_only_batch_does_not_publish() {
2040 let spine = temp_spine("read_batch");
2041 let before = spine.publish_sequence();
2042 let p = spine.begin_publish();
2043 assert_eq!(p.read_stream().cycle, 0);
2044 assert!(p.read_superblock().is_some());
2045 p.commit();
2046 assert_eq!(spine.publish_sequence(), before);
2047 }
2048
2049 #[test]
2050 fn test_batch_is_one_window_and_excludes_partial_reads() {
2051 let spine = temp_spine("batch_window");
2052 let peer = Spine::open_ro(spine.output_dir()).unwrap();
2053 let before = spine.publish_sequence();
2054 let mut p = spine.begin_publish();
2055 p.tick_stream(9, 0.0, 0.0, !9, 0.0, 0, 0, 9.0, 1234);
2056 assert_eq!(peer.read_stream().cycle, 9);
2059 assert_eq!(peer.read_graph().node_count, 0);
2060 assert!(peer
2061 .read_prefixes_once(&[Slot::Stream, Slot::Graph])
2062 .is_none());
2063 let mut graph = p.read_graph();
2064 graph.node_count = 9;
2065 p.write_graph(&graph);
2066 let mut emotion = p.read_emotion();
2067 emotion.current[20] = 9.0;
2068 emotion.total_updates = 9;
2069 p.write_emotion(&emotion);
2070 p.commit();
2071 assert_eq!(spine.publish_sequence(), before + 2);
2072 let snap = peer.read_store_snapshot().unwrap();
2073 assert_eq!(snap.stream.cycle, 9);
2074 assert_eq!(snap.graph.node_count, 9);
2075 assert_eq!(snap.emotion.current[20], 9.0);
2077 assert_eq!(snap.emotion.total_updates, 9);
2078 assert_eq!(snap.epoch_ms, 1234);
2079 assert_eq!(snap.stream.updated_ms, 1234);
2080 }
2081
2082 #[test]
2083 fn test_drop_and_unwinding_close_the_batch_without_rollback() {
2084 let spine = temp_spine("batch_drop");
2085 let before = spine.publish_sequence();
2086 {
2087 let mut p = spine.begin_publish();
2088 let mut h = p.read_meta();
2089 h.block_count = 7;
2090 p.write_meta(&h);
2091 }
2092 assert_eq!(spine.publish_sequence(), before + 2);
2093 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2094 let mut p = spine.begin_publish();
2095 let mut h = p.read_graph();
2096 h.node_count = 13;
2097 p.write_graph(&h);
2098 panic!("batch interrupted");
2099 }));
2100 assert!(result.is_err());
2101 assert_eq!(spine.publish_sequence(), before + 4);
2102 let snap = spine.read_store_snapshot().unwrap();
2103 assert_eq!(snap.meta.block_count, 7);
2104 assert_eq!(snap.graph.node_count, 13);
2105 spine.tick(2, 0.0, 0.0, 0, 0.0, 0, 0, 0.0);
2106 assert!(!spine.publish_window_open());
2107 }
2108
2109 #[test]
2110 fn test_each_legacy_header_write_opens_a_window() {
2111 let spine = temp_spine("legacy_windows");
2112 let before = spine.publish_sequence();
2113 let h = spine.read_hebbian();
2114 spine.write_hebbian(&h);
2115 let g = spine.read_graph();
2116 spine.write_graph(&g);
2117 let m = spine.read_meta();
2118 spine.write_meta(&m);
2119 assert_eq!(spine.publish_sequence(), before + 6);
2120 assert!(spine.read_store_snapshot().is_some());
2121 }
2122
2123 #[test]
2124 fn test_store_snapshot_rejects_short_and_invalid_mappings() {
2125 let spine = temp_spine("short_snapshot");
2126 let path = spine.output_dir().join("short.bin");
2128 let file = OpenOptions::new()
2129 .read(true)
2130 .write(true)
2131 .create(true)
2132 .truncate(true)
2133 .open(path)
2134 .unwrap();
2135 file.set_len((Slot::Stream.offset() + SNAPSHOT_PREFIX_BYTES) as u64)
2136 .unwrap();
2137 let mut short = unsafe { MmapOptions::new().map_mut(&file).unwrap() };
2139 short[..SUPERBLOCK_BYTES].copy_from_slice(&Superblock::new().to_payload());
2140 *spine.mmap.lock() = short;
2141 assert!(spine.read_snapshot().is_some());
2142 assert!(spine.read_store_snapshot().is_none());
2143 assert!(format_summary(&spine).contains("snapshot unavailable"));
2144 spine.mmap.lock()[..SUPERBLOCK_BYTES].fill(0);
2145 assert!(spine.read_snapshot().is_none());
2146 }
2147
2148 #[test]
2149 fn test_republishing_after_a_stall_never_marks_the_window_even() {
2150 let spine = temp_spine("republish_stall");
2151 let mut m = spine.mmap.lock();
2152 let odd = seq_begin(&mut m);
2153 assert_eq!(seq_begin(&mut m), odd);
2154 assert_eq!(seq_load(&mut m), odd);
2155 seq_end(&mut m, odd);
2156 assert_eq!(seq_load(&mut m), odd + 1);
2157 }
2158
2159 #[test]
2160 fn test_open_creates_superblock() {
2161 let spine = temp_spine("sb");
2162 let sb = spine.read_superblock().unwrap();
2163 assert_eq!(sb.magic, MAGIC);
2164 assert_eq!(sb.page_size as usize, PAGE_SIZE);
2165 }
2166 #[test]
2167 fn test_stream_roundtrip() {
2168 let spine = temp_spine("stream");
2169 spine.tick(42, 0.5, 0.7, 0xdeadbeef, 0.9, 0xcafebabe, 12345, 1337.5);
2170 let s = spine.read_stream();
2171 assert_eq!(s.cycle, 42);
2172 assert!((s.surprise_level - 0.5).abs() < 1e-6);
2173 assert_eq!(s.predicted_query_hash, 0xdeadbeef);
2174 assert!((s.hebbian_total_energy - 1337.5).abs() < 1e-6);
2175 assert!(s.updated_ms > 0);
2176 }
2177 #[test]
2178 fn test_layer_headers_roundtrip() {
2179 let spine = temp_spine("layers");
2180 let mut h = spine.read_hebbian();
2181 h.activation_count = 999;
2182 h.delta_tail_offset = 123456789;
2183 h.decay_event_count = 0;
2184 spine.write_hebbian(&h);
2185 assert_eq!(spine.read_hebbian().activation_count, 999);
2186 assert_eq!(spine.read_hebbian().delta_tail_offset, 123456789);
2187 let mut a = spine.read_attention();
2188 a.learned_weights[0] = 0.25;
2189 a.total_recalls = 17;
2190 spine.write_attention(&a);
2191 assert!((spine.read_attention().learned_weights[0] - 0.25).abs() < 1e-6);
2192 assert_eq!(spine.read_attention().total_recalls, 17);
2193 let mut e = spine.read_emotion();
2194 e.current[3] = 0.8;
2195 e.total_updates = 100;
2196 spine.write_emotion(&e);
2197 assert!((spine.read_emotion().current[3] - 0.8).abs() < 1e-6);
2198 let mut g = spine.read_graph();
2199 g.node_count = 500;
2200 spine.write_graph(&g);
2201 assert_eq!(spine.read_graph().node_count, 500);
2202 let mut c = spine.read_cache();
2203 c.total_hits = 10;
2204 c.total_misses = 2;
2205 spine.write_cache(&c);
2206 let rc = spine.read_cache();
2207 assert!((rc.hit_rate() - 10.0 / 12.0).abs() < 1e-5);
2208 }
2209 #[test]
2210 fn test_all_slots_distinct() {
2211 let spine = temp_spine("slots");
2212 spine.tick(1, 1.0, 1.0, 1, 1.0, 1, 1, 1.0);
2213 let mut g = spine.read_graph();
2214 g.node_count = 42;
2215 spine.write_graph(&g);
2216 let mut m = spine.read_mirror();
2217 m.echo_count = 7;
2218 spine.write_mirror(&m);
2219 assert_eq!(spine.read_stream().cycle, 1);
2220 assert_eq!(spine.read_graph().node_count, 42);
2221 assert_eq!(spine.read_mirror().echo_count, 7);
2222 assert_eq!(spine.read_archetype().active_count, 0);
2223 }
2224 #[test]
2225 fn test_reopen_preserves_all_layers() {
2226 let dir = std::env::temp_dir().join(format!("m1c_spine_reopen_{}", std::process::id()));
2227 let _ = std::fs::remove_dir_all(&dir);
2228 {
2229 let spine = Spine::open(&dir).unwrap();
2230 spine.tick(7, 0.3, 0.4, 5, 6.0, 7, 8, 9.0);
2231 let mut a = spine.read_attention();
2232 a.total_recalls = 55;
2233 spine.write_attention(&a);
2234 spine.sync().unwrap();
2235 }
2236 let spine = Spine::open(&dir).unwrap();
2237 assert_eq!(spine.read_stream().cycle, 7);
2238 assert!((spine.read_stream().surprise_level - 0.3).abs() < 1e-6);
2239 assert_eq!(spine.read_attention().total_recalls, 55);
2240 }
2241 #[test]
2242 fn test_superblock_payload_carries_tick_seq() {
2243 let spine = temp_spine("sb_payload");
2244 assert_eq!(spine.read_superblock().unwrap().tick_seq, 0);
2245 spine.tick(1, 0.0, 0.0, 0, 0.0, 0, 0, 0.0);
2246 let after_first = spine.read_superblock().unwrap().tick_seq;
2247 spine.tick(2, 0.0, 0.0, 0, 0.0, 0, 0, 0.0);
2248 let after_second = spine.read_superblock().unwrap().tick_seq;
2249 assert!(after_first > 0, "first publish did not bump the sequence");
2252 assert_eq!(after_second, after_first + 1);
2253 }
2254
2255 #[test]
2256 fn test_snapshot_pairs_stream_and_superblock() {
2257 let spine = temp_spine("snapshot");
2258 for cycle in 100..110 {
2259 spine.tick(cycle, 0.25, 0.5, cycle, 1.0, cycle, cycle, cycle as f64);
2260 let snap = spine
2261 .read_snapshot()
2262 .expect("seqlock should not starve a same-process reader");
2263 assert_eq!(snap.stream.cycle, cycle);
2264 assert_eq!(snap.epoch_ms, snap.stream.updated_ms as i64);
2265 assert_eq!(
2266 snap.sequence % 2,
2267 0,
2268 "a published snapshot is never taken mid-window"
2269 );
2270 }
2271 }
2272
2273 #[test]
2274 fn test_write_slot_on_superblock_keeps_the_counter() {
2275 let spine = temp_spine("sb_slot");
2276 spine.tick(7, 0.0, 0.0, 0, 0.0, 0, 0, 0.0);
2277 let before = spine.publish_sequence();
2278 assert!(before > 0);
2279 let junk = [0xABu8; PAGE_SIZE];
2282 spine.write_slot(Slot::Superblock, &junk);
2283 let after = spine.publish_sequence();
2284 assert_eq!(after, before + 2, "counter did not advance as a publish");
2288 assert_eq!(after % 2, 0, "counter left inside a publish window");
2289 assert!(spine.read_superblock().is_none());
2293 assert!(!spine.publish_in_flight());
2294 }
2295
2296 #[test]
2297 fn test_open_upgrades_a_version_one_superblock() {
2298 let dir = std::env::temp_dir().join(format!("m1c_spine_v1_{}", std::process::id()));
2299 let _ = std::fs::remove_dir_all(&dir);
2300 {
2301 let spine = Spine::open(&dir).unwrap();
2302 let mut payload = [0u8; SUPERBLOCK_BYTES];
2305 payload[0..8].copy_from_slice(&MAGIC.to_le_bytes());
2306 payload[8..12].copy_from_slice(&1u32.to_le_bytes());
2307 payload[12..16].copy_from_slice(&(PAGE_SIZE as u32).to_le_bytes());
2308 payload[16..20].copy_from_slice(&(Slot::WalStart as u32).to_le_bytes());
2309 payload[20..24].copy_from_slice(&(Slot::WalStart as u32).to_le_bytes());
2310 payload[24..32].copy_from_slice(&1_700_000_000_000i64.to_le_bytes());
2311 let mut page = [0u8; PAGE_SIZE];
2312 page[..SUPERBLOCK_BYTES].copy_from_slice(&payload);
2313 spine.write_slot(Slot::Superblock, &page);
2316 }
2317 let spine = Spine::open(&dir).unwrap();
2320 let sb = spine.read_superblock().unwrap();
2321 assert_eq!(sb.version, SUPERBLOCK_VERSION);
2322 assert_eq!(sb.wal_start_page, Slot::WalStart as u32);
2323 assert_eq!(sb.tick_seq, 0);
2324 assert_eq!(sb.epoch_ms, 1_700_000_000_000);
2325 }
2326
2327 #[test]
2328 fn test_read_only_handle_sees_the_writers_publish() {
2329 let dir = std::env::temp_dir().join(format!("m1c_spine_ro_{}", std::process::id()));
2330 let _ = std::fs::remove_dir_all(&dir);
2331 let writer = Spine::open(&dir).unwrap();
2332 let reader = Spine::open_ro(&dir).unwrap();
2333 for cycle in 1..6 {
2334 writer.tick(cycle, 0.0, 0.0, cycle, 0.0, 0, 0, cycle as f64);
2335 let snap = reader
2336 .read_snapshot()
2337 .expect("read-only handle reads published snapshots");
2338 assert_eq!(snap.stream.cycle, cycle);
2339 assert_eq!(snap.stream.predicted_query_hash, cycle);
2340 }
2341 assert!(
2342 reader.resize(1 << 20).is_err(),
2343 "a read-only handle must not remap the file"
2344 );
2345 }
2346
2347 #[test]
2351 #[should_panic(expected = "too short to carry a counter")]
2352 fn test_counter_access_on_a_short_mapping_panics() {
2353 use std::io::Write;
2354 let dir = std::env::temp_dir().join(format!("m1c_spine_short_{}", std::process::id()));
2355 let _ = std::fs::remove_dir_all(&dir);
2356 std::fs::create_dir_all(&dir).expect("create dir");
2357 let path = dir.join("tiny.bin");
2358 {
2359 let mut f = std::fs::OpenOptions::new()
2360 .read(true)
2361 .write(true)
2362 .create(true)
2363 .truncate(true)
2364 .open(&path)
2365 .expect("create tiny file");
2366 f.write_all(&[0u8; 16]).expect("write");
2367 }
2368 let file = std::fs::OpenOptions::new()
2369 .read(true)
2370 .write(true)
2371 .open(&path)
2372 .expect("open tiny file");
2373 let mut mmap = unsafe { MmapOptions::new().map_mut(&file).expect("map") };
2376 let _ = seq_load(&mut mmap);
2377 let _ = std::fs::remove_dir_all(&dir);
2378 }
2379
2380 #[test]
2381 fn test_stalled_publish_window_is_recoverable() {
2382 let spine = temp_spine("stall");
2383 spine.tick(1, 0.0, 0.0, 0, 0.0, 0, 0, 0.0);
2384 {
2388 let mut m = spine.mmap.lock();
2389 let _n = seq_begin(&mut m);
2390 }
2391 assert!(spine.publish_window_open(), "window should read as open");
2392 let before = spine.read_stream().cycle;
2393 assert!(spine.recover_stalled_publish());
2394 assert!(
2395 !spine.publish_window_open(),
2396 "recovery should close the window"
2397 );
2398 assert!(!spine.recover_stalled_publish(), "nothing to recover twice");
2399 assert_eq!(
2400 spine.read_stream().cycle,
2401 before,
2402 "recovery must not publish"
2403 );
2404 {
2408 let mut m = spine.mmap.lock();
2409 let _n = seq_begin(&mut m);
2410 }
2411 assert!(
2412 spine.read_snapshot().is_some(),
2413 "read_snapshot must heal a stalled window"
2414 );
2415 assert!(!spine.publish_window_open());
2416 spine.tick(2, 0.0, 0.0, 0, 0.0, 0, 0, 0.0);
2417 assert_eq!(spine.read_snapshot().unwrap().stream.cycle, 2);
2418 }
2419
2420 #[test]
2421 fn test_summary_reports_the_publish_record() {
2422 let spine = temp_spine("summary_publish");
2423 spine.tick(11, 0.5, 0.6, 0, 0.7, 0, 0, 100.0);
2424 let out = format_summary(&spine);
2425 assert!(out.contains("Publish:"), "{out}");
2426 let seq = spine.read_snapshot().unwrap().tick_seq;
2427 assert!(
2428 out.contains(&format!("seq={seq}")),
2429 "the summary must print the counter, not a literal: {out}"
2430 );
2431 }
2432
2433 #[test]
2434 fn test_summary_formats() {
2435 let spine = temp_spine("summary");
2436 spine.tick(10, 0.5, 0.6, 0, 0.7, 0, 0, 100.0);
2437 let out = format_summary(&spine);
2438 assert!(out.contains("Spine:"));
2439 assert!(out.contains("cycle=10"));
2440 }
2441}