Skip to main content

git_internal/internal/object/
evidence.rs

1//! AI Evidence Definition
2//!
3//! An `Evidence` captures the output of a single validation or quality
4//! assurance step — running tests, linting code, compiling the project,
5//! etc. It is the objective data that supports (or contradicts) the
6//! agent's proposed changes.
7//!
8//! # Position in Lifecycle
9//!
10//! ```text
11//! Run ──patchsets──▶ [PatchSet₀, PatchSet₁, ...]
12//!  │                       │
13//!  │                       ▼
14//!  └──────────────▶ Evidence (run_id + optional patchset_id)
15//!                       │
16//!                       ▼
17//!                   Decision (uses Evidence to justify verdict)
18//! ```
19//!
20//! Evidence is produced **during** a Run, typically after a PatchSet is
21//! generated. The orchestrator runs validation tools against the
22//! PatchSet and creates one Evidence per tool invocation. A single
23//! PatchSet may have multiple Evidence objects (e.g. test + lint +
24//! build). Evidence that is not tied to a specific PatchSet (e.g. a
25//! pre-run environment check) sets `patchset_id` to `None`.
26//!
27//! # Purpose
28//!
29//! - **Validation**: Proves that a PatchSet works as expected (tests
30//!   pass, code compiles, lint clean).
31//! - **Feedback**: Provides error messages, logs, and exit codes to the
32//!   agent so it can fix issues and produce a better PatchSet.
33//! - **Decision Support**: The [`Decision`](super::decision::Decision)
34//!   references Evidence to justify committing or rejecting changes.
35//!   Reviewers can inspect Evidence to understand why a verdict was made.
36
37use std::fmt;
38
39use serde::{Deserialize, Serialize};
40use uuid::Uuid;
41
42use crate::{
43    errors::GitError,
44    hash::ObjectHash,
45    internal::object::{
46        ObjectTrait,
47        types::{ActorRef, ArtifactRef, Header, ObjectType},
48    },
49};
50
51/// Kind of evidence.
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
53#[serde(rename_all = "snake_case")]
54pub enum EvidenceKind {
55    /// Unit, integration, or e2e tests.
56    Test,
57    /// Static analysis results.
58    Lint,
59    /// Compilation or build results.
60    Build,
61    #[serde(untagged)]
62    Other(String),
63}
64
65impl fmt::Display for EvidenceKind {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        match self {
68            EvidenceKind::Test => write!(f, "test"),
69            EvidenceKind::Lint => write!(f, "lint"),
70            EvidenceKind::Build => write!(f, "build"),
71            EvidenceKind::Other(s) => write!(f, "{}", s),
72        }
73    }
74}
75
76impl From<String> for EvidenceKind {
77    fn from(s: String) -> Self {
78        match s.as_str() {
79            "test" => EvidenceKind::Test,
80            "lint" => EvidenceKind::Lint,
81            "build" => EvidenceKind::Build,
82            _ => EvidenceKind::Other(s),
83        }
84    }
85}
86
87impl From<&str> for EvidenceKind {
88    fn from(s: &str) -> Self {
89        match s {
90            "test" => EvidenceKind::Test,
91            "lint" => EvidenceKind::Lint,
92            "build" => EvidenceKind::Build,
93            _ => EvidenceKind::Other(s.to_string()),
94        }
95    }
96}
97
98/// Output of a single validation step (test, lint, build, etc.).
99///
100/// One Evidence per tool invocation. Multiple Evidence objects may
101/// exist for the same PatchSet (one per validation tool). See module
102/// documentation for lifecycle position.
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct Evidence {
105    /// Common header (object ID, type, timestamps, creator, etc.).
106    #[serde(flatten)]
107    header: Header,
108    /// The [`Run`](super::run::Run) during which this validation was
109    /// performed. Every Evidence belongs to exactly one Run.
110    run_id: Uuid,
111    /// The [`PatchSet`](super::patchset::PatchSet) being validated.
112    ///
113    /// `None` for run-level checks that are not specific to any
114    /// PatchSet (e.g. environment health check before patching starts).
115    /// When set, the Evidence applies to that specific PatchSet.
116    #[serde(default, skip_serializing_if = "Option::is_none")]
117    patchset_id: Option<Uuid>,
118    /// Category of validation performed.
119    ///
120    /// `Test` for unit/integration/e2e tests, `Lint` for static
121    /// analysis, `Build` for compilation. `Other(String)` for
122    /// categories not covered by the predefined variants.
123    kind: EvidenceKind,
124    /// Name of the tool that produced this evidence (e.g. "cargo",
125    /// "eslint", "pytest"). Used for display and filtering.
126    tool: String,
127    /// Full command line that was executed (e.g. "cargo test --release").
128    ///
129    /// `None` if the tool was invoked programmatically without a
130    /// shell command. Useful for reproducibility — a reviewer can
131    /// re-run the exact same command locally.
132    #[serde(default, skip_serializing_if = "Option::is_none")]
133    command: Option<String>,
134    /// Process exit code returned by the tool.
135    ///
136    /// `0` typically means success; non-zero means failure. `None` if
137    /// the tool did not produce an exit code (e.g. an in-process check).
138    /// The orchestrator uses this as a quick pass/fail signal before
139    /// parsing the full report.
140    #[serde(default, skip_serializing_if = "Option::is_none")]
141    exit_code: Option<i32>,
142    /// Short human-readable summary of the result.
143    ///
144    /// Typically a one-liner like "42 tests passed", "3 lint errors",
145    /// or an error signature extracted from the output. `None` if no
146    /// summary was produced. For full output, see `report_artifacts`.
147    #[serde(default, skip_serializing_if = "Option::is_none")]
148    summary: Option<String>,
149    /// References to full report files in object storage.
150    ///
151    /// May include log files, HTML coverage reports, JUnit XML, etc.
152    /// Each [`ArtifactRef`] points to one stored file. The list is
153    /// empty when the tool produced no persistent output, or when the
154    /// output is captured entirely in `summary`.
155    #[serde(default, skip_serializing_if = "Vec::is_empty")]
156    report_artifacts: Vec<ArtifactRef>,
157}
158
159impl Evidence {
160    pub fn new(
161        created_by: ActorRef,
162        run_id: Uuid,
163        kind: impl Into<EvidenceKind>,
164        tool: impl Into<String>,
165    ) -> Result<Self, String> {
166        Ok(Self {
167            header: Header::new(ObjectType::Evidence, created_by)?,
168            run_id,
169            patchset_id: None,
170            kind: kind.into(),
171            tool: tool.into(),
172            command: None,
173            exit_code: None,
174            summary: None,
175            report_artifacts: Vec::new(),
176        })
177    }
178
179    pub fn header(&self) -> &Header {
180        &self.header
181    }
182
183    pub fn run_id(&self) -> Uuid {
184        self.run_id
185    }
186
187    pub fn patchset_id(&self) -> Option<Uuid> {
188        self.patchset_id
189    }
190
191    pub fn kind(&self) -> &EvidenceKind {
192        &self.kind
193    }
194
195    pub fn tool(&self) -> &str {
196        &self.tool
197    }
198
199    pub fn command(&self) -> Option<&str> {
200        self.command.as_deref()
201    }
202
203    pub fn exit_code(&self) -> Option<i32> {
204        self.exit_code
205    }
206
207    pub fn summary(&self) -> Option<&str> {
208        self.summary.as_deref()
209    }
210
211    pub fn report_artifacts(&self) -> &[ArtifactRef] {
212        &self.report_artifacts
213    }
214
215    pub fn set_patchset_id(&mut self, patchset_id: Option<Uuid>) {
216        self.patchset_id = patchset_id;
217    }
218
219    pub fn set_command(&mut self, command: Option<String>) {
220        self.command = command;
221    }
222
223    pub fn set_exit_code(&mut self, exit_code: Option<i32>) {
224        self.exit_code = exit_code;
225    }
226
227    pub fn set_summary(&mut self, summary: Option<String>) {
228        self.summary = summary;
229    }
230
231    pub fn add_report_artifact(&mut self, artifact: ArtifactRef) {
232        self.report_artifacts.push(artifact);
233    }
234}
235
236impl fmt::Display for Evidence {
237    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
238        write!(f, "Evidence: {}", self.header.object_id())
239    }
240}
241
242impl ObjectTrait for Evidence {
243    fn from_bytes(data: &[u8], _hash: ObjectHash) -> Result<Self, GitError>
244    where
245        Self: Sized,
246    {
247        serde_json::from_slice(data).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
248    }
249
250    fn get_type(&self) -> ObjectType {
251        ObjectType::Evidence
252    }
253
254    fn get_size(&self) -> usize {
255        match serde_json::to_vec(self) {
256            Ok(v) => v.len(),
257            Err(e) => {
258                tracing::warn!("failed to compute Evidence size: {}", e);
259                0
260            }
261        }
262    }
263
264    fn to_data(&self) -> Result<Vec<u8>, GitError> {
265        serde_json::to_vec(self).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    #[test]
274    fn test_evidence_fields() {
275        let actor = ActorRef::agent("test-agent").expect("actor");
276        let run_id = Uuid::from_u128(0x1);
277        let patchset_id = Uuid::from_u128(0x2);
278
279        let mut evidence = Evidence::new(actor, run_id, "test", "cargo").expect("evidence");
280        evidence.set_patchset_id(Some(patchset_id));
281        evidence.set_exit_code(Some(1));
282        evidence.add_report_artifact(ArtifactRef::new("local", "log.txt").expect("artifact"));
283
284        assert_eq!(evidence.patchset_id(), Some(patchset_id));
285        assert_eq!(evidence.exit_code(), Some(1));
286        assert_eq!(evidence.report_artifacts().len(), 1);
287        assert_eq!(evidence.kind(), &EvidenceKind::Test);
288    }
289}