Skip to main content

frame_conv/
id.rs

1//! Typed identity vocabulary: conversations, correlations, peers, sequences.
2//!
3//! Everything here is frame/bus vocabulary (design ground 6 — no liminal
4//! type names, no transport types). Wire-facing raw values are crate-internal.
5
6use std::fmt;
7use std::str::FromStr;
8
9use serde::{Deserialize, Serialize};
10use uuid::Uuid;
11
12/// Opaque identity of one conversation.
13///
14/// The F-0c characterization established that conversations are created
15/// implicitly by first enrollment against a caller-minted identity
16/// (F-0c-FINDINGS §R1 assertion 1). Per the frame-conv design (§2.4) the
17/// OPENER mints the identity through [`ConversationId::mint`] and raw-value
18/// construction is deliberately not offered; joiners receive the identity
19/// out of band (serialized form) from the opener.
20#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
21#[serde(transparent)]
22pub struct ConversationId(u64);
23
24impl ConversationId {
25    /// Mints a fresh conversation identity from process entropy.
26    ///
27    /// The substrate offers no create-versus-join testimony at enrollment
28    /// (implicit create — F-0c-FINDINGS §R1 assertion 1), so mint collisions
29    /// are not server-detectable at this seam; the identity space is the
30    /// full 64-bit range to keep the collision surface negligible until the
31    /// server-issued binding authority exists (design §2.4).
32    #[must_use]
33    pub fn mint() -> Self {
34        let bytes = *Uuid::new_v4().as_bytes();
35        let mut raw = [0u8; 8];
36        raw.copy_from_slice(&bytes[..8]);
37        Self(u64::from_le_bytes(raw))
38    }
39
40    pub(crate) const fn to_wire(self) -> u64 {
41        self.0
42    }
43}
44
45impl fmt::Display for ConversationId {
46    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
47        write!(formatter, "{}", self.0)
48    }
49}
50
51impl FromStr for ConversationId {
52    type Err = std::num::ParseIntError;
53
54    fn from_str(value: &str) -> Result<Self, Self::Err> {
55        Ok(Self(value.parse()?))
56    }
57}
58
59/// Typed correlation identity for one request/reply exchange.
60///
61/// Correlation is by this identity and never by delivery order — the
62/// request-response pattern stays correct under the weakest ordering the
63/// evidence returns (F-3a constraint 4).
64#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
65#[serde(transparent)]
66pub struct CorrelationId(Uuid);
67
68impl CorrelationId {
69    /// Mints a caller-unique correlation identity.
70    #[must_use]
71    pub fn mint() -> Self {
72        Self(Uuid::new_v4())
73    }
74}
75
76impl fmt::Display for CorrelationId {
77    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
78        write!(formatter, "{}", self.0)
79    }
80}
81
82impl FromStr for CorrelationId {
83    type Err = uuid::Error;
84
85    fn from_str(value: &str) -> Result<Self, Self::Err> {
86        Ok(Self(Uuid::parse_str(value)?))
87    }
88}
89
90/// Caller-supplied identity of one publication (F-3b R1). Sixteen bytes
91/// with two wire lives, both deterministic (amendment A4, as superseded
92/// 2026-07-23): the bytes ARE the substrate's record-admission attempt
93/// token (the identity function is the deterministic map), and the same
94/// bytes ride the envelope correlation slot so every subscriber decodes
95/// the id from the record itself. The pinned substrate does NOT refuse id
96/// reuse on live bindings (admission tokens are per-attempt identity
97/// there — characterized 2026-07-23); one id identifies ONE publication
98/// as the publisher's documented obligation, and stronger admission
99/// idempotency is upstream ask ASK-6.
100#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
101#[serde(transparent)]
102pub struct PublicationId(Uuid);
103
104impl PublicationId {
105    /// Reconstructs a publication identity from caller-held bytes.
106    #[must_use]
107    pub const fn from_bytes(bytes: [u8; 16]) -> Self {
108        Self(Uuid::from_bytes(bytes))
109    }
110
111    /// The identity's bytes — also exactly the admission attempt token.
112    #[must_use]
113    pub const fn to_bytes(self) -> [u8; 16] {
114        *self.0.as_bytes()
115    }
116
117    /// Mints a fresh caller-unique publication identity.
118    #[must_use]
119    pub fn mint() -> Self {
120        Self(Uuid::new_v4())
121    }
122
123    /// The correlation-slot form this id rides on the wire.
124    pub(crate) const fn to_correlation(self) -> CorrelationId {
125        CorrelationId(self.0)
126    }
127
128    /// Recovers the id from a delivered record's correlation slot.
129    pub(crate) const fn from_correlation(correlation: CorrelationId) -> Self {
130        Self(correlation.0)
131    }
132}
133
134impl fmt::Display for PublicationId {
135    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
136        write!(formatter, "{}", self.0)
137    }
138}
139
140/// Identity of one enrolled conversation peer, minted by the substrate at
141/// enrollment and content-verified from the binding receipt.
142#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
143#[serde(transparent)]
144pub struct ParticipantRef(u64);
145
146impl ParticipantRef {
147    pub(crate) const fn from_wire(raw: u64) -> Self {
148        Self(raw)
149    }
150
151    pub(crate) const fn to_wire(self) -> u64 {
152        self.0
153    }
154}
155
156impl fmt::Display for ParticipantRef {
157    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
158        write!(formatter, "{}", self.0)
159    }
160}
161
162/// Position in a conversation's single per-conversation delivery sequence.
163///
164/// The full F-0c characterization proved one sequence covers lifecycle AND
165/// ordinary records, ascending and contiguous, with the sender excluded from
166/// its own record's delivery (F-0c-FINDINGS §Full characterization,
167/// assertion 5 — PROVEN). frame-conv classifies on this sequence; it never
168/// mints a second one (design §2.1).
169#[derive(
170    Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
171)]
172#[serde(transparent)]
173pub struct ConversationSeq(u64);
174
175impl ConversationSeq {
176    /// Constructs a sequence position from its numeric value (for callers
177    /// persisting cursors across processes).
178    #[must_use]
179    pub const fn new(value: u64) -> Self {
180        Self(value)
181    }
182
183    /// The numeric sequence value.
184    #[must_use]
185    pub const fn value(self) -> u64 {
186        self.0
187    }
188}
189
190impl fmt::Display for ConversationSeq {
191    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
192        write!(formatter, "{}", self.0)
193    }
194}
195
196/// The closed set of pattern contract identities this crate ships (design
197/// §2.2, contract ruling 15 grammar). Matching is nominal and exact; a
198/// semantic change mints the next integer.
199#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
200pub enum PatternId {
201    /// `frame:conv-request@v1` — the request-response pattern.
202    RequestResponse,
203    /// `frame:conv-subscription@v1` — the subscription pattern.
204    Subscription,
205    /// `frame:conv-pubsub@v1` — the pub/sub pattern (F-3b R1).
206    PubSub,
207}
208
209impl PatternId {
210    /// The exact nominal contract string.
211    #[must_use]
212    pub const fn as_str(self) -> &'static str {
213        match self {
214            Self::RequestResponse => "frame:conv-request@v1",
215            Self::Subscription => "frame:conv-subscription@v1",
216            Self::PubSub => "frame:conv-pubsub@v1",
217        }
218    }
219
220    /// Exact-match parse; anything else is an unknown pattern.
221    #[must_use]
222    pub fn parse_exact(value: &str) -> Option<Self> {
223        match value {
224            "frame:conv-request@v1" => Some(Self::RequestResponse),
225            "frame:conv-subscription@v1" => Some(Self::Subscription),
226            "frame:conv-pubsub@v1" => Some(Self::PubSub),
227            _ => None,
228        }
229    }
230
231    /// Whether `kind` belongs to this pattern's closed kind set.
232    #[must_use]
233    pub const fn allows(self, kind: MessageKind) -> bool {
234        match self {
235            Self::RequestResponse => matches!(kind, MessageKind::Request | MessageKind::Reply),
236            Self::Subscription | Self::PubSub => matches!(kind, MessageKind::Event),
237        }
238    }
239}
240
241impl fmt::Display for PatternId {
242    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
243        formatter.write_str(self.as_str())
244    }
245}
246
247/// Closed per-pattern message kind vocabulary (design §2.1).
248#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
249pub enum MessageKind {
250    /// A typed request awaiting a correlated reply.
251    Request,
252    /// The correlated reply to a request.
253    Reply,
254    /// A subscription update.
255    Event,
256}
257
258impl MessageKind {
259    /// The exact wire string.
260    #[must_use]
261    pub const fn as_str(self) -> &'static str {
262        match self {
263            Self::Request => "request",
264            Self::Reply => "reply",
265            Self::Event => "event",
266        }
267    }
268
269    /// Exact-match parse; anything else is an invalid kind.
270    #[must_use]
271    pub fn parse_exact(value: &str) -> Option<Self> {
272        match value {
273            "request" => Some(Self::Request),
274            "reply" => Some(Self::Reply),
275            "event" => Some(Self::Event),
276            _ => None,
277        }
278    }
279}
280
281impl fmt::Display for MessageKind {
282    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
283        formatter.write_str(self.as_str())
284    }
285}