Implement closed typed semantic review with immutable proposals

This commit is contained in:
2026-09-06 05:20:54 +03:00
parent ab960cbebb
commit 346fb24692
19 changed files with 5855 additions and 8 deletions
@@ -507,6 +507,7 @@ fn workshop() -> Project {
let baseline = TraceBaseline {
oa: p.properties(t.oa).unwrap(),
sa: p.properties(t.sa).unwrap(),
semantic: None,
};
p.traces.get_mut(&id).unwrap().baseline = Some(baseline);
}
+1
View File
@@ -111,6 +111,7 @@ fn trace(p: &mut Project, oa: Id, sa: Id, kind: TraceKind) {
baseline: Some(TraceBaseline {
oa: p.properties(oa).unwrap(),
sa: p.properties(sa).unwrap(),
semantic: None,
}),
},
);
+2
View File
@@ -7,6 +7,7 @@ pub mod ports;
pub mod projection;
pub mod references;
pub mod routing;
pub mod semantic;
pub mod session;
pub mod source_files;
pub mod source_project;
@@ -14,6 +15,7 @@ pub mod transitions;
pub mod validation;
pub use model::*;
pub use ports::*;
pub use semantic::*;
pub use session::*;
mod demo;
mod state_validation;
+5 -3
View File
@@ -6,7 +6,7 @@ use std::{
use uuid::Uuid;
pub type Id = Uuid;
pub const SCHEMA_VERSION: u32 = 4;
pub const SCHEMA_VERSION: u32 = 5;
pub const MAX_COORDINATE: f32 = 1_000_000.;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
@@ -1492,13 +1492,15 @@ pub struct TracedProperties {
pub name: String,
pub description: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TraceBaseline {
pub oa: TracedProperties,
pub sa: TracedProperties,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub semantic: Option<crate::semantic::SemanticBaseline>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Trace {
pub id: Id,
+7
View File
@@ -222,6 +222,13 @@ pub fn remap_references(p: &mut Project, old: Id, new: Id) {
if t.sa == old {
t.sa = new;
}
if let Some(baseline) = t.baseline.as_mut().and_then(|b| b.semantic.as_mut()) {
crate::semantic::visit_semantic_references(baseline, |id| {
if *id == old {
*id = new;
}
});
}
}
}
/// IDs removed by a cascade. Cross-layer traced counterparts are never included.
+16
View File
@@ -0,0 +1,16 @@
//! Closed typed facts for deliberately reviewed OA/SA semantics.
//!
//! Historical facts are independently acknowledged observations, not a synthetic
//! live model. Their UUIDs are not live references and survive ordinary deletion.
mod catalog;
mod historical;
mod linked;
mod mutate;
mod review;
mod types;
pub use catalog::*;
pub(crate) use historical::{HistoricalSourceKind, historical_source_slots};
pub use historical::{validate_semantic_baseline, visit_semantic_references};
pub use linked::*;
pub use review::*;
pub use types::*;
File diff suppressed because it is too large Load Diff
+404
View File
@@ -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,
}
}
+174
View File
@@ -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)
}
+592
View File
@@ -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(&current_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
+255
View File
@@ -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>,
}
+13 -1
View File
@@ -202,9 +202,15 @@ fn mapped_body(e: &Element, kind: Kind, layer: Layer) -> Option<ElementBody> {
return None;
}
if let Some(shared) = e.shared()
&& kind == e.kind()
&& (kind == e.kind() || (kind.is_state() && e.kind().is_state()))
{
let mut shared = shared.clone();
if let SharedBody::State {
kind: state_kind, ..
} = &mut shared
{
*state_kind = kind.state_kind()?;
}
if let SharedBody::Scenario { viewpoint, .. } = &mut shared {
*viewpoint = match (*viewpoint, layer) {
(ScenarioViewpoint::OperationalActivity, Layer::Sa) => {
@@ -927,6 +933,7 @@ pub fn plan_transition(
.unwrap_or(TraceBaseline {
oa: current_oa.clone(),
sa: current_sa.clone(),
semantic: None,
});
for field in &reviewed_fields {
set(&mut baseline.oa, *field, value(&current_oa, *field).clone());
@@ -1081,6 +1088,11 @@ pub fn counterpart_kinds(source: Kind, direction: TransitionDirection) -> Vec<Ki
}
fn compatible_kinds(source: Kind, target: Kind, direction: TransitionDirection) -> bool {
// State kind is a reviewed structural facet of one state family. An existing
// counterpart remains selectable after either side changes its vertex kind.
if source.is_state() && target.is_state() {
return true;
}
if matches!(
(direction, source, target),
(
+13
View File
@@ -876,6 +876,19 @@ pub fn validate(p: &Project) -> Vec<Issue> {
text(&mut out, t.id, "baseline.name", &props.name);
text(&mut out, t.id, "baseline.description", &props.description);
}
if let Some(semantic) = &b.semantic
&& let Err(errors) = crate::semantic::validate_semantic_baseline(semantic)
{
for message in errors {
error(
&mut out,
t.id,
"semantic_baseline",
"baseline.semantic",
message,
);
}
}
}
match crate::transitions::trace_drift(p, t) {
crate::transitions::Drift::Unchanged => {}
File diff suppressed because it is too large Load Diff
+1
View File
@@ -42,6 +42,7 @@ fn make_trace(p: &mut Project, oa: Id, sa: Id) -> Id {
baseline: Some(TraceBaseline {
oa: p.properties(oa).unwrap(),
sa: p.properties(sa).unwrap(),
semantic: None,
}),
};
p.traces.insert(id, t);
+33
View File
@@ -78,3 +78,36 @@ State camera/tool lifetime follows effective machine and diagram identity, even
when selection changes through Catalog or Layer outside Behavior. Reset only on
actual context change; preserve same-view return. Independent review reproduced
the leak and native Windows/Linux tests verified the correction in316dc55.
## Semantic review proposals and historical admission (2026-09-06)
R3 stores closed, typed, independently reviewed OA/SA facet pairs. Constructor
facts store discriminators only; explicit group decisions enumerate and retire
their exact branch facts. No persisted recursive value tree, own-record carrier,
review mask or order shadow is introduced. The complete contract is
release-semantic-review-implementation.md.
Derive every source proposal from the same immutable initial project. Reset old
mapping selectors before deriving scopes; compare all desired canonical target
slots, including constructor removals, before one materialization. Audit selected
values against both that result and the ordinarily prepared project. This prevents
sequential last-write wins, even when one source introduces a member another
constructor removes. Preserve target-only members during same-target-constructor
reconciliation. Artifact attachment and explicit owner changes share the actual
artifact's owner slot; child/incident operations similarly share actual parent or
endpoint slots. Exact prepared cleanup impacts require explicit acknowledgement
and may not contradict a selected Keep/Take.
Artifact metadata is observed by identified record after authorization against
its initial attachment. An owner move therefore retains the actual resulting
Owner/Name facts, including target-only links; Presence remains owner-relative.
New metadata decisions require the current attached owner trace. Historical paired
selectors survive disappearance of one side and suppress redundant one-sided
projections. A new pairing cannot suppress prior one-sided history without reset.
Current resolution applicability and historical baseline admission are separate.
New decisions require a compatible current family and an applicable side; historical
typed facts can outlive former constructors/families and deleted references. Their
typed reference visitor supports explicit reidentification but does not add live
deletion dependencies. Derived linked-definition drift remains bounded and exposes
missing review coverage instead of copying a shared definition into every baseline.
+73 -4
View File
@@ -120,10 +120,10 @@ Root owns API/OpenAPI/reST, docs, integration and native input. No concurrent wr
scenario-value authoring, then full integrated Windows/Linux verification.
2. Finish R6 core/source/desktop, integrate actual REST/schema/reST, independent
semantic/correctness/UX review, Windows/Linux checks and real data authoring.
3. Implement R3 in the isolated semantic-review checkout using the selected closed
typed per-facet review representation (C). Current production reconciliation
remains prose-only until implementation is verified. The prior typed-carrier
preference is superseded by release-semantic-review-implementation.md.
3. Integrate the verified R3 core/source checkpoint from the isolated semantic-review
checkout, then complete desktop/server and native/platform acceptance. The closed
typed per-facet representation (C) and its immutable-proposal rule are recorded
in release-semantic-review-implementation.md; this is not release acceptance.
4. Complete direct scenario/process gestures, persistent coverage/filters,
Home/recent/recovery, hosted desktop ETag workflows and offline guidance.
5. Fresh final requirements/domain/correctness/native UX/architecture review; full
@@ -140,6 +140,75 @@ critic justified direct typed facts instead of carrier masks/order shadows; more
adapter code is accepted for fewer persisted invariants. This is an active
implementation checkpoint, not R3 acceptance. Root owns concurrent R6 native work.
## R3 core/source frozen checkpoint (2026-09-06)
The isolated semantic-review tree now contains the complete 84-scalar/13-family
catalog, keyed member/order/annotation/binding/child/incident/artifact facts,
bounded linked-definition drift, typed historical reference/remap/admission, and
bidirectional mapped review through one ordinary prepared Command batch. Source
and strict schema1–4→5 compatibility are committed in ab960cb; this core checkpoint
adds the closed review implementation. Desktop/API integration and Windows/Linux
native/platform acceptance remain root-owned work. No native input was used here.
Every source derives proposals from one immutable initial project. Reset old
selectors before scope derivation, compare the union of selected canonical target
slots (including constructor removals), materialize once, then audit every desired
value before and after command preparation. Same-target constructor reconciliation
preserves target-only members; induced cleanup needs exact explicit review and
cannot override an independently selected Keep/Take. Historical facts may outlive
former record families/deleted UUIDs; new resolutions still require current family
and leaf applicability. DECISIONS records why these are distinct admission rules.
Independent review's original five findings and four residuals are covered by these
passing cases in crates/oa-core/tests/r3_semantic.rs:
| Finding | Closure regression |
| --- | --- |
| Repeated unequal constructor Keep hides bound drift | repeated_keep_of_unequal_constructor_pair_cannot_swallow_bound_drift |
| StateKind change fails legacy mapping preflight | changed_state_kind_remains_reviewable_in_both_directions |
| Artifact metadata writes collide through different owner traces | artifact_metadata_is_attached_and_cannot_last_write_through_two_owner_traces |
| Constructor retirement overrides another source's Keep | constructor_retirement_cannot_override_other_sources_kept_bound_in_either_order |
| Partial review falsely rolls up as unchanged | semantic_rollup_does_not_claim_unchanged_when_only_unit_is_reviewed |
| Another source's introduced member escapes constructor removal agreement | constructor_union_conflicts_with_another_sources_new_member_in_either_order |
| Source constructor history causes target-only member loss | same_target_constructor_preserves_local_members_and_reset_precedes_expansion |
| Reset old mapping selector reappears during group expansion | same_target_constructor_preserves_local_members_and_reset_precedes_expansion |
| Artifact Owner cannot explicitly move the attached link | artifact_owner_candidate_moves_link_and_conflicts_with_kept_attachment |
The additional combined_ordered_membership_commutes_or_requires_agreed_anchors
case verifies both request orders: independent removals preserve the local member;
contradictory inferred insertion anchors reject. Coverage includes atomic rollback,
one undo/redo, stale plans, no-op replanning, explicit cleanup acknowledgement,
partial Keep/Take, qualified rows, mapping resets and historical deleted refs.
Final bounded follow-up extends those same Owner/reset findings:
target_only_artifact_move_records_owner_and_metadata_in_either_order covers an
unpaired target link, Owner alone and Owner+Name in either order, actual resulting
typed owner history, and rejected future metadata review through the former owner.
retired_source_branch_retains_historical_member_pair_in_both_directions preserves
the established pair after a branch disappears instead of adding a colliding
one-sided projection. The opposite appearance case still needs an explicit reset.
The independent read-only re-review found no remaining concrete defect within the
nine findings and these two subsets, including the immutable proposal/union/audit
structure. That is scoped core review closure, not UI/API/spec release acceptance.
Final Windows core evidence in D:/CodexBuilds/Capella2-semantic-review:
```text
cargo test --package oa-core --all-targets --locked --offline --config target/semantic/cargo.toml --target-dir D:/CodexBuilds/Capella2-semantic-target --no-fail-fast
cargo clippy --package oa-core --all-targets --locked --offline --config target/semantic/cargo.toml --target-dir D:/CodexBuilds/Capella2-semantic-target -- -D warnings
```
Both exit0: 345 passed, 0 failed, 1 intentionally ignored inheritance scaling
probe across 33 test executables; strict all-target Clippy has no warnings.
Logs: target/semantic/core-final-all-tests.log and core-final-clippy.log. Focused
immutable-proposals-artifact-focus.log contains 28 semantic and 7 source tests; the
source suite includes real prepared writeback and included-file save/reopen. The
catalog coverage unit checks every current model payload field's closed capture
and write scope. These logs use the distinct semantic-target and vendored config.
Native input remains paused following the user's physical stop. Root must handle
resumed native authorization and integrated acceptance; the broader goal is active.
Native input is paused after physical Escape; no R6 native trial has run.
Root exclusively owns native input. Latest saved Windows trial is window1706782,
ui-state-navigation-windows,49objects/28links/1 expected missing-owner warning.
@@ -21,6 +21,13 @@ explicit mapping, target-only preservation, honest unreviewed state, source/JSON
parity, and one stale-checked validated transaction. No generic metamodel, graph
synchronization service, runtime value tree, or second mutation engine is added.
Compiled core catalog: `semantic/types.rs` contains 84 scalar selectors across 13
families, nine reference collections, two ordered collections, and the closed
Literal/Annotation/Binding/Child/Incident/Artifact facts. The catalog coverage test
enumerates every current element payload, constructor branch and relation kind;
an unrepresented payload field fails that gate. Serialization paths used by the
private adapter are fixed implementation details, never accepted user selectors.
## Persisted shape and granularity
`TraceBaseline` retains existing `oa`/`sa: TracedProperties` and adds
@@ -61,6 +68,15 @@ introduced/removed group atomically, retiring obsolete branch facts. Unchanged
kind cannot acknowledge unrelated bound/default drift by a Keep shortcut. An
explicit actual target switch may reconsider an earlier deliberate Keep.
Persist only `DataConstructor`, `PortConstructor`, `OccurrenceConstructor`,
`EventConstructor` and `MessageConstructor` discriminator enums (State uses the
existing StateKind). A grouped decision uses a transient typed record, retires
obsolete branch facts and stores each resulting descendant independently. A
constructor payload snapshot must never overlap later independently reviewed
leaves. Repeating Keep on an already acknowledged unequal discriminator pair
cannot acknowledge newly changed bounds/defaults. Constructor retirement writes
explicit NotApplicable target slots for agreement checking against other reviews.
Artifact reviews are attached facts of the owning semantic trace; they do not
widen ordinary Trace endpoint compatibility. Cover artifact membership and
name/description/owner/relation/URI/revision/fragment, preserving target-only links.
@@ -104,6 +120,36 @@ Command preparation. Keep the prose request/API compatible; add sibling typed
semantic request/preview/apply helpers. Whole existing typed records may appear
as transient candidates or impact snapshots, never as persisted baseline carriers.
The implemented sibling boundary is `SemanticTransitionRequest { transition,
traces, references, reviews, candidates, accepted_impacts }`.
`traces` selects a particular trace by source UUID when one endpoint pair has
several traces; `references` is canonical OA→SA correspondence, including old
deleted IDs. `SemanticReviewRequest { source, resolutions, reset }` and
`FacetResolution { facet, decision }` use KeepTarget/TakeSource/Candidate.
Candidates are full existing typed Element/Relation/Artifact records with identity
and aliases protected; they must equal the result of selected facets exactly.
`plan_semantic_transition`, `prepare_semantic_transition` and
`apply_semantic_transition` reuse the existing mapping planner and Command batch.
Previews expose typed current/drift facts, acknowledged and retired selectors,
exact typed prepared impacts, conflicts, and a stale-check fingerprint.
Artifact metadata slots are keyed by the actual artifact UUID, independent of
which owning trace reaches them. Presence and owner share the ownership slot;
metadata selection requires the currently attached owner. An explicit Owner
candidate can move that link to another valid owner; its canonical owner slot
must agree with any selected attachment decision. Creating a link
requires a complete candidate and explicit review of every introduced metadata
field. Removing a link retires its metadata and checks those removed slots against
other decisions. Historical artifact keys/owners are visited only by explicit
identity remapping, alongside all other historical UUIDs.
Attachment authorization uses the immutable initial snapshot; metadata observations
use the identified artifact record. Thus a target-only Owner or Owner+Name candidate
records the actual resulting owner/metadata after a move, independent of facet
order. Presence remains relative to the trace owner. A subsequent metadata decision
through the former owner trace is rejected, while its historical observation remains
available for drift. This avoids replacing an explicit owner value with both-side NA.
Freeze mappings before proposals. Resolve all live and historical reference
members through explicit correspondence; no display-name matching. Many sources
may affect a target only when resolved deltas agree for each canonical target
@@ -111,12 +157,46 @@ slot. A source/key-pair collision or contradictory target delta is a conflict,
independent of request order. Mapping changes require explicit invalidation/reset
of affected old reviews; never silently replace or accumulate overlapping pairs.
An established paired selector remains the identity when one side's member or
constructor branch disappears. It suppresses the redundant generated one-sided
projection, preserving an NA/remaining-value observation. The reverse case is
different: a newly appearing pair does not suppress old one-sided history and
requires an explicit reset. Apply this rule to the closed keyed selector catalog,
not only enumeration or constructor special cases.
Every selected source derives desired values from the same immutable initial
project. Apply explicit baseline resets before deriving its current catalog or
constructor scope. Union all selected canonical slots across constructor scopes,
including introduced members and removals, and reject disagreement before applying
any writes. Materialize the agreed result once and compare every selected desired
slot against it, then repeat that audit after ordinary command preparation.
An exact approved cleanup impact still cannot contradict a selected Keep/Take.
This invariant replaces sequential proposal mutation and order-dependent audits.
When the target already has the proposed constructor, preserve target-only members
while reconciling its group; a source constructor change is not whole-list deletion
authority. Independently inferred orders must agree; commuting member removals
filter those orders to surviving members without refreshing order history.
Compare each side against its own reviewed value, retaining intentionally unequal
pairs. Keep acknowledges only the selected current source/retained target fact.
Take or a typed candidate acknowledges only the selected resulting fact. Untouched
changes remain drift. Preview unreviewed differences rather than falsely claiming
unchanged semantics. A selected constructor group names every fact it acknowledges.
New resolution admission requires the appropriate current record family and at
least one applicable current side. Persisted historical facts are different: their
typed former family/constructor and deleted UUIDs remain valid history even after
the live endpoints change. Validate their closed types, keys, duplicate slots,
finite values, text and work limits without requiring present-day live references
or present-day family applicability. Historical observations are never live edges.
Linked shared-definition drift is derived, not copied into each referring baseline.
The bounded walk follows reusable data fields/types, exchange items, interfaces,
events and interface allocation declarations, stopping at 4096 objects per side
or 32768 edges with an observable diagnostic. Parent edges and arbitrary architecture
neighborhoods do not expand that walk. Missing pairs/reviews remain Unreviewed;
own and linked rollups explicitly retain their unreviewed counts.
Preserve target-only members and records. Removing source membership proposes
unlinking, not deletion of target semantic objects. Ordered insertion is automatic
only with a unique unchanged mapped anchor; ambiguous insertion/reorder requires