//! 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());
}
}
}
}