Skip to main content

spatialrust_platform/
vision2.rs

1//! SpatialRust Vision 2 fail-closed performance and resource release gate.
2
3use std::fmt::Write;
4
5use crate::{
6    BudgetKind, ConformanceReport, ConformanceStatus, LtsPolicy, PerformanceBudget,
7    PerformanceBudgetReport, ReleaseGate, ReleaseGateDecision, SecurityChecklist,
8    StabilityRegistry,
9};
10
11const REQUIRED_CASES: &[&str] = &[
12    "vision2-rust-accuracy",
13    "vision2-python-accuracy",
14    "vision2-linux",
15    "vision2-windows",
16    "vision2-macos",
17    "vision2-native-performance",
18    "vision2-python-performance",
19    "vision2-resource-budgets",
20    "vision2-gpu-transfer",
21    "vision2-pages",
22    "vision2-unsafe-audit",
23];
24
25const REQUIRED_RECEIPTS: &[&str] = &[
26    "vision2-component-baseline",
27    "vision2-resize-color",
28    "vision2-gaussian-sobel",
29    "vision2-morphology",
30    "vision2-canny",
31    "vision2-gpu-resident-chain",
32];
33
34const REQUIRED_EXAMPLES: &[&str] = &["vision_2_release_gate"];
35
36/// Typed canonical measurements consumed by the Vision 2 release gate.
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub struct Vision2Measurements {
39    /// Native RGB-to-gray allocating latency at 1080p.
40    pub native_allocate_1080p_us: u64,
41    /// Native RGB-to-gray caller-output latency at 1080p.
42    pub native_reuse_1080p_us: u64,
43    /// Python RGB-to-gray allocating latency at 1080p.
44    pub python_allocate_1080p_us: u64,
45    /// Python RGB-to-gray caller-output latency at 1080p.
46    pub python_reuse_1080p_us: u64,
47    /// Peak explicitly accounted host bytes in the canonical CPU receipt.
48    pub peak_host_memory_bytes: u64,
49    /// Dynamic allocations in the canonical caller-output operation.
50    pub steady_state_allocations: u64,
51    /// Worker count recorded by the default-thread receipt.
52    pub worker_threads: u64,
53    /// Explicit upload bytes in the canonical 4K GPU-resident chain.
54    pub gpu_host_to_device_bytes: u64,
55    /// Device-to-host bytes before an optional final readback.
56    pub gpu_device_to_host_bytes: u64,
57}
58
59/// Evidence gathered by CI and release tooling for a Vision 2 candidate.
60#[derive(Clone, Debug)]
61pub struct Vision2ReleaseEvidence {
62    /// Required accuracy, platform, audit, and documentation cases.
63    pub conformance: ConformanceReport,
64    /// Satisfied security audit evidence.
65    pub security: SecurityChecklist,
66    /// Typed performance, memory, allocation, thread, and transfer values.
67    pub measurements: Vision2Measurements,
68    /// Dated benchmark/correctness receipt identifiers.
69    pub passed_receipts: Vec<String>,
70    /// Cargo examples compiled and exercised by the candidate.
71    pub verified_examples: Vec<String>,
72    /// Migration policy identifier; must equal `vision-2`.
73    pub migration_policy: String,
74}
75
76/// Mandatory SpatialRust Vision 2 release policy.
77#[derive(Clone, Copy, Debug, Default)]
78pub struct Vision2ReleaseGate;
79
80impl Vision2ReleaseGate {
81    /// Returns conformance ids that must be present exactly once with `Pass`.
82    pub const fn required_conformance_cases() -> &'static [&'static str] {
83        REQUIRED_CASES
84    }
85
86    /// Returns dated receipt ids required by the release candidate.
87    pub const fn required_receipts() -> &'static [&'static str] {
88        REQUIRED_RECEIPTS
89    }
90
91    /// Returns runnable examples required by the release candidate.
92    pub const fn required_examples() -> &'static [&'static str] {
93        REQUIRED_EXAMPLES
94    }
95
96    /// Evaluates every mandatory item and returns all denial reasons.
97    pub fn evaluate(evidence: &Vision2ReleaseEvidence) -> ReleaseGateDecision {
98        let base = ReleaseGate {
99            stability: Some(StabilityRegistry::vision_v2_surface()),
100            conformance: Some(evidence.conformance.clone()),
101            security: Some(evidence.security.clone()),
102            lts: Some(LtsPolicy::spatialrust_v1()),
103            budgets: Some(vision2_budgets(evidence.measurements)),
104            reject_experimental: true,
105        };
106        let mut decision = base.evaluate();
107        require_passing_cases(&mut decision.reasons, &evidence.conformance);
108        require_names(
109            &mut decision.reasons,
110            "receipt",
111            REQUIRED_RECEIPTS,
112            &evidence.passed_receipts,
113        );
114        require_names(
115            &mut decision.reasons,
116            "example",
117            REQUIRED_EXAMPLES,
118            &evidence.verified_examples,
119        );
120        if evidence.migration_policy != "vision-2" {
121            decision.reasons.push("migration policy `vision-2` was not acknowledged".into());
122        }
123        decision.allowed = decision.reasons.is_empty();
124        decision
125    }
126
127    /// Generates the auditable Markdown receipt embedded in release docs.
128    #[must_use]
129    pub fn render_markdown(evidence: &Vision2ReleaseEvidence) -> String {
130        let decision = Self::evaluate(evidence);
131        let values = evidence.measurements;
132        let mut output = String::from("# Vision 2 release receipt\n\n");
133        let _ = writeln!(
134            output,
135            "Decision: **{}**\n",
136            if decision.allowed { "allowed" } else { "denied" }
137        );
138        output.push_str("| Measurement | Observed | Ceiling |\n");
139        output.push_str("| --- | ---: | ---: |\n");
140        for (label, observed, ceiling) in measurement_rows(values) {
141            let _ = writeln!(output, "| {label} | {observed} | {ceiling} |");
142        }
143        output.push_str("\nRequired receipts:\n\n");
144        for receipt in REQUIRED_RECEIPTS {
145            let present = evidence.passed_receipts.iter().any(|value| value == receipt);
146            let _ = writeln!(output, "- [{}] `{receipt}`", if present { "x" } else { " " });
147        }
148        if !decision.reasons.is_empty() {
149            output.push_str("\nDenial reasons:\n\n");
150            for reason in decision.reasons {
151                let _ = writeln!(output, "- {reason}");
152            }
153        }
154        output
155    }
156}
157
158fn require_passing_cases(reasons: &mut Vec<String>, conformance: &ConformanceReport) {
159    for required in REQUIRED_CASES {
160        let matching =
161            conformance.cases().iter().filter(|case| case.id == *required).collect::<Vec<_>>();
162        match matching.as_slice() {
163            [case] if case.status == ConformanceStatus::Pass => {}
164            [case] => {
165                reasons.push(format!("required conformance `{required}` is {:?}", case.status))
166            }
167            [] => reasons.push(format!("required conformance `{required}` is missing")),
168            _ => reasons.push(format!("required conformance `{required}` is duplicated")),
169        }
170    }
171}
172
173fn require_names(reasons: &mut Vec<String>, kind: &str, required: &[&str], actual: &[String]) {
174    for name in required {
175        let count = actual.iter().filter(|value| value.as_str() == *name).count();
176        match count {
177            1 => {}
178            0 => reasons.push(format!("required {kind} `{name}` is missing")),
179            _ => reasons.push(format!("required {kind} `{name}` is duplicated")),
180        }
181    }
182}
183
184fn measurement_rows(values: Vision2Measurements) -> [(&'static str, u64, u64); 9] {
185    [
186        ("native allocate 1080p (us)", values.native_allocate_1080p_us, 1_000),
187        ("native reuse 1080p (us)", values.native_reuse_1080p_us, 400),
188        ("Python allocate 1080p (us)", values.python_allocate_1080p_us, 1_200),
189        ("Python reuse 1080p (us)", values.python_reuse_1080p_us, 400),
190        ("peak host memory (bytes)", values.peak_host_memory_bytes, 64 * 1024 * 1024),
191        ("steady-state allocations", values.steady_state_allocations, 0),
192        ("worker threads", values.worker_threads, 12),
193        ("GPU upload (bytes)", values.gpu_host_to_device_bytes, 3840 * 2160 * 4),
194        ("GPU resident readback (bytes)", values.gpu_device_to_host_bytes, 0),
195    ]
196}
197
198fn vision2_budgets(values: Vision2Measurements) -> PerformanceBudgetReport {
199    let kinds = [
200        BudgetKind::LatencyMicros,
201        BudgetKind::LatencyMicros,
202        BudgetKind::LatencyMicros,
203        BudgetKind::LatencyMicros,
204        BudgetKind::MemoryBytes,
205        BudgetKind::AllocationCount,
206        BudgetKind::ThreadCount,
207        BudgetKind::BytesCopied,
208        BudgetKind::BytesCopied,
209    ];
210    let mut report = PerformanceBudgetReport::new();
211    for (((label, observed, ceiling), kind), id) in
212        measurement_rows(values).into_iter().zip(kinds).zip([
213            "vision2-native-allocate-1080p-us",
214            "vision2-native-reuse-1080p-us",
215            "vision2-python-allocate-1080p-us",
216            "vision2-python-reuse-1080p-us",
217            "vision2-peak-host-memory-bytes",
218            "vision2-steady-state-allocations",
219            "vision2-worker-threads",
220            "vision2-gpu-upload-bytes",
221            "vision2-gpu-resident-readback-bytes",
222        ])
223    {
224        let _ = label;
225        report.declare(PerformanceBudget { id: id.into(), kind, ceiling });
226        report.sample(id, observed);
227    }
228    report
229}
230
231#[cfg(test)]
232mod tests {
233    use super::{Vision2Measurements, Vision2ReleaseEvidence, Vision2ReleaseGate};
234    use crate::{ConformanceReport, ConformanceStatus, SecurityChecklist};
235
236    fn passing() -> Vision2ReleaseEvidence {
237        let mut conformance = ConformanceReport::new();
238        for &id in Vision2ReleaseGate::required_conformance_cases() {
239            conformance.record(id, ConformanceStatus::Pass, Some("CI receipt".into()));
240        }
241        Vision2ReleaseEvidence {
242            conformance,
243            security: SecurityChecklist::north_star_baseline_satisfied(),
244            measurements: Vision2Measurements {
245                native_allocate_1080p_us: 648,
246                native_reuse_1080p_us: 195,
247                python_allocate_1080p_us: 825,
248                python_reuse_1080p_us: 232,
249                peak_host_memory_bytes: 6_220_800,
250                steady_state_allocations: 0,
251                worker_threads: 12,
252                gpu_host_to_device_bytes: 3840 * 2160 * 4,
253                gpu_device_to_host_bytes: 0,
254            },
255            passed_receipts: Vision2ReleaseGate::required_receipts()
256                .iter()
257                .map(ToString::to_string)
258                .collect(),
259            verified_examples: Vision2ReleaseGate::required_examples()
260                .iter()
261                .map(ToString::to_string)
262                .collect(),
263            migration_policy: "vision-2".into(),
264        }
265    }
266
267    #[test]
268    fn vision2_complete_evidence_is_allowed_and_rendered() {
269        let evidence = passing();
270        assert!(Vision2ReleaseGate::evaluate(&evidence).allowed);
271        let markdown = Vision2ReleaseGate::render_markdown(&evidence);
272        assert!(markdown.contains("Decision: **allowed**"));
273        assert!(markdown.contains("vision2-gpu-resident-chain"));
274    }
275
276    #[test]
277    fn vision2_rejects_missing_skipped_and_duplicate_evidence() {
278        let mut evidence = passing();
279        let mut conformance = ConformanceReport::new();
280        for &id in Vision2ReleaseGate::required_conformance_cases() {
281            if id == "vision2-macos" {
282                conformance.record(id, ConformanceStatus::Skip, None);
283            } else if id == "vision2-pages" {
284                conformance.record(id, ConformanceStatus::Pass, None);
285                conformance.record(id, ConformanceStatus::Pass, None);
286            } else if id != "vision2-python-accuracy" {
287                conformance.record(id, ConformanceStatus::Pass, None);
288            }
289        }
290        evidence.conformance = conformance;
291        evidence.passed_receipts.pop();
292        evidence.verified_examples.push("vision_2_release_gate".into());
293        evidence.migration_policy = "vision-1".into();
294        let decision = Vision2ReleaseGate::evaluate(&evidence);
295        assert!(!decision.allowed);
296        for needle in [
297            "vision2-python-accuracy",
298            "vision2-macos",
299            "vision2-pages",
300            "vision2-gpu-resident-chain",
301            "vision_2_release_gate",
302            "migration policy",
303        ] {
304            assert!(decision.reasons.iter().any(|reason| reason.contains(needle)), "{needle}");
305        }
306    }
307
308    #[test]
309    fn vision2_rejects_each_resource_budget_overrun() {
310        let overruns: &[(&str, fn(&mut Vision2Measurements))] = &[
311            ("native-allocate", |values| values.native_allocate_1080p_us = 1_001),
312            ("native-reuse", |values| values.native_reuse_1080p_us = 401),
313            ("python-allocate", |values| values.python_allocate_1080p_us = 1_201),
314            ("python-reuse", |values| values.python_reuse_1080p_us = 401),
315            ("peak-host-memory", |values| {
316                values.peak_host_memory_bytes = 64 * 1024 * 1024 + 1;
317            }),
318            ("steady-state-allocations", |values| values.steady_state_allocations = 1),
319            ("worker-threads", |values| values.worker_threads = 13),
320            ("gpu-upload", |values| {
321                values.gpu_host_to_device_bytes = 3840 * 2160 * 4 + 1;
322            }),
323            ("gpu-resident-readback", |values| values.gpu_device_to_host_bytes = 4),
324        ];
325        for &(budget_id, mutate) in overruns {
326            let mut evidence = passing();
327            mutate(&mut evidence.measurements);
328            let decision = Vision2ReleaseGate::evaluate(&evidence);
329            assert!(!decision.allowed, "{budget_id}");
330            assert!(
331                decision.reasons.iter().any(|reason| reason.contains(budget_id)),
332                "{budget_id}: {:?}",
333                decision.reasons
334            );
335        }
336    }
337}