Recursive deploy ladder: Company + Org tiers, mesh mark, two-tier rail

Completes the scale ladder (single → team → company → org). Every tier is a
topology whose nodes are the tier below; running a parent recursively runs each
child's sub-topology down to the leaf claws.

Backend:
- migration 0011: companies/company_teams, orgs/org_companies, topology_runs.tier
- cm-db repos for companies + orgs (mirror teams)
- TurnRequest.attrs (forwarded from node.attrs) for child-id binding
- SubTopologyExecutor (recursive_exec.rs): a parent "turn" runs the child's
  sub-topology; durability via parent updated_at keepalive + cancel propagation
  + depth cap; boxed future breaks the org→company recursion
- topology_worker selects executor by job.tier
- routes: /api/companies, /api/orgs (create/list/get/run) + unified
  /api/structure/{level}/{id} for the zoom canvas

Frontend:
- MeshMark: node-mesh brand glyph (replaces the claw PNG), tier variants
- TopologyGraphView: optional onNodeClick/nodeMeta + dark-token theming
- StructureCanvas + Breadcrumb: one recursive zoom view for every tier
  (drill down on node click, breadcrumb up); TeamRunPanel extracted + shared
- two-tier Discord-style rail: StructureRail (mesh mark + org/company/team
  glyphs + tools popover + deploy + user) | RosterColumn (selected group's
  children, or your claws); SecondaryNav for cross-cutting tools
- ComposeWizard (company/org) wired into DeployWizard; /companies + /orgs pages

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-18 14:25:06 -07:00
co-authored by Claude Opus 4.8
parent bba18a4687
commit 3eca4ed70c
58 changed files with 3221 additions and 229 deletions
+148
View File
@@ -0,0 +1,148 @@
//! Persistence for orgs — a baseline topology whose nodes are whole companies
//! (the top rung of the deploy ladder). `orgs` holds the topology graph;
//! `org_companies` is the durable node→company binding. Mirrors `companies`
//! one tier up; the recursive executor runs each bound company's sub-topology,
//! which in turn runs its teams, which run their claws.
use cm_domain::WorkspaceId;
use serde_json::Value;
use sqlx::PgPool;
use time::OffsetDateTime;
use uuid::Uuid;
use crate::DbError;
/// An org row summary (list view).
pub struct OrgSummary {
pub id: Uuid,
pub name: String,
pub kind: String,
pub status: String,
pub created_at: OffsetDateTime,
}
/// A full org (graph + metadata).
pub struct Org {
pub id: Uuid,
pub name: String,
pub kind: String,
pub graph: Value,
pub status: String,
pub created_at: OffsetDateTime,
}
/// A node→company binding within an org.
pub struct OrgCompany {
pub node_id: String,
pub company_id: Uuid,
pub role: String,
}
/// Insert an org (the topology graph). Members are added separately.
pub async fn insert_org(
pool: &PgPool,
id: Uuid,
workspace_id: WorkspaceId,
name: &str,
kind: &str,
graph: &Value,
) -> Result<(), DbError> {
sqlx::query!(
"INSERT INTO orgs (id, workspace_id, name, kind, graph)
VALUES ($1, $2, $3, $4, $5)",
id,
workspace_id.as_uuid(),
name,
kind,
graph,
)
.execute(pool)
.await?;
Ok(())
}
/// Bind a company to a topology node within an org.
pub async fn add_company(
pool: &PgPool,
org_id: Uuid,
node_id: &str,
company_id: Uuid,
role: &str,
) -> Result<(), DbError> {
sqlx::query!(
"INSERT INTO org_companies (org_id, node_id, company_id, role)
VALUES ($1, $2, $3, $4)",
org_id,
node_id,
company_id,
role,
)
.execute(pool)
.await?;
Ok(())
}
/// The most recent orgs for a workspace, newest first.
pub async fn list_for_workspace(
pool: &PgPool,
workspace_id: WorkspaceId,
limit: i64,
) -> Result<Vec<OrgSummary>, DbError> {
let rows = sqlx::query!(
"SELECT id, name, kind, status, created_at FROM orgs
WHERE workspace_id = $1 ORDER BY created_at DESC LIMIT $2",
workspace_id.as_uuid(),
limit,
)
.fetch_all(pool)
.await?;
Ok(rows
.into_iter()
.map(|r| OrgSummary {
id: r.id,
name: r.name,
kind: r.kind,
status: r.status,
created_at: r.created_at,
})
.collect())
}
/// A single org, workspace-scoped.
pub async fn get(pool: &PgPool, id: Uuid, workspace_id: WorkspaceId) -> Result<Org, DbError> {
let row = sqlx::query!(
"SELECT id, name, kind, graph, status, created_at FROM orgs
WHERE id = $1 AND workspace_id = $2",
id,
workspace_id.as_uuid(),
)
.fetch_optional(pool)
.await?
.ok_or(DbError::NotFound)?;
Ok(Org {
id: row.id,
name: row.name,
kind: row.kind,
graph: row.graph,
status: row.status,
created_at: row.created_at,
})
}
/// The node→company bindings for an org.
pub async fn companies_for_org(pool: &PgPool, org_id: Uuid) -> Result<Vec<OrgCompany>, DbError> {
let rows = sqlx::query!(
"SELECT node_id, company_id, role FROM org_companies WHERE org_id = $1 ORDER BY node_id",
org_id,
)
.fetch_all(pool)
.await?;
Ok(rows
.into_iter()
.map(|r| OrgCompany {
node_id: r.node_id,
company_id: r.company_id,
role: r.role,
})
.collect())
}