Skip to main content

lodestone_types/
lib.rs

1//! Foundational, dependency-free types shared by Lodestone components.
2
3use std::collections::BTreeMap;
4use std::error::Error;
5use std::fmt::{self, Display, Formatter};
6use std::io::Write;
7use std::path::{Path, PathBuf};
8use std::str::FromStr;
9use std::sync::OnceLock;
10use std::time::{Duration, Instant};
11
12use base64::Engine as _;
13use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
14use serde::de::{self, MapAccess, SeqAccess, Visitor};
15use serde::{Deserialize, Deserializer, Serialize, Serializer};
16
17mod build_identity;
18mod derived_overload;
19mod identity;
20mod lifecycle_completion;
21mod local_development;
22mod metric;
23mod mutation_lifecycle;
24mod operation_locator;
25mod performance;
26mod profile;
27mod relationship;
28mod replayable_source;
29mod resource_spec;
30mod shared_root_history;
31mod state_outcomes;
32mod workload_class;
33
34pub use build_identity::*;
35pub use derived_overload::*;
36pub use identity::{
37    BackendAuthorityId, BackendAuthorityPair, ClusterIdentity, ClusterStorageId, CollectionId, CollectionIndexRef,
38    FenceToken, IdentityError, IndexAddress, IndexId, IndexName, NodeIncarnation, ObjectIncarnation, PrimaryTerm,
39    ProtectionSnapshotId, RecoveryAttemptId, RecoveryEra, RecoveryGeneration, ShardKey,
40};
41pub use lifecycle_completion::*;
42pub use local_development::{LocalDevelopmentInstanceId, LocalDevelopmentInstanceIdError};
43pub use metric::*;
44pub use mutation_lifecycle::*;
45pub use operation_locator::{
46    ARTIFACT_CHUNK_ALIGNMENT, ARTIFACT_LOCATOR_FORMAT_VERSION, ArtifactLocatorV1, ArtifactReadRangeV1,
47    MAX_ARTIFACT_CHUNK_BYTES, OPERATION_LOCATOR_FORMAT_VERSION, OperationAuthorityKindV1, OperationLocatorError,
48    OperationLocatorV1, VerifiedArtifactChunkV1, verify_artifact_chunk_v1,
49};
50pub use performance::{
51    BoundedStageSnapshotV2, PerformanceObservationV2, PerformanceStageReadingV2, PerformanceStageV2,
52    bounded_stage_snapshot_v2, record_performance_stage_v2,
53};
54pub use profile::*;
55pub use relationship::*;
56pub use replayable_source::*;
57pub use resource_spec::*;
58pub use shared_root_history::*;
59pub use state_outcomes::*;
60pub use workload_class::*;
61
62/// AWS partition is part of an S3 bucket's globally stable provider identity.
63#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
64#[serde(rename_all = "snake_case")]
65pub enum AwsPartition {
66    Aws,
67    AwsCn,
68    AwsUsGov,
69}
70
71/// Provider-observed AWS bucket kind. A directory bucket's AZ ID is durable
72/// identity and must exact-match the writer's provider-observed AZ ID.
73#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
74#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
75pub enum AwsBucketKind {
76    GeneralPurpose,
77    Directory { availability_zone_id: String },
78}
79
80impl AwsBucketKind {
81    #[must_use]
82    pub const fn is_directory(&self) -> bool {
83        matches!(self, Self::Directory { .. })
84    }
85
86    #[must_use]
87    pub fn availability_zone_id(&self) -> Option<&str> {
88        match self {
89            Self::GeneralPurpose => None,
90            Self::Directory { availability_zone_id } => Some(availability_zone_id),
91        }
92    }
93}
94
95macro_rules! semantic_number {
96    ($name:ident, $representation:ty, $documentation:literal) => {
97        #[doc = $documentation]
98        #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
99        #[serde(transparent)]
100        pub struct $name($representation);
101
102        impl $name {
103            /// Constructs the semantic value.
104            #[must_use]
105            pub const fn new(value: $representation) -> Self {
106                Self(value)
107            }
108
109            /// Returns the underlying representation.
110            #[must_use]
111            pub const fn get(self) -> $representation {
112                self.0
113            }
114
115            /// Returns the next value, or `None` at the representation limit.
116            #[must_use]
117            pub const fn checked_next(self) -> Option<Self> {
118                match self.0.checked_add(1) {
119                    Some(value) => Some(Self(value)),
120                    None => None,
121                }
122            }
123        }
124
125        impl Display for $name {
126            fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
127                Display::fmt(&self.0, formatter)
128            }
129        }
130    };
131}
132
133semantic_number!(
134    ShardId,
135    u16,
136    "A semantic identifier for a shard in deterministic models and protocols."
137);
138semantic_number!(Epoch, u32, "A monotonically increasing shard-writer fencing epoch.");
139semantic_number!(
140    SeqNo,
141    u64,
142    "A per-shard monotonically increasing operation sequence number."
143);
144semantic_number!(
145    ManifestGeneration,
146    u64,
147    "A monotonically increasing immutable-manifest generation."
148);
149semantic_number!(
150    ContinuousEvaluationGeneration,
151    u64,
152    "An immutable semantic generation of one continuous temporal evaluation."
153);
154semantic_number!(
155    ContinuousEvaluationRevision,
156    u64,
157    "The CAS revision of one continuous temporal evaluation's durable progress."
158);
159semantic_number!(
160    SealedRootProofGeneration,
161    u64,
162    "A monotonically increasing sealed-root proof receipt generation. Zero is never issued, so it is the reserved never-proved value."
163);
164semantic_number!(
165    ManifestGraphProofGeneration,
166    u64,
167    "A monotonically increasing post-recovery manifest-graph proof generation. Zero is never issued."
168);
169semantic_number!(
170    RoutingHash,
171    u32,
172    "The stored routing hash used for shard-range selection."
173);
174semantic_number!(
175    IdHash,
176    u128,
177    "The mutable-index document identity hash used by visibility overlays."
178);
179semantic_number!(SegmentId, u128, "A stable immutable segment identifier.");
180semantic_number!(
181    StateIncarnation,
182    u64,
183    "A fenced lifetime identity for one state primary/projection pair."
184);
185/// Immutable identity assigned to one vector field for the lifetime of an
186/// index. Names may be presented by adapters, but sidecars bind this identity.
187#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
188#[serde(transparent)]
189pub struct VectorFieldId(u32);
190
191impl VectorFieldId {
192    /// Constructs a nonzero immutable vector-field identity.
193    pub const fn new(value: u32) -> Result<Self, VectorFieldIdError> {
194        if value == 0 {
195            return Err(VectorFieldIdError);
196        }
197        Ok(Self(value))
198    }
199
200    /// Returns the underlying representation.
201    #[must_use]
202    pub const fn get(self) -> u32 {
203        self.0
204    }
205}
206
207impl Display for VectorFieldId {
208    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
209        Display::fmt(&self.0, formatter)
210    }
211}
212
213/// Refusal to construct the reserved zero vector-field identity.
214#[derive(Clone, Copy, Debug, Eq, PartialEq)]
215pub struct VectorFieldIdError;
216
217impl Display for VectorFieldIdError {
218    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
219        formatter.write_str("vector field identity must be nonzero")
220    }
221}
222
223impl Error for VectorFieldIdError {}
224
225impl<'de> Deserialize<'de> for VectorFieldId {
226    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
227    where
228        D: Deserializer<'de>,
229    {
230        Self::new(u32::deserialize(deserializer)?).map_err(de::Error::custom)
231    }
232}
233/// Upper-half segment identities are reserved for system-derived immutable
234/// maintenance artifacts. Ordinary writer allocation is confined below this
235/// boundary so a published maintenance segment cannot poison `max + 1`.
236pub const RESERVED_SEGMENT_ID_NAMESPACE_START: u128 = 1_u128 << 127;
237
238#[must_use]
239pub const fn is_reserved_segment_id(segment_id: SegmentId) -> bool {
240    segment_id.get() >= RESERVED_SEGMENT_ID_NAMESPACE_START
241}
242
243/// Returns the next ordinary segment identifier while ignoring system-owned
244/// reserved artifacts already present in the same manifest. `None` is the
245/// exact exhaustion result at the namespace boundary.
246pub fn next_ordinary_segment_id<I>(current: I) -> Option<SegmentId>
247where
248    I: IntoIterator<Item = SegmentId>,
249{
250    let maximum = current
251        .into_iter()
252        .filter(|segment_id| !is_reserved_segment_id(*segment_id))
253        .max();
254    let next = maximum.map_or(Some(SegmentId::new(1)), SegmentId::checked_next)?;
255    (!is_reserved_segment_id(next)).then_some(next)
256}
257semantic_number!(
258    ChunkId,
259    u64,
260    "A node-local monotonically increasing WAL chunk identifier."
261);
262semantic_number!(NodeId, u64, "A semantic indexer-node identifier.");
263semantic_number!(
264    ControllerEpoch,
265    u64,
266    "A nonzero monotonically increasing placement-controller claim epoch."
267);
268semantic_number!(
269    NodeLifecycleGeneration,
270    u64,
271    "A monotonically increasing generation of one node lifecycle record."
272);
273semantic_number!(
274    CredentialGeneration,
275    u64,
276    "A monotonically increasing workload-credential binding generation."
277);
278semantic_number!(
279    BootstrapGeneration,
280    u64,
281    "A monotonically increasing irreversible cluster-bootstrap generation."
282);
283semantic_number!(
284    MembershipGeneration,
285    u64,
286    "A monotonically increasing external Raft membership generation."
287);
288semantic_number!(
289    PlacementProgressGeneration,
290    u64,
291    "A monotonically increasing committed placement reconciliation progress generation."
292);
293semantic_number!(
294    RangeTransitionGeneration,
295    u64,
296    "A monotonically increasing online split or merge transition generation."
297);
298semantic_number!(
299    AssignmentGeneration,
300    u64,
301    "A monotonically increasing generation for one exact shard-route assignment."
302);
303semantic_number!(
304    ReadLeaseGeneration,
305    u64,
306    "A monotonically increasing generation for one replicated generation-read lease."
307);
308semantic_number!(
309    SchemaGeneration,
310    u32,
311    "A monotonically increasing mapping/schema generation."
312);
313semantic_number!(
314    PolicyGeneration,
315    u64,
316    "A monotonically increasing authoritative index-policy generation."
317);
318semantic_number!(
319    ResourceRevision,
320    u64,
321    "A monotonically increasing revision of one index resource specification."
322);
323semantic_number!(
324    CrdGeneration,
325    u64,
326    "A nonzero Kubernetes metadata generation for one exact custom-resource incarnation."
327);
328semantic_number!(
329    CatalogManagementGeneration,
330    u64,
331    "A monotonically increasing catalog-owned CAS generation for API/CRD management ownership."
332);
333semantic_number!(
334    IndexRevision,
335    u64,
336    "A monotonically increasing revision of one immutable index incarnation's catalog record."
337);
338semantic_number!(
339    HistoryLedgerRevision,
340    u64,
341    "A monotonically increasing CAS revision of one index's retained-history ledger."
342);
343semantic_number!(
344    AdmissionGeneration,
345    u64,
346    "A monotonically increasing catalog admission fence for one index incarnation."
347);
348semantic_number!(
349    RoutingRevision,
350    u64,
351    "A monotonically increasing revision of one index's complete routing partition."
352);
353semantic_number!(
354    MemtableGeneration,
355    u64,
356    "A node-local active or frozen memtable generation."
357);
358semantic_number!(
359    BackendAuthority,
360    u64,
361    "A stable scheduler partition for one authoritative durability backend."
362);
363semantic_number!(
364    ClusterRevision,
365    u64,
366    "A monotonically increasing revision of the replicated cluster view."
367);
368semantic_number!(
369    RootRevision,
370    u64,
371    "A monotonically increasing revision of the authoritative durable root set."
372);
373semantic_number!(
374    LogicalMillis,
375    u64,
376    "Metastore-owned logical milliseconds used for leases without wall-clock dependence."
377);
378semantic_number!(
379    TaskCheckpointGeneration,
380    u64,
381    "A monotonically increasing CAS generation for one durable task checkpoint."
382);
383semantic_number!(
384    AdministrationRevision,
385    u64,
386    "A monotonically increasing revision of the collection-scoped administration catalog."
387);
388semantic_number!(
389    AliasRevision,
390    u64,
391    "A monotonically increasing revision of one exact alias record."
392);
393semantic_number!(
394    ComponentTemplateRevision,
395    u64,
396    "A monotonically increasing revision of one component-template incarnation."
397);
398semantic_number!(
399    IndexTemplateRevision,
400    u64,
401    "A monotonically increasing revision of one index-template incarnation."
402);
403semantic_number!(
404    IngestPipelineRevision,
405    u64,
406    "A monotonically increasing revision of one catalog-owned ingest pipeline."
407);
408semantic_number!(
409    DataStreamRevision,
410    u64,
411    "A monotonically increasing revision of one data-stream incarnation."
412);
413semantic_number!(
414    LifecyclePolicyRevision,
415    u64,
416    "A monotonically increasing revision of one lifecycle-policy incarnation."
417);
418semantic_number!(
419    SecurityPolicyRevision,
420    u64,
421    "A monotonically increasing revision of the sole coordinated security-policy snapshot."
422);
423semantic_number!(
424    CollectionSecurityGeneration,
425    u64,
426    "A monotonically increasing authorization generation for one Collection's public-security records."
427);
428semantic_number!(
429    QuotaPolicyGeneration,
430    u64,
431    "A monotonically increasing quota-policy generation scoped to one Collection."
432);
433semantic_number!(
434    SecurityRoleGeneration,
435    u64,
436    "A monotonically increasing generation of one Collection-local security role."
437);
438semantic_number!(
439    SecurityPrincipalGeneration,
440    u64,
441    "A monotonically increasing generation of one Collection-local security principal."
442);
443semantic_number!(
444    SecurityDirectSequence,
445    u64,
446    "A monotonically increasing position in the coordinated security direct-command replay journal."
447);
448semantic_number!(
449    ApiKeyGeneration,
450    u64,
451    "A monotonically increasing generation of one API-key verifier record."
452);
453
454pub const AUTHORIZATION_BINDING_FORMAT_VERSION: u16 = 1;
455
456/// Exact Collection-local authority used by one authenticated public request.
457///
458/// `ordering_revision` and `public_security_root_digest` retain the global
459/// provenance of the decision. Callers must use [`Self::same_local_authority`]
460/// for ordinary authorization equality so an unrelated Collection mutation
461/// cannot invalidate admitted work.
462#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
463#[serde(deny_unknown_fields)]
464pub struct AuthorizationBindingV1 {
465    pub format_version: u16,
466    pub recovery_era: RecoveryEra,
467    pub public_security_root_digest: [u8; 32],
468    pub ordering_revision: SecurityPolicyRevision,
469    pub collection_id: CollectionId,
470    pub collection_security_generation: CollectionSecurityGeneration,
471    pub principal_id: PrincipalId,
472    pub principal_generation: SecurityPrincipalGeneration,
473    pub api_key_id: ApiKeyId,
474    pub api_key_generation: ApiKeyGeneration,
475    pub role_generations_digest: [u8; 32],
476    pub quota_policy_generation: QuotaPolicyGeneration,
477    pub authorized_scope_digest: [u8; 32],
478}
479
480impl AuthorizationBindingV1 {
481    #[must_use]
482    pub fn is_valid(self) -> bool {
483        self.format_version == AUTHORIZATION_BINDING_FORMAT_VERSION
484            && self.public_security_root_digest != [0; 32]
485            && self.ordering_revision.get() != 0
486            && self.collection_security_generation.get() != 0
487            && self.principal_id.get() != 0
488            && self.principal_generation.get() != 0
489            && self.api_key_id.get() != 0
490            && self.api_key_generation.get() != 0
491            && self.role_generations_digest != [0; 32]
492            && self.quota_policy_generation.get() != 0
493            && self.authorized_scope_digest != [0; 32]
494    }
495
496    /// Compares only fences that can change the authority of this exact
497    /// Collection/principal/key/scope path. Global root and ordering movement
498    /// remain provenance and deliberately do not participate.
499    #[must_use]
500    pub fn same_local_authority(self, other: Self) -> bool {
501        self.same_generation_authority(other)
502            && self.role_generations_digest == other.role_generations_digest
503            && self.authorized_scope_digest == other.authorized_scope_digest
504    }
505
506    /// Compares the local authorization revocation and mutation fences while
507    /// permitting a caller to narrow or re-evaluate the admitted action/index
508    /// scope. Quota owners compare `quota_policy_generation` separately; a
509    /// quota-only change must not revoke authorization or queued resource work.
510    #[must_use]
511    pub fn same_generation_authority(self, other: Self) -> bool {
512        self.is_valid()
513            && other.is_valid()
514            && self.format_version == other.format_version
515            && self.recovery_era == other.recovery_era
516            && self.collection_id == other.collection_id
517            && self.collection_security_generation == other.collection_security_generation
518            && self.principal_id == other.principal_id
519            && self.principal_generation == other.principal_generation
520            && self.api_key_id == other.api_key_id
521            && self.api_key_generation == other.api_key_generation
522    }
523}
524semantic_number!(
525    AuditSequence,
526    u64,
527    "A recovery-backed sequence in the tamper-evident security audit chain."
528);
529semantic_number!(
530    AuditRangeId,
531    u64,
532    "A recovery-backed identity for one fenced Gateway audit sequence range."
533);
534semantic_number!(
535    QuotaGrantId,
536    u64,
537    "A recovery-backed identity for one aggregate Gateway quota grant."
538);
539semantic_number!(
540    QuotaWindow,
541    u64,
542    "A coordinated logical quota-window identity that cannot be supplied by an adapter."
543);
544semantic_number!(
545    ProcessMonotonicMillis,
546    u64,
547    "Process-monotonic milliseconds used only for local freshness, timeout, and lifetime bounds."
548);
549semantic_number!(
550    UnixMillis,
551    u64,
552    "Unix milliseconds used only for certificate validity observations."
553);
554
555/// Nonzero process-lifetime identity for one exact Gateway incarnation.
556///
557/// This is separate from `NodeToken`: embedded all-role development
558/// composition still needs an unforgeable process fence, while distributed
559/// Gateway ownership additionally carries the exact `NodeToken`.
560#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
561#[serde(transparent)]
562pub struct GatewayIncarnation([u8; 32]);
563
564impl GatewayIncarnation {
565    pub fn new(value: [u8; 32]) -> Result<Self, IdentityError> {
566        if value == [0; 32] {
567            return Err(IdentityError::Zero {
568                kind: "GatewayIncarnation",
569            });
570        }
571        Ok(Self(value))
572    }
573
574    #[must_use]
575    pub const fn get(self) -> [u8; 32] {
576        self.0
577    }
578
579    #[must_use]
580    pub const fn as_bytes(&self) -> &[u8; 32] {
581        &self.0
582    }
583}
584
585impl<'de> Deserialize<'de> for GatewayIncarnation {
586    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
587    where
588        D: Deserializer<'de>,
589    {
590        let value = <[u8; 32]>::deserialize(deserializer)?;
591        Self::new(value).map_err(de::Error::custom)
592    }
593}
594
595/// Exact TLS negotiation profile owned by one public listener.
596#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
597#[serde(rename_all = "snake_case")]
598pub enum PublicTlsProfileV1 {
599    Http,
600    Grpc,
601    Native,
602    Resp,
603}
604
605/// One intentional public Gateway protocol edge.
606#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
607#[serde(rename_all = "snake_case")]
608pub enum PublicGatewayListenerV1 {
609    #[serde(rename = "api")]
610    LodestoneApi,
611    ElasticsearchHttp,
612    OtlpHttp,
613    JaegerHttp,
614    OtlpGrpc,
615    SubscriptionGrpc,
616    SubscriptionWebSocket,
617    Resp,
618}
619
620impl PublicGatewayListenerV1 {
621    #[must_use]
622    pub const fn required_tls_profile(self) -> PublicTlsProfileV1 {
623        match self {
624            Self::LodestoneApi => PublicTlsProfileV1::Native,
625            Self::ElasticsearchHttp | Self::OtlpHttp | Self::JaegerHttp | Self::SubscriptionWebSocket => {
626                PublicTlsProfileV1::Http
627            }
628            Self::OtlpGrpc | Self::SubscriptionGrpc => PublicTlsProfileV1::Grpc,
629            Self::Resp => PublicTlsProfileV1::Resp,
630        }
631    }
632}
633
634/// One immutable production workload role. A distributed identity owns
635/// exactly one role; `roles=all` is a development composition and never a
636/// value of this enum.
637#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
638#[serde(rename_all = "snake_case")]
639pub enum NodeRole {
640    MetastoreVoter,
641    Placement,
642    Orchestrator,
643    Gateway,
644    Indexer,
645    Searcher,
646    Compactor,
647}
648
649/// The exact current incarnation of one durable node identity.
650#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
651#[serde(deny_unknown_fields)]
652pub struct NodeToken {
653    pub recovery_era: RecoveryEra,
654    pub node_id: NodeId,
655    pub node_incarnation: NodeIncarnation,
656}
657
658/// Domain-separated identity of one Kubernetes object incarnation.
659///
660/// A digest follows neither a human name nor a delete/recreate. The caller
661/// derives it from the Kubernetes cluster UID, namespace, kind, and object
662/// UID under the P7 domain.
663#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
664#[serde(transparent)]
665pub struct KubernetesObjectUidDigest([u8; 32]);
666
667impl KubernetesObjectUidDigest {
668    pub fn new(value: [u8; 32]) -> Result<Self, IdentityError> {
669        if value == [0; 32] {
670            return Err(IdentityError::Zero {
671                kind: "KubernetesObjectUidDigest",
672            });
673        }
674        Ok(Self(value))
675    }
676
677    #[must_use]
678    pub const fn get(self) -> [u8; 32] {
679        self.0
680    }
681
682    #[must_use]
683    pub const fn as_bytes(&self) -> &[u8; 32] {
684        &self.0
685    }
686}
687
688impl<'de> Deserialize<'de> for KubernetesObjectUidDigest {
689    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
690    where
691        D: Deserializer<'de>,
692    {
693        let value = <[u8; 32]>::deserialize(deserializer)?;
694        Self::new(value).map_err(de::Error::custom)
695    }
696}
697
698impl NodeToken {
699    pub fn new(
700        recovery_era: RecoveryEra,
701        node_id: NodeId,
702        node_incarnation: NodeIncarnation,
703    ) -> Result<Self, IdentityError> {
704        if node_id.get() == 0 {
705            return Err(IdentityError::Zero { kind: "NodeId" });
706        }
707        Ok(Self {
708            recovery_era,
709            node_id,
710            node_incarnation,
711        })
712    }
713}
714
715/// Complete fence for placement-controller-owned mutations.
716#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
717#[serde(deny_unknown_fields)]
718pub struct ControllerToken {
719    pub recovery_era: RecoveryEra,
720    pub controller_epoch: ControllerEpoch,
721}
722
723impl ControllerToken {
724    pub fn new(recovery_era: RecoveryEra, controller_epoch: ControllerEpoch) -> Result<Self, IdentityError> {
725        if controller_epoch.get() == 0 {
726            return Err(IdentityError::Zero {
727                kind: "ControllerEpoch",
728            });
729        }
730        Ok(Self {
731            recovery_era,
732            controller_epoch,
733        })
734    }
735}
736
737/// A stable identifier for a durable background task.
738///
739/// JSON encodes this as a decimal string because JSON numbers and
740/// `serde_json::Value` cannot losslessly carry the complete `u128` domain.
741#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
742pub struct TaskId(u128);
743
744impl TaskId {
745    #[must_use]
746    pub const fn new(value: u128) -> Self {
747        Self(value)
748    }
749
750    #[must_use]
751    pub const fn get(self) -> u128 {
752        self.0
753    }
754
755    #[must_use]
756    pub const fn checked_next(self) -> Option<Self> {
757        match self.0.checked_add(1) {
758            Some(value) => Some(Self(value)),
759            None => None,
760        }
761    }
762}
763
764impl Display for TaskId {
765    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
766        Display::fmt(&self.0, formatter)
767    }
768}
769
770impl Serialize for TaskId {
771    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
772    where
773        S: Serializer,
774    {
775        serializer.collect_str(self)
776    }
777}
778
779impl<'de> Deserialize<'de> for TaskId {
780    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
781    where
782        D: Deserializer<'de>,
783    {
784        struct TaskIdVisitor;
785
786        impl Visitor<'_> for TaskIdVisitor {
787            type Value = TaskId;
788
789            fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
790                formatter.write_str("a decimal task-id string")
791            }
792
793            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
794            where
795                E: de::Error,
796            {
797                value
798                    .parse::<u128>()
799                    .map(TaskId)
800                    .map_err(|_| E::custom("task id is outside the u128 domain"))
801            }
802        }
803
804        deserializer.deserialize_str(TaskIdVisitor)
805    }
806}
807
808macro_rules! semantic_decimal_identity {
809    ($name:ident, $documentation:literal) => {
810        #[doc = $documentation]
811        #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
812        #[serde(transparent)]
813        pub struct $name(TaskId);
814
815        impl $name {
816            /// Constructs the semantic identity.
817            #[must_use]
818            pub const fn new(value: u128) -> Self {
819                Self(TaskId::new(value))
820            }
821
822            /// Returns the underlying recovery-backed allocation value.
823            #[must_use]
824            pub const fn get(self) -> u128 {
825                self.0.get()
826            }
827        }
828
829        impl Display for $name {
830            fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
831                Display::fmt(&self.0, formatter)
832            }
833        }
834    };
835}
836
837semantic_decimal_identity!(
838    ComponentTemplateId,
839    "A recovery-backed identity for one component-template incarnation."
840);
841semantic_decimal_identity!(
842    IndexTemplateId,
843    "A recovery-backed identity for one index-template incarnation."
844);
845semantic_decimal_identity!(
846    IngestPipelineId,
847    "A recovery-backed identity for one ingest-pipeline incarnation."
848);
849semantic_decimal_identity!(
850    DataStreamId,
851    "A recovery-backed identity for one data-stream incarnation."
852);
853semantic_decimal_identity!(
854    LifecyclePolicyId,
855    "A recovery-backed identity for one lifecycle-policy incarnation."
856);
857semantic_decimal_identity!(
858    ContinuousEvaluationId,
859    "A recovery-backed identity for one Collection-owned continuous temporal evaluation."
860);
861semantic_decimal_identity!(
862    ExternalKeyResolverCapabilityId,
863    "An opaque capability identity authorizing one external content-key resolver class."
864);
865semantic_decimal_identity!(
866    PrincipalId,
867    "A recovery-backed, never-reused security-principal identity."
868);
869semantic_decimal_identity!(ApiKeyId, "A recovery-backed, never-reused API-key identity.");
870semantic_decimal_identity!(BackupId, "A recovery-backed, never-reused portable-backup identity.");
871
872/// Stable idempotency identity of one administration action.
873#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
874#[serde(transparent)]
875pub struct AdministrationActionId([u8; 32]);
876
877impl AdministrationActionId {
878    /// Constructs a nonzero action identity.
879    pub fn new(value: [u8; 32]) -> Result<Self, IdentityError> {
880        if value == [0; 32] {
881            return Err(IdentityError::Zero {
882                kind: "AdministrationActionId",
883            });
884        }
885        Ok(Self(value))
886    }
887
888    /// Returns the stable action bytes.
889    #[must_use]
890    pub const fn get(self) -> [u8; 32] {
891        self.0
892    }
893
894    /// Borrows the stable action bytes.
895    #[must_use]
896    pub const fn as_bytes(&self) -> &[u8; 32] {
897        &self.0
898    }
899}
900
901impl<'de> Deserialize<'de> for AdministrationActionId {
902    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
903    where
904        D: Deserializer<'de>,
905    {
906        let value = <[u8; 32]>::deserialize(deserializer)?;
907        Self::new(value).map_err(de::Error::custom)
908    }
909}
910
911/// Stable idempotency identity of one security-policy or audit action.
912#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
913#[serde(transparent)]
914pub struct SecurityActionId([u8; 32]);
915
916impl SecurityActionId {
917    /// Constructs a nonzero action identity.
918    pub fn new(value: [u8; 32]) -> Result<Self, IdentityError> {
919        if value == [0; 32] {
920            return Err(IdentityError::Zero {
921                kind: "SecurityActionId",
922            });
923        }
924        Ok(Self(value))
925    }
926
927    /// Returns the stable action bytes.
928    #[must_use]
929    pub const fn get(self) -> [u8; 32] {
930        self.0
931    }
932
933    /// Borrows the stable action bytes.
934    #[must_use]
935    pub const fn as_bytes(&self) -> &[u8; 32] {
936        &self.0
937    }
938}
939
940impl<'de> Deserialize<'de> for SecurityActionId {
941    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
942    where
943        D: Deserializer<'de>,
944    {
945        let value = <[u8; 32]>::deserialize(deserializer)?;
946        Self::new(value).map_err(de::Error::custom)
947    }
948}
949
950/// Stable idempotency identity of one portable-backup lifecycle action.
951#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
952#[serde(transparent)]
953pub struct BackupActionId([u8; 32]);
954
955impl BackupActionId {
956    /// Constructs a nonzero action identity.
957    pub fn new(value: [u8; 32]) -> Result<Self, IdentityError> {
958        if value == [0; 32] {
959            return Err(IdentityError::Zero { kind: "BackupActionId" });
960        }
961        Ok(Self(value))
962    }
963
964    /// Returns the stable action bytes.
965    #[must_use]
966    pub const fn get(self) -> [u8; 32] {
967        self.0
968    }
969
970    /// Borrows the stable action bytes.
971    #[must_use]
972    pub const fn as_bytes(&self) -> &[u8; 32] {
973        &self.0
974    }
975}
976
977impl<'de> Deserialize<'de> for BackupActionId {
978    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
979    where
980        D: Deserializer<'de>,
981    {
982        let value = <[u8; 32]>::deserialize(deserializer)?;
983        Self::new(value).map_err(de::Error::custom)
984    }
985}
986
987/// The exclusive upper bound of a shard routing-hash range.
988///
989/// This is a `u64` because the final range ends at `2^32`, one past the
990/// largest value representable by [`RoutingHash`].
991#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
992#[serde(transparent)]
993pub struct RoutingRangeEnd(u64);
994
995impl RoutingRangeEnd {
996    /// The exclusive upper bound of the complete u32 routing space.
997    pub const FULL: Self = Self(u32::MAX as u64 + 1);
998
999    /// Constructs a checked exclusive routing-range bound.
1000    pub fn new(value: u64) -> Result<Self, RoutingRangeError> {
1001        if value == 0 || value > Self::FULL.0 {
1002            Err(RoutingRangeError::InvalidEnd(value))
1003        } else {
1004            Ok(Self(value))
1005        }
1006    }
1007
1008    /// Returns the underlying exclusive bound.
1009    #[must_use]
1010    pub const fn get(self) -> u64 {
1011        self.0
1012    }
1013}
1014
1015/// A distribution-ready shard identity and its current fencing epoch.
1016#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1017pub struct ShardDescriptor {
1018    pub shard_id: ShardId,
1019    pub epoch: Epoch,
1020    pub hash_start: RoutingHash,
1021    pub hash_end: RoutingRangeEnd,
1022}
1023
1024impl ShardDescriptor {
1025    /// Constructs a non-empty hash-range descriptor.
1026    pub fn new(
1027        shard_id: ShardId,
1028        epoch: Epoch,
1029        hash_start: RoutingHash,
1030        hash_end: RoutingRangeEnd,
1031    ) -> Result<Self, RoutingRangeError> {
1032        if u64::from(hash_start.get()) >= hash_end.get() {
1033            return Err(RoutingRangeError::EmptyOrReversed {
1034                start: hash_start,
1035                end: hash_end,
1036            });
1037        }
1038        Ok(Self {
1039            shard_id,
1040            epoch,
1041            hash_start,
1042            hash_end,
1043        })
1044    }
1045
1046    /// Returns whether the stored routing hash belongs to this descriptor.
1047    #[must_use]
1048    pub const fn contains(self, routing_hash: RoutingHash) -> bool {
1049        let value = routing_hash.get() as u64;
1050        value >= self.hash_start.get() as u64 && value < self.hash_end.get()
1051    }
1052}
1053
1054/// Invalid routing-range metadata.
1055#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1056pub enum RoutingRangeError {
1057    InvalidEnd(u64),
1058    EmptyOrReversed { start: RoutingHash, end: RoutingRangeEnd },
1059}
1060
1061impl Display for RoutingRangeError {
1062    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
1063        match self {
1064            Self::InvalidEnd(end) => write!(
1065                formatter,
1066                "routing range end {end} is outside 1..={}",
1067                RoutingRangeEnd::FULL.get()
1068            ),
1069            Self::EmptyOrReversed { start, end } => write!(
1070                formatter,
1071                "routing range start {start} must be below exclusive end {}",
1072                end.get()
1073            ),
1074        }
1075    }
1076}
1077
1078impl Error for RoutingRangeError {}
1079
1080/// Versioned durability policy identity.
1081///
1082/// The policy names describe behavior rather than one provider product.
1083/// `ZonalLowLatency` is admitted only when the selected object leaf proves the
1084/// required zonal semantics. The remaining stable names are deliberate
1085/// reservations and return a typed unavailable refusal.
1086#[non_exhaustive]
1087#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
1088#[serde(rename_all = "snake_case")]
1089pub enum DurabilityClass {
1090    RegionalObject,
1091    ZonalLowLatency,
1092    Relaxed,
1093    Ephemeral,
1094}
1095
1096impl DurabilityClass {
1097    /// Returns the stable wire name.
1098    #[must_use]
1099    pub const fn as_str(self) -> &'static str {
1100        match self {
1101            Self::RegionalObject => "regional_object",
1102            Self::ZonalLowLatency => "zonal_low_latency",
1103            Self::Relaxed => "relaxed",
1104            Self::Ephemeral => "ephemeral",
1105        }
1106    }
1107
1108    /// Refuses every durability class without an admitted execution protocol.
1109    pub const fn require_available(self) -> Result<(), DurabilityClassUnavailable> {
1110        match self {
1111            Self::RegionalObject | Self::ZonalLowLatency => Ok(()),
1112            Self::Relaxed | Self::Ephemeral => Err(DurabilityClassUnavailable { class: self }),
1113        }
1114    }
1115
1116    /// Refuses classes without an admitted state-index acknowledgement model.
1117    ///
1118    /// P12.0 admits `ephemeral` at a whole-incarnation memory boundary, but
1119    /// does not make it reachable through the ordinary registered-WAL writer.
1120    pub const fn require_state_available(self) -> Result<(), DurabilityClassUnavailable> {
1121        match self {
1122            Self::RegionalObject | Self::ZonalLowLatency | Self::Ephemeral => Ok(()),
1123            Self::Relaxed => Err(DurabilityClassUnavailable { class: self }),
1124        }
1125    }
1126}
1127
1128impl Display for DurabilityClass {
1129    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
1130        formatter.write_str(self.as_str())
1131    }
1132}
1133
1134/// A recognized durability policy that has no admitted execution backend.
1135#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1136pub struct DurabilityClassUnavailable {
1137    pub class: DurabilityClass,
1138}
1139
1140impl Display for DurabilityClassUnavailable {
1141    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
1142        write!(
1143            formatter,
1144            "durability class '{}' is recognized but unavailable",
1145            self.class
1146        )
1147    }
1148}
1149
1150impl Error for DurabilityClassUnavailable {}
1151
1152/// Client response visibility independent of durability.
1153#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
1154#[serde(rename_all = "snake_case")]
1155pub enum VisibilityMode {
1156    #[default]
1157    Applied,
1158    WaitFor,
1159}
1160
1161/// The durable acknowledgement boundary actually achieved.
1162#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1163#[serde(rename_all = "snake_case")]
1164pub enum DurabilityBoundary {
1165    Registered,
1166    /// P12 state is visible only inside the current fenced RAM incarnation.
1167    IncarnationMemory,
1168}
1169
1170/// Maximum failure loss promised by a successful acknowledgement.
1171#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1172#[serde(rename_all = "snake_case", tag = "kind")]
1173pub enum DurabilityLoss {
1174    Zero,
1175    WholeIncarnation,
1176}
1177
1178/// Observable write-pipeline progress.
1179#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
1180#[serde(rename_all = "snake_case")]
1181pub enum WriteStage {
1182    Validated,
1183    Admitted,
1184    Sequenced,
1185    ObjectPut,
1186    Registered,
1187    AppliedToActiveMemtable,
1188    MicroCommitted,
1189    Sealed,
1190}
1191
1192/// Per-index mutation mode.
1193#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1194#[serde(rename_all = "snake_case")]
1195pub enum IndexMode {
1196    Mutable,
1197    AppendLog,
1198    /// Primary truth is a fenced, fully resident state table; Tantivy is an
1199    /// asynchronously derived mutable projection.
1200    State,
1201}
1202
1203impl IndexMode {
1204    /// Whether indexed versions require identity replacement and tombstones.
1205    #[must_use]
1206    pub const fn has_mutation_visibility(self) -> bool {
1207        matches!(self, Self::Mutable | Self::State)
1208    }
1209
1210    #[must_use]
1211    pub const fn is_state(self) -> bool {
1212        matches!(self, Self::State)
1213    }
1214}
1215
1216/// Current version of the normalized semantic mapping model shared by the
1217/// catalog and Tantivy compiler. Format 8 adds the closed mapping-owned record
1218/// model used by native typed records, format 9 adds relationships, and format
1219/// 10 adds privacy-bound continuous-profile batches. Older mappings are
1220/// deliberately refused rather than reinterpreted.
1221pub const MAPPING_FORMAT_VERSION: u16 = 10;
1222
1223/// Maximum fields admitted by one normalized mapping.
1224pub const MAX_MAPPING_FIELDS: usize = 16_384;
1225
1226/// Version of the vector mapping extension embedded in the catalog mapping.
1227pub const VECTOR_MAPPING_FORMAT_VERSION: u16 = 2;
1228
1229/// Version of the sparse-vector mapping extension embedded in the catalog.
1230pub const SPARSE_VECTOR_MAPPING_FORMAT_VERSION: u16 = 1;
1231
1232/// Version of the vector-set mapping extension embedded in the catalog.
1233pub const VECTOR_SET_MAPPING_FORMAT_VERSION: u16 = 1;
1234
1235/// Maximum vector fields admitted by one mapping generation.
1236pub const MAX_VECTOR_MAPPING_FIELDS: usize = 8;
1237
1238/// Hard logical dimension ceiling shared by mappings, source values, queries,
1239/// codecs, and generated protocol projections.
1240pub const MAX_VECTOR_DIMENSIONS: u16 = 16_384;
1241
1242/// Maximum encoded payload admitted for one dense value.
1243pub const MAX_VECTOR_BYTES_PER_FIELD: usize = 64 * 1_024;
1244
1245/// Maximum encoded dense-vector payload admitted for one document before WAL.
1246pub const MAX_VECTOR_BYTES_PER_DOCUMENT: usize = 64 * 1_024;
1247
1248/// Maximum encoded dense-vector payload admitted for one query branch.
1249pub const MAX_VECTOR_BYTES_PER_QUERY: usize = 64 * 1_024;
1250
1251/// Maximum aggregate encoded dense-vector payload admitted in one request.
1252pub const MAX_VECTOR_BYTES_PER_REQUEST: usize = 16 * MAX_VECTOR_BYTES_PER_QUERY;
1253
1254/// Maximum numeric sparse dimension admitted by mappings and values.
1255pub const MAX_SPARSE_VECTOR_DIMENSION: u32 = 16_777_215;
1256
1257/// Maximum non-zero entries admitted for one sparse field or query.
1258pub const MAX_SPARSE_VECTOR_NONZERO: usize = 4_096;
1259
1260/// Maximum encoded sparse payload admitted for one field or query. Each
1261/// entry owns one u32 dimension and one mapping-selected weight.
1262pub const MAX_SPARSE_VECTOR_BYTES: usize = 32 * 1_024;
1263
1264/// Maximum token vectors admitted for one document vector-set field.
1265pub const MAX_VECTOR_SET_DOCUMENT_VECTORS: usize = 128;
1266
1267/// Maximum token vectors admitted for one late-interaction query branch.
1268pub const MAX_VECTOR_SET_QUERY_VECTORS: usize = 32;
1269
1270/// Maximum canonical element bytes admitted for one document vector set.
1271pub const MAX_VECTOR_SET_BYTES_PER_FIELD: usize = 64 * 1_024;
1272
1273/// Maximum canonical element bytes admitted for one query vector set.
1274pub const MAX_VECTOR_SET_BYTES_PER_QUERY: usize = 64 * 1_024;
1275
1276/// Maximum vector records retained by one mutable fresh-generation overlay.
1277pub const MAX_FRESH_VECTOR_RECORDS: usize = 16_384;
1278
1279/// Maximum decoded vector bytes retained by one mutable fresh-generation
1280/// overlay. This is deliberately independent of one document's byte ceiling.
1281pub const MAX_FRESH_VECTOR_BYTES: usize = 128 * 1_024 * 1_024;
1282
1283/// Absolute ceiling for one immutable-vector build's reconstructible scratch.
1284/// Operators may and normally do assign a smaller resource budget.
1285pub const MAX_VECTOR_BUILD_SCRATCH_BYTES: u64 = 16 * 1_024 * 1_024 * 1_024;
1286
1287/// Absolute ceiling for one process's decoded immutable-vector cache. The
1288/// runtime resource specification remains the smaller operational authority.
1289pub const MAX_VECTOR_DECODED_CACHE_BYTES: u64 = 16 * 1_024 * 1_024 * 1_024;
1290
1291/// Hard logical ceiling for vector bytes returned as explicitly requested
1292/// result features. Ordinary source bytes use the ordinary response budget.
1293pub const MAX_VECTOR_BYTES_PER_RESPONSE: usize = 64 * 1_024;
1294
1295/// Logical dense element representation. Physical ANN strategy remains a
1296/// reconstructible implementation choice and is not encoded here.
1297#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
1298#[serde(rename_all = "snake_case")]
1299pub enum DenseVectorElementType {
1300    F32,
1301    F16,
1302    I8,
1303}
1304
1305/// Logical sparse weight representation. Dimension identifiers remain u32
1306/// and are never hashed or dictionary-resolved by the engine.
1307#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
1308#[serde(rename_all = "snake_case")]
1309pub enum SparseVectorWeightType {
1310    F32,
1311    F16,
1312}
1313
1314impl SparseVectorWeightType {
1315    #[must_use]
1316    pub const fn width_bytes(self) -> usize {
1317        match self {
1318            Self::F32 => 4,
1319            Self::F16 => 2,
1320        }
1321    }
1322}
1323
1324/// One caller-supplied sparse coordinate. Canonical values are strictly
1325/// increasing by dimension and have a finite, non-zero weight.
1326#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
1327#[serde(deny_unknown_fields)]
1328pub struct SparseVectorEntry {
1329    pub dimension: u32,
1330    #[serde(deserialize_with = "deserialize_sparse_weight")]
1331    pub weight: f64,
1332}
1333
1334fn deserialize_sparse_weight<'de, D>(deserializer: D) -> Result<f64, D::Error>
1335where
1336    D: Deserializer<'de>,
1337{
1338    serde_json::Number::deserialize(deserializer)?
1339        .to_string()
1340        .parse::<f64>()
1341        .map_err(de::Error::custom)
1342}
1343
1344/// Native sparse value. Object form keeps this distinct from dense arrays and
1345/// leaves no string-feature hashing or dictionary semantics to infer.
1346#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1347#[serde(deny_unknown_fields)]
1348pub struct SparseVectorValue {
1349    #[serde(deserialize_with = "deserialize_bounded_sparse_entries")]
1350    pub entries: Vec<SparseVectorEntry>,
1351}
1352
1353/// Exact mapping semantics required to encode one sparse value.
1354#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1355pub struct SparseVectorDefinition {
1356    pub maximum_dimension: u32,
1357    pub weight_type: SparseVectorWeightType,
1358}
1359
1360/// One validated sparse coordinate in the mapping-selected logical precision.
1361#[derive(Clone, Copy, Debug, PartialEq)]
1362pub struct DecodedSparseVectorEntry {
1363    pub dimension: u32,
1364    pub weight: f32,
1365}
1366
1367impl SparseVectorDefinition {
1368    pub fn validate(self) -> Result<(), SparseVectorValueError> {
1369        if self.maximum_dimension > MAX_SPARSE_VECTOR_DIMENSION {
1370            return Err(SparseVectorValueError::InvalidMaximumDimension(self.maximum_dimension));
1371        }
1372        let maximum_bytes = MAX_SPARSE_VECTOR_NONZERO
1373            .checked_mul(4_usize.saturating_add(self.weight_type.width_bytes()))
1374            .ok_or(SparseVectorValueError::ByteLengthOverflow)?;
1375        if maximum_bytes > MAX_SPARSE_VECTOR_BYTES {
1376            return Err(SparseVectorValueError::FieldBytesExceeded(maximum_bytes));
1377        }
1378        Ok(())
1379    }
1380}
1381
1382impl SparseVectorValue {
1383    pub fn decode(
1384        &self,
1385        definition: SparseVectorDefinition,
1386    ) -> Result<Vec<DecodedSparseVectorEntry>, SparseVectorValueError> {
1387        definition.validate()?;
1388        self.validate_bounded(definition, MAX_SPARSE_VECTOR_BYTES)?;
1389        self.entries
1390            .iter()
1391            .enumerate()
1392            .map(|(ordinal, entry)| {
1393                let weight = entry.weight as f32;
1394                if !weight.is_finite() {
1395                    return Err(SparseVectorValueError::NonFiniteWeight(ordinal));
1396                }
1397                let weight = match definition.weight_type {
1398                    SparseVectorWeightType::F32 => canonical_vector_zero(weight),
1399                    SparseVectorWeightType::F16 => {
1400                        let bits = f32_to_f16_rne(weight).ok_or(SparseVectorValueError::F16Overflow(ordinal))?;
1401                        canonical_vector_zero(f16_to_f32(bits))
1402                    }
1403                };
1404                if weight == 0.0 {
1405                    return Err(SparseVectorValueError::ZeroWeight(ordinal));
1406                }
1407                Ok(DecodedSparseVectorEntry {
1408                    dimension: entry.dimension,
1409                    weight,
1410                })
1411            })
1412            .collect()
1413    }
1414
1415    pub fn validate_bounded(
1416        &self,
1417        definition: SparseVectorDefinition,
1418        maximum_bytes: usize,
1419    ) -> Result<usize, SparseVectorValueError> {
1420        definition.validate()?;
1421        if self.entries.is_empty() {
1422            return Err(SparseVectorValueError::Empty);
1423        }
1424        if self.entries.len() > MAX_SPARSE_VECTOR_NONZERO {
1425            return Err(SparseVectorValueError::TooManyEntries(self.entries.len()));
1426        }
1427        let bytes = self
1428            .entries
1429            .len()
1430            .checked_mul(4_usize.saturating_add(definition.weight_type.width_bytes()))
1431            .ok_or(SparseVectorValueError::ByteLengthOverflow)?;
1432        if bytes > maximum_bytes.min(MAX_SPARSE_VECTOR_BYTES) {
1433            return Err(SparseVectorValueError::FieldBytesExceeded(bytes));
1434        }
1435        let mut previous = None;
1436        for (ordinal, entry) in self.entries.iter().enumerate() {
1437            if entry.dimension > definition.maximum_dimension {
1438                return Err(SparseVectorValueError::DimensionOutOfRange {
1439                    ordinal,
1440                    dimension: entry.dimension,
1441                    maximum: definition.maximum_dimension,
1442                });
1443            }
1444            if previous.is_some_and(|dimension| entry.dimension <= dimension) {
1445                return Err(SparseVectorValueError::DimensionsNotStrict(ordinal));
1446            }
1447            if !entry.weight.is_finite() {
1448                return Err(SparseVectorValueError::NonFiniteWeight(ordinal));
1449            }
1450            if entry.weight == 0.0 {
1451                return Err(SparseVectorValueError::ZeroWeight(ordinal));
1452            }
1453            previous = Some(entry.dimension);
1454        }
1455        Ok(bytes)
1456    }
1457
1458    pub fn canonical_native_bytes(
1459        &self,
1460        definition: SparseVectorDefinition,
1461    ) -> Result<Vec<u8>, SparseVectorValueError> {
1462        let decoded = self.decode(definition)?;
1463        let mut bytes = Vec::with_capacity(
1464            decoded
1465                .len()
1466                .saturating_mul(4_usize.saturating_add(definition.weight_type.width_bytes())),
1467        );
1468        for entry in decoded {
1469            bytes.extend_from_slice(&entry.dimension.to_le_bytes());
1470            match definition.weight_type {
1471                SparseVectorWeightType::F32 => bytes.extend_from_slice(&entry.weight.to_bits().to_le_bytes()),
1472                SparseVectorWeightType::F16 => {
1473                    let bits = f32_to_f16_rne(entry.weight).ok_or(SparseVectorValueError::F16Overflow(0))?;
1474                    bytes.extend_from_slice(&bits.to_le_bytes());
1475                }
1476            }
1477        }
1478        Ok(bytes)
1479    }
1480}
1481
1482/// Decodes canonical interleaved `(u32 dimension, weight)` bytes. Immutable
1483/// bundle readers use this to reject malformed postings without inventing a
1484/// second sparse codec.
1485pub fn decode_sparse_native_bytes(
1486    definition: SparseVectorDefinition,
1487    bytes: &[u8],
1488) -> Result<Vec<DecodedSparseVectorEntry>, SparseVectorValueError> {
1489    definition.validate()?;
1490    let entry_bytes = 4_usize.saturating_add(definition.weight_type.width_bytes());
1491    if bytes.is_empty() || !bytes.len().is_multiple_of(entry_bytes) {
1492        return Err(SparseVectorValueError::FieldBytesExceeded(bytes.len()));
1493    }
1494    let count = bytes.len() / entry_bytes;
1495    if count > MAX_SPARSE_VECTOR_NONZERO || bytes.len() > MAX_SPARSE_VECTOR_BYTES {
1496        return Err(SparseVectorValueError::FieldBytesExceeded(bytes.len()));
1497    }
1498    let mut entries = Vec::with_capacity(count);
1499    let mut previous = None;
1500    for (ordinal, chunk) in bytes.chunks_exact(entry_bytes).enumerate() {
1501        let dimension = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
1502        if dimension > definition.maximum_dimension {
1503            return Err(SparseVectorValueError::DimensionOutOfRange {
1504                ordinal,
1505                dimension,
1506                maximum: definition.maximum_dimension,
1507            });
1508        }
1509        if previous.is_some_and(|value| dimension <= value) {
1510            return Err(SparseVectorValueError::DimensionsNotStrict(ordinal));
1511        }
1512        let weight = match definition.weight_type {
1513            SparseVectorWeightType::F32 => f32::from_le_bytes([chunk[4], chunk[5], chunk[6], chunk[7]]),
1514            SparseVectorWeightType::F16 => f16_to_f32(u16::from_le_bytes([chunk[4], chunk[5]])),
1515        };
1516        if !weight.is_finite() {
1517            return Err(SparseVectorValueError::NonFiniteWeight(ordinal));
1518        }
1519        let weight = canonical_vector_zero(weight);
1520        if weight == 0.0 {
1521            return Err(SparseVectorValueError::ZeroWeight(ordinal));
1522        }
1523        entries.push(DecodedSparseVectorEntry { dimension, weight });
1524        previous = Some(dimension);
1525    }
1526    Ok(entries)
1527}
1528
1529#[derive(Clone, Debug, Eq, PartialEq)]
1530pub enum SparseVectorValueError {
1531    Empty,
1532    InvalidMaximumDimension(u32),
1533    TooManyEntries(usize),
1534    ByteLengthOverflow,
1535    FieldBytesExceeded(usize),
1536    DimensionsNotStrict(usize),
1537    DimensionOutOfRange {
1538        ordinal: usize,
1539        dimension: u32,
1540        maximum: u32,
1541    },
1542    NonFiniteWeight(usize),
1543    ZeroWeight(usize),
1544    F16Overflow(usize),
1545}
1546
1547impl Display for SparseVectorValueError {
1548    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
1549        match self {
1550            Self::Empty => formatter.write_str("sparse vector value is empty"),
1551            Self::InvalidMaximumDimension(value) => {
1552                write!(formatter, "sparse maximum dimension {value} is invalid")
1553            }
1554            Self::TooManyEntries(value) => write!(formatter, "sparse vector has {value} non-zero entries"),
1555            Self::ByteLengthOverflow => formatter.write_str("sparse vector byte length overflowed"),
1556            Self::FieldBytesExceeded(value) => {
1557                write!(formatter, "sparse vector uses {value} bytes above its field ceiling")
1558            }
1559            Self::DimensionsNotStrict(ordinal) => {
1560                write!(formatter, "sparse vector dimension {ordinal} is duplicate or unsorted")
1561            }
1562            Self::DimensionOutOfRange {
1563                ordinal,
1564                dimension,
1565                maximum,
1566            } => write!(
1567                formatter,
1568                "sparse vector dimension {ordinal} value {dimension} exceeds {maximum}"
1569            ),
1570            Self::NonFiniteWeight(ordinal) => write!(formatter, "sparse vector weight {ordinal} is non-finite"),
1571            Self::ZeroWeight(ordinal) => write!(formatter, "sparse vector weight {ordinal} is zero"),
1572            Self::F16Overflow(ordinal) => write!(formatter, "sparse f16 weight {ordinal} overflows binary16"),
1573        }
1574    }
1575}
1576
1577impl Error for SparseVectorValueError {}
1578
1579fn deserialize_bounded_sparse_entries<'de, D>(deserializer: D) -> Result<Vec<SparseVectorEntry>, D::Error>
1580where
1581    D: Deserializer<'de>,
1582{
1583    struct SparseEntriesVisitor;
1584
1585    impl<'de> serde::de::Visitor<'de> for SparseEntriesVisitor {
1586        type Value = Vec<SparseVectorEntry>;
1587
1588        fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
1589            formatter.write_str("a bounded sparse entry sequence")
1590        }
1591
1592        fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
1593        where
1594            A: serde::de::SeqAccess<'de>,
1595        {
1596            if sequence
1597                .size_hint()
1598                .is_some_and(|length| length > MAX_SPARSE_VECTOR_NONZERO)
1599            {
1600                return Err(de::Error::custom("sparse vector exceeds the non-zero ceiling"));
1601            }
1602            let mut entries =
1603                Vec::with_capacity(sequence.size_hint().unwrap_or_default().min(MAX_SPARSE_VECTOR_NONZERO));
1604            while let Some(entry) = sequence.next_element::<SparseVectorEntry>()? {
1605                if entries.len() == MAX_SPARSE_VECTOR_NONZERO {
1606                    return Err(de::Error::custom("sparse vector exceeds the non-zero ceiling"));
1607                }
1608                entries.push(entry);
1609            }
1610            Ok(entries)
1611        }
1612    }
1613
1614    deserializer.deserialize_seq(SparseEntriesVisitor)
1615}
1616
1617impl DenseVectorElementType {
1618    #[must_use]
1619    pub const fn width_bytes(self) -> usize {
1620        match self {
1621            Self::F32 => 4,
1622            Self::F16 => 2,
1623            Self::I8 => 1,
1624        }
1625    }
1626}
1627
1628/// Positive finite scale for an `i8` logical dense field. The canonical wire
1629/// form is the shortest Rust decimal spelling of the exact f32 value.
1630#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
1631pub struct VectorQuantizationScale(u32);
1632
1633impl VectorQuantizationScale {
1634    pub fn new(value: f32) -> Result<Self, DenseVectorValueError> {
1635        if !value.is_finite() || value <= 0.0 {
1636            return Err(DenseVectorValueError::InvalidQuantizationScale);
1637        }
1638        Ok(Self(value.to_bits()))
1639    }
1640
1641    #[must_use]
1642    pub const fn get(self) -> f32 {
1643        f32::from_bits(self.0)
1644    }
1645}
1646
1647impl Serialize for VectorQuantizationScale {
1648    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1649    where
1650        S: Serializer,
1651    {
1652        serializer.serialize_str(&self.get().to_string())
1653    }
1654}
1655
1656impl<'de> Deserialize<'de> for VectorQuantizationScale {
1657    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1658    where
1659        D: Deserializer<'de>,
1660    {
1661        let text = String::deserialize(deserializer)?;
1662        let value = text.parse::<f32>().map_err(de::Error::custom)?;
1663        if value.to_string() != text {
1664            return Err(de::Error::custom("vector quantization scale is not canonical"));
1665        }
1666        Self::new(value).map_err(de::Error::custom)
1667    }
1668}
1669
1670/// Only little-endian binary vector values are canonical.
1671#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1672#[serde(rename_all = "snake_case")]
1673pub enum DenseVectorByteOrder {
1674    LittleEndian,
1675}
1676
1677/// Binary half of the native dense `VectorValue` union.
1678#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1679#[serde(deny_unknown_fields)]
1680pub struct DenseVectorBinaryValue {
1681    pub element_type: DenseVectorElementType,
1682    pub byte_order: DenseVectorByteOrder,
1683    pub data_base64: String,
1684    #[serde(default, skip_serializing_if = "Option::is_none")]
1685    pub quantization_scale: Option<VectorQuantizationScale>,
1686}
1687
1688/// One native dense value. Document source also admits the numeric variant as
1689/// its compact bare-array spelling; exact `_source` bytes are never rewritten.
1690#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1691#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
1692pub enum DenseVectorValue {
1693    Numeric {
1694        #[serde(deserialize_with = "deserialize_bounded_dense_numbers")]
1695        values: Vec<f64>,
1696    },
1697    Binary {
1698        #[serde(flatten)]
1699        value: DenseVectorBinaryValue,
1700    },
1701}
1702
1703impl DenseVectorValue {
1704    pub fn decode(&self, definition: DenseVectorDefinition) -> Result<Vec<f32>, DenseVectorValueError> {
1705        definition.validate()?;
1706        match self {
1707            Self::Numeric { values } => decode_dense_numeric_value(definition, values),
1708            Self::Binary { value } => decode_dense_binary_value(definition, value),
1709        }
1710    }
1711
1712    /// Returns the canonical native element bytes without metric
1713    /// normalization. These bytes are reconstructible from exact source and
1714    /// let immutable accelerators preserve the mapping's declared element
1715    /// representation instead of narrowing normalized scoring coordinates.
1716    pub fn canonical_native_bytes(&self, definition: DenseVectorDefinition) -> Result<Vec<u8>, DenseVectorValueError> {
1717        definition.validate()?;
1718        match self {
1719            Self::Numeric { values } => encode_dense_numeric_native_bytes(definition, values),
1720            Self::Binary { value } => decode_dense_binary_native_bytes(definition, value),
1721        }
1722    }
1723
1724    #[must_use]
1725    pub fn element_count(&self) -> Option<usize> {
1726        match self {
1727            Self::Numeric { values } => Some(values.len()),
1728            Self::Binary { .. } => None,
1729        }
1730    }
1731
1732    pub fn validate_bounded(&self, maximum_bytes: usize) -> Result<usize, DenseVectorValueError> {
1733        match self {
1734            Self::Numeric { values } => {
1735                if values.is_empty() {
1736                    return Err(DenseVectorValueError::Empty);
1737                }
1738                let bytes = values
1739                    .len()
1740                    .checked_mul(DenseVectorElementType::F32.width_bytes())
1741                    .ok_or(DenseVectorValueError::ByteLengthOverflow)?;
1742                if values.len() > usize::from(MAX_VECTOR_DIMENSIONS) || bytes > maximum_bytes {
1743                    return Err(DenseVectorValueError::FieldBytesExceeded(bytes));
1744                }
1745                if let Some(ordinal) = values.iter().position(|value| !value.is_finite()) {
1746                    return Err(DenseVectorValueError::NonFiniteElement(ordinal));
1747                }
1748                Ok(bytes)
1749            }
1750            Self::Binary { value } => {
1751                match (value.element_type, value.quantization_scale) {
1752                    (DenseVectorElementType::I8, Some(_))
1753                    | (DenseVectorElementType::F32 | DenseVectorElementType::F16, None) => {}
1754                    (DenseVectorElementType::I8, None) => {
1755                        return Err(DenseVectorValueError::MissingQuantizationScale);
1756                    }
1757                    (DenseVectorElementType::F32 | DenseVectorElementType::F16, Some(_)) => {
1758                        return Err(DenseVectorValueError::UnexpectedQuantizationScale);
1759                    }
1760                }
1761                if value.data_base64.is_empty() || value.data_base64.len() > maximum_bytes.div_ceil(3) * 4 {
1762                    return Err(DenseVectorValueError::FieldBytesExceeded(value.data_base64.len()));
1763                }
1764                let bytes = BASE64_STANDARD
1765                    .decode(value.data_base64.as_bytes())
1766                    .map_err(|_| DenseVectorValueError::InvalidBase64)?;
1767                if bytes.is_empty()
1768                    || bytes.len() > maximum_bytes
1769                    || !bytes.len().is_multiple_of(value.element_type.width_bytes())
1770                    || bytes.len() / value.element_type.width_bytes() > usize::from(MAX_VECTOR_DIMENSIONS)
1771                {
1772                    return Err(DenseVectorValueError::FieldBytesExceeded(bytes.len()));
1773                }
1774                if BASE64_STANDARD.encode(&bytes) != value.data_base64 {
1775                    return Err(DenseVectorValueError::InvalidBase64);
1776                }
1777                Ok(bytes.len())
1778            }
1779        }
1780    }
1781}
1782
1783/// Exact mapping semantics required to decode one dense value.
1784#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1785pub struct DenseVectorDefinition {
1786    pub dimensions: u16,
1787    pub element_type: DenseVectorElementType,
1788    pub metric: VectorMetric,
1789    pub quantization_scale: Option<VectorQuantizationScale>,
1790}
1791
1792impl DenseVectorDefinition {
1793    pub fn validate(self) -> Result<(), DenseVectorValueError> {
1794        if self.dimensions == 0 || self.dimensions > MAX_VECTOR_DIMENSIONS {
1795            return Err(DenseVectorValueError::InvalidDimensions(self.dimensions));
1796        }
1797        let bytes = usize::from(self.dimensions)
1798            .checked_mul(self.element_type.width_bytes())
1799            .ok_or(DenseVectorValueError::ByteLengthOverflow)?;
1800        if bytes > MAX_VECTOR_BYTES_PER_FIELD {
1801            return Err(DenseVectorValueError::FieldBytesExceeded(bytes));
1802        }
1803        match (self.element_type, self.quantization_scale) {
1804            (DenseVectorElementType::I8, Some(_))
1805            | (DenseVectorElementType::F32 | DenseVectorElementType::F16, None) => Ok(()),
1806            (DenseVectorElementType::I8, None) => Err(DenseVectorValueError::MissingQuantizationScale),
1807            (DenseVectorElementType::F32 | DenseVectorElementType::F16, Some(_)) => {
1808                Err(DenseVectorValueError::UnexpectedQuantizationScale)
1809            }
1810        }
1811    }
1812
1813    #[must_use]
1814    pub const fn encoded_bytes(self) -> usize {
1815        self.dimensions as usize * self.element_type.width_bytes()
1816    }
1817}
1818
1819/// A vector-set value is an explicit object and can never be confused with one
1820/// dense numeric array. Each member uses the ordinary dense value codec.
1821#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1822#[serde(deny_unknown_fields)]
1823pub struct VectorSetValue {
1824    #[serde(deserialize_with = "deserialize_bounded_vector_set")]
1825    pub vectors: Vec<DenseVectorValue>,
1826}
1827
1828/// Mapping-selected semantics for one bounded document vector set.
1829#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1830pub struct VectorSetDefinition {
1831    pub dense: DenseVectorDefinition,
1832    pub maximum_vectors: u16,
1833}
1834
1835/// One decoded vector set and its per-vector canonical element bytes.
1836#[derive(Clone, Debug, PartialEq)]
1837pub struct DecodedVectorSet {
1838    pub vectors: Vec<Vec<f32>>,
1839    pub native_vectors: Vec<Vec<u8>>,
1840    pub native_bytes: usize,
1841}
1842
1843impl VectorSetDefinition {
1844    pub fn validate(self) -> Result<(), VectorSetValueError> {
1845        self.dense.validate().map_err(VectorSetValueError::Dense)?;
1846        if self.maximum_vectors == 0 || usize::from(self.maximum_vectors) > MAX_VECTOR_SET_DOCUMENT_VECTORS {
1847            return Err(VectorSetValueError::InvalidMaximumVectors(self.maximum_vectors));
1848        }
1849        let maximum_bytes = usize::from(self.maximum_vectors)
1850            .checked_mul(self.dense.encoded_bytes())
1851            .ok_or(VectorSetValueError::ByteLengthOverflow)?;
1852        if maximum_bytes > MAX_VECTOR_SET_BYTES_PER_FIELD {
1853            return Err(VectorSetValueError::FieldBytesExceeded(maximum_bytes));
1854        }
1855        Ok(())
1856    }
1857}
1858
1859impl VectorSetValue {
1860    pub fn decode(
1861        &self,
1862        definition: VectorSetDefinition,
1863        maximum_vectors: usize,
1864        maximum_bytes: usize,
1865    ) -> Result<DecodedVectorSet, VectorSetValueError> {
1866        definition.validate()?;
1867        let admitted_vectors = maximum_vectors
1868            .min(usize::from(definition.maximum_vectors))
1869            .min(MAX_VECTOR_SET_DOCUMENT_VECTORS);
1870        if self.vectors.is_empty() {
1871            return Err(VectorSetValueError::Empty);
1872        }
1873        if self.vectors.len() > admitted_vectors {
1874            return Err(VectorSetValueError::TooManyVectors(self.vectors.len()));
1875        }
1876        let native_bytes = self
1877            .vectors
1878            .len()
1879            .checked_mul(definition.dense.encoded_bytes())
1880            .ok_or(VectorSetValueError::ByteLengthOverflow)?;
1881        if native_bytes > maximum_bytes.min(MAX_VECTOR_SET_BYTES_PER_FIELD) {
1882            return Err(VectorSetValueError::FieldBytesExceeded(native_bytes));
1883        }
1884        let mut vectors = Vec::with_capacity(self.vectors.len());
1885        let mut native_vectors = Vec::with_capacity(self.vectors.len());
1886        for value in &self.vectors {
1887            vectors.push(value.decode(definition.dense).map_err(VectorSetValueError::Dense)?);
1888            native_vectors.push(
1889                value
1890                    .canonical_native_bytes(definition.dense)
1891                    .map_err(VectorSetValueError::Dense)?,
1892            );
1893        }
1894        Ok(DecodedVectorSet {
1895            vectors,
1896            native_vectors,
1897            native_bytes,
1898        })
1899    }
1900}
1901
1902fn deserialize_bounded_vector_set<'de, D>(deserializer: D) -> Result<Vec<DenseVectorValue>, D::Error>
1903where
1904    D: Deserializer<'de>,
1905{
1906    struct VectorSetVisitor;
1907
1908    impl<'de> serde::de::Visitor<'de> for VectorSetVisitor {
1909        type Value = Vec<DenseVectorValue>;
1910
1911        fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
1912            formatter.write_str("a bounded sequence of dense vector values")
1913        }
1914
1915        fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
1916        where
1917            A: serde::de::SeqAccess<'de>,
1918        {
1919            if sequence
1920                .size_hint()
1921                .is_some_and(|length| length > MAX_VECTOR_SET_DOCUMENT_VECTORS)
1922            {
1923                return Err(de::Error::custom(
1924                    "vector set exceeds the document vector-count ceiling",
1925                ));
1926            }
1927            let mut vectors = Vec::with_capacity(
1928                sequence
1929                    .size_hint()
1930                    .unwrap_or_default()
1931                    .min(MAX_VECTOR_SET_DOCUMENT_VECTORS),
1932            );
1933            while let Some(vector) = sequence.next_element::<DenseVectorValue>()? {
1934                if vectors.len() == MAX_VECTOR_SET_DOCUMENT_VECTORS {
1935                    return Err(de::Error::custom(
1936                        "vector set exceeds the document vector-count ceiling",
1937                    ));
1938                }
1939                vectors.push(vector);
1940            }
1941            Ok(vectors)
1942        }
1943    }
1944
1945    deserializer.deserialize_seq(VectorSetVisitor)
1946}
1947
1948#[derive(Clone, Debug, Eq, PartialEq)]
1949pub enum VectorSetValueError {
1950    Empty,
1951    InvalidMaximumVectors(u16),
1952    TooManyVectors(usize),
1953    FieldBytesExceeded(usize),
1954    ByteLengthOverflow,
1955    Dense(DenseVectorValueError),
1956}
1957
1958impl Display for VectorSetValueError {
1959    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
1960        match self {
1961            Self::Empty => formatter.write_str("vector set cannot be empty"),
1962            Self::InvalidMaximumVectors(value) => {
1963                write!(formatter, "vector-set maximum vector count {value} is invalid")
1964            }
1965            Self::TooManyVectors(value) => write!(formatter, "vector set contains {value} vectors"),
1966            Self::FieldBytesExceeded(value) => write!(formatter, "vector set uses {value} element bytes"),
1967            Self::ByteLengthOverflow => formatter.write_str("vector-set byte length overflowed"),
1968            Self::Dense(error) => write!(formatter, "vector-set member is invalid: {error}"),
1969        }
1970    }
1971}
1972
1973impl Error for VectorSetValueError {}
1974
1975#[derive(Clone, Debug, Eq, PartialEq)]
1976pub enum DenseVectorValueError {
1977    Empty,
1978    InvalidDimensions(u16),
1979    DimensionMismatch { expected: u16, observed: usize },
1980    InvalidQuantizationScale,
1981    MissingQuantizationScale,
1982    UnexpectedQuantizationScale,
1983    QuantizationScaleMismatch,
1984    ElementTypeMismatch,
1985    ByteLengthOverflow,
1986    FieldBytesExceeded(usize),
1987    NonFiniteElement(usize),
1988    F16Overflow(usize),
1989    I8ElementOutOfRange(usize),
1990    InvalidBase64Length { expected: usize, observed: usize },
1991    InvalidBase64,
1992    DecodedByteLength { expected: usize, observed: usize },
1993    ZeroNorm,
1994}
1995
1996impl Display for DenseVectorValueError {
1997    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
1998        match self {
1999            Self::Empty => formatter.write_str("dense vector value is empty"),
2000            Self::InvalidDimensions(value) => write!(formatter, "dense vector dimensions {value} are invalid"),
2001            Self::DimensionMismatch { expected, observed } => {
2002                write!(formatter, "dense vector has {observed} elements; expected {expected}")
2003            }
2004            Self::InvalidQuantizationScale => {
2005                formatter.write_str("dense i8 quantization scale must be positive and finite")
2006            }
2007            Self::MissingQuantizationScale => {
2008                formatter.write_str("dense i8 value requires quantization scale metadata")
2009            }
2010            Self::UnexpectedQuantizationScale => {
2011                formatter.write_str("dense f32/f16 value cannot carry quantization metadata")
2012            }
2013            Self::QuantizationScaleMismatch => {
2014                formatter.write_str("dense i8 value quantization scale differs from its mapping")
2015            }
2016            Self::ElementTypeMismatch => {
2017                formatter.write_str("dense binary value element type differs from its mapping")
2018            }
2019            Self::ByteLengthOverflow => formatter.write_str("dense vector byte length overflowed"),
2020            Self::FieldBytesExceeded(value) => {
2021                write!(formatter, "dense vector uses {value} bytes above its field ceiling")
2022            }
2023            Self::NonFiniteElement(ordinal) => write!(formatter, "dense vector element {ordinal} is non-finite"),
2024            Self::F16Overflow(ordinal) => write!(formatter, "dense f16 element {ordinal} overflows binary16"),
2025            Self::I8ElementOutOfRange(ordinal) => {
2026                write!(
2027                    formatter,
2028                    "dense i8 element {ordinal} is not an exact integer in -128..=127"
2029                )
2030            }
2031            Self::InvalidBase64Length { expected, observed } => {
2032                write!(
2033                    formatter,
2034                    "dense binary base64 has {observed} bytes; expected {expected}"
2035                )
2036            }
2037            Self::InvalidBase64 => formatter.write_str("dense binary value is not canonical base64"),
2038            Self::DecodedByteLength { expected, observed } => {
2039                write!(
2040                    formatter,
2041                    "dense binary value has {observed} decoded bytes; expected {expected}"
2042                )
2043            }
2044            Self::ZeroNorm => formatter.write_str("dense cosine vector has zero norm"),
2045        }
2046    }
2047}
2048
2049impl Error for DenseVectorValueError {}
2050
2051fn deserialize_bounded_dense_numbers<'de, D>(deserializer: D) -> Result<Vec<f64>, D::Error>
2052where
2053    D: Deserializer<'de>,
2054{
2055    struct DenseNumbersVisitor;
2056
2057    impl<'de> Visitor<'de> for DenseNumbersVisitor {
2058        type Value = Vec<f64>;
2059
2060        fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
2061            write!(formatter, "at most {MAX_VECTOR_DIMENSIONS} dense vector numbers")
2062        }
2063
2064        fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
2065        where
2066            A: SeqAccess<'de>,
2067        {
2068            if sequence
2069                .size_hint()
2070                .is_some_and(|length| length > usize::from(MAX_VECTOR_DIMENSIONS))
2071            {
2072                return Err(de::Error::custom("dense vector exceeds the element ceiling"));
2073            }
2074            let mut values = Vec::with_capacity(
2075                sequence
2076                    .size_hint()
2077                    .unwrap_or_default()
2078                    .min(usize::from(MAX_VECTOR_DIMENSIONS)),
2079            );
2080            while let Some(value) = sequence.next_element::<f64>()? {
2081                if values.len() == usize::from(MAX_VECTOR_DIMENSIONS) {
2082                    return Err(de::Error::custom("dense vector exceeds the element ceiling"));
2083                }
2084                values.push(value);
2085            }
2086            Ok(values)
2087        }
2088    }
2089
2090    deserializer.deserialize_seq(DenseNumbersVisitor)
2091}
2092
2093pub fn decode_dense_numeric_value(
2094    definition: DenseVectorDefinition,
2095    values: &[f64],
2096) -> Result<Vec<f32>, DenseVectorValueError> {
2097    let bytes = encode_dense_numeric_native_bytes(definition, values)?;
2098    decode_dense_native_bytes(definition, &bytes)
2099}
2100
2101pub fn decode_dense_binary_value(
2102    definition: DenseVectorDefinition,
2103    value: &DenseVectorBinaryValue,
2104) -> Result<Vec<f32>, DenseVectorValueError> {
2105    let bytes = decode_dense_binary_native_bytes(definition, value)?;
2106    decode_dense_native_bytes(definition, &bytes)
2107}
2108
2109fn decode_dense_binary_native_bytes(
2110    definition: DenseVectorDefinition,
2111    value: &DenseVectorBinaryValue,
2112) -> Result<Vec<u8>, DenseVectorValueError> {
2113    definition.validate()?;
2114    if value.element_type != definition.element_type {
2115        return Err(DenseVectorValueError::ElementTypeMismatch);
2116    }
2117    if value.quantization_scale != definition.quantization_scale {
2118        return Err(DenseVectorValueError::QuantizationScaleMismatch);
2119    }
2120    let expected_bytes = definition.encoded_bytes();
2121    let expected_base64 = expected_bytes.div_ceil(3) * 4;
2122    if value.data_base64.len() != expected_base64 {
2123        return Err(DenseVectorValueError::InvalidBase64Length {
2124            expected: expected_base64,
2125            observed: value.data_base64.len(),
2126        });
2127    }
2128    let bytes = BASE64_STANDARD
2129        .decode(value.data_base64.as_bytes())
2130        .map_err(|_| DenseVectorValueError::InvalidBase64)?;
2131    if bytes.len() != expected_bytes {
2132        return Err(DenseVectorValueError::DecodedByteLength {
2133            expected: expected_bytes,
2134            observed: bytes.len(),
2135        });
2136    }
2137    if BASE64_STANDARD.encode(&bytes) != value.data_base64 {
2138        return Err(DenseVectorValueError::InvalidBase64);
2139    }
2140    Ok(bytes)
2141}
2142
2143fn encode_dense_numeric_native_bytes(
2144    definition: DenseVectorDefinition,
2145    values: &[f64],
2146) -> Result<Vec<u8>, DenseVectorValueError> {
2147    definition.validate()?;
2148    if values.len() != usize::from(definition.dimensions) {
2149        return Err(DenseVectorValueError::DimensionMismatch {
2150            expected: definition.dimensions,
2151            observed: values.len(),
2152        });
2153    }
2154    let mut bytes = Vec::with_capacity(definition.encoded_bytes());
2155    for (ordinal, value) in values.iter().copied().enumerate() {
2156        if !value.is_finite() {
2157            return Err(DenseVectorValueError::NonFiniteElement(ordinal));
2158        }
2159        match definition.element_type {
2160            DenseVectorElementType::F32 => {
2161                let value = canonical_vector_zero(value as f32);
2162                if !value.is_finite() {
2163                    return Err(DenseVectorValueError::NonFiniteElement(ordinal));
2164                }
2165                bytes.extend_from_slice(&value.to_bits().to_le_bytes());
2166            }
2167            DenseVectorElementType::F16 => {
2168                let value = value as f32;
2169                if !value.is_finite() {
2170                    return Err(DenseVectorValueError::F16Overflow(ordinal));
2171                }
2172                let bits = f32_to_f16_rne(value).ok_or(DenseVectorValueError::F16Overflow(ordinal))?;
2173                bytes.extend_from_slice(&bits.to_le_bytes());
2174            }
2175            DenseVectorElementType::I8 => {
2176                if value.fract() != 0.0 || !(-128.0..=127.0).contains(&value) {
2177                    return Err(DenseVectorValueError::I8ElementOutOfRange(ordinal));
2178                }
2179                bytes.push(value as i8 as u8);
2180            }
2181        }
2182    }
2183    Ok(bytes)
2184}
2185
2186/// Decodes one exact native element sequence and applies the mapping's metric
2187/// normalization. Durable bundle readers use this instead of widening and
2188/// then re-encoding a caller value.
2189pub fn decode_dense_native_bytes(
2190    definition: DenseVectorDefinition,
2191    bytes: &[u8],
2192) -> Result<Vec<f32>, DenseVectorValueError> {
2193    definition.validate()?;
2194    let expected = definition.encoded_bytes();
2195    if bytes.len() != expected {
2196        return Err(DenseVectorValueError::DecodedByteLength {
2197            expected,
2198            observed: bytes.len(),
2199        });
2200    }
2201    let mut decoded = Vec::with_capacity(usize::from(definition.dimensions));
2202    match definition.element_type {
2203        DenseVectorElementType::F32 => {
2204            for (ordinal, chunk) in bytes.as_chunks::<4>().0.iter().enumerate() {
2205                let value = f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
2206                if !value.is_finite() {
2207                    return Err(DenseVectorValueError::NonFiniteElement(ordinal));
2208                }
2209                decoded.push(canonical_vector_zero(value));
2210            }
2211        }
2212        DenseVectorElementType::F16 => {
2213            for (ordinal, chunk) in bytes.as_chunks::<2>().0.iter().enumerate() {
2214                let value = f16_to_f32(u16::from_le_bytes([chunk[0], chunk[1]]));
2215                if !value.is_finite() {
2216                    return Err(DenseVectorValueError::NonFiniteElement(ordinal));
2217                }
2218                decoded.push(canonical_vector_zero(value));
2219            }
2220        }
2221        DenseVectorElementType::I8 => {
2222            let scale = definition
2223                .quantization_scale
2224                .ok_or(DenseVectorValueError::MissingQuantizationScale)?
2225                .get();
2226            decoded.extend(
2227                bytes
2228                    .iter()
2229                    .map(|value| canonical_vector_zero((*value as i8 as f32) * scale)),
2230            );
2231        }
2232    }
2233    normalize_dense_for_metric(definition.metric, &mut decoded)?;
2234    Ok(decoded)
2235}
2236
2237fn normalize_dense_for_metric(metric: VectorMetric, values: &mut [f32]) -> Result<(), DenseVectorValueError> {
2238    if metric != VectorMetric::Cosine {
2239        return Ok(());
2240    }
2241    let squared_norm = values.iter().try_fold(0.0_f64, |sum, value| {
2242        let next = sum + f64::from(*value) * f64::from(*value);
2243        next.is_finite().then_some(next)
2244    });
2245    let norm = squared_norm
2246        .map(f64::sqrt)
2247        .filter(|value| *value > 0.0 && value.is_finite())
2248        .ok_or(DenseVectorValueError::ZeroNorm)?;
2249    for value in values {
2250        *value = canonical_vector_zero((f64::from(*value) / norm) as f32);
2251        if !value.is_finite() {
2252            return Err(DenseVectorValueError::NonFiniteElement(0));
2253        }
2254    }
2255    Ok(())
2256}
2257
2258const fn canonical_vector_zero(value: f32) -> f32 {
2259    if value == 0.0 { 0.0 } else { value }
2260}
2261
2262fn f32_to_f16_rne(value: f32) -> Option<u16> {
2263    if !value.is_finite() {
2264        return None;
2265    }
2266    let bits = canonical_vector_zero(value).to_bits();
2267    let sign = ((bits >> 16) & 0x8000) as u16;
2268    let exponent = ((bits >> 23) & 0xff) as i32 - 127;
2269    let fraction = bits & 0x7f_ffff;
2270    if exponent > 15 {
2271        return None;
2272    }
2273    if exponent >= -14 {
2274        let mut half_exponent = (exponent + 15) as u16;
2275        let mut half_fraction = (fraction >> 13) as u16;
2276        let remainder = fraction & 0x1fff;
2277        if remainder > 0x1000 || remainder == 0x1000 && half_fraction & 1 == 1 {
2278            half_fraction += 1;
2279            if half_fraction == 0x400 {
2280                half_fraction = 0;
2281                half_exponent += 1;
2282                if half_exponent >= 0x1f {
2283                    return None;
2284                }
2285            }
2286        }
2287        return Some(sign | (half_exponent << 10) | half_fraction);
2288    }
2289    let significand = u64::from((1_u32 << 23) | fraction);
2290    let shift = u32::try_from(-exponent - 1).ok()?;
2291    let rounded = round_shift_right_ties_even(significand, shift);
2292    if rounded >= 0x400 {
2293        Some(sign | 0x0400)
2294    } else {
2295        Some(sign | rounded as u16)
2296    }
2297}
2298
2299fn round_shift_right_ties_even(value: u64, shift: u32) -> u64 {
2300    if shift == 0 {
2301        return value;
2302    }
2303    if shift >= 64 {
2304        return 0;
2305    }
2306    let quotient = value >> shift;
2307    let remainder_mask = (1_u64 << shift) - 1;
2308    let remainder = value & remainder_mask;
2309    let halfway = 1_u64 << (shift - 1);
2310    quotient + u64::from(remainder > halfway || remainder == halfway && quotient & 1 == 1)
2311}
2312
2313fn f16_to_f32(bits: u16) -> f32 {
2314    let sign = u32::from(bits & 0x8000) << 16;
2315    let exponent = (bits >> 10) & 0x1f;
2316    let fraction = bits & 0x03ff;
2317    match (exponent, fraction) {
2318        (0, 0) => f32::from_bits(sign),
2319        (0, _) => {
2320            let value = f32::from(fraction) * 2.0_f32.powi(-24);
2321            if sign == 0 { value } else { -value }
2322        }
2323        (0x1f, _) => f32::from_bits(sign | 0x7f80_0000 | (u32::from(fraction) << 13)),
2324        _ => {
2325            let f32_exponent = u32::from(exponent) + 112;
2326            f32::from_bits(sign | (f32_exponent << 23) | (u32::from(fraction) << 13))
2327        }
2328    }
2329}
2330
2331/// Maximum UTF-8 bytes admitted by one mapped field path.
2332pub const MAX_MAPPING_FIELD_NAME_BYTES: usize = 1_024;
2333
2334/// Normalized mapping semantics for one schema generation.
2335///
2336/// Durable catalog records own the canonical bytes for this exact type.
2337/// Execution crates compile it but do not define a second mapping model.
2338#[derive(Clone, Debug, Eq, PartialEq)]
2339pub struct Mapping {
2340    pub format_version: u16,
2341    pub schema_generation: SchemaGeneration,
2342    pub mode: IndexMode,
2343    pub fields: BTreeMap<String, FieldMapping>,
2344    /// Semantics owned by the sole current mapping format.
2345    pub extensions: MappingExtensions,
2346}
2347
2348impl Mapping {
2349    #[must_use]
2350    pub fn empty(mode: IndexMode) -> Self {
2351        Self {
2352            format_version: MAPPING_FORMAT_VERSION,
2353            schema_generation: SchemaGeneration::new(1),
2354            mode,
2355            fields: BTreeMap::new(),
2356            extensions: crate::MappingExtensions::default(),
2357        }
2358    }
2359
2360    /// Completes format-4 metadata whose defaults are intrinsic to the native
2361    /// mapping model. Adapter-specific policy defaults are never applied here.
2362    #[must_use]
2363    pub fn normalized(mut self) -> Self {
2364        if self.format_version == MAPPING_FORMAT_VERSION {
2365            if self.extensions.analyzer_identities.is_empty() {
2366                self.extensions.analyzer_identities = self
2367                    .fields
2368                    .iter()
2369                    .filter(|(_, definition)| definition.field_type == FieldType::Text)
2370                    .map(|(field, _)| AnalyzerIdentity {
2371                        field: field.clone(),
2372                        index_analyzer: AnalyzerName::Standard,
2373                        search_analyzer: AnalyzerName::Standard,
2374                    })
2375                    .collect();
2376            }
2377            if self.extensions.keyword_limits.is_empty() {
2378                self.extensions.keyword_limits = self
2379                    .fields
2380                    .iter()
2381                    .filter(|(_, definition)| definition.field_type == FieldType::Keyword)
2382                    .map(|(field, _)| KeywordFieldPolicy {
2383                        field: field.clone(),
2384                        max_bytes: DEFAULT_KEYWORD_IGNORE_ABOVE,
2385                        normalizer: KeywordNormalizer::Keyword,
2386                    })
2387                    .collect();
2388            }
2389            if self.extensions.nested_paths.is_empty() {
2390                self.extensions.nested_paths = self
2391                    .fields
2392                    .iter()
2393                    .filter(|(_, definition)| definition.field_type == FieldType::Nested)
2394                    .map(|(field, _)| field.clone())
2395                    .collect();
2396            }
2397        }
2398        self
2399    }
2400
2401    /// Decodes exact canonical catalog/public bytes after a minimal,
2402    /// duplicate-safe current-format probe.
2403    pub fn decode_canonical(bytes: &[u8]) -> Result<Self, MappingCodecError> {
2404        if bytes.len() > MAX_MAPPING_BYTES {
2405            return Err(MappingCodecError::TooLarge(bytes.len()));
2406        }
2407        let mut probe = serde_json::Deserializer::from_slice(bytes);
2408        let version = MappingVersionProbe::deserialize(&mut probe).map_err(|error| {
2409            let message = error.to_string();
2410            if message.starts_with("duplicate mapping key '") {
2411                MappingCodecError::DuplicateKey(message)
2412            } else {
2413                MappingCodecError::Malformed(message)
2414            }
2415        })?;
2416        probe
2417            .end()
2418            .map_err(|error| MappingCodecError::Malformed(error.to_string()))?;
2419        if version != MAPPING_FORMAT_VERSION {
2420            return Err(MappingCodecError::UnsupportedVersion(version));
2421        }
2422        let mapping: Self =
2423            serde_json::from_slice(bytes).map_err(|error| MappingCodecError::Malformed(error.to_string()))?;
2424        mapping.validate().map_err(MappingCodecError::Invalid)?;
2425        let canonical =
2426            serde_json::to_vec(&mapping).map_err(|error| MappingCodecError::Malformed(error.to_string()))?;
2427        if canonical != bytes {
2428            return Err(MappingCodecError::NonCanonical);
2429        }
2430        Ok(mapping)
2431    }
2432
2433    pub fn encode_canonical(&self) -> Result<Vec<u8>, MappingCodecError> {
2434        self.validate().map_err(MappingCodecError::Invalid)?;
2435        serde_json::to_vec(self).map_err(|error| MappingCodecError::Malformed(error.to_string()))
2436    }
2437
2438    /// Validates the dependency-free semantic mapping contract.
2439    pub fn validate(&self) -> Result<(), MappingValidationError> {
2440        if self.format_version != MAPPING_FORMAT_VERSION {
2441            return Err(MappingValidationError::UnsupportedVersion(self.format_version));
2442        }
2443        let normalized = self.clone().normalized();
2444        let mapping = &normalized;
2445        if mapping.schema_generation.get() == 0 {
2446            return Err(MappingValidationError::ZeroSchemaGeneration);
2447        }
2448        if mapping.fields.len() > MAX_MAPPING_FIELDS {
2449            return Err(MappingValidationError::TooManyFields(mapping.fields.len()));
2450        }
2451        let mut vector_field_ids = std::collections::BTreeSet::new();
2452        let mut vector_fields = 0_usize;
2453        for (name, definition) in &mapping.fields {
2454            validate_mapping_field_name(name)?;
2455            for (separator, _) in name.match_indices('.') {
2456                let ancestor = &name[..separator];
2457                if mapping.fields.contains_key(ancestor)
2458                    && !mapping.extensions.nested_paths.iter().any(|path| path == ancestor)
2459                    && mapping
2460                        .extensions
2461                        .multi_field_projections
2462                        .iter()
2463                        .find(|projection| projection.child == *name)
2464                        .is_none_or(|projection| projection.parent != ancestor)
2465                {
2466                    return Err(MappingValidationError::ConflictingFieldPaths {
2467                        ancestor: ancestor.to_owned(),
2468                        descendant: name.clone(),
2469                    });
2470                }
2471            }
2472            match definition.field_type {
2473                FieldType::Vector {
2474                    format_version,
2475                    field_id,
2476                    dimensions,
2477                    metric,
2478                    element_type,
2479                    quantization_scale,
2480                } => {
2481                    vector_fields = vector_fields.saturating_add(1);
2482                    if format_version != VECTOR_MAPPING_FORMAT_VERSION {
2483                        return Err(MappingValidationError::UnsupportedVectorVersion(format_version));
2484                    }
2485                    if dimensions == 0 || dimensions > MAX_VECTOR_DIMENSIONS {
2486                        return Err(MappingValidationError::InvalidVectorDimensions {
2487                            field: name.clone(),
2488                            dimensions,
2489                        });
2490                    }
2491                    DenseVectorDefinition {
2492                        dimensions,
2493                        element_type,
2494                        metric,
2495                        quantization_scale,
2496                    }
2497                    .validate()
2498                    .map_err(|_| MappingValidationError::InvalidVectorElementContract(name.clone()))?;
2499                    if definition.stored {
2500                        return Err(MappingValidationError::StoredVectorField(name.clone()));
2501                    }
2502                    if !vector_field_ids.insert(field_id) {
2503                        return Err(MappingValidationError::DuplicateVectorFieldId(field_id));
2504                    }
2505                }
2506                FieldType::SparseVector {
2507                    format_version,
2508                    field_id,
2509                    maximum_dimension,
2510                    weight_type,
2511                } => {
2512                    vector_fields = vector_fields.saturating_add(1);
2513                    if format_version != SPARSE_VECTOR_MAPPING_FORMAT_VERSION {
2514                        return Err(MappingValidationError::UnsupportedSparseVectorVersion(format_version));
2515                    }
2516                    SparseVectorDefinition {
2517                        maximum_dimension,
2518                        weight_type,
2519                    }
2520                    .validate()
2521                    .map_err(|_| MappingValidationError::InvalidSparseVectorContract(name.clone()))?;
2522                    if definition.stored {
2523                        return Err(MappingValidationError::StoredVectorField(name.clone()));
2524                    }
2525                    if !vector_field_ids.insert(field_id) {
2526                        return Err(MappingValidationError::DuplicateVectorFieldId(field_id));
2527                    }
2528                }
2529                FieldType::VectorSet {
2530                    format_version,
2531                    field_id,
2532                    dimensions,
2533                    metric,
2534                    element_type,
2535                    quantization_scale,
2536                    maximum_vectors,
2537                } => {
2538                    vector_fields = vector_fields.saturating_add(1);
2539                    if format_version != VECTOR_SET_MAPPING_FORMAT_VERSION {
2540                        return Err(MappingValidationError::UnsupportedVectorSetVersion(format_version));
2541                    }
2542                    VectorSetDefinition {
2543                        dense: DenseVectorDefinition {
2544                            dimensions,
2545                            element_type,
2546                            metric,
2547                            quantization_scale,
2548                        },
2549                        maximum_vectors,
2550                    }
2551                    .validate()
2552                    .map_err(|_| MappingValidationError::InvalidVectorSetContract(name.clone()))?;
2553                    if definition.stored {
2554                        return Err(MappingValidationError::StoredVectorField(name.clone()));
2555                    }
2556                    if !vector_field_ids.insert(field_id) {
2557                        return Err(MappingValidationError::DuplicateVectorFieldId(field_id));
2558                    }
2559                }
2560                _ => {}
2561            }
2562            definition.validate(name)?;
2563        }
2564        if vector_fields > MAX_VECTOR_MAPPING_FIELDS {
2565            return Err(MappingValidationError::TooManyVectorFields(vector_fields));
2566        }
2567        mapping.extensions.validate(mapping)?;
2568        mapping
2569            .extensions
2570            .record_model
2571            .validate(mapping.mode)
2572            .map_err(|error| MappingValidationError::InvalidRecordModel(error.to_string()))?;
2573        Ok(())
2574    }
2575
2576    /// Validates a mapping admitted for a new index or successor generation.
2577    pub fn validate_current(&self) -> Result<(), MappingValidationError> {
2578        self.validate()
2579    }
2580
2581    /// Returns whether `self` is an additive successor of `predecessor`.
2582    ///
2583    /// This is the shared semantic check used before entering the catalog
2584    /// CAS. Existing field definitions and every format-4 extension identity
2585    /// are immutable; a successor may only append new definitions and their
2586    /// matching metadata.
2587    #[must_use]
2588    pub fn is_additive_successor_of(&self, predecessor: &Self) -> bool {
2589        let predecessor = predecessor.clone().normalized();
2590        let successor = self.clone().normalized();
2591        predecessor.format_version == MAPPING_FORMAT_VERSION
2592            && successor.format_version == MAPPING_FORMAT_VERSION
2593            && predecessor.mode == successor.mode
2594            && predecessor.extensions.record_model == successor.extensions.record_model
2595            && predecessor.extensions.dynamic_policy == successor.extensions.dynamic_policy
2596            && predecessor
2597                .fields
2598                .iter()
2599                .all(|(path, field)| successor.fields.get(path) == Some(field))
2600            && predecessor
2601                .extensions
2602                .multi_field_projections
2603                .iter()
2604                .all(|entry| successor.extensions.multi_field_projections.contains(entry))
2605            && predecessor
2606                .extensions
2607                .analyzer_identities
2608                .iter()
2609                .all(|entry| successor.extensions.analyzer_identities.contains(entry))
2610            && predecessor
2611                .extensions
2612                .keyword_limits
2613                .iter()
2614                .all(|entry| successor.extensions.keyword_limits.contains(entry))
2615            && predecessor
2616                .extensions
2617                .nested_paths
2618                .iter()
2619                .all(|entry| successor.extensions.nested_paths.contains(entry))
2620            && successor
2621                .extensions
2622                .nested_paths
2623                .iter()
2624                .filter(|path| !predecessor.extensions.nested_paths.contains(path))
2625                .all(|path| {
2626                    predecessor.fields.keys().all(|field| {
2627                        field != path
2628                            && !field
2629                                .strip_prefix(path.as_str())
2630                                .is_some_and(|suffix| suffix.starts_with('.'))
2631                    })
2632                })
2633    }
2634}
2635
2636struct MappingVersionProbe;
2637
2638impl MappingVersionProbe {
2639    fn deserialize<'de, D>(deserializer: D) -> Result<u16, D::Error>
2640    where
2641        D: serde::Deserializer<'de>,
2642    {
2643        struct VersionVisitor;
2644
2645        impl<'de> Visitor<'de> for VersionVisitor {
2646            type Value = u16;
2647
2648            fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
2649                formatter.write_str("a mapping JSON object")
2650            }
2651
2652            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
2653            where
2654                A: MapAccess<'de>,
2655            {
2656                let mut keys = std::collections::BTreeSet::new();
2657                let mut version = None;
2658                while let Some(key) = map.next_key::<String>()? {
2659                    if !keys.insert(key.clone()) {
2660                        return Err(de::Error::custom(format!("duplicate mapping key '{key}'")));
2661                    }
2662                    let raw = map.next_value::<Box<serde_json::value::RawValue>>()?;
2663                    if key == "format_version" {
2664                        version = Some(
2665                            serde_json::from_str::<u16>(raw.get())
2666                                .map_err(|_| de::Error::custom("mapping format_version is invalid"))?,
2667                        );
2668                    }
2669                }
2670                version.ok_or_else(|| de::Error::custom("mapping format_version is missing"))
2671            }
2672        }
2673
2674        deserializer.deserialize_map(VersionVisitor)
2675    }
2676}
2677
2678impl Serialize for Mapping {
2679    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2680    where
2681        S: serde::Serializer,
2682    {
2683        use serde::ser::Error as _;
2684
2685        if self.format_version != MAPPING_FORMAT_VERSION {
2686            return Err(S::Error::custom(format!(
2687                "unsupported mapping format version {}",
2688                self.format_version
2689            )));
2690        }
2691        let normalized = self.clone().normalized();
2692        MappingWireRef {
2693            format_version: normalized.format_version,
2694            generation: normalized.schema_generation,
2695            mode: normalized.mode,
2696            record_model: &normalized.extensions.record_model,
2697            dynamic_policy: &normalized.extensions.dynamic_policy,
2698            fields: &normalized.fields,
2699            multi_field_projections: &normalized.extensions.multi_field_projections,
2700            analyzer_identities: &normalized.extensions.analyzer_identities,
2701            keyword_limits: &normalized.extensions.keyword_limits,
2702            nested_paths: &normalized.extensions.nested_paths,
2703        }
2704        .serialize(serializer)
2705    }
2706}
2707
2708impl<'de> Deserialize<'de> for Mapping {
2709    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2710    where
2711        D: serde::Deserializer<'de>,
2712    {
2713        use serde::de::Error as _;
2714
2715        let value = serde_json::Value::deserialize(deserializer)?;
2716        let version = value
2717            .get("format_version")
2718            .and_then(serde_json::Value::as_u64)
2719            .and_then(|value| u16::try_from(value).ok())
2720            .ok_or_else(|| D::Error::custom("mapping format_version is missing or invalid"))?;
2721        if version != MAPPING_FORMAT_VERSION {
2722            return Err(D::Error::custom(format!(
2723                "unsupported mapping format version {version}"
2724            )));
2725        }
2726        let wire: MappingWire = serde_json::from_value(value).map_err(D::Error::custom)?;
2727        Ok(Self {
2728            format_version: wire.format_version,
2729            schema_generation: wire.generation,
2730            mode: wire.mode,
2731            fields: wire.fields,
2732            extensions: MappingExtensions {
2733                record_model: wire.record_model,
2734                dynamic_policy: wire.dynamic_policy,
2735                multi_field_projections: wire.multi_field_projections,
2736                analyzer_identities: wire.analyzer_identities,
2737                keyword_limits: wire.keyword_limits,
2738                nested_paths: wire.nested_paths,
2739            },
2740        })
2741    }
2742}
2743
2744#[derive(Serialize)]
2745struct MappingWireRef<'a> {
2746    format_version: u16,
2747    generation: SchemaGeneration,
2748    mode: IndexMode,
2749    record_model: &'a IndexRecordModel,
2750    dynamic_policy: &'a DynamicMappingPolicy,
2751    fields: &'a BTreeMap<String, FieldMapping>,
2752    multi_field_projections: &'a Vec<MultiFieldProjection>,
2753    analyzer_identities: &'a Vec<AnalyzerIdentity>,
2754    keyword_limits: &'a Vec<KeywordFieldPolicy>,
2755    nested_paths: &'a Vec<String>,
2756}
2757
2758#[derive(Deserialize)]
2759#[serde(deny_unknown_fields)]
2760struct MappingWire {
2761    format_version: u16,
2762    generation: SchemaGeneration,
2763    mode: IndexMode,
2764    record_model: IndexRecordModel,
2765    dynamic_policy: DynamicMappingPolicy,
2766    fields: BTreeMap<String, FieldMapping>,
2767    multi_field_projections: Vec<MultiFieldProjection>,
2768    analyzer_identities: Vec<AnalyzerIdentity>,
2769    keyword_limits: Vec<KeywordFieldPolicy>,
2770    nested_paths: Vec<String>,
2771}
2772
2773/// Current mapping semantics kept separate from the base field definitions.
2774#[derive(Clone, Debug, Default, Eq, PartialEq)]
2775pub struct MappingExtensions {
2776    pub record_model: IndexRecordModel,
2777    pub dynamic_policy: DynamicMappingPolicy,
2778    pub multi_field_projections: Vec<MultiFieldProjection>,
2779    pub analyzer_identities: Vec<AnalyzerIdentity>,
2780    pub keyword_limits: Vec<KeywordFieldPolicy>,
2781    pub nested_paths: Vec<String>,
2782}
2783
2784impl MappingExtensions {
2785    fn validate(&self, mapping: &Mapping) -> Result<(), MappingValidationError> {
2786        if self.multi_field_projections.len() > MAX_MAPPING_FIELDS
2787            || self.analyzer_identities.len() > MAX_MAPPING_FIELDS
2788            || self.keyword_limits.len() > MAX_MAPPING_FIELDS
2789        {
2790            return Err(MappingValidationError::TooManyMappingMetadata);
2791        }
2792        if self.nested_paths.len() > MAX_NESTED_MAPPING_PATHS {
2793            return Err(MappingValidationError::TooManyNestedPaths(self.nested_paths.len()));
2794        }
2795        require_sorted_unique(
2796            self.multi_field_projections
2797                .iter()
2798                .map(|projection| projection.child.as_str()),
2799            "multi_field_projections",
2800        )?;
2801        require_sorted_unique(
2802            self.analyzer_identities.iter().map(|identity| identity.field.as_str()),
2803            "analyzer_identities",
2804        )?;
2805        require_sorted_unique(
2806            self.keyword_limits.iter().map(|policy| policy.field.as_str()),
2807            "keyword_limits",
2808        )?;
2809        require_sorted_unique(self.nested_paths.iter().map(String::as_str), "nested_paths")?;
2810        let nested_paths = self
2811            .nested_paths
2812            .iter()
2813            .map(String::as_str)
2814            .collect::<std::collections::BTreeSet<_>>();
2815        for child in &self.nested_paths {
2816            for (separator, _) in child.match_indices('.') {
2817                let parent = &child[..separator];
2818                if nested_paths.contains(parent) {
2819                    return Err(MappingValidationError::NestedPathOverlap {
2820                        parent: parent.to_owned(),
2821                        child: child.clone(),
2822                    });
2823                }
2824            }
2825        }
2826        for projection in &self.multi_field_projections {
2827            validate_mapping_field_name(&projection.child)?;
2828            validate_mapping_field_name(&projection.parent)?;
2829            let Some(child) = mapping.fields.get(&projection.child) else {
2830                return Err(MappingValidationError::MissingMultiField(projection.child.clone()));
2831            };
2832            let Some(parent) = mapping.fields.get(&projection.parent) else {
2833                return Err(MappingValidationError::MissingMultiFieldSource {
2834                    field: projection.child.clone(),
2835                    source: projection.parent.clone(),
2836                });
2837            };
2838            if !projection
2839                .child
2840                .strip_prefix(&projection.parent)
2841                .is_some_and(|suffix| suffix.starts_with('.') && suffix.len() > 1)
2842            {
2843                return Err(MappingValidationError::InvalidMultiFieldPath(projection.child.clone()));
2844            }
2845            if matches!(
2846                child.field_type,
2847                FieldType::Vector { .. }
2848                    | FieldType::SparseVector { .. }
2849                    | FieldType::VectorSet { .. }
2850                    | FieldType::Nested
2851            ) || matches!(
2852                parent.field_type,
2853                FieldType::Vector { .. }
2854                    | FieldType::SparseVector { .. }
2855                    | FieldType::VectorSet { .. }
2856                    | FieldType::Nested
2857            ) {
2858                return Err(MappingValidationError::InvalidMultiFieldEndpoint(
2859                    projection.child.clone(),
2860                ));
2861            }
2862            if !multi_field_types_are_compatible(parent.field_type, child.field_type) {
2863                return Err(MappingValidationError::InvalidMultiFieldEndpoint(
2864                    projection.child.clone(),
2865                ));
2866            }
2867        }
2868        for identity in &self.analyzer_identities {
2869            let Some(field) = mapping.fields.get(&identity.field) else {
2870                return Err(MappingValidationError::UnknownFieldMetadata(identity.field.clone()));
2871            };
2872            if field.field_type != FieldType::Text {
2873                return Err(MappingValidationError::AnalyzerOnNonTextField(identity.field.clone()));
2874            }
2875        }
2876        for policy in &self.keyword_limits {
2877            let Some(field) = mapping.fields.get(&policy.field) else {
2878                return Err(MappingValidationError::UnknownFieldMetadata(policy.field.clone()));
2879            };
2880            if field.field_type != FieldType::Keyword {
2881                return Err(MappingValidationError::KeywordPolicyOnNonKeywordField(
2882                    policy.field.clone(),
2883                ));
2884            }
2885            policy.validate()?;
2886            if let Some(MappingScalar::Keyword(value)) = &field.null_value
2887                && value.len() > policy.max_bytes as usize
2888            {
2889                return Err(MappingValidationError::InvalidKeywordNullValue(policy.field.clone()));
2890            }
2891        }
2892        for path in &self.nested_paths {
2893            if mapping
2894                .fields
2895                .get(path)
2896                .is_none_or(|field| field.field_type != FieldType::Nested)
2897            {
2898                return Err(MappingValidationError::InvalidNestedPath(path.clone()));
2899            }
2900            if path.split('.').count() > MAX_NESTED_MAPPING_DEPTH {
2901                return Err(MappingValidationError::NestedPathTooDeep(path.clone()));
2902            }
2903        }
2904        let text_fields = mapping
2905            .fields
2906            .iter()
2907            .filter_map(|(path, field)| (field.field_type == FieldType::Text).then_some(path.as_str()))
2908            .collect::<std::collections::BTreeSet<_>>();
2909        let analyzed_fields = self
2910            .analyzer_identities
2911            .iter()
2912            .map(|identity| identity.field.as_str())
2913            .collect::<std::collections::BTreeSet<_>>();
2914        if text_fields != analyzed_fields {
2915            return Err(MappingValidationError::IncompleteAnalyzerCoverage);
2916        }
2917        let keyword_fields = mapping
2918            .fields
2919            .iter()
2920            .filter_map(|(path, field)| (field.field_type == FieldType::Keyword).then_some(path.as_str()))
2921            .collect::<std::collections::BTreeSet<_>>();
2922        let limited_fields = self
2923            .keyword_limits
2924            .iter()
2925            .map(|policy| policy.field.as_str())
2926            .collect::<std::collections::BTreeSet<_>>();
2927        if keyword_fields != limited_fields {
2928            return Err(MappingValidationError::IncompleteKeywordCoverage);
2929        }
2930        let declared_nested = mapping
2931            .fields
2932            .iter()
2933            .filter_map(|(path, field)| (field.field_type == FieldType::Nested).then_some(path.as_str()))
2934            .collect::<std::collections::BTreeSet<_>>();
2935        let listed_nested = self
2936            .nested_paths
2937            .iter()
2938            .map(String::as_str)
2939            .collect::<std::collections::BTreeSet<_>>();
2940        if declared_nested != listed_nested {
2941            return Err(MappingValidationError::IncompleteNestedCoverage);
2942        }
2943        Ok(())
2944    }
2945}
2946
2947const fn multi_field_types_are_compatible(parent: FieldType, child: FieldType) -> bool {
2948    matches!(
2949        (parent, child),
2950        (
2951            FieldType::Text | FieldType::Keyword,
2952            FieldType::Text | FieldType::Keyword
2953        ) | (
2954            FieldType::U64 | FieldType::Date,
2955            FieldType::U64 | FieldType::Date | FieldType::F64
2956        ) | (FieldType::I64, FieldType::I64 | FieldType::F64)
2957            | (FieldType::F64, FieldType::F64)
2958            | (FieldType::Bool, FieldType::Bool)
2959    )
2960}
2961
2962fn require_sorted_unique<'a>(
2963    values: impl IntoIterator<Item = &'a str>,
2964    section: &'static str,
2965) -> Result<(), MappingValidationError> {
2966    let mut previous = None;
2967    for value in values {
2968        if previous.is_some_and(|prior| prior >= value) {
2969            return Err(MappingValidationError::MetadataNotSorted(section));
2970        }
2971        previous = Some(value);
2972    }
2973    Ok(())
2974}
2975
2976pub const MAX_DYNAMIC_FIELDS_PER_REQUEST: usize = 128;
2977pub const MAX_NESTED_MAPPING_DEPTH: usize = 8;
2978pub const MAX_NESTED_MAPPING_PATHS: usize = 64;
2979pub const DEFAULT_KEYWORD_IGNORE_ABOVE: u32 = 32_766;
2980pub const MAX_SCALAR_ARRAY_DEPTH: usize = 32;
2981pub const MAX_SCALAR_ARRAY_NODES: usize = 4_096;
2982pub const MAX_SCALAR_ARRAY_VALUES: usize = 2_048;
2983pub const MAX_MAPPING_BYTES: usize = 1_048_576;
2984pub const DYNAMIC_MAPPING_PROPOSAL_FORMAT_VERSION: u16 = 1;
2985
2986#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2987#[serde(rename_all = "snake_case")]
2988pub enum DynamicMappingPolicy {
2989    True,
2990    #[default]
2991    False,
2992    Strict,
2993}
2994
2995/// Closed inferred type carried by the catalog-owned dynamic proposal.
2996#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2997#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
2998pub enum DynamicProposedTypeV1 {
2999    Boolean,
3000    I64,
3001    U64,
3002    F64,
3003    TextWithKeyword {
3004        index_analyzer: AnalyzerName,
3005        search_analyzer: AnalyzerName,
3006        keyword_child: String,
3007        keyword_normalizer: KeywordNormalizer,
3008        keyword_max_bytes: u32,
3009    },
3010}
3011
3012impl DynamicProposedTypeV1 {
3013    pub fn from_first_candidate(
3014        field_path: &str,
3015        candidate: &MappingScalar,
3016    ) -> Result<Self, DynamicMappingProposalError> {
3017        validate_mapping_field_name(field_path).map_err(DynamicMappingProposalError::InvalidField)?;
3018        Ok(match candidate {
3019            MappingScalar::Boolean(_) => Self::Boolean,
3020            MappingScalar::I64(_) => Self::I64,
3021            MappingScalar::U64(_) => Self::U64,
3022            MappingScalar::F64Bits(bits) => {
3023                let value = f64::from_bits(*bits);
3024                if !value.is_finite() {
3025                    return Err(DynamicMappingProposalError::InvalidCandidate);
3026                }
3027                Self::F64
3028            }
3029            MappingScalar::Keyword(_) => Self::TextWithKeyword {
3030                index_analyzer: AnalyzerName::Standard,
3031                search_analyzer: AnalyzerName::Standard,
3032                keyword_child: format!("{field_path}.keyword"),
3033                keyword_normalizer: KeywordNormalizer::Keyword,
3034                keyword_max_bytes: DEFAULT_KEYWORD_IGNORE_ABOVE,
3035            },
3036        })
3037    }
3038
3039    fn validate(&self, field_path: &str) -> Result<(), DynamicMappingProposalError> {
3040        if let Self::TextWithKeyword {
3041            keyword_child,
3042            keyword_max_bytes,
3043            ..
3044        } = self
3045            && (keyword_child != &format!("{field_path}.keyword")
3046                || *keyword_max_bytes == 0
3047                || *keyword_max_bytes > DEFAULT_KEYWORD_IGNORE_ABOVE)
3048        {
3049            return Err(DynamicMappingProposalError::InvalidProposedType);
3050        }
3051        Ok(())
3052    }
3053}
3054
3055#[derive(Serialize)]
3056struct DynamicMappingProposalDigestInput<'a> {
3057    format_version: u16,
3058    collection_index_ref: CollectionIndexRef,
3059    base_generation: SchemaGeneration,
3060    field_path: &'a str,
3061    first_non_null_candidate: &'a MappingScalar,
3062    proposed_type: &'a DynamicProposedTypeV1,
3063}
3064
3065/// Canonical proposal-v1 identity frozen by EC4. Attempt fencing is supplied
3066/// out of band by the coordinated command and is deliberately not serialized
3067/// here.
3068#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
3069#[serde(deny_unknown_fields)]
3070pub struct DynamicMappingProposalV1 {
3071    pub format_version: u16,
3072    pub collection_index_ref: CollectionIndexRef,
3073    pub base_generation: SchemaGeneration,
3074    pub field_path: String,
3075    pub first_non_null_candidate: MappingScalar,
3076    pub proposed_type: DynamicProposedTypeV1,
3077    pub proposal_digest: u64,
3078}
3079
3080impl DynamicMappingProposalV1 {
3081    pub fn new(
3082        collection_index_ref: CollectionIndexRef,
3083        base_generation: SchemaGeneration,
3084        field_path: String,
3085        mut first_non_null_candidate: MappingScalar,
3086    ) -> Result<Self, DynamicMappingProposalError> {
3087        if let MappingScalar::F64Bits(bits) = &mut first_non_null_candidate
3088            && f64::from_bits(*bits) == 0.0
3089        {
3090            *bits = 0.0_f64.to_bits();
3091        }
3092        let proposed_type = DynamicProposedTypeV1::from_first_candidate(&field_path, &first_non_null_candidate)?;
3093        let mut proposal = Self {
3094            format_version: DYNAMIC_MAPPING_PROPOSAL_FORMAT_VERSION,
3095            collection_index_ref,
3096            base_generation,
3097            field_path,
3098            first_non_null_candidate,
3099            proposed_type,
3100            proposal_digest: 0,
3101        };
3102        proposal.proposal_digest = proposal.exact_digest()?;
3103        proposal.validate()?;
3104        Ok(proposal)
3105    }
3106
3107    pub fn exact_digest(&self) -> Result<u64, DynamicMappingProposalError> {
3108        let input = DynamicMappingProposalDigestInput {
3109            format_version: self.format_version,
3110            collection_index_ref: self.collection_index_ref,
3111            base_generation: self.base_generation,
3112            field_path: &self.field_path,
3113            first_non_null_candidate: &self.first_non_null_candidate,
3114            proposed_type: &self.proposed_type,
3115        };
3116        let bytes =
3117            serde_json::to_vec(&input).map_err(|error| DynamicMappingProposalError::Codec(error.to_string()))?;
3118        if bytes.len() > MAX_MAPPING_BYTES {
3119            return Err(DynamicMappingProposalError::TooLarge(bytes.len()));
3120        }
3121        Ok(bytes.iter().fold(0xcbf2_9ce4_8422_2325_u64, |digest, byte| {
3122            (digest ^ u64::from(*byte)).wrapping_mul(0x0000_0100_0000_01b3)
3123        }))
3124    }
3125
3126    pub fn validate(&self) -> Result<(), DynamicMappingProposalError> {
3127        if self.format_version != DYNAMIC_MAPPING_PROPOSAL_FORMAT_VERSION {
3128            return Err(DynamicMappingProposalError::UnsupportedVersion(self.format_version));
3129        }
3130        validate_mapping_field_name(&self.field_path).map_err(DynamicMappingProposalError::InvalidField)?;
3131        if matches!(
3132            self.first_non_null_candidate,
3133            MappingScalar::F64Bits(bits) if f64::from_bits(bits) == 0.0 && bits != 0.0_f64.to_bits()
3134        ) {
3135            return Err(DynamicMappingProposalError::InvalidCandidate);
3136        }
3137        self.proposed_type.validate(&self.field_path)?;
3138        if DynamicProposedTypeV1::from_first_candidate(&self.field_path, &self.first_non_null_candidate)?
3139            != self.proposed_type
3140            || self.exact_digest()? != self.proposal_digest
3141        {
3142            return Err(DynamicMappingProposalError::IdentityMismatch);
3143        }
3144        Ok(())
3145    }
3146
3147    /// Verifies that `successor` is exactly the one-field (or text plus
3148    /// generated keyword child) additive delta described by this proposal.
3149    #[must_use]
3150    pub fn matches_successor(&self, predecessor: &Mapping, successor: &Mapping) -> bool {
3151        let Some(next_generation) = predecessor.schema_generation.checked_next() else {
3152            return false;
3153        };
3154        if self.validate().is_err()
3155            || self.base_generation != predecessor.schema_generation
3156            || successor.schema_generation != next_generation
3157        {
3158            return false;
3159        }
3160        let mut expected = predecessor.clone().normalized();
3161        expected.schema_generation = successor.schema_generation;
3162        let (field_type, text) = match &self.proposed_type {
3163            DynamicProposedTypeV1::Boolean => (FieldType::Bool, None),
3164            DynamicProposedTypeV1::I64 => (FieldType::I64, None),
3165            DynamicProposedTypeV1::U64 => (FieldType::U64, None),
3166            DynamicProposedTypeV1::F64 => (FieldType::F64, None),
3167            DynamicProposedTypeV1::TextWithKeyword {
3168                index_analyzer,
3169                search_analyzer,
3170                keyword_child,
3171                keyword_normalizer,
3172                keyword_max_bytes,
3173            } => (
3174                FieldType::Text,
3175                Some((
3176                    *index_analyzer,
3177                    *search_analyzer,
3178                    keyword_child,
3179                    *keyword_normalizer,
3180                    *keyword_max_bytes,
3181                )),
3182            ),
3183        };
3184        if expected
3185            .fields
3186            .insert(
3187                self.field_path.clone(),
3188                FieldMapping {
3189                    field_type,
3190                    stored: false,
3191                    null_value: None,
3192                },
3193            )
3194            .is_some()
3195        {
3196            return false;
3197        }
3198        if let Some((index_analyzer, search_analyzer, keyword_child, keyword_normalizer, keyword_max_bytes)) = text {
3199            if expected
3200                .fields
3201                .insert(
3202                    keyword_child.clone(),
3203                    FieldMapping {
3204                        field_type: FieldType::Keyword,
3205                        stored: false,
3206                        null_value: None,
3207                    },
3208                )
3209                .is_some()
3210            {
3211                return false;
3212            }
3213            expected.extensions.multi_field_projections.push(MultiFieldProjection {
3214                parent: self.field_path.clone(),
3215                child: keyword_child.clone(),
3216            });
3217            expected.extensions.analyzer_identities.push(AnalyzerIdentity {
3218                field: self.field_path.clone(),
3219                index_analyzer,
3220                search_analyzer,
3221            });
3222            expected.extensions.keyword_limits.push(KeywordFieldPolicy {
3223                field: keyword_child.clone(),
3224                max_bytes: keyword_max_bytes,
3225                normalizer: keyword_normalizer,
3226            });
3227            expected
3228                .extensions
3229                .multi_field_projections
3230                .sort_by(|left, right| left.child.cmp(&right.child));
3231            expected
3232                .extensions
3233                .analyzer_identities
3234                .sort_by(|left, right| left.field.cmp(&right.field));
3235            expected
3236                .extensions
3237                .keyword_limits
3238                .sort_by(|left, right| left.field.cmp(&right.field));
3239        }
3240        expected == successor.clone().normalized()
3241    }
3242
3243    pub fn encode_canonical(&self) -> Result<Vec<u8>, DynamicMappingProposalError> {
3244        self.validate()?;
3245        let bytes = serde_json::to_vec(self).map_err(|error| DynamicMappingProposalError::Codec(error.to_string()))?;
3246        if bytes.len() > MAX_MAPPING_BYTES {
3247            return Err(DynamicMappingProposalError::TooLarge(bytes.len()));
3248        }
3249        Ok(bytes)
3250    }
3251
3252    pub fn decode_canonical(bytes: &[u8]) -> Result<Self, DynamicMappingProposalError> {
3253        if bytes.len() > MAX_MAPPING_BYTES {
3254            return Err(DynamicMappingProposalError::TooLarge(bytes.len()));
3255        }
3256        let mut probe = serde_json::Deserializer::from_slice(bytes);
3257        let version = MappingVersionProbe::deserialize(&mut probe)
3258            .map_err(|error| DynamicMappingProposalError::Codec(error.to_string()))?;
3259        probe
3260            .end()
3261            .map_err(|error| DynamicMappingProposalError::Codec(error.to_string()))?;
3262        if version != DYNAMIC_MAPPING_PROPOSAL_FORMAT_VERSION {
3263            return Err(DynamicMappingProposalError::UnsupportedVersion(version));
3264        }
3265        let proposal: Self =
3266            serde_json::from_slice(bytes).map_err(|error| DynamicMappingProposalError::Codec(error.to_string()))?;
3267        proposal.validate()?;
3268        if proposal.encode_canonical()? != bytes {
3269            return Err(DynamicMappingProposalError::NonCanonical);
3270        }
3271        Ok(proposal)
3272    }
3273}
3274
3275#[derive(Debug)]
3276pub enum DynamicMappingProposalError {
3277    UnsupportedVersion(u16),
3278    InvalidField(MappingValidationError),
3279    InvalidCandidate,
3280    InvalidProposedType,
3281    IdentityMismatch,
3282    TooLarge(usize),
3283    NonCanonical,
3284    Codec(String),
3285}
3286
3287impl Display for DynamicMappingProposalError {
3288    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
3289        match self {
3290            Self::UnsupportedVersion(version) => {
3291                write!(formatter, "unsupported dynamic mapping proposal version {version}")
3292            }
3293            Self::InvalidField(error) => write!(formatter, "invalid dynamic mapping field: {error}"),
3294            Self::InvalidCandidate => formatter.write_str("invalid dynamic mapping first candidate"),
3295            Self::InvalidProposedType => formatter.write_str("invalid dynamic mapping proposed type"),
3296            Self::IdentityMismatch => formatter.write_str("dynamic mapping proposal identity mismatch"),
3297            Self::TooLarge(bytes) => write!(formatter, "dynamic mapping proposal is too large: {bytes} bytes"),
3298            Self::NonCanonical => formatter.write_str("dynamic mapping proposal is not canonical"),
3299            Self::Codec(error) => write!(formatter, "dynamic mapping proposal codec error: {error}"),
3300        }
3301    }
3302}
3303
3304impl Error for DynamicMappingProposalError {}
3305
3306#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
3307#[serde(deny_unknown_fields)]
3308pub struct MultiFieldProjection {
3309    pub parent: String,
3310    pub child: String,
3311}
3312
3313#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
3314#[serde(deny_unknown_fields)]
3315pub struct AnalyzerIdentity {
3316    pub field: String,
3317    pub index_analyzer: AnalyzerName,
3318    pub search_analyzer: AnalyzerName,
3319}
3320
3321#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
3322#[serde(rename_all = "snake_case")]
3323pub enum AnalyzerName {
3324    Keyword,
3325    Simple,
3326    Standard,
3327    Whitespace,
3328}
3329
3330#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
3331#[serde(deny_unknown_fields)]
3332pub struct KeywordFieldPolicy {
3333    pub field: String,
3334    pub max_bytes: u32,
3335    pub normalizer: KeywordNormalizer,
3336}
3337
3338impl KeywordFieldPolicy {
3339    fn validate(&self) -> Result<(), MappingValidationError> {
3340        if self.max_bytes == 0 || self.max_bytes > DEFAULT_KEYWORD_IGNORE_ABOVE {
3341            return Err(MappingValidationError::InvalidKeywordLimit {
3342                field: self.field.clone(),
3343                limit: self.max_bytes,
3344            });
3345        }
3346        Ok(())
3347    }
3348}
3349
3350#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
3351#[serde(rename_all = "snake_case")]
3352pub enum KeywordNormalizer {
3353    Keyword,
3354    Lowercase,
3355}
3356
3357/// Explicit normalized field definition.
3358#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
3359#[serde(deny_unknown_fields)]
3360pub struct FieldMapping {
3361    #[serde(rename = "type")]
3362    pub field_type: FieldType,
3363    #[serde(default)]
3364    pub stored: bool,
3365    #[serde(default, skip_serializing_if = "Option::is_none")]
3366    pub null_value: Option<MappingScalar>,
3367}
3368
3369impl FieldMapping {
3370    fn validate(&self, field: &str) -> Result<(), MappingValidationError> {
3371        let Some(value) = &self.null_value else {
3372            return Ok(());
3373        };
3374        let valid = match (self.field_type, value) {
3375            (FieldType::Text | FieldType::Keyword, MappingScalar::Keyword(_))
3376            | (FieldType::U64 | FieldType::Date, MappingScalar::U64(_))
3377            | (FieldType::I64, MappingScalar::I64(_))
3378            | (FieldType::Bool, MappingScalar::Boolean(_)) => true,
3379            (FieldType::U64 | FieldType::Date, MappingScalar::I64(value)) => *value >= 0,
3380            (FieldType::F64, MappingScalar::F64Bits(bits)) => f64::from_bits(*bits).is_finite(),
3381            (FieldType::F64, MappingScalar::I64(_) | MappingScalar::U64(_)) => true,
3382            _ => false,
3383        };
3384        if valid {
3385            Ok(())
3386        } else {
3387            Err(MappingValidationError::InvalidNullValue(field.to_owned()))
3388        }
3389    }
3390}
3391
3392#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
3393#[serde(tag = "kind", content = "value", rename_all = "snake_case", deny_unknown_fields)]
3394pub enum MappingScalar {
3395    Boolean(bool),
3396    I64(i64),
3397    U64(u64),
3398    F64Bits(u64),
3399    Keyword(String),
3400}
3401
3402/// Mapping types admitted by the native mapping model.
3403#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
3404#[serde(rename_all = "snake_case")]
3405pub enum FieldType {
3406    Text,
3407    Keyword,
3408    U64,
3409    /// Unsigned epoch milliseconds stored in the existing u64 fast column.
3410    Date,
3411    I64,
3412    F64,
3413    Bool,
3414    /// Object-correlation boundary reserved by format 4. EC6 owns physical
3415    /// nested rows and makes this field kind externally reachable.
3416    Nested,
3417    /// Immutable vector values live in a manifest-rooted segment sidecar, not
3418    /// in Tantivy. The catalog mapping remains their sole field authority.
3419    Vector {
3420        format_version: u16,
3421        field_id: VectorFieldId,
3422        dimensions: u16,
3423        metric: VectorMetric,
3424        element_type: DenseVectorElementType,
3425        #[serde(default, skip_serializing_if = "Option::is_none")]
3426        quantization_scale: Option<VectorQuantizationScale>,
3427    },
3428    /// Sparse numeric dimensions are explicit and immutable. No string
3429    /// feature hashing or dictionary authority exists inside the engine.
3430    SparseVector {
3431        format_version: u16,
3432        field_id: VectorFieldId,
3433        maximum_dimension: u32,
3434        weight_type: SparseVectorWeightType,
3435    },
3436    /// A bounded set of same-shaped dense vectors scored by the versioned
3437    /// late-interaction MaxSim contract. The exact source object is authority.
3438    VectorSet {
3439        format_version: u16,
3440        field_id: VectorFieldId,
3441        dimensions: u16,
3442        metric: VectorMetric,
3443        element_type: DenseVectorElementType,
3444        #[serde(default, skip_serializing_if = "Option::is_none")]
3445        quantization_scale: Option<VectorQuantizationScale>,
3446        maximum_vectors: u16,
3447    },
3448}
3449
3450/// Frozen vector distance semantics. Greater returned score is always better.
3451#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
3452#[serde(rename_all = "snake_case")]
3453pub enum VectorMetric {
3454    Cosine,
3455    DotProduct,
3456    SquaredL2,
3457}
3458
3459/// Returns whether a field path has one or more non-empty dot-delimited
3460/// components and contains no control bytes.
3461///
3462/// Dots are path separators, never literal key bytes. Callers that retain
3463/// arbitrary JSON therefore must reject object keys containing dots rather
3464/// than choosing a different interpretation at ingest and read time.
3465#[must_use]
3466pub fn is_canonical_field_path(path: &str) -> bool {
3467    !path.is_empty()
3468        && path.len() <= MAX_MAPPING_FIELD_NAME_BYTES
3469        && !path.bytes().any(|byte| byte <= 0x1f || byte == 0x7f)
3470        && path.split('.').all(|component| !component.is_empty())
3471}
3472
3473/// Resolves one canonical dotted path through a caller-provided object member
3474/// lookup. Invalid paths resolve to `None`.
3475///
3476/// Keeping component splitting here gives ingestion, native execution, and
3477/// compatibility response materialization one path interpretation without
3478/// making foundational types depend on a JSON implementation.
3479pub fn resolve_field_path<'a, T>(
3480    root: &'a T,
3481    path: &str,
3482    member: impl FnMut(&'a T, &str) -> Option<&'a T>,
3483) -> Option<&'a T> {
3484    if !is_canonical_field_path(path) {
3485        return None;
3486    }
3487    path.split('.').try_fold(root, member)
3488}
3489
3490/// Exact outcome of traversing a canonical field path.
3491#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3492pub enum FieldPathResolution<'a, T> {
3493    Missing,
3494    Found(&'a T),
3495    /// A present scalar/array was encountered where another object component
3496    /// was required.
3497    Blocked,
3498}
3499
3500/// Resolves a canonical path while distinguishing absence from a present
3501/// non-container intermediate.
3502pub fn resolve_field_path_checked<'a, T>(
3503    root: &'a T,
3504    path: &str,
3505    mut member: impl FnMut(&'a T, &str) -> Option<&'a T>,
3506    is_container: impl Fn(&T) -> bool,
3507) -> FieldPathResolution<'a, T> {
3508    if !is_canonical_field_path(path) {
3509        return FieldPathResolution::Missing;
3510    }
3511    let mut current = root;
3512    for component in path.split('.') {
3513        if !is_container(current) {
3514            return FieldPathResolution::Blocked;
3515        }
3516        let Some(next) = member(current, component) else {
3517            return FieldPathResolution::Missing;
3518        };
3519        current = next;
3520    }
3521    FieldPathResolution::Found(current)
3522}
3523
3524/// Validates one mapped field path without depending on an execution engine.
3525pub fn validate_mapping_field_name(name: &str) -> Result<(), MappingValidationError> {
3526    if !is_canonical_field_path(name) || name.starts_with('_') {
3527        return Err(MappingValidationError::InvalidFieldName(name.to_owned()));
3528    }
3529    Ok(())
3530}
3531
3532/// Invalid normalized mapping semantics.
3533#[derive(Clone, Debug, Eq, PartialEq)]
3534pub enum MappingValidationError {
3535    UnsupportedVersion(u16),
3536    ZeroSchemaGeneration,
3537    TooManyFields(usize),
3538    InvalidFieldName(String),
3539    ConflictingFieldPaths { ancestor: String, descendant: String },
3540    UnsupportedVectorVersion(u16),
3541    UnsupportedSparseVectorVersion(u16),
3542    UnsupportedVectorSetVersion(u16),
3543    TooManyVectorFields(usize),
3544    InvalidVectorDimensions { field: String, dimensions: u16 },
3545    InvalidVectorElementContract(String),
3546    InvalidSparseVectorContract(String),
3547    InvalidVectorSetContract(String),
3548    DuplicateVectorFieldId(VectorFieldId),
3549    StoredVectorField(String),
3550    MissingMultiField(String),
3551    MissingMultiFieldSource { field: String, source: String },
3552    InvalidMultiFieldPath(String),
3553    InvalidMultiFieldEndpoint(String),
3554    UnknownFieldMetadata(String),
3555    AnalyzerOnNonTextField(String),
3556    KeywordPolicyOnNonKeywordField(String),
3557    InvalidKeywordLimit { field: String, limit: u32 },
3558    InvalidKeywordNullValue(String),
3559    InvalidNullValue(String),
3560    InvalidNestedPath(String),
3561    InvalidRecordModel(String),
3562    NestedPathTooDeep(String),
3563    NestedPathOverlap { parent: String, child: String },
3564    IncompleteAnalyzerCoverage,
3565    IncompleteKeywordCoverage,
3566    IncompleteNestedCoverage,
3567    MetadataNotSorted(&'static str),
3568    TooManyMappingMetadata,
3569    TooManyNestedPaths(usize),
3570}
3571
3572impl Display for MappingValidationError {
3573    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
3574        match self {
3575            Self::UnsupportedVersion(version) => write!(formatter, "unsupported mapping format version {version}"),
3576            Self::ZeroSchemaGeneration => formatter.write_str("mapping schema generation must be nonzero"),
3577            Self::TooManyFields(count) => write!(formatter, "mapping has {count} fields"),
3578            Self::InvalidFieldName(name) => write!(formatter, "invalid mapped field name '{name}'"),
3579            Self::ConflictingFieldPaths { ancestor, descendant } => write!(
3580                formatter,
3581                "mapped scalar path '{ancestor}' conflicts with descendant '{descendant}'"
3582            ),
3583            Self::UnsupportedVectorVersion(version) => {
3584                write!(formatter, "unsupported vector mapping format version {version}")
3585            }
3586            Self::UnsupportedSparseVectorVersion(version) => {
3587                write!(formatter, "unsupported sparse-vector mapping format version {version}")
3588            }
3589            Self::UnsupportedVectorSetVersion(version) => {
3590                write!(formatter, "unsupported vector-set mapping format version {version}")
3591            }
3592            Self::TooManyVectorFields(count) => write!(formatter, "mapping has {count} vector fields"),
3593            Self::InvalidVectorDimensions { field, dimensions } => {
3594                write!(formatter, "vector field '{field}' has invalid dimensions {dimensions}")
3595            }
3596            Self::InvalidVectorElementContract(field) => {
3597                write!(
3598                    formatter,
3599                    "vector field '{field}' has invalid element or quantization semantics"
3600                )
3601            }
3602            Self::InvalidSparseVectorContract(field) => {
3603                write!(
3604                    formatter,
3605                    "sparse vector field '{field}' has invalid dimension or weight semantics"
3606                )
3607            }
3608            Self::InvalidVectorSetContract(field) => {
3609                write!(
3610                    formatter,
3611                    "vector-set field '{field}' has invalid element, dimension, or count semantics"
3612                )
3613            }
3614            Self::DuplicateVectorFieldId(field_id) => {
3615                write!(formatter, "vector field identity {field_id} is duplicated")
3616            }
3617            Self::StoredVectorField(field) => {
3618                write!(
3619                    formatter,
3620                    "vector field '{field}' cannot use Tantivy stored-field semantics"
3621                )
3622            }
3623            Self::MissingMultiField(field) => write!(formatter, "multi-field projection '{field}' has no field"),
3624            Self::MissingMultiFieldSource { field, source } => {
3625                write!(
3626                    formatter,
3627                    "multi-field projection '{field}' has missing source '{source}'"
3628                )
3629            }
3630            Self::InvalidMultiFieldPath(field) => {
3631                write!(
3632                    formatter,
3633                    "multi-field projection '{field}' is not a direct child of its source"
3634                )
3635            }
3636            Self::InvalidMultiFieldEndpoint(field) => {
3637                write!(
3638                    formatter,
3639                    "multi-field projection '{field}' has a nested or vector endpoint"
3640                )
3641            }
3642            Self::UnknownFieldMetadata(field) => {
3643                write!(formatter, "mapping metadata references unknown field '{field}'")
3644            }
3645            Self::AnalyzerOnNonTextField(field) => {
3646                write!(formatter, "analyzer identity is attached to non-text field '{field}'")
3647            }
3648            Self::KeywordPolicyOnNonKeywordField(field) => {
3649                write!(formatter, "keyword policy is attached to non-keyword field '{field}'")
3650            }
3651            Self::InvalidKeywordLimit { field, limit } => {
3652                write!(formatter, "keyword field '{field}' has invalid ignore_above {limit}")
3653            }
3654            Self::InvalidKeywordNullValue(field) => {
3655                write!(
3656                    formatter,
3657                    "keyword field '{field}' has a null_value beyond ignore_above"
3658                )
3659            }
3660            Self::InvalidNullValue(field) => write!(formatter, "field '{field}' has an invalid null_value"),
3661            Self::InvalidNestedPath(path) => write!(formatter, "nested path '{path}' has no nested field"),
3662            Self::InvalidRecordModel(reason) => write!(formatter, "invalid index record model: {reason}"),
3663            Self::NestedPathTooDeep(path) => write!(formatter, "nested path '{path}' exceeds maximum depth"),
3664            Self::NestedPathOverlap { parent, child } => {
3665                write!(formatter, "nested path '{child}' is below nested path '{parent}'")
3666            }
3667            Self::IncompleteAnalyzerCoverage => {
3668                formatter.write_str("every text field must have exactly one analyzer identity")
3669            }
3670            Self::IncompleteKeywordCoverage => {
3671                formatter.write_str("every keyword field must have exactly one keyword limit")
3672            }
3673            Self::IncompleteNestedCoverage => formatter.write_str("nested_paths must exactly cover nested fields"),
3674            Self::MetadataNotSorted(section) => {
3675                write!(formatter, "mapping section '{section}' is not sorted and unique")
3676            }
3677            Self::TooManyMappingMetadata => formatter.write_str("mapping metadata exceeds the field bound"),
3678            Self::TooManyNestedPaths(count) => write!(formatter, "mapping has {count} nested paths"),
3679        }
3680    }
3681}
3682
3683impl Error for MappingValidationError {}
3684
3685#[derive(Clone, Debug, Eq, PartialEq)]
3686pub enum MappingCodecError {
3687    TooLarge(usize),
3688    DuplicateKey(String),
3689    Malformed(String),
3690    UnsupportedVersion(u16),
3691    Invalid(MappingValidationError),
3692    NonCanonical,
3693}
3694
3695impl Display for MappingCodecError {
3696    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
3697        match self {
3698            Self::TooLarge(bytes) => write!(formatter, "mapping payload has {bytes} bytes"),
3699            Self::DuplicateKey(reason) | Self::Malformed(reason) => formatter.write_str(reason),
3700            Self::UnsupportedVersion(version) => write!(formatter, "unsupported mapping format version {version}"),
3701            Self::Invalid(error) => write!(formatter, "invalid mapping: {error}"),
3702            Self::NonCanonical => formatter.write_str("mapping payload is not canonical JSON"),
3703        }
3704    }
3705}
3706
3707impl Error for MappingCodecError {
3708    fn source(&self) -> Option<&(dyn Error + 'static)> {
3709        match self {
3710            Self::Invalid(error) => Some(error),
3711            _ => None,
3712        }
3713    }
3714}
3715
3716/// Object-storage residency assigned to one immutable segment file.
3717///
3718/// This enum reserves lifecycle policy identities. Only [`Self::Standard`] is
3719/// admitted until a provider-specific PUT plus metadata-readback probe passes.
3720#[non_exhaustive]
3721#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
3722#[serde(rename_all = "snake_case")]
3723pub enum StorageClass {
3724    #[default]
3725    Standard,
3726    Cold,
3727}
3728
3729/// Reconstructible local-residency preference for one index.
3730#[non_exhaustive]
3731#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
3732#[serde(rename_all = "snake_case")]
3733pub enum CacheClass {
3734    Pinned,
3735    #[default]
3736    Normal,
3737    ColdTolerant,
3738}
3739
3740impl CacheClass {
3741    /// Returns the stable catalog identity.
3742    #[must_use]
3743    pub const fn as_str(self) -> &'static str {
3744        match self {
3745            Self::Pinned => "pinned",
3746            Self::Normal => "normal",
3747            Self::ColdTolerant => "cold_tolerant",
3748        }
3749    }
3750}
3751
3752impl Display for CacheClass {
3753    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
3754        formatter.write_str(self.as_str())
3755    }
3756}
3757
3758impl StorageClass {
3759    /// Returns whether serde may omit this value from a versioned parent.
3760    #[must_use]
3761    pub const fn is_standard(&self) -> bool {
3762        matches!(self, Self::Standard)
3763    }
3764
3765    /// Returns the stable manifest identity.
3766    #[must_use]
3767    pub const fn as_str(self) -> &'static str {
3768        match self {
3769            Self::Standard => "standard",
3770            Self::Cold => "cold",
3771        }
3772    }
3773}
3774
3775impl Display for StorageClass {
3776    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
3777        formatter.write_str(self.as_str())
3778    }
3779}
3780
3781/// Reserved verb carried by a future versioned lifecycle-stage parent.
3782///
3783/// This enum does not execute lifecycle work. ADR-0016 reserves the wire names
3784/// before an index-config format depends on them.
3785#[non_exhaustive]
3786#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
3787#[serde(rename_all = "snake_case")]
3788pub enum LifecycleVerb {
3789    Transition,
3790    AggregateThenDelete,
3791}
3792
3793/// A validated document identifier.
3794#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
3795#[serde(transparent)]
3796pub struct DocumentId(String);
3797
3798impl DocumentId {
3799    /// Constructs a non-empty document identifier no larger than 512 bytes.
3800    pub fn new(value: impl Into<String>) -> Result<Self, IdentifierError> {
3801        let value = value.into();
3802        if value.is_empty() {
3803            return Err(IdentifierError::EmptyDocumentId);
3804        }
3805        if value.len() > 512 {
3806            return Err(IdentifierError::DocumentIdTooLong(value.len()));
3807        }
3808        Ok(Self(value))
3809    }
3810
3811    /// Returns the identifier text.
3812    #[must_use]
3813    pub fn as_str(&self) -> &str {
3814        &self.0
3815    }
3816
3817    /// Consumes this identifier.
3818    #[must_use]
3819    pub fn into_inner(self) -> String {
3820        self.0
3821    }
3822}
3823
3824impl<'de> Deserialize<'de> for DocumentId {
3825    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3826    where
3827        D: Deserializer<'de>,
3828    {
3829        let value = String::deserialize(deserializer)?;
3830        Self::new(value).map_err(serde::de::Error::custom)
3831    }
3832}
3833
3834impl Display for DocumentId {
3835    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
3836        formatter.write_str(self.as_str())
3837    }
3838}
3839
3840/// A bounded client token used to deduplicate one logical write.
3841#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
3842#[serde(transparent)]
3843pub struct IdempotencyKey(String);
3844
3845impl IdempotencyKey {
3846    /// Constructs a non-empty UTF-8 key no larger than 256 bytes.
3847    pub fn new(value: impl Into<String>) -> Result<Self, IdentifierError> {
3848        let value = value.into();
3849        if value.is_empty() {
3850            return Err(IdentifierError::EmptyIdempotencyKey);
3851        }
3852        if value.len() > 256 {
3853            return Err(IdentifierError::IdempotencyKeyTooLong(value.len()));
3854        }
3855        Ok(Self(value))
3856    }
3857
3858    #[must_use]
3859    pub fn as_str(&self) -> &str {
3860        &self.0
3861    }
3862}
3863
3864impl<'de> Deserialize<'de> for IdempotencyKey {
3865    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3866    where
3867        D: Deserializer<'de>,
3868    {
3869        let value = String::deserialize(deserializer)?;
3870        Self::new(value).map_err(serde::de::Error::custom)
3871    }
3872}
3873
3874impl Display for IdempotencyKey {
3875    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
3876        formatter.write_str(self.as_str())
3877    }
3878}
3879
3880/// A bounded opaque point-in-time lease identifier.
3881#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
3882#[serde(transparent)]
3883pub struct PitId(String);
3884
3885impl PitId {
3886    /// Constructs a non-empty opaque PIT identifier no larger than 512 bytes.
3887    pub fn new(value: impl Into<String>) -> Result<Self, IdentifierError> {
3888        let value = value.into();
3889        if value.is_empty() {
3890            return Err(IdentifierError::EmptyPitId);
3891        }
3892        if value.len() > 512 {
3893            return Err(IdentifierError::PitIdTooLong(value.len()));
3894        }
3895        Ok(Self(value))
3896    }
3897
3898    /// Returns the opaque identifier text.
3899    #[must_use]
3900    pub fn as_str(&self) -> &str {
3901        &self.0
3902    }
3903}
3904
3905impl<'de> Deserialize<'de> for PitId {
3906    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3907    where
3908        D: Deserializer<'de>,
3909    {
3910        let value = String::deserialize(deserializer)?;
3911        Self::new(value).map_err(serde::de::Error::custom)
3912    }
3913}
3914
3915impl Display for PitId {
3916    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
3917        formatter.write_str(self.as_str())
3918    }
3919}
3920
3921/// Durable format version for recovery-era-scoped PIT identifiers.
3922pub const SCOPED_PIT_ID_FORMAT_VERSION: u16 = 1;
3923
3924/// A PIT identifier whose namespace is fenced by the coordinated recovery era.
3925///
3926/// The era is part of the identifier rather than adjacent command metadata so
3927/// a nonce may be reused only after the durable authority has advanced.
3928#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize)]
3929#[serde(deny_unknown_fields)]
3930pub struct ScopedPitId {
3931    format_version: u16,
3932    recovery_era: RecoveryEra,
3933    nonce: PitId,
3934}
3935
3936impl ScopedPitId {
3937    #[must_use]
3938    pub const fn new(recovery_era: RecoveryEra, nonce: PitId) -> Self {
3939        Self {
3940            format_version: SCOPED_PIT_ID_FORMAT_VERSION,
3941            recovery_era,
3942            nonce,
3943        }
3944    }
3945
3946    #[must_use]
3947    pub const fn recovery_era(&self) -> RecoveryEra {
3948        self.recovery_era
3949    }
3950
3951    #[must_use]
3952    pub const fn nonce(&self) -> &PitId {
3953        &self.nonce
3954    }
3955}
3956
3957impl Ord for ScopedPitId {
3958    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
3959        self.recovery_era
3960            .to_be_bytes()
3961            .cmp(&other.recovery_era.to_be_bytes())
3962            .then_with(|| self.nonce.cmp(&other.nonce))
3963    }
3964}
3965
3966impl PartialOrd for ScopedPitId {
3967    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
3968        Some(self.cmp(other))
3969    }
3970}
3971
3972impl<'de> Deserialize<'de> for ScopedPitId {
3973    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3974    where
3975        D: Deserializer<'de>,
3976    {
3977        #[derive(Deserialize)]
3978        #[serde(deny_unknown_fields)]
3979        struct Wire {
3980            format_version: u16,
3981            recovery_era: RecoveryEra,
3982            nonce: PitId,
3983        }
3984
3985        let wire = Wire::deserialize(deserializer)?;
3986        if wire.format_version != SCOPED_PIT_ID_FORMAT_VERSION {
3987            return Err(serde::de::Error::custom(
3988                IdentifierError::UnsupportedScopedPitIdVersion(wire.format_version),
3989            ));
3990        }
3991        Ok(Self::new(wire.recovery_era, wire.nonce))
3992    }
3993}
3994
3995/// Stable BLAKE3 fingerprint of the complete client mutation intent.
3996#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
3997#[serde(transparent)]
3998pub struct OperationFingerprint([u8; 32]);
3999
4000impl OperationFingerprint {
4001    #[must_use]
4002    pub const fn new(bytes: [u8; 32]) -> Self {
4003        Self(bytes)
4004    }
4005
4006    #[must_use]
4007    pub fn from_payload(bytes: &[u8]) -> Self {
4008        Self(*blake3::hash(bytes).as_bytes())
4009    }
4010
4011    #[must_use]
4012    pub const fn as_bytes(&self) -> &[u8; 32] {
4013        &self.0
4014    }
4015}
4016
4017/// A validated relative immutable-object key.
4018#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
4019#[serde(transparent)]
4020pub struct ObjectKey(String);
4021
4022impl ObjectKey {
4023    /// Constructs a safe relative object key.
4024    pub fn new(value: impl Into<String>) -> Result<Self, IdentifierError> {
4025        let value = value.into();
4026        if value.is_empty() {
4027            return Err(IdentifierError::EmptyObjectKey);
4028        }
4029        if value.starts_with('/') || value.contains('\\') {
4030            return Err(IdentifierError::InvalidObjectKey(value));
4031        }
4032        if value
4033            .split('/')
4034            .any(|component| component.is_empty() || component == "." || component == "..")
4035        {
4036            return Err(IdentifierError::InvalidObjectKey(value));
4037        }
4038        Ok(Self(value))
4039    }
4040
4041    /// Returns the normalized relative key.
4042    #[must_use]
4043    pub fn as_str(&self) -> &str {
4044        &self.0
4045    }
4046
4047    /// Consumes this object key.
4048    ///
4049    /// The allocation can exceed the encoded key length. Memory admissions
4050    /// retaining this key must use `allocated_capacity_bytes` instead.
4051    #[must_use]
4052    pub fn into_inner(self) -> String {
4053        self.0
4054    }
4055
4056    /// Owned string allocation capacity, excluding the inline string header.
4057    #[must_use]
4058    pub fn allocated_capacity_bytes(&self) -> usize {
4059        self.0.capacity()
4060    }
4061}
4062
4063impl<'de> Deserialize<'de> for ObjectKey {
4064    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
4065    where
4066        D: Deserializer<'de>,
4067    {
4068        let value = String::deserialize(deserializer)?;
4069        Self::new(value).map_err(serde::de::Error::custom)
4070    }
4071}
4072
4073impl Display for ObjectKey {
4074    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
4075        formatter.write_str(self.as_str())
4076    }
4077}
4078
4079/// Current transitive root-set locator. The page count selects the current
4080/// single-page or linked-page physical encoding.
4081pub const ROOT_SET_REF_FORMAT_VERSION: u16 = 2;
4082pub const MAX_ROOT_SET_PAGES: u32 = 256;
4083pub const MAX_ROOT_SET_SCHEMA_GENERATIONS: usize = 256;
4084pub const MAX_ROOT_SET_OBJECT_KEY_BYTES: usize = 1_024;
4085pub const PINNED_MANIFEST_BINDING_FORMAT_VERSION: u16 = 1;
4086
4087/// Never-reused physical identity of one immutable object.
4088#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
4089#[serde(deny_unknown_fields)]
4090pub struct PhysicalObjectId {
4091    pub backend_authority: BackendAuthorityId,
4092    pub object_incarnation: ObjectIncarnation,
4093    pub object_key: ObjectKey,
4094}
4095
4096impl PhysicalObjectId {
4097    pub fn validate(&self) -> Result<(), RootSetRefError> {
4098        let length = self.object_key.as_str().len();
4099        if !(1..=MAX_ROOT_SET_OBJECT_KEY_BYTES).contains(&length) {
4100            return Err(RootSetRefError::ObjectKeyLength(length));
4101        }
4102        Ok(())
4103    }
4104}
4105
4106/// Integrity-qualified physical object used inside immutable root-set pages.
4107#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
4108#[serde(deny_unknown_fields)]
4109pub struct ObjectIdentity {
4110    pub physical: PhysicalObjectId,
4111    pub content_checksum: [u8; 32],
4112}
4113
4114impl ObjectIdentity {
4115    pub fn validate(&self) -> Result<(), RootSetRefError> {
4116        self.physical.validate()
4117    }
4118}
4119
4120/// Bounded immutable root-set header copied only from an authoritative source.
4121#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
4122#[serde(deny_unknown_fields)]
4123pub struct RootSetRef {
4124    pub format_version: u16,
4125    pub root: PhysicalObjectId,
4126    pub content_digest: [u8; 32],
4127    pub item_count: u64,
4128    pub page_count: u32,
4129    pub schema_generations: Vec<SchemaGeneration>,
4130}
4131
4132impl RootSetRef {
4133    pub fn validate(&self) -> Result<(), RootSetRefError> {
4134        if self.format_version != ROOT_SET_REF_FORMAT_VERSION {
4135            return Err(RootSetRefError::UnsupportedVersion(self.format_version));
4136        }
4137        self.root.validate()?;
4138        if self.item_count == 0
4139            || self.page_count == 0
4140            || self.page_count > MAX_ROOT_SET_PAGES
4141            || u64::from(self.page_count) > self.item_count
4142        {
4143            return Err(RootSetRefError::InvalidCounts {
4144                items: self.item_count,
4145                pages: self.page_count,
4146            });
4147        }
4148        if self.schema_generations.len() > MAX_ROOT_SET_SCHEMA_GENERATIONS
4149            || self.schema_generations.windows(2).any(|pair| pair[0] >= pair[1])
4150        {
4151            return Err(RootSetRefError::InvalidSchemaGenerations);
4152        }
4153        Ok(())
4154    }
4155}
4156
4157#[derive(Clone, Debug, Eq, PartialEq)]
4158pub enum RootSetRefError {
4159    UnsupportedVersion(u16),
4160    ObjectKeyLength(usize),
4161    InvalidCounts { items: u64, pages: u32 },
4162    InvalidSchemaGenerations,
4163}
4164
4165impl Display for RootSetRefError {
4166    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
4167        match self {
4168            Self::UnsupportedVersion(version) => write!(formatter, "unsupported root-set reference version {version}"),
4169            Self::ObjectKeyLength(length) => {
4170                write!(formatter, "root-set object key is {length} bytes; expected 1..=1024")
4171            }
4172            Self::InvalidCounts { items, pages } => {
4173                write!(formatter, "root-set counts are invalid: items={items}, pages={pages}")
4174            }
4175            Self::InvalidSchemaGenerations => {
4176                formatter.write_str("root-set schema generations are not bounded strict ascending values")
4177            }
4178        }
4179    }
4180}
4181
4182impl Error for RootSetRefError {}
4183
4184/// Exact immutable-manifest codec selected by a durable PIT binding.
4185#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
4186#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
4187pub enum PinnedManifestDecodeProvenanceV1 {
4188    ScopedManifestV3,
4189}
4190
4191/// Complete immutable manifest binding copied into a durable PIT lease.
4192///
4193/// This is read authority only. It cannot publish or replace a manifest, and
4194/// retaining it beside a PIT is the immutable snapshot ownership transfer
4195/// required when the mutable current pointer later advances.
4196#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
4197#[serde(deny_unknown_fields)]
4198pub struct PinnedManifestBindingV1 {
4199    pub format_version: u16,
4200    pub shard_key: ShardKey,
4201    pub admission_generation: AdmissionGeneration,
4202    pub routing_revision: RoutingRevision,
4203    pub data_backend_authority: BackendAuthorityId,
4204    pub generation: ManifestGeneration,
4205    pub fence: FenceToken,
4206    pub primary_term: PrimaryTerm,
4207    pub manifest: ObjectIdentity,
4208    pub root_set: RootSetRef,
4209    pub committed_through: SeqNo,
4210    pub decode_provenance: PinnedManifestDecodeProvenanceV1,
4211}
4212
4213impl PinnedManifestBindingV1 {
4214    pub fn validate(&self) -> Result<(), PinnedManifestBindingError> {
4215        if self.format_version != PINNED_MANIFEST_BINDING_FORMAT_VERSION {
4216            return Err(PinnedManifestBindingError::UnsupportedVersion(self.format_version));
4217        }
4218        if self.admission_generation.get() == 0
4219            || self.routing_revision.get() == 0
4220            || self.generation.get() == 0
4221            || self.fence.epoch().get() == 0
4222            || self.fence.epoch() != self.primary_term.epoch_component()
4223        {
4224            return Err(PinnedManifestBindingError::InvalidSemanticIdentity);
4225        }
4226        self.manifest
4227            .validate()
4228            .map_err(|error| PinnedManifestBindingError::InvalidObject(error.to_string()))?;
4229        self.root_set
4230            .validate()
4231            .map_err(|error| PinnedManifestBindingError::InvalidRootSet(error.to_string()))?;
4232        if self.manifest.physical.backend_authority != self.data_backend_authority
4233            || self.root_set.root.backend_authority != self.data_backend_authority
4234        {
4235            return Err(PinnedManifestBindingError::BackendAuthorityMismatch);
4236        }
4237        Ok(())
4238    }
4239}
4240
4241/// Stable identity of one exact committed vector-search cut.
4242///
4243/// The digest binds the logical index/mapping incarnation and every immutable
4244/// manifest/root visible to the vector branch. Callers must supply roots in
4245/// ascending shard order so the same cut has exactly one identity.
4246pub fn vector_snapshot_digest_v1(
4247    scope: CollectionIndexRef,
4248    schema_generation: SchemaGeneration,
4249    roots: &[PinnedManifestBindingV1],
4250) -> Result<[u8; 32], PinnedManifestBindingError> {
4251    if schema_generation.get() == 0
4252        || roots
4253            .iter()
4254            .any(|root| root.shard_key.collection_index_ref() != scope || root.validate().is_err())
4255        || roots
4256            .windows(2)
4257            .any(|pair| pair[0].shard_key.shard_id() >= pair[1].shard_key.shard_id())
4258    {
4259        return Err(PinnedManifestBindingError::InvalidSemanticIdentity);
4260    }
4261    let mut hasher = blake3::Hasher::new();
4262    hasher.update(b"lodestone-vector-snapshot-v1\0");
4263    hasher.update(&scope.collection_id().get().to_be_bytes());
4264    hasher.update(&scope.index_id().get().to_be_bytes());
4265    hasher.update(&schema_generation.get().to_be_bytes());
4266    let count = u32::try_from(roots.len()).map_err(|_| PinnedManifestBindingError::InvalidSemanticIdentity)?;
4267    hasher.update(&count.to_be_bytes());
4268    for root in roots {
4269        hasher.update(&root.shard_key.shard_id().get().to_be_bytes());
4270        hasher.update(&root.admission_generation.get().to_be_bytes());
4271        hasher.update(&root.routing_revision.get().to_be_bytes());
4272        hasher.update(&root.generation.get().to_be_bytes());
4273        hasher.update(&root.fence.epoch().get().to_be_bytes());
4274        hasher.update(&root.primary_term.get().to_be_bytes());
4275        hasher.update(&root.manifest.content_checksum);
4276        hasher.update(&root.root_set.content_digest);
4277        hasher.update(&root.committed_through.get().to_be_bytes());
4278    }
4279    Ok(*hasher.finalize().as_bytes())
4280}
4281
4282#[derive(Clone, Debug, Eq, PartialEq)]
4283pub enum PinnedManifestBindingError {
4284    UnsupportedVersion(u16),
4285    InvalidSemanticIdentity,
4286    InvalidObject(String),
4287    InvalidRootSet(String),
4288    BackendAuthorityMismatch,
4289}
4290
4291impl Display for PinnedManifestBindingError {
4292    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
4293        match self {
4294            Self::UnsupportedVersion(version) => {
4295                write!(formatter, "unsupported pinned manifest binding version {version}")
4296            }
4297            Self::InvalidSemanticIdentity => formatter.write_str("pinned manifest semantic identity is invalid"),
4298            Self::InvalidObject(reason) => write!(formatter, "pinned manifest object is invalid: {reason}"),
4299            Self::InvalidRootSet(reason) => write!(formatter, "pinned manifest root set is invalid: {reason}"),
4300            Self::BackendAuthorityMismatch => formatter.write_str("pinned manifest authorities do not match"),
4301        }
4302    }
4303}
4304
4305impl Error for PinnedManifestBindingError {}
4306
4307/// Identifier validation failure.
4308#[derive(Clone, Debug, Eq, PartialEq)]
4309pub enum IdentifierError {
4310    EmptyDocumentId,
4311    DocumentIdTooLong(usize),
4312    EmptyIdempotencyKey,
4313    IdempotencyKeyTooLong(usize),
4314    EmptyPitId,
4315    PitIdTooLong(usize),
4316    UnsupportedScopedPitIdVersion(u16),
4317    EmptyObjectKey,
4318    InvalidObjectKey(String),
4319}
4320
4321impl Display for IdentifierError {
4322    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
4323        match self {
4324            Self::EmptyDocumentId => formatter.write_str("document id must not be empty"),
4325            Self::DocumentIdTooLong(length) => {
4326                write!(formatter, "document id is {length} bytes; maximum is 512")
4327            }
4328            Self::EmptyIdempotencyKey => formatter.write_str("idempotency key must not be empty"),
4329            Self::IdempotencyKeyTooLong(length) => {
4330                write!(formatter, "idempotency key is {length} bytes; maximum is 256")
4331            }
4332            Self::EmptyPitId => formatter.write_str("PIT id must not be empty"),
4333            Self::PitIdTooLong(length) => {
4334                write!(formatter, "PIT id is {length} bytes; maximum is 512")
4335            }
4336            Self::UnsupportedScopedPitIdVersion(version) => {
4337                write!(formatter, "unsupported scoped PIT identifier version {version}")
4338            }
4339            Self::EmptyObjectKey => formatter.write_str("object key must not be empty"),
4340            Self::InvalidObjectKey(key) => {
4341                write!(formatter, "invalid relative object key '{key}'")
4342            }
4343        }
4344    }
4345}
4346
4347impl Error for IdentifierError {}
4348
4349/// Stable P0 routing hash stored as `_rh`.
4350#[must_use]
4351pub const fn routing_hash_v1(bytes: &[u8]) -> RoutingHash {
4352    let mut hash = 0x811c_9dc5_u32;
4353    let mut offset = 0;
4354    while offset < bytes.len() {
4355        hash ^= bytes[offset] as u32;
4356        hash = hash.wrapping_mul(0x0100_0193);
4357        offset += 1;
4358    }
4359    RoutingHash::new(hash)
4360}
4361
4362/// Stable P0 BLAKE3-derived mutable identity hash stored as two u64 fast fields.
4363#[must_use]
4364pub fn identity_hash_v1(bytes: &[u8]) -> IdHash {
4365    let digest = blake3::hash(bytes);
4366    let digest_bytes = digest.as_bytes();
4367    IdHash::new(u128::from_le_bytes([
4368        digest_bytes[0],
4369        digest_bytes[1],
4370        digest_bytes[2],
4371        digest_bytes[3],
4372        digest_bytes[4],
4373        digest_bytes[5],
4374        digest_bytes[6],
4375        digest_bytes[7],
4376        digest_bytes[8],
4377        digest_bytes[9],
4378        digest_bytes[10],
4379        digest_bytes[11],
4380        digest_bytes[12],
4381        digest_bytes[13],
4382        digest_bytes[14],
4383        digest_bytes[15],
4384    ]))
4385}
4386
4387#[cfg(test)]
4388mod policy_axis_tests {
4389    use super::{LifecycleVerb, StorageClass};
4390
4391    #[test]
4392    fn reserved_policy_axis_names_are_stable() {
4393        assert!(serde_json::to_string(&StorageClass::Standard).is_ok_and(|value| value == "\"standard\""));
4394        assert!(serde_json::to_string(&StorageClass::Cold).is_ok_and(|value| value == "\"cold\""));
4395        assert!(serde_json::to_string(&LifecycleVerb::Transition).is_ok_and(|value| value == "\"transition\""));
4396        assert!(
4397            serde_json::to_string(&LifecycleVerb::AggregateThenDelete)
4398                .is_ok_and(|value| value == "\"aggregate_then_delete\"")
4399        );
4400    }
4401}
4402
4403/// Computes CRC32C (Castagnoli) without platform-specific unsafe code.
4404#[must_use]
4405pub fn crc32c(bytes: &[u8]) -> u32 {
4406    let mut crc = u32::MAX;
4407    for byte in bytes {
4408        crc ^= u32::from(*byte);
4409        for _ in 0..8 {
4410            let mask = (crc & 1).wrapping_neg();
4411            crc = (crc >> 1) ^ (0x82f6_3b78 & mask);
4412        }
4413    }
4414    !crc
4415}
4416
4417/// A runtime role that can be composed into the Lodestone process.
4418#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
4419pub enum Role {
4420    Gateway,
4421    Indexer,
4422    Searcher,
4423    Compactor,
4424    Metastore,
4425}
4426
4427impl Role {
4428    /// All roles in stable display and startup order.
4429    pub const ALL: [Self; 5] = [
4430        Self::Gateway,
4431        Self::Indexer,
4432        Self::Searcher,
4433        Self::Compactor,
4434        Self::Metastore,
4435    ];
4436
4437    /// The stable CLI name of this role.
4438    #[must_use]
4439    pub const fn as_str(self) -> &'static str {
4440        match self {
4441            Self::Gateway => "gateway",
4442            Self::Indexer => "indexer",
4443            Self::Searcher => "searcher",
4444            Self::Compactor => "compactor",
4445            Self::Metastore => "metastore",
4446        }
4447    }
4448}
4449
4450impl Display for Role {
4451    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
4452        formatter.write_str(self.as_str())
4453    }
4454}
4455
4456impl FromStr for Role {
4457    type Err = RoleParseError;
4458
4459    fn from_str(value: &str) -> Result<Self, Self::Err> {
4460        match value {
4461            "gateway" => Ok(Self::Gateway),
4462            "indexer" => Ok(Self::Indexer),
4463            "searcher" => Ok(Self::Searcher),
4464            "compactor" => Ok(Self::Compactor),
4465            "metastore" => Ok(Self::Metastore),
4466            _ => Err(RoleParseError::Unknown(value.to_owned())),
4467        }
4468    }
4469}
4470
4471/// A validated, non-empty set of runtime roles.
4472#[derive(Clone, Debug, Eq, PartialEq)]
4473pub struct RoleSelection {
4474    roles: Vec<Role>,
4475}
4476
4477impl RoleSelection {
4478    /// Selects every runtime role.
4479    #[must_use]
4480    pub fn all() -> Self {
4481        Self {
4482            roles: Role::ALL.to_vec(),
4483        }
4484    }
4485
4486    /// Parses `all` or a comma-separated role list.
4487    pub fn parse(value: &str) -> Result<Self, RoleParseError> {
4488        if value == "all" {
4489            return Ok(Self::all());
4490        }
4491
4492        let mut roles = Vec::new();
4493        for candidate in value.split(',') {
4494            if candidate.is_empty() {
4495                return Err(RoleParseError::Empty);
4496            }
4497
4498            let role = candidate.parse::<Role>()?;
4499            if !roles.contains(&role) {
4500                roles.push(role);
4501            }
4502        }
4503
4504        if roles.is_empty() {
4505            return Err(RoleParseError::Empty);
4506        }
4507
4508        roles.sort_unstable();
4509        Ok(Self { roles })
4510    }
4511
4512    /// Returns roles in stable order.
4513    #[must_use]
4514    pub fn roles(&self) -> &[Role] {
4515        &self.roles
4516    }
4517
4518    /// Returns a stable comma-separated representation.
4519    #[must_use]
4520    pub fn display_list(&self) -> String {
4521        self.roles
4522            .iter()
4523            .map(|role| role.as_str())
4524            .collect::<Vec<_>>()
4525            .join(",")
4526    }
4527}
4528
4529impl Default for RoleSelection {
4530    fn default() -> Self {
4531        Self::all()
4532    }
4533}
4534
4535/// Failure to parse a runtime role selection.
4536#[derive(Clone, Debug, Eq, PartialEq)]
4537pub enum RoleParseError {
4538    Empty,
4539    Unknown(String),
4540}
4541
4542impl Display for RoleParseError {
4543    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
4544        match self {
4545            Self::Empty => formatter.write_str("role selection must not be empty"),
4546            Self::Unknown(role) => write!(
4547                formatter,
4548                "unknown role '{role}'; expected all, gateway, indexer, searcher, compactor, or metastore"
4549            ),
4550        }
4551    }
4552}
4553
4554impl Error for RoleParseError {}
4555
4556/// Arms a bounded external SIGKILL handshake at a named P1.5 durability
4557/// boundary.
4558///
4559/// The hook is inert unless all three `LOD_TEST_P15_FAULT_*` variables are
4560/// present. A live harness observes the ready file and kills the child; the
4561/// deadline prevents a misconfigured harness from wedging production code.
4562pub fn process_fault_point(point: &str) {
4563    process_fault_point_inner(point, None);
4564}
4565
4566/// Arms the same bounded process-fault handshake while attaching one small,
4567/// printable fact from the durable transition that reached the boundary.
4568///
4569/// Evidence is test-only observability, never protocol authority. Invalid or
4570/// oversized evidence refuses to publish the handshake so a harness fails by
4571/// timeout instead of accepting a truncated or ambiguous fact.
4572pub fn process_fault_point_with_evidence(point: &str, evidence: &str) {
4573    const MAX_EVIDENCE_BYTES: usize = 512;
4574    if evidence.is_empty()
4575        || evidence.len() > MAX_EVIDENCE_BYTES
4576        || !evidence
4577            .bytes()
4578            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'=' | b'.'))
4579    {
4580        return;
4581    }
4582    process_fault_point_inner(point, Some(evidence));
4583}
4584
4585/// Test-only path receiving one exact P5.2 GC protection observation.
4586///
4587/// Unlike the process-fault handshake, this side channel never waits for a
4588/// release file. The Ready executor publishes it before advancing beyond the
4589/// observed protection boundary, and later fault hooks remain independently
4590/// armed.
4591pub const GC_PROTECTION_OBSERVATION_PATH_ENV: &str = "LOD_TEST_P52_GC_PROTECTION_OBSERVATION_PATH";
4592
4593/// Hard ceiling for one test-only GC protection observation.
4594pub const MAX_GC_PROTECTION_OBSERVATION_BYTES: usize = 8 * 1024 * 1024;
4595
4596/// Failure to publish one bounded test-only GC protection observation.
4597#[derive(Debug)]
4598pub enum GcProtectionObservationError {
4599    InvalidPath,
4600    Empty,
4601    TooLarge {
4602        actual: usize,
4603        maximum: usize,
4604    },
4605    AlreadyPublished(PathBuf),
4606    Io {
4607        operation: &'static str,
4608        path: PathBuf,
4609        source: std::io::Error,
4610    },
4611}
4612
4613impl Display for GcProtectionObservationError {
4614    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
4615        match self {
4616            Self::InvalidPath => formatter.write_str("GC protection observation path has no file name"),
4617            Self::Empty => formatter.write_str("GC protection observation is empty"),
4618            Self::TooLarge { actual, maximum } => write!(
4619                formatter,
4620                "GC protection observation is {actual} bytes; maximum is {maximum}"
4621            ),
4622            Self::AlreadyPublished(path) => {
4623                write!(
4624                    formatter,
4625                    "GC protection observation was already published at {}",
4626                    path.display()
4627                )
4628            }
4629            Self::Io {
4630                operation,
4631                path,
4632                source,
4633            } => write!(
4634                formatter,
4635                "GC protection observation {operation} failed for {}: {source}",
4636                path.display()
4637            ),
4638        }
4639    }
4640}
4641
4642impl Error for GcProtectionObservationError {
4643    fn source(&self) -> Option<&(dyn Error + 'static)> {
4644        match self {
4645            Self::Io { source, .. } => Some(source),
4646            _ => None,
4647        }
4648    }
4649}
4650
4651/// Returns the optional test-only observation destination.
4652#[must_use]
4653pub fn gc_protection_observation_path() -> Option<PathBuf> {
4654    std::env::var_os(GC_PROTECTION_OBSERVATION_PATH_ENV)
4655        .filter(|value| !value.is_empty())
4656        .map(PathBuf::from)
4657}
4658
4659/// Atomically publishes one bounded observation without replacing an older
4660/// one.
4661///
4662/// Callers must execute this blocking filesystem helper on a bounded blocking
4663/// pool. The completed temporary file is linked into place only after every
4664/// byte is written and synced, so readers never observe a partial record.
4665pub fn write_gc_protection_observation(path: &Path, encoded: &[u8]) -> Result<(), GcProtectionObservationError> {
4666    if encoded.is_empty() {
4667        return Err(GcProtectionObservationError::Empty);
4668    }
4669    if encoded.len() > MAX_GC_PROTECTION_OBSERVATION_BYTES {
4670        return Err(GcProtectionObservationError::TooLarge {
4671            actual: encoded.len(),
4672            maximum: MAX_GC_PROTECTION_OBSERVATION_BYTES,
4673        });
4674    }
4675    let file_name = path
4676        .file_name()
4677        .and_then(|name| name.to_str())
4678        .filter(|name| !name.is_empty())
4679        .ok_or(GcProtectionObservationError::InvalidPath)?;
4680    let temporary = path.with_file_name(format!(".{file_name}.{}.tmp", std::process::id()));
4681    let mut file = std::fs::OpenOptions::new()
4682        .write(true)
4683        .create_new(true)
4684        .open(&temporary)
4685        .map_err(|source| GcProtectionObservationError::Io {
4686            operation: "temporary-file creation",
4687            path: temporary.clone(),
4688            source,
4689        })?;
4690    let write_result = file.write_all(encoded).and_then(|()| file.sync_all());
4691    drop(file);
4692    if let Err(source) = write_result {
4693        let _cleanup_result = std::fs::remove_file(&temporary);
4694        return Err(GcProtectionObservationError::Io {
4695            operation: "temporary-file write",
4696            path: temporary,
4697            source,
4698        });
4699    }
4700    if let Err(source) = std::fs::hard_link(&temporary, path) {
4701        let _cleanup_result = std::fs::remove_file(&temporary);
4702        if source.kind() == std::io::ErrorKind::AlreadyExists {
4703            return Err(GcProtectionObservationError::AlreadyPublished(path.to_path_buf()));
4704        }
4705        return Err(GcProtectionObservationError::Io {
4706            operation: "atomic publication",
4707            path: path.to_path_buf(),
4708            source,
4709        });
4710    }
4711    std::fs::remove_file(&temporary).map_err(|source| GcProtectionObservationError::Io {
4712        operation: "temporary-file cleanup",
4713        path: temporary,
4714        source,
4715    })?;
4716    Ok(())
4717}
4718
4719fn process_fault_point_inner(point: &str, evidence: Option<&str>) {
4720    struct FaultConfig {
4721        point: String,
4722        ready_path: PathBuf,
4723        release_path: PathBuf,
4724        timeout: Duration,
4725    }
4726    static CONFIG: OnceLock<Option<FaultConfig>> = OnceLock::new();
4727    let config = CONFIG.get_or_init(|| {
4728        let point = std::env::var("LOD_TEST_LISTENER_FAULT_POINT").ok()?;
4729        let ready_path = std::env::var_os("LOD_TEST_LISTENER_FAULT_READY").map(PathBuf::from)?;
4730        let release_path = std::env::var_os("LOD_TEST_LISTENER_FAULT_RELEASE").map(PathBuf::from)?;
4731        let timeout_millis = std::env::var("LOD_TEST_LISTENER_FAULT_TIMEOUT_MILLIS")
4732            .ok()
4733            .and_then(|value| value.parse::<u64>().ok())
4734            .unwrap_or(30_000)
4735            .min(60_000);
4736        Some(FaultConfig {
4737            point,
4738            ready_path,
4739            release_path,
4740            timeout: Duration::from_millis(timeout_millis),
4741        })
4742    });
4743    let Some(config) = config.as_ref().filter(|config| config.point == point) else {
4744        return;
4745    };
4746    let payload = evidence.map_or_else(|| point.to_owned(), |fact| format!("{point}\n{fact}"));
4747    if std::fs::write(&config.ready_path, payload.as_bytes()).is_err() {
4748        return;
4749    }
4750    let deadline = Instant::now() + config.timeout;
4751    while !config.release_path.exists() && Instant::now() < deadline {
4752        std::thread::yield_now();
4753    }
4754}
4755
4756#[cfg(test)]
4757mod hash_backend_tests;
4758
4759#[cfg(test)]
4760mod tests {
4761    use std::collections::BTreeMap;
4762    use std::sync::atomic::{AtomicU64, Ordering};
4763
4764    use super::{
4765        CollectionId, CollectionIndexRef, DocumentId, DurabilityClass, DynamicMappingProposalError,
4766        DynamicMappingProposalV1, Epoch, FieldMapping, FieldType, GcProtectionObservationError, IndexId, IndexMode,
4767        MAPPING_FORMAT_VERSION, MAX_GC_PROTECTION_OBSERVATION_BYTES, MAX_MAPPING_FIELD_NAME_BYTES, MAX_MAPPING_FIELDS,
4768        ManifestGeneration, Mapping, MappingExtensions, MappingScalar, MappingValidationError, ObjectKey, PitId, Role,
4769        RoleSelection, RoutingHash, RoutingRangeEnd, SchemaGeneration, ScopedPitId, SeqNo, ShardDescriptor, ShardId,
4770        TaskId, VisibilityMode, crc32c, validate_mapping_field_name, write_gc_protection_observation,
4771    };
4772
4773    static OBSERVATION_TEST_SEQUENCE: AtomicU64 = AtomicU64::new(0);
4774
4775    #[test]
4776    fn object_key_memory_charge_preserves_spare_string_capacity() -> Result<(), Box<dyn std::error::Error>> {
4777        let mut value = String::with_capacity(4096);
4778        value.push_str("a/b");
4779        let capacity = value.capacity();
4780        let key = ObjectKey::new(value)?;
4781        assert_eq!(key.as_str(), "a/b");
4782        assert_eq!(key.allocated_capacity_bytes(), capacity);
4783        assert!(key.allocated_capacity_bytes() > key.as_str().len());
4784        assert_eq!(serde_json::to_string(&key)?, "\"a/b\"");
4785        Ok(())
4786    }
4787
4788    #[test]
4789    fn all_expands_to_every_role() {
4790        let selection = RoleSelection::parse("all");
4791
4792        assert_eq!(selection.as_ref().map(|value| value.roles()), Ok(Role::ALL.as_slice()));
4793    }
4794
4795    #[test]
4796    fn explicit_roles_are_deduplicated_and_stable() {
4797        let selection = RoleSelection::parse("searcher,gateway,searcher");
4798
4799        assert_eq!(
4800            selection.as_ref().map(|value| value.roles()),
4801            Ok([Role::Gateway, Role::Searcher].as_slice())
4802        );
4803    }
4804
4805    #[test]
4806    fn unknown_role_is_rejected() {
4807        let selection = RoleSelection::parse("unknown");
4808
4809        assert!(selection.is_err());
4810    }
4811
4812    #[test]
4813    fn semantic_numbers_advance_without_crossing_types() {
4814        assert_eq!(Epoch::new(7).checked_next(), Some(Epoch::new(8)));
4815        assert_eq!(SeqNo::new(41).checked_next(), Some(SeqNo::new(42)));
4816        assert_eq!(ManifestGeneration::new(u64::MAX).checked_next(), None);
4817    }
4818
4819    #[test]
4820    fn sparse_values_are_canonical_bounded_and_mapping_typed() -> Result<(), Box<dyn std::error::Error>> {
4821        let definition = super::SparseVectorDefinition {
4822            maximum_dimension: 12,
4823            weight_type: super::SparseVectorWeightType::F16,
4824        };
4825        let value = super::SparseVectorValue {
4826            entries: vec![
4827                super::SparseVectorEntry {
4828                    dimension: 1,
4829                    weight: 1.5,
4830                },
4831                super::SparseVectorEntry {
4832                    dimension: 9,
4833                    weight: -2.0,
4834                },
4835            ],
4836        };
4837        let decoded = value.decode(definition)?;
4838        let native = value.canonical_native_bytes(definition)?;
4839        assert_eq!(super::decode_sparse_native_bytes(definition, &native)?, decoded);
4840        assert_eq!(native.len(), 12);
4841        let encoded = serde_json::to_vec(&value)?;
4842        assert_eq!(serde_json::from_slice::<super::SparseVectorValue>(&encoded)?, value);
4843        #[derive(Debug, serde::Deserialize, PartialEq, serde::Serialize)]
4844        #[serde(tag = "kind", rename_all = "snake_case")]
4845        enum TaggedSparse {
4846            Sparse {
4847                query_vector: super::SparseVectorValue,
4848                #[serde(deserialize_with = "super::deserialize_sparse_weight")]
4849                weight: f64,
4850            },
4851        }
4852        let tagged = TaggedSparse::Sparse {
4853            query_vector: value.clone(),
4854            weight: 0.5,
4855        };
4856        let encoded = serde_json::to_vec(&tagged)?;
4857        assert_eq!(serde_json::from_slice::<TaggedSparse>(&encoded)?, tagged);
4858
4859        for invalid in [
4860            super::SparseVectorValue {
4861                entries: vec![
4862                    super::SparseVectorEntry {
4863                        dimension: 1,
4864                        weight: 1.0,
4865                    },
4866                    super::SparseVectorEntry {
4867                        dimension: 1,
4868                        weight: 2.0,
4869                    },
4870                ],
4871            },
4872            super::SparseVectorValue {
4873                entries: vec![super::SparseVectorEntry {
4874                    dimension: 2,
4875                    weight: 0.0,
4876                }],
4877            },
4878            super::SparseVectorValue {
4879                entries: vec![super::SparseVectorEntry {
4880                    dimension: 13,
4881                    weight: 1.0,
4882                }],
4883            },
4884            super::SparseVectorValue {
4885                entries: vec![super::SparseVectorEntry {
4886                    dimension: 2,
4887                    weight: f64::NAN,
4888                }],
4889            },
4890        ] {
4891            assert!(invalid.decode(definition).is_err());
4892        }
4893        Ok(())
4894    }
4895
4896    #[test]
4897    fn vector_set_values_are_explicit_typed_and_bounded() -> Result<(), Box<dyn std::error::Error>> {
4898        let definition = super::VectorSetDefinition {
4899            dense: super::DenseVectorDefinition {
4900                dimensions: 2,
4901                element_type: super::DenseVectorElementType::F32,
4902                metric: super::VectorMetric::DotProduct,
4903                quantization_scale: None,
4904            },
4905            maximum_vectors: 2,
4906        };
4907        let value = super::VectorSetValue {
4908            vectors: vec![
4909                super::DenseVectorValue::Numeric { values: vec![1.0, 0.0] },
4910                super::DenseVectorValue::Numeric { values: vec![0.0, 1.0] },
4911            ],
4912        };
4913        let decoded = value.decode(
4914            definition,
4915            super::MAX_VECTOR_SET_DOCUMENT_VECTORS,
4916            super::MAX_VECTOR_SET_BYTES_PER_FIELD,
4917        )?;
4918        assert_eq!(decoded.vectors, vec![vec![1.0, 0.0], vec![0.0, 1.0]]);
4919        assert_eq!(decoded.native_bytes, 16);
4920        assert!(
4921            super::VectorSetValue { vectors: Vec::new() }
4922                .decode(definition, 2, super::MAX_VECTOR_SET_BYTES_PER_FIELD)
4923                .is_err()
4924        );
4925        assert!(
4926            super::VectorSetValue {
4927                vectors: vec![super::DenseVectorValue::Numeric { values: vec![1.0, 0.0] }; 3],
4928            }
4929            .decode(definition, 2, super::MAX_VECTOR_SET_BYTES_PER_FIELD)
4930            .is_err()
4931        );
4932        assert!(
4933            super::VectorSetValue {
4934                vectors: vec![super::DenseVectorValue::Numeric { values: vec![1.0] }],
4935            }
4936            .decode(definition, 2, super::MAX_VECTOR_SET_BYTES_PER_FIELD)
4937            .is_err()
4938        );
4939        let oversized_json = serde_json::json!({
4940            "vectors": (0..=super::MAX_VECTOR_SET_DOCUMENT_VECTORS)
4941                .map(|_| serde_json::json!({"kind": "numeric", "values": [1.0, 0.0]}))
4942                .collect::<Vec<_>>()
4943        });
4944        assert!(serde_json::from_value::<super::VectorSetValue>(oversized_json).is_err());
4945        Ok(())
4946    }
4947
4948    #[test]
4949    fn task_ids_round_trip_the_complete_u128_domain_and_reject_numbers() {
4950        let task_id = TaskId::new(u128::MAX - 7);
4951        let encoded = serde_json::to_string(&task_id);
4952        let expected = format!("\"{}\"", u128::MAX - 7);
4953        assert_eq!(encoded.as_ref().ok().map(String::as_str), Some(expected.as_str()));
4954        assert_eq!(
4955            encoded
4956                .as_deref()
4957                .ok()
4958                .and_then(|encoded| serde_json::from_str(encoded).ok()),
4959            Some(task_id)
4960        );
4961        assert!(serde_json::from_str::<TaskId>("41").is_err());
4962    }
4963
4964    #[test]
4965    fn gc_protection_observation_writer_is_bounded_atomic_and_single_assignment()
4966    -> Result<(), Box<dyn std::error::Error>> {
4967        let sequence = OBSERVATION_TEST_SEQUENCE.fetch_add(1, Ordering::Relaxed);
4968        let root = std::env::temp_dir().join(format!(
4969            "lodestone-p52-gc-protection-observation-{}-{sequence}",
4970            std::process::id()
4971        ));
4972        std::fs::create_dir(&root)?;
4973        let path = root.join("applied.json");
4974        let encoded = br#"{"format_version":1}"#;
4975
4976        write_gc_protection_observation(&path, encoded)?;
4977        assert_eq!(std::fs::read(&path)?, encoded);
4978        assert!(matches!(
4979            write_gc_protection_observation(&path, encoded),
4980            Err(GcProtectionObservationError::AlreadyPublished(actual)) if actual == path
4981        ));
4982        assert!(matches!(
4983            write_gc_protection_observation(
4984                &root.join("oversized.json"),
4985                &vec![0; MAX_GC_PROTECTION_OBSERVATION_BYTES.saturating_add(1)]
4986            ),
4987            Err(GcProtectionObservationError::TooLarge { .. })
4988        ));
4989        assert!(!root.join("oversized.json").exists());
4990
4991        std::fs::remove_dir_all(root)?;
4992        Ok(())
4993    }
4994
4995    #[test]
4996    fn durable_identifiers_reject_ambiguous_values() {
4997        assert!(DocumentId::new("").is_err());
4998        assert!(PitId::new("").is_err());
4999        assert!(PitId::new("opaque-pit-lease").is_ok());
5000        assert!(ObjectKey::new("../escape").is_err());
5001        assert!(ObjectKey::new("valid/prefix/object").is_ok());
5002    }
5003
5004    #[test]
5005    fn scoped_pit_id_round_trips_and_rejects_newer_formats() -> Result<(), Box<dyn std::error::Error>> {
5006        let pit_id = ScopedPitId::new(super::RecoveryEra::new(7)?, PitId::new("opaque-pit-lease")?);
5007        let encoded = serde_json::to_vec(&pit_id)?;
5008        assert_eq!(serde_json::from_slice::<ScopedPitId>(&encoded)?, pit_id);
5009
5010        let mut unsupported: serde_json::Value = serde_json::from_slice(&encoded)?;
5011        let Some(object) = unsupported.as_object_mut() else {
5012            return Err("scoped PIT identifier was not an object".into());
5013        };
5014        object.insert("format_version".to_owned(), serde_json::json!(2));
5015        assert!(serde_json::from_value::<ScopedPitId>(unsupported).is_err());
5016        Ok(())
5017    }
5018
5019    #[test]
5020    fn crc32c_matches_the_standard_check_value() {
5021        assert_eq!(crc32c(b"123456789"), 0xe306_9283);
5022    }
5023
5024    #[test]
5025    fn durability_names_round_trip_and_admitted_classes_are_available() {
5026        let classes = [
5027            DurabilityClass::RegionalObject,
5028            DurabilityClass::ZonalLowLatency,
5029            DurabilityClass::Relaxed,
5030            DurabilityClass::Ephemeral,
5031        ];
5032        for class in classes {
5033            let encoded = serde_json::to_string(&class);
5034            assert_eq!(
5035                encoded.as_ref().ok().and_then(|bytes| serde_json::from_str(bytes).ok()),
5036                Some(class)
5037            );
5038        }
5039        assert!(DurabilityClass::RegionalObject.require_available().is_ok());
5040        assert!(DurabilityClass::ZonalLowLatency.require_available().is_ok());
5041        assert!(DurabilityClass::Relaxed.require_available().is_err());
5042        assert!(DurabilityClass::Ephemeral.require_available().is_err());
5043        assert!(DurabilityClass::RegionalObject.require_state_available().is_ok());
5044        assert!(DurabilityClass::ZonalLowLatency.require_state_available().is_ok());
5045        assert!(DurabilityClass::Ephemeral.require_state_available().is_ok());
5046        assert!(DurabilityClass::Relaxed.require_state_available().is_err());
5047        assert!(
5048            serde_json::to_string(&super::DurabilityBoundary::IncarnationMemory)
5049                .is_ok_and(|encoded| encoded == "\"incarnation_memory\"")
5050        );
5051        assert!(serde_json::from_str::<DurabilityClass>("\"future\"").is_err());
5052        assert!(matches!(
5053            serde_json::from_str::<VisibilityMode>("\"wait_for\""),
5054            Ok(VisibilityMode::WaitFor)
5055        ));
5056    }
5057
5058    #[test]
5059    fn shard_descriptor_covers_exactly_its_hash_range() {
5060        let descriptor = ShardDescriptor::new(
5061            ShardId::new(7),
5062            Epoch::new(3),
5063            RoutingHash::new(10),
5064            RoutingRangeEnd::new(20).unwrap_or(RoutingRangeEnd::FULL),
5065        );
5066        assert!(descriptor.is_ok());
5067        let descriptor = descriptor.unwrap_or(ShardDescriptor {
5068            shard_id: ShardId::new(7),
5069            epoch: Epoch::new(3),
5070            hash_start: RoutingHash::new(10),
5071            hash_end: RoutingRangeEnd::FULL,
5072        });
5073        assert!(!descriptor.contains(RoutingHash::new(9)));
5074        assert!(descriptor.contains(RoutingHash::new(10)));
5075        assert!(descriptor.contains(RoutingHash::new(19)));
5076        assert!(!descriptor.contains(RoutingHash::new(20)));
5077    }
5078
5079    #[test]
5080    fn shared_mapping_model_validates_semantics_and_refuses_retired_versions_and_unknown_fields()
5081    -> Result<(), Box<dyn std::error::Error>> {
5082        let mapping = Mapping {
5083            format_version: MAPPING_FORMAT_VERSION,
5084            schema_generation: SchemaGeneration::new(1),
5085            mode: IndexMode::Mutable,
5086            fields: BTreeMap::from([(
5087                "message".to_owned(),
5088                FieldMapping {
5089                    field_type: FieldType::Text,
5090                    stored: false,
5091                    null_value: None,
5092                },
5093            )]),
5094            extensions: crate::MappingExtensions::default(),
5095        };
5096        assert_eq!(mapping.validate(), Ok(()));
5097        assert_eq!(mapping.validate_current(), Ok(()));
5098        let canonical = serde_json::to_value(&mapping)?;
5099        for retired_version in 1..MAPPING_FORMAT_VERSION {
5100            let retired = Mapping {
5101                format_version: retired_version,
5102                ..mapping.clone()
5103            };
5104            assert_eq!(
5105                retired.validate(),
5106                Err(MappingValidationError::UnsupportedVersion(retired_version))
5107            );
5108            assert!(matches!(
5109                retired.encode_canonical(),
5110                Err(super::MappingCodecError::Invalid(
5111                    MappingValidationError::UnsupportedVersion(version)
5112                )) if version == retired_version
5113            ));
5114
5115            let mut retired_bytes = canonical.clone();
5116            retired_bytes["format_version"] = serde_json::json!(retired_version);
5117            let retired_bytes = serde_json::to_vec(&retired_bytes)?;
5118            let original = retired_bytes.clone();
5119            assert!(matches!(
5120                Mapping::decode_canonical(&retired_bytes),
5121                Err(super::MappingCodecError::UnsupportedVersion(version)) if version == retired_version
5122            ));
5123            assert_eq!(retired_bytes, original);
5124            assert!(serde_json::from_slice::<Mapping>(&retired_bytes).is_err());
5125        }
5126
5127        let future = Mapping {
5128            format_version: MAPPING_FORMAT_VERSION + 1,
5129            ..mapping.clone()
5130        };
5131        assert_eq!(
5132            future.validate(),
5133            Err(MappingValidationError::UnsupportedVersion(MAPPING_FORMAT_VERSION + 1))
5134        );
5135
5136        let invalid = Mapping {
5137            schema_generation: SchemaGeneration::new(0),
5138            ..mapping.clone()
5139        };
5140        assert_eq!(invalid.validate(), Err(MappingValidationError::ZeroSchemaGeneration));
5141        assert!(
5142            serde_json::from_str::<Mapping>(
5143                r#"{"format_version":1,"schema_generation":1,"mode":"mutable","fields":{},"future":true}"#
5144            )
5145            .is_err()
5146        );
5147        for invalid in [".message", "message.", "object..message"] {
5148            let mut invalid_path = mapping.clone();
5149            invalid_path.fields = BTreeMap::from([(
5150                invalid.to_owned(),
5151                FieldMapping {
5152                    field_type: FieldType::Text,
5153                    stored: false,
5154                    null_value: None,
5155                },
5156            )]);
5157            assert_eq!(
5158                invalid_path.validate(),
5159                Err(MappingValidationError::InvalidFieldName(invalid.to_owned()))
5160            );
5161        }
5162        for (ancestor, descendant) in [("a", "a.b"), ("a.b", "a.b.c")] {
5163            let mut conflicting = mapping.clone();
5164            conflicting.fields = BTreeMap::from([
5165                (
5166                    ancestor.to_owned(),
5167                    FieldMapping {
5168                        field_type: FieldType::Keyword,
5169                        stored: false,
5170                        null_value: None,
5171                    },
5172                ),
5173                (
5174                    descendant.to_owned(),
5175                    FieldMapping {
5176                        field_type: FieldType::U64,
5177                        stored: false,
5178                        null_value: None,
5179                    },
5180                ),
5181            ]);
5182            assert_eq!(
5183                conflicting.validate(),
5184                Err(MappingValidationError::ConflictingFieldPaths {
5185                    ancestor: ancestor.to_owned(),
5186                    descendant: descendant.to_owned(),
5187                })
5188            );
5189        }
5190        let mut siblings = mapping;
5191        siblings.fields = BTreeMap::from([
5192            (
5193                "a.b".to_owned(),
5194                FieldMapping {
5195                    field_type: FieldType::Keyword,
5196                    stored: false,
5197                    null_value: None,
5198                },
5199            ),
5200            (
5201                "a.bc".to_owned(),
5202                FieldMapping {
5203                    field_type: FieldType::U64,
5204                    stored: false,
5205                    null_value: None,
5206                },
5207            ),
5208        ]);
5209        assert_eq!(siblings.validate(), Ok(()));
5210        Ok(())
5211    }
5212
5213    #[test]
5214    fn ec6_mapping_refuses_nested_paths_below_nested_paths() {
5215        let nested = || FieldMapping {
5216            field_type: FieldType::Nested,
5217            stored: false,
5218            null_value: None,
5219        };
5220        let mapping = Mapping {
5221            format_version: MAPPING_FORMAT_VERSION,
5222            schema_generation: SchemaGeneration::new(1),
5223            mode: IndexMode::Mutable,
5224            fields: BTreeMap::from([
5225                ("items".to_owned(), nested()),
5226                ("items-options".to_owned(), nested()),
5227                ("items.options".to_owned(), nested()),
5228            ]),
5229            extensions: MappingExtensions {
5230                nested_paths: vec![
5231                    "items".to_owned(),
5232                    "items-options".to_owned(),
5233                    "items.options".to_owned(),
5234                ],
5235                ..MappingExtensions::default()
5236            },
5237        };
5238        assert_eq!(
5239            mapping.validate(),
5240            Err(MappingValidationError::NestedPathOverlap {
5241                parent: "items".to_owned(),
5242                child: "items.options".to_owned(),
5243            })
5244        );
5245
5246        let mut predecessor = Mapping::empty(IndexMode::Mutable);
5247        predecessor.fields.insert(
5248            "items.color".to_owned(),
5249            FieldMapping {
5250                field_type: FieldType::Keyword,
5251                stored: false,
5252                null_value: None,
5253            },
5254        );
5255        let mut successor = predecessor.clone();
5256        successor.schema_generation = SchemaGeneration::new(2);
5257        successor.fields.insert("items".to_owned(), nested());
5258        successor.extensions.nested_paths = vec!["items".to_owned()];
5259        assert!(!successor.is_additive_successor_of(&predecessor));
5260
5261        let mut safe_successor = predecessor.clone();
5262        safe_successor.schema_generation = SchemaGeneration::new(2);
5263        safe_successor.fields.insert("other".to_owned(), nested());
5264        safe_successor.fields.insert(
5265            "other.color".to_owned(),
5266            FieldMapping {
5267                field_type: FieldType::Keyword,
5268                stored: false,
5269                null_value: None,
5270            },
5271        );
5272        safe_successor.extensions.nested_paths = vec!["other".to_owned()];
5273        assert!(safe_successor.is_additive_successor_of(&predecessor));
5274    }
5275
5276    #[test]
5277    fn ec5_dynamic_mapping_proposal_normalizes_signed_zero_and_refuses_noncanonical_decode()
5278    -> Result<(), Box<dyn std::error::Error>> {
5279        let proposal = DynamicMappingProposalV1::new(
5280            CollectionIndexRef::new(CollectionId::new(7)?, IndexId::new(3)?),
5281            SchemaGeneration::new(1),
5282            "temperature".to_owned(),
5283            MappingScalar::F64Bits((-0.0_f64).to_bits()),
5284        )?;
5285        assert_eq!(
5286            proposal.first_non_null_candidate,
5287            MappingScalar::F64Bits(0.0_f64.to_bits())
5288        );
5289
5290        let mut noncanonical = proposal;
5291        noncanonical.first_non_null_candidate = MappingScalar::F64Bits((-0.0_f64).to_bits());
5292        noncanonical.proposal_digest = noncanonical.exact_digest()?;
5293        let encoded = serde_json::to_vec(&noncanonical)?;
5294        assert!(matches!(
5295            DynamicMappingProposalV1::decode_canonical(&encoded),
5296            Err(DynamicMappingProposalError::InvalidCandidate)
5297        ));
5298        Ok(())
5299    }
5300
5301    #[test]
5302    fn ec5_deployment_mapping_fixtures_use_the_current_validated_format() -> Result<(), Box<dyn std::error::Error>> {
5303        for encoded in [
5304            include_str!("../../../fixtures/runtime/secure-strict-mapping.json"),
5305            include_str!("../../../fixtures/performance/search-mapping.json"),
5306            include_str!("../../../fixtures/reindex/mapping.json"),
5307            include_str!("../../../tests/performance/es-comparison-mapping-v1.json"),
5308        ] {
5309            let mapping: Mapping = serde_json::from_str(encoded)?;
5310            mapping.validate_current()?;
5311            assert_eq!(mapping.schema_generation, SchemaGeneration::new(1));
5312        }
5313        Ok(())
5314    }
5315
5316    #[test]
5317    fn public_replayable_source_mapping_contract_matches_current_model() -> Result<(), serde_json::Error> {
5318        let contract: serde_json::Value = serde_json::from_str(include_str!(
5319            "../../../public/development/replayable-source-mapping-v1.json"
5320        ))?;
5321        assert_eq!(contract["mapping"]["format_version"], MAPPING_FORMAT_VERSION);
5322        assert_eq!(contract["mapping"]["maximum_fields"], MAX_MAPPING_FIELDS);
5323        assert_eq!(
5324            contract["mapping"]["maximum_field_name_bytes"],
5325            MAX_MAPPING_FIELD_NAME_BYTES
5326        );
5327        assert_eq!(
5328            contract["mapping"]["scalar_field_types"],
5329            serde_json::json!(["text", "keyword", "u64", "date", "i64", "f64", "bool"])
5330        );
5331
5332        let mapping = Mapping {
5333            format_version: MAPPING_FORMAT_VERSION,
5334            schema_generation: SchemaGeneration::new(1),
5335            mode: IndexMode::AppendLog,
5336            fields: BTreeMap::from([
5337                (
5338                    "message".to_owned(),
5339                    FieldMapping {
5340                        field_type: FieldType::Text,
5341                        stored: false,
5342                        null_value: None,
5343                    },
5344                ),
5345                (
5346                    "record_id".to_owned(),
5347                    FieldMapping {
5348                        field_type: FieldType::Keyword,
5349                        stored: false,
5350                        null_value: None,
5351                    },
5352                ),
5353            ]),
5354            extensions: crate::MappingExtensions::default(),
5355        };
5356        assert_eq!(
5357            serde_json::Value::String(serde_json::to_string(&mapping)?),
5358            contract["example"]
5359        );
5360        for invalid in ["", ".message", "message.", "object..message", "_reserved", "a\u{7f}b"] {
5361            assert_eq!(
5362                validate_mapping_field_name(invalid),
5363                Err(MappingValidationError::InvalidFieldName(invalid.to_owned()))
5364            );
5365        }
5366        Ok(())
5367    }
5368
5369    #[test]
5370    fn shared_field_path_resolver_uses_canonical_nested_components() {
5371        #[derive(Default)]
5372        struct Node {
5373            children: BTreeMap<&'static str, Node>,
5374            value: Option<u64>,
5375        }
5376
5377        let root = Node {
5378            children: BTreeMap::from([(
5379                "metrics",
5380                Node {
5381                    children: BTreeMap::from([(
5382                        "count",
5383                        Node {
5384                            children: BTreeMap::new(),
5385                            value: Some(7),
5386                        },
5387                    )]),
5388                    value: None,
5389                },
5390            )]),
5391            value: None,
5392        };
5393        assert_eq!(
5394            super::resolve_field_path(&root, "metrics.count", |node, component| node.children.get(component))
5395                .and_then(|node| node.value),
5396            Some(7)
5397        );
5398        assert!(
5399            super::resolve_field_path(&root, "metrics..count", |node, component| node.children.get(component))
5400                .is_none()
5401        );
5402    }
5403
5404    #[test]
5405    fn root_set_reference_is_exact_bounded_and_canonical() -> Result<(), Box<dyn std::error::Error>> {
5406        let root = super::RootSetRef {
5407            format_version: super::ROOT_SET_REF_FORMAT_VERSION,
5408            root: super::PhysicalObjectId {
5409                backend_authority: super::BackendAuthorityId::new("standard-data")?,
5410                object_incarnation: super::ObjectIncarnation::new(9)?,
5411                object_key: super::ObjectKey::new("root-sets/0009.json")?,
5412            },
5413            content_digest: [7; 32],
5414            item_count: 4,
5415            page_count: 2,
5416            schema_generations: vec![super::SchemaGeneration::new(1), super::SchemaGeneration::new(3)],
5417        };
5418        root.validate()?;
5419        assert_eq!(
5420            serde_json::from_slice::<super::RootSetRef>(&serde_json::to_vec(&root)?)?,
5421            root
5422        );
5423
5424        let mut duplicate = root.clone();
5425        duplicate.schema_generations = vec![super::SchemaGeneration::new(1), super::SchemaGeneration::new(1)];
5426        assert_eq!(
5427            duplicate.validate(),
5428            Err(super::RootSetRefError::InvalidSchemaGenerations)
5429        );
5430        let mut empty = root;
5431        empty.item_count = 0;
5432        assert_eq!(
5433            empty.validate(),
5434            Err(super::RootSetRefError::InvalidCounts { items: 0, pages: 2 })
5435        );
5436        let mut single_page = empty;
5437        single_page.item_count = 1;
5438        single_page.page_count = 1;
5439        single_page.validate()?;
5440        let mut too_many_pages = single_page.clone();
5441        too_many_pages.page_count = 2;
5442        assert_eq!(
5443            too_many_pages.validate(),
5444            Err(super::RootSetRefError::InvalidCounts { items: 1, pages: 2 })
5445        );
5446        let mut predecessor = single_page;
5447        predecessor.format_version = 1;
5448        assert_eq!(
5449            predecessor.validate(),
5450            Err(super::RootSetRefError::UnsupportedVersion(1))
5451        );
5452        Ok(())
5453    }
5454
5455    #[test]
5456    fn kubernetes_object_uid_digest_refuses_zero_on_construction_and_decode() -> Result<(), Box<dyn std::error::Error>>
5457    {
5458        assert!(super::KubernetesObjectUidDigest::new([0; 32]).is_err());
5459        assert!(
5460            serde_json::from_str::<super::KubernetesObjectUidDigest>(&format!("[{}]", "0,".repeat(31) + "0")).is_err()
5461        );
5462        let digest = super::KubernetesObjectUidDigest::new([7; 32])?;
5463        let decoded: super::KubernetesObjectUidDigest = serde_json::from_slice(&serde_json::to_vec(&digest)?)?;
5464        assert_eq!(decoded, digest);
5465        Ok(())
5466    }
5467
5468    #[test]
5469    fn gateway_d1_semantic_identities_and_listener_profiles_are_exact() -> Result<(), Box<dyn std::error::Error>> {
5470        assert!(super::GatewayIncarnation::new([0; 32]).is_err());
5471        assert!(serde_json::from_str::<super::GatewayIncarnation>(&format!("[{}]", "0,".repeat(31) + "0")).is_err());
5472        let incarnation = super::GatewayIncarnation::new([7; 32])?;
5473        assert_eq!(
5474            serde_json::from_slice::<super::GatewayIncarnation>(&serde_json::to_vec(&incarnation)?)?,
5475            incarnation
5476        );
5477        assert_eq!(super::AuditRangeId::new(4).get(), 4);
5478        assert_eq!(super::QuotaGrantId::new(5).get(), 5);
5479        assert_eq!(super::QuotaWindow::new(6).get(), 6);
5480        assert_eq!(super::ProcessMonotonicMillis::new(7).get(), 7);
5481        assert_eq!(super::UnixMillis::new(8).get(), 8);
5482        for (listener, profile) in [
5483            (
5484                super::PublicGatewayListenerV1::LodestoneApi,
5485                super::PublicTlsProfileV1::Native,
5486            ),
5487            (
5488                super::PublicGatewayListenerV1::ElasticsearchHttp,
5489                super::PublicTlsProfileV1::Http,
5490            ),
5491            (
5492                super::PublicGatewayListenerV1::OtlpHttp,
5493                super::PublicTlsProfileV1::Http,
5494            ),
5495            (
5496                super::PublicGatewayListenerV1::JaegerHttp,
5497                super::PublicTlsProfileV1::Http,
5498            ),
5499            (
5500                super::PublicGatewayListenerV1::OtlpGrpc,
5501                super::PublicTlsProfileV1::Grpc,
5502            ),
5503            (
5504                super::PublicGatewayListenerV1::SubscriptionGrpc,
5505                super::PublicTlsProfileV1::Grpc,
5506            ),
5507            (
5508                super::PublicGatewayListenerV1::SubscriptionWebSocket,
5509                super::PublicTlsProfileV1::Http,
5510            ),
5511            (super::PublicGatewayListenerV1::Resp, super::PublicTlsProfileV1::Resp),
5512        ] {
5513            assert_eq!(listener.required_tls_profile(), profile);
5514        }
5515        Ok(())
5516    }
5517
5518    #[test]
5519    fn ordinary_segment_allocation_ignores_reserved_ids_and_stops_at_boundary() {
5520        let reserved = super::SegmentId::new(super::RESERVED_SEGMENT_ID_NAMESPACE_START + 17);
5521        assert_eq!(
5522            super::next_ordinary_segment_id([super::SegmentId::new(7), reserved, super::SegmentId::new(9),]),
5523            Some(super::SegmentId::new(10))
5524        );
5525        assert_eq!(
5526            super::next_ordinary_segment_id([reserved]),
5527            Some(super::SegmentId::new(1))
5528        );
5529        assert_eq!(
5530            super::next_ordinary_segment_id([super::SegmentId::new(super::RESERVED_SEGMENT_ID_NAMESPACE_START - 1,)]),
5531            None
5532        );
5533    }
5534
5535    #[test]
5536    fn dense_numeric_and_binary_values_decode_to_identical_semantics() -> Result<(), Box<dyn std::error::Error>> {
5537        use base64::Engine as _;
5538
5539        for (element_type, scale, numeric, bytes) in [
5540            (
5541                super::DenseVectorElementType::F32,
5542                None,
5543                vec![1.5, -2.0],
5544                [1.5_f32.to_le_bytes(), (-2.0_f32).to_le_bytes()].concat(),
5545            ),
5546            (
5547                super::DenseVectorElementType::F16,
5548                None,
5549                vec![1.5, -2.0],
5550                [0x3e00_u16.to_le_bytes(), 0xc000_u16.to_le_bytes()].concat(),
5551            ),
5552            (
5553                super::DenseVectorElementType::I8,
5554                Some(super::VectorQuantizationScale::new(0.25)?),
5555                vec![6.0, -8.0],
5556                vec![6_u8, (-8_i8) as u8],
5557            ),
5558        ] {
5559            let definition = super::DenseVectorDefinition {
5560                dimensions: 2,
5561                element_type,
5562                metric: super::VectorMetric::DotProduct,
5563                quantization_scale: scale,
5564            };
5565            let numeric = super::decode_dense_numeric_value(definition, &numeric)?;
5566            let binary = super::decode_dense_binary_value(
5567                definition,
5568                &super::DenseVectorBinaryValue {
5569                    element_type,
5570                    byte_order: super::DenseVectorByteOrder::LittleEndian,
5571                    data_base64: super::BASE64_STANDARD.encode(bytes),
5572                    quantization_scale: scale,
5573                },
5574            )?;
5575            assert_eq!(numeric, binary);
5576        }
5577        Ok(())
5578    }
5579
5580    #[test]
5581    fn dense_value_refuses_narrowing_and_noncanonical_binary_controls() -> Result<(), Box<dyn std::error::Error>> {
5582        use base64::Engine as _;
5583
5584        let f16 = super::DenseVectorDefinition {
5585            dimensions: 1,
5586            element_type: super::DenseVectorElementType::F16,
5587            metric: super::VectorMetric::DotProduct,
5588            quantization_scale: None,
5589        };
5590        assert_eq!(super::decode_dense_numeric_value(f16, &[1.000_488_281_25])?, vec![1.0]);
5591        assert_eq!(
5592            super::decode_dense_numeric_value(f16, &[1.001_464_843_75])?,
5593            vec![1.001_953_1]
5594        );
5595        assert!(matches!(
5596            super::decode_dense_numeric_value(f16, &[70_000.0]),
5597            Err(super::DenseVectorValueError::F16Overflow(0))
5598        ));
5599
5600        let i8_definition = super::DenseVectorDefinition {
5601            dimensions: 1,
5602            element_type: super::DenseVectorElementType::I8,
5603            metric: super::VectorMetric::DotProduct,
5604            quantization_scale: Some(super::VectorQuantizationScale::new(0.5)?),
5605        };
5606        assert!(matches!(
5607            super::decode_dense_numeric_value(i8_definition, &[1.5]),
5608            Err(super::DenseVectorValueError::I8ElementOutOfRange(0))
5609        ));
5610        let encoded = super::BASE64_STANDARD.encode([1_u8]);
5611        let mut binary = super::DenseVectorBinaryValue {
5612            element_type: super::DenseVectorElementType::I8,
5613            byte_order: super::DenseVectorByteOrder::LittleEndian,
5614            data_base64: encoded,
5615            quantization_scale: Some(super::VectorQuantizationScale::new(0.25)?),
5616        };
5617        assert_eq!(
5618            super::decode_dense_binary_value(i8_definition, &binary),
5619            Err(super::DenseVectorValueError::QuantizationScaleMismatch)
5620        );
5621        binary.quantization_scale = i8_definition.quantization_scale;
5622        binary.data_base64.push('=');
5623        assert!(matches!(
5624            super::decode_dense_binary_value(i8_definition, &binary),
5625            Err(super::DenseVectorValueError::InvalidBase64Length { .. })
5626        ));
5627
5628        let wrong_order =
5629            br#"{"kind":"binary","element_type":"f32","byte_order":"big_endian","data_base64":"AAAAAA=="}"#;
5630        assert!(serde_json::from_slice::<super::DenseVectorValue>(wrong_order).is_err());
5631        let signed_zero = super::decode_dense_numeric_value(
5632            super::DenseVectorDefinition {
5633                dimensions: 2,
5634                element_type: super::DenseVectorElementType::F32,
5635                metric: super::VectorMetric::DotProduct,
5636                quantization_scale: None,
5637            },
5638            &[-0.0, 0.0],
5639        )?;
5640        assert!(signed_zero.iter().all(|value| value.to_bits() == 0));
5641        assert_eq!(
5642            super::decode_dense_numeric_value(
5643                super::DenseVectorDefinition {
5644                    dimensions: 2,
5645                    element_type: super::DenseVectorElementType::F32,
5646                    metric: super::VectorMetric::Cosine,
5647                    quantization_scale: None,
5648                },
5649                &[0.0, -0.0],
5650            ),
5651            Err(super::DenseVectorValueError::ZeroNorm)
5652        );
5653        Ok(())
5654    }
5655
5656    #[test]
5657    fn dense_value_refuses_length_scale_and_numeric_boundary_controls() -> Result<(), Box<dyn std::error::Error>> {
5658        use base64::Engine as _;
5659
5660        let f32_definition = super::DenseVectorDefinition {
5661            dimensions: 2,
5662            element_type: super::DenseVectorElementType::F32,
5663            metric: super::VectorMetric::DotProduct,
5664            quantization_scale: None,
5665        };
5666        for bytes in [vec![0_u8; 4], vec![0_u8; 12]] {
5667            let value = super::DenseVectorBinaryValue {
5668                element_type: super::DenseVectorElementType::F32,
5669                byte_order: super::DenseVectorByteOrder::LittleEndian,
5670                data_base64: super::BASE64_STANDARD.encode(bytes),
5671                quantization_scale: None,
5672            };
5673            assert!(matches!(
5674                super::decode_dense_binary_value(f32_definition, &value),
5675                Err(super::DenseVectorValueError::InvalidBase64Length { .. })
5676            ));
5677        }
5678        let wrong_type = super::DenseVectorBinaryValue {
5679            element_type: super::DenseVectorElementType::F16,
5680            byte_order: super::DenseVectorByteOrder::LittleEndian,
5681            data_base64: super::BASE64_STANDARD.encode([0_u8; 8]),
5682            quantization_scale: None,
5683        };
5684        assert_eq!(
5685            super::decode_dense_binary_value(f32_definition, &wrong_type),
5686            Err(super::DenseVectorValueError::ElementTypeMismatch)
5687        );
5688        assert!(matches!(
5689            super::decode_dense_numeric_value(f32_definition, &[f64::INFINITY, 0.0]),
5690            Err(super::DenseVectorValueError::NonFiniteElement(0))
5691        ));
5692
5693        let f16_definition = super::DenseVectorDefinition {
5694            dimensions: 1,
5695            element_type: super::DenseVectorElementType::F16,
5696            metric: super::VectorMetric::DotProduct,
5697            quantization_scale: None,
5698        };
5699        assert_eq!(
5700            super::decode_dense_numeric_value(f16_definition, &[2.0_f64.powi(-25)])?[0].to_bits(),
5701            0
5702        );
5703        assert_eq!(
5704            super::decode_dense_numeric_value(f16_definition, &[2.0_f64.powi(-24)])?[0],
5705            2.0_f32.powi(-24)
5706        );
5707        assert_eq!(
5708            super::decode_dense_numeric_value(f16_definition, &[3.0 * 2.0_f64.powi(-25)])?[0],
5709            2.0_f32.powi(-23)
5710        );
5711        let infinite_half = super::DenseVectorBinaryValue {
5712            element_type: super::DenseVectorElementType::F16,
5713            byte_order: super::DenseVectorByteOrder::LittleEndian,
5714            data_base64: super::BASE64_STANDARD.encode(0x7c00_u16.to_le_bytes()),
5715            quantization_scale: None,
5716        };
5717        assert!(matches!(
5718            super::decode_dense_binary_value(f16_definition, &infinite_half),
5719            Err(super::DenseVectorValueError::NonFiniteElement(0))
5720        ));
5721
5722        let i8_definition = super::DenseVectorDefinition {
5723            dimensions: 1,
5724            element_type: super::DenseVectorElementType::I8,
5725            metric: super::VectorMetric::DotProduct,
5726            quantization_scale: Some(super::VectorQuantizationScale::new(1.0)?),
5727        };
5728        for value in [-129.0, 128.0] {
5729            assert!(matches!(
5730                super::decode_dense_numeric_value(i8_definition, &[value]),
5731                Err(super::DenseVectorValueError::I8ElementOutOfRange(0))
5732            ));
5733        }
5734        for scale in [r#""0""#, r#""-1""#, r#""1.0""#] {
5735            assert!(serde_json::from_str::<super::VectorQuantizationScale>(scale).is_err());
5736        }
5737        Ok(())
5738    }
5739}