pub struct FalsificationLedger {
work_dir: PathBuf,
}
impl FalsificationLedger {
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn new(project_path: &Path) -> Self {
Self {
work_dir: project_path.join(".pmat-work"),
}
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn persist_receipt(&self, receipt: &FalsificationReceipt) -> Result<PathBuf> {
let falsification_dir = self
.work_dir
.join(&receipt.work_item_id)
.join("falsification");
std::fs::create_dir_all(&falsification_dir)
.context("Failed to create falsification directory")?;
let safe_ts = receipt.timestamp.replace(':', "-");
let filename = format!("receipt-{}.json", safe_ts);
let path = falsification_dir.join(filename);
let json = serde_json::to_string_pretty(receipt).context("Failed to serialize receipt")?;
std::fs::write(&path, json).context("Failed to write receipt")?;
Ok(path)
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn append_to_ledger(&self, receipt: &FalsificationReceipt) -> Result<()> {
std::fs::create_dir_all(&self.work_dir).context("Failed to create .pmat-work directory")?;
let ledger_path = self.work_dir.join("ledger.jsonl");
let entry = LedgerEntry::from_receipt(receipt);
let mut line = serde_json::to_string(&entry).context("Failed to serialize ledger entry")?;
line.push('\n');
use std::io::Write;
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&ledger_path)
.context("Failed to open ledger.jsonl")?;
file.write_all(line.as_bytes())
.context("Failed to append to ledger")?;
Ok(())
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn latest_receipt(&self, work_item_id: &str) -> Result<Option<FalsificationReceipt>> {
let falsification_dir = self.work_dir.join(work_item_id).join("falsification");
if !falsification_dir.exists() {
return Ok(None);
}
let mut receipt_files: Vec<PathBuf> = std::fs::read_dir(&falsification_dir)?
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| {
p.extension().map(|e| e == "json").unwrap_or(false)
&& p.file_name()
.and_then(|n| n.to_str())
.map(|n| n.starts_with("receipt-"))
.unwrap_or(false)
})
.collect();
receipt_files.sort();
let Some(latest_path) = receipt_files.last() else {
return Ok(None);
};
let content =
std::fs::read_to_string(latest_path).context("Failed to read latest receipt")?;
let receipt: FalsificationReceipt =
serde_json::from_str(&content).context("Failed to parse receipt JSON")?;
Ok(Some(receipt))
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn has_fresh_receipt(&self, work_item_id: &str, current_sha: &str) -> Result<bool> {
let receipt = self.latest_receipt(work_item_id)?;
match receipt {
Some(r) => {
Ok(r.is_fresh(current_sha, MAX_RECEIPT_AGE_SECS) && r.summary.allows_completion)
}
None => Ok(false),
}
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn verify_integrity(&self, work_item_id: &str) -> Result<IntegrityReport> {
let falsification_dir = self.work_dir.join(work_item_id).join("falsification");
if !falsification_dir.exists() {
return Ok(IntegrityReport {
total: 0,
valid: 0,
tampered: 0,
missing: 0,
});
}
let json_files: Vec<PathBuf> = std::fs::read_dir(&falsification_dir)?
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.extension().map(|e| e == "json").unwrap_or(false))
.collect();
let mut valid = 0;
let mut tampered = 0;
let mut missing = 0;
for path in &json_files {
match Self::check_receipt_file(path) {
Ok(true) => valid += 1,
Ok(false) => tampered += 1,
Err(_) => missing += 1,
}
}
Ok(IntegrityReport {
total: json_files.len(),
valid,
tampered,
missing,
})
}
fn check_receipt_file(path: &Path) -> Result<bool> {
let content = std::fs::read_to_string(path)?;
let receipt: FalsificationReceipt = serde_json::from_str(&content)?;
Ok(receipt.verify_integrity())
}
fn events_path(&self, work_item_id: &str) -> PathBuf {
self.work_dir.join(work_item_id).join("events.jsonl")
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn append_event(&self, work_item_id: &str, event: AgentEvent) -> Result<String> {
use std::io::Write;
let dir = self.work_dir.join(work_item_id);
std::fs::create_dir_all(&dir).context("Failed to create work item directory")?;
let record = WorkEventRecord {
id: format!("ev-{}", Uuid::now_v7().simple()),
recorded_at: chrono::Utc::now().to_rfc3339(),
event,
};
let line = serde_json::to_string(&record).context("Failed to serialize event record")?;
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(self.events_path(work_item_id))
.context("Failed to open events.jsonl")?;
file.write_all(format!("{line}\n").as_bytes())
.context("Failed to append event")?;
Ok(record.id)
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn load_events(&self, work_item_id: &str) -> Result<Vec<WorkEventRecord>> {
let path = self.events_path(work_item_id);
if !path.exists() {
return Ok(Vec::new());
}
let text = std::fs::read_to_string(&path).context("Failed to read events.jsonl")?;
let mut records = Vec::new();
for line in text.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
records.push(
serde_json::from_str::<WorkEventRecord>(line)
.context("Failed to parse event line")?,
);
}
Ok(records)
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn unacked_refusals(&self, work_item_id: &str) -> Result<Vec<WorkEventRecord>> {
let records = self.load_events(work_item_id)?;
let acked: std::collections::HashSet<String> = records
.iter()
.filter_map(|r| match &r.event {
AgentEvent::Ack { ack_of, .. } => Some(ack_of.clone()),
_ => None,
})
.collect();
Ok(records
.into_iter()
.filter(|r| {
matches!(r.event, AgentEvent::Refusal { .. }) && !acked.contains(&r.id)
})
.collect())
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn verify_all(&self) -> Result<LedgerVerification> {
let mut report = LedgerVerification::default();
self.verify_receipts(&mut report);
self.verify_r1_order(&mut report);
Ok(report)
}
fn verify_receipts(&self, report: &mut LedgerVerification) {
let Ok(entries) = std::fs::read_dir(&self.work_dir) else {
return;
};
let mut dirs: Vec<PathBuf> = entries.flatten().map(|e| e.path()).collect();
dirs.sort();
for dir in dirs {
for path in Self::receipt_files_in(&dir.join("falsification")) {
self.verify_one_receipt(&path, report);
}
}
}
fn receipt_files_in(falsification: &Path) -> Vec<PathBuf> {
let Ok(files) = std::fs::read_dir(falsification) else {
return Vec::new();
};
let mut receipt_files: Vec<PathBuf> = files
.flatten()
.map(|e| e.path())
.filter(|p| {
p.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n.starts_with("receipt-") && n.ends_with(".json"))
})
.collect();
receipt_files.sort();
receipt_files
}
fn verify_one_receipt(&self, path: &Path, report: &mut LedgerVerification) {
report.total_receipts += 1;
let Ok(text) = std::fs::read_to_string(path) else {
report.unreadable.push(path.display().to_string());
return;
};
let receipt = match serde_json::from_str::<FalsificationReceipt>(&text) {
Ok(r) => r,
Err(e) => {
report.unreadable.push(format!("{}: {}", path.display(), e));
return;
}
};
if !receipt.verify_integrity() {
report.tampered.push(format!(
"{} (id={}, schema_version={})",
path.display(),
receipt.id,
receipt.schema_version
));
return;
}
report.verified += 1;
let key = match &receipt.agent {
Some(agent) => format!("{} / {} / {:?}", agent.model, agent.effort, agent.harness),
None => "(no provenance)".to_string(),
};
*report.by_provenance.entry(key).or_insert(0) += 1;
}
fn verify_r1_order(&self, report: &mut LedgerVerification) {
let Ok(text) = std::fs::read_to_string(self.work_dir.join("ledger.jsonl")) else {
return;
};
let mut prev: Option<u32> = None;
for line in text.lines().filter(|l| !l.trim().is_empty()) {
let Some(num) = serde_json::from_str::<LedgerEntry>(line)
.ok()
.and_then(|e| e.work_item_id.strip_prefix("MACS-").and_then(|n| n.parse::<u32>().ok()))
else {
continue;
};
if prev.is_some_and(|p| num < p) {
report.r1_violations.push(format!(
"MACS-{num:03} appears after MACS-{:03} (out of ascending order)",
prev.unwrap()
));
}
prev = Some(num);
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct LedgerVerification {
pub total_receipts: usize,
pub verified: usize,
pub tampered: Vec<String>,
pub unreadable: Vec<String>,
pub by_provenance: std::collections::BTreeMap<String, usize>,
pub r1_violations: Vec<String>,
}
impl LedgerVerification {
pub fn ok(&self) -> bool {
self.tampered.is_empty() && self.unreadable.is_empty() && self.r1_violations.is_empty()
}
}