use std::collections::BTreeMap; use fidget_spinner_core::{ AttachmentTargetRef, CommandRecipe, ExperimentAnalysis, ExperimentOutcome, FrontierBrief, FrontierRecord, MetricDefinition, MetricValue, NonEmptyText, RunDimensionDefinition, RunDimensionValue, TagRecord, }; use fidget_spinner_store_sqlite::{ ArtifactDetail, ArtifactSummary, EntityHistoryEntry, ExperimentDetail, ExperimentNearestHit, ExperimentNearestResult, ExperimentSummary, FrontierOpenProjection, FrontierSummary, HypothesisCurrentState, HypothesisDetail, MetricBestEntry, MetricKeySummary, MetricObservationSummary, ProjectStore, StoreError, VertexSummary, }; use libmcp::{ ProjectionError, SelectorProjection, StructuredProjection, SurfaceKind, SurfacePolicy, TimestampText, }; use serde::Serialize; use serde_json::Value; use crate::mcp::fault::{FaultKind, FaultRecord, FaultStage}; #[derive(Clone, Serialize, libmcp::SelectorProjection)] pub(crate) struct HypothesisSelector { pub(crate) slug: String, pub(crate) title: String, } #[derive(Clone, Serialize, libmcp::SelectorProjection)] pub(crate) struct ExperimentSelector { pub(crate) slug: String, pub(crate) title: String, } #[derive(Clone, Serialize, libmcp::SelectorProjection)] pub(crate) struct FrontierSelector { pub(crate) slug: String, #[libmcp(title)] pub(crate) label: String, } #[derive(Clone, Serialize)] pub(crate) struct FrontierSummaryProjection { pub(crate) slug: String, pub(crate) label: String, pub(crate) objective: String, pub(crate) status: String, pub(crate) active_hypothesis_count: u64, pub(crate) open_experiment_count: u64, pub(crate) updated_at: TimestampText, } #[derive(Clone, Serialize)] pub(crate) struct FrontierBriefProjection { #[serde(skip_serializing_if = "Option::is_none")] pub(crate) situation: Option, pub(crate) roadmap: Vec, pub(crate) unknowns: Vec, pub(crate) scoreboard_metric_keys: Vec, pub(crate) revision: u64, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) updated_at: Option, } #[derive(Clone, Serialize)] pub(crate) struct RoadmapItemProjection { pub(crate) rank: u32, pub(crate) hypothesis: Option, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) summary: Option, } #[derive(Clone, Serialize)] pub(crate) struct HypothesisRoadmapProjection { pub(crate) slug: String, pub(crate) title: String, pub(crate) summary: String, } #[derive(Clone, Serialize)] pub(crate) struct FrontierRecordProjection { pub(crate) slug: String, pub(crate) label: String, pub(crate) objective: String, pub(crate) status: String, pub(crate) revision: u64, pub(crate) created_at: TimestampText, pub(crate) updated_at: TimestampText, pub(crate) brief: FrontierBriefProjection, } #[derive(Clone, Serialize, libmcp::ToolProjection)] #[libmcp(kind = "read")] pub(crate) struct FrontierReadOutput { pub(crate) record: FrontierRecordProjection, } #[derive(Clone, Serialize, libmcp::ToolProjection)] #[libmcp(kind = "list")] pub(crate) struct FrontierListOutput { pub(crate) count: usize, pub(crate) frontiers: Vec, } #[derive(Clone, Serialize, libmcp::ToolProjection)] #[libmcp(kind = "overview")] pub(crate) struct FrontierOpenOutput { pub(crate) frontier: FrontierOpenFrontierProjection, pub(crate) active_tags: Vec, pub(crate) scoreboard_metrics: Vec, pub(crate) active_metric_keys: Vec, pub(crate) active_hypotheses: Vec, pub(crate) open_experiments: Vec, } #[derive(Clone, Serialize)] pub(crate) struct FrontierOpenFrontierProjection { pub(crate) slug: String, pub(crate) label: String, pub(crate) objective: String, pub(crate) status: String, pub(crate) brief: FrontierBriefProjection, } #[derive(Clone, Serialize)] pub(crate) struct HypothesisSummaryProjection { pub(crate) slug: String, pub(crate) archived: bool, pub(crate) title: String, pub(crate) summary: String, pub(crate) tags: Vec, pub(crate) open_experiment_count: u64, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) latest_verdict: Option, pub(crate) updated_at: TimestampText, } #[derive(Clone, Serialize)] pub(crate) struct HypothesisRecordProjection { pub(crate) slug: String, pub(crate) archived: bool, pub(crate) title: String, pub(crate) summary: String, pub(crate) body: String, pub(crate) tags: Vec, pub(crate) revision: u64, pub(crate) created_at: TimestampText, pub(crate) updated_at: TimestampText, } #[derive(Clone, Serialize)] pub(crate) struct HypothesisReadRecordProjection { pub(crate) slug: String, pub(crate) archived: bool, pub(crate) title: String, pub(crate) summary: String, pub(crate) tags: Vec, pub(crate) revision: u64, pub(crate) updated_at: TimestampText, } #[derive(Clone, Serialize)] pub(crate) struct FrontierLinkProjection { pub(crate) slug: String, pub(crate) label: String, pub(crate) status: String, } #[derive(Clone, Serialize)] pub(crate) struct HypothesisDetailConcise { pub(crate) record: HypothesisReadRecordProjection, pub(crate) frontier: FrontierLinkProjection, pub(crate) parents: usize, pub(crate) children: usize, pub(crate) open_experiments: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) latest_closed_experiment: Option, pub(crate) artifact_count: usize, } #[derive(Clone, Serialize)] pub(crate) struct HypothesisDetailFull { pub(crate) record: HypothesisRecordProjection, pub(crate) frontier: FrontierLinkProjection, pub(crate) parents: Vec, pub(crate) children: Vec, pub(crate) open_experiments: Vec, pub(crate) closed_experiments: Vec, pub(crate) artifacts: Vec, } pub(crate) struct HypothesisDetailOutput { concise: HypothesisDetailConcise, full: HypothesisDetailFull, } impl StructuredProjection for HypothesisDetailOutput { fn concise_projection(&self) -> Result { Ok(serde_json::to_value(&self.concise)?) } fn full_projection(&self) -> Result { Ok(serde_json::to_value(&self.full)?) } } impl SurfacePolicy for HypothesisDetailOutput { const KIND: SurfaceKind = SurfaceKind::Read; } #[derive(Clone, Serialize, libmcp::ToolProjection)] #[libmcp(kind = "mutation")] pub(crate) struct HypothesisRecordOutput { pub(crate) record: HypothesisRecordProjection, } #[derive(Clone, Serialize, libmcp::ToolProjection)] #[libmcp(kind = "list")] pub(crate) struct HypothesisListOutput { pub(crate) count: usize, pub(crate) hypotheses: Vec, } #[derive(Clone, Serialize)] pub(crate) struct ExperimentSummaryProjection { pub(crate) slug: String, pub(crate) archived: bool, pub(crate) title: String, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) summary: Option, pub(crate) tags: Vec, pub(crate) status: String, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) verdict: Option, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) primary_metric: Option, pub(crate) updated_at: TimestampText, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) closed_at: Option, } #[derive(Clone, Serialize)] pub(crate) struct ExperimentRecordProjection { pub(crate) slug: String, pub(crate) archived: bool, pub(crate) title: String, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) summary: Option, pub(crate) tags: Vec, pub(crate) status: String, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) outcome: Option, pub(crate) revision: u64, pub(crate) created_at: TimestampText, pub(crate) updated_at: TimestampText, } #[derive(Clone, Serialize)] pub(crate) struct ExperimentReadRecordProjection { pub(crate) slug: String, pub(crate) archived: bool, pub(crate) title: String, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) summary: Option, pub(crate) tags: Vec, pub(crate) status: String, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) verdict: Option, pub(crate) revision: u64, pub(crate) updated_at: TimestampText, } #[derive(Clone, Serialize)] pub(crate) struct ExperimentDetailConcise { pub(crate) record: ExperimentReadRecordProjection, pub(crate) frontier: FrontierLinkProjection, pub(crate) owning_hypothesis: HypothesisSummaryProjection, pub(crate) parents: usize, pub(crate) children: usize, pub(crate) artifact_count: usize, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) outcome: Option, } #[derive(Clone, Serialize)] pub(crate) struct ExperimentDetailFull { pub(crate) record: ExperimentRecordProjection, pub(crate) frontier: FrontierLinkProjection, pub(crate) owning_hypothesis: HypothesisSummaryProjection, pub(crate) parents: Vec, pub(crate) children: Vec, pub(crate) artifacts: Vec, } pub(crate) struct ExperimentDetailOutput { concise: ExperimentDetailConcise, full: ExperimentDetailFull, } impl StructuredProjection for ExperimentDetailOutput { fn concise_projection(&self) -> Result { Ok(serde_json::to_value(&self.concise)?) } fn full_projection(&self) -> Result { Ok(serde_json::to_value(&self.full)?) } } impl SurfacePolicy for ExperimentDetailOutput { const KIND: SurfaceKind = SurfaceKind::Read; } #[derive(Clone, Serialize, libmcp::ToolProjection)] #[libmcp(kind = "mutation")] pub(crate) struct ExperimentRecordOutput { pub(crate) record: ExperimentRecordProjection, } #[derive(Clone, Serialize, libmcp::ToolProjection)] #[libmcp(kind = "list")] pub(crate) struct ExperimentListOutput { pub(crate) count: usize, pub(crate) experiments: Vec, } #[derive(Clone, Serialize)] pub(crate) struct ArtifactSummaryProjection { pub(crate) slug: String, pub(crate) kind: String, pub(crate) label: String, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) summary: Option, pub(crate) locator: String, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) media_type: Option, pub(crate) updated_at: TimestampText, } #[derive(Clone, Serialize)] pub(crate) struct ArtifactRecordProjection { pub(crate) slug: String, pub(crate) kind: String, pub(crate) label: String, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) summary: Option, pub(crate) locator: String, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) media_type: Option, pub(crate) revision: u64, pub(crate) created_at: TimestampText, pub(crate) updated_at: TimestampText, } #[derive(Clone, Serialize)] pub(crate) struct ArtifactReadRecordProjection { pub(crate) slug: String, pub(crate) kind: String, pub(crate) label: String, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) summary: Option, pub(crate) locator: String, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) media_type: Option, pub(crate) revision: u64, pub(crate) updated_at: TimestampText, } #[derive(Clone, Serialize)] pub(crate) struct ArtifactDetailConcise { pub(crate) record: ArtifactReadRecordProjection, pub(crate) attachment_count: usize, } #[derive(Clone, Serialize)] pub(crate) struct ArtifactDetailFull { pub(crate) record: ArtifactRecordProjection, pub(crate) attachments: Vec, } pub(crate) struct ArtifactDetailOutput { concise: ArtifactDetailConcise, full: ArtifactDetailFull, } impl StructuredProjection for ArtifactDetailOutput { fn concise_projection(&self) -> Result { Ok(serde_json::to_value(&self.concise)?) } fn full_projection(&self) -> Result { Ok(serde_json::to_value(&self.full)?) } } impl SurfacePolicy for ArtifactDetailOutput { const KIND: SurfaceKind = SurfaceKind::Read; const REFERENCE_ONLY: bool = true; } #[derive(Clone, Serialize, libmcp::ToolProjection)] #[libmcp(kind = "mutation", reference_only)] pub(crate) struct ArtifactRecordOutput { pub(crate) record: ArtifactRecordProjection, } #[derive(Clone, Serialize, libmcp::ToolProjection)] #[libmcp(kind = "list", reference_only)] pub(crate) struct ArtifactListOutput { pub(crate) count: usize, pub(crate) artifacts: Vec, } #[derive(Clone, Serialize)] pub(crate) struct HypothesisCurrentStateProjection { pub(crate) hypothesis: HypothesisSummaryProjection, pub(crate) open_experiments: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) latest_closed_experiment: Option, } #[derive(Clone, Serialize)] pub(crate) struct MetricKeySummaryProjection { pub(crate) key: String, pub(crate) unit: String, pub(crate) objective: String, pub(crate) visibility: String, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) description: Option, pub(crate) reference_count: u64, } #[derive(Clone, Serialize)] pub(crate) struct MetricBestEntryProjection { pub(crate) experiment: ExperimentSummaryProjection, pub(crate) hypothesis: HypothesisSummaryProjection, pub(crate) value: f64, pub(crate) dimensions: BTreeMap, } #[derive(Clone, Serialize)] pub(crate) struct MetricObservationSummaryProjection { pub(crate) key: String, pub(crate) value: f64, pub(crate) unit: String, pub(crate) objective: String, } #[derive(Clone, Serialize)] pub(crate) struct ExperimentOutcomeProjection { pub(crate) backend: String, pub(crate) command: CommandRecipeProjection, pub(crate) dimensions: BTreeMap, pub(crate) primary_metric: MetricValueProjection, pub(crate) supporting_metrics: Vec, pub(crate) verdict: String, pub(crate) rationale: String, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) analysis: Option, pub(crate) closed_at: TimestampText, } #[derive(Clone, Serialize)] pub(crate) struct ExperimentAnalysisProjection { pub(crate) summary: String, pub(crate) body: String, } #[derive(Clone, Serialize)] pub(crate) struct MetricValueProjection { pub(crate) key: String, pub(crate) value: f64, } #[derive(Clone, Serialize)] pub(crate) struct CommandRecipeProjection { pub(crate) argv: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) working_directory: Option, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) env: Option>, } #[derive(Clone, Serialize)] pub(crate) struct VertexSummaryProjection { pub(crate) kind: String, pub(crate) slug: String, pub(crate) archived: bool, pub(crate) title: String, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) summary: Option, pub(crate) updated_at: TimestampText, } #[derive(Clone, Serialize)] pub(crate) struct AttachmentTargetProjection { pub(crate) kind: String, pub(crate) slug: String, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) title: Option, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) label: Option, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) summary: Option, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) status: Option, } #[derive(Clone, Serialize, libmcp::ToolProjection)] #[libmcp(kind = "list")] pub(crate) struct MetricKeysOutput { pub(crate) count: usize, pub(crate) metrics: Vec, } #[derive(Clone, Serialize, libmcp::ToolProjection)] #[libmcp(kind = "list")] pub(crate) struct MetricBestOutput { pub(crate) count: usize, pub(crate) entries: Vec, } #[derive(Clone, Serialize)] pub(crate) struct ExperimentNearestHitProjection { pub(crate) experiment: ExperimentSummaryProjection, pub(crate) hypothesis: HypothesisSummaryProjection, pub(crate) dimensions: BTreeMap, pub(crate) reasons: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) metric_value: Option, } #[derive(Clone, Serialize, libmcp::ToolProjection)] #[libmcp(kind = "read")] pub(crate) struct ExperimentNearestOutput { #[serde(skip_serializing_if = "Option::is_none")] pub(crate) metric: Option, pub(crate) target_dimensions: BTreeMap, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) accepted: Option, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) kept: Option, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) rejected: Option, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) champion: Option, } #[derive(Clone, Serialize)] pub(crate) struct TagRecordProjection { pub(crate) name: String, pub(crate) description: String, pub(crate) created_at: TimestampText, } #[derive(Clone, Serialize, libmcp::ToolProjection)] #[libmcp(kind = "mutation")] pub(crate) struct TagRecordOutput { pub(crate) record: TagRecordProjection, } #[derive(Clone, Serialize, libmcp::ToolProjection)] #[libmcp(kind = "list")] pub(crate) struct TagListOutput { pub(crate) count: usize, pub(crate) tags: Vec, } #[derive(Clone, Serialize)] pub(crate) struct MetricDefinitionProjection { pub(crate) key: String, pub(crate) unit: String, pub(crate) objective: String, pub(crate) visibility: String, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) description: Option, pub(crate) created_at: TimestampText, pub(crate) updated_at: TimestampText, } #[derive(Clone, Serialize, libmcp::ToolProjection)] #[libmcp(kind = "mutation")] pub(crate) struct MetricDefinitionOutput { pub(crate) record: MetricDefinitionProjection, } #[derive(Clone, Serialize)] pub(crate) struct RunDimensionDefinitionProjection { pub(crate) key: String, pub(crate) value_type: String, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) description: Option, pub(crate) created_at: TimestampText, pub(crate) updated_at: TimestampText, } #[derive(Clone, Serialize, libmcp::ToolProjection)] #[libmcp(kind = "mutation")] pub(crate) struct RunDimensionDefinitionOutput { pub(crate) record: RunDimensionDefinitionProjection, } #[derive(Clone, Serialize, libmcp::ToolProjection)] #[libmcp(kind = "list")] pub(crate) struct RunDimensionListOutput { pub(crate) count: usize, pub(crate) dimensions: Vec, } #[derive(Clone, Serialize)] pub(crate) struct HistoryEntryProjection { pub(crate) revision: u64, pub(crate) event_kind: String, pub(crate) occurred_at: TimestampText, pub(crate) snapshot: Value, } #[derive(Clone, Serialize, libmcp::ToolProjection)] #[libmcp(kind = "list")] pub(crate) struct HistoryOutput { pub(crate) count: usize, pub(crate) history: Vec, } pub(crate) fn frontier_list(frontiers: &[FrontierSummary]) -> FrontierListOutput { FrontierListOutput { count: frontiers.len(), frontiers: frontiers.iter().map(frontier_summary).collect(), } } pub(crate) fn frontier_record( store: &ProjectStore, frontier: &FrontierRecord, operation: &str, ) -> Result { Ok(FrontierReadOutput { record: frontier_record_projection(store, frontier, operation)?, }) } pub(crate) fn frontier_open(projection: &FrontierOpenProjection) -> FrontierOpenOutput { FrontierOpenOutput { frontier: FrontierOpenFrontierProjection { slug: projection.frontier.slug.to_string(), label: projection.frontier.label.to_string(), objective: projection.frontier.objective.to_string(), status: projection.frontier.status.as_str().to_owned(), brief: frontier_brief_projection( &projection.frontier.brief, projection .frontier .brief .roadmap .iter() .map(|item| { let hypothesis = projection .active_hypotheses .iter() .find(|state| state.hypothesis.id == item.hypothesis_id) .map(|state| HypothesisRoadmapProjection { slug: state.hypothesis.slug.to_string(), title: state.hypothesis.title.to_string(), summary: state.hypothesis.summary.to_string(), }); RoadmapItemProjection { rank: item.rank, hypothesis, summary: item.summary.as_ref().map(ToString::to_string), } }) .collect(), ), }, active_tags: projection .active_tags .iter() .map(ToString::to_string) .collect(), scoreboard_metrics: projection .scoreboard_metric_keys .iter() .map(metric_key_summary) .collect(), active_metric_keys: projection .active_metric_keys .iter() .map(metric_key_summary) .collect(), active_hypotheses: projection .active_hypotheses .iter() .map(hypothesis_current_state) .collect(), open_experiments: projection .open_experiments .iter() .map(experiment_summary) .collect(), } } pub(crate) fn hypothesis_record( hypothesis: &fidget_spinner_core::HypothesisRecord, ) -> HypothesisRecordOutput { HypothesisRecordOutput { record: hypothesis_record_projection(hypothesis), } } pub(crate) fn hypothesis_list( hypotheses: &[fidget_spinner_store_sqlite::HypothesisSummary], ) -> HypothesisListOutput { HypothesisListOutput { count: hypotheses.len(), hypotheses: hypotheses.iter().map(hypothesis_summary).collect(), } } pub(crate) fn hypothesis_detail( store: &ProjectStore, detail: &HypothesisDetail, operation: &str, ) -> Result { let frontier = store .read_frontier(&detail.record.frontier_id.to_string()) .map_err(store_fault(operation))?; let frontier = FrontierLinkProjection { slug: frontier.slug.to_string(), label: frontier.label.to_string(), status: frontier.status.as_str().to_owned(), }; Ok(HypothesisDetailOutput { concise: HypothesisDetailConcise { record: HypothesisReadRecordProjection { slug: detail.record.slug.to_string(), archived: detail.record.archived, title: detail.record.title.to_string(), summary: detail.record.summary.to_string(), tags: detail.record.tags.iter().map(ToString::to_string).collect(), revision: detail.record.revision, updated_at: timestamp_value(detail.record.updated_at), }, frontier: frontier.clone(), parents: detail.parents.len(), children: detail.children.len(), open_experiments: detail .open_experiments .iter() .map(experiment_summary) .collect(), latest_closed_experiment: detail.closed_experiments.first().map(experiment_summary), artifact_count: detail.artifacts.len(), }, full: HypothesisDetailFull { record: hypothesis_record_projection(&detail.record), frontier, parents: detail.parents.iter().map(vertex_summary).collect(), children: detail.children.iter().map(vertex_summary).collect(), open_experiments: detail .open_experiments .iter() .map(experiment_summary) .collect(), closed_experiments: detail .closed_experiments .iter() .map(experiment_summary) .collect(), artifacts: detail.artifacts.iter().map(artifact_summary).collect(), }, }) } pub(crate) fn experiment_record( experiment: &fidget_spinner_core::ExperimentRecord, ) -> ExperimentRecordOutput { ExperimentRecordOutput { record: experiment_record_projection(experiment), } } pub(crate) fn experiment_list(experiments: &[ExperimentSummary]) -> ExperimentListOutput { ExperimentListOutput { count: experiments.len(), experiments: experiments.iter().map(experiment_summary).collect(), } } pub(crate) fn experiment_detail( store: &ProjectStore, detail: &ExperimentDetail, operation: &str, ) -> Result { let frontier = store .read_frontier(&detail.record.frontier_id.to_string()) .map_err(store_fault(operation))?; let frontier = FrontierLinkProjection { slug: frontier.slug.to_string(), label: frontier.label.to_string(), status: frontier.status.as_str().to_owned(), }; Ok(ExperimentDetailOutput { concise: ExperimentDetailConcise { record: ExperimentReadRecordProjection { slug: detail.record.slug.to_string(), archived: detail.record.archived, title: detail.record.title.to_string(), summary: detail.record.summary.as_ref().map(ToString::to_string), tags: detail.record.tags.iter().map(ToString::to_string).collect(), status: detail.record.status.as_str().to_owned(), verdict: detail .record .outcome .as_ref() .map(|outcome| outcome.verdict.as_str().to_owned()), revision: detail.record.revision, updated_at: timestamp_value(detail.record.updated_at), }, frontier: frontier.clone(), owning_hypothesis: hypothesis_summary(&detail.owning_hypothesis), parents: detail.parents.len(), children: detail.children.len(), artifact_count: detail.artifacts.len(), outcome: detail.record.outcome.as_ref().map(experiment_outcome), }, full: ExperimentDetailFull { record: experiment_record_projection(&detail.record), frontier, owning_hypothesis: hypothesis_summary(&detail.owning_hypothesis), parents: detail.parents.iter().map(vertex_summary).collect(), children: detail.children.iter().map(vertex_summary).collect(), artifacts: detail.artifacts.iter().map(artifact_summary).collect(), }, }) } pub(crate) fn artifact_record( artifact: &fidget_spinner_core::ArtifactRecord, ) -> ArtifactRecordOutput { ArtifactRecordOutput { record: artifact_record_projection(artifact), } } pub(crate) fn artifact_list(artifacts: &[ArtifactSummary]) -> ArtifactListOutput { ArtifactListOutput { count: artifacts.len(), artifacts: artifacts.iter().map(artifact_summary).collect(), } } pub(crate) fn artifact_detail( store: &ProjectStore, detail: &ArtifactDetail, operation: &str, ) -> Result { let attachments = detail .attachments .iter() .copied() .map(|attachment| attachment_target(store, attachment, operation)) .collect::, _>>()?; Ok(ArtifactDetailOutput { concise: ArtifactDetailConcise { record: ArtifactReadRecordProjection { slug: detail.record.slug.to_string(), kind: detail.record.kind.as_str().to_owned(), label: detail.record.label.to_string(), summary: detail.record.summary.as_ref().map(ToString::to_string), locator: detail.record.locator.to_string(), media_type: detail.record.media_type.as_ref().map(ToString::to_string), revision: detail.record.revision, updated_at: timestamp_value(detail.record.updated_at), }, attachment_count: detail.attachments.len(), }, full: ArtifactDetailFull { record: artifact_record_projection(&detail.record), attachments, }, }) } pub(crate) fn metric_keys(keys: &[MetricKeySummary]) -> MetricKeysOutput { MetricKeysOutput { count: keys.len(), metrics: keys.iter().map(metric_key_summary).collect(), } } pub(crate) fn metric_best(entries: &[MetricBestEntry]) -> MetricBestOutput { MetricBestOutput { count: entries.len(), entries: entries.iter().map(metric_best_entry).collect(), } } pub(crate) fn experiment_nearest(result: &ExperimentNearestResult) -> ExperimentNearestOutput { ExperimentNearestOutput { metric: result.metric.as_ref().map(metric_key_summary), target_dimensions: dimension_map(&result.target_dimensions), accepted: result.accepted.as_ref().map(experiment_nearest_hit), kept: result.kept.as_ref().map(experiment_nearest_hit), rejected: result.rejected.as_ref().map(experiment_nearest_hit), champion: result.champion.as_ref().map(experiment_nearest_hit), } } pub(crate) fn tag_record(tag: &TagRecord) -> TagRecordOutput { TagRecordOutput { record: tag_record_projection(tag), } } pub(crate) fn tag_list(tags: &[TagRecord]) -> TagListOutput { TagListOutput { count: tags.len(), tags: tags.iter().map(tag_record_projection).collect(), } } pub(crate) fn metric_definition(metric: &MetricDefinition) -> MetricDefinitionOutput { MetricDefinitionOutput { record: metric_definition_projection(metric), } } pub(crate) fn run_dimension_definition( dimension: &RunDimensionDefinition, ) -> RunDimensionDefinitionOutput { RunDimensionDefinitionOutput { record: run_dimension_definition_projection(dimension), } } pub(crate) fn run_dimension_list(dimensions: &[RunDimensionDefinition]) -> RunDimensionListOutput { RunDimensionListOutput { count: dimensions.len(), dimensions: dimensions .iter() .map(run_dimension_definition_projection) .collect(), } } pub(crate) fn history(history: &[EntityHistoryEntry]) -> HistoryOutput { HistoryOutput { count: history.len(), history: history.iter().map(history_entry_projection).collect(), } } fn frontier_summary(frontier: &FrontierSummary) -> FrontierSummaryProjection { FrontierSummaryProjection { slug: frontier.slug.to_string(), label: frontier.label.to_string(), objective: frontier.objective.to_string(), status: frontier.status.as_str().to_owned(), active_hypothesis_count: frontier.active_hypothesis_count, open_experiment_count: frontier.open_experiment_count, updated_at: timestamp_value(frontier.updated_at), } } fn frontier_record_projection( store: &ProjectStore, frontier: &FrontierRecord, operation: &str, ) -> Result { let roadmap = frontier .brief .roadmap .iter() .map(|item| { let hypothesis = store .read_hypothesis(&item.hypothesis_id.to_string()) .map_err(store_fault(operation))?; Ok(RoadmapItemProjection { rank: item.rank, hypothesis: Some(HypothesisRoadmapProjection { slug: hypothesis.record.slug.to_string(), title: hypothesis.record.title.to_string(), summary: hypothesis.record.summary.to_string(), }), summary: item.summary.as_ref().map(ToString::to_string), }) }) .collect::, FaultRecord>>()?; Ok(FrontierRecordProjection { slug: frontier.slug.to_string(), label: frontier.label.to_string(), objective: frontier.objective.to_string(), status: frontier.status.as_str().to_owned(), revision: frontier.revision, created_at: timestamp_value(frontier.created_at), updated_at: timestamp_value(frontier.updated_at), brief: frontier_brief_projection(&frontier.brief, roadmap), }) } fn frontier_brief_projection( brief: &FrontierBrief, roadmap: Vec, ) -> FrontierBriefProjection { FrontierBriefProjection { situation: brief.situation.as_ref().map(ToString::to_string), roadmap, unknowns: brief.unknowns.iter().map(ToString::to_string).collect(), scoreboard_metric_keys: brief .scoreboard_metric_keys .iter() .map(ToString::to_string) .collect(), revision: brief.revision, updated_at: brief.updated_at.map(timestamp_value), } } fn hypothesis_summary( hypothesis: &fidget_spinner_store_sqlite::HypothesisSummary, ) -> HypothesisSummaryProjection { HypothesisSummaryProjection { slug: hypothesis.slug.to_string(), archived: hypothesis.archived, title: hypothesis.title.to_string(), summary: hypothesis.summary.to_string(), tags: hypothesis.tags.iter().map(ToString::to_string).collect(), open_experiment_count: hypothesis.open_experiment_count, latest_verdict: hypothesis .latest_verdict .map(|verdict| verdict.as_str().to_owned()), updated_at: timestamp_value(hypothesis.updated_at), } } fn hypothesis_record_projection( hypothesis: &fidget_spinner_core::HypothesisRecord, ) -> HypothesisRecordProjection { HypothesisRecordProjection { slug: hypothesis.slug.to_string(), archived: hypothesis.archived, title: hypothesis.title.to_string(), summary: hypothesis.summary.to_string(), body: hypothesis.body.to_string(), tags: hypothesis.tags.iter().map(ToString::to_string).collect(), revision: hypothesis.revision, created_at: timestamp_value(hypothesis.created_at), updated_at: timestamp_value(hypothesis.updated_at), } } fn experiment_summary(experiment: &ExperimentSummary) -> ExperimentSummaryProjection { ExperimentSummaryProjection { slug: experiment.slug.to_string(), archived: experiment.archived, title: experiment.title.to_string(), summary: experiment.summary.as_ref().map(ToString::to_string), tags: experiment.tags.iter().map(ToString::to_string).collect(), status: experiment.status.as_str().to_owned(), verdict: experiment .verdict .map(|verdict| verdict.as_str().to_owned()), primary_metric: experiment .primary_metric .as_ref() .map(metric_observation_summary), updated_at: timestamp_value(experiment.updated_at), closed_at: experiment.closed_at.map(timestamp_value), } } fn experiment_record_projection( experiment: &fidget_spinner_core::ExperimentRecord, ) -> ExperimentRecordProjection { ExperimentRecordProjection { slug: experiment.slug.to_string(), archived: experiment.archived, title: experiment.title.to_string(), summary: experiment.summary.as_ref().map(ToString::to_string), tags: experiment.tags.iter().map(ToString::to_string).collect(), status: experiment.status.as_str().to_owned(), outcome: experiment.outcome.as_ref().map(experiment_outcome), revision: experiment.revision, created_at: timestamp_value(experiment.created_at), updated_at: timestamp_value(experiment.updated_at), } } fn artifact_summary(artifact: &ArtifactSummary) -> ArtifactSummaryProjection { ArtifactSummaryProjection { slug: artifact.slug.to_string(), kind: artifact.kind.as_str().to_owned(), label: artifact.label.to_string(), summary: artifact.summary.as_ref().map(ToString::to_string), locator: artifact.locator.to_string(), media_type: artifact.media_type.as_ref().map(ToString::to_string), updated_at: timestamp_value(artifact.updated_at), } } fn artifact_record_projection( artifact: &fidget_spinner_core::ArtifactRecord, ) -> ArtifactRecordProjection { ArtifactRecordProjection { slug: artifact.slug.to_string(), kind: artifact.kind.as_str().to_owned(), label: artifact.label.to_string(), summary: artifact.summary.as_ref().map(ToString::to_string), locator: artifact.locator.to_string(), media_type: artifact.media_type.as_ref().map(ToString::to_string), revision: artifact.revision, created_at: timestamp_value(artifact.created_at), updated_at: timestamp_value(artifact.updated_at), } } fn hypothesis_current_state(state: &HypothesisCurrentState) -> HypothesisCurrentStateProjection { HypothesisCurrentStateProjection { hypothesis: hypothesis_summary(&state.hypothesis), open_experiments: state .open_experiments .iter() .map(experiment_summary) .collect(), latest_closed_experiment: state .latest_closed_experiment .as_ref() .map(experiment_summary), } } fn metric_key_summary(metric: &MetricKeySummary) -> MetricKeySummaryProjection { MetricKeySummaryProjection { key: metric.key.to_string(), unit: metric.unit.as_str().to_owned(), objective: metric.objective.as_str().to_owned(), visibility: metric.visibility.as_str().to_owned(), description: metric.description.as_ref().map(ToString::to_string), reference_count: metric.reference_count, } } fn tag_record_projection(tag: &TagRecord) -> TagRecordProjection { TagRecordProjection { name: tag.name.to_string(), description: tag.description.to_string(), created_at: timestamp_value(tag.created_at), } } fn metric_definition_projection(metric: &MetricDefinition) -> MetricDefinitionProjection { MetricDefinitionProjection { key: metric.key.to_string(), unit: metric.unit.as_str().to_owned(), objective: metric.objective.as_str().to_owned(), visibility: metric.visibility.as_str().to_owned(), description: metric.description.as_ref().map(ToString::to_string), created_at: timestamp_value(metric.created_at), updated_at: timestamp_value(metric.updated_at), } } fn run_dimension_definition_projection( dimension: &RunDimensionDefinition, ) -> RunDimensionDefinitionProjection { RunDimensionDefinitionProjection { key: dimension.key.to_string(), value_type: dimension.value_type.as_str().to_owned(), description: dimension.description.as_ref().map(ToString::to_string), created_at: timestamp_value(dimension.created_at), updated_at: timestamp_value(dimension.updated_at), } } fn history_entry_projection(entry: &EntityHistoryEntry) -> HistoryEntryProjection { HistoryEntryProjection { revision: entry.revision, event_kind: entry.event_kind.to_string(), occurred_at: timestamp_value(entry.occurred_at), snapshot: entry.snapshot.clone(), } } fn metric_best_entry(entry: &MetricBestEntry) -> MetricBestEntryProjection { MetricBestEntryProjection { experiment: experiment_summary(&entry.experiment), hypothesis: hypothesis_summary(&entry.hypothesis), value: entry.value, dimensions: dimension_map(&entry.dimensions), } } fn experiment_nearest_hit(hit: &ExperimentNearestHit) -> ExperimentNearestHitProjection { ExperimentNearestHitProjection { experiment: experiment_summary(&hit.experiment), hypothesis: hypothesis_summary(&hit.hypothesis), dimensions: dimension_map(&hit.dimensions), reasons: hit.reasons.iter().map(ToString::to_string).collect(), metric_value: hit.metric_value.as_ref().map(metric_observation_summary), } } fn metric_observation_summary( metric: &MetricObservationSummary, ) -> MetricObservationSummaryProjection { MetricObservationSummaryProjection { key: metric.key.to_string(), value: metric.value, unit: metric.unit.as_str().to_owned(), objective: metric.objective.as_str().to_owned(), } } fn experiment_outcome(outcome: &ExperimentOutcome) -> ExperimentOutcomeProjection { ExperimentOutcomeProjection { backend: outcome.backend.as_str().to_owned(), command: command_recipe(&outcome.command), dimensions: dimension_map(&outcome.dimensions), primary_metric: metric_value(&outcome.primary_metric), supporting_metrics: outcome .supporting_metrics .iter() .map(metric_value) .collect(), verdict: outcome.verdict.as_str().to_owned(), rationale: outcome.rationale.to_string(), analysis: outcome.analysis.as_ref().map(experiment_analysis), closed_at: timestamp_value(outcome.closed_at), } } fn experiment_analysis(analysis: &ExperimentAnalysis) -> ExperimentAnalysisProjection { ExperimentAnalysisProjection { summary: analysis.summary.to_string(), body: analysis.body.to_string(), } } fn metric_value(metric: &MetricValue) -> MetricValueProjection { MetricValueProjection { key: metric.key.to_string(), value: metric.value, } } fn command_recipe(command: &CommandRecipe) -> CommandRecipeProjection { CommandRecipeProjection { argv: command.argv.iter().map(ToString::to_string).collect(), working_directory: command.working_directory.as_ref().map(ToString::to_string), env: (!command.env.is_empty()).then(|| { command .env .iter() .map(|(key, value)| (key.clone(), value.clone())) .collect() }), } } fn dimension_map( dimensions: &BTreeMap, ) -> BTreeMap { dimensions .iter() .map(|(key, value)| (key.to_string(), run_dimension_value(value))) .collect() } fn run_dimension_value(value: &RunDimensionValue) -> Value { match value { RunDimensionValue::String(value) => Value::String(value.to_string()), RunDimensionValue::Numeric(value) => serde_json::json!(value), RunDimensionValue::Boolean(value) => serde_json::json!(value), RunDimensionValue::Timestamp(value) => Value::String(value.to_string()), } } fn vertex_summary(vertex: &VertexSummary) -> VertexSummaryProjection { VertexSummaryProjection { kind: vertex.vertex.kind().as_str().to_owned(), slug: vertex.slug.to_string(), archived: vertex.archived, title: vertex.title.to_string(), summary: vertex.summary.as_ref().map(ToString::to_string), updated_at: timestamp_value(vertex.updated_at), } } fn attachment_target( store: &ProjectStore, attachment: AttachmentTargetRef, operation: &str, ) -> Result { match attachment { AttachmentTargetRef::Frontier(id) => { let frontier = store .read_frontier(&id.to_string()) .map_err(store_fault(operation))?; let reference = FrontierSelector { slug: frontier.slug.to_string(), label: frontier.label.to_string(), }; let selector = reference.selector_ref(); Ok(AttachmentTargetProjection { kind: "frontier".to_owned(), slug: selector.slug, title: None, label: selector.title, summary: None, status: Some(frontier.status.as_str().to_owned()), }) } AttachmentTargetRef::Hypothesis(id) => { let hypothesis = store .read_hypothesis(&id.to_string()) .map_err(store_fault(operation))?; let reference = HypothesisSelector { slug: hypothesis.record.slug.to_string(), title: hypothesis.record.title.to_string(), }; let selector = reference.selector_ref(); Ok(AttachmentTargetProjection { kind: "hypothesis".to_owned(), slug: selector.slug, title: selector.title, label: None, summary: Some(hypothesis.record.summary.to_string()), status: None, }) } AttachmentTargetRef::Experiment(id) => { let experiment = store .read_experiment(&id.to_string()) .map_err(store_fault(operation))?; let reference = ExperimentSelector { slug: experiment.record.slug.to_string(), title: experiment.record.title.to_string(), }; let selector = reference.selector_ref(); Ok(AttachmentTargetProjection { kind: "experiment".to_owned(), slug: selector.slug, title: selector.title, label: None, summary: experiment.record.summary.as_ref().map(ToString::to_string), status: None, }) } } } fn timestamp_value(timestamp: time::OffsetDateTime) -> TimestampText { TimestampText::from(timestamp) } fn store_fault(operation: &str) -> impl Fn(StoreError) -> FaultRecord + '_ { move |error| { FaultRecord::new( FaultKind::Internal, FaultStage::Store, operation, error.to_string(), ) } }