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 executor
258 /// produced no outcome. The kernel fabricates no outcome for this notice: it
259 /// settles nothing and reaches an unsettled terminal, leaving the claim to
260 /// lapse and be reclaimed, while the runtime surfaces the error to its caller.
261 ExecutionFailed,
262 /// A settlement was persisted.
263 Settled,
264 }
265
266 /// The terminal result of one lifecycle step.
267 ///
268 /// `#[non_exhaustive]`: this advanced-tier protocol may gain results, so a
269 /// consumer's match must carry a wildcard arm.
270 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
271 #[non_exhaustive]
272 pub enum StepResult {
273 /// No pact was available to claim.
274 Idle,
275 /// The claim was settled with this outcome.
276 Settled(Outcome),
277 /// Execution produced no outcome (an infrastructure failure), so nothing was
278 /// settled. The claim is left held-but-unsettled to lapse and be reclaimed;
279 /// the kernel fabricates no `Outcome` from the absence of one.
280 Unsettled,
281 }
282
283 #[derive(Debug)]
284 enum Phase {
285 Claiming,
286 Executing {
287 pact: Pact,
288 retainer: Retainer,
289 },
290 Settling {
291 retainer: Retainer,
292 outcome: Outcome,
293 },
294 DoneIdle,
295 DoneSettled(Outcome),
296 DoneUnsettled,
297 }
298
299 /// The pure lifecycle state machine for a single step.
300 #[derive(Debug)]
301 pub struct Kernel {
302 phase: Phase,
303 }
304
305 impl Kernel {
306 /// Start a fresh lifecycle step.
307 #[must_use]
308 pub fn new() -> Self {
309 Self {
310 phase: Phase::Claiming,
311 }
312 }
313
314 /// Decide the next directive from the current state.
315 #[must_use]
316 pub fn poll(&self) -> Directive {
317 match &self.phase {
318 Phase::Claiming => Directive::Claim,
319 Phase::Executing { pact, .. } => Directive::Execute(pact.clone()),
320 Phase::Settling { retainer, outcome } => {
321 Directive::Settle(retainer.clone(), *outcome)
322 }
323 Phase::DoneIdle | Phase::DoneSettled(_) | Phase::DoneUnsettled => Directive::Idle,
324 }
325 }
326
327 /// Absorb a runtime report, advancing the lifecycle.
328 pub fn on_event(&mut self, notice: Notice) {
329 let phase = std::mem::replace(&mut self.phase, Phase::DoneIdle);
330 self.phase = match (phase, notice) {
331 (Phase::Claiming, Notice::Claimed(Some(claim))) => Phase::Executing {
332 pact: claim.pact,
333 retainer: claim.retainer,
334 },
335 (Phase::Claiming, Notice::Claimed(None)) => Phase::DoneIdle,
336 (Phase::Executing { retainer, .. }, Notice::Executed(outcome)) => {
337 Phase::Settling { retainer, outcome }
338 }
339 (Phase::Executing { .. }, Notice::ExecutionFailed) => Phase::DoneUnsettled,
340 (Phase::Settling { outcome, .. }, Notice::Settled) => Phase::DoneSettled(outcome),
341 (other, _) => other,
342 };
343 }
344
345 /// The terminal result once the step reaches a terminal state, else `None`.
346 #[must_use]
347 pub fn result(&self) -> Option<StepResult> {
348 match &self.phase {
349 Phase::DoneIdle => Some(StepResult::Idle),
350 Phase::DoneSettled(outcome) => Some(StepResult::Settled(*outcome)),
351 Phase::DoneUnsettled => Some(StepResult::Unsettled),
352 _ => None,
353 }
354 }
355 }
356
357 impl Default for Kernel {
358 fn default() -> Self {
359 Self::new()
360 }
361 }
362
363 #[cfg(test)]
364 mod tests {
365 use super::*;
366 use crate::Timestamp;
367 use uuid::Uuid;
368
369 fn claim() -> Claim {
370 Claim::new(
371 Pact::new(
372 Uuid::new_v4(),
373 "default".to_string(),
374 "example".to_string(),
375 Vec::new(),
376 ),
377 Retainer::new(Uuid::new_v4()),
378 Timestamp::from_millis(0),
379 )
380 }
381
382 fn drive(
383 kernel: &mut Kernel,
384 execution: Result<Outcome, ()>,
385 claimable: bool,
386 ) -> StepResult {
387 loop {
388 if let Some(result) = kernel.result() {
389 return result;
390 }
391 match kernel.poll() {
392 Directive::Claim => {
393 let notice = if claimable {
394 Notice::Claimed(Some(claim()))
395 } else {
396 Notice::Claimed(None)
397 };
398 kernel.on_event(notice);
399 }
400 Directive::Execute(_) => kernel.on_event(match execution {
401 Ok(outcome) => Notice::Executed(outcome),
402 Err(()) => Notice::ExecutionFailed,
403 }),
404 Directive::Settle(_, _) => kernel.on_event(Notice::Settled),
405 Directive::Idle => return StepResult::Idle,
406 }
407 }
408 }
409
410 #[test]
411 fn fulfilled_execution_settles_fulfilled() {
412 let mut kernel = Kernel::new();
413 assert_eq!(
414 drive(&mut kernel, Ok(Outcome::Fulfilled), true),
415 StepResult::Settled(Outcome::Fulfilled)
416 );
417 }
418
419 #[test]
420 fn breached_execution_settles_breached() {
421 let mut kernel = Kernel::new();
422 assert_eq!(
423 drive(&mut kernel, Ok(Outcome::Breached), true),
424 StepResult::Settled(Outcome::Breached)
425 );
426 }
427
428 #[test]
429 fn infrastructure_error_is_unsettled() {
430 let mut kernel = Kernel::new();
431 assert_eq!(drive(&mut kernel, Err(()), true), StepResult::Unsettled);
432 }
433
434 #[test]
435 fn empty_claim_is_idle() {
436 let mut kernel = Kernel::new();
437 assert_eq!(
438 drive(&mut kernel, Ok(Outcome::Fulfilled), false),
439 StepResult::Idle
440 );
441 }
442 }
443}