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