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 = "stable_cell")]
144 StableCell,
145 #[serde(rename = "unsupported_format")]
147 UnsupportedFormat,
148 #[serde(rename = "ledger_recovery")]
150 LedgerRecovery,
151 #[serde(rename = "allocation_validation")]
153 AllocationValidation,
154 #[serde(rename = "policy_identity")]
156 PolicyIdentity,
157 #[serde(rename = "runtime_binding")]
159 RuntimeBinding,
160 #[serde(rename = "memory_size")]
162 MemorySize,
163}
164
165#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
172#[serde(deny_unknown_fields)]
173pub struct DiagnosticFailure {
174 pub code: DiagnosticCode,
176 pub message: String,
178}
179
180impl DiagnosticFailure {
181 #[must_use]
183 pub fn new(code: DiagnosticCode, message: impl Into<String>) -> Self {
184 Self {
185 code,
186 message: message.into(),
187 }
188 }
189}
190
191#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
198#[serde(deny_unknown_fields)]
199pub struct DiagnosticRangeAuthority {
200 pub registered_records: Vec<MemoryManagerAuthorityRecord>,
202 pub effective_authority: MemoryManagerRangeAuthority,
204}
205
206impl DiagnosticRangeAuthority {
207 #[must_use]
209 pub const fn new(
210 registered_records: Vec<MemoryManagerAuthorityRecord>,
211 effective_authority: MemoryManagerRangeAuthority,
212 ) -> Self {
213 Self {
214 registered_records,
215 effective_authority,
216 }
217 }
218}
219
220#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
227#[serde(deny_unknown_fields)]
228pub struct DiagnosticStableCell {
229 pub status: DiagnosticStableCellStatus,
231 pub memory_size: DiagnosticMemorySize,
233}
234
235impl DiagnosticStableCell {
236 #[must_use]
238 pub const fn new(
239 status: DiagnosticStableCellStatus,
240 memory_size: DiagnosticMemorySize,
241 ) -> Self {
242 Self {
243 status,
244 memory_size,
245 }
246 }
247}
248
249#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
256#[serde(deny_unknown_fields)]
257pub enum DiagnosticStableCellStatus {
258 Empty,
260 Readable,
262 Corrupt {
265 failure: DiagnosticFailure,
267 },
268}
269
270#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
277#[serde(deny_unknown_fields)]
278pub enum DiagnosticCheck {
279 NotRun {
281 code: DiagnosticCode,
283 message: String,
285 },
286 Passed,
288 Failed {
290 code: DiagnosticCode,
292 message: String,
294 },
295}
296
297impl DiagnosticCheck {
298 #[must_use]
300 pub const fn passed() -> Self {
301 Self::Passed
302 }
303
304 #[must_use]
306 pub fn failed(code: DiagnosticCode, message: impl Into<String>) -> Self {
307 Self::Failed {
308 code,
309 message: message.into(),
310 }
311 }
312
313 #[must_use]
315 pub fn not_run(code: DiagnosticCode, message: impl Into<String>) -> Self {
316 Self::NotRun {
317 code,
318 message: message.into(),
319 }
320 }
321}
322
323impl DiagnosticExport {
324 #[must_use]
326 pub fn from_ledger(ledger: &AllocationLedger, ledger_anchor: AllocationSlotDescriptor) -> Self {
327 Self::from_ledger_with_commit_recovery(ledger, ledger_anchor, None)
328 }
329
330 #[must_use]
332 pub fn from_ledger_with_commit_recovery(
333 ledger: &AllocationLedger,
334 ledger_anchor: AllocationSlotDescriptor,
335 commit_recovery: Option<CommitStoreDiagnostic>,
336 ) -> Self {
337 Self::from_ledger_with_commit_recovery_and_memory_sizes(
338 ledger,
339 ledger_anchor,
340 commit_recovery,
341 std::iter::empty(),
342 )
343 }
344
345 #[must_use]
347 pub fn from_ledger_with_memory_sizes(
348 ledger: &AllocationLedger,
349 ledger_anchor: AllocationSlotDescriptor,
350 memory_sizes: impl IntoIterator<Item = (AllocationSlotDescriptor, DiagnosticMemorySize)>,
351 ) -> Self {
352 Self::from_ledger_with_commit_recovery_and_memory_sizes(
353 ledger,
354 ledger_anchor,
355 None,
356 memory_sizes,
357 )
358 }
359
360 #[must_use]
362 pub fn from_ledger_with_commit_recovery_and_memory_sizes(
363 ledger: &AllocationLedger,
364 ledger_anchor: AllocationSlotDescriptor,
365 commit_recovery: Option<CommitStoreDiagnostic>,
366 memory_sizes: impl IntoIterator<Item = (AllocationSlotDescriptor, DiagnosticMemorySize)>,
367 ) -> Self {
368 Self::from_ledger_with_commit_recovery_and_memory_size_outcomes(
369 ledger,
370 ledger_anchor,
371 commit_recovery,
372 memory_sizes
373 .into_iter()
374 .map(|(slot, size)| (slot, DiagnosticMemorySizeOutcome::Measured(size))),
375 )
376 }
377
378 pub(crate) fn from_ledger_with_commit_recovery_and_memory_size_outcomes(
379 ledger: &AllocationLedger,
380 ledger_anchor: AllocationSlotDescriptor,
381 commit_recovery: Option<CommitStoreDiagnostic>,
382 memory_sizes: impl IntoIterator<Item = (AllocationSlotDescriptor, DiagnosticMemorySizeOutcome)>,
383 ) -> Self {
384 let memory_sizes: BTreeMap<_, _> = memory_sizes.into_iter().collect();
385 Self {
386 current_generation: ledger.current_generation,
387 ledger_anchor,
388 records: ledger
389 .allocation_history()
390 .records()
391 .iter()
392 .cloned()
393 .map(|allocation| {
394 let memory_size = memory_sizes.get(allocation.slot()).cloned();
395 DiagnosticRecord {
396 allocation,
397 memory_size,
398 }
399 })
400 .collect(),
401 generations: ledger
402 .allocation_history()
403 .generations()
404 .iter()
405 .cloned()
406 .map(|generation| DiagnosticGeneration { generation })
407 .collect(),
408 commit_recovery,
409 }
410 }
411}
412
413#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
418#[serde(deny_unknown_fields)]
419pub struct DiagnosticRecord {
420 pub allocation: AllocationRecord,
422 #[serde(skip_serializing_if = "Option::is_none")]
427 pub memory_size: Option<DiagnosticMemorySizeOutcome>,
428}
429
430#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
441pub enum DiagnosticMemorySizeOutcome {
442 Measured(DiagnosticMemorySize),
444 Failed(DiagnosticFailure),
446}
447
448#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
455#[serde(deny_unknown_fields)]
456pub struct DiagnosticMemorySize {
457 pub wasm_pages: u64,
459 pub bytes: u64,
461}
462
463impl DiagnosticMemorySize {
464 #[must_use]
466 pub const fn from_wasm_pages(wasm_pages: u64) -> Self {
467 Self {
468 wasm_pages,
469 bytes: wasm_pages.saturating_mul(WASM_PAGE_SIZE_BYTES),
470 }
471 }
472}
473
474#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
479#[serde(deny_unknown_fields)]
480pub struct DiagnosticGeneration {
481 pub generation: GenerationRecord,
483}
484
485#[cfg(test)]
486mod tests {
487 use super::*;
488 use crate::{
489 declaration::AllocationDeclaration,
490 ledger::{AllocationHistory, AllocationRecord},
491 physical::{CommitRecoveryError, CommitSlotDiagnostic, CommitStoreDiagnostic},
492 schema::SchemaMetadata,
493 };
494
495 #[test]
496 fn diagnostic_export_copies_ledger_records() {
497 let declaration = AllocationDeclaration::new(
498 "app.users.v1",
499 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
500 None,
501 SchemaMetadata::default(),
502 )
503 .expect("declaration");
504 let ledger = AllocationLedger {
505 current_generation: 3,
506 allocation_history: AllocationHistory::from_parts(
507 vec![AllocationRecord::active(3, declaration)],
508 vec![GenerationRecord {
509 generation: 3,
510 parent_generation: 2,
511 runtime_fingerprint: Some("wasm:abc123".to_string()),
512 declaration_count: 1,
513 committed_at: None,
514 }],
515 ),
516 };
517
518 let export = DiagnosticExport::from_ledger(
519 &ledger,
520 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
521 );
522
523 assert_eq!(export.current_generation, 3);
524 assert_eq!(export.records.len(), 1);
525 assert_eq!(export.records[0].memory_size, None);
526 assert_eq!(export.generations.len(), 1);
527 assert_eq!(
528 export.ledger_anchor,
529 AllocationSlotDescriptor::memory_manager(0).expect("usable slot")
530 );
531 assert_eq!(export.commit_recovery, None);
532 }
533
534 #[test]
535 fn diagnostic_export_rejects_unknown_top_level_fields() {
536 use crate::test_cbor::Value;
537
538 let export = DiagnosticExport {
539 current_generation: 0,
540 ledger_anchor: AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
541 records: Vec::new(),
542 generations: Vec::new(),
543 commit_recovery: None,
544 };
545 let Value::Map(mut map) = crate::test_cbor::to_value(export).expect("diagnostic value")
546 else {
547 panic!("diagnostic export encodes as a map");
548 };
549 crate::test_cbor::map_insert(
550 &mut map,
551 Value::Text("future_field".to_string()),
552 Value::Bool(true),
553 );
554 let bytes = crate::test_cbor::to_vec(&Value::Map(map)).expect("diagnostic bytes");
555
556 let err = crate::test_cbor::from_slice::<DiagnosticExport>(&bytes)
557 .expect_err("unknown diagnostic field must fail closed");
558
559 assert!(err.to_string().contains("future_field"));
560 }
561
562 #[test]
563 fn diagnostic_outcome_states_round_trip() {
564 let stable_cell = DiagnosticStableCell::new(
565 DiagnosticStableCellStatus::Corrupt {
566 failure: DiagnosticFailure::new(
567 DiagnosticCode::StableCell,
568 "bad stable-cell record",
569 ),
570 },
571 DiagnosticMemorySize::from_wasm_pages(1),
572 );
573 let range_authority =
574 DiagnosticRangeAuthority::new(Vec::new(), MemoryManagerRangeAuthority::default());
575 let check = DiagnosticCheck::failed(
576 DiagnosticCode::AllocationValidation,
577 "duplicate declaration",
578 );
579
580 for value in [DiagnosticCheck::passed(), check] {
581 let bytes = crate::test_cbor::to_vec(&value).expect("check bytes");
582 let decoded: DiagnosticCheck =
583 crate::test_cbor::from_slice(&bytes).expect("check round trip");
584 assert_eq!(decoded, value);
585 }
586
587 let bytes = crate::test_cbor::to_vec(&stable_cell).expect("stable-cell diagnostic bytes");
588 let decoded: DiagnosticStableCell =
589 crate::test_cbor::from_slice(&bytes).expect("stable-cell round trip");
590 assert_eq!(decoded, stable_cell);
591
592 let bytes = crate::test_cbor::to_vec(&range_authority).expect("range diagnostic bytes");
593 let decoded: DiagnosticRangeAuthority =
594 crate::test_cbor::from_slice(&bytes).expect("range round trip");
595 assert_eq!(decoded, range_authority);
596 }
597
598 #[test]
599 fn diagnostic_codes_have_stable_wire_names() {
600 let cases = [
601 (DiagnosticCode::StableCell, "stable_cell"),
602 (DiagnosticCode::UnsupportedFormat, "unsupported_format"),
603 (DiagnosticCode::LedgerRecovery, "ledger_recovery"),
604 (
605 DiagnosticCode::AllocationValidation,
606 "allocation_validation",
607 ),
608 (DiagnosticCode::PolicyIdentity, "policy_identity"),
609 (DiagnosticCode::RuntimeBinding, "runtime_binding"),
610 (DiagnosticCode::MemorySize, "memory_size"),
611 ];
612
613 for (code, expected) in cases {
614 assert_eq!(
615 crate::test_cbor::to_value(code).expect("diagnostic code value"),
616 crate::test_cbor::Value::Text(expected.to_string())
617 );
618 }
619 }
620
621 #[test]
622 fn diagnostic_export_can_include_commit_recovery_state() {
623 let ledger = AllocationLedger {
624 current_generation: 3,
625 allocation_history: AllocationHistory::default(),
626 };
627 let commit_recovery = CommitStoreDiagnostic {
628 slot0: CommitSlotDiagnostic::Valid { generation: 3 },
629 slot1: CommitSlotDiagnostic::Empty,
630 recovery: Ok(3),
631 };
632
633 let export = DiagnosticExport::from_ledger_with_commit_recovery(
634 &ledger,
635 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
636 Some(commit_recovery),
637 );
638
639 assert_eq!(export.commit_recovery, Some(commit_recovery));
640 }
641
642 #[test]
643 fn diagnostic_export_can_include_memory_sizes() {
644 let declaration = AllocationDeclaration::new(
645 "app.users.v1",
646 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
647 None,
648 SchemaMetadata::default(),
649 )
650 .expect("declaration");
651 let ledger = AllocationLedger {
652 current_generation: 3,
653 allocation_history: AllocationHistory::from_parts(
654 vec![AllocationRecord::active(3, declaration)],
655 Vec::new(),
656 ),
657 };
658
659 let export = DiagnosticExport::from_ledger_with_memory_sizes(
660 &ledger,
661 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
662 [(
663 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
664 DiagnosticMemorySize::from_wasm_pages(2),
665 )],
666 );
667
668 assert_eq!(
669 export.records[0].memory_size,
670 Some(DiagnosticMemorySizeOutcome::Measured(
671 DiagnosticMemorySize {
672 wasm_pages: 2,
673 bytes: 131_072,
674 }
675 ))
676 );
677 }
678
679 #[test]
680 fn diagnostic_export_preserves_per_slot_size_successes_and_failures() {
681 let users = AllocationDeclaration::memory_manager("app.users.v1", 100, "users")
682 .expect("users declaration");
683 let orders = AllocationDeclaration::memory_manager("app.orders.v1", 101, "orders")
684 .expect("orders declaration");
685 let ledger = AllocationLedger {
686 current_generation: 3,
687 allocation_history: AllocationHistory::from_parts(
688 vec![
689 AllocationRecord::active(3, users),
690 AllocationRecord::active(3, orders),
691 ],
692 Vec::new(),
693 ),
694 };
695 let size_failure =
696 DiagnosticFailure::new(DiagnosticCode::MemorySize, "slot could not be measured");
697
698 let export = DiagnosticExport::from_ledger_with_commit_recovery_and_memory_size_outcomes(
699 &ledger,
700 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
701 None,
702 [
703 (
704 AllocationSlotDescriptor::memory_manager(100).expect("users slot"),
705 DiagnosticMemorySizeOutcome::Measured(DiagnosticMemorySize::from_wasm_pages(2)),
706 ),
707 (
708 AllocationSlotDescriptor::memory_manager(101).expect("orders slot"),
709 DiagnosticMemorySizeOutcome::Failed(size_failure.clone()),
710 ),
711 ],
712 );
713
714 assert_eq!(
715 export.records[0].memory_size,
716 Some(DiagnosticMemorySizeOutcome::Measured(
717 DiagnosticMemorySize::from_wasm_pages(2)
718 ))
719 );
720 assert_eq!(
721 export.records[1].memory_size,
722 Some(DiagnosticMemorySizeOutcome::Failed(size_failure))
723 );
724 }
725
726 #[test]
727 fn diagnostic_export_can_report_recovery_failure() {
728 let ledger = AllocationLedger {
729 current_generation: 0,
730 allocation_history: AllocationHistory::default(),
731 };
732 let commit_recovery = CommitStoreDiagnostic {
733 slot0: CommitSlotDiagnostic::Empty,
734 slot1: CommitSlotDiagnostic::Empty,
735 recovery: Err(CommitRecoveryError::NoValidGeneration),
736 };
737
738 let export = DiagnosticExport::from_ledger_with_commit_recovery(
739 &ledger,
740 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
741 Some(commit_recovery),
742 );
743
744 assert_eq!(
745 export.commit_recovery.expect("commit recovery").recovery,
746 Err(CommitRecoveryError::NoValidGeneration)
747 );
748 }
749}