Preserve nested architecture during authoring and distinguish allocation summaries

This commit is contained in:
2026-09-05 17:58:46 +03:00
parent c20a2073d7
commit c7101b970a
13 changed files with 2719 additions and 1653 deletions
+3 -1
View File
@@ -41,10 +41,12 @@ V2 release bundles are `dist/ArcadiaStudio-Windows.zip` and `dist/ArcadiaStudio-
The coastal rescue example combines people, an organization, a System of Interest, information and physical exchange items, normal/degraded behavior and code/CAD evidence. Choose **New** to start an empty project.
The example opens in an architecture view with work nested inside its performers. Each layer also has dedicated functional dataflow and mission/capability views. Regenerate both example formats with matching identities using `cargo run --locked -p oa-core --example generate_demo`; the command verifies both saved models. Historical migration fixtures remain separate.
1. In **Operational Analysis**, create entities, actors, activities and capabilities. Use **In selected** for semantic decomposition and **Allocated** for new leaf work performed by a participant. Activities can decompose independently of their performer; roles capture shared operational responsibility.
2. Switch to **System Analysis**. Define one System of Interest and external Actors, then functions, missions, capabilities and functional chains. The system boundary has no internal LA/PA components in this version.
3. Use **Create relationship** for allocations, interactions/functional exchanges, capability involvement, mission links, state transitions and interface relationships. Add exchange items, typed data, ports and provided/required interfaces in **Data & interfaces**.
4. Create multiple architecture views. **Decomposition** nests semantic children; **Allocation** nests work within performers, including computed summaries. Placement dragging changes geometry. **Hide** removes a placement; **Delete from model** removes semantics and dependent references. Use **Placement** and **View properties** for exact values.
4. Create focused views. **Containment** nests semantic children; **Architecture · allocated work** nests work within performers; **Functional dataflow** shows work decomposition and exchanges; **Missions & capabilities** shows objectives and participation. Dashed, italic parent work denotes an allocation summary inferred from its leaves. Ports appear on their owner's boundary. Placement dragging changes geometry. **Hide** removes a placement; **Delete from model** removes semantics and dependent references. Use **Placement** and **View properties** for exact values.
5. In **Behavior & scenarios**, author processes/chains, scenario lifelines and ordered occurrences, including repeated uses of the same exchange and explicit fragments/branches. State machines contain regions and states/modes with guarded transitions, availability and state-action links. Conditions are descriptive, not executable simulation code. Creating records in Behavior or Data preserves your architecture layouts; **Show in this diagram** adds an explicit placement when desired.
6. Select an OA or SA element and choose **Create / update counterpart**. Review dependency scope and counterpart choices, then apply one undoable transaction. Existing counterparts retain their structural choices; reviewed updates compare names/descriptions. Manual traces link other engineering correspondences. **Traceability** also edits artifact URI, revision and fragment metadata.
7. Review **Model checks**, then save `.arcadia` source or `.json`. Warnings allow incomplete authoring; structural errors preserve the committed model and rejected draft.
+23
View File
@@ -0,0 +1,23 @@
//! Regenerate both distributable forms from one model, preserving shared IDs.
use oa_core::{Project, persistence};
use std::path::PathBuf;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let directory = std::env::args_os()
.nth(1)
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("examples"));
let project = Project::demo();
let source = directory.join("coastal-rescue.arcadia");
let json = directory.join("coastal-rescue.json");
persistence::save(&source, &project)?;
persistence::save(&json, &project)?;
assert_eq!(persistence::load(&source)?, project);
assert_eq!(persistence::load(&json)?, project);
println!(
"Wrote and verified {} and {}",
source.display(),
json.display()
);
Ok(())
}
+63 -9
View File
@@ -11,6 +11,8 @@ pub struct ProjectedNode {
pub visual_parent: Option<Id>,
pub depth: usize,
pub collapsed: bool,
/// The work is grouped by its allocated leaves, not by a direct allocation.
pub allocation_summary: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ProjectedEdge {
@@ -42,6 +44,17 @@ pub(crate) fn minimum_node_size(p: &Project, element: Id, mut size: Size) -> Siz
}
size
}
pub(crate) fn port_footer(p: &Project, element: Id) -> f32 {
let count = p
.children(element)
.filter(|e| e.kind() == Kind::Port)
.count();
if count == 0 {
24.
} else {
28. * count as f32 + 24.
}
}
pub(crate) fn parents(p: &Project, d: &Diagram) -> BTreeMap<Id, Option<Id>> {
let placed: BTreeMap<_, _> = d
@@ -62,12 +75,22 @@ pub(crate) fn parents(p: &Project, d: &Diagram) -> BTreeMap<Id, Option<Id>> {
if d.projection == ProjectionKind::Allocation
&& let Some(performer) = crate::validation::allocation_summary(p, n.element)
{
let keep_parent = semantic.is_some_and(|parent| {
crate::validation::allocation_summary(p, parent) == Some(performer)
});
if !keep_parent {
semantic = Some(performer);
let mut candidate = semantic;
let mut visible_work_parent = None;
let mut seen = BTreeSet::new();
while let Some(id) = candidate {
if !seen.insert(id)
|| crate::validation::allocation_summary(p, id) != Some(performer)
{
break;
}
if placed.contains_key(&id) {
visible_work_parent = Some(id);
break;
}
candidate = p.elements.get(&id).and_then(|e| e.parent);
}
semantic = visible_work_parent.or(Some(performer));
}
if d.projection == ProjectionKind::Traceability {
return (n.id, None);
@@ -117,6 +140,10 @@ pub fn project_view(p: &Project, diagram: Id) -> ProjectedView {
let Some(d) = p.diagrams.get(&diagram) else {
return ProjectedView::default();
};
project_diagram(p, d)
}
/// Project alternate placement geometry without copying or mutating semantics.
pub fn project_diagram(p: &Project, d: &Diagram) -> ProjectedView {
let parents = parents(p, d);
let mut nodes = Vec::new();
for n in d.nodes.values() {
@@ -145,7 +172,18 @@ pub fn project_view(p: &Project, diagram: Id) -> ProjectedView {
current = parents.get(&id).copied().flatten();
}
if !hidden {
let size = minimum_node_size(p, e.id, n.size);
let size = minimum_node_size(
p,
e.id,
if n.collapsed {
Size {
width: n.size.width,
height: 94.,
}
} else {
n.size
},
);
nodes.push(ProjectedNode {
id: n.id,
element: n.element,
@@ -154,6 +192,12 @@ pub fn project_view(p: &Project, diagram: Id) -> ProjectedView {
visual_parent: parents.get(&n.id).copied().flatten(),
depth,
collapsed: n.collapsed,
allocation_summary: d.projection == ProjectionKind::Allocation
&& crate::validation::allocation_summary(p, e.id).is_some()
&& !p
.relations
.values()
.any(|r| r.kind() == RelationKind::Allocation && r.target() == e.id),
});
}
}
@@ -164,11 +208,12 @@ pub fn project_view(p: &Project, diagram: Id) -> ProjectedView {
.visual_parent
.and_then(|id| nodes.iter().position(|n| n.id == id))
{
let footer = port_footer(p, nodes[parent].element);
let p = &mut nodes[parent];
let right =
(p.position.x + p.size.width).max(child.position.x + child.size.width + 24.);
let bottom =
(p.position.y + p.size.height).max(child.position.y + child.size.height + 24.);
(p.position.y + p.size.height).max(child.position.y + child.size.height + footer);
p.position.x = p.position.x.min(child.position.x - 24.);
// Reserve two readable header lines above the first nested card.
p.position.y = p.position.y.min(child.position.y - 64.);
@@ -209,14 +254,23 @@ pub fn project_view(p: &Project, diagram: Id) -> ProjectedView {
let side: Vec<_> = children.iter().filter(|e| matches!(e.shared(), Some(SharedBody::Port {direction: d, ..}) if *d == direction)).collect();
for (index, port) in side.iter().enumerate() {
let fraction = (index + 1) as f32 / (side.len() + 1) as f32;
let y = if p.elements[&node.element].kind().is_performer()
|| nodes
.iter()
.any(|child| child.visual_parent == Some(node.id))
{
node.position.y + node.size.height - 24. - index as f32 * 28.
} else {
node.position.y + 64. + (node.size.height - 76.).max(0.) * fraction
};
let position = match direction {
PortDirection::In => Position {
x: node.position.x,
y: node.position.y + 64. + (node.size.height - 76.).max(0.) * fraction,
y,
},
PortDirection::Out => Position {
x: node.position.x + node.size.width,
y: node.position.y + 64. + (node.size.height - 76.).max(0.) * fraction,
y,
},
PortDirection::InOut => Position {
x: node.position.x + node.size.width * fraction,
+2 -2
View File
@@ -680,7 +680,7 @@ fn layout(p: &mut Project, diagram: Id) -> Result<(), ModelError> {
width = width.max(size.width + 48.);
height += size.height + 16.;
}
height += 8.;
height += crate::projection::port_footer(p, d.nodes[&id].element) - 16.;
} else {
height = own.height;
}
@@ -779,7 +779,7 @@ fn layout(p: &mut Project, diagram: Id) -> Result<(), ModelError> {
row_height = 0.;
}
place(*id, Position { x, y }, &children, &sizes, &mut positions);
x += sizes[id].width + 60.;
x += sizes[id].width + 240.;
row_height = row_height.max(sizes[id].height);
}
let d = p.diagrams.get_mut(&diagram).unwrap();
+71
View File
@@ -1,5 +1,76 @@
use oa_core::{projection::*, *};
#[test]
fn unplaced_work_parent_cannot_detach_a_leaf_from_its_visible_performer() {
let mut p = Project::empty();
let performer = p
.add_element(Kind::Entity, Layer::Oa, "Rescue team", None)
.unwrap();
let work = p
.add_element(Kind::Activity, Layer::Oa, "Respond", None)
.unwrap();
let leaf = p
.add_element(Kind::Activity, Layer::Oa, "Dispatch", Some(work))
.unwrap();
p.relate(RelationKind::Allocation, "Performs", performer, leaf);
let diagram = p.ensure_diagram(Layer::Oa);
p.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Allocation;
let owner = p.place(diagram, performer, Position { x: 0., y: 0. });
let child = p.place(diagram, leaf, Position { x: 40., y: 80. });
let view = project_view(&p, diagram);
assert_eq!(
view.nodes
.iter()
.find(|n| n.id == child)
.unwrap()
.visual_parent,
Some(owner)
);
let summary = p.place(diagram, work, Position { x: 24., y: 64. });
let view = project_view(&p, diagram);
assert!(
view.nodes
.iter()
.find(|n| n.id == summary)
.unwrap()
.allocation_summary
);
assert!(
!view
.nodes
.iter()
.find(|n| n.id == child)
.unwrap()
.allocation_summary
);
assert_eq!(
view.nodes
.iter()
.find(|n| n.id == child)
.unwrap()
.visual_parent,
Some(summary)
);
p.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::FunctionalDataflow;
assert!(
project_view(&p, diagram)
.nodes
.iter()
.all(|n| !n.allocation_summary)
);
p.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Allocation;
p.diagrams.get_mut(&diagram).unwrap().nodes.remove(&summary);
let view = project_view(&p, diagram);
assert_eq!(
view.nodes
.iter()
.find(|n| n.id == child)
.unwrap()
.visual_parent,
Some(owner)
);
}
#[test]
fn architecture_is_nested_exchange_context_and_ports_attach_without_placements() {
let p = Project::demo();
+111 -17
View File
@@ -873,7 +873,17 @@ impl App {
.relations
.get(&selected)
.map(|r| vec![r.source(), r.target()])
.unwrap_or_else(|| vec![selected]);
.unwrap_or_else(|| vec![selected])
.into_iter()
.map(|id| {
self.project()
.elements
.get(&id)
.filter(|e| e.kind() == Kind::Port)
.and_then(|e| e.parent)
.unwrap_or(id)
})
.collect::<Vec<_>>();
let nodes = view
.nodes
.values()
@@ -1076,11 +1086,15 @@ impl App {
.cloned()
{
n.collapsed = !n.collapsed;
self.execute(Command::EditNode {
let selected = self.selected;
if self.execute(Command::EditNode {
diagram,
original: n.id,
node: n,
});
}) {
self.selected = selected;
self.refresh_draft();
}
}
}
Message::MoveNode(node, position) => {
@@ -1421,28 +1435,35 @@ impl App {
let id = e.id;
let mut commands = vec![Command::CreateElement(e)];
let mut diagram_target = self.active_diagram;
let preferred = if self.create_kind.is_work() || self.create_kind.is_performer() {
Some(ProjectionKind::Allocation)
} else if ProjectionKind::Capabilities.includes(self.create_kind) {
Some(ProjectionKind::Capabilities)
} else {
None
};
if self.page == Page::Diagram
&& self
.active_diagram
.and_then(|id| self.project().diagrams.get(&id))
.is_some_and(|d| !d.projection.includes(self.create_kind))
&& ProjectionKind::Capabilities.includes(self.create_kind)
&& let Some(preferred) = preferred
{
diagram_target = self
.project()
.diagrams
.values()
.find(|d| d.layer == self.layer && d.projection == ProjectionKind::Capabilities)
.find(|d| d.layer == self.layer && d.projection == preferred)
.map(|d| d.id);
if diagram_target.is_none() {
let view = Id::new_v4();
commands.push(Command::CreateDiagram(Diagram {
id: view,
alias: self.project().fresh_alias("capabilities", "view", view),
name: format!("{} missions and capabilities", self.layer),
alias: self.project().fresh_alias("engineering_view", "view", view),
name: format!("{} {preferred}", self.layer),
description: String::new(),
layer: self.layer,
projection: ProjectionKind::Capabilities,
projection: preferred,
nodes: Default::default(),
}));
diagram_target = Some(view);
@@ -1486,25 +1507,98 @@ impl App {
_ => unreachable!(),
},
);
let count = p.diagrams[&diagram].nodes.len();
let node = p.place(
diagram,
id,
Position {
x: 60. + (count % 3) as f32 * 280.,
y: 60. + (count / 3) as f32 * 150.,
},
);
let owner = if allocated {
Some(self.selected)
} else {
parent
};
// Insertion measures an expanded owner path and commits that reveal
// with the new child, so folded contents cannot overlap the new row.
if let Some(owner) = owner {
let mut expanded = p.diagrams[&diagram].clone();
for node in expanded.nodes.values_mut() {
node.collapsed = false;
}
let expanded_view = oa_core::projection::project_diagram(&p, &expanded);
let mut current = expanded_view
.nodes
.iter()
.find(|n| n.element == owner)
.map(|n| n.id);
while let Some(id) = current {
let node = p
.diagrams
.get_mut(&diagram)
.unwrap()
.nodes
.get_mut(&id)
.unwrap();
if node.collapsed {
node.collapsed = false;
commands.push(Command::EditNode {
diagram,
original: id,
node: node.clone(),
});
}
current = expanded_view
.nodes
.iter()
.find(|n| n.id == id)
.and_then(|n| n.visual_parent);
}
}
// Reserve the expanded size of folded child containers so inserting
// a sibling does not overlap their contents when they are unfolded.
let mut expanded = p.diagrams[&diagram].clone();
for node in expanded.nodes.values_mut() {
node.collapsed = false;
}
let view = oa_core::projection::project_diagram(&p, &expanded);
let position =
if let Some(container) = view.nodes.iter().find(|n| Some(n.element) == owner) {
let y = view
.nodes
.iter()
.filter(|n| n.visual_parent == Some(container.id))
.map(|n| n.position.y + n.size.height + 16.)
.fold(container.position.y + 64., f32::max);
Position {
x: container.position.x + 24.,
y,
}
} else {
Position {
x: view
.nodes
.iter()
.map(|n| n.position.x + n.size.width + 240.)
.fold(60., f32::max),
y: 60.,
}
};
let node = p.place(diagram, id, position);
commands.push(Command::CreateNode {
diagram,
node: p.diagrams[&diagram].nodes[&node].clone(),
});
}
if self.page == Page::Diagram
&& let Some(diagram) = diagram_target
&& let Ok(candidate) = execute_batch(self.project(), &commands)
{
commands.extend(crate::diagram::insertion_adjustments(
&candidate, diagram, id,
));
}
if self.execute(Command::Batch(commands)) {
self.active_diagram = diagram_target;
self.selected = id;
self.inspector_tab = crate::navigation::InspectorTab::Properties;
self.refresh_draft();
if self.page == Page::Diagram {
self.fit();
}
self.status = "Element created · enter its engineering name and properties".into();
}
}
+310 -54
View File
@@ -25,6 +25,101 @@ pub struct State {
preview: Option<Position>,
pan_start: Option<Point>,
}
/// Make room along the inserted node's containment path, moving only colliding
/// later siblings and their descendants. Existing semantic ownership is unchanged.
pub fn insertion_adjustments(p: &Project, diagram_id: Id, element: Id) -> Vec<Command> {
let mut diagram = p.diagrams[&diagram_id].clone();
// Reflow folded subtrees at their expanded size, without changing the
// user's fold state. Their hidden placements move with their container.
for node in diagram.nodes.values_mut() {
node.collapsed = false;
}
let view = oa_core::projection::project_diagram(p, &diagram);
let Some(inserted) = view.nodes.iter().find(|n| n.element == element) else {
return vec![];
};
let mut groups = Vec::new();
let mut affected = inserted.id;
let mut parent = inserted.visual_parent;
loop {
groups.push((parent, affected));
let Some(id) = parent else {
break;
};
affected = id;
parent = view
.nodes
.iter()
.find(|n| n.id == id)
.and_then(|n| n.visual_parent);
}
for (group, affected) in groups {
let view = oa_core::projection::project_diagram(p, &diagram);
let mut siblings: Vec<_> = view
.nodes
.iter()
.filter(|n| n.visual_parent == group)
.collect();
siblings.sort_by(|a, b| {
a.position
.y
.total_cmp(&b.position.y)
.then(a.position.x.total_cmp(&b.position.x))
.then(a.id.cmp(&b.id))
});
let ids: Vec<_> = siblings.iter().map(|n| n.id).collect();
let mut settled: Vec<(Rectangle, bool)> = Vec::new();
for id in ids {
let current = oa_core::projection::project_diagram(p, &diagram);
let node = current.nodes.iter().find(|n| n.id == id).unwrap();
let mut rect = Rectangle::new(
Point::new(node.position.x, node.position.y),
Size::new(node.size.width, node.size.height),
);
let original_y = rect.y;
let displaced = id == affected
|| settled
.iter()
.any(|(other, moved)| *moved && rect.intersects(other));
if displaced {
loop {
let next_y = settled
.iter()
.filter(|(other, _)| rect.intersects(other))
.map(|(other, _)| other.y + other.height + 24.)
.fold(rect.y, f32::max);
if next_y == rect.y {
break;
}
rect.y = next_y;
}
}
let dy = rect.y - original_y;
if dy > 0. {
let mut moved = oa_core::projection::descendant_nodes(p, diagram_id, id);
moved.push(id);
for moved in moved {
diagram.nodes.get_mut(&moved).unwrap().position.y += dy;
}
}
settled.push((rect, displaced));
}
}
diagram
.nodes
.into_values()
.map(|mut node| {
node.collapsed = p.diagrams[&diagram_id].nodes[&node.id].collapsed;
node
})
.filter(|n| p.diagrams[&diagram_id].nodes[&n.id] != *n)
.map(|node| Command::EditNode {
diagram: diagram_id,
original: node.id,
node,
})
.collect()
}
impl DiagramCanvas<'_> {
fn world(&self, p: Point) -> Point {
Point::new(
@@ -68,30 +163,21 @@ impl DiagramCanvas<'_> {
})
}
fn preview_view(&self, state: &State) -> ProjectedView {
let mut view = project_view(self.project, self.diagram);
if let (Some((id, _, original)), Some(preview)) = (state.drag, state.preview) {
let dx = preview.x - original.x;
let dy = preview.y - original.y;
let mut moved = BTreeSet::from([id]);
for n in &view.nodes {
if n.visual_parent.is_some_and(|p| moved.contains(&p)) {
moved.insert(n.id);
}
}
for n in &mut view.nodes {
if moved.contains(&n.id) {
let mut diagram = self.project.diagrams[&self.diagram].clone();
let mut moved = oa_core::projection::descendant_nodes(self.project, self.diagram, id);
moved.push(id);
for id in moved {
if let Some(n) = diagram.nodes.get_mut(&id) {
n.position.x += dx;
n.position.y += dy;
}
}
for port in &mut view.ports {
if moved.contains(&port.owner) {
port.position.x += dx;
port.position.y += dy;
}
}
return oa_core::projection::project_diagram(self.project, &diagram);
}
view
project_view(self.project, self.diagram)
}
}
impl canvas::Program<Message> for DiagramCanvas<'_> {
@@ -250,20 +336,7 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
} else {
Color::from_rgb8(144, 158, 173)
};
let capability_label =
self.project
.relations
.get(&edge.id)
.and_then(|r| match r.kind() {
RelationKind::CapabilityInclude => Some("«include»"),
RelationKind::CapabilityExtend => Some("«extend»"),
RelationKind::CapabilityGeneralization => Some("specializes"),
RelationKind::MissionCapability => Some("exploits"),
RelationKind::MissionInvolvement | RelationKind::CapabilityInvolvement => {
Some("participates")
}
_ => None,
});
let capability_label = relationship_stereotype(self.project, edge.id);
let mut stroke =
Stroke::default()
.with_color(color)
@@ -418,23 +491,23 @@ fn paint_node(f: &mut Frame, p: &Project, n: &ProjectedNode, selected: Id, conta
a: 1.,
};
f.fill(&path, if container { tint } else { Color::WHITE });
f.stroke(
&path,
Stroke::default()
.with_color(if e.id == selected { color } else { style::LINE })
.with_width(if e.id == selected { 2.6 } else { 1.2 }),
);
let mut stroke = Stroke::default()
.with_color(if e.id == selected { color } else { style::LINE })
.with_width(if e.id == selected { 2.6 } else { 1.2 });
if n.allocation_summary {
stroke.line_dash = canvas::LineDash {
segments: &[6., 4.],
offset: 0,
};
}
f.stroke(&path, stroke);
f.fill_rectangle(
Point::new(origin.x + 14., origin.y + 15.),
Size::new(4., 12.),
color,
);
f.fill_text(Text {
content: format!(
"{}{}",
e.kind().label().to_uppercase(),
if n.collapsed { " +" } else { "" }
),
content: node_header(e.kind(), n),
position: Point::new(origin.x + 25., origin.y + 14.),
color,
size: 11.into(),
@@ -447,19 +520,32 @@ fn paint_node(f: &mut Frame, p: &Project, n: &ProjectedNode, selected: Id, conta
position: Point::new(origin.x + 14., origin.y + 35.),
color: style::INK,
size: 16.into(),
font: iced::Font {
style: if n.allocation_summary {
iced::font::Style::Italic
} else {
iced::font::Style::Normal
},
..iced::Font::DEFAULT
},
max_width: f32::INFINITY,
..Default::default()
});
if !container && size.height >= 86. {
f.fill_text(Text {
content: format!("@{}", short(&e.alias, max_chars + 6)),
position: Point::new(origin.x + 14., origin.y + 66.),
color: style::MUTED,
size: 10.5.into(),
max_width: f32::INFINITY,
..Default::default()
});
}
}
fn node_header(kind: Kind, node: &ProjectedNode) -> String {
let header = if node.allocation_summary {
format!(
"{} · SUMMARY",
if kind == Kind::Function {
"FUNCTION"
} else {
"ACTIVITY"
}
)
} else {
kind.label().to_uppercase()
};
format!("{header}{}", if node.collapsed { " +" } else { "" })
}
fn highlighted_exchanges(p: &Project, selected: Id) -> BTreeSet<Id> {
p.elements
@@ -489,6 +575,18 @@ fn edge_endpoints(view: &ProjectedView, edge: &ProjectedEdge) -> Option<(Point,
port_point(edge.target_port).unwrap_or(end),
))
}
fn relationship_stereotype(p: &Project, id: Id) -> Option<&'static str> {
match p.relations.get(&id)?.kind() {
RelationKind::CapabilityInclude => Some("«include»"),
RelationKind::CapabilityExtend => Some("«extend»"),
RelationKind::CapabilityGeneralization => Some("specializes"),
RelationKind::MissionCapability => Some("exploits"),
RelationKind::MissionInvolvement | RelationKind::CapabilityInvolvement => {
Some("participates")
}
_ => None,
}
}
fn endpoints(a: &ProjectedNode, b: &ProjectedNode) -> (Point, Point) {
let ax = a.position.x + a.size.width / 2.;
let ay = a.position.y + a.size.height / 2.;
@@ -594,8 +692,13 @@ pub fn export_svg(p: &Project, diagram: Id) -> String {
let color = hex(style::kind_color(e.kind()));
let x = n.position.x;
let y = n.position.y;
let (border, name_style) = if n.allocation_summary {
(" stroke-dasharray=\"6 4\"", " font-style=\"italic\"")
} else {
("", "")
};
format!(
"<g><rect x=\"{x}\" y=\"{y}\" width=\"{}\" height=\"{}\" rx=\"10\" fill=\"{}\" stroke=\"{color}\"/><text x=\"{}\" y=\"{}\" font-family=\"sans-serif\" font-size=\"11\" fill=\"{color}\">{}</text><text x=\"{}\" y=\"{}\" font-family=\"sans-serif\" font-size=\"16\" fill=\"#223044\">{}</text></g>",
"<g><rect x=\"{x}\" y=\"{y}\" width=\"{}\" height=\"{}\" rx=\"10\" fill=\"{}\" stroke=\"{color}\"{border}/><text x=\"{}\" y=\"{}\" font-family=\"sans-serif\" font-size=\"11\" fill=\"{color}\">{}</text><text x=\"{}\" y=\"{}\" font-family=\"sans-serif\" font-size=\"16\" fill=\"#223044\"{name_style}>{}</text></g>",
n.size.width,
n.size.height,
if containers.contains(&n.id) {
@@ -605,7 +708,7 @@ pub fn export_svg(p: &Project, diagram: Id) -> String {
},
x + 14.,
y + 25.,
escape(e.kind().label()),
escape(&node_header(e.kind(), n)),
x + 14.,
y + 51.,
escape(&short(
@@ -622,7 +725,27 @@ pub fn export_svg(p: &Project, diagram: Id) -> String {
for edge in &view.edges {
if let Some((a, b)) = edge_endpoints(&view, edge) {
let mid = (a.x + b.x) / 2.;
svg.push_str(&format!("<path d=\"M {} {} C {mid} {}, {mid} {}, {} {}\" fill=\"none\" stroke=\"#8999a9\" stroke-width=\"1.5\" marker-end=\"url(#arrow)\"><title>{}</title></path>",a.x,a.y,a.y,b.y,b.x,b.y,escape(p.named(edge.id))));
let stereotype = relationship_stereotype(p, edge.id);
let dashed = if matches!(stereotype, Some("«include»" | "«extend»")) {
" stroke-dasharray=\"6 4\""
} else {
""
};
svg.push_str(&format!("<path d=\"M {} {} C {mid} {}, {mid} {}, {} {}\" fill=\"none\" stroke=\"#8999a9\" stroke-width=\"1.5\"{dashed} marker-end=\"url(#arrow)\"><title>{}</title></path>",a.x,a.y,a.y,b.y,b.x,b.y,escape(p.named(edge.id))));
if stereotype.is_some()
|| matches!(
p.relations.get(&edge.id).map(|r| r.kind()),
Some(
RelationKind::Interaction
| RelationKind::FunctionalExchange
| RelationKind::BoundaryExchange
)
)
{
let label = stereotype.unwrap_or_else(|| p.named(edge.id));
let center = curve_point(a, b, 0.5);
svg.push_str(&format!("<rect x=\"{}\" y=\"{}\" width=\"224\" height=\"24\" fill=\"white\"/><text x=\"{}\" y=\"{}\" text-anchor=\"middle\" font-family=\"sans-serif\" font-size=\"13\" fill=\"#637487\">{}</text>",center.x-112.,center.y-30.,center.x,center.y-8.,escape(&short(label,32))));
}
}
}
for n in &view.nodes {
@@ -632,6 +755,9 @@ pub fn export_svg(p: &Project, diagram: Id) -> String {
}
for port in &view.ports {
svg.push_str(&format!("<g><title>{}</title><rect x=\"{}\" y=\"{}\" width=\"14\" height=\"14\" fill=\"white\" stroke=\"#147d7b\"/><path d=\"M {} {} h 8 l -3 -3 m 3 3 l -3 3\" fill=\"none\" stroke=\"#147d7b\"/></g>",escape(p.named(port.element)),port.position.x-7.,port.position.y-7.,port.position.x-4.,port.position.y));
if port.direction == PortDirection::InOut {
svg.push_str(&format!("<path d=\"M {} {} l -3 -3 m 3 3 l -3 3\" transform=\"rotate(180 {} {})\" fill=\"none\" stroke=\"#147d7b\"/>",port.position.x+4.,port.position.y,port.position.x,port.position.y));
}
}
svg.push_str("</svg>");
svg
@@ -641,6 +767,136 @@ pub fn export_svg(p: &Project, diagram: Id) -> String {
mod tests {
use super::*;
#[test]
fn svg_distinguishes_inferred_work_allocation_from_direct_allocation() {
for (layer, performer_kind, work_kind, header) in [
(
Layer::Oa,
Kind::Entity,
Kind::Activity,
"ACTIVITY · SUMMARY",
),
(
Layer::Sa,
Kind::System,
Kind::Function,
"FUNCTION · SUMMARY",
),
] {
let mut p = Project::empty();
let owner = p
.add_element(performer_kind, layer, "Performer", None)
.unwrap();
let parent = p
.add_element(work_kind, layer, "Grouped work", None)
.unwrap();
let child = p
.add_element(work_kind, layer, "Allocated leaf", Some(parent))
.unwrap();
p.relate(RelationKind::Allocation, "Performs", owner, child);
let diagram = p.ensure_diagram(layer);
p.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Allocation;
for id in [owner, parent, child] {
p.place(diagram, id, Position { x: 60., y: 60. });
}
let p = execute_batch(&p, &[Command::Layout(diagram)]).unwrap();
let svg = export_svg(&p, diagram);
let group = svg
.split("<g>")
.find(|g| g.contains(">Grouped work</text>"))
.unwrap();
assert!(group.contains(&format!(">{header}</text>")));
assert!(group.contains("stroke-dasharray=\"6 4\""));
assert!(group.contains("font-style=\"italic\""));
let leaf = svg
.split("<g>")
.find(|g| g.contains(">Allocated leaf</text>"))
.unwrap();
assert!(!leaf.contains("SUMMARY"));
assert!(!leaf.contains("stroke-dasharray"));
assert!(!leaf.contains("font-style=\"italic\""));
assert!(
!p.relations
.values()
.any(|r| r.kind() == RelationKind::Allocation && r.target() == parent)
);
}
}
#[test]
fn dragging_a_child_previews_the_same_enclosure_and_ports_as_commit() {
let p = Project::demo();
let diagram = p
.diagrams
.values()
.find(|d| d.layer == Layer::Sa && d.projection == ProjectionKind::Allocation)
.unwrap()
.id;
let view = project_view(&p, diagram);
let child = view
.nodes
.iter()
.find(|n| n.visual_parent.is_some())
.unwrap()
.id;
let original = p.diagrams[&diagram].nodes[&child].position;
let target = Position {
x: original.x - 400.,
y: original.y - 200.,
};
let canvas = DiagramCanvas {
project: &p,
diagram,
selected: p.id,
zoom: 1.,
pan: Vector::ZERO,
};
let state = State {
drag: Some((child, Point::ORIGIN, original)),
preview: Some(target),
..State::default()
};
let preview = canvas.preview_view(&state);
let committed = execute_batch(
&p,
&[Command::MoveNode {
diagram,
node: child,
position: target,
with_descendants: true,
}],
)
.unwrap();
assert_eq!(preview, project_view(&committed, diagram));
}
#[test]
fn svg_preserves_visible_exchange_labels_and_capability_stereotypes() {
let mut p = Project::demo();
let diagram = p
.diagrams
.values()
.find(|d| d.layer == Layer::Sa && d.projection == ProjectionKind::Allocation)
.unwrap()
.id;
let view = project_view(&p, diagram);
let svg = export_svg(&p, diagram);
for edge in &view.edges {
assert!(svg.contains(&format!(">{}</text>", escape(&short(p.named(edge.id), 32)))));
}
let a = p
.add_element(Kind::Capability, Layer::Oa, "Primary", None)
.unwrap();
let b = p
.add_element(Kind::Capability, Layer::Oa, "Included", None)
.unwrap();
let d = p.ensure_diagram(Layer::Oa);
p.diagrams.get_mut(&d).unwrap().projection = ProjectionKind::Capabilities;
p.place(d, a, Position { x: 0., y: 0. });
p.place(d, b, Position { x: 400., y: 0. });
p.relate(RelationKind::CapabilityInclude, "Uses", a, b);
let svg = export_svg(&p, d);
assert!(svg.contains("«include»</text>"));
assert!(svg.contains("stroke-dasharray=\"6 4\""));
}
#[test]
fn implicit_port_click_selects_semantics_and_drag_preview_svg_share_anchors() {
let p = Project::demo();
let diagram = p
+7 -3
View File
@@ -173,7 +173,7 @@ fn article(topic: Topic) -> Article {
"Create an operational context",
&[
"Choose New in the top bar. Use Project properties to name the model and describe its purpose. Apply changes commits the property draft.",
"On Architecture, choose Create architecture view. The initial Allocation projection shows work inside its performer.",
"On Architecture, choose Create architecture view. Architecture · allocated work shows work inside its performer.",
"Choose Entity under Create element, then + At root. Name it Rescue coordinator and Apply changes. Use Actor for an external participant such as a Vessel in distress.",
"Select Rescue coordinator, choose Activity, then + Allocated. Name the new activity Assess incident. It appears inside its performer; an Allocation relationship records that responsibility.",
"Add a second activity for another participant. Select an activity, choose + Relationship, select Interaction and choose the compatible target. Name the exchange after what is conveyed.",
@@ -239,7 +239,7 @@ fn article(topic: Topic) -> Article {
"Choose the Operational or System perspective. Create participants using Entity/Actor in OA or System/System actor in SA.",
"Create Activity in OA or Function in SA. Select existing work and use + In selected to decompose it into smaller work units.",
"To create work for a participant, select the participant, choose Activity or Function, then + Allocated. To allocate existing work, use + Relationship with Allocation from the participant to the leaf work.",
"Open the Allocation projection to review responsibilities. Open Decomposition to review the activity/function hierarchy independently.",
"Choose Architecture · allocated work to review responsibilities. Choose Functional dataflow to review the activity/function hierarchy and its exchanges independently.",
],
),
explain(
@@ -274,7 +274,7 @@ fn article(topic: Topic) -> Article {
"Choose and edit the relationship you mean",
&[
"Use the diagram selector or + View to work with a focused diagram. View properties edits its name, perspective and projection.",
"Choose Decomposition to nest semantic children, or Allocation to nest work under its performer. Overview keeps a broader view; OA → SA displays counterpart traces.",
"Choose Containment for semantic children, Architecture · allocated work for responsibilities, Functional dataflow for work and exchanges, or Missions & capabilities for outcomes. All relationships is a broader exploration view; OA ↔ SA traceability displays counterparts.",
"Select an element and edit Semantic parent to change ownership. Use an Allocation relationship to change responsibility. Apply changes validates the semantic change.",
"Drag nodes to arrange the current view, scroll to zoom and use Fit to frame its contents. Arrange changes diagram geometry. Export SVG produces a shareable view.",
"Select a diagram node to access Placement, Fold / unfold and Hide. Hide removes its view placement; Delete from model removes model records and requires review of the affected references.",
@@ -284,6 +284,10 @@ fn article(topic: Topic) -> Article {
"Keep diagrams readable",
"Use separate views for responsibility, decomposition and cross-layer coverage. Place existing elements with Show in this diagram. A placement's Diagram field can relocate it while preserving its identity and artifact references. Do not duplicate a semantic element merely to show it in another view.",
),
explain(
"Read allocation summaries and ports",
"A dashed parent activity/function with an italic name and SUMMARY heading groups leaves assigned to the same performer. It has no direct allocation of its own; its inspector explains the leaf performer. Small ports belong to the boundary of their visible owner and are selectable model elements. Folding a container hides its inner work and ports; summary exchange lines do not invent new ports on the container.",
),
explain(
"What nesting does not imply",
"Containment in an allocation projection expresses a performer relationship. It is not a physical deployment statement. Physical deployment is a different architectural concern; do not infer hardware installation from an OA/SA box layout.",
+296
View File
@@ -936,3 +936,299 @@ fn behavior_and_data_authoring_do_not_place_nodes_in_unrelated_architecture_view
}
}
}
#[test]
fn revealing_an_implicit_function_port_unfolds_its_visible_architecture_owner() {
let mut p = Project::demo();
let diagram = p
.diagrams
.values()
.find(|d| d.layer == Layer::Sa && d.projection == ProjectionKind::Allocation)
.unwrap()
.id;
let view = oa_core::projection::project_view(&p, diagram);
let function = view
.nodes
.iter()
.find(|n| p.elements[&n.element].kind() == Kind::Function && n.visual_parent.is_some())
.unwrap();
let ancestor = function.visual_parent.unwrap();
let port = p
.children(function.element)
.find(|e| e.kind() == Kind::Port)
.unwrap()
.id;
p.diagrams
.get_mut(&diagram)
.unwrap()
.nodes
.get_mut(&ancestor)
.unwrap()
.collapsed = true;
assert!(crate::navigation::diagrams_containing(&p, port).contains(&diagram));
let mut app = App::with_project(p);
send(&mut app, Message::RevealInDiagram(diagram, port));
assert_eq!(app.selected, port);
assert_eq!(app.active_diagram, Some(diagram));
assert!(!app.project().diagrams[&diagram].nodes[&ancestor].collapsed);
assert!(
oa_core::projection::project_view(app.project(), diagram)
.ports
.iter()
.any(|p| p.element == port)
);
}
#[test]
fn creation_from_capabilities_routes_work_to_architecture_and_folded_allocation_inserts_below_existing_work()
{
let p = Project::demo();
let system = p
.elements
.values()
.find(|e| e.kind() == Kind::System)
.unwrap()
.id;
let capabilities = p
.diagrams
.values()
.find(|d| d.layer == Layer::Sa && d.projection == ProjectionKind::Capabilities)
.unwrap()
.id;
let mut app = App::with_project(p);
send(&mut app, Message::Layer(Layer::Sa));
send(
&mut app,
Message::Diagram(crate::app::Choice {
id: capabilities,
label: String::new(),
}),
);
send(&mut app, Message::CreateKind(Kind::Function));
send(&mut app, Message::Add(false));
let diagram = app.active_diagram.unwrap();
assert_eq!(
app.project().diagrams[&diagram].projection,
ProjectionKind::Allocation
);
assert!(
app.project().diagrams[&diagram]
.nodes
.values()
.any(|n| n.element == app.selected)
);
send(&mut app, Message::Select(system));
let owner = app.selected_node.unwrap();
send(&mut app, Message::ToggleCollapse);
assert!(app.project().diagrams[&diagram].nodes[&owner].collapsed);
assert_eq!(
app.selected, system,
"Folding must retain semantic selection"
);
let before = app.project().clone();
send(&mut app, Message::Add(true));
let created = app.selected;
let view = oa_core::projection::project_view(app.project(), diagram);
let child = view
.nodes
.iter()
.find(|n| n.element == created)
.unwrap_or_else(|| panic!("Created work is not visible: {}", app.status));
assert_eq!(child.visual_parent, Some(owner));
assert!(!app.project().diagrams[&diagram].nodes[&owner].collapsed);
assert!(
view.nodes
.iter()
.filter(|n| n.visual_parent == Some(owner) && n.id != child.id)
.all(|n| n.position.y + n.size.height <= child.position.y)
);
let parent = view.nodes.iter().find(|n| n.id == owner).unwrap();
assert!(
view.nodes
.iter()
.filter(|n| n.visual_parent.is_none() && n.id != owner)
.all(|n| n.position.x + n.size.width <= parent.position.x
|| parent.position.x + parent.size.width <= n.position.x
|| n.position.y + n.size.height <= parent.position.y
|| parent.position.y + parent.size.height <= n.position.y)
);
send(&mut app, Message::Undo);
assert_eq!(app.project(), &before);
}
#[test]
fn nested_allocation_insertion_reflows_sibling_subtrees_and_preserves_fold_state() {
use oa_core::projection::{project_diagram, project_view};
for folded in [false, true] {
let mut p = Project::empty();
let root = p
.add_element(Kind::Entity, Layer::Oa, "Root", None)
.unwrap();
let a = p
.add_element(Kind::Entity, Layer::Oa, "A", Some(root))
.unwrap();
let b = p
.add_element(Kind::Entity, Layer::Oa, "B", Some(root))
.unwrap();
let other = p
.add_element(Kind::Entity, Layer::Oa, "Other root", None)
.unwrap();
let a_work = p
.add_element(Kind::Activity, Layer::Oa, "A work", None)
.unwrap();
let b_work = p
.add_element(Kind::Activity, Layer::Oa, "B work", None)
.unwrap();
let b_port = p
.add_element(Kind::Port, Layer::Oa, "B boundary", Some(b))
.unwrap();
p.relate(RelationKind::Allocation, "A performs", a, a_work);
p.relate(RelationKind::Allocation, "B performs", b, b_work);
let diagram = p.ensure_diagram(Layer::Oa);
p.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Allocation;
for id in [root, a, b, other, a_work, b_work] {
p.place(diagram, id, Position { x: 60., y: 60. });
}
let mut p = execute_batch(&p, &[Command::Layout(diagram)]).unwrap();
let view = project_view(&p, diagram);
let root_node = view.nodes.iter().find(|n| n.element == root).unwrap();
let a_node = view.nodes.iter().find(|n| n.element == a).unwrap();
let b_node = view.nodes.iter().find(|n| n.element == b).unwrap();
// Layout's UUID order is intentionally irrelevant to this scenario:
// explicitly put A before B, as a user can arrange these participants.
p = execute_batch(
&p,
&[
Command::MoveNode {
diagram,
node: a_node.id,
position: Position {
x: root_node.position.x + 24.,
y: root_node.position.y + 64.,
},
with_descendants: true,
},
Command::MoveNode {
diagram,
node: b_node.id,
position: Position {
x: root_node.position.x + 24.,
y: root_node.position.y + 64. + a_node.size.height + 16.,
},
with_descendants: true,
},
],
)
.unwrap();
let view = project_view(&p, diagram);
let root_node = view.nodes.iter().find(|n| n.element == root).unwrap();
let other_node = view.nodes.iter().find(|n| n.element == other).unwrap().id;
let b_node = view.nodes.iter().find(|n| n.element == b).unwrap().id;
let b_work_node = view.nodes.iter().find(|n| n.element == b_work).unwrap().id;
p.diagrams
.get_mut(&diagram)
.unwrap()
.nodes
.get_mut(&other_node)
.unwrap()
.position = Position {
x: root_node.position.x,
y: root_node.position.y + root_node.size.height + 24.,
};
p.diagrams
.get_mut(&diagram)
.unwrap()
.nodes
.get_mut(&b_node)
.unwrap()
.collapsed = folded;
// Deliberately overlapping unrelated roots must not be rearranged.
let mut unrelated = Vec::new();
for name in ["Unrelated one", "Unrelated two"] {
let id = p.add_element(Kind::Entity, Layer::Oa, name, None).unwrap();
unrelated.push(p.place(diagram, id, Position { x: 2000., y: 1000. }));
}
let before = p.clone();
let mut expanded = p.diagrams[&diagram].clone();
expanded.nodes.get_mut(&b_node).unwrap().collapsed = false;
let before_view = project_diagram(&p, &expanded);
let before_port = before_view
.ports
.iter()
.find(|p| p.element == b_port)
.unwrap()
.position;
let mut app = App::with_project(p);
send(&mut app, Message::Select(a));
send(&mut app, Message::CreateKind(Kind::Activity));
send(&mut app, Message::Add(true));
let created = app.selected;
assert_eq!(
app.project().elements[&created].kind(),
Kind::Activity,
"{}",
app.status
);
let after = &app.project().diagrams[&diagram];
assert_eq!(after.nodes[&b_node].collapsed, folded);
let dy =
after.nodes[&b_node].position.y - before.diagrams[&diagram].nodes[&b_node].position.y;
assert!(dy > 0., "B must move below the enlarged A");
assert_eq!(
after.nodes[&b_work_node].position.y,
before.diagrams[&diagram].nodes[&b_work_node].position.y + dy
);
assert!(
after.nodes[&other_node].position.y
> before.diagrams[&diagram].nodes[&other_node].position.y
);
for id in unrelated {
assert_eq!(after.nodes[&id], before.diagrams[&diagram].nodes[&id]);
}
let mut expanded = after.clone();
expanded.nodes.get_mut(&b_node).unwrap().collapsed = false;
let view = project_diagram(app.project(), &expanded);
let after_port = view
.ports
.iter()
.find(|p| p.element == b_port)
.unwrap()
.position;
assert_eq!(
after_port,
Position {
x: before_port.x,
y: before_port.y + dy
}
);
for (left, right) in [(a, b), (root, other), (a_work, created)] {
let left = view.nodes.iter().find(|n| n.element == left).unwrap();
let right = view.nodes.iter().find(|n| n.element == right).unwrap();
assert!(left.position.y + left.size.height <= right.position.y);
}
assert!(crate::diagram::insertion_adjustments(app.project(), diagram, created).is_empty());
send(&mut app, Message::Undo);
assert_eq!(
app.project(),
&before,
"Creation and all reflow must undo together"
);
if folded {
send(&mut app, Message::Select(root));
send(&mut app, Message::CreateKind(Kind::Entity));
send(&mut app, Message::Add(true));
let mut expanded = app.project().diagrams[&diagram].clone();
expanded.nodes.get_mut(&b_node).unwrap().collapsed = false;
let view = project_diagram(app.project(), &expanded);
let b = view.nodes.iter().find(|n| n.id == b_node).unwrap();
let created = view
.nodes
.iter()
.find(|n| n.element == app.selected)
.unwrap();
assert!(b.position.y + b.size.height <= created.position.y);
assert!(app.project().diagrams[&diagram].nodes[&b_node].collapsed);
}
}
}
+17
View File
@@ -599,6 +599,23 @@ fn inspector(app: &App) -> View<'_, Message> {
.width(Fill),
);
}
if !p
.relations
.values()
.any(|r| r.kind() == RelationKind::Allocation && r.target() == e.id)
&& let Some(performer) = oa_core::validation::allocation_summary(p, e.id)
{
body = body.push(label(
"Allocation summary: all leaf activities or functions belong to the same performer. The parent has no direct allocation.",
));
body = body.push(
action(
&format!("Leaf performer: {}", short(p.named(performer), 24)),
Message::Select(performer),
)
.width(Fill),
);
}
if e.kind() == Kind::Port {
body = body.push(label(
"Shown automatically on the boundary of its visible owner.",
File diff suppressed because it is too large Load Diff
+1113 -972
View File
File diff suppressed because it is too large Load Diff
+43 -38
View File
@@ -1,50 +1,55 @@
# First release state — R4 authoring / native iteration / R5 integration
# First release state — hierarchy and port correction
## Objective and baseline
Active goal: feature-complete OA + SA/Architecture first release, beyond V2; objective attachment81e84f53-2b51-4151-9e34-59414bda8b2d. User demands closer original Capella study and practical usability. Later layers remain future work. V2 source ZIP and dist packages preserved; do not mark release complete. PLAN.md is current milestone/acceptance source.
## Objective and evidence
## Verified implementation
- R2 missions/capabilities, R1 process topology and R4 declared scenarios/message semantics implemented in shared core; exact handoffs release-capability-core.md, release-topology-core.md, release-scenarios-core.md.
- Fresh independent release-semantic-review.md findings CLOSED: branch-local duplicate replies/use-after-delete, Role compatibility, loop repetition diagnostics, reverse Reply picker and state-owner picker. Independent27 R1/R2/R4 tests + compiled external repro passed. Core worker71 focused regressions + strict Clippy passed before R5. Loop warnings distinguish uncertainty from execution claims.
- R1 contextual tools now work from empty and legacy Process/Chain; preserve legacy linear interpretation and perform explained atomic path conversion. Root added measured Fit/zoom and scaled hit testing after native excessive-scroll finding; empty process now has guidance. Latest source not native reverified yet.
- R4 dedicated scenario.rs authoring supports typed lifelines, all message styles, paired Call+Reply, explicit bound ScenarioUse, fragments/operand destinations/actions/state observations, carried values. Core helpers drive endpoint/viewpoint compatibility. Worker six scenario tests; root56 full desktop tests passed session50129 before source-find addition.
- Root R4 property forms include typed message/payload/reuse/binding fields and new constructors. Shared core reply/role helpers; distinct state-owner/effective-performer rule restored after review and tested. Modeling actions picklist near top of capability/mission properties; Connections inspector traverses real core references and diagram placements.
- Source lint runs bounded max1worker/latestversion, caches accepted model checks, rejects stale diagnostics across reset. Source navigation uses layout sensor; root further added focus, declaration context and literal Find/Go to line (CtrlF/CtrlG/F3/ShiftF3), UTF-8 byte cursor offsets, invalid-draft preservation. Latest59 desktop test run session22577 currently building after tests; inspect result before claiming complete.
Active first-release goal: complete OA + SA/System Architecture, Rust/Iced Windows/Linux, equivalent graphical/source editing, reST contract, traceability, REST and built-in guidance. Objective attachment81e84f53-2b51-4151-9e34-59414bda8b2d. V2 is the preserved baseline, not release acceptance. PLAN.md and release-semantics-audit.md retain the full scope.
## Current native verification
Root exclusively owns Computer Use. Sky persistent bindings sky/studioWindow/studioState/studioDialog/dialogState. Refresh inventory before next action. CUA through node_repl @oai/sky skill26.901.41600, not disabledcua_repl. Native file dialogs sometimes return tiny16px screenshot first; use screenshot width>100 for coordinates. One observed action then fresh capture; typing after focus observation.
User prioritizes Capella-like hierarchical architecture and correct ports. release-diagram-port-correction.md records the correction; release-port-contract.md is being finalized before implementation. No release completion claim.
Verified checkpoint executable: D:/CodexBuilds/Capella2-release1-01a06e2e/ui-r1-r4-checkpoint/arcadia-studio.exe SHA256B24611C9AA7BF686BDA3FDD0B25BB0CE71715D5FED471287ECBAE2B0BF385F98; window395472. This predates scenario authoring, topology zoom and Find. AltF4 requested after SaveAs, fresh inventory needed to confirm closure. stderr empty and process responsive during run.
- Native1440x900 and1100x700: guide/search, persistent task/explorer navigation; selected capability correctly identifies placement; Modeling actions menu appears near top. SourceForSelection now reveals correct declaration, but it lands near viewport bottom without focus; root further focus/context patch pending native verification.
- Help→resize→Open saved capability model now automatically fits47% with all nodes visible: original cache/layout Fit bug fixed and native reverified.
- Connections outgoing extension→target capability→incoming capability/chain relationships works. Right panel retains connection tab during creation, hiding name fields; root now switches new element to Properties and added EditProperties(id) message for explicit property actions. Pending native check.
- Created empty OA Process Choose rescue response, involved Assess incident through +Work, used Branch/parallel to create Report distress / Respond to distress alternatives after it, condition Incident needs more information + else. Observed5newSequenceLinks,4branchnodes, rendered guard/alternative routes. At minimum width excessively horizontal graph; root now adds Fit/zoom (not yet native checked). Branch form scroll works and commit is atomic via same Command path.
- Saved full result through native SaveAs to ui-r1-r4-checkpoint/process-branch-ui.arcadia. On-disk source verified process name, exact guard and5SequenceLinks. No original fixture overwritten.
- Earlier R2 native extension fixture ui-r2-checkpoint/capability-extension-ui.arcadia preserved: Rescue in heavy weather extends Protect life at sea, condition Sea state is 6 or higher; exact source verified. Prior app closed.
- Original Capella six native benchmark workflows recorded in release-capella-workflow-benchmark.md. Original window524804 disappeared from fresh inventory this turn; do not assume open. Disposable workspace/sample under D:/CodexBuilds/Capella2-release1-01a06e2e; preserve it. Reopen if more comparisons needed.
## Implemented and verified
## Active ownership
- Root: desktop app/forms/views/workflow/navigation/guide/source_navigation, topology.rs (released by worker after wrapper edits), state docs, exclusive CUA. Next: finish current59test/build result, copy new checkpoint, native Fit/zoom/sourceFind/scenarioauthoring/save-reopen; strict desktop Clippy. Root basicR5forms enums/refs/Event constructors already added; ergonomic state workflow still needed.
- release_capella_benchmark: ONLY behavior.rs + behavior/scenario.rs and benchmark docs. R4 authoring code complete, final focused checks/report then release. Root owns topology now. Shared behaviorEvent::is_submit guards both editors; root integrated. Event routing explicitly tags scenario commit so viewport events cannot redirect it.
- release_capability_core: R5 core model/validation/private state_validation.rs/references/session/transitions/projection/demo/coretests(exceptlanguage). Exact adopted contract release-state-contract.md: Event Signal/Time/Change, After/At, External/Local/Internal, typed trigger/effect refs,9newpseudostates, is_stable_state. Events/refs and state module compile; hierarchy/cardinality/focused tests underway. Source factory defaults preserved; Project::relate/Command::Connect use Local from EntryPoint. No desktop ownership.
- release_text_service_fix: language/persistence/tests/server/API/OpenAPI/language.rst. R5 source/API landed; previously16language+17HTTP/config+1unit passed, reST warning-free. Await core finalfreeze for final focused/strict checks; may read-only draft R3contract while waiting. R6 read-only report done: release-data-contract.md; not implemented.
- release_semantics_review: idle, focused review closed. Reuse after R5 / current graphical changes for fresh review.
- R1 explicit process/chain occurrences and guarded control topology; R2 missions/capabilities; R4 typed scenarios/messages/reuse; R5 state/event semantics, nested regions and pseudostates. Exact contracts/reviews are in the corresponding release-*.md reports. R1/R2/R4 independent semantic findings closed before this checkpoint.
- Desktop contextual scenario/state/process authoring; typed property adapter; searchable offline guide; asynchronous bounded source lint with stale-result rejection; UTF-8 Find/Go to line. State authoring tests pass, but R4/R5 complete native authoring acceptance remains pending.
- Architecture, functional-dataflow and capability projections separated. Demo now has six focused views. Shared projection preserves allocation/containment, calculates owner-bound selectable ports, and feeds canvas, Fit, SVG and movement. Generic port metamodel is still pending correction.
- Git initialized on codex/release1-engineering; source baseline commit93ba83d. No remote publication. User authorizes worktrees. Current post-review changes are uncommitted.
- Baseline full locked core/desktop/server tests passed (desktop64; server1unit+17HTTP); strict workspace Clippy passed at baseline. Post-review core4 projection regressions pass, including missing work-parent fallback and direct-versus-inferred allocation distinction.
## Required remaining work
R5 ergonomic state/event authoring/rendering plus finalcore/source verification; R6 data/contracts per design (lead still must freeze decisions); R3 fullsemantic drift and mapped existing-counterpart review; categories/persistentfilters/coverage; home/recent/recovery; optional hosteddesktop ETagopen/publish/save; explicit schema3 losslessV2migration; workedexamples and currentdocs; independent finalrequirements/correctness/UI/simplicity reviews; Windows/Linux fullchecks/nativeflows/packages. No V2 exclusion substitutes for release acceptance.
## Current changes and ownership
## Environment
Windows Rust1.98.1, target D:/CodexBuilds/Capella2-01a06e2e. Build cargo build --locked -p oa-desktop; tests cargo test --locked -p oa-desktop. Copy checkpoint exe before native launch so further builds can link. WSLUbuntu24.04 config per verification-v2-linux.md. Git unborn/untracked preserve. No disk blocker. Prior target/debug deletion rejected: do not retry/bypass. FinalMSRV/platform/clean-workflow checks remain.
- Root owns core projection/session/regressions, desktop views.rs, docs, integration and native CUA. Fixes: omitted work parent falls back to actual performer; port reveal unfolds owner ancestry; component-port footer and larger root corridors; inferred allocation summary metadata/inspector explanation.
- hierarchy_port_review owns ONLY desktop app.rs, diagram.rs, tests.rs for insertion/reflow fixes and canvas/SVG summary distinction. Earlier full desktop run67/68 failed: folding selects placement ID instead of semantic owner, causing subsequent child creation rejection. Worker fixes root cause and nested sibling reflow; do not claim current tests all pass. Other reviewed fixes: projected child drag preview, creation routing from capability views, unfolded insertion path, SVG labels/stereotypes.
- port_contract_design owns ONLY release-port-contract.md now; adopted design separates FunctionInput/Output item contracts, Component Standard/Flow and independent orientation, PortAllocation, required FE endpoint IDs, atomic generation/reuse and explicit lossless V2→3 migration. Isolated core implementation worktree follows contract freeze.
- capella_startup_diagnosis read-only complete: healthy original Capella pid14556/window1049956; redundant graphical splash pid7048/window395654 hung in native splash update. No deletion/config workaround indicated.
## Usage-limit interruption recovery
All three workers stopped with explicit account usage-limit errors; no worker is still active. Root owns recovery/integration of their current files. R5 core+server full locked tests passed in root session11632 (exit0). Worker left behavior.rs declaring states module without writing states.rs; root is completing that module before the next build. Native currentcheckpoint disappeared from process/window inventory without stderr output; root relaunched preserved ui-scenario-checkpoint executable, new returned window1050736. No unsaved native work was lost (last work was only reopening saved fixture). Root just verified Find/Enter/F3/CtrlG+line1 with actual selection and minimum1100x700layout; Fit/scenarioauthoring remain next native checks.
## Native verification and current pause
## User correction: diagram hierarchy and ports — current priority
User reports architecture diagrams have become generic graphs and port semantics/rendering differ dubiously from Capella. Lead confirmed projection includes almost every relation between placed objects, Overview flattens every parent, and renderer has no boundary-port anchors. Reopening original Capella to inspect SAB/SDFB/OAB ownership and port workflows before freezing corrective contract. Preserve completed state editor work; state native verification is pending, not release complete. Current desktop:59 prior full regressions +4 new state authoring tests pass, cargo check passes after recovery; strict Clippy/current full63 tests not yet run. New state_scene.rs gives actual nested geometry, internal transition rows and vector pseudostate symbols. No fresh agents available: three existing agents still errored on account usage limits; root continues.
Computer Use through node_repl @oai/sky, skill26.901.41600. Root exclusively owns input. Most recent attempt reported physical Escape stop before inventory; CUA paused, no further native input without user continuation. Code work can proceed. Existing persistent handles must be revalidated on resume.
## Git adoption and hierarchy checkpoint
User explicitly authorized Git and optional worktrees. Existing .git was unborn; created codex/release1-engineering, added repository ignore/line-ending rules, and preparing the first source checkpoint. Future isolated implementation can use worktrees after this commit; read-only reviews do not need them. Nothing pushed or published.
Studio checkpoint D:/CodexBuilds/Capella2-release1-01a06e2e/ui-hierarchy-checkpoint/arcadia-studio.exe, SHA256 D6055F659754E9159232483EAF9FA595C1D7F53683F8FB618D855F04B9C931A5, last window723086,1440×900. Personally exercised OA/SA layer changes, nested participants/work, auto function/component port appearance and function-output selection. Found label overlap; footer/corridor/alias fixes are newer than this executable and need native recheck. No unsaved edits in this checkpoint.
Hierarchy correction implementation: dedicated FunctionalDataflow/Capabilities projections, architecture filters participants/work + exchanges, six focused demo views with architecture preferred on open/layer change, calculated boundary ports referenced by FE/BoundaryExchange, shared movement/hit/SVG anchors. Layout measures port room and 64px headers, orders root containers by actual exchange flow. Source/OpenAPI enum support updated. Functional/component port metamodel correction remains required; do not mistake improved rendering for semantic completion.
Healthy original Capella has the disposable In-Flight Entertainment sample open. Root reached System Analysis workflow and expanded System Architecture diagrams; SAB port workflow not yet exercised. Previously observed six workflows are recorded in release-capella-workflow-benchmark.md. Root startup diagnosis now supersedes old unavailable-window notes.
Verification: full locked core+desktop+server tests exit0 root session12111; desktop64 tests, new3 core projection regressions and1 desktop port hit/preview/SVG test. Earlier new group-movement test incorrectly used projected origin as stored MoveNode target; corrected to command's absolute persisted-coordinate contract, and shared layout/header measurement now avoids the native offset mismatch. Strict workspace Clippy passed before projection changes; rerun needed on this checkpoint. Native checkpoint ui-hierarchy-checkpoint SHA256D6055F659754E9159232483EAF9FA595C1D7F53683F8FB618D855F04B9C931A5 built/copied; native launch/verification next. Capella graphical window395654 remains a splash, console launcher14556 unresponsive; no new native port study claimed.
Preserved actual native authoring fixtures: ui-r2-checkpoint/capability-extension-ui.arcadia (conditional extension); ui-r1-r4-checkpoint/process-branch-ui.arcadia (five control links and guarded alternatives), both under the release build directory. Find/Enter/F3/CtrlG and1100×700 layout previously exercised. New process Fit, scenario/state authoring, source focus refinements and latest diagram geometry still require native verification.
## Exact next actions
1. Finish nested insertion/reflow and summary display; run focused then full current tests/Clippy. Regenerate distributable examples with current focused diagrams, preserving historical V2 fixtures. Update docs and commit reviewed hierarchy milestone.
2. Freeze port contract and create isolated Git worktree for core/schema3 implementation. Root integrates typed desktop actions, forms, projection glyphs and API/source parity. Verify migration, deletion/undo, OA↔SA mapping and item/provider roles with independent review.
3. Resume native Capella SAB/port study and Studio default/minimum authoring verification when CUA is resumed; fix from observed evidence.
4. Continue remaining first-release requirements: R6 data, R3 semantic drift/mapped reconciliation, persistent filters/coverage, Home/recent/recovery, optional hosted ETag editing, updated examples/help/reST/API, independent final reviews, Windows/Linux checks and packages.
## Environment and commands
Windows Rust1.98.1, target D:/CodexBuilds/Capella2-01a06e2e. Set CARGO_TARGET_DIR to that path, then cargo test --locked -p oa-core -p oa-desktop -p oa-server; cargo clippy --locked --workspace --all-targets -- -D warnings; cargo build --locked -p oa-desktop. Copy build to a checkpoint path before native launch to avoid linker locks. WSLUbuntu24.04 setup in verification-v2-linux.md; final MSRV/platform verification remains.
Prior recursive target/debug deletion was rejected: do not retry/bypass. No disk blocker. Old agents hit usage limits but current agents work; no usage reset authorized or used. Goal remains active; previous short turn stopped on explicit CUA Escape and made no material implementation progress.
## Current integration checkpoint, 2026-09-05
User resumed CUA explicitly; it is working. Current original Capella window527162 maximized2560x1392. Root opened [SAB] High Level System Overview with some FCs, inspected function output port FOP5 properties (Outgoing Exchange Items, Realized Function Output Ports), created FunctionalExchange38 between Provide Navigation Data and Determine Operating Profiles by palette/source/target. It automatically created output FOP1 and input FIP3; Semantic Browser verifies ownership and actual endpoint identities. Allocation Management had no available component exchange; cancelled. Separate propagation/synchronization commands observed in Modeling Accelerators. Component-port creation/properties is next.
Hierarchy worker released app.rs/diagram.rs/tests.rs after fixing semantic selection lost on folding, nested sibling/ancestor reflow including folded hidden descendants, and canvas/SVG inferred-summary distinction. Root full locked core+desktop+server tests session6598 passed (desktop70, server1unit+17HTTP); strict workspace Clippy and desktop build session38990 passed. Demo source/JSON regenerated from a single model with matching IDs and successful load checks; old V2 fixtures untouched. New maintained core example generate_demo provides reproducible regeneration. Guide and README reflect focused view names/summary semantics.
Latest native checkpoint ready: D:/CodexBuilds/Capella2-release1-01a06e2e/ui-hierarchy-reviewed/arcadia-studio.exe SHA2563603AC957B6E16B4321F7FB7E759B3A2265357679DB8F27DD9ED5642B7E02179. Not launched yet. Old Studio window723086 has dirty marker, so inspect and preserve any unsaved changes before closing (do not infer it is safe from the earlier no-edit state).
Typed-port work isolated at D:/CodexWorktrees/Capella2-typed-ports, branch codex/typed-port-contracts, from c20a207. port_contract_design owns core model/ports/validation/references/session/transitions/demo/projection + non-language tests; capella_startup_diagnosis now owns persistence/migration/language/server/API/reST + source/migration tests. They coordinate shared schema but never edit the same files. Root main remains buildable during their implementation. Temporary model-capacity errors were retried on the same agents; both resumed. Full core port/schema3 acceptance pending.