//! The Unsettled → Settled typestate.
//!
//! Settlement is not a status field that anything can write. It is a type
//! parameter, and the only function in the workspace that produces the
//! `Settled` type takes an attestation. "A settled decision was attested by a
//! human" is therefore not checked at runtime — it is what the word
//! `Settled` *means* to the compiler.
use core::marker::PhantomData;
use jac_core::DecisionId;
use crate::attestation::Attestation;
use crate::decision::{Decision, DecisionError, decision_id};
mod sealed {
/// Sealing keeps the state vocabulary closed: no downstream crate can
/// add a third state that bypasses [`attest`](super::DecisionRecord::attest).
pub trait Sealed {}
impl Sealed for super::Unsettled {}
impl Sealed for super::Settled {}
}
/// A settlement state. Sealed: exactly two states exist.
pub trait SettleState: sealed::Sealed {
/// Label for narration and logs.
const LABEL: &'static str;
}
/// Proposed, not yet attested. The gate blocks work in this state's scope.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Unsettled;
/// Attested by a human. The only path here is [`DecisionRecord::attest`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Settled;
impl SettleState for Unsettled {
const LABEL: &'static str = "unsettled";
}
impl SettleState for Settled {
const LABEL: &'static str = "settled";
}
/// A decision in a known settlement state.
///
/// ```compile_fail
/// use jac_decision::{DecisionRecord, Settled};
///
/// // There is no constructor for the Settled state — fields are private and
/// // `propose` exists only on DecisionRecord<Unsettled>. The single
/// // producing path is `attest`, which consumes an Attestation.
/// let record: DecisionRecord<Settled> = DecisionRecord {
/// _state: core::marker::PhantomData,
/// };
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DecisionRecord<S: SettleState> {
id: DecisionId,
decision: Decision,
/// `Some` if and only if `S = Settled`; privacy plus the single
/// producing path keep that biconditional true.
attestation: Option<Attestation>,
_state: PhantomData<fn() -> S>,
}
impl<S: SettleState> DecisionRecord<S> {
/// The decision's content address.
#[must_use]
pub const fn id(&self) -> DecisionId {
self.id
}
/// The decision itself.
#[must_use]
pub const fn decision(&self) -> &Decision {
&self.decision
}
/// The state's label, for narration.
#[must_use]
pub const fn state_label(&self) -> &'static str {
S::LABEL
}
}
impl DecisionRecord<Unsettled> {
/// The only entry point: every decision starts unsettled.
#[must_use]
pub fn propose(decision: Decision) -> Self {
Self {
id: decision_id(&decision),
decision,
attestation: None,
_state: PhantomData,
}
}
/// The only path to `Settled`.
///
/// Consumes the unsettled record and the attestation; checks the
/// attestation actually names this decision.
///
/// # Errors
///
/// Returns [`DecisionError::WrongDecision`] if the attestation names a
/// different decision id.
pub fn attest(
self,
attestation: Attestation,
) -> Result<DecisionRecord<Settled>, DecisionError> {
if attestation.decision() != self.id {
return Err(DecisionError::WrongDecision);
}
Ok(DecisionRecord {
id: self.id,
decision: self.decision,
attestation: Some(attestation),
_state: PhantomData,
})
}
}
impl DecisionRecord<Settled> {
/// The attestation that settled this decision.
///
/// Infallible by construction: the only producing path stored one. The
/// fallback exists solely to keep this panic-free without `unwrap`.
#[must_use]
pub const fn attestation(&self) -> Option<&Attestation> {
self.attestation.as_ref()
}
}
#[cfg(test)]
#[expect(
clippy::unwrap_used,
reason = "test assertions read better with unwrap"
)]
mod tests {
use jac_core::{Author, DecisionId, Digest, HumanId, HumanIdentity, RepoPath, Timestamp};
use super::*;
use crate::attestation::Statement;
use crate::decision::{DecisionScope, RationaleFamily};
fn ada() -> HumanIdentity {
HumanIdentity {
id: HumanId::new(1).unwrap(),
display_name: "Ada".to_owned(),
}
}
fn proposal() -> DecisionRecord<Unsettled> {
DecisionRecord::propose(Decision {
title: "auth retry fails closed".to_owned(),
rationale: "drafted by the interviewer from the diff".to_owned(),
families: vec![RationaleFamily::Constraints],
proposed_by: Author::Human(HumanId::new(1).unwrap()),
scope: DecisionScope {
path_prefixes: vec![RepoPath::parse("src/auth").unwrap()],
},
at: Timestamp::from_millis(10),
})
}
#[test]
fn attest_settles() {
let record = proposal();
let attestation = Attestation::new(
&ada(),
record.id(),
Statement::new("Walked the 401 path by hand; it fails closed.").unwrap(),
Timestamp::from_millis(20),
);
let settled = record.attest(attestation).unwrap();
assert_eq!(settled.state_label(), "settled");
assert!(settled.attestation().is_some());
}
#[test]
fn attesting_the_wrong_decision_is_rejected() {
let record = proposal();
let stray = Attestation::new(
&ada(),
DecisionId::from_digest(Digest::of(b"some other decision")),
Statement::new("looks fine").unwrap(),
Timestamp::from_millis(20),
);
assert_eq!(record.attest(stray), Err(DecisionError::WrongDecision));
}
}