haematite 0.7.0

Content-addressed, branchable, actor-native storage engine
Documentation
//! The v1-anchor FORK crossing (CHUNKING-POLICY.md §4.2 step 5).
//!
//! Migration leaves fork anchors, snapshots, and commit-log roots v1-shaped
//! read-only pins BY DESIGN (§4.2 step 4). Step 5 is the guard for exactly that
//! decision: when such a v1 anchor becomes a MUTATION BASE under a v2 database
//! (`fork_at`), its working tree is materialised by FULL CANONICAL REBUILD AT
//! FORK TIME — never by mutating v1 leaves in place, which would graft a v2
//! commit onto v1 nodes (the §4.1 forbidden mixed-policy tree).
//!
//! The rule (ruled by the certifying pair via the F1 key-turn): rebuild
//! UNCONDITIONALLY under a v2 policy — there is no per-root policy tag, and the
//! doc's own "cost = anchor size" parenthetical sanctions the simplest honest
//! rule. HI canonicity makes it correct for an already-v2 anchor (identical
//! root; every rebuilt node dedupes on write — a structural no-op). Under a v1
//! policy the crossing is skipped entirely: v1 databases keep today's behaviour
//! byte-for-byte, and no crossing exists there.

use crate::store::NodeStore;
use crate::tree::{Hash, TreeError, TreePolicy, stream_and_rebuild};

use super::commit::BranchCommitError;
use super::registry::{BranchRegistry, BranchRegistryGuard, CrossRegisterError};

/// A single crossed anchor's roots.
pub(super) struct Crossed {
    /// The ORIGINAL anchor — the merge/divergence ancestor (§14.1 anchor role),
    /// held to guard drop for the handle's whole life. It stays v1-shaped;
    /// reads are policy-free so it remains fully readable.
    pub fork_anchor: Hash,
    /// The working root the handle mutates from: under v2 the anchor rebuilt to
    /// v2 shape (so the first commit is pure v2, never a mixed tree); under v1
    /// the anchor itself.
    pub working_root: Hash,
    /// Pins BOTH the anchor (lineage) and the working root (fresh), refcount-2.
    pub guard: BranchRegistryGuard,
}

/// The working root for `anchor` under `policy`: rebuilt from empty under v2
/// (`stream_and_rebuild`), or the anchor verbatim under v1 (skip). Returned as a
/// one-element vec to match [`BranchRegistry::cross_and_register`]'s build
/// contract (one working root per checked anchor, in order).
fn build_working<S: NodeStore + ?Sized>(
    anchors: &[Hash],
    store: &mut S,
    policy: TreePolicy,
) -> Result<Vec<Hash>, TreeError> {
    if policy.is_v2() {
        anchors
            .iter()
            .map(|&anchor| stream_and_rebuild(store, anchor, policy))
            .collect()
    } else {
        Ok(anchors.to_vec())
    }
}

const fn map_cross_error(error: CrossRegisterError<TreeError>) -> BranchCommitError {
    match error {
        CrossRegisterError::UnprotectedAnchor(root) => {
            BranchCommitError::UnprotectedAnchor { root }
        }
        CrossRegisterError::Build(error) => BranchCommitError::Tree(error),
        CrossRegisterError::WorkingRootCountMismatch { expected, got } => {
            BranchCommitError::WorkingRootCountMismatch { expected, got }
        }
    }
}

/// Cross ONE protected anchor into a working root AT FORK TIME (§4.2 step 5).
///
/// `is_protected` is `fork_at`'s existing anchor-protection predicate; the
/// anchor is refused with [`BranchCommitError::UnprotectedAnchor`] if no live
/// pin or durable/named record vouches for it. The rebuild and the registration
/// of both roots happen under the registry's ONE counts-lock
/// ([`BranchRegistry::cross_and_register`]) — the rebuilt working root cannot
/// race a concurrent prune.
pub(super) fn cross_anchor<S, P>(
    anchor: Hash,
    store: &mut S,
    policy: TreePolicy,
    registry: &BranchRegistry,
    is_protected: P,
) -> Result<Crossed, BranchCommitError>
where
    S: NodeStore + ?Sized,
    P: Fn(&Hash) -> bool,
{
    let (working, guard) = registry
        .cross_and_register(&[anchor], is_protected, |anchors| {
            build_working(anchors, store, policy)
        })
        .map_err(map_cross_error)?;
    // `cross_and_register` already guarantees `working.len() == 1` (one root per
    // anchor); destructure it EXPLICITLY so the single-root assumption can never
    // silently fall back to the un-rebuilt v1 anchor — the §4.1 mixed-policy
    // state this crossing exists to prevent. The `else` is unreachable by that
    // guarantee, but is a loud typed error, never a fallback. Dropping `guard`
    // on the error path releases the pins `cross_and_register` took.
    let [working_root] = working.as_slice() else {
        return Err(map_cross_error(
            CrossRegisterError::WorkingRootCountMismatch {
                expected: 1,
                got: working.len(),
            },
        ));
    };
    Ok(Crossed {
        fork_anchor: anchor,
        working_root: *working_root,
        guard,
    })
}

#[cfg(test)]
#[path = "crossing_tests.rs"]
mod crossing_tests;