swarm repositories / source
aboutsummaryrefslogtreecommitdiff
path: root/crates/fidget-spinner-core/src/model.rs
diff options
context:
space:
mode:
authormain <main@swarm.moe>2026-03-20 16:00:30 -0400
committermain <main@swarm.moe>2026-03-20 16:00:30 -0400
commit9d63844f3a28fde70b19500422f17379e99e588a (patch)
tree163cfbd65a8d3528346561410ef39eb1183a16f2 /crates/fidget-spinner-core/src/model.rs
parent22fe3d2ce7478450a1d7443c4ecbd85fd4c46716 (diff)
downloadfidget_spinner-9d63844f3a28fde70b19500422f17379e99e588a.zip
Refound Spinner as an austere frontier ledger
Diffstat (limited to 'crates/fidget-spinner-core/src/model.rs')
-rw-r--r--crates/fidget-spinner-core/src/model.rs956
1 files changed, 335 insertions, 621 deletions
diff --git a/crates/fidget-spinner-core/src/model.rs b/crates/fidget-spinner-core/src/model.rs
index 88050a2..cedd882 100644
--- a/crates/fidget-spinner-core/src/model.rs
+++ b/crates/fidget-spinner-core/src/model.rs
@@ -1,15 +1,14 @@
-use std::collections::{BTreeMap, BTreeSet};
+use std::collections::BTreeMap;
use std::fmt::{self, Display, Formatter};
use camino::Utf8PathBuf;
use serde::{Deserialize, Serialize};
-use serde_json::{Map, Value};
+use serde_json::Value;
use time::OffsetDateTime;
use time::format_description::well_known::Rfc3339;
+use uuid::Uuid;
-use crate::{
- AgentSessionId, AnnotationId, ArtifactId, CoreError, ExperimentId, FrontierId, NodeId, RunId,
-};
+use crate::{ArtifactId, CoreError, ExperimentId, FrontierId, HypothesisId};
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
@@ -90,203 +89,159 @@ impl Display for TagName {
}
}
-pub type JsonObject = Map<String, Value>;
-
-#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
-pub enum NodeClass {
- Contract,
- Hypothesis,
- Run,
- Analysis,
- Decision,
- Source,
- Note,
-}
+#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
+#[serde(try_from = "String", into = "String")]
+pub struct Slug(String);
-impl NodeClass {
- #[must_use]
- pub const fn as_str(self) -> &'static str {
- match self {
- Self::Contract => "contract",
- Self::Hypothesis => "hypothesis",
- Self::Run => "run",
- Self::Analysis => "analysis",
- Self::Decision => "decision",
- Self::Source => "source",
- Self::Note => "note",
+impl Slug {
+ pub fn new(value: impl Into<String>) -> Result<Self, CoreError> {
+ let normalized = value.into().trim().to_ascii_lowercase();
+ if normalized.is_empty() {
+ return Err(CoreError::EmptySlug);
}
- }
-
- #[must_use]
- pub const fn default_track(self) -> NodeTrack {
- match self {
- Self::Contract | Self::Hypothesis | Self::Run | Self::Analysis | Self::Decision => {
- NodeTrack::CorePath
+ if Uuid::parse_str(&normalized).is_ok() {
+ return Err(CoreError::UuidLikeSlug(normalized));
+ }
+ let mut previous_was_separator = true;
+ for character in normalized.chars() {
+ if character.is_ascii_lowercase() || character.is_ascii_digit() {
+ previous_was_separator = false;
+ continue;
+ }
+ if matches!(character, '-' | '_') && !previous_was_separator {
+ previous_was_separator = true;
+ continue;
}
- Self::Source | Self::Note => NodeTrack::OffPath,
+ return Err(CoreError::InvalidSlug(normalized));
}
+ if previous_was_separator {
+ return Err(CoreError::InvalidSlug(normalized));
+ }
+ Ok(Self(normalized))
}
-}
-impl Display for NodeClass {
- fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
- formatter.write_str(self.as_str())
+ #[must_use]
+ pub fn as_str(&self) -> &str {
+ &self.0
}
}
-#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
-pub enum NodeTrack {
- CorePath,
- OffPath,
-}
+impl TryFrom<String> for Slug {
+ type Error = CoreError;
-#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
-pub enum AnnotationVisibility {
- HiddenByDefault,
- Visible,
+ fn try_from(value: String) -> Result<Self, Self::Error> {
+ Self::new(value)
+ }
}
-#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
-pub enum DiagnosticSeverity {
- Error,
- Warning,
- Info,
+impl From<Slug> for String {
+ fn from(value: Slug) -> Self {
+ value.0
+ }
}
-impl DiagnosticSeverity {
- #[must_use]
- pub const fn as_str(self) -> &'static str {
- match self {
- Self::Error => "error",
- Self::Warning => "warning",
- Self::Info => "info",
- }
+impl Display for Slug {
+ fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
+ formatter.write_str(&self.0)
}
}
-#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
-pub enum FieldPresence {
- Required,
- Recommended,
- Optional,
+#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(rename_all = "snake_case")]
+pub enum FrontierStatus {
+ Exploring,
+ Paused,
+ Archived,
}
-impl FieldPresence {
+impl FrontierStatus {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
- Self::Required => "required",
- Self::Recommended => "recommended",
- Self::Optional => "optional",
+ Self::Exploring => "exploring",
+ Self::Paused => "paused",
+ Self::Archived => "archived",
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
-pub enum FieldRole {
- Index,
- ProjectionGate,
- RenderOnly,
- Opaque,
+#[serde(rename_all = "snake_case")]
+pub enum MetricUnit {
+ Seconds,
+ Bytes,
+ Count,
+ Ratio,
+ Custom,
}
-impl FieldRole {
+impl MetricUnit {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
- Self::Index => "index",
- Self::ProjectionGate => "projection_gate",
- Self::RenderOnly => "render_only",
- Self::Opaque => "opaque",
+ Self::Seconds => "seconds",
+ Self::Bytes => "bytes",
+ Self::Count => "count",
+ Self::Ratio => "ratio",
+ Self::Custom => "custom",
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
-pub enum InferencePolicy {
- ManualOnly,
- ModelMayInfer,
+#[serde(rename_all = "snake_case")]
+pub enum OptimizationObjective {
+ Minimize,
+ Maximize,
+ Target,
}
-impl InferencePolicy {
+impl OptimizationObjective {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
- Self::ManualOnly => "manual_only",
- Self::ModelMayInfer => "model_may_infer",
+ Self::Minimize => "minimize",
+ Self::Maximize => "maximize",
+ Self::Target => "target",
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "snake_case")]
-pub enum FieldValueType {
- String,
- Numeric,
- Boolean,
- Timestamp,
+pub enum MetricVisibility {
+ Canonical,
+ Minor,
+ Hidden,
+ Archived,
}
-impl FieldValueType {
+impl MetricVisibility {
#[must_use]
- pub const fn is_plottable(self) -> bool {
- matches!(self, Self::Numeric | Self::Timestamp)
- }
-
- #[must_use]
- pub fn accepts(self, value: &Value) -> bool {
+ pub const fn as_str(self) -> &'static str {
match self {
- Self::String => value.is_string(),
- Self::Numeric => value.is_number(),
- Self::Boolean => value.is_boolean(),
- Self::Timestamp => value
- .as_str()
- .is_some_and(|raw| OffsetDateTime::parse(raw, &Rfc3339).is_ok()),
+ Self::Canonical => "canonical",
+ Self::Minor => "minor",
+ Self::Hidden => "hidden",
+ Self::Archived => "archived",
}
}
#[must_use]
- pub const fn as_str(self) -> &'static str {
- match self {
- Self::String => "string",
- Self::Numeric => "numeric",
- Self::Boolean => "boolean",
- Self::Timestamp => "timestamp",
- }
+ pub const fn is_default_visible(self) -> bool {
+ matches!(self, Self::Canonical | Self::Minor)
}
}
-#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
-pub enum FrontierStatus {
- Exploring,
- Paused,
- Saturated,
- Archived,
-}
-
-#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
-pub enum MetricUnit {
- Seconds,
- Bytes,
- Count,
- Ratio,
- Custom,
-}
-
-#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
-pub enum OptimizationObjective {
- Minimize,
- Maximize,
- Target,
-}
-
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct MetricDefinition {
pub key: NonEmptyText,
pub unit: MetricUnit,
pub objective: OptimizationObjective,
+ pub visibility: MetricVisibility,
pub description: Option<NonEmptyText>,
pub created_at: OffsetDateTime,
+ pub updated_at: OffsetDateTime,
}
impl MetricDefinition {
@@ -295,14 +250,51 @@ impl MetricDefinition {
key: NonEmptyText,
unit: MetricUnit,
objective: OptimizationObjective,
+ visibility: MetricVisibility,
description: Option<NonEmptyText>,
) -> Self {
+ let now = OffsetDateTime::now_utc();
Self {
key,
unit,
objective,
+ visibility,
description,
- created_at: OffsetDateTime::now_utc(),
+ created_at: now,
+ updated_at: now,
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
+#[serde(rename_all = "snake_case")]
+pub enum FieldValueType {
+ String,
+ Numeric,
+ Boolean,
+ Timestamp,
+}
+
+impl FieldValueType {
+ #[must_use]
+ pub const fn as_str(self) -> &'static str {
+ match self {
+ Self::String => "string",
+ Self::Numeric => "numeric",
+ Self::Boolean => "boolean",
+ Self::Timestamp => "timestamp",
+ }
+ }
+
+ #[must_use]
+ pub fn accepts(self, value: &Value) -> bool {
+ match self {
+ Self::String => value.is_string(),
+ Self::Numeric => value.is_number(),
+ Self::Boolean => value.is_boolean(),
+ Self::Timestamp => value
+ .as_str()
+ .is_some_and(|raw| OffsetDateTime::parse(raw, &Rfc3339).is_ok()),
}
}
}
@@ -345,6 +337,7 @@ pub struct RunDimensionDefinition {
pub value_type: FieldValueType,
pub description: Option<NonEmptyText>,
pub created_at: OffsetDateTime,
+ pub updated_at: OffsetDateTime,
}
impl RunDimensionDefinition {
@@ -354,38 +347,44 @@ impl RunDimensionDefinition {
value_type: FieldValueType,
description: Option<NonEmptyText>,
) -> Self {
+ let now = OffsetDateTime::now_utc();
Self {
key,
value_type,
description,
- created_at: OffsetDateTime::now_utc(),
+ created_at: now,
+ updated_at: now,
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct MetricValue {
- #[serde(alias = "metric_key")]
pub key: NonEmptyText,
pub value: f64,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
-pub enum RunStatus {
- Queued,
- Running,
- Succeeded,
- Failed,
- Cancelled,
-}
-
-#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(rename_all = "snake_case")]
pub enum ExecutionBackend {
+ Manual,
LocalProcess,
WorktreeProcess,
SshProcess,
}
+impl ExecutionBackend {
+ #[must_use]
+ pub const fn as_str(self) -> &'static str {
+ match self {
+ Self::Manual => "manual",
+ Self::LocalProcess => "local_process",
+ Self::WorktreeProcess => "worktree_process",
+ Self::SshProcess => "ssh_process",
+ }
+ }
+}
+
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum FrontierVerdict {
@@ -395,65 +394,14 @@ pub enum FrontierVerdict {
Rejected,
}
-#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
-pub enum AdmissionState {
- Admitted,
- Rejected,
-}
-
-#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
-pub struct PayloadSchemaRef {
- pub namespace: NonEmptyText,
- pub version: u32,
-}
-
-#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
-pub struct NodePayload {
- pub schema: Option<PayloadSchemaRef>,
- pub fields: JsonObject,
-}
-
-impl NodePayload {
+impl FrontierVerdict {
#[must_use]
- pub fn empty() -> Self {
- Self {
- schema: None,
- fields: JsonObject::new(),
- }
- }
-
- #[must_use]
- pub fn with_schema(schema: PayloadSchemaRef, fields: JsonObject) -> Self {
- Self {
- schema: Some(schema),
- fields,
- }
- }
-
- #[must_use]
- pub fn field(&self, name: &str) -> Option<&Value> {
- self.fields.get(name)
- }
-}
-
-#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
-pub struct NodeAnnotation {
- pub id: AnnotationId,
- pub visibility: AnnotationVisibility,
- pub label: Option<NonEmptyText>,
- pub body: NonEmptyText,
- pub created_at: OffsetDateTime,
-}
-
-impl NodeAnnotation {
- #[must_use]
- pub fn hidden(body: NonEmptyText) -> Self {
- Self {
- id: AnnotationId::fresh(),
- visibility: AnnotationVisibility::HiddenByDefault,
- label: None,
- body,
- created_at: OffsetDateTime::now_utc(),
+ pub const fn as_str(self) -> &'static str {
+ match self {
+ Self::Accepted => "accepted",
+ Self::Kept => "kept",
+ Self::Parked => "parked",
+ Self::Rejected => "rejected",
}
}
}
@@ -466,490 +414,256 @@ pub struct TagRecord {
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
-pub struct ValidationDiagnostic {
- pub severity: DiagnosticSeverity,
- pub code: String,
- pub message: NonEmptyText,
- pub field_name: Option<String>,
-}
-
-#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
-pub struct NodeDiagnostics {
- pub admission: AdmissionState,
- pub items: Vec<ValidationDiagnostic>,
+pub struct CommandRecipe {
+ #[serde(default)]
+ pub working_directory: Option<Utf8PathBuf>,
+ pub argv: Vec<NonEmptyText>,
+ #[serde(default)]
+ pub env: BTreeMap<String, String>,
}
-impl NodeDiagnostics {
- #[must_use]
- pub const fn admitted() -> Self {
- Self {
- admission: AdmissionState::Admitted,
- items: Vec::new(),
+impl CommandRecipe {
+ pub fn new(
+ working_directory: Option<Utf8PathBuf>,
+ argv: Vec<NonEmptyText>,
+ env: BTreeMap<String, String>,
+ ) -> Result<Self, CoreError> {
+ if argv.is_empty() {
+ return Err(CoreError::EmptyCommand);
}
+ Ok(Self {
+ working_directory,
+ argv,
+ env,
+ })
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
-pub struct ProjectFieldSpec {
- pub name: NonEmptyText,
- pub node_classes: BTreeSet<NodeClass>,
- pub presence: FieldPresence,
- pub severity: DiagnosticSeverity,
- pub role: FieldRole,
- pub inference_policy: InferencePolicy,
- #[serde(default)]
- pub value_type: Option<FieldValueType>,
+pub struct FrontierRoadmapItem {
+ pub rank: u32,
+ pub hypothesis_id: HypothesisId,
+ pub summary: Option<NonEmptyText>,
}
-impl ProjectFieldSpec {
- #[must_use]
- pub fn applies_to(&self, class: NodeClass) -> bool {
- self.node_classes.is_empty() || self.node_classes.contains(&class)
- }
-
- #[must_use]
- pub fn is_plottable(&self) -> bool {
- self.value_type.is_some_and(FieldValueType::is_plottable)
- }
+#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
+pub struct FrontierBrief {
+ pub situation: Option<NonEmptyText>,
+ pub roadmap: Vec<FrontierRoadmapItem>,
+ pub unknowns: Vec<NonEmptyText>,
+ pub revision: u64,
+ pub updated_at: Option<OffsetDateTime>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
-pub struct ProjectSchema {
- pub namespace: NonEmptyText,
- pub version: u32,
- pub fields: Vec<ProjectFieldSpec>,
+pub struct FrontierRecord {
+ pub id: FrontierId,
+ pub slug: Slug,
+ pub label: NonEmptyText,
+ pub objective: NonEmptyText,
+ pub status: FrontierStatus,
+ pub brief: FrontierBrief,
+ pub revision: u64,
+ pub created_at: OffsetDateTime,
+ pub updated_at: OffsetDateTime,
}
-impl ProjectSchema {
- #[must_use]
- pub fn default_with_namespace(namespace: NonEmptyText) -> Self {
- Self {
- namespace,
- version: 1,
- fields: Vec::new(),
- }
- }
-
- #[must_use]
- pub fn schema_ref(&self) -> PayloadSchemaRef {
- PayloadSchemaRef {
- namespace: self.namespace.clone(),
- version: self.version,
- }
- }
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+pub struct HypothesisRecord {
+ pub id: HypothesisId,
+ pub slug: Slug,
+ pub frontier_id: FrontierId,
+ pub archived: bool,
+ pub title: NonEmptyText,
+ pub summary: NonEmptyText,
+ pub body: NonEmptyText,
+ pub tags: Vec<TagName>,
+ pub revision: u64,
+ pub created_at: OffsetDateTime,
+ pub updated_at: OffsetDateTime,
+}
- #[must_use]
- pub fn field_spec(&self, class: NodeClass, name: &str) -> Option<&ProjectFieldSpec> {
- self.fields
- .iter()
- .find(|field| field.applies_to(class) && field.name.as_str() == name)
- }
+#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(rename_all = "snake_case")]
+pub enum ExperimentStatus {
+ Open,
+ Closed,
+}
+impl ExperimentStatus {
#[must_use]
- pub fn validate_node(&self, class: NodeClass, payload: &NodePayload) -> NodeDiagnostics {
- let items = self
- .fields
- .iter()
- .filter(|field| field.applies_to(class))
- .filter_map(|field| {
- let value = payload.field(field.name.as_str());
- let is_missing = value.is_none();
- if !is_missing || field.presence == FieldPresence::Optional {
- if let (Some(value), Some(value_type)) = (value, field.value_type)
- && !value_type.accepts(value)
- {
- return Some(ValidationDiagnostic {
- severity: field.severity,
- code: format!("type.{}", field.name.as_str()),
- message: validation_message(format!(
- "project payload field `{}` expected {}, found {}",
- field.name.as_str(),
- value_type.as_str(),
- json_value_kind(value)
- )),
- field_name: Some(field.name.as_str().to_owned()),
- });
- }
- return None;
- }
- Some(ValidationDiagnostic {
- severity: field.severity,
- code: format!("missing.{}", field.name.as_str()),
- message: validation_message(format!(
- "missing project payload field `{}`",
- field.name.as_str()
- )),
- field_name: Some(field.name.as_str().to_owned()),
- })
- })
- .collect();
- NodeDiagnostics {
- admission: AdmissionState::Admitted,
- items,
+ pub const fn as_str(self) -> &'static str {
+ match self {
+ Self::Open => "open",
+ Self::Closed => "closed",
}
}
}
-fn validation_message(value: String) -> NonEmptyText {
- match NonEmptyText::new(value) {
- Ok(message) => message,
- Err(_) => unreachable!("validation diagnostics are never empty"),
- }
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+pub struct ExperimentAnalysis {
+ pub summary: NonEmptyText,
+ pub body: NonEmptyText,
}
-fn json_value_kind(value: &Value) -> &'static str {
- match value {
- Value::Null => "null",
- Value::Bool(_) => "boolean",
- Value::Number(_) => "numeric",
- Value::String(_) => "string",
- Value::Array(_) => "array",
- Value::Object(_) => "object",
- }
+#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
+pub struct ExperimentOutcome {
+ pub backend: ExecutionBackend,
+ pub command: CommandRecipe,
+ pub dimensions: BTreeMap<NonEmptyText, RunDimensionValue>,
+ pub primary_metric: MetricValue,
+ pub supporting_metrics: Vec<MetricValue>,
+ pub verdict: FrontierVerdict,
+ pub rationale: NonEmptyText,
+ pub analysis: Option<ExperimentAnalysis>,
+ pub closed_at: OffsetDateTime,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
-pub struct DagNode {
- pub id: NodeId,
- pub class: NodeClass,
- pub track: NodeTrack,
- pub frontier_id: Option<FrontierId>,
+pub struct ExperimentRecord {
+ pub id: ExperimentId,
+ pub slug: Slug,
+ pub frontier_id: FrontierId,
+ pub hypothesis_id: HypothesisId,
pub archived: bool,
pub title: NonEmptyText,
pub summary: Option<NonEmptyText>,
- pub tags: BTreeSet<TagName>,
- pub payload: NodePayload,
- pub annotations: Vec<NodeAnnotation>,
- pub diagnostics: NodeDiagnostics,
- pub agent_session_id: Option<AgentSessionId>,
+ pub tags: Vec<TagName>,
+ pub status: ExperimentStatus,
+ pub outcome: Option<ExperimentOutcome>,
+ pub revision: u64,
pub created_at: OffsetDateTime,
pub updated_at: OffsetDateTime,
}
-impl DagNode {
- #[must_use]
- pub fn new(
- class: NodeClass,
- frontier_id: Option<FrontierId>,
- title: NonEmptyText,
- summary: Option<NonEmptyText>,
- payload: NodePayload,
- diagnostics: NodeDiagnostics,
- ) -> Self {
- let now = OffsetDateTime::now_utc();
- Self {
- id: NodeId::fresh(),
- class,
- track: class.default_track(),
- frontier_id,
- archived: false,
- title,
- summary,
- tags: BTreeSet::new(),
- payload,
- annotations: Vec::new(),
- diagnostics,
- agent_session_id: None,
- created_at: now,
- updated_at: now,
- }
- }
-
- #[must_use]
- pub fn is_core_path(&self) -> bool {
- self.track == NodeTrack::CorePath
- }
-}
-
-#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
-pub enum EdgeKind {
- Lineage,
- Evidence,
- Comparison,
- Supersedes,
- Annotation,
-}
-
-#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
-pub struct DagEdge {
- pub source_id: NodeId,
- pub target_id: NodeId,
- pub kind: EdgeKind,
-}
-
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(rename_all = "snake_case")]
pub enum ArtifactKind {
- Note,
- Patch,
- BenchmarkBundle,
- MetricSeries,
+ Document,
+ Link,
+ Log,
Table,
Plot,
- Log,
+ Dump,
Binary,
- Checkpoint,
+ Other,
+}
+
+impl ArtifactKind {
+ #[must_use]
+ pub const fn as_str(self) -> &'static str {
+ match self {
+ Self::Document => "document",
+ Self::Link => "link",
+ Self::Log => "log",
+ Self::Table => "table",
+ Self::Plot => "plot",
+ Self::Dump => "dump",
+ Self::Binary => "binary",
+ Self::Other => "other",
+ }
+ }
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
-pub struct ArtifactRef {
+pub struct ArtifactRecord {
pub id: ArtifactId,
+ pub slug: Slug,
pub kind: ArtifactKind,
pub label: NonEmptyText,
- pub path: Utf8PathBuf,
+ pub summary: Option<NonEmptyText>,
+ pub locator: NonEmptyText,
pub media_type: Option<NonEmptyText>,
- pub produced_by_run: Option<RunId>,
+ pub revision: u64,
+ pub created_at: OffsetDateTime,
+ pub updated_at: OffsetDateTime,
}
-#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
-pub struct CommandRecipe {
- pub working_directory: Utf8PathBuf,
- pub argv: Vec<NonEmptyText>,
- pub env: BTreeMap<String, String>,
+#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
+#[serde(rename_all = "snake_case")]
+pub enum VertexKind {
+ Hypothesis,
+ Experiment,
}
-impl CommandRecipe {
- pub fn new(
- working_directory: Utf8PathBuf,
- argv: Vec<NonEmptyText>,
- env: BTreeMap<String, String>,
- ) -> Result<Self, CoreError> {
- if argv.is_empty() {
- return Err(CoreError::EmptyCommand);
+impl VertexKind {
+ #[must_use]
+ pub const fn as_str(self) -> &'static str {
+ match self {
+ Self::Hypothesis => "hypothesis",
+ Self::Experiment => "experiment",
}
- Ok(Self {
- working_directory,
- argv,
- env,
- })
}
}
-#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
-pub struct MetricSpec {
- pub metric_key: NonEmptyText,
- pub unit: MetricUnit,
- pub objective: OptimizationObjective,
-}
-
-#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
-pub struct EvaluationProtocol {
- pub benchmark_suites: BTreeSet<NonEmptyText>,
- pub primary_metric: MetricSpec,
- pub supporting_metrics: BTreeSet<MetricSpec>,
-}
-
-#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
-pub struct FrontierContract {
- pub objective: NonEmptyText,
- pub evaluation: EvaluationProtocol,
- pub promotion_criteria: Vec<NonEmptyText>,
-}
-
-#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
-pub struct MetricObservation {
- pub metric_key: NonEmptyText,
- pub unit: MetricUnit,
- pub objective: OptimizationObjective,
- pub value: f64,
-}
-
-#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
-pub struct FrontierRecord {
- pub id: FrontierId,
- pub label: NonEmptyText,
- pub root_contract_node_id: NodeId,
- pub status: FrontierStatus,
- pub created_at: OffsetDateTime,
- pub updated_at: OffsetDateTime,
+#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(tag = "kind", content = "id", rename_all = "snake_case")]
+pub enum VertexRef {
+ Hypothesis(HypothesisId),
+ Experiment(ExperimentId),
}
-impl FrontierRecord {
+impl VertexRef {
#[must_use]
- pub fn new(label: NonEmptyText, root_contract_node_id: NodeId) -> Self {
- Self::with_id(FrontierId::fresh(), label, root_contract_node_id)
+ pub const fn kind(self) -> VertexKind {
+ match self {
+ Self::Hypothesis(_) => VertexKind::Hypothesis,
+ Self::Experiment(_) => VertexKind::Experiment,
+ }
}
#[must_use]
- pub fn with_id(id: FrontierId, label: NonEmptyText, root_contract_node_id: NodeId) -> Self {
- let now = OffsetDateTime::now_utc();
- Self {
- id,
- label,
- root_contract_node_id,
- status: FrontierStatus::Exploring,
- created_at: now,
- updated_at: now,
+ pub fn opaque_id(self) -> String {
+ match self {
+ Self::Hypothesis(id) => id.to_string(),
+ Self::Experiment(id) => id.to_string(),
}
}
}
-#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
-pub struct RunRecord {
- pub node_id: NodeId,
- pub run_id: RunId,
- pub frontier_id: Option<FrontierId>,
- pub status: RunStatus,
- pub backend: ExecutionBackend,
- pub dimensions: BTreeMap<NonEmptyText, RunDimensionValue>,
- pub command: CommandRecipe,
- pub started_at: Option<OffsetDateTime>,
- pub finished_at: Option<OffsetDateTime>,
-}
-
-#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
-pub struct ExperimentResult {
- pub dimensions: BTreeMap<NonEmptyText, RunDimensionValue>,
- pub primary_metric: MetricValue,
- pub supporting_metrics: Vec<MetricValue>,
- pub benchmark_bundle: Option<ArtifactId>,
-}
-
-#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
-pub struct OpenExperiment {
- pub id: ExperimentId,
- pub frontier_id: FrontierId,
- pub hypothesis_node_id: NodeId,
- pub title: NonEmptyText,
- pub summary: Option<NonEmptyText>,
- pub created_at: OffsetDateTime,
+#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(rename_all = "snake_case")]
+pub enum AttachmentTargetKind {
+ Frontier,
+ Hypothesis,
+ Experiment,
}
-#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
-pub struct FrontierNote {
- pub summary: NonEmptyText,
- pub next_hypotheses: Vec<NonEmptyText>,
+impl AttachmentTargetKind {
+ #[must_use]
+ pub const fn as_str(self) -> &'static str {
+ match self {
+ Self::Frontier => "frontier",
+ Self::Hypothesis => "hypothesis",
+ Self::Experiment => "experiment",
+ }
+ }
}
-#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
-pub struct CompletedExperiment {
- pub id: ExperimentId,
- pub frontier_id: FrontierId,
- pub hypothesis_node_id: NodeId,
- pub run_node_id: NodeId,
- pub run_id: RunId,
- pub analysis_node_id: Option<NodeId>,
- pub decision_node_id: NodeId,
- pub title: NonEmptyText,
- pub summary: Option<NonEmptyText>,
- pub result: ExperimentResult,
- pub note: FrontierNote,
- pub verdict: FrontierVerdict,
- pub created_at: OffsetDateTime,
+#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(tag = "kind", content = "id", rename_all = "snake_case")]
+pub enum AttachmentTargetRef {
+ Frontier(FrontierId),
+ Hypothesis(HypothesisId),
+ Experiment(ExperimentId),
}
-#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
-pub struct FrontierVerdictCounts {
- pub accepted: u64,
- pub kept: u64,
- pub parked: u64,
- pub rejected: u64,
-}
+impl AttachmentTargetRef {
+ #[must_use]
+ pub const fn kind(self) -> AttachmentTargetKind {
+ match self {
+ Self::Frontier(_) => AttachmentTargetKind::Frontier,
+ Self::Hypothesis(_) => AttachmentTargetKind::Hypothesis,
+ Self::Experiment(_) => AttachmentTargetKind::Experiment,
+ }
+ }
-#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
-pub struct FrontierProjection {
- pub frontier: FrontierRecord,
- pub open_experiment_count: u64,
- pub completed_experiment_count: u64,
- pub verdict_counts: FrontierVerdictCounts,
-}
-
-#[cfg(test)]
-mod tests {
- use std::collections::{BTreeMap, BTreeSet};
-
- use camino::Utf8PathBuf;
- use serde_json::json;
-
- use super::{
- CommandRecipe, DagNode, DiagnosticSeverity, FieldPresence, FieldRole, FieldValueType,
- InferencePolicy, JsonObject, NodeClass, NodePayload, NonEmptyText, ProjectFieldSpec,
- ProjectSchema,
- };
- use crate::CoreError;
-
- #[test]
- fn non_empty_text_rejects_blank_input() {
- let text = NonEmptyText::new(" ");
- assert_eq!(text, Err(CoreError::EmptyText));
- }
-
- #[test]
- fn command_recipe_requires_argv() {
- let recipe = CommandRecipe::new(
- Utf8PathBuf::from("/tmp/worktree"),
- Vec::new(),
- BTreeMap::new(),
- );
- assert_eq!(recipe, Err(CoreError::EmptyCommand));
- }
-
- #[test]
- fn schema_validation_warns_without_rejecting_ingest() -> Result<(), CoreError> {
- let schema = ProjectSchema {
- namespace: NonEmptyText::new("local.libgrid")?,
- version: 1,
- fields: vec![ProjectFieldSpec {
- name: NonEmptyText::new("hypothesis")?,
- node_classes: BTreeSet::from([NodeClass::Hypothesis]),
- presence: FieldPresence::Required,
- severity: DiagnosticSeverity::Warning,
- role: FieldRole::ProjectionGate,
- inference_policy: InferencePolicy::ManualOnly,
- value_type: None,
- }],
- };
- let payload = NodePayload::with_schema(schema.schema_ref(), JsonObject::new());
- let diagnostics = schema.validate_node(NodeClass::Hypothesis, &payload);
-
- assert_eq!(diagnostics.admission, super::AdmissionState::Admitted);
- assert_eq!(diagnostics.items.len(), 1);
- assert_eq!(diagnostics.items[0].severity, DiagnosticSeverity::Warning);
- Ok(())
- }
-
- #[test]
- fn schema_validation_warns_on_type_mismatch() -> Result<(), CoreError> {
- let schema = ProjectSchema {
- namespace: NonEmptyText::new("local.libgrid")?,
- version: 1,
- fields: vec![ProjectFieldSpec {
- name: NonEmptyText::new("improvement")?,
- node_classes: BTreeSet::from([NodeClass::Analysis]),
- presence: FieldPresence::Recommended,
- severity: DiagnosticSeverity::Warning,
- role: FieldRole::RenderOnly,
- inference_policy: InferencePolicy::ManualOnly,
- value_type: Some(FieldValueType::Numeric),
- }],
- };
- let payload = NodePayload::with_schema(
- schema.schema_ref(),
- JsonObject::from_iter([("improvement".to_owned(), json!("not a number"))]),
- );
- let diagnostics = schema.validate_node(NodeClass::Analysis, &payload);
-
- assert_eq!(diagnostics.admission, super::AdmissionState::Admitted);
- assert_eq!(diagnostics.items.len(), 1);
- assert_eq!(diagnostics.items[0].code, "type.improvement");
- Ok(())
- }
-
- #[test]
- fn source_nodes_default_to_off_path() -> Result<(), CoreError> {
- let payload = NodePayload {
- schema: None,
- fields: JsonObject::from_iter([("topic".to_owned(), json!("ideas"))]),
- };
- let node = DagNode::new(
- NodeClass::Source,
- None,
- NonEmptyText::new("feature scouting")?,
- None,
- payload,
- super::NodeDiagnostics::admitted(),
- );
-
- assert!(!node.is_core_path());
- Ok(())
+ #[must_use]
+ pub fn opaque_id(self) -> String {
+ match self {
+ Self::Frontier(id) => id.to_string(),
+ Self::Hypothesis(id) => id.to_string(),
+ Self::Experiment(id) => id.to_string(),
+ }
}
}