Skip to main content

fsqlite_core/
raptorq_integration.rs

1//! §3.3 Asupersync RaptorQ Pipeline Integration (bd-1hi.5).
2//!
3//! This module provides the FrankenSQLite-side wrapper types for the
4//! asupersync RaptorQ pipeline.  Production code uses abstract traits
5//! (`PageSymbolSink`, `PageSymbolSource`, `SymbolCodec`) so that the
6//! actual asupersync dependency remains dev-only.
7//!
8//! # Cx Cancellation
9//!
10//! All long-running encode/decode loops call `cx.checkpoint()` every
11//! `checkpoint_interval` symbols (§4.12.1).  If the context is cancelled
12//! the operation returns `FrankenError::Abort`.
13
14use fsqlite_types::sync_primitives::Instant;
15use std::fmt;
16use std::sync::atomic::{AtomicU64, Ordering};
17
18use fsqlite_error::{FrankenError, Result};
19use fsqlite_types::{ObjectId, cx::Cx};
20use tracing::{debug, error, info, warn};
21use xxhash_rust::xxh3::xxh3_64;
22
23use crate::decode_proofs::EcsDecodeProof;
24
25const BEAD_ID: &str = "bd-1hi.5";
26
27// ---------------------------------------------------------------------------
28// RaptorQ Metrics (bd-3bw.1)
29// ---------------------------------------------------------------------------
30
31/// Global atomic counters for RaptorQ encode/decode operations.
32///
33/// These metrics track cumulative byte and symbol counts for observability
34/// and capacity planning.  All counters are monotonically increasing and
35/// use `Relaxed` ordering (sufficient for diagnostic counters).
36pub struct RaptorQMetrics {
37    /// Total bytes encoded via `encode_pages()`.
38    pub encoded_bytes_total: AtomicU64,
39    /// Total repair symbols generated across all encode calls.
40    pub repair_symbols_generated_total: AtomicU64,
41    /// Total bytes successfully decoded via `decode_pages()`.
42    pub decoded_bytes_total: AtomicU64,
43    /// Total encode operations.
44    pub encode_ops: AtomicU64,
45    /// Total decode operations (success + failure).
46    pub decode_ops: AtomicU64,
47    /// Total decode failures.
48    pub decode_failures: AtomicU64,
49}
50
51impl RaptorQMetrics {
52    /// Create a new zeroed metrics instance.  `const` so it can back a
53    /// `static`.
54    #[must_use]
55    pub const fn new() -> Self {
56        Self {
57            encoded_bytes_total: AtomicU64::new(0),
58            repair_symbols_generated_total: AtomicU64::new(0),
59            decoded_bytes_total: AtomicU64::new(0),
60            encode_ops: AtomicU64::new(0),
61            decode_ops: AtomicU64::new(0),
62            decode_failures: AtomicU64::new(0),
63        }
64    }
65
66    /// Record a successful encode operation.
67    pub fn record_encode(&self, encoded_bytes: u64, repair_symbols: u64) {
68        self.encoded_bytes_total
69            .fetch_add(encoded_bytes, Ordering::Relaxed);
70        self.repair_symbols_generated_total
71            .fetch_add(repair_symbols, Ordering::Relaxed);
72        self.encode_ops.fetch_add(1, Ordering::Relaxed);
73    }
74
75    /// Record a successful decode operation.
76    pub fn record_decode_success(&self, decoded_bytes: u64) {
77        self.decoded_bytes_total
78            .fetch_add(decoded_bytes, Ordering::Relaxed);
79        self.decode_ops.fetch_add(1, Ordering::Relaxed);
80    }
81
82    /// Record a failed decode operation.
83    pub fn record_decode_failure(&self) {
84        self.decode_ops.fetch_add(1, Ordering::Relaxed);
85        self.decode_failures.fetch_add(1, Ordering::Relaxed);
86    }
87
88    /// Take a point-in-time snapshot of all counters.
89    #[must_use]
90    pub fn snapshot(&self) -> RaptorQMetricsSnapshot {
91        RaptorQMetricsSnapshot {
92            encoded_bytes_total: self.encoded_bytes_total.load(Ordering::Relaxed),
93            repair_symbols_generated_total: self
94                .repair_symbols_generated_total
95                .load(Ordering::Relaxed),
96            decoded_bytes_total: self.decoded_bytes_total.load(Ordering::Relaxed),
97            encode_ops: self.encode_ops.load(Ordering::Relaxed),
98            decode_ops: self.decode_ops.load(Ordering::Relaxed),
99            decode_failures: self.decode_failures.load(Ordering::Relaxed),
100        }
101    }
102
103    /// Reset all counters to zero (useful for tests).
104    pub fn reset(&self) {
105        self.encoded_bytes_total.store(0, Ordering::Relaxed);
106        self.repair_symbols_generated_total
107            .store(0, Ordering::Relaxed);
108        self.decoded_bytes_total.store(0, Ordering::Relaxed);
109        self.encode_ops.store(0, Ordering::Relaxed);
110        self.decode_ops.store(0, Ordering::Relaxed);
111        self.decode_failures.store(0, Ordering::Relaxed);
112    }
113}
114
115impl Default for RaptorQMetrics {
116    fn default() -> Self {
117        Self::new()
118    }
119}
120
121/// Global RaptorQ metrics singleton.
122pub static GLOBAL_RAPTORQ_METRICS: RaptorQMetrics = RaptorQMetrics::new();
123
124/// Point-in-time snapshot of [`RaptorQMetrics`].
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126pub struct RaptorQMetricsSnapshot {
127    pub encoded_bytes_total: u64,
128    pub repair_symbols_generated_total: u64,
129    pub decoded_bytes_total: u64,
130    pub encode_ops: u64,
131    pub decode_ops: u64,
132    pub decode_failures: u64,
133}
134
135impl fmt::Display for RaptorQMetricsSnapshot {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        write!(
138            f,
139            "raptorq: encoded={} bytes ({} ops, {} repair syms), decoded={} bytes ({} ops, {} failures)",
140            self.encoded_bytes_total,
141            self.encode_ops,
142            self.repair_symbols_generated_total,
143            self.decoded_bytes_total,
144            self.decode_ops,
145            self.decode_failures,
146        )
147    }
148}
149
150/// Convert a `Duration` to microseconds, saturating at `u64::MAX`.
151fn duration_us_saturating(d: std::time::Duration) -> u64 {
152    u64::try_from(d.as_micros()).unwrap_or(u64::MAX)
153}
154
155// ---------------------------------------------------------------------------
156// Pipeline Configuration (§3.3)
157// ---------------------------------------------------------------------------
158
159/// Minimum allowed symbol size (bytes).
160pub const MIN_PIPELINE_SYMBOL_SIZE: u32 = 512;
161
162/// Maximum allowed symbol size (bytes).
163pub const MAX_PIPELINE_SYMBOL_SIZE: u32 = 65_536;
164
165/// Default Cx checkpoint interval (symbols between cancellation checks).
166pub const DEFAULT_CHECKPOINT_INTERVAL: u32 = 64;
167
168/// Policy surface for decode-proof emission hooks.
169///
170/// This keeps proof generation optional in production while allowing
171/// durability paths and tests to request deterministic proof artifacts.
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173pub struct DecodeProofEmissionPolicy {
174    /// Emit proof records for decode failures.
175    pub emit_on_decode_failure: bool,
176    /// Emit proof records for successful decodes that required repair symbols.
177    pub emit_on_repair_success: bool,
178}
179
180impl DecodeProofEmissionPolicy {
181    /// Default production posture: proof emission disabled.
182    #[must_use]
183    pub const fn disabled() -> Self {
184        Self {
185            emit_on_decode_failure: false,
186            emit_on_repair_success: false,
187        }
188    }
189
190    /// Durability-focused posture for replication/WAL-style decode paths.
191    #[must_use]
192    pub const fn durability_critical() -> Self {
193        Self {
194            emit_on_decode_failure: true,
195            emit_on_repair_success: true,
196        }
197    }
198}
199
200impl Default for DecodeProofEmissionPolicy {
201    fn default() -> Self {
202        Self::disabled()
203    }
204}
205
206/// FrankenSQLite-side RaptorQ pipeline configuration (§3.3).
207///
208/// Mirrors the needed subset of asupersync's `RaptorQConfig` so that
209/// production code does not depend on asupersync directly.
210#[derive(Debug, Clone, PartialEq)]
211pub struct PipelineConfig {
212    /// Symbol size T in bytes.  Must be a power of two in
213    /// `[MIN_PIPELINE_SYMBOL_SIZE, MAX_PIPELINE_SYMBOL_SIZE]`.
214    pub symbol_size: u32,
215    /// Maximum source block size (max K per source block) in bytes.
216    pub max_block_size: u32,
217    /// Repair overhead factor.  E.g. `1.25` means 25 % extra repair symbols.
218    pub repair_overhead: f64,
219    /// Symbols between `Cx::checkpoint()` calls (§4.12.1).
220    pub checkpoint_interval: u32,
221    /// Decode-proof emission policy hooks (§3.5.8 / bd-faz4).
222    pub decode_proof_policy: DecodeProofEmissionPolicy,
223}
224
225impl PipelineConfig {
226    /// Create a configuration for page-sized symbols (T = page_size).
227    #[must_use]
228    pub fn for_page_size(page_size: u32) -> Self {
229        Self {
230            symbol_size: page_size,
231            max_block_size: 64 * 1024,
232            repair_overhead: 1.25,
233            checkpoint_interval: DEFAULT_CHECKPOINT_INTERVAL,
234            decode_proof_policy: DecodeProofEmissionPolicy::default(),
235        }
236    }
237
238    /// Validate this configuration.
239    ///
240    /// Rejects:
241    /// - `symbol_size == 0`
242    /// - `symbol_size` not a power of two
243    /// - `symbol_size` outside `[MIN, MAX]`
244    /// - `max_block_size == 0`
245    /// - `repair_overhead < 1.0`
246    /// - `checkpoint_interval == 0`
247    pub fn validate(&self) -> Result<()> {
248        if self.symbol_size == 0 {
249            return Err(FrankenError::OutOfRange {
250                what: "pipeline symbol_size".to_owned(),
251                value: "0".to_owned(),
252            });
253        }
254        if !self.symbol_size.is_power_of_two() {
255            return Err(FrankenError::OutOfRange {
256                what: "pipeline symbol_size (must be power of 2)".to_owned(),
257                value: self.symbol_size.to_string(),
258            });
259        }
260        if self.symbol_size < MIN_PIPELINE_SYMBOL_SIZE
261            || self.symbol_size > MAX_PIPELINE_SYMBOL_SIZE
262        {
263            return Err(FrankenError::OutOfRange {
264                what: format!(
265                    "pipeline symbol_size (must be in [{MIN_PIPELINE_SYMBOL_SIZE}, {MAX_PIPELINE_SYMBOL_SIZE}])"
266                ),
267                value: self.symbol_size.to_string(),
268            });
269        }
270        if self.max_block_size == 0 {
271            return Err(FrankenError::OutOfRange {
272                what: "pipeline max_block_size".to_owned(),
273                value: "0".to_owned(),
274            });
275        }
276        if self.repair_overhead < 1.0 {
277            return Err(FrankenError::OutOfRange {
278                what: "pipeline repair_overhead (must be >= 1.0)".to_owned(),
279                value: self.repair_overhead.to_string(),
280            });
281        }
282        if self.checkpoint_interval == 0 {
283            return Err(FrankenError::OutOfRange {
284                what: "pipeline checkpoint_interval".to_owned(),
285                value: "0".to_owned(),
286            });
287        }
288        Ok(())
289    }
290}
291
292impl Default for PipelineConfig {
293    fn default() -> Self {
294        Self::for_page_size(4096)
295    }
296}
297
298// ---------------------------------------------------------------------------
299// Page Symbol Sink / Source Traits (§3.3)
300// ---------------------------------------------------------------------------
301
302/// Writes encoded page symbols to WAL/ECS storage.
303pub trait PageSymbolSink {
304    /// Write a single encoded symbol.
305    fn write_symbol(&mut self, esi: u32, data: &[u8]) -> Result<()>;
306
307    /// Flush all buffered symbols to durable storage.
308    fn flush(&mut self) -> Result<()>;
309
310    /// Number of symbols written so far.
311    fn written_count(&self) -> u32;
312}
313
314/// Reads symbols from WAL/ECS storage for decoding.
315pub trait PageSymbolSource {
316    /// Read a symbol by its ESI.  Returns `None` if unavailable (erased).
317    fn read_symbol(&mut self, esi: u32) -> Result<Option<Vec<u8>>>;
318
319    /// All available ESIs in this source.
320    fn available_esis(&self) -> Vec<u32>;
321
322    /// Number of available symbols.
323    fn available_count(&self) -> u32;
324}
325
326// ---------------------------------------------------------------------------
327// Symbol Codec Trait (§3.3)
328// ---------------------------------------------------------------------------
329
330/// Abstraction over the actual RaptorQ encode/decode engine.
331///
332/// In production, this wraps asupersync's `RaptorQSenderBuilder` /
333/// `RaptorQReceiverBuilder`.  In tests, it may be a mock.
334pub trait SymbolCodec: Send + Sync {
335    /// Encode source data into source + repair symbols.
336    fn encode(
337        &self,
338        cx: &Cx,
339        source_data: &[u8],
340        symbol_size: u32,
341        repair_overhead: f64,
342    ) -> Result<CodecEncodeResult>;
343
344    /// Decode from received symbols.
345    fn decode(
346        &self,
347        cx: &Cx,
348        symbols: &[(u32, Vec<u8>)],
349        k_source: u32,
350        symbol_size: u32,
351    ) -> Result<CodecDecodeResult>;
352}
353
354/// Raw encode result from the codec.
355#[derive(Debug, Clone)]
356pub struct CodecEncodeResult {
357    /// Source symbols: `(esi, data)`.
358    pub source_symbols: Vec<(u32, Vec<u8>)>,
359    /// Repair symbols: `(esi, data)`.
360    pub repair_symbols: Vec<(u32, Vec<u8>)>,
361    /// Number of source symbols K.
362    pub k_source: u32,
363}
364
365/// Raw decode result from the codec.
366#[derive(Debug, Clone)]
367pub enum CodecDecodeResult {
368    /// Decode succeeded.
369    Success {
370        /// Recovered source data.
371        data: Vec<u8>,
372        /// Number of symbols consumed.
373        symbols_used: u32,
374        /// Symbols resolved by peeling.
375        peeled_count: u32,
376        /// Symbols resolved by Gaussian elimination (inactive subsystem).
377        inactivated_count: u32,
378    },
379    /// Decode failed.
380    Failure {
381        /// Reason for failure.
382        reason: DecodeFailureReason,
383        /// Number of symbols that were received.
384        symbols_received: u32,
385        /// Source symbols required.
386        k_required: u32,
387    },
388}
389
390// ---------------------------------------------------------------------------
391// Outcome Types (§3.3)
392// ---------------------------------------------------------------------------
393
394/// Result of a pipeline encode operation.
395#[derive(Debug, Clone)]
396pub struct EncodeOutcome {
397    /// Number of source symbols produced.
398    pub source_count: u32,
399    /// Number of repair symbols produced.
400    pub repair_count: u32,
401    /// Symbol size in bytes.
402    pub symbol_size: u32,
403}
404
405/// Result of a pipeline decode operation.
406#[derive(Debug, Clone)]
407pub enum DecodeOutcome {
408    /// Successful decode with recovered pages.
409    Success(DecodeSuccess),
410    /// Failed decode with diagnostic information.
411    Failure(DecodeFailure),
412}
413
414/// Successful decode metadata.
415#[derive(Debug, Clone)]
416pub struct DecodeSuccess {
417    /// Recovered page data, concatenated.
418    pub data: Vec<u8>,
419    /// Number of symbols used for decoding.
420    pub symbols_used: u32,
421    /// Symbols resolved during the peeling phase.
422    pub peeled_count: u32,
423    /// Symbols resolved during the Gaussian elimination phase.
424    pub inactivated_count: u32,
425    /// Optional decode proof emitted under policy control.
426    pub decode_proof: Option<EcsDecodeProof>,
427}
428
429/// Failed decode metadata.
430#[derive(Debug, Clone)]
431pub struct DecodeFailure {
432    /// Why the decode failed.
433    pub reason: DecodeFailureReason,
434    /// Number of symbols that were available.
435    pub symbols_received: u32,
436    /// Source symbols required (K).
437    pub k_required: u32,
438    /// Optional decode proof emitted under policy control.
439    pub decode_proof: Option<EcsDecodeProof>,
440}
441
442/// Reasons a decode can fail.
443#[derive(Debug, Clone, Copy, PartialEq, Eq)]
444pub enum DecodeFailureReason {
445    /// Fewer symbols than K available.
446    InsufficientSymbols,
447    /// The decoding matrix is singular (rank deficient).
448    SingularMatrix,
449    /// Symbol sizes do not match the expected T.
450    SymbolSizeMismatch,
451    /// Cancelled via `Cx::checkpoint()`.
452    Cancelled,
453}
454
455// ---------------------------------------------------------------------------
456// Pipeline Encoder (§3.3)
457// ---------------------------------------------------------------------------
458
459/// RaptorQ page encoder that wraps a [`SymbolCodec`] and writes through
460/// a [`PageSymbolSink`] with Cx cancellation checkpoints.
461pub struct RaptorQPageEncoder<C: SymbolCodec> {
462    config: PipelineConfig,
463    codec: C,
464}
465
466impl<C: SymbolCodec> RaptorQPageEncoder<C> {
467    /// Create a new encoder.  Validates the config eagerly.
468    pub fn new(config: PipelineConfig, codec: C) -> Result<Self> {
469        config.validate()?;
470        info!(
471            bead_id = BEAD_ID,
472            symbol_size = config.symbol_size,
473            max_block_size = config.max_block_size,
474            repair_overhead = config.repair_overhead,
475            "RaptorQ page encoder created"
476        );
477        Ok(Self { config, codec })
478    }
479
480    /// Encode page data and write symbols through the sink.
481    ///
482    /// `cx.checkpoint()` is called every `checkpoint_interval` symbols.
483    /// Emits a `raptorq_encode` tracing span (bd-3bw.1) and updates
484    /// [`GLOBAL_RAPTORQ_METRICS`].
485    #[allow(clippy::cast_possible_truncation)]
486    pub fn encode_pages(
487        &self,
488        cx: &Cx,
489        page_data: &[u8],
490        sink: &mut dyn PageSymbolSink,
491    ) -> Result<EncodeOutcome> {
492        cx.checkpoint().map_err(|_| FrankenError::Abort)?;
493
494        let symbol_size = self.config.symbol_size;
495        let t0 = Instant::now();
496        debug!(
497            bead_id = BEAD_ID,
498            data_len = page_data.len(),
499            symbol_size,
500            "starting page encode"
501        );
502
503        let result = self
504            .codec
505            .encode(cx, page_data, symbol_size, self.config.repair_overhead)?;
506
507        // Write source symbols with checkpoints.
508        let interval = self.config.checkpoint_interval as usize;
509        for (idx, (esi, data)) in result.source_symbols.iter().enumerate() {
510            if idx > 0 && idx % interval == 0 {
511                cx.checkpoint().map_err(|_| FrankenError::Abort)?;
512            }
513            sink.write_symbol(*esi, data)?;
514        }
515
516        // Write repair symbols with checkpoints.
517        for (idx, (esi, data)) in result.repair_symbols.iter().enumerate() {
518            if idx > 0 && idx % interval == 0 {
519                cx.checkpoint().map_err(|_| FrankenError::Abort)?;
520            }
521            sink.write_symbol(*esi, data)?;
522        }
523
524        sink.flush()?;
525
526        let outcome = EncodeOutcome {
527            source_count: result.k_source,
528            repair_count: result.repair_symbols.len() as u32,
529            symbol_size,
530        };
531
532        let encode_time_us = duration_us_saturating(t0.elapsed());
533
534        // bd-3bw.1: structured tracing span with required fields.
535        let span = tracing::span!(
536            tracing::Level::DEBUG,
537            "raptorq_encode",
538            source_symbols = outcome.source_count,
539            repair_symbols = outcome.repair_count,
540            encode_time_us,
541            encoded_bytes = page_data.len(),
542            symbol_size = outcome.symbol_size,
543        );
544        let _guard = span.enter();
545
546        info!(
547            bead_id = BEAD_ID,
548            source_count = outcome.source_count,
549            repair_count = outcome.repair_count,
550            symbol_size = outcome.symbol_size,
551            encode_time_us,
552            "page encode complete"
553        );
554
555        // bd-3bw.1: update global metric counters.
556        GLOBAL_RAPTORQ_METRICS
557            .record_encode(page_data.len() as u64, u64::from(outcome.repair_count));
558
559        Ok(outcome)
560    }
561
562    /// Reference to the pipeline config.
563    #[must_use]
564    pub const fn config(&self) -> &PipelineConfig {
565        &self.config
566    }
567}
568
569// ---------------------------------------------------------------------------
570// Pipeline Decoder (§3.3)
571// ---------------------------------------------------------------------------
572
573/// RaptorQ page decoder that wraps a [`SymbolCodec`] and reads from
574/// a [`PageSymbolSource`] with Cx cancellation checkpoints.
575pub struct RaptorQPageDecoder<C: SymbolCodec> {
576    config: PipelineConfig,
577    codec: C,
578}
579
580impl<C: SymbolCodec> RaptorQPageDecoder<C> {
581    /// Create a new decoder.  Validates the config eagerly.
582    pub fn new(config: PipelineConfig, codec: C) -> Result<Self> {
583        config.validate()?;
584        info!(
585            bead_id = BEAD_ID,
586            symbol_size = config.symbol_size,
587            "RaptorQ page decoder created"
588        );
589        Ok(Self { config, codec })
590    }
591
592    /// Decode pages from the source.
593    ///
594    /// Reads available symbols, delegates to the codec, and returns the
595    /// outcome.  Cx checkpoint is called at read boundaries.  Emits a
596    /// `raptorq_decode` tracing span (bd-3bw.1) and updates
597    /// [`GLOBAL_RAPTORQ_METRICS`].
598    #[allow(clippy::cast_possible_truncation, clippy::too_many_lines)]
599    pub fn decode_pages(
600        &self,
601        cx: &Cx,
602        source: &mut dyn PageSymbolSource,
603        k_source: u32,
604    ) -> Result<DecodeOutcome> {
605        cx.checkpoint().map_err(|_| FrankenError::Abort)?;
606        let t0 = Instant::now();
607
608        let available = source.available_count();
609        debug!(
610            bead_id = BEAD_ID,
611            k_source, available, "starting page decode"
612        );
613
614        if available < k_source {
615            warn!(
616                bead_id = BEAD_ID,
617                k_source, available, "fewer symbols than K_source — decode likely to fail"
618            );
619        }
620
621        // Collect symbols from source with checkpoints.
622        let esis = source.available_esis();
623        let interval = self.config.checkpoint_interval as usize;
624        let mut symbols = Vec::with_capacity(esis.len());
625        for (idx, esi) in esis.iter().enumerate() {
626            if idx > 0 && idx % interval == 0 {
627                cx.checkpoint().map_err(|_| FrankenError::Abort)?;
628            }
629            if let Some(data) = source.read_symbol(*esi)? {
630                symbols.push((*esi, data));
631            }
632        }
633
634        // Delegate to codec.
635        let codec_result = self
636            .codec
637            .decode(cx, &symbols, k_source, self.config.symbol_size)?;
638        let all_esis = canonical_esis(&symbols);
639        let proof_object_id =
640            derive_decode_proof_object_id(k_source, self.config.symbol_size, &all_esis);
641        let proof_seed = xxh3_64(proof_object_id.as_bytes());
642
643        match codec_result {
644            CodecDecodeResult::Success {
645                data,
646                symbols_used,
647                peeled_count,
648                inactivated_count,
649            } => {
650                info!(
651                    bead_id = BEAD_ID,
652                    k_source,
653                    symbols_used,
654                    peeled_count,
655                    inactivated_count,
656                    "page decode succeeded"
657                );
658                let decode_proof = if self.config.decode_proof_policy.emit_on_repair_success
659                    && contains_repair_esi(&all_esis, k_source)
660                {
661                    let proof = EcsDecodeProof::from_esis(
662                        proof_object_id,
663                        k_source,
664                        &all_esis,
665                        true,
666                        Some(symbols_used),
667                        deterministic_timing_ns(k_source, self.config.symbol_size, symbols_used),
668                        proof_seed,
669                    );
670                    debug!(
671                        bead_id = "bd-faz4",
672                        symbols_used, k_source, "emitted repair-success decode proof"
673                    );
674                    Some(proof)
675                } else {
676                    None
677                };
678                if symbols_used == k_source {
679                    warn!(
680                        bead_id = BEAD_ID,
681                        k_source,
682                        symbols_used,
683                        "fragile recovery: decoded with minimum symbol count"
684                    );
685                }
686                let decoded_len = data.len() as u64;
687                let decode_time_us = duration_us_saturating(t0.elapsed());
688
689                // bd-3bw.1: structured tracing span for successful decode.
690                let span = tracing::span!(
691                    tracing::Level::DEBUG,
692                    "raptorq_decode",
693                    k_source,
694                    symbols_used,
695                    decoded_bytes = decoded_len,
696                    decode_time_us,
697                    ok = true,
698                );
699                let _guard = span.enter();
700
701                GLOBAL_RAPTORQ_METRICS.record_decode_success(decoded_len);
702
703                Ok(DecodeOutcome::Success(DecodeSuccess {
704                    data,
705                    symbols_used,
706                    peeled_count,
707                    inactivated_count,
708                    decode_proof,
709                }))
710            }
711            CodecDecodeResult::Failure {
712                reason,
713                symbols_received,
714                k_required,
715            } => {
716                let decode_proof = if self.config.decode_proof_policy.emit_on_decode_failure {
717                    let intermediate_rank = Some(symbols_received.min(k_required));
718                    let proof = EcsDecodeProof::from_esis(
719                        proof_object_id,
720                        k_source,
721                        &all_esis,
722                        false,
723                        intermediate_rank,
724                        deterministic_timing_ns(
725                            k_source,
726                            self.config.symbol_size,
727                            symbols_received,
728                        ),
729                        proof_seed,
730                    );
731                    debug!(
732                        bead_id = "bd-faz4",
733                        symbols_received, k_required, "emitted decode-failure proof"
734                    );
735                    Some(proof)
736                } else {
737                    None
738                };
739                let decode_time_us = duration_us_saturating(t0.elapsed());
740
741                // bd-3bw.1: structured tracing span for failed decode.
742                let span = tracing::span!(
743                    tracing::Level::DEBUG,
744                    "raptorq_decode",
745                    k_source,
746                    symbols_received,
747                    k_required,
748                    decode_time_us,
749                    ok = false,
750                );
751                let _guard = span.enter();
752
753                error!(
754                    bead_id = BEAD_ID,
755                    k_source,
756                    symbols_received,
757                    k_required,
758                    reason = ?reason,
759                    "page decode failed"
760                );
761
762                GLOBAL_RAPTORQ_METRICS.record_decode_failure();
763
764                Ok(DecodeOutcome::Failure(DecodeFailure {
765                    reason,
766                    symbols_received,
767                    k_required,
768                    decode_proof,
769                }))
770            }
771        }
772    }
773
774    /// Reference to the pipeline config.
775    #[must_use]
776    pub const fn config(&self) -> &PipelineConfig {
777        &self.config
778    }
779}
780
781fn canonical_esis(symbols: &[(u32, Vec<u8>)]) -> Vec<u32> {
782    let mut esis: Vec<u32> = symbols.iter().map(|(esi, _)| *esi).collect();
783    esis.sort_unstable();
784    esis.dedup();
785    esis
786}
787
788fn contains_repair_esi(esis: &[u32], k_source: u32) -> bool {
789    esis.iter().any(|&esi| esi >= k_source)
790}
791
792fn derive_decode_proof_object_id(k_source: u32, symbol_size: u32, esis: &[u32]) -> ObjectId {
793    let mut material = Vec::with_capacity(40 + esis.len() * 4);
794    material.extend_from_slice(b"fsqlite:raptorq:decode-proof:v1");
795    material.extend_from_slice(&k_source.to_le_bytes());
796    material.extend_from_slice(&symbol_size.to_le_bytes());
797    for esi in esis {
798        material.extend_from_slice(&esi.to_le_bytes());
799    }
800    ObjectId::derive_from_canonical_bytes(&material)
801}
802
803fn deterministic_timing_ns(k_source: u32, symbol_size: u32, symbols_used: u32) -> u64 {
804    let mut material = [0_u8; 12];
805    material[..4].copy_from_slice(&k_source.to_le_bytes());
806    material[4..8].copy_from_slice(&symbol_size.to_le_bytes());
807    material[8..12].copy_from_slice(&symbols_used.to_le_bytes());
808    xxh3_64(&material)
809}
810
811// ===========================================================================
812// Tests
813// ===========================================================================
814
815#[cfg(test)]
816#[allow(
817    clippy::cast_possible_truncation,
818    clippy::cast_lossless,
819    clippy::cast_precision_loss,
820    clippy::cast_sign_loss
821)]
822mod tests {
823    use std::collections::{BTreeMap, VecDeque};
824    use std::pin::Pin;
825    use std::task::{Context, Poll};
826
827    use asupersync::error::ErrorKind as AsErrorKind;
828    use asupersync::raptorq::RaptorQReceiverBuilder;
829    use asupersync::raptorq::RaptorQSenderBuilder;
830    use asupersync::security::AuthenticationTag;
831    use asupersync::security::authenticated::AuthenticatedSymbol;
832    use asupersync::transport::error::{SinkError, StreamError};
833    use asupersync::transport::sink::SymbolSink;
834    use asupersync::transport::stream::SymbolStream;
835    use asupersync::types::{ObjectId as AsObjectId, ObjectParams, Symbol, SymbolId, SymbolKind};
836    use asupersync::{Cx as AsCx, RaptorQConfig};
837
838    use super::*;
839
840    // -----------------------------------------------------------------------
841    // Mock PageSymbolSink / PageSymbolSource
842    // -----------------------------------------------------------------------
843
844    struct VecPageSink {
845        symbols: BTreeMap<u32, Vec<u8>>,
846        flushed: bool,
847    }
848
849    impl VecPageSink {
850        fn new() -> Self {
851            Self {
852                symbols: BTreeMap::new(),
853                flushed: false,
854            }
855        }
856    }
857
858    impl PageSymbolSink for VecPageSink {
859        fn write_symbol(&mut self, esi: u32, data: &[u8]) -> Result<()> {
860            self.symbols.insert(esi, data.to_vec());
861            Ok(())
862        }
863
864        fn flush(&mut self) -> Result<()> {
865            self.flushed = true;
866            Ok(())
867        }
868
869        fn written_count(&self) -> u32 {
870            self.symbols.len() as u32
871        }
872    }
873
874    struct VecPageSource {
875        symbols: BTreeMap<u32, Vec<u8>>,
876    }
877
878    impl VecPageSource {
879        fn from_sink(sink: &VecPageSink) -> Self {
880            Self {
881                symbols: sink.symbols.clone(),
882            }
883        }
884
885        fn from_map(symbols: BTreeMap<u32, Vec<u8>>) -> Self {
886            Self { symbols }
887        }
888    }
889
890    impl PageSymbolSource for VecPageSource {
891        fn read_symbol(&mut self, esi: u32) -> Result<Option<Vec<u8>>> {
892            Ok(self.symbols.get(&esi).cloned())
893        }
894
895        fn available_esis(&self) -> Vec<u32> {
896            self.symbols.keys().copied().collect()
897        }
898
899        fn available_count(&self) -> u32 {
900            self.symbols.len() as u32
901        }
902    }
903
904    // -----------------------------------------------------------------------
905    // Asupersync-backed SymbolCodec implementation
906    // -----------------------------------------------------------------------
907
908    #[derive(Debug)]
909    struct VecTransportSink {
910        symbols: Vec<Symbol>,
911    }
912
913    impl VecTransportSink {
914        fn new() -> Self {
915            Self {
916                symbols: Vec::new(),
917            }
918        }
919    }
920
921    #[derive(Debug)]
922    struct VecTransportStream {
923        symbols: VecDeque<AuthenticatedSymbol>,
924    }
925
926    impl VecTransportStream {
927        fn new(symbols: Vec<Symbol>) -> Self {
928            let symbols = symbols
929                .into_iter()
930                .map(|symbol| AuthenticatedSymbol::from_parts(symbol, AuthenticationTag::zero()))
931                .collect();
932            Self { symbols }
933        }
934    }
935
936    impl SymbolStream for VecTransportStream {
937        fn poll_next(
938            mut self: Pin<&mut Self>,
939            _cx: &mut Context<'_>,
940        ) -> Poll<Option<std::result::Result<AuthenticatedSymbol, StreamError>>> {
941            match self.symbols.pop_front() {
942                Some(symbol) => Poll::Ready(Some(Ok(symbol))),
943                None => Poll::Ready(None),
944            }
945        }
946
947        fn size_hint(&self) -> (usize, Option<usize>) {
948            (self.symbols.len(), Some(self.symbols.len()))
949        }
950
951        fn is_exhausted(&self) -> bool {
952            self.symbols.is_empty()
953        }
954    }
955
956    const TEST_OBJECT_ID: u64 = 0xBD_1A15;
957    const TEST_MAX_BLOCK_SIZE: usize = 64 * 1024;
958    const PACKED_KIND_REPAIR_BIT: u32 = 1_u32 << 31;
959    const PACKED_SBN_SHIFT: u32 = 23;
960    const PACKED_SBN_MASK: u32 = 0xFF;
961    const PACKED_ESI_MASK: u32 = 0x7F_FFFF;
962
963    fn pack_symbol_key(kind: SymbolKind, sbn: u8, esi: u32) -> Result<u32> {
964        if esi > PACKED_ESI_MASK {
965            return Err(FrankenError::OutOfRange {
966                what: "packed symbol esi (must fit 23 bits)".to_owned(),
967                value: esi.to_string(),
968            });
969        }
970
971        let kind_bit = if kind.is_repair() {
972            PACKED_KIND_REPAIR_BIT
973        } else {
974            0
975        };
976        Ok(kind_bit | (u32::from(sbn) << PACKED_SBN_SHIFT) | esi)
977    }
978
979    fn unpack_symbol_key(packed: u32) -> (SymbolKind, u8, u32) {
980        let kind = if packed & PACKED_KIND_REPAIR_BIT == 0 {
981            SymbolKind::Source
982        } else {
983            SymbolKind::Repair
984        };
985        let sbn = ((packed >> PACKED_SBN_SHIFT) & PACKED_SBN_MASK) as u8;
986        let esi = packed & PACKED_ESI_MASK;
987        (kind, sbn, esi)
988    }
989
990    impl SymbolSink for VecTransportSink {
991        fn poll_send(
992            mut self: Pin<&mut Self>,
993            _cx: &mut Context<'_>,
994            symbol: AuthenticatedSymbol,
995        ) -> Poll<std::result::Result<(), SinkError>> {
996            self.symbols.push(symbol.into_symbol());
997            Poll::Ready(Ok(()))
998        }
999
1000        fn poll_flush(
1001            self: Pin<&mut Self>,
1002            _cx: &mut Context<'_>,
1003        ) -> Poll<std::result::Result<(), SinkError>> {
1004            Poll::Ready(Ok(()))
1005        }
1006
1007        fn poll_close(
1008            self: Pin<&mut Self>,
1009            _cx: &mut Context<'_>,
1010        ) -> Poll<std::result::Result<(), SinkError>> {
1011            Poll::Ready(Ok(()))
1012        }
1013
1014        fn poll_ready(
1015            self: Pin<&mut Self>,
1016            _cx: &mut Context<'_>,
1017        ) -> Poll<std::result::Result<(), SinkError>> {
1018            Poll::Ready(Ok(()))
1019        }
1020    }
1021
1022    /// SymbolCodec backed by asupersync.
1023    struct AsupersyncCodec;
1024
1025    impl SymbolCodec for AsupersyncCodec {
1026        fn encode(
1027            &self,
1028            _cx: &Cx,
1029            source_data: &[u8],
1030            symbol_size: u32,
1031            repair_overhead: f64,
1032        ) -> Result<CodecEncodeResult> {
1033            let mut config = RaptorQConfig::default();
1034            config.encoding.symbol_size = symbol_size as u16;
1035            config.encoding.max_block_size = TEST_MAX_BLOCK_SIZE;
1036            config.encoding.repair_overhead = repair_overhead;
1037
1038            let cx = AsCx::for_testing();
1039            let object_id = AsObjectId::new_for_test(TEST_OBJECT_ID);
1040            let mut sender = RaptorQSenderBuilder::new()
1041                .config(config)
1042                .transport(VecTransportSink::new())
1043                .build()
1044                .map_err(|e| FrankenError::Internal(format!("sender build: {e}")))?;
1045
1046            let outcome = sender
1047                .send_object(&cx, object_id, source_data)
1048                .map_err(|e| FrankenError::Internal(format!("send_object: {e}")))?;
1049
1050            let symbols = std::mem::take(&mut sender.transport_mut().symbols);
1051            let k = outcome.source_symbols as u32;
1052
1053            let mut source_symbols = Vec::new();
1054            let mut repair_symbols = Vec::new();
1055            for s in &symbols {
1056                let packed_key = pack_symbol_key(s.kind(), s.sbn(), s.esi())?;
1057                if s.kind().is_source() {
1058                    source_symbols.push((packed_key, s.data().to_vec()));
1059                } else {
1060                    repair_symbols.push((packed_key, s.data().to_vec()));
1061                }
1062            }
1063
1064            Ok(CodecEncodeResult {
1065                source_symbols,
1066                repair_symbols,
1067                k_source: k,
1068            })
1069        }
1070
1071        fn decode(
1072            &self,
1073            _cx: &Cx,
1074            symbols: &[(u32, Vec<u8>)],
1075            k_source: u32,
1076            symbol_size: u32,
1077        ) -> Result<CodecDecodeResult> {
1078            if symbols.is_empty() {
1079                return Ok(CodecDecodeResult::Failure {
1080                    reason: DecodeFailureReason::InsufficientSymbols,
1081                    symbols_received: 0,
1082                    k_required: k_source,
1083                });
1084            }
1085
1086            let object_id = AsObjectId::new_for_test(TEST_OBJECT_ID);
1087            let mut config = RaptorQConfig::default();
1088            config.encoding.symbol_size = symbol_size as u16;
1089            config.encoding.max_block_size = TEST_MAX_BLOCK_SIZE;
1090
1091            // The test codec must match the same block geometry the encoder
1092            // used.  With max_block_size = TEST_MAX_BLOCK_SIZE, the encoder
1093            // partitions into ceil(k * symbol_size / max_block_size) blocks.
1094            let max_block_symbols = (TEST_MAX_BLOCK_SIZE as u32 / symbol_size.max(1)).max(1);
1095            let source_blocks = k_source.div_ceil(max_block_symbols).max(1) as u16;
1096            let symbols_per_block = k_source.div_ceil(u32::from(source_blocks)).max(1);
1097            let object_size = u64::from(k_source)
1098                .checked_mul(u64::from(symbol_size))
1099                .ok_or_else(|| FrankenError::OutOfRange {
1100                    what: "object_size for decode params".to_owned(),
1101                    value: format!("{k_source}*{symbol_size}"),
1102                })?;
1103            let params = ObjectParams::new(
1104                object_id,
1105                object_size,
1106                u16::try_from(symbol_size).map_err(|_| FrankenError::OutOfRange {
1107                    what: "symbol_size as u16".to_owned(),
1108                    value: symbol_size.to_string(),
1109                })?,
1110                source_blocks,
1111                u16::try_from(symbols_per_block).map_err(|_| FrankenError::OutOfRange {
1112                    what: "symbols_per_block as u16".to_owned(),
1113                    value: symbols_per_block.to_string(),
1114                })?,
1115            );
1116
1117            let mut rebuilt = Vec::with_capacity(symbols.len());
1118            for (packed, data) in symbols {
1119                let (kind, sbn, esi) = unpack_symbol_key(*packed);
1120                rebuilt.push(Symbol::new(
1121                    SymbolId::new(object_id, sbn, esi),
1122                    data.clone(),
1123                    kind,
1124                ));
1125            }
1126
1127            let cx = AsCx::for_testing();
1128            let mut receiver = RaptorQReceiverBuilder::new()
1129                .config(config)
1130                .source(VecTransportStream::new(rebuilt))
1131                .build()
1132                .map_err(|e| FrankenError::Internal(format!("receiver build: {e}")))?;
1133
1134            match receiver.receive_object(&cx, &params) {
1135                Ok(outcome) => Ok(CodecDecodeResult::Success {
1136                    data: outcome.data,
1137                    symbols_used: outcome.symbols_received as u32,
1138                    peeled_count: 0,
1139                    inactivated_count: 0,
1140                }),
1141                Err(err) => {
1142                    let reason = match err.kind() {
1143                        AsErrorKind::InsufficientSymbols => {
1144                            DecodeFailureReason::InsufficientSymbols
1145                        }
1146                        _ => DecodeFailureReason::SingularMatrix,
1147                    };
1148                    Ok(CodecDecodeResult::Failure {
1149                        reason,
1150                        symbols_received: symbols.len() as u32,
1151                        k_required: k_source,
1152                    })
1153                }
1154            }
1155        }
1156    }
1157
1158    // -----------------------------------------------------------------------
1159    // Helpers
1160    // -----------------------------------------------------------------------
1161
1162    fn deterministic_page_data(k: usize, symbol_size: usize, seed: u64) -> Vec<u8> {
1163        let mut state = seed ^ 0x9E37_79B9_7F4A_7C15;
1164        let total = k * symbol_size;
1165        let mut out = Vec::with_capacity(total);
1166        for idx in 0..total {
1167            state ^= state << 7;
1168            state ^= state >> 9;
1169            state = state.wrapping_mul(0xA24B_AED4_963E_E407);
1170            let idx_byte = (idx % 251) as u8;
1171            out.push((state & 0xFF) as u8 ^ idx_byte);
1172        }
1173        out
1174    }
1175
1176    fn test_cx() -> fsqlite_types::cx::Cx {
1177        fsqlite_types::cx::Cx::new()
1178    }
1179
1180    fn default_codec() -> AsupersyncCodec {
1181        AsupersyncCodec
1182    }
1183
1184    fn default_config() -> PipelineConfig {
1185        PipelineConfig::for_page_size(512)
1186    }
1187
1188    // -----------------------------------------------------------------------
1189    // §3.3 Test 12: Pipeline encode (test_pipeline_encode_async)
1190    // -----------------------------------------------------------------------
1191
1192    #[test]
1193    fn test_pipeline_encode_produces_source_and_repair() {
1194        let config = default_config();
1195        let encoder =
1196            RaptorQPageEncoder::new(config.clone(), default_codec()).expect("encoder build");
1197        let cx = test_cx();
1198        let k = 10_usize;
1199        let data = deterministic_page_data(k, config.symbol_size as usize, 0x1234);
1200
1201        let mut sink = VecPageSink::new();
1202        let outcome = encoder
1203            .encode_pages(&cx, &data, &mut sink)
1204            .expect("encode must succeed");
1205
1206        assert_eq!(
1207            outcome.source_count as usize, k,
1208            "bead_id={BEAD_ID} case=encode_source_count"
1209        );
1210        assert!(
1211            outcome.repair_count > 0,
1212            "bead_id={BEAD_ID} case=encode_repair_present"
1213        );
1214        assert_eq!(
1215            outcome.symbol_size, config.symbol_size,
1216            "bead_id={BEAD_ID} case=encode_symbol_size"
1217        );
1218        assert!(sink.flushed, "bead_id={BEAD_ID} case=encode_sink_flushed");
1219
1220        // Verify source symbols contain original page data.
1221        let sym_size = config.symbol_size as usize;
1222        for i in 0..k {
1223            let esi = i as u32;
1224            let expected = &data[i * sym_size..(i + 1) * sym_size];
1225            let actual = sink.symbols.get(&esi);
1226            assert!(actual.is_some(), "source symbol ESI {esi} missing");
1227            let actual = actual.expect("source symbol existence asserted");
1228            assert_eq!(
1229                actual, expected,
1230                "bead_id={BEAD_ID} case=encode_source_symbol_matches esi={esi}"
1231            );
1232        }
1233
1234        info!(
1235            bead_id = BEAD_ID,
1236            source_count = outcome.source_count,
1237            repair_count = outcome.repair_count,
1238            total_written = sink.written_count(),
1239            "test_pipeline_encode complete"
1240        );
1241    }
1242
1243    // -----------------------------------------------------------------------
1244    // §3.3 Test 13: Pipeline decode (test_pipeline_decode_async)
1245    // -----------------------------------------------------------------------
1246
1247    #[test]
1248    fn test_pipeline_decode_with_extra_symbols() {
1249        let config = default_config();
1250        let encoder =
1251            RaptorQPageEncoder::new(config.clone(), default_codec()).expect("encoder build");
1252        let decoder =
1253            RaptorQPageDecoder::new(config.clone(), default_codec()).expect("decoder build");
1254        let cx = test_cx();
1255        let k = 10_usize;
1256        let data = deterministic_page_data(k, config.symbol_size as usize, 0x5678);
1257
1258        // Encode.
1259        let mut sink = VecPageSink::new();
1260        let outcome = encoder
1261            .encode_pages(&cx, &data, &mut sink)
1262            .expect("encode must succeed");
1263
1264        // Decode from all symbols (K + repair).
1265        let mut source = VecPageSource::from_sink(&sink);
1266        let decode_outcome = decoder
1267            .decode_pages(&cx, &mut source, outcome.source_count)
1268            .expect("decode must succeed");
1269
1270        match decode_outcome {
1271            DecodeOutcome::Success(success) => {
1272                assert_eq!(
1273                    success.data, data,
1274                    "bead_id={BEAD_ID} case=decode_roundtrip_bytes"
1275                );
1276                assert!(
1277                    success.symbols_used >= outcome.source_count,
1278                    "bead_id={BEAD_ID} case=decode_symbols_used"
1279                );
1280                info!(
1281                    bead_id = BEAD_ID,
1282                    symbols_used = success.symbols_used,
1283                    peeled = success.peeled_count,
1284                    inactivated = success.inactivated_count,
1285                    "test_pipeline_decode complete"
1286                );
1287            }
1288            DecodeOutcome::Failure(failure) => unreachable!(
1289                "bead_id={BEAD_ID} case=decode_unexpected_failure reason={:?}",
1290                failure.reason
1291            ),
1292        }
1293    }
1294
1295    // -----------------------------------------------------------------------
1296    // §3.3 Test 14: Cancel-safety (test_pipeline_cancel_safe)
1297    // -----------------------------------------------------------------------
1298
1299    #[test]
1300    fn test_pipeline_cancel_safe_encode() {
1301        let config = PipelineConfig {
1302            checkpoint_interval: 2, // checkpoint every 2 symbols
1303            ..default_config()
1304        };
1305        let encoder =
1306            RaptorQPageEncoder::new(config.clone(), default_codec()).expect("encoder build");
1307
1308        // Create a Cx that is already cancelled.
1309        let cx = fsqlite_types::cx::Cx::new();
1310        cx.cancel_with_reason(fsqlite_types::cx::CancelReason::UserInterrupt);
1311
1312        let k = 10_usize;
1313        let data = deterministic_page_data(k, config.symbol_size as usize, 0xABCD);
1314        let mut sink = VecPageSink::new();
1315
1316        let result = encoder.encode_pages(&cx, &data, &mut sink);
1317        assert!(
1318            result.is_err(),
1319            "bead_id={BEAD_ID} case=cancel_safe_encode_aborts"
1320        );
1321        assert!(
1322            matches!(result.unwrap_err(), FrankenError::Abort),
1323            "bead_id={BEAD_ID} case=cancel_safe_encode_error_type"
1324        );
1325        // Sink should not have been flushed.
1326        assert!(!sink.flushed, "bead_id={BEAD_ID} case=cancel_safe_no_flush");
1327    }
1328
1329    #[test]
1330    fn test_pipeline_cancel_safe_decode() {
1331        let config = PipelineConfig {
1332            checkpoint_interval: 2,
1333            ..default_config()
1334        };
1335        let decoder =
1336            RaptorQPageDecoder::new(config.clone(), default_codec()).expect("decoder build");
1337
1338        // Create a Cx that is already cancelled.
1339        let cx = fsqlite_types::cx::Cx::new();
1340        cx.cancel_with_reason(fsqlite_types::cx::CancelReason::UserInterrupt);
1341
1342        // Feed some symbols.
1343        let mut symbols = BTreeMap::new();
1344        for esi in 0..10_u32 {
1345            symbols.insert(esi, vec![0xAA; config.symbol_size as usize]);
1346        }
1347        let mut source = VecPageSource::from_map(symbols);
1348
1349        let result = decoder.decode_pages(&cx, &mut source, 10);
1350        assert!(
1351            result.is_err(),
1352            "bead_id={BEAD_ID} case=cancel_safe_decode_aborts"
1353        );
1354        assert!(
1355            matches!(result.unwrap_err(), FrankenError::Abort),
1356            "bead_id={BEAD_ID} case=cancel_safe_decode_error_type"
1357        );
1358    }
1359
1360    // -----------------------------------------------------------------------
1361    // §3.3 Test 15: Backpressure (test_pipeline_backpressure)
1362    // -----------------------------------------------------------------------
1363
1364    /// Sink that fails after N writes, simulating a full output buffer.
1365    struct BackpressureSink {
1366        limit: u32,
1367        count: u32,
1368    }
1369
1370    impl BackpressureSink {
1371        fn new(limit: u32) -> Self {
1372            Self { limit, count: 0 }
1373        }
1374    }
1375
1376    impl PageSymbolSink for BackpressureSink {
1377        fn write_symbol(&mut self, _esi: u32, _data: &[u8]) -> Result<()> {
1378            if self.count >= self.limit {
1379                return Err(FrankenError::Busy);
1380            }
1381            self.count += 1;
1382            Ok(())
1383        }
1384
1385        fn flush(&mut self) -> Result<()> {
1386            Ok(())
1387        }
1388
1389        fn written_count(&self) -> u32 {
1390            self.count
1391        }
1392    }
1393
1394    #[test]
1395    fn test_pipeline_backpressure_sink_full() {
1396        let config = default_config();
1397        let encoder =
1398            RaptorQPageEncoder::new(config.clone(), default_codec()).expect("encoder build");
1399        let cx = test_cx();
1400        let k = 10_usize;
1401        let data = deterministic_page_data(k, config.symbol_size as usize, 0xEEFF);
1402
1403        // Sink that only accepts 3 symbols then returns Busy.
1404        let mut sink = BackpressureSink::new(3);
1405        let result = encoder.encode_pages(&cx, &data, &mut sink);
1406
1407        assert!(
1408            result.is_err(),
1409            "bead_id={BEAD_ID} case=backpressure_propagated"
1410        );
1411        assert!(
1412            matches!(result.unwrap_err(), FrankenError::Busy),
1413            "bead_id={BEAD_ID} case=backpressure_error_type"
1414        );
1415        assert_eq!(
1416            sink.written_count(),
1417            3,
1418            "bead_id={BEAD_ID} case=backpressure_partial_write"
1419        );
1420    }
1421
1422    // -----------------------------------------------------------------------
1423    // Config Validation Tests
1424    // -----------------------------------------------------------------------
1425
1426    #[test]
1427    fn test_config_validation_zero_symbol_size() {
1428        let config = PipelineConfig {
1429            symbol_size: 0,
1430            ..default_config()
1431        };
1432        assert!(
1433            config.validate().is_err(),
1434            "bead_id={BEAD_ID} case=config_reject_zero_symbol_size"
1435        );
1436    }
1437
1438    #[test]
1439    fn test_config_validation_non_power_of_two() {
1440        let config = PipelineConfig {
1441            symbol_size: 1000,
1442            ..default_config()
1443        };
1444        assert!(
1445            config.validate().is_err(),
1446            "bead_id={BEAD_ID} case=config_reject_non_power_of_two"
1447        );
1448    }
1449
1450    #[test]
1451    fn test_config_validation_below_min() {
1452        let config = PipelineConfig {
1453            symbol_size: 256,
1454            ..default_config()
1455        };
1456        assert!(
1457            config.validate().is_err(),
1458            "bead_id={BEAD_ID} case=config_reject_below_min"
1459        );
1460    }
1461
1462    #[test]
1463    fn test_config_validation_above_max() {
1464        let config = PipelineConfig {
1465            symbol_size: 128 * 1024,
1466            ..default_config()
1467        };
1468        assert!(
1469            config.validate().is_err(),
1470            "bead_id={BEAD_ID} case=config_reject_above_max"
1471        );
1472    }
1473
1474    #[test]
1475    fn test_config_validation_zero_max_block_size() {
1476        let config = PipelineConfig {
1477            max_block_size: 0,
1478            ..default_config()
1479        };
1480        assert!(
1481            config.validate().is_err(),
1482            "bead_id={BEAD_ID} case=config_reject_zero_max_block"
1483        );
1484    }
1485
1486    #[test]
1487    fn test_config_validation_repair_overhead_below_one() {
1488        let config = PipelineConfig {
1489            repair_overhead: 0.5,
1490            ..default_config()
1491        };
1492        assert!(
1493            config.validate().is_err(),
1494            "bead_id={BEAD_ID} case=config_reject_repair_overhead_below_one"
1495        );
1496    }
1497
1498    #[test]
1499    fn test_config_validation_zero_checkpoint_interval() {
1500        let config = PipelineConfig {
1501            checkpoint_interval: 0,
1502            ..default_config()
1503        };
1504        assert!(
1505            config.validate().is_err(),
1506            "bead_id={BEAD_ID} case=config_reject_zero_checkpoint_interval"
1507        );
1508    }
1509
1510    #[test]
1511    fn test_config_validation_valid_configs() {
1512        for symbol_size in [512, 1024, 2048, 4096, 8192, 16384, 32768, 65536] {
1513            let config = PipelineConfig::for_page_size(symbol_size);
1514            assert!(
1515                config.validate().is_ok(),
1516                "bead_id={BEAD_ID} case=config_valid symbol_size={symbol_size}"
1517            );
1518        }
1519    }
1520
1521    // -----------------------------------------------------------------------
1522    // Decode Proof on Failure
1523    // -----------------------------------------------------------------------
1524
1525    #[test]
1526    fn test_decode_failure_insufficient_symbols() {
1527        let config = default_config();
1528        let encoder =
1529            RaptorQPageEncoder::new(config.clone(), default_codec()).expect("encoder build");
1530        let decoder =
1531            RaptorQPageDecoder::new(config.clone(), default_codec()).expect("decoder build");
1532        let cx = test_cx();
1533        let k = 10_usize;
1534        let data = deterministic_page_data(k, config.symbol_size as usize, 0xDEAD);
1535
1536        // Encode.
1537        let mut sink = VecPageSink::new();
1538        let outcome = encoder
1539            .encode_pages(&cx, &data, &mut sink)
1540            .expect("encode must succeed");
1541
1542        // Keep only K-3 source symbols (insufficient).
1543        let mut partial = BTreeMap::new();
1544        for esi in 0..((k - 3) as u32) {
1545            if let Some(sym) = sink.symbols.get(&esi) {
1546                partial.insert(esi, sym.clone());
1547            }
1548        }
1549        let mut source = VecPageSource::from_map(partial);
1550
1551        let decode_outcome = decoder
1552            .decode_pages(&cx, &mut source, outcome.source_count)
1553            .expect("decode call itself should not error");
1554
1555        match decode_outcome {
1556            DecodeOutcome::Failure(failure) => {
1557                assert_eq!(
1558                    failure.reason,
1559                    DecodeFailureReason::InsufficientSymbols,
1560                    "bead_id={BEAD_ID} case=decode_failure_reason"
1561                );
1562                assert!(
1563                    failure.symbols_received < outcome.source_count,
1564                    "bead_id={BEAD_ID} case=decode_failure_symbol_count"
1565                );
1566                assert_eq!(
1567                    failure.k_required, outcome.source_count,
1568                    "bead_id={BEAD_ID} case=decode_failure_k_required"
1569                );
1570                assert!(
1571                    failure.decode_proof.is_none(),
1572                    "bead_id={BEAD_ID} case=decode_failure_proof_disabled_by_default"
1573                );
1574            }
1575            DecodeOutcome::Success(_) => {
1576                unreachable!("bead_id={BEAD_ID} case=decode_should_have_failed")
1577            }
1578        }
1579    }
1580
1581    #[test]
1582    fn test_decode_failure_emits_proof_when_enabled() {
1583        let mut config = default_config();
1584        config.decode_proof_policy = DecodeProofEmissionPolicy {
1585            emit_on_decode_failure: true,
1586            emit_on_repair_success: false,
1587        };
1588        let encoder =
1589            RaptorQPageEncoder::new(config.clone(), default_codec()).expect("encoder build");
1590        let decoder =
1591            RaptorQPageDecoder::new(config.clone(), default_codec()).expect("decoder build");
1592        let cx = test_cx();
1593        let k = 10_usize;
1594        let data = deterministic_page_data(k, config.symbol_size as usize, 0xFA24);
1595
1596        let mut sink = VecPageSink::new();
1597        let outcome = encoder
1598            .encode_pages(&cx, &data, &mut sink)
1599            .expect("encode must succeed");
1600
1601        let mut partial = BTreeMap::new();
1602        for esi in 0..((k - 2) as u32) {
1603            if let Some(sym) = sink.symbols.get(&esi) {
1604                partial.insert(esi, sym.clone());
1605            }
1606        }
1607        let mut source = VecPageSource::from_map(partial);
1608        let decode_outcome = decoder
1609            .decode_pages(&cx, &mut source, outcome.source_count)
1610            .expect("decode call itself should not error");
1611
1612        match decode_outcome {
1613            DecodeOutcome::Failure(failure) => {
1614                let proof = failure
1615                    .decode_proof
1616                    .expect("bead_id=bd-faz4 case=decode_failure_proof_emitted");
1617                assert!(
1618                    !proof.decode_success,
1619                    "bead_id=bd-faz4 case=decode_failure_proof_flag"
1620                );
1621                assert!(
1622                    proof.is_consistent(),
1623                    "bead_id=bd-faz4 case=decode_failure_proof_consistent"
1624                );
1625            }
1626            DecodeOutcome::Success(_) => {
1627                unreachable!("bead_id=bd-faz4 case=decode_failure_expected")
1628            }
1629        }
1630    }
1631
1632    #[test]
1633    fn test_decode_success_with_repair_emits_proof_when_enabled() {
1634        let mut config = default_config();
1635        config.decode_proof_policy = DecodeProofEmissionPolicy {
1636            emit_on_decode_failure: false,
1637            emit_on_repair_success: true,
1638        };
1639        let encoder =
1640            RaptorQPageEncoder::new(config.clone(), default_codec()).expect("encoder build");
1641        let decoder =
1642            RaptorQPageDecoder::new(config.clone(), default_codec()).expect("decoder build");
1643        let cx = test_cx();
1644        let k = 10_usize;
1645        let data = deterministic_page_data(k, config.symbol_size as usize, 0xF0AA);
1646
1647        let mut sink = VecPageSink::new();
1648        let outcome = encoder
1649            .encode_pages(&cx, &data, &mut sink)
1650            .expect("encode must succeed");
1651        let mut source = VecPageSource::from_sink(&sink);
1652        let decode_outcome = decoder
1653            .decode_pages(&cx, &mut source, outcome.source_count)
1654            .expect("decode must succeed");
1655
1656        match decode_outcome {
1657            DecodeOutcome::Success(success) => {
1658                let proof = success
1659                    .decode_proof
1660                    .expect("bead_id=bd-faz4 case=repair_success_proof_emitted");
1661                assert!(proof.decode_success);
1662                assert!(proof.is_repair());
1663                assert!(
1664                    proof.is_consistent(),
1665                    "bead_id=bd-faz4 case=repair_success_proof_consistent"
1666                );
1667            }
1668            DecodeOutcome::Failure(failure) => unreachable!(
1669                "bead_id=bd-faz4 case=repair_success_should_decode reason={:?}",
1670                failure.reason
1671            ),
1672        }
1673    }
1674
1675    // -----------------------------------------------------------------------
1676    // E2E Round-trip: encode → store → read → decode → verify
1677    // -----------------------------------------------------------------------
1678
1679    #[test]
1680    fn test_e2e_roundtrip_multiple_page_sizes() {
1681        for &symbol_size in &[512_u32, 1024, 4096] {
1682            let config = PipelineConfig::for_page_size(symbol_size);
1683            let encoder =
1684                RaptorQPageEncoder::new(config.clone(), default_codec()).expect("encoder build");
1685            let decoder =
1686                RaptorQPageDecoder::new(config.clone(), default_codec()).expect("decoder build");
1687            let cx = test_cx();
1688
1689            let k = 8_usize;
1690            let data = deterministic_page_data(k, symbol_size as usize, u64::from(symbol_size));
1691
1692            // Encode → store.
1693            let mut sink = VecPageSink::new();
1694            let outcome = encoder
1695                .encode_pages(&cx, &data, &mut sink)
1696                .expect("encode must succeed");
1697
1698            // Read → decode.
1699            let mut source = VecPageSource::from_sink(&sink);
1700            let decode_result = decoder
1701                .decode_pages(&cx, &mut source, outcome.source_count)
1702                .expect("decode must succeed");
1703
1704            match decode_result {
1705                DecodeOutcome::Success(success) => {
1706                    assert_eq!(
1707                        success.data, data,
1708                        "bead_id={BEAD_ID} case=e2e_roundtrip symbol_size={symbol_size}"
1709                    );
1710                }
1711                DecodeOutcome::Failure(f) => unreachable!(
1712                    "bead_id={BEAD_ID} case=e2e_roundtrip_failure symbol_size={symbol_size} reason={:?}",
1713                    f.reason
1714                ),
1715            }
1716        }
1717    }
1718
1719    #[test]
1720    fn test_e2e_roundtrip_64_pages() {
1721        let _serial = crate::connection::fsqlite_core_test_serializer();
1722        let config = PipelineConfig::for_page_size(4096);
1723        let encoder =
1724            RaptorQPageEncoder::new(config.clone(), default_codec()).expect("encoder build");
1725        let decoder =
1726            RaptorQPageDecoder::new(config.clone(), default_codec()).expect("decoder build");
1727        let cx = test_cx();
1728
1729        let k = 64_usize;
1730        let data = deterministic_page_data(k, config.symbol_size as usize, 0xE2E6_4000);
1731
1732        let mut sink = VecPageSink::new();
1733        let outcome = encoder
1734            .encode_pages(&cx, &data, &mut sink)
1735            .expect("encode must succeed");
1736
1737        assert_eq!(
1738            outcome.source_count as usize, k,
1739            "bead_id={BEAD_ID} case=e2e_64_source_count"
1740        );
1741
1742        let mut source = VecPageSource::from_sink(&sink);
1743        let decode_result = decoder
1744            .decode_pages(&cx, &mut source, outcome.source_count)
1745            .expect("decode must succeed");
1746
1747        match decode_result {
1748            DecodeOutcome::Success(success) => {
1749                assert_eq!(
1750                    success.data, data,
1751                    "bead_id={BEAD_ID} case=e2e_64_roundtrip_bytes"
1752                );
1753                info!(
1754                    bead_id = BEAD_ID,
1755                    k,
1756                    peeled = success.peeled_count,
1757                    inactivated = success.inactivated_count,
1758                    "E2E 64-page roundtrip complete"
1759                );
1760            }
1761            DecodeOutcome::Failure(f) => unreachable!(
1762                "bead_id={BEAD_ID} case=e2e_64_failure reason={:?}",
1763                f.reason
1764            ),
1765        }
1766    }
1767
1768    #[test]
1769    fn test_e2e_bd_1hi_5() {
1770        let _serial = crate::connection::fsqlite_core_test_serializer();
1771        let config = PipelineConfig::for_page_size(4096);
1772        let encoder =
1773            RaptorQPageEncoder::new(config.clone(), default_codec()).expect("encoder build");
1774        let decoder =
1775            RaptorQPageDecoder::new(config.clone(), default_codec()).expect("decoder build");
1776        let cx = test_cx();
1777
1778        // Realistic load for this lane: 64 pages (256 KiB) with symbol loss.
1779        let k = 64_usize;
1780        let data = deterministic_page_data(k, config.symbol_size as usize, 0xB1D1_5005);
1781        let mut sink = VecPageSink::new();
1782        let outcome = encoder
1783            .encode_pages(&cx, &data, &mut sink)
1784            .expect("encode must succeed");
1785
1786        // Drop one source symbol per source block; keep all repair symbols.
1787        let mut dropped = 0_u32;
1788        let mut degraded = BTreeMap::new();
1789        for (packed_key, symbol_bytes) in &sink.symbols {
1790            let (kind, _sbn, esi) = unpack_symbol_key(*packed_key);
1791            if kind.is_source() && esi == 0 {
1792                dropped += 1;
1793                continue;
1794            }
1795            degraded.insert(*packed_key, symbol_bytes.clone());
1796        }
1797        assert!(dropped > 0, "bead_id={BEAD_ID} case=e2e_named_dropped_some");
1798
1799        let mut source = VecPageSource::from_map(degraded);
1800        let decode_result = decoder
1801            .decode_pages(&cx, &mut source, outcome.source_count)
1802            .expect("decode must complete");
1803
1804        match decode_result {
1805            DecodeOutcome::Success(success) => {
1806                assert_eq!(
1807                    success.data, data,
1808                    "bead_id={BEAD_ID} case=e2e_named_byte_perfect_recovery"
1809                );
1810            }
1811            DecodeOutcome::Failure(f) => unreachable!(
1812                "bead_id={BEAD_ID} case=e2e_named_unexpected_failure reason={:?}",
1813                f.reason
1814            ),
1815        }
1816    }
1817
1818    // -----------------------------------------------------------------------
1819    // E2E: Retry after failure
1820    // -----------------------------------------------------------------------
1821
1822    #[test]
1823    fn test_e2e_retry_after_failure() {
1824        let config = default_config();
1825        let encoder =
1826            RaptorQPageEncoder::new(config.clone(), default_codec()).expect("encoder build");
1827        let decoder =
1828            RaptorQPageDecoder::new(config.clone(), default_codec()).expect("decoder build");
1829        let cx = test_cx();
1830        let k = 10_usize;
1831        let data = deterministic_page_data(k, config.symbol_size as usize, 0xAE_7121);
1832
1833        // Encode.
1834        let mut sink = VecPageSink::new();
1835        let outcome = encoder
1836            .encode_pages(&cx, &data, &mut sink)
1837            .expect("encode must succeed");
1838
1839        // First attempt: K-2 source symbols only → should fail.
1840        let mut partial = BTreeMap::new();
1841        for esi in 0..((k - 2) as u32) {
1842            if let Some(sym) = sink.symbols.get(&esi) {
1843                partial.insert(esi, sym.clone());
1844            }
1845        }
1846        let mut source_attempt1 = VecPageSource::from_map(partial.clone());
1847        let result1 = decoder
1848            .decode_pages(&cx, &mut source_attempt1, outcome.source_count)
1849            .expect("decode call should not error");
1850        assert!(
1851            matches!(result1, DecodeOutcome::Failure(_)),
1852            "bead_id={BEAD_ID} case=retry_first_attempt_fails"
1853        );
1854
1855        // Second attempt: add all remaining symbols → should succeed.
1856        let full = sink.symbols.clone();
1857        let mut source_attempt2 = VecPageSource::from_map(full);
1858        let result2 = decoder
1859            .decode_pages(&cx, &mut source_attempt2, outcome.source_count)
1860            .expect("decode call should not error");
1861        match result2 {
1862            DecodeOutcome::Success(success) => {
1863                assert_eq!(
1864                    success.data, data,
1865                    "bead_id={BEAD_ID} case=retry_second_attempt_succeeds"
1866                );
1867            }
1868            DecodeOutcome::Failure(f) => unreachable!(
1869                "bead_id={BEAD_ID} case=retry_second_should_succeed reason={:?}",
1870                f.reason
1871            ),
1872        }
1873    }
1874
1875    // -----------------------------------------------------------------------
1876    // Decode with exact K symbols (fragile recovery)
1877    // -----------------------------------------------------------------------
1878
1879    #[test]
1880    fn test_decode_source_only_exact_k() {
1881        let config = default_config();
1882        let encoder =
1883            RaptorQPageEncoder::new(config.clone(), default_codec()).expect("encoder build");
1884        let decoder =
1885            RaptorQPageDecoder::new(config.clone(), default_codec()).expect("decoder build");
1886        let cx = test_cx();
1887        let k = 8_usize;
1888        let data = deterministic_page_data(k, config.symbol_size as usize, 0xE4AC7);
1889
1890        let mut sink = VecPageSink::new();
1891        let outcome = encoder
1892            .encode_pages(&cx, &data, &mut sink)
1893            .expect("encode must succeed");
1894
1895        // Keep only K source symbols (no repair).
1896        let mut source_only = BTreeMap::new();
1897        for esi in 0..(k as u32) {
1898            if let Some(sym) = sink.symbols.get(&esi) {
1899                source_only.insert(esi, sym.clone());
1900            }
1901        }
1902
1903        let mut source = VecPageSource::from_map(source_only);
1904        let decode_result = decoder
1905            .decode_pages(&cx, &mut source, outcome.source_count)
1906            .expect("decode must not error");
1907
1908        match decode_result {
1909            DecodeOutcome::Success(success) => {
1910                assert_eq!(
1911                    success.data, data,
1912                    "bead_id={BEAD_ID} case=exact_k_roundtrip"
1913                );
1914                assert_eq!(
1915                    success.symbols_used, k as u32,
1916                    "bead_id={BEAD_ID} case=exact_k_symbols_used"
1917                );
1918            }
1919            DecodeOutcome::Failure(f) => unreachable!(
1920                "bead_id={BEAD_ID} case=exact_k_should_succeed reason={:?}",
1921                f.reason
1922            ),
1923        }
1924    }
1925
1926    // -------------------------------------------------------------------
1927    // bd-3bw.1: RaptorQ Metrics Tests
1928    //
1929    // Unit tests use a local RaptorQMetrics instance to avoid
1930    // interference from parallel tests sharing the global singleton.
1931    // Integration test verifies the global is wired up.
1932    // -------------------------------------------------------------------
1933
1934    #[test]
1935    fn metrics_struct_encode_counters() {
1936        let m = RaptorQMetrics::new();
1937        m.record_encode(2048, 3);
1938        m.record_encode(4096, 5);
1939
1940        let snap = m.snapshot();
1941        assert_eq!(snap.encode_ops, 2);
1942        assert_eq!(snap.encoded_bytes_total, 6144);
1943        assert_eq!(snap.repair_symbols_generated_total, 8);
1944        assert_eq!(snap.decode_ops, 0);
1945    }
1946
1947    #[test]
1948    fn metrics_struct_decode_counters() {
1949        let m = RaptorQMetrics::new();
1950        m.record_decode_success(4096);
1951        m.record_decode_success(2048);
1952        m.record_decode_failure();
1953
1954        let snap = m.snapshot();
1955        assert_eq!(snap.decode_ops, 3);
1956        assert_eq!(snap.decoded_bytes_total, 6144);
1957        assert_eq!(snap.decode_failures, 1);
1958        assert_eq!(snap.encode_ops, 0);
1959    }
1960
1961    #[test]
1962    fn metrics_snapshot_display() {
1963        let m = RaptorQMetrics::new();
1964        m.record_encode(4096, 2);
1965        m.record_decode_success(4096);
1966        let snap = m.snapshot();
1967        let display = format!("{snap}");
1968        assert!(display.contains("4096"), "encoded bytes in display");
1969        assert!(display.contains("2 repair"), "repair syms in display");
1970    }
1971
1972    #[test]
1973    fn metrics_reset() {
1974        let m = RaptorQMetrics::new();
1975        m.record_encode(1000, 5);
1976        m.record_decode_success(500);
1977        m.record_decode_failure();
1978        m.reset();
1979        let snap = m.snapshot();
1980        assert_eq!(snap.encoded_bytes_total, 0);
1981        assert_eq!(snap.repair_symbols_generated_total, 0);
1982        assert_eq!(snap.encode_ops, 0);
1983        assert_eq!(snap.decode_ops, 0);
1984        assert_eq!(snap.decode_failures, 0);
1985        assert_eq!(snap.decoded_bytes_total, 0);
1986    }
1987
1988    #[test]
1989    fn metrics_global_wired_to_encode_decode() {
1990        // Verify that encode_pages / decode_pages bump the global.
1991        // We use >= on deltas because other parallel tests also touch
1992        // the global singleton.
1993        let before = GLOBAL_RAPTORQ_METRICS.snapshot();
1994
1995        let config = default_config();
1996        let encoder =
1997            RaptorQPageEncoder::new(config.clone(), default_codec()).expect("encoder build");
1998        let decoder =
1999            RaptorQPageDecoder::new(config.clone(), default_codec()).expect("decoder build");
2000        let cx = test_cx();
2001        let k = 4_usize;
2002        let data = deterministic_page_data(k, config.symbol_size as usize, 0xF00D);
2003
2004        let mut sink = VecPageSink::new();
2005        let outcome = encoder.encode_pages(&cx, &data, &mut sink).expect("encode");
2006        let mut source = VecPageSource::from_sink(&sink);
2007        let _decode = decoder
2008            .decode_pages(&cx, &mut source, outcome.source_count)
2009            .expect("decode");
2010
2011        let after = GLOBAL_RAPTORQ_METRICS.snapshot();
2012        assert!(
2013            after.encode_ops > before.encode_ops,
2014            "global encode_ops should have increased"
2015        );
2016        assert!(
2017            after.encoded_bytes_total > before.encoded_bytes_total,
2018            "global encoded_bytes should have increased"
2019        );
2020        assert!(
2021            after.decode_ops > before.decode_ops,
2022            "global decode_ops should have increased"
2023        );
2024        assert!(
2025            after.decoded_bytes_total > before.decoded_bytes_total,
2026            "global decoded_bytes should have increased"
2027        );
2028    }
2029}