1use crate::{
2 constants::WASM_PAGE_SIZE_BYTES,
3 declaration::AllocationDeclaration,
4 ledger::{AllocationLedger, AllocationRecord},
5 physical::CommitStoreDiagnostic,
6 policy::PolicyIdentity,
7 registry::SealedDeclarationFingerprint,
8 slot::{MemoryManagerAuthorityRecord, MemoryManagerRangeAuthority, MemoryManagerSlot},
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: MemoryManagerSlot,
23 pub records: Vec<DiagnosticRecord>,
25 #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
27 pub commit_recovery: Option<CommitStoreDiagnostic>,
28}
29
30#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
38#[serde(deny_unknown_fields)]
39pub struct DiagnosticRuntimeBinding {
40 pub policy_identity: PolicyIdentity,
42 pub declaration_fingerprint: SealedDeclarationFingerprint,
44}
45
46impl DiagnosticRuntimeBinding {
47 #[must_use]
49 pub const fn new(
50 policy_identity: PolicyIdentity,
51 declaration_fingerprint: SealedDeclarationFingerprint,
52 ) -> Self {
53 Self {
54 policy_identity,
55 declaration_fingerprint,
56 }
57 }
58}
59
60#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
72#[serde(deny_unknown_fields)]
73pub struct MemoryRuntimeDoctorReport {
74 pub bootstrapped: bool,
76 pub tested_policy_identity: Result<PolicyIdentity, DiagnosticFailure>,
78 pub tested_declaration_fingerprint: SealedDeclarationFingerprint,
80 #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
82 pub established_bootstrap_binding: Option<DiagnosticRuntimeBinding>,
83 pub bootstrap_binding: DiagnosticCheck,
86 pub ledger_anchor: MemoryManagerSlot,
88 pub stable_cell: DiagnosticStableCell,
90 #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
92 pub commit_recovery: Option<CommitStoreDiagnostic>,
93 #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
95 pub ledger: Option<DiagnosticExport>,
96 pub registered_declarations: Vec<DiagnosticDeclaration>,
98 pub range_authority: DiagnosticRangeAuthority,
101 pub validation: DiagnosticCheck,
103}
104
105#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
112#[serde(deny_unknown_fields)]
113pub struct DiagnosticDeclaration {
114 pub authority: String,
116 pub declaration: AllocationDeclaration,
118}
119
120impl DiagnosticDeclaration {
121 #[must_use]
123 pub fn new(authority: impl Into<String>, declaration: AllocationDeclaration) -> Self {
124 Self {
125 authority: authority.into(),
126 declaration,
127 }
128 }
129}
130
131#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
138pub enum DiagnosticCode {
139 #[serde(rename = "stable_cell")]
141 StableCell,
142 #[serde(rename = "unsupported_format")]
144 UnsupportedFormat,
145 #[serde(rename = "ledger_recovery")]
147 LedgerRecovery,
148 #[serde(rename = "allocation_validation")]
150 AllocationValidation,
151 #[serde(rename = "policy_identity")]
153 PolicyIdentity,
154 #[serde(rename = "runtime_binding")]
156 RuntimeBinding,
157}
158
159#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
166#[serde(deny_unknown_fields)]
167pub struct DiagnosticFailure {
168 pub code: DiagnosticCode,
170 pub message: String,
172}
173
174impl DiagnosticFailure {
175 #[must_use]
177 pub fn new(code: DiagnosticCode, message: impl Into<String>) -> Self {
178 Self {
179 code,
180 message: message.into(),
181 }
182 }
183}
184
185#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
192#[serde(deny_unknown_fields)]
193pub struct DiagnosticRangeAuthority {
194 pub registered_records: Vec<MemoryManagerAuthorityRecord>,
196 pub effective_authority: MemoryManagerRangeAuthority,
198}
199
200impl DiagnosticRangeAuthority {
201 #[must_use]
203 pub const fn new(
204 registered_records: Vec<MemoryManagerAuthorityRecord>,
205 effective_authority: MemoryManagerRangeAuthority,
206 ) -> Self {
207 Self {
208 registered_records,
209 effective_authority,
210 }
211 }
212}
213
214#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
221#[serde(deny_unknown_fields)]
222pub struct DiagnosticStableCell {
223 pub status: DiagnosticStableCellStatus,
225 pub memory_size: DiagnosticMemorySize,
227}
228
229impl DiagnosticStableCell {
230 #[must_use]
232 pub const fn new(
233 status: DiagnosticStableCellStatus,
234 memory_size: DiagnosticMemorySize,
235 ) -> Self {
236 Self {
237 status,
238 memory_size,
239 }
240 }
241}
242
243#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
250#[serde(deny_unknown_fields)]
251pub enum DiagnosticStableCellStatus {
252 Empty,
254 Readable,
256 Corrupt {
259 failure: DiagnosticFailure,
261 },
262}
263
264#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
271#[serde(deny_unknown_fields)]
272pub enum DiagnosticCheck {
273 NotRun {
275 code: DiagnosticCode,
277 message: String,
279 },
280 Passed,
282 Failed {
284 code: DiagnosticCode,
286 message: String,
288 },
289}
290
291impl DiagnosticCheck {
292 #[must_use]
294 pub const fn passed() -> Self {
295 Self::Passed
296 }
297
298 #[must_use]
300 pub fn failed(code: DiagnosticCode, message: impl Into<String>) -> Self {
301 Self::Failed {
302 code,
303 message: message.into(),
304 }
305 }
306
307 #[must_use]
309 pub fn not_run(code: DiagnosticCode, message: impl Into<String>) -> Self {
310 Self::NotRun {
311 code,
312 message: message.into(),
313 }
314 }
315}
316
317impl DiagnosticExport {
318 #[must_use]
324 pub fn from_ledger(ledger: &AllocationLedger, ledger_anchor: MemoryManagerSlot) -> Self {
325 Self::from_records(
326 ledger.current_generation,
327 ledger_anchor,
328 ledger.records.iter().cloned(),
329 )
330 }
331
332 pub(crate) fn from_owned_ledger(
335 ledger: AllocationLedger,
336 ledger_anchor: MemoryManagerSlot,
337 ) -> Self {
338 Self::from_records(
339 ledger.current_generation,
340 ledger_anchor,
341 ledger.records.into_iter(),
342 )
343 }
344
345 fn from_records(
346 current_generation: u64,
347 ledger_anchor: MemoryManagerSlot,
348 records: impl Iterator<Item = AllocationRecord>,
349 ) -> Self {
350 Self {
351 current_generation,
352 ledger_anchor,
353 records: records
354 .map(|allocation| DiagnosticRecord {
355 allocation,
356 memory_size: None,
357 })
358 .collect(),
359 commit_recovery: None,
360 }
361 }
362}
363
364#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
369#[serde(deny_unknown_fields)]
370pub struct DiagnosticRecord {
371 pub allocation: AllocationRecord,
373 #[serde(skip_serializing_if = "Option::is_none")]
378 pub memory_size: Option<DiagnosticMemorySize>,
379}
380
381#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
388#[serde(deny_unknown_fields)]
389pub struct DiagnosticMemorySize {
390 pub wasm_pages: u64,
392 pub bytes: u64,
394}
395
396impl DiagnosticMemorySize {
397 #[must_use]
399 pub const fn from_wasm_pages(wasm_pages: u64) -> Self {
400 Self {
401 wasm_pages,
402 bytes: wasm_pages.saturating_mul(WASM_PAGE_SIZE_BYTES),
403 }
404 }
405}
406
407#[cfg(test)]
408mod tests {
409 use super::*;
410 use crate::{
411 declaration::AllocationDeclaration,
412 ledger::AllocationRecord,
413 physical::{CommitRecoveryError, CommitSlotDiagnostic, CommitStoreDiagnostic},
414 schema::SchemaMetadata,
415 };
416
417 #[test]
418 fn diagnostic_export_copies_ledger_records() {
419 let declaration = AllocationDeclaration::new(
420 "app.users.v1",
421 MemoryManagerSlot::new(100).expect("usable slot"),
422 None,
423 SchemaMetadata::default(),
424 )
425 .expect("declaration");
426 let ledger = AllocationLedger {
427 current_generation: 3,
428 records: vec![AllocationRecord::active(&declaration)],
429 };
430
431 let export =
432 DiagnosticExport::from_ledger(&ledger, MemoryManagerSlot::new(0).expect("usable slot"));
433
434 assert_eq!(export.current_generation, 3);
435 assert_eq!(export.records.len(), 1);
436 assert_eq!(export.records[0].memory_size, None);
437
438 assert_eq!(
439 export.ledger_anchor,
440 MemoryManagerSlot::new(0).expect("usable slot")
441 );
442 assert_eq!(export.commit_recovery, None);
443
444 assert_eq!(
445 export,
446 DiagnosticExport::from_owned_ledger(ledger, export.ledger_anchor.clone())
447 );
448 let wire = serde_json::to_value(&export).expect("diagnostic JSON");
449 assert_eq!(
450 wire["records"][0]["allocation"]["schema"],
451 serde_json::json!({"schema_version": null})
452 );
453
454 let decoded: DiagnosticExport = serde_json::from_value(wire).expect("current JSON shape");
455 assert_eq!(decoded, export);
456 }
457
458 #[test]
459 fn diagnostic_export_rejects_unknown_top_level_fields() {
460 use crate::test_cbor::Value;
461
462 let export = DiagnosticExport {
463 current_generation: 0,
464 ledger_anchor: MemoryManagerSlot::new(0).expect("usable slot"),
465 records: Vec::new(),
466 commit_recovery: None,
467 };
468 let Value::Map(mut map) = crate::test_cbor::to_value(export).expect("diagnostic value")
469 else {
470 panic!("diagnostic export encodes as a map");
471 };
472 map.push((Value::Text("future_field".to_string()), Value::Bool(true)));
473 let bytes = crate::test_cbor::to_vec(&Value::Map(map)).expect("diagnostic bytes");
474
475 let err = crate::test_cbor::from_slice::<DiagnosticExport>(&bytes)
476 .expect_err("unknown diagnostic field must fail closed");
477
478 assert!(err.to_string().contains("future_field"));
479 }
480
481 #[test]
482 fn diagnostic_outcome_states_round_trip() {
483 let stable_cell = DiagnosticStableCell::new(
484 DiagnosticStableCellStatus::Corrupt {
485 failure: DiagnosticFailure::new(
486 DiagnosticCode::StableCell,
487 "bad stable-cell record",
488 ),
489 },
490 DiagnosticMemorySize::from_wasm_pages(1),
491 );
492 let range_authority =
493 DiagnosticRangeAuthority::new(Vec::new(), MemoryManagerRangeAuthority::default());
494 let check = DiagnosticCheck::failed(
495 DiagnosticCode::AllocationValidation,
496 "duplicate declaration",
497 );
498
499 for value in [DiagnosticCheck::passed(), check] {
500 let bytes = crate::test_cbor::to_vec(&value).expect("check bytes");
501 let decoded: DiagnosticCheck =
502 crate::test_cbor::from_slice(&bytes).expect("check round trip");
503 assert_eq!(decoded, value);
504 }
505
506 let bytes = crate::test_cbor::to_vec(&stable_cell).expect("stable-cell diagnostic bytes");
507 let decoded: DiagnosticStableCell =
508 crate::test_cbor::from_slice(&bytes).expect("stable-cell round trip");
509 assert_eq!(decoded, stable_cell);
510
511 let bytes = crate::test_cbor::to_vec(&range_authority).expect("range diagnostic bytes");
512 let decoded: DiagnosticRangeAuthority =
513 crate::test_cbor::from_slice(&bytes).expect("range round trip");
514 assert_eq!(decoded, range_authority);
515 }
516
517 #[test]
518 fn diagnostic_codes_have_stable_wire_names() {
519 let cases = [
520 (DiagnosticCode::StableCell, "stable_cell"),
521 (DiagnosticCode::UnsupportedFormat, "unsupported_format"),
522 (DiagnosticCode::LedgerRecovery, "ledger_recovery"),
523 (
524 DiagnosticCode::AllocationValidation,
525 "allocation_validation",
526 ),
527 (DiagnosticCode::PolicyIdentity, "policy_identity"),
528 (DiagnosticCode::RuntimeBinding, "runtime_binding"),
529 ];
530
531 for (code, expected) in cases {
532 assert_eq!(
533 crate::test_cbor::to_value(code).expect("diagnostic code value"),
534 crate::test_cbor::Value::Text(expected.to_string())
535 );
536 }
537 }
538
539 #[test]
540 fn diagnostic_export_can_include_commit_recovery_state() {
541 let ledger = AllocationLedger {
542 current_generation: 3,
543 records: Vec::new(),
544 };
545 let commit_recovery = CommitStoreDiagnostic {
546 slot0: CommitSlotDiagnostic::Valid { generation: 3 },
547 slot1: CommitSlotDiagnostic::Empty,
548 recovery: Ok(3),
549 };
550
551 let mut export =
552 DiagnosticExport::from_ledger(&ledger, MemoryManagerSlot::new(0).expect("usable slot"));
553 export.commit_recovery = Some(commit_recovery);
554
555 assert_eq!(export.commit_recovery, Some(commit_recovery));
556 }
557
558 #[test]
559 fn diagnostic_export_can_include_memory_sizes() {
560 let declaration = AllocationDeclaration::new(
561 "app.users.v1",
562 MemoryManagerSlot::new(100).expect("usable slot"),
563 None,
564 SchemaMetadata::default(),
565 )
566 .expect("declaration");
567 let ledger = AllocationLedger {
568 current_generation: 3,
569 records: vec![AllocationRecord::active(&declaration)],
570 };
571
572 let mut export =
573 DiagnosticExport::from_ledger(&ledger, MemoryManagerSlot::new(0).expect("usable slot"));
574 export.records[0].memory_size = Some(DiagnosticMemorySize::from_wasm_pages(2));
575
576 assert_eq!(
577 export.records[0].memory_size,
578 Some(DiagnosticMemorySize {
579 wasm_pages: 2,
580 bytes: 131_072,
581 })
582 );
583 let wire = serde_json::to_value(&export).expect("diagnostic JSON");
584 assert_eq!(
585 wire["records"][0]["memory_size"],
586 serde_json::json!({"wasm_pages": 2, "bytes": 131_072})
587 );
588 }
589
590 #[test]
591 fn diagnostic_export_can_report_recovery_failure() {
592 let ledger = AllocationLedger {
593 current_generation: 0,
594 records: Vec::new(),
595 };
596 let commit_recovery = CommitStoreDiagnostic {
597 slot0: CommitSlotDiagnostic::Empty,
598 slot1: CommitSlotDiagnostic::Empty,
599 recovery: Err(CommitRecoveryError::NoValidGeneration),
600 };
601
602 let mut export =
603 DiagnosticExport::from_ledger(&ledger, MemoryManagerSlot::new(0).expect("usable slot"));
604 export.commit_recovery = Some(commit_recovery);
605
606 assert_eq!(
607 export.commit_recovery.expect("commit recovery").recovery,
608 Err(CommitRecoveryError::NoValidGeneration)
609 );
610 }
611}