pointbreak 0.10.0

Durable terminal code review for changes humans and coding agents collaborate on together
Documentation
use serde::{Deserialize, Serialize};

use super::{ExactBundleError, ExactBundleManifestV2};
use crate::canonical_hash::{canonical_json_bytes, sha256_bytes_hex};

pub const IMPORT_RECEIPT_SCHEMA_V1: &str = "pointbreak.import-receipt-prototype.v1";

#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ImportReceiptPolicyPrototypeV1 {
    DurableOperational,
    LocalProvenanceEvent,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ReceiptProjectionConsequenceV1 {
    OperationalLookup,
    LocalEventProjection,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ReceiptBackupConsequenceV1 {
    OperationalState,
    LocalJournal,
}

#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
pub struct ImportReceiptPrototypeV1 {
    pub schema: String,
    pub policy: ImportReceiptPolicyPrototypeV1,
    pub source_bundle_sha256: String,
    pub source_event_set_sha256: String,
    pub source_event_sha256: Vec<String>,
    pub local_import_context: String,
    pub receipt_sha256: String,
}

#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct LocalProvenanceEventPrototypeV1 {
    pub source_bundle_sha256: String,
    pub source_event_set_sha256: String,
    pub source_event_sha256: Vec<String>,
    pub local_import_context: String,
}

#[derive(Serialize)]
struct ReceiptHashPreimage<'a> {
    schema: &'a str,
    policy: ImportReceiptPolicyPrototypeV1,
    source_bundle_sha256: &'a str,
    source_event_set_sha256: &'a str,
    source_event_sha256: &'a [String],
    local_import_context: &'a str,
}

impl ImportReceiptPrototypeV1 {
    pub fn new(
        policy: ImportReceiptPolicyPrototypeV1,
        manifest: &ExactBundleManifestV2,
        local_import_context: impl Into<String>,
    ) -> Result<Self, ExactBundleError> {
        manifest.validate()?;
        let local_import_context = local_import_context.into();
        if local_import_context.trim().is_empty() {
            return Err(ExactBundleError::Contract(
                "local import context must not be empty".to_owned(),
            ));
        }
        let mut receipt = Self {
            schema: IMPORT_RECEIPT_SCHEMA_V1.to_owned(),
            policy,
            source_bundle_sha256: manifest.bundle_sha256.clone(),
            source_event_set_sha256: manifest.event_set_sha256.clone(),
            source_event_sha256: manifest
                .events
                .iter()
                .map(|event| event.decoded_sha256.clone())
                .collect(),
            local_import_context,
            receipt_sha256: String::new(),
        };
        receipt.source_event_sha256.sort();
        receipt.receipt_sha256 = receipt.computed_receipt_sha256()?;
        Ok(receipt)
    }

    pub fn projection_consequence(&self) -> ReceiptProjectionConsequenceV1 {
        match self.policy {
            ImportReceiptPolicyPrototypeV1::DurableOperational => {
                ReceiptProjectionConsequenceV1::OperationalLookup
            }
            ImportReceiptPolicyPrototypeV1::LocalProvenanceEvent => {
                ReceiptProjectionConsequenceV1::LocalEventProjection
            }
        }
    }

    pub fn backup_consequence(&self) -> ReceiptBackupConsequenceV1 {
        match self.policy {
            ImportReceiptPolicyPrototypeV1::DurableOperational => {
                ReceiptBackupConsequenceV1::OperationalState
            }
            ImportReceiptPolicyPrototypeV1::LocalProvenanceEvent => {
                ReceiptBackupConsequenceV1::LocalJournal
            }
        }
    }

    pub fn local_provenance_event(&self) -> Option<LocalProvenanceEventPrototypeV1> {
        (self.policy == ImportReceiptPolicyPrototypeV1::LocalProvenanceEvent).then(|| {
            LocalProvenanceEventPrototypeV1 {
                source_bundle_sha256: self.source_bundle_sha256.clone(),
                source_event_set_sha256: self.source_event_set_sha256.clone(),
                source_event_sha256: self.source_event_sha256.clone(),
                local_import_context: self.local_import_context.clone(),
            }
        })
    }

