Skip to main content

cbtop/double_blind/
mod.rs

1//! Double-Blind Verification Framework (PMAT-020)
2//!
3//! Implements the Double-Blind Verification protocol per ยง36.2 of cbtop spec.
4//! Separation of Dev (implementation) and QA (verification) roles with
5//! black-box falsification attempts.
6//!
7//! # Protocol
8//!
9//! 1. **Group A (Dev)**: Implements feature and claims "Falsification Passed"
10//! 2. **Group B (QA)**: Receives only binary + F-criteria (no source)
11//! 3. **Blind Test**: Group B attempts to falsify the binary black-box
12//! 4. **Confirmation**: Only if Group B fails to falsify is release approved
13//!
14//! # Citations
15//!
16//! - [Rosenthal & Fode 1963] "Psychology of the Scientist: Experimenter Bias" Psychological Bulletin
17//! - [Holman et al. 2015] "A Systematic Review of Double-Blind Experiments in SE" IEEE TSE
18
19mod types;
20pub use types::*;
21
22/// Double-blind verification session
23#[derive(Debug)]
24pub struct VerificationSession {
25    /// Session ID
26    pub id: String,
27    /// Claims submitted by Dev
28    claims: Vec<FalsificationClaim>,
29    /// Black-box artifacts generated
30    artifacts: Vec<BlackBoxArtifact>,
31    /// Verification attempts by QA
32    attempts: Vec<VerificationAttempt>,
33    /// Scorecard
34    pub scorecard: ScorecardV2,
35    /// Audit trail
36    audit_trail: Vec<AuditEntry>,
37    /// Current state
38    state: SessionState,
39}
40
41impl Default for VerificationSession {
42    fn default() -> Self {
43        Self::new("session-1")
44    }
45}
46
47impl VerificationSession {
48    /// Create a new session
49    pub fn new(id: &str) -> Self {
50        Self {
51            id: id.to_string(),
52            claims: Vec::new(),
53            artifacts: Vec::new(),
54            attempts: Vec::new(),
55            scorecard: ScorecardV2::new(),
56            audit_trail: Vec::new(),
57            state: SessionState::AwaitingClaims,
58        }
59    }
60
61    /// Get current state
62    pub fn state(&self) -> SessionState {
63        self.state
64    }
65
66    /// Submit a claim (Dev role only)
67    pub fn submit_claim(
68        &mut self,
69        role: Role,
70        claim: FalsificationClaim,
71    ) -> Result<(), &'static str> {
72        if !role.can_claim() {
73            return Err("Only Dev role can submit claims");
74        }
75        if !claim.is_valid() {
76            return Err("Invalid claim structure");
77        }
78
79        // Record audit
80        let entry = AuditEntry::new(
81            &format!("audit-{}", self.audit_trail.len()),
82            role,
83            &claim.claimant,
84            &format!("Submitted claim for {}", claim.feature),
85        )
86        .with_artifact(&claim.id);
87        self.audit_trail.push(entry);
88
89        self.claims.push(claim);
90        self.state = SessionState::AwaitingVerification;
91        Ok(())
92    }
93
94    /// Generate black-box artifact from claim
95    pub fn generate_artifact(
96        &mut self,
97        claim_id: &str,
98        binary_hash: &str,
99    ) -> Option<BlackBoxArtifact> {
100        let claim = self.claims.iter().find(|c| c.id == claim_id)?;
101        let artifact = BlackBoxArtifact::from_claim(claim, binary_hash);
102
103        let entry = AuditEntry::new(
104            &format!("audit-{}", self.audit_trail.len()),
105            Role::System,
106            "System",
107            &format!("Generated black-box artifact from claim {}", claim_id),
108        )
109        .with_artifact(&artifact.id);
110        self.audit_trail.push(entry);
111
112        self.artifacts.push(artifact.clone());
113        Some(artifact)
114    }
115
116    /// Submit verification attempt (QA role only)
117    pub fn submit_attempt(
118        &mut self,
119        role: Role,
120        attempt: VerificationAttempt,
121    ) -> Result<(), &'static str> {
122        if !role.can_verify() {
123            return Err("Only QA role can submit verification attempts");
124        }
125
126        // Record audit
127        let entry = AuditEntry::new(
128            &format!("audit-{}", self.audit_trail.len()),
129            role,
130            &attempt.verifier,
131            &format!("Submitted verification attempt: {:?}", attempt.result),
132        )
133        .with_artifact(&attempt.artifact_id);
134        self.audit_trail.push(entry);
135
136        self.attempts.push(attempt);
137        Ok(())
138    }
139
140    /// Get all attempts for an artifact
141    pub fn get_attempts(&self, artifact_id: &str) -> Vec<&VerificationAttempt> {
142        self.attempts
143            .iter()
144            .filter(|a| a.artifact_id == artifact_id)
145            .collect()
146    }
147
148    /// Make release decision (System role only)
149    pub fn make_decision(&mut self, role: Role) -> Result<ReleaseDecision, &'static str> {
150        if !role.can_approve() {
151            return Err("Only System role can make release decisions");
152        }
153
154        if self.attempts.is_empty() {
155            return Ok(ReleaseDecision::Pending {
156                reason: "No verification attempts yet".to_string(),
157            });
158        }
159
160        // Check if any attempt successfully falsified
161        let falsified = self
162            .attempts
163            .iter()
164            .any(|a| a.result == VerificationResult::Falsified);
165
166        // Check if all attempts are inconclusive
167        let all_inconclusive = self
168            .attempts
169            .iter()
170            .all(|a| a.result == VerificationResult::Inconclusive);
171
172        let decision = if falsified {
173            ReleaseDecision::Rejected {
174                reason: "QA successfully falsified the claim".to_string(),
175            }
176        } else if all_inconclusive {
177            ReleaseDecision::Pending {
178                reason: "All verification attempts were inconclusive".to_string(),
179            }
180        } else {
181            // Check scorecard
182            if self.scorecard.passes() {
183                ReleaseDecision::Approved {
184                    reason: format!(
185                        "QA failed to falsify and scorecard passes ({:.1}/100, grade {})",
186                        self.scorecard.total_score(),
187                        self.scorecard.grade()
188                    ),
189                }
190            } else {
191                ReleaseDecision::Rejected {
192                    reason: format!(
193                        "Scorecard fails ({:.1}/100 < 70, grade {})",
194                        self.scorecard.total_score(),
195                        self.scorecard.grade()
196                    ),
197                }
198            }
199        };
200
201        // Record audit
202        let entry = AuditEntry::new(
203            &format!("audit-{}", self.audit_trail.len()),
204            role,
205            "System",
206            &format!("Release decision: {:?}", decision),
207        );
208        self.audit_trail.push(entry);
209
210        self.state = SessionState::Completed;
211        Ok(decision)
212    }
213
214    /// Get audit trail
215    pub fn audit_trail(&self) -> &[AuditEntry] {
216        &self.audit_trail
217    }
218
219    /// Get claim count
220    pub fn claim_count(&self) -> usize {
221        self.claims.len()
222    }
223
224    /// Get attempt count
225    pub fn attempt_count(&self) -> usize {
226        self.attempts.len()
227    }
228
229    /// Generate verification report
230    pub fn generate_report(&self) -> VerificationReport {
231        VerificationReport {
232            session_id: self.id.clone(),
233            total_claims: self.claims.len(),
234            total_artifacts: self.artifacts.len(),
235            total_attempts: self.attempts.len(),
236            falsified_count: self
237                .attempts
238                .iter()
239                .filter(|a| a.result == VerificationResult::Falsified)
240                .count(),
241            unfalsified_count: self
242                .attempts
243                .iter()
244                .filter(|a| a.result == VerificationResult::Unfalsified)
245                .count(),
246            inconclusive_count: self
247                .attempts
248                .iter()
249                .filter(|a| a.result == VerificationResult::Inconclusive)
250                .count(),
251            scorecard_total: self.scorecard.total_score(),
252            scorecard_grade: self.scorecard.grade().to_string(),
253            audit_entries: self.audit_trail.len(),
254        }
255    }
256}
257
258#[cfg(test)]
259mod tests;