Skip to main content

lodestone_types/
lib.rs

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