//! Persisted research artifacts — one row per run's final synthesis. When //! `topology_worker` completes a run tagged with a `research_topic_id`, it //! extracts the orchestrator's `RunRecord.final_output` and calls //! [`insert`] here. The frontend canvas then renders the latest outcome //! instead of the topic description when the topic has moved past //! `standby`, so reviewers see the actual draft. //! //! Version is per-topic and monotonically increasing so reject-with- //! revision loops accumulate history rather than clobber prior drafts. use serde::{Deserialize, Serialize}; use sqlx::PgPool; use time::OffsetDateTime; use uuid::Uuid; use crate::DbError; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Outcome { pub id: Uuid, pub topic_id: Uuid, pub version: i32, pub body_md: String, pub produced_by_run_id: Option, // RFC3339 on the wire so `new Date(...)` in the browser parses it // instead of choking on the `time` crate's default `[y, ordinal, // ...]` array format (surfaced as "Invalid Date" in the Draft // header). #[serde(with = "time::serde::rfc3339")] pub created_at: OffsetDateTime, } /// Insert a new outcome. Version is derived server-side as `max(version) + 1` /// for the topic (starting at 1) so callers never need to know the current /// count. Returns the persisted row. pub async fn insert( pool: &PgPool, topic_id: Uuid, body_md: &str, produced_by_run_id: Option, ) -> Result { let id = Uuid::now_v7(); let row = sqlx::query!( "INSERT INTO research_outcomes (id, topic_id, version, body_md, produced_by_run_id) SELECT $1, $2, coalesce(max(version), 0) + 1, $3, $4 FROM research_outcomes WHERE topic_id = $2 RETURNING id, topic_id, version, body_md, produced_by_run_id, created_at", id, topic_id, body_md, produced_by_run_id, ) .fetch_one(pool) .await?; Ok(Outcome { id: row.id, topic_id: row.topic_id, version: row.version, body_md: row.body_md, produced_by_run_id: row.produced_by_run_id, created_at: row.created_at, }) } /// Newest outcome for a topic, or `None` if no run has completed yet. pub async fn latest(pool: &PgPool, topic_id: Uuid) -> Result, DbError> { let row = sqlx::query!( "SELECT id, topic_id, version, body_md, produced_by_run_id, created_at FROM research_outcomes WHERE topic_id = $1 ORDER BY version DESC LIMIT 1", topic_id, ) .fetch_optional(pool) .await?; Ok(row.map(|r| Outcome { id: r.id, topic_id: r.topic_id, version: r.version, body_md: r.body_md, produced_by_run_id: r.produced_by_run_id, created_at: r.created_at, })) }