use serde::{Deserialize, Deserializer, Serialize};
use wasm4pm_compat::evidence::Evidence;
use wasm4pm_compat::state::Admitted;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct Blake3Hash(pub String);
impl Blake3Hash {
pub fn from_bytes(bytes: &[u8]) -> Self {
Blake3Hash(blake3::hash(bytes).to_hex().to_string())
}
pub fn from_hex(hex: impl Into<String>) -> Self {
Blake3Hash(hex.into())
}
pub fn as_hex(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for Blake3Hash {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct AffidavitReceiptChain;
pub type AdmittedReceipt = Evidence<Receipt, Admitted, AffidavitReceiptChain>;
impl Receipt {
pub(crate) fn sealed(
format_version: String,
events: Vec<OperationEvent>,
chain_hash: Blake3Hash,
) -> Self {
Receipt {
format_version,
events,
chain_hash,
_seal: (),
}
}
}
impl<'de> Deserialize<'de> for Receipt {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error;
#[derive(Deserialize)]
struct RawReceipt {
format_version: String,
events: Vec<OperationEvent>,
chain_hash: Blake3Hash,
}
let raw = RawReceipt::deserialize(deserializer)?;
let recomputed = crate::chain::recompute_chain(&raw.events)
.map_err(|e| D::Error::custom(format!("chain recomputation failed: {e}")))?;
if recomputed != raw.chain_hash {
return Err(D::Error::custom(format!(
"chain hash mismatch: receipt claims {}, recomputed {}",
raw.chain_hash, recomputed
)));
}
Ok(Receipt {
format_version: raw.format_version,
events: raw.events,
chain_hash: raw.chain_hash,
_seal: (),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ObjectRef {
pub id: String,
pub obj_type: String,
pub qualifier: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OperationEvent {
pub id: String,
pub seq: u64,
pub event_type: String,
pub objects: Vec<ObjectRef>,
pub payload_commitment: Blake3Hash,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Receipt {
pub format_version: String,
pub events: Vec<OperationEvent>,
pub chain_hash: Blake3Hash,
#[serde(skip)]
_seal: (),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ProfileId {
CoreV1,
}
impl ProfileId {
pub fn as_str(&self) -> &'static str {
match self {
ProfileId::CoreV1 => "core/v1",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CheckOutcome {
pub stage: String,
pub passed: bool,
pub detail: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Verdict {
pub accepted: bool,
pub profile: ProfileId,
pub outcomes: Vec<CheckOutcome>,
pub reason: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct InspectionReport {
pub event_count: usize,
pub format_version: String,
pub chain_hash: String,
pub chain_integrity_valid: bool,
pub event_types: std::collections::BTreeMap<String, usize>,
pub object_types: std::collections::BTreeMap<String, usize>,
pub events: Vec<EventSummary>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct EventSummary {
pub seq: u64,
pub id: String,
pub event_type: String,
pub object_count: usize,
pub commitment: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct EmitOutput {
pub event_id: String,
pub seq: u64,
pub event_type: String,
pub commitment: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AssembleOutput {
pub receipt_path: String,
pub content_address: String,
pub event_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct StatsOutput {
pub event_count: usize,
pub chain_depth: usize,
pub chain_hash: String,
pub event_type_histogram: std::collections::BTreeMap<String, usize>,
pub object_type_histogram: std::collections::BTreeMap<String, usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QualityMetricValue {
pub value: f64,
pub description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QualityMeasurement {
pub timestamp: u64,
pub stubs: QualityMetricValue,
pub types: QualityMetricValue,
pub churn: QualityMetricValue,
pub comments: QualityMetricValue,
pub complexity: QualityMetricValue,
pub clippy_warnings: QualityMetricValue,
pub rustfmt_violations: QualityMetricValue,
pub cargo_deny_issues: QualityMetricValue,
pub cargo_audit_vulnerabilities: QualityMetricValue,
pub test_coverage: QualityMetricValue,
pub doc_coverage: QualityMetricValue,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QualityViolationEvent {
pub rule: String,
pub metric: String,
pub value: f64,
pub threshold: f64,
pub z_score: f64,
pub severity: String,
pub description: String,
}
pub fn canonical_bytes<T: Serialize>(value: &T) -> Result<Vec<u8>, serde_json::Error> {
let v = serde_json::to_value(value)?;
let sorted = sort_value(v);
serde_json::to_vec(&sorted)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deserialize_rejects_forged_receipt() {
let mut asm = crate::chain::ChainAssembler::new();
let mut counter = crate::ocel::SeqCounter::new();
let event = crate::ocel::build_event(
"test",
vec![crate::ocel::object_ref("obj", "artifact")],
b"payload",
&mut counter,
)
.expect("build event");
asm.append(event).expect("append");
let honest = asm.finalize();
let honest_json = serde_json::to_string(&honest).expect("serialize");
let forged_json = honest_json.replace("\"test\"", "\"forged\"");
let result: Result<Receipt, _> = serde_json::from_str(&forged_json);
assert!(
result.is_err(),
"forged receipt should fail deserialization (chain mismatch)"
);
assert!(
result
.as_ref()
.unwrap_err()
.to_string()
.contains("chain hash mismatch"),
"error should mention chain hash mismatch"
);
}
#[test]
fn deserialize_accepts_honest_receipt() {
let mut asm = crate::chain::ChainAssembler::new();
let mut counter = crate::ocel::SeqCounter::new();
let event = crate::ocel::build_event(
"test",
vec![crate::ocel::object_ref("obj", "artifact")],
b"payload",
&mut counter,
)
.expect("build event");
asm.append(event).expect("append");
let honest = asm.finalize();
let honest_json = serde_json::to_string(&honest).expect("serialize");
let deserialized: Receipt = serde_json::from_str(&honest_json).expect("deserialize honest");
assert_eq!(deserialized, honest);
}
}
fn sort_value(value: serde_json::Value) -> serde_json::Value {
use serde_json::Value;
match value {
Value::Object(map) => {
let sorted: std::collections::BTreeMap<String, Value> =
map.into_iter().map(|(k, v)| (k, sort_value(v))).collect();
let mut out = serde_json::Map::new();
for (k, v) in sorted {
out.insert(k, v);
}
Value::Object(out)
}
Value::Array(arr) => Value::Array(arr.into_iter().map(sort_value).collect()),
other => other,
}
}