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