use super::common::{Description, LifecycleStatus, ObjectRef};
use super::validation::require_some;
use crate::text::{normalize_required_text, normalize_required_text_vec};
use crate::{CoreError, Id, Provenance, Result, ReviewStatus};
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ScenarioKind {
Hypothetical,
Reachable,
Blocked,
Counterfactual,
Planned,
Refuted,
AcceptedOperationalPlan,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ScenarioChanges {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub added: Vec<Id>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub removed: Vec<Id>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub modified: Vec<Id>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Reachability {
#[serde(rename = "ref")]
pub reference: Id,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub via_morphisms: Vec<Id>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Scenario {
pub id: Id,
pub base_space: Id,
pub scenario_kind: ScenarioKind,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub assumptions: Vec<Id>,
pub changed_structures: ScenarioChanges,
#[serde(skip_serializing_if = "Option::is_none")]
pub reachable_from: Option<Reachability>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub affected_invariants: Vec<Id>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub expected_obstructions: Vec<Id>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub required_witnesses: Vec<Id>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub valuations: Vec<Id>,
pub status: ScenarioStatus,
pub provenance: Provenance,
pub review_status: LifecycleStatus,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ScenarioStatus {
Draft,
Candidate,
UnderReview,
Reachable,
Blocked,
Refuted,
Accepted,
}
impl Scenario {
pub fn validate_acceptance(&self) -> Result<()> {
match self.scenario_kind {
ScenarioKind::Hypothetical if self.status == ScenarioStatus::Accepted => {
return Err(CoreError::malformed_field(
"scenario_kind",
"hypothetical scenario cannot be treated as accepted fact",
));
}
ScenarioKind::Reachable | ScenarioKind::AcceptedOperationalPlan => {
require_some("reachable_from", self.reachable_from.as_ref())?;
}
_ => {}
}
if self.affected_invariants.is_empty() {
return Err(CoreError::malformed_field(
"affected_invariants",
"accepted scenario requires invariant checks or affected invariant records",
));
}
if self.scenario_kind == ScenarioKind::AcceptedOperationalPlan
&& self.provenance.review_status != ReviewStatus::Accepted
{
return Err(CoreError::malformed_field(
"provenance.review_status",
"accepted operational plan requires policy/capability review",
));
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum CapabilityOperation {
Read,
Propose,
Modify,
Accept,
Reject,
Project,
ExecuteMorphism,
MergeEquivalence,
CreateScenario,
ApprovePolicyException,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct RequiredReview {
pub policy: Id,
pub reviewer: Id,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ValidityInterval {
pub starts_at: String,
pub ends_at: String,
}
impl ValidityInterval {
pub fn validate(&self) -> Result<()> {
normalize_required_text("validity_interval.starts_at", &self.starts_at)?;
normalize_required_text("validity_interval.ends_at", &self.ends_at)?;
Ok(())
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum CapabilityStatus {
Active,
Suspended,
Expired,
Revoked,
Candidate,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Capability {
pub id: Id,
pub actor: Id,
pub operation: CapabilityOperation,
pub target_type: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub target_refs: Vec<ObjectRef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub contexts: Vec<Id>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub preconditions: Vec<Id>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub postconditions: Vec<Id>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub forbidden_effects: Vec<Description>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required_review: Option<RequiredReview>,
#[serde(skip_serializing_if = "Option::is_none")]
pub validity_interval: Option<ValidityInterval>,
pub provenance: Provenance,
pub status: CapabilityStatus,
}
impl Capability {
pub fn validate_active_use(&self) -> Result<()> {
if self.status != CapabilityStatus::Active {
return Err(CoreError::malformed_field(
"status",
"capability must be active before use",
));
}
normalize_required_text("target_type", &self.target_type)?;
if let Some(validity_interval) = &self.validity_interval {
validity_interval.validate()?;
}
if self.operation == CapabilityOperation::Accept
&& self.required_review.is_none()
&& self.provenance.review_status != ReviewStatus::Accepted
{
return Err(CoreError::malformed_field(
"required_review",
"accept operation requires explicit policy review",
));
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PolicyKind {
Permission,
Prohibition,
Obligation,
ReviewRequirement,
ProjectionSafety,
CandidateAcceptance,
DataBoundary,
Escalation,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PolicyApplicability {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub target_types: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub contexts: Vec<Id>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub operations: Vec<String>,
}
impl PolicyApplicability {
fn validate(&self) -> Result<()> {
normalize_required_text_vec("target_types", &self.target_types)?;
normalize_required_text_vec("operations", &self.operations)?;
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PolicyRule {
pub description: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub constraints: Vec<Id>,
}
impl PolicyRule {
pub fn new(description: impl Into<String>) -> Result<Self> {
Ok(Self {
description: normalize_required_text("policy.rule.description", description)?,
constraints: Vec::new(),
})
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PolicyStatus {
Draft,
Active,
Deprecated,
Revoked,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Policy {
pub id: Id,
pub policy_kind: PolicyKind,
pub applies_to: PolicyApplicability,
pub rule: PolicyRule,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub required_witnesses: Vec<Id>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub required_derivations: Vec<Id>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub escalation_path: Vec<Id>,
#[serde(skip_serializing_if = "Option::is_none")]
pub violation_obstruction_template: Option<Id>,
pub status: PolicyStatus,
pub provenance: Provenance,
pub review_status: ReviewStatus,
}
impl Policy {
pub fn validate_active(&self) -> Result<()> {
self.applies_to.validate()?;
normalize_required_text("policy.rule.description", &self.rule.description)?;
if self.status == PolicyStatus::Active && self.review_status != ReviewStatus::Accepted {
return Err(CoreError::malformed_field(
"review_status",
"active policy requires accepted review status",
));
}
if self.status == PolicyStatus::Active
&& self.provenance.review_status != ReviewStatus::Accepted
{
return Err(CoreError::malformed_field(
"provenance.review_status",
"active policy provenance must be accepted",
));
}
Ok(())
}
}