Rebuild session indexes from stored provenance graphs. Idempotent: existing `(session, provenance)` pairs are skipped, so a rebuilt repository never gains duplicate turns. Corrupt provenance files are reported in the result rather than aborting the rebuild. Returns `(indexed_count, skipped_existing, corrupt_count)`.
(&self)
| 418 | /// files are reported in the result rather than aborting the rebuild. |
| 419 | /// Returns `(indexed_count, skipped_existing, corrupt_count)`. |
| 420 | pub fn rebuild_session_index(&self) -> Result<(usize, usize, usize), RepositoryError> { |
| 421 | let mut indexed = 0usize; |
| 422 | let mut skipped = 0usize; |
| 423 | let mut corrupt = 0usize; |
| 424 | |
| 425 | // Collect (hash, graph) pairs first so each write txn is short-lived. |
| 426 | let mut graphs: Vec<(Hash, atomic_core::change::ProvenanceGraph)> = Vec::new(); |
| 427 | for result in self.change_store.iter_provenance_graphs() { |
| 428 | match result { |
| 429 | Ok(hash) => match self.load_provenance_graph(&hash) { |
| 430 | Ok(graph) => graphs.push((hash, graph)), |
| 431 | Err(_) => corrupt += 1, |
| 432 | }, |
| 433 | Err(_) => corrupt += 1, |
| 434 | } |
| 435 | } |
| 436 | |
| 437 | // Group by session. The core index derives canonical turn order from |
| 438 | // the complete set, independent of this ingestion order. |
| 439 | let mut by_session: std::collections::BTreeMap< |
| 440 | String, |
| 441 | Vec<(Hash, atomic_core::change::ProvenanceGraph)>, |
| 442 | > = std::collections::BTreeMap::new(); |
| 443 | let mut head_manifests = std::collections::BTreeMap::new(); |
| 444 | let mut fork_parents = std::collections::BTreeMap::new(); |
| 445 | for (hash, graph) in graphs { |
| 446 | by_session |
| 447 | .entry(graph.session_id.clone()) |
| 448 | .or_default() |
| 449 | .push((hash, graph)); |
| 450 | } |
| 451 | // Forked children can contain only inherited turns, whose provenance |
| 452 | // files still name the parent session. Include both existing ledgers |
| 453 | // and portable manifests so rebuild can create or migrate the child. |
| 454 | { |
| 455 | let txn = self |
| 456 | .pristine |
| 457 | .read_txn() |
| 458 | .map_err(|e| RepositoryError::Database(e.to_string()))?; |
| 459 | for record in txn |
| 460 | .list_session_records() |
| 461 | .map_err(|e| RepositoryError::Database(e.to_string()))? |
| 462 | { |
| 463 | if record.turn_count > 0 { |
| 464 | by_session.entry(record.session_id).or_default(); |
| 465 | } |
| 466 | } |
| 467 | for (head_session_id, head) in txn |
| 468 | .list_session_heads() |
| 469 | .map_err(|e| RepositoryError::Database(e.to_string()))? |
| 470 | { |
| 471 | match txn.get_session_manifest(&head) { |
| 472 | Ok(Some(manifest)) if manifest.session_id == head_session_id => { |
| 473 | if let (Some(parent_hash), Some(fork_turn)) = |
| 474 | (manifest.parent_session, manifest.fork_turn) |
| 475 | { |
| 476 | if let Ok(Some(parent)) = txn.get_session_manifest(&parent_hash) { |
| 477 | fork_parents.insert( |