Implement closed typed semantic review with immutable proposals
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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>,
|
||||
}
|
||||
@@ -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(¤t_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),
|
||||
(
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user