Compare commits

..
552 changed files with 4103 additions and 247127 deletions
@@ -0,0 +1,283 @@
//! Native UI wiring fixture/oracle. Expected output comes from candidate Layout,
//! not from an independent geometry algorithm. Run only after parent source freeze.
use oa_core::*;
use serde_json::{Value, json};
use std::{collections::BTreeMap, error::Error, fs, path::PathBuf};
fn id(n: u128) -> Id {
Id::from_u128(700_000 + n)
}
fn at(x: f32, y: f32) -> Position {
Position { x, y }
}
fn function(p: &mut Project, n: u128, name: &str, parent: Option<Id>) -> Id {
let old = p
.add_element(Kind::Function, Layer::Sa, name, parent)
.unwrap();
reidentify(p, old, id(n)).unwrap();
id(n)
}
fn place(p: &mut Project, d: Id, n: u128, element: Id, position: Position, size: Size) {
let old = p.place(d, element, position);
reidentify(p, old, id(n)).unwrap();
let node = p
.diagrams
.get_mut(&d)
.unwrap()
.nodes
.get_mut(&id(n))
.unwrap();
node.alias = format!("placement_{n}");
node.size = size;
}
fn exchange(p: &mut Project, n: u128, name: &str, source: Id, target: Id) -> Id {
let request = FunctionalExchangeRequest {
id: id(n),
alias: format!("exchange_{n}"),
name: name.into(),
description: String::new(),
source,
target,
source_port: PortChoice::Create,
target_port: PortChoice::Create,
items: vec![],
};
let commands = ports::plan_functional_exchange(p, &request).unwrap();
*p = execute_batch(p, &commands).unwrap();
id(n)
}
fn route(edge: Id, waypoints: Vec<Position>, fraction: f32, offset: Position) -> EdgeRoute {
EdgeRoute {
edge,
source_attachment: None,
target_attachment: None,
waypoints,
label: Some(EdgeLabel { fraction, offset }),
}
}
fn original() -> (Project, Id, Id, Value) {
let mut p = Project::empty();
p.id = id(0);
p.alias = "arrange_native_fixture".into();
p.name = "Arrange route reset trial".into();
let old = p.ensure_diagram(Layer::Sa);
let main = id(1);
reidentify(&mut p, old, main).unwrap();
{
let d = p.diagrams.get_mut(&main).unwrap();
d.alias = "main_dispatch".into();
d.name = "Main dispatch".into();
d.projection = ProjectionKind::FunctionalDataflow;
}
let group = function(&mut p, 10, "Prepare report", None);
let send = function(&mut p, 11, "Send report", Some(group));
let check = function(&mut p, 12, "Check report", Some(group));
let monitor = function(&mut p, 13, "Monitor report", None);
let archive = function(&mut p, 14, "Archive report", None);
let card = Size {
width: 224.,
height: 94.,
};
place(
&mut p,
main,
20,
group,
at(40., 80.),
Size {
width: 400.,
height: 420.,
},
);
for (n, element, x, y) in [
(21, send, 80., 160.),
(22, check, 80., 330.),
(23, monitor, 740., 80.),
(24, archive, 740., 380.),
] {
place(&mut p, main, n, element, at(x, y), card);
}
let authored = exchange(&mut p, 100, "Verified report", send, monitor);
exchange(&mut p, 101, "Report for checking", send, check);
exchange(&mut p, 102, "Checked report archive", check, archive);
let hidden = exchange(
&mut p,
103,
"Filtered archive acknowledgement",
monitor,
archive,
);
let RelationBody::FunctionalExchange {
source_port,
target_port,
..
} = p.relations[&authored].body
else {
unreachable!()
};
for (node, port, side, offset) in [
(id(21), source_port, PortAnchorSide::Right, 0.25),
(id(23), target_port, PortAnchorSide::Left, 0.75),
] {
p = prepare_command(
&p,
&Command::SetPortAnchor {
diagram: main,
node,
anchor: PortAnchor { port, side, offset },
},
)
.unwrap()
.project;
}
let mut visible = route(
authored,
vec![at(610., 210.), at(610., 420.)],
0.5,
at(0., -32.),
);
// Real semantic ports remain authoritative; these stored preferences are dormant.
visible.source_attachment = Some(EdgeAttachment {
side: PortAnchorSide::Bottom,
offset: 0.5,
});
visible.target_attachment = Some(EdgeAttachment {
side: PortAnchorSide::Top,
offset: 0.25,
});
let filtered = route(hidden, vec![at(1020., 270.)], 0.75, at(16., -16.));
p.diagrams.get_mut(&main).unwrap().routes = vec![visible, filtered];
p.diagrams.get_mut(&main).unwrap().view_options.hidden_edges = vec![hidden];
let second = id(2);
p.diagrams.insert(
second,
Diagram {
id: second,
alias: "review_sentinel".into(),
name: "Review sentinel".into(),
description:
"Independent other-view route; Arrange Main dispatch must retain this entire view"
.into(),
layer: Layer::Sa,
projection: ProjectionKind::FunctionalDataflow,
view_options: Default::default(),
nodes: BTreeMap::new(),
layout: None,
routes: vec![route(authored, vec![at(420., 40.)], 0.25, at(8., -8.))],
},
);
place(&mut p, second, 30, send, at(40., 120.), card);
place(&mut p, second, 31, monitor, at(660., 120.), card);
validate_candidate(&p).unwrap();
(
p,
main,
second,
json!({"authored_edge":authored,"authored_edge_name":"Verified report",
"filtered_edge":hidden,"container_placement":id(20),"source_port":source_port,"target_port":target_port,
"main_diagram":main,"second_diagram":second,"authored_pin_count":2,
"source_anchor":{"side":"Right","offset":0.25},"target_anchor":{"side":"Left","offset":0.75},
"dormant_source_attachment":{"side":"Bottom","offset":0.5},
"dormant_target_attachment":{"side":"Top","offset":0.25}}),
)
}
fn json_bytes(p: &Project) -> Vec<u8> {
let mut bytes = serde_json::to_vec_pretty(p).unwrap();
if !bytes.ends_with(b"\n") {
bytes.push(b'\n');
}
assert_eq!(persistence::decode(&bytes).unwrap(), *p);
bytes
}
fn source_bytes(p: &Project) -> Vec<u8> {
let source = language::format(p);
assert_eq!(language::parse(&source).unwrap(), *p);
let mut bytes = source.into_bytes();
if !bytes.ends_with(b"\n") {
bytes.push(b'\n');
}
bytes
}
fn main() -> Result<(), Box<dyn Error>> {
let mut args = std::env::args_os().skip(1);
let first = args
.next()
.ok_or("Usage: arrange_reset_native_probe NEW_DIRECTORY | --compare ACTUAL EXPECTED")?;
if first == "--compare" {
let actual_path = args.next().ok_or("Missing ACTUAL path")?;
let expected_path = args.next().ok_or("Missing EXPECTED path")?;
if args.next().is_some() {
return Err("Unexpected comparison argument".into());
}
let actual = persistence::decode(&fs::read(actual_path)?)?;
let expected = persistence::decode(&fs::read(expected_path)?)?;
let equal = actual == expected;
println!(
"{}",
json!({"complete_project_equal": equal,
"actual_diagrams": actual.diagrams.len(),
"expected_diagrams": expected.diagrams.len()})
);
if !equal {
return Err("Complete typed Projects differ".into());
}
return Ok(());
}
let output = PathBuf::from(first);
if args.next().is_some() || output.exists() {
return Err("Require exactly one output directory that does not already exist".into());
}
let (before, main, second, identities) = original();
let mut folded = before.clone();
folded
.diagrams
.get_mut(&main)
.unwrap()
.nodes
.get_mut(&id(20))
.unwrap()
.collapsed = true;
let mut files = Vec::new();
let mut cases = Vec::new();
for (name, project) in [("expanded", before), ("folded", folded)] {
validate_candidate(&project)?;
// Explicitly candidate-derived UI oracle: this is not an independent layout proof.
let expected = execute_batch(&project, &[Command::Layout(main)])?;
assert_eq!(project.elements, expected.elements);
assert_eq!(project.relations, expected.relations);
assert_eq!(project.diagrams[&second], expected.diagrams[&second]);
for (stage, p) in [("original", &project), ("expected-after", &expected)] {
files.push((format!("{name}-{stage}.json"), json_bytes(p)));
files.push((format!("{name}-{stage}.arcadia"), source_bytes(p)));
}
cases.push(json!({"name":name,"diagram":main,
"original_json":format!("{name}-original.json"),
"expected_json":format!("{name}-expected-after.json"),
"original_source":format!("{name}-original.arcadia"),
"expected_source":format!("{name}-expected-after.arcadia"),
"original_routes":project.diagrams[&main].routes,"expected_routes":expected.diagrams[&main].routes,
"expected_origin":"Candidate Command::Layout; UI-wiring oracle only"}));
}
let manifest = json!({"schema":1,"cases":cases,"identities":identities,
"definition":"Source-authored five-Function/two-view fixture; original geometry never generated by Layout.",
"comparison":"Decode saved JSON with production persistence and compare complete Project to immutable original/expected file. Prefer direct serializer bytes for byte checks; do not reserialize Project through generic JSON Value as a float oracle.",
"performed":"Generation and core checks only when this program is actually run; no native UI results recorded here."});
files.push((
"manifest.json".into(),
serde_json::to_vec_pretty(&manifest)?,
));
fs::create_dir(&output)?;
for (name, bytes) in files {
fs::write(output.join(name), bytes)?;
}
println!("{}", output.join("manifest.json").display());
Ok(())
}
+22 -1
View File
@@ -952,6 +952,21 @@ fn layout(p: &mut Project, diagram: Id) -> Result<(), ModelError> {
"Scenario layout requires the Scenario geometry operation.".into(),
));
}
// Freeze reset eligibility before this operation changes geometry. In a
// Batch this is the staged input, not an independently validated prefix.
let view = crate::projection::project_view(p, diagram);
let visible_nodes: BTreeSet<_> = view.nodes.iter().map(|node| node.id).collect();
let reset_routes: BTreeSet<_> = view
.edges
.iter()
.filter_map(|edge| {
let owners = crate::projection::edge_endpoint_placements(p, d, edge.id)?;
(owners == (edge.source, edge.target)
&& visible_nodes.contains(&edge.source)
&& visible_nodes.contains(&edge.target))
.then_some(edge.id)
})
.collect();
let parents = crate::projection::parents(p, d);
let mut children: BTreeMap<Option<Id>, Vec<Id>> = BTreeMap::new();
for (id, parent) in &parents {
@@ -1044,7 +1059,7 @@ fn layout(p: &mut Project, diagram: Id) -> Result<(), ModelError> {
}
node
};
let edges: BTreeSet<_> = crate::projection::project_view(p, diagram)
let edges: BTreeSet<_> = view
.edges
.iter()
.filter_map(|edge| {
@@ -1138,5 +1153,11 @@ fn layout(p: &mut Project, diagram: Id) -> Result<(), ModelError> {
*existing = route;
}
}
for route in &mut d.routes {
if reset_routes.contains(&route.edge) {
route.waypoints.clear();
route.label = None;
}
}
Ok(())
}
+451 -5
View File
@@ -649,7 +649,7 @@ fn group_translation_beyond_coordinate_limit_rolls_back_nodes_and_routes() {
}
#[test]
fn layout_preserves_authored_route_count_order_labels_and_undo() {
fn layout_resets_visible_pins_labels_preserving_route_count_order_and_undo() {
let f = routed(fixture());
let mut session = Session::new(f.project.clone());
session.execute(Command::Layout(f.diagram)).unwrap();
@@ -657,10 +657,10 @@ fn layout_preserves_authored_route_count_order_labels_and_undo() {
assert_eq!(routes.len(), 2);
assert_eq!(routes[0].edge, f.edge);
assert_eq!(routes[1].edge, f.external);
assert_eq!(routes[0].waypoints.len(), 2);
assert_eq!(routes[1].waypoints.len(), 2);
assert_eq!(routes[0].label, route(f.edge).label);
assert_eq!(routes[1].label, route(f.external).label);
assert!(routes[0].waypoints.is_empty());
assert!(routes[1].waypoints.is_empty());
assert_eq!(routes[0].label, None);
assert_eq!(routes[1].label, None);
assert_eq!(
session.project().diagrams[&f.second],
f.project.diagrams[&f.second]
@@ -669,6 +669,452 @@ fn layout_preserves_authored_route_count_order_labels_and_undo() {
assert_eq!(session.project(), &f.project);
}
#[test]
fn layout_keeps_attachments_port_anchors_empty_entries_and_exact_history() {
let mut f = routed(fixture());
let extra = f.project.relate(
RelationKind::FunctionalExchange,
"Attachment only",
f.a,
f.b,
);
let empty = f
.project
.relate(RelationKind::FunctionalExchange, "Explicit empty", f.a, f.b);
let attachment = EdgeAttachment {
side: PortAnchorSide::Bottom,
offset: 0.23,
};
let opposite = EdgeAttachment {
side: PortAnchorSide::Top,
offset: 0.81,
};
let mut fraction_only = route(f.edge);
fraction_only.waypoints.clear();
fraction_only.label = Some(EdgeLabel {
fraction: 0.73,
offset: Position { x: 0., y: 0. },
});
fraction_only.source_attachment = Some(attachment);
fraction_only.target_attachment = Some(opposite);
let attachment_only = EdgeRoute {
edge: extra,
source_attachment: Some(attachment),
target_attachment: None,
waypoints: vec![],
label: None,
};
let empty_route = EdgeRoute {
edge: empty,
source_attachment: None,
target_attachment: None,
waypoints: vec![],
label: None,
};
f.project.diagrams.get_mut(&f.diagram).unwrap().routes = vec![
route(f.external),
fraction_only.clone(),
attachment_only.clone(),
empty_route.clone(),
];
let RelationBody::FunctionalExchange { source_port, .. } = f.project.relations[&f.edge].body
else {
unreachable!()
};
let owner = node(&f.project, f.diagram, f.a);
let anchor = PortAnchor {
port: source_port,
side: PortAnchorSide::Left,
offset: 0.37,
};
f.project
.diagrams
.get_mut(&f.diagram)
.unwrap()
.nodes
.get_mut(&owner)
.unwrap()
.port_anchors
.push(anchor.clone());
validate_candidate(&f.project).unwrap();
let original_source = language::format(&f.project);
let mut session = Session::new(f.project.clone());
session.execute(Command::Layout(f.diagram)).unwrap();
let after = session.project().clone();
let mut expected_external = route(f.external);
expected_external.waypoints.clear();
expected_external.label = None;
fraction_only.label = None;
assert_eq!(
after.diagrams[&f.diagram].routes,
vec![
expected_external,
fraction_only,
attachment_only,
empty_route
]
);
assert_eq!(
after.diagrams[&f.diagram].nodes[&owner].port_anchors,
vec![anchor]
);
assert_eq!(after.elements, f.project.elements);
assert_eq!(after.relations, f.project.relations);
assert_eq!(after.diagrams[&f.second], f.project.diagrams[&f.second]);
assert_eq!(language::parse(&language::format(&after)).unwrap(), after);
let command: Command =
serde_json::from_value(serde_json::json!({"Layout": f.diagram})).unwrap();
assert_eq!(execute_batch(&f.project, &[command]).unwrap(), after);
session.undo();
assert_eq!(session.project(), &f.project);
assert_eq!(language::format(session.project()), original_source);
assert!(!session.can_undo());
session.redo();
assert_eq!(session.project(), &after);
}
fn shift_all(p: &mut Project, d: Id, x: f32, y: f32) {
for placement in p.diagrams.get_mut(&d).unwrap().nodes.values_mut() {
placement.position.x += x;
placement.position.y += y;
}
}
fn translated_expected(mut route: EdgeRoute, x: f32, y: f32) -> EdgeRoute {
for point in &mut route.waypoints {
point.x += x;
point.y += y;
}
route
}
#[test]
fn layout_resets_only_visible_actual_owners_and_retains_filtered_and_missing_routes() {
let mut f = fixture();
let outside = f.project.relations[&f.external].target();
let visible = f.project.relate(
RelationKind::FunctionalExchange,
"Visible overview",
f.group,
outside,
);
let absent = f
.project
.add_element(Kind::Function, Layer::Sa, "Not placed", None)
.unwrap();
let missing = f.project.relate(
RelationKind::FunctionalExchange,
"Dormant missing owner",
f.b,
absent,
);
f.project
.diagrams
.get_mut(&f.diagram)
.unwrap()
.view_options
.hidden_edges = vec![f.edge, f.external];
// Freeze a geometry-only starting point before adding sentinels. The oracle
// below names the eligible edge and exact authored deltas independently.
let base = execute_batch(&f.project, &[Command::Layout(f.diagram)]).unwrap();
f.project = base.clone();
shift_all(&mut f.project, f.diagram, 40., 60.);
let outside_node = node(&f.project, f.diagram, outside);
f.project
.diagrams
.get_mut(&f.diagram)
.unwrap()
.nodes
.get_mut(&outside_node)
.unwrap()
.position
.x += 7.;
f.project.diagrams.get_mut(&f.diagram).unwrap().routes = vec![
route(visible),
route(f.edge),
route(f.external),
route(missing),
];
let view = projection::project_view(&f.project, f.diagram);
assert_eq!(
view.edges.iter().map(|edge| edge.id).collect::<Vec<_>>(),
vec![visible]
);
let after = execute_batch(&f.project, &[Command::Layout(f.diagram)]).unwrap();
assert_eq!(
after.diagrams[&f.diagram].nodes,
base.diagrams[&f.diagram].nodes
);
let mut reset = route(visible);
reset.waypoints.clear();
reset.label = None;
assert_eq!(
after.diagrams[&f.diagram].routes,
vec![
reset,
translated_expected(route(f.edge), -40., -60.),
route(f.external),
route(missing)
]
);
assert_eq!(after.diagrams[&f.second], f.project.diagrams[&f.second]);
}
#[test]
fn layout_folded_routes_use_actual_owner_deltas_without_resetting_promoted_edges() {
let mut f = routed(fixture());
let group = node(&f.project, f.diagram, f.group);
let outside = f.project.relations[&f.external].target();
let outside_node = node(&f.project, f.diagram, outside);
f.project
.diagrams
.get_mut(&f.diagram)
.unwrap()
.nodes
.get_mut(&group)
.unwrap()
.collapsed = true;
f.project
.diagrams
.get_mut(&f.diagram)
.unwrap()
.routes
.clear();
let base = execute_batch(&f.project, &[Command::Layout(f.diagram)]).unwrap();
f.project = base.clone();
shift_all(&mut f.project, f.diagram, 40., 60.);
// The visible container's displacement differs from its hidden children.
f.project
.diagrams
.get_mut(&f.diagram)
.unwrap()
.nodes
.get_mut(&group)
.unwrap()
.position
.x += 13.;
f.project
.diagrams
.get_mut(&f.diagram)
.unwrap()
.nodes
.get_mut(&outside_node)
.unwrap()
.position
.x += 7.;
f.project.diagrams.get_mut(&f.diagram).unwrap().routes = vec![route(f.edge), route(f.external)];
let view = projection::project_view(&f.project, f.diagram);
assert!(!view.edges.iter().any(|edge| edge.id == f.edge));
let promoted = view
.edges
.iter()
.find(|edge| edge.id == f.external)
.unwrap();
assert_eq!(promoted.source, group);
let after = execute_batch(&f.project, &[Command::Layout(f.diagram)]).unwrap();
assert_eq!(
after.diagrams[&f.diagram].nodes,
base.diagrams[&f.diagram].nodes
);
assert_eq!(
after.diagrams[&f.diagram].routes,
vec![
translated_expected(route(f.edge), -40., -60.),
route(f.external)
]
);
assert_eq!(after.diagrams[&f.second], f.project.diagrams[&f.second]);
}
#[test]
fn layout_batch_uses_staged_visibility_and_one_final_validation_history_boundary() {
let f = routed(fixture());
let mut hidden = f.project.diagrams[&f.diagram].clone();
hidden.view_options.hidden_edges = vec![f.edge, f.external];
let mut invalid = f.project.elements[&f.a].clone();
invalid.alias = f.project.elements[&f.b].alias.clone();
// Duplicate alias is repaired later in the same transaction. Layout must
// not add prefix validation, nor reset routes hidden by the preceding edit.
let mut session = Session::new(f.project.clone());
session
.execute(Command::Batch(vec![
Command::EditElement(f.a, invalid),
Command::EditDiagram(f.diagram, hidden),
Command::Layout(f.diagram),
Command::EditElement(f.a, f.project.elements[&f.a].clone()),
]))
.unwrap();
let after = session.project().clone();
assert_eq!(
after.diagrams[&f.diagram]
.routes
.iter()
.map(|r| (r.edge, r.waypoints.len(), r.label.clone()))
.collect::<Vec<_>>(),
vec![
(f.edge, 2, route(f.edge).label),
(f.external, 2, route(f.external).label)
]
);
session.undo();
assert_eq!(session.project(), &f.project);
assert!(!session.can_undo());
session.redo();
assert_eq!(session.project(), &after);
let mut bad = after.elements[&f.a].clone();
bad.alias = after.elements[&f.b].alias.clone();
let revision = session.revision();
assert!(
session
.execute(Command::Batch(vec![
Command::Layout(f.diagram),
Command::EditElement(f.a, bad)
]))
.is_err()
);
assert_eq!(session.project(), &after);
assert_eq!(session.revision(), revision);
}
#[test]
fn layout_outside_projection_routes_keep_overrides_and_inactive_owner_positions() {
let mut f = fixture();
// Decomposition admits ordinary relations, including FunctionalExchange.
// Capabilities explicitly excludes those edges and their Function owners;
// retain their authored placements while arranging a real visible node.
let capability = f
.project
.add_element(
Kind::SystemCapability,
Layer::Sa,
"Visible capability",
None,
)
.unwrap();
let capability_node = f
.project
.place(f.diagram, capability, Position { x: 100., y: 100. });
f.project.diagrams.get_mut(&f.diagram).unwrap().projection = ProjectionKind::Capabilities;
let base = execute_batch(&f.project, &[Command::Layout(f.diagram)]).unwrap();
f.project = base.clone();
shift_all(&mut f.project, f.diagram, 40., 60.);
f.project.diagrams.get_mut(&f.diagram).unwrap().routes = vec![route(f.edge), route(f.external)];
validate_candidate(&f.project).unwrap();
let projected = projection::project_view(&f.project, f.diagram);
assert_eq!(projected.nodes.len(), 1);
assert_eq!(projected.nodes[0].id, capability_node);
assert!(
projected
.edges
.iter()
.all(|edge| edge.id != f.edge && edge.id != f.external)
);
let mut expected = f.project.clone();
expected.diagrams.get_mut(&f.diagram).unwrap().nodes.insert(
capability_node,
base.diagrams[&f.diagram].nodes[&capability_node].clone(),
);
let after = execute_batch(&f.project, &[Command::Layout(f.diagram)]).unwrap();
assert_ne!(
after.diagrams[&f.diagram].nodes[&capability_node].position,
f.project.diagrams[&f.diagram].nodes[&capability_node].position
);
assert_eq!(
after.diagrams[&f.diagram].routes,
vec![route(f.edge), route(f.external)]
);
assert_eq!(
after, expected,
"Owners excluded by the projection receive no Layout delta; their pins stay fixed"
);
}
#[test]
fn identical_layout_transaction_keeps_redo() {
let mut p = Project::empty();
let element = p
.add_element(Kind::Function, Layer::Sa, "Unplaced", None)
.unwrap();
let diagram = p.ensure_diagram(Layer::Sa);
let mut changed = p.elements[&element].clone();
changed.name = "Renamed unplaced".into();
let mut session = Session::new(p.clone());
session
.execute(Command::EditElement(element, changed))
.unwrap();
session.undo();
let revision = session.revision();
session
.execute(Command::Batch(vec![
Command::Layout(diagram),
Command::Layout(diagram),
]))
.unwrap();
assert_eq!(session.project(), &p);
assert_eq!(session.revision(), revision);
assert!(session.can_redo());
}
#[test]
fn layout_route_reset_alone_is_one_edit_and_repeated_layout_keeps_redo() {
let f = fixture();
let mut before = execute_batch(&f.project, &[Command::Layout(f.diagram)]).unwrap();
assert!(!before.diagrams[&f.diagram].nodes.is_empty());
before.diagrams.get_mut(&f.diagram).unwrap().routes = vec![route(f.edge), route(f.external)];
// Node geometry is already arranged. The complete expected Project differs
// only in the two independently named eligible route overrides.
let mut expected = before.clone();
for route in &mut expected.diagrams.get_mut(&f.diagram).unwrap().routes {
route.waypoints.clear();
route.label = None;
}
let mut session = Session::new(before.clone());
session.execute(Command::Layout(f.diagram)).unwrap();
assert_eq!(
session.project().diagrams[&f.diagram].nodes,
before.diagrams[&f.diagram].nodes
);
assert_eq!(session.project(), &expected);
assert_eq!(
session.revision(),
1,
"Clearing overrides is a real edit without any node movement"
);
session.execute(Command::Layout(f.diagram)).unwrap();
assert_eq!(session.project(), &expected);
assert_eq!(
session.revision(),
1,
"A repeated identical Layout adds no history"
);
session.undo();
assert_eq!(session.project(), &before);
assert!(
!session.can_undo(),
"The route-only reset was exactly one history entry"
);
session.redo();
assert_eq!(session.project(), &expected);
let mut renamed = expected.elements[&f.a].clone();
renamed.name = "Pending redo of rename".into();
session.execute(Command::EditElement(f.a, renamed)).unwrap();
let after_rename = session.project().clone();
session.undo();
assert_eq!(session.project(), &expected);
assert!(session.can_redo());
let revision = session.revision();
session.execute(Command::Layout(f.diagram)).unwrap();
assert_eq!(session.project(), &expected);
assert_eq!(session.revision(), revision);
assert!(
session.can_redo(),
"No-op Layout retains an unrelated undone edit"
);
session.redo();
assert_eq!(session.project(), &after_rename);
}
#[test]
fn old_models_get_empty_routes_and_route_json_is_strict() {
for bytes in [
+92
View File
@@ -704,6 +704,98 @@ fn moving_a_region_does_not_translate_pins_attached_to_a_stationary_state_bounda
assert_eq!(after.diagrams[&f.d].routes[0].waypoints, pins);
}
#[test]
fn layout_retains_hidden_state_boundary_pins_and_internal_routes() {
let mut f = fixture();
f.p.diagrams
.get_mut(&f.d)
.unwrap()
.view_options
.hidden_edges = vec![f.local];
let baseline = execute_batch(&f.p, &[Command::Layout(f.d)]).unwrap();
f.p = baseline.clone();
let entry = node(&f.p, f.d, f.entry);
let leaf = node(&f.p, f.d, f.leaf);
let boundary = node(&f.p, f.d, f.a);
for id in [entry, leaf] {
let n =
f.p.diagrams
.get_mut(&f.d)
.unwrap()
.nodes
.get_mut(&id)
.unwrap();
n.position.x += 10.;
n.position.y += 10.;
}
let hidden = EdgeRoute {
edge: f.local,
source_attachment: None,
target_attachment: None,
waypoints: vec![point(280., 390.), point(280., 470.)],
label: Some(EdgeLabel {
fraction: 0.4,
offset: point(3., -7.),
}),
};
let internal = EdgeRoute {
edge: f.internal,
source_attachment: None,
target_attachment: None,
waypoints: vec![point(320., 440.)],
label: Some(EdgeLabel {
fraction: 0.7,
offset: point(0., 0.),
}),
};
let visible = EdgeRoute {
edge: f.self_edge,
source_attachment: Some(EdgeAttachment {
side: PortAnchorSide::Left,
offset: 0.25,
}),
target_attachment: Some(EdgeAttachment {
side: PortAnchorSide::Right,
offset: 0.75,
}),
waypoints: vec![point(700., 100.)],
label: Some(EdgeLabel {
fraction: 0.6,
offset: point(0., 0.),
}),
};
f.p.diagrams.get_mut(&f.d).unwrap().routes =
vec![hidden.clone(), internal.clone(), visible.clone()];
let projected = project_view(&f.p, f.d);
assert!(
!projected
.edges
.iter()
.any(|e| e.id == f.local || e.id == f.internal)
);
assert!(projected.edges.iter().any(|e| e.id == f.self_edge));
let after = execute_batch(&f.p, &[Command::Layout(f.d)]).unwrap();
for id in [entry, leaf, boundary] {
assert_eq!(
after.diagrams[&f.d].nodes[&id].position,
baseline.diagrams[&f.d].nodes[&id].position
);
}
assert_eq!(
after.diagrams[&f.d].nodes[&boundary].position,
f.p.diagrams[&f.d].nodes[&boundary].position,
"The endpoint's owning State boundary stayed stationary"
);
let mut reset = visible;
reset.waypoints.clear();
reset.label = None;
assert_eq!(
after.diagrams[&f.d].routes,
vec![hidden, internal, reset],
"Equal entry/leaf deltas must not translate a pin on their stationary State boundary"
);
}
#[test]
fn initial_transition_attaches_to_the_compact_symbol_and_preview_keeps_settled_geometry() {
let mut f = fixture();
+3 -119
View File
@@ -6,8 +6,6 @@ mod architecture_authoring_tests;
mod behavior_removal_tests;
mod checkpoints;
#[cfg(test)]
mod context_help_tests;
#[cfg(test)]
mod diagram_focus_tests;
pub(crate) mod diagram_removal;
#[cfg(test)]
@@ -378,10 +376,6 @@ pub enum Message {
ExactStrings(bool),
Page(Page),
Guide(crate::guide::Event),
ContextHelp(crate::guide::Topic),
CloseContextHelp,
ContextHelpScroll(crate::guide::ContextScroll),
Help,
Traceability(crate::views::traceability::Event),
ReviewLayerTraces,
Behavior(crate::behavior::Event),
@@ -527,7 +521,6 @@ pub struct App {
/// Home resumes this page; explicit Architecture navigation remains separate.
last_workspace_page: Page,
pub guide: crate::guide::State,
pub context_help: bool,
pub traceability: crate::views::traceability::State,
pub behavior: crate::behavior::State,
pub data_state: crate::data_view::State,
@@ -650,7 +643,6 @@ impl App {
page: Page::Diagram,
last_workspace_page: Page::Diagram,
guide: crate::guide::State::default(),
context_help: false,
traceability: Default::default(),
behavior: Default::default(),
data_state: Default::default(),
@@ -1302,11 +1294,6 @@ impl App {
iced::time::every(std::time::Duration::from_secs(10)).map(|_| Message::CheckpointTick),
window::close_requests().map(|id| Message::Request(Pending::Close(id))),
window::resize_events().map(|(_, size)| Message::WindowResized(size)),
if self.context_help {
iced::event::listen_with(crate::guide::context_scroll_key)
} else {
Subscription::none()
},
iced::event::listen_with(|event, _, _| {
matches!(
event,
@@ -1333,7 +1320,7 @@ impl App {
return Some(Message::Escape);
}
if key == keyboard::Key::Named(keyboard::key::Named::F1) {
return Some(Message::Help);
return Some(Message::Page(Page::Guide));
}
if key == keyboard::Key::Named(keyboard::key::Named::F3) {
return Some(Message::SourceNavigation(
@@ -1361,55 +1348,6 @@ impl App {
])
}
pub fn update(&mut self, message: Message) -> Task<Message> {
if let Message::ContextHelpScroll(scroll) = message {
return if self.context_help {
crate::guide::context_scroll_task(scroll)
} else {
Task::none()
};
}
// Help owns user input before any canvas/reference fast path can act.
// Passive results may finish; they retain their normal lifetime checks.
if self.context_help
&& !matches!(
message,
Message::Guide(_)
| Message::CloseContextHelp
| Message::Escape
| Message::Request(Pending::Close(_))
| Message::CheckpointWritten(..)
| Message::CheckpointTick
| Message::CheckpointLoaded(..)
| Message::WindowResized(_)
| Message::CanvasResized(_)
| Message::DiagramRevealViewport(..)
| Message::SourceViewport(_)
| Message::Lint(_)
| Message::Linted(..)
| Message::Saved(_)
| Message::Hosted(crate::hosted::Event::Completed(..))
| Message::Loaded(_)
| Message::IncludesLoaded(..)
| Message::SavePath(_)
| Message::SourceRecovered(_)
| Message::Exported(_)
| Message::SourceSymbolsReady(_)
| Message::SourceRenameReady(_)
| Message::DiagramUsagesReady(_)
| Message::ScenarioCanvas(
behavior::scenario::CanvasEvent::Scrolled(..)
| behavior::scenario::CanvasEvent::Restored(..)
| behavior::scenario::CanvasEvent::Viewport(..)
)
| Message::TopologyCanvas(
behavior::topology::CanvasEvent::Scrolled(..)
| behavior::topology::CanvasEvent::Viewport(..)
)
| Message::StateDiagram(behavior::state_edit::Event::Viewport(..))
)
{
return Task::none();
}
let preview_read_only = self.process_preview_primary().is_some();
let selection_boundary = self.multiselect_boundary();
// Reject delayed gestures before they can retire newer input ownership.
@@ -1962,10 +1900,6 @@ impl App {
message,
Message::Escape | Message::Request(Pending::Close(_))
) {
if self.context_help {
self.context_help = false;
return Task::none();
}
if self.hosted.visible {
self.hosted.visible = false;
return Task::none();
@@ -2157,7 +2091,6 @@ impl App {
}
pub(crate) fn scenario_focus_owner(&self) -> Option<crate::behavior::scenario::focus::Stamp> {
if self.busy
|| self.context_help
|| self.hosted.visible
|| self.workflow.is_some()
|| self.architecture.is_some()
@@ -2854,35 +2787,6 @@ impl App {
);
}
}
Message::ContextHelp(topic) => {
self.guide.update(crate::guide::Event::Topic(topic));
self.context_help = true;
return iced::widget::operation::snap_to(
crate::guide::ARTICLE_SCROLL_ID,
iced::widget::scrollable::RelativeOffset::START,
);
}
Message::CloseContextHelp => {
self.context_help = false;
}
Message::ContextHelpScroll(_) => {}
Message::Help => {
let topic = match self.page {
Page::Diagram => Some(crate::guide::Topic::Nesting),
Page::Behavior => self.behavior.active_owner.and_then(|id| {
self.project()
.elements
.get(&id)
.map(|element| match element.kind() {
Kind::StateMachine => crate::guide::Topic::States,
Kind::Scenario => crate::guide::Topic::Scenarios,
_ => crate::guide::Topic::Chains,
})
}),
_ => None,
};
return self.update(topic.map_or(Message::Page(Page::Guide), Message::ContextHelp));
}
Message::ExactStrings(enabled) => {
if let Some(d) = &mut self.draft {
match d.set_exact(enabled) {
@@ -5605,7 +5509,6 @@ fn write_snapshot_inner(
.map_err(|errors| crate::document::DocumentError::Source(errors).to_string())?,
);
}
let has_original_destination = previous.is_some();
let previous = previous.filter(|disk| {
path.parent()
.filter(|p| !p.as_os_str().is_empty())
@@ -5616,33 +5519,14 @@ fn write_snapshot_inner(
== Some(&disk.root)
&& entry == disk.bundle.entry
});
// A missing-file expectation colliding with an existing destination is
// not evidence that an observed dependency changed outside the editor.
// Keep save_project's conflict reporting unchanged for that latter case.
let saving_existing = previous.is_some();
let destination_error = |error: source_files::SourceFileError| match error {
source_files::SourceFileError::Conflict(paths) => {
let guidance = if saving_existing {
"Review the new included file's destination before saving."
} else if has_original_destination {
"Choose a fresh directory for Save As, or use Save All to update the open project's current entry."
} else {
"Choose a fresh directory for Save As."
};
format!(
"Source destination files already exist: {paths:?}. Existing files are protected. {guidance}"
)
}
other => other.to_string(),
};
let mut disk = match previous {
Some(disk) => disk,
None => source_files::new_project(&path, LIMITS).map_err(&destination_error)?,
None => source_files::new_project(&path, LIMITS).map_err(|e| e.to_string())?,
};
for path in snapshot.bundle.files.keys() {
if !disk.expectations.contains_key(path) {
source_files::expect_new_file(&mut disk, path, LIMITS)
.map_err(&destination_error)?;
.map_err(|e| e.to_string())?;
}
}
Some(source_files::save_project(&disk, &snapshot, LIMITS).map_err(|e| e.to_string())?)
+2 -8
View File
@@ -16,7 +16,6 @@ use std::{
pub const FINISH_EDIT: &str =
"Finish or cancel the current edit before selecting or moving elements.";
const ARRANGE_ARMED: &str = "Arrange next drag armed · presentation only · Esc cancels";
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Stamp {
@@ -636,7 +635,6 @@ impl App {
}
pub(crate) fn allocation_input_owner(&self) -> bool {
self.page == Page::Diagram
&& !self.context_help
&& self
.active_diagram
.and_then(|id| self.project().diagrams.get(&id))
@@ -701,12 +699,7 @@ impl App {
| Message::DiagramUsagesReady(_)
| Message::DiagramRevealViewport(..)
) {
let clear_armed_status =
self.allocation_drag.arrange_next && self.status == ARRANGE_ARMED;
self.allocation_drag.retire();
if clear_armed_status {
self.status = "Arrange cancelled".into();
}
}
}
pub(super) fn allocation_boundary(&self) -> impl PartialEq + use<> {
@@ -848,7 +841,8 @@ impl App {
self.allocation_drag.retire();
if self.allocation_drag_clean() {
self.allocation_drag.arrange_next = true;
self.status = ARRANGE_ARMED.into();
self.status =
"Arrange next drag armed · presentation only · Esc cancels".into();
} else {
self.status = FINISH_EDIT.into();
}
@@ -190,7 +190,6 @@ async fn actual_keyboard_arrange_shortcut_consumes_once_escape_cancels_and_dirty
)
.await;
assert!(!f.app.allocation_drag.arrange_next);
assert_eq!(f.app.status, "Arrange cancelled");
mounted
.batch(
&mut f.app,
@@ -1,723 +0,0 @@
//! Context help is a reading overlay, not a workspace navigation or edit.
use super::*;
use crate::diagram_usage::Target;
use crate::guide::Topic;
fn send(app: &mut App, message: Message) {
// Do not poll I/O tasks: exercise the same admission boundary as the UI.
let _ = app.update(message);
}
fn architecture() -> (App, Id, Id, Id) {
let mut project = Project::empty();
let a = project
.add_element(Kind::Function, Layer::Sa, "Send", None)
.unwrap();
let b = project
.add_element(Kind::Function, Layer::Sa, "Receive", None)
.unwrap();
let diagram = project.ensure_diagram(Layer::Sa);
project.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::FunctionalDataflow;
let a = project.place(diagram, a, Position { x: 20., y: 20. });
let b = project.place(diagram, b, Position { x: 400., y: 20. });
let mut app = App::with_project(project);
app.page = Page::Diagram;
app.layer = Layer::Sa;
app.active_diagram = Some(diagram);
app.selected = diagram;
app.refresh_draft();
app.reconcile_multiselect();
(app, diagram, a, b)
}
fn choose(app: &mut App, placement: Id, choice: multiselect::Choice) {
let context = app.multiselect_context().unwrap();
send(
app,
Message::DiagramSelection(multiselect::Event::Choose(
context,
Target::Placement(placement),
choice,
)),
);
}
#[test]
fn context_help_preserves_group_property_draft_camera_and_model() {
let (mut app, diagram, a, b) = architecture();
choose(&mut app, a, multiselect::Choice::Replace);
choose(&mut app, b, multiselect::Choice::Toggle);
send(
&mut app,
Message::Field("/name".into(), "Uncommitted name 雨".into()),
);
assert!(app.draft_dirty());
app.zoom = 0.73;
app.pan = iced::Vector::new(31., -57.);
let selected = app.selected;
let selection = app.multiselect_snapshot().unwrap().targets().to_vec();
assert_eq!(selection.len(), 2);
let draft = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap();
let before = app.document.snapshot();
let camera = (app.zoom, app.pan);
send(&mut app, Message::ContextHelp(Topic::Nesting));
assert!(app.context_help);
for message in [
Message::Page(Page::Catalog),
Message::Select(diagram),
Message::Apply,
Message::RevertProperties,
Message::Undo,
Message::Delete,
Message::Field("/name".into(), "Background mutation".into()),
] {
send(&mut app, message);
}
assert!(app.context_help);
send(
&mut app,
Message::Guide(crate::guide::Event::Topic(Topic::Exchanges)),
);
assert_eq!(app.guide.topic, Topic::Exchanges);
send(&mut app, Message::CloseContextHelp);
assert!(!app.context_help);
assert_eq!(app.page, Page::Diagram);
assert_eq!(app.active_diagram, Some(diagram));
assert_eq!(app.selected, selected);
assert_eq!(
app.multiselect_snapshot().unwrap().targets(),
selection.as_slice()
);
assert_eq!((app.zoom, app.pan), camera);
assert_eq!(
serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(),
draft
);
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
assert!(!app.document.can_undo());
}
#[test]
fn context_help_escape_preserves_pending_behavior_form_and_blocks_submit() {
use crate::behavior::states;
let mut project = Project::empty();
let system = project
.add_element(Kind::System, Layer::Sa, "System", None)
.unwrap();
let machine = project
.add_element(Kind::StateMachine, Layer::Sa, "Modes", Some(system))
.unwrap();
let region = project
.add_element(Kind::Region, Layer::Sa, "Main", Some(machine))
.unwrap();
let state = project
.add_element(Kind::State, Layer::Sa, "Ready", Some(region))
.unwrap();
let mut app = App::with_project(project);
send(&mut app, Message::Select(state));
send(&mut app, Message::Page(Page::Behavior));
send(
&mut app,
Message::Behavior(behavior::Event::States(states::Event::Start(
machine,
states::Tool::Transition,
state,
))),
);
send(
&mut app,
Message::Behavior(behavior::Event::States(states::Event::Name(
"Pending transition 雨".into(),
))),
);
assert!(app.behavior.has_pending());
let draft = app.behavior.recovery_snapshot().unwrap();
let before = app.document.snapshot();
let selected = app.selected;
send(&mut app, Message::Help);
assert!(app.context_help);
assert_eq!(app.guide.topic, Topic::States);
for event in [
states::Event::Cancel,
states::Event::Submit,
states::Event::Name("Lost".into()),
] {
send(&mut app, Message::Behavior(behavior::Event::States(event)));
}
send(&mut app, Message::Page(Page::Catalog));
send(&mut app, Message::Escape);
assert!(!app.context_help);
assert_eq!(app.page, Page::Behavior);
assert_eq!(app.selected, selected);
assert_eq!(app.behavior.active_owner, Some(machine));
assert!(app.behavior.has_pending());
assert_eq!(app.behavior.recovery_snapshot().unwrap(), draft);
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
}
#[test]
fn context_help_retains_source_draft_and_rejects_background_source_input() {
let (mut app, diagram, _, _) = architecture();
send(&mut app, Message::Page(Page::Text));
send(
&mut app,
Message::SourceEdit(text_editor::Action::SelectAll),
);
send(
&mut app,
Message::SourceEdit(text_editor::Action::Edit(text_editor::Edit::Paste(
Arc::new("invalid unfinished source 雨".into()),
))),
);
assert!(app.source_dirty());
send(&mut app, Message::Page(Page::Diagram));
let source = app.source_editor.text();
let version = app.source_version;
let before = app.document.snapshot();
send(&mut app, Message::Help);
assert!(app.context_help);
assert_eq!(app.guide.topic, Topic::Nesting);
for message in [
Message::ApplySource,
Message::RevertSource,
Message::SourceEdit(text_editor::Action::Edit(text_editor::Edit::Paste(
Arc::new("overwrite".into()),
))),
] {
send(&mut app, message);
}
send(&mut app, Message::Escape);
assert_eq!(app.page, Page::Diagram);
assert_eq!(app.active_diagram, Some(diagram));
assert!(app.source_dirty());
assert_eq!(app.source_editor.text(), source);
assert_eq!(app.source_version, version);
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
}
#[test]
fn context_help_f1_selects_scenario_topic_without_leaving_the_workspace() {
let fixture = behavior::scenario::direct_tests::fixture();
let mut app = App::with_project(fixture.project);
send(&mut app, Message::Select(fixture.owner));
send(&mut app, Message::Page(Page::Behavior));
let before = app.document.snapshot();
send(&mut app, Message::Help);
assert!(app.context_help);
assert_eq!(app.guide.topic, Topic::Scenarios);
assert_eq!(app.page, Page::Behavior);
assert!(app.scenario_focus_owner().is_none());
assert!(!behavior::canvas_workspace::keyboard_owner_enabled(&app));
send(&mut app, Message::Escape);
assert!(!app.context_help);
assert_eq!(app.selected, fixture.owner);
assert_eq!(app.behavior.active_owner, Some(fixture.owner));
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
}
#[test]
fn context_help_disables_allocation_input_including_early_work_drag_dispatch() {
let mut project = Project::empty();
let diagram = project.ensure_diagram(Layer::Oa);
project.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Allocation;
let mut app = App::with_project(project);
assert!(app.allocation_input_owner());
send(
&mut app,
Message::WorkDrag(allocation_drag::Event::ArrangeNext),
);
assert!(app.allocation_drag.arrange_next);
send(&mut app, Message::Help);
assert!(app.context_help);
assert!(!app.allocation_input_owner());
assert!(!app.allocation_drag.arrange_next);
assert_eq!(app.status, "Arrange cancelled");
let before = app.document.snapshot();
send(
&mut app,
Message::WorkDrag(allocation_drag::Event::ArrangeNext),
);
assert!(!app.allocation_drag.arrange_next);
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
send(&mut app, Message::Escape);
assert!(app.allocation_input_owner());
}
#[test]
fn context_help_rejects_a_reference_picker_open_queued_from_the_inspector() {
let mut project = Project::empty();
let owner = project
.add_element(Kind::DataType, Layer::Oa, "Record", None)
.unwrap();
if let Some(SharedBody::DataType { definition, .. }) =
project.elements.get_mut(&owner).unwrap().shared_mut()
{
*definition = DataTypeDefinition::Record { supertype: None };
}
let mut app = App::with_project(project);
send(&mut app, Message::Select(owner));
let request = app.draft.as_ref().unwrap().reference_open(
app.project(),
"/body/Oa/Shared/DataType/definition/Record/supertype",
iced::Rectangle {
x: 790.,
y: 400.,
width: 278.,
height: 32.,
},
);
let before = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap();
send(&mut app, Message::ContextHelp(Topic::Nesting));
assert!(app.context_help);
send(&mut app, Message::FieldReferenceOpen(request));
assert!(!app.reference_input_active());
send(&mut app, Message::Escape);
assert!(!app.context_help);
assert_eq!(
serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(),
before
);
}
#[test]
fn context_help_accepts_live_process_viewport_and_rejects_stale_measurements() {
use crate::behavior::topology::CanvasEvent;
let mut project = Project::empty();
let owner = project
.add_element(Kind::Process, Layer::Oa, "Response process", None)
.unwrap();
let mut app = App::with_project(project);
send(&mut app, Message::Select(owner));
send(&mut app, Message::Page(Page::Behavior));
let context = app.topology_context().unwrap();
send(
&mut app,
Message::TopologyCanvas(CanvasEvent::Viewport(context, iced::Size::new(640., 480.))),
);
let initial = app.behavior.topology.camera_boundary();
let before = app.document.snapshot();
send(&mut app, Message::Help);
assert!(app.context_help);
assert_eq!(app.guide.topic, Topic::Chains);
let resized = iced::Size::new(420., 330.);
let context = app.topology_context().unwrap();
send(
&mut app,
Message::TopologyCanvas(CanvasEvent::Viewport(context, resized)),
);
let measured = app.behavior.topology.camera_boundary();
assert!(
measured != initial,
"Help must admit passive viewport measurements"
);
assert!(app.context_help);
let epoch = app.canvas_epoch;
for field in 0..3 {
let mut stale = app.topology_context().unwrap();
match field {
0 => stale.document = Id::new_v4(),
1 => stale.owner = Id::new_v4(),
_ => stale.revision += 1,
}
send(
&mut app,
Message::TopologyCanvas(CanvasEvent::Viewport(stale, iced::Size::new(999., 777.))),
);
assert!(app.behavior.topology.camera_boundary() == measured);
assert_eq!(app.canvas_epoch, epoch);
}
send(&mut app, Message::CloseContextHelp);
assert!(!app.context_help);
assert!(app.behavior.topology.camera_boundary() == measured);
let context = app.topology_context().unwrap();
let epoch = app.canvas_epoch;
send(
&mut app,
Message::TopologyCanvas(CanvasEvent::Viewport(context, resized)),
);
assert_eq!(
app.canvas_epoch, epoch,
"The size observed behind Help is already current"
);
let context = app.topology_context().unwrap();
send(
&mut app,
Message::TopologyCanvas(CanvasEvent::Viewport(context, iced::Size::new(700., 500.))),
);
assert!(app.behavior.topology.camera_boundary() != measured);
assert_eq!(app.page, Page::Behavior);
assert_eq!(app.selected, owner);
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
assert!(!app.document.can_undo());
}
async fn help_key(
mounted: &mut crate::behavior::canvas_workspace::tests::Mounted,
app: &mut App,
event: iced::Event,
) -> Vec<Message> {
let (mut messages, statuses) = mounted.input_with_status(
app,
std::slice::from_ref(&event),
iced::mouse::Cursor::Unavailable,
);
super::escape::tests::subscriptions(app, event, statuses[0], &mut messages);
for message in messages.iter().cloned() {
let task = app.update(message);
mounted.perform(app, task).await;
}
messages
}
#[tokio::test]
async fn mounted_context_help_keyboard_scroll_preserves_inspector_focus_and_exact_caret() {
use crate::behavior::canvas_workspace::tests::{Mounted, character, key};
use iced::keyboard::key::Named;
let (mut app, diagram, a, _) = architecture();
let element = app.project().diagrams[&diagram].nodes[&a].element;
send(&mut app, Message::Select(element));
let before = app.document.snapshot();
let mut mounted = Mounted::new(&mut app).await;
let field = mounted
.geometry(&app)
.text_inputs
.into_iter()
.filter(|bounds| bounds.x > app.window_size.width - 320.)
.min_by(|a, b| a.y.total_cmp(&b.y))
.expect("visible Inspector Name input");
mounted.click(&mut app, field.center()).await;
help_key(&mut mounted, &mut app, key(Named::Home)).await;
for character_value in "DRAFT ".chars() {
help_key(
&mut mounted,
&mut app,
character(&character_value.to_string()),
)
.await;
}
help_key(&mut mounted, &mut app, key(Named::ArrowLeft)).await;
assert_eq!(
app.draft.as_ref().unwrap().inputs["/name"].raw,
"DRAFT Send"
);
let camera = (app.zoom, app.pan);
help_key(&mut mounted, &mut app, key(Named::F1)).await;
assert!(app.context_help);
let article = iced::advanced::widget::Id::new(crate::guide::ARTICLE_SCROLL_ID);
let geometry = mounted.geometry(&app);
let background_scrolls = geometry
.scrolls
.iter()
.filter(|(id, _)| **id != article)
.map(|(id, (_, offset))| (id.clone(), *offset))
.collect::<Vec<_>>();
assert_eq!(geometry.scrolls[&article].1.y, 0.);
help_key(&mut mounted, &mut app, key(Named::PageDown)).await;
let down = mounted.geometry(&app).scrolls[&article].1.y;
assert!(down > 0., "PageDown scrolls the mounted Help article");
help_key(&mut mounted, &mut app, key(Named::End)).await;
let end = mounted.geometry(&app).scrolls[&article].1.y;
assert!(end > down);
help_key(&mut mounted, &mut app, key(Named::PageDown)).await;
assert_eq!(mounted.geometry(&app).scrolls[&article].1.y, end);
help_key(&mut mounted, &mut app, key(Named::PageUp)).await;
assert!(mounted.geometry(&app).scrolls[&article].1.y < end);
help_key(&mut mounted, &mut app, key(Named::Home)).await;
assert_eq!(mounted.geometry(&app).scrolls[&article].1.y, 0.);
help_key(&mut mounted, &mut app, key(Named::ArrowDown)).await;
assert_eq!(mounted.geometry(&app).scrolls[&article].1.y, 40.);
help_key(&mut mounted, &mut app, key(Named::ArrowUp)).await;
help_key(&mut mounted, &mut app, key(Named::PageUp)).await;
let geometry = mounted.geometry(&app);
assert_eq!(geometry.scrolls[&article].1.y, 0.);
for (id, offset) in background_scrolls {
assert_eq!(
geometry.scrolls[&id].1, offset,
"Help only scrolls its own article"
);
}
for character_value in "IGNORED".chars() {
help_key(
&mut mounted,
&mut app,
character(&character_value.to_string()),
)
.await;
}
let messages = mounted
.batch(
&mut app,
&[
iced::Event::InputMethod(iced::advanced::input_method::Event::Preedit(
"雨".into(),
None,
)),
iced::Event::InputMethod(iced::advanced::input_method::Event::Commit("雨".into())),
],
iced::mouse::Cursor::Unavailable,
)
.await;
assert!(!messages.iter().any(|m| matches!(m, Message::Field(..))));
assert_eq!(
app.draft.as_ref().unwrap().inputs["/name"].raw,
"DRAFT Send"
);
help_key(&mut mounted, &mut app, key(Named::Escape)).await;
assert!(!app.context_help);
help_key(&mut mounted, &mut app, character("X")).await;
assert_eq!(
app.draft.as_ref().unwrap().inputs["/name"].raw,
"DRAFTX Send",
"Return keeps the actual mid-string caret without refocusing a guessed field"
);
assert_eq!((app.zoom, app.pan), camera);
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
let messages = help_key(&mut mounted, &mut app, key(Named::PageDown)).await;
assert!(
!messages
.iter()
.any(|m| matches!(m, Message::ContextHelpScroll(_)))
);
assert!(
iced_runtime::task::into_stream(
app.update(Message::ContextHelpScroll(crate::guide::ContextScroll::End,))
)
.is_none(),
"A late Help scroll cannot operate after closing"
);
}
#[tokio::test]
async fn mounted_context_help_observes_modifier_release_before_plain_canvas_click() {
use crate::behavior::canvas_workspace::tests::{Mounted, key};
use iced::{
keyboard::{Event, Modifiers, key::Named},
mouse,
};
let (mut app, diagram, a, b) = architecture();
choose(&mut app, a, multiselect::Choice::Replace);
app.window_size = iced::Size::new(2000., 1400.);
let mut mounted = Mounted::new(&mut app).await;
app.zoom = 1.;
app.pan = iced::Vector::ZERO;
app.fit_to_view = false;
mounted.settle(&mut app).await;
mounted
.batch(
&mut app,
&[iced::Event::Keyboard(Event::ModifiersChanged(
Modifiers::SHIFT,
))],
mouse::Cursor::Unavailable,
)
.await;
help_key(&mut mounted, &mut app, key(Named::F1)).await;
assert!(app.context_help);
mounted
.batch(
&mut app,
&[iced::Event::Keyboard(Event::ModifiersChanged(
Modifiers::empty(),
))],
mouse::Cursor::Unavailable,
)
.await;
help_key(&mut mounted, &mut app, key(Named::Escape)).await;
let bounds = mounted
.geometry(&app)
.bounds(crate::allocation_input::CANVAS_ID);
let node = projection::project_view(app.project(), diagram)
.nodes
.into_iter()
.find(|node| node.id == b)
.unwrap();
let at = iced::Point::new(
bounds.x + node.position.x + 40.,
bounds.y + node.position.y + 35.,
);
mounted.click(&mut app, at).await;
assert_eq!(
app.multiselect_snapshot().unwrap().targets(),
&[Target::Placement(b)],
"Shift released during Help must not add the next card to the previous selection"
);
}
#[tokio::test]
async fn mounted_context_help_retires_held_pan_without_replaying_its_release() {
use crate::behavior::canvas_workspace::tests::{Mounted, key};
use iced::{keyboard::key::Named, mouse};
let (mut app, _, _, _) = architecture();
let mut mounted = Mounted::new(&mut app).await;
let canvas = mounted
.geometry(&app)
.bounds(crate::allocation_input::CANVAS_ID);
let start = canvas.center();
let moved = start + iced::Vector::new(20., 10.);
mounted
.batch(
&mut app,
&[
iced::Event::Mouse(mouse::Event::CursorMoved { position: start }),
iced::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)),
iced::Event::Mouse(mouse::Event::CursorMoved { position: moved }),
],
mouse::Cursor::Available(moved),
)
.await;
let camera = (app.zoom, app.pan);
let before = app.document.snapshot();
help_key(&mut mounted, &mut app, key(Named::F1)).await;
assert!(app.context_help);
mounted
.batch(
&mut app,
&[iced::Event::Mouse(mouse::Event::ButtonReleased(
mouse::Button::Middle,
))],
mouse::Cursor::Unavailable,
)
.await;
help_key(&mut mounted, &mut app, key(Named::Escape)).await;
let next = moved + iced::Vector::new(90., 50.);
let messages = mounted
.batch(
&mut app,
&[iced::Event::Mouse(mouse::Event::CursorMoved {
position: next,
})],
mouse::Cursor::Available(next),
)
.await;
assert!(!messages.iter().any(|m| matches!(m, Message::Pan(_))));
assert_eq!(
(app.zoom, app.pan),
camera,
"Moving without a held button cannot resume the pre-Help pan"
);
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
}
#[tokio::test]
async fn context_help_barrier_retires_text_selection_drag_without_changing_the_caret() {
use iced::advanced::{Layout, layout, widget};
use iced::{Element, Event, Rectangle, mouse};
type InputState = iced::widget::text_input::State<
<iced::Renderer as iced::advanced::text::Renderer>::Paragraph,
>;
fn input(open: bool) -> Element<'static, Message> {
crate::guide::protect_workspace(
open,
iced::widget::text_input("Name", "A long retained property name")
.on_input(|value| Message::Field("/name".into(), value))
.width(400)
.padding(8)
.into(),
)
}
fn dispatch(
root: &mut Element<'_, Message>,
tree: &mut widget::Tree,
node: &layout::Node,
renderer: &iced::Renderer,
event: Event,
cursor: mouse::Cursor,
) -> Vec<Message> {
let mut messages = Vec::new();
root.as_widget_mut().update(
tree,
&event,
Layout::new(node),
cursor,
renderer,
&mut iced::advanced::clipboard::Null,
&mut iced::advanced::Shell::new(&mut messages),
&Rectangle::new(iced::Point::ORIGIN, iced::Size::new(400., 60.)),
);
messages
}
let mounted = crate::behavior::canvas_workspace::tests::Mounted::unshown().await;
let renderer = &mounted.renderer;
let mut root = input(false);
let mut tree = widget::Tree::new(root.as_widget());
let node = root.as_widget_mut().layout(
&mut tree,
renderer,
&layout::Limits::new(iced::Size::ZERO, iced::Size::new(400., 60.)),
);
let start = iced::Point::new(20., 18.);
let end = iced::Point::new(110., 18.);
for event in [
mouse::Event::CursorMoved { position: start },
mouse::Event::ButtonPressed(mouse::Button::Left),
mouse::Event::CursorMoved { position: end },
] {
let cursor = if matches!(event, mouse::Event::CursorMoved { position } if position == end) {
mouse::Cursor::Available(end)
} else {
mouse::Cursor::Available(start)
};
dispatch(
&mut root,
&mut tree,
&node,
renderer,
Event::Mouse(event),
cursor,
);
}
let caret = tree.children[0].state.downcast_ref::<InputState>().cursor();
assert!(
tree.children[0]
.state
.downcast_ref::<InputState>()
.is_focused()
);
root = input(true);
tree.diff(root.as_widget());
let messages = dispatch(
&mut root,
&mut tree,
&node,
renderer,
Event::Window(iced::window::Event::RedrawRequested(
std::time::Instant::now(),
)),
mouse::Cursor::Unavailable,
);
assert!(
messages.is_empty(),
"Gesture retirement discards release actions"
);
dispatch(
&mut root,
&mut tree,
&node,
renderer,
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
mouse::Cursor::Unavailable,
);
root = input(false);
tree.diff(root.as_widget());
let later = iced::Point::new(300., 18.);
dispatch(
&mut root,
&mut tree,
&node,
renderer,
Event::Mouse(mouse::Event::CursorMoved { position: later }),
mouse::Cursor::Available(later),
);
let state = tree.children[0].state.downcast_ref::<InputState>();
assert!(state.is_focused());
assert_eq!(
state.cursor(),
caret,
"An unheld pointer no longer extends the pre-Help text selection"
);
}
+1 -2
View File
@@ -14,8 +14,7 @@ pub(crate) fn root<'a, R: renderer::Renderer + 'a>(
content: Element<'a, Message, Theme, R>,
) -> Element<'a, Message, Theme, R> {
let modal = app.text_workspace.menu().is_none()
&& (app.context_help
|| app.behavior.creation.has_pending()
&& (app.behavior.creation.has_pending()
|| app.diagram_usages.is_some()
|| app.behavior.source_active()
|| (app.page == Page::Text
-143
View File
@@ -1,148 +1,5 @@
use super::*;
fn saved_multifile_app(directory: &std::path::Path) -> (App, Id, PathBuf) {
let mut project = Project::empty();
let activity = project
.add_element(Kind::Activity, Layer::Oa, "Assess incident", None)
.unwrap();
let mut app = App::with_project(project);
let path = directory.join("project.arcadia");
complete_save(&mut app, path.clone());
assert!(app.document.bundle().files.len() > 1);
(app, activity, path)
}
fn rejected_save_preserves_editor(app: &mut App, path: PathBuf) -> String {
let snapshot = app.document.snapshot();
let revision = app.document.revision();
let undo = app.document.can_undo();
let redo = app.document.can_redo();
let draft = app.source_workspace.text().to_owned();
let original_path = app.path.clone();
let dirty = app.dirty();
let result = write_snapshot(
app.canvas_document,
revision,
snapshot.clone(),
path,
app.disk_project.clone(),
);
let error = match result {
Err(error) => error,
Ok(_) => panic!("Expected collision refusal"),
};
let _ = app.update(Message::Saved(Err(error.clone())));
assert!(Arc::ptr_eq(&snapshot, &app.document.snapshot()));
assert_eq!(app.document.revision(), revision);
assert_eq!(app.document.can_undo(), undo);
assert_eq!(app.document.can_redo(), redo);
assert_eq!(app.source_workspace.text(), draft);
assert_eq!(app.path, original_path);
assert_eq!(app.dirty(), dirty);
error.message
}
#[test]
fn same_root_new_entry_collision_preserves_files_draft_and_history() {
let directory = tempfile::tempdir().unwrap();
let (mut app, activity, original) = saved_multifile_app(directory.path());
let bytes: Vec<_> = app
.document
.bundle()
.files
.keys()
.map(|p| (p.clone(), std::fs::read(directory.path().join(p)).unwrap()))
.collect();
app.execute(Command::Rename(activity, "Review incident".into()));
app.execute(Command::Rename(activity, "Evaluate incident".into()));
let _ = app.update(Message::Undo);
assert!(app.document.can_undo() && app.document.can_redo());
let cursor = app.source_workspace.files[&app.source_workspace.entry].cursor;
app.source_workspace.edit(
format!("{}\n// retained draft", app.source_workspace.text()),
cursor,
);
let destination = directory.path().join("renamed.arcadia");
let error = rejected_save_preserves_editor(&mut app, destination.clone());
assert!(error.contains("already exist") && error.contains("Save All"));
assert!(!error.contains("changed outside"));
assert!(!destination.exists());
assert_eq!(app.path, Some(original));
for (path, expected) in bytes {
assert_eq!(
std::fs::read(directory.path().join(path)).unwrap(),
expected
);
}
}
#[test]
fn fresh_root_existing_include_is_protected_before_any_bundle_write() {
let original = tempfile::tempdir().unwrap();
let destination = tempfile::tempdir().unwrap();
let (mut app, activity, _) = saved_multifile_app(original.path());
app.execute(Command::Rename(activity, "Review incident".into()));
let fragment = app.document.compiled().index.declarations[&activity]
.path
.clone();
let collision = destination.path().join(&fragment);
std::fs::create_dir_all(collision.parent().unwrap()).unwrap();
std::fs::write(&collision, b"// independent destination work").unwrap();
let target = destination.path().join("copy.arcadia");
let error = rejected_save_preserves_editor(&mut app, target.clone());
assert!(error.contains(&fragment) && error.contains("fresh directory"));
assert!(!error.contains("changed outside"));
assert!(!target.exists());
for path in app.document.bundle().files.keys() {
let file = destination.path().join(path);
if path == &fragment {
assert_eq!(
std::fs::read(file).unwrap(),
b"// independent destination work"
);
} else {
assert!(!file.exists(), "Unexpected write: {path}");
}
}
}
#[test]
fn observed_dependency_change_remains_external_conflict_and_normal_saves_work() {
let original = tempfile::tempdir().unwrap();
let fresh = tempfile::tempdir().unwrap();
let (mut app, activity, path) = saved_multifile_app(original.path());
app.execute(Command::Rename(activity, "Review incident".into()));
complete_save(&mut app, path.clone());
assert!(!app.dirty());
let fragment = app.document.compiled().index.declarations[&activity]
.path
.clone();
let entry_before = std::fs::read(&path).unwrap();
let original_fragment = std::fs::read(original.path().join(&fragment)).unwrap();
std::fs::write(original.path().join(&fragment), b"// external edit").unwrap();
app.execute(Command::Rename(activity, "Evaluate incident".into()));
let error = rejected_save_preserves_editor(&mut app, path.clone());
assert!(error.contains("changed outside") && error.contains(&fragment));
assert!(!error.contains("fresh directory"));
assert_eq!(std::fs::read(&path).unwrap(), entry_before);
assert_eq!(
std::fs::read(original.path().join(&fragment)).unwrap(),
b"// external edit"
);
// Restore the observed bytes, then exercise the legitimate existing Save All.
std::fs::write(original.path().join(&fragment), original_fragment).unwrap();
complete_save(&mut app, path);
assert!(!app.dirty());
let model = app.project().clone();
let target = fresh.path().join("copy.arcadia");
complete_save(&mut app, target.clone());
let saved = source_files::load_project(&target, LIMITS).unwrap();
let compiled = source_project::compile_bundle(&saved.bundle, LIMITS).unwrap();
assert_eq!(compiled.decoded.project, model);
assert_eq!(&saved.bundle, app.document.bundle());
assert!(!app.dirty());
}
fn complete_save(app: &mut App, path: PathBuf) {
let receipt = write_snapshot(
app.canvas_document,
@@ -16,7 +16,6 @@ pub fn keyboard_enabled(app: &crate::app::App) -> bool {
/// Passive I/O suspends commands without changing the logical input owner.
pub(crate) fn keyboard_owner_enabled(app: &crate::app::App) -> bool {
!app.hosted.visible
&& !app.context_help
&& app.workflow.is_none()
&& app.file_prompt.is_none()
&& !app.delete_pending
@@ -1497,9 +1497,7 @@ impl StateCanvas<'_> {
format!("{} · {} · right-click to edit", r.name, routing::state_transition_summary(self.project, r))
}
}).unwrap_or_default(),
// General gestures are available in Help; keep this stable slot
// for the active operation or the item under the pointer.
_ => String::new(),
_ => "Select / move · drag a vertex · drag selected transition endpoints to reconnect · middle-drag to pan".into(),
},
}
}
+9 -9
View File
@@ -225,13 +225,13 @@ fn selection_controls(app: &App) -> Element<'_, Message> {
}
controls = controls
.push(action(
"Clear",
"Clear selection",
Message::DiagramSelection(SelectionEvent::Clear(snapshot.context())),
))
.push(
label(format!(
"Properties: {} only",
crate::views::short(app.project().named(app.selected), 10)
"Properties edit only {}",
crate::views::short(app.project().named(app.selected), 18)
))
.wrapping(iced::widget::text::Wrapping::None),
);
@@ -241,12 +241,12 @@ fn selection_controls(app: &App) -> Element<'_, Message> {
} else {
"Use Select / move to select multiple elements"
};
controls
.push(crate::views::diagram_help(
crate::guide::Topic::States,
hint,
))
.into()
column![
controls,
label(hint).wrapping(iced::widget::text::Wrapping::None)
]
.height(56)
.into()
}
fn transition_details(machine: Id, selected: Id) -> Element<'static, Message> {
@@ -585,13 +585,15 @@ fn selection_controls(app: &App) -> Element<'_, Message> {
.wrapping(iced::widget::text::Wrapping::None),
);
}
// Keep the row even with an empty set so selection never moves the canvas.
controls
.push(crate::views::diagram_help(
crate::guide::Topic::Chains,
"Shift-click toggles selection. Drag selected local cards to reorder; reused previews are read-only.",
))
.into()
// These two rows exist even with an empty set so selection never moves the
// canvas viewport or changes the camera's interpretation of the pointer.
column![
controls,
label("Shift+click toggles · drag selected cards to reorder · previews are read-only")
.wrapping(iced::widget::text::Wrapping::None)
]
.height(56)
.into()
}
fn editor_panel<'a, R: iced::advanced::text::Renderer + 'a>(
-744
View File
@@ -1,744 +0,0 @@
//! Private read-only optical comparison. Normal startup never constructs this runner.
mod inert;
#[cfg(test)]
mod tests;
use crate::{app::App, diagram::DiagramCanvas, style};
use iced::{
Element, Fill, Rectangle, Renderer, Size, Task, Theme, Vector, mouse,
widget::{self, button, canvas, column, container, row, text},
};
use oa_core::{Id, Kind, Layer, Project, projection, routing};
use serde_json::{Value, json};
use std::{
cell::RefCell,
fs,
io::Write,
path::{Path, PathBuf},
rc::Rc,
};
type Result<T> = std::result::Result<T, String>;
type Mailbox = Rc<RefCell<Option<DrawStamp>>>;
pub(crate) fn requested(args: &[String]) -> bool {
args.get(1).is_some_and(|arg| arg == "--focus-optical")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Variant {
Baseline,
Focus,
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct Camera {
zoom: f32,
pan: Vector,
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct DrawStamp {
project: Id,
diagram: Id,
variant: Variant,
generation: u64,
viewport: Size,
camera: Camera,
}
#[derive(Debug, Clone)]
enum Message {
Variant(Variant),
Fit,
Capture,
Measured(u64, Size),
Drawn(DrawStamp),
Header(u64, Size),
Opened(iced::window::Id, Size),
WindowSize(Size),
Scale(f32),
Close(iced::window::Id),
UnexpectedPassive(String),
}
#[derive(Clone)]
struct Frozen {
diagram: Id,
project: Project,
projected: projection::ProjectedView,
routed: routing::RoutedView,
svg: String,
report: Value,
}
impl Frozen {
fn new(project: Project, diagram: Id) -> Result<Self> {
let Some(saved) = project.diagrams.get(&diagram) else {
return Err("The named diagram does not exist.".into());
};
if !saved.projection.is_architecture() || saved.projection.scenario().is_some() {
return Err("The optical comparison accepts saved Architecture diagrams only.".into());
}
let projected = projection::project_diagram(&project, saved);
let routed = routing::route_diagram(&project, saved, &projected);
let svg = crate::diagram::export_svg(&project, diagram).map_err(|e| e.to_string())?;
let report = scene_report(&project, diagram, &projected, &routed);
Ok(Self {
diagram,
project,
projected,
routed,
svg,
report,
})
}
fn check(&self, app: &App, diagram: Id) -> Result<()> {
if app.project() != &self.project {
return Err("Frozen complete Project changed.".into());
}
let saved = &app.project().diagrams[&diagram];
let projected =
projection::project_diagram_with_index(app.project(), saved, app.document.view_index());
let routed = routing::route_diagram(app.project(), saved, &projected);
let svg = crate::diagram::export_svg(app.project(), diagram).map_err(|e| e.to_string())?;
if projected != self.projected || routed != self.routed || svg != self.svg {
return Err("Frozen projection, complete routed geometry or SVG changed.".into());
}
if app.active_diagram != Some(diagram)
|| app.layer != saved.layer
|| app.page != crate::app::Page::Diagram
|| app.selected != app.project().id
|| app.diagram_selection.is_some()
|| app.connection.is_some()
|| app.architecture.is_some()
{
return Err("Optical presentation fixture is no longer neutral.".into());
}
if app.document.dirty()
|| app.document.can_undo()
|| app.document.can_redo()
|| app.draft_dirty()
|| app.source_dirty()
|| app.context_help
|| app.busy
|| app.pending.is_some()
|| app.model_menu.is_some()
|| app.file_prompt.is_some()
|| app.diagram_navigation_focus.is_some()
|| app.diagram_usages.is_some()
{
return Err(
"Optical fixture acquired draft, history, modal or navigation state.".into(),
);
}
Ok(())
}
}
fn scene_report(
project: &Project,
diagram: Id,
projected: &projection::ProjectedView,
routed: &routing::RoutedView,
) -> Value {
let label = |l: &routing::EdgeLabelGeometry| {
json!({
"text": l.text, "fraction": l.fraction, "anchor": l.anchor, "center": l.center,
"bounds": {"position": l.bounds.position, "size": l.bounds.size}
})
};
let edges: Vec<_> = routed
.edges
.values()
.map(|edge| {
let ends = project
.relations
.get(&edge.id)
.map(|relation| {
routing::association_end_geometry(relation, edge).iter().map(|end| json!({
"source_end": end.source_end, "label": label(&end.label), "tip": end.tip,
"inward": end.inward, "navigable": end.navigable
})).collect::<Vec<_>>()
})
.unwrap_or_default();
json!({"id": edge.id, "points": edge.points, "label": label(&edge.label),
"pins": edge.pins, "pin_fractions": edge.pin_fractions,
"warnings": edge.warnings.iter().map(|w| format!("{w:?}")).collect::<Vec<_>>(),
"custom_suspended": edge.custom_suspended, "association_ends": ends})
})
.collect();
json!({"diagram": diagram, "projected": projected,
"routed": {"bounds": {"position": routed.bounds.position, "size": routed.bounds.size}, "edges": edges},
"complete_routed_debug": format!("{routed:#?}"),
"debug_scope": "Includes private derived routing fields; complete RoutedView PartialEq is also checked on every capture."})
}
fn fit(bounds: routing::RoutingBounds, empty: bool, viewport: Size) -> Camera {
if empty {
return Camera {
zoom: 1.,
pan: Vector::new(30., 30.),
};
}
let width = (viewport.width - 48.).max(1.);
let height = (viewport.height - 48.).max(1.);
// Preserve production f32 operation order, including origin addition/subtraction.
let left = bounds.position.x;
let top = bounds.position.y;
let right = left + bounds.size.width;
let bottom = top + bounds.size.height;
let zoom = (width / (right - left).max(1.))
.min(height / (bottom - top).max(1.))
.clamp(0.0001, 1.5);
Camera {
zoom,
pan: Vector::new(
24. + (width - (right - left) * zoom) / 2. - left * zoom,
24. + (height - (bottom - top) * zoom) / 2. - top * zoom,
),
}
}
fn valid_size(size: Size) -> bool {
size.width.is_finite() && size.height.is_finite() && size.width > 0. && size.height > 0.
}
struct LayoutState {
variant: Variant,
generation: u64,
expected: Option<DrawStamp>,
acknowledged: Option<DrawStamp>,
}
impl Default for LayoutState {
fn default() -> Self {
Self {
variant: Variant::Baseline,
generation: 0,
expected: None,
acknowledged: None,
}
}
}
impl LayoutState {
fn invalidate(&mut self) {
self.generation = self.generation.wrapping_add(1);
self.expected = None;
self.acknowledged = None;
}
fn measured(&mut self, generation: u64, viewport: Size, frozen: &Frozen) -> bool {
if generation != self.generation
|| !valid_size(viewport)
|| self
.expected
.is_some_and(|stamp| stamp.viewport == viewport)
{
return false;
}
// A genuine canvas resize can happen without a native WindowResized.
if self.expected.is_some() {
self.invalidate();
}
self.expected = Some(DrawStamp {
project: frozen.project.id,
diagram: frozen.diagram,
variant: self.variant,
generation: self.generation,
viewport,
camera: fit(
frozen.routed.bounds,
frozen.projected.nodes.is_empty(),
viewport,
),
});
self.acknowledged = None;
true
}
fn drawn(&mut self, stamp: DrawStamp) -> bool {
if Some(stamp) != self.expected || self.acknowledged == Some(stamp) {
return false;
}
self.acknowledged = Some(stamp);
true
}
fn ready(&self) -> bool {
self.expected.is_some() && self.expected == self.acknowledged
}
}
/// No update override: no interactive canvas state can ever be mutated here.
struct OpticalCanvas<'a> {
program: DiagramCanvas<'a>,
stamp: Option<DrawStamp>,
drawn: Mailbox,
}
impl canvas::Program<crate::app::Message> for OpticalCanvas<'_> {
type State = crate::diagram::State;
fn draw(
&self,
state: &Self::State,
renderer: &Renderer,
theme: &Theme,
bounds: Rectangle,
_: mouse::Cursor,
) -> Vec<canvas::Geometry> {
let geometry = <DiagramCanvas<'_> as canvas::Program<crate::app::Message>>::draw(
&self.program,
state,
renderer,
theme,
bounds,
mouse::Cursor::Unavailable,
);
if let Some(stamp) = self.stamp.filter(|s| {
s.viewport == bounds.size()
&& s.camera.zoom == self.program.zoom
&& s.camera.pan == self.program.pan
}) {
*self.drawn.borrow_mut() = Some(stamp);
}
geometry
}
}
struct Runner {
fixture: App,
frozen: Frozen,
diagram: Id,
input_path: PathBuf,
input: Vec<u8>,
output: PathBuf,
layout: LayoutState,
drawn: Mailbox,
header: Option<(u64, Size)>,
scale_factor: Option<f32>,
capture: u32,
error: Option<String>,
}
fn fixture(project: Project, diagram: Id) -> Result<App> {
// Preflight the same conversion used by with_project so invalid input is a visible error.
let document =
crate::document::Document::from_project(project.clone(), crate::source_workspace::LIMITS)
.map_err(|e| e.to_string())?;
if document.project() != &project {
return Err("Document admission changed the complete Project.".into());
}
let layer = project.diagrams[&diagram].layer;
let mut app = App::with_project(project);
app.active_diagram = Some(diagram);
app.layer = layer;
app.page = crate::app::Page::Diagram;
app.explorer_index = crate::explorer::Index::new(app.project(), layer);
app.create_kind = if layer == Layer::Oa {
Kind::Activity
} else {
Kind::Function
};
app.architecture = None;
app.architecture_palette_expanded = false;
app.connection = None;
app.selected = app.project().id;
app.selected_node = None;
app.diagram_selection = None;
app.selected_attachment = None;
app.selected_bend = None;
app.draft = crate::forms::Draft::from_project(app.project(), app.selected);
app.diagram_navigation_focus = None;
app.diagram_usages = None;
app.status =
"READ-ONLY OPTICAL EXPERIMENT · Waiting for measured canvas and draw acknowledgment".into();
Ok(app)
}
impl Runner {
fn invalidate(&mut self) {
self.layout.invalidate();
self.header = None;
*self.drawn.borrow_mut() = None;
self.fixture.status =
"READ-ONLY · Waiting for measured canvas and draw acknowledgment".into();
}
fn update(&mut self, message: Message) -> Task<Message> {
// A fault is terminal for this evidence session. Queued redraw/capture
// messages cannot restore readiness or write another successful capture.
if self.error.is_some() && !matches!(&message, Message::Close(_)) {
return Task::none();
}
match message {
Message::Variant(variant) if variant != self.layout.variant => {
self.layout.variant = variant;
self.invalidate();
}
Message::Fit => self.invalidate(),
Message::Measured(generation, size) => {
if self.layout.measured(generation, size, &self.frozen) {
let stamp = self.layout.expected.unwrap();
self.fixture.zoom = stamp.camera.zoom;
self.fixture.pan = stamp.camera.pan;
*self.drawn.borrow_mut() = None;
self.fixture.status =
"READ-ONLY · Fit measured; waiting for matching canvas draw".into();
}
}
Message::Drawn(stamp) => {
if self.layout.drawn(stamp) {
self.fixture.status = format!(
"READ-ONLY · {:?} ready · {:.1} × {:.1} · Fit {:.5} · Capture records draw acknowledgment, not GPU completion",
stamp.variant,
stamp.viewport.width,
stamp.viewport.height,
stamp.camera.zoom
);
}
}
Message::Header(generation, size)
if generation == self.layout.generation && valid_size(size) =>
{
self.header = Some((generation, size));
}
Message::WindowSize(size) if valid_size(size) && size != self.fixture.window_size => {
self.fixture.window_size = size;
self.invalidate();
}
Message::Opened(id, size) => {
if valid_size(size) && size != self.fixture.window_size {
self.fixture.window_size = size;
self.invalidate();
}
return iced::window::scale_factor(id).map(Message::Scale);
}
Message::Scale(scale)
if scale.is_finite() && scale > 0. && self.scale_factor != Some(scale) =>
{
self.scale_factor = Some(scale);
self.invalidate();
}
Message::Capture => {
if !self.layout.ready()
|| *self.drawn.borrow() != self.layout.expected
|| self.header.is_none_or(|(g, _)| g != self.layout.generation)
|| self.scale_factor.is_none()
{
self.fixture.status =
"READ-ONLY · Capture refused: waiting for matching fitted draw".into();
} else if let Err(error) = self.capture() {
self.fail(error);
}
}
Message::UnexpectedPassive(reason) => self.fail(reason),
Message::Close(id) => {
let conservation = self.check_frozen();
if let Err(error) = &conservation {
self.fail(error.clone());
}
let succeeded = conservation.is_ok() && self.error.is_none();
let result = write_json(
&self.output.join("finished.json"),
&json!({
"status": if succeeded { "PASS" } else { "FAIL" },
"complete_project_scene_svg_and_input_unchanged": conservation.is_ok(),
"captures": self.capture, "error": self.error,
"scope": "Optical runner close; failed conservation or an earlier fault cannot produce PASS. No readability acceptance."
}),
);
if let Err(error) = result {
self.fail(error);
}
// A changed fixture or occupied receipt must never trap the
// user. Existing evidence is preserved; failures remain in stderr.
return iced::window::close(id);
}
_ => {}
}
Task::none()
}
fn fail(&mut self, error: String) {
eprintln!("Focus optical error: {error}");
self.error = Some(error);
self.layout.acknowledged = None;
}
fn check_frozen(&self) -> Result<()> {
self.frozen.check(&self.fixture, self.diagram)?;
if fs::read(&self.input_path).map_err(|e| e.to_string())? != self.input {
return Err("The original fixture bytes changed during the comparison.".into());
}
Ok(())
}
fn capture(&mut self) -> Result<()> {
if let Some(error) = &self.error {
return Err(format!(
"This optical evidence session has stopped: {error}"
));
}
if !self.layout.ready()
|| *self.drawn.borrow() != self.layout.expected
|| self.header.is_none_or(|(g, _)| g != self.layout.generation)
|| self.scale_factor.is_none()
{
return Err(
"Capture is pending a matching measured viewport, header, scale factor and draw."
.into(),
);
}
self.check_frozen()?;
let stamp = self.layout.expected.unwrap();
let next = self.capture + 1;
// One fresh directory makes partial failures visible without overwriting any capture.
let directory = self.output.join(format!("capture-{next:03}"));
fs::create_dir(&directory).map_err(|e| e.to_string())?;
write_json(
&directory.join("measurement.json"),
&json!({
"variant": format!("{:?}", stamp.variant), "generation": stamp.generation,
"viewport": {"width": stamp.viewport.width, "height": stamp.viewport.height},
"zoom": stamp.camera.zoom, "pan": {"x": stamp.camera.pan.x, "y": stamp.camera.pan.y},
"logical_client": {"width": self.fixture.window_size.width, "height": self.fixture.window_size.height},
"scale_factor": self.scale_factor, "scale_factor_source": "Iced window scale_factor task / Rescaled event",
"measured_header_height": self.header.unwrap().1.height,
"baseline_deviation": "Only shared header action group is replaced; original logo/project typography/padding and body chrome retained.",
"draw_acknowledged": true, "gpu_completion_claimed": false,
"screenshot": "Separate native capture required to establish displayed output.",
"selection": "project (no projected object)", "hover": false,
"palette_expanded": false, "diagram": self.diagram,
"complete_project_projection_routed_svg_equal": true,
"scope": "Fit-only read-only optical measurement; no reading or editing acceptance."
}),
)?;
write_json(
&directory.join("project.json"),
&serde_json::to_value(&self.frozen.project).map_err(|e| e.to_string())?,
)?;
write_json(&directory.join("scene.json"), &self.frozen.report)?;
write_new(&directory.join("diagram.svg"), self.frozen.svg.as_bytes())?;
self.capture = next;
self.fixture.status = format!(
"READ-ONLY · Captured {next:03} · {:?} · native screenshot still required",
stamp.variant
);
Ok(())
}
fn view(&self) -> Element<'_, Message> {
let generation = self.layout.generation;
let ready = self.layout.ready()
&& self.header.is_some_and(|(g, _)| g == generation)
&& self.scale_factor.is_some()
&& self.error.is_none();
let controls = row![
text("READ ONLY · EXPERIMENT").size(11),
button(text("Baseline").size(12))
.padding([7, 9])
.on_press(Message::Variant(Variant::Baseline)),
button(text("Focus").size(12))
.padding([7, 9])
.on_press(Message::Variant(Variant::Focus)),
button(text("Fit").size(12))
.padding([7, 9])
.on_press(Message::Fit),
button(text("Capture").size(12))
.padding([7, 9])
.on_press_maybe(ready.then_some(Message::Capture)),
]
.spacing(6)
.align_y(iced::Center);
let header: Element<'_, Message> = if self.layout.variant == Variant::Baseline {
crate::views::header(&self.fixture, controls.into())
} else {
container(
row![
text(crate::views::short(
&self.frozen.project.diagrams[&self.diagram].name,
38
))
.size(14),
widget::Space::new().width(Fill),
controls
]
.spacing(10)
.align_y(iced::Center),
)
.padding([6, 12])
.height(48)
.style(style::panel)
.into()
};
let header = widget::sensor(header)
.key(generation)
.on_show(move |size| Message::Header(generation, size))
.on_resize(move |size| Message::Header(generation, size));
if let Some(error) = &self.error {
return column![header, container(column![text("Optical comparison stopped").size(22), text(error).size(16),
text("No further capture is accepted. Inspect logs and preserve this failed session.").size(14)]
.spacing(16)).padding(24)].into();
}
let program = DiagramCanvas {
selection: None,
allocation: None,
project: self.fixture.project(),
view_index: Some(self.fixture.document.view_index()),
diagram: self.diagram,
selected: self.fixture.project().id,
navigation_focus: None,
zoom: self.fixture.zoom,
pan: self.fixture.pan,
context: self.fixture.route_context().unwrap(),
selected_bend: None,
selected_attachment: None,
editable: false,
placing: false,
connection: None,
};
let canvas: Element<'_, crate::app::Message> = widget::canvas(OpticalCanvas {
program,
stamp: self.layout.expected,
drawn: self.drawn.clone(),
})
.width(Fill)
.height(Fill)
.into();
let body = if self.layout.variant == Variant::Baseline {
crate::views::optical_baseline(&self.fixture, canvas)
} else {
widget::sensor(canvas)
.key(generation)
.on_show(crate::app::Message::CanvasResized)
.on_resize(crate::app::Message::CanvasResized)
.into()
};
let body = inert::wrap(body, generation, self.layout.expected, self.drawn.clone());
if self.layout.variant == Variant::Baseline {
column![header, body].into()
} else {
column![
header,
body,
container(
text(&self.fixture.status)
.size(11)
.wrapping(widget::text::Wrapping::None)
)
.padding([6, 12])
.height(28)
.style(style::panel)
]
.into()
}
}
}
fn write_new(path: &Path, data: &[u8]) -> Result<()> {
fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(path)
.and_then(|mut file| file.write_all(data))
.map_err(|e| format!("{}: {e}", path.display()))
}
fn write_json(path: &Path, value: &Value) -> Result<()> {
let mut bytes = serde_json::to_vec_pretty(value).map_err(|e| e.to_string())?;
bytes.push(b'\n');
write_new(path, &bytes)
}
pub(crate) fn run(args: &[String]) -> iced::Result {
let prepare = || -> Result<Runner> {
if args.len() != 3 {
return Err(
"usage: arcadia-studio --focus-optical PROJECT.json DIAGRAM_UUID FRESH_OUTPUT_DIR"
.into(),
);
}
let input_path = fs::canonicalize(&args[0]).map_err(|e| e.to_string())?;
let input = fs::read(&input_path).map_err(|e| e.to_string())?;
let raw: Value = serde_json::from_slice(&input).map_err(|e| e.to_string())?;
let decoded =
oa_core::persistence::decode_with_report(&input).map_err(|e| e.to_string())?;
let diagram: Id = args[1]
.parse()
.map_err(|e| format!("Invalid diagram UUID: {e}"))?;
let frozen = Frozen::new(decoded.project, diagram)?;
let fixture = fixture(frozen.project.clone(), diagram)?;
frozen.check(&fixture, diagram)?;
let output = PathBuf::from(&args[2]);
fs::create_dir(&output).map_err(|e| format!("Fresh output directory required: {e}"))?;
write_new(&output.join("input.json"), &input)?;
write_json(
&output.join("admission.json"),
&json!({
"input_path": input_path, "input_bytes": input.len(), "raw_schema": raw.get("schema_version"),
"admitted_schema": frozen.project.schema_version, "migration": decoded.migration,
"diagnostics": oa_core::validation::validate(&frozen.project),
"diagram": diagram, "projection": frozen.project.diagrams[&diagram].projection,
"layer": frozen.project.diagrams[&diagram].layer,
"document_equals_admitted_complete_project": true,
"hash_identity": "Parent pins input/executable hashes externally; original input bytes retained.",
"scope": "Existing schema migration occurs once at admission; both variants use the same frozen admitted Project."
}),
)?;
write_json(
&output.join("project.json"),
&serde_json::to_value(&frozen.project).map_err(|e| e.to_string())?,
)?;
write_json(&output.join("scene.json"), &frozen.report)?;
write_new(&output.join("diagram.svg"), frozen.svg.as_bytes())?;
Ok(Runner {
fixture,
frozen,
diagram,
input_path,
input,
output,
layout: LayoutState::default(),
drawn: Rc::new(RefCell::new(None)),
header: None,
scale_factor: None,
capture: 0,
error: None,
})
};
let runner = match prepare() {
Ok(runner) => runner,
Err(error) => {
eprintln!("Focus optical admission failed: {error}");
std::process::exit(2);
}
};
let boot = RefCell::new(Some(runner));
iced::application(
move || {
(
boot.borrow_mut().take().expect("Optical runner boots once"),
iced::window::oldest().and_then(|id| {
iced::window::size(id).map(move |size| Message::Opened(id, size))
}),
)
},
Runner::update,
Runner::view,
)
.title("ARCADIA · private read-only optical comparison")
.theme(|_: &Runner| style::theme())
.subscription(|_: &Runner| {
iced::event::listen_with(|event, _, id| match event {
iced::Event::Window(iced::window::Event::Opened { size, .. }) => {
Some(Message::Opened(id, size))
}
iced::Event::Window(iced::window::Event::Resized(size)) => {
Some(Message::WindowSize(size))
}
iced::Event::Window(iced::window::Event::Rescaled(scale)) => {
Some(Message::Scale(scale))
}
iced::Event::Window(iced::window::Event::CloseRequested) => Some(Message::Close(id)),
_ => None,
})
})
.window(iced::window::Settings {
size: Size::new(1440., 900.),
min_size: Some(Size::new(1100., 700.)),
exit_on_close_request: false,
..Default::default()
})
.run()
}
@@ -1,167 +0,0 @@
//! Pre-dispatch boundary: frozen chrome never receives interactive events/operations.
use super::{DrawStamp, Mailbox, Message};
use crate::app::Message as AppMessage;
use iced::advanced::{
Clipboard, Layout, Shell, Widget, layout, renderer,
widget::{Tree, tree},
};
use iced::{Element, Event, Length, Rectangle, Size, Theme, mouse};
pub(super) fn wrap<'a>(
content: Element<'a, AppMessage>,
generation: u64,
expected: Option<DrawStamp>,
drawn: Mailbox,
) -> Element<'a, Message> {
Element::new(Inert {
content,
generation,
expected,
drawn,
})
}
pub(super) struct Inert<'a, R: renderer::Renderer> {
pub(super) content: Element<'a, AppMessage, Theme, R>,
pub(super) generation: u64,
pub(super) expected: Option<DrawStamp>,
pub(super) drawn: Mailbox,
}
#[derive(Default)]
struct State {
generation: u64,
acknowledged: Option<DrawStamp>,
}
impl<R: renderer::Renderer> Widget<Message, Theme, R> for Inert<'_, R> {
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
}
fn state(&self) -> tree::State {
tree::State::new(State {
generation: self.generation,
acknowledged: None,
})
}
fn children(&self) -> Vec<Tree> {
vec![Tree::new(self.content.as_widget())]
}
fn diff(&self, tree: &mut Tree) {
let state = tree.state.downcast_mut::<State>();
if state.generation != self.generation {
// This rig owns no editable widget state. Remount so an unchanged-size
// Baseline sensor still reports after explicit Fit/generation change.
*state = State {
generation: self.generation,
acknowledged: None,
};
tree.children = self.children();
} else {
tree.diff_children(std::slice::from_ref(&self.content));
}
}
fn size(&self) -> Size<Length> {
self.content.as_widget().size()
}
fn size_hint(&self) -> Size<Length> {
self.content.as_widget().size_hint()
}
fn layout(&mut self, tree: &mut Tree, renderer: &R, limits: &layout::Limits) -> layout::Node {
self.content
.as_widget_mut()
.layout(&mut tree.children[0], renderer, limits)
}
// Default operate/overlay/mouse_interaction do not delegate into hidden controls.
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
_: mouse::Cursor,
renderer: &R,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
if matches!(
event,
Event::Window(iced::window::Event::Resized(_) | iced::window::Event::Rescaled(_))
) {
*self.drawn.borrow_mut() = None;
}
if !matches!(
event,
Event::Window(iced::window::Event::RedrawRequested(_))
) {
return;
}
let mut messages = Vec::new();
let mut passive = Shell::new(&mut messages);
self.content.as_widget_mut().update(
&mut tree.children[0],
event,
layout,
mouse::Cursor::Unavailable,
renderer,
clipboard,
&mut passive,
viewport,
);
shell.request_redraw_at(passive.redraw_request());
if passive.is_layout_invalid() {
shell.invalidate_layout();
}
if passive.are_widgets_invalid() {
shell.invalidate_widgets();
}
// Do not dispatch any production command, including an unexpected passive one.
for message in messages {
match message {
AppMessage::CanvasResized(size) => {
if self.expected.is_some_and(|stamp| stamp.viewport != size) {
*self.drawn.borrow_mut() = None;
}
shell.publish(Message::Measured(self.generation, size));
}
other => shell.publish(Message::UnexpectedPassive(format!(
"Frozen chrome emitted unexpected passive message {:?}",
std::mem::discriminant(&other)
))),
}
}
let state = tree.state.downcast_mut::<State>();
if let Some(stamp) = *self.drawn.borrow()
&& Some(stamp) == self.expected
&& state.acknowledged != Some(stamp)
{
state.acknowledged = Some(stamp);
shell.publish(Message::Drawn(stamp));
}
// draw runs after this update. One further redraw lets us acknowledge its
// exact receipt. Stop requesting once that receipt was delivered.
if self.expected.is_some() && state.acknowledged != self.expected {
shell.request_redraw();
}
}
fn draw(
&self,
tree: &Tree,
renderer: &mut R,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
_: mouse::Cursor,
viewport: &Rectangle,
) {
self.content.as_widget().draw(
&tree.children[0],
renderer,
theme,
style,
layout,
mouse::Cursor::Unavailable,
viewport,
);
}
}
@@ -1,612 +0,0 @@
use super::*;
use iced::advanced::{
Clipboard, Layout, Shell, Widget, layout, renderer,
widget::{Operation, Tree},
};
use iced::{Event, Length, Point};
use std::cell::Cell;
fn sample() -> Frozen {
let project = Project::demo();
let id = project
.diagrams
.values()
.find(|d| d.projection.is_architecture())
.unwrap()
.id;
Frozen::new(project, id).unwrap()
}
#[test]
fn fit_uses_both_axes_offsets_margin_clamp_and_empty_scene() {
let bounds = routing::RoutingBounds {
position: oa_core::Position { x: 54., y: 18. },
size: oa_core::Size {
width: 1506.,
height: 814.,
},
};
let width = fit(bounds, false, Size::new(826., 534.));
assert_eq!(width.zoom, 778. / 1506.);
let height = fit(bounds, false, Size::new(1406., 534.));
assert_eq!(height.zoom, 486. / 814.);
assert!((width.pan.x + 54. * width.zoom - 24.).abs() < 0.0001);
assert!((height.pan.y + 18. * height.zoom - 24.).abs() < 0.0001);
assert_eq!(fit(bounds, false, Size::new(10000., 10000.)).zoom, 1.5);
assert_eq!(
fit(bounds, true, Size::new(1., 1.)),
Camera {
zoom: 1.,
pan: Vector::new(30., 30.)
}
);
// Large nonzero origin makes algebraically simplified size differ in f32.
let offset = routing::RoutingBounds {
position: oa_core::Position {
x: 1_000_000.,
y: 500_000.,
},
size: oa_core::Size {
width: 100.03,
height: 80.02,
},
};
let camera = fit(offset, false, Size::new(100., 200.));
assert_eq!(
camera.zoom,
52. / ((offset.position.x + offset.size.width) - offset.position.x)
);
}
#[test]
fn measurements_and_draw_acknowledgments_reject_stale_invalid_repeat_and_wrong_camera() {
let frozen = sample();
let mut layout = LayoutState::default();
for size in [
Size::ZERO,
Size::new(f32::NAN, 10.),
Size::new(10., f32::INFINITY),
Size::new(-1., 20.),
] {
assert!(!layout.measured(0, size, &frozen));
}
assert!(layout.measured(0, Size::new(800., 500.), &frozen));
assert!(
!layout.ready(),
"Sensor receipt is not a draw acknowledgment"
);
let first = layout.expected.unwrap();
assert!(!layout.measured(0, first.viewport, &frozen));
assert!(!layout.drawn(DrawStamp {
camera: Camera {
zoom: first.camera.zoom * 1.1,
..first.camera
},
..first
}));
assert!(layout.drawn(first));
assert!(layout.ready());
assert!(!layout.drawn(first));
assert!(layout.measured(0, Size::new(900., 600.), &frozen));
assert_eq!(
layout.generation, 1,
"Same-variant real resize invalidates old authority"
);
assert!(!layout.drawn(first));
assert!(!layout.measured(0, Size::new(800., 500.), &frozen));
assert!(!layout.ready());
layout.variant = Variant::Focus;
layout.invalidate();
assert!(!layout.drawn(first));
assert!(!layout.measured(1, Size::new(1400., 800.), &frozen));
assert!(layout.measured(2, Size::new(1400., 800.), &frozen));
assert!(layout.drawn(layout.expected.unwrap()));
}
#[test]
fn admission_rejects_missing_state_and_scenario_views_without_project_mutation() {
let original = sample();
assert!(Frozen::new(original.project.clone(), Id::new_v4()).is_err());
for projection in [
oa_core::ProjectionKind::StateMachine {
machine: Id::new_v4(),
},
oa_core::ProjectionKind::Scenario {
scenario: Id::new_v4(),
},
] {
let mut candidate = original.project.clone();
candidate
.diagrams
.get_mut(&original.diagram)
.unwrap()
.projection = projection;
assert!(Frozen::new(candidate, original.diagram).is_err());
}
let app = fixture(original.project.clone(), original.diagram).unwrap();
original.check(&app, original.diagram).unwrap();
assert!(!app.document.dirty() && !app.document.can_undo() && !app.document.can_redo());
}
#[test]
fn fixed_schema_seven_and_migrated_schema_three_fixtures_admit_coherent_oa_sa_chrome() {
let cases: &[(&[u8], &str)] = &[
(
include_bytes!(
"../../../../harness/experiments/focus-optical-20261001/eight-card.json"
),
"00000000-0038-4000-8000-000000000001",
),
(
include_bytes!("../../../../examples/routing-workshop.json"),
"945a7c88-a352-5028-b808-73d852e5abe6",
),
(
include_bytes!("../../../../examples/routing-workshop.json"),
"cb21862d-79e1-5a9d-bdf0-ee344c2fb219",
),
(
include_bytes!("../../../../examples/coastal-rescue.json"),
"487c064d-df33-45f2-b4b7-6dd68d7b601f",
),
];
for (bytes, diagram) in cases {
let decoded = oa_core::persistence::decode_with_report(bytes).unwrap();
let raw: Value = serde_json::from_slice(bytes).unwrap();
assert_eq!(decoded.migration.is_some(), raw["schema_version"] == 3);
let frozen = Frozen::new(decoded.project, diagram.parse().unwrap()).unwrap();
let app = fixture(frozen.project.clone(), frozen.diagram).unwrap();
frozen.check(&app, frozen.diagram).unwrap();
assert_eq!(
app.create_kind,
if app.layer == Layer::Oa {
Kind::Activity
} else {
Kind::Function
}
);
assert_eq!(app.draft.as_ref().unwrap().original, app.project().id);
assert_eq!(
app.draft.as_ref().unwrap().kind,
crate::forms::RecordKind::Project
);
assert!(
app.architecture.is_none()
&& app.connection.is_none()
&& app.diagram_navigation_focus.is_none()
);
let expected_index = crate::explorer::Index::new(app.project(), app.layer);
for group in crate::explorer::Group::ALL {
assert_eq!(
app.explorer_index.rows(group, "", &app.explorer_collapsed),
expected_index.rows(group, "", &app.explorer_collapsed)
);
}
}
}
#[test]
fn captures_require_a_matching_draw_and_preserve_complete_model_scene_svg() {
let frozen = sample();
let dir = tempfile::tempdir().unwrap();
let input = serde_json::to_vec(&frozen.project).unwrap();
let input_path = dir.path().join("fixture.json");
fs::write(&input_path, &input).unwrap();
let mut runner = Runner {
fixture: fixture(frozen.project.clone(), frozen.diagram).unwrap(),
diagram: frozen.diagram,
frozen,
input_path,
input,
output: dir.path().to_owned(),
layout: LayoutState::default(),
drawn: Rc::new(RefCell::new(None)),
header: None,
scale_factor: Some(1.),
capture: 0,
error: None,
};
let original_bundle = runner.fixture.document.bundle().clone();
assert!(runner.capture().is_err());
for (index, variant) in [Variant::Baseline, Variant::Focus, Variant::Baseline]
.into_iter()
.enumerate()
{
runner.layout.variant = variant;
runner.invalidate();
let generation = runner.layout.generation;
let size = if variant == Variant::Focus {
Size::new(1400., 800.)
} else {
Size::new(826., 534.)
};
let _ = runner.update(Message::Measured(generation, size));
runner.header = Some((generation, Size::new(1440., 67.4)));
assert!(runner.capture().is_err());
let stamp = runner.layout.expected.unwrap();
*runner.drawn.borrow_mut() = Some(stamp);
let _ = runner.update(Message::Drawn(stamp));
runner.capture().unwrap();
let captured = dir.path().join(format!("capture-{:03}", index + 1));
assert_eq!(
fs::read(captured.join("diagram.svg")).unwrap(),
runner.frozen.svg.as_bytes()
);
let scene: Value =
serde_json::from_slice(&fs::read(captured.join("scene.json")).unwrap()).unwrap();
assert_eq!(scene, runner.frozen.report);
let project: Project =
serde_json::from_slice(&fs::read(captured.join("project.json")).unwrap()).unwrap();
assert_eq!(project, runner.frozen.project);
assert_eq!(runner.fixture.document.bundle(), &original_bundle);
assert!(!runner.fixture.document.can_undo() && !runner.fixture.document.can_redo());
}
let mut altered = runner.frozen.project.clone();
altered.name.push_str(" changed");
let altered_app = fixture(altered, runner.diagram).unwrap();
assert!(runner.frozen.check(&altered_app, runner.diagram).is_err());
fs::write(&runner.input_path, b"changed").unwrap();
assert!(runner.capture().is_err());
}
#[test]
fn private_flag_is_exact_and_other_launches_remain_opted_out() {
for arguments in [
vec!["app"],
vec!["app", "--write-example", "out"],
vec!["app", "--focus"],
vec!["app", "--focus-optical-extra"],
] {
assert!(!requested(
&arguments.into_iter().map(String::from).collect::<Vec<_>>()
));
}
assert!(requested(&["app".into(), "--focus-optical".into()]));
}
#[test]
fn terminal_fault_rejects_queued_draw_capture_and_close_never_overwrites_or_traps() {
let frozen = sample();
let dir = tempfile::tempdir().unwrap();
let input = serde_json::to_vec(&frozen.project).unwrap();
let input_path = dir.path().join("fixture.json");
fs::write(&input_path, &input).unwrap();
let mut runner = Runner {
fixture: fixture(frozen.project.clone(), frozen.diagram).unwrap(),
diagram: frozen.diagram,
frozen,
input_path,
input,
output: dir.path().to_owned(),
layout: LayoutState::default(),
drawn: Rc::new(RefCell::new(None)),
header: None,
scale_factor: Some(1.),
capture: 0,
error: None,
};
let _ = runner.update(Message::Measured(0, Size::new(800., 500.)));
let stamp = runner.layout.expected.unwrap();
runner.header = Some((0, Size::new(1440., 67.4)));
*runner.drawn.borrow_mut() = Some(stamp);
let _ = runner.update(Message::Drawn(stamp));
assert!(runner.layout.ready());
runner.fail("Injected harness fault".into());
let _ = runner.update(Message::Drawn(stamp));
let _ = runner.update(Message::Capture);
assert!(!runner.layout.ready());
assert!(runner.capture().is_err());
assert_eq!(runner.capture, 0);
assert!(!dir.path().join("capture-001").exists());
fs::write(&runner.input_path, b"changed after admission").unwrap();
let close = runner.update(Message::Close(iced::window::Id::unique()));
assert!(
iced_runtime::task::into_stream(close).is_some(),
"Failure still issues the exit task"
);
let receipt = dir.path().join("finished.json");
let original = fs::read(&receipt).unwrap();
let recorded: Value = serde_json::from_slice(&original).unwrap();
assert_eq!(recorded["status"], "FAIL");
assert_eq!(
recorded["complete_project_scene_svg_and_input_unchanged"],
false
);
let close_again = runner.update(Message::Close(iced::window::Id::unique()));
assert!(
iced_runtime::task::into_stream(close_again).is_some(),
"Existing receipt cannot trap Close"
);
assert_eq!(
fs::read(receipt).unwrap(),
original,
"Never overwrite the first close receipt"
);
}
#[derive(Default)]
struct SpyCounts {
updates: Cell<usize>,
operations: Cell<usize>,
overlays: Cell<usize>,
draws: Cell<usize>,
}
struct Spy {
counts: Rc<SpyCounts>,
unexpected: bool,
}
impl Widget<crate::app::Message, Theme, ()> for Spy {
fn size(&self) -> Size<Length> {
Size::new(Fill, Fill)
}
fn layout(&mut self, _: &mut Tree, _: &(), _: &layout::Limits) -> layout::Node {
layout::Node::new(Size::new(500., 400.))
}
fn update(
&mut self,
_: &mut Tree,
event: &Event,
_: Layout<'_>,
cursor: mouse::Cursor,
_: &(),
_: &mut dyn Clipboard,
shell: &mut Shell<'_, crate::app::Message>,
_: &Rectangle,
) {
self.counts.updates.set(self.counts.updates.get() + 1);
assert!(matches!(
event,
Event::Window(iced::window::Event::RedrawRequested(_))
));
assert_eq!(cursor, mouse::Cursor::Unavailable);
shell.publish(if self.unexpected {
crate::app::Message::Fit
} else {
crate::app::Message::CanvasResized(Size::new(500., 400.))
});
}
fn operate(&mut self, _: &mut Tree, _: Layout<'_>, _: &(), _: &mut dyn Operation) {
self.counts.operations.set(self.counts.operations.get() + 1);
}
fn overlay<'a>(
&'a mut self,
_: &'a mut Tree,
_: Layout<'a>,
_: &(),
_: &Rectangle,
_: Vector,
) -> Option<iced::advanced::overlay::Element<'a, crate::app::Message, Theme, ()>> {
self.counts.overlays.set(self.counts.overlays.get() + 1);
None
}
fn draw(
&self,
_: &Tree,
_: &mut (),
_: &Theme,
_: &renderer::Style,
_: Layout<'_>,
cursor: mouse::Cursor,
_: &Rectangle,
) {
assert_eq!(cursor, mouse::Cursor::Unavailable);
self.counts.draws.set(self.counts.draws.get() + 1);
}
}
struct NoClipboard;
impl Clipboard for NoClipboard {
fn read(&self, _: iced::advanced::clipboard::Kind) -> Option<String> {
panic!("Unexpected clipboard read")
}
fn write(&mut self, _: iced::advanced::clipboard::Kind, _: String) {
panic!("Unexpected clipboard write")
}
}
struct Traverse;
impl Operation for Traverse {
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn Operation)) {
visit(self);
}
}
#[test]
fn inert_widget_blocks_all_input_operations_overlays_and_allows_only_redraw_measurement() {
let counts = Rc::new(SpyCounts::default());
let mut wrapper = inert::Inert {
content: Element::new(Spy {
counts: counts.clone(),
unexpected: false,
}),
generation: 9,
expected: None,
drawn: Rc::new(RefCell::new(None)),
};
let mut tree = Tree::new(&wrapper as &dyn Widget<Message, Theme, ()>);
let node = wrapper.layout(
&mut tree,
&(),
&layout::Limits::new(Size::ZERO, Size::new(500., 400.)),
);
let layout = Layout::new(&node);
let bounds = Rectangle::with_size(Size::new(500., 400.));
let cursor = mouse::Cursor::Available(Point::new(40., 40.));
let key = crate::behavior::canvas_workspace::tests::key;
let mut events = vec![
Event::Mouse(mouse::Event::CursorMoved {
position: Point::new(50., 50.),
}),
Event::Mouse(mouse::Event::WheelScrolled {
delta: mouse::ScrollDelta::Lines { x: 0., y: 1. },
}),
key(iced::keyboard::key::Named::Delete),
key(iced::keyboard::key::Named::Insert),
key(iced::keyboard::key::Named::Tab),
Event::Keyboard(iced::keyboard::Event::ModifiersChanged(
iced::keyboard::Modifiers::SHIFT,
)),
Event::InputMethod(iced::advanced::input_method::Event::Preedit(
"字".into(),
None,
)),
Event::InputMethod(iced::advanced::input_method::Event::Commit("字".into())),
Event::Touch(iced::touch::Event::FingerPressed {
id: iced::touch::Finger(1),
position: Point::ORIGIN,
}),
Event::Window(iced::window::Event::Resized(Size::new(600., 500.))),
Event::Window(iced::window::Event::Rescaled(1.25)),
Event::Window(iced::window::Event::Unfocused),
Event::Window(iced::window::Event::FileDropped(PathBuf::from(
"ignored.json",
))),
];
for button in [
mouse::Button::Left,
mouse::Button::Middle,
mouse::Button::Right,
] {
events.push(Event::Mouse(mouse::Event::ButtonPressed(button)));
events.push(Event::Mouse(mouse::Event::ButtonReleased(button)));
}
let mut messages = vec![];
for event in &events {
wrapper.update(
&mut tree,
event,
layout,
cursor,
&(),
&mut NoClipboard,
&mut Shell::new(&mut messages),
&bounds,
);
}
assert_eq!(counts.updates.get(), 0);
assert!(messages.is_empty());
wrapper.operate(&mut tree, layout, &(), &mut Traverse);
assert!(
wrapper
.overlay(&mut tree, layout, &(), &bounds, Vector::ZERO)
.is_none()
);
assert_eq!(
wrapper.mouse_interaction(&tree, layout, cursor, &bounds, &()),
mouse::Interaction::None
);
assert_eq!(counts.operations.get() + counts.overlays.get(), 0);
wrapper.draw(
&tree,
&mut (),
&Theme::Light,
&renderer::Style::default(),
layout,
cursor,
&bounds,
);
assert_eq!(counts.draws.get(), 1);
wrapper.update(
&mut tree,
&Event::Window(iced::window::Event::RedrawRequested(
iced::time::Instant::now(),
)),
layout,
cursor,
&(),
&mut NoClipboard,
&mut Shell::new(&mut messages),
&bounds,
);
assert!(
matches!(messages.as_slice(), [Message::Measured(9, size)] if *size == Size::new(500., 400.))
);
assert_eq!(counts.updates.get(), 1);
}
#[test]
fn unexpected_passive_message_is_a_fault_and_draw_only_adapter_never_emits_actions() {
let counts = Rc::new(SpyCounts::default());
let mut wrapper = inert::Inert {
content: Element::new(Spy {
counts,
unexpected: true,
}),
generation: 0,
expected: None,
drawn: Rc::new(RefCell::new(None)),
};
let mut tree = Tree::new(&wrapper as &dyn Widget<Message, Theme, ()>);
let node = wrapper.layout(
&mut tree,
&(),
&layout::Limits::new(Size::ZERO, Size::new(500., 400.)),
);
let mut messages = vec![];
wrapper.update(
&mut tree,
&Event::Window(iced::window::Event::RedrawRequested(
iced::time::Instant::now(),
)),
Layout::new(&node),
mouse::Cursor::Unavailable,
&(),
&mut NoClipboard,
&mut Shell::new(&mut messages),
&Rectangle::with_size(Size::new(500., 400.)),
);
assert!(matches!(
messages.as_slice(),
[Message::UnexpectedPassive(_)]
));
let frozen = sample();
let app = fixture(frozen.project.clone(), frozen.diagram).unwrap();
let program = OpticalCanvas {
program: DiagramCanvas {
selection: None,
allocation: None,
project: app.project(),
view_index: Some(app.document.view_index()),
diagram: frozen.diagram,
selected: app.project().id,
navigation_focus: None,
zoom: 1.,
pan: Vector::ZERO,
context: app.route_context().unwrap(),
selected_bend: None,
selected_attachment: None,
editable: false,
placing: false,
connection: None,
},
stamp: None,
drawn: Rc::new(RefCell::new(None)),
};
let mut state = crate::diagram::State::default();
use canvas::Program;
for event in [
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
Event::Mouse(mouse::Event::CursorMoved {
position: Point::new(300., 200.),
}),
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
] {
assert!(
program
.update(
&mut state,
&event,
Rectangle::with_size(Size::new(500., 400.)),
mouse::Cursor::Available(Point::new(200., 100.))
)
.is_none()
);
}
assert_eq!(
program.mouse_interaction(
&state,
Rectangle::with_size(Size::new(500., 400.)),
mouse::Cursor::Available(Point::new(200., 100.))
),
mouse::Interaction::None
);
frozen.check(&app, frozen.diagram).unwrap();
}
+7 -26
View File
@@ -48,11 +48,6 @@ pub enum Event {
pub(crate) const ARTICLE_SCROLL_ID: &str = "engineering-guide-article";
mod context_help;
pub(crate) use context_help::{
ContextScroll, context_scroll_key, context_scroll_task, protect_workspace,
};
impl State {
/// Returns whether navigation selected a different article.
pub fn update(&mut self, event: Event) -> bool {
@@ -315,7 +310,7 @@ fn article(topic: Topic) -> Article {
"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 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 the Allocation palette tool to connect a performer to existing unallocated leaf work in Containment. The relationship can appear as a line there; Architecture · allocated work shows responsibility through nesting instead. Saved visibility filters can hide a line without removing the relationship.",
"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 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.",
"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 view; see Route connections around the model for its bend and label reset rules. 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.",
],
),
@@ -339,7 +334,7 @@ fn article(topic: Topic) -> Article {
"Finish or cancel property and source edits, then drag an Activity or Function body in Architecture · allocated work. Aim at exposed performer space, including a collapsed performer's header. Read Destination above the canvas and the outlined target before releasing; release outside the canvas or press Escape to cancel.",
"Use Shift-click to add or remove cards from the selection, or right-click a card and choose Add to selection. Drag any selected work card to assign the whole work selection to one performer. Independent work can start on different performers. The inspector keeps its primary element during the drag; right-click a selected card and choose Inspect this element to change that primary without losing the group.",
"The move preserves work ownership and identities. A composite work card moves its leaves together when they have one coherent responsibility; if its own leaves are distributed across performers, choose specific leaves instead. Role responsibilities remain explicit: an outward drag that would leave a role-derived performer explains why it cannot remove that responsibility.",
"To move work and performers together without changing responsibility, select their cards and choose Arrange or press Alt+A. This moves the selected work subtrees and each selected performer's contained cards together, if the complete layout stays readable and clear. Ports and connections cannot join a placement move. Dragging only performer cards normally moves them and their contained cards without changing assignments.",
"To move work and performers together without changing responsibility, select their cards and choose Arrange next drag or press Alt+A. This moves the selected work subtrees and each selected performer's contained cards together, if the complete layout stays readable and clear. Ports and connections cannot join a placement move. Dragging only performer cards normally moves them and their contained cards without changing assignments.",
"Make room when work cards would overlap, and move the view or zoom in if the moving work is not readable. The destination name stays above the canvas while the target outline stays fixed during the drag. Hover Destination to inspect a long name: outside the canvas, the tooltip explicitly identifies the last target, and releasing cancels. Return to the canvas to choose the drop. A refused drop keeps the model and history unchanged. One Undo reverses an accepted assignment and placement together; Save all writes the corresponding source changes.",
],
),
@@ -362,7 +357,9 @@ fn article(topic: Topic) -> Article {
&[
"Select an architecture connection and drag a segment to add a bend. Right-click a line for Add bend here; right-click a circle for Remove bend. With the canvas focused, Insert adds a bend and Delete removes the selected bend. Drag the label independently. Escape cancels a gesture; Undo reverses one completed drag. Shift+F10 opens the selected diagram item's context menu; use arrow keys and Enter to choose an action.",
"Automatic orthogonal routing avoids cards and container headers where space permits. Amber connections need attention: move an obstructing element or a bend. In Select / move, drag a port to any border of its owner. Manual positions are per diagram; right-click Reset automatic position restores connection-aware placement. Ports remain attached to their actual owners. Manual routes are saved separately for each diagram and appear in source, API and SVG export.",
"Select a connection and drag a square endpoint handle along its element boundary to move its attachment in this view without changing what it connects. If handles overlap, right-click the selected connection or use its Actions menu and choose Move source attachment or Move target attachment, then drag the handle. Connections to semantic ports retain their port attachment; move the port instead. Reset source attachment or Reset target attachment restores that end's automatic placement while preserving the other end, pins and label. Reset routing clears all these route overrides in this view. Auto-layout preserves pins. Moving one endpoint keeps pins fixed; moving a container carrying both endpoints translates their pins. When folding promotes an endpoint, expand it again to resume and edit its saved route.",
"Select a connection and drag a square endpoint handle along its element boundary to move its attachment in this view without changing what it connects. If handles overlap, right-click the selected connection or use its Actions menu and choose Move source attachment or Move target attachment, then drag the handle. Connections to semantic ports retain their port attachment; move the port instead. Reset source attachment or Reset target attachment restores that end's automatic placement while preserving the other end, pins and label. Reset routing clears all these route overrides, including both attachments, in this view.",
"Auto-layout (Arrange elements) replaces saved bends and label positions on visible connections with their actual endpoints shown; attachment preferences and port anchors stay saved. Hidden or folded routes keep their overrides, though pins can move with their endpoint owners during arrangement. Undo restores the prior arrangement.",
"During ordinary movement, moving one endpoint keeps pins fixed; moving a container carrying both endpoints translates their pins. When folding promotes an endpoint, expand it again to resume and edit its saved route.",
],
),
explain(
@@ -468,10 +465,6 @@ fn article(topic: Topic) -> Article {
summary: "An operational Process or system Functional chain connects contributions of existing work and exchanges, including alternatives and reusable procedures. It explains how work contributes to an outcome.",
keywords: "process functional chain ordered occurrence sequence path involvement dataflow exchange repeated steps caller contextual qualified assembly common work focus reuse",
sections: &[
explain(
"Select a group",
"Use Select, then Shift-click to add or remove contributions from the selection. Drag selected local cards to reorder them in the same column. Reused previews are read-only; open their referenced definition to edit. Properties affect only the primary selected item. Help or F1 opens this guide without leaving the diagram; Close or Escape returns to editing.",
),
walkthrough(
"Describe a path through exchanges",
&[
@@ -578,10 +571,6 @@ fn article(topic: Topic) -> Article {
summary: "Describe the conditions in which the operation or system behaves differently. States and modes belong to regions within a state machine.",
keywords: "state machine mode region initial final transition trigger guard effect entry exit do availability degraded nominal diagram draw reconnect local internal timed fold unfold placement pan zoom undo",
sections: &[
explain(
"Select a group",
"Choose Select / move, then Shift-click to add or remove elements from the selection. Properties affect only the primary selected item; Hide selected removes placements from this view. Help or F1 opens this guide without leaving the diagram; Close or Escape returns to editing.",
),
walkthrough(
"Build a state machine",
&[
@@ -1169,14 +1158,6 @@ fn topic_button(topic: Topic, selected: bool) -> iced::widget::Button<'static, M
}
fn article_view(topic: Topic) -> Element<'static, Message> {
article_content(topic, true)
}
pub(crate) fn context_view(state: &State) -> Element<'static, Message> {
article_content(state.topic, false)
}
fn article_content(topic: Topic, navigation: bool) -> Element<'static, Message> {
let article = article(topic);
let mut content = column![
text(topic.category()).size(11).color(style::MUTED),
@@ -1215,14 +1196,14 @@ fn article_content(topic: Topic, navigation: bool) -> Element<'static, Message>
}
content = content.push(container(body).padding(20).width(Fill).style(style::card));
}
if navigation && let Some((page, label)) = article.destination {
if let Some((page, label)) = article.destination {
content = content.push(
button(text(label).size(13))
.padding([11, 15])
.on_press(Message::Page(page)),
);
}
if navigation && topic == Topic::FirstProject {
if topic == Topic::FirstProject {
content = content.push(text("For a worked model, open the water-pressure walkthrough. Copy the whole examples/release1 folder and open the copied project.arcadia before editing; read its README.md. If you use Save as, also copy evidence/ beside the new entry: Save as copies model source, not external artifact targets.").size(13));
content = content.push(
button(text("Open water-pressure walkthrough").size(13))
-282
View File
@@ -1,282 +0,0 @@
//! Modal reading input and preservation of the mounted editing workspace.
use super::ARTICLE_SCROLL_ID;
use crate::app::Message;
use iced::Element;
#[derive(Debug, Clone, Copy)]
pub(crate) enum ContextScroll {
LineUp,
LineDown,
PageUp,
PageDown,
Home,
End,
}
pub(crate) fn context_scroll_key(
event: iced::Event,
status: iced::event::Status,
_: iced::window::Id,
) -> Option<Message> {
use iced::keyboard::{Event, Key, key::Named};
let iced::Event::Keyboard(Event::KeyPressed { key, modifiers, .. }) = event else {
return None;
};
if status == iced::event::Status::Captured || !modifiers.is_empty() {
return None;
}
let scroll = match key {
Key::Named(Named::ArrowUp) => ContextScroll::LineUp,
Key::Named(Named::ArrowDown) => ContextScroll::LineDown,
Key::Named(Named::PageUp) => ContextScroll::PageUp,
Key::Named(Named::PageDown) => ContextScroll::PageDown,
Key::Named(Named::Home) => ContextScroll::Home,
Key::Named(Named::End) => ContextScroll::End,
_ => return None,
};
Some(Message::ContextHelpScroll(scroll))
}
pub(crate) fn context_scroll_task(scroll: ContextScroll) -> iced::Task<Message> {
struct Scroll(ContextScroll);
impl<T> iced::advanced::widget::Operation<T> for Scroll {
fn traverse(
&mut self,
visit: &mut dyn FnMut(&mut dyn iced::advanced::widget::Operation<T>),
) {
visit(self);
}
fn scrollable(
&mut self,
id: Option<&iced::advanced::widget::Id>,
bounds: iced::Rectangle,
content_bounds: iced::Rectangle,
_: iced::Vector,
state: &mut dyn iced::advanced::widget::operation::Scrollable,
) {
use iced::advanced::widget::operation::scrollable::{AbsoluteOffset, RelativeOffset};
if id != Some(&iced::advanced::widget::Id::new(ARTICLE_SCROLL_ID)) {
return;
}
match self.0 {
ContextScroll::Home | ContextScroll::End => state.snap_to(RelativeOffset {
x: None,
y: Some(if matches!(self.0, ContextScroll::End) {
1.
} else {
0.
}),
}),
scroll => {
let y = match scroll {
ContextScroll::LineUp => -40.,
ContextScroll::LineDown => 40.,
ContextScroll::PageUp => -bounds.height * 0.9,
ContextScroll::PageDown => bounds.height * 0.9,
_ => unreachable!(),
};
// Iced clamps this movement to the actual content bounds.
state.scroll_by(AbsoluteOffset { x: 0., y }, bounds, content_bounds);
}
}
}
}
iced::advanced::widget::operate(Scroll(scroll))
}
/// Keep the mounted editor and its caret, but prevent modal input from reaching
/// widget-internal state before the App message guard can reject edits.
pub(crate) fn protect_workspace<'a, R: iced::advanced::renderer::Renderer + 'a>(
help_open: bool,
content: Element<'a, Message, iced::Theme, R>,
) -> Element<'a, Message, iced::Theme, R> {
Element::new(WorkspaceInput { help_open, content })
}
struct WorkspaceInput<'a, R> {
help_open: bool,
content: Element<'a, Message, iced::Theme, R>,
}
#[derive(Default)]
struct WorkspaceInputState {
help_open: bool,
}
impl<R: iced::advanced::renderer::Renderer> iced::advanced::Widget<Message, iced::Theme, R>
for WorkspaceInput<'_, R>
{
fn tag(&self) -> iced::advanced::widget::tree::Tag {
iced::advanced::widget::tree::Tag::of::<WorkspaceInputState>()
}
fn state(&self) -> iced::advanced::widget::tree::State {
iced::advanced::widget::tree::State::new(WorkspaceInputState::default())
}
fn children(&self) -> Vec<iced::advanced::widget::Tree> {
vec![iced::advanced::widget::Tree::new(self.content.as_widget())]
}
fn diff(&self, tree: &mut iced::advanced::widget::Tree) {
tree.diff_children(std::slice::from_ref(&self.content));
}
fn size(&self) -> iced::Size<iced::Length> {
self.content.as_widget().size()
}
fn size_hint(&self) -> iced::Size<iced::Length> {
self.content.as_widget().size_hint()
}
fn layout(
&mut self,
tree: &mut iced::advanced::widget::Tree,
renderer: &R,
limits: &iced::advanced::layout::Limits,
) -> iced::advanced::layout::Node {
self.content
.as_widget_mut()
.layout(&mut tree.children[0], renderer, limits)
}
fn operate(
&mut self,
tree: &mut iced::advanced::widget::Tree,
layout: iced::advanced::Layout<'_>,
renderer: &R,
operation: &mut dyn iced::advanced::widget::Operation,
) {
self.content
.as_widget_mut()
.operate(&mut tree.children[0], layout, renderer, operation);
}
fn update(
&mut self,
tree: &mut iced::advanced::widget::Tree,
event: &iced::Event,
layout: iced::advanced::Layout<'_>,
cursor: iced::mouse::Cursor,
renderer: &R,
clipboard: &mut dyn iced::advanced::Clipboard,
shell: &mut iced::advanced::Shell<'_, Message>,
viewport: &iced::Rectangle,
) {
let entering = {
let state = tree.state.downcast_mut::<WorkspaceInputState>();
let entering = self.help_open && !state.help_open;
state.help_open = self.help_open;
entering
};
if entering {
// Retire held gestures without executing their release actions.
// Text controls keep their real caret/focus; drawing tools remain
// selected, but an old drag cannot resume when Help is dismissed.
for event in [
iced::mouse::Event::CursorLeft,
iced::mouse::Event::ButtonReleased(iced::mouse::Button::Left),
iced::mouse::Event::ButtonReleased(iced::mouse::Button::Middle),
iced::mouse::Event::ButtonReleased(iced::mouse::Button::Right),
] {
let mut discarded = Vec::new();
let mut cancellation = iced::advanced::Shell::new(&mut discarded);
self.content.as_widget_mut().update(
&mut tree.children[0],
&iced::Event::Mouse(event),
layout,
iced::mouse::Cursor::Unavailable,
renderer,
clipboard,
&mut cancellation,
viewport,
);
shell.request_redraw_at(cancellation.redraw_request());
if cancellation.is_layout_invalid() {
shell.invalidate_layout();
}
if cancellation.are_widgets_invalid() {
shell.invalidate_widgets();
}
}
}
if self.help_open
&& matches!(
event,
iced::Event::Keyboard(
iced::keyboard::Event::KeyPressed { .. }
| iced::keyboard::Event::KeyReleased { .. }
) | iced::Event::Mouse(_)
| iced::Event::Touch(_)
| iced::Event::InputMethod(_)
)
{
return;
}
// ModifiersChanged and passive window/redraw events still reach the
// editor, so releasing Shift during Help cannot stick on return.
self.content.as_widget_mut().update(
&mut tree.children[0],
event,
layout,
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
fn draw(
&self,
tree: &iced::advanced::widget::Tree,
renderer: &mut R,
theme: &iced::Theme,
style: &iced::advanced::renderer::Style,
layout: iced::advanced::Layout<'_>,
cursor: iced::mouse::Cursor,
viewport: &iced::Rectangle,
) {
self.content.as_widget().draw(
&tree.children[0],
renderer,
theme,
style,
layout,
cursor,
viewport,
);
}
fn mouse_interaction(
&self,
tree: &iced::advanced::widget::Tree,
layout: iced::advanced::Layout<'_>,
cursor: iced::mouse::Cursor,
viewport: &iced::Rectangle,
renderer: &R,
) -> iced::mouse::Interaction {
if self.help_open {
iced::mouse::Interaction::None
} else {
self.content.as_widget().mouse_interaction(
&tree.children[0],
layout,
cursor,
viewport,
renderer,
)
}
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut iced::advanced::widget::Tree,
layout: iced::advanced::Layout<'b>,
renderer: &R,
viewport: &iced::Rectangle,
translation: iced::Vector,
) -> Option<iced::advanced::overlay::Element<'b, Message, iced::Theme, R>> {
if self.help_open {
None
} else {
self.content.as_widget_mut().overlay(
&mut tree.children[0],
layout,
renderer,
viewport,
translation,
)
}
}
}
-4
View File
@@ -15,7 +15,6 @@ mod diagram_usage;
mod document;
mod explorer;
mod explorer_view;
mod focus_optical;
mod forms;
mod guide;
mod highlight;
@@ -45,9 +44,6 @@ mod workflow;
fn main() -> iced::Result {
// Small headless entry points support reproducible examples and CI validation.
let args: Vec<_> = std::env::args().collect();
if focus_optical::requested(&args) {
return focus_optical::run(&args[2..]);
}
if args.get(1).is_some_and(|s| s == "--write-example") {
let path = args
.get(2)
+158
View File
@@ -1692,3 +1692,161 @@ fn adding_bend_between_close_ports_stays_outside_owner_clearance() {
}]
);
}
fn arrange_reset_fixture() -> (Project, Id, Id) {
let mut project = Project::empty();
let source = project.add(Kind::Activity, "Send report", 20., 70.);
let target = project.add(Kind::Activity, "Receive report", 650., 200.);
let edge = project.relate(RelationKind::Interaction, "Report", source, target);
let diagram = project.ensure_diagram(Layer::Oa);
project
.diagrams
.get_mut(&diagram)
.unwrap()
.routes
.push(EdgeRoute {
edge,
source_attachment: Some(EdgeAttachment {
side: PortAnchorSide::Bottom,
offset: 0.25,
}),
target_attachment: Some(EdgeAttachment {
side: PortAnchorSide::Top,
offset: 0.75,
}),
waypoints: vec![Position { x: 400., y: 500. }],
label: Some(EdgeLabel {
fraction: 0.25,
offset: Position { x: 11., y: 7. },
}),
});
(project, diagram, source)
}
#[test]
fn arrange_reset_document_preserves_multifile_comments_and_exact_undo_redo_reopen() {
use crate::document::Document;
use oa_core::source_project::{self, ProjectLimits};
let (project, diagram, _) = arrange_reset_fixture();
let mut bundle = source_project::format_bundle(&project);
assert!(
bundle.files.len() > 1,
"This must exercise a real include closure"
);
for (path, text) in &mut bundle.files {
*text = format!("// Keep exact authored module: {path}\r\n{text}");
}
let mut document = Document::from_bundle(bundle, ProjectLimits::default()).unwrap();
document.mark_saved(document.snapshot());
let before = document.snapshot();
let original_route = before.decoded.project.diagrams[&diagram].routes[0].clone();
document.execute(Command::Layout(diagram)).unwrap();
let after = document.snapshot();
let routes = &after.decoded.project.diagrams[&diagram].routes;
let mut expected_route = original_route;
expected_route.waypoints.clear();
expected_route.label = None;
assert_eq!(routes, &vec![expected_route]);
let mut outside = after.decoded.project.clone();
outside
.diagrams
.insert(diagram, before.decoded.project.diagrams[&diagram].clone());
assert_eq!(outside, before.decoded.project);
let affected: std::collections::BTreeSet<_> = std::iter::once(diagram)
.chain(
before.decoded.project.diagrams[&diagram]
.nodes
.keys()
.copied(),
)
.map(|id| document.compiled().index.declarations[&id].path.clone())
.collect();
for (path, text) in &before.bundle.files {
assert!(
after.bundle.files[path].contains(&format!("// Keep exact authored module: {path}"))
);
if !affected.contains(path) {
assert_eq!(
&after.bundle.files[path], text,
"Unrelated accepted bytes: {path}"
);
}
}
assert!(document.undo());
assert_eq!(document.bundle(), &before.bundle);
assert_eq!(document.project(), &before.decoded.project);
assert!(!document.dirty());
assert!(document.redo());
assert_eq!(document.bundle(), &after.bundle);
assert_eq!(document.project(), &after.decoded.project);
let reopened = Document::from_bundle(after.bundle.clone(), ProjectLimits::default()).unwrap();
assert_eq!(reopened.bundle(), &after.bundle);
assert_eq!(reopened.project(), &after.decoded.project);
}
#[test]
fn arrange_reset_pending_valid_property_has_separate_apply_and_layout_history() {
let (project, diagram, source) = arrange_reset_fixture();
let mut control = App::with_project(project.clone());
send(&mut control, Message::Select(source));
send(
&mut control,
Message::Field("/name".into(), "Revised sender".into()),
);
send(&mut control, Message::Apply);
let applied_project = control.project().clone();
let applied_bundle = control.document.bundle().clone();
assert_eq!(applied_project.elements[&source].name, "Revised sender");
assert_eq!(
applied_project.diagrams[&diagram].routes,
project.diagrams[&diagram].routes
);
let mut app = App::with_project(project);
let before_project = app.project().clone();
let before_bundle = app.document.bundle().clone();
send(&mut app, Message::Select(source));
send(
&mut app,
Message::Field("/name".into(), "Revised sender".into()),
);
send(&mut app, Message::Command(Command::Layout(diagram)));
assert_eq!(app.project().elements[&source].name, "Revised sender");
assert!(
app.project().diagrams[&diagram].routes[0]
.waypoints
.is_empty()
);
assert!(app.project().diagrams[&diagram].routes[0].label.is_none());
let arranged_project = app.project().clone();
let arranged_bundle = app.document.bundle().clone();
send(&mut app, Message::Undo);
assert_eq!(app.project(), &applied_project);
assert_eq!(app.document.bundle(), &applied_bundle);
send(&mut app, Message::Undo);
assert_eq!(app.project(), &before_project);
assert_eq!(app.document.bundle(), &before_bundle);
send(&mut app, Message::Redo);
assert_eq!(app.project(), &applied_project);
assert_eq!(app.document.bundle(), &applied_bundle);
send(&mut app, Message::Redo);
assert_eq!(app.project(), &arranged_project);
assert_eq!(app.document.bundle(), &arranged_bundle);
}
#[test]
fn arrange_reset_invalid_property_blocks_layout_without_losing_draft_or_accepted_bytes() {
let (project, diagram, _) = arrange_reset_fixture();
let node = *project.diagrams[&diagram].nodes.keys().next().unwrap();
let mut app = App::with_project(project);
send(&mut app, Message::Select(node));
let before_project = app.project().clone();
let before_bundle = app.document.bundle().clone();
let revision = app.document.revision();
send(&mut app, Message::Field("/position/x".into(), "-".into()));
send(&mut app, Message::Command(Command::Layout(diagram)));
assert_eq!(app.project(), &before_project);
assert_eq!(app.document.bundle(), &before_bundle);
assert_eq!(app.document.revision(), revision);
assert!(!app.document.can_undo());
assert_eq!(app.draft.as_ref().unwrap().inputs["/position/x"].raw, "-");
}
+117 -187
View File
@@ -25,23 +25,6 @@ pub fn action<'a>(s: &str, m: Message) -> widget::Button<'a, Message> {
.on_press(m)
.style(style::nav)
}
/// Optional instructions belong beside the work, without taking a canvas row.
/// The button also opens keyboard-accessible help; the tooltip is only a preview.
pub(crate) fn diagram_help<'a>(
topic: crate::guide::Topic,
hint: impl Into<String>,
) -> View<'a, Message> {
tooltip(
action("Help · F1", Message::ContextHelp(topic)),
container(text(hint.into()).size(12))
.padding(10)
.max_width(360)
.style(style::card),
tooltip::Position::Bottom,
)
.into()
}
pub fn short(s: &str, limit: usize) -> String {
let s = s.split_whitespace().collect::<Vec<_>>().join(" ");
if s.graphemes(true).count() > limit {
@@ -65,18 +48,8 @@ pub fn heading<'a>(title: &str, subtitle: &str) -> View<'a, Message> {
}
pub fn view(app: &App) -> View<'_, Message> {
workspace(app, None)
}
/// Private optical rig: preserve actual Baseline chrome below the shared header.
pub(crate) fn optical_baseline<'a>(app: &'a App, canvas: View<'a, Message>) -> View<'a, Message> {
workspace(app, Some(canvas))
}
/// Same logo, project typography, padding and intrinsic height in both runners.
pub(crate) fn header<'a, M: 'a>(app: &App, actions: View<'a, M>) -> View<'a, M> {
let p = app.project();
container(
let header = container(
row![
row![
text("◈").size(28).color(style::TEAL),
@@ -91,22 +64,6 @@ pub(crate) fn header<'a, M: 'a>(app: &App, actions: View<'a, M>) -> View<'a, M>
column![text(short(&p.name, 46)).size(17), label(app.hosted_label())]
.spacing(3)
.width(Fill),
actions
]
.spacing(8)
.align_y(iced::Center),
)
.padding([14, 18])
.style(style::panel)
.into()
}
fn workspace<'a>(app: &'a App, optical_canvas: Option<View<'a, Message>>) -> View<'a, Message> {
let p = app.project();
let optical = optical_canvas.is_some();
let header = header(
app,
row![
action("New", Message::Request(Pending::New)),
crate::hosted::view::header_action(),
action("Open", Message::Request(Pending::Open)),
@@ -116,9 +73,10 @@ fn workspace<'a>(app: &'a App, optical_canvas: Option<View<'a, Message>>) -> Vie
.on_press(Message::Save(false))
]
.spacing(8)
.align_y(iced::Center)
.into(),
);
.align_y(iced::Center),
)
.padding([14, 18])
.style(style::panel);
let mut nav = column![
label("ENGINEERING PERSPECTIVE"),
row![
@@ -265,7 +223,7 @@ fn workspace<'a>(app: &'a App, optical_canvas: Option<View<'a, Message>>) -> Vie
&app.recovery,
)
.map(Message::Home),
Page::Diagram => diagram(app, optical_canvas),
Page::Diagram => diagram(app),
Page::Catalog => crate::explorer_view::catalog(app),
Page::Data => data_catalog(app),
Page::Behavior => crate::behavior::view(app),
@@ -282,23 +240,11 @@ fn workspace<'a>(app: &'a App, optical_canvas: Option<View<'a, Message>>) -> Vie
if !matches!(app.page, Page::Home | Page::Text | Page::Guide) {
body = body.push(inspector(app));
}
let diagram_feedback = diagram_feedback(app);
// Keep operation outcomes visible even while a drawing hint is active.
let (status, status_detail) = if app.busy {
("Working…".into(), "Working…".into())
} else if let Some(feedback) = diagram_feedback.filter(|feedback| *feedback != app.status) {
(
format!("{} · {}", short(&app.status, 60), short(&feedback, 80)),
format!("{}\n{}", app.status, feedback),
)
} else {
(short(&app.status, 145), app.status.clone())
};
let footer = container(
row![
container(tooltip(
text(status).size(11).wrapping(widget::text::Wrapping::None),
container(text(status_detail).size(12))
text(short(if app.busy { "Working…" } else { &app.status }, 145)).size(11),
container(text(if app.busy { "Working…" } else { &app.status }).size(12))
.padding(10)
.max_width(600)
.style(style::card),
@@ -320,29 +266,7 @@ fn workspace<'a>(app: &'a App, optical_canvas: Option<View<'a, Message>>) -> Vie
)
.padding([4, 16])
.style(style::panel);
if optical {
return column![task_nav, body, footer].into();
}
let root = crate::allocation_input::root(app, column![header, task_nav, body, footer]);
let root = crate::guide::protect_workspace(app.context_help, root);
if app.context_help {
return modal_width(
root,
column![
row![
text("Diagram help").size(18),
Space::new().width(Fill),
action("Close · Esc", Message::CloseContextHelp),
]
.align_y(iced::Center),
crate::guide::context_view(&app.guide),
]
.spacing(12)
.height((app.window_size.height - 140.).max(300.))
.into(),
(app.window_size.width - 80.).min(900.),
);
}
if app.architecture.as_ref().is_some_and(|s| s.modal()) {
return modal_width(
root,
@@ -438,8 +362,7 @@ fn workspace<'a>(app: &'a App, optical_canvas: Option<View<'a, Message>>) -> Vie
(app.window_size.width - 32.).clamp(120., 760.),
);
}
// Match modal_width's first child so help does not recreate the editor tree.
widget::stack![root].into()
root
}
fn modal<'a>(base: View<'a, Message>, content: View<'a, Message>) -> View<'a, Message> {
modal_width(base, content, 800.)
@@ -467,7 +390,7 @@ fn modal_width<'a>(
]
.into()
}
fn diagram<'a>(app: &'a App, optical_canvas: Option<View<'a, Message>>) -> View<'a, Message> {
fn diagram(app: &App) -> View<'_, Message> {
if app
.active_diagram
.and_then(|id| app.project().diagrams.get(&id))
@@ -524,7 +447,7 @@ fn diagram<'a>(app: &'a App, optical_canvas: Option<View<'a, Message>>) -> View<
.active_diagram
.and_then(|id| app.project().diagrams.get(&id))
.map(|d| d.projection);
let mut modes = row![
let modes = row![
label("View content"),
pick_list(
[
@@ -539,15 +462,7 @@ fn diagram<'a>(app: &'a App, optical_canvas: Option<View<'a, Message>>) -> View<
Message::Projection
)
.padding(8)
.text_size(12),
diagram_help(
crate::guide::Topic::Nesting,
if projection == Some(ProjectionKind::Traceability) {
"Shows OA and SA elements with correspondence lines. Place both ends of a trace to show it; port traces use their owner cards. Open Help for trace visibility and editing controls."
} else {
"Shift-click to add or remove selection. Right-click for actions; drag to arrange; scroll to zoom. Drag a connection line, bend or label to move it; Insert adds a bend and Escape cancels. Open Help for full editing controls."
},
)
.text_size(12)
]
.spacing(10)
.align_y(iced::Center);
@@ -597,7 +512,6 @@ fn diagram<'a>(app: &'a App, optical_canvas: Option<View<'a, Message>>) -> View<
if let Some(editor) = crate::app::architecture_authoring::inline_editor(app) {
scene = scene.push(editor);
}
let scene: View<'_, Message> = optical_canvas.unwrap_or_else(|| scene.into());
let reveal = app.architecture_reveal_stamp(id);
row![
widget::sensor(
@@ -641,26 +555,6 @@ fn diagram<'a>(app: &'a App, optical_canvas: Option<View<'a, Message>>) -> View<
.center(Fill)
.into()
};
if projection == Some(ProjectionKind::Allocation) {
modes = modes.push(tooltip(
action(
if app.allocation_drag.arrange_next {
"Arrange ✓"
} else {
"Arrange"
},
Message::WorkDrag(crate::app::allocation_drag::Event::ArrangeNext),
),
container(text(format!(
"Arrange next drag · {}. Move work within its current allocation; Escape cancels.",
crate::allocation_input::ARRANGE_SHORTCUT
)))
.padding(10)
.max_width(420)
.style(style::card),
tooltip::Position::Bottom,
));
}
let mut tools = column![toolbar].spacing(8);
if app.active_diagram.is_some() {
tools = tools.push(modes);
@@ -674,50 +568,82 @@ fn diagram<'a>(app: &'a App, optical_canvas: Option<View<'a, Message>>) -> View<
.feedback
.as_ref()
.map(|feedback| feedback.destination_identity())
.unwrap_or_else(|| "—".into());
.unwrap_or_else(|| "None · drag work to choose".into());
let inspection = app
.allocation_drag
.feedback
.as_ref()
.map(|feedback| feedback.inspection_identity())
.unwrap_or_else(|| destination.clone());
let destination_control = container(tooltip(
text(format!("Destination: {}", short(&destination, 28)))
.size(13)
.wrapping(widget::text::Wrapping::None),
container(text(inspection).size(13))
.padding(10)
.max_width(420)
.style(style::card),
tooltip::Position::Top,
))
.id("allocation-destination")
.height(26)
.width(Fill)
.clip(true);
tools = tools.push(destination_control);
tools = tools.push(
container(tooltip(
text(format!("Destination: {}", short(&destination, 28)))
.size(13)
.wrapping(widget::text::Wrapping::None),
container(text(inspection).size(13))
.padding(10)
.max_width(420)
.style(style::card),
tooltip::Position::Top,
))
.id("allocation-destination")
.height(26)
.width(Fill)
.clip(true),
);
tools = tools.push(
row![
action(
&format!(
"{} · {}",
if app.allocation_drag.arrange_next {
"Arrange next drag: armed"
} else {
"Arrange next drag"
},
crate::allocation_input::ARRANGE_SHORTCUT
),
Message::WorkDrag(crate::app::allocation_drag::Event::ArrangeNext)
),
label("Drag work into a performer to assign · out to remove assignment")
]
.spacing(8)
.align_y(iced::Center),
);
}
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(
row![
label(format!("{correspondence_lines} trace lines")),
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)),
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(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(
app.project(),
&app.project().diagrams[&diagram],
edge.id,
)
.is_some_and(|(source, target)| source != edge.source || target != edge.target)
});
let selection = app.multiselect_snapshot();
let count = selection.as_ref().map_or(0, |s| s.targets().len());
let mut selection_controls =
@@ -736,62 +662,66 @@ fn diagram<'a>(app: &'a App, optical_canvas: Option<View<'a, Message>>) -> View<
}
selection_controls = selection_controls
.push(action(
"Clear",
"Clear selection",
Message::DiagramSelection(crate::app::multiselect::Event::Clear(
snapshot.context(),
)),
))
.push(
label(format!(
"Properties: {} only",
short(app.project().named(app.selected), 14)
"Properties edit only {}",
short(app.project().named(app.selected), 36)
))
.wrapping(widget::text::Wrapping::None),
);
}
// Selection never moves the canvas. General controls live in Help;
// active gesture feedback uses the persistent application status bar.
let hint = if let Some(feedback) = &app.allocation_drag.feedback {
feedback.text.as_str()
} else if let Some(session) = &app.connection {
if session.source.is_some() {
if session.candidates.is_empty() {
"No compatible targets are visible. Expand containers or show an endpoint · Esc cancels"
} else {
"Choose a highlighted target · middle-drag or arrow keys pan · Esc cancels"
}
} else {
session.tool.hint()
}
} else if count > 1 {
if selection
.as_ref()
.is_some_and(|s| crate::app::multiselect::placements(s).is_some())
&& projection != Some(ProjectionKind::Allocation)
{
"Shift-click to add/remove · drag selected to move · right-click for actions · scroll to zoom"
} else {
"Shift-click to add/remove · right-click for selection actions · Esc clears selection"
}
} else if promoted {
"Expand endpoint containers to edit this connection route"
} else if edge.is_some() {
"Right-click for routing · drag line / bend / label · Insert adds a bend · Esc cancels"
} else {
"Shift-click to add/remove · right-click for actions · drag to arrange · scroll to zoom"
};
// Selection changes must not resize the measured canvas or refit its
// camera. Reserve both rows even when no group actions are available.
tools = tools.push(
container(selection_controls)
.height(32)
.width(Fill)
.clip(true),
container(
column![
selection_controls,
label(hint).wrapping(widget::text::Wrapping::None)
]
.spacing(3),
)
.height(56)
.width(Fill)
.clip(true),
);
}
column![container(tools).padding(14), canvas].into()
}
fn diagram_feedback(app: &App) -> Option<String> {
if app.page != Page::Diagram {
return None;
}
if let Some(feedback) = &app.allocation_drag.feedback {
return Some(feedback.text.clone());
}
if let Some(session) = &app.connection {
return Some(
if session.source.is_none() {
session.tool.hint()
} else if session.candidates.is_empty() {
"No compatible targets visible. Expand containers or show an endpoint · Esc cancels"
} else {
"Choose a highlighted target · Esc cancels"
}
.into(),
);
}
let diagram = app.active_diagram?;
let view = oa_core::projection::project_view(app.project(), diagram);
let edge = view.edges.iter().find(|edge| edge.id == app.selected)?;
let (source, target) = oa_core::projection::edge_endpoint_placements(
app.project(),
&app.project().diagrams[&diagram],
edge.id,
)?;
(source != edge.source || target != edge.target)
.then(|| "Expand endpoint containers to edit this connection route".into())
}
fn inspector(app: &App) -> View<'_, Message> {
let p = app.project();
if let Some(target) = app.process_preview_primary() {
-2
View File
@@ -2,8 +2,6 @@
The normative contract is [language.rst](language.rst); [scope.md](scope.md) explains deliberate differences from the complete Arcadia/Capella ecosystem. V2 uses one typed model and one validated mutation boundary across the desktop, text editor and REST service.
Desktop changes follow the [editing workspace UX review criteria](ui-review.md) for concise persistent content, contextual help, visible actionable feedback and native interaction review.
## Crate boundaries
| Crate | Responsibility | Does not depend on |
+49
View File
@@ -1255,6 +1255,55 @@ translates pins when both actual endpoint owners move by the same delta; label
offsets remain relative. Raw ``EditNode`` and source position edits are exact
record edits and do not implicitly translate pins.
Explicit ``Layout(diagram)`` (Auto-layout / Arrange elements) is an exception to
route-override preservation. Immediately before this operation changes geometry,
it MUST determine eligibility from the staged Project's ordinary filtered
projection. An edge is eligible only if it is visible, both actual endpoint-owner
placements exist, both projected owner nodes exist, and its projected source and
target equal those actual owners in stored orientation. A semantic Port belongs
to its actual owning placement; hiding its marker does not make the edge
ineligible. An endpoint promoted to a folded ancestor does. Missing owners MUST
NOT be inferred. In a Batch, this input includes preceding staged operations;
determining eligibility MUST NOT introduce separate validation of a batch prefix.
For each existing eligible route entry, Layout MUST clear ``waypoints`` and the
entire ``label`` override (both fraction and offset). It MUST retain the entry,
its list position, edge identity, and both attachment values, including dormant
attachments at semantic ports. Empty and attachments-only entries MUST NOT be
removed. Layout MUST NOT create route entries merely to record generated elbows
or automatic label placement. Node port-anchor records remain exact, although
moving or resizing their owners can change their physical positions.
Ineligible routes, including hidden, filtered and promoted edges, MUST NOT be
reset. Their labels, attachments, identities and list order remain exact. Their
waypoints follow the existing movement rule: translate only when both actual
endpoint-owner placements receive the same nonzero original-to-final delta,
subject also to the existing State boundary movement rule. For a State Entry/Exit
endpoint whose owning State boundary has a placement, that boundary placement
must move with the same delta before the stored pins translate. Otherwise retain
the waypoint values.
Promotion to a visible ancestor alone does not authorize translation. Eligibility
limits route reset, not placement scope: ordinary Layout may still reposition or
resize hidden and folded descendant placements. Other diagrams remain unchanged.
Placement changes and eligible resets form one atomic Layout mutation. A changed
standalone Layout creates one history entry; Layout within Batch or execute_many
shares the enclosing transaction's entry, even when that transaction contains
several Layout operations. Validate the complete candidate through the ordinary
transaction path. Rejection leaves the accepted Project and history unchanged;
an identical enclosing transaction creates no entry and preserves Redo. Undo
restores the pre-transaction placement and route records. A separately applied
desktop property draft retains its own history entry; Layout does not merge it.
This exception does not change ordinary dragging, group movement, reconnect,
folding, filtering, raw ``EditNode`` or source position edits. Reset routing
remains the distinct whole-entry removal operation, including both attachments.
Internal State transitions have no external edge eligible for reset. Process
generated topology is unchanged; saved Scenario layout retains its dedicated
operation and rejects this Layout command. Automatic elbows remain derived;
resetting constraints does not guarantee an intersection-free or collision-free
arrangement.
This is an additive schema-3 field. Earlier schema-3 JSON/source that omits
``routes`` loads as an empty list. Frozen schema-1 and schema-2 readers do not
accept this new field; migration creates empty route lists. Source projects keep
-27
View File
@@ -1,27 +0,0 @@
# Editing workspace UX review
These development criteria implement the [project goal](../PROJECT_GOAL.md): reduce system engineers' cognitive load, keep graphical editing intuitive, and provide concise, navigable built-in help. They also address the user's request tracked in Gitea issue #39 to prevent persistent instructional text from consuming editing space. They are review criteria, not a claim that any particular build has passed them.
## Persistent content and progressive disclosure
Populated editing workspaces prioritize model content, controls, current state and actionable feedback. General explanations, gesture tutorials and repeated shortcut lists belong in contextual Help, tooltips or explicitly opened details. Do not add permanent instructional paragraphs above a populated canvas merely to explain a feature.
Keep essential control labels and relevant status visible. Offer concise next-step guidance in empty states and while a user is performing a multi-step action. Put longer explanations behind a clearly labeled disclosure near the relevant control or context. Essential instructions must be accessible through an explicit keyboard-reachable route; a hover-only tooltip is insufficient. Reuse the built-in guide for broader modeling explanations.
Distinguish instruction from information needed to act safely. Current errors, pending edits, failed operations, fallback rendering, unavailable actions and recovery steps must remain apparent. Preserve consequential distinctions such as editing a shared definition, changing semantic order rather than saved positions, or applying properties to only one member of a selection. Long diagnostic detail may be disclosed on demand, but its visible summary must identify the problem and a usable next step.
## Stable editing context
Selection changes and gesture feedback must not unexpectedly move or resize the canvas, reset its camera, or change the interpretation of the pointer. Compact existing stable layout slots instead of conditionally removing them when their content is absent. Opening and closing help must preserve the model, selection and pending drafts, and return the user to a usable editing context. Explicitly requested panel changes may resize the workspace; verify their effect on focus and navigation.
## Native review
Exercise the changed workspace in the actual built application at its configured minimum window size and a representative normal size. Include an independent reviewer for material interaction or layout changes, following [the working agreement](../AGENTS.md). Review representative populated models as well as relevant empty, pending, failure and recovery states.
- Inspect whether instructional prose or unused space displaces useful model content. Compare before/after screenshots at the same window size, display scaling, model and panel state when assessing space savings.
- Perform the affected workflow: select, edit, navigate, cancel and recover as applicable. Change between empty, single and multiple selections; exercise affected gestures and inspect camera and pointer stability.
- Find and use the disclosed guidance by keyboard as well as pointer. Check focus, dismissal, return to editing, and preservation of drafts.
- Trigger relevant disabled or rejected operations. Confirm their reason and recovery action remain visible, accurate and reachable.
- Check text, controls, panels and warnings for clipping, overlap, excessive scrolling and lost actions at the minimum size. Do not trade recovered space for unreadable text or unexplained symbols.
Record the tested code version, runtime environment, workflows, observations and representative screenshots in the change's review evidence. Screenshots support layout findings; they do not establish working interaction, accessibility or state preservation. Fix material findings and recheck affected behavior before treating the change as verified.
+20 -16
View File
@@ -499,21 +499,25 @@ map](edge-attachments-20260922.md) records alternatives, lifecycle rules,
independent falsifiers, evaluators and the Process/Scenario applicability limits.
This is branch-specific work; it does not establish an integrated release.
## Two-axis Focus optical scope — 2026-10-01
## Explicit Arrange resets visible authored geometry — 2026-09-25
Use a private, opt-in inert runner to measure complete-scene magnification before
implementing an editable Focus workspace. Width-only recovery was rejected for
this question because height limits Fit; a hand-built blank Baseline would not
represent real chrome. Freeze complete model/projection/routes/labels/ports and
reuse production drawing with actual measured viewports. Reject unmeasured or
stale captures; draw acknowledgment is CPU evidence, not GPU completion.
Branch-specific candidate for issue #32, based on released715036e; not an integrated
release checkpoint. Explicit Layout clears pins and the complete label override
for existing projected routes whose displayed owners equal their actual placed
owners. Freeze that eligibility before geometry changes, including at the operation's
position in a Batch. Preserve route entries, order, attachments and semantic/manual
port choices. Preserve existing equal-actual-owner translation for ineligible
routes and retain ordinary movement behavior. Keep whole Reset routing distinct.
The [predeclared comparison](experiments/focus-optical-20261001/plan.md) and
[native result](focus-optical-validation-20261001.md) establish substantial
two-axis gains on the mixed and nested examples while conserving whole recorded
values/bytes. They do not establish unassisted reading: fresh-reader availability
failed and shared runs/foreign-label crossings remain. Retain a workspace
hypothesis without promoting the experiment or closing #38. Category recognition
and individual connection identity are distinct next questions. Editable
promotion must separately prove draft/caret/focus/input, camera, gesture,
navigation, history/save/reopen and native behavior with #43 coordination.
This narrowly amends the former pin-preserving Layout contract so an explicit
arrangement can recover automatic route/label placement without deleting endpoint
choices. Clearing the whole label record avoids retaining a stale authored fraction;
it does not guarantee better geometry. The routing and node-placement algorithms
remain unchanged. More aggressive experimental routing candidates were not adopted
because independent scenes exposed tradeoffs and regressions.
[Results and evidence](experiments/arrange-reset-20260925/results.md) bind the
contract, source/history tests, strict Clippy and expanded/folded native save/reopen
acceptance to their actual code pins. Residual crossing/layout quality and viewport/
guide discoverability remain outside this bounded acceptance. Gitea owns live
publication and integration status; this record does not close issue #32.
+34 -62
View File
@@ -1,73 +1,45 @@
# Verified Windows source checkpoint — 2026-10-01 PDT
# Integrated prerelease source checkpoint — 2026-09-24 UTC
This branch combines the released integrated foundation
`715036e132677dd2bc2fb37ed316d93dc9987aec` with compact diagram controls and
contextual Help production source `6e7c4ddaf0775198384a8c4882809ea8e14b2976`,
and the protected Save As destination diagnostic correction tested at
`0e83157e09d52e6152df91208e41693b62da2d46`.
[PROJECT_GOAL.md](../PROJECT_GOAL.md) remains authoritative; complete OA/SA
acceptance is unfinished.
This branch assembles peer contributions for the Windows prerelease in issue37.
[PROJECT_GOAL.md](../PROJECT_GOAL.md) remains authoritative. The complete OA/SA
application is unfinished; published previews and finite prototypes are intermediate.
**Linux/WSL builds remain excluded by the user's explicit request.**
**The current prerelease is Windows-only. Linux/WSL builds and checks remain
excluded by the user's explicit request, including after networking recovers.**
## Verified capabilities and evidence
## Assembled source and acceptance boundary
The [frozen integrated Windows preview](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/releases/tag/v0.2.0-preview.integrated.20260924)
provides the accepted 715 foundation and its combined test/package evidence.
It remains unchanged. The older pending-build description previously at the top
of this file is superseded by that published result.
Combined code through **4deced53b98cd4d7649ba7aa60fa4b9b30d2f3b8**, plus the
test-only import correction from **7c2b06b63f8a0fe14b3f001996bdf965b36a4e85**, adds the reviewed
Connection/Route early-admission correction to the assembled Scenario/Text,
ordinary/State/Process/Allocation selection, direct drawing, Traceability and
picker changes. The [source handoff](prerelease-integrated-source-20260924.md)
identifies the incorporated commits, exact new test paths and required gates.
The nine event regressions and existing completed-Text-Apply cases are retained.
Direct formatting and whitespace checks pass; this source checkpoint does not
claim combined Cargo, native, packaged or release acceptance. The independent
[event-fix review](reviews/prerelease37-event-admission-20260924.md) found no
further material source defect in its stated scope. A subsequent peer compile
found a missing `projection::` qualification in the new Route test helper;
the current code includes that one-line correction. The original compile failed
before running tests. The first combined compile at `897d9f04` subsequently
found a missing `text_editor` import in the Allocation Text Apply regression;
the current source includes that one-line correction too. Both failures remain
recorded in the source handoff. The subsequent full test run at `8ddfadf4`
failed the mounted State picker fixture: added selection controls invalidated
its canvas-relative clicks. Source successor `a9cd364eacbbffa0693bab05b5a2b65f9d105535`
measures an identified fixed-height picker slot and checks Escape capture,
retaining the exact preservation/reopen/Place/Undo assertions. The source
handoff links the preserved failed and interrupted attempts. Corrected combined
Windows execution and package acceptance remain pending.
The [compact Help contribution](compact-help-validation-20261001.md) removes
permanent canvas tutorial prose, keeps stable concise selection rows and full
allocation destinations, and supports contextual Help/F1 with keyboard reading,
exact caret/draft preservation and safe gesture retirement. Final focused gates
pass 125 tests; the earlier full-suite result retains its separate source scope.
[Fresh package validation](release-compact-help-validation-20261001.md) verifies
actual extracted desktop/server behavior, Save/Undo/Redo/reopen and complete
source/model oracles. This is a scoped development preview, not complete release
or acceptance of the separately owned follow-up union.
The [scoped compact Help Windows preview](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/releases/tag/v0.2.0-preview.compact-help.20261001)
is published from evidence checkpoint `f1fdca371394aec1148e67f33f4d8c6b53a0c350`.
Its ZIP, checksum and validation attachment were downloaded and matched after
publication; [publication receipts](experiments/compact-help-20261001/publication.md)
retain the exact asset hashes and provenance.
Dense diagrams, remaining Scenario group adapters, quantities/calculations/
constraints, maintained libraries, broad accessibility/DPI and full OA/SA
acceptance remain unfinished. The independent projection/navigation, outgoing-drag,
icon, grid/performance and State-label contributions are outside this code.
The published compact Help ZIP retains misleading external-change wording for a
protected Save As destination collision. This branch's
[diagnostic correction](save-as-diagnostics-20261001.md) distinguishes existing
protected destination files from observed external edits and explains recovery.
Its 29 targeted checks, strict Clippy, matching GUI build, native normal/minimum
window workflows, eight complete byte checkpoints and parsed-model oracle pass;
independent review finds no material blocker in the bounded contribution.
The [fresh scoped package validation](release-save-diagnostics-validation-20261001.md)
checks the actual extracted correction: accurate protected-destination refusal,
Save All/fresh Save As recovery, actual external-edit protection, fresh reopen,
complete source/model oracles and 23 server assertions. Independent final review
finds no material blocker in this artifact. The [save-diagnostics Windows preview](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/releases/tag/v0.2.0-preview.save-diagnostics.20261001)
is published from evidence checkpoint `e144e32e9984477a258759a9dcc75f6e499fc224`;
[publication receipts](experiments/save-diagnostics-package-20261001/publication.md)
verify its ZIP, checksum and validation attachment. It remains outside the
separately owned follow-up union.
[Issue 46](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/46)
holds integration and delivery status. Live ownership and release coordination
remain in Gitea.
Live build, final-source and publication results belong in
[issue37](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/37).
Human workflows are described in [the preview checklist](../docs/preview-human-checks.md).
Private mixed-parser source is excluded from this application candidate.
## Contribution-specific verification
This experiment branch also contains a private, explicitly opted-in read-only
[two-axis Focus runner](focus-optical-validation-20261001.md), tested at
`38789512626cb24f78f274998977fc8c6cf960d1`. Its 92 focused checks, strict desktop
Clippy, matching Windows build, five normal native exits and 22 complete-value/
byte captures pass within the documented mechanical scope. Full-scene Fit gains
about79% on the mixed example,67% on nested OA and58% on nested SA; the small
view correctly remains at its clamp. Shared-path/label ownership and fresh-reader
acceptance remain unresolved. This is branch-specific optical evidence, not an
editable workspace, production promotion, new release or complete #38 remedy.
The following records retain their original source and runtime scope; they are
not relabeled as acceptance of the combined source above.
-34
View File
@@ -1,34 +0,0 @@
# Compact diagram controls and contextual help
Branch-specific work for [issue 39](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/39), based on published preview source `715036e132677dd2bc2fb37ed316d93dc9987aec`.
This record does not accept the complete first release or the other diagram defects.
## Problem and choice
The user's screenshot showed two permanent Traceability explanation paragraphs and a separate generic gesture-hint row. Direct Windows inspection of the released water-pressure example reproduced these. At a 1100×700 client size (1102×732 captured window), the Traceability canvas began around window y=470 and ended around y=692, leaving about 220 pixels for modeling. At the normal 1440×900 client size its top was around y=417. The release was opened from the existing downloaded package; only an in-memory projection switch was made and Undo restored the clean model before closure.
The candidate moves general instructions into contextual Help, reusing the offline guide. Architecture, State and Process selection bars use a stable compact row. Allocation retains its full-width destination slot; a concise Arrange control sits beside the existing view-content and Help controls. Traceability retains its displayed-line count and existing actions. Active drawing/assignment feedback appears beside the operation outcome in the persistent status bar, with full text on hover. Operation errors must never be replaced by generic guidance.
Help opens as an overlay and closes with Close or Escape. F1 opens the relevant diagram topic. The workspace, selected group, pending source/property/behavior drafts and camera remain owned by their existing state. Background user commands are rejected before specialized canvas/reference dispatch. Passive results retain their established validity checks. The editor remains the first child of a stable Stack while Help opens and closes.
Rejected alternatives: ordinary navigation to the Guide page cleared selection and could reject Help while a behavior draft was pending; placing feedback beside the entire selection toolbar exhausted narrow-window width; unconditional status substitution hid operation failures. Independent source review identified these before acceptance. Hover-only help was rejected because the complete instructions need a keyboard-accessible route.
[Editing workspace UX review](../docs/ui-review.md) is the development/review contract requested by the user. It covers persistent prose, actionable feedback, context preservation and independent native interaction checks.
## Verification status
Clean source `f83068757af885572f6b3f932d07233dd1b94b7e` passed `cargo build -p oa-desktop --locked` on Windows gnullvm. Its candidate executable SHA-256 is `c1a9b196ec03e86f049fa2aadb2b86bfcaa3f0fbae582a80ee39ee8adf0758e3e`. The [independent source review](compact-context-help-source-review-20260925.md) records the corrected source findings, including passive viewport updates while Help is open.
Independent native review of that executable recovered approximately 61 pixels in normal Traceability, 114 pixels in minimum Traceability and 58 pixels in minimum Allocation. Help resizes, pointer-scrolls, selects the State topic, and closes through Close/Escape. The review also reproduced two acceptance failures: PageDown cannot scroll Help, and F1/Escape preserves a property or Process-creation draft but loses typing focus/caret continuation. These findings require correction and another native check; model-state unit tests alone do not cover internal widget focus. The idle State-canvas gesture sentence is also removed in the successor; active/pending and hovered-item feedback retain their stable slot.
The initial focused test run passed nine and failed one: the allocation test fixture used the default Overview projection before asserting allocation ownership. A separate ten-case mounted geometry run passed nine and failed the destination's existing 400-pixel minimum-width check. Sharing its row with the verbose Arrange action reduced that slot to 328 pixels at minimum size. The successor fixes the fixture explicitly and restores the full destination row without weakening its width check. The original failed logs are preserved in the [native/test evidence](experiments/compact-help-20260925/README.md). These diagnostic runs began before the successor edits and are not frozen-source acceptance of those edits.
Frozen successor `5657a996ed2cefb1fa84ef967b655eaf8f9f6817` passed formatting, strict all-target desktop Clippy and the Windows GUI build. The full desktop run passed 1,538 tests, ignored seven and failed one new mounted Help test before Help opened: its synthetic multi-character KeyPressed event inserted only the first character, as the actual Iced text input specifies. The final correction sends individual character events and retains the exact-caret, scrolling and IME assertions. The existing allocation width and ownership checks and new modifier/pointer cancellation checks passed. See the [preserved successor evidence](experiments/compact-help-20260925/candidate-5657/review.md) and logs; the failed run is not reported as a passing suite.
Independent native review of that exact executable confirmed keyboard Help scrolling, precise Inspector and Process-draft caret continuation after Help, resize, retained State tool state and complete allocation-error tooltips. Minimum Allocation recovers approximately 64 pixels with the full destination slot; minimum Traceability recovers 114 pixels. The reviewer found no new blocker within that scope.
Final source `6e7c4ddaf0775198384a8c4882809ea8e14b2976` passes formatting, strict desktop/all-target Clippy, GUI build and 125 focused Help/allocation/guide tests (zero failures/ignores). The corrected mounted caret test now executes its complete assertions. Unchanged evidence from the earlier full run is retained at its original source; the remaining 1,421 desktop cases were not rerun at the final source. The initial final GUI link failed when the disk filled; after the user expanded the disk, the same-source rebuild passed. The failed log remains preserved.
The [final native report](experiments/compact-help-20260925/candidate-6e7c/review.md) directly verifies cancellation status, guide wording, native group selection through context menus, primary Inspector changes, long-name controls at minimum size, exact caret return after both Help dismissal routes, and Apply/Undo/Redo/Undo recovery. Independent source review found no blocker in the final small delta. The watcher records exit 0 and empty stdout/stderr; all 37 copied model files still match the original after closure. An early transient OSK/modifier obstruction occurred before model loading and is recorded separately from app behavior. No blocking defect was found within this contribution's review scope. Shared integration and release admission remain in issue 39; full product completion is not claimed.
Required native checks: minimum and normal windows; Traceability and Allocation space comparison; multi-selection with long names; Help/F1/Close/Escape with property and behavior drafts; editor focus and canvas scroll/camera restoration; active feedback plus operation failure; State and Process controls; subsequent edit/Undo/Redo and source preservation. Linux remains deferred under the recorded user instruction.
@@ -1,44 +0,0 @@
# Compact contextual help: independent source review
Review date: 2026-09-25. Scope: Gitea issue #39 compact editing chrome and contextual Help, reviewed against base `715036e1` and the issue screenshot. Reviewed source includes `e0812b3932fbb36100516f0981dc9c646eeda56d` plus the subsequent local passive-geometry admission patch and regression test in this change. This is source-review evidence, not native acceptance or executed test evidence.
Criteria: [editing workspace UX review](../docs/ui-review.md), especially preserving pending edits and selection, stable canvas geometry, accessible disclosure, and retaining actionable feedback.
## Findings addressed in source
- The initial Help action used ordinary page navigation, clearing multiselection and selected bends, including before a pending behavior form rejected navigation. Contextual Help now overlays the existing workspace. Close/Escape changes reading state without navigating or applying drafts.
- App-level modal admission originally ran after allocation and reference-picker fast paths. Help now gates user messages at the beginning of `App::update`; allocation and behavior keyboard ownership are also disabled while Help is open.
- Combining live gesture feedback with multiselection controls could leave no width for feedback at the configured minimum window size. Selection controls now keep a compact, fixed-height row; current drawing/allocation feedback uses the existing application footer.
- Footer feedback initially replaced operation status, concealing errors while a promoted edge or drawing tool remained selected. The footer now retains operation status alongside feedback and exposes complete text in its tooltip.
- Conditional modal wrapping could recreate the editor widget tree. Both closed and open states now use the same outer Stack with the workspace as its first child.
- State/Process selection guidance removed from persistent chrome was missing from its target Help articles. The States and Chains articles now include group selection, primary-properties semantics and relevant tool/read-only guidance.
- The new early modal gate initially rejected passive behavior viewport/scroll notifications. It now admits Process/Scenario geometry and State viewport notifications to their existing context-validated handlers, preventing resize observations from being lost while Help is open.
## Regression coverage added
[`context_help_tests.rs`](../crates/oa-desktop/src/app/context_help_tests.rs) covers property drafts and multiselection, source drafts, pending state-transition forms, blocked background actions, contextual Help topic dispatch, Close/Escape, allocation input ownership, queued inspector reference-picker admission, and valid/stale Process viewport measurements during Help and after closing. Assertions include accepted-model snapshot identity and absence of history changes where applicable. Passive geometry intentionally validates document/owner/revision; stale pointer epochs alone do not invalidate measurements.
The reviewer did not compile or execute these tests. Suggested focused command: `cargo test -p oa-desktop context_help_tests`. The integration owner records actual execution and native results separately.
## Follow-up from native review
Native review of the subsequent candidate found that contextual Help did not respond to PageDown and that Escape reached a focused underlying text input, dropping its typing focus. The follow-up patch adds a Help-only keyboard subscription and article-ID-targeted scrolling (40-pixel arrows, 90% of the actual viewport for PageUp/Down, Home/End), using Iced's bounds-clamped scroll state. A stable workspace input barrier preserves the actual editor caret while suppressing key, pointer, touch and IME input behind Help; modifier changes and passive geometry continue. Opening Help retires held pointer gestures through cancellation releases with all authored actions discarded, while forwarding redraw/layout invalidation. This also prevents a held canvas pan or text selection from resuming after release behind Help.
New mounted regressions exercise the production App tree and subscriptions for keyboard scrolling, unchanged background scroll positions, precise Inspector caret return, IME exclusion, Shift release, and held-pan retirement. A real text-input widget regression checks selection-drag retirement while preserving its caret. The allocation ownership fixture was corrected to explicitly request Allocation, because `ensure_diagram` creates Overview. These follow-up changes were formatted and source-reviewed here; compilation, execution and native acceptance remain the integration owner's checks.
A second, read-only critic reviewed the follow-up independently of its implementation. It found and prompted corrections for stale modifiers, IME leakage and interrupted pointer gestures. Its final read found no further blocking source defect in the input barrier, cancellation, passive-event handling or relocated Arrange control. The separate native reviewer supplied the original keyboard/focus failures. Neither source review establishes executed or native acceptance of the successor.
## Scope and remaining verification
A final independent read-only source review covered the small post-`5657a996`
delta. It confirmed that the mounted typing fixture must send one KeyPressed per
character because Iced consumes the first scalar, with the original caret/IME
assertions retained. Arrange cancellation now replaces only its own obsolete
armed status, preserving unrelated errors; input epochs and model commands are
unchanged. The guide names the actual Arrange control. No blocking finding was
identified in that delta. Actual executed and native evidence remains separately
identified by source in the design record and evidence index.
Production changes reviewed: desktop App messages/admission, allocation input ownership, Escape bridging, behavior keyboard ownership, architecture/State/Process toolbar layout, guide article disclosure and footer feedback; supporting architecture/UI-review documentation. Regression tests and this record are review artifacts. No model schema, persistence format or authored-model transformation is introduced.
No further concrete source blocker was identified after the fixes above. Native checks remain necessary at 1100×700 and a representative normal size: Help discovery and keyboard reading/dismissal, focus and caret return, preservation of canvas camera/selection and form drafts, resize while Help is open, multiselection control visibility, allocation refusal/destination feedback, current operation failures, and stable canvas bounds through selection/gesture changes. Source inspection and App tests do not establish layout quality, keyboard scrolling, pointer behavior or usability.
@@ -1,41 +0,0 @@
# Compact diagram Help: Windows validation
The source contribution for [issue 39](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/39)
is based on released `715036e132677dd2bc2fb37ed316d93dc9987aec`; final production/test
source is `6e7c4ddaf0775198384a8c4882809ea8e14b2976`. It removes permanent canvas
instructions, preserves compact stable controls and offers contextual Help/F1.
Model schema, core, server, dependencies and persistence code are unchanged.
[Decision and acceptance boundary](compact-context-help-20260925.md),
[independent source review](compact-context-help-source-review-20260925.md),
[native reports, screenshots and gate logs](experiments/compact-help-20260925/README.md)
and [development UX criteria](../docs/ui-review.md) are authoritative for this change.
Windows gnullvm / Rust 1.98.1: formatting, strict desktop/all-target Clippy and the
GUI build pass. Final focused run: **125 passed, zero failed/ignored**. Earlier
full desktop execution at `5657a996` passed 1,538, failed one new test's typing
setup and ignored seven; the corrected test executes fully in the final passing
run. This is not a claim of a full-suite run at the final commit. The initial
final link failed with a full disk and is retained; the recovered build succeeds.
Final Studio SHA256:
`d55ecad4b0f75c0c9c26625e5441bef9c9360890b65bc13236c6ebfaf03d631e`.
Matching runtime SHA256:
`f45604a12e8bcd839fd86f667c1be1cc7c4f9da2b3c69ff381ac708f0e1ae7d4`.
Independent native checks pass group selection/primary properties, Help dismissal
and exact caret continuation, long names at 1100×700, Arrange cancellation,
guide consistency and Apply/Undo/Redo/Undo. Earlier versioned native checks cover
Process drafts, State tools, Help resize and actual operation refusals. Minimum
Allocation recovers about 64 pixels; minimum Traceability about 114 pixels.
Final process exit is 0, both logs are empty, and all 37 copied example files
remain unchanged. Native counts do not establish whole-Project equality; mounted
regressions provide the separately scoped model/history checks.
The application remains an unfinished development preview. The separate
projection/navigation, outgoing-drag, icon, grid/performance and State-label
contributions are outside this source. Dense routes/labels, remaining Scenario
group adapters, quantities/calculations/constraints, maintained libraries,
complete OA/SA acceptance and broad accessibility/DPI coverage remain unfinished.
Linux/WSL checks remain excluded by the user's recorded instruction. This change
does not accept the complete first release.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,15 @@
# Supplemental export checkpoint: source review
Prepared `.local/arrange-reset-export-checkpoint-20260925.py`, SHA256 `5bcd8a568d5318dd62217c1e0b35538b22e34403c9727eee8814a5d0ec5a899f`. AST parsing passed; script/oracle not executed. This is an author source check, not independent runtime acceptance.
Example after the parent observes JSON export to a new destination:
```text
python .local/arrange-reset-export-checkpoint-20260925.py --root TRIAL --actual TRIAL/working/expanded-after.json --name expanded-after-export --case expanded --expected after
```
Use fresh names/files for Undo (`--expected original`) and Redo (`--expected after`). The script requires an existing JSON directly inside the admitted working directory and refuses the two original imported filenames. It does not create an export or establish when/how that file was created; the parent's GUI observation supplies that evidence.
It loads only the exact reviewed staged common helper, then reuses admitted stage/source/package/baseline checks. The supplemental script is explicitly **not stage-admitted**: its own exact file identity appears in the checkpoint receipt and oracle invocation bindings. It snapshots the export without rewriting it, invokes the preserved same-candidate `--compare` oracle, and checks the export, entire working inventory, supplemental script and admitted inputs afterward. Fresh checkpoint directories and receipts preserve attempts; failure quarantines the trial through the existing sentinel convention. No staged tool, manifest, original trial file or source checkout is changed.
An accepted receipt proves complete typed Project equality for the captured JSON. It does not establish native canonical-project Save, clean-document state, UI action cause, source-bundle byte equality or fresh-process reopening. JSON Save As is export-only here; parent must separately observe canonical Save and reopening. The earlier refused overwrite remains a recorded UI observation, not a successful save.
@@ -0,0 +1,34 @@
# Arrange reset native outcome independent review
**Accept the bounded expanded/folded Arrange-reset workflow demonstrated here. No material blocker found in the reviewed outcome.** This is Windows native wiring, preservation, history, source-save and reopen evidence at `d6f45a13a7b8ab8dc9b84ee432a832609f5af6ed`, not full issue32 layout-quality acceptance or J/L router adoption.
Reviewed all18 observation JSON notes and independently viewed all18 corresponding full-window PNGs with view_image. Read all eight checkpoint receipts/oracle outputs and both final close receipts. No GUI action, compiler, model helper or checkpoint rerun by reviewer.
## Model and lifecycle evidence
Each case has after/Undo/Redo/reopened exports: eight checkpoints accepted, failure null, immutable inputs unchanged, oracle exit0 and complete_project_equal=true. Independently read the captured exported JSON and expected baseline files: all eight pairs are byte-identical, stronger than the reported typed-model equality for these particular artifacts. Verified snapshot/expected/supplemental-script/comparison-receipt hashes. Expected after-Projects remain candidate-derived UI-wiring oracles; independent core tests establish the reset contract separately.
Additional independent model inspection confirms expanded Verified report resets its two pins `(610,210),(610,420)` and full label override; folded Verified report retains both pins and label fraction0.5/offset(0,-32). Both cases retain route entry order, attachments, semantic port anchor records, semantic elements/relations and the entire Review sentinel diagram. Filtered edge's pin `(1020,270)` remains exact in this fixture; this is not a promise that all filtered routes remain at fixed world coordinates. Original working JSON imports remain byte-identical to immutable originals.
Run01 PID7812 and run02 PID4328 both have final close receipts: exit0, failure null, observed exit, immutable inputs unchanged and empty stdout/stderr. Parent observations establish normal AltF4 causes; receipts establish actual exits. Run02 close is now complete, not merely window disappearance. Both saved source projects were reopened in fresh process run02 after run01 closure, then exported and compared against the same frozen after-models.
## Native observations
Expanded before/Undo images show two authored circular handles and manual label; arranged/Redo images show the selected route without those handles while automatic elbows remain. Folded images preserve collapsed presentation and visible promoted exchanges. Source-save images report all source files saved; reopen images report source project opened and retain expanded/folded state. Both sentinel-view images show retained route/label geometry, including the selected pin in expanded sentinel. Whole-model comparison verifies hidden fields that screenshots alone cannot.
The arranged view required Fit and displays71% zoom; Undo retained viewport zoom, so folded Archive partly leaves the viewport until refit. These are recorded viewing limitations, not model-history failures. Guide card13 is readable and correctly distinguishes explicit Layout's visible actual-endpoint pin/label reset from full Reset routing, retained port/attachment choices, dormant hidden/folded overrides and Undo. Search filters topics but required six scrolls to the relevant card; retain this existing discoverability limitation.
## Protocol deviation and adapter incidents
The original plan assumed overwriteable JSON checkpoints. Observation03 visibly reports that the JSON destination already exists and asks for a new file. The original remained intact. The actual workflow instead used fresh JSON exports for model checkpoints, then saved expanded and folded multi-file source projects in separate locations and reopened those sources. Export did not mark source saved or consume model history; source Save supplied the persistence/reopen evidence. This is a justified workflow adjustment, not a falsely reported successful overwrite.
The supplemental export checker is explicitly marked not stage-admitted in its receipts, with its own hash and narrow equality scope; original staged tools remained the existing set. Preserve that provenance rather than describing it as original staging. A rejected click targeted a16×16 icon screenshot; full-window screenshot1 corrected targeting. Short filename typing/set_value failures and successful absolute-path typing are adapter interaction observations, not demonstrated application defects. None authorizes editing expected model bytes; no such repair is evidenced.
## Binding and limits
Native manifest SHA256 `eb208a3a1fa0843be4087e1797d109dc0e3dcddb3255f3ad9f536f3d98292b65`.
Recorded GUI SHA256 `133375d57bdecf3f3effe5ecc13ec121e1257b90838f2cdb64fb91aa9a530d1b`; oracle `0792aff4cf8460c58fa2bf2f5ddbfa478523b7c9fe9b45e19ec93090191c37c1`.
Supplemental checker `5bcd8a568d5318dd62217c1e0b35538b22e34403c9727eee8814a5d0ec5a899f`.
Run01 close SHA256 `ffe8a3cab307ecc4ad45caf027787efdb6e41449fb304fc2cadd242a7fb5daf8`; run02 `6abce58644e8bf2c85e133aa479f44d66eeeb0a6d59361afb9db1d996b4ed745`.
No claim of general crossing/label-aware arrangement quality, arbitrary State-boundary native behavior, Process/Scenario changes, performance, Linux, full workspace/regression-suite completion, or remote publication. This trial saves newly generated multi-file sources; original authored multi-file comment/draft preservation is separately covered by desktop tests, not inferred from this UI sequence. Remote disk-full blockage still separates local acceptance from delivery.
@@ -0,0 +1,61 @@
# Explicit Arrange route reset — branch evidence
**Bounded reset candidate verified on Windows; local-only pending publication.** Branch `codex/arrange-reset-ytr83r-20260925` is based on released `715036e132677dd2bc2fb37ed316d93dc9987aec`. Final native/build/Clippy source is `d6f45a13a7b8ab8dc9b84ee432a832609f5af6ed`. This result covers the reset capability within issue #32, not a replacement node-layout algorithm or completion of intersection/label-aware automatic arrangement. No routing algorithm change is included.
Publication remains blocked by the [recorded remote disk-space failure](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/32#issuecomment-1330). This branch/evidence is local-only; no successful push, PR, merge or release is claimed. Existing published prereleases are unaffected.
## Behavior
Explicit `Command::Layout` freezes reset eligibility from the staged pre-operation filtered projection. It clears existing pins and the complete label override only for projected edges whose displayed endpoint owners equal both actual placed owners and whose projected owner nodes exist. It retains route entries/order, edge identities, both endpoint attachments, semantic ports and manual port anchors. Ordinary placement movement and the routing engine are unchanged.
Filtered, promoted, missing-owner and outside-projection routes are not reset. Retained pins follow the existing actual-owner equal-delta translation rule, including the State boundary exception; a retained record is not a promise of fixed world coordinates. Projection-excluded owners receive no movement when outside the existing Layout placement scope. Other views remain exact. Whole **Reset routing** remains a distinct operation that removes all overrides. Documentation now describes this deliberate behavior amendment instead of promising that Auto-layout always preserves pins.
Standalone changes form one history entry, including a route-only reset when nodes are already arranged. Nested Layout participates in its enclosing Batch's final validation and history. No-op Layout preserves Redo. A valid pending property draft remains a separate Apply before Layout; an invalid draft blocks Layout without losing its input.
## Recorded checks
All successful receipts below record exit code 0, unchanged pinned source, and no runner failure. Environment: local Windows `x86_64-pc-windows-gnullvm`, frozen offline toolchain, one Cargo build job.
| Source | Check | Actual result |
| --- | --- | --- |
| `6c44a2d6a16d8c80320d35a9c8d2eee3b77e8047` | Core library + `release_routing`, `state_diagrams`, `routing_geometry`, `routing_labels`, `route_source`, `group_movement` | **195 passed**, 0 failed/ignored across seven harnesses |
| `6c44a2d6a16d8c80320d35a9c8d2eee3b77e8047` | `cargo clippy --locked --offline -p oa-core -p oa-desktop --all-targets -- -D warnings` | Passed |
| `9eb4642e50564165aa0db38d8e91f4c14b818bdc` | `cargo test --locked --offline -p oa-desktop arrange_reset_` | **3 passed**, 1535 filtered out; not a full desktop-suite run |
| `d6f45a13a7b8ab8dc9b84ee432a832609f5af6ed` | `cargo build --locked --offline -p oa-core --example arrange_reset_native_probe` | Passed |
| `d6f45a13a7b8ab8dc9b84ee432a832609f5af6ed` | `cargo build --locked --offline -p oa-desktop --bin arcadia-studio` | Passed |
| `d6f45a13a7b8ab8dc9b84ee432a832609f5af6ed` | Strict core/desktop all-target Clippy command above | Passed |
| `d6f45a13a7b8ab8dc9b84ee432a832609f5af6ed` | Native expanded/folded Arrange, Undo, Redo, source-save and fresh-process reopen | Eight complete-Project comparisons passed; two normal closes, exit 0, empty stdout/stderr |
The three desktop tests cover exact multi-file comment/source preservation through Undo/Redo/reopen, an invalid property draft blocking Layout, and separate valid-property Apply/Layout history. Core cases include explicit reset-only change history, preserved attachments/anchors and empty entries, filtered equal/different-owner deltas, folded actual-owner versus displayed-owner movement, missing owners, excluded projections, State boundary retention, temporary invalid Batch prefix repaired before final validation, and no-op Redo.
The difference from the core-test source `6c44a2d6` to `d6f45a13` is the added native oracle example and desktop history tests. The production reset implementation, production routing and node arrangement did not change in that interval. Each gate is reported at its actual source pin; the 195-test run was not repeated against `d6f45a13`.
Receipts, raw logs and before/after source inventories for all eight gates, including both failures, are preserved in the accompanying evidence archive under `gates/`. Commands run from the identified commit's repository root. Environment was Rust 1.98.1, Windows gnullvm, LLVM-MinGW 20260908 UCRT, dev builds and one Cargo job. `--offline` requires dependencies already cached. The core command is:
```text
cargo test --locked --offline -p oa-core --lib --test release_routing --test state_diagrams --test routing_geometry --test routing_labels --test route_source --test group_movement
```
Preserved failures: `01-core-contracts` at `f0d64b531addcea584083e35296d1e5a4cb1fa8d` failed compilation with E0507 because a test moved `Option<EdgeLabel>` through a shared reference; the assertion now clones it. `02-core-contracts-fixed` at `3dfbe1959d8b5717bf8b8c1a76f31afc5ca7072a` compiled, then stopped with 19/20 routing tests passing: the new fixture incorrectly assumed Decomposition excludes FunctionalExchanges. It now uses Capabilities, includes one genuinely visible capability and retains inactive Function placements, with full independently cloned expected Project equality. No production behavior was weakened to accommodate that fixture. The corrected complete selected run is the 195-pass result above.
## Native acceptance
The native fixture supplies expanded/folded originals, two authored visible pins and a label override, fixed semantic anchors, dormant attachments, a filtered-route sentinel and an independent second view. Its expected after-Projects are produced by the candidate `Command::Layout`: they verify UI wiring/save/history against the candidate, not independently prove geometric quality. Independent contract tests and source review supply separate checks. Generate a fresh fixture directory with `arrange_reset_native_probe NEW_DIRECTORY`; compare saved JSON using `arrange_reset_native_probe --compare ACTUAL EXPECTED`. Build commands are above. Run the desktop executable with its matching LLVM-MinGW `libunwind.dll`, open the generated original JSON, and use fresh destinations for every JSON export.
Executed for expanded and folded cases: Open → Arrange → export/compare expected → Undo/export/compare original → Redo/export/compare expected → save source project. After normal process closure, a fresh process reopened each saved source project, inspected its main and sentinel views, and exported another exact comparison. All eight typed-Project comparisons passed; an independent reviewer also confirmed byte equality for these particular JSON artifact pairs. Original imports remained intact. Saved source projects are multi-file bundles, not byte-identical to the generator's single canonical source file; their fresh-process model comparisons establish semantic persistence. Desktop tests separately cover authored multi-file comments and exact source history.
Protocol correction: the initial plan assumed JSON Save overwrites the opened file. The app instead imports JSON without adopting its path; JSON Save As exports to a new destination and refuses an existing destination. The first overwrite attempt was visibly refused and preserved. Subsequent checkpoints used new JSON exports, then actual source Save for clean closure and reopen. The original staged checkpoint helper was not used. A reviewed supplemental checker, explicitly recorded as not stage-admitted, snapshots exports and invokes exact production decoding/equality without mutation. Its SHA256 is `5bcd8a568d5318dd62217c1e0b35538b22e34403c9727eee8814a5d0ec5a899f`.
The launcher admission review found and corrected a package-inventory gap before staging. Captured GUI SHA256 is `133375d57bdecf3f3effe5ecc13ec121e1257b90838f2cdb64fb91aa9a530d1b`; oracle SHA256 is `0792aff4cf8460c58fa2bf2f5ddbfa478523b7c9fe9b45e19ec93090191c37c1`. Both came from reviewed same-pin builds under exclusive local compiler/target ownership and immediate stage capture. Ordinary Cargo receipts do not hash executables at compiler exit; the capture attribution depends on that ownership observation. The evidence archive excludes executables/private profiles but retains their manifests and inventories.
All 18 full-window native screenshots and eight comparisons received independent review. Both GUI processes closed normally, with exit 0 and empty logs. At maximized 1605×872 capture size, selected pins disappeared/reappeared with Arrange/Undo, folded metadata survived, and the guide correctly explained the narrower reset versus whole Reset routing. An icon-sized screenshot was initially selected by the automation helper; its rejected click was corrected using the full-window screenshot. Short filename/set-value helper failures were resolved by absolute-path typing. These observations do not establish application input defects or resolution of the separate restored-window issue.
Material existing usability limits: Arrange requires Fit to bring the wider arrangement into view; Undo retains the viewport and can clip earlier placements until refit. Guide search filters a topic but required six scroll actions to reach the routing card. Automatic shared contacts/crossings remain. These are follow-up work, not evidence of complete issue #32 acceptance.
No full-workspace regression, performance, Linux, general layout-quality, arbitrary State-boundary native or Process/Scenario acceptance is claimed. No source reset rule promises fewer automatic elbows, universal nonintersection or readable placement by itself. Linux remains excluded by the user. Live ownership and delivery status remain in Gitea; this document records branch-specific evidence, not an integrated-state checkpoint.
## Evidence navigation
[Source review](source-review.md), [native acceptance review](native-review.md), [staging review](tooling-review.md), [supplemental checker review](export-checker-review.md). Historical plans and rejected attempts are retained as historical inputs in the archive; the normative implemented contract is in `docs/language.rst` at the tested pin.
[Archive](evidence/arrange-reset-d6f45a13.zip) and [member manifest](evidence/manifest.json): 221 members, 6529778 bytes, SHA256 `219c6dde234f1f30c4af191a538c21eacf874f234af0b8506bb66251db412650`. Every member was reread and verified by length, SHA256 and CRC. Raw evidence retains original machine paths for provenance; peers should use the commit, repository-relative paths and commands above. Native binaries/private profiles are excluded.
@@ -0,0 +1,52 @@
# Arrange reset candidate source review
Reviewed exact commit `3923860ad2923ad2bcae93b2f0c3ccbcf02d7d75` in `.local/arrange-reset-20260925` against released `715036e132677dd2bc2fb37ed316d93dc9987aec`. Diff is confined to session.rs, release_routing.rs and state_diagrams.rs. No compiler/test execution, router adoption or source changes by reviewer.
No material implementation blocker found in this bounded core reset candidate. Approval here means ready for parent compilation/verification, not accepted product behavior or successful tests.
## Implementation
session::layout checks diagram existence and Scenario rejection before freezing the ordinary projected view. Eligibility requires actual endpoint-placement IDs to equal projected source/target and both projected owner nodes present. This excludes hidden, missing-owner, outside-projection and promoted edges without routing to infer suspension. The same input view is reused for root topology ordering; no post-layout eligibility recomputation occurs.
The existing node sizing/placement and translated_routes calls remain intact. Translation uses original stored positions and actual-owner deltas with State boundary_moves_with unchanged. Only afterward, existing eligible entries have waypoints cleared and label=None. No entry insertion/removal/reordering, attachment mutation, semantic edit, other-view mutation, new routing call or generated elbow persistence is introduced. Temporarily translating an eligible pin before clearing is redundant work, but leaves the specified final state and is not a correctness defect.
Batch execution still determines staged input and final validation/history through the existing session path; no new prefix validation or nested history entry. Borrow lifetimes are coherent: immutable Project/Diagram/view work precedes the existing final mutable diagram borrow. Added owner lookup is fallible and skips absent owners. No new indexing/unwrap panic path found in the added code. Existing layout assumptions about malformed transient candidates are unchanged, not newly certified as universally panic-free.
## Test-oracle assessment
The modified preservation test narrows the intentionally changed expectation to visible pins/labels, while still checking route count/order, other view and Undo. Additional tests cover full exact route vectors including attachments-only/empty entries, fraction-only label reset, port anchors, semantic records, canonical source roundtrip, JSON Layout command parity, Undo/Redo and one-entry history.
Filtered/missing-owner test explicitly identifies one eligible edge and uses authored40/60 offsets plus a7-unit unequal owner offset; folded test adds a13-unit visible-container offset distinct from hidden children. Their expected translations are computed arithmetically by a local helper, not by the production reset/translation helper. Baseline Layout supplies stable node geometry, but named eligibility and sentinel-route expectations remain independent and nonvacuous.
The staged Batch test introduces a duplicate alias, hides routes, runs Layout and repairs the alias afterward. This meaningfully detects accidental prefix validation and stale pre-batch visibility. Its failure branch checks model/revision rollback. The no-op test confirms retained Redo for an empty-view enclosing Batch. State test exercises equal entry/leaf displacement with a stationary owning boundary and retains internal-route metadata; visible external self-route clears overrides while preserving authored attachments.
Existing State `layout_reserves_internal_rows_and_state_routes_retain_hard_pins` checks router preservation BEFORE Layout and internal-row sizing afterward. It does not contradict the new reset policy, despite its broad name; no need to erase ordinary router pin-preservation coverage.
## Limits for parent verification/integration
- No new explicit hidden semantic-port-marker case; anchor preservation is covered, marker-hidden eligibility remains source reasoning pending focused verification.
- No nonempty already-arranged diagram proving route-reset-only change/history, or a second Layout no-op on that same diagram. Current empty-view no-op test does not establish that particular scenario. A focused regression is useful if the parent expands verification, but the present reset loop visibly handles it.
- Canonical language parse/format and JSON command parity are core evidence only. They do not prove HTTP transaction handling, desktop pending-draft history, multi-file comments/raw-byte Undo/Redo or native workflows. Those remain separate integration gates rather than grounds for claiming this candidate complete.
- Eligibility tests use explicit hidden_edges and Decomposition/folding; not every saved category/coverage/filter combination is individually exercised. The implementation correctly reuses the ordinary projection, so no separate filter policy is introduced.
Keep ordinary movement, raw source edits, folded suspension and RemoveEdgeRoute tests unchanged except where the deliberate explicit Layout amendment applies. Documentation must carry the reviewed staged-Batch, enclosing-history and hidden-placement movement qualifications. Actual build/test outcomes remain pending and must be recorded independently.
## Desktop tests and native fixture follow-up at9eb4642e
Reviewed exact `9eb4642e50564165aa0db38d8e91f4c14b818bdc` above `6c44a2d6`, plus the native plan. No new source blocker found. Core production changes since3923860 are formatting only. No builds/tests/native actions executed by reviewer; parent-reported core195 pass and Clippy results remain separate receipts, and desktop tests were in progress at assignment.
Three desktop tests are meaningful at their actual layers:
- Document test creates a real include closure, inserts authored CRLF comment lines, independently clears the expected route's pins/label while retaining attachments, compares all non-target-diagram model state, preserves unrelated module bytes, and checks exact accepted bundle/model Undo/Redo plus Document reconstruction. This closes much of the earlier multi-file source evidence gap. Reconstructing Document from the bundle is not native disk-save/fresh-process reopening.
- Valid pending-property test uses ordinary Select/Field/Apply as its control, explicitly checks the revised name and original routes, then proves Layout's separate history entry with complete Project and bundle comparisons through two Undo/Redo steps. This is an App message-wiring/history test, not mounted pointer/keyboard UI evidence or an independent layout-geometry oracle.
- Invalid property test feeds incomplete numeric input, verifies accepted Project/bundle/revision/history unchanged after Layout, and retains exact raw draft. It does not simulate every invalid source draft, which is correctly outside this narrow scope.
The added nonempty already-arranged route-only core test independently names the two routes, asserts unchanged nodes and complete expected Project, one real edit, repeated no-op, Undo/Redo and retained unrelated Redo. It addresses the prior route-only history coverage gap.
Correction to earlier fixture reasoning: Decomposition admits FunctionalExchange, so it was not a valid excluded-projection sentinel. The corrected Capabilities test includes one visible capability, asserts real movement there, and exact retained inactive Function placements/routes. `projection::parents` filters out placements whose kinds are excluded by the projection; folded/filtered descendants within the admitted placement domain may still move, but arbitrary outside-projection owners receive no delta. Earlier broad wording about all stored d.nodes must be read with this admitted-domain qualification. The corrected test has a concrete oracle rather than weakening assertions to accommodate observed output.
Native fixture SHA256 `2781d2915d7fd4e428be119c36b8ffde5105e48e76f6e8f7843069f69e5899cf`; desktop tests file `935d09da577b5e2c7e4b65329e0d89010bde23b0199ad3072f82f99b5e060d26`. Original five-Function geometry is hand-authored; IDs and placement aliases are made explicit, port IDs are deterministic, and fractional witnesses are binary-exact. Both expanded/folded originals contain a filtered route and an entire other-view route sentinel. Expected after-Projects are intentionally generated by candidate Layout and asserted to preserve semantic records/other view. This is a full-model UI wiring/history oracle, not independent algorithm correctness; the source/plan label that limitation correctly. Full JSON and canonical source forms roundtrip through production parsers before writing a fresh directory.
The native plan separates immutable fixtures from writable originals, compares complete saved models, includes Undo/save/Redo/save and fresh-process reopening, and preserves normal close/log evidence. It correctly distinguishes generated elbows from authored pins and does not infer reset from fewer corners. Hidden/promoted route translation is checked against the frozen candidate expected model; independent core tests supply the separate translation contract oracle. Any inspection-only expansion must be a recorded edit outside the accepted checkpoint. No native outcome has yet been reviewed or asserted. GUI artifact/source hashes and fixture hashes must bind the eventual run; source-generated expected files alone are not acceptance.
These changes are local-only while remote publication is blocked; no pushed-code or peer-availability claim is made in this review.
@@ -0,0 +1,24 @@
# Arrange reset native tooling review
Source reviewed at `d6f45a13a7b8ab8dc9b84ee432a832609f5af6ed`; evaluator SHA256 `6a691d6605dd1da0d862ffa3da0cfca2f83547e47b26f9cb648b83061f8f7a39`. Static review plus AST parsing and isolated inventory dictionary checks only. No build, fixture/oracle execution, GUI, source edit or native acceptance.
**Found and corrected one prelaunch admission gap.** The copied launcher originally captured a new package inventory at launch and compared only against that baseline. It would accept an extra file added after stage, although a later checkpoint would reject it. With parent authorization, the draft launcher now converts the admitted stage inventory to its local row shape and requires exact equality before any process launch. Existing close verification preserves that same set. `common.py::LAUNCHER_SHA` and README are updated. Original `.local/state-label-native-20260924/launch.py` remains hash-identical. AST parsing of all four scripts and isolated equal/extra-file comparisons passed; no filesystem trial was performed.
No remaining blocker found within the declared evidence contract:
- Stage binds matching successful dev build/source/environment receipts, preserves exact GUI/oracle/runtime bytes, checks source before/after generation, and freezes package/baseline/tool inventories. It expressly depends on exclusive compiler/target ownership between build completion and capture: these Cargo receipts do not themselves hash emitted binaries. Parent must retain that ownership; this review does not upgrade that attribution to CargoJSON artifact provenance.
- `--compare` decodes actual and expected via production persistence and compares complete typed Projects. Inequality/decode failure returns nonzero; it does not call Layout, routing or write input files. This avoids the generic-JSON float representation problem. Generation labels its expected Project as candidate-Layout-derived, so it tests GUI/save wiring, not independent arrangement quality.
- Checkpoint snapshots one explicitly named working JSON, verifies exact captured input and expected/oracle/runtime bindings before/after comparison, and requires unchanged current working bytes, full source and immutable inventory. Its accepted flag asserts saved Project equality only. Source-bundle/comment restoration remains covered by separate desktop tests, not this single-JSON checkpoint.
- Launcher records PID, argv, environment, private profile/cwd, working/package inventories and observed owned-process exit. Empty logs/exit0 cannot prove how it closed or what the user clicked. Parent observations must separately bind actual Open, Arrange, Save, Undo/Redo, reopening and normal-close actions to the correct process/case/checkpoint.
Failure evidence remains in fresh directories; stage/checkpoint failures quarantine the trial. Launch observer failures preserve their receipts and do not become a successful close. Parent must reject failed/missing process receipts rather than infer acceptance from a later model checkpoint. External supervision remains responsible for a stuck GUI/oracle; the scripts intentionally do not implement automated UI or automatic GUI termination.
Reviewed draft identities after correction:
| File | SHA256 |
| --- | --- |
| common.py | `1da64294c8b2380861a4fe2d715f18563a1a0bba1317572e4c3d0a516f04132d` |
| stage.py | `2185bc1b20eee1eea3e1e4a7527318be14b76d81c3c16e67eff7104083c06d39` |
| checkpoint.py | `d8b21c75c2c8f1d29f2a730f3125992575038a1353b87d967d53edd1d5d9caf0` |
| launch.py | `9f25579451895334ca4c3c80137c4a47cd4256c720740fea70b6c6dc664255a7` |
| README.md | `920c674e65ea6f1533e7f15fd9b7ec7ad1b501e7561561a7df93b92e91022409` |
@@ -1,62 +0,0 @@
# Compact Help native evidence
Branch-specific evidence for [issue 39](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/39).
The [design record](../../compact-context-help-20260925.md) owns the decision and current acceptance boundary.
`native/` contains the Windows baseline captures from published source
`715036e132677dd2bc2fb37ed316d93dc9987aec`.
`candidate-f830/` preserves the independent review of source
`f83068757af885572f6b3f932d07233dd1b94b7e`, including its two failed Help workflows,
original screenshots, exact executable/runtime hashes and successful build log.
These observations must not be relabeled as acceptance of a successor executable.
`candidate-5657/` preserves the next frozen source's build, strict all-target
Clippy, full desktop test log and independent native review. Formatting, Clippy
and build passed. Tests: 1,538 passed, one failed, seven ignored. The sole failure
was the new caret test's synthetic typing setup before Help opened; `help-failure.log`
records its exact rerun. The next correction uses one character per key event.
The native report records corrected Help keyboard/caret behavior, minimum-window
layout and real operation-refusal feedback, with explicit scope limitations.
Copied text logs have normalized line endings and trailing blank lines removed;
original local logs are retained. Image and fixture hashes are unchanged.
`candidate-6e7c/` preserves final source
`6e7c4ddaf0775198384a8c4882809ea8e14b2976`: formatting and strict desktop/all-target
Clippy pass, the recovered GUI build passes, and 125 focused Help/allocation/guide
tests pass with zero failures/ignores. `build-diskfull.log` preserves the first
failed link; no code change was needed for the recovered build. Its native report
and 15 screenshots cover cancellation, guide consistency, group/primary selection,
minimum-window long names, exact caret return and Undo/Redo/Undo. The runtime
receipt confirms exit 0 and empty logs. Post-close baseline comparison confirms
all 37 copied fixture files unchanged. The transient OSK observation happened
before loading the model and is explicitly retained. Prior source-specific
workflows remain attributed to their actual binaries.
Reproduce final code checks on the documented Windows gnullvm/Rust 1.98.1 toolchain:
```powershell
cargo fmt --all -- --check
cargo clippy -p oa-desktop --all-targets --locked -- -D warnings
cargo build -p oa-desktop --locked
cargo test -p oa-desktop --locked -- context_help_tests allocation_drag guide::tests --test-threads=1
```
The initial test compilation began at that commit; successor work was edited
while it ran. `tests.log` (nine passed, one failed fixture precondition) and
`geometry-tests.log` (nine passed, one failed destination-width invariant) are
diagnostic evidence from that cycle, not frozen-source acceptance. The geometry
run reused its test executable without recompiling. The first command's
`allocation_drag_full_tree_tests` filter matched no tests; the second used the
actual `full_tree` filter plus the affected mounted State/Process checks.
The review used a copy of `examples/release1/project.arcadia` from the published
Windows package. All 37 fixture files matched the baseline before the trial and
again after closing without saving. `baseline-proof.json` records those bytes.
The executable is a local development build, not a published release artifact.
Reproduce the build using the documented Windows gnullvm toolchain and
`cargo build -p oa-desktop --locked`. Place its matching `libunwind.dll` beside the
executable, launch it, and open an isolated copy of the release1 example. Perform
the workflows and use the window sizes in [the native report](candidate-f830/review.md).
The baseline captures establish layout comparison; the report records actual
interaction results and explicit unverified workflows.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

@@ -1,49 +0,0 @@
{
"baseline_binary": "2c03bd6cc3f1110c219e4f57ab94874a121590f83779239a93289b7170664399",
"baseline_source": "715036e132677dd2bc2fb37ed316d93dc9987aec",
"model_files": {
"evidence/repair-check.md": "f4db17bdca4ecbb1bf0caef01b2da5f1f7f798ab3152b648ce6dd8696a6a68e5",
"expected-diagnostics.json": "3700393d76b940cfc076bf52861bd7b1f43370f8eabce3594531bd0bf1fc851e",
"model.json": "ef0ef4006c3e09d5db6e1b621256b3f7d6642f3a7b09cc34607cfbcca830c727",
"model.rs": "ddc2d92b0d7699a4cadfbb35ae379f0a68d17f4f5d7df82b4b1afcf8d5425fbe",
"oa/activities/activity_oa_restore_work.arcadia": "21cbd7176af8fad68453306f2371ff03e127a994b8796cdf044567332de9686f",
"oa/behavior/process_oa_assessment_procedure.arcadia": "23cabc213c264a16c8595414db6acb09248c1cef9cd699f1b0316e5bb82c062a",
"oa/behavior/process_oa_field_procedure.arcadia": "9c52086759b7344b4809ceb8d372f65647b16fc24153a03b887faa378c37fe96",
"oa/behavior/process_oa_response.arcadia": "675798bd19e6b40d03e7076b0e59b110cdaaab7b6d16c47c3ac80fa960072e84",
"oa/behavior/scenario_oa_conversation.arcadia": "e217e5394a0771dc16206ed9437a231da27905d2f19be64b3f48281c0a32fb6b",
"oa/behavior/scenario_oa_full_scenario.arcadia": "c5a285f2d7c61bd2a136107f267fbf9cddb1cba3f63ff36a6f53f7ab98f33df5",
"oa/behavior/state_machine_oa_monitor.arcadia": "e7349e1e5336282eed3f5a103129db33d3ae4ed500788ee7129e8fc193ac57f6",
"oa/capabilities.arcadia": "26a3d9b615a3251ff010e0b47127f2d16bc2dcd45ed95967208b0076d725843a",
"oa/model.arcadia": "069a5eaaf525089935cc9d73ae7b916866ed78c6534705d75739650ecdea7ada",
"oa/participants.arcadia": "591a75a9ecad280302be4d4ae0171ad616377ed11ead5e233848ca2dc0a71819",
"oa/types.arcadia": "f02c391d584674c879f662a418abba3a220f1fee88471ca49322ac9873de628d",
"project.arcadia": "b2674e6bf77cfe0f6f0ba79c81d2e96d92f39c02ca2c6266d80e91723bfb9fda",
"README.md": "113cfb1cf38a98b51bff60442d86a35d636a3d80ef206bece7d53d0aae67f2d5",
"sa/behavior/process_sa_assessment_procedure.arcadia": "5c414fbdaabf9f9e402b0441fcacf1cacf4dbbc6c822162f028de35abf59be2d",
"sa/behavior/process_sa_field_procedure.arcadia": "ece846fdf93857e71a9ab0efcbdf64a5c41c71180f62afa3f79077fffb53b8f2",
"sa/behavior/process_sa_response.arcadia": "af7fdda280a917cf626f6e077162d481f9aaad280f8e81330c07d7fe384b1c52",
"sa/behavior/scenario_sa_conversation.arcadia": "01339699ec3ee409418d9bb232542f2bcd9419f1e67b183e77700c56bb9f71d2",
"sa/behavior/scenario_sa_full_scenario.arcadia": "5dce6b00a3a441162e8dee3e23d9ca5ab7bed4a24de669b1fba03664a3086dda",
"sa/behavior/state_machine_sa_monitor.arcadia": "17dbcfa5bbfd10fc8e23a325fc950ce853c219b4aa89c74f96278cbb3f34acc8",
"sa/capabilities.arcadia": "81706a0ca9d7db2aa0350888b9c00d853885185db2e6b90557d3d51a11c3d645",
"sa/functions/function_sa_audit_thresholds.arcadia": "27f798f78f36e72b38edab3725af2feb0eaa010fd52a60b5c6b6bef57ad3b686",
"sa/functions/function_sa_restore_work.arcadia": "357c6acb80f86b4e8d8f5c98f9f79d140f85fbe09f3406b41b7ac6eba464ec8c",
"sa/model.arcadia": "805b90262e27090ae7da81a51bcefa49c97fb0b3d3220b4a72323b4c4351c395",
"sa/participants.arcadia": "ca144d6a8f45fe720b5d18c7b59c7671444baa8c37e5ce9117bcba34b3a6aa84",
"sa/types.arcadia": "540ddbc2bcd0ccf45298e3ea5660637341aff63ff65fd87ac2f26469d1adc064",
"traceability.arcadia": "474bd28d955ff7af7722f6e9a23220d816879e3f46202e79a233544b9c593116",
"transition-before.json": "2253e6434f741687e22327c37d520ed15c043bc36025498650bc2366bb6a874d",
"transition-request.json": "43b9a22e5f95ad3bc0ebd2478ead0e14406854abc989ae94ded9367b013d812a",
"views/diagram_oa_allocation_view.arcadia": "c45d7467839219d894804c1ef2d22ff4d4a5a84a6a273c526bafd4c1facde624",
"views/diagram_sa_allocation_view.arcadia": "352a3ca745517592b5c9b1239b5770f62f0466a329a91fd03457ef2b53c06ed6",
"views/diagram_sa_capability_view.arcadia": "8f18fcccbcddcf3c1ea0ffe727b5b23b77fc148c5da4fde953abe681e4c20cd2",
"views/diagram_sa_service_view.arcadia": "4c72cb7593d9c4be8d2600b0656a7a3a6ce84c45bb2f3e58a4ee5ae3bf91913c",
"views/diagram_sa_state_view.arcadia": "dd1bea81ac7dc549a53f9b190be6f13052e00ede345bd180444cc31edb892230"
},
"matching_files": 37,
"baseline_captures": {
"baseline-allocation-compact.png": "d92a0cca62a038dcf0894d92b37a64028342c2365e3fb8880ed597222123d96f",
"baseline-trace-compact.png": "b608a3d75612d8c06aacc094a7aebd12a0798ce30f2126c00bc3ad61a9103217",
"baseline-trace-normal.png": "6dfb6c595af4247aab3600da0e0a25ba4b11151436e9fc4a406f1036053f71d0"
}
}
@@ -1,6 +0,0 @@
{
"buildExit": 0,
"runtime": "F45604A12E8BCD839FD86F667C1BE1CC7C4F9DA2B3C69FF381AC708F0E1AE7D4",
"source": "5657a996ed2cefb1fa84ef967b655eaf8f9f6817",
"binary": "83CCEFF8B713491AFEE92FFCAFE62B02F5EEA6A3D899F6F334848FDC7BB0D88D"
}
@@ -1,2 +0,0 @@
Compiling oa-desktop v0.2.0 (C:\ar-compact-01a0d717\crates\oa-desktop)
Finished `dev` profile [unoptimized] target(s) in 2m 12s
@@ -1,6 +0,0 @@
Compiling oa-core v0.2.0 (C:\ar-compact-01a0d717\crates\oa-core)
Checking iced_widget v0.14.2 (C:\ar-compact-01a0d717\crates\vendor\iced_widget)
Compiling oa-server v0.2.0 (C:\ar-compact-01a0d717\crates\oa-server)
Compiling oa-desktop v0.2.0 (C:\ar-compact-01a0d717\crates\oa-desktop)
Checking iced v0.14.0
Finished `dev` profile [unoptimized] target(s) in 4m 09s
@@ -1,16 +0,0 @@
running 1 test
thread 'app::context_help_tests::mounted_context_help_keyboard_scroll_preserves_inspector_focus_and_exact_caret' (7368) panicked at crates\oa-desktop\src\app\context_help_tests.rs:389:5:
assertion `left == right` failed
left: "DSend"
right: "DRAFT Send"
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
test app::context_help_tests::mounted_context_help_keyboard_scroll_preserves_inspector_focus_and_exact_caret ... FAILED
failures:
failures:
app::context_help_tests::mounted_context_help_keyboard_scroll_preserves_inspector_focus_and_exact_caret
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 1545 filtered out; finished in 1.56s
@@ -1,32 +0,0 @@
# Independent native successor review
Reviewed source `5657a996ed2cefb1fa84ef967b655eaf8f9f6817`; executable SHA-256 `83CCEFF8B713491AFEE92FFCAFE62B02F5EEA6A3D899F6F334848FDC7BB0D88D` independently rechecked after the run; runtime SHA-256 `F45604A12E8BCD839FD86F667C1BE1CC7C4F9DA2B3C69FF381AC708F0E1AE7D4` from build.json. Used Windows native Computer Use, the isolated `model/project.arcadia`, normal 1440x900 and minimum 1100x700 client sizes (1442x932 and 1102x732 captures). The prior candidate report remains historical under `../candidate-f830/review.md`.
## Result
Both prior blocking native findings are corrected in the exercised workflows. No new blocking finding was observed in this bounded retest. This is not complete product, accessibility, multiselection or first-release acceptance.
### Help keyboard reading and caret preservation
1. In normal Allocation, focused inspector Name, selected its text, typed `LEFT RIGHT`, and pressed Ctrl+Left to put the caret before `RIGHT`. Opened Help with F1. PageDown visibly scrolled to lower sections; PageUp returned to the top; End reached the bottom; Home returned to the top; Down moved one increment and Up reversed it. Evidence: `02-help-pagedown.png`, `03-help-end.png`.
2. Pressed Escape, then typed `X` without refocusing. Name became exactly `LEFT XRIGHT`. Reopened with F1, clicked visible Close, then typed `Y` without refocusing. Name became exactly `LEFT XYRIGHT`. Thus keyboard reading did not move the background caret, and both dismissal paths preserved input continuation. Evidence: `04-caret-after-escape.png`, `05-caret-after-close.png`. Revert restored the accepted property.
3. Opened Help again and resized the native window to its minimum width and height through the native system menu. Help reflowed with Close reachable; End still reached the bottom and Escape returned to the same diagram. Evidence: `06-help-minimum-resized.png`.
4. At minimum size, opened System functional chain `Handle a pressure alarm`, chose W, clicked blank canvas, and entered `LEFT RIGHT` in the pending contribution Name. Ctrl+Left, F1, End, Escape, then `X` produced exactly `LEFT XRIGHT` without refocusing. Create remained disabled with its missing-source explanation and Cancel reachable. Cancel preserved the accepted model. Evidence: `15-process-caret-restored.png`.
### Compact controls and actionable feedback
- Normal and minimum Allocation show projection, Help and Arrange together, with a separate full-width Destination slot. At minimum size the canvas begins near y=350 versus baseline y=414, approximately 64 pixels recovered. Changing from no selection to a selected work card and exercising refused drags did not move the canvas boundary. Evidence: `01-allocation-normal.png`, `07-allocation-minimum.png`.
- Clicking Arrange displays `Arrange ✓`; the tooltip explains one-drag scope, Alt+A and Escape. Escape removes the check; Alt+A arms it again. Evidence: `08-arrange-tooltip-armed.png`.
- With Arrange armed, dragged `Assess pressure alarm` from Control room toward Field technician. The operation was refused with the visible footer message `Make room or adjust the view, then drag again. Zoom in to read the work and destination.` The accepted model remained saved and the work stayed allocated to Control room. Evidence: `09-arrange-refusal.png`.
- Repeated the drag without Arrange. Core validation refused the reassignment with `exchange_allocation_performer: Actual activity allocations must agree with one permitted communication direction; review the means or the activity allocations.` The shortened footer remained visible, and pointing at it exposed the complete readable explanation. The selected activity and accepted model remained unchanged. Evidence: `10-assignment-refusal.png`, `11-refusal-tooltip.png`.
- Changed View content to Traceability. Count, Add existing elements, Review layer traces, View visibility and Help all remain reachable at minimum width. Canvas starts near y=356 versus baseline y=470, approximately 114 pixels recovered. Undid this temporary projection change. Evidence: `12-trace-minimum.png`.
- Opened saved State view `5 · SA pressure supervision`: the generic idle canvas instruction is removed. Choosing +Region still shows its active creation prompt. F1 opens the States topic; Escape returns with +Region still selected, without creating an object. Evidence: `13-state-idle.png`, `14-state-active-prompt.png`.
## Limitations and minor observations
- The existing State active prompt still truncates at minimum width (`Create Region ... machine or composite s...`). This retest confirms it was retained, not that every pre-existing State prompt has complete narrow-window readability.
- The compact control says `Arrange`, while the guide still says `Arrange next drag`; its tooltip explains that wording. Minor consistency cleanup was reported to the integration owner; no user workflow failed because of it.
- Modifier-click/native multiselection and long-name group toolbar layout were not independently operated because the supported Computer Use API has no modifier-click/key-hold operation. Do not infer them from the single-selection screenshots. The parent owns mounted-widget verification separately. Source-editor caret behavior and full Scenario workflows were not repeated in this bounded successor retest.
- The approved native launch API does not expose stdout/stderr or process exit status. After Alt+F4, refreshed native window enumeration confirmed both successor application and console windows disappeared. No crash dialog was observed; this is not an asserted exit code or log inspection.
No save was performed. A post-run hash comparison against baseline-proof.json found no changed files among all 37 copied fixture files. The UI returned to the saved baseline with 94 objects, 62 links and one model check before closure. Screenshots are supporting evidence; the results above describe actual input and observed responses.
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

@@ -1,49 +0,0 @@
{
"baseline_binary": "2c03bd6cc3f1110c219e4f57ab94874a121590f83779239a93289b7170664399",
"baseline_source": "715036e132677dd2bc2fb37ed316d93dc9987aec",
"model_files": {
"evidence/repair-check.md": "f4db17bdca4ecbb1bf0caef01b2da5f1f7f798ab3152b648ce6dd8696a6a68e5",
"expected-diagnostics.json": "3700393d76b940cfc076bf52861bd7b1f43370f8eabce3594531bd0bf1fc851e",
"model.json": "ef0ef4006c3e09d5db6e1b621256b3f7d6642f3a7b09cc34607cfbcca830c727",
"model.rs": "ddc2d92b0d7699a4cadfbb35ae379f0a68d17f4f5d7df82b4b1afcf8d5425fbe",
"oa/activities/activity_oa_restore_work.arcadia": "21cbd7176af8fad68453306f2371ff03e127a994b8796cdf044567332de9686f",
"oa/behavior/process_oa_assessment_procedure.arcadia": "23cabc213c264a16c8595414db6acb09248c1cef9cd699f1b0316e5bb82c062a",
"oa/behavior/process_oa_field_procedure.arcadia": "9c52086759b7344b4809ceb8d372f65647b16fc24153a03b887faa378c37fe96",
"oa/behavior/process_oa_response.arcadia": "675798bd19e6b40d03e7076b0e59b110cdaaab7b6d16c47c3ac80fa960072e84",
"oa/behavior/scenario_oa_conversation.arcadia": "e217e5394a0771dc16206ed9437a231da27905d2f19be64b3f48281c0a32fb6b",
"oa/behavior/scenario_oa_full_scenario.arcadia": "c5a285f2d7c61bd2a136107f267fbf9cddb1cba3f63ff36a6f53f7ab98f33df5",
"oa/behavior/state_machine_oa_monitor.arcadia": "e7349e1e5336282eed3f5a103129db33d3ae4ed500788ee7129e8fc193ac57f6",
"oa/capabilities.arcadia": "26a3d9b615a3251ff010e0b47127f2d16bc2dcd45ed95967208b0076d725843a",
"oa/model.arcadia": "069a5eaaf525089935cc9d73ae7b916866ed78c6534705d75739650ecdea7ada",
"oa/participants.arcadia": "591a75a9ecad280302be4d4ae0171ad616377ed11ead5e233848ca2dc0a71819",
"oa/types.arcadia": "f02c391d584674c879f662a418abba3a220f1fee88471ca49322ac9873de628d",
"project.arcadia": "b2674e6bf77cfe0f6f0ba79c81d2e96d92f39c02ca2c6266d80e91723bfb9fda",
"README.md": "113cfb1cf38a98b51bff60442d86a35d636a3d80ef206bece7d53d0aae67f2d5",
"sa/behavior/process_sa_assessment_procedure.arcadia": "5c414fbdaabf9f9e402b0441fcacf1cacf4dbbc6c822162f028de35abf59be2d",
"sa/behavior/process_sa_field_procedure.arcadia": "ece846fdf93857e71a9ab0efcbdf64a5c41c71180f62afa3f79077fffb53b8f2",
"sa/behavior/process_sa_response.arcadia": "af7fdda280a917cf626f6e077162d481f9aaad280f8e81330c07d7fe384b1c52",
"sa/behavior/scenario_sa_conversation.arcadia": "01339699ec3ee409418d9bb232542f2bcd9419f1e67b183e77700c56bb9f71d2",
"sa/behavior/scenario_sa_full_scenario.arcadia": "5dce6b00a3a441162e8dee3e23d9ca5ab7bed4a24de669b1fba03664a3086dda",
"sa/behavior/state_machine_sa_monitor.arcadia": "17dbcfa5bbfd10fc8e23a325fc950ce853c219b4aa89c74f96278cbb3f34acc8",
"sa/capabilities.arcadia": "81706a0ca9d7db2aa0350888b9c00d853885185db2e6b90557d3d51a11c3d645",
"sa/functions/function_sa_audit_thresholds.arcadia": "27f798f78f36e72b38edab3725af2feb0eaa010fd52a60b5c6b6bef57ad3b686",
"sa/functions/function_sa_restore_work.arcadia": "357c6acb80f86b4e8d8f5c98f9f79d140f85fbe09f3406b41b7ac6eba464ec8c",
"sa/model.arcadia": "805b90262e27090ae7da81a51bcefa49c97fb0b3d3220b4a72323b4c4351c395",
"sa/participants.arcadia": "ca144d6a8f45fe720b5d18c7b59c7671444baa8c37e5ce9117bcba34b3a6aa84",
"sa/types.arcadia": "540ddbc2bcd0ccf45298e3ea5660637341aff63ff65fd87ac2f26469d1adc064",
"traceability.arcadia": "474bd28d955ff7af7722f6e9a23220d816879e3f46202e79a233544b9c593116",
"transition-before.json": "2253e6434f741687e22327c37d520ed15c043bc36025498650bc2366bb6a874d",
"transition-request.json": "43b9a22e5f95ad3bc0ebd2478ead0e14406854abc989ae94ded9367b013d812a",
"views/diagram_oa_allocation_view.arcadia": "c45d7467839219d894804c1ef2d22ff4d4a5a84a6a273c526bafd4c1facde624",
"views/diagram_sa_allocation_view.arcadia": "352a3ca745517592b5c9b1239b5770f62f0466a329a91fd03457ef2b53c06ed6",
"views/diagram_sa_capability_view.arcadia": "8f18fcccbcddcf3c1ea0ffe727b5b23b77fc148c5da4fde953abe681e4c20cd2",
"views/diagram_sa_service_view.arcadia": "4c72cb7593d9c4be8d2600b0656a7a3a6ce84c45bb2f3e58a4ee5ae3bf91913c",
"views/diagram_sa_state_view.arcadia": "dd1bea81ac7dc549a53f9b190be6f13052e00ede345bd180444cc31edb892230"
},
"matching_files": 37,
"baseline_captures": {
"baseline-allocation-compact.png": "d92a0cca62a038dcf0894d92b37a64028342c2365e3fb8880ed597222123d96f",
"baseline-trace-compact.png": "b608a3d75612d8c06aacc094a7aebd12a0798ce30f2126c00bc3ad61a9103217",
"baseline-trace-normal.png": "6dfb6c595af4247aab3600da0e0a25ba4b11151436e9fc4a406f1036053f71d0"
}
}
File diff suppressed because one or more lines are too long
@@ -1,6 +0,0 @@
{
"source": "6e7c4ddaf0775198384a8c4882809ea8e14b2976",
"binary": "D55ECAD4B0F75C0C9C26625E5441BEF9C9360890B65BC13236C6EBFAF03D631E",
"buildExit": 0,
"runtime": "F45604A12E8BCD839FD86F667C1BE1CC7C4F9DA2B3C69FF381AC708F0E1AE7D4"
}
@@ -1,2 +0,0 @@
Compiling oa-desktop v0.2.0 (C:\ar-compact-01a0d717\crates\oa-desktop)
Finished `dev` profile [unoptimized] target(s) in 2m 21s
@@ -1,2 +0,0 @@
Checking oa-desktop v0.2.0 (C:\ar-compact-01a0d717\crates\oa-desktop)
Finished `dev` profile [unoptimized] target(s) in 5m 24s
@@ -1,49 +0,0 @@
# Final native UX retest
Independent review by `/root/ux_critique`, using Windows Computer Use (`@oai/sky`). Final preparation and launch began on 2026-09-29; the resumed input session completed on 2026-10-01 America/Los_Angeles (completion clock: 2026-10-02 00:42:55 UTC).
## Identity and scope
- Source: `6e7c4ddaf0775198384a8c4882809ea8e14b2976`.
- Executable: `.local/native-final/arcadia-studio.exe`, SHA-256 `D55ECAD4B0F75C0C9C26625E5441BEF9C9360890B65BC13236C6EBFAF03D631E`.
- Runtime: `libunwind.dll`, SHA-256 `F45604A12E8BCD839FD86F667C1BE1CC7C4F9DA2B3C69FF381AC708F0E1AE7D4`.
- Read build identity from `build.json`. Parent launched this executable with a stdout/stderr/exit watcher; reviewer selected its unique returned native window and did not launch another executable.
- Model: only `.local/native-final/model/project.arcadia`, copied from the released water-pressure fixture. `baseline-proof.json` records its 37 original file hashes and baseline source/binary identity. No Save was performed.
- Normal window: 1440x900 client, captured 1442x932. Minimum: 1100x700 client, captured 1102x732. Same native environment and baseline fixture as prior review.
This is a bounded final-delta retest plus previously missing native group-selection/long-name coverage. The earlier `../candidate-5657/review.md` remains historical evidence for Process draft, State, Traceability, refused allocation, and resize-with-Help workflows on source `5657a996...`; those are not silently relabeled as this binary's results.
## Result
No blocking UX defect found in the workflows below. The two final minor corrections behave as intended, and the newly exercised native group workflow passed.
### Arrange cancellation and disclosed guidance
1. In the populated Allocation view, clicked Arrange. The control displayed its check and footer showed the armed state.
2. Pressed Escape. The check cleared and footer became `Arrange cancelled`, replacing the obsolete armed outcome. Repeated at minimum size with the same result. Evidence: `02-arrange-escape-cancelled.png`, `14-minimum-arrange-cancelled.png`.
3. Armed Arrange again, then opened contextual Help with the pointer. Help opened and footer became `Arrange cancelled`. Visible Close returned to the same model and canvas. Evidence: `03-help-cancels-arrange.png`.
4. Used PageDown to reach the relevant guide paragraphs. They now say `Choose Arrange` / `choose Arrange or press Alt+A`, matching the visible control, and explain right-click Add to selection / Inspect this element. Evidence: `04-guide-arrange-and-context-selection.png`.
### Native group selection and primary inspector
1. Clicked `Assess pressure alarm`; right-clicked `Repair valve in the field`; chose `Add to selection` from the ordinary context menu. Both cards gained selected outlines and the row displayed `2 selected`, `Hide selected`, `Clear`, and `Properties: Repair valve ... only`. Inspector showed the Repair activity. Evidence: `05-native-group-selection.png`.
2. Right-clicked the selected Assess card and chose `Inspect this element`. Both cards stayed selected; Inspector switched to Assess and the row's `Properties: ... only` summary followed it. Evidence: `06-group-primary-inspector.png`.
3. Opened F1 Help and dismissed with Escape. The same pair, primary inspector, canvas position and zoom remained. Evidence: `07-group-help-preserved.png`.
4. Resized to the configured minimum through the native window system menu. The group controls remained readable and reachable, with the full-width Destination slot above them. The intentional resize refitted the view; subsequent selection/draft/help transitions did not move its canvas boundary. Evidence: `08-group-minimum.png`.
### Long name, exact caret, Apply, Undo and Redo
1. With both cards still selected and Assess as primary, focused its Name field and replaced the draft with `Pressure alarm assessment with a deliberately long operational activity name LEFT RIGHT`.
2. Ctrl+Left placed the caret before RIGHT. Opened F1, pressed End to scroll Help, dismissed with Escape, and typed X without clicking the field. Reopened F1, used visible Close, and typed Y without refocusing. Pressing End in the restored field exposed the ending `LEFT XYRIGHT`. Thus both dismissal paths preserved the exact draft caret, not merely the draft contents. The pair and primary inspector also remained unchanged. Evidence: `09-group-long-draft-caret.png`.
3. Clicked Apply changes. Only the primary Assess activity acquired the long name; Repair retained its original label. At minimum size, the selection row remained one stable row: count, Hide selected, Clear, and a shortened `Properties: Pressure alar... only` summary. The truncation did not hide the `only` distinction or an action. The Inspector Name field remained editable/scrollable for the complete name. Evidence: `10-long-name-group-minimum.png`.
4. Clicked the non-editable summary row to leave the text input, then Ctrl+Z restored `Assess pressure alarm` and the UI's saved state. Ctrl+Y restored the long name and unsaved state. Ctrl+Z restored the original name and saved state again. The group remained selected through all three operations. Evidence: `11-long-name-undo.png`, `12-long-name-redo.png`.
5. Clicked Clear. The group cleared, Inspector returned to view properties, and the canvas boundary and card positions remained stable. Evidence: `13-clear-selection-stable-canvas.png`.
## Environment observation and limits
- Initial Ctrl+O unexpectedly opened Windows On-Screen Keyboard, obscuring the target. Its screenshot showed a highlighted Windows key; another observation showed a different highlighted key without another keyboard action from this reviewer. Input was stopped and the parent notified. The keyboard was already absent on the next fresh window enumeration; reviewer did not close it or press any Windows-key control. Used pointer Open and a literal copied-fixture path to continue. Subsequent Escape, PageDown, F1, Ctrl+A, Ctrl+Left, Alt+Space, Undo/Redo and typing produced the expected target-app responses. This transient environment interference is preserved in `00-environment-input-interference.png`; it is not attributed to an application defect.
- Native modifier-click remains untested; native group selection was genuinely exercised through the supported context menu instead. No multi-item drag/assignment was attempted in this final retest. Prior successor refused-drag evidence is separate.
- Process/State/Scenario, source-editor caret, all Help navigation keys and resize while Help is open were not all repeated on this final binary. The final run directly exercised property draft/caret, group preservation, PageDown and End, F1/Escape/Close, minimum sizing and edit recovery. Full acceptance must combine the accurately versioned evidence rather than infer missing workflows from screenshots.
- Saved-state labels, counts and visible Undo results do not prove whole-Project equality. This review made no Save; post-close byte-hash and watcher-log checks belong to the parent and are recorded separately.
Before closure, UI showed `All changes saved`, original activity names, 94 objects, 62 links and one model check. Closed normally with Alt+F4. Fresh native window enumeration confirmed the final application's window was absent. Parent was notified to inspect watcher stdout/stderr/exit and fixture hashes. No crash dialog was observed; native window disappearance alone is not an asserted exit code.
@@ -1,10 +0,0 @@
{
"source": "6e7c4ddaf0775198384a8c4882809ea8e14b2976",
"binary": "D55ECAD4B0F75C0C9C26625E5441BEF9C9360890B65BC13236C6EBFAF03D631E",
"started_utc": "2026-09-29T14:58:41.721798+00:00",
"pid": 7964,
"exit_code": 0,
"finished_utc": "2026-10-02T00:42:39.601915+00:00",
"stdout_bytes": 0,
"stderr_bytes": 0
}
@@ -1,132 +0,0 @@
Compiling oa-desktop v0.2.0 (C:\ar-compact-01a0d717\crates\oa-desktop)
Finished `test` profile [unoptimized] target(s) in 5m 01s
Running unittests src\main.rs (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps\arcadia_studio-c5b42126ba36cd38.exe)
running 125 tests
test allocation_drag::tests::allocation_artifact_and_manual_trace_cascades_are_named_and_refused ... ok
test allocation_drag::tests::arrange_tests::explicit_arrange_detects_hidden_moved_child_and_stationary_enclosure_growth ... ok
test allocation_drag::tests::arrange_tests::explicit_arrange_distributed_oa_sa_exact_scope_routes_source_history_and_anchor ... ok
test allocation_drag::tests::arrange_tests::explicit_arrange_noop_stale_tamper_target_and_all_selected_headers ... ok
test allocation_drag::tests::arrange_tests::explicit_arrange_performer_closure_does_not_acquire_incidental_semantic_root ... ok
test allocation_drag::tests::arrange_tests::explicit_arrange_preserves_existing_overlap_but_refuses_its_growth ... ok
test allocation_drag::tests::arrange_tests::explicit_arrange_preserves_roles_and_does_not_invent_unplaced_descendants ... ok
test allocation_drag::tests::arrange_tests::explicit_arrange_preserves_unrelated_folded_interior_diagnostic_boundary ... ok
test allocation_drag::tests::arrange_tests::explicit_arrange_simultaneous_roots_may_enter_each_others_vacated_bounds ... ok
test allocation_drag::tests::arrange_tests::explicit_arrange_validates_all_original_members_before_normalizing ... ok
test allocation_drag::tests::assignment_boundary_tests::group_assignment_checks_original_identity_and_visibility_before_normalization ... ok
test allocation_drag::tests::assignment_boundary_tests::group_assignment_distributed_parent_refuses_but_its_independent_leaves_can_reassign ... ok
test allocation_drag::tests::assignment_boundary_tests::group_assignment_finish_rejects_stale_project_and_each_tampered_result ... ok
test allocation_drag::tests::assignment_boundary_tests::group_assignment_normalizes_parent_child_and_assigns_unplaced_leaf_once ... ok
test allocation_drag::tests::assignment_boundary_tests::group_assignment_requires_non_anchor_selected_header_inside_viewport ... ok
test allocation_drag::tests::assignment_boundary_tests::group_assignment_unresolved_oa_role_leaf_accepts_explicit_target_but_not_outside ... ok
test allocation_drag::tests::assignment_tests::group_assignment_accumulates_stable_ids_and_aliases_without_partial_reuse ... ok
test allocation_drag::tests::assignment_tests::group_assignment_collateral_surviving_constraint_refuses_whole_semantic_candidate ... ok
test allocation_drag::tests::assignment_tests::group_assignment_deletes_only_own_routes_across_views_and_keeps_internal_flow ... ok
test allocation_drag::tests::assignment_tests::group_assignment_independent_performers_preserves_already_correct_provenance ... ok
test allocation_drag::tests::assignment_tests::group_assignment_source_failure_preserves_existing_history_and_retry_ids ... ok
test allocation_drag::tests::assignment_tests::group_assignment_unfolds_only_changed_destination_and_refuses_newly_exposed_overlap ... ok
test allocation_drag::tests::assignment_tests::group_assignment_uses_vacated_space_and_one_complete_route_owner_map ... ok
test allocation_drag::tests::assignment_tests::group_assignment_zero_delta_changes_semantics_once_and_then_is_noop ... ok
test allocation_drag::tests::cache_plan_uses_candidate_index_and_same_pixel_different_target_changes_preview ... ok
test allocation_drag::tests::collapsed_target_unfold_is_explicit_and_no_other_fold_changes ... ok
test allocation_drag::tests::direct_exit_revealing_another_role_performer_has_truthful_refusal ... ok
test allocation_drag::tests::explicit_different_performer_retains_advisory_role_warning ... ok
test allocation_drag::tests::folded_subtree_cannot_hide_new_collision ... ok
test allocation_drag::tests::geometry_refuses_new_overlap_bounds_and_invisible_headers_and_preserves_existing_overlap ... ok
test allocation_drag::tests::group_arrange_experiment::mixed_arrange_experiment_detects_moved_folded_child_collision ... ok
test allocation_drag::tests::group_arrange_experiment::mixed_arrange_experiment_detects_stationary_enclosure_growth_collision ... ok
test allocation_drag::tests::group_arrange_experiment::mixed_arrange_experiment_distributed_root_and_performer_can_translate_rigidly ... ok
test allocation_drag::tests::group_arrange_experiment::mixed_arrange_experiment_unrelated_folded_interiors_are_outside_diagnostic_domain ... ok
test allocation_drag::tests::malformed_scope_wrong_projection_duplicate_placement_and_stable_id_collision_refuse ... ok
test allocation_drag::tests::mixed_p_x_y_refuses_semantic_and_arrange_instead_of_stretching_destination ... ok
test allocation_drag::tests::oa_communication_incompleteness_remains_advisory_without_collateral_loss ... ok
test allocation_drag::tests::oa_sa_incoming_between_outgoing_preserve_identity_source_history_and_other_views ... ok
test allocation_drag::tests::oa_sa_one_leaf_move_refuses_when_ancestor_regrouping_introduces_unrelated_overlap ... ok
test allocation_drag::tests::oa_sa_one_leaf_reassignment_and_outward_move_regroup_ancestors_without_moving_sibling ... ok
test allocation_drag::tests::oa_sa_populated_collapsed_target_accepts_room_and_refuses_collision_without_relocating_contents ... ok
test allocation_drag::tests::oa_sa_state_owned_action_and_availability_reject_reassignment_atomically ... ok
test allocation_drag::tests::oa_sa_unfolding_prior_hidden_overlap_refuses_even_when_insertion_does_not_collide ... ok
test allocation_drag::tests::own_deleted_allocation_routes_may_disappear_in_every_diagram_only ... ok
test allocation_drag::tests::role_only_same_effective_performer_is_semantic_noop_and_outgoing_refuses ... ok
test allocation_drag::tests::sa_port_and_deferred_exchange_allocation_cascade_refuses_and_retarget_is_invalid ... ok
test allocation_drag::tests::same_performer_readability_preserves_snapped_noop_and_allocation_identity ... ok
test allocation_drag::tests::scope_detects_first_child_allocation_and_role_transfer_and_finish_rejects_live_edits ... ok
test allocation_drag::tests::selected_header_visibility_allows_viewport_edge_but_refuses_clipping ... ok
test allocation_drag::tests::snapped_noop_returns_no_command_and_keeps_source_revision_and_redo ... ok
test allocation_drag::tests::source_budget_refusal_keeps_project_bundle_history_and_retry_identity_exact ... ok
test allocation_drag::tests::surviving_constraint_mutation_is_refused_even_when_core_candidate_is_valid ... ok
test allocation_drag::tests::target_presentation_requires_accepted_identity_and_selected_header_visibility ... ok
test allocation_drag::tests::toolbar_identity_allows_exact_incoming_placement_at_old_label_overlap ... ok
test allocation_drag::tests::uniform_folded_grandchild_and_unplaced_scope_moves_exactly_q_and_all_l_oa_sa ... ok
test allocation_drag::tests::uniform_role_only_and_mixed_direct_role_composites_same_target_do_not_allocate ... ok
test allocation_drag::tests::uniformly_unassigned_composite_creates_only_stable_leaf_allocations_including_unplaced ... ok
test allocation_drag::tests::unrequested_view_option_cleanup_is_not_whitelisted ... ok
test allocation_drag::tests::unresolved_role_leaf_can_arrange_or_accept_explicit_target_without_guessing ... ok
test app::allocation_drag::tests::actual_canvas_origin_change_cancels_held_work_before_any_legacy_release ... ok
test app::allocation_drag::tests::arrange_is_one_gesture_and_snapped_noop_preserves_redo_and_source ... ok
test app::allocation_drag::tests::candidate_semantics_are_cached_per_destination_with_stable_ids_and_own_index ... ok
test app::allocation_drag::tests::destination_identity_does_not_describe_blocked_or_outside_viewport_as_unallocation ... ok
test app::allocation_drag::tests::frozen_collapsed_target_uses_only_visible_header_and_port_edge_controls_occlude ... ok
test app::allocation_drag::tests::frozen_targets_use_half_open_semantic_priority_and_refuse_incomparable_overlap ... ok
test app::allocation_drag::tests::full_tree::actual_app_tree_dirty_description_retains_focus_and_middle_caret_for_next_key ... ok
test app::allocation_drag::tests::full_tree::actual_app_tree_dirty_name_retains_focus_and_middle_caret_for_next_key ... ok
test app::allocation_drag::tests::full_tree::actual_app_tree_exact_incoming_drag_keeps_destination_visible_and_canvas_stable ... ok
test app::allocation_drag::tests::full_tree::actual_keyboard_arrange_shortcut_consumes_once_escape_cancels_and_dirty_owner_refuses ... ok
test app::allocation_drag::tests::full_tree::actual_rendered_canvas_pixels_do_not_change_when_toolbar_feedback_text_changes ... ok
test app::allocation_drag::tests::full_tree::actual_toolbar_hover_inspects_last_full_name_without_authorizing_an_outside_drop ... ok
test app::allocation_drag::tests::full_tree::control_focus::dirty_selected_pin_outer_ring_keeps_real_name_focus_and_caret ... ok
test app::allocation_drag::tests::group::allocation_bare_moves_cannot_bypass_armed_active_or_retired_lifetime ... ok
test app::allocation_drag::tests::group::completed_text_apply_rejects_old_allocation_packets_without_retiring_a_new_drag ... ok
test app::allocation_drag::tests::group::connection_lifetime::captured_connection_after_cancel_preserves_arrange_arm_and_live_group ... ok
test app::allocation_drag::tests::group::connection_lifetime::captured_connection_context_mismatches_preserve_the_current_session ... ok
test app::allocation_drag::tests::group::connection_lifetime::captured_connection_current_restart_and_pair_commit_after_tool_boundary ... ok
test app::allocation_drag::tests::group::group_arrange_work_and_performer_anchors_preserve_every_responsibility ... ok
test app::allocation_drag::tests::group::group_assignment_split_rebuild_preserves_primary_and_commits_one_complete_candidate ... ok
test app::allocation_drag::tests::group::group_out_and_back_assigns_at_zero_delta_then_true_noop_preserves_redo ... ok
test app::allocation_drag::tests::group::group_selection_aba_and_old_press_cannot_retire_a_new_token ... ok
test app::allocation_drag::tests::group::group_source_refusal_retains_primary_source_and_both_history_paths ... ok
test app::allocation_drag::tests::group::idle_group_press_rejects_selection_generation_aba_with_unchanged_other_stamps ... ok
test app::allocation_drag::tests::group::ordinary_off_grid_performer_out_and_back_is_noop_with_redo ... ok
test app::allocation_drag::tests::group::route_lifetime::captured_route_context_fields_independently_reject_before_retirement ... ok
test app::allocation_drag::tests::group::route_lifetime::current_route_commits_without_session_preserve_exact_route_history ... ok
test app::allocation_drag::tests::group::route_lifetime::same_context_route_attachment_after_selection_preserves_arrange_arm_and_live_group ... ok
test app::allocation_drag::tests::group::route_lifetime::same_context_route_commit_after_selection_preserves_arrange_arm_and_live_group ... ok
test app::allocation_drag::tests::group::route_lifetime::stale_route_attachment_preserves_arrange_arm_and_live_group ... ok
test app::allocation_drag::tests::group::route_lifetime::stale_route_commit_preserves_arrange_arm_and_live_group ... ok
test app::allocation_drag::tests::group::unsupported_group_click_collapses_but_threshold_crossing_never_becomes_click ... ok
test app::allocation_drag::tests::modal_roundtrip_equal_fit_undo_resize_and_focus_retire_press_before_replay ... ok
test app::allocation_drag::tests::mounted_clean_stationary_selects_exact_work_without_revision_or_legacy_apply ... ok
test app::allocation_drag::tests::mounted_dirty_port_press_cannot_fall_through_to_applying_port_selection ... ok
test app::allocation_drag::tests::mounted_dirty_property_press_does_not_select_apply_or_destroy_draft ... ok
test app::allocation_drag::tests::mounted_oa_sa_ordered_incoming_between_outgoing_are_exact_single_history_steps ... ok
test app::allocation_drag::tests::mounted_role_only_outgoing_refusal_retains_placement_and_same_performer_creates_no_allocation ... ok
test app::allocation_drag::tests::mounted_uniform_parent_moves_hidden_placements_and_assigns_only_leaves ... ok
test app::allocation_drag::tests::navigation_union::old_chooser_envelopes_preserve_a_current_canvas_gesture ... ok
test app::allocation_drag::tests::navigation_union::stale_gesture_packets_preserve_navigation_and_a_new_live_cancel_still_works ... ok
test app::allocation_drag::tests::oa_sa_clipped_selected_header_refuses_preview_release_and_retries_without_partial_source_or_history ... ok
test app::allocation_drag::tests::oa_sa_toolbar_identity_accepts_exact_receiver_and_collapsed_header_drag ... ok
test app::allocation_drag::tests::outside_release_is_cancel_and_does_not_remove_an_assignment ... ok
test app::allocation_drag::tests::pending_behavior_draft_refuses_actual_work_press_and_equal_document_replacement_retires_packet ... ok
test app::allocation_drag::tests::release_uses_ordered_native_position_before_later_motion_in_same_batch ... ok
test app::allocation_drag::tests::same_snapped_position_on_opposite_shared_border_rebuilds_semantics_even_if_layout_refuses ... ok
test app::allocation_drag::tests::source_and_modal_admission_reject_prebuilt_clean_press_without_selecting ... ok
test app::allocation_drag::tests::source_refusal_is_atomic_and_a_new_gesture_can_retry_against_live_revision ... ok
test app::allocation_drag::tests::stale_packets_do_not_cancel_a_newer_token_and_each_release_is_consumed_once ... ok
test app::allocation_drag::tests::toolbar_destination_names_transformed_target_whose_header_is_clipped ... ok
test app::allocation_drag::tests::toolbar_status_leaves_transformed_top_edge_work_header_readable ... ok
test app::context_help_tests::context_help_accepts_live_process_viewport_and_rejects_stale_measurements ... ok
test app::context_help_tests::context_help_barrier_retires_text_selection_drag_without_changing_the_caret ... ok
test app::context_help_tests::context_help_disables_allocation_input_including_early_work_drag_dispatch ... ok
test app::context_help_tests::context_help_escape_preserves_pending_behavior_form_and_blocks_submit ... ok
test app::context_help_tests::context_help_f1_selects_scenario_topic_without_leaving_the_workspace ... ok
test app::context_help_tests::context_help_preserves_group_property_draft_camera_and_model ... ok
test app::context_help_tests::context_help_rejects_a_reference_picker_open_queued_from_the_inspector ... ok
test app::context_help_tests::context_help_retains_source_draft_and_rejects_background_source_input ... ok
test app::context_help_tests::mounted_context_help_keyboard_scroll_preserves_inspector_focus_and_exact_caret ... ok
test app::context_help_tests::mounted_context_help_observes_modifier_release_before_plain_canvas_click ... ok
test app::context_help_tests::mounted_context_help_retires_held_pan_without_replaying_its_release ... ok
test guide::tests::filtering_and_related_navigation_keep_selection_visible ... ok
test guide::tests::guide_topic_navigation_resets_only_article_and_same_topic_search_keeps_scroll ... ok
test guide::tests::search_finds_concepts_in_walkthroughs_and_api_reference ... ok
test result: ok. 125 passed; 0 failed; 0 ignored; 0 measured; 1421 filtered out; finished in 75.83s
Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

Some files were not shown because too many files have changed in this diff Show More