Skip to main content

canwu_sim/runtime/
manifest.rs

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/// Stable identity for a scenario, ruleset, content pack, run policy,
10/// localization contract, or source ledger.
11#[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")]
78/// The exact non-executable environment bound to a simulation run.
79pub 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}