weavatrix-rust 2.17.0

Protocol-independent Rust repository intelligence: typed evidence graphs for impact, architecture, APIs, Git, n8n, Dify, Agent catalogs, Mermaid, and Web3 ABI
Documentation
use super::digest::sha3_256_parts;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;

pub const ANALYSIS_IDENTITY_SCHEMA: u32 = 1;
pub const CONFIG_INVENTORY_DETECTOR: &str = "weavatrix.config-inventory.v1";

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AnalysisIdentity {
    pub schema_version: u32,
    pub repository: String,
    pub code_revision: String,
    pub source_manifest_digest: String,
    pub extraction_config_digest: String,
    pub detector_versions: BTreeMap<String, String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub external_evidence_digest: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub external_observed_at: Option<String>,
}

impl AnalysisIdentity {
    #[must_use]
    pub fn local(
        repository: impl Into<String>,
        code_revision: impl Into<String>,
        inputs: &[(String, String)],
    ) -> Self {
        let detector_versions = BTreeMap::from([
            ("engine".to_owned(), crate::VERSION.to_owned()),
            (
                "config_inventory".to_owned(),
                CONFIG_INVENTORY_DETECTOR.to_owned(),
            ),
        ]);
        let extraction_config_digest = sha3_256_parts(&[
            CONFIG_INVENTORY_DETECTOR.as_bytes(),
            crate::VERSION.as_bytes(),
        ]);
        let mut encoded = Vec::new();
        for (path, digest) in inputs {
            encoded.extend_from_slice(path.as_bytes());
            encoded.push(0);
            encoded.extend_from_slice(digest.as_bytes());
            encoded.push(0);
        }
        Self {
            schema_version: ANALYSIS_IDENTITY_SCHEMA,
            repository: repository.into(),
            code_revision: code_revision.into(),
            source_manifest_digest: sha3_256_parts(&[&encoded]),
            extraction_config_digest,
            detector_versions,
            external_evidence_digest: None,
            external_observed_at: None,
        }
    }
}

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

    #[test]
    fn identity_json_keeps_stable_field_order() {
        let identity = AnalysisIdentity::local("repo", "rev", &[("a.yml".into(), "d1".into())]);
        let json = blazingly_json::to_string(&identity).unwrap();
        assert!(json.contains("\"schema_version\":1"));
        assert!(json.contains("source_manifest_digest"));
        assert!(!json.contains("external_evidence_digest"));
    }

    #[test]
    fn changing_one_input_digest_changes_the_manifest() {
        let left = AnalysisIdentity::local("r", "c", &[("ci.yml".into(), "one".into())]);
        let right = AnalysisIdentity::local("r", "c", &[("ci.yml".into(), "two".into())]);
        assert_ne!(left.source_manifest_digest, right.source_manifest_digest);
        assert_eq!(
            left.extraction_config_digest,
            right.extraction_config_digest
        );
    }
}