Skip to main content

pacta_contract/
lib.rs

1//! The isolated core contract for Pacta.
2//!
3//! Pacta operates on three axioms:
4//! 1. Registry is Lifecycle (no business logic, no retry/delay logic).
5//! 2. Execution is Middleware.
6//! 3. This contract has no dependency on other workspace crates.
7
8#![forbid(unsafe_code)]
9#![warn(missing_docs)]
10
11use serde::{Deserialize, Serialize};
12use uuid::Uuid;
13
14/// A durable command or contract, generated from a Signal, ready to be executed.
15/// Note the deliberate absence of `attempts`, `delay`, and `priority`.
16///
17/// Construct through [`Pact::new`]; the fields stay public for reading. The type is
18/// `#[non_exhaustive]` so it can gain a field in a later minor release without a
19/// breaking change.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21#[non_exhaustive]
22pub struct Pact {
23    /// Stable identifier for this pact.
24    pub id: Uuid,
25    /// Logical docket from which the pact can be claimed.
26    pub docket: String,
27    /// Application-defined pact kind.
28    pub kind: String,
29    /// Business data required to fulfill the pact.
30    pub clause: Vec<u8>,
31}
32
33impl Pact {
34    /// Build a pact from its identifier, docket, kind, and clause.
35    #[must_use]
36    pub fn new(id: Uuid, docket: String, kind: String, clause: Vec<u8>) -> Self {
37        Self {
38            id,
39            docket,
40            kind,
41            clause,
42        }
43    }
44}
45
46/// A retainer: the authority token a registry issues with a claim and validates
47/// when settling it. Authority is registry-validated — a forged identifier does not
48/// match an issued claim — not proven by the type system. Construct via
49/// [`Retainer::new`] and read the identifier via [`Retainer::id`]. Derives
50/// `PartialEq`/`Eq`/`Hash` so a durable backend can index lease state by holder
51/// identity — the orphan rule makes providing these the contract's responsibility.
52#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
53pub struct Retainer(Uuid);
54
55impl Retainer {
56    /// Mint a retainer from an identifier. A registry issues tokens through this.
57    #[must_use]
58    pub fn new(id: Uuid) -> Self {
59        Self(id)
60    }
61
62    /// The retainer's identifier, which a registry validates on settlement.
63    #[must_use]
64    pub fn id(&self) -> Uuid {
65        self.0
66    }
67}
68
69/// A point in time as milliseconds since an epoch the runtime chooses. This is a
70/// pure value: the core names time but never reads it. There is deliberately no
71/// `now` constructor — a runtime obtains the current time and injects it, keeping
72/// lease decisions deterministic and testable.
73#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
74pub struct Timestamp(u64);
75
76impl Timestamp {
77    /// Build a timestamp from milliseconds since the runtime's chosen epoch.
78    #[must_use]
79    pub fn from_millis(millis: u64) -> Self {
80        Self(millis)
81    }
82
83    /// The milliseconds since the runtime's chosen epoch.
84    #[must_use]
85    pub fn as_millis(self) -> u64 {
86        self.0
87    }
88
89    /// The timestamp `millis` milliseconds after this one, saturating at the maximum.
90    #[must_use]
91    pub fn plus_millis(self, millis: u64) -> Self {
92        Self(self.0.saturating_add(millis))
93    }
94}
95
96/// A claimed pact and the retainer required to settle it.
97///
98/// Construct through [`Claim::new`]; the fields stay public for reading. The type is
99/// `#[non_exhaustive]` so it can gain a field in a later minor release without a
100/// breaking change.
101#[derive(Debug, Clone, Serialize, Deserialize)]
102#[non_exhaustive]
103pub struct Claim {
104    /// Pact claimed for execution.
105    pub pact: Pact,
106    /// Authority required to heartbeat, fulfill, or breach the claim.
107    pub retainer: Retainer,
108    /// When the claim's lease expires. After this the pact may be lapsed and
109    /// reclaimed unless the holder heartbeats first.
110    pub lease_expiry: Timestamp,
111}
112
113impl Claim {
114    /// Build a claim from a pact, the settling retainer, and the lease expiry.
115    #[must_use]
116    pub fn new(pact: Pact, retainer: Retainer, lease_expiry: Timestamp) -> Self {
117        Self {
118            pact,
119            retainer,
120            lease_expiry,
121        }
122    }
123}
124
125// The lease identity must be usable as a durable-backend key; removing the derives
126// fails this build rather than silently regressing the backend contract.
127const _: fn() = || {
128    fn assert_key<T: Eq + std::hash::Hash>() {}
129    assert_key::<Retainer>();
130};
131
132/// The lifecycle outcome an execution produces for a claimed pact.
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub enum Outcome {
135    /// The pact was fulfilled successfully.
136    Fulfilled,
137    /// The pact could not be fulfilled and must be breached.
138    Breached,
139}
140
141/// The lifecycle conclusion applied to a claim, currently a fulfilled or breached
142/// [`Outcome`].
143pub type Settlement = Outcome;
144
145/// The Registry manages the lifecycle of Pacts. It is a pure state machine.
146///
147/// Time is injected: [`claim`](Registry::claim) and
148/// [`heartbeat`](Registry::heartbeat) take the current time as a parameter, and the
149/// registry reads no ambient clock. Settlement takes no time because a rotated
150/// retainer already tells a stale holder apart from the current one.
151pub trait Registry: Send + Sync {
152    /// Error returned by the registry implementation.
153    type Error: std::error::Error;
154
155    /// Claim a pact for execution from one of the requested dockets, using `now`
156    /// to set the new lease and to reclaim any pact whose lease already expired
157    /// without settlement — a lapse, realized through this normal claim path.
158    /// Reclaiming rotates the retainer, so the prior holder can no longer settle.
159    fn claim(&self, dockets: &[&str], now: Timestamp) -> Result<Option<Claim>, Self::Error>;
160
161    /// Extend the retainer's lease using `now`. A heartbeat presented after the
162    /// lease already expired is rejected: the holder must claim again rather than
163    /// revive a lapsed lease, so two holders never both hold settlement authority.
164    fn heartbeat(&self, retainer: &Retainer, now: Timestamp) -> Result<(), Self::Error>;
165
166    /// Mark the pact as successfully fulfilled. Rejected when the retainer is not
167    /// the current holder.
168    fn fulfill(&self, retainer: &Retainer) -> Result<(), Self::Error>;
169
170    /// Mark the pact as breached. Rejected when the retainer is not the current
171    /// holder.
172    fn breach(&self, retainer: &Retainer) -> Result<(), Self::Error>;
173}
174
175/// The sans-I/O lifecycle kernel.
176///
177/// The kernel is a pure state machine: it decides the next [`Directive`](kernel::Directive)
178/// from its state and absorbs [`Notice`](kernel::Notice) reports a runtime feeds
179/// back. It performs no I/O
180/// and exposes no `async fn`, so it commits to no runtime shape. It encodes the
181/// lifecycle decision table only — it adds no orchestration behavior.
182///
183/// # Advanced surface
184///
185/// This is the **advanced** tier of Pacta's public API: lower stability intent than
186/// the recommended surface (its API may evolve as the runtime story settles), though
187/// it stays a supported, governed core surface — not unsupported or slated for
188/// removal. Most consumers should compose with the `Driver` runtime (or the `pacta`
189/// facade) and never touch the kernel. Reach for it only to build a custom runtime;
190/// it is reached through `pacta-contract` directly, never through the `pacta` facade.
191///
192/// # Driving the kernel
193///
194/// A runtime drives one lifecycle step by looping: ask [`poll`](kernel::Kernel::poll)
195/// for the next [`Directive`](kernel::Directive), perform it, report the outcome back with
196/// [`on_event`](kernel::Kernel::on_event), and repeat until [`result`](kernel::Kernel::result)
197/// yields a terminal [`StepResult`](kernel::StepResult). The kernel decides *what*;
198/// the runtime performs it and injects time — the kernel reads no clock.
199///
200/// ```
201/// use pacta_contract::kernel::{Directive, Kernel, Notice, StepResult};
202/// use pacta_contract::{Claim, Outcome, Pact, Retainer, Timestamp};
203///
204/// let mut kernel = Kernel::new();
205/// let mut available = Some(Claim::new(
206///     Pact::new(Default::default(), "demo".into(), "demo".into(), Vec::new()),
207///     Retainer::new(Default::default()),
208///     Timestamp::from_millis(0),
209/// ));
210///
211/// let result = loop {
212///     if let Some(result) = kernel.result() {
213///         break result;
214///     }
215///     match kernel.poll() {
216///         Directive::Claim => kernel.on_event(Notice::Claimed(available.take())),
217///         Directive::Execute(_pact) => kernel.on_event(Notice::Executed(Outcome::Fulfilled)),
218///         Directive::Settle(_retainer, _outcome) => kernel.on_event(Notice::Settled),
219///         Directive::Idle => break StepResult::Idle,
220///         _ => unreachable!("driver handles every current kernel directive"),
221///     }
222/// };
223///
224/// assert_eq!(result, StepResult::Settled(Outcome::Fulfilled));
225/// ```
226pub mod kernel {
227    use crate::{Claim, Outcome, Pact, Retainer};
228
229    /// An instruction the kernel issues for a runtime to perform.
230    ///
231    /// `#[non_exhaustive]`: this advanced-tier protocol may gain directives, so a
232    /// runtime's match must carry a wildcard arm.
233    #[derive(Debug, Clone)]
234    #[non_exhaustive]
235    pub enum Directive {
236        /// Claim a pact from the runtime's configured dockets.
237        Claim,
238        /// Execute the given claimed pact.
239        Execute(Pact),
240        /// Settle the claim identified by the retainer with the decided outcome.
241        Settle(Retainer, Outcome),
242        /// Nothing remains to be performed for this step.
243        Idle,
244    }
245
246    /// A report a runtime feeds back after performing a [`Directive`].
247    ///
248    /// `#[non_exhaustive]`: this advanced-tier protocol may gain notices, so a
249    /// consumer's match must carry a wildcard arm.
250    #[derive(Debug, Clone)]
251    #[non_exhaustive]
252    pub enum Notice {
253        /// Result of a claim: a claim if one was available, else none.
254        Claimed(Option<Claim>),
255        /// An execution produced a lifecycle outcome.
256        Executed(Outcome),
257        /// The execution infrastructure failed to run the pact. The kernel decides a
258        /// breach settlement for this notice while the runtime surfaces the error.
259        ExecutionFailed,
260        /// A settlement was persisted.
261        Settled,
262    }
263
264    /// The terminal result of one lifecycle step.
265    ///
266    /// `#[non_exhaustive]`: this advanced-tier protocol may gain results, so a
267    /// consumer's match must carry a wildcard arm.
268    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
269    #[non_exhaustive]
270    pub enum StepResult {
271        /// No pact was available to claim.
272        Idle,
273        /// The claim was settled with this outcome.
274        Settled(Outcome),
275    }
276
277    #[derive(Debug)]
278    enum Phase {
279        Claiming,
280        Executing {
281            pact: Pact,
282            retainer: Retainer,
283        },
284        Settling {
285            retainer: Retainer,
286            outcome: Outcome,
287        },
288        DoneIdle,
289        DoneSettled(Outcome),
290    }
291
292    /// The pure lifecycle state machine for a single step.
293    #[derive(Debug)]
294    pub struct Kernel {
295        phase: Phase,
296    }
297
298    impl Kernel {
299        /// Start a fresh lifecycle step.
300        #[must_use]
301        pub fn new() -> Self {
302            Self {
303                phase: Phase::Claiming,
304            }
305        }
306
307        /// Decide the next directive from the current state.
308        #[must_use]
309        pub fn poll(&self) -> Directive {
310            match &self.phase {
311                Phase::Claiming => Directive::Claim,
312                Phase::Executing { pact, .. } => Directive::Execute(pact.clone()),
313                Phase::Settling { retainer, outcome } => {
314                    Directive::Settle(retainer.clone(), *outcome)
315                }
316                Phase::DoneIdle | Phase::DoneSettled(_) => Directive::Idle,
317            }
318        }
319
320        /// Absorb a runtime report, advancing the lifecycle.
321        pub fn on_event(&mut self, notice: Notice) {
322            let phase = std::mem::replace(&mut self.phase, Phase::DoneIdle);
323            self.phase = match (phase, notice) {
324                (Phase::Claiming, Notice::Claimed(Some(claim))) => Phase::Executing {
325                    pact: claim.pact,
326                    retainer: claim.retainer,
327                },
328                (Phase::Claiming, Notice::Claimed(None)) => Phase::DoneIdle,
329                (Phase::Executing { retainer, .. }, Notice::Executed(outcome)) => {
330                    Phase::Settling { retainer, outcome }
331                }
332                (Phase::Executing { retainer, .. }, Notice::ExecutionFailed) => Phase::Settling {
333                    retainer,
334                    outcome: Outcome::Breached,
335                },
336                (Phase::Settling { outcome, .. }, Notice::Settled) => Phase::DoneSettled(outcome),
337                (other, _) => other,
338            };
339        }
340
341        /// The terminal result once the step reaches a terminal state, else `None`.
342        #[must_use]
343        pub fn result(&self) -> Option<StepResult> {
344            match &self.phase {
345                Phase::DoneIdle => Some(StepResult::Idle),
346                Phase::DoneSettled(outcome) => Some(StepResult::Settled(*outcome)),
347                _ => None,
348            }
349        }
350    }
351
352    impl Default for Kernel {
353        fn default() -> Self {
354            Self::new()
355        }
356    }
357
358    #[cfg(test)]
359    mod tests {
360        use super::*;
361        use crate::Timestamp;
362        use uuid::Uuid;
363
364        fn claim() -> Claim {
365            Claim::new(
366                Pact::new(
367                    Uuid::new_v4(),
368                    "default".to_string(),
369                    "example".to_string(),
370                    Vec::new(),
371                ),
372                Retainer::new(Uuid::new_v4()),
373                Timestamp::from_millis(0),
374            )
375        }
376
377        fn drive(
378            kernel: &mut Kernel,
379            execution: Result<Outcome, ()>,
380            claimable: bool,
381        ) -> StepResult {
382            loop {
383                if let Some(result) = kernel.result() {
384                    return result;
385                }
386                match kernel.poll() {
387                    Directive::Claim => {
388                        let notice = if claimable {
389                            Notice::Claimed(Some(claim()))
390                        } else {
391                            Notice::Claimed(None)
392                        };
393                        kernel.on_event(notice);
394                    }
395                    Directive::Execute(_) => kernel.on_event(match execution {
396                        Ok(outcome) => Notice::Executed(outcome),
397                        Err(()) => Notice::ExecutionFailed,
398                    }),
399                    Directive::Settle(_, _) => kernel.on_event(Notice::Settled),
400                    Directive::Idle => return StepResult::Idle,
401                }
402            }
403        }
404
405        #[test]
406        fn fulfilled_execution_settles_fulfilled() {
407            let mut kernel = Kernel::new();
408            assert_eq!(
409                drive(&mut kernel, Ok(Outcome::Fulfilled), true),
410                StepResult::Settled(Outcome::Fulfilled)
411            );
412        }
413
414        #[test]
415        fn breached_execution_settles_breached() {
416            let mut kernel = Kernel::new();
417            assert_eq!(
418                drive(&mut kernel, Ok(Outcome::Breached), true),
419                StepResult::Settled(Outcome::Breached)
420            );
421        }
422
423        #[test]
424        fn infrastructure_error_settles_breached() {
425            let mut kernel = Kernel::new();
426            assert_eq!(
427                drive(&mut kernel, Err(()), true),
428                StepResult::Settled(Outcome::Breached)
429            );
430        }
431
432        #[test]
433        fn empty_claim_is_idle() {
434            let mut kernel = Kernel::new();
435            assert_eq!(
436                drive(&mut kernel, Ok(Outcome::Fulfilled), false),
437                StepResult::Idle
438            );
439        }
440    }
441}