1use super::{
2 CanwuError, ErrorCode, RunConfiguration, RunConfigurationSnapshot, Scenario, canonical_hash,
3 is_canonical_hash, policy,
4};
5use serde::{Deserialize, Serialize};
6
7pub const RUN_MANIFEST_FORMAT_VERSION: u32 = 1;
8
9#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
12pub struct ArtifactManifest {
13 pub namespace: String,
14 pub name: String,
15 pub version: String,
16 pub semantic_hash: String,
17}
18
19impl ArtifactManifest {
20 pub fn new(
21 namespace: impl Into<String>,
22 name: impl Into<String>,
23 version: impl Into<String>,
24 semantic_hash: impl Into<String>,
25 ) -> Result<Self, CanwuError> {
26 let manifest = Self {
27 namespace: namespace.into(),
28 name: name.into(),
29 version: version.into(),
30 semantic_hash: semantic_hash.into(),
31 };
32 validate_artifact(&manifest, "artifact")?;
33 Ok(manifest)
34 }
35
36 pub fn from_bytes(
37 namespace: impl Into<String>,
38 name: impl Into<String>,
39 version: impl Into<String>,
40 bytes: &[u8],
41 ) -> Result<Self, CanwuError> {
42 let mut hasher = blake3::Hasher::new();
43 hasher.update(b"canwu.artifact-bytes.v1");
44 hasher.update(&[0]);
45 hasher.update(bytes);
46 Self::new(
47 namespace,
48 name,
49 version,
50 hasher.finalize().to_hex().to_string(),
51 )
52 }
53
54 pub fn for_scenario(
55 namespace: impl Into<String>,
56 name: impl Into<String>,
57 version: impl Into<String>,
58 scenario: &Scenario,
59 ) -> Result<Self, CanwuError> {
60 Self::new(namespace, name, version, scenario_semantic_hash(scenario)?)
61 }
62
63 pub fn for_run_configuration(
64 namespace: impl Into<String>,
65 name: impl Into<String>,
66 version: impl Into<String>,
67 configuration: &RunConfiguration,
68 ) -> Result<Self, CanwuError> {
69 let mut configuration = configuration.clone();
70 configuration.canonicalize();
71 configuration.validate()?;
72 Self::new(namespace, name, version, configuration.semantic_hash()?)
73 }
74}
75
76#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
77#[serde(tag = "provenance", rename_all = "snake_case")]
78pub enum RunManifest {
80 Declared {
81 format_version: u32,
82 scenario: ArtifactManifest,
83 #[serde(default)]
84 rules: Vec<ArtifactManifest>,
85 #[serde(default)]
86 content: Vec<ArtifactManifest>,
87 #[serde(default)]
88 localization_contracts: Vec<ArtifactManifest>,
89 run_configuration: Box<ArtifactManifest>,
90 #[serde(default)]
91 sources: Vec<ArtifactManifest>,
92 },
93 MigratedLegacy {
94 format_version: u32,
95 source_engine_version: String,
96 source_snapshot_format: u32,
97 checkpoint_hash: String,
98 },
99}
100
101impl RunManifest {
102 pub fn for_scenario(
103 namespace: impl Into<String>,
104 name: impl Into<String>,
105 version: impl Into<String>,
106 scenario: &Scenario,
107 ) -> Result<Self, CanwuError> {
108 let scenario = ArtifactManifest::for_scenario(namespace, name, version, scenario)?;
109 let run_configuration = default_run_configuration_manifest()?;
110 Ok(Self::Declared {
111 format_version: RUN_MANIFEST_FORMAT_VERSION,
112 scenario,
113 rules: Vec::new(),
114 content: Vec::new(),
115 localization_contracts: Vec::new(),
116 run_configuration: Box::new(run_configuration),
117 sources: Vec::new(),
118 })
119 }
120
121 #[must_use]
122 pub fn declared(scenario: ArtifactManifest, run_configuration: ArtifactManifest) -> Self {
123 Self::Declared {
124 format_version: RUN_MANIFEST_FORMAT_VERSION,
125 scenario,
126 rules: Vec::new(),
127 content: Vec::new(),
128 localization_contracts: Vec::new(),
129 run_configuration: Box::new(run_configuration),
130 sources: Vec::new(),
131 }
132 }
133
134 pub(crate) fn migrated_legacy(
135 source_engine_version: String,
136 source_snapshot_format: u32,
137 checkpoint_hash: String,
138 ) -> Self {
139 Self::MigratedLegacy {
140 format_version: RUN_MANIFEST_FORMAT_VERSION,
141 source_engine_version,
142 source_snapshot_format,
143 checkpoint_hash,
144 }
145 }
146}
147
148pub(crate) fn canonicalize(manifest: &mut RunManifest) {
149 if let RunManifest::Declared {
150 rules,
151 content,
152 localization_contracts,
153 sources,
154 ..
155 } = manifest
156 {
157 rules.sort();
158 content.sort();
159 localization_contracts.sort();
160 sources.sort();
161 }
162}
163
164pub(crate) fn validate(
165 manifest: &RunManifest,
166 scenario: Option<&Scenario>,
167 allow_legacy: bool,
168) -> Result<(), CanwuError> {
169 match manifest {
170 RunManifest::Declared {
171 format_version,
172 scenario: scenario_manifest,
173 rules,
174 content,
175 localization_contracts,
176 run_configuration,
177 sources,
178 } => {
179 if *format_version != RUN_MANIFEST_FORMAT_VERSION {
180 return invalid_manifest(format!(
181 "run manifest format {format_version} is unsupported"
182 ));
183 }
184 validate_artifact(scenario_manifest, "scenario")?;
185 validate_artifact(run_configuration, "run configuration")?;
186 validate_artifact_list(rules, "rules")?;
187 validate_artifact_list(content, "content")?;
188 validate_artifact_list(localization_contracts, "localization contract")?;
189 validate_artifact_list(sources, "source")?;
190 if let Some(scenario) = scenario
191 && scenario_manifest.semantic_hash != scenario_semantic_hash(scenario)?
192 {
193 return invalid_manifest(
194 "scenario manifest hash does not match the admitted scenario",
195 );
196 }
197 Ok(())
198 }
199 RunManifest::MigratedLegacy {
200 format_version,
201 source_engine_version,
202 source_snapshot_format,
203 checkpoint_hash,
204 } => {
205 if !allow_legacy {
206 return invalid_manifest(
207 "new simulations require a declared scenario and run configuration manifest",
208 );
209 }
210 if *format_version != RUN_MANIFEST_FORMAT_VERSION
211 || source_engine_version.trim().is_empty()
212 || source_engine_version != source_engine_version.trim()
213 || !matches!(source_snapshot_format, 2 | 3)
214 || !is_canonical_hash(checkpoint_hash)
215 {
216 return invalid_manifest("migrated legacy run provenance is invalid");
217 }
218 Ok(())
219 }
220 }
221}
222
223pub(crate) fn hash(manifest: &RunManifest) -> Result<String, CanwuError> {
224 canonical_hash("canwu.run-manifest.v1", manifest)
225}
226
227pub(crate) fn validate_run_configuration(
228 manifest: &RunManifest,
229 configuration: &RunConfigurationSnapshot,
230) -> Result<(), CanwuError> {
231 configuration.validate()?;
232 match (manifest, configuration) {
233 (
234 RunManifest::Declared {
235 run_configuration, ..
236 },
237 RunConfigurationSnapshot::Declared(_) | RunConfigurationSnapshot::CompatibilityV1,
238 ) => {
239 if configuration.semantic_hash()?.as_deref()
240 != Some(run_configuration.semantic_hash.as_str())
241 {
242 return invalid_manifest(
243 "run configuration snapshot does not match its manifest identity",
244 );
245 }
246 Ok(())
247 }
248 (
249 RunManifest::Declared {
250 run_configuration, ..
251 },
252 RunConfigurationSnapshot::ManifestOnlyV1,
253 ) => {
254 if run_configuration.semantic_hash == policy::compatibility_configuration_hash()? {
255 return invalid_manifest(
256 "the default run configuration must use compatibility-v1 provenance",
257 );
258 }
259 Ok(())
260 }
261 (RunManifest::MigratedLegacy { .. }, RunConfigurationSnapshot::LegacyUnspecified) => Ok(()),
262 (RunManifest::Declared { .. }, RunConfigurationSnapshot::LegacyUnspecified) => {
263 invalid_manifest("declared runs cannot use an identity-unbound run configuration")
264 }
265 (RunManifest::MigratedLegacy { .. }, _) => invalid_manifest(
266 "legacy migrations cannot silently inherit a current run configuration",
267 ),
268 }
269}
270
271fn scenario_semantic_hash(scenario: &Scenario) -> Result<String, CanwuError> {
272 let mut canonical = scenario.clone();
273 canonical.world.people.sort_by_key(|value| value.id);
274 canonical.world.governments.sort_by_key(|value| value.id);
275 canonical.world.territories.sort_by_key(|value| value.id);
276 canonical.world.routes.sort_by_key(|value| value.id);
277 canonical.world.armies.sort_by_key(|value| value.id);
278 canonical
279 .domain_records
280 .sort_by(|left, right| left.reference.cmp(&right.reference));
281 canonical_hash("canwu.scenario.v1", &canonical)
282}
283
284fn default_run_configuration_manifest() -> Result<ArtifactManifest, CanwuError> {
285 ArtifactManifest::new(
286 "canwu.core",
287 "default-run-configuration",
288 "1",
289 policy::compatibility_configuration_hash()?,
290 )
291}
292
293fn validate_artifact_list(manifests: &[ArtifactManifest], label: &str) -> Result<(), CanwuError> {
294 let mut previous = None;
295 for manifest in manifests {
296 validate_artifact(manifest, label)?;
297 if previous.is_some_and(|value: &ArtifactManifest| {
298 value >= manifest
299 || (value.namespace == manifest.namespace && value.name == manifest.name)
300 }) {
301 return invalid_manifest(format!(
302 "{label} manifests must have unique identities and canonical order"
303 ));
304 }
305 previous = Some(manifest);
306 }
307 Ok(())
308}
309
310fn validate_artifact(manifest: &ArtifactManifest, label: &str) -> Result<(), CanwuError> {
311 if !canonical_text(&manifest.namespace)
312 || !canonical_text(&manifest.name)
313 || !canonical_text(&manifest.version)
314 || !is_canonical_hash(&manifest.semantic_hash)
315 {
316 return invalid_manifest(format!(
317 "{label} manifests require canonical namespace, name, version, and semantic hash"
318 ));
319 }
320 Ok(())
321}
322
323fn canonical_text(value: &str) -> bool {
324 !value.is_empty() && value == value.trim()
325}
326
327fn invalid_manifest<T>(message: impl Into<String>) -> Result<T, CanwuError> {
328 Err(CanwuError::new(ErrorCode::InvalidRunManifest, message))
329}