ainl-contracts 0.1.4

Shared policy contracts for repo intelligence, context freshness, and impact-first tooling (no OpenFang deps)
Documentation
//! Feature nodes within a mission DAG.

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

use crate::assertion::AssertionId;

/// Stable feature identifier within a mission.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct FeatureId(pub String);

impl FeatureId {
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl From<String> for FeatureId {
    fn from(s: String) -> Self {
        Self(s)
    }
}

impl From<&str> for FeatureId {
    fn from(s: &str) -> Self {
        Self(s.to_string())
    }
}

/// Execution status for a feature/work item.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum FeatureStatus {
    #[default]
    Pending,
    InProgress,
    Completed,
    Cancelled,
    RolledBack,
}

/// Git snapshot metadata for rollback (lazy per-feature).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FeatureSnapshot {
    pub repo_toplevel: PathBuf,
    pub stash_sha: String,
    pub head_sha: String,
    pub taken_at: DateTime<Utc>,
}

/// How to verify a feature or assertion.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum VerificationStep {
    ShellCommand {
        cmd: String,
        #[serde(default)]
        expected_exit_code: i32,
    },
    BrowserFlow {
        url: String,
        success_criteria: String,
    },
    AgentSupervise {
        agent_id: String,
        task: String,
        success_criteria: String,
    },
}

/// A unit of work in the mission DAG.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Feature {
    pub feature_id: FeatureId,
    pub description: String,
    pub status: FeatureStatus,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub milestone: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub skill_name: Option<String>,
    #[serde(default)]
    pub touches_files: Vec<String>,
    #[serde(default)]
    pub preconditions: Vec<FeatureId>,
    #[serde(default)]
    pub expected_behavior: Vec<String>,
    #[serde(default)]
    pub verification_steps: Vec<VerificationStep>,
    #[serde(default)]
    pub fulfills: Vec<AssertionId>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub snapshot: Option<FeatureSnapshot>,
}