Skip to main content

spatialrust_platform/
stability.rs

1//! API surface stability registry.
2
3/// Stability class for a public API item.
4#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
5pub enum ApiStabilityClass {
6    /// Guaranteed within a major version.
7    Stable,
8    /// May change with notice inside a major version.
9    Provisional,
10    /// Explicitly experimental.
11    Experimental,
12}
13
14/// One registered public API surface item.
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct ApiSurfaceItem {
17    /// Crate or feature path.
18    pub path: String,
19    /// Stability class.
20    pub class: ApiStabilityClass,
21}
22
23/// Registry of API surface items used by freeze checklists.
24#[derive(Clone, Debug, Default, PartialEq, Eq)]
25pub struct StabilityRegistry {
26    items: Vec<ApiSurfaceItem>,
27}
28
29impl StabilityRegistry {
30    /// Creates an empty registry.
31    #[must_use]
32    pub fn new() -> Self {
33        Self::default()
34    }
35
36    /// Registers an item (replaces an existing path if present).
37    pub fn register(&mut self, path: impl Into<String>, class: ApiStabilityClass) {
38        let path = path.into();
39        if let Some(existing) = self.items.iter_mut().find(|item| item.path == path) {
40            existing.class = class;
41            return;
42        }
43        self.items.push(ApiSurfaceItem { path, class });
44    }
45
46    /// Looks up one path.
47    #[must_use]
48    pub fn lookup(&self, path: &str) -> Option<&ApiSurfaceItem> {
49        self.items.iter().find(|item| item.path == path)
50    }
51
52    /// Returns items.
53    #[must_use]
54    pub fn items(&self) -> &[ApiSurfaceItem] {
55        &self.items
56    }
57
58    /// Counts provisional APIs.
59    #[must_use]
60    pub fn provisional_count(&self) -> usize {
61        self.items.iter().filter(|item| item.class == ApiStabilityClass::Provisional).count()
62    }
63
64    /// Counts experimental APIs.
65    #[must_use]
66    pub fn experimental_count(&self) -> usize {
67        self.items.iter().filter(|item| item.class == ApiStabilityClass::Experimental).count()
68    }
69
70    /// Seeds the SpatialRust Vision 1.x ownership and algorithm-entry surface.
71    ///
72    /// Backend implementations and calibration/video algorithms added by later
73    /// OpenCV-outcome Epics remain provisional until their own freeze gates.
74    #[must_use]
75    pub fn vision_v1_surface() -> Self {
76        let mut registry = Self::new();
77        let stable = [
78            "spatialrust-image::Image",
79            "spatialrust-image::ImageView",
80            "spatialrust-image::ImageViewMut",
81            "spatialrust-image::PlanarImage",
82            "spatialrust-image::PlanarImageView",
83            "spatialrust-image::ImageMetadata",
84            "spatialrust-image::ImageRegion",
85            "spatialrust-camera::CameraIntrinsics",
86            "spatialrust-camera::PinholeCamera",
87            "spatialrust-camera::BrownConrady",
88            "spatialrust-camera::DepthConversionOptions",
89            "spatialrust-camera::depth_to_xyz_dense",
90            "spatialrust-camera::depth_to_xyz_dense_into",
91            "spatialrust-camera::rgbd_to_point_cloud",
92            "spatialrust-vision::VisionError",
93            "spatialrust-vision::BorderMode",
94            "spatialrust-vision::Interpolation",
95            "spatialrust-vision::resize",
96            "spatialrust-vision::resize_into",
97            "spatialrust-vision::normalize_into",
98            "spatialrust-vision::pack_chw_into",
99            "spatialrust-vision::rgb_to_gray_into",
100            "spatialrust-vision::Kernel1D",
101            "spatialrust-vision::Kernel2D",
102            "spatialrust-vision::filter2d",
103            "spatialrust-vision::BoundingBox2",
104            "spatialrust-vision::Detection",
105            "spatialrust-vision::nms",
106            "spatialrust-vision::DepthMap",
107            "spatialrust-vision::BinaryMask",
108            "spatialrust-vision::Keypoint2",
109            "spatialrust-vision::DescriptorBuffer",
110            "spatialrust-vision::FeatureSet2",
111            "spatialrust-vision::FeatureMatch",
112        ];
113        for path in stable {
114            registry.register(path, ApiStabilityClass::Stable);
115        }
116        let provisional = [
117            "spatialrust-camera::calibration",
118            "spatialrust-vision::geometry",
119            "spatialrust-vision::stereo",
120            "spatialrust-vision::optical-flow",
121            "spatialrust-vision::video",
122            "spatialrust-vision::odometry",
123            "spatialrust-vision::photography",
124            "spatialrust-vision::distance_transform_edt",
125            "spatialrust-vision::distance_transform_edt_with_spacing",
126            "spatialrust-runtime::execution-graph",
127            "spatialrust-vision::ai-adapters",
128            "spatialrust-gpu::GpuImage",
129        ];
130        for path in provisional {
131            registry.register(path, ApiStabilityClass::Provisional);
132        }
133        registry
134    }
135
136    /// Seeds the Vision 2 release surface while retaining every Vision 1 item.
137    #[must_use]
138    pub fn vision_v2_surface() -> Self {
139        let mut registry = Self::vision_v1_surface();
140        for path in [
141            "spatialrust-vision::BilinearResizeU8Plan",
142            "spatialrust-vision::NearestResizeU8Plan",
143            "spatialrust-vision::AreaResizeU8Plan",
144            "spatialrust-vision::GaussianBlurU8Workspace",
145            "spatialrust-vision::MorphologyU8Workspace",
146            "spatialrust-vision::CannyWorkspace",
147            "spatialrust-gpu::GpuAiTensor",
148            "spatialrust-gpu::GpuVisionChainOptions",
149            "spatialrust-gpu::run_gpu_vision_chain",
150        ] {
151            registry.register(path, ApiStabilityClass::Provisional);
152        }
153        registry
154    }
155
156    /// Seeds the SpatialRust 1.2 bounded-streaming release surface.
157    #[must_use]
158    pub fn bounded_streaming_v1_2_surface() -> Self {
159        let mut registry = Self::new();
160        for path in [
161            "spatialrust-records::MemoryBudget",
162            "spatialrust-records::MemoryTracker",
163            "spatialrust-records::CancellationToken",
164            "spatialrust-records::StreamOptions",
165            "spatialrust-records::StreamingReceipt",
166            "spatialrust-records::SpatialRecordChunk",
167            "spatialrust-records::BoundedSpatialRecordSource",
168            "spatialrust-records::BoundedSpatialRecordSink",
169        ] {
170            registry.register(path, ApiStabilityClass::Stable);
171        }
172        for path in [
173            "spatialrust-io::PcdChunkSource",
174            "spatialrust-io::PlyChunkSource",
175            "spatialrust-io::LasChunkSource",
176            "spatialrust-io::CopcChunkSource",
177            "spatialrust-io::BoundedSpool",
178            "spatialrust-pipeline::ChunkMapSource",
179            "spatialrust-pipeline::StreamingVoxelSource",
180            "spatialrust-pipeline::StreamingPipeline",
181            "spatialrust::spatialrust-stream",
182            "spatialrust-py::PointCloudStream",
183        ] {
184            registry.register(path, ApiStabilityClass::Provisional);
185        }
186        registry
187    }
188
189    /// Seeds the Visual 1 release surface.
190    ///
191    /// Backend-independent contracts are stable. Renderers, viewers, LOD, and
192    /// language/browser adapters remain provisional behind explicit features.
193    #[must_use]
194    pub fn visual_surface() -> Self {
195        let mut registry = Self::new();
196        for path in [
197            "spatialrust-viz::VisualPrimitive",
198            "spatialrust-viz::Camera",
199            "spatialrust-viz::VisualStyle",
200            "spatialrust-viz::VisualLayer",
201            "spatialrust-viz::TransferReceipt",
202        ] {
203            registry.register(path, ApiStabilityClass::Stable);
204        }
205        for path in [
206            "spatialrust-render-wgpu::WgpuRenderer",
207            "spatialrust-render-wgpu::GpuGeometry",
208            "spatialrust-render-wgpu::HeadlessRender",
209            "spatialrust-viewer::ViewerState",
210            "spatialrust-viewer::ViewerController",
211            "spatialrust-lod::LodPlanner",
212            "spatialrust-lod::LodGpuCache",
213            "spatialrust-web::WebViewerState",
214            "spatialrust-web::RangePlanner",
215            "python::ViewerState",
216            "python::ViewerPointSource",
217            "spatialrust_jupyter::ViewerWidget",
218        ] {
219            registry.register(path, ApiStabilityClass::Provisional);
220        }
221        registry
222    }
223
224    /// Seeds the north-star crate surface used by Epic 100 gates.
225    #[must_use]
226    pub fn north_star_surface() -> Self {
227        let mut registry = Self::new();
228        let provisional = [
229            "spatialrust-records",
230            "spatialrust-arrow",
231            "spatialrust-sync",
232            "spatialrust-mapping",
233            "spatialrust-scene",
234            "spatialrust-semantic",
235            "spatialrust-episode",
236            "spatialrust-runtime",
237            "spatialrust-interchange",
238            "spatialrust-distribute",
239            "spatialrust-platform",
240        ];
241        for path in provisional {
242            registry.register(path, ApiStabilityClass::Provisional);
243        }
244        registry.register("spatialrust-core", ApiStabilityClass::Stable);
245        registry.register("spatialrust-math", ApiStabilityClass::Stable);
246        registry.register("spatialrust-platform::LtsPolicy", ApiStabilityClass::Stable);
247        registry
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::{ApiStabilityClass, StabilityRegistry};
254
255    #[test]
256    fn north_star_surface_has_core_stable() {
257        let registry = StabilityRegistry::north_star_surface();
258        assert_eq!(registry.lookup("spatialrust-core").unwrap().class, ApiStabilityClass::Stable);
259        assert!(registry.provisional_count() >= 10);
260        assert_eq!(registry.experimental_count(), 0);
261    }
262
263    #[test]
264    fn vision_v1_surface_freezes_ownership_and_entry_points() {
265        let registry = StabilityRegistry::vision_v1_surface();
266        assert_eq!(
267            registry.lookup("spatialrust-image::Image").unwrap().class,
268            ApiStabilityClass::Stable
269        );
270        assert_eq!(
271            registry.lookup("spatialrust-gpu::GpuImage").unwrap().class,
272            ApiStabilityClass::Provisional
273        );
274        assert!(registry.items().len() >= 39);
275        assert_eq!(registry.experimental_count(), 0);
276    }
277
278    #[test]
279    fn vision_v2_surface_extends_v1_without_experimental_items() {
280        let registry = StabilityRegistry::vision_v2_surface();
281        assert_eq!(
282            registry.lookup("spatialrust-image::Image").unwrap().class,
283            ApiStabilityClass::Stable
284        );
285        assert_eq!(
286            registry.lookup("spatialrust-gpu::run_gpu_vision_chain").unwrap().class,
287            ApiStabilityClass::Provisional
288        );
289        assert_eq!(registry.experimental_count(), 0);
290    }
291
292    #[test]
293    fn streaming_surface_freezes_contracts_but_not_adapters() {
294        let registry = StabilityRegistry::bounded_streaming_v1_2_surface();
295        assert_eq!(
296            registry.lookup("spatialrust-records::StreamingReceipt").unwrap().class,
297            ApiStabilityClass::Stable
298        );
299        assert_eq!(
300            registry.lookup("spatialrust-pipeline::StreamingPipeline").unwrap().class,
301            ApiStabilityClass::Provisional
302        );
303        assert_eq!(registry.experimental_count(), 0);
304    }
305
306    #[test]
307    fn visual_surface_freezes_contracts_but_not_adapters() {
308        let registry = StabilityRegistry::visual_surface();
309        assert_eq!(
310            registry.lookup("spatialrust-viz::TransferReceipt").unwrap().class,
311            ApiStabilityClass::Stable
312        );
313        assert_eq!(
314            registry.lookup("spatialrust-web::RangePlanner").unwrap().class,
315            ApiStabilityClass::Provisional
316        );
317        assert_eq!(registry.experimental_count(), 0);
318    }
319}