ahri_tre_app/service/
semantic_lifecycle.rs1use super::*;
2use ahri_tre_protocol::{lifecycle as wire, refs::ObjectKind};
3
4impl AppService {
5 pub async fn plan_governed_semantic_delete(
6 &self,
7 request: wire::SemanticDeleteRequest,
8 authenticated_actor: &str,
9 ) -> Result<SemanticDeletePlanningResult, AppError> {
10 use wire::SemanticDeleteSelector as Selector;
11 let actor = super::lifecycle::lifecycle_actor(&request.intent, authenticated_actor)?;
12 let mut constraint = None;
13 let target = match request.target {
14 Selector::Reference { reference, domain } => {
15 let (kind, scope) = match reference.kind {
16 ObjectKind::Domain => (SemanticDeleteTargetKind::Domain, "domain"),
17 ObjectKind::Variable => (SemanticDeleteTargetKind::Variable, "variable"),
18 ObjectKind::Vocabulary => (SemanticDeleteTargetKind::Vocabulary, "vocabulary"),
19 ObjectKind::Entity => (SemanticDeleteTargetKind::Entity, "entity"),
20 ObjectKind::Relation => (SemanticDeleteTargetKind::EntityRelation, "relation"),
21 _ => {
22 return Err(AppError::Validation(
23 "Invalid semantic deletion kind".into(),
24 ));
25 }
26 };
27 let id = ahri_tre_protocol::refs::decode_scoped_integer_ref(
28 scope,
29 self.catalogue_reference_id(reference, reference.kind)?,
30 )
31 .ok_or_else(|| AppError::Validation("Invalid semantic reference".into()))?;
32 if let Some(domain) = domain {
33 constraint = Some(
34 self.get_domain(GetDomainRequest {
35 domain: self.catalogue_domain_selector(domain)?,
36 })
37 .await?
38 .ok_or_else(|| {
39 AppError::NotFound("Supplied Domain was not found".into())
40 })?,
41 );
42 }
43 pinned_semantic_target(kind, id)
44 }
45 Selector::Domain { domain } => SemanticDeleteTarget::Domain {
46 domain: self.catalogue_domain_selector(domain)?,
47 },
48 Selector::Variable { domain, name } => SemanticDeleteTarget::Variable {
49 variable: VariableSelector::Name {
50 domain: self.catalogue_domain_selector(domain)?,
51 name: name.into(),
52 },
53 },
54 Selector::Vocabulary { domain, name } => SemanticDeleteTarget::Vocabulary {
55 vocabulary: VocabularySelector::Name {
56 domain: self.catalogue_domain_selector(domain)?,
57 name: name.into(),
58 },
59 },
60 Selector::Entity {
61 entity: ahri_tre_protocol::model::EntitySelector::Name { domain, name },
62 } => SemanticDeleteTarget::Entity {
63 entity: EntitySelector::Name {
64 domain: self.catalogue_domain_selector(domain)?,
65 name: name.into(),
66 },
67 },
68 Selector::Relation {
69 relation: ahri_tre_protocol::model::RelationSelector::Name { domain, name },
70 } => SemanticDeleteTarget::EntityRelation {
71 relation: EntityRelationSelector::Name {
72 domain: self.catalogue_domain_selector(domain)?,
73 name: name.into(),
74 },
75 },
76 _ => {
77 return Err(AppError::Validation(
78 "Semantic deletion requires a canonical reference or qualified name".into(),
79 ));
80 }
81 };
82 let mut planning = self
83 .plan_semantic_delete(PlanSemanticDeleteRequest {
84 target,
85 reason: request.intent.reason,
86 actor: Some(actor),
87 dry_run: request.intent.dry_run,
88 })
89 .await?;
90 if let Some(plan) = &mut planning.plan {
91 let identity = &plan.target_identity;
92 if let Some(domain) = constraint
93 && identity.domain_id != domain.domain_id
94 {
95 return Err(AppError::Conflict(
96 "Semantic target does not belong to the supplied Domain".into(),
97 ));
98 }
99 let id = identity.id.parse().map_err(|_| {
100 AppError::Infrastructure("Invalid resolved semantic identity".into())
101 })?;
102 plan.target = pinned_semantic_target(identity.kind, id);
103 }
104 Ok(planning)
105 }
106
107 pub async fn execute_governed_semantic_delete(
108 &self,
109 request: wire::SemanticDeleteRequest,
110 authenticated_actor: &str,
111 ) -> Result<GovernedSemanticDeleteOutcome, AppError> {
112 let dry_run = request.intent.dry_run;
113 let confirmed = request.intent.confirmed;
114 let planning = self
115 .plan_governed_semantic_delete(request, authenticated_actor)
116 .await?;
117 if dry_run {
118 return Ok(GovernedSemanticDeleteOutcome::Planned(planning));
119 }
120 let plan = planning
121 .plan
122 .ok_or_else(|| AppError::Validation("Semantic deletion plan was rejected".into()))?;
123 if !confirmed {
124 return Err(AppError::Validation(
125 "Semantic deletion requires confirmation".into(),
126 ));
127 }
128 if plan.disposition == SemanticDeleteDisposition::Blocked {
129 return Ok(GovernedSemanticDeleteOutcome::Blocked(plan));
130 }
131 self.execute_semantic_delete(ExecuteSemanticDeleteRequest { plan, confirmed })
132 .await
133 .map(GovernedSemanticDeleteOutcome::Executed)
134 }
135}
136
137pub enum GovernedSemanticDeleteOutcome {
138 Planned(SemanticDeletePlanningResult),
139 Blocked(SemanticDeletePlan),
140 Executed(ExecuteSemanticDeleteResult),
141}
142
143fn pinned_semantic_target(kind: SemanticDeleteTargetKind, id: i64) -> SemanticDeleteTarget {
144 match kind {
145 SemanticDeleteTargetKind::Domain => SemanticDeleteTarget::Domain {
146 domain: DomainSelector::Id {
147 domain_id: ahri_tre_types::DomainId(id),
148 },
149 },
150 SemanticDeleteTargetKind::Variable => SemanticDeleteTarget::Variable {
151 variable: VariableSelector::Id {
152 variable_id: ahri_tre_types::VariableId(id),
153 },
154 },
155 SemanticDeleteTargetKind::Vocabulary => SemanticDeleteTarget::Vocabulary {
156 vocabulary: VocabularySelector::Id {
157 vocabulary_id: ahri_tre_types::VocabularyId(id),
158 },
159 },
160 SemanticDeleteTargetKind::Entity => SemanticDeleteTarget::Entity {
161 entity: EntitySelector::Id {
162 entity_id: ahri_tre_types::EntityId(id),
163 },
164 },
165 SemanticDeleteTargetKind::EntityRelation => SemanticDeleteTarget::EntityRelation {
166 relation: EntityRelationSelector::Id {
167 entity_relation_id: ahri_tre_types::EntityRelationId(id),
168 },
169 },
170 }
171}