Skip to main content

higher_graphen_core/extension/
scenario_policy.rs

1use super::common::{Description, LifecycleStatus, ObjectRef};
2use super::validation::require_some;
3use crate::text::{normalize_required_text, normalize_required_text_vec};
4use crate::{CoreError, Id, Provenance, Result, ReviewStatus};
5use serde::{Deserialize, Serialize};
6
7/// Scenario kind.
8#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
9#[serde(rename_all = "snake_case")]
10pub enum ScenarioKind {
11    /// Hypothetical world.
12    Hypothetical,
13    /// Reachable world.
14    Reachable,
15    /// Blocked world.
16    Blocked,
17    /// Counterfactual world.
18    Counterfactual,
19    /// Planned world.
20    Planned,
21    /// Refuted world.
22    Refuted,
23    /// Accepted operational plan.
24    AcceptedOperationalPlan,
25}
26
27/// Structures changed by a scenario.
28#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
29#[serde(deny_unknown_fields)]
30pub struct ScenarioChanges {
31    /// Added cells.
32    #[serde(default, skip_serializing_if = "Vec::is_empty")]
33    pub added: Vec<Id>,
34    /// Removed cells.
35    #[serde(default, skip_serializing_if = "Vec::is_empty")]
36    pub removed: Vec<Id>,
37    /// Modified morphisms.
38    #[serde(default, skip_serializing_if = "Vec::is_empty")]
39    pub modified: Vec<Id>,
40}
41
42/// Reachability path from a base space.
43#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
44#[serde(deny_unknown_fields)]
45pub struct Reachability {
46    /// Source space.
47    #[serde(rename = "ref")]
48    pub reference: Id,
49    /// Morphisms used to reach the scenario.
50    #[serde(default, skip_serializing_if = "Vec::is_empty")]
51    pub via_morphisms: Vec<Id>,
52}
53
54/// Reviewable hypothetical, reachable, or counterfactual world.
55#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
56#[serde(deny_unknown_fields)]
57pub struct Scenario {
58    /// Scenario identifier.
59    pub id: Id,
60    /// Base space.
61    pub base_space: Id,
62    /// Scenario kind.
63    pub scenario_kind: ScenarioKind,
64    /// Assumption cells.
65    #[serde(default, skip_serializing_if = "Vec::is_empty")]
66    pub assumptions: Vec<Id>,
67    /// Changed structures.
68    pub changed_structures: ScenarioChanges,
69    /// Reachability relation from another space.
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub reachable_from: Option<Reachability>,
72    /// Affected invariants.
73    #[serde(default, skip_serializing_if = "Vec::is_empty")]
74    pub affected_invariants: Vec<Id>,
75    /// Expected obstructions.
76    #[serde(default, skip_serializing_if = "Vec::is_empty")]
77    pub expected_obstructions: Vec<Id>,
78    /// Required witnesses.
79    #[serde(default, skip_serializing_if = "Vec::is_empty")]
80    pub required_witnesses: Vec<Id>,
81    /// Valuations attached to the scenario.
82    #[serde(default, skip_serializing_if = "Vec::is_empty")]
83    pub valuations: Vec<Id>,
84    /// Scenario status.
85    pub status: ScenarioStatus,
86    /// Scenario provenance.
87    pub provenance: Provenance,
88    /// Review status.
89    pub review_status: LifecycleStatus,
90}
91
92/// Scenario-specific status.
93#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
94#[serde(rename_all = "snake_case")]
95pub enum ScenarioStatus {
96    /// Draft scenario.
97    Draft,
98    /// Candidate scenario.
99    Candidate,
100    /// Under review.
101    UnderReview,
102    /// Reachable from the base.
103    Reachable,
104    /// Blocked by obstructions.
105    Blocked,
106    /// Refuted.
107    Refuted,
108    /// Accepted for its declared use.
109    Accepted,
110}
111
112impl Scenario {
113    /// Validates conditions required before treating the scenario as accepted.
114    pub fn validate_acceptance(&self) -> Result<()> {
115        match self.scenario_kind {
116            ScenarioKind::Hypothetical if self.status == ScenarioStatus::Accepted => {
117                return Err(CoreError::malformed_field(
118                    "scenario_kind",
119                    "hypothetical scenario cannot be treated as accepted fact",
120                ));
121            }
122            ScenarioKind::Reachable | ScenarioKind::AcceptedOperationalPlan => {
123                require_some("reachable_from", self.reachable_from.as_ref())?;
124            }
125            _ => {}
126        }
127        if self.affected_invariants.is_empty() {
128            return Err(CoreError::malformed_field(
129                "affected_invariants",
130                "accepted scenario requires invariant checks or affected invariant records",
131            ));
132        }
133        if self.scenario_kind == ScenarioKind::AcceptedOperationalPlan
134            && self.provenance.review_status != ReviewStatus::Accepted
135        {
136            return Err(CoreError::malformed_field(
137                "provenance.review_status",
138                "accepted operational plan requires policy/capability review",
139            ));
140        }
141        Ok(())
142    }
143}
144
145/// Operation governed by a capability.
146#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
147#[serde(rename_all = "snake_case")]
148pub enum CapabilityOperation {
149    /// Read a target.
150    Read,
151    /// Propose a target.
152    Propose,
153    /// Modify a target.
154    Modify,
155    /// Accept a target.
156    Accept,
157    /// Reject a target.
158    Reject,
159    /// Project a target.
160    Project,
161    /// Execute a morphism.
162    ExecuteMorphism,
163    /// Merge an equivalence.
164    MergeEquivalence,
165    /// Create a scenario.
166    CreateScenario,
167    /// Approve a policy exception.
168    ApprovePolicyException,
169}
170
171/// Review required by a capability.
172#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
173#[serde(deny_unknown_fields)]
174pub struct RequiredReview {
175    /// Policy requiring review.
176    pub policy: Id,
177    /// Reviewer actor.
178    pub reviewer: Id,
179}
180
181/// Validity interval for a capability.
182#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
183#[serde(deny_unknown_fields)]
184pub struct ValidityInterval {
185    /// Start timestamp.
186    pub starts_at: String,
187    /// End timestamp.
188    pub ends_at: String,
189}
190
191impl ValidityInterval {
192    /// Validates the portable timestamp payloads.
193    pub fn validate(&self) -> Result<()> {
194        normalize_required_text("validity_interval.starts_at", &self.starts_at)?;
195        normalize_required_text("validity_interval.ends_at", &self.ends_at)?;
196        Ok(())
197    }
198}
199
200/// Capability status.
201#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
202#[serde(rename_all = "snake_case")]
203pub enum CapabilityStatus {
204    /// Active capability.
205    Active,
206    /// Temporarily suspended capability.
207    Suspended,
208    /// Expired capability.
209    Expired,
210    /// Revoked capability.
211    Revoked,
212    /// Candidate capability.
213    Candidate,
214}
215
216/// Actor-specific ability to operate on a target in a context.
217#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
218#[serde(deny_unknown_fields)]
219pub struct Capability {
220    /// Capability identifier.
221    pub id: Id,
222    /// Actor cell.
223    pub actor: Id,
224    /// Operation granted.
225    pub operation: CapabilityOperation,
226    /// Target type.
227    pub target_type: String,
228    /// Target references.
229    #[serde(default, skip_serializing_if = "Vec::is_empty")]
230    pub target_refs: Vec<ObjectRef>,
231    /// Valid contexts.
232    #[serde(default, skip_serializing_if = "Vec::is_empty")]
233    pub contexts: Vec<Id>,
234    /// Preconditions.
235    #[serde(default, skip_serializing_if = "Vec::is_empty")]
236    pub preconditions: Vec<Id>,
237    /// Postconditions.
238    #[serde(default, skip_serializing_if = "Vec::is_empty")]
239    pub postconditions: Vec<Id>,
240    /// Forbidden effects.
241    #[serde(default, skip_serializing_if = "Vec::is_empty")]
242    pub forbidden_effects: Vec<Description>,
243    /// Required review.
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub required_review: Option<RequiredReview>,
246    /// Validity interval.
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub validity_interval: Option<ValidityInterval>,
249    /// Capability provenance.
250    pub provenance: Provenance,
251    /// Capability status.
252    pub status: CapabilityStatus,
253}
254
255impl Capability {
256    /// Validates whether this capability may be used for a mutating operation.
257    pub fn validate_active_use(&self) -> Result<()> {
258        if self.status != CapabilityStatus::Active {
259            return Err(CoreError::malformed_field(
260                "status",
261                "capability must be active before use",
262            ));
263        }
264        normalize_required_text("target_type", &self.target_type)?;
265        if let Some(validity_interval) = &self.validity_interval {
266            validity_interval.validate()?;
267        }
268        if self.operation == CapabilityOperation::Accept
269            && self.required_review.is_none()
270            && self.provenance.review_status != ReviewStatus::Accepted
271        {
272            return Err(CoreError::malformed_field(
273                "required_review",
274                "accept operation requires explicit policy review",
275            ));
276        }
277        Ok(())
278    }
279}
280
281/// Policy kind.
282#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
283#[serde(rename_all = "snake_case")]
284pub enum PolicyKind {
285    /// Permission policy.
286    Permission,
287    /// Prohibition policy.
288    Prohibition,
289    /// Obligation policy.
290    Obligation,
291    /// Review requirement policy.
292    ReviewRequirement,
293    /// Projection safety policy.
294    ProjectionSafety,
295    /// Candidate acceptance policy.
296    CandidateAcceptance,
297    /// Data boundary policy.
298    DataBoundary,
299    /// Escalation policy.
300    Escalation,
301}
302
303/// Targets and operations a policy applies to.
304#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
305#[serde(deny_unknown_fields)]
306pub struct PolicyApplicability {
307    /// Target type names.
308    #[serde(default, skip_serializing_if = "Vec::is_empty")]
309    pub target_types: Vec<String>,
310    /// Context identifiers.
311    #[serde(default, skip_serializing_if = "Vec::is_empty")]
312    pub contexts: Vec<Id>,
313    /// Operation names.
314    #[serde(default, skip_serializing_if = "Vec::is_empty")]
315    pub operations: Vec<String>,
316}
317
318impl PolicyApplicability {
319    fn validate(&self) -> Result<()> {
320        normalize_required_text_vec("target_types", &self.target_types)?;
321        normalize_required_text_vec("operations", &self.operations)?;
322        Ok(())
323    }
324}
325
326/// Policy rule payload.
327#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
328#[serde(deny_unknown_fields)]
329pub struct PolicyRule {
330    /// Rule description.
331    pub description: String,
332    /// Constraint identifiers.
333    #[serde(default, skip_serializing_if = "Vec::is_empty")]
334    pub constraints: Vec<Id>,
335}
336
337impl PolicyRule {
338    /// Creates a policy rule.
339    pub fn new(description: impl Into<String>) -> Result<Self> {
340        Ok(Self {
341            description: normalize_required_text("policy.rule.description", description)?,
342            constraints: Vec::new(),
343        })
344    }
345}
346
347/// Policy status.
348#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
349#[serde(rename_all = "snake_case")]
350pub enum PolicyStatus {
351    /// Draft policy.
352    Draft,
353    /// Active policy.
354    Active,
355    /// Deprecated policy.
356    Deprecated,
357    /// Revoked policy.
358    Revoked,
359}
360
361/// System-wide or context-bound rule.
362#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
363#[serde(deny_unknown_fields)]
364pub struct Policy {
365    /// Policy identifier.
366    pub id: Id,
367    /// Policy kind.
368    pub policy_kind: PolicyKind,
369    /// Applicability.
370    pub applies_to: PolicyApplicability,
371    /// Policy rule.
372    pub rule: PolicyRule,
373    /// Required witnesses.
374    #[serde(default, skip_serializing_if = "Vec::is_empty")]
375    pub required_witnesses: Vec<Id>,
376    /// Required derivations.
377    #[serde(default, skip_serializing_if = "Vec::is_empty")]
378    pub required_derivations: Vec<Id>,
379    /// Escalation path.
380    #[serde(default, skip_serializing_if = "Vec::is_empty")]
381    pub escalation_path: Vec<Id>,
382    /// Violation obstruction template id.
383    #[serde(skip_serializing_if = "Option::is_none")]
384    pub violation_obstruction_template: Option<Id>,
385    /// Policy status.
386    pub status: PolicyStatus,
387    /// Policy provenance.
388    pub provenance: Provenance,
389    /// Policy review status.
390    pub review_status: ReviewStatus,
391}
392
393impl Policy {
394    /// Validates conditions required for an active policy.
395    pub fn validate_active(&self) -> Result<()> {
396        self.applies_to.validate()?;
397        normalize_required_text("policy.rule.description", &self.rule.description)?;
398        if self.status == PolicyStatus::Active && self.review_status != ReviewStatus::Accepted {
399            return Err(CoreError::malformed_field(
400                "review_status",
401                "active policy requires accepted review status",
402            ));
403        }
404        if self.status == PolicyStatus::Active
405            && self.provenance.review_status != ReviewStatus::Accepted
406        {
407            return Err(CoreError::malformed_field(
408                "provenance.review_status",
409                "active policy provenance must be accepted",
410            ));
411        }
412        Ok(())
413    }
414}