cargo-allow 0.1.3

Source-tree exception ledger and policy scanner for Rust repositories.
use allow_core::{MatchOutcome, MatchStatus};
use serde_json::Value;

pub(crate) struct StructuredDiffFixture {
    pub(crate) outcomes: Vec<MatchOutcome>,
    pub(crate) finding_changes: Vec<allow_diff::FindingPostureChange>,
    pub(crate) policy_changes: Vec<allow_diff::PolicyChange>,
}

pub(crate) fn structured_diff_fixture() -> StructuredDiffFixture {
    StructuredDiffFixture {
        outcomes: vec![test_outcome(
            MatchStatus::New,
            None,
            Some(0),
            "unreceipted panic.unwrap at src/lib.rs:1:1",
        )],
        finding_changes: vec![finding_posture_change(
            allow_diff::FindingPostureKind::New,
            "panic",
            Some("unwrap"),
            "src/lib.rs",
        )],
        policy_changes: vec![
            allow_diff::PolicyChange {
                allow_id: "allow-0001".to_string(),
                kind: allow_diff::PolicyChangeKind::SelectorPrecisionDecreased,
                severity: allow_diff::PolicyChangeSeverity::Fail,
                message: "allow-0001 selector precision decreased: 80 -> 45".to_string(),
                exception_identity: None,
                selector_identity: None,
                selector_precision: Some(allow_diff::SelectorPrecisionChange {
                    before: 80,
                    after: 45,
                    removed_fields: vec!["container", "normalized_snippet_hash"],
                    added_fields: vec![],
                }),
                scope: None,
                occurrence_limit: None,
                lifecycle: None,
                evidence: None,
                metadata: None,
                requirement: None,
                policy_status: None,
            },
            allow_diff::PolicyChange {
                allow_id: "allow-0002".to_string(),
                kind: allow_diff::PolicyChangeKind::ScopeBroadened,
                severity: allow_diff::PolicyChangeSeverity::Fail,
                message: "allow-0002 scope broadened".to_string(),
                exception_identity: None,
                selector_identity: None,
                selector_precision: None,
                scope: Some(allow_diff::ScopeChange {
                    field: allow_diff::ScopeChangeField::Effective,
                    before: Some("src/lib.rs".to_string()),
                    after: Some("src/**".to_string()),
                }),
                occurrence_limit: None,
                lifecycle: None,
                evidence: None,
                metadata: None,
                requirement: None,
                policy_status: None,
            },
            allow_diff::PolicyChange {
                allow_id: "allow-0003".to_string(),
                kind: allow_diff::PolicyChangeKind::OccurrenceLimitLoosened,
                severity: allow_diff::PolicyChangeSeverity::Fail,
                message: "allow-0003 occurrence_limit increased or removed".to_string(),
                exception_identity: None,
                selector_identity: None,
                selector_precision: None,
                scope: None,
                occurrence_limit: Some(allow_diff::OccurrenceLimitChange {
                    before: Some(1),
                    after: None,
                }),
                lifecycle: None,
                evidence: None,
                metadata: None,
                requirement: None,
                policy_status: None,
            },
            allow_diff::PolicyChange {
                allow_id: "allow-0004".to_string(),
                kind: allow_diff::PolicyChangeKind::ExpiryExtended,
                severity: allow_diff::PolicyChangeSeverity::Review,
                message: "allow-0004 expiry extended or removed".to_string(),
                exception_identity: None,
                selector_identity: None,
                selector_precision: None,
                scope: None,
                occurrence_limit: None,
                lifecycle: Some(allow_diff::LifecycleChange {
                    field: allow_diff::LifecycleChangeField::Expires,
                    before: Some("2026-09-01".to_string()),
                    after: Some("2026-12-01".to_string()),
                }),
                evidence: None,
                metadata: None,
                requirement: None,
                policy_status: None,
            },
            allow_diff::PolicyChange {
                allow_id: "allow-0005".to_string(),
                kind: allow_diff::PolicyChangeKind::EvidenceRemoved,
                severity: allow_diff::PolicyChangeSeverity::Fail,
                message: "allow-0005 evidence removed".to_string(),
                exception_identity: None,
                selector_identity: None,
                selector_precision: None,
                scope: None,
                occurrence_limit: None,
                lifecycle: None,
                evidence: Some(allow_diff::EvidenceChange {
                    field: allow_diff::EvidenceChangeField::Evidence,
                    removed: vec!["test:old-proof".to_string()],
                    added: vec![],
                }),
                metadata: None,
                requirement: None,
                policy_status: None,
            },
            allow_diff::PolicyChange {
                allow_id: "allow-0006".to_string(),
                kind: allow_diff::PolicyChangeKind::OwnerRemoved,
                severity: allow_diff::PolicyChangeSeverity::Fail,
                message: "allow-0006 owner removed".to_string(),
                exception_identity: None,
                selector_identity: None,
                selector_precision: None,
                scope: None,
                occurrence_limit: None,
                lifecycle: None,
                evidence: None,
                metadata: Some(allow_diff::MetadataChange {
                    field: allow_diff::MetadataChangeField::Owner,
                    before: Some("core".to_string()),
                    after: None,
                }),
                requirement: None,
                policy_status: None,
            },
            allow_diff::PolicyChange {
                allow_id: "requirements.owner_required".to_string(),
                kind: allow_diff::PolicyChangeKind::RequirementLoosened,
                severity: allow_diff::PolicyChangeSeverity::Fail,
                message: "requirements.owner_required loosened: true -> false".to_string(),
                exception_identity: None,
                selector_identity: None,
                selector_precision: None,
                scope: None,
                occurrence_limit: None,
                lifecycle: None,
                evidence: None,
                metadata: None,
                requirement: Some(allow_diff::RequirementChange {
                    field: allow_diff::RequirementChangeField::OwnerRequired,
                    before: true,
                    after: false,
                }),
                policy_status: None,
            },
            allow_diff::PolicyChange {
                allow_id: "policy.status".to_string(),
                kind: allow_diff::PolicyChangeKind::PolicyStatusWeakened,
                severity: allow_diff::PolicyChangeSeverity::Fail,
                message: "policy.status weakened: active -> advisory".to_string(),
                exception_identity: None,
                selector_identity: None,
                selector_precision: None,
                scope: None,
                occurrence_limit: None,
                lifecycle: None,
                evidence: None,
                metadata: None,
                requirement: None,
                policy_status: Some(allow_diff::PolicyStatusChange {
                    before: Some("active".to_string()),
                    after: Some("advisory".to_string()),
                }),
            },
            allow_diff::PolicyChange {
                allow_id: "allow-0007".to_string(),
                kind: allow_diff::PolicyChangeKind::KindChanged,
                severity: allow_diff::PolicyChangeSeverity::Fail,
                message: "allow-0007 changed governed exception kind: panic -> unsafe".to_string(),
                exception_identity: Some(allow_diff::ExceptionIdentityChange {
                    field: allow_diff::ExceptionIdentityChangeField::Kind,
                    before: Some("panic".to_string()),
                    after: Some("unsafe".to_string()),
                }),
                selector_identity: None,
                selector_precision: None,
                scope: None,
                occurrence_limit: None,
                lifecycle: None,
                evidence: None,
                metadata: None,
                requirement: None,
                policy_status: None,
            },
            allow_diff::PolicyChange {
                allow_id: "allow-0008".to_string(),
                kind: allow_diff::PolicyChangeKind::SelectorChanged,
                severity: allow_diff::PolicyChangeSeverity::Review,
                message: "allow-0008 selector identity changed".to_string(),
                exception_identity: None,
                selector_identity: Some(allow_diff::SelectorIdentityChange {
                    changed_fields: vec!["container", "normalized_snippet_hash"],
                }),
                selector_precision: None,
                scope: None,
                occurrence_limit: None,
                lifecycle: None,
                evidence: None,
                metadata: None,
                requirement: None,
                policy_status: None,
            },
        ],
    }
}

