crtx-core 0.1.1

Core IDs, errors, and schema constants for Cortex.
Documentation
//! Source-attestation compatibility shape for the schema v2 cutover.
//!
//! ADR 0018 requires legacy v1 rows without cryptographic proof to be
//! represented explicitly, never as a fake zero-signature [`crate::Attestation`].

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

use crate::Attestation;

/// Attestation state attached to an event source across the v1 -> v2 cutover.
///
/// `Verified` is the normal v2 path. `LegacyUnattested` records honest absence
/// of cryptographic source proof for v1-era rows or imports. `Missing` is a
/// partial-read state and must not satisfy authority-required paths.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "state", content = "value", rename_all = "snake_case")]
pub enum SourceAttestation {
    /// Normal v2 path: verified Ed25519 per ADR 0010/0014.
    Verified(Attestation),
    /// v1-era row or import without cryptographic proof.
    LegacyUnattested {
        /// When Cortex imported or migrated the legacy record.
        imported_at: DateTime<Utc>,
        /// Original timestamp recorded on the legacy source record.
        original_recorded_at: DateTime<Utc>,
    },
    /// Missing source attestation on a partial read.
    Missing,
}

impl SourceAttestation {
    /// Returns true only for cryptographically verified source proof.
    #[must_use]
    pub const fn is_verified(&self) -> bool {
        matches!(self, Self::Verified(_))
    }

    /// Returns true for legacy or partial states that cannot satisfy
    /// authority-required paths.
    #[must_use]
    pub const fn is_unattested(&self) -> bool {
        matches!(self, Self::LegacyUnattested { .. } | Self::Missing)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use std::path::{Path, PathBuf};

    use crate::{Attestation, Attestor, InMemoryAttestor};

    fn attestation_fixture() -> Attestation {
        let attestor = InMemoryAttestor::from_seed(&[7; 32]);
        Attestation {
            key_id: attestor.key_id().to_string(),
            signature: attestor.sign(b"source-attestation-test").to_bytes(),
            signed_at: "2026-05-04T12:00:00Z".parse().unwrap(),
        }
    }

    #[test]
    fn source_attestation_variants_round_trip() {
        let imported_at = "2026-05-04T13:00:00Z".parse().unwrap();
        let original_recorded_at = "2026-05-04T11:59:00Z".parse().unwrap();
        let variants = [
            SourceAttestation::Verified(attestation_fixture()),
            SourceAttestation::LegacyUnattested {
                imported_at,
                original_recorded_at,
            },
            SourceAttestation::Missing,
        ];

        for variant in variants {
            let json = serde_json::to_string(&variant).unwrap();
            let decoded: SourceAttestation = serde_json::from_str(&json).unwrap();
            assert_eq!(decoded, variant);
        }
    }

    #[test]
    fn source_attestation_wire_strings_are_stable() {
        let verified_json =
            serde_json::to_value(SourceAttestation::Verified(attestation_fixture()))
                .expect("verified variant serializes");
        assert_eq!(verified_json["state"], "verified");

        let legacy_json = serde_json::to_value(SourceAttestation::LegacyUnattested {
            imported_at: "2026-05-04T13:00:00Z".parse().unwrap(),
            original_recorded_at: "2026-05-04T11:59:00Z".parse().unwrap(),
        })
        .expect("legacy variant serializes");
        assert_eq!(legacy_json["state"], "legacy_unattested");

        let missing_json =
            serde_json::to_value(SourceAttestation::Missing).expect("missing variant serializes");
        assert_eq!(missing_json["state"], "missing");
    }

    #[test]
    fn source_attestation_authority_helpers_separate_unattested_rows() {
        assert!(SourceAttestation::Verified(attestation_fixture()).is_verified());
        assert!(!SourceAttestation::Verified(attestation_fixture()).is_unattested());
        assert!(SourceAttestation::LegacyUnattested {
            imported_at: "2026-05-04T13:00:00Z".parse().unwrap(),
            original_recorded_at: "2026-05-04T11:59:00Z".parse().unwrap(),
        }
        .is_unattested());
        assert!(SourceAttestation::Missing.is_unattested());
    }

    #[test]
    fn no_zero_signature_sentinel_in_codebase() {
        let workspace = Path::new(env!("CARGO_MANIFEST_DIR"))
            .parent()
            .and_then(Path::parent)
            .expect("cortex-core sits under crates/");
        let mut files = Vec::new();
        collect_rs_files(workspace, &mut files);
        let compact_array = ["[0;", "64]"].join(" ");
        let attestation_field = ["signature:", compact_array.as_str()].join(" ");

        for file in files {
            let text = fs::read_to_string(&file).expect("source file is readable");
            assert!(
                !text.contains(&attestation_field) && !text.contains(&compact_array),
                "zero-signature attestation sentinel is forbidden by ADR 0018: {}",
                file.display()
            );
        }
    }

    fn collect_rs_files(dir: &Path, files: &mut Vec<PathBuf>) {
        let Ok(entries) = fs::read_dir(dir) else {
            return;
        };
        for entry in entries {
            let entry = entry.expect("directory entry is readable");
            let path = entry.path();
            if should_skip(&path) {
                continue;
            }
            if path.is_dir() {
                collect_rs_files(&path, files);
            } else if path.extension().and_then(|ext| ext.to_str()) == Some("rs") {
                files.push(path);
            }
        }
    }

    fn should_skip(path: &Path) -> bool {
        path.file_name()
            .and_then(|name| name.to_str())
            .is_some_and(|name| {
                matches!(
                    name,
                    "target" | ".git" | ".cargo" | "generated" | "fixtures"
                )
            })
    }
}