1#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
5pub enum ApiStabilityClass {
6 Stable,
8 Provisional,
10 Experimental,
12}
13
14#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct ApiSurfaceItem {
17 pub path: String,
19 pub class: ApiStabilityClass,
21}
22
23#[derive(Clone, Debug, Default, PartialEq, Eq)]
25pub struct StabilityRegistry {
26 items: Vec<ApiSurfaceItem>,
27}
28
29impl StabilityRegistry {
30 #[must_use]
32 pub fn new() -> Self {
33 Self::default()
34 }
35
36 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 #[must_use]
48 pub fn lookup(&self, path: &str) -> Option<&ApiSurfaceItem> {
49 self.items.iter().find(|item| item.path == path)
50 }
51
52 #[must_use]
54 pub fn items(&self) -> &[ApiSurfaceItem] {
55 &self.items
56 }
57
58 #[must_use]
60 pub fn provisional_count(&self) -> usize {
61 self.items.iter().filter(|item| item.class == ApiStabilityClass::Provisional).count()
62 }
63
64 #[must_use]
66 pub fn experimental_count(&self) -> usize {
67 self.items.iter().filter(|item| item.class == ApiStabilityClass::Experimental).count()
68 }
69
70 #[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 #[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 #[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 #[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 #[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}