feat(topology): persist comparison runs + history
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

Save each comparison and let users reload past ones.
- migration 0008: topology_runs (workspace-scoped; full comparison as JSONB).
- cm-db repo::topology_runs (insert / list_recent / get) + regenerated .sqlx.
- cm-api: compare persists best-effort (never loses the LLM result on a DB
  hiccup); GET /api/topology-runs (recent) + GET /api/topology-runs/{id}.
  Integration test asserts persist → list → get.
- frontend: "Recent comparisons" list on the Compare tab; click to reload a
  saved run. e2e p8 green (39 suite); offline build + clippy clean.

Server self-migrates at boot (cm_db::MIGRATOR), so 0008 applies on deploy.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-16 05:11:52 -07:00
co-authored by Claude Opus 4.8
parent 654ec0f511
commit 7baf2082d0
10 changed files with 345 additions and 3 deletions
+1
View File
@@ -11,5 +11,6 @@ pub mod sessions;
pub mod skills;
pub mod steps;
pub mod threads;
pub mod topology_runs;
pub mod users;
pub mod workspaces;
+92
View File
@@ -0,0 +1,92 @@
//! Persistence for saved multi-topology comparison runs.
use cm_domain::WorkspaceId;
use serde_json::Value;
use sqlx::PgPool;
use time::OffsetDateTime;
use uuid::Uuid;
use crate::DbError;
/// A row summary for the recent-runs list.
pub struct TopologyRunSummary {
pub id: Uuid,
pub task: String,
pub created_at: OffsetDateTime,
}
/// A full saved comparison run.
pub struct TopologyRun {
pub id: Uuid,
pub task: String,
pub comparison: Value,
pub created_at: OffsetDateTime,
}
/// Save a comparison run for a workspace.
pub async fn insert(
pool: &PgPool,
id: Uuid,
workspace_id: WorkspaceId,
task: &str,
comparison: &Value,
) -> Result<(), DbError> {
sqlx::query!(
"INSERT INTO topology_runs (id, workspace_id, task, comparison)
VALUES ($1, $2, $3, $4)",
id,
workspace_id.as_uuid(),
task,
comparison,
)
.execute(pool)
.await?;
Ok(())
}
/// The most recent runs for a workspace, newest first.
pub async fn list_recent(
pool: &PgPool,
workspace_id: WorkspaceId,
limit: i64,
) -> Result<Vec<TopologyRunSummary>, DbError> {
let rows = sqlx::query!(
"SELECT id, task, created_at FROM topology_runs
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| TopologyRunSummary {
id: r.id,
task: r.task,
created_at: r.created_at,
})
.collect())
}
/// A single saved run, scoped to its workspace.
pub async fn get(
pool: &PgPool,
id: Uuid,
workspace_id: WorkspaceId,
) -> Result<TopologyRun, DbError> {
let row = sqlx::query!(
"SELECT id, task, comparison, created_at FROM topology_runs
WHERE id = $1 AND workspace_id = $2",
id,
workspace_id.as_uuid(),
)
.fetch_optional(pool)
.await?
.ok_or(DbError::NotFound)?;
Ok(TopologyRun {
id: row.id,
task: row.task,
comparison: row.comparison,
created_at: row.created_at,
})
}