Skip to main content

phoxal_bundle/
error.rs

1//! Bundle document, selection, and I/O failures.
2
3use std::path::PathBuf;
4
5use phoxal_model::component::capability::MotorCommand;
6use phoxal_model::identity::{CapabilityRef, ComponentInstanceId};
7use phoxal_model::{AssetId, Clock};
8use phoxal_runtime_contract::identity::{ParticipantArtifactId, ParticipantId};
9use phoxal_runtime_contract::metadata::ParticipantKind;
10use phoxal_runtime_contract::metadata::ParticipantRequirement;
11use phoxal_runtime_contract::version::FrameworkVersion;
12
13use crate::{BundlePath, BundlePathError, DigestError, ParticipantClock, Sha256Digest};
14
15/// Why a persisted runtime document is not a valid execution plan.
16#[derive(Clone, Debug, thiserror::Error)]
17pub enum DocumentError {
18    #[error(
19        "runtime graph has {count} participants; limit is {}",
20        crate::MAX_RUNTIME_PARTICIPANTS
21    )]
22    TooManyParticipants { count: usize },
23    /// Artifacts may mix trains within one compatibility line; a runtime
24    /// spanning two lines has no valid launch, because its participants do not
25    /// speak each other's contracts.
26    #[error(
27        "artifact '{artifact}' was built from framework {actual}, but this runtime is on the {} \
28         line (from framework {expected})",
29        expected.compatibility_line()
30    )]
31    MixedFrameworkLine {
32        artifact: ParticipantArtifactId,
33        expected: FrameworkVersion,
34        actual: FrameworkVersion,
35    },
36    #[error("duplicate participant id '{id}'")]
37    DuplicateParticipant { id: ParticipantId },
38    #[error("duplicate participant binary path '{path}'")]
39    DuplicateBinary { path: BundlePath },
40    #[error("participant '{participant}' references unknown artifact '{artifact}'")]
41    UnknownArtifact {
42        participant: ParticipantId,
43        artifact: ParticipantArtifactId,
44    },
45    #[error("artifact '{artifact}' contract names '{contract}'")]
46    ArtifactContractMismatch {
47        artifact: ParticipantArtifactId,
48        contract: ParticipantArtifactId,
49    },
50    #[error("artifact '{artifact}' path is outside bin/: {path}")]
51    ArtifactOutsideBin {
52        artifact: ParticipantArtifactId,
53        path: BundlePath,
54    },
55    #[error("brain artifact id is '{actual}', expected 'brain'")]
56    BrainArtifactId { actual: ParticipantArtifactId },
57    #[error("brain artifact path is '{actual}', expected 'bin/brain'")]
58    BrainArtifactPath { actual: BundlePath },
59    #[error("artifact '{artifact}' is not selected by any runtime participant")]
60    UnusedArtifact { artifact: ParticipantArtifactId },
61    #[error("participant '{participant}' names unknown component instance '{component_instance}'")]
62    UnknownComponent {
63        participant: ParticipantId,
64        component_instance: ComponentInstanceId,
65    },
66    #[error("driver participant '{participant}' has no component binding")]
67    MissingDriverComponent { participant: ParticipantId },
68    #[error(
69        "{kind:?} participant '{participant}' cannot bind component instance '{component_instance}'"
70    )]
71    UnexpectedComponent {
72        participant: ParticipantId,
73        kind: ParticipantKind,
74        component_instance: ComponentInstanceId,
75    },
76    #[error("runtime graph has no mandatory 'brain' participant")]
77    MissingBrain,
78    #[error("brain artifact is mounted as participant '{actual}', expected 'brain'")]
79    BrainIdMismatch { actual: ParticipantId },
80    #[error("runtime graph contains more than one brain participant")]
81    DuplicateBrain,
82    #[error("simulated runtime graph has no simulator world authority")]
83    MissingSimulator,
84    #[error("simulated runtime graph contains more than one simulator world authority")]
85    DuplicateSimulator,
86    #[error(
87        "participant '{participant}' clock {participant_clock:?} conflicts with robot clock {robot:?}"
88    )]
89    ClockMismatch {
90        participant: ParticipantId,
91        robot: Clock,
92        participant_clock: ParticipantClock,
93    },
94    #[error(
95        "{kind:?} participant '{participant}' clock {participant_clock:?} is incompatible with robot clock {robot:?}"
96    )]
97    ExecutionModeMismatch {
98        participant: ParticipantId,
99        kind: ParticipantKind,
100        robot: Clock,
101        participant_clock: ParticipantClock,
102    },
103    #[error(
104        "artifact '{artifact}' requires {requirement:?}, but the robot uses {actual} kinematics"
105    )]
106    RequirementKinematicsMismatch {
107        artifact: ParticipantArtifactId,
108        requirement: ParticipantRequirement,
109        actual: phoxal_model::robot::KinematicKind,
110    },
111    #[error("artifact '{artifact}' requires at least one {side} actuator")]
112    RequirementActuatorListEmpty {
113        artifact: ParticipantArtifactId,
114        side: &'static str,
115    },
116    #[error("artifact '{artifact}' requirement could not resolve actuator '{actuator}': {error}")]
117    RequirementActuatorInvalid {
118        artifact: ParticipantArtifactId,
119        actuator: CapabilityRef,
120        error: String,
121    },
122    #[error(
123        "artifact '{artifact}' actuator '{actuator}' is configured for {actual:?}, but the binary requires {expected:?}"
124    )]
125    RequirementMotorModeMismatch {
126        artifact: ParticipantArtifactId,
127        actuator: CapabilityRef,
128        expected: MotorCommand,
129        actual: MotorCommand,
130    },
131    #[error("artifact '{artifact}' has an invalid config schema: {error}")]
132    InvalidConfigSchema {
133        artifact: ParticipantArtifactId,
134        error: String,
135    },
136    #[error("participant '{participant}' config does not match its binary schema: {error}")]
137    InvalidConfig {
138        participant: ParticipantId,
139        error: String,
140    },
141    #[error("asset id '{id}' is persisted at the wrong path '{path}'", id = id.as_str())]
142    AssetPathMismatch { id: AssetId, path: BundlePath },
143    #[error("asset path is outside assets/: {path}")]
144    AssetOutsideAssets { path: BundlePath },
145    #[error("duplicate asset id '{id}'", id = id.as_str())]
146    DuplicateAssetId { id: AssetId },
147    #[error("duplicate asset path '{path}'")]
148    DuplicateAssetPath { path: BundlePath },
149    #[error("supplied asset bytes do not match the persisted asset index")]
150    AssetIndexMismatch,
151    #[error("supplied executable sources do not match the persisted artifact set")]
152    BinaryIndexMismatch,
153    #[error("bundle path is not valid: {0}")]
154    Path(#[from] BundlePathError),
155}
156
157/// Why an exact participant selection failed.
158#[derive(Clone, Debug, thiserror::Error)]
159pub enum SelectionError {
160    #[error("runtime bundle has no participant '{requested}'")]
161    Unknown { requested: ParticipantId },
162    #[error("participant '{participant}' references missing artifact '{artifact}'")]
163    MissingArtifact {
164        participant: ParticipantId,
165        artifact: ParticipantArtifactId,
166    },
167}
168
169/// Why reading or validating a bundle failed.
170#[derive(Debug, thiserror::Error)]
171pub enum BundleError {
172    #[error("failed to resolve bundle root {}: {source}", path.display())]
173    Root {
174        path: PathBuf,
175        #[source]
176        source: std::io::Error,
177    },
178    #[error("bundle root is not a directory: {0}")]
179    NotDirectory(PathBuf),
180    #[error("bundle target already exists: {0}")]
181    TargetExists(PathBuf),
182    #[error("failed to read runtime document {}: {source}", path.display())]
183    ReadDocument {
184        path: PathBuf,
185        #[source]
186        source: std::io::Error,
187    },
188    #[error("runtime document is not valid JSON: {0}")]
189    DocumentJson(#[from] serde_json::Error),
190    #[error(transparent)]
191    Document(#[from] DocumentError),
192    #[error("bundle contains unsupported filesystem entry {path}")]
193    UnsupportedEntry { path: PathBuf },
194    #[error("bundle executable is not executable: {path}")]
195    NotExecutable { path: PathBuf },
196    #[error("bundle contains unexpected file {path}")]
197    UnexpectedFile { path: PathBuf },
198    #[error("bundle contains unindexed directory {path}")]
199    UnindexedDirectory { path: PathBuf },
200    #[error("bundle is missing required file {path}")]
201    MissingFile { path: PathBuf },
202    #[error("failed to read bundle file {path}: {source}", path = path.display())]
203    ReadFile {
204        path: PathBuf,
205        #[source]
206        source: std::io::Error,
207    },
208    #[error("bundle file {path} changed after indexing: expected {expected}, found {actual}")]
209    Integrity {
210        path: PathBuf,
211        expected: Sha256Digest,
212        actual: Sha256Digest,
213    },
214    #[error("bundle file {path} has size {actual}, expected {expected}")]
215    Size {
216        path: PathBuf,
217        expected: u64,
218        actual: u64,
219    },
220    #[error("asset '{id}' is not declared by runtime.json", id = id.as_str())]
221    UndeclaredAsset { id: AssetId },
222    #[error(transparent)]
223    Path(#[from] BundlePathError),
224    #[error(transparent)]
225    Digest(#[from] DigestError),
226    #[error(transparent)]
227    Selection(#[from] SelectionError),
228}