determs 0.1.0

Deterministic replay & proof engine for verifiable automated decisions — capture, verify, and replay records anyone can check. AI agents are the first profile.
Documentation
use crate::capsule::{Capsule, ExecutionOutput, ExecutionStatus, Manifest, ProblemSeverity};
use crate::hash::{sha256, Digest};
use crate::json::to_canonical_string;
use crate::profiles;
use crate::value::Value;
use std::time::{SystemTime, UNIX_EPOCH};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Receipt {
    pub capsule_id: String,
    pub capsule_digest: Digest,
    pub input_digest: Digest,
    pub output_digest: Digest,
    pub run_id: Digest,
    pub executed_at_unix_ms: u128,
}

impl Receipt {
    pub fn to_value(&self) -> Value {
        Value::object(vec![
            ("capsule_id", Value::from(self.capsule_id.clone())),
            ("capsule_digest", Value::from(self.capsule_digest.to_hex())),
            ("input_digest", Value::from(self.input_digest.to_hex())),
            ("output_digest", Value::from(self.output_digest.to_hex())),
            ("run_id", Value::from(self.run_id.to_hex())),
            (
                "executed_at_unix_ms",
                Value::from(self.executed_at_unix_ms.to_string()),
            ),
        ])
    }
}

#[derive(Debug, Clone, PartialEq)]
pub struct ExecutionRecord {
    pub manifest: Manifest,
    pub receipt: Receipt,
    pub output: ExecutionOutput,
}

impl ExecutionRecord {
    pub fn to_value(&self) -> Value {
        Value::object(vec![
            ("manifest", self.manifest.to_value()),
            ("receipt", self.receipt.to_value()),
            ("output", self.output.to_value()),
        ])
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EngineError {
    UnknownCapsule(String),
}

impl core::fmt::Display for EngineError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            EngineError::UnknownCapsule(id) => write!(f, "Unknown capsule: {}", id),
        }
    }
}

pub fn list_manifests() -> Vec<Manifest> {
    profiles::registry()
        .into_iter()
        .map(Capsule::manifest)
        .collect()
}

pub fn manifest_for(id: &str) -> Option<Manifest> {
    profiles::find(id).map(Capsule::manifest)
}

pub fn execute_capsule(id: &str, input: &Value) -> Result<ExecutionRecord, EngineError> {
    let capsule = profiles::find(id).ok_or_else(|| EngineError::UnknownCapsule(id.to_string()))?;
    let manifest = capsule.manifest();

    let mut schema_problems = Vec::new();
    manifest
        .input_schema
        .validate(input, "", &mut schema_problems);

    let mut output = capsule.execute(input);
    if !schema_problems.is_empty() {
        let mut merged = schema_problems;
        merged.extend(output.problems);
        output.problems = merged;
    }

    if output
        .problems
        .iter()
        .any(|problem| matches!(problem.severity, ProblemSeverity::Error))
    {
        output.status = ExecutionStatus::Rejected;
    }

    let capsule_digest = digest_value(&manifest.to_value());
    let input_digest = digest_value(input);
    let output_digest = digest_value(&output.to_value());
    let executed_at_unix_ms = now_unix_ms();
    let seed = format!(
        "{}:{}:{}:{}",
        capsule_digest.to_hex(),
        input_digest.to_hex(),
        output_digest.to_hex(),
        executed_at_unix_ms
    );
    let run_id = sha256(seed.as_bytes());

    Ok(ExecutionRecord {
        manifest,
        receipt: Receipt {
            capsule_id: id.to_string(),
            capsule_digest,
            input_digest,
            output_digest,
            run_id,
            executed_at_unix_ms,
        },
        output,
    })
}

pub fn brief_manifest_value(manifest: &Manifest) -> Value {
    Value::object(vec![
        ("id", Value::from(manifest.id.clone())),
        ("name", Value::from(manifest.name.clone())),
        ("profile", Value::from(manifest.profile.clone())),
        ("summary", Value::from(manifest.summary.clone())),
        ("version", Value::from(manifest.version.clone())),
    ])
}

pub fn digest_value(value: &Value) -> Digest {
    let canonical = to_canonical_string(value);
    sha256(canonical.as_bytes())
}

fn now_unix_ms() -> u128 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_millis()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_unknown_capsule() {
        let input = Value::Null;
        let error = execute_capsule("missing", &input).unwrap_err();
        assert_eq!(error, EngineError::UnknownCapsule("missing".to_string()));
    }

    #[test]
    fn test_digest_is_stable_for_same_value() {
        let value = Value::object(vec![("a", Value::from(1usize)), ("b", Value::from(true))]);
        assert_eq!(digest_value(&value), digest_value(&value));
    }
}