epiphany/crates/epiphany-bundle/src/ids.rs

516 lines
20 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Bundle-level identifiers and small value types.
//!
//! `epiphany-bundle` depends on `epiphany-determinism` (Agent A) and on nothing
//! else — in particular **not** on `epiphany-core` (Agent B) or `epiphany-ops`
//! (Agent C). The QUICKSTART fixes that boundary deliberately: *"bundles handle
//! bytes, ops handles semantics. A canonical-base snapshot from the bundle's
//! perspective is opaque bytes plus a frontier DVV; only `epiphany-ops`
//! interprets it."*
//!
//! So the identifiers the manifest carries that *belong* to the semantic layer
//! ([`DocumentId`], [`LineageId`], [`SnapshotId`], [`ExtensionId`], the causal
//! [`FrontierBytes`]) are modeled here as opaque fixed-width or length-prefixed
//! values. The bundle stores, orders, and integrity-checks them; it does not
//! interpret them. The identifiers the bundle *owns* ([`FileUuid`],
//! [`ManifestId`]) are defined and derived here in full.
use crate::codec::{DecodeError, Reader, Writer};
use epiphany_determinism::{ContentHash, DomainTag, Preimage};
/// Physical-bundle identity (Chapter 8 §"File, Document, and Lineage
/// Identity"): a 128-bit UUID set at file creation, persisting for the lifetime
/// of the physical file and changing on Save As. Distinct from [`DocumentId`],
/// which identifies the logical work. Opaque to the bundle: 16 raw bytes.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
pub struct FileUuid(pub [u8; 16]);
impl FileUuid {
/// The all-zero UUID. A valid sentinel only for an uninitialized prelude;
/// a created bundle carries a caller-supplied value. (`as_bytes`/`encode`/
/// `decode` are provided by the `opaque_id16!` macro below.)
pub const ZERO: FileUuid = FileUuid([0u8; 16]);
}
/// Logical-work identity (Chapter 8): stable across Save As copies of the same
/// work; a derivative-work fork mints a new one. Opaque to the bundle.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
pub struct DocumentId(pub [u8; 16]);
/// Shared-ancestor identity (Chapter 8): records that two documents share a
/// common ancestor, for version-control genealogy. Opaque to the bundle.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
pub struct LineageId(pub [u8; 16]);
/// A materialized-snapshot identity (Chapter 8 §"Snapshots and Canonical
/// Bases"). Opaque to the bundle; only the semantic layer materializes state.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
pub struct SnapshotId(pub [u8; 16]);
/// An extension's identity (Chapter 8 §"Extension Declarations"). Opaque to the
/// bundle, which preserves unknown extensions' chunks without interpreting them.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
pub struct ExtensionId(pub [u8; 16]);
/// Registry id for a [`crate::ProfileId::Custom`] profile (Chapter 8
/// §"Format Profiles"). Opaque 128-bit value.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
pub struct ProfileRegistryId(pub [u8; 16]);
macro_rules! opaque_id16 {
($name:ident) => {
impl $name {
/// The raw 16 bytes.
#[inline]
pub const fn as_bytes(&self) -> &[u8; 16] {
&self.0
}
#[inline]
pub(crate) fn encode(&self, w: &mut Writer) {
w.put_bytes(&self.0);
}
#[inline]
pub(crate) fn decode(r: &mut Reader) -> Result<Self, DecodeError> {
Ok($name(r.take_array::<16>()?))
}
}
};
}
opaque_id16!(FileUuid);
opaque_id16!(DocumentId);
opaque_id16!(LineageId);
opaque_id16!(SnapshotId);
opaque_id16!(ExtensionId);
opaque_id16!(ProfileRegistryId);
/// The identity of a manifest (Chapter 8 §"The Manifest"): *"Each commit
/// produces a new `ManifestId`."* The bundle owns this derivation. It is a
/// content-derived 128-bit value, `trunc128(BLAKE3("MUSCMNIF" || preimage))`,
/// using the determinism crate's reserved [`DomainTag::MANIFEST_ID`] tag and
/// the same big-endian truncation as every other content-derived id
/// (`ConflictId`, etc. — Chapter 6/8).
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct ManifestId(pub u128);
impl ManifestId {
/// Derives the manifest id from the manifest's identity-bearing preimage:
/// the document id, generation, and the canonical bytes of the manifest
/// body (with the `manifest_id` field itself excluded to avoid a circular
/// dependency). Deterministic — two writers encoding the same manifest
/// content derive the same id. RATIFIED by Pass 11 (item 1.6, P11-D5):
/// core_spec §"Manifest Encoding", Requirement `req:format:manifest-id`
/// (`trunc128(BLAKE3("MUSCMNIF" || document_id || generation || body))`,
/// body excluding `manifest_id`).
///
/// Note: `document_id` and `generation` are committed twice — explicitly
/// here and again inside `body_preimage` (the canonical manifest body opens
/// with them). This duplication is intentional and golden-locked, not an
/// oversight: the preimage shape above is the ratified format.
pub(crate) fn derive(document_id: DocumentId, generation: u64, body_preimage: &[u8]) -> Self {
let mut p = Preimage::new(DomainTag::MANIFEST_ID);
p.push_bytes(document_id.as_bytes());
p.push_u64_le(generation);
p.push_bytes(body_preimage);
ManifestId(p.finish_trunc128())
}
#[inline]
pub(crate) fn encode(&self, w: &mut Writer) {
w.put_u128(self.0);
}
#[inline]
pub(crate) fn decode(r: &mut Reader) -> Result<Self, DecodeError> {
Ok(ManifestId(r.get_u128()?))
}
}
impl core::fmt::Debug for ManifestId {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "ManifestId({:032x})", self.0)
}
}
/// A blob identity (Chapter 8 §"Blobs"): the [`ContentHash`] of the blob's
/// uncompressed payload under the `MUSCBLOB` domain tag. A newtype over
/// `ContentHash`, mirroring the determinism crate's `ChunkId`.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
pub struct BlobId(pub ContentHash);
impl BlobId {
/// The blob id of `payload`: `BLAKE3("MUSCBLOB" || payload)`
/// ([`ContentHash::of_blob`]). Blobs are the one content hash that is a
/// bare `domain || payload` (Chapter 8 §"Blobs"; determinism crate
/// `ContentHash::of_blob`), not the structured chunk preimage.
#[inline]
pub fn of_payload(payload: &[u8]) -> Self {
BlobId(ContentHash::of_blob(payload))
}
#[inline]
pub(crate) fn encode(&self, w: &mut Writer) {
w.put_bytes(self.0.as_bytes());
}
#[inline]
pub(crate) fn decode(r: &mut Reader) -> Result<Self, DecodeError> {
Ok(BlobId(ContentHash(r.take_array::<32>()?)))
}
}
/// The schema version a chunk payload is encoded against (Chapter 8
/// §"Schema Versioning"). Major changes are non-backward-compatible; minor
/// changes only add optional fields/variants.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
pub struct SchemaVersion {
pub major: u16,
pub minor: u16,
}
impl SchemaVersion {
/// The baseline prototype schema version (major 0). Every chunk whose
/// layout is unchanged by the schema-major-1 bump keeps this version.
pub const V0: SchemaVersion = SchemaVersion { major: 0, minor: 1 };
/// Schema major 1 — the first data-model expansion major (Binary Format
/// companion §"Schema Major 1"). Stamped on chunks whose payload carries a
/// v1 layout: the acceleration full-`Score` snapshot, the resolved-layout
/// `LayoutCache`, and any operation-envelope block bearing a v1
/// `CreateRegion`. The canonical-base `MaterializedState`, the manifest,
/// and operation blocks without a changed payload stay at [`Self::V0`].
pub const V1: SchemaVersion = SchemaVersion { major: 1, minor: 0 };
/// Schema major 2 — the second data-model expansion major (Binary Format
/// companion §"Schema Major 2"): the cross-cutting bodies, repeats/voltas,
/// staff/instrument/metadata fills. Stamped (minimally — the lowest major
/// whose layouts decode the bytes) on chunks whose payload carries a v2
/// layout: the acceleration full-`Score` snapshot and any
/// operation-envelope block bearing a v2 value.
pub const V2: SchemaVersion = SchemaVersion { major: 2, minor: 0 };
/// Schema major 3 — the third data-model expansion major (Binary Format
/// companion §"Schema Major 3", Push 4b tranche 3b-i): `ScoreTuningContext`
/// gains `smufl` and `overrides` on the wire. Stamped on the acceleration
/// full-`Score` snapshot **and** on any operation-envelope block carrying a
/// `SetTuningContext` payload.
///
/// **Genesis tranche G2b raised `OperationEnvelopeBlock`'s accept-set to
/// `[0, 3]`** (`spec/CONTRACT_GENESIS_G2B_TUNING.md`). This comment
/// previously said major 3 was snapshot-only because no operation payload
/// embedded the tuning context; `SetTuningContext` (kind/tag 34) is exactly
/// that payload, carrying `epiphany_core::TuningContextSettings`, so the
/// claim no longer holds. See `max_supported_major` in `bundle.rs`, which
/// carried the same falsehood.
pub const V3: SchemaVersion = SchemaVersion { major: 3, minor: 0 };
/// Constructs a schema version.
#[inline]
pub const fn new(major: u16, minor: u16) -> Self {
SchemaVersion { major, minor }
}
/// The **baseline** schema version at a given major: [`Self::V0`] for
/// major 0, [`Self::V1`] for major 1, [`Self::V2`] for major 2,
/// [`Self::V3`] for major 3, and `{major, 0}` for any higher (future)
/// major. A writer maps a chunk's derived schema major to a version this
/// way — e.g. an operation-envelope block stamps the max over its
/// operations' `schema_major()`.
///
/// This yields the **baseline** minor only (G-minor,
/// `spec/PLAN_GMINOR_SCHEMA_MINOR.md` §4, pin 4): a writer whose payload
/// emits a post-baseline discriminant (an appended `OperationKind`,
/// `OperationKindTag`, `OperationPayload`, `ReanchorReason`, or
/// `PreconditionFailureReason` variant) must use
/// [`Self::for_major_at_epoch`] instead, which raises the minor to the
/// epoch that variant requires. This constructor is **not** replaced —
/// `for_major_at_epoch` needs exactly the baseline this computes as its
/// floor (pin 3: `max(baseline_minor(major), epoch_max)`).
#[inline]
pub const fn for_major(major: u16) -> Self {
match major {
0 => SchemaVersion::V0,
1 => SchemaVersion::V1,
2 => SchemaVersion::V2,
3 => SchemaVersion::V3,
m => SchemaVersion { major: m, minor: 0 },
}
}
/// The schema version a writer stamps when its payload's discriminants
/// may require more than `major`'s own baseline minor (G-minor, pin 3):
/// `major` as given, and the minor raised to `epoch_max` when it exceeds
/// [`Self::for_major`]'s baseline minor for that major. `epoch_max` is
/// the maximum [`OperationKind::introduced_minor`](crate)-style epoch
/// actually emitted, or `None` for "no additive requirement" — the
/// distinct sentinel pin 3 requires (never `0`, which is a real baseline
/// minor for `V1``V3`). Baselines are never lowered: an `epoch_max` at
/// or below the baseline, or `None`, yields the baseline unchanged.
#[inline]
pub const fn for_major_at_epoch(major: u16, epoch_max: Option<u16>) -> Self {
let baseline = SchemaVersion::for_major(major);
if let Some(epoch) = epoch_max {
if epoch > baseline.minor {
return SchemaVersion {
major,
minor: epoch,
};
}
}
baseline
}
/// Canonical 4 bytes for the hash preimage (Chapter 8
/// §"Domain-Separated Preimages"): major then minor, little-endian.
#[inline]
pub fn canonical_bytes(self) -> [u8; 4] {
let mut out = [0u8; 4];
out[0..2].copy_from_slice(&self.major.to_le_bytes());
out[2..4].copy_from_slice(&self.minor.to_le_bytes());
out
}
#[inline]
pub(crate) fn encode(&self, w: &mut Writer) {
w.put_u16(self.major).put_u16(self.minor);
}
#[inline]
pub(crate) fn decode(r: &mut Reader) -> Result<Self, DecodeError> {
Ok(SchemaVersion {
major: r.get_u16()?,
minor: r.get_u16()?,
})
}
}
/// The reduction-algorithm version that produced a canonical-base snapshot
/// (Chapter 8): a snapshot may serve as a canonical base only if this matches
/// **the semantics the running build implements**, which the caller states via
/// `BundleCapabilities` at `open` and `create`.
///
/// Modeled as an opaque monotonically-versioned `u32`. **The authoritative
/// number lives in `epiphany-ops` as
/// `epiphany_ops::CURRENT_REDUCTION_ALGORITHM_VERSION`, a plain `u32`; this
/// wrapper type is constructed at the composition boundary** by whichever crate
/// depends on both. `epiphany-bundle` deliberately does **not** depend on
/// `epiphany-ops` — its only workspace dependency is `epiphany-determinism` —
/// so it cannot read that constant itself, which is exactly why the capability
/// is injected rather than looked up.
///
/// This doc comment previously claimed "the algorithm catalog itself lives in
/// `epiphany-ops`" while nothing of the kind existed there — a doc asserting a
/// false fact about another module, and as written **unimplementable from where
/// the check must run**. P13-S27 pin 8 is the rung that made it true.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
pub struct ReductionAlgorithmVersion(pub u32);
impl ReductionAlgorithmVersion {
#[inline]
pub(crate) fn encode(&self, w: &mut Writer) {
w.put_u32(self.0);
}
#[inline]
pub(crate) fn decode(r: &mut Reader) -> Result<Self, DecodeError> {
Ok(ReductionAlgorithmVersion(r.get_u32()?))
}
}
/// A semantic version (Chapter 8 §"Format Profiles" / §"Extension
/// Declarations"). Ordered major, then minor, then patch — the
/// "semantic version lexicographic" order Appendix D names for extension
/// declarations.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
pub struct SemVer {
pub major: u32,
pub minor: u32,
pub patch: u32,
}
impl SemVer {
/// Constructs a semantic version.
#[inline]
pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
SemVer {
major,
minor,
patch,
}
}
#[inline]
pub(crate) fn encode(&self, w: &mut Writer) {
w.put_u32(self.major)
.put_u32(self.minor)
.put_u32(self.patch);
}
#[inline]
pub(crate) fn decode(r: &mut Reader) -> Result<Self, DecodeError> {
Ok(SemVer {
major: r.get_u32()?,
minor: r.get_u32()?,
patch: r.get_u32()?,
})
}
}
/// A point in wall-clock time, nanoseconds from a region origin (mirrors
/// `epiphany-core`'s Chapter 3 `WallClockTime`, redefined locally to keep the
/// A-only dependency boundary). In the bundle it is the superblock's advisory
/// `commit_timestamp` only; **superblock selection is by generation, never by
/// timestamp** (Chapter 8 §"The Superblock Slots").
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
pub struct WallClockTime(pub i64);
impl WallClockTime {
#[inline]
pub(crate) fn encode(&self, w: &mut Writer) {
w.put_i64(self.0);
}
#[inline]
pub(crate) fn decode(r: &mut Reader) -> Result<Self, DecodeError> {
Ok(WallClockTime(r.get_i64()?))
}
}
/// A wall-clock duration, nanoseconds (Chapter 3). Used by [`crate::RetentionPolicy`].
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
pub struct WallClockDuration(pub i64);
impl WallClockDuration {
#[inline]
pub(crate) fn encode(&self, w: &mut Writer) {
w.put_i64(self.0);
}
#[inline]
pub(crate) fn decode(r: &mut Reader) -> Result<Self, DecodeError> {
Ok(WallClockDuration(r.get_i64()?))
}
}
/// A causal frontier (a dotted version vector) as seen by the bundle: an opaque,
/// length-prefixed byte string. The bundle stores and round-trips it but does
/// not interpret it — coverage and the DVV partial order are `epiphany-ops`'s
/// job (QUICKSTART: *"a frontier DVV; only `epiphany-ops` interprets it"*).
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
pub struct FrontierBytes(pub Vec<u8>);
impl FrontierBytes {
/// An empty frontier (the natural value for a bundle with no canonical base).
pub const fn empty() -> Self {
FrontierBytes(Vec::new())
}
/// Wraps opaque DVV bytes produced by the semantic layer.
#[inline]
pub fn from_bytes(bytes: Vec<u8>) -> Self {
FrontierBytes(bytes)
}
/// The opaque bytes.
#[inline]
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
#[inline]
pub(crate) fn encode(&self, w: &mut Writer) {
w.put_var_bytes(&self.0);
}
#[inline]
pub(crate) fn decode(r: &mut Reader) -> Result<Self, DecodeError> {
Ok(FrontierBytes(r.get_var_bytes()?))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn manifest_id_is_content_derived_and_deterministic() {
let doc = DocumentId([7u8; 16]);
let a = ManifestId::derive(doc, 3, b"body-bytes");
let b = ManifestId::derive(doc, 3, b"body-bytes");
let c = ManifestId::derive(doc, 4, b"body-bytes");
assert_eq!(a, b, "same inputs derive the same manifest id");
assert_ne!(a, c, "a different generation derives a different id");
}
#[test]
fn for_major_at_epoch_keeps_the_baseline_when_no_epoch_applies() {
// s3 anchor: a major-0 payload emitting only baseline vocabulary
// stamps {0, 1} — V0's baseline — never {0, 0}.
assert_eq!(
SchemaVersion::for_major_at_epoch(0, None),
SchemaVersion::V0
);
assert_eq!(SchemaVersion::for_major_at_epoch(0, None).minor, 1);
}
#[test]
fn for_major_at_epoch_raises_the_minor_when_the_epoch_exceeds_baseline() {
assert_eq!(
SchemaVersion::for_major_at_epoch(0, Some(8)),
SchemaVersion::new(0, 8)
);
}
#[test]
fn s6_major_and_minor_combine_independently_kind_28_example() {
// A major-2 block containing `CreateRepeatStructure` (kind 28: schema
// major 2, G-minor epoch 6) stamps exactly {2, 6} — the major comes
// from `schema_major()`, the minor from `introduced_minor()`, and
// this constructor combines them without either influencing the
// other (`epiphany-ops`' own `s6_major_and_minor_derive_
// independently` test establishes the two inputs; this closes the
// loop by combining them the way a real writer does).
assert_eq!(
SchemaVersion::for_major_at_epoch(2, Some(6)),
SchemaVersion::new(2, 6)
);
}
#[test]
fn for_major_at_epoch_never_lowers_the_baseline() {
// V1's baseline minor is 0; an epoch below it must not lower it.
assert_eq!(
SchemaVersion::for_major_at_epoch(1, Some(0)),
SchemaVersion::V1
);
}
#[test]
fn schema_version_canonical_bytes_are_major_then_minor_le() {
let v = SchemaVersion::new(0x0102, 0x0304);
assert_eq!(v.canonical_bytes(), [0x02, 0x01, 0x04, 0x03]);
}
#[test]
fn blob_id_matches_bare_domain_payload() {
assert_eq!(BlobId::of_payload(b"x").0, ContentHash::of_blob(b"x"));
}
#[test]
fn value_types_round_trip() {
let mut w = Writer::new();
SchemaVersion::new(2, 9).encode(&mut w);
SemVer::new(1, 4, 7).encode(&mut w);
WallClockTime(-123).encode(&mut w);
FrontierBytes::from_bytes(vec![1, 2, 3]).encode(&mut w);
let bytes = w.into_bytes();
let mut r = Reader::new(&bytes);
assert_eq!(
SchemaVersion::decode(&mut r).unwrap(),
SchemaVersion::new(2, 9)
);
assert_eq!(SemVer::decode(&mut r).unwrap(), SemVer::new(1, 4, 7));
assert_eq!(WallClockTime::decode(&mut r).unwrap(), WallClockTime(-123));
assert_eq!(
FrontierBytes::decode(&mut r).unwrap(),
FrontierBytes::from_bytes(vec![1, 2, 3])
);
assert!(r.finish().is_ok());
}
}