Skip to main content

triblespace_core/collection/
encoding.rs

1//! Canonical encodings and mappings for typed collections.
2//!
3//! Durable collection records carry representation-neutral content hashes.
4//! This module restores their physical meaning at the API boundary without
5//! inventing a second runtime planner:
6//!
7//! - a [`CollectionEncoding`] owns the canonical bytes, validation, and join
8//!   within one collection;
9//! - a [`CollectionDerivation`] lets one target encoding own its canonical,
10//!   parameterized, join-preserving conversion from one source encoding;
11//! - a [`DeriveMapping`] is an explicit mapping whose images are derived into
12//!   a collection of their own, and a [`MapMapping`] (or the target-owned
13//!   [`CollectionAttachment`]) indexes another collection's nodes instead,
14//!   into an attached collection;
15//! - [`Collection`] binds an encoding to one exact, content-addressed
16//!   descriptor.
17//!
18//! Logical interpretation is deliberately separate in
19//! [`TryFromCover`](crate::collection::TryFromCover). Maintained-index views
20//! retain their mmap-backed shards and check only the framing, bounds and
21//! alignment needed to access them safely. Attachment does not re-prove
22//! canonical contents, serialize a logical union, or construct an accelerator.
23//! Query preparation may derive scratch data the encoding does not persist;
24//! that work belongs to the requested query, not to attaching the index.
25
26use std::error::Error;
27use std::fmt;
28use std::marker::PhantomData;
29
30use crate::blob::encodings::simplearchive::SimpleArchive;
31use crate::blob::{Blob, BlobEncoding};
32use crate::metadata::{self, MetaDescribe};
33use crate::prelude::{exists, pattern};
34use crate::repo::{BlobStoreGet, BlobStoreMeta, StoreRead};
35use crate::trible::Fragment;
36
37use super::{
38    collection_representation, CollectionData, CollectionHandle, KIND_COLLECTION_DESCRIPTOR,
39};
40
41/// Failure of one exact canonical collection operation.
42///
43/// `Capacity` is reserved for deterministic geometry limits of the chosen
44/// encoding. It must not describe transient allocation, I/O, or accelerator
45/// failures. Malformed or noncanonical bytes are always `Fatal`.
46#[derive(Clone, Debug, Eq, PartialEq)]
47pub enum CollectionOperationError {
48    /// The operation or supplied bytes are invalid and another cover cannot
49    /// repair it.
50    Fatal(String),
51    /// This exact encoding cannot hold the result, but a finer physical cover
52    /// may still represent the same logical value.
53    Capacity(String),
54    /// The canonical result names an immutable dependency which is not present
55    /// in the current snapshot. Storage may materialize or fetch it and retry
56    /// the otherwise pure operation.
57    MissingDependency(CollectionData),
58}
59
60impl fmt::Display for CollectionOperationError {
61    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
62        match self {
63            Self::Fatal(reason) | Self::Capacity(reason) => formatter.write_str(reason),
64            Self::MissingDependency(member) => write!(
65                formatter,
66                "collection operation requires resident blob {}",
67                hex::encode_upper(member.raw),
68            ),
69        }
70    }
71}
72
73impl Error for CollectionOperationError {}
74
75/// One canonical physical shape carried by a blob encoding.
76///
77/// Collection members are always ordinary typed [`Blob`] values. An encoding
78/// validates its own bytes and owns one canonical intra-shape join. Derived
79/// collections are ordinary lattices connected to their source by a
80/// join-preserving [`CollectionDerivation`], not a weaker kind of collection.
81/// The logical join remains total at the [`Cover`](super::Cover) level: when
82/// one physical member cannot represent the result, `Capacity` retains a finer
83/// cover of members with the same value.
84///
85/// This is intentionally stronger than [`BlobEncoding`]: not every blob format
86/// is a collection member, while every `CollectionEncoding` has an exact
87/// validation boundary.
88pub trait CollectionEncoding: BlobEncoding + MetaDescribe + Sized + 'static {
89    /// Validate encoding-specific context carried by one descriptor.
90    ///
91    /// Most encodings need no context. An encoding whose canonical bytes are
92    /// parameterized (for example a path summary over one automaton) validates
93    /// only the shape information it needs here; the source-to-target mapping
94    /// still owns the parameterized conversion itself.
95    fn validate_descriptor(_descriptor: &Fragment) -> Result<(), CollectionOperationError> {
96        Ok(())
97    }
98
99    /// Explicitly validate one member independently of its provenance.
100    ///
101    /// The root bytes have already passed the blob store's content-address
102    /// boundary. A Merkle encoding may inspect children through `reader`; a
103    /// monolithic encoding normally ignores it. Warm collection resolution
104    /// does not invoke this hook; it is available to producers, untrusted
105    /// ingress, and offline audits. It is not part of ordinary typed-view
106    /// construction, nor a requirement to check locally produced output again.
107    fn validate_member<R>(
108        descriptor: &Fragment,
109        member: &Blob<Self>,
110        reader: &R,
111    ) -> Result<(), CollectionOperationError>
112    where
113        R: BlobStoreGet + BlobStoreMeta;
114
115    /// Return immutable representation dependencies missing from a resident
116    /// member root in this snapshot.
117    ///
118    /// Self-contained encodings need no extra work. Merkle encodings override
119    /// this narrow availability query so cover resolution can ignore an
120    /// incomplete compacted root and fall back to a finer support-equivalent
121    /// cover. Returned handles are only the blobs required to interpret this
122    /// representation, not optional attachments or semantic provenance.
123    ///
124    /// This is not semantic validation: it neither recomputes the member nor
125    /// proves its canonical bytes. It may read the named resident root, but
126    /// must not request missing dependencies, persist, or otherwise change
127    /// storage.
128    fn missing_representation_dependencies<R>(
129        _member: CollectionData,
130        _reader: &R,
131    ) -> Result<Vec<CollectionData>, CollectionOperationError>
132    where
133        R: BlobStoreGet + BlobStoreMeta,
134    {
135        Ok(Vec::new())
136    }
137
138    /// Compute the exact canonical join of two members.
139    ///
140    /// The implementation owns decoding and rejecting malformed inputs while
141    /// it performs this new work; warm resolution never calls this method.
142    ///
143    /// `reader` resolves immutable content-addressed dependencies named by the
144    /// inputs or by their canonical result. Availability may delay publication
145    /// but cannot alter the result bytes. Other resident content is not an
146    /// input to the join.
147    ///
148    fn join_members<R>(
149        descriptor: &Fragment,
150        low: &Blob<Self>,
151        high: &Blob<Self>,
152        reader: &R,
153    ) -> Result<Blob<Self>, CollectionOperationError>
154    where
155        R: BlobStoreGet + BlobStoreMeta;
156
157    /// Compute the exact canonical join of one or more members: the k-way
158    /// join an n-ary MERGE states.
159    ///
160    /// The default folds [`Self::join_members`] left to right, which is the
161    /// same canonical result by associativity; an encoding with a cheaper
162    /// k-way join overrides it. The same availability rules apply.
163    fn join_many<R>(
164        descriptor: &Fragment,
165        members: &[Blob<Self>],
166        reader: &R,
167    ) -> Result<Blob<Self>, CollectionOperationError>
168    where
169        R: BlobStoreGet + BlobStoreMeta,
170    {
171        let (first, rest) = members.split_first().ok_or_else(|| {
172            CollectionOperationError::Fatal("a join needs at least one member".to_owned())
173        })?;
174        let mut joined = first.clone();
175        for member in rest {
176            joined = Self::join_members(descriptor, &joined, member, reader)?;
177        }
178        Ok(joined)
179    }
180}
181
182/// The canonical incoming derivation owned by one target encoding.
183///
184/// The target names its one source encoding and the runtime argument carried
185/// by the mapping fragment embedded in its descriptor. Canonical builders
186/// normally derive the mapping entity id, but binding validates its algorithm
187/// and argument rather than its minting history. Implementations must be a
188/// join homomorphism:
189///
190/// `map(a join b) = map(a) join map(b)`.
191pub trait CollectionDerivation: CollectionEncoding {
192    /// Canonical source encoding.
193    type Source: CollectionEncoding;
194
195    /// Runtime argument which distinguishes concrete mappings of this
196    /// canonical source-to-target relation.
197    type Argument;
198
199    /// Canonical concrete mapping fragment embedded in a target descriptor.
200    ///
201    /// Parameterized derivations carry their argument in this fragment;
202    /// parameter-free derivations use `()`.
203    fn fragment(argument: &Self::Argument) -> Fragment;
204
205    /// Bind and validate the concrete mapping named by the target descriptor.
206    fn bind(
207        source: &Fragment,
208        target: &Fragment,
209    ) -> Result<Self::Argument, CollectionOperationError>;
210
211    /// Compute the canonical target image of one source member.
212    ///
213    /// `reader` is the same frozen store observation from which the
214    /// source was loaded. The mapping owns decoding and rejecting malformed
215    /// input while it performs new work. It may use `reader` only to resolve
216    /// dependencies named by `source` or its concrete argument. An explicit
217    /// collection reference is resolved using this observation's records and
218    /// capability evidence; unrelated store contents are not semantic input.
219    fn map<R>(
220        argument: &Self::Argument,
221        source: &Blob<Self::Source>,
222        reader: &R,
223    ) -> Result<Blob<Self>, CollectionOperationError>
224    where
225        R: StoreRead;
226
227    /// Join target images: the k-way join a MERGE of the derived collection
228    /// states. See [`DeriveMapping::join_images`].
229    fn join_images<R>(
230        _argument: &Self::Argument,
231        target_descriptor: &Fragment,
232        images: &[Blob<Self>],
233        reader: &R,
234    ) -> Result<Blob<Self>, CollectionOperationError>
235    where
236        R: BlobStoreGet + BlobStoreMeta,
237    {
238        Self::join_many(target_descriptor, images, reader)
239    }
240}
241
242/// One explicit parameterized mapping between collection encodings, whose
243/// images are derived into a collection of their own.
244///
245/// Its images are published as `DERIVE` leaves of the derived collection and
246/// replicate with it. Only a mapping not every reader of a node can compute
247/// is derived; an index that every reader of a node can compute for itself
248/// is a [`MapMapping`] instead. The contract an implementor takes on:
249///
250/// - It maps foundations only: a commit payload of a root source, or a leaf
251///   image of a derived one. It never maps a merge result: the derived
252///   collection carries its own leaf images into a lattice of its own, joined
253///   with [`Self::join_images`], and never reads its source's merges.
254/// - The target encoding's join is total: any two members that are each
255///   valid join, so every set of leaves has a join, whoever derived them.
256///   Several leaves may name one source foundation -- two derivations that
257///   disagree, or a re-derivation -- and the join keeps what each says. A
258///   join that cannot be represented in one member is `Capacity`, which
259///   leaves the finer cover standing; it is never `Fatal` for valid members.
260/// - A deterministic mapping is a join homomorphism into that join,
261///   `map(a join b) = map(a) join map(b)`, so the join of a collection's
262///   leaves is the image of everything the source's foundations hold. A
263///   mapping may also be non-deterministic -- an embedding that does not
264///   reproduce bit for bit -- and then the join of its leaves holds what
265///   each derivation observed.
266/// - It may be pinned to a class of host ([`Self::computable_here`]): a model
267///   whose output is canonical on one accelerator class only. Another host
268///   derives nothing through it and still carries the collection; its leaves
269///   arrive by replication.
270///
271/// Derivation is scheduled per source foundation by one rule, which every
272/// mapping in this crate follows: any admitted leaf suffices. Maintenance
273/// derives a foundation only when no believed leaf for its locator has an
274/// output that is here or can be fetched, whoever signed that leaf, and any
275/// key the derived collection admits may derive it on a host that can
276/// compute the mapping, not only the foundation's owner. A leaf whose output
277/// cannot be obtained does not count; whether it can is asked only of a
278/// store that reaches other holders, and a store that cannot waits for the
279/// output instead. A fetch that fails says the output is
280/// not available now, not that it is lost: an original output that arrives
281/// after its foundation was derived again stands beside the newer leaf,
282/// both are joined, and nothing further is derived. A leaf known to be bad
283/// is supplemented only on request
284/// ([`CollectionStoreExt::rederive_with`](super::CollectionStoreExt::rederive_with)),
285/// which adds a second leaf and never replaces one.
286///
287/// This is the coherence-safe extension point for mappings whose source and
288/// target encodings are both owned elsewhere. Prefer [`CollectionDerivation`]
289/// when a target encoding owns one canonical incoming derivation; explicit
290/// mappings are selected through the `*_with` collection-store methods.
291pub trait DeriveMapping: Sized {
292    /// Canonical source encoding.
293    type Source: CollectionEncoding;
294    /// Canonical target encoding.
295    type Target: CollectionEncoding;
296
297    /// Canonical concrete mapping fragment embedded in a target descriptor.
298    fn fragment(&self) -> Fragment;
299
300    /// Bind and validate the concrete mapping named by the target descriptor.
301    fn bind(source: &Fragment, target: &Fragment) -> Result<Self, CollectionOperationError>;
302
303    /// Compute the canonical target image of one source member.
304    ///
305    /// The reader freezes the source and any explicitly named dependencies
306    /// together, including collection records and authorization. A mapping may
307    /// resolve references carried by its descriptor or input, but must not
308    /// discover ambient inputs or advance the control-plane observation.
309    fn map<R>(
310        &self,
311        source: &Blob<Self::Source>,
312        reader: &R,
313    ) -> Result<Blob<Self::Target>, CollectionOperationError>
314    where
315        R: StoreRead;
316
317    /// Whether this host can compute the mapping: the class-pin hook.
318    ///
319    /// A mapping pinned to a class of host -- a model whose output is
320    /// canonical on one accelerator class, say -- answers `false` on every
321    /// other host. There maintenance derives nothing through it, raises no
322    /// error for it, and still carries the derived collection: its leaves
323    /// arrive by replication and are joined like any others. The default,
324    /// `true`, is every host.
325    fn computable_here(&self) -> bool {
326        true
327    }
328
329    /// Join target images: the k-way join one MERGE of the derived
330    /// collection states, which is how a derived collection carries its own
331    /// lattice.
332    ///
333    /// The result must be the encoding's canonical join
334    /// ([`CollectionEncoding::join_many`], the default), without constructing
335    /// upstream members or changing storage; a mapping may compute it through
336    /// its own backend. `Capacity` and `MissingDependency` leave the finer
337    /// cover standing.
338    fn join_images<R>(
339        &self,
340        target_descriptor: &Fragment,
341        images: &[Blob<Self::Target>],
342        reader: &R,
343    ) -> Result<Blob<Self::Target>, CollectionOperationError>
344    where
345        R: BlobStoreGet + BlobStoreMeta,
346    {
347        Self::Target::join_many(target_descriptor, images, reader)
348    }
349}
350
351/// Internal adapter from a target-owned derivation to the explicit mapping
352/// engine. Keeping this wrapper private leaves downstream coherence open.
353pub(crate) struct CanonicalDerivation<T: CollectionDerivation> {
354    argument: T::Argument,
355}
356
357impl<T: CollectionDerivation> CanonicalDerivation<T> {
358    pub(crate) fn new(argument: T::Argument) -> Self {
359        Self { argument }
360    }
361}
362
363impl<T: CollectionDerivation> DeriveMapping for CanonicalDerivation<T> {
364    type Source = T::Source;
365    type Target = T;
366
367    fn fragment(&self) -> Fragment {
368        T::fragment(&self.argument)
369    }
370
371    fn bind(source: &Fragment, target: &Fragment) -> Result<Self, CollectionOperationError> {
372        T::bind(source, target).map(Self::new)
373    }
374
375    fn map<R>(
376        &self,
377        source: &Blob<Self::Source>,
378        reader: &R,
379    ) -> Result<Blob<Self::Target>, CollectionOperationError>
380    where
381        R: StoreRead,
382    {
383        T::map(&self.argument, source, reader)
384    }
385
386    fn join_images<R>(
387        &self,
388        target_descriptor: &Fragment,
389        images: &[Blob<Self::Target>],
390        reader: &R,
391    ) -> Result<Blob<Self::Target>, CollectionOperationError>
392    where
393        R: BlobStoreGet + BlobStoreMeta,
394    {
395        T::join_images(&self.argument, target_descriptor, images, reader)
396    }
397}
398
399/// A mapping attached to another collection's nodes: an index of each node,
400/// not a collection of its own.
401///
402/// An attached collection's descriptor names its parent collection and one
403/// concrete mapping, and no policy: its records are `MAP(node ->
404/// attachment)` signed by the host, which no other host believes: one that
405/// reaches another store stays inert there. The contract an implementor
406/// takes on:
407///
408/// - It maps any node of its parent's lattice, a merge result as well as a
409///   foundation. The parent is a root collection: its nodes are
410///   `SimpleArchive`s.
411/// - It is deterministic. Its inputs are the node, immutable dependencies the
412///   node or the mapping names, and optionally the attachments of the same
413///   node in other attached collections of the same parent, named by
414///   [`Self::siblings`]. A dependency that is not resident is reported as
415///   [`CollectionOperationError::MissingDependency`], never read as absent.
416/// - Every host that can read the node can compute it: it is not pinned to a
417///   model, an accelerator class or any other property of a host. A mapping
418///   that is pinned cannot implement this trait, so it cannot be attached by
419///   mistake; it stays a [`DeriveMapping`].
420/// - Its images need no join. `Capacity` means this node's attachment cannot
421///   be represented; a reader then descends to finer nodes.
422///
423/// Its law is cover-query equivalence. A reader answers a query over a cover
424/// of attachments at mixed granularity -- one node's image beside the images
425/// of finer nodes, possibly overlapping -- and the answer must equal the
426/// query over the image of the union the cover stands for:
427///
428/// `view(cover { map(a_1), ..., map(a_n) }) = view(map(a_1 join ... join a_n))`.
429///
430/// Because the `a_i` may overlap, a view combines its per-node answers
431/// idempotently: a last-writer-wins register pairs partial rows across the
432/// cover, a text index takes each entity's maximum score, and a count or any
433/// other non-idempotent aggregate is never summed over overlapping supports.
434/// Every implementor is tested against this law over random covers.
435///
436/// The law holds for every cover when an image is built from single facts,
437/// as every implementor in this crate's is. A mapping whose image aggregates
438/// facts of several entities cannot tell from one node that an entity has
439/// more facts elsewhere, and then no combination of per-node images is
440/// exact once a cover splits that entity; nothing refuses such a node, so
441/// the answer can be wrong with an empty residual and nothing unread. For
442/// such a mapping the law is a premise about its input, which the
443/// implementor states. The one known is the Archive block BM25 mapping in
444/// faculties (`ArchiveBlockTextBm25Mapping`), whose document is a block and
445/// whose term frequency sums over the block's parts: it answers like the
446/// union for every cover whose nodes hold whole source units (a block with
447/// all its parts and their facts); a node holding part of a block scores
448/// that block from the part it holds, with no refusal and no residual to
449/// show it, so a cover that splits a block undercounts it. Faculties' own
450/// Archive importer commits whole source units, and a test there pins it:
451/// that is the premise for data it writes, not for another writer's commits
452/// or historical input, and the writer keeps the fragments it is given
453/// whole without checking that they hold whole blocks. The ignored
454/// faculties tests `a_block_whose_parts_sit_in_two_nodes_scores_like_their_union`,
455/// `a_block_extended_by_a_node_without_its_tag_scores_like_their_union` and
456/// `archive_block_covers_answer_like_the_union_when_parts_spread_over_nodes`
457/// are the acceptance tests of the structural fix: a per-fact text index
458/// over the content fact's payload, with blocks ranked at query time.
459pub trait MapMapping: Sized {
460    /// Encoding of the attachments.
461    type Target: CollectionEncoding;
462
463    /// Canonical concrete mapping fragment embedded in an attached
464    /// descriptor.
465    fn fragment(&self) -> Fragment;
466
467    /// Bind and validate the concrete mapping an attached descriptor names.
468    fn bind(parent: &Fragment, attached: &Fragment) -> Result<Self, CollectionOperationError>;
469
470    /// The attached collections of the same parent whose attachment of a
471    /// node this mapping reads, in the order [`Self::map`] receives them.
472    /// Their handles are named by the mapping's own descriptor, never
473    /// recomputed.
474    fn siblings(&self) -> Vec<CollectionHandle> {
475        Vec::new()
476    }
477
478    /// Compute the attachment of one node.
479    ///
480    /// `siblings` holds one usable attachment of this node in each collection
481    /// [`Self::siblings`] names. `reader` resolves immutable dependencies
482    /// named by the node, the siblings or the mapping; nothing else in the
483    /// store is an input.
484    fn map<R>(
485        &self,
486        node: &Blob<SimpleArchive>,
487        siblings: &[CollectionData],
488        reader: &R,
489    ) -> Result<Blob<Self::Target>, CollectionOperationError>
490    where
491        R: StoreRead;
492}
493
494/// The canonical attachment owned by one target encoding: the
495/// [`MapMapping`] counterpart of [`CollectionDerivation`], with the same
496/// contract and law.
497///
498/// The target names the runtime argument its mapping fragment carries;
499/// attaching it is [`crate::collection::CollectionStoreExt::attach`].
500pub trait CollectionAttachment: CollectionEncoding {
501    /// Runtime argument which distinguishes concrete mappings of this
502    /// canonical relation.
503    type Argument;
504
505    /// Canonical concrete mapping fragment embedded in an attached
506    /// descriptor.
507    fn fragment(argument: &Self::Argument) -> Fragment;
508
509    /// Bind and validate the concrete mapping an attached descriptor names.
510    fn bind(
511        parent: &Fragment,
512        attached: &Fragment,
513    ) -> Result<Self::Argument, CollectionOperationError>;
514
515    /// See [`MapMapping::siblings`].
516    fn siblings(_argument: &Self::Argument) -> Vec<CollectionHandle> {
517        Vec::new()
518    }
519
520    /// See [`MapMapping::map`].
521    fn map<R>(
522        argument: &Self::Argument,
523        node: &Blob<SimpleArchive>,
524        siblings: &[CollectionData],
525        reader: &R,
526    ) -> Result<Blob<Self>, CollectionOperationError>
527    where
528        R: StoreRead;
529}
530
531/// Internal adapter from a target-owned attachment to the explicit mapping
532/// engine, the counterpart of [`CanonicalDerivation`].
533pub(crate) struct CanonicalAttachment<T: CollectionAttachment> {
534    argument: T::Argument,
535}
536
537impl<T: CollectionAttachment> CanonicalAttachment<T> {
538    pub(crate) fn new(argument: T::Argument) -> Self {
539        Self { argument }
540    }
541}
542
543impl<T: CollectionAttachment> MapMapping for CanonicalAttachment<T> {
544    type Target = T;
545
546    fn fragment(&self) -> Fragment {
547        T::fragment(&self.argument)
548    }
549
550    fn bind(parent: &Fragment, attached: &Fragment) -> Result<Self, CollectionOperationError> {
551        T::bind(parent, attached).map(Self::new)
552    }
553
554    fn siblings(&self) -> Vec<CollectionHandle> {
555        T::siblings(&self.argument)
556    }
557
558    fn map<R>(
559        &self,
560        node: &Blob<SimpleArchive>,
561        siblings: &[CollectionData],
562        reader: &R,
563    ) -> Result<Blob<Self::Target>, CollectionOperationError>
564    where
565        R: StoreRead,
566    {
567        T::map(&self.argument, node, siblings, reader)
568    }
569}
570
571/// A descriptor does not denote the encoding requested by its Rust type.
572#[derive(Clone, Debug, Eq, PartialEq)]
573pub enum CollectionTypeError {
574    /// No tagged descriptor entity names the requested encoding.
575    WrongEncoding {
576        /// Encoding required by the Rust type.
577        expected: crate::id::Id,
578    },
579    /// The descriptor names the right encoding but supplies invalid context.
580    InvalidDescriptor(CollectionOperationError),
581}
582
583impl fmt::Display for CollectionTypeError {
584    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
585        match self {
586            Self::WrongEncoding { expected } => write!(
587                formatter,
588                "no collection descriptor names encoding {expected:X}",
589            ),
590            Self::InvalidDescriptor(source) => {
591                write!(formatter, "invalid collection encoding context: {source}")
592            }
593        }
594    }
595}
596
597impl Error for CollectionTypeError {
598    fn source(&self) -> Option<&(dyn Error + 'static)> {
599        match self {
600            Self::InvalidDescriptor(source) => Some(source),
601            Self::WrongEncoding { .. } => None,
602        }
603    }
604}
605
606/// Recognize the descriptor facts needed to interpret members as `E`.
607///
608/// A matching encoding fact is sufficient; other representations, annotations,
609/// and legacy fields do not invalidate it. Policy and lineage are separate
610/// queries at their respective consumers, not encoding prerequisites.
611pub(crate) fn validate_descriptor_type<E>(
612    descriptor_fragment: &Fragment,
613) -> Result<(), CollectionTypeError>
614where
615    E: CollectionEncoding,
616{
617    let expected = E::id();
618    if !exists!(pattern!(descriptor_fragment.facts(), [{ _?descriptor @
619        metadata::tag: KIND_COLLECTION_DESCRIPTOR,
620        collection_representation: expected,
621    }])) {
622        return Err(CollectionTypeError::WrongEncoding { expected });
623    }
624    E::validate_descriptor(descriptor_fragment).map_err(CollectionTypeError::InvalidDescriptor)
625}
626
627/// One exact collection descriptor, typed by its canonical member encoding.
628///
629/// The store owns the descriptor bytes. This is only its cheap, cloneable
630/// content address plus compile-time meaning; constructing it is restricted to
631/// descriptor-validation boundaries.
632pub struct Collection<E: CollectionEncoding> {
633    handle: CollectionHandle,
634    encoding: PhantomData<fn() -> E>,
635}
636
637impl<E: CollectionEncoding> Collection<E> {
638    pub(crate) const fn from_handle(handle: CollectionHandle) -> Self {
639        Self {
640            handle,
641            encoding: PhantomData,
642        }
643    }
644
645    /// Representation-neutral descriptor handle stored in dense records.
646    pub const fn handle(self) -> CollectionHandle {
647        self.handle
648    }
649}
650
651impl<E: CollectionEncoding> Clone for Collection<E> {
652    fn clone(&self) -> Self {
653        *self
654    }
655}
656
657impl<E: CollectionEncoding> Copy for Collection<E> {}
658
659impl<E: CollectionEncoding> fmt::Debug for Collection<E> {
660    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
661        formatter
662            .debug_tuple("Collection")
663            .field(&hex::encode_upper(self.handle.raw))
664            .finish()
665    }
666}
667
668impl<E: CollectionEncoding> PartialEq for Collection<E> {
669    fn eq(&self, other: &Self) -> bool {
670        self.handle == other.handle
671    }
672}
673
674impl<E: CollectionEncoding> Eq for Collection<E> {}
675
676impl<E: CollectionEncoding> PartialOrd for Collection<E> {
677    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
678        Some(self.cmp(other))
679    }
680}
681
682impl<E: CollectionEncoding> Ord for Collection<E> {
683    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
684        self.handle.cmp(&other.handle)
685    }
686}
687
688impl<E: CollectionEncoding> std::hash::Hash for Collection<E> {
689    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
690        self.handle.hash(state);
691    }
692}
693
694impl<E: CollectionEncoding> From<Collection<E>> for CollectionHandle {
695    fn from(collection: Collection<E>) -> Self {
696        collection.handle
697    }
698}