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}
575
576impl<'a> LintConfig<'a> {
577 pub fn new(contract_dir: &'a Path, binding_path: Option<&'a Path>, min_score: f64) -> Self {
579 Self {
580 contract_dir,
581 binding_path,
582 min_score,
583 severity_filter: None,
584 severity_overrides: HashMap::new(),
585 suppressed_findings: Vec::new(),
586 suppressed_rules: Vec::new(),
587 suppressed_files: Vec::new(),
588 strict: false,
589 no_cache: false,
590 cache_stats: false,
591 crate_dir: None,
592 min_level: None,
593 strict_test_binding: false,
594 }
595 }
596}
597
598fn push_gate<F>(
603 gates: &mut Vec<GateResult>,
604 all_findings: &mut Vec<LintFinding>,
605 run: F,
606 name: &str,
607 validation_passed: bool,
608) where
609 F: FnOnce() -> (GateResult, Vec<LintFinding>),
610{
611 let (result, mut findings) = if validation_passed {
612 run()
613 } else {
614 (skipped_gate(name, "validation failed"), Vec::new())
615 };
616 gates.push(result);
617 all_findings.append(&mut findings);
618}
619
620fn per_contract_timings(
623 contracts: &[(String, crate::schema::Contract)],
624 binding: Option<&crate::binding::BindingRegistry>,
625) -> Vec<(String, u64)> {
626 let mut contract_timings: Vec<(String, u64)> = Vec::with_capacity(contracts.len());
627 for (stem, contract) in contracts {
628 let ct_start = Instant::now();
629 let _ = crate::schema::validate_contract(contract);
631 let _ = crate::audit::audit_contract(contract);
633 let _ = crate::scoring::score_contract(contract, binding, stem);
635 let ct_ms = u64::try_from(ct_start.elapsed().as_micros() / 1000).unwrap_or(0);
636 contract_timings.push((format!("{stem}.yaml"), ct_ms));
637 }
638 contract_timings.sort_by_key(|b| std::cmp::Reverse(b.1));
640 contract_timings
641}
642
643fn store_findings_in_cache(
645 root: &std::path::Path,
646 config: &LintConfig,
647 contracts: &[(String, crate::schema::Contract)],
648 all_findings: &[LintFinding],
649 stats: &mut cache::CacheStats,
650) {
651 let rule_cfg = format!("{:?}{:?}", config.severity_overrides, config.strict);
652 for (stem, _) in contracts {
653 stats.total += 1;
654 let yaml_path = config.contract_dir.join(format!("{stem}.yaml"));
655 let yaml_content = std::fs::read_to_string(&yaml_path).unwrap_or_default();
656 let hash = cache::content_hash(&yaml_content, &rule_cfg);
657 if cache::cache_get(root, &hash).is_some() {
658 stats.hits += 1;
659 } else {
660 stats.misses += 1;
661 let contract_findings: Vec<_> = all_findings
662 .iter()
663 .filter(|f| f.contract_stem.as_deref() == Some(stem.as_str()))
664 .cloned()
665 .collect();
666 let _ = cache::cache_put(root, &hash, &contract_findings);
667 }
668 }
669}
670
671fn run_gates_2_to_9(
674 config: &LintConfig,
675 contracts: &[(String, crate::schema::Contract)],
676 binding: Option<&crate::binding::BindingRegistry>,
677 validation_passed: bool,
678 gates: &mut Vec<GateResult>,
679 all_findings: &mut Vec<LintFinding>,
680) {
681 push_gate(
683 gates,
684 all_findings,
685 || run_audit_gate(contracts),
686 "audit",
687 validation_passed,
688 );
689
690 push_gate(
692 gates,
693 all_findings,
694 || run_score_gate(contracts, binding, config.min_score),
695 "score",
696 validation_passed,
697 );
698
699 push_gate(
701 gates,
702 all_findings,
703 || {
704 let project_root = config.contract_dir.parent().unwrap_or(config.contract_dir);
705 run_verify_gate(contracts, project_root)
706 },
707 "verify",
708 validation_passed,
709 );
710
711 push_gate(
713 gates,
714 all_findings,
715 || run_enforce_gate(contracts),
716 "enforce",
717 validation_passed,
718 );
719
720 push_gate(
722 gates,
723 all_findings,
724 || {
725 let min_level = config
726 .min_level
727 .unwrap_or(crate::schema::EnforcementLevel::Standard);
728 run_enforcement_level_gate(contracts, min_level)
729 },
730 "enforcement-level",
731 validation_passed,
732 );
733
734 push_gate(
736 gates,
737 all_findings,
738 || match (config.binding_path, config.crate_dir) {
739 (Some(bp), Some(cd)) => run_reverse_coverage_gate(bp, cd),
740 _ => (
741 skipped_gate("reverse-coverage", "no --binding or --crate-dir provided"),
742 Vec::new(),
743 ),
744 },
745 "reverse-coverage",
746 validation_passed,
747 );
748
749 let duplicates = duplicate_stems::scan_duplicate_stems(config.contract_dir);
753 let ambiguous = duplicate_stems::ambiguous_stems(&duplicates);
754 push_gate(
755 gates,
756 all_findings,
757 || {
758 let project_root = config.contract_dir.parent().unwrap_or(config.contract_dir);
759 let baseline = duplicate_stems::read_baseline(project_root);
760 duplicate_stems::run_duplicate_stem_gate(&duplicates, &baseline)
761 },
762 "duplicate-stems",
763 validation_passed,
764 );
765
766 push_gate(
768 gates,
769 all_findings,
770 || composition_gate::run_composition_gate(contracts, &ambiguous),
771 "composition",
772 validation_passed,
773 );
774}
775
776fn run_ontology_gates_10_to_22(
779 contract_dir: &Path,
780 validation_passed: bool,
781 gates: &mut Vec<GateResult>,
782 all_findings: &mut Vec<LintFinding>,
783) {
784 let (sigma_gate_result, mut sigma_findings) = sigma_result(contract_dir, validation_passed);
787 gates.push(sigma_gate_result);
788 all_findings.append(&mut sigma_findings);
789
790 let (relations_gate_result, mut relations_findings) =
792 relations_result(contract_dir, validation_passed);
793 gates.push(relations_gate_result);
794 all_findings.append(&mut relations_findings);
795
796 let (shapes_gate_result, mut shapes_findings) = shapes_result(contract_dir, validation_passed);
798 gates.push(shapes_gate_result);
799 all_findings.append(&mut shapes_findings);
800
801 let (valid_under_gate_result, mut valid_under_findings) =
803 valid_under_result(contract_dir, validation_passed);
804 gates.push(valid_under_gate_result);
805 all_findings.append(&mut valid_under_findings);
806
807 for (name, run) in RATCHET_GATES {
810 let (result, mut findings) = ratchet_result(contract_dir, validation_passed, name, run);
811 gates.push(result);
812 all_findings.append(&mut findings);
813 }
814
815 gates.push(challenge_result(contract_dir));
818
819 let (consistency_gate_result, mut consistency_findings) =
822 consistency_result(contract_dir, validation_passed);
823 gates.push(consistency_gate_result);
824 all_findings.append(&mut consistency_findings);
825
826 let (refines_gate_result, mut refines_findings) =
828 refines_result(contract_dir, validation_passed);
829 gates.push(refines_gate_result);
830 all_findings.append(&mut refines_findings);
831
832 let (bindings_gate_result, mut bindings_findings) = ratchet_result(
834 contract_dir,
835 validation_passed,
836 bindings_gate::GATE,
837 bindings_gate::run_bindings_gate,
838 );
839 gates.push(bindings_gate_result);
840 all_findings.append(&mut bindings_findings);
841
842 let (refinement_gate_result, mut refinement_findings) = ratchet_result(
844 contract_dir,
845 validation_passed,
846 refinement_gate::GATE,
847 refinement_gate::run_refinement_gate,
848 );
849 gates.push(refinement_gate_result);
850 all_findings.append(&mut refinement_findings);
851
852 let (evidence_gate_result, mut evidence_findings) =
854 evidence_result(contract_dir, validation_passed);
855 gates.push(evidence_gate_result);
856 all_findings.append(&mut evidence_findings);
857}
858
859fn run_strict_test_binding_gate_if_enabled(
861 config: &LintConfig,
862 contracts: &[(String, crate::schema::Contract)],
863 validation_passed: bool,
864 gates: &mut Vec<GateResult>,
865 all_findings: &mut Vec<LintFinding>,
866) {
867 if !config.strict_test_binding {
868 return;
869 }
870 push_gate(
871 gates,
872 all_findings,
873 || {
874 let project_root = config.contract_dir.parent().unwrap_or(config.contract_dir);
875 strict_test_binding::run_strict_test_binding_gate(
876 contracts,
877 project_root,
878 config.strict,
879 )
880 },
881 "strict-test-binding",
882 validation_passed,
883 );
884}
885
886#[allow(clippy::too_many_arguments)]
889fn finalize_findings(
890 config: &LintConfig,
891 contracts: &[(String, crate::schema::Contract)],
892 binding: Option<&crate::binding::BindingRegistry>,
893 validation_passed: bool,
894 cache_root: Option<&std::path::Path>,
895 stats: &mut cache::CacheStats,
896 all_findings: &mut Vec<LintFinding>,
897 mut validate_findings: Vec<LintFinding>,
898) -> Vec<(String, u64)> {
899 all_findings.append(&mut validate_findings);
900
901 let contract_timings = if validation_passed {
903 per_contract_timings(contracts, binding)
904 } else {
905 Vec::new()
906 };
907
908 let mut stale_findings = check_stale_suppressions(
910 all_findings,
911 &config.suppressed_rules,
912 &config.suppressed_findings,
913 );
914 all_findings.append(&mut stale_findings);
915
916 mark_new_findings(all_findings, config.contract_dir);
918
919 if let Some(root) = cache_root {
921 store_findings_in_cache(root, config, contracts, all_findings, stats);
922 }
923
924 apply_suppressions(all_findings, config);
926 apply_severity_overrides(all_findings, config);
927 if let Some(min_sev) = config.severity_filter {
928 all_findings.retain(|f| f.severity >= min_sev);
929 }
930
931 contract_timings
932}
933
934pub fn run_lint(config: &LintConfig) -> LintReport {
936 let overall_start = Instant::now();
937 let mut gates = Vec::with_capacity(3);
938 let mut all_findings = Vec::new();
939 let mut stats = cache::CacheStats::default();
940
941 let cache_root = if config.no_cache {
942 None
943 } else {
944 Some(cache::cache_dir(config.contract_dir))
945 };
946
947 let (contracts, parse_errors) = load_contracts(config.contract_dir);
948 let binding = load_binding(config.binding_path);
949
950 let (validate_result, validate_findings) = run_validate_gate(&contracts, &parse_errors);
952 let validation_passed = validate_result.passed;
953 gates.push(validate_result);
954
955 run_gates_2_to_9(
956 config,
957 &contracts,
958 binding.as_ref(),
959 validation_passed,
960 &mut gates,
961 &mut all_findings,
962 );
963 run_ontology_gates_10_to_22(
964 config.contract_dir,
965 validation_passed,
966 &mut gates,
967 &mut all_findings,
968 );
969 run_strict_test_binding_gate_if_enabled(
970 config,
971 &contracts,
972 validation_passed,
973 &mut gates,
974 &mut all_findings,
975 );
976
977 let contract_timings = finalize_findings(
978 config,
979 &contracts,
980 binding.as_ref(),
981 validation_passed,
982 cache_root.as_deref(),
983 &mut stats,
984 &mut all_findings,
985 validate_findings,
986 );
987
988 let passed = gates.iter().all(|g| g.passed || g.skipped);
989
990 let mut report = LintReport {
991 passed,
992 gates,
993 total_duration_ms: u64::try_from(overall_start.elapsed().as_millis()).unwrap_or(u64::MAX),
994 findings: all_findings,
995 cache_stats: stats,
996 contract_timings,
997 verdict: Verdict::Unknown(crate::ontology::verdict::Reason::NotArmed),
998 armed_gates: Vec::new(),
999 not_armed: Vec::new(),
1000 armed_monotone: None,
1001 armed_shapes_monotone: None,
1002 };
1003 report.arm(&ArmedGates::default_set());
1006 report
1007}
1008
1009pub enum NamedGateOutcome {
1011 UnknownGate,
1013 Sigma(sigma_gate::SigmaOutcome),
1015 Relations(relations_gate::RelationsOutcome),
1017 Shapes(shapes_gate::ShapesOutcome),
1019 Consistency(consistency_gate::ConsistencyOutcome),
1021 Refines(refines_gate::RefinesOutcome),
1023 Tbox(tbox_gate::TboxOutcome),
1025 Evidence(evidence_gate::EvidenceOutcome),
1027 ValidUnder(valid_under_gate::ValidUnderOutcome),
1029 Ratchet(ratchet_gates::RatchetOutcome),
1031 Ran {
1033 result: Box<GateResult>,
1034 findings: Vec<LintFinding>,
1035 },
1036}
1037
1038#[must_use]
1044pub fn run_named_gate(contract_dir: &Path, name: &str) -> NamedGateOutcome {
1045 run_named_gate_with(contract_dir, name, &shapes_gate::ShapesOptions::default())
1046}
1047
1048pub fn run_named_gate_with(
1050 contract_dir: &Path,
1051 name: &str,
1052 shapes_opts: &shapes_gate::ShapesOptions,
1053) -> NamedGateOutcome {
1054 match name {
1055 "evidence" => NamedGateOutcome::Evidence(evidence_gate::run_evidence_gate(contract_dir)),
1056 "relations" => {
1057 NamedGateOutcome::Relations(relations_gate::run_relations_gate(contract_dir))
1058 }
1059 "shapes" => {
1060 NamedGateOutcome::Shapes(shapes_gate::run_shapes_gate_with(contract_dir, shapes_opts))
1061 }
1062 "sigma" => NamedGateOutcome::Sigma(sigma_gate::run_sigma_gate(contract_dir)),
1063 consistency_gate::GATE => {
1064 NamedGateOutcome::Consistency(consistency_gate::run_consistency_gate(contract_dir))
1065 }
1066 refines_gate::GATE => {
1067 NamedGateOutcome::Refines(refines_gate::run_refines_gate(contract_dir))
1068 }
1069 bindings_gate::GATE => {
1070 NamedGateOutcome::Ratchet(bindings_gate::run_bindings_gate(contract_dir))
1071 }
1072 refinement_gate::GATE => {
1073 NamedGateOutcome::Ratchet(refinement_gate::run_refinement_gate(contract_dir))
1074 }
1075 "tbox" => NamedGateOutcome::Tbox(tbox_gate::run_tbox_gate(contract_dir)),
1076 "valid-under" => {
1077 NamedGateOutcome::ValidUnder(valid_under_gate::run_valid_under_gate(contract_dir))
1078 }
1079 "theorem-pairing" => {
1080 NamedGateOutcome::Ratchet(ratchet_gates::run_theorem_pairing_gate(contract_dir))
1081 }
1082 "depends-on-present" => {
1083 NamedGateOutcome::Ratchet(ratchet_gates::run_depends_on_present_gate(contract_dir))
1084 }
1085 "proved-is-derived" => {
1086 NamedGateOutcome::Ratchet(ratchet_gates::run_proved_is_derived_gate(contract_dir))
1087 }
1088 "validate" => {
1089 let (contracts, parse_errors) = load_contracts(contract_dir);
1090 let (result, findings) = run_validate_gate(&contracts, &parse_errors);
1091 NamedGateOutcome::Ran {
1092 result: Box::new(result),
1093 findings,
1094 }
1095 }
1096 _ => NamedGateOutcome::UnknownGate,
1097 }
1098}
1099
1100pub const NAMED_GATES: [&str; 14] = [
1102 bindings_gate::GATE,
1103 refinement_gate::GATE,
1104 "depends-on-present",
1105 "evidence",
1106 consistency_gate::GATE,
1107 "proved-is-derived",
1108 refines_gate::GATE,
1109 "relations",
1110 "shapes",
1111 "sigma",
1112 "tbox",
1113 "theorem-pairing",
1114 "valid-under",
1115 "validate",
1116];
1117
1118type RatchetRun = fn(&Path) -> ratchet_gates::RatchetOutcome;
1120const RATCHET_GATES: [(&str, RatchetRun); 3] = [
1121 ("theorem-pairing", ratchet_gates::run_theorem_pairing_gate),
1122 (
1123 "depends-on-present",
1124 ratchet_gates::run_depends_on_present_gate,
1125 ),
1126 (
1127 "proved-is-derived",
1128 ratchet_gates::run_proved_is_derived_gate,
1129 ),
1130];
1131
1132fn ratchet_result(
1135 contract_dir: &Path,
1136 validation_passed: bool,
1137 name: &str,
1138 run: RatchetRun,
1139) -> (GateResult, Vec<LintFinding>) {
1140 if !validation_passed {
1141 return (skipped_gate(name, "validation failed"), Vec::new());
1142 }
1143 match run(contract_dir) {
1144 ratchet_gates::RatchetOutcome::Ran { result, findings } => (*result, findings),
1145 ratchet_gates::RatchetOutcome::Declined(why) => (skipped_gate(name, &why), Vec::new()),
1146 }
1147}
1148
1149fn sigma_result(contract_dir: &Path, validation_passed: bool) -> (GateResult, Vec<LintFinding>) {
1153 if !validation_passed {
1154 return (skipped_gate("sigma", "validation failed"), Vec::new());
1155 }
1156 match sigma_gate::run_sigma_gate(contract_dir) {
1157 sigma_gate::SigmaOutcome::Ran { result, findings } => (*result, findings),
1158 sigma_gate::SigmaOutcome::NoSigma => (
1159 skipped_gate("sigma", "no contracts/ontology.yaml"),
1160 Vec::new(),
1161 ),
1162 sigma_gate::SigmaOutcome::Malformed(e) => (
1163 skipped_gate("sigma", &format!("Σ is malformed: {e}")),
1164 Vec::new(),
1165 ),
1166 }
1167}
1168
1169fn evidence_result(contract_dir: &Path, validation_passed: bool) -> (GateResult, Vec<LintFinding>) {
1172 if !validation_passed {
1173 return (skipped_gate("evidence", "validation failed"), Vec::new());
1174 }
1175 match evidence_gate::run_evidence_gate(contract_dir) {
1176 evidence_gate::EvidenceOutcome::Ran { result, findings } => (*result, findings),
1177 evidence_gate::EvidenceOutcome::NoSigma => (
1178 skipped_gate("evidence", "no contracts/ontology.yaml"),
1179 Vec::new(),
1180 ),
1181 evidence_gate::EvidenceOutcome::Malformed(e) => (
1182 skipped_gate("evidence", &format!("Σ is malformed: {e}")),
1183 Vec::new(),
1184 ),
1185 evidence_gate::EvidenceOutcome::NoEvidence { contracts_checked } => (
1186 skipped_gate(
1187 "evidence",
1188 &format!(
1189 "no evidence block in {contracts_checked} contracts — R-2: zero is a decline"
1190 ),
1191 ),
1192 Vec::new(),
1193 ),
1194 }
1195}
1196
1197fn valid_under_result(
1200 contract_dir: &Path,
1201 validation_passed: bool,
1202) -> (GateResult, Vec<LintFinding>) {
1203 if !validation_passed {
1204 return (skipped_gate("valid-under", "validation failed"), Vec::new());
1205 }
1206 match valid_under_gate::run_valid_under_gate(contract_dir) {
1207 valid_under_gate::ValidUnderOutcome::Ran { result, findings } => (*result, findings),
1208 valid_under_gate::ValidUnderOutcome::NoSigma => (
1209 skipped_gate("valid-under", "no contracts/ontology.yaml"),
1210 Vec::new(),
1211 ),
1212 valid_under_gate::ValidUnderOutcome::Malformed(e) => (
1213 skipped_gate("valid-under", &format!("Σ is malformed: {e}")),
1214 Vec::new(),
1215 ),
1216 valid_under_gate::ValidUnderOutcome::NoKernels { contracts_checked } => (
1217 skipped_gate(
1218 "valid-under",
1219 &format!("no kernel-kind contract and no valid_under in {contracts_checked} contracts — R-2: zero is a decline"),
1220 ),
1221 Vec::new(),
1222 ),
1223 }
1224}
1225
1226fn relations_result(
1229 contract_dir: &Path,
1230 validation_passed: bool,
1231) -> (GateResult, Vec<LintFinding>) {
1232 if !validation_passed {
1233 return (skipped_gate("relations", "validation failed"), Vec::new());
1234 }
1235 match relations_gate::run_relations_gate(contract_dir) {
1236 relations_gate::RelationsOutcome::Ran { result, findings } => (*result, findings),
1237 relations_gate::RelationsOutcome::NoSigma => (
1238 skipped_gate("relations", "no contracts/ontology.yaml"),
1239 Vec::new(),
1240 ),
1241 relations_gate::RelationsOutcome::Malformed(e) => (
1242 skipped_gate("relations", &format!("Σ is malformed: {e}")),
1243 Vec::new(),
1244 ),
1245 relations_gate::RelationsOutcome::NoRelations {
1246 contracts_checked,
1247 legacy_depends_on,
1248 } => (
1249 skipped_gate(
1250 "relations",
1251 &format!("no typed relations in {contracts_checked} contracts ({legacy_depends_on} legacy metadata.depends_on edges) — R-2: zero is a decline"),
1252 ),
1253 Vec::new(),
1254 ),
1255 }
1256}
1257
1258fn shapes_result(contract_dir: &Path, validation_passed: bool) -> (GateResult, Vec<LintFinding>) {
1261 if !validation_passed {
1262 return (skipped_gate("shapes", "validation failed"), Vec::new());
1263 }
1264 match shapes_gate::run_shapes_gate(contract_dir) {
1265 shapes_gate::ShapesOutcome::Ran { result, findings } => (*result, findings),
1266 shapes_gate::ShapesOutcome::Unsupported(e) => (skipped_gate("shapes", &format!("{e}")), Vec::new()),
1267 shapes_gate::ShapesOutcome::ExtractFailed(e) => (skipped_gate("shapes", &format!("{e}")), Vec::new()),
1268 shapes_gate::ShapesOutcome::NoShapes { contracts_checked } => (
1269 skipped_gate("shapes", &format!("no `shape:` block in {contracts_checked} contracts — R-2: zero is a decline")),
1270 Vec::new(),
1271 ),
1272 shapes_gate::ShapesOutcome::NoFocus { shapes_n } => (
1273 skipped_gate("shapes", &format!("{shapes_n} shape(s), no focus node")),
1274 Vec::new(),
1275 ),
1276 shapes_gate::ShapesOutcome::WrongCorpus { shapes_n, expected, found, refused } => (
1277 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("; ")) })),
1278 Vec::new(),
1279 ),
1280 shapes_gate::ShapesOutcome::HarnessBroken { causes } => (
1281 skipped_gate("shapes", &format!("the CRUX harness measured itself, not apr: {}", causes.join("; "))),
1282 Vec::new(),
1283 ),
1284 shapes_gate::ShapesOutcome::NoReceipts { shapes_n, dir } => (
1285 skipped_gate("shapes", &format!("{shapes_n} shape(s) resolve receipts and the tree holds none under {dir}/ — R-2: unmeasured is a decline")),
1286 Vec::new(),
1287 ),
1288 shapes_gate::ShapesOutcome::EmptyDomain { shapes_n } => (
1289 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")),
1290 Vec::new(),
1291 ),
1292 shapes_gate::ShapesOutcome::PositiveControlFailed { shapes_n, focus_nodes_n, which } => (
1293 skipped_gate("shapes", &format!("positive control {which} did not fire ({shapes_n} shape(s), {focus_nodes_n} focus node(s)) — the gate cannot reject")),
1294 Vec::new(),
1295 ),
1296 shapes_gate::ShapesOutcome::Differential { shapes_n, focus_nodes_n, passed, n, failed } => {
1297 let mut g = skipped_gate(
1298 "shapes",
1299 &format!(
1300 "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",
1301 failed.join(" | ")
1302 ),
1303 );
1304 g.verdict = Verdict::Unknown(crate::ontology::verdict::Reason::Differential);
1305 (g, Vec::new())
1306 }
1307 }
1308}
1309
1310fn consistency_result(
1313 contract_dir: &Path,
1314 validation_passed: bool,
1315) -> (GateResult, Vec<LintFinding>) {
1316 if !validation_passed {
1317 return (
1318 skipped_gate(consistency_gate::GATE, "validation failed"),
1319 Vec::new(),
1320 );
1321 }
1322 let outcome = consistency_gate::run_consistency_gate(contract_dir);
1323 if let consistency_gate::ConsistencyOutcome::Ran { result, findings } = outcome {
1324 return (*result, findings);
1325 }
1326 let mut g = skipped_gate(consistency_gate::GATE, &consistency_gate::why(&outcome));
1327 if let Some(reason) = consistency_gate::decline_reason(&outcome) {
1328 g.verdict = Verdict::Unknown(reason);
1329 }
1330 (g, Vec::new())
1331}
1332
1333fn refines_result(contract_dir: &Path, validation_passed: bool) -> (GateResult, Vec<LintFinding>) {
1335 if !validation_passed {
1336 return (
1337 skipped_gate(refines_gate::GATE, "validation failed"),
1338 Vec::new(),
1339 );
1340 }
1341 let outcome = refines_gate::run_refines_gate(contract_dir);
1342 if let refines_gate::RefinesOutcome::Ran { result, findings } = outcome {
1343 return (*result, findings);
1344 }
1345 let mut g = skipped_gate(refines_gate::GATE, &refines_gate::why(&outcome));
1346 if let Some(reason) = refines_gate::decline_reason(&outcome) {
1347 g.verdict = Verdict::Unknown(reason);
1348 }
1349 (g, Vec::new())
1350}
1351
1352fn challenge_result(contract_dir: &Path) -> GateResult {
1355 const NAME: &str = "challenge-fresh";
1356 let start = Instant::now();
1357 let root = crate::ontology::extract::repo_root(contract_dir);
1358 let Some(lean) = crate::ontology::extract::lean::base_under(&root) else {
1359 return skipped_gate(NAME, "no Lean theorem base under the repo root");
1360 };
1361 let r = match crate::discharge::challenge::render(&lean, contract_dir) {
1362 Ok(r) => r,
1363 Err(e) => return skipped_gate(NAME, &format!("the Lean tree does not load: {e}")),
1364 };
1365 if r.files.is_empty() && r.unrestated.is_empty() {
1366 return skipped_gate(
1367 NAME,
1368 "zero challenges: no contract binds a theorem — R-2: zero is a decline",
1369 );
1370 }
1371 let stale = crate::discharge::challenge::diff(&lean, &r);
1372 let unrestated: Vec<String> = r
1373 .unrestated
1374 .iter()
1375 .map(|(c, fqn, why)| format!("{c}: {fqn}: {why}"))
1376 .collect();
1377 let passed = stale.is_empty() && unrestated.is_empty();
1378 GateResult {
1379 name: NAME.into(),
1380 passed,
1381 skipped: false,
1382 verdict: Verdict::from_gate(passed, false),
1383 duration_ms: u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX),
1384 detail: GateDetail::Validate {
1386 contracts: r.files.len(),
1387 errors: stale.len() + unrestated.len(),
1388 warnings: 0,
1389 error_messages: stale.iter().chain(&unrestated).cloned().collect(),
1390 },
1391 extra: Some(GateExtra::ChallengeFresh {
1392 lean_dir: lean.display().to_string(),
1393 files: r.files.len(),
1394 stale,
1395 unrestated,
1396 }),
1397 }
1398}
1399
1400fn skipped_gate(name: &str, reason: &str) -> GateResult {
1401 GateResult {
1402 name: name.into(),
1403 passed: false,
1404 skipped: true,
1405 verdict: Verdict::from_gate(false, true),
1406 duration_ms: 0,
1407 detail: GateDetail::Skipped {
1408 reason: reason.into(),
1409 },
1410 extra: None,
1411 }
1412}
1413
1414fn apply_suppressions(findings: &mut [LintFinding], config: &LintConfig) {
1415 for f in findings.iter_mut() {
1416 if config.suppressed_rules.iter().any(|r| r == &f.rule_id) {
1417 f.suppressed = true;
1418 f.suppression_reason = Some("Suppressed by --suppress-rule".into());
1419 }
1420 if let Some(ref stem) = f.contract_stem {
1421 if config.suppressed_findings.iter().any(|s| s == stem) {
1422 f.suppressed = true;
1423 f.suppression_reason = Some("Suppressed by --suppress".into());
1424 }
1425 }
1426 if config.suppressed_files.iter().any(|p| f.file.contains(p)) {
1427 f.suppressed = true;
1428 f.suppression_reason = Some("Suppressed by --suppress-file".into());
1429 }
1430 }
1431}
1432
1433fn pv_state_dir(contract_dir: &Path) -> std::path::PathBuf {
1435 contract_dir.parent().unwrap_or(contract_dir).join(".pv")
1436}
1437
1438fn mark_new_findings(findings: &mut [LintFinding], contract_dir: &Path) {
1441 let state_dir = pv_state_dir(contract_dir);
1442 let previous_path = state_dir.join("lint-previous.json");
1443
1444 let previous: HashSet<String> = std::fs::read_to_string(&previous_path)
1446 .ok()
1447 .and_then(|s| serde_json::from_str(&s).ok())
1448 .unwrap_or_default();
1449
1450 let mut current = HashSet::new();
1452 for f in findings.iter_mut() {
1453 let fp = f.fingerprint();
1454 if !previous.contains(&fp) {
1455 f.is_new = true;
1456 }
1457 current.insert(fp);
1458 }
1459
1460 if let Err(e) = std::fs::create_dir_all(&state_dir) {
1462 eprintln!("pv lint: cannot create {}: {e}", state_dir.display());
1463 return;
1464 }
1465 if let Ok(json) = serde_json::to_string(¤t) {
1466 let _ = std::fs::write(&previous_path, json);
1467 }
1468}
1469
1470fn apply_severity_overrides(findings: &mut [LintFinding], config: &LintConfig) {
1471 for f in findings.iter_mut() {
1472 if let Some(&sev) = config.severity_overrides.get(&f.rule_id) {
1473 f.severity = sev;
1474 }
1475 }
1476 if config.strict {
1477 for f in findings.iter_mut() {
1478 if f.severity == RuleSeverity::Warning {
1479 f.severity = RuleSeverity::Error;
1480 }
1481 }
1482 }
1483}
1484
1485#[cfg(test)]
1486#[path = "mod_tests.rs"]
1487mod tests;