Preserve responsibility assignments when creating child work
This commit is contained in:
@@ -3290,6 +3290,52 @@ impl App {
|
||||
id,
|
||||
));
|
||||
}
|
||||
// Refining a leaf moves its explicit responsibilities to the new leaf.
|
||||
// Later children inherit only the existing unambiguous performer summary.
|
||||
if self.create_kind.is_work()
|
||||
&& let Some(owner) = parent
|
||||
&& self
|
||||
.project()
|
||||
.elements
|
||||
.get(&owner)
|
||||
.is_some_and(|e| e.layer() == self.layer && e.kind() == self.create_kind)
|
||||
{
|
||||
let performer = oa_core::validation::allocation_summary(self.project(), owner);
|
||||
let leaf = !self
|
||||
.project()
|
||||
.children(owner)
|
||||
.any(|e| e.kind() == self.create_kind);
|
||||
let responsibilities: Vec<_> = self
|
||||
.project()
|
||||
.relations
|
||||
.values()
|
||||
.filter(|r| {
|
||||
r.target() == owner
|
||||
&& matches!(
|
||||
r.kind(),
|
||||
RelationKind::Allocation | RelationKind::RoleAssignment
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
if leaf && !responsibilities.is_empty() {
|
||||
for original in responsibilities {
|
||||
let mut responsibility = original.clone();
|
||||
match &mut responsibility.body {
|
||||
RelationBody::Allocation { target, .. }
|
||||
| RelationBody::RoleAssignment { target, .. } => *target = id,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
commands.push(Command::EditRelation(original.id, responsibility));
|
||||
}
|
||||
} else if let Some(performer) = performer {
|
||||
commands.push(Command::Connect(
|
||||
RelationKind::Allocation,
|
||||
"Performs".into(),
|
||||
performer,
|
||||
id,
|
||||
));
|
||||
}
|
||||
}
|
||||
if self.create_kind == Kind::Occurrence
|
||||
&& let Some(owner) = parent.and_then(|id| self.project().elements.get(&id))
|
||||
&& let Some(steps) = owner.steps()
|
||||
@@ -3307,19 +3353,13 @@ impl App {
|
||||
.get(&diagram)
|
||||
.is_none_or(|d| d.projection.includes(self.create_kind))
|
||||
{
|
||||
let mut p = self.project().clone();
|
||||
for command in &commands {
|
||||
if let Command::CreateDiagram(d) = command {
|
||||
p.diagrams.insert(d.id, d.clone());
|
||||
let mut p = match execute_batch(self.project(), &commands) {
|
||||
Ok(candidate) => candidate,
|
||||
Err(error) => {
|
||||
self.status = error.to_string();
|
||||
return;
|
||||
}
|
||||
}
|
||||
p.elements.insert(
|
||||
id,
|
||||
match &commands[0] {
|
||||
Command::CreateElement(e) => e.clone(),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
);
|
||||
};
|
||||
let owner = if allocated {
|
||||
Some(self.selected)
|
||||
} else {
|
||||
|
||||
@@ -1663,3 +1663,388 @@ fn adding_bend_between_close_ports_stays_outside_owner_clearance() {
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn child_work_preserves_allocated_context_and_atomic_history() {
|
||||
use oa_core::projection::project_view;
|
||||
for (layer, performer_kind, work_kind) in [
|
||||
(Layer::Oa, Kind::Entity, Kind::Activity),
|
||||
(Layer::Sa, Kind::System, Kind::Function),
|
||||
] {
|
||||
for folded in [false, true] {
|
||||
let mut p = Project::empty();
|
||||
let performer = p
|
||||
.add_element(performer_kind, layer, "Performer", None)
|
||||
.unwrap();
|
||||
let work = p
|
||||
.add_element(work_kind, layer, "Parent work", None)
|
||||
.unwrap();
|
||||
let allocation = p.relate(
|
||||
RelationKind::Allocation,
|
||||
"Engineering assignment",
|
||||
performer,
|
||||
work,
|
||||
);
|
||||
p.relations.get_mut(&allocation).unwrap().description =
|
||||
"Retain assignment metadata".into();
|
||||
let original = p.relations[&allocation].clone();
|
||||
let diagram = p.ensure_diagram(layer);
|
||||
p.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Allocation;
|
||||
let performer_node = p.place(diagram, performer, Position { x: 60., y: 60. });
|
||||
let work_node = p.place(diagram, work, Position { x: 84., y: 124. });
|
||||
p = execute_batch(&p, &[Command::Layout(diagram)]).unwrap();
|
||||
for node in [performer_node, work_node] {
|
||||
p.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&node)
|
||||
.unwrap()
|
||||
.collapsed = folded;
|
||||
}
|
||||
let mut app = App::with_project(p);
|
||||
send(&mut app, Message::Layer(layer));
|
||||
send(&mut app, Message::Page(Page::Diagram));
|
||||
send(
|
||||
&mut app,
|
||||
Message::Diagram(crate::app::Choice {
|
||||
id: diagram,
|
||||
label: String::new(),
|
||||
}),
|
||||
);
|
||||
let mut first = None;
|
||||
for step in 0..3 {
|
||||
let parent = if step == 2 { first.unwrap() } else { work };
|
||||
send(&mut app, Message::Select(parent));
|
||||
let before = app.project().clone();
|
||||
let before_source = app.document.bundle().clone();
|
||||
send(&mut app, Message::CreateContextElement(work_kind, true));
|
||||
let created = app.selected;
|
||||
assert!(
|
||||
!before.elements.contains_key(&created),
|
||||
"{layer:?} step {step}: {}",
|
||||
app.status
|
||||
);
|
||||
assert_eq!(app.project().elements.len(), before.elements.len() + 1);
|
||||
assert_eq!(app.project().elements[&created].parent, Some(parent));
|
||||
for (id, element) in &before.elements {
|
||||
assert_eq!(&app.project().elements[id], element);
|
||||
}
|
||||
assert!(
|
||||
!oa_core::validation::validate(app.project())
|
||||
.iter()
|
||||
.any(|i| i.severity == oa_core::validation::Severity::Error)
|
||||
);
|
||||
assert_eq!(
|
||||
oa_core::validation::allocation_summary(app.project(), work),
|
||||
Some(performer)
|
||||
);
|
||||
assert_eq!(
|
||||
oa_core::validation::effective_performer(app.project(), created),
|
||||
Some(performer)
|
||||
);
|
||||
assert!(
|
||||
!app.project()
|
||||
.relations
|
||||
.values()
|
||||
.any(|r| r.kind() == RelationKind::Allocation && r.target() == parent)
|
||||
);
|
||||
if step != 1 {
|
||||
let mut expected = original.clone();
|
||||
if let RelationBody::Allocation { target, .. } = &mut expected.body {
|
||||
*target = created;
|
||||
}
|
||||
assert_eq!(app.project().relations[&allocation], expected);
|
||||
} else {
|
||||
assert_eq!(
|
||||
app.project().relations[&allocation],
|
||||
before.relations[&allocation]
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
app.project().relations.len(),
|
||||
before.relations.len() + usize::from(step == 1)
|
||||
);
|
||||
let view = project_view(app.project(), diagram);
|
||||
let parent_node = view.nodes.iter().find(|n| n.element == parent).unwrap();
|
||||
let child_node = view.nodes.iter().find(|n| n.element == created).unwrap();
|
||||
let root_node = view.nodes.iter().find(|n| n.element == work).unwrap();
|
||||
assert_eq!(root_node.visual_parent, Some(performer_node));
|
||||
assert_eq!(child_node.visual_parent, Some(parent_node.id));
|
||||
assert!(!app.project().diagrams[&diagram].nodes[&performer_node].collapsed);
|
||||
assert!(!app.project().diagrams[&diagram].nodes[&parent_node.id].collapsed);
|
||||
assert!(child_node.position.x >= parent_node.position.x);
|
||||
assert!(child_node.position.y >= parent_node.position.y + 64.);
|
||||
assert!(
|
||||
child_node.position.x + child_node.size.width
|
||||
<= parent_node.position.x + parent_node.size.width
|
||||
);
|
||||
assert!(
|
||||
child_node.position.y + child_node.size.height
|
||||
<= parent_node.position.y + parent_node.size.height
|
||||
);
|
||||
for sibling in view
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|n| n.visual_parent == Some(parent_node.id) && n.id != child_node.id)
|
||||
{
|
||||
assert!(sibling.position.y + sibling.size.height <= child_node.position.y);
|
||||
}
|
||||
let after = app.project().clone();
|
||||
let after_source = app.document.bundle().clone();
|
||||
send(&mut app, Message::Undo);
|
||||
assert_eq!(
|
||||
app.project(),
|
||||
&before,
|
||||
"creation, allocation and reveal/reflow undo together"
|
||||
);
|
||||
assert_eq!(app.document.bundle(), &before_source);
|
||||
send(&mut app, Message::Redo);
|
||||
assert_eq!(app.project(), &after);
|
||||
assert_eq!(app.document.bundle(), &after_source);
|
||||
if step == 0 {
|
||||
first = Some(created);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn child_work_does_not_invent_unallocated_or_mixed_performers() {
|
||||
for (layer, performer_kind, other_kind, work_kind) in [
|
||||
(Layer::Oa, Kind::Entity, Kind::Entity, Kind::Activity),
|
||||
(Layer::Sa, Kind::System, Kind::SystemActor, Kind::Function),
|
||||
] {
|
||||
// Root leaf, partly unallocated composite, and mixed-performer composite.
|
||||
for context in 0..3 {
|
||||
let mut p = Project::empty();
|
||||
let performer = p
|
||||
.add_element(performer_kind, layer, "Performer", None)
|
||||
.unwrap();
|
||||
let other = p
|
||||
.add_element(other_kind, layer, "Other performer", None)
|
||||
.unwrap();
|
||||
let work = p
|
||||
.add_element(work_kind, layer, "Parent work", None)
|
||||
.unwrap();
|
||||
let mut placed = vec![performer, other, work];
|
||||
if context != 0 {
|
||||
let first = p
|
||||
.add_element(work_kind, layer, "First leaf", Some(work))
|
||||
.unwrap();
|
||||
let second = p
|
||||
.add_element(work_kind, layer, "Second leaf", Some(work))
|
||||
.unwrap();
|
||||
p.relate(
|
||||
RelationKind::Allocation,
|
||||
"First assignment",
|
||||
performer,
|
||||
first,
|
||||
);
|
||||
if context == 2 {
|
||||
p.relate(RelationKind::Allocation, "Other assignment", other, second);
|
||||
}
|
||||
placed.extend([first, second]);
|
||||
}
|
||||
assert_eq!(oa_core::validation::allocation_summary(&p, work), None);
|
||||
let diagram = p.ensure_diagram(layer);
|
||||
p.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Allocation;
|
||||
for id in placed {
|
||||
p.place(diagram, id, Position { x: 60., y: 60. });
|
||||
}
|
||||
p = execute_batch(&p, &[Command::Layout(diagram)]).unwrap();
|
||||
let mut app = App::with_project(p);
|
||||
send(&mut app, Message::Layer(layer));
|
||||
send(&mut app, Message::Page(Page::Diagram));
|
||||
send(
|
||||
&mut app,
|
||||
Message::Diagram(crate::app::Choice {
|
||||
id: diagram,
|
||||
label: String::new(),
|
||||
}),
|
||||
);
|
||||
send(&mut app, Message::Select(work));
|
||||
let before = app.project().clone();
|
||||
let before_source = app.document.bundle().clone();
|
||||
send(&mut app, Message::CreateContextElement(work_kind, true));
|
||||
let child = app.selected;
|
||||
assert!(!before.elements.contains_key(&child), "{}", app.status);
|
||||
assert_eq!(app.project().elements[&child].parent, Some(work));
|
||||
assert_eq!(app.project().relations, before.relations);
|
||||
assert_eq!(
|
||||
oa_core::validation::effective_performer(app.project(), child),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
oa_core::validation::allocation_summary(app.project(), work),
|
||||
None
|
||||
);
|
||||
let after = app.project().clone();
|
||||
let after_source = app.document.bundle().clone();
|
||||
send(&mut app, Message::Undo);
|
||||
assert_eq!(app.project(), &before);
|
||||
assert_eq!(app.document.bundle(), &before_source);
|
||||
send(&mut app, Message::Redo);
|
||||
assert_eq!(app.project(), &after);
|
||||
assert_eq!(app.document.bundle(), &after_source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn child_work_transfers_leaf_roles_without_fabricating_responsibilities() {
|
||||
// Direct allocation + matching roles, uniquely realized roles only,
|
||||
// unresolved roles, and roles realized by different performers.
|
||||
for context in 0..4 {
|
||||
let mut p = Project::empty();
|
||||
let performer = p
|
||||
.add_element(Kind::Entity, Layer::Oa, "Performer", None)
|
||||
.unwrap();
|
||||
let other = p
|
||||
.add_element(Kind::Entity, Layer::Oa, "Other performer", None)
|
||||
.unwrap();
|
||||
let work = p
|
||||
.add_element(Kind::Activity, Layer::Oa, "Responsible work", None)
|
||||
.unwrap();
|
||||
let mut responsibilities = Vec::new();
|
||||
if context == 0 {
|
||||
responsibilities.push(p.relate(
|
||||
RelationKind::Allocation,
|
||||
"Direct responsibility",
|
||||
performer,
|
||||
work,
|
||||
));
|
||||
}
|
||||
for index in 0..2 {
|
||||
let role = p
|
||||
.add_element(Kind::Role, Layer::Oa, &format!("Role {index}"), None)
|
||||
.unwrap();
|
||||
responsibilities.push(p.relate(
|
||||
RelationKind::RoleAssignment,
|
||||
&format!("Assigned role {index}"),
|
||||
role,
|
||||
work,
|
||||
));
|
||||
if context != 2 {
|
||||
let source = if context == 3 && index == 1 {
|
||||
other
|
||||
} else {
|
||||
performer
|
||||
};
|
||||
p.relate(RelationKind::RoleRealization, "Realizes role", source, role);
|
||||
}
|
||||
}
|
||||
for (index, id) in responsibilities.iter().enumerate() {
|
||||
let relation = p.relations.get_mut(id).unwrap();
|
||||
relation.description =
|
||||
format!("Responsibility {index}: preserve engineering rationale");
|
||||
let link = Id::new_v4();
|
||||
p.artifact_links.insert(
|
||||
link,
|
||||
ArtifactLink {
|
||||
id: link,
|
||||
alias: format!("responsibility_evidence_{index}"),
|
||||
name: format!("Responsibility evidence {index}"),
|
||||
description: "Preserve the reference to this exact relation".into(),
|
||||
owner: *id,
|
||||
relation: ArtifactRelation::References,
|
||||
uri: format!("https://example.invalid/responsibility/{index}"),
|
||||
revision: Some("review-7".into()),
|
||||
fragment: Some("assignment".into()),
|
||||
},
|
||||
);
|
||||
}
|
||||
let original_relations = p.relations.clone();
|
||||
let diagram = p.ensure_diagram(Layer::Oa);
|
||||
p.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Allocation;
|
||||
p.place(diagram, performer, Position { x: 60., y: 60. });
|
||||
p.place(diagram, work, Position { x: 84., y: 124. });
|
||||
p = execute_batch(&p, &[Command::Layout(diagram)]).unwrap();
|
||||
let expected_performer = (context < 2).then_some(performer);
|
||||
assert_eq!(
|
||||
oa_core::validation::allocation_summary(&p, work),
|
||||
expected_performer
|
||||
);
|
||||
let mut app = App::with_project(p);
|
||||
send(&mut app, Message::Page(Page::Diagram));
|
||||
send(
|
||||
&mut app,
|
||||
Message::Diagram(crate::app::Choice {
|
||||
id: diagram,
|
||||
label: String::new(),
|
||||
}),
|
||||
);
|
||||
let mut first = None;
|
||||
for step in 0..2 {
|
||||
send(&mut app, Message::Select(work));
|
||||
let before = app.project().clone();
|
||||
let before_source = app.document.bundle().clone();
|
||||
send(
|
||||
&mut app,
|
||||
Message::CreateContextElement(Kind::Activity, true),
|
||||
);
|
||||
let child = app.selected;
|
||||
assert!(
|
||||
!before.elements.contains_key(&child),
|
||||
"context {context}, step {step}: {}",
|
||||
app.status
|
||||
);
|
||||
assert_eq!(app.project().elements[&child].parent, Some(work));
|
||||
if step == 0 {
|
||||
first = Some(child);
|
||||
}
|
||||
for (id, original) in &original_relations {
|
||||
let mut expected = original.clone();
|
||||
if responsibilities.contains(id) {
|
||||
match &mut expected.body {
|
||||
RelationBody::Allocation { target, .. }
|
||||
| RelationBody::RoleAssignment { target, .. } => *target = first.unwrap(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
assert_eq!(app.project().relations[id], expected);
|
||||
}
|
||||
assert_eq!(app.project().artifact_links, before.artifact_links);
|
||||
assert!(!app.project().artifact_links.is_empty());
|
||||
let added = usize::from(step == 1 && expected_performer.is_some());
|
||||
assert_eq!(
|
||||
app.project().relations.len(),
|
||||
before.relations.len() + added
|
||||
);
|
||||
assert_eq!(
|
||||
oa_core::validation::effective_performer(app.project(), child),
|
||||
expected_performer
|
||||
);
|
||||
assert_eq!(
|
||||
app.project()
|
||||
.relations
|
||||
.values()
|
||||
.filter(|r| r.kind() == RelationKind::RoleAssignment && r.target() == child)
|
||||
.count(),
|
||||
if step == 0 { 2 } else { 0 }
|
||||
);
|
||||
if context != 0 && step == 0 {
|
||||
assert!(
|
||||
!app.project()
|
||||
.relations
|
||||
.values()
|
||||
.any(|r| r.kind() == RelationKind::Allocation)
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
!oa_core::validation::validate(app.project())
|
||||
.iter()
|
||||
.any(|i| i.severity == oa_core::validation::Severity::Error)
|
||||
);
|
||||
let after = app.project().clone();
|
||||
let after_source = app.document.bundle().clone();
|
||||
send(&mut app, Message::Undo);
|
||||
assert_eq!(app.project(), &before);
|
||||
assert_eq!(app.document.bundle(), &before_source);
|
||||
send(&mut app, Message::Redo);
|
||||
assert_eq!(app.project(), &after);
|
||||
assert_eq!(app.document.bundle(), &after_source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user