Save unverified allocation and repair work at user-requested pause
This commit is contained in:
@@ -416,6 +416,12 @@ impl Rect {
|
||||
&& other.position.x + other.size.width <= self.position.x + self.size.width
|
||||
&& other.position.y + other.size.height <= self.position.y + self.size.height
|
||||
}
|
||||
fn overlaps(self, other: Self) -> bool {
|
||||
self.position.x < other.position.x + other.size.width
|
||||
&& other.position.x < self.position.x + self.size.width
|
||||
&& self.position.y < other.position.y + other.size.height
|
||||
&& other.position.y < self.position.y + self.size.height
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
@@ -583,6 +589,18 @@ pub fn place(
|
||||
"Make room or adjust the view, then drag again.".into(),
|
||||
));
|
||||
}
|
||||
// A performer may legitimately enclose the work, but its frozen feedback
|
||||
// label is painted above the candidate. Check their joint readability after
|
||||
// projection, including same-performer moves; never shift either rectangle.
|
||||
if presentation
|
||||
.target
|
||||
.is_some_and(|target| header.overlaps(target.label))
|
||||
{
|
||||
return Err(Refusal::LayoutRequired(
|
||||
"The work header overlaps the performer label. Aim below or beside the label, then drag again."
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
Ok(PlacementPlan {
|
||||
diagram,
|
||||
view,
|
||||
|
||||
@@ -1029,6 +1029,91 @@ fn target_presentation_must_name_the_destination_and_its_visible_frozen_header()
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frozen_performer_label_refuses_overlapping_work_header_and_allows_nearby_exact_move() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = fixture(layer);
|
||||
f.p.elements.get_mut(&f.a).unwrap().name = "Receiver".into();
|
||||
let plan = semantic(&f, Destination::Performer(f.a));
|
||||
let mut context = presentation(&f.p, f.d, Some(f.a));
|
||||
// The actual frozen renderer bounds for Receiver at (100, 100).
|
||||
context.target.as_mut().unwrap().label = Rect {
|
||||
position: xy(110., 131.),
|
||||
size: Size { width: 88., height: 22. },
|
||||
};
|
||||
let label = context.target.unwrap().label;
|
||||
let counterexample = Rect {
|
||||
position: xy(120., 110.),
|
||||
size: Size { width: 224., height: 62. },
|
||||
};
|
||||
assert!(context.viewport.contains(label));
|
||||
assert!(context.viewport.contains(counterexample));
|
||||
assert!(label.contains(Rect {
|
||||
position: xy(134., 145.),
|
||||
size: Size { width: 1., height: 1. },
|
||||
}), "the formerly admitted work name begins underneath the label");
|
||||
let before = plan.project().clone();
|
||||
assert!(matches!(
|
||||
place(&plan, xy(120., 105.), context),
|
||||
Err(Refusal::LayoutRequired(message)) if message.contains("Aim below or beside")
|
||||
));
|
||||
assert_eq!(plan.project(), &before);
|
||||
|
||||
// A 50-unit lower pointer position clears the frozen label. The planner
|
||||
// preserves the user's snapped position instead of packing either card.
|
||||
let placed = place(&plan, xy(120., 155.), context).unwrap();
|
||||
assert_eq!(placed.diagram.nodes[&f.n].position, xy(120., 160.));
|
||||
let after = roundtrip(&f.p, finish(&plan, &f.p, &placed).unwrap().unwrap());
|
||||
assert_eq!(validation::effective_performer(&after, f.w), Some(f.a));
|
||||
assert_eq!(after.diagrams[&f.d].nodes[&f.n].position, xy(120., 160.));
|
||||
assert_eq!(after.elements, f.p.elements);
|
||||
assert_eq!(after.diagrams[&f.second], f.p.diagrams[&f.second]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frozen_label_readability_allows_touching_edges_but_refuses_positive_area_overlap() {
|
||||
let f = fixture(Layer::Oa);
|
||||
let plan = semantic(&f, Destination::Performer(f.a));
|
||||
let mut context = presentation(&f.p, f.d, Some(f.a));
|
||||
context.target.as_mut().unwrap().label = Rect {
|
||||
position: xy(110., 131.),
|
||||
size: Size { width: 90., height: 19. },
|
||||
};
|
||||
for (position, allowed) in [
|
||||
(xy(190., 110.), false),
|
||||
(xy(200., 110.), true),
|
||||
(xy(210., 110.), true),
|
||||
(xy(120., 140.), false),
|
||||
(xy(120., 150.), true),
|
||||
(xy(120., 160.), true),
|
||||
] {
|
||||
let result = place(&plan, position, context);
|
||||
assert_eq!(result.is_ok(), allowed, "{position:?}: {result:?}");
|
||||
if let Ok(placed) = result {
|
||||
assert_eq!(placed.diagram.nodes[&f.n].position, position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_performer_readability_preserves_snapped_noop_and_allocation_identity() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = fixture(layer);
|
||||
let a = f.a;
|
||||
allocation(&mut f, a);
|
||||
f.p.diagrams.get_mut(&f.d).unwrap().nodes.get_mut(&f.n).unwrap().position = xy(150., 200.);
|
||||
let plan = semantic(&f, Destination::Performer(f.a));
|
||||
let context = presentation(&f.p, f.d, Some(f.a));
|
||||
assert_eq!(plan.project(), &f.p);
|
||||
let noop = place(&plan, xy(154., 200.), context).unwrap();
|
||||
assert!(finish(&plan, &f.p, &noop).unwrap().is_none());
|
||||
let moved = place(&plan, xy(160., 210.), context).unwrap();
|
||||
let after = roundtrip(&f.p, finish(&plan, &f.p, &moved).unwrap().unwrap());
|
||||
assert_eq!(after.relations, f.p.relations);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geometry_refuses_new_overlap_bounds_and_invisible_headers_and_preserves_existing_overlap() {
|
||||
let mut f = fixture(Layer::Oa);
|
||||
|
||||
@@ -3060,11 +3060,17 @@ impl App {
|
||||
|| self.path.as_ref() == Some(&receipt.target)
|
||||
{
|
||||
self.path = Some(receipt.target);
|
||||
self.document.bind_repair_baseline(disk.reachable_bundle());
|
||||
self.disk_project = Some(disk);
|
||||
}
|
||||
self.status = "Interrupted disk save recovered. Current model and drafts are retained; Save all can be retried.".into();
|
||||
}
|
||||
Err(error) => {
|
||||
if self.path.is_none()
|
||||
|| self.path.as_ref() == Some(&receipt.target)
|
||||
{
|
||||
self.document.invalidate_saved_baseline();
|
||||
}
|
||||
self.status = format!(
|
||||
"Disk recovery completed; files could not be reloaded: {error}"
|
||||
)
|
||||
@@ -3841,6 +3847,7 @@ impl App {
|
||||
self.canvas_document = Id::new_v4();
|
||||
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
||||
self.path = Some(path);
|
||||
self.document.bind_repair_baseline(disk.reachable_bundle());
|
||||
self.source_workspace = Workspace::new(&disk.bundle, false);
|
||||
self.source_workspace.reachable = disk.reachable.clone();
|
||||
self.show_source_file(false);
|
||||
@@ -4508,6 +4515,10 @@ fn read_snapshot(path: PathBuf) -> Result<LoadedSnapshot, String> {
|
||||
#[path = "app/save_as_tests.rs"]
|
||||
mod save_as_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "app/repair_save_state_tests.rs"]
|
||||
mod repair_save_state_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod source_project_tests {
|
||||
use super::*;
|
||||
|
||||
@@ -132,7 +132,7 @@ fn mounted_oa_sa_ordered_incoming_between_outgoing_are_exact_single_history_step
|
||||
let mut f = fixture(layer);
|
||||
let original = f.app.project().clone();
|
||||
let original_source = f.app.document.bundle().clone();
|
||||
drag(&mut f, screen(xy(160., 140.)));
|
||||
drag(&mut f, screen(xy(160., 190.)));
|
||||
assert_eq!(f.app.document.revision(), 1, "{}", f.app.status);
|
||||
let relation = f
|
||||
.app
|
||||
@@ -150,7 +150,7 @@ fn mounted_oa_sa_ordered_incoming_between_outgoing_are_exact_single_history_step
|
||||
assert!(!f.app.document.can_undo());
|
||||
let _ = f.app.update(Message::Redo);
|
||||
assert_eq!(f.app.document.bundle(), &after_in);
|
||||
drag(&mut f, screen(xy(960., 140.)));
|
||||
drag(&mut f, screen(xy(960., 190.)));
|
||||
let reassigned = &f.app.project().relations[&relation.id];
|
||||
assert_eq!(reassigned.source(), f.b, "{}", f.app.status);
|
||||
assert_eq!(reassigned.name, relation.name);
|
||||
@@ -208,7 +208,7 @@ fn mounted_dirty_property_press_does_not_select_apply_or_destroy_draft() {
|
||||
let input = &f.app.draft.as_ref().unwrap().inputs["/name"];
|
||||
let raw = input.raw.clone();
|
||||
let cursor = input.editor.cursor();
|
||||
let events = batch(&f.app, start(&f), screen(xy(160., 140.)));
|
||||
let events = batch(&f.app, start(&f), screen(xy(160., 190.)));
|
||||
assert!(matches!(
|
||||
events.first(),
|
||||
Some(Message::WorkDrag(Event::Refused(..)))
|
||||
@@ -256,7 +256,7 @@ fn source_and_modal_admission_reject_prebuilt_clean_press_without_selecting() {
|
||||
let _ = f.app.update(Message::WorkDrag(Event::Release(
|
||||
p.stamp,
|
||||
p.token,
|
||||
Some(screen(xy(160., 140.))),
|
||||
Some(screen(xy(160., 190.))),
|
||||
)));
|
||||
assert_eq!(f.app.selected, selection);
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
@@ -302,7 +302,7 @@ fn modal_roundtrip_equal_fit_undo_resize_and_focus_retire_press_before_replay()
|
||||
let _ = f.app.update(Message::WorkDrag(Event::Release(
|
||||
p.stamp,
|
||||
p.token,
|
||||
Some(screen(xy(160., 140.))),
|
||||
Some(screen(xy(160., 190.))),
|
||||
)));
|
||||
assert_eq!(f.app.document.bundle(), &before);
|
||||
assert!(!f.app.document.can_undo());
|
||||
@@ -321,7 +321,7 @@ fn stale_packets_do_not_cancel_a_newer_token_and_each_release_is_consumed_once()
|
||||
let _ = f.app.update(Message::WorkDrag(Event::Preview(
|
||||
old.stamp,
|
||||
old.token,
|
||||
Some(screen(xy(160., 140.))),
|
||||
Some(screen(xy(160., 190.))),
|
||||
)));
|
||||
assert_eq!(
|
||||
f.app.allocation_drag.active.as_ref().unwrap().press.token,
|
||||
@@ -330,7 +330,7 @@ fn stale_packets_do_not_cancel_a_newer_token_and_each_release_is_consumed_once()
|
||||
let release = Message::WorkDrag(Event::Release(
|
||||
new.stamp,
|
||||
new.token,
|
||||
Some(screen(xy(160., 140.))),
|
||||
Some(screen(xy(160., 190.))),
|
||||
));
|
||||
let _ = f.app.update(release.clone());
|
||||
let revision = f.app.document.revision();
|
||||
@@ -370,7 +370,7 @@ fn arrange_is_one_gesture_and_snapped_noop_preserves_redo_and_source() {
|
||||
#[test]
|
||||
fn outside_release_is_cancel_and_does_not_remove_an_assignment() {
|
||||
let mut f = fixture(Layer::Oa);
|
||||
drag(&mut f, screen(xy(160., 140.)));
|
||||
drag(&mut f, screen(xy(160., 190.)));
|
||||
let before = f.app.document.bundle().clone();
|
||||
let revision = f.app.document.revision();
|
||||
drag(
|
||||
@@ -386,8 +386,8 @@ fn candidate_semantics_are_cached_per_destination_with_stable_ids_and_own_index(
|
||||
let mut f = fixture(Layer::Oa);
|
||||
let p = press(&f);
|
||||
let _ = f.app.update(Message::WorkDrag(Event::Press(p.clone())));
|
||||
let a = screen(xy(160., 140.));
|
||||
let b = screen(xy(960., 140.));
|
||||
let a = screen(xy(160., 190.));
|
||||
let b = screen(xy(960., 190.));
|
||||
let _ = f
|
||||
.app
|
||||
.update(Message::WorkDrag(Event::Preview(p.stamp, p.token, Some(a))));
|
||||
@@ -508,8 +508,8 @@ fn frozen_targets_use_half_open_semantic_priority_and_refuse_incomparable_overla
|
||||
fn release_uses_ordered_native_position_before_later_motion_in_same_batch() {
|
||||
let mut f = fixture(Layer::Oa);
|
||||
let from = start(&f);
|
||||
let a = screen(xy(160., 140.));
|
||||
let b = screen(xy(960., 140.));
|
||||
let a = screen(xy(160., 190.));
|
||||
let b = screen(xy(960., 190.));
|
||||
let mut state = CanvasState::default();
|
||||
let canvas = canvas(&f.app);
|
||||
let events: Vec<_> = [
|
||||
@@ -603,7 +603,7 @@ fn source_refusal_is_atomic_and_a_new_gesture_can_retry_against_live_revision()
|
||||
f.app.document = crate::document::Document::from_bundle(before.clone(), limits).unwrap();
|
||||
f.app.sync();
|
||||
let snapshot = f.app.document.snapshot();
|
||||
drag(&mut f, screen(xy(160., 140.)));
|
||||
drag(&mut f, screen(xy(160., 190.)));
|
||||
assert!(Arc::ptr_eq(&snapshot, &f.app.document.snapshot()));
|
||||
assert_eq!(f.app.document.bundle(), &before);
|
||||
assert!(!f.app.document.can_undo());
|
||||
@@ -611,7 +611,7 @@ fn source_refusal_is_atomic_and_a_new_gesture_can_retry_against_live_revision()
|
||||
assert!(f.app.allocation_drag.active.is_none());
|
||||
f.app.document = crate::document::Document::from_bundle(before, Default::default()).unwrap();
|
||||
f.app.sync();
|
||||
drag(&mut f, screen(xy(160., 140.)));
|
||||
drag(&mut f, screen(xy(160., 190.)));
|
||||
assert_eq!(
|
||||
validation::effective_performer(f.app.project(), f.work),
|
||||
Some(f.a),
|
||||
@@ -656,7 +656,7 @@ fn mounted_role_only_outgoing_refusal_retains_placement_and_same_performer_creat
|
||||
.find(|t| t.element == f.a)
|
||||
.unwrap()
|
||||
.region;
|
||||
drag(&mut f, screen(xy(target.x + 45., target.y + 40.)));
|
||||
drag(&mut f, screen(xy(target.x + 45., target.y + 100.)));
|
||||
assert_eq!(f.app.document.revision(), 1, "{}", f.app.status);
|
||||
assert_eq!(f.app.project().relations, relations);
|
||||
assert!(
|
||||
@@ -686,7 +686,7 @@ fn mounted_uniform_parent_moves_hidden_placements_and_assigns_only_leaves() {
|
||||
.collapsed = true;
|
||||
replace(&mut f, project);
|
||||
let before = f.app.project().diagrams[&f.diagram].nodes.clone();
|
||||
drag(&mut f, screen(xy(960., 140.)));
|
||||
drag(&mut f, screen(xy(960., 190.)));
|
||||
assert_eq!(f.app.document.revision(), 1, "{}", f.app.status);
|
||||
let after = &f.app.project().diagrams[&f.diagram].nodes;
|
||||
let dx = after[&f.node].position.x - before[&f.node].position.x;
|
||||
@@ -805,7 +805,7 @@ fn pending_behavior_draft_refuses_actual_work_press_and_equal_document_replaceme
|
||||
crate::behavior::Event::Creation(crate::behavior::creation::Event::Open(Layer::Oa)),
|
||||
);
|
||||
assert!(f.app.behavior.has_pending());
|
||||
let events = batch(&f.app, start(&f), screen(xy(160., 140.)));
|
||||
let events = batch(&f.app, start(&f), screen(xy(160., 190.)));
|
||||
assert!(matches!(
|
||||
events.first(),
|
||||
Some(Message::WorkDrag(Event::Refused(..)))
|
||||
@@ -823,7 +823,7 @@ fn pending_behavior_draft_refuses_actual_work_press_and_equal_document_replaceme
|
||||
let _ = f.app.update(Message::WorkDrag(Event::Release(
|
||||
p.stamp,
|
||||
p.token,
|
||||
Some(screen(xy(160., 140.))),
|
||||
Some(screen(xy(160., 190.))),
|
||||
)));
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
assert!(!f.app.document.can_undo());
|
||||
@@ -854,7 +854,7 @@ fn actual_canvas_origin_change_cancels_held_work_before_any_legacy_release() {
|
||||
&mut state,
|
||||
&canvas::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
|
||||
moved_bounds,
|
||||
mouse::Cursor::Available(screen(xy(160., 140.))),
|
||||
mouse::Cursor::Available(screen(xy(160., 190.))),
|
||||
)
|
||||
.and_then(|a| a.into_inner().0)
|
||||
.unwrap();
|
||||
@@ -953,3 +953,92 @@ fn toolbar_status_leaves_transformed_top_edge_frozen_target_label_readable() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oa_sa_label_overlap_refuses_preview_release_and_retries_without_partial_source_or_history() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
for (zoom, pan) in [
|
||||
(1., Vector::ZERO),
|
||||
(0.75, Vector::new(37., -15.)),
|
||||
(1.25, Vector::new(-30., 23.)),
|
||||
] {
|
||||
let mut f = fixture(layer);
|
||||
let mut project = f.app.project().clone();
|
||||
project.elements.get_mut(&f.a).unwrap().name = "Receiver".into();
|
||||
replace(&mut f, project);
|
||||
// Refusals must also preserve an existing redo branch.
|
||||
let _ = f.app.update(Message::Command(Command::Rename(f.work, "Temporary".into())));
|
||||
let _ = f.app.update(Message::Undo);
|
||||
f.app.zoom = zoom;
|
||||
f.app.pan = pan;
|
||||
f.app.fit_to_view = false;
|
||||
let screen = |world: Position| Point::new(
|
||||
bounds().x + world.x * zoom + pan.x,
|
||||
bounds().y + world.y * zoom + pan.y,
|
||||
);
|
||||
let from = screen(xy(140., 535.));
|
||||
let overlap = screen(xy(160., 140.));
|
||||
let clear = screen(xy(160., 190.));
|
||||
let accepted = f.app.project().clone();
|
||||
let source = f.app.document.bundle().clone();
|
||||
let snapshot = f.app.document.snapshot();
|
||||
let revision = f.app.document.revision();
|
||||
let mut events = batch(&f.app, from, overlap);
|
||||
let release = events.pop().unwrap();
|
||||
send(&mut f.app, events);
|
||||
assert!(f.app.allocation_drag.preview.is_none());
|
||||
let feedback = f.app.allocation_drag.feedback.as_ref().unwrap();
|
||||
assert!(feedback.refused, "{}", feedback.text);
|
||||
assert!(feedback.text.contains("Aim below or beside"));
|
||||
assert_eq!(feedback.press.frozen.targets[feedback.targets[0]].element, f.a);
|
||||
assert_eq!(f.app.project(), &accepted);
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
assert!(Arc::ptr_eq(&snapshot, &f.app.document.snapshot()));
|
||||
assert!(!f.app.document.can_undo());
|
||||
assert!(f.app.document.can_redo());
|
||||
let _ = f.app.update(release);
|
||||
assert!(f.app.allocation_drag.active.is_none());
|
||||
assert!(f.app.allocation_drag.preview.is_none());
|
||||
assert_eq!(f.app.project(), &accepted);
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
assert_eq!(f.app.document.revision(), revision);
|
||||
assert!(Arc::ptr_eq(&snapshot, &f.app.document.snapshot()));
|
||||
assert!(!f.app.document.can_undo());
|
||||
assert!(f.app.document.can_redo());
|
||||
|
||||
// In a new gesture, moving out of the collision reuses its semantic
|
||||
// candidate and stable allocation identity; only geometry changes.
|
||||
let mut events = batch(&f.app, from, overlap);
|
||||
events.pop();
|
||||
send(&mut f.app, events);
|
||||
let active = f.app.allocation_drag.active.as_ref().unwrap();
|
||||
let p = active.press.clone();
|
||||
let semantic = active.cache[0].1.as_ref().unwrap().clone();
|
||||
let _ = f.app.update(Message::WorkDrag(Event::Preview(p.stamp, p.token, Some(clear))));
|
||||
let preview = f.app.allocation_drag.preview.as_ref().expect(&f.app.status);
|
||||
assert!(Arc::ptr_eq(&semantic, &preview.semantic));
|
||||
assert_eq!(preview.placement.diagram.nodes[&f.node].position, xy(120., 160.));
|
||||
assert_eq!(preview.placement.view, projection::project_diagram_with_index(
|
||||
semantic.project(), &preview.placement.diagram, semantic.index(),
|
||||
));
|
||||
let mut expected = semantic.project().clone();
|
||||
expected.diagrams.insert(f.diagram, preview.placement.diagram.clone());
|
||||
assert_eq!(f.app.project(), &accepted);
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
let _ = f.app.update(Message::WorkDrag(Event::Release(p.stamp, p.token, Some(clear))));
|
||||
assert_eq!(f.app.project(), &expected, "{}", f.app.status);
|
||||
assert_eq!(f.app.document.revision(), revision + 1);
|
||||
assert!(f.app.document.can_undo());
|
||||
assert!(!f.app.document.can_redo());
|
||||
let after = f.app.document.bundle().clone();
|
||||
assert_eq!(oa_core::source_project::compile_bundle(&after, Default::default()).unwrap().decoded.project, expected);
|
||||
let _ = f.app.update(Message::Undo);
|
||||
assert_eq!(f.app.project(), &accepted);
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
assert!(!f.app.document.can_undo());
|
||||
let _ = f.app.update(Message::Redo);
|
||||
assert_eq!(f.app.project(), &expected);
|
||||
assert_eq!(f.app.document.bundle(), &after);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
use super::*;
|
||||
|
||||
const VALID: &str = concat!(
|
||||
"arcadia 7;\r\n",
|
||||
"project repair \"Repair 救援\" id \"9e8f0000-0000-4000-8000-000000000001\" {\r\n",
|
||||
" oa_activity assess \"Assess\" id \"9e8f0000-0000-4000-8000-000000000002\" {}\r\n",
|
||||
"}\r\n",
|
||||
);
|
||||
|
||||
fn open(app: &mut App, path: &std::path::Path) {
|
||||
let _ = app.update(Message::Loaded(Ok(Some(
|
||||
read_snapshot(path.to_owned()).unwrap(),
|
||||
))));
|
||||
}
|
||||
|
||||
fn edit(app: &mut App, text: &str) {
|
||||
let _ = app.update(Message::SourceEdit(text_editor::Action::SelectAll));
|
||||
let _ = app.update(Message::SourceEdit(text_editor::Action::Edit(
|
||||
text_editor::Edit::Paste(Arc::new(text.into())),
|
||||
)));
|
||||
}
|
||||
|
||||
fn save(app: &mut App, path: &std::path::Path) {
|
||||
let receipt = write_snapshot(
|
||||
app.canvas_document,
|
||||
app.document.revision(),
|
||||
app.document.snapshot(),
|
||||
path.to_owned(),
|
||||
app.disk_project.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
let _ = app.update(Message::Saved(Ok(Some(receipt))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repair_to_previous_saved_bytes_requires_save_at_new_destination_and_guards_close() {
|
||||
for extra in [
|
||||
" include \"missing.arcadia\";\r\n",
|
||||
" oa_activity assess \"Duplicate\" id \"9e8f0000-0000-4000-8000-000000000003\" {}\r\n",
|
||||
] {
|
||||
let first = tempfile::tempdir().unwrap();
|
||||
let second = tempfile::tempdir().unwrap();
|
||||
let first_path = first.path().canonicalize().unwrap().join("project.arcadia");
|
||||
let second_path = second
|
||||
.path()
|
||||
.canonicalize()
|
||||
.unwrap()
|
||||
.join("project.arcadia");
|
||||
let invalid = VALID.strip_suffix("}\r\n").unwrap().to_owned() + extra + "}\r\n";
|
||||
std::fs::write(&first_path, VALID).unwrap();
|
||||
std::fs::write(&second_path, &invalid).unwrap();
|
||||
let mut app = App::with_project(Project::empty());
|
||||
open(&mut app, &first_path);
|
||||
assert!(!app.dirty());
|
||||
let before = app.document.snapshot();
|
||||
let revision = app.document.revision();
|
||||
open(&mut app, &second_path);
|
||||
assert!(!app.source_workspace.applied);
|
||||
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
|
||||
edit(&mut app, VALID);
|
||||
assert!(app.apply_source(), "{}", app.status);
|
||||
assert!(app.source_workspace.applied);
|
||||
assert!(!app.source_dirty());
|
||||
assert_eq!(app.document.bundle(), &before.bundle);
|
||||
assert_eq!(app.project(), &before.decoded.project);
|
||||
assert_eq!(app.document.revision(), revision);
|
||||
assert!(
|
||||
!app.document.can_undo(),
|
||||
"Equal content creates no history step"
|
||||
);
|
||||
assert!(app.document.dirty());
|
||||
assert_eq!(app.hosted_label(), "Unsaved changes");
|
||||
assert_eq!(std::fs::read(&second_path).unwrap(), invalid.as_bytes());
|
||||
assert_eq!(std::fs::read(&first_path).unwrap(), VALID.as_bytes());
|
||||
let close = window::Id::unique();
|
||||
let task = app.update(Message::Request(Pending::Close(close)));
|
||||
assert!(iced_runtime::task::into_stream(task).is_none());
|
||||
assert!(matches!(app.pending, Some(Pending::Close(id)) if id == close));
|
||||
let _ = app.update(Message::Cancel);
|
||||
save(&mut app, &second_path);
|
||||
assert!(!app.dirty());
|
||||
assert_eq!(app.hosted_label(), "All changes saved");
|
||||
assert_eq!(std::fs::read(&second_path).unwrap(), VALID.as_bytes());
|
||||
assert_eq!(std::fs::read(&first_path).unwrap(), VALID.as_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repair_history_compares_to_new_disk_baseline_and_then_successful_save() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let path = directory
|
||||
.path()
|
||||
.canonicalize()
|
||||
.unwrap()
|
||||
.join("project.arcadia");
|
||||
std::fs::write(&path, VALID).unwrap();
|
||||
let mut app = App::with_project(Project::empty());
|
||||
open(&mut app, &path);
|
||||
let before = app.document.snapshot();
|
||||
let invalid = format!("{VALID} unfinished {{");
|
||||
std::fs::write(&path, &invalid).unwrap();
|
||||
open(&mut app, &path);
|
||||
let repaired = format!("{VALID}// Repaired rationale\r\n");
|
||||
edit(&mut app, &repaired);
|
||||
assert!(app.apply_source());
|
||||
assert!(app.dirty());
|
||||
let _ = app.update(Message::Undo);
|
||||
assert_eq!(app.document.bundle(), &before.bundle);
|
||||
assert!(
|
||||
app.dirty(),
|
||||
"Old saved bytes are not saved at the repair destination"
|
||||
);
|
||||
let _ = app.update(Message::Redo);
|
||||
assert!(app.dirty());
|
||||
assert_eq!(std::fs::read(&path).unwrap(), invalid.as_bytes());
|
||||
|
||||
std::fs::write(&path, "external edit").unwrap();
|
||||
let failed = write_snapshot(
|
||||
app.canvas_document,
|
||||
app.document.revision(),
|
||||
app.document.snapshot(),
|
||||
path.clone(),
|
||||
app.disk_project.clone(),
|
||||
)
|
||||
.unwrap_err();
|
||||
let _ = app.update(Message::Saved(Err(failed)));
|
||||
assert!(app.dirty());
|
||||
assert_eq!(std::fs::read(&path).unwrap(), b"external edit");
|
||||
std::fs::write(&path, &invalid).unwrap();
|
||||
save(&mut app, &path);
|
||||
assert!(!app.dirty());
|
||||
let _ = app.update(Message::Undo);
|
||||
assert!(app.dirty());
|
||||
assert_eq!(app.document.bundle(), &before.bundle);
|
||||
let _ = app.update(Message::Redo);
|
||||
assert!(!app.dirty(), "Redo to the new saved snapshot is clean");
|
||||
assert_eq!(std::fs::read(&path).unwrap(), repaired.as_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_equal_content_at_distinct_destinations_stays_clean() {
|
||||
let first = tempfile::tempdir().unwrap();
|
||||
let second = tempfile::tempdir().unwrap();
|
||||
let first_path = first.path().canonicalize().unwrap().join("project.arcadia");
|
||||
let second_path = second
|
||||
.path()
|
||||
.canonicalize()
|
||||
.unwrap()
|
||||
.join("project.arcadia");
|
||||
std::fs::write(&first_path, VALID).unwrap();
|
||||
std::fs::write(&second_path, VALID).unwrap();
|
||||
let mut app = App::with_project(Project::empty());
|
||||
open(&mut app, &first_path);
|
||||
let before = app.document.bundle().clone();
|
||||
open(&mut app, &second_path);
|
||||
assert_eq!(app.document.bundle(), &before);
|
||||
assert_eq!(app.path.as_ref(), Some(&second_path));
|
||||
assert!(!app.dirty());
|
||||
assert_eq!(app.hosted_label(), "All changes saved");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recovering_applied_repair_rebinds_rolled_back_disk_bytes_without_changing_model() {
|
||||
let first = tempfile::tempdir().unwrap();
|
||||
let second = tempfile::tempdir().unwrap();
|
||||
let first_path = first.path().canonicalize().unwrap().join("project.arcadia");
|
||||
let root = second.path().canonicalize().unwrap();
|
||||
let path = root.join("project.arcadia");
|
||||
let before = VALID.replace("Repair 救援", "Before 救援");
|
||||
std::fs::write(&first_path, VALID).unwrap();
|
||||
std::fs::write(&path, VALID).unwrap();
|
||||
let journal = root.join(".arcadia-save-project.arcadia.journal");
|
||||
std::fs::create_dir(&journal).unwrap();
|
||||
std::fs::write(
|
||||
journal.join("journal.json"),
|
||||
br#"{"version":1,"entry":"project.arcadia","files":[{"path":"project.arcadia","before":true,"write":true}]}"#,
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(journal.join("0.before"), &before).unwrap();
|
||||
std::fs::write(journal.join("0.after"), VALID).unwrap();
|
||||
let mut app = App::with_project(Project::empty());
|
||||
open(&mut app, &first_path);
|
||||
let accepted = app.document.snapshot();
|
||||
open(&mut app, &path);
|
||||
assert_eq!(app.pending_recovery.as_ref(), Some(&path));
|
||||
assert!(!app.source_workspace.applied);
|
||||
assert!(app.apply_source());
|
||||
assert!(
|
||||
!app.dirty(),
|
||||
"Current bytes still match disk before rollback"
|
||||
);
|
||||
let _ = app.update(Message::SourceRecovered(recover_snapshot(path.clone())));
|
||||
assert!(app.pending_recovery.is_none());
|
||||
assert!(!journal.exists());
|
||||
assert_eq!(std::fs::read(&path).unwrap(), before.as_bytes());
|
||||
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
||||
assert!(!app.source_dirty());
|
||||
assert!(app.dirty(), "Recovery changed disk, not the accepted model");
|
||||
assert_eq!(app.hosted_label(), "Unsaved changes");
|
||||
save(&mut app, &path);
|
||||
assert!(!app.dirty());
|
||||
assert_eq!(std::fs::read(&path).unwrap(), VALID.as_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recovery_missing_entry_or_failed_reload_preserves_current_destination_save_authority() {
|
||||
for (current_destination, failed_reload) in
|
||||
[(true, false), (false, false), (true, true), (false, true)]
|
||||
{
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let root = directory.path().canonicalize().unwrap();
|
||||
let path = root.join("project.arcadia");
|
||||
std::fs::write(&path, VALID).unwrap();
|
||||
let mut app = App::with_project(Project::empty());
|
||||
open(&mut app, &path);
|
||||
let accepted = app.document.snapshot();
|
||||
let journal = root.join(".arcadia-save-project.arcadia.journal");
|
||||
std::fs::create_dir(&journal).unwrap();
|
||||
std::fs::write(
|
||||
journal.join("journal.json"),
|
||||
br#"{"version":1,"entry":"project.arcadia","files":[{"path":"project.arcadia","before":false,"write":true}]}"#,
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(journal.join("0.after"), VALID).unwrap();
|
||||
open(&mut app, &path);
|
||||
assert!(app.apply_source());
|
||||
assert!(!app.dirty());
|
||||
let destination = tempfile::tempdir().unwrap();
|
||||
let saved_elsewhere = destination
|
||||
.path()
|
||||
.canonicalize()
|
||||
.unwrap()
|
||||
.join("project.arcadia");
|
||||
if !current_destination {
|
||||
save(&mut app, &saved_elsewhere);
|
||||
assert_eq!(app.path.as_ref(), Some(&saved_elsewhere));
|
||||
assert_eq!(app.pending_recovery.as_ref(), Some(&path));
|
||||
}
|
||||
let mut receipt = recover_snapshot(path.clone()).unwrap();
|
||||
assert!(
|
||||
receipt.disk.as_ref().unwrap().bundle.files.is_empty(),
|
||||
"Missing entry reloads as an empty repair workspace"
|
||||
);
|
||||
if failed_reload {
|
||||
// Separately exercise the adapter's successful-recovery/failed-read
|
||||
// boundary; first-save rollback itself does not cause a read error.
|
||||
receipt.disk = Err("Injected post-recovery reload failure".into());
|
||||
}
|
||||
let _ = app.update(Message::SourceRecovered(Ok(receipt)));
|
||||
assert!(!path.exists());
|
||||
assert!(!journal.exists());
|
||||
assert!(app.pending_recovery.is_none());
|
||||
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
||||
assert_eq!(app.dirty(), current_destination);
|
||||
if current_destination {
|
||||
let _ = app.update(Message::Request(Pending::New));
|
||||
assert!(matches!(app.pending, Some(Pending::New)));
|
||||
} else {
|
||||
assert_eq!(app.path.as_ref(), Some(&saved_elsewhere));
|
||||
assert_eq!(std::fs::read(&saved_elsewhere).unwrap(), VALID.as_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,7 +95,7 @@ pub struct Document {
|
||||
past: Vec<HistoryEntry>,
|
||||
future: Vec<HistoryEntry>,
|
||||
current_bytes: usize,
|
||||
saved: Arc<SourceBundle>,
|
||||
saved: Option<Arc<SourceBundle>>,
|
||||
revision: u64,
|
||||
view_index: std::cell::OnceCell<oa_core::view_options::ViewIndex>,
|
||||
limits: ProjectLimits,
|
||||
@@ -125,7 +125,7 @@ impl Document {
|
||||
current_bytes,
|
||||
past: Vec::new(),
|
||||
future: Vec::new(),
|
||||
saved: Arc::new(bundle),
|
||||
saved: Some(Arc::new(bundle)),
|
||||
revision: 0,
|
||||
view_index: Default::default(),
|
||||
limits,
|
||||
@@ -156,7 +156,7 @@ impl Document {
|
||||
}
|
||||
|
||||
pub fn dirty(&self) -> bool {
|
||||
self.recovered_unsaved || self.bundle() != self.saved.as_ref()
|
||||
self.recovered_unsaved || self.saved.as_deref() != Some(self.bundle())
|
||||
}
|
||||
|
||||
/// Recovered work is a detached copy until explicitly saved by its user.
|
||||
@@ -181,10 +181,24 @@ impl Document {
|
||||
/// Records the bytes actually saved, even when later edits remain current.
|
||||
/// The I/O adapter owns document identity and save completion ordering.
|
||||
pub fn mark_saved(&mut self, snapshot: Arc<CompiledSourceProject>) {
|
||||
self.saved = Arc::new(snapshot.bundle.clone());
|
||||
self.saved = Some(Arc::new(snapshot.bundle.clone()));
|
||||
self.recovered_unsaved = false;
|
||||
}
|
||||
|
||||
/// Bind an installed or recovered repair destination to source read there.
|
||||
/// These bytes may be invalid or differ from the retained accepted model,
|
||||
/// but must replace the previous disk baseline. Accepted history is retained;
|
||||
/// applying or undoing to old saved content does not save this destination.
|
||||
pub(crate) fn bind_repair_baseline(&mut self, source: SourceBundle) {
|
||||
self.saved = Some(Arc::new(source));
|
||||
}
|
||||
|
||||
/// Recovery changed the current destination, but its source could not be
|
||||
/// reloaded. No accepted or historical snapshot is known to be saved there.
|
||||
pub(crate) fn invalidate_saved_baseline(&mut self) {
|
||||
self.saved = None;
|
||||
}
|
||||
|
||||
/// Check a source Save as before writing any files. A retained included file
|
||||
/// must never be displaced by the new entry, including on a later Undo.
|
||||
pub fn check_save_entry(&self, entry: &str) -> Result<(), DocumentError> {
|
||||
|
||||
@@ -188,7 +188,7 @@ fn article(topic: Topic) -> Article {
|
||||
"Choose New in the top bar. Use Project properties to name the model and describe its purpose. Apply changes commits the property draft.",
|
||||
"On Architecture, choose Create architecture view. Architecture · allocated work shows work inside its performer.",
|
||||
"Expand the diagram's side palette to see tool names. Choose Entity, click its position in the diagram, enter Rescue coordinator and press Enter. Escape cancels the draft. Use Actor for an external participant such as a Vessel in distress. Right-clicking a blank area also offers Create Entity here.",
|
||||
"Right-click Rescue coordinator and choose Add activity. Enter Assess incident and press Enter to create the activity and its performer allocation together. Ordinary Activity creation in the palette leaves work unallocated; use + Relationship with Allocation to assign existing work. The allocated-work view shows work inside its performer.",
|
||||
"Right-click Rescue coordinator and choose Add activity. Enter Assess incident and press Enter to create the activity and its performer allocation together. Ordinary Activity creation in the palette leaves work unallocated. In the allocated-work view, drag existing work into exposed space inside its performer to assign it; the highlighted performer and status explain the proposed change.",
|
||||
"Use Add existing to place an element that is already in the model. The initial choices match this view; no search is required. Include already shown offers Locate for elements already placed here.",
|
||||
"Add a second activity for another participant. Select an activity, choose + Relationship, select Interaction and choose the compatible target. Name the exchange after what is conveyed.",
|
||||
"Visit Model checks to review unfinished work. Save all writes the accepted model and applied drafts to your chosen file.",
|
||||
@@ -263,7 +263,7 @@ fn article(topic: Topic) -> Article {
|
||||
&[
|
||||
"Choose the Operational or System perspective. Create participants using Entity/Actor in OA or System/System actor in SA.",
|
||||
"In the diagram's side palette, choose Activity in OA or Function in SA, click its position, enter a name and press Enter. Escape cancels without creating an element. Select a card and press F2 to rename it in place. For semantic decomposition, use Model explorer, select existing work and use + In selected to create smaller work units.",
|
||||
"Right-click a participant and choose Add activity or Add function to create new work and its allocation together. To assign existing leaf work, use + Relationship with Allocation from the participant to the work. Model explorer also offers + Allocated when a participant and the appropriate work kind are selected.",
|
||||
"Right-click a participant and choose Add activity or Add function to create new work and its allocation together. In Architecture · allocated work, drag existing work into a performer to assign it, between performers to reassign it, or onto empty canvas to remove its direct assignment. + Relationship with Allocation and Model explorer's + Allocated provide explicit alternatives.",
|
||||
"Choose Architecture · allocated work to review responsibilities. Choose Functional dataflow to review the activity/function hierarchy and its exchanges independently.",
|
||||
],
|
||||
),
|
||||
@@ -289,7 +289,7 @@ fn article(topic: Topic) -> Article {
|
||||
const {
|
||||
Article {
|
||||
summary: "A diagram is a view of the model. Its boxes, positions and nesting reveal selected relationships; moving a box does not redefine engineering ownership.",
|
||||
keywords: "containment semantic parent hierarchy nested nesting allocation deployment diagram placement arrange fit fold hide zoom resize routing connection bend waypoint detour label reset selective visibility category categories coverage process context duplicate copy items",
|
||||
keywords: "containment semantic parent hierarchy nested nesting allocation deployment diagram placement arrange next drag Alt+A auto-layout fit fold hide zoom resize routing connection bend waypoint detour label reset selective visibility category categories coverage process context duplicate copy items",
|
||||
sections: &[
|
||||
explain(
|
||||
"Three things to distinguish",
|
||||
@@ -309,7 +309,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 is a broader exploration view; OA ↔ SA traceability displays counterparts.",
|
||||
"Select an element and edit Semantic parent to change ownership. Use an Allocation relationship to change responsibility. Apply changes validates the semantic change.",
|
||||
"Drag nodes to arrange the current view, scroll to zoom and use Fit to frame its contents. Arrange changes diagram geometry. Export SVG produces a shareable view.",
|
||||
"Scroll to zoom and use Fit to frame the current view. In allocated-work views, dragging work changes its assignment and placement together. Choose Arrange next drag or press Alt+A to move work without changing responsibility for that one drag; Escape cancels the armed action. Auto-layout arranges the whole view. Export SVG produces a shareable view.",
|
||||
"Select a diagram node to access Placement, Fold / unfold and Hide. Hide removes its view placement; Delete from model removes model records and requires review of the affected references.",
|
||||
],
|
||||
),
|
||||
@@ -317,6 +317,14 @@ fn article(topic: Topic) -> Article {
|
||||
"Keep diagrams readable",
|
||||
"Use separate views for responsibility, decomposition and cross-layer coverage. Place existing elements with Show in this diagram. A placement's Diagram field can relocate it while preserving its identity and artifact references. Do not duplicate a semantic element merely to show it in another view.",
|
||||
),
|
||||
walkthrough(
|
||||
"Move work between performers",
|
||||
&[
|
||||
"Finish or cancel property and source edits, then drag an Activity or Function body in Architecture · allocated work. Aim at exposed performer space. Read the highlighted destination and status before releasing; release outside the canvas or press Escape to cancel.",
|
||||
"The move preserves work ownership and identities. A uniformly assigned work group moves its leaves together; a group distributed across performers asks you to choose a leaf. Role responsibilities remain explicit: an outward drag that would leave a role-derived performer explains why it cannot remove that responsibility.",
|
||||
"Make room when headers, labels or other cards would overlap. Move the view or zoom in if the work and destination are not readable. 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.",
|
||||
],
|
||||
),
|
||||
walkthrough(
|
||||
"Save a focused exchange view",
|
||||
&[
|
||||
@@ -336,7 +344,7 @@ 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.",
|
||||
"Right-click a connection and choose Reset routing to clear its pins and label position in this view. The Properties Actions menu provides the same commands. Arrange 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.",
|
||||
"Right-click a connection and choose Reset routing to clear its pins and label position in this view. The Properties Actions menu provides the same commands. 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.",
|
||||
],
|
||||
),
|
||||
explain(
|
||||
|
||||
@@ -2037,6 +2037,23 @@ will occur. Reparenting work must not silently change its performer. A computed
|
||||
parent summary must remain distinguishable from direct allocation. A collapse
|
||||
operation hides presentation detail without deleting the children.
|
||||
|
||||
In an Allocation view, dragging an Activity or Function body MAY combine a
|
||||
performer change with diagram placement. The preview MUST identify the
|
||||
destination and preserve readable work and performer labels. A clean click
|
||||
selects without applying unrelated drafts. A refused work press with an unapplied
|
||||
property draft MUST preserve its text, caret and focused editor. An accepted drag is one atomic
|
||||
model/source/history transaction: semantic ownership and work identities are
|
||||
preserved; only leaf direct Allocation relations, their own retired routes and
|
||||
the affected view geometry change. Existing Allocation identities are retained on reassignment. Role
|
||||
realizations and assignments MUST NOT be silently changed. Uniform work groups
|
||||
may move their leaf responsibilities together; ambiguous or distributed groups
|
||||
and changes requiring collateral record edits MUST be refused with an explanation.
|
||||
Moving outside performer regions removes direct assignment only when the
|
||||
effective role responsibility is also absent. Releasing outside the viewport
|
||||
cancels the gesture. Explicit Arrange next drag applies geometry alone for one
|
||||
gesture. Refused and snapped no-op drops MUST NOT create history entries or
|
||||
discard Redo. Undo restores the exact source bundle and identities together.
|
||||
|
||||
Applying a reachable source bundle is one model transaction. Graphical edits
|
||||
update its owning declarations through the source-preserving updater. Both
|
||||
paths validate their candidate and model/source equality before
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ Graphical and textual parity applies to the persistent fields of this profile. D
|
||||
|
||||
- OA captures operational needs; SA defines the selected system contribution. SA contains no internal logical/physical components, and Studio does not decompose SA Actors. The System may be absent while authoring, with a completeness warning.
|
||||
- Work decomposition and performer allocation are independent. Only leaves have direct allocations. A parent drawn within one performer can be a computed leaf summary, not an additional allocation. Role sharing does not create several direct performers for one function.
|
||||
- Nesting expresses semantic containment or the selected allocation projection. Placement movement does not change ownership or allocation. Hide and delete have different effects.
|
||||
- Nesting expresses semantic containment or the selected allocation projection. A placement edit preserves ownership and allocation. In an Allocation view, a work-body drag previews and commits performer reassignment and placement as one transaction; Arrange next drag (Alt+A) keeps that gesture limited to geometry. Role responsibilities remain unchanged, and drops requiring ambiguous responsibility or collateral edits are refused. Hide and delete have different effects. Current verification and native limits are recorded in [issue 3](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/3).
|
||||
- Counterpart creation copies mapped typed content and required references. Existing-counterpart semantic review compares typed facts and prior observations, with explicit keep, take or final-value decisions for supported scalar, structural, constructor and individual collection-row changes. Reference mapping and candidate validation apply before an undoable transaction. Ordered collections have explicit final-order review with retained rejected drafts. Included trace rows can combine multiple source contributions into one validated model/history transaction; conflicting decisions and incompatible reference mappings remain explicit blockers. Existing Child/Incident attachment fields and artifact lifecycle review are implemented. Artifact creation reserves an explicit counterpart identity and requires all seven metadata decisions; identified source-owner absence can delete a link, while destination-only Take retains it. Owner moves and kept Presence must agree. Latest attachment/artifact native acceptance remains pending. Review does not automatically synchronize arbitrary structure or overwrite the other perspective. Creation scope follows ownership/reference dependencies, not every incident relationship.
|
||||
- Processes/chains use contributions and explicit sequence links; imported ordered exchange paths retain their implied order until topology conversion. Rich fragments belong to scenarios. Guards, expressions and state behavior text are descriptive; there is no execution engine. Mixed states and modes are permitted by this Studio profile.
|
||||
- OA Communication means connect Entities/Actors directly and can allocate Interactions whose actual performers match their endpoints and direction. Transition to SA requires explicit compatible boundary-port decisions; the tool does not guess system ports. Preserved OA boundary exchanges use Legacy ports. Capability grouping is containment; include, extend and generalization use separate typed relationships in OA and SA. Data/item references remain within their layer and can have explicit transitioned counterparts.
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Usage-limit pause — 2026-09-12
|
||||
|
||||
The user requested saving all work and stopping. Root session Codex-956atf stopped the allocation worker and started no further builds or checks. Resume only when requested. Linux/WSL setup, networking, builds and checks remain paused pending the user's explicit instruction; Computer Use is also paused.
|
||||
|
||||
This is a saved handoff, not a replacement for live coordination in Gitea issue #3. Branch: `codex/behavior-authoring-20260910`; publication branch: `codex/release1-956atf`.
|
||||
|
||||
## Verified baseline
|
||||
|
||||
- `f52d81c1b96a34bb78b8ada27b7761dc0b009f5d` integrated allocation drag with guarded input and atomic history. The Windows focused run passed 54/54 tests, with all 525 build inputs matching Git before the run and unchanged afterward. Evidence remains in `.local/allocation-union-20260912/focused-f52d81c.{log,result.json,inputs.json}`. No combined full-suite, strict Clippy, GUI or native acceptance is claimed for that revision.
|
||||
- `bc8bc4f5a1bf4a8cd53ad1829becd559ea536657` archived the older exact a31b40d Windows and bounded OA native evidence. See `release-behavior-canvas-a31b40d-native-20260911.md` and its linked archive. Those results apply to a31b40d, not the subsequent allocation changes.
|
||||
|
||||
## Work saved in this checkpoint — unverified
|
||||
|
||||
- Adapted the peer's saved-destination repair correction from `33c6d83e9ba9a5b1310b1b627d7c40affcbcf15c` into `document.rs`, the existing App paths, and five regression tests in `app/repair_save_state_tests.rs`. This binds or invalidates the saved baseline when an invalid destination is opened/recovered, preserving the accepted model and history. Tests have not run in this checkout.
|
||||
- Updated the embedded guide, scope and language specification for allocation work dragging, atomic assignment and placement, draft preservation, and the one-gesture Arrange/Alt+A mode. `PROJECT_GOAL.md` was not changed.
|
||||
- Interrupted the allocation worker during its correction for frozen performer labels covering the moving work's name/header. Its current changes in `allocation_drag.rs`, `allocation_drag/tests.rs`, and `app/allocation_drag_tests.rs` are saved as incomplete and unverified. Do not infer that requested full-tree coverage was completed.
|
||||
|
||||
The source review remains locally at `.local/allocation-repair-review-20260912.md`. Its remaining P2 reproducer places a work rectangle at approximately (120,110) under a frozen Receiver label at (110,131), 88 by 22: each fits the viewport, but the label obscures the work name. Resume by reviewing the interrupted admission rule and refusal feedback, then covering refusal plus a nearby usable drop under representative transforms. Refusal/release must preserve source, IDs and history exactly.
|
||||
|
||||
After reviewing the saved changes, run the combined Windows suite and strict Clippy against an identified commit, using `.local/run-allocation-gate.py` and the existing `.local/toolchain/env.ps1`. Serialize compiler work and record exact inputs. Resolve failures before claiming acceptance or publishing a release. Keep native usability gaps explicit. The source checkpoint is not a tested deliverable.
|
||||
|
||||
At the stop check, no `cargo`, `rustc`, or `arcadia-studio` process was running. All three delegated agents are stopped or completed.
|
||||
|
||||
Publication of this checkpoint was rejected because the remote publication branch contains newer work. A read-only verification found `codex/release1-956atf` at `97c91cc06c9a4880eeb5868b83973b6b983bff19`. The local checkpoint is preserved; no force push or integration was attempted during the requested stop. Inspect remote changes and ownership before reconciling on resume.
|
||||
Reference in New Issue
Block a user