"""Models for declarative Beaver plugin packages.""" from __future__ import annotations from dataclasses import dataclass, field from pathlib import Path from typing import Any @dataclass(frozen=True, slots=True) class PluginSkillDeclaration: name: str relative_path: str root: Path @dataclass(frozen=True, slots=True) class PluginManifest: schema_version: int plugin_id: str name: str version: str root: Path manifest_path: Path display_path: str skills: tuple[PluginSkillDeclaration, ...] @dataclass(frozen=True, slots=True) class PluginSkillFileDigest: path: str size: int executable: bool content_hash: str @dataclass(frozen=True, slots=True) class PluginSkillTreeDigest: skill_content_hash: str skill_tree_hash: str files: tuple[PluginSkillFileDigest, ...] @dataclass(frozen=True, slots=True) class PluginDiscoveryError: path: Path display_path: str message: str plugin_id: str | None = None @dataclass(frozen=True, slots=True) class PluginDiscoveryResult: manifests: dict[str, PluginManifest] errors: list[PluginDiscoveryError] @dataclass(slots=True) class PluginSkillBinding: accepted_upstream_tree_hash: str | None = None observed_upstream_tree_hash: str | None = None accepted_beaver_version: str | None = None current_beaver_version: str | None = None pending_candidate_id: str | None = None status: str = "discovered" last_error: str | None = None def to_dict(self) -> dict[str, Any]: return { "accepted_upstream_tree_hash": self.accepted_upstream_tree_hash, "observed_upstream_tree_hash": self.observed_upstream_tree_hash, "accepted_beaver_version": self.accepted_beaver_version, "current_beaver_version": self.current_beaver_version, "pending_candidate_id": self.pending_candidate_id, "status": self.status, "last_error": self.last_error, } @classmethod def from_dict(cls, payload: dict[str, Any] | None) -> "PluginSkillBinding": data = payload if isinstance(payload, dict) else {} return cls( accepted_upstream_tree_hash=_optional_str(data.get("accepted_upstream_tree_hash")), observed_upstream_tree_hash=_optional_str(data.get("observed_upstream_tree_hash")), accepted_beaver_version=_optional_str(data.get("accepted_beaver_version")), current_beaver_version=_optional_str(data.get("current_beaver_version")), pending_candidate_id=_optional_str(data.get("pending_candidate_id")), status=str(data.get("status") or "discovered"), last_error=_optional_str(data.get("last_error")), ) @dataclass(slots=True) class PluginState: plugin_id: str enabled: bool = False updates_paused: bool = False installed_version: str | None = None manifest_path: str | None = None status: str = "discovered" last_error: str | None = None skills: dict[str, PluginSkillBinding] = field(default_factory=dict) def to_dict(self) -> dict[str, Any]: return { "enabled": self.enabled, "updates_paused": self.updates_paused, "installed_version": self.installed_version, "manifest_path": self.manifest_path, "status": self.status, "last_error": self.last_error, "skills": {name: binding.to_dict() for name, binding in sorted(self.skills.items())}, } @classmethod def from_dict(cls, plugin_id: str, payload: dict[str, Any] | None) -> "PluginState": data = payload if isinstance(payload, dict) else {} raw_skills = data.get("skills") if isinstance(data.get("skills"), dict) else {} return cls( plugin_id=plugin_id, enabled=bool(data.get("enabled", False)), updates_paused=bool(data.get("updates_paused", False)), installed_version=_optional_str(data.get("installed_version")), manifest_path=_optional_str(data.get("manifest_path")), status=str(data.get("status") or "discovered"), last_error=_optional_str(data.get("last_error")), skills={ str(name): PluginSkillBinding.from_dict(binding if isinstance(binding, dict) else {}) for name, binding in raw_skills.items() }, ) def _optional_str(value: Any) -> str | None: if value in (None, ""): return None return str(value)