use serde_json::Value as JsonValue;
use vti_common::error::AppError;
use super::verdict::Verdict;
use super::verify::VerifiedFacts;
use crate::policy::engine::{self, CompiledPolicy};
pub fn evaluate(verified: &VerifiedFacts, policy: &CompiledPolicy) -> Result<Verdict, AppError> {
let input = verified.to_input()?;
let query = verified.purpose().decision_query();
let results = engine::evaluate(policy, &query, input)?;
match decision_value(&results) {
Some(decision) => Verdict::from_decision(decision),
None => Ok(Verdict::default_deny()),
}
}
fn decision_value(results: &JsonValue) -> Option<JsonValue> {
let value = results.pointer("/result/0/expressions/0/value")?;
if matches!(value, JsonValue::Object(o) if o.is_empty()) {
return None;
}
Some(value.clone())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ceremony::facts::{
Actor, Context, Credential, CredentialStatus, Evidence, Facts, Presentation, Purpose,
State, Subject,
};
use crate::policy::engine::compile;
use serde_json::json;
use uuid::Uuid;
const JOIN_REGO: &str = r#"
package vtc.join
import future.keywords.if
import future.keywords.in
default decision := {"effect": "deny", "with": {"code": "no-matching-route"}}
decision := {"effect": "allow", "with": {"role": "member", "obligations": ["reciprocate_vmc"]}} if {
cred_trusted("WitnessCredential")
}
cred_trusted(t) if {
some c in input.evidence.presentation.credentials
c.type == t
c.issuer_trusted
c.status == "valid"
}
"#;
fn join_facts(issuer_trusted: bool) -> VerifiedFacts {
let facts = Facts {
purpose: Purpose::Join,
now: "2026-05-30T12:00:00Z".parse().unwrap(),
actor: Actor {
did: "did:key:zHuman".into(),
role: None,
authenticated: true,
},
subject: Subject {
did: "did:key:zHuman".into(),
},
context: Context {
community_did: "did:webvh:acme.example".into(),
channel: "rest".into(),
member_count: 10,
},
evidence: Evidence {
invitation: None,
presentation: Some(Presentation {
verified: true,
holder: "did:key:zHuman".into(),
credentials: vec![Credential {
credential_type: "WitnessCredential".into(),
issuer: "did:webvh:notary.example".into(),
issuer_trusted,
status: CredentialStatus::Valid,
holder_bound: true,
claims: json!({}),
valid_until: None,
}],
}),
request: Some(json!({ "agreements": {} })),
},
state: State {
subject_member: None,
},
};
VerifiedFacts::assemble(facts).expect("verified")
}
#[test]
fn allow_branch_parses_into_verdict() {
let policy = compile(JOIN_REGO, Uuid::new_v4()).expect("join.rego compiles");
let verdict = evaluate(&join_facts(true), &policy).expect("evaluate");
assert_eq!(verdict.effect(), "allow");
match verdict {
Verdict::Allow(a) => {
assert_eq!(a.role.as_deref(), Some("member"));
assert_eq!(a.obligations, vec!["reciprocate_vmc".to_string()]);
}
other => panic!("expected allow, got {other:?}"),
}
}
#[test]
fn unmatched_falls_through_to_policy_default_deny() {
let policy = compile(JOIN_REGO, Uuid::new_v4()).unwrap();
let verdict = evaluate(&join_facts(false), &policy).expect("evaluate");
assert_eq!(verdict, Verdict::default_deny());
}
#[test]
fn undefined_decision_degrades_to_host_default_deny() {
const NO_DEFAULT: &str = r#"
package vtc.join
import future.keywords.if
decision := {"effect": "allow", "with": {"role": "admin"}} if {
input.actor.role == "admin"
}
"#;
let policy = compile(NO_DEFAULT, Uuid::new_v4()).unwrap();
let verdict = evaluate(&join_facts(true), &policy).expect("evaluate");
assert_eq!(verdict, Verdict::default_deny());
}
#[test]
fn purpose_mismatched_policy_denies() {
const DIRECTORY: &str = r#"
package vtc.directory
import future.keywords.if
default decision := {"effect": "allow", "with": {"fields": ["did"]}}
"#;
let policy = compile(DIRECTORY, Uuid::new_v4()).unwrap();
let verdict = evaluate(&join_facts(true), &policy).expect("evaluate");
assert_eq!(verdict, Verdict::default_deny());
}
}