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 obligation, 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    /// Release the claim without concluding the obligation, making the pact
175    /// reclaimable again only at or after `reclaimable_at`.
176    ///
177    /// This is **non-terminal**: unlike [`fulfill`](Registry::fulfill) and
178    /// [`breach`](Registry::breach), it settles nothing — the pact is left to be
179    /// attempted again. The registry computes no delay: `reclaimable_at` is a
180    /// consumer-supplied instant, honored exactly as the injected `now` is honored
181    /// (compared, never computed), so backoff policy stays with the caller and `Pact`
182    /// carries no delay. A `reclaimable_at` at or before now makes the pact immediately
183    /// claimable, as a voluntary lapse. Release rotates authority like a lapse, so the
184    /// prior retainer can no longer settle or heartbeat. Rejected when the retainer is
185    /// not the current holder.
186    fn release(&self, retainer: &Retainer, reclaimable_at: Timestamp) -> Result<(), Self::Error>;
187}
188
189/// The sans-I/O lifecycle kernel.
190///
191/// The kernel is a pure state machine: it decides the next [`Directive`](kernel::Directive)
192/// from its state and absorbs [`Notice`](kernel::Notice) reports a runtime feeds
193/// back. It performs no I/O
194/// and exposes no `async fn`, so it commits to no runtime shape. It encodes the
195/// lifecycle decision table only — it adds no orchestration behavior.
196///
197/// # Advanced surface
198///
199/// This is the **advanced** tier of Pacta's public API: lower stability intent than
200/// the recommended surface (its API may evolve as the runtime story settles), though
201/// it stays a supported, governed core surface — not unsupported or slated for
202/// removal. Most consumers should compose with the `Driver` runtime (or the `pacta`
203/// facade) and never touch the kernel. Reach for it only to build a custom runtime;
204/// it is reached through `pacta-contract` directly, never through the `pacta` facade.
205///
206/// # Driving the kernel
207///
208/// A runtime drives one lifecycle step by looping: ask [`poll`](kernel::Kernel::poll)
209/// for the next [`Directive`](kernel::Directive), perform it, report the outcome back with
210/// [`on_event`](kernel::Kernel::on_event), and repeat until [`result`](kernel::Kernel::result)
211/// yields a terminal [`StepResult`](kernel::StepResult). The kernel decides *what*;
212/// the runtime performs it and injects time — the kernel reads no clock.
213///
214/// ```
215/// use pacta_contract::kernel::{Directive, Kernel, Notice, StepResult};
216/// use pacta_contract::{Claim, Outcome, Pact, Retainer, Timestamp};
217///
218/// let mut kernel = Kernel::new();
219/// let mut available = Some(Claim::new(
220///     Pact::new(Default::default(), "demo".into(), "demo".into(), Vec::new()),
221///     Retainer::new(Default::default()),
222///     Timestamp::from_millis(0),
223/// ));
224///
225/// let result = loop {
226///     if let Some(result) = kernel.result() {
227///         break result;
228///     }
229///     match kernel.poll() {
230///         Directive::Claim => kernel.on_event(Notice::Claimed(available.take())),
231///         Directive::Execute(_pact) => kernel.on_event(Notice::Executed(Outcome::Fulfilled)),
232///         Directive::Settle(_retainer, _outcome) => kernel.on_event(Notice::Settled),
233///         Directive::Idle => break StepResult::Idle,
234///         _ => unreachable!("driver handles every current kernel directive"),
235///     }
236/// };
237///
238/// assert_eq!(result, StepResult::Settled(Outcome::Fulfilled));
239/// ```
240pub mod kernel {
241    use crate::{Claim, Outcome, Pact, Retainer};
242
243    /// An instruction the kernel issues for a runtime to perform.
244    ///
245    /// `#[non_exhaustive]`: this advanced-tier protocol may gain directives, so a
246    /// runtime's match must carry a wildcard arm.
247    #[derive(Debug, Clone)]
248    #[non_exhaustive]
249    pub enum Directive {
250        /// Claim a pact from the runtime's configured dockets.
251        Claim,
252        /// Execute the given claimed pact.
253        Execute(Pact),
254        /// Settle the claim identified by the retainer with the decided outcome.
255        Settle(Retainer, Outcome),
256        /// Nothing remains to be performed for this step.
257        Idle,
258    }
259
260    /// A report a runtime feeds back after performing a [`Directive`].
261    ///
262    /// `#[non_exhaustive]`: this advanced-tier protocol may gain notices, so a
263    /// consumer's match must carry a wildcard arm.
264    #[derive(Debug, Clone)]
265    #[non_exhaustive]
266    pub enum Notice {
267        /// Result of a claim: a claim if one was available, else none.
268        Claimed(Option<Claim>),
269        /// An execution produced a lifecycle outcome.
270        Executed(Outcome),
271        /// The execution infrastructure failed to run the pact — the executor
272        /// produced no outcome. The kernel fabricates no outcome for this notice: it
273        /// settles nothing and reaches an unsettled terminal, leaving the claim to
274        /// lapse and be reclaimed, while the runtime surfaces the error to its caller.
275        ExecutionFailed,
276        /// A settlement was persisted.
277        Settled,
278    }
279
280    /// The terminal result of one lifecycle step.
281    ///
282    /// `#[non_exhaustive]`: this advanced-tier protocol may gain results, so a
283    /// consumer's match must carry a wildcard arm.
284    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
285    #[non_exhaustive]
286    pub enum StepResult {
287        /// No pact was available to claim.
288        Idle,
289        /// The claim was settled with this outcome.
290        Settled(Outcome),
291        /// Execution produced no outcome (an infrastructure failure), so nothing was
292        /// settled. The claim is left held-but-unsettled to lapse and be reclaimed;
293        /// the kernel fabricates no `Outcome` from the absence of one.
294        Unsettled,
295    }
296
297    #[derive(Debug)]
298    enum Phase {
299        Claiming,
300        Executing {
301            pact: Pact,
302            retainer: Retainer,
303        },
304        Settling {
305            retainer: Retainer,
306            outcome: Outcome,
307        },
308        DoneIdle,
309        DoneSettled(Outcome),
310        DoneUnsettled,
311    }
312
313    /// The pure lifecycle state machine for a single step.
314    #[derive(Debug)]
315    pub struct Kernel {
316        phase: Phase,
317    }
318
319    impl Kernel {
320        /// Start a fresh lifecycle step.
321        #[must_use]
322        pub fn new() -> Self {
323            Self {
324                phase: Phase::Claiming,
325            }
326        }
327
328        /// Decide the next directive from the current state.
329        #[must_use]
330        pub fn poll(&self) -> Directive {
331            match &self.phase {
332                Phase::Claiming => Directive::Claim,
333                Phase::Executing { pact, .. } => Directive::Execute(pact.clone()),
334                Phase::Settling { retainer, outcome } => {
335                    Directive::Settle(retainer.clone(), *outcome)
336                }
337                Phase::DoneIdle | Phase::DoneSettled(_) | Phase::DoneUnsettled => Directive::Idle,
338            }
339        }
340
341        /// Absorb a runtime report, advancing the lifecycle.
342        pub fn on_event(&mut self, notice: Notice) {
343            let phase = std::mem::replace(&mut self.phase, Phase::DoneIdle);
344            self.phase = match (phase, notice) {
345                (Phase::Claiming, Notice::Claimed(Some(claim))) => Phase::Executing {
346                    pact: claim.pact,
347                    retainer: claim.retainer,
348                },
349                (Phase::Claiming, Notice::Claimed(None)) => Phase::DoneIdle,
350                (Phase::Executing { retainer, .. }, Notice::Executed(outcome)) => {
351                    Phase::Settling { retainer, outcome }
352                }
353                (Phase::Executing { .. }, Notice::ExecutionFailed) => Phase::DoneUnsettled,
354                (Phase::Settling { outcome, .. }, Notice::Settled) => Phase::DoneSettled(outcome),
355                (other, _) => other,
356            };
357        }
358
359        /// The terminal result once the step reaches a terminal state, else `None`.
360        #[must_use]
361        pub fn result(&self) -> Option<StepResult> {
362            match &self.phase {
363                Phase::DoneIdle => Some(StepResult::Idle),
364                Phase::DoneSettled(outcome) => Some(StepResult::Settled(*outcome)),
365                Phase::DoneUnsettled => Some(StepResult::Unsettled),
366                _ => None,
367            }
368        }
369    }
370
371    impl Default for Kernel {
372        fn default() -> Self {
373            Self::new()
374        }
375    }
376
377    #[cfg(test)]
378    mod tests {
379        use super::*;
380        use crate::Timestamp;
381        use uuid::Uuid;
382
383        fn claim() -> Claim {
384            Claim::new(
385                Pact::new(
386                    Uuid::new_v4(),
387                    "default".to_string(),
388                    "example".to_string(),
389                    Vec::new(),
390                ),
391                Retainer::new(Uuid::new_v4()),
392                Timestamp::from_millis(0),
393            )
394        }
395
396        fn drive(
397            kernel: &mut Kernel,
398            execution: Result<Outcome, ()>,
399            claimable: bool,
400        ) -> StepResult {
401            loop {
402                if let Some(result) = kernel.result() {
403                    return result;
404                }
405                match kernel.poll() {
406                    Directive::Claim => {
407                        let notice = if claimable {
408                            Notice::Claimed(Some(claim()))
409                        } else {
410                            Notice::Claimed(None)
411                        };
412                        kernel.on_event(notice);
413                    }
414                    Directive::Execute(_) => kernel.on_event(match execution {
415                        Ok(outcome) => Notice::Executed(outcome),
416                        Err(()) => Notice::ExecutionFailed,
417                    }),
418                    Directive::Settle(_, _) => kernel.on_event(Notice::Settled),
419                    Directive::Idle => return StepResult::Idle,
420                }
421            }
422        }
423
424        #[test]
425        fn fulfilled_execution_settles_fulfilled() {
426            let mut kernel = Kernel::new();
427            assert_eq!(
428                drive(&mut kernel, Ok(Outcome::Fulfilled), true),
429                StepResult::Settled(Outcome::Fulfilled)
430            );
431        }
432
433        #[test]
434        fn breached_execution_settles_breached() {
435            let mut kernel = Kernel::new();
436            assert_eq!(
437                drive(&mut kernel, Ok(Outcome::Breached), true),
438                StepResult::Settled(Outcome::Breached)
439            );
440        }
441
442        #[test]
443        fn infrastructure_error_is_unsettled() {
444            let mut kernel = Kernel::new();
445            assert_eq!(drive(&mut kernel, Err(()), true), StepResult::Unsettled);
446        }
447
448        #[test]
449        fn empty_claim_is_idle() {
450            let mut kernel = Kernel::new();
451            assert_eq!(
452                drive(&mut kernel, Ok(Outcome::Fulfilled), false),
453                StepResult::Idle
454            );
455        }
456    }
457}