Skip to main content

fsqlite_core/
repair_engine.rs

1//! §3.5 Unified RaptorQ Decode/Repair Engine with BLAKE3 Proofs (bd-n0g4q.3).
2//!
3//! Provides BLAKE3-based corruption detection and RaptorQ-powered automatic
4//! repair with auditable witness proofs.  This is the "self-healing" core:
5//!
6//! 1. **Detection**: compute BLAKE3 checksum of page data; compare against
7//!    expected hash from group metadata.
8//! 2. **Repair**: gather available source + repair symbols, invoke RaptorQ
9//!    `InactivationDecoder`, reconstruct missing source pages.
10//! 3. **Witness**: emit a `RepairWitness` triple `(corrupted_hash,
11//!    repaired_hash, expected_hash)` for every repair action.
12
13use tracing::{debug, error, info, warn};
14
15use crate::db_fec::{self, DbFecGroupMeta, RepairResult};
16
17const BEAD_ID: &str = "bd-n0g4q.3";
18
19// ---------------------------------------------------------------------------
20// BLAKE3 page checksum
21// ---------------------------------------------------------------------------
22
23/// Compute the BLAKE3 checksum of a page (32 bytes).
24#[must_use]
25pub fn blake3_page_checksum(page_data: &[u8]) -> [u8; 32] {
26    *blake3::hash(page_data).as_bytes()
27}
28
29/// Verify a page against its expected BLAKE3 checksum.
30#[must_use]
31pub fn verify_page_blake3(page_data: &[u8], expected: &[u8; 32]) -> bool {
32    blake3_page_checksum(page_data) == *expected
33}
34
35// ---------------------------------------------------------------------------
36// BLAKE3 witness proof
37// ---------------------------------------------------------------------------
38
39/// Auditable witness proof for a page repair action.
40///
41/// The triple `(corrupted_hash, repaired_hash, expected_hash)` provides
42/// cryptographic evidence of what was observed, what was produced, and
43/// what was expected.  This is logged to the evidence ledger (§3.5.8).
44#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
45pub struct RepairWitness {
46    /// 1-based page number that was repaired.
47    pub pgno: u32,
48    /// BLAKE3 hash of the corrupted page data (before repair).
49    pub corrupted_hash: [u8; 32],
50    /// BLAKE3 hash of the repaired page data (after repair).
51    pub repaired_hash: [u8; 32],
52    /// Expected BLAKE3 hash from group metadata.
53    pub expected_hash: [u8; 32],
54    /// Whether the repair was verified (repaired_hash == expected_hash).
55    pub verified: bool,
56    /// Number of symbols consumed during RaptorQ decode.
57    pub symbols_used: u32,
58    /// Number of corrupt pages detected in the group.
59    pub corrupt_pages_in_group: u32,
60}
61
62impl RepairWitness {
63    /// Whether this witness records a successful, verified repair.
64    #[must_use]
65    pub fn is_success(&self) -> bool {
66        self.verified
67    }
68}
69
70// ---------------------------------------------------------------------------
71// Repair engine outcome
72// ---------------------------------------------------------------------------
73
74/// Outcome of a detect-and-repair operation.
75#[derive(Debug, Clone)]
76pub enum RepairOutcome {
77    /// Page was intact — no repair needed.
78    Intact { pgno: u32, blake3_hash: [u8; 32] },
79    /// Page was corrupted and successfully repaired.
80    Repaired {
81        pgno: u32,
82        repaired_data: Vec<u8>,
83        witness: RepairWitness,
84    },
85    /// Page was corrupted but repair failed (insufficient symbols or
86    /// verification mismatch).
87    Unrecoverable {
88        pgno: u32,
89        witness: Option<RepairWitness>,
90        detail: String,
91    },
92}
93
94// ---------------------------------------------------------------------------
95// Unified detect-and-repair entry point
96// ---------------------------------------------------------------------------
97
98/// Detect corruption and attempt automatic repair for a single page.
99///
100/// This is the unified entry point for self-healing durability:
101///
102/// 1. Read the page and compute its BLAKE3 checksum.
103/// 2. If it matches `expected_blake3`, the page is intact.
104/// 3. If mismatched, invoke `db_fec::attempt_page_repair()` with available
105///    repair symbols.
106/// 4. Produce a `RepairWitness` for every repair (success or failure).
107///
108/// The caller provides:
109/// - `target_pgno`: 1-based page number to check.
110/// - `page_data`: current (possibly corrupted) page bytes.
111/// - `expected_blake3`: expected BLAKE3 hash for this page.
112/// - `group_meta`: db-fec group metadata for this page's group.
113/// - `all_page_data`: closure to read any page in the group by pgno.
114/// - `repair_symbols`: `(esi, data)` pairs from the `.db-fec` sidecar.
115#[allow(clippy::too_many_lines)]
116pub fn detect_and_repair_page(
117    target_pgno: u32,
118    page_data: &[u8],
119    expected_blake3: &[u8; 32],
120    group_meta: &DbFecGroupMeta,
121    all_page_data: &dyn Fn(u32) -> Vec<u8>,
122    repair_symbols: &[(u32, Vec<u8>)],
123) -> RepairOutcome {
124    // Validate target_pgno is within the group range.
125    let group_end = group_meta.start_pgno + group_meta.group_size;
126    if target_pgno < group_meta.start_pgno || target_pgno >= group_end {
127        return RepairOutcome::Unrecoverable {
128            pgno: target_pgno,
129            witness: None,
130            detail: format!(
131                "page {target_pgno} is outside group range [{}, {})",
132                group_meta.start_pgno, group_end,
133            ),
134        };
135    }
136
137    let actual_hash = blake3_page_checksum(page_data);
138
139    // Fast path: page is intact.
140    if actual_hash == *expected_blake3 {
141        debug!(
142            bead_id = BEAD_ID,
143            pgno = target_pgno,
144            "page intact — BLAKE3 checksum verified"
145        );
146        return RepairOutcome::Intact {
147            pgno: target_pgno,
148            blake3_hash: actual_hash,
149        };
150    }
151
152    info!(
153        bead_id = BEAD_ID,
154        pgno = target_pgno,
155        group_start = group_meta.start_pgno,
156        K = group_meta.group_size,
157        R = group_meta.r_repair,
158        "BLAKE3 mismatch detected — initiating repair"
159    );
160
161    // Count corrupt pages in this group for the witness.
162    let mut corrupt_count: u32 = 0;
163    for i in 0..group_meta.group_size {
164        let pgno = group_meta.start_pgno + i;
165        let data = if pgno == target_pgno {
166            page_data.to_vec()
167        } else {
168            all_page_data(pgno)
169        };
170        if !db_fec::verify_page_xxh3_128(&data, &group_meta.source_page_xxh3_128[i as usize]) {
171            corrupt_count += 1;
172        }
173    }
174
175    // Attempt RaptorQ repair.
176    match db_fec::attempt_page_repair(target_pgno, group_meta, all_page_data, repair_symbols) {
177        Ok((repaired_data, repair_result)) => {
178            let repaired_hash = blake3_page_checksum(&repaired_data);
179            let verified = repaired_hash == *expected_blake3;
180
181            let RepairResult::Repaired { symbols_used, .. } = &repair_result else {
182                // attempt_page_repair only returns Ok with RepairResult::Repaired;
183                // other variants are returned via Err.  Defensive fallback.
184                return RepairOutcome::Unrecoverable {
185                    pgno: target_pgno,
186                    witness: None,
187                    detail: format!(
188                        "page {target_pgno}: unexpected repair result variant: {repair_result:?}"
189                    ),
190                };
191            };
192            let symbols_used = *symbols_used;
193
194            let witness = RepairWitness {
195                pgno: target_pgno,
196                corrupted_hash: actual_hash,
197                repaired_hash,
198                expected_hash: *expected_blake3,
199                verified,
200                symbols_used,
201                corrupt_pages_in_group: corrupt_count,
202            };
203
204            if verified {
205                info!(
206                    bead_id = BEAD_ID,
207                    pgno = target_pgno,
208                    symbols_used,
209                    corrupt_in_group = corrupt_count,
210                    "page repair VERIFIED — BLAKE3 witness confirmed"
211                );
212                RepairOutcome::Repaired {
213                    pgno: target_pgno,
214                    repaired_data,
215                    witness,
216                }
217            } else {
218                warn!(
219                    bead_id = BEAD_ID,
220                    pgno = target_pgno,
221                    "page repair produced data but BLAKE3 verification FAILED"
222                );
223                RepairOutcome::Unrecoverable {
224                    pgno: target_pgno,
225                    witness: Some(witness),
226                    detail: format!("page {target_pgno}: repaired data failed BLAKE3 verification"),
227                }
228            }
229        }
230        Err(err) => {
231            error!(
232                bead_id = BEAD_ID,
233                pgno = target_pgno,
234                corrupt_in_group = corrupt_count,
235                error = %err,
236                "page repair FAILED — insufficient symbols"
237            );
238            RepairOutcome::Unrecoverable {
239                pgno: target_pgno,
240                witness: Some(RepairWitness {
241                    pgno: target_pgno,
242                    corrupted_hash: actual_hash,
243                    repaired_hash: [0u8; 32],
244                    expected_hash: *expected_blake3,
245                    verified: false,
246                    symbols_used: 0,
247                    corrupt_pages_in_group: corrupt_count,
248                }),
249                detail: format!("{err}"),
250            }
251        }
252    }
253}
254
255/// Batch repair: detect and repair multiple pages in a group.
256///
257/// Returns a `RepairOutcome` for every page in the target list.  Pages
258/// that are intact are returned as `Intact`; corrupted pages are repaired
259/// (or reported as unrecoverable) individually.
260pub fn detect_and_repair_pages(
261    target_pgnos: &[u32],
262    group_meta: &DbFecGroupMeta,
263    all_page_data: &dyn Fn(u32) -> Vec<u8>,
264    expected_blake3s: &dyn Fn(u32) -> [u8; 32],
265    repair_symbols: &[(u32, Vec<u8>)],
266) -> Vec<RepairOutcome> {
267    target_pgnos
268        .iter()
269        .map(|&pgno| {
270            let data = all_page_data(pgno);
271            let expected = expected_blake3s(pgno);
272            detect_and_repair_page(
273                pgno,
274                &data,
275                &expected,
276                group_meta,
277                all_page_data,
278                repair_symbols,
279            )
280        })
281        .collect()
282}
283
284// ---------------------------------------------------------------------------
285// Tests
286// ---------------------------------------------------------------------------
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291    use crate::db_fec::{
292        DbFecGroupMeta, compute_db_gen_digest, compute_raptorq_repair_symbols, page_xxh3_128,
293    };
294
295    /// Build test pages with deterministic content.
296    #[allow(clippy::cast_possible_truncation)]
297    fn make_test_pages(k: u32, page_size: usize) -> Vec<Vec<u8>> {
298        (0..k)
299            .map(|i| {
300                let mut data = vec![0u8; page_size];
301                for (j, b) in data.iter_mut().enumerate() {
302                    *b = ((i as usize * 41 + j * 7) & 0xFF) as u8;
303                }
304                data
305            })
306            .collect()
307    }
308
309    /// Build group metadata and repair symbols for test pages.
310    fn make_test_group(
311        pages: &[Vec<u8>],
312        page_size: u32,
313        r_repair: u32,
314        start_pgno: u32,
315    ) -> (DbFecGroupMeta, Vec<(u32, Vec<u8>)>) {
316        let k = u32::try_from(pages.len()).expect("k fits u32");
317        let hashes: Vec<[u8; 16]> = pages.iter().map(|d| page_xxh3_128(d)).collect();
318        let digest = compute_db_gen_digest(1, k + 1, 0, 1);
319        let meta = DbFecGroupMeta::new(page_size, start_pgno, k, r_repair, hashes, digest);
320
321        let slices: Vec<&[u8]> = pages.iter().map(Vec::as_slice).collect();
322        let repair_data =
323            compute_raptorq_repair_symbols(&meta, &slices, page_size as usize).expect("encode");
324        let repair_symbols: Vec<(u32, Vec<u8>)> = repair_data
325            .into_iter()
326            .enumerate()
327            .map(|(i, d)| (k + u32::try_from(i).expect("i fits u32"), d))
328            .collect();
329
330        (meta, repair_symbols)
331    }
332
333    // -- BLAKE3 checksum tests --
334
335    #[test]
336    fn test_blake3_checksum_deterministic() {
337        let data = vec![0xAB_u8; 512];
338        let h1 = blake3_page_checksum(&data);
339        let h2 = blake3_page_checksum(&data);
340        assert_eq!(h1, h2);
341    }
342
343    #[test]
344    fn test_blake3_checksum_sensitive_to_content() {
345        let data_a = vec![0x01_u8; 512];
346        let data_b = vec![0x02_u8; 512];
347        assert_ne!(blake3_page_checksum(&data_a), blake3_page_checksum(&data_b));
348    }
349
350    #[test]
351    fn test_blake3_verify_page() {
352        let data = vec![0x42_u8; 1024];
353        let hash = blake3_page_checksum(&data);
354        assert!(verify_page_blake3(&data, &hash));
355        let mut corrupted = data;
356        corrupted[0] ^= 0xFF;
357        assert!(!verify_page_blake3(&corrupted, &hash));
358    }
359
360    // -- Intact detection --
361
362    #[test]
363    fn test_detect_intact_page() {
364        let pages = make_test_pages(4, 128);
365        let (meta, repair_symbols) = make_test_group(&pages, 128, 4, 2);
366        let expected_hash = blake3_page_checksum(&pages[0]);
367
368        let read_fn = |pgno: u32| -> Vec<u8> { pages[(pgno - 2) as usize].clone() };
369
370        let outcome = detect_and_repair_page(
371            2,
372            &pages[0],
373            &expected_hash,
374            &meta,
375            &read_fn,
376            &repair_symbols,
377        );
378
379        assert!(matches!(outcome, RepairOutcome::Intact { pgno: 2, .. }));
380    }
381
382    // -- Single-page repair --
383
384    #[test]
385    fn test_detect_and_repair_single_corruption() {
386        let pages = make_test_pages(4, 128);
387        let (meta, repair_symbols) = make_test_group(&pages, 128, 4, 2);
388        let expected_hash = blake3_page_checksum(&pages[1]);
389
390        let corrupted = vec![0xFF_u8; 128];
391        let read_fn = |pgno: u32| -> Vec<u8> {
392            if pgno == 3 {
393                corrupted.clone()
394            } else {
395                pages[(pgno - 2) as usize].clone()
396            }
397        };
398
399        let outcome = detect_and_repair_page(
400            3,
401            &corrupted,
402            &expected_hash,
403            &meta,
404            &read_fn,
405            &repair_symbols,
406        );
407
408        match outcome {
409            RepairOutcome::Repaired {
410                pgno,
411                repaired_data,
412                witness,
413            } => {
414                assert_eq!(pgno, 3);
415                assert_eq!(repaired_data, pages[1]);
416                assert!(witness.verified);
417                assert_eq!(witness.corrupted_hash, blake3_page_checksum(&corrupted));
418                assert_eq!(witness.repaired_hash, expected_hash);
419                assert_eq!(witness.expected_hash, expected_hash);
420                assert!(witness.corrupt_pages_in_group >= 1);
421            }
422            other => panic!("expected Repaired, got {other:?}"),
423        }
424    }
425
426    // -- Multi-page corruption --
427
428    #[test]
429    fn test_detect_and_repair_multi_corruption() {
430        let pages = make_test_pages(8, 128);
431        let (meta, repair_symbols) = make_test_group(&pages, 128, 4, 2);
432
433        let corrupted = vec![0xCC_u8; 128];
434        let corrupt_pgnos = [2_u32, 3, 4]; // 3 corrupted pages (within R=4 budget)
435
436        let read_fn = |pgno: u32| -> Vec<u8> {
437            if corrupt_pgnos.contains(&pgno) {
438                corrupted.clone()
439            } else {
440                pages[(pgno - 2) as usize].clone()
441            }
442        };
443
444        for &target in &corrupt_pgnos {
445            let idx = (target - 2) as usize;
446            let expected_hash = blake3_page_checksum(&pages[idx]);
447            let outcome = detect_and_repair_page(
448                target,
449                &corrupted,
450                &expected_hash,
451                &meta,
452                &read_fn,
453                &repair_symbols,
454            );
455
456            match outcome {
457                RepairOutcome::Repaired {
458                    repaired_data,
459                    witness,
460                    ..
461                } => {
462                    assert_eq!(repaired_data, pages[idx]);
463                    assert!(witness.verified);
464                    assert!(witness.corrupt_pages_in_group >= 3);
465                }
466                other => panic!("expected Repaired for page {target}, got {other:?}"),
467            }
468        }
469    }
470
471    // -- Contiguous range corruption --
472
473    #[test]
474    fn test_detect_and_repair_contiguous_range() {
475        let pages = make_test_pages(8, 64);
476        // R=8 gives ample overhead for 4 corruptions (RaptorQ needs symbols
477        // beyond the exact boundary due to potential linear dependence in the
478        // binary LT encoding pattern).
479        let (meta, repair_symbols) = make_test_group(&pages, 64, 8, 2);
480
481        let corrupted = vec![0xBB_u8; 64];
482        // Corrupt pages 5,6,7,8 (contiguous range, indices 3..7)
483        let corrupt_pgnos = [5_u32, 6, 7, 8];
484
485        let read_fn = |pgno: u32| -> Vec<u8> {
486            if corrupt_pgnos.contains(&pgno) {
487                corrupted.clone()
488            } else {
489                pages[(pgno - 2) as usize].clone()
490            }
491        };
492
493        for &target in &corrupt_pgnos {
494            let idx = (target - 2) as usize;
495            let expected_hash = blake3_page_checksum(&pages[idx]);
496            let outcome = detect_and_repair_page(
497                target,
498                &corrupted,
499                &expected_hash,
500                &meta,
501                &read_fn,
502                &repair_symbols,
503            );
504
505            match outcome {
506                RepairOutcome::Repaired { witness, .. } => {
507                    assert!(witness.verified, "page {target} should be repaired");
508                }
509                other => panic!("expected Repaired for page {target}, got {other:?}"),
510            }
511        }
512    }
513
514    // -- Graceful degradation --
515
516    #[test]
517    fn test_graceful_degradation_beyond_repair_capacity() {
518        let pages = make_test_pages(8, 64);
519        let (meta, repair_symbols) = make_test_group(&pages, 64, 4, 2);
520
521        let corrupted = vec![0xEE_u8; 64];
522        // Corrupt 5 pages (exceeds R=4 budget)
523        let corrupt_pgnos = [2_u32, 3, 4, 5, 6];
524
525        let read_fn = |pgno: u32| -> Vec<u8> {
526            if corrupt_pgnos.contains(&pgno) {
527                corrupted.clone()
528            } else {
529                pages[(pgno - 2) as usize].clone()
530            }
531        };
532
533        let expected_hash = blake3_page_checksum(&pages[0]);
534        let outcome = detect_and_repair_page(
535            2,
536            &corrupted,
537            &expected_hash,
538            &meta,
539            &read_fn,
540            &repair_symbols,
541        );
542
543        match outcome {
544            RepairOutcome::Unrecoverable {
545                pgno,
546                witness,
547                detail,
548            } => {
549                assert_eq!(pgno, 2);
550                assert!(witness.is_some());
551                assert!(!detail.is_empty());
552                let w = witness.unwrap();
553                assert!(!w.verified);
554                assert!(w.corrupt_pages_in_group >= 5);
555            }
556            other => panic!("expected Unrecoverable, got {other:?}"),
557        }
558    }
559
560    // -- Witness proof completeness --
561
562    #[test]
563    fn test_witness_proof_completeness() {
564        let pages = make_test_pages(4, 128);
565        let (meta, repair_symbols) = make_test_group(&pages, 128, 4, 2);
566
567        let corrupted = vec![0xAA_u8; 128];
568        let expected_hash = blake3_page_checksum(&pages[2]);
569
570        let read_fn = |pgno: u32| -> Vec<u8> {
571            if pgno == 4 {
572                corrupted.clone()
573            } else {
574                pages[(pgno - 2) as usize].clone()
575            }
576        };
577
578        let outcome = detect_and_repair_page(
579            4,
580            &corrupted,
581            &expected_hash,
582            &meta,
583            &read_fn,
584            &repair_symbols,
585        );
586
587        match outcome {
588            RepairOutcome::Repaired { witness, .. } => {
589                // The witness triple must be complete.
590                assert_ne!(witness.corrupted_hash, [0u8; 32]);
591                assert_ne!(witness.repaired_hash, [0u8; 32]);
592                assert_ne!(witness.expected_hash, [0u8; 32]);
593                assert_ne!(witness.corrupted_hash, witness.repaired_hash);
594                assert_eq!(witness.repaired_hash, witness.expected_hash);
595                assert!(witness.symbols_used > 0);
596                assert!(witness.is_success());
597            }
598            other => panic!("expected Repaired, got {other:?}"),
599        }
600    }
601
602    // -- Corruption percentage boundary tests --
603
604    /// Test repair at varying corruption levels to find the success/failure boundary.
605    #[test]
606    fn test_corruption_boundary_1_percent() {
607        corruption_boundary_test(64, 4, 1); // 1% of 64 = ~1 page
608    }
609
610    #[test]
611    fn test_corruption_boundary_5_percent() {
612        // 5% of 64 = ~4 pages; R=8 gives 2x overhead for the binary LT decoder.
613        corruption_boundary_test(64, 8, 5);
614    }
615
616    #[test]
617    fn test_corruption_boundary_10_percent() {
618        // 10% of 64 = ~7 pages; R=16 gives ~2x overhead.
619        corruption_boundary_test(64, 16, 10);
620    }
621
622    #[test]
623    fn test_corruption_boundary_20_percent() {
624        // 20% of 64 = ~13 pages; R=32 gives ~2.5x overhead.
625        corruption_boundary_test(64, 32, 20);
626    }
627
628    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
629    fn corruption_boundary_test(k: u32, r: u32, corruption_pct: u32) {
630        let page_size = 64_usize;
631        let pages = make_test_pages(k, page_size);
632        let (meta, repair_symbols) = make_test_group(&pages, page_size as u32, r, 2);
633
634        let num_corrupt = (f64::from(k) * f64::from(corruption_pct) / 100.0).ceil() as u32;
635        let num_corrupt = num_corrupt.max(1).min(k);
636
637        let corrupted = vec![0xDD_u8; page_size];
638        let corrupt_pgnos: Vec<u32> = (2..2 + num_corrupt).collect();
639
640        let read_fn = |pgno: u32| -> Vec<u8> {
641            if corrupt_pgnos.contains(&pgno) {
642                corrupted.clone()
643            } else {
644                pages[(pgno - 2) as usize].clone()
645            }
646        };
647
648        let target = corrupt_pgnos[0];
649        let idx = (target - 2) as usize;
650        let expected_hash = blake3_page_checksum(&pages[idx]);
651
652        let outcome = detect_and_repair_page(
653            target,
654            &corrupted,
655            &expected_hash,
656            &meta,
657            &read_fn,
658            &repair_symbols,
659        );
660
661        if num_corrupt <= r {
662            // Should succeed.
663            match outcome {
664                RepairOutcome::Repaired {
665                    repaired_data,
666                    witness,
667                    ..
668                } => {
669                    assert_eq!(repaired_data, pages[idx]);
670                    assert!(
671                        witness.verified,
672                        "repair should succeed: {num_corrupt} corrupt <= R={r}"
673                    );
674                }
675                other => {
676                    panic!("expected Repaired for {num_corrupt} corrupt (R={r}), got {other:?}")
677                }
678            }
679        } else {
680            // Should fail gracefully.
681            assert!(
682                matches!(outcome, RepairOutcome::Unrecoverable { .. }),
683                "expected Unrecoverable for {num_corrupt} corrupt > R={r}, got {outcome:?}"
684            );
685        }
686    }
687
688    // -- Batch repair --
689
690    #[test]
691    fn test_batch_detect_and_repair() {
692        let pages = make_test_pages(4, 128);
693        let (meta, repair_symbols) = make_test_group(&pages, 128, 4, 2);
694
695        let corrupted = vec![0xFF_u8; 128];
696        let read_fn = |pgno: u32| -> Vec<u8> {
697            if pgno == 3 {
698                corrupted.clone()
699            } else {
700                pages[(pgno - 2) as usize].clone()
701            }
702        };
703
704        let blake3_fn = |pgno: u32| -> [u8; 32] {
705            let idx = (pgno - 2) as usize;
706            blake3_page_checksum(&pages[idx])
707        };
708
709        let outcomes =
710            detect_and_repair_pages(&[2, 3, 4, 5], &meta, &read_fn, &blake3_fn, &repair_symbols);
711
712        assert_eq!(outcomes.len(), 4);
713        assert!(matches!(outcomes[0], RepairOutcome::Intact { .. }));
714        assert!(matches!(outcomes[1], RepairOutcome::Repaired { .. }));
715        assert!(matches!(outcomes[2], RepairOutcome::Intact { .. }));
716        assert!(matches!(outcomes[3], RepairOutcome::Intact { .. }));
717    }
718
719    // -- Compliance gate --
720
721    #[test]
722    fn test_bd_n0g4q_3_compliance_gate() {
723        assert_eq!(BEAD_ID, "bd-n0g4q.3");
724        // Verify key types exist and are constructible.
725        let witness = RepairWitness {
726            pgno: 1,
727            corrupted_hash: [0u8; 32],
728            repaired_hash: [1u8; 32],
729            expected_hash: [1u8; 32],
730            verified: true,
731            symbols_used: 5,
732            corrupt_pages_in_group: 1,
733        };
734        assert!(witness.is_success());
735    }
736}