allow-policy-legacy 0.1.9

Legacy policy adapters for cargo-allow migrations.
Documentation
use allow_core::{AllowEntry, FindingKind, Selector, normalize_path};
use std::path::PathBuf;

use crate::converter_lifecycle_support::lifecycle_from_legacy_fields;
use crate::types::LegacyDependencySurfaceRule;

pub(crate) fn entry_from_dependency_surface_rule(rule: &LegacyDependencySurfaceRule) -> AllowEntry {
    let pattern = normalize_path(&rule.pattern);
    let reason = match &rule.broad_glob_reason {
        Some(scope_reason) if !scope_reason.trim().is_empty() => {
            format!("{} Scope note: {scope_reason}", rule.reason)
        }
        _ => rule.reason.clone(),
    };
    AllowEntry {
        id: rule.id.clone(),
        kind: FindingKind::PolicyException,
        family: Some("dependency_surface".to_string()),
        path: (!rule.is_glob).then(|| PathBuf::from(&pattern)),
        glob: rule.is_glob.then(|| pattern.clone()),
        owner: rule.owner.clone(),
        classification: rule.surface.clone(),
        reason,
        evidence: dependency_surface_evidence(rule),
        links: vec![format!("legacy-policy:{}", rule.id)],
        occurrence_limit: None,
        lifecycle: lifecycle_from_legacy_fields(
            rule.created.clone(),
            rule.review_after.clone(),
            rule.expires.clone(),
        ),
        selector: Selector {
            ast_kind: Some("dependency_surface".to_string()),
            symbol: (!rule.is_glob).then(|| pattern.clone()),
            glob: Some(pattern),
            ..Selector::default()
        },
        last_seen: None,
    }
}

fn dependency_surface_evidence(rule: &LegacyDependencySurfaceRule) -> Vec<String> {
    let mut evidence = rule.evidence.clone();
    evidence.push(format!("legacy-policy:{}", rule.id));
    evidence.push(format!("surface:{}", rule.surface));
    if let Some(count) = rule.dep_count_at_baseline {
        evidence.push(format!("dep_count_at_baseline:{count}"));
    }
    evidence
}

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

    #[test]
    fn dependency_surface_path_rule_preserves_metadata_and_selector() {
        let rule = LegacyDependencySurfaceRule {
            id: "dependency-workspace".to_string(),
            pattern: "crates\\core\\Cargo.toml".to_string(),
            is_glob: false,
            surface: "workspace_manifest".to_string(),
            owner: "build".to_string(),
            reason: "Workspace manifest owns dependency declarations.".to_string(),
            broad_glob_reason: None,
            dep_count_at_baseline: Some(42),
            evidence: vec!["test:dependency_surface".to_string()],
            created: Some("2026-01-01".to_string()),
            review_after: Some("2026-10-01".to_string()),
            expires: Some("2027-01-01".to_string()),
        };

        let entry = entry_from_dependency_surface_rule(&rule);

        assert_eq!(entry.id, "dependency-workspace");
        assert_eq!(entry.kind, FindingKind::PolicyException);
        assert_eq!(entry.family.as_deref(), Some("dependency_surface"));
        assert_eq!(entry.path, Some(PathBuf::from("crates/core/Cargo.toml")));
        assert_eq!(entry.glob, None);
        assert_eq!(entry.owner, "build");
        assert_eq!(entry.classification, "workspace_manifest");
        assert_eq!(
            entry.reason,
            "Workspace manifest owns dependency declarations."
        );
        assert_eq!(
            entry.evidence,
            vec![
                "test:dependency_surface".to_string(),
                "legacy-policy:dependency-workspace".to_string(),
                "surface:workspace_manifest".to_string(),
                "dep_count_at_baseline:42".to_string(),
            ]
        );
        assert_eq!(
            entry.links,
            vec!["legacy-policy:dependency-workspace".to_string()]
        );
        assert_eq!(entry.occurrence_limit, None);
        assert_eq!(entry.lifecycle.created.as_deref(), Some("2026-01-01"));
        assert_eq!(entry.lifecycle.review_after.as_deref(), Some("2026-10-01"));
        assert_eq!(entry.lifecycle.expires.as_deref(), Some("2027-01-01"));
        assert_eq!(
            entry.selector.ast_kind.as_deref(),
            Some("dependency_surface")
        );
        assert_eq!(
            entry.selector.symbol.as_deref(),
            Some("crates/core/Cargo.toml")
        );
        assert_eq!(
            entry.selector.glob.as_deref(),
            Some("crates/core/Cargo.toml")
        );
        assert!(entry.last_seen.is_none());
    }

    #[test]
    fn dependency_surface_glob_rule_records_scope_note_and_lifecycle() {
        let rule = LegacyDependencySurfaceRule {
            id: "dependency-glob".to_string(),
            pattern: "crates\\*\\Cargo.toml".to_string(),
            is_glob: true,
            surface: "crate_manifests".to_string(),
            owner: "build".to_string(),
            reason: "Crate manifests own dependency declarations.".to_string(),
            broad_glob_reason: Some("Workspace crate manifests are the boundary.".to_string()),
            dep_count_at_baseline: None,
            evidence: Vec::new(),
            created: Some("2026-02-01".to_string()),
            review_after: None,
            expires: Some("never".to_string()),
        };

        let entry = entry_from_dependency_surface_rule(&rule);

        assert_eq!(entry.path, None);
        assert_eq!(entry.glob.as_deref(), Some("crates/*/Cargo.toml"));
        assert_eq!(
            entry.reason,
            "Crate manifests own dependency declarations. Scope note: Workspace crate manifests are the boundary."
        );
        assert_eq!(
            entry.evidence,
            vec![
                "legacy-policy:dependency-glob".to_string(),
                "surface:crate_manifests".to_string(),
            ]
        );
        assert_eq!(entry.lifecycle.created.as_deref(), Some("2026-02-01"));
        assert_eq!(entry.lifecycle.review_after.as_deref(), Some("2026-02-01"));
        assert_eq!(entry.lifecycle.expires.as_deref(), Some("never"));
        assert_eq!(entry.selector.symbol, None);
        assert_eq!(entry.selector.glob.as_deref(), Some("crates/*/Cargo.toml"));
    }
}