jacquardSnapshot

← snapshot

4334 bytes
//! People, agents, and the provenance vocabulary.
//!
//! The human/agent distinction is carried in the type system, not in a role
//! field: [`HumanIdentity`] and [`AgentIdentity`] are different types, and the
//! attestation constructor in `jac-decision` accepts only the former. There
//! is no conversion between them in either direction.

use core::fmt;

use serde::{Deserialize, Serialize};

use crate::id::{AgentId, HumanId};

/// A person.
///
/// The only type from which an attestation can be minted. In this milestone a
/// `HumanIdentity` is constructible by any in-process caller, so the
/// invariant it carries is type-level — no *code path* mints an attestation
/// from an agent — not authentication. Verifying personhood at creation is an
/// identity/signing problem deferred to a later milestone; see the
/// architecture's unblock conditions.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HumanIdentity {
    /// Stable identifier for this person.
    pub id: HumanId,
    /// Name rendered in narration and logs.
    pub display_name: String,
}

/// A coding agent or model.
///
/// Agents author snapshots and propose decisions. Deliberately has no path
/// into an attestation: there is no `From`, no `TryFrom`, and no constructor
/// anywhere that accepts one.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AgentIdentity {
    /// Stable identifier for this agent.
    pub id: AgentId,
    /// Model or tool name, for narration and logs.
    pub model: String,
}

/// Who authored an object: a person or an agent, by id.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Author {
    /// Authored by a person.
    Human(HumanId),
    /// Authored by an agent.
    Agent(AgentId),
}

impl fmt::Display for Author {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Human(id) => write!(f, "human {id}"),
            Self::Agent(id) => write!(f, "agent {id}"),
        }
    }
}

/// Whose hands made a snapshot.
///
/// Part of the snapshot's *hashed identity* (see `jac-object`), not an
/// annotation: the same tree under different provenance is a different
/// snapshot. What this makes unforgeable is stated precisely — provenance
/// cannot be rewritten *after the fact* without changing every downstream id.
/// Nothing in this milestone verifies the label was honest *at creation*.
///
/// `#[non_exhaustive]`: the vocabulary will grow (e.g. finer mixed grades),
/// and every consumer match carries a wildcard arm — which, in the gate, maps
/// to *blocked*.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
#[repr(u8)]
pub enum Provenance {
    /// Every hunk typed by a person.
    Human = 1,
    /// Every hunk produced by an agent.
    Agent = 2,
    /// A person and an agent both had their hands in it.
    Mixed = 3,
}

impl Provenance {
    /// The byte hashed into a snapshot's identity.
    #[must_use]
    pub const fn as_byte(self) -> u8 {
        self as u8
    }

    /// Stable lowercase label, used in git trailers and narration.
    #[must_use]
    pub const fn label(self) -> &'static str {
        match self {
            Self::Human => "human",
            Self::Agent => "agent",
            Self::Mixed => "mixed",
        }
    }

    /// Parses a trailer label back into a provenance.
    #[must_use]
    pub fn parse_label(s: &str) -> Option<Self> {
        match s {
            "human" => Some(Self::Human),
            "agent" => Some(Self::Agent),
            "mixed" => Some(Self::Mixed),
            _ => None,
        }
    }
}

impl fmt::Display for Provenance {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.label())
    }
}

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

    #[test]
    fn provenance_labels_roundtrip() {
        for p in [Provenance::Human, Provenance::Agent, Provenance::Mixed] {
            assert_eq!(Provenance::parse_label(p.label()), Some(p));
        }
        assert_eq!(Provenance::parse_label("robot"), None);
    }

    #[test]
    fn provenance_bytes_are_distinct() {
        assert_ne!(Provenance::Human.as_byte(), Provenance::Agent.as_byte());
        assert_ne!(Provenance::Agent.as_byte(), Provenance::Mixed.as_byte());
    }
}