Skip to main content

ainl_contracts/
feature.rs

1//! Feature nodes within a mission DAG.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::path::PathBuf;
6
7use crate::assertion::AssertionId;
8
9/// Stable feature identifier within a mission.
10#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
11#[serde(transparent)]
12pub struct FeatureId(pub String);
13
14impl FeatureId {
15    pub fn as_str(&self) -> &str {
16        &self.0
17    }
18}
19
20impl From<String> for FeatureId {
21    fn from(s: String) -> Self {
22        Self(s)
23    }
24}
25
26impl From<&str> for FeatureId {
27    fn from(s: &str) -> Self {
28        Self(s.to_string())
29    }
30}
31
32/// Execution status for a feature/work item.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
34#[serde(rename_all = "snake_case")]
35pub enum FeatureStatus {
36    #[default]
37    Pending,
38    InProgress,
39    Completed,
40    Cancelled,
41    RolledBack,
42}
43
44/// Git snapshot metadata for rollback (lazy per-feature).
45#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
46pub struct FeatureSnapshot {
47    pub repo_toplevel: PathBuf,
48    pub stash_sha: String,
49    pub head_sha: String,
50    pub taken_at: DateTime<Utc>,
51}
52
53/// How to verify a feature or assertion.
54#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
55#[serde(tag = "kind", rename_all = "snake_case")]
56pub enum VerificationStep {
57    ShellCommand {
58        cmd: String,
59        #[serde(default)]
60        expected_exit_code: i32,
61    },
62    BrowserFlow {
63        url: String,
64        success_criteria: String,
65    },
66    AgentSupervise {
67        agent_id: String,
68        task: String,
69        success_criteria: String,
70    },
71}
72
73/// A unit of work in the mission DAG.
74#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
75pub struct Feature {
76    pub feature_id: FeatureId,
77    pub description: String,
78    pub status: FeatureStatus,
79    #[serde(default, skip_serializing_if = "Option::is_none")]
80    pub milestone: Option<String>,
81    #[serde(default, skip_serializing_if = "Option::is_none")]
82    pub skill_name: Option<String>,
83    #[serde(default)]
84    pub touches_files: Vec<String>,
85    #[serde(default)]
86    pub preconditions: Vec<FeatureId>,
87    #[serde(default)]
88    pub expected_behavior: Vec<String>,
89    #[serde(default)]
90    pub verification_steps: Vec<VerificationStep>,
91    #[serde(default)]
92    pub fulfills: Vec<AssertionId>,
93    #[serde(default, skip_serializing_if = "Option::is_none")]
94    pub snapshot: Option<FeatureSnapshot>,
95}