1use crate::config::Config;
6use crate::reader::{read_append_log, FileLock, MicroscopeReader};
7use crate::types::BlockHeader;
8use crate::{
9 content_coords_blended, crc16_ccitt, hex_str, layer_to_id, merkle, safe_truncate, to_block,
10 BLOCK_DATA_SIZE, DEPTH_ENTRY_SIZE, HEADER_SIZE, META_HEADER_SIZE,
11};
12
13use crate::parallel::*;
14use colored::Colorize;
15use sha2::{Digest, Sha256};
16use std::collections::HashMap;
17use std::fs;
18use std::io::{BufWriter, Seek, Write};
19use std::path::Path;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct RebuildOutcome {
23 pub rebuilt: bool,
24 pub pending_entries: usize,
25}
26
27pub fn rebuild_pending(
35 config: &Config,
36 force_when_empty: bool,
37 heavy: bool,
38) -> Result<RebuildOutcome, String> {
39 let _lock = FileLock::acquire(config)?;
40 let output_dir = Path::new(&config.paths.output_dir);
41 let append_path = output_dir.join("append.bin");
42 let pending_entries = read_append_log(&append_path).len();
43
44 if pending_entries == 0 && !force_when_empty {
45 return Ok(RebuildOutcome {
46 rebuilt: false,
47 pending_entries: 0,
48 });
49 }
50
51 let previous_blocks = read_meta_block_count(output_dir);
52
53 build(config, true, heavy)?;
54
55 let rebuilt_blocks = read_meta_block_count(output_dir);
56 if pending_entries > 0 && previous_blocks > 0 && rebuilt_blocks < previous_blocks {
57 return Err(format!(
58 "refusing rebuild shrink: {} -> {} blocks",
59 previous_blocks, rebuilt_blocks
60 ));
61 }
62 if append_path.exists() {
63 fs::remove_file(&append_path).map_err(|e| format!("clear append log: {}", e))?;
64 let sidecar = Path::new(&config.paths.output_dir)
68 .join(crate::embedding_index::APPEND_EMBEDDINGS_FILE);
69 let _ = fs::remove_file(sidecar);
70 }
71
72 Ok(RebuildOutcome {
73 rebuilt: true,
74 pending_entries,
75 })
76}
77
78pub fn maybe_auto_rebuild(config: &Config) -> Result<Option<usize>, String> {
81 if !config.index.auto_rebuild {
82 return Ok(None);
83 }
84
85 let output_dir = Path::new(&config.paths.output_dir);
86 let append_path = output_dir.join("append.bin");
87 let pending_entries = read_append_log(&append_path).len();
88 if pending_entries == 0 {
89 return Ok(None);
90 }
91
92 let threshold = config.index.auto_rebuild_entries.max(1);
93 let index_missing = !output_dir.join("meta.bin").exists();
94 if !index_missing && pending_entries < threshold {
95 return Ok(None);
96 }
97
98 let outcome = rebuild_pending(config, false, false)?;
99 Ok(outcome.rebuilt.then_some(outcome.pending_entries))
100}
101
102fn read_meta_block_count(output_dir: &Path) -> usize {
103 let meta = match fs::read(output_dir.join("meta.bin")) {
104 Ok(meta) => meta,
105 Err(_) => return 0,
106 };
107 if meta.len() < 12 {
108 return 0;
109 }
110 u32::from_le_bytes(meta[8..12].try_into().unwrap()) as usize
111}
112
113struct RawBlock {
115 data: Vec<u8>,
116 depth: u8,
117 x: f32,
118 y: f32,
119 z: f32,
120 layer_id: u8,
121 parent_idx: u32,
122 child_count: u16,
123 importance: u8,
124}
125
126fn split_oversized_entry(line: &str) -> Vec<String> {
141 let limit = BLOCK_DATA_SIZE;
142 let mut pieces: Vec<String> = Vec::new();
143 let mut current = String::new();
144
145 for sentence in split_sentences(line) {
146 let sentence = sentence.trim();
147 if sentence.is_empty() {
148 continue;
149 }
150 if sentence.len() > limit {
151 if !current.is_empty() {
154 pieces.push(std::mem::take(&mut current));
155 }
156 for word in sentence.split_whitespace() {
157 if !current.is_empty() && current.len() + word.len() + 1 > limit {
158 pieces.push(std::mem::take(&mut current));
159 }
160 if !current.is_empty() {
161 current.push(' ');
162 }
163 current.push_str(word);
164 }
165 continue;
166 }
167 if !current.is_empty() && current.len() + sentence.len() + 2 > limit {
168 pieces.push(std::mem::take(&mut current));
169 }
170 if !current.is_empty() {
171 current.push(' ');
172 }
173 current.push_str(sentence);
174 }
175 if !current.is_empty() {
176 pieces.push(current);
177 }
178 if pieces.is_empty() {
179 pieces.push(line.to_string());
180 }
181 pieces
182}
183
184fn extract_texts_from_file(path: &Path) -> Vec<(String, u8)> {
187 let mut texts: Vec<(String, u8)> = Vec::new();
188 let raw = match fs::read_to_string(path) {
189 Ok(s) => s,
190 Err(_) => return texts,
191 };
192
193 for chunk in raw.split("\n\n") {
195 let trimmed = chunk.trim();
196 if trimmed.len() > 3 {
197 let (text, importance) = crate::reader::parse_imp_marker(trimmed);
198 texts.push((text.to_string(), importance));
199 }
200 }
201
202 if texts.len() < 2 {
219 texts.clear();
220 for line in raw.lines() {
221 let line = line.trim();
222 if line.is_empty() {
223 continue;
224 }
225 if line.len() > BLOCK_DATA_SIZE {
226 for piece in split_oversized_entry(line) {
232 let (text, importance) = crate::reader::parse_imp_marker(&piece);
233 if text.trim().len() > 3 {
234 texts.push((text.to_string(), importance));
235 }
236 }
237 } else {
238 let (text, importance) = crate::reader::parse_imp_marker(line);
239 if text.trim().len() > 3 {
240 texts.push((text.to_string(), importance));
241 }
242 }
243 }
244 }
245
246 texts
247}
248
249fn split_sentences(text: &str) -> Vec<String> {
251 let mut sentences = Vec::new();
252 let mut current = String::new();
253 let chars: Vec<char> = text.chars().collect();
254 for (i, &ch) in chars.iter().enumerate() {
255 current.push(ch);
256 if !matches!(ch, '.' | '!' | '?' | '\n') || current.trim().len() <= 10 {
257 continue;
258 }
259 let next = chars.get(i + 1).copied();
267 let ends_here = match next {
268 None => true,
269 Some(c) => c.is_whitespace() || c.is_uppercase(),
270 };
271 if ends_here {
272 sentences.push(current.trim().to_string());
273 current = String::new();
274 }
275 }
276 if current.trim().len() > 5 {
277 sentences.push(current.trim().to_string());
278 }
279 sentences
280}
281
282pub fn compute_layers_hash(config: &Config) -> [u8; 32] {
284 let layers_dir = Path::new(&config.paths.layers_dir);
285 let layer_files = &config.memory_layers.layers;
286 let mut sorted_names: Vec<&String> = layer_files.iter().collect();
287 sorted_names.sort();
288 let mut hasher = Sha256::new();
289 for name in &sorted_names {
290 let path = layers_dir.join(format!("{}.txt", name));
291 if let Ok(contents) = fs::read(&path) {
292 hasher.update(&contents);
293 }
294 }
295 let result = hasher.finalize();
296 let mut hash = [0u8; 32];
297 hash.copy_from_slice(&result);
298 hash
299}
300
301pub fn build(config: &Config, force: bool, heavy: bool) -> Result<(), String> {
308 let layers_hash = compute_layers_hash(config);
309
310 if !force {
312 let output_dir = Path::new(&config.paths.output_dir);
313 let meta_path = output_dir.join("meta.bin");
314 if let Ok(meta) = fs::read(&meta_path) {
315 if meta.len() >= 152 && (&meta[0..4] == b"MSC3" || &meta[0..4] == b"MSC4") {
316 let stored_hash = &meta[120..152];
317 if stored_hash == &layers_hash[..] {
318 println!("{}", "Layers unchanged — skipping rebuild".green().bold());
319 return Ok(());
320 }
321 }
322 }
323 }
324
325 println!(
326 "{}",
327 "Building microscope from raw layers (zero JSON)..."
328 .cyan()
329 .bold()
330 );
331
332 let layers_dir = Path::new(&config.paths.layers_dir);
333 let output_dir = Path::new(&config.paths.output_dir);
334
335 let ledger = crate::epistemic::EvidenceLedger::load_or_init(output_dir);
337
338 let old_snapshot = capture_old_snapshot(config);
345 let layers_changed = read_stored_layers_hash(output_dir)
346 .map(|stored| stored != layers_hash)
347 .unwrap_or(true);
348
349 if !output_dir.exists() {
350 fs::create_dir_all(output_dir).map_err(|e| format!("create output dir: {}", e))?;
351 }
352
353 let layer_files = &config.memory_layers.layers;
354
355 let mut layer_texts: Vec<(String, Vec<(String, u8)>)> = Vec::new();
357 for name in layer_files {
358 let path = layers_dir.join(format!("{}.txt", name));
359 let texts = extract_texts_from_file(&path);
360 println!(" {} {}: {} items", ">".green(), name, texts.len());
361 layer_texts.push((name.clone(), texts));
362 }
363
364 let mut blocks: Vec<RawBlock> = Vec::new();
365
366 let identity = "Microscope Memory: 9-depth hierarchical cognitive engine. Binary mmap, sub-microsecond spatial search, Hebbian learning, Merkle integrity.";
368 blocks.push(RawBlock {
369 data: to_block(identity),
370 depth: 0,
371 x: 0.25,
372 y: 0.25,
373 z: 0.25,
374 layer_id: 0,
375 parent_idx: u32::MAX,
376 child_count: layer_files.len() as u16,
377 importance: 8,
378 });
379
380 let sw = config.search.semantic_weight;
382 let depth1_start = blocks.len();
383 for (name, texts) in &layer_texts {
384 let preview: Vec<String> = texts
385 .iter()
386 .take(3)
387 .map(|(t, _)| safe_truncate(t, 40))
388 .collect();
389 let summary = format!("[{}] {} elem. {}", name, texts.len(), preview.join(" | "));
390 let (x, y, z) = content_coords_blended(name, name, sw);
391 blocks.push(RawBlock {
392 data: to_block(&summary),
393 depth: 1,
394 x,
395 y,
396 z,
397 layer_id: layer_to_id(name),
398 parent_idx: 0,
399 child_count: texts.len().div_ceil(5) as u16, importance: 5,
401 });
402 }
403
404 let _depth2_start = blocks.len();
406 let mut depth2_layer_offsets: Vec<(usize, usize)> = Vec::new(); for (li, (name, texts)) in layer_texts.iter().enumerate() {
408 let cluster_start = blocks.len();
409 for ci in (0..texts.len()).step_by(5) {
410 let chunk: Vec<String> = texts[ci..texts.len().min(ci + 5)]
411 .iter()
412 .map(|(t, _)| safe_truncate(t, 40))
413 .collect();
414 let summary = format!("[{} #{}] {}", name, ci / 5, chunk.join(" | "));
415 let (x, y, z) = content_coords_blended(&summary, name, sw);
416 blocks.push(RawBlock {
417 data: to_block(&summary),
418 depth: 2,
419 x,
420 y,
421 z,
422 layer_id: layer_to_id(name),
423 parent_idx: (depth1_start + li) as u32,
424 child_count: chunk.len() as u16,
425 importance: 5,
426 });
427 }
428 depth2_layer_offsets.push((cluster_start, blocks.len() - cluster_start));
429 }
430
431 let depth3_start = blocks.len();
433 let mut depth3_positions: Vec<(f32, f32, f32)> = Vec::new();
434 for (li, (name, texts)) in layer_texts.iter().enumerate() {
435 for (ti, (text, item_importance)) in texts.iter().enumerate() {
436 let (x, y, z) = content_coords_blended(text, name, sw);
437 let cluster_idx = ti / 5;
438 let (d2_start, d2_count) = depth2_layer_offsets[li];
439 let parent = if cluster_idx < d2_count {
440 (d2_start + cluster_idx) as u32
441 } else {
442 u32::MAX
443 };
444
445 blocks.push(RawBlock {
446 data: to_block(text),
447 depth: 3,
448 x,
449 y,
450 z,
451 layer_id: layer_to_id(name),
452 parent_idx: parent,
453 child_count: 0, importance: *item_importance,
455 });
456 depth3_positions.push((x, y, z));
457 }
458 }
459
460 let _depth4_start = blocks.len();
462 let mut depth4_parents: Vec<usize> = Vec::new();
463
464 let d4_results: Vec<Vec<RawBlock>> = (depth3_start..(depth3_start + depth3_positions.len()))
465 .into_par_iter()
466 .map(|d3i| {
467 let text = std::str::from_utf8(&blocks[d3i].data).unwrap_or("");
468 let sentences = split_sentences(text);
469 let mut local_blocks = Vec::new();
470 for sent in &sentences {
471 if sent.len() < 10 {
472 continue;
473 }
474 let (px, py, pz) = depth3_positions[d3i - depth3_start];
475 let h = sent
476 .as_bytes()
477 .iter()
478 .fold(0u64, |a, &b| a.wrapping_mul(31).wrapping_add(b as u64));
479 let ox = ((h & 0xFF) as f32 - 128.0) / 25500.0;
480 let oy = (((h >> 8) & 0xFF) as f32 - 128.0) / 25500.0;
481 let oz = (((h >> 16) & 0xFF) as f32 - 128.0) / 25500.0;
482
483 local_blocks.push(RawBlock {
484 data: to_block(sent),
485 depth: 4,
486 x: px + ox,
487 y: py + oy,
488 z: pz + oz,
489 layer_id: blocks[d3i].layer_id,
490 parent_idx: d3i as u32,
491 child_count: 0,
492 importance: 5,
493 });
494 }
495 local_blocks
496 })
497 .collect();
498
499 for (i, local) in d4_results.into_iter().enumerate() {
500 let d3i = depth3_start + i;
501 blocks[d3i].child_count = local.len() as u16;
502 for b in local {
503 blocks.push(b);
504 depth4_parents.push(blocks.len() - 1);
505 }
506 }
507
508 let mut depth5_parents: Vec<usize> = Vec::new();
510 let depth4_parents_clone = depth4_parents.clone();
511 let d5_results: Vec<Vec<RawBlock>> = depth4_parents
512 .into_par_iter()
513 .map(|d4i| {
514 let text_owned = String::from_utf8_lossy(&blocks[d4i].data).to_string();
515 let px = blocks[d4i].x;
516 let py = blocks[d4i].y;
517 let pz = blocks[d4i].z;
518 let lid = blocks[d4i].layer_id;
519
520 let tokens: Vec<String> = text_owned
521 .split_whitespace()
522 .take(8)
523 .map(|s| s.to_string())
524 .collect();
525 let mut local_blocks = Vec::new();
526 for tok in &tokens {
527 if tok.len() < 2 {
528 continue;
529 }
530 let h = tok
531 .as_bytes()
532 .iter()
533 .fold(0u64, |a, &b| a.wrapping_mul(31).wrapping_add(b as u64));
534 let ox = ((h & 0xFF) as f32 - 128.0) / 255000.0;
535 let oy = (((h >> 8) & 0xFF) as f32 - 128.0) / 255000.0;
536 let oz = (((h >> 16) & 0xFF) as f32 - 128.0) / 255000.0;
537
538 local_blocks.push(RawBlock {
539 data: to_block(tok),
540 depth: 5,
541 x: px + ox,
542 y: py + oy,
543 z: pz + oz,
544 layer_id: lid,
545 parent_idx: d4i as u32,
546 child_count: 0,
547 importance: 5,
548 });
549 }
550 local_blocks
551 })
552 .collect();
553
554 for (i, local) in d5_results.into_iter().enumerate() {
555 let d4i = depth4_parents_clone[i];
556 blocks[d4i].child_count = local.len() as u16;
557 for b in local {
558 blocks.push(b);
559 depth5_parents.push(blocks.len() - 1);
560 }
561 }
562
563 let mut depth6_parents: Vec<usize> = Vec::new();
565 let d6_results: Vec<Vec<RawBlock>> = depth5_parents
566 .clone()
567 .into_par_iter()
568 .map(|d5i| {
569 let text_owned = String::from_utf8_lossy(&blocks[d5i].data).to_string();
570 let px = blocks[d5i].x;
571 let py = blocks[d5i].y;
572 let pz = blocks[d5i].z;
573 let lid = blocks[d5i].layer_id;
574
575 let chars: Vec<char> = text_owned.chars().collect();
576 if chars.len() < 3 {
577 return vec![];
578 }
579 let chunk_size = 3.max(chars.len() / 3).min(5);
580 let mut local_blocks = Vec::new();
581 for chunk in chars.chunks(chunk_size) {
582 let syl: String = chunk.iter().collect();
583 if syl.trim().is_empty() {
584 continue;
585 }
586 let h = syl
587 .as_bytes()
588 .iter()
589 .fold(0u64, |a, &b| a.wrapping_mul(37).wrapping_add(b as u64));
590 let ox = ((h & 0xFF) as f32 - 128.0) / 2550000.0;
591 let oy = (((h >> 8) & 0xFF) as f32 - 128.0) / 2550000.0;
592 let oz = (((h >> 16) & 0xFF) as f32 - 128.0) / 2550000.0;
593
594 local_blocks.push(RawBlock {
595 data: to_block(&syl),
596 depth: 6,
597 x: px + ox,
598 y: py + oy,
599 z: pz + oz,
600 layer_id: lid,
601 parent_idx: d5i as u32,
602 child_count: 0,
603 importance: 5,
604 });
605 }
606 local_blocks
607 })
608 .collect();
609
610 for (i, local) in d6_results.into_iter().enumerate() {
611 let d5i = depth5_parents[i];
612 blocks[d5i].child_count = local.len() as u16;
613 for b in local {
614 blocks.push(b);
615 depth6_parents.push(blocks.len() - 1);
616 }
617 }
618
619 let mut depth7_parents: Vec<usize> = Vec::new();
621 let d7_results: Vec<Vec<RawBlock>> = depth6_parents
622 .clone()
623 .into_par_iter()
624 .map(|d6i| {
625 let text_owned = String::from_utf8_lossy(&blocks[d6i].data).to_string();
626 let px = blocks[d6i].x;
627 let py = blocks[d6i].y;
628 let pz = blocks[d6i].z;
629 let lid = blocks[d6i].layer_id;
630
631 let mut local_blocks = Vec::new();
632 for ch in text_owned.chars() {
633 if ch.is_whitespace() {
634 continue;
635 }
636 let h = (ch as u64).wrapping_mul(0x517cc1b727220a95);
637 let ox = ((h & 0xFF) as f32 - 128.0) / 25500000.0;
638 let oy = (((h >> 8) & 0xFF) as f32 - 128.0) / 25500000.0;
639 let oz = (((h >> 16) & 0xFF) as f32 - 128.0) / 25500000.0;
640
641 let ch_str = ch.to_string();
642 local_blocks.push(RawBlock {
643 data: to_block(&ch_str),
644 depth: 7,
645 x: px + ox,
646 y: py + oy,
647 z: pz + oz,
648 layer_id: lid,
649 parent_idx: d6i as u32,
650 child_count: 0,
651 importance: 5,
652 });
653 }
654 local_blocks
655 })
656 .collect();
657
658 for (i, local) in d7_results.into_iter().enumerate() {
659 let d6i = depth6_parents[i];
660 blocks[d6i].child_count = local.len() as u16;
661 for b in local {
662 blocks.push(b);
663 depth7_parents.push(blocks.len() - 1);
664 }
665 }
666
667 let d8_results: Vec<Vec<RawBlock>> = depth7_parents
669 .clone()
670 .into_par_iter()
671 .map(|d7i| {
672 let text_owned = String::from_utf8_lossy(&blocks[d7i].data).to_string();
673 let px = blocks[d7i].x;
674 let py = blocks[d7i].y;
675 let pz = blocks[d7i].z;
676 let lid = blocks[d7i].layer_id;
677
678 let bytes = text_owned.as_bytes();
679 let mut local_blocks = Vec::new();
680 for &byte in bytes {
681 let hex = format!("0x{:02X}", byte);
682 let h = (byte as u64).wrapping_mul(0x9E3779B97F4A7C15);
683 let ox = ((h & 0xFF) as f32 - 128.0) / 255000000.0;
684 let oy = (((h >> 8) & 0xFF) as f32 - 128.0) / 255000000.0;
685 let oz = (((h >> 16) & 0xFF) as f32 - 128.0) / 255000000.0;
686
687 local_blocks.push(RawBlock {
688 data: to_block(&hex),
689 depth: 8,
690 x: px + ox,
691 y: py + oy,
692 z: pz + oz,
693 layer_id: lid,
694 parent_idx: d7i as u32,
695 child_count: 0, importance: 5,
697 });
698 }
699 local_blocks
700 })
701 .collect();
702
703 for (i, local) in d8_results.into_iter().enumerate() {
704 let d7i = depth7_parents[i];
705 blocks[d7i].child_count = local.len() as u16;
706 for b in local {
707 blocks.push(b);
708 }
709 }
710
711 let n = blocks.len();
712 println!("\n {} blocks total", n);
713
714 let mut indices: Vec<usize> = (0..n).collect();
717 indices.sort_by_key(|&i| blocks[i].depth);
718
719 let mut sort_old_to_new = vec![0u32; n];
721 for (new_i, &old_i) in indices.iter().enumerate() {
722 sort_old_to_new[old_i] = new_i as u32;
723 }
724
725 let content_old_to_new = compute_content_remap(&old_snapshot, &blocks);
731
732 let output_dir = Path::new(&config.paths.output_dir);
734 fs::create_dir_all(output_dir).ok();
735
736 let hdr_path = output_dir.join("microscope.bin");
737 let dat_path = output_dir.join("data.bin");
738 let meta_path = output_dir.join("meta.bin");
739
740 let mut hdr_file = BufWriter::new(
741 fs::File::create(&hdr_path).map_err(|e| format!("create microscope.bin: {}", e))?,
742 );
743 let mut dat_file =
744 BufWriter::new(fs::File::create(&dat_path).map_err(|e| format!("create data.bin: {}", e))?);
745
746 let mut depth_ranges: Vec<(u32, u32)> = vec![(0, 0); 9];
747 let mut cur_depth: u8 = 0;
748 let mut range_start: u32 = 0;
749
750 for (new_i, &old_i) in indices.iter().enumerate() {
751 let b = &blocks[old_i];
752 let offset = dat_file
753 .stream_position()
754 .map_err(|e| format!("data.bin stream_position: {}", e))? as u32;
755 let len = b.data.len().min(BLOCK_DATA_SIZE) as u16;
756 dat_file
757 .write_all(&b.data[..len as usize])
758 .map_err(|e| format!("write data.bin: {}", e))?;
759
760 let parent = if b.parent_idx == u32::MAX {
761 u32::MAX
762 } else {
763 sort_old_to_new[b.parent_idx as usize]
764 };
765
766 let crc = crc16_ccitt(&b.data[..len as usize]);
767 let hdr = BlockHeader {
768 x: b.x,
769 y: b.y,
770 z: b.z,
771 zoom: b.depth as f32 / 8.0,
772 depth: b.depth,
773 layer_id: b.layer_id,
774 data_offset: offset,
775 data_len: len,
776 parent_idx: parent,
777 child_count: b.child_count,
778 crc16: crc.to_le_bytes(),
779 project_id: config.project_id,
780 importance: b.importance,
781 flags: {
782 let text_str = std::str::from_utf8(&b.data[..len as usize])
784 .unwrap_or("")
785 .trim_end_matches('\0')
786 .trim();
787 let ch = crate::epistemic::content_hash(text_str);
788 match ledger.records.get(&ch) {
789 Some(rec) => rec.class as u8 & 0x07,
790 None => 0,
791 }
792 },
793 };
794
795 let bytes: &[u8] = unsafe {
796 std::slice::from_raw_parts(&hdr as *const BlockHeader as *const u8, HEADER_SIZE)
797 };
798 hdr_file
799 .write_all(bytes)
800 .map_err(|e| format!("write microscope.bin: {}", e))?;
801
802 if b.depth != cur_depth {
804 depth_ranges[cur_depth as usize] = (range_start, new_i as u32 - range_start);
805 range_start = new_i as u32;
806 cur_depth = b.depth;
807 }
808 }
809 depth_ranges[cur_depth as usize] = (range_start, n as u32 - range_start);
810 hdr_file
811 .flush()
812 .map_err(|e| format!("flush microscope.bin: {}", e))?;
813 dat_file
814 .flush()
815 .map_err(|e| format!("flush data.bin: {}", e))?;
816
817 #[cfg(feature = "compression")]
819 if config.performance.compression {
820 let raw_data =
821 fs::read(&dat_path).map_err(|e| format!("read data.bin for compression: {}", e))?;
822 let raw_size = raw_data.len();
823 let compressed = zstd::encode_all(std::io::Cursor::new(&raw_data), 3)
824 .map_err(|e| format!("zstd compress: {}", e))?;
825 let comp_size = compressed.len();
826 let zst_path = output_dir.join("data.bin.zst");
827 fs::write(&zst_path, &compressed).map_err(|e| format!("write data.bin.zst: {}", e))?;
828 let ratio = if comp_size > 0 {
829 raw_size as f64 / comp_size as f64
830 } else {
831 0.0
832 };
833 println!(
834 " {}: {} → {} bytes ({:.1}x ratio)",
835 "zstd".green(),
836 raw_size,
837 comp_size,
838 ratio,
839 );
840 }
841
842 let merkle_path = output_dir.join("merkle.bin");
844 hdr_file
846 .flush()
847 .map_err(|e| format!("flush microscope.bin: {}", e))?;
848 dat_file
849 .flush()
850 .map_err(|e| format!("flush data.bin: {}", e))?;
851
852 let dat_bytes = fs::read(&dat_path).map_err(|e| format!("read data.bin for merkle: {}", e))?;
853 let hdr_bytes =
854 fs::read(&hdr_path).map_err(|e| format!("read microscope.bin for merkle: {}", e))?;
855 let mut leaf_slices: Vec<&[u8]> = Vec::with_capacity(n);
856 for i in 0..n {
857 let hdr_off = i * HEADER_SIZE;
858 let data_offset =
859 u32::from_le_bytes(hdr_bytes[hdr_off + 18..hdr_off + 22].try_into().unwrap()) as usize;
860 let data_len =
861 u16::from_le_bytes(hdr_bytes[hdr_off + 22..hdr_off + 24].try_into().unwrap()) as usize;
862 if data_offset + data_len <= dat_bytes.len() {
863 leaf_slices.push(&dat_bytes[data_offset..data_offset + data_len]);
864 } else {
865 leaf_slices.push(&[]);
866 }
867 }
868
869 let merkle_tree = merkle::MerkleTree::build(&leaf_slices);
870 let merkle_tmp = merkle_path.with_extension("bin.tmp");
871 fs::write(&merkle_tmp, merkle_tree.to_bytes())
872 .map_err(|e| format!("write merkle.bin: {}", e))?;
873 fs::rename(&merkle_tmp, &merkle_path).map_err(|e| format!("rename merkle.bin: {}", e))?;
874 println!(
875 " {}: {} leaves, root={}",
876 "merkle".green(),
877 merkle_tree.leaf_count,
878 hex_str(&merkle_tree.root)
879 );
880
881 let mut meta_buf = Vec::with_capacity(META_HEADER_SIZE + 9 * DEPTH_ENTRY_SIZE + 32 + 32);
883 meta_buf.extend_from_slice(b"MSC4"); meta_buf.extend_from_slice(&4u32.to_le_bytes()); meta_buf.extend_from_slice(&(n as u32).to_le_bytes()); meta_buf.extend_from_slice(&9u32.to_le_bytes()); for &(start, count) in &depth_ranges {
888 meta_buf.extend_from_slice(&start.to_le_bytes());
889 meta_buf.extend_from_slice(&count.to_le_bytes());
890 }
891 meta_buf.extend_from_slice(&merkle_tree.root); meta_buf.extend_from_slice(&layers_hash); let meta_tmp = meta_path.with_extension("bin.tmp");
894 fs::write(&meta_tmp, &meta_buf).map_err(|e| format!("write meta.bin: {}", e))?;
895 fs::rename(&meta_tmp, meta_path).map_err(|e| format!("rename meta.bin: {}", e))?;
896
897 let hdr_size = n * HEADER_SIZE;
899 let dat_size = dat_file.stream_position().unwrap_or(0) as usize; let meta_size = meta_buf.len();
901 println!(
902 "\n {}: {} bytes ({:.1} KB)",
903 "headers".green(),
904 hdr_size,
905 hdr_size as f64 / 1024.0
906 );
907 println!(
908 " {}: {} bytes ({:.1} KB)",
909 "data".green(),
910 dat_size,
911 dat_size as f64 / 1024.0
912 );
913 println!(" {}: {} bytes", "meta".green(), meta_size);
914 println!(
915 " {}: {:.1} KB",
916 "TOTAL".yellow().bold(),
917 (hdr_size + dat_size + meta_size) as f64 / 1024.0
918 );
919
920 let fits = if hdr_size < 32768 {
921 "L1d (32KB)"
922 } else if hdr_size < 262144 {
923 "L2 (256KB)"
924 } else {
925 "L3"
926 };
927 println!(" cache: {}", fits.green().bold());
928
929 for (d, &(_start, count)) in depth_ranges.iter().enumerate() {
930 println!(" Depth {}: {:>5} blocks", d, count);
931 }
932
933 if heavy {
938 if config.embedding.provider != "none" {
940 println!("\n Building embedding index...");
941 let emb_path = output_dir.join("embeddings.bin");
942 let reader = MicroscopeReader::open(config)?;
943 let max_depth = config.embedding.max_depth;
944
945 let provider: Box<dyn crate::embeddings::EmbeddingProvider> =
946 crate::embeddings::provider_from_config(&config.embedding, config.embedding.dim);
947
948 match crate::embedding_index::build_embedding_index(
949 &*provider, &reader, max_depth, &emb_path,
950 ) {
951 Ok(()) => println!(" {} embeddings.bin built", "OK".green()),
952 Err(e) => eprintln!(" {} embedding build: {}", "ERR".red(), e),
953 }
954 }
955
956 let text_index_result = (|| -> Result<(), String> {
958 let reader = MicroscopeReader::open(config)?;
959 crate::text_index::build_text_index(
960 (0..reader.block_count).map(|i| reader.text(i)),
961 reader.block_count,
962 &output_dir.join("text_index.bin"),
963 )
964 })();
965 match text_index_result {
966 Ok(()) => println!(" {} text_index.bin built", "OK".green()),
967 Err(e) => eprintln!(" {} text index build: {}", "ERR".red(), e),
968 }
969
970 let hebb_path = output_dir.join("activations.bin");
972 if hebb_path.exists() {
973 let hebb = crate::hebbian::HebbianState::load_or_init(output_dir, n);
974 let drifted = hebb
975 .activations
976 .iter()
977 .filter(|r| {
978 r.drift_x.abs() > 0.001 || r.drift_y.abs() > 0.001 || r.drift_z.abs() > 0.001
979 })
980 .count();
981
982 if drifted > 0 {
983 apply_hebbian_deltas(output_dir, &hebb, n)?;
984 println!(
985 " {} Hebbian deltas applied to {} blocks",
986 "HEBBIAN".magenta(),
987 drifted
988 );
989 }
990 }
991
992 {
994 let reader = MicroscopeReader::open(config)?;
995 let texts: Vec<&str> = (0..reader.block_count).map(|i| reader.text(i)).collect();
996 let table = crate::fingerprint::LinkTable::build(&texts);
997 table.save(output_dir)?;
998 let stats = table.stats();
999 println!(
1000 " {} {} links across {} blocks",
1001 "FINGERPRINT".cyan(),
1002 stats.link_count,
1003 stats.block_count
1004 );
1005 }
1006 } else if layers_changed {
1007 for name in [
1021 "embeddings.bin",
1022 "links.bin",
1023 "fingerprints.idx",
1024 "text_index.bin",
1025 ] {
1026 let p = output_dir.join(name);
1027 if p.exists() {
1028 let _ = fs::remove_file(&p);
1029 }
1030 }
1031 println!(
1032 " {} heavy sidecars skipped (embeddings, fingerprint); stale copies removed",
1033 "LIGHT".cyan()
1034 );
1035 } else {
1036 println!(
1037 " {} layers unchanged — heavy sidecars kept",
1038 "LIGHT".cyan()
1039 );
1040 }
1041
1042 {
1044 let reader = MicroscopeReader::open(config)?;
1045 crate::reader::build_emotions_from_log(output_dir, &reader)?;
1046 }
1047
1048 {
1055 let mut hebb = crate::hebbian::HebbianState::load_or_init(output_dir, n);
1056 hebb.remap_indexes(&content_old_to_new);
1057 if hebb.activations.len() < n {
1060 hebb.activations
1061 .resize(n, crate::hebbian::ActivationRecord::default());
1062 }
1063 hebb.save(output_dir)
1064 .map_err(|e| format!("save hebbian after remap: {e}"))?;
1065 println!(" {} Hebbian state remapped (content)", "REMAP".magenta());
1066 }
1067 {
1068 let mut pc = crate::predictive_cache::PredictiveCache::load_or_init(output_dir);
1069 pc.remap_indexes(&content_old_to_new);
1070 pc.save(output_dir)
1071 .map_err(|e| format!("save predictive_cache after remap: {e}"))?;
1072 println!(" {} PredictiveCache remapped", "REMAP".magenta());
1073 }
1074 {
1075 let mut mirror = crate::mirror::MirrorState::load_or_init(output_dir);
1076 mirror.remap_indexes(&content_old_to_new);
1077 mirror
1078 .save(output_dir)
1079 .map_err(|e| format!("save mirror after remap: {e}"))?;
1080 println!(" {} MirrorState remapped", "REMAP".magenta());
1081 }
1082 {
1083 let mut spaced = crate::spaced_repetition::SpacedRepetition::load_or_init(output_dir);
1084 spaced.remap_indexes(&content_old_to_new);
1085 spaced
1086 .save(output_dir)
1087 .map_err(|e| format!("save spaced after remap: {e}"))?;
1088 println!(" {} SpacedRepetition remapped", "REMAP".magenta());
1089 }
1090 {
1091 let mut archetypes = crate::archetype::ArchetypeState::load_or_init(output_dir);
1092 archetypes.remap_indexes(&content_old_to_new);
1093 archetypes
1094 .save(output_dir)
1095 .map_err(|e| format!("save archetypes after remap: {e}"))?;
1096 println!(" {} Archetypes remapped", "REMAP".magenta());
1097 }
1098 {
1099 let mut tg = crate::thought_graph::ThoughtGraphState::load_or_init(output_dir);
1100 tg.remap_indexes(&content_old_to_new);
1101 tg.save(output_dir)
1102 .map_err(|e| format!("save thought_graph after remap: {e}"))?;
1103 println!(" {} ThoughtPatterns remapped", "REMAP".magenta());
1104 }
1105 {
1106 let mut eureka = crate::eureka::EurekaLog::load_or_init(output_dir);
1107 eureka.remap_indexes(&content_old_to_new);
1108 eureka
1109 .save(output_dir)
1110 .map_err(|e| format!("save eureka after remap: {e}"))?;
1111 println!(" {} EurekaLog remapped", "REMAP".magenta());
1112 }
1113
1114 {
1123 let tix_path = output_dir.join("text_index.bin");
1124 if !tix_path.exists() {
1125 let tix_result = (|| -> Result<(), String> {
1126 let reader = MicroscopeReader::open(config)?;
1127 crate::text_index::build_text_index(
1128 (0..reader.block_count).map(|i| reader.text(i)),
1129 reader.block_count,
1130 &tix_path,
1131 )
1132 })();
1133 match tix_result {
1134 Ok(()) => println!(" {} text_index.bin regenerated", "OK".green()),
1135 Err(e) => eprintln!(" {} text index regenerate: {}", "ERR".red(), e),
1136 }
1137 }
1138 }
1139
1140 println!("\n{}", "ZERO JSON. Pure binary. Done.".green().bold());
1141 Ok(())
1142}
1143
1144fn apply_hebbian_deltas(
1146 output_dir: &Path,
1147 hebb: &crate::hebbian::HebbianState,
1148 block_count: usize,
1149) -> Result<(), String> {
1150 let hdr_path = output_dir.join("microscope.bin");
1151 let mut data = fs::read(&hdr_path).map_err(|e| format!("read microscope.bin: {}", e))?;
1152
1153 for i in 0..block_count.min(hebb.activations.len()) {
1154 let rec = &hebb.activations[i];
1155 if rec.drift_x.abs() < 0.001 && rec.drift_y.abs() < 0.001 && rec.drift_z.abs() < 0.001 {
1156 continue;
1157 }
1158
1159 let off = i * HEADER_SIZE;
1160 if off + 12 > data.len() {
1161 break;
1162 }
1163
1164 let x = f32::from_le_bytes(data[off..off + 4].try_into().unwrap());
1166 let y = f32::from_le_bytes(data[off + 4..off + 8].try_into().unwrap());
1167 let z = f32::from_le_bytes(data[off + 8..off + 12].try_into().unwrap());
1168
1169 let new_x = x + rec.drift_x;
1171 let new_y = y + rec.drift_y;
1172 let new_z = z + rec.drift_z;
1173
1174 data[off..off + 4].copy_from_slice(&new_x.to_le_bytes());
1175 data[off + 4..off + 8].copy_from_slice(&new_y.to_le_bytes());
1176 data[off + 8..off + 12].copy_from_slice(&new_z.to_le_bytes());
1177 }
1178
1179 let hdr_tmp = hdr_path.with_extension("bin.tmp");
1180 fs::write(&hdr_tmp, &data).map_err(|e| format!("write microscope.bin: {}", e))?;
1181 fs::rename(&hdr_tmp, &hdr_path).map_err(|e| format!("rename microscope.bin: {}", e))?;
1182
1183 let mut hebb_clone = crate::hebbian::HebbianState {
1185 activations: hebb.activations.clone(),
1186 coactivations: hebb.coactivations.clone(),
1187 fingerprints: hebb.fingerprints.clone(),
1188 };
1189 for rec in &mut hebb_clone.activations {
1190 rec.drift_x = 0.0;
1191 rec.drift_y = 0.0;
1192 rec.drift_z = 0.0;
1193 }
1194 hebb_clone
1195 .save(output_dir)
1196 .map_err(|e| format!("save cleared Hebbian: {}", e))
1197}
1198
1199struct OldBlockSnapshot {
1202 depth: u8,
1203 layer_id: u8,
1204 text: Vec<u8>,
1205}
1206
1207fn capture_old_snapshot(config: &Config) -> Option<Vec<OldBlockSnapshot>> {
1209 let reader = MicroscopeReader::open(config).ok()?;
1210 let snapshots: Vec<OldBlockSnapshot> = (0..reader.block_count)
1211 .map(|i| {
1212 let h = reader.header(i);
1213 OldBlockSnapshot {
1214 depth: h.depth,
1215 layer_id: h.layer_id,
1216 text: reader.text(i).as_bytes().to_vec(),
1217 }
1218 })
1219 .collect();
1220 Some(snapshots)
1221}
1222
1223fn read_stored_layers_hash(output_dir: &Path) -> Option<[u8; 32]> {
1225 let meta = fs::read(output_dir.join("meta.bin")).ok()?;
1226 if meta.len() < 152 || &meta[0..4] != b"MSC4" {
1227 return None;
1228 }
1229 let mut hash = [0u8; 32];
1230 hash.copy_from_slice(&meta[120..152]);
1231 Some(hash)
1232}
1233
1234fn compute_content_remap(old: &Option<Vec<OldBlockSnapshot>>, new_blocks: &[RawBlock]) -> Vec<u32> {
1240 let old = match old {
1241 Some(v) => v,
1242 None => return Vec::new(),
1243 };
1244 if old.is_empty() {
1245 return Vec::new();
1246 }
1247
1248 let mut index: HashMap<(u8, u8, Vec<u8>), Vec<u32>> = HashMap::new();
1250 for (j, b) in new_blocks.iter().enumerate() {
1251 index
1252 .entry((b.depth, b.layer_id, b.data.clone()))
1253 .or_default()
1254 .push(j as u32);
1255 }
1256
1257 let mut cursor: HashMap<(u8, u8, Vec<u8>), usize> = HashMap::new();
1258 let mut map = vec![u32::MAX; old.len()];
1259 for (i, s) in old.iter().enumerate() {
1260 let key = (s.depth, s.layer_id, s.text.clone());
1261 if let Some(list) = index.get(&key) {
1262 let c = cursor.entry(key).or_insert(0);
1263 if *c < list.len() {
1264 map[i] = list[*c];
1265 *c += 1;
1266 }
1267 }
1268 }
1269 map
1270}
1271
1272#[cfg(test)]
1273mod tests {
1274 use super::*;
1275
1276 fn snap(depth: u8, layer_id: u8, text: &str) -> OldBlockSnapshot {
1277 OldBlockSnapshot {
1278 depth,
1279 layer_id,
1280 text: text.as_bytes().to_vec(),
1281 }
1282 }
1283
1284 fn block(depth: u8, layer_id: u8, text: &str) -> RawBlock {
1285 RawBlock {
1286 data: to_block(text),
1287 depth,
1288 x: 0.0,
1289 y: 0.0,
1290 z: 0.0,
1291 layer_id,
1292 parent_idx: u32::MAX,
1293 child_count: 0,
1294 importance: 5,
1295 }
1296 }
1297
1298 #[test]
1302 fn content_remap_follows_retention_shift() {
1303 let old = Some(vec![
1304 snap(3, 1, "Alpha first entry"),
1305 snap(3, 1, "Beta second entry"),
1306 snap(3, 1, "Gamma third entry"),
1307 ]);
1308 let new_blocks = vec![
1310 block(0, 0, "identity"),
1311 block(3, 1, "Beta second entry"),
1312 block(3, 1, "Gamma third entry"),
1313 block(4, 1, "Beta second entry"),
1314 ];
1315 let map = compute_content_remap(&old, &new_blocks);
1316 assert_eq!(map[0], u32::MAX);
1318 assert_eq!(map[1], 1);
1322 assert_eq!(map[2], 2);
1323 }
1324
1325 #[test]
1327 fn content_remap_is_identity_for_unchanged_content() {
1328 let old = Some(vec![
1329 snap(3, 1, "Alpha first entry"),
1330 snap(3, 1, "Beta second entry"),
1331 ]);
1332 let new_blocks = vec![
1333 block(0, 0, "identity"),
1334 block(3, 1, "Alpha first entry"),
1335 block(3, 1, "Beta second entry"),
1336 ];
1337 let map = compute_content_remap(&old, &new_blocks);
1338 assert_eq!(map, vec![1, 2]);
1339 }
1340
1341 #[test]
1343 fn content_remap_empty_without_old_index() {
1344 let map = compute_content_remap(&None, &[block(3, 1, "x")]);
1345 assert!(map.is_empty());
1346 }
1347
1348 #[test]
1356 fn oversized_entry_is_split_without_losing_bytes() {
1357 let sentence = "The quick brown fox jumps over the lazy dog. ";
1358 let entry = sentence.repeat(400);
1362 assert!(entry.len() > BLOCK_DATA_SIZE);
1363
1364 let pieces = split_oversized_entry(entry.trim());
1365 assert!(pieces.len() > 1, "expected the entry to be split");
1366 for p in &pieces {
1367 assert!(
1368 p.len() <= BLOCK_DATA_SIZE,
1369 "piece of {} bytes exceeds the block size",
1370 p.len()
1371 );
1372 }
1373
1374 let rejoined = pieces.join(" ");
1375 let original_words: Vec<&str> = entry.split_whitespace().collect();
1376 let rejoined_words: Vec<&str> = rejoined.split_whitespace().collect();
1377 assert_eq!(
1378 original_words, rejoined_words,
1379 "splitting changed the content of the entry"
1380 );
1381 }
1382
1383 #[test]
1385 fn oversized_single_sentence_is_split_on_whitespace() {
1386 let entry = "word ".repeat(4000); assert!(entry.len() > BLOCK_DATA_SIZE);
1388 let pieces = split_oversized_entry(entry.trim());
1389 assert!(pieces.len() > 1);
1390 for p in &pieces {
1391 assert!(p.len() <= BLOCK_DATA_SIZE);
1392 }
1393 assert_eq!(pieces.join(" "), entry.trim());
1394 }
1395
1396 #[test]
1398 fn short_entry_is_returned_whole() {
1399 let entry = "A short fact that fits in a block. Another sentence.";
1400 let pieces = split_oversized_entry(entry);
1401 assert_eq!(pieces, vec![entry.to_string()]);
1402 }
1403
1404 #[test]
1416 fn one_line_per_entry_is_not_packed_into_shared_blocks() {
1417 let n = 40usize;
1418 let mut file = String::new();
1419 for i in 0..n {
1420 file.push_str(&format!("document number {} with some body text\n", i));
1421 }
1422 let tmp = tempfile::tempdir().expect("tempdir");
1423 let path = tmp.path().join("long_term.txt");
1424 fs::write(&path, &file).unwrap();
1425
1426 let texts = extract_texts_from_file(&path);
1427 assert_eq!(
1428 texts.len(),
1429 n,
1430 "one line must be one block; {} lines produced {} blocks",
1431 n,
1432 texts.len()
1433 );
1434 for (i, (text, _)) in texts.iter().enumerate() {
1435 assert!(
1436 text.contains(&format!("document number {}", i)),
1437 "block {} does not hold its own entry: {:?}",
1438 i,
1439 text
1440 );
1441 }
1442 }
1443}