Return a deterministic, causality-aware ordering for a session's turns. Stored provenance can arrive in any order (for example during pull). A timestamp plus full provenance hash provides a stable tie-break, while the `previous_provenance` link keeps a child after its parent when both are present. Turn numbers are reassigned from zero after ordering.
(turns: Vec<SessionTurn>)
| 72 | /// `previous_provenance` link keeps a child after its parent when both are |
| 73 | /// present. Turn numbers are reassigned from zero after ordering. |
| 74 | pub fn canonicalize_session_turns(turns: Vec<SessionTurn>) -> Vec<SessionTurn> { |
| 75 | type SortKey = (i64, [u8; 32], usize); |
| 76 | |
| 77 | let count = turns.len(); |
| 78 | let positions: std::collections::HashMap<Hash, usize> = turns |
| 79 | .iter() |
| 80 | .enumerate() |
| 81 | .map(|(index, turn)| (turn.provenance_hash, index)) |
| 82 | .collect(); |
| 83 | let sort_keys: Vec<SortKey> = turns |
| 84 | .iter() |
| 85 | .enumerate() |
| 86 | .map(|(index, turn)| (turn.timestamp, *turn.provenance_hash.as_bytes(), index)) |
| 87 | .collect(); |
| 88 | let mut children = vec![Vec::new(); count]; |
| 89 | let mut blocked = vec![false; count]; |
| 90 | |
| 91 | for (index, turn) in turns.iter().enumerate() { |
| 92 | if let Some(parent) = turn |
| 93 | .previous_provenance |
| 94 | .and_then(|hash| positions.get(&hash).copied()) |
| 95 | { |
| 96 | blocked[index] = true; |
| 97 | children[parent].push(index); |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | let mut remaining: std::collections::BTreeSet<SortKey> = sort_keys.iter().copied().collect(); |
| 102 | let mut ready: std::collections::BTreeSet<SortKey> = sort_keys |
| 103 | .iter() |
| 104 | .copied() |
| 105 | .filter(|key| !blocked[key.2]) |
| 106 | .collect(); |
| 107 | let mut slots: Vec<Option<SessionTurn>> = turns.into_iter().map(Some).collect(); |
| 108 | let mut ordered = Vec::with_capacity(count); |
| 109 | |
| 110 | while ordered.len() < count { |
| 111 | // If malformed provenance contains a cycle, break it at the same |
| 112 | // timestamp/hash point in every repository. |
| 113 | let key = ready |
| 114 | .pop_first() |
| 115 | .or_else(|| remaining.first().copied()) |
| 116 | .expect("remaining turn while canonicalizing"); |
| 117 | remaining.remove(&key); |
| 118 | let index = key.2; |
| 119 | let Some(turn) = slots[index].take() else { |
| 120 | continue; |
| 121 | }; |
| 122 | ordered.push(turn); |
| 123 | |
| 124 | for child in &children[index] { |
| 125 | if blocked[*child] { |
| 126 | blocked[*child] = false; |
| 127 | ready.insert(sort_keys[*child]); |
| 128 | } |
| 129 | } |
| 130 | } |
| 131 |