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 = 2;
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}
94
95impl RunManifest {
96 pub fn for_scenario(
97 namespace: impl Into<String>,
98 name: impl Into<String>,
99 version: impl Into<String>,
100 scenario: &Scenario,
101 ) -> Result<Self, CanwuError> {
102 let scenario = ArtifactManifest::for_scenario(namespace, name, version, scenario)?;
103 let run_configuration = default_run_configuration_manifest()?;
104 Ok(Self::Declared {
105 format_version: RUN_MANIFEST_FORMAT_VERSION,
106 scenario,
107 rules: Vec::new(),
108 content: Vec::new(),
109 localization_contracts: Vec::new(),
110 run_configuration: Box::new(run_configuration),
111 sources: Vec::new(),
112 })
113 }
114
115 #[must_use]
116 pub fn declared(scenario: ArtifactManifest, run_configuration: ArtifactManifest) -> Self {
117 Self::Declared {
118 format_version: RUN_MANIFEST_FORMAT_VERSION,
119 scenario,
120 rules: Vec::new(),
121 content: Vec::new(),
122 localization_contracts: Vec::new(),
123 run_configuration: Box::new(run_configuration),
124 sources: Vec::new(),
125 }
126 }
127}
128
129pub(crate) fn canonicalize(manifest: &mut RunManifest) {
130 let RunManifest::Declared {
131 rules,
132 content,
133 localization_contracts,
134 sources,
135 ..
136 } = manifest;
137 rules.sort();
138 content.sort();
139 localization_contracts.sort();
140 sources.sort();
141}
142
143pub(crate) fn validate(
144 manifest: &RunManifest,
145 scenario: Option<&Scenario>,
146) -> Result<(), CanwuError> {
147 match manifest {
148 RunManifest::Declared {
149 format_version,
150 scenario: scenario_manifest,
151 rules,
152 content,
153 localization_contracts,
154 run_configuration,
155 sources,
156 } => {
157 if *format_version != RUN_MANIFEST_FORMAT_VERSION {
158 return invalid_manifest(format!(
159 "run manifest format {format_version} is unsupported"
160 ));
161 }
162 validate_artifact(scenario_manifest, "scenario")?;
163 validate_artifact(run_configuration, "run configuration")?;
164 validate_artifact_list(rules, "rules")?;
165 validate_artifact_list(content, "content")?;
166 validate_artifact_list(localization_contracts, "localization contract")?;
167 validate_artifact_list(sources, "source")?;
168 if let Some(scenario) = scenario
169 && scenario_manifest.semantic_hash != scenario_semantic_hash(scenario)?
170 {
171 return invalid_manifest(
172 "scenario manifest hash does not match the admitted scenario",
173 );
174 }
175 Ok(())
176 }
177 }
178}
179
180pub(crate) fn hash(manifest: &RunManifest) -> Result<String, CanwuError> {
181 canonical_hash("canwu.run-manifest.v1", manifest)
182}
183
184pub(crate) fn validate_run_configuration(
185 manifest: &RunManifest,
186 configuration: &RunConfigurationSnapshot,
187) -> Result<(), CanwuError> {
188 configuration.validate()?;
189 match (manifest, configuration) {
190 (
191 RunManifest::Declared {
192 run_configuration, ..
193 },
194 RunConfigurationSnapshot::Declared(_) | RunConfigurationSnapshot::CompatibilityV1,
195 ) => {
196 if configuration.semantic_hash()?.as_deref()
197 != Some(run_configuration.semantic_hash.as_str())
198 {
199 return invalid_manifest(
200 "run configuration snapshot does not match its manifest identity",
201 );
202 }
203 Ok(())
204 }
205 (
206 RunManifest::Declared {
207 run_configuration, ..
208 },
209 RunConfigurationSnapshot::ManifestOnlyV1,
210 ) => {
211 if run_configuration.semantic_hash == policy::compatibility_configuration_hash()? {
212 return invalid_manifest(
213 "the default run configuration must use compatibility-v1 provenance",
214 );
215 }
216 Ok(())
217 }
218 (RunManifest::Declared { .. }, RunConfigurationSnapshot::LegacyUnspecified) => {
219 invalid_manifest("declared runs cannot use an identity-unbound run configuration")
220 }
221 }
222}
223
224fn scenario_semantic_hash(scenario: &Scenario) -> Result<String, CanwuError> {
225 let mut canonical = scenario.clone();
226 canonical.entities.sort();
227 canonical.entities.dedup();
228 if canonical.entities == super::scenario::legacy_entities(&canonical.world) {
229 canonical.entities.clear();
230 }
231 canonical.world.people.sort_by_key(|value| value.id);
232 canonical.world.governments.sort_by_key(|value| value.id);
233 canonical.world.territories.sort_by_key(|value| value.id);
234 canonical.world.routes.sort_by_key(|value| value.id);
235 canonical.world.armies.sort_by_key(|value| value.id);
236 canonical
237 .domain_records
238 .sort_by(|left, right| left.reference.cmp(&right.reference));
239 canonical_hash("canwu.scenario.v1", &canonical)
240}
241
242fn default_run_configuration_manifest() -> Result<ArtifactManifest, CanwuError> {
243 ArtifactManifest::new(
244 "canwu.core",
245 "default-run-configuration",
246 "1",
247 policy::compatibility_configuration_hash()?,
248 )
249}
250
251fn validate_artifact_list(manifests: &[ArtifactManifest], label: &str) -> Result<(), CanwuError> {
252 let mut previous = None;
253 for manifest in manifests {
254 validate_artifact(manifest, label)?;
255 if previous.is_some_and(|value: &ArtifactManifest| {
256 value >= manifest
257 || (value.namespace == manifest.namespace && value.name == manifest.name)
258 }) {
259 return invalid_manifest(format!(
260 "{label} manifests must have unique identities and canonical order"
261 ));
262 }
263 previous = Some(manifest);
264 }
265 Ok(())
266}
267
268fn validate_artifact(manifest: &ArtifactManifest, label: &str) -> Result<(), CanwuError> {
269 if !canonical_text(&manifest.namespace)
270 || !canonical_text(&manifest.name)
271 || !canonical_text(&manifest.version)
272 || !is_canonical_hash(&manifest.semantic_hash)
273 {
274 return invalid_manifest(format!(
275 "{label} manifests require canonical namespace, name, version, and semantic hash"
276 ));
277 }
278 Ok(())
279}
280
281fn canonical_text(value: &str) -> bool {
282 !value.is_empty() && value == value.trim()
283}
284
285fn invalid_manifest<T>(message: impl Into<String>) -> Result<T, CanwuError> {
286 Err(CanwuError::new(ErrorCode::InvalidRunManifest, message))
287}