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}