Skip to main content

knowledge_base_extension_framework/
manifest.rs

1//! Versioned, repository-local configuration for compiled extensions.
2
3use crate::bindings::{BindingValue, ResolvedBindings};
4use crate::contracts::{BindingKey, BindingKind, BindingReference, ContractVersion, ExtensionId};
5use crate::error::FrameworkError;
6use crate::ontology::{OntologyContractDiagnostic, verify_ontology_contracts};
7use crate::registry::{ActiveExtensions, ExtensionRegistry};
8use knowledge_base_models::{EntityTypeId, PropertyId};
9use knowledge_base_snapshot::{Error as SnapshotError, RepositorySnapshot};
10use serde::{Deserialize, Serialize};
11use std::collections::{BTreeMap, BTreeSet};
12use std::fmt;
13use std::fs;
14use std::io;
15use std::path::{Path, PathBuf};
16
17pub const MANIFEST_FILE_NAME: &str = "extensions.yaml";
18pub const MANIFEST_VERSION: u32 = 1;
19
20/// The versioned extension configuration stored in a repository's `extensions.yaml`.
21#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
22#[serde(deny_unknown_fields)]
23pub struct ExtensionManifest {
24    pub version: u32,
25    #[serde(default)]
26    pub extensions: BTreeMap<ExtensionId, ManifestExtension>,
27}
28
29/// The contract and semantic bindings activated for one extension.
30#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
31#[serde(deny_unknown_fields)]
32pub struct ManifestExtension {
33    pub contract: ContractVersion,
34    #[serde(default)]
35    pub entity_types: BTreeMap<BindingKey, EntityTypeId>,
36    #[serde(default)]
37    pub properties: BTreeMap<BindingKey, PropertyId>,
38}
39
40/// One stable semantic error found while validating a syntactically valid manifest.
41#[derive(Clone, Debug, Eq, PartialEq)]
42pub enum ManifestDiagnostic {
43    UnsupportedVersion {
44        version: u32,
45    },
46    UnavailableExtension {
47        extension: ExtensionId,
48    },
49    UnsupportedContract {
50        extension: ExtensionId,
51        declared: ContractVersion,
52        available: ContractVersion,
53    },
54    MissingDependency {
55        extension: ExtensionId,
56        dependency: ExtensionId,
57    },
58    DuplicateBinding {
59        binding: BindingReference,
60    },
61    UndeclaredBinding {
62        binding: BindingReference,
63    },
64    BindingKindMismatch {
65        binding: BindingReference,
66        expected: BindingKind,
67        actual: BindingKind,
68    },
69    MissingBinding {
70        binding: BindingReference,
71    },
72    MissingEntityType {
73        binding: BindingReference,
74        id: EntityTypeId,
75    },
76    MissingProperty {
77        binding: BindingReference,
78        id: PropertyId,
79    },
80}
81
82impl fmt::Display for ManifestDiagnostic {
83    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
84        match self {
85            Self::UnsupportedVersion { version } => write!(formatter, "unsupported extension manifest version {version}; expected {MANIFEST_VERSION}"),
86            Self::UnavailableExtension { extension } => write!(formatter, "extension {extension} is not compiled into this distribution"),
87            Self::UnsupportedContract { extension, declared, available } => write!(
88                formatter,
89                "extension {extension} declares contract {declared}, but compiled implementation supports {available}"
90            ),
91            Self::MissingDependency { extension, dependency } => write!(formatter, "extension {extension} requires declared dependency {dependency}"),
92            Self::DuplicateBinding { binding } => write!(formatter, "binding {binding} is declared in more than one typed segment"),
93            Self::UndeclaredBinding { binding } => write!(formatter, "extension does not declare binding {binding}"),
94            Self::BindingKindMismatch { binding, expected, actual } => write!(formatter, "binding {binding} is declared as {actual:?}, but extension requires {expected:?}"),
95            Self::MissingBinding { binding } => write!(formatter, "manifest is missing required binding {binding}"),
96            Self::MissingEntityType { binding, id } => write!(formatter, "binding {binding} resolves missing entity type {id}"),
97            Self::MissingProperty { binding, id } => write!(formatter, "binding {binding} resolves missing property {id}"),
98        }
99    }
100}
101
102/// Errors loading, activating, or resolving a repository extension manifest.
103#[derive(Debug)]
104pub enum ManifestError {
105    Read { path: PathBuf, source: io::Error },
106    Parse { path: PathBuf, source: serde_yaml::Error },
107    Diagnostics { path: PathBuf, diagnostics: Vec<ManifestDiagnostic> },
108    OntologyContracts { path: PathBuf, diagnostics: Vec<OntologyContractDiagnostic> },
109    Framework(FrameworkError),
110    Snapshot(SnapshotError),
111}
112
113impl fmt::Display for ManifestError {
114    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
115        match self {
116            Self::Read { path, source } => write!(formatter, "cannot read extension manifest {}: {source}", path.display()),
117            Self::Parse { path, source } => write!(formatter, "cannot parse extension manifest {}: {source}", path.display()),
118            Self::Diagnostics { path, diagnostics } => write!(
119                formatter,
120                "invalid extension manifest {}: {}",
121                path.display(),
122                diagnostics.iter().map(ToString::to_string).collect::<Vec<_>>().join("; ")
123            ),
124            Self::OntologyContracts { path, diagnostics } => write!(
125                formatter,
126                "invalid extension ontology contracts for {}: {}",
127                path.display(),
128                diagnostics.iter().map(ToString::to_string).collect::<Vec<_>>().join("; ")
129            ),
130            Self::Framework(source) => source.fmt(formatter),
131            Self::Snapshot(source) => source.fmt(formatter),
132        }
133    }
134}
135
136impl std::error::Error for ManifestError {
137    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
138        match self {
139            Self::Read { source, .. } => Some(source),
140            Self::Parse { source, .. } => Some(source),
141            Self::Framework(source) => Some(source),
142            Self::Snapshot(source) => Some(source),
143            Self::Diagnostics { .. } | Self::OntologyContracts { .. } => None,
144        }
145    }
146}
147
148/// The dependency-ordered extensions and typed bindings enabled for one repository.
149pub struct ManifestActivation {
150    active: ActiveExtensions,
151    bindings: ResolvedBindings,
152}
153
154impl fmt::Debug for ManifestActivation {
155    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
156        formatter
157            .debug_struct("ManifestActivation")
158            .field("active_extension_count", &self.active.extensions().len())
159            .field("binding_count", &self.bindings.values.len())
160            .finish()
161    }
162}
163
164impl ManifestActivation {
165    pub fn active(&self) -> &ActiveExtensions {
166        &self.active
167    }
168
169    pub fn bindings(&self) -> &ResolvedBindings {
170        &self.bindings
171    }
172}
173
174impl ExtensionManifest {
175    pub fn path(root: impl AsRef<Path>) -> PathBuf {
176        root.as_ref().join(MANIFEST_FILE_NAME)
177    }
178
179    /// Loads `extensions.yaml` before any repository resources are opened.
180    pub fn load(root: impl AsRef<Path>) -> Result<Self, ManifestError> {
181        let path = Self::path(root);
182        let source = fs::read_to_string(&path).map_err(|source| ManifestError::Read { path: path.clone(), source })?;
183        serde_yaml::from_str(&source).map_err(|source| ManifestError::Parse { path, source })
184    }
185
186    /// Loads, validates, activates, and resolves bindings against repository ontology records.
187    pub fn load_and_activate(root: impl AsRef<Path>, registry: &ExtensionRegistry) -> Result<ManifestActivation, ManifestError> {
188        let root = root.as_ref();
189        Self::load(root)?.activate(root, registry)
190    }
191
192    /// Activates this manifest, loading repository resources only after manifest checks succeed.
193    pub fn activate(&self, root: impl AsRef<Path>, registry: &ExtensionRegistry) -> Result<ManifestActivation, ManifestError> {
194        let root = root.as_ref();
195        let path = Self::path(root);
196        let mut diagnostics = Vec::new();
197        if self.version != MANIFEST_VERSION {
198            diagnostics.push(ManifestDiagnostic::UnsupportedVersion { version: self.version });
199        }
200
201        let mut available = BTreeSet::new();
202        for (id, extension) in &self.extensions {
203            match registry.metadata(id) {
204                None => diagnostics.push(ManifestDiagnostic::UnavailableExtension { extension: id.clone() }),
205                Some(metadata) if metadata.contract != extension.contract => diagnostics.push(ManifestDiagnostic::UnsupportedContract {
206                    extension: id.clone(),
207                    declared: extension.contract,
208                    available: metadata.contract,
209                }),
210                Some(_) => {
211                    available.insert(id.clone());
212                }
213            }
214        }
215
216        for id in &available {
217            let metadata = registry.metadata(id).expect("available extensions have metadata");
218            for dependency in &metadata.dependencies {
219                if !self.extensions.contains_key(&dependency.id) {
220                    diagnostics.push(ManifestDiagnostic::MissingDependency {
221                        extension: id.clone(),
222                        dependency: dependency.id.clone(),
223                    });
224                }
225            }
226        }
227
228        let mut values = BTreeMap::new();
229        for (id, extension) in &self.extensions {
230            let Some(metadata) = registry.metadata(id) else {
231                continue;
232            };
233            for (key, value) in &extension.entity_types {
234                let reference = BindingReference::new(id.clone(), key.clone());
235                validate_binding(metadata, &reference, BindingValue::EntityType(value.clone()), &mut values, &mut diagnostics);
236            }
237            for (key, value) in &extension.properties {
238                let reference = BindingReference::new(id.clone(), key.clone());
239                validate_binding(metadata, &reference, BindingValue::Property(value.clone()), &mut values, &mut diagnostics);
240            }
241            let mut declarations = metadata.bindings.iter().collect::<Vec<_>>();
242            declarations.sort_by(|left, right| left.key.cmp(&right.key));
243            for declaration in declarations {
244                let reference = BindingReference::new(id.clone(), declaration.key.clone());
245                if !values.contains_key(&reference) {
246                    diagnostics.push(ManifestDiagnostic::MissingBinding { binding: reference });
247                }
248            }
249        }
250
251        if !diagnostics.is_empty() {
252            return Err(ManifestError::Diagnostics { path, diagnostics });
253        }
254
255        let active = registry.resolve_active(self.extensions.keys().cloned()).map_err(ManifestError::Framework)?;
256        let bindings = active.resolve_bindings(values).map_err(ManifestError::Framework)?;
257        let snapshot = RepositorySnapshot::load(root).map_err(ManifestError::Snapshot)?;
258        let mut diagnostics = Vec::new();
259        for (binding, value) in &bindings.values {
260            match value {
261                BindingValue::EntityType(id) if !snapshot.entity_types().contains_key(id) => {
262                    diagnostics.push(ManifestDiagnostic::MissingEntityType {
263                        binding: binding.clone(),
264                        id: id.clone(),
265                    });
266                }
267                BindingValue::Property(id) if !snapshot.properties().contains_key(id) => {
268                    diagnostics.push(ManifestDiagnostic::MissingProperty {
269                        binding: binding.clone(),
270                        id: id.clone(),
271                    });
272                }
273                _ => {}
274            }
275        }
276        if !diagnostics.is_empty() {
277            return Err(ManifestError::Diagnostics {
278                path: Self::path(root),
279                diagnostics,
280            });
281        }
282        let diagnostics = verify_ontology_contracts(&snapshot, &active, &bindings);
283        if !diagnostics.is_empty() {
284            return Err(ManifestError::OntologyContracts {
285                path: Self::path(root),
286                diagnostics,
287            });
288        }
289        Ok(ManifestActivation { active, bindings })
290    }
291}
292
293fn validate_binding(
294    metadata: &crate::contracts::ExtensionMetadata,
295    reference: &BindingReference,
296    value: BindingValue,
297    values: &mut BTreeMap<BindingReference, BindingValue>,
298    diagnostics: &mut Vec<ManifestDiagnostic>,
299) {
300    let Some(declaration) = metadata.bindings.iter().find(|declaration| declaration.key == *reference.key()) else {
301        diagnostics.push(ManifestDiagnostic::UndeclaredBinding { binding: reference.clone() });
302        return;
303    };
304    if declaration.kind != value.kind() {
305        diagnostics.push(ManifestDiagnostic::BindingKindMismatch {
306            binding: reference.clone(),
307            expected: declaration.kind,
308            actual: value.kind(),
309        });
310        return;
311    }
312    if values.insert(reference.clone(), value).is_some() {
313        diagnostics.push(ManifestDiagnostic::DuplicateBinding { binding: reference.clone() });
314    }
315}