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}