Implement closed typed semantic review with immutable proposals
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,404 @@
|
||||
use super::types::*;
|
||||
use crate::model::*;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum HistoricalSourceKind {
|
||||
Reference,
|
||||
Enum,
|
||||
}
|
||||
|
||||
const MAX_FACTS: usize = 16_384;
|
||||
const MAX_LEAVES: usize = 1_000_000;
|
||||
const MAX_PATH: usize = 128;
|
||||
|
||||
struct Walk<'a> {
|
||||
path: Vec<String>,
|
||||
slots: Vec<(Vec<String>, HistoricalSourceKind)>,
|
||||
errors: Vec<String>,
|
||||
leaves: usize,
|
||||
visitor: &'a mut dyn FnMut(&mut Id),
|
||||
}
|
||||
impl Walk<'_> {
|
||||
fn at(&mut self, key: impl ToString, f: impl FnOnce(&mut Self)) {
|
||||
if self.leaves >= MAX_LEAVES {
|
||||
return;
|
||||
}
|
||||
self.leaves += 1;
|
||||
self.path.push(key.to_string());
|
||||
f(self);
|
||||
self.path.pop();
|
||||
}
|
||||
fn slot(&mut self, kind: HistoricalSourceKind) {
|
||||
self.slots.push((self.path.clone(), kind));
|
||||
}
|
||||
fn error(&mut self, message: &str) {
|
||||
self.errors
|
||||
.push(format!("{}: {message}", self.path.join(".")));
|
||||
}
|
||||
}
|
||||
trait Historical {
|
||||
fn walk(&mut self, w: &mut Walk<'_>);
|
||||
}
|
||||
impl Historical for Id {
|
||||
fn walk(&mut self, w: &mut Walk<'_>) {
|
||||
w.slot(HistoricalSourceKind::Reference);
|
||||
(w.visitor)(self);
|
||||
}
|
||||
}
|
||||
macro_rules! plain {
|
||||
($($ty:ty),*) => { $( impl Historical for $ty { fn walk(&mut self, _: &mut Walk<'_>) {} } )* };
|
||||
}
|
||||
plain!(bool, u32, i64);
|
||||
impl Historical for String {
|
||||
fn walk(&mut self, w: &mut Walk<'_>) {
|
||||
if !crate::validation::valid_text(self) {
|
||||
w.error("historical text contains an invalid character");
|
||||
}
|
||||
if self.len() > 1_000_000 {
|
||||
w.error("historical text work limit exceeded");
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Historical for f64 {
|
||||
fn walk(&mut self, w: &mut Walk<'_>) {
|
||||
if !self.is_finite() {
|
||||
w.error("historical real must be finite");
|
||||
}
|
||||
}
|
||||
}
|
||||
macro_rules! enums {
|
||||
($($ty:ty),*) => { $( impl Historical for $ty {
|
||||
fn walk(&mut self, w: &mut Walk<'_>) { w.slot(HistoricalSourceKind::Enum); }
|
||||
} )* };
|
||||
}
|
||||
enums!(
|
||||
ExchangeItemKind,
|
||||
ExchangeMechanism,
|
||||
ParameterDirection,
|
||||
FieldStorage,
|
||||
ComponentPortKind,
|
||||
ComponentPortOrientation,
|
||||
PortDirection,
|
||||
ScenarioViewpoint,
|
||||
ControlKind,
|
||||
FragmentOperator,
|
||||
TimeEventKind,
|
||||
StateKind,
|
||||
TransmissionProtocol,
|
||||
AcquisitionProtocol,
|
||||
TransitionKind,
|
||||
StateActionKind,
|
||||
ArtifactRelation,
|
||||
ReferenceCollection,
|
||||
OrderedCollection,
|
||||
IncidentEnd,
|
||||
DataConstructor,
|
||||
PortConstructor,
|
||||
OccurrenceConstructor,
|
||||
EventConstructor,
|
||||
MessageConstructor
|
||||
);
|
||||
impl<T: Historical> Historical for Option<T> {
|
||||
fn walk(&mut self, w: &mut Walk<'_>) {
|
||||
if let Some(v) = self {
|
||||
v.walk(w);
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T: Historical> Historical for Vec<T> {
|
||||
fn walk(&mut self, w: &mut Walk<'_>) {
|
||||
for (i, v) in self.iter_mut().enumerate().take(MAX_LEAVES) {
|
||||
w.at(i, |w| v.walk(w));
|
||||
}
|
||||
if self.len() > MAX_LEAVES {
|
||||
w.error("historical collection work limit exceeded");
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T: Historical> Historical for Observed<T> {
|
||||
fn walk(&mut self, w: &mut Walk<'_>) {
|
||||
match self {
|
||||
Self::NotApplicable => w.slot(HistoricalSourceKind::Enum),
|
||||
Self::Value(v) => w.at("Value", |w| v.walk(w)),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T: Historical> Historical for Reviewed<T> {
|
||||
fn walk(&mut self, w: &mut Walk<'_>) {
|
||||
if matches!(
|
||||
(&self.oa, &self.sa),
|
||||
(Observed::NotApplicable, Observed::NotApplicable)
|
||||
) {
|
||||
w.error("a reviewed facet must apply on at least one side");
|
||||
}
|
||||
w.at("oa", |w| self.oa.walk(w));
|
||||
w.at("sa", |w| self.sa.walk(w));
|
||||
}
|
||||
}
|
||||
impl<T: Historical> Historical for KeyPair<T> {
|
||||
fn walk(&mut self, w: &mut Walk<'_>) {
|
||||
if self.oa.is_none() && self.sa.is_none() {
|
||||
w.error("key pair cannot be empty on both sides");
|
||||
}
|
||||
w.at("oa", |w| self.oa.walk(w));
|
||||
w.at("sa", |w| self.sa.walk(w));
|
||||
}
|
||||
}
|
||||
impl Historical for AnnotationKey {
|
||||
fn walk(&mut self, w: &mut Walk<'_>) {
|
||||
if self.path.len() > MAX_PATH {
|
||||
w.error("historical annotation path limit exceeded");
|
||||
}
|
||||
w.at("path", |w| self.path.walk(w));
|
||||
w.at("field", |w| self.field.walk(w));
|
||||
}
|
||||
}
|
||||
impl Historical for MessageValue {
|
||||
fn walk(&mut self, w: &mut Walk<'_>) {
|
||||
if self.path.len() > MAX_PATH {
|
||||
w.error("historical annotation path limit exceeded");
|
||||
}
|
||||
w.at("path", |w| self.path.walk(w));
|
||||
w.at("field", |w| self.field.walk(w));
|
||||
w.at("expression", |w| self.expression.walk(w));
|
||||
}
|
||||
}
|
||||
impl Historical for LifelineBinding {
|
||||
fn walk(&mut self, w: &mut Walk<'_>) {
|
||||
w.at("referenced", |w| self.referenced.walk(w));
|
||||
w.at("local", |w| self.local.walk(w));
|
||||
}
|
||||
}
|
||||
impl Historical for DataValue {
|
||||
fn walk(&mut self, w: &mut Walk<'_>) {
|
||||
match self {
|
||||
Self::Real(value) => w.at("Real", |w| value.walk(w)),
|
||||
Self::String(value) => w.at("String", |w| value.walk(w)),
|
||||
Self::Enumeration(value) => w.at("Enumeration", |w| value.walk(w)),
|
||||
Self::Boolean(_) | Self::Integer(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Historical for TypeRef {
|
||||
fn walk(&mut self, w: &mut Walk<'_>) {
|
||||
match self {
|
||||
Self::Named(id) => w.at("Named", |w| id.walk(w)),
|
||||
_ => w.slot(HistoricalSourceKind::Enum),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Historical for ArtifactReview {
|
||||
fn walk(&mut self, w: &mut Walk<'_>) {
|
||||
match self {
|
||||
Self::Presence(v) => w.at("Presence", |w| v.walk(w)),
|
||||
Self::Name(v) => w.at("Name", |w| v.walk(w)),
|
||||
Self::Description(v) => w.at("Description", |w| v.walk(w)),
|
||||
Self::Owner(v) => w.at("Owner", |w| v.walk(w)),
|
||||
Self::Relation(v) => w.at("Relation", |w| v.walk(w)),
|
||||
Self::Uri(v) => w.at("Uri", |w| v.walk(w)),
|
||||
Self::Revision(v) => w.at("Revision", |w| v.walk(w)),
|
||||
Self::Fragment(v) => w.at("Fragment", |w| v.walk(w)),
|
||||
}
|
||||
}
|
||||
}
|
||||
macro_rules! family_visitor {
|
||||
($( $family:ident, $selector:ident, $review:ident { $( $leaf:ident: $ty:ty ),* } )*) => {
|
||||
impl Historical for FacetReview {
|
||||
fn walk(&mut self, w: &mut Walk<'_>) {
|
||||
match self {
|
||||
$( Self::$family(v) => w.at(stringify!($family), |w| match v {
|
||||
$( $review::$leaf(v) => w.at(stringify!($leaf), |w| v.walk(w)), )*
|
||||
}), )*
|
||||
Self::Member { collection, keys, review } => w.at("Member", |w| {
|
||||
w.at("collection", |w| collection.walk(w)); w.at("keys", |w| keys.walk(w)); w.at("review", |w| review.walk(w));
|
||||
}),
|
||||
Self::Order { collection, review } => w.at("Order", |w| {
|
||||
w.at("collection", |w| collection.walk(w)); w.at("review", |w| review.walk(w));
|
||||
for side in [&review.oa, &review.sa] { if let Observed::Value(order) = side {
|
||||
if order.iter().collect::<BTreeSet<_>>().len() != order.len() { w.error("duplicate historical order member"); }
|
||||
}}
|
||||
}),
|
||||
Self::Literal { keys, review } => w.at("Literal", |w| {
|
||||
w.at("keys", |w| keys.walk(w)); w.at("review", |w| review.walk(w));
|
||||
}),
|
||||
Self::Annotation { keys, review } => w.at("Annotation", |w| {
|
||||
w.at("keys", |w| keys.walk(w)); w.at("review", |w| review.walk(w));
|
||||
for (key, value) in [(&keys.oa, &review.oa), (&keys.sa, &review.sa)] {
|
||||
if let Observed::Value(Some(row)) = value {
|
||||
if key.as_ref() != Some(&AnnotationKey::from(row)) { w.error("historical annotation key and payload disagree"); }
|
||||
}
|
||||
}
|
||||
}),
|
||||
Self::Binding { keys, review } => w.at("Binding", |w| {
|
||||
w.at("keys", |w| keys.walk(w)); w.at("review", |w| review.walk(w));
|
||||
for (key, value) in [(&keys.oa, &review.oa), (&keys.sa, &review.sa)] {
|
||||
if let Observed::Value(Some(row)) = value {
|
||||
if *key != Some(row.referenced) { w.error("historical binding key and payload disagree"); }
|
||||
}
|
||||
}
|
||||
}),
|
||||
Self::Child { keys, review } => w.at("Child", |w| {
|
||||
w.at("keys", |w| keys.walk(w)); w.at("review", |w| review.walk(w));
|
||||
}),
|
||||
Self::Incident { keys, end, review } => w.at("Incident", |w| {
|
||||
w.at("keys", |w| keys.walk(w)); w.at("end", |w| end.walk(w)); w.at("review", |w| review.walk(w));
|
||||
}),
|
||||
Self::Artifact { keys, review } => w.at("Artifact", |w| {
|
||||
w.at("keys", |w| keys.walk(w)); w.at("review", |w| review.walk(w));
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
scalar_families!(family_visitor);
|
||||
|
||||
fn walk_baseline<'a>(
|
||||
baseline: &mut SemanticBaseline,
|
||||
visitor: &'a mut dyn FnMut(&mut Id),
|
||||
) -> Walk<'a> {
|
||||
let mut walk = Walk {
|
||||
path: vec!["facets".into()],
|
||||
slots: vec![],
|
||||
errors: vec![],
|
||||
leaves: 0,
|
||||
visitor,
|
||||
};
|
||||
if baseline.facets.len() > MAX_FACTS {
|
||||
walk.error("historical facet count limit exceeded");
|
||||
}
|
||||
for (i, facet) in baseline.facets.iter_mut().enumerate().take(MAX_FACTS) {
|
||||
walk.at(i, |w| facet.walk(w));
|
||||
}
|
||||
if walk.leaves >= MAX_LEAVES {
|
||||
walk.error("historical reference work limit exceeded");
|
||||
}
|
||||
walk
|
||||
}
|
||||
/// Historical UUIDs are visited only on explicit identity remapping, never deletion.
|
||||
pub fn visit_semantic_references(
|
||||
baseline: &mut SemanticBaseline,
|
||||
mut visitor: impl FnMut(&mut Id),
|
||||
) {
|
||||
walk_baseline(baseline, &mut visitor);
|
||||
}
|
||||
pub(crate) fn historical_source_slots(
|
||||
baseline: &SemanticBaseline,
|
||||
) -> Vec<(Vec<String>, HistoricalSourceKind)> {
|
||||
walk_baseline(&mut baseline.clone(), &mut |_| {}).slots
|
||||
}
|
||||
/// Checks persisted facts without demanding that historical references still exist.
|
||||
pub fn validate_semantic_baseline(baseline: &SemanticBaseline) -> Result<(), Vec<String>> {
|
||||
let mut errors = walk_baseline(&mut baseline.clone(), &mut |_| {}).errors;
|
||||
let mut slots = BTreeSet::new();
|
||||
for selector in baseline
|
||||
.facets
|
||||
.iter()
|
||||
.take(MAX_FACTS)
|
||||
.map(FacetReview::selector)
|
||||
{
|
||||
if selector_slots(&selector)
|
||||
.into_iter()
|
||||
.any(|slot| !slots.insert(slot))
|
||||
{
|
||||
errors.push(format!(
|
||||
"duplicate or overlapping historical facet: {selector:?}"
|
||||
));
|
||||
}
|
||||
}
|
||||
if errors.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(errors)
|
||||
}
|
||||
}
|
||||
pub(super) fn selector_slots(selector: &FacetSelector) -> Vec<FacetSelector> {
|
||||
fn split<T: Clone>(keys: &KeyPair<T>) -> Vec<KeyPair<T>> {
|
||||
let mut result = vec![];
|
||||
if let Some(oa) = &keys.oa {
|
||||
result.push(KeyPair {
|
||||
oa: Some(oa.clone()),
|
||||
sa: None,
|
||||
});
|
||||
}
|
||||
if let Some(sa) = &keys.sa {
|
||||
result.push(KeyPair {
|
||||
oa: None,
|
||||
sa: Some(sa.clone()),
|
||||
});
|
||||
}
|
||||
result
|
||||
}
|
||||
match selector {
|
||||
FacetSelector::Member { collection, keys } => split(keys)
|
||||
.into_iter()
|
||||
.map(|keys| FacetSelector::Member {
|
||||
collection: *collection,
|
||||
keys,
|
||||
})
|
||||
.collect(),
|
||||
FacetSelector::Literal(keys) => split(keys)
|
||||
.into_iter()
|
||||
.map(FacetSelector::Literal)
|
||||
.collect(),
|
||||
FacetSelector::Annotation(keys) => split(keys)
|
||||
.into_iter()
|
||||
.map(FacetSelector::Annotation)
|
||||
.collect(),
|
||||
FacetSelector::Binding(keys) => split(keys)
|
||||
.into_iter()
|
||||
.map(FacetSelector::Binding)
|
||||
.collect(),
|
||||
FacetSelector::Child(keys) => split(keys).into_iter().map(FacetSelector::Child).collect(),
|
||||
FacetSelector::Incident { keys, end } => split(keys)
|
||||
.into_iter()
|
||||
.map(|keys| FacetSelector::Incident { keys, end: *end })
|
||||
.collect(),
|
||||
FacetSelector::Artifact { keys, facet } => split(keys)
|
||||
.into_iter()
|
||||
.map(|keys| FacetSelector::Artifact {
|
||||
keys,
|
||||
facet: *facet,
|
||||
})
|
||||
.collect(),
|
||||
value => vec![value.clone()],
|
||||
}
|
||||
}
|
||||
fn pair_overlap<T: Eq>(a: &KeyPair<T>, b: &KeyPair<T>) -> bool {
|
||||
a.oa.as_ref()
|
||||
.zip(b.oa.as_ref())
|
||||
.is_some_and(|(a, b)| a == b)
|
||||
|| a.sa
|
||||
.as_ref()
|
||||
.zip(b.sa.as_ref())
|
||||
.is_some_and(|(a, b)| a == b)
|
||||
}
|
||||
/// Slot identity is each actual side's key, not equality of the complete pair.
|
||||
pub(crate) fn keyed_slots_overlap(a: &FacetSelector, b: &FacetSelector) -> bool {
|
||||
match (a, b) {
|
||||
(
|
||||
FacetSelector::Member {
|
||||
collection: a,
|
||||
keys: ak,
|
||||
},
|
||||
FacetSelector::Member {
|
||||
collection: b,
|
||||
keys: bk,
|
||||
},
|
||||
) => a == b && pair_overlap(ak, bk),
|
||||
(FacetSelector::Literal(a), FacetSelector::Literal(b)) => pair_overlap(a, b),
|
||||
(FacetSelector::Annotation(a), FacetSelector::Annotation(b)) => pair_overlap(a, b),
|
||||
(FacetSelector::Binding(a), FacetSelector::Binding(b)) => pair_overlap(a, b),
|
||||
(FacetSelector::Child(a), FacetSelector::Child(b)) => pair_overlap(a, b),
|
||||
(
|
||||
FacetSelector::Incident { keys: ak, end: a },
|
||||
FacetSelector::Incident { keys: bk, end: b },
|
||||
) => a == b && pair_overlap(ak, bk),
|
||||
(
|
||||
FacetSelector::Artifact { keys: ak, facet: a },
|
||||
FacetSelector::Artifact { keys: bk, facet: b },
|
||||
) => a == b && pair_overlap(ak, bk),
|
||||
_ => a == b,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
use super::*;
|
||||
use crate::model::*;
|
||||
use crate::transitions::Drift;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, BTreeSet, VecDeque};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct LinkedSemanticImpact {
|
||||
pub objects: KeyPair<Id>,
|
||||
pub traces: Vec<Id>,
|
||||
pub drift: Drift,
|
||||
pub unreviewed: bool,
|
||||
}
|
||||
const MAX_LINKED_OBJECTS: usize = 4_096;
|
||||
const MAX_LINKED_EDGES: usize = 32_768;
|
||||
|
||||
/// Derived dependency diagnostics, never another historical baseline. Follow
|
||||
/// direct references/children, then only reusable dictionary/event definitions.
|
||||
/// Association endpoints and ordinary architecture neighborhoods do not expand.
|
||||
pub fn linked_semantic_impacts(
|
||||
project: &Project,
|
||||
trace: &Trace,
|
||||
references: &BTreeMap<Id, Id>,
|
||||
) -> Result<Vec<LinkedSemanticImpact>, String> {
|
||||
let mut children: BTreeMap<Id, Vec<Id>> = BTreeMap::new();
|
||||
for element in project.elements.values() {
|
||||
if let Some(parent) = element.parent {
|
||||
children.entry(parent).or_default().push(element.id);
|
||||
}
|
||||
}
|
||||
let mut contracts: BTreeMap<Id, Vec<Id>> = BTreeMap::new();
|
||||
for relation in project.relations.values() {
|
||||
if let RelationBody::InterfaceItemAllocation { source, .. } = relation.body {
|
||||
contracts.entry(source).or_default().push(relation.id);
|
||||
}
|
||||
}
|
||||
let direct = |id| -> Vec<Id> {
|
||||
let mut out = vec![];
|
||||
if let Some(element) = project.elements.get(&id) {
|
||||
out.extend(
|
||||
crate::references::element_references(element)
|
||||
.into_iter()
|
||||
.filter(|r| Some(*r) != element.parent),
|
||||
);
|
||||
}
|
||||
if let Some(relation) = project.relations.get(&id) {
|
||||
out.extend(crate::references::relation_references(relation));
|
||||
}
|
||||
out.extend(children.get(&id).into_iter().flatten().copied());
|
||||
out.extend(contracts.get(&id).into_iter().flatten().copied());
|
||||
out
|
||||
};
|
||||
let expandable = |id| {
|
||||
project.elements.get(&id).is_some_and(|e| {
|
||||
matches!(
|
||||
e.kind(),
|
||||
Kind::DataType
|
||||
| Kind::DataField
|
||||
| Kind::ExchangeItem
|
||||
| Kind::Interface
|
||||
| Kind::Event
|
||||
)
|
||||
}) || project
|
||||
.relations
|
||||
.get(&id)
|
||||
.is_some_and(|r| matches!(r.body, RelationBody::InterfaceItemAllocation { .. }))
|
||||
};
|
||||
let mut edges = 0usize;
|
||||
let mut traverse = |root| -> Result<BTreeSet<Id>, String> {
|
||||
let mut seen = BTreeSet::from([root]);
|
||||
let first = direct(root);
|
||||
edges += first.len();
|
||||
if edges > MAX_LINKED_EDGES {
|
||||
return Err(
|
||||
"Linked semantic dependency work limit exceeded; narrow the review scope".into(),
|
||||
);
|
||||
}
|
||||
let mut queue: VecDeque<_> = first.into();
|
||||
while let Some(id) = queue.pop_front() {
|
||||
if !seen.insert(id) {
|
||||
continue;
|
||||
}
|
||||
if seen.len() > MAX_LINKED_OBJECTS || edges > MAX_LINKED_EDGES {
|
||||
return Err(
|
||||
"Linked semantic dependency work limit exceeded; narrow the review scope"
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
if expandable(id) {
|
||||
let next = direct(id);
|
||||
edges += next.len();
|
||||
if edges > MAX_LINKED_EDGES {
|
||||
return Err(
|
||||
"Linked semantic dependency work limit exceeded; narrow the review scope"
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
queue.extend(next);
|
||||
}
|
||||
}
|
||||
seen.remove(&root);
|
||||
Ok(seen)
|
||||
};
|
||||
let oa = traverse(trace.oa)?;
|
||||
let sa = traverse(trace.sa)?;
|
||||
let mut traced: BTreeMap<(Id, Id), Vec<&Trace>> = BTreeMap::new();
|
||||
for t in project.traces.values() {
|
||||
traced.entry((t.oa, t.sa)).or_default().push(t);
|
||||
}
|
||||
let mut pairs = vec![];
|
||||
let mut used = BTreeSet::new();
|
||||
for id in oa {
|
||||
let target = references.get(&id).copied();
|
||||
if let Some(id) = target {
|
||||
used.insert(id);
|
||||
}
|
||||
pairs.push(KeyPair {
|
||||
oa: Some(id),
|
||||
sa: target,
|
||||
});
|
||||
}
|
||||
for id in sa.difference(&used) {
|
||||
pairs.push(KeyPair {
|
||||
oa: None,
|
||||
sa: Some(*id),
|
||||
});
|
||||
}
|
||||
let mut result = vec![];
|
||||
for objects in pairs {
|
||||
if objects.oa == Some(trace.oa) && objects.sa == Some(trace.sa) {
|
||||
continue;
|
||||
}
|
||||
let links = objects
|
||||
.oa
|
||||
.zip(objects.sa)
|
||||
.and_then(|pair| traced.get(&pair));
|
||||
let mut flags = (false, false, links.is_none());
|
||||
let mut ids = vec![];
|
||||
for linked in links.into_iter().flatten() {
|
||||
ids.push(linked.id);
|
||||
let facts = super::catalog::own_semantic_facets(project, linked, references)?;
|
||||
if facts.is_empty() {
|
||||
flags.2 = true;
|
||||
}
|
||||
for fact in facts {
|
||||
match fact.drift {
|
||||
Drift::OaChanged => flags.0 = true,
|
||||
Drift::SaChanged => flags.1 = true,
|
||||
Drift::BothChanged => {
|
||||
flags.0 = true;
|
||||
flags.1 = true;
|
||||
}
|
||||
Drift::Unreviewed => flags.2 = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
let drift = match flags {
|
||||
(true, true, _) => Drift::BothChanged,
|
||||
(true, false, _) => Drift::OaChanged,
|
||||
(false, true, _) => Drift::SaChanged,
|
||||
(false, false, true) => Drift::Unreviewed,
|
||||
_ => Drift::Unchanged,
|
||||
};
|
||||
result.push(LinkedSemanticImpact {
|
||||
objects,
|
||||
traces: ids,
|
||||
drift,
|
||||
unreviewed: flags.2,
|
||||
});
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
@@ -0,0 +1,592 @@
|
||||
use super::catalog::{collection, order_collection, sides, write_scalar};
|
||||
use super::*;
|
||||
use crate::model::*;
|
||||
use crate::transitions::TransitionDirection;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
pub(crate) fn set_collection(
|
||||
p: &mut Project,
|
||||
id: Id,
|
||||
which: ReferenceCollection,
|
||||
list: Vec<Id>,
|
||||
) -> Result<(), String> {
|
||||
if list.iter().collect::<BTreeSet<_>>().len() != list.len() {
|
||||
return Err("Collection contains duplicate members".into());
|
||||
}
|
||||
use ReferenceCollection as C;
|
||||
if which == C::Steps {
|
||||
let e = p.elements.get_mut(&id).ok_or("Missing sequence")?;
|
||||
if let Some(steps) = e.steps_mut() {
|
||||
*steps = list;
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Branch { occurrences, .. },
|
||||
..
|
||||
}) = e.shared_mut()
|
||||
{
|
||||
*occurrences = list;
|
||||
return Ok(());
|
||||
}
|
||||
return Err("Target has no ordered steps".into());
|
||||
}
|
||||
let slot = match which {
|
||||
C::Steps => unreachable!(),
|
||||
C::FragmentBranches => match p.elements.get_mut(&id).and_then(Element::shared_mut) {
|
||||
Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Fragment { branches, .. },
|
||||
..
|
||||
}) => Some(branches),
|
||||
_ => None,
|
||||
},
|
||||
C::FunctionInputItems => match p.elements.get_mut(&id).and_then(Element::shared_mut) {
|
||||
Some(SharedBody::Port {
|
||||
contract: PortContract::FunctionInput { incoming_items },
|
||||
..
|
||||
}) => Some(incoming_items),
|
||||
_ => None,
|
||||
},
|
||||
C::FunctionOutputItems => match p.elements.get_mut(&id).and_then(Element::shared_mut) {
|
||||
Some(SharedBody::Port {
|
||||
contract: PortContract::FunctionOutput { outgoing_items },
|
||||
..
|
||||
}) => Some(outgoing_items),
|
||||
_ => None,
|
||||
},
|
||||
C::SignalItems => match p.elements.get_mut(&id).and_then(Element::shared_mut) {
|
||||
Some(SharedBody::Event {
|
||||
body: EventBody::Signal { items },
|
||||
}) => Some(items),
|
||||
_ => None,
|
||||
},
|
||||
C::ExchangeItems => match p.relations.get_mut(&id).map(|r| &mut r.body) {
|
||||
Some(
|
||||
RelationBody::Interaction { items, .. }
|
||||
| RelationBody::FunctionalExchange { items, .. }
|
||||
| RelationBody::BoundaryExchange { items, .. },
|
||||
) => Some(items),
|
||||
_ => None,
|
||||
},
|
||||
C::TransitionTriggers => match p.relations.get_mut(&id).map(|r| &mut r.body) {
|
||||
Some(RelationBody::StateTransition { triggers, .. }) => Some(triggers),
|
||||
_ => None,
|
||||
},
|
||||
C::TransitionEffects => match p.relations.get_mut(&id).map(|r| &mut r.body) {
|
||||
Some(RelationBody::StateTransition { effects, .. }) => Some(effects),
|
||||
_ => None,
|
||||
},
|
||||
C::Constrained => match p.elements.get_mut(&id).and_then(Element::shared_mut) {
|
||||
Some(SharedBody::Constraint { constrained, .. }) => Some(constrained),
|
||||
_ => None,
|
||||
},
|
||||
}
|
||||
.ok_or("Collection is inapplicable on target")?;
|
||||
*slot = list;
|
||||
Ok(())
|
||||
}
|
||||
pub(crate) fn chosen<T>(pair: &KeyPair<T>, layer: Layer) -> Option<&T> {
|
||||
match layer {
|
||||
Layer::Oa => pair.oa.as_ref(),
|
||||
Layer::Sa => pair.sa.as_ref(),
|
||||
}
|
||||
}
|
||||
pub(crate) fn value<T>(review: &Reviewed<T>, layer: Layer) -> Result<&T, String> {
|
||||
match match layer {
|
||||
Layer::Oa => &review.oa,
|
||||
Layer::Sa => &review.sa,
|
||||
} {
|
||||
Observed::Value(v) => Ok(v),
|
||||
Observed::NotApplicable => {
|
||||
Err("Facet does not exist in the selected constructor branch".into())
|
||||
}
|
||||
}
|
||||
}
|
||||
pub(crate) fn directional_map(
|
||||
map: &BTreeMap<Id, Id>,
|
||||
direction: TransitionDirection,
|
||||
) -> Result<BTreeMap<Id, Id>, String> {
|
||||
if direction == TransitionDirection::OaToSa {
|
||||
return Ok(map.clone());
|
||||
}
|
||||
let mut inverse = BTreeMap::new();
|
||||
for (oa, sa) in map {
|
||||
if inverse.insert(*sa, *oa).is_some_and(|old| old != *oa) {
|
||||
return Err(format!(
|
||||
"Reverse reference {sa} has multiple OA correspondences; supply an unambiguous mapping"
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(inverse)
|
||||
}
|
||||
pub(crate) fn remap_record(
|
||||
record: &mut SemanticRecord,
|
||||
map: &BTreeMap<Id, Id>,
|
||||
) -> Result<(), String> {
|
||||
let mut missing = BTreeSet::new();
|
||||
let mut remap = |id: &mut Id| {
|
||||
if let Some(new) = map.get(id) {
|
||||
*id = *new;
|
||||
} else {
|
||||
missing.insert(*id);
|
||||
}
|
||||
};
|
||||
match record {
|
||||
SemanticRecord::Element(e) => crate::references::visit_element_refs(e, &mut remap),
|
||||
SemanticRecord::Relation(r) => crate::references::visit_relation_refs(r, &mut remap),
|
||||
SemanticRecord::Artifact(a) => remap(&mut a.owner),
|
||||
}
|
||||
if missing.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!(
|
||||
"Missing explicit reference correspondence: {missing:?}"
|
||||
))
|
||||
}
|
||||
}
|
||||
pub(crate) fn remap_fact(
|
||||
fact: &FacetReview,
|
||||
map: &BTreeMap<Id, Id>,
|
||||
source: Layer,
|
||||
) -> Result<FacetReview, String> {
|
||||
// Remap only the selected observation. Keys remain in canonical OA/SA space.
|
||||
let mut json = serde_json::to_value(fact).map_err(|e| e.to_string())?;
|
||||
let base = SemanticBaseline {
|
||||
facets: vec![fact.clone()],
|
||||
};
|
||||
let side = match source {
|
||||
Layer::Oa => "oa",
|
||||
Layer::Sa => "sa",
|
||||
};
|
||||
let mut missing = BTreeSet::new();
|
||||
for (path, kind) in historical_source_slots(&base) {
|
||||
if kind != HistoricalSourceKind::Reference {
|
||||
continue;
|
||||
}
|
||||
let rest = &path[2..];
|
||||
if !rest.windows(2).any(|w| w[0] == side && w[1] == "Value") {
|
||||
continue;
|
||||
}
|
||||
let mut leaf = &mut json;
|
||||
for part in rest {
|
||||
leaf = if leaf.is_array() {
|
||||
leaf.get_mut(
|
||||
part.parse::<usize>()
|
||||
.map_err(|_| "Invalid historical array index")?,
|
||||
)
|
||||
} else {
|
||||
leaf.get_mut(part)
|
||||
}
|
||||
.ok_or("Invalid historical source slot")?;
|
||||
}
|
||||
let id: Id = serde_json::from_value(leaf.clone()).map_err(|e| e.to_string())?;
|
||||
if let Some(new) = map.get(&id) {
|
||||
*leaf = serde_json::to_value(new).unwrap();
|
||||
} else {
|
||||
missing.insert(id);
|
||||
}
|
||||
}
|
||||
if !missing.is_empty() {
|
||||
return Err(format!(
|
||||
"Missing explicit reference correspondence: {missing:?}"
|
||||
));
|
||||
}
|
||||
serde_json::from_value(json).map_err(|e| e.to_string())
|
||||
}
|
||||
pub(crate) fn check_keys(selector: &FacetSelector, map: &BTreeMap<Id, Id>) -> Result<(), String> {
|
||||
fn pair(keys: &KeyPair<Id>, map: &BTreeMap<Id, Id>) -> Result<(), String> {
|
||||
if let (Some(oa), Some(sa)) = (keys.oa, keys.sa)
|
||||
&& map.get(&oa) != Some(&sa)
|
||||
{
|
||||
return Err("Key pair disagrees with frozen reference correspondence".into());
|
||||
}
|
||||
if keys.oa.is_none() && keys.sa.is_none() {
|
||||
return Err("Empty key pair".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
match selector {
|
||||
FacetSelector::Member { keys, .. }
|
||||
| FacetSelector::Binding(keys)
|
||||
| FacetSelector::Child(keys)
|
||||
| FacetSelector::Incident { keys, .. }
|
||||
| FacetSelector::Artifact { keys, .. } => pair(keys, map),
|
||||
FacetSelector::Annotation(keys) => {
|
||||
if let (Some(oa), Some(sa)) = (&keys.oa, &keys.sa) {
|
||||
if oa.path.len() != sa.path.len() {
|
||||
return Err("Annotation path pairing disagrees".into());
|
||||
}
|
||||
for (oa, sa) in oa
|
||||
.path
|
||||
.iter()
|
||||
.chain([&oa.field])
|
||||
.zip(sa.path.iter().chain([&sa.field]))
|
||||
{
|
||||
pair(
|
||||
&KeyPair {
|
||||
oa: Some(*oa),
|
||||
sa: Some(*sa),
|
||||
},
|
||||
map,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
if keys.oa.is_none() && keys.sa.is_none() {
|
||||
return Err("Empty annotation pair".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
FacetSelector::Literal(keys) => {
|
||||
if keys
|
||||
.oa
|
||||
.as_ref()
|
||||
.zip(keys.sa.as_ref())
|
||||
.is_some_and(|(oa, sa)| oa != sa)
|
||||
{
|
||||
return Err(
|
||||
"Enum literal identity is exact text; renaming is removal/addition".into(),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply just one selected fact to an in-memory expected project. The caller
|
||||
/// subsequently prepares ordinary Commands and checks every induced difference.
|
||||
pub(crate) fn write_fact(
|
||||
p: &mut Project,
|
||||
target: Id,
|
||||
fact: &FacetReview,
|
||||
from: Layer,
|
||||
target_layer: Layer,
|
||||
preserve_target_only: bool,
|
||||
) -> Result<(), String> {
|
||||
match fact {
|
||||
FacetReview::Member {
|
||||
collection: which,
|
||||
keys,
|
||||
review,
|
||||
} => {
|
||||
if preserve_target_only && chosen(keys, from).is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
let present = *value(review, from)?;
|
||||
let key = chosen(keys, target_layer)
|
||||
.copied()
|
||||
.ok_or("Map the target collection member explicitly")?;
|
||||
let mut list = collection(p, target, *which).ok_or("Inapplicable target collection")?;
|
||||
if present {
|
||||
if !list.contains(&key) {
|
||||
list.push(key);
|
||||
}
|
||||
} else {
|
||||
list.retain(|id| *id != key);
|
||||
}
|
||||
set_collection(p, target, *which, list)
|
||||
}
|
||||
FacetReview::Order {
|
||||
collection: which,
|
||||
review,
|
||||
} => {
|
||||
let supplied = value(review, from)?.clone();
|
||||
let current = collection(p, target, order_collection(*which))
|
||||
.ok_or("Inapplicable target order")?;
|
||||
let supplied_set: BTreeSet<_> = supplied.iter().copied().collect();
|
||||
let current_set: BTreeSet<_> = current.iter().copied().collect();
|
||||
if supplied_set != current_set {
|
||||
if preserve_target_only
|
||||
&& supplied_set.is_subset(¤t_set)
|
||||
&& current
|
||||
.iter()
|
||||
.filter(|id| supplied_set.contains(id))
|
||||
.copied()
|
||||
.collect::<Vec<_>>()
|
||||
== supplied
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
return Err("Order must contain every surviving target member exactly once; target-only insertion requires an explicit final order".into());
|
||||
}
|
||||
set_collection(p, target, order_collection(*which), supplied)
|
||||
}
|
||||
FacetReview::Literal { keys, review } => {
|
||||
if preserve_target_only && chosen(keys, from).is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
let present = *value(review, from)?;
|
||||
let key = chosen(keys, target_layer).ok_or("Missing literal key on target")?;
|
||||
let Some(SharedBody::DataType {
|
||||
definition: DataTypeDefinition::Enumeration { literals },
|
||||
..
|
||||
}) = p.elements.get_mut(&target).and_then(Element::shared_mut)
|
||||
else {
|
||||
return Err("Target is not an enumeration".into());
|
||||
};
|
||||
if present {
|
||||
if !literals.contains(key) {
|
||||
literals.push(key.clone());
|
||||
}
|
||||
} else {
|
||||
literals.retain(|v| v != key);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
FacetReview::Annotation { keys, review } => {
|
||||
if preserve_target_only && chosen(keys, from).is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
let key = chosen(keys, target_layer).ok_or("Missing target annotation key")?;
|
||||
let row = value(review, from)?.clone();
|
||||
if row.as_ref().is_some_and(|v| AnnotationKey::from(v) != *key) {
|
||||
return Err(
|
||||
"Annotation candidate changes its row key; review removal/addition".into(),
|
||||
);
|
||||
}
|
||||
let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { values, .. },
|
||||
..
|
||||
}) = p.elements.get_mut(&target).and_then(Element::shared_mut)
|
||||
else {
|
||||
return Err("Target has no message annotations".into());
|
||||
};
|
||||
values.retain(|v| AnnotationKey::from(v) != *key);
|
||||
if let Some(row) = row {
|
||||
values.push(row);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
FacetReview::Binding { keys, review } => {
|
||||
if preserve_target_only && chosen(keys, from).is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
let key = *chosen(keys, target_layer).ok_or("Missing target binding key")?;
|
||||
let row = value(review, from)?.clone();
|
||||
if row.as_ref().is_some_and(|v| v.referenced != key) {
|
||||
return Err(
|
||||
"Binding candidate changes its row key; review removal/addition".into(),
|
||||
);
|
||||
}
|
||||
let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::ScenarioUse { bindings, .. },
|
||||
..
|
||||
}) = p.elements.get_mut(&target).and_then(Element::shared_mut)
|
||||
else {
|
||||
return Err("Target has no scenario bindings".into());
|
||||
};
|
||||
bindings.retain(|v| v.referenced != key);
|
||||
if let Some(row) = row {
|
||||
bindings.push(row);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
FacetReview::Child { keys, review } => {
|
||||
if preserve_target_only && chosen(keys, from).is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
let key = chosen(keys, target_layer).ok_or("Map target child explicitly")?;
|
||||
let e = p
|
||||
.elements
|
||||
.get_mut(key)
|
||||
.ok_or("Target child does not exist; create it through transition mapping")?;
|
||||
if *value(review, from)? {
|
||||
e.parent = Some(target);
|
||||
} else if e.parent == Some(target) {
|
||||
e.parent = None;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
FacetReview::Incident { keys, end, review } => {
|
||||
if preserve_target_only && chosen(keys, from).is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
let key = chosen(keys, target_layer).ok_or("Map incident relation explicitly")?;
|
||||
if !*value(review, from)? {
|
||||
return Err("Unlinking an incident relation requires an explicit final endpoint through the relation trace".into());
|
||||
}
|
||||
let relation = p
|
||||
.relations
|
||||
.get_mut(key)
|
||||
.ok_or("Mapped incident relation is absent")?;
|
||||
let (a, b) = relation.body.endpoints();
|
||||
match end {
|
||||
IncidentEnd::Source => relation.body.set_endpoints(target, b),
|
||||
IncidentEnd::Target => relation.body.set_endpoints(a, target),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
FacetReview::Artifact { keys, review } => {
|
||||
if preserve_target_only && chosen(keys, from).is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
let key = chosen(keys, target_layer).ok_or("Map artifact explicitly")?;
|
||||
if let ArtifactReview::Presence(review) = review {
|
||||
if !*value(review, from)? {
|
||||
p.artifact_links.remove(key);
|
||||
return Ok(());
|
||||
}
|
||||
if p.artifact_links.get(key).is_some_and(|a| a.owner == target) {
|
||||
return Ok(());
|
||||
}
|
||||
return Err("Artifact creation requires a complete typed candidate".into());
|
||||
}
|
||||
let a = p
|
||||
.artifact_links
|
||||
.get_mut(key)
|
||||
.ok_or("Artifact candidate is absent")?;
|
||||
if a.owner != target {
|
||||
return Err(
|
||||
"Artifact metadata can only be reviewed on its currently attached owner trace"
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
match review {
|
||||
ArtifactReview::Presence(_) => unreachable!(),
|
||||
ArtifactReview::Name(r) => a.name = value(r, from)?.clone(),
|
||||
ArtifactReview::Description(r) => a.description = value(r, from)?.clone(),
|
||||
ArtifactReview::Owner(r) => a.owner = *value(r, from)?,
|
||||
ArtifactReview::Relation(r) => a.relation = *value(r, from)?,
|
||||
ArtifactReview::Uri(r) => a.uri = value(r, from)?.clone(),
|
||||
ArtifactReview::Revision(r) => a.revision = value(r, from)?.clone(),
|
||||
ArtifactReview::Fragment(r) => a.fragment = value(r, from)?.clone(),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
_ => {
|
||||
let (oa, sa) = sides(fact);
|
||||
let side = match from {
|
||||
Layer::Oa => oa,
|
||||
Layer::Sa => sa,
|
||||
};
|
||||
let value = side
|
||||
.get("Value")
|
||||
.ok_or("Selected branch is not applicable")?
|
||||
.clone();
|
||||
write_scalar(p, target, &fact.selector(), value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn constructor_contains(group: &FacetSelector, facet: &FacetSelector) -> bool {
|
||||
use FacetSelector as F;
|
||||
match group {
|
||||
F::Data(DataFacet::Constructor) => {
|
||||
matches!(facet,F::Data(f) if *f != DataFacet::Unit) || matches!(facet, F::Literal(_))
|
||||
}
|
||||
F::Port(PortFacet::Constructor) => matches!(
|
||||
facet,
|
||||
F::Port(_)
|
||||
| F::Member {
|
||||
collection: ReferenceCollection::FunctionInputItems
|
||||
| ReferenceCollection::FunctionOutputItems,
|
||||
..
|
||||
}
|
||||
),
|
||||
F::Occurrence(OccurrenceFacet::Constructor) => {
|
||||
matches!(facet,F::Occurrence(f) if !matches!(f,OccurrenceFacet::Precondition | OccurrenceFacet::Postcondition))
|
||||
|| matches!(
|
||||
facet,
|
||||
F::Annotation(_)
|
||||
| F::Binding(_)
|
||||
| F::Member {
|
||||
collection: ReferenceCollection::Steps
|
||||
| ReferenceCollection::FragmentBranches,
|
||||
..
|
||||
}
|
||||
| F::Order(_)
|
||||
)
|
||||
}
|
||||
F::Occurrence(OccurrenceFacet::MessageKind) => matches!(
|
||||
facet,
|
||||
F::Occurrence(OccurrenceFacet::MessageKind | OccurrenceFacet::TimerExpression)
|
||||
),
|
||||
F::Event(EventFacet::Constructor) => matches!(
|
||||
facet,
|
||||
F::Event(_)
|
||||
| F::Member {
|
||||
collection: ReferenceCollection::SignalItems,
|
||||
..
|
||||
}
|
||||
),
|
||||
F::State(StateFacet::Kind) => facet == group,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
pub(crate) fn copy_constructor(
|
||||
p: &mut Project,
|
||||
target: Id,
|
||||
source: &SemanticRecord,
|
||||
group: &FacetSelector,
|
||||
) -> Result<(), String> {
|
||||
let SemanticRecord::Element(source) = source else {
|
||||
return Err("Constructor source must be an element".into());
|
||||
};
|
||||
let target = p
|
||||
.elements
|
||||
.get_mut(&target)
|
||||
.ok_or("Missing target element")?;
|
||||
let a = source.shared().ok_or("Source has no shared constructor")?;
|
||||
let b = target
|
||||
.shared_mut()
|
||||
.ok_or("Target has no shared constructor")?;
|
||||
match (group, a, b) {
|
||||
(
|
||||
FacetSelector::Data(DataFacet::Constructor),
|
||||
SharedBody::DataType {
|
||||
definition,
|
||||
default,
|
||||
..
|
||||
},
|
||||
SharedBody::DataType {
|
||||
definition: d,
|
||||
default: v,
|
||||
..
|
||||
},
|
||||
) => {
|
||||
*d = definition.clone();
|
||||
*v = default.clone();
|
||||
}
|
||||
(
|
||||
FacetSelector::Port(PortFacet::Constructor),
|
||||
SharedBody::Port { contract, legacy },
|
||||
SharedBody::Port {
|
||||
contract: c,
|
||||
legacy: l,
|
||||
},
|
||||
) => {
|
||||
*c = contract.clone();
|
||||
*l = legacy.clone();
|
||||
}
|
||||
(
|
||||
FacetSelector::Occurrence(OccurrenceFacet::Constructor),
|
||||
SharedBody::Occurrence { body, .. },
|
||||
SharedBody::Occurrence { body: b, .. },
|
||||
) => *b = body.clone(),
|
||||
(
|
||||
FacetSelector::Occurrence(OccurrenceFacet::MessageKind),
|
||||
SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { message_kind, .. },
|
||||
..
|
||||
},
|
||||
SharedBody::Occurrence {
|
||||
body:
|
||||
OccurrenceBody::Exchange {
|
||||
message_kind: k, ..
|
||||
},
|
||||
..
|
||||
},
|
||||
) => *k = message_kind.clone(),
|
||||
(
|
||||
FacetSelector::Event(EventFacet::Constructor),
|
||||
SharedBody::Event { body },
|
||||
SharedBody::Event { body: b },
|
||||
) => *b = body.clone(),
|
||||
(
|
||||
FacetSelector::State(StateFacet::Kind),
|
||||
SharedBody::State { kind, .. },
|
||||
SharedBody::State { kind: k, .. },
|
||||
) => *k = *kind,
|
||||
_ => return Err("Constructor family mismatch".into()),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,255 @@
|
||||
use crate::model::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub enum Observed<T> {
|
||||
NotApplicable,
|
||||
Value(T),
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Reviewed<T> {
|
||||
pub oa: Observed<T>,
|
||||
pub sa: Observed<T>,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct KeyPair<K> {
|
||||
pub oa: Option<K>,
|
||||
pub sa: Option<K>,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct AnnotationKey {
|
||||
pub path: Vec<Id>,
|
||||
pub field: Id,
|
||||
}
|
||||
impl From<&MessageValue> for AnnotationKey {
|
||||
fn from(value: &MessageValue) -> Self {
|
||||
Self {
|
||||
path: value.path.clone(),
|
||||
field: value.field,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum DataConstructor {
|
||||
Boolean,
|
||||
Integer,
|
||||
Real,
|
||||
String,
|
||||
Enumeration,
|
||||
Record,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PortConstructor {
|
||||
FunctionInput,
|
||||
FunctionOutput,
|
||||
Component,
|
||||
Legacy,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum OccurrenceConstructor {
|
||||
Exchange,
|
||||
Action,
|
||||
Control,
|
||||
SequenceReference,
|
||||
ScenarioUse,
|
||||
State,
|
||||
Fragment,
|
||||
Branch,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum EventConstructor {
|
||||
Signal,
|
||||
Time,
|
||||
Change,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum MessageConstructor {
|
||||
Asynchronous,
|
||||
Call,
|
||||
Reply,
|
||||
Create,
|
||||
Delete,
|
||||
Timer,
|
||||
}
|
||||
|
||||
// This finite catalog is also consumed by the typed historical leaf visitor.
|
||||
// Each selector fixes its payload type; there is no arbitrary value/path schema.
|
||||
macro_rules! scalar_families {
|
||||
($m:ident) => { $m! {
|
||||
Header, HeaderFacet, HeaderReview {
|
||||
Name: String, Description: String, Parent: Option<Id>
|
||||
}
|
||||
ExchangeItem, ExchangeItemFacet, ExchangeItemReview {
|
||||
Nature: ExchangeItemKind, Mechanism: ExchangeMechanism, Supertype: Option<Id>
|
||||
}
|
||||
Interface, InterfaceFacet, InterfaceReview { Supertype: Option<Id> }
|
||||
Data, DataFacet, DataReview {
|
||||
Constructor: DataConstructor,
|
||||
IntegerMinimum: Option<i64>, IntegerMaximum: Option<i64>,
|
||||
RealMinimum: Option<f64>, RealMaximum: Option<f64>,
|
||||
StringMinimumLength: u32, StringMaximumLength: Option<u32>,
|
||||
RecordSupertype: Option<Id>, Default: Option<DataValue>, Unit: String
|
||||
}
|
||||
Field, FieldFacet, FieldReview {
|
||||
Type: TypeRef, Minimum: u32, Maximum: Option<u32>, Unit: String,
|
||||
Direction: ParameterDirection, Storage: FieldStorage, Default: Option<Vec<DataValue>>
|
||||
}
|
||||
Port, PortFacet, PortReview {
|
||||
Constructor: PortConstructor, ComponentKind: ComponentPortKind,
|
||||
Orientation: ComponentPortOrientation, LegacyDirection: PortDirection,
|
||||
LegacyInterface: Option<Id>
|
||||
}
|
||||
Scenario, ScenarioFacet, ScenarioReview {
|
||||
Viewpoint: ScenarioViewpoint, Context: Option<Id>, Precondition: String, Postcondition: String
|
||||
}
|
||||
Lifeline, LifelineFacet, LifelineReview { Represented: Option<Id> }
|
||||
Occurrence, OccurrenceFacet, OccurrenceReview {
|
||||
Constructor: OccurrenceConstructor, Precondition: String, Postcondition: String,
|
||||
Exchange: Option<Id>, Sender: Option<Id>, Receiver: Option<Id>,
|
||||
MessageKind: MessageConstructor, TimerExpression: String, ReplyTo: Option<Id>,
|
||||
Action: Option<Id>, ActionLifeline: Option<Id>, ControlKind: ControlKind,
|
||||
Sequence: Option<Id>, Scenario: Option<Id>, State: Option<Id>,
|
||||
StateLifeline: Option<Id>, FragmentOperator: FragmentOperator, BranchGuard: String
|
||||
}
|
||||
Event, EventFacet, EventReview {
|
||||
Constructor: EventConstructor, TimeKind: TimeEventKind, TimeExpression: String, ChangeExpression: String
|
||||
}
|
||||
State, StateFacet, StateReview { Kind: StateKind, Entry: String, Exit: String, DoActivity: String }
|
||||
Constraint, ConstraintFacet, ConstraintReview { Expression: String }
|
||||
Relation, RelationFacet, RelationReview {
|
||||
Source: Id, Target: Id, AllocationOrder: u32, Transmission: TransmissionProtocol,
|
||||
Acquisition: AcquisitionProtocol, SourceRole: String, TargetRole: String,
|
||||
SourceMinimum: u32, SourceMaximum: Option<u32>, TargetMinimum: u32,
|
||||
TargetMaximum: Option<u32>, SourceNavigable: bool, TargetNavigable: bool,
|
||||
SourcePort: Id, TargetPort: Id, Oriented: bool, ExtendCondition: String,
|
||||
SequenceGuard: String, TransitionKind: TransitionKind, Trigger: String,
|
||||
Guard: String, Effect: String, StateActionPhase: StateActionKind
|
||||
}
|
||||
}};
|
||||
}
|
||||
pub(super) use scalar_families;
|
||||
|
||||
macro_rules! define_families {
|
||||
($( $family:ident, $selector:ident, $review:ident { $( $leaf:ident: $ty:ty ),* } )*) => {
|
||||
$(
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub enum $selector { $( $leaf ),* }
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub enum $review { $( $leaf(Reviewed<$ty>) ),* }
|
||||
)*
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub enum FacetSelector {
|
||||
$( $family($selector), )*
|
||||
Member { collection: ReferenceCollection, keys: KeyPair<Id> },
|
||||
Order(OrderedCollection),
|
||||
Literal(KeyPair<String>),
|
||||
Annotation(KeyPair<AnnotationKey>),
|
||||
Binding(KeyPair<Id>),
|
||||
Child(KeyPair<Id>),
|
||||
Incident { keys: KeyPair<Id>, end: IncidentEnd },
|
||||
Artifact { keys: KeyPair<Id>, facet: ArtifactFacet },
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub enum FacetReview {
|
||||
$( $family($review), )*
|
||||
Member { collection: ReferenceCollection, keys: KeyPair<Id>, review: Reviewed<bool> },
|
||||
Order { collection: OrderedCollection, review: Reviewed<Vec<Id>> },
|
||||
Literal { keys: KeyPair<String>, review: Reviewed<bool> },
|
||||
Annotation { keys: KeyPair<AnnotationKey>, review: Reviewed<Option<MessageValue>> },
|
||||
Binding { keys: KeyPair<Id>, review: Reviewed<Option<LifelineBinding>> },
|
||||
Child { keys: KeyPair<Id>, review: Reviewed<bool> },
|
||||
Incident { keys: KeyPair<Id>, end: IncidentEnd, review: Reviewed<bool> },
|
||||
Artifact { keys: KeyPair<Id>, review: ArtifactReview },
|
||||
}
|
||||
impl FacetReview {
|
||||
pub fn selector(&self) -> FacetSelector {
|
||||
match self {
|
||||
$( Self::$family(value) => FacetSelector::$family(match value {
|
||||
$( $review::$leaf(_) => $selector::$leaf, )*
|
||||
}), )*
|
||||
Self::Member { collection, keys, .. } => FacetSelector::Member { collection: *collection, keys: keys.clone() },
|
||||
Self::Order { collection, .. } => FacetSelector::Order(*collection),
|
||||
Self::Literal { keys, .. } => FacetSelector::Literal(keys.clone()),
|
||||
Self::Annotation { keys, .. } => FacetSelector::Annotation(keys.clone()),
|
||||
Self::Binding { keys, .. } => FacetSelector::Binding(keys.clone()),
|
||||
Self::Child { keys, .. } => FacetSelector::Child(keys.clone()),
|
||||
Self::Incident { keys, end, .. } => FacetSelector::Incident { keys: keys.clone(), end: *end },
|
||||
Self::Artifact { keys, review } => FacetSelector::Artifact { keys: keys.clone(), facet: review.facet() },
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
scalar_families!(define_families);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub enum ReferenceCollection {
|
||||
Steps,
|
||||
FragmentBranches,
|
||||
FunctionInputItems,
|
||||
FunctionOutputItems,
|
||||
SignalItems,
|
||||
ExchangeItems,
|
||||
TransitionTriggers,
|
||||
TransitionEffects,
|
||||
Constrained,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub enum OrderedCollection {
|
||||
Steps,
|
||||
FragmentBranches,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub enum IncidentEnd {
|
||||
Source,
|
||||
Target,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub enum ArtifactFacet {
|
||||
Presence,
|
||||
Name,
|
||||
Description,
|
||||
Owner,
|
||||
Relation,
|
||||
Uri,
|
||||
Revision,
|
||||
Fragment,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub enum ArtifactReview {
|
||||
Presence(Reviewed<bool>),
|
||||
Name(Reviewed<String>),
|
||||
Description(Reviewed<String>),
|
||||
Owner(Reviewed<Id>),
|
||||
Relation(Reviewed<ArtifactRelation>),
|
||||
Uri(Reviewed<String>),
|
||||
Revision(Reviewed<Option<String>>),
|
||||
Fragment(Reviewed<Option<String>>),
|
||||
}
|
||||
impl ArtifactReview {
|
||||
pub fn facet(&self) -> ArtifactFacet {
|
||||
match self {
|
||||
Self::Presence(_) => ArtifactFacet::Presence,
|
||||
Self::Name(_) => ArtifactFacet::Name,
|
||||
Self::Description(_) => ArtifactFacet::Description,
|
||||
Self::Owner(_) => ArtifactFacet::Owner,
|
||||
Self::Relation(_) => ArtifactFacet::Relation,
|
||||
Self::Uri(_) => ArtifactFacet::Uri,
|
||||
Self::Revision(_) => ArtifactFacet::Revision,
|
||||
Self::Fragment(_) => ArtifactFacet::Fragment,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct SemanticBaseline {
|
||||
pub facets: Vec<FacetReview>,
|
||||
}
|
||||
Reference in New Issue
Block a user