fn test_outcome(
    status: MatchStatus,
    allow_id: Option<&str>,
    finding_index: Option<usize>,
    message: &str,
) -> MatchOutcome {
    MatchOutcome {
        status,
        allow_id: allow_id.map(str::to_string),
        finding_index,
        message: message.to_string(),
        score: 100,
    }
}

fn finding_posture_change(
    kind: allow_diff::FindingPostureKind,
    finding_kind: &str,
    family: Option<&str>,
    path: &str,
) -> allow_diff::FindingPostureChange {
    allow_diff::FindingPostureChange {
        kind,
        key: format!("{finding_kind}:{path}"),
        finding_kind: finding_kind.to_string(),
        family: family.map(str::to_string),
        path: path.to_string(),
    }
}

pub(crate) fn parse_json(name: &str, json: &str) -> Value {
    match serde_json::from_str(json) {
        Ok(value) => value,
        Err(err) => std::panic::panic_any(format!("{name} should parse as JSON: {err}\n{json}")),
    }
}

pub(crate) fn first_array_item<'a>(value: &'a Value, pointer: &str) -> &'a Value {
    let Some(items) = value.pointer(pointer).and_then(Value::as_array) else {
        std::panic::panic_any(format!("{pointer} should be an array"));
    };
    let Some(item) = items.first() else {
        std::panic::panic_any(format!("{pointer} should contain at least one item"));
    };
    item
}