adding-rakkotan 0.1.0

Explainable governance checks for organizational Rakkotan adoption
Documentation
//! Explainable governance checks for an organization's Rakkotan adoption plan.
//!
//! This independent crate models decision evidence. It does not provision the
//! service, grant a license, or replace legal, privacy, security, or procurement
//! review.

#![forbid(unsafe_code)]
#![warn(missing_docs)]

use std::collections::BTreeMap;
use std::error::Error;
use std::fmt;

/// Official Rakkotan service description from Education Network Co., Ltd.
pub const RAKKOTAN_OFFICIAL_URL: &str = "https://edu-net.co.jp/rakkotan_bak";

/// Official Rakkotan Biz page for organizations.
pub const RAKKOTAN_BIZ_OFFICIAL_URL: &str = "https://edu-net.co.jp/rakkotan_biz";

/// A governance requirement for organizational adoption.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum AdoptionPrinciple {
    /// Name the learning or organizational outcome and how it will be measured.
    PurposeAndOutcome,
    /// Confirm decision authority, purchasing approval, and official terms.
    AuthorityAndTerms,
    /// Document what data is used, why, where, for how long, and by whom.
    DataGovernance,
    /// Protect learners and other users, including accessibility and support.
    UserProtection,
    /// Assign an accountable owner, support path, budget, and review date.
    OperationalOwnership,
    /// Keep export, deletion, handover, and service-exit options explicit.
    ReversibleAdoption,
}

impl AdoptionPrinciple {
    /// Every principle in stable report order.
    pub const ALL: [Self; 6] = [
        Self::PurposeAndOutcome,
        Self::AuthorityAndTerms,
        Self::DataGovernance,
        Self::UserProtection,
        Self::OperationalOwnership,
        Self::ReversibleAdoption,
    ];

    const fn is_safety_gate(self) -> bool {
        matches!(
            self,
            Self::AuthorityAndTerms | Self::DataGovernance | Self::UserProtection
        )
    }
}

/// Evidence-based finding for one adoption principle.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Finding {
    /// Evidence shows the requirement is satisfied.
    Satisfied,
    /// Evidence exists, but a concrete revision remains necessary.
    NeedsRevision,
    /// No reliable conclusion can yet be made.
    Unknown,
    /// Evidence shows the plan violates the requirement.
    Violated,
}

/// A finding and the non-empty evidence supporting it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Assessment {
    finding: Finding,
    evidence: String,
}

impl Assessment {
    /// Creates an assessment, rejecting blank evidence.
    pub fn new(finding: Finding, evidence: impl Into<String>) -> Result<Self, AdoptionError> {
        let evidence = evidence.into();
        if evidence.trim().is_empty() {
            return Err(AdoptionError::EmptyEvidence);
        }
        Ok(Self { finding, evidence })
    }

    /// Returns the finding.
    pub const fn finding(&self) -> Finding {
        self.finding
    }

    /// Returns the supporting evidence.
    pub fn evidence(&self) -> &str {
        &self.evidence
    }
}

/// An organization's proposed Rakkotan adoption.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AdoptionPlan {
    organization: String,
    assessments: BTreeMap<AdoptionPrinciple, Assessment>,
}

impl AdoptionPlan {
    /// Creates an unassessed plan for a non-blank organization name.
    pub fn new(organization: impl Into<String>) -> Result<Self, AdoptionError> {
        let organization = organization.into();
        if organization.trim().is_empty() {
            return Err(AdoptionError::EmptyOrganization);
        }
        Ok(Self {
            organization,
            assessments: BTreeMap::new(),
        })
    }

    /// Adds or replaces an assessment and returns the plan for chaining.
    pub fn assess(
        mut self,
        principle: AdoptionPrinciple,
        finding: Finding,
        evidence: impl Into<String>,
    ) -> Result<Self, AdoptionError> {
        self.set_assessment(principle, finding, evidence)?;
        Ok(self)
    }

    /// Adds or replaces one assessment in place.
    pub fn set_assessment(
        &mut self,
        principle: AdoptionPrinciple,
        finding: Finding,
        evidence: impl Into<String>,
    ) -> Result<(), AdoptionError> {
        self.assessments
            .insert(principle, Assessment::new(finding, evidence)?);
        Ok(())
    }

    /// Returns the organization name.
    pub fn organization(&self) -> &str {
        &self.organization
    }

    /// Returns an assessment when recorded.
    pub fn assessment(&self, principle: AdoptionPrinciple) -> Option<&Assessment> {
        self.assessments.get(&principle)
    }
}

/// Final adoption decision.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AdoptionDecision {
    /// A safety gate is known to be violated; do not adopt as proposed.
    Decline,
    /// One or more required assessments have not been supplied.
    NeedsEvidence,
    /// Evidence is complete, but revision or clarification remains necessary.
    Revise,
    /// Every principle is satisfied with evidence.
    Ready,
}

