328 lines
12 KiB
Rust
328 lines
12 KiB
Rust
//! Closed constructor payload bindings. The core owns decisions and review scope;
|
|
//! this adapter supplies complete target records for its Candidate agreement check.
|
|
use super::{scalar, *};
|
|
|
|
pub(super) fn selectors() -> [FacetSelector; 5] {
|
|
[
|
|
FacetSelector::Data(DataFacet::Constructor),
|
|
FacetSelector::Port(PortFacet::Constructor),
|
|
FacetSelector::Occurrence(OccurrenceFacet::Constructor),
|
|
FacetSelector::Occurrence(OccurrenceFacet::MessageKind),
|
|
FacetSelector::Event(EventFacet::Constructor),
|
|
]
|
|
}
|
|
|
|
pub(super) fn roots(project: &Project, target: Id, facet: &FacetSelector) -> Option<Vec<String>> {
|
|
let root = scalar::binding(project, target, facet)?;
|
|
let mut roots = vec![root.clone()];
|
|
match facet {
|
|
FacetSelector::Data(DataFacet::Constructor) => {
|
|
roots.push(format!("{}/default", root.strip_suffix("/definition")?));
|
|
}
|
|
FacetSelector::Port(PortFacet::Constructor) => {
|
|
roots.push(format!("{}/legacy", root.strip_suffix("/contract")?));
|
|
}
|
|
_ => {}
|
|
}
|
|
Some(roots)
|
|
}
|
|
|
|
pub(super) fn available(project: &Project, trace: &Trace, facet: &FacetSelector) -> bool {
|
|
roots(project, trace.oa, facet).is_some() && roots(project, trace.sa, facet).is_some()
|
|
}
|
|
|
|
/// Used for explanation and explicit overlap controls, never for acknowledgment.
|
|
pub(super) fn 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,
|
|
..
|
|
}
|
|
),
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
pub(super) fn description(facet: &FacetSelector) -> &'static str {
|
|
match facet {
|
|
FacetSelector::Data(_) => {
|
|
"Definition, bounds or literal members, record supertype and default. Unit stays independent."
|
|
}
|
|
FacetSelector::Port(_) => {
|
|
"Contract, item members and retained generic association. Owner stays independent."
|
|
}
|
|
FacetSelector::Occurrence(OccurrenceFacet::Constructor) => {
|
|
"Occurrence body, including references, message annotations, bindings and branch or step order. Pre/postconditions stay independent."
|
|
}
|
|
FacetSelector::Occurrence(_) => {
|
|
"Message kind and Timer Expression. Exchange references and annotations stay independent."
|
|
}
|
|
FacetSelector::Event(_) => {
|
|
"Event body, including signal items or its complete time/change expression."
|
|
}
|
|
_ => "",
|
|
}
|
|
}
|
|
|
|
fn chosen<T>(keys: &KeyPair<T>, layer: Layer) -> Option<&T> {
|
|
match layer {
|
|
Layer::Oa => keys.oa.as_ref(),
|
|
Layer::Sa => keys.sa.as_ref(),
|
|
}
|
|
}
|
|
|
|
// The source payload is mapped by the core's typed visitor, never by replacing
|
|
// UUID-looking strings. For a historical same-kind Take, retain only current
|
|
// target-present/source-absent rows with no prior source-present observation. The final core planner
|
|
// checks this assembled record against its own independent expected result.
|
|
pub(super) fn take_value(
|
|
project: &Project,
|
|
trace: &Trace,
|
|
direction: TransitionDirection,
|
|
facet: &FacetSelector,
|
|
map: &BTreeMap<Id, Id>,
|
|
) -> Result<Value, String> {
|
|
let (source, target) = if direction == TransitionDirection::OaToSa {
|
|
(trace.oa, trace.sa)
|
|
} else {
|
|
(trace.sa, trace.oa)
|
|
};
|
|
let mut element = project
|
|
.elements
|
|
.get(&source)
|
|
.cloned()
|
|
.ok_or("Missing constructor source.")?;
|
|
if !message_kind::handles(facet) {
|
|
element.parent = None;
|
|
let mut missing = BTreeSet::new();
|
|
oa_core::references::visit_element_refs(&mut element, |id| {
|
|
if let Some(mapped) = map.get(id) {
|
|
*id = *mapped;
|
|
} else {
|
|
missing.insert(*id);
|
|
}
|
|
});
|
|
if !missing.is_empty() {
|
|
return Err(format!(
|
|
"Missing explicit reference correspondence: {missing:?}"
|
|
));
|
|
}
|
|
}
|
|
let supplied = serde_json::to_value(element).map_err(|e| e.to_string())?;
|
|
let mut value = Draft::from_project(project, target)
|
|
.ok_or("Missing target draft.")?
|
|
.value;
|
|
for (from, to) in roots(project, source, facet)
|
|
.ok_or("Source group is unavailable.")?
|
|
.iter()
|
|
.zip(roots(project, target, facet).ok_or("Target group is unavailable.")?)
|
|
{
|
|
*value
|
|
.pointer_mut(&to)
|
|
.ok_or("Target group payload is unavailable.")? = supplied
|
|
.pointer(from)
|
|
.cloned()
|
|
.ok_or("Source group payload is unavailable.")?;
|
|
}
|
|
let current = capture_facet(project, trace.oa, trace.sa, facet)?;
|
|
if scalar::observation(¤t, Layer::Oa) != scalar::observation(¤t, Layer::Sa) {
|
|
return Ok(value);
|
|
}
|
|
let references = if direction == TransitionDirection::OaToSa {
|
|
map.clone()
|
|
} else {
|
|
map.iter().map(|(sa, oa)| (*oa, *sa)).collect()
|
|
};
|
|
let old = trace.baseline.as_ref().and_then(|b| b.semantic.as_ref());
|
|
for fact in semantic_facets(project, trace, &references)? {
|
|
if !contains(facet, &fact.selector) {
|
|
continue;
|
|
}
|
|
let Some((path, key)) =
|
|
target_row(project, target, &fact.selector, direction.target_layer())
|
|
else {
|
|
continue;
|
|
};
|
|
if row_present(&fact.current, direction.source_layer())
|
|
|| !row_present(&fact.current, direction.target_layer())
|
|
{
|
|
continue;
|
|
}
|
|
let was_paired = old.is_some_and(|b| {
|
|
b.facets.iter().any(|previous| {
|
|
row_present(previous, direction.source_layer())
|
|
&& target_row(
|
|
project,
|
|
target,
|
|
&previous.selector(),
|
|
direction.target_layer(),
|
|
)
|
|
.as_ref()
|
|
== Some(&(path.clone(), key.clone()))
|
|
})
|
|
});
|
|
if was_paired {
|
|
continue;
|
|
}
|
|
let row = scalar::value(&fact.current, direction.target_layer())
|
|
.ok_or("Target row is inapplicable.")?;
|
|
let items = value
|
|
.pointer_mut(&path)
|
|
.and_then(Value::as_array_mut)
|
|
.ok_or("Target collection is unavailable.")?;
|
|
match &fact.selector {
|
|
FacetSelector::Member { .. } | FacetSelector::Literal(_) => {
|
|
if row == Value::Bool(true) && !items.contains(&key) {
|
|
items.push(key);
|
|
} else if row == Value::Bool(false) {
|
|
items.retain(|v| v != &key);
|
|
}
|
|
}
|
|
FacetSelector::Annotation(_) => {
|
|
items.retain(|v| {
|
|
serde_json::from_value::<MessageValue>(v.clone())
|
|
.ok()
|
|
.map(|v| serde_json::to_value(AnnotationKey::from(&v)).unwrap())
|
|
.as_ref()
|
|
!= Some(&key)
|
|
});
|
|
if !row.is_null() {
|
|
items.push(row);
|
|
}
|
|
}
|
|
FacetSelector::Binding(_) => {
|
|
items.retain(|v| v.get("referenced") != Some(&key));
|
|
if !row.is_null() {
|
|
items.push(row);
|
|
}
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
Ok(value)
|
|
}
|
|
|
|
fn row_present(fact: &FacetReview, layer: Layer) -> bool {
|
|
match fact {
|
|
FacetReview::Member { .. } | FacetReview::Literal { .. } => {
|
|
scalar::value(fact, layer) == Some(Value::Bool(true))
|
|
}
|
|
FacetReview::Annotation { .. } | FacetReview::Binding { .. } => {
|
|
scalar::value(fact, layer).is_some_and(|value| !value.is_null())
|
|
}
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
fn row_key(facet: &FacetSelector, layer: Layer) -> Option<Value> {
|
|
match facet {
|
|
FacetSelector::Member { keys, .. } | FacetSelector::Binding(keys) => {
|
|
chosen(keys, layer).map(|v| serde_json::json!(v))
|
|
}
|
|
FacetSelector::Literal(keys) => chosen(keys, layer).map(|v| serde_json::json!(v)),
|
|
FacetSelector::Annotation(keys) => chosen(keys, layer).map(|v| serde_json::json!(v)),
|
|
_ => None,
|
|
}
|
|
}
|
|
fn target_row(
|
|
project: &Project,
|
|
target: Id,
|
|
facet: &FacetSelector,
|
|
layer: Layer,
|
|
) -> Option<(String, Value)> {
|
|
let key = row_key(facet, layer)?;
|
|
let (group, suffix) = match facet {
|
|
FacetSelector::Literal(_) => (
|
|
FacetSelector::Data(DataFacet::Constructor),
|
|
"/Enumeration/literals",
|
|
),
|
|
FacetSelector::Binding(_) => (
|
|
FacetSelector::Occurrence(OccurrenceFacet::Constructor),
|
|
"/ScenarioUse/bindings",
|
|
),
|
|
FacetSelector::Annotation(_) => (
|
|
FacetSelector::Occurrence(OccurrenceFacet::Constructor),
|
|
"/Exchange/values",
|
|
),
|
|
FacetSelector::Member { collection, .. } => match collection {
|
|
ReferenceCollection::FunctionInputItems => (
|
|
FacetSelector::Port(PortFacet::Constructor),
|
|
"/FunctionInput/incoming_items",
|
|
),
|
|
ReferenceCollection::FunctionOutputItems => (
|
|
FacetSelector::Port(PortFacet::Constructor),
|
|
"/FunctionOutput/outgoing_items",
|
|
),
|
|
ReferenceCollection::SignalItems => (
|
|
FacetSelector::Event(EventFacet::Constructor),
|
|
"/Signal/items",
|
|
),
|
|
ReferenceCollection::Steps => (
|
|
FacetSelector::Occurrence(OccurrenceFacet::Constructor),
|
|
"/Branch/occurrences",
|
|
),
|
|
ReferenceCollection::FragmentBranches => (
|
|
FacetSelector::Occurrence(OccurrenceFacet::Constructor),
|
|
"/Fragment/branches",
|
|
),
|
|
_ => return None,
|
|
},
|
|
_ => return None,
|
|
};
|
|
Some((
|
|
format!("{}{suffix}", scalar::binding(project, target, &group)?),
|
|
key,
|
|
))
|
|
}
|
|
|
|
/// A selected descendant may fill untouched group fields during first-equal
|
|
/// acknowledgment, but cannot silently replace an authored group field.
|
|
pub(super) fn authored_agrees(original: &Value, authored: &Value, final_value: &Value) -> bool {
|
|
if original == authored || authored == final_value {
|
|
return true;
|
|
}
|
|
match (original, authored, final_value) {
|
|
(Value::Object(a), Value::Object(b), Value::Object(c)) if a.keys().eq(b.keys()) => {
|
|
b.iter().all(|(key, v)| {
|
|
c.get(key)
|
|
.is_some_and(|final_value| authored_agrees(&a[key], v, final_value))
|
|
})
|
|
}
|
|
_ => false,
|
|
}
|
|
}
|