cbtop/double_blind/
mod.rs1mod types;
20pub use types::*;
21
22#[derive(Debug)]
24pub struct VerificationSession {
25 pub id: String,
27 claims: Vec<FalsificationClaim>,
29 artifacts: Vec<BlackBoxArtifact>,
31 attempts: Vec<VerificationAttempt>,
33 pub scorecard: ScorecardV2,
35 audit_trail: Vec<AuditEntry>,
37 state: SessionState,
39}
40
41impl Default for VerificationSession {
42 fn default() -> Self {
43 Self::new("session-1")
44 }
45}
46
47impl VerificationSession {
48 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 pub fn state(&self) -> SessionState {
63 self.state
64 }
65
66 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 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 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 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 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 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 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 let falsified = self
162 .attempts
163 .iter()
164 .any(|a| a.result == VerificationResult::Falsified);
165
166 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 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 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 pub fn audit_trail(&self) -> &[AuditEntry] {
216 &self.audit_trail
217 }
218
219 pub fn claim_count(&self) -> usize {
221 self.claims.len()
222 }
223
224 pub fn attempt_count(&self) -> usize {
226 self.attempts.len()
227 }
228
229 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;