Plan whole saved-view model deletion from exact visible groups
This commit is contained in:
@@ -43,6 +43,8 @@ pub use port_labels::PortLabels;
|
||||
#[path = "diagram/focus.rs"]
|
||||
pub(crate) mod focus;
|
||||
pub(crate) mod group_gesture;
|
||||
#[cfg(test)]
|
||||
pub(crate) mod group_removal;
|
||||
mod multiselect;
|
||||
#[cfg(test)]
|
||||
mod multiselect_tests;
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
//! Pure, whole-selection Delete from model planning for saved architecture and
|
||||
//! State views. UI lifetime, drafts and confirmation remain the caller's concern.
|
||||
use crate::diagram_usage::Target;
|
||||
use oa_core::{
|
||||
Command, Id, Kind, Project, execute_batch, projection, references, validate_candidate,
|
||||
};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Plan {
|
||||
pub(crate) command: Command,
|
||||
/// Semantic identities resolved from every admitted exact target.
|
||||
pub(crate) selected: BTreeSet<Id>,
|
||||
/// Actual identity and surviving-record differences after the complete batch.
|
||||
pub(crate) removed: BTreeSet<Id>,
|
||||
pub(crate) changed: BTreeSet<Id>,
|
||||
}
|
||||
|
||||
pub(crate) fn prepare(project: &Project, diagram: Id, targets: &[Target]) -> Result<Plan, String> {
|
||||
let saved = project
|
||||
.diagrams
|
||||
.get(&diagram)
|
||||
.ok_or("The saved view no longer exists.")?;
|
||||
if !saved.projection.is_architecture() && saved.projection.state_machine().is_none() {
|
||||
return Err("Delete from model supports saved Architecture and State views here.".into());
|
||||
}
|
||||
if targets.is_empty() {
|
||||
return Err("Select at least one visible model object to delete.".into());
|
||||
}
|
||||
validate_candidate(project).map_err(|error| error.to_string())?;
|
||||
let view = projection::project_diagram(project, saved);
|
||||
let mut selected = BTreeSet::new();
|
||||
// Resolve every original typed identity BEFORE removing any cascade overlap.
|
||||
// A stale/hidden/wrong-kind target cannot become admissible because another
|
||||
// selection member would happen to delete its semantic identity.
|
||||
for (index, target) in targets.iter().enumerate() {
|
||||
if targets[..index].contains(target) {
|
||||
return Err("Every selected exact target must be unique.".into());
|
||||
}
|
||||
let id = match target {
|
||||
Target::Placement(id) => saved.nodes.get(id)
|
||||
.filter(|node| view.nodes.iter().any(|visible| visible.id == *id && visible.element == node.element))
|
||||
.map(|node| node.element)
|
||||
.ok_or("A selected placement is no longer visible in this saved view.")?,
|
||||
Target::Port { port, placement } => {
|
||||
let owner = saved.nodes.get(placement)
|
||||
.ok_or("A selected port's exact owner placement no longer exists in this view.")?;
|
||||
if !view.ports.iter().any(|visible| visible.element == *port && visible.owner == *placement)
|
||||
|| !project.elements.get(port).is_some_and(|element| element.kind() == Kind::Port && element.parent == Some(owner.element))
|
||||
{
|
||||
return Err("A selected port is no longer visible on its exact owner placement.".into());
|
||||
}
|
||||
*port
|
||||
}
|
||||
Target::Edge(id) => {
|
||||
if !view.edges.iter().any(|edge| edge.id == *id)
|
||||
|| !(project.relations.contains_key(id) || project.traces.contains_key(id))
|
||||
{
|
||||
return Err("Every selected edge must be a visible stored relation or trace; inferred edges are read-only.".into());
|
||||
}
|
||||
*id
|
||||
}
|
||||
Target::StateRow { placement, relation } => {
|
||||
if !crate::behavior::state_canvas::visible_row_target(project, diagram, &view, *placement, *relation) {
|
||||
return Err("A selected State row is no longer visible on its exact placement.".into());
|
||||
}
|
||||
*relation
|
||||
}
|
||||
_ => return Err("The complete selection must contain only visible placements, ports, stored edges or State rows; grouping and read-only targets cannot be deleted here.".into()),
|
||||
};
|
||||
selected.insert(id);
|
||||
}
|
||||
|
||||
// Prefer cascades covering the most remaining selected subjects. Selecting a
|
||||
// parent together with its child/port/incident relation must not issue Delete
|
||||
// for an identity already removed by that parent. Stable ID order breaks ties.
|
||||
// Core defers conditional exchange-allocation cleanup until batch completion;
|
||||
// admission and the final result are therefore never singleton-validated.
|
||||
let impacts: BTreeMap<_, _> = selected
|
||||
.iter()
|
||||
.map(|&id| (id, references::deletion_impact(project, id)))
|
||||
.collect();
|
||||
let mut pending = selected.clone();
|
||||
let mut commands = Vec::new();
|
||||
while !pending.is_empty() {
|
||||
let id = *pending
|
||||
.iter()
|
||||
.max_by_key(|&&id| {
|
||||
(
|
||||
impacts[&id].intersection(&pending).count(),
|
||||
std::cmp::Reverse(id),
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
commands.push(Command::Delete(id));
|
||||
pending.retain(|remaining| !impacts[&id].contains(remaining));
|
||||
}
|
||||
let command = Command::Batch(commands);
|
||||
let candidate = execute_batch(project, std::slice::from_ref(&command))
|
||||
.map_err(|error| error.to_string())?;
|
||||
let original: BTreeSet<_> = project.identities().into_iter().map(|(id, _)| id).collect();
|
||||
let retained: BTreeSet<_> = candidate
|
||||
.identities()
|
||||
.into_iter()
|
||||
.map(|(id, _)| id)
|
||||
.collect();
|
||||
if !retained.is_subset(&original) {
|
||||
return Err("Removal unexpectedly created model identities; no change was applied.".into());
|
||||
}
|
||||
let removed: BTreeSet<_> = original.difference(&retained).copied().collect();
|
||||
if !selected.is_subset(&removed) {
|
||||
return Err("The complete selected set could not be removed.".into());
|
||||
}
|
||||
let mut changed = BTreeSet::new();
|
||||
changed_records(&project.elements, &candidate.elements, &mut changed);
|
||||
changed_records(&project.relations, &candidate.relations, &mut changed);
|
||||
changed_records(&project.diagrams, &candidate.diagrams, &mut changed);
|
||||
changed_records(&project.traces, &candidate.traces, &mut changed);
|
||||
changed_records(
|
||||
&project.artifact_links,
|
||||
&candidate.artifact_links,
|
||||
&mut changed,
|
||||
);
|
||||
for (&id, diagram) in &project.diagrams {
|
||||
if let Some(after) = candidate.diagrams.get(&id) {
|
||||
changed_records(&diagram.nodes, &after.nodes, &mut changed);
|
||||
}
|
||||
}
|
||||
Ok(Plan {
|
||||
command,
|
||||
selected,
|
||||
removed,
|
||||
changed,
|
||||
})
|
||||
}
|
||||
|
||||
fn changed_records<T: PartialEq>(
|
||||
before: &BTreeMap<Id, T>,
|
||||
after: &BTreeMap<Id, T>,
|
||||
changed: &mut BTreeSet<Id>,
|
||||
) {
|
||||
changed.extend(
|
||||
before
|
||||
.iter()
|
||||
.filter_map(|(&id, record)| after.get(&id).filter(|next| *next != record).map(|_| id)),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod conditional_tests;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,302 @@
|
||||
//! Independent conditional-cascade checks for overlapping selected endpoint allocations.
|
||||
use super::prepare;
|
||||
use crate::diagram_usage::Target;
|
||||
use oa_core::*;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
struct Fixture {
|
||||
project: Project,
|
||||
diagram: Id,
|
||||
selected_allocations: [Id; 2],
|
||||
exchange_allocation: Id,
|
||||
functional_exchange: Id,
|
||||
}
|
||||
|
||||
fn relation(project: &mut Project, number: u128, alias: &str, body: RelationBody) -> Id {
|
||||
let id = Id::from_u128(number);
|
||||
assert!(
|
||||
project
|
||||
.relations
|
||||
.insert(
|
||||
id,
|
||||
Relation {
|
||||
id,
|
||||
alias: alias.into(),
|
||||
name: alias.into(),
|
||||
description: String::new(),
|
||||
body,
|
||||
}
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
id
|
||||
}
|
||||
|
||||
fn fixture(reverse_ids: bool, invalid_dual_mapping: bool) -> Fixture {
|
||||
let mut project = Project::empty();
|
||||
let performer = project
|
||||
.add_element(Kind::System, Layer::Sa, "Send performer", None)
|
||||
.unwrap();
|
||||
let receiver = if invalid_dual_mapping {
|
||||
performer
|
||||
} else {
|
||||
project
|
||||
.add_element(Kind::SystemActor, Layer::Sa, "Receive performer", None)
|
||||
.unwrap()
|
||||
};
|
||||
let source = project
|
||||
.add_element(Kind::Function, Layer::Sa, "Send", None)
|
||||
.unwrap();
|
||||
let target = project
|
||||
.add_element(Kind::Function, Layer::Sa, "Receive", None)
|
||||
.unwrap();
|
||||
let retained = project.relate(RelationKind::Allocation, "Performs send", performer, source);
|
||||
project.relate(
|
||||
RelationKind::Allocation,
|
||||
"Performs receive",
|
||||
receiver,
|
||||
target,
|
||||
);
|
||||
let functional_exchange = project.relate(
|
||||
RelationKind::FunctionalExchange,
|
||||
"Information",
|
||||
source,
|
||||
target,
|
||||
);
|
||||
let (output, input) = match project.relations[&functional_exchange].body {
|
||||
RelationBody::FunctionalExchange {
|
||||
source_port,
|
||||
target_port,
|
||||
..
|
||||
} => (source_port, target_port),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let mut component_ports = vec![];
|
||||
for (owner, name) in [
|
||||
(performer, "First component port"),
|
||||
(receiver, "Second component port"),
|
||||
] {
|
||||
let id = Id::new_v4();
|
||||
let port = make_port(
|
||||
&project,
|
||||
id,
|
||||
owner,
|
||||
name,
|
||||
PortContract::Component {
|
||||
kind: ComponentPortKind::Flow,
|
||||
orientation: ComponentPortOrientation::InOut,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
project.elements.insert(id, port);
|
||||
component_ports.push(id);
|
||||
}
|
||||
let [a, b] = [component_ports[0], component_ports[1]];
|
||||
let boundary = relation(
|
||||
&mut project,
|
||||
300,
|
||||
"boundary",
|
||||
RelationBody::BoundaryExchange {
|
||||
source: a,
|
||||
target: b,
|
||||
oriented: false,
|
||||
items: vec![],
|
||||
},
|
||||
);
|
||||
// Either endpoint allocation is required. Their overlapping conditional
|
||||
// cascade must remove the exchange allocation once, after one whole batch.
|
||||
let numbers = if reverse_ids { [200, 100] } else { [100, 200] };
|
||||
let first = relation(
|
||||
&mut project,
|
||||
numbers[0],
|
||||
"first_output",
|
||||
RelationBody::PortAllocation {
|
||||
source: a,
|
||||
target: output,
|
||||
},
|
||||
);
|
||||
let second = relation(
|
||||
&mut project,
|
||||
numbers[1],
|
||||
"second_input",
|
||||
RelationBody::PortAllocation {
|
||||
source: b,
|
||||
target: input,
|
||||
},
|
||||
);
|
||||
if invalid_dual_mapping {
|
||||
// Two simultaneous direct/reverse mappings would need the same actual
|
||||
// performer at both ends, violating the different-boundary-owners rule.
|
||||
relation(
|
||||
&mut project,
|
||||
400,
|
||||
"first_input",
|
||||
RelationBody::PortAllocation {
|
||||
source: a,
|
||||
target: input,
|
||||
},
|
||||
);
|
||||
relation(
|
||||
&mut project,
|
||||
500,
|
||||
"second_output",
|
||||
RelationBody::PortAllocation {
|
||||
source: b,
|
||||
target: output,
|
||||
},
|
||||
);
|
||||
}
|
||||
let exchange_allocation = relation(
|
||||
&mut project,
|
||||
150,
|
||||
"realizes_exchange",
|
||||
RelationBody::ExchangeAllocation {
|
||||
source: boundary,
|
||||
target: functional_exchange,
|
||||
},
|
||||
);
|
||||
let diagram = project.ensure_diagram(Layer::Sa);
|
||||
project.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Overview;
|
||||
for (element, x) in [
|
||||
(performer, 40.),
|
||||
(receiver, 1360.),
|
||||
(source, 480.),
|
||||
(target, 920.),
|
||||
] {
|
||||
project.place(diagram, element, Position { x, y: 200. });
|
||||
}
|
||||
project.diagrams.get_mut(&diagram).unwrap().routes = [first, retained]
|
||||
.into_iter()
|
||||
.map(|edge| EdgeRoute {
|
||||
edge,
|
||||
source_attachment: None,
|
||||
target_attachment: None,
|
||||
waypoints: vec![Position { x: 360., y: 350. }],
|
||||
label: None,
|
||||
})
|
||||
.collect();
|
||||
Fixture {
|
||||
project,
|
||||
diagram,
|
||||
selected_allocations: [first, second],
|
||||
exchange_allocation,
|
||||
functional_exchange,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overlapping_conditional_allocation_loss_is_reviewed_from_final_candidate() {
|
||||
for reverse_ids in [false, true] {
|
||||
let f = fixture(reverse_ids, false);
|
||||
validate_candidate(&f.project)
|
||||
.expect("Separate actual performers and required endpoint mappings are valid");
|
||||
for selected in f.selected_allocations {
|
||||
assert!(
|
||||
references::deletion_impact(&f.project, selected).contains(&f.exchange_allocation),
|
||||
"Both selected allocations overlap on the same conditional dependent"
|
||||
);
|
||||
let singleton = execute_batch(&f.project, &[Command::Delete(selected)]).unwrap();
|
||||
assert!(!singleton.relations.contains_key(&f.exchange_allocation));
|
||||
}
|
||||
let targets = f.selected_allocations.map(Target::Edge);
|
||||
let plan = prepare(&f.project, f.diagram, &targets).unwrap();
|
||||
let reversed = prepare(
|
||||
&f.project,
|
||||
f.diagram,
|
||||
&[targets[1].clone(), targets[0].clone()],
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
serde_json::to_value(&plan.command).unwrap(),
|
||||
serde_json::to_value(&reversed.command).unwrap(),
|
||||
"Click order must not change normalization"
|
||||
);
|
||||
let removed = BTreeSet::from([
|
||||
f.selected_allocations[0],
|
||||
f.selected_allocations[1],
|
||||
f.exchange_allocation,
|
||||
]);
|
||||
assert_eq!(plan.selected, BTreeSet::from(f.selected_allocations));
|
||||
assert_eq!(plan.removed, removed);
|
||||
assert_eq!(plan.changed, BTreeSet::from([f.diagram]));
|
||||
// Literal record oracle, independent of deletion_impact / planner output.
|
||||
let mut expected = f.project.clone();
|
||||
for id in removed {
|
||||
expected.relations.remove(&id);
|
||||
}
|
||||
expected
|
||||
.diagrams
|
||||
.get_mut(&f.diagram)
|
||||
.unwrap()
|
||||
.routes
|
||||
.remove(0);
|
||||
let actual = execute_batch(&f.project, &[plan.command]).unwrap();
|
||||
assert_eq!(
|
||||
actual, expected,
|
||||
"Only two port allocations, their conditional exchange allocation and the deleted edge's authored route change"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_exchange_cascade_and_conditional_support_loss_share_one_batch() {
|
||||
for reverse_ids in [false, true] {
|
||||
let f = fixture(reverse_ids, false);
|
||||
validate_candidate(&f.project)
|
||||
.expect("Separate actual performers and required endpoint mappings are valid");
|
||||
let targets = [
|
||||
Target::Edge(f.selected_allocations[0]),
|
||||
Target::Edge(f.functional_exchange),
|
||||
Target::Edge(f.selected_allocations[1]),
|
||||
];
|
||||
let plan = prepare(&f.project, f.diagram, &targets).unwrap();
|
||||
let removed = BTreeSet::from([
|
||||
f.selected_allocations[0],
|
||||
f.selected_allocations[1],
|
||||
f.functional_exchange,
|
||||
f.exchange_allocation,
|
||||
]);
|
||||
assert_eq!(
|
||||
plan.selected,
|
||||
BTreeSet::from([
|
||||
f.selected_allocations[0],
|
||||
f.selected_allocations[1],
|
||||
f.functional_exchange
|
||||
])
|
||||
);
|
||||
assert_eq!(plan.removed, removed);
|
||||
assert_eq!(plan.changed, BTreeSet::from([f.diagram]));
|
||||
let mut expected = f.project.clone();
|
||||
for id in removed {
|
||||
expected.relations.remove(&id);
|
||||
}
|
||||
expected
|
||||
.diagrams
|
||||
.get_mut(&f.diagram)
|
||||
.unwrap()
|
||||
.routes
|
||||
.remove(0);
|
||||
assert_eq!(
|
||||
execute_batch(&f.project, &[plan.command]).unwrap(),
|
||||
expected
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alternative_pairing_counterexample_is_not_a_valid_saved_model() {
|
||||
let f = fixture(false, true);
|
||||
assert!(validation::validate(&f.project).iter().any(
|
||||
|issue| issue.code == "boundary_owner" && issue.severity == validation::Severity::Error
|
||||
));
|
||||
let before = f.project.clone();
|
||||
assert!(
|
||||
prepare(
|
||||
&f.project,
|
||||
f.diagram,
|
||||
&f.selected_allocations.map(Target::Edge)
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(f.project, before);
|
||||
}
|
||||
@@ -0,0 +1,896 @@
|
||||
use super::*;
|
||||
use oa_core::*;
|
||||
|
||||
fn pos(x: f32, y: f32) -> Position {
|
||||
Position { x, y }
|
||||
}
|
||||
|
||||
fn diagram(project: &mut Project, layer: Layer, projection: ProjectionKind) -> Id {
|
||||
let id = Id::new_v4();
|
||||
project.diagrams.insert(
|
||||
id,
|
||||
Diagram {
|
||||
id,
|
||||
alias: format!("view_{}", id.simple()),
|
||||
name: "Saved view".into(),
|
||||
description: String::new(),
|
||||
layer,
|
||||
projection,
|
||||
view_options: Default::default(),
|
||||
nodes: BTreeMap::new(),
|
||||
routes: vec![],
|
||||
layout: None,
|
||||
},
|
||||
);
|
||||
id
|
||||
}
|
||||
|
||||
fn artifact(project: &mut Project, owner: Id) -> Id {
|
||||
let id = Id::new_v4();
|
||||
project.artifact_links.insert(
|
||||
id,
|
||||
ArtifactLink {
|
||||
id,
|
||||
alias: format!("evidence_{}", id.simple()),
|
||||
name: "Evidence".into(),
|
||||
description: String::new(),
|
||||
owner,
|
||||
relation: ArtifactRelation::References,
|
||||
uri: "docs/evidence.md".into(),
|
||||
revision: Some("r1".into()),
|
||||
fragment: None,
|
||||
},
|
||||
);
|
||||
id
|
||||
}
|
||||
|
||||
fn route(edge: Id) -> EdgeRoute {
|
||||
EdgeRoute {
|
||||
edge,
|
||||
source_attachment: None,
|
||||
target_attachment: None,
|
||||
waypoints: vec![pos(450., 150.)],
|
||||
label: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Every success compares the whole resulting Project with a separately authored
|
||||
/// expected value; the planner's own impact calculation is never the oracle.
|
||||
fn check(
|
||||
project: &Project,
|
||||
view: Id,
|
||||
targets: &[Target],
|
||||
expected: &Project,
|
||||
selected: &[Id],
|
||||
removed: &[Id],
|
||||
changed: &[Id],
|
||||
) -> Plan {
|
||||
validate_candidate(project).unwrap();
|
||||
validate_candidate(expected).unwrap();
|
||||
let original = project.clone();
|
||||
let plan = prepare(project, view, targets).unwrap();
|
||||
assert_eq!(project, &original, "planning must be pure");
|
||||
assert_eq!(plan.selected, selected.iter().copied().collect());
|
||||
assert_eq!(plan.removed, removed.iter().copied().collect());
|
||||
assert_eq!(plan.changed, changed.iter().copied().collect());
|
||||
assert_eq!(
|
||||
execute_batch(project, std::slice::from_ref(&plan.command)).unwrap(),
|
||||
*expected
|
||||
);
|
||||
assert!(
|
||||
matches!(&plan.command, Command::Batch(commands) if commands.iter().all(|command| matches!(command, Command::Delete(_))))
|
||||
);
|
||||
plan
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn architecture_parent_child_and_incident_edge_collapse_to_one_delete_across_views() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut project = Project::empty();
|
||||
let kind = if layer == Layer::Oa {
|
||||
Kind::Activity
|
||||
} else {
|
||||
Kind::Function
|
||||
};
|
||||
let parent = project.add_element(kind, layer, "Parent", None).unwrap();
|
||||
let child = project
|
||||
.add_element(kind, layer, "Child", Some(parent))
|
||||
.unwrap();
|
||||
let survivor = project.add_element(kind, layer, "Survivor", None).unwrap();
|
||||
let edge_kind = if layer == Layer::Oa {
|
||||
RelationKind::Interaction
|
||||
} else {
|
||||
RelationKind::FunctionalExchange
|
||||
};
|
||||
let edge = project.relate(edge_kind, "Incident", child, survivor);
|
||||
let ports = match project.relations[&edge].body {
|
||||
RelationBody::FunctionalExchange {
|
||||
source_port,
|
||||
target_port,
|
||||
..
|
||||
} => Some((source_port, target_port)),
|
||||
_ => None,
|
||||
};
|
||||
let evidence = artifact(&mut project, child);
|
||||
let view = diagram(&mut project, layer, ProjectionKind::Overview);
|
||||
let parent_node = project.place(view, parent, pos(0., 0.));
|
||||
let child_node = project.place(view, child, pos(30., 80.));
|
||||
project.place(view, survivor, pos(800., 100.));
|
||||
project
|
||||
.diagrams
|
||||
.get_mut(&view)
|
||||
.unwrap()
|
||||
.routes
|
||||
.push(route(edge));
|
||||
let other_view = diagram(&mut project, layer, ProjectionKind::Overview);
|
||||
let other_child = project.place(other_view, child, pos(100., 100.));
|
||||
project.place(other_view, survivor, pos(800., 100.));
|
||||
project
|
||||
.diagrams
|
||||
.get_mut(&other_view)
|
||||
.unwrap()
|
||||
.view_options
|
||||
.hidden_edges
|
||||
.push(edge);
|
||||
let mut expected = project.clone();
|
||||
expected.elements.remove(&parent);
|
||||
expected.elements.remove(&child);
|
||||
expected.relations.remove(&edge);
|
||||
expected.artifact_links.remove(&evidence);
|
||||
let mut removed = vec![
|
||||
parent,
|
||||
child,
|
||||
edge,
|
||||
evidence,
|
||||
parent_node,
|
||||
child_node,
|
||||
other_child,
|
||||
];
|
||||
if let Some((source_port, _)) = ports {
|
||||
expected.elements.remove(&source_port);
|
||||
removed.push(source_port);
|
||||
}
|
||||
let current = expected.diagrams.get_mut(&view).unwrap();
|
||||
current.nodes.remove(&parent_node);
|
||||
current.nodes.remove(&child_node);
|
||||
current.routes.clear();
|
||||
let other = expected.diagrams.get_mut(&other_view).unwrap();
|
||||
other.nodes.remove(&other_child);
|
||||
other.view_options.hidden_edges.clear();
|
||||
for targets in [
|
||||
vec![
|
||||
Target::Placement(parent_node),
|
||||
Target::Placement(child_node),
|
||||
Target::Edge(edge),
|
||||
],
|
||||
vec![
|
||||
Target::Edge(edge),
|
||||
Target::Placement(child_node),
|
||||
Target::Placement(parent_node),
|
||||
],
|
||||
] {
|
||||
let plan = check(
|
||||
&project,
|
||||
view,
|
||||
&targets,
|
||||
&expected,
|
||||
&[parent, child, edge],
|
||||
&removed,
|
||||
&[view, other_view],
|
||||
);
|
||||
assert!(
|
||||
matches!(plan.command, Command::Batch(commands) if matches!(commands.as_slice(), [Command::Delete(id)] if *id == parent))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn port_owner_port_and_exchange_admission_preserve_other_endpoint_and_nested_anchor_diff() {
|
||||
let mut project = Project::empty();
|
||||
let a = project
|
||||
.add_element(Kind::Function, Layer::Sa, "Send", None)
|
||||
.unwrap();
|
||||
let b = project
|
||||
.add_element(Kind::Function, Layer::Sa, "Receive", None)
|
||||
.unwrap();
|
||||
let edge = project.relate(RelationKind::FunctionalExchange, "Payload", a, b);
|
||||
let RelationBody::FunctionalExchange {
|
||||
source_port,
|
||||
target_port,
|
||||
..
|
||||
} = project.relations[&edge].body
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
let view = diagram(&mut project, Layer::Sa, ProjectionKind::FunctionalDataflow);
|
||||
let na = project.place(view, a, pos(0., 0.));
|
||||
let nb = project.place(view, b, pos(700., 0.));
|
||||
project
|
||||
.diagrams
|
||||
.get_mut(&view)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&na)
|
||||
.unwrap()
|
||||
.port_anchors
|
||||
.push(PortAnchor {
|
||||
port: source_port,
|
||||
side: PortAnchorSide::Top,
|
||||
offset: 0.3,
|
||||
});
|
||||
project
|
||||
.diagrams
|
||||
.get_mut(&view)
|
||||
.unwrap()
|
||||
.routes
|
||||
.push(route(edge));
|
||||
|
||||
let mut port_expected = project.clone();
|
||||
port_expected.elements.remove(&source_port);
|
||||
port_expected.relations.remove(&edge);
|
||||
let d = port_expected.diagrams.get_mut(&view).unwrap();
|
||||
d.routes.clear();
|
||||
d.nodes.get_mut(&na).unwrap().port_anchors.clear();
|
||||
check(
|
||||
&project,
|
||||
view,
|
||||
&[
|
||||
Target::Port {
|
||||
port: source_port,
|
||||
placement: na,
|
||||
},
|
||||
Target::Edge(edge),
|
||||
],
|
||||
&port_expected,
|
||||
&[source_port, edge],
|
||||
&[source_port, edge],
|
||||
&[view, na],
|
||||
);
|
||||
|
||||
let mut owner_expected = port_expected;
|
||||
owner_expected.elements.remove(&a);
|
||||
owner_expected
|
||||
.diagrams
|
||||
.get_mut(&view)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.remove(&na);
|
||||
let plan = check(
|
||||
&project,
|
||||
view,
|
||||
&[
|
||||
Target::Port {
|
||||
port: source_port,
|
||||
placement: na,
|
||||
},
|
||||
Target::Placement(na),
|
||||
Target::Edge(edge),
|
||||
],
|
||||
&owner_expected,
|
||||
&[a, source_port, edge],
|
||||
&[a, source_port, edge, na],
|
||||
&[view],
|
||||
);
|
||||
assert!(
|
||||
owner_expected.elements.contains_key(&b)
|
||||
&& owner_expected.elements.contains_key(&target_port)
|
||||
);
|
||||
assert!(owner_expected.diagrams[&view].nodes.contains_key(&nb));
|
||||
assert!(matches!(plan.command, Command::Batch(commands) if commands.len() == 1));
|
||||
// A wrong owner placement is invalid even though the selected owner covers it.
|
||||
assert!(
|
||||
prepare(
|
||||
&project,
|
||||
view,
|
||||
&[
|
||||
Target::Placement(na),
|
||||
Target::Port {
|
||||
port: source_port,
|
||||
placement: nb
|
||||
}
|
||||
]
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_rows_delete_relations_not_referenced_work_and_overlap_with_state_cascade() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut project = Project::empty();
|
||||
let machine = project
|
||||
.add_element(Kind::StateMachine, layer, "Modes", None)
|
||||
.unwrap();
|
||||
let region = project
|
||||
.add_element(Kind::Region, layer, "Main", Some(machine))
|
||||
.unwrap();
|
||||
let state = project
|
||||
.add_element(Kind::State, layer, "Ready", Some(region))
|
||||
.unwrap();
|
||||
let other = project
|
||||
.add_element(Kind::State, layer, "Other", Some(region))
|
||||
.unwrap();
|
||||
let work = project
|
||||
.add_element(
|
||||
if layer == Layer::Oa {
|
||||
Kind::Activity
|
||||
} else {
|
||||
Kind::Function
|
||||
},
|
||||
layer,
|
||||
"Work",
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let action = project.relate(RelationKind::StateAction, "Entry", state, work);
|
||||
let internal = project.relate(RelationKind::StateTransition, "Internal", state, state);
|
||||
if let RelationBody::StateTransition {
|
||||
transition_kind,
|
||||
effects,
|
||||
..
|
||||
} = &mut project.relations.get_mut(&internal).unwrap().body
|
||||
{
|
||||
*transition_kind = TransitionKind::Internal;
|
||||
*effects = vec![work];
|
||||
}
|
||||
let external = project.relate(RelationKind::StateTransition, "External", state, other);
|
||||
let view = diagram(
|
||||
&mut project,
|
||||
layer,
|
||||
ProjectionKind::StateMachine { machine },
|
||||
);
|
||||
let state_node = project.place(view, state, pos(100., 100.));
|
||||
let other_node = project.place(view, other, pos(700., 100.));
|
||||
let rows = [
|
||||
Target::StateRow {
|
||||
placement: state_node,
|
||||
relation: action,
|
||||
},
|
||||
Target::StateRow {
|
||||
placement: state_node,
|
||||
relation: internal,
|
||||
},
|
||||
];
|
||||
let mut expected = project.clone();
|
||||
expected.relations.remove(&action);
|
||||
expected.relations.remove(&internal);
|
||||
check(
|
||||
&project,
|
||||
view,
|
||||
&rows,
|
||||
&expected,
|
||||
&[action, internal],
|
||||
&[action, internal],
|
||||
&[],
|
||||
);
|
||||
expected.relations.remove(&external);
|
||||
expected.elements.remove(&state);
|
||||
expected
|
||||
.diagrams
|
||||
.get_mut(&view)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.remove(&state_node);
|
||||
let mut targets = rows.to_vec();
|
||||
targets.extend([Target::Placement(state_node), Target::Edge(external)]);
|
||||
check(
|
||||
&project,
|
||||
view,
|
||||
&targets,
|
||||
&expected,
|
||||
&[action, internal, state, external],
|
||||
&[action, internal, state, external, state_node],
|
||||
&[view],
|
||||
);
|
||||
assert_eq!(expected.elements[&work], project.elements[&work]);
|
||||
assert!(
|
||||
prepare(
|
||||
&project,
|
||||
view,
|
||||
&[
|
||||
Target::Placement(state_node),
|
||||
Target::StateRow {
|
||||
placement: other_node,
|
||||
relation: action
|
||||
}
|
||||
]
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
assert!(prepare(&project, view, &[Target::Edge(internal)]).is_err());
|
||||
assert!(
|
||||
prepare(
|
||||
&project,
|
||||
view,
|
||||
&[Target::StateRow {
|
||||
placement: state_node,
|
||||
relation: external
|
||||
}]
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
let region_node = project.place(view, region, pos(0., 0.));
|
||||
project
|
||||
.diagrams
|
||||
.get_mut(&view)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(®ion_node)
|
||||
.unwrap()
|
||||
.collapsed = true;
|
||||
assert!(
|
||||
prepare(
|
||||
&project,
|
||||
view,
|
||||
&[
|
||||
Target::Placement(region_node),
|
||||
Target::StateRow {
|
||||
placement: state_node,
|
||||
relation: action
|
||||
}
|
||||
]
|
||||
)
|
||||
.is_err(),
|
||||
"hidden State row cannot borrow its selected ancestor's permission"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deleting_machine_cascades_saved_diagram_and_its_embedded_placements() {
|
||||
let mut project = Project::empty();
|
||||
let machine = project
|
||||
.add_element(Kind::StateMachine, Layer::Sa, "Modes", None)
|
||||
.unwrap();
|
||||
let region = project
|
||||
.add_element(Kind::Region, Layer::Sa, "Main", Some(machine))
|
||||
.unwrap();
|
||||
let state = project
|
||||
.add_element(Kind::State, Layer::Sa, "Ready", Some(region))
|
||||
.unwrap();
|
||||
let view = diagram(
|
||||
&mut project,
|
||||
Layer::Sa,
|
||||
ProjectionKind::StateMachine { machine },
|
||||
);
|
||||
let machine_node = project.place(view, machine, pos(0., 0.));
|
||||
let region_node = project.place(view, region, pos(30., 90.));
|
||||
let state_node = project.place(view, state, pos(60., 180.));
|
||||
let evidence = artifact(&mut project, view);
|
||||
let architecture = diagram(&mut project, Layer::Sa, ProjectionKind::Overview);
|
||||
let architecture_node = project.place(architecture, machine, pos(0., 0.));
|
||||
let mut expected = project.clone();
|
||||
for id in [machine, region, state] {
|
||||
expected.elements.remove(&id);
|
||||
}
|
||||
expected.diagrams.remove(&view);
|
||||
expected.artifact_links.remove(&evidence);
|
||||
expected
|
||||
.diagrams
|
||||
.get_mut(&architecture)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.remove(&architecture_node);
|
||||
check(
|
||||
&project,
|
||||
view,
|
||||
&[
|
||||
Target::Placement(machine_node),
|
||||
Target::Placement(state_node),
|
||||
],
|
||||
&expected,
|
||||
&[machine, state],
|
||||
&[
|
||||
machine,
|
||||
region,
|
||||
state,
|
||||
view,
|
||||
machine_node,
|
||||
region_node,
|
||||
state_node,
|
||||
evidence,
|
||||
architecture_node,
|
||||
],
|
||||
&[architecture],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trace_edge_and_endpoint_deletion_preserve_counterpart_unless_explicitly_selected() {
|
||||
let mut project = Project::empty();
|
||||
let oa = project
|
||||
.add_element(Kind::Activity, Layer::Oa, "Operational", None)
|
||||
.unwrap();
|
||||
let sa = project
|
||||
.add_element(Kind::Function, Layer::Sa, "System", None)
|
||||
.unwrap();
|
||||
let trace = Id::new_v4();
|
||||
project.traces.insert(
|
||||
trace,
|
||||
Trace {
|
||||
id: trace,
|
||||
alias: "refinement".into(),
|
||||
name: "Refinement".into(),
|
||||
description: String::new(),
|
||||
oa,
|
||||
sa,
|
||||
kind: TraceKind::Refinement,
|
||||
baseline: None,
|
||||
},
|
||||
);
|
||||
let evidence = artifact(&mut project, trace);
|
||||
let view = diagram(&mut project, Layer::Oa, ProjectionKind::Traceability);
|
||||
let noa = project.place(view, oa, pos(0., 0.));
|
||||
let nsa = project.place(view, sa, pos(700., 0.));
|
||||
project
|
||||
.diagrams
|
||||
.get_mut(&view)
|
||||
.unwrap()
|
||||
.routes
|
||||
.push(route(trace));
|
||||
let mut expected = project.clone();
|
||||
expected.traces.remove(&trace);
|
||||
expected.artifact_links.remove(&evidence);
|
||||
expected.diagrams.get_mut(&view).unwrap().routes.clear();
|
||||
check(
|
||||
&project,
|
||||
view,
|
||||
&[Target::Edge(trace)],
|
||||
&expected,
|
||||
&[trace],
|
||||
&[trace, evidence],
|
||||
&[view],
|
||||
);
|
||||
expected.elements.remove(&oa);
|
||||
expected.diagrams.get_mut(&view).unwrap().nodes.remove(&noa);
|
||||
check(
|
||||
&project,
|
||||
view,
|
||||
&[Target::Placement(noa), Target::Edge(trace)],
|
||||
&expected,
|
||||
&[oa, trace],
|
||||
&[oa, noa, trace, evidence],
|
||||
&[view],
|
||||
);
|
||||
assert_eq!(expected.elements[&sa], project.elements[&sa]);
|
||||
expected.elements.remove(&sa);
|
||||
expected.diagrams.get_mut(&view).unwrap().nodes.remove(&nsa);
|
||||
check(
|
||||
&project,
|
||||
view,
|
||||
&[
|
||||
Target::Placement(noa),
|
||||
Target::Placement(nsa),
|
||||
Target::Edge(trace),
|
||||
],
|
||||
&expected,
|
||||
&[oa, sa, trace],
|
||||
&[oa, noa, sa, nsa, trace, evidence],
|
||||
&[view],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn surviving_semantic_records_report_reference_pruning_as_changed() {
|
||||
let mut project = Project::empty();
|
||||
let work = project
|
||||
.add_element(Kind::Function, Layer::Sa, "Work", None)
|
||||
.unwrap();
|
||||
let machine = project
|
||||
.add_element(Kind::StateMachine, Layer::Sa, "Modes", None)
|
||||
.unwrap();
|
||||
let region = project
|
||||
.add_element(Kind::Region, Layer::Sa, "Main", Some(machine))
|
||||
.unwrap();
|
||||
let a = project
|
||||
.add_element(Kind::State, Layer::Sa, "A", Some(region))
|
||||
.unwrap();
|
||||
let b = project
|
||||
.add_element(Kind::State, Layer::Sa, "B", Some(region))
|
||||
.unwrap();
|
||||
let transition = project.relate(RelationKind::StateTransition, "Advance", a, b);
|
||||
if let RelationBody::StateTransition { effects, .. } =
|
||||
&mut project.relations.get_mut(&transition).unwrap().body
|
||||
{
|
||||
*effects = vec![work];
|
||||
}
|
||||
let chain = project
|
||||
.add_element(Kind::FunctionalChain, Layer::Sa, "Chain", None)
|
||||
.unwrap();
|
||||
let occurrence = project
|
||||
.add_element(Kind::Occurrence, Layer::Sa, "Action", Some(chain))
|
||||
.unwrap();
|
||||
if let Some(SharedBody::Occurrence { body, .. }) =
|
||||
project.elements.get_mut(&occurrence).unwrap().shared_mut()
|
||||
{
|
||||
*body = OccurrenceBody::Action {
|
||||
action: Some(work),
|
||||
lifeline: None,
|
||||
};
|
||||
}
|
||||
project
|
||||
.elements
|
||||
.get_mut(&chain)
|
||||
.unwrap()
|
||||
.steps_mut()
|
||||
.unwrap()
|
||||
.push(occurrence);
|
||||
let view = diagram(&mut project, Layer::Sa, ProjectionKind::Overview);
|
||||
let placement = project.place(view, work, pos(100., 100.));
|
||||
let mut expected = project.clone();
|
||||
expected.elements.remove(&work);
|
||||
expected
|
||||
.diagrams
|
||||
.get_mut(&view)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.remove(&placement);
|
||||
if let RelationBody::StateTransition { effects, .. } =
|
||||
&mut expected.relations.get_mut(&transition).unwrap().body
|
||||
{
|
||||
effects.clear();
|
||||
}
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Action { action, .. },
|
||||
..
|
||||
}) = expected.elements.get_mut(&occurrence).unwrap().shared_mut()
|
||||
{
|
||||
*action = None;
|
||||
}
|
||||
check(
|
||||
&project,
|
||||
view,
|
||||
&[Target::Placement(placement)],
|
||||
&expected,
|
||||
&[work],
|
||||
&[work, placement],
|
||||
&[view, occurrence, transition],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_union_is_not_rejected_by_an_invalid_intermediate_singleton() {
|
||||
let mut project = Project::empty();
|
||||
let sender = project
|
||||
.add_element(Kind::SystemActor, Layer::Sa, "Sender", None)
|
||||
.unwrap();
|
||||
let receiver = project
|
||||
.add_element(Kind::System, Layer::Sa, "Receiver", None)
|
||||
.unwrap();
|
||||
let mut ports = vec![];
|
||||
for owner in [sender, receiver] {
|
||||
let id = Id::new_v4();
|
||||
let port = make_port(
|
||||
&project,
|
||||
id,
|
||||
owner,
|
||||
"Service port",
|
||||
PortContract::Component {
|
||||
kind: ComponentPortKind::Standard,
|
||||
orientation: ComponentPortOrientation::Unset,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
project.elements.insert(id, port);
|
||||
ports.push(id);
|
||||
}
|
||||
// Ensure the planner's deterministic tie break orders this invalid-alone
|
||||
// deletion first; a singleton transaction implementation would fail here.
|
||||
let protocol = Id::from_u128(1);
|
||||
let mut protocol_element = project
|
||||
.make_element(Kind::Interface, Layer::Sa, "Payload protocol", None)
|
||||
.unwrap();
|
||||
protocol_element.id = protocol;
|
||||
project.elements.insert(protocol, protocol_element);
|
||||
let empty_protocol = project
|
||||
.add_element(Kind::Interface, Layer::Sa, "Other protocol", None)
|
||||
.unwrap();
|
||||
let item = project
|
||||
.add_element(Kind::ExchangeItem, Layer::Sa, "Payload", None)
|
||||
.unwrap();
|
||||
let allocation = project.relate(
|
||||
RelationKind::InterfaceItemAllocation,
|
||||
"Declares payload",
|
||||
protocol,
|
||||
item,
|
||||
);
|
||||
let role = project.relate(
|
||||
RelationKind::InterfaceRequirement,
|
||||
"Requires payload",
|
||||
ports[0],
|
||||
protocol,
|
||||
);
|
||||
project.relate(
|
||||
RelationKind::InterfaceRequirement,
|
||||
"Requires other",
|
||||
ports[0],
|
||||
empty_protocol,
|
||||
);
|
||||
let boundary = project.relate(
|
||||
RelationKind::BoundaryExchange,
|
||||
"Channel",
|
||||
ports[0],
|
||||
ports[1],
|
||||
);
|
||||
if let RelationBody::BoundaryExchange { items, .. } =
|
||||
&mut project.relations.get_mut(&boundary).unwrap().body
|
||||
{
|
||||
*items = vec![item];
|
||||
}
|
||||
let view = diagram(&mut project, Layer::Sa, ProjectionKind::Overview);
|
||||
project.place(view, sender, pos(0., 0.));
|
||||
project.place(view, receiver, pos(800., 0.));
|
||||
let protocol_node = project.place(view, protocol, pos(400., 300.));
|
||||
project
|
||||
.diagrams
|
||||
.get_mut(&view)
|
||||
.unwrap()
|
||||
.routes
|
||||
.push(route(boundary));
|
||||
validate_candidate(&project).unwrap();
|
||||
// The remaining interface declaration becomes an empty constraint on the
|
||||
// live channel. This is an invalid intermediate state, but deleting the
|
||||
// selected channel in the SAME batch makes the complete intent valid.
|
||||
assert!(execute_batch(&project, &[Command::Delete(protocol)]).is_err());
|
||||
assert!(prepare(&project, view, &[Target::Placement(protocol_node)]).is_err());
|
||||
let mut expected = project.clone();
|
||||
expected.elements.remove(&protocol);
|
||||
for id in [allocation, role, boundary] {
|
||||
expected.relations.remove(&id);
|
||||
}
|
||||
let d = expected.diagrams.get_mut(&view).unwrap();
|
||||
d.nodes.remove(&protocol_node);
|
||||
d.routes.clear();
|
||||
for targets in [
|
||||
vec![Target::Placement(protocol_node), Target::Edge(boundary)],
|
||||
vec![Target::Edge(boundary), Target::Placement(protocol_node)],
|
||||
] {
|
||||
check(
|
||||
&project,
|
||||
view,
|
||||
&targets,
|
||||
&expected,
|
||||
&[protocol, boundary],
|
||||
&[protocol, protocol_node, allocation, role, boundary],
|
||||
&[view],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_original_target_must_be_visible_local_supported_and_unique_even_when_covered() {
|
||||
let mut project = Project::empty();
|
||||
let a = project
|
||||
.add_element(Kind::Activity, Layer::Oa, "A", None)
|
||||
.unwrap();
|
||||
let child = project
|
||||
.add_element(Kind::Activity, Layer::Oa, "Child", Some(a))
|
||||
.unwrap();
|
||||
let b = project
|
||||
.add_element(Kind::Activity, Layer::Oa, "B", None)
|
||||
.unwrap();
|
||||
let edge = project.relate(RelationKind::Interaction, "Flow", child, b);
|
||||
let view = diagram(&mut project, Layer::Oa, ProjectionKind::Overview);
|
||||
let na = project.place(view, a, pos(0., 0.));
|
||||
let nc = project.place(view, child, pos(30., 90.));
|
||||
let nb = project.place(view, b, pos(700., 0.));
|
||||
project
|
||||
.diagrams
|
||||
.get_mut(&view)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&na)
|
||||
.unwrap()
|
||||
.collapsed = true;
|
||||
project
|
||||
.diagrams
|
||||
.get_mut(&view)
|
||||
.unwrap()
|
||||
.view_options
|
||||
.hidden_edges
|
||||
.push(edge);
|
||||
let other = diagram(&mut project, Layer::Oa, ProjectionKind::Overview);
|
||||
let foreign = project.place(other, child, pos(0., 0.));
|
||||
validate_candidate(&project).unwrap();
|
||||
let original = project.clone();
|
||||
for inadmissible in [
|
||||
Target::Whole,
|
||||
Target::Placement(nc),
|
||||
Target::Placement(foreign),
|
||||
Target::Placement(Id::new_v4()),
|
||||
Target::Edge(edge),
|
||||
Target::Edge(Id::new_v4()),
|
||||
Target::Edge(a),
|
||||
Target::Placement(a),
|
||||
Target::Port {
|
||||
port: child,
|
||||
placement: na,
|
||||
},
|
||||
Target::StateRow {
|
||||
placement: na,
|
||||
relation: edge,
|
||||
},
|
||||
Target::AllocationGrouping {
|
||||
relation: edge,
|
||||
work_placement: nc,
|
||||
performer_placement: nb,
|
||||
},
|
||||
Target::ProcessLink(edge),
|
||||
Target::ScenarioObject(child),
|
||||
Target::ProcessOccurrence(OccurrenceEndpoint {
|
||||
references: vec![],
|
||||
occurrence: child,
|
||||
}),
|
||||
Target::ProcessPreviewLink {
|
||||
relation: edge,
|
||||
source: OccurrenceEndpoint {
|
||||
references: vec![],
|
||||
occurrence: a,
|
||||
},
|
||||
target: OccurrenceEndpoint {
|
||||
references: vec![],
|
||||
occurrence: b,
|
||||
},
|
||||
},
|
||||
] {
|
||||
assert!(
|
||||
prepare(
|
||||
&project,
|
||||
view,
|
||||
&[Target::Placement(na), inadmissible.clone()]
|
||||
)
|
||||
.is_err(),
|
||||
"accepted {inadmissible:?}"
|
||||
);
|
||||
assert_eq!(project, original);
|
||||
}
|
||||
assert!(prepare(&project, view, &[]).is_err());
|
||||
assert!(prepare(&project, Id::new_v4(), &[Target::Placement(na)]).is_err());
|
||||
assert!(
|
||||
prepare(
|
||||
&project,
|
||||
view,
|
||||
&[Target::Placement(na), Target::Placement(na)]
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scenario_surface_and_invalid_duplicate_placements_are_rejected_before_planning() {
|
||||
let mut project = Project::empty();
|
||||
let activity = project
|
||||
.add_element(Kind::Activity, Layer::Oa, "Work", None)
|
||||
.unwrap();
|
||||
let view = diagram(&mut project, Layer::Oa, ProjectionKind::Overview);
|
||||
let node = project.place(view, activity, pos(0., 0.));
|
||||
let scenario = project
|
||||
.add_element(Kind::Scenario, Layer::Oa, "Scenario", None)
|
||||
.unwrap();
|
||||
let scenario_view = diagram(
|
||||
&mut project,
|
||||
Layer::Oa,
|
||||
ProjectionKind::Scenario { scenario },
|
||||
);
|
||||
assert!(prepare(&project, scenario_view, &[Target::Placement(node)]).is_err());
|
||||
let duplicate = Id::new_v4();
|
||||
let mut placement = project.diagrams[&view].nodes[&node].clone();
|
||||
placement.id = duplicate;
|
||||
placement.alias = "duplicate_placement".into();
|
||||
project
|
||||
.diagrams
|
||||
.get_mut(&view)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.insert(duplicate, placement);
|
||||
assert!(
|
||||
prepare(
|
||||
&project,
|
||||
view,
|
||||
&[Target::Placement(node), Target::Placement(duplicate)]
|
||||
)
|
||||
.unwrap_err()
|
||||
.contains("duplicate_placement")
|
||||
);
|
||||
}
|
||||
@@ -2279,6 +2279,28 @@ transition row, uses confirmed relation deletion for either key. States and
|
||||
referenced Activity/Function definitions remain. Hidden or collapsed rows are
|
||||
not keyboard deletion targets.
|
||||
|
||||
For a group in a saved architecture or state view, plain Delete retains its
|
||||
placement-only Hide meaning; a mixture containing ports, edges or state rows
|
||||
MUST refuse Hide rather than silently delete model objects. Explicit group
|
||||
model deletion, including focused Shift+Delete, resolves each exact visible
|
||||
placement to its semantic element, each port to that port, each stored edge to
|
||||
its relation or trace, and each state row to its maintained relation. Navigation
|
||||
groupings and inferred edges are not semantic deletion subjects. Every original
|
||||
typed member MUST be admitted before redundant semantic subjects or cascade
|
||||
commands are reduced. A selected ancestor does not make a hidden or invalid
|
||||
member admissible. Existing project validity constraints, including placement
|
||||
uniqueness within a diagram, remain in force.
|
||||
|
||||
Saved-view aggregate review distinguishes selected visual targets, unique
|
||||
semantic subjects, actual deleted records and changed surviving records. Model
|
||||
deletion affects all placements of deleted elements in all views. A trace does
|
||||
not cause deletion of its counterpart endpoints; an explicitly selected
|
||||
counterpart remains an independent deletion subject. Removing a state action
|
||||
row deletes its maintained relation, not the referenced Activity or Function.
|
||||
The combined candidate and source update MUST form one atomic transaction;
|
||||
neither partial primary-only deletion nor intermediate singleton history is
|
||||
permitted.
|
||||
|
||||
Process and scenario projections have no independent occurrence placements.
|
||||
Delete and Shift+Delete request confirmed removal of the selected occurrence,
|
||||
participant or maintained link. A link's endpoints and outbound reusable
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
# Saved Architecture and State aggregate model removal
|
||||
|
||||
Development policy under [issue 29](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/29),
|
||||
derived from the [multiple-selection contract](multiselect-contract-20260923.md)
|
||||
and `docs/language.rst` section 8.1. Implementation and verification must be
|
||||
identified separately; this document does not establish App/native acceptance.
|
||||
|
||||
## User operation and identity
|
||||
|
||||
Keep **Hide selected placements** and plain Delete placement-only. An explicit
|
||||
**Delete selected from model…** action, also available through focused
|
||||
Shift+Delete, requests one semantic operation for the complete current group.
|
||||
Adding a port, relation or State row must never turn plain Delete into model
|
||||
deletion. Unsupported Hide mixtures explain the refusal without changing any
|
||||
member. Existing singleton behavior remains governed by section 8.1.
|
||||
|
||||
Every original typed target must be unique, structurally current and actually
|
||||
visible in the captured saved projection before deduplication or cascade
|
||||
normalization. A hidden, stale or unsupported member refuses the whole request,
|
||||
even if a selected ancestor would otherwise delete it. Pointer focus, current
|
||||
tool, revision, exact selection generation/primary and pending drafts remain
|
||||
App admission requirements, beyond this pure model planner.
|
||||
|
||||
| Selected visual target | Semantic deletion subject | Required identity evidence |
|
||||
| --- | --- | --- |
|
||||
| Placement | Its referenced model element | Exact placement belongs to captured diagram and current projected node |
|
||||
| Port | The semantic port itself | Exact visible port and its owner placement, not a promoted owner node |
|
||||
| Stored edge | Its relation or trace record | Exact visible edge with corresponding stored identity; inferred edges are not deletable objects |
|
||||
| State action/internal-transition row | Its StateAction or StateTransition relation | Exact visible row and state placement, not its referenced activity/function |
|
||||
|
||||
Whole-diagram targets, navigation-only Allocation groupings, Process/Scenario
|
||||
targets and unsupported synthetic identities refuse this saved-view operation.
|
||||
Saved Scenario presentations retain their dedicated local-occurrence adapter.
|
||||
Admit each visual identity first, then delete each semantic subject once.
|
||||
Current model validation forbids two placements of the same element in one
|
||||
diagram (`duplicate_placement`); semantic deduplication must not admit such an
|
||||
invalid project. The same element can appear in different diagrams, all of
|
||||
which participate in deletion impact. Selecting both a state and one of its
|
||||
rows is valid if both are visible.
|
||||
|
||||
## Candidate and impact
|
||||
|
||||
Validate the accepted input project, resolve the complete subject set, then
|
||||
normalize only redundant Delete commands covered by existing core cascades.
|
||||
Ancestry alone is insufficient: selected owners, ports, incident relations,
|
||||
traces and action rows can overlap through dependencies. Deterministic
|
||||
normalization must not depend on click order or primary selection. Do not run
|
||||
separate validated singleton transactions; an intermediate candidate may be
|
||||
invalid even though the full group deletion is valid.
|
||||
|
||||
Execute one final Batch candidate using the existing core deletion semantics.
|
||||
The final candidate is authoritative for actual removed identities and changed
|
||||
surviving records. Assert every selected semantic subject was removed and no
|
||||
identity was created. Include elements, relations, traces, artifact links,
|
||||
diagrams and embedded placements in impact accounting. Do not estimate union
|
||||
impact merely by summing individual deletion counts; conditional allocation
|
||||
dependents and global view/reference pruning require the combined result.
|
||||
|
||||
The foundation chooses original core deletion-impact coverage to suppress
|
||||
redundant selected commands, with deterministic subject ordering. Coverage is
|
||||
only command normalization, never the final impact report. Compared with
|
||||
mutating a scratch project after every root, this keeps admission and coverage
|
||||
bound to the same accepted input and avoids treating immediate scratch cleanup
|
||||
as identical to the engine's deferred allocation cleanup. The final Batch and
|
||||
complete selected-subject removal check remain mandatory. Tests cover two
|
||||
endpoint allocations whose deferred cascades overlap, together with a direct
|
||||
exchange deletion. An attempted alternate-support counterexample was invalid:
|
||||
the current unique allocation, single performer and distinct boundary-owner
|
||||
rules exclude simultaneous direct/reverse mappings. Preserve those rules and
|
||||
test refusal of the invalid fixture; do not claim a union-only cascade case.
|
||||
|
||||
Show the number of visual targets separately from unique semantic subjects,
|
||||
then actual removed/updated records with searchable names, aliases and kinds.
|
||||
All placements of deleted elements across views are affected. Referenced work
|
||||
survives removal of a State action relation; trace endpoint counterparts
|
||||
survive deletion of a trace. A counterpart may itself be deleted when it was
|
||||
explicitly selected, but a trace must not cause implicit counterpart deletion.
|
||||
Do not invent replacement links, activities or state transitions.
|
||||
|
||||
Deleting an exchange can remove Process occurrences and alter implicit order
|
||||
or a reused definition's interpretation. Changed owner records must be shown;
|
||||
unchanged stored caller records do not prove unchanged caller behavior. Before
|
||||
App acceptance, compare derived effects on reused callers using the Process
|
||||
review approach, or explicitly resolve that review requirement.
|
||||
|
||||
The App must confirm the captured candidate through one Document transaction,
|
||||
which includes source synchronization. A failed source update, cancelled review
|
||||
or stale token must not partially change model, source, selection, camera or
|
||||
history. Success removes transient deleted selection, chooses a surviving
|
||||
inspection context and supports one exact model/source Undo and Redo. The
|
||||
review footer remains reachable while impact content scrolls or is filtered.
|
||||
|
||||
## Verification obligations
|
||||
|
||||
- OA and SA; exact placement-to-subject mapping and other-view occurrences;
|
||||
invalid duplicate placements; nested semantic ownership;
|
||||
selected ancestors with selected descendants and incident connections.
|
||||
- Exact ports with owner/edge mixtures, allocation dependency interactions,
|
||||
State rows and transitions preserving referenced work, and trace deletion
|
||||
preserving endpoints/counterparts.
|
||||
- Wrong placement/port/row association, foreign/hidden/synthetic members,
|
||||
duplicates and cross-family inputs refuse before normalization. Selected
|
||||
ancestor coverage must not make an invalid descendant admissible.
|
||||
- Independently construct the complete expected Project; inspect final Batch
|
||||
effects and exact source/history rather than comparing two calls to the same
|
||||
planner. Preserve unrelated views, authored metadata and surviving records.
|
||||
- App stale/ABA/private-gesture/draft/tool/modal admission and failure recovery;
|
||||
actual Windows build plus native group review, Cancel, confirmation, Save,
|
||||
Undo/Redo/reopen. Bounded planner checks do not satisfy these later gates.
|
||||
|
||||
Rejected UI approach: overloaded plain Delete changes destructiveness when a
|
||||
row or edge joins a placement group. Explicit model deletion preserves a stable
|
||||
meaning and still supports a clear mixed semantic selection. Rejected planner
|
||||
approach: only removing selected ancestor duplicates misses non-ownership
|
||||
cascade dependencies; repeated validated singleton deletes can reject a valid
|
||||
union or create partial history.
|
||||
Reference in New Issue
Block a user