frame-conv 0.2.0

Conversation patterns — request-response, subscription, pub/sub, and workflow over liminal
Documentation
//! Typed identity vocabulary: conversations, correlations, peers, sequences.
//!
//! Everything here is frame/bus vocabulary (design ground 6 — no liminal
//! type names, no transport types). Wire-facing raw values are crate-internal.

use std::fmt;
use std::str::FromStr;

use serde::{Deserialize, Serialize};
use uuid::Uuid;

/// Opaque identity of one conversation.
///
/// The F-0c characterization established that conversations are created
/// implicitly by first enrollment against a caller-minted identity
/// (F-0c-FINDINGS §R1 assertion 1). Per the frame-conv design (§2.4) the
/// OPENER mints the identity through [`ConversationId::mint`] and raw-value
/// construction is deliberately not offered; joiners receive the identity
/// out of band (serialized form) from the opener.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ConversationId(u64);

impl ConversationId {
    /// Mints a fresh conversation identity from process entropy.
    ///
    /// The substrate offers no create-versus-join testimony at enrollment
    /// (implicit create — F-0c-FINDINGS §R1 assertion 1), so mint collisions
    /// are not server-detectable at this seam; the identity space is the
    /// full 64-bit range to keep the collision surface negligible until the
    /// server-issued binding authority exists (design §2.4).
    #[must_use]
    pub fn mint() -> Self {
        let bytes = *Uuid::new_v4().as_bytes();
        let mut raw = [0u8; 8];
        raw.copy_from_slice(&bytes[..8]);
        Self(u64::from_le_bytes(raw))
    }

    pub(crate) const fn to_wire(self) -> u64 {
        self.0
    }
}

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

impl FromStr for ConversationId {
    type Err = std::num::ParseIntError;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        Ok(Self(value.parse()?))
    }
}

/// Typed correlation identity for one request/reply exchange.
///
/// Correlation is by this identity and never by delivery order — the
/// request-response pattern stays correct under the weakest ordering the
/// evidence returns (F-3a constraint 4).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct CorrelationId(Uuid);

impl CorrelationId {
    /// Mints a caller-unique correlation identity.
    #[must_use]
    pub fn mint() -> Self {
        Self(Uuid::new_v4())
    }
}

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

impl FromStr for CorrelationId {
    type Err = uuid::Error;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        Ok(Self(Uuid::parse_str(value)?))
    }
}

/// Caller-supplied identity of one publication (F-3b R1). Sixteen bytes
/// with two wire lives, both deterministic (amendment A4, as superseded
/// 2026-07-23): the bytes ARE the substrate's record-admission attempt
/// token (the identity function is the deterministic map), and the same
/// bytes ride the envelope correlation slot so every subscriber decodes
/// the id from the record itself. The pinned substrate does NOT refuse id
/// reuse on live bindings (admission tokens are per-attempt identity
/// there — characterized 2026-07-23); one id identifies ONE publication
/// as the publisher's documented obligation, and stronger admission
/// idempotency is upstream ask ASK-6.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct PublicationId(Uuid);

impl PublicationId {
    /// Reconstructs a publication identity from caller-held bytes.
    #[must_use]
    pub const fn from_bytes(bytes: [u8; 16]) -> Self {
        Self(Uuid::from_bytes(bytes))
    }

    /// The identity's bytes — also exactly the admission attempt token.
    #[must_use]
    pub const fn to_bytes(self) -> [u8; 16] {
        *self.0.as_bytes()
    }

    /// Mints a fresh caller-unique publication identity.
    #[must_use]
    pub fn mint() -> Self {
        Self(Uuid::new_v4())
    }

    /// The correlation-slot form this id rides on the wire.
    pub(crate) const fn to_correlation(self) -> CorrelationId {
        CorrelationId(self.0)
    }

    /// Recovers the id from a delivered record's correlation slot.
    pub(crate) const fn from_correlation(correlation: CorrelationId) -> Self {
        Self(correlation.0)
    }
}

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

/// Identity of one enrolled conversation peer, minted by the substrate at
/// enrollment and content-verified from the binding receipt.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ParticipantRef(u64);

impl ParticipantRef {
    pub(crate) const fn from_wire(raw: u64) -> Self {
        Self(raw)
    }

    pub(crate) const fn to_wire(self) -> u64 {
        self.0
    }
}

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

/// Position in a conversation's single per-conversation delivery sequence.
///
/// The full F-0c characterization proved one sequence covers lifecycle AND
/// ordinary records, ascending and contiguous, with the sender excluded from
/// its own record's delivery (F-0c-FINDINGS §Full characterization,
/// assertion 5 — PROVEN). frame-conv classifies on this sequence; it never
/// mints a second one (design §2.1).
#[derive(
    Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
)]
#[serde(transparent)]
pub struct ConversationSeq(u64);

impl ConversationSeq {
    /// Constructs a sequence position from its numeric value (for callers
    /// persisting cursors across processes).
    #[must_use]
    pub const fn new(value: u64) -> Self {
        Self(value)
    }

    /// The numeric sequence value.
    #[must_use]
    pub const fn value(self) -> u64 {
        self.0
    }
}

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

/// The closed set of pattern contract identities this crate ships (design
/// §2.2, contract ruling 15 grammar). Matching is nominal and exact; a
/// semantic change mints the next integer.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum PatternId {
    /// `frame:conv-request@v1` — the request-response pattern.
    RequestResponse,
    /// `frame:conv-subscription@v1` — the subscription pattern.
    Subscription,
    /// `frame:conv-pubsub@v1` — the pub/sub pattern (F-3b R1).
    PubSub,
}

impl PatternId {
    /// The exact nominal contract string.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::RequestResponse => "frame:conv-request@v1",
            Self::Subscription => "frame:conv-subscription@v1",
            Self::PubSub => "frame:conv-pubsub@v1",
        }
    }

    /// Exact-match parse; anything else is an unknown pattern.
    #[must_use]
    pub fn parse_exact(value: &str) -> Option<Self> {
        match value {
            "frame:conv-request@v1" => Some(Self::RequestResponse),
            "frame:conv-subscription@v1" => Some(Self::Subscription),
            "frame:conv-pubsub@v1" => Some(Self::PubSub),
            _ => None,
        }
    }

    /// Whether `kind` belongs to this pattern's closed kind set.
    #[must_use]
    pub const fn allows(self, kind: MessageKind) -> bool {
        match self {
            Self::RequestResponse => matches!(kind, MessageKind::Request | MessageKind::Reply),
            Self::Subscription | Self::PubSub => matches!(kind, MessageKind::Event),
        }
    }
}

impl fmt::Display for PatternId {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(self.as_str())
    }
}

/// Closed per-pattern message kind vocabulary (design §2.1).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum MessageKind {
    /// A typed request awaiting a correlated reply.
    Request,
    /// The correlated reply to a request.
    Reply,
    /// A subscription update.
    Event,
}

impl MessageKind {
    /// The exact wire string.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Request => "request",
            Self::Reply => "reply",
            Self::Event => "event",
        }
    }

    /// Exact-match parse; anything else is an invalid kind.
    #[must_use]
    pub fn parse_exact(value: &str) -> Option<Self> {
        match value {
            "request" => Some(Self::Request),
            "reply" => Some(Self::Reply),
            "event" => Some(Self::Event),
            _ => None,
        }
    }
}

impl fmt::Display for MessageKind {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(self.as_str())
    }
}