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