feat(topology): cm-topology crate + architecture doc (Phases 0–1)
ci / gates (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / sandbox-k8s (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / e2e (push) Has been cancelled

Foundation for the dynamic agentic-topologies platform (see
docs/topology-platform.md), porting agentorg's topology modeling into pure Rust:

- TopologyKind: curated 12-kind taxonomy (hierarchical, flat, pipeline, swarm,
  mesh, hub_spoke, ring, star_moe, market, blackboard, debate, holacratic).
- TopologyGraph: role-slot nodes + typed edges, with validation.
- adapter: normalize a loose JSON spec → validated graph (fills edge kinds).
- classifier: structural metrics (density, hub dominance, clustering, diameter,
  hierarchy score) → inferred kind + confidence (tree→hierarchical, line→pipeline,
  cycle→ring, star→hub_spoke, complete→mesh, empty→flat).
- heuristics: per-kind role distributions (ported from topology_manager.py).

Pure, offline, dependency-light (serde/thiserror). 17 unit tests, clippy clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-15 20:05:06 -07:00
co-authored by Claude Opus 4.8
parent 34da54ccaa
commit 817d8c712c
10 changed files with 1048 additions and 0 deletions
+115
View File
@@ -0,0 +1,115 @@
//! Normalize a loose JSON spec into a validated [`TopologyGraph`].
//!
//! Mirrors agentorg's `org_adapter` normalization: callers may omit each
//! edge's `kind`, and it is filled with the sensible default for the declared
//! topology. (YAML/CSV inputs can be added later behind feature flags.)
use serde::Deserialize;
use crate::graph::{Edge, EdgeKind, Node, TopologyGraph};
use crate::kind::TopologyKind;
use crate::TopologyError;
#[derive(Deserialize)]
struct EdgeSpec {
from: String,
to: String,
#[serde(default)]
kind: Option<EdgeKind>,
}
#[derive(Deserialize)]
struct GraphSpec {
kind: TopologyKind,
nodes: Vec<Node>,
#[serde(default)]
edges: Vec<EdgeSpec>,
}
/// The natural edge semantic for a topology, used when a spec omits it.
fn default_edge_kind(t: TopologyKind) -> EdgeKind {
use TopologyKind::*;
match t {
Hierarchical => EdgeKind::DelegatesTo,
Flat | Mesh | Holacratic | Debate => EdgeKind::PeersWith,
Pipeline | Ring => EdgeKind::PipesTo,
Swarm | HubSpoke | StarMoe => EdgeKind::RoutesTo,
Market => EdgeKind::BidsTo,
Blackboard => EdgeKind::ReadsWrites,
}
}
/// Parse and normalize a JSON topology spec into a validated graph.
pub fn from_json(s: &str) -> Result<TopologyGraph, TopologyError> {
let spec: GraphSpec =
serde_json::from_str(s).map_err(|e| TopologyError::Spec(e.to_string()))?;
let default = default_edge_kind(spec.kind);
let edges = spec
.edges
.into_iter()
.map(|e| Edge {
from: e.from,
to: e.to,
kind: e.kind.unwrap_or(default),
})
.collect();
TopologyGraph::new(spec.kind, spec.nodes, edges)
}
/// Serialize a graph back to pretty JSON.
pub fn to_json(g: &TopologyGraph) -> Result<String, TopologyError> {
serde_json::to_string_pretty(g).map_err(|e| TopologyError::Spec(e.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fills_default_edge_kind() {
let g = from_json(
r#"{
"kind": "pipeline",
"nodes": [{"id":"a","role":"x"},{"id":"b","role":"y"}],
"edges": [{"from":"a","to":"b"}]
}"#,
)
.unwrap();
assert_eq!(g.edges[0].kind, EdgeKind::PipesTo);
}
#[test]
fn explicit_edge_kind_wins() {
let g = from_json(
r#"{
"kind": "hierarchical",
"nodes": [{"id":"a","role":"x"},{"id":"b","role":"y"}],
"edges": [{"from":"a","to":"b","kind":"reports_to"}]
}"#,
)
.unwrap();
assert_eq!(g.edges[0].kind, EdgeKind::ReportsTo);
}
#[test]
fn round_trips_through_json() {
let g = TopologyGraph::new(
TopologyKind::HubSpoke,
vec![Node::new("hub", "router"), Node::new("s1", "worker")],
vec![Edge {
from: "hub".into(),
to: "s1".into(),
kind: EdgeKind::RoutesTo,
}],
)
.unwrap();
let json = to_json(&g).unwrap();
let back = from_json(&json).unwrap();
assert_eq!(g, back);
}
#[test]
fn bad_spec_is_an_error() {
assert!(matches!(from_json("not json"), Err(TopologyError::Spec(_))));
}
}