jacquardSnapshot

← snapshot

4769 bytes
//! Typed object ids: content addresses for the version DAG.
//!
//! Objects in jacquard's DAG are identified by what they *are*, not by when
//! they were made: an object id is the BLAKE3 digest of the object's canonical
//! encoding, tagged by kind. Two consequences are load-bearing:
//!
//! - **Provenance is identity.** A snapshot's provenance is hashed along with its tree,
//!   so the same tree authored by a human and by an agent produces two different
//!   [`SnapshotId`]s. Provenance cannot be edited after the fact without changing every
//!   id downstream.
//! - **Kinds cannot collide.** Every id starts from a one-byte [`ObjectTag`], so a blob
//!   whose bytes happen to encode a valid tree still has a different id from that tree.
//!
//! The wrappers are typed so a [`TreeId`] cannot be passed where a
//! [`SnapshotId`] is expected. Each is `repr(transparent)` over [`Digest`].

use core::fmt;

use serde::{Deserialize, Serialize};

use crate::digest::Digest;

/// One-byte domain separator fed into the hasher before an object's fields.
///
/// `#[non_exhaustive]`: the object vocabulary will grow, and consumers must
/// write a wildcard arm — which, in the gate, maps to *blocked*.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
#[repr(u8)]
pub enum ObjectTag {
    /// File content.
    Blob = 1,
    /// A directory: named references to blobs and subtrees.
    Tree = 2,
    /// A committed state: a tree plus parents, author, and provenance.
    Snapshot = 3,
    /// A design decision governing part of the repository.
    Decision = 4,
    /// A human's written account settling a decision.
    Attestation = 5,
}

impl ObjectTag {
    /// The tag as the byte fed into the hasher.
    #[must_use]
    pub const fn as_byte(self) -> u8 {
        self as u8
    }
}

macro_rules! define_oid {
    ($(#[$meta:meta])* $name:ident, $tag:expr, $prefix:literal) => {
        $(#[$meta])*
        #[derive(
            Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
        )]
        #[serde(transparent)]
        #[repr(transparent)]
        pub struct $name(Digest);

        impl $name {
            /// The object kind this id addresses.
            pub const TAG: ObjectTag = $tag;

            /// Wraps a digest already computed over this kind's canonical
            /// encoding.
            ///
            /// The canonical encoders (in `jac-object` and `jac-decision`)
            /// are the intended callers; an id minted from any other digest
            /// will simply never resolve in a store.
            #[must_use]
            pub const fn from_digest(d: Digest) -> Self {
                Self(d)
            }

            /// The underlying digest.
            #[must_use]
            pub const fn digest(self) -> Digest {
                self.0
            }

            /// Short rendering for log lines. Never compare short forms.
            #[must_use]
            pub fn short(self) -> String {
                self.0.short(12)
            }
        }

        impl fmt::Display for $name {
            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                write!(f, "{}:{}", $prefix, self.0)
            }
        }

        impl fmt::Debug for $name {
            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                write!(f, "{}:{}", $prefix, self.0.short(12))
            }
        }
    };
}

define_oid!(
    /// Content address of a blob.
    BlobId,
    ObjectTag::Blob,
    "blob"
);

define_oid!(
    /// Content address of a tree.
    TreeId,
    ObjectTag::Tree,
    "tree"
);

define_oid!(
    /// Content address of a snapshot.
    ///
    /// Provenance is inside this address: re-labelling an agent's work as
    /// human-authored produces a different snapshot, not a relabelled one.
    SnapshotId,
    ObjectTag::Snapshot,
    "snap"
);

define_oid!(
    /// Content address of a design decision.
    DecisionId,
    ObjectTag::Decision,
    "dec"
);

define_oid!(
    /// Content address of an attestation.
    AttestationId,
    ObjectTag::Attestation,
    "att"
);

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn ids_are_digest_sized() {
        assert_eq!(size_of::<BlobId>(), size_of::<Digest>());
        assert_eq!(size_of::<SnapshotId>(), size_of::<Digest>());
    }

    #[test]
    fn tags_are_distinct_bytes() {
        let tags = [
            ObjectTag::Blob,
            ObjectTag::Tree,
            ObjectTag::Snapshot,
            ObjectTag::Decision,
            ObjectTag::Attestation,
        ];
        for (i, a) in tags.iter().enumerate() {
            for b in &tags[i + 1..] {
                assert_ne!(a.as_byte(), b.as_byte());
            }
        }
    }
}