1use crate::{
2 constants::WASM_PAGE_SIZE_BYTES,
3 declaration::AllocationDeclaration,
4 ledger::{AllocationLedger, AllocationRecord, GenerationRecord},
5 physical::CommitStoreDiagnostic,
6 policy::PolicyIdentity,
7 registry::SealedDeclarationFingerprint,
8 slot::{AllocationSlotDescriptor, MemoryManagerAuthorityRecord, MemoryManagerRangeAuthority},
9};
10use serde::{Deserialize, Serialize};
11use std::collections::BTreeMap;
12
13#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
18#[serde(deny_unknown_fields)]
19pub struct DiagnosticExport {
20 pub current_generation: u64,
22 pub ledger_anchor: AllocationSlotDescriptor,
24 pub records: Vec<DiagnosticRecord>,
26 pub generations: Vec<DiagnosticGeneration>,
28 #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
30 pub commit_recovery: Option<CommitStoreDiagnostic>,
31}
32
33#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
41#[serde(deny_unknown_fields)]
42pub struct DiagnosticRuntimeBinding {
43 pub policy_identity: PolicyIdentity,
45 pub declaration_fingerprint: SealedDeclarationFingerprint,
47}
48
49impl DiagnosticRuntimeBinding {
50 #[must_use]
52 pub const fn new(
53 policy_identity: PolicyIdentity,
54 declaration_fingerprint: SealedDeclarationFingerprint,
55 ) -> Self {
56 Self {
57 policy_identity,
58 declaration_fingerprint,
59 }
60 }
61}
62
63#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
75#[serde(deny_unknown_fields)]
76pub struct MemoryRuntimeDoctorReport {
77 pub bootstrapped: bool,
79 pub tested_policy_identity: Result<PolicyIdentity, DiagnosticFailure>,
81 pub tested_declaration_fingerprint: SealedDeclarationFingerprint,
83 #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
85 pub established_bootstrap_binding: Option<DiagnosticRuntimeBinding>,
86 pub bootstrap_binding: DiagnosticCheck,
89 pub ledger_anchor: AllocationSlotDescriptor,
91 pub stable_cell: DiagnosticStableCell,
93 #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
95 pub commit_recovery: Option<CommitStoreDiagnostic>,
96 #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
98 pub ledger: Option<DiagnosticExport>,
99 pub registered_declarations: Vec<DiagnosticDeclaration>,
101 pub range_authority: DiagnosticRangeAuthority,
104 pub validation: DiagnosticCheck,
106}
107
108#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
115#[serde(deny_unknown_fields)]
116pub struct DiagnosticDeclaration {
117 pub authority: String,
119 pub declaration: AllocationDeclaration,
121}
122
123impl DiagnosticDeclaration {
124 #[must_use]
126 pub fn new(authority: impl Into<String>, declaration: AllocationDeclaration) -> Self {
127 Self {
128 authority: authority.into(),
129 declaration,
130 }
131 }
132}
133
134#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
141pub enum DiagnosticCode {
142 #[serde(rename = "eager_init")]
144 EagerInit,
145 #[serde(rename = "declaration_registry")]
147 DeclarationRegistry,
148 #[serde(rename = "range_registry")]
150 RangeRegistry,
151 #[serde(rename = "range_authority")]
153 RangeAuthority,
154 #[serde(rename = "declaration_snapshot")]
156 DeclarationSnapshot,
157 #[serde(rename = "stable_cell")]
159 StableCell,
160 #[serde(rename = "unsupported_format")]
162 UnsupportedFormat,
163 #[serde(rename = "ledger_recovery")]
165 LedgerRecovery,
166 #[serde(rename = "genesis_ledger")]
168 GenesisLedger,
169 #[serde(rename = "allocation_validation")]
171 AllocationValidation,
172 #[serde(rename = "policy_identity")]
174 PolicyIdentity,
175 #[serde(rename = "runtime_binding")]
177 RuntimeBinding,
178 #[serde(rename = "memory_size")]
180 MemorySize,
181}
182
183#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
190#[serde(deny_unknown_fields)]
191pub struct DiagnosticFailure {
192 pub code: DiagnosticCode,
194 pub message: String,
196}
197
198impl DiagnosticFailure {
199 #[must_use]
201 pub fn new(code: DiagnosticCode, message: impl Into<String>) -> Self {
202 Self {
203 code,
204 message: message.into(),
205 }
206 }
207}
208
209#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
216#[serde(deny_unknown_fields)]
217pub struct DiagnosticRangeAuthority {
218 pub registered_records: Vec<MemoryManagerAuthorityRecord>,
220 pub effective_authority: Result<MemoryManagerRangeAuthority, DiagnosticFailure>,
222}
223
224impl DiagnosticRangeAuthority {
225 #[must_use]
227 pub const fn new(
228 registered_records: Vec<MemoryManagerAuthorityRecord>,
229 effective_authority: Result<MemoryManagerRangeAuthority, DiagnosticFailure>,
230 ) -> Self {
231 Self {
232 registered_records,
233 effective_authority,
234 }
235 }
236}
237
238#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
245#[serde(deny_unknown_fields)]
246pub struct DiagnosticStableCell {
247 pub status: DiagnosticStableCellStatus,
249 pub memory_size: DiagnosticMemorySize,
251}
252
253impl DiagnosticStableCell {
254 #[must_use]
256 pub const fn new(
257 status: DiagnosticStableCellStatus,
258 memory_size: DiagnosticMemorySize,
259 ) -> Self {
260 Self {
261 status,
262 memory_size,
263 }
264 }
265}
266
267#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
274#[serde(deny_unknown_fields)]
275pub enum DiagnosticStableCellStatus {
276 Empty,
278 Readable,
280 Corrupt {
283 failure: DiagnosticFailure,
285 },
286}
287
288#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
295#[serde(deny_unknown_fields)]
296pub enum DiagnosticCheck {
297 NotRun {
299 code: DiagnosticCode,
301 message: String,
303 },
304 Passed,
306 Failed {
308 code: DiagnosticCode,
310 message: String,
312 },
313}
314
315impl DiagnosticCheck {
316 #[must_use]
318 pub const fn passed() -> Self {
319 Self::Passed
320 }
321
322 #[must_use]
324 pub fn failed(code: DiagnosticCode, message: impl Into<String>) -> Self {
325 Self::Failed {
326 code,
327 message: message.into(),
328 }
329 }
330
331 #[must_use]
333 pub fn not_run(code: DiagnosticCode, message: impl Into<String>) -> Self {
334 Self::NotRun {
335 code,
336 message: message.into(),
337 }
338 }
339}
340
341impl DiagnosticExport {
342 #[must_use]
344 pub fn from_ledger(ledger: &AllocationLedger, ledger_anchor: AllocationSlotDescriptor) -> Self {
345 Self::from_ledger_with_commit_recovery(ledger, ledger_anchor, None)
346 }
347
348 #[must_use]
350 pub fn from_ledger_with_commit_recovery(
351 ledger: &AllocationLedger,
352 ledger_anchor: AllocationSlotDescriptor,
353 commit_recovery: Option<CommitStoreDiagnostic>,
354 ) -> Self {
355 Self::from_ledger_with_commit_recovery_and_memory_sizes(
356 ledger,
357 ledger_anchor,
358 commit_recovery,
359 std::iter::empty(),
360 )
361 }
362
363 #[must_use]
365 pub fn from_ledger_with_memory_sizes(
366 ledger: &AllocationLedger,
367 ledger_anchor: AllocationSlotDescriptor,
368 memory_sizes: impl IntoIterator<Item = (AllocationSlotDescriptor, DiagnosticMemorySize)>,
369 ) -> Self {
370 Self::from_ledger_with_commit_recovery_and_memory_sizes(
371 ledger,
372 ledger_anchor,
373 None,
374 memory_sizes,
375 )
376 }
377
378 #[must_use]
380 pub fn from_ledger_with_commit_recovery_and_memory_sizes(
381 ledger: &AllocationLedger,
382 ledger_anchor: AllocationSlotDescriptor,
383 commit_recovery: Option<CommitStoreDiagnostic>,
384 memory_sizes: impl IntoIterator<Item = (AllocationSlotDescriptor, DiagnosticMemorySize)>,
385 ) -> Self {
386 Self::from_ledger_with_commit_recovery_and_memory_size_outcomes(
387 ledger,
388 ledger_anchor,
389 commit_recovery,
390 memory_sizes
391 .into_iter()
392 .map(|(slot, size)| (slot, DiagnosticMemorySizeOutcome::Measured(size))),
393 )
394 }
395
396 pub(crate) fn from_ledger_with_commit_recovery_and_memory_size_outcomes(
397 ledger: &AllocationLedger,
398 ledger_anchor: AllocationSlotDescriptor,
399 commit_recovery: Option<CommitStoreDiagnostic>,
400 memory_sizes: impl IntoIterator<Item = (AllocationSlotDescriptor, DiagnosticMemorySizeOutcome)>,
401 ) -> Self {
402 let memory_sizes: BTreeMap<_, _> = memory_sizes.into_iter().collect();
403 Self {
404 current_generation: ledger.current_generation,
405 ledger_anchor,
406 records: ledger
407 .allocation_history()
408 .records()
409 .iter()
410 .cloned()
411 .map(|allocation| {
412 let memory_size = memory_sizes.get(allocation.slot()).cloned();
413 DiagnosticRecord {
414 allocation,
415 memory_size,
416 }
417 })
418 .collect(),
419 generations: ledger
420 .allocation_history()
421 .generations()
422 .iter()
423 .cloned()
424 .map(|generation| DiagnosticGeneration { generation })
425 .collect(),
426 commit_recovery,
427 }
428 }
429}
430
431#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
436#[serde(deny_unknown_fields)]
437pub struct DiagnosticRecord {
438 pub allocation: AllocationRecord,
440 #[serde(skip_serializing_if = "Option::is_none")]
445 pub memory_size: Option<DiagnosticMemorySizeOutcome>,
446}
447
448#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
455pub enum DiagnosticMemorySizeOutcome {
456 Measured(DiagnosticMemorySize),
458 Failed(DiagnosticFailure),
460}
461
462#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
469#[serde(deny_unknown_fields)]
470pub struct DiagnosticMemorySize {
471 pub wasm_pages: u64,
473 pub bytes: u64,
475}
476
477impl DiagnosticMemorySize {
478 #[must_use]
480 pub const fn from_wasm_pages(wasm_pages: u64) -> Self {
481 Self {
482 wasm_pages,
483 bytes: wasm_pages.saturating_mul(WASM_PAGE_SIZE_BYTES),
484 }
485 }
486}
487
488#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
493#[serde(deny_unknown_fields)]
494pub struct DiagnosticGeneration {
495 pub generation: GenerationRecord,
497}
498
499#[cfg(test)]
500mod tests {
501 use super::*;
502 use crate::{
503 declaration::AllocationDeclaration,
504 ledger::{AllocationHistory, AllocationRecord},
505 physical::{CommitRecoveryError, CommitSlotDiagnostic, CommitStoreDiagnostic},
506 schema::SchemaMetadata,
507 };
508
509 #[test]
510 fn diagnostic_export_copies_ledger_records() {
511 let declaration = AllocationDeclaration::new(
512 "app.users.v1",
513 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
514 None,
515 SchemaMetadata::default(),
516 )
517 .expect("declaration");
518 let ledger = AllocationLedger {
519 current_generation: 3,
520 allocation_history: AllocationHistory::from_parts(
521 vec![AllocationRecord::active(3, declaration).expect("valid schema metadata")],
522 vec![GenerationRecord {
523 generation: 3,
524 parent_generation: 2,
525 runtime_fingerprint: Some("wasm:abc123".to_string()),
526 declaration_count: 1,
527 committed_at: None,
528 }],
529 ),
530 };
531
532 let export = DiagnosticExport::from_ledger(
533 &ledger,
534 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
535 );
536
537 assert_eq!(export.current_generation, 3);
538 assert_eq!(export.records.len(), 1);
539 assert_eq!(export.records[0].memory_size, None);
540 assert_eq!(export.generations.len(), 1);
541 assert_eq!(
542 export.ledger_anchor,
543 AllocationSlotDescriptor::memory_manager(0).expect("usable slot")
544 );
545 assert_eq!(export.commit_recovery, None);
546 }
547
548 #[test]
549 fn diagnostic_export_rejects_unknown_top_level_fields() {
550 use crate::test_cbor::Value;
551
552 let export = DiagnosticExport {
553 current_generation: 0,
554 ledger_anchor: AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
555 records: Vec::new(),
556 generations: Vec::new(),
557 commit_recovery: None,
558 };
559 let Value::Map(mut map) = crate::test_cbor::to_value(export).expect("diagnostic value")
560 else {
561 panic!("diagnostic export encodes as a map");
562 };
563 crate::test_cbor::map_insert(
564 &mut map,
565 Value::Text("future_field".to_string()),
566 Value::Bool(true),
567 );
568 let bytes = crate::test_cbor::to_vec(&Value::Map(map)).expect("diagnostic bytes");
569
570 let err = crate::test_cbor::from_slice::<DiagnosticExport>(&bytes)
571 .expect_err("unknown diagnostic field must fail closed");
572
573 assert!(err.to_string().contains("future_field"));
574 }
575
576 #[test]
577 fn diagnostic_outcome_states_round_trip() {
578 let stable_cell = DiagnosticStableCell::new(
579 DiagnosticStableCellStatus::Corrupt {
580 failure: DiagnosticFailure::new(
581 DiagnosticCode::StableCell,
582 "bad stable-cell record",
583 ),
584 },
585 DiagnosticMemorySize::from_wasm_pages(1),
586 );
587 let range_authority = DiagnosticRangeAuthority::new(
588 Vec::new(),
589 Err(DiagnosticFailure::new(
590 DiagnosticCode::RangeAuthority,
591 "overlapping authority ranges",
592 )),
593 );
594 let check = DiagnosticCheck::failed(
595 DiagnosticCode::AllocationValidation,
596 "duplicate declaration",
597 );
598
599 for value in [DiagnosticCheck::passed(), check] {
600 let bytes = crate::test_cbor::to_vec(&value).expect("check bytes");
601 let decoded: DiagnosticCheck =
602 crate::test_cbor::from_slice(&bytes).expect("check round trip");
603 assert_eq!(decoded, value);
604 }
605
606 let bytes = crate::test_cbor::to_vec(&stable_cell).expect("stable-cell diagnostic bytes");
607 let decoded: DiagnosticStableCell =
608 crate::test_cbor::from_slice(&bytes).expect("stable-cell round trip");
609 assert_eq!(decoded, stable_cell);
610
611 let bytes = crate::test_cbor::to_vec(&range_authority).expect("range diagnostic bytes");
612 let decoded: DiagnosticRangeAuthority =
613 crate::test_cbor::from_slice(&bytes).expect("range round trip");
614 assert_eq!(decoded, range_authority);
615 }
616
617 #[test]
618 fn diagnostic_codes_have_stable_wire_names() {
619 let cases = [
620 (DiagnosticCode::EagerInit, "eager_init"),
621 (DiagnosticCode::DeclarationRegistry, "declaration_registry"),
622 (DiagnosticCode::RangeRegistry, "range_registry"),
623 (DiagnosticCode::RangeAuthority, "range_authority"),
624 (DiagnosticCode::DeclarationSnapshot, "declaration_snapshot"),
625 (DiagnosticCode::StableCell, "stable_cell"),
626 (DiagnosticCode::UnsupportedFormat, "unsupported_format"),
627 (DiagnosticCode::LedgerRecovery, "ledger_recovery"),
628 (DiagnosticCode::GenesisLedger, "genesis_ledger"),
629 (
630 DiagnosticCode::AllocationValidation,
631 "allocation_validation",
632 ),
633 (DiagnosticCode::PolicyIdentity, "policy_identity"),
634 (DiagnosticCode::RuntimeBinding, "runtime_binding"),
635 (DiagnosticCode::MemorySize, "memory_size"),
636 ];
637
638 for (code, expected) in cases {
639 assert_eq!(
640 crate::test_cbor::to_value(code).expect("diagnostic code value"),
641 crate::test_cbor::Value::Text(expected.to_string())
642 );
643 }
644 }
645
646 #[test]
647 fn diagnostic_export_can_include_commit_recovery_state() {
648 let ledger = AllocationLedger {
649 current_generation: 3,
650 allocation_history: AllocationHistory::default(),
651 };
652 let commit_recovery = CommitStoreDiagnostic {
653 slot0: CommitSlotDiagnostic::Valid { generation: 3 },
654 slot1: CommitSlotDiagnostic::Empty,
655 recovery: Ok(3),
656 };
657
658 let export = DiagnosticExport::from_ledger_with_commit_recovery(
659 &ledger,
660 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
661 Some(commit_recovery),
662 );
663
664 assert_eq!(export.commit_recovery, Some(commit_recovery));
665 }
666
667 #[test]
668 fn diagnostic_export_can_include_memory_sizes() {
669 let declaration = AllocationDeclaration::new(
670 "app.users.v1",
671 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
672 None,
673 SchemaMetadata::default(),
674 )
675 .expect("declaration");
676 let ledger = AllocationLedger {
677 current_generation: 3,
678 allocation_history: AllocationHistory::from_parts(
679 vec![AllocationRecord::active(3, declaration).expect("valid schema metadata")],
680 Vec::new(),
681 ),
682 };
683
684 let export = DiagnosticExport::from_ledger_with_memory_sizes(
685 &ledger,
686 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
687 [(
688 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
689 DiagnosticMemorySize::from_wasm_pages(2),
690 )],
691 );
692
693 assert_eq!(
694 export.records[0].memory_size,
695 Some(DiagnosticMemorySizeOutcome::Measured(
696 DiagnosticMemorySize {
697 wasm_pages: 2,
698 bytes: 131_072,
699 }
700 ))
701 );
702 }
703
704 #[test]
705 fn diagnostic_export_preserves_per_slot_size_successes_and_failures() {
706 let users = AllocationDeclaration::memory_manager("app.users.v1", 100, "users")
707 .expect("users declaration");
708 let orders = AllocationDeclaration::memory_manager("app.orders.v1", 101, "orders")
709 .expect("orders declaration");
710 let ledger = AllocationLedger {
711 current_generation: 3,
712 allocation_history: AllocationHistory::from_parts(
713 vec![
714 AllocationRecord::active(3, users).expect("users record"),
715 AllocationRecord::active(3, orders).expect("orders record"),
716 ],
717 Vec::new(),
718 ),
719 };
720 let size_failure =
721 DiagnosticFailure::new(DiagnosticCode::MemorySize, "slot could not be measured");
722
723 let export = DiagnosticExport::from_ledger_with_commit_recovery_and_memory_size_outcomes(
724 &ledger,
725 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
726 None,
727 [
728 (
729 AllocationSlotDescriptor::memory_manager(100).expect("users slot"),
730 DiagnosticMemorySizeOutcome::Measured(DiagnosticMemorySize::from_wasm_pages(2)),
731 ),
732 (
733 AllocationSlotDescriptor::memory_manager(101).expect("orders slot"),
734 DiagnosticMemorySizeOutcome::Failed(size_failure.clone()),
735 ),
736 ],
737 );
738
739 assert_eq!(
740 export.records[0].memory_size,
741 Some(DiagnosticMemorySizeOutcome::Measured(
742 DiagnosticMemorySize::from_wasm_pages(2)
743 ))
744 );
745 assert_eq!(
746 export.records[1].memory_size,
747 Some(DiagnosticMemorySizeOutcome::Failed(size_failure))
748 );
749 }
750
751 #[test]
752 fn diagnostic_export_can_report_recovery_failure() {
753 let ledger = AllocationLedger {
754 current_generation: 0,
755 allocation_history: AllocationHistory::default(),
756 };
757 let commit_recovery = CommitStoreDiagnostic {
758 slot0: CommitSlotDiagnostic::Empty,
759 slot1: CommitSlotDiagnostic::Empty,
760 recovery: Err(CommitRecoveryError::NoValidGeneration),
761 };
762
763 let export = DiagnosticExport::from_ledger_with_commit_recovery(
764 &ledger,
765 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
766 Some(commit_recovery),
767 );
768
769 assert_eq!(
770 export.commit_recovery.expect("commit recovery").recovery,
771 Err(CommitRecoveryError::NoValidGeneration)
772 );
773 }
774}