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)
| 398 | /// files are reported in the result rather than aborting the rebuild. |
| 399 | /// Returns `(indexed_count, skipped_existing, corrupt_count)`. |
| 400 | pub fn rebuild_session_index(&self) -> Result<(usize, usize, usize), RepositoryError> { |
| 401 | let mut indexed = 0usize; |
| 402 | let mut skipped = 0usize; |
| 403 | let mut corrupt = 0usize; |
| 404 | |
| 405 | // Collect (hash, graph) pairs first so each write txn is short-lived. |
| 406 | let mut graphs: Vec<(Hash, atomic_core::change::ProvenanceGraph)> = Vec::new(); |
| 407 | for result in self.change_store.iter_provenance_graphs() { |
| 408 | match result { |
| 409 | Ok(hash) => match self.load_provenance_graph(&hash) { |
| 410 | Ok(graph) => graphs.push((hash, graph)), |
| 411 | Err(_) => corrupt += 1, |
| 412 | }, |
| 413 | Err(_) => corrupt += 1, |
| 414 | } |
| 415 | } |
| 416 | |
| 417 | // Group by session. The core index derives canonical turn order from |
| 418 | // the complete set, independent of this ingestion order. |
| 419 | let mut by_session: std::collections::BTreeMap< |
| 420 | String, |
| 421 | Vec<(Hash, atomic_core::change::ProvenanceGraph)>, |
| 422 | > = std::collections::BTreeMap::new(); |
| 423 | let mut head_manifests = std::collections::BTreeMap::new(); |
| 424 | let mut fork_parents = std::collections::BTreeMap::new(); |
| 425 | for (hash, graph) in graphs { |
| 426 | by_session |
| 427 | .entry(graph.session_id.clone()) |
| 428 | .or_default() |
| 429 | .push((hash, graph)); |
| 430 | } |
| 431 | // Forked children can contain only inherited turns, whose provenance |
| 432 | // files still name the parent session. Include both existing ledgers |
| 433 | // and portable manifests so rebuild can create or migrate the child. |
| 434 | { |
| 435 | let txn = self |
| 436 | .pristine |
| 437 | .read_txn() |
| 438 | .map_err(|e| RepositoryError::Database(e.to_string()))?; |
| 439 | for record in txn |
| 440 | .list_session_records() |
| 441 | .map_err(|e| RepositoryError::Database(e.to_string()))? |
| 442 | { |
| 443 | if record.turn_count > 0 { |
| 444 | by_session.entry(record.session_id).or_default(); |
| 445 | } |
| 446 | } |
| 447 | for (head_session_id, head) in txn |
| 448 | .list_session_heads() |
| 449 | .map_err(|e| RepositoryError::Database(e.to_string()))? |
| 450 | { |
| 451 | match txn.get_session_manifest(&head) { |
| 452 | Ok(Some(manifest)) if manifest.session_id == head_session_id => { |
| 453 | if let (Some(parent_hash), Some(fork_turn)) = |
| 454 | (manifest.parent_session, manifest.fork_turn) |
| 455 | { |
| 456 | if let Ok(Some(parent)) = txn.get_session_manifest(&parent_hash) { |
| 457 | fork_parents.insert( |