feat(api): topology endpoints (catalog / classify / build)
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

Stateless, auth-gated routes backing the topology builder UI:
- GET  /api/topologies          — catalog of the 12 kinds + descriptions + role mix
- POST /api/topologies/classify — infer a kind + metrics from a posted graph
- POST /api/topologies/build    — build a canonical graph from {kind, roles}

Add Serialize to cm-topology Classification/GraphMetrics; add ApiError::BadRequest
(400) for invalid build input; add cm-topology dep. 3 integration tests
(catalog/build/auth) green against Postgres; offline build + clippy clean.

No DB or provider yet — running/comparing topologies is a later provider-backed
endpoint. Server redeploy will batch with the ReactFlow UI that consumes these.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-15 22:04:26 -07:00
co-authored by Claude Opus 4.8
parent b0c122b88d
commit fa7e5dec5f
8 changed files with 155 additions and 2 deletions
+72
View File
@@ -0,0 +1,72 @@
//! Topology endpoints: the catalog, structural classification, and building a
//! canonical graph from a kind + roles. Stateless (no DB, no provider) — these
//! back the topology builder UI. Running/comparing topologies is a later,
//! provider-backed endpoint.
use axum::Json;
use cm_topology::{build, classify, heuristics, Classification, TopologyGraph, TopologyKind};
use serde::{Deserialize, Serialize};
use crate::{ApiError, Authed};
/// A role and its suggested share of the team.
#[derive(Serialize)]
pub struct RoleWeight {
pub role: String,
pub weight: f32,
}
/// One supported topology kind, with its description and default role mix.
#[derive(Serialize)]
pub struct CatalogEntry {
pub kind: TopologyKind,
pub name: String,
pub description: String,
pub role_distribution: Vec<RoleWeight>,
}
/// `GET /api/topologies` — the catalog of supported topology kinds.
pub async fn catalog(_auth: Authed) -> Json<Vec<CatalogEntry>> {
let entries = TopologyKind::ALL
.iter()
.map(|&kind| {
let h = heuristics(kind);
CatalogEntry {
kind,
name: kind.as_str().to_string(),
description: kind.description().to_string(),
role_distribution: h
.role_distribution
.iter()
.map(|(role, weight)| RoleWeight {
role: (*role).to_string(),
weight: *weight,
})
.collect(),
}
})
.collect();
Json(entries)
}
/// `POST /api/topologies/classify` — infer a topology kind from a graph.
pub async fn classify_graph(_auth: Authed, Json(graph): Json<TopologyGraph>) -> Json<Classification> {
Json(classify(&graph))
}
/// Request body for building a canonical topology.
#[derive(Deserialize)]
pub struct BuildRequest {
pub kind: TopologyKind,
pub roles: Vec<String>,
}
/// `POST /api/topologies/build` — build a canonical graph from a kind + roles.
pub async fn build_graph(
_auth: Authed,
Json(req): Json<BuildRequest>,
) -> Result<Json<TopologyGraph>, ApiError> {
let roles: Vec<&str> = req.roles.iter().map(String::as_str).collect();
let graph = build(req.kind, &roles).map_err(|_| ApiError::BadRequest)?;
Ok(Json(graph))
}