Skip to main content

salamander/
branch.rs

1//! Engine-owned branch metadata and ancestry catalog.
2
3use std::collections::{BTreeMap, HashMap};
4
5use serde::{Deserialize, Serialize};
6
7use crate::format::{BranchId, Metadata, OwnedStoredRecord};
8use crate::{Result, SalamanderError};
9
10/// Name of the default branch every database has.
11pub const DEFAULT_BRANCH_NAME: &str = "main";
12/// Maximum number of branch nodes accepted in one ancestry chain.
13pub const MAX_LINEAGE_DEPTH: usize = 64;
14
15/// Validated, stable human-readable branch label.
16#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
17pub struct BranchName(String);
18
19impl BranchName {
20    /// Maximum length of a branch name, in bytes.
21    pub const MAX_BYTES: usize = 255;
22
23    /// Validates and constructs a branch name, rejecting empty, oversized,
24    /// or NUL-containing input.
25    pub fn new(value: impl Into<String>) -> Result<Self> {
26        let value = value.into();
27        if value.is_empty() || value.as_bytes().contains(&0) {
28            return Err(SalamanderError::InvalidArgument(
29                "branch name must be nonempty and contain no NUL".into(),
30            ));
31        }
32        if value.len() > Self::MAX_BYTES {
33            return Err(SalamanderError::ResourceLimit {
34                resource: "branch name bytes",
35                actual: value.len() as u64,
36                maximum: Self::MAX_BYTES as u64,
37            });
38        }
39        Ok(Self(value))
40    }
41
42    /// The name as a string slice.
43    pub fn as_str(&self) -> &str {
44        &self.0
45    }
46}
47
48/// Whether a branch accepts new writes.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
50pub enum BranchStatus {
51    /// Accepts reads and writes.
52    Active,
53    /// Retains readable history but rejects new writes.
54    Archived,
55}
56
57/// Durable identity and immutable ancestry for one branch.
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59pub struct BranchInfo {
60    /// Permanent machine identity.
61    pub id: BranchId,
62    /// Permanent human-readable label.
63    pub name: BranchName,
64    /// Immediate ancestor, or `None` for the default branch.
65    pub parent: Option<BranchId>,
66    /// Exclusive upper position inherited from `parent`.
67    pub fork_position: Option<u64>,
68    /// Creation wall-clock time for diagnostics, not ordering.
69    pub created_at_unix_nanos: i64,
70    /// Caller-supplied opaque branch metadata.
71    pub metadata: BTreeMap<String, Vec<u8>>,
72    /// Current non-destructive lifecycle state.
73    pub status: BranchStatus,
74}
75
76#[derive(Clone)]
77pub(crate) struct BranchCatalog {
78    by_id: HashMap<BranchId, BranchInfo>,
79    by_name: HashMap<String, BranchId>,
80}
81
82impl BranchCatalog {
83    pub(crate) fn rebuild(
84        system_records: impl Iterator<Item = Result<OwnedStoredRecord>>,
85    ) -> Result<Self> {
86        let default = BranchInfo {
87            id: BranchId::ZERO,
88            name: BranchName::new(DEFAULT_BRANCH_NAME)?,
89            parent: None,
90            fork_position: None,
91            created_at_unix_nanos: 0,
92            metadata: Metadata::new(),
93            status: BranchStatus::Active,
94        };
95        let mut catalog = Self {
96            by_id: HashMap::from([(default.id, default.clone())]),
97            by_name: HashMap::from([(default.name.as_str().to_string(), default.id)]),
98        };
99        for item in system_records {
100            let record = item?;
101            let event_type = record.envelope.event_type.as_str();
102            if event_type != "salamander.branch.created"
103                && event_type != "salamander.branch.archived"
104            {
105                continue;
106            }
107            let info: BranchInfo = serde_json::from_slice(&record.payload).map_err(|error| {
108                SalamanderError::Corrupt {
109                    offset: record.position,
110                    reason: format!("branch metadata decode: {error}"),
111                }
112            })?;
113            if event_type == "salamander.branch.created" {
114                catalog.insert(info)?;
115            } else {
116                catalog.archive(info)?;
117            }
118        }
119        Ok(catalog)
120    }
121
122    pub(crate) fn insert(&mut self, info: BranchInfo) -> Result<()> {
123        if self.by_id.contains_key(&info.id) {
124            return Err(SalamanderError::BranchExists(
125                info.name.as_str().to_string(),
126            ));
127        }
128        if self.by_name.contains_key(info.name.as_str()) {
129            return Err(SalamanderError::BranchExists(
130                info.name.as_str().to_string(),
131            ));
132        }
133        if let Some(parent) = info.parent {
134            if !self.by_id.contains_key(&parent) {
135                return Err(SalamanderError::BranchNotFound(format!("{parent:?}")));
136            }
137        }
138        self.validate_depth(&info)?;
139        self.by_name.insert(info.name.as_str().to_string(), info.id);
140        self.by_id.insert(info.id, info);
141        Ok(())
142    }
143
144    pub(crate) fn archive(&mut self, archived: BranchInfo) -> Result<()> {
145        let current = self
146            .by_id
147            .get_mut(&archived.id)
148            .ok_or_else(|| SalamanderError::BranchNotFound(format!("{:?}", archived.id)))?;
149        if current.id == BranchId::ZERO {
150            return Err(SalamanderError::InvalidArgument(
151                "the default branch cannot be archived".into(),
152            ));
153        }
154        let mut expected = current.clone();
155        expected.status = BranchStatus::Archived;
156        if archived != expected {
157            return Err(SalamanderError::InvalidBranchAncestry(
158                "archive metadata may only change branch status".into(),
159            ));
160        }
161        *current = archived;
162        Ok(())
163    }
164
165    fn validate_depth(&self, info: &BranchInfo) -> Result<()> {
166        let mut parent = info.parent;
167        for _ in 0..MAX_LINEAGE_DEPTH {
168            let Some(id) = parent else {
169                return Ok(());
170            };
171            if id == info.id {
172                return Err(SalamanderError::InvalidBranchAncestry(
173                    "cycle detected".into(),
174                ));
175            }
176            parent = self.by_id.get(&id).and_then(|branch| branch.parent);
177        }
178        Err(SalamanderError::InvalidBranchAncestry(format!(
179            "lineage exceeds maximum depth {MAX_LINEAGE_DEPTH}"
180        )))
181    }
182
183    pub(crate) fn get(&self, id: BranchId) -> Option<&BranchInfo> {
184        self.by_id.get(&id)
185    }
186
187    pub(crate) fn named(&self, name: &str) -> Option<&BranchInfo> {
188        self.by_name.get(name).and_then(|id| self.by_id.get(id))
189    }
190
191    pub(crate) fn ancestry(&self, id: BranchId) -> Result<Vec<BranchInfo>> {
192        let mut result = Vec::new();
193        let mut current = Some(id);
194        while let Some(branch_id) = current {
195            let branch = self
196                .by_id
197                .get(&branch_id)
198                .ok_or_else(|| SalamanderError::BranchNotFound(format!("{branch_id:?}")))?;
199            result.push(branch.clone());
200            current = branch.parent;
201            if result.len() > MAX_LINEAGE_DEPTH {
202                return Err(SalamanderError::InvalidBranchAncestry(
203                    "lineage depth exceeded".into(),
204                ));
205            }
206        }
207        result.reverse();
208        Ok(result)
209    }
210
211    pub(crate) fn children(&self, id: BranchId) -> Vec<BranchInfo> {
212        let mut children: Vec<_> = self
213            .by_id
214            .values()
215            .filter(|branch| branch.parent == Some(id))
216            .cloned()
217            .collect();
218        children.sort_by(|left, right| left.name.cmp(&right.name));
219        children
220    }
221
222    pub(crate) fn replay_scopes(&self, id: BranchId, upto: u64) -> Result<Vec<(BranchId, u64)>> {
223        let ancestry = self.ancestry(id)?;
224        // A fork inherits history strictly up to its position — including
225        // what its parent had itself inherited — so each level's upper
226        // bound is the *running minimum* of every downstream fork position
227        // on the path, not just the immediate child's. The two differ only
228        // when a fork sits below its parent's own fork point (legal, if
229        // odd); capping by the immediate child alone leaked grandparent
230        // records into that window.
231        let mut upper = upto;
232        let mut scopes: Vec<(BranchId, u64)> = ancestry
233            .iter()
234            .enumerate()
235            .rev()
236            .map(|(index, branch)| {
237                if let Some(child) = ancestry.get(index + 1) {
238                    upper = upper.min(child.fork_position.unwrap_or(upto));
239                }
240                (branch.id, upper)
241            })
242            .collect();
243        scopes.reverse();
244        Ok(scopes)
245    }
246
247    /// The common ancestor and exclusive divergence position of two
248    /// timelines, per the DIFF contract
249    /// (`docs/specs/first-class-diff.md`). Pure catalog arithmetic over
250    /// engine-owned ancestry positions — payload bytes are never consulted
251    /// (DIFF-6), and no log I/O happens here (untils arrive pre-resolved).
252    pub(crate) fn divergence(
253        &self,
254        left: BranchId,
255        left_until: u64,
256        right: BranchId,
257        right_until: u64,
258    ) -> Result<(BranchInfo, u64)> {
259        let left_path = self.ancestry(left)?;
260        let right_path = self.ancestry(right)?;
261        Ok(divergence_of(
262            &left_path,
263            left_until,
264            &right_path,
265            right_until,
266        ))
267    }
268
269    pub(crate) fn common_ancestor(&self, left: BranchId, right: BranchId) -> Result<BranchInfo> {
270        let left = self.ancestry(left)?;
271        let right = self.ancestry(right)?;
272        left.into_iter()
273            .zip(right)
274            .take_while(|(a, b)| a.id == b.id)
275            .map(|(branch, _)| branch)
276            .last()
277            .ok_or_else(|| {
278                SalamanderError::InvalidBranchAncestry("branches have no common root".into())
279            })
280    }
281}
282
283/// Divergence over two resolved root-first ancestry paths: the deepest
284/// shared branch node, and the smallest of — each side's exclusive until
285/// and *every* non-shared node's fork position on either path. Every
286/// non-shared node caps the divergence (its local records sit at
287/// positions at or above its fork and are visible to one side only), and
288/// fork positions are not monotone along a path — a fork below its
289/// parent's own fork point is legal — so the minimum runs over the whole
290/// divergent tail, mirroring the cascaded caps in `replay_scopes`. Every
291/// ancestry begins at the default branch, so the shared prefix is never
292/// empty.
293fn divergence_of(
294    left_path: &[BranchInfo],
295    left_until: u64,
296    right_path: &[BranchInfo],
297    right_until: u64,
298) -> (BranchInfo, u64) {
299    let shared = left_path
300        .iter()
301        .zip(right_path)
302        .take_while(|(a, b)| a.id == b.id)
303        .count();
304    debug_assert!(shared >= 1, "ancestries always share the default branch");
305    let ancestor = left_path[shared - 1].clone();
306    let side_min = |path: &[BranchInfo], until: u64| {
307        path[shared..]
308            .iter()
309            .filter_map(|branch| branch.fork_position)
310            .fold(until, u64::min)
311    };
312    let divergence = side_min(left_path, left_until).min(side_min(right_path, right_until));
313    (ancestor, divergence)
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319
320    fn node(id: u8, parent: Option<u8>, fork_position: Option<u64>) -> BranchInfo {
321        BranchInfo {
322            id: BranchId::from_bytes([id; 16]),
323            name: BranchName::new(format!("branch-{id}")).unwrap(),
324            parent: parent.map(|p| BranchId::from_bytes([p; 16])),
325            fork_position,
326            created_at_unix_nanos: 0,
327            metadata: Metadata::new(),
328            status: BranchStatus::Active,
329        }
330    }
331
332    fn root() -> BranchInfo {
333        BranchInfo {
334            id: BranchId::ZERO,
335            name: BranchName::new(DEFAULT_BRANCH_NAME).unwrap(),
336            parent: None,
337            fork_position: None,
338            created_at_unix_nanos: 0,
339            metadata: Metadata::new(),
340            status: BranchStatus::Active,
341        }
342    }
343
344    #[test]
345    fn ancestor_vs_descendant_diverges_at_the_fork() {
346        let main = vec![root()];
347        let fork = vec![root(), node(1, Some(0), Some(8))];
348        let (ancestor, d) = divergence_of(&main, 12, &fork, 17);
349        assert_eq!(ancestor.id, BranchId::ZERO);
350        assert_eq!(d, 8);
351    }
352
353    #[test]
354    fn siblings_diverge_at_the_earlier_fork() {
355        let left = vec![root(), node(1, Some(0), Some(5))];
356        let right = vec![root(), node(2, Some(0), Some(9))];
357        let (ancestor, d) = divergence_of(&left, 20, &right, 20);
358        assert_eq!(ancestor.id, BranchId::ZERO);
359        assert_eq!(d, 5);
360    }
361
362    #[test]
363    fn same_branch_diverges_at_the_smaller_until() {
364        let path = vec![root(), node(1, Some(0), Some(3))];
365        let (ancestor, d) = divergence_of(&path, 4, &path, 9);
366        assert_eq!(ancestor.id, path[1].id);
367        assert_eq!(d, 4);
368        let (_, d) = divergence_of(&path, 9, &path, 9);
369        assert_eq!(d, 9);
370    }
371
372    #[test]
373    fn an_until_below_the_fork_caps_the_divergence() {
374        let main = vec![root()];
375        let fork = vec![root(), node(1, Some(0), Some(8))];
376        let (_, d) = divergence_of(&main, 3, &fork, 17);
377        assert_eq!(d, 3);
378    }
379
380    #[test]
381    fn grandchildren_share_the_deepest_common_node() {
382        let child = node(1, Some(0), Some(4));
383        let left = vec![root(), child.clone(), node(2, Some(1), Some(7))];
384        let right = vec![root(), child.clone(), node(3, Some(1), Some(10))];
385        let (ancestor, d) = divergence_of(&left, 20, &right, 20);
386        assert_eq!(ancestor.id, child.id);
387        assert_eq!(d, 7);
388    }
389
390    #[test]
391    fn fork_at_zero_diverges_at_zero() {
392        let main = vec![root()];
393        let fork = vec![root(), node(1, Some(0), Some(0))];
394        let (_, d) = divergence_of(&main, 6, &fork, 6);
395        assert_eq!(d, 0);
396    }
397
398    #[test]
399    fn a_fork_below_its_parents_fork_caps_the_divergence() {
400        // b = fork(a, 0) where a = fork(main, 1): b inherits *nothing*
401        // (its position caps the whole inherited prefix), so diffing b
402        // against a — or against anything — diverges at 0, not at a's
403        // fork. Found by the diff property test's double-replay oracle.
404        let a = node(1, Some(0), Some(1));
405        let a_path = vec![root(), a.clone()];
406        let b_path = vec![root(), a, node(2, Some(1), Some(0))];
407        let (ancestor, d) = divergence_of(&b_path, 2, &a_path, 2);
408        assert_eq!(ancestor.id, b_path[1].id);
409        assert_eq!(d, 0);
410    }
411
412    #[test]
413    fn replay_scopes_cascade_downstream_fork_caps() {
414        let mut catalog = BranchCatalog {
415            by_id: HashMap::from([(BranchId::ZERO, root())]),
416            by_name: HashMap::from([(DEFAULT_BRANCH_NAME.to_string(), BranchId::ZERO)]),
417        };
418        catalog.insert(node(1, Some(0), Some(3))).unwrap();
419        catalog.insert(node(2, Some(1), Some(1))).unwrap();
420        // Grandchild forked at 1, below its parent's fork at 3: every
421        // inherited level is capped at 1 — the parent's cap must not leak
422        // root records from [1, 3) into the grandchild's timeline.
423        assert_eq!(
424            catalog
425                .replay_scopes(BranchId::from_bytes([2; 16]), 10)
426                .unwrap(),
427            vec![
428                (BranchId::ZERO, 1),
429                (BranchId::from_bytes([1; 16]), 1),
430                (BranchId::from_bytes([2; 16]), 10),
431            ]
432        );
433    }
434}