1use std::collections::BTreeMap;
9use std::fs;
10use std::io::Read;
11use std::path::Path;
12use std::process::Command;
13
14use serde::{Deserialize, Serialize};
15use vyre_foundation::hashing::update_length_delimited_field as update_hash_field;
16use vyre_foundation::ir::Program;
17use vyre_foundation::serial::wire::encode::PROGRAM_WIRE_DIGEST_VERSION;
18
19use crate::backend::{BackendError, DispatchConfig, TimedDispatchResult, VyreBackend};
20use crate::pipeline::{
21 dispatch_policy_cache_digest, dispatch_policy_cache_string, hex_encode,
22 try_normalized_program_cache_digest, PipelineReproManifest,
23};
24
25pub const NORMALIZED_PROGRAM_DIGEST_VERSION: &str =
33 vyre_foundation::ir::NORMALIZED_PROGRAM_CACHE_DIGEST_VERSION;
34
35pub const SOURCE_FINGERPRINT_VERSION: &str = "vyre-source-fingerprint-v1";
37const MAX_SOURCE_FINGERPRINT_FILE_BYTES: u64 = 64 * 1024 * 1024;
38
39pub const SOURCE_TREE_FINGERPRINT_VERSION: &str = "source-tree-v1";
41
42pub const WORKLOAD_FINGERPRINT_VERSION: &str = "vyre-dispatch-workload-v1";
44
45pub const ENVIRONMENT_FINGERPRINT_VERSION: &str = "vyre-evidence-environment-v1";
47
48#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
50pub struct SourceProvenance {
51 pub git: BTreeMap<String, String>,
53 pub source_fingerprint: String,
55 pub source_tree_fingerprint: String,
57}
58
59impl SourceProvenance {
60 #[must_use]
62 pub fn capture_current() -> Self {
63 Self::capture_at(Path::new("."))
64 }
65
66 #[must_use]
68 pub fn capture_at(workspace_root: &Path) -> Self {
69 let git = capture_git_info_at(workspace_root);
70 let source_fingerprint = source_fingerprint(&git);
71 let source_tree_fingerprint = source_tree_fingerprint_at(workspace_root);
72 Self {
73 git,
74 source_fingerprint,
75 source_tree_fingerprint,
76 }
77 }
78
79 pub fn validate(&self) -> Result<(), BackendError> {
85 if self.source_fingerprint.trim().is_empty() {
86 return Err(BackendError::InvalidProgram {
87 fix: "Fix: source_fingerprint must be non-empty before emitting driver evidence."
88 .to_string(),
89 });
90 }
91 if self.source_tree_fingerprint.trim().is_empty() {
92 return Err(BackendError::InvalidProgram {
93 fix: "Fix: source_tree_fingerprint must be non-empty before emitting driver evidence."
94 .to_string(),
95 });
96 }
97 Ok(())
98 }
99}
100
101#[must_use]
103pub fn capture_git_info() -> BTreeMap<String, String> {
104 capture_git_info_at(Path::new("."))
105}
106
107#[must_use]
109pub fn capture_git_info_at(workspace_root: &Path) -> BTreeMap<String, String> {
110 let mut info = BTreeMap::new();
111
112 if let Ok(commit) = shell(workspace_root, &["rev-parse", "HEAD"]) {
113 info.insert("commit".to_string(), commit);
114 }
115 if let Ok(branch) = shell(workspace_root, &["rev-parse", "--abbrev-ref", "HEAD"]) {
116 info.insert("branch".to_string(), branch);
117 }
118 let dirty_status = shell_bytes(
119 workspace_root,
120 &[
121 "status",
122 "--porcelain=v1",
123 "-z",
124 "--untracked-files=all",
125 "--",
126 ".",
127 ":!release/evidence/**",
128 ],
129 );
130 let dirty = match dirty_status.as_ref() {
131 Ok(status) if status.is_empty() => "false",
132 Ok(status) => {
133 if let Some(fingerprint) = dirty_worktree_fingerprint(workspace_root, status) {
134 info.insert("dirty_worktree_fingerprint".to_string(), fingerprint);
135 }
136 "true"
137 }
138 Err(_) => "unknown",
139 };
140 info.insert("dirty".to_string(), dirty.to_string());
141
142 if let Ok(parent) = shell(workspace_root, &["rev-parse", "HEAD^"]) {
143 info.insert("parent_commit".to_string(), parent);
144 }
145 if let Ok(timestamp) = shell(workspace_root, &["log", "-1", "--format=%ct"]) {
146 info.insert("commit_timestamp".to_string(), timestamp);
147 }
148
149 info
150}
151
152#[must_use]
154pub fn source_fingerprint(git: &BTreeMap<String, String>) -> String {
155 if let Some(commit) = git.get("commit").filter(|commit| !commit.is_empty()) {
156 let dirty = git.get("dirty").map(String::as_str).unwrap_or("unknown");
157 if dirty == "true" {
158 let worktree = git
159 .get("dirty_worktree_fingerprint")
160 .filter(|fingerprint| !fingerprint.is_empty())
161 .map(String::as_str)
162 .unwrap_or("unknown");
163 return format!("git:{commit}:dirty=true:worktree={worktree}");
164 }
165 return format!("git:{commit}:dirty={dirty}");
166 }
167 format!(
168 "crate:{}:{}",
169 env!("CARGO_PKG_NAME"),
170 env!("CARGO_PKG_VERSION")
171 )
172}
173
174#[must_use]
176pub fn source_tree_fingerprint() -> String {
177 source_tree_fingerprint_at(Path::new("."))
178}
179
180#[must_use]
185pub fn source_tree_fingerprint_at(workspace_root: &Path) -> String {
186 match shell_bytes(
187 workspace_root,
188 &[
189 "ls-files",
190 "-z",
191 "--cached",
192 "--others",
193 "--exclude-standard",
194 ],
195 ) {
196 Ok(paths) => format!(
197 "source-tree-v1:{}",
198 source_tree_fingerprint_from_paths(workspace_root, &paths)
199 ),
200 Err(_) => format!(
201 "crate-source:{}:{}",
202 env!("CARGO_PKG_NAME"),
203 env!("CARGO_PKG_VERSION")
204 ),
205 }
206}
207
208fn source_tree_fingerprint_from_paths(workspace_root: &Path, paths: &[u8]) -> String {
209 let mut hasher = blake3::Hasher::new();
210 update_hash_field(&mut hasher, b"format", b"vyre-bench-source-tree-v1");
211 for path in paths
212 .split(|byte| *byte == 0)
213 .filter(|path| !path.is_empty())
214 .filter(|path| !source_tree_path_is_benchmark_provenance_ignored(path))
215 {
216 update_hash_field(&mut hasher, b"path", path);
217 let path = String::from_utf8_lossy(path);
218 match read_source_fingerprint_file_bounded(&workspace_root.join(path.as_ref())) {
219 Ok(Some(bytes)) => update_hash_field(&mut hasher, b"content", &bytes),
220 Ok(None) => update_hash_field(
221 &mut hasher,
222 b"content-oversized",
223 MAX_SOURCE_FINGERPRINT_FILE_BYTES.to_string().as_bytes(),
224 ),
225 Err(error) => {
226 update_hash_field(&mut hasher, b"read-error", error.to_string().as_bytes())
227 }
228 }
229 }
230 hasher.finalize().to_hex().to_string()
231}
232
233fn source_tree_path_is_benchmark_provenance_ignored(path: &[u8]) -> bool {
234 path == b"cargo_full"
235 || path.starts_with(b".github/")
236 || path.starts_with(b"release/evidence/")
237 || path.starts_with(b"scripts/")
238 || path.starts_with(b"xtask/")
239 || source_tree_path_is_operator_internal(path)
240 || source_tree_path_is_test_evidence(path)
241}
242
243fn source_tree_path_is_operator_internal(path: &[u8]) -> bool {
244 const FILE_NAMES: &[&[u8]] = &[
245 b"AGENTS.md",
246 b"BACKLOG.md",
247 b"CLAUDE.md",
248 b"GEMINI.md",
249 b"SKILL.md",
250 ];
251
252 FILE_NAMES.iter().any(|file_name| {
253 path == *file_name
254 || path
255 .strip_suffix(*file_name)
256 .is_some_and(|prefix| prefix.ends_with(b"/"))
257 })
258}
259
260fn source_tree_path_is_test_evidence(path: &[u8]) -> bool {
261 path.starts_with(b"tests/")
262 || path_contains(path, b"/tests/")
263 || path.ends_with(b"/tests.rs")
264 || path.ends_with(b"_tests.rs")
265 || path.ends_with(b"_test.rs")
266 || path_contains(path, b"_tests_")
267 || path_contains(path, b"_test_")
268}
269
270fn path_contains(path: &[u8], needle: &[u8]) -> bool {
271 !needle.is_empty() && path.windows(needle.len()).any(|window| window == needle)
272}
273
274fn dirty_worktree_fingerprint(workspace_root: &Path, status: &[u8]) -> Option<String> {
275 let diff = shell_bytes(
276 workspace_root,
277 &[
278 "diff",
279 "--binary",
280 "HEAD",
281 "--",
282 ".",
283 ":!release/evidence/**",
284 ],
285 )
286 .ok()?;
287 let untracked = shell_bytes(
288 workspace_root,
289 &[
290 "ls-files",
291 "--others",
292 "--exclude-standard",
293 "-z",
294 "--",
295 ".",
296 ":!release/evidence/**",
297 ],
298 )
299 .unwrap_or_default();
300 Some(dirty_worktree_fingerprint_from_parts(
301 workspace_root,
302 status,
303 &diff,
304 &untracked,
305 ))
306}
307
308fn dirty_worktree_fingerprint_from_parts(
309 workspace_root: &Path,
310 status: &[u8],
311 diff: &[u8],
312 untracked: &[u8],
313) -> String {
314 let mut hasher = blake3::Hasher::new();
315 update_hash_field(&mut hasher, b"format", b"vyre-bench-dirty-source-v1");
316 update_hash_field(&mut hasher, b"status", status);
317 update_hash_field(&mut hasher, b"diff", diff);
318 for path in untracked
319 .split(|byte| *byte == 0)
320 .filter(|path| !path.is_empty())
321 {
322 update_hash_field(&mut hasher, b"untracked-path", path);
323 let path = String::from_utf8_lossy(path);
324 match read_source_fingerprint_file_bounded(&workspace_root.join(path.as_ref())) {
325 Ok(Some(bytes)) => update_hash_field(&mut hasher, b"untracked-content", &bytes),
326 Ok(None) => update_hash_field(
327 &mut hasher,
328 b"untracked-content-oversized",
329 MAX_SOURCE_FINGERPRINT_FILE_BYTES.to_string().as_bytes(),
330 ),
331 Err(_) => {}
332 }
333 }
334 hasher.finalize().to_hex().to_string()
335}
336
337fn read_source_fingerprint_file_bounded(path: &Path) -> std::io::Result<Option<Vec<u8>>> {
338 let mut reader = fs::File::open(path)?;
339 let mut bytes = Vec::new();
340 let mut total = 0u64;
341 let mut chunk = [0u8; 8192];
342 loop {
343 let read = reader.read(&mut chunk)?;
344 if read == 0 {
345 return Ok(Some(bytes));
346 }
347 let read = read as u64;
348 total = total.saturating_add(read);
349 if total > MAX_SOURCE_FINGERPRINT_FILE_BYTES {
350 return Ok(None);
351 }
352 bytes.extend_from_slice(&chunk[..read as usize]);
353 }
354}
355
356fn digest_to_hex(digest: [u8; 32]) -> String {
357 hex_encode(&digest)
358}
359
360fn evidence_environment_digest(backend_id: &str, backend_version: &str) -> String {
361 let mut hasher = blake3::Hasher::new();
362 update_hash_field(
363 &mut hasher,
364 b"format",
365 ENVIRONMENT_FINGERPRINT_VERSION.as_bytes(),
366 );
367 update_hash_field(&mut hasher, b"backend-id", backend_id.as_bytes());
368 update_hash_field(&mut hasher, b"backend-version", backend_version.as_bytes());
369 hasher.finalize().to_hex().to_string()
370}
371
372fn shell(workspace_root: &Path, args: &[&str]) -> Result<String, String> {
373 let stdout = shell_bytes(workspace_root, args)?;
374 Ok(String::from_utf8_lossy(&stdout).trim().to_string())
375}
376
377fn shell_bytes(workspace_root: &Path, args: &[&str]) -> Result<Vec<u8>, String> {
378 let output = Command::new("git")
379 .args(args)
380 .current_dir(workspace_root)
381 .output()
382 .map_err(|e| e.to_string())?;
383 if output.status.success() {
384 Ok(output.stdout)
385 } else {
386 Err(String::from_utf8_lossy(&output.stderr).trim().to_string())
387 }
388}
389
390#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
392pub struct DispatchTimingEvidence {
393 pub wall_ns: Option<u64>,
395 pub device_ns: Option<u64>,
397 pub enqueue_ns: Option<u64>,
399 pub wait_ns: Option<u64>,
401}
402
403impl DispatchTimingEvidence {
404 #[must_use]
406 pub fn from_timed_dispatch(result: &TimedDispatchResult) -> Self {
407 Self {
408 wall_ns: Some(result.wall_ns),
409 device_ns: result.device_ns,
410 enqueue_ns: result.enqueue_ns,
411 wait_ns: result.wait_ns,
412 }
413 }
414
415 #[must_use]
417 pub fn has_timing(&self) -> bool {
418 self.wall_ns.is_some()
419 || self.device_ns.is_some()
420 || self.enqueue_ns.is_some()
421 || self.wait_ns.is_some()
422 }
423}
424
425#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
427pub struct EvidenceArtifact {
428 pub kind: String,
430 pub backend_id: Option<String>,
432 pub path: Option<String>,
434 pub digest: Option<String>,
436}
437
438impl EvidenceArtifact {
439 #[must_use]
441 pub fn new(
442 kind: impl Into<String>,
443 backend_id: Option<String>,
444 path: Option<String>,
445 digest: Option<String>,
446 ) -> Self {
447 Self {
448 kind: kind.into(),
449 backend_id,
450 path,
451 digest,
452 }
453 }
454
455 #[must_use]
457 pub fn from_pipeline_manifest(manifest: &PipelineReproManifest) -> Self {
458 Self {
459 kind: "pipeline_manifest".to_string(),
460 backend_id: Some(manifest.backend_id.clone()),
461 path: None,
462 digest: Some(manifest.program_digest.clone()),
463 }
464 }
465}
466
467#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
469pub struct ReplayEvidence {
470 pub command: String,
472 pub capsule_digest: Option<String>,
474}
475
476impl ReplayEvidence {
477 #[must_use]
479 pub fn new(command: impl Into<String>, capsule_digest: Option<String>) -> Self {
480 Self {
481 command: command.into(),
482 capsule_digest,
483 }
484 }
485}
486
487#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
490pub struct EvidenceDigestLedger {
491 pub schema: u32,
493 pub program_wire_version: String,
495 pub program_wire_digest: String,
497 pub normalized_program_version: String,
499 pub normalized_program_digest: String,
501 pub workload_version: String,
503 pub workload_digest: String,
505 pub source_version: String,
507 pub source_fingerprint: String,
509 pub source_tree_version: String,
511 pub source_tree_fingerprint: String,
513 pub environment_version: String,
515 pub environment_digest: String,
517}
518
519impl EvidenceDigestLedger {
520 pub const SCHEMA: u32 = 1;
522
523 pub fn for_inputs(
531 backend_id: &str,
532 backend_version: &str,
533 program: &Program,
534 config: &DispatchConfig,
535 source: &SourceProvenance,
536 ) -> Result<Self, BackendError> {
537 let normalized_program_digest =
538 try_normalized_program_cache_digest(program).map_err(|error| {
539 BackendError::InvalidProgram {
540 fix: format!(
541 "Fix: failed to build evidence Program digest: {error}. Validate and normalize the Program before dispatch evidence emission."
542 ),
543 }
544 })?;
545 Ok(Self {
546 schema: Self::SCHEMA,
547 program_wire_version: PROGRAM_WIRE_DIGEST_VERSION.to_string(),
548 program_wire_digest: digest_to_hex(program.fingerprint()),
549 normalized_program_version: NORMALIZED_PROGRAM_DIGEST_VERSION.to_string(),
550 normalized_program_digest: digest_to_hex(normalized_program_digest),
551 workload_version: WORKLOAD_FINGERPRINT_VERSION.to_string(),
552 workload_digest: digest_to_hex(dispatch_policy_cache_digest(config)),
553 source_version: SOURCE_FINGERPRINT_VERSION.to_string(),
554 source_fingerprint: source.source_fingerprint.clone(),
555 source_tree_version: SOURCE_TREE_FINGERPRINT_VERSION.to_string(),
556 source_tree_fingerprint: source.source_tree_fingerprint.clone(),
557 environment_version: ENVIRONMENT_FINGERPRINT_VERSION.to_string(),
558 environment_digest: evidence_environment_digest(backend_id, backend_version),
559 })
560 }
561
562 pub fn validate(&self) -> Result<(), BackendError> {
569 if self.schema != Self::SCHEMA {
570 return Err(BackendError::InvalidProgram {
571 fix: format!(
572 "Fix: evidence digest ledger schema {} is unsupported; regenerate evidence with schema {}.",
573 self.schema,
574 Self::SCHEMA
575 ),
576 });
577 }
578 validate_ledger_version(
579 "program_wire_version",
580 &self.program_wire_version,
581 PROGRAM_WIRE_DIGEST_VERSION,
582 )?;
583 validate_ledger_version(
584 "normalized_program_version",
585 &self.normalized_program_version,
586 NORMALIZED_PROGRAM_DIGEST_VERSION,
587 )?;
588 validate_ledger_version(
589 "workload_version",
590 &self.workload_version,
591 WORKLOAD_FINGERPRINT_VERSION,
592 )?;
593 validate_ledger_version(
594 "source_version",
595 &self.source_version,
596 SOURCE_FINGERPRINT_VERSION,
597 )?;
598 validate_ledger_version(
599 "source_tree_version",
600 &self.source_tree_version,
601 SOURCE_TREE_FINGERPRINT_VERSION,
602 )?;
603 validate_ledger_version(
604 "environment_version",
605 &self.environment_version,
606 ENVIRONMENT_FINGERPRINT_VERSION,
607 )?;
608 validate_hex_digest("program_wire_digest", &self.program_wire_digest)?;
609 validate_hex_digest("normalized_program_digest", &self.normalized_program_digest)?;
610 validate_hex_digest("workload_digest", &self.workload_digest)?;
611 validate_hex_digest("environment_digest", &self.environment_digest)?;
612 if self.source_fingerprint.trim().is_empty() {
613 return Err(BackendError::InvalidProgram {
614 fix: "Fix: evidence digest ledger source_fingerprint must be non-empty."
615 .to_string(),
616 });
617 }
618 if self.source_tree_fingerprint.trim().is_empty() {
619 return Err(BackendError::InvalidProgram {
620 fix: "Fix: evidence digest ledger source_tree_fingerprint must be non-empty."
621 .to_string(),
622 });
623 }
624 Ok(())
625 }
626}
627
628fn validate_ledger_version(label: &str, actual: &str, expected: &str) -> Result<(), BackendError> {
629 if actual != expected {
630 return Err(BackendError::InvalidProgram {
631 fix: format!(
632 "Fix: evidence digest ledger {label} must be `{expected}`, got `{actual}`."
633 ),
634 });
635 }
636 Ok(())
637}
638
639fn validate_hex_digest(label: &str, value: &str) -> Result<(), BackendError> {
640 if value.len() != 64 || !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
641 return Err(BackendError::InvalidProgram {
642 fix: format!("Fix: evidence digest ledger {label} must be a 64-character hex digest."),
643 });
644 }
645 Ok(())
646}
647
648#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
650pub struct EvidenceBundle {
651 pub schema: u32,
653 pub backend_id: String,
655 pub backend_version: String,
657 pub program_digest: String,
659 pub dispatch_policy: String,
661 pub digest_ledger: EvidenceDigestLedger,
664 pub source: SourceProvenance,
666 pub timing: DispatchTimingEvidence,
668 pub artifacts: Vec<EvidenceArtifact>,
670 pub replay: Option<ReplayEvidence>,
672}
673
674impl EvidenceBundle {
675 pub const SCHEMA: u32 = 1;
677
678 pub fn for_program(
684 backend: &dyn VyreBackend,
685 program: &Program,
686 config: &DispatchConfig,
687 source: SourceProvenance,
688 ) -> Result<Self, BackendError> {
689 source.validate()?;
690 let backend_id = backend.id();
691 let backend_version = backend.version();
692 let digest_ledger = EvidenceDigestLedger::for_inputs(
693 backend_id,
694 backend_version,
695 program,
696 config,
697 &source,
698 )?;
699 Ok(Self {
700 schema: Self::SCHEMA,
701 backend_id: backend_id.to_string(),
702 backend_version: backend_version.to_string(),
703 program_digest: digest_ledger.normalized_program_digest.clone(),
704 dispatch_policy: dispatch_policy_cache_string(config),
705 digest_ledger,
706 source,
707 timing: DispatchTimingEvidence::default(),
708 artifacts: Vec::new(),
709 replay: None,
710 })
711 }
712
713 #[must_use]
715 pub fn with_timed_dispatch(mut self, result: &TimedDispatchResult) -> Self {
716 self.timing = DispatchTimingEvidence::from_timed_dispatch(result);
717 self
718 }
719
720 #[must_use]
722 pub fn with_artifact(mut self, artifact: EvidenceArtifact) -> Self {
723 self.artifacts.push(artifact);
724 self
725 }
726
727 #[must_use]
729 pub fn with_replay(mut self, replay: ReplayEvidence) -> Self {
730 self.replay = Some(replay);
731 self
732 }
733
734 pub fn validate(&self) -> Result<(), BackendError> {
740 if self.schema != Self::SCHEMA {
741 return Err(BackendError::InvalidProgram {
742 fix: format!(
743 "Fix: evidence bundle schema {} is unsupported; regenerate evidence with schema {}.",
744 self.schema,
745 Self::SCHEMA
746 ),
747 });
748 }
749 if self.backend_id.trim().is_empty() {
750 return Err(BackendError::InvalidProgram {
751 fix: "Fix: evidence bundle backend_id must be non-empty.".to_string(),
752 });
753 }
754 if self.program_digest.len() != 64
755 || !self
756 .program_digest
757 .bytes()
758 .all(|byte| byte.is_ascii_hexdigit())
759 {
760 return Err(BackendError::InvalidProgram {
761 fix: "Fix: evidence bundle program_digest must be a 64-character hex digest."
762 .to_string(),
763 });
764 }
765 self.digest_ledger.validate()?;
766 if self.digest_ledger.normalized_program_digest != self.program_digest {
767 return Err(BackendError::InvalidProgram {
768 fix: "Fix: evidence bundle program_digest must match digest_ledger.normalized_program_digest.".to_string(),
769 });
770 }
771 if self.digest_ledger.source_fingerprint != self.source.source_fingerprint {
772 return Err(BackendError::InvalidProgram {
773 fix: "Fix: evidence bundle source_fingerprint must match digest_ledger.source_fingerprint.".to_string(),
774 });
775 }
776 if self.digest_ledger.source_tree_fingerprint != self.source.source_tree_fingerprint {
777 return Err(BackendError::InvalidProgram {
778 fix: "Fix: evidence bundle source_tree_fingerprint must match digest_ledger.source_tree_fingerprint.".to_string(),
779 });
780 }
781 self.source.validate()
782 }
783}
784
785#[cfg(test)]
786mod tests {
787 use std::sync::Arc;
788
789 use super::*;
790 use crate::backend::{private, CompiledPipeline, OutputBuffers};
791 use vyre_foundation::ir::{BufferDecl, DataType, Expr, Node};
792
793 #[derive(Clone)]
794 struct EvidenceTestBackend;
795
796 impl private::Sealed for EvidenceTestBackend {}
797
798 impl VyreBackend for EvidenceTestBackend {
799 fn id(&self) -> &'static str {
800 "evidence-test"
801 }
802
803 fn version(&self) -> &'static str {
804 "test-version"
805 }
806
807 fn dispatch(
808 &self,
809 _program: &Program,
810 _inputs: &[Vec<u8>],
811 _config: &DispatchConfig,
812 ) -> Result<Vec<Vec<u8>>, BackendError> {
813 Ok(vec![42_u32.to_le_bytes().to_vec()])
814 }
815 }
816
817 #[derive(Clone)]
818 struct VersionedEvidenceTestBackend {
819 id: &'static str,
820 version: &'static str,
821 }
822
823 impl private::Sealed for VersionedEvidenceTestBackend {}
824
825 impl VyreBackend for VersionedEvidenceTestBackend {
826 fn id(&self) -> &'static str {
827 self.id
828 }
829
830 fn version(&self) -> &'static str {
831 self.version
832 }
833
834 fn dispatch(
835 &self,
836 _program: &Program,
837 _inputs: &[Vec<u8>],
838 _config: &DispatchConfig,
839 ) -> Result<Vec<Vec<u8>>, BackendError> {
840 Ok(vec![42_u32.to_le_bytes().to_vec()])
841 }
842 }
843
844 struct EvidencePipeline;
845
846 impl private::Sealed for EvidencePipeline {}
847
848 impl CompiledPipeline for EvidencePipeline {
849 fn id(&self) -> &str {
850 "evidence-test:pipeline"
851 }
852
853 fn dispatch(
854 &self,
855 _inputs: &[Vec<u8>],
856 _config: &DispatchConfig,
857 ) -> Result<OutputBuffers, BackendError> {
858 Ok(vec![42_u32.to_le_bytes().to_vec()])
859 }
860 }
861
862 fn evidence_program() -> Program {
863 Program::wrapped(
864 vec![
865 BufferDecl::read("input", 0, DataType::U32).with_count(1),
866 BufferDecl::output("output", 1, DataType::U32).with_count(1),
867 ],
868 [1, 1, 1],
869 vec![Node::store(
870 "output",
871 Expr::u32(0),
872 Expr::load("input", Expr::u32(0)),
873 )],
874 )
875 }
876
877 fn source() -> SourceProvenance {
878 SourceProvenance {
879 git: BTreeMap::from([
880 ("commit".to_string(), "abc123".to_string()),
881 ("dirty".to_string(), "false".to_string()),
882 ]),
883 source_fingerprint: "git:abc123:dirty=false".to_string(),
884 source_tree_fingerprint: "source-tree-v1:test".to_string(),
885 }
886 }
887
888 fn changed_ledger_lanes(
889 left: &EvidenceDigestLedger,
890 right: &EvidenceDigestLedger,
891 ) -> Vec<&'static str> {
892 let mut changed = Vec::new();
893 if left.program_wire_digest != right.program_wire_digest {
894 changed.push("program_wire_digest");
895 }
896 if left.normalized_program_digest != right.normalized_program_digest {
897 changed.push("normalized_program_digest");
898 }
899 if left.workload_digest != right.workload_digest {
900 changed.push("workload_digest");
901 }
902 if left.source_fingerprint != right.source_fingerprint {
903 changed.push("source_fingerprint");
904 }
905 if left.source_tree_fingerprint != right.source_tree_fingerprint {
906 changed.push("source_tree_fingerprint");
907 }
908 if left.environment_digest != right.environment_digest {
909 changed.push("environment_digest");
910 }
911 changed
912 }
913
914 #[test]
915 fn evidence_bundle_records_backend_program_policy_source_timing_and_artifacts() {
916 let backend = EvidenceTestBackend;
917 let program = evidence_program();
918 let mut config = DispatchConfig::default();
919 config.workgroup_override = Some([8, 1, 1]);
920 let timed = TimedDispatchResult {
921 outputs: vec![42_u32.to_le_bytes().to_vec()],
922 wall_ns: 100,
923 device_ns: Some(70),
924 enqueue_ns: Some(10),
925 wait_ns: Some(20),
926 };
927 let pipeline = Arc::new(EvidencePipeline);
928 let manifest = PipelineReproManifest::new(
929 backend.id(),
930 pipeline.id(),
931 try_normalized_program_cache_digest(&program)
932 .expect("Fix: evidence test Program must fingerprint"),
933 dispatch_policy_cache_string(&config),
934 Some(true),
935 );
936
937 let bundle = EvidenceBundle::for_program(&backend, &program, &config, source())
938 .expect("Fix: evidence bundle should build for valid source/program")
939 .with_timed_dispatch(&timed)
940 .with_artifact(EvidenceArtifact::from_pipeline_manifest(&manifest))
941 .with_replay(ReplayEvidence::new(
942 "vyre-conform dispatch --backend evidence-test --ops evidence.test",
943 Some("capsule-digest".to_string()),
944 ));
945
946 bundle
947 .validate()
948 .expect("Fix: complete evidence bundle should validate");
949 assert_eq!(bundle.backend_id, "evidence-test");
950 assert_eq!(bundle.backend_version, "test-version");
951 assert_eq!(bundle.program_digest.len(), 64);
952 assert_eq!(
953 bundle.program_digest,
954 bundle.digest_ledger.normalized_program_digest
955 );
956 assert_eq!(
957 bundle.digest_ledger.program_wire_version,
958 PROGRAM_WIRE_DIGEST_VERSION
959 );
960 assert_eq!(
961 bundle.digest_ledger.normalized_program_version,
962 NORMALIZED_PROGRAM_DIGEST_VERSION
963 );
964 assert_eq!(
965 bundle.digest_ledger.workload_version,
966 WORKLOAD_FINGERPRINT_VERSION
967 );
968 assert_eq!(bundle.dispatch_policy, "ulp=None:wg=Some([8, 1, 1])");
969 assert_eq!(bundle.source.source_fingerprint, "git:abc123:dirty=false");
970 assert_eq!(bundle.timing.device_ns, Some(70));
971 assert_eq!(bundle.artifacts[0].kind, "pipeline_manifest");
972 assert_eq!(
973 bundle.replay.as_ref().map(|replay| replay.command.as_str()),
974 Some("vyre-conform dispatch --backend evidence-test --ops evidence.test")
975 );
976 }
977
978 #[test]
979 fn digest_ledger_scopes_program_source_workload_and_environment_changes() {
980 let backend = VersionedEvidenceTestBackend {
981 id: "evidence-test",
982 version: "test-version",
983 };
984 let program = evidence_program();
985 let config = DispatchConfig::default();
986 let source = source();
987 let base = EvidenceBundle::for_program(&backend, &program, &config, source.clone())
988 .expect("Fix: base evidence bundle must build")
989 .digest_ledger;
990
991 let changed_program = Program::wrapped(
992 vec![
993 BufferDecl::read("input", 0, DataType::U32).with_count(1),
994 BufferDecl::output("output", 1, DataType::U32).with_count(1),
995 ],
996 [1, 1, 1],
997 vec![Node::store("output", Expr::u32(0), Expr::u32(7))],
998 );
999 let program_changed =
1000 EvidenceBundle::for_program(&backend, &changed_program, &config, source.clone())
1001 .expect("Fix: changed Program evidence bundle must build")
1002 .digest_ledger;
1003 assert_eq!(
1004 changed_ledger_lanes(&base, &program_changed),
1005 vec!["program_wire_digest", "normalized_program_digest"],
1006 "Fix: Program body mutations must not perturb source, workload, or environment digest lanes."
1007 );
1008
1009 let source_changed = SourceProvenance {
1010 source_fingerprint: "git:def456:dirty=false".to_string(),
1011 ..source.clone()
1012 };
1013 let source_ledger =
1014 EvidenceBundle::for_program(&backend, &program, &config, source_changed)
1015 .expect("Fix: changed source evidence bundle must build")
1016 .digest_ledger;
1017 assert_eq!(
1018 changed_ledger_lanes(&base, &source_ledger),
1019 vec!["source_fingerprint"],
1020 "Fix: source fingerprint mutations must stay in the source lane."
1021 );
1022
1023 let source_tree_changed = SourceProvenance {
1024 source_tree_fingerprint: "source-tree-v1:changed".to_string(),
1025 ..source.clone()
1026 };
1027 let source_tree_ledger =
1028 EvidenceBundle::for_program(&backend, &program, &config, source_tree_changed)
1029 .expect("Fix: changed source-tree evidence bundle must build")
1030 .digest_ledger;
1031 assert_eq!(
1032 changed_ledger_lanes(&base, &source_tree_ledger),
1033 vec!["source_tree_fingerprint"],
1034 "Fix: source-tree mutations must stay in the source-tree lane."
1035 );
1036
1037 let mut workload_changed = DispatchConfig::default();
1038 workload_changed.workgroup_override = Some([8, 1, 1]);
1039 let workload_ledger =
1040 EvidenceBundle::for_program(&backend, &program, &workload_changed, source.clone())
1041 .expect("Fix: changed workload evidence bundle must build")
1042 .digest_ledger;
1043 assert_eq!(
1044 changed_ledger_lanes(&base, &workload_ledger),
1045 vec!["workload_digest"],
1046 "Fix: workload/config mutations must stay in the workload digest lane."
1047 );
1048
1049 let environment_changed = VersionedEvidenceTestBackend {
1050 id: "evidence-test",
1051 version: "test-version-2",
1052 };
1053 let environment_ledger =
1054 EvidenceBundle::for_program(&environment_changed, &program, &config, source)
1055 .expect("Fix: changed environment evidence bundle must build")
1056 .digest_ledger;
1057 assert_eq!(
1058 changed_ledger_lanes(&base, &environment_ledger),
1059 vec!["environment_digest"],
1060 "Fix: backend environment mutations must stay in the environment digest lane."
1061 );
1062 }
1063
1064 #[test]
1065 fn evidence_bundle_rejects_digest_ledger_mismatch() {
1066 let backend = EvidenceTestBackend;
1067 let program = evidence_program();
1068 let mut bundle =
1069 EvidenceBundle::for_program(&backend, &program, &DispatchConfig::default(), source())
1070 .expect("Fix: evidence bundle should build before ledger mutation");
1071 bundle.digest_ledger.normalized_program_digest =
1072 "0000000000000000000000000000000000000000000000000000000000000000".to_string();
1073
1074 let error = bundle
1075 .validate()
1076 .expect_err("Fix: evidence validation must reject a mismatched digest ledger");
1077 assert!(
1078 error.to_string().contains("digest_ledger"),
1079 "Fix: digest ledger mismatch rejection must name the mismatched field: {error}"
1080 );
1081 }
1082
1083 #[test]
1084 fn evidence_bundle_rejects_weak_source_provenance() {
1085 let backend = EvidenceTestBackend;
1086 let program = evidence_program();
1087 let invalid = SourceProvenance {
1088 git: BTreeMap::new(),
1089 source_fingerprint: " ".to_string(),
1090 source_tree_fingerprint: "source-tree-v1:test".to_string(),
1091 };
1092
1093 let error =
1094 EvidenceBundle::for_program(&backend, &program, &DispatchConfig::default(), invalid)
1095 .expect_err("Fix: evidence bundle must reject blank source_fingerprint");
1096
1097 assert!(
1098 error.to_string().contains("source_fingerprint"),
1099 "Fix: source provenance rejection must name the weak field: {error}"
1100 );
1101 }
1102
1103 #[test]
1104 fn clean_source_fingerprint_keeps_commit_dirty_contract() {
1105 let git = BTreeMap::from([
1106 ("commit".to_string(), "abc123".to_string()),
1107 ("dirty".to_string(), "false".to_string()),
1108 ]);
1109
1110 assert_eq!(
1111 source_fingerprint(&git),
1112 "git:abc123:dirty=false",
1113 "Fix: clean source fingerprints must remain stable for existing release evidence contracts."
1114 );
1115 }
1116
1117 #[test]
1118 fn dirty_source_fingerprint_carries_worktree_digest() {
1119 let git = BTreeMap::from([
1120 ("commit".to_string(), "abc123".to_string()),
1121 ("dirty".to_string(), "true".to_string()),
1122 (
1123 "dirty_worktree_fingerprint".to_string(),
1124 "worktree-hash".to_string(),
1125 ),
1126 ]);
1127
1128 assert_eq!(
1129 source_fingerprint(&git),
1130 "git:abc123:dirty=true:worktree=worktree-hash",
1131 "Fix: dirty source fingerprints must distinguish different dirty worktree states."
1132 );
1133 }
1134
1135 #[test]
1136 fn dirty_source_fingerprint_without_digest_fails_closed() {
1137 let git = BTreeMap::from([
1138 ("commit".to_string(), "abc123".to_string()),
1139 ("dirty".to_string(), "true".to_string()),
1140 ]);
1141
1142 assert_eq!(
1143 source_fingerprint(&git),
1144 "git:abc123:dirty=true:worktree=unknown",
1145 "Fix: dirty source fingerprints must not fall back to the broad legacy dirty=true contract."
1146 );
1147 }
1148
1149 #[test]
1150 fn dirty_worktree_digest_changes_with_status_diff_and_untracked_content() {
1151 let workspace = Path::new(".");
1152 let base =
1153 dirty_worktree_fingerprint_from_parts(workspace, b" M a.rs\0", b"-old\n+new\n", b"");
1154 let changed_status =
1155 dirty_worktree_fingerprint_from_parts(workspace, b" M b.rs\0", b"-old\n+new\n", b"");
1156 let changed_diff =
1157 dirty_worktree_fingerprint_from_parts(workspace, b" M a.rs\0", b"-old\n+newer\n", b"");
1158 let changed_untracked_inventory =
1159 dirty_worktree_fingerprint_from_parts(workspace, b"?? c.rs\0", b"", b"c.rs\0");
1160 let untracked_workspace = temp_workspace("vyre-driver-dirty-fingerprint");
1161 fs::write(untracked_workspace.join("c.rs"), b"one")
1162 .expect("Fix: write first untracked content fingerprint fixture.");
1163 let untracked_one = dirty_worktree_fingerprint_from_parts(
1164 &untracked_workspace,
1165 b"?? c.rs\0",
1166 b"",
1167 b"c.rs\0",
1168 );
1169 fs::write(untracked_workspace.join("c.rs"), b"two")
1170 .expect("Fix: write second untracked content fingerprint fixture.");
1171 let untracked_two = dirty_worktree_fingerprint_from_parts(
1172 &untracked_workspace,
1173 b"?? c.rs\0",
1174 b"",
1175 b"c.rs\0",
1176 );
1177 let _ = fs::remove_dir_all(&untracked_workspace);
1178
1179 assert_ne!(
1180 base, changed_status,
1181 "Fix: dirty source fingerprints must change when modified paths change."
1182 );
1183 assert_ne!(
1184 base, changed_diff,
1185 "Fix: dirty source fingerprints must change when tracked diff bytes change."
1186 );
1187 assert_ne!(
1188 base, changed_untracked_inventory,
1189 "Fix: dirty source fingerprints must change when untracked inventory changes."
1190 );
1191 assert_ne!(
1192 untracked_one, untracked_two,
1193 "Fix: dirty source fingerprints must change when untracked file content changes."
1194 );
1195 }
1196
1197 #[test]
1198 fn source_tree_fingerprint_ignores_generated_release_evidence() {
1199 let workspace = temp_workspace("vyre-driver-source-tree-fingerprint");
1200 fs::create_dir_all(workspace.join("src")).expect("Fix: create source fixture directory.");
1201 fs::create_dir_all(workspace.join("release/evidence/benchmarks"))
1202 .expect("Fix: create generated evidence fixture directory.");
1203 fs::write(workspace.join("src/lib.rs"), b"pub fn source() {}\n")
1204 .expect("Fix: write source-tree fingerprint source fixture.");
1205 fs::write(
1206 workspace.join("release/evidence/benchmarks/workload.json"),
1207 b"{\"old\":true}\n",
1208 )
1209 .expect("Fix: write source-tree fingerprint evidence fixture.");
1210 let paths = b"src/lib.rs\0release/evidence/benchmarks/workload.json\0";
1211
1212 let base = source_tree_fingerprint_from_paths(&workspace, paths);
1213 fs::write(
1214 workspace.join("release/evidence/benchmarks/workload.json"),
1215 b"{\"new\":true}\n",
1216 )
1217 .expect("Fix: mutate generated evidence fixture.");
1218 let evidence_changed = source_tree_fingerprint_from_paths(&workspace, paths);
1219 fs::write(
1220 workspace.join("src/lib.rs"),
1221 b"pub fn source_changed() {}\n",
1222 )
1223 .expect("Fix: mutate source fixture.");
1224 let source_changed = source_tree_fingerprint_from_paths(&workspace, paths);
1225 let _ = fs::remove_dir_all(&workspace);
1226
1227 assert_eq!(
1228 base, evidence_changed,
1229 "Fix: generated release evidence must not invalidate committed benchmark source provenance."
1230 );
1231 assert_ne!(
1232 base, source_changed,
1233 "Fix: source-tree provenance must still change when real source files change."
1234 );
1235 }
1236
1237 #[test]
1238 fn source_tree_fingerprint_ignores_release_tooling_source() {
1239 let workspace = temp_workspace("vyre-driver-source-tree-tooling");
1240 fs::create_dir_all(workspace.join("vyre-bench/src"))
1241 .expect("Fix: create benchmark source fixture directory.");
1242 fs::create_dir_all(workspace.join(".github/workflows"))
1243 .expect("Fix: create workflow fixture directory.");
1244 fs::create_dir_all(workspace.join("scripts"))
1245 .expect("Fix: create release script fixture directory.");
1246 fs::create_dir_all(workspace.join("xtask/src"))
1247 .expect("Fix: create release tooling fixture directory.");
1248 fs::write(workspace.join("cargo_full"), b"#!/usr/bin/env bash\n")
1249 .expect("Fix: write cargo wrapper fixture.");
1250 fs::write(
1251 workspace.join("vyre-bench/src/lib.rs"),
1252 b"pub fn benchmark() {}\n",
1253 )
1254 .expect("Fix: write benchmark source fixture.");
1255 fs::write(
1256 workspace.join("xtask/src/hygiene_matrix.rs"),
1257 b"pub fn tooling() {}\n",
1258 )
1259 .expect("Fix: write release tooling fixture.");
1260 fs::write(
1261 workspace.join("scripts/install_lego_quick_hook.sh"),
1262 b"#!/usr/bin/env bash\n",
1263 )
1264 .expect("Fix: write release script fixture.");
1265 fs::write(
1266 workspace.join(".github/workflows/ci.yml"),
1267 b"run: ./cargo_full test --workspace\n",
1268 )
1269 .expect("Fix: write workflow fixture.");
1270 let paths = b".github/workflows/ci.yml\0cargo_full\0scripts/install_lego_quick_hook.sh\0vyre-bench/src/lib.rs\0xtask/src/hygiene_matrix.rs\0";
1271
1272 let base = source_tree_fingerprint_from_paths(&workspace, paths);
1273 fs::write(
1274 workspace.join("cargo_full"),
1275 b"#!/usr/bin/env bash\nexec cargo \"$@\"\n",
1276 )
1277 .expect("Fix: mutate cargo wrapper fixture.");
1278 let wrapper_changed = source_tree_fingerprint_from_paths(&workspace, paths);
1279 fs::write(
1280 workspace.join("scripts/install_lego_quick_hook.sh"),
1281 b"#!/usr/bin/env bash\n./cargo_full run --bin xtask -- lego-quick\n",
1282 )
1283 .expect("Fix: mutate release script fixture.");
1284 let script_changed = source_tree_fingerprint_from_paths(&workspace, paths);
1285 fs::write(
1286 workspace.join(".github/workflows/ci.yml"),
1287 b"run: ./cargo_full test --workspace --all-targets\n",
1288 )
1289 .expect("Fix: mutate workflow fixture.");
1290 let workflow_changed = source_tree_fingerprint_from_paths(&workspace, paths);
1291 fs::write(
1292 workspace.join("xtask/src/hygiene_matrix.rs"),
1293 b"pub fn tooling_changed() {}\n",
1294 )
1295 .expect("Fix: mutate release tooling fixture.");
1296 let tooling_changed = source_tree_fingerprint_from_paths(&workspace, paths);
1297 fs::write(
1298 workspace.join("vyre-bench/src/lib.rs"),
1299 b"pub fn benchmark_changed() {}\n",
1300 )
1301 .expect("Fix: mutate benchmark source fixture.");
1302 let benchmark_changed = source_tree_fingerprint_from_paths(&workspace, paths);
1303 let _ = fs::remove_dir_all(&workspace);
1304
1305 assert_eq!(
1306 base, tooling_changed,
1307 "Fix: release evidence/tooling generators must not invalidate benchmark runtime source provenance."
1308 );
1309 assert_eq!(
1310 base, wrapper_changed,
1311 "Fix: bounded cargo wrapper changes must not invalidate benchmark runtime source provenance."
1312 );
1313 assert_eq!(
1314 base, script_changed,
1315 "Fix: release scripts must not invalidate benchmark runtime source provenance."
1316 );
1317 assert_eq!(
1318 base, workflow_changed,
1319 "Fix: CI workflow edits must not invalidate benchmark runtime source provenance."
1320 );
1321 assert_ne!(
1322 base, benchmark_changed,
1323 "Fix: benchmark source edits must still invalidate benchmark source provenance."
1324 );
1325 }
1326
1327 #[test]
1328 fn source_tree_fingerprint_ignores_test_evidence() {
1329 let workspace = temp_workspace("vyre-driver-source-tree-tests");
1330 fs::create_dir_all(workspace.join("vyre-libs/src"))
1331 .expect("Fix: create library source fixture directory.");
1332 fs::create_dir_all(workspace.join("vyre-libs/tests/support"))
1333 .expect("Fix: create integration test support fixture directory.");
1334 fs::create_dir_all(workspace.join("vyre-libs/src/graph"))
1335 .expect("Fix: create inline test fixture directory.");
1336 fs::write(
1337 workspace.join("vyre-libs/src/lib.rs"),
1338 b"pub fn source() {}\n",
1339 )
1340 .expect("Fix: write source-tree fingerprint source fixture.");
1341 fs::write(
1342 workspace.join("vyre-libs/tests/filter_roundtrip.rs"),
1343 b"#[test]\nfn roundtrip() {}\n",
1344 )
1345 .expect("Fix: write integration test fixture.");
1346 fs::write(
1347 workspace.join("vyre-libs/tests/support/filter.rs"),
1348 b"pub fn helper() {}\n",
1349 )
1350 .expect("Fix: write test support fixture.");
1351 fs::write(
1352 workspace.join("vyre-libs/src/graph/tests.rs"),
1353 b"#[test]\nfn graph_contract() {}\n",
1354 )
1355 .expect("Fix: write inline tests fixture.");
1356 let paths = b"vyre-libs/src/lib.rs\0vyre-libs/tests/filter_roundtrip.rs\0vyre-libs/tests/support/filter.rs\0vyre-libs/src/graph/tests.rs\0";
1357
1358 let base = source_tree_fingerprint_from_paths(&workspace, paths);
1359 fs::write(
1360 workspace.join("vyre-libs/tests/filter_roundtrip.rs"),
1361 b"#[test]\nfn roundtrip_modularized() {}\n",
1362 )
1363 .expect("Fix: mutate integration test fixture.");
1364 fs::write(
1365 workspace.join("vyre-libs/tests/support/filter.rs"),
1366 b"pub fn helper_modularized() {}\n",
1367 )
1368 .expect("Fix: mutate test support fixture.");
1369 fs::write(
1370 workspace.join("vyre-libs/src/graph/tests.rs"),
1371 b"#[test]\nfn graph_contract_modularized() {}\n",
1372 )
1373 .expect("Fix: mutate inline tests fixture.");
1374 let tests_changed = source_tree_fingerprint_from_paths(&workspace, paths);
1375 fs::write(
1376 workspace.join("vyre-libs/src/lib.rs"),
1377 b"pub fn source_changed() {}\n",
1378 )
1379 .expect("Fix: mutate production source fixture.");
1380 let source_changed = source_tree_fingerprint_from_paths(&workspace, paths);
1381 let _ = fs::remove_dir_all(&workspace);
1382
1383 assert_eq!(
1384 base, tests_changed,
1385 "Fix: test-only modularization must not invalidate runtime benchmark source provenance."
1386 );
1387 assert_ne!(
1388 base, source_changed,
1389 "Fix: source-tree provenance must still change when production source changes."
1390 );
1391 }
1392
1393 #[test]
1394 fn source_fingerprint_ignores_generated_release_evidence_dirty_status() {
1395 let workspace = temp_workspace("vyre-driver-source-fingerprint-evidence");
1396 fs::create_dir_all(workspace.join("src"))
1397 .expect("Fix: create source fingerprint fixture source directory.");
1398 fs::create_dir_all(workspace.join("release/evidence/benchmarks"))
1399 .expect("Fix: create source fingerprint fixture evidence directory.");
1400 fs::write(workspace.join("src/lib.rs"), b"pub fn source() {}\n")
1401 .expect("Fix: write source fingerprint source fixture.");
1402 fs::write(
1403 workspace.join("release/evidence/benchmarks/workload.json"),
1404 b"{\"old\":true}\n",
1405 )
1406 .expect("Fix: write tracked generated evidence fixture.");
1407 git_fixture(&workspace, &["init", "--quiet", "--initial-branch", "main"]);
1408 git_fixture(
1409 &workspace,
1410 &["config", "user.email", "vyre@example.invalid"],
1411 );
1412 git_fixture(&workspace, &["config", "user.name", "Vyre Test"]);
1413 git_fixture(
1414 &workspace,
1415 &[
1416 "add",
1417 "src/lib.rs",
1418 "release/evidence/benchmarks/workload.json",
1419 ],
1420 );
1421 git_fixture(&workspace, &["commit", "--quiet", "-m", "seed"]);
1422
1423 fs::write(
1424 workspace.join("release/evidence/benchmarks/workload.json"),
1425 b"{\"new\":true}\n",
1426 )
1427 .expect("Fix: mutate tracked generated evidence fixture.");
1428 fs::write(
1429 workspace.join("release/evidence/benchmarks/new-workload.json"),
1430 b"{\"new\":true}\n",
1431 )
1432 .expect("Fix: write untracked generated evidence fixture.");
1433 let evidence_only = capture_git_info_at(&workspace);
1434 fs::write(
1435 workspace.join("src/lib.rs"),
1436 b"pub fn source_changed() {}\n",
1437 )
1438 .expect("Fix: mutate real source fixture.");
1439 let source_changed = capture_git_info_at(&workspace);
1440 let _ = fs::remove_dir_all(&workspace);
1441
1442 assert_eq!(
1443 evidence_only.get("dirty").map(String::as_str),
1444 Some("false"),
1445 "Fix: generated release evidence writes must not mark benchmark source provenance dirty."
1446 );
1447 assert_eq!(
1448 source_changed.get("dirty").map(String::as_str),
1449 Some("true"),
1450 "Fix: real source edits must still mark benchmark source provenance dirty."
1451 );
1452 }
1453
1454 fn temp_workspace(prefix: &str) -> std::path::PathBuf {
1455 let workspace = std::env::temp_dir().join(format!(
1456 "{prefix}-{}-{}",
1457 std::process::id(),
1458 std::time::SystemTime::now()
1459 .duration_since(std::time::UNIX_EPOCH)
1460 .expect("Fix: system clock must support unix epoch duration for temp test id.")
1461 .as_nanos()
1462 ));
1463 fs::create_dir_all(&workspace).expect("Fix: create temporary provenance test workspace.");
1464 workspace
1465 }
1466
1467 fn git_fixture(workspace: &Path, args: &[&str]) {
1468 let output = Command::new("git")
1469 .args(args)
1470 .current_dir(workspace)
1471 .output()
1472 .expect("Fix: git fixture command must start.");
1473 assert!(
1474 output.status.success(),
1475 "Fix: git fixture command `git {}` failed: {}",
1476 args.join(" "),
1477 String::from_utf8_lossy(&output.stderr).trim()
1478 );
1479 }
1480}