231 lines
6.9 KiB
Rust
231 lines
6.9 KiB
Rust
use std::collections::HashMap;
|
|
use std::fs;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use thiserror::Error;
|
|
use zeroclaw::skills::{load_skills_from_directory, Skill};
|
|
|
|
use crate::scene_contract::manifest::{
|
|
SceneManifest, SCENE_MANIFEST_FILE_NAME, SUPPORTED_SCENE_CATEGORY_V1, SUPPORTED_SCENE_KIND_V1,
|
|
SUPPORTED_SCHEMA_VERSION_V1,
|
|
};
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct SceneRegistryEntry {
|
|
pub manifest: SceneManifest,
|
|
pub skill_root: PathBuf,
|
|
}
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum SceneRegistryError {
|
|
#[error("failed to read skills directory {path}: {source}")]
|
|
ReadSkillsDir {
|
|
path: PathBuf,
|
|
#[source]
|
|
source: std::io::Error,
|
|
},
|
|
#[error("failed to read scene manifest {path}: {source}")]
|
|
ReadManifest {
|
|
path: PathBuf,
|
|
#[source]
|
|
source: std::io::Error,
|
|
},
|
|
#[error("failed to parse scene manifest {path}: {source}")]
|
|
ParseManifest {
|
|
path: PathBuf,
|
|
#[source]
|
|
source: toml::de::Error,
|
|
},
|
|
#[error(
|
|
"scene manifest {path} declares unsupported schema_version {version}; only {supported} is supported in v1"
|
|
)]
|
|
UnsupportedSchemaVersion {
|
|
path: PathBuf,
|
|
version: String,
|
|
supported: &'static str,
|
|
},
|
|
#[error("scene manifest {path} declares unsupported kind {kind}; only {supported} is supported in v1")]
|
|
UnsupportedSceneKind {
|
|
path: PathBuf,
|
|
kind: String,
|
|
supported: &'static str,
|
|
},
|
|
#[error(
|
|
"scene manifest {path} declares unsupported category {category}; only {supported} is supported in v1"
|
|
)]
|
|
UnsupportedSceneCategory {
|
|
path: PathBuf,
|
|
category: String,
|
|
supported: &'static str,
|
|
},
|
|
#[error(
|
|
"scene manifest {path} declares skill {manifest_skill}, but containing skill package is {package_skill}"
|
|
)]
|
|
SkillPackageMismatch {
|
|
path: PathBuf,
|
|
manifest_skill: String,
|
|
package_skill: String,
|
|
},
|
|
#[error("scene manifest {path} points to missing skill package {skill}")]
|
|
MissingSkill { path: PathBuf, skill: String },
|
|
#[error("scene manifest {path} points to tool {tool} that is missing from skill {skill}")]
|
|
MissingTool {
|
|
path: PathBuf,
|
|
skill: String,
|
|
tool: String,
|
|
},
|
|
#[error("scene id {scene_id} is declared twice: {first_path} and {second_path}")]
|
|
DuplicateSceneId {
|
|
scene_id: String,
|
|
first_path: PathBuf,
|
|
second_path: PathBuf,
|
|
},
|
|
}
|
|
|
|
pub fn load_scene_registry(
|
|
skills_dir: &Path,
|
|
) -> Result<Vec<SceneRegistryEntry>, SceneRegistryError> {
|
|
if !skills_dir.exists() {
|
|
return Ok(Vec::new());
|
|
}
|
|
|
|
let mut skill_roots = Vec::new();
|
|
for entry in fs::read_dir(skills_dir).map_err(|source| SceneRegistryError::ReadSkillsDir {
|
|
path: skills_dir.to_path_buf(),
|
|
source,
|
|
})? {
|
|
let entry = entry.map_err(|source| SceneRegistryError::ReadSkillsDir {
|
|
path: skills_dir.to_path_buf(),
|
|
source,
|
|
})?;
|
|
let path = entry.path();
|
|
if path.is_dir() {
|
|
skill_roots.push(path);
|
|
}
|
|
}
|
|
skill_roots.sort();
|
|
|
|
let skills_by_root = index_skills_by_root(skills_dir);
|
|
let mut scene_ids = HashMap::new();
|
|
let mut registry = Vec::new();
|
|
|
|
for skill_root in skill_roots {
|
|
let manifest_path = skill_root.join(SCENE_MANIFEST_FILE_NAME);
|
|
if !manifest_path.exists() {
|
|
continue;
|
|
}
|
|
|
|
let manifest = load_manifest(&manifest_path)?;
|
|
validate_manifest(&manifest, &manifest_path, &skill_root, &skills_by_root)?;
|
|
|
|
if let Some(first_path) = scene_ids.insert(manifest.scene.id.clone(), manifest_path.clone())
|
|
{
|
|
return Err(SceneRegistryError::DuplicateSceneId {
|
|
scene_id: manifest.scene.id.clone(),
|
|
first_path,
|
|
second_path: manifest_path,
|
|
});
|
|
}
|
|
|
|
registry.push(SceneRegistryEntry {
|
|
manifest,
|
|
skill_root,
|
|
});
|
|
}
|
|
|
|
Ok(registry)
|
|
}
|
|
|
|
fn index_skills_by_root(skills_dir: &Path) -> HashMap<PathBuf, Skill> {
|
|
load_skills_from_directory(skills_dir, true)
|
|
.into_iter()
|
|
.filter_map(|skill| {
|
|
let skill_root = skill
|
|
.location
|
|
.as_deref()
|
|
.and_then(Path::parent)
|
|
.map(Path::to_path_buf)?;
|
|
Some((skill_root, skill))
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn load_manifest(path: &Path) -> Result<SceneManifest, SceneRegistryError> {
|
|
let content = fs::read_to_string(path).map_err(|source| SceneRegistryError::ReadManifest {
|
|
path: path.to_path_buf(),
|
|
source,
|
|
})?;
|
|
toml::from_str(&content).map_err(|source| SceneRegistryError::ParseManifest {
|
|
path: path.to_path_buf(),
|
|
source,
|
|
})
|
|
}
|
|
|
|
fn validate_manifest(
|
|
manifest: &SceneManifest,
|
|
manifest_path: &Path,
|
|
skill_root: &Path,
|
|
skills_by_root: &HashMap<PathBuf, Skill>,
|
|
) -> Result<(), SceneRegistryError> {
|
|
if manifest.manifest.schema_version != SUPPORTED_SCHEMA_VERSION_V1 {
|
|
return Err(SceneRegistryError::UnsupportedSchemaVersion {
|
|
path: manifest_path.to_path_buf(),
|
|
version: manifest.manifest.schema_version.clone(),
|
|
supported: SUPPORTED_SCHEMA_VERSION_V1,
|
|
});
|
|
}
|
|
|
|
if manifest.scene.kind != SUPPORTED_SCENE_KIND_V1 {
|
|
return Err(SceneRegistryError::UnsupportedSceneKind {
|
|
path: manifest_path.to_path_buf(),
|
|
kind: manifest.scene.kind.clone(),
|
|
supported: SUPPORTED_SCENE_KIND_V1,
|
|
});
|
|
}
|
|
|
|
if manifest.scene.category != SUPPORTED_SCENE_CATEGORY_V1 {
|
|
return Err(SceneRegistryError::UnsupportedSceneCategory {
|
|
path: manifest_path.to_path_buf(),
|
|
category: manifest.scene.category.clone(),
|
|
supported: SUPPORTED_SCENE_CATEGORY_V1,
|
|
});
|
|
}
|
|
|
|
let Some(skill) = skills_by_root.get(skill_root) else {
|
|
return Err(SceneRegistryError::MissingSkill {
|
|
path: manifest_path.to_path_buf(),
|
|
skill: manifest.scene.skill.clone(),
|
|
});
|
|
};
|
|
|
|
if skill.name != manifest.scene.skill {
|
|
return Err(SceneRegistryError::SkillPackageMismatch {
|
|
path: manifest_path.to_path_buf(),
|
|
manifest_skill: manifest.scene.skill.clone(),
|
|
package_skill: skill.name.clone(),
|
|
});
|
|
}
|
|
|
|
let Some(tool) = skill
|
|
.tools
|
|
.iter()
|
|
.find(|tool| tool.name == manifest.scene.tool)
|
|
else {
|
|
return Err(SceneRegistryError::MissingTool {
|
|
path: manifest_path.to_path_buf(),
|
|
skill: skill.name.clone(),
|
|
tool: manifest.scene.tool.clone(),
|
|
});
|
|
};
|
|
|
|
if tool.kind != SUPPORTED_SCENE_KIND_V1 {
|
|
return Err(SceneRegistryError::UnsupportedSceneKind {
|
|
path: manifest_path.to_path_buf(),
|
|
kind: tool.kind.clone(),
|
|
supported: SUPPORTED_SCENE_KIND_V1,
|
|
});
|
|
}
|
|
|
|
Ok(())
|
|
}
|