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};
11
12#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
17#[serde(deny_unknown_fields)]
18pub struct DiagnosticExport {
19 pub current_generation: u64,
21 pub ledger_anchor: AllocationSlotDescriptor,
23 pub records: Vec<DiagnosticRecord>,
25 pub generations: Vec<GenerationRecord>,
27 #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
29 pub commit_recovery: Option<CommitStoreDiagnostic>,
30}
31
32#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
40#[serde(deny_unknown_fields)]
41pub struct DiagnosticRuntimeBinding {
42 pub policy_identity: PolicyIdentity,
44 pub declaration_fingerprint: SealedDeclarationFingerprint,
46}
47
48impl DiagnosticRuntimeBinding {
49 #[must_use]
51 pub const fn new(
52 policy_identity: PolicyIdentity,
53 declaration_fingerprint: SealedDeclarationFingerprint,
54 ) -> Self {
55 Self {
56 policy_identity,
57 declaration_fingerprint,
58 }
59 }
60}
61
62#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
74#[serde(deny_unknown_fields)]
75pub struct MemoryRuntimeDoctorReport {
76 pub bootstrapped: bool,
78 pub tested_policy_identity: Result<PolicyIdentity, DiagnosticFailure>,
80 pub tested_declaration_fingerprint: SealedDeclarationFingerprint,
82 #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
84 pub established_bootstrap_binding: Option<DiagnosticRuntimeBinding>,
85 pub bootstrap_binding: DiagnosticCheck,
88 pub ledger_anchor: AllocationSlotDescriptor,
90 pub stable_cell: DiagnosticStableCell,
92 #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
94 pub commit_recovery: Option<CommitStoreDiagnostic>,
95 #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
97 pub ledger: Option<DiagnosticExport>,
98 pub registered_declarations: Vec<DiagnosticDeclaration>,
100 pub range_authority: DiagnosticRangeAuthority,
103 pub validation: DiagnosticCheck,
105}
106
107#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
114#[serde(deny_unknown_fields)]
115pub struct DiagnosticDeclaration {
116 pub authority: String,
118 pub declaration: AllocationDeclaration,
120}
121
122impl DiagnosticDeclaration {
123 #[must_use]
125 pub fn new(authority: impl Into<String>, declaration: AllocationDeclaration) -> Self {
126 Self {
127 authority: authority.into(),
128 declaration,
129 }
130 }
131}
132
133#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
140pub enum DiagnosticCode {
141 #[serde(rename = "stable_cell")]
143 StableCell,
144 #[serde(rename = "unsupported_format")]
146 UnsupportedFormat,
147 #[serde(rename = "ledger_recovery")]
149 LedgerRecovery,
150 #[serde(rename = "allocation_validation")]
152 AllocationValidation,
153 #[serde(rename = "policy_identity")]
155 PolicyIdentity,
156 #[serde(rename = "runtime_binding")]
158 RuntimeBinding,
159}
160
161#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
168#[serde(deny_unknown_fields)]
169pub struct DiagnosticFailure {
170 pub code: DiagnosticCode,
172 pub message: String,
174}
175
176impl DiagnosticFailure {
177 #[must_use]
179 pub fn new(code: DiagnosticCode, message: impl Into<String>) -> Self {
180 Self {
181 code,
182 message: message.into(),
183 }
184 }
185}
186
187#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
194#[serde(deny_unknown_fields)]
195pub struct DiagnosticRangeAuthority {
196 pub registered_records: Vec<MemoryManagerAuthorityRecord>,
198 pub effective_authority: MemoryManagerRangeAuthority,
200}
201
202impl DiagnosticRangeAuthority {
203 #[must_use]
205 pub const fn new(
206 registered_records: Vec<MemoryManagerAuthorityRecord>,
207 effective_authority: MemoryManagerRangeAuthority,
208 ) -> Self {
209 Self {
210 registered_records,
211 effective_authority,
212 }
213 }
214}
215
216#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
223#[serde(deny_unknown_fields)]
224pub struct DiagnosticStableCell {
225 pub status: DiagnosticStableCellStatus,
227 pub memory_size: DiagnosticMemorySize,
229}
230
231impl DiagnosticStableCell {
232 #[must_use]
234 pub const fn new(
235 status: DiagnosticStableCellStatus,
236 memory_size: DiagnosticMemorySize,
237 ) -> Self {
238 Self {
239 status,
240 memory_size,
241 }
242 }
243}
244
245#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
252#[serde(deny_unknown_fields)]
253pub enum DiagnosticStableCellStatus {
254 Empty,
256 Readable,
258 Corrupt {
261 failure: DiagnosticFailure,
263 },
264}
265
266#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
273#[serde(deny_unknown_fields)]
274pub enum DiagnosticCheck {
275 NotRun {
277 code: DiagnosticCode,
279 message: String,
281 },
282 Passed,
284 Failed {
286 code: DiagnosticCode,
288 message: String,
290 },
291}
292
293impl DiagnosticCheck {
294 #[must_use]
296 pub const fn passed() -> Self {
297 Self::Passed
298 }
299
300 #[must_use]
302 pub fn failed(code: DiagnosticCode, message: impl Into<String>) -> Self {
303 Self::Failed {
304 code,
305 message: message.into(),
306 }
307 }
308
309 #[must_use]
311 pub fn not_run(code: DiagnosticCode, message: impl Into<String>) -> Self {
312 Self::NotRun {
313 code,
314 message: message.into(),
315 }
316 }
317}
318
319impl DiagnosticExport {
320 #[must_use]
326 pub fn from_ledger(ledger: &AllocationLedger, ledger_anchor: AllocationSlotDescriptor) -> Self {
327 Self::from_records(
328 ledger.current_generation,
329 ledger_anchor,
330 ledger.allocation_history.records.iter().cloned(),
331 ledger.allocation_history.generations.clone(),
332 )
333 }
334
335 pub(crate) fn from_owned_ledger(
338 ledger: AllocationLedger,
339 ledger_anchor: AllocationSlotDescriptor,
340 ) -> Self {
341 Self::from_records(
342 ledger.current_generation,
343 ledger_anchor,
344 ledger.allocation_history.records.into_iter(),
345 ledger.allocation_history.generations,
346 )
347 }
348
349 fn from_records(
350 current_generation: u64,
351 ledger_anchor: AllocationSlotDescriptor,
352 records: impl Iterator<Item = AllocationRecord>,
353 generations: Vec<GenerationRecord>,
354 ) -> Self {
355 Self {
356 current_generation,
357 ledger_anchor,
358 records: records
359 .map(|allocation| DiagnosticRecord {
360 allocation,
361 memory_size: None,
362 })
363 .collect(),
364 generations,
365 commit_recovery: None,
366 }
367 }
368}
369
370#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
375#[serde(deny_unknown_fields)]
376pub struct DiagnosticRecord {
377 pub allocation: AllocationRecord,
379 #[serde(skip_serializing_if = "Option::is_none")]
384 pub memory_size: Option<DiagnosticMemorySize>,
385}
386
387#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
394#[serde(deny_unknown_fields)]
395pub struct DiagnosticMemorySize {
396 pub wasm_pages: u64,
398 pub bytes: u64,
400}
401
402impl DiagnosticMemorySize {
403 #[must_use]
405 pub const fn from_wasm_pages(wasm_pages: u64) -> Self {
406 Self {
407 wasm_pages,
408 bytes: wasm_pages.saturating_mul(WASM_PAGE_SIZE_BYTES),
409 }
410 }
411}
412
413#[cfg(test)]
414mod tests {
415 use super::*;
416 use crate::{
417 declaration::AllocationDeclaration,
418 ledger::{AllocationHistory, AllocationRecord},
419 physical::{CommitRecoveryError, CommitSlotDiagnostic, CommitStoreDiagnostic},
420 schema::SchemaMetadata,
421 };
422
423 #[test]
424 fn diagnostic_export_copies_ledger_records() {
425 let declaration = AllocationDeclaration::new(
426 "app.users.v1",
427 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
428 None,
429 SchemaMetadata::default(),
430 )
431 .expect("declaration");
432 let ledger = AllocationLedger {
433 current_generation: 3,
434 allocation_history: AllocationHistory::from_parts(
435 vec![AllocationRecord::active(3, &declaration)],
436 vec![GenerationRecord {
437 generation: 3,
438 parent_generation: 2,
439 runtime_fingerprint: Some("wasm:abc123".to_string()),
440 declaration_count: 1,
441 committed_at: None,
442 }],
443 ),
444 };
445
446 let export = DiagnosticExport::from_ledger(
447 &ledger,
448 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
449 );
450
451 assert_eq!(export.current_generation, 3);
452 assert_eq!(export.records.len(), 1);
453 assert_eq!(export.records[0].memory_size, None);
454 assert_eq!(export.generations.len(), 1);
455 assert_eq!(
456 export.ledger_anchor,
457 AllocationSlotDescriptor::memory_manager(0).expect("usable slot")
458 );
459 assert_eq!(export.commit_recovery, None);
460 assert_eq!(
461 export.generations,
462 ledger.allocation_history().generations()
463 );
464 assert_eq!(
465 export,
466 DiagnosticExport::from_owned_ledger(ledger, export.ledger_anchor.clone())
467 );
468 let wire = serde_json::to_value(&export).expect("diagnostic JSON");
469 assert_eq!(
470 wire["generations"][0],
471 serde_json::json!({
472 "generation": 3,
473 "parent_generation": 2,
474 "runtime_fingerprint": "wasm:abc123",
475 "declaration_count": 1,
476 "committed_at": null,
477 })
478 );
479 let decoded: DiagnosticExport = serde_json::from_value(wire).expect("current JSON shape");
480 assert_eq!(decoded, export);
481 }
482
483 #[test]
484 fn diagnostic_export_rejects_unknown_top_level_fields() {
485 use crate::test_cbor::Value;
486
487 let export = DiagnosticExport {
488 current_generation: 0,
489 ledger_anchor: AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
490 records: Vec::new(),
491 generations: Vec::new(),
492 commit_recovery: None,
493 };
494 let Value::Map(mut map) = crate::test_cbor::to_value(export).expect("diagnostic value")
495 else {
496 panic!("diagnostic export encodes as a map");
497 };
498 map.push((Value::Text("future_field".to_string()), Value::Bool(true)));
499 let bytes = crate::test_cbor::to_vec(&Value::Map(map)).expect("diagnostic bytes");
500
501 let err = crate::test_cbor::from_slice::<DiagnosticExport>(&bytes)
502 .expect_err("unknown diagnostic field must fail closed");
503
504 assert!(err.to_string().contains("future_field"));
505 }
506
507 #[test]
508 fn diagnostic_outcome_states_round_trip() {
509 let stable_cell = DiagnosticStableCell::new(
510 DiagnosticStableCellStatus::Corrupt {
511 failure: DiagnosticFailure::new(
512 DiagnosticCode::StableCell,
513 "bad stable-cell record",
514 ),
515 },
516 DiagnosticMemorySize::from_wasm_pages(1),
517 );
518 let range_authority =
519 DiagnosticRangeAuthority::new(Vec::new(), MemoryManagerRangeAuthority::default());
520 let check = DiagnosticCheck::failed(
521 DiagnosticCode::AllocationValidation,
522 "duplicate declaration",
523 );
524
525 for value in [DiagnosticCheck::passed(), check] {
526 let bytes = crate::test_cbor::to_vec(&value).expect("check bytes");
527 let decoded: DiagnosticCheck =
528 crate::test_cbor::from_slice(&bytes).expect("check round trip");
529 assert_eq!(decoded, value);
530 }
531
532 let bytes = crate::test_cbor::to_vec(&stable_cell).expect("stable-cell diagnostic bytes");
533 let decoded: DiagnosticStableCell =
534 crate::test_cbor::from_slice(&bytes).expect("stable-cell round trip");
535 assert_eq!(decoded, stable_cell);
536
537 let bytes = crate::test_cbor::to_vec(&range_authority).expect("range diagnostic bytes");
538 let decoded: DiagnosticRangeAuthority =
539 crate::test_cbor::from_slice(&bytes).expect("range round trip");
540 assert_eq!(decoded, range_authority);
541 }
542
543 #[test]
544 fn diagnostic_codes_have_stable_wire_names() {
545 let cases = [
546 (DiagnosticCode::StableCell, "stable_cell"),
547 (DiagnosticCode::UnsupportedFormat, "unsupported_format"),
548 (DiagnosticCode::LedgerRecovery, "ledger_recovery"),
549 (
550 DiagnosticCode::AllocationValidation,
551 "allocation_validation",
552 ),
553 (DiagnosticCode::PolicyIdentity, "policy_identity"),
554 (DiagnosticCode::RuntimeBinding, "runtime_binding"),
555 ];
556
557 for (code, expected) in cases {
558 assert_eq!(
559 crate::test_cbor::to_value(code).expect("diagnostic code value"),
560 crate::test_cbor::Value::Text(expected.to_string())
561 );
562 }
563 }
564
565 #[test]
566 fn diagnostic_export_can_include_commit_recovery_state() {
567 let ledger = AllocationLedger {
568 current_generation: 3,
569 allocation_history: AllocationHistory::default(),
570 };
571 let commit_recovery = CommitStoreDiagnostic {
572 slot0: CommitSlotDiagnostic::Valid { generation: 3 },
573 slot1: CommitSlotDiagnostic::Empty,
574 recovery: Ok(3),
575 };
576
577 let mut export = DiagnosticExport::from_ledger(
578 &ledger,
579 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
580 );
581 export.commit_recovery = Some(commit_recovery);
582
583 assert_eq!(export.commit_recovery, Some(commit_recovery));
584 }
585
586 #[test]
587 fn diagnostic_export_can_include_memory_sizes() {
588 let declaration = AllocationDeclaration::new(
589 "app.users.v1",
590 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
591 None,
592 SchemaMetadata::default(),
593 )
594 .expect("declaration");
595 let ledger = AllocationLedger {
596 current_generation: 3,
597 allocation_history: AllocationHistory::from_parts(
598 vec![AllocationRecord::active(3, &declaration)],
599 Vec::new(),
600 ),
601 };
602
603 let mut export = DiagnosticExport::from_ledger(
604 &ledger,
605 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
606 );
607 export.records[0].memory_size = Some(DiagnosticMemorySize::from_wasm_pages(2));
608
609 assert_eq!(
610 export.records[0].memory_size,
611 Some(DiagnosticMemorySize {
612 wasm_pages: 2,
613 bytes: 131_072,
614 })
615 );
616 let wire = serde_json::to_value(&export).expect("diagnostic JSON");
617 assert_eq!(
618 wire["records"][0]["memory_size"],
619 serde_json::json!({"wasm_pages": 2, "bytes": 131_072})
620 );
621 }
622
623 #[test]
624 fn diagnostic_export_can_report_recovery_failure() {
625 let ledger = AllocationLedger {
626 current_generation: 0,
627 allocation_history: AllocationHistory::default(),
628 };
629 let commit_recovery = CommitStoreDiagnostic {
630 slot0: CommitSlotDiagnostic::Empty,
631 slot1: CommitSlotDiagnostic::Empty,
632 recovery: Err(CommitRecoveryError::NoValidGeneration),
633 };
634
635 let mut export = DiagnosticExport::from_ledger(
636 &ledger,
637 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
638 );
639 export.commit_recovery = Some(commit_recovery);
640
641 assert_eq!(
642 export.commit_recovery.expect("commit recovery").recovery,
643 Err(CommitRecoveryError::NoValidGeneration)
644 );
645 }
646}