#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ContractProfile {
Universal,
Rust,
Pmat,
Stack { manifest_path: PathBuf },
Custom { claim_ids: Vec<String> },
}
impl ContractProfile {
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn detect(project_path: &Path) -> Self {
let config_path = project_path.join(".pmat-work").join("config.toml");
if config_path.exists() {
if let Ok(config) = DbcConfig::load_from_path(&config_path) {
if let Some(profile) = config.profile_override {
return profile;
}
}
}
let stack_path = project_path.join(".dbc-stack.toml");
if stack_path.exists() {
return ContractProfile::Stack {
manifest_path: stack_path,
};
}
let pmat_db = project_path.join(".pmat").join("context.db");
let pmat_idx = project_path.join(".pmat").join("context.idx");
if pmat_db.exists() || pmat_idx.exists() {
return ContractProfile::Pmat;
}
if project_path.join("Cargo.toml").exists() {
return ContractProfile::Rust;
}
if project_path.join(".git").exists() {
return ContractProfile::Universal;
}
ContractProfile::Universal
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn name(&self) -> &str {
match self {
ContractProfile::Universal => "Universal",
ContractProfile::Rust => "Rust",
ContractProfile::Pmat => "Pmat",
ContractProfile::Stack { .. } => "Stack",
ContractProfile::Custom { .. } => "Custom",
}
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn required_tools(&self) -> Vec<RequiredTool> {
match self {
ContractProfile::Universal => vec![RequiredTool {
name: "git".to_string(),
claim_id: "require.compiles".to_string(),
install_hint: "Install git from https://git-scm.com".to_string(),
}],
ContractProfile::Rust => vec![
RequiredTool {
name: "cargo".to_string(),
claim_id: "require.compiles".to_string(),
install_hint: "Install Rust from https://rustup.rs".to_string(),
},
RequiredTool {
name: "cargo-clippy".to_string(),
claim_id: "invariant.lint".to_string(),
install_hint: "rustup component add clippy".to_string(),
},
RequiredTool {
name: "cargo-llvm-cov".to_string(),
claim_id: "ensure.coverage".to_string(),
install_hint: "cargo +nightly install cargo-llvm-cov".to_string(),
},
RequiredTool {
name: "cargo-audit".to_string(),
claim_id: "ensure.supply_chain".to_string(),
install_hint: "cargo install cargo-audit".to_string(),
},
],
ContractProfile::Pmat => vec![
RequiredTool {
name: "cargo".to_string(),
claim_id: "require.compiles".to_string(),
install_hint: "Install Rust from https://rustup.rs".to_string(),
},
RequiredTool {
name: "cargo-clippy".to_string(),
claim_id: "invariant.lint".to_string(),
install_hint: "rustup component add clippy".to_string(),
},
RequiredTool {
name: "cargo-llvm-cov".to_string(),
claim_id: "ensure.coverage".to_string(),
install_hint: "cargo +nightly install cargo-llvm-cov".to_string(),
},
RequiredTool {
name: "cargo-audit".to_string(),
claim_id: "ensure.supply_chain".to_string(),
install_hint: "cargo install cargo-audit".to_string(),
},
RequiredTool {
name: "pmat".to_string(),
claim_id: "invariant.satd".to_string(),
install_hint: "cargo install pmat".to_string(),
},
],
ContractProfile::Stack { .. } | ContractProfile::Custom { .. } => {
vec![]
}
}
}
}
#[derive(Debug, Clone)]
pub struct RequiredTool {
pub name: String,
pub claim_id: String,
pub install_hint: String,
}
impl RequiredTool {
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn is_available(&self) -> bool {
which_tool(&self.name)
}
}
#[derive(Debug, Clone)]
pub struct MissingTool {
pub name: String,
pub claim_id: String,
pub install_hint: String,
}
fn which_tool(name: &str) -> bool {
match name {
"cargo-clippy" => {
std::process::Command::new("cargo")
.args(["clippy", "--version"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
"cargo-llvm-cov" => {
std::process::Command::new("cargo")
.args(["llvm-cov", "--version"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
"cargo-audit" => {
std::process::Command::new("cargo")
.args(["audit", "--version"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
_ => {
std::process::Command::new("which")
.arg(name)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
}
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn check_toolchain(profile: &ContractProfile, _project_path: &Path) -> Vec<MissingTool> {
profile
.required_tools()
.into_iter()
.filter(|tool| !tool.is_available())
.map(|tool| MissingTool {
name: tool.name,
claim_id: tool.claim_id,
install_hint: tool.install_hint,
})
.collect()
}
#[derive(Debug, Clone, Default)]
pub struct DbcConfig {
pub profile_override: Option<ContractProfile>,
pub thresholds: DbcThresholdOverrides,
pub rescue_enabled: Option<bool>,
pub pre_commit_hook: bool,
}
#[derive(Debug, Clone, Default)]
pub struct DbcThresholdOverrides {
pub coverage_pct: Option<f64>,
pub max_complexity: Option<u32>,
pub max_file_lines: Option<usize>,
}
impl DbcConfig {
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn load(project_path: &Path) -> Self {
let config_path = project_path.join(".pmat-work").join("config.toml");
Self::load_from_path(&config_path).unwrap_or_default()
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn load_from_path(path: &Path) -> Result<Self> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read config: {}", path.display()))?;
Self::parse_toml(&content)
}
fn parse_toml(content: &str) -> Result<Self> {
let table: toml::Table = content
.parse()
.context("Failed to parse config.toml")?;
let mut config = DbcConfig::default();
if let Some(dbc) = table.get("dbc").and_then(|v| v.as_table()) {
if let Some(profile_str) = dbc.get("profile").and_then(|v| v.as_str()) {
config.profile_override = match profile_str {
"universal" => Some(ContractProfile::Universal),
"rust" => Some(ContractProfile::Rust),
"pmat" => Some(ContractProfile::Pmat),
"custom" => {
let claims = dbc
.get("claims")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
Some(ContractProfile::Custom { claim_ids: claims })
}
_ => None,
};
}
if let Some(thresholds) = dbc.get("thresholds").and_then(|v| v.as_table()) {
if let Some(cov) = thresholds.get("coverage_pct").and_then(|v| v.as_float()) {
config.thresholds.coverage_pct = Some(cov);
}
if let Some(cx) = thresholds.get("max_complexity").and_then(|v| v.as_integer()) {
config.thresholds.max_complexity = Some(cx as u32);
}
if let Some(fl) = thresholds.get("max_file_lines").and_then(|v| v.as_integer()) {
config.thresholds.max_file_lines = Some(fl as usize);
}
}
if let Some(rescue) = dbc.get("rescue").and_then(|v| v.as_table()) {
if let Some(enabled) = rescue.get("enabled").and_then(|v| v.as_bool()) {
config.rescue_enabled = Some(enabled);
}
}
if let Some(checkpoints) = dbc.get("checkpoints").and_then(|v| v.as_table()) {
if let Some(hook) = checkpoints.get("pre_commit_hook").and_then(|v| v.as_bool()) {
config.pre_commit_hook = hook;
}
}
}
Ok(config)
}
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn claims_for_profile(
profile: &ContractProfile,
config: &DbcConfig,
) -> Vec<ContractClause> {
let max_complexity = config.thresholds.max_complexity.unwrap_or(20) as f64;
let max_file_lines = config.thresholds.max_file_lines.unwrap_or(500) as f64;
let coverage_pct = config.thresholds.coverage_pct.unwrap_or(95.0);
match profile {
ContractProfile::Universal => universal_claims(max_file_lines),
ContractProfile::Rust => rust_claims(max_complexity, max_file_lines, coverage_pct),
ContractProfile::Pmat => pmat_claims(max_complexity, max_file_lines, coverage_pct),
ContractProfile::Custom { claim_ids } => {
let all = pmat_claims(max_complexity, max_file_lines, coverage_pct);
all.into_iter()
.filter(|c| claim_ids.contains(&c.id))
.collect()
}
ContractProfile::Stack { manifest_path } => {
match std::fs::read_to_string(manifest_path) {
Ok(content) => match StackManifest::parse(&content) {
Ok(manifest) => manifest.resolve_base_claims(config),
Err(_) => universal_claims(max_file_lines),
},
Err(_) => universal_claims(max_file_lines),
}
}
}
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn classify_claims(
clauses: &[ContractClause],
) -> (Vec<ContractClause>, Vec<ContractClause>, Vec<ContractClause>) {
let mut require = Vec::new();
let mut ensure = Vec::new();
let mut invariant = Vec::new();
for clause in clauses {
match clause.kind {
ClauseKind::Require => require.push(clause.clone()),
ClauseKind::Ensure => ensure.push(clause.clone()),
ClauseKind::Invariant => invariant.push(clause.clone()),
}
}
(require, ensure, invariant)
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn apply_exclusions(
clauses: Vec<ContractClause>,
without: &[String],
) -> (Vec<ContractClause>, Vec<ExcludedClaim>) {
let (excluded_clauses, active): (Vec<_>, Vec<_>) =
clauses.into_iter().partition(|c| without.contains(&c.id));
let excluded = excluded_clauses
.into_iter()
.map(|c| ExcludedClaim {
flag: format!("--without {}", c.id),
id: c.id,
reason: "developer_excluded".to_string(),
})
.collect();
(active, excluded)
}
#[cfg(test)]
mod profile_tests {
use super::*;
use tempfile::TempDir;
fn clause(id: &str, kind: ClauseKind) -> ContractClause {
ContractClause {
id: id.to_string(),
description: format!("desc-{id}"),
kind,
falsification_method: FalsificationMethod::ManifestIntegrity,
threshold: None,
blocking: false,
source: ClauseSource::Default,
}
}
#[test]
fn test_profile_name_universal() {
assert_eq!(ContractProfile::Universal.name(), "Universal");
}
#[test]
fn test_profile_name_rust() {
assert_eq!(ContractProfile::Rust.name(), "Rust");
}
#[test]
fn test_profile_name_pmat() {
assert_eq!(ContractProfile::Pmat.name(), "Pmat");
}
#[test]
fn test_profile_name_stack() {
let p = ContractProfile::Stack {
manifest_path: PathBuf::from(".dbc-stack.toml"),
};
assert_eq!(p.name(), "Stack");
}
#[test]
fn test_profile_name_custom() {
let p = ContractProfile::Custom {
claim_ids: vec!["x".to_string()],
};
assert_eq!(p.name(), "Custom");
}
#[test]
fn test_required_tools_universal() {
let tools = ContractProfile::Universal.required_tools();
assert_eq!(tools.len(), 1);
assert_eq!(tools[0].name, "git");
}
#[test]
fn test_required_tools_rust_includes_cargo_clippy_llvmcov_audit() {
let tools = ContractProfile::Rust.required_tools();
let names: Vec<_> = tools.iter().map(|t| t.name.as_str()).collect();
assert!(names.contains(&"cargo"));
assert!(names.contains(&"cargo-clippy"));
assert!(names.contains(&"cargo-llvm-cov"));
assert!(names.contains(&"cargo-audit"));
}
#[test]
fn test_detect_returns_universal_when_dir_empty() {
let tmp = TempDir::new().unwrap();
let p = ContractProfile::detect(tmp.path());
assert!(matches!(p, ContractProfile::Universal));
}
#[test]
fn test_detect_returns_rust_when_cargo_toml_present() {
let tmp = TempDir::new().unwrap();
std::fs::write(tmp.path().join("Cargo.toml"), "[package]\nname=\"x\"").unwrap();
let p = ContractProfile::detect(tmp.path());
assert!(matches!(p, ContractProfile::Rust));
}
#[test]
fn test_detect_returns_universal_when_only_git_present() {
let tmp = TempDir::new().unwrap();
std::fs::create_dir(tmp.path().join(".git")).unwrap();
let p = ContractProfile::detect(tmp.path());
assert!(matches!(p, ContractProfile::Universal));
}
#[test]
fn test_detect_returns_pmat_when_context_db_present() {
let tmp = TempDir::new().unwrap();
let pmat_dir = tmp.path().join(".pmat");
std::fs::create_dir(&pmat_dir).unwrap();
std::fs::write(pmat_dir.join("context.db"), "").unwrap();
let p = ContractProfile::detect(tmp.path());
assert!(matches!(p, ContractProfile::Pmat));
}
#[test]
fn test_detect_returns_pmat_when_context_idx_present() {
let tmp = TempDir::new().unwrap();
let pmat_dir = tmp.path().join(".pmat");
std::fs::create_dir(&pmat_dir).unwrap();
std::fs::create_dir(pmat_dir.join("context.idx")).unwrap();
let p = ContractProfile::detect(tmp.path());
assert!(matches!(p, ContractProfile::Pmat));
}
#[test]
fn test_detect_returns_stack_when_dbc_stack_toml_present() {
let tmp = TempDir::new().unwrap();
std::fs::write(tmp.path().join(".dbc-stack.toml"), "extends=[]").unwrap();
let p = ContractProfile::detect(tmp.path());
assert!(matches!(p, ContractProfile::Stack { .. }));
}
#[test]
fn test_detect_stack_wins_over_pmat() {
let tmp = TempDir::new().unwrap();
let pmat_dir = tmp.path().join(".pmat");
std::fs::create_dir(&pmat_dir).unwrap();
std::fs::write(pmat_dir.join("context.db"), "").unwrap();
std::fs::write(tmp.path().join(".dbc-stack.toml"), "").unwrap();
let p = ContractProfile::detect(tmp.path());
assert!(matches!(p, ContractProfile::Stack { .. }));
}
#[test]
fn test_detect_pmat_wins_over_rust() {
let tmp = TempDir::new().unwrap();
std::fs::write(tmp.path().join("Cargo.toml"), "[package]\nname=\"x\"").unwrap();
let pmat_dir = tmp.path().join(".pmat");
std::fs::create_dir(&pmat_dir).unwrap();
std::fs::write(pmat_dir.join("context.db"), "").unwrap();
let p = ContractProfile::detect(tmp.path());
assert!(matches!(p, ContractProfile::Pmat));
}
#[test]
fn test_classify_claims_empty() {
let (req, ens, inv) = classify_claims(&[]);
assert!(req.is_empty());
assert!(ens.is_empty());
assert!(inv.is_empty());
}
#[test]
fn test_classify_claims_partitions_by_kind() {
let clauses = vec![
clause("r1", ClauseKind::Require),
clause("r2", ClauseKind::Require),
clause("e1", ClauseKind::Ensure),
clause("i1", ClauseKind::Invariant),
clause("i2", ClauseKind::Invariant),
clause("i3", ClauseKind::Invariant),
];
let (req, ens, inv) = classify_claims(&clauses);
assert_eq!(req.len(), 2);
assert_eq!(ens.len(), 1);
assert_eq!(inv.len(), 3);
assert!(req.iter().all(|c| matches!(c.kind, ClauseKind::Require)));
assert!(ens.iter().all(|c| matches!(c.kind, ClauseKind::Ensure)));
assert!(inv.iter().all(|c| matches!(c.kind, ClauseKind::Invariant)));
}
#[test]
fn test_apply_exclusions_empty_without_keeps_all() {
let clauses = vec![
clause("a", ClauseKind::Require),
clause("b", ClauseKind::Ensure),
];
let (active, excluded) = apply_exclusions(clauses, &[]);
assert_eq!(active.len(), 2);
assert_eq!(excluded.len(), 0);
}
#[test]
fn test_apply_exclusions_drops_matching_ids() {
let clauses = vec![
clause("a", ClauseKind::Require),
clause("b", ClauseKind::Ensure),
clause("c", ClauseKind::Invariant),
];
let (active, excluded) =
apply_exclusions(clauses, &["a".to_string(), "c".to_string()]);
assert_eq!(active.len(), 1);
assert_eq!(active[0].id, "b");
assert_eq!(excluded.len(), 2);
assert!(excluded.iter().any(|e| e.id == "a"));
assert!(excluded.iter().any(|e| e.id == "c"));
}
#[test]
fn test_apply_exclusions_excluded_carries_flag_and_reason() {
let clauses = vec![clause("ensure.coverage", ClauseKind::Ensure)];
let (_, excluded) = apply_exclusions(clauses, &["ensure.coverage".to_string()]);
assert_eq!(excluded.len(), 1);
assert_eq!(excluded[0].flag, "--without ensure.coverage");
assert_eq!(excluded[0].reason, "developer_excluded");
}
#[test]
fn test_apply_exclusions_no_match_keeps_all() {
let clauses = vec![clause("a", ClauseKind::Require)];
let (active, excluded) = apply_exclusions(clauses, &["nonexistent".to_string()]);
assert_eq!(active.len(), 1);
assert_eq!(excluded.len(), 0);
}
}