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//! ⑥ ToolInvocation / ⑦ PatchSet
12//!      │              │
13//!      │              ▼
14//!      └──────────▶ Evidence (run_id + optional patchset_id)
15//!                         │
16//!                         ▼
17//!                     ⑨ Decision (verdict justification)
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//!
37//! # How Libra should use this object
38//!
39//! - Create one `Evidence` object per validation tool execution or
40//!   report.
41//! - Attach `patchset_id` when the validation targets a specific
42//!   candidate diff.
43//! - Use `summary`, `exit_code`, and `report_artifacts` for the durable
44//!   audit record.
45//! - Derive pass/fail dashboards and gating status in Libra; do not
46//!   rewrite `PatchSet` or `Run` snapshots with validation summaries.
47
48use std::fmt;
49
50use serde::{Deserialize, Serialize};
51use uuid::Uuid;
52
53use crate::{
54    errors::GitError,
55    hash::ObjectHash,
56    internal::object::{
57        ObjectTrait,
58        types::{ActorRef, ArtifactRef, Header, ObjectType},
59    },
60};
61
62/// Kind of evidence.
63#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
64#[serde(rename_all = "snake_case")]
65pub enum EvidenceKind {
66    /// Unit, integration, or e2e tests.
67    Test,
68    /// Static analysis results.
69    Lint,
70    /// Compilation or build results.
71    Build,
72    #[serde(untagged)]
73    Other(String),
74}
75
76impl fmt::Display for EvidenceKind {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        match self {
79            EvidenceKind::Test => write!(f, "test"),
80            EvidenceKind::Lint => write!(f, "lint"),
81            EvidenceKind::Build => write!(f, "build"),
82            EvidenceKind::Other(s) => write!(f, "{}", s),
83        }
84    }
85}
86
87impl From<String> for EvidenceKind {
88    fn from(s: String) -> Self {
89        match s.as_str() {
90            "test" => EvidenceKind::Test,
91            "lint" => EvidenceKind::Lint,
92            "build" => EvidenceKind::Build,
93            _ => EvidenceKind::Other(s),
94        }
95    }
96}
97
98impl From<&str> for EvidenceKind {
99    fn from(s: &str) -> Self {
100        match s {
101            "test" => EvidenceKind::Test,
102            "lint" => EvidenceKind::Lint,
103            "build" => EvidenceKind::Build,
104            _ => EvidenceKind::Other(s.to_string()),
105        }
106    }
107}
108
109/// Output of a single validation step (test, lint, build, etc.).
110///
111/// One Evidence per tool invocation. Multiple Evidence objects may
112/// exist for the same PatchSet (one per validation tool). See module
113/// documentation for lifecycle position and Libra calling guidance.
114#[derive(Debug, Clone, Serialize, Deserialize)]
115#[serde(deny_unknown_fields)]
116pub struct Evidence {
117    /// Common header (object ID, type, timestamps, creator, etc.).
118    #[serde(flatten)]
119    header: Header,
120    /// The [`Run`](super::run::Run) during which this validation was
121    /// performed. Every Evidence belongs to exactly one Run.
122    run_id: Uuid,
123    /// The [`PatchSet`](super::patchset::PatchSet) being validated.
124    ///
125    /// `None` for run-level checks that are not specific to any
126    /// PatchSet (e.g. environment health check before patching starts).
127    /// When set, the Evidence applies to that specific PatchSet.
128    #[serde(default, skip_serializing_if = "Option::is_none")]
129    patchset_id: Option<Uuid>,
130    /// Category of validation performed.
131    ///
132    /// `Test` for unit/integration/e2e tests, `Lint` for static
133    /// analysis, `Build` for compilation. `Other(String)` for
134    /// categories not covered by the predefined variants.
135    kind: EvidenceKind,
136    /// Name of the tool that produced this evidence (e.g. "cargo",
137    /// "eslint", "pytest"). Used for display and filtering.
138    tool: String,
139    /// Full command line that was executed (e.g. "cargo test --release").
140    ///
141    /// `None` if the tool was invoked programmatically without a
142    /// shell command. Useful for reproducibility — a reviewer can
143    /// re-run the exact same command locally.
144    #[serde(default, skip_serializing_if = "Option::is_none")]
145    command: Option<String>,
146    /// Process exit code returned by the tool.
147    ///
148    /// `0` typically means success; non-zero means failure. `None` if
149    /// the tool did not produce an exit code (e.g. an in-process check).
150    /// The orchestrator uses this as a quick pass/fail signal before
151    /// parsing the full report.
152    #[serde(default, skip_serializing_if = "Option::is_none")]
153    exit_code: Option<i32>,
154    /// Short human-readable summary of the result.
155    ///
156    /// Typically a one-liner like "42 tests passed", "3 lint errors",
157    /// or an error signature extracted from the output. `None` if no
158    /// summary was produced. For full output, see `report_artifacts`.
159    #[serde(default, skip_serializing_if = "Option::is_none")]
160    summary: Option<String>,
161    /// References to full report files in object storage.
162    ///
163    /// May include log files, HTML coverage reports, JUnit XML, etc.
164    /// Each [`ArtifactRef`] points to one stored file. The list is
165    /// empty when the tool produced no persistent output, or when the
166    /// output is captured entirely in `summary`.
167    #[serde(default, skip_serializing_if = "Vec::is_empty")]
168    report_artifacts: Vec<ArtifactRef>,
169}
170
171impl Evidence {
172    /// Create a new validation evidence record for the given run and
173    /// validation category.
174    pub fn new(
175        created_by: ActorRef,
176        run_id: Uuid,
177        kind: impl Into<EvidenceKind>,
178        tool: impl Into<String>,
179    ) -> Result<Self, String> {
180        Ok(Self {
181            header: Header::new(ObjectType::Evidence, created_by)?,
182            run_id,
183            patchset_id: None,
184            kind: kind.into(),
185            tool: tool.into(),
186            command: None,
187            exit_code: None,
188            summary: None,
189            report_artifacts: Vec::new(),
190        })
191    }
192
193    /// Return the immutable header for this evidence object.
194    pub fn header(&self) -> &Header {
195        &self.header
196    }
197
198    /// Return the owning run id.
199    pub fn run_id(&self) -> Uuid {
200        self.run_id
201    }
202
203    /// Return the validated patchset id, if present.
204    pub fn patchset_id(&self) -> Option<Uuid> {
205        self.patchset_id
206    }
207
208    /// Return the validation category.
209    pub fn kind(&self) -> &EvidenceKind {
210        &self.kind
211    }
212
213    /// Return the tool name that produced this evidence.
214    pub fn tool(&self) -> &str {
215        &self.tool
216    }
217
218    /// Return the executed command line, if present.
219    pub fn command(&self) -> Option<&str> {
220        self.command.as_deref()
221    }
222
223    /// Return the process exit code, if present.
224    pub fn exit_code(&self) -> Option<i32> {
225        self.exit_code
226    }
227
228    /// Return the short human-readable summary, if present.
229    pub fn summary(&self) -> Option<&str> {
230        self.summary.as_deref()
231    }
232
233    /// Return the persistent report artifacts.
234    pub fn report_artifacts(&self) -> &[ArtifactRef] {
235        &self.report_artifacts
236    }
237
238    /// Set or clear the validated patchset id.
239    pub fn set_patchset_id(&mut self, patchset_id: Option<Uuid>) {
240        self.patchset_id = patchset_id;
241    }
242
243    /// Set or clear the executed command line.
244    pub fn set_command(&mut self, command: Option<String>) {
245        self.command = command;
246    }
247
248    /// Set or clear the process exit code.
249    pub fn set_exit_code(&mut self, exit_code: Option<i32>) {
250        self.exit_code = exit_code;
251    }
252
253    /// Set or clear the short human-readable summary.
254    pub fn set_summary(&mut self, summary: Option<String>) {
255        self.summary = summary;
256    }
257
258    /// Append one persistent validation report artifact.
259    pub fn add_report_artifact(&mut self, artifact: ArtifactRef) {
260        self.report_artifacts.push(artifact);
261    }
262}
263
264impl fmt::Display for Evidence {
265    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266        write!(f, "Evidence: {}", self.header.object_id())
267    }
268}
269
270impl ObjectTrait for Evidence {
271    fn from_bytes(data: &[u8], _hash: ObjectHash) -> Result<Self, GitError>
272    where
273        Self: Sized,
274    {
275        serde_json::from_slice(data).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
276    }
277
278    fn get_type(&self) -> ObjectType {
279        ObjectType::Evidence
280    }
281
282    fn get_size(&self) -> usize {
283        match serde_json::to_vec(self) {
284            Ok(v) => v.len(),
285            Err(e) => {
286                tracing::warn!("failed to compute Evidence size: {}", e);
287                0
288            }
289        }
290    }
291
292    fn to_data(&self) -> Result<Vec<u8>, GitError> {
293        serde_json::to_vec(self).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    // Coverage:
300    // - evidence field access
301    // - optional patchset association
302    // - command, exit-code, summary, and report artifact storage
303
304    use super::*;
305
306    #[test]
307    fn test_evidence_fields() {
308        let actor = ActorRef::agent("test-agent").expect("actor");
309        let run_id = Uuid::from_u128(0x1);
310        let patchset_id = Uuid::from_u128(0x2);
311
312        let mut evidence = Evidence::new(actor, run_id, "test", "cargo").expect("evidence");
313        evidence.set_patchset_id(Some(patchset_id));
314        evidence.set_exit_code(Some(1));
315        evidence.add_report_artifact(ArtifactRef::new("local", "log.txt").expect("artifact"));
316
317        assert_eq!(evidence.patchset_id(), Some(patchset_id));
318        assert_eq!(evidence.exit_code(), Some(1));
319        assert_eq!(evidence.report_artifacts().len(), 1);
320        assert_eq!(evidence.kind(), &EvidenceKind::Test);
321    }
322}