Skip to main content

git_internal/internal/object/
evidence.rs

1//! AI Evidence Definition
2//!
3//! `Evidence` represents the result of a validation or quality assurance step, such as
4//! running tests, linting code, or building artifacts.
5//!
6//! # Purpose
7//!
8//! - **Validation**: Proves that a patchset works as expected.
9//! - **Feedback**: Provides error messages and logs to the agent so it can fix issues.
10//! - **Decision Support**: Used by the `Decision` object to justify committing or rejecting changes.
11
12use std::fmt;
13
14use serde::{Deserialize, Serialize};
15use uuid::Uuid;
16
17use crate::{
18    errors::GitError,
19    hash::ObjectHash,
20    internal::object::{
21        ObjectTrait,
22        types::{ActorRef, ArtifactRef, Header, ObjectType},
23    },
24};
25
26/// Kind of evidence.
27#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
28#[serde(rename_all = "snake_case")]
29pub enum EvidenceKind {
30    /// Unit, integration, or e2e tests.
31    Test,
32    /// Static analysis results.
33    Lint,
34    /// Compilation or build results.
35    Build,
36    #[serde(untagged)]
37    Other(String),
38}
39
40impl fmt::Display for EvidenceKind {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        match self {
43            EvidenceKind::Test => write!(f, "test"),
44            EvidenceKind::Lint => write!(f, "lint"),
45            EvidenceKind::Build => write!(f, "build"),
46            EvidenceKind::Other(s) => write!(f, "{}", s),
47        }
48    }
49}
50
51impl From<String> for EvidenceKind {
52    fn from(s: String) -> Self {
53        match s.as_str() {
54            "test" => EvidenceKind::Test,
55            "lint" => EvidenceKind::Lint,
56            "build" => EvidenceKind::Build,
57            _ => EvidenceKind::Other(s),
58        }
59    }
60}
61
62impl From<&str> for EvidenceKind {
63    fn from(s: &str) -> Self {
64        match s {
65            "test" => EvidenceKind::Test,
66            "lint" => EvidenceKind::Lint,
67            "build" => EvidenceKind::Build,
68            _ => EvidenceKind::Other(s.to_string()),
69        }
70    }
71}
72
73/// Evidence object for test/lint/build results.
74/// Links tooling output back to a run or patchset.
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct Evidence {
77    #[serde(flatten)]
78    header: Header,
79    run_id: Uuid,
80    patchset_id: Option<Uuid>,
81    kind: EvidenceKind,
82    tool: String,
83    command: Option<String>,
84    exit_code: Option<i32>,
85    summary: Option<String>, // passed/failed, error signature
86    #[serde(default)]
87    report_artifacts: Vec<ArtifactRef>,
88}
89
90impl Evidence {
91    pub fn new(
92        repo_id: Uuid,
93        created_by: ActorRef,
94        run_id: Uuid,
95        kind: impl Into<EvidenceKind>,
96        tool: impl Into<String>,
97    ) -> Result<Self, String> {
98        Ok(Self {
99            header: Header::new(ObjectType::Evidence, repo_id, created_by)?,
100            run_id,
101            patchset_id: None,
102            kind: kind.into(),
103            tool: tool.into(),
104            command: None,
105            exit_code: None,
106            summary: None,
107            report_artifacts: Vec::new(),
108        })
109    }
110
111    pub fn header(&self) -> &Header {
112        &self.header
113    }
114
115    pub fn run_id(&self) -> Uuid {
116        self.run_id
117    }
118
119    pub fn patchset_id(&self) -> Option<Uuid> {
120        self.patchset_id
121    }
122
123    pub fn kind(&self) -> &EvidenceKind {
124        &self.kind
125    }
126
127    pub fn tool(&self) -> &str {
128        &self.tool
129    }
130
131    pub fn command(&self) -> Option<&str> {
132        self.command.as_deref()
133    }
134
135    pub fn exit_code(&self) -> Option<i32> {
136        self.exit_code
137    }
138
139    pub fn summary(&self) -> Option<&str> {
140        self.summary.as_deref()
141    }
142
143    pub fn report_artifacts(&self) -> &[ArtifactRef] {
144        &self.report_artifacts
145    }
146
147    pub fn set_patchset_id(&mut self, patchset_id: Option<Uuid>) {
148        self.patchset_id = patchset_id;
149    }
150
151    pub fn set_command(&mut self, command: Option<String>) {
152        self.command = command;
153    }
154
155    pub fn set_exit_code(&mut self, exit_code: Option<i32>) {
156        self.exit_code = exit_code;
157    }
158
159    pub fn set_summary(&mut self, summary: Option<String>) {
160        self.summary = summary;
161    }
162
163    pub fn add_report_artifact(&mut self, artifact: ArtifactRef) {
164        self.report_artifacts.push(artifact);
165    }
166}
167
168impl fmt::Display for Evidence {
169    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170        write!(f, "Evidence: {}", self.header.object_id())
171    }
172}
173
174impl ObjectTrait for Evidence {
175    fn from_bytes(data: &[u8], _hash: ObjectHash) -> Result<Self, GitError>
176    where
177        Self: Sized,
178    {
179        serde_json::from_slice(data).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
180    }
181
182    fn get_type(&self) -> ObjectType {
183        ObjectType::Evidence
184    }
185
186    fn get_size(&self) -> usize {
187        serde_json::to_vec(self).map(|v| v.len()).unwrap_or(0)
188    }
189
190    fn to_data(&self) -> Result<Vec<u8>, GitError> {
191        serde_json::to_vec(self).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn test_evidence_fields() {
201        let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef);
202        let actor = ActorRef::agent("test-agent").expect("actor");
203        let run_id = Uuid::from_u128(0x1);
204        let patchset_id = Uuid::from_u128(0x2);
205
206        let mut evidence =
207            Evidence::new(repo_id, actor, run_id, "test", "cargo").expect("evidence");
208        evidence.set_patchset_id(Some(patchset_id));
209        evidence.set_exit_code(Some(1));
210        evidence.add_report_artifact(ArtifactRef::new("local", "log.txt").expect("artifact"));
211
212        assert_eq!(evidence.patchset_id(), Some(patchset_id));
213        assert_eq!(evidence.exit_code(), Some(1));
214        assert_eq!(evidence.report_artifacts().len(), 1);
215        assert_eq!(evidence.kind(), &EvidenceKind::Test);
216    }
217}