use crate::types::{AdmittedReceipt, AffidavitReceiptChain, Receipt};
use wasm4pm_compat::admission::Admission;
use wasm4pm_compat::ocel::{
EventObjectLink, Object, ObjectChange, ObjectObjectLink, OcelEvent, OcelLog, OcelRefusal,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AffidavitRefusal {
OcelLawViolation(OcelRefusal),
StructuralLawViolation {
stage: String,
reason: String,
},
}
impl std::fmt::Display for AffidavitRefusal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AffidavitRefusal::OcelLawViolation(r) => write!(f, "ocel_law_violation: {r}"),
AffidavitRefusal::StructuralLawViolation { stage, reason } => {
write!(f, "structural_law_violation[{stage}]: {reason}")
}
}
}
}
impl std::error::Error for AffidavitRefusal {}
fn project_to_ocel(receipt: &Receipt) -> OcelLog {
let mut objects: Vec<Object> = Vec::new();
let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new();
let mut events: Vec<OcelEvent> = Vec::new();
let mut e2o: Vec<EventObjectLink> = Vec::new();
for ev in &receipt.events {
events.push(OcelEvent::new(&ev.id, &ev.event_type));
for o in &ev.objects {
if seen.insert(o.id.as_str()) {
objects.push(Object::new(&o.id, &o.obj_type));
}
e2o.push(EventObjectLink::new(&ev.id, &o.id));
}
}
OcelLog::new(
objects,
events,
e2o,
Vec::<ObjectObjectLink>::new(),
Vec::<ObjectChange>::new(),
)
}
pub fn admit(receipt: Receipt) -> Result<AdmittedReceipt, AffidavitRefusal> {
project_to_ocel(&receipt)
.validate()
.map_err(AffidavitRefusal::OcelLawViolation)?;
let verdict = crate::verifier::verify(&receipt);
if !verdict.accepted {
let first_fail = verdict
.outcomes
.iter()
.find(|o| !o.passed)
.map(|o| o.stage.clone())
.unwrap_or_else(|| "unknown".to_string());
return Err(AffidavitRefusal::StructuralLawViolation {
stage: first_fail,
reason: verdict.reason,
});
}
Ok(Admission::<Receipt, AffidavitReceiptChain>::new(receipt).into_evidence())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ocel::{build_event, object_ref, SeqCounter};
use crate::types::{Blake3Hash, OperationEvent};
fn honest_receipt() -> Receipt {
let mut asm = crate::chain::ChainAssembler::new();
let mut counter = SeqCounter::new();
let event = build_event(
"create",
vec![object_ref("file-1", "artifact")],
b"content",
&mut counter,
)
.expect("build event");
asm.append(event).expect("append");
asm.finalize()
}
#[test]
fn honest_receipt_is_admitted() {
let receipt = honest_receipt();
let admitted = admit(receipt.clone());
assert!(admitted.is_ok(), "honest receipt must be admittable");
assert_eq!(admitted.unwrap().into_inner(), receipt);
}
#[test]
fn empty_object_links_refused_by_ocel_court() {
let objectless = OperationEvent {
id: "evt-0".to_string(),
seq: 0,
event_type: "create".to_string(),
objects: vec![], payload_commitment: Blake3Hash::from_bytes(b"content"),
};
let chain_hash = crate::chain::recompute_chain(std::slice::from_ref(&objectless))
.expect("recompute chain");
let receipt = Receipt::sealed(
crate::chain::FORMAT_VERSION.to_string(),
vec![objectless],
chain_hash,
);
assert!(
crate::verifier::verify(&receipt).accepted,
"the affidavit verifier alone ACCEPTS the objectless receipt — proving the OCEL court is what catches it"
);
let result = admit(receipt);
assert_eq!(
result.err(),
Some(AffidavitRefusal::OcelLawViolation(
wasm4pm_compat::ocel::OcelRefusal::EmptyEventObjectLinks
)),
"objectless receipt MUST be refused by the OCEL court (EmptyEventObjectLinks)"
);
}
#[test]
fn forged_receipt_cannot_be_admitted() {
let forged_event = OperationEvent {
id: "evt-5".to_string(),
seq: 5, event_type: "create".to_string(),
objects: vec![object_ref("file-1", "artifact")],
payload_commitment: Blake3Hash::from_bytes(b"content"),
};
let chain_hash = crate::chain::recompute_chain(std::slice::from_ref(&forged_event))
.expect("recompute chain");
let forged = Receipt::sealed(
crate::chain::FORMAT_VERSION.to_string(),
vec![forged_event],
chain_hash,
);
let result = admit(forged);
assert!(
result.is_err(),
"forged (continuity-violating) receipt MUST be refused — admit() must run the structural law"
);
match result.unwrap_err() {
AffidavitRefusal::StructuralLawViolation { stage, .. } => {
assert_eq!(
stage, "continuity",
"refusal must name the continuity stage that caught the forgery"
);
}
other => panic!("expected continuity StructuralLawViolation, got {other:?}"),
}
}
}