1use std::collections::BTreeMap;
11use std::fmt;
12
13use serde::{Deserialize, Serialize};
14
15use crate::binding::{BindingRegistry, ImplStatus};
16use crate::discharge::summary::{self, Discharged, L4Source};
17use crate::schema::Contract;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
30pub enum ProofLevel {
31 L1,
33 L2,
35 L3,
37 L4,
39 L5,
41}
42
43impl fmt::Display for ProofLevel {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 let s = match self {
47 Self::L1 => "L1",
48 Self::L2 => "L2",
49 Self::L3 => "L3",
50 Self::L4 => "L4",
51 Self::L5 => "L5",
52 };
53 write!(f, "{s}")
54 }
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct ContractProofStatus {
62 pub stem: String,
64 pub proof_level: ProofLevel,
66 pub obligations: u32,
68 #[serde(default)]
72 pub not_applicable: u32,
73 pub falsification_tests: u32,
75 pub kani_harnesses: u32,
77 pub lean_proved: u32,
79 pub lean_grounded: u32,
81 pub l4_self_declared: bool,
83 pub bindings_implemented: u32,
85 pub bindings_total: u32,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct KernelClassSummary {
94 pub label: String,
96 pub description: String,
98 pub contract_stems: Vec<String>,
100 pub min_proof_level: ProofLevel,
102 pub all_bound: bool,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct ProofStatusReport {
111 pub schema_version: String,
113 pub timestamp: String,
115 pub contracts: Vec<ContractProofStatus>,
117 pub kernel_classes: Vec<KernelClassSummary>,
119 pub l4_self_declared_excluded: bool,
121 #[serde(default)]
124 pub l4_source: L4Source,
125 #[serde(default)]
127 pub l4_withheld: Option<String>,
128 pub totals: ProofStatusTotals,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct ProofStatusTotals {
135 pub contracts: u32,
137 pub obligations: u32,
139 #[serde(default)]
142 pub not_applicable: u32,
143 pub falsification_tests: u32,
145 pub kani_harnesses: u32,
147 pub lean_proved: u32,
149 pub lean_grounded: u32,
151 pub l4_self_declared: u32,
153 pub bindings_implemented: u32,
155 pub bindings_total: u32,
157}
158
159fn kernel_class_map() -> Vec<(&'static str, &'static str, &'static [&'static str])> {
170 vec![
171 (
172 "A",
173 "GQA+RMSNorm+SiLU+SwiGLU+RoPE",
174 &[
175 "rmsnorm-kernel-v1",
176 "silu-kernel-v1",
177 "swiglu-kernel-v1",
178 "rope-kernel-v1",
179 "gqa-kernel-v1",
180 "softmax-kernel-v1",
181 "matmul-kernel-v1",
182 ],
183 ),
184 (
185 "B",
186 "MHA+LayerNorm+GELU+AbsPos",
187 &[
188 "layernorm-kernel-v1",
189 "gelu-kernel-v1",
190 "attention-kernel-v1",
191 "softmax-kernel-v1",
192 "matmul-kernel-v1",
193 "absolute-position-v1",
194 ],
195 ),
196 (
197 "C",
198 "MHA+LayerNorm+GELU+ALiBi",
199 &[
200 "layernorm-kernel-v1",
201 "gelu-kernel-v1",
202 "attention-kernel-v1",
203 "softmax-kernel-v1",
204 "alibi-kernel-v1",
205 "matmul-kernel-v1",
206 ],
207 ),
208 (
209 "D",
210 "LayerNorm+GELU+SiLU+GQA",
211 &[
212 "layernorm-kernel-v1",
213 "gelu-kernel-v1",
214 "silu-kernel-v1",
215 "gqa-kernel-v1",
216 "softmax-kernel-v1",
217 "matmul-kernel-v1",
218 ],
219 ),
220 (
221 "E",
222 "RMSNorm+SwiGLU+GQA",
223 &[
224 "rmsnorm-kernel-v1",
225 "swiglu-kernel-v1",
226 "gqa-kernel-v1",
227 "softmax-kernel-v1",
228 "matmul-kernel-v1",
229 ],
230 ),
231 ]
232}
233
234#[must_use]
261pub fn is_lean_proved_with_grounding(contract: &Contract, grounded: u32) -> bool {
262 let total = contract.proof_obligations.len() as u32;
263 if total == 0 {
264 return false;
265 }
266 let not_applicable = contract
274 .verification_summary
275 .as_ref()
276 .map_or(0, |vs| vs.l4_not_applicable);
277 grounded > 0 && grounded + not_applicable >= total
278}
279
280#[must_use]
287pub fn is_l4_self_declared(contract: &Contract) -> bool {
288 is_l4_self_declared_with_grounding(contract, count_lean_theorems_for_contract(contract))
289}
290
291#[must_use]
293pub fn is_l4_self_declared_with_grounding(contract: &Contract, grounded: u32) -> bool {
294 let total = contract.proof_obligations.len() as u32;
295 if total == 0 {
296 return false;
297 }
298 let Some(vs) = contract.verification_summary.as_ref() else {
299 return false;
300 };
301 let claim_covers = vs.l4_lean_proved > 0 && vs.l4_lean_proved + vs.l4_not_applicable >= total;
302 claim_covers && !is_lean_proved_with_grounding(contract, grounded)
303}
304
305#[must_use]
316#[allow(clippy::cast_possible_truncation)]
317pub fn count_not_applicable(contract: &Contract) -> u32 {
318 contract
319 .proof_obligations
320 .iter()
321 .filter(|ob| ob.is_not_applicable())
322 .count() as u32
323}
324
325fn is_fully_bound(binding_status: Option<(u32, u32)>) -> bool {
327 binding_status.is_some_and(|(implemented, total)| total > 0 && implemented == total)
328}
329
330#[allow(clippy::cast_possible_truncation)]
341pub fn compute_proof_level(contract: &Contract, binding_status: Option<(u32, u32)>) -> ProofLevel {
342 compute_proof_level_with_grounding(
343 contract,
344 binding_status,
345 count_lean_theorems_for_contract(contract),
346 )
347}
348
349#[allow(clippy::cast_possible_truncation)]
351#[must_use]
352pub fn compute_proof_level_with_grounding(
353 contract: &Contract,
354 binding_status: Option<(u32, u32)>,
355 grounded: u32,
356) -> ProofLevel {
357 let total_obligations = contract.proof_obligations.len() as u32;
358 let ft_count = contract.falsification_tests.len() as u32;
359 let kani_count = contract.kani_harnesses.len() as u32;
360
361 if is_lean_proved_with_grounding(contract, grounded) {
363 return if is_fully_bound(binding_status) {
364 ProofLevel::L5
365 } else {
366 ProofLevel::L4
367 };
368 }
369
370 let has_tests = ft_count >= total_obligations && total_obligations > 0;
372 if kani_count > 0 && has_tests {
373 return ProofLevel::L3;
374 }
375
376 if has_tests {
378 return ProofLevel::L2;
379 }
380
381 ProofLevel::L1
383}
384
385pub(crate) const LEAN_THEOREM_BASES: &[&str] = &[
392 "crates/aprender-contracts-staging/lean",
393 "lean",
394 "../provable-contracts/lean",
395];
396
397fn insert_name_forms(names: &mut std::collections::HashSet<String>, label: &str) {
399 names.insert(format!("Theorems.{label}"));
400 names.insert(label.to_string());
401 names.insert(label.to_lowercase());
402}
403
404pub(crate) fn camel_case(snake: &str) -> String {
406 snake
407 .split('_')
408 .map(|s| {
409 let mut c = s.chars();
410 match c.next() {
411 None => String::new(),
412 Some(f) => f.to_uppercase().chain(c).collect(),
413 }
414 })
415 .collect()
416}
417
418pub(crate) fn first_camel_word(camel: &str) -> String {
420 camel
421 .chars()
422 .enumerate()
423 .take_while(|(i, c)| *i == 0 || !c.is_uppercase())
424 .map(|(_, c)| c)
425 .collect()
426}
427
428fn insert_theorem_names_from_content(names: &mut std::collections::HashSet<String>, content: &str) {
430 for line in content.lines() {
431 let Some(pos) = line.find("theorem ") else {
432 continue;
433 };
434 let tname: String = line[pos + 8..]
435 .chars()
436 .take_while(|c| c.is_alphanumeric() || *c == '_')
437 .collect();
438 if tname.is_empty() {
439 continue;
440 }
441 let camel = camel_case(&tname);
442 names.insert(format!("Theorems.{camel}"));
443 names.insert(camel.clone());
444 let first_word = first_camel_word(&camel);
445 if first_word.len() >= 3 {
446 names.insert(format!("Theorems.{first_word}"));
447 names.insert(first_word);
448 }
449 }
450}
451
452fn is_lean_ident_byte(c: u8) -> bool {
454 c.is_ascii_alphanumeric() || c == b'_' || c == b'\''
455}
456
457pub(crate) fn lean_has_sorry(content: &str) -> bool {
467 let b = content.as_bytes();
468 let mut i = 0;
469 let mut depth = 0usize;
470 while i < b.len() {
471 if depth > 0 {
472 (i, depth) = step_block_comment(b, i, depth);
473 continue;
474 }
475 match scan_code_at(b, i) {
476 LeanScan::Next(j) => i = j,
477 LeanScan::OpenBlock(j) => {
478 depth = 1;
479 i = j;
480 }
481 LeanScan::Sorry => return true,
482 LeanScan::EndsInLineComment => return false,
483 }
484 }
485 depth > 0
486}
487
488enum LeanScan {
490 Next(usize),
492 OpenBlock(usize),
494 Sorry,
496 EndsInLineComment,
498}
499
500fn sorry_token_at(b: &[u8], i: usize) -> bool {
502 b[i..].starts_with(b"sorry")
503 && (i == 0 || !is_lean_ident_byte(b[i - 1]))
504 && b.get(i + 5).is_none_or(|&c| !is_lean_ident_byte(c))
505}
506
507fn step_block_comment(b: &[u8], i: usize, depth: usize) -> (usize, usize) {
509 if b[i..].starts_with(b"/-") {
510 (i + 2, depth + 1)
511 } else if b[i..].starts_with(b"-/") {
512 (i + 2, depth - 1)
513 } else {
514 (i + 1, depth)
515 }
516}
517
518fn scan_code_at(b: &[u8], i: usize) -> LeanScan {
520 let prev_ident = i > 0 && is_lean_ident_byte(b[i - 1]);
521 if b[i..].starts_with(b"--") {
522 return match b[i..].iter().position(|&c| c == b'\n') {
523 Some(p) => LeanScan::Next(i + p + 1),
524 None => LeanScan::EndsInLineComment,
525 };
526 }
527 if b[i..].starts_with(b"/-") {
528 return LeanScan::OpenBlock(i + 2);
529 }
530 if b[i] == b'r' && !prev_ident && matches!(b.get(i + 1), Some(b'"' | b'#')) {
531 return scan_raw_string(b, i);
532 }
533 if b[i] == b'"' {
534 return scan_string(b, i);
535 }
536 if b[i] == b'\'' && !prev_ident {
537 return LeanScan::Next(skip_char_literal(b, i));
538 }
539 if sorry_token_at(b, i) {
540 LeanScan::Sorry
541 } else {
542 LeanScan::Next(i + 1)
543 }
544}
545
546fn scan_literal_body(b: &[u8], from: usize, to: usize, resume: usize) -> LeanScan {
548 if (from..to).any(|k| sorry_token_at(b, k)) {
549 LeanScan::Sorry
550 } else {
551 LeanScan::Next(resume)
552 }
553}
554
555fn scan_raw_string(b: &[u8], i: usize) -> LeanScan {
557 let hashes = b[i + 1..].iter().take_while(|&&c| c == b'#').count();
558 let open = i + 1 + hashes;
559 if b.get(open) != Some(&b'"') {
560 return LeanScan::Next(i + 1);
561 }
562 let mut close = vec![b'"'];
563 close.extend(std::iter::repeat_n(b'#', hashes));
564 let Some(p) = b[open + 1..]
565 .windows(close.len())
566 .position(|w| w == close.as_slice())
567 else {
568 return LeanScan::Sorry;
569 };
570 let end = open + 1 + p;
571 scan_literal_body(b, open + 1, end, end + close.len())
572}
573
574fn scan_string(b: &[u8], i: usize) -> LeanScan {
576 let mut j = i + 1;
577 loop {
578 match b.get(j) {
579 None => return LeanScan::Sorry,
580 Some(b'\\') => j += 2,
581 Some(b'"') => break,
582 Some(_) => j += 1,
583 }
584 }
585 scan_literal_body(b, i + 1, j, j + 1)
586}
587
588fn skip_char_literal(b: &[u8], i: usize) -> usize {
590 let from = if b.get(i + 1) == Some(&b'\\') {
591 i + 3
592 } else {
593 i + 2
594 };
595 match b
596 .get(from..b.len().min(i + 7))
597 .and_then(|s| s.iter().position(|&c| c == b'\''))
598 {
599 Some(p) => from + p + 1,
600 None => i + 1,
601 }
602}
603
604fn insert_domain_theorems(names: &mut std::collections::HashSet<String>, domain: &std::path::Path) {
611 let domain_name = domain
612 .file_name()
613 .unwrap_or_default()
614 .to_string_lossy()
615 .to_string();
616 let Ok(files) = std::fs::read_dir(domain) else {
617 return;
618 };
619 for file in files.flatten() {
620 let path = file.path();
621 if path.extension().is_none_or(|e| e != "lean") {
622 continue;
623 }
624 let Ok(content) = std::fs::read_to_string(&path) else {
625 continue;
626 };
627 if lean_has_sorry(&content) {
628 continue;
629 }
630 let stem = path
631 .file_stem()
632 .unwrap_or_default()
633 .to_string_lossy()
634 .to_string();
635 names.insert(format!("Theorems.{domain_name}.{stem}"));
636 insert_name_forms(names, &domain_name);
637 insert_name_forms(names, &stem);
638 insert_theorem_names_from_content(names, &content);
639 }
640}
641
642fn scan_theorem_base(base: &str) -> std::collections::HashSet<String> {
644 let mut names = std::collections::HashSet::new();
645 let search_dir = std::path::Path::new(base).join("ProvableContracts/Theorems");
646 if !search_dir.exists() {
647 return names;
648 }
649 let Ok(domains) = std::fs::read_dir(&search_dir) else {
650 return names;
651 };
652 for domain_entry in domains.flatten() {
653 let path = domain_entry.path();
654 if path.is_dir() {
655 insert_domain_theorems(&mut names, &path);
656 }
657 }
658 names
659}
660
661fn lean_theorem_names() -> &'static std::collections::HashSet<String> {
664 use std::sync::OnceLock;
665 static CACHE: OnceLock<std::collections::HashSet<String>> = OnceLock::new();
666 CACHE.get_or_init(|| {
667 for base in LEAN_THEOREM_BASES {
668 let names = scan_theorem_base(base);
669 if !names.is_empty() {
670 return names;
671 }
672 }
673 std::collections::HashSet::new()
674 })
675}
676
677struct Grounding {
680 source: L4Source,
681 discharged: Discharged,
682}
683
684fn grounding() -> &'static Grounding {
685 use std::sync::OnceLock;
686 static CACHE: OnceLock<Grounding> = OnceLock::new();
687 CACHE.get_or_init(|| {
688 let bases: Vec<&std::path::Path> = LEAN_THEOREM_BASES
689 .iter()
690 .map(std::path::Path::new)
691 .collect();
692 grounding_from(&bases, summary::current_tree_sha)
693 })
694}
695
696fn grounding_from(
699 bases: &[&std::path::Path],
700 tree_sha: impl Fn(&std::path::Path) -> Option<String>,
701) -> Grounding {
702 for &dir in bases {
703 let path = summary::summary_path(dir);
704 if !dir.is_dir() || !path.is_file() {
705 continue;
706 }
707 let mut discharged = match summary::load(&path) {
708 Ok(s) => summary::discharged(&s, tree_sha(dir).as_deref()),
709 Err(why) => Discharged {
710 withheld: Some(why),
711 ..Discharged::default()
712 },
713 };
714 refine(dir, &mut discharged);
715 return Grounding {
716 source: L4Source::Discharge,
717 discharged,
718 };
719 }
720 Grounding {
721 source: L4Source::SelfDeclared,
722 discharged: Discharged::default(),
723 }
724}
725
726fn refine(lean_dir: &std::path::Path, discharged: &mut Discharged) {
729 use crate::discharge::refinement;
730 if discharged.theorems.is_empty() {
731 return;
732 }
733 let l4 = match refinement::load(lean_dir) {
734 Ok(rec) => {
735 let modules = discharged.module_of.values().cloned().collect();
736 let root = workspace_root(lean_dir);
737 refinement::l4_modules(&refinement::judge(
738 &rec.models,
739 &modules,
740 refinement::workspace_resolver(&root),
741 ))
742 }
743 Err(_) => std::collections::BTreeSet::new(),
744 };
745 discharged.require_refinement(&l4);
746}
747
748fn workspace_root(dir: &std::path::Path) -> std::path::PathBuf {
750 let abs = std::fs::canonicalize(dir).unwrap_or_else(|_| dir.to_path_buf());
751 abs.ancestors()
752 .find(|a| {
753 std::fs::read_to_string(a.join("Cargo.toml"))
754 .is_ok_and(|t| t.lines().any(|l| l.trim() == "[workspace]"))
755 })
756 .map_or_else(
757 || std::path::PathBuf::from("."),
758 std::path::Path::to_path_buf,
759 )
760}
761
762#[must_use]
764pub fn discharge_grounding() -> &'static Discharged {
765 &grounding().discharged
766}
767
768#[must_use]
771pub fn count_discharged_for_contract(contract: &Contract, discharged: &Discharged) -> u32 {
772 let n = contract
773 .equations
774 .values()
775 .filter_map(|eq| eq.lean_theorem.as_deref())
776 .filter(|t| discharged.grants(t))
777 .count();
778 u32::try_from(n).unwrap_or(u32::MAX)
779}
780
781fn count_lean_theorems_for_contract(contract: &Contract) -> u32 {
783 let g = grounding();
784 match g.source {
785 L4Source::Discharge => count_discharged_for_contract(contract, &g.discharged),
786 L4Source::SelfDeclared => count_scanned_theorems_for_contract(contract),
787 }
788}
789
790fn count_scanned_theorems_for_contract(contract: &Contract) -> u32 {
793 let theorems = lean_theorem_names();
794 let mut count = 0u32;
795 for eq in contract.equations.values() {
796 if let Some(ref theorem_ref) = eq.lean_theorem {
797 let name = theorem_ref.trim().trim_matches('"');
798 if theorems.contains(name)
800 || theorems.contains(name.strip_prefix("Theorems.").unwrap_or(name))
801 || theorems.contains(&name.to_lowercase())
802 {
803 count += 1;
804 }
805 }
806 }
807 count
808}
809
810#[allow(clippy::cast_possible_truncation)]
816pub fn proof_status_report(
817 contracts: &[(String, &Contract)],
818 binding: Option<&BindingRegistry>,
819 include_classes: bool,
820) -> ProofStatusReport {
821 let mut statuses = Vec::new();
822 let mut totals = ProofStatusTotals {
823 contracts: contracts.len() as u32,
824 obligations: 0,
825 not_applicable: 0,
826 falsification_tests: 0,
827 kani_harnesses: 0,
828 lean_proved: 0,
829 lean_grounded: 0,
830 l4_self_declared: 0,
831 bindings_implemented: 0,
832 bindings_total: 0,
833 };
834
835 for (stem, contract) in contracts {
836 let contract_file = format!("{stem}.yaml");
837
838 let obligations = contract.proof_obligations.len() as u32;
839 let not_applicable = count_not_applicable(contract);
840 let ft_count = contract.falsification_tests.len() as u32;
841 let kani_count = contract.kani_harnesses.len() as u32;
842 let lean_proved = contract
845 .verification_summary
846 .as_ref()
847 .map_or(0, |vs| vs.l4_lean_proved);
848 let lean_grounded = count_lean_theorems_for_contract(contract);
849 let lean_proved = if lean_proved == 0 {
850 lean_grounded
851 } else {
852 lean_proved
853 };
854 let l4_self_declared = is_l4_self_declared_with_grounding(contract, lean_grounded);
855
856 let (b_impl, b_total) = if let Some(reg) = binding {
858 count_bindings(&contract_file, contract, reg)
859 } else {
860 (0, contract.equations.len() as u32)
861 };
862
863 let binding_status = if binding.is_some() {
864 Some((b_impl, b_total))
865 } else {
866 None
867 };
868
869 let proof_level =
870 compute_proof_level_with_grounding(contract, binding_status, lean_grounded);
871
872 totals.obligations += obligations;
873 totals.not_applicable += not_applicable;
874 totals.falsification_tests += ft_count;
875 totals.kani_harnesses += kani_count;
876 totals.lean_proved += lean_proved;
877 totals.lean_grounded += lean_grounded;
878 totals.l4_self_declared += u32::from(l4_self_declared);
879 totals.bindings_implemented += b_impl;
880 totals.bindings_total += b_total;
881
882 statuses.push(ContractProofStatus {
883 stem: stem.clone(),
884 proof_level,
885 obligations,
886 not_applicable,
887 falsification_tests: ft_count,
888 kani_harnesses: kani_count,
889 lean_proved,
890 lean_grounded,
891 l4_self_declared,
892 bindings_implemented: b_impl,
893 bindings_total: b_total,
894 });
895 }
896
897 let kernel_classes = if include_classes {
899 build_kernel_classes(&statuses)
900 } else {
901 Vec::new()
902 };
903
904 let timestamp = current_timestamp();
905
906 ProofStatusReport {
907 schema_version: "1.0.0".to_string(),
908 l4_self_declared_excluded: true,
909 l4_source: grounding().source,
910 l4_withheld: grounding().discharged.withheld.clone(),
911 timestamp,
912 contracts: statuses,
913 kernel_classes,
914 totals,
915 }
916}
917
918pub fn format_text(report: &ProofStatusReport) -> String {
920 let mut out = String::new();
921
922 out.push_str(&format!(
923 "Proof Status ({} contracts)\n\n",
924 report.totals.contracts
925 ));
926
927 out.push_str(&format!(
928 " {:<35} {:>5} {:>6} {:>5} {:>4} {:>4} {:>9} {:>13}\n",
929 "Contract", "Level", "Obligs", "Tests", "Kani", "Lean", "Bindings", "L4 evidence"
930 ));
931 out.push_str(&format!(" {}\n", "─".repeat(86)));
932
933 for c in &report.contracts {
934 let l4_evidence = if c.l4_self_declared {
937 "self-declared"
938 } else if c.lean_grounded > 0 {
939 "grounded"
940 } else {
941 "-"
942 };
943 out.push_str(&format!(
944 " {:<35} {:>5} {:>6} {:>5} {:>4} {:>4} {:>4}/{:<4} {:>13}\n",
945 truncate(&c.stem, 35),
946 c.proof_level,
947 c.obligations,
948 c.falsification_tests,
949 c.kani_harnesses,
950 c.lean_proved,
951 c.bindings_implemented,
952 c.bindings_total,
953 l4_evidence,
954 ));
955 }
956
957 if !report.kernel_classes.is_empty() {
958 out.push_str("\nKernel Classes:\n");
959 for kc in &report.kernel_classes {
960 let bound_str = if kc.all_bound { "all bound" } else { "gaps" };
961 out.push_str(&format!(
962 " {} ({}): min={}, {} contracts, {}\n",
963 kc.label,
964 kc.description,
965 kc.min_proof_level,
966 kc.contract_stems.len(),
967 bound_str,
968 ));
969 }
970 }
971
972 out.push_str(&format!(
973 "\nTotals: {} obligations ({} N/A, never counted as proved), {} tests, {} kani, {} lean claimed ({} grounded), {}/{} bound\n\
974 L4 evidence: {} contract(s) self-declared and excluded from L4 (ONT-2a andon); grounded means a \
975 theorem the equation names that a green, fresh discharge-summary.json lists (EV-8b), or with no summary \
976 a sorry-free in-tree Lean theorem\n",
977 report.totals.obligations,
978 report.totals.not_applicable,
979 report.totals.falsification_tests,
980 report.totals.kani_harnesses,
981 report.totals.lean_proved,
982 report.totals.lean_grounded,
983 report.totals.bindings_implemented,
984 report.totals.bindings_total,
985 report.totals.l4_self_declared,
986 ));
987 let source = match report.l4_source {
988 L4Source::Discharge => "discharge (discharge-summary.json)",
989 L4Source::SelfDeclared => "self-declared (no discharge-summary.json; in-tree .lean scan)",
990 };
991 out.push_str(&format!("L4 source: {source}\n"));
992 if let Some(why) = &report.l4_withheld {
993 out.push_str(&format!("L4 withheld: {why}\n"));
994 }
995
996 out
997}
998
999#[allow(clippy::cast_possible_truncation)]
1003pub(crate) fn count_bindings(
1004 contract_file: &str,
1005 contract: &Contract,
1006 binding: &BindingRegistry,
1007) -> (u32, u32) {
1008 let total = contract.equations.len() as u32;
1009 let implemented = binding
1010 .bindings_for(contract_file)
1011 .iter()
1012 .filter(|b| b.status == ImplStatus::Implemented)
1013 .count() as u32;
1014 (implemented, total)
1015}
1016
1017fn build_kernel_classes(statuses: &[ContractProofStatus]) -> Vec<KernelClassSummary> {
1019 let status_map: BTreeMap<&str, &ContractProofStatus> =
1020 statuses.iter().map(|s| (s.stem.as_str(), s)).collect();
1021
1022 kernel_class_map()
1023 .into_iter()
1024 .map(|(label, desc, stems)| {
1025 let found_stems: Vec<String> = stems
1026 .iter()
1027 .filter(|s| status_map.contains_key(**s))
1028 .map(|s| (*s).to_string())
1029 .collect();
1030
1031 let min_level = found_stems
1032 .iter()
1033 .filter_map(|s| status_map.get(s.as_str()))
1034 .map(|c| c.proof_level)
1035 .min()
1036 .unwrap_or(ProofLevel::L1);
1037
1038 let all_bound = !found_stems.is_empty()
1039 && found_stems.iter().all(|s| {
1040 status_map.get(s.as_str()).is_some_and(|c| {
1041 c.bindings_total > 0 && c.bindings_implemented == c.bindings_total
1042 })
1043 });
1044
1045 KernelClassSummary {
1046 label: label.to_string(),
1047 description: desc.to_string(),
1048 contract_stems: found_stems,
1049 min_proof_level: min_level,
1050 all_bound,
1051 }
1052 })
1053 .collect()
1054}
1055
1056fn truncate(s: &str, max: usize) -> &str {
1058 if s.len() > max {
1059 &s[..max]
1060 } else {
1061 s
1062 }
1063}
1064
1065fn current_timestamp() -> String {
1067 let duration = std::time::SystemTime::now()
1071 .duration_since(std::time::UNIX_EPOCH)
1072 .unwrap_or_default();
1073 format!("{}Z", duration.as_secs())
1074}
1075
1076#[cfg(test)]
1077#[path = "proof_status_tests.rs"]
1078mod tests;