Skip to main content

polyc_query_model/
evidence.rs

1//! The query protocol's own closed source-evidence vocabulary.
2//!
3//! A terminal frame reports the exact premises a query read. Those premises
4//! originate in the state plane's audit types, but a client must not acquire
5//! the state plane's component graph to read them. This module owns a
6//! wire-neutral mirror instead: plain data, no transport, no engine, and no
7//! dependency outside this crate.
8//!
9//! The inward composition layer converts between these types and the state
10//! plane's own. That layer is the only place both vocabularies meet.
11//!
12//! # Canonical order
13//!
14//! [`SourceEvidence`] holds strictly ascending, duplicate-free pins under the
15//! same identity rule the durable audit applies: a variant tag, then each
16//! source's identity as length-prefixed bytes. Reproducing the rule here lets
17//! the transport refuse a reordered or duplicated vector on its own, without
18//! reaching for the state plane to do it.
19
20use std::fmt;
21use std::time::Duration;
22
23use crate::ModelError;
24
25/// Largest number of exact source premises one result may report.
26pub const MAX_SOURCE_PINS: usize = 32;
27/// Width of a partition incarnation.
28pub const INCARNATION_BYTES: usize = 32;
29/// Width of a content digest.
30pub const DIGEST_BYTES: usize = 32;
31/// Width of a journal commit root.
32pub const COMMIT_ROOT_BYTES: usize = 32;
33/// Width of a journal attestation signature.
34pub const ATTESTATION_SIGNATURE_BYTES: usize = 64;
35/// Width of a journal attestation signer key.
36pub const ATTESTATION_SIGNER_BYTES: usize = 32;
37
38fn push_len_prefixed(buffer: &mut Vec<u8>, bytes: &[u8]) {
39    buffer.extend_from_slice(&u64::try_from(bytes.len()).unwrap_or(u64::MAX).to_be_bytes());
40    buffer.extend_from_slice(bytes);
41}
42
43const fn non_empty(field: &'static str, value: &str) -> Result<(), ModelError> {
44    if value.is_empty() {
45        return Err(ModelError::Bounds(field));
46    }
47    Ok(())
48}
49
50/// One physical journal lineage.
51#[derive(Clone, PartialEq, Eq)]
52pub struct JournalSource {
53    partition: String,
54    incarnation: [u8; INCARNATION_BYTES],
55}
56
57impl JournalSource {
58    /// Names one partition incarnation.
59    ///
60    /// # Errors
61    ///
62    /// Returns [`ModelError::Bounds`] for an empty partition.
63    pub fn try_new(
64        partition: String,
65        incarnation: [u8; INCARNATION_BYTES],
66    ) -> Result<Self, ModelError> {
67        non_empty("partition", &partition)?;
68        Ok(Self {
69            partition,
70            incarnation,
71        })
72    }
73
74    /// Returns the partition this lineage belongs to.
75    #[must_use]
76    pub fn partition(&self) -> &str {
77        &self.partition
78    }
79
80    /// Returns the exact incarnation bytes.
81    #[must_use]
82    pub const fn incarnation(&self) -> &[u8; INCARNATION_BYTES] {
83        &self.incarnation
84    }
85}
86
87/// A signed journal root covering one checkpoint.
88#[derive(Clone, PartialEq, Eq)]
89pub struct JournalAttestation {
90    root: [u8; COMMIT_ROOT_BYTES],
91    leaf_count: u64,
92    signature: [u8; ATTESTATION_SIGNATURE_BYTES],
93    signer: [u8; ATTESTATION_SIGNER_BYTES],
94}
95
96impl JournalAttestation {
97    /// Records one covering attestation.
98    #[must_use]
99    pub const fn new(
100        root: [u8; COMMIT_ROOT_BYTES],
101        leaf_count: u64,
102        signature: [u8; ATTESTATION_SIGNATURE_BYTES],
103        signer: [u8; ATTESTATION_SIGNER_BYTES],
104    ) -> Self {
105        Self {
106            root,
107            leaf_count,
108            signature,
109            signer,
110        }
111    }
112
113    /// Returns the attested commit root.
114    #[must_use]
115    pub const fn root(&self) -> &[u8; COMMIT_ROOT_BYTES] {
116        &self.root
117    }
118
119    /// Returns how many leaves the root covers.
120    #[must_use]
121    pub const fn leaf_count(&self) -> u64 {
122        self.leaf_count
123    }
124
125    /// Returns the signature over the root.
126    #[must_use]
127    pub const fn signature(&self) -> &[u8; ATTESTATION_SIGNATURE_BYTES] {
128        &self.signature
129    }
130
131    /// Returns the signer key that produced the signature.
132    #[must_use]
133    pub const fn signer(&self) -> &[u8; ATTESTATION_SIGNER_BYTES] {
134        &self.signer
135    }
136}
137
138/// The exact source frontier one projection generation covers.
139#[derive(Clone, PartialEq, Eq)]
140pub struct SourceCheckpoint {
141    source: JournalSource,
142    feed_position: u64,
143    journal_position: u64,
144    evidence_leaf: u64,
145    covering_attestation: JournalAttestation,
146}
147
148impl SourceCheckpoint {
149    /// Records one exact source frontier.
150    #[must_use]
151    pub const fn new(
152        source: JournalSource,
153        feed_position: u64,
154        journal_position: u64,
155        evidence_leaf: u64,
156        covering_attestation: JournalAttestation,
157    ) -> Self {
158        Self {
159            source,
160            feed_position,
161            journal_position,
162            evidence_leaf,
163            covering_attestation,
164        }
165    }
166
167    /// Returns the lineage this checkpoint covers.
168    #[must_use]
169    pub const fn source(&self) -> &JournalSource {
170        &self.source
171    }
172
173    /// Returns the feed position.
174    #[must_use]
175    pub const fn feed_position(&self) -> u64 {
176        self.feed_position
177    }
178
179    /// Returns the journal position.
180    #[must_use]
181    pub const fn journal_position(&self) -> u64 {
182        self.journal_position
183    }
184
185    /// Returns the evidence leaf index.
186    #[must_use]
187    pub const fn evidence_leaf(&self) -> u64 {
188        self.evidence_leaf
189    }
190
191    /// Returns the attestation covering this checkpoint.
192    #[must_use]
193    pub const fn covering_attestation(&self) -> &JournalAttestation {
194        &self.covering_attestation
195    }
196}
197
198/// How long a stored object is retained.
199#[derive(Debug, Clone, Copy, PartialEq, Eq)]
200pub enum Retention {
201    /// Retained for a fixed duration.
202    For(Duration),
203    /// Retained until an explicit release.
204    UntilReleased,
205}
206
207/// What protection a stored object's content requires.
208#[derive(Debug, Clone, Copy, PartialEq, Eq)]
209pub enum Classification {
210    /// Readable without restriction.
211    Public,
212    /// Internal to the deployment.
213    Internal,
214    /// Confidential tenant content.
215    Confidential,
216    /// The most restricted class.
217    Restricted,
218}
219
220/// One immutable stored object generation.
221#[derive(Clone, PartialEq, Eq)]
222pub struct ObjectDescriptor {
223    object: String,
224    generation: u64,
225    digest: [u8; DIGEST_BYTES],
226    owner: String,
227    classification: Classification,
228    retention: Retention,
229    byte_len: u64,
230    content_reference: String,
231}
232
233impl ObjectDescriptor {
234    /// Records one immutable object generation.
235    ///
236    /// # Errors
237    ///
238    /// Returns [`ModelError::Bounds`] for an empty object id, owner, or
239    /// content reference.
240    #[allow(
241        clippy::too_many_arguments,
242        reason = "every field of the signed descriptor is named explicitly"
243    )]
244    pub fn try_new(
245        object: String,
246        generation: u64,
247        digest: [u8; DIGEST_BYTES],
248        owner: String,
249        classification: Classification,
250        retention: Retention,
251        byte_len: u64,
252        content_reference: String,
253    ) -> Result<Self, ModelError> {
254        non_empty("object", &object)?;
255        non_empty("owner", &owner)?;
256        non_empty("content_reference", &content_reference)?;
257        Ok(Self {
258            object,
259            generation,
260            digest,
261            owner,
262            classification,
263            retention,
264            byte_len,
265            content_reference,
266        })
267    }
268
269    /// Returns the object id.
270    #[must_use]
271    pub fn object(&self) -> &str {
272        &self.object
273    }
274
275    /// Returns the object generation.
276    #[must_use]
277    pub const fn generation(&self) -> u64 {
278        self.generation
279    }
280
281    /// Returns the content digest.
282    #[must_use]
283    pub const fn digest(&self) -> &[u8; DIGEST_BYTES] {
284        &self.digest
285    }
286
287    /// Returns the owner.
288    #[must_use]
289    pub fn owner(&self) -> &str {
290        &self.owner
291    }
292
293    /// Returns the content classification.
294    #[must_use]
295    pub const fn classification(&self) -> Classification {
296        self.classification
297    }
298
299    /// Returns the retention policy.
300    #[must_use]
301    pub const fn retention(&self) -> Retention {
302        self.retention
303    }
304
305    /// Returns the stored byte length.
306    #[must_use]
307    pub const fn byte_len(&self) -> u64 {
308        self.byte_len
309    }
310
311    /// Returns the content reference.
312    #[must_use]
313    pub fn content_reference(&self) -> &str {
314        &self.content_reference
315    }
316}
317
318/// One exact stored object in one namespace.
319#[derive(Clone, PartialEq, Eq)]
320pub struct ExactObjectRef {
321    namespace: String,
322    key: String,
323    backend_generation: u64,
324}
325
326impl ExactObjectRef {
327    /// Names one exact stored generation.
328    ///
329    /// # Errors
330    ///
331    /// Returns [`ModelError::Bounds`] for an empty namespace or key.
332    pub fn try_new(
333        namespace: String,
334        key: String,
335        backend_generation: u64,
336    ) -> Result<Self, ModelError> {
337        non_empty("namespace", &namespace)?;
338        non_empty("key", &key)?;
339        Ok(Self {
340            namespace,
341            key,
342            backend_generation,
343        })
344    }
345
346    /// Returns the object namespace.
347    #[must_use]
348    pub fn namespace(&self) -> &str {
349        &self.namespace
350    }
351
352    /// Returns the object key.
353    #[must_use]
354    pub fn key(&self) -> &str {
355        &self.key
356    }
357
358    /// Returns the backend generation.
359    #[must_use]
360    pub const fn backend_generation(&self) -> u64 {
361        self.backend_generation
362    }
363}
364
365/// One projection family bound to one source partition.
366#[derive(Clone, PartialEq, Eq)]
367pub struct ProjectionKey {
368    family: String,
369    source_partition: String,
370}
371
372impl ProjectionKey {
373    /// Names one projection key.
374    ///
375    /// # Errors
376    ///
377    /// Returns [`ModelError::Bounds`] for an empty family or partition.
378    pub fn try_new(family: String, source_partition: String) -> Result<Self, ModelError> {
379        non_empty("family", &family)?;
380        non_empty("source_partition", &source_partition)?;
381        Ok(Self {
382            family,
383            source_partition,
384        })
385    }
386
387    /// Returns the family name.
388    #[must_use]
389    pub fn family(&self) -> &str {
390        &self.family
391    }
392
393    /// Returns the source partition.
394    #[must_use]
395    pub fn source_partition(&self) -> &str {
396        &self.source_partition
397    }
398}
399
400/// The publisher term that produced one generation.
401#[derive(Clone, PartialEq, Eq)]
402pub struct PublisherFence {
403    key: ProjectionKey,
404    source_incarnation: [u8; INCARNATION_BYTES],
405    term: u64,
406}
407
408impl PublisherFence {
409    /// Records one publisher term.
410    ///
411    /// # Errors
412    ///
413    /// Returns [`ModelError::Bounds`] for a zero term.
414    pub fn try_new(
415        key: ProjectionKey,
416        source_incarnation: [u8; INCARNATION_BYTES],
417        term: u64,
418    ) -> Result<Self, ModelError> {
419        if term == 0 {
420            return Err(ModelError::Bounds("term"));
421        }
422        Ok(Self {
423            key,
424            source_incarnation,
425            term,
426        })
427    }
428
429    /// Returns the fenced projection key.
430    #[must_use]
431    pub const fn key(&self) -> &ProjectionKey {
432        &self.key
433    }
434
435    /// Returns the fenced source incarnation.
436    #[must_use]
437    pub const fn source_incarnation(&self) -> &[u8; INCARNATION_BYTES] {
438        &self.source_incarnation
439    }
440
441    /// Returns the publisher term.
442    #[must_use]
443    pub const fn term(&self) -> u64 {
444        self.term
445    }
446}
447
448/// One published projection generation, described exactly.
449#[derive(Clone, PartialEq, Eq)]
450pub struct ProjectionManifest {
451    key: ProjectionKey,
452    generation: u64,
453    checkpoint: SourceCheckpoint,
454    schema_version: u32,
455    fact_version: u32,
456    object: ObjectDescriptor,
457    artifact_object: ExactObjectRef,
458    publisher: String,
459    fence: PublisherFence,
460}
461
462impl ProjectionManifest {
463    /// Records one exact published generation.
464    ///
465    /// # Errors
466    ///
467    /// Returns [`ModelError::Bounds`] for a zero generation, a zero schema or
468    /// fact version, or an empty publisher.
469    #[allow(
470        clippy::too_many_arguments,
471        reason = "every field of the signed manifest is named explicitly"
472    )]
473    pub fn try_new(
474        key: ProjectionKey,
475        generation: u64,
476        checkpoint: SourceCheckpoint,
477        schema_version: u32,
478        fact_version: u32,
479        object: ObjectDescriptor,
480        artifact_object: ExactObjectRef,
481        publisher: String,
482        fence: PublisherFence,
483    ) -> Result<Self, ModelError> {
484        if generation == 0 {
485            return Err(ModelError::Bounds("generation"));
486        }
487        if schema_version == 0 {
488            return Err(ModelError::Bounds("schema_version"));
489        }
490        if fact_version == 0 {
491            return Err(ModelError::Bounds("fact_version"));
492        }
493        non_empty("publisher", &publisher)?;
494        Ok(Self {
495            key,
496            generation,
497            checkpoint,
498            schema_version,
499            fact_version,
500            object,
501            artifact_object,
502            publisher,
503            fence,
504        })
505    }
506
507    /// Returns the projection key.
508    #[must_use]
509    pub const fn key(&self) -> &ProjectionKey {
510        &self.key
511    }
512
513    /// Returns the projection generation.
514    #[must_use]
515    pub const fn generation(&self) -> u64 {
516        self.generation
517    }
518
519    /// Returns the source checkpoint.
520    #[must_use]
521    pub const fn checkpoint(&self) -> &SourceCheckpoint {
522        &self.checkpoint
523    }
524
525    /// Returns the schema version.
526    #[must_use]
527    pub const fn schema_version(&self) -> u32 {
528        self.schema_version
529    }
530
531    /// Returns the fact version.
532    #[must_use]
533    pub const fn fact_version(&self) -> u32 {
534        self.fact_version
535    }
536
537    /// Returns the manifest's own object descriptor.
538    #[must_use]
539    pub const fn object(&self) -> &ObjectDescriptor {
540        &self.object
541    }
542
543    /// Returns the exact artifact object reference.
544    #[must_use]
545    pub const fn artifact_object(&self) -> &ExactObjectRef {
546        &self.artifact_object
547    }
548
549    /// Returns the publisher identity.
550    #[must_use]
551    pub fn publisher(&self) -> &str {
552        &self.publisher
553    }
554
555    /// Returns the publisher fence.
556    #[must_use]
557    pub const fn fence(&self) -> &PublisherFence {
558        &self.fence
559    }
560}
561
562/// One anchored journal prefix.
563#[derive(Clone, PartialEq, Eq)]
564pub struct JournalAnchor {
565    source: JournalSource,
566    head: u64,
567}
568
569impl JournalAnchor {
570    /// Records one anchored journal prefix.
571    #[must_use]
572    pub const fn new(source: JournalSource, head: u64) -> Self {
573        Self { source, head }
574    }
575
576    /// Returns the anchored lineage.
577    #[must_use]
578    pub const fn source(&self) -> &JournalSource {
579        &self.source
580    }
581
582    /// Returns the anchored head position.
583    #[must_use]
584    pub const fn head(&self) -> u64 {
585        self.head
586    }
587}
588
589/// One exact source premise a result read.
590#[derive(Clone, PartialEq, Eq)]
591pub enum SourcePin {
592    /// An exact published projection generation.
593    ///
594    /// Boxed: a manifest is far larger than the other two premises, and an
595    /// unboxed variant would make every pin in a thirty-two-pin vector that
596    /// size.
597    Projected(Box<ProjectionManifest>),
598    /// An exact anchored journal prefix.
599    Journal(JournalAnchor),
600    /// An authoritative revision.
601    Authoritative(u64),
602}
603
604impl SourcePin {
605    /// Returns the canonical identity this pin sorts by.
606    ///
607    /// The rule matches the durable audit's own: a variant tag, then each
608    /// source's identity as length-prefixed bytes. Two projected descriptors
609    /// for one key, two anchors for one partition, and two authoritative
610    /// revisions therefore compare equal and are refused rather than
611    /// collapsed.
612    #[must_use]
613    pub fn identity_bytes(&self) -> Vec<u8> {
614        let mut bytes = Vec::new();
615        match self {
616            Self::Projected(manifest) => {
617                bytes.push(0);
618                push_len_prefixed(&mut bytes, manifest.key().family().as_bytes());
619                push_len_prefixed(&mut bytes, manifest.key().source_partition().as_bytes());
620            }
621            Self::Journal(anchor) => {
622                bytes.push(1);
623                push_len_prefixed(&mut bytes, anchor.source().partition().as_bytes());
624            }
625            Self::Authoritative(_) => bytes.push(2),
626        }
627        bytes
628    }
629}
630
631/// The bounded, canonical source premises one result reports.
632#[derive(Clone, PartialEq, Eq)]
633pub struct SourceEvidence {
634    pins: Vec<SourcePin>,
635}
636
637impl SourceEvidence {
638    /// Bounds and canonicalizes one complete source vector.
639    ///
640    /// # Errors
641    ///
642    /// Returns [`ModelError::Bounds`] past [`MAX_SOURCE_PINS`], and
643    /// [`ModelError::Order`] when the pins are not strictly ascending by
644    /// identity. A duplicate source is an out-of-order pin, never a silent
645    /// collapse.
646    pub fn try_new(pins: Vec<SourcePin>) -> Result<Self, ModelError> {
647        if pins.len() > MAX_SOURCE_PINS {
648            return Err(ModelError::Bounds("source_pins"));
649        }
650        for pair in pins.windows(2) {
651            if pair[0].identity_bytes() >= pair[1].identity_bytes() {
652                return Err(ModelError::Order("source_pins"));
653            }
654        }
655        Ok(Self { pins })
656    }
657
658    /// Returns every exact source premise in canonical order.
659    #[must_use]
660    pub fn pins(&self) -> &[SourcePin] {
661        &self.pins
662    }
663}
664
665// ---------------------------------------------------------------------------
666// Shape-only formatting
667// ---------------------------------------------------------------------------
668//
669// Source evidence names partitions, object keys, namespaces, publishers,
670// signer keys, and signatures. `Debug` is what a `tracing` field and an
671// `#[instrument]` attribute render, so every type here formats its shape and
672// nothing that addresses tenant content. The typed getters above remain the
673// way a mechanism reads the values it genuinely needs.
674
675impl fmt::Debug for JournalSource {
676    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
677        formatter.write_str("JournalSource")
678    }
679}
680
681impl fmt::Debug for JournalAttestation {
682    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
683        formatter
684            .debug_struct("JournalAttestation")
685            .field("leaf_count", &self.leaf_count)
686            .finish_non_exhaustive()
687    }
688}
689
690impl fmt::Debug for SourceCheckpoint {
691    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
692        formatter
693            .debug_struct("SourceCheckpoint")
694            .field("feed_position", &self.feed_position)
695            .field("journal_position", &self.journal_position)
696            .finish_non_exhaustive()
697    }
698}
699
700impl fmt::Debug for ObjectDescriptor {
701    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
702        formatter
703            .debug_struct("ObjectDescriptor")
704            .field("generation", &self.generation)
705            .field("classification", &self.classification)
706            .field("byte_len", &self.byte_len)
707            .finish_non_exhaustive()
708    }
709}
710
711impl fmt::Debug for ExactObjectRef {
712    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
713        formatter
714            .debug_struct("ExactObjectRef")
715            .field("backend_generation", &self.backend_generation)
716            .finish_non_exhaustive()
717    }
718}
719
720impl fmt::Debug for ProjectionKey {
721    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
722        formatter.write_str("ProjectionKey")
723    }
724}
725
726impl fmt::Debug for PublisherFence {
727    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
728        formatter
729            .debug_struct("PublisherFence")
730            .field("term", &self.term)
731            .finish_non_exhaustive()
732    }
733}
734
735impl fmt::Debug for ProjectionManifest {
736    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
737        formatter
738            .debug_struct("ProjectionManifest")
739            .field("generation", &self.generation)
740            .field("schema_version", &self.schema_version)
741            .field("fact_version", &self.fact_version)
742            .finish_non_exhaustive()
743    }
744}
745
746impl fmt::Debug for JournalAnchor {
747    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
748        formatter
749            .debug_struct("JournalAnchor")
750            .field("head", &self.head)
751            .finish_non_exhaustive()
752    }
753}
754
755impl fmt::Debug for SourcePin {
756    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
757        formatter.write_str(match self {
758            Self::Projected(_) => "projected",
759            Self::Journal(_) => "journal",
760            Self::Authoritative(_) => "authoritative",
761        })
762    }
763}
764
765impl fmt::Debug for SourceEvidence {
766    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
767        formatter
768            .debug_struct("SourceEvidence")
769            .field("pins", &self.pins.len())
770            .finish()
771    }
772}
773
774#[cfg(test)]
775mod tests {
776    use super::*;
777
778    fn journal(partition: &str) -> SourcePin {
779        SourcePin::Journal(JournalAnchor::new(
780            JournalSource::try_new(partition.to_owned(), [7; INCARNATION_BYTES]).unwrap(),
781            4,
782        ))
783    }
784
785    #[test]
786    fn pins_must_be_strictly_ascending_and_bounded() {
787        assert!(SourceEvidence::try_new(vec![journal("a"), journal("b")]).is_ok());
788        assert_eq!(
789            SourceEvidence::try_new(vec![journal("b"), journal("a")]),
790            Err(ModelError::Order("source_pins"))
791        );
792        assert_eq!(
793            SourceEvidence::try_new(vec![journal("a"), journal("a")]),
794            Err(ModelError::Order("source_pins")),
795            "one partition may be anchored once"
796        );
797
798        let overflowing = (0..=MAX_SOURCE_PINS)
799            .map(|index| journal(&format!("p{index:04}")))
800            .collect();
801        assert_eq!(
802            SourceEvidence::try_new(overflowing),
803            Err(ModelError::Bounds("source_pins"))
804        );
805    }
806
807    #[test]
808    fn identity_ignores_everything_except_the_source_it_names() {
809        let first = SourcePin::Journal(JournalAnchor::new(
810            JournalSource::try_new("a".to_owned(), [1; INCARNATION_BYTES]).unwrap(),
811            1,
812        ));
813        let second = SourcePin::Journal(JournalAnchor::new(
814            JournalSource::try_new("a".to_owned(), [2; INCARNATION_BYTES]).unwrap(),
815            9,
816        ));
817        assert_eq!(first.identity_bytes(), second.identity_bytes());
818    }
819
820    #[test]
821    fn structural_bounds_refuse_empty_identifiers() {
822        assert_eq!(
823            JournalSource::try_new(String::new(), [0; INCARNATION_BYTES]),
824            Err(ModelError::Bounds("partition"))
825        );
826        assert_eq!(
827            ProjectionKey::try_new(String::new(), "p".to_owned()),
828            Err(ModelError::Bounds("family"))
829        );
830        assert_eq!(
831            ExactObjectRef::try_new("ns".to_owned(), String::new(), 1),
832            Err(ModelError::Bounds("key"))
833        );
834    }
835}