use super::obligations::AggregateObligations;
use crate::model::{DependencyGraph, LicenseAnalysis, LicenseCategory, SourceDisclosureLevel};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CompatibilityStatus {
Compatible,
Warning,
Incompatible,
NeedsReview,
}
impl CompatibilityStatus {
pub fn label(&self) -> &'static str {
match self {
CompatibilityStatus::Compatible => "COMPATIBLE",
CompatibilityStatus::Warning => "WARNING",
CompatibilityStatus::Incompatible => "INCOMPATIBLE",
CompatibilityStatus::NeedsReview => "NEEDS_REVIEW",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompatibilityReport {
pub outbound_license: String,
pub status: CompatibilityStatus,
pub summary: String,
pub findings: Vec<String>,
pub obligations: AggregateObligations,
}
impl CompatibilityReport {
pub fn evaluate(outbound_license: &str, graph: &DependencyGraph, prod_only: bool) -> Self {
let obs = AggregateObligations::compute_from_graph(graph, prod_only);
let mut status = CompatibilityStatus::Compatible;
let mut findings = Vec::new();
let outbound_analysis = LicenseAnalysis::parse(outbound_license);
if !obs.commercial_use_allowed {
if outbound_analysis.category != LicenseCategory::NonCommercial {
status = CompatibilityStatus::Incompatible;
findings.push("Dependency contains Non-Commercial (e.g., CC-BY-NC or BUSL) license, which prohibits unrestricted commercial distribution.".to_string());
}
}
match outbound_analysis.category {
LicenseCategory::Permissive => {
if obs.worst_source_disclosure >= SourceDisclosureLevel::ProjectLevel {
status = CompatibilityStatus::Incompatible;
findings.push(format!(
"Incompatible Copyleft: Project contains Strong/Network Copyleft dependencies (e.g., GPL/AGPL). You cannot distribute this project under a permissive '{}' license without complying with project-wide source disclosure requirements.",
outbound_license
));
} else if obs.worst_source_disclosure == SourceDisclosureLevel::LibraryLevel {
if status != CompatibilityStatus::Incompatible {
status = CompatibilityStatus::Warning;
}
findings.push(
"Weak Copyleft Notice: Dependencies include Weak Copyleft (e.g., LGPL/MPL/CC-BY-SA). Releasing under a permissive license is permitted provided that the weak-copyleft components remain unmodified and dynamic linking/replacement is supported.".to_string()
);
}
}
LicenseCategory::WeakCopyleft
if obs.worst_source_disclosure >= SourceDisclosureLevel::ProjectLevel =>
{
status = CompatibilityStatus::Incompatible;
findings.push(format!(
"Incompatible Copyleft: Project contains Strong/Network Copyleft dependencies (e.g., GPL/AGPL). Distributing under Weak Copyleft '{}' is not compatible.",
outbound_license
));
}
LicenseCategory::StrongCopyleft
if obs.worst_source_disclosure >= SourceDisclosureLevel::NetworkLevel =>
{
status = CompatibilityStatus::Incompatible;
findings.push(format!(
"Network Copyleft Notice: Project contains Network Copyleft dependencies (e.g., AGPL/SSPL). Distributing under '{}' is incompatible if operated over a network (AGPL/SSPL requires network-triggered source disclosure).",
outbound_license
));
}
_ => {}
}
if obs.unknown_license_count > 0 {
if status == CompatibilityStatus::Compatible {
status = CompatibilityStatus::NeedsReview;
}
findings.push(format!(
"Manual Review Required: Found {} package(s) with UNKNOWN or non-standard licenses. Please inspect them to ensure license compliance.",
obs.unknown_license_count
));
}
if obs.notice_required {
findings.push(
"Attribution Requirement: Dependencies require copyright notices and original license texts to be included in distributions or documentation.".to_string()
);
}
let summary = match status {
CompatibilityStatus::Compatible => format!(
"All dependencies are compatible with '{}'. Safe for distribution!",
outbound_license
),
CompatibilityStatus::Warning => {
if obs.unknown_license_count > 0 {
format!(
"WARNING: Conditions required (e.g. weak copyleft) and {} unknown package(s) require review for '{}'.",
obs.unknown_license_count, outbound_license
)
} else {
format!(
"Compatible with '{}' under certain conditions (e.g. notices & un-modified weak copyleft).",
outbound_license
)
}
}
CompatibilityStatus::Incompatible => {
if obs.unknown_license_count > 0 {
format!(
"CRITICAL: Incompatible with '{}' due to strong copyleft/non-commercial constraints (plus {} unknown package(s)).",
outbound_license, obs.unknown_license_count
)
} else {
format!(
"CRITICAL: Incompatible with '{}' due to strong copyleft or non-commercial constraints.",
outbound_license
)
}
}
CompatibilityStatus::NeedsReview => format!(
"Needs manual review: {} unknown package(s) detected while verifying for '{}' (all identified packages are compatible).",
obs.unknown_license_count, outbound_license
),
};
Self {
outbound_license: outbound_license.to_string(),
status,
summary,
findings,
obligations: obs,
}
}
}