pe-core 0.1.0

Core types for Potential Expectations — messages, channels, state, traits
Documentation
//! Execution validation — guardrail checks and write governance enforcement.
//!
//! These types carry only primitive data (no external deps) so pe-core can
//! validate execution results without depending on pe-runtime or pe-tools.
//! Higher crates construct `ExecutionMetrics` and `WriteRecord` from their
//! own result types and pass them here for validation.

use crate::boundaries::{Guardrail, WriteAccess, WriteGovernance};
use crate::error::PeError;
use serde::{Deserialize, Serialize};

/// Lightweight execution summary for guardrail validation.
///
/// Constructed by pe-runtime from `ExecutionResult`. Contains only
/// the metrics that guardrails check against — no `Arc<dyn Tool>`,
/// no state generics, no external crate types.
///
/// # Example
///
/// ```
/// use pe_core::validation::ExecutionMetrics;
///
/// let metrics = ExecutionMetrics {
///     output_tokens: 1500,
///     tool_calls_made: 3,
///     output_text: "Some output".into(),
/// };
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionMetrics {
    pub output_tokens: u32,
    pub tool_calls_made: u32,
    pub output_text: String,
}

/// A write operation that needs governance validation.
///
/// Represents a single write the agent attempted during execution.
/// The runtime collects these and passes them to
/// [`WriteGovernance::validate_writes`] after execution completes.
///
/// # Example
///
/// ```
/// use pe_core::validation::WriteRecord;
///
/// let write = WriteRecord {
///     destination: "collective".into(),
///     key: "project_notes".into(),
///     has_grant: false,
/// };
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WriteRecord {
    /// Target store: "own_memory", "collective", "vault", "task_store".
    pub destination: String,
    /// Key being written.
    pub key: String,
    /// Whether a DataGrant was obtained for this write.
    pub has_grant: bool,
}

impl Guardrail {
    /// Validate execution metrics against this guardrail.
    ///
    /// Returns `Ok(())` if the guardrail is satisfied, or
    /// `Err(PeError::GuardrailViolation)` with details.
    ///
    /// Content-analysis guardrails (MustCiteSources, NoCodeExecution)
    /// are deferred to Plan 014 — they return Ok(()) for now.
    #[must_use = "this returns a Result that must be checked"]
    pub fn validate(&self, metrics: &ExecutionMetrics) -> Result<(), PeError> {
        match self {
            Self::MaxOutputTokens(max) => {
                if metrics.output_tokens > *max {
                    return Err(PeError::GuardrailViolation {
                        guardrail: format!("MaxOutputTokens({})", max),
                        details: format!("output was {} tokens", metrics.output_tokens),
                    });
                }
            }
            Self::MaxToolCallsPerTurn(max) => {
                if metrics.tool_calls_made > *max {
                    return Err(PeError::GuardrailViolation {
                        guardrail: format!("MaxToolCallsPerTurn({})", max),
                        details: format!("{} calls made", metrics.tool_calls_made),
                    });
                }
            }
            // Content analysis guardrails — deferred to Plan 014
            Self::MustCiteSources | Self::NoCodeExecution => {}
            // Custom guardrails need a user-provided validation fn (Plan 014)
            Self::Custom { .. } => {}
        }
        Ok(())
    }
}

impl WriteGovernance {
    /// Validate that all writes respect this governance policy.
    ///
    /// Checks each write's destination against the access level.
    /// ReadOnly destinations reject all writes. RequiresGrant
    /// destinations reject writes without a grant.
    #[must_use = "this returns a Result that must be checked"]
    pub fn validate_writes(&self, writes: &[WriteRecord]) -> Result<(), PeError> {
        for write in writes {
            let access = match write.destination.as_str() {
                "own_memory" => &self.own_memory,
                "collective" => &self.collective,
                "vault" => &self.vault,
                "task_store" => &self.task_store,
                _ => continue, // Unknown destinations are not governed
            };
            match access {
                WriteAccess::ReadOnly => {
                    return Err(PeError::WriteGovernanceViolation {
                        destination: write.destination.clone(),
                        reason: "ReadOnly — no writes permitted".into(),
                    });
                }
                WriteAccess::RequiresGrant if !write.has_grant => {
                    return Err(PeError::WriteGovernanceViolation {
                        destination: write.destination.clone(),
                        reason: "RequiresGrant — no grant obtained".into(),
                    });
                }
                _ => {} // Free or Attributed — always permitted
            }
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::boundaries::WriteGovernance;

    fn metrics(output_tokens: u32, tool_calls: u32) -> ExecutionMetrics {
        ExecutionMetrics {
            output_tokens,
            tool_calls_made: tool_calls,
            output_text: String::new(),
        }
    }

    #[test]
    fn max_output_tokens_within_limit() {
        let g = Guardrail::MaxOutputTokens(2000);
        assert!(g.validate(&metrics(1500, 0)).is_ok());
    }

    #[test]
    fn max_output_tokens_violation() {
        let g = Guardrail::MaxOutputTokens(2000);
        let err = g.validate(&metrics(2500, 0)).unwrap_err();
        assert!(matches!(err, PeError::GuardrailViolation { .. }));
    }

    #[test]
    fn max_tool_calls_within_limit() {
        let g = Guardrail::MaxToolCallsPerTurn(5);
        assert!(g.validate(&metrics(0, 3)).is_ok());
    }

    #[test]
    fn max_tool_calls_violation() {
        let g = Guardrail::MaxToolCallsPerTurn(5);
        let err = g.validate(&metrics(0, 8)).unwrap_err();
        assert!(matches!(err, PeError::GuardrailViolation { .. }));
    }

    #[test]
    fn write_governance_free_allows_all() {
        let gov = WriteGovernance::default(); // all Free
        let writes = vec![WriteRecord {
            destination: "own_memory".into(),
            key: "test".into(),
            has_grant: false,
        }];
        assert!(gov.validate_writes(&writes).is_ok());
    }

    #[test]
    fn write_governance_read_only_rejects() {
        let gov = WriteGovernance {
            vault: WriteAccess::ReadOnly,
            ..Default::default()
        };
        let writes = vec![WriteRecord {
            destination: "vault".into(),
            key: "secret".into(),
            has_grant: false,
        }];
        let err = gov.validate_writes(&writes).unwrap_err();
        assert!(matches!(err, PeError::WriteGovernanceViolation { .. }));
    }

    #[test]
    fn write_governance_requires_grant_without_grant_rejects() {
        let gov = WriteGovernance {
            collective: WriteAccess::RequiresGrant,
            ..Default::default()
        };
        let writes = vec![WriteRecord {
            destination: "collective".into(),
            key: "notes".into(),
            has_grant: false,
        }];
        assert!(gov.validate_writes(&writes).is_err());

        // With grant — should succeed
        let writes_granted = vec![WriteRecord {
            destination: "collective".into(),
            key: "notes".into(),
            has_grant: true,
        }];
        assert!(gov.validate_writes(&writes_granted).is_ok());
    }
}