Remove NodeOutcomeRecord alias; embed CatalogRecord in RunSummary

NodeOutcomeRecord was a duplicate alias for Outcome<Option<StageUsage>>
which fabro-workflow already calls Outcome. Inline the type instead.

RunSummary duplicated CatalogRecord's four fields. Use #[serde(flatten)]
to embed CatalogRecord directly, eliminating the duplication.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-04-03 16:31:44 -07:00
parent 7bc0b5db3a
commit 22115a180b
No known key found for this signature in database
6 changed files with 24 additions and 32 deletions

View file

@ -15,9 +15,6 @@ pub use runtime::RuntimeState;
pub use slate::{SlateRunStore, SlateStore};
pub use types::{CatalogRecord, EventEnvelope, EventPayload, NodeVisitRef, RunSummary};
use fabro_types::{Outcome, StageUsage};
pub type NodeOutcomeRecord = Outcome<Option<StageUsage>>;
pub type StoreHandle = Arc<SlateStore>;
#[derive(Debug, Default, Clone, PartialEq, Eq)]

View file

@ -6,13 +6,11 @@ use chrono::{DateTime, Utc};
use serde::de::DeserializeOwned;
use serde_json::Value;
use crate::{
CatalogRecord, EventEnvelope, NodeOutcomeRecord, NodeVisitRef, Result, RunSummary, StoreError,
};
use crate::{CatalogRecord, EventEnvelope, NodeVisitRef, Result, RunSummary, StoreError};
use fabro_types::{
Checkpoint, Conclusion, FailureSignature, NodeStatusRecord, Outcome, PullRequestRecord, Retro,
RunId, RunRecord, RunStatus, RunStatusRecord, SandboxRecord, StageStatus, StartRecord,
StatusReason,
RunId, RunRecord, RunStatus, RunStatusRecord, SandboxRecord, StageStatus, StageUsage,
StartRecord, StatusReason,
};
#[derive(Debug, Clone, Default)]
@ -39,7 +37,7 @@ pub struct NodeState {
pub prompt: Option<String>,
pub response: Option<String>,
pub status: Option<NodeStatusRecord>,
pub outcome: Option<NodeOutcomeRecord>,
pub outcome: Option<Outcome<Option<StageUsage>>>,
pub provider_used: Option<serde_json::Value>,
pub diff: Option<String>,
pub script_invocation: Option<serde_json::Value>,
@ -309,10 +307,7 @@ impl RunState {
(!goal.is_empty()).then(|| goal.to_string())
});
RunSummary {
run_id: catalog.run_id,
created_at: catalog.created_at,
db_prefix: catalog.db_prefix.clone(),
run_dir: catalog.run_dir.clone(),
catalog: catalog.clone(),
workflow_name,
workflow_slug: self.run.as_ref().and_then(|run| run.workflow_slug.clone()),
goal,
@ -564,7 +559,7 @@ fn stage_visit(
fn stage_outcome_from_properties(
properties: &serde_json::Map<String, Value>,
) -> Result<NodeOutcomeRecord> {
) -> Result<Outcome<Option<StageUsage>>> {
let status = StageStatus::from_str(&required_string(properties, "status")?)
.map_err(|err| StoreError::InvalidEvent(format!("invalid stage status: {err}")))?;
Ok(Outcome {
@ -582,7 +577,7 @@ fn stage_outcome_from_properties(
}
fn node_status_from_outcome(
outcome: &NodeOutcomeRecord,
outcome: &Outcome<Option<StageUsage>>,
timestamp: DateTime<Utc>,
) -> NodeStatusRecord {
NodeStatusRecord {

View file

@ -284,7 +284,7 @@ impl SlateStore {
let summary = summary?;
summaries.push(summary);
}
summaries.sort_by(|a, b| b.created_at.cmp(&a.created_at));
summaries.sort_by(|a, b| b.catalog.created_at.cmp(&a.catalog.created_at));
Ok(summaries)
}
@ -656,7 +656,7 @@ mod tests {
let summary = store.list_runs(&ListRunsQuery::default()).await.unwrap();
assert_eq!(summary.len(), 1);
assert_eq!(summary[0].run_id, test_run_id("run-1"));
assert_eq!(summary[0].catalog.run_id, test_run_id("run-1"));
assert_eq!(summary[0].workflow_name, Some("night-sky".to_string()));
assert_eq!(summary[0].goal, Some("map the constellations".to_string()));
assert_eq!(summary[0].status, Some(RunStatus::Succeeded));
@ -2020,7 +2020,7 @@ mod tests {
let all = store.list_runs(&ListRunsQuery::default()).await.unwrap();
assert_eq!(all.len(), 2);
assert_eq!(all[0].run_id, test_run_id("run-late"));
assert_eq!(all[0].catalog.run_id, test_run_id("run-late"));
assert_eq!(all[0].workflow_name, Some("night-sky".to_string()));
assert_eq!(all[0].goal, Some("map the constellations".to_string()));
assert_eq!(
@ -2040,6 +2040,6 @@ mod tests {
.await
.unwrap();
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].run_id, test_run_id("run-late"));
assert_eq!(filtered[0].catalog.run_id, test_run_id("run-late"));
}
}

View file

@ -22,10 +22,8 @@ pub struct CatalogRecord {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RunSummary {
pub run_id: RunId,
pub created_at: DateTime<Utc>,
pub db_prefix: String,
pub run_dir: Option<String>,
#[serde(flatten)]
pub catalog: CatalogRecord,
pub workflow_name: Option<String>,
pub workflow_slug: Option<String>,
pub goal: Option<String>,

View file

@ -185,9 +185,9 @@ pub async fn find_run_id_by_prefix_or_store(
let current_repo_root = canonical_repo_root(repo)?;
let mut matches = Vec::new();
for summary in fabro_store.list_runs(&ListRunsQuery::default()).await? {
if summary.run_id.to_string() == prefix {
if summary.catalog.run_id.to_string() == prefix {
if summary.host_repo_path.is_none() {
return Ok(summary.run_id);
return Ok(summary.catalog.run_id);
}
let Some(host_repo_path) = summary.host_repo_path.as_deref() else {
@ -200,7 +200,7 @@ pub async fn find_run_id_by_prefix_or_store(
continue;
};
if host_repo_root == current_repo_root {
return Ok(summary.run_id);
return Ok(summary.catalog.run_id);
}
continue;
}
@ -214,8 +214,10 @@ pub async fn find_run_id_by_prefix_or_store(
let Ok(host_repo_root) = canonical_repo_root(&host_repo) else {
continue;
};
if host_repo_root == current_repo_root && summary.run_id.to_string().starts_with(prefix) {
matches.push(summary.run_id);
if host_repo_root == current_repo_root
&& summary.catalog.run_id.to_string().starts_with(prefix)
{
matches.push(summary.catalog.run_id);
}
}

View file

@ -124,7 +124,7 @@ pub async fn scan_runs_combined(store: &SlateStore, base: &Path) -> Result<Vec<R
let Some(run_info) = run_info_from_summary(&summary) else {
continue;
};
runs_by_id.insert(summary.run_id, run_info);
runs_by_id.insert(summary.catalog.run_id, run_info);
}
}
@ -145,7 +145,7 @@ pub async fn scan_runs_combined(store: &SlateStore, base: &Path) -> Result<Vec<R
}
fn run_info_from_summary(summary: &fabro_store::RunSummary) -> Option<RunInfo> {
let run_dir = summary.run_dir.as_deref()?;
let run_dir = summary.catalog.run_dir.as_deref()?;
let path = PathBuf::from(run_dir);
if !path.exists() {
return None;
@ -153,7 +153,7 @@ fn run_info_from_summary(summary: &fabro_store::RunSummary) -> Option<RunInfo> {
let dir_name = path
.file_name()
.map(|name| name.to_string_lossy().to_string())?;
let start_time_dt = summary.created_at;
let start_time_dt = summary.catalog.created_at;
let start_time = summary.start_time.unwrap_or(start_time_dt);
let end_time = if summary.status.is_some_and(RunStatus::is_terminal) {
summary.duration_ms.and_then(|duration_ms| {
@ -164,7 +164,7 @@ fn run_info_from_summary(summary: &fabro_store::RunSummary) -> Option<RunInfo> {
};
Some(RunInfo {
run_id: summary.run_id,
run_id: summary.catalog.run_id,
dir_name,
workflow_name: summary
.workflow_name