spatialrust_platform/
gate.rs1use crate::{
4 ApiStabilityClass, ConformanceReport, LtsPolicy, PerformanceBudgetReport, PlatformError,
5 PlatformResult, SecurityChecklist, StabilityRegistry,
6};
7
8#[derive(Clone, Debug, PartialEq, Eq)]
10pub struct ReleaseGateDecision {
11 pub allowed: bool,
13 pub reasons: Vec<String>,
15}
16
17#[derive(Clone, Debug, Default)]
19pub struct ReleaseGate {
20 pub stability: Option<StabilityRegistry>,
22 pub conformance: Option<ConformanceReport>,
24 pub security: Option<SecurityChecklist>,
26 pub lts: Option<LtsPolicy>,
28 pub budgets: Option<PerformanceBudgetReport>,
30 pub reject_experimental: bool,
32}
33
34impl ReleaseGate {
35 #[must_use]
37 pub fn new() -> Self {
38 Self::default()
39 }
40
41 #[must_use]
43 pub fn north_star_defaults() -> Self {
44 Self {
45 stability: Some(StabilityRegistry::north_star_surface()),
46 conformance: Some(ConformanceReport::new()),
47 security: Some(SecurityChecklist::north_star_baseline()),
48 lts: Some(LtsPolicy::spatialrust_v1()),
49 budgets: Some({
50 let mut budgets = PerformanceBudgetReport::new();
51 budgets.declare(crate::PerformanceBudget {
52 id: "north-star-e2e-latency-ms".into(),
53 kind: crate::BudgetKind::LatencyMillis,
54 ceiling: 5_000,
55 });
56 budgets.declare(crate::PerformanceBudget {
57 id: "north-star-e2e-bytes-copied".into(),
58 kind: crate::BudgetKind::BytesCopied,
59 ceiling: 64 * 1024 * 1024,
60 });
61 budgets
62 }),
63 reject_experimental: true,
64 }
65 }
66
67 pub fn evaluate(&self) -> ReleaseGateDecision {
69 let mut reasons = Vec::new();
70
71 if self.stability.is_none()
72 && self.conformance.is_none()
73 && self.security.is_none()
74 && self.lts.is_none()
75 && self.budgets.is_none()
76 {
77 reasons.push("release gate has no configured surfaces".into());
78 return ReleaseGateDecision { allowed: false, reasons };
79 }
80
81 if let Some(stability) = &self.stability {
82 if stability.items().is_empty() {
83 reasons.push("stability registry is empty".into());
84 }
85 if self.reject_experimental {
86 let experimental = stability
87 .items()
88 .iter()
89 .filter(|item| item.class == ApiStabilityClass::Experimental)
90 .count();
91 if experimental > 0 {
92 reasons.push(format!(
93 "{experimental} experimental API(s) present while reject_experimental=true"
94 ));
95 }
96 }
97 }
98
99 if let Some(conformance) = &self.conformance {
100 if conformance.cases().is_empty() {
101 reasons.push("conformance report has no cases".into());
102 }
103 if let Err(err) = conformance.assert_no_failures() {
104 reasons.push(err.to_string());
105 }
106 if conformance.pass_count() == 0 && !conformance.cases().is_empty() {
107 reasons.push("conformance report has zero passes".into());
108 }
109 }
110
111 if let Some(security) = &self.security {
112 if !security.all_satisfied() {
113 reasons.push("security checklist is not fully satisfied".into());
114 }
115 }
116
117 if let Some(lts) = &self.lts {
118 if lts.windows().is_empty() {
119 reasons.push("LTS policy has no support windows".into());
120 }
121 }
122
123 if let Some(budgets) = &self.budgets {
124 if let Err(err) = budgets.assert_within_budgets() {
125 reasons.push(err.to_string());
126 }
127 }
128
129 ReleaseGateDecision { allowed: reasons.is_empty(), reasons }
130 }
131
132 pub fn assert_allowed(&self) -> PlatformResult<()> {
134 let decision = self.evaluate();
135 if decision.allowed {
136 Ok(())
137 } else {
138 Err(PlatformError::ReleaseGateDenied { reasons: decision.reasons })
139 }
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::ReleaseGate;
146 use crate::{ConformanceStatus, SecurityChecklist};
147
148 #[test]
149 fn north_star_defaults_need_conformance_passes() {
150 let mut gate = ReleaseGate::north_star_defaults();
151 assert!(!gate.evaluate().allowed);
152 if let Some(report) = gate.conformance.as_mut() {
153 report.record("smoke", ConformanceStatus::Pass, None);
154 }
155 if let Some(security) = gate.security.as_mut() {
157 *security = SecurityChecklist::north_star_baseline_satisfied();
158 }
159 assert!(gate.assert_allowed().is_ok());
160 }
161}