pub mod loader;
use std::collections::{HashMap, HashSet, VecDeque};
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum PluginTier {
Core = 0,
Fleet = 1,
Edge = 2,
}
impl std::fmt::Display for PluginTier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PluginTier::Core => write!(f, "Core(T1)"),
PluginTier::Fleet => write!(f, "Fleet(T2)"),
PluginTier::Edge => write!(f, "Edge(T3)"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginManifest {
pub id: String,
pub name: String,
pub version: String,
pub tier: PluginTier,
pub requires: Vec<String>,
pub provides: Vec<String>,
}
pub trait PlatoPlugin: Send + Sync + 'static {
fn manifest(&self) -> &PluginManifest;
}
#[derive(Debug, Clone)]
pub struct MountPlan {
pub order: Vec<String>,
}
#[derive(Debug, Error)]
pub enum DependencyError {
#[error("Plugin '{plugin}' requires '{requires}', which is not registered")]
MissingDependency { plugin: String, requires: String },
#[error("Circular dependency detected among: {cycle:?}")]
CircularDependency { cycle: Vec<String> },
#[error(
"Tier violation: '{plugin}' ({plugin_tier}) requires '{dep}' ({dep_tier}) \
— a plugin may only depend on equal-or-lower tiers"
)]
TierViolation {
plugin: String,
plugin_tier: PluginTier,
dep: String,
dep_tier: PluginTier,
},
#[error("Plugin '{id}' is already registered")]
AlreadyRegistered { id: String },
#[error("Plugin '{id}' is not registered")]
NotFound { id: String },
}
pub struct PluginRegistry {
plugins: HashMap<String, Box<dyn PlatoPlugin>>,
mounted: HashSet<String>,
}
impl PluginRegistry {
pub fn new() -> Self {
Self {
plugins: HashMap::new(),
mounted: HashSet::new(),
}
}
pub fn register(&mut self, plugin: Box<dyn PlatoPlugin>) -> Result<(), DependencyError> {
let id = plugin.manifest().id.clone();
if self.plugins.contains_key(&id) {
return Err(DependencyError::AlreadyRegistered { id });
}
tracing::debug!("plugin::register {} ({})", id, plugin.manifest().tier);
self.plugins.insert(id, plugin);
Ok(())
}
pub fn resolve_mount(&self, plugin_id: &str) -> Result<MountPlan, DependencyError> {
let subgraph = self.transitive_deps(plugin_id)?;
for id in &subgraph {
let m = self.plugins[id].manifest();
for req_id in &m.requires {
if let Some(req_plugin) = self.plugins.get(req_id) {
let req_tier = req_plugin.manifest().tier;
if req_tier > m.tier {
return Err(DependencyError::TierViolation {
plugin: id.clone(),
plugin_tier: m.tier,
dep: req_id.clone(),
dep_tier: req_tier,
});
}
}
}
}
let mut in_degree: HashMap<String, usize> =
subgraph.iter().map(|id| (id.clone(), 0)).collect();
let mut dependents: HashMap<String, Vec<String>> =
subgraph.iter().map(|id| (id.clone(), vec![])).collect();
for id in &subgraph {
let m = self.plugins[id].manifest();
for req_id in &m.requires {
if subgraph.contains(req_id) {
*in_degree.entry(id.clone()).or_insert(0) += 1;
dependents.entry(req_id.clone()).or_default().push(id.clone());
}
}
}
let mut queue: VecDeque<String> = {
let mut seeds: Vec<String> = in_degree
.iter()
.filter(|(_, °)| deg == 0)
.map(|(id, _)| id.clone())
.collect();
seeds.sort();
seeds.into()
};
let mut order: Vec<String> = Vec::with_capacity(subgraph.len());
while let Some(id) = queue.pop_front() {
order.push(id.clone());
if let Some(deps) = dependents.get(&id) {
let mut ready: Vec<String> = deps
.iter()
.filter_map(|dep| {
let deg = in_degree.get_mut(dep)?;
*deg -= 1;
if *deg == 0 { Some(dep.clone()) } else { None }
})
.collect();
ready.sort(); for r in ready {
queue.push_back(r);
}
}
}
if order.len() != subgraph.len() {
let mut cycle: Vec<String> = in_degree
.into_iter()
.filter(|(_, deg)| *deg > 0)
.map(|(id, _)| id)
.collect();
cycle.sort();
return Err(DependencyError::CircularDependency { cycle });
}
Ok(MountPlan { order })
}
pub fn apply_plan(&mut self, plan: &MountPlan) -> Vec<String> {
let mut newly_mounted = Vec::new();
for id in &plan.order {
if !self.mounted.contains(id.as_str()) {
tracing::info!("plugin::mount {}", id);
self.mounted.insert(id.clone());
newly_mounted.push(id.clone());
}
}
newly_mounted
}
pub fn mount(&mut self, plugin_id: &str) -> Result<MountPlan, DependencyError> {
let plan = self.resolve_mount(plugin_id)?;
self.apply_plan(&plan);
Ok(plan)
}
pub fn mount_tier(&mut self, tier: PluginTier) -> Result<Vec<MountPlan>, DependencyError> {
let mut targets: Vec<String> = self.plugins
.iter()
.filter(|(_, p)| p.manifest().tier == tier)
.map(|(id, _)| id.clone())
.collect();
targets.sort_by(|a, b| {
let da = self.plugins[a].manifest().requires.len();
let db = self.plugins[b].manifest().requires.len();
da.cmp(&db).then_with(|| a.cmp(b))
});
let mut plans = Vec::with_capacity(targets.len());
for id in targets {
if self.mounted.contains(&id) {
continue;
}
let plan = self.resolve_mount(&id)?;
self.apply_plan(&plan);
plans.push(plan);
}
Ok(plans)
}
pub fn provides(&self, capability: &str) -> bool {
self.mounted.iter().any(|id| {
self.plugins
.get(id)
.map(|p| p.manifest().provides.iter().any(|c| c == capability))
.unwrap_or(false)
})
}
pub fn is_mounted(&self, id: &str) -> bool {
self.mounted.contains(id)
}
pub fn registered_count(&self) -> usize {
self.plugins.len()
}
pub fn registered_ids(&self) -> impl Iterator<Item = &str> {
self.plugins.keys().map(String::as_str)
}
pub fn mounted_ids(&self) -> impl Iterator<Item = &str> {
self.mounted.iter().map(String::as_str)
}
fn transitive_deps(&self, root: &str) -> Result<HashSet<String>, DependencyError> {
let mut visited: HashSet<String> = HashSet::new();
let mut stack = vec![root.to_string()];
while let Some(id) = stack.pop() {
if visited.contains(&id) {
continue;
}
let plugin = self.plugins.get(&id).ok_or_else(|| DependencyError::NotFound {
id: id.clone(),
})?;
for req in &plugin.manifest().requires {
if !self.plugins.contains_key(req) {
return Err(DependencyError::MissingDependency {
plugin: id.clone(),
requires: req.clone(),
});
}
if !visited.contains(req) {
stack.push(req.clone());
}
}
visited.insert(id);
}
Ok(visited)
}
}
impl Default for PluginRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_plugin(id: &str, tier: PluginTier, requires: Vec<&str>, provides: Vec<&str>) -> Box<dyn PlatoPlugin> {
struct Stub(PluginManifest);
impl PlatoPlugin for Stub {
fn manifest(&self) -> &PluginManifest { &self.0 }
}
Box::new(Stub(PluginManifest {
id: id.to_string(),
name: id.to_string(),
version: "0.0.1".to_string(),
tier,
requires: requires.iter().map(|s| s.to_string()).collect(),
provides: provides.iter().map(|s| s.to_string()).collect(),
}))
}
#[test]
fn test_basic_mount_order() {
let mut reg = PluginRegistry::new();
reg.register(make_plugin("a", PluginTier::Core, vec![], vec!["cap-a"])).unwrap();
reg.register(make_plugin("b", PluginTier::Core, vec!["a"], vec!["cap-b"])).unwrap();
reg.register(make_plugin("c", PluginTier::Core, vec!["b"], vec!["cap-c"])).unwrap();
let plan = reg.resolve_mount("c").unwrap();
let pos = |id: &str| plan.order.iter().position(|x| x == id).unwrap();
assert!(pos("a") < pos("b"));
assert!(pos("b") < pos("c"));
}
#[test]
fn test_missing_dependency_rejected() {
let mut reg = PluginRegistry::new();
reg.register(make_plugin("b", PluginTier::Core, vec!["a"], vec![])).unwrap();
let err = reg.resolve_mount("b").unwrap_err();
assert!(matches!(err, DependencyError::MissingDependency { .. }));
}
#[test]
fn test_circular_dependency_detected() {
let mut reg = PluginRegistry::new();
reg.register(make_plugin("x", PluginTier::Core, vec!["y"], vec![])).unwrap();
reg.register(make_plugin("y", PluginTier::Core, vec!["x"], vec![])).unwrap();
let err = reg.resolve_mount("x").unwrap_err();
assert!(matches!(err, DependencyError::CircularDependency { .. }));
}
#[test]
fn test_tier_violation_rejected() {
let mut reg = PluginRegistry::new();
reg.register(make_plugin("fleet-thing", PluginTier::Fleet, vec![], vec![])).unwrap();
reg.register(make_plugin("core-bad", PluginTier::Core, vec!["fleet-thing"], vec![])).unwrap();
let err = reg.resolve_mount("core-bad").unwrap_err();
assert!(matches!(err, DependencyError::TierViolation { .. }));
}
#[test]
fn test_provides_capability() {
let mut reg = PluginRegistry::new();
reg.register(make_plugin("a", PluginTier::Core, vec![], vec!["event-bus"])).unwrap();
reg.mount("a").unwrap();
assert!(reg.provides("event-bus"));
assert!(!reg.provides("gpu-simulation"));
}
#[test]
fn test_duplicate_registration_rejected() {
let mut reg = PluginRegistry::new();
reg.register(make_plugin("a", PluginTier::Core, vec![], vec![])).unwrap();
let err = reg.register(make_plugin("a", PluginTier::Core, vec![], vec![])).unwrap_err();
assert!(matches!(err, DependencyError::AlreadyRegistered { .. }));
}
#[test]
fn test_apply_plan_idempotent() {
let mut reg = PluginRegistry::new();
reg.register(make_plugin("a", PluginTier::Core, vec![], vec![])).unwrap();
let plan = reg.resolve_mount("a").unwrap();
let first = reg.apply_plan(&plan);
let second = reg.apply_plan(&plan);
assert_eq!(first, vec!["a"]);
assert!(second.is_empty()); }
#[test]
fn test_mount_tier_basic() {
let mut reg = PluginRegistry::new();
reg.register(make_plugin("core-a", PluginTier::Core, vec![], vec!["cap-a"])).unwrap();
reg.register(make_plugin("core-b", PluginTier::Core, vec!["core-a"], vec!["cap-b"])).unwrap();
let plans = reg.mount_tier(PluginTier::Core).unwrap();
assert!(reg.is_mounted("core-a"));
assert!(reg.is_mounted("core-b"));
assert!(!plans.is_empty());
}
#[test]
fn test_mount_tier_ignores_other_tiers() {
let mut reg = PluginRegistry::new();
reg.register(make_plugin("core-1", PluginTier::Core, vec![], vec![])).unwrap();
reg.register(make_plugin("fleet-1", PluginTier::Fleet, vec!["core-1"], vec![])).unwrap();
reg.mount_tier(PluginTier::Core).unwrap();
assert!(reg.is_mounted("core-1"));
assert!(!reg.is_mounted("fleet-1"));
}
#[test]
fn test_mount_tier_cross_tier_deps() {
let mut reg = PluginRegistry::new();
reg.register(make_plugin("core-1", PluginTier::Core, vec![], vec![])).unwrap();
reg.register(make_plugin("fleet-1", PluginTier::Fleet, vec!["core-1"], vec![])).unwrap();
let plans = reg.mount_tier(PluginTier::Fleet).unwrap();
assert!(reg.is_mounted("core-1"));
assert!(reg.is_mounted("fleet-1"));
}
#[test]
fn test_mount_tier_idempotent() {
let mut reg = PluginRegistry::new();
reg.register(make_plugin("core-1", PluginTier::Core, vec![], vec![])).unwrap();
reg.mount_tier(PluginTier::Core).unwrap();
let plans2 = reg.mount_tier(PluginTier::Core).unwrap();
assert!(plans2.is_empty());
}
#[test]
fn test_mount_tier_fail_fast() {
let mut reg = PluginRegistry::new();
reg.register(make_plugin("core-ok", PluginTier::Core, vec![], vec![])).unwrap();
reg.register(make_plugin("core-bad", PluginTier::Core, vec!["nonexistent"], vec![])).unwrap();
let err = reg.mount_tier(PluginTier::Core).unwrap_err();
assert!(matches!(err, DependencyError::MissingDependency { .. }));
}
#[test]
fn test_mount_tier_respects_dependency_order() {
let mut reg = PluginRegistry::new();
reg.register(make_plugin("c", PluginTier::Core, vec!["b"], vec![])).unwrap();
reg.register(make_plugin("b", PluginTier::Core, vec!["a"], vec![])).unwrap();
reg.register(make_plugin("a", PluginTier::Core, vec![], vec![])).unwrap();
reg.mount_tier(PluginTier::Core).unwrap();
assert!(reg.is_mounted("a"));
assert!(reg.is_mounted("b"));
assert!(reg.is_mounted("c"));
}
#[test]
fn test_mount_tier_empty() {
let mut reg = PluginRegistry::new();
let plans = reg.mount_tier(PluginTier::Core).unwrap();
assert!(plans.is_empty());
}
#[test]
fn test_fleet_can_depend_on_core() {
let mut reg = PluginRegistry::new();
reg.register(make_plugin("core-bus", PluginTier::Core, vec![], vec!["event-bus"])).unwrap();
reg.register(make_plugin("fleet-sw", PluginTier::Fleet, vec!["core-bus"], vec![])).unwrap();
let plan = reg.resolve_mount("fleet-sw").unwrap();
let pos = |id: &str| plan.order.iter().position(|x| x == id).unwrap();
assert!(pos("core-bus") < pos("fleet-sw"));
}
}