    fn computed_receipt_sha256(&self) -> Result<String, ExactBundleError> {
        let preimage = ReceiptHashPreimage {
            schema: &self.schema,
            policy: self.policy,
            source_bundle_sha256: &self.source_bundle_sha256,
            source_event_set_sha256: &self.source_event_set_sha256,
            source_event_sha256: &self.source_event_sha256,
            local_import_context: &self.local_import_context,
        };
        let value = serde_json::to_value(preimage)
            .map_err(|error| ExactBundleError::Contract(error.to_string()))?;
        let bytes = canonical_json_bytes(&value)
            .map_err(|error| ExactBundleError::Contract(error.to_string()))?;
        Ok(sha256_bytes_hex(&bytes))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::bench_support::foundation::{
        ExactBundleClosureV2, ExactBundleManifestV2, LogicalCapabilityEpochV1,
        QualificationRecordKindV1, QualificationRecordV1,
    };

    fn manifest() -> ExactBundleManifestV2 {
        ExactBundleManifestV2::new(
            "source-manifest-sha256",
            LogicalCapabilityEpochV1::foundation(),
            vec![
                QualificationRecordV1::new(
                    "event:one",
                    QualificationRecordKindV1::LegacyEvent,
                    br#"{"eventId":"event:one"}"#.to_vec(),
                ),
                QualificationRecordV1::new(
                    "content:one",
                    QualificationRecordKindV1::ObjectArtifact,
                    b"content".to_vec(),
                ),
            ],
            vec![ExactBundleClosureV2 {
                event_logical_key: "event:one".to_owned(),
                required_content_keys: vec!["content:one".to_owned()],
            }],
        )
        .expect("manifest")
    }

    #[test]
    fn receipt_is_separate_from_event_bytes_and_event_set_root() {
        let manifest = manifest();
        let original_event_bytes = manifest.events[0].decoded_bytes.clone();
        let original_event_set = manifest.event_set_sha256.clone();
        let receipt = ImportReceiptPrototypeV1::new(
            ImportReceiptPolicyPrototypeV1::DurableOperational,
            &manifest,
            "local-import-context",
        )
        .expect("receipt");

        assert_eq!(manifest.events[0].decoded_bytes, original_event_bytes);
        assert_eq!(manifest.event_set_sha256, original_event_set);
        assert_eq!(
            receipt.source_event_sha256,
            vec![manifest.events[0].decoded_sha256.clone()]
        );
        assert!(
            !serde_json::to_string(&manifest)
                .expect("manifest serializes")
                .contains("local-import-context")
        );
    }

    #[test]
    fn both_receipt_policies_expose_projection_and_backup_consequences() {
        let manifest = manifest();
        let operational = ImportReceiptPrototypeV1::new(
            ImportReceiptPolicyPrototypeV1::DurableOperational,
            &manifest,
            "local",
        )
        .expect("operational receipt");
        let provenance = ImportReceiptPrototypeV1::new(
            ImportReceiptPolicyPrototypeV1::LocalProvenanceEvent,
            &manifest,
            "local",
        )
        .expect("provenance receipt");

        assert_eq!(
            operational.projection_consequence(),
            ReceiptProjectionConsequenceV1::OperationalLookup
        );
        assert_eq!(
            operational.backup_consequence(),
            ReceiptBackupConsequenceV1::OperationalState
        );
        assert_eq!(
            provenance.projection_consequence(),
            ReceiptProjectionConsequenceV1::LocalEventProjection
        );
        assert_eq!(
            provenance.backup_consequence(),
            ReceiptBackupConsequenceV1::LocalJournal
        );
        assert!(operational.local_provenance_event().is_none());
        assert!(provenance.local_provenance_event().is_some());
    }
}