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, 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 { 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 { 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, ) -> 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(()) }