Compare commits

..
Author SHA1 Message Date
Codex-ytr83r 7c2b06b63f test: import text editor actions in Allocation regression 2026-09-23 23:44:24 -07:00
108 changed files with 183 additions and 18070 deletions
-3
View File
@@ -12,9 +12,6 @@
*.png binary
*.jpg binary
*.zip binary
harness/scenario-selection-evidence-20260924/** -text whitespace=cr-at-eol
harness/scenario-group-spacing-evidence-20260924/** -text whitespace=cr-at-eol
harness/scenario-reorder-interaction-evidence-20260924/** -text whitespace=cr-at-eol
# Exact source/recovery oracles and archived ledgers retain their measured bytes.
# Keep ordinary text diffs; only disable checkout/check-in newline conversion.
+4 -83
View File
@@ -25,8 +25,6 @@ mod review_navigation_tests;
#[cfg(test)]
mod scenario_details_tests;
mod scenario_diagram;
mod scenario_multiselect;
pub(crate) mod scenario_reorder;
mod scenario_spacing;
#[cfg(test)]
mod source_indentation_tests;
@@ -217,14 +215,11 @@ mod direct_scenario_tests {
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Select(
context, f.sender,
)));
let fresh = app.scenario_context().unwrap();
assert_eq!(fresh.visit, context.visit);
assert_eq!(fresh.representation, context.representation);
assert_ne!(
fresh.epoch, context.epoch,
"selection retires older gestures"
assert_eq!(
app.scenario_context(),
Some(context),
"same-owner selection retains gesture context"
);
let context = fresh;
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Reorder {
context,
occurrence: created,
@@ -442,7 +437,6 @@ pub enum Message {
BehaviorDiagram(Choice),
StateDiagram(crate::behavior::state_edit::Event),
ScenarioCanvas(crate::behavior::scenario::CanvasEvent),
ScenarioReorder(Box<scenario_reorder::Event>),
TopologyCanvas(crate::behavior::topology::CanvasEvent),
PlaceSelected,
HideSelected,
@@ -529,7 +523,6 @@ pub struct App {
pub guide: crate::guide::State,
pub traceability: crate::views::traceability::State,
pub behavior: crate::behavior::State,
pub(crate) scenario_reorder: scenario_reorder::State,
pub data_state: crate::data_view::State,
pub inspector_tab: crate::navigation::InspectorTab,
pub layer: Layer,
@@ -652,7 +645,6 @@ impl App {
guide: crate::guide::State::default(),
traceability: Default::default(),
behavior: Default::default(),
scenario_reorder: Default::default(),
data_state: Default::default(),
inspector_tab: Default::default(),
layer,
@@ -1356,45 +1348,8 @@ impl App {
])
}
pub fn update(&mut self, message: Message) -> Task<Message> {
// Captured reorder packets must be admitted before shared navigation,
// camera or source boundaries can disturb a newer operation.
if let Message::ScenarioReorder(event) = message {
return self.scenario_reorder_event(*event);
}
if let Message::ScenarioCanvas(behavior::scenario::CanvasEvent::Selection(event)) = message
{
return self.scenario_selection_event(*event);
}
// A delayed Scenario menu cannot retire the current reveal/gesture even
// when it uses the same semantic owner after a representation round trip.
if let Message::Menu(crate::model_menu::Event::Open(request)) = &message
&& request.scenario.is_some()
&& crate::model_menu::Opened::new(self, request.clone(), self.menu_sequence).is_none()
{
return Task::none();
}
if let Message::Menu(crate::model_menu::Event::Invoke(token, _)) = &message
&& let Some(menu) = &self.model_menu
&& menu.request.scenario.is_some()
&& (menu.token != *token || !menu.valid(self))
{
return Task::none();
}
if self.scenario_group_selected()
&& matches!(
&message,
Message::Command(Command::MoveStep(..)) | Message::Delete
)
{
self.status = "Group reordering and removal need a whole-selection operation. Select one occurrence to edit it individually.".into();
return Task::none();
}
let preview_read_only = self.process_preview_primary().is_some();
let selection_boundary = self.multiselect_boundary();
let scenario_selection_before = self.scenario_context().and_then(|context| {
self.multiselect_snapshot()
.map(|selection| (context, selection))
});
// Reject delayed gestures before they can retire newer input ownership.
if let Message::Connection(
crate::connection_edit::Event::Endpoint(captured, _)
@@ -1563,16 +1518,6 @@ impl App {
}) {
return Task::none();
}
if self.scenario_group_selected()
&& matches!(
event,
behavior::scenario::CanvasEvent::Select(..)
| behavior::scenario::CanvasEvent::Remove(..)
| behavior::scenario::CanvasEvent::Reorder { .. }
)
{
return Task::none();
}
// Passive layout/form reports retain their existing epoch rules;
// camera input uses its separate lifetime below. All other actions
// must match the exact context before shared reveal reconciliation.
@@ -1660,20 +1605,9 @@ impl App {
{
self.reconcile_multiselect();
}
if let Some((context, selection)) = scenario_selection_before
&& self.scenario_context() == Some(context)
&& self.multiselect_snapshot().as_ref() != Some(&selection)
{
// Legacy singleton selection retires old spacing packets. If the
// action already advanced its context, its new navigation/reveal
// owns that context and must not be retired by reconciliation.
self.behavior.scenario.cancel_spacing();
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
}
if preview_read_only && self.process_preview_primary().is_none() && self.draft.is_none() {
self.refresh_draft();
}
self.scenario_reorder_reconcile();
task
}
fn behavior_control(&mut self, intent: behavior::canvas_workspace::Intent) -> Task<Message> {
@@ -2224,7 +2158,6 @@ impl App {
}
Message::CheckpointLoaded(result) => return self.checkpoint_loaded(result),
Message::Hosted(event) => return self.hosted_event(event),
Message::ScenarioReorder(event) => return self.scenario_reorder_event(*event),
Message::ScenarioCanvas(event) => {
use crate::behavior::scenario::{self, CanvasEvent};
let context = event.context();
@@ -2733,9 +2666,6 @@ impl App {
if request.process.is_some() {
request.process = self.topology_context().map(Box::new);
}
if request.scenario.is_some() {
request.scenario = self.scenario_context().map(Box::new);
}
self.model_menu = crate::model_menu::Opened::new(self, request, self.menu_sequence);
}
Message::Menu(crate::model_menu::Event::Close) => {
@@ -2768,11 +2698,6 @@ impl App {
if matches!(*action, Message::DiagramSelection(_))
|| (menu.request.process.is_some()
&& matches!(*action, Message::TopologyCanvas(_)))
|| (menu.request.scenario.is_some()
&& matches!(
*action,
Message::ScenarioCanvas(_) | Message::ScenarioReorder(_)
))
{
return self.update(*action);
}
@@ -2815,10 +2740,6 @@ impl App {
return self.update(Message::Add(child));
}
Message::Escape => {
if self.scenario_reorder.session.is_some() {
self.scenario_reorder_cancel();
return Task::none();
}
let restore_source_focus = self.source_navigation_available()
&& (self.source_navigation.open || self.text_workspace.drawer().is_some());
let dismiss_find_only = restore_source_focus && self.source_navigation.open;
@@ -179,7 +179,7 @@ async fn mounted_scenario_use_delete_keeps_definition_and_second_use_and_rejects
let token = delete(&mut mounted, &mut app, true).await;
send(&mut mounted, &mut app, Message::ConfirmDelete(stale)).await;
assert!(app.delete_pending, "old token does not cancel new dialog");
// Keep selection and epoch unchanged while moving to another Scenario owner.
// Keep selection and epoch deliberately unchanged: owner is an independent witness.
let captured = app.scenario_context().unwrap();
assert_eq!(captured.scenario, owners[0]);
assert_eq!(
@@ -197,28 +197,9 @@ async fn mounted_scenario_use_delete_keeps_definition_and_second_use_and_rejects
assert_eq!(other.epoch, captured.epoch);
assert_eq!(app.selected, uses[0]);
assert!(
!app.delete_target.as_ref().unwrap().snapshot.valid(&app),
"the real captured snapshot rejects the changed Scenario destination"
app.delete_target.as_ref().unwrap().snapshot.valid(&app),
"the independent owner guard must reject an otherwise current token"
);
// Isolate the independent behavior-owner predicate: a current, unqualified
// object request has no old Scenario envelope or destination to reject it.
// This test-only replacement does not refresh the captured behavior owner.
let otherwise_current = crate::model_menu::Opened::new(
&app,
crate::model_menu::Request::object(uses[0], Point::ORIGIN),
token,
)
.unwrap();
assert!(otherwise_current.valid(&app));
let target = app.delete_target.as_ref().unwrap();
assert_eq!(target.selection, app.selected);
assert_eq!(target.placement, None);
assert_eq!(app.selected_node, None);
assert_eq!(target.behavior_owner, Some(owners[0]));
assert_eq!(diagram_removal::behavior_owner(&app), Some(owners[1]));
assert!(app.delete_pending);
assert!(!app.behavior.has_pending() && !app.source_dirty() && !app.draft_dirty());
app.delete_target.as_mut().unwrap().snapshot = otherwise_current;
send(&mut mounted, &mut app, Message::ConfirmDelete(token)).await;
assert_eq!(app.document.bundle(), &original);
assert!(!app.delete_pending && app.delete_target.is_none());
+2 -57
View File
@@ -1,4 +1,4 @@
//! Keep overlay ownership ahead of cancellation and captured reorder Apply.
//! Keep overlay Escape ahead of modal cancellation in the ordinary widget tree.
//!
//! Iced skips root.update when an overlay captures an event. Keeping this
//! wrapper present even when inactive preserves the child's widget state. A
@@ -24,18 +24,9 @@ pub(crate) fn root<'a, R: renderer::Renderer + 'a>(
&& app.architecture.is_none()));
Element::new(Bridge {
content,
reorder_active: app.scenario_reorder_enabled() && app.scenario_reorder.session.is_some(),
reorder_apply: crate::behavior::scenario::reorder_view::apply_message(app),
messages: [
architecture_authoring::captured_escape_message(app),
modal.then_some(Message::Escape).or_else(|| {
app.scenario_reorder.session.as_ref().map(|session| {
crate::behavior::scenario::reorder_view::message(
session,
super::scenario_reorder::Intent::Cancel,
)
})
}),
modal.then_some(Message::Escape),
],
})
}
@@ -43,8 +34,6 @@ pub(crate) fn root<'a, R: renderer::Renderer + 'a>(
struct Bridge<'a, R> {
content: Element<'a, Message, Theme, R>,
messages: [Option<Message>; 2],
reorder_active: bool,
reorder_apply: Option<Message>,
}
impl<R: renderer::Renderer> Widget<Message, Theme, R> for Bridge<'_, R> {
@@ -98,43 +87,6 @@ impl<R: renderer::Renderer> Widget<Message, Theme, R> for Bridge<'_, R> {
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
if self.reorder_active
&& matches!(event,
Event::Keyboard(keyboard::Event::KeyPressed { key: keyboard::Key::Named(keyboard::key::Named::Enter), modifiers, .. })
if modifiers.control() && !modifiers.alt() && !modifiers.shift() && !modifiers.logo())
{
// A picker may have opened earlier in this same input batch, before
// App state can invalidate the captured reorder action.
if let Some(element) = self.content.as_widget_mut().overlay(
&mut tree.children[0],
layout,
renderer,
viewport,
Vector::ZERO,
) {
let mut current = overlay::Nested::new(element);
let node = current.layout(renderer, viewport.size());
current.update(
event,
Layout::new(&node),
cursor,
renderer,
clipboard,
shell,
);
shell.capture_event();
return;
}
if matches!(
event,
Event::Keyboard(keyboard::Event::KeyPressed { repeat: false, .. })
) && let Some(message) = &self.reorder_apply
{
shell.publish(message.clone());
}
shell.capture_event();
return;
}
let escape = matches!(
event,
Event::Keyboard(keyboard::Event::KeyPressed {
@@ -168,13 +120,6 @@ impl<R: renderer::Renderer> Widget<Message, Theme, R> for Bridge<'_, R> {
if shell.is_event_captured() {
return;
}
if self.reorder_active
&& let Some(cancel) = &self.messages[1]
{
shell.publish(cancel.clone());
shell.capture_event();
return;
}
}
self.content.as_widget_mut().update(
&mut tree.children[0],
-24
View File
@@ -100,7 +100,6 @@ impl App {
(self.page == Page::Behavior).then_some(self.behavior.diagram.placement),
self.topology_context(),
self.behavior.topology.selection_address(),
self.scenario_context(),
)
}
pub(crate) fn multiselect_context(&self) -> Option<Context> {
@@ -110,9 +109,6 @@ impl App {
if let Some(current) = self.topology_context() {
return Some(process_context(current));
}
if let Some(current) = self.scenario_context() {
return Some(crate::behavior::scenario::selection::context(current));
}
let current = self.state_context()?;
crate::behavior::state_edit::validate_context(self.project(), current, current)
.ok()?;
@@ -123,14 +119,6 @@ impl App {
}
pub(crate) fn multiselect_target_visible(&self, target: &Target) -> bool {
if let Some(context) = self.scenario_context() {
return crate::behavior::scenario::selection::visible(
self.project(),
context,
self.selected,
)
.contains(target);
}
if let Some(context) = self.topology_context() {
return self
.behavior
@@ -164,10 +152,6 @@ impl App {
}
pub(crate) fn multiselect_envelope(&self, event: &Event) -> bool {
// Scenario input additionally needs its representation and visit.
if self.scenario_context().is_some() {
return false;
}
let Some(current) = self.multiselect_context() else {
return false;
};
@@ -235,10 +219,6 @@ impl App {
}
pub(crate) fn reconcile_multiselect(&mut self) {
if self.scenario_context().is_some() {
self.reconcile_scenario_multiselect();
return;
}
if self.topology_context().is_some() {
self.reconcile_process_multiselect();
return;
@@ -304,10 +284,6 @@ impl App {
}
pub(super) fn inspect_multiselect_primary(&mut self) {
if self.scenario_context().is_some() {
self.inspect_scenario_multiselect_primary();
return;
}
if self.topology_context().is_some() {
self.inspect_process_multiselect_primary();
return;
@@ -1,135 +0,0 @@
//! Exact rendered Scenario selection, independent of semantic movement permission.
use super::*;
use crate::behavior::scenario::{self, selection};
use crate::diagram_selection::Selection;
use crate::diagram_usage::Target;
#[cfg(test)]
mod tests;
impl App {
pub(crate) fn scenario_selection_envelope(&self, event: &selection::Event) -> bool {
self.scenario_context() == Some(event.context)
&& self.diagram_selection.as_ref().is_some_and(|selected| {
selected.matches(&event.snapshot, selection::context(event.context))
})
}
pub(super) fn reconcile_scenario_multiselect(&mut self) {
let Some(context) = self.scenario_context() else {
return;
};
let scope = selection::context(context).scope;
let visible = selection::visible(self.project(), context, self.selected);
let old = self
.diagram_selection
.as_ref()
.filter(|s| s.scope() == scope)
.and_then(Selection::primary)
.cloned();
let legacy = Target::ScenarioObject(self.selected);
let held = old.as_ref() == Some(&legacy);
let lost = old.as_ref().is_some_and(|target| !visible.contains(target));
let selected = self
.diagram_selection
.get_or_insert_with(|| Selection::new(scope));
selected.reconcile(scope, |target| visible.contains(target));
if held || lost {
if selected.primary() != old.as_ref() {
self.inspect_scenario_multiselect_primary();
}
} else if selected.primary() != Some(&legacy) {
if visible.contains(&legacy) {
selected.replace(legacy, |target| visible.contains(target));
} else {
selected.clear();
}
}
}
pub(super) fn inspect_scenario_multiselect_primary(&mut self) {
let Some(context) = self.scenario_context() else {
return;
};
self.selected = match self.diagram_selection.as_ref().and_then(Selection::primary) {
Some(Target::ScenarioObject(id)) => *id,
_ => context.scenario,
};
self.selected_node = None;
self.selected_bend = None;
self.selected_attachment = None;
self.behavior.diagram.placement = None;
self.diagram_navigation_focus = None;
self.refresh_draft();
}
/// This is dispatched before shared navigation/gesture retirement. Invalid
/// captures and unchanged selection must not disturb a newer input owner.
pub(super) fn scenario_selection_event(&mut self, event: selection::Event) -> Task<Message> {
use multiselect::Choice;
use selection::Intent;
if !self.scenario_selection_envelope(&event) {
return Task::none();
}
let visible = selection::visible(self.project(), event.context, self.selected);
if !event.snapshot.targets().iter().all(|t| visible.contains(t)) {
return Task::none();
}
if !diagram_removal::enabled(self)
|| self.source_rename.open()
|| self.behavior.scenario.canvas_tool(event.context.scenario)
!= scenario::CanvasTool::Select
{
self.status =
"Finish or cancel the active edit before changing the Scenario selection.".into();
return Task::none();
}
let selected = self.diagram_selection.as_mut().expect("admitted selection");
let valid = |target: &Target| visible.contains(target);
let changed = match &event.intent {
Intent::Choose(target, choice) => {
if !valid(target) {
return Task::none();
}
match choice {
Choice::Replace => selected.replace(target.clone(), valid),
Choice::Add => selected.add(target.clone(), valid),
Choice::Remove => selected.remove(target),
Choice::Toggle => selected.toggle(target.clone(), valid),
Choice::Inspect => selected.set_primary(target),
}
}
Intent::Collapse(target) => {
if !selected.contains(target) || !valid(target) {
return Task::none();
}
selected.replace(target.clone(), valid)
}
Intent::Clear => selected.clear(),
Intent::Refuse(reason) => {
self.status = reason.clone();
return Task::none();
}
};
if changed {
self.scenario_reorder_cancel();
self.source_interaction();
self.reconcile_pending_diagram_reveal(&Message::ScenarioCanvas(
scenario::CanvasEvent::Selection(Box::new(event)),
));
self.behavior.scenario.cancel_spacing();
// Every generation change, including a secondary membership ABA,
// retires old singleton spacing packets as well as group captures.
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
self.inspect_scenario_multiselect_primary();
}
Task::none()
}
pub(super) fn scenario_group_selected(&self) -> bool {
self.scenario_context().is_some()
&& self
.multiselect_snapshot()
.is_some_and(|s| s.targets().len() > 1)
}
}
@@ -1,379 +0,0 @@
use super::*;
use crate::behavior::scenario::{CanvasEvent, selection::Intent, spacing};
use crate::behavior::scenario_layout::Representation;
use crate::diagram_usage::Surface;
use iced::{Size, Vector};
fn setup(saved: bool) -> (App, Id, Id, Id, Id) {
let (document, owner, diagram, lane, message) = spacing::controller::tests::fixture();
let mut app = App::with_project(document.project().clone());
let _ = app.update(Message::Select(owner));
if saved {
let _ = app.update(Message::Diagram(super::super::Choice {
id: diagram,
label: "Saved".into(),
}));
} else {
let _ = app.update(Message::Page(Page::Behavior));
}
let context = app.scenario_context().unwrap();
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Viewport(
context,
Size::new(900., 600.),
)));
assert!(app.multiselect_snapshot().unwrap().targets().is_empty());
(app, owner, diagram, lane, message)
}
fn capture(app: &App, intent: Intent) -> selection::Event {
selection::Event {
context: app.scenario_context().unwrap(),
snapshot: app.multiselect_snapshot().unwrap(),
intent,
}
}
fn send(app: &mut App, event: selection::Event) {
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Selection(Box::new(
event,
))));
}
fn choose(app: &mut App, id: Id, choice: multiselect::Choice) {
send(
app,
capture(app, Intent::Choose(Target::ScenarioObject(id), choice)),
);
}
fn inert(app: &mut App, messages: Vec<Message>, preserve_status: bool) {
let model = app.project().clone();
let bundle = app.document.bundle().clone();
let source = app.source_workspace.bundle();
let revision = app.document.revision();
let history = (app.document.can_undo(), app.document.can_redo());
let selected = app.multiselect_snapshot();
let primary = app.selected;
let context = app.scenario_context();
let camera = app.camera_boundary();
let status = app.status.clone();
let spacing = (
app.behavior.scenario.spacing_stamp(),
app.behavior.scenario.spacing_token(),
);
for message in messages {
let _ = app.update(message);
assert_eq!(app.project(), &model);
assert_eq!(app.document.bundle(), &bundle);
assert_eq!(app.source_workspace.bundle(), source);
assert_eq!(app.document.revision(), revision);
assert_eq!((app.document.can_undo(), app.document.can_redo()), history);
assert_eq!(app.multiselect_snapshot(), selected);
assert_eq!(app.selected, primary);
assert_eq!(app.scenario_context(), context);
assert!(app.camera_boundary() == camera);
assert_eq!(
(
app.behavior.scenario.spacing_stamp(),
app.behavior.scenario.spacing_token()
),
spacing
);
if preserve_status {
assert_eq!(app.status, status);
}
}
}
fn packet(event: selection::Event) -> Message {
Message::ScenarioCanvas(CanvasEvent::Selection(Box::new(event)))
}
#[test]
fn exact_scenario_membership_primary_and_noops_preserve_model_source_and_history() {
for saved in [false, true] {
let (mut app, owner, diagram, lane, message) = setup(saved);
let model = app.project().clone();
let bundle = app.document.bundle().clone();
let revision = app.document.revision();
choose(&mut app, message, multiselect::Choice::Add);
choose(&mut app, lane, multiselect::Choice::Add);
choose(&mut app, message, multiselect::Choice::Inspect);
let snapshot = app.multiselect_snapshot().unwrap();
assert_eq!(
snapshot.targets(),
&[
Target::ScenarioObject(message),
Target::ScenarioObject(lane)
]
);
assert_eq!(snapshot.primary(), Some(&Target::ScenarioObject(message)));
assert_eq!(
snapshot.context().scope.surface,
if saved {
Surface::Saved(diagram)
} else {
Surface::Scenario(owner)
}
);
assert_eq!(app.selected, message);
let repeated = capture(
&app,
Intent::Choose(Target::ScenarioObject(message), multiselect::Choice::Add),
);
inert(&mut app, vec![packet(repeated)], true);
let stale_collapse = capture(&app, Intent::Collapse(Target::ScenarioObject(lane)));
choose(&mut app, lane, multiselect::Choice::Remove);
choose(&mut app, lane, multiselect::Choice::Add);
choose(&mut app, message, multiselect::Choice::Inspect);
assert_eq!(
app.multiselect_snapshot().unwrap().targets(),
snapshot.targets()
);
assert_eq!(
app.multiselect_snapshot().unwrap().primary(),
snapshot.primary()
);
inert(&mut app, vec![packet(stale_collapse)], true);
assert_eq!(app.project(), &model);
assert_eq!(app.document.bundle(), &bundle);
assert_eq!(app.document.revision(), revision);
assert!(!app.document.can_undo() && !app.document.can_redo());
}
}
#[test]
fn same_owner_representation_round_trip_clears_group_and_rejects_old_packets() {
let (mut app, owner, diagram, lane, message) = setup(true);
choose(&mut app, message, multiselect::Choice::Add);
choose(&mut app, lane, multiselect::Choice::Add);
let old = capture(&app, Intent::Clear);
let snapshot = app.multiselect_snapshot().unwrap();
let mut second = app.project().diagrams[&diagram].clone();
let other = Id::new_v4();
second.id = other;
second.alias = "other_saved".into();
second.name = "Other saved".into();
// Navigation itself is tested against a model with both saved identities.
let mut project = app.project().clone();
project.diagrams.insert(other, second);
let mut app = App::with_project(project);
let _ = app.update(Message::Select(owner));
let _ = app.update(Message::Diagram(super::super::Choice {
id: diagram,
label: "A".into(),
}));
choose(&mut app, message, multiselect::Choice::Add);
choose(&mut app, lane, multiselect::Choice::Add);
let first = capture(&app, Intent::Clear);
let first_visit = first.context.visit;
for representation in [
Representation::Saved { diagram: other },
Representation::Derived { owner },
Representation::Saved { diagram },
] {
let context = app.scenario_context().unwrap();
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Representation(
context,
representation,
)));
assert!(app.multiselect_snapshot().unwrap().targets().len() <= 1);
}
assert_ne!(app.scenario_context().unwrap().visit, first_visit);
choose(&mut app, message, multiselect::Choice::Replace);
choose(&mut app, lane, multiselect::Choice::Add);
assert_eq!(
app.multiselect_snapshot().unwrap().targets(),
snapshot.targets()
);
inert(&mut app, vec![packet(first), packet(old)], true);
}
#[test]
fn unsupported_targets_and_bare_generic_packets_are_inert_before_side_effects() {
let (mut app, _, _, lane, message) = setup(true);
choose(&mut app, message, multiselect::Choice::Add);
choose(&mut app, lane, multiselect::Choice::Add);
let current = app.multiselect_context().unwrap();
let events = [
Target::Whole,
Target::Placement(message),
Target::ScenarioObject(Id::new_v4()),
]
.into_iter()
.map(|target| {
packet(capture(
&app,
Intent::Choose(target, multiselect::Choice::Replace),
))
})
.chain([
Message::DiagramSelection(multiselect::Event::Clear(current)),
Message::DiagramSelection(multiselect::Event::Choose(
current,
Target::ScenarioObject(message),
multiselect::Choice::Replace,
)),
])
.collect();
inert(&mut app, events, true);
}
#[test]
fn group_rejects_legacy_singleton_remove_reorder_and_index_arrows() {
let (mut app, owner, _, lane, message) = setup(true);
choose(&mut app, message, multiselect::Choice::Add);
choose(&mut app, lane, multiselect::Choice::Add);
let context = app.scenario_context().unwrap();
inert(
&mut app,
vec![
Message::ScenarioCanvas(CanvasEvent::Select(context, message)),
Message::ScenarioCanvas(CanvasEvent::Remove(context, lane)),
Message::ScenarioCanvas(CanvasEvent::Reorder {
context,
occurrence: message,
slot: scenario::Slot {
parent: owner,
before: None,
},
}),
Message::Command(Command::MoveStep(owner, 0, 1)),
Message::Delete,
],
false,
);
assert!(!app.delete_pending);
}
#[test]
fn secondary_selection_aba_retires_old_spacing_without_canceling_a_new_gesture() {
let (mut app, _, _, lane, message) = setup(true);
choose(&mut app, message, multiselect::Choice::Add);
let old = super::super::scenario_spacing::tests::packet_for_app(
&app,
spacing::Target::Message(message),
);
choose(&mut app, lane, multiselect::Choice::Add);
let group_begin = super::super::scenario_spacing::tests::packet_for_app(
&app,
spacing::Target::Message(message),
);
let original = app.document.snapshot();
let selected = app.multiselect_snapshot().unwrap();
let epoch = app.canvas_epoch;
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Spacing(group_begin)));
assert_eq!(app.project(), &original.decoded.project);
assert_eq!(app.document.bundle(), &original.bundle);
assert_eq!(
app.multiselect_snapshot().unwrap().targets(),
selected.targets()
);
assert_eq!(
app.multiselect_snapshot().unwrap().primary(),
selected.primary()
);
assert_eq!(
app.canvas_epoch, epoch,
"pending click retains its admitted lifetime"
);
assert_eq!(
app.behavior.scenario.spacing_token(),
Some(group_begin.token())
);
choose(&mut app, lane, multiselect::Choice::Remove);
assert!(app.behavior.scenario.spacing_token().is_none());
inert(
&mut app,
vec![Message::ScenarioCanvas(CanvasEvent::Spacing(old))],
true,
);
let current = super::super::scenario_spacing::tests::packet_for_app(
&app,
spacing::Target::Message(message),
);
let spacing::Event::Begin { press_screen, .. } = current else {
unreachable!()
};
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Spacing(current)));
assert_eq!(app.behavior.scenario.spacing_token(), Some(current.token()));
inert(
&mut app,
vec![
Message::ScenarioCanvas(CanvasEvent::Spacing(old)),
Message::ScenarioCanvas(CanvasEvent::Spacing(spacing::Event::Cancel {
stamp: old.stamp(),
token: old.token(),
})),
Message::ScenarioCanvas(CanvasEvent::Spacing(spacing::Event::Finish {
stamp: old.stamp(),
token: old.token(),
screen: press_screen + Vector::new(0., 25.),
})),
],
true,
);
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Spacing(
spacing::Event::Finish {
stamp: current.stamp(),
token: current.token(),
screen: press_screen + Vector::new(0., 25.),
},
)));
assert!(app.document.can_undo());
assert_eq!(
app.multiselect_snapshot().unwrap().targets(),
&[Target::ScenarioObject(message)]
);
}
#[test]
fn legacy_singleton_selection_aba_retires_old_spacing_begin() {
for canvas in [false, true] {
let (mut app, _, _, lane, message) = setup(true);
choose(&mut app, message, multiselect::Choice::Add);
let old = super::super::scenario_spacing::tests::packet_for_app(
&app,
spacing::Target::Message(message),
);
for id in [lane, message] {
let event = if canvas {
Message::ScenarioCanvas(CanvasEvent::Select(app.scenario_context().unwrap(), id))
} else {
Message::Select(id)
};
let _ = app.update(event);
assert_eq!(app.selected, id);
assert_eq!(
app.multiselect_snapshot().unwrap().targets(),
&[Target::ScenarioObject(id)]
);
}
assert_ne!(app.scenario_context().unwrap(), old.stamp().context);
inert(
&mut app,
vec![Message::ScenarioCanvas(CanvasEvent::Spacing(old))],
true,
);
let current = super::super::scenario_spacing::tests::packet_for_app(
&app,
spacing::Target::Message(message),
);
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Spacing(current)));
assert_eq!(app.behavior.scenario.spacing_token(), Some(current.token()));
}
}
#[test]
fn pending_primary_edit_is_not_applied_by_selection_input() {
let (mut app, _, _, lane, message) = setup(true);
choose(&mut app, message, multiselect::Choice::Add);
let _ = app.update(Message::Field(
"/name".into(),
"Pending primary name".into(),
));
assert!(app.draft_dirty());
let event = capture(
&app,
Intent::Choose(Target::ScenarioObject(lane), multiselect::Choice::Add),
);
inert(&mut app, vec![packet(event)], false);
assert!(app.draft_dirty());
assert_ne!(app.project().named(message), "Pending primary name");
}
@@ -1,339 +0,0 @@
//! Exact-input adapter for an explicit semantic reorder operation.
use super::*;
use crate::behavior::scenario_layout::presentation::PresentationStatus;
use crate::{
behavior::scenario::{
self,
reorder_session::{Outcome, Session},
selection,
},
diagram_selection::Snapshot,
};
#[derive(Default)]
pub(crate) struct State {
pub(crate) session: Option<Session>,
pub(crate) query: String,
pub(crate) page: usize,
pub(crate) summary: Option<(scenario::Context, String)>,
summary_selection: Option<Snapshot>,
}
#[derive(Debug, Clone)]
pub(crate) struct Event {
pub(crate) context: scenario::Context,
pub(crate) snapshot: Snapshot,
pub(crate) token: Id,
pub(crate) intent: Intent,
}
#[derive(Debug, Clone)]
pub(crate) enum Intent {
Begin,
Choose(scenario::Slot),
Query(String),
Page(usize),
Finish(scenario::Slot),
Cancel,
}
fn short_label(value: &str) -> String {
crate::views::short(value, 24)
}
/// Actual committed block order, independent of capture order or saved Y.
fn result_neighborhood(project: &oa_core::Project, parent: Id, snapshot: &Snapshot) -> String {
let Some(order) = project
.elements
.get(&parent)
.and_then(|element| element.steps())
else {
return String::new();
};
let selected: Vec<_> = order
.iter()
.enumerate()
.filter(|(_, id)| {
snapshot
.targets()
.contains(&crate::diagram_usage::Target::ScenarioObject(**id))
})
.collect();
let Some((first, _)) = selected.first() else {
return String::new();
};
let last = selected.last().expect("nonempty block").0;
let label = |index: usize| format!("#{} @{}", index + 1, project.elements[&order[index]].alias);
let left = first
.checked_sub(1)
.map_or_else(|| "Start".to_owned(), label);
let right = if last + 1 < order.len() {
label(last + 1)
} else {
"End".to_owned()
};
let mut members = selected
.iter()
.take(3)
.map(|(index, _)| label(*index))
.collect::<Vec<_>>()
.join(", ");
if selected.len() > 3 {
members.push_str(&format!(", +{} more", selected.len() - 3));
}
format!("{left} → [{members}] → {right}")
}
impl App {
pub(crate) fn scenario_reorder_enabled(&self) -> bool {
self.spacing_enabled()
&& diagram_removal::enabled(self)
&& self.behavior.scenario.spacing_token().is_none()
&& self.scenario_context().is_some_and(|context| {
self.behavior.scenario.canvas_tool(context.scenario) == scenario::CanvasTool::Select
})
}
pub(crate) fn scenario_reorder_cancel(&mut self) {
if self.scenario_reorder.session.take().is_some() {
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
}
self.scenario_reorder.query.clear();
self.scenario_reorder.page = 0;
self.scenario_reorder.summary = None;
self.scenario_reorder.summary_selection = None;
}
pub(crate) fn scenario_reorder_reconcile(&mut self) {
if self
.scenario_reorder
.summary
.as_ref()
.is_some_and(|(context, _)| {
// A committed result is presentation, not a live authoring packet.
// Resizing diagnostics or moving the camera retires input epochs
// without changing the result's document, view or selection.
let same_result = self.scenario_context().is_some_and(|mut current| {
current.epoch = context.epoch;
current == *context
});
let same_selection = self
.scenario_reorder
.summary_selection
.as_ref()
.zip(self.diagram_selection.as_ref())
.is_some_and(|(snapshot, selected)| {
selected.matches(snapshot, snapshot.context())
});
!same_result || !same_selection
})
{
self.scenario_reorder.summary = None;
self.scenario_reorder.summary_selection = None;
}
let Some(session) = self.scenario_reorder.session.as_ref() else {
return;
};
let valid = self.scenario_reorder_enabled()
&& self
.scenario_context()
.zip(self.multiselect_snapshot())
.is_some_and(|(context, snapshot)| {
session.matches(self.project(), context, &snapshot, session.token())
});
if !valid {
self.scenario_reorder_cancel();
}
}
pub(crate) fn scenario_reorder_event(&mut self, event: Event) -> Task<Message> {
// Reject stale envelopes before touching any newer operation or UI state.
if event.token.is_nil()
|| self.scenario_context() != Some(event.context)
|| !self
.diagram_selection
.as_ref()
.is_some_and(|selection_state| {
selection_state.matches(&event.snapshot, selection::context(event.context))
})
{
return Task::none();
}
if matches!(&event.intent, Intent::Begin) {
if self.scenario_reorder.session.is_some() || !self.scenario_reorder_enabled() {
return Task::none();
}
// Retire the captured Begin before construction, including failed starts.
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
let context = self.scenario_context().expect("admitted scenario");
let snapshot = self.multiselect_snapshot().expect("admitted selection");
self.scenario_reorder.query.clear();
self.scenario_reorder.page = 0;
self.scenario_reorder.summary = None;
self.scenario_reorder.summary_selection = None;
match Session::new(
self.document.snapshot().decoded.project.clone().into(),
context,
snapshot,
event.token,
) {
Ok(session) => {
self.scenario_reorder.session = Some(session);
self.status = "Choose a semantic destination, then apply reorder. Saved positions are preserved.".into();
return scenario::reorder_view::focus_query(event.token);
}
Err(reason) => self.status = reason,
}
return Task::none();
}
let Some(session) = self.scenario_reorder.session.as_ref() else {
return Task::none();
};
if !session.matches(self.project(), event.context, &event.snapshot, event.token) {
return Task::none();
}
if let Intent::Finish(slot) = &event.intent
&& session
.current_preview()
.is_none_or(|preview| preview.slot != *slot)
{
return Task::none();
}
if matches!(&event.intent, Intent::Cancel) {
self.scenario_reorder_cancel();
self.status = "Reorder cancelled. Sequence unchanged.".into();
return Task::none();
}
if !self.scenario_reorder_enabled() {
self.scenario_reorder_cancel();
return Task::none();
}
match event.intent {
Intent::Choose(slot) => {
let preview = self
.scenario_reorder
.session
.as_mut()
.expect("admitted session")
.preview(slot);
self.status = match &preview.outcome {
Outcome::Refused(reason) => reason.clone(),
Outcome::Noop => "This destination leaves the semantic order unchanged.".into(),
Outcome::Change => match preview.presentation.as_ref().map(|shown| shown.status()) {
Some(PresentationStatus::Ready) => "Review the semantic order, then apply reorder. Saved positions are preserved.".into(),
Some(PresentationStatus::Fallback { .. }) => "The new semantic order requires automatic display fallback. Apply keeps the requested saved view and its authored positions.".into(),
_ => "The candidate cannot be displayed; reorder cannot be applied.".into(),
},
};
}
Intent::Query(query) => {
self.scenario_reorder.query = query;
self.scenario_reorder.page = 0;
}
Intent::Page(page) => self.scenario_reorder.page = page,
Intent::Finish(slot) => {
let label = |id| {
self.project().elements.get(&id).map_or_else(
|| id.to_string(),
|element| {
format!(
"{} (@{})",
short_label(&element.name),
short_label(&element.alias)
)
},
)
};
let parent_name = label(slot.parent);
let destination = slot.before.map_or_else(
|| "at the end".to_owned(),
|anchor| format!("before {}", label(anchor)),
);
let session = self
.scenario_reorder
.session
.take()
.expect("admitted session");
let selection_visible = session
.current_preview()
.is_some_and(|preview| preview.selection_visible);
let result =
session.finish(self.project(), event.context, &event.snapshot, event.token);
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
self.scenario_reorder.query.clear();
self.scenario_reorder.page = 0;
match result {
Err(reason) => self.status = reason,
Ok(completion) => {
debug_assert_eq!(completion.representation, event.context.representation);
if !selection_visible {
self.status = "The candidate would hide a selected occurrence; reorder was not applied.".into();
} else if matches!(
completion.presentation_status,
PresentationStatus::Unavailable { .. }
) {
self.status =
"The candidate cannot be displayed; reorder was not applied."
.into();
} else if let Some(command) = completion.command {
match self.document.execute(*command) {
Ok(_) => {
if let Some(crate::diagram_usage::Target::ScenarioObject(
primary,
)) = completion.snapshot.primary()
{
self.selected = *primary;
}
self.sync();
self.reconcile_multiselect();
self.inspect_scenario_multiselect_primary();
let mut summary = format!(
"Moved {} selected occurrences {} in {}. {}. Saved positions are preserved.",
completion.snapshot.targets().len(),
destination,
parent_name,
result_neighborhood(
self.project(),
slot.parent,
&completion.snapshot
)
);
if matches!(
completion.presentation_status,
PresentationStatus::Fallback { .. }
) {
summary.push_str(" Automatic display fallback is active.");
}
self.scenario_reorder.summary =
self.scenario_context().map(|context| (context, summary));
self.scenario_reorder.summary_selection =
self.multiselect_snapshot();
self.status = if matches!(
completion.presentation_status,
PresentationStatus::Fallback { .. }
) {
"Semantic order updated. Automatic display fallback is active; the requested saved view and authored positions are preserved.".into()
} else {
"Semantic order updated. Saved positions are preserved."
.into()
};
}
Err(error) => {
self.status = format!("Reorder was not applied: {error}")
}
}
} else {
self.status = "Semantic order unchanged.".into();
}
}
}
}
Intent::Begin | Intent::Cancel => unreachable!(),
}
Task::none()
}
}
#[cfg(test)]
mod summary_tests;
#[cfg(test)]
mod tests;
@@ -1,231 +0,0 @@
use super::*;
use crate::behavior::scenario::{CanvasEvent, Slot, direct_tests};
use crate::behavior::scenario_layout::Representation;
use crate::diagram_usage::Target;
use oa_core::{Diagram, DiagramLayout, Layer, OccurrenceBody, ProjectionKind, ScenarioLayout};
fn committed() -> (App, Id, Id, oa_core::source_project::SourceBundle) {
let f = direct_tests::fixture();
let mut project = f.project;
let mut extra = project
.make_element(Kind::Occurrence, Layer::Oa, "Tail", Some(f.owner))
.unwrap();
if let Some(oa_core::SharedBody::Occurrence { body, .. }) = extra.shared_mut() {
*body = OccurrenceBody::Action {
action: None,
lifeline: None,
};
}
let extra_id = extra.id;
project.elements.insert(extra_id, extra);
project
.elements
.get_mut(&f.owner)
.unwrap()
.steps_mut()
.unwrap()
.push(extra_id);
let diagram = Id::new_v4();
project.diagrams.insert(
diagram,
Diagram {
id: diagram,
alias: "summary_view".into(),
name: "Summary view".into(),
description: String::new(),
layer: Layer::Oa,
projection: ProjectionKind::Scenario { scenario: f.owner },
nodes: Default::default(),
routes: vec![],
view_options: Default::default(),
layout: Some(DiagramLayout::Scenario(ScenarioLayout::default())),
},
);
let mut app = App::with_project(project);
let _ = app.update(Message::Select(f.owner));
let _ = app.update(Message::Diagram(Choice {
id: diagram,
label: "Summary view".into(),
}));
for (id, choice) in [
(f.call, multiselect::Choice::Replace),
(f.reply, multiselect::Choice::Add),
] {
let event = selection::Event {
context: app.scenario_context().unwrap(),
snapshot: app.multiselect_snapshot().unwrap(),
intent: selection::Intent::Choose(Target::ScenarioObject(id), choice),
};
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Selection(Box::new(
event,
))));
}
viewport(&mut app, 700.);
let baseline = app.document.bundle().clone();
dispatch(&mut app, Intent::Begin);
let slot = Slot {
parent: f.owner,
before: None,
};
dispatch(&mut app, Intent::Choose(slot));
dispatch(&mut app, Intent::Finish(slot));
assert_eq!(
app.project().elements[&f.owner].steps().unwrap(),
&[extra_id, f.call, f.reply]
);
assert!(app.scenario_reorder.summary.is_some(), "{}", app.status);
(app, f.owner, f.call, baseline)
}
fn dispatch(app: &mut App, intent: Intent) {
let event = Event {
context: app.scenario_context().unwrap(),
snapshot: app.multiselect_snapshot().unwrap(),
token: app
.scenario_reorder
.session
.as_ref()
.map_or_else(Id::new_v4, Session::token),
intent,
};
let _ = app.update(Message::ScenarioReorder(Box::new(event)));
}
fn viewport(app: &mut App, height: f32) {
let context = app.scenario_context().unwrap();
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Viewport(
context,
iced::Size::new(900., height),
)));
}
#[test]
fn scenario_reorder_summary_survives_diagnostics_resize_and_camera_then_undo_clears() {
let (mut app, _, _, baseline) = committed();
let summary = app.scenario_reorder.summary.clone();
let changed = app.document.bundle().clone();
let epoch = app.canvas_epoch;
viewport(&mut app, 660.);
assert_ne!(
app.canvas_epoch, epoch,
"real diagnostics-height viewport change must retire input"
);
assert_eq!(app.scenario_reorder.summary, summary);
let context = app.scenario_context().unwrap();
let camera = app.behavior.scenario.camera_boundary();
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Camera(
context,
app.camera_epoch,
crate::behavior::canvas_workspace::Event::Pan(iced::Vector::new(24., 12.)),
)));
assert!(app.behavior.scenario.camera_boundary() != camera);
assert_eq!(app.scenario_reorder.summary, summary);
assert_eq!(app.document.bundle(), &changed);
let _ = app.update(Message::Undo);
assert!(app.scenario_reorder.summary.is_none());
assert!(app.scenario_reorder.summary_selection.is_none());
assert_eq!(app.document.bundle(), &baseline);
}
#[test]
fn scenario_reorder_summary_retires_on_view_selection_and_new_operation() {
for action in 0..3 {
let (mut app, owner, call, _) = committed();
let source = app.document.bundle().clone();
match action {
0 => {
let context = app.scenario_context().unwrap();
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Representation(
context,
Representation::Derived { owner },
)));
assert_eq!(
app.scenario_context().unwrap().representation,
Representation::Derived { owner }
);
}
1 => {
let event = selection::Event {
context: app.scenario_context().unwrap(),
snapshot: app.multiselect_snapshot().unwrap(),
intent: selection::Intent::Collapse(Target::ScenarioObject(call)),
};
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Selection(Box::new(
event,
))));
}
_ => {
dispatch(&mut app, Intent::Begin);
assert!(app.scenario_reorder.session.is_some());
dispatch(&mut app, Intent::Cancel);
assert!(app.scenario_reorder.session.is_none());
assert_eq!(app.status, "Reorder cancelled. Sequence unchanged.");
}
}
assert!(app.scenario_reorder.summary.is_none());
assert!(app.scenario_reorder.summary_selection.is_none());
assert_eq!(app.document.bundle(), &source);
}
}
#[test]
fn scenario_reorder_summary_selection_generation_aba_is_not_a_camera_change() {
let (mut app, _, call, _) = committed();
let captured = app.multiselect_snapshot().unwrap();
let selected = app.diagram_selection.as_mut().unwrap();
assert!(selected.set_primary(&Target::ScenarioObject(call)));
assert!(selected.set_primary(captured.primary().unwrap()));
assert_eq!(selected.targets(), captured.targets());
assert_eq!(selected.primary(), captured.primary());
app.scenario_reorder_reconcile();
assert!(app.scenario_reorder.summary.is_none());
}
#[test]
fn scenario_reorder_summary_clears_after_real_spacing_commit_without_another_event() {
use crate::behavior::scenario::spacing::{Event as Spacing, Target as SpacingTarget};
let (mut app, owner, call, _) = committed();
let reordered = app.document.snapshot();
let selection = app.multiselect_snapshot().unwrap();
let begin =
crate::app::scenario_spacing::tests::packet_for_app(&app, SpacingTarget::Message(call));
let Spacing::Begin { press_screen, .. } = begin else {
unreachable!()
};
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Spacing(begin)));
assert_eq!(app.behavior.scenario.spacing_token(), Some(begin.token()));
assert!(app.scenario_reorder.summary.is_some());
let screen = press_screen + iced::Vector::new(0., 24.);
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Spacing(
Spacing::Move {
stamp: begin.stamp(),
token: begin.token(),
screen,
},
)));
assert_eq!(app.document.bundle(), &reordered.bundle);
let finish = Spacing::Finish {
stamp: begin.stamp(),
token: begin.token(),
screen,
};
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Spacing(finish)));
assert_ne!(
app.document.bundle(),
&reordered.bundle,
"spacing must actually commit: {}",
app.status
);
assert_eq!(
app.project().elements[&owner].steps(),
reordered.decoded.project.elements[&owner].steps()
);
assert_eq!(
app.multiselect_snapshot().unwrap().targets(),
selection.targets()
);
assert!(app.scenario_reorder.summary.is_none());
assert!(app.scenario_reorder.summary_selection.is_none());
let _ = app.update(Message::Undo);
assert_eq!(app.document.bundle(), &reordered.bundle);
}
File diff suppressed because it is too large Load Diff
+27 -113
View File
@@ -1,33 +1,9 @@
//! Admit spacing packets before ordinary navigation can retire a newer reveal.
use super::*;
use crate::diagram_usage::Target;
use behavior::scenario::{
selection,
spacing::{Event, Refusal, controller::Finish},
};
use behavior::scenario::spacing::{Event, controller::Finish};
#[cfg(test)]
mod group_tests;
#[cfg(test)]
pub(super) mod tests;
fn refusal_reason(refusal: &Refusal) -> String {
match refusal {
Refusal::InvalidSelection(reason) => (*reason).into(),
Refusal::TargetNotRendered(_) => {
"Every selected object must be a rendered local message or lifeline in this saved view."
.into()
}
Refusal::Geometry(_) => {
"These positions cannot be displayed together. The saved arrangement is unchanged."
.into()
}
Refusal::Command(reason) => reason.clone(),
_ => {
"The drag could not be applied in this view. The saved arrangement is unchanged.".into()
}
}
}
mod tests;
impl App {
pub(super) fn spacing_enabled(&self) -> bool {
@@ -40,54 +16,22 @@ impl App {
}
pub(super) fn scenario_spacing(&mut self, event: Event) -> Task<Message> {
if self.scenario_reorder.session.is_some() {
return Task::none();
}
let stamp = event.stamp();
if self.scenario_context() != Some(stamp.context) || self.camera_epoch != stamp.camera_epoch
{
return Task::none();
}
let Some(snapshot) = self.multiselect_snapshot() else {
return Task::none();
};
if snapshot.generation() != stamp.selection_generation
|| snapshot.context() != selection::context(stamp.context)
{
return Task::none();
}
let state = &self.behavior.scenario;
let matches =
state.spacing_token() == Some(event.token()) && state.spacing_stamp() == Some(stamp);
if match event {
Event::Begin { token, .. } => token.is_nil() || state.spacing_token().is_some(),
_ => {
!matches
|| !state.spacing_snapshot().is_some_and(|captured| {
self.diagram_selection.as_ref().is_some_and(|selected| {
selected.matches(captured, selection::context(stamp.context))
})
})
}
_ => !matches,
} {
return Task::none();
}
if matches!(event, Event::Begin { .. }) {
let visible = selection::visible(self.project(), stamp.context, self.selected);
if !snapshot
.targets()
.iter()
.all(|target| visible.contains(target))
{
// This otherwise current Begin cannot be replayed after repair.
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
self.status =
"Every selected object must still be visible before moving the group.".into();
return Task::none();
}
}
if matches!(event, Event::Cancel { .. }) || !self.spacing_enabled() {
if self.behavior.scenario.cancel_spacing() || matches!(event, Event::Begin { .. }) {
if self.behavior.scenario.cancel_spacing() {
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
}
return Task::none();
@@ -95,38 +39,18 @@ impl App {
match event {
Event::Begin { .. } => {
let mut state = std::mem::take(&mut self.behavior.scenario);
let result = state.begin_spacing_with_snapshot(self.project(), event, snapshot);
let result = state.begin_spacing(self.project(), event);
self.behavior.scenario = state;
if let Err(error) = result {
// Consume admitted failed starts too; a repeated Begin is stale.
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
self.status = refusal_reason(&error);
if result.is_err() {
self.status = "The drag could not start in this view. Try again.".into();
}
}
Event::Move { screen, .. } => self.behavior.scenario.move_spacing(screen),
Event::Finish { screen, .. } => {
// Full captured selection matched above, before taking the session.
let captured = self
.behavior
.scenario
.spacing_snapshot()
.cloned()
.expect("admitted session snapshot");
let group = captured.targets().len() > 1;
let finished = self.behavior.scenario.finish_spacing(screen);
// Retire queued packets even for zero motion or a refused finish.
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
match finished {
Ok(Finish::Click(target)) if group => {
let target = Target::ScenarioObject(target);
// The controller only captures a pressed member of this set.
if captured.targets().contains(&target) {
self.diagram_selection
.as_mut()
.expect("admitted selection")
.replace(target, |_| true);
self.inspect_scenario_multiselect_primary();
}
}
Ok(Finish::Click(target)) => self.select(target),
Ok(Finish::Noop) => {}
Ok(Finish::Commit {
@@ -134,38 +58,28 @@ impl App {
selected,
scale,
pan,
}) => match self.document.execute(*command) {
Ok(_) => {
self.selected = if group {
match captured.primary() {
Some(Target::ScenarioObject(id)) => *id,
_ => selected,
}
} else {
selected
};
self.selected_node = None;
self.diagram_navigation_focus = None;
self.behavior.scenario.accept_spacing_camera(scale, pan);
self.sync();
if group {
self.inspect_scenario_multiselect_primary();
}) => {
// Document prepares and writes the source candidate before
// committing its snapshot. Failure leaves camera/selection
// and source/history at their accepted values.
match self.document.execute(*command) {
Ok(_) => {
self.selected = selected;
self.selected_node = None;
self.diagram_navigation_focus = None;
self.behavior.scenario.accept_spacing_camera(scale, pan);
self.sync();
self.status = "Scenario spacing updated.".into();
}
self.status = if group {
format!(
"Spacing updated for {} selected objects. Scenario order is unchanged.",
captured.targets().len()
)
} else {
"Scenario spacing updated.".into()
};
Err(error) => self.status = format!("Spacing was not applied: {error}"),
}
Err(error) => self.status = format!("Spacing was not applied: {error}"),
},
Err(error) => self.status = refusal_reason(&error),
}
Err(_) => {
self.status =
"That position cannot be applied. The saved arrangement is unchanged."
.into()
}
}
self.reconcile_multiselect();
self.scenario_reorder_reconcile();
}
Event::Cancel { .. } => unreachable!(),
}
@@ -1,374 +0,0 @@
use super::tests::{begin, finish, move_to, packet_for_app, send, setup};
use super::*;
use crate::behavior::scenario::{CanvasEvent, selection, spacing::Target};
use crate::behavior::scenario_layout::Representation;
use crate::diagram_usage::Target as SelectedTarget;
use iced::Vector;
use oa_core::{
DiagramLayout, OccurrenceEndpoint, ScenarioLanePosition, ScenarioLayout,
ScenarioMessagePosition,
};
fn id(app: &App, alias: &str) -> Id {
app.project()
.identities()
.into_iter()
.find(|(_, a)| *a == alias)
.unwrap()
.0
}
fn choose(app: &mut App, id: Id, choice: multiselect::Choice) {
let event = selection::Event {
context: app.scenario_context().unwrap(),
snapshot: app.multiselect_snapshot().unwrap(),
intent: selection::Intent::Choose(SelectedTarget::ScenarioObject(id), choice),
};
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Selection(Box::new(
event,
))));
}
fn group(messages: bool, reverse: bool) -> (App, [Id; 2]) {
let (mut app, lane, message) = setup();
let ids = if messages {
[message, id(&app, "second")]
} else {
[lane, id(&app, "operator")]
};
let order = if reverse { [ids[1], ids[0]] } else { ids };
for member in order {
choose(&mut app, member, multiselect::Choice::Add);
}
(app, ids)
}
fn target(messages: bool, id: Id) -> Target {
if messages {
Target::Message(id)
} else {
Target::Lane(id)
}
}
fn delta(messages: bool, pixels: f32) -> Vector {
if messages {
Vector::new(0., pixels)
} else {
Vector::new(pixels, 0.)
}
}
#[test]
fn either_pressed_member_moves_complete_group_and_preserves_primary_source_and_one_history_step() {
for messages in [false, true] {
for pressed_index in 0..2 {
let (mut app, ids) = group(messages, true);
// Automatic lane order depends on generated identities. Give this
// literal geometry oracle explicit authored starting coordinates.
let c = app.scenario_context().unwrap();
let Representation::Saved { diagram } = c.representation else {
unreachable!()
};
let mut initial = app.project().clone();
let mut members = ids.to_vec();
members.sort();
let mut layout = ScenarioLayout::default();
for member in members {
if messages {
layout.messages.push(ScenarioMessagePosition {
address: OccurrenceEndpoint {
references: vec![],
occurrence: member,
},
send_y: if member == ids[0] { 148. } else { 240. },
});
} else {
layout.lanes.push(ScenarioLanePosition {
lifeline: member,
x: if member == ids[0] { 160. } else { 400. },
});
}
}
initial.diagrams.get_mut(&diagram).unwrap().layout =
Some(DiagramLayout::Scenario(layout));
app.document =
Document::from_project(initial, oa_core::source_project::ProjectLimits::default())
.unwrap();
app.sync();
let original = app.document.snapshot();
let original_selection = app.multiselect_snapshot().unwrap();
let primary = app.selected;
let c = app.scenario_context().unwrap();
let Representation::Saved { diagram } = c.representation else {
unreachable!()
};
let (event, start, scale) = begin(&mut app, target(messages, ids[pressed_index]));
let world_delta = 24. / scale;
move_to(&mut app, event, start + delta(messages, 24.));
assert_eq!(app.project(), &original.decoded.project);
assert_eq!(app.document.bundle(), &original.bundle);
assert_eq!(app.selected, primary);
assert_eq!(
app.behavior.scenario.spacing_snapshot(),
Some(&original_selection)
);
let mut expected = original.decoded.project.clone();
let mut members = ids.to_vec();
members.sort();
let mut layout = ScenarioLayout::default();
for member in members {
if messages {
let y: f32 = if member == ids[0] { 148. } else { 240. };
layout.messages.push(ScenarioMessagePosition {
address: OccurrenceEndpoint {
references: vec![],
occurrence: member,
},
send_y: (f64::from(y) + f64::from(world_delta)) as f32,
});
} else {
let x: f32 = if member == ids[0] { 160. } else { 400. };
layout.lanes.push(ScenarioLanePosition {
lifeline: member,
x: (f64::from(x) + f64::from(world_delta)) as f32,
});
}
}
expected.diagrams.get_mut(&diagram).unwrap().layout =
Some(DiagramLayout::Scenario(layout));
finish(&mut app, event, start + delta(messages, 24.));
assert_eq!(app.project(), &expected);
assert_eq!(app.selected, primary);
assert_eq!(
app.multiselect_snapshot().unwrap().targets(),
original_selection.targets()
);
assert_eq!(
app.multiselect_snapshot().unwrap().primary(),
original_selection.primary()
);
let saved = app.document.bundle().clone();
let _ = app.update(Message::ShortcutUndo(false));
assert_eq!(app.project(), &original.decoded.project);
assert_eq!(app.document.bundle(), &original.bundle);
assert!(!app.document.can_undo() && app.document.can_redo());
let _ = app.update(Message::ShortcutUndo(true));
assert_eq!(app.project(), &expected);
assert_eq!(app.document.bundle(), &saved);
let reopened =
Document::from_bundle(saved, oa_core::source_project::ProjectLimits::default())
.unwrap();
assert_eq!(reopened.project(), &expected);
}
}
}
#[test]
fn group_stationary_click_collapses_but_offaxis_outback_cancel_preserve_group_and_redo() {
for messages in [false, true] {
for mode in 0..4 {
let (mut app, ids) = group(messages, true);
let (seed, start, _) = begin(&mut app, target(messages, ids[0]));
finish(&mut app, seed, start + delta(messages, 20.));
let _ = app.update(Message::ShortcutUndo(false));
assert!(app.document.can_redo());
let original = app.document.snapshot();
let selected = app.multiselect_snapshot().unwrap();
let primary = app.selected;
let camera = app.behavior.scenario.camera_boundary();
let (event, start, _) = begin(&mut app, target(messages, ids[1]));
match mode {
0 => finish(&mut app, event, start),
1 => finish(&mut app, event, start + delta(!messages, 50.)),
2 => {
move_to(&mut app, event, start + delta(messages, 30.));
finish(&mut app, event, start);
}
_ => {
move_to(&mut app, event, start + delta(messages, 30.));
send(
&mut app,
Event::Cancel {
stamp: event.stamp(),
token: event.token(),
},
);
}
}
assert_eq!(app.project(), &original.decoded.project);
assert_eq!(app.document.bundle(), &original.bundle);
assert!(app.document.can_redo() && !app.document.can_undo());
assert!(camera == app.behavior.scenario.camera_boundary());
if mode == 0 {
assert_eq!(
app.multiselect_snapshot().unwrap().targets(),
&[SelectedTarget::ScenarioObject(ids[1])]
);
assert_eq!(app.selected, ids[1]);
} else {
assert_eq!(
app.multiselect_snapshot().unwrap().targets(),
selected.targets()
);
assert_eq!(app.selected, primary);
}
}
}
}
#[test]
fn stale_group_packets_and_generation_forgery_do_not_disturb_a_newer_live_gesture() {
let (mut app, ids) = group(true, true);
let (old, start, _) = begin(&mut app, Target::Message(ids[0]));
send(
&mut app,
Event::Cancel {
stamp: old.stamp(),
token: old.token(),
},
);
let (current, current_start, _) = begin(&mut app, Target::Message(ids[1]));
let original = app.document.snapshot();
let selection = app.multiselect_snapshot();
let epoch = app.canvas_epoch;
let status = app.status.clone();
let mut forged = current;
if let Event::Begin { stamp, .. } = &mut forged {
stamp.selection_generation = stamp.selection_generation.wrapping_add(1);
}
for event in [
old,
forged,
Event::Move {
stamp: old.stamp(),
token: old.token(),
screen: start + Vector::new(0., 90.),
},
Event::Finish {
stamp: old.stamp(),
token: old.token(),
screen: start + Vector::new(0., 90.),
},
Event::Cancel {
stamp: old.stamp(),
token: old.token(),
},
] {
send(&mut app, event);
assert_eq!(app.canvas_epoch, epoch);
assert_eq!(app.status, status);
assert_eq!(app.multiselect_snapshot(), selection);
assert_eq!(app.document.bundle(), &original.bundle);
assert_eq!(app.behavior.scenario.spacing_token(), Some(current.token()));
}
finish(&mut app, current, current_start + Vector::new(0., 24.));
assert!(app.document.can_undo());
}
#[test]
fn mixed_group_drag_refuses_whole_set_without_primary_only_change_or_replay() {
let (mut app, lane, message) = setup();
choose(&mut app, message, multiselect::Choice::Add);
choose(&mut app, lane, multiselect::Choice::Add);
let event = packet_for_app(&app, Target::Message(message));
let original = app.document.snapshot();
let selected = app.multiselect_snapshot().unwrap();
let epoch = app.canvas_epoch;
send(&mut app, event);
assert_eq!(app.canvas_epoch, epoch);
let Event::Begin { press_screen, .. } = event else {
unreachable!()
};
move_to(&mut app, event, press_screen + Vector::new(0., 30.));
assert_eq!(app.project(), &original.decoded.project);
finish(&mut app, event, press_screen + Vector::new(0., 30.));
assert_ne!(app.canvas_epoch, epoch);
assert!(app.behavior.scenario.spacing_token().is_none());
assert_eq!(app.document.bundle(), &original.bundle);
assert_eq!(app.project(), &original.decoded.project);
assert_eq!(
app.multiselect_snapshot().unwrap().targets(),
selected.targets()
);
assert_eq!(
app.multiselect_snapshot().unwrap().primary(),
selected.primary()
);
let status = app.status.clone();
let epoch = app.canvas_epoch;
send(&mut app, event);
assert_eq!(app.status, status);
assert_eq!(app.canvas_epoch, epoch);
}
#[test]
fn mixed_group_stationary_click_on_either_role_still_selects_only_that_member() {
for lane_hit in [false, true] {
let (mut app, lane, message) = setup();
choose(&mut app, message, multiselect::Choice::Add);
choose(&mut app, lane, multiselect::Choice::Add);
let original = app.document.snapshot();
let member = if lane_hit { lane } else { message };
let (event, start, _) = begin(&mut app, target(!lane_hit, member));
finish(&mut app, event, start);
assert_eq!(
app.multiselect_snapshot().unwrap().targets(),
&[SelectedTarget::ScenarioObject(member)]
);
assert_eq!(app.selected, member);
assert_eq!(app.document.bundle(), &original.bundle);
assert!(!app.document.can_undo());
}
}
#[test]
fn malformed_current_begin_consumes_its_lifetime_without_editing_or_replaying() {
let (mut app, ids) = group(true, true);
let mut event = packet_for_app(&app, Target::Message(ids[0]));
if let Event::Begin { canvas_bounds, .. } = &mut event {
canvas_bounds.width = 0.;
}
let original = app.document.snapshot();
let selection = app.multiselect_snapshot().unwrap();
let epoch = app.canvas_epoch;
send(&mut app, event);
assert_ne!(app.canvas_epoch, epoch);
assert!(app.behavior.scenario.spacing_token().is_none());
assert_eq!(app.document.bundle(), &original.bundle);
assert_eq!(
app.multiselect_snapshot().unwrap().targets(),
selection.targets()
);
let epoch = app.canvas_epoch;
let status = app.status.clone();
send(&mut app, event);
assert_eq!(app.canvas_epoch, epoch);
assert_eq!(app.status, status);
}
#[test]
fn group_source_write_failure_is_atomic_and_preserves_selection_and_camera() {
let (mut app, ids) = group(true, true);
let bundle = app.document.bundle().clone();
let mut limits = oa_core::source_project::ProjectLimits::default();
limits.model.max_source_bytes = bundle.files.values().map(String::len).sum();
app.document = Document::from_bundle(bundle, limits).unwrap();
app.sync();
let original = app.document.snapshot();
let selection = app.multiselect_snapshot().unwrap();
let primary = app.selected;
let camera = app.behavior.scenario.camera_boundary();
let (event, start, _) = begin(&mut app, Target::Message(ids[1]));
finish(&mut app, event, start + Vector::new(0., 24.));
assert!(
app.status.starts_with("Spacing was not applied:"),
"{}",
app.status
);
assert_eq!(app.project(), &original.decoded.project);
assert_eq!(app.document.bundle(), &original.bundle);
assert_eq!(
app.multiselect_snapshot().unwrap().targets(),
selection.targets()
);
assert_eq!(app.selected, primary);
assert!(camera == app.behavior.scenario.camera_boundary());
assert!(!app.document.can_undo());
}
@@ -8,7 +8,7 @@ use behavior::scenario::{
};
use iced::{Point, Size, Vector};
pub(super) fn setup() -> (App, Id, Id) {
fn setup() -> (App, Id, Id) {
let (document, owner, diagram, lane, message) = fixture();
let mut app = App::with_project(document.project().clone());
let _ = app.update(Message::Select(owner));
@@ -88,21 +88,14 @@ fn stale_text_and_scenario_messages_do_not_retire_a_new_saved_spacing_gesture()
"the current gesture remains usable"
);
}
pub(in crate::app) fn packet_for_app(app: &App, target: Target) -> Event {
let mut event = packet(
fn begin(app: &mut App, target: Target) -> (Event, Point, f32) {
let event = packet(
app.project(),
&app.behavior.scenario,
app.scenario_context().unwrap(),
app.camera_epoch,
target,
);
if let Event::Begin { stamp, .. } = &mut event {
stamp.selection_generation = app.multiselect_snapshot().unwrap().generation();
}
event
}
pub(super) fn begin(app: &mut App, target: Target) -> (Event, Point, f32) {
let event = packet_for_app(app, target);
let Event::Begin {
press_screen,
scale,
@@ -115,10 +108,10 @@ pub(super) fn begin(app: &mut App, target: Target) -> (Event, Point, f32) {
assert_eq!(app.behavior.scenario.spacing_token(), Some(event.token()));
(event, press_screen, scale)
}
pub(super) fn send(app: &mut App, event: Event) {
fn send(app: &mut App, event: Event) {
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Spacing(event)));
}
pub(super) fn move_to(app: &mut App, begin: Event, screen: Point) {
fn move_to(app: &mut App, begin: Event, screen: Point) {
send(
app,
Event::Move {
@@ -128,7 +121,7 @@ pub(super) fn move_to(app: &mut App, begin: Event, screen: Point) {
},
);
}
pub(super) fn finish(app: &mut App, begin: Event, screen: Point) {
fn finish(app: &mut App, begin: Event, screen: Point) {
send(
app,
Event::Finish {
+6 -209
View File
@@ -1243,7 +1243,6 @@ struct Scene {
camera_pan: iced::Vector,
interaction: Option<(scenario::Context, scenario::CanvasTool, bool)>,
spacing_stamp: Option<scenario::spacing::Stamp>,
selection: Option<(scenario::Context, crate::diagram_selection::Snapshot)>,
coordinate_input: bool,
pending_placement: Option<(scenario::Slot, Option<Id>)>,
lanes: BTreeMap<Id, f32>,
@@ -1263,8 +1262,6 @@ struct SequenceSlot {
}
#[derive(Default)]
struct SequenceInput {
modifiers: iced::keyboard::Modifiers,
group_press: Option<ScenarioGroupPress>,
context: Option<scenario::Context>,
tool: scenario::CanvasTool,
pointer: Option<Point>,
@@ -1272,13 +1269,6 @@ struct SequenceInput {
source: Option<(Id, scenario::Slot)>,
press: Option<SequencePress>,
}
struct ScenarioGroupPress {
context: scenario::Context,
snapshot: crate::diagram_selection::Snapshot,
target: crate::diagram_usage::Target,
start: Point,
moved: bool,
}
#[derive(Clone, Copy)]
enum SequencePress {
Spacing {
@@ -1319,48 +1309,11 @@ impl SequenceInput {
cancel
}
fn cancel(&mut self) {
self.group_press = None;
self.source = None;
self.press = None;
}
}
impl Scene {
fn selection_targets(&self) -> Vec<crate::diagram_usage::Target> {
let mut ids = std::collections::BTreeSet::new();
ids.extend(
self.boxes
.iter()
.filter(|b| !b.title.is_empty())
.map(|b| b.id),
);
ids.extend(self.lines.iter().map(|line| line.id));
ids.extend(self.activations.iter().map(|segment| segment.call));
ids.extend(self.render_lanes.keys().copied());
ids.extend(self.lifecycle.selection_markers().map(|(id, _)| id));
ids.into_iter()
.map(crate::diagram_usage::Target::ScenarioObject)
.collect()
}
fn is_selected(&self, id: Id) -> bool {
self.selection
.as_ref()
.map_or(id == self.selected, |(_, snapshot)| {
snapshot
.targets()
.contains(&crate::diagram_usage::Target::ScenarioObject(id))
})
}
fn is_group(&self) -> bool {
self.selection
.as_ref()
.is_some_and(|(_, snapshot)| snapshot.targets().len() > 1)
}
fn selection_action(&self, intent: scenario::selection::Intent) -> Option<Action<Message>> {
let (context, snapshot) = self.selection.as_ref()?;
Some(
Action::publish(scenario::selection::message(*context, snapshot, intent)).and_capture(),
)
}
fn removal_target(&self, owner: Id, id: Id) -> bool {
id != owner
&& (self.boxes.iter().any(|item| item.id == id)
@@ -1387,7 +1340,6 @@ impl Scene {
selected,
interaction: None,
spacing_stamp: None,
selection: None,
coordinate_input: true,
pending_placement: None,
lanes: BTreeMap::new(),
@@ -2030,24 +1982,7 @@ impl canvas::Program<Message> for Scene {
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Option<Action<Message>> {
use crate::{app::multiselect::Choice, diagram_usage::Target};
use scenario::selection::Intent;
use scenario::{CanvasEvent, CanvasTool};
if let canvas::Event::Keyboard(iced::keyboard::Event::ModifiersChanged(modifiers)) = event {
state.modifiers = *modifiers;
return None;
}
if matches!(event, canvas::Event::Window(iced::window::Event::Unfocused)) {
state.modifiers = iced::keyboard::Modifiers::default();
}
if state.group_press.as_ref().is_some_and(|press| {
self.selection.as_ref().is_none_or(|(context, snapshot)| {
*context != press.context || *snapshot != press.snapshot
})
}) {
state.cancel();
}
let (context, tool, editable) = self
.interaction
.map_or((None, CanvasTool::Select, false), |(c, t, e)| {
@@ -2071,11 +2006,7 @@ impl canvas::Program<Message> for Scene {
}
}
state.bounds = Some(bounds);
if !editable {
state.group_press = None;
}
if (!editable || !self.coordinate_input)
&& matches!(state.press, Some(SequencePress::Spacing { .. }))
&& let Some(cancelled) = state.cancel_spacing()
{
return Some(cancelled);
@@ -2106,9 +2037,6 @@ impl canvas::Program<Message> for Scene {
let cursor = match event {
canvas::Event::Mouse(mouse::Event::CursorMoved { position }) => {
state.pointer = Some(*position);
if let Some(press) = &mut state.group_press {
press.moved |= position.distance(press.start) >= 3.;
}
if let Some(press) = &mut state.press {
match press {
SequencePress::Message { start, moved, .. }
@@ -2147,9 +2075,6 @@ impl canvas::Program<Message> for Scene {
&& state.source.is_none() =>
{
let context = context?;
if self.is_group() {
return self.selection_action(Intent::Refuse("Removing multiple Scenario objects is being implemented; select one occurrence to remove it".into()));
}
if self.removal_target(context.scenario, self.selected) {
return Some(
Action::publish(Message::ScenarioCanvas(CanvasEvent::Remove(
@@ -2164,10 +2089,7 @@ impl canvas::Program<Message> for Scene {
canvas::Event::Keyboard(iced::keyboard::Event::KeyPressed {
key: iced::keyboard::Key::Named(iced::keyboard::key::Named::Escape),
..
}) if state.source.is_some()
|| state.press.is_some()
|| state.group_press.is_some() =>
{
}) if state.source.is_some() || state.press.is_some() => {
return state
.cancel_spacing()
.or_else(|| Some(Action::request_redraw().and_capture()));
@@ -2181,18 +2103,6 @@ impl canvas::Program<Message> for Scene {
)
});
match event {
canvas::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) if editable => {
let (context, _) = self.selection.as_ref()?;
let hit = self.hit_item(world?)?;
let mut request = crate::model_menu::Request::object(hit.id, cursor.position()?);
request.scenario = Some(Box::new(*context));
request.selection_target = Some(Box::new(Target::ScenarioObject(hit.id)));
state.cancel();
Some(
Action::publish(Message::Menu(crate::model_menu::Event::Open(request)))
.and_capture(),
)
}
canvas::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
if !visible {
return None;
@@ -2202,83 +2112,6 @@ impl canvas::Program<Message> for Scene {
if !editable {
return None;
}
if let Some((selection_context, snapshot)) = &self.selection {
let hit = self.hit_item(point).or_else(|| {
(!self.is_group()
&& self.is_selected(self.selected)
&& matches!(
context.representation,
scenario_layout::Representation::Derived { .. }
))
.then(|| {
self.reorder_handle()
.filter(|handle| handle.contains(point))
})
.flatten()
.map(|_| SceneHit {
id: self.selected,
role: SceneHitRole::Box,
})
});
if tool != CanvasTool::Select && self.is_group() {
return self.selection_action(Intent::Refuse(
"Select one occurrence before using an authoring tool".into(),
));
}
if tool == CanvasTool::Select {
if let Some(hit) = hit {
let target = Target::ScenarioObject(hit.id);
if state.modifiers.shift() {
state.cancel();
return self
.selection_action(Intent::Choose(target, Choice::Toggle));
}
let group_spacing_hit = self.coordinate_input
&& self.spacing_stamp.is_some()
&& matches!(
context.representation,
scenario_layout::Representation::Saved { .. }
)
&& matches!(
hit.role,
SceneHitRole::LifelineHeader
| SceneHitRole::MessageBody
| SceneHitRole::MessageLabel
);
if self.is_group() && self.is_selected(hit.id) && !group_spacing_hit
{
state.group_press = Some(ScenarioGroupPress {
context: *selection_context,
snapshot: snapshot.clone(),
target,
start: cursor.position()?,
moved: false,
});
return Some(Action::request_redraw().and_capture());
}
// Preserve the existing first-press singleton
// spacing/reorder gesture. Replacing a displayed
// group is an explicit selection change first.
if (self.is_group() && !self.is_selected(hit.id))
|| !self.coordinate_input
{
return self
.selection_action(Intent::Choose(target, Choice::Replace));
}
} else {
state.cancel();
if state.modifiers.shift() {
return Some(Action::capture());
}
// Background clearing shares this press with camera panning.
return Some(Action::publish(scenario::selection::message(
*selection_context,
snapshot,
Intent::Clear,
)));
}
}
}
if !self.coordinate_input {
return self.hit(point).map(|id| {
Action::publish(Message::ScenarioCanvas(CanvasEvent::Select(
@@ -2381,27 +2214,6 @@ impl canvas::Program<Message> for Scene {
.map(|id| Action::publish(Message::Select(id)).and_capture())
}
canvas::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
if let Some(press) = state.group_press.take() {
if world.is_none() {
return Some(Action::request_redraw());
}
let intent = if press.moved {
Intent::Refuse(
"This selection cannot be moved here; select one occurrence for its individual actions"
.into(),
)
} else {
Intent::Collapse(press.target)
};
return Some(
Action::publish(scenario::selection::message(
press.context,
&press.snapshot,
intent,
))
.and_capture(),
);
}
let press = state.press.take()?;
let context = context?;
match press {
@@ -2553,7 +2365,7 @@ impl canvas::Program<Message> for Scene {
}
}),
Stroke::default()
.with_color(if self.is_selected(guide.id) {
.with_color(if guide.id == self.selected {
style::TEAL
} else {
style::MUTED
@@ -2579,7 +2391,7 @@ impl canvas::Program<Message> for Scene {
f.stroke(
&path,
Stroke::default()
.with_color(if self.is_selected(activation.call) {
.with_color(if activation.call == self.selected {
style::TEAL
} else {
style::BOUNDARY
@@ -2596,7 +2408,7 @@ impl canvas::Program<Message> for Scene {
b.marker,
Some(Kind::Region | Kind::StateMachine | Kind::Occurrence)
);
let color = if self.is_selected(b.id) {
let color = if b.id == self.selected {
style::TEAL
} else {
style::BOUNDARY
@@ -2618,7 +2430,7 @@ impl canvas::Program<Message> for Scene {
}
}
for l in self.lines.iter().filter(|line| !line.label.is_empty()) {
let color = if self.is_selected(l.id) {
let color = if l.id == self.selected {
style::TEAL
} else {
style::MUTED
@@ -2674,7 +2486,7 @@ impl canvas::Program<Message> for Scene {
content: short(&b.title, ((b.rect.width - 25.) / 7.) as usize),
position: Point::new(b.rect.x + 12., b.rect.y + 12.),
size: 13.into(),
color: if self.is_selected(b.id) {
color: if b.id == self.selected {
style::TEAL
} else {
style::INK
@@ -2704,19 +2516,6 @@ impl canvas::Program<Message> for Scene {
}
self.lifecycle
.draw_markers(&mut f, self.selected, self.scale, self.camera_pan);
if self.selection.is_some() {
// Lifecycle glyphs use their exact painted hit identity too.
for (id, point) in self.lifecycle.selection_markers() {
if self.is_selected(id) {
f.stroke(
&Path::circle(point, 12.),
Stroke::default()
.with_color(style::TEAL)
.with_width(if id == self.selected { 3. } else { 1.5 }),
);
}
}
}
if let Some((context, tool, editable)) = self.interaction {
let active = editable
&& self.coordinate_input
@@ -2743,8 +2542,6 @@ impl canvas::Program<Message> for Scene {
context.representation,
scenario_layout::Representation::Derived { .. }
)
&& !self.is_group()
&& self.is_selected(self.selected)
&& let Some(handle) = self.reorder_handle()
{
f.fill_rectangle(handle.position(), handle.size(), style::TEAL);
+19 -22
View File
@@ -6,12 +6,6 @@ use iced::widget::{Column, column, text_input};
use std::{cell::RefCell, collections::BTreeSet};
mod camera;
pub(crate) mod focus;
pub(crate) mod group_reorder;
pub(crate) mod reorder_session;
#[cfg(test)]
mod reorder_slot_experiments;
pub(crate) mod reorder_view;
pub(crate) mod selection;
pub(crate) mod spacing;
use super::scenario_layout::{
Representation,
@@ -149,7 +143,6 @@ impl std::fmt::Display for CanvasTool {
}
#[derive(Debug, Clone)]
pub enum CanvasEvent {
Selection(Box<selection::Event>),
Spacing(spacing::Event),
Scrolled(Context, ScrollArea, iced::Vector),
Restored(Context, Id, ScrollArea, iced::Vector),
@@ -193,7 +186,6 @@ impl CanvasEvent {
pub fn context(&self) -> Context {
match self {
Self::Spacing(event) => event.stamp().context,
Self::Selection(event) => event.context,
Self::Scrolled(context, _, _)
| Self::Restored(context, ..)
| Self::Viewport(context, _)
@@ -1261,21 +1253,26 @@ pub(crate) fn reorder(
occurrence: Id,
slot: Slot,
) -> Result<Command, String> {
let prepared = group_reorder::Prepared::new(
std::sync::Arc::new(p.clone()),
owner,
&[crate::diagram_usage::Target::ScenarioObject(occurrence)],
)?;
match prepared.plan(slot)? {
group_reorder::Plan::Change(command) => Ok(*command),
group_reorder::Plan::Noop => Ok(Command::SetSteps(
slot.parent,
p.elements[&slot.parent]
.steps()
.expect("admitted sequence")
.clone(),
)),
let to = slot_index(p, owner, slot)?;
if p.elements.get(&occurrence).and_then(|e| e.parent) != Some(slot.parent) {
return Err(
"Reorder inside the occurrence's current sequence; dragging never changes its operand."
.into(),
);
}
let mut steps = p.elements[&slot.parent]
.steps()
.expect("slot validated")
.clone();
let from = steps
.iter()
.position(|id| *id == occurrence)
.ok_or("This occurrence is not in the sequence.")?;
steps.remove(from);
steps.insert(to - usize::from(from < to), occurrence);
let command = Command::SetSteps(slot.parent, steps);
execute_batch(p, std::slice::from_ref(&command)).map_err(|error| error.to_string())?;
Ok(command)
}
fn compatible_binding(p: &Project, referenced: Id, local: Id) -> bool {
represented(p, referenced)
@@ -1,179 +0,0 @@
//! Frozen, all-or-nothing reorder of one immediate Scenario sequence.
use super::Slot;
use crate::diagram_usage::Target;
use oa_core::{Command, Id, Kind, OccurrenceBody, Project, SharedBody, execute_batch};
use std::{collections::BTreeSet, sync::Arc};
#[derive(Debug)]
pub(crate) enum Plan {
Noop,
Change(Box<Command>),
}
pub(crate) struct Prepared {
project: Arc<Project>,
parent: Id,
original: Vec<Id>,
selected: BTreeSet<Id>,
}
fn step_body(project: &Project, id: Id) -> Option<&OccurrenceBody> {
match project.elements.get(&id)?.shared()? {
SharedBody::Occurrence { body, .. }
if matches!(
body,
OccurrenceBody::Exchange { .. }
| OccurrenceBody::Action { .. }
| OccurrenceBody::State { .. }
| OccurrenceBody::ScenarioUse { .. }
| OccurrenceBody::Fragment { .. }
) =>
{
Some(body)
}
_ => None,
}
}
// Follow the same structural edges that make rows visible; parent pointers alone
// would admit children of an operand omitted from its fragment's branch list.
fn reachable_parent(project: &Project, owner: Id, wanted: Id) -> bool {
let mut pending = vec![owner];
let mut visited = BTreeSet::new();
while let Some(parent) = pending.pop() {
if !visited.insert(parent) {
continue;
}
if parent == wanted {
return true;
}
let Some(steps) = project.elements.get(&parent).and_then(|e| e.steps()) else {
continue;
};
for id in steps {
if project.elements.get(id).and_then(|e| e.parent) != Some(parent) {
continue;
}
if let Some(OccurrenceBody::Fragment { branches, .. }) = step_body(project, *id) {
pending.extend(branches.iter().copied().filter(|branch| {
project.elements.get(branch).is_some_and(|element| {
element.parent == Some(*id)
&& matches!(
element.shared(),
Some(SharedBody::Occurrence {
body: OccurrenceBody::Branch { .. },
..
})
)
})
}));
}
}
}
false
}
impl Prepared {
pub(crate) fn new(
project: Arc<Project>,
owner: Id,
targets: &[Target],
) -> Result<Self, String> {
if project.elements.get(&owner).map(|e| e.kind()) != Some(Kind::Scenario) {
return Err("This scenario no longer exists.".into());
}
let mut selected = BTreeSet::new();
let mut parent = None;
for target in targets {
let Target::ScenarioObject(id) = target else {
return Err("Select only ordered occurrences in this scenario.".into());
};
if !selected.insert(*id) || step_body(&project, *id).is_none() {
return Err(
"Every selected subject must be a unique ordered scenario occurrence.".into(),
);
}
let actual = project.elements[id]
.parent
.ok_or("The occurrence has no sequence.")?;
if parent.is_some_and(|parent| parent != actual) {
return Err(
"Reorder inside one immediate sequence; dragging never changes operands."
.into(),
);
}
parent = Some(actual);
}
let parent = parent.ok_or("Select at least one occurrence.")?;
if !reachable_parent(&project, owner, parent) {
return Err("The selected sequence is not reachable in this scenario.".into());
}
let original = project.elements[&parent]
.steps()
.ok_or("The parent is not a sequence.")?
.clone();
let mut listed = BTreeSet::new();
for id in &original {
if !listed.insert(*id)
|| step_body(&project, *id).is_none()
|| project.elements[id].parent != Some(parent)
{
return Err("The complete sequence must contain unique, existing occurrences of this parent.".into());
}
}
if !selected.is_subset(&listed) {
return Err("A selected occurrence is not in this sequence.".into());
}
// Cache a validated immutable baseline, including descendants and other
// sequences. Advisory authoring diagnostics deliberately remain allowed.
oa_core::session::validate_candidate(&project).map_err(|e| e.to_string())?;
Ok(Self {
project,
parent,
original,
selected,
})
}
pub(crate) fn plan(&self, slot: Slot) -> Result<Plan, String> {
if slot.parent != self.parent {
return Err("Reorder inside the occurrence's current sequence; dragging never changes its operand.".into());
}
if let Some(anchor) = slot.before {
if !self.original.contains(&anchor) {
return Err("The insertion slot changed. Choose its position again.".into());
}
if self.selected.contains(&anchor) {
return Ok(Plan::Noop);
}
}
let mut steps: Vec<_> = self
.original
.iter()
.copied()
.filter(|id| !self.selected.contains(id))
.collect();
let at = slot.before.map_or(steps.len(), |anchor| {
steps
.iter()
.position(|id| *id == anchor)
.expect("admitted anchor")
});
steps.splice(
at..at,
self.original
.iter()
.copied()
.filter(|id| self.selected.contains(id)),
);
if steps == self.original {
return Ok(Plan::Noop);
}
let command = Command::SetSteps(self.parent, steps);
execute_batch(&self.project, std::slice::from_ref(&command)).map_err(|e| e.to_string())?;
Ok(Plan::Change(Box::new(command)))
}
}
#[cfg(test)]
mod tests;
@@ -1,720 +0,0 @@
use super::*;
use crate::{behavior::topology::occurrence, document::Document};
use oa_core::{FragmentOperator, Layer, LifelineBinding, MessageKind, language};
fn add(p: &mut Project, parent: Id, name: &str, body: OccurrenceBody) -> Id {
let e = occurrence(p, parent, name, body);
let id = e.id;
p.elements.insert(id, e);
p.elements
.get_mut(&parent)
.unwrap()
.steps_mut()
.unwrap()
.push(id);
id
}
fn row(p: &mut Project, parent: Id, name: &str) -> Id {
add(
p,
parent,
name,
OccurrenceBody::Action {
action: None,
lifeline: None,
},
)
}
fn operand(p: &mut Project, parent: Id, operator: FragmentOperator) -> (Id, Id) {
let fragment = add(
p,
parent,
"Fragment",
OccurrenceBody::Fragment {
operator,
branches: vec![],
},
);
let branch = occurrence(
p,
fragment,
"Operand",
OccurrenceBody::Branch {
guard: "ready".into(),
occurrences: vec![],
},
);
let id = branch.id;
p.elements.insert(id, branch);
if let Some(SharedBody::Occurrence {
body: OccurrenceBody::Fragment { branches, .. },
..
}) = p.elements.get_mut(&fragment).unwrap().shared_mut()
{
branches.push(id);
}
(fragment, id)
}
fn prepare(p: &Project, owner: Id, ids: &[Id]) -> Result<Prepared, String> {
Prepared::new(
Arc::new(p.clone()),
owner,
&ids.iter()
.copied()
.map(Target::ScenarioObject)
.collect::<Vec<_>>(),
)
}
fn slot(parent: Id, before: Option<Id>) -> Slot {
Slot { parent, before }
}
fn ordered(p: &Project, parent: Id, ids: &[Id]) -> Project {
let mut expected = p.clone();
*expected
.elements
.get_mut(&parent)
.unwrap()
.steps_mut()
.unwrap() = ids.to_vec();
expected
}
fn exact_transaction(p: &Project, plan: Plan, expected: Project) {
let Plan::Change(command) = plan else {
panic!("expected a change")
};
assert_eq!(
execute_batch(p, std::slice::from_ref(&command)).unwrap(),
expected
);
let mut doc = Document::from_project(p.clone(), Default::default()).unwrap();
let original_source = doc.bundle().clone();
doc.execute(*command).unwrap();
assert_eq!(doc.project(), &expected);
assert_eq!(
language::parse(&language::format(doc.project())).unwrap(),
expected
);
let changed_source = doc.bundle().clone();
assert_eq!(
Document::from_bundle(changed_source.clone(), Default::default())
.unwrap()
.project(),
&expected
);
assert!(doc.undo());
assert_eq!(doc.project(), p);
assert_eq!(doc.bundle(), &original_source);
assert!(!doc.can_undo());
assert!(doc.redo());
assert_eq!(doc.project(), &expected);
assert_eq!(doc.bundle(), &changed_source);
assert!(!doc.can_redo());
}
fn four(nested: bool) -> (Project, Id, Id, [Id; 4]) {
four_in_layer(nested, Layer::Oa)
}
fn four_in_layer(nested: bool, layer: Layer) -> (Project, Id, Id, [Id; 4]) {
let mut p = Project::empty();
let owner = p
.add_element(oa_core::Kind::Scenario, layer, "Scenario", None)
.unwrap();
let parent = if nested {
operand(&mut p, owner, FragmentOperator::Alternative).1
} else {
owner
};
let ids = ["A", "B", "C", "D"].map(|name| row(&mut p, parent, name));
oa_core::session::validate_candidate(&p).unwrap();
(p, owner, parent, ids)
}
#[test]
fn root_and_operand_use_semantic_block_order_with_exact_history() {
for (nested, layer) in [
(false, Layer::Oa),
(true, Layer::Oa),
(false, Layer::Sa),
(true, Layer::Sa),
] {
let (p, owner, parent, [a, b, c, d]) = four_in_layer(nested, layer);
let prepared = prepare(&p, owner, &[c, a]).unwrap();
exact_transaction(
&p,
prepared.plan(slot(parent, Some(d))).unwrap(),
ordered(&p, parent, &[b, a, c, d]),
);
exact_transaction(
&p,
prepared.plan(slot(parent, None)).unwrap(),
ordered(&p, parent, &[b, d, a, c]),
);
}
}
#[test]
fn singleton_wrapper_noop_preserves_existing_redo_and_source() {
for nested in [false, true] {
let (p, owner, parent, [a, b, c, d]) = four(nested);
let mut doc = Document::from_project(p.clone(), Default::default()).unwrap();
let change = super::super::reorder(doc.project(), owner, a, slot(parent, None)).unwrap();
doc.execute(change).unwrap();
assert_eq!(doc.project(), &ordered(&p, parent, &[b, c, d, a]));
let changed = doc.bundle().clone();
assert!(doc.undo());
let source = doc.bundle().clone();
let revision = doc.revision();
for before in [Some(a), Some(b)] {
let noop =
super::super::reorder(doc.project(), owner, a, slot(parent, before)).unwrap();
doc.execute(noop).unwrap();
assert_eq!(doc.project(), &p);
assert_eq!(doc.bundle(), &source);
assert_eq!(doc.revision(), revision);
assert!(!doc.can_undo());
assert!(doc.can_redo());
}
assert!(doc.redo());
assert_eq!(doc.bundle(), &changed);
}
}
#[test]
fn selected_anchor_and_exact_noop_preserve_redo_and_frozen_snapshot() {
let (p, owner, parent, [a, b, c, d]) = four(false);
let prepared = prepare(&p, owner, &[c, a]).unwrap();
let mut doc = Document::from_project(p.clone(), Default::default()).unwrap();
let Plan::Change(command) = prepared.plan(slot(parent, Some(d))).unwrap() else {
panic!()
};
doc.execute(*command).unwrap();
let changed = doc.bundle().clone();
assert!(doc.undo());
let source = doc.bundle().clone();
let revision = doc.revision();
for plan in [
prepared.plan(slot(parent, Some(c))),
prepare(&p, owner, &[a])
.unwrap()
.plan(slot(parent, Some(b))),
prepare(&p, owner, &[c, d])
.unwrap()
.plan(slot(parent, None)),
] {
assert!(matches!(plan.unwrap(), Plan::Noop));
assert_eq!(doc.project(), &p);
assert_eq!(doc.bundle(), &source);
assert_eq!(doc.revision(), revision);
assert!(doc.can_redo());
}
assert!(doc.redo());
assert_eq!(doc.bundle(), &changed);
// The earlier prepared value still plans from its original owned snapshot.
exact_transaction(
&p,
prepared.plan(slot(parent, Some(d))).unwrap(),
ordered(&p, parent, &[b, a, c, d]),
);
}
#[test]
fn admission_rejects_every_invalid_member_list_and_destination_before_noop() {
let (p, owner, parent, [a, b, c, d]) = four(false);
for ids in [vec![], vec![a, a], vec![a, Id::new_v4()]] {
assert!(prepare(&p, owner, &ids).is_err());
}
for target in [Target::Whole, Target::Placement(a)] {
assert!(
Prepared::new(
Arc::new(p.clone()),
owner,
&[Target::ScenarioObject(a), target]
)
.is_err()
);
}
let mut with_line = p.clone();
let line = with_line
.add_element(Kind::Lifeline, Layer::Oa, "Line", Some(owner))
.unwrap();
assert!(prepare(&with_line, owner, &[a, line]).is_err());
let mut nested = p.clone();
let (fragment, branch) = operand(&mut nested, owner, FragmentOperator::Alternative);
let child = row(&mut nested, branch, "Child");
assert!(prepare(&nested, owner, &[a, child]).is_err());
assert!(prepare(&nested, owner, &[fragment, child]).is_err());
let prepared = prepare(&p, owner, &[a, c]).unwrap();
assert!(prepared.plan(slot(branch, Some(a))).is_err());
assert!(prepared.plan(slot(parent, Some(Id::new_v4()))).is_err());
for list in [vec![a, b, c, b], vec![a, Id::new_v4(), c, d]] {
assert!(prepare(&ordered(&p, parent, &list), owner, &[a, c]).is_err());
}
let mut wrong_parent = p.clone();
wrong_parent.elements.get_mut(&b).unwrap().parent = Some(branch);
assert!(prepare(&wrong_parent, owner, &[a, c]).is_err());
for body in [
OccurrenceBody::Control {
kind: oa_core::ControlKind::Choice,
},
OccurrenceBody::SequenceReference { sequence: None },
] {
let mut wrong_type = p.clone();
if let Some(SharedBody::Occurrence { body: actual, .. }) =
wrong_type.elements.get_mut(&b).unwrap().shared_mut()
{
*actual = body;
}
assert!(prepare(&wrong_type, owner, &[a, c]).is_err());
}
if let Some(SharedBody::Occurrence {
body: OccurrenceBody::Fragment { branches, .. },
..
}) = nested.elements.get_mut(&fragment).unwrap().shared_mut()
{
branches.clear();
}
assert!(
prepare(&nested, owner, &[child]).is_err(),
"parent ancestry cannot admit omitted operand"
);
}
#[test]
fn unlisted_warning_child_is_preserved_but_cannot_be_selected() {
let (mut p, owner, parent, [a, b, c, d]) = four(false);
let hidden = row(&mut p, parent, "Hidden draft");
p.elements
.get_mut(&parent)
.unwrap()
.steps_mut()
.unwrap()
.pop();
assert!(
oa_core::validation::validate(&p)
.iter()
.any(|i| i.code == "unordered_occurrence")
);
assert!(prepare(&p, owner, &[hidden]).is_err());
exact_transaction(
&p,
prepare(&p, owner, &[c, a])
.unwrap()
.plan(slot(parent, Some(d)))
.unwrap(),
ordered(&p, parent, &[b, a, c, d]),
);
}
#[test]
fn call_reply_moves_as_explicit_stable_pair_and_invalid_baseline_refuses_noop() {
let f = super::super::direct_tests::fixture();
let mut p = f.project;
let x = row(&mut p, f.owner, "X");
let y = row(&mut p, f.owner, "Y");
p = ordered(&p, f.owner, &[f.call, x, f.reply, y]);
let prepared = prepare(&p, f.owner, &[f.reply, f.call]).unwrap();
exact_transaction(
&p,
prepared.plan(slot(f.owner, None)).unwrap(),
ordered(&p, f.owner, &[x, y, f.call, f.reply]),
);
assert!(
prepare(&p, f.owner, &[f.reply])
.unwrap()
.plan(slot(f.owner, Some(f.call)))
.unwrap_err()
.contains("reply_order")
);
let invalid = ordered(&p, f.owner, &[f.reply, x, f.call, y]);
assert!(prepare(&invalid, f.owner, &[x]).is_err());
}
#[test]
fn nested_fragment_descendant_reply_is_validated_and_subtree_preserved() {
let f = super::super::direct_tests::fixture();
let mut p = f.project;
let x = row(&mut p, f.owner, "X");
let (fragment, outer) = operand(&mut p, f.owner, FragmentOperator::Alternative);
let (_, inner) = operand(&mut p, outer, FragmentOperator::Parallel);
p.elements.get_mut(&f.reply).unwrap().parent = Some(inner);
p.elements
.get_mut(&inner)
.unwrap()
.steps_mut()
.unwrap()
.push(f.reply);
let y = row(&mut p, f.owner, "Y");
p = ordered(&p, f.owner, &[f.call, x, fragment, y]);
let prepared = prepare(&p, f.owner, &[fragment, x]).unwrap();
assert!(
prepared
.plan(slot(f.owner, Some(f.call)))
.unwrap_err()
.contains("reply_order")
);
exact_transaction(
&p,
prepared.plan(slot(f.owner, None)).unwrap(),
ordered(&p, f.owner, &[f.call, y, x, fragment]),
);
}
#[test]
fn nested_lifetime_and_opaque_local_bindings_are_checked_without_expansion() {
let f = super::super::direct_tests::fixture();
let mut p = execute_batch(
&f.project,
&[Command::Delete(f.reply), Command::Delete(f.call)],
)
.unwrap();
let create = add(
&mut p,
f.owner,
"Create",
OccurrenceBody::Exchange {
exchange: None,
sender: Some(f.sender),
receiver: Some(f.receiver),
message_kind: MessageKind::Create,
reply_to: None,
values: vec![],
operation: None,
},
);
let x = row(&mut p, f.owner, "X");
let (fragment, outer) = operand(&mut p, f.owner, FragmentOperator::Alternative);
let (_, inner) = operand(&mut p, outer, FragmentOperator::Parallel);
add(
&mut p,
inner,
"Use local",
OccurrenceBody::Action {
action: None,
lifeline: Some(f.receiver),
},
);
let y = row(&mut p, f.owner, "Y");
let prepared = prepare(&p, f.owner, &[fragment, x]).unwrap();
assert!(
prepared
.plan(slot(f.owner, Some(create)))
.unwrap_err()
.contains("lifeline_lifetime")
);
exact_transaction(
&p,
prepared.plan(slot(f.owner, None)).unwrap(),
ordered(&p, f.owner, &[create, y, x, fragment]),
);
let definition = p
.add_element(Kind::Scenario, Layer::Oa, "Definition", None)
.unwrap();
let represented = match p.elements[&f.receiver].shared().unwrap() {
SharedBody::Lifeline { represented } => *represented,
_ => panic!(),
};
let remote = p
.add_element(Kind::Lifeline, Layer::Oa, "Formal", Some(definition))
.unwrap();
*p.elements.get_mut(&remote).unwrap().shared_mut().unwrap() =
SharedBody::Lifeline { represented };
let uses = ["First use", "Second use"].map(|name| {
add(
&mut p,
f.owner,
name,
OccurrenceBody::ScenarioUse {
scenario: Some(definition),
bindings: vec![LifelineBinding {
referenced: remote,
local: f.receiver,
}],
},
)
});
let prepared = prepare(&p, f.owner, &[uses[1], uses[0]]).unwrap();
assert!(
prepared
.plan(slot(f.owner, Some(create)))
.unwrap_err()
.contains("lifeline_lifetime")
);
exact_transaction(
&p,
prepared.plan(slot(f.owner, Some(x))).unwrap(),
ordered(&p, f.owner, &[create, uses[0], uses[1], x, fragment, y]),
);
}
fn delete_line(p: &mut Project, parent: Id, sender: Id, receiver: Id) {
add(
p,
parent,
"Delete participant",
OccurrenceBody::Exchange {
exchange: None,
sender: Some(sender),
receiver: Some(receiver),
message_kind: MessageKind::Delete,
reply_to: None,
values: vec![],
operation: None,
},
);
}
fn second_operand(p: &mut Project, fragment: Id) -> Id {
let branch = occurrence(
p,
fragment,
"Other operand",
OccurrenceBody::Branch {
guard: "otherwise".into(),
occurrences: vec![],
},
);
let id = branch.id;
p.elements.insert(id, branch);
let Some(SharedBody::Occurrence {
body: OccurrenceBody::Fragment { branches, .. },
..
}) = p.elements.get_mut(&fragment).unwrap().shared_mut()
else {
panic!()
};
branches.push(id);
id
}
#[test]
fn conditional_delete_rejects_moving_an_earlier_use_after_the_fragment() {
let f = super::super::direct_tests::fixture();
let mut p = f.project;
let use_line = add(
&mut p,
f.owner,
"Use participant",
OccurrenceBody::Action {
action: None,
lifeline: Some(f.receiver),
},
);
let x = row(&mut p, f.owner, "X");
let (_, branch) = operand(&mut p, f.owner, FragmentOperator::Alternative);
delete_line(&mut p, branch, f.sender, f.receiver);
row(&mut p, f.owner, "Y");
oa_core::session::validate_candidate(&p).unwrap();
let prepared = prepare(&p, f.owner, &[x, use_line]).unwrap();
assert!(
prepared
.plan(slot(f.owner, None))
.unwrap_err()
.contains("lifeline_lifetime")
);
}
#[test]
fn exclusive_operand_move_is_legal_but_parallel_baseline_is_invalid() {
for operator in [FragmentOperator::Alternative, FragmentOperator::Parallel] {
let f = super::super::direct_tests::fixture();
let mut p = f.project;
let (fragment, left) = operand(&mut p, f.owner, operator);
let right = second_operand(&mut p, fragment);
delete_line(&mut p, left, f.sender, f.receiver);
let use_line = add(
&mut p,
right,
"Use participant",
OccurrenceBody::Action {
action: None,
lifeline: Some(f.receiver),
},
);
let x = row(&mut p, right, "X");
let y = row(&mut p, right, "Y");
if operator == FragmentOperator::Alternative {
oa_core::session::validate_candidate(&p).unwrap();
exact_transaction(
&p,
prepare(&p, f.owner, &[x, use_line])
.unwrap()
.plan(slot(right, None))
.unwrap(),
ordered(&p, right, &[y, use_line, x]),
);
} else {
// This is an invalid input control, not a valid model broken by reorder.
assert!(
oa_core::session::validate_candidate(&p)
.unwrap_err()
.to_string()
.contains("lifeline_lifetime")
);
assert!(prepare(&p, f.owner, &[x, use_line]).is_err());
}
}
}
#[test]
fn repeated_reply_warning_does_not_block_independent_sibling_reorder() {
let f = super::super::direct_tests::fixture();
let mut p = f.project;
let (_, branch) = operand(&mut p, f.owner, FragmentOperator::Loop);
p.elements
.get_mut(&f.owner)
.unwrap()
.steps_mut()
.unwrap()
.retain(|id| *id != f.reply);
p.elements.get_mut(&f.reply).unwrap().parent = Some(branch);
p.elements
.get_mut(&branch)
.unwrap()
.steps_mut()
.unwrap()
.push(f.reply);
let x = row(&mut p, branch, "X");
let y = row(&mut p, branch, "Y");
let z = row(&mut p, branch, "Z");
let warning = |project: &Project| {
oa_core::validation::validate(project).iter().any(|issue| {
issue.element == Some(f.reply)
&& issue.code == "reply_repetition"
&& issue.severity == oa_core::validation::Severity::Warning
})
};
assert!(warning(&p));
let expected = ordered(&p, branch, &[f.reply, y, x, z]);
assert!(warning(&expected));
exact_transaction(
&p,
prepare(&p, f.owner, &[z, x])
.unwrap()
.plan(slot(branch, None))
.unwrap(),
expected,
);
}
#[test]
fn semantic_reorder_preserves_affected_and_unrelated_authored_layouts() {
use oa_core::{
Diagram, DiagramLayout, OccurrenceEndpoint, ProjectionKind, ScenarioLanePosition,
ScenarioLayout, ScenarioMessagePosition,
};
let f = super::super::direct_tests::fixture();
let mut p = f.project;
let x = row(&mut p, f.owner, "X");
let y = row(&mut p, f.owner, "Y");
p = ordered(&p, f.owner, &[f.call, x, f.reply, y]);
for (alias, offset) in [("group_saved_a", 0.), ("group_saved_b", 1500.)] {
let id = Id::new_v4();
p.diagrams.insert(
id,
Diagram {
id,
alias: alias.into(),
name: alias.into(),
description: "Preserve my layout".into(),
layer: Layer::Oa,
projection: ProjectionKind::Scenario { scenario: f.owner },
view_options: Default::default(),
nodes: Default::default(),
routes: vec![],
layout: Some(DiagramLayout::Scenario(ScenarioLayout {
lanes: vec![
ScenarioLanePosition {
lifeline: f.sender,
x: -300. + offset,
},
ScenarioLanePosition {
lifeline: f.receiver,
x: 700. + offset,
},
],
messages: vec![
ScenarioMessagePosition {
address: OccurrenceEndpoint {
references: vec![],
occurrence: f.call,
},
send_y: 123. + offset,
},
ScenarioMessagePosition {
address: OccurrenceEndpoint {
references: vec![],
occurrence: f.reply,
},
send_y: 567. + offset,
},
],
})),
},
);
}
let unrelated = p
.add_element(Kind::Scenario, Layer::Oa, "Unrelated scenario", None)
.unwrap();
let line = p
.add_element(
Kind::Lifeline,
Layer::Oa,
"Unrelated clock",
Some(unrelated),
)
.unwrap();
let timer = add(
&mut p,
unrelated,
"Wait",
OccurrenceBody::Exchange {
exchange: None,
sender: Some(line),
receiver: None,
message_kind: MessageKind::Timer {
expression: "5 s".into(),
},
reply_to: None,
values: vec![],
operation: None,
},
);
let diagram = Id::new_v4();
p.diagrams.insert(
diagram,
Diagram {
id: diagram,
alias: "group_unrelated_saved".into(),
name: "Unrelated arrangement".into(),
description: "Do not touch this view".into(),
layer: Layer::Oa,
projection: ProjectionKind::Scenario {
scenario: unrelated,
},
view_options: Default::default(),
nodes: Default::default(),
routes: vec![],
layout: Some(DiagramLayout::Scenario(ScenarioLayout {
lanes: vec![ScenarioLanePosition {
lifeline: line,
x: -123.,
}],
messages: vec![ScenarioMessagePosition {
address: OccurrenceEndpoint {
references: vec![],
occurrence: timer,
},
send_y: 987.,
}],
})),
},
);
oa_core::session::validate_candidate(&p).unwrap();
exact_transaction(
&p,
prepare(&p, f.owner, &[f.reply, f.call])
.unwrap()
.plan(slot(f.owner, None))
.unwrap(),
ordered(&p, f.owner, &[x, y, f.call, f.reply]),
);
}
@@ -218,7 +218,6 @@ fn complete_fallback_scene_selects_but_cannot_capture_or_finish_coordinate_gestu
let stamp = spacing::Stamp {
context,
camera_epoch: 17,
selection_generation: 0,
};
ready_scene.coordinate_input = true;
ready_scene.interaction = Some((context, CanvasTool::Select, true));
@@ -1,241 +0,0 @@
//! Immutable semantic reorder operation, independent of its destination UI.
use super::{
Context, Slot,
group_reorder::{Plan, Prepared},
presentation, selection,
};
use crate::{diagram_selection::Snapshot, diagram_usage::Target};
use oa_core::{Command, Id, Project, execute_batch};
use std::sync::Arc;
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Outcome {
Change,
Noop,
Refused(String),
}
pub(crate) struct Preview {
pub(crate) slot: Slot,
pub(crate) before: Vec<Id>,
pub(crate) after: Vec<Id>,
pub(crate) outcome: Outcome,
/// Geometry status is independent of semantic validity; requested Saved
/// representation is retained even when presentation reports fallback.
pub(crate) presentation: Option<presentation::ScenarioPresentation>,
pub(crate) selection_visible: bool,
command: Option<Box<Command>>,
}
pub(crate) struct Completion {
pub(crate) command: Option<Box<Command>>,
pub(crate) snapshot: Snapshot,
pub(crate) representation: super::Representation,
pub(crate) presentation_status: presentation::PresentationStatus,
}
pub(crate) struct Session {
project: Arc<Project>,
context: Context,
snapshot: Snapshot,
token: Id,
primary: Id,
parent: Id,
before: Vec<Id>,
prepared: Prepared,
preview: Option<Preview>,
#[cfg(test)]
proposal_builds: usize,
}
impl Session {
pub(crate) fn new(
project: Arc<Project>,
context: Context,
snapshot: Snapshot,
token: Id,
) -> Result<Self, String> {
if token.is_nil()
|| context.document.is_nil()
|| context.visit.is_nil()
|| snapshot.context() != selection::context(context)
|| presentation::checked_owner(&project, context.representation)
!= Some(context.scenario)
{
return Err("The reorder input no longer matches this scenario view.".into());
}
let Some(Target::ScenarioObject(primary)) = snapshot.primary() else {
return Err("Select ordered scenario occurrences.".into());
};
let primary = *primary;
let visible = selection::visible(&project, context, primary);
if !snapshot
.targets()
.contains(&Target::ScenarioObject(primary))
|| snapshot
.targets()
.iter()
.any(|target| !visible.contains(target))
{
return Err("Every selected occurrence must be visible in this scenario view.".into());
}
let prepared = Prepared::new(project.clone(), context.scenario, snapshot.targets())?;
let parent = project.elements[&primary]
.parent
.expect("prepared occurrence parent");
let before = project.elements[&parent]
.steps()
.expect("prepared sequence")
.clone();
Ok(Self {
project,
context,
snapshot,
token,
primary,
parent,
before,
prepared,
preview: None,
#[cfg(test)]
proposal_builds: 0,
})
}
pub(crate) fn context(&self) -> Context {
self.context
}
pub(crate) fn token(&self) -> Id {
self.token
}
pub(crate) fn snapshot(&self) -> &Snapshot {
&self.snapshot
}
pub(crate) fn parent(&self) -> Id {
self.parent
}
pub(crate) fn before(&self) -> &[Id] {
&self.before
}
pub(crate) fn current_preview(&self) -> Option<&Preview> {
self.preview.as_ref()
}
#[cfg(test)]
pub(crate) fn current_preview_mut(&mut self) -> Option<&mut Preview> {
self.preview.as_mut()
}
pub(crate) fn matches(
&self,
current: &Project,
context: Context,
snapshot: &Snapshot,
token: Id,
) -> bool {
current == self.project.as_ref()
&& context == self.context
&& snapshot == &self.snapshot
&& token == self.token
}
pub(crate) fn preview(&mut self, slot: Slot) -> &Preview {
if self
.preview
.as_ref()
.is_none_or(|preview| preview.slot != slot)
{
#[cfg(test)]
{
self.proposal_builds += 1;
}
let result = self.prepared.plan(slot).and_then(|plan| match plan {
Plan::Noop => Ok((None, self.project.clone())),
Plan::Change(command) => {
let candidate =
execute_batch(&self.project, std::slice::from_ref(command.as_ref()))
.map_err(|error| error.to_string())?;
Ok((Some(command), Arc::new(candidate)))
}
});
let preview = match result {
Ok((command, candidate)) => {
let shown = presentation::present(
&candidate,
self.context.representation,
self.primary,
);
let visible = shown
.scene()
.map_or_else(Vec::new, |scene| scene.selection_targets());
let selection_visible = self
.snapshot
.targets()
.iter()
.all(|target| visible.contains(target));
Preview {
slot,
before: self.before.clone(),
after: candidate.elements[&self.parent]
.steps()
.expect("validated sequence")
.clone(),
outcome: if command.is_some() {
Outcome::Change
} else {
Outcome::Noop
},
presentation: Some(shown),
selection_visible,
command,
}
}
Err(reason) => Preview {
slot,
before: self.before.clone(),
after: self.before.clone(),
outcome: Outcome::Refused(reason),
presentation: None,
selection_visible: false,
command: None,
},
};
self.preview = Some(preview);
}
self.preview.as_ref().expect("explicit destination preview")
}
/// Consume once; never execute a Document transaction or alter selection.
pub(crate) fn finish(
self,
current: &Project,
context: Context,
snapshot: &Snapshot,
token: Id,
) -> Result<Completion, String> {
if !self.matches(current, context, snapshot, token) {
return Err("The reorder input changed; choose the operation again.".into());
}
let preview = self
.preview
.ok_or("Choose an explicit destination before applying reorder.")?;
if let Outcome::Refused(reason) = preview.outcome {
return Err(reason);
}
let presentation_status = preview
.presentation
.expect("valid plan presentation")
.status()
.clone();
Ok(Completion {
command: preview.command,
snapshot: self.snapshot,
representation: self.context.representation,
presentation_status,
})
}
}
#[cfg(test)]
mod tests;
@@ -1,354 +0,0 @@
use super::*;
use crate::{
behavior::{
scenario::{Representation, direct_tests},
topology::occurrence,
},
diagram_selection::Selection,
};
use oa_core::{Diagram, DiagramLayout, Layer, OccurrenceBody, ProjectionKind, ScenarioLayout};
fn row(project: &mut Project, parent: Id, name: &str) -> Id {
let element = occurrence(
project,
parent,
name,
OccurrenceBody::Action {
action: None,
lifeline: None,
},
);
let id = element.id;
project.elements.insert(id, element);
project
.elements
.get_mut(&parent)
.unwrap()
.steps_mut()
.unwrap()
.push(id);
id
}
fn snapshot(context: Context, ids: &[Id]) -> Snapshot {
let mut selection = Selection::new(selection::context(context).scope);
for id in ids {
selection.add(Target::ScenarioObject(*id), |_| true);
}
selection.snapshot(selection::context(context)).unwrap()
}
fn session(project: &Project, context: Context, ids: &[Id]) -> Session {
Session::new(
Arc::new(project.clone()),
context,
snapshot(context, ids),
Id::new_v4(),
)
.unwrap()
}
#[test]
fn explicit_destination_cache_and_exact_whole_model_saved_presentation() {
for mode in 0..3 {
let f = direct_tests::fixture();
let mut project = f.project;
let x = row(&mut project, f.owner, "X");
let y = row(&mut project, f.owner, "Y");
let mut context = direct_tests::context(f.owner);
for name in ["A", "B"] {
let id = Id::new_v4();
project.diagrams.insert(
id,
Diagram {
id,
alias: name.into(),
name: name.into(),
description: "Keep this view".into(),
layer: Layer::Oa,
projection: ProjectionKind::Scenario { scenario: f.owner },
nodes: Default::default(),
routes: vec![],
view_options: Default::default(),
layout: Some(DiagramLayout::Scenario(ScenarioLayout {
lanes: vec![oa_core::ScenarioLanePosition {
lifeline: f.sender,
x: if name == "A" { 180. } else { 220. },
}],
messages: [(f.call, 600.), (f.reply, 800.)]
.into_iter()
.map(|(occurrence, send_y)| oa_core::ScenarioMessagePosition {
address: oa_core::OccurrenceEndpoint {
references: vec![],
occurrence,
},
send_y,
})
.collect(),
})),
},
);
if mode != 0 && name == "A" {
context.representation = Representation::Saved { diagram: id };
if mode == 2 {
project.diagrams.get_mut(&id).unwrap().layout =
Some(DiagramLayout::Scenario(ScenarioLayout {
lanes: vec![],
messages: [(f.call, 600.), (f.reply, 200.)]
.into_iter()
.map(|(occurrence, send_y)| oa_core::ScenarioMessagePosition {
address: oa_core::OccurrenceEndpoint {
references: vec![],
occurrence,
},
send_y,
})
.collect(),
}));
}
}
}
let mut operation = session(&project, context, &[f.reply, f.call]);
assert_eq!(operation.parent(), f.owner);
assert_eq!(operation.before(), &[f.call, f.reply, x, y]);
assert_eq!(operation.context(), context);
let original_snapshot = operation.snapshot().clone();
let token = operation.token();
let slot = Slot {
parent: f.owner,
before: None,
};
let preview = operation.preview(slot);
assert_eq!(preview.before, [f.call, f.reply, x, y]);
assert_eq!(preview.after, [x, y, f.call, f.reply]);
assert_eq!(preview.outcome, Outcome::Change);
assert_eq!(
preview.presentation.as_ref().unwrap().requested(),
context.representation
);
assert_eq!(operation.proposal_builds, 1);
operation.preview(slot);
assert_eq!(operation.proposal_builds, 1);
operation.preview(Slot {
parent: f.owner,
before: Some(f.call),
});
assert_eq!(operation.proposal_builds, 2);
operation.preview(slot);
assert_eq!(operation.proposal_builds, 3);
assert_eq!(operation.preview.as_ref().unwrap().slot, slot);
let completion = operation
.finish(&project, context, &original_snapshot, token)
.unwrap();
assert_eq!(completion.snapshot, original_snapshot);
assert_eq!(completion.representation, context.representation);
if mode == 2 {
assert!(matches!(
completion.presentation_status,
presentation::PresentationStatus::Fallback { .. }
));
} else {
assert!(matches!(
completion.presentation_status,
presentation::PresentationStatus::Ready
));
}
let mut expected = project.clone();
*expected
.elements
.get_mut(&f.owner)
.unwrap()
.steps_mut()
.unwrap() = vec![x, y, f.call, f.reply];
assert_eq!(
execute_batch(&project, &[*completion.command.unwrap()]).unwrap(),
expected
);
assert_eq!(expected.diagrams, project.diagrams);
}
}
#[test]
fn no_destination_is_not_noop_and_selected_anchor_is_explicit_noop() {
let f = direct_tests::fixture();
let context = direct_tests::context(f.owner);
let mut operation = session(&f.project, context, &[f.call, f.reply]);
let captured = operation.snapshot().clone();
let token = operation.token();
assert!(
operation
.finish(&f.project, context, &captured, token)
.is_err()
);
operation = session(&f.project, context, &[f.call, f.reply]);
let token = operation.token();
assert_eq!(
operation
.preview(Slot {
parent: f.owner,
before: Some(f.reply)
})
.outcome,
Outcome::Noop
);
assert!(
operation
.finish(&f.project, context, &captured, token)
.unwrap()
.command
.is_none()
);
}
#[test]
fn temporal_candidate_refuses_without_changing_baseline_or_selection() {
let f = direct_tests::fixture();
let context = direct_tests::context(f.owner);
let mut project = f.project;
let later = row(&mut project, f.owner, "Later action");
let mut operation = session(&project, context, &[f.reply]);
let captured = operation.snapshot().clone();
let token = operation.token();
// A previously valid proposal must not survive a new refused destination.
let valid = operation.preview(Slot {
parent: f.owner,
before: None,
});
assert_eq!(valid.outcome, Outcome::Change);
assert_eq!(valid.after, [f.call, later, f.reply]);
let preview = operation.preview(Slot {
parent: f.owner,
before: Some(f.call),
});
assert!(matches!(&preview.outcome, Outcome::Refused(reason) if reason.contains("reply_order")));
assert!(preview.presentation.is_none());
assert_eq!(preview.before, preview.after);
assert!(
operation
.finish(&project, context, &captured, token)
.is_err()
);
}
#[test]
fn mixed_cross_parent_and_unrendered_sets_never_reduce_to_primary() {
let f = direct_tests::fixture();
let context = direct_tests::context(f.owner);
let mut project = f.project;
let other = project
.add_element(oa_core::Kind::Scenario, Layer::Oa, "Other", None)
.unwrap();
let foreign = row(&mut project, other, "Foreign");
let fragment = occurrence(
&project,
f.owner,
"Fragment",
OccurrenceBody::Fragment {
operator: oa_core::FragmentOperator::Alternative,
branches: vec![],
},
);
let fragment_id = fragment.id;
project.elements.insert(fragment_id, fragment);
project
.elements
.get_mut(&f.owner)
.unwrap()
.steps_mut()
.unwrap()
.push(fragment_id);
let branch = occurrence(
&project,
fragment_id,
"Branch",
OccurrenceBody::Branch {
guard: "ready".into(),
occurrences: vec![],
},
);
let branch_id = branch.id;
project.elements.insert(branch_id, branch);
if let Some(oa_core::SharedBody::Occurrence {
body: OccurrenceBody::Fragment { branches, .. },
..
}) = project.elements.get_mut(&fragment_id).unwrap().shared_mut()
{
branches.push(branch_id);
}
let nested = row(&mut project, branch_id, "Nested");
assert!(
selection::visible(&project, context, f.call).contains(&Target::ScenarioObject(nested))
);
for ids in [
vec![f.sender, f.call],
vec![nested, f.call],
vec![foreign, f.call],
vec![Id::new_v4(), f.call],
] {
assert!(
Session::new(
Arc::new(project.clone()),
context,
snapshot(context, &ids),
Id::new_v4()
)
.is_err()
);
}
}
#[test]
fn stale_finish_checks_every_immutable_input_and_constructor_rejects_nil_lifetimes() {
let f = direct_tests::fixture();
let context = direct_tests::context(f.owner);
for altered in 0..8 {
let mut operation = session(&f.project, context, &[f.call, f.reply]);
let mut token = operation.token();
let mut captured = operation.snapshot().clone();
let mut current = f.project.clone();
let mut live = context;
operation.preview(Slot {
parent: f.owner,
before: None,
});
match altered {
0 => {
current.elements.get_mut(&f.call).unwrap().name.push('!');
}
1 => live.epoch += 1,
2 => captured = snapshot(context, &[f.reply, f.call]),
3 => token = Id::new_v4(),
4 => live.visit = Id::new_v4(),
5 => live.document = Id::new_v4(),
6 => live.revision += 1,
_ => {
let mut selection = Selection::new(selection::context(context).scope);
selection.add(Target::ScenarioObject(f.call), |_| true);
selection.add(Target::ScenarioObject(f.reply), |_| true);
selection.set_primary(&Target::ScenarioObject(f.call));
selection.set_primary(&Target::ScenarioObject(f.reply));
captured = selection.snapshot(selection::context(context)).unwrap();
assert_eq!(captured.targets(), operation.snapshot().targets());
assert_eq!(captured.primary(), operation.snapshot().primary());
assert_ne!(captured.generation(), operation.snapshot().generation());
}
}
assert!(operation.finish(&current, live, &captured, token).is_err());
}
for altered in 0..3 {
let mut live = context;
let mut token = Id::new_v4();
match altered {
0 => live.document = Id::nil(),
1 => live.visit = Id::nil(),
_ => token = Id::nil(),
}
assert!(
Session::new(
Arc::new(f.project.clone()),
live,
snapshot(live, &[f.call]),
token
)
.is_err()
);
}
}
@@ -1,234 +0,0 @@
//! Candidate-input experiment, not an accepted new interaction policy.
//! Exercise production presentation, nearest-Y picking, and exact semantic slots.
use super::{Representation, Slot, group_reorder, presentation};
use crate::{behavior::Scene, diagram_usage::Target};
use iced::Point;
use oa_core::{
Diagram, DiagramLayout, Id, OccurrenceEndpoint, Project, ProjectionKind, ScenarioLayout,
ScenarioMessagePosition, execute_batch, language, validation,
};
use std::sync::Arc;
struct Fixture {
project: Project,
owner: Id,
diagram: Id,
ids: [Id; 4],
}
fn fixture(ys: [f32; 4]) -> Fixture {
let mut project = language::parse(SOURCE).unwrap();
let named = |name: &str| {
project
.identities()
.into_iter()
.find(|(_, alias)| *alias == name)
.unwrap()
.0
};
let owner = named("conversation");
let ids = [named("a"), named("b"), named("c"), named("d")];
let diagram = Id::new_v4();
project.diagrams.insert(
diagram,
Diagram {
id: diagram,
alias: "saved".into(),
name: "Authored coordinates".into(),
description: String::new(),
layer: project.elements[&owner].layer(),
projection: ProjectionKind::Scenario { scenario: owner },
view_options: Default::default(),
nodes: Default::default(),
routes: vec![],
layout: Some(DiagramLayout::Scenario(ScenarioLayout {
lanes: vec![],
messages: ids
.into_iter()
.zip(ys)
.map(|(occurrence, send_y)| ScenarioMessagePosition {
address: OccurrenceEndpoint {
occurrence,
references: vec![],
},
send_y,
})
.collect(),
})),
},
);
assert!(
validation::validate(&project)
.iter()
.all(|issue| issue.severity != validation::Severity::Error)
);
Fixture {
project,
owner,
diagram,
ids,
}
}
fn shown(f: &Fixture) -> presentation::ScenarioPresentation {
let shown = presentation::present(
&f.project,
Representation::Saved { diagram: f.diagram },
f.ids[0],
);
assert_eq!(shown.status(), &presentation::PresentationStatus::Ready);
assert_eq!(shown.requested(), shown.geometry_representation().unwrap());
shown
}
fn before(f: &Fixture, id: Id) -> Slot {
Slot {
parent: f.owner,
before: Some(id),
}
}
fn y(scene: &Scene, slot: Slot) -> f32 {
scene.slots.iter().find(|item| item.slot == slot).unwrap().y
}
fn picked(scene: &Scene, f: &Fixture, world_y: f32) -> Slot {
scene
.slot_at(Point::new(100., world_y), Some(f.owner))
.unwrap()
.slot
}
fn exact_change(f: &Fixture, selected: &[Id], slot: Slot, order: &[Id]) {
let prepared = group_reorder::Prepared::new(
Arc::new(f.project.clone()),
f.owner,
&selected
.iter()
.copied()
.map(Target::ScenarioObject)
.collect::<Vec<_>>(),
)
.unwrap();
let group_reorder::Plan::Change(command) = prepared.plan(slot).unwrap() else {
panic!("expected an actual semantic change")
};
let actual = execute_batch(&f.project, std::slice::from_ref(command.as_ref())).unwrap();
let mut expected = f.project.clone();
*expected
.elements
.get_mut(&f.owner)
.unwrap()
.steps_mut()
.unwrap() = order.to_vec();
// Literal full-model oracle: every authored position and other field survives.
assert_eq!(actual, expected);
assert_eq!(
language::parse(&language::format(&actual)).unwrap(),
expected
);
assert_eq!(
presentation::present(
&actual,
Representation::Saved { diagram: f.diagram },
selected[0]
)
.status(),
&presentation::PresentationStatus::Ready
);
}
#[test]
fn crossed_saved_slots_are_reachable_but_screen_order_is_not_semantic_order() {
let f = fixture([200., 500., 300., 400.]);
let [a, b, c, d] = f.ids;
let shown = shown(&f);
let scene = shown.scene().unwrap();
assert_eq!(
f.ids.map(|id| y(scene, before(&f, id))),
[158., 458., 258., 358.]
);
let mut spatial = f.ids.to_vec();
spatial.sort_by(|a, b| y(scene, before(&f, *a)).total_cmp(&y(scene, before(&f, *b))));
assert_eq!(spatial, [a, c, d, b]);
for id in f.ids {
assert_eq!(picked(scene, &f, y(scene, before(&f, id))), before(&f, id));
}
exact_change(&f, &[c, a], before(&f, d), &[b, a, c, d]);
}
#[test]
fn equal_saved_slots_hide_one_destination_while_exact_chooser_can_select_it() {
let f = fixture([200., 300., 300., 500.]);
let [a, b, c, d] = f.ids;
let shown = shown(&f);
let scene = shown.scene().unwrap();
assert_eq!(y(scene, before(&f, b)), 258.);
assert_eq!(y(scene, before(&f, c)), 258.);
assert!(
scene
.slots
.iter()
.position(|item| item.slot == before(&f, b))
< scene
.slots
.iter()
.position(|item| item.slot == before(&f, c))
);
// The two distances are identical for every finite Y; min_by keeps the first
// tie. The sample exercises that production behavior, not an exhaustive proof.
for quarter_pixel in -4000..=12000 {
assert_ne!(picked(scene, &f, quarter_pixel as f32 / 4.), before(&f, c));
}
assert_eq!(picked(scene, &f, 258.), before(&f, b));
exact_change(&f, &[d, a], before(&f, c), &[b, a, d, c]);
exact_change(&f, &[d, a], picked(scene, &f, 258.), &[a, d, b, c]);
}
#[test]
fn close_saved_slots_have_subpixel_hit_regions_without_becoming_unreachable_continuously() {
let f = fixture([200., 300., 300.125, 300.25]);
let [_, b, c, d] = f.ids;
let shown = shown(&f);
let scene = shown.scene().unwrap();
assert_eq!(y(scene, before(&f, b)), 258.);
assert_eq!(y(scene, before(&f, c)), 258.125);
assert_eq!(y(scene, before(&f, d)), 258.25);
assert_eq!(picked(scene, &f, 258.125), before(&f, c));
// Fixed scale=1/pan=0: C's open Voronoi interval is only
// (258.0625, 258.1875). Integer-pixel samples miss it. This does not assert
// that all platforms quantize pointer input to integer coordinates.
for screen_y in 0..=600 {
assert_ne!(picked(scene, &f, screen_y as f32), before(&f, c));
}
}
#[test]
fn offscreen_saved_slot_needs_camera_change_or_nonspatial_destination_input() {
let f = fixture([200., 300., 400., 2000.]);
let [a, b, c, d] = f.ids;
let shown = shown(&f);
let scene = shown.scene().unwrap();
assert_eq!(y(scene, before(&f, d)), 1958.);
// Fixed scale=1/pan=0 and a 600px-high canvas. The C/D midpoint is
// 1158, beyond the whole viewport, so D cannot win within that viewport.
for quarter_pixel in 0..=2400 {
assert_ne!(picked(scene, &f, quarter_pixel as f32 / 4.), before(&f, d));
}
assert_eq!(picked(scene, &f, 1958.), before(&f, d));
exact_change(&f, &[c, a], before(&f, d), &[b, a, c, d]);
}
const SOURCE: &str = r#"arcadia 7;
project slot_probe "Slot probe" id "71000000-0000-4000-8000-000000000001" {
oa_activity source "Source" id "71000000-0000-4000-8000-000000000002" {}
oa_activity target "Target" id "71000000-0000-4000-8000-000000000003" {}
oa_scenario conversation "Conversation" id "71000000-0000-4000-8000-000000000004" { viewpoint OperationalActivity; steps [@a, @b, @c, @d]; }
oa_lifeline left "Left" id "71000000-0000-4000-8000-000000000005" { parent @conversation; represented @source; }
oa_lifeline right "Right" id "71000000-0000-4000-8000-000000000006" { parent @conversation; represented @target; }
oa_occurrence a "A" id "71000000-0000-4000-8000-000000000007" { parent @conversation; body { Exchange { exchange none; sender @left; receiver @right; message_kind Asynchronous; }; }; }
oa_occurrence b "B" id "71000000-0000-4000-8000-000000000008" { parent @conversation; body { Exchange { exchange none; sender @left; receiver @right; message_kind Asynchronous; }; }; }
oa_occurrence c "C" id "71000000-0000-4000-8000-000000000009" { parent @conversation; body { Exchange { exchange none; sender @left; receiver @right; message_kind Asynchronous; }; }; }
oa_occurrence d "D" id "71000000-0000-4000-8000-00000000000a" { parent @conversation; body { Exchange { exchange none; sender @left; receiver @right; message_kind Asynchronous; }; }; }
}
"#;
@@ -1,323 +0,0 @@
//! Identity-based destination choice, independent of saved screen coordinates.
use super::*;
use crate::app::scenario_reorder::{Event, Intent};
use crate::behavior::scenario_layout::presentation::PresentationStatus;
use iced::widget::{column, opaque, scrollable};
const PAGE_SIZE: usize = 5;
pub(crate) fn query_id(token: Id) -> iced::widget::Id {
iced::widget::Id::from(format!("scenario-reorder-query:{token}"))
}
pub(crate) fn focus_query(token: Id) -> iced::Task<Message> {
use iced::advanced::widget::{Operation, operation};
struct Find {
target: iced::widget::Id,
present: bool,
}
impl Operation for Find {
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn Operation)) {
visit(self);
}
fn focusable(
&mut self,
id: Option<&iced::widget::Id>,
_: iced::Rectangle,
_: &mut dyn operation::Focusable,
) {
self.present |= id == Some(&self.target);
}
fn finish(&self) -> operation::Outcome<()> {
if self.present {
operation::Outcome::Chain(Box::new(operation::focusable::focus(
self.target.clone(),
)))
} else {
operation::Outcome::None
}
}
}
// Generic focus would blur every current widget if the old token vanished.
// Discover the exact target without changing focus before chaining it.
iced::advanced::widget::operate(Find {
target: query_id(token),
present: false,
})
.discard()
}
pub(crate) fn apply_message(app: &App) -> Option<Message> {
if !app.scenario_reorder_enabled() {
return None;
}
let session = app.scenario_reorder.session.as_ref()?;
let preview = session.current_preview()?;
(preview.selection_visible
&& !matches!(preview.outcome, reorder_session::Outcome::Refused(_))
&& matches!(
preview.presentation.as_ref()?.status(),
PresentationStatus::Ready | PresentationStatus::Fallback { .. }
))
.then(|| message(session, Intent::Finish(preview.slot)))
}
pub(crate) fn message(session: &reorder_session::Session, intent: Intent) -> Message {
Message::ScenarioReorder(Box::new(Event {
context: session.context(),
snapshot: session.snapshot().clone(),
token: session.token(),
intent,
}))
}
pub(crate) fn begin(app: &App) -> Option<Message> {
(app.scenario_reorder_enabled() && app.scenario_reorder.session.is_none()).then_some(())?;
let snapshot = app.multiselect_snapshot()?;
if snapshot.targets().is_empty() {
return None;
}
Some(Message::ScenarioReorder(Box::new(Event {
context: app.scenario_context()?,
snapshot,
token: Id::new_v4(),
intent: Intent::Begin,
})))
}
fn label(p: &Project, id: Id) -> String {
p.elements.get(&id).map_or_else(
|| id.to_string(),
|e| format!("{} (@{})", short(&e.name, 38), e.alias),
)
}
fn destination(p: &Project, before: &[Id], slot: Slot) -> String {
slot.before.map_or_else(
|| "End of sequence".into(),
|id| {
let position = before
.iter()
.position(|item| *item == id)
.map_or(0, |i| i + 1);
format!("Before {position}. {}", label(p, id))
},
)
}
fn order(p: &Project, ids: &[Id], snapshot: &crate::diagram_selection::Snapshot) -> String {
// A bounded neighborhood keeps the moved block and its immediate neighbors
// visible. Ellipses explicitly mark omitted, unchanged sequence members.
let selected = |id: &Id| {
snapshot
.targets()
.contains(&crate::diagram_usage::Target::ScenarioObject(*id))
};
let mut parts = Vec::new();
let mut omitted = false;
for (index, id) in ids.iter().enumerate() {
let near = selected(id)
|| index.checked_sub(1).is_some_and(|i| selected(&ids[i]))
|| ids.get(index + 1).is_some_and(selected);
if !near {
omitted = true;
continue;
}
if omitted {
parts.push("…".into());
omitted = false;
}
parts.push(format!(
"{}{}. {}",
if selected(id) { "• " } else { "" },
index + 1,
label(p, *id)
));
}
if omitted {
parts.push("…".into());
}
parts.join("\n")
}
pub(super) fn panel(app: &App, viewport: Option<Size>) -> Option<Element<'_, Message>> {
let session = app.scenario_reorder.session.as_ref()?;
let p = app.project();
let state = &app.scenario_reorder;
let query = state.query.to_lowercase();
let slots = session
.before()
.iter()
.copied()
.map(Some)
.chain(std::iter::once(None))
.enumerate()
.map(|(index, before)| {
let slot = Slot {
parent: session.parent(),
before,
};
let title = before.map_or_else(
|| "End of sequence".into(),
|id| format!("Before {}. {}", index + 1, label(p, id)),
);
(slot, title)
})
.filter(|(slot, title)| {
title.to_lowercase().contains(&query)
|| slot
.before
.and_then(|id| p.elements.get(&id))
.is_some_and(|e| {
e.name.to_lowercase().contains(&query)
|| e.alias.to_lowercase().contains(&query)
})
})
.collect::<Vec<_>>();
let pages = slots.len().div_ceil(PAGE_SIZE).max(1);
let page = state.page.min(pages - 1);
let mut choices = column![].spacing(4).width(Fill);
for (slot, title) in slots.iter().skip(page * PAGE_SIZE).take(PAGE_SIZE) {
choices = choices.push(
button(
text(title.clone())
.size(12)
.width(Fill)
.wrapping(iced::widget::text::Wrapping::WordOrGlyph),
)
.width(Fill)
.on_press(message(session, Intent::Choose(*slot))),
);
}
if slots.is_empty() {
choices = choices
.push(text("No matching destination. Try its name, alias or position.").size(12));
}
let mut body = column![
text(format!(
"Reorder {} selected",
session.snapshot().targets().len()
))
.size(17),
text(format!("Sequence: {}", label(p, session.parent())))
.size(12)
.width(Fill)
.wrapping(iced::widget::text::Wrapping::WordOrGlyph),
text("Changes execution order; saved positions stay as authored.").size(12),
text_input("Find destination by name, alias or position", &state.query)
.id(query_id(session.token()))
.on_input(move |query| message(session, Intent::Query(query)))
.on_submit_maybe(
(slots.len() == 1).then(|| message(session, Intent::Choose(slots[0].0)))
)
.size(13),
choices,
row![
button("Previous")
.on_press_maybe((page > 0).then(|| message(session, Intent::Page(page - 1)))),
text(format!("{} / {pages}", page + 1)).size(12),
button("Next").on_press_maybe(
(page + 1 < pages).then(|| message(session, Intent::Page(page + 1)))
)
]
.spacing(8),
]
.spacing(8)
.width(Fill);
let mut apply = None;
let mut footer =
column![text("Enter previews a unique match. Ctrl+Enter applies; Esc cancels.").size(12)]
.spacing(6);
if let Some(preview) = session.current_preview() {
footer = footer.push(
text(format!(
"Destination: {}",
destination(p, session.before(), preview.slot)
))
.size(13)
.width(Fill)
.wrapping(iced::widget::text::Wrapping::WordOrGlyph),
);
let comparison = row![
container(
column![
text("Before").size(13),
text(order(p, &preview.before, session.snapshot()))
.size(12)
.width(Fill)
.wrapping(iced::widget::text::Wrapping::WordOrGlyph)
]
.width(Fill)
)
.width(Fill)
.id("scenario-reorder-before"),
container(
column![
text("After • selected").size(13),
text(order(p, &preview.after, session.snapshot()))
.size(12)
.width(Fill)
.wrapping(iced::widget::text::Wrapping::WordOrGlyph)
]
.width(Fill)
)
.width(Fill)
.id("scenario-reorder-after")
]
.spacing(10)
.width(Fill);
body = body.push(comparison);
let feedback = match &preview.outcome {
reorder_session::Outcome::Refused(reason) => reason.clone(),
_ if !preview.selection_visible => {
"This destination would hide a selected occurrence; choose another destination."
.into()
}
_ => match preview.presentation.as_ref().map(|shown| shown.status()) {
Some(PresentationStatus::Unavailable { .. }) | None => {
"This order cannot be displayed; choose another destination.".into()
}
Some(status) => {
apply = Some(message(session, Intent::Finish(preview.slot)));
if matches!(status, PresentationStatus::Fallback { .. }) {
"Apply will display an automatic arrangement. Your requested saved view and authored positions are retained; they are not repaired.".into()
} else if matches!(preview.outcome, reorder_session::Outcome::Noop) {
"This destination keeps the current order; Apply will close without an edit.".into()
} else {
"The selected occurrences move together in their existing order. One Undo restores the sequence.".into()
}
}
},
};
footer = footer.push(text(feedback).size(12));
} else {
body = body.push(text("Choose a destination to preview the new sequence.").size(12));
}
let height = viewport.map_or(500., |size| (size.height - 32.).max(100.));
let card = column![
scrollable(body)
.height(Fill)
.width(Fill)
.spacing(8)
.id("scenario-reorder-body"),
footer,
row![
button("Cancel (Esc)").on_press(message(session, Intent::Cancel)),
button("Apply order (Ctrl+Enter)").on_press_maybe(apply)
]
.spacing(8)
]
.spacing(10);
Some(
container(opaque(
container(card)
.padding(12)
.width(480)
.height(height.min(630.))
.style(style::card),
))
.align_right(Fill)
.padding(8)
.into(),
)
}
@@ -1,54 +0,0 @@
//! Captured exact Scenario selection input; membership grants no movement permission.
use super::{Context, presentation};
use crate::{
app::multiselect::Choice,
diagram_selection,
diagram_usage::{Surface, Target},
};
use oa_core::{Id, Project};
#[derive(Debug, Clone)]
pub(crate) struct Event {
pub context: Context,
pub snapshot: diagram_selection::Snapshot,
pub intent: Intent,
}
#[derive(Debug, Clone)]
pub(crate) enum Intent {
Choose(Target, Choice),
Clear,
Collapse(Target),
Refuse(String),
}
pub(crate) fn context(context: Context) -> diagram_selection::Context {
diagram_selection::Context {
scope: diagram_selection::Scope {
document: context.document,
surface: match context.representation {
super::Representation::Derived { owner } => Surface::Scenario(owner),
super::Representation::Saved { diagram } => Surface::Saved(diagram),
},
},
revision: context.revision,
epoch: context.epoch,
}
}
pub(crate) fn visible(project: &Project, context: Context, selected: Id) -> Vec<Target> {
let shown = presentation::present(project, context.representation, selected);
shown
.scene()
.map_or_else(Vec::new, |scene| scene.selection_targets())
}
pub(crate) fn message(
context: Context,
snapshot: &diagram_selection::Snapshot,
intent: Intent,
) -> crate::app::Message {
crate::app::Message::ScenarioCanvas(super::CanvasEvent::Selection(Box::new(Event {
context,
snapshot: snapshot.clone(),
intent,
})))
}
#[cfg(test)]
mod tests;
@@ -1,268 +0,0 @@
use super::*;
use crate::behavior::{
Scene, SequenceInput,
scenario::{self, CanvasEvent, CanvasTool},
};
use crate::diagram_selection::Selection;
use iced::{
Point, Rectangle, Vector, mouse,
widget::canvas::{self, Program},
};
fn fixture(group: bool) -> (scenario::direct_tests::Fixture, Scene, Point) {
let f = scenario::direct_tests::fixture();
let context = scenario::direct_tests::context(f.owner);
let mut scene = Scene::sequence(&f.project, f.owner, f.reply);
scene.interaction = Some((context, CanvasTool::Select, true));
let mut selection = Selection::new(super::context(context).scope);
selection.add(Target::ScenarioObject(f.call), |_| true);
if group {
selection.add(Target::ScenarioObject(f.reply), |_| true);
}
scene.selection = Some((
context,
selection.snapshot(super::context(context)).unwrap(),
));
let line = scene.lines.iter().find(|line| line.id == f.call).unwrap();
let point = line.label_at + Vector::new(8., 8.);
assert_eq!(scene.hit(point), Some(f.call));
(f, scene, point)
}
fn send(
scene: &Scene,
state: &mut SequenceInput,
event: canvas::Event,
point: Option<Point>,
) -> Option<CanvasEvent> {
scene
.update(
state,
&event,
Rectangle::new(Point::ORIGIN, scene.size),
point.map_or(mouse::Cursor::Unavailable, mouse::Cursor::Available),
)
.and_then(|action| action.into_inner().0)
.and_then(|message| match message {
crate::app::Message::ScenarioCanvas(event) => Some(event),
_ => None,
})
}
fn mouse_event(event: mouse::Event) -> canvas::Event {
canvas::Event::Mouse(event)
}
#[test]
fn saved_group_body_press_emits_one_spacing_lifetime_without_collapsing_selection() {
use crate::behavior::scenario::spacing::{self, Event};
use crate::behavior::scenario_layout::{Representation, resolve};
let (doc, owner, diagram, _, first) = spacing::controller::tests::fixture();
let second = doc
.project()
.identities()
.into_iter()
.find(|(_, a)| *a == "second")
.unwrap()
.0;
let mut context = scenario::direct_tests::context(owner);
context.representation = Representation::Saved { diagram };
let mut scene = resolve(doc.project(), context.representation, second)
.unwrap()
.scene;
let mut selection = Selection::new(super::context(context).scope);
selection.add(Target::ScenarioObject(first), |_| true);
selection.add(Target::ScenarioObject(second), |_| true);
let snapshot = selection.snapshot(super::context(context)).unwrap();
let stamp = spacing::Stamp {
context,
camera_epoch: 9,
selection_generation: snapshot.generation(),
};
scene.selection = Some((context, snapshot.clone()));
scene.selected = second;
scene.interaction = Some((context, CanvasTool::Select, true));
scene.coordinate_input = true;
scene.spacing_stamp = Some(stamp);
let line = scene.lines.iter().find(|line| line.id == first).unwrap();
let point = Point::new((line.points[0].x + line.points[1].x) / 2., line.points[0].y);
assert_eq!(scene.hit(point), Some(first));
let mut input = SequenceInput::default();
let Some(CanvasEvent::Spacing(Event::Begin {
stamp: got,
token,
target,
..
})) = send(
&scene,
&mut input,
mouse_event(mouse::Event::ButtonPressed(mouse::Button::Left)),
Some(point),
)
else {
panic!("group spacing begins on first press")
};
assert_eq!(got, stamp);
assert_eq!(target, spacing::Target::Message(first));
assert!(input.group_press.is_none());
let moved = point + Vector::new(0., 24.);
assert!(
matches!(send(&scene,&mut input,mouse_event(mouse::Event::CursorMoved{position:moved}),Some(moved)),Some(CanvasEvent::Spacing(Event::Move{stamp:s,token:t,..})) if s==stamp && t==token)
);
assert!(
matches!(send(&scene,&mut input,mouse_event(mouse::Event::ButtonReleased(mouse::Button::Left)),Some(moved)),Some(CanvasEvent::Spacing(Event::Finish{stamp:s,token:t,..})) if s==stamp && t==token)
);
assert_eq!(scene.selection, Some((context, snapshot)));
assert_eq!(scene.selected, second);
assert!(input.press.is_none() && input.group_press.is_none());
}
#[test]
fn group_press_retains_snapshot_and_collapses_only_stationary_release_once() {
let (f, scene, point) = fixture(true);
let mut state = SequenceInput::default();
assert!(
send(
&scene,
&mut state,
mouse_event(mouse::Event::ButtonPressed(mouse::Button::Left)),
Some(point)
)
.is_none()
);
assert!(state.press.is_none());
let captured = state.group_press.as_ref().unwrap().snapshot.clone();
assert_eq!(captured.primary(), Some(&Target::ScenarioObject(f.reply)));
let Some(CanvasEvent::Selection(event)) = send(
&scene,
&mut state,
mouse_event(mouse::Event::ButtonReleased(mouse::Button::Left)),
Some(point),
) else {
panic!("captured collapse")
};
assert_eq!(event.snapshot, captured);
assert!(matches!(event.intent, Intent::Collapse(Target::ScenarioObject(id)) if id == f.call));
assert!(
send(
&scene,
&mut state,
mouse_event(mouse::Event::ButtonReleased(mouse::Button::Left)),
Some(point)
)
.is_none()
);
}
#[test]
fn crossed_threshold_return_to_origin_refuses_whole_set_and_outside_release_cancels() {
for outside in [false, true] {
let (_, scene, point) = fixture(true);
let mut state = SequenceInput::default();
send(
&scene,
&mut state,
mouse_event(mouse::Event::ButtonPressed(mouse::Button::Left)),
Some(point),
);
let moved = point + Vector::new(12., 0.);
send(
&scene,
&mut state,
mouse_event(mouse::Event::CursorMoved { position: moved }),
Some(moved),
);
send(
&scene,
&mut state,
mouse_event(mouse::Event::CursorMoved { position: point }),
Some(point),
);
let event = send(
&scene,
&mut state,
mouse_event(mouse::Event::ButtonReleased(mouse::Button::Left)),
(!outside).then_some(point),
);
if outside {
assert!(event.is_none());
} else {
let Some(CanvasEvent::Selection(event)) = event else {
panic!("whole-set refusal")
};
assert!(matches!(event.intent, Intent::Refuse(_)));
assert_eq!(event.snapshot.targets().len(), 2);
}
assert!(state.group_press.is_none() && state.press.is_none());
}
}
#[test]
fn passive_shift_survives_context_refresh_and_window_loss_clears_it() {
let (f, mut scene, point) = fixture(false);
let mut state = SequenceInput::default();
assert!(
send(
&scene,
&mut state,
canvas::Event::Keyboard(iced::keyboard::Event::ModifiersChanged(
iced::keyboard::Modifiers::SHIFT
)),
Some(point)
)
.is_none()
);
let Some(CanvasEvent::Selection(event)) = send(
&scene,
&mut state,
mouse_event(mouse::Event::ButtonPressed(mouse::Button::Left)),
Some(point),
) else {
panic!("toggle")
};
assert!(
matches!(event.intent, Intent::Choose(Target::ScenarioObject(id), Choice::Toggle) if id == f.call)
);
assert!(state.press.is_none());
scene.interaction.as_mut().unwrap().0.epoch += 1;
send(
&scene,
&mut state,
canvas::Event::Window(iced::window::Event::Unfocused),
None,
);
assert!(state.modifiers.is_empty());
}
#[test]
fn stale_selection_and_fallback_release_do_not_move_primary() {
let (_, mut scene, point) = fixture(true);
scene.coordinate_input = false;
let mut state = SequenceInput::default();
send(
&scene,
&mut state,
mouse_event(mouse::Event::ButtonPressed(mouse::Button::Left)),
Some(point),
);
let Some(CanvasEvent::Selection(event)) = send(
&scene,
&mut state,
mouse_event(mouse::Event::ButtonReleased(mouse::Button::Left)),
Some(point),
) else {
panic!("fallback stationary selection remains usable")
};
assert!(matches!(event.intent, Intent::Collapse(_)));
send(
&scene,
&mut state,
mouse_event(mouse::Event::ButtonPressed(mouse::Button::Left)),
Some(point),
);
scene.selection.as_mut().unwrap().0.visit = Id::new_v4();
assert!(
send(
&scene,
&mut state,
mouse_event(mouse::Event::ButtonReleased(mouse::Button::Left)),
Some(point)
)
.is_none()
);
assert!(state.group_press.is_none());
}
@@ -5,17 +5,13 @@
use crate::behavior::scenario_layout::{
GeometryIssue, Representation, ResolvedScenarioPlan, presentation::checked_owner, resolve,
};
use crate::diagram_usage::Target as SelectionTarget;
use oa_core::{
Command, Id, Kind, OccurrenceBody, OccurrenceEndpoint, Project, ScenarioLanePosition,
ScenarioMessagePosition, SharedBody, execute_batch,
};
use std::collections::BTreeSet;
pub(crate) mod controller;
#[cfg(test)]
mod group_tests;
#[cfg(test)]
mod tests;
pub(crate) use controller::{Event, Stamp};
@@ -34,7 +30,6 @@ impl Target {
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Refusal {
InvalidSelection(&'static str),
InvalidSavedView(Id),
TargetNotRendered(Target),
InvalidDelta,
@@ -50,7 +45,6 @@ pub(super) struct Drag {
diagram: Id,
target: Target,
origin: f32,
other_origins: Vec<(Target, f32)>,
original_geometry: ResolvedScenarioPlan,
}
@@ -64,135 +58,17 @@ pub(super) struct Preview {
/// Capture an accepted, actually drawn target. A Saved view displaying automatic
/// fallback is not an editable original, even if the target appears in fallback.
pub(super) fn capture(project: &Project, diagram: Id, target: Target) -> Result<Drag, Refusal> {
capture_targets(project, diagram, target, &[target])
}
/// The exact selected set is one operation, never an eligible subset. Selection
/// order and the pressed member do not determine persistence order.
pub(super) fn capture_group(
project: &Project,
diagram: Id,
pressed: Target,
targets: &[SelectionTarget],
) -> Result<Drag, Refusal> {
if targets.is_empty() {
return Err(Refusal::InvalidSelection(
"Select at least one occurrence to move.",
));
}
let mut seen = BTreeSet::new();
let mut members = Vec::with_capacity(targets.len());
for selected in targets {
let SelectionTarget::ScenarioObject(id) = selected else {
return Err(Refusal::InvalidSelection(
"Move only local Scenario lifelines or messages.",
));
};
if !seen.insert(*id) {
return Err(Refusal::InvalidSelection(
"The selection contains a duplicate occurrence.",
));
}
let target = match pressed {
Target::Lane(_) => Target::Lane(*id),
Target::Message(_) => Target::Message(*id),
};
let Some(element) = project.elements.get(id) else {
return Err(Refusal::TargetNotRendered(target));
};
let same_role = match target {
Target::Lane(_) => element.kind() == Kind::Lifeline,
Target::Message(_) => matches!(
element.shared(),
Some(SharedBody::Occurrence {
body: OccurrenceBody::Exchange { .. },
..
})
),
};
if !same_role {
return Err(Refusal::InvalidSelection(
"Move a group of lifelines or a group of messages; mixed and other occurrence types are not supported.",
));
}
members.push(target);
}
if !members.contains(&pressed) {
return Err(Refusal::InvalidSelection(
"The pressed occurrence is not in the captured selection.",
));
}
members.sort_by_key(|target| target.id());
capture_targets(project, diagram, pressed, &members)
}
// Require the actual local sequence/operand path, not just an ancestor pointer.
// Cross-operand groups are allowed; no member changes its existing parent.
fn local_message(project: &Project, owner: Id, id: Id) -> bool {
let mut current = id;
let mut visited = BTreeSet::new();
while current != owner && visited.insert(current) {
let Some(element) = project.elements.get(&current) else {
return false;
};
let Some(parent) = element.parent.and_then(|id| project.elements.get(&id)) else {
return false;
};
if element.layer() != project.elements[&owner].layer() {
return false;
}
let listed = match parent.shared() {
Some(SharedBody::Occurrence {
body: OccurrenceBody::Fragment { branches, .. },
..
}) => {
matches!(
element.shared(),
Some(SharedBody::Occurrence {
body: OccurrenceBody::Branch { .. },
..
})
) && branches.iter().filter(|&&id| id == current).count() == 1
}
_ => parent
.steps()
.is_some_and(|steps| steps.iter().filter(|&&id| id == current).count() == 1),
};
if !listed {
return false;
}
current = parent.id;
}
current == owner
}
fn captured_origin(
project: &Project,
owner: Id,
geometry: &ResolvedScenarioPlan,
target: Target,
) -> Result<f32, Refusal> {
let representation = Representation::Saved { diagram };
let owner = checked_owner(project, representation).ok_or(Refusal::InvalidSavedView(diagram))?;
let geometry = resolve(project, representation, target.id()).map_err(Refusal::Geometry)?;
let element = project.elements.get(&target.id());
let origin = match target {
Target::Lane(id)
if element.is_some_and(|e| {
e.kind() == Kind::Lifeline
&& e.parent == Some(owner)
&& e.layer() == project.elements[&owner].layer()
}) =>
if element.is_some_and(|e| e.kind() == Kind::Lifeline && e.parent == Some(owner)) =>
{
// Unassigned headers are still rendered local targets.
let headers = geometry
.scene
.boxes
.iter()
.filter(|item| {
item.id == id && item.role == crate::behavior::SceneHitRole::LifelineHeader
})
.count();
(headers == 1)
.then(|| geometry.scene.render_lanes.get(&id).copied())
.flatten()
// Unassigned lifelines still have drawn headers. `scene.lanes`
// excludes them only from semantic message-endpoint hit testing.
geometry.scene.render_lanes.get(&id).copied()
}
Target::Message(id)
if element.is_some_and(|e| {
@@ -203,16 +79,17 @@ fn captured_origin(
..
})
)
}) && local_message(project, owner, id) =>
}) =>
{
// A lifecycle receive row is not the stored send coordinate. Reject
// ambiguous repeated drawing IDs rather than choosing the first.
let mut lines = geometry.scene.lines.iter().filter(|line| line.id == id);
let first = lines
.next()
// A lifecycle row can denote receive y; the public position means
// send y. Read the actual outgoing endpoint, including self calls.
geometry
.scene
.lines
.iter()
.find(|line| line.id == id)
.and_then(|line| line.points.first())
.map(|point| point.y);
if lines.next().is_some() { None } else { first }
.map(|point| point.y)
}
_ => None,
}
@@ -220,38 +97,11 @@ fn captured_origin(
if !origin.is_finite() {
return Err(Refusal::TargetNotRendered(target));
}
Ok(origin)
}
fn capture_targets(
project: &Project,
diagram: Id,
target: Target,
targets: &[Target],
) -> Result<Drag, Refusal> {
let representation = Representation::Saved { diagram };
let owner = checked_owner(project, representation).ok_or(Refusal::InvalidSavedView(diagram))?;
let geometry = resolve(project, representation, target.id()).map_err(Refusal::Geometry)?;
let origins = targets
.iter()
.map(|member| {
captured_origin(project, owner, &geometry, *member).map(|origin| (*member, origin))
})
.collect::<Result<Vec<_>, _>>()?;
let origin = origins
.iter()
.find(|(member, _)| *member == target)
.expect("admitted pressed member")
.1;
Ok(Drag {
original: project.clone(),
diagram,
target,
origin,
other_origins: origins
.into_iter()
.filter(|(member, _)| *member != target)
.collect(),
original_geometry: geometry,
})
}
@@ -267,50 +117,28 @@ impl Drag {
if original_delta == 0. {
return Ok(None);
}
let mut commands = Vec::new();
for (target, origin) in
std::iter::once((self.target, self.origin)).chain(self.other_origins.iter().copied())
{
let position = (f64::from(origin) + f64::from(original_delta)) as f32;
if !position.is_finite() || position.abs() > oa_core::MAX_COORDINATE {
return Err(Refusal::Command(
"Scenario positions must be finite and within the shared coordinate bounds."
.into(),
));
}
if position == origin {
continue;
}
let command = match target {
Target::Lane(lifeline) => Command::SetScenarioLanePosition {
diagram: self.diagram,
position: ScenarioLanePosition {
lifeline,
x: position,
},
},
Target::Message(occurrence) => Command::SetScenarioMessagePosition {
diagram: self.diagram,
position: ScenarioMessagePosition {
address: OccurrenceEndpoint {
references: vec![],
occurrence,
},
send_y: position,
},
},
};
commands.push((target.id(), command));
}
if commands.is_empty() {
let position = (f64::from(self.origin) + f64::from(original_delta)) as f32;
if position == self.origin {
return Ok(None);
}
commands.sort_by_key(|(id, _)| *id);
let mut commands: Vec<_> = commands.into_iter().map(|(_, command)| command).collect();
let command = if self.other_origins.is_empty() {
commands.pop().expect("changed singleton")
} else {
Command::Batch(commands)
let command = match self.target {
Target::Lane(lifeline) => Command::SetScenarioLanePosition {
diagram: self.diagram,
position: ScenarioLanePosition {
lifeline,
x: position,
},
},
Target::Message(occurrence) => Command::SetScenarioMessagePosition {
diagram: self.diagram,
position: ScenarioMessagePosition {
address: OccurrenceEndpoint {
references: vec![],
occurrence,
},
send_y: position,
},
},
};
let project = execute_batch(&self.original, std::slice::from_ref(&command))
.map_err(|error| Refusal::Command(error.to_string()))?;
@@ -5,7 +5,6 @@ use crate::behavior::{
scenario::{CanvasTool, Context, State},
scenario_layout::Diagnostic,
};
use crate::{diagram_selection::Snapshot, diagram_usage::Target as SelectionTarget};
use iced::{Point, Rectangle, Vector};
#[cfg(test)]
@@ -17,7 +16,6 @@ mod timing_tests;
pub struct Stamp {
pub(crate) context: Context,
pub(crate) camera_epoch: u64,
pub(crate) selection_generation: u64,
}
#[derive(Clone, Copy, Debug)]
@@ -74,14 +72,11 @@ pub(crate) struct Session {
drag: Drag,
preview: Option<Preview>,
refusal: Option<Refusal>,
group_refusal: Option<Refusal>,
ever_crossed: bool,
snapshot: Option<Snapshot>,
}
pub(in crate::behavior) struct Paint<'a> {
pub(in crate::behavior) scene: &'a Scene,
pub(in crate::behavior) selected_count: usize,
#[cfg(test)]
pub(in crate::behavior) bounds: Rectangle,
pub(in crate::behavior) refusal: Option<&'a Refusal>,
@@ -103,21 +98,7 @@ pub(crate) enum Finish {
impl Session {
fn configure(&self, scene: &mut Scene) {
scene.selected = self
.snapshot
.as_ref()
.filter(|s| s.targets().len() > 1)
.and_then(|s| match s.primary() {
Some(SelectionTarget::ScenarioObject(id)) => Some(*id),
_ => None,
})
.unwrap_or(self.drag.target.id());
scene.selection = self
.snapshot
.as_ref()
.filter(|s| s.targets().len() > 1)
.cloned()
.map(|s| (self.stamp.context, s));
scene.selected = self.drag.target.id();
scene.scale = self.scale;
scene.camera_pan = self.pan;
scene.interaction = Some((self.stamp.context, CanvasTool::Select, true));
@@ -136,18 +117,8 @@ impl Session {
Target::Lane(_) => screen.x - self.press_screen.x,
Target::Message(_) => screen.y - self.press_screen.y,
};
self.ever_crossed |= if self
.snapshot
.as_ref()
.is_some_and(|s| s.targets().len() > 1)
{
screen.distance(self.press_screen) >= 3.
} else {
distance.abs() >= 3.
};
if !self.ever_crossed || self.group_refusal.is_some() {
// A rejected whole set may retain a pressed singleton only as its
// original display witness. It must never prepare a partial move.
self.ever_crossed |= distance.abs() >= 3.;
if !self.ever_crossed {
return;
}
match self.drag.prepare(distance / self.scale) {
@@ -168,9 +139,6 @@ impl State {
pub(crate) fn spacing_stamp(&self) -> Option<Stamp> {
self.spacing.as_ref().map(|s| s.stamp)
}
pub(crate) fn spacing_snapshot(&self) -> Option<&Snapshot> {
self.spacing.as_ref()?.snapshot.as_ref()
}
pub(crate) fn cancel_spacing(&mut self) -> bool {
self.spacing.take().is_some()
}
@@ -181,55 +149,28 @@ impl State {
camera_epoch: u64,
) -> Option<Paint<'_>> {
let session = self.spacing.as_ref()?;
if session.stamp.context != context || session.stamp.camera_epoch != camera_epoch {
if session.stamp
!= (Stamp {
context,
camera_epoch,
})
{
return None;
}
let original = &session.drag.original_geometry;
let shown = session.preview.as_ref().map_or(original, |p| &p.geometry);
Some(Paint {
scene: &shown.scene,
selected_count: session
.snapshot
.as_ref()
.map_or(1, |s| s.targets().len().max(1)),
#[cfg(test)]
bounds: shown.bounds,
refusal: session.refusal.as_ref().or_else(|| {
session
.ever_crossed
.then_some(session.group_refusal.as_ref())
.flatten()
}),
refusal: session.refusal.as_ref(),
original_diagnostics: &original.plan.diagnostics,
original_latent: &original.latent_messages,
diagnostics: &shown.plan.diagnostics,
})
}
#[cfg(test)]
pub(crate) fn begin_spacing(&mut self, project: &Project, event: Event) -> Result<(), Refusal> {
self.begin_spacing_capture(project, event, None)
}
pub(crate) fn begin_spacing_with_snapshot(
&mut self,
project: &Project,
event: Event,
snapshot: Snapshot,
) -> Result<(), Refusal> {
if snapshot.generation() != event.stamp().selection_generation
|| snapshot.context()
!= crate::behavior::scenario::selection::context(event.stamp().context)
{
return Err(Refusal::InvalidGesture);
}
self.begin_spacing_capture(project, event, Some(snapshot))
}
fn begin_spacing_capture(
&mut self,
project: &Project,
event: Event,
snapshot: Option<Snapshot>,
) -> Result<(), Refusal> {
let Event::Begin {
stamp,
token,
@@ -275,34 +216,7 @@ impl State {
let Representation::Saved { diagram } = context.representation else {
return Err(Refusal::InvalidGesture);
};
let mut group_refusal = None;
let mut drag = if let Some(selected) = snapshot.as_ref().filter(|s| s.targets().len() > 1) {
if !selected
.targets()
.contains(&SelectionTarget::ScenarioObject(target.id()))
{
return Err(Refusal::TargetNotHit(target));
}
match capture_group(project, diagram, target, selected.targets()) {
Ok(drag) => drag,
Err(error)
if matches!(
&error,
Refusal::InvalidSelection(_) | Refusal::TargetNotRendered(_)
) =>
{
// Selection clicks do not require movement eligibility.
// This capture supplies original paint and the exact hit
// only; group_refusal forbids calling its prepare method.
let original = capture(project, diagram, target)?;
group_refusal = Some(error);
original
}
Err(error) => return Err(error),
}
} else {
capture(project, diagram, target)?
};
let mut drag = capture(project, diagram, target)?;
let camera = self.presentation.camera(drag.original_geometry.bounds);
if self.presentation.viewport != Some(canvas_bounds.size())
|| camera.scale != scale
@@ -332,19 +246,7 @@ impl State {
}
// Configure baseline without changing the persistent camera or selection.
let scene = &mut drag.original_geometry.scene;
scene.selected = snapshot
.as_ref()
.filter(|s| s.targets().len() > 1)
.and_then(|s| match s.primary() {
Some(SelectionTarget::ScenarioObject(id)) => Some(*id),
_ => None,
})
.unwrap_or(target.id());
scene.selection = snapshot
.as_ref()
.filter(|s| s.targets().len() > 1)
.cloned()
.map(|s| (context, s));
scene.selected = target.id();
scene.scale = scale;
scene.camera_pan = pan;
scene.interaction = Some((context, CanvasTool::Select, true));
@@ -360,9 +262,7 @@ impl State {
drag,
preview: None,
refusal: None,
group_refusal,
ever_crossed: false,
snapshot,
});
Ok(())
}
@@ -382,9 +282,6 @@ impl State {
if !session.ever_crossed {
return Ok(Finish::Click(session.drag.target.id()));
}
if let Some(error) = session.group_refusal {
return Err(error);
}
Ok(match session.preview {
None => Finish::Noop,
Some(preview) => Finish::Commit {
@@ -95,7 +95,6 @@ pub(crate) fn packet(
stamp: Stamp {
context,
camera_epoch,
selection_generation: 0,
},
token: Id::new_v4(),
target,
@@ -136,55 +135,6 @@ fn press(event: Event) -> Point {
press_screen
}
#[test]
fn group_preview_keeps_captured_members_and_nonpressed_primary() {
use crate::diagram_selection::Selection;
use crate::diagram_usage::Target as Selected;
let (doc, mut state, mut event) = setup(true);
let context = event.stamp().context;
let first = match event {
Event::Begin { target, .. } => target.id(),
_ => unreachable!(),
};
let second = doc
.project()
.identities()
.into_iter()
.find(|(_, alias)| *alias == "second")
.unwrap()
.0;
let selection_context = crate::behavior::scenario::selection::context(context);
let mut selection = Selection::new(selection_context.scope);
selection.add(Selected::ScenarioObject(first), |_| true);
selection.add(Selected::ScenarioObject(second), |_| true);
let snapshot = selection.snapshot(selection_context).unwrap();
if let Event::Begin { stamp, .. } = &mut event {
stamp.selection_generation = snapshot.generation();
}
let start = press(event);
state
.begin_spacing_with_snapshot(doc.project(), event, snapshot.clone())
.unwrap();
for delta in [Vector::new(0., 0.), Vector::new(0., 24.)] {
state.move_spacing(start + delta);
let paint = state
.spacing_paint(context, event.stamp().camera_epoch)
.unwrap();
assert_eq!(paint.scene.selected, second);
assert_eq!(
paint.scene.selection.as_ref().unwrap(),
&(context, snapshot.clone())
);
assert_eq!(paint.selected_count, 2);
}
state.move_spacing(start + Vector::new(50., 0.));
assert!(matches!(
state.finish_spacing(start + Vector::new(50., 0.)).unwrap(),
Finish::Noop
));
assert!(!doc.can_undo());
}
#[test]
fn real_preview_moves_only_active_axis_from_original_and_never_changes_document() {
for message in [false, true] {
@@ -1,675 +0,0 @@
use super::*;
use crate::document::Document;
use oa_core::{Diagram, DiagramLayout, Layer, ProjectionKind, ScenarioLayout, language};
fn id(project: &Project, alias: &str) -> Id {
project
.identities()
.into_iter()
.find(|(_, name)| *name == alias)
.unwrap()
.0
}
fn fixture(layout: Option<ScenarioLayout>) -> (Document, Id, Id) {
let mut project = language::parse(FIXTURE).unwrap();
let owner = id(&project, "conversation");
let right = id(&project, "right");
let mut views = Vec::new();
for (alias, layout) in [
("active_view", layout),
(
"untouched_view",
Some(ScenarioLayout {
lanes: vec![ScenarioLanePosition {
lifeline: right,
x: 700.,
}],
messages: vec![],
}),
),
] {
let diagram = Id::new_v4();
project.diagrams.insert(
diagram,
Diagram {
id: diagram,
alias: alias.into(),
name: alias.into(),
description: "Preserve authored view metadata".into(),
layer: Layer::Oa,
projection: ProjectionKind::Scenario { scenario: owner },
nodes: Default::default(),
routes: vec![],
view_options: Default::default(),
layout: layout.map(DiagramLayout::Scenario),
},
);
views.push(diagram);
}
(
Document::from_project(project, Default::default()).unwrap(),
views[0],
views[1],
)
}
fn targets(ids: &[Id]) -> Vec<SelectionTarget> {
ids.iter()
.copied()
.map(SelectionTarget::ScenarioObject)
.collect()
}
fn message(occurrence: Id, send_y: f32) -> ScenarioMessagePosition {
ScenarioMessagePosition {
address: OccurrenceEndpoint {
references: vec![],
occurrence,
},
send_y,
}
}
// Literal expected authored payload, not another execution of planner commands.
fn expected(
project: &Project,
diagram: Id,
lanes: Vec<ScenarioLanePosition>,
messages: Vec<ScenarioMessagePosition>,
) -> Project {
let mut result = project.clone();
result.diagrams.get_mut(&diagram).unwrap().layout =
Some(DiagramLayout::Scenario(ScenarioLayout { lanes, messages }));
result
}
fn exact_transaction(mut document: Document, preview: Preview, expected: Project) {
let original = document.snapshot();
let revision = document.revision();
assert_eq!(preview.project, expected);
assert!(matches!(&preview.command, Command::Batch(_)));
document.execute(preview.command).unwrap();
assert_eq!(document.project(), &expected);
assert_eq!(document.revision(), revision + 1);
let changed = document.bundle().clone();
assert_ne!(changed, original.bundle);
assert_eq!(
language::parse(&language::format(document.project())).unwrap(),
expected
);
assert!(document.undo());
assert_eq!(document.project(), &original.decoded.project);
assert_eq!(document.bundle(), &original.bundle);
assert!(!document.can_undo());
assert!(document.redo());
assert_eq!(document.project(), &expected);
assert_eq!(document.bundle(), &changed);
assert!(!document.can_redo());
let reopened = Document::from_bundle(changed, Default::default()).unwrap();
assert_eq!(reopened.project(), &expected);
assert!(!reopened.dirty() && !reopened.can_undo());
}
#[test]
fn group_messages_move_from_each_original_with_press_and_selection_order_independent() {
let (document, diagram, other) = fixture(None);
let before = document.project();
let first = id(before, "first");
let second = id(before, "second");
let wanted = expected(
before,
diagram,
vec![],
vec![message(first, 188.), message(second, 280.)],
);
for members in [[first, second], [second, first]] {
for pressed in [first, second] {
let drag = capture_group(
before,
diagram,
Target::Message(pressed),
&targets(&members),
)
.unwrap();
let preview = drag.prepare(40.).unwrap().unwrap();
assert_eq!(preview.project, wanted);
assert_eq!(preview.project.diagrams[&other], before.diagrams[&other]);
assert_eq!(drag.original, *before);
}
}
let preview = capture_group(
before,
diagram,
Target::Message(second),
&targets(&[second, first]),
)
.unwrap()
.prepare(40.)
.unwrap()
.unwrap();
exact_transaction(document, preview, wanted);
}
#[test]
fn group_lanes_vacate_each_others_space_in_one_candidate_and_one_history_entry() {
let (document, diagram, _) = fixture(None);
let before = document.project();
let left = id(before, "left");
let right = id(before, "right");
// Moving only Left into Right's old x cannot render a distinct-endpoint arrow.
assert!(matches!(
capture(before, diagram, Target::Lane(left))
.unwrap()
.prepare(240.),
Err(Refusal::Geometry(_))
));
let wanted = expected(
before,
diagram,
vec![
ScenarioLanePosition {
lifeline: left,
x: 400.,
},
ScenarioLanePosition {
lifeline: right,
x: 640.,
},
],
vec![],
);
for pressed in [left, right] {
let preview = capture_group(
before,
diagram,
Target::Lane(pressed),
&targets(&[right, left]),
)
.unwrap()
.prepare(240.)
.unwrap()
.unwrap();
assert_eq!(preview.project, wanted);
assert_eq!(preview.geometry.scene.render_lanes[&left], 400.);
assert_eq!(preview.geometry.scene.render_lanes[&right], 640.);
}
let preview = capture_group(
before,
diagram,
Target::Lane(right),
&targets(&[left, right]),
)
.unwrap()
.prepare(240.)
.unwrap()
.unwrap();
exact_transaction(document, preview, wanted);
}
#[test]
fn self_call_and_reply_are_validated_together_but_partial_candidate_is_refused() {
let (document, diagram, _) = fixture(None);
let before = document.project();
let call = id(before, "call");
let reply = id(before, "reply");
let first = id(before, "first");
assert!(matches!(
capture(before, diagram, Target::Message(call))
.unwrap()
.prepare(118.),
Err(Refusal::Geometry(_))
));
let drag = capture_group(
before,
diagram,
Target::Message(reply),
&targets(&[reply, call]),
)
.unwrap();
let preview = drag.prepare(118.).unwrap().unwrap();
assert_eq!(
preview.project,
expected(
before,
diagram,
vec![],
vec![message(call, 450.), message(reply, 542.)]
)
);
let line = preview
.geometry
.scene
.lines
.iter()
.find(|line| line.id == call)
.unwrap();
assert_eq!(line.points.first().unwrap().y, 450.);
assert_eq!(line.points.last().unwrap().y, 474.);
let partial = capture_group(
before,
diagram,
Target::Message(first),
&targets(&[first, call]),
)
.unwrap();
assert!(matches!(partial.prepare(118.), Err(Refusal::Geometry(_))));
assert_eq!(partial.original, *before);
assert!(!document.can_undo());
}
#[test]
fn exact_set_admission_rejects_empty_duplicates_mixed_unsupported_missing_and_unpressed() {
let (document, diagram, _) = fixture(None);
let before = document.project();
let first = id(before, "first");
let second = id(before, "second");
let left = id(before, "left");
let source = id(before, "source");
let unfinished = id(before, "unfinished");
for selection in [
vec![],
targets(&[first, first]),
targets(&[first, left]),
targets(&[first, source]),
targets(&[first, Id::new_v4()]),
targets(&[second]),
targets(&[first, unfinished]),
vec![
SelectionTarget::ScenarioObject(first),
SelectionTarget::Whole,
],
vec![
SelectionTarget::ScenarioObject(first),
SelectionTarget::Placement(second),
],
] {
assert!(
capture_group(before, diagram, Target::Message(first), &selection).is_err(),
"{selection:?}"
);
}
assert!(
capture_group(
before,
Id::nil(),
Target::Message(first),
&targets(&[first, second])
)
.is_err()
);
assert!(
capture_group(
before,
diagram,
Target::Lane(first),
&targets(&[first, second])
)
.is_err()
);
assert!(!document.dirty() && !document.can_undo());
}
#[test]
fn foreign_unlisted_and_ambiguous_local_messages_cannot_enter_a_group() {
let (document, diagram, _) = fixture(None);
let before = document.project();
let first = id(before, "first");
let second = id(before, "second");
let owner = id(before, "conversation");
for case in 0..3 {
let mut project = before.clone();
match case {
0 => {
let foreign = project
.add_element(Kind::Scenario, Layer::Oa, "Foreign", None)
.unwrap();
project.elements.get_mut(&second).unwrap().parent = Some(foreign);
}
1 => project
.elements
.get_mut(&owner)
.unwrap()
.steps_mut()
.unwrap()
.retain(|id| *id != second),
_ => project
.elements
.get_mut(&owner)
.unwrap()
.steps_mut()
.unwrap()
.push(second),
}
assert!(
capture_group(
&project,
diagram,
Target::Message(first),
&targets(&[first, second])
)
.is_err()
);
}
}
#[test]
fn fallback_baseline_is_refused_before_any_zero_motion_can_be_planned() {
let (document, diagram, _) = fixture(None);
let call = id(document.project(), "call");
let reply = id(document.project(), "reply");
let invalid = expected(
document.project(),
diagram,
vec![],
vec![message(call, 450.)],
);
let shown = crate::behavior::scenario_layout::presentation::present(
&invalid,
Representation::Saved { diagram },
call,
);
assert!(shown.capabilities().geometry_navigation);
assert!(!shown.capabilities().coordinate_input);
assert!(matches!(
capture_group(
&invalid,
diagram,
Target::Message(reply),
&targets(&[reply, call])
),
Err(Refusal::Geometry(_))
));
}
#[test]
fn group_noops_preserve_sparse_overrides_complete_source_and_existing_redo() {
for sparse in [true, false] {
let (mut document, diagram, _) = fixture((!sparse).then(ScenarioLayout::default));
let first = id(document.project(), "first");
let second = id(document.project(), "second");
let owner = id(document.project(), "conversation");
document
.execute(Command::Rename(owner, "Redo sentinel".into()))
.unwrap();
let redo = document.snapshot();
assert!(document.undo());
let original = document.snapshot();
let revision = document.revision();
let drag = capture_group(
document.project(),
diagram,
Target::Message(second),
&targets(&[second, first]),
)
.unwrap();
assert!(drag.prepare(30.).unwrap().is_some());
for delta in [0., -0., f32::from_bits(1)] {
assert!(drag.prepare(delta).unwrap().is_none());
}
assert!(std::sync::Arc::ptr_eq(&original, &document.snapshot()));
assert_eq!(document.bundle(), &original.bundle);
assert_eq!(document.revision(), revision);
assert_eq!(drag.original, original.decoded.project);
assert!(document.redo());
assert_eq!(document.project(), &redo.decoded.project);
assert_eq!(document.bundle(), &redo.bundle);
}
}
#[test]
fn subprecision_unchanged_member_does_not_materialize_an_absent_override() {
let (document, diagram, _) = fixture(None);
let first = id(document.project(), "first");
let second = id(document.project(), "second");
let project = expected(
document.project(),
diagram,
vec![],
vec![message(first, 0.)],
);
let delta = 0.000001_f32;
assert_eq!(240_f32 + delta, 240.);
let preview = capture_group(
&project,
diagram,
Target::Message(second),
&targets(&[second, first]),
)
.unwrap()
.prepare(delta)
.unwrap()
.unwrap();
assert_eq!(
preview.project,
expected(&project, diagram, vec![], vec![message(first, delta)])
);
assert!(matches!(preview.command, Command::Batch(ref commands) if commands.len() == 1));
}
#[test]
fn invalid_delta_or_one_out_of_bounds_member_refuses_the_whole_group() {
let (document, diagram, _) = fixture(None);
let left = id(document.project(), "left");
let right = id(document.project(), "right");
let project = expected(
document.project(),
diagram,
vec![ScenarioLanePosition {
lifeline: right,
x: 999_990.,
}],
vec![],
);
let drag = capture_group(
&project,
diagram,
Target::Lane(left),
&targets(&[left, right]),
)
.unwrap();
for delta in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
assert!(matches!(drag.prepare(delta), Err(Refusal::InvalidDelta)));
}
for delta in [20., f32::MAX, -f32::MAX] {
assert!(matches!(drag.prepare(delta), Err(Refusal::Command(_))));
}
assert_eq!(drag.original, project);
assert!(drag.prepare(0.).unwrap().is_none());
}
#[test]
fn singleton_capture_group_retains_existing_singleton_command_and_result() {
let (document, diagram, _) = fixture(None);
for target in [
Target::Lane(id(document.project(), "right")),
Target::Message(id(document.project(), "second")),
] {
let single = capture(document.project(), diagram, target)
.unwrap()
.prepare(25.)
.unwrap()
.unwrap();
let group = capture_group(
document.project(),
diagram,
target,
&targets(&[target.id()]),
)
.unwrap()
.prepare(25.)
.unwrap()
.unwrap();
assert_eq!(group.project, single.project);
assert!(!matches!(group.command, Command::Batch(_)));
}
}
#[test]
fn existing_layout_order_and_unselected_values_are_preserved_exactly() {
let (document, diagram, _) = fixture(None);
let first = id(document.project(), "first");
let second = id(document.project(), "second");
let reply = id(document.project(), "reply");
let right = id(document.project(), "right");
let lanes = vec![ScenarioLanePosition {
lifeline: right,
x: 400.,
}];
let project = expected(
document.project(),
diagram,
lanes.clone(),
vec![message(reply, 424.), message(first, 148.)],
);
let preview = capture_group(
&project,
diagram,
Target::Message(second),
&targets(&[second, first]),
)
.unwrap()
.prepare(40.)
.unwrap()
.unwrap();
assert_eq!(
preview.project,
expected(
&project,
diagram,
lanes,
vec![
message(reply, 424.),
message(first, 188.),
message(second, 280.)
]
)
);
}
#[test]
fn local_messages_in_different_operands_move_without_reparenting_or_reordering() {
use crate::behavior::topology::occurrence;
let (document, diagram, _) = fixture(None);
let mut project = document.project().clone();
let owner = id(&project, "conversation");
let first = id(&project, "first");
let second = id(&project, "second");
let fragment = occurrence(
&project,
owner,
"Alternatives",
OccurrenceBody::Fragment {
operator: oa_core::FragmentOperator::Alternative,
branches: vec![],
},
);
let frame = fragment.id;
project.elements.insert(frame, fragment);
let mut branches = Vec::new();
for member in [first, second] {
let branch = occurrence(
&project,
frame,
"Operand",
OccurrenceBody::Branch {
guard: "ready".into(),
occurrences: vec![member],
},
);
let branch_id = branch.id;
project.elements.insert(branch_id, branch);
project.elements.get_mut(&member).unwrap().parent = Some(branch_id);
branches.push(branch_id);
}
if let Some(SharedBody::Occurrence {
body: OccurrenceBody::Fragment {
branches: items, ..
},
..
}) = project.elements.get_mut(&frame).unwrap().shared_mut()
{
*items = branches.clone();
} else {
unreachable!();
}
let steps = project
.elements
.get_mut(&owner)
.unwrap()
.steps_mut()
.unwrap();
steps.retain(|id| ![first, second].contains(id));
steps.insert(0, frame);
let document = Document::from_project(project, Default::default()).unwrap();
let project = document.project();
let baseline = resolve(project, Representation::Saved { diagram }, first).unwrap();
let original_send = |id| {
baseline
.scene
.lines
.iter()
.find(|line| line.id == id)
.unwrap()
.points[0]
.y
};
let preview = capture_group(
project,
diagram,
Target::Message(second),
&targets(&[second, first]),
)
.unwrap()
.prepare(-5.)
.unwrap()
.unwrap();
assert_eq!(
preview.project,
expected(
project,
diagram,
vec![],
vec![
message(first, original_send(first) - 5.),
message(second, original_send(second) - 5.)
]
)
);
assert_eq!(preview.project.elements, project.elements);
let mut omitted = project.clone();
if let Some(SharedBody::Occurrence {
body: OccurrenceBody::Fragment {
branches: items, ..
},
..
}) = omitted.elements.get_mut(&frame).unwrap().shared_mut()
{
items.retain(|id| *id != branches[1]);
}
assert!(
capture_group(
&omitted,
diagram,
Target::Message(first),
&targets(&[first, second])
)
.is_err()
);
}
const FIXTURE: &str = r#"arcadia 7;
project spacing_group "Saved group spacing" id "71000000-0000-4000-8000-000000000001" {
oa_activity source "Source" id "71000000-0000-4000-8000-000000000002" {}
oa_activity target "Target" id "71000000-0000-4000-8000-000000000003" {}
interaction exchange "Repeated exchange" id "71000000-0000-4000-8000-000000000004" { source @source; target @target; }
oa_scenario conversation "Conversation" id "71000000-0000-4000-8000-000000000005" { viewpoint OperationalActivity; steps [@first, @second, @call, @reply, @unfinished]; }
oa_lifeline left "Left" id "71000000-0000-4000-8000-000000000006" { parent @conversation; represented @source; }
oa_lifeline right "Right" id "71000000-0000-4000-8000-000000000007" { parent @conversation; represented @target; }
oa_occurrence first "First" id "71000000-0000-4000-8000-000000000008" { parent @conversation; body { Exchange { exchange @exchange; sender @left; receiver @right; message_kind Asynchronous; }; }; }
oa_occurrence second "Second" id "71000000-0000-4000-8000-000000000009" { parent @conversation; body { Exchange { exchange @exchange; sender @left; receiver @right; message_kind Asynchronous; }; }; }
oa_occurrence call "Call" id "71000000-0000-4000-8000-00000000000a" { parent @conversation; body { Exchange { exchange none; sender @right; receiver @right; message_kind Call; }; }; }
oa_occurrence reply "Reply" id "71000000-0000-4000-8000-00000000000b" { parent @conversation; body { Exchange { exchange none; sender @right; receiver @right; message_kind Reply; reply_to @call; }; }; }
oa_occurrence unfinished "Unfinished" id "71000000-0000-4000-8000-00000000000c" { parent @conversation; body { Exchange { exchange @exchange; sender @left; receiver none; message_kind Asynchronous; }; }; }
}
"#;
@@ -75,7 +75,6 @@ pub fn view<'a>(app: &'a App, owner: Id, out: Column<'a, Message>) -> Element<'a
.spacing(8)
.into();
};
let snapshot = app.multiselect_snapshot();
let tool = state.canvas_tool(owner);
let clean = !app.behavior.has_pending() && !app.draft_dirty() && !app.source_dirty();
// Resolve once per accepted view. An active spacing session owns its exact
@@ -91,8 +90,7 @@ pub fn view<'a>(app: &'a App, owner: Id, out: Column<'a, Message>) -> Element<'a
},
ScenarioPresentation::capabilities,
);
let coordinate_input =
clean && capabilities.coordinate_input && app.scenario_reorder.session.is_none();
let coordinate_input = clean && capabilities.coordinate_input;
let notice = if let Some(paint) = &preview {
display_notice_parts(
p,
@@ -125,7 +123,6 @@ pub fn view<'a>(app: &'a App, owner: Id, out: Column<'a, Message>) -> Element<'a
scenario_view: Some((context.representation, context.visit)),
};
let keyboard = clean
&& app.scenario_reorder.session.is_none()
&& capabilities.geometry_navigation
&& super::super::canvas_workspace::keyboard_enabled(app);
let camera = move |e| Message::ScenarioCanvas(CanvasEvent::Camera(context, scope.epoch, e));
@@ -141,21 +138,15 @@ pub fn view<'a>(app: &'a App, owner: Id, out: Column<'a, Message>) -> Element<'a
scene.scale = camera_state.scale;
scene.camera_pan = camera_state.pan;
scene.interaction = Some((context, tool, clean));
scene.selection = snapshot.clone().map(|snapshot| (context, snapshot));
scene.spacing_stamp = (coordinate_input
&& !app.source_rename.open()
&& keyboard
&& matches!(context.representation, Representation::Saved { .. }))
.then(|| {
snapshot.as_ref().map(|snapshot| spacing::Stamp {
context,
camera_epoch: app.camera_epoch,
selection_generation: snapshot.generation(),
})
})
.flatten();
scene.coordinate_input =
capabilities.coordinate_input && app.scenario_reorder.session.is_none();
.then_some(spacing::Stamp {
context,
camera_epoch: app.camera_epoch,
});
scene.coordinate_input = capabilities.coordinate_input;
scene.pending_placement = coordinate_input
.then(|| state.pending_placement(owner))
.flatten();
@@ -167,27 +158,8 @@ pub fn view<'a>(app: &'a App, owner: Id, out: Column<'a, Message>) -> Element<'a
// Keep this Stack in both branches: replacing the Canvas with a wrapper
// on Begin would discard its pointer capture before Move/Finish.
let mut layers = iced::widget::stack![raw_canvas];
if let Some((_, summary)) = &app.scenario_reorder.summary {
layers = layers.push(
container(iced::widget::opaque(
container(
text(format!("{summary} Use Undo to restore the previous order.")).size(12),
)
.padding(8)
.style(style::card),
))
.align_bottom(Fill)
.width(Fill)
.padding(8),
);
}
if let Some(paint) = &preview {
let feedback = spacing_feedback(p, paint.refusal, paint.diagnostics);
let feedback = if paint.selected_count > 1 {
format!("{} selected - {feedback}", paint.selected_count)
} else {
feedback
};
layers = layers.push(
container(
container(text(feedback).size(12))
@@ -224,30 +196,7 @@ pub fn view<'a>(app: &'a App, owner: Id, out: Column<'a, Message>) -> Element<'a
container(text("No supported canvas geometry is available. Inspect the model or open its source to repair this view."))
.id("scenario-viewport-unavailable").padding(16).width(Fill).height(Fill).style(style::card).into()
};
// The chooser must sit outside Viewport: that camera wrapper deliberately
// consumes Space, arrows and wheel before its child receives input.
let mut canvas_layers = iced::widget::stack![canvas];
if let Some(panel) = reorder_view::panel(app, presentation.viewport) {
canvas_layers = canvas_layers.push(panel);
}
let canvas = canvas_layers.into();
let selected_count = snapshot.as_ref().map_or(0, |s| s.targets().len());
let mut navigation = row![
out.width(Fill),
text(format!("{selected_count} selected")).size(12)
]
.spacing(6);
let reorder = reorder_view::begin(app);
navigation = navigation.push(super::super::canvas_workspace::Control::new(
button(text("Reorder…").size(12))
.padding(6)
.on_press_maybe(reorder.clone()),
scope,
"Reorder",
keyboard,
reorder,
camera,
));
let mut navigation = row![out.width(Fill)].spacing(6);
if state.can_back(p, context) {
navigation =
navigation.push(button(text("Back").size(12)).padding(6).on_press_maybe(
@@ -313,12 +262,7 @@ pub fn view<'a>(app: &'a App, owner: Id, out: Column<'a, Message>) -> Element<'a
let entries = [
(
"S",
if selected_count > 1 && matches!(context.representation, Representation::Saved { .. })
{
"Drag messages vertically or lifelines horizontally; Shift-click to change selection"
} else if selected_count > 1 {
"Shift-click to change selection; click a member to select only it"
} else if matches!(context.representation, Representation::Saved { .. }) {
if matches!(context.representation, Representation::Saved { .. }) {
"Select / move messages and lifelines"
} else {
"Select / reorder occurrences"
@@ -399,15 +343,7 @@ pub fn view<'a>(app: &'a App, owner: Id, out: Column<'a, Message>) -> Element<'a
}),
);
let mut details = column![
text(if selected_count > 1 {
format!(
"{selected_count} selected; properties: {}",
p.named(app.selected)
)
} else {
format!("Selected: {}", p.named(app.selected))
})
.size(12),
text(format!("Selected: {}", p.named(app.selected))).size(12),
container(button(text("Properties").size(12)).on_press_maybe(
(clean && properties_target(p, context, app.selected)).then_some(
Message::ScenarioCanvas(CanvasEvent::Properties(context, app.selected)),
@@ -431,7 +367,7 @@ pub fn view<'a>(app: &'a App, owner: Id, out: Column<'a, Message>) -> Element<'a
let details = details_pane(
context,
state.scroll_restore(context, ScrollArea::Page),
sequence_rows(p, context, owner, details, clean, snapshot.as_ref()).into(),
sequence_rows(p, context, owner, details, clean).into(),
);
let mut form_panel = editor_panel(
Id::nil(),
@@ -749,9 +685,7 @@ pub fn view<'a>(app: &'a App, owner: Id, out: Column<'a, Message>) -> Element<'a
.into();
}
}
let editor = if app.scenario_reorder.session.is_some() {
None
} else if pending {
let editor = if pending {
Some(form_panel)
} else {
presentation.shelf.map(|_| details)
@@ -790,13 +724,9 @@ fn display_notice_parts<'a>(
for diagnostic in diagnostics.iter().take(3) {
let explanation = match diagnostic {
Diagnostic::SourceOrderCrossing { before, after, .. } => format!(
"Saved positions place @{} below @{}, contrary to execution order.",
p.elements
.get(before)
.map_or("?", |element| element.alias.as_str()),
p.elements
.get(after)
.map_or("?", |element| element.alias.as_str())
"Saved spacing places '{}' below '{}'. The Scenario order is unchanged.",
p.named(*before),
p.named(*after)
),
Diagnostic::FrameExtendsPastFollowingAnchor { frame, following } => format!(
"Saved spacing makes '{}' overlap '{}'. Both keep their saved positions.",
@@ -821,7 +751,7 @@ fn display_notice_parts<'a>(
if let Some(issue) = requested_issues.first() {
notice = notice.push(text(geometry_problem(p, issue)).size(12));
}
notice = notice.push(text("Selection, navigation and Fit remain available. Coordinate editing requires repaired saved positions. Reorder uses explicit sequence destinations and previews its display consequences.").size(12));
notice = notice.push(text("Selection, navigation and Fit remain available. Canvas insertion and reordering are unavailable until the saved positions are repaired.").size(12));
}
PresentationStatus::Unavailable { .. } => {
notice = notice.push(text("This view is unavailable. Its identity or automatic geometry cannot be resolved; the model and source remain available for inspection and repair.").size(12));
@@ -978,24 +908,7 @@ fn sequence_rows<'a>(
owner: Id,
mut out: Column<'a, Message>,
enabled: bool,
snapshot: Option<&crate::diagram_selection::Snapshot>,
) -> Column<'a, Message> {
let choose = |id| {
snapshot.map_or_else(
|| Message::ScenarioCanvas(CanvasEvent::Select(context, id)),
|snapshot| {
selection::message(
context,
snapshot,
selection::Intent::Choose(
crate::diagram_usage::Target::ScenarioObject(id),
crate::app::multiselect::Choice::Replace,
),
)
},
)
};
let order_enabled = enabled && snapshot.is_none_or(|snapshot| snapshot.targets().len() <= 1);
if let Some(SharedBody::Occurrence {
body: OccurrenceBody::Fragment { branches, .. },
..
@@ -1004,10 +917,10 @@ fn sequence_rows<'a>(
for branch in branches {
out = out.push(sequence_action(
&format!("Operand: {}", p.named(*branch)),
choose(*branch),
Message::Select(*branch),
enabled,
));
out = sequence_rows(p, context, *branch, out, enabled, snapshot);
out = sequence_rows(p, context, *branch, out, enabled);
}
}
if let Some(steps) = p.elements.get(&owner).and_then(|e| e.steps()) {
@@ -1016,17 +929,17 @@ fn sequence_rows<'a>(
row![
sequence_action(
&format!("{}. {}", i + 1, p.named(*id)),
choose(*id),
Message::Select(*id),
enabled
)
.width(Fill),
button("↑")
.padding(8)
.on_press_maybe((order_enabled && i > 0).then_some(Message::Command(
.on_press_maybe((enabled && i > 0).then_some(Message::Command(
Command::MoveStep(owner, i, i.saturating_sub(1))
))),
button("↓").padding(8).on_press_maybe(
(order_enabled && i + 1 < steps.len())
(enabled && i + 1 < steps.len())
.then_some(Message::Command(Command::MoveStep(owner, i, i + 1)))
)
]
@@ -1047,7 +960,7 @@ fn sequence_rows<'a>(
enabled,
));
}
out = sequence_rows(p, context, *id, out, enabled, snapshot);
out = sequence_rows(p, context, *id, out, enabled);
}
}
out
@@ -388,16 +388,13 @@ fn opening_scenario_properties_cancels_held_spacing_and_rejects_its_late_finish(
CanvasEvent::Viewport(c, Size::new(900., 600.))
});
open_details(&mut app);
let mut begin = packet(
let begin = packet(
app.project(),
&app.behavior.scenario,
app.scenario_context().unwrap(),
app.camera_epoch,
Target::Message(message),
);
if let Spacing::Begin { stamp, .. } = &mut begin {
stamp.selection_generation = app.multiselect_snapshot().unwrap().generation();
}
let Spacing::Begin { press_screen, .. } = begin else {
panic!()
};
@@ -446,180 +443,3 @@ fn opening_scenario_properties_cancels_held_spacing_and_rejects_its_late_finish(
assert!(Arc::ptr_eq(&snapshot, &app.document.snapshot()));
assert!(!app.document.can_undo() && !app.document.can_redo());
}
#[tokio::test]
async fn saved_selection_shift_survives_properties_focus_busy_rebuild_and_clears_on_window_loss() {
use crate::app::multiselect::Choice;
use crate::behavior::canvas_workspace::tests::focused;
use crate::diagram_usage::Target;
fn at(mounted: &mut Mounted, app: &App, target: Id) -> Point {
let context = app.scenario_context().unwrap();
let geometry = resolve(app.project(), context.representation, app.selected).unwrap();
let line = geometry
.scene
.lines
.iter()
.find(|line| line.id == target)
.unwrap();
let world = line.label_at + Vector::new(8., 8.);
assert_eq!(geometry.scene.hit_item(world).unwrap().id, target);
let bounds = mounted.geometry(app).bounds("scenario-viewport");
let camera = app.behavior.scenario.presentation.camera(geometry.bounds);
let point = bounds.position() + camera.pan + Vector::new(world.x, world.y) * camera.scale;
assert!(bounds.contains(point));
point
}
async fn focus_name(mounted: &mut Mounted, app: &mut App) -> Point {
let shown = mounted.geometry(app);
let (inspector, offset) = shown.scrolls[&widget::Id::new("inspector")];
let input = shown
.text_inputs
.iter()
.map(|bounds| bounds.center() - offset)
.filter(|point| inspector.contains(*point))
.min_by(|a, b| a.y.total_cmp(&b.y))
.expect("visible Name input");
mounted.click(app, input).await;
assert!(!focused(mounted, app, "viewport"));
assert!(
mounted
.geometry(app)
.focused
.iter()
.any(|(_, bounds, yes)| *yes && bounds.contains(input + offset))
);
assert!(!app.draft_dirty());
input
}
let shift = |held| {
iced::Event::Keyboard(keyboard::Event::ModifiersChanged(if held {
keyboard::Modifiers::SHIFT
} else {
keyboard::Modifiers::empty()
}))
};
let (mut app, diagram, first) = setup();
let second = id(&app, "updated_report");
let mut mounted = Mounted::new(&mut app).await;
open_details(&mut app);
mounted.settle(&mut app).await;
let shown = mounted.geometry(&app);
let (details, offset) = shown.scrolls[&widget::Id::new("scenario-details")];
let properties = shown.bounds("scenario-properties").center() - offset;
assert!(details.contains(properties));
mounted.click(&mut app, properties).await;
mounted.settle(&mut app).await;
let input = focus_name(&mut mounted, &mut app).await;
let before = app.document.snapshot();
let camera = stored_camera(&app);
let context = app.scenario_context().unwrap();
let selected = app.multiselect_snapshot().unwrap();
assert_eq!(selected.targets(), &[Target::ScenarioObject(first)]);
let passive = mounted
.batch(&mut app, &[shift(true)], mouse::Cursor::Available(input))
.await;
assert!(
!passive
.iter()
.any(|message| matches!(message, Message::ScenarioCanvas(_)))
);
assert!(!focused(&mut mounted, &app, "viewport"));
assert_eq!(app.multiselect_snapshot().unwrap(), selected);
let second_at = at(&mut mounted, &app, second);
let messages = mounted.click(&mut app, second_at).await;
assert_eq!(messages.iter().filter(|message| matches!(message,
Message::ScenarioCanvas(CanvasEvent::Selection(event))
if matches!(&event.intent, selection::Intent::Choose(Target::ScenarioObject(id), Choice::Toggle) if *id == second))).count(), 1);
assert!(!messages.iter().any(|message| matches!(
message,
Message::ScenarioCanvas(CanvasEvent::Spacing(_) | CanvasEvent::Reorder { .. })
)));
assert_eq!(
app.multiselect_snapshot().unwrap().targets(),
&[
Target::ScenarioObject(first),
Target::ScenarioObject(second)
]
);
assert_eq!(
app.multiselect_snapshot().unwrap().primary(),
Some(&Target::ScenarioObject(second))
);
app.busy = true;
mounted.settle(&mut app).await;
mounted
.batch(
&mut app,
&[shift(false)],
mouse::Cursor::Available(second_at),
)
.await;
app.busy = false;
mounted.settle(&mut app).await;
let first_at = at(&mut mounted, &app, first);
let messages = mounted.click(&mut app, first_at).await;
// Saved group clicks now complete the captured spacing lifetime; its
// stationary outcome must retain the same selection-only collapse.
assert!(messages.iter().any(|message| matches!(
message,
Message::ScenarioCanvas(CanvasEvent::Spacing(
super::super::spacing::Event::Finish { .. }
))
)));
assert_eq!(
app.multiselect_snapshot().unwrap().targets(),
&[Target::ScenarioObject(first)]
);
let input = focus_name(&mut mounted, &mut app).await;
app.busy = true;
mounted.settle(&mut app).await;
mounted
.batch(&mut app, &[shift(true)], mouse::Cursor::Available(input))
.await;
app.busy = false;
mounted.settle(&mut app).await;
let second_at = at(&mut mounted, &app, second);
mounted.click(&mut app, second_at).await;
assert_eq!(
app.multiselect_snapshot().unwrap().targets(),
&[
Target::ScenarioObject(first),
Target::ScenarioObject(second)
]
);
assert_eq!(
app.multiselect_snapshot().unwrap().primary(),
Some(&Target::ScenarioObject(second))
);
mounted
.batch(
&mut app,
&[iced::Event::Window(iced::window::Event::Unfocused)],
mouse::Cursor::Unavailable,
)
.await;
let first_at = at(&mut mounted, &app, first);
mounted.click(&mut app, first_at).await;
assert_eq!(
app.multiselect_snapshot().unwrap().targets(),
&[Target::ScenarioObject(first)]
);
assert_eq!(
app.multiselect_snapshot().unwrap().primary(),
Some(&Target::ScenarioObject(first))
);
assert_eq!(app.active_diagram, Some(diagram));
assert_eq!(
app.scenario_context().unwrap().representation,
context.representation
);
assert_eq!(app.scenario_context().unwrap().visit, context.visit);
assert_eq!(stored_camera(&app), camera);
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
assert_eq!(app.document.bundle(), &before.bundle);
assert!(!app.document.can_undo() && !app.document.can_redo());
assert!(app.behavior.scenario.spacing_token().is_none());
}
@@ -436,204 +436,3 @@ async fn saved_drag_return_to_origin_preserves_redo_selection_and_original_fit_c
assert_eq!(app.project(), &redo.decoded.project);
assert_eq!(app.document.bundle(), &redo.bundle);
}
#[tokio::test]
async fn saved_first_press_spaces_unselected_message_with_existing_singleton_selection() {
use crate::diagram_usage::Target;
let (mut app, mut mounted, diagram) = setup(false).await;
let initial = id(&app, "initial_report");
let target = id(&app, "updated_report");
let context = app.scenario_context().unwrap();
let snapshot = app.multiselect_snapshot().unwrap();
let task = app.update(selection::message(
context,
&snapshot,
selection::Intent::Choose(
Target::ScenarioObject(initial),
crate::app::multiselect::Choice::Replace,
),
));
mounted.perform(&mut app, task).await;
mounted.settle(&mut app).await;
assert_eq!(
app.multiselect_snapshot().unwrap().targets(),
&[Target::ScenarioObject(initial)]
);
let before = app.document.snapshot();
let revision = app.document.revision();
let context = app.scenario_context().unwrap();
let camera = stored_camera(&app);
let projected = screen(&mut mounted, &app, Point::new(400., 252.));
let start = Point::new(projected.x.round(), projected.y.round());
let geometry = resolved(&app);
let effective = app.behavior.scenario.presentation.camera(geometry.bounds);
assert_eq!(effective.scale, 0.5);
let bounds = mounted.geometry(&app).bounds("scenario-viewport");
let world = Point::new(
(start.x - bounds.x - effective.pan.x) / effective.scale,
(start.y - bounds.y - effective.pan.y) / effective.scale,
);
let hit = geometry.scene.hit_item(world).unwrap();
assert_eq!(hit.id, target);
assert_eq!(hit.role, crate::behavior::SceneHitRole::MessageBody);
let messages = press(&mut mounted, &mut app, start).await;
assert_eq!(
messages
.iter()
.filter(|message| matches!(
message,
Message::ScenarioCanvas(CanvasEvent::Spacing(spacing::Event::Begin { .. }))
))
.count(),
1
);
assert!(
!messages
.iter()
.any(|message| matches!(message, Message::ScenarioCanvas(CanvasEvent::Selection(_))))
);
let token = app
.behavior
.scenario
.spacing_token()
.expect("first press starts saved spacing despite different selected singleton");
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
let end = start + Vector::new(0., 10.);
move_to(&mut mounted, &mut app, end).await;
mounted.settle(&mut app).await;
assert_eq!(app.behavior.scenario.spacing_token(), Some(token));
let messages = release(&mut mounted, &mut app, end).await;
assert_eq!(
messages
.iter()
.filter(|message| matches!(
message,
Message::ScenarioCanvas(CanvasEvent::Spacing(spacing::Event::Finish { .. }))
))
.count(),
1
);
let mut expected = before.decoded.project.clone();
set_expected_message(&mut expected, diagram, target, 272.);
assert_eq!(app.project(), &expected);
assert_eq!(app.document.revision(), revision + 1);
assert_eq!(
app.multiselect_snapshot().unwrap().targets(),
&[Target::ScenarioObject(target)]
);
assert_eq!(
app.multiselect_snapshot().unwrap().primary(),
Some(&Target::ScenarioObject(target))
);
assert!(app.behavior.scenario.spacing_token().is_none());
assert_eq!(stored_camera(&app), camera);
assert_eq!(
app.scenario_context().unwrap().representation,
context.representation
);
assert_eq!(app.scenario_context().unwrap().visit, context.visit);
assert_reopens_exactly(&app);
let after = app.document.snapshot();
let revision = app.document.revision();
let duplicate = release(&mut mounted, &mut app, end).await;
assert!(!duplicate.iter().any(|message| matches!(
message,
Message::ScenarioCanvas(CanvasEvent::Spacing(spacing::Event::Finish { .. }))
)));
assert_eq!(app.document.revision(), revision);
assert!(Arc::ptr_eq(&after, &app.document.snapshot()));
let task = app.update(Message::Undo);
mounted.perform(&mut app, task).await;
assert_eq!(app.project(), &before.decoded.project);
assert_eq!(app.document.bundle(), &before.bundle);
assert!(!app.document.can_undo() && app.document.can_redo());
let task = app.update(Message::Redo);
mounted.perform(&mut app, task).await;
assert_eq!(app.project(), &after.decoded.project);
assert_eq!(app.document.bundle(), &after.bundle);
}
#[tokio::test]
async fn saved_blank_left_drag_clears_selection_and_pans_on_that_same_press() {
use crate::diagram_usage::Target;
for group in [false, true] {
for same_batch in [false, true] {
let (mut app, mut mounted, _) = setup(false).await;
let first = id(&app, "initial_report");
let second = id(&app, "updated_report");
for target in [Some(first), group.then_some(second)].into_iter().flatten() {
let context = app.scenario_context().unwrap();
let snapshot = app.multiselect_snapshot().unwrap();
let task = app.update(selection::message(
context,
&snapshot,
selection::Intent::Choose(
Target::ScenarioObject(target),
crate::app::multiselect::Choice::Add,
),
));
mounted.perform(&mut app, task).await;
}
mounted.settle(&mut app).await;
assert_eq!(
app.multiselect_snapshot().unwrap().targets().len(),
if group { 2 } else { 1 }
);
let before = app.document.snapshot();
let context = app.scenario_context().unwrap();
let bounds = mounted.geometry(&app).bounds("scenario-viewport");
let start = bounds.position() + Vector::new(bounds.width - 25., bounds.height - 25.);
let delta = Vector::new(-25., -20.);
let end = start + delta;
let geometry = resolved(&app);
let camera = app.behavior.scenario.presentation.camera(geometry.bounds);
let world = Point::new(
(start.x - bounds.x - camera.pan.x) / camera.scale,
(start.y - bounds.y - camera.pan.y) / camera.scale,
);
assert!(
geometry.scene.hit_item(world).is_none(),
"test must start on actual blank geometry"
);
let stored = stored_camera(&app);
let messages = if same_batch {
mounted
.batch(
&mut app,
&[
mouse_event(mouse::Event::CursorMoved { position: start }),
mouse_event(mouse::Event::ButtonPressed(mouse::Button::Left)),
mouse_event(mouse::Event::CursorMoved { position: end }),
mouse_event(mouse::Event::ButtonReleased(mouse::Button::Left)),
],
mouse::Cursor::Available(end),
)
.await
} else {
let mut messages = press(&mut mounted, &mut app, start).await;
assert!(app.multiselect_snapshot().unwrap().targets().is_empty());
mounted.settle(&mut app).await;
messages.extend(move_to(&mut mounted, &mut app, end).await);
messages.extend(release(&mut mounted, &mut app, end).await);
messages
};
assert_eq!(messages.iter().filter(|message| matches!(message, Message::ScenarioCanvas(CanvasEvent::Selection(event)) if matches!(&event.intent, selection::Intent::Clear))).count(), 1);
assert!(app.multiselect_snapshot().unwrap().targets().is_empty());
let actual = stored_camera(&app);
let difference = actual.0 - (stored.0 + delta);
assert!(
difference.x.hypot(difference.y) < 0.01,
"same-press pan lost after Clear: group={group}, same_batch={same_batch}, {stored:?} -> {actual:?}"
);
assert_eq!(actual.1, stored.1);
assert_eq!(
app.scenario_context().unwrap().representation,
context.representation
);
assert_eq!(app.scenario_context().unwrap().visit, context.visit);
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
assert_eq!(app.document.bundle(), &before.bundle);
assert!(!app.document.can_undo() && !app.document.can_redo());
}
}
}
@@ -234,13 +234,6 @@ impl Rendering {
.collect()
}
/// Exact painted marker subjects and centers for shared selection adapters.
pub(in crate::behavior) fn selection_markers(&self) -> impl Iterator<Item = (Id, Point)> + '_ {
self.markers
.iter()
.map(|marker| (marker.event.occurrence, marker.center))
}
pub(in crate::behavior) fn marker_at(&self, point: Point) -> Option<Id> {
self.markers
.iter()
@@ -184,10 +184,6 @@ impl Selection {
}
impl Snapshot {
pub(crate) fn generation(&self) -> u64 {
self.generation
}
pub fn context(&self) -> Context {
self.context
}
+39 -165
View File
@@ -19,7 +19,6 @@ pub struct Request {
pub creation_point: Option<Position>,
pub canvas: Option<route_edit::Context>,
pub process: Option<Box<crate::behavior::topology::Context>>,
pub scenario: Option<Box<crate::behavior::scenario::Context>>,
pub source_file: Option<String>,
}
impl Request {
@@ -35,7 +34,6 @@ impl Request {
creation_point: None,
canvas: None,
process: None,
scenario: None,
source_file: None,
}
}
@@ -57,7 +55,6 @@ pub struct Opened {
page: Page,
layer: Layer,
diagram: Option<Id>,
scenario_selection: Option<crate::diagram_selection::Snapshot>,
}
impl Opened {
pub fn new(app: &App, request: Request, token: u64) -> Option<Self> {
@@ -70,18 +67,9 @@ impl Opened {
.process
.as_deref()
.is_some_and(|c| Some(*c) != app.topology_context())
|| request
.scenario
.as_deref()
.is_some_and(|c| Some(*c) != app.scenario_context())
{
return None;
}
let scenario_selection = if request.scenario.is_some() {
Some(app.multiselect_snapshot()?)
} else {
None
};
Some(Self {
token,
request,
@@ -91,7 +79,6 @@ impl Opened {
page: app.page,
layer: app.layer,
diagram: current_diagram(app),
scenario_selection,
})
}
pub fn valid(&self, app: &App) -> bool {
@@ -112,15 +99,6 @@ impl Opened {
.process
.as_deref()
.is_none_or(|c| Some(*c) == app.topology_context())
&& self
.request
.scenario
.as_deref()
.is_none_or(|c| Some(*c) == app.scenario_context())
&& self
.scenario_selection
.as_ref()
.is_none_or(|captured| app.multiselect_snapshot().as_ref() == Some(captured))
&& self
.request
.source_file
@@ -131,19 +109,9 @@ impl Opened {
fn target_matches(app: &App, request: &Request) -> bool {
use crate::diagram_usage::Target;
if request.scenario.is_some()
&& (request.canvas.is_some() || request.process.is_some() || request.placement.is_some())
{
return false;
}
let Some(target) = request.selection_target.as_deref() else {
return true;
};
if let Some(context) = request.scenario.as_deref() {
return Some(*context) == app.scenario_context()
&& matches!(target, Target::ScenarioObject(id) if *id == request.object)
&& app.multiselect_target_visible(target);
}
if (request.canvas.is_none() && request.process.is_none())
|| !app.multiselect_target_visible(target)
{
@@ -191,17 +159,6 @@ fn current_diagram(app: &App) -> Option<Id> {
if app.page == Page::Behavior {
app.topology_context()
.map(|context| context.owner)
.or_else(|| {
app.scenario_context()
.map(|context| match context.representation {
crate::behavior::scenario_layout::Representation::Derived { owner } => {
owner
}
crate::behavior::scenario_layout::Representation::Saved { diagram } => {
diagram
}
})
})
.or_else(|| app.state_context().map(|context| context.diagram))
} else {
app.active_diagram
@@ -215,56 +172,49 @@ pub fn open(object: Id, anchor: Point) -> Message {
pub fn trigger(app: &App) -> iced::Element<'_, Message> {
crate::context_menu::Area::left(
crate::views::action("Actions ▾", Message::Menu(Event::Close)),
move |anchor| Message::Menu(Event::Open(actions_request(app, anchor))),
move |anchor| {
let mut request = Request::object(app.selected, anchor);
if app.page == Page::Diagram {
request.placement = app.selected_node;
request.bend = app
.selected_bend
.filter(|(id, _)| *id == app.selected)
.map(|(_, i)| i);
request.canvas = app.route_context();
} else if app.page == Page::Behavior {
request.canvas = app.state_context();
request.process = app.topology_context().map(Box::new);
if let Some(target) = app.diagram_selection.as_ref().and_then(|s| s.primary()) {
request.selection_target = Some(Box::new(target.clone()));
request.placement = match target {
crate::diagram_usage::Target::Placement(id) => {
if let Some(node) = request
.canvas
.and_then(|c| app.project().diagrams.get(&c.diagram))
.and_then(|d| d.nodes.get(id))
{
request.object = node.element;
}
Some(*id)
}
_ => None,
};
} else if let Some(context) = request.canvas
&& let Some(node) = app.project().diagrams[&context.diagram]
.nodes
.values()
.find(|node| node.element == app.selected || node.id == app.selected)
{
request.object = node.element;
request.placement = Some(node.id);
}
}
Message::Menu(Event::Open(request))
},
)
.into()
}
fn actions_request(app: &App, anchor: Point) -> Request {
let mut request = Request::object(app.selected, anchor);
if app.page == Page::Diagram {
request.placement = app.selected_node;
request.bend = app
.selected_bend
.filter(|(id, _)| *id == app.selected)
.map(|(_, i)| i);
request.canvas = app.route_context();
} else if app.page == Page::Behavior {
request.canvas = app.state_context();
request.process = app.topology_context().map(Box::new);
request.scenario = app.scenario_context().map(Box::new);
if let Some(target) = app.diagram_selection.as_ref().and_then(|s| s.primary()) {
request.selection_target = Some(Box::new(target.clone()));
request.placement = match target {
crate::diagram_usage::Target::Placement(id) => {
if let Some(node) = request
.canvas
.and_then(|c| app.project().diagrams.get(&c.diagram))
.and_then(|d| d.nodes.get(id))
{
request.object = node.element;
}
Some(*id)
}
crate::diagram_usage::Target::ScenarioObject(id) => {
request.object = *id;
None
}
_ => None,
};
} else if let Some(context) = request.canvas
&& let Some(node) = app.project().diagrams[&context.diagram]
.nodes
.values()
.find(|node| node.element == app.selected || node.id == app.selected)
{
request.object = node.element;
request.placement = Some(node.id);
}
}
request
}
pub fn popup(app: &App) -> Option<Menu<Message>> {
let opened = app.model_menu.as_ref().filter(|menu| menu.valid(app))?;
let request = &opened.request;
@@ -311,78 +261,6 @@ pub fn popup(app: &App) -> Option<Menu<Message>> {
});
}
let mut entries = vec![Entry::title(crate::views::short(p.named(id), 38))];
if let Some(context) = request.scenario.as_deref().copied() {
use crate::app::multiselect::Choice;
use crate::behavior::scenario::{CanvasEvent, selection};
let snapshot = opened.scenario_selection.as_ref()?;
let selection_message = |intent| {
Message::ScenarioCanvas(CanvasEvent::Selection(Box::new(selection::Event {
context,
snapshot: snapshot.clone(),
intent,
})))
};
if let Some(target) = request.selection_target.as_deref() {
let selected = snapshot.targets().contains(target);
entries.push(item(
if selected {
"Remove from selection"
} else {
"Add to selection"
},
selection_message(selection::Intent::Choose(
target.clone(),
if selected {
Choice::Remove
} else {
Choice::Add
},
)),
true,
));
if snapshot.targets().len() > 1 {
if selected {
entries.push(item(
"Inspect this element",
selection_message(selection::Intent::Choose(
target.clone(),
Choice::Inspect,
)),
true,
));
}
entries.push(item(
"Select only this element",
selection_message(selection::Intent::Choose(target.clone(), Choice::Replace)),
true,
));
}
entries.push(Entry::separator());
}
// A menu on an unselected subject must not expose singleton model actions
// while the canvas still depicts a group. Selection changes are explicit.
if snapshot.targets().len() > 1 {
entries.push(Entry::title(format!(
"{} selected",
snapshot.targets().len()
)));
entries.push(Entry::title(
"Use Reorder above the canvas to change sequence order",
));
entries.push(Entry::disabled(
"Remove selected occurrences (not yet available)",
));
entries.push(Entry::title(
"Select one occurrence for its individual actions",
));
return Some(Menu {
token: opened.token,
anchor: request.anchor,
entries,
on_close: Message::Menu(Event::Close),
});
}
}
if (request.canvas.is_some() || request.process.is_some())
&& let Some(context) = app.multiselect_context()
{
@@ -920,10 +798,6 @@ fn relation_actions(kind: Kind) -> &'static [(&'static str, RelationKind, bool)]
}
}
#[cfg(test)]
#[path = "model_menu_scenario_tests.rs"]
mod scenario_tests;
#[cfg(test)]
mod tests {
use super::*;
@@ -1,303 +0,0 @@
use super::*;
use crate::{
app::multiselect::Choice,
behavior::scenario::{CanvasEvent, Context, selection},
behavior::scenario_layout::Representation,
diagram_usage::Target,
};
fn saved(project: &mut Project, owner: Id, name: &str) -> Id {
let id = Id::new_v4();
project.diagrams.insert(
id,
Diagram {
id,
alias: format!("scenario_menu_view_{}", project.diagrams.len()),
name: name.into(),
description: String::new(),
layer: Layer::Oa,
projection: ProjectionKind::Scenario { scenario: owner },
layout: None,
routes: vec![],
nodes: Default::default(),
view_options: Default::default(),
},
);
id
}
fn fixture() -> (App, [Id; 3], Id, [Id; 2]) {
let (mut project, [owner, _, _], [first, second, hidden]) =
crate::behavior::scenario::direct_tests::navigation_fixture();
let mut third = project.elements[&first].clone();
third.id = Id::new_v4();
third.alias = "third_menu_reference".into();
third.name = "Third reference".into();
let third_id = third.id;
project = execute_batch(&project, &[Command::CreateElement(third)]).unwrap();
let views = [
saved(&mut project, owner, "A"),
saved(&mut project, owner, "B"),
];
let mut app = App::with_project(project);
send(&mut app, Message::Select(owner));
send(&mut app, Message::Page(Page::Behavior));
(app, [first, second, third_id], hidden, views)
}
fn send(app: &mut App, message: Message) {
let _ = app.update(message);
}
fn choose(app: &mut App, id: Id, choice: Choice) {
let event = selection::Event {
context: app.scenario_context().unwrap(),
snapshot: app.multiselect_snapshot().unwrap(),
intent: selection::Intent::Choose(Target::ScenarioObject(id), choice),
};
send(
app,
Message::ScenarioCanvas(CanvasEvent::Selection(Box::new(event))),
);
}
fn request(app: &App, id: Id) -> Request {
let mut request = Request::object(id, Point::new(120., 80.));
request.scenario = app.scenario_context().map(Box::new);
request.selection_target = Some(Box::new(Target::ScenarioObject(id)));
request
}
fn action(app: &App, caption: &str) -> Message {
popup(app)
.unwrap()
.entries
.into_iter()
.find_map(|entry| match entry {
Entry::Action { label, message, .. } if label == caption => message,
_ => None,
})
.unwrap_or_else(|| panic!("Expected enabled Scenario menu action {caption}"))
}
#[test]
fn scenario_requests_require_exact_visible_target_and_complete_exclusive_context() {
let (mut app, [first, second, _], hidden, [view, _]) = fixture();
choose(&mut app, first, Choice::Replace);
let valid = request(&app, first);
assert!(Opened::new(&app, valid.clone(), 1).is_some());
let current = app.scenario_context().unwrap();
for captured in [
Context {
document: Id::new_v4(),
..current
},
Context {
scenario: Id::new_v4(),
..current
},
Context {
representation: Representation::Saved { diagram: view },
..current
},
Context {
visit: Id::new_v4(),
..current
},
Context {
revision: current.revision + 1,
..current
},
Context {
epoch: current.epoch + 1,
..current
},
] {
let mut stale = valid.clone();
stale.scenario = Some(Box::new(captured));
assert!(Opened::new(&app, stale, 2).is_none());
}
let mut wrong = valid.clone();
wrong.object = second;
assert!(Opened::new(&app, wrong, 2).is_none());
assert!(app.project().contains(hidden));
assert!(!app.multiselect_target_visible(&Target::ScenarioObject(hidden)));
assert!(Opened::new(&app, request(&app, hidden), 2).is_none());
let mut wrong = valid.clone();
wrong.placement = Some(Id::new_v4());
assert!(Opened::new(&app, wrong, 2).is_none());
let mut wrong = valid.clone();
wrong.canvas = Some(route_edit::Context {
document: current.document,
diagram: view,
revision: current.revision,
epoch: current.epoch,
});
assert!(Opened::new(&app, wrong, 2).is_none());
let mut wrong = valid.clone();
wrong.process = Some(Box::new(crate::behavior::topology::Context {
document: current.document,
owner: current.scenario,
revision: current.revision,
epoch: current.epoch,
}));
assert!(Opened::new(&app, wrong, 2).is_none());
let mut wrong = valid.clone();
wrong.selection_target = Some(Box::new(Target::Edge(first)));
assert!(Opened::new(&app, wrong, 2).is_none());
let mut wrong = valid;
wrong.scenario = None;
assert!(Opened::new(&app, wrong, 2).is_none());
}
#[test]
fn scenario_menu_rejects_saved_visit_roundtrip_even_with_equal_generic_snapshot() {
let (mut app, [first, second, _], _, [a, b]) = fixture();
let switch = |app: &mut App, diagram| {
let context = app.scenario_context().unwrap();
send(
app,
Message::ScenarioCanvas(CanvasEvent::Representation(
context,
Representation::Saved { diagram },
)),
);
};
switch(&mut app, a);
choose(&mut app, first, Choice::Replace);
choose(&mut app, second, Choice::Add);
let opened = Opened::new(&app, request(&app, first), 1).unwrap();
let old_context = app.scenario_context().unwrap();
let before = app.document.snapshot();
let selected = app.diagram_selection.take().unwrap();
switch(&mut app, b);
assert!(!opened.valid(&app));
switch(&mut app, a);
assert_ne!(app.scenario_context().unwrap().visit, old_context.visit);
// Restore the same shared selection and epoch deliberately. Only the full
// Scenario visit may distinguish the old request from this returned view.
app.canvas_epoch = old_context.epoch;
app.diagram_selection = Some(selected);
app.selected = second;
assert_eq!(app.multiselect_snapshot(), opened.scenario_selection);
assert!(!opened.valid(&app));
assert!(Opened::new(&app, opened.request.clone(), 2).is_none());
assert!(std::sync::Arc::ptr_eq(&before, &app.document.snapshot()));
}
#[test]
fn scenario_group_menus_capture_set_and_never_expose_partial_model_actions() {
let (mut app, [first, second, third], _, _) = fixture();
choose(&mut app, first, Choice::Replace);
choose(&mut app, second, Choice::Add);
let before = app.document.snapshot();
let capture = actions_request(&app, Point::ORIGIN);
assert_eq!(capture.object, second);
assert_eq!(
capture.selection_target.as_deref(),
Some(&Target::ScenarioObject(second))
);
assert_eq!(capture.scenario.as_deref().copied(), app.scenario_context());
assert!(capture.canvas.is_none() && capture.process.is_none() && capture.placement.is_none());
for target in [first, third] {
let captured_request = request(&app, target);
send(&mut app, Message::Menu(Event::Open(captured_request)));
let snapshot = app.multiselect_snapshot().unwrap();
let menu = popup(&app).unwrap();
assert!(
menu.entries
.iter()
.any(|e| matches!(e, Entry::Title(label) if label == "2 selected"))
);
let mut enabled = 0;
for entry in menu.entries {
match entry {
Entry::Action {
label,
message: Some(Message::Menu(Event::Invoke(_, message))),
..
} => {
let Message::ScenarioCanvas(CanvasEvent::Selection(event)) = *message else {
panic!("Scenario group menu exposed a singleton model action");
};
assert_eq!(event.context, app.scenario_context().unwrap());
assert_eq!(event.snapshot, snapshot);
let selection::Intent::Choose(actual_target, choice) = event.intent else {
panic!("Unexpected group selection intent");
};
assert_eq!(actual_target, Target::ScenarioObject(target));
assert!(matches!(
(label.as_str(), choice),
("Add to selection", Choice::Add)
| ("Remove from selection", Choice::Remove)
| ("Inspect this element", Choice::Inspect)
| ("Select only this element", Choice::Replace)
));
enabled += 1;
}
Entry::Action {
label,
message: None,
..
} => {
assert!(!label.contains("Hide"));
}
Entry::Action {
message: Some(_), ..
} => panic!("Uncaptured menu action"),
_ => {}
}
}
assert_eq!(enabled, if target == first { 3 } else { 2 });
assert_eq!(
app.diagram_selection.as_ref().unwrap().targets(),
&[
Target::ScenarioObject(first),
Target::ScenarioObject(second)
]
);
assert_eq!(app.selected, second);
}
let selected_request = request(&app, first);
send(&mut app, Message::Menu(Event::Open(selected_request)));
let inspect = action(&app, "Inspect this element");
send(&mut app, inspect);
assert_eq!(app.selected, first);
assert_eq!(
app.diagram_selection.as_ref().unwrap().targets(),
&[
Target::ScenarioObject(first),
Target::ScenarioObject(second)
]
);
let remove_request = request(&app, second);
send(&mut app, Message::Menu(Event::Open(remove_request)));
let remove = action(&app, "Remove from selection");
send(&mut app, remove);
assert_eq!(
app.diagram_selection.as_ref().unwrap().targets(),
&[Target::ScenarioObject(first)]
);
assert!(std::sync::Arc::ptr_eq(&before, &app.document.snapshot()));
assert_eq!(app.source_workspace.bundle(), before.bundle);
}
#[test]
fn scenario_menu_snapshot_rejects_selection_aba_and_singleton_actions_stay_available() {
let (mut app, [first, second, _], _, _) = fixture();
choose(&mut app, first, Choice::Replace);
let captured = request(&app, first);
send(&mut app, Message::Menu(Event::Open(captured)));
let _ = action(&app, "Properties");
let _ = action(&app, "Delete from model…");
let opened = app.model_menu.take().unwrap();
let context = app.scenario_context();
// Isolate generation admission from the adapter's additional epoch defense.
let selected = app.diagram_selection.as_mut().unwrap();
selected.add(Target::ScenarioObject(second), |_| true);
selected.remove(&Target::ScenarioObject(second));
assert_eq!(app.scenario_context(), context);
assert_eq!(app.selected, first);
assert!(!opened.valid(&app));
}
+2 -6
View File
@@ -9,8 +9,7 @@ excluded by the user's explicit request, including after networking recovers.**
## Assembled source and acceptance boundary
Combined code through **4deced53b98cd4d7649ba7aa60fa4b9b30d2f3b8**, plus the
test-only import correction from **7c2b06b63f8a0fe14b3f001996bdf965b36a4e85**, adds the reviewed
Combined code **4deced53b98cd4d7649ba7aa60fa4b9b30d2f3b8** 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)
@@ -22,10 +21,7 @@ claim combined Cargo, native, packaged or release acceptance. The independent
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. Corrected combined execution remains pending.
before running tests. Combined execution remains pending.
Live build, final-source and publication results belong in
[issue37](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/37).
+5 -10
View File
@@ -62,11 +62,9 @@ command or source schema is unnecessary. Preview and commit must consume the
same plan; tests independently construct expected geometry rather than merely
comparing those two uses of the implementation.
Process reorder and Scenario occurrence reorder change semantic sequence order.
Saved Scenario presentations can additionally store lane/message coordinates;
semantic group reordering preserves those authored records. A sequence group drag
must reorder a stable block while retaining its internal order and applicable
owner/column/operand boundaries. Existing topology
Process and Scenario layouts are derived from semantic order, not free XY node
placements. Their group drag must reorder a stable block while retaining its
internal order and applicable owner/column/operand boundaries. Existing topology
and temporal/lifecycle validation remains authoritative. Qualified reused
Process content must not silently edit the reusable definition. These adapters
are required work for the issue, not waived because ordinary diagrams are easier.
@@ -86,11 +84,8 @@ The [explicit Arrange amendment](allocation-arrange-amendment-20260923.md) defin
the bounded one-or-more work/performer footprint and diagnostic collision domain.
Its [pure planner evidence](allocation-arrange-planner-20260923.md) and the
[ordinary assignment foundation](allocation-assignment-planner-20260923.md)
establish bounded source/planner behavior. The [Allocation App/native checkpoint](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/57aec979f640ba1b8aa6fcbb7d9e26b29cf51596/harness/allocation-group-app-20260924.md)
adds bounded Windows group assignment, explicit Arrange and ordinary performer
acceptance; its limitations and source identity remain explicit. The
[Scenario reorder policy](scenario-group-reorder-policy-20260924.md) defines the
strict sibling-block increment without implying adapter or removal acceptance.
establish bounded source/planner behavior. App/native group input acceptance
remains pending.
## Related group actions
@@ -90,16 +90,6 @@ single test line. Production code is unchanged. This concrete finding supersedes
the earlier `9e99618c` build handoff; the failed compile ran no tests and remains
part of the peer's evidence. Source review and formatting did not catch it.
The first combined Windows attempt on `897d9f04` then found E0433 in the
Allocation Text Apply test added at `542b10ac`: the test's module lacked
`iced::widget::text_editor`. The current successor incorporates peer
`7c2b06b63f8a0fe14b3f001996bdf965b36a4e85`, adding that import only. Production
code remains unchanged. The failed combined compile ran no tests, lint or
product build; its 3,709 source inputs stayed unchanged and formatting passed.
[Preserved failure evidence](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/2bd0e615204be728b5c2b77cc7e28504c18a2277/harness/prerelease37-windows-gates-20260924/README.md)
records that distinct failure. The corrected successor requires fresh combined
execution; no earlier pass is substituted.
Codex-ytr83r owns the exact-candidate Windows build/package on its separate
machine. Codex-9e8r6t owns independent source/seam review and a downloaded-package
check. Codex-956atf remains the sole integration, tag and release publication owner.
@@ -1,40 +0,0 @@
# Scenario sibling-block planner: Windows foundation verification
This is the post-prerelease foundation for [issue 29](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/29), owned by Codex-9e8r6t /root on `codex/scenario-multiselect-9e8r6t-20260924`. It is excluded from the current issue 37 prerelease. No Scenario group canvas/App adapter or aggregate removal is delivered by this milestone.
## Source and behavior
Tested source **7d3d7b23e768b66b93b608bcc6b4fb1e7f972f89** adds the planner on integrated base **8ddfadf440b5c208b1adb6b211a97ba0c99cff04**. The original local port `d79a5ed37b2b64ac81d90137275fad8a97067418` used base `897d9f04`; its first compilation failed on the already identified upstream missing `text_editor` test import. Rebasing the unpublished contribution onto the owner's corrected base brought in that import. The failed run remains preserved and establishes no executed-test result.
`behavior/scenario/group_reorder.rs` admits the entire exact typed set under one actual immediate Scenario or Branch parent, validates complete edited-list integrity and structural reachability, then freezes a validated complete Project. It preserves semantic list order within the selected block, regardless of selection order. A selected destination anchor or identical result is a no-op only after admission. Other proposals are fully validated before returning one `SetSteps` command. Unsupported mixtures, different parents and invalid temporal/lifecycle outcomes refuse without pruning or dependency expansion. Advisory authoring warnings remain allowed.
The existing singleton `scenario::reorder` uses the same planner while retaining its Command-returning interface. The UI's existing singleton feedback is not claimed to distinguish a no-op; the future group adapter must do so. Saved presentation records, all other sequences, references and metadata remain untouched by the semantic operation. [Policy and adapter decision boundary](scenario-group-reorder-policy-20260924.md) define the exact scope.
## Executed Windows checks
Environment: Rust/Cargo 1.98.1, `stable-x86_64-pc-windows-gnullvm`, LLVM-MinGW 20260826; one build job and incremental compilation disabled. Commands run from the named worktree with the configured toolchain environment. No Linux/WSL build or check was run.
| Command | Outcome |
| --- | --- |
| `cargo test -p oa-desktop --bin arcadia-studio --message-format=json group_reorder -- --test-threads=1` | 20 passed, 0 failed: all 12 new Scenario cases and eight existing Process cases. |
| `cargo test -p oa-desktop --bin arcadia-studio --message-format=json scenario -- --test-threads=1` | 250 passed, 0 failed, 1 existing ignored diagnostic timing test. |
| `cargo fmt --all -- --check` | Exit 0. |
| `cargo clippy -p oa-desktop --all-targets --message-format=json -- -D warnings` | Exit 0; warnings denied. |
These filters overlap; their counts are not additive. The ignored case is `behavior::scenario::spacing::controller::timing_tests::saved_spacing_capture_prepare_20_lanes_1000_messages_3_views`, explicitly an opt-in diagnostic without a latency acceptance threshold.
The 12 new evaluators cover OA and SA root/operand stable-block order; exact selected-anchor and unchanged no-ops with redo; actual singleton Document no-op; malformed complete lists, wrong target types and omitted Branch reachability; unlisted warning children; explicit call/reply pairs; nested Fragment reply and lifetime failures; opaque local ScenarioUse bindings; conditional Delete; ALT exclusivity versus invalid PAR baseline; repeated-reply warnings; and two nonempty affected saved layouts plus an unrelated saved view. Literal whole-Project expectations are independent of planner output. Successful cases check full execution, Document state, source format/parse, saved bundle reopen and one exact Undo/Redo.
## Evidence and review
Closed raw runs, hashes, commands, captured source inputs, original helper bytes and exact Git-blob comparison receipts are packaged in [the evidence directory](scenario-group-planner-evidence-20260924/checks/README.md). Each run captures 804 relevant tracked/visible inputs, including the 71 patched vendor files and explicit compile-time fixtures; successful final runs retain the same exact input map. The gate checks before/after equality, and separate source proofs compare the map with the named commit. This is identified local build evidence, not a claim of a hermetic or independently reproduced compiler environment.
The archive is **6,416,449 bytes**, SHA-256 **211530e47aec6254ee1476081352139497091aae6fef249274c20aa1808b741f**. Its manifest preserves five closed runs (one failed, four successful), five source proofs, 43 logical files and 37 stored members across two distinct source capsules. Offline readback passes. The retained [preparation selfcheck transcript](scenario-group-planner-evidence-20260924/selfcheck-1.log) confirms exact original raw/helper/proof bytes, existing-output refusal and 11 corruption-rejection checks. `checks/selfcheck.py` is the preparation utility; it requires the original raw-run directory and is separate from the pinned offline package inventory.
The [independent planner source review](scenario-group-planner-evidence-20260924/reviews/planner-review.md) found no material defect and requested discriminating fixtures and a singleton no-op check. Its follow-up inspected the expanded 12 cases; the parent then executed the checks above. That review retains its original inspected-base and line-reference boundaries.
The final [independent delivery review](scenario-group-planner-evidence-20260924/reviews/delivery-review.md) found no material discrepancy in the closed results, executed names, claims or assembled evidence. Its offline verification passed, and all 40 archived raw/proof records matched the original captures exactly. This review does not independently rerun the compiler or accept remote publication. Original review bytes are retained through scoped Git attributes; the parent's final staging check compares every delivery file with its exact Git index bytes.
The [adapter mapping](scenario-group-planner-evidence-20260924/reviews/adapter-design.md) and [fresh design critique](scenario-group-planner-evidence-20260924/reviews/adapter-critique.md) are proposals/review, not implemented behavior. Parent synthesis accepts full Scenario event envelopes with representation-specific selection scopes. The saved-view gesture remains unresolved: exact authored Y coordinates can leave arrows in their prior positions after semantic reorder. Compare a group handle and an explicit operation-local order preview before accepting the proposed arm. Gesture retirement, selection ABA, saved spacing precedence, actual visibility, raw legacy controls and observable semantic success need adapter verification.
There is no full-workspace, native group-gesture, packaged application or release acceptance for this contribution. Existing prerelease source remains frozen independently. Live integration and subsequent work stay in issue 29, while issue 37 owns the current Windows package and separate-machine QA.
@@ -1,3 +0,0 @@
# Preserve original independent review bytes for provenance.
reviews/*.md -text whitespace=blank-at-eol,blank-at-eof,space-before-tab,cr-at-eol
selfcheck-1.log -text whitespace=blank-at-eol,blank-at-eof,space-before-tab,cr-at-eol
@@ -1,14 +0,0 @@
# Scenario group planner captured checks
These tools package explicitly named, completed runs from `CHECKOUT/.local/scenario-group-gates` without executing archived source or running Cargo. Adapted only in input-directory and manifest-kind names from the published prerelease Connection/Route evidence tools at `93d2a6f9af40061bf92cd81ba33d5648d0181cb1`.
```powershell
python -B prepare.py --root CHECKOUT --runs planner-focused-1 planner-focused-2 format-1 --output NEW_DIRECTORY
python -B NEW_DIRECTORY/verify.py
```
The manifest is authoritative for the selected runs. Failed runs remain failures: archive consistency is not a test pass. Every raw stream and receipt retains its original bytes. Identical input maps and ZIPs are stored by hash; the logical-file map preserves their original run paths. Historical capture helpers and exact source comparators are included as data, never executed by verification.
Source proofs compare every captured input with a named Git commit; the offline package verifier checks those receipts and their pinned comparator identity but does not repeat the Git comparison. The captured HEAD and the named proof commit retain their respective meanings. The verifier checks archive inventory, hashes, input ZIP contents, process/command/result consistency and executable artifact relationships. It does not establish hermetic builds, native workflow acceptance, UI integration, or release acceptance.
Use `--check-only` instead of `--output` for an in-memory consistency check. Existing output directories refuse. Python 3.11+ standard library is sufficient; `-B` avoids bytecode writes.
@@ -1,22 +0,0 @@
{
"prepare.py": {
"bytes": 6312,
"sha256": "242b5d26bee8dcac7f14a8e9556e92add336f03799c38039686a52aa23839759"
},
"verify.py": {
"bytes": 11440,
"sha256": "40d5a689202f254cc55144ada947d135a35fdf05bc182e3ec015aa64875343c0"
},
"README.md": {
"bytes": 1629,
"sha256": "3902ade7d0ee79918e6bfb68f5fab0e976e0248c3a9958b7b23eb1879ddead37"
},
"manifest.json": {
"bytes": 14400,
"sha256": "0f21337cdf3d29615b258bf1bed9d7449c359b20c2807ff0114d5d5d68946edd"
},
"evidence.zip": {
"bytes": 6416449,
"sha256": "211530e47aec6254ee1476081352139497091aae6fef249274c20aa1808b741f"
}
}
@@ -1,329 +0,0 @@
{
"schema": 1,
"kind": "scenario-group-planner-cargo-gates",
"runs": {
"planner-focused-1": {
"command": [
"cargo",
"test",
"-p",
"oa-desktop",
"--bin",
"arcadia-studio",
"--message-format=json",
"group_reorder",
"--",
"--test-threads=1"
],
"head": "d79a5ed37b2b64ac81d90137275fad8a97067418",
"exitCode": 101,
"inputCount": 804,
"helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"sourceProof": {
"commit": "d79a5ed37b2b64ac81d90137275fad8a97067418",
"pin": {
"bytes": 596,
"sha256": "93dcff027cb74e4fbd48c43c9b81b1d6d4fdadfaf4c35d12f302f4dfff1a04ca"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"planner-focused-2": {
"command": [
"cargo",
"test",
"-p",
"oa-desktop",
"--bin",
"arcadia-studio",
"--message-format=json",
"group_reorder",
"--",
"--test-threads=1"
],
"head": "7d3d7b23e768b66b93b608bcc6b4fb1e7f972f89",
"exitCode": 0,
"inputCount": 804,
"helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"sourceProof": {
"commit": "7d3d7b23e768b66b93b608bcc6b4fb1e7f972f89",
"pin": {
"bytes": 596,
"sha256": "ce40e7eeb5985eca7895f0c0e2c69f05d15d3d6133064d164e7aea4c88abfc78"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"scenario-regressions-1": {
"command": [
"cargo",
"test",
"-p",
"oa-desktop",
"--bin",
"arcadia-studio",
"--message-format=json",
"scenario",
"--",
"--test-threads=1"
],
"head": "7d3d7b23e768b66b93b608bcc6b4fb1e7f972f89",
"exitCode": 0,
"inputCount": 804,
"helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"sourceProof": {
"commit": "7d3d7b23e768b66b93b608bcc6b4fb1e7f972f89",
"pin": {
"bytes": 601,
"sha256": "61f82b2d7131ae97e4c36b7825d6dc60dbe93b1e8f8c909fa255838a8d065a78"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"format-1": {
"command": [
"cargo",
"fmt",
"--all",
"--",
"--check"
],
"head": "7d3d7b23e768b66b93b608bcc6b4fb1e7f972f89",
"exitCode": 0,
"inputCount": 804,
"helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"sourceProof": {
"commit": "7d3d7b23e768b66b93b608bcc6b4fb1e7f972f89",
"pin": {
"bytes": 587,
"sha256": "63ac957646cc9864555a1200635e8aa3c74f78e050ed04b97fd64cb75b4ea5b4"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"strict-lint-1": {
"command": [
"cargo",
"clippy",
"-p",
"oa-desktop",
"--all-targets",
"--message-format=json",
"--",
"-D",
"warnings"
],
"head": "7d3d7b23e768b66b93b608bcc6b4fb1e7f972f89",
"exitCode": 0,
"inputCount": 804,
"helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"sourceProof": {
"commit": "7d3d7b23e768b66b93b608bcc6b4fb1e7f972f89",
"pin": {
"bytes": 592,
"sha256": "9cd3e8c7065725fcf96bc738a279dc410c137b5a1b7b348e6351866314192431"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
}
},
"captureHelpers": {
"gate.py": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"source-proof.py": "helpers/c86f51d9b8465341dab73d045923ec306b15ebabf7c97b81beac7afe73bd72a1.py"
},
"logicalFiles": {
"cargo/format-1/inputs.json": "capsules/e307e14fc0ec93bc6ffedd8f7155cdb398651e31cb11cb68ff5ae8160fd507d6.json",
"cargo/format-1/inputs.zip": "capsules/2d208776732619a1a7bdd2c7275b74db5efc9e3bf5606723afc8cb3c225027fe.zip",
"cargo/format-1/process.json": "cargo/format-1/process.json",
"cargo/format-1/result.json": "cargo/format-1/result.json",
"cargo/format-1/source-proof.json": "cargo/format-1/source-proof.json",
"cargo/format-1/started.json": "cargo/format-1/started.json",
"cargo/format-1/stderr.log": "cargo/format-1/stderr.log",
"cargo/format-1/stdout.log": "cargo/format-1/stdout.log",
"cargo/planner-focused-1/inputs.json": "capsules/886dfd71d5c504069e8616115fbf32bc8191d9284bb11a334789b6b26414ff2a.json",
"cargo/planner-focused-1/inputs.zip": "capsules/78421097bc1e5c3c09feaac55c2a4cab158ba4477b43a443e38605b177ca06c2.zip",
"cargo/planner-focused-1/process.json": "cargo/planner-focused-1/process.json",
"cargo/planner-focused-1/result.json": "cargo/planner-focused-1/result.json",
"cargo/planner-focused-1/source-proof.json": "cargo/planner-focused-1/source-proof.json",
"cargo/planner-focused-1/started.json": "cargo/planner-focused-1/started.json",
"cargo/planner-focused-1/stderr.log": "cargo/planner-focused-1/stderr.log",
"cargo/planner-focused-1/stdout.log": "cargo/planner-focused-1/stdout.log",
"cargo/planner-focused-2/inputs.json": "capsules/e307e14fc0ec93bc6ffedd8f7155cdb398651e31cb11cb68ff5ae8160fd507d6.json",
"cargo/planner-focused-2/inputs.zip": "capsules/2d208776732619a1a7bdd2c7275b74db5efc9e3bf5606723afc8cb3c225027fe.zip",
"cargo/planner-focused-2/process.json": "cargo/planner-focused-2/process.json",
"cargo/planner-focused-2/result.json": "cargo/planner-focused-2/result.json",
"cargo/planner-focused-2/source-proof.json": "cargo/planner-focused-2/source-proof.json",
"cargo/planner-focused-2/started.json": "cargo/planner-focused-2/started.json",
"cargo/planner-focused-2/stderr.log": "cargo/planner-focused-2/stderr.log",
"cargo/planner-focused-2/stdout.log": "cargo/planner-focused-2/stdout.log",
"cargo/scenario-regressions-1/inputs.json": "capsules/e307e14fc0ec93bc6ffedd8f7155cdb398651e31cb11cb68ff5ae8160fd507d6.json",
"cargo/scenario-regressions-1/inputs.zip": "capsules/2d208776732619a1a7bdd2c7275b74db5efc9e3bf5606723afc8cb3c225027fe.zip",
"cargo/scenario-regressions-1/process.json": "cargo/scenario-regressions-1/process.json",
"cargo/scenario-regressions-1/result.json": "cargo/scenario-regressions-1/result.json",
"cargo/scenario-regressions-1/source-proof.json": "cargo/scenario-regressions-1/source-proof.json",
"cargo/scenario-regressions-1/started.json": "cargo/scenario-regressions-1/started.json",
"cargo/scenario-regressions-1/stderr.log": "cargo/scenario-regressions-1/stderr.log",
"cargo/scenario-regressions-1/stdout.log": "cargo/scenario-regressions-1/stdout.log",
"cargo/strict-lint-1/inputs.json": "capsules/e307e14fc0ec93bc6ffedd8f7155cdb398651e31cb11cb68ff5ae8160fd507d6.json",
"cargo/strict-lint-1/inputs.zip": "capsules/2d208776732619a1a7bdd2c7275b74db5efc9e3bf5606723afc8cb3c225027fe.zip",
"cargo/strict-lint-1/process.json": "cargo/strict-lint-1/process.json",
"cargo/strict-lint-1/result.json": "cargo/strict-lint-1/result.json",
"cargo/strict-lint-1/source-proof.json": "cargo/strict-lint-1/source-proof.json",
"cargo/strict-lint-1/started.json": "cargo/strict-lint-1/started.json",
"cargo/strict-lint-1/stderr.log": "cargo/strict-lint-1/stderr.log",
"cargo/strict-lint-1/stdout.log": "cargo/strict-lint-1/stdout.log",
"comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py",
"helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"helpers/c86f51d9b8465341dab73d045923ec306b15ebabf7c97b81beac7afe73bd72a1.py": "helpers/c86f51d9b8465341dab73d045923ec306b15ebabf7c97b81beac7afe73bd72a1.py"
},
"members": {
"capsules/2d208776732619a1a7bdd2c7275b74db5efc9e3bf5606723afc8cb3c225027fe.zip": {
"bytes": 3218365,
"sha256": "2d208776732619a1a7bdd2c7275b74db5efc9e3bf5606723afc8cb3c225027fe"
},
"capsules/78421097bc1e5c3c09feaac55c2a4cab158ba4477b43a443e38605b177ca06c2.zip": {
"bytes": 3218350,
"sha256": "78421097bc1e5c3c09feaac55c2a4cab158ba4477b43a443e38605b177ca06c2"
},
"capsules/886dfd71d5c504069e8616115fbf32bc8191d9284bb11a334789b6b26414ff2a.json": {
"bytes": 134988,
"sha256": "886dfd71d5c504069e8616115fbf32bc8191d9284bb11a334789b6b26414ff2a"
},
"capsules/e307e14fc0ec93bc6ffedd8f7155cdb398651e31cb11cb68ff5ae8160fd507d6.json": {
"bytes": 134988,
"sha256": "e307e14fc0ec93bc6ffedd8f7155cdb398651e31cb11cb68ff5ae8160fd507d6"
},
"cargo/format-1/process.json": {
"bytes": 116,
"sha256": "70ca560612155befea0c80a2cb5e21db594491bd6aab041cfb67838b589cb09b"
},
"cargo/format-1/result.json": {
"bytes": 1869,
"sha256": "ed99994b3949f4c7c098daec09f7c2c6a26604553c575ac1bc37fe7be8c1f835"
},
"cargo/format-1/source-proof.json": {
"bytes": 587,
"sha256": "63ac957646cc9864555a1200635e8aa3c74f78e050ed04b97fd64cb75b4ea5b4"
},
"cargo/format-1/started.json": {
"bytes": 1480,
"sha256": "ecb67c04dccedcd46066d001be2ded356610d4f4826c20f6ca9a778f3f7be050"
},
"cargo/format-1/stderr.log": {
"bytes": 0,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"cargo/format-1/stdout.log": {
"bytes": 0,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"cargo/planner-focused-1/process.json": {
"bytes": 206,
"sha256": "cfc8eb78e28367ef82a2f437f3b749c96599707211333ce25a9d253e2438e0ad"
},
"cargo/planner-focused-1/result.json": {
"bytes": 1993,
"sha256": "550148e118132c776621579dc31c0fea628201f392da446854dd6ccdeef2268e"
},
"cargo/planner-focused-1/source-proof.json": {
"bytes": 596,
"sha256": "93dcff027cb74e4fbd48c43c9b81b1d6d4fdadfaf4c35d12f302f4dfff1a04ca"
},
"cargo/planner-focused-1/started.json": {
"bytes": 1595,
"sha256": "1780b84f863f5947b8514f85f07faa1249e99c01dc8999fc9352cfc562c3cfa4"
},
"cargo/planner-focused-1/stderr.log": {
"bytes": 411,
"sha256": "ac34a07cf32fc7ea9db97e71e03ec4dbe96ba4d45456aac54e4910432ad6873f"
},
"cargo/planner-focused-1/stdout.log": {
"bytes": 307738,
"sha256": "32c704b9cf7c722e8e84a0f4842f953737a983a44c67374515a5ce968df71e53"
},
"cargo/planner-focused-2/process.json": {
"bytes": 206,
"sha256": "b97bb844bb9e1aa92b3a6d387a86547a36f7fe7b20483b13f9dbdf5bca1f87fa"
},
"cargo/planner-focused-2/result.json": {
"bytes": 3320,
"sha256": "a622c0e9bf572b8d23ad41c0024d5cc3e05fcb0323c464c7c50c0f694726b57e"
},
"cargo/planner-focused-2/source-proof.json": {
"bytes": 596,
"sha256": "ce40e7eeb5985eca7895f0c0e2c69f05d15d3d6133064d164e7aea4c88abfc78"
},
"cargo/planner-focused-2/started.json": {
"bytes": 1595,
"sha256": "39c0b687cb212389b4fe2f75900c7a21dc0de7f15da4583f1889d91e697fbde2"
},
"cargo/planner-focused-2/stderr.log": {
"bytes": 284,
"sha256": "8c86923ad2d9f97c60fd6c93c1d565c656f47e979819e3ded0d9cc2c11b6b810"
},
"cargo/planner-focused-2/stdout.log": {
"bytes": 295615,
"sha256": "4bcceeace911767afd99b5f58e628825453ff699782bab147de9e62c100adcc6"
},
"cargo/scenario-regressions-1/process.json": {
"bytes": 201,
"sha256": "f4fed7b6cff71de94a9eee3677bb257f79eee183a8e72a811f1d1225d0450b53"
},
"cargo/scenario-regressions-1/result.json": {
"bytes": 3314,
"sha256": "bff168dbb514672e80c1a0e3a74c4ae222ef97fc1b2dc9e0f3d5763d900fb93c"
},
"cargo/scenario-regressions-1/source-proof.json": {
"bytes": 601,
"sha256": "61f82b2d7131ae97e4c36b7825d6dc60dbe93b1e8f8c909fa255838a8d065a78"
},
"cargo/scenario-regressions-1/started.json": {
"bytes": 1590,
"sha256": "1360f7e14a269a64d040cdbddac11e23c4bd2accd59ab0805a91ff5154c88278"
},
"cargo/scenario-regressions-1/stderr.log": {
"bytes": 210,
"sha256": "504bdaa5c008c52a9462aeca71413f9933a4e9c6b0272715ba5ca916411781c1"
},
"cargo/scenario-regressions-1/stdout.log": {
"bytes": 324556,
"sha256": "423497185a1e5fdd26cac3af041dd9b95cce6a1ab7b4f8edcdbfe0d8ab28ac5f"
},
"cargo/strict-lint-1/process.json": {
"bytes": 179,
"sha256": "2db232b16369d1ac79a81f6e0ca6f7f42c11afa9c333564e2674452463f2a63b"
},
"cargo/strict-lint-1/result.json": {
"bytes": 1959,
"sha256": "116208e1d3cd913068d6b7d894a99e4afe56444c7adf14fb4271c279c7d1d7e8"
},
"cargo/strict-lint-1/source-proof.json": {
"bytes": 592,
"sha256": "9cd3e8c7065725fcf96bc738a279dc410c137b5a1b7b348e6351866314192431"
},
"cargo/strict-lint-1/started.json": {
"bytes": 1563,
"sha256": "6018965fbef137071589d0f38f1005cbe11f1c3b8d3d4feb71bfdf554d3868a0"
},
"cargo/strict-lint-1/stderr.log": {
"bytes": 381,
"sha256": "3030ae32e30806c0c92cac0d876c8284e4bbcb81899bdd73f6587df4f5a316d2"
},
"cargo/strict-lint-1/stdout.log": {
"bytes": 269439,
"sha256": "50ae306ee4baacc73c51810ba4c41e2162ce7ef137ad104165d2f30f7fe90a60"
},
"comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py": {
"bytes": 10094,
"sha256": "7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a"
},
"helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py": {
"bytes": 3778,
"sha256": "3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2"
},
"helpers/c86f51d9b8465341dab73d045923ec306b15ebabf7c97b81beac7afe73bd72a1.py": {
"bytes": 1396,
"sha256": "c86f51d9b8465341dab73d045923ec306b15ebabf7c97b81beac7afe73bd72a1"
}
}
}
@@ -1,110 +0,0 @@
"""Package explicitly selected closed gate captures without running Cargo."""
from pathlib import Path
import argparse
import json
import zipfile
from verify import RAW_FILES, pin, parsed, need, run_name, verify_data, verify_package
HERE = Path(__file__).resolve().parent
def collect(root, runs, helpers, comparators=()):
gates = root / ".local/scenario-group-gates"
candidates = [gates / name for name in ("gate.py", "source-proof.py")] + helpers
helper_bytes = [path.read_bytes() for path in candidates]
comparator_paths = [root / ".local/process-source-evidence-tools/verify.py", *comparators]
comparator_bytes = [path.read_bytes() for path in comparator_paths if path.is_file()]
payload, logical, specs = {}, {}, {}
def add(name, data):
stored = ("capsules/" + pin(data)["sha256"] + Path(name).suffix
if name.rsplit("/", 1)[1] in {"inputs.json", "inputs.zip"} else name)
need(stored not in payload or payload[stored] == data, "deduplication collision")
payload[stored] = data
logical[name] = stored
helper_catalog = {}
for name, data in zip(("gate.py", "source-proof.py"), helper_bytes):
helper = "helpers/" + pin(data)["sha256"] + ".py"
add(helper, data)
helper_catalog[name] = helper
for run in runs:
folder = gates / run
need(folder.is_dir(), "run directory missing: " + run)
paths = list(folder.iterdir())
names = {path.name for path in paths}
need(all(path.is_file() and not path.is_symlink() for path in paths)
and names in (RAW_FILES, RAW_FILES | {"source-proof.json"}), "exact closed raw files required: " + run)
raw = {path.name: path.read_bytes() for path in paths}
result = parsed(raw["result.json"])
need("finish" in result and result["inputsUnchanged"] is True, "unfinished/changed run refused: " + run)
matches = [data for data in helper_bytes if pin(data) == result["helper"]]
need(bool(matches), "exact historical helper unavailable for " + run + "; supply --helper PATH")
helper = "helpers/" + result["helper"]["sha256"] + ".py"
add(helper, matches[0])
for name, data in sorted(raw.items()):
add("cargo/" + run + "/" + name, data)
specs[run] = {key: result[key] for key in ("command", "head", "exitCode", "inputCount")}
specs[run]["helper"] = helper
if "source-proof.json" in raw:
proof = parsed(raw["source-proof.json"])
matches = [data for data in comparator_bytes if pin(data) == proof["comparator"]]
need(bool(matches), "exact source comparator unavailable for " + run + "; supply --comparator PATH")
comparator = "comparators/" + proof["comparator"]["sha256"] + ".py"
add(comparator, matches[0])
specs[run]["sourceProof"] = {"commit": proof["commit"], "pin": pin(raw["source-proof.json"]), "comparator": comparator}
manifest = {"schema": 1, "kind": "scenario-group-planner-cargo-gates", "runs": specs,
"captureHelpers": helper_catalog,
"logicalFiles": dict(sorted(logical.items())),
"members": {name: pin(data) for name, data in sorted(payload.items())}}
summary = verify_data(manifest, payload.__getitem__)
return payload, manifest, summary
def write_json(path, value):
with path.open("xb") as output:
output.write((json.dumps(value, indent=2) + "\n").encode("utf-8"))
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, required=True, help="checkout holding .local/scenario-group-gates")
parser.add_argument("--runs", nargs="+", required=True, help="explicit run names, including failures to retain")
parser.add_argument("--helper", type=Path, action="append", default=[], help="additional exact historical helper (repeatable)")
parser.add_argument("--comparator", type=Path, action="append", default=[], help="additional exact historical source comparator (repeatable)")
parser.add_argument("--output", type=Path, help="fresh, nonexistent package directory (required unless --check-only)")
parser.add_argument("--check-only", action="store_true", help="validate raw captures in memory without creating an archive")
args = parser.parse_args()
need(all(run_name(run) for run in args.runs) and len(args.runs) == len(set(args.runs)), "unique safe run names required")
need(args.check_only or args.output is not None, "--output is required unless --check-only")
if not args.check_only:
need(not args.output.exists(), "output must be a fresh directory: " + str(args.output))
payload, manifest, summary = collect(args.root.resolve(), args.runs, args.helper, args.comparator)
if args.check_only:
print("PASS raw consistency; no package written:", summary)
return
out = args.output.resolve()
out.mkdir(parents=True, exist_ok=False)
for name in ("evidence.zip", "manifest.json", "files.json"):
need(not (out / name).exists(), "existing package output refused: " + name)
for name in ("prepare.py", "verify.py", "README.md"):
data = (HERE / name).read_bytes()
destination = out / name
if destination.exists():
need(destination.read_bytes() == data, "different existing package tool: " + name)
else:
with destination.open("xb") as output:
output.write(data)
with zipfile.ZipFile(out / "evidence.zip", "x", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive:
for name, data in sorted(payload.items()):
info = zipfile.ZipInfo(name, date_time=(2026, 9, 23, 0, 0, 0))
info.compress_type = zipfile.ZIP_DEFLATED
info.external_attr = 0o100644 << 16
archive.writestr(info, data)
write_json(out / "manifest.json", manifest)
names = ("prepare.py", "verify.py", "README.md", "manifest.json", "evidence.zip")
write_json(out / "files.json", {name: pin((out / name).read_bytes()) for name in names})
need(verify_package(out) == summary, "written package differs from validated captures")
print("PASS package readback:", out, summary)
if __name__ == "__main__":
main()
@@ -1,116 +0,0 @@
"""Exercise packaging of explicit real closed runs; mutate only in-memory test copies."""
from pathlib import Path
import argparse
import contextlib
import copy
import io
import json
import subprocess
import sys
import zipfile
import prepare
from verify import RAW_FILES, need, pin, run_name, verify_data, verify_package
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, required=True)
parser.add_argument("--runs", nargs="+", required=True)
parser.add_argument("--output", type=Path, required=True, help="fresh package under this tools directory")
args = parser.parse_args()
root = args.root.resolve()
output = args.output.resolve()
need(output.is_relative_to(prepare.HERE) and output != prepare.HERE,
"selfcheck output must stay beneath the tools directory")
need(all(run_name(run) for run in args.runs) and len(args.runs) == len(set(args.runs)),
"unique explicit run names")
need(not output.exists(), "fresh selfcheck output required")
for path in prepare.HERE.glob("*.py"):
compile(path.read_bytes(), str(path), "exec")
payload, manifest, expected = prepare.collect(root, args.runs, [])
command = [sys.executable, "-B", str(prepare.HERE / "prepare.py"),
"--root", str(root), "--runs", *args.runs, "--output", str(output)]
subprocess.run(command, check=True)
need(verify_package(output) == expected, "actual ZIP readback summary")
gates = root / ".local/scenario-group-gates"
with zipfile.ZipFile(output / "evidence.zip") as archive:
for run in args.runs:
for name in RAW_FILES | ({"source-proof.json"} if "sourceProof" in manifest["runs"][run] else set()):
logical = "cargo/" + run + "/" + name
need(archive.read(manifest["logicalFiles"][logical]) == (gates / run / name).read_bytes(),
"original raw byte equality: " + logical)
for name, stored in manifest["captureHelpers"].items():
need(archive.read(stored) == (gates / name).read_bytes(), "named original helper: " + name)
repeats = subprocess.run(command, capture_output=True, text=True)
need(repeats.returncode != 0 and "fresh directory" in repeats.stderr,
"existing output must be refused")
checked = []
def rejected(label, changed, altered):
try:
with contextlib.redirect_stdout(io.StringIO()):
verify_data(changed, altered.__getitem__)
except (ValueError, KeyError, zipfile.BadZipFile):
checked.append(label)
else:
raise AssertionError("corruption accepted: " + label)
run = args.runs[0]
prefix = "cargo/" + run + "/"
# Re-pin the outer member to ensure the verifier reaches receipt relationships.
altered = dict(payload)
changed = copy.deepcopy(manifest)
stream = changed["logicalFiles"][prefix + "stdout.log"]
altered[stream] += b"\nselfcheck changed log\n"
changed["members"][stream] = pin(altered[stream])
rejected("raw stream/receipt mismatch", changed, altered)
altered = dict(payload)
changed = copy.deepcopy(manifest)
helper = changed["runs"][run]["helper"]
altered[helper] += b"\n# selfcheck changed helper\n"
changed["members"][helper] = pin(altered[helper])
rejected("helper bytes/name mismatch", changed, altered)
changed = copy.deepcopy(manifest)
changed["runs"][run]["exitCode"] = 99
rejected("run/result exit mismatch", changed, payload)
changed = copy.deepcopy(manifest)
changed["logicalFiles"][prefix + "inputs.zip"] = changed["logicalFiles"][prefix + "inputs.json"]
rejected("input archive mapping mismatch", changed, payload)
changed = copy.deepcopy(manifest)
del changed["logicalFiles"][prefix + "result.json"]
rejected("missing closed receipt", changed, payload)
for proof_run, spec in manifest["runs"].items():
if "sourceProof" not in spec:
continue
proof_path = "cargo/" + proof_run + "/source-proof.json"
for field, value in [("run", "wrong-run"), ("commit", "0"*40), ("result", {"bytes":0,"sha256":"0"*64}), ("inputCount", 1)]:
altered = dict(payload)
changed = copy.deepcopy(manifest)
proof = json.loads(altered[proof_path])
proof[field] = value
altered[proof_path] = json.dumps(proof).encode()
changed["members"][proof_path] = pin(altered[proof_path])
changed["runs"][proof_run]["sourceProof"]["pin"] = pin(altered[proof_path])
rejected("source proof " + field + " relationship", changed, altered)
changed = copy.deepcopy(manifest)
del changed["logicalFiles"][proof_path]
rejected("missing declared optional source proof", changed, payload)
altered = dict(payload)
changed = copy.deepcopy(manifest)
comparator = spec["sourceProof"]["comparator"]
altered[comparator] += b"\n# corrupted comparator\n"
changed["members"][comparator] = pin(altered[comparator])
rejected("source comparator bytes mismatch", changed, altered)
break
print("PASS original raw/optional-proof/helper archive equality, existing-output refusal, and corruption rejection:", checked)
print("Historical captured failures remain failures:", expected["recordedFailures"])
print("No Cargo, archived code, or GUI executed.")
if __name__ == "__main__":
main()
@@ -1,194 +0,0 @@
"""Offline consistency checks only; never execute or extract archived inputs."""
from pathlib import Path, PurePosixPath
from datetime import datetime
import argparse
import hashlib
import io
import json
import math
import re
import stat
import zipfile
RAW_FILES = {"started.json", "process.json", "result.json", "stdout.log", "stderr.log", "inputs.json", "inputs.zip"}
OUTER_FILES = {"prepare.py", "verify.py", "README.md", "manifest.json", "evidence.zip"}
def need(condition, message):
if not condition:
raise ValueError(message)
def pin(data):
return {"bytes": len(data), "sha256": hashlib.sha256(data).hexdigest()}
def valid_pin(value):
return (isinstance(value, dict) and set(value) == {"bytes", "sha256"}
and type(value["bytes"]) is int and value["bytes"] >= 0
and isinstance(value["sha256"], str)
and re.fullmatch(r"[0-9a-f]{64}", value["sha256"]) is not None)
def safe(name):
return (isinstance(name, str) and bool(name) and "\\" not in name and ":" not in name
and all(ord(char) >= 32 for char in name)
and not PurePosixPath(name).is_absolute()
and all(part not in ("", ".", "..") for part in name.split("/")))
def run_name(name):
return isinstance(name, str) and re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]*", name) is not None
def parsed(data):
def pairs(items):
result = {}
for key, value in items:
need(key not in result, "duplicate JSON key: " + key)
result[key] = value
return result
def constant(value):
raise ValueError("nonfinite JSON constant: " + value)
return json.loads(data, object_pairs_hook=pairs, parse_constant=constant)
def inventory(archive, expected):
names = archive.namelist()
need(len(names) == len(set(names)) and set(names) == set(expected), "exact ZIP membership")
for entry in archive.infolist():
need(safe(entry.filename) and not entry.is_dir(), "unsafe ZIP member: " + entry.filename)
need(not stat.S_ISLNK(entry.external_attr >> 16), "ZIP symlink: " + entry.filename)
need(valid_pin(expected[entry.filename]), "invalid member pin: " + entry.filename)
need(entry.file_size == expected[entry.filename]["bytes"], "ZIP recorded size: " + entry.filename)
def ordered_times(*values):
times = [datetime.fromisoformat(value) for value in values]
return all(value.utcoffset() is not None for value in times) and times == sorted(times)
def verify_data(manifest, read):
"""Shared read-only verifier for packaged bytes or prepare's in-memory payload."""
need(manifest["schema"] == 1 and manifest["kind"] == "scenario-group-planner-cargo-gates", "manifest schema")
runs, logical, stored = manifest["runs"], manifest["logicalFiles"], manifest["members"]
helpers = manifest["captureHelpers"]
need(isinstance(helpers, dict) and set(helpers) == {"gate.py", "source-proof.py"}, "capture helper names")
need(all(isinstance(name, str) and re.fullmatch(r"helpers/[0-9a-f]{64}\.py", name)
for name in helpers.values()), "capture helper mapping")
need(isinstance(runs, dict) and bool(runs) and all(run_name(run) for run in runs), "explicit run inventory")
need(all(safe(name) for name in logical) and all(safe(name) for name in stored), "unsafe manifest path")
need(set(logical.values()) == set(stored), "unreferenced or absent stored members")
for name, expected in stored.items():
need(valid_pin(expected) and pin(read(name)) == expected, "stored member hash: " + name)
expected_logical = {"cargo/" + run + "/" + name for run in runs for name in RAW_FILES}
expected_logical.update(spec["helper"] for spec in runs.values())
expected_logical.update(helpers.values())
for run, spec in runs.items():
if "sourceProof" in spec:
proof_spec = spec["sourceProof"]
need(set(proof_spec) == {"commit", "pin", "comparator"}, run + ": exact source proof spec")
need(isinstance(proof_spec["commit"], str) and re.fullmatch(r"[0-9a-f]{40}", proof_spec["commit"]), run + ": named full source commit")
need(valid_pin(proof_spec["pin"]), run + ": source proof pin")
need(isinstance(proof_spec["comparator"], str) and re.fullmatch(r"comparators/[0-9a-f]{64}\.py", proof_spec["comparator"]), run + ": comparator path")
expected_logical.update({"cargo/" + run + "/source-proof.json", proof_spec["comparator"]})
need(set(logical) == expected_logical, "exact logical raw/helper inventory")
for name, destination in logical.items():
if name.startswith("cargo/") and name.rsplit("/", 1)[1] in {"inputs.json", "inputs.zip"}:
expected = "capsules/" + stored[destination]["sha256"] + PurePosixPath(name).suffix
need(destination == expected, "input capsule mapping: " + name)
else:
need(destination == name, "only input maps/archives may be deduplicated: " + name)
def raw(name):
return read(logical[name])
for name, helper in helpers.items():
need(helper == "helpers/" + pin(raw(helper))["sha256"] + ".py", "named helper hash: " + name)
seen_capsules = set()
for run, spec in runs.items():
prefix = "cargo/" + run + "/"
start = parsed(raw(prefix + "started.json"))
process = parsed(raw(prefix + "process.json"))
result = parsed(raw(prefix + "result.json"))
required_start = {"start", "command", "cwd", "head", "scope", "inputs", "archive",
"inputCount", "environment", "helper", "cargo", "rustc"}
need(required_start <= start.keys(), run + ": incomplete start receipt")
need(all(result.get(key) == value for key, value in start.items()), run + ": start/result relationship")
command = result["command"]
need(isinstance(command, list) and command and command[0] == "cargo"
and all(isinstance(arg, str) for arg in command), run + ": Cargo command")
need(command == process["command"] == spec["command"], run + ": command relationship")
need(result["head"] == spec["head"] and re.fullmatch(r"(?:[0-9a-f]{40}|[0-9a-f]{64})", result["head"]), run + ": HEAD")
need(type(result["exitCode"]) is int and result["exitCode"] == spec["exitCode"], run + ": recorded exit")
need(result["inputsUnchanged"] is True, run + ": changed inputs refused")
need(type(result["seconds"]) in (int, float) and math.isfinite(result["seconds"])
and result["seconds"] >= 0, run + ": duration")
need(type(process["pid"]) is int and process["pid"] > 0, run + ": process identity")
need(ordered_times(start["start"], process["observed"], result["finish"]), run + ": completed timestamp order")
for field, filename in [("inputs", "inputs.json"), ("archive", "inputs.zip"), ("stdout", "stdout.log"), ("stderr", "stderr.log")]:
need(result[field] == pin(raw(prefix + filename)), run + ": " + field + " pin")
need(valid_pin(result["helper"]) and result["helper"] == pin(raw(spec["helper"])), run + ": helper pin")
need(spec["helper"] == "helpers/" + result["helper"]["sha256"] + ".py", run + ": helper mapping")
inputs = parsed(raw(prefix + "inputs.json"))
need(isinstance(inputs, dict) and all(safe(name) for name in inputs), run + ": safe input inventory")
need(type(result["inputCount"]) is int and len(inputs) == result["inputCount"] == spec["inputCount"], run + ": input counts")
if "sourceProof" in spec:
proof_spec = spec["sourceProof"]
proof_bytes = raw(prefix + "source-proof.json")
proof = parsed(proof_bytes)
need(pin(proof_bytes) == proof_spec["pin"], run + ": source proof pinned bytes")
need(proof["run"] == run and proof["commit"] == proof_spec["commit"], run + ": proof run/commit identity")
need(proof["status"] == "PASS exact captured input inventory and Git blob bytes", run + ": source proof status")
need(proof["inputCount"] == len(inputs) and proof["inputMap"] == result["inputs"], run + ": proof input relationship")
need(proof["result"] == pin(raw(prefix + "result.json")), run + ": proof result relationship")
need(ordered_times(result["finish"], proof["verified"]), run + ": proof time after closed gate")
comparator = proof_spec["comparator"]
need(valid_pin(proof["comparator"]) and pin(raw(comparator)) == proof["comparator"], run + ": comparator bytes")
need(comparator == "comparators/" + proof["comparator"]["sha256"] + ".py", run + ": comparator identity")
need(sum(name.startswith("crates/vendor/iced_widget/") for name in inputs) == 71, run + ": vendor count")
capsule = (logical[prefix + "inputs.json"], logical[prefix + "inputs.zip"])
if capsule not in seen_capsules:
with zipfile.ZipFile(io.BytesIO(raw(prefix + "inputs.zip"))) as source:
inventory(source, inputs)
for name, expected in inputs.items():
need(pin(source.read(name)) == expected, run + ": source hash " + name)
seen_capsules.add(capsule)
artifacts = []
for line in raw(prefix + "stdout.log").decode("utf-8", errors="replace").splitlines():
try:
value = json.loads(line)
except json.JSONDecodeError:
continue
if isinstance(value, dict) and value.get("reason") == "compiler-artifact" and value.get("executable"):
artifacts.append(value)
need(isinstance(result["artifacts"], list), run + ": artifact records")
need(artifacts == [record["cargo"] for record in result["artifacts"]], run + ": Cargo JSON artifact relationship")
for record in result["artifacts"]:
need(set(record) == {"cargo", "readback"}
and (record["readback"] is None or valid_pin(record["readback"])), run + ": artifact readback pin")
print(run, "recorded exit", result["exitCode"], "inputs", len(inputs), "executable records", len(artifacts))
return {"runs": len(runs), "recordedFailures": sum(spec["exitCode"] != 0 for spec in runs.values()),
"logicalFiles": len(logical), "storedMembers": len(stored), "sourceCapsulePairs": len(seen_capsules),
"captureHelpers": helpers, "sourceProofs": sum("sourceProof" in spec for spec in runs.values())}
def verify_package(root):
files = parsed((root / "files.json").read_bytes())
need(set(files) == OUTER_FILES, "outer file inventory")
for name, expected in files.items():
need(valid_pin(expected) and pin((root / name).read_bytes()) == expected, "outer hash: " + name)
manifest = parsed((root / "manifest.json").read_bytes())
with zipfile.ZipFile(root / "evidence.zip") as archive:
inventory(archive, manifest["members"])
return verify_data(manifest, archive.read)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("package", nargs="?", type=Path, default=Path(__file__).resolve().parent)
args = parser.parse_args()
print("PASS offline consistency:", verify_package(args.package))
print("Historical receipts only; no source/binary execution or native acceptance claim.")
if __name__ == "__main__":
main()
@@ -1,56 +0,0 @@
# Scenario group adapter: independent design critique
2026-09-24. Bounded read-only critique in `C:/ar-scenario-9e8r6t`, identified by the parent as source `7d3d7b2` on the combined base. Read the working agreement, user specification, multiple-selection contract, Scenario reorder policy, proposed adapter design and relevant actual source. No Cargo, Git, network, GUI or production changes. This file is the reviewer's only output. Findings below concern the proposed adapter; they do not negate or accept the separately tested pure planner.
## Assessment
The representation-specific selection Scope plus full Scenario Context envelope is the best-supported part of the proposal. It reuses existing exact-target selection while preserving saved/derived identity and visit admission. The saved-view one-shot reorder arm is **not yet justified as the preferred interaction**. It separates semantics correctly, but adds an interaction state without solving the more consequential problem: a successful semantic reorder can leave the authored messages visibly in their old positions.
The user's requirements emphasize direct graphical editing, low cognitive load and avoiding a bloated interface (`PROJECT_GOAL.md`, user-owned general description/UI). The accepted contract preserves saved presentation records during semantic reorder. Both constraints must be satisfied; neither a successful planner nor correct preserved coordinates establishes understandable interaction.
## 1. Resolve observable success before choosing the saved-view gesture
**Material design gap.** The proposed arm says “change shared order; saved positions stay unchanged,” but does not specify how the engineer sees the resulting semantic order when saved geometry stays put. `scenario_layout.rs:635` restores exact saved send/receive positions; `:437` only diagnoses source-order crossings. `behavior.rs:1576` puts insertion slots near those painted anchors and `:1807` chooses the nearest physical slot. A saved canvas is therefore not necessarily a visual ordering of semantic destinations.
Concrete falsifier: use four independent asynchronous messages with authored Y values A=200, B=300, C=400, D=500 and semantic order `[A,B,C,D]`. Select C then A, then reorder before D. The planner correctly returns `[B,A,C,D]` while all four authored Y values remain exact. The principal arrows can remain in the same places; the operation has changed semantics without the visual movement the user may expect. A frozen model comparison is necessary but cannot establish that this interaction is understandable.
There is also concrete misleading copy: `scenario/workspace.rs:726` displays “The Scenario order is unchanged” for a `SourceOrderCrossing` diagnostic. A successful semantic reorder can itself create that diagnostic. Keep action-specific “order stays unchanged” in spacing-preview feedback if accurate, but make the general presentation notice neutral, e.g. “Saved positions differ from Scenario order.” The existing order shelf already distinguishes stored positions from automatically positioned items (`workspace.rs:363`); reuse that distinction. “Authored positions are preserved” is more precise than promising every painted position stays unchanged.
**Required before adapter acceptance:** a visible exact destination (“before D in Main sequence”), a preview of the resulting semantic order, and an observable successful result after commit that does not require opening source to infer what happened. Candidate geometry may also become fallback after a valid semantic reorder while saved records remain exact; state whether that is allowed, and show the outcome rather than presenting an unexplained representation change as success. Do not expand the semantic planner into a renderer validator solely to avoid this product decision.
## 2. Compare a direct handle and operation-local order preview; the arm is not the only bounded alternative
The initial comparison omits an existing affordance: `behavior.rs:1827` defines a reorder handle, painted only in Derived mode at `:2545`. A group-aware, explicitly labelled handle could start semantic reorder directly while ordinary saved message/header drags retain spacing semantics. It avoids an armed interval, an extra click, and the need to remember that the next body drag has changed meaning. It still needs clear group scope and a keyboard/contextual equivalent. Merely enabling the current singleton handle would be incorrect.
| Candidate | Simplicity and result visibility | Recommendation |
| --- | --- | --- |
| One-shot arm followed by saved body drag | Explicit intent, but requires an extra mode and leaves physical/semantic order ambiguity. | Retain as an experiment, not the accepted preference. |
| Explicit group reorder handle in the saved view | One direct gesture and no persistent arm; saved spacing remains familiar. Saved geometry alone still does not show the changed order. | Strong simpler candidate, paired with a semantic order preview. |
| Operation-local automatic/order preview | Captures the current saved Context and exact set, presents resulting sequence order while retaining authored records and the saved view underneath. Makes destination and outcome observable. | Prototype alongside the handle; prefer a compact graphical/neighbor preview over another large homogeneous list. |
| Navigate to Automatic arrangement | Reuses existing interaction, but contractually clears group selection and forces reselection. | Acceptable explicitly limited first delivery, not full saved-view group acceptance. |
An operation preview is a pending command against the captured saved selection, not a silent representation navigation. Keep the actual saved representation/visit and live group intact; use an explicit preview/gesture lifetime, and execute only the exact captured set. Do not change the arrangement picker or transfer a group across surfaces by exception. Derived preview geometry must not retroactively authorize originally hidden or unsupported selected targets. If the chosen preview becomes a separately navigable editor, define its input identity explicitly instead of pretending it is the unchanged saved canvas.
**Bounded comparison task:** select noncontiguous A/C in the saved view, reorder before D, identify the new sequence without consulting source, and perform the next ordinary spacing operation correctly. Compare handle-plus-preview and arm-plus-preview for errors, unexpected movement, extra recovery steps and discoverability. Verify preserved authored records and exact `[B,A,C,D]` independently. A reversible prototype can resolve this choice after publishing the pure planner; no production arm is needed to obtain that evidence.
## 3. Add an explicit gesture-retirement invariant
**Missing lifetime contract, not a demonstrated implementation defect.** The proposed state list (`.local/scenario-group-adapter-design.md:41`) captures Context, selection Snapshot and tool/arm state, but does not name an App-admitted gesture identity or specify which retirement advances invalidate delayed packets. `diagram_selection.rs:170` detects context and selection-generation changes; it does not identify two gestures on the same unchanged selection. Completing a no-op or cancel followed by rearming can leave all those values equal unless an additional lifetime changes.
Specify exactly one solution: an App-owned token/generation for each admitted gesture/arm, or mandatory captured-context invalidation on every relevant retirement. A boolean “armed” check is insufficient. Rejected old packets must not disarm a newer operation. Distinguish stationary click, changed commit, no-op, refusal, Escape, outside release and focus/camera loss; each consumes only its own matching capability. Retirement may change transient gesture state without changing model/source/history/selection. Existing spacing already provides a useful bounded pattern: `spacing/controller.rs:17` plus token-bearing events, and `app/scenario_spacing.rs:26` matches the active token before mutation and advances epoch on Finish at `:52`.
Also strengthen the spacing bridge requirement at design line 56. Refusing Begin only while the live set is a group is weaker than matching captured selection identity: a delayed singleton Begin after a group was created and reduced again can pass that test. Either snapshot-match Begin/Finish, or prove and test epoch retirement for **every** selection generation change, including add/remove of a secondary member that leaves the legacy primary unchanged. Do not rely only on `self.selected` or group size.
Falsifiers: cancel A, start B with the same model/selection, deliver A's release/cancel; finish an unchanged A, rearm B, deliver duplicate A; create/remove a secondary selection member before delivering the old spacing Begin. B and the restored exact selection must remain intact, with no command or history effect from A.
## 4. Make the promised origin-return no-op explicit
The proposed evaluator promises “out/back origin remains threshold-crossed Noop” (design line 71). The planner cannot establish that: `group_reorder.rs:138` only receives a Slot and returns no-op for a selected anchor or unchanged transformed list. `behavior.rs:2043` latches threshold crossing, while `:1807` maps the final physical position to the nearest slot. A press on the body at a row's Y is not generally nearest its own slot, which is offset by 42 at `:1582`; saved anchors may be arbitrarily close or crossed. Returning to the original pointer can therefore choose an unselected neighbor and compact/move a noncontiguous group.
If origin-return no-op is intended, define it in the gesture adapter before slot planning: preserve the ever-crossed flag for selection behavior, but classify return to the captured origin using an explicit screen-space tolerance/axis policy. Admit the envelope and baseline first, then return no-op without executing. Test actual message-body, label and handle press positions in closely spaced saved geometry. Otherwise remove that stronger promise and document the destination-based behavior; do not assume the selected-anchor rule supplies it.
## Sound constraints to retain
Keep full representation/visit in the Scenario envelope and representation identity in the generic Scope; no global generic-context redesign is justified. Use actual presented typed visibility, preserve whole-set refusal and original primary, and close raw order-row/menu/singleton bypasses before showing an operational group. Clear groups on actual surface navigation as required. The current nearest-displayed-slot-then-parent-admission rule is coherent, but a preview must name the semantic destination when physical order differs.
These are source/design conclusions only. Native focus/Shift behavior, preview stability, event ordering, readability, exact history and actual usability remain unverified. The pure planner can be delivered as a bounded milestone while the saved-view interaction remains an explicit open decision.
@@ -1,74 +0,0 @@
# Scenario group adapter design against the combined source
Read-only design review for issue 29, 2026-09-24. Inspected the actual combined source in C:/ar-scenario-9e8r6t while the parent's frozen planner gate runs. This report is the only written file. No code/tests, Cargo, Git, network, or GUI operations. References below are repository-relative current-source locations, not evidence of runtime acceptance. The parent owns synthesis, integration and publication.
## Recommendation
Reuse the existing shared Selection/Snapshot data structure, but deliver Scenario selection and semantic reorder through a Scenario-specific event envelope containing the full `scenario::Context`. Map the shared selection surface to the actual representation: Derived => `Surface::Scenario(owner)`; Saved => `Surface::Saved(diagram)`. Keep `Representation` and `visit` in the Scenario envelope and captured gesture/menu; do not extend every generic Context or change diagram-usage Surface globally.
Preserve the current saved-view default interaction: header/message drags mean authored spacing. Add an explicit, clearly labelled one-shot **Reorder selected occurrences** action for semantic group reorder in a saved representation. It arms only the accepted exact selection for the next drag and visibly explains that shared order changes while saved positions remain. Derived Select can retain its ordinary semantic-reorder behavior. A saved group body drag without that explicit action must never silently reorder, move only its primary, or infer group spacing: retain the set and explain the unavailable group spacing operation after threshold crossing. Singleton saved spacing continues through its existing controller.
This is a recommended bounded product/design decision, not an already accepted user requirement. It follows the existing distinction between saved presentation edits and semantic order, avoids forcing a representation switch merely to move a group, and preserves the existing default gesture.
## What the combined source actually does
1. **Shared selection has no active Scenario adapter yet.** `app/multiselect.rs:91` boundary includes document/revision/page/active_diagram/legacy selected/placement, State identity and Process context/address. `:105` returns Process or State contexts on Behavior; it never returns a Scenario selection context. `:122` visibility similarly handles Process and projected saved Architecture/State; `:223` reconciliation falls into those same branches. `diagram_selection.rs:141` reconciles by Scope and exact visibility, while `:179` matches full generic context, generation, ordered membership and primary. `Surface::Scenario` and `Target::ScenarioObject` exist as diagram-usage identities, but their existence does not wire live group input.
2. **Scenario already has the missing lifetime authority.** `behavior/scenario.rs:117` Context contains document, owner, exact Representation, visit, revision and epoch. `:391` enter_representation generates a fresh visit on representation change or reentry; `:422` leave invalidates it. Thus A -> B -> A gets a different visit even when the Scenario owner and final saved diagram are the same. `app.rs:2073` constructs the authoritative current context. `app/scenario_diagram.rs:6` entry updates active_diagram and canvas_epoch, while reconciliation retains an invalid Saved identity rather than silently switching to Derived.
3. **The top-level update order matters.** `app.rs:1374` rejects generic selection envelopes early. `:1483` routes Spacing directly to its handler and returns; `:1501` rejects stale Scenario document/owner/representation/visit before reveal/allocation/camera reconciliation. Only specified passive events relax epoch. `:1553` onward can retire navigation/gesture state, and `:1603` performs shared selection reconciliation after ordinary dispatch. New Scenario group packets must reject both the full Scenario envelope and shared snapshot before these side effects. Adding only an inner-handler check is too late.
4. **Presentation is the actual visibility authority.** `scenario/workspace.rs:78` obtains either frozen spacing paint or `presentation::present` for the exact representation. At `:138` it configures the actual Scene, coordinate capability and full spacing stamp. A saved view can display fallback/unavailable geometry with different capabilities. Do not use `Scene::sequence` or ancestor pointers as current saved-view visibility; those are derived semantic geometry and can authorize a target absent from the actual scene. Add a small Scenario accessor returning the visible typed hit targets from the exact displayed Scene. Keep visibility separate from reorder eligibility so lifelines, operands and mixed sets can be selected/inspected and then refused as a complete movement set.
5. **Input currently knows only a singleton.** `behavior.rs:1264` SequenceInput has no modifier state or shared Snapshot. `:1290` captures one occurrence/parent; `:2293` releases either Select or singleton Reorder. Scene keeps rows/slots plus boxes/lines/activation/lifecycle primitives. `:1841` hit_item deliberately resolves marker, topmost titled box/header, exact line, activation's Call, label, then tolerant items and lanes. Reuse this exact winner for Shift and menu selection; do not build a second hit order. `:1807` picks the nearest slot first and only then checks parent; preserve that policy and expose a reason instead of choosing a farther legal slot.
6. **Saved spacing already owns precise roles.** `behavior.rs:2149` branches on Saved before semantic reorder. Only LifelineHeader and MessageBody/MessageLabel start spacing; lifecycle markers, activation segments, guides, fragments and other boxes select only. `scenario/spacing/controller.rs:17` Stamp is full Context plus camera_epoch; sessions additionally own token, initial screen/scale/pan and frozen model. `app/scenario_spacing.rs:20` checks context, camera and active token before any mutation. Do not broaden these role permissions merely because a target is in a group.
7. **Passive modifier support is already delivered by the wrapper.** `behavior/canvas_workspace/viewport.rs:152` observes ModifiersChanged before keyboard focus gates and forwards it to the child; `:361` replays held modifiers before a pointer press, including after child rebuild. Scenario currently ignores it except Delete's event modifiers. Add a local modifier field updated before gesture admission, clear on window unfocus, and never publish a focus/camera command for passive modifier events. The existing mounted regression is `scenario/workspace/tests.rs:7`.
8. **Other controls still bypass group intent.** `scenario/workspace.rs:913` recursively builds order rows whose labels emit raw `Message::Select`; arrows at `:939` emit raw `Message::Command(MoveStep)`. `app.rs:2277` handles singleton Remove; `:2511` singleton Reorder; `:3557` applies raw commands (including MoveStep) after apply(). Delete has a shared group guard at `:3605`, but its non-Process message offers Hide, which is inappropriate for Scenario occurrences. All need Scenario-aware handling before a displayed group can exist.
9. **Menus are not yet Scenario-capable.** `model_menu.rs:11` Request has canvas/process but no Scenario context. `:112` target_matches rejects selection targets without those contexts and does not admit ScenarioObject. `:171` Actions-trigger captures Process/State context only. `:264` selection menu rows publish generic packets and treat non-Process groups as placements. Merely populating diagram_selection would therefore make the existing Actions trigger fail or offer wrong actions; wire the menu deliberately.
## Lifetime alternatives
| Candidate | Concrete implementation | Tradeoff and decision |
| --- | --- | --- |
| Extend generic Context/Scope globally | Add optional Scenario representation/visit to Context, or introduce a new generic SelectionSurface variant that holds them, then update every constructor/matcher/test in Diagram, State, Process and menus. | Can express everything, but pushes a Scenario-only lifecycle into unrelated adapters and conflates usage identities with input visits. No current requirement justifies that migration. Reject for this increment. |
| Scenario event envelope plus existing generic Snapshot | `CanvasEvent::Selection { context: scenario::Context, event: ScenarioSelectionEvent }`; mutation events carry the existing immutable Snapshot. A Scenario conversion maps actual representation to an existing Surface. Match full context and Selection::matches before side effects. | Reuses the already authoritative visit mechanism, needs only a bounded Scenario adapter and small dispatch branches, preserves existing generic invariants. **Preferred.** |
| Owner-only Scope plus canvas_epoch alone | Convert full Scenario Context to `Surface::Scenario(owner)` and trust epoch to reject navigation; use bare generic selection messages. | Epoch normally changes today, but representation identity is erased, surface changes fail to clear membership, and a future/passive path can weaken the guarantee. It also leaves menus without full visit admission. Reject as an implicit assumption. |
The recommended Scope mapping means changing saved A to saved B or Automatic clears the set, as required by the current multiselect contract's “changing surface clears the set.” Legacy single-object inspection can survive a same-owner representation switch, as it already does, then seed at most one visible target on the new surface. Do not silently preserve the whole group across representation navigation. This corrects a tempting owner-scoped shortcut: Scene representation is a distinct rendered surface even when semantic membership happens to match.
Keep visit out of Selection scope so routine context refreshes do not redefine logical membership. The full Scenario envelope is still mandatory: after A -> B -> A, an old A packet cannot pass even if the user recreated identical membership/primary and the shared surface equals A. Clear selection on leave/navigation under the existing contract; a new active visit also retires any gesture, menu or explicit reorder arm.
## Minimal integration shape
- Add `scenario_context` conversion in multiselect.rs, with Scenario dispatch before the generic Saved projected-view branch. Include `self.scenario_context()` in multiselect_boundary (or the specific owner/representation/visit identity plus existing revision). Route Scenario visibility, reconciliation and primary inspection to a bounded `app/scenario_multiselect.rs` module, patterned after process_multiselect without importing qualified Process semantics.
- On reconcile, preserve an existing exact group when legacy `selected` still names its primary and the representation Scope is unchanged; prune only after ordinary accepted model changes. If primary disappears, choose the shared Selection's survivor; never retarget by definition. Explicit unrelated selection replaces the set. Inspection sets selected to the ScenarioObject ID, clears placement/bend/attachment/navigation focus as appropriate, refreshes the primary draft, and does not navigate representations. Keep property edits primary-only.
- Scene gets a captured Scenario selection envelope and an immutable model Arc (or other existing accepted-document snapshot access) for pure Prepared construction. SequenceInput group state owns full context, Snapshot, pressed target, screen origin, bounds, pan/scale, camera_epoch, threshold-crossed flag, Prepared Result, last exact Slot, and cached Plan/refusal. These fields are local to Scenario, not a new shared gesture framework.
- Shift toggles the exact hit and captures the event before either spacing or reorder starts. Plain unselected press replaces. Plain press on a selected member freezes the complete set and primary; stationary release collapses to that member; once threshold is crossed, refusal, cancel, origin return and selected-anchor Noop keep both unchanged. Do not inspect the pressed member before knowing the outcome, because Details layout changes can change hit geometry. Background clear can coexist with the existing background pan only if it does not capture the semantic hit path.
- For each changed nearest Slot, call the frozen Prepared planner once and cache its result; exact same parent/anchor identity is the cache key. Preview must show selected count, destination and no-change/refusal, not a singleton ghost that implies only the primary will move. Fragment interior slots can produce a deliberate operand refusal.
- At release, first compare full current Context (including representation/visit), shared Snapshot generation/membership/primary, exact current visible membership, camera lifetime, bounds and tool/arm state. Refuse before any reconcile can prune the old set. Recompute using the same captured project/selection or prove current model equals that accepted revision; execute one admitted command through Document only for Change. Do not use `apply()` to silently apply pending forms during this operation. On success restore the original primary before shared reconciliation: App::execute at `app.rs:1033` receives SetSteps' parent as the core selected result and calls sync; blindly trusting that returned primary would collapse or misinspect the group. `sync` does not itself call reconcile_multiselect, so restore/inspect immediately before the outer reconciliation. Noop bypasses execute and navigation/reveal retirement.
- Add Scenario group envelope checks alongside the existing early checks in App::update, not just CanvasEvent::context. Any bare generic DiagramSelection packet while Scenario is active should refuse unless it entered through the full Scenario wrapper; otherwise generic Choose/Clear remains an unguarded representation/visit channel. This applies equally to menu-generated selection packets.
## Saved spacing versus explicit semantic reorder
Two viable bounded choices were compared:
1. **Require switching to Automatic arrangement.** Existing representation picker (`workspace.rs:210`) already exposes it, Derived already reorders, and saved spacing remains untouched. This has the least new input mode but switching rendered surfaces must clear the group under the accepted selection contract, requiring reselection. Preserving a group automatically to make this shortcut convenient would need a contract amendment. It also changes the working view when the user only asked to change order.
2. **Explicit one-shot semantic reorder in the current saved view (preferred).** Show a selected-count action such as “Reorder selected occurrences”; on activation capture full Context + Snapshot and validate the whole planner baseline. Display “Drag a selected occurrence to change shared order; saved positions stay unchanged” and Cancel. Only this armed gesture can outrank saved spacing. Complete or cancel disarms it; changing selection, tool, view, visit, revision, focus/camera or pending state also disarms. The default remains Select / move messages and lifelines. This adds one bounded Scenario state rather than changing meaning based on group size or Shift (Shift remains selection only).
For singleton normal saved drags, preserve the existing header/message spacing precedence and exact hit-role checks. For a multi-selection normal saved drag, capture a selection-owned pending press: stationary release follows the ordinary collapse rule; threshold crossing reports “Moving saved positions as a group is unavailable; choose Reorder selected occurrences to change sequence order.” No spacing Begin should be dispatched for only its primary. Unsupported sets (including lanes, Branch headers, cross-parent members) remain selectable but the explicit reorder action refuses the complete set with the planner reason. Do not expand role eligibility for activation/lifecycle hits in the ordinary saved-spacing mode.
The `scenario_spacing` early return bypasses the outer reconciliation. Once group selection exists, its Click/Commit paths must deliberately reconcile/inspect accepted singleton identity or use the Scenario selection adapter, and unchanged/failed spacing must preserve the captured membership/primary. Capturing a selection Snapshot with the spacing interaction or refusing Begin when the live set is a group prevents a queued singleton spacing packet from becoming a partial group edit after a selection change. Full stamp/token alone does not include selection generation. Do not gratuitously reset spacing camera/preview ownership by routing its packets through ordinary navigation.
A simpler first implementation may choose option 1 explicitly, but must label group reorder as Automatic-view-only and require reselection; it cannot claim full saved-view group operation acceptance. The preferred option provides the intended semantic action without changing presentation records or the active representation.
## Controls and menus that must be closed before enabling group input
- Add full Scenario Context (and group Snapshot where appropriate) to contextual Request and validate it in Opened::new/valid plus target_matches. Admit only exact ScenarioObject with matching object, no placement, and actual represented visibility. Actions-trigger and right-click should capture without replacing; Add/Remove/Inspect/Select-only publish Scenario-wrapped events. Group menu offers the semantic reorder action and explains aggregate removal/Hide limits; no placement Hide.
- Replace Scenario order-row arrows with captured Scenario actions, or disable them for groups and add an early refusal for stale `Message::Command(MoveStep)` while a Scenario group is current. Raw index packets are not safe against changed order/visit; a captured parent and occurrence/destination identity is better even for singleton arrows. Row selection should be captured, not delayed raw `Message::Select`; otherwise old Details rows can navigate after a representation switch. Group row arrows must never apply to one displayed member.
- Mirror Process early singleton guards for Scenario Select/Remove/Reorder, including stale menu/Delete packets, before reveal/camera mutation. Normal allowed group selection changes arrive only in the new envelope. Existing aggregate removal is separate: reject group Remove/Delete with an accurate reason, not “Hide selected.” Keep singleton deletion's existing review.
- Guard new selection, arm and reorder against source/property/behavior drafts, modal/rename/busy/read-only state, coordinate-unavailable fallback and wrong tool. Allow safe visual selection only where existing capabilities permit; a fallback hit is not movement authorization. Keep open-definition navigation and representation changes explicit boundaries.
## Discriminating acceptance before integration
- A/B/A same-owner saved visits: stale Choose, Clear, Collapse, menu invoke, arm, reorder release, singleton Remove/MoveStep, and spacing Finish are inert before model/source/history/selection/primary/camera/reveal changes; include same-membership recreation and same owner/revision cases.
- Native/mounted first Shift press from text/Properties focus and after busy child replacement; release/repress of Shift; no focus theft from passive modifiers. Hit the same subject at message body/label, activation, lifecycle marker, Fragment header and Branch header; compare actual hit identity with selection and support/refusal.
- Noncontiguous C/A selection and pressed-member variants preserve semantic order and original primary on drag; stationary click collapses, selected-anchor Noop keeps redo, out/back origin remains threshold-crossed Noop, Escape cancels before clearing, and rejected member sets never move a subset.
- Saved default header/message singleton spacing still updates only coordinates; normal group drag refuses without changing any coordinate/order or primary. Armed saved semantic drag changes only SetSteps and preserves both affected/unrelated authored layouts; default spacing resumes afterward. Camera/visit/selection changes cancel both kinds of captured input as intended.
- Stale toolbar/index commands and context-menu Add/Remove/Inspect do not bypass envelope admission; property edits address only primary. Pending forms remain untouched on group refusal. Exact saved/derived surface changes clear group membership as specified.
- Use the current planner's complete model/source/history oracle for successful App operations, plus exact selection/primary and zero-history refusal/no-op checks. Reuse the existing spacing/controller and mounted passive-focus regression evidence; rerun affected workflows after changes. Source design alone establishes none of these runtime outcomes.
@@ -1,111 +0,0 @@
# Scenario planner delivery record: bounded independent review
2026-09-24. Reviewed the delivery documentation and four closed raw gates while strict lint was still outside this review's acceptance scope. HEAD independently read as `7d3d7b23e768b66b93b608bcc6b4fb1e7f972f89`, parent `8ddfadf440b5c208b1adb6b211a97ba0c99cff04`. Only this report was written. No Cargo, Git mutation, network, GUI, package assembly or publication was performed.
## Result
No material finding in the reviewed planner claims, test counts, source identity, copied reviews or available relative documentation links. The record accurately limits delivery to the semantic planner foundation and its singleton wrapper. It does not claim a new Scenario group App/canvas adapter, aggregate removal, native gesture acceptance, full-workspace verification, Linux verification, packaged application or release acceptance.
The strict lint result and the not-yet-assembled checks package remain explicit delivery gates outside this review. The prose “are packaged” and checks/README link are prospective at this snapshot: the parent has explicitly reserved package construction/verification until lint closes. Do not publish that statement with a missing package. This is the known excluded boundary, not a finding against the four closed gates.
## Exact records inspected
Documentation:
- `harness/scenario-group-planner-20260924.md` (complete record).
- `harness/scenario-group-reorder-policy-20260924.md` (complete amended policy, including adapter decision boundary).
- `harness/multiselect-contract-20260923.md` (linked contract and relative-link targets).
- `harness/scenario-group-planner-evidence-20260924/reviews/planner-review.md` (complete initial review and expanded-test follow-up).
- `harness/scenario-group-planner-evidence-20260924/reviews/adapter-design.md` (reused prior full read; verified SHA-256 exact copy of the already reviewed `.local/scenario-group-adapter-design.md`).
- `harness/scenario-group-planner-evidence-20260924/reviews/adapter-critique.md` (complete critique).
Source:
- `crates/oa-desktop/src/behavior/scenario/group_reorder.rs`, `group_reorder/tests.rs`, and singleton wrapper in `behavior/scenario.rs`; previously reviewed full source was checked against current HEAD with no working-tree delta. Current test declarations and planner admission/command sites were reread.
- Exact parent identity and the `897d9f04` -> `8ddfadf4` diff in `crates/oa-desktop/src/app/allocation_drag_tests/group.rs`, which adds `use iced::widget::text_editor;`.
Closed runs under `.local/scenario-group-gates/`:
- `planner-focused-1/{result.json,source-proof.json,stdout.log,stderr.log,inputs.json,inputs.zip}`; input map/archive/log bytes independently hashed and sized against result pins. Parsed compile diagnostics, no test execution result.
- `planner-focused-2/{started.json,result.json,source-proof.json,stdout.log,stderr.log,inputs.json,inputs.zip}`; input inventory parsed, complete executed test names extracted, raw files independently hashed/sized.
- `scenario-regressions-1/{result.json,source-proof.json,stdout.log,stderr.log,inputs.json,inputs.zip}`; complete test status lines independently counted, ignored name/reason inspected, raw files independently hashed/sized.
- `format-1/{result.json,source-proof.json,stdout.log,stderr.log,inputs.json,inputs.zip}`; zero exit and empty logs inspected, raw files independently hashed/sized.
- `.local/scenario-group-gates/gate.py` and `source-proof.py`; the captured gate helper hash matches the current 3778-byte helper. The relevant source-comparator implementation in `.local/process-source-evidence-tools/verify.py` was read. Source-proof receipts were checked against result hashes, but the 804-file Git-blob comparator was not independently rerun in this bounded review.
The three successful runs all name `7d3d7b23...`, report `inputsUnchanged: true`, and have the same 804-entry input-map hash `e307e14fc0ec93bc6ffedd8f7155cdb398651e31cb11cb68ff5ae8160fd507d6`. The map contains 71 patched iced_widget inputs and both named explicit compile-time fixtures. The three archives have the same SHA-256 `2d208776732619a1a7bdd2c7275b74db5efc9e3bf5606723afc8cb3c225027fe`. The failed run has its distinct `d79a5ed...` source and map; no accidental substitution of the passing source was observed.
All four input-map/archive/stdout/stderr byte lengths and SHA-256 values match their result records. All four source-proof receipts pin their actual result bytes and identify their expected commits. The two successful test runs name the same 160743936-byte test executable and SHA-256 `48d0ee9505573e9aabd5d175b159275e12b75ddd7f261f89bab7b71106df14c3`; the first compilation is fresh=false and the broader run reuses it fresh=true. This supports the claimed run identity; it is not an independent compiler-environment reproduction.
## Executed names and outcomes
`planner-focused-1`: exit 101; three E0433 diagnostics for the missing `text_editor` module at allocation_drag_tests/group.rs lines 226, 229 and 230. No test result or executable artifact. The delivery record correctly preserves this as a compilation failure rather than an executed-test result.
`planner-focused-2`: raw result is 20 passed, 0 failed, 0 ignored, 1527 filtered out. Exactly 12 names have prefix `behavior::scenario::group_reorder::tests::`; these match all 12 current `#[test]` declarations one-for-one:
- admission_rejects_every_invalid_member_list_and_destination_before_noop
- call_reply_moves_as_explicit_stable_pair_and_invalid_baseline_refuses_noop
- conditional_delete_rejects_moving_an_earlier_use_after_the_fragment
- exclusive_operand_move_is_legal_but_parallel_baseline_is_invalid
- nested_fragment_descendant_reply_is_validated_and_subtree_preserved
- nested_lifetime_and_opaque_local_bindings_are_checked_without_expansion
- repeated_reply_warning_does_not_block_independent_sibling_reorder
- root_and_operand_use_semantic_block_order_with_exact_history
- selected_anchor_and_exact_noop_preserve_redo_and_frozen_snapshot
- semantic_reorder_preserves_affected_and_unrelated_authored_layouts
- singleton_wrapper_noop_preserves_existing_redo_and_source
- unlisted_warning_child_is_preserved_but_cannot_be_selected
Exactly eight names have prefix `behavior::topology::group_reorder::tests::`, all successful:
- process_group_columns_accept_all_local_card_kinds_and_different_heights
- process_group_columns_end_gaps_and_selected_anchor_noops
- process_group_columns_full_order_admission_precedes_selected_anchor_noop
- process_group_columns_legacy_exchange_order_refuses_cross_rank_group
- process_group_columns_parallel_links_keep_exact_ranks_and_relation_records
- process_group_columns_refuse_entire_invalid_typed_sets_before_noop
- process_group_columns_renderer_omits_links_to_unlisted_nodes_without_panicking
- process_group_columns_reverse_noncontiguous_selection_preserves_project_and_history
`scenario-regressions-1`: 251 discovered; independent raw status-line count is 250 ok and one ignored, agreeing with the final summary (0 failed, 1296 filtered out). Ignored: `behavior::scenario::spacing::controller::timing_tests::saved_spacing_capture_prepare_20_lanes_1000_messages_3_views`, with the logged opt-in diagnostic/no-latency-threshold reason. The report correctly says filtered counts overlap and must not be added.
`format-1`: exit 0 with empty stdout/stderr and unchanged input map. No inferred test-execution claim.
Rust/Cargo 1.98.1, stable-x86_64-pc-windows-gnullvm, CARGO_BUILD_JOBS=1 and CARGO_INCREMENTAL=0 are directly recorded. The additional LLVM-MinGW distribution version in the prose is parent-provided environment context, not independently established by these four gate records: they do not capture PATH/linker identity. This does not undermine their recorded outcomes; retain that distinction if describing reproducibility strength.
## Claim and link readback
Planner behavior matches source: complete typed/list/reachability checks precede baseline validation, selected-anchor/no-change returns occur only after that preparation, semantic block order comes from the original list, and changes emit only a validated SetSteps. The whole-Project oracle, source parse/format, bundle reopen and exact history helper support the stated successful transformation coverage. Singleton no-op is separately executed through Document with existing redo. The report does not turn these into App membership/primary or gesture guarantees.
The copied original reviews retain their historical base/line boundaries and read-only limits; the main record correctly states that the parent subsequently executed the checks. Planner-review and adapter-design copies match their local originals byte-for-byte. The independent critique's observable-result, gesture-retirement, spacing selection ABA and origin-return issues are reflected in the amended policy as future adapter obligations. The one-shot saved reorder arm is explicitly unresolved rather than silently accepted; comparing it with a handle/order preview preserves the critique.
Every checked relative Markdown link from the delivery record, policy and multiselect contract exists except `scenario-group-planner-evidence-20260924/checks/README.md`, the expressly pending package. Remote issue/evidence links were not fetched under this task's no-network scope. Copied reviews use historical plain-text source/local paths as provenance, not a false claim that those local paths are accessible to remote peers.
The parent may proceed to final lint-result incorporation and independent package verification. This report accepts neither the pending strict-lint gate nor package contents/publication, and does not close issue 29's adapter/removal work or alter issue 37's frozen release.
## Final extension: closed lint and assembled offline package
The parent subsequently extended this review to the closed strict-lint run and assembled checks package. This section supersedes the earlier pending-lint/package boundary; remote publication, App/native behavior and release acceptance remain outside scope.
Inspected additional records:
- `.local/scenario-group-gates/strict-lint-1/result.json`, `source-proof.json`, `stderr.log`, and the stdout build-finished record.
- `harness/scenario-group-planner-evidence-20260924/checks/{README.md,verify.py,files.json,manifest.json,evidence.zip}` through source inspection, direct hashing and offline execution.
- Updated `harness/scenario-group-planner-20260924.md` and `harness/scenario-group-planner-evidence-20260924/selfcheck-1.log`.
Strict lint closed at exit 0, `inputsUnchanged: true`, 804 inputs, source HEAD/proof `7d3d7b23...`, the same passing input-map/archive pins, and raw `build-finished` success=true. Its recorded command is exactly `cargo clippy -p oa-desktop --all-targets --message-format=json -- -D warnings`. The updated delivery table accurately reports warnings denied; no further Cargo was run by this reviewer.
Independently measured archive: 6,416,449 bytes, SHA-256 `211530e47aec6254ee1476081352139497091aae6fef249274c20aa1808b741f`, matching the delivery record and parent handoff.
After reading verify.py, executed only the standalone offline verifier with the full bundled Python runtime:
`C:/Users/User/.cache/codex-runtimes/codex-primary-runtime/dependencies/python/python.exe -B harness/scenario-group-planner-evidence-20260924/checks/verify.py`
Exit 0. Reported five runs, one recorded failure, 43 logical files, 37 stored members, two source capsule pairs and five source proofs. It verifies outer pins, exact archive member names, nested source archive contents, recorded commands/process/result relationships, helper/comparator/proof byte identities and executable-record relationships. Historical archived helpers, source and binaries were not executed or extracted. The comparator receipts remain evidence of the parent's Git comparison, not a rerun of that comparison by the offline verifier.
Separately compared all eight archived raw/proof records for each of the five runs against the original `.local/scenario-group-gates` files: **40 exact byte matches**. Independently recounted the packaged focused stdout names: **12 Scenario + 8 Process successful tests**. Thus the tested names/counts reviewed earlier are the ones actually packaged, not merely those in a separate local log.
The retained selfcheck transcript reports original raw/optional-proof/helper equality, existing-output refusal and exactly 11 named corruption rejections. I read this transcript; I did not rerun the preparation selfcheck, since it creates temporary package outputs. `checks/selfcheck.py` remains explicitly outside the pinned offline inventory, consistent with the report; the official verifier does not require it. All relative links in the updated delivery record now resolve, including checks/README and the selfcheck log.
One initial reviewer attempt used LLVM-MinGW's bundled Python 3.14, which lacks zlib and failed before reading compressed members. Retrying with the full available runtime passed. This is a verifier-runtime limitation, not a package finding; no install or environment modification was needed.
**Final bounded assessment:** no material findings. Closed gate claims and the assembled local evidence package are internally consistent and accurately bounded to the planner foundation. Suitable for the parent's delivery/publication step after its ordinary coordination; this review does not claim that remote publication has happened, does not accept any Scenario group UI/gesture implementation, and does not modify the independent prerelease scope.
@@ -1,47 +0,0 @@
# Scenario group planner: independent source review
Reviewed 2026-09-24 for the parent session's issue 29 contribution. Scope: staged port of preserved WIP `763f39d` onto combined base `897d9f049406d5be597543eb0a6755b796259302`, working branch identified by parent as `codex/scenario-multiselect-9e8r6t-20260924`. HEAD was independently checked at that base. This is local-only post-prerelease work; it does not revise or accept the independently frozen issue 37 release.
Read AGENTS.md, PROJECT_GOAL.md, staged changes, the accepted policy and prior independent policy review, relevant core execution/validation/layout code, Document execution/history, and the existing singleton App call path. No Cargo, GUI, source/test changes, Git mutation, Gitea operations, or remote publication were performed. This report is the only file written. The seven staged planner tests were inspected, not executed; parent is independently extending the suite.
## Result
No material correctness defect or combined-schema incompatibility found in the reviewed pure planner. This is a bounded source-review result, not implementation, native workflow, integration, or release acceptance. No change to the strict same-parent/no-dependency-expansion policy is needed.
## Evidence
- `crates/oa-desktop/src/behavior/scenario/group_reorder.rs:20` admits actual Exchange, Action, State, ScenarioUse and Fragment bodies, rather than generic Occurrence kind. `:60` examines every target and rejects foreign variants, duplicate IDs, missing elements and ineligible bodies without filtering the submitted set. `:67` establishes one exact immediate parent; an empty set refuses at `:73`.
- `group_reorder.rs:32` follows only listed Fragment steps and their listed, correctly parented Branch bodies, with a visited set. Therefore an omitted operand or an unlisted containing Fragment cannot manufacture reachability through ancestry. `:77` then checks the entire edited sequence for unique, existing, eligible, correctly parented entries; selected membership is checked at `:85`. It intentionally does not require every parented child to be listed, preserving the supported warning-only draft state.
- `group_reorder.rs:90` validates the immutable full baseline before a Prepared value can exist. `:95` checks destination parent and `:99` anchor identity before selected-anchor Noop at `:102`. A selected anchor retains the original noncontiguous list, rather than compacting the block. Both block and remainder at `:104-106` are filtered from original semantic order; BTreeSet iteration/click order never determines order. This implements `[A,B,C,D]`, selection C/A, before D => `[B,A,C,D]`.
- `group_reorder.rs:109` executes the full candidate with `execute_batch`; `crates/oa-core/src/session.rs:292` gates only Error diagnostics, and `:304` clones/applies/validates the entire model. Thus descendant replies and lifetime uses participate through the existing validator, while missing-reference and loop warnings remain advisory. Relevant nested semantics are `validation.rs:1493` (guaranteed order), `:1520` (possible order and ALT/PAR distinction), `:1600` (local lifeline uses including opaque ScenarioUse bindings), and `:1781` (Create/Delete lifetime checks).
- `session.rs:739` replaces only the addressed steps list. The planner emits no other command. `references.rs:448` performs no deletion cleanup without pending deletions; `data.rs:378` returns without pruning when inheritance is unchanged. This supports preservation of branches, descendants, bindings, correlations, IDs, relations and other records.
- New combined saved Scenario layout data is compatible with this operation: `crates/oa-core/src/scenario_layout.rs:102` computes structurally reachable message membership, and `:154` validates identity/ownership/address/finite-coordinate constraints rather than imposing saved-Y ordering. Same-parent permutation preserves that membership. The planner does not rewrite authored lanes/message positions or unrelated views. A rendered-layout usability check remains separate from structural validity.
- The singleton wrapper at `crates/oa-desktop/src/behavior/scenario.rs:1251` retains its existing Command-returning interface, forwarding a real change or returning an identical SetSteps for Noop. Its same-list result matches old singleton reorder semantics; the tighter type/reachability/baseline checks are deliberate policy enforcement. Source preserves camera, focus, spacing and presentation imports/modules from the combined base.
- Tests `group_reorder/tests.rs:27-49` use a literal expected parent list applied to a cloned complete Project, then compare full candidate/Document state, source format/parse, bundle reopen, and one Undo/Redo. This is stronger than checking only selected positions. The source review confirms the test design, not their result.
## Remaining evidence and adapter boundaries
1. Parent's already planned discriminating conditional Delete, ALT-vs-PAR, warning and saved-layout fixtures remain necessary before planner acceptance. Existing positive tests contain no authored saved layout records, so full-Project equality alone does not exercise preservation of that new data. Include nonempty layouts on affected and unrelated saved Scenario views, plus metadata/value/reference records, then require exact preservation outside the chosen parent list.
2. Add a bounded singleton no-op regression while touching its wrapper: with an existing redo branch, obtain the identical SetSteps via `scenario::reorder`, execute it through Document, and verify exact bundle/revision/history. The existing no-op test at `group_reorder/tests.rs:71-91` intentionally calls only `Prepared::plan`; it cannot establish behavior of the wrapper's actual Command execution. `document.rs:322` recompiles and `:399` suppresses equal-bundle commits, so source suggests preservation, but it is not exercised by that test. This is an evidence gap, not a demonstrated regression.
3. The pure planner has no document revision, surface, exact rendered visibility or selection-generation identity. Its owned Arc snapshot correctly freezes semantics, but a Change command must not be executed against a changed live model. The forthcoming App adapter must perform the policy's full snapshot/membership refusal before reconciliation or execution, and preserve primary/selection on refusal/Noop. Do not infer those guarantees from the current source-only planner tests.
4. Preserve a genuine Noop path in the forthcoming group adapter. Existing singleton `app.rs:2514` executes a Command and unconditionally reports a reorder on success, including an unchanged Command; this predates the port and is not a new planner defect. The adapter should provide explicit no-change/refusal feedback and must not turn selected-anchor Noop into a new history transaction or claim an undoable move.
5. Build and execute the port plus existing Scenario direct/workspace regression tests against the identified combined source. Native acceptance must cover nearest-slot parent refusal, selected Fragment interiors, camera/spacing interaction, saved-layout rendering, stable whole-set preview/commit, cancellation and exact one-step history. No group UI adapter exists in the reviewed diff; no aggregate removal acceptance is implied.
No production fix is requested from this review. Follow up on the bounded evidence above and separately review the adapter once present; do not broaden to cross-parent movement, selected-subset pruning, parent/child normalization or automatic dependency closure.
## Follow-up: expanded staged evaluator readback
The parent requested a second read after formatting and extending the suite to 12 test families, before the first focused execution gate. I reread the new tests and policy/contract changes. No Cargo/GUI or code/test edits were performed. The earlier line numbers identify the original unformatted port; the locations below identify this formatted readback.
No concrete fixture/oracle defect found. Proceed to the focused gate; passing execution remains required and is not asserted here.
- `group_reorder/tests.rs:132`: the literal root/operand stable-block oracle now runs in both OA and SA. `behavior/topology.rs:572` creates each occurrence in its actual parent's layer, so the SA cases are genuinely SA trees rather than OA bodies hidden under an SA owner.
- `group_reorder/tests.rs:155`: both selected-anchor and unchanged-unselected-anchor singleton cases obtain a Command from the modified wrapper and execute it through Document after Undo. Exact project, bundle, revision, empty past and preserved redo checks now cover the earlier singleton evidence gap. Nested operands are also covered.
- `group_reorder/tests.rs:489`: conditional Delete fixture keeps the original Call/Reply before the later ALT fragment, then validates the complete baseline. Moving the earlier Action/X block to append is therefore the operation that introduces the post-delete use; the asserted `lifeline_lifetime` error is not merely inherited from an invalid starting model.
- `group_reorder/tests.rs:516`: the ALT fixture has separate actual operand lists for Delete and Use/X/Y. The literal expected right list `[Y,Use,X]` follows semantic block order although selection is X/Use. The PAR control correctly asserts baseline `lifeline_lifetime` and preparation refusal, without claiming a valid reorder caused the PAR error.
- `group_reorder/tests.rs:558`: Reply is removed from the root list, reparented into the Loop operand and listed there exactly once. The outer Call still precedes its containing fragment. The test checks that `reply_repetition` is attached to that Reply with Warning severity in both original and literal expected models. `[Reply,X,Y,Z]`, selected Z/X, append => `[Reply,Y,X,Z]` is the correct stable-order oracle. Preparation and full transaction independently require no Error diagnostics.
- `group_reorder/tests.rs:599`: two saved views of the affected Scenario carry distinct, nonempty lane and Call/Reply position records plus metadata. A third saved view belongs to a separate Scenario and contains an actual local Timer with sender lifeline and absent receiver, which matches the existing timer contract. All are admitted before planning. The only intended difference in the expected complete Project is the affected root list `[X,Y,Call,Reply]`; exact_transaction additionally checks format/parse, bundle reopen, and exact Undo/Redo. This now exercises the combined saved-layout preservation gap rather than relying on empty/default records.
- Policy explicitly distinguishes semantic ordering from authored Scenario presentation, preserves stored coordinates/metadata/views, and requires representation plus visit-lifetime admission, including a same-owner round trip. The contract mirrors that distinction. These changes resolve the stale assumption that every Scenario presentation is wholly derived. The cross-commit Allocation evidence link is explicitly scoped to its published source rather than implying that report exists in this combined tree; I did not independently retrieve/verify the remote publication in this bounded review.
The new source tests address the original report's items 1 and 2 at the evaluator-design level. Their runtime outcome is still pending. Items 3-5 remain adapter/integration/native boundaries. No production repair or extra scope is requested from this follow-up.
@@ -1,24 +0,0 @@
planner-focused-1 recorded exit 101 inputs 804 executable records 0
planner-focused-2 recorded exit 0 inputs 804 executable records 1
scenario-regressions-1 recorded exit 0 inputs 804 executable records 1
format-1 recorded exit 0 inputs 804 executable records 0
strict-lint-1 recorded exit 0 inputs 804 executable records 0
planner-focused-1 recorded exit 101 inputs 804 executable records 0
planner-focused-2 recorded exit 0 inputs 804 executable records 1
scenario-regressions-1 recorded exit 0 inputs 804 executable records 1
format-1 recorded exit 0 inputs 804 executable records 0
strict-lint-1 recorded exit 0 inputs 804 executable records 0
PASS package readback: C:\ar-scenario-9e8r6t\.local\scenario-evidence-tools\selfcheck-1 {'runs': 5, 'recordedFailures': 1, 'logicalFiles': 43, 'storedMembers': 37, 'sourceCapsulePairs': 2, 'captureHelpers': {'gate.py': 'helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py', 'source-proof.py': 'helpers/c86f51d9b8465341dab73d045923ec306b15ebabf7c97b81beac7afe73bd72a1.py'}, 'sourceProofs': 5}
planner-focused-1 recorded exit 101 inputs 804 executable records 0
planner-focused-2 recorded exit 0 inputs 804 executable records 1
scenario-regressions-1 recorded exit 0 inputs 804 executable records 1
format-1 recorded exit 0 inputs 804 executable records 0
strict-lint-1 recorded exit 0 inputs 804 executable records 0
planner-focused-1 recorded exit 101 inputs 804 executable records 0
planner-focused-2 recorded exit 0 inputs 804 executable records 1
scenario-regressions-1 recorded exit 0 inputs 804 executable records 1
format-1 recorded exit 0 inputs 804 executable records 0
strict-lint-1 recorded exit 0 inputs 804 executable records 0
PASS original raw/optional-proof/helper archive equality, existing-output refusal, and corruption rejection: ['raw stream/receipt mismatch', 'helper bytes/name mismatch', 'run/result exit mismatch', 'input archive mapping mismatch', 'missing closed receipt', 'source proof run relationship', 'source proof commit relationship', 'source proof result relationship', 'source proof inputCount relationship', 'missing declared optional source proof', 'source comparator bytes mismatch']
Historical captured failures remain failures: 1
No Cargo, archived code, or GUI executed.
@@ -1,65 +0,0 @@
# Scenario group reorder policy
Derived from [issue 29](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/29) and the [multiple-selection contract](multiselect-contract-20260923.md). Accepted design for the next Scenario adapter; this document alone establishes no implementation, native, integration or release acceptance. The original decision inspected `495464e84b5bae8ddad93e9fc69ef353e7f1c884`; the post-prerelease port additionally inspects the combined base `897d9f049406d5be597543eb0a6755b796259302` and preserves its saved Scenario presentation model.
## Operation and identity
This semantic reorder operation moves the exact selected local occurrences as one stable block inside their common immediate Scenario or Branch parent. It does not edit free XY positions, migrate operands, change a referenced definition or automatically select dependencies. Saved-view body drags already adjust presentation spacing; this policy does not redefine their default meaning.
Saved Scenario layouts are separate presentation records. Semantic reorder preserves all authored lane/message coordinates, metadata and other views exactly, even when the changed order requires derived presentation to accommodate those preferences. [Saved group presentation movement](scenario-group-spacing-20260924.md) is now separately implemented on this branch; semantic reorder must not reinterpret stored coordinates as sequence order or replace that default body/header drag. App admission must distinguish the active saved/derived representation and its visit lifetime, including a same-owner representation round trip.
Eligible bodies are Exchange (including lifecycle and timer messages), Action, State, ScenarioUse and Fragment. A Fragment moves as one occurrence; its branches and descendant lists remain unchanged. Lifelines, Branch headers, Process-only occurrence bodies and other typed targets are unsupported movement members. Cross-parent sets and a Fragment plus one of its descendants refuse as a whole. These objects may still be selected and inspected; selection is not movement permission.
Every original target must exist, have its actual eligible body, belong to the same immediate parent and occur exactly once in that parent's list. Check the entire edited list, including unselected entries, for duplicate/missing IDs, body and parent mismatches. The selected parent must be reachable from the active Scenario through the actual sequence lists and Fragment branch lists. An ancestor pointer alone cannot authorize a hidden Branch omitted from its Fragment. The App must also admit against exact current rendered visibility.
An unlisted child is a supported warning-only authoring state: leave it unlisted and unchanged. Integrity of the edited list does not require every parented element to appear in it. Missing optional reference/binding information or loop warnings must not become blanket movement errors. The existing full-project error validator remains authoritative.
## Stable-block transformation
Let `S` be the original parent list and `T` the exact selected set. Form `B` by retaining members of `T` in their order in `S`; form `R` from the other members. The destination is an exact `Slot { parent, before }`.
1. Complete typed/list/reachability admission and validate the unchanged baseline before any no-op result.
2. Require the destination's same parent and an existing exact anchor, or append.
3. If the anchor is selected, leave `S` exactly unchanged. Do not normalize to a neighboring row or compact a noncontiguous selection.
4. Otherwise insert all of `B` before the anchor in `R`, or append. Exact unchanged order is a no-op.
5. Validate the complete executed candidate. On success use one `SetSteps` transaction; on error change nothing.
For `S=[A,B,C,D]`, selecting C then A and dropping before D produces `[B,A,C,D]`, regardless of primary or pressed member. Dropping that same set before C leaves `[A,B,C,D]`. Selecting `[Call,Reply]` preserves that internal order; it never implicitly selects another reply or expands a referenced Scenario.
Full candidate validation includes unselected descendants. Moving a Fragment ahead of an external Call can invalidate a nested Reply; moving an occurrence with local lifeline use before Create or after a possibly coexecuting Delete must be refused. ALT exclusivity and PAR coexecution cannot be inferred from painted row order. Opaque ScenarioUse bindings remain local lifetime uses under the existing validator, without interprocedural expansion.
## Gesture and transaction requirements
Capture immutable model and exact selection identity before movement. Cache proposals by distinct Slot, not pointer pixels. Before release, verify document, surface, revision, epoch, selection generation and complete membership; refuse a stale set before reconciliation can prune it. Recompute against the same accepted snapshot and execute once through Document. Successful source updates, Undo and Redo each remain one transaction. Refusal and no-op preserve model, source, history branch, selection and primary.
Use the nearest displayed slot, then apply parent admission. A farther legal slot must not replace a nearer invalid operand destination. Feedback identifies the selected count, destination/parent and refusal or no-change reason. A Fragment's interior can contain invalid operand slots even when a header anchor would be a no-op. Pointer/camera/focus lifetime, real selection modifiers and native readability need separate adapter verification; a passing pure planner does not establish them.
This nearest-slot rule governs a **geometric drag input**, not a direct semantic destination chooser. An explicit chooser may offer exact before-anchor/end identities from the named common immediate parent, including selected anchors as no-change choices. It must retain the entire original selected set and expose temporal refusal instead of silently skipping invalid same-parent candidates. The [destination experiment](scenario-reorder-interaction-20260924.md) proves that valid saved slots can have identical Y, making one identity unreachable through unmodified nearest-Y input. That picker alone is therefore rejected as a complete group reorder interaction. An exact semantic chooser, order strip or explicit tie disambiguator must provide the missing reachability; this negative decision does not accept a particular UI.
## Alternatives and acceptance
Contiguous-only selection rejects useful sibling groups without simplifying nested temporal validation. Stable semantic ordering is therefore preferred. Parent/child normalization silently drops selected intent, while automatic call/reply expansion has no unique closure in the presence of alternative replies, loops and nested fragments. Strict refusal keeps this first increment precise. Revisit these decisions only with an explicit user workflow and a compatible whole-set specification.
Discriminating evaluators include literal whole-Project transformations at root and operand levels, noncontiguous order versus click order, selected-anchor/unchanged no-op with existing redo, malformed unselected entries, omitted Branch reachability, warning-only unlisted children, nested reply/lifetime failures and legal nested-fragment movement. Successful cases must preserve every other sequence, binding, relation, value, ID and metadata record. Use independent source round trips and one-step Document history, not preview/commit agreement alone.
Aggregate removal remains separate required work. Until it exists, an interface displaying a group must refuse legacy primary-only Remove/Delete/reorder actions. Live ownership and progress remain in issue 29.
## Adapter decision boundary after combined-source review
Use the existing shared Selection/Snapshot with a full Scenario Context envelope. Map a derived representation to `Surface::Scenario(owner)` and a saved representation to `Surface::Saved(diagram)`. Representation navigation clears the group; the envelope additionally admits document, owner, representation, visit, revision and epoch, including A-to-B-to-A visits. Exact visibility comes from the actual presented scene. This choice preserves existing selection semantics without adding Scenario-specific fields to every generic adapter.
At this historical review boundary, the saved-view gesture remained open. The initial [adapter mapping](scenario-group-planner-evidence-20260924/reviews/adapter-design.md) proposed a one-shot arm; the [independent critique](scenario-group-planner-evidence-20260924/reviews/adapter-critique.md) identified a simpler group handle and an operation-local order preview worth comparing first. Authored coordinates may remain visually unchanged after semantic reorder, so an arm label alone cannot establish observable success. The later destination experiment and implemented chooser decision below supersede that open choice for this increment; the earlier alternatives remain historical evidence. Preserve the active saved representation and captured group during preview; do not transfer selection across representation navigation.
Before enabling group input, specify a consumed gesture identity (or prove equivalent epoch retirement) so canceled, completed/no-op and duplicate packets cannot affect a newer gesture on the same unchanged selection. Match captured selection identity on spacing packets too; group-size checks alone miss selection ABA. If returning to the original press position promises no change, define that screen-space rule separately from nearest-slot selection; the nearest slot need not be the pressed occurrence's anchor. Close raw Details arrows, menus and singleton removal/reorder paths before they can apply to only the primary of a visible group.
The planner's [verification record](scenario-group-planner-20260924.md) establishes the bounded semantic foundation. It does not accept any of these future UI choices or claim group gestures are implemented.
## Implemented successor: explicit semantic destination chooser
The [App implementation and Windows evidence](scenario-reorder-app-20260924.md) at `1c4099a9ef76dd28d17ef892e95edf89e860569f` promotes the shared frozen session into production input. The header Reorder control offers exact same-parent before-anchor/end identities with search, paging, and a before/after semantic-order preview. This is the selected bounded implementation following the tied-Y counterexample; no one-shot arm or nearest-Y-only group drag is enabled. Ordinary Saved body/header spacing retains its meaning. Future graphical access must preserve exact identity reachability and the same whole-set semantics.
Begin retires captured input before acquiring the new full Context/Snapshot/token. Every proposal retains the requested representation, complete selection and primary. Apply/Finish captures the exact displayed Slot; queued Apply for another preview is inert. No default destination exists. Enter previews a unique match; nonrepeated Ctrl+Enter applies an eligible displayed proposal after child-overlay precedence; Escape cancels. Stale token-specific focus tasks cannot disturb newer focus. The current Slot alone is cached, bounding retained scene memory.
Baseline Fallback admits semantic choice only when every selected target is visible. Candidate Fallback may commit after an explicit adjacent warning, retaining the requested Saved view and all authored layout records. Candidate Unavailable or loss of any selected rendered target blocks commit. These are explicit App policies, not implicit permissions from semantic validation. The completed result shows actual semantic order with full aliases; its lifetime tolerates camera/diagnostic input-epoch changes but remains bound to document/revision/view/visit and exact selection generation. Subsequent spacing commits retire that explanation.
Scenario tests, formatting, strict lint and build pass at this source with exact 818-input source proofs. Windows native run 2 exercises tied/distant identity destinations, ordinary/compact preview readability, cancellation/no-op Redo retention, semantic result feedback and keyboard entry; fresh-process run 3 verifies exact saved-source reopen. Native selection used contextual Add, not demonstrated Shift-click; F6 followed by six reverse Tab steps is reachable but remains a discoverability limitation. Real App source-write failure remains a runtime coverage gap. These scoped results do not claim default-branch integration, a new release, aggregate removal or full issue-29 completion. The linked report retains failed attempts and evidence boundaries.
@@ -1,76 +0,0 @@
# Saved Scenario group spacing
Branch-specific continuation of Scenario selection, tracked in [Gitea #29](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/29). Final source `8d6fac4701933c7ada13cc7e6e07060662aa7465`; spacing operation logic is unchanged from `3b113e5316724eca001cd92c5b8efed0a48ccc86`. Intervening `67534ce4a5fab29cb69b461f00d447eca2e7d12b` and `4a27c8ef0b8f4954e208fdfda5efd2b41c240577` change tests only; final 8d6fac47 revises four help/refusal/menu text locations. This work is outside the frozen #37 Windows prerelease and does not complete #29.
## Implemented behavior
In a supported saved Scenario view, dragging a selected lifeline header moves the selected local lifelines horizontally; dragging a selected message body or label moves the selected local messages vertically. A group must contain only rendered local members of one kind. Pressing any selected member starts the whole-group operation on that first press and preserves the existing primary. Message groups may span local operands without changing their parent or semantic order.
Each member moves from its own original displayed coordinate by the common world-space delta. The preview retains the complete selection and primary, shows the count, and reports geometry refusals. Release prepares one batch and commits model/source together through Document. Semantic order, unselected presentation records and other views remain untouched; Undo/Redo use the normal single transaction.
A stationary plain click collapses a group to the clicked member even when that group cannot move. Mixed or unsupported groups retain their complete selection and refuse actual motion; no eligible subset is moved. The three-pixel group threshold is Euclidean in screen space. After crossing it, a supported group's off-axis movement or return to origin is a no-op that retains selection and history. An ineligible group refuses the whole motion even after returning to origin. Escape cancels the captured gesture.
Admission checks the complete Scenario representation/visit Context, camera lifetime, selection generation, session token and captured full Snapshot before effects. Current terminal or failed-start input retires its lifetime; old packets cannot cancel a newer session. Source-update failure leaves the accepted model/source/history and stored camera unchanged. Ordinary singleton spacing remains supported.
This is presentation movement. The separate [semantic group reorder policy](scenario-group-reorder-policy-20260924.md) still governs sequence reorder; its user-facing gesture and destination design remain unresolved. No saved-view semantic reorder arm is introduced.
## Review and first gate
Independent implementation review found a click regression: immediate rejection of mixed-group spacing prevented stationary click-to-collapse. The corrected controller retains a full-snapshot click session, using the pressed singleton only as an unchanged display/hit/camera witness. Its retained group refusal prevents any partial preparation; stationary Finish collapses, crossed motion refuses. The correction and frozen pure planner received static reinspection with no unresolved material finding. The scoped review is retained beside the evidence; static review is distinct from runtime acceptance.
The first Scenario gate at `3b113e53` closed with exit 101: **286 passed, 3 failed, 1 ignored**, with all 811 captured inputs unchanged. Preserve that run as a failure. The three failed tests concerned a literal lane oracle that assumed an order for randomly generated IDs, a synthetic packet that omitted current selection generation, and an expected direct Collapse message superseded by the captured spacing Finish path.
Test-only correction `67534ce4` explicitly authors the initial geometry for the literal oracle, captures the current generation, and checks the new event path while retaining actual selection outcomes. Independent review confirmed that exact Project/source, primary/set, camera/visit, history and reopen assertions remain; pure tests retain missing-override coverage. No production fix is attributed to this test-only commit.
Scenario repeat `group-spacing-scenario-2` at **67534ce4** passed **289 tests, zero failed, one ignored**, exit 0 with all 811 captured inputs unchanged. Strict lint `group-spacing-clippy-1` at that source then failed on two redundant `f64::from` conversions in the test oracle. Commit 4a27c8ef adds only explicit `f32` types to the two literal starting-coordinate variables. Values 148, 240, 160 and 400 are exact in both formats; promotion back to f64 preserves the exact prior addition and final f32 cast. Independent review found numeric equivalence and no production change. Final 8d6fac47 distinguishes unavailable semantic group reordering/removal from available saved spacing and adds a saved-group drag hint; it changes no operation admission or command path. Reusing the 67534ce4 executed-test evidence alongside final-source all-target lint/build/native evidence is proportionate; **the 289 tests were not executed at 4a27c8ef or 8d6fac47**.
## Verification record
| Evidence | Closed result |
| --- | --- |
| Scenario repeat at `67534ce4` | 289 passed, 0 failed, 1 ignored; closed exit 0, unchanged 811-input capture and exact source proof retained in the evidence package. |
| Formatting at `4a27c8ef` | `group-spacing-format-3` passed, closed exit 0 with unchanged inputs. |
| Strict desktop lint at `4a27c8ef` | `group-spacing-clippy-2` passed, closed exit 0 with unchanged inputs. |
| Formatting at `8d6fac47` | `group-spacing-format-4` passed, closed exit 0 with unchanged inputs. |
| Windows Studio/CLI build at `8d6fac47` | `group-spacing-build-1` passed, exit 0, 811 unchanged inputs; both executable artifacts pinned. |
| Strict desktop lint at `8d6fac47` | `group-spacing-clippy-3` passed, exit 0, 811 unchanged inputs. |
| Native saved-group exercise at `8d6fac47` | Two fresh processes, normal exits 0 and empty logs; nine run-1 saved snapshots and one fresh-process reopened snapshot pass exact declared phases. |
| Portable evidence | [Package index and replay commands](scenario-group-spacing-evidence-20260924/README.md); raw failed gates, old-CLI preflight and all native captures retained. |
The passing Scenario filter includes pure whole-Project transformations, both pressed members, deterministic command order, local cross-operand groups, typed/mixed/foreign/latent refusal, simultaneous call/reply and lane geometry, zero/subprecision no-op, exact source Undo/Redo/reopen, stale input and selection ABA, source-failure atomicity, and mounted modifier/input coverage. Filters overlap and must not be summed as unique tests; the existing timing diagnostic remains ignored.
## Native observations and source checks
Windows 11, existing user profile, maximized 1605×872 window, actual-size 100% during measured movements. Both isolated processes use the closed build's Studio executable (119,703,552 bytes, SHA-256 `e74342f3e06a28f91d378a429661fc88e5b009ce60549b3a2611160d48d16cdc`) with `libunwind.dll`, a system-only PATH and an otherwise empty working directory. The matching current CLI validates all three immutable fixtures with exit 0 and only three expected unallocated-activity advisory warnings. Preflight 1 used an obsolete CLI lacking saved-layout syntax and failed; it remains historical evidence, not a current-source failure.
The original fixture has three local messages and lanes in two saved views. Run 1 selected Gamma then contextually added Alpha, making Alpha primary. Dragging nonprimary Gamma by +24 screen/world Y moved both selected messages: A Alpha 160→184 and Gamma 360→384; Beta stayed 260. One Undo returned the entire file to baseline bytes; one Redo restored both positions. A horizontal-only message drag retained the full group and made no source edit.
Selecting Intake and adding Review made Review primary. Dragging nonprimary Intake +32 X changed A Intake 140→172 and Review 400→432; Archive stayed 660. One Undo reverted this whole lane edit. A vertical-only lane drag preserved the group and available Redo; Redo restored both lanes. A→B→A navigation showed B's unchanged spacing and A's edits; the group reduced to the visible Review singleton, not an empty selection. Semantic Alpha/Beta/Gamma order and all B records remained byte-identical.
A mixed Alpha/Intake selection refused message-body and lane-header drags with explicit feedback and both members retained. Stationary message and header clicks each collapsed the mixed group correctly. Dragging a supported lane group outside the canvas cancelled the whole edit and retained selection. Final Save exactly matched the declared combined phase. Run 2 copied that actual saved source, opened it in a fresh process, selected saved A and actual size, visually retained both edits, and saved the same bytes again. Both processes closed through Alt+F4 with exit 0, empty stdout/stderr and unchanged immutable inputs.
The whole-source oracle permits only exact Decimal-equivalent spelling of the four predeclared changed A coordinate tokens (for example `184.0` versus `184`); it has no numeric tolerance. Every other byte must match the intended phase. Final saved source is 3,554 bytes, SHA-256 `360b9e24564a10cce3379a097bf30f33db5c583cceaaaf595fc595f662b556d5`. Original expected fixtures were never rewritten from observed output.
Native selection used contextual Add, not held Shift; mounted modifier tests remain separate evidence. Native Escape during a held drag, out-and-back drag, dirty Properties, stale packets and intermediate preview frames were not exercised. Labels remain ellipsized, and the narrowed Review→Archive gap lets Beta's white label background obscure a short section of Archive's lifeline. Names remain identifiable and semantic/source checks pass; broad readability/accessibility acceptance is not claimed. Initial navigation/focus retries, one disallowed JavaScript eval and an undeclared local binding error are tool/harness recovery history, not asserted product failures. Raw captures preserve the actual observations; filename labels alone are not assertions.
## Reproduction
Use a Windows checkout at the stated source with the recorded GNU/LLVM toolchain and matching dependency cache. The first gate used `stable-x86_64-pc-windows-gnullvm`, `CARGO_BUILD_JOBS=1`, `CARGO_INCREMENTAL=0`, and the captured Cargo home/target directory. Retain the complete environment receipt with final results. Linux checks remain excluded for this prerelease workflow.
The first Scenario command, also suitable for the corrected-source repeat, is:
```powershell
cargo test -p oa-desktop scenario --message-format=json -- --nocapture
```
Additional completed gate commands:
```powershell
cargo fmt --all -- --check
cargo clippy -p oa-desktop --all-targets --message-format=json -- -D warnings
cargo build -p oa-desktop -p oa-cli --message-format=json
```
## Limits
This increment does not provide automatic-view group semantic reorder, arbitrary mixed-group movement, referenced-definition movement, aggregate removal, or complete Scenario authoring/accessibility acceptance. Unsupported or fallback geometry cannot be edited through spacing. The native observations above establish only the stated bounded workflows. This is branch-specific contribution evidence; no combined integration, new release or full-workspace gate is claimed. Current prerelease #37 is separately delivered and excludes this work.
@@ -1,21 +0,0 @@
# Saved Scenario group spacing evidence
Branch-specific evidence for [the implementation report](../scenario-group-spacing-20260924.md). Executed Scenario tests belong to `67534ce4`; final lint, format, Studio/CLI build and native runs belong to `8d6fac47`. The report explains the intervening test annotations and UI text changes. This is not combined integration or release acceptance.
| Archive | Bytes | SHA-256 |
| --- | ---: | --- |
| `checks/evidence.zip` | 12897498 | `d3de224502a2b7f8236d2ad82af6d6c57bb3a349468aebeea7369c3c3c267342` |
| `native/native-evidence.zip` | 6069072 | `27e0e189053fdfdbfcecd8c91ae718849448b090aab557afc7d9445af1ceaba8` |
Run from a checkout with Python 3.11+; these checks need no Cargo, Git, network or installed Studio:
```powershell
python -B harness/scenario-group-spacing-evidence-20260924/checks/verify.py
python -B harness/scenario-group-spacing-evidence-20260924/native/verify.py harness/scenario-group-spacing-evidence-20260924/native
```
Checks contain ten closed runs, two preserved failures, ten exact source proofs and four deduplicated source capsules. Explicit run names are `group-spacing-format-1`, `group-spacing-scenario-1`, `group-spacing-format-2`, `group-spacing-scenario-2`, `group-spacing-clippy-1`, `group-spacing-format-3`, `group-spacing-clippy-2`, `group-spacing-format-4`, `group-spacing-build-1` and `group-spacing-clippy-3`. The reused checks helper README's selection example/title and manifest kind are historical schema names; the actual run inventory and report govern this package. Failed-run consistency is not a passing test result.
Native evidence retains both normal-exit processes, all 67 captures, actual saved snapshots, immutable expected fixtures, current and obsolete-CLI preflights, receipts and original helpers. The helpers' archived preparation notes saying “not run” describe their original preparation stage; the subsequent closed receipts establish execution. Executable/DLL bytes are excluded explicitly, with exact pins and source/build bindings retained. This is a portable evidence archive, not a runnable distribution. The offline checker independently compares actual saved source with declared phases and validates fresh-reopen provenance; it does not judge visual usability or rehash excluded binaries.
The [independent review](review.md) distinguishes source review, native observations and package consistency. Reproduction/environment and visual/input limitations are in the implementation report. Raw screenshot labels are identifiers, not assertions (run-1 capture35 clears the group to one visible member, not zero selection).
@@ -1,14 +0,0 @@
# Scenario selection captured checks
These tools package explicitly named, completed runs from `CHECKOUT/.local/scenario-group-gates` without executing archived source or running Cargo. Adapted only in input-directory and manifest-kind names from the published prerelease Connection/Route evidence tools at `93d2a6f9af40061bf92cd81ba33d5648d0181cb1`.
```powershell
python -B prepare.py --root CHECKOUT --runs selection-scenario-1 selection-scenario-2 selection-format-1 --output NEW_DIRECTORY
python -B NEW_DIRECTORY/verify.py
```
The manifest is authoritative for the selected runs. Failed runs remain failures: archive consistency is not a test pass. Every raw stream and receipt retains its original bytes. Identical input maps and ZIPs are stored by hash; the logical-file map preserves their original run paths. Historical capture helpers and exact source comparators are included as data, never executed by verification.
Source proofs compare every captured input with a named Git commit; the offline package verifier checks those receipts and their pinned comparator identity but does not repeat the Git comparison. The captured HEAD and the named proof commit retain their respective meanings. The verifier checks archive inventory, hashes, input ZIP contents, process/command/result consistency and executable artifact relationships. It does not establish hermetic builds, native workflow acceptance, UI integration, or release acceptance.
Use `--check-only` instead of `--output` for an in-memory consistency check. Existing output directories refuse. Python 3.11+ standard library is sufficient; `-B` avoids bytecode writes.
@@ -1,22 +0,0 @@
{
"prepare.py": {
"bytes": 6308,
"sha256": "2d500f5d43a49893abb3936f880e61b8adc623531e26f0288983502e386894db"
},
"verify.py": {
"bytes": 11436,
"sha256": "ea8e781003bec21f2ac4af23e21b68ca5639a98eb31505c10b2869f57565e917"
},
"README.md": {
"bytes": 1641,
"sha256": "4c5c582cbdf02c8704b9add656d273c275b10eed87219131e3bba639a4c4bd8a"
},
"manifest.json": {
"bytes": 28330,
"sha256": "42bfa9a3a0f62ab82efecff040851347411873bfcb670c214928841939cb6c16"
},
"evidence.zip": {
"bytes": 12897498,
"sha256": "d3de224502a2b7f8236d2ad82af6d6c57bb3a349468aebeea7369c3c3c267342"
}
}
@@ -1,611 +0,0 @@
{
"schema": 1,
"kind": "scenario-selection-cargo-gates",
"runs": {
"group-spacing-format-1": {
"command": [
"cargo",
"fmt",
"--all",
"--",
"--check"
],
"head": "3b113e5316724eca001cd92c5b8efed0a48ccc86",
"exitCode": 0,
"inputCount": 811,
"helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"sourceProof": {
"commit": "3b113e5316724eca001cd92c5b8efed0a48ccc86",
"pin": {
"bytes": 601,
"sha256": "a177782dfa1c85efc817350537b965c11b40fdcec09fe01850442c86017289f5"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"group-spacing-scenario-1": {
"command": [
"cargo",
"test",
"-p",
"oa-desktop",
"scenario",
"--message-format=json",
"--",
"--nocapture"
],
"head": "3b113e5316724eca001cd92c5b8efed0a48ccc86",
"exitCode": 101,
"inputCount": 811,
"helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"sourceProof": {
"commit": "3b113e5316724eca001cd92c5b8efed0a48ccc86",
"pin": {
"bytes": 603,
"sha256": "3bd14b592fab993457a5770d1894f442d57103d662334994d8f7ed84ba08b3bc"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"group-spacing-format-2": {
"command": [
"cargo",
"fmt",
"--all",
"--",
"--check"
],
"head": "67534ce4a5fab29cb69b461f00d447eca2e7d12b",
"exitCode": 0,
"inputCount": 811,
"helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"sourceProof": {
"commit": "67534ce4a5fab29cb69b461f00d447eca2e7d12b",
"pin": {
"bytes": 601,
"sha256": "2a88c77c7275472a74ce3a97f8dfebc38c87793bfbee4388fe4c52b2345c73c8"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"group-spacing-scenario-2": {
"command": [
"cargo",
"test",
"-p",
"oa-desktop",
"scenario",
"--message-format=json",
"--",
"--nocapture"
],
"head": "67534ce4a5fab29cb69b461f00d447eca2e7d12b",
"exitCode": 0,
"inputCount": 811,
"helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"sourceProof": {
"commit": "67534ce4a5fab29cb69b461f00d447eca2e7d12b",
"pin": {
"bytes": 603,
"sha256": "059b06d427f3bc1a505f0ff612d54df5da01a1545f49eff0842c2d92851e42dc"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"group-spacing-clippy-1": {
"command": [
"cargo",
"clippy",
"-p",
"oa-desktop",
"--all-targets",
"--message-format=json",
"--",
"-D",
"warnings"
],
"head": "67534ce4a5fab29cb69b461f00d447eca2e7d12b",
"exitCode": 101,
"inputCount": 811,
"helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"sourceProof": {
"commit": "67534ce4a5fab29cb69b461f00d447eca2e7d12b",
"pin": {
"bytes": 601,
"sha256": "afd1a8066f2906ef5362dfb4a63b6d412e2e83b9461f14d7a00572ea40128c84"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"group-spacing-format-3": {
"command": [
"cargo",
"fmt",
"--all",
"--",
"--check"
],
"head": "4a27c8ef0b8f4954e208fdfda5efd2b41c240577",
"exitCode": 0,
"inputCount": 811,
"helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"sourceProof": {
"commit": "4a27c8ef0b8f4954e208fdfda5efd2b41c240577",
"pin": {
"bytes": 601,
"sha256": "0232c97e7a02b7971ae078ed2d4c3257e56e42e6bb9f031e195c3e7f895d166a"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"group-spacing-clippy-2": {
"command": [
"cargo",
"clippy",
"-p",
"oa-desktop",
"--all-targets",
"--message-format=json",
"--",
"-D",
"warnings"
],
"head": "4a27c8ef0b8f4954e208fdfda5efd2b41c240577",
"exitCode": 0,
"inputCount": 811,
"helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"sourceProof": {
"commit": "4a27c8ef0b8f4954e208fdfda5efd2b41c240577",
"pin": {
"bytes": 601,
"sha256": "8ece8f2bf1e1ca5a5df2f5983a65573b046c84ddaa396906dc79269823fb698f"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"group-spacing-format-4": {
"command": [
"cargo",
"fmt",
"--all",
"--",
"--check"
],
"head": "8d6fac4701933c7ada13cc7e6e07060662aa7465",
"exitCode": 0,
"inputCount": 811,
"helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"sourceProof": {
"commit": "8d6fac4701933c7ada13cc7e6e07060662aa7465",
"pin": {
"bytes": 601,
"sha256": "a303f7255ae967155885e704c842ba578e888d2f11a78b3cf95d6722ea357893"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"group-spacing-build-1": {
"command": [
"cargo",
"build",
"-p",
"oa-desktop",
"-p",
"oa-cli",
"--message-format=json"
],
"head": "8d6fac4701933c7ada13cc7e6e07060662aa7465",
"exitCode": 0,
"inputCount": 811,
"helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"sourceProof": {
"commit": "8d6fac4701933c7ada13cc7e6e07060662aa7465",
"pin": {
"bytes": 600,
"sha256": "27a0e2d80704a592ba280b6079e985e2b52f0b1aedd6eb84b79fba8469be9e9d"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"group-spacing-clippy-3": {
"command": [
"cargo",
"clippy",
"-p",
"oa-desktop",
"--all-targets",
"--message-format=json",
"--",
"-D",
"warnings"
],
"head": "8d6fac4701933c7ada13cc7e6e07060662aa7465",
"exitCode": 0,
"inputCount": 811,
"helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"sourceProof": {
"commit": "8d6fac4701933c7ada13cc7e6e07060662aa7465",
"pin": {
"bytes": 601,
"sha256": "5932a0cba7607647967c84a8c8ac582bbfabc2e1c00b89242a14445924ea92a6"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
}
},
"captureHelpers": {
"gate.py": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"source-proof.py": "helpers/c86f51d9b8465341dab73d045923ec306b15ebabf7c97b81beac7afe73bd72a1.py"
},
"logicalFiles": {
"cargo/group-spacing-build-1/inputs.json": "capsules/c834d8403b9b82e5d17a0be38df288194cd9180c707744feeefec6d7465d8381.json",
"cargo/group-spacing-build-1/inputs.zip": "capsules/bd67f0bd33451482a69ae2972846c472f309393b378da34b004acbdbd89001e6.zip",
"cargo/group-spacing-build-1/process.json": "cargo/group-spacing-build-1/process.json",
"cargo/group-spacing-build-1/result.json": "cargo/group-spacing-build-1/result.json",
"cargo/group-spacing-build-1/source-proof.json": "cargo/group-spacing-build-1/source-proof.json",
"cargo/group-spacing-build-1/started.json": "cargo/group-spacing-build-1/started.json",
"cargo/group-spacing-build-1/stderr.log": "cargo/group-spacing-build-1/stderr.log",
"cargo/group-spacing-build-1/stdout.log": "cargo/group-spacing-build-1/stdout.log",
"cargo/group-spacing-clippy-1/inputs.json": "capsules/775bffd4103c646e5a854e67b1782eeadc529e6fb968695cd9f68e7dec584a0b.json",
"cargo/group-spacing-clippy-1/inputs.zip": "capsules/0965e5df5fc02fc861f5e7daee471f0cfc341ed35afb7c98a0fd3469843664ad.zip",
"cargo/group-spacing-clippy-1/process.json": "cargo/group-spacing-clippy-1/process.json",
"cargo/group-spacing-clippy-1/result.json": "cargo/group-spacing-clippy-1/result.json",
"cargo/group-spacing-clippy-1/source-proof.json": "cargo/group-spacing-clippy-1/source-proof.json",
"cargo/group-spacing-clippy-1/started.json": "cargo/group-spacing-clippy-1/started.json",
"cargo/group-spacing-clippy-1/stderr.log": "cargo/group-spacing-clippy-1/stderr.log",
"cargo/group-spacing-clippy-1/stdout.log": "cargo/group-spacing-clippy-1/stdout.log",
"cargo/group-spacing-clippy-2/inputs.json": "capsules/7b53a6a3fe17ddf8e212b071b598b1566eff54e5f6d4551ae9215dbb00bd0e95.json",
"cargo/group-spacing-clippy-2/inputs.zip": "capsules/9869cf9ab485d4ce5e80dc69f71a959d5e3d4bdc6b6f633c7148640455f3c217.zip",
"cargo/group-spacing-clippy-2/process.json": "cargo/group-spacing-clippy-2/process.json",
"cargo/group-spacing-clippy-2/result.json": "cargo/group-spacing-clippy-2/result.json",
"cargo/group-spacing-clippy-2/source-proof.json": "cargo/group-spacing-clippy-2/source-proof.json",
"cargo/group-spacing-clippy-2/started.json": "cargo/group-spacing-clippy-2/started.json",
"cargo/group-spacing-clippy-2/stderr.log": "cargo/group-spacing-clippy-2/stderr.log",
"cargo/group-spacing-clippy-2/stdout.log": "cargo/group-spacing-clippy-2/stdout.log",
"cargo/group-spacing-clippy-3/inputs.json": "capsules/c834d8403b9b82e5d17a0be38df288194cd9180c707744feeefec6d7465d8381.json",
"cargo/group-spacing-clippy-3/inputs.zip": "capsules/bd67f0bd33451482a69ae2972846c472f309393b378da34b004acbdbd89001e6.zip",
"cargo/group-spacing-clippy-3/process.json": "cargo/group-spacing-clippy-3/process.json",
"cargo/group-spacing-clippy-3/result.json": "cargo/group-spacing-clippy-3/result.json",
"cargo/group-spacing-clippy-3/source-proof.json": "cargo/group-spacing-clippy-3/source-proof.json",
"cargo/group-spacing-clippy-3/started.json": "cargo/group-spacing-clippy-3/started.json",
"cargo/group-spacing-clippy-3/stderr.log": "cargo/group-spacing-clippy-3/stderr.log",
"cargo/group-spacing-clippy-3/stdout.log": "cargo/group-spacing-clippy-3/stdout.log",
"cargo/group-spacing-format-1/inputs.json": "capsules/68fb2028446c3760010886a1cb839224616672d5916d467e6d4e177b8d07e2a0.json",
"cargo/group-spacing-format-1/inputs.zip": "capsules/cfe694b469443b2922d2a76400d306a54bf179426d2c318ba6ccfdd26441959a.zip",
"cargo/group-spacing-format-1/process.json": "cargo/group-spacing-format-1/process.json",
"cargo/group-spacing-format-1/result.json": "cargo/group-spacing-format-1/result.json",
"cargo/group-spacing-format-1/source-proof.json": "cargo/group-spacing-format-1/source-proof.json",
"cargo/group-spacing-format-1/started.json": "cargo/group-spacing-format-1/started.json",
"cargo/group-spacing-format-1/stderr.log": "cargo/group-spacing-format-1/stderr.log",
"cargo/group-spacing-format-1/stdout.log": "cargo/group-spacing-format-1/stdout.log",
"cargo/group-spacing-format-2/inputs.json": "capsules/775bffd4103c646e5a854e67b1782eeadc529e6fb968695cd9f68e7dec584a0b.json",
"cargo/group-spacing-format-2/inputs.zip": "capsules/0965e5df5fc02fc861f5e7daee471f0cfc341ed35afb7c98a0fd3469843664ad.zip",
"cargo/group-spacing-format-2/process.json": "cargo/group-spacing-format-2/process.json",
"cargo/group-spacing-format-2/result.json": "cargo/group-spacing-format-2/result.json",
"cargo/group-spacing-format-2/source-proof.json": "cargo/group-spacing-format-2/source-proof.json",
"cargo/group-spacing-format-2/started.json": "cargo/group-spacing-format-2/started.json",
"cargo/group-spacing-format-2/stderr.log": "cargo/group-spacing-format-2/stderr.log",
"cargo/group-spacing-format-2/stdout.log": "cargo/group-spacing-format-2/stdout.log",
"cargo/group-spacing-format-3/inputs.json": "capsules/7b53a6a3fe17ddf8e212b071b598b1566eff54e5f6d4551ae9215dbb00bd0e95.json",
"cargo/group-spacing-format-3/inputs.zip": "capsules/9869cf9ab485d4ce5e80dc69f71a959d5e3d4bdc6b6f633c7148640455f3c217.zip",
"cargo/group-spacing-format-3/process.json": "cargo/group-spacing-format-3/process.json",
"cargo/group-spacing-format-3/result.json": "cargo/group-spacing-format-3/result.json",
"cargo/group-spacing-format-3/source-proof.json": "cargo/group-spacing-format-3/source-proof.json",
"cargo/group-spacing-format-3/started.json": "cargo/group-spacing-format-3/started.json",
"cargo/group-spacing-format-3/stderr.log": "cargo/group-spacing-format-3/stderr.log",
"cargo/group-spacing-format-3/stdout.log": "cargo/group-spacing-format-3/stdout.log",
"cargo/group-spacing-format-4/inputs.json": "capsules/c834d8403b9b82e5d17a0be38df288194cd9180c707744feeefec6d7465d8381.json",
"cargo/group-spacing-format-4/inputs.zip": "capsules/bd67f0bd33451482a69ae2972846c472f309393b378da34b004acbdbd89001e6.zip",
"cargo/group-spacing-format-4/process.json": "cargo/group-spacing-format-4/process.json",
"cargo/group-spacing-format-4/result.json": "cargo/group-spacing-format-4/result.json",
"cargo/group-spacing-format-4/source-proof.json": "cargo/group-spacing-format-4/source-proof.json",
"cargo/group-spacing-format-4/started.json": "cargo/group-spacing-format-4/started.json",
"cargo/group-spacing-format-4/stderr.log": "cargo/group-spacing-format-4/stderr.log",
"cargo/group-spacing-format-4/stdout.log": "cargo/group-spacing-format-4/stdout.log",
"cargo/group-spacing-scenario-1/inputs.json": "capsules/68fb2028446c3760010886a1cb839224616672d5916d467e6d4e177b8d07e2a0.json",
"cargo/group-spacing-scenario-1/inputs.zip": "capsules/cfe694b469443b2922d2a76400d306a54bf179426d2c318ba6ccfdd26441959a.zip",
"cargo/group-spacing-scenario-1/process.json": "cargo/group-spacing-scenario-1/process.json",
"cargo/group-spacing-scenario-1/result.json": "cargo/group-spacing-scenario-1/result.json",
"cargo/group-spacing-scenario-1/source-proof.json": "cargo/group-spacing-scenario-1/source-proof.json",
"cargo/group-spacing-scenario-1/started.json": "cargo/group-spacing-scenario-1/started.json",
"cargo/group-spacing-scenario-1/stderr.log": "cargo/group-spacing-scenario-1/stderr.log",
"cargo/group-spacing-scenario-1/stdout.log": "cargo/group-spacing-scenario-1/stdout.log",
"cargo/group-spacing-scenario-2/inputs.json": "capsules/775bffd4103c646e5a854e67b1782eeadc529e6fb968695cd9f68e7dec584a0b.json",
"cargo/group-spacing-scenario-2/inputs.zip": "capsules/0965e5df5fc02fc861f5e7daee471f0cfc341ed35afb7c98a0fd3469843664ad.zip",
"cargo/group-spacing-scenario-2/process.json": "cargo/group-spacing-scenario-2/process.json",
"cargo/group-spacing-scenario-2/result.json": "cargo/group-spacing-scenario-2/result.json",
"cargo/group-spacing-scenario-2/source-proof.json": "cargo/group-spacing-scenario-2/source-proof.json",
"cargo/group-spacing-scenario-2/started.json": "cargo/group-spacing-scenario-2/started.json",
"cargo/group-spacing-scenario-2/stderr.log": "cargo/group-spacing-scenario-2/stderr.log",
"cargo/group-spacing-scenario-2/stdout.log": "cargo/group-spacing-scenario-2/stdout.log",
"comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py",
"helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"helpers/c86f51d9b8465341dab73d045923ec306b15ebabf7c97b81beac7afe73bd72a1.py": "helpers/c86f51d9b8465341dab73d045923ec306b15ebabf7c97b81beac7afe73bd72a1.py"
},
"members": {
"capsules/0965e5df5fc02fc861f5e7daee471f0cfc341ed35afb7c98a0fd3469843664ad.zip": {
"bytes": 3246619,
"sha256": "0965e5df5fc02fc861f5e7daee471f0cfc341ed35afb7c98a0fd3469843664ad"
},
"capsules/68fb2028446c3760010886a1cb839224616672d5916d467e6d4e177b8d07e2a0.json": {
"bytes": 136196,
"sha256": "68fb2028446c3760010886a1cb839224616672d5916d467e6d4e177b8d07e2a0"
},
"capsules/775bffd4103c646e5a854e67b1782eeadc529e6fb968695cd9f68e7dec584a0b.json": {
"bytes": 136196,
"sha256": "775bffd4103c646e5a854e67b1782eeadc529e6fb968695cd9f68e7dec584a0b"
},
"capsules/7b53a6a3fe17ddf8e212b071b598b1566eff54e5f6d4551ae9215dbb00bd0e95.json": {
"bytes": 136196,
"sha256": "7b53a6a3fe17ddf8e212b071b598b1566eff54e5f6d4551ae9215dbb00bd0e95"
},
"capsules/9869cf9ab485d4ce5e80dc69f71a959d5e3d4bdc6b6f633c7148640455f3c217.zip": {
"bytes": 3246622,
"sha256": "9869cf9ab485d4ce5e80dc69f71a959d5e3d4bdc6b6f633c7148640455f3c217"
},
"capsules/bd67f0bd33451482a69ae2972846c472f309393b378da34b004acbdbd89001e6.zip": {
"bytes": 3246686,
"sha256": "bd67f0bd33451482a69ae2972846c472f309393b378da34b004acbdbd89001e6"
},
"capsules/c834d8403b9b82e5d17a0be38df288194cd9180c707744feeefec6d7465d8381.json": {
"bytes": 136196,
"sha256": "c834d8403b9b82e5d17a0be38df288194cd9180c707744feeefec6d7465d8381"
},
"capsules/cfe694b469443b2922d2a76400d306a54bf179426d2c318ba6ccfdd26441959a.zip": {
"bytes": 3246346,
"sha256": "cfe694b469443b2922d2a76400d306a54bf179426d2c318ba6ccfdd26441959a"
},
"cargo/group-spacing-build-1/process.json": {
"bytes": 153,
"sha256": "e04b54ec8ed3f28af0e21715462a1824cecb39ff08feff25fabc5ade6c7143d4"
},
"cargo/group-spacing-build-1/result.json": {
"bytes": 4455,
"sha256": "0fc72640a3bbb4bc3743009fbc196d2b6f25f41cb2b6db4bb0cc8d5182638289"
},
"cargo/group-spacing-build-1/source-proof.json": {
"bytes": 600,
"sha256": "27a0e2d80704a592ba280b6079e985e2b52f0b1aedd6eb84b79fba8469be9e9d"
},
"cargo/group-spacing-build-1/started.json": {
"bytes": 1527,
"sha256": "b6438a7289b98d139b87168ce8a3800e3f4b77d3d77192dd816e117cc401a451"
},
"cargo/group-spacing-build-1/stderr.log": {
"bytes": 199,
"sha256": "b69e70aaaff6ba2aa6abc8dc858968eccdfcaea7f71af664477548675c0538ee"
},
"cargo/group-spacing-build-1/stdout.log": {
"bytes": 285564,
"sha256": "8dcb4708b37a826caf8f127183df013dd6dd43a4e92257aa39e1001dfb42cbec"
},
"cargo/group-spacing-clippy-1/process.json": {
"bytes": 179,
"sha256": "f33cd462f337f293df9f5f1d8b543133c0292f674d805637edb889a5d0dc8bf0"
},
"cargo/group-spacing-clippy-1/result.json": {
"bytes": 1961,
"sha256": "8fd72a2a7149498a2b392d7fd06d633817588625eee28b077e923027fe6314df"
},
"cargo/group-spacing-clippy-1/source-proof.json": {
"bytes": 601,
"sha256": "afd1a8066f2906ef5362dfb4a63b6d412e2e83b9461f14d7a00572ea40128c84"
},
"cargo/group-spacing-clippy-1/started.json": {
"bytes": 1563,
"sha256": "82bf44bf6ec5bf39408e9898f8ddcb561b59ed9df44a1835de1b1f34209bedd0"
},
"cargo/group-spacing-clippy-1/stderr.log": {
"bytes": 164,
"sha256": "c2a1fb944bc1ae35fa3131726dd7a717a5cbd95a7d955cbc155e94bd79772c8a"
},
"cargo/group-spacing-clippy-1/stdout.log": {
"bytes": 272239,
"sha256": "a9296b7e3c41f33f4cf1435e89a535d55f2dc27be38cef72b4c7cded398c5e99"
},
"cargo/group-spacing-clippy-2/process.json": {
"bytes": 179,
"sha256": "e4a8b8e0cdb1c1ed837b64fb1accc71dc5569a418ed5cfe70d77e89e85f7740b"
},
"cargo/group-spacing-clippy-2/result.json": {
"bytes": 1958,
"sha256": "74cfa66be75b6011d05ba016be1fa2365652ec012979d3ef17ec5c4c60b2301b"
},
"cargo/group-spacing-clippy-2/source-proof.json": {
"bytes": 601,
"sha256": "8ece8f2bf1e1ca5a5df2f5983a65573b046c84ddaa396906dc79269823fb698f"
},
"cargo/group-spacing-clippy-2/started.json": {
"bytes": 1563,
"sha256": "0f2a6bacc4a494ad3e753ed2608d0bd03c161fb27ac426378b4b237b3e059e8a"
},
"cargo/group-spacing-clippy-2/stderr.log": {
"bytes": 134,
"sha256": "0eaa0a26ef167caa5fc820dac39622d5aae74e3048fb5dec488713638bb1ed08"
},
"cargo/group-spacing-clippy-2/stdout.log": {
"bytes": 269431,
"sha256": "ed058495aca1b23078917c312823a8a55a488a0d51eb2a2a2d1029b0dc04aee0"
},
"cargo/group-spacing-clippy-3/process.json": {
"bytes": 179,
"sha256": "b885c9043fa1163a1f82f47fc63ce4f8bc6f8f07dd261c0c8fcbc542fd3b983b"
},
"cargo/group-spacing-clippy-3/result.json": {
"bytes": 1958,
"sha256": "4c84853553eaa155ed56ec313cddac59db1452e8d39424c41a0de52a2e023520"
},
"cargo/group-spacing-clippy-3/source-proof.json": {
"bytes": 601,
"sha256": "5932a0cba7607647967c84a8c8ac582bbfabc2e1c00b89242a14445924ea92a6"
},
"cargo/group-spacing-clippy-3/started.json": {
"bytes": 1563,
"sha256": "7bf0f106625864d432f8064645e692a1f635ede6bcb1f9d73e869cfcf33dfd26"
},
"cargo/group-spacing-clippy-3/stderr.log": {
"bytes": 134,
"sha256": "44c893a9fb01c36a19bb50efb4135844cf54a67f0b63b871effdf20f0235c5ea"
},
"cargo/group-spacing-clippy-3/stdout.log": {
"bytes": 269431,
"sha256": "89a806be408c3dae92647c6e6215d8597cd980e5926486787804f0d0a823ea37"
},
"cargo/group-spacing-format-1/process.json": {
"bytes": 116,
"sha256": "27be7eb0aa17687d8a9db613084b89de8a2911151af8b3cea39066e136e55dbb"
},
"cargo/group-spacing-format-1/result.json": {
"bytes": 1869,
"sha256": "461160515ff0551161c8f666976c7781f108a73d94346ab0b6a5b5cd944aab92"
},
"cargo/group-spacing-format-1/source-proof.json": {
"bytes": 601,
"sha256": "a177782dfa1c85efc817350537b965c11b40fdcec09fe01850442c86017289f5"
},
"cargo/group-spacing-format-1/started.json": {
"bytes": 1480,
"sha256": "ca53e726e096e24c6ce7501d622ddb32a6145385e4e6244b6e45a09ab2142533"
},
"cargo/group-spacing-format-1/stderr.log": {
"bytes": 0,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"cargo/group-spacing-format-1/stdout.log": {
"bytes": 0,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"cargo/group-spacing-format-2/process.json": {
"bytes": 116,
"sha256": "048bd326f3e14074bbf620b4e94ea7cc6ee1ee7099bbb02c6fd7cd4828a4ed0e"
},
"cargo/group-spacing-format-2/result.json": {
"bytes": 1869,
"sha256": "8c3e25b4c9270a65fa0b5b2ea0e819569a8f714baa73282ab49a40f604c475ad"
},
"cargo/group-spacing-format-2/source-proof.json": {
"bytes": 601,
"sha256": "2a88c77c7275472a74ce3a97f8dfebc38c87793bfbee4388fe4c52b2345c73c8"
},
"cargo/group-spacing-format-2/started.json": {
"bytes": 1480,
"sha256": "6c8e2f298186c70b1d5a06b68fb10530b70852278b65704b6bcf3de667872333"
},
"cargo/group-spacing-format-2/stderr.log": {
"bytes": 0,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"cargo/group-spacing-format-2/stdout.log": {
"bytes": 0,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"cargo/group-spacing-format-3/process.json": {
"bytes": 116,
"sha256": "5a5957fa2bd4ee626f9442c680a59af4f5da200cd7d2c2c769ba8dfa8eda13a2"
},
"cargo/group-spacing-format-3/result.json": {
"bytes": 1869,
"sha256": "1f9ccb94189574eb595fd4941985dd85592ca50fde65c5e89444b1e64d4230f6"
},
"cargo/group-spacing-format-3/source-proof.json": {
"bytes": 601,
"sha256": "0232c97e7a02b7971ae078ed2d4c3257e56e42e6bb9f031e195c3e7f895d166a"
},
"cargo/group-spacing-format-3/started.json": {
"bytes": 1480,
"sha256": "5c8f9417834a579bb4cbff7a25fb89f283a7d4563fca0cf9d9ed972a065dee3b"
},
"cargo/group-spacing-format-3/stderr.log": {
"bytes": 0,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"cargo/group-spacing-format-3/stdout.log": {
"bytes": 0,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"cargo/group-spacing-format-4/process.json": {
"bytes": 116,
"sha256": "95e6cbf35c191284c49fba65aa6028382edaf3c146ee959223ec1c0269692474"
},
"cargo/group-spacing-format-4/result.json": {
"bytes": 1869,
"sha256": "6c174fe42df8c36b78f48395bf5e5185e8d968212c0074afc2eb1e0305488f5e"
},
"cargo/group-spacing-format-4/source-proof.json": {
"bytes": 601,
"sha256": "a303f7255ae967155885e704c842ba578e888d2f11a78b3cf95d6722ea357893"
},
"cargo/group-spacing-format-4/started.json": {
"bytes": 1480,
"sha256": "c7c6eef0b25668b4a3850b13678b6742274382c2b8f83bf4a62bcc00e8386ffd"
},
"cargo/group-spacing-format-4/stderr.log": {
"bytes": 0,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"cargo/group-spacing-format-4/stdout.log": {
"bytes": 0,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"cargo/group-spacing-scenario-1/process.json": {
"bytes": 169,
"sha256": "bfd663721b39df60de39c248fb053a67f364a6725bbb4a83227974519217c1e5"
},
"cargo/group-spacing-scenario-1/result.json": {
"bytes": 3277,
"sha256": "a191bdc36a671f6c2a675d19870236cde7e86af2ce30651d2d499803b77c9c2b"
},
"cargo/group-spacing-scenario-1/source-proof.json": {
"bytes": 603,
"sha256": "3bd14b592fab993457a5770d1894f442d57103d662334994d8f7ed84ba08b3bc"
},
"cargo/group-spacing-scenario-1/started.json": {
"bytes": 1548,
"sha256": "e061e076acd292132f51c800856e0457a3d51f4519b15feb8187692eda780251"
},
"cargo/group-spacing-scenario-1/stderr.log": {
"bytes": 8066,
"sha256": "8f09e83cb1970d3e72bd50813c8d4ad64a63ac0dd968dfcd190fdf5091f963fd"
},
"cargo/group-spacing-scenario-1/stdout.log": {
"bytes": 329799,
"sha256": "3e1112c810ab2ba3d598fb05a0526a3596719629f82dcd503e1ceb4c275f7ebe"
},
"cargo/group-spacing-scenario-2/process.json": {
"bytes": 169,
"sha256": "33a5142748cf6d90a3e99d07658110c091d6748d77b9ab436c81bfc8804e4079"
},
"cargo/group-spacing-scenario-2/result.json": {
"bytes": 3273,
"sha256": "20c5b05b3819b84b875b388493588c471e742926288ea02f394b26908fef485a"
},
"cargo/group-spacing-scenario-2/source-proof.json": {
"bytes": 603,
"sha256": "059b06d427f3bc1a505f0ff612d54df5da01a1545f49eff0842c2d92851e42dc"
},
"cargo/group-spacing-scenario-2/started.json": {
"bytes": 1548,
"sha256": "54df040b57a3258bc78cc1acc3ec73bd0234fe468642304f48b154e45b286d6b"
},
"cargo/group-spacing-scenario-2/stderr.log": {
"bytes": 626,
"sha256": "c88cd5be27a7aae243f25d1ff24766313f7d97a32df7fac17da95a99f8574637"
},
"cargo/group-spacing-scenario-2/stdout.log": {
"bytes": 329364,
"sha256": "22f9cbc1fa2dd02a653aa2eab1741a1f3198ae32a2495aa2fbf4a6b172e84a9e"
},
"comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py": {
"bytes": 10094,
"sha256": "7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a"
},
"helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py": {
"bytes": 3778,
"sha256": "3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2"
},
"helpers/c86f51d9b8465341dab73d045923ec306b15ebabf7c97b81beac7afe73bd72a1.py": {
"bytes": 1396,
"sha256": "c86f51d9b8465341dab73d045923ec306b15ebabf7c97b81beac7afe73bd72a1"
}
}
}
@@ -1,110 +0,0 @@
"""Package explicitly selected closed gate captures without running Cargo."""
from pathlib import Path
import argparse
import json
import zipfile
from verify import RAW_FILES, pin, parsed, need, run_name, verify_data, verify_package
HERE = Path(__file__).resolve().parent
def collect(root, runs, helpers, comparators=()):
gates = root / ".local/scenario-group-gates"
candidates = [gates / name for name in ("gate.py", "source-proof.py")] + helpers
helper_bytes = [path.read_bytes() for path in candidates]
comparator_paths = [root / ".local/process-source-evidence-tools/verify.py", *comparators]
comparator_bytes = [path.read_bytes() for path in comparator_paths if path.is_file()]
payload, logical, specs = {}, {}, {}
def add(name, data):
stored = ("capsules/" + pin(data)["sha256"] + Path(name).suffix
if name.rsplit("/", 1)[1] in {"inputs.json", "inputs.zip"} else name)
need(stored not in payload or payload[stored] == data, "deduplication collision")
payload[stored] = data
logical[name] = stored
helper_catalog = {}
for name, data in zip(("gate.py", "source-proof.py"), helper_bytes):
helper = "helpers/" + pin(data)["sha256"] + ".py"
add(helper, data)
helper_catalog[name] = helper
for run in runs:
folder = gates / run
need(folder.is_dir(), "run directory missing: " + run)
paths = list(folder.iterdir())
names = {path.name for path in paths}
need(all(path.is_file() and not path.is_symlink() for path in paths)
and names in (RAW_FILES, RAW_FILES | {"source-proof.json"}), "exact closed raw files required: " + run)
raw = {path.name: path.read_bytes() for path in paths}
result = parsed(raw["result.json"])
need("finish" in result and result["inputsUnchanged"] is True, "unfinished/changed run refused: " + run)
matches = [data for data in helper_bytes if pin(data) == result["helper"]]
need(bool(matches), "exact historical helper unavailable for " + run + "; supply --helper PATH")
helper = "helpers/" + result["helper"]["sha256"] + ".py"
add(helper, matches[0])
for name, data in sorted(raw.items()):
add("cargo/" + run + "/" + name, data)
specs[run] = {key: result[key] for key in ("command", "head", "exitCode", "inputCount")}
specs[run]["helper"] = helper
if "source-proof.json" in raw:
proof = parsed(raw["source-proof.json"])
matches = [data for data in comparator_bytes if pin(data) == proof["comparator"]]
need(bool(matches), "exact source comparator unavailable for " + run + "; supply --comparator PATH")
comparator = "comparators/" + proof["comparator"]["sha256"] + ".py"
add(comparator, matches[0])
specs[run]["sourceProof"] = {"commit": proof["commit"], "pin": pin(raw["source-proof.json"]), "comparator": comparator}
manifest = {"schema": 1, "kind": "scenario-selection-cargo-gates", "runs": specs,
"captureHelpers": helper_catalog,
"logicalFiles": dict(sorted(logical.items())),
"members": {name: pin(data) for name, data in sorted(payload.items())}}
summary = verify_data(manifest, payload.__getitem__)
return payload, manifest, summary
def write_json(path, value):
with path.open("xb") as output:
output.write((json.dumps(value, indent=2) + "\n").encode("utf-8"))
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, required=True, help="checkout holding .local/scenario-group-gates")
parser.add_argument("--runs", nargs="+", required=True, help="explicit run names, including failures to retain")
parser.add_argument("--helper", type=Path, action="append", default=[], help="additional exact historical helper (repeatable)")
parser.add_argument("--comparator", type=Path, action="append", default=[], help="additional exact historical source comparator (repeatable)")
parser.add_argument("--output", type=Path, help="fresh, nonexistent package directory (required unless --check-only)")
parser.add_argument("--check-only", action="store_true", help="validate raw captures in memory without creating an archive")
args = parser.parse_args()
need(all(run_name(run) for run in args.runs) and len(args.runs) == len(set(args.runs)), "unique safe run names required")
need(args.check_only or args.output is not None, "--output is required unless --check-only")
if not args.check_only:
need(not args.output.exists(), "output must be a fresh directory: " + str(args.output))
payload, manifest, summary = collect(args.root.resolve(), args.runs, args.helper, args.comparator)
if args.check_only:
print("PASS raw consistency; no package written:", summary)
return
out = args.output.resolve()
out.mkdir(parents=True, exist_ok=False)
for name in ("evidence.zip", "manifest.json", "files.json"):
need(not (out / name).exists(), "existing package output refused: " + name)
for name in ("prepare.py", "verify.py", "README.md"):
data = (HERE / name).read_bytes()
destination = out / name
if destination.exists():
need(destination.read_bytes() == data, "different existing package tool: " + name)
else:
with destination.open("xb") as output:
output.write(data)
with zipfile.ZipFile(out / "evidence.zip", "x", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive:
for name, data in sorted(payload.items()):
info = zipfile.ZipInfo(name, date_time=(2026, 9, 23, 0, 0, 0))
info.compress_type = zipfile.ZIP_DEFLATED
info.external_attr = 0o100644 << 16
archive.writestr(info, data)
write_json(out / "manifest.json", manifest)
names = ("prepare.py", "verify.py", "README.md", "manifest.json", "evidence.zip")
write_json(out / "files.json", {name: pin((out / name).read_bytes()) for name in names})
need(verify_package(out) == summary, "written package differs from validated captures")
print("PASS package readback:", out, summary)
if __name__ == "__main__":
main()
@@ -1,194 +0,0 @@
"""Offline consistency checks only; never execute or extract archived inputs."""
from pathlib import Path, PurePosixPath
from datetime import datetime
import argparse
import hashlib
import io
import json
import math
import re
import stat
import zipfile
RAW_FILES = {"started.json", "process.json", "result.json", "stdout.log", "stderr.log", "inputs.json", "inputs.zip"}
OUTER_FILES = {"prepare.py", "verify.py", "README.md", "manifest.json", "evidence.zip"}
def need(condition, message):
if not condition:
raise ValueError(message)
def pin(data):
return {"bytes": len(data), "sha256": hashlib.sha256(data).hexdigest()}
def valid_pin(value):
return (isinstance(value, dict) and set(value) == {"bytes", "sha256"}
and type(value["bytes"]) is int and value["bytes"] >= 0
and isinstance(value["sha256"], str)
and re.fullmatch(r"[0-9a-f]{64}", value["sha256"]) is not None)
def safe(name):
return (isinstance(name, str) and bool(name) and "\\" not in name and ":" not in name
and all(ord(char) >= 32 for char in name)
and not PurePosixPath(name).is_absolute()
and all(part not in ("", ".", "..") for part in name.split("/")))
def run_name(name):
return isinstance(name, str) and re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]*", name) is not None
def parsed(data):
def pairs(items):
result = {}
for key, value in items:
need(key not in result, "duplicate JSON key: " + key)
result[key] = value
return result
def constant(value):
raise ValueError("nonfinite JSON constant: " + value)
return json.loads(data, object_pairs_hook=pairs, parse_constant=constant)
def inventory(archive, expected):
names = archive.namelist()
need(len(names) == len(set(names)) and set(names) == set(expected), "exact ZIP membership")
for entry in archive.infolist():
need(safe(entry.filename) and not entry.is_dir(), "unsafe ZIP member: " + entry.filename)
need(not stat.S_ISLNK(entry.external_attr >> 16), "ZIP symlink: " + entry.filename)
need(valid_pin(expected[entry.filename]), "invalid member pin: " + entry.filename)
need(entry.file_size == expected[entry.filename]["bytes"], "ZIP recorded size: " + entry.filename)
def ordered_times(*values):
times = [datetime.fromisoformat(value) for value in values]
return all(value.utcoffset() is not None for value in times) and times == sorted(times)
def verify_data(manifest, read):
"""Shared read-only verifier for packaged bytes or prepare's in-memory payload."""
need(manifest["schema"] == 1 and manifest["kind"] == "scenario-selection-cargo-gates", "manifest schema")
runs, logical, stored = manifest["runs"], manifest["logicalFiles"], manifest["members"]
helpers = manifest["captureHelpers"]
need(isinstance(helpers, dict) and set(helpers) == {"gate.py", "source-proof.py"}, "capture helper names")
need(all(isinstance(name, str) and re.fullmatch(r"helpers/[0-9a-f]{64}\.py", name)
for name in helpers.values()), "capture helper mapping")
need(isinstance(runs, dict) and bool(runs) and all(run_name(run) for run in runs), "explicit run inventory")
need(all(safe(name) for name in logical) and all(safe(name) for name in stored), "unsafe manifest path")
need(set(logical.values()) == set(stored), "unreferenced or absent stored members")
for name, expected in stored.items():
need(valid_pin(expected) and pin(read(name)) == expected, "stored member hash: " + name)
expected_logical = {"cargo/" + run + "/" + name for run in runs for name in RAW_FILES}
expected_logical.update(spec["helper"] for spec in runs.values())
expected_logical.update(helpers.values())
for run, spec in runs.items():
if "sourceProof" in spec:
proof_spec = spec["sourceProof"]
need(set(proof_spec) == {"commit", "pin", "comparator"}, run + ": exact source proof spec")
need(isinstance(proof_spec["commit"], str) and re.fullmatch(r"[0-9a-f]{40}", proof_spec["commit"]), run + ": named full source commit")
need(valid_pin(proof_spec["pin"]), run + ": source proof pin")
need(isinstance(proof_spec["comparator"], str) and re.fullmatch(r"comparators/[0-9a-f]{64}\.py", proof_spec["comparator"]), run + ": comparator path")
expected_logical.update({"cargo/" + run + "/source-proof.json", proof_spec["comparator"]})
need(set(logical) == expected_logical, "exact logical raw/helper inventory")
for name, destination in logical.items():
if name.startswith("cargo/") and name.rsplit("/", 1)[1] in {"inputs.json", "inputs.zip"}:
expected = "capsules/" + stored[destination]["sha256"] + PurePosixPath(name).suffix
need(destination == expected, "input capsule mapping: " + name)
else:
need(destination == name, "only input maps/archives may be deduplicated: " + name)
def raw(name):
return read(logical[name])
for name, helper in helpers.items():
need(helper == "helpers/" + pin(raw(helper))["sha256"] + ".py", "named helper hash: " + name)
seen_capsules = set()
for run, spec in runs.items():
prefix = "cargo/" + run + "/"
start = parsed(raw(prefix + "started.json"))
process = parsed(raw(prefix + "process.json"))
result = parsed(raw(prefix + "result.json"))
required_start = {"start", "command", "cwd", "head", "scope", "inputs", "archive",
"inputCount", "environment", "helper", "cargo", "rustc"}
need(required_start <= start.keys(), run + ": incomplete start receipt")
need(all(result.get(key) == value for key, value in start.items()), run + ": start/result relationship")
command = result["command"]
need(isinstance(command, list) and command and command[0] == "cargo"
and all(isinstance(arg, str) for arg in command), run + ": Cargo command")
need(command == process["command"] == spec["command"], run + ": command relationship")
need(result["head"] == spec["head"] and re.fullmatch(r"(?:[0-9a-f]{40}|[0-9a-f]{64})", result["head"]), run + ": HEAD")
need(type(result["exitCode"]) is int and result["exitCode"] == spec["exitCode"], run + ": recorded exit")
need(result["inputsUnchanged"] is True, run + ": changed inputs refused")
need(type(result["seconds"]) in (int, float) and math.isfinite(result["seconds"])
and result["seconds"] >= 0, run + ": duration")
need(type(process["pid"]) is int and process["pid"] > 0, run + ": process identity")
need(ordered_times(start["start"], process["observed"], result["finish"]), run + ": completed timestamp order")
for field, filename in [("inputs", "inputs.json"), ("archive", "inputs.zip"), ("stdout", "stdout.log"), ("stderr", "stderr.log")]:
need(result[field] == pin(raw(prefix + filename)), run + ": " + field + " pin")
need(valid_pin(result["helper"]) and result["helper"] == pin(raw(spec["helper"])), run + ": helper pin")
need(spec["helper"] == "helpers/" + result["helper"]["sha256"] + ".py", run + ": helper mapping")
inputs = parsed(raw(prefix + "inputs.json"))
need(isinstance(inputs, dict) and all(safe(name) for name in inputs), run + ": safe input inventory")
need(type(result["inputCount"]) is int and len(inputs) == result["inputCount"] == spec["inputCount"], run + ": input counts")
if "sourceProof" in spec:
proof_spec = spec["sourceProof"]
proof_bytes = raw(prefix + "source-proof.json")
proof = parsed(proof_bytes)
need(pin(proof_bytes) == proof_spec["pin"], run + ": source proof pinned bytes")
need(proof["run"] == run and proof["commit"] == proof_spec["commit"], run + ": proof run/commit identity")
need(proof["status"] == "PASS exact captured input inventory and Git blob bytes", run + ": source proof status")
need(proof["inputCount"] == len(inputs) and proof["inputMap"] == result["inputs"], run + ": proof input relationship")
need(proof["result"] == pin(raw(prefix + "result.json")), run + ": proof result relationship")
need(ordered_times(result["finish"], proof["verified"]), run + ": proof time after closed gate")
comparator = proof_spec["comparator"]
need(valid_pin(proof["comparator"]) and pin(raw(comparator)) == proof["comparator"], run + ": comparator bytes")
need(comparator == "comparators/" + proof["comparator"]["sha256"] + ".py", run + ": comparator identity")
need(sum(name.startswith("crates/vendor/iced_widget/") for name in inputs) == 71, run + ": vendor count")
capsule = (logical[prefix + "inputs.json"], logical[prefix + "inputs.zip"])
if capsule not in seen_capsules:
with zipfile.ZipFile(io.BytesIO(raw(prefix + "inputs.zip"))) as source:
inventory(source, inputs)
for name, expected in inputs.items():
need(pin(source.read(name)) == expected, run + ": source hash " + name)
seen_capsules.add(capsule)
artifacts = []
for line in raw(prefix + "stdout.log").decode("utf-8", errors="replace").splitlines():
try:
value = json.loads(line)
except json.JSONDecodeError:
continue
if isinstance(value, dict) and value.get("reason") == "compiler-artifact" and value.get("executable"):
artifacts.append(value)
need(isinstance(result["artifacts"], list), run + ": artifact records")
need(artifacts == [record["cargo"] for record in result["artifacts"]], run + ": Cargo JSON artifact relationship")
for record in result["artifacts"]:
need(set(record) == {"cargo", "readback"}
and (record["readback"] is None or valid_pin(record["readback"])), run + ": artifact readback pin")
print(run, "recorded exit", result["exitCode"], "inputs", len(inputs), "executable records", len(artifacts))
return {"runs": len(runs), "recordedFailures": sum(spec["exitCode"] != 0 for spec in runs.values()),
"logicalFiles": len(logical), "storedMembers": len(stored), "sourceCapsulePairs": len(seen_capsules),
"captureHelpers": helpers, "sourceProofs": sum("sourceProof" in spec for spec in runs.values())}
def verify_package(root):
files = parsed((root / "files.json").read_bytes())
need(set(files) == OUTER_FILES, "outer file inventory")
for name, expected in files.items():
need(valid_pin(expected) and pin((root / name).read_bytes()) == expected, "outer hash: " + name)
manifest = parsed((root / "manifest.json").read_bytes())
with zipfile.ZipFile(root / "evidence.zip") as archive:
inventory(archive, manifest["members"])
return verify_data(manifest, archive.read)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("package", nargs="?", type=Path, default=Path(__file__).resolve().parent)
args = parser.parse_args()
print("PASS offline consistency:", verify_package(args.package))
print("Historical receipts only; no source/binary execution or native acceptance claim.")
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
@@ -1,99 +0,0 @@
"""Offline archive/source/lifetime evidence checks. Screenshots require independent human review."""
from pathlib import Path,PurePosixPath
from decimal import Decimal
import argparse,hashlib,json,re,zipfile
def pin(data):return {'bytes':len(data),'sha256':hashlib.sha256(data).hexdigest()}
def main():
parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('directory',type=Path);args=parser.parse_args();root=args.directory.resolve()
manifest=json.loads((root/'manifest.json').read_text(encoding='utf-8'));raw=(root/'native-evidence.zip').read_bytes();assert pin(raw)==manifest['archive'],'Archive pin mismatch'
with zipfile.ZipFile(root/'native-evidence.zip') as archive:
names=archive.namelist();assert len(names)==len(set(names)) and set(names)==set(manifest['members']),'Archive member inventory mismatch'
for name in names:
p=PurePosixPath(name);assert not p.is_absolute() and '..' not in p.parts and '\\' not in name and ((archive.getinfo(name).external_attr>>16)&0o170000)!=0o120000,'Unsafe member'
files={name:archive.read(name) for name in names}
for name,data in files.items():assert pin(data)==manifest['members'][name],name
assert not any(PurePosixPath(n).suffix.lower() in ('.exe','.dll') for n in files),'Binary bytes unexpectedly included'
assert (root/'verify.py').read_bytes()==files['helpers/native-evidence-verify.py'],'External verifier differs from archived exact helper'
def js(name):return json.loads(files[name].decode('utf-8-sig'))
fixture=js('fixtures/fixture-manifest.json')
for name,expected in fixture['files'].items():assert pin(files['fixtures/'+name])==expected,'Fixture pin mismatch'
phase_names={'baseline':'baseline.arcadia','messages':'expected-messages.arcadia','messages-and-lanes':'expected-messages-and-lanes.arcadia'}
number=rb'[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?'
fields=[(rb'(occurrence @alpha_report; \}; send_y )('+number+rb')(;)',Decimal(184)),(rb'(occurrence @gamma_report; \}; send_y )('+number+rb')(;)',Decimal(384)),(rb'(lifeline @intake_lane; x )('+number+rb')(;)',Decimal(172)),(rb'(lifeline @review_lane; x )('+number+rb')(;)',Decimal(432))]
def check_source(actual,phase):
expected=files['fixtures/'+phase_names[phase]]
if phase=='baseline':assert actual==expected,'Baseline source differs';return
def section(data):
a=b' diagram trial_a ';b=b' diagram trial_b ';assert data.count(a)==data.count(b)==1
start=data.index(a);end=data.index(b);assert start<end;return start,end
ea,eb=section(expected);aa,ab=section(actual);replacements=[]
for pattern,wanted in fields[:2 if phase=='messages' else 4]:
em=list(re.finditer(pattern,expected[ea:eb]));am=list(re.finditer(pattern,actual[aa:ab]));assert len(em)==len(am)==1
value=Decimal(am[0].group(2).decode('ascii'));assert value.is_finite() and value==wanted==Decimal(em[0].group(2).decode('ascii')),'Wrong moved coordinate'
replacements.append((aa+am[0].start(2),aa+am[0].end(2),em[0].group(2)))
for start,end,spelling in sorted(replacements,reverse=True):actual=actual[:start]+spelling+actual[end:]
assert actual==expected,'Other source bytes differ'
config=js('review-config.json');assert config['runs']==manifest['runs'];reports=[]
for run in config['runs']:
prefix='runs/'+run['name']+'/'
prepared=js(prefix+'prepared.json');started=js(prefix+'started.json');finished=js(prefix+'finished.json');problems=[]
def require(condition,description):
if not condition: problems.append(description)
require(pin(files[prefix+'prepared.json'])==started['prepared'],'Started prepared receipt binding')
for rel,expected in prepared['immutable'].items():
name=prefix+rel
require((pin(files[name]) if name in files else manifest['excluded_binary_bytes'].get(name))==expected,'Immutable pin: '+rel)
require(prepared['helper']==pin(files[prefix+'evidence/native-harness.py']),'Native helper binding')
receipt=js(prefix+'evidence/result.json');proof=js(prefix+'evidence/source-proof.json');inputs=js(prefix+'evidence/inputs.json')
require(receipt['exitCode']==0 and receipt['inputsUnchanged'] is True,'Closed successful unchanged build')
require(proof['result']==pin(files[prefix+'evidence/result.json']),'Build result proof pin')
require(proof['inputMap']==receipt['inputs']==pin(files[prefix+'evidence/inputs.json']),'Input map binding')
require(proof['inputCount']==receipt['inputCount']==len(inputs),'Input inventory count')
require(proof['commit']==receipt['head']==prepared['source'],'Source identity')
gui=[a for a in receipt['artifacts'] if a['cargo']['target']['name']=='arcadia-studio' and not a['cargo']['profile']['test']]
require(len(gui)==1 and gui[0]['readback']==prepared['artifact']==manifest['excluded_binary_bytes'].get(prefix+'bin/arcadia-studio.exe'),'Exact GUI artifact binding')
require(finished['exitCode']==0 and finished['immutableUnchanged'] is True,'Normal exit and immutable report')
for stream in ['stdout','stderr']:require(finished[stream]==pin(files[prefix+'app.'+stream+'.log']),'Final log pin: '+stream)
require(finished['working']==pin(files[prefix+'working.arcadia']),'Final working source pin')
if prefix+'reopen-origin.json' in files:
origin=js(prefix+'reopen-origin.json');from_name='runs/'+origin['from_run']+'/'+origin['from_file'];require(from_name in files,'Reopen source present')
if from_name in files:require(pin(files[from_name])==origin['from_pin']==origin['copied_pin']==started['workingBefore'],'Reopened input exact copy binding')
else:require(started['workingBefore']==fixture['files']['baseline.arcadia'],'Fresh run begins at original baseline')
phases=dict(run.get('source_phases',{}));source_files=[n for n in files if n.startswith(prefix) and n.endswith('.arcadia') and '/evidence/' not in n and n!=prefix+'baseline.arcadia']
for name in source_files:
rel=name[len(prefix):];sidecar=name[:-8]+'.json'
phase=phases.get(rel)
if sidecar in files:
saved=js(sidecar)
if 'phase' in saved:
require(phase is None or phase==saved['phase'],'Snapshot phase assignment consistency');phase=phase or saved['phase']
require(saved['pin']==pin(files[name]),'Snapshot receipt source pin')
if phase not in phase_names:problems.append('Explicit phase missing: '+rel);continue
try:check_source(files[name],phase)
except (AssertionError,ValueError,KeyError) as error:problems.append('Source comparison '+rel+': '+str(error))
# Every referenced screenshot must be present; archive inventory pins its exact bytes.
captures=0
for name in files:
if name.startswith(prefix) and name.endswith('.json'):
obj=js(name)
if isinstance(obj,dict) and 'images' in obj:
captures+=1
for image in obj['images']:require(prefix+image['file'] in files,'Capture image missing: '+image['file'])
require(captures>0,'No native capture observations retained')
reports.append({'run':run['name'],'reviewer_accepted':run['accepted'],'checks_pass':not problems,'problems':problems,'capture_records':captures})
if run['accepted']:assert not problems,json.dumps(reports[-1],indent=2)
old=js('preflight/fixture-preflight-1/result.json') # Retained as history, never promoted to a pass.
current=js('preflight/fixture-preflight-2/result.json');assert current['status'].startswith('PASS') and current['cliUnchanged'] is True
cp='preflight/fixture-preflight-2/';cr=js(cp+'evidence/result.json');proof=js(cp+'evidence/source-proof.json')
assert current['receipt']==proof['result']==pin(files[cp+'evidence/result.json']) and current['proof']==pin(files[cp+'evidence/source-proof.json'])
cli=[a for a in cr['artifacts'] if a['cargo']['target']['name']=='arcadia' and not a['cargo']['profile']['test']];assert len(cli)==1 and cli[0]['readback']==current['cli']==manifest['excluded_binary_bytes'][cp+'bin/arcadia.exe']
assert {check['fixture'] for check in current['checks']}==set(fixture['files']) and len(current['checks'])==3,'All three fixture preflights required'
assert cr['exitCode']==0 and cr['inputsUnchanged'] is True and current['source']==cr['head']==proof['commit'],'Current CLI closed source binding'
for check in current['checks']:
name=check['fixture'];assert check['exitCode']==0 and check['json_report'] and check['fixtureUnchanged']
assert pin(files[cp+name+'.stdout.json'])==check['stdout'] and pin(files[cp+name+'.stderr.log'])==check['stderr']
assert pin(files[cp+'fixtures/'+name])==fixture['files'][name]
report=js(cp+name+'.stdout.json');assert isinstance(report,dict) and report['operation']=='check' and report['exit_code']==0 and report['written'] is False and not any(d['severity']=='error' for d in report['diagnostics']),'Raw CLI report must independently confirm valid read-only check'
print(json.dumps({'status':'PASS offline archive, source and receipt consistency checks','runs':reports,'old_preflight_retained':True,'limitations':['Human visual/interaction claims not inferred from screenshots.','Omitted executable/DLL bytes cannot be rehashed offline.','Build input map/source proof consistency checked; Git blobs not independently available offline.','Failed attempts retained with accepted=false are reported, not silently asserted passing.']},indent=2))
if __name__=='__main__': main()
@@ -1,87 +0,0 @@
# Scenario group spacing implementation review
2026-09-24. Read-only review of the local, uncommitted extension outside prerelease #37. No Cargo, Git, network or GUI operation. Tests authored independently have not been executed by this reviewer. Reviewed the frozen pure implementation and the completed controller click correction. **No unresolved material finding in this reviewed implementation; build/test and native evidence remain separate.**
## Addressed material finding
**[P2] Preserve stationary click-to-collapse for mixed and unsupported groups.** `behavior.rs:2236-2248` classifies a saved-view hit as a group spacing hit solely by coordinate capability and the pressed role. Therefore an existing mixed selection containing a local lifeline and local message, plain-clicked on either selected spacing-role object, bypasses `ScenarioGroupPress`. The widget immediately publishes spacing Begin (`:2330-2349`). `spacing/controller.rs` routes the original set to `capture_group`, which correctly rejects mixed membership. `app/scenario_spacing.rs` consumes that failed Begin by advancing `canvas_epoch`, making the later release inert. The result is refusal without collapsing to the clicked member, whereas the prior deferred group-press path emits `Intent::Collapse` on a stationary release (`behavior.rs:2384-2404`).
Rejecting the drag as a whole is required, but eligibility for moving the group should not remove the established selection-only click operation. Preserve a pending click/collapse route for ineligible groups, using exact original Context/Snapshot and Euclidean movement tracking; actual motion should refuse the whole group. A whole-set role preflight that retains the existing group_press path is one possible bounded fix. Add mounted tests for a lane+message group and a message+unsupported occurrence group: stationary click on each selected spacing-role member selects only it without a source/history change; motion retains the whole set and refuses; stale release after Context/selection change is inert.
**Correction reinspected:** the final controller stores `group_refusal` with the full original Snapshot. An eligibility refusal may use a successfully captured pressed singleton only to establish the unchanged full-scene geometry and actual camera/hit witness. `Session::update` returns before `Drag::prepare` whenever that refusal exists, so neither a partial preview nor a singleton command can be constructed from this fallback. Finish returns Click if the Euclidean threshold was never crossed; after crossing it returns the retained whole-set refusal, including off-axis/out-and-back motion. Invalid actual baseline, camera or hit still fails Begin and retires its epoch through App. Group paint retains original membership and primary. This resolves the identified click regression without making movement eligibility a selection requirement. The newly read App tests cover mixed stationary clicks on both roles, whole-set drag refusal/replay, malformed Begin retirement and source-write failure; execution is pending outside this review.
## Adapter findings without a defect so far
- App admission checks complete Scenario Context and camera epoch, then shared selection context/generation before any status, session or model side effect. Non-Begin packets additionally require exact session token/stamp and `Selection::matches` against the immutable captured Snapshot, including ordered typed targets and primary.
- A valid Begin captures the complete current Snapshot. A selected nonprimary spacing-role hit reaches Begin without replacing the group or its primary. Group capture receives the original typed target slice rather than an eligible subset.
- Cancel, Finish, and otherwise admitted failed Begin retire the canvas epoch; old mismatched packets return before touching a newer session. Current disabled input cancels its own admitted session. This preserves the camera visit rather than treating a selection-only change as a camera navigation event.
- Session preview configures the full group Snapshot and captured primary in both original and prepared scenes. The workspace retains the Canvas structure and displays selected count with spacing feedback.
- Group threshold is Euclidean in screen pixels while spacing delta remains axis-specific. `ever_crossed` survives a return to origin: off-axis or out-and-back motion can finish with no prepared command rather than collapse. An actually stationary supported group click collapses to the pressed member.
- Finish takes the session and retires its epoch before execution. Group success restores the captured primary before synchronization/reconciliation; only successful document execution accepts the frozen camera. `Document::execute` prepares the candidate and source update before committing, so source-update failure leaves model/source/history uncommitted while the gesture is retired.
## Frozen pure implementation review
No additional concrete finding in `behavior/scenario/spacing.rs`. The typed group entrypoint rejects empty, duplicate, wrong-target-type, mixed-role, missing, and unpressed-member sets before planning. Every captured origin then requires exact local ownership, appropriate semantic role, and a unique actually rendered header/message; the message path checks actual sequence/operand membership and blocks cycles or foreign references. Cross-operand local groups remain permitted without changing parents.
Planning uses frozen original display origins and the original Project, applies one common axis delta, deterministically orders commands independently of primary/pressed member/insertion order, and validates the final batch geometry. Any invalid member or final geometry refuses the whole plan. Skipping a member whose f32 coordinate is unchanged is a representational no-op, not an eligibility subset; it preserves missing/explicit overrides rather than manufacturing equivalent records. Zero/out-and-back input returns no command. Final candidate execution and source synchronization remain atomic through `Document::execute`.
Challenged the newly shared singleton origin checks: direct local lifeline ownership and unique rendered message path are stricter than merely selecting a drawing by ID, but I found no valid preexisting local singleton spacing workflow excluded. They intentionally reject malformed, repeated, referenced or unrendered input; rendered unassigned local headers remain supported. This conclusion is static and still depends on the singleton regression gates passing.
## Verification still needed
Run the parent's focused evaluators. In particular retain coverage for first-press nonprimary group drag, primary/order preservation, mixed/unsupported full-set refusal, current failed-Begin retirement, stale terminal packets against newer sessions, selection/representation ABA, Euclidean off-axis and out-and-back behavior, source-failure atomicity, and unchanged camera/visit. Supported groups return Noop after off-axis/out-and-back motion; movement-ineligible groups refuse the whole set after crossing, and neither collapses. The Scene's existing cached-pointer event policy is retained: this review does not claim a new position source for a release lacking CursorMoved. This review is not build, native usability, or release acceptance.
## Test-only correction review at 67534ce4
Independently compared `67534ce4a5fab29cb69b461f00d447eca2e7d12b` with failed source `3b113e5316724eca001cd92c5b8efed0a48ccc86` and read the retained run-1 failures (exit 101, unchanged inputs; 286 passed, 3 failed, 1 ignored). Only two test files changed. **No concrete concern that these three corrections hide a production defect.** No Cargo was run; the parent's run-2 result was not assumed.
- The complete-group movement test previously assigned fixed expected X values to randomly generated lane identities. Its failure shows exactly those expected X values reversed between the two lane IDs; actual automatic lane positions follow identity order (`behavior.rs:1468`). The correction authors explicit starting lane/message positions before capture. The expected complete Project remains independently constructed from those literal positions plus the screen-to-world delta; it does not copy the production result. Both pressed members, lane/message cases, preview nonmutation, captured Snapshot, primary/set preservation, exact Undo/Redo source restoration and reopen checks remain. This particular App test now covers explicit initial layout; unchanged pure tests still cover absent overrides and group movement from automatically resolved origins.
- The Properties cancellation test used the controller-only packet helper's generation zero against a live App selection. Filling its stamp from the current Snapshot matches real workspace packet construction and the existing App packet helper; it does not bypass production admission. The test still requires a real held session and moved preview before opening Properties, then checks session cancellation, unchanged stored camera, inert late Finish, pointer-identical document snapshot and no history entries.
- The mounted modifier test now expects a spacing Finish for a stationary saved group click instead of the retired direct `Intent::Collapse` transport. It retains the actual singleton target assertion immediately afterward and the later exact primary, representation/visit, camera, original document/source and empty history/session assertions. Shift toggle counts and absence of spacing/reorder for Shift input remain checked. This changes an internal message expectation while preserving the selection-only behavior oracle.
## Numeric annotation and evidence reuse at 4a27c8ef
Compared final `4a27c8ef0b8f4954e208fdfda5efd2b41c240577` with 67534ce4: only the two test-oracle variables `x` and `y` acquire explicit `f32` annotations. Previously inferred f64 literal values 148, 240, 160 and 400 are exactly representable in f32, and `f64::from` then restores the same exact f64 values before the unchanged f64 delta addition and f32 result cast. There is no numeric change in the oracle and no production change. No material finding.
Read the actual closed run-2 receipt/log: 289 passed, 0 failed, 1 ignored at 67534ce4, exit 0, inputs unchanged. Read the preserved clippy-1 failure at 67534ce4: two `useless_conversion` errors, exit 101, inputs unchanged. Final format-3 passes at 4a27c8ef. Clippy-2 had no closed receipt at review time. Reusing the executed 67534ce4 test evidence with final-source all-target lint (which checks the edited test code), product build and native exercise is proportionate for this exact annotation-only correction. Those final checks must actually pass and retain their own source pins; this conclusion neither invents their results nor represents the 289 tests as executed at 4a27c8ef. Further production edits or a concrete gate failure would require reassessing reuse. No Cargo was run by this reviewer.
## Copy-only follow-up at 8d6fac47
Reviewed all four changed production files between 4a27c8ef and `8d6fac4701933c7ada13cc7e6e07060662aa7465` (7 insertions, 4 deletions). Three refusal/menu strings replace the obsolete blanket movement-unavailable wording with unavailable reordering/removal or current-selection movement refusal. The fourth adds a saved-group Select tooltip; its new conditional selects text only. No operation admission, captured-state lifecycle, command or geometry path changes. No material defect found, and reuse of the 67534ce4 executed-test result remains proportionate with final source gates and native verification kept distinct.
Nonblocking wording observation sent to the parent: the saved-group tooltip is also shown for mixed/unsupported/fallback selections. “Drag a group of messages vertically or lifelines horizontally” would make homogeneous-group intent clearer than the current generic “Drag messages vertically or lifelines horizontally”; the actual full-set refusal remains enforced.
Read the newly closed clippy-2 receipt: exit 0, unchanged inputs at 4a27c8ef. Format-4 likewise closed exit 0 at 8d6fac47. Build-1 has started for `cargo build -p oa-desktop -p oa-cli --message-format=json` at 8d6fac47; no closed build, final lint or native result is claimed here. The 289 tests remain an execution result at 67534ce4 only.
## Native run-1 and helper audit
Reviewed `native-evidence-package.py`, `native-evidence-verify.py` (the file copied as portable `verify.py`), run-1 launch/close/build/fixture records and selected original screenshots. **No material helper/schema or run-1 evidence mismatch found.** Run-2 fresh reopen was still separate ongoing work, and no final portable package had yet been audited.
Independently rehashed every run-1 prepared immutable input, including the copied executable and DLL. The GUI is 119,703,552 bytes, SHA256 `e74342f3e06a28f91d378a429661fc88e5b009ce60549b3a2611160d48d16cdc`; it equals the uniquely identified non-test Studio artifact in the closed successful unchanged build at 8d6fac47. Source-proof/result/input-map bindings and the 811-input count agree. Started binds the exact prepared receipt. Finished records normal exit 0, immutable inputs unchanged, empty stdout/stderr, and final working source 3,554 bytes, SHA256 `360b9e24564a10cce3379a097bf30f33db5c583cceaaaf595fc595f662b556d5`.
All nine actual source files outside immutable evidence (eight saved snapshots plus final working file) pass their declared phases. The checker permits only exact Decimal-equivalent numeric spelling at the predeclared changed A coordinates; baseline remains byte-exact, and every other byte, including B, must match the pinned expected fixture. This is explicit numeric spelling handling, not float tolerance or a claim that the whole moved source has identical raw bytes to the integer-spelled expectation.
Exercised the complete offline verifier against an in-memory package assembled from actual run-1 records, both preflights, fixtures and helpers with the intended schema. It passed with no run problems and 52 capture records, including validation of all three current raw CLI check reports and retention of the differently shaped old failed preflight. This rehearsal wrote no archive/files and is not a final-package integrity result. The packager is create-only; actual final archive/member equality and run-2 origin binding still require their delivery audit.
Viewed full original screenshots 15/16: both selected message lines move down while count two and Alpha primary remain; unselected Beta stays in place and the final status reports two-object spacing. Screenshot 43 shows count two, mixed Intake primary, and full-set refusal feedback. Screenshot 45 shows count one and Alpha primary after stationary collapse. These establish visible endpoints; precise first-press input sequence is the executing agent's observation, and exact model/source claims come from the byte checks. No native Shift, Escape key, full preview timing, or fresh-reopen outcome is inferred by this review.
The original helper README/HARNESS/EVIDENCE files retain historical “prepared only/not run” statements. Keep those raw notes unchanged, but label them as preparation history in the final package's current report so they are not mistaken for the final run status. Executed tests remain 289/0/1 at 67534ce4; the final 8d6fac47 binary incorporates only the already reviewed test annotations and UI copy beyond that tested source. No product launch, build, or production edit occurred during this audit.
## Final native reopen and delivered-package audit
**No material package integrity or final-report overclaim found within this bounded audit.** Reviewed the final implementation report and root evidence index. They distinguish the 289 executed tests at 67534ce4 from final 8d6fac47 lint/build/native evidence, retain the two failed gates and obsolete-CLI preflight, explain historical helper names/preparation notes, and explicitly exclude combined integration, release and broad usability acceptance.
Correction to the run-1 paragraph above: the retained directory contains **nine** `.arcadia` snapshots (baseline, final-after-refusal-cancel, lane-move, lane-redo, lane-undo, message-move, message-offaxis, message-redo and message-undo), plus the working file: **ten** actual source files. The earlier eight-snapshot/nine-file count was a review wording error. The final package verifier checks all nine snapshots and working source; all declared phases pass. Run 2 adds one reopened-save snapshot.
Run 2 started at 2026-09-24T23:25:23.494008Z as PID 1036, after run 1 had closed, and finished normally at 23:27:42.098383Z with exit 0 and empty stdout/stderr. Independently rehashed its immutable files and checked the prepared/start bindings, distinct process, reopen-origin source/copy pins, snapshot sidecar and final receipt. The copied origin, launch input, reopened-save snapshot and final working source match run 1's actual saved 3,554-byte source SHA256 `360b9e24564a10cce3379a097bf30f33db5c583cceaaaf595fc595f662b556d5`. Both edited A groups therefore survive this fresh-process reopen/save without further source changes.
Viewed run-2 original actual-size screenshot 14. Saved A visibly retains the two moved message/lane groups at 100%; exact coordinates are established by the source comparison. The Review-to-Archive gap is now 228, and Beta's white label backing visibly interrupts a short Archive lifeline section above Beta. Labels remain ellipsized. These are bounded visual/readability limitations, not evidence that stored group coordinates failed; the report discloses them and appropriately avoids broad readability/accessibility acceptance. Screenshot 09 shows the intended run-2 path after filename refocus/retry. The failed local binding attempt before any action is the executing agent's reported tool recovery; neither that nor the recorded dialog focus retry supports a product-failure claim.
Ran both delivered portable verifiers successfully without launching Studio or Cargo. Checks: **10 closed runs, 2 preserved failures, 10 source proofs, 4 deduplicated source capsule pairs, 83 logical files and 71 stored members**, with 811 inputs per run. Native: **2 accepted closed runs, 67 capture records (52 + 15), 319 ZIP members and 6 explicitly excluded executable/DLL pins**. Independently compared all 80 archived raw Cargo/run files with their actual `.local/scenario-group-gates` originals. Independently verified all native member hashes and exact byte equality of its 318 retained raw/helper files to their local originals; the remaining member is generated review configuration matching the manifest. Also rehashed all six locally retained excluded binaries against their pins. This local binary rehash is additional review evidence; portable replay cannot rehash omitted bytes. Source proofs and artifact links were checked for internal agreement; this audit does not claim a new Git comparison of every captured input.
Audited archive identities:
- `checks/evidence.zip`: 12,897,498 bytes, SHA256 `d3de224502a2b7f8236d2ad82af6d6c57bb3a349468aebeea7369c3c3c267342`.
- `native/native-evidence.zip`: 6,069,072 bytes, SHA256 `27e0e189053fdfdbfcecd8c91ae718849448b090aab557afc7d9445af1ceaba8`.
The root index resolves the nonblocking historical checks-README title/example mismatch by identifying reused schema names and listing the actual group-spacing runs; original pinned bytes remain untouched. The archived preparation notes are explicitly historical. Review remains limited to source reasoning, selected visual endpoints, retained receipts/source bytes and packaging consistency; it does not independently reproduce the executing agent's complete input sequence or exercise native Shift/Escape, intermediate preview frames, accessibility, or another platform. No production edits, builds, native actions, Git mutations or network operations were performed for this final audit.
@@ -1,36 +0,0 @@
# Saved Scenario group spacing
Derived from [issue 29](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/29), the user-owned graphical editing requirements, the [multiple-selection contract](multiselect-contract-20260923.md), and existing Saved singleton spacing. This specifies the next bounded implementation, not completed runtime acceptance.
## Intent and scope
Ordinary dragging in a Saved Scenario already adjusts presentation spacing. Extend that operation to the complete homogeneous selected group: messages move their authored send coordinates by one vertical delta; lifelines move their authored horizontal positions by one horizontal delta. Measure each original coordinate from the actual rendered local occurrence, including lifecycle messages whose displayed receive row differs from the send endpoint. Click order and the pressed member do not choose a subset, reorder semantics or change primary.
Selected local messages may span immediate operands because spacing does not change their containment or step lists. All original targets must be unique, locally owned and actually rendered in the admitted Ready saved representation. Reject mixed lifeline/message sets, unsupported body/typed targets, foreign/referenced subjects, missing/ambiguous targets and fallback/unavailable originals as complete sets. Do not filter to eligible members. These refusals delimit this increment; mixed presentation movement, semantic reorder and aggregate removal remain required follow-ups.
Prepare all changed positions together in one command batch and validate the complete candidate geometry once. Preserve every semantic declaration, sequence, reference, binding, other view, route and unrelated authored position. The presentation resolver remains authoritative for legal layout; visible dependent geometry may respond to the new positions. Do not promise rigid translation of every line point, since lifecycle and other derived geometry already has its own constraints. Reject an invalid whole candidate without partially applying a member. A point whose requested delta is below its representable precision should not materialize or normalize its unchanged override.
## Input and lifetime
Capture full Scenario document/owner/representation/visit/revision/epoch, camera/bounds and exact shared selection Snapshot. Copy event packets carry the captured selection generation; admission matches it and context before capturing the full live Snapshot into the session. Every later packet must match the same session token/stamp and full snapshot before status, selection, reconciliation or mutation. Selection-generation changes, source/model changes, representation visits, camera/bounds changes, focus loss, Escape and outside release retire the applicable input lifetime.
The original pressed member must belong to a captured group. First-press movement onto a different singleton retains existing behavior. A stationary release on a selected group member performs the existing explicit collapse to that member. For groups, once pointer displacement reaches three screen pixels in any direction, returning to the origin or moving only along the inactive axis is a no-op retaining the group and primary, not a delayed collapse. Singleton threshold semantics remain unchanged. A preview retains the original group/primary and frozen camera; it changes no document/source/history.
Consume admitted terminal outcomes and admitted failed starts even if the document is unchanged, so an old Begin cannot restart after cancel/no-op/refusal. Reject stale packets before they can consume a newer operation. Commit one Document transaction after revalidating the captured selection; a source-write failure preserves the accepted document/source/history, camera and complete group. Successful Undo/Redo and reopened source must reproduce the complete expected project. Selection membership is transient and is not persisted into source.
Movement ineligibility must not disable ordinary selection: an otherwise admitted stationary press/release on a mixed group's member still collapses to that member. A pending eligibility refusal may retain that member's original display as a hit/camera witness, but must never prepare or preview its singleton move. Once movement crosses the threshold, report the complete-set refusal and preserve the group.
## Priority decision and alternatives
The existing [semantic sibling-block planner](scenario-group-reorder-policy-20260924.md) remains useful but does not fulfill the user's ordinary group-move request. Extending default Saved spacing now is more direct than first building an explicit semantic reorder handle or one-shot arm. It also avoids silently giving the same body drag a different meaning for singleton versus group. This changes implementation priority, not the semantic reorder contract.
For the later explicitly named Reorder operation, compare a fixed drag handle plus order preview with a compact destination chooser plus the same preview; an arm introduces an extra lifetime and potential next-drag ambiguity. Saved authored positions may differ from semantic order, so exact before-anchor/parent and proposed/resulting order must be observable. Geometric input must use its actual nearest slot without substituting a farther legal target; direct named-slot choice is a distinct input contract that must be specified explicitly. The comparison and unavailable-candidate-display policy remain open; no chooser or handle is accepted here.
## Discriminating acceptance
- Shift-select two messages in reverse order and drag either member; both send coordinates change by the same world delta, primary/membership and all semantic steps/other views stay exact. Repeat with two lifeline headers and a horizontal delta.
- Verify actual outgoing positions for lifecycle/self/repeated messages, local cross-operand groups, absent and existing overrides, and deterministic whole results independent of pressed/click order.
- Preview remains transient and displays the whole selection. One commit, one Undo/Redo, save/reopen and the following singleton spacing operation retain their intended meanings.
- Stationary collapse, off-axis drag, out-and-back, cancellation and invalid destination preserve the appropriate group/document and an existing redo branch. Mixed or hidden sets never become primary-only operations.
- Reject selection ABA, representation A→B→A, stale Begin/Hover/Finish/Cancel and duplicate packets before newer state is disturbed. Include dirty source/property drafts and atomic source-write failure.
- Run a mounted pointer/selection sequence and real packaged/direct desktop workflows, inspecting feedback and preserved group. Automated input and native operator evidence remain distinct; do not claim unexercised held-modifier accessibility or general usability.
-81
View File
@@ -1,81 +0,0 @@
# Scenario semantic reorder App increment — Windows evidence
Implementation and bounded Windows verification at `1c4099a9ef76dd28d17ef892e95edf89e860569f`, on the issue-29 working branch. This succeeds the historical [interaction experiment](scenario-reorder-interaction-20260924.md) and implements the [reorder policy](scenario-group-reorder-policy-20260924.md). It does not claim integration into the default branch, a new release, or completion of all issue-29 work. The issue-37 prerelease is unchanged; Linux remains user-paused.
At this exact source, Scenario tests **315 passed / 0 failed / 1 ignored / 1,296 filtered**, formatting, strict desktop Clippy and the desktop/CLI build passed, each with 818 unchanged inputs and exact source proof. Native run 2 confirms the corrected chooser at ordinary and compact sizes; fresh-process run 3 confirms exact saved-source reopen. The complete checks package retains **17 runs including five failures**, rather than replacing failed history.
[Portable evidence and verification](scenario-reorder-app-evidence-20260924/README.md): checks ZIP 29,229,448 bytes, SHA-256 `80406d81e0a5d47bea2ef284bc50a079713d828e8ea291cb96cfcedfeaa16235`; native ZIP 12,792,711 bytes, SHA-256 `0686d691a6ecea80310c5ee31056de36a7389d89a41ca501e6db9427f1ba4a82`. Both standalone verifiers pass their documented consistency checks. Source, command, UI and platform limits below remain part of acceptance.
## Implemented behavior and design
The Scenario header's Reorder control opens an explicit semantic destination chooser for the entire exact selection. Ordinary Saved message/lifeline drags retain their spacing meaning. Ordered local occurrences must be visible and share one immediate Scenario/Branch parent; unsupported or cross-parent members refuse the complete set. Selection does not grant movement permission. A selected anchor is an explicit no-op; append is explicit; there is no default destination.
The chooser searches full names, aliases and positions, pages five exact destinations at a time, and compares before/after semantic neighborhoods. The selected block follows original sequence order, independent of click order or primary. Exact destination and display consequences remain beside Apply in a fixed footer. This addresses the preceding experiment's counterexample: tied Saved Y positions make some semantic destinations unreachable through unchanged nearest-Y-only dragging. The chooser is a justified identity-based implementation; it does not establish that a graphical handle can never work or that all future interaction choices are settled.
Header access uses the existing keyboard control mechanism. Successful Begin requests token-specific query focus. Enter previews a unique search match; Ctrl+Enter applies the exact displayed proposal, ignores autorepeat, and is consumed when no valid proposal exists. Escape cancels. The current child overlay gets priority: an arrangement picker opened earlier in the same Iced input batch must block cached Apply. A stale focus task first checks for its exact target and cannot blur a newer query. The chooser is outside the camera Viewport so that its input reaches text and scrolling controls.
One immutable Session captures Project, full Scenario Context, complete selection Snapshot and token. It retains only the current exact-Slot proposal; repeated current Slot reuses it, a changed/revisited Slot recomputes. Commands remain private until consumed Finish. The App rejects stale packets before touching a newer session, retires admitted Begin before fresh capture, and retires terminal success/no-op/refusal/source-write failure. Finish binds the actual displayed Slot, preventing queued Apply(A) from committing preview(B). One Document transaction changes the parent list and preserves all authored layouts, unrelated views, selection and primary. No-op/cancel preserve history, including Redo.
Semantic validity and candidate presentation are separate. Baseline Fallback permits explicit semantic choice only when all selected subjects are actually visible; it does not enable coordinate editing. Candidate Fallback may commit after explicit adjacent warning while retaining the requested Saved representation and authored coordinates. Unavailable candidates or candidates hiding any original selected target cannot commit. Real fallback tests and synthetic guard injections have different evidential scope.
A post-commit result identifies parent, destination, affected count and actual ordered neighborhood using ordinals and full aliases, so repeated names and unchanged Saved Y positions do not obscure the result. The revised summary lifetime ignores passive input-epoch changes but retains document/owner/representation/visit/revision and exact selection generation/membership/primary checks. Live operation admission still requires the full epoch. At `1c4099a9`, admitted spacing Finish reconciles the summary after multiselection reconciliation; stale spacing rejects still return before this change. The passing regression commits real spacing after reorder, checks immediate summary removal without another event, confirms the semantic list/selection remain, and verifies Undo returns to the reordered source. This closes the earlier early-return gap at the source/test level.
## Executed gates and retained failures
Raw receipts, streams, input inventories/archives and source proofs originated under `.local/scenario-group-gates/` and are published in the checks archive's logical-file map. Every entry below records unchanged captured inputs; scenario gates 1–8 each have a PASS exact-inventory/Git-blob source proof. This report reads those records; it does not rerun gates or independently repeat their Git comparison. Commands were Windows GNU/LLVM Rust/Cargo 1.98.1, using the project toolchain environment, one Cargo job and incremental compilation disabled (format-2 records no job variable; formatting does not compile).
| Gate | Exact source | Result and interpretation |
| --- | --- | --- |
| `reorder-app-test-1` | `61099ed4742eaa51c3bd81129bc072d8321961b7` | Compile failure: ambiguous `column` macros and private test fixture helper import. |
| `reorder-app-test-2` | `f3ed6e97dd3caf06046711cf74d268c7274bc780` | `reorder_` filter: 23 pass, 0 fail, 1,581 filtered. Selected only one of nine App tests; not nine-test coverage. |
| `reorder-app-scenario-1` | `f3ed6e97dd3caf06046711cf74d268c7274bc780` | 301 pass, 6 fail, 1 ignore. Five fixtures omitted Derived Behavior navigation; sixth incorrectly expected comma normalization to exceed the source budget. |
| `reorder-app-scenario-2` | `c9f6a3da40e288e9f993d9cacf516142c0017cf3` | 307 pass, 0 fail, 1 ignore after fixture/oracle corrections. |
| `reorder-app-scenario-3` | `652ffb4d1b21f414a69ebac549459f4bb9e3c16a` | Compile failure: nonexistent `text_input::focus` API. |
| `reorder-app-scenario-4` | `9bfa113aa52a957ef268839b5e6d87f97b20d6ea` | Compile failure: widget operation `finish` receiver required `&self`, not `&mut self`. |
| `reorder-app-scenario-5` | `04485e001798430fb1b2e8d1347e6bde51e6e7a1` | 310 pass, 0 fail, 1 ignore; all 12 App tests included. Strict Clippy also passed at this source. |
| `reorder-app-scenario-6` | `14df924b98d74144c1e40fbe7098994ff5eb2e94` | 310 pass, 0 fail, 1 ignore; format, strict Clippy and build also passed at this source. Used for native run 1. |
| `reorder-app-scenario-7` | `201097793c010a181d113bdec7c5bf7ec81eacf9` | Compile failure in new summary test: `oa_core::SourceBundle` is not exported at that path. This does not validate the new layout/summary fixes. |
| `reorder-app-scenario-8` | `1c4099a9ef76dd28d17ef892e95edf89e860569f` | 315 pass, 0 fail, 1 ignore. Includes all 17 App tests: 12 previous adapter/keyboard cases, one long-alias mounted layout case, four summary-lifetime cases. Format-2 also passes at this source. |
The other final-source receipts are `reorder-app-format-2` (`cargo fmt --all -- --check`), `reorder-app-clippy-3` (`cargo clippy -p oa-desktop --all-targets --message-format=json -- -D warnings`) and `reorder-app-build-2` (`cargo build -p oa-desktop -p oa-cli --message-format=json`), all exit 0 with exact 818-input source proofs. The complete 17-run package additionally retains the earlier format-1, clippy-1/2 and build-1 receipts; the five failures are test-1 and scenario-1/3/4/7.
Scenario gates use `cargo test -p oa-desktop scenario --message-format=json -- --nocapture`; completed test executions above each exclude 1,296 other tests. The single ignored test is the existing timing case. Inventories contain 817 inputs through scenario-6 and 818 at scenario-7/8. Scenario-8 and format-2 both record exact 818-input source proofs. The bounded evidence review independently counted all 17 named App passes in raw scenario-8 output and verified both runs' stdout/stderr/input-map/archive hashes and lengths against receipts; it did not repeat Git-blob comparison. Passing source proofs establish source attribution, not functional success for failed gates. All failures must remain in portable evidence.
Coverage includes exact whole-model/source Undo/Redo, selection preservation, no-op Redo, temporal refusal, stale packets/selection ABA, real baseline Fallback, synthetic Unavailable/hidden-target guards, and mounted query/keyboard/overlay ownership. The source writer reorders existing identity chunks with their trivia; the corrected exact-budget test establishes successful byte-preserving permutation and exact Undo readback. **No real App source-write failure fixture was established.** Its error branch remains a runtime coverage gap; separate Document atomicity evidence is not a substitute for executing that adapter branch.
## Native run 1: observations and defects
The retained `.local/scenario-reorder-app-native/run-1-observations.md` records source `14df924b…`, build-1 and current CLI preflight-2. All four fixture checks exited zero with expected incomplete-model warnings. The fixture has 24 repeated display names, tied B/C positions and a distant report_24. Selection used click plus contextual Add to selection; this is **not** native Shift-click evidence. Initial nonmaximized navigation misses and a file-picker UIA failure remain disclosed; maximizing and ordinary filename typing allowed the trial to proceed.
Observed: no initial destination; Enter alone did not enable Apply; search `@c` auto-focused and Enter previewed B/A/D/C despite tied geometry; Escape and explicit Cancel retained selection; scrolling kept the footer available and did not pan the canvas. Ctrl+Enter applied before-C once, retaining count two/primary A and authored coordinates. One Undo restored the exact 12,764-byte baseline; one Redo restored the exact applied source SHA-256 `f48147dbc5101d8dd0386f72ef769d8684935a410d7e60b85d420033d5243f06`. Normal close exited zero with empty process streams and unchanged immutable inputs.
Two product defects prevent treating this run as final acceptance:
- Captures 24/28 show After text clipped beneath the floating scrollbar. The later source reserves a gutter, propagates Fill widths and uses WordOrGlyph wrapping. The mounted long-alias layout evaluator passes in scenario-8; corrected-source native screenshots in run 2 confirm readable columns and a reserved scrollbar gutter.
- New Saved-order diagnostics reduced viewport height by 40px and erased the result summary through epoch equality. The revised lifetime described above preserves passive-resize/camera results while retiring substantive changes in four passing summary tests at scenario-8. Diagnostic wording now identifies aliases and explains disagreement between positions and execution order. Corrected-source native run 2 confirms persistent result summaries after real diagnostic height changes.
The original native full-file oracle also failed at byte 647: expected comma spacing differed from the writer's moved reference trivia. This was an evaluator error, not evidence of coordinate/semantic corruption. Keep its failed receipt and original expectations intact; a corrected exact oracle must be separately pinned and must continue to compare every other source byte. Do not silently normalize arbitrary differences.
## Corrected native run 2 and fresh-process run 3
Run 2 uses source `1c4099a9…` and build-2. The executable is 119,900,672 bytes, SHA-256 `123e5bebda06c8446cb86c3e071599feea318e69953a949f64b29ec3010d2717`. Independent review of original captures finds readable Before/After columns, a fixed destination/action footer, and a persistent semantic result after diagnostics resize the viewport. Actual image sizes are **1605 × 872** and **1104 × 732**. At compact size the comparison body requires scrolling; footer and result remain visible. These dimensions are capture sizes, not exact client-area claims.
The repeated-name D-then-A selection moves in semantic A/D order. Before-C is separately reachable despite B/C tied Y. Search reaches offscreen `@report_24`; a later no-match query preserves the previously explicit proposal and labeled destination. Cancellation and selected-anchor no-op preserve Redo. Exact snapshots independently confirm baseline after Undo/cancel/no-op, identical before-C after Redo, far-anchor commit, and the final before-C commit. All are 12,764 bytes, compared without normalization against separately pinned v2 expected files:
| Phase | Snapshot numbers | SHA-256 |
| --- | --- | --- |
| baseline | 29, 34 | `0ad45e5ec3768862823175d9664c0e746e2a3afbc4df367b14477dc9f8705bf7` |
| before-C | 27, 36, 76 | `f48147dbc5101d8dd0386f72ef769d8684935a410d7e60b85d420033d5243f06` |
| before report_24 | 62 | `0b1cf1bba8ba2df869d34210b611d9551821ab24fdd4fbefd29effa76167dd3e` |
Captures 64–71 confirm native keyboard entry: F6 enters the viewport; six Shift+Tab steps reach Reorder through Actual Size, Zoom In, Zoom Out, Fit and Details; Enter opens the focused query. This relatively long route and subtle viewport outline remain discoverability limitations. Earlier F6/Tab/Shift+Tab returned to the viewport and was not a blocked Reorder route. Escape cancellation is observed. Native selection remains click plus contextual Add; native Shift-click is not claimed. Captures 44–56 retain system window-sizing recovery; resize cancels the live operation under exact-epoch rules, followed by a fresh compact operation.
Run 3 starts a fresh copied process and explicitly uses run-2 snapshot 76 as its prepared initial source. Original captures 14/15 show Saved A with persisted ordinals B1/A2/D3/C4 and the expected alias-based diagnostic notices. Selection is zero in the new process, as appropriate for transient selection. Independently, the prepared initial pin, run-2 snapshot 76, run-3 snapshot 15, final working source and pinned v2 before-C expectation are all exactly equal, with SHA `f48147…3f06` above. This is saved-source persistence evidence, not persistence of transient selection or operation history.
Runs 2 and 3 both closed normally with exit 0 and empty stdout/stderr. Their complete prepared immutable inventories were independently rehashed and matched; final source and log pins match the close receipts. Separate local reviews are retained as `run-2-review.md` and `run-3-review.md` in the native evidence materials. Run 1 remains findings-only evidence, including original evaluator failure; v2 expectations do not retroactively turn it into an accepted run.
## Verification and delivery limits
The bounded increment is implemented and verified for the stated source and Windows workflows. Tests and native observations do not establish arbitrary window sizes, native temporal-refusal/fallback scenarios, screen-reader completeness, or Linux behavior. Accessibility captures expose mostly the window shell; native focus is supported by visual highlights and the observed route, with separate mounted input coverage. The real App source-write error branch still lacks a runtime failure fixture; Document atomicity evidence remains separate. Aggregate removal and broader issue-29 completeness remain separate work.
The native package retains all 535 members from three closed run attempts and two CLI preflights, including original failures, exact helpers, source proofs and original captures. Run 1 remains unaccepted with its failed source-oracle receipt; run 2/3 and matching current CLI preflight are explicitly accepted only for the bounded cases above. Omitted executable/DLL bytes retain their build/readback pins; an offline archive check cannot rehash omitted bytes. No default-branch integration or new published release is asserted by this report.
@@ -1,42 +0,0 @@
# Scenario semantic reorder: portable Windows evidence
Tested production source: `1c4099a9ef76dd28d17ef892e95edf89e860569f`, branch `codex/scenario-multiselect-9e8r6t-20260924`. The delivery descendant adds records only. [Implementation, exact gate history, native observations and limitations](../scenario-reorder-app-20260924.md); [policy](../scenario-group-reorder-policy-20260924.md). Live coordination belongs to [issue 29](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/29). This is branch evidence, not integrated/release acceptance. Linux remains user-paused.
## Offline verification
From the checkout, Python 3.11+ standard library:
```text
python -B harness/scenario-reorder-app-evidence-20260924/checks/verify.py
python -B harness/scenario-reorder-app-evidence-20260924/native/verify.py harness/scenario-reorder-app-evidence-20260924/native
```
Neither verifier executes archived code/binaries, launches a GUI or contacts a network. A consistency PASS does not turn a failed historical run into a passing test. The native verifier reports the original run-1 snapshot failure explicitly, with that attempt unaccepted.
| Archive | Bytes | SHA-256 |
| --- | ---: | --- |
| [checks/evidence.zip](checks/evidence.zip) | 29,229,448 | `80406d81e0a5d47bea2ef284bc50a079713d828e8ea291cb96cfcedfeaa16235` |
| [native/native-evidence.zip](native/native-evidence.zip) | 12,792,711 | `0686d691a6ecea80310c5ee31056de36a7389d89a41ca501e6db9427f1ba4a82` |
Checks retain 17 runs, five failures, 17 exact source proofs and nine source capsules. Final-source Scenario315/0/1, format, strict desktop all-target Clippy and Studio+CLI build pass; final inventories each contain818 inputs. Filters are scoped, not the full desktop suite. The checks README describes the generic capture format; manifest records the actual selected reorder runs.
Native retains535 members: three closed native attempts, two closed CLI preflights, every original capture/receipt, strict source snapshots, two separately pinned oracle revisions, helpers and reviews. Run1 at14df is historical findings-only; its original whitespace-oracle failure stays unchanged. Current-source run2 has78 capture records and six exact snapshots; fresh-process run3 has15 capture records and one exact reopened snapshot. Both accepted attempts exit0 with empty streams and unchanged immutables. CLI preflight3 matches their source and v2 fixture manifest. Excluded executable/DLL bytes are represented by measured build/runtime/readback pins, not bundled runnable release assets.
Inside the native ZIP, `helpers/run-2-observations.md` records parent actions and recovery; `helpers/run-2-review.md` and `helpers/run-3-review.md` independently inspect original screenshots and exact source/lifecycle bytes. `review-config.json` declares each attempt's scope, phases and reopen relationship. `runs/run-2/` and `runs/run-3/` contain originals; `oracle-revisions/oracle-v2/README.md` explains the strict corrected expectation. Use the [development review](reviews/development-review.md) for the sequence of findings/corrections, with historical pending statements interpreted at their stated source.
## Reproduce the Windows checks
Use the project's documented Windows GNU/LLVM toolchain and Rust/Cargo1.98.1 with `CARGO_BUILD_JOBS=1`, `CARGO_INCREMENTAL=0`. Original commands/environment and measured compiler/binary inputs are in receipts. At the tested commit:
```text
cargo test -p oa-desktop scenario --message-format=json -- --nocapture
cargo fmt --all -- --check
cargo clippy -p oa-desktop --all-targets --message-format=json -- -D warnings
cargo build -p oa-desktop -p oa-cli --message-format=json
```
The native launcher copies the build's Studio executable and measured `libunwind.dll` into a clean bin directory, uses a separate cwd and a Windows-system-only child PATH, and opens working.arcadia through the application File UI. It does not claim a GUI model-path command-line argument. Preparation, launch, source phase snapshots, and normal closes are captured by helpers in the archive. Required runtime SHA is `f45604a12e8bcd839fd86f667c1be1cc7c4f9da2b3c69ff381ac708f0e1ae7d4`.
Native selection used contextual Add; mounted tests separately cover modifier handling. Actual capture sizes1605×872 and1104×732 do not establish all sizes or screen-reader accessibility. Runtime coverage of a real App source-write failure remains open. Aggregate removal and full issue29 acceptance are separate work. The published issue37 prerelease is unchanged.
[Final bounded audit](reviews/final-audit.md) independently repeats both offline verifiers, compares package members with original records, checks local omitted-binary pins and inspects selected original native images. It records reviewer authorship limits and makes no remote-publication claim.
@@ -1,14 +0,0 @@
# Scenario selection captured checks
These tools package explicitly named, completed runs from `CHECKOUT/.local/scenario-group-gates` without executing archived source or running Cargo. Adapted only in input-directory and manifest-kind names from the published prerelease Connection/Route evidence tools at `93d2a6f9af40061bf92cd81ba33d5648d0181cb1`.
```powershell
python -B prepare.py --root CHECKOUT --runs selection-scenario-1 selection-scenario-2 selection-format-1 --output NEW_DIRECTORY
python -B NEW_DIRECTORY/verify.py
```
The manifest is authoritative for the selected runs. Failed runs remain failures: archive consistency is not a test pass. Every raw stream and receipt retains its original bytes. Identical input maps and ZIPs are stored by hash; the logical-file map preserves their original run paths. Historical capture helpers and exact source comparators are included as data, never executed by verification.
Source proofs compare every captured input with a named Git commit; the offline package verifier checks those receipts and their pinned comparator identity but does not repeat the Git comparison. The captured HEAD and the named proof commit retain their respective meanings. The verifier checks archive inventory, hashes, input ZIP contents, process/command/result consistency and executable artifact relationships. It does not establish hermetic builds, native workflow acceptance, UI integration, or release acceptance.
Use `--check-only` instead of `--output` for an in-memory consistency check. Existing output directories refuse. Python 3.11+ standard library is sufficient; `-B` avoids bytecode writes.
@@ -1,22 +0,0 @@
{
"prepare.py": {
"bytes": 6308,
"sha256": "2d500f5d43a49893abb3936f880e61b8adc623531e26f0288983502e386894db"
},
"verify.py": {
"bytes": 11436,
"sha256": "ea8e781003bec21f2ac4af23e21b68ca5639a98eb31505c10b2869f57565e917"
},
"README.md": {
"bytes": 1641,
"sha256": "4c5c582cbdf02c8704b9add656d273c275b10eed87219131e3bba639a4c4bd8a"
},
"manifest.json": {
"bytes": 47794,
"sha256": "4044c2c4ba6985022c68ba2802bee66f0f24ee454f77fbd0db2c6eee4d936ed4"
},
"evidence.zip": {
"bytes": 29229448,
"sha256": "80406d81e0a5d47bea2ef284bc50a079713d828e8ea291cb96cfcedfeaa16235"
}
}
File diff suppressed because it is too large Load Diff
@@ -1,110 +0,0 @@
"""Package explicitly selected closed gate captures without running Cargo."""
from pathlib import Path
import argparse
import json
import zipfile
from verify import RAW_FILES, pin, parsed, need, run_name, verify_data, verify_package
HERE = Path(__file__).resolve().parent
def collect(root, runs, helpers, comparators=()):
gates = root / ".local/scenario-group-gates"
candidates = [gates / name for name in ("gate.py", "source-proof.py")] + helpers
helper_bytes = [path.read_bytes() for path in candidates]
comparator_paths = [root / ".local/process-source-evidence-tools/verify.py", *comparators]
comparator_bytes = [path.read_bytes() for path in comparator_paths if path.is_file()]
payload, logical, specs = {}, {}, {}
def add(name, data):
stored = ("capsules/" + pin(data)["sha256"] + Path(name).suffix
if name.rsplit("/", 1)[1] in {"inputs.json", "inputs.zip"} else name)
need(stored not in payload or payload[stored] == data, "deduplication collision")
payload[stored] = data
logical[name] = stored
helper_catalog = {}
for name, data in zip(("gate.py", "source-proof.py"), helper_bytes):
helper = "helpers/" + pin(data)["sha256"] + ".py"
add(helper, data)
helper_catalog[name] = helper
for run in runs:
folder = gates / run
need(folder.is_dir(), "run directory missing: " + run)
paths = list(folder.iterdir())
names = {path.name for path in paths}
need(all(path.is_file() and not path.is_symlink() for path in paths)
and names in (RAW_FILES, RAW_FILES | {"source-proof.json"}), "exact closed raw files required: " + run)
raw = {path.name: path.read_bytes() for path in paths}
result = parsed(raw["result.json"])
need("finish" in result and result["inputsUnchanged"] is True, "unfinished/changed run refused: " + run)
matches = [data for data in helper_bytes if pin(data) == result["helper"]]
need(bool(matches), "exact historical helper unavailable for " + run + "; supply --helper PATH")
helper = "helpers/" + result["helper"]["sha256"] + ".py"
add(helper, matches[0])
for name, data in sorted(raw.items()):
add("cargo/" + run + "/" + name, data)
specs[run] = {key: result[key] for key in ("command", "head", "exitCode", "inputCount")}
specs[run]["helper"] = helper
if "source-proof.json" in raw:
proof = parsed(raw["source-proof.json"])
matches = [data for data in comparator_bytes if pin(data) == proof["comparator"]]
need(bool(matches), "exact source comparator unavailable for " + run + "; supply --comparator PATH")
comparator = "comparators/" + proof["comparator"]["sha256"] + ".py"
add(comparator, matches[0])
specs[run]["sourceProof"] = {"commit": proof["commit"], "pin": pin(raw["source-proof.json"]), "comparator": comparator}
manifest = {"schema": 1, "kind": "scenario-selection-cargo-gates", "runs": specs,
"captureHelpers": helper_catalog,
"logicalFiles": dict(sorted(logical.items())),
"members": {name: pin(data) for name, data in sorted(payload.items())}}
summary = verify_data(manifest, payload.__getitem__)
return payload, manifest, summary
def write_json(path, value):
with path.open("xb") as output:
output.write((json.dumps(value, indent=2) + "\n").encode("utf-8"))
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, required=True, help="checkout holding .local/scenario-group-gates")
parser.add_argument("--runs", nargs="+", required=True, help="explicit run names, including failures to retain")
parser.add_argument("--helper", type=Path, action="append", default=[], help="additional exact historical helper (repeatable)")
parser.add_argument("--comparator", type=Path, action="append", default=[], help="additional exact historical source comparator (repeatable)")
parser.add_argument("--output", type=Path, help="fresh, nonexistent package directory (required unless --check-only)")
parser.add_argument("--check-only", action="store_true", help="validate raw captures in memory without creating an archive")
args = parser.parse_args()
need(all(run_name(run) for run in args.runs) and len(args.runs) == len(set(args.runs)), "unique safe run names required")
need(args.check_only or args.output is not None, "--output is required unless --check-only")
if not args.check_only:
need(not args.output.exists(), "output must be a fresh directory: " + str(args.output))
payload, manifest, summary = collect(args.root.resolve(), args.runs, args.helper, args.comparator)
if args.check_only:
print("PASS raw consistency; no package written:", summary)
return
out = args.output.resolve()
out.mkdir(parents=True, exist_ok=False)
for name in ("evidence.zip", "manifest.json", "files.json"):
need(not (out / name).exists(), "existing package output refused: " + name)
for name in ("prepare.py", "verify.py", "README.md"):
data = (HERE / name).read_bytes()
destination = out / name
if destination.exists():
need(destination.read_bytes() == data, "different existing package tool: " + name)
else:
with destination.open("xb") as output:
output.write(data)
with zipfile.ZipFile(out / "evidence.zip", "x", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive:
for name, data in sorted(payload.items()):
info = zipfile.ZipInfo(name, date_time=(2026, 9, 23, 0, 0, 0))
info.compress_type = zipfile.ZIP_DEFLATED
info.external_attr = 0o100644 << 16
archive.writestr(info, data)
write_json(out / "manifest.json", manifest)
names = ("prepare.py", "verify.py", "README.md", "manifest.json", "evidence.zip")
write_json(out / "files.json", {name: pin((out / name).read_bytes()) for name in names})
need(verify_package(out) == summary, "written package differs from validated captures")
print("PASS package readback:", out, summary)
if __name__ == "__main__":
main()
@@ -1,194 +0,0 @@
"""Offline consistency checks only; never execute or extract archived inputs."""
from pathlib import Path, PurePosixPath
from datetime import datetime
import argparse
import hashlib
import io
import json
import math
import re
import stat
import zipfile
RAW_FILES = {"started.json", "process.json", "result.json", "stdout.log", "stderr.log", "inputs.json", "inputs.zip"}
OUTER_FILES = {"prepare.py", "verify.py", "README.md", "manifest.json", "evidence.zip"}
def need(condition, message):
if not condition:
raise ValueError(message)
def pin(data):
return {"bytes": len(data), "sha256": hashlib.sha256(data).hexdigest()}
def valid_pin(value):
return (isinstance(value, dict) and set(value) == {"bytes", "sha256"}
and type(value["bytes"]) is int and value["bytes"] >= 0
and isinstance(value["sha256"], str)
and re.fullmatch(r"[0-9a-f]{64}", value["sha256"]) is not None)
def safe(name):
return (isinstance(name, str) and bool(name) and "\\" not in name and ":" not in name
and all(ord(char) >= 32 for char in name)
and not PurePosixPath(name).is_absolute()
and all(part not in ("", ".", "..") for part in name.split("/")))
def run_name(name):
return isinstance(name, str) and re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]*", name) is not None
def parsed(data):
def pairs(items):
result = {}
for key, value in items:
need(key not in result, "duplicate JSON key: " + key)
result[key] = value
return result
def constant(value):
raise ValueError("nonfinite JSON constant: " + value)
return json.loads(data, object_pairs_hook=pairs, parse_constant=constant)
def inventory(archive, expected):
names = archive.namelist()
need(len(names) == len(set(names)) and set(names) == set(expected), "exact ZIP membership")
for entry in archive.infolist():
need(safe(entry.filename) and not entry.is_dir(), "unsafe ZIP member: " + entry.filename)
need(not stat.S_ISLNK(entry.external_attr >> 16), "ZIP symlink: " + entry.filename)
need(valid_pin(expected[entry.filename]), "invalid member pin: " + entry.filename)
need(entry.file_size == expected[entry.filename]["bytes"], "ZIP recorded size: " + entry.filename)
def ordered_times(*values):
times = [datetime.fromisoformat(value) for value in values]
return all(value.utcoffset() is not None for value in times) and times == sorted(times)
def verify_data(manifest, read):
"""Shared read-only verifier for packaged bytes or prepare's in-memory payload."""
need(manifest["schema"] == 1 and manifest["kind"] == "scenario-selection-cargo-gates", "manifest schema")
runs, logical, stored = manifest["runs"], manifest["logicalFiles"], manifest["members"]
helpers = manifest["captureHelpers"]
need(isinstance(helpers, dict) and set(helpers) == {"gate.py", "source-proof.py"}, "capture helper names")
need(all(isinstance(name, str) and re.fullmatch(r"helpers/[0-9a-f]{64}\.py", name)
for name in helpers.values()), "capture helper mapping")
need(isinstance(runs, dict) and bool(runs) and all(run_name(run) for run in runs), "explicit run inventory")
need(all(safe(name) for name in logical) and all(safe(name) for name in stored), "unsafe manifest path")
need(set(logical.values()) == set(stored), "unreferenced or absent stored members")
for name, expected in stored.items():
need(valid_pin(expected) and pin(read(name)) == expected, "stored member hash: " + name)
expected_logical = {"cargo/" + run + "/" + name for run in runs for name in RAW_FILES}
expected_logical.update(spec["helper"] for spec in runs.values())
expected_logical.update(helpers.values())
for run, spec in runs.items():
if "sourceProof" in spec:
proof_spec = spec["sourceProof"]
need(set(proof_spec) == {"commit", "pin", "comparator"}, run + ": exact source proof spec")
need(isinstance(proof_spec["commit"], str) and re.fullmatch(r"[0-9a-f]{40}", proof_spec["commit"]), run + ": named full source commit")
need(valid_pin(proof_spec["pin"]), run + ": source proof pin")
need(isinstance(proof_spec["comparator"], str) and re.fullmatch(r"comparators/[0-9a-f]{64}\.py", proof_spec["comparator"]), run + ": comparator path")
expected_logical.update({"cargo/" + run + "/source-proof.json", proof_spec["comparator"]})
need(set(logical) == expected_logical, "exact logical raw/helper inventory")
for name, destination in logical.items():
if name.startswith("cargo/") and name.rsplit("/", 1)[1] in {"inputs.json", "inputs.zip"}:
expected = "capsules/" + stored[destination]["sha256"] + PurePosixPath(name).suffix
need(destination == expected, "input capsule mapping: " + name)
else:
need(destination == name, "only input maps/archives may be deduplicated: " + name)
def raw(name):
return read(logical[name])
for name, helper in helpers.items():
need(helper == "helpers/" + pin(raw(helper))["sha256"] + ".py", "named helper hash: " + name)
seen_capsules = set()
for run, spec in runs.items():
prefix = "cargo/" + run + "/"
start = parsed(raw(prefix + "started.json"))
process = parsed(raw(prefix + "process.json"))
result = parsed(raw(prefix + "result.json"))
required_start = {"start", "command", "cwd", "head", "scope", "inputs", "archive",
"inputCount", "environment", "helper", "cargo", "rustc"}
need(required_start <= start.keys(), run + ": incomplete start receipt")
need(all(result.get(key) == value for key, value in start.items()), run + ": start/result relationship")
command = result["command"]
need(isinstance(command, list) and command and command[0] == "cargo"
and all(isinstance(arg, str) for arg in command), run + ": Cargo command")
need(command == process["command"] == spec["command"], run + ": command relationship")
need(result["head"] == spec["head"] and re.fullmatch(r"(?:[0-9a-f]{40}|[0-9a-f]{64})", result["head"]), run + ": HEAD")
need(type(result["exitCode"]) is int and result["exitCode"] == spec["exitCode"], run + ": recorded exit")
need(result["inputsUnchanged"] is True, run + ": changed inputs refused")
need(type(result["seconds"]) in (int, float) and math.isfinite(result["seconds"])
and result["seconds"] >= 0, run + ": duration")
need(type(process["pid"]) is int and process["pid"] > 0, run + ": process identity")
need(ordered_times(start["start"], process["observed"], result["finish"]), run + ": completed timestamp order")
for field, filename in [("inputs", "inputs.json"), ("archive", "inputs.zip"), ("stdout", "stdout.log"), ("stderr", "stderr.log")]:
need(result[field] == pin(raw(prefix + filename)), run + ": " + field + " pin")
need(valid_pin(result["helper"]) and result["helper"] == pin(raw(spec["helper"])), run + ": helper pin")
need(spec["helper"] == "helpers/" + result["helper"]["sha256"] + ".py", run + ": helper mapping")
inputs = parsed(raw(prefix + "inputs.json"))
need(isinstance(inputs, dict) and all(safe(name) for name in inputs), run + ": safe input inventory")
need(type(result["inputCount"]) is int and len(inputs) == result["inputCount"] == spec["inputCount"], run + ": input counts")
if "sourceProof" in spec:
proof_spec = spec["sourceProof"]
proof_bytes = raw(prefix + "source-proof.json")
proof = parsed(proof_bytes)
need(pin(proof_bytes) == proof_spec["pin"], run + ": source proof pinned bytes")
need(proof["run"] == run and proof["commit"] == proof_spec["commit"], run + ": proof run/commit identity")
need(proof["status"] == "PASS exact captured input inventory and Git blob bytes", run + ": source proof status")
need(proof["inputCount"] == len(inputs) and proof["inputMap"] == result["inputs"], run + ": proof input relationship")
need(proof["result"] == pin(raw(prefix + "result.json")), run + ": proof result relationship")
need(ordered_times(result["finish"], proof["verified"]), run + ": proof time after closed gate")
comparator = proof_spec["comparator"]
need(valid_pin(proof["comparator"]) and pin(raw(comparator)) == proof["comparator"], run + ": comparator bytes")
need(comparator == "comparators/" + proof["comparator"]["sha256"] + ".py", run + ": comparator identity")
need(sum(name.startswith("crates/vendor/iced_widget/") for name in inputs) == 71, run + ": vendor count")
capsule = (logical[prefix + "inputs.json"], logical[prefix + "inputs.zip"])
if capsule not in seen_capsules:
with zipfile.ZipFile(io.BytesIO(raw(prefix + "inputs.zip"))) as source:
inventory(source, inputs)
for name, expected in inputs.items():
need(pin(source.read(name)) == expected, run + ": source hash " + name)
seen_capsules.add(capsule)
artifacts = []
for line in raw(prefix + "stdout.log").decode("utf-8", errors="replace").splitlines():
try:
value = json.loads(line)
except json.JSONDecodeError:
continue
if isinstance(value, dict) and value.get("reason") == "compiler-artifact" and value.get("executable"):
artifacts.append(value)
need(isinstance(result["artifacts"], list), run + ": artifact records")
need(artifacts == [record["cargo"] for record in result["artifacts"]], run + ": Cargo JSON artifact relationship")
for record in result["artifacts"]:
need(set(record) == {"cargo", "readback"}
and (record["readback"] is None or valid_pin(record["readback"])), run + ": artifact readback pin")
print(run, "recorded exit", result["exitCode"], "inputs", len(inputs), "executable records", len(artifacts))
return {"runs": len(runs), "recordedFailures": sum(spec["exitCode"] != 0 for spec in runs.values()),
"logicalFiles": len(logical), "storedMembers": len(stored), "sourceCapsulePairs": len(seen_capsules),
"captureHelpers": helpers, "sourceProofs": sum("sourceProof" in spec for spec in runs.values())}
def verify_package(root):
files = parsed((root / "files.json").read_bytes())
need(set(files) == OUTER_FILES, "outer file inventory")
for name, expected in files.items():
need(valid_pin(expected) and pin((root / name).read_bytes()) == expected, "outer hash: " + name)
manifest = parsed((root / "manifest.json").read_bytes())
with zipfile.ZipFile(root / "evidence.zip") as archive:
inventory(archive, manifest["members"])
return verify_data(manifest, archive.read)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("package", nargs="?", type=Path, default=Path(__file__).resolve().parent)
args = parser.parse_args()
print("PASS offline consistency:", verify_package(args.package))
print("Historical receipts only; no source/binary execution or native acceptance claim.")
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
@@ -1,264 +0,0 @@
"""Offline archive/source/receipt checks. Never executes archived helpers or native binaries."""
from pathlib import Path, PurePosixPath, PureWindowsPath
import argparse
import datetime
import hashlib
import json
import re
import zipfile
KIND = 'scenario-semantic-reorder-native-evidence'
PHASES = {'baseline': 'baseline.arcadia', **{phase: f'expected-{phase}.arcadia' for phase in ('before-c', 'before-b', 'before-last')}}
def pin(data):
return {'bytes': len(data), 'sha256': hashlib.sha256(data).hexdigest()}
def safe_name(name):
p = PurePosixPath(name)
assert name and not p.is_absolute() and str(p) == name, 'Unsafe member: ' + name
assert '\\' not in name and ':' not in name and not any(x in ('.', '..', '') for x in name.split('/')), 'Unsafe member: ' + name
def check_oracles(files, fixture, prefix='fixtures/'):
assert set(fixture['files']) == set(PHASES.values()), 'Wrong fixture inventory'
baseline = files[prefix + 'baseline.arcadia']
aliases = ['a', 'b', 'c', 'd'] + [f'report_{i:02d}' for i in range(5, 25)]
orders = {
'baseline': aliases,
'before-c': ['b', 'a', 'd', 'c'] + aliases[4:],
'before-b': ['a', 'd', 'b', 'c'] + aliases[4:],
'before-last': ['b', 'c'] + aliases[4:-1] + ['a', 'd', 'report_24'],
}
clause = lambda ids: ('steps [' + ', '.join('@' + alias for alias in ids) + '];').encode()
revision = fixture.get('fixture_revision', 'oracle-v1-canonical-spacing')
assert revision in ('oracle-v1-canonical-spacing', 'oracle-v2-trivia-chunks'), 'Unknown fixture revision'
if revision == 'oracle-v2-trivia-chunks':
# Baseline A owns no prefix; each other item owns one leading space.
# These exact chunks move with identity in source_project.rs:1581-1632.
result_clause = lambda ids: ('steps [' + ','.join(('' if alias == 'a' else ' ') + '@' + alias for alias in ids) + '];').encode()
else:
result_clause = clause
assert baseline.count(clause(aliases)) == 1 and len(re.findall(rb'\bsteps\s*\[', baseline)) == 1
assert baseline.count(b'oa_occurrence ') == baseline.count(b'"Repeated report awaiting engineering review"') == 24
assert fixture['orders'] == orders, 'Wrong declared semantic oracles'
for phase, name in PHASES.items():
data = files[prefix + name]
assert pin(data) == fixture['files'][name], 'Fixture pin mismatch: ' + name
assert data == baseline.replace(clause(aliases), result_clause(orders[phase])), 'Oracle changes more than literal expected steps: ' + name
def check_source(files, actual, phase, prefix='fixtures/'):
assert phase in PHASES, 'Unknown source phase'
assert actual == files[prefix + PHASES[phase]], 'Exact full source differs from ' + phase
def verify(root):
manifest = json.loads((root / 'manifest.json').read_text(encoding='utf-8'))
assert manifest['kind'] == KIND, 'Wrong package kind'
assert pin((root / 'native-evidence.zip').read_bytes()) == manifest['archive'], 'Archive pin mismatch'
with zipfile.ZipFile(root / 'native-evidence.zip') as archive:
names = archive.namelist()
assert len(names) == len(set(names)) and set(names) == set(manifest['members']), 'Archive member inventory mismatch'
for name in names:
safe_name(name)
assert ((archive.getinfo(name).external_attr >> 16) & 0o170000) != 0o120000, 'Symlink member'
files = {name: archive.read(name) for name in names}
for name, data in files.items():
assert pin(data) == manifest['members'][name], 'Member pin mismatch: ' + name
assert PurePosixPath(name).suffix.lower() not in ('.exe', '.dll'), 'Unexpected archived binary'
for name in manifest['excluded_binary_bytes']:
safe_name(name)
assert name not in files and PurePosixPath(name).suffix.lower() in ('.exe', '.dll'), 'Invalid omitted binary entry'
assert (root / 'verify.py').read_bytes() == files['helpers/native-evidence-verify.py'], 'External verifier differs from pinned helper'
def js(name):
return json.loads(files[name].decode('utf-8-sig'))
def present_pin(name):
return pin(files[name]) if name in files else manifest['excluded_binary_bytes'].get(name)
def build_binding(prefix, source):
receipt = js(prefix + 'result.json')
proof = js(prefix + 'source-proof.json')
inputs = js(prefix + 'inputs.json')
assert receipt['exitCode'] == 0 and receipt['inputsUnchanged'] is True, 'Build not successful/unchanged'
assert proof['status'].startswith('PASS') and proof['result'] == pin(files[prefix + 'result.json']), 'Build proof result binding'
assert proof['inputMap'] == receipt['inputs'] == pin(files[prefix + 'inputs.json']), 'Build input-map binding'
assert proof['inputCount'] == receipt['inputCount'] == len(inputs), 'Build input count'
assert proof['commit'] == receipt['head'] == source and re.fullmatch('[0-9a-f]{40}', source), 'Build source identity'
return receipt
fixture = js('fixtures/fixture-manifest.json')
check_oracles(files, fixture)
for name in files:
if name.startswith('oracle-revisions/') and name.endswith('/fixture-manifest.json'):
check_oracles(files, js(name), name.removesuffix('fixture-manifest.json'))
config = js('review-config.json')
assert config['runs'] == manifest['runs'] and config['preflights'] == manifest['preflights']
for entries, prefix, pattern in [(config['runs'], 'runs', r'run-[0-9]+'), (config['preflights'], 'preflight', r'fixture-preflight-[0-9]+')]:
assert entries and len({x['name'] for x in entries}) == len(entries)
assert all(re.fullmatch(pattern, x['name']) and type(x['accepted']) is bool for x in entries)
assert {name.split('/')[1] for name in files if name.startswith(prefix + '/')} == {x['name'] for x in entries}, 'Undeclared archived attempts'
reports, preflights = [], []
accepted_cli_bindings = set()
for entry in config['preflights']:
prefix = 'preflight/' + entry['name'] + '/'
problems = []
try:
current = js(prefix + 'result.json')
started = js(prefix + 'started.json')
fixture_name = prefix + 'evidence/fixture-manifest.json'
current_fixture = js(fixture_name)
check_oracles(files, current_fixture, prefix + 'fixtures/')
revision = current_fixture.get('fixture_revision', 'oracle-v1-canonical-spacing')
if 'fixture_revision' in current:
assert current['fixture_revision'] == revision and current['fixture_manifest'] == pin(files[fixture_name]), 'Preflight fixture revision binding'
assert current['fixture_comparator'] == pin(files[prefix + 'evidence/verify-captured.py']), 'Preflight comparator pin'
assert started['checks'] == [] and all(current[key] == started[key] for key in started if key != 'checks'), 'Preflight started/result changed identity'
assert current['status'].startswith('PASS') and current['cliUnchanged'] is True, 'Preflight not passed'
receipt = build_binding(prefix + 'evidence/', current['source'])
assert current['receipt'] == pin(files[prefix + 'evidence/result.json']) and current['proof'] == pin(files[prefix + 'evidence/source-proof.json']), 'Preflight proof pins'
cli = [a for a in receipt['artifacts'] if a['cargo']['target']['name'] == 'arcadia' and not a['cargo']['profile']['test']]
assert len(cli) == 1 and cli[0]['readback'] == current['cli'] == present_pin(prefix + 'bin/arcadia.exe'), 'CLI artifact binding'
assert len(current['checks']) == len(PHASES) and {x['fixture'] for x in current['checks']} == set(PHASES.values()), 'Incomplete fixture preflight'
for check in current['checks']:
name = check['fixture']
assert check['exitCode'] == 0 and check['json_report'] is True and check['fixtureUnchanged'] is True, 'Fixture check failed: ' + name
assert check['stdout'] == pin(files[prefix + name + '.stdout.json']) and check['stderr'] == pin(files[prefix + name + '.stderr.log']), 'CLI output pins'
assert pin(files[prefix + 'fixtures/' + name]) == current_fixture['files'][name], 'Preflight fixture differs'
raw = js(prefix + name + '.stdout.json')
assert raw['operation'] == 'check' and raw['exit_code'] == 0 and raw['written'] is False and not any(x['severity'] == 'error' for x in raw['diagnostics']), 'Raw CLI report rejects fixture'
for runtime in current['runtime']:
assert present_pin(prefix + 'bin/' + PureWindowsPath(runtime['source']).name) == {k: runtime[k] for k in ('bytes', 'sha256')}, 'CLI runtime pin'
except (AssertionError, KeyError, ValueError, TypeError) as error:
problems.append(str(error))
preflights.append({'preflight': entry['name'], 'reviewer_accepted': entry['accepted'], 'checks_pass': not problems, 'problems': problems})
if entry['accepted']:
assert not problems, json.dumps(preflights[-1])
accepted_cli_bindings.add((current['source'], pin(files[fixture_name])['sha256']))
for entry in config['runs']:
prefix = 'runs/' + entry['name'] + '/'
problems, captures, sources = [], 0, []
# Report the actual discovered count even when an earlier historical
# source failure prevents the remaining per-run checks from passing.
for name in files:
if name.startswith(prefix) and name.endswith('.json'):
try:
record = js(name)
captures += int(isinstance(record, dict) and 'images' in record)
except (ValueError, UnicodeError):
pass
try:
prepared, started, finished = (js(prefix + name + '.json') for name in ('prepared', 'started', 'finished'))
fixture_name = prefix + 'evidence/fixture-manifest.json'
run_fixture = js(fixture_name)
check_oracles(files, run_fixture, prefix + 'evidence/')
revision = run_fixture.get('fixture_revision', 'oracle-v1-canonical-spacing')
if 'fixture_revision' in prepared:
assert prepared['fixture_revision'] == revision and prepared['fixture_manifest'] == pin(files[fixture_name]), 'Run fixture revision binding'
assert started['prepared'] == pin(files[prefix + 'prepared.json']), 'Started/prepared binding'
for rel, expected in prepared['immutable'].items():
safe_name(rel)
assert present_pin(prefix + rel) == expected, 'Immutable pin: ' + rel
assert prepared['helper'] == pin(files[prefix + 'evidence/native-harness.py']), 'Native helper binding'
for helper in ('capture.js', 'verify-captured.py'):
rel = 'evidence/' + helper
assert prepared['immutable'].get(rel) == pin(files[prefix + rel]), 'Captured observation/source helper is not immutable: ' + helper
for name in [*run_fixture['files'], 'fixture-manifest.json']:
assert prepared['immutable'].get('evidence/' + name) == pin(files[prefix + 'evidence/' + name]), 'Run oracle is not immutable: ' + name
receipt = build_binding(prefix + 'evidence/', prepared['source'])
gui = [a for a in receipt['artifacts'] if a['cargo']['target']['name'] == 'arcadia-studio' and not a['cargo']['profile']['test']]
assert len(gui) == 1 and gui[0]['readback'] == prepared['artifact'] == present_pin(prefix + 'bin/arcadia-studio.exe'), 'GUI artifact binding'
for runtime in prepared['runtime_sources']:
assert present_pin(prefix + 'bin/' + PureWindowsPath(runtime['path']).name) == {k: runtime[k] for k in ('bytes', 'sha256')}, 'GUI runtime pin'
assert started['command'] == prepared['command'] and started['cwd'] == prepared['cwd'], 'Launch command/cwd changed'
identity = started['identity']
assert identity['pid'] == started['pid'] and identity['created_filetime'] > 0 and identity['running'] is True, 'Missing initial live process identity'
assert PureWindowsPath(identity['image']) == PureWindowsPath(prepared['command'][0]), 'Wrong process image'
assert finished['exitCode'] == 0 and finished['immutableUnchanged'] is True, 'Normal exit/immutability not established'
for stream in ('stdout', 'stderr'):
assert finished[stream] == pin(files[prefix + 'app.' + stream + '.log']), 'Final log binding'
assert finished['working'] == pin(files[prefix + 'working.arcadia']), 'Final source binding'
assert prepared.get('initial_pin', started['workingBefore']) == started['workingBefore'], 'Initial source pin changed'
origin = entry.get('reopen_from')
if prefix + 'reopen-origin.json' in files:
old = js(prefix + 'reopen-origin.json')
declared = {'run': old['from_run'], 'file': old['from_file']}
assert origin is None or origin == declared, 'Contradictory reopen origins'
origin = declared
assert old['from_pin'] == old['copied_pin'] == started['workingBefore'], 'Reopen-origin pins disagree'
if origin:
assert origin['run'] != entry['name'] and origin['run'] in {x['name'] for x in config['runs']}, 'Unknown/self reopen origin'
safe_name(origin['file'])
assert pin(files['runs/' + origin['run'] + '/' + origin['file']]) == started['workingBefore'], 'Reopen source is not the exact captured copy'
else:
assert started['workingBefore'] == run_fixture['files']['baseline.arcadia'], 'Nonbaseline start needs explicit reopen_from'
phases = entry.get('source_phases', {})
actual_sources = [name for name in files if name.startswith(prefix) and name.endswith('.arcadia') and '/evidence/' not in name and name != prefix + 'baseline.arcadia']
assert set(phases) <= {name[len(prefix):] for name in actual_sources}, 'Phase config references missing source'
for name in actual_sources:
rel = name[len(prefix):]
sidecar = name[:-8] + '.json'
phase = phases.get(rel)
if sidecar in files and 'phase' in js(sidecar):
saved = js(sidecar)
assert phase is None or phase == saved['phase'], 'Contradictory snapshot phase'
phase = saved['phase']
assert saved['pin'] == pin(files[name]) and saved['exitCode'] == 0, 'Snapshot check/pin failed'
evaluated = js(name[:-8] + '.evaluator.stdout.json')
assert evaluated['status'] == 'PASS' and evaluated['phase'] == phase and evaluated['sha256'] == pin(files[name])['sha256'], 'Raw source evaluator report mismatch'
assert not files[name[:-8] + '.evaluator.stderr.log'], 'Source evaluator stderr is nonempty'
check_source(files, files[name], phase, prefix + 'evidence/')
sources.append({'file': rel, 'phase': phase})
start_time = datetime.datetime.fromisoformat(started['started'])
finish_time = datetime.datetime.fromisoformat(finished['finished'])
assert start_time <= finish_time, 'Reversed process lifetime'
for name in files:
if not name.startswith(prefix) or not name.endswith('.json'):
continue
record = js(name)
if not isinstance(record, dict) or 'images' not in record:
continue
assert record['images'], 'Empty capture record'
assert record['window']['app'].casefold() == ('process:' + identity['image']).casefold(), 'Capture uses a different executable surface'
observed = datetime.datetime.fromisoformat(record['time'])
assert start_time <= observed <= finish_time, 'Capture is outside process lifetime'
for image in record['images']:
safe_name(image['file'])
assert prefix + image['file'] in files and image['width'] > 0 and image['height'] > 0, 'Missing/invalid capture image'
assert captures > 0, 'No captured native observations'
if entry['accepted']:
assert (prepared['source'], pin(files[fixture_name])['sha256']) in accepted_cli_bindings, 'No accepted current-source/current-oracle CLI fixture preflight'
except (AssertionError, KeyError, ValueError, TypeError) as error:
problems.append(str(error))
reports.append({'run': entry['name'], 'reviewer_accepted': entry['accepted'], 'checks_pass': not problems, 'problems': problems, 'capture_records': captures, 'source_checks': sources})
if entry['accepted']:
assert not problems, json.dumps(reports[-1], indent=2)
return {
'status': 'PASS offline archive, exact source and receipt consistency checks',
'runs': reports,
'preflights': preflights,
'accepted_run_count': sum(entry['accepted'] for entry in config['runs']),
'limitations': [
'Human visual, keyboard, selection and interaction claims are not inferred from screenshots.',
'Omitted executable/DLL bytes cannot be rehashed offline.',
'Build proof/input map consistency checked; Git blobs are not independently available offline.',
'accepted=false failures are retained and reported, never silently promoted.',
'No native history-depth introspection, usability acceptance, release or publication assertion.',
],
}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('directory', type=Path)
args = parser.parse_args()
print(json.dumps(verify(args.directory.resolve()), indent=2))
if __name__ == '__main__':
main()
@@ -1,139 +0,0 @@
# Scenario reorder App and UX policy review
Bounded independent critique on the parent's identified `a16e0140` base in `C:/ar-scenario-9e8r6t`. No production edits, Cargo, native actions or Gitea. Initial policy review precedes the writers' source handoff. A working exact destination chooser is a candidate interaction, not full acceptance of the user's graphical editing and low-cognitive-load requirements.
## Initial concrete challenges
1. **Bind Apply to the proposal actually displayed.** Context, selection Snapshot and operation token remain identical while a destination changes. A queued Apply for Ready destination A must not commit subsequently chosen Fallback destination B under the same token. Capture exact Slot or proposal generation in Finish, check it before taking/retiring the session, and bind warning acknowledgement to that same proposal. An explicit Apply button can serve as acknowledgement without adding a modal, provided its packet approves the displayed exact destination/status. Verify A-Apply/B-Choose/replayed-A and destination ABA. The existing context/session freshness checks alone do not establish this invariant.
2. **Resolve existing fallback copy.** `behavior/scenario/workspace.rs` currently says canvas insertion and reordering are unavailable until saved positions are repaired. Baseline Fallback semantic reorder would make that blanket statement misleading. Distinguish coordinate input from explicit sequence reorder. The proposal warning must explain that the requested saved view stays selected and authored coordinates survive, while the actual drawing uses automatic geometry. Do not imply that committing repairs those coordinates or leaves the visible drawing unchanged.
3. **Retain the exact set across candidate geometry and commit reconciliation.** Baseline actual-scene visibility is necessary, but final selection is also constrained by actual candidate presentation. If a successful candidate can make an original member latent/unrendered, silently pruning it after commit conflicts with the promised full-set/primary preservation. Either establish that this cannot occur for admitted reorder targets in supported Ready/Fallback candidates, or reject/explain that candidate before effects. This is an invariant to verify, not a demonstrated present defect.
## Proportionate policy assessment
Allowing a semantically valid whole set from a visible Fallback baseline is coherent for exact semantic input: the chooser does not infer destinations from unusable saved coordinates. Candidate Fallback can be an explicit supported outcome when preview and commit show the same requested view and clear automatic-geometry consequences, with model/source preservation and Undo. Candidate Unavailable refusal is a defensible bounded initial policy because no usable resulting diagram can be previewed. The App must enforce these rules independently of button disabling; session Completion is not commit permission.
No destination should exist initially. Search/paging must leave the chosen exact Slot clearly identified and never substitute a row index after filtering. A destination selected on another page must remain named next to Apply; selected anchors remain explicit no-change choices, and temporal failures remain discoverable choices with reasons. Before/after order should emphasize the changed block and its neighbors without expanding into long unstructured lists. Repeated long names need visible position/alias disambiguation at ordinary and compact sizes. These requirements still need actual native evidence; tests cannot accept the chooser by themselves.
## Adapter source handoff review
Reviewed the initial frozen `app/scenario_reorder.rs` and its six tests. Exact `Finish(Slot)` now checks the current preview before taking the session or advancing any lifetime, addressing the queued-Apply/different-destination problem. Same-Slot return within the frozen immutable Project is the same semantic proposal/status; a distinct proposal generation is not required merely to distinguish identical results. The source checks the complete current Context/Snapshot and session match before query/page/status changes, consumes current terminal attempts once, and executes through Document only after policy checks.
The six tests use actual App messages and independently authored complete Project/source/history outcomes. They check changed commit across Saved/Derived, one-step exact Undo/Redo, stale packets against a newer session, different-slot queued Apply, no-op preservation of Redo, generation-only primary ABA, busy cancellation, temporal refusal retirement and a source-size-limit failure intended to exercise real source-write atomicity. The source-limit fixture's formatting assumption requires the pending runtime gate; this review does not presume it passes.
Additional concrete integration/UX findings sent to the parent while wiring was still being written:
- Early Scenario spacing dispatch returns before ordinary reorder reconciliation. Without an explicit owner rule in that path, a fresh body drag can start spacing while an old chooser remains active. The eventual fix must leave stale packets inert and avoid advancing an epoch after capturing the new spacing session, which would invalidate it immediately. Current selection changes already cancel reorder explicitly.
- Clearing the proposal after a successful commit and showing only generic “Semantic order updated” is insufficient observable success when preserved saved geometry remains visually unchanged. Retain a bounded result summary naming exact sequence/destination/result neighborhood, with Undo. Bind the summary to document/revision/view so later navigation or Undo does not present old order as current.
- The six initial adapter tests do not constrain the new allowed-Fallback or blocked-Unavailable/hidden-selection policy branches. Add actual Fallback admission/commit coverage and direct guard coverage before claiming that policy verified; synthetic Unavailable/hidden candidate injection may test guard behavior if explicitly distinguished from a real geometry transition.
The parent is addressing final wiring/UI and additional coverage. No final App/UX acceptance or passing runtime result is claimed at this handoff stage.
## First UI source findings (before compile checkpoint)
**Material input interference:** the chooser overlay is currently a child of `canvas_workspace::Viewport`. That wrapper handles wheel and Space/Tab/arrows/Escape before forwarding events to children (`canvas_workspace/viewport.rs:258-350`). Clicking the search field gives the viewport focus; a subsequent space can publish `CancelGesture`, arrows can pan, wheel can move the canvas instead of scrolling the chooser, and captured Escape can publish camera Leave instead of the advertised chooser cancel. `opaque(card)` only handles child routing and cannot prevent this parent pre-handler. Move the chooser outside the Viewport/sensor or implement explicit child-first ownership, preserving the raw canvas widget tree. Verify mounted typing with spaces/arrows, chooser scroll and Escape; App message tests do not cover this widget-routing defect.
Additional UI findings sent to the parent:
- `reorder_view::panel` searches the displayed title after `short(name, 38)`. A distinctive full-name suffix is absent from that title, so searching by that suffix falsely reports no destination. Search full model names/aliases/position independently of display truncation.
- The selected destination and Fallback warning are in the scrolling body while Apply is fixed below it. With a long order comparison they can be outside the viewport when Apply is enabled. Keep the exact chosen destination and relevant policy warning adjacent to Apply in a fixed footer, so explicit Apply approves a visible consequence.
- `workspace` computes chooser-aware `coordinate_input` but assigns `scene.coordinate_input` directly from capabilities. A singleton Derived body/handle can still begin a legacy coordinate reorder under the chooser. Use the chooser-aware permission for the actual scene; deliberate selection changes may remain available to cancel the operation.
- The new context-bound summary improves persistence but initially names only count/destination/sequence. Include a bounded resulting semantic neighborhood/block in semantic order (for example B -> [A,C] -> D); count-only feedback does not resolve the select-C-then-A versus semantic-A/C question after the preview disappears.
- Context-menu reorder is currently always disabled because its Begin eligibility transitively rejects an open model menu. The header entry remains available. This may be an explicit bounded limitation, but do not claim contextual access or display a misleading dead action without explanation.
These findings are static observations of current code during concurrent implementation, not final findings against a frozen compiled source. They require reinspection after the parent's changes. No native workflow has been accepted.
## Reinspection of the revised adapter/UI
The parent addressed the concrete findings above. The chooser is now an outer sibling layer above the sensor/Viewport rather than its child, with underlying canvas keyboard and coordinate authoring disabled while the session owns input. The normal canvas tree remains stable. The early spacing handler is inert during reorder, and real selection changes explicitly cancel it. Full model names/aliases are searched independently of truncated display labels. Exact chosen destination and policy feedback now occupy a fixed footer beside Apply. The fallback notice distinguishes coordinate editing from explicit semantic reorder; the context menu honestly directs users to the header entry instead of presenting an unusable action.
The persistent result is tied to the current Scenario Context and cleared on stale context or a new operation. It now derives a compact neighborhood from actual committed sequence order, with positions, up to three selected names, an explicit omitted-member count and immediate neighbors. It therefore no longer repeats click order or merely reports a count. The original full selection remains available, and the UI points to Undo without adding another approval step.
The additional real baseline-Fallback test authors reversed saved call/reply coordinates, asserts actual Fallback, commits the semantic operation and retains the requested Saved identity, complete selected set and authored Project records. The Unavailable and hidden-selection tests deliberately inject preview outcomes to exercise App refusal/terminal atomicity. Their comments correctly avoid claiming natural geometry transitions. No Ready-to-Unavailable transition is inferred from synthetic guard coverage.
**No new blocking static issue found in this revised adapter/UI source.** The resolved wrapper defect still requires the planned mounted input regression and native exercise. Actual compact footer layout with long aliases, search/scroll focus, destination reachability, repeated names, intermediate feedback, and keyboard activation/discoverability remain empirical questions. The chooser is a concrete candidate implementation, not acceptance of the entire requested graphical authoring workflow. Pending compilation/test/native results are not assumed by this review.
## Frozen source and evaluator review at f3ed6e97
The parent froze `f3ed6e97dd3caf06046711cf74d268c7274bc780` for the next gate. Reinspected the final source and nine App test functions. Destination-title construction now enumerates positions once, preserving full-name/alias search without a repeated linear position lookup per row. Result neighborhoods use actual semantic positions and aliases, distinguishing repeated display names; final display sizing still needs native inspection.
The new mounted evaluator constructs the real `app.view()` through the installed Iced UserInterface and headless WGPU renderer, settles layout, finds the actual identified query widget and clicks/focuses it. It injects raw Space and requires an emitted `ScenarioReorder(Query(" "))`, then checks actual query state. It injects arrow and wheel events while requiring the camera and live session token remain unchanged. Finally it injects raw Escape and requires the root bridge's captured `ScenarioReorder(Cancel)` before checking exact model/source, selected targets/primary and no history. This meaningfully detects the earlier parent-Viewport pre-handler defect; it is not merely direct App message dispatch. The new root escape capture remains within the existing wrapper, preserving widget state and overlay precedence.
Evaluator limits: arrow checks establish noninterference, not precise caret motion; the wheel check establishes no camera/session disturbance, not successful scrolling through overflowing destinations. The short mounted fixture does not establish twenty-sibling navigation, compact footer clipping, keyboard traversal/activation, or native accessibility. Those remain explicit follow-up observations, not reasons to mislabel this mounted regression as comprehensive UX acceptance.
Read the preserved first gate: `reorder-app-test-1` at `61099ed4742eaa51c3bd81129bc072d8321961b7` closed exit 101 with all 817 inputs unchanged and no executable records. Its raw Cargo stream contains six compile errors: five E0659 ambiguous `column` uses and one E0603 private test-helper `occurrence` import. No tests executed in that failed attempt. It must remain a compilation failure, not be relabelled by subsequent source corrections. The second gate at f3ed6e97 was running at this review point.
**Scoped final source assessment: no unresolved material static defect found after the recorded fixes.** Runtime/native results are pending. This finding accepts the source for its bounded verification step, not the chooser's complete user experience or issue #29 completion.
## First corrected-source execution: filter limitation
Independently read `reorder-app-test-2` at f3ed6e97: exit 0, 817 unchanged inputs, **23 passed / 0 failed / 0 ignored / 1,581 filtered**. Raw stream/map/archive hashes and source-proof/result bindings agree. Exactly one new App test ran: `real_baseline_fallback_allows_explicit_semantic_reorder_and_retains_saved_view`. The `reorder_` name filter matches the earlier session/slot tests, but not the module name `scenario_reorder::` itself; the remaining eight new App tests were filtered out. This result therefore does not verify all nine App tests or the mounted evaluator. The parent detected that coverage gap and started the broader `scenario` filter at the same frozen source; its result is pending.
## Broader gate exposes fixture failures
Read the closed `reorder-app-scenario-1` output at f3ed6e97: **301 passed, 6 failed, 1 ignored, 1,296 filtered**. Its source proof records exact comparison of all 817 captured inputs with that source. The mounted query test, real Fallback case and synthetic presentation guards passed. Five failures panic in shared Derived setup at `scenario_context().unwrap()` before the intended operation; the fixture selected the owner without entering the Behavior page. The sixth expected a source-budget failure, but its observed status is successful semantic update: the current fixture did not actually force source growth/refusal. These failures are retained and must not be described as an all-green adapter gate. Correcting setup and constructing a real source-write refusal remain necessary; the current evidence does not yet establish all nine App tests pass.
## Review of the six failed-test corrections
Independently inspected the source writer's `SyntaxUpdate::list` (`oa-core/src/source_project.rs:1516-1655`). A same-parent permutation maps each existing reference chunk once, preserves its prefix/suffix trivia and uses the same comma count. It does not necessarily rewrite the compact list into longer canonical spacing. The previous budget-failure expectation was false for this operation.
The corrected test now requires successful permutation at the exact existing source-byte budget, an independently specified complete Project, retained target order/primary/camera, and exact original model/source after Undo; dirty Properties must still reject Begin. The five Derived setup failures are repaired by entering `Page::Behavior` before capturing a Scenario context. Neither correction changes production code or hides a demonstrated production defect. The attempted error-injection test is not silently repurposed as error-branch evidence: a real App `Document::execute` failure during this semantic permutation remains an explicit runtime coverage gap. Static inspection still shows the error branch changes only status after retiring the operation; model/source/history atomicity relies on Document's separate contract and tests. A new gate is required for the corrected evaluators.
## Keyboard workflow gap and proposed correction
The parent inspected installed Iced buttons and found ordinary chooser buttons do not provide Tab/KeyPressed activation. Existing tests establish search input and Escape, not a complete keyboard reorder workflow. This is a real remaining interaction gap, not a passing-test acceptance inference.
Recommended bounded path: reuse the established keyboard-aware Control for the header entry, focus the query after admitted Begin, let plain Enter preview only a unique exact filtered destination, and use an explicitly labelled **Ctrl+Enter** to Apply the displayed eligible proposal. Ctrl+Enter must capture its exact Slot, reject repeats, and be inert without an eligible proposal. Plain Enter must never commit. Rejected the proposed 'Enter again applies' phase switch: key repeat or queued Enter events can make a newly chosen warning-bearing proposal execute before its consequence has been displayed. A distinct Apply shortcut is easier to state and verify without adding another mode or modal. Actual F6/Tab entry, typing, Enter preview, Ctrl+Enter Apply and Escape still require mounted/native verification once implemented; no implementation result is assumed here.
## Corrected Scenario gate and subsequent keyboard source review
Independently audited closed `reorder-app-scenario-2` at `c9f6a3da40e288e9f993d9cacf516142c0017cf3`: **307 named passes, 0 failures, 1 ignored, 1,296 filtered**, including all nine new App tests. Exit 0, all 817 captured inputs unchanged, raw stdout/stderr/map/archive hashes and source-proof/result bindings agree. This establishes the corrected tests, including actual mounted query ownership, at that source. It does not include the subsequent keyboard workflow changes.
The next keyboard code uses a token-specific query widget ID and focus task, the existing keyboard-aware header Control, plain Enter for a unique exact destination preview, and Ctrl+Enter for captured exact-Slot Apply. Repeated Ctrl+Enter is ignored, and an invalid active Ctrl+Enter is captured rather than falling through to query submission. Those choices address the earlier phase-switch concern.
One additional source finding was sent before the keyboard gate: the new root Ctrl+Enter handler precedes current-overlay preflight. A picker opened within the same Iced batch can be widget-local before any App context changes, leaving the cached Apply action live behind that picker. Give current overlays precedence for Ctrl+Enter as already done for Escape, or consume it without Apply while an overlay exists. Verify picker-open plus Ctrl+Enter in the same batch; an App-modal predicate alone does not prove this case. Keyboard/native acceptance remains pending that revision and its tests.
## Additional keyboard lifetime finding
The parent raised a stale focus-task concern, confirmed independently against installed `iced_core-0.14.0/src/widget/operation/focusable.rs:29-49`: the generic target-focus operation unfocuses every nonmatching focusable even when its target ID is absent. Token-specific IDs prevent focusing a newer query, but do not prevent an old task from blurring it. The initial evaluator runs the old focus task before focusing the current query, so it does not detect this disturbance.
Required correction: a read-only target-existence/owner check before chaining any focus-mutating operation; no matching current target must mean no focus changes. Verify by focusing the newer query first, replaying the old task, and requiring that current focus and actual typing remain intact. This is a concrete lifetime defect in the initial generic-focus implementation, separate from proposal/model freshness. The root Ctrl+Enter overlay-preflight correction was re-read and is sound; its current overlay gets precedence and cannot fall through to cached Apply.
## Keyboard lifetime and overlay corrections rechecked
Re-read the final guarded `reorder_view::focus_query`: its first operation only discovers the exact token-specific focusable; only a present target chains the focus mutation. An absent old token therefore leaves the newer query's focus unchanged. The strengthened mounted evaluator first focuses the current query, then replays the older task, requires current focus to remain, and sends a raw `x` key that must publish and apply `Query("x")`. This directly addresses the previously missing ordering and observable typing behavior.
The root bridge now offers Escape to the current overlay first; if no overlay captures it, an active reorder emits the captured, context-bound Cancel and captures the event regardless of underlying child handling. This closes the ignored-child/global-subscription gap after a picker has closed. Ctrl+Enter still gives the current overlay precedence before the cached exact-Slot Apply. No additional material static defect found in this bounded revision. These changes and new evaluators await their own execution gate; the earlier c9f6a3da result remains evidence for its earlier source only. Native keyboard and full chooser usability remain unaccepted here.
## Executed keyboard gate independently audited
Closed `reorder-app-scenario-5` at `04485e001798430fb1b2e8d1347e6bde51e6e7a1` records exit 0 and **310 passed / 0 failed / 1 ignored / 1,296 filtered**. Independently counted all 310 named passes and all 12 App adapter tests, including the keyboard preview/Apply sequence, same-batch arrangement picker ownership, and old focus task replay after current focus with actual typing. Raw stdout/stderr, input map/archive and source-proof result/map hashes match. All 817 archive members match their inventory pins; independently read all 817 corresponding Git blobs at that exact commit and verified their sizes/SHA-256 against the captured inventory. The receipt records unchanged inputs.
This accepts bounded runtime evidence for the exact keyboard, envelope, selection, source/history and presentation cases those tests exercise. It does not establish native focus behavior, visual layout, long-list usability, or complete chooser UX acceptance. The real App source-write failure branch remains the earlier disclosed runtime fixture gap. Strict Clippy was still in progress at this review checkpoint.
The proposed replacement of local `short_label` with existing `crate::views::short(..., 24)` is appropriate for display-only owner labels: it avoids splitting grapheme clusters and removes duplicate truncation logic. Its intentional display differences are whitespace normalization and counting the ellipsis within the limit (rather than appending it after 24 Unicode scalars). Full identity and semantic destination remain independently captured; search uses full strings elsewhere. Any such later edit requires its own source attribution and suitable verification, and is not silently included in the 04485e00 test result above.
## Final helper-only source and repeated gate
Independently compared `04485e001798430fb1b2e8d1347e6bde51e6e7a1` with `14df924b98d74144c1e40fbe7098994ff5eb2e94`: the entire commit difference is the previously reviewed `short_label` body delegating to `crate::views::short(value, 24)` (one file, one insertion, six deletions). No session, input, policy or test changes accompany it.
At final source `14df924b98d74144c1e40fbe7098994ff5eb2e94`, independently audited `reorder-app-scenario-6`: **310 named passes / 0 failed / 1 ignored / 1,296 filtered**, including all 12 App adapter tests; and `reorder-app-format-1`: `cargo fmt --all -- --check`, exit 0. Both receipts record 817 unchanged inputs and exact source proofs. Verified their raw stream/map/archive/result pins and every archived member. The inventories differ from the previously independently Git-checked 04485e00 inventory only at the adapter file; that changed entry independently matches its exact 14df924b Git blob. Thus the final helper-only source has its own repeated runtime evidence rather than relying only on predecessor tests. No material issue found. Final strict Clippy/build and native visual/interaction acceptance remain pending at this checkpoint.
## Native preview clipping finding (14df924b, run 1)
Inspected the actual 1605×872 capture `.local/scenario-reorder-app-native/run-1/24-preview-c-1.jpg` (the supplied PNG spelling was resolved to the retained JPG). The chooser's After text is occluded at the right edge, and the input and destination button crowd the same scrollbar. The intended after-order B, A, D, C can still be recovered from the wrapped aliases, but this is a real preview readability defect; passing mounted input tests did not detect it. The fixed destination/warning/Apply footer remains visible in this capture.
Concrete cause: `reorder_view.rs` uses `scrollable(body).height(Fill)` around two Fill comparison columns. Installed `iced_widget-0.14.2/src/scrollable.rs` defaults to a floating 10-pixel scrollbar over content. Its documented `.spacing(...)` method embeds the scrollbar and reserves its width plus spacing in layout. Recommend embedding it with a modest gap (for example `.width(Fill).spacing(8)`), keeping the body/comparison/text constrained to available width and retaining wrapping for long names/aliases. Horizontal scrolling would add unnecessary friction here. Recheck the same repeated-name preview and both columns at native size after the narrow fix; explicit Fill constraints alone do not establish that the scrollbar no longer covers text. No production file was edited by this reviewer. The parent continues this old-source trial for functional evidence, not final layout acceptance.
## Native result-summary lifetime fix (implementation handoff)
The parent observed the post-Apply summary disappear when Saved spacing diagnostics changed viewport height. Source tracing confirms the accepted `CanvasEvent::Viewport` handler increments `canvas_epoch`; the old summary reconciler compared the whole Context and incorrectly treated that passive resize as loss of the completed result.
At the parent's explicit request this reviewer implemented the bounded correction in `app/scenario_reorder.rs` plus new `app/scenario_reorder/summary_tests.rs` (no other production/test files). A completed summary now ignores only input epoch, while preserving document-instance, Scenario owner, representation, visit and document revision matching. It additionally captures the post-commit full selection Snapshot and requires the same generation, targets and primary; selection ABA cannot masquerade as a camera-only change. New Begin/cancel clears both summary and capture. Live operation/event admission remains exact and unchanged.
Three new regressions drive a real 40-pixel viewport reduction followed by Camera Pan and exact-source Undo; real Saved-to-Derived navigation, selection collapse and new Begin/Cancel; and primary-change/restore ABA. Only the owned files were formatted with standalone rustfmt and `skip_children=true`. No Cargo, native or Git operation was run for this implementation; execution and native confirmation await the parent. This paragraph records an implementation handoff, not an independent review of this reviewer's own fix.
## Summary test compile correction and spacing terminal integration
The parent reported the first correction gate at `20109779` closed with E0425: this reviewer's new fixture referred to `oa_core::SourceBundle`, which is not a root export. Corrected the return type to `oa_core::source_project::SourceBundle`; this is a test compile repair, not a passing execution claim.
A separate early-return integration gap was identified: admitted spacing Finish bypasses App's common reconciliation. Added `scenario_reorder_reconcile()` immediately after its existing terminal `reconcile_multiselect()`, leaving all stale-envelope and session early rejections untouched. A fourth bounded summary test reuses the successful-reorder fixture and existing real spacing packet helper, sends Begin/Move/Finish, requires an actual spacing source edit with unchanged semantic order/selection, observes immediate summary retirement before another event, and verifies exact reordered source after Undo. The two authorized files were formatted and frozen for parent execution; no Cargo/Git/native action was run by this reviewer.
@@ -1,49 +0,0 @@
# Scenario semantic reorder — final bounded delivery audit
Source under review: `1c4099a9ef76dd28d17ef892e95edf89e860569f`. Reviewed the checks/native packages, final report, policy successor and root evidence index. No Cargo, Git, network, native application or GUI action was run in this audit. Only this local review file was written.
**Result: no material unresolved packaging, source-attribution, report-consistency or reviewed layout finding.** This is bounded Windows branch evidence, not default-branch integration, release acceptance or full issue-29 completion. Issue 37 remains outside this increment.
## Independence
I independently reviewed the chooser layout correction, diagnostic copy, evidence packaging and claims. I previously authored the summary lifetime fix, its tests and the spacing-terminal reconciliation addition; this audit must not be presented as an independent-author review of those changes. I did independently inspect their captured execution results and native output. Earlier development findings and failed attempts remain in the development review and packages.
## Checks package
Ran the standalone `checks/verify.py` using Python with `-B`: PASS, **17 runs, five recorded failures, 139 logical files, 123 stored members, nine source capsule pairs and 17 source proofs**. Verified all **136** logical Cargo-run files against the original `.local/scenario-group-gates` bytes. The three remaining logical records are packaged helpers/comparator data. Failed history is preserved: test-1 and scenario-1/3/4/7 remain failed, including the bad SourceBundle test path I introduced and subsequently corrected.
Archive: **29,229,448 bytes**, SHA-256 `80406d81e0a5d47bea2ef284bc50a079713d828e8ea291cb96cfcedfeaa16235`.
Final-source scenario-8 records **315 named passes, zero failures, one ignored, 1,296 filtered**, including all **17** App reorder tests (13 adapter/keyboard/layout and four summary-lifetime tests). Final clippy-3, format-2 and build-2 record exit 0, unchanged inputs and matching exact-source proof receipts at 1c4099a9; each inventory has **818** inputs. Commands match the report: Scenario-filter desktop tests, all-target desktop strict Clippy, all-project format check and desktop/CLI build. This is not the full desktop test suite.
The package verifier validates input archives and proof/receipt bindings, not Git itself. Under this audit's no-Git constraint I did not independently repeat the final commit's blob comparison. Earlier review independently compared the predecessor's Git inputs; this audit compared the captured 14df924b and 1c4099a9 source capsules and final proof identities.
## Final source changes
The two captured source inventories differ at exactly six paths: `app/scenario_reorder.rs`, its new `summary_tests.rs`, its existing `tests.rs`, `app/scenario_spacing.rs`, `behavior/scenario/reorder_view.rs` and `behavior/scenario/workspace.rs`.
Independent layout review confirms an embedded scrollbar gutter (`spacing(8)`), explicit available-width constraints and WordOrGlyph wrapping for long aliases. The mounted regression exercises repeated long unbroken aliases, equal allocated columns, right-edge clearance, query containment and multiline height at two scroll offsets. It does not alone establish native readability; the screenshots below supply that separate evidence. The diagnostic copy now states that authored positions disagree with execution order and identifies aliases, avoiding the former misleading claim that semantic order remained unchanged after Apply.
The remaining delta comprises the disclosed summary lifetime/cancel feedback, four summary tests, corrected SourceBundle module path and spacing-terminal reconciliation. Their live event/session exact-envelope checks were not relaxed. Because I authored these, their source assessment here is a scope/accounting statement, not an independent review claim.
## Native package and fidelity
Ran `native/verify.py native-directory`: PASS for its offline archive/source/receipt checks. It reports run-1 as **unaccepted**, with the original snapshot/oracle failure, and runs 2/3 as accepted with passing checks. Accepted flags come from the explicit review configuration; verifier success does not manufacture usability acceptance.
Archive: **12,792,711 bytes**, SHA-256 `0686d691a6ecea80310c5ee31056de36a7389d89a41ca501e6db9427f1ba4a82`, **535 members**. Compared all **534 original-derived members** byte-for-byte against local retained files; the remaining member is generated `review-config.json`, whose contents the verifier checks against manifest declarations. Independently rehashed all **10 omitted executable/DLL entries** against their local retained bytes and manifest pins. These omitted binaries remain unavailable to an offline recipient; the archive does not claim to be a runnable release.
The verifier checks closed process identities, build-result/source-proof/input-map relationships, captured executable/runtime pins, immutable inventories, captured image references and timestamps, exact source sidecars and separately pinned oracle revisions. The v2 oracle restricts the complete-file difference to the literal expected permutation of existing reference chunks, preserving their whitespace trivia; it does not normalize arbitrary source differences. The v1 failure is not relabeled.
Runs 2/3 closed normally at `2026-09-25T01:12:35.077492+00:00` and `2026-09-25T01:15:48.854403+00:00`, with exit 0 and independently empty stdout/stderr. Final-source Studio pin is 119,900,672 bytes, SHA-256 `123e5bebda06c8446cb86c3e071599feea318e69953a949f64b29ec3010d2717`. The captured CLI preflight belongs to the same source and v2 fixture. Run-2 snapshot 76, run-3 snapshot 15, run-3 final source and v2 before-C expectation are exactly equal: **12,764 bytes**, SHA-256 `f48147dbc5101d8dd0386f72ef769d8684935a410d7e60b85d420033d5243f06`.
## Bounded visual and report review
Inspected original run-2 captures 25/26 (1605×872), 60/75 (1104×732), and run-3 capture 15 (1605×872). Before/After text now wraps before the scrollbar with a visible gutter. The compact body needs scrolling; destination and Apply/Cancel remain visible. The committed summary remains visible after the alias-based Saved-order diagnostics increase header height, at ordinary and compact sizes. The fresh-process capture shows the persisted semantic ordinals and zero transient selection. No new material visual issue found in these specific captures.
Read the final report, policy and root index after the native archive pin was filled. Their source versions, test counts, failure list, package pins, corrected-versus-historical native runs and source persistence claims agree with the inspected evidence. The development review's older pending statements are explicitly historical. Both documented verifier commands work when invoked as shown in the root index.
Limitations remain explicit: no native Shift-click claim (selection used contextual Add), no native source-write-failure/temporal-refusal/fallback coverage claim, no arbitrary-window-size or complete screen-reader acceptance, no Linux execution, and no release/default-branch integration assertion. The lengthy F6/reverse-Tab keyboard entry route remains a discoverability limitation. The real App source-write failure branch is still a runtime fixture gap. No broad acceptance is inferred from the package consistency PASS.
This review file is stable for the parent's delivery copy. Publication and remote availability were not performed or verified here.
Delivery-note attribution: the parent reports its first staged-byte check found LF normalization differences in three external text records (report, native manifest, development review), normalized those records, and then passed all 13 staged-file byte checks. ZIP/raw members and archive hashes were unchanged. I did not run Git or independently inspect the index; this is a reported packaging correction, not a product change or a staged-byte verification claim by this reviewer.
@@ -1,50 +0,0 @@
# Scenario semantic reorder interaction experiment
This is a branch-specific experiment toward [issue 29](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/29), not a finished user interaction. [Stable-block semantics](scenario-group-reorder-policy-20260924.md) and [ordinary saved group spacing](scenario-group-spacing-20260924.md) remain distinct. The user requires direct graphical editing, low cognitive load and avoidance of long homogeneous lists. A passing planner or chooser-only prototype cannot establish the entire requested workflow.
## Question and observable success
Can an engineer select C then A in semantic `[A,B,C,D]`, request a position before D, identify `[B,A,C,D]` before and after committing, and then adjust B's saved spacing without confusing the two operations? Repeat with authored coordinates whose visual order differs from semantic order, close or equal-Y anchors, a nested operand, repeated long names, an offscreen anchor and twenty siblings. The exact full-model oracle changes only the common parent's step list; saved coordinates, metadata, unrelated sequences and other views stay unchanged. One Undo/Redo must cover the whole source transaction.
Compare an inline explicit destination chooser with a fixed labelled “Reorder N” drag handle, using the same frozen planner and preview. The chooser begins without a chosen destination; Enter alone must never commit an implicit default. The drag candidate resolves the nearest displayed slot before parent validation and must expose invalid nearer destinations instead of skipping to a farther legal one. Both name the exact parent and before-anchor (or end), display before/after semantic order, retain complete selection/primary and explain that authored positions are preserved.
The chooser's bounded destination set is the explicitly named common parent in semantic order, including selected anchors as no-change choices. That is an explicit semantic Slot choice, distinct from nearest-screen-slot resolution. It must not silently discard same-parent destinations because temporal validation rejects them. Their refusal reasons need to be inspectable. A searchable, bounded display must keep the chosen anchor and local before/after neighborhood visible; a giant unstructured menu fails the stated requirement. The drag candidate must demonstrate that every offered destination is actually reachable, including ties and offscreen destinations, before claiming equivalence.
## Shared frozen session boundary
Capture one immutable Project, full Scenario Context, complete Selection Snapshot and nonnil operation token. Match requested representation's actual owner and the snapshot's document/surface/revision/epoch. Admit every original member against the actual rendered scene and the whole-set semantic planner before considering any no-op. Preserve click order and primary as selection data; construct the moved block in semantic order.
An explicit Slot produces a cached proposal with original/result ID order, semantic Change/Noop/Refused outcome and separate candidate presentation status. Repeating the current Slot reuses its proposal; pointer pixel motion must not revalidate the full Project. Keep only the current proposal: retaining a whole rendered scene for every visited destination would grow with the number of destinations times scene size. Revisiting a previous destination can recompute it. Keep the requested Saved/Automatic identity while resolving the candidate. Ready, Fallback and Unavailable are different presentation states, independent of semantic validity. The experiment must show fallback/unavailable feedback; it must not silently change representation or equate semantic validity with usable geometry. The final UI's commit policy for degraded candidates remains a decision to verify, not an implicit permission in the session API.
Finishing requires an explicitly selected destination, exact unchanged Project, Context, Snapshot and token. The session returns the already validated command and retained selection; it does not execute Document or edit live state. Its command cache stays private. No destination, refusal, cancellation and no-op have no model/source/history side effects. A consumed session cannot finish twice.
## Required App integration after the experiment
The App must reject stale events **before taking a current session or retiring any live owner**. Session consumption on an invalid Finish is not a substitute for early App admission. Start captures a fresh token and the whole rendered selection; another active input owner, dirty Properties/source, pending insertion, source rename or modal must prevent start. Selection generation, representation visit, revision and document-instance identity reject ABA and replacement. Current cancellation, every admitted terminal Finish (success, no-op, semantic refusal or source-commit failure) and admitted failed Start retire the operation lifetime; old Begin/Choose/Finish/Cancel packets cannot affect a newer session on an unchanged selection.
Input paths need explicit ownership: ordinary selected body/header spacing keeps its current meaning; reorder input is separate. Selection/navigation, source and property edits, Undo/Redo and conflicting gestures cancel the old reorder operation without pruning its captured set. A chooser can allow passive layout measurement and scrolling only if those events cannot retarget the exact chosen Slot. A drag additionally owns camera, bounds, pointer threshold and outside-release rules. Source-update failure leaves the accepted model/source/history, stored camera, selection and primary unchanged. Commit executes one command through Document and retains a nonmodal result/Undo explanation. Legacy menu, Details and keyboard actions must never apply to the primary alone while a group is displayed.
Baseline Fallback admission is separate from candidate degradation: the frozen session can admit a semantically valid whole selection visible in the actual fallback scene, but this does not enable coordinate input or accept a UI commit policy. A chooser might offer semantic repair despite a baseline fallback; a geometric drag must establish which geometry its slots mean. Candidate Fallback/Unavailable must be described and explicitly governed before either adapter is enabled. Also settle whether a handle drag returning to its original screen point means cancel/no-change regardless of nearest Slot. Stationary release must not accidentally reorder to the handle's incidental nearest anchor. These are pending interaction decisions, not behavior inferred from the pure planner.
## Evidence and decision boundary
The test-only session and slot experiments are isolated from production input until their results are reviewed. Source-level tests establish transformations, rendering facts and admission invariants; they do not establish discoverability, keyboard access, result comprehension or native usability. Those need a built App adapter, direct Windows workflows at ordinary and compact sizes, then independent review. Linux remains user-paused.
The two candidates are materially different: the chooser favors explicit identity and keyboard reachability; the handle favors continuous canvas manipulation but inherits geometric ambiguity. Their shared session is an implementation dependency, not evidence that either user interaction is accepted. Record actual experiment results before choosing or revising either candidate. Aggregate removal and broader feature completeness remain required beyond this experiment.
## Executed result and resulting decision
At source `cae7a22297d09810c412dbdf4ce5d8a28164b247`, Windows GNU/LLVM `cargo test -p oa-desktop reorder_ --message-format=json -- --nocapture` passed **22 tests, zero failed, zero ignored** (1,573 filtered). This includes all five new session tests, all four slot experiments and thirteen existing tests selected by this name filter. It is not the complete Scenario or desktop suite. The gate captured 814 unchanged inputs and independently compared their exact inventory and bytes with that commit. Workspace formatting and strict all-target desktop lint also passed with the same exact source proof. [Portable checks and independent review](scenario-reorder-interaction-evidence-20260924/README.md) retain all three complete runs.
All four geometry fixtures passed real Saved-presentation Ready and core-error-free assertions. Their source-order list is A/B/C/D:
| Authored message Y | Actual before-slot Y | Observed consequence |
| --- | --- | --- |
| 200 / 500 / 300 / 400 | 158 / 458 / 258 / 358 | Physical A/C/D/B order differs from semantic order; each of the four before-anchors is individually reachable. |
| 200 / 300 / 300 / 500 | 158 / 258 / 258 / 458 | B and C share slot Y; the actual nearest picker chooses B and cannot uniquely address C. Explicit before-C and nearest before-B produce different literal complete-model results for selected D/A. |
| 200 / 300 / 300.125 / 300.25 | 158 / 258 / 258.125 / 258.25 | C is continuously reachable in a subpixel interval, but one-pixel samples at scale 1/pan 0 miss it. This does not claim that Windows always quantizes pointer coordinates. |
| 200 / 300 / 400 / 2000 | 158 / 258 / 358 / 1958 | D cannot win in a fixed 600px-high scale-1/pan-0 viewport; changing the camera can make it reachable. |
The tied-slot counterexample is decisive for the original fixed-handle candidate using unchanged nearest-Y picking: it cannot be the sole method for selecting every valid semantic destination. A different deterministic tie winner only exchanges which identity is unreachable. Preserve exact semantic destination input or add an explicit disambiguation/order-strip interaction. Crossed and close geometry also require feedback, but do not prove universal unreachability. Offscreen evidence is limited to the stated fixed viewport.
**Accepted decision:** reject the unchanged nearest-Y-only candidate as a complete group reorder interface. Build the exact destination and before/after semantic preview foundation first; compare click/keyboard access and a graphical handle using explicit destination identity or a disambiguator before final UI acceptance. The current session and tests remain behind `cfg(test)`; no new product command is enabled by this checkpoint. The model/source equality and round-trip oracles pass, but no actual chooser/handle, App lifetime adapter, native comprehension, keyboard workflow, long-name/twenty-sibling usability or new release is claimed.
@@ -1,27 +0,0 @@
# Scenario reorder destination experiment evidence
[Question, result and bounded decision](../scenario-reorder-interaction-20260924.md). All new code remains under `cfg(test)`; this checkpoint enables no production group reorder command and claims no native UI acceptance. Ordinary group spacing was delivered separately.
Tested source: `cae7a22297d09810c412dbdf4ce5d8a28164b247`. The three Windows GNU/LLVM captures use Rust/Cargo 1.98.1, `stable-x86_64-pc-windows-gnullvm`, one build job and `CARGO_INCREMENTAL=0`. Each captures 814 unchanged input files; exact inventory and Git-blob comparisons pass. Full environment, command, process, output and source-proof records are inside the archive.
```powershell
cargo test -p oa-desktop reorder_ --message-format=json -- --nocapture
cargo fmt --all -- --check
cargo clippy -p oa-desktop --all-targets --message-format=json -- -D warnings
```
Tests: 22 passed / 0 failed / 0 ignored, 1,573 filtered, including all five session and four slot experiment functions. The other thirteen are existing name-filter matches; this is not full Scenario/desktop testing. Format and strict all-target desktop Clippy each exit 0. No Linux or native process was run for this test-only increment.
`checks/evidence.zip`: **3,244,466 bytes**, SHA-256 **`b713a5c8c03f4cb4f4a6884bde7eb081fca00d1f5ebf85427ce14bc03acae166`**. Three closed runs, zero failures, 27 logical files, 23 stored members, one deduplicated source capsule and three exact source proofs. These are all attempts for this increment. Previous spacing failures remain in that contribution's separate evidence.
After checkout, with Python 3.11+:
```powershell
python -B harness/scenario-reorder-interaction-evidence-20260924/checks/verify.py
```
The verifier checks every archive/member pin, captured source capsule, command/result relationship and source-proof receipt. It does not rerun Cargo or the original Git-blob comparison. Reused helper README examples and the manifest's `scenario-selection-cargo-gates` kind are historical naming; actual run names here are `reorder-interaction-test-1`, `reorder-interaction-format-1` and `reorder-interaction-clippy-1`.
The [independent review](review.md) records the original specification ambiguities, bounded cache correction and static/runtime evidence audit. The [slot experiment notes](slot-experiment.md) retain original expectations followed by actual executed findings. Preparation or “pending execution” statements in their earlier sections describe that earlier phase; later closed receipts and appended results supersede them.
The result rejects unchanged nearest-Y picking as the only method for choosing exact semantic destinations: admitted equal-Y slots leave one anchor unreachable. It does not accept a chooser's usability, reject every possible graphical handle, prove all nested/append destinations, establish Windows pointer quantization, or claim that offscreen targets cannot be reached after a camera change. See the report for remaining App and native requirements.
@@ -1,14 +0,0 @@
# Scenario selection captured checks
These tools package explicitly named, completed runs from `CHECKOUT/.local/scenario-group-gates` without executing archived source or running Cargo. Adapted only in input-directory and manifest-kind names from the published prerelease Connection/Route evidence tools at `93d2a6f9af40061bf92cd81ba33d5648d0181cb1`.
```powershell
python -B prepare.py --root CHECKOUT --runs selection-scenario-1 selection-scenario-2 selection-format-1 --output NEW_DIRECTORY
python -B NEW_DIRECTORY/verify.py
```
The manifest is authoritative for the selected runs. Failed runs remain failures: archive consistency is not a test pass. Every raw stream and receipt retains its original bytes. Identical input maps and ZIPs are stored by hash; the logical-file map preserves their original run paths. Historical capture helpers and exact source comparators are included as data, never executed by verification.
Source proofs compare every captured input with a named Git commit; the offline package verifier checks those receipts and their pinned comparator identity but does not repeat the Git comparison. The captured HEAD and the named proof commit retain their respective meanings. The verifier checks archive inventory, hashes, input ZIP contents, process/command/result consistency and executable artifact relationships. It does not establish hermetic builds, native workflow acceptance, UI integration, or release acceptance.
Use `--check-only` instead of `--output` for an in-memory consistency check. Existing output directories refuse. Python 3.11+ standard library is sufficient; `-B` avoids bytecode writes.
@@ -1,22 +0,0 @@
{
"prepare.py": {
"bytes": 6308,
"sha256": "2d500f5d43a49893abb3936f880e61b8adc623531e26f0288983502e386894db"
},
"verify.py": {
"bytes": 11436,
"sha256": "ea8e781003bec21f2ac4af23e21b68ca5639a98eb31505c10b2869f57565e917"
},
"README.md": {
"bytes": 1641,
"sha256": "4c5c582cbdf02c8704b9add656d273c275b10eed87219131e3bba639a4c4bd8a"
},
"manifest.json": {
"bytes": 9761,
"sha256": "34b9791fca0d23e6d27e68daf769fe6952cb38aef83dec22594ff8900bbdcdb6"
},
"evidence.zip": {
"bytes": 3244466,
"sha256": "b713a5c8c03f4cb4f4a6884bde7eb081fca00d1f5ebf85427ce14bc03acae166"
}
}
@@ -1,203 +0,0 @@
{
"schema": 1,
"kind": "scenario-selection-cargo-gates",
"runs": {
"reorder-interaction-format-1": {
"command": [
"cargo",
"fmt",
"--all",
"--",
"--check"
],
"head": "cae7a22297d09810c412dbdf4ce5d8a28164b247",
"exitCode": 0,
"inputCount": 814,
"helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"sourceProof": {
"commit": "cae7a22297d09810c412dbdf4ce5d8a28164b247",
"pin": {
"bytes": 607,
"sha256": "b0e53cc3fe43733664c57fcb1894da1a966c9dae07bbd30af03d9e743aeba747"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"reorder-interaction-test-1": {
"command": [
"cargo",
"test",
"-p",
"oa-desktop",
"reorder_",
"--message-format=json",
"--",
"--nocapture"
],
"head": "cae7a22297d09810c412dbdf4ce5d8a28164b247",
"exitCode": 0,
"inputCount": 814,
"helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"sourceProof": {
"commit": "cae7a22297d09810c412dbdf4ce5d8a28164b247",
"pin": {
"bytes": 605,
"sha256": "c0cae0486b5d47a4d2bfb79e097cb1487a22bd10bbd759af030439a2b94ac7a7"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"reorder-interaction-clippy-1": {
"command": [
"cargo",
"clippy",
"-p",
"oa-desktop",
"--all-targets",
"--message-format=json",
"--",
"-D",
"warnings"
],
"head": "cae7a22297d09810c412dbdf4ce5d8a28164b247",
"exitCode": 0,
"inputCount": 814,
"helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"sourceProof": {
"commit": "cae7a22297d09810c412dbdf4ce5d8a28164b247",
"pin": {
"bytes": 607,
"sha256": "224c3cfb30b88de8afa9981491826e7332127042542b68a042a87e90b5ffe5bc"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
}
},
"captureHelpers": {
"gate.py": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"source-proof.py": "helpers/c86f51d9b8465341dab73d045923ec306b15ebabf7c97b81beac7afe73bd72a1.py"
},
"logicalFiles": {
"cargo/reorder-interaction-clippy-1/inputs.json": "capsules/43bfafea79cfd2268d667504dbce7531482ff016ff5daf54065285f0564fa036.json",
"cargo/reorder-interaction-clippy-1/inputs.zip": "capsules/a6aca4ff50b27cefa53e34113988f7574f87678349d594c3d4bb769bb3fe3450.zip",
"cargo/reorder-interaction-clippy-1/process.json": "cargo/reorder-interaction-clippy-1/process.json",
"cargo/reorder-interaction-clippy-1/result.json": "cargo/reorder-interaction-clippy-1/result.json",
"cargo/reorder-interaction-clippy-1/source-proof.json": "cargo/reorder-interaction-clippy-1/source-proof.json",
"cargo/reorder-interaction-clippy-1/started.json": "cargo/reorder-interaction-clippy-1/started.json",
"cargo/reorder-interaction-clippy-1/stderr.log": "cargo/reorder-interaction-clippy-1/stderr.log",
"cargo/reorder-interaction-clippy-1/stdout.log": "cargo/reorder-interaction-clippy-1/stdout.log",
"cargo/reorder-interaction-format-1/inputs.json": "capsules/43bfafea79cfd2268d667504dbce7531482ff016ff5daf54065285f0564fa036.json",
"cargo/reorder-interaction-format-1/inputs.zip": "capsules/a6aca4ff50b27cefa53e34113988f7574f87678349d594c3d4bb769bb3fe3450.zip",
"cargo/reorder-interaction-format-1/process.json": "cargo/reorder-interaction-format-1/process.json",
"cargo/reorder-interaction-format-1/result.json": "cargo/reorder-interaction-format-1/result.json",
"cargo/reorder-interaction-format-1/source-proof.json": "cargo/reorder-interaction-format-1/source-proof.json",
"cargo/reorder-interaction-format-1/started.json": "cargo/reorder-interaction-format-1/started.json",
"cargo/reorder-interaction-format-1/stderr.log": "cargo/reorder-interaction-format-1/stderr.log",
"cargo/reorder-interaction-format-1/stdout.log": "cargo/reorder-interaction-format-1/stdout.log",
"cargo/reorder-interaction-test-1/inputs.json": "capsules/43bfafea79cfd2268d667504dbce7531482ff016ff5daf54065285f0564fa036.json",
"cargo/reorder-interaction-test-1/inputs.zip": "capsules/a6aca4ff50b27cefa53e34113988f7574f87678349d594c3d4bb769bb3fe3450.zip",
"cargo/reorder-interaction-test-1/process.json": "cargo/reorder-interaction-test-1/process.json",
"cargo/reorder-interaction-test-1/result.json": "cargo/reorder-interaction-test-1/result.json",
"cargo/reorder-interaction-test-1/source-proof.json": "cargo/reorder-interaction-test-1/source-proof.json",
"cargo/reorder-interaction-test-1/started.json": "cargo/reorder-interaction-test-1/started.json",
"cargo/reorder-interaction-test-1/stderr.log": "cargo/reorder-interaction-test-1/stderr.log",
"cargo/reorder-interaction-test-1/stdout.log": "cargo/reorder-interaction-test-1/stdout.log",
"comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py",
"helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"helpers/c86f51d9b8465341dab73d045923ec306b15ebabf7c97b81beac7afe73bd72a1.py": "helpers/c86f51d9b8465341dab73d045923ec306b15ebabf7c97b81beac7afe73bd72a1.py"
},
"members": {
"capsules/43bfafea79cfd2268d667504dbce7531482ff016ff5daf54065285f0564fa036.json": {
"bytes": 136737,
"sha256": "43bfafea79cfd2268d667504dbce7531482ff016ff5daf54065285f0564fa036"
},
"capsules/a6aca4ff50b27cefa53e34113988f7574f87678349d594c3d4bb769bb3fe3450.zip": {
"bytes": 3254695,
"sha256": "a6aca4ff50b27cefa53e34113988f7574f87678349d594c3d4bb769bb3fe3450"
},
"cargo/reorder-interaction-clippy-1/process.json": {
"bytes": 179,
"sha256": "cb0063779120dff8b7a512c94ac157974503a8da4d33e7db72041c8783493df2"
},
"cargo/reorder-interaction-clippy-1/result.json": {
"bytes": 1958,
"sha256": "ec2e7b0dfd7f5aa2a34783f4b7082afc159a665e6c09f55d1924f1df9f22b17c"
},
"cargo/reorder-interaction-clippy-1/source-proof.json": {
"bytes": 607,
"sha256": "224c3cfb30b88de8afa9981491826e7332127042542b68a042a87e90b5ffe5bc"
},
"cargo/reorder-interaction-clippy-1/started.json": {
"bytes": 1563,
"sha256": "9192fb388be502a8e99ce1140ad6ef18b495d9eac752e4a682ea40acf1a04334"
},
"cargo/reorder-interaction-clippy-1/stderr.log": {
"bytes": 134,
"sha256": "b6b278b2d1f23091255fe43a194eb4c496fe6a51f98838af9f0f1d02f1bdb4b9"
},
"cargo/reorder-interaction-clippy-1/stdout.log": {
"bytes": 269431,
"sha256": "f2a6d72c9ee496aecb3db4126c8e8a4248746ccc07a04ed199fa354ab51d5a20"
},
"cargo/reorder-interaction-format-1/process.json": {
"bytes": 116,
"sha256": "99e05ce65425dc3d23db945201b2d10808295189374dd75ee5632d78925af632"
},
"cargo/reorder-interaction-format-1/result.json": {
"bytes": 1868,
"sha256": "743188d86811f129ba5991228edb82b200844aef3046619de49e7245a945e9b3"
},
"cargo/reorder-interaction-format-1/source-proof.json": {
"bytes": 607,
"sha256": "b0e53cc3fe43733664c57fcb1894da1a966c9dae07bbd30af03d9e743aeba747"
},
"cargo/reorder-interaction-format-1/started.json": {
"bytes": 1480,
"sha256": "4df85b8b570f1d1fcd810205efc1c057ce02aeca996739a6616c689d9087ef9e"
},
"cargo/reorder-interaction-format-1/stderr.log": {
"bytes": 0,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"cargo/reorder-interaction-format-1/stdout.log": {
"bytes": 0,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"cargo/reorder-interaction-test-1/process.json": {
"bytes": 169,
"sha256": "31ddcefd1a64b54ba33eac12325491f94a001e109ba48769548014824d53b2a5"
},
"cargo/reorder-interaction-test-1/result.json": {
"bytes": 3273,
"sha256": "de71d8a0b8b4b7de401b4ccc1efd03bbb3bf9760bd4b97883ba6c4786d0ada5b"
},
"cargo/reorder-interaction-test-1/source-proof.json": {
"bytes": 605,
"sha256": "c0cae0486b5d47a4d2bfb79e097cb1487a22bd10bbd759af030439a2b94ac7a7"
},
"cargo/reorder-interaction-test-1/started.json": {
"bytes": 1548,
"sha256": "056cbfc2ec65f864cdab91fcd314b3be10c72721878e6f659215d3580e247e3c"
},
"cargo/reorder-interaction-test-1/stderr.log": {
"bytes": 284,
"sha256": "356bc71e25ef09fb5af8521c4a4513f8a77cd55fbe498328ee7ff77f8693e83c"
},
"cargo/reorder-interaction-test-1/stdout.log": {
"bytes": 295941,
"sha256": "ea183f5dbe6a5d369525f6d0de171e3df6175c60ffc5d2ebcdb1023218d77d84"
},
"comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py": {
"bytes": 10094,
"sha256": "7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a"
},
"helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py": {
"bytes": 3778,
"sha256": "3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2"
},
"helpers/c86f51d9b8465341dab73d045923ec306b15ebabf7c97b81beac7afe73bd72a1.py": {
"bytes": 1396,
"sha256": "c86f51d9b8465341dab73d045923ec306b15ebabf7c97b81beac7afe73bd72a1"
}
}
}
@@ -1,110 +0,0 @@
"""Package explicitly selected closed gate captures without running Cargo."""
from pathlib import Path
import argparse
import json
import zipfile
from verify import RAW_FILES, pin, parsed, need, run_name, verify_data, verify_package
HERE = Path(__file__).resolve().parent
def collect(root, runs, helpers, comparators=()):
gates = root / ".local/scenario-group-gates"
candidates = [gates / name for name in ("gate.py", "source-proof.py")] + helpers
helper_bytes = [path.read_bytes() for path in candidates]
comparator_paths = [root / ".local/process-source-evidence-tools/verify.py", *comparators]
comparator_bytes = [path.read_bytes() for path in comparator_paths if path.is_file()]
payload, logical, specs = {}, {}, {}
def add(name, data):
stored = ("capsules/" + pin(data)["sha256"] + Path(name).suffix
if name.rsplit("/", 1)[1] in {"inputs.json", "inputs.zip"} else name)
need(stored not in payload or payload[stored] == data, "deduplication collision")
payload[stored] = data
logical[name] = stored
helper_catalog = {}
for name, data in zip(("gate.py", "source-proof.py"), helper_bytes):
helper = "helpers/" + pin(data)["sha256"] + ".py"
add(helper, data)
helper_catalog[name] = helper
for run in runs:
folder = gates / run
need(folder.is_dir(), "run directory missing: " + run)
paths = list(folder.iterdir())
names = {path.name for path in paths}
need(all(path.is_file() and not path.is_symlink() for path in paths)
and names in (RAW_FILES, RAW_FILES | {"source-proof.json"}), "exact closed raw files required: " + run)
raw = {path.name: path.read_bytes() for path in paths}
result = parsed(raw["result.json"])
need("finish" in result and result["inputsUnchanged"] is True, "unfinished/changed run refused: " + run)
matches = [data for data in helper_bytes if pin(data) == result["helper"]]
need(bool(matches), "exact historical helper unavailable for " + run + "; supply --helper PATH")
helper = "helpers/" + result["helper"]["sha256"] + ".py"
add(helper, matches[0])
for name, data in sorted(raw.items()):
add("cargo/" + run + "/" + name, data)
specs[run] = {key: result[key] for key in ("command", "head", "exitCode", "inputCount")}
specs[run]["helper"] = helper
if "source-proof.json" in raw:
proof = parsed(raw["source-proof.json"])
matches = [data for data in comparator_bytes if pin(data) == proof["comparator"]]
need(bool(matches), "exact source comparator unavailable for " + run + "; supply --comparator PATH")
comparator = "comparators/" + proof["comparator"]["sha256"] + ".py"
add(comparator, matches[0])
specs[run]["sourceProof"] = {"commit": proof["commit"], "pin": pin(raw["source-proof.json"]), "comparator": comparator}
manifest = {"schema": 1, "kind": "scenario-selection-cargo-gates", "runs": specs,
"captureHelpers": helper_catalog,
"logicalFiles": dict(sorted(logical.items())),
"members": {name: pin(data) for name, data in sorted(payload.items())}}
summary = verify_data(manifest, payload.__getitem__)
return payload, manifest, summary
def write_json(path, value):
with path.open("xb") as output:
output.write((json.dumps(value, indent=2) + "\n").encode("utf-8"))
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, required=True, help="checkout holding .local/scenario-group-gates")
parser.add_argument("--runs", nargs="+", required=True, help="explicit run names, including failures to retain")
parser.add_argument("--helper", type=Path, action="append", default=[], help="additional exact historical helper (repeatable)")
parser.add_argument("--comparator", type=Path, action="append", default=[], help="additional exact historical source comparator (repeatable)")
parser.add_argument("--output", type=Path, help="fresh, nonexistent package directory (required unless --check-only)")
parser.add_argument("--check-only", action="store_true", help="validate raw captures in memory without creating an archive")
args = parser.parse_args()
need(all(run_name(run) for run in args.runs) and len(args.runs) == len(set(args.runs)), "unique safe run names required")
need(args.check_only or args.output is not None, "--output is required unless --check-only")
if not args.check_only:
need(not args.output.exists(), "output must be a fresh directory: " + str(args.output))
payload, manifest, summary = collect(args.root.resolve(), args.runs, args.helper, args.comparator)
if args.check_only:
print("PASS raw consistency; no package written:", summary)
return
out = args.output.resolve()
out.mkdir(parents=True, exist_ok=False)
for name in ("evidence.zip", "manifest.json", "files.json"):
need(not (out / name).exists(), "existing package output refused: " + name)
for name in ("prepare.py", "verify.py", "README.md"):
data = (HERE / name).read_bytes()
destination = out / name
if destination.exists():
need(destination.read_bytes() == data, "different existing package tool: " + name)
else:
with destination.open("xb") as output:
output.write(data)
with zipfile.ZipFile(out / "evidence.zip", "x", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive:
for name, data in sorted(payload.items()):
info = zipfile.ZipInfo(name, date_time=(2026, 9, 23, 0, 0, 0))
info.compress_type = zipfile.ZIP_DEFLATED
info.external_attr = 0o100644 << 16
archive.writestr(info, data)
write_json(out / "manifest.json", manifest)
names = ("prepare.py", "verify.py", "README.md", "manifest.json", "evidence.zip")
write_json(out / "files.json", {name: pin((out / name).read_bytes()) for name in names})
need(verify_package(out) == summary, "written package differs from validated captures")
print("PASS package readback:", out, summary)
if __name__ == "__main__":
main()
@@ -1,194 +0,0 @@
"""Offline consistency checks only; never execute or extract archived inputs."""
from pathlib import Path, PurePosixPath
from datetime import datetime
import argparse
import hashlib
import io
import json
import math
import re
import stat
import zipfile
RAW_FILES = {"started.json", "process.json", "result.json", "stdout.log", "stderr.log", "inputs.json", "inputs.zip"}
OUTER_FILES = {"prepare.py", "verify.py", "README.md", "manifest.json", "evidence.zip"}
def need(condition, message):
if not condition:
raise ValueError(message)
def pin(data):
return {"bytes": len(data), "sha256": hashlib.sha256(data).hexdigest()}
def valid_pin(value):
return (isinstance(value, dict) and set(value) == {"bytes", "sha256"}
and type(value["bytes"]) is int and value["bytes"] >= 0
and isinstance(value["sha256"], str)
and re.fullmatch(r"[0-9a-f]{64}", value["sha256"]) is not None)
def safe(name):
return (isinstance(name, str) and bool(name) and "\\" not in name and ":" not in name
and all(ord(char) >= 32 for char in name)
and not PurePosixPath(name).is_absolute()
and all(part not in ("", ".", "..") for part in name.split("/")))
def run_name(name):
return isinstance(name, str) and re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]*", name) is not None
def parsed(data):
def pairs(items):
result = {}
for key, value in items:
need(key not in result, "duplicate JSON key: " + key)
result[key] = value
return result
def constant(value):
raise ValueError("nonfinite JSON constant: " + value)
return json.loads(data, object_pairs_hook=pairs, parse_constant=constant)
def inventory(archive, expected):
names = archive.namelist()
need(len(names) == len(set(names)) and set(names) == set(expected), "exact ZIP membership")
for entry in archive.infolist():
need(safe(entry.filename) and not entry.is_dir(), "unsafe ZIP member: " + entry.filename)
need(not stat.S_ISLNK(entry.external_attr >> 16), "ZIP symlink: " + entry.filename)
need(valid_pin(expected[entry.filename]), "invalid member pin: " + entry.filename)
need(entry.file_size == expected[entry.filename]["bytes"], "ZIP recorded size: " + entry.filename)
def ordered_times(*values):
times = [datetime.fromisoformat(value) for value in values]
return all(value.utcoffset() is not None for value in times) and times == sorted(times)
def verify_data(manifest, read):
"""Shared read-only verifier for packaged bytes or prepare's in-memory payload."""
need(manifest["schema"] == 1 and manifest["kind"] == "scenario-selection-cargo-gates", "manifest schema")
runs, logical, stored = manifest["runs"], manifest["logicalFiles"], manifest["members"]
helpers = manifest["captureHelpers"]
need(isinstance(helpers, dict) and set(helpers) == {"gate.py", "source-proof.py"}, "capture helper names")
need(all(isinstance(name, str) and re.fullmatch(r"helpers/[0-9a-f]{64}\.py", name)
for name in helpers.values()), "capture helper mapping")
need(isinstance(runs, dict) and bool(runs) and all(run_name(run) for run in runs), "explicit run inventory")
need(all(safe(name) for name in logical) and all(safe(name) for name in stored), "unsafe manifest path")
need(set(logical.values()) == set(stored), "unreferenced or absent stored members")
for name, expected in stored.items():
need(valid_pin(expected) and pin(read(name)) == expected, "stored member hash: " + name)
expected_logical = {"cargo/" + run + "/" + name for run in runs for name in RAW_FILES}
expected_logical.update(spec["helper"] for spec in runs.values())
expected_logical.update(helpers.values())
for run, spec in runs.items():
if "sourceProof" in spec:
proof_spec = spec["sourceProof"]
need(set(proof_spec) == {"commit", "pin", "comparator"}, run + ": exact source proof spec")
need(isinstance(proof_spec["commit"], str) and re.fullmatch(r"[0-9a-f]{40}", proof_spec["commit"]), run + ": named full source commit")
need(valid_pin(proof_spec["pin"]), run + ": source proof pin")
need(isinstance(proof_spec["comparator"], str) and re.fullmatch(r"comparators/[0-9a-f]{64}\.py", proof_spec["comparator"]), run + ": comparator path")
expected_logical.update({"cargo/" + run + "/source-proof.json", proof_spec["comparator"]})
need(set(logical) == expected_logical, "exact logical raw/helper inventory")
for name, destination in logical.items():
if name.startswith("cargo/") and name.rsplit("/", 1)[1] in {"inputs.json", "inputs.zip"}:
expected = "capsules/" + stored[destination]["sha256"] + PurePosixPath(name).suffix
need(destination == expected, "input capsule mapping: " + name)
else:
need(destination == name, "only input maps/archives may be deduplicated: " + name)
def raw(name):
return read(logical[name])
for name, helper in helpers.items():
need(helper == "helpers/" + pin(raw(helper))["sha256"] + ".py", "named helper hash: " + name)
seen_capsules = set()
for run, spec in runs.items():
prefix = "cargo/" + run + "/"
start = parsed(raw(prefix + "started.json"))
process = parsed(raw(prefix + "process.json"))
result = parsed(raw(prefix + "result.json"))
required_start = {"start", "command", "cwd", "head", "scope", "inputs", "archive",
"inputCount", "environment", "helper", "cargo", "rustc"}
need(required_start <= start.keys(), run + ": incomplete start receipt")
need(all(result.get(key) == value for key, value in start.items()), run + ": start/result relationship")
command = result["command"]
need(isinstance(command, list) and command and command[0] == "cargo"
and all(isinstance(arg, str) for arg in command), run + ": Cargo command")
need(command == process["command"] == spec["command"], run + ": command relationship")
need(result["head"] == spec["head"] and re.fullmatch(r"(?:[0-9a-f]{40}|[0-9a-f]{64})", result["head"]), run + ": HEAD")
need(type(result["exitCode"]) is int and result["exitCode"] == spec["exitCode"], run + ": recorded exit")
need(result["inputsUnchanged"] is True, run + ": changed inputs refused")
need(type(result["seconds"]) in (int, float) and math.isfinite(result["seconds"])
and result["seconds"] >= 0, run + ": duration")
need(type(process["pid"]) is int and process["pid"] > 0, run + ": process identity")
need(ordered_times(start["start"], process["observed"], result["finish"]), run + ": completed timestamp order")
for field, filename in [("inputs", "inputs.json"), ("archive", "inputs.zip"), ("stdout", "stdout.log"), ("stderr", "stderr.log")]:
need(result[field] == pin(raw(prefix + filename)), run + ": " + field + " pin")
need(valid_pin(result["helper"]) and result["helper"] == pin(raw(spec["helper"])), run + ": helper pin")
need(spec["helper"] == "helpers/" + result["helper"]["sha256"] + ".py", run + ": helper mapping")
inputs = parsed(raw(prefix + "inputs.json"))
need(isinstance(inputs, dict) and all(safe(name) for name in inputs), run + ": safe input inventory")
need(type(result["inputCount"]) is int and len(inputs) == result["inputCount"] == spec["inputCount"], run + ": input counts")
if "sourceProof" in spec:
proof_spec = spec["sourceProof"]
proof_bytes = raw(prefix + "source-proof.json")
proof = parsed(proof_bytes)
need(pin(proof_bytes) == proof_spec["pin"], run + ": source proof pinned bytes")
need(proof["run"] == run and proof["commit"] == proof_spec["commit"], run + ": proof run/commit identity")
need(proof["status"] == "PASS exact captured input inventory and Git blob bytes", run + ": source proof status")
need(proof["inputCount"] == len(inputs) and proof["inputMap"] == result["inputs"], run + ": proof input relationship")
need(proof["result"] == pin(raw(prefix + "result.json")), run + ": proof result relationship")
need(ordered_times(result["finish"], proof["verified"]), run + ": proof time after closed gate")
comparator = proof_spec["comparator"]
need(valid_pin(proof["comparator"]) and pin(raw(comparator)) == proof["comparator"], run + ": comparator bytes")
need(comparator == "comparators/" + proof["comparator"]["sha256"] + ".py", run + ": comparator identity")
need(sum(name.startswith("crates/vendor/iced_widget/") for name in inputs) == 71, run + ": vendor count")
capsule = (logical[prefix + "inputs.json"], logical[prefix + "inputs.zip"])
if capsule not in seen_capsules:
with zipfile.ZipFile(io.BytesIO(raw(prefix + "inputs.zip"))) as source:
inventory(source, inputs)
for name, expected in inputs.items():
need(pin(source.read(name)) == expected, run + ": source hash " + name)
seen_capsules.add(capsule)
artifacts = []
for line in raw(prefix + "stdout.log").decode("utf-8", errors="replace").splitlines():
try:
value = json.loads(line)
except json.JSONDecodeError:
continue
if isinstance(value, dict) and value.get("reason") == "compiler-artifact" and value.get("executable"):
artifacts.append(value)
need(isinstance(result["artifacts"], list), run + ": artifact records")
need(artifacts == [record["cargo"] for record in result["artifacts"]], run + ": Cargo JSON artifact relationship")
for record in result["artifacts"]:
need(set(record) == {"cargo", "readback"}
and (record["readback"] is None or valid_pin(record["readback"])), run + ": artifact readback pin")
print(run, "recorded exit", result["exitCode"], "inputs", len(inputs), "executable records", len(artifacts))
return {"runs": len(runs), "recordedFailures": sum(spec["exitCode"] != 0 for spec in runs.values()),
"logicalFiles": len(logical), "storedMembers": len(stored), "sourceCapsulePairs": len(seen_capsules),
"captureHelpers": helpers, "sourceProofs": sum("sourceProof" in spec for spec in runs.values())}
def verify_package(root):
files = parsed((root / "files.json").read_bytes())
need(set(files) == OUTER_FILES, "outer file inventory")
for name, expected in files.items():
need(valid_pin(expected) and pin((root / name).read_bytes()) == expected, "outer hash: " + name)
manifest = parsed((root / "manifest.json").read_bytes())
with zipfile.ZipFile(root / "evidence.zip") as archive:
inventory(archive, manifest["members"])
return verify_data(manifest, archive.read)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("package", nargs="?", type=Path, default=Path(__file__).resolve().parent)
args = parser.parse_args()
print("PASS offline consistency:", verify_package(args.package))
print("Historical receipts only; no source/binary execution or native acceptance claim.")
if __name__ == "__main__":
main()
@@ -1,57 +0,0 @@
# Scenario reorder session and interaction experiment review
Bounded independent review in `C:/ar-scenario-9e8r6t`, on the parent's identified base `53817298`. No production edits, Cargo, native actions or Gitea operations. This review distinguishes an isolated semantic foundation from an accepted user interaction. Source/test review will follow the writers' frozen handoff; no gate result is assumed.
## Initial contract assessment
The experiment is justified as a foundation. `harness/scenario-reorder-interaction-20260924.md:7-11` supplies a discriminating noncontiguous selection task, preserved saved coordinates, observable before/after order, subsequent ordinary spacing, equal anchors, offscreen destinations and repeated names. This directly addresses the user's graphical editing and low-cognitive-load requirements (`PROJECT_GOAL.md:93-106`). The explicit chooser and labelled handle are materially different candidates; passing chooser transformations alone cannot decide between them or satisfy the user's complaint about menu-driven diagram editing.
The exact requested representation plus full Scenario Context/Snapshot/token boundary is appropriate. Whole original typed selection must be admitted before no-op; no selected subset, silent representation switch or pruning is permitted. The App's early admission obligation is correctly distinct from consuming a pure session. Isolating both new modules behind `cfg(test)` prevents this experiment from silently enabling production group reorder.
## Contract questions before production input
1. **Retire every admitted terminal outcome.** The initial App boundary at interaction line 23 names cancellation, successful/no-op Finish and failed Start, but omits Refused Finish and a failed Document/source commit. A refused current operation can leave all selection/model values unchanged, making duplicate terminal packets a meaningful adversary. Explicitly retire these outcomes too, without changing accepted model/source/history/selection/camera and without touching a newer owner on stale input. This is a missing future-adapter invariant, not a current production defect.
2. **Separate baseline presentation admission from candidate commit policy.** `scenario_layout/presentation.rs:79-85` makes coordinate input available only for Ready, while Fallback retains inspectable automatic geometry and requested Saved identity. A semantic chooser is not coordinate input. Specify whether a Fallback baseline can start each candidate and separately decide how a semantically valid Fallback/Unavailable candidate may finish. A returned command plus status must not be treated as implicit permission to commit. The current test-only foundation may expose both facts while keeping the product decision unresolved.
3. **Choose stationary/origin-return semantics for the drag.** The required camera/threshold/outside rules do not yet settle whether return to the pressed screen location cancels or selects the nearest semantic destination. A saved press need not be nearest its own anchor, particularly with close/crossed rows. Define and test either behavior before adapter acceptance; do not infer an origin-return no-op from the planner's selected-anchor no-op rule.
These points were sent to the parent. They do not prevent publishing an explicitly incomplete test-only session/slot experiment. Native success still requires users to identify the exact parent/anchor and final semantic order despite preserved authored positions, then perform a normal spacing operation without accidental semantic edits. Repeated long names need a usable visible disambiguator; internal IDs alone do not establish this. A bounded searchable list is an experiment to evaluate, not an automatic low-cognitive-load success.
## Contract revision and frozen session source review
Re-read the revised interaction specification: it now explicitly retires every admitted terminal Finish including semantic refusal and source-commit failure, separates baseline Fallback admission from candidate commit policy, and leaves stationary/origin-return drag rules as decisions required before enabling an adapter. The initial three contract findings above are therefore resolved as specification clarifications; the product decisions remain openly pending.
Reviewed the writer's frozen `behavior/scenario/reorder_session.rs` and its five test functions. **No concrete unresolved semantic/admission defect found in this isolated session.** `Session::new` checks nonnil document/visit/token, exact generic selection context and requested representation's actual Scenario owner, then admits every original member against the actual presented scene before whole-set planner preparation. The private immutable Project and Snapshot retain selection click order and primary; the planner constructs semantic order independently. Current proposal storage is bounded to one rendered proposal. Refusal replaces the current proposal rather than leaving an earlier valid command armed. `finish(self, ...)` compares complete Project, Context, Snapshot (including generation) and token before returning a private cached command, and Rust ownership prevents reuse of that session. It neither mutates Document nor proves App early admission or lifetime retirement.
One concrete evaluator weakness was repaired during review: pointer equality to an `Option<Preview>` field would remain true even if that field were recomputed in place. The final test uses test-only proposal-build instrumentation, checking one build for repeated current Slot, a second for another Slot and a third when returning to the original. This now distinguishes the intended bounded cache behavior.
The tests independently construct the expected complete Project by editing only the literal parent steps, retaining nonempty saved lane/message overrides, metadata and a second saved view. They cover original selection/primary, explicit absence of destination versus selected-anchor no-op, temporal refusal, mixed and visibly cross-parent full sets, foreign/missing visibility, changed Project, epoch/revision/document/visit/token, changed selection order and generation-only primary ABA. Equality with the expected complete Project is stronger than preview/commit agreement alone. The Fallback case starts from authored call/reply geometry already requiring fallback; it establishes baseline Fallback admission and requested Saved identity preservation, **not** a Ready-to-Fallback transition.
Remaining bounded coverage gaps: no constructed semantically valid candidate yielding Unavailable, no independently exercised Ready-to-Fallback transition, no native feedback, and no App command/history/source transaction or stale-event adapter in this module. Returning a command alongside a degraded status remains information for future policy, not permission to execute it. All new session code is test-only. These omissions must remain explicit and must be addressed where the subsequent adapter's actual policy needs them; they do not justify claiming either interaction accepted. No test execution result is claimed by this reviewer.
## Frozen slot experiment and final pre-gate assessment
Reviewed all four `reorder_slot_experiments.rs` tests against actual `Scene::slot_at` (`behavior.rs:1855`), which chooses the global nearest Y before checking parent identity. The experiments resolve actual Saved presentation and assert Ready rather than assuming authored coordinates rendered. Their `exact_change` oracle (`reorder_slot_experiments.rs:102`) alters only the independently specified literal parent order, compares the complete Project, and round-trips source. No concrete fixture/oracle flaw found statically.
The crossed case (`:142`) distinguishes semantic order from the actual A/C/D/B screen order while checking each before-anchor remains reachable. The equal-Y case (`:161`) demonstrates the B/C tie winner and compares two distinct literal semantic outcomes: exact before-C versus the nearest picker returning before-B. The close case (`:189`) explicitly retains continuous reachability and limits its negative result to integer-pixel samples at a fixed camera; it does not establish platform pointer quantization. The offscreen case (`:207`) fixes scale one, pan zero and a 600-pixel viewport, and separately proves reachability outside that viewport. Neither justifies a claim that scrolling/zooming can never reach the destination.
These are geometry/planner experiments, not an implemented chooser or drag handle. They make unchanged nearest-Y input an insufficient universal equivalent of exact semantic destinations, but do not select the final interaction. Nested invalid-nearer destinations, repeated long names, twenty-sibling display, keyboard reachability, discoverability, feedback, Ready-to-degraded candidate transitions and a built App lifetime adapter remain outside these four cases. The chooser could still fail the user's direct graphical editing requirement; a handle could add a bounded disambiguator or operation-local order preview. Compare the resulting workflows before acceptance.
Also reviewed the parent's final temporal test enhancement: it first caches a valid changed order, then chooses the temporally invalid before-call destination and requires Finish to refuse. This meaningfully guards against executing an earlier valid proposal after a later refusal.
**Final static conclusion: no unresolved material defect found in the bounded test-only foundation.** The specification findings and cache-test weakness were addressed as recorded above. Nine new test functions (five session, four slot experiments) await the parent's gate; this review ran no Cargo and claims no passing test/native result. Review file is stable for handoff.
## Closed gate and decision audit at cae7a222
Read the actual closed receipts and logs for `reorder-interaction-test-1`, `reorder-interaction-format-1` and, now closed, `reorder-interaction-clippy-1` at `cae7a22297d09810c412dbdf4ce5d8a28164b247`. All three report exit 0 and unchanged 814 inputs. Independently rehashed each retained input map, input ZIP and raw stdout/stderr against its receipt, counted all 814 ZIP members, and checked each source-proof receipt's commit/count and exact input-map/result bindings. This confirms receipt consistency; this audit does not claim to rerun the complete Git blob comparison.
The actual test log has exactly **22 named passing tests: five new session tests, four new slot experiments and thirteen existing tests**, with zero failed, zero ignored and 1,573 filtered. Both formatting and strict `cargo clippy -p oa-desktop --all-targets --message-format=json -- -D warnings` are successful closed runs at that same source. No additional Cargo was run by this reviewer.
Re-read the interaction document's Executed result/decision section and the policy's new geometric-versus-semantic input clarification. **No material overclaim found.** Actual Ready equal-Y geometry supplies a concrete counterexample to unchanged nearest-Y input being the sole complete semantic destination mechanism; changing only the deterministic tie winner leaves another identity unreachable. This supports the negative decision while leaving chooser, order strip and explicit drag disambiguation open. The text correctly limits the close-slot result to integer samples and the offscreen result to a fixed camera, and explicitly withholds App/native/keyboard/large-list acceptance. The passing filter is not described as a complete Scenario or desktop suite. A final portable package audit remains separate from these closed-run checks.
## Final portable package audit
Ran `harness/scenario-reorder-interaction-evidence-20260924/checks/verify.py` successfully: **3 runs, 0 recorded failures, 27 logical files, 23 stored ZIP members, 1 deduplicated source capsule pair and 3 source proofs**. Independently checked the exact ZIP inventory and every member hash, all 24 raw run/logical files byte-for-byte against `.local/scenario-group-gates`, and all external `files.json` pins. Archive `evidence.zip` is **3,244,466 bytes**, SHA256 **`b713a5c8c03f4cb4f4a6884bde7eb081fca00d1f5ebf85427ce14bc03acae166`**. No integrity mismatch found.
The checks README and manifest kind retain historical Scenario-selection helper naming. At audit time a root package index had not yet been written; parent was asked to explain those reused names and actual reorder run inventory there, preserving the pinned helper bytes. This is delivery clarity, not an evidence defect. The portable verifier checks historical proofs and archive consistency without re-executing source, binaries or Git comparisons. The tested source remains cae7a222; subsequent record edits are not relabelled as having executed those 22 tests. No native interaction or accepted chooser/handle is implied. This review is stable for delivery copy.
Final delivery clarification: read the newly written root evidence README. It now lists all three actual reorder runs, exact commands/environment, archive identity, reused historical names and the distinction between prior preparation notes and executed results. The delivery-clarity note above is resolved; no material overclaim found. File remains stable for copy.
@@ -1,42 +0,0 @@
# Scenario semantic destination reachability experiment
2026-09-24; production base 53817298 plus parent-owned test-only session and this test module, tested together at cae7a22297d09810c412dbdf4ce5d8a28164b247. Only `crates/oa-desktop/src/behavior/scenario/reorder_slot_experiments.rs` and this local note are owned by this subagent. No production input change, Cargo, GUI or publication performed by this subagent. Parent ran the captured test gate; executed findings and independently inspected records are appended below. The four tests remain frozen without fixture correction.
## Question, method and literal expectations
Does the existing nearest-Y picker give every exact same-parent semantic destination a usable pointer target when Saved geometry differs from semantic order? The test module uses actual `presentation::present`, `Scene::slot_at`, `group_reorder::Prepared` and core command execution. Every fixture must be core error-free and actual requested Saved presentation `Ready`. Four independent asynchronous messages have semantic order `[A,B,C,D]`. Expected candidate Projects are constructed by changing only the literal parent step vector in a clone, then compared in full; format/parse is also checked. The fixture is deliberately uncomplicated: it isolates destination identity, not lifecycle admission or user comprehension.
| Case | Authored send Y | Literal expectation |
| --- | --- | --- |
| Crossed | 200,500,300,400 | Before-slot Y 158,458,258,358; physical order A,C,D,B; each exact anchor still reachable at its own Y. C-then-A selection before D gives `[B,A,C,D]`. |
| Equal | 200,300,300,500 | B/C share slot Y258. B precedes C in the slot vector and wins every tie. Exact chooser before C gives `[B,A,D,C]` for D-then-A selection; nearest at Y258 chooses before B and gives `[A,D,B,C]`. |
| Close | 200,300,300.125,300.25 | C slot258.125 is continuously reachable, but its open nearest-Y interval `(258.0625,258.1875)` contains no integer pixel at scale1/pan0. |
| Offscreen | 200,300,400,2000 | D slot1958 is reachable there; its boundary against C is Y1158, outside a fixed world viewport Y0..600. Exact chooser before D remains valid. |
The equal case has a mathematical identity argument in addition to a sampled execution check: two slots with equal Y have equal distance for every finite pointer Y, and `Iterator::min_by` keeps the earlier equal minimum. Sampling is not represented as exhaustive proof. Close and offscreen cases are deliberately weaker: there is no claim that Windows quantizes all input to integer pixels, or that a destination remains offscreen after pan/zoom. A fixed-camera handle with camera-change cancellation cannot silently rely on pan to make that case work.
Source anchors: `behavior.rs:1621-1632` seeds before-slots at each resolved anchor Y minus42; `behavior.rs:1855-1860` minimizes distance before filtering parent, ignores X, and has no tie disambiguator; `behavior.rs:2179-2180` maps screen coordinates using pan/scale. The same-parent root fixture intentionally avoids wrong-parent nearest-slot filtering. Existing policy still requires the drag to expose that refusal instead of searching farther.
## Design implication, conditional on the gate
An unchanged nearest-Y handle cannot establish equivalent destination coverage to an exact Slot chooser if the equal case passes its actual Ready assertion. A better hover label does not create C's missing hit region. Candidate remedies are a temporary semantic-order destination strip, an explicit tie chooser, or keyboard cycling through exact slots. Each adds a nonspatial identity mechanism; do not pretend a source-order tie-break alone makes both tied anchors reachable. Crossed coordinates alone do not falsify the handle: they test user comprehension. Close coordinates establish target-size sensitivity, not total continuous unreachability. Offscreen coordinates establish the need for a deliberate camera or alternate destination policy.
## Minimal shared operation boundary
The parent-owned `scenario/reorder_session.rs` is consistent with the useful minimum: immutable `Arc<Project>`, exact full Scenario `Context`, complete `Snapshot`, fresh nonnil operation token; one admitted `Prepared`; no initially chosen destination; a single cached current `Preview{slot,before,after,outcome,presentation,private command}`; consuming finish returns the cached command plus selection/requested representation. Do not store a scene for every sibling or every pointer sample. Do not add a global gesture abstraction for this experiment.
Both inputs should feed the same exact `preview(Slot)`. The chooser supplies identity directly. A drag first resolves the actual global nearest displayed slot, then submits that exact identity to the planner; `None` from parent filtering is refusal, never a redirect to the closest legal slot. Repeated current Slot should reuse the candidate. Candidate `Ready`, `Fallback` and `Unavailable` remain separate from semantic Change/Noop/Refused, and requested representation stays Saved even when actual display falls back. Baseline fallback admission is a separate decision: a chooser may use identities visible in the fallback scene, but coordinate input cannot gain permission just because the semantic planner is valid.
The pure session cannot own the whole input lifetime. The App must compare full context/snapshot/token before taking or mutating the active session. Cancel matches the current token; old Cancel must not discard a newer operation. Every admitted terminal event, including no-op, refusal, failed begin and failed source commit, retires the operation generation so a replayed Begin cannot reopen it on an identical selection. A pointer adapter additionally freezes camera/bounds and defines threshold/outside release/out-and-back; a stationary press/release on a fixed handle must not accidentally select its incidental nearest slot. A chooser can permit passive scrolling only if exact destination identity is stable. Source/Properties dirty state, draft insertion, menu capture, ownership and document-instance/representation-visit ABA admission remain App responsibilities.
No universal commit policy for candidate Fallback/Unavailable is inferred from a returned valid command. The prototype must show that status and make the product decision explicit before enabling a user path. No GUI/accessibility/comprehension claim follows from these source-level tests. Existing selected body/header group spacing retains its meaning, aggregate removal remains separate, and issue29 is incomplete.
## Executed findings and negative design decision
Read back `.local/scenario-group-gates/reorder-interaction-test-1/result.json`, `source-proof.json` and the executed test names/result in `stdout.log`. The parent-run command was `cargo test -p oa-desktop reorder_ --message-format=json -- --nocapture`, with exit0, unchanged inputs, and **22 passed / 0 failed / 0 ignored** at commit **cae7a22297d09810c412dbdf4ce5d8a28164b247**. The source-proof record reports all **814 captured inputs** matching that commit's exact Git blob bytes. This subagent inspected those records; it did not rerun Cargo or the source-proof helper.
All four named slot experiments and all five new session tests executed successfully. The unmodified equal/coincident fixture passed both core admission and actual requested Saved `Ready` presentation. Thus the missing BeforeC target is a real production geometry/picker combination, not an invalid-coordinate or fallback artifact. The full-model chooser BeforeC and nearest BeforeB changes passed their distinct literal expected orders. Crossed slot ordering, close continuous reachability versus integer-pixel samples, and the explicitly fixed-viewport offscreen result also matched the table above. No fixture corrections were needed.
**Reject an unmodified nearest-Y fixed handle as the sole exact-destination method.** In the admitted equal-Y case, BeforeB and BeforeC have identical distance functions; the current stable tie winner makes BeforeC unreachable. Reversing that winner would merely make BeforeB unreachable instead. Pan, uniform zoom, a clearer hover label, or a different fixed tie-break cannot distinguish identical coordinates. Recovering both choices requires explicit identity disambiguation, a semantic destination strip with distinct targets, or another mechanism that introduces a choice beyond nearest original Y. This rejects equivalence of that particular sole-input method, not every possible canvas drag design.
An exact identity chooser remains the recommended next comparator, **not accepted UX**. These tests do not establish discoverability, direct graphical editing quality, keyboard behavior, result comprehension, nested/offscreen navigation policy, full App lifetime admission or native usability. They also do not claim every root append slot or nested wrong-parent case was covered. The bounded reachability question is resolved without broadening tests; the shared session and a concrete chooser/strip prototype still require their own integration and user-workflow evaluation.
-51
View File
@@ -1,51 +0,0 @@
# Scenario selection adapter
Branch-specific continuation of [the Scenario group planner](scenario-group-planner-20260924.md), tracked in [Gitea #29](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/29). This work is outside the frozen #37 prerelease candidate. It does not complete Scenario group movement or aggregate removal.
## Behavior and boundaries
Scenario selection uses exact rendered targets and the shared insertion-ordered selection with an explicit primary. Its envelope additionally includes the complete Scenario representation and visit, so saved A, saved B, and Automatic remain independent input destinations. Shift-click toggles a member; a stationary plain click on a selected group member selects only that member. A background press clears selection and retains ordinary panning. Contextual actions expose Add, Remove from selection, Inspect, and Select only. The normal workspace shows the selected count and identifies the primary whose properties are inspected.
Selecting a group does not grant permission to apply a singleton operation to its primary. Motion, deletion and index arrows refuse the whole group while the semantic operation adapter is unfinished. Ordinary first-press singleton saved spacing remains available, including a press on a message different from the current singleton. Automatic singleton ordering remains on its existing path. Selection alone does not edit model/source or history; pending edits are not silently applied.
The source boundary preserves existing saved body/header spacing. The open group gesture decision and semantic destination preview are recorded in [the group policy](scenario-group-reorder-policy-20260924.md); this increment does not resolve them by reinterpreting a saved body drag.
## Review and experiments
The first adapter source was `879eb1d5c8792a720433022e49f415d2c58ff72c`. Its Windows Scenario run executed 263 passing tests, one failing test, and one ignored test. All 809 captured inputs remained unchanged and matched that Git commit. The failure was the older mounted deletion test's expectation that a menu snapshot remained valid after a Scenario owner change. The new destination check correctly invalidated it. The corrected test first asserts that real invalidation, then uses a deliberately current unqualified snapshot to isolate the independent captured-owner guard; it retains actual removal, source consistency, and Undo assertions.
Independent review also identified two production issues: selection Clear advanced the camera lifetime and interrupted the pan started by the same blank press; legacy singleton selection reconciliation could change generation without retiring an old spacing Begin. The correction retires spacing through the canvas context on all admitted selection changes, while keeping the camera lifetime during selection-only input. A legacy A/B/A evaluator covers both generic selection and Scenario canvas selection. Mounted tests cover same-press blank panning with singleton/group selection and batched/split events.
Additional mounted evaluators use the actual Properties Name input to test passive Shift across focus, busy rebuilding, release and window loss. A saved spacing evaluator begins with an unrelated singleton, requires Begin on the first press without a replacement-selection packet, checks the complete expected project after the drag, and verifies one Undo/Redo. These are production widget-tree tests; busy rebuilding is not evidence of a child-state remount.
The next run at `a0a70340a625c16b1249d0beb71dca4fdd206636` passed 266 tests and failed two, with one ignored. All new mounted tests and the deletion fixture passed. One old direct reorder assertion still expected selection to preserve its gesture context; the test now requires a new epoch with the same view visit and captures a fresh reorder. The other failure exposed a real regression: unconditional retirement during reconciliation invalidated a newly scheduled reveal. Retirement now occurs at the App action boundary only when its complete context is unchanged and its selection snapshot changed. Navigation that already advanced the context retains its newly scheduled reveal. Independent review was updated to acknowledge the missed reveal path.
Run `selection-scenario-3` was intentionally interrupted because the parent started it before the formatter/commit command finished. `app.rs` changed after input capture; its closed result records exit 1 and `inputsUnchanged: false`, with no executable or executed tests. It is preserved in [the invalid-attempt archive](scenario-selection-evidence-20260924/invalid-attempt-3/manifest.json), excluded from accepted checks, and has no source-proof claim.
Corrective source `ca1f18e6b0d53814f0ff16261f810aef49281c26`: `selection-scenario-4` passed 268 tests, zero failed, one ignored; the complete menu filter passed 14 tests with none failed or ignored. Formatting passed. Each closed run retained unchanged 809-input captures, with an independent comparison to the exact Git blobs.
Strict lint then found a redundant `expect` following an `is_some` check. Final source `ed55b1781d86ec49af5d2588cbd06f7b878d9009` binds the same captured Context/Snapshot directly with `if let`. Independent review found no behavior change in either group press or background Clear. Strict lint, formatting, executable build and the final-source regression repeat all pass with exact input proofs.
UTC changed discontinuously during run 4, also observed by both prerelease peers. The raw timestamps are retained verbatim; its separately measured monotonic duration is 515.96 seconds. Do not calculate build duration or infer inactivity from the wall-clock difference. Source/stream/executable pins and process completion are independent of that clock change.
## Final-source verification
Windows toolchain: Cargo/Rust 1.98.1, `stable-x86_64-pc-windows-gnullvm`, LLVM-MinGW 20260826, clang 23.1; `CARGO_BUILD_JOBS=1`, `CARGO_INCREMENTAL=0`. Linux builds/checks remain excluded by the user's current prerelease instruction.
| Command | Receipt | Result |
| --- | --- | --- |
| `cargo clippy -p oa-desktop --all-targets --message-format=json -- -D warnings` | selection-lint-2 | Pass |
| `cargo fmt --all -- --check` | selection-format-3 | Pass |
| `cargo build -p oa-desktop --bin arcadia-studio --message-format=json` | selection-build-1 | Pass |
| `cargo test -p oa-desktop --bin arcadia-studio --message-format=json scenario -- --test-threads=1` | selection-scenario-5 | 268 passed, 0 failed, 1 ignored |
| `cargo test -p oa-desktop --bin arcadia-studio --message-format=json model_menu -- --test-threads=1` | selection-menu-2 | 14 passed, 0 failed, 0 ignored |
Filters overlap and must not be summed as unique tests. The ignored Scenario timing test is not a pass. These are scoped desktop checks, not a full workspace gate. Each final receipt captured unchanged inputs and was independently matched to all 809 exact Git-visible input blobs at `ed55b178`.
[Portable checks](scenario-selection-evidence-20260924/checks/README.md) contain 12 closed runs, three preserved ordinary failures, 12 source proofs and four distinct input capsules. The invalid changed-input attempt remains separate. Checks ZIP: 12,926,901 bytes, SHA256 `05d040e856d0b37adf44f6a9e3d2789d4d76c026e61e85fdfcab0e7bebca4dd4`. Run `python -B harness/scenario-selection-evidence-20260924/checks/verify.py`. The packaging self-check also passed archive equality, existing-output refusal and 11 corruption rejection cases; its local command output is not an archived raw gate. [Independent final audit](scenario-selection-evidence-20260924/reviews/final-checks.md) repeated all four input/Git comparisons and raw archive comparisons.
[Native Windows exercise](scenario-selection-evidence-20260924/native/README.md) records contextual group selection, primary inspection, whole-group refusal, same-press background clear/pan, stationary singleton collapse, first-press singleton spacing, exact saved-source comparisons and one Undo. Selection-only Save all preserves exact source bytes. The spacing edit changes only the expected saved A ordinate; Undo restores the entire original file. Representation navigation and a clean exit were also observed. Native Shift and other unexercised paths are explicitly separated from mounted test evidence. Run `python -B harness/scenario-selection-evidence-20260924/native/verify.py` for archived byte and source-delta checks.
The [independent native evidence review](scenario-selection-evidence-20260924/reviews/native-checks.md) verified all 99 original archive members, linked the build to the accepted Cargo package, compared the executable and fixture, repeated the full saved-file comparisons, and inspected the pan/collapse/spacing screenshot pairs. It found no material issue within this scope.
This delivers the selection adapter increment for review on its contribution branch. It remains outside frozen #37, does not complete #29, and does not claim integrated or published-release acceptance. Group movement/removal and their user-facing destination design remain next work.
@@ -1,14 +0,0 @@
# Scenario selection captured checks
These tools package explicitly named, completed runs from `CHECKOUT/.local/scenario-group-gates` without executing archived source or running Cargo. Adapted only in input-directory and manifest-kind names from the published prerelease Connection/Route evidence tools at `93d2a6f9af40061bf92cd81ba33d5648d0181cb1`.
```powershell
python -B prepare.py --root CHECKOUT --runs selection-scenario-1 selection-scenario-2 selection-format-1 --output NEW_DIRECTORY
python -B NEW_DIRECTORY/verify.py
```
The manifest is authoritative for the selected runs. Failed runs remain failures: archive consistency is not a test pass. Every raw stream and receipt retains its original bytes. Identical input maps and ZIPs are stored by hash; the logical-file map preserves their original run paths. Historical capture helpers and exact source comparators are included as data, never executed by verification.
Source proofs compare every captured input with a named Git commit; the offline package verifier checks those receipts and their pinned comparator identity but does not repeat the Git comparison. The captured HEAD and the named proof commit retain their respective meanings. The verifier checks archive inventory, hashes, input ZIP contents, process/command/result consistency and executable artifact relationships. It does not establish hermetic builds, native workflow acceptance, UI integration, or release acceptance.
Use `--check-only` instead of `--output` for an in-memory consistency check. Existing output directories refuse. Python 3.11+ standard library is sufficient; `-B` avoids bytecode writes.
@@ -1,22 +0,0 @@
{
"prepare.py": {
"bytes": 6308,
"sha256": "2d500f5d43a49893abb3936f880e61b8adc623531e26f0288983502e386894db"
},
"verify.py": {
"bytes": 11436,
"sha256": "ea8e781003bec21f2ac4af23e21b68ca5639a98eb31505c10b2869f57565e917"
},
"README.md": {
"bytes": 1641,
"sha256": "4c5c582cbdf02c8704b9add656d273c275b10eed87219131e3bba639a4c4bd8a"
},
"manifest.json": {
"bytes": 32714,
"sha256": "113c6c4275c8bccb69086bb52b6ce62b8cc2eb743be9febd107b425c51933429"
},
"evidence.zip": {
"bytes": 12926901,
"sha256": "05d040e856d0b37adf44f6a9e3d2789d4d76c026e61e85fdfcab0e7bebca4dd4"
}
}
@@ -1,737 +0,0 @@
{
"schema": 1,
"kind": "scenario-selection-cargo-gates",
"runs": {
"selection-scenario-1": {
"command": [
"cargo",
"test",
"-p",
"oa-desktop",
"--bin",
"arcadia-studio",
"--message-format=json",
"scenario",
"--",
"--test-threads=1"
],
"head": "879eb1d5c8792a720433022e49f415d2c58ff72c",
"exitCode": 101,
"inputCount": 809,
"helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"sourceProof": {
"commit": "879eb1d5c8792a720433022e49f415d2c58ff72c",
"pin": {
"bytes": 599,
"sha256": "6dd4a2e751a46252063e0cd61fb1a23dbd6aa0639519e19a0e7e2f73b781714d"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"selection-scenario-2": {
"command": [
"cargo",
"test",
"-p",
"oa-desktop",
"--bin",
"arcadia-studio",
"--message-format=json",
"scenario",
"--",
"--test-threads=1"
],
"head": "a0a70340a625c16b1249d0beb71dca4fdd206636",
"exitCode": 101,
"inputCount": 809,
"helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"sourceProof": {
"commit": "a0a70340a625c16b1249d0beb71dca4fdd206636",
"pin": {
"bytes": 599,
"sha256": "164bc88b11dda75d38d4b61f44c8a03089df73dbbf031905bc2cccbf0997ad61"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"selection-format-1": {
"command": [
"cargo",
"fmt",
"--all",
"--",
"--check"
],
"head": "a0a70340a625c16b1249d0beb71dca4fdd206636",
"exitCode": 0,
"inputCount": 809,
"helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"sourceProof": {
"commit": "a0a70340a625c16b1249d0beb71dca4fdd206636",
"pin": {
"bytes": 597,
"sha256": "c2d44e0b2ec9d93676f270404c3bc814bbc006aacb4df53f248fe9e01635c055"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"selection-format-2": {
"command": [
"cargo",
"fmt",
"--all",
"--",
"--check"
],
"head": "ca1f18e6b0d53814f0ff16261f810aef49281c26",
"exitCode": 0,
"inputCount": 809,
"helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"sourceProof": {
"commit": "ca1f18e6b0d53814f0ff16261f810aef49281c26",
"pin": {
"bytes": 597,
"sha256": "3add2c994cf44be9756c6843214104c3ae9fd3092d031be816013dfba5b43401"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"selection-scenario-4": {
"command": [
"cargo",
"test",
"-p",
"oa-desktop",
"--bin",
"arcadia-studio",
"--message-format=json",
"scenario",
"--",
"--test-threads=1"
],
"head": "ca1f18e6b0d53814f0ff16261f810aef49281c26",
"exitCode": 0,
"inputCount": 809,
"helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"sourceProof": {
"commit": "ca1f18e6b0d53814f0ff16261f810aef49281c26",
"pin": {
"bytes": 599,
"sha256": "a524249d9fa4ba6b29086e103780c03e345850b3cd390d92ec924ac6e5899b40"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"selection-menu-1": {
"command": [
"cargo",
"test",
"-p",
"oa-desktop",
"--bin",
"arcadia-studio",
"--message-format=json",
"model_menu",
"--",
"--test-threads=1"
],
"head": "ca1f18e6b0d53814f0ff16261f810aef49281c26",
"exitCode": 0,
"inputCount": 809,
"helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"sourceProof": {
"commit": "ca1f18e6b0d53814f0ff16261f810aef49281c26",
"pin": {
"bytes": 595,
"sha256": "978da6f95bf74ce0fa7ac871ec582a7312ca87576d7f39f4b5ebec71fa96e9fd"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"selection-lint-1": {
"command": [
"cargo",
"clippy",
"-p",
"oa-desktop",
"--all-targets",
"--message-format=json",
"--",
"-D",
"warnings"
],
"head": "ca1f18e6b0d53814f0ff16261f810aef49281c26",
"exitCode": 101,
"inputCount": 809,
"helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"sourceProof": {
"commit": "ca1f18e6b0d53814f0ff16261f810aef49281c26",
"pin": {
"bytes": 595,
"sha256": "386bc87e04ab7eea93781fbe41a9366843125fc1d08f6e967ad98b9c88b58651"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"selection-lint-2": {
"command": [
"cargo",
"clippy",
"-p",
"oa-desktop",
"--all-targets",
"--message-format=json",
"--",
"-D",
"warnings"
],
"head": "ed55b1781d86ec49af5d2588cbd06f7b878d9009",
"exitCode": 0,
"inputCount": 809,
"helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"sourceProof": {
"commit": "ed55b1781d86ec49af5d2588cbd06f7b878d9009",
"pin": {
"bytes": 595,
"sha256": "616d9d9f6c7a49220c6cda6497ca30e78f309b753745853e7b43a5ab31dbee47"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"selection-format-3": {
"command": [
"cargo",
"fmt",
"--all",
"--",
"--check"
],
"head": "ed55b1781d86ec49af5d2588cbd06f7b878d9009",
"exitCode": 0,
"inputCount": 809,
"helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"sourceProof": {
"commit": "ed55b1781d86ec49af5d2588cbd06f7b878d9009",
"pin": {
"bytes": 597,
"sha256": "a867e8f941b4dc5c4651f571a10f341134775c1184b6b2e2cb8c1f8ad9ba410a"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"selection-build-1": {
"command": [
"cargo",
"build",
"-p",
"oa-desktop",
"--bin",
"arcadia-studio",
"--message-format=json"
],
"head": "ed55b1781d86ec49af5d2588cbd06f7b878d9009",
"exitCode": 0,
"inputCount": 809,
"helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"sourceProof": {
"commit": "ed55b1781d86ec49af5d2588cbd06f7b878d9009",
"pin": {
"bytes": 596,
"sha256": "657eb7e9a252e29764eda37557b81aed551750eaada46018b5f9f097a85cf780"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"selection-scenario-5": {
"command": [
"cargo",
"test",
"-p",
"oa-desktop",
"--bin",
"arcadia-studio",
"--message-format=json",
"scenario",
"--",
"--test-threads=1"
],
"head": "ed55b1781d86ec49af5d2588cbd06f7b878d9009",
"exitCode": 0,
"inputCount": 809,
"helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"sourceProof": {
"commit": "ed55b1781d86ec49af5d2588cbd06f7b878d9009",
"pin": {
"bytes": 599,
"sha256": "5382c0455497058de4dbac8c263c1a23976bdef04d5ad165882d47c4420582fd"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
},
"selection-menu-2": {
"command": [
"cargo",
"test",
"-p",
"oa-desktop",
"--bin",
"arcadia-studio",
"--message-format=json",
"model_menu",
"--",
"--test-threads=1"
],
"head": "ed55b1781d86ec49af5d2588cbd06f7b878d9009",
"exitCode": 0,
"inputCount": 809,
"helper": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"sourceProof": {
"commit": "ed55b1781d86ec49af5d2588cbd06f7b878d9009",
"pin": {
"bytes": 595,
"sha256": "3f8b03bc437717f8b5f185483e83dac4eca29dc9f344a31c7ee30fd9a32ba807"
},
"comparator": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py"
}
}
},
"captureHelpers": {
"gate.py": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"source-proof.py": "helpers/c86f51d9b8465341dab73d045923ec306b15ebabf7c97b81beac7afe73bd72a1.py"
},
"logicalFiles": {
"cargo/selection-build-1/inputs.json": "capsules/995ccb10de056f3b19370da8b47bc176de9fcc5fdddc5e292fd0c31252776aff.json",
"cargo/selection-build-1/inputs.zip": "capsules/405dd5aca8f8137a130af8b4e35c88b560e6b0f90637befca8c3483514276377.zip",
"cargo/selection-build-1/process.json": "cargo/selection-build-1/process.json",
"cargo/selection-build-1/result.json": "cargo/selection-build-1/result.json",
"cargo/selection-build-1/source-proof.json": "cargo/selection-build-1/source-proof.json",
"cargo/selection-build-1/started.json": "cargo/selection-build-1/started.json",
"cargo/selection-build-1/stderr.log": "cargo/selection-build-1/stderr.log",
"cargo/selection-build-1/stdout.log": "cargo/selection-build-1/stdout.log",
"cargo/selection-format-1/inputs.json": "capsules/36a101249a3038caf64293c4339bc2ac356e6730c8c57bd7b755eff8dbd43865.json",
"cargo/selection-format-1/inputs.zip": "capsules/0ddad5faad0d547e591142b95d4cd94064e621ce648f97cceedc5f27aa2a4275.zip",
"cargo/selection-format-1/process.json": "cargo/selection-format-1/process.json",
"cargo/selection-format-1/result.json": "cargo/selection-format-1/result.json",
"cargo/selection-format-1/source-proof.json": "cargo/selection-format-1/source-proof.json",
"cargo/selection-format-1/started.json": "cargo/selection-format-1/started.json",
"cargo/selection-format-1/stderr.log": "cargo/selection-format-1/stderr.log",
"cargo/selection-format-1/stdout.log": "cargo/selection-format-1/stdout.log",
"cargo/selection-format-2/inputs.json": "capsules/33112027b1b34993570bb3d35d44fa53fc82249f5a711a93f3ef1f6cc52f1cfd.json",
"cargo/selection-format-2/inputs.zip": "capsules/70cd4a304c5f314225283e548d884edc584ebde476c451948c4e230ee65a6a24.zip",
"cargo/selection-format-2/process.json": "cargo/selection-format-2/process.json",
"cargo/selection-format-2/result.json": "cargo/selection-format-2/result.json",
"cargo/selection-format-2/source-proof.json": "cargo/selection-format-2/source-proof.json",
"cargo/selection-format-2/started.json": "cargo/selection-format-2/started.json",
"cargo/selection-format-2/stderr.log": "cargo/selection-format-2/stderr.log",
"cargo/selection-format-2/stdout.log": "cargo/selection-format-2/stdout.log",
"cargo/selection-format-3/inputs.json": "capsules/995ccb10de056f3b19370da8b47bc176de9fcc5fdddc5e292fd0c31252776aff.json",
"cargo/selection-format-3/inputs.zip": "capsules/405dd5aca8f8137a130af8b4e35c88b560e6b0f90637befca8c3483514276377.zip",
"cargo/selection-format-3/process.json": "cargo/selection-format-3/process.json",
"cargo/selection-format-3/result.json": "cargo/selection-format-3/result.json",
"cargo/selection-format-3/source-proof.json": "cargo/selection-format-3/source-proof.json",
"cargo/selection-format-3/started.json": "cargo/selection-format-3/started.json",
"cargo/selection-format-3/stderr.log": "cargo/selection-format-3/stderr.log",
"cargo/selection-format-3/stdout.log": "cargo/selection-format-3/stdout.log",
"cargo/selection-lint-1/inputs.json": "capsules/33112027b1b34993570bb3d35d44fa53fc82249f5a711a93f3ef1f6cc52f1cfd.json",
"cargo/selection-lint-1/inputs.zip": "capsules/70cd4a304c5f314225283e548d884edc584ebde476c451948c4e230ee65a6a24.zip",
"cargo/selection-lint-1/process.json": "cargo/selection-lint-1/process.json",
"cargo/selection-lint-1/result.json": "cargo/selection-lint-1/result.json",
"cargo/selection-lint-1/source-proof.json": "cargo/selection-lint-1/source-proof.json",
"cargo/selection-lint-1/started.json": "cargo/selection-lint-1/started.json",
"cargo/selection-lint-1/stderr.log": "cargo/selection-lint-1/stderr.log",
"cargo/selection-lint-1/stdout.log": "cargo/selection-lint-1/stdout.log",
"cargo/selection-lint-2/inputs.json": "capsules/995ccb10de056f3b19370da8b47bc176de9fcc5fdddc5e292fd0c31252776aff.json",
"cargo/selection-lint-2/inputs.zip": "capsules/405dd5aca8f8137a130af8b4e35c88b560e6b0f90637befca8c3483514276377.zip",
"cargo/selection-lint-2/process.json": "cargo/selection-lint-2/process.json",
"cargo/selection-lint-2/result.json": "cargo/selection-lint-2/result.json",
"cargo/selection-lint-2/source-proof.json": "cargo/selection-lint-2/source-proof.json",
"cargo/selection-lint-2/started.json": "cargo/selection-lint-2/started.json",
"cargo/selection-lint-2/stderr.log": "cargo/selection-lint-2/stderr.log",
"cargo/selection-lint-2/stdout.log": "cargo/selection-lint-2/stdout.log",
"cargo/selection-menu-1/inputs.json": "capsules/33112027b1b34993570bb3d35d44fa53fc82249f5a711a93f3ef1f6cc52f1cfd.json",
"cargo/selection-menu-1/inputs.zip": "capsules/70cd4a304c5f314225283e548d884edc584ebde476c451948c4e230ee65a6a24.zip",
"cargo/selection-menu-1/process.json": "cargo/selection-menu-1/process.json",
"cargo/selection-menu-1/result.json": "cargo/selection-menu-1/result.json",
"cargo/selection-menu-1/source-proof.json": "cargo/selection-menu-1/source-proof.json",
"cargo/selection-menu-1/started.json": "cargo/selection-menu-1/started.json",
"cargo/selection-menu-1/stderr.log": "cargo/selection-menu-1/stderr.log",
"cargo/selection-menu-1/stdout.log": "cargo/selection-menu-1/stdout.log",
"cargo/selection-menu-2/inputs.json": "capsules/995ccb10de056f3b19370da8b47bc176de9fcc5fdddc5e292fd0c31252776aff.json",
"cargo/selection-menu-2/inputs.zip": "capsules/405dd5aca8f8137a130af8b4e35c88b560e6b0f90637befca8c3483514276377.zip",
"cargo/selection-menu-2/process.json": "cargo/selection-menu-2/process.json",
"cargo/selection-menu-2/result.json": "cargo/selection-menu-2/result.json",
"cargo/selection-menu-2/source-proof.json": "cargo/selection-menu-2/source-proof.json",
"cargo/selection-menu-2/started.json": "cargo/selection-menu-2/started.json",
"cargo/selection-menu-2/stderr.log": "cargo/selection-menu-2/stderr.log",
"cargo/selection-menu-2/stdout.log": "cargo/selection-menu-2/stdout.log",
"cargo/selection-scenario-1/inputs.json": "capsules/e9a4588279a66a16ef89136867a2517b279a6f66ea08fedd66322c733d4b6396.json",
"cargo/selection-scenario-1/inputs.zip": "capsules/cc0e1425e15adacc48cf167bbada1eb34ebb651228b0429331d59ab7d3ba05fa.zip",
"cargo/selection-scenario-1/process.json": "cargo/selection-scenario-1/process.json",
"cargo/selection-scenario-1/result.json": "cargo/selection-scenario-1/result.json",
"cargo/selection-scenario-1/source-proof.json": "cargo/selection-scenario-1/source-proof.json",
"cargo/selection-scenario-1/started.json": "cargo/selection-scenario-1/started.json",
"cargo/selection-scenario-1/stderr.log": "cargo/selection-scenario-1/stderr.log",
"cargo/selection-scenario-1/stdout.log": "cargo/selection-scenario-1/stdout.log",
"cargo/selection-scenario-2/inputs.json": "capsules/36a101249a3038caf64293c4339bc2ac356e6730c8c57bd7b755eff8dbd43865.json",
"cargo/selection-scenario-2/inputs.zip": "capsules/0ddad5faad0d547e591142b95d4cd94064e621ce648f97cceedc5f27aa2a4275.zip",
"cargo/selection-scenario-2/process.json": "cargo/selection-scenario-2/process.json",
"cargo/selection-scenario-2/result.json": "cargo/selection-scenario-2/result.json",
"cargo/selection-scenario-2/source-proof.json": "cargo/selection-scenario-2/source-proof.json",
"cargo/selection-scenario-2/started.json": "cargo/selection-scenario-2/started.json",
"cargo/selection-scenario-2/stderr.log": "cargo/selection-scenario-2/stderr.log",
"cargo/selection-scenario-2/stdout.log": "cargo/selection-scenario-2/stdout.log",
"cargo/selection-scenario-4/inputs.json": "capsules/33112027b1b34993570bb3d35d44fa53fc82249f5a711a93f3ef1f6cc52f1cfd.json",
"cargo/selection-scenario-4/inputs.zip": "capsules/70cd4a304c5f314225283e548d884edc584ebde476c451948c4e230ee65a6a24.zip",
"cargo/selection-scenario-4/process.json": "cargo/selection-scenario-4/process.json",
"cargo/selection-scenario-4/result.json": "cargo/selection-scenario-4/result.json",
"cargo/selection-scenario-4/source-proof.json": "cargo/selection-scenario-4/source-proof.json",
"cargo/selection-scenario-4/started.json": "cargo/selection-scenario-4/started.json",
"cargo/selection-scenario-4/stderr.log": "cargo/selection-scenario-4/stderr.log",
"cargo/selection-scenario-4/stdout.log": "cargo/selection-scenario-4/stdout.log",
"cargo/selection-scenario-5/inputs.json": "capsules/995ccb10de056f3b19370da8b47bc176de9fcc5fdddc5e292fd0c31252776aff.json",
"cargo/selection-scenario-5/inputs.zip": "capsules/405dd5aca8f8137a130af8b4e35c88b560e6b0f90637befca8c3483514276377.zip",
"cargo/selection-scenario-5/process.json": "cargo/selection-scenario-5/process.json",
"cargo/selection-scenario-5/result.json": "cargo/selection-scenario-5/result.json",
"cargo/selection-scenario-5/source-proof.json": "cargo/selection-scenario-5/source-proof.json",
"cargo/selection-scenario-5/started.json": "cargo/selection-scenario-5/started.json",
"cargo/selection-scenario-5/stderr.log": "cargo/selection-scenario-5/stderr.log",
"cargo/selection-scenario-5/stdout.log": "cargo/selection-scenario-5/stdout.log",
"comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py": "comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py",
"helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py": "helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py",
"helpers/c86f51d9b8465341dab73d045923ec306b15ebabf7c97b81beac7afe73bd72a1.py": "helpers/c86f51d9b8465341dab73d045923ec306b15ebabf7c97b81beac7afe73bd72a1.py"
},
"members": {
"capsules/0ddad5faad0d547e591142b95d4cd94064e621ce648f97cceedc5f27aa2a4275.zip": {
"bytes": 3234298,
"sha256": "0ddad5faad0d547e591142b95d4cd94064e621ce648f97cceedc5f27aa2a4275"
},
"capsules/33112027b1b34993570bb3d35d44fa53fc82249f5a711a93f3ef1f6cc52f1cfd.json": {
"bytes": 135839,
"sha256": "33112027b1b34993570bb3d35d44fa53fc82249f5a711a93f3ef1f6cc52f1cfd"
},
"capsules/36a101249a3038caf64293c4339bc2ac356e6730c8c57bd7b755eff8dbd43865.json": {
"bytes": 135839,
"sha256": "36a101249a3038caf64293c4339bc2ac356e6730c8c57bd7b755eff8dbd43865"
},
"capsules/405dd5aca8f8137a130af8b4e35c88b560e6b0f90637befca8c3483514276377.zip": {
"bytes": 3234337,
"sha256": "405dd5aca8f8137a130af8b4e35c88b560e6b0f90637befca8c3483514276377"
},
"capsules/70cd4a304c5f314225283e548d884edc584ebde476c451948c4e230ee65a6a24.zip": {
"bytes": 3234365,
"sha256": "70cd4a304c5f314225283e548d884edc584ebde476c451948c4e230ee65a6a24"
},
"capsules/995ccb10de056f3b19370da8b47bc176de9fcc5fdddc5e292fd0c31252776aff.json": {
"bytes": 135839,
"sha256": "995ccb10de056f3b19370da8b47bc176de9fcc5fdddc5e292fd0c31252776aff"
},
"capsules/cc0e1425e15adacc48cf167bbada1eb34ebb651228b0429331d59ab7d3ba05fa.zip": {
"bytes": 3231538,
"sha256": "cc0e1425e15adacc48cf167bbada1eb34ebb651228b0429331d59ab7d3ba05fa"
},
"capsules/e9a4588279a66a16ef89136867a2517b279a6f66ea08fedd66322c733d4b6396.json": {
"bytes": 135839,
"sha256": "e9a4588279a66a16ef89136867a2517b279a6f66ea08fedd66322c733d4b6396"
},
"cargo/selection-build-1/process.json": {
"bytes": 164,
"sha256": "5b75f4cf36cadf630959bfcee18e340f337217a371f67fc9cd50fcaae55d62a1"
},
"cargo/selection-build-1/result.json": {
"bytes": 3219,
"sha256": "c7fe2098ad813e50f64f8f5ff45a7459859c74ff10fef68e8ef4cb151d5bfb7e"
},
"cargo/selection-build-1/source-proof.json": {
"bytes": 596,
"sha256": "657eb7e9a252e29764eda37557b81aed551750eaada46018b5f9f097a85cf780"
},
"cargo/selection-build-1/started.json": {
"bytes": 1538,
"sha256": "bc90bae8e10e0aeb2c23e4cf5c918f5b213be0c625ffe8da31884bd720ddca33"
},
"cargo/selection-build-1/stderr.log": {
"bytes": 310,
"sha256": "c5c4c5d1788772cb7b2d1a49370c7acef5996b3234b498cd9cb1a2bdfa1fec2f"
},
"cargo/selection-build-1/stdout.log": {
"bytes": 284131,
"sha256": "312000c0ce68949d4960dd9bb18454a4715fffe45cc2a3f5a5657910703504f7"
},
"cargo/selection-format-1/process.json": {
"bytes": 116,
"sha256": "2dd8cb1910f748c6723d23d5f50985efa6e5cc7e9cbf40e8251fde2b5af0558f"
},
"cargo/selection-format-1/result.json": {
"bytes": 1868,
"sha256": "423432a5475292ad7c7a90b5f72aa7963e3d4e2d7e3854e7c5f5d10e866fa100"
},
"cargo/selection-format-1/source-proof.json": {
"bytes": 597,
"sha256": "c2d44e0b2ec9d93676f270404c3bc814bbc006aacb4df53f248fe9e01635c055"
},
"cargo/selection-format-1/started.json": {
"bytes": 1480,
"sha256": "fa1bdc782ae26f6127b8662568c0eb6c7a1996add06943e2fd751717776ac628"
},
"cargo/selection-format-1/stderr.log": {
"bytes": 0,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"cargo/selection-format-1/stdout.log": {
"bytes": 0,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"cargo/selection-format-2/process.json": {
"bytes": 116,
"sha256": "d2c1cac86c9d5038ca50ed6edd0cc210b77baa869f87242fcca3c5a018e4cc9a"
},
"cargo/selection-format-2/result.json": {
"bytes": 1868,
"sha256": "ed855ccae891d7b0eaa9bb07cfec7183d0145f5618712d820cab7127bc8d6274"
},
"cargo/selection-format-2/source-proof.json": {
"bytes": 597,
"sha256": "3add2c994cf44be9756c6843214104c3ae9fd3092d031be816013dfba5b43401"
},
"cargo/selection-format-2/started.json": {
"bytes": 1480,
"sha256": "f32fdfd7d926eef08a7268663b099f7f917fb7c12cdc1e721245a44a37981ca3"
},
"cargo/selection-format-2/stderr.log": {
"bytes": 0,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"cargo/selection-format-2/stdout.log": {
"bytes": 0,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"cargo/selection-format-3/process.json": {
"bytes": 116,
"sha256": "6b80a98424fa35b2b38bc756318eaba3f54535fb3e56c3fbd140c555a6b785ac"
},
"cargo/selection-format-3/result.json": {
"bytes": 1868,
"sha256": "dd9b3129e1951e8b03c341f40f121b29f002c6cb776baf26808c64fe70f40c63"
},
"cargo/selection-format-3/source-proof.json": {
"bytes": 597,
"sha256": "a867e8f941b4dc5c4651f571a10f341134775c1184b6b2e2cb8c1f8ad9ba410a"
},
"cargo/selection-format-3/started.json": {
"bytes": 1480,
"sha256": "bb644f39da11e1f60172fb3c8c220ceba0a73a085faa9865fa1888225e3b9607"
},
"cargo/selection-format-3/stderr.log": {
"bytes": 0,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"cargo/selection-format-3/stdout.log": {
"bytes": 0,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"cargo/selection-lint-1/process.json": {
"bytes": 178,
"sha256": "465892514aaf5a0857c56a548e2c63836d399536eadac421f88d916882d14b9d"
},
"cargo/selection-lint-1/result.json": {
"bytes": 1960,
"sha256": "90374fd2ae51a586f9039626d15deefae3353adeefbb8920f6ef66b95ed6b86f"
},
"cargo/selection-lint-1/source-proof.json": {
"bytes": 595,
"sha256": "386bc87e04ab7eea93781fbe41a9366843125fc1d08f6e967ad98b9c88b58651"
},
"cargo/selection-lint-1/started.json": {
"bytes": 1563,
"sha256": "664f8e03c3aa0a086c6d9b0004f17a57f5b9d3355f1a34c4117313cb1ca58b65"
},
"cargo/selection-lint-1/stderr.log": {
"bytes": 163,
"sha256": "6cd1a10aa8ce9ee8a5ac74fd1dacd158709abeeb9483b49fc61c272157cbfc6d"
},
"cargo/selection-lint-1/stdout.log": {
"bytes": 270943,
"sha256": "9ab293656c1c7df03508a522240e050efd6d380ef37ff2f6404babe233c4902d"
},
"cargo/selection-lint-2/process.json": {
"bytes": 179,
"sha256": "a93b393302c10f24887d17084c32d71d1b5c30ef7777dd20466815b0f912dfef"
},
"cargo/selection-lint-2/result.json": {
"bytes": 1958,
"sha256": "9f7397469f4ac8287d3ea10b418a73020eef350b670c4645c116246af2b779f3"
},
"cargo/selection-lint-2/source-proof.json": {
"bytes": 595,
"sha256": "616d9d9f6c7a49220c6cda6497ca30e78f309b753745853e7b43a5ab31dbee47"
},
"cargo/selection-lint-2/started.json": {
"bytes": 1563,
"sha256": "16ecb83d83f6361cf5d9d0316864378940920e6cfd32ab9a4c80ea474bef5e34"
},
"cargo/selection-lint-2/stderr.log": {
"bytes": 134,
"sha256": "aa99195a7582f495f50056e1db02db3e44240bdb140c98326308259eb0eca959"
},
"cargo/selection-lint-2/stdout.log": {
"bytes": 269431,
"sha256": "172cb2a751dbce271060ee14b6ec5c1245e417bb209a03e4e7569c56526ae9f9"
},
"cargo/selection-menu-1/process.json": {
"bytes": 203,
"sha256": "6039ae1765c928d56b53376ae5853a739e5c96bee5dd1b81f2e1f21ce19c7aba"
},
"cargo/selection-menu-1/result.json": {
"bytes": 3317,
"sha256": "07a2826b348bfc1363f75dd3cf2ec02bf9a197ca435e0e110e263513d58e5b7a"
},
"cargo/selection-menu-1/source-proof.json": {
"bytes": 595,
"sha256": "978da6f95bf74ce0fa7ac871ec582a7312ca87576d7f39f4b5ebec71fa96e9fd"
},
"cargo/selection-menu-1/started.json": {
"bytes": 1592,
"sha256": "f5624c379b683f850c43612586122a3f40d4328e4cd5b0e5c577c3bc75108736"
},
"cargo/selection-menu-1/stderr.log": {
"bytes": 210,
"sha256": "e5728659305abd888a0fef144908828ff52f0d748a3baacfdb7cb2d86192e078"
},
"cargo/selection-menu-1/stdout.log": {
"bytes": 294645,
"sha256": "e9f845ead540fa8160e85a03bdc805f814d3c7a9dc7beb9c5a1f3978065c4c94"
},
"cargo/selection-menu-2/process.json": {
"bytes": 203,
"sha256": "06a711647a26d589e6c30a13e83be0ac6f51a234cc16e7595df85f805411e324"
},
"cargo/selection-menu-2/result.json": {
"bytes": 3315,
"sha256": "779fde518b19d5f85a33158e6629b0c0a08b6f1520fde350043d9d237518dca5"
},
"cargo/selection-menu-2/source-proof.json": {
"bytes": 595,
"sha256": "3f8b03bc437717f8b5f185483e83dac4eca29dc9f344a31c7ee30fd9a32ba807"
},
"cargo/selection-menu-2/started.json": {
"bytes": 1592,
"sha256": "8d037d2319a470520f9d7755f467d865bcf9b7647956672e06b264512a474f21"
},
"cargo/selection-menu-2/stderr.log": {
"bytes": 210,
"sha256": "ecb7980dd03e7f4ba9c56cf9e199abd6031a88e36e236b1ac99315dc59e19731"
},
"cargo/selection-menu-2/stdout.log": {
"bytes": 294645,
"sha256": "4ffba3ef252a554a08ab8a6e0920e6f87e580d6548152a708743464f0e57c9fe"
},
"cargo/selection-scenario-1/process.json": {
"bytes": 201,
"sha256": "bff973999bfea9c00def564106659d0b5bbd05f5387818c611edae98f910ab18"
},
"cargo/selection-scenario-1/result.json": {
"bytes": 3317,
"sha256": "ca91df9036f49c43fcfe47cd0515eb6581401630c51d43eec7c0afdea088c4c6"
},
"cargo/selection-scenario-1/source-proof.json": {
"bytes": 599,
"sha256": "6dd4a2e751a46252063e0cd61fb1a23dbd6aa0639519e19a0e7e2f73b781714d"
},
"cargo/selection-scenario-1/started.json": {
"bytes": 1590,
"sha256": "a8bc241061e0eb80078eeef41720ef9b4888904b53068ac6429230b1b248fa51"
},
"cargo/selection-scenario-1/stderr.log": {
"bytes": 355,
"sha256": "bc821b72e52604d99572f3fcbe63dd7dd9a59c93bfcb011121a729501c161e4e"
},
"cargo/selection-scenario-1/stdout.log": {
"bytes": 326826,
"sha256": "467ba77948bdbfc429be7416955a8864dc660a1c9754d20ec39c0f7d60dd435d"
},
"cargo/selection-scenario-2/process.json": {
"bytes": 201,
"sha256": "3132573dd21a97b6b78e06147a8cbb1d6999028d9c60ae932442e3bb462fc7ba"
},
"cargo/selection-scenario-2/result.json": {
"bytes": 3317,
"sha256": "03b0d2b7039699e348d41c3969e7ff575e37c8d118dcc53867a266edb9d76f0e"
},
"cargo/selection-scenario-2/source-proof.json": {
"bytes": 599,
"sha256": "164bc88b11dda75d38d4b61f44c8a03089df73dbbf031905bc2cccbf0997ad61"
},
"cargo/selection-scenario-2/started.json": {
"bytes": 1590,
"sha256": "d297110e2995f50d5540a452f6ab652fba5416459bbb5200605d4d080ec0500c"
},
"cargo/selection-scenario-2/stderr.log": {
"bytes": 355,
"sha256": "08f0e397bd8873bcce61d9df0d66cba853f00a029ea59ac6748a4ebfcbf8f8bf"
},
"cargo/selection-scenario-2/stdout.log": {
"bytes": 328272,
"sha256": "92660586178ff00a0c7bb1c5257d9296b206fe31525e40b6a7909f676868eee8"
},
"cargo/selection-scenario-4/process.json": {
"bytes": 201,
"sha256": "1e16c0737c62b69cca9634dff1449b80d5c04906c35c3cdd00990e6f1608aa23"
},
"cargo/selection-scenario-4/result.json": {
"bytes": 3309,
"sha256": "10a7257534036979f89bea0c176ca81f90e9a0c3d9b6b36b85da79df415d7f03"
},
"cargo/selection-scenario-4/source-proof.json": {
"bytes": 599,
"sha256": "a524249d9fa4ba6b29086e103780c03e345850b3cd390d92ec924ac6e5899b40"
},
"cargo/selection-scenario-4/started.json": {
"bytes": 1590,
"sha256": "89d8089a688d86209de08dbf6c3aaba90493fde7d78542a695e0fc877db10af2"
},
"cargo/selection-scenario-4/stderr.log": {
"bytes": 284,
"sha256": "a7a607c88c6c19b1fc31ed42e51f85b8873fcaaaf3773d540118078ec538c4d0"
},
"cargo/selection-scenario-4/stdout.log": {
"bytes": 326707,
"sha256": "6f9080bbaa82b66722d1d1d5b1dd0541560fbe1a5e344cf601532c50b233246f"
},
"cargo/selection-scenario-5/process.json": {
"bytes": 201,
"sha256": "a8985e970ba4065afc687fbf537e793ab27b8ed07b79f60314690d24a434ae9f"
},
"cargo/selection-scenario-5/result.json": {
"bytes": 3316,
"sha256": "e9230b6d27237ed459ba45eb3e16f24d08501945e42d15b3dd275ac13713f80e"
},
"cargo/selection-scenario-5/source-proof.json": {
"bytes": 599,
"sha256": "5382c0455497058de4dbac8c263c1a23976bdef04d5ad165882d47c4420582fd"
},
"cargo/selection-scenario-5/started.json": {
"bytes": 1590,
"sha256": "b3a5f65569da96c54d31ab57ca13496ed475aae5b486a48731c733ffc72de949"
},
"cargo/selection-scenario-5/stderr.log": {
"bytes": 284,
"sha256": "f81b8cb9f2232b63f1b60c9d7b753c62fc3aa677b1cd44ccf960b9173fee1356"
},
"cargo/selection-scenario-5/stdout.log": {
"bytes": 326707,
"sha256": "60ca908e049dc967c90fed940babbdf1d36701149a32299a0b80a2d239af0422"
},
"comparators/7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a.py": {
"bytes": 10094,
"sha256": "7805832d5f563ab2d7a50dd3de700792b5d6f459c68f7b1d8bcfcc9800fa0c3a"
},
"helpers/3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2.py": {
"bytes": 3778,
"sha256": "3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2"
},
"helpers/c86f51d9b8465341dab73d045923ec306b15ebabf7c97b81beac7afe73bd72a1.py": {
"bytes": 1396,
"sha256": "c86f51d9b8465341dab73d045923ec306b15ebabf7c97b81beac7afe73bd72a1"
}
}
}
@@ -1,110 +0,0 @@
"""Package explicitly selected closed gate captures without running Cargo."""
from pathlib import Path
import argparse
import json
import zipfile
from verify import RAW_FILES, pin, parsed, need, run_name, verify_data, verify_package
HERE = Path(__file__).resolve().parent
def collect(root, runs, helpers, comparators=()):
gates = root / ".local/scenario-group-gates"
candidates = [gates / name for name in ("gate.py", "source-proof.py")] + helpers
helper_bytes = [path.read_bytes() for path in candidates]
comparator_paths = [root / ".local/process-source-evidence-tools/verify.py", *comparators]
comparator_bytes = [path.read_bytes() for path in comparator_paths if path.is_file()]
payload, logical, specs = {}, {}, {}
def add(name, data):
stored = ("capsules/" + pin(data)["sha256"] + Path(name).suffix
if name.rsplit("/", 1)[1] in {"inputs.json", "inputs.zip"} else name)
need(stored not in payload or payload[stored] == data, "deduplication collision")
payload[stored] = data
logical[name] = stored
helper_catalog = {}
for name, data in zip(("gate.py", "source-proof.py"), helper_bytes):
helper = "helpers/" + pin(data)["sha256"] + ".py"
add(helper, data)
helper_catalog[name] = helper
for run in runs:
folder = gates / run
need(folder.is_dir(), "run directory missing: " + run)
paths = list(folder.iterdir())
names = {path.name for path in paths}
need(all(path.is_file() and not path.is_symlink() for path in paths)
and names in (RAW_FILES, RAW_FILES | {"source-proof.json"}), "exact closed raw files required: " + run)
raw = {path.name: path.read_bytes() for path in paths}
result = parsed(raw["result.json"])
need("finish" in result and result["inputsUnchanged"] is True, "unfinished/changed run refused: " + run)
matches = [data for data in helper_bytes if pin(data) == result["helper"]]
need(bool(matches), "exact historical helper unavailable for " + run + "; supply --helper PATH")
helper = "helpers/" + result["helper"]["sha256"] + ".py"
add(helper, matches[0])
for name, data in sorted(raw.items()):
add("cargo/" + run + "/" + name, data)
specs[run] = {key: result[key] for key in ("command", "head", "exitCode", "inputCount")}
specs[run]["helper"] = helper
if "source-proof.json" in raw:
proof = parsed(raw["source-proof.json"])
matches = [data for data in comparator_bytes if pin(data) == proof["comparator"]]
need(bool(matches), "exact source comparator unavailable for " + run + "; supply --comparator PATH")
comparator = "comparators/" + proof["comparator"]["sha256"] + ".py"
add(comparator, matches[0])
specs[run]["sourceProof"] = {"commit": proof["commit"], "pin": pin(raw["source-proof.json"]), "comparator": comparator}
manifest = {"schema": 1, "kind": "scenario-selection-cargo-gates", "runs": specs,
"captureHelpers": helper_catalog,
"logicalFiles": dict(sorted(logical.items())),
"members": {name: pin(data) for name, data in sorted(payload.items())}}
summary = verify_data(manifest, payload.__getitem__)
return payload, manifest, summary
def write_json(path, value):
with path.open("xb") as output:
output.write((json.dumps(value, indent=2) + "\n").encode("utf-8"))
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, required=True, help="checkout holding .local/scenario-group-gates")
parser.add_argument("--runs", nargs="+", required=True, help="explicit run names, including failures to retain")
parser.add_argument("--helper", type=Path, action="append", default=[], help="additional exact historical helper (repeatable)")
parser.add_argument("--comparator", type=Path, action="append", default=[], help="additional exact historical source comparator (repeatable)")
parser.add_argument("--output", type=Path, help="fresh, nonexistent package directory (required unless --check-only)")
parser.add_argument("--check-only", action="store_true", help="validate raw captures in memory without creating an archive")
args = parser.parse_args()
need(all(run_name(run) for run in args.runs) and len(args.runs) == len(set(args.runs)), "unique safe run names required")
need(args.check_only or args.output is not None, "--output is required unless --check-only")
if not args.check_only:
need(not args.output.exists(), "output must be a fresh directory: " + str(args.output))
payload, manifest, summary = collect(args.root.resolve(), args.runs, args.helper, args.comparator)
if args.check_only:
print("PASS raw consistency; no package written:", summary)
return
out = args.output.resolve()
out.mkdir(parents=True, exist_ok=False)
for name in ("evidence.zip", "manifest.json", "files.json"):
need(not (out / name).exists(), "existing package output refused: " + name)
for name in ("prepare.py", "verify.py", "README.md"):
data = (HERE / name).read_bytes()
destination = out / name
if destination.exists():
need(destination.read_bytes() == data, "different existing package tool: " + name)
else:
with destination.open("xb") as output:
output.write(data)
with zipfile.ZipFile(out / "evidence.zip", "x", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive:
for name, data in sorted(payload.items()):
info = zipfile.ZipInfo(name, date_time=(2026, 9, 23, 0, 0, 0))
info.compress_type = zipfile.ZIP_DEFLATED
info.external_attr = 0o100644 << 16
archive.writestr(info, data)
write_json(out / "manifest.json", manifest)
names = ("prepare.py", "verify.py", "README.md", "manifest.json", "evidence.zip")
write_json(out / "files.json", {name: pin((out / name).read_bytes()) for name in names})
need(verify_package(out) == summary, "written package differs from validated captures")
print("PASS package readback:", out, summary)
if __name__ == "__main__":
main()
@@ -1,194 +0,0 @@
"""Offline consistency checks only; never execute or extract archived inputs."""
from pathlib import Path, PurePosixPath
from datetime import datetime
import argparse
import hashlib
import io
import json
import math
import re
import stat
import zipfile
RAW_FILES = {"started.json", "process.json", "result.json", "stdout.log", "stderr.log", "inputs.json", "inputs.zip"}
OUTER_FILES = {"prepare.py", "verify.py", "README.md", "manifest.json", "evidence.zip"}
def need(condition, message):
if not condition:
raise ValueError(message)
def pin(data):
return {"bytes": len(data), "sha256": hashlib.sha256(data).hexdigest()}
def valid_pin(value):
return (isinstance(value, dict) and set(value) == {"bytes", "sha256"}
and type(value["bytes"]) is int and value["bytes"] >= 0
and isinstance(value["sha256"], str)
and re.fullmatch(r"[0-9a-f]{64}", value["sha256"]) is not None)
def safe(name):
return (isinstance(name, str) and bool(name) and "\\" not in name and ":" not in name
and all(ord(char) >= 32 for char in name)
and not PurePosixPath(name).is_absolute()
and all(part not in ("", ".", "..") for part in name.split("/")))
def run_name(name):
return isinstance(name, str) and re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]*", name) is not None
def parsed(data):
def pairs(items):
result = {}
for key, value in items:
need(key not in result, "duplicate JSON key: " + key)
result[key] = value
return result
def constant(value):
raise ValueError("nonfinite JSON constant: " + value)
return json.loads(data, object_pairs_hook=pairs, parse_constant=constant)
def inventory(archive, expected):
names = archive.namelist()
need(len(names) == len(set(names)) and set(names) == set(expected), "exact ZIP membership")
for entry in archive.infolist():
need(safe(entry.filename) and not entry.is_dir(), "unsafe ZIP member: " + entry.filename)
need(not stat.S_ISLNK(entry.external_attr >> 16), "ZIP symlink: " + entry.filename)
need(valid_pin(expected[entry.filename]), "invalid member pin: " + entry.filename)
need(entry.file_size == expected[entry.filename]["bytes"], "ZIP recorded size: " + entry.filename)
def ordered_times(*values):
times = [datetime.fromisoformat(value) for value in values]
return all(value.utcoffset() is not None for value in times) and times == sorted(times)
def verify_data(manifest, read):
"""Shared read-only verifier for packaged bytes or prepare's in-memory payload."""
need(manifest["schema"] == 1 and manifest["kind"] == "scenario-selection-cargo-gates", "manifest schema")
runs, logical, stored = manifest["runs"], manifest["logicalFiles"], manifest["members"]
helpers = manifest["captureHelpers"]
need(isinstance(helpers, dict) and set(helpers) == {"gate.py", "source-proof.py"}, "capture helper names")
need(all(isinstance(name, str) and re.fullmatch(r"helpers/[0-9a-f]{64}\.py", name)
for name in helpers.values()), "capture helper mapping")
need(isinstance(runs, dict) and bool(runs) and all(run_name(run) for run in runs), "explicit run inventory")
need(all(safe(name) for name in logical) and all(safe(name) for name in stored), "unsafe manifest path")
need(set(logical.values()) == set(stored), "unreferenced or absent stored members")
for name, expected in stored.items():
need(valid_pin(expected) and pin(read(name)) == expected, "stored member hash: " + name)
expected_logical = {"cargo/" + run + "/" + name for run in runs for name in RAW_FILES}
expected_logical.update(spec["helper"] for spec in runs.values())
expected_logical.update(helpers.values())
for run, spec in runs.items():
if "sourceProof" in spec:
proof_spec = spec["sourceProof"]
need(set(proof_spec) == {"commit", "pin", "comparator"}, run + ": exact source proof spec")
need(isinstance(proof_spec["commit"], str) and re.fullmatch(r"[0-9a-f]{40}", proof_spec["commit"]), run + ": named full source commit")
need(valid_pin(proof_spec["pin"]), run + ": source proof pin")
need(isinstance(proof_spec["comparator"], str) and re.fullmatch(r"comparators/[0-9a-f]{64}\.py", proof_spec["comparator"]), run + ": comparator path")
expected_logical.update({"cargo/" + run + "/source-proof.json", proof_spec["comparator"]})
need(set(logical) == expected_logical, "exact logical raw/helper inventory")
for name, destination in logical.items():
if name.startswith("cargo/") and name.rsplit("/", 1)[1] in {"inputs.json", "inputs.zip"}:
expected = "capsules/" + stored[destination]["sha256"] + PurePosixPath(name).suffix
need(destination == expected, "input capsule mapping: " + name)
else:
need(destination == name, "only input maps/archives may be deduplicated: " + name)
def raw(name):
return read(logical[name])
for name, helper in helpers.items():
need(helper == "helpers/" + pin(raw(helper))["sha256"] + ".py", "named helper hash: " + name)
seen_capsules = set()
for run, spec in runs.items():
prefix = "cargo/" + run + "/"
start = parsed(raw(prefix + "started.json"))
process = parsed(raw(prefix + "process.json"))
result = parsed(raw(prefix + "result.json"))
required_start = {"start", "command", "cwd", "head", "scope", "inputs", "archive",
"inputCount", "environment", "helper", "cargo", "rustc"}
need(required_start <= start.keys(), run + ": incomplete start receipt")
need(all(result.get(key) == value for key, value in start.items()), run + ": start/result relationship")
command = result["command"]
need(isinstance(command, list) and command and command[0] == "cargo"
and all(isinstance(arg, str) for arg in command), run + ": Cargo command")
need(command == process["command"] == spec["command"], run + ": command relationship")
need(result["head"] == spec["head"] and re.fullmatch(r"(?:[0-9a-f]{40}|[0-9a-f]{64})", result["head"]), run + ": HEAD")
need(type(result["exitCode"]) is int and result["exitCode"] == spec["exitCode"], run + ": recorded exit")
need(result["inputsUnchanged"] is True, run + ": changed inputs refused")
need(type(result["seconds"]) in (int, float) and math.isfinite(result["seconds"])
and result["seconds"] >= 0, run + ": duration")
need(type(process["pid"]) is int and process["pid"] > 0, run + ": process identity")
need(ordered_times(start["start"], process["observed"], result["finish"]), run + ": completed timestamp order")
for field, filename in [("inputs", "inputs.json"), ("archive", "inputs.zip"), ("stdout", "stdout.log"), ("stderr", "stderr.log")]:
need(result[field] == pin(raw(prefix + filename)), run + ": " + field + " pin")
need(valid_pin(result["helper"]) and result["helper"] == pin(raw(spec["helper"])), run + ": helper pin")
need(spec["helper"] == "helpers/" + result["helper"]["sha256"] + ".py", run + ": helper mapping")
inputs = parsed(raw(prefix + "inputs.json"))
need(isinstance(inputs, dict) and all(safe(name) for name in inputs), run + ": safe input inventory")
need(type(result["inputCount"]) is int and len(inputs) == result["inputCount"] == spec["inputCount"], run + ": input counts")
if "sourceProof" in spec:
proof_spec = spec["sourceProof"]
proof_bytes = raw(prefix + "source-proof.json")
proof = parsed(proof_bytes)
need(pin(proof_bytes) == proof_spec["pin"], run + ": source proof pinned bytes")
need(proof["run"] == run and proof["commit"] == proof_spec["commit"], run + ": proof run/commit identity")
need(proof["status"] == "PASS exact captured input inventory and Git blob bytes", run + ": source proof status")
need(proof["inputCount"] == len(inputs) and proof["inputMap"] == result["inputs"], run + ": proof input relationship")
need(proof["result"] == pin(raw(prefix + "result.json")), run + ": proof result relationship")
need(ordered_times(result["finish"], proof["verified"]), run + ": proof time after closed gate")
comparator = proof_spec["comparator"]
need(valid_pin(proof["comparator"]) and pin(raw(comparator)) == proof["comparator"], run + ": comparator bytes")
need(comparator == "comparators/" + proof["comparator"]["sha256"] + ".py", run + ": comparator identity")
need(sum(name.startswith("crates/vendor/iced_widget/") for name in inputs) == 71, run + ": vendor count")
capsule = (logical[prefix + "inputs.json"], logical[prefix + "inputs.zip"])
if capsule not in seen_capsules:
with zipfile.ZipFile(io.BytesIO(raw(prefix + "inputs.zip"))) as source:
inventory(source, inputs)
for name, expected in inputs.items():
need(pin(source.read(name)) == expected, run + ": source hash " + name)
seen_capsules.add(capsule)
artifacts = []
for line in raw(prefix + "stdout.log").decode("utf-8", errors="replace").splitlines():
try:
value = json.loads(line)
except json.JSONDecodeError:
continue
if isinstance(value, dict) and value.get("reason") == "compiler-artifact" and value.get("executable"):
artifacts.append(value)
need(isinstance(result["artifacts"], list), run + ": artifact records")
need(artifacts == [record["cargo"] for record in result["artifacts"]], run + ": Cargo JSON artifact relationship")
for record in result["artifacts"]:
need(set(record) == {"cargo", "readback"}
and (record["readback"] is None or valid_pin(record["readback"])), run + ": artifact readback pin")
print(run, "recorded exit", result["exitCode"], "inputs", len(inputs), "executable records", len(artifacts))
return {"runs": len(runs), "recordedFailures": sum(spec["exitCode"] != 0 for spec in runs.values()),
"logicalFiles": len(logical), "storedMembers": len(stored), "sourceCapsulePairs": len(seen_capsules),
"captureHelpers": helpers, "sourceProofs": sum("sourceProof" in spec for spec in runs.values())}
def verify_package(root):
files = parsed((root / "files.json").read_bytes())
need(set(files) == OUTER_FILES, "outer file inventory")
for name, expected in files.items():
need(valid_pin(expected) and pin((root / name).read_bytes()) == expected, "outer hash: " + name)
manifest = parsed((root / "manifest.json").read_bytes())
with zipfile.ZipFile(root / "evidence.zip") as archive:
inventory(archive, manifest["members"])
return verify_data(manifest, archive.read)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("package", nargs="?", type=Path, default=Path(__file__).resolve().parent)
args = parser.parse_args()
print("PASS offline consistency:", verify_package(args.package))
print("Historical receipts only; no source/binary execution or native acceptance claim.")
if __name__ == "__main__":
main()
@@ -1,43 +0,0 @@
{
"status": "INVALID interrupted source-change attempt; no verification claim",
"run": "selection-scenario-3",
"reason": "Cargo started while the final formatter/commit command was still running. app.rs bytes changed after capture. Parent deliberately terminated the observed Cargo process tree; no executable/test result was emitted. Raw HEAD identifies only the starting checkout, not the captured or compiled working tree.",
"archive": {
"bytes": 3192626,
"sha256": "9afef85b7e395d811bf99cd3a49909da881403cac3bcfcaea9fdc701f4ee5180"
},
"members": {
"gate.py": {
"bytes": 3778,
"sha256": "3e4cd38fd3807a4010a5a7102354f9139cab236ca55163c96090e416161e5fd2"
},
"inputs.json": {
"bytes": 135839,
"sha256": "88df77614a1030033090cc10dac0ab3959dbcfc81a12317150840b2ea1c86f8f"
},
"inputs.zip": {
"bytes": 3234362,
"sha256": "856f066c247803006f410b2f9066358c4b257239b3b8838b8291778eb8d49ecd"
},
"process.json": {
"bytes": 201,
"sha256": "5eb0b49241515d2712aa6c69d17f21fc0a78402aa4a92700196b2b1bb1803ddf"
},
"result.json": {
"bytes": 1985,
"sha256": "511abf94627b13757771fb3488895a7a4a402b229cfa4bdeb02dc4558236aa7e"
},
"started.json": {
"bytes": 1590,
"sha256": "978ad4d80edf791be33362305116327018fce2dec6fa3b83919ce0b3ed7e71b5"
},
"stderr.log": {
"bytes": 73,
"sha256": "9019f4d830dc881adca029fed3b2f22cfd586c4634eca2cbb47350ca66d1aa34"
},
"stdout.log": {
"bytes": 292222,
"sha256": "9c1457380f81b5651d645548c94f720314eea8fd3cceebfd840d9c1f2abc1f67"
}
}
}
@@ -1,29 +0,0 @@
"""Preserve an interrupted changed-source attempt; never produce acceptance proof."""
from pathlib import Path
import hashlib, json, zipfile
root = Path('C:/ar-scenario-9e8r6t')
run = root/'.local/scenario-group-gates/selection-scenario-3'
out = root/'harness/scenario-selection-evidence-20260924/invalid-attempt-3'
result = json.loads((run/'result.json').read_text())
assert result['exitCode'] == 1 and result['inputsUnchanged'] is False
assert result['artifacts'] == [] and 'finish' in result
assert not (run/'source-proof.json').exists()
def pin(data): return {'bytes':len(data), 'sha256':hashlib.sha256(data).hexdigest()}
payload = {p.name:p.read_bytes() for p in run.iterdir() if p.is_file()}
assert set(payload) == {'started.json','process.json','result.json','stdout.log','stderr.log','inputs.json','inputs.zip'}
payload['gate.py'] = (run.parent/'gate.py').read_bytes()
assert pin(payload['gate.py']) == result['helper']
out.mkdir(parents=True, exist_ok=False)
with zipfile.ZipFile(out/'raw.zip','x',zipfile.ZIP_DEFLATED) as archive:
for name, data in sorted(payload.items()): archive.writestr(name,data)
with zipfile.ZipFile(out/'raw.zip') as archive:
assert set(archive.namelist()) == set(payload)
assert all(archive.read(name) == data for name,data in payload.items())
manifest = {'status':'INVALID interrupted source-change attempt; no verification claim',
'run':'selection-scenario-3',
'reason':'Cargo started while the final formatter/commit command was still running. app.rs bytes changed after capture. Parent deliberately terminated the observed Cargo process tree; no executable/test result was emitted. Raw HEAD identifies only the starting checkout, not the captured or compiled working tree.',
'archive':pin((out/'raw.zip').read_bytes()), 'members':{n:pin(d) for n,d in sorted(payload.items())}}
with (out/'manifest.json').open('x',encoding='utf-8',newline='\n') as stream:
json.dump(manifest,stream,indent=2);stream.write('\n')
print(json.dumps(manifest,indent=2))
@@ -1,28 +0,0 @@
# Native Windows selection exercise
Source `ed55b1781d86ec49af5d2588cbd06f7b878d9009`, closed build `selection-build-1`. The launcher checked that receipt, source proof, binary and tracked fixture before copying the executable and opening an isolated fixture through the app. The archive omits the executable; its identity is recorded in `started.json` and the build receipt. This is branch-specific verification, not the #37 candidate or a release package.
Run `python -B verify.py` with Python 3.11+ to verify the archive and exact file comparisons. The verifier does not execute archived helpers or establish visual assertions. `native.zip` contains original screenshot/metadata bytes, launch/close records, empty app logs, fixture snapshots and the original launch/capture helpers. `prepare.py` requires the retained local run and refuses an existing output directory.
## Observations
All numbered files below are under `captures/` inside the archive. Native inputs used the Computer Use skill's `@oai/sky` API.
- 17: selected saved A in a maximized window. 18–21: contextual Add creates a two-occurrence selection; count, both strokes and Properties primary agree.
- 22–23: group menu exposes selection commands and disabled movement/removal. Inspect first occurrence preserves both members and switches primary.
- 24–26: dragging a selected body and pressing Delete refuse the whole group, retain both members and display the unfinished-operation feedback.
- 27: a single blank drag from (1110,730) to (1060,700) clears the group and moves the visible geometry by (-50,-30). No second press was needed.
- 28–31: contextual Add restores the two-member group after panning. 32: a stationary plain click on the first member collapses to that singleton.
- 33: Save all after selection and pan preserves the entire original 6,206-byte file, SHA256 `74cd2c1dd5eed7eb19895a752bb8ff9ce93655f7638a5312c96f56218568ebb3`.
- 34–35: with the first occurrence selected, the first drag of the second body from (675,454) to (675,474) immediately moves it and selects it. Save all changes only saved A's `updated_report` `send_y`, from 252 to 281.8841247558594. The entire expected file is compared, including unchanged semantic order and other representations.
- 36–37: one Ctrl+Z and Save all restore exact baseline bytes.
- 38–44: A → B → Automatic → A displays each correct representation, retains the existing singleton primary, and final Save all preserves baseline. This is not a native group-clearing-on-navigation claim.
- 46: app remains usable after an unsuccessful title-bar close attempt. Alt+F4 then closes normally with exit 0; binary and fixture unchanged; stdout and stderr empty.
## Limits and automation anomalies
Native Shift-click was not exercised because the API cannot hold Shift across a click. Actual modifier/focus/busy/window-loss sequences are covered by the mounted widget tests in the separate Cargo evidence. Native Redo, dirty Properties admission, stale queued events, group navigation and fallback C were not independently exercised here; test coverage must not be described as native coverage.
Early nonmaximized picker targeting selected B while intending A. Maximizing and using the full-window screenshot resolved targeting; no product picker defect is established. One action selected a 16×16 icon screenshot and was rejected for out-of-bounds coordinates before any app action. Early setup observations remain local; accepted captures start at 17. Later indexed title-bar Close minimized the window instead; restoration was observed, then Alt+F4 succeeded. These are automation limitations, not evidence of a product crash. Capture labels record intent; claims use observed state. Capture 45 produced no file because the API reported a minimized window.
The fixture intentionally contains five model checks and an incomplete occurrence. No new checks appeared. Group semantic movement and aggregate removal remain unfinished. Screenshots and successful launch do not imply full usability or release acceptance.

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