1pub mod cache;
12pub mod capability_cells_gate;
13mod composition_gate;
14pub mod config;
15pub mod consistency_gate;
16pub mod diff;
17pub mod duplicate_stems;
18pub mod evidence_gate;
19pub mod finding;
20mod gates;
21pub use gates::collect_yaml_files;
22pub mod bindings_gate;
23mod gates_extended;
24pub mod ratchet_gates;
25pub mod refinement_gate;
26pub mod refines_gate;
27pub mod relations_gate;
28pub mod rules;
29pub mod sarif;
30pub mod shapes_gate;
31pub mod sigma_gate;
32pub mod sigma_symbols;
33mod strict_test_binding;
34pub mod subsumption;
35pub mod tbox_gate;
36pub mod trend;
37pub mod valid_under_gate;
38
39use std::collections::{HashMap, HashSet};
40use std::path::Path;
41use std::time::Instant;
42
43use serde::Serialize;
44
45use self::finding::LintFinding;
46use self::gates::{
47 load_binding, load_contracts, run_audit_gate, run_score_gate, run_validate_gate,
48};
49use self::gates_extended::{
50 check_stale_suppressions, run_enforce_gate, run_enforcement_level_gate,
51 run_reverse_coverage_gate, run_verify_gate,
52};
53use self::rules::RuleSeverity;
54use crate::ontology::arming::{meet_armed, ArmedGates};
55use crate::ontology::verdict::Verdict;
56
57#[derive(Debug, Clone, Serialize)]
59pub struct GateResult {
60 pub name: String,
61 pub passed: bool,
62 pub skipped: bool,
63 pub verdict: Verdict,
65 pub duration_ms: u64,
66 pub detail: GateDetail,
67 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub extra: Option<GateExtra>,
75}
76
77#[derive(Debug, Clone, Serialize)]
96#[serde(tag = "type")]
97pub enum GateDetail {
98 #[serde(rename = "validate")]
99 Validate {
100 contracts: usize,
101 errors: usize,
102 warnings: usize,
103 error_messages: Vec<String>,
104 },
105 #[serde(rename = "audit")]
106 Audit {
107 contracts: usize,
108 findings: usize,
109 finding_messages: Vec<String>,
110 },
111 #[serde(rename = "score")]
112 Score {
113 contracts: usize,
114 min_score: f64,
115 mean_score: f64,
116 threshold: f64,
117 below_threshold: Vec<String>,
118 },
119 #[serde(rename = "verify")]
120 Verify {
121 total_refs: usize,
122 existing: usize,
123 missing: usize,
124 },
125 #[serde(rename = "enforce")]
126 Enforce {
127 equations_total: usize,
128 equations_with_pre: usize,
129 equations_with_post: usize,
130 equations_with_lean: usize,
131 },
132 #[serde(rename = "reverse_coverage")]
133 ReverseCoverage {
134 total_pub_fns: usize,
135 bound_fns: usize,
136 unbound_fns: usize,
137 coverage_pct: f64,
138 threshold_pct: f64,
139 },
140 #[serde(rename = "composition")]
141 Composition {
142 edges_checked: usize,
143 edges_satisfied: usize,
144 edges_broken: usize,
145 },
146 #[serde(rename = "skipped")]
147 Skipped { reason: String },
148}
149
150#[derive(Debug, Clone, Serialize)]
153pub struct ShapesControls {
154 pub pc_extract: std::collections::BTreeMap<String, String>,
156 pub pc_shapes: std::collections::BTreeMap<String, String>,
159 #[serde(skip_serializing_if = "Option::is_none")]
162 pub capability_cells: Option<capability_cells_gate::CapabilityCellsReport>,
163}
164
165#[allow(clippy::large_enum_variant)]
189#[derive(Debug, Clone, Serialize)]
190#[serde(tag = "type")]
192#[non_exhaustive]
193pub enum GateExtra {
194 #[serde(rename = "duplicate_stems")]
196 DuplicateStems {
197 divergent: usize,
199 baselined: usize,
201 unbaselined: Vec<String>,
203 stale: Vec<String>,
205 divergent_stems: Vec<String>,
207 },
208 #[serde(rename = "sigma")]
210 Sigma {
211 entity_types: usize,
213 roles: usize,
215 symbols: usize,
217 contracts_checked: usize,
219 violations: usize,
221 formal_total: usize,
223 formal_prose: usize,
225 },
226 #[serde(rename = "relations")]
228 Relations {
229 relations_n: usize,
231 contracts_with_relations: usize,
233 roles_used: Vec<String>,
235 cycles: Vec<String>,
237 legacy_depends_on: usize,
239 legacy_unresolved_depends_on: usize,
241 violations: usize,
243 },
244 #[serde(rename = "evidence")]
246 Evidence {
247 contracts_checked: usize,
249 contracts_with_evidence: usize,
251 entity_types_checked: usize,
253 entity_types: Vec<String>,
255 levels_source: String,
257 by_level: Vec<String>,
259 census_git_sha: Option<String>,
261 unresolved_git_shas: usize,
263 violations: usize,
265 },
266 #[serde(rename = "valid_under")]
268 ValidUnder {
269 worlds: Vec<String>,
271 contracts_checked: usize,
273 kernel_contracts: usize,
275 contracts_with_valid_under: usize,
277 contracts_without_valid_under: usize,
279 baseline: Option<usize>,
281 by_world: Vec<String>,
283 violations: usize,
285 },
286 #[serde(rename = "theorem_pairing")]
288 TheoremPairing {
289 lean_base: String,
291 book_pages: usize,
293 theorem_modules: usize,
295 paired: usize,
297 unpaired_theorem_modules: usize,
299 baseline: Option<usize>,
301 unpaired: Vec<String>,
303 violations: usize,
305 },
306 #[serde(rename = "depends_on_present")]
308 DependsOnPresent {
309 contracts_checked: usize,
311 kernel_contracts: usize,
313 contracts_without_depends_on: usize,
315 baseline: Option<usize>,
317 violations: usize,
319 },
320 #[serde(rename = "proved_is_derived")]
322 ProvedIsDerived {
323 summary: Option<String>,
325 summary_error: Option<String>,
327 summary_green: bool,
329 contracts_checked: usize,
331 proved_claims: usize,
333 underived_proved_claims: usize,
335 baseline: Option<usize>,
337 underived: Vec<String>,
339 violations: usize,
341 },
342 #[serde(rename = "challenge_fresh")]
344 ChallengeFresh {
345 lean_dir: String,
347 files: usize,
349 stale: Vec<String>,
351 unrestated: Vec<String>,
353 },
354 #[serde(rename = "shapes")]
356 Shapes {
357 shapes_n: usize,
359 focus_nodes_n: usize,
361 pc_shape: String,
363 plant_violations: usize,
365 violations: usize,
367 warnings: usize,
369 by_shape: Vec<String>,
371 triples: usize,
373 armed_shapes: Vec<String>,
375 not_armed_shapes: Vec<String>,
377 declines: Vec<String>,
398 unarmed_violations: usize,
400 by_entity_type: std::collections::BTreeMap<String, usize>,
402 #[serde(flatten)]
406 controls: Box<ShapesControls>,
407 receipts: usize,
409 witnesses: usize,
411 hex_mismatches: usize,
413 unmeasured_rows: usize,
415 w3c_cases_passed: usize,
417 w3c_cases_n: usize,
418 #[serde(flatten)]
422 counters: Box<ShapesCounters>,
423 #[serde(skip_serializing_if = "Option::is_none")]
425 release: Option<Box<crate::ontology::extract::release_evidence::ReleaseStats>>,
426 readme: MeasuredSet,
428 claude_md: MeasuredSet,
430 ratchets: Ratchets,
432 },
433 #[serde(rename = "ont_consistency")]
435 Consistency {
436 checkable_n: usize,
438 units: usize,
440 unencoded_edges: usize,
442 pc_checker: String,
444 core: Vec<String>,
446 violations: usize,
448 witness: consistency_gate::WitnessReport,
450 },
451 #[serde(rename = "refines")]
453 Refines(Box<refines_gate::RefinesCounters>),
454 #[serde(rename = "bindings")]
456 Bindings(Box<bindings_gate::BindingsCounters>),
457 #[serde(rename = "refinement")]
459 Refinement(Box<refinement_gate::RefinementCounters>),
460}
461
462#[derive(Debug, Clone, Default, Serialize)]
464pub struct ShapesCounters {
465 pub symbols_resolved: usize,
467 pub symbols_unresolved: usize,
468 pub lean_statements: usize,
470 pub lean_refs_unresolved: usize,
471 pub inherited_shapes_applied: usize,
476 pub inherited_by_shape: Vec<String>,
478}
479
480#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
482pub struct MeasuredSet {
483 pub verified_commands: Vec<String>,
484}
485
486#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
489pub struct Ratchets {
490 pub measured_sets: String,
491}
492
493impl Default for Ratchets {
494 fn default() -> Self {
495 Self {
496 measured_sets: "not-checked".into(),
497 }
498 }
499}
500
501#[derive(Debug, Clone, Serialize)]
503pub struct LintReport {
504 pub passed: bool,
505 pub gates: Vec<GateResult>,
506 pub total_duration_ms: u64,
507 #[serde(skip_serializing_if = "Vec::is_empty")]
508 pub findings: Vec<LintFinding>,
509 #[serde(skip)]
510 pub cache_stats: cache::CacheStats,
511 #[serde(default, skip_serializing_if = "Vec::is_empty")]
513 pub contract_timings: Vec<(String, u64)>,
514 pub verdict: Verdict,
517 pub armed_gates: Vec<ArmedGateVerdict>,
519 pub not_armed: Vec<String>,
521 #[serde(skip_serializing_if = "Option::is_none")]
523 pub armed_monotone: Option<String>,
524 #[serde(skip_serializing_if = "Option::is_none")]
526 pub armed_shapes_monotone: Option<String>,
527}
528
529#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
531pub struct ArmedGateVerdict {
532 pub name: String,
533 pub verdict: Verdict,
534}
535
536impl LintReport {
537 pub fn arm(&mut self, armed: &ArmedGates) {
539 let results: Vec<(String, Verdict)> = self
540 .gates
541 .iter()
542 .map(|g| (g.name.clone(), g.verdict))
543 .collect();
544 let meet = meet_armed(&results, armed);
545 self.verdict = meet.verdict;
546 self.armed_gates = meet
547 .armed
548 .into_iter()
549 .map(|(name, verdict)| ArmedGateVerdict { name, verdict })
550 .collect();
551 self.not_armed = meet.not_armed;
552 }
553}
554
555pub struct LintConfig<'a> {
557 pub contract_dir: &'a Path,
558 pub binding_path: Option<&'a Path>,
559 pub min_score: f64,
560 pub severity_filter: Option<RuleSeverity>,
561 pub severity_overrides: HashMap<String, RuleSeverity>,
562 pub suppressed_findings: Vec<String>,
563 pub suppressed_rules: Vec<String>,
564 pub suppressed_files: Vec<String>,
565 pub strict: bool,
566 pub no_cache: bool,
567 pub cache_stats: bool,
568 pub crate_dir: Option<&'a Path>,
570 pub min_level: Option<crate::schema::EnforcementLevel>,
572 pub strict_test_binding: bool,
574 pub strict_test_binding_only: bool,
578}
579
580impl<'a> LintConfig<'a> {
581 pub fn new(contract_dir: &'a Path, binding_path: Option<&'a Path>, min_score: f64) -> Self {
583 Self {
584 contract_dir,
585 binding_path,
586 min_score,
587 severity_filter: None,
588 severity_overrides: HashMap::new(),
589 suppressed_findings: Vec::new(),
590 suppressed_rules: Vec::new(),
591 suppressed_files: Vec::new(),
592 strict: false,
593 no_cache: false,
594 cache_stats: false,
595 crate_dir: None,
596 min_level: None,
597 strict_test_binding: false,
598 strict_test_binding_only: false,
599 }
600 }
601}
602
603fn push_gate<F>(
608 gates: &mut Vec<GateResult>,
609 all_findings: &mut Vec<LintFinding>,
610 run: F,
611 name: &str,
612 validation_passed: bool,
613) where
614 F: FnOnce() -> (GateResult, Vec<LintFinding>),
615{
616 let (result, mut findings) = if validation_passed {
617 run()
618 } else {
619 (skipped_gate(name, "validation failed"), Vec::new())
620 };
621 gates.push(result);
622 all_findings.append(&mut findings);
623}
624
625fn per_contract_timings(
628 contracts: &[(String, crate::schema::Contract)],
629 binding: Option<&crate::binding::BindingRegistry>,
630) -> Vec<(String, u64)> {
631 let mut contract_timings: Vec<(String, u64)> = Vec::with_capacity(contracts.len());
632 for (stem, contract) in contracts {
633 let ct_start = Instant::now();
634 let _ = crate::schema::validate_contract(contract);
636 let _ = crate::audit::audit_contract(contract);
638 let _ = crate::scoring::score_contract(contract, binding, stem);
640 let ct_ms = u64::try_from(ct_start.elapsed().as_micros() / 1000).unwrap_or(0);
641 contract_timings.push((format!("{stem}.yaml"), ct_ms));
642 }
643 contract_timings.sort_by_key(|b| std::cmp::Reverse(b.1));
645 contract_timings
646}
647
648fn store_findings_in_cache(
650 root: &std::path::Path,
651 config: &LintConfig,
652 contracts: &[(String, crate::schema::Contract)],
653 all_findings: &[LintFinding],
654 stats: &mut cache::CacheStats,
655) {
656 let rule_cfg = format!("{:?}{:?}", config.severity_overrides, config.strict);
657 for (stem, _) in contracts {
658 stats.total += 1;
659 let yaml_path = config.contract_dir.join(format!("{stem}.yaml"));
660 let yaml_content = std::fs::read_to_string(&yaml_path).unwrap_or_default();
661 let hash = cache::content_hash(&yaml_content, &rule_cfg);
662 if cache::cache_get(root, &hash).is_some() {
663 stats.hits += 1;
664 } else {
665 stats.misses += 1;
666 let contract_findings: Vec<_> = all_findings
667 .iter()
668 .filter(|f| f.contract_stem.as_deref() == Some(stem.as_str()))
669 .cloned()
670 .collect();
671 let _ = cache::cache_put(root, &hash, &contract_findings);
672 }
673 }
674}
675
676fn run_gates_2_to_9(
679 config: &LintConfig,
680 contracts: &[(String, crate::schema::Contract)],
681 binding: Option<&crate::binding::BindingRegistry>,
682 validation_passed: bool,
683 gates: &mut Vec<GateResult>,
684 all_findings: &mut Vec<LintFinding>,
685) {
686 push_gate(
688 gates,
689 all_findings,
690 || run_audit_gate(contracts),
691 "audit",
692 validation_passed,
693 );
694
695 push_gate(
697 gates,
698 all_findings,
699 || run_score_gate(contracts, binding, config.min_score),
700 "score",
701 validation_passed,
702 );
703
704 push_gate(
706 gates,
707 all_findings,
708 || {
709 let project_root = config.contract_dir.parent().unwrap_or(config.contract_dir);
710 run_verify_gate(contracts, project_root)
711 },
712 "verify",
713 validation_passed,
714 );
715
716 push_gate(
718 gates,
719 all_findings,
720 || run_enforce_gate(contracts),
721 "enforce",
722 validation_passed,
723 );
724
725 push_gate(
727 gates,
728 all_findings,
729 || {
730 let min_level = config
731 .min_level
732 .unwrap_or(crate::schema::EnforcementLevel::Standard);
733 run_enforcement_level_gate(contracts, min_level)
734 },
735 "enforcement-level",
736 validation_passed,
737 );
738
739 push_gate(
741 gates,
742 all_findings,
743 || match (config.binding_path, config.crate_dir) {
744 (Some(bp), Some(cd)) => run_reverse_coverage_gate(bp, cd),
745 _ => (
746 skipped_gate("reverse-coverage", "no --binding or --crate-dir provided"),
747 Vec::new(),
748 ),
749 },
750 "reverse-coverage",
751 validation_passed,
752 );
753
754 let duplicates = duplicate_stems::scan_duplicate_stems(config.contract_dir);
758 let ambiguous = duplicate_stems::ambiguous_stems(&duplicates);
759 push_gate(
760 gates,
761 all_findings,
762 || {
763 let project_root = config.contract_dir.parent().unwrap_or(config.contract_dir);
764 let baseline = duplicate_stems::read_baseline(project_root);
765 duplicate_stems::run_duplicate_stem_gate(&duplicates, &baseline)
766 },
767 "duplicate-stems",
768 validation_passed,
769 );
770
771 push_gate(
773 gates,
774 all_findings,
775 || composition_gate::run_composition_gate(contracts, &ambiguous),
776 "composition",
777 validation_passed,
778 );
779}
780
781fn run_ontology_gates_10_to_22(
784 contract_dir: &Path,
785 validation_passed: bool,
786 gates: &mut Vec<GateResult>,
787 all_findings: &mut Vec<LintFinding>,
788) {
789 let (sigma_gate_result, mut sigma_findings) = sigma_result(contract_dir, validation_passed);
792 gates.push(sigma_gate_result);
793 all_findings.append(&mut sigma_findings);
794
795 let (relations_gate_result, mut relations_findings) =
797 relations_result(contract_dir, validation_passed);
798 gates.push(relations_gate_result);
799 all_findings.append(&mut relations_findings);
800
801 let (shapes_gate_result, mut shapes_findings) = shapes_result(contract_dir, validation_passed);
803 gates.push(shapes_gate_result);
804 all_findings.append(&mut shapes_findings);
805
806 let (valid_under_gate_result, mut valid_under_findings) =
808 valid_under_result(contract_dir, validation_passed);
809 gates.push(valid_under_gate_result);
810 all_findings.append(&mut valid_under_findings);
811
812 for (name, run) in RATCHET_GATES {
815 let (result, mut findings) = ratchet_result(contract_dir, validation_passed, name, run);
816 gates.push(result);
817 all_findings.append(&mut findings);
818 }
819
820 gates.push(challenge_result(contract_dir));
823
824 let (consistency_gate_result, mut consistency_findings) =
827 consistency_result(contract_dir, validation_passed);
828 gates.push(consistency_gate_result);
829 all_findings.append(&mut consistency_findings);
830
831 let (refines_gate_result, mut refines_findings) =
833 refines_result(contract_dir, validation_passed);
834 gates.push(refines_gate_result);
835 all_findings.append(&mut refines_findings);
836
837 let (bindings_gate_result, mut bindings_findings) = ratchet_result(
839 contract_dir,
840 validation_passed,
841 bindings_gate::GATE,
842 bindings_gate::run_bindings_gate,
843 );
844 gates.push(bindings_gate_result);
845 all_findings.append(&mut bindings_findings);
846
847 let (refinement_gate_result, mut refinement_findings) = ratchet_result(
849 contract_dir,
850 validation_passed,
851 refinement_gate::GATE,
852 refinement_gate::run_refinement_gate,
853 );
854 gates.push(refinement_gate_result);
855 all_findings.append(&mut refinement_findings);
856
857 let (evidence_gate_result, mut evidence_findings) =
859 evidence_result(contract_dir, validation_passed);
860 gates.push(evidence_gate_result);
861 all_findings.append(&mut evidence_findings);
862}
863
864fn run_strict_test_binding_gate_if_enabled(
866 config: &LintConfig,
867 contracts: &[(String, crate::schema::Contract)],
868 validation_passed: bool,
869 gates: &mut Vec<GateResult>,
870 all_findings: &mut Vec<LintFinding>,
871) {
872 if !config.strict_test_binding {
873 return;
874 }
875 push_gate(
876 gates,
877 all_findings,
878 || {
879 let project_root = config.contract_dir.parent().unwrap_or(config.contract_dir);
880 strict_test_binding::run_strict_test_binding_gate(
881 contracts,
882 project_root,
883 config.strict,
884 )
885 },
886 "strict-test-binding",
887 validation_passed,
888 );
889}
890
891#[allow(clippy::too_many_arguments)]
894fn finalize_findings(
895 config: &LintConfig,
896 contracts: &[(String, crate::schema::Contract)],
897 binding: Option<&crate::binding::BindingRegistry>,
898 validation_passed: bool,
899 cache_root: Option<&std::path::Path>,
900 stats: &mut cache::CacheStats,
901 all_findings: &mut Vec<LintFinding>,
902 mut validate_findings: Vec<LintFinding>,
903) -> Vec<(String, u64)> {
904 all_findings.append(&mut validate_findings);
905
906 let contract_timings = if validation_passed {
908 per_contract_timings(contracts, binding)
909 } else {
910 Vec::new()
911 };
912
913 let mut stale_findings = check_stale_suppressions(
915 all_findings,
916 &config.suppressed_rules,
917 &config.suppressed_findings,
918 );
919 all_findings.append(&mut stale_findings);
920
921 mark_new_findings(all_findings, config.contract_dir);
923
924 if let Some(root) = cache_root {
926 store_findings_in_cache(root, config, contracts, all_findings, stats);
927 }
928
929 apply_suppressions(all_findings, config);
931 apply_severity_overrides(all_findings, config);
932 if let Some(min_sev) = config.severity_filter {
933 all_findings.retain(|f| f.severity >= min_sev);
934 }
935
936 contract_timings
937}
938
939pub fn run_lint(config: &LintConfig) -> LintReport {
941 let overall_start = Instant::now();
942 let mut gates = Vec::with_capacity(3);
943 let mut all_findings = Vec::new();
944 let mut stats = cache::CacheStats::default();
945
946 let cache_root = if config.no_cache || config.strict_test_binding_only {
948 None
949 } else {
950 Some(cache::cache_dir(config.contract_dir))
951 };
952
953 let (contracts, parse_errors) = load_contracts(config.contract_dir);
954 let binding = load_binding(config.binding_path);
955
956 let (validate_result, validate_findings) = run_validate_gate(&contracts, &parse_errors);
958 let validation_passed = validate_result.passed;
959 gates.push(validate_result);
960
961 if !config.strict_test_binding_only {
962 run_gates_2_to_9(
963 config,
964 &contracts,
965 binding.as_ref(),
966 validation_passed,
967 &mut gates,
968 &mut all_findings,
969 );
970 run_ontology_gates_10_to_22(
971 config.contract_dir,
972 validation_passed,
973 &mut gates,
974 &mut all_findings,
975 );
976 }
977 run_strict_test_binding_gate_if_enabled(
978 config,
979 &contracts,
980 validation_passed,
981 &mut gates,
982 &mut all_findings,
983 );
984
985 let contract_timings = finalize_findings(
986 config,
987 &contracts,
988 binding.as_ref(),
989 validation_passed,
990 cache_root.as_deref(),
991 &mut stats,
992 &mut all_findings,
993 validate_findings,
994 );
995
996 let passed = gates.iter().all(|g| g.passed || g.skipped);
997
998 let mut report = LintReport {
999 passed,
1000 gates,
1001 total_duration_ms: u64::try_from(overall_start.elapsed().as_millis()).unwrap_or(u64::MAX),
1002 findings: all_findings,
1003 cache_stats: stats,
1004 contract_timings,
1005 verdict: Verdict::Unknown(crate::ontology::verdict::Reason::NotArmed),
1006 armed_gates: Vec::new(),
1007 not_armed: Vec::new(),
1008 armed_monotone: None,
1009 armed_shapes_monotone: None,
1010 };
1011 report.arm(&ArmedGates::default_set());
1014 report
1015}
1016
1017pub enum NamedGateOutcome {
1019 UnknownGate,
1021 Sigma(sigma_gate::SigmaOutcome),
1023 Relations(relations_gate::RelationsOutcome),
1025 Shapes(shapes_gate::ShapesOutcome),
1027 Consistency(consistency_gate::ConsistencyOutcome),
1029 Refines(refines_gate::RefinesOutcome),
1031 Tbox(tbox_gate::TboxOutcome),
1033 Evidence(evidence_gate::EvidenceOutcome),
1035 ValidUnder(valid_under_gate::ValidUnderOutcome),
1037 Ratchet(ratchet_gates::RatchetOutcome),
1039 Ran {
1041 result: Box<GateResult>,
1042 findings: Vec<LintFinding>,
1043 },
1044}
1045
1046#[must_use]
1052pub fn run_named_gate(contract_dir: &Path, name: &str) -> NamedGateOutcome {
1053 run_named_gate_with(contract_dir, name, &shapes_gate::ShapesOptions::default())
1054}
1055
1056pub fn run_named_gate_with(
1058 contract_dir: &Path,
1059 name: &str,
1060 shapes_opts: &shapes_gate::ShapesOptions,
1061) -> NamedGateOutcome {
1062 match name {
1063 "evidence" => NamedGateOutcome::Evidence(evidence_gate::run_evidence_gate(contract_dir)),
1064 "relations" => {
1065 NamedGateOutcome::Relations(relations_gate::run_relations_gate(contract_dir))
1066 }
1067 "shapes" => {
1068 NamedGateOutcome::Shapes(shapes_gate::run_shapes_gate_with(contract_dir, shapes_opts))
1069 }
1070 "sigma" => NamedGateOutcome::Sigma(sigma_gate::run_sigma_gate(contract_dir)),
1071 consistency_gate::GATE => {
1072 NamedGateOutcome::Consistency(consistency_gate::run_consistency_gate(contract_dir))
1073 }
1074 refines_gate::GATE => {
1075 NamedGateOutcome::Refines(refines_gate::run_refines_gate(contract_dir))
1076 }
1077 bindings_gate::GATE => {
1078 NamedGateOutcome::Ratchet(bindings_gate::run_bindings_gate(contract_dir))
1079 }
1080 refinement_gate::GATE => {
1081 NamedGateOutcome::Ratchet(refinement_gate::run_refinement_gate(contract_dir))
1082 }
1083 "tbox" => NamedGateOutcome::Tbox(tbox_gate::run_tbox_gate(contract_dir)),
1084 "valid-under" => {
1085 NamedGateOutcome::ValidUnder(valid_under_gate::run_valid_under_gate(contract_dir))
1086 }
1087 "theorem-pairing" => {
1088 NamedGateOutcome::Ratchet(ratchet_gates::run_theorem_pairing_gate(contract_dir))
1089 }
1090 "depends-on-present" => {
1091 NamedGateOutcome::Ratchet(ratchet_gates::run_depends_on_present_gate(contract_dir))
1092 }
1093 "proved-is-derived" => {
1094 NamedGateOutcome::Ratchet(ratchet_gates::run_proved_is_derived_gate(contract_dir))
1095 }
1096 "validate" => {
1097 let (contracts, parse_errors) = load_contracts(contract_dir);
1098 let (result, findings) = run_validate_gate(&contracts, &parse_errors);
1099 NamedGateOutcome::Ran {
1100 result: Box::new(result),
1101 findings,
1102 }
1103 }
1104 _ => NamedGateOutcome::UnknownGate,
1105 }
1106}
1107
1108pub const NAMED_GATES: [&str; 14] = [
1110 bindings_gate::GATE,
1111 refinement_gate::GATE,
1112 "depends-on-present",
1113 "evidence",
1114 consistency_gate::GATE,
1115 "proved-is-derived",
1116 refines_gate::GATE,
1117 "relations",
1118 "shapes",
1119 "sigma",
1120 "tbox",
1121 "theorem-pairing",
1122 "valid-under",
1123 "validate",
1124];
1125
1126type RatchetRun = fn(&Path) -> ratchet_gates::RatchetOutcome;
1128const RATCHET_GATES: [(&str, RatchetRun); 3] = [
1129 ("theorem-pairing", ratchet_gates::run_theorem_pairing_gate),
1130 (
1131 "depends-on-present",
1132 ratchet_gates::run_depends_on_present_gate,
1133 ),
1134 (
1135 "proved-is-derived",
1136 ratchet_gates::run_proved_is_derived_gate,
1137 ),
1138];
1139
1140fn ratchet_result(
1143 contract_dir: &Path,
1144 validation_passed: bool,
1145 name: &str,
1146 run: RatchetRun,
1147) -> (GateResult, Vec<LintFinding>) {
1148 if !validation_passed {
1149 return (skipped_gate(name, "validation failed"), Vec::new());
1150 }
1151 match run(contract_dir) {
1152 ratchet_gates::RatchetOutcome::Ran { result, findings } => (*result, findings),
1153 ratchet_gates::RatchetOutcome::Declined(why) => (skipped_gate(name, &why), Vec::new()),
1154 }
1155}
1156
1157fn sigma_result(contract_dir: &Path, validation_passed: bool) -> (GateResult, Vec<LintFinding>) {
1161 if !validation_passed {
1162 return (skipped_gate("sigma", "validation failed"), Vec::new());
1163 }
1164 match sigma_gate::run_sigma_gate(contract_dir) {
1165 sigma_gate::SigmaOutcome::Ran { result, findings } => (*result, findings),
1166 sigma_gate::SigmaOutcome::NoSigma => (
1167 skipped_gate("sigma", "no contracts/ontology.yaml"),
1168 Vec::new(),
1169 ),
1170 sigma_gate::SigmaOutcome::Malformed(e) => (
1171 skipped_gate("sigma", &format!("Σ is malformed: {e}")),
1172 Vec::new(),
1173 ),
1174 }
1175}
1176
1177fn evidence_result(contract_dir: &Path, validation_passed: bool) -> (GateResult, Vec<LintFinding>) {
1180 if !validation_passed {
1181 return (skipped_gate("evidence", "validation failed"), Vec::new());
1182 }
1183 match evidence_gate::run_evidence_gate(contract_dir) {
1184 evidence_gate::EvidenceOutcome::Ran { result, findings } => (*result, findings),
1185 evidence_gate::EvidenceOutcome::NoSigma => (
1186 skipped_gate("evidence", "no contracts/ontology.yaml"),
1187 Vec::new(),
1188 ),
1189 evidence_gate::EvidenceOutcome::Malformed(e) => (
1190 skipped_gate("evidence", &format!("Σ is malformed: {e}")),
1191 Vec::new(),
1192 ),
1193 evidence_gate::EvidenceOutcome::NoEvidence { contracts_checked } => (
1194 skipped_gate(
1195 "evidence",
1196 &format!(
1197 "no evidence block in {contracts_checked} contracts — R-2: zero is a decline"
1198 ),
1199 ),
1200 Vec::new(),
1201 ),
1202 }
1203}
1204
1205fn valid_under_result(
1208 contract_dir: &Path,
1209 validation_passed: bool,
1210) -> (GateResult, Vec<LintFinding>) {
1211 if !validation_passed {
1212 return (skipped_gate("valid-under", "validation failed"), Vec::new());
1213 }
1214 match valid_under_gate::run_valid_under_gate(contract_dir) {
1215 valid_under_gate::ValidUnderOutcome::Ran { result, findings } => (*result, findings),
1216 valid_under_gate::ValidUnderOutcome::NoSigma => (
1217 skipped_gate("valid-under", "no contracts/ontology.yaml"),
1218 Vec::new(),
1219 ),
1220 valid_under_gate::ValidUnderOutcome::Malformed(e) => (
1221 skipped_gate("valid-under", &format!("Σ is malformed: {e}")),
1222 Vec::new(),
1223 ),
1224 valid_under_gate::ValidUnderOutcome::NoKernels { contracts_checked } => (
1225 skipped_gate(
1226 "valid-under",
1227 &format!("no kernel-kind contract and no valid_under in {contracts_checked} contracts — R-2: zero is a decline"),
1228 ),
1229 Vec::new(),
1230 ),
1231 }
1232}
1233
1234fn relations_result(
1237 contract_dir: &Path,
1238 validation_passed: bool,
1239) -> (GateResult, Vec<LintFinding>) {
1240 if !validation_passed {
1241 return (skipped_gate("relations", "validation failed"), Vec::new());
1242 }
1243 match relations_gate::run_relations_gate(contract_dir) {
1244 relations_gate::RelationsOutcome::Ran { result, findings } => (*result, findings),
1245 relations_gate::RelationsOutcome::NoSigma => (
1246 skipped_gate("relations", "no contracts/ontology.yaml"),
1247 Vec::new(),
1248 ),
1249 relations_gate::RelationsOutcome::Malformed(e) => (
1250 skipped_gate("relations", &format!("Σ is malformed: {e}")),
1251 Vec::new(),
1252 ),
1253 relations_gate::RelationsOutcome::NoRelations {
1254 contracts_checked,
1255 legacy_depends_on,
1256 } => (
1257 skipped_gate(
1258 "relations",
1259 &format!("no typed relations in {contracts_checked} contracts ({legacy_depends_on} legacy metadata.depends_on edges) — R-2: zero is a decline"),
1260 ),
1261 Vec::new(),
1262 ),
1263 }
1264}
1265
1266fn shapes_result(contract_dir: &Path, validation_passed: bool) -> (GateResult, Vec<LintFinding>) {
1269 if !validation_passed {
1270 return (skipped_gate("shapes", "validation failed"), Vec::new());
1271 }
1272 match shapes_gate::run_shapes_gate(contract_dir) {
1273 shapes_gate::ShapesOutcome::Ran { result, findings } => (*result, findings),
1274 shapes_gate::ShapesOutcome::Unsupported(e) => (skipped_gate("shapes", &format!("{e}")), Vec::new()),
1275 shapes_gate::ShapesOutcome::ExtractFailed(e) => (skipped_gate("shapes", &format!("{e}")), Vec::new()),
1276 shapes_gate::ShapesOutcome::NoShapes { contracts_checked } => (
1277 skipped_gate("shapes", &format!("no `shape:` block in {contracts_checked} contracts — R-2: zero is a decline")),
1278 Vec::new(),
1279 ),
1280 shapes_gate::ShapesOutcome::NoFocus { shapes_n } => (
1281 skipped_gate("shapes", &format!("{shapes_n} shape(s), no focus node")),
1282 Vec::new(),
1283 ),
1284 shapes_gate::ShapesOutcome::WrongCorpus { shapes_n, expected, found, refused } => (
1285 skipped_gate("shapes", &format!("extract:parity-receipt matched {found} focus node(s) and evidence/parity/EXPECTED_RECEIPTS says {expected} ({shapes_n} shape(s)){} — an extractor that saw the wrong corpus reports the same \"no violations\" as one that saw all of it", if refused.is_empty() { String::new() } else { format!("; refused: {}", refused.join("; ")) })),
1286 Vec::new(),
1287 ),
1288 shapes_gate::ShapesOutcome::HarnessBroken { causes } => (
1289 skipped_gate("shapes", &format!("the CRUX harness measured itself, not apr: {}", causes.join("; "))),
1290 Vec::new(),
1291 ),
1292 shapes_gate::ShapesOutcome::NoReceipts { shapes_n, dir } => (
1293 skipped_gate("shapes", &format!("{shapes_n} shape(s) resolve receipts and the tree holds none under {dir}/ — R-2: unmeasured is a decline")),
1294 Vec::new(),
1295 ),
1296 shapes_gate::ShapesOutcome::EmptyDomain { shapes_n } => (
1297 skipped_gate("shapes", &format!("capability-cells: the required-cell domain D is empty ({shapes_n} shape(s)) — R-2: zero is a decline, never a pass")),
1298 Vec::new(),
1299 ),
1300 shapes_gate::ShapesOutcome::PositiveControlFailed { shapes_n, focus_nodes_n, which } => (
1301 skipped_gate("shapes", &format!("positive control {which} did not fire ({shapes_n} shape(s), {focus_nodes_n} focus node(s)) — the gate cannot reject")),
1302 Vec::new(),
1303 ),
1304 shapes_gate::ShapesOutcome::Differential { shapes_n, focus_nodes_n, passed, n, failed } => {
1305 let mut g = skipped_gate(
1306 "shapes",
1307 &format!(
1308 "W3C SHACL-Core differential: {passed} of {n} vendored case(s) pass; failed: {} ({shapes_n} shape(s), {focus_nodes_n} focus node(s)) — the validator disagrees with the standard, so no corpus verdict",
1309 failed.join(" | ")
1310 ),
1311 );
1312 g.verdict = Verdict::Unknown(crate::ontology::verdict::Reason::Differential);
1313 (g, Vec::new())
1314 }
1315 }
1316}
1317
1318fn consistency_result(
1321 contract_dir: &Path,
1322 validation_passed: bool,
1323) -> (GateResult, Vec<LintFinding>) {
1324 if !validation_passed {
1325 return (
1326 skipped_gate(consistency_gate::GATE, "validation failed"),
1327 Vec::new(),
1328 );
1329 }
1330 let outcome = consistency_gate::run_consistency_gate(contract_dir);
1331 if let consistency_gate::ConsistencyOutcome::Ran { result, findings } = outcome {
1332 return (*result, findings);
1333 }
1334 let mut g = skipped_gate(consistency_gate::GATE, &consistency_gate::why(&outcome));
1335 if let Some(reason) = consistency_gate::decline_reason(&outcome) {
1336 g.verdict = Verdict::Unknown(reason);
1337 }
1338 (g, Vec::new())
1339}
1340
1341fn refines_result(contract_dir: &Path, validation_passed: bool) -> (GateResult, Vec<LintFinding>) {
1343 if !validation_passed {
1344 return (
1345 skipped_gate(refines_gate::GATE, "validation failed"),
1346 Vec::new(),
1347 );
1348 }
1349 let outcome = refines_gate::run_refines_gate(contract_dir);
1350 if let refines_gate::RefinesOutcome::Ran { result, findings } = outcome {
1351 return (*result, findings);
1352 }
1353 let mut g = skipped_gate(refines_gate::GATE, &refines_gate::why(&outcome));
1354 if let Some(reason) = refines_gate::decline_reason(&outcome) {
1355 g.verdict = Verdict::Unknown(reason);
1356 }
1357 (g, Vec::new())
1358}
1359
1360fn challenge_result(contract_dir: &Path) -> GateResult {
1363 const NAME: &str = "challenge-fresh";
1364 let start = Instant::now();
1365 let root = crate::ontology::extract::repo_root(contract_dir);
1366 let Some(lean) = crate::ontology::extract::lean::base_under(&root) else {
1367 return skipped_gate(NAME, "no Lean theorem base under the repo root");
1368 };
1369 let r = match crate::discharge::challenge::render(&lean, contract_dir) {
1370 Ok(r) => r,
1371 Err(e) => return skipped_gate(NAME, &format!("the Lean tree does not load: {e}")),
1372 };
1373 if r.files.is_empty() && r.unrestated.is_empty() {
1374 return skipped_gate(
1375 NAME,
1376 "zero challenges: no contract binds a theorem — R-2: zero is a decline",
1377 );
1378 }
1379 let stale = crate::discharge::challenge::diff(&lean, &r);
1380 let unrestated: Vec<String> = r
1381 .unrestated
1382 .iter()
1383 .map(|(c, fqn, why)| format!("{c}: {fqn}: {why}"))
1384 .collect();
1385 let passed = stale.is_empty() && unrestated.is_empty();
1386 GateResult {
1387 name: NAME.into(),
1388 passed,
1389 skipped: false,
1390 verdict: Verdict::from_gate(passed, false),
1391 duration_ms: u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX),
1392 detail: GateDetail::Validate {
1394 contracts: r.files.len(),
1395 errors: stale.len() + unrestated.len(),
1396 warnings: 0,
1397 error_messages: stale.iter().chain(&unrestated).cloned().collect(),
1398 },
1399 extra: Some(GateExtra::ChallengeFresh {
1400 lean_dir: lean.display().to_string(),
1401 files: r.files.len(),
1402 stale,
1403 unrestated,
1404 }),
1405 }
1406}
1407
1408fn skipped_gate(name: &str, reason: &str) -> GateResult {
1409 GateResult {
1410 name: name.into(),
1411 passed: false,
1412 skipped: true,
1413 verdict: Verdict::from_gate(false, true),
1414 duration_ms: 0,
1415 detail: GateDetail::Skipped {
1416 reason: reason.into(),
1417 },
1418 extra: None,
1419 }
1420}
1421
1422fn apply_suppressions(findings: &mut [LintFinding], config: &LintConfig) {
1423 for f in findings.iter_mut() {
1424 if config.suppressed_rules.iter().any(|r| r == &f.rule_id) {
1425 f.suppressed = true;
1426 f.suppression_reason = Some("Suppressed by --suppress-rule".into());
1427 }
1428 if let Some(ref stem) = f.contract_stem {
1429 if config.suppressed_findings.iter().any(|s| s == stem) {
1430 f.suppressed = true;
1431 f.suppression_reason = Some("Suppressed by --suppress".into());
1432 }
1433 }
1434 if config.suppressed_files.iter().any(|p| f.file.contains(p)) {
1435 f.suppressed = true;
1436 f.suppression_reason = Some("Suppressed by --suppress-file".into());
1437 }
1438 }
1439}
1440
1441fn pv_state_dir(contract_dir: &Path) -> std::path::PathBuf {
1443 contract_dir.parent().unwrap_or(contract_dir).join(".pv")
1444}
1445
1446fn mark_new_findings(findings: &mut [LintFinding], contract_dir: &Path) {
1449 let state_dir = pv_state_dir(contract_dir);
1450 let previous_path = state_dir.join("lint-previous.json");
1451
1452 let previous: HashSet<String> = std::fs::read_to_string(&previous_path)
1454 .ok()
1455 .and_then(|s| serde_json::from_str(&s).ok())
1456 .unwrap_or_default();
1457
1458 let mut current = HashSet::new();
1460 for f in findings.iter_mut() {
1461 let fp = f.fingerprint();
1462 if !previous.contains(&fp) {
1463 f.is_new = true;
1464 }
1465 current.insert(fp);
1466 }
1467
1468 if let Err(e) = std::fs::create_dir_all(&state_dir) {
1470 eprintln!("pv lint: cannot create {}: {e}", state_dir.display());
1471 return;
1472 }
1473 if let Ok(json) = serde_json::to_string(¤t) {
1474 let _ = std::fs::write(&previous_path, json);
1475 }
1476}
1477
1478fn apply_severity_overrides(findings: &mut [LintFinding], config: &LintConfig) {
1479 for f in findings.iter_mut() {
1480 if let Some(&sev) = config.severity_overrides.get(&f.rule_id) {
1481 f.severity = sev;
1482 }
1483 }
1484 if config.strict {
1485 for f in findings.iter_mut() {
1486 if f.severity == RuleSeverity::Warning {
1487 f.severity = RuleSeverity::Error;
1488 }
1489 }
1490 }
1491}
1492
1493#[cfg(test)]
1494#[path = "mod_tests.rs"]
1495mod tests;