Skip to main content

vernier_partial/
error.rs

1//! Typed errors for the partial wire format and merge surface
2//! (ADR-0031, generalized in ADR-0032).
3//!
4//! These variants live in `vernier-partial` so all three paradigm
5//! crates (`vernier-core`, `vernier-semantic`, `vernier-panoptic`)
6//! share one vocabulary. Each paradigm's top-level error type wraps
7//! [`PartialError`] via a single `From` arm so the FFI can map the
8//! five Python exception classes uniformly.
9//!
10//! The [`PartialFormatErrorKind::tag`] strings are part of the public
11//! Python surface — tests assert against them — and must stay stable
12//! across format-version bumps.
13
14use thiserror::Error;
15
16/// Top-level error returned by the encode/decode and merge surface in
17/// this crate. Each variant maps 1:1 to one of the five Python
18/// exception classes (`PartialFormatMismatch`, `PartialDatasetMismatch`,
19/// `PartialParamsMismatch`, `PartialPartitionOverlap`,
20/// `PartialRankCollision`).
21#[derive(Debug, Clone, PartialEq, Eq, Error)]
22pub enum PartialError {
23    /// Framing or structural check tripped. See
24    /// [`PartialFormatErrorKind`] for which one.
25    #[error("partial wire format rejected: {kind}")]
26    Format {
27        /// Sub-discriminator naming the specific check.
28        kind: PartialFormatErrorKind,
29    },
30
31    /// Partial's `dataset_hash` does not match the receiving rank's
32    /// live dataset. Sampler / config bug; refusing protects the merge
33    /// from silently producing un-reproducible numbers.
34    #[error("partial dataset_hash mismatch: expected {expected:02x?}, got {actual:02x?}")]
35    DatasetMismatch {
36        /// Receiving rank's `dataset_hash`.
37        expected: [u8; 32],
38        /// Partial's declared `dataset_hash`.
39        actual: [u8; 32],
40    },
41
42    /// Partial's `params_hash` does not match the receiving rank's.
43    /// Means params (max_dets / iou_thresholds / use_cats / …) diverged.
44    #[error("partial params_hash mismatch: expected {expected:02x?}, got {actual:02x?}")]
45    ParamsMismatch {
46        /// Receiving rank's `params_hash`.
47        expected: [u8; 32],
48        /// Partial's declared `params_hash`.
49        actual: [u8; 32],
50    },
51
52    /// Two partials cover the same `image_id` — the disjoint-partition
53    /// rule is violated. Names both rank ids and the colliding image
54    /// so the user can fix their `DistributedSampler`.
55    #[error("partials cover image_id={image_id} on both rank {rank_a} and rank {rank_b}")]
56    PartitionOverlap {
57        /// Lower rank id (`min(a, b)`) — sorted for determinism.
58        rank_a: u32,
59        /// Higher rank id.
60        rank_b: u32,
61        /// Image id that appeared in both partials' `seen_images`.
62        image_id: i64,
63    },
64
65    /// Two strict-mode partials declare the same `rank_id`. Strict
66    /// merge requires distinct ids so the cross-rank tiebreak gives a
67    /// total order. Corrected mode tolerates collisions.
68    #[error("partials share rank_id={rank_id} in strict mode")]
69    RankCollision {
70        /// The duplicated rank id.
71        rank_id: u32,
72    },
73}
74
75/// Sub-discriminator for [`PartialError::Format`]. The validation
76/// pipeline runs cheapest-first, so the variant also indicates how
77/// far the validator got before tripping.
78///
79/// **Tag stability is part of the public Python surface.** The
80/// snake_case [`Self::tag`] strings appear on the FFI exception's
81/// `kind` attribute and are asserted against in the parity test
82/// suite. Renames require a coordinated stub + test update.
83#[derive(Debug, Clone, PartialEq, Eq, Error)]
84pub enum PartialFormatErrorKind {
85    /// Length too short to contain even the framing header.
86    #[error("partial too short: observed {observed} bytes, minimum {minimum}")]
87    TooShort {
88        /// Observed length.
89        observed: usize,
90        /// Minimum required.
91        minimum: usize,
92    },
93    /// Magic-bytes prefix did not match `b"VRPS"`.
94    #[error("wrong magic bytes: expected \"VRPS\", got {found:02x?}")]
95    WrongMagic {
96        /// First four bytes found.
97        found: [u8; 4],
98    },
99    /// `format_version` byte did not match the receiving rank's
100    /// compiled-in [`crate::envelope::FORMAT_VERSION`].
101    #[error("wrong format_version: expected {expected}, got {found}")]
102    WrongVersion {
103        /// Compiled-in version.
104        expected: u8,
105        /// Found in the partial.
106        found: u8,
107    },
108    /// CRC32 footer did not match. Truncation, transport corruption,
109    /// or hand-crafted payload.
110    #[error("crc32 footer mismatch")]
111    Crc,
112    /// `paradigm_kind` byte did not match. Means the partial belongs
113    /// to a different paradigm (e.g., a semantic partial loaded by
114    /// an instance evaluator).
115    #[error("paradigm_kind mismatch: expected {expected}, got {found}")]
116    ParadigmMismatch {
117        /// Receiving evaluator's paradigm.
118        expected: u8,
119        /// Partial's declared paradigm.
120        found: u8,
121    },
122    /// `discriminator` did not match within a paradigm. For instance:
123    /// merging a bbox partial into a segm evaluator. Each paradigm
124    /// defines its own discriminator semantics; the value is opaque
125    /// to this crate.
126    ///
127    /// The tag `"kernel_mismatch"` is preserved for backward
128    /// compatibility with ADR-0031 v1 instance partials.
129    #[error("kernel_kind mismatch: expected discriminant {expected}, got {found}")]
130    KernelMismatch {
131        /// Receiving evaluator's discriminator.
132        expected: u32,
133        /// Partial's declared discriminator.
134        found: u32,
135    },
136    /// Header `shape_fingerprint` did not match. Each paradigm
137    /// interprets the four `u32` slots: instance uses
138    /// `(n_categories, n_area_ranges, n_images, retain_iou)`,
139    /// semantic uses `(n_classes, 0, n_images, 0)`, panoptic uses
140    /// `(n_categories, 0, n_images, things_stuff_split)`.
141    ///
142    /// The tag `"grid_mismatch"` is preserved for backward
143    /// compatibility with ADR-0031 v1 instance partials.
144    #[error("shape fingerprint mismatch: {detail}")]
145    GridMismatch {
146        /// Free-form detail naming which slot mismatched.
147        detail: String,
148    },
149    /// `parity_mode` byte did not match.
150    #[error("parity_mode mismatch: expected discriminant {expected}, got {found}")]
151    ParityMismatch {
152        /// Receiving evaluator's parity mode discriminant.
153        expected: u8,
154        /// Partial's declared parity mode discriminant.
155        found: u8,
156    },
157    /// rkyv archive validation refused the payload. Pointer offsets
158    /// out of range, bad enum tag, etc.
159    #[error("rkyv archive validation failed: {detail}")]
160    RkyvDecode {
161        /// rkyv's diagnostic message.
162        detail: String,
163    },
164    /// Catch-all for internal-state failures that need to surface
165    /// through the partial-error vocabulary because they share the
166    /// FFI exception class but aren't framing-related: e.g., a
167    /// background worker that was shut down before the FFI call
168    /// reached it, or an op invoked after `finalize`. Distinct from
169    /// [`Self::RkyvDecode`] so users don't see a misleading
170    /// "rkyv_decode" tag for a non-archive failure.
171    #[error("partial wire-format internal error: {detail}")]
172    Internal {
173        /// Free-form detail. Surfaced on the FFI exception's `kind`
174        /// attribute as the literal `"internal"` tag.
175        detail: String,
176    },
177}
178
179impl PartialFormatErrorKind {
180    /// Stable snake_case identifier for this variant. Surfaced on the
181    /// FFI exception's `kind` attribute. Adding a variant requires
182    /// adding a tag here — the exhaustive match makes the omission a
183    /// compile error.
184    pub fn tag(&self) -> &'static str {
185        match self {
186            Self::TooShort { .. } => "too_short",
187            Self::WrongMagic { .. } => "wrong_magic",
188            Self::WrongVersion { .. } => "wrong_version",
189            Self::Crc => "crc",
190            Self::ParadigmMismatch { .. } => "paradigm_mismatch",
191            Self::KernelMismatch { .. } => "kernel_mismatch",
192            Self::GridMismatch { .. } => "grid_mismatch",
193            Self::ParityMismatch { .. } => "parity_mismatch",
194            Self::RkyvDecode { .. } => "rkyv_decode",
195            Self::Internal { .. } => "internal",
196        }
197    }
198}