ic_query/ic/node_status/model.rs
1//! Module: ic::node_status::model
2//!
3//! Responsibility: public observed node-status requests, evidence, reports, and errors.
4//! Does not own: source calls, cache IO, projection, or rendering.
5//! Boundary: preserves raw Dashboard status data and explicit off-chain/cache provenance.
6
7use crate::ic::IcDashboardReportProvenance;
8use serde::{Deserialize as SerdeDeserialize, Serialize};
9use std::fmt;
10#[cfg(feature = "host")]
11use std::path::PathBuf;
12use thiserror::Error as ThisError;
13
14///
15/// IcNodeStatusScope
16///
17/// Dashboard collection scope retained by an observed node-status snapshot.
18///
19
20#[derive(Clone, Copy, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
21#[serde(rename_all = "snake_case")]
22pub enum IcNodeStatusScope {
23 /// Dashboard's default public-mainnet node set, excluding cloud-engine Type4 nodes.
24 DashboardMainnetDefault,
25}
26
27impl IcNodeStatusScope {
28 /// Return the stable serialized scope label.
29 #[must_use]
30 pub const fn as_str(self) -> &'static str {
31 match self {
32 Self::DashboardMainnetDefault => super::IC_NODE_STATUS_SCOPE,
33 }
34 }
35}
36
37///
38/// IcNodeOperationalStatus
39///
40/// Known Dashboard node-status classification used for filtering and counts.
41///
42
43#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
44#[serde(rename_all = "snake_case")]
45pub enum IcNodeOperationalStatus {
46 /// The Dashboard reports the node as up.
47 Up,
48 /// The Dashboard reports the node as down.
49 Down,
50 /// The Dashboard reports the node as administratively disabled.
51 Disabled,
52 /// The Dashboard reports degraded operation.
53 Degraded,
54 /// The raw Dashboard value is not recognized by this release.
55 Unknown,
56}
57
58impl IcNodeOperationalStatus {
59 /// Classify raw Dashboard status text without discarding unknown future values.
60 #[must_use]
61 pub fn from_raw(raw: &str) -> Self {
62 match raw {
63 "UP" => Self::Up,
64 "DOWN" => Self::Down,
65 "DISABLED" => Self::Disabled,
66 "DEGRADED" => Self::Degraded,
67 _ => Self::Unknown,
68 }
69 }
70
71 /// Return the stable lowercase display label.
72 #[must_use]
73 pub const fn as_str(self) -> &'static str {
74 match self {
75 Self::Up => "up",
76 Self::Down => "down",
77 Self::Disabled => "disabled",
78 Self::Degraded => "degraded",
79 Self::Unknown => "unknown",
80 }
81 }
82}
83
84impl fmt::Display for IcNodeOperationalStatus {
85 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
86 formatter.write_str(self.as_str())
87 }
88}
89
90///
91/// IcNodeStatusCounts
92///
93/// Exact status totals derived from raw observed node rows.
94///
95
96#[derive(Clone, Debug, Default, Eq, PartialEq, SerdeDeserialize, Serialize)]
97pub struct IcNodeStatusCounts {
98 /// Total observed rows in the group.
99 pub total: usize,
100 /// Rows classified as `UP`.
101 pub up: usize,
102 /// Rows classified as `DOWN`.
103 pub down: usize,
104 /// Rows classified as `DISABLED`.
105 pub disabled: usize,
106 /// Rows classified as `DEGRADED`.
107 pub degraded: usize,
108 /// Rows whose raw status is not recognized.
109 pub unknown: usize,
110}
111
112impl IcNodeStatusCounts {
113 /// Return every row not classified as raw Dashboard `UP`.
114 #[must_use]
115 pub const fn non_up(&self) -> usize {
116 self.total.saturating_sub(self.up)
117 }
118}
119
120///
121/// IcNodeAssignmentStatusCounts
122///
123/// Operational-status totals partitioned by mutually exclusive assignment class.
124///
125
126#[derive(Clone, Debug, Default, Eq, PartialEq, SerdeDeserialize, Serialize)]
127pub struct IcNodeAssignmentStatusCounts {
128 /// Status totals for nodes with an observed assigned Subnet.
129 pub assigned: IcNodeStatusCounts,
130 /// Status totals for rows whose raw node type is `UNASSIGNED`.
131 pub unassigned: IcNodeStatusCounts,
132 /// Status totals for rows whose raw node type is `API_BOUNDARY`.
133 pub api_boundary: IcNodeStatusCounts,
134 /// Status totals for rows without a known assignment class.
135 pub unknown: IcNodeStatusCounts,
136}
137
138///
139/// IcNodeStatusGroupCounts
140///
141/// Overall and assignment-partitioned status totals for one aggregate group.
142///
143
144#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
145pub struct IcNodeStatusGroupCounts {
146 /// Raw operational-status totals for the group.
147 pub statuses: IcNodeStatusCounts,
148 /// Raw operational-status totals partitioned by assignment class.
149 pub assignment_statuses: IcNodeAssignmentStatusCounts,
150}
151
152///
153/// IcNodeStatusRow
154///
155/// One raw node observation retained from the official Dashboard node resource.
156///
157
158#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
159pub struct IcNodeStatusRow {
160 /// Canonical node principal.
161 pub node_id: String,
162 /// Canonical node-operator principal.
163 pub node_operator_id: String,
164 /// Canonical node-provider principal.
165 pub node_provider_id: String,
166 /// Off-chain Dashboard node-provider name.
167 pub node_provider_name: String,
168 /// Raw Dashboard node type.
169 pub node_type: String,
170 /// Raw Registry reward type reported by the Dashboard.
171 pub node_reward_type: String,
172 /// Raw Dashboard operational status.
173 pub status: String,
174 /// Raw Dashboard alert name when present.
175 pub alert_name: Option<String>,
176 /// Assigned Subnet principal when present.
177 pub subnet_id: Option<String>,
178 /// Cloud-engine Subnet principal when present in a future compatible scope.
179 pub cloud_engine_subnet_id: Option<String>,
180 /// Dashboard data-center identifier.
181 pub data_center_id: String,
182 /// Dashboard data-center name.
183 pub data_center_name: String,
184 /// Dashboard infrastructure owner label.
185 pub owner: String,
186 /// Raw Dashboard geographic region label.
187 pub region: String,
188 /// Observed GuestOS version when reported.
189 pub guestos_version: Option<String>,
190 /// Observed GuestOS trusted-execution status when reported.
191 pub guestos_tee_active: Option<bool>,
192 /// Observed node IP address when reported.
193 pub ip_address: Option<String>,
194 /// Observed IPv4-connectivity state when reported.
195 pub ipv4_connectivity_status: Option<bool>,
196 /// Dashboard hardware-generation label when reported.
197 pub node_hardware_generation: Option<String>,
198}
199
200impl IcNodeStatusRow {
201 /// Return the known classification of this row's raw status.
202 #[must_use]
203 pub fn operational_status(&self) -> IcNodeOperationalStatus {
204 IcNodeOperationalStatus::from_raw(&self.status)
205 }
206
207 /// Return whether the raw row is anything other than known `UP`.
208 #[must_use]
209 pub fn is_non_up(&self) -> bool {
210 self.operational_status() != IcNodeOperationalStatus::Up
211 }
212}
213
214///
215/// IcNodeStatusCacheEvidence
216///
217/// Caller-relative local cache evidence attached to a projected status report.
218///
219
220#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
221pub struct IcNodeStatusCacheEvidence {
222 /// Canonical local cache path used by the report.
223 pub cache_path: String,
224 /// Whether the cache is within the default age policy for this caller's time.
225 pub cache_fresh: bool,
226 /// Cache age at report construction.
227 pub age_seconds: u64,
228 /// Age threshold used to classify freshness.
229 pub stale_after_seconds: u64,
230}
231
232///
233/// IcNodeStatusObservation
234///
235/// Shared source, scope, and optional cache evidence for status reports.
236///
237
238#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
239pub struct IcNodeStatusObservation {
240 #[serde(flatten)]
241 /// Official Dashboard source provenance.
242 pub source: IcDashboardReportProvenance,
243 /// Explicit node collection scope.
244 pub scope: IcNodeStatusScope,
245 /// Whether cloud-engine nodes were included in the source collection.
246 pub cloud_engine_nodes_included: bool,
247 /// Local cache evidence, absent for a pure live snapshot.
248 pub cache: Option<IcNodeStatusCacheEvidence>,
249}
250
251///
252/// IcNodeStatusSnapshot
253///
254/// Complete canonical observed node snapshot used by every status projection.
255///
256
257#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
258pub struct IcNodeStatusSnapshot {
259 #[serde(flatten)]
260 /// Shared source and scope evidence.
261 pub observation: IcNodeStatusObservation,
262 /// Exact number of raw node rows.
263 pub node_count: usize,
264 /// Snapshot-wide overall and assignment-partitioned totals.
265 pub counts: IcNodeStatusGroupCounts,
266 /// Canonically ordered raw node rows.
267 pub nodes: Vec<IcNodeStatusRow>,
268}
269
270///
271/// IcNodeStatusSnapshotRequest
272///
273/// Request for one live finite Dashboard node-status snapshot.
274///
275
276#[derive(Clone, Debug, Eq, PartialEq)]
277pub struct IcNodeStatusSnapshotRequest {
278 /// Dashboard API v3 base endpoint.
279 pub source_endpoint: String,
280 /// Caller-supplied collection timestamp as Unix seconds.
281 pub now_unix_secs: u64,
282}
283
284impl IcNodeStatusSnapshotRequest {
285 /// Construct one live node-status snapshot request.
286 #[must_use]
287 pub fn new(source_endpoint: impl Into<String>, now_unix_secs: u64) -> Self {
288 Self {
289 source_endpoint: source_endpoint.into(),
290 now_unix_secs,
291 }
292 }
293}
294
295///
296/// IcNodeStatusSourceData
297///
298/// Untrusted raw node rows and provenance returned by a Dashboard source capability.
299///
300
301#[cfg(feature = "host")]
302#[derive(Clone, Debug, Eq, PartialEq)]
303pub struct IcNodeStatusSourceData {
304 /// Source-call provenance echoed by the source.
305 pub source: crate::ic::IcSourceRequest,
306 /// Explicit Dashboard collection scope.
307 pub scope: IcNodeStatusScope,
308 /// Whether the source claims cloud-engine node inclusion.
309 pub cloud_engine_nodes_included: bool,
310 /// Raw observed node rows.
311 pub nodes: Vec<IcNodeStatusRow>,
312}
313
314///
315/// IcNodeStatusView
316///
317/// Target and attention selection shared by node, Subnet, and provider views.
318///
319
320#[derive(Clone, Debug, Default, Eq, PartialEq)]
321pub struct IcNodeStatusView {
322 /// Optional exact identifier or unique principal prefix.
323 pub target: Option<String>,
324 /// Whether fully-up rows or groups are included without a target.
325 pub include_all: bool,
326}
327
328impl IcNodeStatusView {
329 /// Construct the default attention-only view.
330 #[must_use]
331 pub const fn attention() -> Self {
332 Self {
333 target: None,
334 include_all: false,
335 }
336 }
337
338 /// Select one exact identifier or unique principal prefix.
339 #[must_use]
340 pub fn with_target(mut self, target: impl Into<String>) -> Self {
341 self.target = Some(target.into());
342 self
343 }
344
345 /// Include fully-up rows as well as attention rows.
346 #[must_use]
347 pub const fn with_all(mut self, include_all: bool) -> Self {
348 self.include_all = include_all;
349 self
350 }
351}
352
353///
354/// IcNodeStatusReport
355///
356/// Node-level view over one complete observed status snapshot.
357///
358
359#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
360pub struct IcNodeStatusReport {
361 #[serde(flatten)]
362 /// Shared source, scope, and cache evidence.
363 pub observation: IcNodeStatusObservation,
364 /// Total rows in the complete source snapshot.
365 pub snapshot_node_count: usize,
366 /// Snapshot-wide overall and assignment-partitioned totals.
367 pub counts: IcNodeStatusGroupCounts,
368 /// Whether the view includes fully-up rows.
369 pub include_all: bool,
370 /// Target text supplied by the caller.
371 pub requested_target: Option<String>,
372 /// Canonical target resolved by exact or unique-prefix matching.
373 pub resolved_target: Option<String>,
374 /// Stable label describing how the target resolved.
375 pub resolved_from: Option<String>,
376 /// Number of returned node rows.
377 pub returned_node_count: usize,
378 /// Selected canonical node rows.
379 pub nodes: Vec<IcNodeStatusRow>,
380}
381
382///
383/// IcSubnetStatusRow
384///
385/// Observed operational counts and conservative threshold evidence for one Subnet.
386///
387
388#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
389pub struct IcSubnetStatusRow {
390 /// Canonical Subnet principal.
391 pub subnet_id: String,
392 /// Operational-status totals for observed assigned nodes.
393 pub statuses: IcNodeStatusCounts,
394 /// Byzantine fault count `floor((n - 1) / 3)` for the observed membership size.
395 pub fault_tolerance_node_count: usize,
396 /// Additional down nodes required for the down count to exceed the derived threshold.
397 pub additional_down_nodes_to_exceed_fault_tolerance: usize,
398 /// Additional non-up nodes required for the conservative count to exceed the threshold.
399 pub additional_non_up_nodes_to_exceed_fault_tolerance: usize,
400 /// Whether the observed down count already exceeds the derived threshold.
401 pub down_fault_tolerance_exceeded: bool,
402 /// Whether the observed conservative non-up count already exceeds the threshold.
403 pub conservative_non_up_fault_tolerance_exceeded: bool,
404 /// Raw non-up node evidence in canonical node order.
405 pub non_up_nodes: Vec<IcNodeStatusRow>,
406}
407
408///
409/// IcSubnetStatusReport
410///
411/// Subnet-level operational view over one complete observed node snapshot.
412///
413
414#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
415pub struct IcSubnetStatusReport {
416 #[serde(flatten)]
417 /// Shared source, scope, and cache evidence.
418 pub observation: IcNodeStatusObservation,
419 /// Total rows in the complete source snapshot.
420 pub snapshot_node_count: usize,
421 /// Snapshot rows with an assigned Subnet.
422 pub assigned_node_count: usize,
423 /// Number of observed Subnets before view filtering.
424 pub subnet_count: usize,
425 /// Number of observed Subnets containing a non-up node.
426 pub attention_subnet_count: usize,
427 /// Whether fully-up Subnets are included without a target.
428 pub include_all: bool,
429 /// Target text supplied by the caller.
430 pub requested_target: Option<String>,
431 /// Canonical Subnet target resolved by exact or unique-prefix matching.
432 pub resolved_target: Option<String>,
433 /// Stable label describing how the target resolved.
434 pub resolved_from: Option<String>,
435 /// Number of returned Subnet rows.
436 pub returned_subnet_count: usize,
437 /// Canonically ordered selected Subnet rows.
438 pub subnets: Vec<IcSubnetStatusRow>,
439}
440
441///
442/// IcNodeProviderStatusRow
443///
444/// Assignment and raw operational counts for one observed node provider.
445///
446
447#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
448pub struct IcNodeProviderStatusRow {
449 /// Canonical node-provider principal.
450 pub node_provider_id: String,
451 /// Off-chain Dashboard provider name.
452 pub node_provider_name: String,
453 /// Operational-status and assignment totals for the provider.
454 pub counts: IcNodeStatusGroupCounts,
455 /// Raw non-up node evidence in canonical node order.
456 pub non_up_nodes: Vec<IcNodeStatusRow>,
457}
458
459///
460/// IcNodeProviderStatusReport
461///
462/// Node-provider operational view over one complete observed node snapshot.
463///
464
465#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
466pub struct IcNodeProviderStatusReport {
467 #[serde(flatten)]
468 /// Shared source, scope, and cache evidence.
469 pub observation: IcNodeStatusObservation,
470 /// Total rows in the complete source snapshot.
471 pub snapshot_node_count: usize,
472 /// Number of observed provider principals before view filtering.
473 pub provider_count: usize,
474 /// Number of provider groups containing a non-up node.
475 pub attention_provider_count: usize,
476 /// Whether fully-up providers are included without a target.
477 pub include_all: bool,
478 /// Target text supplied by the caller.
479 pub requested_target: Option<String>,
480 /// Canonical provider target resolved by exact or unique-prefix matching.
481 pub resolved_target: Option<String>,
482 /// Stable label describing how the target resolved.
483 pub resolved_from: Option<String>,
484 /// Number of returned provider rows.
485 pub returned_provider_count: usize,
486 /// Canonically ordered selected provider rows.
487 pub providers: Vec<IcNodeProviderStatusRow>,
488}
489
490///
491/// IcNodeStatusProjectionError
492///
493/// Failure to resolve or project a requested observed status view.
494///
495
496#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
497pub enum IcNodeStatusProjectionError {
498 /// Snapshot-level counts or canonical ordering do not match raw rows.
499 #[error("invalid observed node-status snapshot: {reason}")]
500 InvalidSnapshot {
501 /// Deterministic validation failure.
502 reason: String,
503 },
504 /// The supplied target is empty after trimming.
505 #[error("{kind} target must not be empty")]
506 EmptyTarget {
507 /// Projection kind being selected.
508 kind: &'static str,
509 },
510 /// No observed identifier matches the supplied target.
511 #[error("{kind} target {target:?} did not match the observed snapshot")]
512 UnknownTarget {
513 /// Projection kind being selected.
514 kind: &'static str,
515 /// Unmatched target text.
516 target: String,
517 },
518 /// More than one observed identifier matches the supplied prefix.
519 #[error("{kind} prefix {prefix:?} is ambiguous; matches: {matches:?}")]
520 AmbiguousTarget {
521 /// Projection kind being selected.
522 kind: &'static str,
523 /// Ambiguous principal prefix.
524 prefix: String,
525 /// Canonically ordered matching principals.
526 matches: Vec<String>,
527 },
528}
529
530///
531/// IcNodeStatusCacheRequest
532///
533/// Stable identity of one network-level observed node-status cache.
534///
535
536#[cfg(feature = "host")]
537#[derive(Clone, Debug, Eq, PartialEq)]
538pub struct IcNodeStatusCacheRequest {
539 /// Root directory containing all `ic-query` caches.
540 pub cache_root: PathBuf,
541 /// Requested network name.
542 pub network: String,
543}
544
545#[cfg(feature = "host")]
546impl IcNodeStatusCacheRequest {
547 /// Construct one observed node-status cache identity.
548 #[must_use]
549 pub fn new(cache_root: impl Into<PathBuf>, network: impl Into<String>) -> Self {
550 Self {
551 cache_root: cache_root.into(),
552 network: network.into(),
553 }
554 }
555}
556
557///
558/// IcNodeStatusRefreshRequest
559///
560/// Settings for one forced observed node-status snapshot refresh.
561///
562
563#[cfg(feature = "host")]
564#[derive(Clone, Debug, Eq, PartialEq)]
565pub struct IcNodeStatusRefreshRequest {
566 /// Stable cache identity.
567 pub cache: IcNodeStatusCacheRequest,
568 /// Explicit Dashboard API base endpoint.
569 pub source_endpoint: String,
570 /// Caller-supplied observation time as Unix seconds.
571 pub now_unix_secs: u64,
572 /// Age after which an abandoned refresh lock can be reclaimed.
573 pub lock_stale_after_seconds: u64,
574}
575
576#[cfg(feature = "host")]
577impl IcNodeStatusRefreshRequest {
578 /// Construct one forced node-status snapshot refresh request.
579 #[must_use]
580 pub fn new(
581 cache_root: impl Into<PathBuf>,
582 network: impl Into<String>,
583 source_endpoint: impl Into<String>,
584 now_unix_secs: u64,
585 lock_stale_after_seconds: u64,
586 ) -> Self {
587 Self {
588 cache: IcNodeStatusCacheRequest::new(cache_root, network),
589 source_endpoint: source_endpoint.into(),
590 now_unix_secs,
591 lock_stale_after_seconds,
592 }
593 }
594}
595
596///
597/// IcNodeStatusReadRequest
598///
599/// Cache-backed status-view request shared by node, Subnet, and provider reports.
600///
601
602#[cfg(feature = "host")]
603#[derive(Clone, Debug, Eq, PartialEq)]
604pub struct IcNodeStatusReadRequest {
605 /// Live and cache settings used when refresh is required.
606 pub refresh: IcNodeStatusRefreshRequest,
607 /// Projection target and attention selection.
608 pub view: IcNodeStatusView,
609 /// Whether to force a live replacement before reading.
610 pub force_refresh: bool,
611}
612
613#[cfg(feature = "host")]
614impl IcNodeStatusReadRequest {
615 /// Construct one stale-refresh status-view request.
616 #[must_use]
617 pub fn new(
618 cache_root: impl Into<PathBuf>,
619 network: impl Into<String>,
620 source_endpoint: impl Into<String>,
621 now_unix_secs: u64,
622 ) -> Self {
623 Self {
624 refresh: IcNodeStatusRefreshRequest::new(
625 cache_root,
626 network,
627 source_endpoint,
628 now_unix_secs,
629 super::DEFAULT_IC_NODE_STATUS_REFRESH_LOCK_STALE_SECONDS,
630 ),
631 view: IcNodeStatusView::attention(),
632 force_refresh: false,
633 }
634 }
635
636 /// Apply one target and attention selection.
637 #[must_use]
638 pub fn with_view(mut self, view: IcNodeStatusView) -> Self {
639 self.view = view;
640 self
641 }
642
643 /// Select a forced refresh before projecting the report.
644 #[must_use]
645 pub const fn with_force_refresh(mut self, force_refresh: bool) -> Self {
646 self.force_refresh = force_refresh;
647 self
648 }
649}
650
651///
652/// IcNodeStatusRefreshReport
653///
654/// Result of atomically replacing the complete observed node-status cache.
655///
656
657#[cfg(feature = "host")]
658#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
659pub struct IcNodeStatusRefreshReport {
660 /// Cache/report schema version.
661 pub schema_version: u32,
662 /// Network identity attached to the replacement.
663 pub network: String,
664 /// Explicit Dashboard endpoint queried.
665 pub source_endpoint: String,
666 /// Canonical UTC collection timestamp.
667 pub fetched_at: String,
668 /// Collector identity.
669 pub fetched_by: String,
670 /// Published cache path.
671 pub cache_path: String,
672 /// Sibling refresh-lock path.
673 pub refresh_lock_path: String,
674 /// Whether an existing complete snapshot was replaced.
675 pub replaced_existing_cache: bool,
676 /// Number of published node rows.
677 pub node_count: usize,
678 /// Published overall and assignment-partitioned status totals.
679 pub counts: IcNodeStatusGroupCounts,
680}
681
682///
683/// IcNodeStatusHostError
684///
685/// Host source, cache, and projection failures for observed node-status reports.
686///
687
688#[cfg(feature = "host")]
689#[derive(Debug, ThisError)]
690pub enum IcNodeStatusHostError {
691 /// The observed Dashboard source supports only public mainnet identity.
692 #[error("observed IC node status supports only the mainnet `ic` network, not {network:?}")]
693 UnsupportedNetwork {
694 /// Unsupported requested network.
695 network: String,
696 },
697 /// Live-source or source-validation failure.
698 #[error(transparent)]
699 Source(#[from] crate::ic::IcHostError),
700 /// Pure projection failure.
701 #[error(transparent)]
702 Projection(#[from] IcNodeStatusProjectionError),
703 /// Strict cache load found no complete snapshot.
704 #[error("observed node-status cache is missing at {}", path.display())]
705 MissingCache {
706 /// Expected cache path.
707 path: PathBuf,
708 },
709 /// Existing cache content could not be read.
710 #[error("failed to read observed node-status cache at {}: {source}", path.display())]
711 ReadCache {
712 /// Cache path that failed.
713 path: PathBuf,
714 /// Underlying filesystem failure.
715 source: std::io::Error,
716 },
717 /// Existing cache content was not valid JSON for the schema.
718 #[error("failed to parse observed node-status cache at {}: {source}", path.display())]
719 ParseCache {
720 /// Cache path that failed.
721 path: PathBuf,
722 /// Underlying JSON failure.
723 source: serde_json::Error,
724 },
725 /// Existing cache uses an unsupported schema version.
726 #[error("observed node-status cache schema {version} is unsupported; expected {expected}")]
727 UnsupportedCacheSchemaVersion {
728 /// Observed schema version.
729 version: u32,
730 /// Required current schema version.
731 expected: u32,
732 },
733 /// Existing cache belongs to another network.
734 #[error("observed node-status cache network is {actual:?}, expected {requested:?}")]
735 CacheNetworkMismatch {
736 /// Requested network.
737 requested: String,
738 /// Network stored by the cache.
739 actual: String,
740 },
741 /// Existing cache snapshot-key identity does not match its path.
742 #[error(
743 "observed node-status cache identity mismatch at {}: {field} is {actual:?}, expected {expected:?}",
744 path.display()
745 )]
746 CacheIdentityMismatch {
747 /// Cache path that failed validation.
748 path: PathBuf,
749 /// Identity field that differs.
750 field: &'static str,
751 /// Required identity value.
752 expected: String,
753 /// Stored identity value.
754 actual: String,
755 },
756 /// Existing cache is structurally readable but semantically invalid.
757 #[error("invalid observed node-status cache at {}: {reason}", path.display())]
758 InvalidCache {
759 /// Cache path that failed validation.
760 path: PathBuf,
761 /// Deterministic validation failure.
762 reason: String,
763 },
764 /// A complete replacement could not be serialized.
765 #[error("failed to serialize observed node-status cache at {}: {source}", path.display())]
766 SerializeCache {
767 /// Target cache path.
768 path: PathBuf,
769 /// Underlying JSON serialization failure.
770 source: serde_json::Error,
771 },
772 /// Shared atomic cache or refresh-lock operation failed.
773 #[error(transparent)]
774 Cache(#[from] crate::cache_file::HostCacheError),
775}