//! The attunement gate: pure, sans-IO, fail-closed.
//!
//! One function — [`evaluate`] — answers one question: given the paths a
//! change touched, do any decisions governing those paths remain unsettled?
//! Every failure mode lands on [`Verdict::Blocked`]: a decision the ledger
//! names but cannot report a status for, an unrecognised status variant from
//! a future ledger, a verdict nobody computed. Unproven is not passed.
//!
//! Honest scope, stated once and repeated in the demo: admitting a change
//! with *no* governing decisions means the gate binds **declared** scopes
//! only. It proves "every settled decision in the blast radius was attested
//! by a human"; it does not prove the change is good, and it does not invent
//! governance where none was declared.
//!
//! Nobody is punished by a block. The verdict names the unsettled decisions
//! so the surface can route a human to them; the work parks, the decision
//! surfaces, and attestation — recognised, never policed — reopens the way.
use std::collections::BTreeSet;
use jac_core::{DecisionId, RepoPath};
use jac_decision::{DecisionLedger, DecisionStatus};
/// Why a change is blocked.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum BlockReason {
/// No evaluation has happened. The default state of the world.
NotEvaluated,
/// Governing decisions exist that no human has attested.
UnsettledDecisions,
/// The ledger named a decision it could not report a status for.
LedgerInconsistent,
}
/// The gate's answer.
///
/// The `Default` is the restrictive variant. A `GateReport` nobody filled in
/// blocks; a match arm nobody wrote blocks (consumers match
/// `Admitted { .. }` and treat everything else as blocked).
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Verdict {
/// The change may not promote.
Blocked {
/// The unsettled decisions in the blast radius, for routing a human
/// to them. Empty when the block is structural (not evaluated,
/// ledger inconsistency).
unsettled: Vec<DecisionId>,
/// Why.
reason: BlockReason,
},
/// Every governing decision in the blast radius is settled.
Admitted {
/// How many settled decisions were checked. Zero is reported
/// honestly: it means the blast radius was ungoverned, not that
/// anything was verified.
decisions_checked: usize,
},
}
impl Default for Verdict {
/// Unproven is not passed.
fn default() -> Self {
Self::Blocked {
unsettled: Vec::new(),
reason: BlockReason::NotEvaluated,
}
}
}
impl Verdict {
/// Whether this verdict admits the change.
///
/// Matches the `Admitted` variant literally; any other variant — present
/// or future — is not an admission.
#[must_use]
pub const fn is_admitted(&self) -> bool {
matches!(self, Self::Admitted { .. })
}
}
/// Evaluates a change's blast radius against the decision ledger.
///
/// Pure: paths in, verdict out. The ledger is read, never written.
#[must_use]
pub fn evaluate(ledger: &dyn DecisionLedger, changed: &BTreeSet<RepoPath>) -> Verdict {
let mut unsettled = BTreeSet::new();
let mut settled = BTreeSet::new();
for path in changed {
for (id, status) in ledger.governing(path) {
match status {
DecisionStatus::Settled => {
settled.insert(id);
}
DecisionStatus::Unsettled => {
unsettled.insert(id);
}
// A status this gate does not recognise is not a settlement.
_ => {
return Verdict::Blocked {
unsettled: vec![id],
reason: BlockReason::LedgerInconsistent,
};
}
}
}
}
// Cross-check: every decision the ledger claimed must still answer for
// itself. A ledger that names a decision and then cannot report it is
// not a ledger the gate can trust.
for id in unsettled.iter().chain(settled.iter()) {
if ledger.status(*id).is_none() {
return Verdict::Blocked {
unsettled: vec![*id],
reason: BlockReason::LedgerInconsistent,
};
}
}
if unsettled.is_empty() {
Verdict::Admitted {
decisions_checked: settled.len(),
}
} else {
Verdict::Blocked {
unsettled: unsettled.into_iter().collect(),
reason: BlockReason::UnsettledDecisions,
}
}
}
#[cfg(test)]
#[expect(
clippy::unwrap_used,
reason = "test assertions read better with unwrap"
)]
mod tests {
use jac_core::{AgentId, Author, HumanId, HumanIdentity, Timestamp};
use jac_decision::{
Attestation, Decision, DecisionRecord, DecisionScope, MemoryLedger, RationaleFamily,
Statement,
};
use super::*;
fn paths(list: &[&str]) -> BTreeSet<RepoPath> {
list.iter().map(|p| RepoPath::parse(p).unwrap()).collect()
}
fn auth_decision() -> DecisionRecord<jac_decision::Unsettled> {
DecisionRecord::propose(Decision {
title: "auth retry fails closed".to_owned(),
rationale: "never retry 401/403 with cached credentials".to_owned(),
families: vec![RationaleFamily::Constraints],
proposed_by: Author::Agent(AgentId::new(3).unwrap()),
scope: DecisionScope {
path_prefixes: vec![RepoPath::parse("src/auth").unwrap()],
},
at: Timestamp::from_millis(1),
})
}
#[test]
fn default_verdict_is_blocked() {
assert!(!Verdict::default().is_admitted());
assert_eq!(
Verdict::default(),
Verdict::Blocked {
unsettled: Vec::new(),
reason: BlockReason::NotEvaluated
}
);
}
#[test]
fn unsettled_decision_in_scope_blocks() {
let mut ledger = MemoryLedger::new();
let record = auth_decision();
let id = record.id();
ledger.record_proposed(&record);
let verdict = evaluate(&ledger, &paths(&["src/auth/backoff.rs"]));
assert_eq!(
verdict,
Verdict::Blocked {
unsettled: vec![id],
reason: BlockReason::UnsettledDecisions
}
);
}
#[test]
fn settled_decision_admits_and_is_counted() {
let mut ledger = MemoryLedger::new();
let record = auth_decision();
let id = record.id();
let ada = HumanIdentity {
id: HumanId::new(1).unwrap(),
display_name: "Ada".to_owned(),
};
let settled = record
.attest(Attestation::new(
&ada,
id,
Statement::new("walked the 401 path, fails closed").unwrap(),
Timestamp::from_millis(2),
))
.unwrap();
ledger.record_settled(&settled);
let verdict = evaluate(&ledger, &paths(&["src/auth/backoff.rs"]));
assert_eq!(
verdict,
Verdict::Admitted {
decisions_checked: 1
}
);
}
#[test]
fn ungoverned_paths_admit_with_zero_checked() {
let mut ledger = MemoryLedger::new();
ledger.record_proposed(&auth_decision());
let verdict = evaluate(&ledger, &paths(&["docs/readme.md"]));
assert_eq!(
verdict,
Verdict::Admitted {
decisions_checked: 0
},
"the gate binds declared scopes only, and says so via the count"
);
}
#[test]
fn out_of_scope_unsettled_decisions_do_not_block() {
let mut ledger = MemoryLedger::new();
ledger.record_proposed(&auth_decision());
// `src/auth-x` is a string-prefix of nothing under `src/auth`
// segment-wise; the classic prefix bug must not create governance.
let verdict = evaluate(&ledger, &paths(&["src/auth-x/lib.rs"]));
assert!(verdict.is_admitted());
}
#[test]
fn empty_change_admits_trivially() {
let ledger = MemoryLedger::new();
let verdict = evaluate(&ledger, &BTreeSet::new());
assert_eq!(
verdict,
Verdict::Admitted {
decisions_checked: 0
}
);
}
}