Files
claw/src/compat/scene_platform/registry.rs

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