fn clause_ids_from_json(contract: &serde_json::Value) -> Vec<String> {
let mut ids = Vec::new();
for section in ["require", "ensure", "invariant"] {
if let Some(arr) = contract.get(section).and_then(|v| v.as_array()) {
for c in arr {
if let Some(id) = c.get("id").and_then(|v| v.as_str()) {
ids.push(id.to_string());
}
}
}
}
ids
}
pub(crate) fn check_attribute_has_generated_module(project_path: &Path) -> ComplianceCheck {
let name = "CB-1631: Attribute Has Generated Module";
let usages = collect_attribute_usages(project_path);
if usages.is_empty() {
return ComplianceCheck {
name: name.into(),
status: CheckStatus::Skip,
message: "No `#[pmat_work_contract]` attribute usage found in `src/`".into(),
severity: Severity::Info,
};
}
let mut missing: Vec<String> = Vec::new();
for usage in &usages {
let generated = project_path
.join("contracts")
.join("work")
.join(format!("{}.rs", usage.id));
if !generated.exists() {
missing.push(format!(
"{}: attribute id=\"{}\" but {} is missing",
usage.file.display(),
usage.id,
generated.display()
));
}
}
if missing.is_empty() {
ComplianceCheck {
name: name.into(),
status: CheckStatus::Pass,
message: format!(
"All {} `#[pmat_work_contract]` usage(s) resolve to generated modules",
usages.len()
),
severity: Severity::Info,
}
} else {
ComplianceCheck {
name: name.into(),
status: CheckStatus::Fail,
message: format!("Missing generated module(s): {}", missing.join("; ")),
severity: Severity::Error,
}
}
}
pub(crate) fn check_attribute_clause_ids_exist(project_path: &Path) -> ComplianceCheck {
let name = "CB-1632: Attribute Clause IDs Exist";
let usages = collect_attribute_usages(project_path);
if usages.is_empty() {
return ComplianceCheck {
name: name.into(),
status: CheckStatus::Skip,
message: "No `#[pmat_work_contract]` attribute usage found in `src/`".into(),
severity: Severity::Info,
};
}
let mut mismatches: Vec<String> = Vec::new();
for usage in &usages {
let Some(contract) = load_contract_json(project_path, &usage.id) else {
mismatches.push(format!(
"{}: ticket `{}` has no `.pmat-work/{}/contract.json`",
usage.file.display(),
usage.id,
usage.id
));
continue;
};
let ids = clause_ids_from_json(&contract);
for claim in usage.requires.iter().chain(usage.ensures.iter()) {
if !ids.iter().any(|i| i == claim) {
mismatches.push(format!(
"{}: attribute references `{}` on {} but no matching clause id",
usage.file.display(),
claim,
usage.id
));
}
}
}
if mismatches.is_empty() {
ComplianceCheck {
name: name.into(),
status: CheckStatus::Pass,
message: format!(
"All {} attribute clause id(s) resolve to ticket clauses",
usages.len()
),
severity: Severity::Info,
}
} else {
ComplianceCheck {
name: name.into(),
status: CheckStatus::Fail,
message: format!("Clause id mismatches: {}", mismatches.join("; ")),
severity: Severity::Error,
}
}
}