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/// This enum reserves lifecycle policy identities. Only [`Self::Standard`] is
3726/// admitted until a provider-specific PUT plus metadata-readback probe passes.
3727#[non_exhaustive]
3728#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
3729#[serde(rename_all = "snake_case")]
3730pub enum StorageClass {
3731    #[default]
3732    Standard,
3733    Cold,
3734}
3735
3736/// Reconstructible local-residency preference for one index.
3737#[non_exhaustive]
3738#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
3739#[serde(rename_all = "snake_case")]
3740pub enum CacheClass {
3741    Pinned,
3742    #[default]
3743    Normal,
3744    ColdTolerant,
3745}
3746
3747impl CacheClass {
3748    /// Returns the stable catalog identity.
3749    #[must_use]
3750    pub const fn as_str(self) -> &'static str {
3751        match self {
3752            Self::Pinned => "pinned",
3753            Self::Normal => "normal",
3754            Self::ColdTolerant => "cold_tolerant",
3755        }
3756    }
3757}
3758
3759impl Display for CacheClass {
3760    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
3761        formatter.write_str(self.as_str())
3762    }
3763}
3764
3765impl StorageClass {
3766    /// Returns whether serde may omit this value from a versioned parent.
3767    #[must_use]
3768    pub const fn is_standard(&self) -> bool {
3769        matches!(self, Self::Standard)
3770    }
3771
3772    /// Returns the stable manifest identity.
3773    #[must_use]
3774    pub const fn as_str(self) -> &'static str {
3775        match self {
3776            Self::Standard => "standard",
3777            Self::Cold => "cold",
3778        }
3779    }
3780}
3781
3782impl Display for StorageClass {
3783    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
3784        formatter.write_str(self.as_str())
3785    }
3786}
3787
3788/// Reserved verb carried by a future versioned lifecycle-stage parent.
3789///
3790/// This enum does not execute lifecycle work. ADR-0016 reserves the wire names
3791/// before an index-config format depends on them.
3792#[non_exhaustive]
3793#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
3794#[serde(rename_all = "snake_case")]
3795pub enum LifecycleVerb {
3796    Transition,
3797    AggregateThenDelete,
3798}
3799
3800/// A validated document identifier.
3801#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
3802#[serde(transparent)]
3803pub struct DocumentId(String);
3804
3805impl DocumentId {
3806    /// Constructs a non-empty document identifier no larger than 512 bytes.
3807    pub fn new(value: impl Into<String>) -> Result<Self, IdentifierError> {
3808        let value = value.into();
3809        if value.is_empty() {
3810            return Err(IdentifierError::EmptyDocumentId);
3811        }
3812        if value.len() > 512 {
3813            return Err(IdentifierError::DocumentIdTooLong(value.len()));
3814        }
3815        Ok(Self(value))
3816    }
3817
3818    /// Returns the identifier text.
3819    #[must_use]
3820    pub fn as_str(&self) -> &str {
3821        &self.0
3822    }
3823
3824    /// Consumes this identifier.
3825    #[must_use]
3826    pub fn into_inner(self) -> String {
3827        self.0
3828    }
3829}
3830
3831impl<'de> Deserialize<'de> for DocumentId {
3832    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3833    where
3834        D: Deserializer<'de>,
3835    {
3836        let value = String::deserialize(deserializer)?;
3837        Self::new(value).map_err(serde::de::Error::custom)
3838    }
3839}
3840
3841impl Display for DocumentId {
3842    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
3843        formatter.write_str(self.as_str())
3844    }
3845}
3846
3847/// A bounded client token used to deduplicate one logical write.
3848#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
3849#[serde(transparent)]
3850pub struct IdempotencyKey(String);
3851
3852impl IdempotencyKey {
3853    /// Constructs a non-empty UTF-8 key no larger than 256 bytes.
3854    pub fn new(value: impl Into<String>) -> Result<Self, IdentifierError> {
3855        let value = value.into();
3856        if value.is_empty() {
3857            return Err(IdentifierError::EmptyIdempotencyKey);
3858        }
3859        if value.len() > 256 {
3860            return Err(IdentifierError::IdempotencyKeyTooLong(value.len()));
3861        }
3862        Ok(Self(value))
3863    }
3864
3865    #[must_use]
3866    pub fn as_str(&self) -> &str {
3867        &self.0
3868    }
3869}
3870
3871impl<'de> Deserialize<'de> for IdempotencyKey {
3872    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3873    where
3874        D: Deserializer<'de>,
3875    {
3876        let value = String::deserialize(deserializer)?;
3877        Self::new(value).map_err(serde::de::Error::custom)
3878    }
3879}
3880
3881impl Display for IdempotencyKey {
3882    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
3883        formatter.write_str(self.as_str())
3884    }
3885}
3886
3887/// A bounded opaque point-in-time lease identifier.
3888#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
3889#[serde(transparent)]
3890pub struct PitId(String);
3891
3892impl PitId {
3893    /// Constructs a non-empty opaque PIT identifier no larger than 512 bytes.
3894    pub fn new(value: impl Into<String>) -> Result<Self, IdentifierError> {
3895        let value = value.into();
3896        if value.is_empty() {
3897            return Err(IdentifierError::EmptyPitId);
3898        }
3899        if value.len() > 512 {
3900            return Err(IdentifierError::PitIdTooLong(value.len()));
3901        }
3902        Ok(Self(value))
3903    }
3904
3905    /// Returns the opaque identifier text.
3906    #[must_use]
3907    pub fn as_str(&self) -> &str {
3908        &self.0
3909    }
3910}
3911
3912impl<'de> Deserialize<'de> for PitId {
3913    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3914    where
3915        D: Deserializer<'de>,
3916    {
3917        let value = String::deserialize(deserializer)?;
3918        Self::new(value).map_err(serde::de::Error::custom)
3919    }
3920}
3921
3922impl Display for PitId {
3923    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
3924        formatter.write_str(self.as_str())
3925    }
3926}
3927
3928/// Durable format version for recovery-era-scoped PIT identifiers.
3929pub const SCOPED_PIT_ID_FORMAT_VERSION: u16 = 1;
3930
3931/// A PIT identifier whose namespace is fenced by the coordinated recovery era.
3932///
3933/// The era is part of the identifier rather than adjacent command metadata so
3934/// a nonce may be reused only after the durable authority has advanced.
3935#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize)]
3936#[serde(deny_unknown_fields)]
3937pub struct ScopedPitId {
3938    format_version: u16,
3939    recovery_era: RecoveryEra,
3940    nonce: PitId,
3941}
3942
3943impl ScopedPitId {
3944    #[must_use]
3945    pub const fn new(recovery_era: RecoveryEra, nonce: PitId) -> Self {
3946        Self {
3947            format_version: SCOPED_PIT_ID_FORMAT_VERSION,
3948            recovery_era,
3949            nonce,
3950        }
3951    }
3952
3953    #[must_use]
3954    pub const fn recovery_era(&self) -> RecoveryEra {
3955        self.recovery_era
3956    }
3957
3958    #[must_use]
3959    pub const fn nonce(&self) -> &PitId {
3960        &self.nonce
3961    }
3962}
3963
3964impl Ord for ScopedPitId {
3965    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
3966        self.recovery_era
3967            .to_be_bytes()
3968            .cmp(&other.recovery_era.to_be_bytes())
3969            .then_with(|| self.nonce.cmp(&other.nonce))
3970    }
3971}
3972
3973impl PartialOrd for ScopedPitId {
3974    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
3975        Some(self.cmp(other))
3976    }
3977}
3978
3979impl<'de> Deserialize<'de> for ScopedPitId {
3980    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3981    where
3982        D: Deserializer<'de>,
3983    {
3984        #[derive(Deserialize)]
3985        #[serde(deny_unknown_fields)]
3986        struct Wire {
3987            format_version: u16,
3988            recovery_era: RecoveryEra,
3989            nonce: PitId,
3990        }
3991
3992        let wire = Wire::deserialize(deserializer)?;
3993        if wire.format_version != SCOPED_PIT_ID_FORMAT_VERSION {
3994            return Err(serde::de::Error::custom(
3995                IdentifierError::UnsupportedScopedPitIdVersion(wire.format_version),
3996            ));
3997        }
3998        Ok(Self::new(wire.recovery_era, wire.nonce))
3999    }
4000}
4001
4002/// Stable BLAKE3 fingerprint of the complete client mutation intent.
4003#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
4004#[serde(transparent)]
4005pub struct OperationFingerprint([u8; 32]);
4006
4007impl OperationFingerprint {
4008    #[must_use]
4009    pub const fn new(bytes: [u8; 32]) -> Self {
4010        Self(bytes)
4011    }
4012
4013    #[must_use]
4014    pub fn from_payload(bytes: &[u8]) -> Self {
4015        Self(*blake3::hash(bytes).as_bytes())
4016    }
4017
4018    #[must_use]
4019    pub const fn as_bytes(&self) -> &[u8; 32] {
4020        &self.0
4021    }
4022}
4023
4024/// A validated relative immutable-object key.
4025#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
4026#[serde(transparent)]
4027pub struct ObjectKey(String);
4028
4029impl ObjectKey {
4030    /// Constructs a safe relative object key.
4031    pub fn new(value: impl Into<String>) -> Result<Self, IdentifierError> {
4032        let value = value.into();
4033        if value.is_empty() {
4034            return Err(IdentifierError::EmptyObjectKey);
4035        }
4036        if value.starts_with('/') || value.contains('\\') {
4037            return Err(IdentifierError::InvalidObjectKey(value));
4038        }
4039        if value
4040            .split('/')
4041            .any(|component| component.is_empty() || component == "." || component == "..")
4042        {
4043            return Err(IdentifierError::InvalidObjectKey(value));
4044        }
4045        Ok(Self(value))
4046    }
4047
4048    /// Returns the normalized relative key.
4049    #[must_use]
4050    pub fn as_str(&self) -> &str {
4051        &self.0
4052    }
4053
4054    /// Consumes this object key.
4055    ///
4056    /// The allocation can exceed the encoded key length. Memory admissions
4057    /// retaining this key must use `allocated_capacity_bytes` instead.
4058    #[must_use]
4059    pub fn into_inner(self) -> String {
4060        self.0
4061    }
4062
4063    /// Owned string allocation capacity, excluding the inline string header.
4064    #[must_use]
4065    pub fn allocated_capacity_bytes(&self) -> usize {
4066        self.0.capacity()
4067    }
4068}
4069
4070impl<'de> Deserialize<'de> for ObjectKey {
4071    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
4072    where
4073        D: Deserializer<'de>,
4074    {
4075        let value = String::deserialize(deserializer)?;
4076        Self::new(value).map_err(serde::de::Error::custom)
4077    }
4078}
4079
4080impl Display for ObjectKey {
4081    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
4082        formatter.write_str(self.as_str())
4083    }
4084}
4085
4086/// Current transitive root-set locator. The page count selects the current
4087/// single-page or linked-page physical encoding.
4088pub const ROOT_SET_REF_FORMAT_VERSION: u16 = 2;
4089pub const MAX_ROOT_SET_PAGES: u32 = 256;
4090pub const MAX_ROOT_SET_SCHEMA_GENERATIONS: usize = 256;
4091pub const MAX_ROOT_SET_OBJECT_KEY_BYTES: usize = 1_024;
4092pub const PINNED_MANIFEST_BINDING_FORMAT_VERSION: u16 = 1;
4093
4094/// Never-reused physical identity of one immutable object.
4095#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
4096#[serde(deny_unknown_fields)]
4097pub struct PhysicalObjectId {
4098    pub backend_authority: BackendAuthorityId,
4099    pub object_incarnation: ObjectIncarnation,
4100    pub object_key: ObjectKey,
4101}
4102
4103impl PhysicalObjectId {
4104    pub fn validate(&self) -> Result<(), RootSetRefError> {
4105        let length = self.object_key.as_str().len();
4106        if !(1..=MAX_ROOT_SET_OBJECT_KEY_BYTES).contains(&length) {
4107            return Err(RootSetRefError::ObjectKeyLength(length));
4108        }
4109        Ok(())
4110    }
4111}
4112
4113/// Integrity-qualified physical object used inside immutable root-set pages.
4114#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
4115#[serde(deny_unknown_fields)]
4116pub struct ObjectIdentity {
4117    pub physical: PhysicalObjectId,
4118    pub content_checksum: [u8; 32],
4119}
4120
4121impl ObjectIdentity {
4122    pub fn validate(&self) -> Result<(), RootSetRefError> {
4123        self.physical.validate()
4124    }
4125}
4126
4127/// Bounded immutable root-set header copied only from an authoritative source.
4128#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
4129#[serde(deny_unknown_fields)]
4130pub struct RootSetRef {
4131    pub format_version: u16,
4132    pub root: PhysicalObjectId,
4133    pub content_digest: [u8; 32],
4134    pub item_count: u64,
4135    pub page_count: u32,
4136    pub schema_generations: Vec<SchemaGeneration>,
4137}
4138
4139impl RootSetRef {
4140    pub fn validate(&self) -> Result<(), RootSetRefError> {
4141        if self.format_version != ROOT_SET_REF_FORMAT_VERSION {
4142            return Err(RootSetRefError::UnsupportedVersion(self.format_version));
4143        }
4144        self.root.validate()?;
4145        if self.item_count == 0
4146            || self.page_count == 0
4147            || self.page_count > MAX_ROOT_SET_PAGES
4148            || u64::from(self.page_count) > self.item_count
4149        {
4150            return Err(RootSetRefError::InvalidCounts {
4151                items: self.item_count,
4152                pages: self.page_count,
4153            });
4154        }
4155        if self.schema_generations.len() > MAX_ROOT_SET_SCHEMA_GENERATIONS
4156            || self.schema_generations.windows(2).any(|pair| pair[0] >= pair[1])
4157        {
4158            return Err(RootSetRefError::InvalidSchemaGenerations);
4159        }
4160        Ok(())
4161    }
4162}
4163
4164#[derive(Clone, Debug, Eq, PartialEq)]
4165pub enum RootSetRefError {
4166    UnsupportedVersion(u16),
4167    ObjectKeyLength(usize),
4168    InvalidCounts { items: u64, pages: u32 },
4169    InvalidSchemaGenerations,
4170}
4171
4172impl Display for RootSetRefError {
4173    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
4174        match self {
4175            Self::UnsupportedVersion(version) => write!(formatter, "unsupported root-set reference version {version}"),
4176            Self::ObjectKeyLength(length) => {
4177                write!(formatter, "root-set object key is {length} bytes; expected 1..=1024")
4178            }
4179            Self::InvalidCounts { items, pages } => {
4180                write!(formatter, "root-set counts are invalid: items={items}, pages={pages}")
4181            }
4182            Self::InvalidSchemaGenerations => {
4183                formatter.write_str("root-set schema generations are not bounded strict ascending values")
4184            }
4185        }
4186    }
4187}
4188
4189impl Error for RootSetRefError {}
4190
4191/// Exact immutable-manifest codec selected by a durable PIT binding.
4192#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
4193#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
4194pub enum PinnedManifestDecodeProvenanceV1 {
4195    ScopedManifestV3,
4196}
4197
4198/// Complete immutable manifest binding copied into a durable PIT lease.
4199///
4200/// This is read authority only. It cannot publish or replace a manifest, and
4201/// retaining it beside a PIT is the immutable snapshot ownership transfer
4202/// required when the mutable current pointer later advances.
4203#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
4204#[serde(deny_unknown_fields)]
4205pub struct PinnedManifestBindingV1 {
4206    pub format_version: u16,
4207    pub shard_key: ShardKey,
4208    pub admission_generation: AdmissionGeneration,
4209    pub routing_revision: RoutingRevision,
4210    pub data_backend_authority: BackendAuthorityId,
4211    pub generation: ManifestGeneration,
4212    pub fence: FenceToken,
4213    pub primary_term: PrimaryTerm,
4214    pub manifest: ObjectIdentity,
4215    pub root_set: RootSetRef,
4216    pub committed_through: SeqNo,
4217    pub decode_provenance: PinnedManifestDecodeProvenanceV1,
4218}
4219
4220impl PinnedManifestBindingV1 {
4221    pub fn validate(&self) -> Result<(), PinnedManifestBindingError> {
4222        if self.format_version != PINNED_MANIFEST_BINDING_FORMAT_VERSION {
4223            return Err(PinnedManifestBindingError::UnsupportedVersion(self.format_version));
4224        }
4225        if self.admission_generation.get() == 0
4226            || self.routing_revision.get() == 0
4227            || self.generation.get() == 0
4228            || self.fence.epoch().get() == 0
4229            || self.fence.epoch() != self.primary_term.epoch_component()
4230        {
4231            return Err(PinnedManifestBindingError::InvalidSemanticIdentity);
4232        }
4233        self.manifest
4234            .validate()
4235            .map_err(|error| PinnedManifestBindingError::InvalidObject(error.to_string()))?;
4236        self.root_set
4237            .validate()
4238            .map_err(|error| PinnedManifestBindingError::InvalidRootSet(error.to_string()))?;
4239        if self.manifest.physical.backend_authority != self.data_backend_authority
4240            || self.root_set.root.backend_authority != self.data_backend_authority
4241        {
4242            return Err(PinnedManifestBindingError::BackendAuthorityMismatch);
4243        }
4244        Ok(())
4245    }
4246}
4247
4248/// Stable identity of one exact committed vector-search cut.
4249///
4250/// The digest binds the logical index/mapping incarnation and every immutable
4251/// manifest/root visible to the vector branch. Callers must supply roots in
4252/// ascending shard order so the same cut has exactly one identity.
4253pub fn vector_snapshot_digest_v1(
4254    scope: CollectionIndexRef,
4255    schema_generation: SchemaGeneration,
4256    roots: &[PinnedManifestBindingV1],
4257) -> Result<[u8; 32], PinnedManifestBindingError> {
4258    if schema_generation.get() == 0
4259        || roots
4260            .iter()
4261            .any(|root| root.shard_key.collection_index_ref() != scope || root.validate().is_err())
4262        || roots
4263            .windows(2)
4264            .any(|pair| pair[0].shard_key.shard_id() >= pair[1].shard_key.shard_id())
4265    {
4266        return Err(PinnedManifestBindingError::InvalidSemanticIdentity);
4267    }
4268    let mut hasher = blake3::Hasher::new();
4269    hasher.update(b"lodestone-vector-snapshot-v1\0");
4270    hasher.update(&scope.collection_id().get().to_be_bytes());
4271    hasher.update(&scope.index_id().get().to_be_bytes());
4272    hasher.update(&schema_generation.get().to_be_bytes());
4273    let count = u32::try_from(roots.len()).map_err(|_| PinnedManifestBindingError::InvalidSemanticIdentity)?;
4274    hasher.update(&count.to_be_bytes());
4275    for root in roots {
4276        hasher.update(&root.shard_key.shard_id().get().to_be_bytes());
4277        hasher.update(&root.admission_generation.get().to_be_bytes());
4278        hasher.update(&root.routing_revision.get().to_be_bytes());
4279        hasher.update(&root.generation.get().to_be_bytes());
4280        hasher.update(&root.fence.epoch().get().to_be_bytes());
4281        hasher.update(&root.primary_term.get().to_be_bytes());
4282        hasher.update(&root.manifest.content_checksum);
4283        hasher.update(&root.root_set.content_digest);
4284        hasher.update(&root.committed_through.get().to_be_bytes());
4285    }
4286    Ok(*hasher.finalize().as_bytes())
4287}
4288
4289/// Public semantic format of an exact committed read-cut projection.
4290pub const COMMITTED_READ_CUT_FORMAT_VERSION: u16 = 1;
4291
4292/// Maximum number of exact index incarnations in one committed read cut.
4293pub const MAX_COMMITTED_READ_CUT_INDEXES: usize = 128;
4294
4295/// One index entry in a provider-neutral committed read-cut projection.
4296#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
4297#[serde(deny_unknown_fields)]
4298pub struct CommittedReadCutIndexV1 {
4299    pub index: IndexName,
4300    pub collection_index_ref: CollectionIndexRef,
4301    pub pinned_schema_generation: SchemaGeneration,
4302    pub cut_digest: [u8; 32],
4303}
4304
4305/// Exact provider-neutral identity of one durable committed PIT cut.
4306#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
4307#[serde(deny_unknown_fields)]
4308pub struct CommittedReadCutEvidenceV1 {
4309    pub format_version: u16,
4310    pub collection_id: CollectionId,
4311    pub recovery_era: RecoveryEra,
4312    pub catalog_revision: ClusterRevision,
4313    pub indexes: Vec<CommittedReadCutIndexV1>,
4314    pub aggregate_digest: [u8; 32],
4315}
4316
4317impl CommittedReadCutEvidenceV1 {
4318    pub fn new(
4319        collection_id: CollectionId,
4320        recovery_era: RecoveryEra,
4321        catalog_revision: ClusterRevision,
4322        indexes: Vec<CommittedReadCutIndexV1>,
4323    ) -> Result<Self, PinnedManifestBindingError> {
4324        let aggregate_digest = committed_read_cut_digest_v1(collection_id, recovery_era, catalog_revision, &indexes)?;
4325        Ok(Self {
4326            format_version: COMMITTED_READ_CUT_FORMAT_VERSION,
4327            collection_id,
4328            recovery_era,
4329            catalog_revision,
4330            indexes,
4331            aggregate_digest,
4332        })
4333    }
4334
4335    pub fn validate(&self) -> Result<(), PinnedManifestBindingError> {
4336        if self.format_version != COMMITTED_READ_CUT_FORMAT_VERSION
4337            || self.aggregate_digest
4338                != committed_read_cut_digest_v1(
4339                    self.collection_id,
4340                    self.recovery_era,
4341                    self.catalog_revision,
4342                    &self.indexes,
4343                )?
4344        {
4345            return Err(PinnedManifestBindingError::InvalidSemanticIdentity);
4346        }
4347        Ok(())
4348    }
4349}
4350
4351/// Stable opaque identity of one index's complete immutable committed root vector.
4352pub fn committed_index_cut_digest_v1(
4353    scope: CollectionIndexRef,
4354    pinned_schema_generation: SchemaGeneration,
4355    roots: &[PinnedManifestBindingV1],
4356) -> Result<[u8; 32], PinnedManifestBindingError> {
4357    if pinned_schema_generation.get() == 0
4358        || roots.is_empty()
4359        || roots
4360            .iter()
4361            .any(|root| root.shard_key.collection_index_ref() != scope || root.validate().is_err())
4362        || roots
4363            .windows(2)
4364            .any(|pair| pair[0].shard_key.shard_id() >= pair[1].shard_key.shard_id())
4365    {
4366        return Err(PinnedManifestBindingError::InvalidSemanticIdentity);
4367    }
4368    let mut hasher = blake3::Hasher::new();
4369    hasher.update(b"lodestone-committed-index-cut-v1\0");
4370    hasher.update(&scope.collection_id().get().to_be_bytes());
4371    hasher.update(&scope.index_id().get().to_be_bytes());
4372    hasher.update(&pinned_schema_generation.get().to_be_bytes());
4373    let count = u32::try_from(roots.len()).map_err(|_| PinnedManifestBindingError::InvalidSemanticIdentity)?;
4374    hasher.update(&count.to_be_bytes());
4375    for root in roots {
4376        hasher.update(&root.shard_key.shard_id().get().to_be_bytes());
4377        hasher.update(&root.admission_generation.get().to_be_bytes());
4378        hasher.update(&root.routing_revision.get().to_be_bytes());
4379        hasher.update(&root.generation.get().to_be_bytes());
4380        hasher.update(&root.fence.epoch().get().to_be_bytes());
4381        hasher.update(&root.primary_term.get().to_be_bytes());
4382        hasher.update(&root.manifest.content_checksum);
4383        hasher.update(&root.root_set.content_digest);
4384        hasher.update(&root.committed_through.get().to_be_bytes());
4385    }
4386    Ok(*hasher.finalize().as_bytes())
4387}
4388
4389/// Stable aggregate identity of a canonical complete multi-index committed cut.
4390pub fn committed_read_cut_digest_v1(
4391    collection_id: CollectionId,
4392    recovery_era: RecoveryEra,
4393    catalog_revision: ClusterRevision,
4394    indexes: &[CommittedReadCutIndexV1],
4395) -> Result<[u8; 32], PinnedManifestBindingError> {
4396    if catalog_revision.get() == 0
4397        || indexes.is_empty()
4398        || indexes.len() > MAX_COMMITTED_READ_CUT_INDEXES
4399        || indexes.iter().any(|entry| {
4400            entry.collection_index_ref.collection_id() != collection_id
4401                || entry.pinned_schema_generation.get() == 0
4402                || entry.cut_digest == [0; 32]
4403        })
4404        || indexes
4405            .windows(2)
4406            .any(|pair| pair[0].collection_index_ref >= pair[1].collection_index_ref)
4407        || indexes.iter().map(|entry| &entry.index).collect::<BTreeSet<_>>().len() != indexes.len()
4408    {
4409        return Err(PinnedManifestBindingError::InvalidSemanticIdentity);
4410    }
4411    let mut hasher = blake3::Hasher::new();
4412    hasher.update(b"lodestone-committed-read-cut-v1\0");
4413    hasher.update(&COMMITTED_READ_CUT_FORMAT_VERSION.to_be_bytes());
4414    hasher.update(&recovery_era.to_be_bytes());
4415    hasher.update(&collection_id.get().to_be_bytes());
4416    hasher.update(&catalog_revision.get().to_be_bytes());
4417    let count = u32::try_from(indexes.len()).map_err(|_| PinnedManifestBindingError::InvalidSemanticIdentity)?;
4418    hasher.update(&count.to_be_bytes());
4419    for entry in indexes {
4420        let name = entry.index.as_str().as_bytes();
4421        let name_length = u16::try_from(name.len()).map_err(|_| PinnedManifestBindingError::InvalidSemanticIdentity)?;
4422        hasher.update(&name_length.to_be_bytes());
4423        hasher.update(name);
4424        hasher.update(&entry.collection_index_ref.index_id().get().to_be_bytes());
4425        hasher.update(&entry.pinned_schema_generation.get().to_be_bytes());
4426        hasher.update(&entry.cut_digest);
4427    }
4428    Ok(*hasher.finalize().as_bytes())
4429}
4430
4431#[derive(Clone, Debug, Eq, PartialEq)]
4432pub enum PinnedManifestBindingError {
4433    UnsupportedVersion(u16),
4434    InvalidSemanticIdentity,
4435    InvalidObject(String),
4436    InvalidRootSet(String),
4437    BackendAuthorityMismatch,
4438}
4439
4440impl Display for PinnedManifestBindingError {
4441    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
4442        match self {
4443            Self::UnsupportedVersion(version) => {
4444                write!(formatter, "unsupported pinned manifest binding version {version}")
4445            }
4446            Self::InvalidSemanticIdentity => formatter.write_str("pinned manifest semantic identity is invalid"),
4447            Self::InvalidObject(reason) => write!(formatter, "pinned manifest object is invalid: {reason}"),
4448            Self::InvalidRootSet(reason) => write!(formatter, "pinned manifest root set is invalid: {reason}"),
4449            Self::BackendAuthorityMismatch => formatter.write_str("pinned manifest authorities do not match"),
4450        }
4451    }
4452}
4453
4454impl Error for PinnedManifestBindingError {}
4455
4456/// Identifier validation failure.
4457#[derive(Clone, Debug, Eq, PartialEq)]
4458pub enum IdentifierError {
4459    EmptyDocumentId,
4460    DocumentIdTooLong(usize),
4461    EmptyIdempotencyKey,
4462    IdempotencyKeyTooLong(usize),
4463    EmptyPitId,
4464    PitIdTooLong(usize),
4465    UnsupportedScopedPitIdVersion(u16),
4466    EmptyObjectKey,
4467    InvalidObjectKey(String),
4468}
4469
4470impl Display for IdentifierError {
4471    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
4472        match self {
4473            Self::EmptyDocumentId => formatter.write_str("document id must not be empty"),
4474            Self::DocumentIdTooLong(length) => {
4475                write!(formatter, "document id is {length} bytes; maximum is 512")
4476            }
4477            Self::EmptyIdempotencyKey => formatter.write_str("idempotency key must not be empty"),
4478            Self::IdempotencyKeyTooLong(length) => {
4479                write!(formatter, "idempotency key is {length} bytes; maximum is 256")
4480            }
4481            Self::EmptyPitId => formatter.write_str("PIT id must not be empty"),
4482            Self::PitIdTooLong(length) => {
4483                write!(formatter, "PIT id is {length} bytes; maximum is 512")
4484            }
4485            Self::UnsupportedScopedPitIdVersion(version) => {
4486                write!(formatter, "unsupported scoped PIT identifier version {version}")
4487            }
4488            Self::EmptyObjectKey => formatter.write_str("object key must not be empty"),
4489            Self::InvalidObjectKey(key) => {
4490                write!(formatter, "invalid relative object key '{key}'")
4491            }
4492        }
4493    }
4494}
4495
4496impl Error for IdentifierError {}
4497
4498/// Stable P0 routing hash stored as `_rh`.
4499#[must_use]
4500pub const fn routing_hash_v1(bytes: &[u8]) -> RoutingHash {
4501    let mut hash = 0x811c_9dc5_u32;
4502    let mut offset = 0;
4503    while offset < bytes.len() {
4504        hash ^= bytes[offset] as u32;
4505        hash = hash.wrapping_mul(0x0100_0193);
4506        offset += 1;
4507    }
4508    RoutingHash::new(hash)
4509}
4510
4511/// Stable P0 BLAKE3-derived mutable identity hash stored as two u64 fast fields.
4512#[must_use]
4513pub fn identity_hash_v1(bytes: &[u8]) -> IdHash {
4514    let digest = blake3::hash(bytes);
4515    let digest_bytes = digest.as_bytes();
4516    IdHash::new(u128::from_le_bytes([
4517        digest_bytes[0],
4518        digest_bytes[1],
4519        digest_bytes[2],
4520        digest_bytes[3],
4521        digest_bytes[4],
4522        digest_bytes[5],
4523        digest_bytes[6],
4524        digest_bytes[7],
4525        digest_bytes[8],
4526        digest_bytes[9],
4527        digest_bytes[10],
4528        digest_bytes[11],
4529        digest_bytes[12],
4530        digest_bytes[13],
4531        digest_bytes[14],
4532        digest_bytes[15],
4533    ]))
4534}
4535
4536#[cfg(test)]
4537mod policy_axis_tests {
4538    use super::{LifecycleVerb, StorageClass};
4539
4540    #[test]
4541    fn reserved_policy_axis_names_are_stable() {
4542        assert!(serde_json::to_string(&StorageClass::Standard).is_ok_and(|value| value == "\"standard\""));
4543        assert!(serde_json::to_string(&StorageClass::Cold).is_ok_and(|value| value == "\"cold\""));
4544        assert!(serde_json::to_string(&LifecycleVerb::Transition).is_ok_and(|value| value == "\"transition\""));
4545        assert!(
4546            serde_json::to_string(&LifecycleVerb::AggregateThenDelete)
4547                .is_ok_and(|value| value == "\"aggregate_then_delete\"")
4548        );
4549    }
4550}
4551
4552/// Computes CRC32C (Castagnoli) without platform-specific unsafe code.
4553#[must_use]
4554pub fn crc32c(bytes: &[u8]) -> u32 {
4555    let mut crc = u32::MAX;
4556    for byte in bytes {
4557        crc ^= u32::from(*byte);
4558        for _ in 0..8 {
4559            let mask = (crc & 1).wrapping_neg();
4560            crc = (crc >> 1) ^ (0x82f6_3b78 & mask);
4561        }
4562    }
4563    !crc
4564}
4565
4566/// A runtime role that can be composed into the Lodestone process.
4567#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
4568pub enum Role {
4569    Gateway,
4570    Indexer,
4571    Searcher,
4572    Compactor,
4573    Metastore,
4574}
4575
4576impl Role {
4577    /// All roles in stable display and startup order.
4578    pub const ALL: [Self; 5] = [
4579        Self::Gateway,
4580        Self::Indexer,
4581        Self::Searcher,
4582        Self::Compactor,
4583        Self::Metastore,
4584    ];
4585
4586    /// The stable CLI name of this role.
4587    #[must_use]
4588    pub const fn as_str(self) -> &'static str {
4589        match self {
4590            Self::Gateway => "gateway",
4591            Self::Indexer => "indexer",
4592            Self::Searcher => "searcher",
4593            Self::Compactor => "compactor",
4594            Self::Metastore => "metastore",
4595        }
4596    }
4597}
4598
4599impl Display for Role {
4600    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
4601        formatter.write_str(self.as_str())
4602    }
4603}
4604
4605impl FromStr for Role {
4606    type Err = RoleParseError;
4607
4608    fn from_str(value: &str) -> Result<Self, Self::Err> {
4609        match value {
4610            "gateway" => Ok(Self::Gateway),
4611            "indexer" => Ok(Self::Indexer),
4612            "searcher" => Ok(Self::Searcher),
4613            "compactor" => Ok(Self::Compactor),
4614            "metastore" => Ok(Self::Metastore),
4615            _ => Err(RoleParseError::Unknown(value.to_owned())),
4616        }
4617    }
4618}
4619
4620/// A validated, non-empty set of runtime roles.
4621#[derive(Clone, Debug, Eq, PartialEq)]
4622pub struct RoleSelection {
4623    roles: Vec<Role>,
4624}
4625
4626impl RoleSelection {
4627    /// Selects every runtime role.
4628    #[must_use]
4629    pub fn all() -> Self {
4630        Self {
4631            roles: Role::ALL.to_vec(),
4632        }
4633    }
4634
4635    /// Parses `all` or a comma-separated role list.
4636    pub fn parse(value: &str) -> Result<Self, RoleParseError> {
4637        if value == "all" {
4638            return Ok(Self::all());
4639        }
4640
4641        let mut roles = Vec::new();
4642        for candidate in value.split(',') {
4643            if candidate.is_empty() {
4644                return Err(RoleParseError::Empty);
4645            }
4646
4647            let role = candidate.parse::<Role>()?;
4648            if !roles.contains(&role) {
4649                roles.push(role);
4650            }
4651        }
4652
4653        if roles.is_empty() {
4654            return Err(RoleParseError::Empty);
4655        }
4656
4657        roles.sort_unstable();
4658        Ok(Self { roles })
4659    }
4660
4661    /// Returns roles in stable order.
4662    #[must_use]
4663    pub fn roles(&self) -> &[Role] {
4664        &self.roles
4665    }
4666
4667    /// Returns a stable comma-separated representation.
4668    #[must_use]
4669    pub fn display_list(&self) -> String {
4670        self.roles
4671            .iter()
4672            .map(|role| role.as_str())
4673            .collect::<Vec<_>>()
4674            .join(",")
4675    }
4676}
4677
4678impl Default for RoleSelection {
4679    fn default() -> Self {
4680        Self::all()
4681    }
4682}
4683
4684/// Failure to parse a runtime role selection.
4685#[derive(Clone, Debug, Eq, PartialEq)]
4686pub enum RoleParseError {
4687    Empty,
4688    Unknown(String),
4689}
4690
4691impl Display for RoleParseError {
4692    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
4693        match self {
4694            Self::Empty => formatter.write_str("role selection must not be empty"),
4695            Self::Unknown(role) => write!(
4696                formatter,
4697                "unknown role '{role}'; expected all, gateway, indexer, searcher, compactor, or metastore"
4698            ),
4699        }
4700    }
4701}
4702
4703impl Error for RoleParseError {}
4704
4705/// Arms a bounded external SIGKILL handshake at a named P1.5 durability
4706/// boundary.
4707///
4708/// The hook is inert unless all three `LOD_TEST_P15_FAULT_*` variables are
4709/// present. A live harness observes the ready file and kills the child; the
4710/// deadline prevents a misconfigured harness from wedging production code.
4711pub fn process_fault_point(point: &str) {
4712    process_fault_point_inner(point, None);
4713}
4714
4715/// Arms the same bounded process-fault handshake while attaching one small,
4716/// printable fact from the durable transition that reached the boundary.
4717///
4718/// Evidence is test-only observability, never protocol authority. Invalid or
4719/// oversized evidence refuses to publish the handshake so a harness fails by
4720/// timeout instead of accepting a truncated or ambiguous fact.
4721pub fn process_fault_point_with_evidence(point: &str, evidence: &str) {
4722    const MAX_EVIDENCE_BYTES: usize = 512;
4723    if evidence.is_empty()
4724        || evidence.len() > MAX_EVIDENCE_BYTES
4725        || !evidence
4726            .bytes()
4727            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'=' | b'.'))
4728    {
4729        return;
4730    }
4731    process_fault_point_inner(point, Some(evidence));
4732}
4733
4734/// Test-only path receiving one exact P5.2 GC protection observation.
4735///
4736/// Unlike the process-fault handshake, this side channel never waits for a
4737/// release file. The Ready executor publishes it before advancing beyond the
4738/// observed protection boundary, and later fault hooks remain independently
4739/// armed.
4740pub const GC_PROTECTION_OBSERVATION_PATH_ENV: &str = "LOD_TEST_P52_GC_PROTECTION_OBSERVATION_PATH";
4741
4742/// Hard ceiling for one test-only GC protection observation.
4743pub const MAX_GC_PROTECTION_OBSERVATION_BYTES: usize = 8 * 1024 * 1024;
4744
4745/// Failure to publish one bounded test-only GC protection observation.
4746#[derive(Debug)]
4747pub enum GcProtectionObservationError {
4748    InvalidPath,
4749    Empty,
4750    TooLarge {
4751        actual: usize,
4752        maximum: usize,
4753    },
4754    AlreadyPublished(PathBuf),
4755    Io {
4756        operation: &'static str,
4757        path: PathBuf,
4758        source: std::io::Error,
4759    },
4760}
4761
4762impl Display for GcProtectionObservationError {
4763    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
4764        match self {
4765            Self::InvalidPath => formatter.write_str("GC protection observation path has no file name"),
4766            Self::Empty => formatter.write_str("GC protection observation is empty"),
4767            Self::TooLarge { actual, maximum } => write!(
4768                formatter,
4769                "GC protection observation is {actual} bytes; maximum is {maximum}"
4770            ),
4771            Self::AlreadyPublished(path) => {
4772                write!(
4773                    formatter,
4774                    "GC protection observation was already published at {}",
4775                    path.display()
4776                )
4777            }
4778            Self::Io {
4779                operation,
4780                path,
4781                source,
4782            } => write!(
4783                formatter,
4784                "GC protection observation {operation} failed for {}: {source}",
4785                path.display()
4786            ),
4787        }
4788    }
4789}
4790
4791impl Error for GcProtectionObservationError {
4792    fn source(&self) -> Option<&(dyn Error + 'static)> {
4793        match self {
4794            Self::Io { source, .. } => Some(source),
4795            _ => None,
4796        }
4797    }
4798}
4799
4800/// Returns the optional test-only observation destination.
4801#[must_use]
4802pub fn gc_protection_observation_path() -> Option<PathBuf> {
4803    std::env::var_os(GC_PROTECTION_OBSERVATION_PATH_ENV)
4804        .filter(|value| !value.is_empty())
4805        .map(PathBuf::from)
4806}
4807
4808/// Atomically publishes one bounded observation without replacing an older
4809/// one.
4810///
4811/// Callers must execute this blocking filesystem helper on a bounded blocking
4812/// pool. The completed temporary file is linked into place only after every
4813/// byte is written and synced, so readers never observe a partial record.
4814pub fn write_gc_protection_observation(path: &Path, encoded: &[u8]) -> Result<(), GcProtectionObservationError> {
4815    if encoded.is_empty() {
4816        return Err(GcProtectionObservationError::Empty);
4817    }
4818    if encoded.len() > MAX_GC_PROTECTION_OBSERVATION_BYTES {
4819        return Err(GcProtectionObservationError::TooLarge {
4820            actual: encoded.len(),
4821            maximum: MAX_GC_PROTECTION_OBSERVATION_BYTES,
4822        });
4823    }
4824    let file_name = path
4825        .file_name()
4826        .and_then(|name| name.to_str())
4827        .filter(|name| !name.is_empty())
4828        .ok_or(GcProtectionObservationError::InvalidPath)?;
4829    let temporary = path.with_file_name(format!(".{file_name}.{}.tmp", std::process::id()));
4830    let mut file = std::fs::OpenOptions::new()
4831        .write(true)
4832        .create_new(true)
4833        .open(&temporary)
4834        .map_err(|source| GcProtectionObservationError::Io {
4835            operation: "temporary-file creation",
4836            path: temporary.clone(),
4837            source,
4838        })?;
4839    let write_result = file.write_all(encoded).and_then(|()| file.sync_all());
4840    drop(file);
4841    if let Err(source) = write_result {
4842        let _cleanup_result = std::fs::remove_file(&temporary);
4843        return Err(GcProtectionObservationError::Io {
4844            operation: "temporary-file write",
4845            path: temporary,
4846            source,
4847        });
4848    }
4849    if let Err(source) = std::fs::hard_link(&temporary, path) {
4850        let _cleanup_result = std::fs::remove_file(&temporary);
4851        if source.kind() == std::io::ErrorKind::AlreadyExists {
4852            return Err(GcProtectionObservationError::AlreadyPublished(path.to_path_buf()));
4853        }
4854        return Err(GcProtectionObservationError::Io {
4855            operation: "atomic publication",
4856            path: path.to_path_buf(),
4857            source,
4858        });
4859    }
4860    std::fs::remove_file(&temporary).map_err(|source| GcProtectionObservationError::Io {
4861        operation: "temporary-file cleanup",
4862        path: temporary,
4863        source,
4864    })?;
4865    Ok(())
4866}
4867
4868fn process_fault_point_inner(point: &str, evidence: Option<&str>) {
4869    struct FaultConfig {
4870        point: String,
4871        ready_path: PathBuf,
4872        release_path: PathBuf,
4873        timeout: Duration,
4874    }
4875    static CONFIG: OnceLock<Option<FaultConfig>> = OnceLock::new();
4876    let config = CONFIG.get_or_init(|| {
4877        let point = std::env::var("LOD_TEST_LISTENER_FAULT_POINT").ok()?;
4878        let ready_path = std::env::var_os("LOD_TEST_LISTENER_FAULT_READY").map(PathBuf::from)?;
4879        let release_path = std::env::var_os("LOD_TEST_LISTENER_FAULT_RELEASE").map(PathBuf::from)?;
4880        let timeout_millis = std::env::var("LOD_TEST_LISTENER_FAULT_TIMEOUT_MILLIS")
4881            .ok()
4882            .and_then(|value| value.parse::<u64>().ok())
4883            .unwrap_or(30_000)
4884            .min(60_000);
4885        Some(FaultConfig {
4886            point,
4887            ready_path,
4888            release_path,
4889            timeout: Duration::from_millis(timeout_millis),
4890        })
4891    });
4892    let Some(config) = config.as_ref().filter(|config| config.point == point) else {
4893        return;
4894    };
4895    let payload = evidence.map_or_else(|| point.to_owned(), |fact| format!("{point}\n{fact}"));
4896    // Publish the handshake only after its complete payload is visible. A
4897    // direct write creates the ready path before writing its bytes, which can
4898    // let a loaded parent observe an empty or partial fault identity.
4899    let temporary = config
4900        .ready_path
4901        .with_extension(format!("fault-{}.tmp", std::process::id()));
4902    if std::fs::write(&temporary, payload.as_bytes()).is_err() {
4903        let _cleanup_result = std::fs::remove_file(&temporary);
4904        return;
4905    }
4906    if std::fs::rename(&temporary, &config.ready_path).is_err() {
4907        let _cleanup_result = std::fs::remove_file(&temporary);
4908        return;
4909    }
4910    let deadline = Instant::now() + config.timeout;
4911    while !config.release_path.exists() && Instant::now() < deadline {
4912        std::thread::yield_now();
4913    }
4914}
4915
4916#[cfg(test)]
4917mod hash_backend_tests;
4918
4919#[cfg(test)]
4920mod tests {
4921    use std::collections::BTreeMap;
4922    use std::sync::atomic::{AtomicU64, Ordering};
4923
4924    use super::{
4925        CollectionId, CollectionIndexRef, DocumentId, DurabilityClass, DynamicMappingProposalError,
4926        DynamicMappingProposalV1, Epoch, FieldMapping, FieldType, GcProtectionObservationError, IndexId, IndexMode,
4927        MAPPING_FORMAT_VERSION, MAX_GC_PROTECTION_OBSERVATION_BYTES, MAX_MAPPING_FIELD_NAME_BYTES, MAX_MAPPING_FIELDS,
4928        ManifestGeneration, Mapping, MappingExtensions, MappingScalar, MappingValidationError, ObjectKey, PitId, Role,
4929        RoleSelection, RoutingHash, RoutingRangeEnd, SchemaGeneration, ScopedPitId, SeqNo, ShardDescriptor, ShardId,
4930        TaskId, VisibilityMode, crc32c, validate_mapping_field_name, write_gc_protection_observation,
4931    };
4932
4933    static OBSERVATION_TEST_SEQUENCE: AtomicU64 = AtomicU64::new(0);
4934
4935    #[test]
4936    fn object_key_memory_charge_preserves_spare_string_capacity() -> Result<(), Box<dyn std::error::Error>> {
4937        let mut value = String::with_capacity(4096);
4938        value.push_str("a/b");
4939        let capacity = value.capacity();
4940        let key = ObjectKey::new(value)?;
4941        assert_eq!(key.as_str(), "a/b");
4942        assert_eq!(key.allocated_capacity_bytes(), capacity);
4943        assert!(key.allocated_capacity_bytes() > key.as_str().len());
4944        assert_eq!(serde_json::to_string(&key)?, "\"a/b\"");
4945        Ok(())
4946    }
4947
4948    #[test]
4949    fn all_expands_to_every_role() {
4950        let selection = RoleSelection::parse("all");
4951
4952        assert_eq!(selection.as_ref().map(|value| value.roles()), Ok(Role::ALL.as_slice()));
4953    }
4954
4955    #[test]
4956    fn explicit_roles_are_deduplicated_and_stable() {
4957        let selection = RoleSelection::parse("searcher,gateway,searcher");
4958
4959        assert_eq!(
4960            selection.as_ref().map(|value| value.roles()),
4961            Ok([Role::Gateway, Role::Searcher].as_slice())
4962        );
4963    }
4964
4965    #[test]
4966    fn unknown_role_is_rejected() {
4967        let selection = RoleSelection::parse("unknown");
4968
4969        assert!(selection.is_err());
4970    }
4971
4972    #[test]
4973    fn semantic_numbers_advance_without_crossing_types() {
4974        assert_eq!(Epoch::new(7).checked_next(), Some(Epoch::new(8)));
4975        assert_eq!(SeqNo::new(41).checked_next(), Some(SeqNo::new(42)));
4976        assert_eq!(ManifestGeneration::new(u64::MAX).checked_next(), None);
4977    }
4978
4979    #[test]
4980    fn sparse_values_are_canonical_bounded_and_mapping_typed() -> Result<(), Box<dyn std::error::Error>> {
4981        let definition = super::SparseVectorDefinition {
4982            maximum_dimension: 12,
4983            weight_type: super::SparseVectorWeightType::F16,
4984        };
4985        let value = super::SparseVectorValue {
4986            entries: vec![
4987                super::SparseVectorEntry {
4988                    dimension: 1,
4989                    weight: 1.5,
4990                },
4991                super::SparseVectorEntry {
4992                    dimension: 9,
4993                    weight: -2.0,
4994                },
4995            ],
4996        };
4997        let decoded = value.decode(definition)?;
4998        let native = value.canonical_native_bytes(definition)?;
4999        assert_eq!(super::decode_sparse_native_bytes(definition, &native)?, decoded);
5000        assert_eq!(native.len(), 12);
5001        let encoded = serde_json::to_vec(&value)?;
5002        assert_eq!(serde_json::from_slice::<super::SparseVectorValue>(&encoded)?, value);
5003        #[derive(Debug, serde::Deserialize, PartialEq, serde::Serialize)]
5004        #[serde(tag = "kind", rename_all = "snake_case")]
5005        enum TaggedSparse {
5006            Sparse {
5007                query_vector: super::SparseVectorValue,
5008                #[serde(deserialize_with = "super::deserialize_sparse_weight")]
5009                weight: f64,
5010            },
5011        }
5012        let tagged = TaggedSparse::Sparse {
5013            query_vector: value.clone(),
5014            weight: 0.5,
5015        };
5016        let encoded = serde_json::to_vec(&tagged)?;
5017        assert_eq!(serde_json::from_slice::<TaggedSparse>(&encoded)?, tagged);
5018
5019        for invalid in [
5020            super::SparseVectorValue {
5021                entries: vec![
5022                    super::SparseVectorEntry {
5023                        dimension: 1,
5024                        weight: 1.0,
5025                    },
5026                    super::SparseVectorEntry {
5027                        dimension: 1,
5028                        weight: 2.0,
5029                    },
5030                ],
5031            },
5032            super::SparseVectorValue {
5033                entries: vec![super::SparseVectorEntry {
5034                    dimension: 2,
5035                    weight: 0.0,
5036                }],
5037            },
5038            super::SparseVectorValue {
5039                entries: vec![super::SparseVectorEntry {
5040                    dimension: 13,
5041                    weight: 1.0,
5042                }],
5043            },
5044            super::SparseVectorValue {
5045                entries: vec![super::SparseVectorEntry {
5046                    dimension: 2,
5047                    weight: f64::NAN,
5048                }],
5049            },
5050        ] {
5051            assert!(invalid.decode(definition).is_err());
5052        }
5053        Ok(())
5054    }
5055
5056    #[test]
5057    fn vector_set_values_are_explicit_typed_and_bounded() -> Result<(), Box<dyn std::error::Error>> {
5058        let definition = super::VectorSetDefinition {
5059            dense: super::DenseVectorDefinition {
5060                dimensions: 2,
5061                element_type: super::DenseVectorElementType::F32,
5062                metric: super::VectorMetric::DotProduct,
5063                quantization_scale: None,
5064            },
5065            maximum_vectors: 2,
5066        };
5067        let value = super::VectorSetValue {
5068            vectors: vec![
5069                super::DenseVectorValue::Numeric { values: vec![1.0, 0.0] },
5070                super::DenseVectorValue::Numeric { values: vec![0.0, 1.0] },
5071            ],
5072        };
5073        let decoded = value.decode(
5074            definition,
5075            super::MAX_VECTOR_SET_DOCUMENT_VECTORS,
5076            super::MAX_VECTOR_SET_BYTES_PER_FIELD,
5077        )?;
5078        assert_eq!(decoded.vectors, vec![vec![1.0, 0.0], vec![0.0, 1.0]]);
5079        assert_eq!(decoded.native_bytes, 16);
5080        assert!(
5081            super::VectorSetValue { vectors: Vec::new() }
5082                .decode(definition, 2, super::MAX_VECTOR_SET_BYTES_PER_FIELD)
5083                .is_err()
5084        );
5085        assert!(
5086            super::VectorSetValue {
5087                vectors: vec![super::DenseVectorValue::Numeric { values: vec![1.0, 0.0] }; 3],
5088            }
5089            .decode(definition, 2, super::MAX_VECTOR_SET_BYTES_PER_FIELD)
5090            .is_err()
5091        );
5092        assert!(
5093            super::VectorSetValue {
5094                vectors: vec![super::DenseVectorValue::Numeric { values: vec![1.0] }],
5095            }
5096            .decode(definition, 2, super::MAX_VECTOR_SET_BYTES_PER_FIELD)
5097            .is_err()
5098        );
5099        let oversized_json = serde_json::json!({
5100            "vectors": (0..=super::MAX_VECTOR_SET_DOCUMENT_VECTORS)
5101                .map(|_| serde_json::json!({"kind": "numeric", "values": [1.0, 0.0]}))
5102                .collect::<Vec<_>>()
5103        });
5104        assert!(serde_json::from_value::<super::VectorSetValue>(oversized_json).is_err());
5105        Ok(())
5106    }
5107
5108    #[test]
5109    fn task_ids_round_trip_the_complete_u128_domain_and_reject_numbers() {
5110        let task_id = TaskId::new(u128::MAX - 7);
5111        let encoded = serde_json::to_string(&task_id);
5112        let expected = format!("\"{}\"", u128::MAX - 7);
5113        assert_eq!(encoded.as_ref().ok().map(String::as_str), Some(expected.as_str()));
5114        assert_eq!(
5115            encoded
5116                .as_deref()
5117                .ok()
5118                .and_then(|encoded| serde_json::from_str(encoded).ok()),
5119            Some(task_id)
5120        );
5121        assert!(serde_json::from_str::<TaskId>("41").is_err());
5122    }
5123
5124    #[test]
5125    fn gc_protection_observation_writer_is_bounded_atomic_and_single_assignment()
5126    -> Result<(), Box<dyn std::error::Error>> {
5127        let sequence = OBSERVATION_TEST_SEQUENCE.fetch_add(1, Ordering::Relaxed);
5128        let root = std::env::temp_dir().join(format!(
5129            "lodestone-p52-gc-protection-observation-{}-{sequence}",
5130            std::process::id()
5131        ));
5132        std::fs::create_dir(&root)?;
5133        let path = root.join("applied.json");
5134        let encoded = br#"{"format_version":1}"#;
5135
5136        write_gc_protection_observation(&path, encoded)?;
5137        assert_eq!(std::fs::read(&path)?, encoded);
5138        assert!(matches!(
5139            write_gc_protection_observation(&path, encoded),
5140            Err(GcProtectionObservationError::AlreadyPublished(actual)) if actual == path
5141        ));
5142        assert!(matches!(
5143            write_gc_protection_observation(
5144                &root.join("oversized.json"),
5145                &vec![0; MAX_GC_PROTECTION_OBSERVATION_BYTES.saturating_add(1)]
5146            ),
5147            Err(GcProtectionObservationError::TooLarge { .. })
5148        ));
5149        assert!(!root.join("oversized.json").exists());
5150
5151        std::fs::remove_dir_all(root)?;
5152        Ok(())
5153    }
5154
5155    #[test]
5156    fn durable_identifiers_reject_ambiguous_values() {
5157        assert!(DocumentId::new("").is_err());
5158        assert!(PitId::new("").is_err());
5159        assert!(PitId::new("opaque-pit-lease").is_ok());
5160        assert!(ObjectKey::new("../escape").is_err());
5161        assert!(ObjectKey::new("valid/prefix/object").is_ok());
5162    }
5163
5164    #[test]
5165    fn scoped_pit_id_round_trips_and_rejects_newer_formats() -> Result<(), Box<dyn std::error::Error>> {
5166        let pit_id = ScopedPitId::new(super::RecoveryEra::new(7)?, PitId::new("opaque-pit-lease")?);
5167        let encoded = serde_json::to_vec(&pit_id)?;
5168        assert_eq!(serde_json::from_slice::<ScopedPitId>(&encoded)?, pit_id);
5169
5170        let mut unsupported: serde_json::Value = serde_json::from_slice(&encoded)?;
5171        let Some(object) = unsupported.as_object_mut() else {
5172            return Err("scoped PIT identifier was not an object".into());
5173        };
5174        object.insert("format_version".to_owned(), serde_json::json!(2));
5175        assert!(serde_json::from_value::<ScopedPitId>(unsupported).is_err());
5176        Ok(())
5177    }
5178
5179    #[test]
5180    fn crc32c_matches_the_standard_check_value() {
5181        assert_eq!(crc32c(b"123456789"), 0xe306_9283);
5182    }
5183
5184    #[test]
5185    fn durability_names_round_trip_and_admitted_classes_are_available() {
5186        let classes = [
5187            DurabilityClass::RegionalObject,
5188            DurabilityClass::ZonalLowLatency,
5189            DurabilityClass::Relaxed,
5190            DurabilityClass::Ephemeral,
5191        ];
5192        for class in classes {
5193            let encoded = serde_json::to_string(&class);
5194            assert_eq!(
5195                encoded.as_ref().ok().and_then(|bytes| serde_json::from_str(bytes).ok()),
5196                Some(class)
5197            );
5198        }
5199        assert!(DurabilityClass::RegionalObject.require_available().is_ok());
5200        assert!(DurabilityClass::ZonalLowLatency.require_available().is_ok());
5201        assert!(DurabilityClass::Relaxed.require_available().is_err());
5202        assert!(DurabilityClass::Ephemeral.require_available().is_err());
5203        assert!(DurabilityClass::RegionalObject.require_state_available().is_ok());
5204        assert!(DurabilityClass::ZonalLowLatency.require_state_available().is_ok());
5205        assert!(DurabilityClass::Ephemeral.require_state_available().is_ok());
5206        assert!(DurabilityClass::Relaxed.require_state_available().is_err());
5207        assert!(
5208            serde_json::to_string(&super::DurabilityBoundary::IncarnationMemory)
5209                .is_ok_and(|encoded| encoded == "\"incarnation_memory\"")
5210        );
5211        assert!(serde_json::from_str::<DurabilityClass>("\"future\"").is_err());
5212        assert!(matches!(
5213            serde_json::from_str::<VisibilityMode>("\"wait_for\""),
5214            Ok(VisibilityMode::WaitFor)
5215        ));
5216    }
5217
5218    #[test]
5219    fn shard_descriptor_covers_exactly_its_hash_range() {
5220        let descriptor = ShardDescriptor::new(
5221            ShardId::new(7),
5222            Epoch::new(3),
5223            RoutingHash::new(10),
5224            RoutingRangeEnd::new(20).unwrap_or(RoutingRangeEnd::FULL),
5225        );
5226        assert!(descriptor.is_ok());
5227        let descriptor = descriptor.unwrap_or(ShardDescriptor {
5228            shard_id: ShardId::new(7),
5229            epoch: Epoch::new(3),
5230            hash_start: RoutingHash::new(10),
5231            hash_end: RoutingRangeEnd::FULL,
5232        });
5233        assert!(!descriptor.contains(RoutingHash::new(9)));
5234        assert!(descriptor.contains(RoutingHash::new(10)));
5235        assert!(descriptor.contains(RoutingHash::new(19)));
5236        assert!(!descriptor.contains(RoutingHash::new(20)));
5237    }
5238
5239    #[test]
5240    fn shared_mapping_model_validates_semantics_and_refuses_retired_versions_and_unknown_fields()
5241    -> Result<(), Box<dyn std::error::Error>> {
5242        let mapping = Mapping {
5243            format_version: MAPPING_FORMAT_VERSION,
5244            schema_generation: SchemaGeneration::new(1),
5245            mode: IndexMode::Mutable,
5246            fields: BTreeMap::from([(
5247                "message".to_owned(),
5248                FieldMapping {
5249                    field_type: FieldType::Text,
5250                    stored: false,
5251                    null_value: None,
5252                },
5253            )]),
5254            extensions: crate::MappingExtensions::default(),
5255        };
5256        assert_eq!(mapping.validate(), Ok(()));
5257        assert_eq!(mapping.validate_current(), Ok(()));
5258        let canonical = serde_json::to_value(&mapping)?;
5259        for retired_version in 1..MAPPING_FORMAT_VERSION {
5260            let retired = Mapping {
5261                format_version: retired_version,
5262                ..mapping.clone()
5263            };
5264            assert_eq!(
5265                retired.validate(),
5266                Err(MappingValidationError::UnsupportedVersion(retired_version))
5267            );
5268            assert!(matches!(
5269                retired.encode_canonical(),
5270                Err(super::MappingCodecError::Invalid(
5271                    MappingValidationError::UnsupportedVersion(version)
5272                )) if version == retired_version
5273            ));
5274
5275            let mut retired_bytes = canonical.clone();
5276            retired_bytes["format_version"] = serde_json::json!(retired_version);
5277            let retired_bytes = serde_json::to_vec(&retired_bytes)?;
5278            let original = retired_bytes.clone();
5279            assert!(matches!(
5280                Mapping::decode_canonical(&retired_bytes),
5281                Err(super::MappingCodecError::UnsupportedVersion(version)) if version == retired_version
5282            ));
5283            assert_eq!(retired_bytes, original);
5284            assert!(serde_json::from_slice::<Mapping>(&retired_bytes).is_err());
5285        }
5286
5287        let future = Mapping {
5288            format_version: MAPPING_FORMAT_VERSION + 1,
5289            ..mapping.clone()
5290        };
5291        assert_eq!(
5292            future.validate(),
5293            Err(MappingValidationError::UnsupportedVersion(MAPPING_FORMAT_VERSION + 1))
5294        );
5295
5296        let invalid = Mapping {
5297            schema_generation: SchemaGeneration::new(0),
5298            ..mapping.clone()
5299        };
5300        assert_eq!(invalid.validate(), Err(MappingValidationError::ZeroSchemaGeneration));
5301        assert!(
5302            serde_json::from_str::<Mapping>(
5303                r#"{"format_version":1,"schema_generation":1,"mode":"mutable","fields":{},"future":true}"#
5304            )
5305            .is_err()
5306        );
5307        for invalid in [".message", "message.", "object..message"] {
5308            let mut invalid_path = mapping.clone();
5309            invalid_path.fields = BTreeMap::from([(
5310                invalid.to_owned(),
5311                FieldMapping {
5312                    field_type: FieldType::Text,
5313                    stored: false,
5314                    null_value: None,
5315                },
5316            )]);
5317            assert_eq!(
5318                invalid_path.validate(),
5319                Err(MappingValidationError::InvalidFieldName(invalid.to_owned()))
5320            );
5321        }
5322        for (ancestor, descendant) in [("a", "a.b"), ("a.b", "a.b.c")] {
5323            let mut conflicting = mapping.clone();
5324            conflicting.fields = BTreeMap::from([
5325                (
5326                    ancestor.to_owned(),
5327                    FieldMapping {
5328                        field_type: FieldType::Keyword,
5329                        stored: false,
5330                        null_value: None,
5331                    },
5332                ),
5333                (
5334                    descendant.to_owned(),
5335                    FieldMapping {
5336                        field_type: FieldType::U64,
5337                        stored: false,
5338                        null_value: None,
5339                    },
5340                ),
5341            ]);
5342            assert_eq!(
5343                conflicting.validate(),
5344                Err(MappingValidationError::ConflictingFieldPaths {
5345                    ancestor: ancestor.to_owned(),
5346                    descendant: descendant.to_owned(),
5347                })
5348            );
5349        }
5350        let mut siblings = mapping;
5351        siblings.fields = BTreeMap::from([
5352            (
5353                "a.b".to_owned(),
5354                FieldMapping {
5355                    field_type: FieldType::Keyword,
5356                    stored: false,
5357                    null_value: None,
5358                },
5359            ),
5360            (
5361                "a.bc".to_owned(),
5362                FieldMapping {
5363                    field_type: FieldType::U64,
5364                    stored: false,
5365                    null_value: None,
5366                },
5367            ),
5368        ]);
5369        assert_eq!(siblings.validate(), Ok(()));
5370        Ok(())
5371    }
5372
5373    #[test]
5374    fn ec6_mapping_refuses_nested_paths_below_nested_paths() {
5375        let nested = || FieldMapping {
5376            field_type: FieldType::Nested,
5377            stored: false,
5378            null_value: None,
5379        };
5380        let mapping = Mapping {
5381            format_version: MAPPING_FORMAT_VERSION,
5382            schema_generation: SchemaGeneration::new(1),
5383            mode: IndexMode::Mutable,
5384            fields: BTreeMap::from([
5385                ("items".to_owned(), nested()),
5386                ("items-options".to_owned(), nested()),
5387                ("items.options".to_owned(), nested()),
5388            ]),
5389            extensions: MappingExtensions {
5390                nested_paths: vec![
5391                    "items".to_owned(),
5392                    "items-options".to_owned(),
5393                    "items.options".to_owned(),
5394                ],
5395                ..MappingExtensions::default()
5396            },
5397        };
5398        assert_eq!(
5399            mapping.validate(),
5400            Err(MappingValidationError::NestedPathOverlap {
5401                parent: "items".to_owned(),
5402                child: "items.options".to_owned(),
5403            })
5404        );
5405
5406        let mut predecessor = Mapping::empty(IndexMode::Mutable);
5407        predecessor.fields.insert(
5408            "items.color".to_owned(),
5409            FieldMapping {
5410                field_type: FieldType::Keyword,
5411                stored: false,
5412                null_value: None,
5413            },
5414        );
5415        let mut successor = predecessor.clone();
5416        successor.schema_generation = SchemaGeneration::new(2);
5417        successor.fields.insert("items".to_owned(), nested());
5418        successor.extensions.nested_paths = vec!["items".to_owned()];
5419        assert!(!successor.is_additive_successor_of(&predecessor));
5420
5421        let mut safe_successor = predecessor.clone();
5422        safe_successor.schema_generation = SchemaGeneration::new(2);
5423        safe_successor.fields.insert("other".to_owned(), nested());
5424        safe_successor.fields.insert(
5425            "other.color".to_owned(),
5426            FieldMapping {
5427                field_type: FieldType::Keyword,
5428                stored: false,
5429                null_value: None,
5430            },
5431        );
5432        safe_successor.extensions.nested_paths = vec!["other".to_owned()];
5433        assert!(safe_successor.is_additive_successor_of(&predecessor));
5434    }
5435
5436    #[test]
5437    fn ec5_dynamic_mapping_proposal_normalizes_signed_zero_and_refuses_noncanonical_decode()
5438    -> Result<(), Box<dyn std::error::Error>> {
5439        let proposal = DynamicMappingProposalV1::new(
5440            CollectionIndexRef::new(CollectionId::new(7)?, IndexId::new(3)?),
5441            SchemaGeneration::new(1),
5442            "temperature".to_owned(),
5443            MappingScalar::F64Bits((-0.0_f64).to_bits()),
5444        )?;
5445        assert_eq!(
5446            proposal.first_non_null_candidate,
5447            MappingScalar::F64Bits(0.0_f64.to_bits())
5448        );
5449
5450        let mut noncanonical = proposal;
5451        noncanonical.first_non_null_candidate = MappingScalar::F64Bits((-0.0_f64).to_bits());
5452        noncanonical.proposal_digest = noncanonical.exact_digest()?;
5453        let encoded = serde_json::to_vec(&noncanonical)?;
5454        assert!(matches!(
5455            DynamicMappingProposalV1::decode_canonical(&encoded),
5456            Err(DynamicMappingProposalError::InvalidCandidate)
5457        ));
5458        Ok(())
5459    }
5460
5461    #[test]
5462    fn ec5_deployment_mapping_fixtures_use_the_current_validated_format() -> Result<(), Box<dyn std::error::Error>> {
5463        for encoded in [
5464            include_str!("../../../fixtures/runtime/secure-strict-mapping.json"),
5465            include_str!("../../../fixtures/performance/search-mapping.json"),
5466            include_str!("../../../fixtures/reindex/mapping.json"),
5467            include_str!("../../../tests/performance/es-comparison-mapping-v1.json"),
5468        ] {
5469            let mapping: Mapping = serde_json::from_str(encoded)?;
5470            mapping.validate_current()?;
5471            assert_eq!(mapping.schema_generation, SchemaGeneration::new(1));
5472        }
5473        Ok(())
5474    }
5475
5476    #[test]
5477    fn public_replayable_source_mapping_contract_matches_current_model() -> Result<(), serde_json::Error> {
5478        let contract: serde_json::Value = serde_json::from_str(include_str!(
5479            "../../../public/development/replayable-source-mapping-v1.json"
5480        ))?;
5481        assert_eq!(contract["mapping"]["format_version"], MAPPING_FORMAT_VERSION);
5482        assert_eq!(contract["mapping"]["maximum_fields"], MAX_MAPPING_FIELDS);
5483        assert_eq!(
5484            contract["mapping"]["maximum_field_name_bytes"],
5485            MAX_MAPPING_FIELD_NAME_BYTES
5486        );
5487        assert_eq!(
5488            contract["mapping"]["scalar_field_types"],
5489            serde_json::json!(["text", "keyword", "u64", "date", "i64", "f64", "bool"])
5490        );
5491
5492        let mapping = Mapping {
5493            format_version: MAPPING_FORMAT_VERSION,
5494            schema_generation: SchemaGeneration::new(1),
5495            mode: IndexMode::AppendLog,
5496            fields: BTreeMap::from([
5497                (
5498                    "message".to_owned(),
5499                    FieldMapping {
5500                        field_type: FieldType::Text,
5501                        stored: false,
5502                        null_value: None,
5503                    },
5504                ),
5505                (
5506                    "record_id".to_owned(),
5507                    FieldMapping {
5508                        field_type: FieldType::Keyword,
5509                        stored: false,
5510                        null_value: None,
5511                    },
5512                ),
5513            ]),
5514            extensions: crate::MappingExtensions::default(),
5515        };
5516        assert_eq!(
5517            serde_json::Value::String(serde_json::to_string(&mapping)?),
5518            contract["example"]
5519        );
5520        for invalid in ["", ".message", "message.", "object..message", "_reserved", "a\u{7f}b"] {
5521            assert_eq!(
5522                validate_mapping_field_name(invalid),
5523                Err(MappingValidationError::InvalidFieldName(invalid.to_owned()))
5524            );
5525        }
5526        Ok(())
5527    }
5528
5529    #[test]
5530    fn shared_field_path_resolver_uses_canonical_nested_components() {
5531        #[derive(Default)]
5532        struct Node {
5533            children: BTreeMap<&'static str, Node>,
5534            value: Option<u64>,
5535        }
5536
5537        let root = Node {
5538            children: BTreeMap::from([(
5539                "metrics",
5540                Node {
5541                    children: BTreeMap::from([(
5542                        "count",
5543                        Node {
5544                            children: BTreeMap::new(),
5545                            value: Some(7),
5546                        },
5547                    )]),
5548                    value: None,
5549                },
5550            )]),
5551            value: None,
5552        };
5553        assert_eq!(
5554            super::resolve_field_path(&root, "metrics.count", |node, component| node.children.get(component))
5555                .and_then(|node| node.value),
5556            Some(7)
5557        );
5558        assert!(
5559            super::resolve_field_path(&root, "metrics..count", |node, component| node.children.get(component))
5560                .is_none()
5561        );
5562    }
5563
5564    #[test]
5565    fn root_set_reference_is_exact_bounded_and_canonical() -> Result<(), Box<dyn std::error::Error>> {
5566        let root = super::RootSetRef {
5567            format_version: super::ROOT_SET_REF_FORMAT_VERSION,
5568            root: super::PhysicalObjectId {
5569                backend_authority: super::BackendAuthorityId::new("standard-data")?,
5570                object_incarnation: super::ObjectIncarnation::new(9)?,
5571                object_key: super::ObjectKey::new("root-sets/0009.json")?,
5572            },
5573            content_digest: [7; 32],
5574            item_count: 4,
5575            page_count: 2,
5576            schema_generations: vec![super::SchemaGeneration::new(1), super::SchemaGeneration::new(3)],
5577        };
5578        root.validate()?;
5579        assert_eq!(
5580            serde_json::from_slice::<super::RootSetRef>(&serde_json::to_vec(&root)?)?,
5581            root
5582        );
5583
5584        let mut duplicate = root.clone();
5585        duplicate.schema_generations = vec![super::SchemaGeneration::new(1), super::SchemaGeneration::new(1)];
5586        assert_eq!(
5587            duplicate.validate(),
5588            Err(super::RootSetRefError::InvalidSchemaGenerations)
5589        );
5590        let mut empty = root;
5591        empty.item_count = 0;
5592        assert_eq!(
5593            empty.validate(),
5594            Err(super::RootSetRefError::InvalidCounts { items: 0, pages: 2 })
5595        );
5596        let mut single_page = empty;
5597        single_page.item_count = 1;
5598        single_page.page_count = 1;
5599        single_page.validate()?;
5600        let mut too_many_pages = single_page.clone();
5601        too_many_pages.page_count = 2;
5602        assert_eq!(
5603            too_many_pages.validate(),
5604            Err(super::RootSetRefError::InvalidCounts { items: 1, pages: 2 })
5605        );
5606        let mut predecessor = single_page;
5607        predecessor.format_version = 1;
5608        assert_eq!(
5609            predecessor.validate(),
5610            Err(super::RootSetRefError::UnsupportedVersion(1))
5611        );
5612        Ok(())
5613    }
5614
5615    #[test]
5616    fn kubernetes_object_uid_digest_refuses_zero_on_construction_and_decode() -> Result<(), Box<dyn std::error::Error>>
5617    {
5618        assert!(super::KubernetesObjectUidDigest::new([0; 32]).is_err());
5619        assert!(
5620            serde_json::from_str::<super::KubernetesObjectUidDigest>(&format!("[{}]", "0,".repeat(31) + "0")).is_err()
5621        );
5622        let digest = super::KubernetesObjectUidDigest::new([7; 32])?;
5623        let decoded: super::KubernetesObjectUidDigest = serde_json::from_slice(&serde_json::to_vec(&digest)?)?;
5624        assert_eq!(decoded, digest);
5625        Ok(())
5626    }
5627
5628    #[test]
5629    fn gateway_d1_semantic_identities_and_listener_profiles_are_exact() -> Result<(), Box<dyn std::error::Error>> {
5630        assert!(super::GatewayIncarnation::new([0; 32]).is_err());
5631        assert!(serde_json::from_str::<super::GatewayIncarnation>(&format!("[{}]", "0,".repeat(31) + "0")).is_err());
5632        let incarnation = super::GatewayIncarnation::new([7; 32])?;
5633        assert_eq!(
5634            serde_json::from_slice::<super::GatewayIncarnation>(&serde_json::to_vec(&incarnation)?)?,
5635            incarnation
5636        );
5637        assert_eq!(super::AuditRangeId::new(4).get(), 4);
5638        assert_eq!(super::QuotaGrantId::new(5).get(), 5);
5639        assert_eq!(super::QuotaWindow::new(6).get(), 6);
5640        assert_eq!(super::ProcessMonotonicMillis::new(7).get(), 7);
5641        assert_eq!(super::UnixMillis::new(8).get(), 8);
5642        for (listener, profile) in [
5643            (
5644                super::PublicGatewayListenerV1::LodestoneApi,
5645                super::PublicTlsProfileV1::Native,
5646            ),
5647            (
5648                super::PublicGatewayListenerV1::ElasticsearchHttp,
5649                super::PublicTlsProfileV1::Http,
5650            ),
5651            (
5652                super::PublicGatewayListenerV1::OtlpHttp,
5653                super::PublicTlsProfileV1::Http,
5654            ),
5655            (
5656                super::PublicGatewayListenerV1::JaegerHttp,
5657                super::PublicTlsProfileV1::Http,
5658            ),
5659            (
5660                super::PublicGatewayListenerV1::OtlpGrpc,
5661                super::PublicTlsProfileV1::Grpc,
5662            ),
5663            (
5664                super::PublicGatewayListenerV1::SubscriptionGrpc,
5665                super::PublicTlsProfileV1::Grpc,
5666            ),
5667            (
5668                super::PublicGatewayListenerV1::SubscriptionWebSocket,
5669                super::PublicTlsProfileV1::Http,
5670            ),
5671            (super::PublicGatewayListenerV1::Resp, super::PublicTlsProfileV1::Resp),
5672        ] {
5673            assert_eq!(listener.required_tls_profile(), profile);
5674        }
5675        Ok(())
5676    }
5677
5678    #[test]
5679    fn ordinary_segment_allocation_ignores_reserved_ids_and_stops_at_boundary() {
5680        let reserved = super::SegmentId::new(super::RESERVED_SEGMENT_ID_NAMESPACE_START + 17);
5681        assert_eq!(
5682            super::next_ordinary_segment_id([super::SegmentId::new(7), reserved, super::SegmentId::new(9),]),
5683            Some(super::SegmentId::new(10))
5684        );
5685        assert_eq!(
5686            super::next_ordinary_segment_id([reserved]),
5687            Some(super::SegmentId::new(1))
5688        );
5689        assert_eq!(
5690            super::next_ordinary_segment_id([super::SegmentId::new(super::RESERVED_SEGMENT_ID_NAMESPACE_START - 1,)]),
5691            None
5692        );
5693    }
5694
5695    #[test]
5696    fn dense_numeric_and_binary_values_decode_to_identical_semantics() -> Result<(), Box<dyn std::error::Error>> {
5697        use base64::Engine as _;
5698
5699        for (element_type, scale, numeric, bytes) in [
5700            (
5701                super::DenseVectorElementType::F32,
5702                None,
5703                vec![1.5, -2.0],
5704                [1.5_f32.to_le_bytes(), (-2.0_f32).to_le_bytes()].concat(),
5705            ),
5706            (
5707                super::DenseVectorElementType::F16,
5708                None,
5709                vec![1.5, -2.0],
5710                [0x3e00_u16.to_le_bytes(), 0xc000_u16.to_le_bytes()].concat(),
5711            ),
5712            (
5713                super::DenseVectorElementType::I8,
5714                Some(super::VectorQuantizationScale::new(0.25)?),
5715                vec![6.0, -8.0],
5716                vec![6_u8, (-8_i8) as u8],
5717            ),
5718        ] {
5719            let definition = super::DenseVectorDefinition {
5720                dimensions: 2,
5721                element_type,
5722                metric: super::VectorMetric::DotProduct,
5723                quantization_scale: scale,
5724            };
5725            let numeric = super::decode_dense_numeric_value(definition, &numeric)?;
5726            let binary = super::decode_dense_binary_value(
5727                definition,
5728                &super::DenseVectorBinaryValue {
5729                    element_type,
5730                    byte_order: super::DenseVectorByteOrder::LittleEndian,
5731                    data_base64: super::BASE64_STANDARD.encode(bytes),
5732                    quantization_scale: scale,
5733                },
5734            )?;
5735            assert_eq!(numeric, binary);
5736        }
5737        Ok(())
5738    }
5739
5740    #[test]
5741    fn dense_value_refuses_narrowing_and_noncanonical_binary_controls() -> Result<(), Box<dyn std::error::Error>> {
5742        use base64::Engine as _;
5743
5744        let f16 = super::DenseVectorDefinition {
5745            dimensions: 1,
5746            element_type: super::DenseVectorElementType::F16,
5747            metric: super::VectorMetric::DotProduct,
5748            quantization_scale: None,
5749        };
5750        assert_eq!(super::decode_dense_numeric_value(f16, &[1.000_488_281_25])?, vec![1.0]);
5751        assert_eq!(
5752            super::decode_dense_numeric_value(f16, &[1.001_464_843_75])?,
5753            vec![1.001_953_1]
5754        );
5755        assert!(matches!(
5756            super::decode_dense_numeric_value(f16, &[70_000.0]),
5757            Err(super::DenseVectorValueError::F16Overflow(0))
5758        ));
5759
5760        let i8_definition = super::DenseVectorDefinition {
5761            dimensions: 1,
5762            element_type: super::DenseVectorElementType::I8,
5763            metric: super::VectorMetric::DotProduct,
5764            quantization_scale: Some(super::VectorQuantizationScale::new(0.5)?),
5765        };
5766        assert!(matches!(
5767            super::decode_dense_numeric_value(i8_definition, &[1.5]),
5768            Err(super::DenseVectorValueError::I8ElementOutOfRange(0))
5769        ));
5770        let encoded = super::BASE64_STANDARD.encode([1_u8]);
5771        let mut binary = super::DenseVectorBinaryValue {
5772            element_type: super::DenseVectorElementType::I8,
5773            byte_order: super::DenseVectorByteOrder::LittleEndian,
5774            data_base64: encoded,
5775            quantization_scale: Some(super::VectorQuantizationScale::new(0.25)?),
5776        };
5777        assert_eq!(
5778            super::decode_dense_binary_value(i8_definition, &binary),
5779            Err(super::DenseVectorValueError::QuantizationScaleMismatch)
5780        );
5781        binary.quantization_scale = i8_definition.quantization_scale;
5782        binary.data_base64.push('=');
5783        assert!(matches!(
5784            super::decode_dense_binary_value(i8_definition, &binary),
5785            Err(super::DenseVectorValueError::InvalidBase64Length { .. })
5786        ));
5787
5788        let wrong_order =
5789            br#"{"kind":"binary","element_type":"f32","byte_order":"big_endian","data_base64":"AAAAAA=="}"#;
5790        assert!(serde_json::from_slice::<super::DenseVectorValue>(wrong_order).is_err());
5791        let signed_zero = super::decode_dense_numeric_value(
5792            super::DenseVectorDefinition {
5793                dimensions: 2,
5794                element_type: super::DenseVectorElementType::F32,
5795                metric: super::VectorMetric::DotProduct,
5796                quantization_scale: None,
5797            },
5798            &[-0.0, 0.0],
5799        )?;
5800        assert!(signed_zero.iter().all(|value| value.to_bits() == 0));
5801        assert_eq!(
5802            super::decode_dense_numeric_value(
5803                super::DenseVectorDefinition {
5804                    dimensions: 2,
5805                    element_type: super::DenseVectorElementType::F32,
5806                    metric: super::VectorMetric::Cosine,
5807                    quantization_scale: None,
5808                },
5809                &[0.0, -0.0],
5810            ),
5811            Err(super::DenseVectorValueError::ZeroNorm)
5812        );
5813        Ok(())
5814    }
5815
5816    #[test]
5817    fn dense_value_refuses_length_scale_and_numeric_boundary_controls() -> Result<(), Box<dyn std::error::Error>> {
5818        use base64::Engine as _;
5819
5820        let f32_definition = super::DenseVectorDefinition {
5821            dimensions: 2,
5822            element_type: super::DenseVectorElementType::F32,
5823            metric: super::VectorMetric::DotProduct,
5824            quantization_scale: None,
5825        };
5826        for bytes in [vec![0_u8; 4], vec![0_u8; 12]] {
5827            let value = super::DenseVectorBinaryValue {
5828                element_type: super::DenseVectorElementType::F32,
5829                byte_order: super::DenseVectorByteOrder::LittleEndian,
5830                data_base64: super::BASE64_STANDARD.encode(bytes),
5831                quantization_scale: None,
5832            };
5833            assert!(matches!(
5834                super::decode_dense_binary_value(f32_definition, &value),
5835                Err(super::DenseVectorValueError::InvalidBase64Length { .. })
5836            ));
5837        }
5838        let wrong_type = super::DenseVectorBinaryValue {
5839            element_type: super::DenseVectorElementType::F16,
5840            byte_order: super::DenseVectorByteOrder::LittleEndian,
5841            data_base64: super::BASE64_STANDARD.encode([0_u8; 8]),
5842            quantization_scale: None,
5843        };
5844        assert_eq!(
5845            super::decode_dense_binary_value(f32_definition, &wrong_type),
5846            Err(super::DenseVectorValueError::ElementTypeMismatch)
5847        );
5848        assert!(matches!(
5849            super::decode_dense_numeric_value(f32_definition, &[f64::INFINITY, 0.0]),
5850            Err(super::DenseVectorValueError::NonFiniteElement(0))
5851        ));
5852
5853        let f16_definition = super::DenseVectorDefinition {
5854            dimensions: 1,
5855            element_type: super::DenseVectorElementType::F16,
5856            metric: super::VectorMetric::DotProduct,
5857            quantization_scale: None,
5858        };
5859        assert_eq!(
5860            super::decode_dense_numeric_value(f16_definition, &[2.0_f64.powi(-25)])?[0].to_bits(),
5861            0
5862        );
5863        assert_eq!(
5864            super::decode_dense_numeric_value(f16_definition, &[2.0_f64.powi(-24)])?[0],
5865            2.0_f32.powi(-24)
5866        );
5867        assert_eq!(
5868            super::decode_dense_numeric_value(f16_definition, &[3.0 * 2.0_f64.powi(-25)])?[0],
5869            2.0_f32.powi(-23)
5870        );
5871        let infinite_half = super::DenseVectorBinaryValue {
5872            element_type: super::DenseVectorElementType::F16,
5873            byte_order: super::DenseVectorByteOrder::LittleEndian,
5874            data_base64: super::BASE64_STANDARD.encode(0x7c00_u16.to_le_bytes()),
5875            quantization_scale: None,
5876        };
5877        assert!(matches!(
5878            super::decode_dense_binary_value(f16_definition, &infinite_half),
5879            Err(super::DenseVectorValueError::NonFiniteElement(0))
5880        ));
5881
5882        let i8_definition = super::DenseVectorDefinition {
5883            dimensions: 1,
5884            element_type: super::DenseVectorElementType::I8,
5885            metric: super::VectorMetric::DotProduct,
5886            quantization_scale: Some(super::VectorQuantizationScale::new(1.0)?),
5887        };
5888        for value in [-129.0, 128.0] {
5889            assert!(matches!(
5890                super::decode_dense_numeric_value(i8_definition, &[value]),
5891                Err(super::DenseVectorValueError::I8ElementOutOfRange(0))
5892            ));
5893        }
5894        for scale in [r#""0""#, r#""-1""#, r#""1.0""#] {
5895            assert!(serde_json::from_str::<super::VectorQuantizationScale>(scale).is_err());
5896        }
5897        Ok(())
5898    }
5899}