Skip to main content

higher_graphen_core/extension/
valuation_schema.rs

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/// Direction for a valuation criterion.
8#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
9#[serde(rename_all = "snake_case")]
10pub enum CriterionDirection {
11    /// Maximize this criterion.
12    Maximize,
13    /// Minimize this criterion.
14    Minimize,
15    /// Preserve this criterion.
16    Preserve,
17    /// Avoid this criterion.
18    Avoid,
19}
20
21/// Criterion used in a valuation.
22#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
23#[serde(deny_unknown_fields)]
24pub struct ValuationCriterion {
25    /// Criterion identifier.
26    pub criterion_id: String,
27    /// Human-readable name.
28    pub name: String,
29    /// Optimization direction.
30    pub direction: CriterionDirection,
31    /// Optional weight.
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub weight: Option<f64>,
34    /// Whether the criterion is mandatory.
35    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/// Valuation ordering mode.
55#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
56#[serde(rename_all = "snake_case")]
57pub enum OrderType {
58    /// Scalar score.
59    ScalarScore,
60    /// Lexicographic ordering.
61    LexicographicOrder,
62    /// Partial order.
63    PartialOrder,
64    /// Pareto frontier.
65    ParetoFrontier,
66    /// Threshold acceptance.
67    ThresholdAcceptance,
68    /// Qualitative ranking.
69    QualitativeRanking,
70}
71
72/// Value attached to a valuation criterion.
73#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
74#[serde(deny_unknown_fields)]
75pub struct CriterionValue {
76    /// Criterion id.
77    pub criterion_id: String,
78    /// Stable value representation.
79    pub value: ValuationValue,
80    /// Evidence witness.
81    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/// Primitive valuation value.
92#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
93#[serde(untagged)]
94pub enum ValuationValue {
95    /// String value.
96    Text(String),
97    /// Numeric value.
98    Number(f64),
99    /// Boolean value.
100    Boolean(bool),
101}
102
103/// Trade-off recorded by a valuation.
104#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
105#[serde(deny_unknown_fields)]
106pub struct Tradeoff {
107    /// Gains introduced by the target.
108    pub gains: String,
109    /// Losses introduced by the target.
110    pub losses: String,
111    /// Affected invariants.
112    #[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/// Value judgment under an explicit evaluation context.
125#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
126#[serde(deny_unknown_fields)]
127pub struct Valuation {
128    /// Valuation identifier.
129    pub id: Id,
130    /// Valued target.
131    pub target: ObjectRef,
132    /// Evaluation context.
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub valuation_context: Option<Id>,
135    /// Evaluation criteria.
136    #[serde(default, skip_serializing_if = "Vec::is_empty")]
137    pub criteria: Vec<ValuationCriterion>,
138    /// Ordering mode.
139    pub order_type: OrderType,
140    /// Criterion values.
141    #[serde(default, skip_serializing_if = "Vec::is_empty")]
142    pub values: Vec<CriterionValue>,
143    /// Recorded trade-offs.
144    #[serde(default, skip_serializing_if = "Vec::is_empty")]
145    pub tradeoffs: Vec<Tradeoff>,
146    /// Valuations that cannot be compared with this one.
147    #[serde(default, skip_serializing_if = "Vec::is_empty")]
148    pub incomparable_with: Vec<Id>,
149    /// Confidence in the valuation.
150    pub confidence: Confidence,
151    /// Valuation provenance.
152    pub provenance: Provenance,
153    /// Review lifecycle state.
154    pub review_status: LifecycleStatus,
155}
156
157impl Valuation {
158    /// Validates conditions required before using the valuation in a decision.
159    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/// Schema mapping kind.
188#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
189#[serde(rename_all = "snake_case")]
190pub enum SchemaMappingKind {
191    /// Rename mapping.
192    Rename,
193    /// Split mapping.
194    Split,
195    /// Merge mapping.
196    Merge,
197    /// Refinement mapping.
198    Refinement,
199    /// Abstraction mapping.
200    Abstraction,
201    /// Deprecation mapping.
202    Deprecation,
203    /// Semantic redefinition.
204    SemanticRedefinition,
205    /// Custom mapping.
206    Custom,
207}
208
209/// Compatibility result for a schema morphism.
210#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
211#[serde(rename_all = "snake_case")]
212pub enum SchemaCompatibility {
213    /// Backward compatible mapping.
214    BackwardCompatible,
215    /// Forward compatible mapping.
216    ForwardCompatible,
217    /// Lossy mapping.
218    Lossy,
219    /// Incompatible mapping.
220    Incompatible,
221    /// Unknown compatibility.
222    Unknown,
223}
224
225/// Single mapping inside a schema morphism.
226#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
227#[serde(deny_unknown_fields)]
228pub struct SchemaMapping {
229    /// Source schema reference.
230    pub source_ref: String,
231    /// Target schema reference.
232    pub target_ref: String,
233    /// Mapping rule.
234    pub mapping_rule: String,
235    /// Invariant preservation claims.
236    #[serde(default, skip_serializing_if = "Vec::is_empty")]
237    pub preservation_claims: Vec<Id>,
238    /// Loss claims.
239    #[serde(default, skip_serializing_if = "Vec::is_empty")]
240    pub loss_claims: Vec<Description>,
241    /// Required review policies.
242    #[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/// Verification references for a schema morphism.
256#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
257#[serde(deny_unknown_fields)]
258pub struct SchemaVerification {
259    /// Derivation checks.
260    #[serde(default, skip_serializing_if = "Vec::is_empty")]
261    pub checks: Vec<Id>,
262    /// Witness checks.
263    #[serde(default, skip_serializing_if = "Vec::is_empty")]
264    pub witnesses: Vec<Id>,
265}
266
267/// Morphism describing schema, ontology, or interpretation package evolution.
268#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
269#[serde(deny_unknown_fields)]
270pub struct SchemaMorphism {
271    /// Schema morphism identifier.
272    pub id: Id,
273    /// Source schema.
274    pub source_schema: Id,
275    /// Target schema.
276    pub target_schema: Id,
277    /// Source interpretation package.
278    pub source_interpretation_package: Id,
279    /// Target interpretation package.
280    pub target_interpretation_package: Id,
281    /// Mapping kind.
282    pub mapping_kind: SchemaMappingKind,
283    /// Individual mappings.
284    #[serde(default, skip_serializing_if = "Vec::is_empty")]
285    pub mappings: Vec<SchemaMapping>,
286    /// Affected objects.
287    #[serde(default, skip_serializing_if = "Vec::is_empty")]
288    pub affected_objects: Vec<ObjectRef>,
289    /// Compatibility classification.
290    pub compatibility: SchemaCompatibility,
291    /// Verification checks.
292    pub verification: SchemaVerification,
293    /// Schema morphism provenance.
294    pub provenance: Provenance,
295    /// Review lifecycle state.
296    pub review_status: LifecycleStatus,
297}
298
299impl SchemaMorphism {
300    /// Validates conditions required before applying the schema morphism.
301    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}