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};
12
13/// The identifier type the durable records and the fencing token are built on, re-exported from the
14/// [`uuid`] crate. A backend needs it to build a [`Pact`] (`Pact::new(id, …)`) and to mint a fresh
15/// [`Retainer`] (`Retainer::new(Uuid…)`), so it is part of the backend-author surface.
16pub use uuid::Uuid;
17
18/// A durable obligation, generated from a Signal, ready to be executed.
19/// Note the deliberate absence of `attempts`, `delay`, and `priority`.
20///
21/// Construct through [`Pact::new`]; the fields stay public for reading. The type is
22/// `#[non_exhaustive]` so it can gain a field in a later minor release without a
23/// breaking change.
24#[derive(Debug, Clone, Serialize, Deserialize)]
25#[non_exhaustive]
26pub struct Pact {
27    /// Stable identifier for this pact.
28    pub id: Uuid,
29    /// Logical docket from which the pact can be claimed.
30    pub docket: String,
31    /// Application-defined pact kind.
32    pub kind: String,
33    /// Business data required to fulfill the pact.
34    pub clause: Vec<u8>,
35}
36
37impl Pact {
38    /// Build a pact from its identifier, docket, kind, and clause.
39    #[must_use]
40    pub fn new(id: Uuid, docket: String, kind: String, clause: Vec<u8>) -> Self {
41        Self {
42            id,
43            docket,
44            kind,
45            clause,
46        }
47    }
48}
49
50/// A retainer: the authority token a registry issues with a claim and validates
51/// when settling it. Authority is registry-validated — a forged identifier does not
52/// match an issued claim — not proven by the type system. Construct via
53/// [`Retainer::new`] and read the identifier via [`Retainer::id`]. Derives
54/// `PartialEq`/`Eq`/`Hash` so a durable backend can index lease state by holder
55/// identity — the orphan rule makes providing these the contract's responsibility.
56#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
57pub struct Retainer(Uuid);
58
59impl Retainer {
60    /// Mint a retainer from an identifier. A registry issues tokens through this.
61    #[must_use]
62    pub fn new(id: Uuid) -> Self {
63        Self(id)
64    }
65
66    /// The retainer's identifier, which a registry validates on settlement.
67    #[must_use]
68    pub fn id(&self) -> Uuid {
69        self.0
70    }
71}
72
73/// A point in time as milliseconds since an epoch the runtime chooses. This is a
74/// pure value: the core names time but never reads it. There is deliberately no
75/// `now` constructor — a runtime obtains the current time and injects it, keeping
76/// lease decisions deterministic and testable.
77#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
78pub struct Timestamp(u64);
79
80impl Timestamp {
81    /// Build a timestamp from milliseconds since the runtime's chosen epoch.
82    #[must_use]
83    pub fn from_millis(millis: u64) -> Self {
84        Self(millis)
85    }
86
87    /// The milliseconds since the runtime's chosen epoch.
88    #[must_use]
89    pub fn as_millis(self) -> u64 {
90        self.0
91    }
92
93    /// The timestamp `millis` milliseconds after this one, saturating at the maximum.
94    #[must_use]
95    pub fn plus_millis(self, millis: u64) -> Self {
96        Self(self.0.saturating_add(millis))
97    }
98}
99
100/// A claimed pact and the retainer required to settle it.
101///
102/// Construct through [`Claim::new`]; the fields stay public for reading. The type is
103/// `#[non_exhaustive]` so it can gain a field in a later minor release without a
104/// breaking change.
105#[derive(Debug, Clone, Serialize, Deserialize)]
106#[non_exhaustive]
107pub struct Claim {
108    /// Pact claimed for execution.
109    pub pact: Pact,
110    /// Authority required to heartbeat, fulfill, or breach the claim.
111    pub retainer: Retainer,
112    /// When the claim's lease expires. After this the pact may be lapsed and
113    /// reclaimed unless the holder heartbeats first.
114    pub lease_expiry: Timestamp,
115}
116
117impl Claim {
118    /// Build a claim from a pact, the settling retainer, and the lease expiry.
119    #[must_use]
120    pub fn new(pact: Pact, retainer: Retainer, lease_expiry: Timestamp) -> Self {
121        Self {
122            pact,
123            retainer,
124            lease_expiry,
125        }
126    }
127}
128
129// The lease identity must be usable as a durable-backend key; removing the derives
130// fails this build rather than silently regressing the backend contract.
131const _: fn() = || {
132    fn assert_key<T: Eq + std::hash::Hash>() {}
133    assert_key::<Retainer>();
134};
135
136/// The lifecycle outcome an execution produces for a claimed pact.
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum Outcome {
139    /// The pact was fulfilled successfully.
140    Fulfilled,
141    /// The pact could not be fulfilled and must be breached.
142    Breached,
143}
144
145/// The lifecycle conclusion applied to a claim, currently a fulfilled or breached
146/// [`Outcome`].
147pub type Settlement = Outcome;
148
149/// The pure lifecycle state machine every `Registry` backend composes over.
150///
151/// This is the single source of the pact lifecycle *semantics* — the claim-eligibility
152/// predicate, the state transitions, the current-holder authority check, and the lease
153/// arithmetic. A backend owns its own storage and mints its own retainer (a fencing
154/// value); it delegates every eligibility decision and transition here, so the semantics
155/// are defined once and cannot drift between backends (or between a synchronous and a
156/// future asynchronous binding).
157///
158/// It is colorless and sans-I/O: it reads no clock (time is an injected parameter),
159/// performs no I/O, and mints nothing non-deterministic (the retainer is supplied by the
160/// caller). Named `lifecycle` to distinguish it from the executor step-driver
161/// [`kernel`], which is a different pure machine.
162pub mod lifecycle {
163    use crate::{Retainer, Timestamp};
164
165    /// A pact's position in its claim lifecycle: the pure state a backend maps to its own storage.
166    /// The backend owns where it lives; this owns what it means.
167    ///
168    /// For the 0.2 series this is a **closed** enumeration of exactly these four variants — it is
169    /// deliberately not `#[non_exhaustive]` — so a backend author knows the complete set of states to
170    /// represent and can match it exhaustively, distinct from the growing `#[non_exhaustive]`
171    /// protocol enums (`Directive`/`Notice`/`StepResult`) elsewhere in this crate. (This is a
172    /// stability statement for 0.2.x, not a promise never to evolve the model in a later minor.)
173    #[derive(Debug, Clone, PartialEq, Eq)]
174    pub enum State {
175        /// Never claimed, or freshly seeded: immediately claimable.
176        Available,
177        /// Held under a lease by `retainer` until `expiry`. Claimable again only once
178        /// the lease has lapsed (`expiry < now`), which rotates authority away.
179        Held {
180            /// The current holder's authority token.
181            retainer: Retainer,
182            /// When the lease expires.
183            expiry: Timestamp,
184        },
185        /// Released non-terminally: claimable again only at or after `reclaimable_at`.
186        Deferred {
187            /// The instant at or after which the pact may be reclaimed.
188            reclaimable_at: Timestamp,
189        },
190        /// Concluded (fulfilled or breached): never claimable again.
191        ///
192        /// This is the *model* (and reference-backend) representation of a concluded obligation, not
193        /// a required storage obligation. A durable backend MAY represent settled by **removing the
194        /// row** — a load of the absent row returns no state, so the pact is trivially not claimable
195        /// and the prior retainer can no longer transition it, which is the whole of what settlement
196        /// guarantees. Do not assume a settled pact persists.
197        Settled,
198    }
199
200    /// A transition was attempted by something that is not the state's current holder —
201    /// a stale retainer, or a state (available, deferred, settled) with no holder at all.
202    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
203    pub struct NotCurrentHolder;
204
205    impl std::fmt::Display for NotCurrentHolder {
206        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
207            write!(f, "retainer is not the current holder of this pact")
208        }
209    }
210
211    impl std::error::Error for NotCurrentHolder {}
212
213    /// The lease expiry for a claim taken at `now` for `lease_millis` — the single
214    /// source of the lease arithmetic.
215    #[must_use]
216    pub fn lease_expiry(now: Timestamp, lease_millis: u64) -> Timestamp {
217        now.plus_millis(lease_millis)
218    }
219
220    /// Whether a pact in `state` may be claimed at `now`: the eligibility invariant.
221    /// `Available` always; a `Held` lease that has lapsed; a `Deferred` pact at or past
222    /// its instant; never a `Settled` one. Only positive, unambiguous eligibility.
223    #[must_use]
224    pub fn is_claimable(state: &State, now: Timestamp) -> bool {
225        match state {
226            State::Available => true,
227            State::Held { expiry, .. } => *expiry < now,
228            State::Deferred { reclaimable_at } => *reclaimable_at <= now,
229            State::Settled => false,
230        }
231    }
232
233    /// The state a successful claim produces: `Held` by `retainer` until the lease
234    /// expiry for `now`/`lease_millis`. The backend mints `retainer` and passes it in.
235    #[must_use]
236    pub fn on_claim(retainer: &Retainer, now: Timestamp, lease_millis: u64) -> State {
237        State::Held {
238            retainer: retainer.clone(),
239            expiry: lease_expiry(now, lease_millis),
240        }
241    }
242
243    /// The state a heartbeat produces: the lease extended to the expiry for
244    /// `now`/`lease_millis`, provided `retainer` currently holds `state` and the lease
245    /// has not already lapsed. A lapsed lease is not revived — the holder must re-claim.
246    pub fn on_heartbeat(
247        state: &State,
248        retainer: &Retainer,
249        now: Timestamp,
250        lease_millis: u64,
251    ) -> Result<State, NotCurrentHolder> {
252        match state {
253            State::Held {
254                retainer: held,
255                expiry,
256            } if held == retainer && *expiry >= now => Ok(State::Held {
257                retainer: retainer.clone(),
258                expiry: lease_expiry(now, lease_millis),
259            }),
260            _ => Err(NotCurrentHolder),
261        }
262    }
263
264    /// The state a settlement produces: `Settled`, provided `retainer` currently holds
265    /// `state`. Fulfill and breach share this — the lifecycle state records that the
266    /// obligation concluded, not which outcome concluded it.
267    pub fn on_settle(state: &State, retainer: &Retainer) -> Result<State, NotCurrentHolder> {
268        if is_current_holder(state, retainer) {
269            Ok(State::Settled)
270        } else {
271            Err(NotCurrentHolder)
272        }
273    }
274
275    /// The state a release produces: `Deferred` until `reclaimable_at`, provided
276    /// `retainer` currently holds `state`. Non-terminal; rotates authority away.
277    pub fn on_release(
278        state: &State,
279        retainer: &Retainer,
280        reclaimable_at: Timestamp,
281    ) -> Result<State, NotCurrentHolder> {
282        if is_current_holder(state, retainer) {
283            Ok(State::Deferred { reclaimable_at })
284        } else {
285            Err(NotCurrentHolder)
286        }
287    }
288
289    fn is_current_holder(state: &State, retainer: &Retainer) -> bool {
290        matches!(state, State::Held { retainer: held, .. } if held == retainer)
291    }
292
293    #[cfg(test)]
294    mod tests {
295        use super::*;
296        use uuid::Uuid;
297
298        fn retainer() -> Retainer {
299            Retainer::new(Uuid::new_v4())
300        }
301
302        #[test]
303        fn eligibility_covers_each_state() {
304            let now = Timestamp::from_millis(100);
305            assert!(is_claimable(&State::Available, now));
306            // A held lease is claimable only once lapsed.
307            assert!(!is_claimable(
308                &State::Held {
309                    retainer: retainer(),
310                    expiry: Timestamp::from_millis(101)
311                },
312                now
313            ));
314            assert!(is_claimable(
315                &State::Held {
316                    retainer: retainer(),
317                    expiry: Timestamp::from_millis(99)
318                },
319                now
320            ));
321            // A deferred pact is claimable at or past its instant.
322            assert!(!is_claimable(
323                &State::Deferred {
324                    reclaimable_at: Timestamp::from_millis(101)
325                },
326                now
327            ));
328            assert!(is_claimable(
329                &State::Deferred {
330                    reclaimable_at: Timestamp::from_millis(100)
331                },
332                now
333            ));
334            assert!(!is_claimable(&State::Settled, now));
335        }
336
337        #[test]
338        fn transitions_require_the_current_holder() {
339            let holder = retainer();
340            let held = State::Held {
341                retainer: holder.clone(),
342                expiry: Timestamp::from_millis(200),
343            };
344            let stranger = retainer();
345
346            assert_eq!(on_settle(&held, &stranger), Err(NotCurrentHolder));
347            assert_eq!(
348                on_release(&held, &stranger, Timestamp::from_millis(0)),
349                Err(NotCurrentHolder)
350            );
351            assert_eq!(on_settle(&State::Settled, &holder), Err(NotCurrentHolder));
352
353            assert_eq!(on_settle(&held, &holder), Ok(State::Settled));
354            assert_eq!(
355                on_release(&held, &holder, Timestamp::from_millis(500)),
356                Ok(State::Deferred {
357                    reclaimable_at: Timestamp::from_millis(500)
358                })
359            );
360        }
361
362        #[test]
363        fn heartbeat_refreshes_but_does_not_revive_a_lapsed_lease() {
364            let holder = retainer();
365            let held = State::Held {
366                retainer: holder.clone(),
367                expiry: Timestamp::from_millis(200),
368            };
369            // Live lease refreshes.
370            assert_eq!(
371                on_heartbeat(&held, &holder, Timestamp::from_millis(150), 100),
372                Ok(State::Held {
373                    retainer: holder.clone(),
374                    expiry: Timestamp::from_millis(250)
375                })
376            );
377            // Lapsed lease is not revived.
378            assert_eq!(
379                on_heartbeat(&held, &holder, Timestamp::from_millis(201), 100),
380                Err(NotCurrentHolder)
381            );
382        }
383    }
384}
385
386/// A pure kernel transition decision — a [`lifecycle`] `on_X` — passed to the transition port
387/// [`Registry::apply`] (and its async twin). The same type is used by both bindings, so the port is
388/// literally one shape.
389///
390/// The `Send + Sync` bound is on the transition **closure**: it lets a backend hold the decision
391/// across its own atomic scope or hand it to a worker thread. It does **not** make the async
392/// binding's `apply` *future* `Send` — future coloring stays the consumer's (the async binding is
393/// deliberately `Send`-agnostic at its futures). A backend that needs a `Send` `apply` future
394/// satisfies that at its own concrete call site, not from this bound.
395pub type Transition<'a> = dyn Fn(&lifecycle::State) -> Result<lifecycle::State, lifecycle::NotCurrentHolder>
396    + Send
397    + Sync
398    + 'a;
399
400/// The asynchronous binding of the [`Registry`] contract, available behind the `async` feature.
401/// [`AsyncRegistry`] is the same five-op contract over the same [`Transition`] port, made async;
402/// [`apply_via_cas`] is the optional compare-and-set helper. A consumer that does not enable `async`
403/// compiles none of it.
404#[cfg(feature = "async")]
405mod async_registry;
406#[cfg(feature = "async")]
407pub use async_registry::{AsyncRegistry, apply_via_cas};
408
409/// The Registry is the durable lifecycle-authority **port**: it preserves pacts and decides claim,
410/// lease, and settlement authority over them. It is *not itself* the pure state machine — the pure,
411/// colorless machine is [`lifecycle`], which every backend composes over; a `Registry`
412/// implementation is the I/O-owning port that persists that machine's states and enforces its
413/// authority (the async twin is [`AsyncRegistry`]).
414///
415/// A backend implements three primitives — a native [`claim`](Registry::claim) selection, a
416/// [`lease_millis`](Registry::lease_millis) accessor, and an atomic [`apply`](Registry::apply)
417/// transition port — and inherits heartbeat, fulfill, breach, and release as defaults over `apply`.
418/// The obligations mirror the async binding exactly:
419///
420/// - **`claim` selects atomically, admits only an eligible pact, and rotates the retainer.** It
421///   returns only a pact [`lifecycle::is_claimable`] would admit and mints a fresh retainer, all in
422///   one atomic step. A durable backend expresses this as a native, full-scan-free selection.
423/// - **`apply` is `load → decide → store` in one atomic scope.** It loads the state held by the
424///   retainer, computes the next state through the passed [`lifecycle`] decision, and stores it
425///   atomically; a non-atomic load-to-store window lets two workers both write and breaks
426///   exactly-once and retainer fencing.
427/// - **Reclaim — not mere expiry — rotates settlement authority.** A holder whose lease lapsed but
428///   whose pact no one reclaimed is still the current holder and can still settle; authority rotates
429///   only when the pact is actually reclaimed (or released). A transition against a pact the retainer
430///   no longer holds surfaces as a not-current-holder error through the backend's `Error`.
431///
432/// `pacta-conformance` proves the *behavioral* half of this (eligibility, transitions, lapse/reclaim
433/// rotation, and — via its contention checks — at-most-once claim and settlement). It does **not**
434/// prove the *query-shape* obligation that `claim` is full-scan-free; a sequential functional suite
435/// cannot observe query cost, so that stays a backend obligation established by review.
436///
437/// Time is injected: [`claim`](Registry::claim) and [`heartbeat`](Registry::heartbeat) take the
438/// current time as a parameter, and the registry reads no ambient clock. Settlement takes no time
439/// because a rotated retainer already tells a stale holder apart from the current one.
440///
441/// The backend type itself need not be [`Send`] or [`Sync`]. A single-threaded backend may stay
442/// local; a caller that moves or shares a registry across threads adds `Send + Sync` at that
443/// concurrency boundary. In particular, generic code that needs thread shareability must write
444/// `R: Registry + Send + Sync` rather than treating it as implied by this trait.
445pub trait Registry {
446    /// Error returned by the registry implementation.
447    type Error: std::error::Error;
448
449    /// Claim a pact for execution from one of the requested dockets, using `now`
450    /// to set the new lease and to reclaim any pact whose lease already expired
451    /// without settlement — a lapse, realized through this normal claim path.
452    ///
453    /// **Obligation:** select — atomically — only a pact [`lifecycle::is_claimable`] would admit
454    /// (available, a lapsed hold, or a deferred pact past its instant; never a settled one) and mint
455    /// a fresh retainer, so reclaiming rotates authority and the prior holder can no longer settle. A
456    /// durable backend expresses this as a native, full-scan-free selection (for example SQL
457    /// `SKIP LOCKED`), not by loading the whole docket to filter in memory.
458    fn claim(&self, dockets: &[&str], now: Timestamp) -> Result<Option<Claim>, Self::Error>;
459
460    /// The backend's lease duration in milliseconds, used by [`heartbeat`](Registry::heartbeat)
461    /// to compute the extended lease. Lease sizing is the backend's; the contract supplies the
462    /// mechanism, not a constant.
463    fn lease_millis(&self) -> u64;
464
465    /// Apply a lifecycle transition to the pact held by `retainer`, within the backend's own
466    /// atomic scope. `transition` is the pure kernel decision (a [`lifecycle`] `on_X`): the
467    /// backend loads the held state, computes the next state through `transition`, and applies
468    /// it atomically — it never decides the transition itself, so the lifecycle semantics stay
469    /// single-sourced in the kernel and cannot drift. A transition applied against a pact the
470    /// retainer no longer holds resolves to a not-current-holder error (the kernel's
471    /// [`NotCurrentHolder`](lifecycle::NotCurrentHolder) surfaces through `transition`). This is
472    /// the one transition port; the four transition operations below are provided over it.
473    ///
474    /// The backend owns *how* the scope is made atomic (a lock, a transaction, a native
475    /// conditional write, or compare-and-set); the contract mandates no concurrency-control
476    /// mechanism.
477    fn apply(&self, retainer: &Retainer, transition: &Transition<'_>) -> Result<(), Self::Error>;
478
479    /// Extend the retainer's lease using `now`. A heartbeat presented after the
480    /// lease already expired is rejected: the holder must claim again rather than
481    /// revive a lapsed lease, so two holders never both hold settlement authority.
482    fn heartbeat(&self, retainer: &Retainer, now: Timestamp) -> Result<(), Self::Error> {
483        let lease = self.lease_millis();
484        self.apply(retainer, &|state| {
485            lifecycle::on_heartbeat(state, retainer, now, lease)
486        })
487    }
488
489    /// Mark the pact as successfully fulfilled. Rejected when the retainer is not
490    /// the current holder.
491    fn fulfill(&self, retainer: &Retainer) -> Result<(), Self::Error> {
492        self.apply(retainer, &|state| lifecycle::on_settle(state, retainer))
493    }
494
495    /// Mark the pact as breached. Rejected when the retainer is not the current
496    /// holder. Shares the settlement transition with [`fulfill`](Registry::fulfill) — the
497    /// lifecycle records that the obligation concluded, not which outcome concluded it.
498    fn breach(&self, retainer: &Retainer) -> Result<(), Self::Error> {
499        self.apply(retainer, &|state| lifecycle::on_settle(state, retainer))
500    }
501
502    /// Release the claim without concluding the obligation, making the pact
503    /// reclaimable again only at or after `reclaimable_at`.
504    ///
505    /// This is **non-terminal**: unlike [`fulfill`](Registry::fulfill) and
506    /// [`breach`](Registry::breach), it settles nothing — the pact is left to be
507    /// attempted again. The registry computes no delay: `reclaimable_at` is a
508    /// consumer-supplied instant, honored exactly as the injected `now` is honored
509    /// (compared, never computed), so backoff policy stays with the caller and `Pact`
510    /// carries no delay. A `reclaimable_at` at or before now makes the pact immediately
511    /// claimable, as a voluntary lapse. Release rotates authority like a lapse, so the
512    /// prior retainer can no longer settle or heartbeat. Rejected when the retainer is
513    /// not the current holder.
514    fn release(&self, retainer: &Retainer, reclaimable_at: Timestamp) -> Result<(), Self::Error> {
515        self.apply(retainer, &|state| {
516            lifecycle::on_release(state, retainer, reclaimable_at)
517        })
518    }
519}
520
521/// The sans-I/O lifecycle kernel.
522///
523/// The kernel is a pure state machine: it decides the next [`Directive`](kernel::Directive)
524/// from its state and absorbs [`Notice`](kernel::Notice) reports a runtime feeds
525/// back. It performs no I/O
526/// and exposes no `async fn`, so it commits to no runtime shape. It encodes the
527/// lifecycle decision table only — it adds no orchestration behavior.
528///
529/// # Advanced surface
530///
531/// This is the **advanced** tier of Pacta's public API: lower stability intent than
532/// the recommended surface (its API may evolve as the runtime story settles), though
533/// it stays a supported, governed core surface — not unsupported or slated for
534/// removal. Most consumers should compose with the `Driver` runtime (or the `pacta`
535/// facade) and never touch the kernel. Reach for it only to build a custom runtime;
536/// it is reached through `pacta-contract` directly, never through the `pacta` facade.
537///
538/// # Driving the kernel
539///
540/// A runtime drives one lifecycle step by looping: ask [`poll`](kernel::Kernel::poll)
541/// for the next [`Directive`](kernel::Directive), perform it, report the outcome back with
542/// [`on_event`](kernel::Kernel::on_event), and repeat until [`result`](kernel::Kernel::result)
543/// yields a terminal [`StepResult`](kernel::StepResult). The kernel decides *what*;
544/// the runtime performs it and injects time — the kernel reads no clock.
545///
546/// ```
547/// use pacta_contract::kernel::{Directive, Kernel, Notice, StepResult};
548/// use pacta_contract::{Claim, Outcome, Pact, Retainer, Timestamp};
549///
550/// let mut kernel = Kernel::new();
551/// let mut available = Some(Claim::new(
552///     Pact::new(Default::default(), "demo".into(), "demo".into(), Vec::new()),
553///     Retainer::new(Default::default()),
554///     Timestamp::from_millis(0),
555/// ));
556///
557/// let result = loop {
558///     if let Some(result) = kernel.result() {
559///         break result;
560///     }
561///     match kernel.poll() {
562///         Directive::Claim => kernel.on_event(Notice::Claimed(available.take())),
563///         Directive::Execute(_pact) => kernel.on_event(Notice::Executed(Outcome::Fulfilled)),
564///         Directive::Settle(_retainer, _outcome) => kernel.on_event(Notice::Settled),
565///         Directive::Idle => break StepResult::Idle,
566///         _ => unreachable!("driver handles every current kernel directive"),
567///     }
568/// };
569///
570/// assert_eq!(result, StepResult::Settled(Outcome::Fulfilled));
571/// ```
572pub mod kernel {
573    use crate::{Claim, Outcome, Pact, Retainer};
574
575    /// An instruction the kernel issues for a runtime to perform.
576    ///
577    /// `#[non_exhaustive]`: this advanced-tier protocol may gain directives, so a
578    /// runtime's match must carry a wildcard arm.
579    #[derive(Debug, Clone)]
580    #[non_exhaustive]
581    pub enum Directive {
582        /// Claim a pact from the runtime's configured dockets.
583        Claim,
584        /// Execute the given claimed pact.
585        Execute(Pact),
586        /// Settle the claim identified by the retainer with the decided outcome.
587        Settle(Retainer, Outcome),
588        /// Nothing remains to be performed for this step.
589        Idle,
590    }
591
592    /// A report a runtime feeds back after performing a [`Directive`].
593    ///
594    /// `#[non_exhaustive]`: this advanced-tier protocol may gain notices, so a
595    /// consumer's match must carry a wildcard arm.
596    #[derive(Debug, Clone)]
597    #[non_exhaustive]
598    pub enum Notice {
599        /// Result of a claim: a claim if one was available, else none.
600        Claimed(Option<Claim>),
601        /// An execution produced a lifecycle outcome.
602        Executed(Outcome),
603        /// The execution infrastructure failed to run the pact — the executor
604        /// produced no outcome. The kernel fabricates no outcome for this notice: it
605        /// settles nothing and reaches an unsettled terminal, leaving the claim to
606        /// lapse and be reclaimed, while the runtime surfaces the error to its caller.
607        ExecutionFailed,
608        /// A settlement was persisted.
609        Settled,
610    }
611
612    /// The terminal result of one lifecycle step.
613    ///
614    /// `#[non_exhaustive]`: this advanced-tier protocol may gain results, so a
615    /// consumer's match must carry a wildcard arm.
616    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
617    #[non_exhaustive]
618    pub enum StepResult {
619        /// No pact was available to claim.
620        Idle,
621        /// The claim was settled with this outcome.
622        Settled(Outcome),
623        /// Execution produced no outcome (an infrastructure failure), so nothing was
624        /// settled. The claim is left held-but-unsettled to lapse and be reclaimed;
625        /// the kernel fabricates no `Outcome` from the absence of one.
626        Unsettled,
627    }
628
629    #[derive(Debug)]
630    enum Phase {
631        Claiming,
632        Executing {
633            pact: Pact,
634            retainer: Retainer,
635        },
636        Settling {
637            retainer: Retainer,
638            outcome: Outcome,
639        },
640        DoneIdle,
641        DoneSettled(Outcome),
642        DoneUnsettled,
643    }
644
645    /// The pure lifecycle state machine for a single step.
646    #[derive(Debug)]
647    pub struct Kernel {
648        phase: Phase,
649    }
650
651    impl Kernel {
652        /// Start a fresh lifecycle step.
653        #[must_use]
654        pub fn new() -> Self {
655            Self {
656                phase: Phase::Claiming,
657            }
658        }
659
660        /// Decide the next directive from the current state.
661        #[must_use]
662        pub fn poll(&self) -> Directive {
663            match &self.phase {
664                Phase::Claiming => Directive::Claim,
665                Phase::Executing { pact, .. } => Directive::Execute(pact.clone()),
666                Phase::Settling { retainer, outcome } => {
667                    Directive::Settle(retainer.clone(), *outcome)
668                }
669                Phase::DoneIdle | Phase::DoneSettled(_) | Phase::DoneUnsettled => Directive::Idle,
670            }
671        }
672
673        /// Absorb a runtime report, advancing the lifecycle.
674        pub fn on_event(&mut self, notice: Notice) {
675            let phase = std::mem::replace(&mut self.phase, Phase::DoneIdle);
676            self.phase = match (phase, notice) {
677                (Phase::Claiming, Notice::Claimed(Some(claim))) => Phase::Executing {
678                    pact: claim.pact,
679                    retainer: claim.retainer,
680                },
681                (Phase::Claiming, Notice::Claimed(None)) => Phase::DoneIdle,
682                (Phase::Executing { retainer, .. }, Notice::Executed(outcome)) => {
683                    Phase::Settling { retainer, outcome }
684                }
685                (Phase::Executing { .. }, Notice::ExecutionFailed) => Phase::DoneUnsettled,
686                (Phase::Settling { outcome, .. }, Notice::Settled) => Phase::DoneSettled(outcome),
687                (other, _) => other,
688            };
689        }
690
691        /// The terminal result once the step reaches a terminal state, else `None`.
692        #[must_use]
693        pub fn result(&self) -> Option<StepResult> {
694            match &self.phase {
695                Phase::DoneIdle => Some(StepResult::Idle),
696                Phase::DoneSettled(outcome) => Some(StepResult::Settled(*outcome)),
697                Phase::DoneUnsettled => Some(StepResult::Unsettled),
698                _ => None,
699            }
700        }
701    }
702
703    impl Default for Kernel {
704        fn default() -> Self {
705            Self::new()
706        }
707    }
708
709    #[cfg(test)]
710    mod tests {
711        use super::*;
712        use crate::Timestamp;
713        use uuid::Uuid;
714
715        fn claim() -> Claim {
716            Claim::new(
717                Pact::new(
718                    Uuid::new_v4(),
719                    "default".to_string(),
720                    "example".to_string(),
721                    Vec::new(),
722                ),
723                Retainer::new(Uuid::new_v4()),
724                Timestamp::from_millis(0),
725            )
726        }
727
728        fn drive(
729            kernel: &mut Kernel,
730            execution: Result<Outcome, ()>,
731            claimable: bool,
732        ) -> StepResult {
733            loop {
734                if let Some(result) = kernel.result() {
735                    return result;
736                }
737                match kernel.poll() {
738                    Directive::Claim => {
739                        let notice = if claimable {
740                            Notice::Claimed(Some(claim()))
741                        } else {
742                            Notice::Claimed(None)
743                        };
744                        kernel.on_event(notice);
745                    }
746                    Directive::Execute(_) => kernel.on_event(match execution {
747                        Ok(outcome) => Notice::Executed(outcome),
748                        Err(()) => Notice::ExecutionFailed,
749                    }),
750                    Directive::Settle(_, _) => kernel.on_event(Notice::Settled),
751                    Directive::Idle => return StepResult::Idle,
752                }
753            }
754        }
755
756        #[test]
757        fn fulfilled_execution_settles_fulfilled() {
758            let mut kernel = Kernel::new();
759            assert_eq!(
760                drive(&mut kernel, Ok(Outcome::Fulfilled), true),
761                StepResult::Settled(Outcome::Fulfilled)
762            );
763        }
764
765        #[test]
766        fn breached_execution_settles_breached() {
767            let mut kernel = Kernel::new();
768            assert_eq!(
769                drive(&mut kernel, Ok(Outcome::Breached), true),
770                StepResult::Settled(Outcome::Breached)
771            );
772        }
773
774        #[test]
775        fn infrastructure_error_is_unsettled() {
776            let mut kernel = Kernel::new();
777            assert_eq!(drive(&mut kernel, Err(()), true), StepResult::Unsettled);
778        }
779
780        #[test]
781        fn empty_claim_is_idle() {
782            let mut kernel = Kernel::new();
783            assert_eq!(
784                drive(&mut kernel, Ok(Outcome::Fulfilled), false),
785                StepResult::Idle
786            );
787        }
788    }
789}