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}