/// Strict policy in which authority, data, and user protection are gates.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct AdoptionPolicy;

impl AdoptionPolicy {
    /// Evaluates a plan without averaging away a safety-gate violation.
    pub fn evaluate(plan: &AdoptionPlan) -> AdoptionReport {
        let mut missing = Vec::new();
        let mut unresolved = Vec::new();
        let mut violated = Vec::new();

        for principle in AdoptionPrinciple::ALL {
            match plan.assessment(principle).map(Assessment::finding) {
                None => missing.push(principle),
                Some(Finding::Satisfied) => {}
                Some(Finding::NeedsRevision | Finding::Unknown) => unresolved.push(principle),
                Some(Finding::Violated) => violated.push(principle),
            }
        }

        let gate_violation = violated.iter().any(|principle| principle.is_safety_gate());
        let decision = if gate_violation {
            AdoptionDecision::Decline
        } else if !missing.is_empty() {
            AdoptionDecision::NeedsEvidence
        } else if !unresolved.is_empty() || !violated.is_empty() {
            AdoptionDecision::Revise
        } else {
            AdoptionDecision::Ready
        };

        AdoptionReport {
            decision,
            missing,
            unresolved,
            violated,
        }
    }
}

/// Explainable result in stable principle order.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AdoptionReport {
    decision: AdoptionDecision,
    missing: Vec<AdoptionPrinciple>,
    unresolved: Vec<AdoptionPrinciple>,
    violated: Vec<AdoptionPrinciple>,
}

impl AdoptionReport {
    /// Returns the final decision.
    pub const fn decision(&self) -> AdoptionDecision {
        self.decision
    }

    /// Returns principles without an assessment.
    pub fn missing(&self) -> &[AdoptionPrinciple] {
        &self.missing
    }

    /// Returns principles requiring clarification or revision.
    pub fn unresolved(&self) -> &[AdoptionPrinciple] {
        &self.unresolved
    }

    /// Returns principles with a known violation.
    pub fn violated(&self) -> &[AdoptionPrinciple] {
        &self.violated
    }
}

/// Invalid adoption-plan input.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AdoptionError {
    /// Organization name was blank.
    EmptyOrganization,
    /// Assessment evidence was blank.
    EmptyEvidence,
}

impl fmt::Display for AdoptionError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::EmptyOrganization => formatter.write_str("organization must not be blank"),
            Self::EmptyEvidence => formatter.write_str("assessment evidence must not be blank"),
        }
    }
}

impl Error for AdoptionError {}

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

    fn ready_plan() -> AdoptionPlan {
        AdoptionPrinciple::ALL
            .into_iter()
            .try_fold(
                AdoptionPlan::new("Example Corp").unwrap(),
                |plan, principle| plan.assess(principle, Finding::Satisfied, "reviewed evidence"),
            )
            .unwrap()
    }

    #[test]
    fn complete_satisfied_plan_is_ready() {
        assert_eq!(
            AdoptionPolicy::evaluate(&ready_plan()).decision(),
            AdoptionDecision::Ready
        );
    }

    #[test]
    fn data_violation_is_a_veto_even_with_missing_items() {
        let plan = AdoptionPlan::new("Example Corp")
            .unwrap()
            .assess(
                AdoptionPrinciple::DataGovernance,
                Finding::Violated,
                "retention has no limit",
            )
            .unwrap();
        let report = AdoptionPolicy::evaluate(&plan);
        assert_eq!(report.decision(), AdoptionDecision::Decline);
        assert_eq!(report.violated(), &[AdoptionPrinciple::DataGovernance]);
    }

    #[test]
    fn incomplete_plan_needs_evidence_in_stable_order() {
        let report = AdoptionPolicy::evaluate(&AdoptionPlan::new("Example Corp").unwrap());
        assert_eq!(report.decision(), AdoptionDecision::NeedsEvidence);
        assert_eq!(report.missing(), &AdoptionPrinciple::ALL);
    }

    #[test]
    fn revision_is_explained() {
        let mut plan = ready_plan();
        plan.set_assessment(
            AdoptionPrinciple::OperationalOwnership,
            Finding::NeedsRevision,
            "support owner is temporary",
        )
        .unwrap();
        let report = AdoptionPolicy::evaluate(&plan);
        assert_eq!(report.decision(), AdoptionDecision::Revise);
        assert_eq!(
            report.unresolved(),
            &[AdoptionPrinciple::OperationalOwnership]
        );
    }

    #[test]
    fn blank_inputs_are_rejected() {
        assert_eq!(
            AdoptionPlan::new(" "),
            Err(AdoptionError::EmptyOrganization)
        );
        assert_eq!(
            Assessment::new(Finding::Satisfied, "\t"),
            Err(AdoptionError::EmptyEvidence)
        );
    }
}