1use super::common::{Description, LifecycleStatus, ObjectRef};
2use super::validation::{require_non_empty, require_some};
3use crate::text::normalize_required_text;
4use crate::{Confidence, CoreError, Id, Provenance, Result};
5use serde::{Deserialize, Serialize};
6
7#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
9#[serde(rename_all = "snake_case")]
10pub enum CriterionDirection {
11 Maximize,
13 Minimize,
15 Preserve,
17 Avoid,
19}
20
21#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
23#[serde(deny_unknown_fields)]
24pub struct ValuationCriterion {
25 pub criterion_id: String,
27 pub name: String,
29 pub direction: CriterionDirection,
31 #[serde(skip_serializing_if = "Option::is_none")]
33 pub weight: Option<f64>,
34 pub required: bool,
36}
37
38impl ValuationCriterion {
39 fn validate(&self) -> Result<()> {
40 normalize_required_text("criteria.criterion_id", &self.criterion_id)?;
41 normalize_required_text("criteria.name", &self.name)?;
42 if let Some(weight) = self.weight {
43 if !weight.is_finite() || weight < 0.0 {
44 return Err(CoreError::malformed_field(
45 "criteria.weight",
46 "weight must be finite and non-negative",
47 ));
48 }
49 }
50 Ok(())
51 }
52}
53
54#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
56#[serde(rename_all = "snake_case")]
57pub enum OrderType {
58 ScalarScore,
60 LexicographicOrder,
62 PartialOrder,
64 ParetoFrontier,
66 ThresholdAcceptance,
68 QualitativeRanking,
70}
71
72#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
74#[serde(deny_unknown_fields)]
75pub struct CriterionValue {
76 pub criterion_id: String,
78 pub value: ValuationValue,
80 pub evidence: Id,
82}
83
84impl CriterionValue {
85 fn validate(&self) -> Result<()> {
86 normalize_required_text("values.criterion_id", &self.criterion_id)?;
87 Ok(())
88 }
89}
90
91#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
93#[serde(untagged)]
94pub enum ValuationValue {
95 Text(String),
97 Number(f64),
99 Boolean(bool),
101}
102
103#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
105#[serde(deny_unknown_fields)]
106pub struct Tradeoff {
107 pub gains: String,
109 pub losses: String,
111 #[serde(default, skip_serializing_if = "Vec::is_empty")]
113 pub affected_invariants: Vec<Id>,
114}
115
116impl Tradeoff {
117 fn validate(&self) -> Result<()> {
118 normalize_required_text("tradeoffs.gains", &self.gains)?;
119 normalize_required_text("tradeoffs.losses", &self.losses)?;
120 Ok(())
121 }
122}
123
124#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
126#[serde(deny_unknown_fields)]
127pub struct Valuation {
128 pub id: Id,
130 pub target: ObjectRef,
132 #[serde(skip_serializing_if = "Option::is_none")]
134 pub valuation_context: Option<Id>,
135 #[serde(default, skip_serializing_if = "Vec::is_empty")]
137 pub criteria: Vec<ValuationCriterion>,
138 pub order_type: OrderType,
140 #[serde(default, skip_serializing_if = "Vec::is_empty")]
142 pub values: Vec<CriterionValue>,
143 #[serde(default, skip_serializing_if = "Vec::is_empty")]
145 pub tradeoffs: Vec<Tradeoff>,
146 #[serde(default, skip_serializing_if = "Vec::is_empty")]
148 pub incomparable_with: Vec<Id>,
149 pub confidence: Confidence,
151 pub provenance: Provenance,
153 pub review_status: LifecycleStatus,
155}
156
157impl Valuation {
158 pub fn validate_for_decision(&self) -> Result<()> {
160 require_some("valuation_context", self.valuation_context.as_ref())?;
161 require_non_empty("criteria", &self.criteria)?;
162 require_non_empty("values", &self.values)?;
163 for criterion in &self.criteria {
164 criterion.validate()?;
165 }
166 for value in &self.values {
167 value.validate()?;
168 }
169 for tradeoff in &self.tradeoffs {
170 tradeoff.validate()?;
171 }
172 if !self.incomparable_with.is_empty()
173 && matches!(
174 self.order_type,
175 OrderType::ScalarScore | OrderType::LexicographicOrder
176 )
177 {
178 return Err(CoreError::malformed_field(
179 "incomparable_with",
180 "incomparable valuations must not be projected as a single ranking",
181 ));
182 }
183 Ok(())
184 }
185}
186
187#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
189#[serde(rename_all = "snake_case")]
190pub enum SchemaMappingKind {
191 Rename,
193 Split,
195 Merge,
197 Refinement,
199 Abstraction,
201 Deprecation,
203 SemanticRedefinition,
205 Custom,
207}
208
209#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
211#[serde(rename_all = "snake_case")]
212pub enum SchemaCompatibility {
213 BackwardCompatible,
215 ForwardCompatible,
217 Lossy,
219 Incompatible,
221 Unknown,
223}
224
225#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
227#[serde(deny_unknown_fields)]
228pub struct SchemaMapping {
229 pub source_ref: String,
231 pub target_ref: String,
233 pub mapping_rule: String,
235 #[serde(default, skip_serializing_if = "Vec::is_empty")]
237 pub preservation_claims: Vec<Id>,
238 #[serde(default, skip_serializing_if = "Vec::is_empty")]
240 pub loss_claims: Vec<Description>,
241 #[serde(default, skip_serializing_if = "Vec::is_empty")]
243 pub required_reviews: Vec<Id>,
244}
245
246impl SchemaMapping {
247 fn validate(&self) -> Result<()> {
248 normalize_required_text("mappings.source_ref", &self.source_ref)?;
249 normalize_required_text("mappings.target_ref", &self.target_ref)?;
250 normalize_required_text("mappings.mapping_rule", &self.mapping_rule)?;
251 Ok(())
252 }
253}
254
255#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
257#[serde(deny_unknown_fields)]
258pub struct SchemaVerification {
259 #[serde(default, skip_serializing_if = "Vec::is_empty")]
261 pub checks: Vec<Id>,
262 #[serde(default, skip_serializing_if = "Vec::is_empty")]
264 pub witnesses: Vec<Id>,
265}
266
267#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
269#[serde(deny_unknown_fields)]
270pub struct SchemaMorphism {
271 pub id: Id,
273 pub source_schema: Id,
275 pub target_schema: Id,
277 pub source_interpretation_package: Id,
279 pub target_interpretation_package: Id,
281 pub mapping_kind: SchemaMappingKind,
283 #[serde(default, skip_serializing_if = "Vec::is_empty")]
285 pub mappings: Vec<SchemaMapping>,
286 #[serde(default, skip_serializing_if = "Vec::is_empty")]
288 pub affected_objects: Vec<ObjectRef>,
289 pub compatibility: SchemaCompatibility,
291 pub verification: SchemaVerification,
293 pub provenance: Provenance,
295 pub review_status: LifecycleStatus,
297}
298
299impl SchemaMorphism {
300 pub fn validate_application(&self) -> Result<()> {
302 require_non_empty("mappings", &self.mappings)?;
303 for mapping in &self.mappings {
304 mapping.validate()?;
305 }
306 if self.mapping_kind == SchemaMappingKind::SemanticRedefinition
307 && self.review_status != LifecycleStatus::Accepted
308 {
309 return Err(CoreError::malformed_field(
310 "review_status",
311 "semantic redefinition requires explicit accepted review",
312 ));
313 }
314 if matches!(
315 self.mapping_kind,
316 SchemaMappingKind::Split | SchemaMappingKind::Merge
317 ) {
318 require_non_empty("affected_objects", &self.affected_objects)?;
319 if self
320 .mappings
321 .iter()
322 .all(|mapping| mapping.loss_claims.is_empty())
323 {
324 return Err(CoreError::malformed_field(
325 "mappings.loss_claims",
326 "split or merge schema mapping requires loss claims",
327 ));
328 }
329 }
330 if self.compatibility == SchemaCompatibility::Incompatible {
331 return Err(CoreError::malformed_field(
332 "compatibility",
333 "incompatible schema morphism cannot be applied as a migration",
334 ));
335 }
336 if self.compatibility == SchemaCompatibility::Lossy
337 && self
338 .mappings
339 .iter()
340 .all(|mapping| mapping.loss_claims.is_empty())
341 {
342 return Err(CoreError::malformed_field(
343 "mappings.loss_claims",
344 "lossy schema morphism requires explicit loss claims",
345 ));
346 }
347 Ok(())
348 }
349}