//! Canonical topology builders: instantiate a [`TopologyGraph`] of any //! [`TopologyKind`] from a list of roles. //! //! This is the "template catalog" — it turns a kind + roles into a runnable //! graph, and it is the candidate generator for the evolution/search layer. //! Node ids are `n0..nk`; `roles[i]` is the role of `ni`. use crate::graph::{Edge, EdgeKind, Node, TopologyGraph}; use crate::kind::TopologyKind; use crate::TopologyError; fn id(i: usize) -> String { format!("n{i}") } fn star(n: usize, kind: EdgeKind) -> Vec { (1..n) .map(|i| Edge { from: id(0), to: id(i), kind, }) .collect() } fn chain(n: usize, close: bool) -> Vec { let mut edges: Vec = (0..n.saturating_sub(1)) .map(|i| Edge { from: id(i), to: id(i + 1), kind: EdgeKind::PipesTo, }) .collect(); if close && n > 1 { edges.push(Edge { from: id(n - 1), to: id(0), kind: EdgeKind::PipesTo, }); } edges } fn complete(n: usize, kind: EdgeKind) -> Vec { let mut edges = Vec::new(); for i in 0..n { for j in (i + 1)..n { edges.push(Edge { from: id(i), to: id(j), kind, }); } } edges } /// Build a canonical graph of `kind` over `roles` (node `ni` plays `roles[i]`). pub fn build(kind: TopologyKind, roles: &[&str]) -> Result { if roles.is_empty() { return Err(TopologyError::Empty); } let n = roles.len(); let nodes: Vec = roles .iter() .enumerate() .map(|(i, r)| Node::new(id(i), *r)) .collect(); use TopologyKind::*; let edges = match kind { Hierarchical => star(n, EdgeKind::DelegatesTo), HubSpoke | StarMoe => star(n, EdgeKind::RoutesTo), Market => star(n, EdgeKind::BidsTo), Pipeline => chain(n, false), Ring => chain(n, true), Mesh => complete(n, EdgeKind::PeersWith), Blackboard => complete(n, EdgeKind::ReadsWrites), // Peer/parallel kinds wire structure at run time, not via edges. Flat | Holacratic | Swarm | Debate => Vec::new(), }; TopologyGraph::new(kind, nodes, edges) } #[cfg(test)] mod tests { use super::*; use crate::classify; #[test] fn builds_every_kind() { let roles = ["coordinator", "researcher", "writer"]; for kind in TopologyKind::ALL { let g = build(kind, &roles).expect("build"); assert_eq!(g.order(), 3); assert_eq!(g.kind, kind); g.validate().expect("valid"); } } #[test] fn canonical_shapes_classify_back() { let roles = ["a", "b", "c", "d"]; assert_eq!( classify(&build(TopologyKind::Hierarchical, &roles).unwrap()).primary, TopologyKind::HubSpoke // a 1→(n-1) star reads as hub/star structurally ); assert_eq!( classify(&build(TopologyKind::Pipeline, &roles).unwrap()).primary, TopologyKind::Pipeline ); assert_eq!( classify(&build(TopologyKind::Ring, &roles).unwrap()).primary, TopologyKind::Ring ); assert_eq!( classify(&build(TopologyKind::Mesh, &roles).unwrap()).primary, TopologyKind::Mesh ); } #[test] fn empty_roles_error() { assert_eq!(build(TopologyKind::Flat, &[]), Err(TopologyError::Empty)); } }