Skip to main content

adding_rakkotan/
lib.rs

1//! Explainable governance checks for an organization's Rakkotan adoption plan.
2//!
3//! This independent crate models decision evidence. It does not provision the
4//! service, grant a license, or replace legal, privacy, security, or procurement
5//! review.
6
7#![forbid(unsafe_code)]
8#![warn(missing_docs)]
9
10use std::collections::BTreeMap;
11use std::error::Error;
12use std::fmt;
13
14/// Official Rakkotan service description from Education Network Co., Ltd.
15pub const RAKKOTAN_OFFICIAL_URL: &str = "https://edu-net.co.jp/rakkotan_bak";
16
17/// Official Rakkotan Biz page for organizations.
18pub const RAKKOTAN_BIZ_OFFICIAL_URL: &str = "https://edu-net.co.jp/rakkotan_biz";
19
20/// A governance requirement for organizational adoption.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
22pub enum AdoptionPrinciple {
23    /// Name the learning or organizational outcome and how it will be measured.
24    PurposeAndOutcome,
25    /// Confirm decision authority, purchasing approval, and official terms.
26    AuthorityAndTerms,
27    /// Document what data is used, why, where, for how long, and by whom.
28    DataGovernance,
29    /// Protect learners and other users, including accessibility and support.
30    UserProtection,
31    /// Assign an accountable owner, support path, budget, and review date.
32    OperationalOwnership,
33    /// Keep export, deletion, handover, and service-exit options explicit.
34    ReversibleAdoption,
35}
36
37impl AdoptionPrinciple {
38    /// Every principle in stable report order.
39    pub const ALL: [Self; 6] = [
40        Self::PurposeAndOutcome,
41        Self::AuthorityAndTerms,
42        Self::DataGovernance,
43        Self::UserProtection,
44        Self::OperationalOwnership,
45        Self::ReversibleAdoption,
46    ];
47
48    const fn is_safety_gate(self) -> bool {
49        matches!(
50            self,
51            Self::AuthorityAndTerms | Self::DataGovernance | Self::UserProtection
52        )
53    }
54}
55
56/// Evidence-based finding for one adoption principle.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum Finding {
59    /// Evidence shows the requirement is satisfied.
60    Satisfied,
61    /// Evidence exists, but a concrete revision remains necessary.
62    NeedsRevision,
63    /// No reliable conclusion can yet be made.
64    Unknown,
65    /// Evidence shows the plan violates the requirement.
66    Violated,
67}
68
69/// A finding and the non-empty evidence supporting it.
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct Assessment {
72    finding: Finding,
73    evidence: String,
74}
75
76impl Assessment {
77    /// Creates an assessment, rejecting blank evidence.
78    pub fn new(finding: Finding, evidence: impl Into<String>) -> Result<Self, AdoptionError> {
79        let evidence = evidence.into();
80        if evidence.trim().is_empty() {
81            return Err(AdoptionError::EmptyEvidence);
82        }
83        Ok(Self { finding, evidence })
84    }
85
86    /// Returns the finding.
87    pub const fn finding(&self) -> Finding {
88        self.finding
89    }
90
91    /// Returns the supporting evidence.
92    pub fn evidence(&self) -> &str {
93        &self.evidence
94    }
95}
96
97/// An organization's proposed Rakkotan adoption.
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct AdoptionPlan {
100    organization: String,
101    assessments: BTreeMap<AdoptionPrinciple, Assessment>,
102}
103
104impl AdoptionPlan {
105    /// Creates an unassessed plan for a non-blank organization name.
106    pub fn new(organization: impl Into<String>) -> Result<Self, AdoptionError> {
107        let organization = organization.into();
108        if organization.trim().is_empty() {
109            return Err(AdoptionError::EmptyOrganization);
110        }
111        Ok(Self {
112            organization,
113            assessments: BTreeMap::new(),
114        })
115    }
116
117    /// Adds or replaces an assessment and returns the plan for chaining.
118    pub fn assess(
119        mut self,
120        principle: AdoptionPrinciple,
121        finding: Finding,
122        evidence: impl Into<String>,
123    ) -> Result<Self, AdoptionError> {
124        self.set_assessment(principle, finding, evidence)?;
125        Ok(self)
126    }
127
128    /// Adds or replaces one assessment in place.
129    pub fn set_assessment(
130        &mut self,
131        principle: AdoptionPrinciple,
132        finding: Finding,
133        evidence: impl Into<String>,
134    ) -> Result<(), AdoptionError> {
135        self.assessments
136            .insert(principle, Assessment::new(finding, evidence)?);
137        Ok(())
138    }
139
140    /// Returns the organization name.
141    pub fn organization(&self) -> &str {
142        &self.organization
143    }
144
145    /// Returns an assessment when recorded.
146    pub fn assessment(&self, principle: AdoptionPrinciple) -> Option<&Assessment> {
147        self.assessments.get(&principle)
148    }
149}
150
151/// Final adoption decision.
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub enum AdoptionDecision {
154    /// A safety gate is known to be violated; do not adopt as proposed.
155    Decline,
156    /// One or more required assessments have not been supplied.
157    NeedsEvidence,
158    /// Evidence is complete, but revision or clarification remains necessary.
159    Revise,
160    /// Every principle is satisfied with evidence.
161    Ready,
162}
163
164/// Strict policy in which authority, data, and user protection are gates.
165#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
166pub struct AdoptionPolicy;
167
168impl AdoptionPolicy {
169    /// Evaluates a plan without averaging away a safety-gate violation.
170    pub fn evaluate(plan: &AdoptionPlan) -> AdoptionReport {
171        let mut missing = Vec::new();
172        let mut unresolved = Vec::new();
173        let mut violated = Vec::new();
174
175        for principle in AdoptionPrinciple::ALL {
176            match plan.assessment(principle).map(Assessment::finding) {
177                None => missing.push(principle),
178                Some(Finding::Satisfied) => {}
179                Some(Finding::NeedsRevision | Finding::Unknown) => unresolved.push(principle),
180                Some(Finding::Violated) => violated.push(principle),
181            }
182        }
183
184        let gate_violation = violated.iter().any(|principle| principle.is_safety_gate());
185        let decision = if gate_violation {
186            AdoptionDecision::Decline
187        } else if !missing.is_empty() {
188            AdoptionDecision::NeedsEvidence
189        } else if !unresolved.is_empty() || !violated.is_empty() {
190            AdoptionDecision::Revise
191        } else {
192            AdoptionDecision::Ready
193        };
194
195        AdoptionReport {
196            decision,
197            missing,
198            unresolved,
199            violated,
200        }
201    }
202}
203
204/// Explainable result in stable principle order.
205#[derive(Debug, Clone, PartialEq, Eq)]
206pub struct AdoptionReport {
207    decision: AdoptionDecision,
208    missing: Vec<AdoptionPrinciple>,
209    unresolved: Vec<AdoptionPrinciple>,
210    violated: Vec<AdoptionPrinciple>,
211}
212
213impl AdoptionReport {
214    /// Returns the final decision.
215    pub const fn decision(&self) -> AdoptionDecision {
216        self.decision
217    }
218
219    /// Returns principles without an assessment.
220    pub fn missing(&self) -> &[AdoptionPrinciple] {
221        &self.missing
222    }
223
224    /// Returns principles requiring clarification or revision.
225    pub fn unresolved(&self) -> &[AdoptionPrinciple] {
226        &self.unresolved
227    }
228
229    /// Returns principles with a known violation.
230    pub fn violated(&self) -> &[AdoptionPrinciple] {
231        &self.violated
232    }
233}
234
235/// Invalid adoption-plan input.
236#[derive(Debug, Clone, Copy, PartialEq, Eq)]
237pub enum AdoptionError {
238    /// Organization name was blank.
239    EmptyOrganization,
240    /// Assessment evidence was blank.
241    EmptyEvidence,
242}
243
244impl fmt::Display for AdoptionError {
245    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
246        match self {
247            Self::EmptyOrganization => formatter.write_str("organization must not be blank"),
248            Self::EmptyEvidence => formatter.write_str("assessment evidence must not be blank"),
249        }
250    }
251}
252
253impl Error for AdoptionError {}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    fn ready_plan() -> AdoptionPlan {
260        AdoptionPrinciple::ALL
261            .into_iter()
262            .try_fold(
263                AdoptionPlan::new("Example Corp").unwrap(),
264                |plan, principle| plan.assess(principle, Finding::Satisfied, "reviewed evidence"),
265            )
266            .unwrap()
267    }
268
269    #[test]
270    fn complete_satisfied_plan_is_ready() {
271        assert_eq!(
272            AdoptionPolicy::evaluate(&ready_plan()).decision(),
273            AdoptionDecision::Ready
274        );
275    }
276
277    #[test]
278    fn data_violation_is_a_veto_even_with_missing_items() {
279        let plan = AdoptionPlan::new("Example Corp")
280            .unwrap()
281            .assess(
282                AdoptionPrinciple::DataGovernance,
283                Finding::Violated,
284                "retention has no limit",
285            )
286            .unwrap();
287        let report = AdoptionPolicy::evaluate(&plan);
288        assert_eq!(report.decision(), AdoptionDecision::Decline);
289        assert_eq!(report.violated(), &[AdoptionPrinciple::DataGovernance]);
290    }
291
292    #[test]
293    fn incomplete_plan_needs_evidence_in_stable_order() {
294        let report = AdoptionPolicy::evaluate(&AdoptionPlan::new("Example Corp").unwrap());
295        assert_eq!(report.decision(), AdoptionDecision::NeedsEvidence);
296        assert_eq!(report.missing(), &AdoptionPrinciple::ALL);
297    }
298
299    #[test]
300    fn revision_is_explained() {
301        let mut plan = ready_plan();
302        plan.set_assessment(
303            AdoptionPrinciple::OperationalOwnership,
304            Finding::NeedsRevision,
305            "support owner is temporary",
306        )
307        .unwrap();
308        let report = AdoptionPolicy::evaluate(&plan);
309        assert_eq!(report.decision(), AdoptionDecision::Revise);
310        assert_eq!(
311            report.unresolved(),
312            &[AdoptionPrinciple::OperationalOwnership]
313        );
314    }
315
316    #[test]
317    fn blank_inputs_are_rejected() {
318        assert_eq!(
319            AdoptionPlan::new(" "),
320            Err(AdoptionError::EmptyOrganization)
321        );
322        assert_eq!(
323            Assessment::new(Finding::Satisfied, "\t"),
324            Err(AdoptionError::EmptyEvidence)
325        );
326    }
327}