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