Skip to main content

lodestone_types/
replayable_source.rs

1//! P13.1 replayable-source authority and current-format contracts.
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::error::Error;
5use std::fmt::{self, Display, Formatter};
6use std::future::Future;
7use std::num::{NonZeroU32, NonZeroU64, NonZeroU128};
8use std::pin::Pin;
9use std::time::Instant;
10
11use serde::de::{DeserializeOwned, Visitor};
12use serde::{Deserialize, Deserializer, Serialize, Serializer};
13
14use crate::{CollectionIndexRef, FenceToken, IndexMode, ManifestGeneration, ShardKey};
15
16pub const REPLAYABLE_SOURCE_FORMAT_VERSION: u16 = 1;
17pub const MAX_SOURCE_PARTITIONS: usize = 128;
18pub const MAX_SOURCE_RESUME_TOKEN_BYTES: usize = 4_096;
19pub const MAX_SOURCE_RECORD_BYTES: usize = 1_048_576;
20pub const MAX_SOURCE_BATCH_RECORDS: usize = 4_096;
21pub const MAX_SOURCE_BATCH_BYTES: usize = 16_777_216;
22pub const MAX_SOURCE_CONFIGURATION_BYTES: usize = 262_144;
23pub const MAX_ATOMIC_SOURCE_SHARDS: usize = 512;
24pub const MAX_ATOMIC_SOURCE_COMMAND_BYTES: usize = 1_048_576;
25
26macro_rules! nonzero_number {
27    ($name:ident, $inner:ty, $primitive:ty, $description:literal) => {
28        #[doc = $description]
29        #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
30        #[serde(transparent)]
31        pub struct $name($inner);
32
33        impl $name {
34            pub fn new(value: $primitive) -> Result<Self, SourceContractError> {
35                <$inner>::new(value)
36                    .map(Self)
37                    .ok_or(SourceContractError::ZeroIdentity(stringify!($name)))
38            }
39
40            #[must_use]
41            pub const fn get(self) -> $primitive {
42                self.0.get()
43            }
44        }
45
46        impl Display for $name {
47            fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
48                Display::fmt(&self.get(), formatter)
49            }
50        }
51    };
52}
53
54// INVARIANT: JSON cannot portably represent the complete u128 domain. Source
55// identities are therefore canonical nonzero decimal strings on every durable
56// and network boundary, while the Rust API retains its semantic numeric type.
57macro_rules! nonzero_u128_decimal {
58    ($name:ident, $description:literal) => {
59        #[doc = $description]
60        #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
61        pub struct $name(NonZeroU128);
62
63        impl $name {
64            pub fn new(value: u128) -> Result<Self, SourceContractError> {
65                NonZeroU128::new(value)
66                    .map(Self)
67                    .ok_or(SourceContractError::ZeroIdentity(stringify!($name)))
68            }
69
70            #[must_use]
71            pub const fn get(self) -> u128 {
72                self.0.get()
73            }
74        }
75
76        impl Display for $name {
77            fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
78                Display::fmt(&self.get(), formatter)
79            }
80        }
81
82        impl Serialize for $name {
83            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
84            where
85                S: Serializer,
86            {
87                serializer.serialize_str(&self.get().to_string())
88            }
89        }
90
91        impl<'de> Deserialize<'de> for $name {
92            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
93            where
94                D: Deserializer<'de>,
95            {
96                struct CanonicalNonzeroDecimalVisitor;
97
98                impl Visitor<'_> for CanonicalNonzeroDecimalVisitor {
99                    type Value = $name;
100
101                    fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
102                        formatter.write_str("a canonical nonzero u128 decimal string")
103                    }
104
105                    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
106                    where
107                        E: serde::de::Error,
108                    {
109                        let bytes = value.as_bytes();
110                        if bytes.is_empty()
111                            || !matches!(bytes[0], b'1'..=b'9')
112                            || !bytes[1..].iter().all(u8::is_ascii_digit)
113                        {
114                            return Err(E::custom("expected a canonical nonzero u128 decimal string"));
115                        }
116                        let parsed = value
117                            .parse::<u128>()
118                            .map_err(|_| E::custom("source identity exceeds u128"))?;
119                        NonZeroU128::new(parsed)
120                            .map($name)
121                            .ok_or_else(|| E::custom("source identity cannot be zero"))
122                    }
123                }
124
125                deserializer.deserialize_str(CanonicalNonzeroDecimalVisitor)
126            }
127        }
128    };
129}
130
131nonzero_number!(
132    SourceIncarnation,
133    NonZeroU64,
134    u64,
135    "A never-reused lifetime of one exact external source binding."
136);
137/// A stable, zero-based partition identity inside one source incarnation.
138#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
139#[serde(transparent)]
140pub struct SourcePartitionId(u32);
141
142impl SourcePartitionId {
143    #[must_use]
144    pub const fn new(value: u32) -> Self {
145        Self(value)
146    }
147
148    #[must_use]
149    pub const fn get(self) -> u32 {
150        self.0
151    }
152}
153
154impl Display for SourcePartitionId {
155    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
156        Display::fmt(&self.0, formatter)
157    }
158}
159nonzero_number!(
160    SourcePartitionIncarnation,
161    NonZeroU64,
162    u64,
163    "A never-reused lifetime of one source partition identity."
164);
165nonzero_u128_decimal!(
166    SourceOperationId,
167    "A stable operation identity reproduced by every replay."
168);
169nonzero_u128_decimal!(
170    SourceTransactionId,
171    "A stable source transaction identity reproduced by every replay."
172);
173nonzero_u128_decimal!(SourceBatchId, "A stable identity for one bounded provider batch.");
174nonzero_number!(
175    SourceControlGeneration,
176    NonZeroU64,
177    u64,
178    "The monotonic generation of one replayable-source lifecycle state."
179);
180
181/// An exclusive per-partition source position: the next ordinal to return.
182#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
183#[serde(transparent)]
184pub struct SourceOrdinal(u64);
185
186impl SourceOrdinal {
187    #[must_use]
188    pub const fn new(value: u64) -> Self {
189        Self(value)
190    }
191
192    #[must_use]
193    pub const fn get(self) -> u64 {
194        self.0
195    }
196
197    #[must_use]
198    pub const fn checked_next(self) -> Option<Self> {
199        match self.0.checked_add(1) {
200            Some(value) => Some(Self(value)),
201            None => None,
202        }
203    }
204}
205
206impl Display for SourceOrdinal {
207    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
208        Display::fmt(&self.0, formatter)
209    }
210}
211
212#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
213#[serde(rename_all = "snake_case")]
214pub enum SourceProviderKindV1 {
215    Kafka,
216    ObjectLog,
217    HttpReplay,
218    GrpcReplay,
219}
220
221#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
222#[serde(rename_all = "snake_case")]
223pub enum SourceDecoderKindV1 {
224    NativeDocumentJson,
225    DebeziumPostgresJson,
226}
227
228#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
229#[serde(deny_unknown_fields)]
230pub struct SourceReplayGuaranteesV1 {
231    pub durable_replay: bool,
232    pub stable_partition_identity: bool,
233    pub stable_operation_identity: bool,
234    pub ordered_positions: bool,
235    pub bounded_reads: bool,
236    pub explicit_retention: bool,
237    pub explicit_rewind: bool,
238    pub minimum_replay_window_millis: u64,
239}
240
241impl SourceReplayGuaranteesV1 {
242    pub fn validate(&self) -> Result<(), SourceContractError> {
243        if !self.durable_replay
244            || !self.stable_partition_identity
245            || !self.stable_operation_identity
246            || !self.ordered_positions
247            || !self.bounded_reads
248            || !self.explicit_retention
249            || !self.explicit_rewind
250            || self.minimum_replay_window_millis == 0
251        {
252            return Err(SourceContractError::Violation(
253                "WAL elision requires every replay guarantee and a positive replay window",
254            ));
255        }
256        Ok(())
257    }
258}
259
260#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
261#[serde(deny_unknown_fields)]
262pub struct SourceCursorV1 {
263    /// Exclusive position: the next record the provider must return.
264    pub next_ordinal: SourceOrdinal,
265    /// Provider-owned opaque resume material. It is metadata, never a secret.
266    pub resume_token: Vec<u8>,
267}
268
269impl SourceCursorV1 {
270    pub fn validate(&self) -> Result<(), SourceContractError> {
271        if self.resume_token.len() > MAX_SOURCE_RESUME_TOKEN_BYTES {
272            return Err(SourceContractError::Budget {
273                field: "source resume token bytes",
274                observed: self.resume_token.len(),
275                maximum: MAX_SOURCE_RESUME_TOKEN_BYTES,
276            });
277        }
278        Ok(())
279    }
280}
281
282#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
283#[serde(deny_unknown_fields)]
284pub struct SourcePartitionCheckpointV1 {
285    pub partition_id: SourcePartitionId,
286    pub partition_incarnation: SourcePartitionIncarnation,
287    pub cursor: SourceCursorV1,
288}
289
290#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
291#[serde(deny_unknown_fields)]
292pub struct SourceCutV1 {
293    pub format_version: u16,
294    pub source_incarnation: SourceIncarnation,
295    pub partitions: Vec<SourcePartitionCheckpointV1>,
296}
297
298impl SourceCutV1 {
299    pub fn validate(&self) -> Result<(), SourceContractError> {
300        validate_format(self.format_version)?;
301        if self.partitions.is_empty() || self.partitions.len() > MAX_SOURCE_PARTITIONS {
302            return Err(SourceContractError::Budget {
303                field: "source partitions",
304                observed: self.partitions.len(),
305                maximum: MAX_SOURCE_PARTITIONS,
306            });
307        }
308        let mut prior = None;
309        for checkpoint in &self.partitions {
310            if prior.is_some_and(|value| value >= checkpoint.partition_id) {
311                return Err(SourceContractError::Violation(
312                    "partition checkpoints must be strictly ordered and unique",
313                ));
314            }
315            checkpoint.cursor.validate()?;
316            prior = Some(checkpoint.partition_id);
317        }
318        Ok(())
319    }
320
321    pub fn advances_to(&self, replacement: &Self) -> Result<(), SourceContractError> {
322        self.validate()?;
323        replacement.validate()?;
324        if self.source_incarnation != replacement.source_incarnation
325            || self.partitions.len() != replacement.partitions.len()
326        {
327            return Err(SourceContractError::Violation(
328                "a source cut cannot change source incarnation or partition membership",
329            ));
330        }
331        let mut advanced = false;
332        for (expected, next) in self.partitions.iter().zip(&replacement.partitions) {
333            if expected.partition_id != next.partition_id
334                || expected.partition_incarnation != next.partition_incarnation
335                || next.cursor.next_ordinal < expected.cursor.next_ordinal
336                || (next.cursor.next_ordinal == expected.cursor.next_ordinal
337                    && next.cursor.resume_token != expected.cursor.resume_token)
338            {
339                return Err(SourceContractError::Violation(
340                    "replacement source cut must preserve partition identities and move forward",
341                ));
342            }
343            advanced |= next.cursor.next_ordinal > expected.cursor.next_ordinal;
344        }
345        if !advanced {
346            return Err(SourceContractError::Violation(
347                "replacement source cut must advance at least one partition",
348            ));
349        }
350        Ok(())
351    }
352}
353
354#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
355#[serde(deny_unknown_fields)]
356pub struct SourcePartitionBindingV1 {
357    pub partition_id: SourcePartitionId,
358    pub partition_incarnation: SourcePartitionIncarnation,
359}
360
361#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
362#[serde(deny_unknown_fields)]
363pub struct SourceBindingV1 {
364    pub format_version: u16,
365    pub collection_index_ref: CollectionIndexRef,
366    pub source_incarnation: SourceIncarnation,
367    pub source_identity_digest: [u8; 32],
368    pub provider: SourceProviderKindV1,
369    pub decoder: SourceDecoderKindV1,
370    pub partitions: Vec<SourcePartitionBindingV1>,
371    pub guarantees: SourceReplayGuaranteesV1,
372}
373
374impl SourceBindingV1 {
375    pub fn validate(&self) -> Result<(), SourceContractError> {
376        validate_format(self.format_version)?;
377        if self.source_identity_digest == [0; 32] {
378            return Err(SourceContractError::ZeroIdentity("source_identity_digest"));
379        }
380        if self.partitions.is_empty() || self.partitions.len() > MAX_SOURCE_PARTITIONS {
381            return Err(SourceContractError::Budget {
382                field: "source partitions",
383                observed: self.partitions.len(),
384                maximum: MAX_SOURCE_PARTITIONS,
385            });
386        }
387        let mut prior = None;
388        for partition in &self.partitions {
389            if prior.is_some_and(|value| value >= partition.partition_id) {
390                return Err(SourceContractError::Violation(
391                    "source partition bindings must be strictly ordered and unique",
392                ));
393            }
394            prior = Some(partition.partition_id);
395        }
396        self.guarantees.validate()
397    }
398
399    pub fn validate_cut(&self, cut: &SourceCutV1) -> Result<(), SourceContractError> {
400        self.validate()?;
401        cut.validate()?;
402        if self.source_incarnation != cut.source_incarnation || self.partitions.len() != cut.partitions.len() {
403            return Err(SourceContractError::Violation(
404                "source cut does not belong to the catalog source binding",
405            ));
406        }
407        for (binding, checkpoint) in self.partitions.iter().zip(&cut.partitions) {
408            if binding.partition_id != checkpoint.partition_id
409                || binding.partition_incarnation != checkpoint.partition_incarnation
410            {
411                return Err(SourceContractError::Violation(
412                    "source cut partition identity differs from the catalog binding",
413                ));
414            }
415        }
416        Ok(())
417    }
418}
419
420#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
421#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
422pub enum IngestAuthorityV1 {
423    PushWithWal {
424        format_version: u16,
425    },
426    ReplayableSource {
427        format_version: u16,
428        binding: SourceBindingV1,
429    },
430}
431
432impl IngestAuthorityV1 {
433    #[must_use]
434    pub const fn push_with_wal() -> Self {
435        Self::PushWithWal {
436            format_version: REPLAYABLE_SOURCE_FORMAT_VERSION,
437        }
438    }
439
440    #[must_use]
441    pub const fn replayable_source(binding: SourceBindingV1) -> Self {
442        Self::ReplayableSource {
443            format_version: REPLAYABLE_SOURCE_FORMAT_VERSION,
444            binding,
445        }
446    }
447
448    pub fn validate_for_mode(&self, mode: IndexMode) -> Result<(), SourceContractError> {
449        match self {
450            Self::PushWithWal { format_version } => validate_format(*format_version),
451            Self::ReplayableSource {
452                format_version,
453                binding,
454            } if mode.is_state() => {
455                validate_format(*format_version)?;
456                let _ = binding;
457                Err(SourceContractError::UnsupportedMode(mode))
458            }
459            Self::ReplayableSource {
460                format_version,
461                binding,
462            } => {
463                validate_format(*format_version)?;
464                binding.validate()
465            }
466        }
467    }
468}
469
470#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
471#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
472pub enum SourceTransactionMembershipV1 {
473    Standalone,
474    Transaction {
475        transaction_id: SourceTransactionId,
476        member_index: u32,
477        member_count: NonZeroU32,
478    },
479}
480
481#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
482#[serde(deny_unknown_fields)]
483pub struct RawSourceRecordV1 {
484    pub source_incarnation: SourceIncarnation,
485    pub partition_id: SourcePartitionId,
486    pub partition_incarnation: SourcePartitionIncarnation,
487    pub position: SourceCursorV1,
488    pub next_position: SourceCursorV1,
489    pub operation_id: SourceOperationId,
490    pub transaction: SourceTransactionMembershipV1,
491    pub key: Vec<u8>,
492    pub payload: Vec<u8>,
493}
494
495impl RawSourceRecordV1 {
496    pub fn validate(&self) -> Result<(), SourceContractError> {
497        self.position.validate()?;
498        self.next_position.validate()?;
499        let bytes = self
500            .key
501            .len()
502            .checked_add(self.payload.len())
503            .ok_or(SourceContractError::ArithmeticOverflow)?;
504        if bytes > MAX_SOURCE_RECORD_BYTES {
505            return Err(SourceContractError::Budget {
506                field: "source record bytes",
507                observed: bytes,
508                maximum: MAX_SOURCE_RECORD_BYTES,
509            });
510        }
511        if self.next_position.next_ordinal <= self.position.next_ordinal {
512            return Err(SourceContractError::Violation(
513                "a source record must advance its exclusive cursor",
514            ));
515        }
516        if let SourceTransactionMembershipV1::Transaction {
517            member_index,
518            member_count,
519            ..
520        } = self.transaction
521            && member_index >= member_count.get()
522        {
523            return Err(SourceContractError::Violation(
524                "source transaction member index is outside member count",
525            ));
526        }
527        Ok(())
528    }
529}
530
531#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
532#[serde(deny_unknown_fields)]
533pub struct SourceBatchV1 {
534    pub format_version: u16,
535    pub batch_id: SourceBatchId,
536    pub expected_cut: SourceCutV1,
537    pub replacement_cut: SourceCutV1,
538    pub records: Vec<RawSourceRecordV1>,
539}
540
541impl SourceBatchV1 {
542    pub fn validate(&self) -> Result<(), SourceContractError> {
543        validate_format(self.format_version)?;
544        self.expected_cut.advances_to(&self.replacement_cut)?;
545        if self.records.is_empty() || self.records.len() > MAX_SOURCE_BATCH_RECORDS {
546            return Err(SourceContractError::Budget {
547                field: "source batch records",
548                observed: self.records.len(),
549                maximum: MAX_SOURCE_BATCH_RECORDS,
550            });
551        }
552        let encoded = serde_json::to_vec(self).map_err(SourceContractError::serde)?;
553        if encoded.len() > MAX_SOURCE_BATCH_BYTES {
554            return Err(SourceContractError::Budget {
555                field: "source batch bytes",
556                observed: encoded.len(),
557                maximum: MAX_SOURCE_BATCH_BYTES,
558            });
559        }
560
561        let mut cursors = self
562            .expected_cut
563            .partitions
564            .iter()
565            .map(|checkpoint| {
566                (
567                    checkpoint.partition_id,
568                    (checkpoint.partition_incarnation, checkpoint.cursor.clone()),
569                )
570            })
571            .collect::<BTreeMap<_, _>>();
572        let mut operations = BTreeSet::new();
573        let mut transactions = BTreeMap::<SourceTransactionId, (u32, BTreeSet<u32>)>::new();
574        let mut prior_record_key = None;
575        for record in &self.records {
576            record.validate()?;
577            let record_key = (record.partition_id, record.position.next_ordinal, record.operation_id);
578            if prior_record_key.is_some_and(|prior| prior >= record_key) {
579                return Err(SourceContractError::Violation(
580                    "source records must be strictly ordered by partition, position, and operation",
581                ));
582            }
583            prior_record_key = Some(record_key);
584            if record.source_incarnation != self.expected_cut.source_incarnation {
585                return Err(SourceContractError::Violation("source record incarnation mismatch"));
586            }
587            let (partition_incarnation, cursor) = cursors
588                .get_mut(&record.partition_id)
589                .ok_or(SourceContractError::Violation("source record has an unknown partition"))?;
590            if *partition_incarnation != record.partition_incarnation || *cursor != record.position {
591                return Err(SourceContractError::Violation(
592                    "source records must form a gap-free cursor chain",
593                ));
594            }
595            *cursor = record.next_position.clone();
596            if !operations.insert(record.operation_id) {
597                return Err(SourceContractError::Violation(
598                    "source operation identity was duplicated",
599                ));
600            }
601            if let SourceTransactionMembershipV1::Transaction {
602                transaction_id,
603                member_index,
604                member_count,
605            } = record.transaction
606            {
607                let entry = transactions
608                    .entry(transaction_id)
609                    .or_insert_with(|| (member_count.get(), BTreeSet::new()));
610                if entry.0 != member_count.get() || !entry.1.insert(member_index) {
611                    return Err(SourceContractError::Violation(
612                        "source transaction membership is inconsistent or duplicated",
613                    ));
614                }
615            }
616        }
617        for (_, (member_count, members)) in transactions {
618            if members.len() != member_count as usize || members.iter().copied().ne(0..member_count) {
619                return Err(SourceContractError::Violation(
620                    "a source batch must contain every member of each admitted transaction",
621                ));
622            }
623        }
624        for replacement in &self.replacement_cut.partitions {
625            let observed = cursors
626                .get(&replacement.partition_id)
627                .ok_or(SourceContractError::Violation(
628                    "replacement cut has an unknown partition",
629                ))?;
630            if observed.0 != replacement.partition_incarnation || observed.1 != replacement.cursor {
631                return Err(SourceContractError::Violation(
632                    "replacement cut must equal the exact end of the record chains",
633                ));
634            }
635        }
636        Ok(())
637    }
638}
639
640#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
641#[serde(deny_unknown_fields)]
642pub struct SourceShardPublicationV1 {
643    pub shard_key: ShardKey,
644    pub fence_token: FenceToken,
645    pub expected_generation: ManifestGeneration,
646    pub replacement_generation: ManifestGeneration,
647    pub replacement_root_digest: [u8; 32],
648}
649
650#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
651#[serde(deny_unknown_fields)]
652/// The sole bounded multi-shard publication primitive for one replayable
653/// source batch. It is internal authority state, not a client transaction.
654pub struct AtomicSourcePublicationV1 {
655    pub format_version: u16,
656    pub collection_index_ref: CollectionIndexRef,
657    pub source_incarnation: SourceIncarnation,
658    pub batch_id: SourceBatchId,
659    pub expected_cut: SourceCutV1,
660    pub replacement_cut: SourceCutV1,
661    pub operation_count: u32,
662    pub operation_set_digest: [u8; 32],
663    pub committed_transactions: Vec<SourceTransactionId>,
664    pub shard_publications: Vec<SourceShardPublicationV1>,
665}
666
667impl AtomicSourcePublicationV1 {
668    pub fn validate(&self) -> Result<(), SourceContractError> {
669        validate_format(self.format_version)?;
670        self.expected_cut.advances_to(&self.replacement_cut)?;
671        if self.source_incarnation != self.expected_cut.source_incarnation
672            || self.operation_count == 0
673            || self.operation_set_digest == [0; 32]
674        {
675            return Err(SourceContractError::Violation(
676                "atomic source publication identity, operation count, or operation digest is invalid",
677            ));
678        }
679        if self.shard_publications.is_empty() || self.shard_publications.len() > MAX_ATOMIC_SOURCE_SHARDS {
680            return Err(SourceContractError::Budget {
681                field: "atomic source shards",
682                observed: self.shard_publications.len(),
683                maximum: MAX_ATOMIC_SOURCE_SHARDS,
684            });
685        }
686        let mut prior_shard = None;
687        for publication in &self.shard_publications {
688            if publication.shard_key.collection_index_ref() != self.collection_index_ref
689                || prior_shard.is_some_and(|prior| prior >= publication.shard_key)
690                || publication.expected_generation.checked_next() != Some(publication.replacement_generation)
691                || publication.replacement_root_digest == [0; 32]
692            {
693                return Err(SourceContractError::Violation(
694                    "source shard publications must be scoped, ordered, unique, and one-generation successors",
695                ));
696            }
697            prior_shard = Some(publication.shard_key);
698        }
699        if self.committed_transactions.windows(2).any(|pair| pair[0] >= pair[1]) {
700            return Err(SourceContractError::Violation(
701                "committed source transactions must be strictly ordered and unique",
702            ));
703        }
704        let encoded = serde_json::to_vec(self).map_err(SourceContractError::serde)?;
705        if encoded.len() > MAX_ATOMIC_SOURCE_COMMAND_BYTES {
706            return Err(SourceContractError::Budget {
707                field: "atomic source command bytes",
708                observed: encoded.len(),
709                maximum: MAX_ATOMIC_SOURCE_COMMAND_BYTES,
710            });
711        }
712        Ok(())
713    }
714
715    pub fn validate_against_batch(&self, batch: &SourceBatchV1) -> Result<(), SourceContractError> {
716        self.validate()?;
717        batch.validate()?;
718        if self.source_incarnation != batch.expected_cut.source_incarnation
719            || self.batch_id != batch.batch_id
720            || self.expected_cut != batch.expected_cut
721            || self.replacement_cut != batch.replacement_cut
722            || usize::try_from(self.operation_count).ok() != Some(batch.records.len())
723            || self.operation_set_digest != source_batch_operation_set_digest_v1(batch)?
724            || self.committed_transactions != source_batch_transaction_ids_v1(batch)?
725        {
726            return Err(SourceContractError::Violation(
727                "atomic source publication does not exactly summarize its validated source batch",
728            ));
729        }
730        Ok(())
731    }
732}
733
734pub fn source_batch_operation_set_digest_v1(batch: &SourceBatchV1) -> Result<[u8; 32], SourceContractError> {
735    batch.validate()?;
736    let mut hasher = blake3::Hasher::new();
737    hasher.update(b"lodestone:source-operation-set:v1\0");
738    for record in &batch.records {
739        let encoded = serde_json::to_vec(record).map_err(SourceContractError::serde)?;
740        let length = u64::try_from(encoded.len()).map_err(|_| SourceContractError::ArithmeticOverflow)?;
741        hasher.update(&length.to_be_bytes());
742        hasher.update(&encoded);
743    }
744    Ok(*hasher.finalize().as_bytes())
745}
746
747pub fn source_batch_transaction_ids_v1(batch: &SourceBatchV1) -> Result<Vec<SourceTransactionId>, SourceContractError> {
748    batch.validate()?;
749    Ok(batch
750        .records
751        .iter()
752        .filter_map(|record| match record.transaction {
753            SourceTransactionMembershipV1::Standalone => None,
754            SourceTransactionMembershipV1::Transaction { transaction_id, .. } => Some(transaction_id),
755        })
756        .collect::<BTreeSet<_>>()
757        .into_iter()
758        .collect())
759}
760
761#[derive(Clone, Debug, Eq, PartialEq)]
762pub struct SourceFetchRequestV1 {
763    pub cut: SourceCutV1,
764    pub maximum_records: usize,
765    pub maximum_bytes: usize,
766    pub deadline: Instant,
767}
768
769impl SourceFetchRequestV1 {
770    pub fn validate(&self, now: Instant) -> Result<(), SourceContractError> {
771        self.cut.validate()?;
772        if self.maximum_records == 0 || self.maximum_records > MAX_SOURCE_BATCH_RECORDS {
773            return Err(SourceContractError::Budget {
774                field: "fetch records",
775                observed: self.maximum_records,
776                maximum: MAX_SOURCE_BATCH_RECORDS,
777            });
778        }
779        if self.maximum_bytes == 0 || self.maximum_bytes > MAX_SOURCE_BATCH_BYTES {
780            return Err(SourceContractError::Budget {
781                field: "fetch bytes",
782                observed: self.maximum_bytes,
783                maximum: MAX_SOURCE_BATCH_BYTES,
784            });
785        }
786        if self.deadline <= now {
787            return Err(SourceContractError::DeadlineElapsed);
788        }
789        Ok(())
790    }
791}
792
793#[derive(Clone, Debug, Eq, PartialEq)]
794pub enum SourceHighWatermarkV1 {
795    Known { cursor: SourceCursorV1 },
796    Unknown,
797}
798
799#[derive(Clone, Debug, Eq, PartialEq)]
800pub struct SourceFetchResponseV1 {
801    pub batch: SourceBatchV1,
802    pub high_watermarks: Vec<(SourcePartitionId, SourceHighWatermarkV1)>,
803    pub retention_floors: Vec<(SourcePartitionId, SourceCursorV1)>,
804}
805
806impl SourceFetchResponseV1 {
807    pub fn validate(&self) -> Result<(), SourceContractError> {
808        self.batch.validate()?;
809        if self.high_watermarks.len() != self.batch.replacement_cut.partitions.len() {
810            return Err(SourceContractError::Violation(
811                "source response must report one watermark state per partition",
812            ));
813        }
814        if self.retention_floors.len() != self.batch.expected_cut.partitions.len() {
815            return Err(SourceContractError::Violation(
816                "source response must report one retention floor per partition",
817            ));
818        }
819        for (reported, checkpoint) in self.high_watermarks.iter().zip(&self.batch.replacement_cut.partitions) {
820            if reported.0 != checkpoint.partition_id {
821                return Err(SourceContractError::Violation(
822                    "source watermarks must follow the complete ordered partition cut",
823                ));
824            }
825            if let SourceHighWatermarkV1::Known { cursor } = &reported.1 {
826                cursor.validate()?;
827                if cursor.next_ordinal < checkpoint.cursor.next_ordinal
828                    || (cursor.next_ordinal == checkpoint.cursor.next_ordinal
829                        && cursor.resume_token != checkpoint.cursor.resume_token)
830                {
831                    return Err(SourceContractError::Violation(
832                        "known source watermark cannot precede the fetched replacement cut",
833                    ));
834                }
835            }
836        }
837        for (reported, checkpoint) in self.retention_floors.iter().zip(&self.batch.expected_cut.partitions) {
838            reported.1.validate()?;
839            if reported.0 != checkpoint.partition_id
840                || reported.1.next_ordinal > checkpoint.cursor.next_ordinal
841                || (reported.1.next_ordinal == checkpoint.cursor.next_ordinal
842                    && reported.1.resume_token != checkpoint.cursor.resume_token)
843            {
844                return Err(SourceContractError::Violation(
845                    "source retention floors must cover the complete requested cut",
846                ));
847            }
848        }
849        Ok(())
850    }
851}
852
853#[derive(Clone, Debug, Eq, PartialEq)]
854pub enum DecodedSourceMutationV1 {
855    /// A source control record whose position must advance atomically with the
856    /// surrounding batch but which produces no document mutation.
857    Control { operation_id: SourceOperationId },
858    Put {
859        operation_id: SourceOperationId,
860        document_key: Vec<u8>,
861        source: Vec<u8>,
862    },
863    Delete {
864        operation_id: SourceOperationId,
865        document_key: Vec<u8>,
866    },
867}
868
869pub type SourceProviderFuture<'a> =
870    Pin<Box<dyn Future<Output = Result<SourceFetchResponseV1, SourceProviderError>> + Send + 'a>>;
871
872/// Provider owns replay position and stable raw-record identity; it never decodes documents.
873pub trait ReplayableSourceProvider: Send {
874    fn provider_kind(&self) -> SourceProviderKindV1;
875    fn fetch<'a>(&'a mut self, request: &'a SourceFetchRequestV1) -> SourceProviderFuture<'a>;
876}
877
878/// Decoder owns strict envelope-to-mutation semantics; it never owns source position.
879pub trait SourceRecordDecoder: Send + Sync {
880    fn decoder_kind(&self) -> SourceDecoderKindV1;
881    fn decode(&self, record: &RawSourceRecordV1) -> Result<Vec<DecodedSourceMutationV1>, SourceDecodeError>;
882}
883
884#[derive(Clone, Debug, Eq, PartialEq)]
885pub enum SourceProviderError {
886    Contract(SourceContractError),
887    TemporarilyUnavailable,
888    AuthenticationRefused,
889    SourceIdentityChanged,
890    AmbiguousPosition(SourcePartitionId),
891    RetentionExpired {
892        partition_id: SourcePartitionId,
893        requested: SourceCursorV1,
894        earliest_available: SourceCursorV1,
895    },
896}
897
898impl Display for SourceProviderError {
899    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
900        match self {
901            Self::Contract(error) => Display::fmt(error, formatter),
902            Self::TemporarilyUnavailable => formatter.write_str("replay source is temporarily unavailable"),
903            Self::AuthenticationRefused => formatter.write_str("replay source authentication was refused"),
904            Self::SourceIdentityChanged => formatter.write_str("replay source identity changed"),
905            Self::AmbiguousPosition(partition_id) => {
906                write!(
907                    formatter,
908                    "replay source position is ambiguous for partition {partition_id}"
909                )
910            }
911            Self::RetentionExpired {
912                partition_id,
913                requested,
914                earliest_available,
915            } => write!(
916                formatter,
917                "replay source retention expired for partition {partition_id}: requested {}, earliest available {}",
918                requested.next_ordinal, earliest_available.next_ordinal
919            ),
920        }
921    }
922}
923
924impl Error for SourceProviderError {}
925
926impl From<SourceContractError> for SourceProviderError {
927    fn from(error: SourceContractError) -> Self {
928        Self::Contract(error)
929    }
930}
931
932#[derive(Clone, Debug, Eq, PartialEq)]
933pub enum SourceDecodeError {
934    Contract(SourceContractError),
935    UnsupportedEnvelopeVersion(u32),
936    MissingTransactionMetadata(SourceOperationId),
937    PoisonRecord(SourceOperationId),
938    SchemaRefused(SourceOperationId),
939}
940
941impl Display for SourceDecodeError {
942    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
943        match self {
944            Self::Contract(error) => Display::fmt(error, formatter),
945            Self::UnsupportedEnvelopeVersion(version) => {
946                write!(formatter, "unsupported source envelope version {version}")
947            }
948            Self::MissingTransactionMetadata(operation_id) => {
949                write!(
950                    formatter,
951                    "source operation {operation_id} is missing transaction metadata"
952                )
953            }
954            Self::PoisonRecord(operation_id) => write!(formatter, "source operation {operation_id} is malformed"),
955            Self::SchemaRefused(operation_id) => {
956                write!(
957                    formatter,
958                    "source operation {operation_id} was refused by the index schema"
959                )
960            }
961        }
962    }
963}
964
965impl Error for SourceDecodeError {}
966
967impl From<SourceContractError> for SourceDecodeError {
968    fn from(error: SourceContractError) -> Self {
969        Self::Contract(error)
970    }
971}
972
973pub fn encode_source_binding_v1(binding: &SourceBindingV1) -> Result<Vec<u8>, SourceContractError> {
974    binding.validate()?;
975    encode_with_budget(binding, "source configuration bytes", MAX_SOURCE_CONFIGURATION_BYTES)
976}
977
978pub fn encode_ingest_authority_v1(authority: &IngestAuthorityV1) -> Result<Vec<u8>, SourceContractError> {
979    authority.validate_for_mode(IndexMode::Mutable)?;
980    encode_with_budget(authority, "source configuration bytes", MAX_SOURCE_CONFIGURATION_BYTES)
981}
982
983pub fn decode_ingest_authority_v1(bytes: &[u8]) -> Result<IngestAuthorityV1, SourceContractError> {
984    decode_current(
985        bytes,
986        "source configuration bytes",
987        MAX_SOURCE_CONFIGURATION_BYTES,
988        |authority: &IngestAuthorityV1| authority.validate_for_mode(IndexMode::Mutable),
989    )
990}
991
992pub fn decode_source_binding_v1(bytes: &[u8]) -> Result<SourceBindingV1, SourceContractError> {
993    decode_current(
994        bytes,
995        "source configuration bytes",
996        MAX_SOURCE_CONFIGURATION_BYTES,
997        SourceBindingV1::validate,
998    )
999}
1000
1001pub fn encode_source_batch_v1(batch: &SourceBatchV1) -> Result<Vec<u8>, SourceContractError> {
1002    batch.validate()?;
1003    serde_json::to_vec(batch).map_err(SourceContractError::serde)
1004}
1005
1006pub fn decode_source_batch_v1(bytes: &[u8]) -> Result<SourceBatchV1, SourceContractError> {
1007    decode_current(
1008        bytes,
1009        "source batch bytes",
1010        MAX_SOURCE_BATCH_BYTES,
1011        SourceBatchV1::validate,
1012    )
1013}
1014
1015pub fn encode_atomic_source_publication_v1(commit: &AtomicSourcePublicationV1) -> Result<Vec<u8>, SourceContractError> {
1016    commit.validate()?;
1017    serde_json::to_vec(commit).map_err(SourceContractError::serde)
1018}
1019
1020pub fn decode_atomic_source_publication_v1(bytes: &[u8]) -> Result<AtomicSourcePublicationV1, SourceContractError> {
1021    decode_current(
1022        bytes,
1023        "atomic source command bytes",
1024        MAX_ATOMIC_SOURCE_COMMAND_BYTES,
1025        AtomicSourcePublicationV1::validate,
1026    )
1027}
1028
1029fn decode_current<T>(
1030    bytes: &[u8],
1031    budget_field: &'static str,
1032    maximum_bytes: usize,
1033    validate: impl FnOnce(&T) -> Result<(), SourceContractError>,
1034) -> Result<T, SourceContractError>
1035where
1036    T: DeserializeOwned + Serialize,
1037{
1038    #[derive(Deserialize)]
1039    struct FormatProbe {
1040        format_version: Option<u16>,
1041    }
1042
1043    validate_encoded_budget(bytes.len(), budget_field, maximum_bytes)?;
1044    let probe: FormatProbe = serde_json::from_slice(bytes).map_err(SourceContractError::serde)?;
1045    let version = probe.format_version.ok_or(SourceContractError::MissingFormatVersion)?;
1046    validate_format(version)?;
1047    // INVARIANT: decode the original bytes directly. An intermediate map
1048    // would collapse duplicate keys before serde can refuse them.
1049    let decoded: T = serde_json::from_slice(bytes).map_err(SourceContractError::serde)?;
1050    validate(&decoded)?;
1051    let canonical = serde_json::to_vec(&decoded).map_err(SourceContractError::serde)?;
1052    if canonical != bytes {
1053        return Err(SourceContractError::NonCanonical);
1054    }
1055    Ok(decoded)
1056}
1057
1058fn encode_with_budget<T: Serialize>(
1059    value: &T,
1060    budget_field: &'static str,
1061    maximum_bytes: usize,
1062) -> Result<Vec<u8>, SourceContractError> {
1063    let encoded = serde_json::to_vec(value).map_err(SourceContractError::serde)?;
1064    validate_encoded_budget(encoded.len(), budget_field, maximum_bytes)?;
1065    Ok(encoded)
1066}
1067
1068fn validate_encoded_budget(observed: usize, field: &'static str, maximum: usize) -> Result<(), SourceContractError> {
1069    if observed > maximum {
1070        return Err(SourceContractError::Budget {
1071            field,
1072            observed,
1073            maximum,
1074        });
1075    }
1076    Ok(())
1077}
1078
1079fn validate_format(version: u16) -> Result<(), SourceContractError> {
1080    if version != REPLAYABLE_SOURCE_FORMAT_VERSION {
1081        return Err(SourceContractError::UnsupportedFormat(version));
1082    }
1083    Ok(())
1084}
1085
1086#[derive(Clone, Debug, Eq, PartialEq)]
1087pub enum SourceContractError {
1088    UnsupportedFormat(u16),
1089    MissingFormatVersion,
1090    NonCanonical,
1091    ZeroIdentity(&'static str),
1092    UnsupportedMode(IndexMode),
1093    Budget {
1094        field: &'static str,
1095        observed: usize,
1096        maximum: usize,
1097    },
1098    Violation(&'static str),
1099    DeadlineElapsed,
1100    ArithmeticOverflow,
1101    Serde(String),
1102}
1103
1104impl SourceContractError {
1105    fn serde(error: serde_json::Error) -> Self {
1106        Self::Serde(error.to_string())
1107    }
1108}
1109
1110impl Display for SourceContractError {
1111    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
1112        match self {
1113            Self::UnsupportedFormat(version) => write!(formatter, "unsupported replayable-source format {version}"),
1114            Self::MissingFormatVersion => formatter.write_str("replayable-source format version is missing"),
1115            Self::NonCanonical => formatter.write_str("replayable-source bytes are not canonical current format"),
1116            Self::ZeroIdentity(kind) => write!(formatter, "{kind} cannot use the reserved zero identity"),
1117            Self::UnsupportedMode(mode) => write!(
1118                formatter,
1119                "replayable-source authority is unavailable for {mode:?} indexes"
1120            ),
1121            Self::Budget {
1122                field,
1123                observed,
1124                maximum,
1125            } => {
1126                write!(formatter, "{field} {observed} is outside 1..={maximum}")
1127            }
1128            Self::Violation(message) => formatter.write_str(message),
1129            Self::DeadlineElapsed => formatter.write_str("source fetch deadline elapsed"),
1130            Self::ArithmeticOverflow => formatter.write_str("source contract arithmetic overflowed"),
1131            Self::Serde(message) => write!(formatter, "replayable-source codec refused input: {message}"),
1132        }
1133    }
1134}
1135
1136impl Error for SourceContractError {}
1137
1138#[cfg(test)]
1139mod tests {
1140    use super::*;
1141    use crate::{CollectionId, Epoch, IndexId, RecoveryEra, ShardId};
1142
1143    fn scope() -> Result<CollectionIndexRef, Box<dyn Error>> {
1144        Ok(CollectionIndexRef::new(CollectionId::new(1)?, IndexId::new(2)?))
1145    }
1146
1147    fn cursor(ordinal: u64) -> SourceCursorV1 {
1148        SourceCursorV1 {
1149            next_ordinal: SourceOrdinal::new(ordinal),
1150            resume_token: ordinal.to_be_bytes().to_vec(),
1151        }
1152    }
1153
1154    fn cut(ordinal: u64) -> Result<SourceCutV1, Box<dyn Error>> {
1155        Ok(SourceCutV1 {
1156            format_version: REPLAYABLE_SOURCE_FORMAT_VERSION,
1157            source_incarnation: SourceIncarnation::new(1)?,
1158            partitions: vec![SourcePartitionCheckpointV1 {
1159                partition_id: SourcePartitionId::new(0),
1160                partition_incarnation: SourcePartitionIncarnation::new(1)?,
1161                cursor: cursor(ordinal),
1162            }],
1163        })
1164    }
1165
1166    fn batch() -> Result<SourceBatchV1, Box<dyn Error>> {
1167        Ok(SourceBatchV1 {
1168            format_version: REPLAYABLE_SOURCE_FORMAT_VERSION,
1169            batch_id: SourceBatchId::new(1)?,
1170            expected_cut: cut(0)?,
1171            replacement_cut: cut(1)?,
1172            records: vec![RawSourceRecordV1 {
1173                source_incarnation: SourceIncarnation::new(1)?,
1174                partition_id: SourcePartitionId::new(0),
1175                partition_incarnation: SourcePartitionIncarnation::new(1)?,
1176                position: cursor(0),
1177                next_position: cursor(1),
1178                operation_id: SourceOperationId::new(1)?,
1179                transaction: SourceTransactionMembershipV1::Standalone,
1180                key: b"doc-1".to_vec(),
1181                payload: br#"{"value":1}"#.to_vec(),
1182            }],
1183        })
1184    }
1185
1186    #[test]
1187    fn current_batch_round_trips_exactly() -> Result<(), Box<dyn Error>> {
1188        let expected = batch()?;
1189        let bytes = encode_source_batch_v1(&expected)?;
1190        assert_eq!(decode_source_batch_v1(&bytes)?, expected);
1191        Ok(())
1192    }
1193
1194    #[test]
1195    fn full_width_source_identities_use_canonical_decimal_strings() -> Result<(), Box<dyn Error>> {
1196        let operation_id = SourceOperationId::new(u128::MAX)?;
1197        assert_eq!(serde_json::to_string(&operation_id)?, format!("\"{}\"", u128::MAX));
1198        assert_eq!(
1199            serde_json::from_str::<SourceOperationId>(&format!("\"{}\"", u128::MAX))?,
1200            operation_id
1201        );
1202
1203        for refused in ["0", "01", "+1", " 1", "1 ", "340282366920938463463374607431768211456"] {
1204            assert!(serde_json::from_str::<SourceBatchId>(&format!("\"{refused}\"")).is_err());
1205        }
1206        assert!(serde_json::from_str::<SourceTransactionId>("1").is_err());
1207
1208        let mut expected = batch()?;
1209        expected.batch_id = SourceBatchId::new(u128::from(u64::MAX) + 1)?;
1210        expected.records[0].operation_id = SourceOperationId::new(u128::MAX)?;
1211        let bytes = encode_source_batch_v1(&expected)?;
1212        assert_eq!(decode_source_batch_v1(&bytes)?, expected);
1213        Ok(())
1214    }
1215
1216    #[test]
1217    fn newer_unknown_and_noncanonical_inputs_refuse() -> Result<(), Box<dyn Error>> {
1218        let bytes = encode_source_batch_v1(&batch()?)?;
1219        let mut value: serde_json::Value = serde_json::from_slice(&bytes)?;
1220        value["format_version"] = serde_json::Value::from(2);
1221        let newer = serde_json::to_vec(&value)?;
1222        assert_eq!(
1223            decode_source_batch_v1(&newer),
1224            Err(SourceContractError::UnsupportedFormat(2))
1225        );
1226
1227        let mut older: serde_json::Value = serde_json::from_slice(&bytes)?;
1228        older["format_version"] = serde_json::Value::from(0);
1229        assert_eq!(
1230            decode_source_batch_v1(&serde_json::to_vec(&older)?),
1231            Err(SourceContractError::UnsupportedFormat(0))
1232        );
1233
1234        let mut missing: serde_json::Value = serde_json::from_slice(&bytes)?;
1235        missing
1236            .as_object_mut()
1237            .ok_or("source batch must encode as an object")?
1238            .remove("format_version");
1239        assert_eq!(
1240            decode_source_batch_v1(&serde_json::to_vec(&missing)?),
1241            Err(SourceContractError::MissingFormatVersion)
1242        );
1243
1244        let mut value: serde_json::Value = serde_json::from_slice(&bytes)?;
1245        value["unknown"] = serde_json::Value::Bool(true);
1246        let unknown = serde_json::to_vec(&value)?;
1247        assert!(matches!(
1248            decode_source_batch_v1(&unknown),
1249            Err(SourceContractError::Serde(_))
1250        ));
1251
1252        let spaced = [b" ".as_slice(), bytes.as_slice()].concat();
1253        assert_eq!(decode_source_batch_v1(&spaced), Err(SourceContractError::NonCanonical));
1254
1255        let text = String::from_utf8(bytes)?;
1256        let duplicate = text.replacen("\"batch_id\":\"1\"", "\"batch_id\":\"1\",\"batch_id\":\"1\"", 1);
1257        if duplicate == text {
1258            return Err("batch id was not present in canonical bytes".into());
1259        }
1260        assert!(matches!(
1261            decode_source_batch_v1(duplicate.as_bytes()),
1262            Err(SourceContractError::Serde(_))
1263        ));
1264        Ok(())
1265    }
1266
1267    #[test]
1268    fn incomplete_transaction_and_duplicate_operation_refuse() -> Result<(), Box<dyn Error>> {
1269        let mut incomplete = batch()?;
1270        incomplete.records[0].transaction = SourceTransactionMembershipV1::Transaction {
1271            transaction_id: SourceTransactionId::new(1)?,
1272            member_index: 0,
1273            member_count: NonZeroU32::new(2).ok_or("nonzero member count")?,
1274        };
1275        assert!(matches!(incomplete.validate(), Err(SourceContractError::Violation(_))));
1276
1277        let mut duplicate = batch()?;
1278        let mut second = duplicate.records[0].clone();
1279        second.position = cursor(1);
1280        second.next_position = cursor(2);
1281        duplicate.replacement_cut = cut(2)?;
1282        duplicate.records.push(second);
1283        assert_eq!(
1284            duplicate.validate(),
1285            Err(SourceContractError::Violation(
1286                "source operation identity was duplicated"
1287            ))
1288        );
1289        Ok(())
1290    }
1291
1292    #[test]
1293    fn replay_authority_mode_matrix_is_explicit() -> Result<(), Box<dyn Error>> {
1294        let binding = SourceBindingV1 {
1295            format_version: REPLAYABLE_SOURCE_FORMAT_VERSION,
1296            collection_index_ref: scope()?,
1297            source_incarnation: SourceIncarnation::new(1)?,
1298            source_identity_digest: [1; 32],
1299            provider: SourceProviderKindV1::Kafka,
1300            decoder: SourceDecoderKindV1::DebeziumPostgresJson,
1301            partitions: vec![SourcePartitionBindingV1 {
1302                partition_id: SourcePartitionId::new(0),
1303                partition_incarnation: SourcePartitionIncarnation::new(1)?,
1304            }],
1305            guarantees: SourceReplayGuaranteesV1 {
1306                durable_replay: true,
1307                stable_partition_identity: true,
1308                stable_operation_identity: true,
1309                ordered_positions: true,
1310                bounded_reads: true,
1311                explicit_retention: true,
1312                explicit_rewind: true,
1313                minimum_replay_window_millis: 86_400_000,
1314            },
1315        };
1316        binding.validate_cut(&cut(0)?)?;
1317        let authority = IngestAuthorityV1::replayable_source(binding);
1318        assert_eq!(authority.validate_for_mode(IndexMode::Mutable), Ok(()));
1319        assert_eq!(authority.validate_for_mode(IndexMode::AppendLog), Ok(()));
1320        assert_eq!(
1321            authority.validate_for_mode(IndexMode::State),
1322            Err(SourceContractError::UnsupportedMode(IndexMode::State))
1323        );
1324        assert_eq!(
1325            IngestAuthorityV1::push_with_wal().validate_for_mode(IndexMode::State),
1326            Ok(())
1327        );
1328        let encoded = encode_ingest_authority_v1(&authority)?;
1329        assert_eq!(decode_ingest_authority_v1(&encoded)?, authority);
1330        Ok(())
1331    }
1332
1333    #[test]
1334    fn same_ordinal_token_drift_is_not_progress() -> Result<(), Box<dyn Error>> {
1335        let expected = cut(1)?;
1336        let mut replacement = cut(1)?;
1337        replacement.partitions[0].cursor.resume_token = b"different".to_vec();
1338        assert!(matches!(
1339            expected.advances_to(&replacement),
1340            Err(SourceContractError::Violation(_))
1341        ));
1342        Ok(())
1343    }
1344
1345    #[test]
1346    fn fetch_response_requires_exact_watermark_and_retention_partition_sets() -> Result<(), Box<dyn Error>> {
1347        let mut response = SourceFetchResponseV1 {
1348            batch: batch()?,
1349            high_watermarks: vec![(
1350                SourcePartitionId::new(0),
1351                SourceHighWatermarkV1::Known { cursor: cursor(2) },
1352            )],
1353            retention_floors: vec![(SourcePartitionId::new(0), cursor(0))],
1354        };
1355        response.validate()?;
1356        response.retention_floors[0].1 = cursor(1);
1357        assert!(matches!(response.validate(), Err(SourceContractError::Violation(_))));
1358        Ok(())
1359    }
1360
1361    #[test]
1362    fn atomic_cut_requires_one_scoped_successor_per_shard() -> Result<(), Box<dyn Error>> {
1363        let scope = scope()?;
1364        let commit = AtomicSourcePublicationV1 {
1365            format_version: REPLAYABLE_SOURCE_FORMAT_VERSION,
1366            collection_index_ref: scope,
1367            source_incarnation: SourceIncarnation::new(1)?,
1368            batch_id: SourceBatchId::new(1)?,
1369            expected_cut: cut(0)?,
1370            replacement_cut: cut(1)?,
1371            operation_count: 1,
1372            operation_set_digest: source_batch_operation_set_digest_v1(&batch()?)?,
1373            committed_transactions: Vec::new(),
1374            shard_publications: vec![SourceShardPublicationV1 {
1375                shard_key: ShardKey::new(scope, ShardId::new(1)),
1376                fence_token: FenceToken::new(RecoveryEra::new(1)?, Epoch::new(1))?,
1377                expected_generation: ManifestGeneration::new(4),
1378                replacement_generation: ManifestGeneration::new(5),
1379                replacement_root_digest: [3; 32],
1380            }],
1381        };
1382        let bytes = encode_atomic_source_publication_v1(&commit)?;
1383        assert_eq!(decode_atomic_source_publication_v1(&bytes)?, commit);
1384        commit.validate_against_batch(&batch()?)?;
1385        Ok(())
1386    }
1387
1388    #[test]
1389    fn oversized_inputs_refuse_before_json_decode() {
1390        let oversized_configuration = vec![b' '; MAX_SOURCE_CONFIGURATION_BYTES + 1];
1391        assert_eq!(
1392            decode_source_binding_v1(&oversized_configuration),
1393            Err(SourceContractError::Budget {
1394                field: "source configuration bytes",
1395                observed: MAX_SOURCE_CONFIGURATION_BYTES + 1,
1396                maximum: MAX_SOURCE_CONFIGURATION_BYTES,
1397            })
1398        );
1399
1400        let oversized_batch = vec![b' '; MAX_SOURCE_BATCH_BYTES + 1];
1401        assert_eq!(
1402            decode_source_batch_v1(&oversized_batch),
1403            Err(SourceContractError::Budget {
1404                field: "source batch bytes",
1405                observed: MAX_SOURCE_BATCH_BYTES + 1,
1406                maximum: MAX_SOURCE_BATCH_BYTES,
1407            })
1408        );
1409
1410        let oversized_command = vec![b' '; MAX_ATOMIC_SOURCE_COMMAND_BYTES + 1];
1411        assert_eq!(
1412            decode_atomic_source_publication_v1(&oversized_command),
1413            Err(SourceContractError::Budget {
1414                field: "atomic source command bytes",
1415                observed: MAX_ATOMIC_SOURCE_COMMAND_BYTES + 1,
1416                maximum: MAX_ATOMIC_SOURCE_COMMAND_BYTES,
1417            })
1418        );
1419    }
1420}