Skip to main content

vyre_driver/
evidence.rs

1//! Backend-neutral evidence, provenance, and replay metadata.
2//!
3//! This module is the shared driver-layer contract for source provenance and
4//! dispatch evidence. Benchmark reports, conformance artifacts, replay
5//! capsules, and consumer APIs should import this surface instead of owning
6//! parallel fingerprint or artifact schemas.
7
8use 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
25/// Version label for the normalized Program digest used by compiled-pipeline
26/// caches.
27///
28/// Aliased to the constant the digest algorithm itself uses as its domain
29/// separator, so the label cannot describe an algorithm the digest no longer
30/// implements. It was hand-mirrored before, which is exactly the silent drift
31/// this ledger exists to prevent.
32pub const NORMALIZED_PROGRAM_DIGEST_VERSION: &str =
33    vyre_foundation::ir::NORMALIZED_PROGRAM_CACHE_DIGEST_VERSION;
34
35/// Version label for commit/dirty-state source fingerprints.
36pub const SOURCE_FINGERPRINT_VERSION: &str = "vyre-source-fingerprint-v1";
37const MAX_SOURCE_FINGERPRINT_FILE_BYTES: u64 = 64 * 1024 * 1024;
38
39/// Version label for source-tree content fingerprints.
40pub const SOURCE_TREE_FINGERPRINT_VERSION: &str = "source-tree-v1";
41
42/// Version label for dispatch workload/config fingerprints.
43pub const WORKLOAD_FINGERPRINT_VERSION: &str = "vyre-dispatch-workload-v1";
44
45/// Version label for backend environment fingerprints.
46pub const ENVIRONMENT_FINGERPRINT_VERSION: &str = "vyre-evidence-environment-v1";
47
48/// Git and source-tree provenance for evidence-producing runs.
49#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
50pub struct SourceProvenance {
51    /// Raw git facts captured from the source workspace.
52    pub git: BTreeMap<String, String>,
53    /// Commit/dirty-state source identity used by release evidence gates.
54    pub source_fingerprint: String,
55    /// Source-tree content identity used to tolerate evidence-only commit drift.
56    pub source_tree_fingerprint: String,
57}
58
59impl SourceProvenance {
60    /// Capture provenance for the current working directory.
61    #[must_use]
62    pub fn capture_current() -> Self {
63        Self::capture_at(Path::new("."))
64    }
65
66    /// Capture provenance for `workspace_root`.
67    #[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    /// Validate that required provenance fields are non-empty and shaped.
80    ///
81    /// # Errors
82    /// Returns [`BackendError::InvalidProgram`] when an evidence producer
83    /// attempts to emit a weak source identity.
84    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/// Capture git facts for the current working directory.
102#[must_use]
103pub fn capture_git_info() -> BTreeMap<String, String> {
104    capture_git_info_at(Path::new("."))
105}
106
107/// Capture git facts for `workspace_root`.
108#[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/// Build the commit/dirty-state source fingerprint used by release evidence.
153#[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/// Capture a source-tree fingerprint for the current working directory.
175#[must_use]
176pub fn source_tree_fingerprint() -> String {
177    source_tree_fingerprint_at(Path::new("."))
178}
179
180/// Capture a source-tree fingerprint for `workspace_root`.
181#[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/// Timing evidence normalized across host and device timing sources.
370#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
371pub struct DispatchTimingEvidence {
372    /// Host-observed dispatch duration.
373    pub wall_ns: Option<u64>,
374    /// Device-observed elapsed time when available.
375    pub device_ns: Option<u64>,
376    /// Host enqueue duration when available.
377    pub enqueue_ns: Option<u64>,
378    /// Host wait/readback duration when available.
379    pub wait_ns: Option<u64>,
380}
381
382impl DispatchTimingEvidence {
383    /// Build timing evidence from a timed dispatch result.
384    #[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    /// Return true when the evidence has at least one timing source.
395    #[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/// One artifact referenced by an evidence bundle.
405#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
406pub struct EvidenceArtifact {
407    /// Stable artifact kind, such as `pipeline_manifest`, `benchmark_report`, or `replay_capsule`.
408    pub kind: String,
409    /// Backend that produced or owns the artifact when applicable.
410    pub backend_id: Option<String>,
411    /// Relative or consumer-provided artifact path.
412    pub path: Option<String>,
413    /// Content digest or identity digest when available.
414    pub digest: Option<String>,
415}
416
417impl EvidenceArtifact {
418    /// Build an artifact row.
419    #[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    /// Build an artifact row from a compiled-pipeline manifest.
435    #[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/// Replay metadata attached to a dispatch or conformance failure.
447#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
448pub struct ReplayEvidence {
449    /// Human-runnable replay command.
450    pub command: String,
451    /// Capsule digest when the replay payload has been materialized.
452    pub capsule_digest: Option<String>,
453}
454
455impl ReplayEvidence {
456    /// Build replay evidence.
457    #[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/// Versioned digest ledger for every identity lane that participates in
467/// evidence replay, provenance, and cache correlation.
468#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
469pub struct EvidenceDigestLedger {
470    /// Ledger schema version.
471    pub schema: u32,
472    /// Version label for `program_wire_digest`.
473    pub program_wire_version: String,
474    /// BLAKE3 digest of canonical VIR0 Program wire bytes.
475    pub program_wire_digest: String,
476    /// Version label for `normalized_program_digest`.
477    pub normalized_program_version: String,
478    /// Normalized Program digest used by pipeline caches.
479    pub normalized_program_digest: String,
480    /// Version label for `workload_digest`.
481    pub workload_version: String,
482    /// Dispatch workload/config digest.
483    pub workload_digest: String,
484    /// Version label for `source_fingerprint`.
485    pub source_version: String,
486    /// Commit/dirty-state source fingerprint.
487    pub source_fingerprint: String,
488    /// Version label for `source_tree_fingerprint`.
489    pub source_tree_version: String,
490    /// Source-tree content fingerprint.
491    pub source_tree_fingerprint: String,
492    /// Version label for `environment_digest`.
493    pub environment_version: String,
494    /// Backend id/version digest.
495    pub environment_digest: String,
496}
497
498impl EvidenceDigestLedger {
499    /// Current digest-ledger schema.
500    pub const SCHEMA: u32 = 1;
501
502    /// Build the digest ledger from the same inputs used to build an evidence
503    /// bundle.
504    ///
505    /// # Errors
506    ///
507    /// Returns [`BackendError::InvalidProgram`] when the normalized Program
508    /// digest cannot be built.
509    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    /// Validate ledger version labels and digest shapes.
542    ///
543    /// # Errors
544    ///
545    /// Returns [`BackendError::InvalidProgram`] when any ledger lane is missing,
546    /// malformed, or versioned against the wrong contract.
547    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/// Shared evidence bundle for dispatch, benchmark, conformance, and replay surfaces.
628#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
629pub struct EvidenceBundle {
630    /// Bundle schema version.
631    pub schema: u32,
632    /// Backend that produced the result or artifact.
633    pub backend_id: String,
634    /// Backend implementation version.
635    pub backend_version: String,
636    /// Canonical normalized Program digest as lowercase hex.
637    pub program_digest: String,
638    /// Dispatch policy fields that affect generated backend code.
639    pub dispatch_policy: String,
640    /// Versioned digest ledger binding Program, workload, source, and backend
641    /// environment identity.
642    pub digest_ledger: EvidenceDigestLedger,
643    /// Source provenance for the code that produced this evidence.
644    pub source: SourceProvenance,
645    /// Timing evidence for the dispatch or run.
646    pub timing: DispatchTimingEvidence,
647    /// Artifacts referenced by this bundle.
648    pub artifacts: Vec<EvidenceArtifact>,
649    /// Replay metadata when a replay capsule exists.
650    pub replay: Option<ReplayEvidence>,
651}
652
653impl EvidenceBundle {
654    /// Current evidence bundle schema.
655    pub const SCHEMA: u32 = 1;
656
657    /// Build an evidence bundle for a backend/program/config tuple.
658    ///
659    /// # Errors
660    /// Returns [`BackendError`] when the Program cannot be fingerprinted or
661    /// provenance is too weak to emit.
662    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    /// Attach timing from a backend dispatch result.
693    #[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    /// Attach an artifact row.
700    #[must_use]
701    pub fn with_artifact(mut self, artifact: EvidenceArtifact) -> Self {
702        self.artifacts.push(artifact);
703        self
704    }
705
706    /// Attach replay metadata.
707    #[must_use]
708    pub fn with_replay(mut self, replay: ReplayEvidence) -> Self {
709        self.replay = Some(replay);
710        self
711    }
712
713    /// Validate the bundle's load-bearing fields.
714    ///
715    /// # Errors
716    /// Returns [`BackendError::InvalidProgram`] when a bundle is missing a
717    /// required identity field or carries malformed digest metadata.
718    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}