1use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11pub enum Severity {
12 Critical,
14 Major,
16 Minor,
18 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35pub enum CheckStatus {
36 Pass,
38 Partial,
40 Fail,
42 Skipped,
44}
45
46impl CheckStatus {
47 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#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct CheckItem {
71 pub id: String,
73
74 pub name: String,
76
77 pub claim: String,
79
80 pub severity: Severity,
82
83 pub status: CheckStatus,
85
86 pub evidence: Vec<Evidence>,
88
89 pub rejection_reason: Option<String>,
91
92 pub tps_principle: String,
94
95 pub duration_ms: u64,
97}
98
99impl CheckItem {
100 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 pub fn with_severity(mut self, severity: Severity) -> Self {
117 self.severity = severity;
118 self
119 }
120
121 pub fn with_tps(mut self, principle: impl Into<String>) -> Self {
123 self.tps_principle = principle.into();
124 self
125 }
126
127 pub fn pass(mut self) -> Self {
129 self.status = CheckStatus::Pass;
130 self
131 }
132
133 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 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 pub fn with_evidence(mut self, evidence: Evidence) -> Self {
149 self.evidence.push(evidence);
150 self
151 }
152
153 pub fn with_duration(mut self, ms: u64) -> Self {
155 self.duration_ms = ms;
156 self
157 }
158
159 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 pub fn is_critical_failure(&self) -> bool {
167 self.severity == Severity::Critical && self.status == CheckStatus::Fail
168 }
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct Evidence {
174 pub evidence_type: EvidenceType,
176
177 pub description: String,
179
180 pub data: Option<String>,
182
183 pub files: Vec<PathBuf>,
185}
186
187impl Evidence {
188 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
231pub enum EvidenceType {
232 FileAudit,
234 DependencyAudit,
236 SchemaValidation,
238 TestResult,
240 StaticAnalysis,
242 Coverage,
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct ChecklistResult {
249 pub project_path: PathBuf,
251
252 pub timestamp: String,
254
255 pub sections: HashMap<String, Vec<CheckItem>>,
257
258 pub score: f64,
260
261 pub grade: TpsGrade,
263
264 pub has_critical_failure: bool,
266
267 pub total_items: usize,
269
270 pub passed_items: usize,
272
273 pub failed_items: usize,
275}
276
277impl ChecklistResult {
278 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 pub fn add_section(&mut self, name: impl Into<String>, items: Vec<CheckItem>) {
295 self.sections.insert(name.into(), items);
296 }
297
298 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 pub fn passes(&self) -> bool {
339 !self.has_critical_failure && self.grade.passes()
340 }
341
342 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
357pub enum TpsGrade {
358 ToyotaStandard,
360 KaizenRequired,
362 AndonWarning,
364 StopTheLine,
366}
367
368impl TpsGrade {
369 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 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 #[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 #[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 #[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 #[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 #[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 let grade = TpsGrade::from_score(100.0, true);
516 assert_eq!(grade, TpsGrade::StopTheLine);
517 assert!(!grade.passes());
518 }
519
520 #[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 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 #[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 #[test]
719 fn test_check_item_finish_timed() {
720 let start = std::time::Instant::now();
721 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 #[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 assert!((result.score - 37.5).abs() < 0.1);
752 assert!(!result.has_critical_failure);
754 }
755
756 #[test]
761 fn test_checklist_result_passes_critical_blocks() {
762 let mut result = ChecklistResult::new(Path::new("/test"));
763
764 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 assert!(result.has_critical_failure);
780 assert!(!result.passes());
781 }
782}