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}
161
162#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
169#[serde(deny_unknown_fields)]
170pub struct DiagnosticFailure {
171 pub code: DiagnosticCode,
173 pub message: String,
175}
176
177impl DiagnosticFailure {
178 #[must_use]
180 pub fn new(code: DiagnosticCode, message: impl Into<String>) -> Self {
181 Self {
182 code,
183 message: message.into(),
184 }
185 }
186}
187
188#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
195#[serde(deny_unknown_fields)]
196pub struct DiagnosticRangeAuthority {
197 pub registered_records: Vec<MemoryManagerAuthorityRecord>,
199 pub effective_authority: MemoryManagerRangeAuthority,
201}
202
203impl DiagnosticRangeAuthority {
204 #[must_use]
206 pub const fn new(
207 registered_records: Vec<MemoryManagerAuthorityRecord>,
208 effective_authority: MemoryManagerRangeAuthority,
209 ) -> Self {
210 Self {
211 registered_records,
212 effective_authority,
213 }
214 }
215}
216
217#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
224#[serde(deny_unknown_fields)]
225pub struct DiagnosticStableCell {
226 pub status: DiagnosticStableCellStatus,
228 pub memory_size: DiagnosticMemorySize,
230}
231
232impl DiagnosticStableCell {
233 #[must_use]
235 pub const fn new(
236 status: DiagnosticStableCellStatus,
237 memory_size: DiagnosticMemorySize,
238 ) -> Self {
239 Self {
240 status,
241 memory_size,
242 }
243 }
244}
245
246#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
253#[serde(deny_unknown_fields)]
254pub enum DiagnosticStableCellStatus {
255 Empty,
257 Readable,
259 Corrupt {
262 failure: DiagnosticFailure,
264 },
265}
266
267#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
274#[serde(deny_unknown_fields)]
275pub enum DiagnosticCheck {
276 NotRun {
278 code: DiagnosticCode,
280 message: String,
282 },
283 Passed,
285 Failed {
287 code: DiagnosticCode,
289 message: String,
291 },
292}
293
294impl DiagnosticCheck {
295 #[must_use]
297 pub const fn passed() -> Self {
298 Self::Passed
299 }
300
301 #[must_use]
303 pub fn failed(code: DiagnosticCode, message: impl Into<String>) -> Self {
304 Self::Failed {
305 code,
306 message: message.into(),
307 }
308 }
309
310 #[must_use]
312 pub fn not_run(code: DiagnosticCode, message: impl Into<String>) -> Self {
313 Self::NotRun {
314 code,
315 message: message.into(),
316 }
317 }
318}
319
320impl DiagnosticExport {
321 #[must_use]
323 pub fn from_ledger(ledger: &AllocationLedger, ledger_anchor: AllocationSlotDescriptor) -> Self {
324 Self::from_ledger_with_commit_recovery(ledger, ledger_anchor, None)
325 }
326
327 #[must_use]
329 pub fn from_ledger_with_commit_recovery(
330 ledger: &AllocationLedger,
331 ledger_anchor: AllocationSlotDescriptor,
332 commit_recovery: Option<CommitStoreDiagnostic>,
333 ) -> Self {
334 Self::from_ledger_with_commit_recovery_and_memory_sizes(
335 ledger,
336 ledger_anchor,
337 commit_recovery,
338 std::iter::empty(),
339 )
340 }
341
342 #[must_use]
344 pub fn from_ledger_with_memory_sizes(
345 ledger: &AllocationLedger,
346 ledger_anchor: AllocationSlotDescriptor,
347 memory_sizes: impl IntoIterator<Item = (AllocationSlotDescriptor, DiagnosticMemorySize)>,
348 ) -> Self {
349 Self::from_ledger_with_commit_recovery_and_memory_sizes(
350 ledger,
351 ledger_anchor,
352 None,
353 memory_sizes,
354 )
355 }
356
357 #[must_use]
359 pub fn from_ledger_with_commit_recovery_and_memory_sizes(
360 ledger: &AllocationLedger,
361 ledger_anchor: AllocationSlotDescriptor,
362 commit_recovery: Option<CommitStoreDiagnostic>,
363 memory_sizes: impl IntoIterator<Item = (AllocationSlotDescriptor, DiagnosticMemorySize)>,
364 ) -> Self {
365 let memory_sizes: BTreeMap<_, _> = memory_sizes.into_iter().collect();
366 Self {
367 current_generation: ledger.current_generation,
368 ledger_anchor,
369 records: ledger
370 .allocation_history()
371 .records()
372 .iter()
373 .cloned()
374 .map(|allocation| {
375 let memory_size = memory_sizes.get(allocation.slot()).copied();
376 DiagnosticRecord {
377 allocation,
378 memory_size,
379 }
380 })
381 .collect(),
382 generations: ledger
383 .allocation_history()
384 .generations()
385 .iter()
386 .cloned()
387 .map(|generation| DiagnosticGeneration { generation })
388 .collect(),
389 commit_recovery,
390 }
391 }
392}
393
394#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
399#[serde(deny_unknown_fields)]
400pub struct DiagnosticRecord {
401 pub allocation: AllocationRecord,
403 #[serde(skip_serializing_if = "Option::is_none")]
408 pub memory_size: Option<DiagnosticMemorySize>,
409}
410
411#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
418#[serde(deny_unknown_fields)]
419pub struct DiagnosticMemorySize {
420 pub wasm_pages: u64,
422 pub bytes: u64,
424}
425
426impl DiagnosticMemorySize {
427 #[must_use]
429 pub const fn from_wasm_pages(wasm_pages: u64) -> Self {
430 Self {
431 wasm_pages,
432 bytes: wasm_pages.saturating_mul(WASM_PAGE_SIZE_BYTES),
433 }
434 }
435}
436
437#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
442#[serde(deny_unknown_fields)]
443pub struct DiagnosticGeneration {
444 pub generation: GenerationRecord,
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451 use crate::{
452 declaration::AllocationDeclaration,
453 ledger::{AllocationHistory, AllocationRecord},
454 physical::{CommitRecoveryError, CommitSlotDiagnostic, CommitStoreDiagnostic},
455 schema::SchemaMetadata,
456 };
457
458 #[test]
459 fn diagnostic_export_copies_ledger_records() {
460 let declaration = AllocationDeclaration::new(
461 "app.users.v1",
462 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
463 None,
464 SchemaMetadata::default(),
465 )
466 .expect("declaration");
467 let ledger = AllocationLedger {
468 current_generation: 3,
469 allocation_history: AllocationHistory::from_parts(
470 vec![AllocationRecord::active(3, declaration)],
471 vec![GenerationRecord {
472 generation: 3,
473 parent_generation: 2,
474 runtime_fingerprint: Some("wasm:abc123".to_string()),
475 declaration_count: 1,
476 committed_at: None,
477 }],
478 ),
479 };
480
481 let export = DiagnosticExport::from_ledger(
482 &ledger,
483 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
484 );
485
486 assert_eq!(export.current_generation, 3);
487 assert_eq!(export.records.len(), 1);
488 assert_eq!(export.records[0].memory_size, None);
489 assert_eq!(export.generations.len(), 1);
490 assert_eq!(
491 export.ledger_anchor,
492 AllocationSlotDescriptor::memory_manager(0).expect("usable slot")
493 );
494 assert_eq!(export.commit_recovery, None);
495 }
496
497 #[test]
498 fn diagnostic_export_rejects_unknown_top_level_fields() {
499 use crate::test_cbor::Value;
500
501 let export = DiagnosticExport {
502 current_generation: 0,
503 ledger_anchor: AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
504 records: Vec::new(),
505 generations: Vec::new(),
506 commit_recovery: None,
507 };
508 let Value::Map(mut map) = crate::test_cbor::to_value(export).expect("diagnostic value")
509 else {
510 panic!("diagnostic export encodes as a map");
511 };
512 crate::test_cbor::map_insert(
513 &mut map,
514 Value::Text("future_field".to_string()),
515 Value::Bool(true),
516 );
517 let bytes = crate::test_cbor::to_vec(&Value::Map(map)).expect("diagnostic bytes");
518
519 let err = crate::test_cbor::from_slice::<DiagnosticExport>(&bytes)
520 .expect_err("unknown diagnostic field must fail closed");
521
522 assert!(err.to_string().contains("future_field"));
523 }
524
525 #[test]
526 fn diagnostic_outcome_states_round_trip() {
527 let stable_cell = DiagnosticStableCell::new(
528 DiagnosticStableCellStatus::Corrupt {
529 failure: DiagnosticFailure::new(
530 DiagnosticCode::StableCell,
531 "bad stable-cell record",
532 ),
533 },
534 DiagnosticMemorySize::from_wasm_pages(1),
535 );
536 let range_authority =
537 DiagnosticRangeAuthority::new(Vec::new(), MemoryManagerRangeAuthority::default());
538 let check = DiagnosticCheck::failed(
539 DiagnosticCode::AllocationValidation,
540 "duplicate declaration",
541 );
542
543 for value in [DiagnosticCheck::passed(), check] {
544 let bytes = crate::test_cbor::to_vec(&value).expect("check bytes");
545 let decoded: DiagnosticCheck =
546 crate::test_cbor::from_slice(&bytes).expect("check round trip");
547 assert_eq!(decoded, value);
548 }
549
550 let bytes = crate::test_cbor::to_vec(&stable_cell).expect("stable-cell diagnostic bytes");
551 let decoded: DiagnosticStableCell =
552 crate::test_cbor::from_slice(&bytes).expect("stable-cell round trip");
553 assert_eq!(decoded, stable_cell);
554
555 let bytes = crate::test_cbor::to_vec(&range_authority).expect("range diagnostic bytes");
556 let decoded: DiagnosticRangeAuthority =
557 crate::test_cbor::from_slice(&bytes).expect("range round trip");
558 assert_eq!(decoded, range_authority);
559 }
560
561 #[test]
562 fn diagnostic_codes_have_stable_wire_names() {
563 let cases = [
564 (DiagnosticCode::StableCell, "stable_cell"),
565 (DiagnosticCode::UnsupportedFormat, "unsupported_format"),
566 (DiagnosticCode::LedgerRecovery, "ledger_recovery"),
567 (
568 DiagnosticCode::AllocationValidation,
569 "allocation_validation",
570 ),
571 (DiagnosticCode::PolicyIdentity, "policy_identity"),
572 (DiagnosticCode::RuntimeBinding, "runtime_binding"),
573 ];
574
575 for (code, expected) in cases {
576 assert_eq!(
577 crate::test_cbor::to_value(code).expect("diagnostic code value"),
578 crate::test_cbor::Value::Text(expected.to_string())
579 );
580 }
581 }
582
583 #[test]
584 fn diagnostic_export_can_include_commit_recovery_state() {
585 let ledger = AllocationLedger {
586 current_generation: 3,
587 allocation_history: AllocationHistory::default(),
588 };
589 let commit_recovery = CommitStoreDiagnostic {
590 slot0: CommitSlotDiagnostic::Valid { generation: 3 },
591 slot1: CommitSlotDiagnostic::Empty,
592 recovery: Ok(3),
593 };
594
595 let export = DiagnosticExport::from_ledger_with_commit_recovery(
596 &ledger,
597 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
598 Some(commit_recovery),
599 );
600
601 assert_eq!(export.commit_recovery, Some(commit_recovery));
602 }
603
604 #[test]
605 fn diagnostic_export_can_include_memory_sizes() {
606 let declaration = AllocationDeclaration::new(
607 "app.users.v1",
608 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
609 None,
610 SchemaMetadata::default(),
611 )
612 .expect("declaration");
613 let ledger = AllocationLedger {
614 current_generation: 3,
615 allocation_history: AllocationHistory::from_parts(
616 vec![AllocationRecord::active(3, declaration)],
617 Vec::new(),
618 ),
619 };
620
621 let export = DiagnosticExport::from_ledger_with_memory_sizes(
622 &ledger,
623 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
624 [(
625 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
626 DiagnosticMemorySize::from_wasm_pages(2),
627 )],
628 );
629
630 assert_eq!(
631 export.records[0].memory_size,
632 Some(DiagnosticMemorySize {
633 wasm_pages: 2,
634 bytes: 131_072,
635 })
636 );
637 let wire = serde_json::to_value(&export).expect("diagnostic JSON");
638 assert_eq!(
639 wire["records"][0]["memory_size"],
640 serde_json::json!({"wasm_pages": 2, "bytes": 131_072})
641 );
642 }
643
644 #[test]
645 fn diagnostic_export_can_report_recovery_failure() {
646 let ledger = AllocationLedger {
647 current_generation: 0,
648 allocation_history: AllocationHistory::default(),
649 };
650 let commit_recovery = CommitStoreDiagnostic {
651 slot0: CommitSlotDiagnostic::Empty,
652 slot1: CommitSlotDiagnostic::Empty,
653 recovery: Err(CommitRecoveryError::NoValidGeneration),
654 };
655
656 let export = DiagnosticExport::from_ledger_with_commit_recovery(
657 &ledger,
658 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
659 Some(commit_recovery),
660 );
661
662 assert_eq!(
663 export.commit_recovery.expect("commit recovery").recovery,
664 Err(CommitRecoveryError::NoValidGeneration)
665 );
666 }
667}