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