#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LintConfig {
#[serde(default)]
pub min_score: f64,
#[serde(default)]
pub strict: bool,
#[serde(default)]
pub rules: HashMap<String, String>,
#[serde(default)]
pub suppress: Vec<String>,
#[serde(default)]
pub trend: LintTrendConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LintTrendConfig {
#[serde(default = "lint_config_default_true")]
pub enabled: bool,
#[serde(default = "lint_config_default_retention_days")]
pub retention_days: u32,
#[serde(default = "lint_config_default_drift_threshold")]
pub drift_threshold: f64,
}
fn lint_config_default_true() -> bool {
true
}
fn lint_config_default_retention_days() -> u32 {
90
}
fn lint_config_default_drift_threshold() -> f64 {
0.05
}
impl Default for LintTrendConfig {
fn default() -> Self {
Self {
enabled: true,
retention_days: 90,
drift_threshold: 0.05,
}
}
}
impl Default for LintConfig {
fn default() -> Self {
Self {
min_score: 0.0,
strict: false,
rules: HashMap::new(),
suppress: Vec::new(),
trend: LintTrendConfig::default(),
}
}
}
impl LintConfig {
#[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("dbc-lint.toml");
if !config_path.exists() {
return Self::default();
}
match std::fs::read_to_string(&config_path) {
Ok(content) => Self::parse(&content).unwrap_or_default(),
Err(_) => Self::default(),
}
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn parse(toml_str: &str) -> Result<Self> {
let mut config = Self::default();
for line in toml_str.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') || line.starts_with('[') {
continue;
}
if let Some((key, value)) = line.split_once('=') {
let key = key.trim();
let value = value.trim().trim_matches('"');
match key {
"min_score" => {
if let Ok(v) = value.parse::<f64>() {
config.min_score = v;
}
}
"strict" => {
config.strict = value == "true";
}
"retention_days" => {
if let Ok(v) = value.parse::<u32>() {
config.trend.retention_days = v;
}
}
"drift_threshold" => {
if let Ok(v) = value.parse::<f64>() {
config.trend.drift_threshold = v;
}
}
"enabled" => {
config.trend.enabled = value == "true";
}
_ => {
if key.starts_with("DBC-") {
config.rules.insert(key.to_string(), value.to_string());
}
if key == "suppress" || key == "rules" {
let items: Vec<String> = value
.trim_matches(|c| c == '[' || c == ']')
.split(',')
.map(|s| s.trim().trim_matches('"').to_string())
.filter(|s| !s.is_empty())
.collect();
config.suppress.extend(items);
}
}
}
}
}
Ok(config)
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn effective_severity(&self, rule_id: &str, default: LintSeverity) -> Option<LintSeverity> {
if self.suppress.contains(&rule_id.to_string()) {
return None;
}
if let Some(override_sev) = self.rules.get(rule_id) {
return match override_sev.as_str() {
"off" => None,
"error" => Some(LintSeverity::Error),
"warning" => Some(LintSeverity::Warning),
"info" => Some(LintSeverity::Info),
_ => Some(default),
};
}
if self.strict && default == LintSeverity::Warning {
return Some(LintSeverity::Error);
}
Some(default)
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn is_suppressed(&self, rule_id: &str) -> bool {
self.suppress.contains(&rule_id.to_string())
|| self
.rules
.get(rule_id)
.map(|v| v == "off")
.unwrap_or(false)
}
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn apply_lint_config(report: &LintReport, config: &LintConfig) -> LintReport {
let mut findings: Vec<LintFinding> = report
.findings
.iter()
.filter_map(|f| {
let default_severity = f.severity;
config.effective_severity(&f.rule_id, default_severity).map(|new_severity| LintFinding {
rule_id: f.rule_id.clone(),
severity: new_severity,
message: f.message.clone(),
clause_id: f.clause_id.clone(),
})
})
.collect();
if config.min_score > 0.0 {
}
let error_count = findings
.iter()
.filter(|f| f.severity == LintSeverity::Error)
.count();
let warning_count = findings
.iter()
.filter(|f| f.severity == LintSeverity::Warning)
.count();
let info_count = findings
.iter()
.filter(|f| f.severity == LintSeverity::Info)
.count();
findings.sort_by_key(|f| match f.severity {
LintSeverity::Error => 0,
LintSeverity::Warning => 1,
LintSeverity::Info => 2,
});
LintReport {
passed: error_count == 0,
findings,
error_count,
warning_count,
info_count,
}
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn changed_contracts_since(project_path: &Path, git_ref: &str) -> Vec<String> {
let output = std::process::Command::new("git")
.args(["diff", "--name-only", git_ref, "HEAD"])
.current_dir(project_path)
.output();
let output = match output {
Ok(o) if o.status.success() => o,
_ => return Vec::new(),
};
let stdout = String::from_utf8_lossy(&output.stdout);
let mut changed_ids = Vec::new();
for line in stdout.lines() {
if let Some(rest) = line.strip_prefix(".pmat-work/") {
if let Some(id) = rest.split('/').next() {
if id != "ledger.jsonl" && id != "trusted-stacks.json" && !changed_ids.contains(&id.to_string()) {
changed_ids.push(id.to_string());
}
}
}
}
changed_ids
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CodebaseScore {
pub contract_count: usize,
pub contract_coverage: f64,
pub mean_score: f64,
pub min_score: f64,
pub max_score: f64,
pub mean_drift: f64,
pub lint_pass_rate: f64,
pub composite: f64,
pub grade: ScoreGrade,
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn compute_codebase_score(project_path: &Path) -> CodebaseScore {
let pmat_work = project_path.join(".pmat-work");
if !pmat_work.exists() {
return CodebaseScore {
contract_count: 0,
contract_coverage: 0.0,
mean_score: 0.0,
min_score: 0.0,
max_score: 0.0,
mean_drift: 0.0,
lint_pass_rate: 0.0,
composite: 0.0,
grade: ScoreGrade::F,
};
}
let mut scores = Vec::new();
let mut drift_bounds = Vec::new();
let mut lint_passes = 0usize;
let mut total_contracts = 0usize;
if let Ok(entries) = std::fs::read_dir(&pmat_work) {
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
let contract_path = path.join("contract.json");
if !contract_path.exists() {
continue;
}
let item_id = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown");
if let Ok(contract) = WorkContract::load(project_path, item_id) {
total_contracts += 1;
let score = score_contract(&contract, project_path);
scores.push(score.total);
let drift = compute_drift_metrics(&contract, project_path);
drift_bounds.push(drift.bounded_drift);
let lint = lint_contract(&contract, project_path, 0.0);
if lint.passed {
lint_passes += 1;
}
}
}
}
if total_contracts == 0 {
return CodebaseScore {
contract_count: 0,
contract_coverage: 0.0,
mean_score: 0.0,
min_score: 0.0,
max_score: 0.0,
mean_drift: 0.0,
lint_pass_rate: 0.0,
composite: 0.0,
grade: ScoreGrade::F,
};
}
let mean_score = scores.iter().sum::<f64>() / total_contracts as f64;
let min_score = scores
.iter()
.cloned()
.fold(f64::INFINITY, f64::min);
let max_score = scores
.iter()
.cloned()
.fold(f64::NEG_INFINITY, f64::max);
let contract_coverage = scores.iter().filter(|s| **s >= 0.60).count() as f64
/ total_contracts as f64;
let mean_drift = drift_bounds.iter().sum::<f64>() / total_contracts as f64;
let lint_pass_rate = lint_passes as f64 / total_contracts as f64;
let composite = 0.40 * mean_score
+ 0.25 * contract_coverage
+ 0.20 * lint_pass_rate
+ 0.15 * (1.0 - mean_drift).max(0.0);
CodebaseScore {
contract_count: total_contracts,
contract_coverage,
mean_score,
min_score,
max_score,
mean_drift,
lint_pass_rate,
composite,
grade: ScoreGrade::from_score(composite),
}
}