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::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
24/// Version label for the normalized Program digest used by compiled-pipeline
25/// caches. The byte contract currently lives in `pipeline::hashing`; evidence
26/// records the same label in its digest ledger so cache identity and replay
27/// evidence cannot silently drift.
28pub const NORMALIZED_PROGRAM_DIGEST_VERSION: &str = "vyre-pipeline-cache-norm-v2";
29
30/// Version label for commit/dirty-state source fingerprints.
31pub const SOURCE_FINGERPRINT_VERSION: &str = "vyre-source-fingerprint-v1";
32const MAX_SOURCE_FINGERPRINT_FILE_BYTES: u64 = 64 * 1024 * 1024;
33
34/// Version label for source-tree content fingerprints.
35pub const SOURCE_TREE_FINGERPRINT_VERSION: &str = "source-tree-v1";
36
37/// Version label for dispatch workload/config fingerprints.
38pub const WORKLOAD_FINGERPRINT_VERSION: &str = "vyre-dispatch-workload-v1";
39
40/// Version label for backend environment fingerprints.
41pub const ENVIRONMENT_FINGERPRINT_VERSION: &str = "vyre-evidence-environment-v1";
42
43/// Git and source-tree provenance for evidence-producing runs.
44#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
45pub struct SourceProvenance {
46    /// Raw git facts captured from the source workspace.
47    pub git: BTreeMap<String, String>,
48    /// Commit/dirty-state source identity used by release evidence gates.
49    pub source_fingerprint: String,
50    /// Source-tree content identity used to tolerate evidence-only commit drift.
51    pub source_tree_fingerprint: String,
52}
53
54impl SourceProvenance {
55    /// Capture provenance for the current working directory.
56    #[must_use]
57    pub fn capture_current() -> Self {
58        Self::capture_at(Path::new("."))
59    }
60
61    /// Capture provenance for `workspace_root`.
62    #[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    /// Validate that required provenance fields are non-empty and shaped.
75    ///
76    /// # Errors
77    /// Returns [`BackendError::InvalidProgram`] when an evidence producer
78    /// attempts to emit a weak source identity.
79    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/// Capture git facts for the current working directory.
97#[must_use]
98pub fn capture_git_info() -> BTreeMap<String, String> {
99    capture_git_info_at(Path::new("."))
100}
101
102/// Capture git facts for `workspace_root`.
103#[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/// Build the commit/dirty-state source fingerprint used by release evidence.
148#[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/// Capture a source-tree fingerprint for the current working directory.
170#[must_use]
171pub fn source_tree_fingerprint() -> String {
172    source_tree_fingerprint_at(Path::new("."))
173}
174
175/// Capture a source-tree fingerprint for `workspace_root`.
176#[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/// Timing evidence normalized across host and device timing sources.
372#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
373pub struct DispatchTimingEvidence {
374    /// Host-observed dispatch duration.
375    pub wall_ns: Option<u64>,
376    /// Device-observed elapsed time when available.
377    pub device_ns: Option<u64>,
378    /// Host enqueue duration when available.
379    pub enqueue_ns: Option<u64>,
380    /// Host wait/readback duration when available.
381    pub wait_ns: Option<u64>,
382}
383
384impl DispatchTimingEvidence {
385    /// Build timing evidence from a timed dispatch result.
386    #[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    /// Return true when the evidence has at least one timing source.
397    #[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/// One artifact referenced by an evidence bundle.
407#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
408pub struct EvidenceArtifact {
409    /// Stable artifact kind, such as `pipeline_manifest`, `benchmark_report`, or `replay_capsule`.
410    pub kind: String,
411    /// Backend that produced or owns the artifact when applicable.
412    pub backend_id: Option<String>,
413    /// Relative or consumer-provided artifact path.
414    pub path: Option<String>,
415    /// Content digest or identity digest when available.
416    pub digest: Option<String>,
417}
418
419impl EvidenceArtifact {
420    /// Build an artifact row.
421    #[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    /// Build an artifact row from a compiled-pipeline manifest.
437    #[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/// Replay metadata attached to a dispatch or conformance failure.
449#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
450pub struct ReplayEvidence {
451    /// Human-runnable replay command.
452    pub command: String,
453    /// Capsule digest when the replay payload has been materialized.
454    pub capsule_digest: Option<String>,
455}
456
457impl ReplayEvidence {
458    /// Build replay evidence.
459    #[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/// Versioned digest ledger for every identity lane that participates in
469/// evidence replay, provenance, and cache correlation.
470#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
471pub struct EvidenceDigestLedger {
472    /// Ledger schema version.
473    pub schema: u32,
474    /// Version label for `program_wire_digest`.
475    pub program_wire_version: String,
476    /// BLAKE3 digest of canonical VIR0 Program wire bytes.
477    pub program_wire_digest: String,
478    /// Version label for `normalized_program_digest`.
479    pub normalized_program_version: String,
480    /// Normalized Program digest used by pipeline caches.
481    pub normalized_program_digest: String,
482    /// Version label for `workload_digest`.
483    pub workload_version: String,
484    /// Dispatch workload/config digest.
485    pub workload_digest: String,
486    /// Version label for `source_fingerprint`.
487    pub source_version: String,
488    /// Commit/dirty-state source fingerprint.
489    pub source_fingerprint: String,
490    /// Version label for `source_tree_fingerprint`.
491    pub source_tree_version: String,
492    /// Source-tree content fingerprint.
493    pub source_tree_fingerprint: String,
494    /// Version label for `environment_digest`.
495    pub environment_version: String,
496    /// Backend id/version digest.
497    pub environment_digest: String,
498}
499
500impl EvidenceDigestLedger {
501    /// Current digest-ledger schema.
502    pub const SCHEMA: u32 = 1;
503
504    /// Build the digest ledger from the same inputs used to build an evidence
505    /// bundle.
506    ///
507    /// # Errors
508    ///
509    /// Returns [`BackendError::InvalidProgram`] when the normalized Program
510    /// digest cannot be built.
511    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    /// Validate ledger version labels and digest shapes.
544    ///
545    /// # Errors
546    ///
547    /// Returns [`BackendError::InvalidProgram`] when any ledger lane is missing,
548    /// malformed, or versioned against the wrong contract.
549    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/// Shared evidence bundle for dispatch, benchmark, conformance, and replay surfaces.
630#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
631pub struct EvidenceBundle {
632    /// Bundle schema version.
633    pub schema: u32,
634    /// Backend that produced the result or artifact.
635    pub backend_id: String,
636    /// Backend implementation version.
637    pub backend_version: String,
638    /// Canonical normalized Program digest as lowercase hex.
639    pub program_digest: String,
640    /// Dispatch policy fields that affect generated backend code.
641    pub dispatch_policy: String,
642    /// Versioned digest ledger binding Program, workload, source, and backend
643    /// environment identity.
644    pub digest_ledger: EvidenceDigestLedger,
645    /// Source provenance for the code that produced this evidence.
646    pub source: SourceProvenance,
647    /// Timing evidence for the dispatch or run.
648    pub timing: DispatchTimingEvidence,
649    /// Artifacts referenced by this bundle.
650    pub artifacts: Vec<EvidenceArtifact>,
651    /// Replay metadata when a replay capsule exists.
652    pub replay: Option<ReplayEvidence>,
653}
654
655impl EvidenceBundle {
656    /// Current evidence bundle schema.
657    pub const SCHEMA: u32 = 1;
658
659    /// Build an evidence bundle for a backend/program/config tuple.
660    ///
661    /// # Errors
662    /// Returns [`BackendError`] when the Program cannot be fingerprinted or
663    /// provenance is too weak to emit.
664    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    /// Attach timing from a backend dispatch result.
695    #[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    /// Attach an artifact row.
702    #[must_use]
703    pub fn with_artifact(mut self, artifact: EvidenceArtifact) -> Self {
704        self.artifacts.push(artifact);
705        self
706    }
707
708    /// Attach replay metadata.
709    #[must_use]
710    pub fn with_replay(mut self, replay: ReplayEvidence) -> Self {
711        self.replay = Some(replay);
712        self
713    }
714
715    /// Validate the bundle's load-bearing fields.
716    ///
717    /// # Errors
718    /// Returns [`BackendError::InvalidProgram`] when a bundle is missing a
719    /// required identity field or carries malformed digest metadata.
720    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}