Compare commits

...
10 changed files with 629 additions and 4 deletions
+43
View File
@@ -22,6 +22,8 @@ pub(crate) mod source_rename;
mod source_symbols;
pub(crate) mod text_workspace;
#[cfg(test)]
mod traceability_navigation_tests;
#[cfg(test)]
mod transition_context_tests;
#[cfg(test)]
mod view_copy_tests;
@@ -359,6 +361,7 @@ pub enum Message {
Page(Page),
Guide(crate::guide::Event),
Traceability(crate::views::traceability::Event),
ReviewLayerTraces,
Behavior(crate::behavior::Event),
Layer(Layer),
Select(Id),
@@ -2310,7 +2313,30 @@ impl App {
"Apply or revert pending edits before using modeling actions.".into();
return Task::none();
}
// Show acts on the diagram captured by the validated menu.
// Ordinary cross-layer selection would switch to that element's
// preferred same-layer view before the placement command runs.
let show_in_mixed_view = matches!(&*action, Message::PlaceSelected)
&& self
.active_diagram
.and_then(|id| self.project().diagrams.get(&id))
.is_some_and(|d| {
d.projection == ProjectionKind::Traceability
&& self
.project()
.elements
.get(&menu.request.object)
.is_some_and(|e| {
e.kind() != Kind::Port && d.projection.includes(e.kind())
})
});
if show_in_mixed_view {
self.selected = menu.request.object;
self.selected_attachment = None;
self.refresh_draft();
} else {
self.select(menu.request.object);
}
self.selected_node = menu.request.placement;
self.selected_bend = menu.request.bend.map(|i| (menu.request.object, i));
return self.update(*action);
@@ -2508,6 +2534,23 @@ impl App {
iced::widget::scrollable::RelativeOffset::START,
);
}
Message::ReviewLayerTraces => {
// Reuse Page navigation so modal, placement and behavior-form
// guards run before changing the transient inventory filter.
let navigation = self.update(Message::Page(Page::Traces));
if self.page == Page::Traces && !self.behavior.has_pending() {
self.traceability.filter = crate::views::traceability::Filter::Traces;
self.traceability.query.clear();
return Task::batch([
navigation,
iced::widget::operation::snap_to(
"traceability-list",
iced::widget::scrollable::RelativeOffset::START,
),
]);
}
return navigation;
}
Message::Search(value) => {
self.search = value;
self.explorer_page = 0;
@@ -301,6 +301,17 @@ impl App {
_ => return Task::none(),
};
let placing = point.is_none() && matches!(intent, Intent::Create { .. });
let filter = if matches!(intent, Intent::Existing { .. })
&& self
.project()
.diagrams
.get(&diagram)
.is_some_and(|d| d.projection == ProjectionKind::Traceability)
{
Filter::AllCompatible
} else {
Filter::Context
};
let saved = Saved {
version: 1,
base: oa_core::transitions::project_fingerprint(self.project()),
@@ -309,7 +320,7 @@ impl App {
point,
placing,
query: String::new(),
filter: Filter::Context,
filter,
show_placed: false,
page: 0,
};
@@ -1,4 +1,5 @@
use super::*;
mod traceability;
fn setup(layer: Layer) -> App {
let mut app = App::with_project(Project::empty());
@@ -0,0 +1,124 @@
use super::*;
#[test]
fn traceability_existing_defaults_show_opposite_layer_and_place_only_one_node() {
let mut project = Project::empty();
let sa = project
.add_element(Kind::Function, Layer::Sa, "Deliver report", None)
.unwrap();
let system = project
.add_element(Kind::System, Layer::Sa, "System", None)
.unwrap();
let diagram = project.ensure_diagram(Layer::Oa);
project.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Traceability;
let mut app = App::with_project(project);
app.page = Page::Diagram;
app.layer = Layer::Oa;
app.active_diagram = Some(diagram);
let before = app.document.snapshot();
send(&mut app, Event::Existing(None));
assert_eq!(
app.architecture.as_ref().unwrap().saved.filter,
Filter::AllCompatible
);
let listed = candidates(&app);
assert!(listed.iter().any(|(id, _, shown)| *id == sa && !shown));
assert!(listed.iter().any(|(id, _, shown)| *id == system && !shown));
send(&mut app, Event::Pick(sa));
assert!(app.architecture.as_ref().unwrap().saved.placing);
assert_eq!(app.project(), &before.decoded.project);
let context = app.route_context().unwrap();
let point = Position { x: 345., y: 215. };
send(&mut app, Event::Click(context, point));
assert!(app.architecture.is_none(), "{}", app.status);
assert_eq!(app.active_diagram, Some(diagram));
let placed = app.project().diagrams[&diagram]
.nodes
.values()
.find(|n| n.element == sa)
.unwrap()
.clone();
assert_eq!(placed.position, point);
let mut expected = before.decoded.project.clone();
expected
.diagrams
.get_mut(&diagram)
.unwrap()
.nodes
.insert(placed.id, placed);
assert_eq!(app.project(), &expected);
let accepted = app.document.bundle().clone();
let _ = app.update(Message::Undo);
assert_eq!(app.document.bundle(), &before.bundle);
assert!(!app.document.can_undo());
let _ = app.update(Message::Redo);
assert_eq!(app.project(), &expected);
assert_eq!(app.document.bundle(), &accepted);
}
#[test]
fn traceability_default_does_not_change_creation_or_ordinary_picker_guards() {
let mut app = setup(Layer::Oa);
let diagram = app.active_diagram.unwrap();
send(&mut app, Event::Existing(None));
assert_eq!(
app.architecture.as_ref().unwrap().saved.filter,
Filter::Context
);
send(&mut app, Event::Cancel);
let _ = app.update(Message::Projection(ProjectionKind::Traceability));
send(&mut app, Event::Start(Kind::Entity, None));
assert_eq!(
app.architecture.as_ref().unwrap().saved.filter,
Filter::Context
);
send(&mut app, Event::Cancel);
assert_eq!(app.active_diagram, Some(diagram));
let mut project = app.project().clone();
let sa = project
.add_element(Kind::Function, Layer::Sa, "Other layer", None)
.unwrap();
let port = Id::new_v4();
project.elements.insert(
port,
oa_core::Element {
id: port,
alias: "legacy_port".into(),
name: "Port".into(),
description: String::new(),
parent: Some(sa),
body: ElementBody::shared_in(
Layer::Sa,
SharedBody::Port {
contract: PortContract::Legacy,
legacy: Some(LegacyPortContract {
direction: PortDirection::InOut,
interface: None,
}),
},
),
},
);
oa_core::validate_candidate(&project).unwrap();
for projection in [ProjectionKind::Overview, ProjectionKind::Traceability] {
let mut p = project.clone();
p.diagrams.get_mut(&diagram).unwrap().projection = projection;
let mut trial = App::with_project(p);
trial.page = Page::Diagram;
trial.active_diagram = Some(diagram);
trial.layer = Layer::Oa;
let before = trial.document.bundle().clone();
send(&mut trial, Event::Existing(None));
send(&mut trial, Event::Filter(Filter::AllCompatible));
assert_eq!(
candidates(&trial).iter().any(|(id, _, _)| *id == sa),
projection == ProjectionKind::Traceability
);
assert!(!candidates(&trial).iter().any(|(id, _, _)| *id == port));
send(&mut trial, Event::Pick(port));
assert!(!trial.architecture.as_ref().unwrap().saved.placing);
send(&mut trial, Event::Cancel);
assert_eq!(trial.document.bundle(), &before);
}
}
@@ -0,0 +1,277 @@
use super::*;
use crate::{context_menu::Entry, model_menu, views::traceability::Filter};
fn send(app: &mut App, message: Message) {
let _ = app.update(message);
}
fn fixture() -> (App, Id, Id, Id, Id) {
let mut p = Project::empty();
let oa = p
.add_element(Kind::Activity, Layer::Oa, "Observe", None)
.unwrap();
let sa = p
.add_element(Kind::Function, Layer::Sa, "Detect", None)
.unwrap();
let mixed = p.ensure_diagram(Layer::Oa);
p.diagrams.get_mut(&mixed).unwrap().projection = ProjectionKind::Traceability;
p.place(mixed, oa, Position { x: 300., y: 200. });
let system = p.ensure_diagram(Layer::Sa);
p.place(system, sa, Position { x: 700., y: 200. });
oa_core::validate_candidate(&p).unwrap();
let mut app = App::with_project(p);
app.page = Page::Diagram;
app.layer = Layer::Oa;
app.active_diagram = Some(mixed);
app.selected = oa;
app.refresh_draft();
app.traceability.filter = Filter::Artifacts;
app.traceability.query = "stale artifact query".into();
(app, mixed, system, oa, sa)
}
fn show_action(app: &mut App, element: Id) -> Message {
send(app, model_menu::open(element, iced::Point::ORIGIN));
model_menu::popup(app)
.unwrap()
.entries
.into_iter()
.find_map(|entry| match entry {
Entry::Action { label, message, .. } if label == "Show in this diagram" => {
Some(message)
}
_ => None,
})
.expect("eligible real menu action")
.expect("eligible menu action must be enabled")
}
#[test]
fn review_layer_traces_resets_inventory_without_applying_property_or_source_drafts() {
for source_draft in [false, true] {
let (mut app, mixed, _, oa, _) = fixture();
if source_draft {
send(&mut app, Message::Page(Page::Text));
send(
&mut app,
Message::SourceEdit(iced::widget::text_editor::Action::Edit(
iced::widget::text_editor::Edit::Insert('x'),
)),
);
assert!(app.source_dirty());
} else {
send(
&mut app,
Message::Field("/alias".into(), "invalid alias".into()),
);
assert!(app.draft_dirty());
}
let raw = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap();
let source = app.source_editor.text();
let before = app.document.snapshot();
let camera = (app.zoom, app.pan);
send(&mut app, Message::ReviewLayerTraces);
assert_eq!(app.page, Page::Traces);
assert_eq!(app.traceability.filter, Filter::Traces);
assert!(app.traceability.query.is_empty());
assert_eq!(
(app.layer, app.active_diagram, app.selected),
(Layer::Oa, Some(mixed), oa)
);
assert_eq!((app.zoom, app.pan), camera);
assert_eq!(
serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(),
raw
);
assert_eq!(app.source_editor.text(), source);
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
assert!(!app.document.can_undo());
}
}
#[test]
fn review_layer_traces_respects_busy_modal_and_active_placement_guards() {
for already_traces in [false, true] {
for guard in 0..5 {
let (mut app, mixed, _, oa, _) = fixture();
match guard {
0 => app.busy = true,
1 => app.delete_pending = true,
2 => app.hosted.visible = true,
3 => send(
&mut app,
Message::Architecture(architecture_authoring::Event::Existing(None)),
),
4 => {
app.page = Page::Behavior;
let p = app.project().clone();
app.behavior
.creation
.update(&p, behavior::creation::Event::Open(Layer::Oa))
.unwrap();
assert!(app.behavior.has_pending());
}
_ => unreachable!(),
}
if already_traces {
app.page = Page::Traces;
}
let original_page = app.page;
let before = app.document.snapshot();
let behavior_before = app.behavior.recovery_snapshot().unwrap();
send(&mut app, Message::ReviewLayerTraces);
assert_eq!(
app.page, original_page,
"guard {guard}, already traces {already_traces}"
);
assert_eq!(app.traceability.filter, Filter::Artifacts);
assert_eq!(app.traceability.query, "stale artifact query");
assert_eq!(
(app.layer, app.active_diagram, app.selected),
(Layer::Oa, Some(mixed), oa)
);
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
assert_eq!(app.behavior.recovery_snapshot().unwrap(), behavior_before);
if guard == 3 {
assert!(app.architecture.is_some());
}
}
}
}
#[test]
fn mixed_view_show_keeps_its_context_for_every_diagram_app_and_target_layer() {
for diagram_layer in [Layer::Oa, Layer::Sa] {
for app_layer in [Layer::Oa, Layer::Sa] {
for target_layer in [Layer::Oa, Layer::Sa] {
let mut p = Project::empty();
let oa = p
.add_element(Kind::Activity, Layer::Oa, "Observe", None)
.unwrap();
let sa = p
.add_element(Kind::Function, Layer::Sa, "Detect", None)
.unwrap();
let target = if target_layer == Layer::Oa { oa } else { sa };
let other = if target_layer == Layer::Oa { sa } else { oa };
let mixed = p.ensure_diagram(diagram_layer);
p.diagrams.get_mut(&mixed).unwrap().projection = ProjectionKind::Traceability;
// An attractive same-layer alternative makes accidental
// semantic-selection navigation observable in both directions.
let mut alternative = p.diagrams[&mixed].clone();
alternative.id = Id::new_v4();
alternative.alias = "same_layer_alternative".into();
alternative.layer = target_layer;
alternative.projection = ProjectionKind::Overview;
let alternative_id = alternative.id;
p.diagrams.insert(alternative_id, alternative);
p.place(alternative_id, target, Position { x: 700., y: 200. });
oa_core::validate_candidate(&p).unwrap();
let mut app = App::with_project(p);
app.page = Page::Diagram;
app.layer = app_layer;
app.active_diagram = Some(mixed);
app.selected = other;
app.refresh_draft();
let before = app.project().clone();
let action = show_action(&mut app, target);
send(&mut app, action);
// The existing RevealInDiagram action synchronizes the layer
// to the captured view, even if the prior perspective differed.
assert_eq!(
(app.active_diagram, app.layer, app.selected),
(Some(mixed), diagram_layer, target)
);
let added = app.project().diagrams[&mixed]
.nodes
.values()
.find(|n| n.element == target)
.unwrap()
.clone();
assert_eq!(app.selected_node, Some(added.id));
let mut expected = before;
expected
.diagrams
.get_mut(&mixed)
.unwrap()
.nodes
.insert(added.id, added);
assert_eq!(app.project(), &expected);
}
}
}
}
#[test]
fn mixed_view_show_menu_places_in_captured_view_with_exact_undo_redo() {
let (mut app, mixed, system, _, sa) = fixture();
let before = app.document.snapshot();
let action = show_action(&mut app, sa);
send(&mut app, action);
assert_eq!(
(app.active_diagram, app.layer, app.selected),
(Some(mixed), Layer::Oa, sa)
);
let added = app.project().diagrams[&mixed]
.nodes
.values()
.find(|n| n.element == sa)
.unwrap()
.clone();
assert_eq!(added.position, Position { x: 60., y: 60. });
let mut expected = before.decoded.project.clone();
expected
.diagrams
.get_mut(&mixed)
.unwrap()
.nodes
.insert(added.id, added);
assert_eq!(app.project(), &expected);
assert_eq!(
app.project().diagrams[&system],
before.decoded.project.diagrams[&system]
);
let after = app.document.snapshot();
send(&mut app, Message::Undo);
assert_eq!(app.project(), &before.decoded.project);
assert_eq!(app.document.bundle(), &before.bundle);
assert!(!app.document.can_undo());
send(&mut app, Message::Redo);
assert_eq!(app.project(), &after.decoded.project);
assert_eq!(app.document.bundle(), &after.bundle);
}
#[test]
fn mixed_view_show_menu_rejects_stale_context_and_pending_property_changes() {
for refusal in 0..3 {
let (mut app, mixed, system, oa, sa) = fixture();
let action = show_action(&mut app, sa);
match refusal {
0 => app.active_diagram = Some(system),
1 => app.canvas_epoch = app.canvas_epoch.wrapping_add(1),
2 => {
// A queued action must not apply a draft that became dirty
// after the menu was rendered, even when its context is valid.
app.draft
.as_mut()
.unwrap()
.input("/name", "Unapplied name".into());
assert!(app.draft_dirty());
}
_ => unreachable!(),
}
let before = app.document.snapshot();
let raw = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap();
send(&mut app, action);
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
assert_eq!(app.selected, oa);
assert_eq!(
app.active_diagram,
Some(if refusal == 0 { system } else { mixed })
);
assert_eq!(
serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(),
raw
);
assert!(!app.document.can_undo());
}
}
+11 -1
View File
@@ -307,7 +307,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, projection and Saved view visibility. Apply changes commits the view settings.",
"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.",
"Choose Containment for semantic children, Architecture · allocated work for responsibilities, Functional dataflow for work and exchanges, or Missions & capabilities for outcomes. All relationships explores one layer; OA ↔ SA traceability can show placed elements from both layers and their correspondence lines alongside ordinary relationships.",
"Select an element and edit Semantic parent to change ownership. Use an Allocation relationship to change responsibility. Apply changes validates the semantic change.",
"Scroll to zoom and use Fit to frame the current view. In allocated-work views, dragging work changes its assignment and placement together. Choose Arrange next drag or press Alt+A to move work without changing responsibility for that one drag; Escape cancels the armed action. Auto-layout arranges the whole view. Export SVG produces a shareable view.",
"Select a diagram node to access Placement, Fold / unfold and Hide. With the canvas focused, Delete hides its placement; Shift+Delete asks to remove the model element. Other views retain a hidden element. Undo restores either change.",
@@ -317,6 +317,16 @@ 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.",
),
walkthrough(
"Show a correspondence between OA and SA",
&[
"Choose OA ↔ SA traceability in View content. Changing this setting does not create counterparts or add their placements, so an existing single-layer diagram can initially look unchanged.",
"Use Review layer traces to inspect named Operational and System endpoints and the meaning of their link. This opens the model's layer-trace list with no search restriction; traces between relationships are reviewed here rather than drawn between element cards.",
"In the chosen architecture view, use Add existing elements. The chooser offers compatible elements from both layers. Select an existing endpoint and click its position in the canvas; repeat for the other end if needed. This adds a placement, not a new semantic element or trace. For port correspondences, place the ports' owners.",
"The view shows correspondence lines alongside ordinary relationships. The shown-line count reflects the current projection and visibility settings, not trace coverage. If a line is hidden, use View visibility and inspect Saved view visibility; placing an endpoint does not remove explicit hides or filters.",
"If the counterpart is already placed, use Include already shown (Locate) in the chooser. Undo restores an added placement; saving preserves each view's placements independently. Use the Traceability page's existing counterpart or manual-trace workflow when the semantic link itself does not yet exist.",
],
),
walkthrough(
"Move work between performers",
&[
+3 -1
View File
@@ -458,7 +458,8 @@ pub fn popup(app: &App) -> Option<Menu<Message>> {
entries.push(item(
"Show in this diagram",
Message::PlaceSelected,
d.layer == e.layer() && d.projection.includes(e.kind()),
(d.layer == e.layer() || d.projection == ProjectionKind::Traceability)
&& d.projection.includes(e.kind()),
));
}
}
@@ -589,6 +590,7 @@ fn relation_actions(kind: Kind) -> &'static [(&'static str, RelationKind, bool)]
#[cfg(test)]
mod tests {
use super::*;
include!("model_menu_traceability_tests.rs");
fn send(app: &mut App, message: Message) {
let _ = app.update(message);
}
@@ -0,0 +1,62 @@
#[test]
fn traceability_show_menu_matches_cross_layer_and_port_placement_eligibility() {
let mut project = Project::empty();
let sa = project
.add_element(Kind::Function, Layer::Sa, "Counterpart", None)
.unwrap();
let port = Id::new_v4();
project.elements.insert(
port,
Element {
id: port,
alias: "legacy_port".into(),
name: "Port".into(),
description: String::new(),
parent: Some(sa),
body: ElementBody::shared_in(
Layer::Sa,
SharedBody::Port {
contract: PortContract::Legacy,
legacy: Some(LegacyPortContract {
direction: PortDirection::InOut,
interface: None,
}),
},
),
},
);
let diagram = project.ensure_diagram(Layer::Oa);
oa_core::validate_candidate(&project).unwrap();
for projection in [ProjectionKind::Overview, ProjectionKind::Traceability] {
let mut p = project.clone();
p.diagrams.get_mut(&diagram).unwrap().projection = projection;
let mut app = App::with_project(p);
app.page = Page::Diagram;
app.layer = Layer::Oa;
app.active_diagram = Some(diagram);
for object in [sa, port] {
send(&mut app, open(object, Point::ORIGIN));
let action = popup(&app)
.unwrap()
.entries
.into_iter()
.find_map(|e| match e {
Entry::Action { label, message, .. } if label == "Show in this diagram" => {
Some(message)
}
_ => None,
});
if object == port {
assert!(
action.is_none(),
"Ports never acquire independent node placements"
);
} else {
assert_eq!(
action.expect("non-port placement entry").is_some(),
projection == ProjectionKind::Traceability
);
}
}
}
}
+22
View File
@@ -599,6 +599,28 @@ fn diagram(app: &App) -> View<'_, Message> {
}
if let Some(diagram) = app.active_diagram {
let view = oa_core::projection::project_view(app.project(), diagram);
if projection == Some(ProjectionKind::Traceability) {
let correspondence_lines = view.edges.iter().filter(|edge| edge.trace).count();
tools = tools.push(
column![
label("Shows OA and SA elements together, with correspondence lines alongside ordinary relationships."),
label("Place both ends of a trace to show its line. Port traces use their owner cards; traces between relationships are reviewed on the Traceability page."),
row![
label(format!("{correspondence_lines} correspondence lines in this view")),
action(
"Add existing elements…",
Message::Architecture(crate::app::architecture_authoring::Event::Existing(None))
),
action("Review layer traces", Message::ReviewLayerTraces),
action("View visibility…", Message::Select(diagram)),
]
.spacing(7)
.align_y(iced::Center)
.wrap(),
]
.spacing(4),
);
}
let edge = view.edges.iter().find(|edge| edge.id == app.selected);
let promoted = edge.is_some_and(|edge| {
oa_core::projection::edge_endpoint_placements(
+73
View File
@@ -0,0 +1,73 @@
# Traceability view comprehension — issue 31
Branch-specific work above `b8685416f56e53e30bd11ecf93a54cea10ac8458`.
[Issue31](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/31)
owns live progress. This note records the chosen design and acceptance plan,
not completed validation or integration.
The user's observed problem is that OA↔SA traceability looks like All
relationships and its purpose is unclear. Existing core behavior already
projects both layers' placed elements, ordinary relationships and explicit
traces with available endpoints. Switching projection neither creates semantic
counterparts nor adds missing placements. Existing Add existing supports both
layers, but its default suggested-type filter conceals opposite-layer choices.
Show in this diagram also has inconsistent cross-layer eligibility and selects
an opposite-layer element before placement, potentially changing the target view.
## Decision and criteria
Make the existing workflow understandable and reachable before introducing a
second counterpart browser. Criteria: a new user can explain the distinction,
find both named ends, place existing counterparts in the chosen saved diagram,
see/inspect correspondence, understand absent lines, and preserve semantic data.
Keep the established mixed ordinary/trace contract, explicit semantic creation,
saved view filters and single-operation placement history.
Use concise projection-local explanation, the actual count of projected trace
edges, Add existing elements, Review layer traces, and existing View visibility.
Default the Existing chooser to All compatible types only for Traceability;
creation and ordinary-view defaults stay scoped to their existing contexts.
Align Show in this diagram with actual supported placement and retain the
captured diagram across cross-layer selection. Review layer traces opens a
predictable layer-trace inventory without a stale artifact filter/search, through
existing navigation/draft guards. It does not promise that subsequently choosing
an opposite-layer endpoint preserves the originating diagram.
The count is exactly `project_view(...).edges.filter(trace).count()`, after saved
filters. It is neither a project-wide trace total nor a coverage denominator.
No overlapping missing/hidden reasons are added into invented totals. Port
correspondences use owner-card placements; relation-to-relation correspondences
remain reviewable semantic records and are not promised a diagram line.
An expandable view-local counterpart browser with named Place/Locate actions
was independently proposed. It can reduce name memorization, but introduces
another scope, owner-mapping, omission-reason and targeted-placement interface.
The existing chooser already completes the bounded placement task once its
default and shortcut are corrected. Revisit the richer browser if the actual
native task remains confusing. Tooltip-only, traces-only replacement, automatic
counterpart creation/placement and forced OA/SA layout were rejected: respectively
weak task completion, broken mixed-context contract, surprising semantics/history,
and unwanted authored-layout changes.
## Verification plan
Exercise the real Existing chooser and actual Show menu path for opposite-layer
elements, including stale/pending refusal and ordinary-view restrictions. Only
the current diagram's intended placement may change; preserve semantic maps,
other views, route/visibility choices, exact source Undo/Redo and save/reopen.
Ports cannot become node placements. An already placed element must not acquire
a duplicate placement. Predictable trace review must preserve drafts and obey
the usual modal/navigation guards.
Use core projection/filter evidence for no trace, missing endpoints, both placed,
ordinary OA/SA edges, multiple traces, explicit hides plus disabled Trace kind,
port owners and relation correspondence. Do not infer visible lines from
unfiltered edge_availability or confuse unrelated model traces with counterparts.
Native acceptance starts with an OA-owned mixed view, one already visible
correspondence pair and another existing named SA counterpart not yet placed.
Find/place it through the visible controls, see/select the additional trace,
inspect an absent-line visibility reason, Undo/Redo and Save/reopen. Inspect
toolbar/canvas space and chooser clarity at a realistic window size. Different
help text or count tests alone do not resolve the user's comprehension complaint.
Linux remains user-paused; Codex-956atf retains shared integration ownership.