use crate::types::{Receipt, Verdict, CheckOutcome};
use crate::verifier::verify;
use std::path::{Path, PathBuf};
use std::fs;
use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArchitectureProposal {
pub rule_reference: String,
pub root_cause: String,
pub proposed_change: String,
pub confidence: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GovernanceReport {
pub receipt_id: String,
pub verdict: Verdict,
pub proposals: Vec<ArchitectureProposal>,
}
pub struct GovernanceAgent {
receipts_dir: PathBuf,
rules_path: PathBuf,
}
impl GovernanceAgent {
pub fn new() -> Self {
Self {
receipts_dir: PathBuf::from(".ggen/receipts"),
rules_path: PathBuf::from("GEMINI.md"),
}
}
pub fn audit_workspace(&self) -> anyhow::Result<Vec<GovernanceReport>> {
let rejected = self.discover_rejections()?;
let rules = self.load_rules()?;
let mut reports = Vec::new();
for (path, receipt, verdict) in rejected {
let proposals = self.analyze_and_propose(&receipt, &verdict, &rules);
reports.push(GovernanceReport {
receipt_id: path.file_name().unwrap_or_default().to_string_lossy().into_owned(),
verdict,
proposals,
});
}
Ok(reports)
}
fn discover_rejections(&self) -> anyhow::Result<Vec<(PathBuf, Receipt, Verdict)>> {
let mut rejections = Vec::new();
if !self.receipts_dir.exists() {
return Ok(rejections);
}
for entry in fs::read_dir(&self.receipts_dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().map_or(false, |ext| ext == "json") {
let content = fs::read_to_string(&path)?;
if let Ok(receipt) = serde_json::from_str::<Receipt>(&content) {
let verdict = verify(&receipt);
if !verdict.accepted {
rejections.push((path, receipt, verdict));
}
}
}
}
Ok(rejections)
}
fn load_rules(&self) -> anyhow::Result<String> {
if self.rules_path.exists() {
Ok(fs::read_to_string(&self.rules_path)?)
} else {
Ok("RULE 1: All receipts must have contiguous sequence numbers.\n\
RULE 2: All commitments must be valid BLAKE3 hashes.\n\
RULE 3: All event types must be non-empty.".to_string())
}
}
fn analyze_and_propose(
&self,
_receipt: &Receipt,
verdict: &Verdict,
rules: &str,
) -> Vec<ArchitectureProposal> {
let mut proposals = Vec::new();
if let Some(failure) = verdict.outcomes.iter().find(|o| !o.passed) {
let proposal = match failure.stage.as_str() {
"continuity" => ArchitectureProposal {
rule_reference: "RULE 1: Sequence Contiguity".to_string(),
root_cause: format!("Sequence gap detected: {}", failure.detail),
proposed_change: "Modify `src/ocel.rs::SeqCounter` to be immutable and managed strictly by a singleton `ChainAssembler` to prevent manual sequence assignment errors.".to_string(),
confidence: 0.95,
},
"chain_integrity" => ArchitectureProposal {
rule_reference: "RULE: Cryptographic Integrity".to_string(),
root_cause: "Chain hash mismatch (possible out-of-order event mutation)".to_string(),
proposed_change: "Enforce `Receipt` immutability by moving `_seal: ()` to a mandatory private constructor pattern in `src/types.rs`, ensuring receipts can only be generated via the finalized assembler.".to_string(),
confidence: 0.98,
},
"verify_commitments" => ArchitectureProposal {
rule_reference: "RULE 2: Commitment Validity".to_string(),
root_cause: "Malformed BLAKE3 hash detected.".to_string(),
proposed_change: "Update `Blake3Hash` type in `src/types.rs` to validate hex format upon construction/deserialization, moving the failure to the edge of the system.".to_string(),
confidence: 0.90,
},
_ => ArchitectureProposal {
rule_reference: "General Governance".to_string(),
root_cause: failure.detail.clone(),
proposed_change: format!("Investigate the {} pipeline stage for logic leaks that allow invalid state construction.", failure.stage),
confidence: 0.50,
},
};
proposals.push(proposal);
}
if rules.contains("ADR") {
proposals.push(ArchitectureProposal {
rule_reference: "Architectural Decision Record Compliance".to_string(),
root_cause: "Potential drift from ADR-specified non-forgeable carriers.".to_string(),
proposed_change: "Audit `src/chain.rs` against ADR-3 to ensure every deserialization path re-verifies the rolling chain hash.".to_string(),
confidence: 0.85,
});
}
proposals
}
}
pub fn handle_governance_audit() -> anyhow::Result<String> {
let agent = GovernanceAgent::new();
let reports = agent.audit_workspace()?;
if reports.is_empty() {
return Ok("No governance violations detected. Workspace is in compliance. ✓".to_string());
}
let mut output = String::new();
output.push_str("=== Autonomous Governance Audit Report ===\n\n");
for report in reports {
output.push_str(&format!("Violation in: {}\n", report.receipt_id));
output.push_str(&format!(" Verdict: REJECTED\n"));
output.push_str(&format!(" Reason: {}\n", report.verdict.reason));
output.push_str(" Proposals:\n");
for (i, p) in report.proposals.iter().enumerate() {
output.push_str(&format!(" {}. [Rule: {}] (Conf: {:.0}%)\n", i + 1, p.rule_reference, p.confidence * 100.0));
output.push_str(&format!(" Cause: {}\n", p.root_cause));
output.push_str(&format!(" Fix: {}\n", p.proposed_change));
}
output.push_str("\n");
}
Ok(output)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ocel::{build_event, object_ref, SeqCounter};
use crate::chain::ChainAssembler;
use tempfile::tempdir;
#[test]
fn test_governance_detects_rejected_receipt() {
let dir = tempdir().unwrap();
let receipts_path = dir.path().join(".ggen/receipts");
fs::create_dir_all(&receipts_path).unwrap();
let mut asm = ChainAssembler::new();
let mut counter = SeqCounter::new();
let ev1 = build_event("op1", vec![], b"p1", &mut counter).unwrap();
let mut ev2 = build_event("op2", vec![], b"p2", &mut counter).unwrap();
ev2.seq = 5;
asm.append(ev1).unwrap();
asm.append(ev2).unwrap();
let receipt = asm.finalize();
let receipt_file = receipts_path.join("fail.json");
let json = serde_json::to_string(&receipt).unwrap();
fs::write(&receipt_file, json).unwrap();
let agent = GovernanceAgent {
receipts_dir: receipts_path,
rules_path: PathBuf::from("GEMINI.md"),
};
let reports = agent.audit_workspace().unwrap();
assert_eq!(reports.len(), 1);
assert_eq!(reports[0].verdict.accepted, false);
assert!(reports[0].proposals.iter().any(|p| p.rule_reference.contains("Sequence")));
}
}