Skip to main content

batuta/falsification/
types.rs

1//! Falsification Checklist Types
2//!
3//! Types for representing checklist items, results, and evaluation status.
4
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8
9/// Severity level for checklist items.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11pub enum Severity {
12    /// Project FAIL - blocks release entirely
13    Critical,
14    /// Requires remediation before release
15    Major,
16    /// Documented limitation
17    Minor,
18    /// Clarification needed
19    Info,
20}
21
22impl std::fmt::Display for Severity {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        match self {
25            Severity::Critical => write!(f, "CRITICAL"),
26            Severity::Major => write!(f, "MAJOR"),
27            Severity::Minor => write!(f, "MINOR"),
28            Severity::Info => write!(f, "INFO"),
29        }
30    }
31}
32
33/// Result status for a checklist item.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35pub enum CheckStatus {
36    /// All criteria passed
37    Pass,
38    /// Partial evidence, minor issues
39    Partial,
40    /// Rejection criteria met - claim falsified
41    Fail,
42    /// Check could not be performed
43    Skipped,
44}
45
46impl CheckStatus {
47    /// Get the score contribution for this status.
48    pub fn score(&self) -> f64 {
49        match self {
50            CheckStatus::Pass => 1.0,
51            CheckStatus::Partial => 0.5,
52            CheckStatus::Fail | CheckStatus::Skipped => 0.0,
53        }
54    }
55}
56
57impl std::fmt::Display for CheckStatus {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        match self {
60            CheckStatus::Pass => write!(f, "PASS"),
61            CheckStatus::Partial => write!(f, "PARTIAL"),
62            CheckStatus::Fail => write!(f, "FAIL"),
63            CheckStatus::Skipped => write!(f, "SKIPPED"),
64        }
65    }
66}
67
68/// A single checklist item result.
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct CheckItem {
71    /// Item ID (e.g., "AI-01", "SDG-05")
72    pub id: String,
73
74    /// Human-readable name
75    pub name: String,
76
77    /// The claim being tested
78    pub claim: String,
79
80    /// Severity level
81    pub severity: Severity,
82
83    /// Evaluation status
84    pub status: CheckStatus,
85
86    /// Evidence collected
87    pub evidence: Vec<Evidence>,
88
89    /// Rejection reason (if failed)
90    pub rejection_reason: Option<String>,
91
92    /// TPS principle mapping
93    pub tps_principle: String,
94
95    /// Duration of check in milliseconds
96    pub duration_ms: u64,
97}
98
99impl CheckItem {
100    /// Create a new check item.
101    pub fn new(id: impl Into<String>, name: impl Into<String>, claim: impl Into<String>) -> Self {
102        Self {
103            id: id.into(),
104            name: name.into(),
105            claim: claim.into(),
106            severity: Severity::Major,
107            status: CheckStatus::Skipped,
108            evidence: Vec::new(),
109            rejection_reason: None,
110            tps_principle: String::new(),
111            duration_ms: 0,
112        }
113    }
114
115    /// Set severity level.
116    pub fn with_severity(mut self, severity: Severity) -> Self {
117        self.severity = severity;
118        self
119    }
120
121    /// Set TPS principle.
122    pub fn with_tps(mut self, principle: impl Into<String>) -> Self {
123        self.tps_principle = principle.into();
124        self
125    }
126
127    /// Mark as passed.
128    pub fn pass(mut self) -> Self {
129        self.status = CheckStatus::Pass;
130        self
131    }
132
133    /// Mark as failed with reason.
134    pub fn fail(mut self, reason: impl Into<String>) -> Self {
135        self.status = CheckStatus::Fail;
136        self.rejection_reason = Some(reason.into());
137        self
138    }
139
140    /// Mark as partial.
141    pub fn partial(mut self, reason: impl Into<String>) -> Self {
142        self.status = CheckStatus::Partial;
143        self.rejection_reason = Some(reason.into());
144        self
145    }
146
147    /// Add evidence.
148    pub fn with_evidence(mut self, evidence: Evidence) -> Self {
149        self.evidence.push(evidence);
150        self
151    }
152
153    /// Set duration.
154    pub fn with_duration(mut self, ms: u64) -> Self {
155        self.duration_ms = ms;
156        self
157    }
158
159    /// Record elapsed time from a start instant.
160    pub fn finish_timed(self, start: std::time::Instant) -> Self {
161        let ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
162        self.with_duration(ms)
163    }
164
165    /// Check if this is a critical failure.
166    pub fn is_critical_failure(&self) -> bool {
167        self.severity == Severity::Critical && self.status == CheckStatus::Fail
168    }
169}
170
171/// Evidence collected for a check.
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct Evidence {
174    /// Type of evidence
175    pub evidence_type: EvidenceType,
176
177    /// Description
178    pub description: String,
179
180    /// Raw data (if applicable)
181    pub data: Option<String>,
182
183    /// File paths involved
184    pub files: Vec<PathBuf>,
185}
186
187impl Evidence {
188    /// Create file audit evidence.
189    pub fn file_audit(description: impl Into<String>, files: Vec<PathBuf>) -> Self {
190        Self {
191            evidence_type: EvidenceType::FileAudit,
192            description: description.into(),
193            data: None,
194            files,
195        }
196    }
197
198    /// Create dependency audit evidence.
199    pub fn dependency_audit(description: impl Into<String>, data: impl Into<String>) -> Self {
200        Self {
201            evidence_type: EvidenceType::DependencyAudit,
202            description: description.into(),
203            data: Some(data.into()),
204            files: Vec::new(),
205        }
206    }
207
208    /// Create schema validation evidence.
209    pub fn schema_validation(description: impl Into<String>, data: impl Into<String>) -> Self {
210        Self {
211            evidence_type: EvidenceType::SchemaValidation,
212            description: description.into(),
213            data: Some(data.into()),
214            files: Vec::new(),
215        }
216    }
217
218    /// Create test result evidence.
219    pub fn test_result(description: impl Into<String>, passed: bool) -> Self {
220        Self {
221            evidence_type: EvidenceType::TestResult,
222            description: description.into(),
223            data: Some(if passed { "PASSED" } else { "FAILED" }.to_string()),
224            files: Vec::new(),
225        }
226    }
227}
228
229/// Type of evidence collected.
230#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
231pub enum EvidenceType {
232    /// File existence/content audit
233    FileAudit,
234    /// Dependency tree audit
235    DependencyAudit,
236    /// YAML/config schema validation
237    SchemaValidation,
238    /// Test execution result
239    TestResult,
240    /// Static analysis result
241    StaticAnalysis,
242    /// Coverage measurement
243    Coverage,
244}
245
246/// Complete checklist evaluation result.
247#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct ChecklistResult {
249    /// Project path evaluated
250    pub project_path: PathBuf,
251
252    /// Evaluation timestamp
253    pub timestamp: String,
254
255    /// Results by section
256    pub sections: HashMap<String, Vec<CheckItem>>,
257
258    /// Overall score (0-100)
259    pub score: f64,
260
261    /// TPS assessment grade
262    pub grade: TpsGrade,
263
264    /// Whether any critical items failed
265    pub has_critical_failure: bool,
266
267    /// Total items evaluated
268    pub total_items: usize,
269
270    /// Items passed
271    pub passed_items: usize,
272
273    /// Items failed
274    pub failed_items: usize,
275}
276
277impl ChecklistResult {
278    /// Create a new checklist result.
279    pub fn new(project_path: &Path) -> Self {
280        Self {
281            project_path: project_path.to_path_buf(),
282            timestamp: chrono::Utc::now().to_rfc3339(),
283            sections: HashMap::new(),
284            score: 0.0,
285            grade: TpsGrade::StopTheLine,
286            has_critical_failure: false,
287            total_items: 0,
288            passed_items: 0,
289            failed_items: 0,
290        }
291    }
292
293    /// Add a section of results.
294    pub fn add_section(&mut self, name: impl Into<String>, items: Vec<CheckItem>) {
295        self.sections.insert(name.into(), items);
296    }
297
298    /// Finalize the result, calculating scores.
299    pub fn finalize(&mut self) {
300        let mut total_score = 0.0;
301        let mut total_items = 0;
302        let mut passed = 0;
303        let mut failed = 0;
304        let mut has_critical = false;
305
306        for items in self.sections.values() {
307            for item in items {
308                total_items += 1;
309                total_score += item.status.score();
310
311                match item.status {
312                    CheckStatus::Pass => passed += 1,
313                    CheckStatus::Fail => {
314                        failed += 1;
315                        if item.severity == Severity::Critical {
316                            has_critical = true;
317                        }
318                    }
319                    CheckStatus::Partial => {}
320                    CheckStatus::Skipped => {}
321                }
322            }
323        }
324
325        self.total_items = total_items;
326        self.passed_items = passed;
327        self.failed_items = failed;
328        self.has_critical_failure = has_critical;
329
330        if total_items > 0 {
331            self.score = (total_score / total_items as f64) * 100.0;
332        }
333
334        self.grade = TpsGrade::from_score(self.score, has_critical);
335    }
336
337    /// Check if the project passes.
338    pub fn passes(&self) -> bool {
339        !self.has_critical_failure && self.grade.passes()
340    }
341
342    /// Get summary string.
343    pub fn summary(&self) -> String {
344        format!(
345            "{}: {:.1}% ({}/{} passed) - {}",
346            self.grade,
347            self.score,
348            self.passed_items,
349            self.total_items,
350            if self.passes() { "RELEASE OK" } else { "BLOCKED" }
351        )
352    }
353}
354
355/// TPS-aligned assessment grade.
356#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
357pub enum TpsGrade {
358    /// 95-100%: "Good Thinking, Good Products" - Release
359    ToyotaStandard,
360    /// 85-94%: Beta/Preview with documented issues
361    KaizenRequired,
362    /// 70-84%: Significant revision, no release
363    AndonWarning,
364    /// <70% or critical failure: Major rework, halt development
365    StopTheLine,
366}
367
368impl TpsGrade {
369    /// Determine grade from score and critical failure status.
370    pub fn from_score(score: f64, has_critical_failure: bool) -> Self {
371        if has_critical_failure {
372            return TpsGrade::StopTheLine;
373        }
374
375        if score >= 95.0 {
376            TpsGrade::ToyotaStandard
377        } else if score >= 85.0 {
378            TpsGrade::KaizenRequired
379        } else if score >= 70.0 {
380            TpsGrade::AndonWarning
381        } else {
382            TpsGrade::StopTheLine
383        }
384    }
385
386    /// Check if this grade allows release.
387    pub fn passes(&self) -> bool {
388        matches!(self, TpsGrade::ToyotaStandard | TpsGrade::KaizenRequired)
389    }
390}
391
392impl std::fmt::Display for TpsGrade {
393    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
394        match self {
395            TpsGrade::ToyotaStandard => write!(f, "Toyota Standard"),
396            TpsGrade::KaizenRequired => write!(f, "Kaizen Required"),
397            TpsGrade::AndonWarning => write!(f, "Andon Warning"),
398            TpsGrade::StopTheLine => write!(f, "STOP THE LINE"),
399        }
400    }
401}
402
403#[cfg(test)]
404mod tests {
405    use super::*;
406
407    // =========================================================================
408    // FALS-TYP-001: Severity Display
409    // =========================================================================
410
411    #[test]
412    fn test_fals_typ_001_severity_display() {
413        assert_eq!(format!("{}", Severity::Critical), "CRITICAL");
414        assert_eq!(format!("{}", Severity::Major), "MAJOR");
415        assert_eq!(format!("{}", Severity::Minor), "MINOR");
416        assert_eq!(format!("{}", Severity::Info), "INFO");
417    }
418
419    // =========================================================================
420    // FALS-TYP-002: CheckStatus Scoring
421    // =========================================================================
422
423    #[test]
424    fn test_fals_typ_002_check_status_scores() {
425        assert_eq!(CheckStatus::Pass.score(), 1.0);
426        assert_eq!(CheckStatus::Partial.score(), 0.5);
427        assert_eq!(CheckStatus::Fail.score(), 0.0);
428        assert_eq!(CheckStatus::Skipped.score(), 0.0);
429    }
430
431    // =========================================================================
432    // FALS-TYP-003: CheckItem Builder
433    // =========================================================================
434
435    #[test]
436    fn test_fals_typ_003_check_item_builder() {
437        let item = CheckItem::new("AI-01", "Declarative YAML", "Project offers YAML config")
438            .with_severity(Severity::Critical)
439            .with_tps("Poka-Yoke")
440            .pass();
441
442        assert_eq!(item.id, "AI-01");
443        assert_eq!(item.severity, Severity::Critical);
444        assert_eq!(item.status, CheckStatus::Pass);
445        assert_eq!(item.tps_principle, "Poka-Yoke");
446    }
447
448    #[test]
449    fn test_fals_typ_003_check_item_fail() {
450        let item = CheckItem::new("AI-02", "Zero Scripting", "No Python/JS")
451            .with_severity(Severity::Critical)
452            .fail("Found .py files in src/");
453
454        assert_eq!(item.status, CheckStatus::Fail);
455        assert!(item.rejection_reason.is_some());
456        assert!(item.is_critical_failure());
457    }
458
459    // =========================================================================
460    // FALS-TYP-004: Evidence Types
461    // =========================================================================
462
463    #[test]
464    fn test_fals_typ_004_evidence_file_audit() {
465        let evidence =
466            Evidence::file_audit("Found 3 Python files", vec![PathBuf::from("src/main.py")]);
467
468        assert_eq!(evidence.evidence_type, EvidenceType::FileAudit);
469        assert_eq!(evidence.files.len(), 1);
470    }
471
472    #[test]
473    fn test_fals_typ_004_evidence_dependency_audit() {
474        let evidence = Evidence::dependency_audit("No pyo3 found", "cargo tree output");
475
476        assert_eq!(evidence.evidence_type, EvidenceType::DependencyAudit);
477        assert!(evidence.data.is_some());
478    }
479
480    // =========================================================================
481    // FALS-TYP-005: TpsGrade Determination
482    // =========================================================================
483
484    #[test]
485    fn test_fals_typ_005_tps_grade_toyota_standard() {
486        let grade = TpsGrade::from_score(95.0, false);
487        assert_eq!(grade, TpsGrade::ToyotaStandard);
488        assert!(grade.passes());
489    }
490
491    #[test]
492    fn test_fals_typ_005_tps_grade_kaizen() {
493        let grade = TpsGrade::from_score(90.0, false);
494        assert_eq!(grade, TpsGrade::KaizenRequired);
495        assert!(grade.passes());
496    }
497
498    #[test]
499    fn test_fals_typ_005_tps_grade_andon() {
500        let grade = TpsGrade::from_score(75.0, false);
501        assert_eq!(grade, TpsGrade::AndonWarning);
502        assert!(!grade.passes());
503    }
504
505    #[test]
506    fn test_fals_typ_005_tps_grade_stop_line() {
507        let grade = TpsGrade::from_score(50.0, false);
508        assert_eq!(grade, TpsGrade::StopTheLine);
509        assert!(!grade.passes());
510    }
511
512    #[test]
513    fn test_fals_typ_005_critical_failure_stops_line() {
514        // Even with 100% score, critical failure = stop
515        let grade = TpsGrade::from_score(100.0, true);
516        assert_eq!(grade, TpsGrade::StopTheLine);
517        assert!(!grade.passes());
518    }
519
520    // =========================================================================
521    // FALS-TYP-006: ChecklistResult Finalization
522    // =========================================================================
523
524    #[test]
525    fn test_fals_typ_006_checklist_result_finalize() {
526        let mut result = ChecklistResult::new(Path::new("."));
527
528        let items = vec![
529            CheckItem::new("T-01", "Test 1", "Claim 1").pass(),
530            CheckItem::new("T-02", "Test 2", "Claim 2").pass(),
531            CheckItem::new("T-03", "Test 3", "Claim 3").fail("Failed"),
532        ];
533
534        result.add_section("Test Section", items);
535        result.finalize();
536
537        assert_eq!(result.total_items, 3);
538        assert_eq!(result.passed_items, 2);
539        assert_eq!(result.failed_items, 1);
540        // (1.0 + 1.0 + 0.0) / 3 * 100 = 66.67%
541        assert!((result.score - 66.67).abs() < 1.0);
542    }
543
544    #[test]
545    fn test_fals_typ_006_critical_failure_detection() {
546        let mut result = ChecklistResult::new(Path::new("."));
547
548        let items = vec![CheckItem::new("AI-01", "Test", "Claim")
549            .with_severity(Severity::Critical)
550            .fail("Critical failure")];
551
552        result.add_section("Critical", items);
553        result.finalize();
554
555        assert!(result.has_critical_failure);
556        assert!(!result.passes());
557        assert_eq!(result.grade, TpsGrade::StopTheLine);
558    }
559
560    // =========================================================================
561    // Additional coverage tests
562    // =========================================================================
563
564    #[test]
565    fn test_check_status_display() {
566        assert_eq!(format!("{}", CheckStatus::Pass), "PASS");
567        assert_eq!(format!("{}", CheckStatus::Partial), "PARTIAL");
568        assert_eq!(format!("{}", CheckStatus::Fail), "FAIL");
569        assert_eq!(format!("{}", CheckStatus::Skipped), "SKIPPED");
570    }
571
572    #[test]
573    fn test_check_item_partial() {
574        let item = CheckItem::new("T-01", "Test", "Claim").partial("Missing docs");
575
576        assert_eq!(item.status, CheckStatus::Partial);
577        assert_eq!(item.rejection_reason, Some("Missing docs".to_string()));
578    }
579
580    #[test]
581    fn test_check_item_with_evidence() {
582        let evidence = Evidence::file_audit("Found file", vec![PathBuf::from("test.rs")]);
583        let item = CheckItem::new("T-01", "Test", "Claim").with_evidence(evidence);
584
585        assert_eq!(item.evidence.len(), 1);
586        assert_eq!(item.evidence[0].evidence_type, EvidenceType::FileAudit);
587    }
588
589    #[test]
590    fn test_check_item_with_duration() {
591        let item = CheckItem::new("T-01", "Test", "Claim").with_duration(150);
592
593        assert_eq!(item.duration_ms, 150);
594    }
595
596    #[test]
597    fn test_check_item_is_not_critical_failure() {
598        let item = CheckItem::new("T-01", "Test", "Claim")
599            .with_severity(Severity::Minor)
600            .fail("Minor issue");
601
602        assert!(!item.is_critical_failure());
603    }
604
605    #[test]
606    fn test_evidence_schema_validation() {
607        let evidence = Evidence::schema_validation("Config valid", "schema: valid");
608
609        assert_eq!(evidence.evidence_type, EvidenceType::SchemaValidation);
610        assert_eq!(evidence.description, "Config valid");
611        assert_eq!(evidence.data, Some("schema: valid".to_string()));
612    }
613
614    #[test]
615    fn test_evidence_test_result_passed() {
616        let evidence = Evidence::test_result("Unit tests", true);
617
618        assert_eq!(evidence.evidence_type, EvidenceType::TestResult);
619        assert_eq!(evidence.data, Some("PASSED".to_string()));
620    }
621
622    #[test]
623    fn test_evidence_test_result_failed() {
624        let evidence = Evidence::test_result("Integration tests", false);
625
626        assert_eq!(evidence.evidence_type, EvidenceType::TestResult);
627        assert_eq!(evidence.data, Some("FAILED".to_string()));
628    }
629
630    #[test]
631    fn test_checklist_result_summary() {
632        let mut result = ChecklistResult::new(Path::new("/test"));
633        result.add_section(
634            "section",
635            vec![
636                CheckItem::new("T-01", "Test 1", "Claim 1").pass(),
637                CheckItem::new("T-02", "Test 2", "Claim 2").pass(),
638            ],
639        );
640        result.finalize();
641
642        let summary = result.summary();
643        assert!(summary.contains("Toyota Standard"));
644        assert!(summary.contains("2/2"));
645        assert!(summary.contains("RELEASE OK"));
646    }
647
648    #[test]
649    fn test_checklist_result_summary_blocked() {
650        let mut result = ChecklistResult::new(Path::new("/test"));
651        result.add_section(
652            "section",
653            vec![
654                CheckItem::new("T-01", "Test 1", "Claim 1").fail("Failed"),
655                CheckItem::new("T-02", "Test 2", "Claim 2").fail("Failed"),
656            ],
657        );
658        result.finalize();
659
660        let summary = result.summary();
661        assert!(summary.contains("BLOCKED"));
662    }
663
664    #[test]
665    fn test_tps_grade_display() {
666        assert_eq!(format!("{}", TpsGrade::ToyotaStandard), "Toyota Standard");
667        assert_eq!(format!("{}", TpsGrade::KaizenRequired), "Kaizen Required");
668        assert_eq!(format!("{}", TpsGrade::AndonWarning), "Andon Warning");
669        assert_eq!(format!("{}", TpsGrade::StopTheLine), "STOP THE LINE");
670    }
671
672    #[test]
673    fn test_severity_equality() {
674        assert_eq!(Severity::Critical, Severity::Critical);
675        assert_ne!(Severity::Critical, Severity::Major);
676        assert_ne!(Severity::Major, Severity::Minor);
677        assert_ne!(Severity::Minor, Severity::Info);
678    }
679
680    #[test]
681    fn test_check_status_equality() {
682        assert_eq!(CheckStatus::Pass, CheckStatus::Pass);
683        assert_ne!(CheckStatus::Pass, CheckStatus::Fail);
684    }
685
686    #[test]
687    fn test_evidence_type_equality() {
688        assert_eq!(EvidenceType::FileAudit, EvidenceType::FileAudit);
689        assert_ne!(EvidenceType::FileAudit, EvidenceType::TestResult);
690    }
691
692    #[test]
693    fn test_checklist_result_empty() {
694        let mut result = ChecklistResult::new(Path::new("."));
695        result.finalize();
696
697        assert_eq!(result.total_items, 0);
698        assert_eq!(result.score, 0.0);
699        assert!(!result.has_critical_failure);
700    }
701
702    #[test]
703    fn test_check_item_default_values() {
704        let item = CheckItem::new("ID", "Name", "Claim");
705
706        assert_eq!(item.severity, Severity::Major);
707        assert_eq!(item.status, CheckStatus::Skipped);
708        assert!(item.evidence.is_empty());
709        assert!(item.rejection_reason.is_none());
710        assert!(item.tps_principle.is_empty());
711        assert_eq!(item.duration_ms, 0);
712    }
713
714    // =========================================================================
715    // Coverage gap: finish_timed
716    // =========================================================================
717
718    #[test]
719    fn test_check_item_finish_timed() {
720        let start = std::time::Instant::now();
721        // Small delay to ensure non-zero duration
722        std::thread::sleep(std::time::Duration::from_millis(1));
723        let item = CheckItem::new("T-01", "Timed", "Timed claim").finish_timed(start);
724        assert!(item.duration_ms >= 1, "Duration should be at least 1ms");
725    }
726
727    // =========================================================================
728    // Coverage gap: finalize with Skipped + Partial items (line 319-320)
729    // =========================================================================
730
731    #[test]
732    fn test_checklist_result_finalize_with_all_statuses() {
733        let mut result = ChecklistResult::new(Path::new("/test"));
734
735        let items = vec![
736            CheckItem::new("P-01", "Pass", "Pass claim").pass(),
737            CheckItem::new("F-01", "Fail", "Fail claim")
738                .with_severity(Severity::Major)
739                .fail("Failed"),
740            CheckItem::new("PT-01", "Partial", "Partial claim").partial("Partial reason"),
741            CheckItem::new("S-01", "Skipped", "Skipped claim"),
742        ];
743
744        result.add_section("Mixed", items);
745        result.finalize();
746
747        assert_eq!(result.total_items, 4);
748        assert_eq!(result.passed_items, 1);
749        assert_eq!(result.failed_items, 1);
750        // Score: (1.0 + 0.0 + 0.5 + 0.0) / 4 * 100 = 37.5%
751        assert!((result.score - 37.5).abs() < 0.1);
752        // Major failure (not critical) — has_critical_failure should be false
753        assert!(!result.has_critical_failure);
754    }
755
756    // =========================================================================
757    // Coverage gap: passes() with critical failure + high score
758    // =========================================================================
759
760    #[test]
761    fn test_checklist_result_passes_critical_blocks() {
762        let mut result = ChecklistResult::new(Path::new("/test"));
763
764        // All pass except one critical failure
765        let items = vec![
766            CheckItem::new("P-01", "Pass 1", "Claim 1").pass(),
767            CheckItem::new("P-02", "Pass 2", "Claim 2").pass(),
768            CheckItem::new("P-03", "Pass 3", "Claim 3").pass(),
769            CheckItem::new("P-04", "Pass 4", "Claim 4").pass(),
770            CheckItem::new("C-01", "Critical Fail", "Critical claim")
771                .with_severity(Severity::Critical)
772                .fail("Critical issue"),
773        ];
774
775        result.add_section("Test", items);
776        result.finalize();
777
778        // Score is 80% but critical failure blocks
779        assert!(result.has_critical_failure);
780        assert!(!result.passes());
781    }
782}