1use std::fmt;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
16pub enum QAGrade {
17 F,
19 C,
21 B,
23 BPlus,
25 A,
27 APlus,
29}
30
31impl QAGrade {
32 #[must_use]
34 pub const fn from_score(score: u8) -> Self {
35 match score {
36 95..=100 => Self::APlus,
37 90..=94 => Self::A,
38 85..=89 => Self::BPlus,
39 80..=84 => Self::B,
40 70..=79 => Self::C,
41 0..=69 => Self::F,
42 101.. => Self::F,
43 }
44 }
45
46 #[must_use]
48 pub const fn is_passing(&self) -> bool {
49 !matches!(self, Self::F)
50 }
51}
52
53impl fmt::Display for QAGrade {
54 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55 match self {
56 Self::APlus => write!(f, "A+ (Excellent)"),
57 Self::A => write!(f, "A (Very Good)"),
58 Self::BPlus => write!(f, "B+ (Good)"),
59 Self::B => write!(f, "B (Satisfactory)"),
60 Self::C => write!(f, "C (Needs Improvement)"),
61 Self::F => write!(f, "F (Failing)"),
62 }
63 }
64}
65
66#[allow(clippy::struct_excessive_bools)]
72#[derive(Debug, Clone, Default)]
73pub struct PopperianQA {
74 pub r1_same_loss_curve: bool,
78 pub r2_same_final_weights: bool,
80 pub r3_same_eval_metrics: bool,
82 pub r4_environment_locked: bool,
84
85 pub c1_parses_as_rust: bool,
89 pub c2_type_checks: bool,
91 pub c3_no_unused_warnings: bool,
93 pub c4_links_correctly: bool,
95
96 pub x1_tests_pass_on_correct: bool,
100 pub x2_tests_fail_on_mutant: bool,
102 pub x3_assertions_meaningful: bool,
104 pub x4_no_tautologies: bool,
106
107 pub v1_branch_coverage_delta: bool,
111 pub v2_line_coverage_delta: bool,
113 pub v3_edge_cases_present: bool,
115
116 pub e1_vram_under_8gb: bool,
120 pub e2_training_under_4hrs: bool,
122 pub e3_inference_under_1s: bool,
124
125 pub g1_handles_generics: bool,
129 pub g2_handles_lifetimes: bool,
131 pub g3_handles_async: bool,
133 pub g4_handles_unsafe: bool,
135 pub g5_handles_macros: bool,
137
138 pub d1_test_names_descriptive: bool,
142 pub d2_comments_present: bool,
144 pub d3_proptest_strategies_clear: bool,
146}
147
148impl PopperianQA {
149 #[must_use]
151 pub const fn new() -> Self {
152 Self {
153 r1_same_loss_curve: false,
154 r2_same_final_weights: false,
155 r3_same_eval_metrics: false,
156 r4_environment_locked: false,
157 c1_parses_as_rust: false,
158 c2_type_checks: false,
159 c3_no_unused_warnings: false,
160 c4_links_correctly: false,
161 x1_tests_pass_on_correct: false,
162 x2_tests_fail_on_mutant: false,
163 x3_assertions_meaningful: false,
164 x4_no_tautologies: false,
165 v1_branch_coverage_delta: false,
166 v2_line_coverage_delta: false,
167 v3_edge_cases_present: false,
168 e1_vram_under_8gb: false,
169 e2_training_under_4hrs: false,
170 e3_inference_under_1s: false,
171 g1_handles_generics: false,
172 g2_handles_lifetimes: false,
173 g3_handles_async: false,
174 g4_handles_unsafe: false,
175 g5_handles_macros: false,
176 d1_test_names_descriptive: false,
177 d2_comments_present: false,
178 d3_proptest_strategies_clear: false,
179 }
180 }
181
182 #[must_use]
184 pub fn score(&self) -> u8 {
185 let weighted: &[(bool, u8)] = &[
186 (self.r1_same_loss_curve, 5),
188 (self.r2_same_final_weights, 5),
189 (self.r3_same_eval_metrics, 5),
190 (self.r4_environment_locked, 5),
191 (self.c1_parses_as_rust, 5),
193 (self.c2_type_checks, 5),
194 (self.c3_no_unused_warnings, 5),
195 (self.c4_links_correctly, 5),
196 (self.x1_tests_pass_on_correct, 5),
198 (self.x2_tests_fail_on_mutant, 5),
199 (self.x3_assertions_meaningful, 5),
200 (self.x4_no_tautologies, 5),
201 (self.v1_branch_coverage_delta, 5),
203 (self.v2_line_coverage_delta, 5),
204 (self.v3_edge_cases_present, 5),
205 (self.e1_vram_under_8gb, 3),
207 (self.e2_training_under_4hrs, 4),
208 (self.e3_inference_under_1s, 3),
209 (self.g1_handles_generics, 2),
211 (self.g2_handles_lifetimes, 2),
212 (self.g3_handles_async, 2),
213 (self.g4_handles_unsafe, 2),
214 (self.g5_handles_macros, 2),
215 (self.d1_test_names_descriptive, 2),
217 (self.d2_comments_present, 2),
218 (self.d3_proptest_strategies_clear, 1),
219 ];
220 weighted.iter().filter(|(passed, _)| *passed).map(|(_, pts)| pts).sum()
221 }
222
223 #[must_use]
225 pub fn grade(&self) -> QAGrade {
226 QAGrade::from_score(self.score())
227 }
228
229 #[must_use]
231 pub const fn reproducibility_passed(&self) -> bool {
232 self.r1_same_loss_curve
233 && self.r2_same_final_weights
234 && self.r3_same_eval_metrics
235 && self.r4_environment_locked
236 }
237
238 #[must_use]
240 pub const fn compilation_passed(&self) -> bool {
241 self.c1_parses_as_rust
242 && self.c2_type_checks
243 && self.c3_no_unused_warnings
244 && self.c4_links_correctly
245 }
246
247 #[must_use]
249 pub const fn correctness_passed(&self) -> bool {
250 self.x1_tests_pass_on_correct
251 && self.x2_tests_fail_on_mutant
252 && self.x3_assertions_meaningful
253 && self.x4_no_tautologies
254 }
255
256 #[must_use]
258 pub fn passed_count(&self) -> usize {
259 let bools = [
260 self.r1_same_loss_curve,
261 self.r2_same_final_weights,
262 self.r3_same_eval_metrics,
263 self.r4_environment_locked,
264 self.c1_parses_as_rust,
265 self.c2_type_checks,
266 self.c3_no_unused_warnings,
267 self.c4_links_correctly,
268 self.x1_tests_pass_on_correct,
269 self.x2_tests_fail_on_mutant,
270 self.x3_assertions_meaningful,
271 self.x4_no_tautologies,
272 self.v1_branch_coverage_delta,
273 self.v2_line_coverage_delta,
274 self.v3_edge_cases_present,
275 self.e1_vram_under_8gb,
276 self.e2_training_under_4hrs,
277 self.e3_inference_under_1s,
278 self.g1_handles_generics,
279 self.g2_handles_lifetimes,
280 self.g3_handles_async,
281 self.g4_handles_unsafe,
282 self.g5_handles_macros,
283 self.d1_test_names_descriptive,
284 self.d2_comments_present,
285 self.d3_proptest_strategies_clear,
286 ];
287 bools.iter().filter(|&&b| b).count()
288 }
289
290 #[must_use]
292 pub const fn total_items(&self) -> usize {
293 26
294 }
295
296 #[must_use]
298 pub fn report(&self) -> String {
299 let mut out = String::new();
300 report_header(&mut out, self);
301 report_section(&mut out, "## Reproducibility (20 pts)\n", &self.reproducibility_items());
302 report_section(&mut out, "\n## Compilation (20 pts)\n", &self.compilation_items());
303 report_section(&mut out, "\n## Correctness (20 pts)\n", &self.correctness_items());
304 report_section(&mut out, "\n## Coverage (15 pts)\n", &self.coverage_items());
305 report_section(&mut out, "\n## Efficiency (10 pts)\n", &self.efficiency_items());
306 report_section(&mut out, "\n## Edge Cases (10 pts)\n", &self.edge_case_items());
307 report_section(&mut out, "\n## Documentation (5 pts)\n", &self.documentation_items());
308 out
309 }
310
311 fn reproducibility_items(&self) -> Vec<(bool, &'static str)> {
312 vec![
313 (self.r1_same_loss_curve, "R1: Same loss curve"),
314 (self.r2_same_final_weights, "R2: Same final weights"),
315 (self.r3_same_eval_metrics, "R3: Same eval metrics"),
316 (self.r4_environment_locked, "R4: Environment locked"),
317 ]
318 }
319
320 fn compilation_items(&self) -> Vec<(bool, &'static str)> {
321 vec![
322 (self.c1_parses_as_rust, "C1: Parses as Rust"),
323 (self.c2_type_checks, "C2: Type checks"),
324 (self.c3_no_unused_warnings, "C3: No unused warnings"),
325 (self.c4_links_correctly, "C4: Links correctly"),
326 ]
327 }
328
329 fn correctness_items(&self) -> Vec<(bool, &'static str)> {
330 vec![
331 (self.x1_tests_pass_on_correct, "X1: Tests pass on correct"),
332 (self.x2_tests_fail_on_mutant, "X2: Tests fail on mutant"),
333 (self.x3_assertions_meaningful, "X3: Assertions meaningful"),
334 (self.x4_no_tautologies, "X4: No tautologies"),
335 ]
336 }
337
338 fn coverage_items(&self) -> Vec<(bool, &'static str)> {
339 vec![
340 (self.v1_branch_coverage_delta, "V1: Branch coverage +5%"),
341 (self.v2_line_coverage_delta, "V2: Line coverage +10%"),
342 (self.v3_edge_cases_present, "V3: Edge cases present"),
343 ]
344 }
345
346 fn efficiency_items(&self) -> Vec<(bool, &'static str)> {
347 vec![
348 (self.e1_vram_under_8gb, "E1: VRAM < 8GB"),
349 (self.e2_training_under_4hrs, "E2: Training < 4hrs"),
350 (self.e3_inference_under_1s, "E3: Inference < 1s"),
351 ]
352 }
353
354 fn edge_case_items(&self) -> Vec<(bool, &'static str)> {
355 vec![
356 (self.g1_handles_generics, "G1: Handles generics"),
357 (self.g2_handles_lifetimes, "G2: Handles lifetimes"),
358 (self.g3_handles_async, "G3: Handles async"),
359 (self.g4_handles_unsafe, "G4: Handles unsafe"),
360 (self.g5_handles_macros, "G5: Handles macros"),
361 ]
362 }
363
364 fn documentation_items(&self) -> Vec<(bool, &'static str)> {
365 vec![
366 (self.d1_test_names_descriptive, "D1: Descriptive test names"),
367 (self.d2_comments_present, "D2: Comments present"),
368 (self.d3_proptest_strategies_clear, "D3: Clear proptest strategies"),
369 ]
370 }
371}
372
373fn report_header(out: &mut String, qa: &PopperianQA) {
375 out.push_str("# Popperian Falsification QA Report\n\n");
376 out.push_str(&format!("**Score:** {}/100\n", qa.score()));
377 out.push_str(&format!("**Grade:** {}\n", qa.grade()));
378 out.push_str(&format!("**Items Passed:** {}/{}\n\n", qa.passed_count(), qa.total_items()));
379}
380
381fn report_section(out: &mut String, heading: &str, items: &[(bool, &str)]) {
383 out.push_str(heading);
384 for &(passed, label) in items {
385 let mark = if passed { "x" } else { " " };
386 out.push_str(&format!("- [{mark}] {label}\n"));
387 }
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393
394 #[test]
395 fn test_from_score_95_to_100_arm() {
396 for score in [95u8, 97, 100] {
397 match score {
398 95..=100 => assert_eq!(QAGrade::from_score(score), QAGrade::APlus),
399 _ => unreachable!(),
400 }
401 }
402 }
403
404 #[test]
405 fn test_from_score_90_to_94_arm() {
406 for score in [90u8, 92, 94] {
407 match score {
408 90..=94 => assert_eq!(QAGrade::from_score(score), QAGrade::A),
409 _ => unreachable!(),
410 }
411 }
412 }
413
414 #[test]
415 fn test_from_score_85_to_89_arm() {
416 for score in [85u8, 87, 89] {
417 match score {
418 85..=89 => assert_eq!(QAGrade::from_score(score), QAGrade::BPlus),
419 _ => unreachable!(),
420 }
421 }
422 }
423
424 #[test]
425 fn test_from_score_80_to_84_arm() {
426 for score in [80u8, 82, 84] {
427 match score {
428 80..=84 => assert_eq!(QAGrade::from_score(score), QAGrade::B),
429 _ => unreachable!(),
430 }
431 }
432 }
433
434 #[test]
435 fn test_from_score_70_to_79_arm() {
436 for score in [70u8, 75, 79] {
437 match score {
438 70..=79 => assert_eq!(QAGrade::from_score(score), QAGrade::C),
439 _ => unreachable!(),
440 }
441 }
442 }
443
444 #[test]
445 fn test_from_score_0_to_69_arm() {
446 for score in [0u8, 35, 69] {
447 match score {
448 0..=69 => assert_eq!(QAGrade::from_score(score), QAGrade::F),
449 _ => unreachable!(),
450 }
451 }
452 }
453
454 #[test]
455 fn test_qa_grade_is_passing() {
456 assert!(QAGrade::APlus.is_passing());
457 assert!(QAGrade::A.is_passing());
458 assert!(QAGrade::BPlus.is_passing());
459 assert!(QAGrade::B.is_passing());
460 assert!(QAGrade::C.is_passing());
461 assert!(!QAGrade::F.is_passing());
462 }
463
464 #[test]
465 fn test_popperian_qa_new() {
466 let qa = PopperianQA::new();
467 assert_eq!(qa.score(), 0);
468 assert_eq!(qa.grade(), QAGrade::F);
469 assert_eq!(qa.passed_count(), 0);
470 }
471
472 #[test]
473 fn test_popperian_qa_full_score() {
474 let qa = PopperianQA {
475 r1_same_loss_curve: true,
476 r2_same_final_weights: true,
477 r3_same_eval_metrics: true,
478 r4_environment_locked: true,
479 c1_parses_as_rust: true,
480 c2_type_checks: true,
481 c3_no_unused_warnings: true,
482 c4_links_correctly: true,
483 x1_tests_pass_on_correct: true,
484 x2_tests_fail_on_mutant: true,
485 x3_assertions_meaningful: true,
486 x4_no_tautologies: true,
487 v1_branch_coverage_delta: true,
488 v2_line_coverage_delta: true,
489 v3_edge_cases_present: true,
490 e1_vram_under_8gb: true,
491 e2_training_under_4hrs: true,
492 e3_inference_under_1s: true,
493 g1_handles_generics: true,
494 g2_handles_lifetimes: true,
495 g3_handles_async: true,
496 g4_handles_unsafe: true,
497 g5_handles_macros: true,
498 d1_test_names_descriptive: true,
499 d2_comments_present: true,
500 d3_proptest_strategies_clear: true,
501 };
502 assert_eq!(qa.score(), 100);
503 assert_eq!(qa.grade(), QAGrade::APlus);
504 assert_eq!(qa.passed_count(), 26);
505 }
506
507 #[test]
508 fn test_popperian_qa_partial_score() {
509 let mut qa = PopperianQA::new();
510 qa.r1_same_loss_curve = true;
512 qa.r2_same_final_weights = true;
513 qa.r3_same_eval_metrics = true;
514 qa.r4_environment_locked = true;
515
516 assert_eq!(qa.score(), 20);
517 assert!(qa.reproducibility_passed());
518 assert!(!qa.compilation_passed());
519 }
520
521 #[test]
522 fn test_popperian_qa_category_checks() {
523 let mut qa = PopperianQA::new();
524
525 qa.c1_parses_as_rust = true;
527 qa.c2_type_checks = true;
528 qa.c3_no_unused_warnings = true;
529 qa.c4_links_correctly = true;
530
531 assert!(qa.compilation_passed());
532 assert!(!qa.reproducibility_passed());
533 assert!(!qa.correctness_passed());
534 }
535
536 #[test]
537 fn test_popperian_qa_report_contains_sections() {
538 let qa = PopperianQA::new();
539 let report = qa.report();
540
541 assert!(report.contains("# Popperian Falsification QA Report"));
542 assert!(report.contains("## Reproducibility"));
543 assert!(report.contains("## Compilation"));
544 assert!(report.contains("## Correctness"));
545 assert!(report.contains("## Coverage"));
546 assert!(report.contains("## Efficiency"));
547 assert!(report.contains("## Edge Cases"));
548 assert!(report.contains("## Documentation"));
549 }
550
551 #[test]
552 fn test_qa_grade_display_aplus_arm() {
553 let g = QAGrade::APlus;
554 match g {
555 QAGrade::APlus => assert_eq!(g.to_string(), "A+ (Excellent)"),
556 _ => unreachable!(),
557 }
558 }
559
560 #[test]
561 fn test_qa_grade_display_a_arm() {
562 let g = QAGrade::A;
563 match g {
564 QAGrade::A => assert_eq!(g.to_string(), "A (Very Good)"),
565 _ => unreachable!(),
566 }
567 }
568
569 #[test]
570 fn test_qa_grade_display_bplus_arm() {
571 let g = QAGrade::BPlus;
572 match g {
573 QAGrade::BPlus => assert_eq!(g.to_string(), "B+ (Good)"),
574 _ => unreachable!(),
575 }
576 }
577
578 #[test]
579 fn test_qa_grade_display_b_arm() {
580 let g = QAGrade::B;
581 match g {
582 QAGrade::B => assert_eq!(g.to_string(), "B (Satisfactory)"),
583 _ => unreachable!(),
584 }
585 }
586
587 #[test]
588 fn test_qa_grade_display_c_arm() {
589 let g = QAGrade::C;
590 match g {
591 QAGrade::C => assert_eq!(g.to_string(), "C (Needs Improvement)"),
592 _ => unreachable!(),
593 }
594 }
595
596 #[test]
597 fn test_qa_grade_display_f_arm() {
598 let g = QAGrade::F;
599 match g {
600 QAGrade::F => assert_eq!(g.to_string(), "F (Failing)"),
601 _ => unreachable!(),
602 }
603 }
604
605 #[test]
606 fn test_qa_grade_from_score_overflow() {
607 assert_eq!(QAGrade::from_score(101), QAGrade::F);
609 assert_eq!(QAGrade::from_score(255), QAGrade::F);
610 }
611
612 #[test]
613 fn test_qa_grade_ordering() {
614 assert!(QAGrade::APlus > QAGrade::A);
615 assert!(QAGrade::A > QAGrade::BPlus);
616 assert!(QAGrade::BPlus > QAGrade::B);
617 assert!(QAGrade::B > QAGrade::C);
618 assert!(QAGrade::C > QAGrade::F);
619 }
620}