Skip to main content

effect_signature/
lib.rs

1//! # effect-signature
2//!
3//! Stable effect payloads and hashing helpers for check outputs.
4//!
5//! This crate owns the normalized signature types that let execution results be
6//! compared, attributed, and persisted across the Forge lane. It does not own
7//! check execution or causal graph policy.
8
9//! # effect-signature
10//!
11//! Shared effect identity types for internal verification flows.
12//!
13//! This Tier 3 crate keeps one narrow schema for observed check effects so
14//! runners, attribution, and storage code do not fork their own effect models.
15
16use std::path::PathBuf;
17
18#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
19pub struct EffectSignature {
20    pub check_kind: String,
21    pub outcome: String,
22    pub severity: String,
23    pub message_class: String,
24    pub line_offset_from_edit: Option<i32>,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
28pub struct LocatedEffect {
29    pub file: Option<PathBuf>,
30    pub line: Option<u32>,
31    pub col: Option<u32>,
32    pub message: String,
33    pub sig: EffectSignature,
34}
35
36pub fn effect_signature_hash(signature: &EffectSignature) -> String {
37    let payload = serde_json::to_vec(signature).unwrap_or_default();
38    blake3::hash(&payload).to_hex().to_string()
39}
40
41#[cfg(test)]
42mod wave1_tests {
43    use super::*;
44
45    fn sample_signature() -> EffectSignature {
46        EffectSignature {
47            check_kind: "clippy".into(),
48            outcome: "fail".into(),
49            severity: "warning".into(),
50            message_class: "unused_variable".into(),
51            line_offset_from_edit: Some(2),
52        }
53    }
54
55    #[test]
56    fn effect_signature_hash_is_deterministic_and_sensitive_to_fields() {
57        let baseline = sample_signature();
58        let same_hash = effect_signature_hash(&baseline);
59        assert_eq!(same_hash, effect_signature_hash(&baseline));
60
61        let mut changed = baseline.clone();
62        changed.message_class = "different".into();
63        assert_ne!(same_hash, effect_signature_hash(&changed));
64    }
65
66    #[test]
67    fn located_effect_round_trips_through_json() {
68        let located = LocatedEffect {
69            file: Some(PathBuf::from("src/lib.rs")),
70            line: Some(12),
71            col: Some(4),
72            message: "unused variable".into(),
73            sig: sample_signature(),
74        };
75
76        let encoded = serde_json::to_string(&located).unwrap();
77        let decoded: LocatedEffect = serde_json::from_str(&encoded).unwrap();
78        assert_eq!(decoded, located);
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    fn sample_signature() -> EffectSignature {
87        EffectSignature {
88            check_kind: "clippy".into(),
89            outcome: "warning".into(),
90            severity: "medium".into(),
91            message_class: "lint".into(),
92            line_offset_from_edit: Some(2),
93        }
94    }
95
96    #[test]
97    fn located_effect_round_trips_through_json() {
98        let effect = LocatedEffect {
99            file: Some("src/lib.rs".into()),
100            line: Some(12),
101            col: Some(4),
102            message: "unused variable".into(),
103            sig: sample_signature(),
104        };
105
106        let encoded = serde_json::to_string(&effect).unwrap();
107        let decoded: LocatedEffect = serde_json::from_str(&encoded).unwrap();
108
109        assert_eq!(decoded, effect);
110    }
111
112    #[test]
113    fn hash_is_stable_for_identical_signatures() {
114        let left = sample_signature();
115        let right = sample_signature();
116
117        assert_eq!(effect_signature_hash(&left), effect_signature_hash(&right));
118    }
119
120    #[test]
121    fn hash_changes_when_signature_changes() {
122        let baseline = sample_signature();
123        let mut changed = sample_signature();
124        changed.message_class = "type_mismatch".into();
125
126        assert_ne!(
127            effect_signature_hash(&baseline),
128            effect_signature_hash(&changed)
129        );
130    }
131}